ARTICLE DETAIL

资讯详情

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

基于LSTM的中文文本情感分析毕业设计全流程指南

基于LSTM的中文文本情感分析毕业设计全流程指南 简介基于LSTM网络实现中文文本情感分析的高分毕业设计项目面向自然语言处理入门学习者与计算机相关专业学生解决中文评论或文本情感二分类问题。项目完整覆盖从中文分词到模型训练与预测流程使用jieba分词对积极/消极文本预处理利用Word2Vec将词语转为向量表示再输入LSTM网络训练分类模型数据集包含积极与消极样本各约8000条。压缩包共8个文件合计6.62MB以Python源码、训练好的LSTM模型与Word2Vec词向量、YAML配置、TXT文本数据集、Markdown说明文档为主覆盖代码、数据、模型与说明便于按需取用。目前已有379人学习下载。代码经测试可正常运行作者在答辩中获平均96分支持下载后远程咨询与教学。适合作为毕业设计、课程设计或课设作业的完整参考也可在此基础之上调整网络结构或数据集拓展到其他情感分析场景。1. 有8000条正向和8000条负向数据这个LSTM情感分析毕业设计该不该照做同样是“基于LSTM实现文本情感分析”的题目有人答辩拿到优秀有人却在“模型跑不动”和“指标上不去”之间反复折腾。差别几乎不在模型代码而在拿到这16000条数据之后你有没有能力把一条原始评论文本变成模型真正吃得了的序列再把训练好的模型保存成一个换机器也能复现的结果。这个题目能解决的问题很直接给你一批积极和消极各8000条的中文评论文本通过LSTM二分类模型做情感极性判断最终交付源码、文档、模型文件和一份稳定的验证指标。它适合两类人——第一类是毕业设计选了文本情感分析方向、需要快速跑通全流程的学生第二类是刚接触NLP、想用一个小型数据集完整走一遍数据清洗、分词、词表构建、模型训练、推理部署的开发者。先说结论值得做但你要把重心放在数据管线和验证策略上LSTM本身只是其中很小一部分。2. 把原始评论文本变成可训练样本清洗、分词、序列编码一条线拿到数据集第一步不是写模型而是先确认这16000条数据到底长什么样。很多人在这个环节翻车直接读进来就跑模型结果训练到一半发现标签错位、文本串行、编码乱码白白浪费时间。我一般会先花半天时间把数据管线搭扎实后面训练和调参才有意义。2.1 读取、标签映射与按比例划分先确认数据没白拿常见的数据组织方式是单个CSV文件每行包含text和label两列label可能是pos/neg也可能是1/0甚至可能混着中文标签“积极/消极”。拿到文件第一件事是打印类别分布和文本长度统计确认数据量和标题描述一致。这个动作能暴露问题比如积极和消极样本数不对等或者文件里混入了空行。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(data/sentiment.csv, encodingutf-8) print(df[label].value_counts()) # 确认两极样本数量 print(df[text].str.len().describe()) # 看文本长度分布确定max_len # 标签统一映射成 0/1负向为0正向为1 label_map {neg: 0, negative: 0, pos: 1, positive: 1, 消极: 0, 积极: 1} if df[label].dtype object: df[label] df[label].map(label_map) # 按比例切分stratify保证训练/验证/测试里正负比例一致 train_df, tmp_df train_test_split(df, test_size0.2, random_state42, stratifydf[label]) val_df, test_df train_test_split(tmp_df, test_size0.5, random_state42, stratifytmp_df[label]) print(len(train_df), len(val_df), len(test_df))这里有一个容易被忽略的关键点train_test_split的stratify参数必须传原始标签。如果不做分层抽样正负样本分配不均后面验证集的指标会有很大波动你甚至会把这种波动误判成模型问题。random_state固定为42保证每次运行切分结果一致这也是整个项目可复现的地基。如果你还没装pandas和scikit-learn先在终端把环境装好pip install pandas scikit-learn jieba torch装完再往下走。数据切分之后训练集大约12800条、验证集和测试集各1600条这个体量对LSTM来说已经足够训练出一个可用的情感分类器。2.2 清洗与分词去掉的是噪声留下的是特征中文文本进模型之前必须分词这是和英文最大的区别。英文按空格切就行中文需要jieba这类工具。但在分词之前要先做清洗。文本里的HTML标签、URL、多余的空白字符对情感判断没有任何贡献反而会让词表膨胀、训练变慢必须先去掉。这里要特别注意清洗函数和分词函数必须单独定义不能混在一个函数里写死因为后面做推理时还要原样调用它。import re import jieba def clean_text(text: str) - str: text re.sub(r[^], , text) # 去HTML标签 text re.sub(rhttps?://\S|www\.\S, , text) # 去URL text re.sub(r\s, , text).strip() # 合并多余空白 return text def tokenize(text: str) - list: return jieba.lcut(clean_text(text)) # 应用清洗和分词 df[tokens] df[text].apply(tokenize) print(df[tokens].head(3))一个值得斟酌的点是标点符号要不要去掉。比如“太棒了”里的感叹号其实是情绪增强信号保留对情感分析有帮助但会让词表里出现一堆单个标点token。我的常见做法是基础版本先去掉标点把“标点符号和表情作为情感增强特征”写进论文的后续改进点答辩时这是一个很好的延伸话题。另外停用词表不是必须的——LSTM能自己学到“的、了、是”这些虚词的上下文作用盲目删停用词反而可能破坏句子结构。你只需要保证文本里不混入爬虫留下的乱码和广告文本那才是真正的噪声。2.3 建词表、编码、填充PAD和UNK两个坑位一次讲清词表是整个模型大小的决定性因素。16000条短文本去重后可能有几万个词如果不做低频词过滤Embedding层会非常大训练也容易过拟合。常见做法是统计词频后只保留出现次数不少于2次的词。同时固定留出两个特殊token的位置索引0给PAD用来补齐序列长度索引1给UNK用来处理预测阶段遇到的新词。这两个位置的顺序不能乱因为模型加载时按索引查词表训练和预测用的是同一套映射。from collections import Counter all_tokens [token for tokens in df[tokens] for token in tokens] counter Counter(all_tokens) # 出现次数 2 才保留过滤低频噪声同时给PAD和UNK留出0和1 vocab {word: idx 2 for idx, (word, count) in enumerate(counter.items()) if count 2} vocab[PAD] 0 vocab[UNK] 1 def encode(text: str, vocab: dict, max_len: int 64) - list: tokens tokenize(text) ids [vocab.get(token, vocab[UNK]) for token in tokens] # 不在词表 - UNK ids ids[:max_len] # 超长截断 ids ids [vocab[PAD]] * (max_len - len(ids)) # 短则填充 return ids print(encode(这部电影真的太棒了, vocab, max_len10))max_len这个参数怎么定看df[text].str.len().describe()的输出结果取90分位左右的值。如果90%的文本长度在60字以内max_len设64就够如果文本偏长比如到120那max_len设128。设太短会截断大量有效信息设太长会让padding部分过多浪费算力还容易把模型带偏。编码完成之后文本就变成了一个固定长度的整数序列这个序列才能喂给Embedding层。注意vocab.get(token, vocab[UNK])这个写法保证了预测阶段遇到训练集没出现过的词时不会报KeyError这也是后面推理不崩的前提之一。接下来封装Dataset和DataLoader。数据集类返回两个东西编码后的整数序列和标签数据类型都要是torch.long因为Embedding层只接收LongTensor。import torch from torch.utils.data import Dataset, DataLoader class SentimentDataset(Dataset): def __init__(self, texts, labels, vocab, max_len): self.texts texts self.labels labels self.vocab vocab self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): x torch.tensor(encode(self.texts[idx], self.vocab, self.max_len), dtypetorch.long) y torch.tensor(self.labels[idx], dtypetorch.long) return x, y train_dataset SentimentDataset(train_df[text].tolist(), train_df[label].tolist(), vocab, max_len64) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue)DataLoader的shuffle参数在训练时一定要设为True否则每个epoch内样本顺序完全一样模型会顺着固定顺序学出虚假的模式。到这一步数据管线闭环了原始文本 → 清洗分词 → 词表映射 → 截断填充 → 批量加载。后面训练和推理共用这套逻辑我在项目里会把clean_text、tokenize、encode三个函数单独放进一个utils.py文件训练脚本和预测脚本都从同一个文件里import避免两套逻辑不一致的尴尬。3. 模型怎么搭基于PyTorch实现双向LSTM情感分类器的完整结构数据准备好之后下一步是模型。这里我见过的最大误区是一上来就套Bert或者注意力机制——不是不行而是16000条数据微调预训练模型性价比很低训练时间长、显存要求高、答辩时又讲不清内部机理。LSTM在这个数据规模下是恰到好处的选择结构直观、训练快、参数可解释性强而且能让答辩老师看到你真正理解“序列依赖”这个概念。3.1 为什么用LSTM而不是更强的模型16000条数据撑不起大模型微调很多同学一搜“lstm神经网络”跳出来的全是设备寿命预测、时间序列预测的博客以为LSTM只能处理数字序列这是典型的搜索误导。LSTM在文本分类里的地位虽然被Transformer取代了但它仍然是毕业设计阶段最适合讲清楚的循环神经网络结构。核心机制是门控输入门决定当前信息写入多少遗忘门决定历史信息保留多少输出门决定把什么状态传给下一个时刻。这种结构天然适合处理“虽然……但是……”这类转折关系——关键语义可能出现在句子后半段普通词袋模型会丢掉这部分信息。选LSTM不选BERT还有一个现实原因BERT微调至少需要一张像样的显卡而且默认分词器和中文文本不一定适配。反观LSTM用CPU就能完成16000条数据的训练一个epoch通常只要几十秒。这意味你可以反复调参而不需要每次改动都等半小时。更实际的好处是答辩好讲Embedding的词向量可以可视化LSTM的hidden state可以抽出来分析连梯度裁剪的作用都能讲得一清二楚。这些实验做出来论文的“分析与讨论”章节就有了实打实的内容。3.2 从Embedding到双向LSTM再到分类头核心模型代码模型结构分四块Embedding层把词索引映射成稠密向量双向LSTM提取正反两个方向的上下文特征Dropout做正则化最后的全连接层把特征压缩成2个类别的logits。一个细节是双向LSTM的输出处理bidirectionalTrue时最后一层的隐状态是一个(num_layers * 2, batch, hidden_dim)的张量前一半是正向最后一个时刻的输出后一半是反向最后一个时刻的输出。我一般取倒数两行然后拼接这样就同时拿到了“从前往后看”和“从后往前看”两个方向对整句话的总结。import torch.nn as nn class BiLSTMClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim100, hidden_dim128, num_layers1, num_classes2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.lstm nn.LSTM(embedding_dim, hidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_dim * 2, num_classes) # 双向所以乘2 def forward(self, x): emb self.embedding(x) # (batch, seq_len, embedding_dim) out, (h_n, c_n) self.lstm(emb) # out: (batch, seq_len, hidden_dim*2) last_fwd h_n[-2, :, :] # 最后一层正向隐状态 last_bwd h_n[-1, :, :] # 最后一层反向隐状态 h_cat torch.cat((last_fwd, last_bwd), dim1) # (batch, hidden_dim*2) logits self.fc(self.dropout(h_cat)) # (batch, num_classes) return logits模型参数怎么定embedding_dim取100够用中文情感分析不是复杂语义推理任务300维的词向量在这个数据量上不会带来明显提升。hidden_dim取128是平衡点太小欠拟合太大过拟合而且训练变慢。num_layers取1就够了很多人迷信层数但在16000条数据上两层LSTM的收益往往被过拟合抵消单层双向结构已经能捕捉正反两个方向的信息。padding_idx0这个参数很关键它告诉Embedding层索引0对应的向量始终是零向量且在训练中不更新这样padding部分就不会给LSTM带来虚假信号。如果这里漏了模型会在大量padding上浪费注意力训练指标也会变得很奇怪。3.3 训练循环与梯度裁剪一个能直接跑起来的train()骨架训练循环本身不复杂但有几个容易被忽略的设置。第一个是损失函数二分类用CrossEntropyLoss它会自动对输出做softmax不要再额外手动加softmax。第二个是优化器Adam配1e-3的初始学习率是文本分类最常见的起点。第三个是梯度裁剪LSTM在长序列上容易出现梯度爆炸clip_grad_norm_把梯度的二范数限制在5.0以内这是LSTM训练的常规操作也是它和普通前馈网络训练的一大区别。第四个是学习率调度器用ReduceLROnPlateau在验证集指标停滞时自动降一半学习率比手动调要省心得多。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model BiLSTMClassifier(vocab_sizelen(vocab)).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemax, factor0.5, patience1) for epoch in range(20): model.train() total_loss 0.0 for x_batch, y_batch in train_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() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() avg_loss total_loss / len(train_loader) print(fepoch {epoch 1} | loss {avg_loss:.4f})这里有个实操经验每个epoch结束都打印平均loss观察它的下降曲线。正常情况前3个epoch loss会快速下降从0.69附近降到0.3左右之后降速放缓。如果loss在0.69附近纹丝不动基本可以断定管线有问题先去查标签和文本是否对齐不要急着改模型。梯度裁剪的max_norm一般取5.0到10.0之间取太小会抑制正常学习取太大起不到防护作用。ReduceLROnPlateau的mode参数要设成max因为我们监控的是验证集准确率而不是loss准确率越高越好这个方向搞反了学习率永远不会降。4. 训练到验证集90%的调参路径学习率、早停、dropout与序列长度模型能跑起来只是第一步真正决定答辩分数的是你能不能稳定地把验证集指标做到一个能讲出口的数字。对二分类情感分析来说90%是一个分水岭——超过90%答辩老师会认为你的模型确实学到了情感特征卡在85%以下大概率会被追问“和朴素贝叶斯比有什么优势”。这一章讲清楚我从85%追到92%的调参路径。4.1 固定种子、设备与baseline先定“及格线”再谈“高分”调参之前先做两件事固定所有随机种子训练出一个简单的baseline模型。固定种子是为了让每次实验可复现——LSTM初始化、数据加载顺序、Dropout都涉及随机性如果不固定你调参时看到的指标波动里有一部分是随机噪声这会让任何对比都失去意义。这个环节很多人认为“太基础”就跳过了实际等到答辩现场复现时才发现跑出来的指标和报告对不上那就是血泪教训。import random import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)baseline的意义是给LSTM找一个比较对象。常见做法是用TF-IDF加逻辑回归这个组合在短文本情感分析上往往能到85%左右的准确率。如果LSTM经过调参后只比逻辑回归高1到2个点说明模型没有真正发挥出序列建模的优势。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline train_texts train_df[text].tolist() val_texts val_df[text].tolist() train_labels train_df[label].tolist() val_labels val_df[label].tolist() pipe make_pipeline(TfidfVectorizer(), LogisticRegression(max_iter1000)) pipe.fit(train_texts, train_labels) baseline_acc pipe.score(val_texts, val_labels) print(fTF-IDF LR baseline: {baseline_acc:.4f})我见过很多同学跳过这步直接训LSTM最后指标确实到了90%但答辩时老师问“凭什么比统计方法好”只能支支吾吾说“因为用了深度模型”。这就是典型的没有对照实验。baseline的价值不在于它有多强而在于它给LSTM的指标提供了一个解释框架如果LSTM是91%TF-IDF是86%那你能明确说出那5个点的增量来自序列信息。这个逻辑答辩时非常加分。4.2 三个必调参数max_len、batch_size、dropout的联动规律调参不是每个参数都调一遍而是抓住几个杠杆。我最先观察的永远是max_len。max_len设太小会截断关键语义比如“这部电影前半段无聊后半段绝了”这样的句子转折在后半段截断了模型就只看到“无聊”。max_len设太大会让padding占比过高注意力被稀释。正确做法是先看文本长度分布取90分位值作为初始设定再尝试±32的浮动对比验证集指标。如果max_len从64加到96后验证集提升超过0.5个点说明原始数据里被截断的有效信息不少值得继续加大试试。batch_size和dropout是一对配合使用的参数。batch_size太小比如16梯度估计的噪声会增大验证集损失曲线会出现明显震荡batch_size过大比如256训练收敛变慢而且显存压力大。64是16000条数据规模下的稳定起点。如果验证集指标出现震荡优先把batch_size调到64或128看是否稳定下来而不是急着调学习率。dropout是对抗过拟合最直接的旋钮。LSTM本身参数多单层双向结构的参数量也在百万级别16000条训练数据很容易让模型记住训练集而不是学出泛化规律。训练集99%、验证集88%就是过拟合的典型信号此时把dropout从0.3调到0.5再配合早停基本能把差距压缩到5个点以内。调参顺序我一般遵循一个原则先固定max_len和batch_size调出可用的baseline再动dropout和hidden_dim解决过拟合最后才用学习率和调度器做精调。4.3 训练日志、早停与模型保存再也不用担心训废了回去找参数很多同学训练时只有一个感受玄学。每轮loss在降但验证集指标忽高忽低好不容易一个epoch跑到92%下一个epoch又掉回88%。这时候最需要的是训练日志和早停机制。每轮记录train_loss、val_acc、lr、当前最优指标训练结束后回头看日志才能定位问题。早停的含义是验证集指标连续多个epoch没有刷新最高纪录就停止训练防止模型在过拟合阶段浪费算力。best_val_acc 0.0 patience 3 bad_epochs 0 for epoch in range(20): model.train() for x_batch, y_batch in train_loader: x_batch, y_batch x_batch.to(device), y_batch.to(device) optimizer.zero_grad() loss criterion(model(x_batch), y_batch) loss.backward() optimizer.step() # 验证阶段 model.eval() val_correct 0 with torch.no_grad(): for x_batch, y_batch in val_loader: x_batch, y_batch x_batch.to(device), y_batch.to(device) preds model(x_batch).argmax(dim1) val_correct (preds y_batch).sum().item() val_acc val_correct / len(val_dataset) scheduler.step(val_acc) if val_acc best_val_acc: best_val_acc val_acc bad_epochs 0 torch.save(model.state_dict(), bilstm_best.pt) # 只保存权重 print(fepoch {epoch 1} | val_acc {val_acc:.4f} | saved best) else: bad_epochs 1 if bad_epochs patience: print(fearly stop at epoch {epoch 1}, best val_acc {best_val_acc:.4f}) break模型保存有个关键细节只保存state_dict而不是整个模型对象这样模型结构变化后还能加载兼容的权重。配合前文固定种子、记录超参数的两个步骤整套训练流程真正做到了“每次实验都有据可查”。我在做文档说明时会切一个专门章节放超参数表把max_len、batch_size、hidden_dim、dropout、learning_rate、best_val_acc这六个值全部记录在表格里既方便自己复盘也方便答辩评委快速理解实验配置。文档里再顺手记录训练时间和机器配置比如CPU训练一个epoch时间、GPU训练时间、显存占用这些信息在答辩的“实验环境”章节是必须的现在记下来后面能省很多事。5. 避坑指南LSTM情感分析从数据到答辩的五个翻车现场这一章把我见过的翻车现场集中整理成五条踩坑记录每条都是“现象 → 原因 → 解决”的结构。这些坑几乎不会同时出现但只要中一个就够你折腾一两天。5.1 训练loss卡在0.693附近先怀疑标签和文本没对齐现象loss从第一个epoch开始就稳定在0.69左右十几个epoch之后仍然纹丝不动训练集和验证集准确率都接近50%约等于随机猜。原因二分类交叉熵随机猜测的期望loss值就是ln2约等于0.693所以这个数值是模型什么都没学到的信号灯根源通常在数据管线而不是模型代码——标签和文本错位了或者DataLoader里shuffle之后标签没有跟着文本一起打乱。解决办法是把训练脚本里构建训练集的部分单独拎出来打印DataLoader返回的第一个batch人工检查文本内容与标签是否对应。我在2.1里用stratify切分数据后会把训练集保存成一个独立的CSV文件后续每次跑实验都从这个文件读就是为了避免每次重跑都要重新检查一次数据对齐。5.2 验证集指标忽高忽低loss曲线像锯齿先调batch_size和learning_rate现象训练到第5轮验证集准确率到了89%第6轮掉到84%第7轮又回到88%曲线锯齿状明显无法判断模型到底有没有在收敛。原因batch_size太小导致梯度估计方差过大或者learning_rate太高让参数在最优解附近来回震荡又或者是验证集太小本身就有波动。解决办法先确认验证集有没有用stratify划分——验证集只有1600条时4%的波动也就是60多条样本属于正常噪声之后把batch_size提到64或128learning_rate从1e-3降到5e-4看曲线是否变得更平滑。一个小技巧是记录loss的滑动平均而不是原始值在打印日志时用avg_loss 0.9 * avg_loss 0.1 * current_loss锯齿会被抹平很多。5.3 训练集99%、验证集88%过拟合的三个信号与对策现象前几个epoch训练集和验证集指标一起涨第6个epoch之后训练集逼近99%验证集却停滞在88%不再上涨两个指标之间的差距越拉越大。原因这是过拟合的标准信号LSTM参数量大16000条训练数据在模型看来还不足以约束所有参数。解决办法把dropout从0.3加到0.5观察验证集是否回弹再把hidden_dim从128降到64牺牲一点模型容量换取泛化能力最后打开early stopping让模型在验证集指标不再刷新时立即停止避免后面的epoch纯粹在恶化泛化性能。我在这个场景里的经验是单层双向LSTM加dropout0.5在这个数据规模下几乎不会出现过拟合失控的情况如果仍然失控就要回头检查是不是词表里混入了太多只在单条文本出现的垃圾token。5.4 换一台机器就复现不出结果权重要和词表一起打包保存现象训练好的模型在自己电脑上验证集准确率92%把代码和权重文件发给同学后对方跑出来的结果变成85%甚至直接报错。原因模型加载时只用到了权重但词表、max_len、label_map这些预处理配置没有同步保存。对方电脑上重新构建词表时vocab的token到索引的映射顺序和原来完全不同同一个词在两端被编码成不同的整数权重自然对不上。解决办法保存模型时把词表和超参数一起打包成一个字典文件而不是只保存state_dict。这个问题的本质是“模型权重”和“数据管线”必须绑定运输我在第6章会给出一个完整的打包方案这里先记住结论只保存权重文件等于丢了半套项目。5.5 答辩被问“和朴素贝叶斯比有什么优势”答不上来没有提前准备对照组现象指标做到了91%答辩时老师问了一句“你的模型和朴素贝叶斯或者逻辑回归相比到底强在哪”当场愣住只能回答“因为用了更深的网络”。原因这不是知识问题而是实验设计问题——只在LSTM一个模型上修炼没有建立比较的参照系回答时自然没有数据支撑。解决办法在4.1里跑通TF-IDF加逻辑回归的baseline把两个模型在测试集上的准确率、F1值、训练时间列成一张对照表然后从三个角度组织回答第一LSTM通过Embedding学到了词与词之间的语义相似性而词袋模型把每个词当成独立符号第二LSTM能建模“虽然……但是……”这样的长距离转折依赖词袋模型只看词频统计第三双向结构让模型同时利用上文和下文信息这是一个词袋模型完全不具备的机制。这三个点讲清楚技术深度直接体现在答辩得分上。6. 让训练好的模型落地可用模型保存、加载与单句推理对齐模型训练完最后一个环节是把训练好的模型包装成一个能对新文本预测的函数同时把词表、label_map、max_len全部打包保存。这个环节最容易出的问题就是训练和预测用的预处理逻辑不一致——训练时用了jieba分词预测时忘了import训练时max_len是64预测时改成128结果padding长度不一样喂给模型的序列长度就变了。我的习惯是把第2章的clean_text、tokenize、encode统一放到一个utils.py文件里训练脚本和预测脚本都从它import永远不写第二份预处理逻辑。def predict(text: str, bundle: dict) - tuple: vocab bundle[vocab] max_len bundle[max_len] model BiLSTMClassifier(vocab_sizelen(vocab), embedding_dim100, hidden_dim128, num_layers1, num_classes2, dropout0.3) model.load_state_dict(bundle[model_state]) model.eval() ids encode(text, vocab, max_len) x torch.tensor([ids], dtypetorch.long).to(device) with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1) pred_idx int(prob.argmax(dim1)) label bundle[label_map][pred_idx] return label, float(prob[0, pred_idx]) bundle { model_state: torch.load(bilstm_best.pt, map_locationdevice), vocab: vocab, max_len: 64, label_map: {0: 消极, 1: 积极}, } print(predict(这部电影的剧情很紧凑演员演技在线, bundle))预测阶段的三个对齐点值得确认一下。第一词表必须和训练时完全一致包括PAD和UNK的索引位置第二encode里的max_len必须和训练时相同截断和填充的逻辑也会影响最终预测第三model.eval()一定要调用它会关闭Dropout否则每次预测的结果会有随机波动。把这个predict函数和bundle打包到同一个脚本里就能对一个数据集之外的新句子做预测了。这样毕业设计的交付物才完整你能跑通训练、能复现指标、能对新样本预测每一步都有代码支撑。最后交项目之前我会拿20条不在数据集里的新鲜评论跑一遍预测人工检查错判的案例。这一步的收获往往比训练过程更大。比如有一次我发现模型把所有带“无语”的句子都判成消极但“剧情无语伦比的精彩”实际是积极这暴露了分词和上下文建模的边界值得写进论文的“失败案例分析”部分希望帮到你。本文还有配套的精品资源点击获取
返回列表