资讯动态

基于深度学习的电影评论情感分析系统:从数据到Flask部署

发布时间:2026/9/13 16:59:38 来源:尧图企业网站定制
简介一套面向Python毕业设计及课程设计的深度学习实战项目聚焦电影评论情感分析场景适合需要独立完成Web系统与模型训练部署的学生开发者。资源整合项目源码、数据库脚本、开发工具及前后端代码功能完整、界面简洁项目经过严格调试可快速运行体验。压缩包共290个文件涵盖Python程序文件py/pyc、HTML/CSS/JS前端页面、scss/less样式源码、MySql数据库脚本、模型权重文件pkl/pb/npy以及说明文档等整体约122.97MB目录条理清晰便于按模块查阅。目前已有163人学习下载既可直接用于课题答辩演示也可作为情感分析技术学习与二次开发的参考基底。后台基于Python 3.7构建配合Navicat与PyCharm即可完成环境配置与启动适合具备一定Python基础、希望快速获取完整项目模板的本科及高职学生使用。1. 这套情感分析系统先想清楚交付物再写代码电影评论情感分析是每届毕业设计里出现频率最高的题目之一但大多数同学交出来的东西只是能跑的 Notebook。真正能拿去答辩的系统至少分三层数据管线和模型训练是底座可交互的推理接口是门面。用 Python 做基于深度学习的电影评论情感分析核心不是把准确率刷到 95%而是把“用户输入一段影评 - 模型判断情感倾向”这条链路完整跑通并且每一步都能解释清楚。本文按做这类项目最稳的路径拆开讲评论数据怎么准备、模型怎么选型、训练参数怎么调、最终怎么封装成 Web 接口和命令行工具。你在居中过程的每个环节会遇到什么坑我会直接点出来。适合正在做毕业设计、或者想快速落地一个文本分类小系统的开发者参考。2. 数据准备影评情感分析的上限在数据不在模型2.1 语料选型中文豆瓣短评还是英文 IMDb 评论深度学习文本分类对数据质量高度敏感。如果题目没有限定语言我一般建议做中文评论答辩现场演示时输入一句“特效炸裂但剧情稀碎”比输入英文例句更有说服力。中文语料来源有两个常见选择一是公开的中文情感分析语料二是自己爬取豆瓣短评再打标注。爬虫方案不要一上来就写爬虫先确认有没有可以直接用的标注数据。公开语料已经把每条评论标好了 positive / 负向省去标注环节。如果只能爬取未标注评论至少准备 8000 到 10000 条有效数据自己打标推荐工具是 label-studio 或者简单的 Excel 表格手工标注。数据规模上不需要追求几十万条。一万条左右、类别平衡的评论数据足以支撑一个演示级的深度学习模型。注意切分顺序先打乱全量数据再按 8:1:1 划分训练集、验证集、测试集。不要按时间先后切分否则训练集和测试集会存在明显的语言风格漂移。2.2 清洗、分词和截断填充的具体写法拿到原始评论后先做一轮基础清洗。下面这份 Python 脚本是我不论处理什么文本分类任务都会先跑一遍的标准流程import re import jieba def clean_text(text: str) - str: 清洗影评文本去HTML标签、去URL、去特殊符号 text re.sub(r[^], , text) # 去掉HTML标签 text re.sub(rhttp\S, , text) # 去掉URL text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) # 保留中英文和数字 return text.strip() def tokenize(text: str) - list: 分词后过滤单字和停用词 words jieba.lcut(clean_text(text)) stopwords {的, 了, 是, 在, 我, 有, 和, 就, 不, 人} return [w for w in words if w.strip() and w not in stopwords and len(w) 1]clean_text的参数解释[^]匹配影评中残留的加粗、换行标签http\S处理评论区常出现的链接最后一步把表情符号、标点统一替换为空格避免分词器把“真的好看”和“真的好看”当成两个不同词。tokenize里过滤掉长度为 1 的单字是因为电影评论里“好”“烂”这种单字虽然有情感色彩但单独作为特征时容易带来噪声配合二元语法反而更稳定。2.3 构建词表并转换训练样本清洗完文本后要把不定长的评论序列转换成定长张量。常见做法是统计词频后截断词表再对每条评论按最大长度做 padding 和截断。from collections import Counter from torch.nn.utils.rnn import pad_sequence import torch def build_vocab(tokenized_texts: list, max_vocab: int 20000) - dict: 按词频截断构建词表预留 unk 和 pad 两个专用位 counter Counter() for tokens in tokenized_texts: counter.update(tokens) vocab {word: idx 2 for idx, (word, _) in enumerate(counter.most_common(max_vocab))} vocab[pad] 0 vocab[unk] 1 return vocab def encode(tokens: list, vocab: dict, max_len: int 128) - torch.Tensor: 词序列转ID超过128截断不足128补0 ids [vocab.get(w, vocab[unk]) for w in tokens[:max_len]] ids ids [vocab[pad]] * (max_len - len(ids)) return torch.tensor(ids, dtypetorch.long)max_vocab20000是经验值影评语料的高频词通常集中在前一万到两万之间再往后的词出现次数极少强行全部保留只会让嵌入层变得稀疏。max_len128对应的是长影评场景短评大多在 50 词以内如果数据里全是短评max_len压缩到 64 能显著降低显存占用。unk位用于处理验证集和测试集中没见过的词这是初学者最容易漏掉的一步——直接查词表会 KeyError。3. 深度学习模型选型TextCNN 比 LSTM 更适合影评分类3.1 影评文本的局部特征决定了 CNN 更顺手动手深度学习项目时第一反应往往是用 LSTM 或 BiLSTM因为文本是序列数据。但影评分类任务有个特点情感判断主要依赖局部词组——“演技炸裂”“剧情拖沓”“全程无尿点”这些关键词组已经足够判定情绪。TextCNN 通过不同尺寸的卷积核提取 n-gram 特征恰好匹配这种局部模式匹配需求。对比之下LSTM 的优势在于捕捉长距离依赖但影评很少出现“开头很烂但结尾……所以整体还是好”这种跨 50 个词才呼应的复杂表述。从训练速度看CNN 在 GPU 上并行度远高于 RNN同样数据量下训练时间大约只有 LSTM 的三分之一到二分之一。对毕业设计而言算力资源有限TextCNN 是性价比最高的起点。3.2 用 PyTorch 写出最小可跑的 TextCNN这里直接给出一个可以在单卡或纯 CPU 上跑通的全连接分类器结构包含嵌入层、多尺寸卷积、全局池化和输出层。import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size: int, embed_dim: int 100, num_filters: int 128, filter_sizes(2, 3, 4), num_classes: int 2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (size, embed_dim), padding(size - 1, 0)) for size in filter_sizes ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(len(filter_sizes) * num_filters, num_classes) def forward(self, x): # x shape: (batch, seq_len) emb self.embedding(x).unsqueeze(1) # (batch, 1, seq_len, embed_dim) convs [] for conv in self.convs: c conv(emb).squeeze(3) # (batch, filters, padded_len) c F.relu(c) c F.max_pool1d(c, c.size(2)).squeeze(2) # (batch, filters) convs.append(c) out torch.cat(convs, dim1) # (batch, filters * len) return self.fc(self.dropout(out))几个关键参数的作用要弄清楚filter_sizes(2, 3, 4)表示同时提取 2 元组、3 元组、4 元组窗口的特征对应“不好”“太差”“一塌糊涂”这类长度不等的情感短语padding(size - 1, 0)保证每个卷积核输出的序列长度一致方便统一池化padding_idx0让嵌入层在遇到pad时输出全零向量不参与梯度更新。num_filters从 64 调到 256 能小幅度提升准确率但显存占用和数据需求同时上升128 是兼顾两者的取值。Embedding 层在预训练词向量和随机初始化之间我的建议是直接随机初始化。预训练向量虽然在语义表达上更强但导入权重后适配领域语料还要单独微调嵌入层对初学者来说调试成本高。随机初始化配合充足训练数据同样能学到“烂片”和“佳作”的语义边界。3.3 要不要加注意力机制很多基于深度学习的案例都会在卷积后接一层注意力做法是对每个卷积核的输出计算权重后加权求和替换掉简单 max pooling。说实话在 TextCNN 上加注意力只能看到 0.1 到 0.3 个百分点的波动多数情况还可能是负优化。但如果论文里的创新点写的是“融合注意力机制的影评情感分析”这个模块就值得实现。注意力机制的作用是让模型关注句子中决定情感极性的核心区域但对短影评来说max pooling 已经在做类似的事——每段卷积输出中的最大值就是最显著的特征。我的建议是一个折中方案先不加注意力把 TextCNN 的基线准确率跑出来验证集上的准确率稳定后再加入注意力模块与原模型做消融对比。这样论文里有对比数据答辩时也讲得出“注意力机制提升/未提升”的实验结论。比一上来就堆模型结构更稳妥。4. 训练循环与超参数固定随机种子先跑通再谈调优4.1 最小训练代码与评估指标选择模型结构和数据准备好了接下来要有一个可靠的训练流程。这里给出训练一个 epoch 并输出验证集指标的核心代码from sklearn.metrics import f1_score, accuracy_score def train_one_epoch(model, dataloader, optimizer, criterion): model.train() total_loss 0 for batch_x, batch_y in dataloader: optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(dataloader) def evaluate(model, dataloader, criterion): model.eval() preds, labels, total_loss [], [], 0 with torch.no_grad(): for batch_x, batch_y in dataloader: logits model(batch_x) loss criterion(logits, batch_y) total_loss loss.item() preds.extend(logits.argmax(dim1).cpu().tolist()) labels.extend(batch_y.cpu().tolist()) return accuracy_score(labels, preds), f1_score(labels, preds), total_loss / len(dataloader)criterion用交叉熵损失对应二分类输出层的两个 logit。评估时不能只看 accuracy当正负样本比例接近 1:1 时 accuracy 有参考价值但一旦某类样本多出 10% 以上F1-score 才是决定模型真实水平的标准。上面的evaluate函数把两者同时打印出来训练过程中我一般每轮记录 train loss、val acc 和 val f1 三列后续画曲线和写论文也直接用得上。4.2 超参数怎么设先固定后搜索别一上来就网格搜索影评情感分析项目的超参数在显存不紧张的情况下以下配置可以放心起步参数推荐值说明batch_size64影评句子短单条占显存小64 可以充分压榨 GPUlearning_rate1e-3Adam 优化器搭配 1e-3 是起步值2e-3 以上容易震荡max_len128按 2.3 节所述根据评论长度分布调整embedding_dim100再大收益有限训练时间线性增长epoch20固定 seed 跑完 20 轮看 loss 曲线是否收敛patience (早停)3val loss 连续 3 轮不降就停止训练如果learning_rate设为 1e-3 后 val loss 在训练后期波动明显下调到 3e-4 或 5e-4。batch_size不建议设到 128 以上影评虽然短但 vocab 20000 的嵌入表很大反向传播时梯度累积过大会让收敛变慢。早停参数patience3能防止过拟合这比手动调weight_decay更直观。4.3 固定随机种子让每次训练结果都可复现直接贴出训练完所有 epoch 后要保留的工作这个模块很多人会无视但答辩被问到“为什么两次跑结果不一样”时无法回答。真正可靠的随机种子设置要同时固定 Python、NumPy 和 PyTorch 三个层面的随机源import random, numpy as np, torch def set_seed(seed: int 42): 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 Falsecudnn.deterministic True强制卷积算法使用确定性实现代价是训练速度下降大约 10%但换来的是同一份代码多次运行得到相同指标。cudnn.benchmark False关闭了 cuDNN 的自动调优保证每次选择同一套卷积算法进一步确保可复现性。这套配置在写论文和调试阶段必须开启正式提交结果显示时不要关闭。5. 用 Flask 把模型封装成可答辩演示的情感分析系统5.1 模型保存与加载的标准写法训练完成后不能每次预测都重新初始化模型权重要把模型结构、权重和词表一起持久化到磁盘。我会把所有必要信息打包成一个字典再保存避免分开保存时版本不一致torch.save({ model_state_dict: model.state_dict(), vocab: vocab, config: { embed_dim: 100, num_filters: 128, filter_sizes: (2, 3, 4), max_len: 128, } }, sentiment_model.pt)加载时从同一个文件恢复三个关键对象checkpoint torch.load(sentiment_model.pt, map_locationcpu) model TextCNN(len(checkpoint[vocab]), **checkpoint[config]) model.load_state_dict(checkpoint[model_state_dict]) model.eval()map_locationcpu是兼容关键点训练如果用 GPU加载到没有独显的答辩机器时必须指定 CPU。model.eval()往往会漏掉不调用它的话 Dropout 层仍处于激活状态同样的输入每次预测结果都会不同这个 bug 在答辩现场一旦触发非常尴尬。5.2 Flask 路由与推理接口Web 服务用 Flask 搭建核心路由只做一件事接收 POST 请求中的评论文本清洗分词后送给模型返回情感类别和置信度。from flask import Flask, request, jsonify app Flask(__name__) model, vocab, config None, None, None app.route(/predict, methods[POST]) def predict(): data request.get_json() text data.get(comment, ) tokens tokenize(text) ids encode(tokens, vocab, config[max_len]) with torch.no_grad(): logits model(ids.unsqueeze(0)) prob torch.softmax(logits, dim1) pos_score prob[0][1].item() label positive if pos_score 0.5 else negative return jsonify({label: label, confidence: round(max(pos_score, 1 - pos_score), 4)})tokens tokenize(text)不能直接复用训练脚本里的encode因为这里的vocab是训练集的完整词表输入的词一旦不在词表内就会被替换为unk这是正确行为表示模型对这个词没有学习过。logits是二分类的原始输出softmax转成概率后pos_score表示积极概率阈值默认 0.5这个阈值在最后一章会展开优化。服务启动前先在模块顶层加载模型绝不能在请求函数内部加载。我见过在多线程环境下每个请求都执行一次torch.load的写法不仅慢还会导致显存分配异常if __name__ __main__: ckpt torch.load(sentiment_model.pt, map_locationcpu) model TextCNN(len(ckpt[vocab]), **ckpt[config]) model.load_state_dict(ckpt[model_state_dict]) model.eval() app.run(host0.0.0.0, port5000)5.3 给答辩演示做个最简前端页面Flask 的render_template渲染一个输入框用户提交评论后在同一页展示结果这个流程对答辩来说已经足够。这里给出前端模板的核心部分!-- templates/index.html核心交互表单 -- form idsentiment-form textarea namecomment rows4 cols50 placeholder请输入电影评论,如剧情紧凑演员演技在线/textarea button typesubmit分析情感/button /form div idresult/div对应的 Flask 路由返回index.html而/predict接口保持纯 JSON 返回。前端用 fetch 提交数据并渲染结果。这样的前后端分离设计既能在浏览器里演示也能用curl直接调用/predict验证接口答辩时万一页面样式出问题可以直接切换到命令行演示。6. 验证技巧与可提交目录把系统拔高到可维护的水平6.1 测试集人工抽检跑通端到端比准确率重要模型训练完成后的验证不能只看准确率数字。先跑测试集随机抽 30 条误判样本逐条分析。这一步的技巧是观察词表覆盖率也就是每一条样本有多少词被替换成了unk。如果这一比例超过 20%说明训练数据覆盖不充分需要扩充语料如果误判集中在带有讽刺语气的评论比如“这片子好得我差点睡着”说明模型只学到了字面特征无法识别反讽这属于当前模型结构的已知局限在论文里诚实描述即可。接着做端到端验证用curl向/predict接口分别发送“特效很不错值回票价”和“剧情无聊到想提前离场”两条用例确认接口返回的 JSON 中label和confidence都符合预期。这一步排除的是模型训练正常但推理链路断掉的情况。6.2 置信度阈值调优让模型承认“不确定”最后这一步是实际应用和答辩加分项。二分类 softmax 输出的两个概率永远加起来等于 1所以“不确定”不会自然出现。常见的做法是引入一个置信度阈值tau当最大概率低于tau时返回“中性/无法判断”prob, label torch.softmax(logits, dim1).max(dim1) confidence prob.item() if confidence 0.6: result 中性/无法判断置信度较低 else: result 正向 if label.item() 1 else 负向tau取 0.6 意味着样本只有达到 60% 把握才给出判断否则返回低置信度提示。调参方法跑一遍测试集统计 confidence 的分布把tau设置在分布 25 百分位附近。这个设置的实用价值在于用户输入“这部电影还行吧”这类模糊评论时硬性归为正向或负向都是错误直接输出“无法判断”反而更合理答辩时也更容易解释模型的行为边界。6.3 交付的 ZIP 包里应该包含什么最终要提交的文件建议严格按区分功能划分sentiment-system/ ├── data/ # 训练与测试语料 ├── src/ │ ├── train.py # 训练入口 │ ├── predict.py # 模型加载与推理 │ └── utils.py # 清洗/分词/词表构建 ├── app.py # Flask 服务 ├── templates/index.html # 前端页面 ├── requirements.txt └── README.md # 运行说明README 里至少写清楚 Python 版本、依赖安装命令、训练和启动命令、测试用例各一条确保换一台机器能在十分钟内复现。到这里一个从数据到模型再到 Web 演示的影评情感分析系统完整落地梯度方向明确每一步都有验证手段无论答辩还是实际演示都能稳定运行。本文还有配套的精品资源点击获取

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

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

免费获取报价