利用Pytorch重写《任务一》,实现基于CNN、RNN的文本分类。
-
参考
- https://pytorch.org/
- Convolutional Neural Networks for Sentence Classification https://arxiv.org/abs/1408.5882
- https://machinelearningmastery.com/sequence-classification-lstm-recurrent-neural-networks-python-keras/
-
word embedding 的方式初始化
-
随机embedding的初始化方式
-
用glove 预训练的embedding进行初始化 https://nlp.stanford.edu/projects/glove/
-
知识点:
- CNN/RNN的特征抽取
- 词嵌入
- Dropout
-
时间:两周
词嵌入模型:随机初始化、Glove embedding。
网络构建:TextCNN、LSTM、BiLstm。
框架:pytorch,sklearn
对于NLP任务,我们利用torchtext对文本数据进行封装。核心是四步
- 利用Field对分词方法,序列化,是否转成小写,起始字符,结束字符,补全字符以及词典等进行定义。在本次实验采用的是jieba分词。
def tokenizer(text):
return [wd for wd in jieba.cut(text, cut_all=False)]
en_stopwords=stopwords.words('english')
LABEL = data.Field(sequential=False, use_vocab=False)
TEXT = data.Field(sequential=True, tokenize=tokenizer, lower=True, stop_words=en_stopwords)
在后面使用的两个模型中,LSTM模型是可以处理可变长的文本的,但是TextCNN只能处理定长文本。因此如果是TextCNN需要加上fix_length
参数,限定文本的长度。如果文本太短则补全,如果文本太长则截断。
- 利用TabularDataset 进行数据加载。
train, val = data.TabularDataset.splits(
path='./data', train='train.csv', validation='val.csv', format='csv', skip_header=True,
fields=[('PhraseId', None), ('SentenceId', None), ('Phrase', TEXT), ('Sentiment', LABEL)]
)
test = data.TabularDataset('./data/test.tsv', format='tsv', skip_header=True,
fields=[('PhraseId', None), ('SentenceId', None), ('Phrase', TEXT)])
- 构建词表。因为后续可能会用到word embedding ,因此提前先构建好词表。后续如果使用的是glove的embedding我们就需要将其加载到模型中。
# 建立vocab(加载预训练的词向量,如果路径没有该词向量,会自动下载)
TEXT.build_vocab(train, vectors='glove.6B.100d')#, max_size=30000)
# 当 corpus 中有的 token 在 vectors 中不存在时 的初始化方式.
TEXT.vocab.vectors.unk_init = init.xavier_uniform
- 构造迭代器,确定batch_size。
# 构造迭代器
'''
sort_key指在一个batch内根据文本长度进行排序。
'''
train_iter = data.BucketIterator(train, batch_size=128, sort_key=lambda x: len(x.Phrase),
shuffle=True,device=DEVICE)
val_iter = data.BucketIterator(val, batch_size=128, sort_key=lambda x: len(x.Phrase),
shuffle=True,device=DEVICE)
# 在 test_iter , sort一定要设置成 False, 要不然会被 torchtext 搞乱样本顺序
test_iter = data.Iterator(dataset=test, batch_size=128, train=False,
sort=False, device=DEVICE)
对lstm的理解可以参考另外一篇博客《lstm 从理论到实践》。最基本的模型构建上使用的是单向的双层LSTM,首先对数据进行embedding,进过lstm,序列的最后一个输出作为特征,接入线性层并做softmax。如下是我在实验中构建的lstm。
class LSTM_base(nn.Module):
#定义模型中使用的所有层
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers,
bidirectional, dropout):
#构造函数
super().__init__()
#embeddding层
self.embedding = nn.Embedding(vocab_size, embedding_dim)
#lstm层
self.lstm = nn.LSTM(embedding_dim,
hidden_dim,
num_layers=n_layers,
bidirectional=bidirectional,
dropout=dropout,
batch_first=True)
self.fc = nn.Linear(hidden_dim , output_dim)
def forward(self, text):
#text = [batch size,sent_length]
embedded = self.embedding(text)
out,_=self.lstm(embedded)
out=self.fc(out[:,-1,:])
#最终激活函数
out = F.softmax(out,-1)
return out
其中对于output的取值,我们需要取的是lstm的最后一维,但是因为有batch_first的缘故,所以我们是取out的第二维的最后一个数。但是在上述LSTM的实验过程中,发现效果并不良好,有大神告诉了我max-pool的双层结构。如下是这种结构的描述:
class LSTM_MAX(nn.Module):
#定义模型中使用的所有层
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers,
bidirectional, dropout):
#构造函数
super().__init__()
#embeddding层
self.embedding = nn.Embedding(vocab_size, embedding_dim)
#lstm层
self.lstm = nn.LSTM(embedding_dim,
hidden_dim,
num_layers=1,
dropout=dropout,
batch_first=True)
self.fc = nn.Linear(hidden_dim*2 , output_dim)
def forward(self, text):
#text = [batch size,sent_length]
h_embedding = self.embedding(text)
h_lstm1, _ = self.lstm(h_embedding)
h_lstm2, _ = self.lstm(h_lstm1)
# global average pooling
avg_pool = torch.mean(h_lstm2, 1)
# global max pooling
max_pool, _ = torch.max(h_lstm2, 1)
h_conc = torch.cat((max_pool, avg_pool), 1)
out=self.fc(h_conc)
#最终激活函数
out = F.softmax(out,-1)
return out
在实际测试时发现效果仍然是一般。
参数设置与模型的实例化:
#定义超参数
size_of_vocab = len(TEXT.vocab)
embedding_dim = 100
num_hidden_nodes = 100
num_output_nodes = 5
num_layers = 2
bidirection = False
dropout = 0.4
#实例化模型
model = LSTM_base(size_of_vocab, embedding_dim, num_hidden_nodes,num_output_nodes, num_layers,
bidirectional = bidirection, dropout = dropout)
# model = LSTM_MAX(size_of_vocab, embedding_dim, num_hidden_nodes,num_output_nodes, num_layers,
# bidirectional = bidirection, dropout = dropout)
#模型框架
print(model)
#可训练参数的数量
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'The model has {count_parameters(model):,} trainable parameters')
#初始化预训练的词嵌入
pretrained_embeddings = TEXT.vocab.vectors
model.embedding.weight.data.copy_(pretrained_embeddings)
print(pretrained_embeddings.shape)
#定义优化器和损失
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss()
#定义度量指标
def binary_accuracy(preds, y):
#round预测到最接近的整数
# rounded_preds = torch.round(preds)
correct = (preds == y).float()
acc = correct.sum() / len(correct)
return acc
#转化为cuda(如果可用)
model = model.to(DEVICE)
criterion = criterion.to(DEVICE)
网络架构如下:
embedding_size = 60
sequence_length = FIX_LENGTH # every sentences contains sequence_length(=3) words
num_classes = 5 # 0 or 1
batch_size = 128
vocab_size = len(TEXT.vocab)
class TextCNN(nn.Module):
def __init__(self):
super(TextCNN, self).__init__()
self.W = nn.Embedding(vocab_size, embedding_size)
output_channel = 3
self.conv = nn.Sequential(
# conv : [input_channel(=1), output_channel, (filter_height, filter_width), stride=1]
nn.Conv2d(1, output_channel, (2, embedding_size)),
nn.ReLU(),
# pool : ((filter_height, filter_width))
nn.MaxPool2d((FIX_LENGTH-1, 1)),
)
# fc
self.fc = nn.Linear(output_channel, num_classes)
def forward(self, X):
'''
X: [batch_size, sequence_length]
'''
batch_size = X.shape[0]
# print(batch_size)
embedding_X = self.W(X) # [batch_size, sequence_length, embedding_size]
# print(embedding_X.shape)
embedding_X = embedding_X.unsqueeze(1) # add channel(=1) [batch, channel(=1), sequence_length, embedding_size]
# print(embedding_X.shape)
conved = self.conv(embedding_X) # [batch_size, output_channel, 1, 1]
# print(conved.shape)
flatten = conved.view(batch_size, -1) # [batch_size, output_channel*1*1]
# print(flatten.shape)
output = self.fc(flatten)
return output
def Train(model, iterator, optimizer, criterion):
#每个epoch进行初始化
epoch_loss = 0
epoch_acc = 0
#将模型设置为训练阶段
model.train()
predictions_val=[]
for batch in tqdm(iterator):
#重设梯度
optimizer.zero_grad()
#获取文本和单词数量
text = batch.Phrase
text = text.permute(1,0)
# print(text.shape)
#转换为一维张量
predictions = model(text).squeeze()
#计算loss
loss = criterion(predictions, batch.Sentiment)
#计算二分类准确度
predictions_val= predictions.argmax(dim=1)
acc = binary_accuracy(predictions_val, batch.Sentiment.float())
#后向传播损失并计算梯度
loss.backward()
#更新权重
optimizer.step()
#损失和准确度
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
def Evaluate(model, iterator, criterion):
#每个epoch进行初始化
epoch_loss = 0
epoch_acc = 0
#停用dropout层
model.eval()
#停用自动求导
with torch.no_grad():
for batch in tqdm(iterator):
#获取文本和单词数量
text = batch.Phrase
text = text.permute(1,0)
#转换为一维张量
predictions = model(text).squeeze()
#计算损失和准确度
loss = criterion(predictions, batch.Sentiment)
predictions_val= predictions.argmax(dim=1)
acc = binary_accuracy(predictions_val, batch.Sentiment.float())
#跟踪损失和准确度
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
def Test(model, iterator, criterion):
#每个epoch进行初始化
epoch_loss = 0
epoch_acc = 0
#停用dropout层
model.eval()
predict_list=[]
#停用自动求导
with torch.no_grad():
for batch in tqdm(iterator):
#获取文本和单词数量
text = batch.Phrase
text = text.permute(1,0)
#转换为一维张量
predictions = model(text).squeeze()
#计算损失和准确度
predict= predictions.argmax(dim=1).cpu()
predict_list+=predict.numpy().flatten().tolist()
return predict_list
N_EPOCHS = 50
best_valid_loss = float('inf')
train_loss_list=[]
valid_loss_list=[]
train_acc_list=[]
valid_acc_list=[]
for epoch in range(N_EPOCHS):
#训练模型
print('epoch:',epoch)
train_loss, train_acc = Train(model, train_iter, optimizer, criterion)
#评估模型
valid_loss, valid_acc = Evaluate(model, val_iter, criterion)
#保存模型
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'saved_weights.pt')
train_loss_list.append(train_loss)
valid_loss_list.append(valid_loss)
train_acc_list.append(train_acc)
valid_acc_list.append(valid_acc)
print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')
总共做了5组实验:
模型 | 词嵌入方法(维度) | 轮数 | Dropout | 编号 |
---|---|---|---|---|
TextCNN | random(60) | 50 | / | 1 |
LSTM | glove | 50 | 0.5 | 2 |
LSTM | glove | 50 | 0.8 | 3 |
LSTM | random | 50 | 0.4 | 4 |
MAX-LSTM | glove | 50 | 0.4 | 5 |
因为设备问题,所以并没有很好的控制变量,所进行的实验大多也仅针对实验本身分析。如下是实验结果:
TextCNN收敛是最慢的,但是收敛曲线非常平滑。
droup out取0.8的时候可以看到收敛曲线也比较平滑,但是训练集和测试集的acc存在着差异越来越大的问题,我认为是过拟合了,所以减小了droupout的值以增大droupout的比例。
droupout=0.5的时候,收敛曲线变得非常陡峭,但是acc的最终结果也没有很大的变化。
采用random的embedding方式,收敛曲线最为平坦,同样的收敛速度较慢,50轮仍然没有收敛完全。
采用MaxLSTM的方式进行实验,结果很遗憾,仍然没有所谓的更新。