
简介这是一份面向本科毕业设计的Python深度学习实战项目围绕电影评论情感分析场景采用flaskmysql搭建B/S端系统并利用word2vec模型完成评论文本的情感值判断与正负面分类。项目从需求分析、模型训练到网站部署均有完整设计适合计算机相关专业学生作为毕业设计参考也适合深度学习初学者用于理解NLP情感分析落地流程。资源包共290个文件压缩后约122.33MB。其中23个py文件提供核心源码17个html与34个js、30个css组成前端展示层14个scss/less用作样式扩展另有npy、pkl、pb等模型权重与训练数据文件以及sql数据库脚本和readme说明结构清晰便于按模块检索学习。此外包内附带说明文档和演示视频可辅助快速复现系统、理解关键实现细节。目前已有992人学习对于需要完成课程设计或毕业设计的同学具有一定借鉴价值。1. 深度学习电影评论情感分析系统从毕业设计到一条完整的 NLP 应用链路输入一段电影评论返回正面或负面判断听起来只是一个二分类任务但真正把它做成一个系统时数据清洗、词表构建、模型训练、指标评估、服务封装每一环都可能卡住。这个毕业设计选择深度学习 影评情感分析作为方向不是因为模型原理复杂而是因为它把 Python 生态里最常用的几块工具完整串了一遍HuggingFace 数据集、PyTorch 建模、Flask 接口封装。下面以 IMDB 50K 影评公开数据为语料用 TextCNN 作为主模型从数据准备到 Web 演示把一套可以跑出指标、可以展示结果的链路完整铺开。2. 准备影评数据IMDB 语料加载、清洗规则与 DataLoader 封装数据链路不打通后面模型写得再漂亮也没法训练。IMDB 影评数据集包含 50,000 条英文影评正负样本各 25,000 条标签为 1 表示正面、0 表示负面。这一章完成三件事加载数据并划分、清洗文本、把文本变成模型能读取的整数 ID 序列。2.1 加载 IMDB 数据集并划分训练集和验证集HuggingFace 的datasets库把 IMDB 语料做成了可直接加载的数据集不需要手动解压原始 tar.gz 文件也不用手动拼接文件路径。from datasets import load_dataset ds load_dataset(imdb, splittrain).shuffle(seed42).select(range(8000)) train_val ds.train_test_split(test_size0.1, seed42) train_data train_val[train] val_data train_val[test] print(len(train_data), len(val_data))参数细节seed42固定随机种子保证多机多轮实验的结果可复现删掉 seed 每次数据划分都不一样答辩复现实验时会很被动。select(range(8000))从 25,000 条训练样本中取前 8,000 条。如果环境只有 CPU建议改成 4,000训练时间可以压缩一半以上。test_size0.1分出 10% 作为验证集。验证集太小loss 曲线会剧烈抖动0.1 在 8,000 条数据下足够稳定。中间调参阶段不要碰官方 test split把它留到最终评估时再使用验证集只用于判断每个 epoch 的模型好坏。2.2 文本清洗规则去 HTML 标签、去 URL、统一小写标点怎么取舍原始影评里常见br /换行标签、电影年份和 URL。对 TextCNN 这种基于词向量的模型来说标点和数字对情感判断的贡献很小反而会无谓扩大词表规模。常用清洗函数如下import re def clean_text(text: str) - str: text re.sub(rbr\s*/?, , text) text re.sub(rhttps?://\S, , text) text text.lower() text re.sub(r[^a-z\s], , text) return re.sub(r\s, , text).strip()处理步骤正则表达式效果说明去换行标签br\s*/?替换成空格避免lovebr /it粘连成loveit去 URLhttps?://\S链接与情绪无关直接删除统一小写无避免Great和great被当作两个词去标点数字[^a-z\s]逗号、感叹号、数字全部替换成空格最容易忽略的是标签替换成空格而不是空字符串。如果直接替换成空字符串两个本来独立的词会变成一个无效词词表里多出一条噪声训练时卷积核还会在这类伪词上浪费时间。2.3 构建词表min_freq、max_vocab 与特殊标记的作用模型不能直接吃字符串需要先给每个词分配一个整数 ID。常见做法是扫描全部训练文本做词频统计只保留出现次数达到阈值的词。from collections import Counter def build_vocab(dataset, min_freq2, max_vocab20000): counter Counter() for sample in dataset: counter.update(clean_text(sample[text]).split()) vocab {pad: 0, unk: 1} for word, count in counter.items(): if count min_freq and len(vocab) max_vocab: vocab[word] len(vocab) return vocab vocab build_vocab(train_data) print(词表大小:, len(vocab))pad和unk分别占 ID 0 和 1这是透明工场惯例Embedding 层在遇到长度不足的评论时会用pad的向量参与计算遇到词表外的词时统一映射到unk。min_freq2表示只出现过一次的词全部归入unkmax_vocab20000限制 Embedding 矩阵规模词表扩大一倍Embedding 参数量就从 200 万涨到 400 万对 8,000 条训练数据来说没有必要。2.4 制定 Dataset 与 DataLoader固定 max_len、按 batch 装载样本Dataset 的职责是把「文本 标签」配对并转换成张量。每条评论长度不同统一截成max_len200的序列不足补 0。import torch from torch.utils.data import Dataset, DataLoader class IMDbDataset(Dataset): def __init__(self, hf_dataset, vocab, max_len200): self.samples list(hf_dataset) self.vocab vocab self.max_len max_len def __len__(self): return len(self.samples) def __getitem__(self, idx): text clean_text(self.samples[idx][text]) ids [self.vocab.get(w, 1) for w in text.split()[:self.max_len]] ids ids [0] * (self.max_len - len(ids)) return torch.tensor(ids, dtypetorch.long), torch.tensor(self.samples[idx][label]) train_loader DataLoader(IMDbDataset(train_data, vocab), batch_size64, shuffleTrue) val_loader DataLoader(IMDbDataset(val_data, vocab), batch_size128, shuffleFalse)max_len200能覆盖 IMDB 约九成评论的有效信息显存低于 4GB 时可以降到 128指标损失很小。batch_size64在 6GB 显存 GPU 上是安全值纯 CPU 环境建议改 32因为卷积层在 CPU 上对 batch 大小不敏感主要受内存占用影响。提示如果训练时报 CUDA out of memory优先降 batch_size不要直接改模型结构。batch_size 减半峰值显存通常能下降约三分之一。3. TextCNN 模型实现从 Embedding 到全连接层的完整路径TextCNN 是情感分析系统里性价比最高的基线模型。核心思路是用一维卷积在评论的连续词窗口上滑动提取类似 n-gram 的模式再用最大池化保留窗口内最强信号。对这个题目来说TextCNN 比 LSTM 更适合做主体模型。3.1 为什么选 TextCNN并行计算、小参数、短特征捕捉电影评论的情感倾向通常由若干关键词决定比如 terrible、boring、disappointing 或者 brilliant、touching。TextCNN 的卷积核覆盖 3 到 7 个连续词能捕捉到 not good 这类组合特征。LSTM 必须按时间步串行计算CPU 环境下训练速度差 5 倍以上在只有几千条训练数据时LSTM 的长距离建模能力发挥不出来反而更容易过拟合。TextCNN 的缺点是不能建模远距离依赖具体到影评就是 The acting is great, but the plot ruined everything 这种前后转折。如果答辩需要横向对比可以在同一个训练脚本里加一个 BiLSTM 模型用相同数据、相同评估方式做对照BiLSTM 参数量约为 TextCNN 的 2 到 3 倍训练时间也多 3 倍但在 50 词以上的长评论上准确率提升通常不到 1 个百分点。3.2 完整模型代码与张量维度演算import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim100, num_classes2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, 128, kernel_size3, padding1), nn.Conv1d(embed_dim, 128, kernel_size5, padding2), nn.Conv1d(embed_dim, 128, kernel_size7, padding3), ]) self.dropout nn.Dropout(0.3) self.fc nn.Linear(128 * 3, num_classes) def forward(self, x): x self.embedding(x) # (B, seq_len, embed_dim) x x.transpose(1, 2) # (B, embed_dim, seq_len) pooled [] for conv in self.convs: out F.relu(conv(x)) # (B, 128, seq_len) out F.max_pool1d(out, out.size(2)).squeeze(2) pooled.append(out) x torch.cat(pooled, dim1) # (B, 384) x self.dropout(x) return self.fc(x)维度变化需要盯紧输入(64, 200)经过 Embedding 变成(64, 200, 100)transpose(1, 2)之后是(64, 100, 200)100 是词向量维度被当作 Conv1d 的输入通道200 是序列长度。每个卷积核的 padding 分别设为 1、2、3目的是让卷积后的长度保持 200这样max_pool1d窗口直接取out.size(2)即可。kernel_sizepadding窗口含义能捕捉的典型模式31连续 3 个词not good52连续 5 个词waste of time73连续 7 个词one of the best movies三个池化结果拼接成 384 维向量Dropout 后接全连接层输出 2 类 logits。如果想用 GloVe 6B 预训练词向量可以用nn.Embedding.from_pretrained(pretrained_matrix, freezeFalse)覆盖 Embedding 的初始权重在 8,000 条数据下不推荐freezeTrue让模型微调词向量通常能提升 1 到 2 个百分点的 F1。3.3 损失函数、优化器与学习率调度参数model TextCNN(vocab_sizelen(vocab)) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.5)CrossEntropyLoss内部已融合 Softmax模型最后一层不用再挂nn.Softmax。lr1e-3是 TextCNN 配 Adam 的常用起点如果前两个 epoch 的 loss 震荡明显把学习率降到3e-4重新跑。weight_decay1e-4对 Embedding 权重做 L2 衰减防止词向量更新幅度过大StepLR每 3 个 epoch 让学习率减半让后期收敛更细。4. 模型训练与评估读 loss 曲线、算 F1、按验证集保存最优权重训练代码本身不难写难的是判断模型是不是真的能用了。最常见的现象是训练集准确率到了 0.98验证集却只有 0.80模型把训练集的噪声背了下来。这一章给出训练循环、评估代码和一张调参对照表。4.1 训练循环模板梯度清零、反向传播与梯度裁剪def train_one_epoch(model, loader, criterion, optimizer, devicecuda): model.train() total_loss, correct, total 0.0, 0, 0 for texts, labels in loader: texts, labels texts.to(device), labels.to(device) optimizer.zero_grad() logits model(texts) loss criterion(logits, labels) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() * len(texts) correct (logits.argmax(1) labels).sum().item() total len(texts) return total_loss / total, correct / totalclip_grad_norm_是文本模型训练里常被忽略的细节。不裁剪梯度时少数超长评论会把部分词向量梯度推到极大值loss 突然跳到 30 以上且再也回不来max_norm5.0表示整组参数的 L2 范数超过 5 就等比缩放训练过程会更平稳。验证循环与训练循环的最大区别是torch.no_grad()和model.eval()前者关闭自动求导、减少约一半显存占用后者关闭 Dropout 让结果可重现。def evaluate(model, loader, criterion, devicecuda): model.eval() total_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for texts, labels in loader: texts, labels texts.to(device), labels.to(device) logits model(texts) loss criterion(logits, labels) total_loss loss.item() * len(texts) correct (logits.argmax(1) labels).sum().item() total len(labels) return total_loss / total, correct / total4.2 用 matplotlib 绘制 loss 曲线判断过拟合和欠拟合每个 epoch 结束后把 train_loss、val_loss 记录到列表里训练完毕用 matplotlib 出图import matplotlib.pyplot as plt plt.plot(train_losses, labeltrain_loss) plt.plot(val_losses, labelval_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.grid(True) plt.savefig(loss_curve.png, dpi150)如果 train_loss 持续下降val_loss 在第四个或第五个 epoch 开始反弹说明已经进入过拟合区域最终模型应取反弹前那个 epoch 的权重。如果两条曲线都平行居高不下优先检查数据清洗流程再看学习率是否需要降低。4.3 计算 precision、recall、F1 的完整代码只有 accuracy 不够演示时用户输入评论的分布可能偏向某一个类型precision 和 recall 才能暴露模型短板。from sklearn.metrics import precision_recall_fscore_support def evaluate_f1(model, loader, devicecuda): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for texts, labels in loader: texts texts.to(device) preds model(texts).argmax(1).cpu().tolist() all_preds.extend(preds) all_labels.extend(labels.tolist()) pre, rec, f1, _ precision_recall_fscore_support( all_labels, all_preds, averagebinary, pos_label1 ) return pre, rec, f1pos_label1把正面评论视为正类。IMDb 正负样本通常均衡训练 10 个 epoch 后 F1 可以接近 0.90。如果发现 recall 明显低于 precision说明模型过于保守只对自己有把握的正面样本着色这时可以适当调低 Dropout 或增加训练数据量。现象可能原因调整方案train_loss 下降val_loss 上升过拟合Dropout 0.3 调到 0.5或 weight_decay 调到 1e-3两条 loss 都高且几乎不降学习率不合适lr 降到 3e-4或换用 ReduceLROnPlateauF1 比 accuracy 低 0.2 以上分类阈值偏向多数类先检查验证集标签分布再按 F1 最优的 epoch 保存权重某一类别 recall 过低训练数据不均衡考虑分层采样或使用 WeightedRandomSampler4.4 模型保存与加载state_dict 和词表必须一起维护torch.save(model.state_dict(), textcnn_best.pt)加载时不能直接对torch.load的结果调用预测。state_dict只保存参数不包含模型结构正确做法是先创建同结构模型再覆盖权重loaded_model TextCNN(vocab_sizelen(vocab)) loaded_model.load_state_dict(torch.load(textcnn_best.pt, map_locationcpu)) loaded_model.eval()另一个容易被跳过的坑是词表的保存。只保存权重文件的话换到另一台机器测试新评论时文本无法映射成整数 ID。稳妥做法是把vocab和max_len、embed_dim、num_classes一起存入 JSON推理脚本启动时先加载词表再加载权重。5. Flask 封装演示系统模型预热、预测接口与依赖管理训练完成后用 Flask 把模型包装成可交互的 HTTP 接口演示时只需打开浏览器输入影评内容。5.1 路由设计与预测接口from flask import Flask, request, jsonify, render_template app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): review request.json.get(review, ) prob, label predict_sentiment(review) return jsonify({label: label, probability: prob})5.2 预测函数与模型预热def predict_sentiment(review: str): ids encode_text(clean_text(review), vocab, max_len200) with torch.no_grad(): logits model(torch.tensor([ids])) prob torch.softmax(logits, dim1)[0, 1].item() return round(prob, 4), positive if prob 0.5 else negative服务启动后立刻接第一个请求可能因为权重加载和内存分配消耗 2 秒以上。常见做法是在app.run()之前先调用一次predict_sentiment(test)完成模型预热实际演示时才不会出现首包延迟。5.3 启动、验证与依赖管理curl -X POST http://127.0.0.1:5000/predict \ -H Content-Type: application/json \ -d {review: An absolute masterpiece of cinema.}返回结果中如果 probability 大于 0.8说明模型对强正面样本的判断足够稳定。依赖文件至少包含torch、flask、datasets、scikit-learn固定版本号后写入requirements.txt避免换环境时因 PyTorch 版本差异导致load_state_dict报错。项目目录里把训练脚本、清洗函数、模型定义、Flask 应用拆成四个模块文件演示视频中重点展示四个动作启动服务、打开网页、输入一条评论、展示正负面概率结果这样一套交付内容对答辩和技术分享都已经完整。本文还有配套的精品资源点击获取