资讯动态

基于LSTM的文本情感分析:正负8000条数据毕业设计实战

发布时间:2026/10/27 1:16:01 来源:尧图企业网站定制
简介这是一套面向计算机相关专业学生、教师及企业员工的LSTM文本情感分析完整项目可作为毕业设计、课程设计或作业参考。项目以中文积极与消极情感文本各约8000条为训练数据借助jieba分词完成预处理并基于LSTM搭建情感倾向识别模型覆盖数据加载、模型构建、训练、评估与预测全流程。压缩包共11个文件约6.63MB包含Python源码、h5模型文件、pkl词向量文件、yml配置、txt数据集、md说明文档及png结构图等其中README.md用于指导运行与使用范围。目前已有79人学习。读者可据此掌握中文分词、深度模型搭建与Python自然语言处理的关键技能理解从理论到实践的完整链路同时需注意仅供学习研究勿用于商业用途。1. 正负各8000条的情感分析项目为什么LSTM仍是毕业设计的安全牌做过毕业设计的人都有一个共识选题太新容易翻车太旧又怕答辩老师一句“这个三年前就做烂了”把你堵回去。基于LSTM的文本情感分析恰好卡在一个舒服的位置——它是深度学习入门最经典的序列建模任务正负面各8000条的数据集规模不大不小单卡甚至CPU都能跑完但里面该有的坑一个不少分词、词表截断、变长padding、过拟合、类别不均衡。把这套东西完整走一遍你对nn.LSTM、nn.Embedding、pack_padded_sequence这些API的理解会比看十篇教程都扎实。这篇文章面向的是要交毕业设计、或者想拿一个完整NLP小项目练手的同学。我会按“数据长什么样 → 模型怎么搭 → 训练怎么调 → 哪里最容易翻车 → 怎么把结果做得好看”的顺序讲代码可以直接抄参数我会说清楚每个值为什么这么设。正负面各8000条这个量级意味着你不需要GPU集群一台普通笔记本就能复现全部流程这对毕业设计来说太重要了——能跑通、能改、能讲清楚比什么都强。2. 数据集处理从原始文本到LSTM能吃的张量2.1 先搞清楚你的数据到底长什么样正负面各8000条合计16000条这是一个典型的二分类均衡数据集。但“均衡”只是数量上的实际拿到手你会发现几个问题有的样本只有三五个字“好评”“垃圾”有的样本两三百字有的带HTML标签残留有的夹杂emoji和全角标点。这些不处理后面padding和词表都会出问题。我一般会先写一个统计脚本把长度分布、字符集、标签分布摸清楚再决定截断长度和词表大小。这一步花十分钟能省后面两小时debug。import pandas as pd import matplotlib.pyplot as plt # 假设数据是两列text, label0负面 1正面 df pd.read_csv(sentiment_data.csv, encodingutf-8) print(总样本数:, len(df)) print(标签分布:\n, df[label].value_counts()) # 文本长度分布按字符数粗估 df[char_len] df[text].astype(str).apply(len) print(长度分位数:\n, df[char_len].quantile([0.5, 0.9, 0.95, 0.99])) # 画个直方图看看长尾 df[char_len].hist(bins50) plt.xlabel(char length) plt.ylabel(count) plt.show()这段代码的逻辑很直接先确认标签是不是真的1:1再看长度分布决定max_len取多少。经验上取95%分位数比较稳比如算出来95%的样本在120字以内那max_len128就够了再长的大部分是噪声截掉反而减少计算量。注意astype(str)这一步不能省真实数据里经常混进NaN或数字不转字符串后面分词直接报错。2.2 分词、建词表、转id三个必须按顺序做的动作中文情感分析绕不开分词。常见做法是jieba分词但毕业设计里我更推荐先做一个“字符级”的baseline再上词级。原因很简单字符级不需要分词器没有OOV未登录词问题16000条数据下字符级效果往往不比词级差多少而且代码少一半答辩时也好解释。不过既然标题写的是标准LSTM文本分类我还是按词级讲字符级你可以自己替换。流程是分词 → 统计词频 → 建词表含pad和unk→ 文本转id序列。import jieba from collections import Counter def tokenize(text): return list(jieba.cut(str(text).strip())) # 1. 全量分词 df[tokens] df[text].apply(tokenize) # 2. 统计词频保留出现次数2的词其余归为unk counter Counter() for tokens in df[tokens]: counter.update(tokens) MIN_FREQ 2 vocab_tokens [w for w, c in counter.items() if c MIN_FREQ] # 3. 建词表0pad, 1unk word2idx {pad: 0, unk: 1} for w in vocab_tokens: word2idx[w] len(word2idx) print(词表大小:, len(word2idx)) # 4. 文本转id def to_ids(tokens, max_len128): ids [word2idx.get(w, 1) for w in tokens][:max_len] return ids df[ids] df[tokens].apply(to_ids)这里有几个参数值得说清楚。MIN_FREQ2是为了砍掉只出现一次的词这类词在训练集里学不到有效表示留着只会撑大词表、增加过拟合风险。max_len128来自上一步的统计。pad固定为0因为后面Embedding的padding_idx0要靠它。unk固定为1所有词表外的词都映射到它。注意word2idx.get(w, 1)这个写法比先判断再取更简洁也不会漏。2.3 变长序列的padding与DataLoaderLSTM要求一个batch里所有序列等长所以要么统一pad到max_len要么用collate_fn做动态padding。前者简单后者省显存。16000条数据量不大我一般直接pad到固定长度代码少、不容易出错。import torch from torch.utils.data import Dataset, DataLoader class SentimentDataset(Dataset): def __init__(self, ids_list, labels, max_len128): self.ids_list ids_list self.labels labels self.max_len max_len def __len__(self): return len(self.labels) def __getitem__(self, idx): ids self.ids_list[idx] # pad到max_len pad_len self.max_len - len(ids) ids ids [0] * pad_len return torch.tensor(ids, dtypetorch.long), torch.tensor(self.labels[idx], dtypetorch.long) # 划分训练/验证集8:2 from sklearn.model_selection import train_test_split train_ids, val_ids, train_y, val_y train_test_split( df[ids].tolist(), df[label].tolist(), test_size0.2, random_state42, stratifydf[label] ) train_ds SentimentDataset(train_ids, train_y) val_ds SentimentDataset(val_ids, val_y) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse)stratifydf[label]保证训练集和验证集的标签比例一致这个在二分类里是基本操作不加的话万一验证集全是一类指标就没意义了。batch_size64是个保守值显存不够就降到32够就往上加对最终效果影响不大。shuffleTrue只在训练集开验证集必须关掉否则复现结果会对不上。3. LSTM模型搭建Embedding、LSTM、全连接三件套怎么配3.1 模型结构选型为什么用双向LSTM而不是单向单向LSTM只能看到当前词之前的上下文双向LSTM同时看前后文对情感分析这种“否定词在后面”的句子特别重要。比如“这个产品好得没话说”和“这个产品好什么好”关键信息都在后半句。双向LSTM的代价是参数量翻倍、训练慢一点但16000条数据完全扛得住。结构上就是三层Embedding把id转成向量LSTM做序列建模取最后一个时间步的输出接Linear做二分类。这里有个细节双向LSTM的输出要把正向最后一步和反向最后一步拼起来不能只取一个方向。import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim128, num_layers1, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM( input_sizeembed_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0 ) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_dim * 2, 2) # 双向所以*2 def forward(self, x): # x: [batch, seq_len] emb self.embedding(x) # [batch, seq_len, embed_dim] out, (h, c) self.lstm(emb) # out: [batch, seq_len, hidden*2] # 取最后一个时间步双向拼接 last out[:, -1, :] # [batch, hidden*2] last self.dropout(last) logits self.fc(last) # [batch, 2] return logitsembed_dim128和hidden_dim128是经验值词表几千到几万这个量级都适用。num_layers1是有意为之两层以上LSTM在小数据集上极易过拟合而且dropout参数在num_layers1时不生效PyTorch会警告所以我用条件表达式直接设0。padding_idx0让pad位置的embedding不参与梯度更新这是标准操作。out[:, -1, :]取的是最后一个时间步因为pad在尾部最后一个非pad位置之后全是padLSTM对pad的处理虽然会稀释信息但配合padding_idx基本可控。更严谨的做法是用pack_padded_sequence但代码复杂度上升毕业设计里固定长度pad够用。3.2 训练循环损失函数、优化器、学习率怎么定二分类用CrossEntropyLoss优化器用Adam学习率1e-3起步。这三个几乎是默认组合不需要纠结。真正要调的是epoch数和早停策略。from torch.optim import Adam from torch.nn import CrossEntropyLoss device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMClassifier(vocab_sizelen(word2idx)).to(device) criterion CrossEntropyLoss() optimizer Adam(model.parameters(), lr1e-3) def evaluate(model, loader): model.eval() correct, total 0, 0 with torch.no_grad(): for x, y in loader: x, y x.to(device), y.to(device) logits model(x) pred logits.argmax(dim1) correct (pred y).sum().item() total y.size(0) return correct / total best_acc 0 patience, wait 3, 0 for epoch in range(20): model.train() total_loss 0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() optimizer.step() total_loss loss.item() val_acc evaluate(model, val_loader) print(fepoch {epoch1}, loss {total_loss/len(train_loader):.4f}, val_acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pt) wait 0 else: wait 1 if wait patience: print(early stop) breakpatience3的意思是验证集准确率连续3个epoch不提升就停这是防止过拟合最省事的办法。torch.save只存state_dict不存整个模型对象这样加载时不受代码路径影响。注意model.eval()和model.train()的切换dropout和batchnorm在两种模式下行为不同忘了切会导致验证结果偏低。optimizer.zero_grad()必须在loss.backward()之前顺序反了梯度会累加。3.3 关键参数对照表参数推荐值作用调整方向embed_dim128词向量维度词表大→256数据少→64hidden_dim128LSTM隐藏层维度与embed_dim同量级即可num_layers1LSTM层数小数据集不要超过2dropout0.3防过拟合过拟合严重→0.5batch_size64批大小显存不够→32lr1e-3学习率不收敛→3e-4max_len128截断长度按95%分位数定这张表建议直接抄进你的实验记录里答辩时老师问“参数怎么定的”你能说出每个值的依据比背概念强得多。4. 训练过程避坑那些让准确率卡在0.5的常见问题4.1 现象loss不降准确率一直在0.5附近晃原因通常有三个。第一标签没对齐比如text和label两列在读取时错位了模型学到的是随机噪声。第二学习率太大1e-2以上Adam容易震荡。第三padding_idx没设pad位置的embedding参与训练把有效信息淹没了。解决先打印前10条(text, label)人工核对学习率降到1e-3或3e-4确认nn.Embedding(..., padding_idx0)写了。我遇到过最隐蔽的一次是CSV里有BOM头第一列列名变成\ufefftextdf[text]直接KeyError但有人用iloc取列就绕过了报错结果取错列。所以读CSV统一加encodingutf-8-sig。4.2 现象训练集准确率99%验证集只有70%这是典型过拟合。16000条数据对LSTM来说不算多hidden_dim256加两层LSTM参数量轻松上百万几个epoch就能把训练集背下来。解决先降模型容量hidden_dim从256降到128num_layers从2降到1再加dropout到0.5还不行就加权重衰减Adam(model.parameters(), lr1e-3, weight_decay1e-4)。另外检查一下训练集和验证集是不是同一个分布train_test_split的random_state固定后每次划分一致但如果你先划分再做了不同的预处理就会引入泄漏。4.3 现象验证集准确率波动大一会儿0.85一会儿0.72原因一般是验证集太小。16000条的20%是3200条其实不算小但如果你的batch_size64且验证集没打乱某些batch可能恰好全是一类导致单次评估偏差大。另外model.eval()忘了加也会让dropout在验证时生效结果随机。解决验证集评估前确认model.eval()并且用torch.no_grad()包住如果波动仍然大把验证集比例提到0.25或者做k折交叉验证取平均。毕业设计里5折交叉验证是个加分项代码量不大但能显著提升结果可信度。4.4 现象预测时所有样本都输出同一类这是“模型坍塌”通常发生在类别不均衡或者学习率过大的时候。虽然你的数据是1:1但如果你在DataLoader里不小心把标签转成了floatCrossEntropyLoss会报错或行为异常。解决确认标签是torch.long类型打印一下模型输出的logits如果两个类别的值几乎一样说明模型没学到东西回到4.1排查如果某个类别值恒大于另一个检查fc层输出维度是不是写成了1而不是2。4.5 现象CPU训练慢到无法忍受LSTM在CPU上确实慢16000条数据、max_len128、hidden_dim128一个epoch大概几分钟到十几分钟取决于CPU。如果只有CPU把max_len降到64batch_size提到128能快不少。解决优先用GPUColab免费版或学校实验室机器都行没有GPU就把num_layers1、hidden_dim64、max_len64三件套安排上准确率掉一两个点但能跑完。另外DataLoader的num_workers设成4或8Windows下设0能加速数据加载。5. 把结果做得能写进论文指标、可视化和可复现性5.1 别只报准确率混淆矩阵和F1一起上准确率在均衡数据集上够用但答辩老师更想看你对指标的理解。加一段sklearn的classification_report把precision、recall、F1都打出来再画个混淆矩阵工作量不大但论文里“实验结果与分析”那一章立刻充实了。from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns model.load_state_dict(torch.load(best_model.pt)) model.eval() all_preds, all_labels [], [] with torch.no_grad(): for x, y in val_loader: x x.to(device) pred model(x).argmax(dim1).cpu().numpy() all_preds.extend(pred) all_labels.extend(y.numpy()) print(classification_report(all_labels, all_preds, target_names[负面, 正面])) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabels[负面, 正面], yticklabels[负面, 正面])target_names按标签0、1的顺序写写反了报告里的类别就对不上。混淆矩阵用seaborn画比matplotlib省事annotTrue把数字标在格子里论文截图直接能用。5.2 固定随机种子让结果可复现毕业设计答辩时老师可能让你当场跑一遍如果每次结果不一样会很尴尬。在代码开头固定所有随机源import random, numpy as np, torch 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)cudnn.deterministicTrue会让GPU训练稍慢但结果可复现。benchmarkFalse同理。如果追求速度可以关掉这两个但论文里建议开着并在文中注明“固定随机种子结果可复现”。5.3 一个提升准确率的小技巧把pad位置的输出屏蔽掉前面取out[:, -1, :]其实取到了pad位置的信息。更干净的做法是用mask把pad位置的输出置零然后做平均池化或最大池化。平均池化通常比取最后一步更稳尤其是短文本多的时候。def forward(self, x): emb self.embedding(x) out, _ self.lstm(emb) # [batch, seq_len, hidden*2] mask (x ! 0).unsqueeze(-1) # [batch, seq_len, 1] out out * mask # pad位置置零 summed out.sum(dim1) # [batch, hidden*2] lengths mask.sum(dim1).clamp(min1) # 防止除零 pooled summed / lengths # 平均池化 pooled self.dropout(pooled) return self.fc(pooled)mask (x ! 0)利用pad的id是0这个事实unsqueeze(-1)把维度对齐到out。clamp(min1)防止全pad的极端样本导致除零。这个改动通常能带来1到2个点的准确率提升代码量却很小性价比很高。我一般会在baseline跑通后加上这个作为“改进点”写进论文。5.4 论文里怎么描述这套流程别写成“本文使用了LSTM模型”太单薄。按“数据预处理 → 词表构建 → 模型结构 → 训练策略 → 评价指标”五段写每段配一个参数表或公式。模型结构那段把Embedding、LSTM、Linear的维度变化写清楚比如“输入序列长度128经Embedding映射为128维向量双向LSTM隐藏层128维拼接后256维经Dropout后送入全连接层输出2类”。这样老师一看就知道是你自己跑的不是抄的。最后一章不写总结说个我自己的习惯每次跑完实验我会把word2idx、best_model.pt、训练日志、混淆矩阵图打包到一个文件夹命名带日期和参数比如lstm_emb128_hid128_lr1e-3_20240501。毕业设计改到第三版的时候你会感谢自己留了后悔药。这套东西值不值得做如果你要的是一个能跑通、能讲清楚、能改出花样的NLP入门项目它值得。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