电影评论情感分析毕设实战:从TextCNN训练到Flask部署

发布时间:2026/10/7 10:17:35
电影评论情感分析毕设实战:从TextCNN训练到Flask部署 简介一份基于深度学习的电影评论情感分析系统完整源码包面向需要完成毕业设计或课程设计的Python学习者核心采用word2vec向量模型实现电影评论正负面情感判别项目后端基于Python 3.6.8前端包含完整页面资源数据库选用MySQL 5.7并配合Navicat管理整体环境配置清晰适合当作NLP实战练习和二次开发底稿。压缩包共293个文件整体约126.37MB内容涵盖Python源码、前端页面、模型与数据文件以及sql数据库脚本和多份说明文档其中模型文件可帮助理解word2vec特征表示与训练结果文档则能辅助完成环境搭建与部署目录结构较完整能够支撑从数据处理、模型训练到Web展示的整个项目环节。已有68人浏览学习整体规模适合课程设计展示通过源码包可系统掌握电影评论情感分析的项目组织方式理清文本预处理、向量化、模型加载与判别输出的关键环节也能在此基础上替换数据集或调整网络结构做进一步实验。1. 电影评论情感分析毕设源码解压之后真正要复用的不是模型这个标题给出的是一套很典型的 Python 毕设方案基于深度学习的电影评论情感分析系统附带源代码、论文或设计文档标题里的 LW 一般指的就是配套论文/设计文档。很多同学拿到这类压缩包第一反应是直接跑但实际上真正容易翻车的不是模型而是数据管道和路径配置。解压后你会发现模型文件往往只有几百行剩下的时间几乎都耗在数据清洗、词表对齐和“训练时能用、预测时崩掉”这类问题上。这篇内容按一线实现顺序拆一遍从系统架构、文本预处理、模型训练到最后的 Flask 演示把参数、默认值和踩坑点都标出来。适合想快速跑通毕设或者把课设从 Jupyter Notebook 升级成完整系统的人新手能照着命令复现熟手可以直接对齐边界条件和训练细节。2. 系统怎么拆深度学习情感分析的架构与模型选型2.1 为什么最终选 TextCNN / LSTM 而不是传统机器学习电影评论情感分析本质上是文本二分类传统做法是 TF-IDF 加 SVM 或 Logistic Regression。这里有个很容易被忽略的问题TF-IDF 把句子拆成独立的词袋丢失了“虽然……但是……”这种转折关系而电影评论里恰恰充满了转折和讽刺。所以毕设选题一旦把“深度学习”写进题目就不太可能退回词袋模型否则答辩时的核心创新点就站不住。深度学习的优势在于先用 embedding 把词映射成稠密向量再用 CNN 或 RNN 捕捉局部和时序上下文。选型时我一般按以下顺序考虑方案CPU 训练耗时效果区间复现难度适合场景TF-IDF SVM分钟级基线 85% 左右低对比实验TextCNN十几分钟88%-90%低主模型首选BiLSTM比 TextCNN 慢数倍和 TextCNN 接近中强调长文本语义时BERT 微调需要 GPU小时级92%-95%高加分对比项不建议当主模型对毕设源码来说TextCNN 是性价比最高的选择参数量小CPU 也能跑训练稳定卷积核的 3/4/5 窗口可以直观解释成“抽取连续三个词构成的局部特征”。如果题目明确写了 LSTM那就把模型替换成 BiLSTM但要做好训练时间翻倍的心理准备。BERT 则更适合放进“模型对比”章节单独拿出一节说明“为什么我不用 BERT”既能展示工作量又能避开环境配置的大坑。2.2 解压后先看目录一个可用系统的四层结构拿到“完整源码LW.zip”后不要急着python train.py。先按四层结构核对一下目录缺哪层补哪层data/原始影评数据、清洗后的 train.csv / valid.csv / test.csv、stopwords.txtsrc/ 或 code/preprocess.py、dataset.py、train.py、predict.pycheckpoints/ 或 model/训练产出的最优权重 best.ptapp.pyFlask 演示页面或 JSON 接口LW 文档一般描述的是选题背景、系统设计、测试结果正文里不会写“坑在哪”所以代码侧的问题还得靠运行来暴露。环境依赖建议直接用一个虚拟环境隔离避免把系统 Python 装乱python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install torch scikit-learn pandas jieba flask这里要特别说一句 PyTorch 安装的坑网上很多教程默认带着 CUDA 的安装命令如果你的机器没有 NVIDIA GPUpip install torch会把 2GB 多的依赖装进去但torch.cuda.is_available()仍然是 False。毕设演示只用 CPU 版就够进入 PyTorch 官网选择对应操作系统用不带 CUDA 的安装命令会快很多也省掉后续驱动不匹配的问题。2.3 数据流向与预测时的复用关系这个系统的数据流看起来很简单原始文本 → 清洗 → 分词 → 词表映射 → 定长序列 → 模型 → 正面/负面概率。但有一条边界条件必须刻在脑子里预测时使用的预处理必须与训练时完全一致词表也必须从训练集构建并保存。很多毕设源码“能训练但预测乱码”就是因为 predict.py 里重新走了一遍分词和建词表token 编号完全错位模型拿到的是另一套语言。所以解压后第一步不是找模型而是找有没有保存word2idx.json或者vocab.pkl。如果源码包里没有我会先补上这一步训练完成后把词表、预处理函数和模型权重放同一个目录。否则后面做 Web 演示时必然要翻车这条放在最后一章再展开。3. 把影评文本变成模型能吃的东西预处理与 DataLoader 的落地细节3.1 数据获取IMDb 原始数据与中文影评的两种常见来源英文场景直接用 IMDb 数据集现在不建议再用 torchtext 自带的 dataset 接口因为它在新版本里改动很大最容易出现“照着老教程写语法全报错”。常见做法是用 HuggingFace 的datasets库拉取from datasets import load_dataset dataset load_dataset(imdb) # 返回的 train 和 test 各 25000 条label 0 表示负面1 表示正面IMDb 的好处是样本量大且标签均衡不需要做太多类别处理。中文影评则要自己解决数据源常见做法有两种一是用爬虫采集豆瓣短评二是使用网络上整理好的商品评论/影评数据集。爬虫请务必控制请求频率并且在系统设计文档里说明数据规模和时间范围。无论用哪种来源落到本地时统一整理成两列text和label文本去重后打乱顺序避免同一个用户的多条评论同时出现在训练集和测试集导致评估结果虚高。3.2 清洗与分词保留否定词比提升准确率更关键拿到原始文本后清洗规则不要写得太激进。电影评论里常见br这类 HTML 标签、URL、多余空格这些可以去掉但标点符号里的感叹号和问号对情感有增强作用粗暴删除会损失一部分判断依据。我的习惯是保留中文、英文字母和数字至少保留句号、感叹号、问号作为停用词过滤的边界。分词环节中文用 jieba英文直接按空格切就行。这里有一个很多人踩过的坑直接下载一个几千行的停用词表全盘剔除结果把“不、没、莫、别”这些否定词也删了“不好”变成“好”模型准确率直接崩到 50%。所以停用词表要么自己精简要么加载后人为排除否定词和转折词。给一份能直接跑通的预处理脚本import re import jieba import pandas as pd from sklearn.model_selection import train_test_split def build_stopwords(path: str) - set: stopwords set() with open(path, encodingutf-8) as f: for line in f: word line.strip() if not word: continue stopwords.add(word) # 手工保留对情感判断起关键作用的词 stopwords.difference_update({不, 没, 没有, 别, 再, 却, 但是, 但}) return stopwords def clean_text(text: str) - str: text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(r.*?, , text) # 保留中英文、数字和基本标点其他符号去掉 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9\s。!?], , text) return text.strip() def tokenize(text: str, stopwords: set) - list: text clean_text(text) words jieba.lcut(text) return [w for w in words if w and w not in stopwords and len(w) 1] df pd.read_csv(data/raw.csv) stopwords build_stopwords(data/stopwords.txt) df[tokens] df[text].apply(lambda x: tokenize(x, stopwords)) # 8:1:1 划分训练、验证、测试集 train_df, tmp train_test_split(df, test_size0.2, random_state42) valid_df, test_df train_test_split(tmp, test_size0.5, random_state42) train_df.to_csv(data/train.csv, indexFalse) valid_df.to_csv(data/valid.csv, indexFalse) test_df.to_csv(data/test.csv, indexFalse)逻辑说明train_test_split的random_state固定成 42保证每次运行划分结果一致测试集再从临时集里切一半得到 8:1:1 的比例。len(w) 1把单字词过滤掉这里同样要小心“好”和“烂”这类单字对情感判断很重要所以我在分词后没有全盘过滤所有单字词只是过滤长度大于 1 的空串和停用词。参数设置上random_state可以在 0 到 100 之间随便固定但一旦定下就不要改否则答辩时复现不出原来的准确率。停用词表建议控制在 200-400 个高频无意义词而不是网上动辄几千行的通用表。清洗规则里保留。的好处是后续可以把句子切分成短句再做注意力但 TextCNN 阶段不强制。3.3 词表构建与 embedding 初始化词表只从训练集构建验证集和测试集里出现的新词一律映射为unk。这里有一个默认值约定pad用编号 0unk用编号 1真正从训练集统计的词从 2 开始编号。构建词表时过滤掉出现次数少于 2 的词能显著减少未登录词造成的噪声。from collections import Counter counter Counter() for tokens in train_df[tokens]: counter.update(tokens) # 出现次数 2 才进词表 vocab {pad: 0, unk: 1} for word, count in counter.items(): if count 2: vocab[word] len(vocab) # 保存词表预测阶段必须复用 import json with open(data/vocab.json, w, encodingutf-8) as f: json.dump(vocab, f, ensure_asciiFalse)embedding 初始化有两种常见做法一种是直接用nn.Embedding随机初始化让模型自己学样本量小的时候效果不太稳定另一种是加载预训练词向量比如 gensim 的 Word2Vec 或 GloVe然后用词向量矩阵覆盖 embedding 层。对毕设源码来说预训练词向量属于加分项可以放在“实验对比”里证明它的价值。一个通用加载片段是import numpy as np from gensim.models import KeyedVectors wv KeyedVectors.load_word2vec_format(data/vectors.bin, binaryTrue) embed_dim 100 embedding_matrix np.random.uniform(-0.05, 0.05, (len(vocab), embed_dim)) for word, idx in vocab.items(): if word in wv: embedding_matrix[idx] wv[word] # 拿到 model 后执行 # model.embedding.weight.data torch.tensor(embedding_matrix, dtypetorch.float)参数说明embedding_matrix 是[词表大小, embed_dim]的矩阵随机初始化范围-0.05到0.05是为了避免一开始数值范围太大把梯度撑爆。embed_dim用 100 还是 300取决于词向量文件英文 GloVe 常见 100/200/300 维中文预训练词向量一般 100 维居多。3.4 DataLoader 与 max_len 的确定文本长度必须统一常见做法是截断和补位。IMDb 平均长度约 230 个单词但截断到 128 通常不会损失太多准确率因为电影评论的核心观点往往出现在开头和结尾。中文短评平均长度更短128 是安全值。如果想更稳妥可以统计训练集的长度分布后取 95% 分位点。import torch from torch.utils.data import Dataset, DataLoader class SentimentDataset(Dataset): def __init__(self, df_path, vocab, max_len128): self.df pd.read_csv(df_path) self.vocab vocab self.max_len max_len def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] tokens str(row[tokens]).split() ids [self.vocab.get(w, 1) for w in tokens] # 1 是 unk if len(ids) self.max_len: ids ids[:self.max_len] else: ids ids [0] * (self.max_len - len(ids)) # 0 是 pad return torch.tensor(ids, dtypetorch.long), torch.tensor(row[label], dtypetorch.long) train_loader DataLoader( SentimentDataset(data/train.csv, vocab), batch_size64, shuffleTrue ) valid_loader DataLoader( SentimentDataset(data/valid.csv, vocab), batch_size128, shuffleFalse )逻辑说明tokens列在 CSV 里是以空格分隔的字符串__getitem__里按空格切回列表。max_len截断时我采用“抬头去尾”的简单策略如果想保留头尾信息可以改成“取前 64 和后 64 拼接”但在 TextCNN 里效果差别不大。batch_size在 CPU 训练时可以取 64GPU 上可以到 128 或 256具体按显存调。4. 用 PyTorch 跑通 TextCNN 训练从模型定义到参数调优4.1 模型定义为什么三个卷积核尺寸就够TextCNN 的核心思路是用多个不同宽度的卷积核并行提取 n-gram 特征。卷积核宽度是 3、4、5 时分别相当于看连续三个词、四个词、五个词的组合。电影评论里“非常好看”是四词组合“不推荐”是三词组合几种宽度并行的好处是模型不必自己去选哪种 n-gram 最有效而是把三种特征全部拼接后交给全连接层学习权重。import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim100, num_classes2, kernel_sizes(3, 4, 5), num_filters100): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k, paddingk // 2) for k in kernel_sizes ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * len(kernel_sizes), num_classes) def forward(self, x): # x: [batch, seq_len] emb self.embedding(x) # [batch, seq_len, embed_dim] emb emb.transpose(1, 2) # [batch, embed_dim, seq_len] pooled [] for conv in self.convs: conv_out torch.relu(conv(emb)) pooled.append(torch.max(conv_out, dim2).values) cat torch.cat(pooled, dim1) # [batch, num_filters * 3] return self.fc(self.dropout(cat))参数说明padding_idx0让 pad 位置的 embedding 始终为全零向量这样补位不会引入噪声。paddingk//2是为了让卷积输出长度和输入长度保持一致但这里其实没有强依赖因为后面直接取了最大值池化。nn.Dropout(0.5)放在全连接层前是防止过拟合的关键位置如果放在 embedding 后反而会破坏词向量一致性。num_filters100是三组卷积核各自输出 100 个通道最终拼接成 300 维特征这个维度对二分类问题足够。4.2 训练循环与超参一份能跑通的最小配置训练部分最影响成败的不是网络结构而是学习率、早停和梯度裁剪。学习率用 Adam 时默认 1e-3 可以起步但如果 loss 一开始就震荡不降先降到 5e-4 试试。下面这段是经过验证的最小训练框架import torch from sklearn.metrics import accuracy_score device torch.device(cuda if torch.cuda.is_available() else cpu) model TextCNN(len(vocab)).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss() best_acc 0.0 patience 3 no_improve 0 for epoch in range(20): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) optimizer.zero_grad() out model(batch_x) loss criterion(out, batch_y) loss.backward() # 梯度裁剪防止 embedding 在训练初期被冲飞 nn.utils.clip_grad_norm_(model.parameters(), 3.0) optimizer.step() # 每个 epoch 结束做一次验证 model.eval() all_preds, all_labels [], [] with torch.no_grad(): for batch_x, batch_y in valid_loader: batch_x batch_x.to(device) logits model(batch_x) preds torch.argmax(logits, dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(batch_y.numpy()) acc accuracy_score(all_labels, all_preds) if acc best_acc: best_acc acc no_improve 0 torch.save(model.state_dict(), checkpoint/best.pt) else: no_improve 1 if no_improve patience: print(fearly stop at epoch {epoch}, best_acc{best_acc:.4f}) break参数说明weight_decay1e-4是 Adam 下的 L2 正则化作用是抑制过拟合clip_grad_norm_(model.parameters(), 3.0)把梯度 L2 范数裁剪到 3.0防止个别样本把 embedding 层梯度撑大。patience3表示连续 3 个 epoch 验证准确率不增长就提前停止。这个值对影评二分类来说比较保守想让训练更充分可以调到 5但要注意验证集上准确率可能先升后降早停本质上就是在帮你找最高点。4.3 评估只看准确率会把自己的毕设坑掉如果测试集正负样本恰好各占一半准确率还有参考价值但实际项目里很容易遇到“90% 的样本都是负面评论”这种不平衡数据此时全输出负面也能拿到 90% 准确率答辩时经不起问。所以我习惯固定输出分类报告和混淆矩阵from sklearn.metrics import classification_report, confusion_matrix # test_preds 和 test_labels 由 predict.py 在测试集上产生 print(classification_report(test_labels, test_preds, target_names[负面, 正面])) print(confusion_matrix(test_labels, test_preds))这里要看的指标是 F1-score而不是准确率。F1 是精确率和召回率的调和平均能同时反映“预测为正面里有多少是真的正面”和“真正的正面里被找回了多少”。毕设答辩被追问“模型哪里不好”时最稳的回答是“从混淆矩阵看模型容易把含蓄的负面评论判成正面这主要是因为训练数据里讽刺性表达占的比例不够”这句话比单调的“准确率 90%”有说服力得多。5. 复现避坑从环境配错到训练结果不对劲的 5 条排查记录5.1 现象pip install torch后torch.cuda.is_available()一直是 False原因通常有两个一是装了 CPU 版 PyTorch二是显卡驱动支持的最高 CUDA 版本和 wheel 包要求的版本不匹配。解决方法是先跑一句nvidia-smi看右上角的 CUDA Version再到 PyTorch 官网选对应版本安装。如果只是毕设演示完全可以在 CPU 上训练TextCNN 在 2 万条数据上跑 10 个 epoch 也只要二十多分钟。5.2 现象预处理后 tokens 全部为空训练准确率停在 50%原因基本是停用词表太狠把“不”“没”“好”“烂”全过滤了清洗完之后一篇文章只剩“的、了、啊”。这个现象在中文数据集上尤其常见。解决方法是每次清洗后随机打印 20 条分词结果肉眼看一遍重点检查否定词和转折词是否还在。我一般会在tokenize函数里加一句if not words: print(text)把分词后为空的文本单独暴露出来。5.3 现象训练 loss 不降或者直接变成 NaN原因通常是学习率太大或者 embedding 层随机初始化范围过大遇到数值不稳定的样本后梯度爆炸。解决方法是先把学习率降到 5e-4并加上梯度裁剪。如果还是 NaN检查输入数据里有没有空行和 Nonemax_len为 0 的序列会让卷积层报错。词表构建时过滤低频词也能缓解因为低频词对应的 embedding 几乎得不到有效梯度容易产生异常值。5.4 现象训练 loss 在下降但验证准确率从第 5 个 epoch 开始回头这是过拟合的典型信号。原因不是模型太强而是训练数据量偏少、dropout 位置不对。解决方法是把Dropout(0.5)放在全连接层之前而不是 embedding 之后同时开启早停。如果数据只有几千条不要盲目加深网络TextCNN 用一层卷积加一层全连接就够。还可以在损失函数上做标签平滑但毕设里用weight_decay加早停通常就足够了。5.5 现象训练时没事预测阶段报“词表不存在”或预测结果全为 0原因是训练脚本里构建了词表但预测脚本里又重新建了一遍词表顺序和编号全部错位模型拿到的是完全不同的输入分布。解决方法是把词表保存成vocab.json预测脚本启动时直接加载并且强制复用同一个预处理函数。这条是血泪经验训练、验证、预测三套代码里的预处理如果不集中在一个preprocess.py里迟早会在某个深夜复制粘贴时漏改一处。6. 把模型封装成 Flask 演示十分钟跑通 Web 端电影评论打分发很多人训练完模型就以为结束了但毕设演示环节要求你能现场输入一句“这部电影太烂了浪费我两个小时”并且看到系统返回“负面”。用 Flask 是最快的方式不需要写完整的前端页面一个jsonify接口就能完成答辩演示。关键点是预测函数必须加载训练时保存的词表并且把自己写的预处理逻辑原样复制过来。import json import torch from flask import Flask, request, jsonify app Flask(__name__) vocab json.load(open(data/vocab.json, encodingutf-8)) model TextCNN(len(vocab)).to(cpu) model.load_state_dict(torch.load(checkpoint/best.pt, map_locationcpu)) model.eval() def to_ids(text: str, max_len: int 128): # 复用训练阶段的 tokenize 和 clean_text不要重新实现 tokens tokenize(text, stopwords) ids [vocab.get(w, 1) for w in tokens] if len(ids) max_len: ids ids[:max_len] else: ids ids [0] * (max_len - len(ids)) return ids app.route(/predict, methods[POST]) def predict(): data request.get_json() text data[text] with torch.no_grad(): ids_tensor torch.tensor([to_ids(text)]) logits model(ids_tensor) pred torch.argmax(logits, dim1).item() return jsonify({text: text, sentiment: 正面 if pred 1 else 负面}) if __name__ __main__: app.run(host0.0.0.0, port8080)启动后可以用一条 curl 命令验证curl -X POST http://localhost:8080/predict \ -H Content-Type: application/json \ -d {text: 剧情很烂浪费了两个小时}返回{text: 剧情很烂浪费了两个小时, sentiment: 负面}就说明整条链路通了。这里有一个我踩过无数次的教训map_locationcpu必须写否则在 GPU 机器上训练的权重直接加载到 CPU 会报错词表、预处理函数、模型权重这三样东西要放到同一份代码里管理任何一方更新另外两方必须同步。把模型跑通后建议再加上两条典型的反讽评论做测试比如“这部电影拍得真好好到我中途睡着了”如果模型误判成正面可以顺势在答辩里讲“这是情绪识别中的讽刺检测难点”这比吹嘘 90% 准确率更有含金量。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询