新闻详情

新闻详情

首页 / 资讯中心 / 详情

Python LSTM三分类情感分析实战:从数据标注到模型调参

发布时间:2026/9/28 5:04:31来源:尧图网络
Python LSTM三分类情感分析实战:从数据标注到模型调参
简介这份资源是Python基于LSTM三分类的文本情感分析完整项目面向计算机相关专业正在做课程设计、期末大作业的学生以及需要项目实战练习的学习者。项目将文本情感划分为三类采用LSTM网络建模配套数据预处理、词向量训练与模型评估流程代码经导师指导并认可评审分99分可直接运行适合作为高分大作业参考。压缩包共13个文件约11.63MB包含3个csv情感语料、3个py训练与测试脚本、2个ipynb交互式笔记以及pkl词向量模型、h5权重文件、yml配置、md说明文档等覆盖从数据到模型落地的完整链路。目前已有307人学习下载。读者可据此掌握文本清洗、Word2vec词向量构建、LSTM三分类建模与测试评估的完整方案并借助文档说明快速复现与二次修改降低大作业与实战练习的入门门槛。1. 三分类情感分析到底在分什么从二分类到三分类的那道坎做电商评论、课程评价、App 反馈这类文本时很多人第一反应是二分类好评、差评。真上手跑一遍就会发现最难的从来不是把「垃圾」「太差了」判成负面而是那些「还行吧」「凑合能用」「不算差但也没惊喜」的句子——二分类模型会把它们硬塞进某一类置信度还经常在 0.5 附近晃。三分类要解决的就是这件事把情感拆成正面、中性、负面让「没态度」本身成为一个合法类别。Python 生态里做这件事LSTM 是最经典的基线方案配合分词、词向量和分类头就能跑通全流程源码结构清晰、文档好写也难怪它常年是课程设计和入门实战的高分选题。这篇笔记就按「数据怎么准备、LSTM 三分类模型怎么搭、参数怎么调、坑在哪」的顺序把一套能复现的方案讲透适合刚学完 Python 和 PyTorch、想拿一个完整项目练手的人也适合需要快速搭情感分析基线的从业者。2. 数据准备与三分类标注别让中性样本毁在标注上2.1 三分类的数据从哪来、怎么标三分类和二分最大的区别在数据。二分类可以靠打分阈值粗暴切分比如 1-2 星负面、4-5 星正面三分类必须给中性留出空间否则模型学到的「中性」只是正负之间的模糊地带预测时全挤在中间。常见做法是先用星级或评分做粗筛1-2 星负面、4-5 星正面3 星候选中性再人工过一遍把「3 星但明显是夸」或「3 星但明显在骂」的样本挪走。如果拿不到星级就用规则加人工含明确褒贬词的归正负纯陈述、疑问、无情感倾向的归中性。标注阶段最容易翻车的是类别不平衡。真实评论里中性往往最少可能只占 10% 到 15%。直接训练模型会倾向于把样本判成正负中性召回率低得可怜。我一般会先统计分布中性少于 15% 就做两件事一是对中性样本做同义替换、随机插入删除这类轻量增强二是训练时给中性类别更高的损失权重。下面这段代码就是加载数据并检查分布顺便把标签映射固定下来避免后面训练和预测时标签对不上。import pandas as pd from collections import Counter # 假设数据是 csv两列text 文本label 取值 0/1/2 df pd.read_csv(sentiment_3class.csv, encodingutf-8) df df.dropna(subset[text, label]).drop_duplicates(subset[text]) # 标签约定0 负面1 中性2 正面务必全流程统一 label_map {0: 负面, 1: 中性, 2: 正面} print(样本总数:, len(df)) print(类别分布:, Counter(df[label])) # 中性占比过低时给出提示方便决定是否做增强或加权 neutral_ratio (df[label] 1).mean() if neutral_ratio 0.15: print(f中性占比仅 {neutral_ratio:.2%}建议做数据增强或类别加权)这段代码做了三件事去空去重、打印分布、对中性占比做预警。drop_duplicates很关键爬来的评论经常有重复重复样本会让模型记住而不是学会。label_map只是给人看的真正训练用的是 0/1/2 这三个整数顺序一旦定下就不能改否则预测结果会整体错位。2.2 分词、词表与序列长度三个必须定死的参数中文文本进 LSTM 之前要变成整数序列。流程是分词、建词表、转 id、截断补齐。分词工具用 jieba 最常见词表按词频保留前 N 个低频词统一映射成unk。这里有三个参数必须定死词表大小vocab_size、最大序列长度max_len、以及pad和unk的 id。max_len怎么定不要拍脑袋写 100。先统计所有文本分词后的长度分布取 95 分位数再往上取整。太短会截掉关键情感词太长则 padding 过多LSTM 在长 padding 上白算还容易梯度消失。下面这段把分词、词表构建和序列转换一次做完。import jieba import numpy as np from collections import Counter # 1. 分词 def tokenize(text): return list(jieba.cut(str(text).strip())) df[tokens] df[text].apply(tokenize) # 2. 统计长度分布确定 max_len lengths df[tokens].apply(len) max_len int(np.percentile(lengths, 95)) max_len max(max_len, 10) # 兜底防止极端数据把长度压到个位数 print(95 分位长度:, max_len) # 3. 建词表保留高频词 vocab_size 20000 counter Counter() for tokens in df[tokens]: counter.update(tokens) special_tokens [pad, unk] word_list special_tokens [w for w, _ in counter.most_common(vocab_size - len(special_tokens))] word2id {w: i for i, w in enumerate(word_list)} pad_id, unk_id word2id[pad], word2id[unk] # 4. 转 id 并截断补齐 def encode(tokens): ids [word2id.get(w, unk_id) for w in tokens][:max_len] ids ids [pad_id] * (max_len - len(ids)) return ids df[input_ids] df[tokens].apply(encode) print(词表大小:, len(word2id), 序列长度:, max_len)逻辑上先分词再统计长度用 95 分位而不是最大值是为了避免个别超长文本把max_len拉到几百拖慢训练。词表保留 20000 是经验值中文情感任务里高频词覆盖已经够用再大收益很小还占显存。pad和unk放在最前面id 固定为 0 和 1后面 embedding 的padding_idx要设成pad_id这样 padding 不参与梯度更新。参数上vocab_size和max_len一旦确定训练、验证、预测必须用同一套建议把word2id和这两个值一起存成 json预测时直接加载别重新建词表。3. LSTM 三分类模型搭建从 embedding 到 softmax 的每一层3.1 模型结构怎么定embedding LSTM 全连接三分类 LSTM 的结构不复杂embedding 层把 id 变成向量LSTM 层抽序列特征取最后一个时间步的隐藏状态或做池化接全连接映射到 3 维最后 softmax。关键选择有几个用单向还是双向、取最后隐藏状态还是平均池化、要不要加 dropout。我的经验是情感分析这种整句判断任务双向 LSTM 通常比单向好因为它能同时看到前后文但如果要部署到流式场景、要求低延迟单向更合适。取特征时双向的输出不能直接取最后一个时间步因为反向那一路的「最后一步」其实是句首常见做法是对时间维做平均池化或最大池化再送全连接。下面是一个可直接用的模型定义。import torch import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes3, num_layers1, dropout0.3, pad_id0, bidirectionalTrue): super().__init__() # padding_idx 让 pad 的向量不参与更新 self.embedding nn.Embedding(vocab_size, embed_dim, padding_idxpad_id) self.lstm nn.LSTM( input_sizeembed_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalbidirectional, dropoutdropout if num_layers 1 else 0.0 ) feat_dim hidden_dim * (2 if bidirectional else 1) self.dropout nn.Dropout(dropout) self.fc nn.Linear(feat_dim, num_classes) def forward(self, input_ids): # input_ids: [batch, seq_len] mask (input_ids ! 0).float().unsqueeze(-1) # [batch, seq_len, 1] emb self.embedding(input_ids) # [batch, seq_len, embed_dim] out, _ self.lstm(emb) # [batch, seq_len, feat_dim] # 用 mask 做平均池化排除 padding 的干扰 out out * mask summed out.sum(dim1) counts mask.sum(dim1).clamp(min1e-6) pooled summed / counts logits self.fc(self.dropout(pooled)) return logits这段代码里padding_idxpad_id保证 padding 不更新mask是关键平均池化时把 padding 位置清零再除以真实长度否则短句会被大量 padding 稀释特征被拉平。bidirectionalTrue时feat_dim翻倍全连接输入维度要跟着改这是新手最容易写错的地方。dropout在 embedding 后和全连接前都起作用num_layers1时 LSTM 内部 dropout 无效所以用条件判断关掉避免警告。3.2 训练循环与损失函数类别加权和早停训练部分要处理两件事类别不平衡和过拟合。类别不平衡用带权重的交叉熵权重取类别频率的倒数归一化过拟合用验证集早停连续几轮验证损失不降就停。优化器用 Adam学习率 1e-3 起步LSTM 这类模型对学习率比较敏感太大容易震荡太小收敛慢。from torch.utils.data import Dataset, DataLoader import torch.nn.functional as F class TextDataset(Dataset): def __init__(self, ids, labels): self.ids torch.tensor(ids, dtypetorch.long) self.labels torch.tensor(labels, dtypetorch.long) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.ids[idx], self.labels[idx] # 按类别频率算权重中性样本少则权重高 counts np.bincount(df[label].values, minlength3) weights torch.tensor((1.0 / counts) / (1.0 / counts).sum() * 3, dtypetorch.float) device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMClassifier(len(word2id), embed_dim128, hidden_dim128).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss(weightweights.to(device)) train_ds TextDataset(df[input_ids].tolist(), df[label].tolist()) loader DataLoader(train_ds, batch_size64, shuffleTrue) best_loss, patience, wait float(inf), 3, 0 for epoch in range(20): model.train() total_loss 0 for ids, labels in loader: ids, labels ids.to(device), labels.to(device) optimizer.zero_grad() logits model(ids) loss criterion(logits, labels) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() avg_loss total_loss / len(loader) print(fepoch {epoch1} loss {avg_loss:.4f}) if avg_loss best_loss: best_loss, wait avg_loss, 0 torch.save(model.state_dict(), best_lstm.pt) else: wait 1 if wait patience: print(验证损失不再下降提前停止) breakweights的计算让中性类别拿到更高权重缓解不平衡。clip_grad_norm_是 LSTM 训练的后悔药梯度爆炸时没有它 loss 会直接变 nan。早停用训练损失只是简化写法严谨做法是划出验证集看验证损失这里为了代码紧凑先用训练损失演示实际项目务必切验证集。batch_size64是显存和稳定性的折中显存够可以到 128不够就 32。4. 评估与调参三分类不能只看准确率4.1 混淆矩阵和每类指标怎么读三分类的准确率有欺骗性。如果中性只占 10%模型全判正负也能有 90% 准确率但中性一个都抓不到。所以必须看每类的 precision、recall、f1以及混淆矩阵。重点关心中性那一行如果中性大量被误判成正或负说明模型没学到中性特征要么数据不够要么max_len截掉了关键上下文。from sklearn.metrics import classification_report, confusion_matrix model.eval() preds, trues [], [] with torch.no_grad(): for ids, labels in loader: ids ids.to(device) logits model(ids) pred logits.argmax(dim1).cpu().numpy() preds.extend(pred) trues.extend(labels.numpy()) print(classification_report(trues, preds, target_names[负面, 中性, 正面])) print(confusion_matrix(trues, preds))classification_report直接给出每类指标confusion_matrix看错分方向。如果中性 recall 低于 0.5优先补中性数据或提高它的损失权重如果正面和负面互相误判多多半是情感词覆盖不够考虑换更强的预训练词向量或加注意力。4.2 几个真正影响效果的参数调参不要盲目网格搜索先抓影响最大的几个。下表是我在实际项目里总结的参数优先级和调整方向。参数典型取值影响调整建议max_len40~80截断信息量按 95 分位定别拍脑袋embed_dim128~300表达能力数据少用 128数据多用 300hidden_dim128~256序列特征容量和 embed_dim 同量级即可num_layers1~2拟合能力先 1 层欠拟合再加dropout0.3~0.5防过拟合过拟合就加大lr1e-3~5e-4收敛速度震荡就减半优先级上max_len和类别权重最先调它们直接决定模型能不能学到中性其次是hidden_dim和dropoutnum_layers放最后两层 LSTM 在小数据上很容易过拟合收益不明显。5. 避坑与排查三分类 LSTM 最常见的五个翻车点5.1 预测时标签错位现象训练时准确率正常预测时正面判成负面整体像反了。原因训练用的 label 映射和预测时不一致或者word2id重新建了一遍id 对不上。解决把word2id、max_len、label_map一起存成 json预测时加载同一份绝不重新构建。5.2 loss 变成 nan现象训练几轮后 loss 突然 nan。原因梯度爆炸或者学习率太大。解决加clip_grad_norm_学习率降到 5e-4检查输入里有没有超长序列没截断。5.3 中性类别完全学不到现象混淆矩阵里中性全被分到正负recall 接近 0。原因中性样本太少或中性文本本身特征弱。解决补数据、做增强、提高中性损失权重必要时把中性单独做二阶段判断。5.4 验证集效果好上线就崩现象离线指标漂亮真实数据一跑就错。原因训练数据分布和线上不一致比如训练全是电商评论线上是社交短文本。解决上线前用真实分布的小样本做一次评估差异大就补充对应领域数据。5.5 padding 干扰池化现象短句预测普遍偏差。原因平均池化没排除 padding短句被大量 0 稀释。解决像第 3 章那样用 mask 做池化或者改用pack_padded_sequence。6. 让三分类更稳的一个技巧把中性拆出来单独看跑通基线之后如果中性还是不稳我一般会做一个改动不直接三分类而是先做正负二分类再对「置信度低」的样本做中性判断。具体做法是二分类模型输出概率落在 0.4 到 0.6 之间的样本交给一个专门的中性分类器或规则判断。这样中性不再和正负抢特征召回率往往能提一截。验证方法是分别统计二分类阶段和中性阶段的指标看整体 f1 是否比直接三分类高。另一个实用技巧是给模型加一个简单的注意力层让情感词权重更高。实现上就是在 LSTM 输出后加一层nn.Linear(feat_dim, 1)算权重softmax 后加权求和再分类。代码不长但对「虽然便宜但是质量差」这种转折句效果明显。我自己的习惯是先把基线跑稳再动结构别一上来就堆注意力、堆层数那样出了问题根本不知道是哪一层的事。这套三分类 LSTM 方案的价值在于结构透明、每一步都能解释适合作为情感分析的起点也适合作为课程设计的高分底子。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

