ARTICLE DETAIL

资讯详情

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

LSTM中文短文本情感分析:数据预处理到模型避坑全解析

LSTM中文短文本情感分析:数据预处理到模型避坑全解析 简介这是一份基于LSTM的中文短文本情感分析完整源码专为Python课程设计、期末大作业场景准备适合具备Python基础、希望快速完成情感分类项目的学生参考。项目围绕中文短文本情感二分类展开覆盖数据加载、模型训练、预测评估等环节代码结构简洁注释清晰下载即用无需修改即可运行。压缩包共14个文件1.96MB主要包括5个txt文本数据与说明、3个py核心脚本、2个pyc缓存文件、1个csv标注数据、1个pt模型权重、1个Markdown说明文档等其中txt与csv提供正负情感样本py文件实现完整流程pt文件可直接加载训练好的模型。目前已有245人浏览学习适合作为高分作业参考模板。通过源码可学习LSTM处理中文文本的完整思路包括分词、序列填充、词向量映射与情感分类器设计同时附带模型权重和测试数据便于快速验证效果。整体轻量、聚焦适合初学者在短时间内部署并理解深度学习文本分类任务。1. 一套“高分期末大作业”的 LSTM 中文短文本情感分析到底值不值得复现“Python实现基于LSTM的中文短文本情感分析源码”这个标题在很多期末大作业资源包里反复出现。我见过太多人下载完.zip之后卡在跑不起来或者跑完只得到一个没法解释的准确率。这篇内容就是把这个方向完整拆开数据怎么准备、LSTM模型怎么写、训练怎么调、哪些坑会让你翻车。适合要交能演示程序的在校生也想用PyTorch认真入门自然语言处理的初学者。一个反直觉的结论是决定作业是否拿高分的往往不是LSTM结构有多深而是预处理是否干净、评估是否诚实。这个任务本质是一个二分类问题输入一句中文短文本比如酒店评论、外卖评价、微博输出正负情感。LSTM负责捕捉句子里的词序和局部搭配。短文本长度有限训练数据从几千到几万条都容易跑甚至CPU就能完成很适合作为第一个NLP落地项目。后面所有代码以PyTorch为主配jieba和pandas。环境是Python 3.8以上版本安装好PyTorch、jieba、numpy就能跑起来。2. 中文短文本的情感数据工程语料、分词、去停用词与序列化2.1 选数据集公开中文语料与标注格式这个标题对应的项目第一步不是搭模型而是找到能用的语料。常见做法是使用公开的中文情感分析数据集例如ChnSentiCorp酒店评论、weibo_senti_100k、电商或外卖平台的评论数据。选择标准只有一条文本够不够“短”。酒店客评平均几十字微博评论更短都是为这个项目量身定做的场景。相反如果拿长篇小说段落当语料那就不叫短文本分析了。拿到数据后不要急着写模型先把数据读进来看标签分布和句长分布。原因有二第一步决定要不要做类平衡第二步决定后面max_len设多少。下面这段代码就是检查用的import pandas as pd df pd.read_csv(sentiment_data.csv) # 约定text 列是评论文本label 列是 0负向或 1正向 print(df[label].value_counts()) df[length] df[text].astype(str).apply(lambda x: len(x)) print(df[length].describe())逻辑说明value_counts()能让你看清正负样本比例describe()给出句子长度的均值、四分位数和最大值后面选max_len时直接看P90。参数说明如果下载的压缩包里不是csv而是tsv或Excelpandas都有对应的read_excel、read_csv(sep\t)。中文数据经常是utf-8或gbk编码读出来乱码时在read_csv里加encodingutf-8或gbk。还需要确认label的具体含义。很多公开资源里负样本标2、正样本标1或者反过来如果网上某个版本跑了95%准确率却全是负样本多半是标签反了。我的习惯是手动打印10条文本和对应标签人眼过一遍。虽然土但能避免后面整个训练变成黑匣子。这一步花不了五分钟却能省下你排查模型结构的一整个晚上。2.2 分词与停用词处理jieba 的用法和参数中文不能像英文那样按空格切词所以要先分词。绝大多数实现用jieba因为它安装简单、精确模式够用。这里我们只用jieba.lcut()它返回一个列表方便后续过滤。下面这个版本除了分词还顺带做了空白过滤和长度过滤import jieba def tokenize(text): words jieba.lcut(str(text)) return [w for w in words if w.strip() and len(w) 1]然后再加停用词过滤stopwords set() with open(stopwords.txt, encodingutf-8) as f: for line in f: if line.strip(): stopwords.add(line.strip()) def tokenize(text): words jieba.lcut(str(text)) kept [] for w in words: if w.strip() and w not in stopwords and len(w) 1: kept.append(w) return kept逻辑说明停用词表里存的词通常是“的、了、而且、在、我们”这类没有情感倾向的词。过滤后序列变短模型能更集中学习情感词。但坑也在这里停用词表里绝对不能放“不、没、太、很、别、莫”这类能改变情感极性的词。如果你把“不”放进停用词表“不好吃”先被jieba切成“不/好吃”然后“不”被删掉剩下“好吃”就是正向模型直接学错。这是这个项目里最容易翻车的地方之一。参数说明len(w) 1会丢弃单字词对大多数评论语料是安全的。但如果语料是口语像“牛”“绝”“菜”这种单字情感词也有信息量这种情况下可以去掉该条件或者在下游交给模型自己去学。jieba对网络新词识别差“yyds”“绝绝子”这类会被切得更碎。你可以在加载后调用jieba.add_word(绝绝子, freq100)让分词结果保持完整。另外停用词表不需要自己一穷二白从头收集公开的中文停用词表有很多几十KB就够。但因为每个语料的风格不同我一般先默认过滤然后打印二十条预处理结果看看如果发现某些关键词被误杀就从表里单独删掉。这个“人工审查”环节比筛词数量更重要。2.3 构建词汇表与序列 padding代码与长度选择分词之后文本还是字符串列表LSTM只能吃数字。常见做法是先统计词频按高频词构建词汇表把出现频率极低的词统一映射成unk再用词汇表把每个句子转换成整数序列最后padding到定长。具体实现如下from collections import Counter max_len 50 # 句子最长保留的词数 max_vocab_size 20000 # 词汇表上限 counter Counter() texts, labels [], [] for text, label in zip(df[text], df[label]): tokens tokenize(text)[:max_len] texts.append(tokens) labels.append(label) counter.update(tokens) vocab {w: i 2 for i, (w, _) in enumerate(counter.most_common(max_vocab_size))} vocab[pad] 0 vocab[unk] 1 def encode(tokens): ids [vocab.get(w, vocab[unk]) for w in tokens] pad_len max_len - len(ids) return ids [vocab[pad]] * pad_len X [encode(tokens) for tokens in texts] y list(labels)逻辑说明先截断再padding截断保证长度不会超过max_lenpadding保证短句补齐到max_len。vocab的索引从2开始把0和1留给了pad和unk。Embedding层的padding_idx会被设为0这部分向量不参与更新等于模型自动忽略空白位置。参数说明max_len为什么取50因为前面describe()看过P90短文本90%的句子都在50个词以内如果你手里是电影评论可能要放大到80。max_vocab_size取20000对几万条语料够用如果语料只有几千条5000更加稳妥不然vocab很大但大部分词只出现几次模型容易把它们当作噪声。还有一个容易被忽略的点不要因为“难吃”出现频率高就迷信词频。真正有判别力的词往往处于中频地带。出现次数小于2的词直接归入unk是合理的。但如果把“惊艳”这种词误归unk模型就学不到这个信号所以阈值不要设成5以上。把数据处理成Tensor和Dataset训练就方便了import torch from torch.utils.data import TensorDataset, DataLoader, random_split X_tensor torch.tensor(X, dtypetorch.long) y_tensor torch.tensor(y, dtypetorch.long) dataset TensorDataset(X_tensor, y_tensor) train_size int(len(dataset) * 0.8) val_size len(dataset) - train_size train_ds, val_ds random_split(dataset, [train_size, val_size]) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse)说明这里用TensorDataset数据在内存里一次性转为Tensor简单直接适合短文本规模。DataLoader的shuffle只在训练集打开验证集保持顺序。batch_size64是CPU上的稳妥值如果内存紧张改32。代码不需要collate_fn因为所有向量已经是定长的了这一点后面避坑章节还会说。3. 搭建 LSTM 分类模型Embedding、nn.LSTM 与全连接层的维度对齐3.1 为什么选 LSTM短文本里词序和转折太重要标题中已经定了LSTM但你要能说清为什么。短文本情感分析里否定词、转折连词对语义影响巨大。“没有期待中那么难吃”是一句正面评价如果只看词频或者CNN的局部n-gram很容易被“难吃”带偏。LSTM按顺序读入每个词通过门控机制让“没有”和“那么”抑制“难吃”的负面激活。相比TransformerLSTM没有位置编码也天然自带顺序信息参数量小几千条数据就能收敛而Transformer需要更多数据和更长训练时间。所以期末作业选LSTM是一个性价比很高的选择。但LSTM也有两个明显短板训练比CNN慢超参数更敏感。所以后面代码里我们会刻意用小尺寸模型两层、128维用早停和梯度裁剪做保护而不是追求“更深更大”。这也是很多网上下载的源码不同的地方它们喜欢把模型堆得很高然后在一个小数据集上过拟合最后给你一张漂亮的曲线图。3.2 模型定义一个可以直接抄的最小实现PyTorch里LSTM分类模型通常分为三层Embedding把词索引变成密集向量nn.LSTM读取向量序列输出隐状态全连接层把最终隐状态映射成类别分数。这里有一个最容易被作业报告忽略的点h_n的形状。h_n是(num_layers, batch, hidden_size)不是(batch, hidden_size)。我们分类时只取最后一层的末时刻隐状态也就是h_n[-1]。import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim128, hidden_size128, num_layers2, num_classes2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.lstm nn.LSTM(input_sizeembedding_dim, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size, num_classes) def forward(self, x): x self.embedding(x) # [B, T, D] out, (h_n, c_n) self.lstm(x) # out: [B, T, H] final_hidden h_n[-1] # [B, H] logits self.fc(self.dropout(final_hidden)) return logits逻辑说明batch_firstTrue让输入形状是(batch, seq_len, embedding_dim)而不是(seq_len, batch, embedding_dim)。nn.LSTM的dropout参数只在num_layers1时生效且不影响最后一层所以我们在全连接前再手动放一个Dropout。padding_idx0意味着pad对应的embedding向量永远是零向量梯度不会被它带偏。最后输出logits每个样本两个实数分别代表负向和正向的原始分数没有过softmax。参数说明embedding_dim和hidden_size都是128这是经验和稳妥的组合。如果语料很大10万条以上可以把hidden_size加到256但你需要更长训练时间。num_layers2是短文本场景的甜点值1层太弱3层开始难收敛且容易过拟合。dropout0.3对小数据足够如果发现验证集明显过拟合升到0.5。3.3 超参数为什么这么设embedding_dim、hidden_size、num_layers 与 dropout把上一节参数再展开因为期末作业答辩时老师最爱问的就是“你这个数从哪来的”。对于中文短文本词表大小通常在1万到3万之间embedding_dim取128时词向量矩阵大约有128乘20000等于2.56M参数这个体量在CPU上训练完全可行。如果降到64训练更快但表达能力下降如果升到256小语料上很容易把训练集背下来验证集崩。hidden_size影响LSTM隐状态容量。128对2万词的表够用而且和128维的embedding正好对齐。这里不是必须相等但相等时维度感更清晰。num_layers2意味着时间维度的反向传播路径变长梯度更容易衰减因此配合4.1的梯度裁剪很有必要。如果数据集很小比如只有两三千条我建议只用1层效果没准更好。dropout的取值跟过拟合风险正相关。数据越多dropout可以越小数据少dropout就大。但也不能过大0.5以上会让模型在训练阶段信息损失过多收敛变慢。你会发现这堆选择里没有绝对正确但很多人输在“调参像调宝贝、结构像堆积木”。我的建议是先跑通当前默认参数再只改一个变量做对比这样报告里才能写出有依据的“实验”。提示不要同时把所有超参数都改掉一次只改一个否则你没法解释是哪个变化带来收益。3.4 优化器、损失函数与初始化写训练循环前的三件事模型定义完之后先别急着套训练循环。先把device、随机种子、损失函数和优化器订好然后用一个小batch试跑一次forward确定没有维度错误。下面这段是每次都会用的骨架import torch device torch.device(cuda if torch.cuda.is_available() else cpu) torch.manual_seed(42) model LSTMClassifier(vocab_sizelen(vocab)).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) # 试跑一个 batch确认维度 batch_x, batch_y next(iter(train_loader)) batch_x, batch_y batch_x.to(device), batch_y.to(device) logits model(batch_x) print(logits.shape) # 预期: [batch_size, 2]逻辑说明CrossEntropyLoss要求输入是logits不是softmax概率它内部会先做softmax再算交叉熵。如果你在模型里提前softmax或者用NLLLoss就会得到完全不对的训练曲线。torch.manual_seed(42)保证每次运行得到的划分和初始化一致作业报告里描述“可复现性”时才不会被质疑。参数说明Adam的lr1e-3是NLP任务常见起点。碰到训练loss乱跳降到5e-4如果loss下降太慢可以先用1e-3跑5个epoch再降。优化器选择上Adam适合RNN因为一阶动量能稳定梯度的尺度SGD在RNN上需要精细调学习率不推荐给作业。这段代码能跑通说明模型和数据处理已经对接上了可以进入训练阶段。4. 训练循环与评估指标让模型收敛并识别“假高准确率”4.1 一个 epoch 的训练代码batch 循环、梯度清零与梯度裁剪训练循环的本质是对每个batch做四步前向传播、算loss、反向传播、更新参数。这里加入了梯度裁剪以及用平均loss衡量训练进度。写成函数方便后续管理def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0.0 total_num 0 for x_batch, y_batch in loader: x_batch, y_batch x_batch.to(device), y_batch.to(device) optimizer.zero_grad() logits model(x_batch) loss criterion(logits, y_batch) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() * x_batch.size(0) total_num x_batch.size(0) return total_loss / total_num逻辑说明optimizer.zero_grad()必须放在backward之前因为PyTorch在调用backward时会把梯度累加到param.grad上不清零就会把上一个batch的梯度叠加进来。梯度裁剪clip_grad_norm_对LSTM这类循环网络尤其重要因为反向传播经过多个时间步梯度范数很容易爆炸一旦爆炸模型权重变成NaN后面所有loss都是NaN。max_norm1.0是个常见的保守值如果你发现loss偶尔变大但训练仍能恢复可以保持不动。为什么不在这里做model.eval()训练函数应只处理训练模式验证是单独的函数。这样每个epoch里训练和验证职责清晰也方便你为报告分别记录曲线。4.2 验证函数与评估指标准确率不是唯一答案验证函数要在no_grad下运行同时关闭Dropout。写法如下def evaluate(model, loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for x_batch, y_batch in loader: x_batch, y_batch x_batch.to(device), y_batch.to(device) logits model(x_batch) preds torch.argmax(logits, dim1) correct (preds y_batch).sum().item() total y_batch.size(0) return correct / total逻辑说明model.eval()和no_grad()必须同时出现。否则Dropout仍然生效每次推理结果不同no_grad关闭自动求导省内存也防止意外改变权重。argmax(dim1)在类别维度上取分数最大的索引0是负向1是正向。参数说明验证函数只接受model、loader、device三个入参loader的batch_size由外面创建时决定这里不需要额外设置。但一个重要的坑如果数据集中90%都是负样本模型把每句话都判断成负向这个函数依然返回90%。所以在正式实验里我用sklearn的classification_report看Precision、Recall、F1from sklearn.metrics import classification_report def evaluate_report(model, loader, device): model.eval() preds_all, labels_all [], [] with torch.no_grad(): for x_batch, y_batch in loader: x_batch x_batch.to(device) logits model(x_batch) pred torch.argmax(logits, dim1).cpu().tolist() preds_all.extend(pred) labels_all.extend(y_batch.tolist()) print(classification_report(labels_all, preds_all, target_names[negative, positive])) return preds_all, labels_all说明classification_report会同时输出两类的精确率、召回率、F1。答辩时老师问你“模型到底行不行”你拿出F1而不是只报准确率至少说明你理解类别不平衡的影响。如果正类F1远低于准确率就要走第5章的加权loss。4.3 早停与模型保存给训练过程留一剂后悔药RNN训练里最常见的翻车场景是训练loss还在下降验证loss已经回升。你不可能每个epoch都手动守着所以用早停来自动保留最优模型。常见设定是连续3个epoch没有刷新最优验证指标就停止并把最优state_dict存盘best_val_acc 0.0 patience 3 wait 0 for epoch in range(30): train_loss train_one_epoch(model, train_loader, optimizer, criterion, device) val_acc evaluate(model, val_loader, device) print(fepoch{epoch1}, train_loss{train_loss:.4f}, val_acc{val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc wait 0 torch.save(model.state_dict(), best_model.pt) else: wait 1 if wait patience: print(fearly stop at epoch {epoch1}) break逻辑说明只有验证指标刷新时才保存模型所以磁盘上的best_model.pt始终对应验证集上表现最好的那一个epoch而不是最后一个epoch过拟合后的参数。patience3是保守值保证即使遇到一个坏epoch也不会立刻放弃。如果你的训练曲线浮动大可以把patience调到5但也要注意wait计数的起始逻辑别在第一个epoch就把模型存成一个低分。另一个细节保存state_dict而不是model。state_dict只含参数和张量加载时要求模型结构完全一致文件小且可移植直接torch.save整个model虽然方便但跨环境加载时经常因为类路径不一致报错。我一般只用state_dict。4.4 可视化训练曲线用 loss 而不是玄学判断欠拟合与过拟合模型训练不能只靠数字板。一个能拿高分的期末作业通常有train_loss和val_loss两条曲线。画图的代码如下import matplotlib.pyplot as plt train_losses [] # 由每次训练返回后追加 val_losses [] # 由验证函数计算后追加 plt.figure(figsize(8, 5)) plt.plot(range(1, len(train_losses) 1), train_losses, labeltrain loss) plt.plot(range(1, len(val_losses) 1), val_losses, labelval loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.show()参数说明plot里的label只影响图例显示两条曲线长度不一定要相等matplotlib按索引对齐即可。更精细的调整可以加linewidth、linestyle等但对作业来说默认样式已经够了。光有这条曲线还不够得会读。欠拟合的典型特征是两条loss都高悬不降说明模型容量不足或学习率太小过拟合的典型特征是train_loss持续下降但val_loss在第N个epoch开始掉头上升这就是早停要停止的位置。如果你发现loss曲线上下跳动得像心电图上床先减小lr到5e-4或者把batch从64调到32不要急着加层。如果val_loss很难看别慌再做一个预测样例的检查。随机挑20条验证集句子把真实标签和预测标签打出来人眼看一遍。这个“黑匣子透视”能发现一些指标看不出的问题比如模型把所有带“不”的句子都判成负向说明停用词处理或数据标注有问题这种问题可视化曲线是看不出来的。5. 避坑指南LSTM 中文短文本情感分析常见的 5 个坑5.1 标签不均衡90%的准确率是假象现象训练后验证准确率高达90%但随便拿几句真的好评来预测模型一律判负混淆矩阵里正样本的召回率只有0.1。原因数据集的负样本占了九成。模型只要全猜负就能拿到90%的准确率而你的训练逻辑却把这个分数当成了“优质模型”。交叉熵损失在类别不均衡下被多数类主导少数的正类样本几乎起不到学习信号。解决给CrossEntropyLoss加class_weight。先看2.1节统计的label分布假如正负比例是1:9就把权重设为[1.0, 9.0]意思是正样本预测错时罚9倍。代码改动极小class_weights torch.tensor([1.0, 9.0], dtypetorch.float, devicedevice) criterion nn.CrossEntropyLoss(weightclass_weights)参数说明不要机械地把权重设成9先计算比例再取一个略小于比例的权重比如5.0以免少数类被过度强调后把多数类压低。更稳健的做法是直接用第4.2节的F1做早停指标因为F1对少数类更敏感。5.2 截断长度过短转折结构里的核心情感被切没现象短句预测基本正确但“虽然贵但是味道真的不错服务也热情”被判成负向。原因max_len设得太小比如15分词后“但是味道真的不错”还没读到就截断了模型只能看到“虽然贵”负面信号占绝对优势。另一个可能虽然max_len50但由于你的预处理先做了停用词过滤“不”等词被删掉转折后的正面词也失去反转依据。解决先看2.1节的length.describe()结果取P90作为max_len如果你的语料是微博或外卖评论P90很可能在40到60之间设64更稳妥。不要在截断上抠性能短文本多padding一些位置对LSTM计算量影响很小但漏掉转折词是致命的。如果希望更精细可以保留前10词和后30词再拼接但这种策略会改变原文顺序对LSTM不友好所以我不推荐一上来就做截断拼接。5.3 预训练词向量越搞越差冻结embedding是常见误用现象用word2vec初始化Embedding后验证准确率反而不如随机初始化训练也慢。原因很多人把pretrained weight加载进来后又设置requires_gradFalse冻结了Embedding。冻结后LSTM只能学着组合静态词向量而这些通用词向量没有针对你的餐馆评论语料做适配于是模型反而被约束住了。另一个原因是加载时shuffle了词索引或者词表覆盖率太低大部分词落到unk。解决把embedding设为可训练加载的词向量只作为初始值如果你的数据规模太小可以选很小的学习率微调它。词表覆盖率在80%以下就别用预训练向量了直接随机初始化即可。这里有个经验中文短文本情感分析随机初始化的Embedding配合一个还行的语料规模往往已经能跑出足够好的效果不需要去网上找那种动辄几个G的词向量。对期末作业来说省下这些时间做实验分析分数更高。5.4 长度不齐直接进 DataLoader报错或训练龟速现象DataLoader第一个batch就报错类似“expected sequence of length 50 at dim 1, got length 63”或者某些torch版本下训练极慢。原因如果Dataset的__getitem__返回的是不等长的列表PyTorch会自动尝试把batch组装成张量组装失败就报错有些教程用collate_fn动态padding但你没写于是时灵时不灵。短文本项目里最可靠的方案是第2.3节的做法在构造数据集之前就统一padding成定长然后直接用TensorDataset。不要试图在DataLoader里做动态padding除非你要省内存且语料长度差异巨大但明显地差项目的复杂度不值得。解决检查你的X是不是一个形状为(N, max_len)的二维数组。用print(torch.tensor(X).shape)确认。如果是(N,)或者list of lists with varying lengths去跑一遍2.3的encode保证所有行定长。数据量不大时num_workers保持默认0即可多进程反而在Windows下容易出错。5.5 模型加载预测时 key 对不上state_dict 的存储与加载姿势现象训练结束第二天在另一个notebook里加载best_model.pt却报“Missing key(s) in state_dict”或“Unexpected key(s)”。原因模型类的结构变了例如vocab_size从10000变成5000或者你把LSTM从2层改成1层后忘了重训练就加载旧权重。此外GPU上保存的tensor与当前device不匹配时会在load_state_dict之前报device mismatch。解决每次加载前用完全相同的超参数重建模型对象然后用map_location指定到当前devicedevice torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMClassifier(vocab_sizelen(vocab)).to(device) state_dict torch.load(best_model.pt, map_locationdevice) model.load_state_dict(state_dict) model.eval()参数说明map_location的取值可以是cpu或cuda:0它会先把张量加载到指定设备避免device mismatch。这段代码还有一个隐含动作model.eval()必须在预测前调用否则模型残留训练模式的Dropout每次推理结果都不同看起来像模型不稳定。经验里还有一个老玄学在Windows上多进程训练代码必须放在ifname main:里否则会无限速启动数据加载线程这也是很多人被PyTorch坑过的地方。6. 进阶写一个可复用的预测函数并做错误分析6.1 一个可以直接交差的 predict 函数模型训练完后作业展示最好能在命令行甚至Jupyter里对一个句子实时预测。定义一个predict函数内部复用第2章的tokenize和encode逻辑def predict(text): tokens tokenize(text)[:max_len] ids encode(tokens) x torch.tensor([ids], dtypetorch.long).to(device) with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1) label torch.argmax(prob, dim1).item() return label, prob[0, 1].item() for s in [这家店的麻辣烫太好吃下次还来, 外卖送到时全撒了差评, 虽然贵但是味道很好]: label, pos_prob predict(s) print(s, -, 正向 if label 1 else 负向, f(pos_prob{pos_prob:.3f}))逻辑说明softmax把logits转成概率索引0是负向概率索引1是正向概率。predict返回两个值label是0或1pos_prob是预测为正向的概率便于在报告里展示置信度。参数说明如果要扩展成三分类需要把模型的num_classes改成3并把这行取正类概率的索引改成对应位置。一个小建议把这三句测试结果截图放进作业报告比只贴训练曲线更能让老师相信你的模型有真实理解力。第三句专门测转折也是答辩时老师喜欢问的场景。6.2 错误分析用混淆矩阵和坏 Case 决定下一步调哪里有了predict和验证集再做一次错误分析。常见的做法是调用4.2的evaluate_report得到混淆矩阵再把预测错误的样本按“真实为正向但预测负向”筛选打印。看10条坏Case后你会很快发现是哪类问题未登录新词、否定结构、反讽还是标签本身标错。这也决定了下一步是扩充词典、调整停用词还是增加数据。很多所谓“调参”都是白调因为问题根本不在模型。我的习惯是把错误样本记录为项目的一个副产品它对报告里的“不足与展望”很有价值。比如“我们发现模型对反问句‘你怎么还不倒闭’会产生误判计划引入扩充数据解决”。这一句话的含金量比吹十句95%准确率都高。回想我自己做这个方向时的教训曾花两天去调LSTM层数最后发现是数据里“不”被停用词表误杀。从此以后每次训练前打印20条预处理结果成了固定动作。希望这些内容能帮你少走这些弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表