ARTICLE DETAIL

资讯详情

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

PyTorch微博评论文本分类实战:三个基线模型均超97%

PyTorch微博评论文本分类实战:三个基线模型均超97% 简介面向中文微博评论情感分类任务的完整数据与PyTorch代码适合自然语言处理初学者、算法工程师及情感分析研究者用于基线对比和快速实验。资源基于十万级中文语料中的微博评论数据共计十一万余条带情感标注样本正负类别基本均衡并给出负面、正面二分类标签。项目中实现了双向长短时记忆网络加注意力机制、文本循环卷积网络、快速文本分类三种模型准确率分别达到百分之九十七点九二、九十七点八七和九十七点六五超参数与模型定义集中在同一文件内便于理解与调参。压缩包共十七个文件大小十九点八兆包含七个派森脚本、五个文本说明、两个向量矩阵、一个模型权重、一个序列化文件及一份项目说明覆盖数据预处理、模型训练、评估和权重保存流程并附带训练结果下载链接。目前已有三十二人学习适合需要可直接运行的中文情感分类代码作为参考。1. 微博评论文本分类为什么三个基线模型都能上 97%把微博评论做文本分类最容易翻车的不是模型选型而是数据预处理和超参配置。这份基于 PyTorch 的微博评论文本分类代码用 weibo_senti_100k 的 119988 条带情感标注评论训练了三个模型BiLSTM_Att 到 97.92%TextRCNN 到 97.87%FastText 到 97.65%全部落在 models 目录里超参数直接写在模型文件中不用到处翻参数表。数据集是中文情感分类里很典型的正负二分类正向 59993 条、负向 59995 条基本均衡普通单卡甚至 CPU 都能跑完。适合刚入门文本分类、想找一份能直接跑通的 LSTM 文本分类代码的人也适合准备做舆情评论极性分类、不想从零调参的从业者。2. 数据准备与预处理12 万条评论先过 utils.py 这一关2.1 压缩包目录结构与数据形态拿到 Traditional_Net_Classification-main 压缩包先别急着跑 run.py把目录结构扫一遍比先跑通更重要。根目录下是 utils.py、train_eval.py、run.py 三个核心脚本WEIBO 目录放原始微博评论数据saved_dict 目录存放训练好的模型权重models 目录下面是模型定义文件BiLSTM_Att.py、TextRCNN.py、FastText.py外加 FastText 专用的 utils_fasttext.py。README.md 里写清了环境要求和复现步骤。这份数据来自 ChineseNlpCorpus 的 weibo_senti_100k119988 条评论都属于情感倾向性分析没有中性样本只有 negative 和 positive 两类。正向 59993 条负向 59995 条比例几乎 1:1。这个分布对分类器非常友好不需要处理类别不平衡直接交叉熵损失就能稳定收敛。数据形态上就是一条评论配一个标签原始格式通常是 CSV 或者按行分隔的文本文件。目录/文件作用run.py入口脚本解析参数并调用训练和评估train_eval.py训练循环、验证、保存最优模型utils.py数据读取、清洗、字切分、paddingWEIBO/微博评论原始数据目录saved_dict/训练好的模型权重目录models/三个模型的网络结构和超参数定义权重文件如果不在压缩包里按 README 里给出的下载链接获取放到 saved_dict/ 之后可以直接加载做验证。我自己拿到这类资源时习惯先把 README 读完再动手因为环境版本、数据路径、权重下载方式这三个信息都写在里面少了任何一个都会在复现中途卡住。2.2 utils.py 的文本清洗与字级别切分微博评论短、口语化、表情符号和语气词多比如「哈哈哈哈」「绝了」「真的会谢」这种表达分词工具很容易切碎或者切错。所以 utils.py 采用的是字级别切分把每个字符当作最小单位。好处是不会有词级别的未登录词问题网络新词出现也不影响编码「无语子」拆成「无」「语」「子」三个字模型照样能学出这个组合的倾向。清洗这一步主要处理 URL、用户名、多余空格和换行符。微博数据里「某某转发微博」这种形态很多不过滤的话模型会把用户名当成有效特征导致验证集上看着精度不错、换一批真实数据立刻掉点。常见做法是只保留中文、英文、数字和基础标点把无用噪声全部剥掉。import re from collections import Counter UNK, PAD UNK, PAD def clean_text(text: str) - str: text re.sub(rhttp\S, , text) # 去掉 URL text re.sub(r[\w\u4e00-\u9fa5], , text) # 去掉 用户名 text re.sub(r\s, , text).strip() # 压缩空白 return text def build_vocab(texts, min_count1): counter Counter() for text in texts: counter.update(list(clean_text(text))) # 按字统计 words [w for w, c in counter.items() if c min_count] vocab {w: i 2 for i, w in enumerate(words)} # 0 给 PAD1 给 UNK vocab[PAD] 0 vocab[UNK] 1 return vocab def encode_text(text, vocab, max_len64): ids [vocab.get(tok, vocab[UNK]) for tok in list(clean_text(text))] if len(ids) max_len: ids ids [vocab[PAD]] * (max_len - len(ids)) else: ids ids[:max_len] return ids逻辑说明build_vocab 统计的是字符频次而不是词频PAD 和 UNK 索引固定在 0 和 1这样做 embedding 初始化的同时保证未知字符和 padding 字符有稳定位置。encode_text 先查词典拿到索引序列再统一补齐或截断到 max_len。注意这里没有做词性标注、没有用预训练词向量原因很简单12 万条短文本用随机初始化的字向量足够学出情感特征引入外部词向量反而增加调参成本和依赖。参数说明min_count1 表示频次为 1 的字符也进词典这样不会丢生僻字代价是词典会略大但微博评论用字量本身有限通常不会超过 5000 个字。max_len64 对微博评论够用绝大多数样本不超过 40 个字超长截断丢弃的信息量很小如果你要跑长文本可以改到 128但显存和训练时间都会相应上涨。2.3 标签映射、数据切分与 DataLoader 组装标签映射很直接negative 对应 0positive 对应 1。这个映射在数据准备阶段就已经固定下来之后训练、验证、预测三处必须用同一个映射一旦不一致模型精度会直接崩盘到接近 50%。数据切分我习惯用 9:1保留约 12000 条做验证剩余约 108000 条训练。from sklearn.model_selection import train_test_split from torch.utils.data import DataLoader, Dataset class WeiboDataset(Dataset): def __init__(self, texts, labels, vocab, max_len): self.data [encode_text(t, vocab, max_len) for t in texts] self.labels labels def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx], self.labels[idx] texts, labels load_raw_data(WEIBO/weibo_senti_100k.csv) # 原始读取 labels [label2id[l] for l in labels] # 标签数值化 train_texts, dev_texts, train_labels, dev_labels train_test_split( texts, labels, test_size0.1, random_state2020, stratifylabels, # 分层抽样保证正负比例一致 ) train_loader DataLoader(WeiboDataset(train_texts, train_labels, vocab, 64), batch_size128, shuffleTrue) dev_loader DataLoader(WeiboDataset(dev_texts, dev_labels, vocab, 64), batch_size128, shuffleFalse)逻辑说明WeiboDataset 在初始化阶段就把原始文本一次性编码成索引序列训练过程中 DataLoader 只是机械地取数、组 batch不用反复做正则清洗跑起来更快。train_test_split 用 stratify 按类别比例分层抽样保证验证集的正负比例和原始数据一致得出的精度数字更有参考性。参数说明random_state2020 是固定种子不固定的话每次跑得到的切分都不一样精度波动 0.5 个百分点非常常见。batch_size128 是短文本任务的常见取值显存不够就改成 64。shuffle 只在训练集打开验证集保持顺序这样评估结果不依赖 batch 顺序也方便逐条排查错误样本。3. 三个模型逐一拆解从 FastText 到 BiLSTM_Att 的精度差异3.1 先看对比三个模型的配置差异三个模型精度差距很小FastText 97.65%、TextRCNN 97.87%、BiLSTM_Att 97.92%说明这批数据本身比较「好分」情感倾向明显的评论占了大多数。差距主要来自特征提取方式的不同而不是数据集难度的原因。我先把核心配置整理成表格后面逐一说清结构和参数怎么调。模型文件核心结构效果关键超参BiLSTM_Attmodels/BiLSTM_Att.py双向 LSTM Attention97.92%embed_size128, hidden_size128, dropout0.3TextRCNNmodels/TextRCNN.pyBiLSTM 输出 池化97.87%embed_size128, hidden_size128FastTextmodels/FastText.pybow bigram trigram97.65%n-gram 上限 3要特别提醒一点这份代码里的 TextRCNN 和论文原版 TextRCNNRNN 卷积组合并不完全一样它走的是 BiLSTM 编码后直接池化的路线所以项目备注里写的是「BiLSTM 池化」。复现的时候别拿论文结构去套代码一切以 models/TextRCNN.py 里的实际实现为准。3.2 BiLSTM_AttAttention 加在哪个位置、为什么有效BiLSTM_Att.py 是三个模型里精度最高的。结构上先过 Embedding 层把字索引转成向量再过双向 LSTM每个时间步输出前向和后向两个隐状态拼接后得到当前步的完整表示。Attention 层随后为每个时间步计算一个权重权重由 Query 向量和隐状态的点积经过 softmax 得到最后把所有隐状态按权重加权求和得到整句话的向量表示再过全连接层输出两个类别的 logits。Attention 解决的核心问题是「关键信息在句子里的位置不固定」。微博评论的情感词往往集中在最后几个字比如「这家店真的绝了」正向信号就在「绝了」上。单向 LSTM 读到末尾时可能已经稀释了开头内容双向 LSTM 的反向路径把末尾信息提前编码Attention 再决定模型重点看哪些时间步。加 Attention 意味着模型可以从「平均所有位置」变成「重点看几个位置」这个能力正好契合短文本里情感词集中出现的模式。dropout 放在两个位置embedding 输出之后和 LSTM 输出之后。12 万条数据规模不算大dropout 0.3 能明显抑制过拟合。如果你的数据量翻倍到 50 万条dropout 可以降到 0.2收敛会更快一点。hidden_size 这个参数建议先保持 128盲目加大会让训练时间成倍增加而精度提升通常不到 0.1 个点。3.3 TextRCNN池化路线在短文本上为什么稳TextRCNN.py 的实现里BiLSTM 编码完每个时间步后不接 Attention直接对所有时间步做池化。具体做法是把前向隐状态和后向隐状态沿时间维度分别取最大值再拼接得到最终向量。max pooling 选的是每个特征维度上的最强信号对情感分类来说相当于「把这句话最突出的情感特征挑出来」。这种做法在短文本上表现很稳原因是句子短、时间步数量少池化不会损失太多细节。对比 Attention它少了一套额外的权重矩阵要学训练速度更快调试起来也更简单。实际工程里我一般会把 TextRCNN 作为第一个跑通的基线模型确认数据链路没问题之后再换 BiLSTM_Att 做精度优化。它的超参配置和 BiLSTM_Att 高度一致embed_size、hidden_size 可以直接沿用切模型只需要改 run.py 的 --model 参数。3.4 FastTextn-gram 特征的成本与收益FastText.py 和 utils_fasttext.py 是配套的核心逻辑是把文本按字切分后同时生成 bigram 和 trigram 特征最终把所有特征做词袋编码再接一个线性分类器。这个模型没有 LSTM、没有 CNN效果却只比 BiLSTM_Att 低 0.27 个百分点训练速度是几十倍级别适合做大规模数据上的特征基线。n-gram 对短文本的作用在于补足单字信息的不足。「哈哈哈哈」这种连续重复字单个字符「哈」看不出倾向但 trigram 抓到「哈哈哈」这种组合模式后模型就能识别出这是强烈的正向情绪。bigram 类似能捕捉「绝了」「难吃」这种双字词倾向。要调的参数主要是 ngram_range 上限这份代码里固定到 3。如果你跑出来的特征矩阵太大导致内存吃紧常见做法是把 min_count 提到 2 或 3把低频组合砍掉特征数量能下降一大截。代价也在这里bigram 加 trigram 的组合数远大于单字数量特征矩阵的稀疏度很高。utils_fasttext.py 里如果用的是手写计数器建议对每个样本只保留 n-gram 的命中组合不要用稠密矩阵存储否则 12 万条数据跑下来内存会非常紧张。4. 训练与评估run.py 到 train_eval.py 完整复现流程4.1 run.py 一键启动参数解析与模型映射整个项目的入口是 run.py它负责解析命令行参数、决定用哪个模型、跑多少个 epoch、要不要 GPU然后调用 train_eval.py 里的训练和评估函数。run.py 的设计有个值得借鉴的点模型超参数和模型定义放同一个文件入口只做调度不做业务逻辑这样代码解耦得很干净换模型不用改主流程。import argparse import torch from train_eval import train, test parser argparse.ArgumentParser() parser.add_argument(--model, defaultBiLSTM_Att, choices[BiLSTM_Att, TextRCNN, FastText]) parser.add_argument(--epochs, typeint, default10) parser.add_argument(--batch_size, typeint, default128) parser.add_argument(--lr, typefloat, default1e-3) parser.add_argument(--device, defaultNone, helpcuda:0 或 cpu不传则自动检测) args parser.parse_args() config load_config(args.model) # 从 model 对应文件里取 Config config.epochs args.epochs config.batch_size args.batch_size config.lr args.lr config.device torch.device( args.device if args.device else (cuda if torch.cuda.is_available() else cpu) ) train(config) test(config)逻辑说明load_config 根据 --model 参数去 models 目录下对应文件里拿 Config 对象Config 里定义 embed_size、hidden_size、dropout、num_classes 这些超参。命令行参数覆盖 Config 的同名字段这样既保留了模型文件的默认值又允许临时调整。train 函数负责训练和保存最优权重test 函数负责加载权重并输出验证集精度。参数说明--device 不传时自动选 GPU没有 CUDA 的机器会退回 CPU。epochs10 对 12 万条数据来说适中FastText 结构简单通常 5 轮就收敛了跑 BiLSTM_Att 建议给满 10 轮配合早停。--lr 默认 1e-3Adam 优化器对这个值不敏感一般不需要动。4.2 train_eval.py 的训练循环、早停与权重保存train_eval.py 是核心执行文件。标准流程是每个 epoch 遍历训练数据前向计算 loss、反向传播、更新参数每个 epoch 结束后在验证集上评估准确率比历史最优高就把当前权重保存下来。这个「只在变好时保存」的策略保证 saved_dict 里留下的一定是验证集最优版本。import torch import torch.nn as nn from tensorboardX import SummaryWriter def train(config, model, train_loader, dev_loader): optimizer torch.optim.Adam(model.parameters(), lrconfig.lr) loss_func nn.CrossEntropyLoss() writer SummaryWriter(logdirruns/{}.format(config.model_name)) best_acc, no_improve 0., 0 for epoch in range(config.epochs): model.train() for i, (batch_text, batch_label) in enumerate(train_loader): batch_text batch_text.to(config.device) batch_label batch_label.to(config.device) optimizer.zero_grad() logits model(batch_text) loss loss_func(logits, batch_label) loss.backward() optimizer.step() if i % 200 0: writer.add_scalar(train_loss, loss.item(), epoch * len(train_loader) i) acc evaluate(model, dev_loader, config) writer.add_scalar(dev_acc, acc, epoch) if acc best_acc: best_acc acc no_improve 0 torch.save(model.state_dict(), saved_dict/{}.pth.format(config.model_name)) else: no_improve 1 if no_improve getattr(config, early_stop, 3): break return best_acc def evaluate(model, dev_loader, config): model.eval() correct, total 0, 0 with torch.no_grad(): for batch_text, batch_label in dev_loader: batch_text batch_text.to(config.device) batch_label batch_label.to(config.device) logits model(batch_text) pred torch.argmax(logits, dim1) correct (pred batch_label).sum().item() total batch_label.size(0) return correct / total逻辑说明evaluate 函数全程放在 torch.no_grad() 下验证阶段不需要梯度能省显存和计算时间。pred 用 argmax 取 logits 里最大的下标二分类场景下 logits 形状是 [batch_size, 2]下标 0 和 1 正好对应 negative 和 positive。早停条件用 getattr 从 Config 里读 early_stop默认 3验证精度连续三轮不涨就终止。参数说明保存的是 model.state_dict() 而不是整个 model这样加载时对模型结构更敏感反而方便排查结构不一致的问题。saved_dict 里的文件名和模型名一一对应切到 TextRCNN 时保存为 TextRCNN.pth加载时按同名取。需要注意的是 optimizer 没有做学习率衰减靠早停兜底不需要 scheduler。4.3 tensorboardX 监控与训练日志排查训练开始前先把 tensorboardX 的 SummaryWriter 路径跑通方式是项目根目录执行 tensorboard --logdirruns然后浏览器开 http://localhost:6006。这个工具能看到 train_loss 的下降曲线和 dev_acc 的上升曲线判断过拟合比看控制台日志直观得多。如果网页打不开多半是端口占用换一个端口即可。另一个常见问题是 Python 3.6 环境里 tensorboardX 版本和 numpy 版本打架报错集中在 np.float 这类旧属性上。稳妥的做法是严格按照 README 里的环境来python 3.6.12、pytorch 1.6.0、tensorboardX 用 1.8 左右的老版本依赖冲突最少。跑通之后再考虑升级环境。5. 复现过程中的避坑清单从分词到显存的五个常见问题5.1 分词报错或分词结果导致精度异常现象跑 run.py 时报 AttributeError或者不报错但模型精度明显偏低看训练日志发现序列长度很短大量文本 padding 后只剩一两个有效 token。原因有人把字级别模型的预处理硬套成词级别额外引入了 jieba.cut 之类的分词步骤导致「绝了」被当成一个 token序列长度大幅缩短。部分环境里 jieba 版本不兼容接口名变了也会报上述错误。这份代码从 utils.py 开始就是按字切分设计的清洗后直接 list(text) 就是完整流程不需要分词器。解决删掉所有与 jieba 相关的调用回到 utils.py 原生的字级别逻辑。如果确实想用词级别必须同步改 embedding 的 vocab 构建方式不能只改预处理不管模型输入维度否则 embedding 查表时索引对不上。5.2 显存溢出 OOM 导致训练中断现象训练到第二个 epoch 时报 CUDA out of memory程序直接退出之前算的 epoch 全部浪费。原因batch_size 和 max_len 的乘积超出显存容量。输入矩阵本身是 128×64 的整数张量看起来不大但 LSTM 的隐状态、Attention 中间权重会额外占用数倍显存4G 显存的卡很容易在这个配置下爆掉。解决先把 batch_size 从 128 降到 64再不行降到 32确认能跑完一个 epoch 再逐步加回。另一个有效手段是把 max_len 从 64 砍到 48微博评论多数不超过 30 个字48 能覆盖绝大多数样本精度损失可以忽略。显存实在不够就把 device 设为 CPU用 CPU 跑 BiLSTM_Att 大约三到四小时收敛做验证完全够用。5.3 tensorboardX 与新版 numpy 的版本冲突现象训练刚启动就报 AttributeError定位到 tensorboardX 内部错误信息是 module numpy has no attribute float。原因numpy 1.24 及以上版本删除了 np.float 别名而老版本 tensorboardX 内部还在使用。python 3.6.12 环境下 pip 安装的 numpy 版本通常不会触发但如果你在 Python 3.8 以上环境强行复现这份代码就会撞上兼容性问题。解决两个方向一是严格按 README 要求使用 python 3.6.12 pytorch 1.6.0二是把 numpy 降到 1.23.5 同时把 tensorboardX 降到 2.1 以下。我个人更推荐第二种不用重装 Python 环境改动最小。5.4 精度卡在 96% 上不去现象三个模型都完整跑完BestAcc 稳定在 96.2% 到 96.8%和 README 里的 97.92% 有明显差距。原因这类差距绝大多数不是模型问题而是随机种子不一致导致的数据切分不同。train_test_split 不固定 random_state 时每次切分都会变验证集里混入不同比例的高难度样本精度自然波动。另一部分原因是 embedding 层用随机初始化恰好某次落在局部较差点精度会再掉零点几个点这个只能说运气不好。解决全局固定随机种子把 torch.manual_seed、torch.cuda.manual_seed_all、numpy.random.seed 三处全部设置成同一个值并在代码最前面执行。还差一点的话把 max_len 从 64 加到 128让长评论保留更完整的信息BiLSTM_Att 通常能补回 0.1 个点。5.5 加载 saved_dict 权重时报 state_dict 不匹配现象加载权重做验证或预测时报 Missing key(s) in state_dict错误集中在 embedding.weight 或 bilstm 相关键名。原因训练和加载时的模型定义不一致。最常见的是训练时用了 DataParallelstate_dict 里的 key 多了 module. 前缀加载时用普通单卡模型就对不上。另一种情况是训练时保存的是整个模型加载时却用了 load_state_dict 模式。解决加载前先打印一下 state_dict 的 keys 和当前模型参数的 keys做一次人工对比。如果发现多出 module. 前缀做一个字符串替换剥掉后再加载。血的教训是保存时只存 state_dict、不存整个模型结构变更时定位问题会快很多。6. 进阶把训练好的模型接成文本分类预测接口6.1 单条微博的预测函数训练完的模型要真正投入使用得自己写一个 predict 流程。关键点是预测时的预处理函数必须和训练时完全一致否则 vocab 索引对不上输出结果乱套。下面是我根据这份资源的 utils.py 和 train_eval.py 整理的预测函数import torch import torch.nn.functional as F def predict_single(text, model, vocab, max_len64, devicecpu): model.eval() ids encode_text(text, vocab, max_len) # 和训练共用同一套预处理 ids_tensor torch.tensor([ids], dtypetorch.long).to(device) with torch.no_grad(): logits model(ids_tensor) prob F.softmax(logits, dim1).squeeze(0) # [负面概率, 正面概率] label_id int(torch.argmax(prob)) label positive if label_id 1 else negative return label, prob[label_id].item() model.load_state_dict(torch.load(saved_dict/BiLSTM_Att.pth, map_locationcpu)) label, confidence predict_single(哈哈哈这家店真的太绝了, model, vocab)逻辑说明ids_tensor 特意保留成二维张量 [1, max_len]模型内部有 batch 维度的计算只传一维向量会直接报错。softmax 在 dim1 上做得到正负两个类别的概率返回 argmax 对应的 label 和置信度。map_locationcpu 让 GPU 上训练的权重能在纯 CPU 环境加载适合没有显卡的部署机器。6.2 验证预测结果的三个习惯写完 predict 函数后我会强制自己走三遍检查第一遍拿训练集里标签为 positive 的原始文本反测确认输出 positive再拿一条 negative 反测防止 label2id 映射写反第二遍输入空字符串确认不报错且置信度接近 0.5说明 padding 路径是通的第三遍同一批文本分别在 CPU 和 GPU 下推理对比 argmax 结果完全一致排除设备引起的偶发偏差。从那以后每次拿到新的文本分类资源我都先把这三件事走完再去看 README 里写的精度数字。这份微博评论文本分类资源把数据和代码都放在 Traditional_Net_Classification-main 里数据链路完整按 README 配好环境就能直接复现希望帮到你。本文还有配套的精品资源点击获取
返回列表