LSTM自然语言处理情感分析项目(三)定义模型结构与模型训练评估测试
目录
一.构建双向LSTM网络
1. 词嵌入层(Embedding Layer)
2. LSTM 层详解
3. 全连接层
4.前向传播过程
二.模型的训练,评估和测试
1.评估函数
核心功能
2.训练函数
训练前准备
训练循环
性能监控与模型保存
早停机制
3.测试函数
4.单个样本测试
单样本测试函数的作用
1. 模型加载与准备
2. 词汇表加载
3. 文本预处理
4. 文本向量化
5. 张量准备
6. 模型预测
7. 结果解析与展示
一.构建双向LSTM网络
我们的模型主要包含三个核心组件:
- 词嵌入层(Embedding Layer)
- 双向 LSTM 层
- 全连接输出层
1. 词嵌入层(Embedding Layer)
import torch.nn as nn
class Model(nn.Module):def __init__(self,embedding_pretrained,n_vocal,embed,num_classes):super(Model,self).__init__()if embedding_pretrained is not None:self.embedding=nn.Embedding.from_pretrained(embedding_pretrained,padding_idx=n_vocal-1,freeze=False)else:self.embedding=nn.Embedding(n_vocal,embed,padding_idx=n_vocal-1)
- 这里我们提供了两种词嵌入方式:使用预训练词向量(如 Word2Vec、GloVe)或随机初始化
padding_idx
参数指定了填充符号的索引,在训练过程中不会更新其嵌入值freeze=False
表示预训练词向量在训练过程中可以微调,有助于适应特定任务
2. LSTM 层详解
LSTM 层是模型的核心,负责捕捉文本序列中的上下文信息:
self.lstm = nn.LSTM(input_size=embed, # 输入特征维度(词向量维度)hidden_size=128, # 隐藏层维度num_layers=3, # LSTM层数bidirectional=True, # 双向LSTMbatch_first=True, # 输入输出格式为(batch, seq, feature)dropout=0.3 # Dropout比率,防止过拟合
)
dropout=0.3,训练的参数比例为0.7,舍弃一些极端的参数如0.0001等,防止过拟合
bidirectional=True,双向LSTM,网络会同时从前向后和从后向前处理序列,两个方向的输出结合起来
128为每一层中每个隐状态中的U,W,V的神经元个数
3为隐藏层 层的个数,batch_first=True表示输入和输出张量以(batch,seq,feature)提供
- 双向 LSTM:模型会同时从左到右和从右到左处理序列,捕捉更全面的上下文信息
- 多层结构:3 层 LSTM 可以学习更复杂的特征表示,每一层的输出作为下一层的输入
- Batch First:设置为 True 时,输入输出的形状为 (batch_size, sequence_length, features),更符合我们的使用习惯
3. 全连接层
self.fc = nn.Linear(128*2, num_classes)
- 由于使用了双向 LSTM,每个时间步的输出是两个方向的拼接,所以输入维度是 128×2
- 输出维度为类别数量,最终通过 softmax(通常在损失函数中集成)得到各类别的概率
4.前向传播过程
def forward(self,x):#([23,34,.13],69)x,_=x#就是只提取评论的独热编码out=self.embedding(x)out,_=self.lstm(out)#调试模型,你来观察lstm输出结果是什么样的数据类型?为什么有一个下划线out=self.fc(out[:,-1,:]) # 句子最后时刻的 hidden statereturn out
关于代码中out, _ = self.lstm(out)
的下划线:LSTM 的输出包含两部分,第一部分是所有时间步的隐藏状态,第二部分是最后一个时间步的隐藏状态和细胞状态。在这里我们用下划线忽略了第二部分,因为后续计算只需要所有时间步的输出。
forward 方法定义了数据在模型中的流动过程:
- 首先从输入中提取文本序列部分
- 将序列通过嵌入层转换为词向量序列
- 将词向量序列输入 LSTM 层,得到所有时间步的输出
- 取最后一个时间步的输出(
out[:, -1, :]
)作为整个句子的表示 - 通过全连接层得到最终的分类结果
这种取最后一个时间步输出的做法适用于文本分类任务,假设最后一个时间步的状态已经捕捉了整个序列的信息。
二.模型的训练,评估和测试
我们的代码包含三个主要函数:
evaluate()
:评估函数,用于在验证集上评估模型性能test()
:测试函数,用于在测试集上获取最终评估结果train()
:训练函数,实现模型的完整训练流程
1.评估函数
def evaluate(class_list, model, data_iter,test=False):model.eval()loss_total=0predict_all=np.array([],dtype=int)labels_all=np.array([],dtype=int)with torch.no_grad():for texts,labels in data_iter:outputs=model(texts)loss=F.cross_entropy(outputs,labels)loss_total+=losslabels=labels.data.cpu().numpy()predic=torch.max(outputs.data,1)[1].cpu().numpy()labels_all=np.append(labels_all,labels)predict_all=np.append(predict_all,predic)acc=metrics.accuracy_score(labels_all,predict_all)if test:report=metrics.classification_report(labels_all,predict_all,target_names=class_list,digits=4)return acc,loss_total/len(data_iter),reportreturn acc,loss_total/len(data_iter)
核心功能
- 将模型设置为评估模式(
model.eval()
) - 禁用梯度计算(
torch.no_grad()
)以提高效率 - 计算整体损失和准确率
- 生成详细的分类报告(针对测试模式)
评估模式与训练模式的主要区别在于:评估模式会关闭 dropout 层,固定批量归一化层的统计量,确保评估结果的一致性
2.训练函数
训练前准备
def train(model,train_iter,dev_iter,test_iter,class_list):model.train() # 设置为训练模式optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) # 初始化优化器
这里使用了 Adam 优化器,这是一种在实践中表现优异的自适应学习率优化器,适合大多数深度学习任务。
训练循环
训练过程采用双层循环结构:
- 外层循环控制训练轮数(epochs)
- 内层循环迭代处理每个批次的数据
total_batch=0# 记录进行到多少batchdev_best_loss=float('inf')#表示无穷大last_improve=0#记录上次验证集loss下降的batch数flag=False#记录是否很久没有效果提升epochs=2#设置训练次数for epoch in range(epochs):print('Epoch [{}/{}]'.format(epoch+1,epochs))for i,(trains,labels) in enumerate(train_iter):# 经过DatasetIterater中的_to_tensor返回的数据格式为:(x,seg_len),youtputs=model(trains)loss=F.cross_entropy(outputs,labels)model.zero_grad()loss.backward()optimizer.step()
每处理一个批次,都会:
- 执行前向传播计算输出
- 计算损失(使用交叉熵损失函数)
- 反向传播计算梯度
- 更新模型参数
性能监控与模型保存
if total_batch % 100 == 0:# 计算训练集准确率# 在验证集上评估if dev_loss < dev_best_loss:dev_best_loss = dev_losstorch.save(model.state_dict(), 'TextRNN.ckpt') # 保存最佳模型last_improve = total_batch
每 100 个批次,我们会评估模型在训练集和验证集上的性能,并保存验证集损失最小的模型(最佳模型)。
早停机制
为了防止过拟合和节省训练时间,代码实现了早停机制:
if total_batch - last_improve > 10000:print('No optimization for a long time, auto-stopping...')flag = Truebreakacc,loss_avg,report=test(model,test_iter,class_list)print('Test Acc:{} Loss_avg:{}'.format(acc,loss_avg))print('Test report:{}'.format(report))
如果连续 10000 个批次验证集性能没有提升,训练会自动停止。
3.测试函数
测试函数与评估函数功能相似,主要区别在于参数默认值的设置。这种设计允许我们在调用时使用更简洁的语法:
- 调用测试时:
test(class_list, model, test_iter)
- 调用评估时:
evaluate(class_list, model, dev_iter)
在实际应用中,这两个函数可以合并为一个,通过参数来控制行为,减少代码冗余。
def test(class_list, model, data_iter,test=True):model.eval()loss_total=0predict_all=np.array([],dtype=int)labels_all=np.array([],dtype=int)with torch.no_grad():for texts,labels in data_iter:outputs=model(texts)loss=F.cross_entropy(outputs,labels)loss_total+=losslabels=labels.data.cpu().numpy()predic=torch.max(outputs.data,1)[1].cpu().numpy()labels_all=np.append(labels_all,labels)predict_all=np.append(predict_all,predic)acc=metrics.accuracy_score(labels_all,predict_all)if test:report=metrics.classification_report(labels_all,predict_all,target_names=class_list,digits=4)return acc,loss_total/len(data_iter),reportreturn acc,loss_total/len(data_iter)
4.单个样本测试
单样本测试函数的作用
单样本测试函数是连接模型与实际应用的重要桥梁,它实现了从原始文本到情感类别的完整转换流程。这个函数通常包含以下步骤:
- 加载训练好的模型参数
- 对输入文本进行预处理
- 执行模型预测
- 返回并展示预测结果
1. 模型加载与准备
def test_sample(sample, model):# 加载训练好的模型参数model.load_state_dict(torch.load('TextRNN.ckpt', map_location=device))# 设置模型为评估模式model.eval()
这部分代码负责加载训练好的模型权重。map_location=device
参数确保模型可以正确加载到指定的设备(CPU 或 GPU)上。model.eval()
将模型设置为评估模式,这会关闭 dropout 等只在训练时使用的功能,确保预测结果的一致性
2. 词汇表加载
# 加载词汇表vocab = pkl.load(open('simplifyweibo_4_moods.pkl', 'rb'))
词汇表(vocabulary)是训练过程中创建的,用于将文本中的词语(或字符)映射到数字索引。这里加载的词汇表必须与训练时使用的保持一致,否则会导致索引不匹配的错误。
3. 文本预处理
文本预处理是确保模型能够正确处理输入的关键步骤,需要与训练数据的预处理方式完全一致:
# 将文本分割为字符token = [x for x in sample]
# 填充或截断文本至固定长度(70)token.extend(['<PAD>'] * (70 - len(token)))
这段代码首先将输入文本分割为字符序列(这里使用的是字符级模型),然后将序列长度统一调整为 70—— 短于 70 的文本用<PAD>
(填充符)补足,长于 70 的文本会被截断(在这段代码中只展示了填充部分)。
统一序列长度是因为神经网络通常要求输入具有固定的维度。
4. 文本向量化
# 将文本转换为数字索引word_line = []for word in token:# 查找词汇表,未知词用<UNK>的索引代替word_line.append((vocab.get(word, vocab.get('<UNK>'))))
这一步将字符序列转换为数字索引序列。对于词汇表中不存在的字符(未知字符),使用<UNK>
(未知符)的索引代替,这与训练过程中的处理方式一致。
5. 张量准备
# 转换为PyTorch张量并添加批次维度x = torch.LongTensor(word_line).unsqueeze(0).to(device)
# 构造模型所需的输入格式x = (x, torch.tensor([0]).to(device))
PyTorch 模型要求输入为张量(Tensor)格式,因此需要进行以下转换:
- 将列表转换为
LongTensor
(因为是整数索引) - 使用
unsqueeze(0)
添加批次维度(模型通常处理批次数据,即使批次大小为 1) - 将张量移动到适当的设备(CPU 或 GPU)
- 构造与训练时一致的输入格式(这里是包含两个元素的元组)
6. 模型预测
# 执行预测with torch.no_grad(): # 关闭梯度计算,节省内存outputs = model(x)
with torch.no_grad():
上下文管理器用于禁用梯度计算,这在纯预测阶段可以显著节省内存并提高计算速度。模型的输出outputs
通常是每个类别的得分(logits)。
7. 结果解析与展示
# 获取预测结果predic = torch.max(outputs.data, 1)[1].cpu().numpy()[0]# 定义情感类别列表class_list = ['喜悦', '愤怒', '厌恶', '低落']# 输出预测结果print(f"{sample}的情绪是: {class_list[predic]}")
torch.max(outputs.data, 1)
返回指定维度(这里是类别维度)的最大值和对应的索引[1]
取索引部分,即预测的类别索引cpu().numpy()[0]
将结果从张量转换为 numpy 数组,并取出唯一的元素(因为批次大小为 1)- 最后将数字索引转换为对应的情感类别名称并打印