简介:面向中文微博评论情感分析场景,这份资源提供了一套从数据清洗、分词、词表构建到模型训练、评估的完整PyTorch实现方案,适合自然语言处理学习者、算法工程师及文本分类项目开发者参考。项目基于weibo_senti_100k数据集,包含近12万条正负向微博评论,内置BiLSTM+Attention、TextRCNN、FastText三种经典模型,在测试集上准确率分别达到97.92%、97.87%和97.65%,可直接对比不同架构对短文本情感分类的效果。资源包共17个文件,以7个Python脚本为主,覆盖模型定义、训练评估与工具函数;另含5个文本说明文档、2个numpy数据文件、1个模型检查点、1个pkl文件及1个Markdown说明,整体压缩包约19.81MB,数据与代码组织规整,便于二次开发与复用。包内目录结构清晰,各模型独立成文件,超参与定义位于同一文件,方便调整参数复现实验或迁移到其他文本分类任务;代码注释清晰简洁,既能作为入门参考,也可加载预训练权重快速验证。目前已有31人学习该资源,对想要上手中文情感分析或搭建文本分类基线的读者具有实际价值。
1. 微博评论文本分类:完整数据与代码的落地路径
微博评论文本分类的难点从来不在模型结构,而在数据质量。一条“哈哈哈哈哈哈”在不同语境里可能是讥讽,也可能是真的开心;一条“这波操作666”在数码测评和娱乐八卦下的情感极性截然相反。标题强调“完整数据和代码”,说明提供方想把从原始评论到最终分类结果的整条链路一次性交付,而不是丢给你一个裸模型。对想复现的人而言,拿到这类资源包后,第一件事不是急着训练,而是先把文件结构、编码格式、字段含义、标签分布这四件事按顺序确认掉,后面每一步的调参才有依据。这篇文章按真实工作流的顺序展开,从数据装载讲到清洗、建模、评估,再到把模型包装成可调用的分类服务,每步都给出可直接复制的代码和参数解释,也把微博评论特有的坑单独拉出来说。
2. 数据读取与标签分布:微博评论文本分类的第一步
2.1 先认清数据文件长什么样
在接到以“完整数据和代码”为名的仓库时,常见的做法是先扫一遍目录结构,而不是直接双击运行 train.py。这类资源里 data 目录下一般是 CSV 或 JSON Lines 文件,code 目录下是预处理和训练脚本,有时带一份 requirements.txt 说明依赖版本。你需要用命令确认数据文件的编码、行数和字段类型,这一步能避免后面八成以上的诡异报错。
file data/weibo_comments.csv wc -l data/weibo_comments.csv head -c 500 data/weibo_comments.csvfile用来检测文件是 UTF-8 还是带 BOM 的 UTF-8-SIG,wc -l看总行数,head直接预览前几百字节里有没有乱码。很多从 Excel 二次导出或从数据库直接拉取的 CSV,第一行第一列会藏着\ufeff这个不可见字符,导致 pandas 里的列名变成\ufefflabel,后续按列名取数会直接报 KeyError。
确认完编码后,用 pandas 做正式加载。这里有个值得说明的细节:微博评论的 ID 和 UID 字段都是纯数字字符串,如果直接用默认参数读取,pandas 可能把它们解析成 int64,但超长 ID 会损失精度。最稳妥的方式是加载时就指定 dtype。
import pandas as pd df = pd.read_csv( "data/weibo_comments.csv", encoding="utf-8-sig", dtype={"id": str, "uid": str} ) print("数据集行数:", len(df)) print("字段列表:", df.columns.tolist()) print(df[["id", "comment", "label"]].head(3).to_string())用utf-8-sig而不是utf-8,就是为了让 pandas 自动吃掉上文提到的 BOM 头;即使文件本来没有 BOM,它也能正常解析。dtype参数把强数字型的 ID 保持成字符串,避免精度溢出。加载完成后第一件事是检查缺失值,评论内容为空或 label 为空的样本,在后续训练中要么被模型当成噪声,要么直接让 loss 计算报错。
2.2 标签分布直接决定评估方式
加载完数据,先别急着做清洗,第一步永远是看标签的分布情况。
print(df["label"].value_counts()) print(df["label"].value_counts(normalize=True))value_counts(normalize=True)会输出每个类别的占比。微博评论分类常见的标签体系有正负二分类、四分类(喜、怒、哀、惧)、事件类别多分类,以及垃圾评论过滤。normalize 之后的数字一眼就能看出数据是否均衡,而这一点决定了后续选什么评估指标。如果某个类别占比超过 75%,直接看准确率没有任何意义——模型无脑预测多数类就能拿高分。
| 最大类别占比 | 建议评估指标 | 分类器处理方式 |
|---|---|---|
| 50% 左右 | 准确率 + 宏平均 F1 | 默认参数即可 |
| 60% - 75% | 宏平均 F1 + 少数类召回率 | 开启 class_weight="balanced" |
| 75% 以上 | 每类 F1 + PR-AUC | 过采样或重采样后再训练 |
宏平均 F1 给每个类平等权重,少数类表现差会立刻拉低总分。所以如果看到某个小众类别的召回率小于 0.5,光调模型是不够的,要回到数据层面补充该类样本或合并弱语义类别。做训练测试切分时还要注意,社交媒体数据往往随时间漂移,事件热度会改变评论的情感分布,最合理的做法是按时间切分而不是随机切分,否则测试分数会偏高,上线效果打折扣。
3. 微博评论清洗与预处理:三个高频噪声源的处理
3.1 URL、@用户与话题标签的取舍逻辑
微博正文和评论里的噪声与普通新闻语料完全不同。URL 短链、@用户名、#话题词#、中括号表情符号,这四类是最高频的文本噪声。它们该不该清除,取决于你的任务目标。
做情感分类时,URL 和 @用户名几乎不携带可泛化的情绪信息,直接删除即可。但如果你的任务是识别垃圾营销评论,URL 是否存在本身就是一个强特征,不建议删除,而是把它转成一个has_url的标志位字段。话题标签里的文字往往是情感指向的对象,例如“#某明星演技#”中的明星名,模型需要这部分上下文,所以一般保留文字、去掉井号。中括号表情如[微笑]、[怒]则比较复杂,它本身是情绪的浓缩,但直接留在文本里会让模型不得不额外学习一套符号映射——更常见的做法是删除原文,同时单独记录表情数量作为一个数值特征喂给模型。
一个干净的项目结构应该是:原始文本永远存一份不动,清洗后的版本单独建列。这样你在后续尝试新特征时,不需要重新去读原始文件。
3.2 代码:一份兼顾可配置性的清洗函数
import re def clean_weibo_comment(text, remove_at=True, keep_url_flag=False): text = str(text) url_flag = 0 if keep_url_flag: url_flag = 1 if re.search(r"http[s]?://\S+", text) else 0 text = re.sub(r"http[s]?://\S+", " ", text) # 去 URL text = re.sub(r"#(.*?)#", r"\1", text) # 去井号留话题 text = re.sub(r"\[.*?\]", " ", text) # 去表情占位符 if remove_at: text = re.sub(r"@[\w\u4e00-\u9fa5._-]+", " ", text) # 去 @用户名 text = re.sub(r"\s+", " ", text).strip() return text.strip(), url_flag逐个正则解释。http[s]?://\S+匹配以 http 或 https 开头的非空白序列,微博短链形如http://t.cn/A1B2C3,会被整体替换成空格。#(.*?)#使用非贪婪匹配,确保只截取到第一个右井号,不会把相邻多个话题串在一起误删。\[.*?\]匹配中括号及其内部文字,注意这里的.*?同样是非贪婪,避免跨多个表情的误吞。@[\w\u4e00-\u9fa5._-]+兼容了用户名中的中文、数字、下划线、点号和连字符,比只匹配字母数字的版本更贴近微博实际。
这里有一个参数需要根据任务调整:keep_url_flag为 True 时,返回的是一个元组(clean_text, url_flag);为 False 时只返回字符串。设计成开关而不是写死,是为了同一个函数在垃圾评论识别和情感分析两个场景中都能复用。
3.3 短文本长度对模型输入的影响
微博评论平均长度通常只有 20 到 40 个字符,在做 tokenization 时会碰到一个有意思的分叉。传统 TF-IDF 方法中,短文本的词频几乎全是 1,IDF 区分度不足,因此要开启ngram_range=(1,2),用 bigram 组合来弥补单次信息量不够的问题。BERT 类模型相反,短序列长度设到 64 个 token 就已覆盖绝大多数评论,设到 128 只是增加 GPU 显存占用和推理时延,精度几乎不涨。所以在处理这份数据时,先统计一下df["cleaned"].str.len().quantile(0.95)的长度分位值,再决定模型的 max_length,这是让显存花在刀刃上的常用做法。
4. TF-IDF 基线模型:先让微博文本分类跑起来的方案
4.1 为什么先用 TF-IDF 而不是 BERT
拿到干净的文本后,建模选型上的常见误区是直接上预训练模型。但在微博评论这种短文本、高噪声、标签含主观分歧的任务里,BERT 的收益往往没有想象中高,反而会掩盖数据本身的问题。我的建议是,先花五分钟用 TF-IDF 加逻辑回归跑出一个基线,把数据端的毛病暴露干净,再决定是否升级模型。这个基线同时承担另一个职责:验证清洗流程是否有效。
基线由两部分组成。TF-IDF 负责把短文本转成向量,逻辑回归负责分类。前者有三个关键参数,直接影响特征质量。
| 参数 | 取值 | 作用说明 |
|---|---|---|
| max_features | 10000 | 只保留词频最高的 1 万个词,去掉长尾错别字和生僻词 |
| ngram_range | (1, 2) | 在单字词之外组合出双字词,捕捉“不/好吃”“太/贵”这类局部否定 |
| sublinear_tf | True | 用 1+log(tf) 替换原始词频,抑制高频词的量级差异 |
逻辑回归端值得调的只有C和class_weight。C控制正则强度,特征和数据量都上万时,C从 0.1 调到 10 对结果影响很小;真正起作用的是class_weight="balanced",它会按类别频率自动加权,直接应对第 2 章发现的标签不均衡问题。
4.2 代码:用 sklearn Pipeline 一次跑通
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( df["cleaned"], df["label"], test_size=0.2, random_state=42, stratify=df["label"] ) pipe = Pipeline([ ("tfidf", TfidfVectorizer( max_features=10000, ngram_range=(1, 2), sublinear_tf=True )), ("clf", LogisticRegression( C=1.0, class_weight="balanced", max_iter=1000 )) ]) pipe.fit(X_train, y_train) print("验证集准确率:", pipe.score(X_test, y_test))这里有两个细节值得关注。stratify=df["label"]保证切分后的训练集和测试集类别比例与原始数据一致,否则不均衡数据在随机切分后可能出现测试集里某个类一个样本都没有的情况。max_iter=1000则是针对高维特征下的收敛问题,sklearn 的逻辑回归默认迭代 100 次,词向量上万维时常常提前停止并警告,直接调大迭代次数比换求解器更省事。
跑完准确率后,真正有用的一步是看模型的判断依据。Pipeline 里的逻辑回归系数可以直接取出,并按绝对值排序输出 top 特征词,快速判断模型是在学语义还是被无关干扰词带偏。如果 top 特征是“哈哈哈”“666”这类词,说明模型学到的是网络用语风格,而非情感本身,这时应回到特征层面排查。
import numpy as np clf = pipe.named_steps["clf"] vectorizer = pipe.named_steps["tfidf"] feature_names = vectorizer.get_feature_names_out() topk_indices = np.argsort(np.abs(clf.coef_[0]))[-20:] for i in topk_indices: print(feature_names[i], round(clf.coef_[0][i], 4))sorted出来的词表如果和业务直觉差太远,说明清洗函数有漏洞,或者标注本身有大量噪声。这一步相当于给数据质量做了一次逆向检验,比任何验证集指标都直观。
5. BERT 微调:微博评论短文本分类的进阶参数与代码
5.1 预训练模型怎么选
如果 TF-IDF 基线的宏平均 F1 达不到业务要求,或者任务里大量存在需要结合上下文的语义判断时,再升级到 BERT 类模型。对于微博评论这种中文短文本,模型选择的权衡点是参数量、速度和效果三者的平衡。既不要一上来就加载一个庞大的中文预训练大模型,也不建议用英文 BERT 加中文词表。常见做法是选用中文 RoBERTa 系列,其中hfl/rbt3这类小参数量模型在短文本任务上表现接近完整 BERT,但推理速度快不少,降低了部署成本。另一条路是使用中文 woBERT 或 MacBERT,领域适配更充分,但这类模型权重文件更大,更适合离线分析场景。
模型选型有一个通用原则:先看测试集里有没有任务特有的词语模式。微博评论网络用语多、错别字频繁出现,通用预训练模型对这类变异词的识别能力有限,所以需要微调数据里的真实表达来适配,而不是换个更大的基础模型就能解决问题。
5.2 用 Transformers 跑通微调流程
微调部分需要transformers和datasets两个库,先做 tokenization,再做训练。这里重点说说参数的选择逻辑,而不是照着抄。
from transformers import AutoTokenizer, AutoModelForSequenceClassification from transformers import TrainingArguments, Trainer, EvalPrediction from datasets import Dataset import numpy as np from sklearn.metrics import f1_score, accuracy_score tokenizer = AutoTokenizer.from_pretrained("hfl/rbt3") model = AutoModelForSequenceClassification.from_pretrained( "hfl/rbt3", num_labels=df["label"].nunique() ) def tokenize_fn(examples): return tokenizer( examples["cleaned"], truncation=True, max_length=64, padding="max_length" ) ds = Dataset.from_pandas(df[["cleaned", "label"]]) ds = ds.map(tokenize_fn, batched=True) ds = ds.train_test_split(test_size=0.2, seed=42)padding="max_length"配合max_length=64,表示每条样本统一填充或截断到 64 个 token。微博评论文本原本长短不一,如果不做 padding,Trainer需要按 batch 内最大长度动态计算,处理起来更慢。而truncation=True保证超长样本被截断而不是报错。
接下来定义评估指标,这里特别要注意macro_f1必须自己写,Trainer默认只根据 loss 选模型。这个设置要单独实现。
def compute_metrics(eval_pred: EvalPrediction): logits, labels = eval_pred preds = np.argmax(logits, axis=-1) return { "accuracy": accuracy_score(labels, preds), "macro_f1": f1_score(labels, preds, average="macro") } training_args = TrainingArguments( output_dir="./checkpoints", num_train_epochs=3, per_device_train_batch_size=32, per_device_eval_batch_size=64, learning_rate=2e-5, weight_decay=0.01, eval_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="macro_f1", logging_dir="./logs" ) trainer = Trainer( model=model, args=training_args, train_dataset=ds["train"], eval_dataset=ds["test"], compute_metrics=compute_metrics ) trainer.train()learning_rate=2e-5是微调预训练模型的安全默认值。调大到 5e-5 可以加快收敛,但微博评论本身由大量口语和噪声构成,高学习率后期容易在验证集上出现震荡。num_train_epochs=3在 5 万级样本下是个平衡点,单个 epoch 对数据的学习不足,epoch 超过 5 后模型开始记忆个别样本的错别字和网络用语,验证集分数会不升反降。
per_device_train_batch_size=32在 8GB 显存上运行 rbt3 已经接近上限。如果显存不足,优先降到 16,而不是去换更小的模型。同理,save_strategy和eval_strategy保持一致,都设为 epoch,避免每 500 步存一个 checkpoint 导致磁盘占用失控。
训练完成后要单独保存 tokenizer,这一点经常被遗漏。部署时如果只加载模型权重而不加载 tokenizer,tokenization 方式默认值可能与训练时不一致,间接影响预测结果。
model.save_pretrained("./models/weibo_cls") tokenizer.save_pretrained("./models/weibo_cls")save_pretrained会把模型结构配置、权重和 label 映射一并写入目录。这样后续可以用pipeline("text-classification", model="./models/weibo_cls")一行代码加载,无需手动对齐 label 和 id 的对应关系。
6. 置信度过滤与多分类混淆矩阵:微博评论分类落地的最后技巧
模型训练完成后,验证集分数只是底线,真正影响上线体验的是置信度策略。微博评论噪声大,模型对很多文本的输出概率会集中在一个类别上,但它未必是正确分类。实践经验里,最有效的方法不是调阈值,而是利用 top-1 和 top-2 类别概率之间的差值,以及类别混淆模式来辅助判断。
先看多分类混淆矩阵,这一点比准确率更能暴露问题。用训练好的模型在测试集上生成预测结果后,直接打印出每一类的精确率、召回率、F1 和样本数。如果某个类别召回率明显低于其他类,多半是该类样本被系统性地误分到了相邻语义类别,这个信息能直接指导数据扩充的方向。
第二个实用技巧是温度缩放。模型输出的 softmax 概率并不天然代表置信度,微调后的模型经常给任意文本打出 0.98 的高分。解决方法是把 logits 除以一个温度系数后再做 softmax,让概率分布拉平一些,这样概率真正反映了分类的不确定性,并配合 top1-top2 概率差来标记分类可靠度。
import numpy as np def predict_with_confidence(model, tokenizer, text, temperature=1.2, gap_threshold=0.15): encoded = tokenizer(text, truncation=True, max_length=64, return_tensors="pt") outputs = model(**encoded) logits = outputs.logits.detach().numpy()[0] logits = logits / temperature exp_logits = np.exp(logits - np.max(logits)) probs = exp_logits / exp_logits.sum() top_indices = np.argsort(probs)[::-1] top1_score = probs[top_indices[0]] top2_score = probs[top_indices[1]] if top1_score - top2_score >= gap_threshold: confidence = "高置信度" else: confidence = "低置信度,建议人工复核" return int(top_indices[0]), round(float(top1_score), 4), confidence前面那段代码里的temperature=1.2需要按实际效果调整:温度大于 1 会让概率分布更平滑,top1 与 top2 的差值变小,高置信度样本比例自然下降;小于 1 则相反。gap_threshold=0.15表示 top1 和 top2 概率差在 0.15 以上才认为分类可靠。实际应用中,先用一批验证集样本跑出概率差值分布,再看哪个阈值能覆盖你要的人工复核比例,比拍脑袋定数值更合理。
这个函数可以直接接到 FastAPI 服务里,也可以并进离线批量预测脚本。当数据输入是单条评论时,它返回标签索引、置信度和是否需要人工复核的标记。分类模型部署时最大的隐患不是精度上差零点几个百分点,而是无条件相信模型的输出。加上置信度过滤通道后,误判的高危样本会被拦下来,这样的微博评论文本分类方案才真正具备上线条件。
本文还有配套的精品资源,点击获取