ProxySQL读写分离实战:从主从复制到流量路由与故障切换 2026/9/28 7:02:00

ProxySQL读写分离实战:从主从复制到流量路由与故障切换

读写分离系列写到了第十篇。前面聊过主从复制原理、从库搭建、半同步复制、延迟监控,这一篇终于要把流量真正“分开”的关键组件摆上台面:ProxySQL。这几年我处理过不少读写分离做不下去的线上项目,共同点几乎一样——不是不会搭主从&#xf…

阅读更多 →
GESP二级真题:用二维数组判断等差矩阵的解题思路与调试技巧 2026/9/28 7:02:00

GESP二级真题:用二维数组判断等差矩阵的解题思路与调试技巧

每年GESP二级的题目里,二维数组都是重头戏。今年3月的“等差矩阵”,看起来是个数学名词,实际考察的是你对数组下标的掌控力。很多学生考完说:明明每一行都判断了,怎么还是错?问题多半出在列判断的循环写法上…

阅读更多 →
pgBackRest增量备份报错:WAL summarization未开启的定位与修复指南 2026/9/28 7:01:54

pgBackRest增量备份报错:WAL summarization未开启的定位与修复指南

凌晨四点十七分,告警群突然刷屏。定时任务里pgbackrest backup的日志停在一条 ERROR 上:incremental backups cannot be taken unless WAL summarization is enabled。数据库实例本身一切正常,CPU、连接数、慢查询都没有波动,但备…

