☰
基于深度学习与TextCNN的电影评论情感分析系统:从数据清洗到Flask部署
2026/10/17 1:05:49 网站建设 项目流程

简介:这是一份面向本科毕业设计的Python深度学习实战项目,围绕电影评论情感分析场景,采用flask+mysql搭建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", split="train").shuffle(seed=42).select(range(8000)) train_val = ds.train_test_split(test_size=0.1, seed=42) train_data = train_val["train"] val_data = train_val["test"] print(len(train_data), len(val_data))

参数细节:

  • seed=42固定随机种子,保证多机多轮实验的结果可复现;删掉 seed 每次数据划分都不一样,答辩复现实验时会很被动。
  • select(range(8000))从 25,000 条训练样本中取前 8,000 条。如果环境只有 CPU,建议改成 4,000,训练时间可以压缩一半以上。
  • test_size=0.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(r"<br\s*/?>", " ", text) text = re.sub(r"https?://\S+", "", text) text = text.lower() text = re.sub(r"[^a-z\s]", " ", text) return re.sub(r"\s+", " ", text).strip()
处理步骤正则表达式效果说明
去换行标签<br\s*/?>替换成空格,避免love<br />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_freq=2, max_vocab=20000): 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_freq=2表示只出现过一次的词全部归入<unk>,max_vocab=20000限制 Embedding 矩阵规模,词表扩大一倍,Embedding 参数量就从 200 万涨到 400 万,对 8,000 条训练数据来说没有必要。

2.4 制定 Dataset 与 DataLoader:固定 max_len、按 batch 装载样本

Dataset 的职责是把「文本 + 标签」配对并转换成张量。每条评论长度不同,统一截成max_len=200的序列,不足补 0。

import torch from torch.utils.data import Dataset, DataLoader class IMDbDataset(Dataset): def __init__(self, hf_dataset, vocab, max_len=200): 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, dtype=torch.long), torch.tensor(self.samples[idx]["label"]) train_loader = DataLoader(IMDbDataset(train_data, vocab), batch_size=64, shuffle=True) val_loader = DataLoader(IMDbDataset(val_data, vocab), batch_size=128, shuffle=False)

max_len=200能覆盖 IMDB 约九成评论的有效信息,显存低于 4GB 时可以降到 128,指标损失很小。batch_size=64在 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_dim=100, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, 128, kernel_size=3, padding=1), nn.Conv1d(embed_dim, 128, kernel_size=5, padding=2), nn.Conv1d(embed_dim, 128, kernel_size=7, padding=3), ]) 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, dim=1) # (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 good"
52连续 5 个词"waste of time"
73连续 7 个词"one of the best movies"

三个池化结果拼接成 384 维向量,Dropout 后接全连接层输出 2 类 logits。如果想用 GloVe 6B 预训练词向量,可以用nn.Embedding.from_pretrained(pretrained_matrix, freeze=False)覆盖 Embedding 的初始权重;在 8,000 条数据下不推荐freeze=True,让模型微调词向量通常能提升 1 到 2 个百分点的 F1。

3.3 损失函数、优化器与学习率调度参数

model = TextCNN(vocab_size=len(vocab)) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.5)

CrossEntropyLoss内部已融合 Softmax,模型最后一层不用再挂nn.Softmax。lr=1e-3是 TextCNN 配 Adam 的常用起点;如果前两个 epoch 的 loss 震荡明显,把学习率降到3e-4重新跑。weight_decay=1e-4对 Embedding 权重做 L2 衰减,防止词向量更新幅度过大;StepLR每 3 个 epoch 让学习率减半,让后期收敛更细。

4. 模型训练与评估:读 loss 曲线、算 F1、按验证集保存最优权重

训练代码本身不难写,难的是判断模型是不是真的能用了。最常见的现象是训练集准确率到了 0.98,验证集却只有 0.80,模型把训练集的噪声背了下来。这一章给出训练循环、评估代码和一张调参对照表。

4.1 训练循环模板:梯度清零、反向传播与梯度裁剪

def train_one_epoch(model, loader, criterion, optimizer, device="cuda"): 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_norm=5.0) optimizer.step() total_loss += loss.item() * len(texts) correct += (logits.argmax(1) == labels).sum().item() total += len(texts) return total_loss / total, correct / total

clip_grad_norm_是文本模型训练里常被忽略的细节。不裁剪梯度时,少数超长评论会把部分词向量梯度推到极大值,loss 突然跳到 30 以上且再也回不来;max_norm=5.0表示整组参数的 L2 范数超过 5 就等比缩放,训练过程会更平稳。

验证循环与训练循环的最大区别是torch.no_grad()和model.eval(),前者关闭自动求导、减少约一半显存占用,后者关闭 Dropout 让结果可重现。

def evaluate(model, loader, criterion, device="cuda"): 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 / total

4.2 用 matplotlib 绘制 loss 曲线,判断过拟合和欠拟合

每个 epoch 结束后把 train_loss、val_loss 记录到列表里,训练完毕用 matplotlib 出图:

import matplotlib.pyplot as plt plt.plot(train_losses, label="train_loss") plt.plot(val_losses, label="val_loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend() plt.grid(True) plt.savefig("loss_curve.png", dpi=150)

如果 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, device="cuda"): 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, average="binary", pos_label=1 ) return pre, rec, f1

pos_label=1把正面评论视为正类。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,或换用 ReduceLROnPlateau
F1 比 accuracy 低 0.2 以上分类阈值偏向多数类先检查验证集标签分布,再按 F1 最优的 epoch 保存权重
某一类别 recall 过低训练数据不均衡考虑分层采样,或使用 WeightedRandomSampler

4.4 模型保存与加载:state_dict 和词表必须一起维护

torch.save(model.state_dict(), "textcnn_best.pt")

加载时不能直接对torch.load的结果调用预测。state_dict只保存参数,不包含模型结构,正确做法是先创建同结构模型再覆盖权重:

loaded_model = TextCNN(vocab_size=len(vocab)) loaded_model.load_state_dict(torch.load("textcnn_best.pt", map_location="cpu")) 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_len=200) with torch.no_grad(): logits = model(torch.tensor([ids])) prob = torch.softmax(logits, dim=1)[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 应用拆成四个模块文件,演示视频中重点展示四个动作:启动服务、打开网页、输入一条评论、展示正负面概率结果;这样一套交付内容对答辩和技术分享都已经完整。

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

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

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

立即咨询