拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

NLP-情感分析项目(四):训练评估 + 主函数

NLP-情感分析项目(四):训练评估 + 主函数

情感分析项目代码详解(四):train_eval_test.py 训练评估 + main.py 项目总入口

上一篇搭好了 TextRNN 模型,这一篇进入实战环节。train_eval_test.py 包含训练、验证、测试三个函数,负责让模型"学会"分类、及时保存最优版本、最后报告总成绩。

一、导入库

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 评估函数

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():评估阶段不需要反向传播,关掉梯度计算能省大量显存、跑得更快。

三、逐批计算损失与预测

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 把每批的结果拼进大数组,循环结束后就得到全数据集的预测和标签。

四、计算准确率与分类报告

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 测试函数

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 训练函数开头

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 轮(一轮 = 完整过一遍训练集)。

七、训练核心循环

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 批评估一次

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,记录总进度。

九、早停机制

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 导入与设备选择

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 字符串会传给迭代器和模型,决定张量在哪里运算。

十一、固定随机种子

np.random.seed(1) torch.manual_seed(1) torch.cuda.manual_seed_all(1) torch.backends.cudnn.deterministic = True

随机种子相当于"随机数的起点"。训练中很多环节是随机的(打乱数据、初始化参数、dropout 等),固定种子后每次运行结果都一致,方便调试和对比实验;否则每次结果都略有差异,出了问题很难排查。

三行分别固定 numpy、CPU 和 GPU 的种子;最后一行让 GPU 上的卷积运算也走确定性路径。

十二、组装数据

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 条数据。

十三、加载预训练向量

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 参数。

十四、定义类别并创建模型

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 上。到这里,所有零件全部就位。

十五、开始训练

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 做的事就是:选设备、定种子、组装数据、加载预训练向量、创建模型、开训,把各模块像零件一样拼成一个完整的深度学习情感分类项目。

返回列表