资讯动态

微博情感分类实战:从分词到LSTM与BiGRU的完整NLP流程

发布时间:2026/10/14 11:33:51 来源:尧图企业网站定制
简介面向自然语言处理情感分析的新人这是一套微博内容情感分类的完整实战源码覆盖数据分析、文本预处理、模型训练与效果测评全流程并配有博主文字教程能帮助初学者快速打通从数据到模型的实践链路。压缩包共二十五份文件以脚本文件为主其中脚本涵盖数据清洗、词表构建、词向量训练、模型训练与评估等环节辅以可视化图片、停用词表与说明文档、预训练词向量及映射文件整体仅三兆字节左右轻量易下载。项目中实现了长短期记忆网络加注意力机制、双向门控循环单元等经典情感分类模型训练过程中保存了多组超参数下的损失与准确率曲线图便于对比学习率、隐藏层维度等设置对结果的影响同时可用于课程设计、毕业设计入门或算法复现。该资源已有三千零七十人学习是经过验证的新手友好型实战素材。1. 微博情感分类实战这份资源把 NLP 入门的四个大坑一次填平做中文 NLP 情感分析的新人最先撞上的往往不是模型而是数据分词结果乱七八糟、词表里全是低频噪声、训练时 loss 一路横盘、好不容易收敛了换个数据集立刻失效。这份微博情感二分类实战源码把「数据分析 → 预处理 → word2vec 训练 → 两个模型训练 → 测评」的完整链路一次走通适合想用深度学习做中文情感分析、又不想从零踩坑的入门者。代码结构清晰带配置文件和训练图表照着跑一遍就能建立起对 NLP 项目的整体手感。2. 数据预处理从原始微博到 word2id先过中文分词的坎2.1 数据集的四份文件与标签分布项目里有train_org.txt、train.txt、test_org.txt、test.txt四份文本。带org后缀的是原始文本不带的是预处理后的版本训练与测试分开存放避免了标签泄漏的问题。如果你想复现到这个数据集打开任意一份原文件就能看到每一行的存储格式前半部分是标签后半部分是微博正文两者用分隔符隔开。这种一行一条样本的格式是文本分类最常见的数据组织方式后续做 batch 读取时最省事。数据处理前第一件事是看标签分布。项目中images/label_distrubution.png就是干这个的它直接画出了正负样本的比例。做情感二分类时如果正负样本接近 1:1可以直接用准确率做主要指标如果某一类明显偏多就要考虑用 F1 值或者在后处理时调整分类阈值。我一般处理微博这类社交文本时会顺手检查一下样本里有没有重复文本、有没有空行——这类脏数据经常让训练集的 loss 表现很好测试集却莫名其妙地崩。2.2 停用词表与分词别把情感词也过滤掉中文停用词词表.txt是项目自带的过滤底表preprocess.py会按顺序完成原始文本的清洗、分词和停用词过滤。常见的清洗规则包括去掉 URL 链接、去掉 用户昵称、把数字统一换成占位符、过滤掉长度过短的残句。这些看起来琐碎但对结果影响很大——一条微博里大量出现的无效字符如果原样进入词向量训练会白白占掉词表的位置。分词阶段项目常用的做法是调用结巴这类中文分词工具。跑preprocess.py时要注意一个参数是否保留停用词表之外的词。停用词的本意是过滤掉「的、了、在」这类无信息量词但「不要」「不太」「绝了」中的「不」「太」恰恰是情感转折的关键词如果在停用词表里直接删掉它们句子的情感极性可能被反转。项目自带的停用词表对新手来说可以先用但等你换到自己的业务文本时一定得把「不」「没」「别」这类否定词从停用词表中捞回来。2.3 用 gensim 训练词向量word2vec.bin 的选型参数gensim_word2vec.py负责从预处理后的语料里训练出word2vec.bin。对微博这种短文本word2vec 的静态词向量依然是性价比最高的选择不需要 GPU几分钟就能训练完而且后续接 LSTM 或 BiGRU 时直接用预训练权重初始化 Embedding 层收敛速度会明显快于随机初始化。# 参考词向量训练配置与项目中的 gensim_word2vec.py 思路一致 from gensim.models import Word2Vec # 语料是 preprocess.py 输出后的分词列表 sentences [line.strip().split() for line in open(train.txt, encodingutf-8)] model Word2Vec( sentences, vector_size128, # 词向量维度与模型 hidden_size 匹配 window5, # 上下文窗口微博短文本用 5~7 都可以 min_count2, # 低于 2 次的词直接丢弃控制词表规模 sg0, # 0 用 CBOW1 用 Skip-gram小语料推荐 CBOW workers4, # 多线程训练 epochs10 ) model.save(word2vec.bin)这里最关键的参数是vector_size。项目中模型的hidden_size是 128词向量维度也设成 128这样 Embedding 层初始化时可以直接互相映射不需要额外做维度线性变换。min_count2是一个适合微博语料的保守值——微博噪声词多词频太低的词学了也是噪声不如直接丢弃。sg的选择上我自己的习惯是语料小于 10 万句用 CBOW大于 50 万句用 Skip-gram前者在小语料上更稳后者对大语料的长尾词更友好。2.4 word2id 构建与 OOV 兜底策略word2id.json是训练语料中每个词和数字 ID 的映射表。构建它看起来只是给每个词排个序但有个细节必须处理为填充符和未知词预留 ID。常见的做法是0留给PAD1留给UNK其余词从2开始编号。这并不是多余的仪式感——batch 内句子长度不一padding 的 token 要有一个专门 ID测试集里出现训练集没见过的词映射时也要落到UNK而不是直接报错。# 构建 word2id保留 PAD 和 UNK 的固定位置 UNK_TOKEN, PAD_TOKEN UNK, PAD word2id {PAD_TOKEN: 0, UNK_TOKEN: 1} for line in open(train.txt, encodingutf-8): for word in line.strip().split(): if word not in word2id: word2id[word] len(word2id) # 超过词表上限的词也可映射为 UNK避免词表过大 max_vocab_size 50000 word2id dict(list(word2id.items())[:max_vocab_size]) import json with open(word2id.json, w, encodingutf-8) as f: json.dump(word2id, f, ensure_asciiFalse, indent2)加一个max_vocab_size上限的做法可能和项目代码不完全一致但值得保留在思路上微博语料会产出大量只出现一两次的人名、数字串和品牌名全部收进词表只会拖慢训练。超过上限的词统一映射到UNK效果往往比把一个超大稀疏词表喂进模型更好。3. 数据加载与超参配置把 batch、维度、正则项的边界划定3.1 data_loader.pypadding、shuffle 与 tensor 化的顺序data_loader.py做的是把word2id.json映射后的序列切成 batch并补齐到统一长度。很多新人会忽略一个顺序问题应该先对原始序列做截断或 padding再转 tensor最后放进 batch。如果先转 tensor 再 padding不同长度的 tensor 无法直接堆叠代码就会在torch.stack处报错。# 参考 data_loader 的 batch 构造逻辑 import torch from torch.utils.data import Dataset, DataLoader class WeiboSentimentDataset(Dataset): def __init__(self, texts, labels, word2id, max_len128): self.texts texts self.labels labels self.word2id word2id self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): tokens self.texts[idx].strip().split() # 截断超长文本保留前半部分 tokens tokens[:self.max_len] token_ids [self.word2id.get(w, self.word2id[UNK]) for w in tokens] # 短文本补 PAD保证 batch 内形状一致 token_ids token_ids [self.word2id[PAD]] * (self.max_len - len(token_ids)) return torch.tensor(token_ids, dtypetorch.long), torch.tensor(self.labels[idx], dtypetorch.long) # 组合 DataLoadershuffle 只对训练集开启 loader DataLoader(dataset, batch_size64, shuffleTrue, num_workers4)max_len128对微博正文来说是够用的量级绝大多数文本在这个长度内已经能表达完整情感。pad操作的本质是制造假数据所以后续代码里必须配合torch.nn.utils.rnn.pack_padded_sequence或 attention mask 把 PAD 的位置遮掉否则模型会把这些无意义位置也当成有效输入来学习。新手常犯的一个错误是只看 input 的形状对不对忽略了 PAD 位置参与运算导致的噪声——这会让训练曲线非常难看。3.2 configuration.py每个超参数是给哪一层用的项目中的configuration.py把模型相关的参数聚合在一起文件里能看到epoch、batch_size、hidden_size、lr、dropout、weight_decay这些字段。资源中的训练图表命名直接暴露了默认参数组合比如e100_b64_h128_lr0.0001_drop0.5_wd0.0005表示训练 100 轮、batch 大小 64、隐藏层维度 128、学习率 0.0001、dropout 0.5、权重衰减 0.0005。把这些参数和代码对应起来你才知道调参时动的是哪一块。参数名对应模块作用边界hidden_size128LSTM / GRU 隐藏维度决定句子表示向量的容量过大容易过拟合lr0.0001~0.001Adam 优化器0.001 适合起步loss 震荡时降到 0.0001dropout0.5Embedding 输出后、分类层之前抑制过拟合0.3~0.5 是文本分类常见区间weight_decay0.0005Adam 的 L2 正则给权重加衰减间接防过拟合batch_size64DataLoader太小 loss 震荡大太大收敛慢微博短文本 64 适中weight_decay是一个容易被忽略的参数。对 Adam 优化器来说如果不设置weight_decay默认是 0意味着模型在训练后期几乎是靠 dropout 在孤立地防过拟合。项目中同时启用 dropout 0.5 和 wd 0.0005属于双保险思路适合样本量不高的场景。你可以对比一下e50_b64_h128_lr0.001_drop0.5_wd0.0005_Loss.png和e100_b64_h128_lr0.0001_drop0.5_wd0.0005_Loss.png这两张图前者 lr 高但轮数少后者 lr 低但轮数多观察 loss 曲线的下降斜率差异就能直观感受到 lr 对收敛速度的影响。3.3 func.py训练辅助函数里的后悔药逻辑func.py是工具函数集合其中至少包含两类东西。一类是训练过程中的指标记录函数把每个 epoch 的 loss 和 acc 记录下来并调用 matplotlib 画图输出到images/目录另一类是模型保存与加载逻辑。训练时只看最后一个 epoch 的参数是新手常犯的错误正确做法是在训练过程中记录验证集上表现最好的那一次参数训练结束后再加载回来。这种中途存档、结束后取最优的做法就是模型的后悔药——不用重跑训练也能确保你拿到的是整个训练过程中最稳的模型。# 参考 func.py 中最优模型保存逻辑 def save_best_model(model, optimizer, epoch, valid_acc, best_acc, save_path): if valid_acc best_acc: best_acc valid_acc state { epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_acc: best_acc, } torch.save(state, save_path) # 覆盖写同一个文件避免磁盘囤积 return best_acc保存时把optimizer_state_dict也存下来是值得保留的习惯。如果在之后想从断点继续训练只有模型权重没有优化器状态Adam 的动量信息就丢了恢复训练的初始几步 loss 会出现明显跳动。这个文件和main.py配合的典型流程是main.py里循环 epoch → 每个 epoch 结束计算验证集指标 → 调用func.py中的保存函数。4. 模型设计对比LSTM-Attention 与 BiGRU 的分类能力拆解4.1 为什么情感分类不能只靠词向量如果只用 word2vec 把一句话里的词向量取平均你得到的是一个静态句子表示词序信息完全丢失。「我不喜欢」和「喜欢不我」会得到几乎相同的向量分类器很难区分。循环神经网络的作用就是把每个词的上下文信息和词序编码进 hidden state让最终的句子表示是「动态」的。项目中的两个模型文件都遵循这个思路词向量负责提供静态语义循环层负责在上下文中重组语义最后接全连接层输出二分类概率。输入到模型的张量形状是[batch_size, seq_len, embedding_dim]。对每个 batchseq_len 由max_len统一embedding_dim 与词向量维度一致。这里要强调的是词向量可以用预训练的冻结参数也可以在训练中微调。项目里更推荐的做法是让词向量参与训练——微博语言和通用语料差异较大微调能让 Embedding 学到微博语境下的特殊语义。4.2 LSTM-Attention 的结构拆解lstm_attention.py的核心是用注意力机制替代取最后一个 hidden state的做法。LSTM 在每个时间步都会产出一个 hidden state如果只取最后一个句子前半部分的信息经过多步传递后会弱化。Attention 的思路是给每个时间步的 hidden state 算一个权重最后把所有 hidden state 按权重加权求和得到一个携带全句重点信息的上下文向量。# LSTM-Attention 的核心计算片段参考项目结构示意 import torch import torch.nn as nn import torch.nn.functional as F class LSTMAttention(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_size128, num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_size, batch_firstTrue, bidirectionalFalse) # 注意力权重参数把 hidden_size 映射成标量 self.attn nn.Linear(hidden_size, 1, biasFalse) self.fc nn.Linear(hidden_size, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): embeds self.dropout(self.embedding(x)) # [batch, seq_len, embed_dim] lstm_out, (h_n, c_n) self.lstm(embeds) # lstm_out: [batch, seq_len, hidden] attn_scores self.attn(lstm_out).squeeze(-1) # [batch, seq_len] attn_weights F.softmax(attn_scores, dim1) # 在序列维度上归一化 context torch.bmm(attn_weights.unsqueeze(1), lstm_out).squeeze(1) # 加权求和 logits self.fc(self.dropout(context)) return logits注意力权重在F.softmax之前可以加上 mask把 PAD 位置对应的分数替换成一个极小的负数这样 softmax 之后 PAD 位置的权重趋近于零不会污染上下文向量。这个细节在 3.1 中提到过在模型实现里同样重要。训练结束后你可以把测试集的attn_weights取出来按 token 打印能直观看到模型把注意力给了哪些词——通常会发现「太」「很」「垃圾」「喜欢」这类强情感词权重最高。这也是 Attention 机制除了提精度之外的另一层价值给模型的判断提供了一个可解释的参考依据。4.3 BiGRU 的对称结构与参数量优势bigru.py里实现的是双向 GRU。GRU 相比 LSTM 少了门控中的 cell state参数量更小在小数据集上更不容易过拟合。双向的含义是把文本从前往后和从后往前各跑一遍循环网络把两个方向的最后一个 hidden state 拼接成一个2 * hidden_size的向量。对微博这种短文本后文对前文的语义修正同样重要「电影好看但是票价贵」这种转折句反向编码能把「贵」的负面信号有效保留下来。# BiGRU 参考实现重点关注双向输出的拼接 class BiGRUClassifier(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_size128, num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.bigru nn.GRU(embed_dim, hidden_size, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden_size * 2, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): embeds self.dropout(self.embedding(x)) gru_out, h_n self.bigru(embeds) # 取最后一个时间步的前向与后向 hidden 拼接 batch_size x.size(0) h_forward h_n[-2, :, :] # 前向最后一步 h_backward h_n[-1, :, :] # 后向最后一步 h_concat torch.cat((h_forward, h_backward), dim1) # [batch, hidden*2] logits self.fc(self.dropout(h_concat)) return logits文本长度超过 30 时双向 GRU 直接取最后一个 hidden state 的效果往往不如 Attention 加权平均因为长距离信息在两个方向上传递都会产生损耗。这属于一个可以诚实面对的现象不是模型越复杂效果越好而是模型类型和数据长度之间存在匹配关系。项目把两个模型放在一起本身就提供了对比实验的素材——同一个数据集上跑两份代码比对自己的具体数据场景做选型判断比网上的标准答案可靠得多。4.4 选型建议从训练速度和可解释性两个角度选根据项目代码给出的默认配置LSTM-Attention 在参数量上略大于 BiGRU训练时间也会相应变长。如果训练资源有限先跑 BiGRU 拿到一个合理的 baseline 分数再跑 LSTM-Attention 验证是否带来提升——这是文本分类调参的标准流程。反过来如果你后续要做注意力权重的可视化分析或者需要在报告里说明模型看了哪些词LSTM-Attention 是更合适的选择。注意两份代码都要从pretrained参数处检查一下word2vec.bin是否正确加载到 Embedding 的weight.data——我见过不少复现失败的案例最后发现是模型根本没读到预训练向量Embedding 层从头到尾都在随机初始化。5. 训练避坑指南Loss 不降、Acc 震荡、过拟合的排查清单跑通这份源码并不难但要训练出像images/目录里那样平滑的曲线有几个坑几乎每个新人都躲不掉。这里列的是我复现同类项目时的真实排障记录按「现象 → 原因 → 解决」展开。5.1 训练 loss 下降了验证集 acc 却越来越差这个现象几乎可以定义为文本分类的第一道坎。训练集上 loss 一路走低验证集上 acc 却在一个 epoch 后开始下滑说明模型已经进入过拟合状态。微博语料本身的噪声多、表达方式碎片化模型很容易记住训练集中的高频微博句式却没有学到泛化性的情感模式。解决思路有两条路线第一增大dropout到 0.6 并把weight_decay从 0.0005 提到 0.001增强正则第二检查max_len是否过长——对短文本任务超长的 padding 区域反而给了模型更多记忆噪声的空间。项目中同时存在e50和e100的图表对比同名参数的两个文件就能看出e100 版本后半段曲线出现抖动这是典型的过拟合信号。5.2 模型训练完加载保存的模型去预测效果不如最后一次训练的这是「保存时机」问题。如果main.py里只在全部 epoch 结束后保存一次你拿到的是训练末尾的模型A 同学最常见的改法是每个 epoch 都保存然后手动挑一个最好的但这样磁盘会存下几十份权重文件。项目里func.py提供的模式是「验证集表现最好时覆盖保存」直接把最优状态锁在一个文件中既省空间又保证效果。你需要检查保存条件是valid_acc best_acc还是train_acc best_train_acc——后者会让模型选择过程也过拟合必须换成验证集指标做判断。5.3 加载 word2vec.bin 时提示文件格式不兼容word2vec.bin是 gensim 训练出的二进制模型gensim 4.x 和旧版本在加载接口上有差异。如果你用高版本 gensim 直接执行Word2Vec.load(word2vec.bin)报错多半是版本不匹配项目代码在 Python 3.6 环境下编译__pycache__目录里的.pyc后缀为cpython-36说明原作者的运行环境比当前主流版本旧。解决方式是在加载时用KeyedVectors.load_word2vec_format读取或者在预处理阶段直接用 gensim 在线训练替代预加载。另外.pyc缓存文件不会自动适配当前 Python 版本如果 import 报错把__pycache__整个目录清掉再跑。# 兼容 gensim 版本差异的加载写法 from gensim.models import KeyedVectors try: model Word2Vec.load(word2vec.bin) pretrained_vecs model.wv except TypeError: # 旧格式模型用下面这种方式读取向量部分 pretrained_vecs KeyedVectors.load(word2vec.bin, mmapr) embedding_matrix pretrained_vecs.vectors # 按 word2id 顺序重排后赋给模型5.4 测试集上准确率不差但新来的微博预测效果很差这是 OOV 问题的外延。训练语料覆盖有限新微博里会出现训练词表中不存在的词它们被统一映射为UNK。如果UNK在测试集里的占比过高模型就失去了判断依据。排查方式是统计测试集中 UNK 的占比如果超过 10%需要在预处理阶段补充领域词或者降低min_count到 1 以扩容词表。另一个容易忽略的坑是在训练前的预处理里不要把「哈哈哈」「笑死」这类微博高频语气词全部清洗掉——它们在情感分类中是强信号词过滤掉等于自断一臂。排查点判断方式处理方向过拟合训练 acc 高但验证 acc 低增大 dropout / wd缩短 max_len收敛过慢loss 曲线长时间不降lr 从 0.0001 提到 0.001检查是否加载词向量UNK 过多统计 OOV 比例超 10%重训词向量min_count 下调扩充领域语料训练不稳定同参数多次训练结果方差大固定随机种子检查 DataLoader shuffle 设置6. 进阶用法把这份微博情感项目迁移到自己的文本场景跑通项目之后你要做的不是停在这里而是把整套流程移植到自己的场景里。迁移时最重要的判断是「哪些能复用、哪些必须重造」。词向量和停用词表必须基于新语料重新生成因为不同领域的用词习惯差异非常大而word2id的构建逻辑、模型结构、训练循环这些代码骨架可以原样保留。比如把目标换成电商评论的情感分析你只需要修改数据加载路径和标签字段配置好新的停用词表重新跑一遍preprocess.py和gensim_word2vec.py两个模型就能直接训练出适配新任务的分类器。如果你连标注数据都没有也有一个实用技巧拿这份微博语料做预训练让模型先学会「情感语言的基本表达」再拿几十条人工标注的电商评论做微调最终效果比从零训练好得多。超参数迁移也有可参考的方向。项目默认的 lr 0.001 面向的是规模不大的微博语料如果你迁移到长文本场景max_len调到 256 或 512注意力机制带来的收益会更明显。训练时固定随机种子是一个值得养成的习惯——不固定 seed 的话同一个模型跑两次acc 可能相差两个百分点你根本分不清调参是否真的有效。批量跑不同超参组合时把每次实验对应的 Loss 图和 Acc 图按「参数名 时间戳」命名保存比看一组数字直观得多。项目中的图表命名本身就演示了这个规范e100_b64_h128_lr0.0001_drop0.5_wd0.0005_Loss.png一目了然你完全可以沿用这套命名逻辑。作为验证手段我最后会从测试集里抽 20 条样本把注意力权重打印出来看看模型在负面样本上是否真的关注到了「欺骗」「垃圾」「气愤」这类核心词——这个检查每年都帮我抓住过至少一次数据预处理层面的疏漏。从那以后每次换新数据集我都强制走一遍「分词检查 → OOV 统计 → 最优模型回载 → 注意力可视化」四步流程这几个动作加在一起基本锁死了复现项目时的翻车区间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