ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyTorch微博情感分类实战:BiLSTM+Attention模型与文本清洗全流程

PyTorch微博情感分类实战:BiLSTM+Attention模型与文本清洗全流程 简介这是一套基于PyTorch的深度学习微博情感分类项目源码曾在课程作业中取得98分主要面向计算机相关专业需要完成课程设计、期末大作业的学生也适合希望通过项目实战提升建模能力的初学者。资源围绕微博短文本情感分类任务展开完整覆盖数据预处理、词向量表示、模型构建、训练与效果评估等关键环节能够帮助学习者快速掌握情感分类项目从零到一的具体落地方案。压缩包仅31KB共包含5个文件两个Python脚本train.py与test.py分别承担模型训练和测试推理两个Jupyter Notebookmodel_train.ipynb及对应的checkpoint版本提供了可逐步运行的交互式训练流程另有README.md说明文档对项目结构和使用方法做了简要介绍便于二次开发与仿写。页面目前已有209人学习浏览对于正在准备相似课题或需要参考高分课设模板的同学这套源码既能直接复现训练与预测流程也有助于理解PyTorch工程化项目的常用组织结构和调试思路。1. 高分课设里的pytorch微博情感分类难点从来不在模型微博情感分类在深度学习课设和大作业里出现频率极高因为一条微博评正面、负面还是中性直观、样本好找、指标可量化。但真动手做过的都知道这个项目翻车点比想象中多微博文本短且噪声大表情和话题标签怎么处理、分词用不用自定义词典、标签分布严重倾斜时模型会不会学成“全判中性”……这些才是决定项目最终是 70 分还是 95 分的关键。本文围绕“pytorch基础框架 深度学习 微博情感分类”这套标准组合把数据清洗、模型选型、训练调参到交付源码的全过程拆开讲。适合正在做课设的学生也适合想找一个完整文本分类范式来迁移的工程师。2. 微博语料的清洗与Vocabulary构建决定了模型上限2.1 微博文本的噪声比一般NLP数据集更“脏”微博和新闻、商品评论最大的区别是语言极度碎片化。一条真实微博里通常同时混了网页链接、用户名、#话题标签#、emoji、繁体字、谐音词、重复标点和“哈哈哈哈哈哈哈哈”这类连续语气词。深度学习模型读的是数字索引不是原始字符所以清洗阶段的质量直接影响embedding层能不能学到有效语义。常见做法是分三层处理先定向去噪再统一字符格式最后做分词与停用词过滤。import re import jieba def clean_weibo_text(raw: str) - str: # 1. 去掉URL text re.sub(rhttps?://\S|www\.\S, , raw) # 2. 去掉用户名保留话题标签 text re.sub(r[\w\u4e00-\u9fff\-], , text) # 3. 去掉重复标点保留一个 text re.sub(r([!?。])\1, r\1, text) # 4. 全角转半角 text text.replace(, ,).replace(, !).replace(, ?) # 5. 去掉连续重复的单个汉字比如“哈哈哈”压成“哈” text re.sub(r(.)\1{2,}, r\1, text) return text.strip()这段函数的核心逻辑是用四个正则表达式依次把噪声从文本里剥离。第 1 步去 URL 是因为http://...这一长串对情感没有贡献反而会撑爆max_len第 2 步保留话题标签是有意为之#票房大卖#本身可以作为情感信号第 3、4 步处理的是语气强度微博用户习惯用连续叹号表达强烈情绪压成单个标点可以控制词汇表的规模。参数上注意两点\u4e00-\u9fff只匹配常用汉字否则 后跟了英文名会被误判连续重复字符的压缩规则只针对单字不要对双字词做同样处理否则“手机手机手机”会直接变成“手机”丢失语气上的重复强调信息。2.2 分词策略jieba基础词典 自定义情感词表分词这一步课设项目里 90% 直接用 jieba 默认词典也能跑但标点容易碎网络新词拦不住。“绝绝子”“yyds”这种词一旦被切碎embedding 层永远学不到它们的整体语义。我一般会单独维护一个weibo_dict.txt自定义词典每行一个词格式是“词语 词频 词性”词频可以不填。# weibo_dict.txt 绝绝子 yyds yysd 破防了 集美们加载方式很简单jieba.load_userdict(weibo_dict.txt) words [] # 分词结果 stopwords set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) for raw in raw_texts: cleaned clean_weibo_text(raw) tokens [w for w in jieba.lcut(cleaned) if w.strip() and w not in stopwords] words.append(tokens)这里要注意一个和课设验收直接相关的点保存清洗后的中间结果。高分的课设项目通常要求你写清楚“做了什么、为什么做、效果如何变化”所以每步处理后的数据最好都存成独立文件比如cleaned_texts.json、tokens.json。答辩时老师问“你为什么去掉重复标点”你可以直接把处理前后对比贴出来这就是真实的工作量。2.3 构建词表、Padding与DataLoader词表构建有一套相对固定的参数min_count2即出现次数少于 2 的词直接丢弃否则词表里全是低频噪声max_vocab_size50000防止内存膨胀每个 batch 内长度对齐max_len64足够覆盖 90% 的微博文本——微博正文本身限 140 字清洗后平均长度在 30-50 个 token 之间。from torch.utils.data import Dataset, DataLoader from collections import Counter import torch class WeiboDataset(Dataset): def __init__(self, tokens_list, labels, vocab, max_len64): self.data [] self.labels labels for tokens in tokens_list: ids [vocab.get(t, 1) for t in tokens[:max_len]] # 1 unk ids ids [0] * (max_len - len(ids)) # 0 pad self.data.append(torch.tensor(ids, dtypetorch.long)) def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx], torch.tensor(self.labels[idx], dtypetorch.long) counter Counter() for tokens in words: counter.update(tokens) vocab {w: i2 for i, (w, c) in enumerate(counter.most_common()) if c 2} vocab[pad] 0 vocab[unk] 1 def collate_fn(batch): return torch.stack([x[0] for x in batch]), torch.tensor([x[1] for x in batch])collate_fn是一个人容易漏写的部分。不写它会发现同一个 batch 里句子长度不一致时报错因为 Python list 切片tokens[:max_len]得到的长度其实受原始文本长度影响短文本即便截断也到不了 64。上面的代码在__init__阶段就统一 padding 到了max_len所以collate_fn里只需要 stack 即可。2.4 标签分布检查提前发现类别不平衡在写模型前先做一次标签统计。微博情感数据集常见三类标签正面、负面、中性其中中性样本往往占 40%-50%而负面样本可能只有 20%。标签样本数占比正面1043242.1%中性983839.6%负面453018.3%这个分布直接决定你要不要用WeightedRandomSampler或者在 loss 里设置class_weight。如果什么都不做模型会倾向于把所有样本预测成多数类来刷 accuracy。这一点提前检查清楚后面调参时心里就有数了。3. 基于pytorch的BiLSTM Attention情感分类模型实现3.1 模型选型为什么不是TextCNN课设项目选模型第一原则是“能讲清楚”第二才是“效果更好”。TextCNN 只有一层卷积加池化代码短、运行快但答辩时能讲的技术点太少很难撑起一篇报告。BiLSTM Attention 兼顾了两头双向长短时记忆网络能体现序列建模能力Attention 机制又是一个可展开的理论点正好对应热搜里“深度学习”和“pytorch实战”这两个方向的考察需求。常见的做法是三层结构embedding 层负责把词索引映射成稠密向量BiLSTM 层提取上下文特征正反向各输出一个hidden_size维向量Attention 层对每个时间步的隐状态加权求和把序列压成一个固定维度向量最后过一个全连接层输出三个类别的 logits。3.2 核心模型代码与参数设计import torch import torch.nn as nn import torch.nn.functional as F class BiLSTMAttention(nn.Module): def __init__(self, vocab_size, embedding_dim100, hidden_size128, num_layers2, num_classes3, dropout0.5, pretrained_embeddingNone): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) if pretrained_embedding is not None: self.embedding.weight.data.copy_(torch.tensor(pretrained_embedding)) self.lstm nn.LSTM(embedding_dim, hidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0) self.attention nn.Sequential( nn.Linear(hidden_size * 2, hidden_size * 2), nn.Tanh(), nn.Linear(hidden_size * 2, 1) ) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size * 2, num_classes) def forward(self, x, maskNone): emb self.embedding(x) # [B, T, D] lstm_out, _ self.lstm(emb) # [B, T, 2H] # Attention加权求和 attn_score self.attention(lstm_out).squeeze(-1) # [B, T] if mask is not None: attn_score attn_score.masked_fill(mask 0, -1e9) attn_weight F.softmax(attn_score, dim1) # [B, T] context torch.bmm(attn_weight.unsqueeze(1), lstm_out).squeeze(1) logits self.fc(self.dropout(context)) return logits上面的代码里有几个直接影响训练效果的参数细节。padding_idx0告诉 embedding 层索引 0 对应的词向量永远是零向量不参与梯度更新。如果不设padding 位置会随机初始化反向传播时更新出一个非零向量等于人为注入噪声。bidirectionalTrue时output的最后一个维度从hidden_size翻倍成hidden_size * 2所以 Attention 和全连接层的输入维度必须同步写成hidden_size * 2这是初学者最容易报错的地方。masked_fill(mask 0, -1e9)这句针对的是 padding 位置。softmax 之后 padding 位置的权重应该为 0但如果不先把 score 压成一个极小的负数padding 位也能分到权重等于模型把“空位”当成了语义的一部分。3.3 预训练词向量直接用Word2vec别从零训从零训练 embedding 不是不行但微博语料里“绝绝子”“破防”这类词的语义学习需要大量共现样本课设数据量通常只有几万条效果很难出来。常见做法是在外部语料上训练好的词向量加载进来作为 embedding 初始化权重。gensim 训练两百维的 Word2vec 只需要几步这里的 embedding_dim 就要改成 200 保持一致。from gensim.models import Word2Vec # 训练部分 w2v_model Word2Vec(sentenceswords_vector, vector_size200, window5, min_count2, workers4, epochs10) # 构建词向量矩阵 embedding_matrix np.zeros((len(vocab), 200)) for word, idx in vocab.items(): if word in w2v_model.wv: embedding_matrix[idx] w2v_model.wv[word]window5是语义任务里表现比较稳的参数epochs10在十几万条微博语料上大约几分钟就能训完。加载后要把embedding.weight.requires_grad设成True让模型在训练中同步微调向量只冻住不做的话预训练向量和下游分类任务之间的适配会差一些。4. 训练循环、类别不平衡与调参路径4.1 损失函数和优化器怎么配from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau class_weights torch.tensor([1.0, 1.0, 2.5]).to(device) # 负面样本权重拉高 criterion nn.CrossEntropyLoss(weightclass_weights) optimizer Adam(model.parameters(), lr0.001, weight_decay1e-5) scheduler ReduceLROnPlateau(optimizer, modemax, patience2, factor0.5, verboseTrue)class_weights对应前文统计的 18.3% 的负面样本占比。按 1.0、1.0、2.5 的比例拉高负面样本的梯度贡献可以让模型不再无脑预测中性类。优化器选Adam而非 SGD 有两个原因一是 Adam 对学习率的敏感度低课设场景下不用花大量时间做学习率扫描二是微博情感分类的梯度噪声较大Adam 的动量和自适应学习率机制稳定得多。但要注意weight_decay1e-5是必须的否则 BiLSTM 这类参数量大的模型非常容易过拟合。损失函数这里出现了一个容易忽略的细节nn.CrossEntropyLoss内部已经包含了 softmax所以模型 forward 输出的是 logits 而不要提前做 softmax。很多第一次写的人会在 forward 里加F.softmax然后在损失函数里等待概率输入结果 loss 不降反升。4.2 完整训练循环与早停机制def train_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss, total_correct, total_num 0, 0, 0 for batch_input, batch_label in dataloader: batch_input, batch_label batch_input.to(device), batch_label.to(device) mask (batch_input ! 0).long() # 传给attention做mask optimizer.zero_grad() logits model(batch_input, mask) loss criterion(logits, batch_label) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() pred logits.argmax(dim1) total_loss loss.item() * len(batch_label) total_correct (pred batch_label).sum().item() total_num len(batch_label) return total_loss / total_num, total_correct / total_numclip_grad_norm_是 BiLSTM 训练中保质保量的一行代码。梯度裁剪把梯度的 L2 范数限制在 5.0 以内防止长序列反向传播时梯度爆炸导致 loss 变成 nan。早停策略要固定在验证集上判断不是训练集。每个 epoch 结束后在验证集上计算 loss连续 3 轮没有下降则停止训练并恢复最优模型参数best_f1, patience_counter 0, 0 for epoch in range(30): train_loss, train_acc train_epoch(...) val_loss, val_f1 validate(...) scheduler.step(val_f1) if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_model.pt) patience_counter 0 else: patience_counter 1 if patience_counter 3: breakscheduler.step(val_f1)接的是验证指标而不是 loss这样学习率只有在验证集上表现停滞时才会减半。4.3 评估指标不要只盯accuracy指标公式说明Accuracy(TPTN)/N类别不平衡时会虚高Macro F1各类F1的均值平衡了少数类表现Weighted F1按样本占比加权同时兼顾多数类和少数类Confusion Matrix3x3 矩阵直观展示错分规律我一般会用 sklearn 的classification_report一次性输出全部指标然后额外存一份混淆矩阵。如果发现负面样本大量被预测成中性先别急着加正则化回头检查class_weights是不是没生效或者负面样本占比实在太低需要做数据增强——把负面样本中的某些词替换成同义词是一种有效的提升手段。4.4 一张可复现的参数表参数名推荐值调整方向embedding_dim200语料不足时降到 100hidden_size128调大提升表达力注意过拟合num_layers23层以上大幅增加训练时间dropout0.5过拟合时升高欠拟合时降到 0.3max_len64微博短文本不用超过 128batch_size64显存充裕可以 128lr1e-3训练不稳定时降到 5e-4weight_decay1e-5可调大至 1e-3 缓解过拟合class_weight (负面)2.5错分增多时往上调5. 让课设源码真正可交付的三个小技巧课设提交不只是“模型能跑”还要让老师能一行命令复现、看到可视化结果、理解你的工作。这里分享三个从答辩实际情况中总结出的细节。第一个技巧是写一个独立的预测脚本。很多人把训练和推理写在同一个 notebook 里老师打开时得从头跑一遍才能看结果体验很差。单独建一个predict.py加载保存在本地的模型权重和同款 vocab命令行传入一条文本就能得到预测结果再配合一张 web 页面或者简单的脚本输出整个项目的完成度会高一个台阶。# 加载推理 import json, torch model.load_state_dict(torch.load(best_model.pt, map_locationcpu)) model.eval() def predict(text: str): cleaned clean_weibo_text(text) tokens [w for w in jieba.lcut(cleaned) if w.strip()] ids [vocab.get(t, 1) for t in tokens[:64]] ids ids [0] * (64 - len(ids)) with torch.no_grad(): logits model(torch.tensor([ids])) prob torch.softmax(logits, dim1).squeeze().tolist() return {正面: prob[0], 中性: prob[1], 负面: prob[2]}第二个技巧是把训练过程中的 loss 曲线和验证 F1 变化画出来存成图。matplotlib.pyplot每轮记录一次数值训练结束后保存为training_curve.png放进展览报告里一张图的说明力胜过三页文字。第三个技巧是模型上报个注意力权重最高的几个词。随机挑几条预测正确的样本把attn_weight最大的前 5 个词打出来你会直观看到模型把注意力放在“太好看了”“强烈推荐”这些情感词上这个细节能直接体现你对 pytorch 模型内部机制的理解深度。结合 attention 的可视化输出准备一篇以“数据清洗、不均衡处理、双向 LSTM 的序列语义、注意力权重分析”为主线的答辩讲稿这套源码就从“能跑”变成了“能讲透”。本文还有配套的精品资源点击获取
返回列表