资讯详情

资讯详情

基于Python深度学习的电影评论情感分析系统

简介针对基于深度学习的电影评论情感分析毕业设计项目这份资料以PythonFlaskMySQL搭建B/S架构系统利用word2vec模型对影评进行情感值判断与正负面情绪分类可帮助读者掌握从数据爬取、模型训练到Web端展示的完整流程。压缩包共290个文件涵盖Python源码、HTML页面、CSS/JS静态资源、模型权重及训练数据npy、pkl等并附有说明文档和演示视频整体约122.33MB目录结构清晰便于按模块查阅。已有992人学习下载适合作为本科毕业设计、课程实训或深度学习入门项目的参考范例。资源内提供了可直接运行的完整工程、环境配置思路与操作演示可据此复现评论情感分析功能并为后续扩展爬虫、模型调优等设计提供基础。1. 电影评论情感分析技术选型与落地约束电商评论、哔哩哔哩弹幕、豆瓣影评……凡是用户产出文本的平台上情感分析都是一件「做了不亏、不做难运营」的事。标题里这个基于 Python 的深度学习系统本质上是把一条不定长的影评文本映射为「积极 / 消极」二分类或五档情绪标定用卷积或循环网络替代人工规则换来对「烂片但演技在线」这类复杂表述的泛化能力。适合刚接触 NLP 的开发者也适合已经有规则模型、想置换为深度方案的团队。这个课题最常用的落地路径是「预训练词向量 BiLSTM/TextCNN 轻量 Web 接口」下面按照数据集 → 模型 → API → 调优的顺序展开代码基于 Python 3.9 与 PyTorch2.0 及以上GPU 非必需但训练时建议开启 CUDA。2. 影评数据预处理清洗、分词与词向量的三个关键选择2.1 数据集选型IMDb 50K 与中文影评的差异先解决一个问题数据从哪来。最常用的种子数据是 IMDb 50K 英文影评包含 25000 条训练与 25000 条测试数据标签为二分类。这个数据集的好处是数量适中、分布均衡用于毕业设计或小型系统足够。中文侧可选的公开标注语料较少常见出路是爬取短评后用规则打初标再人工校准或用已有的酒店评论语料做迁移。数据源不同会直接影响预处理流程英文需要处理大小写、缩写、词形还原中文则必须分词而且「难看」与「很难看」的否定程度需要模型从词序中学会。读取 IMDb 原始 tsv 并映射标签是数据处理的第一步import pandas as pd from torch.utils.data import Dataset class IMDBDataset(Dataset): def __init__(self, tsv_path): df pd.read_csv(tsv_path, sep\t, headerNone) # 第 0 列是评论文本第 1 列是 pos / neg self.texts df[0].astype(str).tolist() self.labels [1 if l pos else 0 for l in df[1].tolist()] def __len__(self): return len(self.texts) def __getitem__(self, idx): # 返回原文与标签后续交给 collate_fn 做批次级处理 return self.texts[idx], self.labels[idx]这段代码没有做任何清洗原因是你需要先确认原始数据的书写质量再决定清洗强度——多数影评里包含 HTML 标签、引号转义和多余空白直接用会在词表里塞进大量无效 token。2.2 文本清洗三条具体规则与截断策略清洗步骤不需要模型参与但直接影响词表大小和最终指标。一般按顺序做三件事第一用 BeautifulSoup 剥离 HTML 标签并还原常见转义第二统一小写并处理缩写英文里do nt应拆为do与nt因为否定词本身就是情绪信号第三截断到固定 token 数超长句子从右侧截断。IMDb 平均长约 230 词全保留会拖慢训练256 token 截断下信息损失可忽略中文影评平均更短128 即可。一个典型的清洗函数长这样import re from bs4 import BeautifulSoup def clean_review(raw: str, max_tokens: int 256) - str: soup BeautifulSoup(raw, html.parser) text soup.get_text( ) text re.sub(rhttp\S, , text) # 去掉 URL text text.replace(\\, ).replace(\n, ) text re.sub(r\s, , text).strip().lower() tokens text.split( ) return .join(tokens[:max_tokens])注意这里用的是简单按空格分词的写法适合英文中文要在分词环节换成jieba.lcut不能直接复用这段。截断方向值得对比一下很多人默认从前面截但如果否定词出现在句子后半段头部截断会直接丢掉情绪信号。两种策略在小验证集上的差异通常在 0.5~1.5 个百分点截断策略适用场景对验证集的影响头部截断保留前 N开头即观点句的短评长文本掉点明显尾部截断保留后 N结论常出现在末尾的影评更适合 IMDb分段平均预测超长文本效果稳定但推理成本高2.3 词向量选择随机初始化、GloVe 还是微调 BERT深度学习模型的初始 embedding 有两种常见选择随机初始化靠模型自己学或用 GloVe / word2vec 预训练向量做热身。对几万条影评这个规模随机初始化不一定输但预训练能加速收敛尤其为「少见词」提供更好的语义先验。英文语料用 GloVe 6B.100d 是性价比较高的方案文件约 400MB加载后内存占用可控中文可以考虑腾讯词向量但文件更大加载与 build 的时间会明显变长。维度选 100 还是 300 取决于显存100 维在 CPU 上训练更友好效果差距在二分类任务上通常小于 1%。import torch from collections import defaultdict def load_glove(path: str, dim: int 100): vectors defaultdict(lambda: torch.zeros(dim)) # OOV 单词映射为全零向量 with open(path, r, encodingutf-8) as f: for line in f: fields line.rstrip().split( ) word fields[0] try: values [float(x) for x in fields[1:]] vectors[word] torch.tensor(values) except ValueError: continue return vectors解析要点文件每行是「词 100 个浮点数」split 后第一个字段是词其余是向量OOV 单词做全零映射比随机生成稳定全零表示「无信息」模型会更快学会忽略不常见词。同时要留意个别行可能因空格异常导致ValueError这里用 try/except 跳过脏数据避免整个词表加载中断。2.4 词汇表构建与批次生成预处理最后一步是把文本映射为索引矩阵。需要先统计词频、按频率建立 vocab然后把每个 token 替换成 id。用 GloVe 时需要用 GloVe 的词表确定 id未出现的词统一分到unk这样 embedding 矩阵的第一行能对齐 padding 索引。from collections import Counter def build_vocab(texts, min_freq2): counter Counter() for t in texts: counter.update(t.split()) vocab {pad: 0, unk: 1} for w, c in counter.most_common(): if c min_freq: break vocab[w] len(vocab) return vocabmin_freq取 2 是将只出现一次的单词并入unk缩小模型输入规模也能缓解罕见词带来的过拟合。padding 序列在collate_fn中完成固定用pad_idx0与后续nn.Embedding的padding_idx参数保持一致这是训练循环能正常跑通的前提。3. 核心模型实现BiLSTM 与 TextCNN 的 PyTorch 对比与调参3.1 为什么先跑一个传统基线在动笔写深度模型之前花 15 分钟用 TF-IDF 加逻辑回归跑一遍数据能给你一个底线分数。IMDb 上这套组合的准确率大约 85%深度学习模型如果调完还没到 86%就该回头查预处理或超参数。这个基线的意义不在模型本身而在于确认数据链路连通、标签没有错位顺带给出一个「及格线」。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression vec TfidfVectorizer(max_features50000, ngram_range(1, 2)) X_train vec.fit_transform(train_texts) model_lr LogisticRegression() model_lr.fit(X_train, train_labels) val_score model_lr.score(vec.transform(val_texts), val_labels)参数说明max_features50000限制特征维度防止内存爆炸ngram_range(1, 2)引入二元词组能让「not good」这类表达被单独记录逻辑回归没有词序概念这是替代词序信息的一种手段。val_score 在 0.84 以上再进入下一步否则问题大概率出在数据清洗或标签映射上。3.2 模型一Embedding BiLSTM MaxPoolLSTM 适合影评这种序列化文本因为否定、转折都依赖词序。常用结构是Embedding → 双向 LSTM → 对两个方向的隐藏状态做全局最大池化 → Dropout → 全连接输出。双向的原因是「not」出现在「good」之前位置信息对情绪极性很关键。import torch.nn as nn class BiLSTMClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim128, num_classes2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers2, batch_firstTrue, bidirectionalTrue) self.head nn.Sequential( nn.Dropout(0.4), nn.Linear(hidden_dim * 2, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_classes) ) self.lstm.flatten_parameters() def forward(self, x, lengths): x self.embedding(x) # [batch, max_len, embed_dim] x nn.utils.rnn.pack_padded_sequence(x, lengths.cpu(), batch_firstTrue, enforce_sortedFalse) _, (h_n, _) self.lstm(x) # h_n: [num_layers*2, batch, hidden] h_n h_n[-2:].transpose(0, 1).reshape(x.size(0), -1) return self.head(h_n)这里有两个常见坑一是pack_padded_sequence要求 lengths 与实际序列长度严格一致长度为零的样本必须在 DataLoader 阶段过滤掉二是反向最后隐藏层拼接的顺序h_n[-2:]取两个方向最后一层的终态transpose reshape保证拼接顺序是「正向 → 反向」。拼反了准确率会掉 2%且不会报错。参数推荐范围说明hidden_dim64~256太大在小数据上过拟合num_layers1~23 层收益甚微显存翻倍dropout0.3~0.5加在分类头而不是 LSTM 内部embedding_dim100与预训练向量保持一致3.3 模型二TextCNN 多尺寸卷积核TextCNN 把文本当作单通道序列用多个宽度的卷积核常见 kernel size 为 3、4、5在同一时刻扫描不同粒度的短语模式。它比 LSTM 并行度高、训练快对短文本表现常优于 BiLSTM影评这种以局部短语为主要情绪载体的场景适合用它作为对照模型。class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Sequential( nn.Conv1d(embed_dim, out_channels128, kernel_sizek), nn.ReLU(), nn.AdaptiveMaxPool1d(1) ) for k in (3, 4, 5) ]) self.head nn.Linear(128 * 3, num_classes) def forward(self, x, lengthsNone): x self.embedding(x) # [B, T, D] x x.transpose(1, 2) # [B, D, T] pooled [conv(x).squeeze(-1) for conv in self.convs] return self.head(torch.cat(pooled, dim1))AdaptiveMaxPool1d(1)把每个卷积核的输出压成一个标量三个尺寸的卷积结果拼接后是全连接层的输入。kernel size 可以理解为 n-gram 窗口3 覆盖「太差」「不好」5 覆盖「surprisingly good」这类稍长短语。padding 位置会被 Conv1d 纳入卷积范围但因为池化取最大值padding 区域不会主导结果这就是 TextCNN 不严格依赖 mask 的原因。3.4 训练循环与三个关键超参训练参数对效果的影响排在模型结构之前。情感分析任务的标准配置基本固定Adam 学习率 2e-3batch size 64epoch 10~15配合早停。先把学习率设为 1e-3 跑 3 个 epoch观察损失曲线是否下降若震荡剧烈降到 5e-4若一直不降检查数据是否有 NaN 或词表是否大量命中 unk。def train_one_epoch(model, loader, optimizer, loss_fn, device): model.train() total_loss, correct 0.0, 0.0 for texts, labels in loader: texts, labels texts.to(device), labels.to(device) optimizer.zero_grad() logits model(texts) loss loss_fn(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() * texts.size(0) correct (logits.argmax(1) labels).sum().item() return total_loss / len(loader.dataset), correct / len(loader.dataset)clip_grad_norm_在 LSTM 里是必选项长序列反向传播梯度范数容易超过 20裁剪到 5.0 后训练波动明显变小。早停阈值设 2即验证准确率连续两轮不升就停止并加载最佳权重如果只追求最低代码量可以省略早停直接跑满 15 轮但小数据集上一般会过拟合 1~2 个百分点。4. 系统落地Flask 接口、模型热加载与评估报告4.1 目录结构与 checkpoint 保存格式模型训完后要作为独立产物保存整个项目源码的目录结构通常是这样data/放原始语料models/放训练好的权重app.py是推理服务入口utils/放清洗与词表加载函数。模型文件我习惯拆成三个vocab.json保存 token 到 id 的映射config.yaml记录超参数model.pt只存state_dict不把三者塞进同一个 pkl方便后续替换词表或只微调分类头。# config.yaml model: name: bilstm embed_dim: 100 hidden_dim: 128 num_layers: 2 vocab_size: 50000 padding_idx: 0 data: max_len: 256 min_freq: 2 train: batch_size: 64 lr: 0.002 epochs: 15保存权重用torch.save(model.state_dict(), model.pt)而非torch.save(model, ...)。只存权重可以避免 PyTorch 升级后 pickling 机制变化导致加载失败加载时先按 config 重建模型实例再执行load_state_dict。加载后做一次 dummy forward确保权重与词表尺寸匹配。4.2 Flask 单机推理接口单条预测走 Web 接口批量评测走脚本两者逻辑要分开。Flask 单进程下模型推理是 C 扩展调用推理过程会释放 GIL200 QPS 对影评系统足够。接口接收 JSON返回极性字符串与概率值格式统一方便前端直接渲染。from flask import Flask, request, jsonify import torch, json app Flask(__name__) def tokenize(text: str): # 与训练时清洗保持一致不能另写一套逻辑 cleaned clean_review(text, max_tokens256) ids [vocab.get(w, vocab.get(unk)) for w in cleaned.split( )] return torch.tensor([ids]) model.eval() with torch.no_grad(): def predict(text): ids tokenize(text) probs torch.softmax(model(ids, torch.tensor([ids.size(1)])), dim1) return probs[0, 1].item() app.route(/predict, methods[POST]) def handle_predict(): data request.get_json() if text not in data: return jsonify({error: missing field: text}), 400 score predict(data[text]) return jsonify({sentiment: pos if score 0.5 else neg, score: score})score是积极类别的概率。默认阈值 0.5但模型输出很少落在 0.4~0.6 区间若业务需要更高精度阈值调整在第五章展开。生产环境还要限制text字段长度防止超长文本撑爆 embedding lookup 的显存。如果想在服务运行时替换模型需要先把新权重写入临时文件再原子替换避免加载过程中服务返回不完整结果。4.3 评估报告准确率之外一定要看混淆矩阵技术评审通常只问「准确率多少」但真正能指导决策的是分类明细。一个 90% 准确率但消极类别只有 60% 召回的系统在运营侧会漏掉大量差评。所以在测试集上同时计算 precision、recall、F1 和 AUC并输出按文本长度分桶的准确率这是定位模型短板最直接的方法。from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score pred_proba model.predict_proba_val(test_loader) preds (pred_proba 0.5).astype(int) print(classification_report(test_labels, preds, target_names[neg, pos])) cm confusion_matrix(test_labels, preds) print(AUC: {:.4f}.format(roc_auc_score(test_labels, pred_proba)))output 里如果「短文本段准确率高、长文本低」说明截断策略过早丢失了信息可以测试改为头部截断或者把长文本切成多个片段分别预测再取平均。混淆矩阵的用处在于检查类别偏向若 negative 的 recall 显著低于 positive大概率是训练数据本身类不平衡或阈值偏向概率高的一侧。5. 进阶验证Attention 可视化与预测阈值校准最后一环是解释模型而不只是报告准确率。情感分析模型被质疑时最常见的问题是「它依据什么判断的」。可视化预测时重点关注的 token 是最直接的回应也能反向发现数据标注缺陷比如模型总是聚焦「film」而不是「boring」。5.1 一种易复现的 Attention 可视化在 BiLSTM 上额外接一个 attention 层把双向编码器每个时间步的输出加权求和权重就是关键词的热度。训练完成后把 token 与权重对齐用热力图绘出高亮词。这个做法不需要引入 BERT 等外部模型结构可控演示视频里也容易展示。class AttnBiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, batch_firstTrue, bidirectionalTrue) self.attn nn.Linear(hidden_dim * 2, 1) def forward(self, x): emb self.embedding(x) out, _ self.lstm(emb) # [B, T, 2H] logits self.attn(out).squeeze(-1) # [B, T] attn_weights torch.softmax(logits, dim1) ctx (out * attn_weights.unsqueeze(-1)).sum(dim1) return ctx, attn_weights计算attn_weights时padding 位置会被 softmax 分到少量概率需要乘上 mask 并在归一化前将 padding 位置置为负无穷否则可视化时句子尾部会有一团模糊的高亮。展示时对权重做 min-max 归一化高亮 top-3 token 即可。5.2 阈值校准不要迷信 0.5测试集上 0.5 未必是最优阈值。当运营方更在意「差评不误判」或更在意「分类更均衡」时可以扫描 0.3~0.7 的阈值找到代价敏感目标下的最优点。这个过程不需要重新训练一次推理后离线完成。from sklearn.metrics import f1_score for th in [0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7]: preds (proba th).astype(int) f1 f1_score(y, preds, pos_label1) acc (preds y).mean() print(fth{th:.2f} f1{f1:.4f} acc{acc:.4f})如果业务上要求「差评优先」就选召回率高而阈值略低于 0.5 的那一档如果追求整体判断均衡看 F1 峰值对应的阈值。把阈值写进配置文件而不是硬编码在代码里后续评审会不用改逻辑就能调整。演示环节最抓眼的不是准确率数字而是「给出一条评论 → 系统高亮关键词语 → 输出概率」这条完整链路把模型接进一个即时推理页面比放一百张训练曲线有说服力。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →