☰
基于LSTM的电影评论情感分析毕设实战:从数据预处理到模型调优
2026/10/1 19:05:36 网站建设 项目流程

简介:这份资源面向计算机相关专业正在准备课程设计、期末大作业或毕业设计的学生,提供一套基于LSTM实现电影评论情感倾向分析的完整Python项目。项目已获98分评价,适合需要项目实战练习、希望掌握深度学习文本分类流程的学习者参考。压缩包共22个文件,约30.85MB,包含3个Python源码文件、6个JSON配置与索引文件、2个Markdown说明文档、1个PDF实践手册、1个训练好的model模型、2个arrow数据文件及5张结果图片,覆盖数据预处理、词向量构建、模型训练与结果可视化等环节。目录中区分了主程序、旧版训练脚本、模型配置与IMDB数据集,结构清晰,便于按模块阅读与复现。目前已有156人学习下载。读者可从中获得完整的赛题实现方案、可运行的训练与推理代码、词向量与标签映射配置、实验报告及结果图表,既能直接用于作业提交,也可作为理解LSTM情感分析全流程的实践参考。

1. 电影评论情感分析:为什么 LSTM 比词袋模型更适合做毕设

做情感分析毕设,很多同学第一反应是“把评论分词,统计正负词频,扔进朴素贝叶斯”。这个方案跑起来确实快,准确率也能到 80% 左右,但答辩时老师一句“否定句怎么处理”就能把你问住。比如“我不觉得这部电影好看”,词袋模型看到的是“不”“觉得”“电影”“好看”,正负词频一抵消,大概率判成正向。而 LSTM 按顺序读入每个词,前面的“不”会通过门控机制影响后面“好看”的语义表示,这就是循环神经网络在文本任务上的核心优势。

这个标题对应的是一套完整的毕设交付物:Python 实现的情感分类模型,加上一份能写进论文的实验报告。适合正在找毕设题目、或者大作业需要交代码和文档的本科生。技术栈上,常见做法是用 PyTorch 或 TensorFlow 搭 LSTM,配合中文电影评论数据集(比如豆瓣短评或 IMDB 中文翻译版),做二分类或多分类。源码部分通常包含数据预处理、模型定义、训练脚本、评估指标和可视化,报告则覆盖背景、相关工作、实验设置和结果分析。下面从数据准备到模型调参,把这条链路拆开讲清楚。

2. 数据准备与预处理:从原始评论到 LSTM 可吃的张量

2.1 中文电影评论数据集的获取与清洗

公开的中文情感分析数据集不算多,常见的有 ChnSentiCorp(酒店评论,但情感极性通用)、weibo_senti_100k(微博情感,口语化强),以及从豆瓣短评爬取的电影评论。如果标题里明确是“电影评论”,建议优先用豆瓣数据,因为领域匹配度高,模型学到的特征更贴近电影场景。爬取时注意控制频率,别给人家服务器添麻烦,一般每请求一次 sleep 1 到 2 秒。

拿到原始数据后,清洗步骤直接影响最终准确率。我一般会做这几件事:去掉 HTML 标签和 URL、统一全半角、去除表情符号(或者用占位符替换)、处理重复评论。下面是一个清洗函数的骨架:

import re import jieba def clean_text(text): # 去掉 HTML 标签 text = re.sub(r'<[^>]+>', '', text) # 去掉 URL text = re.sub(r'http[s]?://\S+', '', text) # 全角转半角 text = ''.join([chr(ord(ch) - 65248) if 65281 <= ord(ch) <= 65374 else ch for ch in text]) # 只保留中文、英文、数字和基本标点 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:]', '', text) # 分词 words = jieba.lcut(text) # 去掉停用词(需自行加载停用词表) # words = [w for w in words if w not in stopwords] return ' '.join(words)

这段代码的逻辑是:先做字符级清洗,再用 jieba 做词级切分。参数上,re.sub的正则模式决定了保留哪些字符,如果你做的是细粒度情感分析(比如五分类),标点符号可能携带情感信息,建议保留感叹号和问号。停用词表不要直接用百度那个通用表,最好根据电影评论语料统计一下高频无意义词,手动补充“这部电影”“感觉”“整体”这类领域停用词。

2.2 构建词表与序列填充:pad_sequence 的三种策略

LSTM 要求输入是定长序列,但评论长度从几个字到几百字不等。常见做法是设定一个最大长度max_len,比如 200,超过的截断,不足的补零。PyTorch 里用pad_sequence配合collate_fn可以动态填充,比全局统一截断更省显存。

