基于深度学习的电影评论情感分析系统:从数据到Flask部署
2026/9/13 16:59:37 网站建设 项目流程

简介:一套面向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(r'http\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, dtype=torch.long)

max_vocab=20000是经验值,影评语料的高频词通常集中在前一万到两万之间,再往后的词出现次数极少,强行全部保留只会让嵌入层变得稀疏。max_len=128对应的是长影评场景,短评大多在 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_idx=0) 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, dim=1) # (batch, filters * len) return self.fc(self.dropout(out))

几个关键参数的作用要弄清楚:filter_sizes=(2, 3, 4)表示同时提取 2 元组、3 元组、4 元组窗口的特征,对应“不好”“太差”“一塌糊涂”这类长度不等的情感短语;padding=(size - 1, 0)保证每个卷积核输出的序列长度一致,方便统一池化;padding_idx=0让嵌入层在遇到<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(dim=1).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 可以充分压榨 GPU
learning_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 的嵌入表很大,反向传播时梯度累积过大会让收敛变慢。早停参数patience=3能防止过拟合,这比手动调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 = False

cudnn.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_location='cpu') model = TextCNN(len(checkpoint['vocab']), **checkpoint['config']) model.load_state_dict(checkpoint['model_state_dict']) model.eval()

map_location='cpu'是兼容关键点,训练如果用 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, dim=1) 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_location='cpu') model = TextCNN(len(ckpt['vocab']), **ckpt['config']) model.load_state_dict(ckpt['model_state_dict']) model.eval() app.run(host='0.0.0.0', port=5000)

5.3 给答辩演示做个最简前端页面

Flask 的render_template渲染一个输入框,用户提交评论后在同一页展示结果,这个流程对答辩来说已经足够。这里给出前端模板的核心部分:

<!-- templates/index.html:核心交互表单 --> <form id="sentiment-form"> <textarea name="comment" rows="4" cols="50" placeholder="请输入电影评论,如:剧情紧凑,演员演技在线"></textarea> <button type="submit">分析情感</button> </form> <div id="result"></div>

对应的 Flask 路由返回index.html,而/predict接口保持纯 JSON 返回。前端用 fetch 提交数据并渲染结果。这样的前后端分离设计,既能在浏览器里演示,也能用curl直接调用/predict验证接口,答辩时万一页面样式出问题,可以直接切换到命令行演示。

6. 验证技巧与可提交目录:把系统拔高到可维护的水平

6.1 测试集人工抽检:跑通端到端比准确率重要

模型训练完成后的验证,不能只看准确率数字。先跑测试集,随机抽 30 条误判样本,逐条分析。这一步的技巧是观察词表覆盖率,也就是每一条样本有多少词被替换成了<unk>。如果这一比例超过 20%,说明训练数据覆盖不充分,需要扩充语料;如果误判集中在带有讽刺语气的评论,比如“这片子好得我差点睡着”,说明模型只学到了字面特征,无法识别反讽,这属于当前模型结构的已知局限,在论文里诚实描述即可。

接着做端到端验证:用curl/predict接口分别发送“特效很不错,值回票价”和“剧情无聊到想提前离场”两条用例,确认接口返回的 JSON 中labelconfidence都符合预期。这一步排除的是模型训练正常但推理链路断掉的情况。

6.2 置信度阈值调优:让模型承认“不确定”

最后这一步是实际应用和答辩加分项。二分类 softmax 输出的两个概率永远加起来等于 1,所以“不确定”不会自然出现。常见的做法是引入一个置信度阈值tau,当最大概率低于tau时返回“中性/无法判断”:

prob, label = torch.softmax(logits, dim=1).max(dim=1) 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 演示的影评情感分析系统完整落地,梯度方向明确,每一步都有验证手段,无论答辩还是实际演示都能稳定运行。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询