阅读更多 →
Agent工具调用实战:从设计到落地的完整指南 2026/9/28 7:01:54

Agent工具调用实战:从设计到落地的完整指南

1. 工具调用:Agent 从“会说”到“会做”的分水岭很多人做 Agent 做到第七篇的时候,手里已经有一个能聊天、能记住上下文、能按角色设定回复的“对话体”了。但只要你稍微往真实场景里推一步,立刻就会发现一个尴尬的事实:它除了说…

阅读更多 →
协程原理深度拆解:从线程到事件循环,看清挂起与恢复的底层机制 2026/9/28 7:01:54

协程原理深度拆解:从线程到事件循环,看清挂起与恢复的底层机制

协程这个概念,我在面试里被问到过很多次,也在不同语言的项目里被折腾得不轻。第一次接触Python的async/await时,我脑子里一直绕着一个问题:你说挂起就挂起,那挂起的时候函数里的局部变量到底放在哪了?恢复的…

阅读更多 →
从零搭建答疑机器人:Spring AI 实战与避坑指南 2026/9/28 7:01:54

从零搭建答疑机器人:Spring AI 实战与避坑指南

1. 从零搭建答疑机器人前,先把这几个问题想透做答疑机器人这件事,我前前后后折腾过三套方案,从最早的规则匹配到后来的检索增强,再到现在的纯大模型驱动,踩的坑足够写一本小册子。很多人一上来就问“用哪个模型”“怎么…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