from torch.nn.utils.rnn import pad_sequence from torch.utils.data import Dataset, DataLoader import torch class ReviewDataset(Dataset): def __init__(self, texts, labels, vocab, max_len=200): self.texts = texts self.labels = labels self.vocab = vocab self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): words = self.texts[idx].split() # 词转索引,未知词用 <unk> 的索引 ids = [self.vocab.get(w, self.vocab['<unk>']) for w in words] ids = ids[:self.max_len] return torch.tensor(ids, dtype=torch.long), torch.tensor(self.labels[idx], dtype=torch.long) def collate_fn(batch): texts, labels = zip(*batch) # 动态填充到当前 batch 的最大长度 texts_padded = pad_sequence(texts, batch_first=True, padding_value=0) labels = torch.stack(labels) return texts_padded, labels

这里的关键参数是padding_value,一般设为 0,对应<pad>的索引。batch_first=True让输出形状是(batch_size, seq_len),符合 LSTM 的默认输入格式。注意:如果你在 Embedding 层设置了padding_idx=0,那么补零位置的词向量不会参与梯度更新,这是标准做法。另一种策略是全局统一长度,实现简单但浪费计算;动态填充在 batch 内对齐,训练效率更高,但需要自定义collate_fn。我一般用动态填充,除非显存特别紧张。

词表构建时,低频词直接映射到<unk>,阈值通常设 2 或 3。词表大小控制在 2 万到 5 万之间,太大容易过拟合,太小则覆盖不足。可以用collections.Counter统计词频,然后按频率排序取 top-k。

3. LSTM 模型搭建:从 Embedding 到分类头的完整代码

3.1 单层 LSTM 与双向 LSTM 的选型对比

PyTorch 里定义 LSTM 分类模型,核心参数就几个:input_size(词向量维度)、hidden_size(隐藏层维度)、num_layers(层数)、bidirectional(是否双向)。对于电影评论这种中等长度文本,我一般从单层双向 LSTM 开始试,hidden_size设 128 或 256。

import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_layers=1, num_classes=2, dropout=0.5): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers=num_layers, bidirectional=True, batch_first=True, dropout=dropout if num_layers > 1 else 0) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(hidden_dim * 2, num_classes) # 双向所以乘2 def forward(self, x): # x: (batch_size, seq_len) embedded = self.embedding(x) # (batch, seq_len, embed_dim) lstm_out, (hidden, cell) = self.lstm(embedded) # 取最后一个时间步的隐藏状态,双向需要拼接 # hidden: (num_layers*2, batch, hidden_dim) hidden = torch.cat([hidden[-2], hidden[-1]], dim=1) # (batch, hidden_dim*2) out = self.dropout(hidden) logits = self.fc(out) return logits

逻辑说明:Embedding 层把词索引转成稠密向量,padding_idx=0保证补零位置不更新。LSTM 层输出整个序列的隐藏状态和最后一步的(hidden, cell)。双向 LSTM 的hidden形状是(num_layers*2, batch, hidden_dim),hidden[-2]是正向最后一层,hidden[-1]是反向最后一层,拼接后送入全连接分类。参数上,dropout在num_layers=1时不生效(PyTorch 会警告),所以单层时我通常在外面加nn.Dropout。hidden_dim从 128 到 512 都有人用,经验值是 256 在多数电影评论数据集上性价比最高。

3.2 训练循环与关键超参数设置

训练部分需要定义损失函数、优化器和评估指标。二分类用CrossEntropyLoss,优化器用 Adam,学习率从 1e-3 开始,配合ReduceLROnPlateau在验证集损失不下降时减半。

from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau from sklearn.metrics import accuracy_score, f1_score def train_model(model, train_loader, val_loader, epochs=10, lr=1e-3, device='cuda'): model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = Adam(model.parameters(), lr=lr) scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2) for epoch in range(epochs): model.train() total_loss = 0 for texts, labels in train_loader: texts, labels = texts.to(device), labels.to(device) optimizer.zero_grad() logits = model(texts) loss = criterion(logits, labels) loss.backward() # 梯度裁剪,防止 LSTM 梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() total_loss += loss.item() # 验证 model.eval() preds, true_labels = [], [] val_loss = 0 with torch.no_grad(): for texts, labels in val_loader: texts, labels = texts.to(device), labels.to(device) logits = model(texts) val_loss += criterion(logits, labels).item() preds.extend(torch.argmax(logits, dim=1).cpu().numpy()) true_labels.extend(labels.cpu().numpy()) acc = accuracy_score(true_labels, preds) f1 = f1_score(true_labels, preds, average='weighted') scheduler.step(val_loss) print(f'Epoch {epoch+1}: train_loss={total_loss/len(train_loader):.4f}, val_loss={val_loss/len(val_loader):.4f}, acc={acc:.4f}, f1={f1:.4f}')

