
简介面向计算机相关专业期末大作业的 Python 项目基于 LSTM 模型完成用户评论情感趋势的分析、预测与可视化可作为课程设计或综合实践参考。项目曾作为大三期末大作业提交经导师指导并获 98 分高分适合正在选题或需要完整流程借鉴的学生。压缩包共 24 个文件、约 793KB主要包含 2 个 ipynb 与 2 个 py 源码、5 个 CSV 数据集、5 个 txt正面/负面情感词典、评价词、停用词表、4 个 PNG 可视化结果图词云、热力矩阵、饼状图另有项目说明、图片与 XML/IML 工程配置文件结构清晰便于按模块复用。已有 136 人学习浏览。读者可从中获得从数据处理、LSTM 模型训练到情感趋势可视化的一整套实现包括情感分类语料、停用词表及可直接运行的脚本有助于理解评论情感分析的关键流程也为同类大作业提供可扩展的改造蓝本。1. 用户评论情感趋势为什么 LSTM 是期末项目里最值钱的选择期末做 Python 大作业如果选了「基于 LSTM 模型对用户评论情感趋势的分析与预测可视化」这个方向你最后要交的不是一个分类器而是一条能讲完整故事的流水线把几千条用户评论洗干净让 LSTM 学会打情感分再把情感分按时间聚合成曲线用另一套 LSTM 预测未来走势最后把所有证据画成图。这个选题在课程设计里性价比很高因为它同时踩中自然语言处理和时间序列预测两个考点可视化又能撑起答辩展示面。适合两类人手头需要可复现课程设计源码的学生以及想把零散评论变成可量化业务指标的产品或运营同学。一个反直觉的结论是LSTM 本身反而是最标准、最不容易翻车的部分真正卡人的是标签口径、时间对齐和数据泄漏。2. 评论数据清洗与向量化把中文评论变成 LSTM 能吃的序列评论数据是整个项目的地基地基没打对后面 LSTM 调得再好也白搭。数据来源常见两条路自己写爬虫去电商或视频平台抓取评论评论自带星级和时间正好当弱标签或者使用公开的中文评论数据集。不管哪条路落地时一定保留三列评论文本、评分1-5 或正负、发布时间。很多同学只盯着文本把时间和评分丢掉等到做趋势分析和预测时才发现无米下锅只能回头重跑采集流程。爬虫部分这里不展开但要注意评论接口通常有反爬和频率限制抓取频率调低、断点续爬这种基本功得先做好。2.1 情感标签怎么定五档还是二分类情感分析本质上是一个文本分类任务标签定义决定了模型学什么。最常见的做法有两种。二分类把评分大于等于 4 的评论视为正向小于等于 2 的视为负向3 分的评论直接丢掉。理由是 3 分评论的表述通常是「还行」「一般吧」情感极性很弱硬塞进正类或负类都会引入噪音模型学起来两头不讨好。五分类把 1-5 星直接映射成五个情感强度等级信息量更大答辩时能讲的东西更多但类别之间的边界也更模糊训练难度明显高于二分类。我一般会采用「五分类训练、二分类汇报」的折中方案模型先做 5 档预测评估时再把 1-2 折成负向、4-5 折成正向、3 分单独作为中性这样既保留了细粒度的情感强度信息又能在答辩时给出通俗易懂的准确率。如果抓来的评论没有评分字段常见做法是先用 SnowNLP 跑一遍粗标签再人工抽几百条修正错误这样生成的训练集虽然质量一般但足够支撑一个 5000 条以内的课程项目。有一点值得写进报告3 分评论的丢弃策略要在报告里写明评阅老师看到这种处理细节比看到模型结构更愿意给分。2.2 清洗与分词去掉评论里的噪音比调参更提分LSTM 吃的是 token 序列评论里的 HTML 标签、URL、用户、连续重复字符都会变成无效 token拉长序列长度、稀释情感信号。清洗顺序固定先去 HTML 转义和标签再去 URL 和 提及然后压缩连续重复字符把「哈哈哈哈哈哈哈哈」压成「哈哈」最后统一全半角。这个压缩步骤很小但很关键如果不做一条「好好好好好好好」的评论会被切出 7 个重复 token白白占掉 max_len。分词用 jieba 的 lcut 即可需要加自定义词典把产品名、品牌名切成一个整体比如「戴森吹风机」默认会被切成「戴森」和「吹风机」加入自定义词典后变成一个词对后续情感判断更友好。注意停用词表里一定要人工剔除「不、没、别、莫」这类否定词。把「不好」切成「好」等于把一条差评改成了好评标签这种数据错误是任何模型都救不回来的。分词完成后构建词表。词表里必须预留两个位置PAD和UNK分别对应填充符和未登录词。max_len取序列长度分布的 95 分位中文电商评论一般落在 50-100 之间超过就截断不足就补PAD。代码可以这样组织import re import jieba from collections import Counter def clean_comment(s: str) - str: s re.sub(r[^], , s) # 去 HTML 标签 s re.sub(rhttps?://\S, , s) # 去 URL s re.sub(r\S, , s) # 去 用户 s re.sub(r(.)\1{3,}, r\1\1, s) # 连续重复字符压缩 return s.strip() def tokenize_and_build_vocab(texts, max_vocab30000, min_count1): counter Counter() for t in texts: for w in jieba.lcut(clean_comment(t)): counter[w] 1 vocab {PAD: 0, UNK: 1} for w, c in counter.most_common(max_vocab - 2): if c min_count: vocab[w] len(vocab) return vocab这段代码把清洗、分词和词表构建一次做完。clean_comment里的正则顺序是固定的先清结构再清噪音否则 URL 里的符号会干扰后续规则。(.)\1{3,}这个正则是把任意连续出现 3 次以上的字符压成 2 个只压缩 4 个及以上连续重复避免误伤「哈哈」这类双字正常表达。max_vocab30000对课程数据是足够的上限中文评论的常用词表通常在 2 万到 3 万之间饱和min_count1表示出现 1 次就保留适合几千条的小数据数据量如果超过 2 万条可以把min_count调到 2 或 3 来压掉噪音词。2.3 Embedding随机初始化、Word2Vec 还是预训练词向量Embedding 层的作用是把 token id 转成稠密向量常见有三种做法。随机初始化Embedding 矩阵随机生成在训练中自己学语义适合 2 万条以内的数据课程作业选这个最省事。Word2Vec用 gensim 在自己爬的评论语料上训练词向量vector_size100, window5, min_count2然后用训练好的向量初始化 Embedding 权重并在训练中微调比随机初始化能少跑几十个 epoch但小语料提升有限。预训练词向量腾讯或搜狗的中文词向量效果最好但文件动辄几个 GB加载成 numpy 矩阵非常吃内存期末答辩时演示容易把机器跑崩这个翻车场景我见过太多次。实操建议是课程项目选随机初始化但要在报告里写明「为什么不直接用 Word2Vec」。理由其实站得住脚评论是强领域语料电商评论里的「客服」「差评师」这些词在通用语料里语义权重不高随机初始化让模型在训练中自己调整这些词的向量反而更贴合当前领域。如果坚持用 Word2Vec有一个坑要避开未登录词OOV的向量不能给全零否则 LSTM 对没见过的新词一律无反应相当于这个词直接消失。正确做法是给 OOV 一个以零为中心的小范围随机向量比如np.random.uniform(-0.05, 0.05, size100)。提示词表和向量配置要序列化成文件保存预测阶段直接读取不要每次重新构建词表。词表 id 对不上是模型部署时最隐蔽的 bug 之一。3. 用 LSTM 搭建情感分类器模型结构、六个必调参数与训练流程LSTM 能成为评论情感分析的主流选择靠的是门控机制遗忘门决定丢掉哪些旧信息输入门决定写入哪些新信息输出门决定当前时刻放出什么。评论里最常见的「一开始觉得一般后来发现真香」这种转折信息词袋模型完全看不出来因为词袋把两个半句拆散混在一起LSTM 的门控能把「后来发现」这几个字记住并在句尾输出时把它融合进情感判断。课程作业不要求你发明结构但答辩时能把门控机制讲清楚比背 LSTM 公式管用得多。3.1 单层还是双向先定结构再定参数模型结构的选择取决于数据量。几千到两万条评论的规模下单层 LSTM 是稳妥的默认值。双向 LSTM 能看到后文信息对短文本分类通常有 2-3 个点的提升但参数翻倍小数据上更容易过拟合。如果训练集只有三四千条老老实实用单层。整体结构按这个顺序搭Embedding 层padding_idx0→ LSTM 层batch_firstTrue→ 取最后一个时间步的 hidden state → Dropout → 全连接层 → 输出。取最后一个时间步而不是对所有时间步做平均池化是因为评论情感往往由收尾几句决定「总之不推荐」「但整体很满意」这类信号都在句尾最后的 hidden state 天然聚合了整句话的信息也更贴近人类的阅读习惯。import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim128, hidden_size128, num_layers2, num_classes5, dropout0.4): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.lstm nn.LSTM(embedding_dim, hidden_size, num_layers, batch_firstTrue, dropoutdropout) self.classifier nn.Sequential( nn.Dropout(dropout), nn.Linear(hidden_size, num_classes) ) def forward(self, x): emb self.embedding(x) # x: (B, T) - emb: (B, T, E) out, (h_n, _) self.lstm(emb) # out: (B, T, H) last_h h_n[-1] # 取最后一层最后时间步 return self.classifier(last_h) # (B, num_classes)这段代码里有两个容易被忽略的细节。batch_firstTrue让输入形状从(seq_len, batch, embedding_dim)变成(batch, seq_len, embedding_dim)新手排错时不用算维度下标强烈建议打开。h_n的形状是(num_layers, batch, hidden_size)h_n[-1]取的是最上层 LSTM 的最后一个时间步输出这是out[:, -1, :]的等价写法。注意 forward 里不要手动加 softmaxCrossEntropyLoss内部会做 log_softmax 加 NLLLoss手动加了反而会得到错误的置信度。参数上num_layers1时 PyTorch 的 LSTM 会忽略dropout参数这是框架的规则不是你的 bug。3.2 六个影响收敛的参数与推荐值参数调优是课程作业里最耗时的环节下面这张表是按评论分类场景整理的首轮推荐值。参数推荐值主要影响embedding_dim128太小语义表达弱太大训练变慢hidden_size128小于 64 欠拟合大于 256 小数据过拟合num_layers1-2超过 2 层小数据梯度信号难回传dropout0.4防过拟合数据越少越要开batch_size32 或 64影响 loss 曲线平滑度和显存占用learning_rate1e-3 起步1e-2 容易震荡不收敛这些参数不是拍脑袋定的。评论数据是短文本单条序列长度一般不超过 100hidden_size128已经能承载整句语义数据集只有几千条时num_layers超过 2 层会让梯度在反向传播中逐层衰减第一层几乎学不到东西训练曲线表现为 loss 降得很慢。dropout放在 Embedding 之后和全连接之前都行但注意它只在训练时生效评估时要切换model.eval()。优化器用 AdamW 而不是普通 Adamweight_decay1e-4对文本分类更稳。学习率建议配合 StepLR 衰减每 2 个 epoch 乘 0.5前几个 epoch 用 1e-3 快速逼近后面用 5e-4 慢慢收敛。3.3 训练流程时间切分、早停与检查点这个项目有一个和其他分类任务截然不同的要求训练集和验证集不能随机打乱必须按评论发布时间切分。常见做法是前 80% 按时间排序的评论进训练集后 20% 进验证集。原因很简单后面要做的趋势预测是时间序列任务如果训练时把时间顺序打乱模型就会提前看到未来的评论分布趋势预测部分的验证就失去说服力。这个切分方式在报告里写清楚评阅老师会觉得你的项目意识到位。训练循环里加两个保险梯度裁剪和早停。import torch from torch.nn.utils import clip_grad_norm_ optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size2, gamma0.5) criterion torch.nn.CrossEntropyLoss() best_val_loss float(inf) patience 0 for epoch in range(15): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() out model(batch_x) loss criterion(out, batch_y) loss.backward() clip_grad_norm_(model.parameters(), 1.0) # LSTM 防梯度爆炸 optimizer.step() scheduler.step() model.eval() val_loss 0.0 with torch.no_grad(): for batch_x, batch_y in val_loader: out model(batch_x) val_loss criterion(out, batch_y).item() val_loss / len(val_loader) if val_loss best_val_loss: best_val_loss val_loss torch.save({model: model.state_dict(), vocab: vocab}, best_model.pt) patience 0 else: patience 1 if patience 5: print(fEarly stop at epoch {epoch}) break这段循环的顺序不能乱optimizer.zero_grad()必须在backward()之前否则梯度会累加clip_grad_norm_必须在backward()之后、optimizer.step()之前。梯度的 L2 范数裁剪到 1.0是 LSTM 长期依赖场景下防梯度爆炸的后悔药不裁剪的话训练后期 loss 可能突然跳到 NaN。patience5表示验证集 loss 连续 5 个 epoch 不下降就停止保存的best_model.pt里同时打包了词表预测新评论时加载同一个文件保证 id 映射一致。如果数据集超过 2 万条patience可以放宽到 7。4. 从情感得分到趋势预测时间聚合、滑动窗口与方向准确率模型学会打情感分之后标题里的「趋势分析与预测」才开始落地。这一步把每一条评论变成每一天的情感曲线再用 LSTM 对曲线做时间序列预测。很多人的课程作业做到分类就停了能往下走出这一步的答辩效果完全不同。血泪经验先跟 naive baseline 对比不然可能白调了一周参数还不知道模型根本没学到东西。4.1 情感得分聚合评分数变成时间序列每条评论经过分类器会得到一个情感得分常见有两种口径。分类概率取正向类别的 softmax 概率得到一个 0-1 之间的连续值直观但偏保守。五分类期望值把五个类别的 softmax 概率按 1 到 5 求加权平均得到一个 1-5 之间的情感强度值更平滑而且保留了中性位置我一般用这个。然后按时间聚合三种方式按天简单平均把当天所有评论的情感得分取均值实现最简单但评论量小的天波动会非常剧烈评论量加权平均用评论数作为权重评论多的天更可信加权加插值先过滤掉评论数少于 5 条的天再用前后均值填充缺口。聚合之后还要做两步。第一步是重采样成连续时间索引用 pandas 的resample(D)缺失日期补前一天的值否则时间序列中间断档LSTM 会把断档当成正常的时间间隔。第二步是滑动平均平滑窗口设 3 或 7把短期的噪声抹掉。这里的窗口大小要写在报告里。一个常见误用是直接用三天移动平均后的序列去预测但移动平均本身有滞后性预测出的转折点会比真实滞后一天到两天。4.2 滑动窗口构造训练样本窗口、步长与数据泄漏有了情感得分时间序列趋势预测就变成一个监督学习任务。用过去window天的得分预测未来第horizon天的得分常见配置是window7, horizon1即用一周数据预测明天。构造样本时第 i 个样本的输入是score[i:iwindow]标签是score[iwindowhorizon-1]。这个逻辑写成函数import numpy as np def build_dataset(score_seq, window7, horizon1): X, y [], [] for i in range(len(score_seq) - window - horizon 1): X.append(score_seq[i:i window]) # 过去 window 天 y.append(score_seq[i window horizon - 1]) # 未来第 horizon 天 return (np.array(X, dtypenp.float32).reshape(-1, window, 1), np.array(y, dtypenp.float32))返回的 X 形状是(样本数, window, 1)三个维度分别表示样本数、时间步数、特征数。LSTM 要求输入必须是三维张量第三维是 1 说明这是单变量时间序列也就是只用情感得分这一个特征预测未来。如果想加评论量或者价格作为第二路特征把最后一维改成 2但课程项目通常单变量就够。window取值要看业务周期电商评论有明显的一周周期工作日和周末评分分布不同取 7 最合理如果数据只有十几天window3也能跑。滑动步长默认是 1样本最密集步长设成 7 则样本之间完全独立能降低相邻样本的强相关性避免模型过拟合到重复模式。注意构造样本时绝对不能把第iwindowhorizon天及以后的数据放进输入。一个隐蔽的翻车位是先对整个序列做移动平均再切窗口如果移动平均窗口包含了未来值就等于是把未来信息提前喂给了模型。先切窗口再平滑或者确认平滑是因果的只回溯不前瞻顺序别搞反。4.3 预测评估MAE、RMSE 之外还要看方向准确率趋势预测项目里用户最终关心的是「明天情绪是升是降」不是误差小数点后几位。因此评估要三个指标一起看MAE平均绝对误差看整体偏差RMSE均方根误差放大大的误差点方向准确率看预测的涨跌方向是否与真实一致。方向准确率的计算容易被忽略但恰恰是最能说明模型价值的指标。def evaluate_trend(y_true, y_pred): mae float(np.mean(np.abs(y_true - y_pred))) rmse float(np.sqrt(np.mean((y_true - y_pred) ** 2))) direction float(np.mean( np.sign(y_pred[1:] - y_true[:-1]) np.sign(y_true[1:] - y_true[:-1]) )) return mae, rmse, directionnp.sign会把预测增量和真实增量的符号提取成 -1、0、1两者相等说明方向判断正确。方向准确率超过 0.5 才有意义低于 0.5 说明模型连「升还是降」都猜不对这时先别调参回去检查窗口序列有没有泄漏。还必须跑一个 baseline 对比用上一期的真实值直接作为本期预测naive 模型。很多平滑的情感序列用 naive 就能拿到很低的结构误差如果 LSTM 没超过 naive不是代码 bug而是这个任务本身太平滑、不需要神经网络或者窗口设得太长引入了无用历史。报告里同时放 LSTM 和 naive 两个结果评阅老师会觉得你的评估是诚实的。5. LSTM 情感项目避坑手册五条高频翻车点与排查方法下面的内容来自实际操作中反复踩过的坑按现象、原因、解决三段式列出。模型是黑匣子但我们排错时不需要猜按这三步走能省下不少冤枉时间。5.1 五条高频踩坑记录第一条训练 loss 降不下去准确率停在多数类比例。现象是训练了几个 epoch 后 loss 几乎不变准确率恰好等于训练集中占比最大的类别比例。原因是情感标签的类别不均衡比如五星评论占 60%模型学到了「全猜五星」的偷懒策略。解决给CrossEntropyLoss传入class_weight让少数类的错分惩罚更大或者用WeightedRandomSampler在 DataLoader 里重采样。处理不均衡这段要在报告里写这是评阅老师很看重的数据意识。第二条验证集指标不错拿新评论一预测全是负面。现象是验证集 F1 值有 0.85但随便抽几条真实新评论模型几乎全部判负。原因是训练和预测时的预处理流程不一致比如训练用了 jieba 分词和清洗函数预测时却直接传了原始文本或者词表里没有UNK新词被直接忽略。解决把清洗、分词、id 映射封装成一个函数训练和预测都调用同一个函数词表构建时预留UNK位置训练时以一定概率把高频新词替换成UNK让模型见过未知词。第三条预测曲线比真实曲线整体滞后一天看起来像昨天值的平移。现象是预测曲线和真实曲线形状几乎一样但错开一个周期这通常是两个原因之一模型学到了 naive 拷贝逻辑尤其是序列平滑时或者构造样本时不慎把当天的均值也放进了输入相当于给了一点未来信息。解决先检查窗口边界确认没有泄漏再对序列做一阶差分预测delta而不是原始值评估时把预测的 delta 累加回去还原曲线。最后一定要跟 naive baseline 对比如果 LSTM 和 naive 的误差差不多说明任务本身的信号太弱。第四条matplotlib 画中文标题和坐标轴变成方块。现象是图上的中文全部显示为空心方块负号也变成了一个奇怪的横杠。原因是 matplotlib 默认字体 DejaVu Sans 不支持中文而且 SimHei 字体下负号会异常显示。解决在绘图脚本开头加两行plt.rcParams[font.sans-serif] [SimHei]和plt.rcParams[axes.unicode_minus] False。第二个设置很容易漏掉坐标轴上出现负数时照样是方块我见过不少人只设了中文字体没管负号最后曲线图上的负刻度全部乱码。第五条CPU 上训练一个 epoch 要十几分钟。现象是明明数据量只有一万条训练却慢到没法调试。原因是max_len截到了 500、词表开了 10 万、LSTM 堆了三层把算力都浪费在长度离谱的序列上。解决统计序列长度分布取 95 分位作为截断值中文电商评论一般 80-100 足够词表按min_count2截到 3 万以内层数降到 1-2 层。数据量小的时候LSTM 的训练耗时主要取决于序列长度而不是参数量把序列截短是最快的加速方式。5.2 复现性玄学为什么两次训练结果不一样现象是同一份代码同一个数据集跑两次准确率差两三个点loss 曲线也不完全一样。原因是 DataLoader 的 shuffle、GPU 上卷积和 LSTM 算子的非确定性、以及随机初始化的波动共同作用的结果。模型是黑匣子但随机种子是后悔药。解决在训练脚本开头固定三个随机源torch.manual_seed(42)、np.random.seed(42)、random.seed(42)如果用了 GPU 还要加torch.backends.cudnn.deterministic True。另外保存 checkpoint 时把词表、配置参数一起打包文件名带上日期。答辩时如果老师要求重跑一遍固定种子能保证结果和报告一致这个细节很加分。5.3 没有 GPU 的兜底方案TF-IDF 与 LSTM 特征拼接现象是实验室机器没有 GPULSTM 每个 epoch 要跑很久而且小数据下老是欠拟合老师催着要看效果。原因不是模型写错了是算力不够支撑在有限时间内把神经网络训充分。解决先跑一个 TF-IDF 加逻辑回归的 baselinesklearn 里十分钟出结果得到一组不依赖神经网络的分类概率再把 TF-IDF 向量和 LSTM 最后一个时间步的 hidden state 拼接起来过一层全连接层。这样的混合模型比纯 LSTM 收敛快、指标高而且答辩时多了一个「特征融合」的技术点可以讲。LSTM 部分甚至不需要训练太多 epochhidden state 只要有基本语义区分度拼接后的效果就不会差。6. 可视化进阶从训练曲线到注意力热力图可视化的及格线是画训练过程的 loss 曲线和验证集混淆矩阵但要把趋势预测的可靠性展示清楚重点在三张图上。第一张是真实情感得分曲线与预测曲线的对比测试区间用阴影标注评阅老师一眼就能看出模型是跟住了走势还是原地打转。第二张是正负情感评论的词云注意 wordcloud 库要指定中文字体路径否则输出的图片全是方块。第三张是注意力热力图用最后一个时间步的 hidden state 与句子中每个 token 的 hidden state 做点积再 softmax就能看到模型在哪些词上分配了高权重。图表用途推荐工具训练 loss/acc 曲线判断收敛和过拟合matplotlib真实 vs 预测趋势曲线展示预测效果主图matplotlib / pyecharts注意力权重柱状图解释模型关注哪些词matplotlib正负词云展示情感主题分布wordcloud如果你们老师偏爱可视化大屏用 pyecharts 的 Line 组件把预测曲线做成可缩放、可悬浮看数值的交互式图表导出成 HTML 报告直接打开视觉效果比静态 matplotlib 图好一个档次。注意力权重的实现很简单就是矩阵乘法和 softmax但它在答辩中的说服力极强当模型在「但是」「失望」「值得」这些词上显示出高权重时比一句「LSTM 效果很好」有分量得多。再往后走把文本和表情符号、图片封面拼进来就进入了多模态情感分析的领域视频人物情感分析也是同一思路加一路视觉特征再做融合。我的习惯是每次跑完实验把图表连同 checkpoint、词表、配置参数一起打包成一个报告文件夹这样复现和重跑都有据可查这个习惯救过我很多次。希望帮到你。本文还有配套的精品资源点击获取