基于 Python 深度学习的电影评论情感分析算法
Bi-LSTM 情感分析算法详解
博主介绍:✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w+、ZEEKLOG博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌
🍅文末获取源码联系🍅
👇🏻 精彩专栏推荐订阅👇🏻 不然下次找不到哟
2022-2024年最全的计算机软件毕业设计选题大全:1000个热门选题推荐✅
Java项目精品实战案例《100套》
Java微信小程序项目实战《100套》
Python项目实战《100套》
感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及文档编写等相关问题都可以给我留言咨询,希望帮助更多的人
文章目录

1. 算法概述
1.1 什么是 Bi-LSTM?
Bi-LSTM(Bidirectional Long Short-Term Memory)是一种能够同时考虑前文和后文信息的循环神经网络。
为什么需要 LSTM?
普通 RNN 存在梯度消失问题,难以处理长序列。LSTM 通过引入门控机制(遗忘门、输入门、输出门)解决了这个问题。
为什么需要双向?
情感分析中,词语的情感色彩可能依赖前后文。例如:
“这部电影虽然节奏慢,但是剧情非常精彩”
如果只看到"但是"之前的内容,会误判为负面评论。Bi-LSTM 可以同时从两个方向理解文本。
1.2 整体架构
输入: "这部电影太精彩了!" ↓ [分词] 这部 电影 太 精彩 了 ↓ [向量化] [101, 523, 89, 2341, 12, ...] # 词语索引 ↓ [Embedding] 将索引转换为稠密向量 ↓ [Bi-LSTM] 前向 LSTM + 后向 LSTM ↓ [Attention] 自动关注重要词语(如"精彩") ↓ [全连接层] 输出各类别分数 ↓ [Softmax] 转换为概率 ↓ 输出: 正面 (0.92), 负面 (0.08) 2. 数据清洗与预处理
2.1 数据集介绍
我们使用 dmsc_v2 数据集,包含超过 200 万条豆瓣电影评论:
| 字段 | 说明 | 示例 |
|---|---|---|
| userId | 用户ID | 123456 |
| movieId | 电影ID | 2052619 |
| rating | 评分(1-5分) | 5 |
| comment | 评论内容 | “这部电影太精彩了!” |
2.2 情感标签生成
核心思想:根据评分自动生成情感标签
# 评分 >= 4 → 正面评论 (1)# 评分 <= 2 → 负面评论 (0)# 评分 == 3 → 中性评论(过滤掉) df['sentiment']=(df['rating']>=4).astype(int) df = df[df['rating']!=3]# 过滤中性评分为什么过滤 3 分?
3 分属于中性评价,情感倾向不明显,容易影响模型训练。
2.3 均衡采样
问题:原始数据中正面评论远多于负面评论
解决方案:正负样本各采样 50%
# 从 200 万条评论中采样 2 万条 samples_per_class =10000# 正负各 1 万条 positive_samples = df[df['sentiment']==1].sample(n=samples_per_class) negative_samples = df[df['sentiment']==0].sample(n=samples_per_class) df = pd.concat([positive_samples, negative_samples])为什么需要均衡采样?
如果不均衡,模型会偏向预测多数类(如全预测为正面),准确率虽高但实际无用。
2.4 文本清洗
步骤:
defclean_text(text):# 1. 去除特殊字符和数字 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z]',' ', text)# 2. 去除多余空格 text =' '.join(text.split())return text.strip()示例:
- 原始:
"这部电影太精彩了!!演员演技100分..." - 清洗后:
"这部电影太精彩了 演员演技分"
2.5 中文分词
使用 jieba 分词库:
import jieba words = jieba.lcut("这部电影太精彩了")# ['这部', '电影', '太', '精彩', '了']2.6 去停用词
停用词:指没有实际含义的常见词(如"的"、“是”、“了”)
stopwords ={'的','是','在','了',...} words =[w for w in words if w notin stopwords andlen(w)>1]示例:
- 分词后:
['这部', '电影', '太', '精彩', '了'] - 去停用词:
['电影', '精彩']
2.7 构建词汇表
将词语转换为数字索引:
word2idx ={'<PAD>':0,# 填充符号'<UNK>':1,# 未知词'电影':2,'精彩':3,'剧情':4,...}处理流程:
# 统计词频,只保留高频词for word, freq in word_counter.most_common(50000):if freq >= min_freq:# 最小词频阈值 word2idx[word]=len(word2idx)2.8 序列填充与截断
问题:评论长度不一,神经网络需要固定长度输入
解决方案:
- 短于
max_len:用<PAD>填充 - 长于
max_len:截断
deftext_to_indices(text, max_len=128): indices =[word2idx.get(w, word2idx['<UNK>'])for w in words]iflen(indices)> max_len: indices = indices[:max_len]# 截断else: indices = indices +[0]*(max_len -len(indices))# 填充return indices 3. Bi-LSTM 模型架构
3.1 词嵌入层 (Embedding)
作用:将离散的词语索引转换为稠密向量
self.embedding = nn.Embedding( num_embeddings=50000,# 词汇表大小 embedding_dim=128,# 嵌入维度 padding_idx=0# PAD 的索引)# 输入: [batch_size, seq_len] = [32, 128]# 输出: [batch_size, seq_len, embedding_dim] = [32, 128, 128]为什么需要 Embedding?
- One-hot 编码维度太高、稀疏
- Embedding 可以学习词语之间的语义关系
3.2 Bi-LSTM 层
核心组件:前向 LSTM + 后向 LSTM
self.lstm = nn.LSTM( input_size=128,# 输入维度(embedding_dim) hidden_size=64,# 隐藏层维度 num_layers=1,# LSTM 层数 batch_first=True, bidirectional=True# 双向)# 输入: [batch_size, seq_len, embedding_dim] = [32, 128, 128]# 输出: [batch_size, seq_len, hidden_dim * 2] = [32, 128, 128]LSTM 内部结构:
输入: xt ↓ ┌─────────────────────────────────────┐ │ 遗忘门 (ft): 决定丢弃哪些信息 │ │ ft = sigmoid(Wf · [ht-1, xt]) │ │ │ │ 输入门 (it): 决定存储哪些新信息 │ │ it = sigmoid(Wi · [ht-1, xt]) │ │ │ │ 候选值 (C̃t): 新候选值 │ │ C̃t = tanh(WC · [ht-1, xt]) │ │ │ │ 更新细胞状态: Ct = ft * Ct-1 + it * C̃t │ │ │ │ 输出门 (ot): 决定输出什么 │ │ ot = sigmoid(Wo · [ht-1, xt]) │ │ │ │ 隐藏状态: ht = ot * tanh(Ct) │ └─────────────────────────────────────┘ ↓ 输出: ht 双向拼接:
前向 LSTM: → → → → 后向 LSTM: ← ← ← ← 拼接输出: [→, →, →, →] + [←, ←, ←, ←] 3.3 注意力层 (Attention)
作用:让模型自动关注对情感判断更重要的词语
classAttentionLayer(nn.Module):defforward(self, lstm_output, mask):# 1. 计算注意力分数 attention_scores = self.attention(lstm_output)# [batch, seq_len, 1]# 2. 应用 mask(忽略 padding) attention_scores = attention_scores.masked_fill(mask ==0,-1e10)# 3. Softmax 归一化 attention_weights = F.softmax(attention_scores, dim=1)# 4. 加权求和 context = torch.bmm(attention_weights.unsqueeze(1), lstm_output)return context.squeeze(1), attention_weights 注意力权重可视化:
评论: "这部电影虽然节奏慢,但是剧情非常精彩" 权重: [0.05, 0.08, 0.06, 0.03, 0.08, 0.09, 0.15, 0.12, 0.34] ↑ ↑ 关注"但是" 最关注"精彩" 3.4 全连接层
self.fc = nn.Linear(hidden_dim *2, num_classes)# 输入: [batch_size, hidden_dim * 2] = [32, 128]# 输出: [batch_size, num_classes] = [32, 2]3.5 完整前向传播流程
defforward(self, inputs):# 1. Embedding embedded = self.embedding(inputs)# [32, 128, 128]# 2. Bi-LSTM lstm_output, _ = self.lstm(embedded)# [32, 128, 128]# 3. Attention context, attn_weights = self.attention(lstm_output, mask)# [32, 128]# 4. Dropout context = self.dropout(context)# 5. FC logits = self.fc(context)# [32, 2]return{'logits': logits,'attention_weights': attn_weights}4. 训练流程
4.1 损失函数
使用 交叉熵损失 (CrossEntropyLoss):
criterion = nn.CrossEntropyLoss()# logits: [batch_size, 2] 模型输出# labels: [batch_size] 真实标签 (0 或 1) loss = criterion(logits, labels)交叉熵公式:
CE = -Σ log(softmax(logits)[true_class]) 4.2 优化器
使用 Adam 优化器(自适应学习率):
optimizer = torch.optim.Adam( model.parameters(), lr=0.001,# 学习率 weight_decay=1e-5# L2 正则化)4.3 训练循环
for epoch inrange(num_epochs):# 训练阶段 model.train()for inputs, labels in train_loader:# 1. 前向传播 outputs = model(inputs) loss = criterion(outputs['logits'], labels)# 2. 反向传播 optimizer.zero_grad() loss.backward()# 3. 梯度裁剪(防止梯度爆炸) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)# 4. 更新参数 optimizer.step()# 验证阶段 model.eval()with torch.no_grad(): val_loss, val_acc = validate(val_loader)4.4 学习率调度
使用 ReduceLROnPlateau:验证损失不再下降时降低学习率
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=2)# 每个 epoch 后 scheduler.step(val_loss)4.5 早停 (Early Stopping)
best_val_loss =float('inf') patience_counter =0if val_loss < best_val_loss: best_val_loss = val_loss patience_counter =0 save_model(model)# 保存最佳模型else: patience_counter +=1if patience_counter >= early_stopping_patience:print("早停!")break5. 如何提高准确率
5.1 数据层面优化
5.1.1 增加训练数据
# 使用更多样本 python train.py --sample_size 100000# 从 2万增加到 10万原则:在不过拟合的前提下,数据越多越好。
5.1.2 数据增强
| 方法 | 说明 | 示例 |
|---|---|---|
| 同义词替换 | 用同义词替换关键词 | “精彩” → “出色” |
| 随机删除 | 随机删除词语 | “这部电影很精彩” → “这部电影精彩” |
| 回译 | 翻译成外文再翻译回来 | 中文 → 英文 → 中文 |
5.1.3 提高词频阈值
# 过滤低频词,减少噪声 python train.py --min_freq 8# 默认 55.2 模型层面优化
5.2.1 调整模型复杂度
原则:从简单开始,逐步增加复杂度
# 简单模型(防止过拟合) hidden_dim =48 num_layers =1 dropout =0.7# 复杂模型(如果数据充足) hidden_dim =256 num_layers =2 dropout =0.35.2.2 正则化技术
| 技术 | 作用 | 配置 |
|---|---|---|
| Dropout | 随机丢弃神经元,防止过拟合 | 0.5-0.7 |
| Weight Decay | L2 正则化,惩罚大权重 | 1e-4 ~ 2e-4 |
| 标签平滑 | 防止过度自信 | 0.1 |
# 标签平滑classLabelSmoothingLoss(nn.Module):def__init__(self, num_classes=2, smoothing=0.1): self.smoothing = smoothing self.confidence =1.0- smoothing defforward(self, logits, target):# 将硬标签 [0, 1] 转换为软标签 [0.05, 0.95]...5.3 训练层面优化
5.3.1 学习率调整
# 余弦学习率调度(带预热) scheduler = CosineLRScheduler( optimizer, num_epochs=20, warmup_epochs=3,# 前 3 轮线性增加学习率 min_lr_ratio=0.01# 最小学习率为初始值的 1%)学习率曲线:
lr │ ┌───┐ │ / \ ──── 余弦衰减 │ / \ │ / \ └─────────────────→ epoch 预热阶段 5.3.2 批次大小调整
# 较大批次 → 更稳定的梯度,但泛化可能变差 batch_size =256# 较小批次 → 更好的泛化,但训练不稳定 batch_size =32推荐:64 ~ 256
5.3.3 序列长度优化
# 缩短序列,减少噪声 python train.py --max_seq_len 100# 默认 1285.4 优化历程总结
| 版本 | 训练准确率 | 验证准确率 | 过拟合差距 | 优化措施 |
|---|---|---|---|---|
| 原始版 | 87% | 81% | 6% | 基线配置 |
| 优化版 | 86% | 81% | 5.5% | +Weight Decay, +Dropout |
| 超优化版 | 83% | 81% | 2% | +标签平滑, +余弦调度, 更小模型 |
关键发现:
- 降低模型复杂度比增加正则化更有效
- 验证准确率相同时,训练准确率越低越好(说明泛化更好)
6. 实战案例
6.1 快速开始
# 1. 进入项目目录cd qingan/stm # 2. 基础训练(2万条样本) python train.py # 3. 优化训练(6万条样本,防过拟合) python train_optimized.py # 4. 超优化训练(8万条样本,强防过拟合) python train_ultra.py --use_cosine_lr 6.2 自定义训练
# 使用全部数据 python train.py \ --sample_size -1 \ --num_epochs 20\ --hidden_dim 128\ --batch_size 64# 快速测试 python train.py \ --sample_size 5000\ --num_epochs 5\ --batch_size 1286.3 使用模型
from qingan.stm.stm_utils import STMUtils # 初始化 analyzer = STMUtils()# 分析单条文本 result = analyzer.analyze_sentiment("这部电影太精彩了!")print(result)# {'result': '积极', 'sentiment': 0.92, 'words': ['电影', '精彩'], 'message': '分析成功'}# 批量分析 texts =["太好了","很差劲","剧情一般"] results = analyzer.batch_analyze(texts)6.4 训练结果解读
训练完成后会生成:
models/ ├── stm_model.pth # 训练好的模型 ├── vocab.pkl # 词汇表 ├── training_history.json # 训练历史数据 ├── training_report.html # HTML 训练报告 └── images/ # 训练图表 ├── training_curves.png # 训练曲线 ├── metrics_comparison.png # Precision/Recall/F1 └── confusion_matrix.png # 混淆矩阵 训练曲线说明:
- Loss 下降:模型在学习
- 准确率上升:模型预测变准
- 训练/验证差距:过拟合程度
- 验证指标稳定:可以停止训练
7. 常见问题
Q1: 训练时显存不足怎么办?
# 减小批次大小 python train.py --batch_size 32# 减小序列长度 python train.py --max_seq_len 64# 减小模型规模 python train.py --hidden_dim 64 --embedding_dim 100Q2: 如何判断模型是否过拟合?
观察训练/验证准确率差距:
- 差距 > 5%:严重过拟合
- 差距 2-5%:轻微过拟合
- 差距 < 2%:良好
解决方案:
python train_ultra.py # 使用超优化版Q3: 为什么验证准确率不升反降?
可能原因:
- 学习率过大
- 正则化过强
- 数据分布不一致
解决方案:
python train.py --learning_rate 0.0005 --dropout 0.5Q4: 如何提高模型对特定词汇的敏感度?
方法 1:添加自定义词典
jieba.load_userdict('custom_dict.txt')方法 2:调整停用词列表
# 不要去掉对情感判断重要的词 stopwords.discard('不')# 保留"不"用于否定判断Q5: 可以用预训练词向量吗?
可以!修改 stm_model.py:
# 加载预训练词向量(如 Word2Vec、GloVe) pretrained_embeddings = load_word2vec('path/to/embeddings') self.embedding = nn.Embedding.from_pretrained(pretrained_embeddings)8. 进阶阅读
8.1 相关论文
| 论文 | 简介 |
|---|---|
| LSTM 原论文 | Hochreiter & Schmidhuber, 1997 |
| Bi-LSTM for NLP | 双向 LSTM 在 NLP 中的应用 |
| Attention Mechanism | 注意力机制详解 |
| BERT | 超越 LSTM 的 Transformer 架构 |
8.2 下一步优化方向
- 使用预训练模型:BERT、RoBERTa 等在中文情感分析上表现更好
- 多任务学习:同时预测情感、主题、评分
- 集成学习:融合多个模型的预测结果
- 在线学习:根据用户反馈持续更新模型
9. 总结
Bi-LSTM 情感分析的核心要点:
| 要点 | 关键 |
|---|---|
| 数据 | 均衡采样、去噪声、构建好词汇表 |
| 模型 | Bi-LSTM + Attention,从简单开始 |
| 训练 | Dropout + Weight Decay + 早停 |
| 优化 | 先调数据,再调模型,最后调训练参数 |
记住:好的数据比复杂的模型更重要!
源码获取:
大家点赞、收藏、关注、评论啦 、查看👇🏻获取联系方式👇🏻
👇🏻 精彩专栏推荐订阅👇🏻 不然下次找不到哟
2022-2024年最全的计算机软件毕业设计选题大全:1000个热门选题推荐✅
Java项目精品实战案例《100套》
Java微信小程序项目实战《100套》
Python项目实战《100套》
感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及文档编写等相关问题都可以给我留言咨询,希望帮助更多的人