
简介基于深度学习的电影评论情感分析系统完整源代码包面向Python毕业设计与课程设计人群帮助开发者掌握word2vec向量模型在文本情感分类中的实际应用与系统部署流程覆盖数据预处理、模型构建、训练评估和Web展示完整链路。资源包共293个文件约126.37MB其中py文件负责业务逻辑与模型训练html/css/js构建可视化前端npy/pkl/pb保存词向量与模型参数sql脚本用于初始化MySQL数据库另有部署说明文档帮助快速复现目录结构清晰。目前已有68人学习浏览整体集成了可运行源码、数据库建表脚本、PyCharm环境配置说明及配套说明文档可作为从零搭建情感分析项目的参考范本也适合在此基础上复现模型训练、调整网络结构、替换评论数据集或扩展更多功能模块。1. 基于深度学习的电影评论情感分析系统不是模型难而是链路长这套基于深度学习的电影评论情感分析系统源代码是奔着毕设答辩去的端到端项目从 MySQL 里的影评表到分词和序列化再到 BiLSTMAttention 训练与 Flask 预测接口整条链路是通的不是只有模型文件的半成品。说一个和直觉相反的经验源码包里最容易翻车的根本不是模型结构而是数据编码、标签映射和部署生命周期这三个环节模型反而一改就能跑。适合两种人一是拿来直接交毕设或课设需要有数据库和 Web 端佐证工作量二是刚把 Python 和 PyTorch 环境装好想找一份能看懂每一行代码的完整工程照着复现。2. 从评论到正负结论数据管道与 MySQL 存储设计拿到源码先别急着跑模型。这个项目里数据是从 MySQL 里来的经过清洗、入库、读出来训练最后预测结果还要写回库。如果一开始就把数据流理顺后面调模型时会省掉大量无效调试。2.1 目录结构与功能模块划分先看整体布局这一套源码的目录设计是按「配置隔离、训练与预测分离」来的sentiment_analysis/ ├── app.py # Flask 预测接口与页面渲染 ├── train.py # 训练主流程支持命令行传参 ├── config.py # 所有可调参数集中管理 ├── models/ │ ├── __init__.py │ └── bilstm_attention.py # 模型定义 ├── utils/ │ ├── preprocessing.py # 清洗、分词、序列化 │ ├── dataset.py # 数据集封装与 batch 切分 │ └── db.py # MySQL 读写封装 ├── data/ │ ├── imdb_reviews.csv # 演示用英文影评 │ ├── chinese_movies.csv # 可替换的中文影评数据 │ └── stopwords.txt ├── checkpoints/ │ └── best.pt # 训练后保存的最佳权重 └── requirements.txtconfig.py是这套代码里我最喜欢的部分epoch、batch_size、学习率、数据库连接串全都收敛在一个文件里不用满项目找参数。models/下只放了模型定义训练逻辑和模型结构分离答辩时画架构图也很方便。utils/db.py是 MySQL 的读写封装毕设外审时「有数据库操作」这一项它就能撑住。资源包里的 LW 目录对应配套文档答辩前可以拿里面的流程描述直接改但代码为主。我的习惯是先把config.py里数据库密码和端口改成自己的再往下走。2.2 评论数据入库建表、清洗与幂等写入这套系统默认的 MySQL 表结构是这样设计的注意字符集和唯一索引这两个细节CREATE TABLE review ( id INT AUTO_INCREMENT PRIMARY KEY, movie_name VARCHAR(100) NOT NULL, content TEXT NOT NULL, cleaned_content TEXT, label TINYINT COMMENT 1 正向, 0 负向, predict_label TINYINT, confidence FLOAT, content_md5 CHAR(32) UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时强制utf8mb4是为了让中文和 emoji 都能存后面排「写入乱码」的坑时这一步能挡掉一半问题。label用 TINYINT 而不是 VARCHAR 存「positive/negative」是因为训练代码里直接用整数做交叉熵少一层转换。content_md5加唯一索引是实现幂等写入的关键同一句影评重复插入时会被 MySQL 直接拒绝。数据写入用参数化查询而不是拼接字符串这是 MySQL 写操作的基本素质import pymysql import hashlib from config import DB_CONFIG def insert_review(cursor, movie_name, content, label, cleanedNone, predNone, confNone): md5 hashlib.md5(content.encode(utf-8)).hexdigest() sql INSERT IGNORE INTO review (movie_name, content, cleaned_content, label, predict_label, confidence, content_md5) VALUES (%s, %s, %s, %s, %s, %s, %s) cursor.execute(sql, (movie_name, content, cleaned, label, pred, conf, md5))INSERT IGNORE配合content_md5的唯一索引重复调用不会报错也不会产生脏数据。label在代码里指定pred和conf是模型推理后回填的字段。清洗这一层我一般放在入库之前但清洗结果会单独存一列方便后面回溯原始评论文本import re def clean_text(text): text re.sub(r[^], , text) text re.sub(r\s, , text).strip() return text第一行剥掉 HTML 标签第二行压缩多余空白。注意清洗只针对评论正文不要把 movie_name 也拿去跑清洗没有必要。选 MySQL 而不是直接读 CSV 训练理由很简单毕设需要展示「数据入库 → 读写 → 展示」的闭环MySQL 让这个闭环可见另外预测完把结果回写库里后续做统计报表也有了数据基础。3. 文本向量化与序列建模BiLSTMAttention 的选型与实现情感分析本质上是一个文本分类任务输入是一句评论输出是一个二分类标签。这个项目里模型用的是 BiLSTM 加 Attention而不是一上来就上 BERT这个选型是有意为之的。3.1 为什么选 BiLSTMAttention 而不是纯 BERT如果你按《动手深度学习》里那套 LSTM 章节的思路来搭会发现这套结构的复现成本极低而且每个模块都能在论文里讲出道理来。对比一下几个可选项方案显存需求部署体积训练时间答辩可讲性Word2Vec BiLSTM Attention低小分钟级高BERT fine-tune高大小时级中黑盒偏重纯朴素贝叶斯无极小秒级低BERT 在情感分析上确实更强但毕设场景里它的显存要求、推理延迟、以及「为什么这么设计」的讲解难度都在劝退。BiLSTM 从两个方向读序列能捕捉「虽然...但是」这种转折Attention 给每个词分配一个权重告诉你是哪些词在决定情感倾向。这套组合在 5 万条影评规模上能跑到 85% 上下的准确率性价比足够。3.2 分词、去停用词与序列填充的实现先看分词和清洗这段代码同时处理中英文因为演示数据里既有 IMDb 英文影评也有中文影评import jieba def tokenize(text, langzh): if lang zh: return [w for w in jieba.lcut(text) if w.strip()] return [w.lower() for w in text.split() if w.strip()]中文用 jieba 的lcut做精确模式分词英文按空格切分后统一小写。stopwords 的过滤我放在分词之后、建词典之前直接比对词表命中就去掉。注意不要在这里做过度清洗像「不」这类否定词一旦被当成停用词过滤掉模型会失去判断转折的关键信号。词典构建的核心参数是max_vocab和min_count两个阈值from collections import Counter def build_vocab(tokenized_texts, max_vocab20000, min_count2): counter Counter() for tokens in tokenized_texts: counter.update(tokens) vocab {pad: 0, unk: 1} for word, freq in counter.most_common(max_vocab - 2): if freq min_count: vocab[word] len(vocab) return vocabmax_vocab20000限制词表规模防止高频但对分类没帮助的生僻词撑爆 embedding 层min_count2过滤只出现一次的词。这两个值影响训练速度也影响模型效果词表太大模型参数多词表太小unk占比高。序列化时做截断和 paddingimport numpy as np def encode(text, vocab, max_len100): tokens tokenize(text) ids [vocab.get(w, 1) for w in tokens] ids ids[:max_len] ids ids [0] * (max_len - len(ids)) return np.array(ids, dtypenp.int64)超过max_len100的评论直接截断不足的补pad的 id 0。这里有个血泪经验build_vocab只能 fit 在训练集上如果在全部数据上构建词典验证集和测试集的信息就提前泄漏进了训练过程后面验证集分数会虚高。3.3 模型结构与注意力权重的计算模型定义放在models/bilstm_attention.py核心结构如下import torch import torch.nn as nn import torch.nn.functional as F class BiLSTMAttention(nn.Module): def __init__(self, vocab_size, embedding_dim200, hidden_dim128, num_layers2, num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.lstm nn.LSTM(embedding_dim, hidden_dim, num_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout) self.attn_weight nn.Linear(hidden_dim * 2, 1) self.fc nn.Linear(hidden_dim * 2, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): emb self.dropout(self.embedding(x)) out, _ self.lstm(emb) # (B, T, 2*hidden_dim) attn_scores self.attn_weight(out).squeeze(-1) # (B, T) mask (x ! 0).float() attn_scores attn_scores.masked_fill(mask 0, -1e9) attn_weights F.softmax(attn_scores, dim-1) weighted torch.bmm(attn_weights.unsqueeze(1), out).squeeze(1) logits self.fc(self.dropout(weighted)) return logits, attn_weightspadding_idx0让pad位置的 embedding 不参与梯度更新LSTM 设置bidirectionalTrue后输出维度翻倍所以后面的线性层输入都是hidden_dim * 2。Attention 这一步先对每个时间步打分然后用 mask 把 padding 位置的分数压成-1e9softmax 后这些位置的权重趋近于零。最后torch.bmm做加权求和得到整句的向量表示。attn_weights这个返回值很重要答辩展示「模型关注了哪些词」就靠它。训练时只取logits算 loss推理时把attn_weights也取出来画可视化。4. 训练参数与评估矩阵怎么把准确率稳定拉到 85% 附近模型结构定了之后训练代码是一套标准的 PyTorch 流程但有几个参数直接影响能不能训练出来下面逐个说清楚。4.1 训练超参数与学习率策略config.py里的默认参数是我验证过的一组稳定配置参数值说明epochs10配合早停一般第 4~6 轮就收敛batch_size64过大容易显存溢出过小 loss 震荡learning_rate1e-3Adam 的默认区间再大就发散embedding_dim200与预训练词向量维度对齐hidden_dim128双向后维度翻倍为 256num_layers2再深收益有限且训练变慢dropout0.5防止过拟合的核心手段max_len100影评平均长度约 60~80 词max_vocab20000词表上限weight_decay1e-5L2 正则顺手加上训练循环里值得抄的细节是梯度裁剪和最佳权重保存import torch import torch.nn as nn from config import args model BiLSTMAttention(vocab_sizelen(vocab)) optimizer torch.optim.Adam(model.parameters(), lrargs.lr, weight_decay1e-5) criterion nn.CrossEntropyLoss() for epoch in range(args.epochs): model.train() total_loss, correct, total 0, 0, 0 for batch in train_loader: x, y batch[ids].cuda(), batch[label].cuda() optimizer.zero_grad() logits, _ model(x) loss criterion(logits, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() pred logits.argmax(dim1) correct (pred y).sum().item() total y.size(0) total_loss loss.item() valid_acc evaluate(model, valid_loader) if valid_acc best_acc: best_acc valid_acc torch.save(model.state_dict(), checkpoints/best.pt) print(fepoch {epoch} loss {total_loss/len(train_loader):.4f} ftrain_acc {correct/total:.4f} valid_acc {valid_acc:.4f})clip_grad_norm_把梯度模长限制在 5.0防止 LSTM 训练中梯度爆炸导致 loss 变成 NaN。best.pt只保留验证集上最好的权重而不是最后一个 epoch 的权重——很多源码包默认存最后一轮如果最后一轮过拟合了你拿到的模型反而更差。4.2 评估矩阵只看准确率是不够的准确率和 loss 是训练过程指标交作业时还需要一份更完整的评估报告from sklearn.metrics import classification_report, confusion_matrix model.eval() y_true, y_pred [], [] with torch.no_grad(): for batch in test_loader: logits, _ model(batch[ids].cuda()) y_pred logits.argmax(dim1).cpu().tolist() y_true batch[label].tolist() print(classification_report(y_true, y_pred, target_names[negative, positive])) print(confusion_matrix(y_true, y_pred))classification_report给出 precision、recall、F1。影评数据集里正负样本通常接近均衡准确率有意义但实际预测时你会发现「中性偏负面的影评」最容易混这时候看混淆矩阵能确定模型到底在哪个类别上犯错。如果negative的召回率明显低于positive说明模型倾向于预测正面需要检查训练集是否真的均衡。4.3 调参方向过拟合与欠拟合怎么判断训练完先画 loss 曲线和验证集准确率曲线不用 tensorboard直接 matplotlib 存图就能看。判断方法如下现象原因调整train loss 不降acc 在 52% 附近学习率太大或标签反了调 lr 到 1e-4检查 label 映射train acc 99%valid acc 78%过拟合增大 dropout 到 0.6减小 hidden_dimtrain acc 和 valid acc 都低欠拟合增大 embedding_dim加一层 LSTMloss 训练后期开始回升学习率没衰减用 ReduceLROnPlateau 或手工 decay常见的做法是每 2 个 epoch 把学习率乘以 0.5或者在验证集 acc 不再上升时触发ReduceLROnPlateau。这套代码默认用的是前者改动最小也不容易出现学习率调出边界的问题。5. 避坑排查五个让毕业设计翻车的经典现场这套源码跑通不难但每个环节都有固定的坑。下面这五条是我拆过类似毕设源码后总结的高频翻车点每条都按现象、原因、解决的顺序写。5.1 五个高频故障与处置方法1. 运行 train.py 直接 UnicodeDecodeError。现象报错utf-8 codec cant decode byte程序还没加载数据就崩了。原因Windows 下用 Excel 编辑过 CSV 后文件可能被存成了 GBK 编码而pandas.read_csv默认按 utf-8 读。解决读取时显式指定编码pd.read_csv(path, encodinggbk)或者用编辑器把文件统一转成utf-8-sig。顺手在config.py里加一个DATA_ENCODING utf-8-sig参数以后换数据集只改配置。2. 模型准确率一直在 50% 上下扎堆。现象loss 不降训练集准确率稳定在 0.5 左右和随机猜没区别。原因标签映射反了。比如 CSV 里 1 表示负向代码里却把 1 当期正向训练或者 DataLoader 没开 shuffle每个 batch 全是同一个类别。解决训练前打印前 20 条样本的content label人工核对一遍确认 1 到底是正向还是负向。DataLoader 里shuffleTrue必须打开否则模型学到的是 batch 顺序不是语义。3. 验证集 90%测试集 75%差距明显。现象训练时验证集 acc 一路飙到 0.9但随便拿一条新评论去预测效果很差。原因这是典型的数据泄漏。build_vocab或者清洗时的统计步骤 fit 在了全量数据上验证集的词表信息提前进入了训练导致验证分数虚高。解决build_vocab、min_count统计这些只能在 train 部分上构建validate 和 test 只调用encode做转换。检查点就是看代码里build_vocab的输入是train_texts还是all_texts。4. MySQL 写入中文出现??或者插入失败。现象库里存的评论变成问号或者pymysql报字符集相关错误。原因建表语句没带utf8mb4或者 pymysql 连接时没指定 charset。MySQL 默认字符集在有些环境下是 latin1中文写进去就乱。解决建表语句里加DEFAULT CHARSETutf8mb4连接串写成pymysql.connect(..., charsetutf8mb4)。这两个地方要同时改只改一处都会继续乱码。5. 预测接口每调一次要等好几秒。现象Flask 接口第一次请求要等 5 秒以上第二次稍微快一点但还是慢。原因代码把torch.load和model.cuda()写在了预测函数里每次请求都重新加载一遍模型权重模型文件 100MB 以上时这个开销会非常大。解决模型加载放到模块级别Flask 进程启动时执行一次预测函数里只做forwardmodel load_model() # 进程启动时执行一次 app.route(/predict, methods[POST]) def predict(): ids torch.tensor([encode(request.json[review], vocab)]) logits, _ model(ids) ...5.2 定位思路先日志后模型跑崩了先别急着改模型。我的排查顺序是固定的先看数据能不能读出来再看 tensor 形状对不对最后才看模型结构。具体做法是在train.py里加两行日志import logging logging.basicConfig(levellogging.INFO, format%(asctime)s %(message)s) logging.info(ftrain samples: {len(train_texts)}, vocab size: {len(vocab)}) logging.info(fbatch shape: {batch[ids].shape}, dtype: {batch[ids].dtype})打印出来的东西能挡住七成问题样本数为 0 说明 CSV 路径或编码错了batch 形状不是(batch_size, max_len)说明序列化出了问题dtype 不是torch.int64说明 numpy 数组类型没转对。日志做好之前不要动模型结构。6. 端到端验证与模型导出一个 POST 请求打通全链路训练完不是结束把模型部署成可调用的接口、并且验证一条真实评论走通全链路这才是毕业设计的完整交付。6.1 用 curl 做端到端验证启动 Flask 服务后用 curl 发一条真实影评验证从文本输入到预测输出的完整链路curl -X POST http://127.0.0.1:5000/predict \ -H Content-Type: application/json \ -d {review: 剧情很拖沓但摄影和配乐在线整体平庸。}预期返回的 JSON 结构{ label: positive, confidence: 0.87, attention: [0.02, 0.05, 0.31, 0.26, 0.03, 0.12, 0.21] }这组数据里label是预测类别confidence是 softmax 概率的最大值attention是模型对每个词的关注权重。发请求之前先想好期望输出上面这句影评有明确负面关键词但尾句「平庸」偏中性如果模型给出 positive 高置信度说明转折结构的捕获有问题。这条 curl 命令值得放进答辩演示脚本里比现场打开浏览器输文本稳定得多。6.2 用 TorchScript 导出部署产物如果不想每次预测都依赖 Python 训练环境可以把模型导出成 TorchScript用 Java 或 C 服务也能加载。导出代码很少但有一个关键约束import torch model.eval() dummy torch.tensor([[1, 2, 3, 0, 0, 0]], dtypetorch.long) scripted torch.jit.trace(model, dummy) torch.jit.save(scripted, checkpoints/best_scripted.pt)torch.jit.trace会把一次 forward 的计算图固化下来这意味着模型 forward 里不能有依赖 Python 控制流的操作。上面模型代码里masked_fill和bmm都是可导出的但如果 forward 里用了if或for动态改变行为trace 就会失败。另外返回值如果包含 dict某些版本的 TorchScript 会报错这也是我把 forward 返回值设计成(logits, attn_weights)这个 tuple 的原因。导出后加载方式和torch.load不一样loaded torch.jit.load(checkpoints/best_scripted.pt) logits, attn loaded(dummy)用 TorchScript 后预测接口的响应时间能再降一个量级。从那以后我每次拿到类似的源码包第一件事不是看模型结构而是先把config.py里的数据路径和build_vocab的位置标红先手动跑 5 个 epoch 确认 loss 在下降再做全量训练最后强制走一遍 curl 的端到端验证。这套流程帮我避开了绝大多数跟模型无关的坑希望帮到你。本文还有配套的精品资源点击获取