目录
[1. 词嵌入层(Embedding Layer)](#1. 词嵌入层(Embedding Layer))
[2. LSTM 层详解](#2. LSTM 层详解)
[3. 全连接层](#3. 全连接层)
[1. 模型加载与准备](#1. 模型加载与准备)
[2. 词汇表加载](#2. 词汇表加载)
[3. 文本预处理](#3. 文本预处理)
[4. 文本向量化](#4. 文本向量化)
[5. 张量准备](#5. 张量准备)
[6. 模型预测](#6. 模型预测)
[7. 结果解析与展示](#7. 结果解析与展示)
一.构建双向LSTM网络
我们的模型主要包含三个核心组件:
- 词嵌入层(Embedding Layer)
- 双向 LSTM 层
- 全连接输出层
1. 词嵌入层(Embedding Layer)
python
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 层是模型的核心,负责捕捉文本序列中的上下文信息:
python
self.lstm = nn.LSTM(
input_size=embed, # 输入特征维度(词向量维度)
hidden_size=128, # 隐藏层维度
num_layers=3, # LSTM层数
bidirectional=True, # 双向LSTM
batch_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. 全连接层
python
self.fc = nn.Linear(128*2, num_classes)
- 由于使用了双向 LSTM,每个时间步的输出是两个方向的拼接,所以输入维度是 128×2
- 输出维度为类别数量,最终通过 softmax(通常在损失函数中集成)得到各类别的概率
4.前向传播过程
python
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 state
return out
关于代码中**out, _ = self.lstm(out)
**的下划线:LSTM 的输出包含两部分,第一部分是所有时间步的隐藏状态,第二部分是最后一个时间步的隐藏状态和细胞状态。在这里我们用下划线忽略了第二部分,因为后续计算只需要所有时间步的输出。
forward 方法定义了数据在模型中的流动过程:
- 首先从输入中提取文本序列部分
- 将序列通过嵌入层转换为词向量序列
- 将词向量序列输入 LSTM 层,得到所有时间步的输出
- 取最后一个时间步的输出(
out[:, -1, :]
)作为整个句子的表示 - 通过全连接层得到最终的分类结果
这种取最后一个时间步输出 的做法适用于文本分类任务,假设最后一个时间步的状态已经捕捉了整个序列的信息。
二.模型的训练,评估和测试
我们的代码包含三个主要函数:
evaluate()
:评估函数,用于在验证集上评估模型性能test()
:测试函数,用于在测试集上获取最终评估结果train()
:训练函数,实现模型的完整训练流程
1.评估函数
python
def evaluate(class_list, model, data_iter,test=False):
model.eval()
loss_total=0
predict_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+=loss
labels=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),report
return acc,loss_total/len(data_iter)
核心功能
- 将模型设置为评估模式(
model.eval()
) - 禁用梯度计算(
torch.no_grad()
)以提高效率 - 计算整体损失和准确率
- 生成详细的分类报告(针对测试模式)
评估模式与训练模式的主要区别在于:评估模式会关闭 dropout 层,固定批量归一化层的统计量,确保评估结果的一致性
2.训练函数
训练前准备
python
def train(model,train_iter,dev_iter,test_iter,class_list):
model.train() # 设置为训练模式
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) # 初始化优化器
这里使用了 Adam 优化器,这是一种在实践中表现优异的自适应学习率优化器,适合大多数深度学习任务。
训练循环
训练过程采用双层循环结构:
- 外层循环控制训练轮数(epochs)
- 内层循环迭代处理每个批次的数据
python
total_batch=0# 记录进行到多少batch
dev_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),y
outputs=model(trains)
loss=F.cross_entropy(outputs,labels)
model.zero_grad()
loss.backward()
optimizer.step()
每处理一个批次,都会:
- 执行前向传播计算输出
- 计算损失(使用交叉熵损失函数)
- 反向传播计算梯度
- 更新模型参数
性能监控与模型保存
python
if total_batch % 100 == 0:
# 计算训练集准确率
# 在验证集上评估
if dev_loss < dev_best_loss:
dev_best_loss = dev_loss
torch.save(model.state_dict(), 'TextRNN.ckpt') # 保存最佳模型
last_improve = total_batch
每 100 个批次,我们会评估模型在训练集和验证集上的性能,并保存验证集损失最小的模型(最佳模型)。
早停机制
为了防止过拟合和节省训练时间,代码实现了早停机制:
python
if total_batch - last_improve > 10000:
print('No optimization for a long time, auto-stopping...')
flag = True
break
acc,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)
在实际应用中,这两个函数可以合并为一个,通过参数来控制行为,减少代码冗余。
python
def test(class_list, model, data_iter,test=True):
model.eval()
loss_total=0
predict_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+=loss
labels=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),report
return acc,loss_total/len(data_iter)
4.单个样本测试
单样本测试函数的作用
单样本测试函数是连接模型与实际应用的重要桥梁,它实现了从原始文本到情感类别的完整转换流程。这个函数通常包含以下步骤:
- 加载训练好的模型参数
- 对输入文本进行预处理
- 执行模型预测
- 返回并展示预测结果
1. 模型加载与准备
python
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. 词汇表加载
python
# 加载词汇表
vocab = pkl.load(open('simplifyweibo_4_moods.pkl', 'rb'))
词汇表(vocabulary)是训练过程中创建的,用于将文本中的词语(或字符)映射到数字索引。这里加载的词汇表必须与训练时使用的保持一致,否则会导致索引不匹配的错误。
3. 文本预处理
文本预处理是确保模型能够正确处理输入的关键步骤,需要与训练数据的预处理方式完全一致:
python
# 将文本分割为字符
token = [x for x in sample]
# 填充或截断文本至固定长度(70)
token.extend(['<PAD>'] * (70 - len(token)))
这段代码首先将输入文本分割为字符序列(这里使用的是字符级模型),然后将序列长度统一调整为 70------ 短于 70 的文本用<PAD>
(填充符)补足,长于 70 的文本会被截断(在这段代码中只展示了填充部分)。
统一序列长度是因为神经网络通常要求输入具有固定的维度。
4. 文本向量化
python
# 将文本转换为数字索引
word_line = []
for word in token:
# 查找词汇表,未知词用<UNK>的索引代替
word_line.append((vocab.get(word, vocab.get('<UNK>'))))
这一步将字符序列转换为数字索引序列。对于词汇表中不存在的字符(未知字符),使用<UNK>
(未知符)的索引代替,这与训练过程中的处理方式一致。
5. 张量准备
python
# 转换为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. 模型预测
python
# 执行预测
with torch.no_grad(): # 关闭梯度计算,节省内存
outputs = model(x)
with torch.no_grad():
上下文管理器用于禁用梯度计算,这在纯预测阶段可以显著节省内存并提高计算速度。模型的输出outputs
通常是每个类别的得分(logits)。
7. 结果解析与展示
python
# 获取预测结果
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)- 最后将数字索引转换为对应的情感类别名称并打印