Simple Sentiment Analysis
在第一篇教程中不关心实验结果好坏,只介绍基本概念,是读者对情感分析有初步了解。
使用PyTorch和TorchText构建模型用来检测一句话情感(检测句子是持1
肯定或0
否定态度)本文使用IMDB电影评论数据集。
1 - 介绍
RNN网络简单介绍
输入:一句话(单词序列)X={x1,x2,......xt}
该序列依次输入模型(一次输入一个)得到响应隐藏层输出H={h1,h2,......ht }
例:当前单词
x2
及上一个单词的隐藏层输出h1
同时输入RNN得到h2
(本文中h0
初始化为一个全零向量)
递推公式:ht = RNN( xt , ht-1 )
输出:得到ht
将其送入全连接层,得到所预测的情感y = f ( ht )
结果:下图所示,输入句子 I hate this film
经过模型得到输出0
,表明本句话持否定态度
2 - 数据预处理
在TorchText
中有一个重要的概念Field
,它了数据应该怎样被处理(如:分词等)pytorch
使用TEXT
处理评论字段,使用LABEL
处理情感标签(即"pos" 或 “neg”.)
torchtext
认为一个样本是由多个字段(文本字段,标签字段)组成,不同的字段可能会有不同的处理方式。(想要了解更多Field相关内容go here)
-
tokenize
='spacy'
完成了将一句话拆分为多个单词的分词操作(默认以空格拆分)
#设置分词函数
TEXT = data.Field(tokenize = 'spacy')
LABEL = data.LabelField(dtype = torch.float)
-
TorchText
中内含许多NLP通用数据集,本文是用的IMDB数据集包含50,000个电影评论,每个评论都标记为正面或负面评论。
#加载语料库,并划分训练集和测试集
from torchtext import datasets
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
- 可查看默认划分的测试集和训练集大小
print(f'训练集中样本数: {len(train_data)}') #25000
print(f'测试集中样本数: {len(test_data)}') #25000
#查看第一个样本的格式
print(vars(train_data[0]))
#vars() 函数返回对象的属性和属性值的字典对象
- 使用
.spilt()
划分验证集
#从训练集中划分出一部分作为验证集
import random
#为了使用相同的随即初始化数据,使用初始化种子
#1234是随即数字的位数
SEED =1234
#每次得到相同的随即数字
torch.manual_seed(SEED)
#保证实验的可重复性
torch.backends.cudnn.deterministic = True
# 在同一份数据集上,相同的种子产生相同的结果,不同的种子产生不同的划分结果,split默认7:3
train_data,valid_data = train_data.split(random_state = random.seed(SEED))
#random_state(int),int是随机数的种子
- 查看划分结果
#查验数据集划分情况
print(f'训练集中样本数: {len(train_data)}')#17500
print(f'验证集中样本数: {len(valid_data)}')#7500
print(f'测试集中样本数: {len(test_data)}') #25000
- 构建词表,采用one-hot向量,由于词向量过多所以设置只使用出现频率最高的25000个单词构建
#构建词汇表:给每一个单词对应为向量,词汇表中不包含的单词被显示为<unk>
#这个横杠只是为了便于分辨 没有实际意义
MAX_VOCAB_SIZE = 25_000
#只在训练集上构建词表,以保护测试集的隐秘
TEXT.build_vocab(train_data, max_size = MAX_VOCAB_SIZE)
LABEL.build_vocab(train_data)
#查看词表长度
print(f"TEXT vocabulary: {len(TEXT.vocab)}")
#TEXT.vocab为25002,因为包含<unk> <pad>填充,pad是为了保持句子长度的统一
print(f"LABEL vocabulary: {len(LABEL.vocab)}") # 2
#查看词表中出现频率较高的单词及其出现频率
print(TEXT.vocab.freqs.most_common(20))
#也可以用下面的方式只查看频率较高词汇
print(TEXT.vocab.itos[:10])
#可以查看标签所对应数字化向量
print(LABEL.vocab.stoi)
print(TEXT.vocab.stoi)
- 构建迭代器
#处理数据的最后一步是构建迭代器
BATCH_SIZE = 64
#选择是否使用GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#使用Buketlterator迭代器,他返回一个Batch,最大程度的减少了每个实例的填充量
train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size = BATCH_SIZE,
device = device)
3 - 构建模型
- Pytorch中
nn.Module.Embedding
使向量降维 - 句子的输入纬度为
[sent len, batch size]
在这里可能会产生困惑为什么不是
[sent len,词向量维度, batch size]
,因为 pytorch使用index value代表句子的词向量维度
import torch.nn as nn
class RNN(nn.Module):
def __init__(self, input_dim, embedding_dim, hidden_dim, output_dim):
super().__init__()
self.embedding = nn.Embedding(input_dim, embedding_dim)
self.rnn = nn.RNN(embedding_dim, hidden_dim)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, text):
# text = [sent len, batch size]
#首先输入的是one-hot向量,和词汇表大小一样
embedded = self.embedding(text)
#经过embedding层后变为稠密的向量
# embedded = [sent len, batch size, emb dim]
#隐藏层的维度取决于隐藏层的cell数目
output, hidden = self.rnn(embedded)
# output = [sent len, batch size, hid dim]
# hidden = [1, batch size, hid dim]
#squeeze()方法将第一列维度删去
assert torch.equal(output[-1, :, :], hidden.squeeze(0))
#返回最后一个隐藏层输出
return self.fc(hidden.squeeze(0))
4 - 训练模型
import torch.optim as optim
#创建优化器,采用梯度下降,学习率为e-3
#优化器就是需要根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值的作用
#第一个参数是可训练参数,第二个参数是学习率
optimizer = optim.SGD(model.parameters(), lr=1e-3)
#损失函数
criterion = nn.BCEWithLogitsLoss()
#定义精度计算函数
#返回每个batch的精确度
def binary_accuracy(preds, y):
#round预测出距离最近的整数,比如0.34 距离0和1选出更近的
rounded_preds = torch.round(torch.sigmoid(preds))
#转换为浮点数进行除法
correct = (rounded_preds == y).float()
acc = correct.sum() / len(correct)
return acc
def train(model, iterator, optimizer, criterion):
#初始化损失函数和准确度
epoch_loss = 0
epoch_acc = 0
#将模型设置为train模式
model.train()
for batch in iterator:
#将梯度置零
optimizer.zero_grad()
#predictions通常初始化为[batch size, 1]
# 但criterion要求[batch]输入,所以移除维度为1的维度
predictions = model(batch.text).squeeze(1)
#计算损失
loss = criterion(predictions, batch.label)
#计算准确率
acc = binary_accuracy(predictions, batch.label)
#计算每个参数的梯度
loss.backward()
#更新参数
optimizer.step()
#item()方法从仅包含单个值的张量中提取标量
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
5 - 模型评估
def evaluate(model, iterator, criterion):
epoch_loss = 0
epoch_acc = 0
#关闭正则化和归一化
model.eval()
with torch.no_grad():
for batch in iterator:
predictions = model(batch.text).squeeze(1)
loss = criterion(predictions, batch.label)
acc = binary_accuracy(predictions, batch.label)
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
6 - 迭代训练
N_EPOCHS = 5
best_valid_loss = float('inf')
for epoch in range(N_EPOCHS):
start_time = time.time()
train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)
end_time = time.time()
epoch_mins, epoch_secs = epoch_time(start_time, end_time)
#如果这次得到的损失函数最小,则保存这个模型,在测试集上可以使用该模型
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), '')
print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
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}%')
7 - 总结
第一篇文章仅简单介绍使用TorchText处理一个分类模型的方法,不关注准确率及损失函数的改进。第二篇进行如下优化:
- 打包填充序列
- 使用词嵌入预训练模型
- 不同的的RNN架构
- 双向及多层RNN
- 改进正则化及不同的优化器