情感分析项目代码详解(四):train_eval_test.py 训练评估 + main.py 项目总入口
上一篇搭好了 TextRNN 模型,这一篇进入实战环节。train_eval_test.py 包含训练、验证、测试三个函数,负责让模型"学会"分类、及时保存最优版本、最后报告总成绩。
一、导入库
python
import torch
import torch.nn as nn
import numpy as np
import torch.nn.functional as F
from sklearn import metrics
numpy:数值计算库,这里用来收集和拼接预测结果数组。
torch.nn.functional(简写 F):提供交叉熵损失等函数式接口,只计算、不保存参数。
sklearn.metrics:机器学习评估工具包,准确率、分类报告都有现成函数,不用自己手写。
二、evaluate 评估函数
python
def evaluate(class_list, model, data_iter, test=False):
model.eval()
loss_total = 0
predict_all = np.array([], dtype=int)
label_all = np.array([], dtype=int)
with torch.no_grad():
model.eval():把模型切换到"评估模式"。训练时用到的 dropout 等随机行为会被关闭,保证评估结果稳定可复现。
loss_total:累加所有批次的损失,最后取平均。
predict_all、label_all:两个空数组,用来收集整个数据集上的预测值和真实标签。
with torch.no_grad():评估阶段不需要反向传播,关掉梯度计算能省大量显存、跑得更快。
三、逐批计算损失与预测
python
for texts, labels in data_iter:
outputs = model(texts)
loss = F.cross_entropy(outputs, labels)
loss_total += loss.item()
labels = labels.data.cpu().numpy()
predic = torch.max(outputs, 1)[1].cpu().numpy()
label_all = np.append(label_all, labels)
predict_all = np.append(predict_all, predic)
data_iter 就是第二篇写的 DatasetIterator,每次吐出一批 (texts, labels)。
F.cross_entropy:交叉熵损失,衡量预测分数和真实标签差多少,是多分类任务最常用的损失函数。loss.item() 把张量转成普通 Python 数字。
torch.max(outputs, 1)1:沿第 1 维(类别方向)找最大值,1 取最大值的下标,也就是预测的类别;再用 .cpu().numpy() 转成 numpy 数组。
np.append 把每批的结果拼进大数组,循环结束后就得到全数据集的预测和标签。
四、计算准确率与分类报告
python
acc = metrics.accuracy_score(label_all, predict_all)
if test:
report = metrics.classification_report(label_all, predict_all,
target_names=class_list, digits=4)
return acc, loss_total / len(data_iter), report
return acc, loss_total / len(data_iter)
accuracy_score 直接算出整体准确率:预测对的条数 ÷ 总条数。
test=True 时(正式测试),额外生成分类报告:每种情绪各自的精确率、召回率、F1 值(digits=4 保留 4 位小数),能看出模型对哪类情绪识别得好、哪类容易混淆。
平均损失 = 总损失 ÷ 批数。评估在验证集上只返回准确率和损失,在测试集上多返回一份报告。
五、test 测试函数
python
def test(model, test_iter, class_list):
model.eval()
test_acc, test_loss, test_report = evaluate(class_list, model, test_iter, True)
msg = 'Test Loss: {0:>5.2} , Test Acc: {1:>6.2%}'
print(msg.format(test_loss, test_acc))
print(test_report)
测试其实就是"开全量报告"的评估:把 test=True 传给 evaluate。
msg 里的 {1:>6.2%} 是格式控制符:百分数保留 2 位小数、右对齐占 6 格,输出整齐好看。
最后打印测试集上的损失、准确率和完整分类报告。
六、train 训练函数开头
python
def train(model, train_iter, dev_iter, test_iter, class_list):
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
total_batch = 0
dev_best_loss = float('inf')
last_improve = 0
flag = False
epochs = 20
model.train():切换到训练模式,启用 dropout 等机制(与 eval 正好相反)。
优化器选用 Adam,学习率 1e-3(即 0.001)。Adam 是目前最常用的优化器,收敛快、调参省心;model.parameters() 把模型所有可训练参数交给它管理。
几个记录变量:total_batch 已训练批数;dev_best_loss 历史最好验证损失,初始化为正无穷------这样第一次评估一定会刷新它;last_improve 上次成绩提升时的批数;flag 早停标记;epochs=20 表示最多训练 20 轮(一轮 = 完整过一遍训练集)。
七、训练核心循环
python
for epoch in range(epochs):
print('Epoch {}/{}'.format(epoch + 1, epochs))
for i, (trains, labels) in enumerate(train_iter):
outputs = model(trains)
loss = F.cross_entropy(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
双层循环:外层 20 轮,内层遍历训练集的每一个批次。
每个批次走"标准四步曲":①前向传播算输出;②交叉熵算损失;③zero_grad() 清空上一步的梯度(PyTorch 的梯度默认累加,不清会越积越乱);④backward() 反向传播求梯度,step() 用梯度更新参数。
模型就是这样一批一批、一轮一轮"学习"的:不断算错、求梯度、改参数,损失越来越小。
八、每 100 批评估一次
python
if total_batch % 100 == 0:
predic = torch.max(outputs.data, 1)[1].cpu()
train_acc = metrics.accuracy_score(labels.data.cpu(), predic)
dev_acc, dev_loss = evaluate(class_list, model, dev_iter)
if dev_loss < dev_best_loss:
dev_best_loss = dev_loss
torch.save(model.state_dict(), 'textrnn.ckpt')
last_improve = total_batch
msg = 'Iter:{0:>6}, Train Loss:{1:>5.2}, Train Acc:{2:>6.2%}, Val Loss:{3:>5.2}, Val Acc:{4:>6.2%}'
print(msg.format(total_batch, loss.item(), train_acc, dev_loss, dev_acc))
model.train()
total_batch += 1
每训练 100 个批次,就在验证集上"小考"一次:
先算当前批次的训练准确率;再调用 evaluate 在验证集上得到 dev_loss 和 dev_acc。
如果这次验证损失打破了历史最低记录,就把当前模型参数保存到 textrnn.ckpt(state_dict 就是模型参数),并更新 last_improve------这就是"保存最优模型"的常用做法。
打印一行统计信息,格式符保证对齐。
最后记得 model.train() 切回训练模式------因为 evaluate 内部调用了 model.eval(),不切回来的话 dropout 就一直关着了。
循环末尾 total_batch += 1,记录总进度。
九、早停机制
python
if total_batch - last_improve > 10000:
print('No optimization for a long time, auto-stopping...')
flag = True
break
if flag:
break
test(model, test_iter, class_list)
早停(early stopping):如果已经训练了 10000 个批次,验证损失却一直没刷新最好成绩,说明继续练下去多半只是浪费时间甚至过拟合,直接中断。
flag 配合两层 break:先跳出内层批次循环,再跳出外层轮次循环。
训练全部结束后,调用 test 在测试集上做最终"大考",打印损失、准确率和分类报告。
训练流程讲完后,顺便看看项目的总入口 main.py。它负责把前几篇写的所有零件组装起来,代码不长,一并放在本篇结尾讲完。
十、main.py 导入与设备选择
python
import torch
import numpy as np
import load_dataset, textrnn
from train_eval_test import train
if torch.cuda.is_available():
device = 'cuda'
elif torch.backends.mps.is_available():
device = 'mps'
else:
device = 'cpu'
load_dataset、textrnn 是同一个项目目录下的模块,直接 import 就能用里面写好的函数和模型类;from train_eval_test import train 只导入训练函数。
接着判断用哪个设备计算:cuda 代表 NVIDIA 显卡,mps 代表苹果芯片的显卡,都没有就退回 cpu。这个 device 字符串会传给迭代器和模型,决定张量在哪里运算。
十一、固定随机种子
python
np.random.seed(1)
torch.manual_seed(1)
torch.cuda.manual_seed_all(1)
torch.backends.cudnn.deterministic = True
随机种子相当于"随机数的起点"。训练中很多环节是随机的(打乱数据、初始化参数、dropout 等),固定种子后每次运行结果都一致,方便调试和对比实验;否则每次结果都略有差异,出了问题很难排查。
三行分别固定 numpy、CPU 和 GPU 的种子;最后一行让 GPU 上的卷积运算也走确定性路径。
十二、组装数据
python
vocab, train_data, dev_data, test_data = load_dataset.load_dataset('simplifyweibo_4_moods.csv')
train_iter = load_dataset.DatasetIterator(train_data, 128, device)
dev_iter = load_dataset.DatasetIterator(dev_data, 128, device)
test_iter = load_dataset.DatasetIterator(test_data, 128, device)
调用第二篇写好的 load_dataset 加载词汇表和三个数据集,再用 DatasetIterator 把每个数据集包装成迭代器,batch_size 设为 128------模型每次吃 128 条数据。
十三、加载预训练向量
python
embedding_pretrained = torch.tensor(np.load('embedding_Tencent.npz')['embeddings'].astype('float32'))
embed = embedding_pretrained.size(1) if embedding_pretrained is not None else 300
np.load 读取腾讯开源的中文词向量文件(npz 压缩格式),取出里面的 embeddings 数组;astype('float32') 转成 32 位浮点,最后用 torch.tensor 转成 PyTorch 张量。
embed 取向量的维度 size(1)(比如腾讯向量是 200 维);如果没加载到预训练向量,就退回默认 300 维。这个值对应第三篇 Model 里的 embed 参数。
十四、定义类别并创建模型
python
class_list = ['喜悦', '愤怒', '厌恶', '低落']
num_classes = len(class_list)
model = textrnn.Model(embedding_pretrained, len(vocab), embed, num_classes).to(device)
class_list 列出四种情绪,和标签 0~3 一一对应,之后会作为分类报告每一行的名字。
按第三篇 Model 的签名传入四个参数:预训练向量、词汇表大小、向量维度、类别数;最后 .to(device) 把模型整体搬到 GPU 上。到这里,所有零件全部就位。
十五、开始训练
python
train(model, train_iter, dev_iter, test_iter, class_list)
调用第四篇的 train 函数,训练、验证、早停、保存最优模型、最终测试一气呵成。运行 main.py,就等于把整个项目跑了一遍。
小结
整个项目至此全部讲完:vocab_create.py 建词汇表 → load_dataset.py 数字化并分批 → textrnn.py 搭建模型 → train_eval_test.py 训练评估 → main.py 总调度。main.py 做的事就是:选设备、定种子、组装数据、加载预训练向量、创建模型、开训,把各模块像零件一样拼成一个完整的深度学习情感分类项目。