参数说明:clip_grad_norm_的max_norm设 5.0 是经验值,LSTM 容易梯度爆炸,不加裁剪训练后期 loss 可能直接变 NaN。ReduceLROnPlateau的patience=2表示验证损失连续两个 epoch 不降就减半学习率。batch_size一般设 32 或 64,太小训练不稳定,太大显存吃紧。训练轮数 10 到 20 足够,配合早停(验证集 F1 连续 3 轮不升就停)可以防止过拟合。

4. 避坑与排查:LSTM 情感分析翻车实录

4.1 准确率卡在 50% 上不去

现象:训练 loss 缓慢下降,但验证集准确率一直在 50% 左右,跟随机猜差不多。原因通常是标签没对齐,或者数据里正负样本极度不均衡。解决:先打印前 10 条样本的文本和标签,确认没有错位;再用Counter统计标签分布,如果正负比超过 3:1,需要在损失函数里加weight参数,或者对少数类过采样。

4.2 训练 loss 震荡剧烈,验证 loss 飙升

现象:训练 loss 时高时低,验证 loss 在几个 epoch 后突然增大。原因一般是学习率太大,或者 batch_size 太小导致梯度噪声大。解决:把学习率降到 5e-4 或 1e-4,batch_size 提到 64,同时加上梯度裁剪。如果还不行,检查 Embedding 层是否冻结了预训练词向量——如果用了预训练词向量且冻结,学习率要设得更小。

4.3 模型对否定句仍然判断错误

现象:“我不喜欢这部电影”被判成正向。原因:LSTM 虽然能建模顺序,但如果训练数据里否定句太少,模型学不到“不”对后续词的影响。解决:在数据增强阶段,手动构造否定样本,比如把“我喜欢”替换成“我不喜欢”,把“好看”替换成“不好看”,扩充到训练集里。另外,可以把hidden_dim从 128 提到 256,给模型更多容量去记忆长距离依赖。

4.4 推理时显存溢出

现象:训练时正常,推理时 batch 稍微大一点就 OOM。原因:推理时没有torch.no_grad(),或者pad_sequence填充到了整个数据集的最大长度。解决:推理代码包在with torch.no_grad():里,并且推理的collate_fn也按 batch 动态填充,不要用训练集的全局最大长度。

4.5 保存的模型加载后预测结果全一样

现象:训练完保存state_dict,重新加载后所有输入都预测成同一类。原因:保存时只存了state_dict,加载时模型结构参数(vocab_size、hidden_dim等)跟保存时不一致,导致权重错位。解决:保存时同时存model.state_dict()和模型初始化参数,或者用torch.save(model, path)保存整个模型(不推荐,但毕设够用)。加载后先在一个已知样本上验证输出是否合理。

5. 报告撰写与答辩加分项:把实验数据变成论文图表

5.1 实验对比表格的设计

毕设报告里,老师最想看的是对比实验。至少做三组:词袋模型 + 朴素贝叶斯、单层 LSTM、双向 LSTM。表格列包括准确率、精确率、召回率、F1 值。下面是一个示例结构:

模型准确率精确率召回率F1 值
朴素贝叶斯0.8120.8050.7980.801
单层 LSTM0.8760.8710.8680.869
双向 LSTM0.9030.8990.8970.898

注意:表格里的数字要跟你实际跑出来的结果一致,不要编。如果双向 LSTM 只比单层高 1 个点,就如实写,然后分析原因(比如数据量不够大,双向的优势没完全发挥)。

5.2 混淆矩阵与错误分析

除了准确率,画一个混淆矩阵能直观看出模型在哪类样本上容易错。用sklearn.metrics.confusion_matrix配合seaborn.heatmap几行代码就能出图。错误分析部分,挑 10 条预测错误的样本,逐条看是文本本身歧义(比如反讽),还是预处理把关键信息洗掉了。这部分写进报告,答辩时老师会觉得你确实动手分析了,不是只跑了个模型。

5.3 一个提升答辩通过率的小技巧

答辩前,把模型对几条典型评论的预测过程打印出来,比如“这部电影太棒了”每个词经过 LSTM 后的隐藏状态变化。不用画复杂的图,就在终端里打印每个时间步的hidden向量范数,展示模型确实在“读”句子。这个技巧我每次带毕设都会让学生做,老师看到你理解模型内部行为,比只看准确率数字印象深得多。

做毕设这些年,我最大的教训是:别一上来就调参,先把数据洗干净、把标签对齐、把基线跑通。LSTM 情感分析这套东西,代码网上到处都是,但能跑出好结果的人,时间都花在数据上。希望帮到你。

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

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

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

立即咨询