
简介NLPCC2013评测数据集聚焦微博情绪分析任务是面向自然语言处理研究者、数据挖掘工程师及舆情分析人员的标准评测数据。资源为zip压缩包共1个文件主要类型为XML标注文档约7MB内置带情感极性正面/负面/中性及情感目标标注的微博文本样本可直接用于分类模型训练、情感目标抽取及算法效果验证。已有294人学习下载。借助该数据集可系统体验情感极性分类与情感目标检测两大子任务熟悉微博非正式语言、网络用语及表情符号等真实场景难点支撑基于传统机器学习或BERT等预训练模型的对比实验与论文复现。对于希望深入社交媒体情感计算、构造中文情绪分析基准的读者是一份简洁但信息密度较高的基础语料能够支持情感强度与多义现象等进阶分析。1. nlpcc2013评测数据集微博情绪分析绕不开的落点做中文微博情绪分析跑 OpenSource 模型之前得先有一份大家都认可的 benchmark。nlpcc2013评测数据集就是这批语料里最常被翻出来的一次评测沉淀从微博文本里判断整体情绪正面、负面、中性之外还有细粒度情绪类别数据规模中等标注质量相对可控。拿它做模型验证比自己在网上随手抓一批微博再人工打标要靠谱得多。它适合刚入门情感分类的开发者也适合已经在做舆情系统、想换一个标准测试集做对比的工程师。本文会把标签体系、预处理、微调、评估和常见坑全部过一遍尽量让这份旧数据在你手上能直接用起来。2. 标签体系与数据格式先把这批微博语料读对2.1 情绪标签六类细粒度和中性这份数据最常被复用的版本是六类细粒度情绪加一个中性共七类。常见标签包括高兴、悲伤、愤怒、厌恶、恐惧、惊讶以及中性。不过要提醒的是不同渠道流传的版本在标签命名上有差异有的把happiness写成happy有的把neutral写成none加载时先做一次标签分布统计非常有必要。我通常的做法是先写一段脚本把全部标签拉出来数一遍再决定建模目标。从实用角度说七分类问题比普通正负二分类要麻烦很多。类与类之间存在天然语义重叠比如“惊讶”和“高兴”经常混在一起中性样本占比通常不低模型容易偷懒全吐中性。这也是我不建议一上来就做七分类的原因之一先跑一个三分类正面、负面、中性把流程打通再切换到七分类去验证细粒度效果可以少走弯路。2.2 文件结构一行一条JSON是最常见的落盘方式我拿到的这份nlpcc2013评测数据集公开后有人重新整理过比较通用的格式是训练集一个文件、测试集一个文件每行一条JSON。字段大致如下{ id: 4632, content: 今天天气真好心情特别棒, label: happiness }这里我见过两类变体。一种把文本字段叫content另一种叫text或weibo_text标签字段有的叫label有的叫sentiment或emotion。写加载脚本时不要写死字段名先把键打印出来确认一下再继续。JSON文件规模不算大用jsonlines逐行读取即可不需要上内存数据库。import json def load_jsonlines(path): with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue yield json.loads(line) samples list(load_jsonlines(nlpcc2013_train.jsonl)) print(样本数:, len(samples)) print(字段:, list(samples[0].keys())) print(标签分布:, {label: sum(1 for x in samples if x[label] label) for label in set(x[label] for x in samples)})这段代码做了三件事按行读取JSON、统计样本总量、统计每个类别的样本数。注意集合推导那里重复遍历了列表数据量不大所以无所谓但如果你拿到的是百万级语料建议改成Counter一次遍历完成。标签分布这一步不能省后续要不要做类别均衡、用什么损失函数全看这里的结果。2.3 数据预览脚本先统计再训练字段名和标签搞清楚之后我习惯再花五分钟打印20条样本看一眼内容确认几个问题文本里有没有夹杂HTML标签URL是不是已经变成了http://t.cn/xxxx这种短链有没有大量用户名残留这些直接决定预处理规则怎么写。from collections import Counter counter Counter(sample[label] for sample in samples) for label, count in counter.most_common(): print(f{label}: {count} ({count / len(samples):.2%})) for sample in samples[:10]: print(sample[id], sample[label], sample[content][:80])打印出来的内容会暴露很多问题。比如标签不一致的样本、空文本样本、以及文本中夹杂的噪音字符。这一步花不了两分钟但能帮你避免后面在脏数据上训练了一整晚才发现预处理漏了步骤的尴尬。3. 预处理与微调把BERT压到微博情绪上3.1 微博文本预处理四步清洗、切分、映射、缓存微博文本的特点决定了预处理和普通新闻语料不一样。官方评测数据里文本来源杂用户昵称、转发标记、URL、表情符号混在一起直接丢给BERT不是不行但效果会打折。我一般走四步先去URL和HTML实体再去用户名然后处理表情符号最后做标签到ID的映射。import re def clean_weibo_text(text: str) - str: # 连续步骤把url、html实体、用户、话题标记先清掉 text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(ramp;|lt;|gt;, , text) text re.sub(r[\u4e00-\u9fa5a-zA-Z0-9_], , text) text re.sub(r#(.?)#, r\1, text) text re.sub(r\s, , text).strip() return text label_map { neutral: 0, happiness: 1, sadness: 2, anger: 3, disgust: 4, fear: 5, surprise: 6, }这个清洗函数我特意把正则分开写便于逐行理解和调整。amp;这类HTML实体在老数据里出现的频率不低直接替换成空字符串虽然粗暴但在分类任务里影响有限。#话题#我保留话题内容但去掉#号因为话题词本身承载情绪信号删掉整段会损失信息。有一个点需要注意七类标签的映射字典要保持全局一致。训练和评估用同一个字典不要一边在训练脚本里写一份、另一边在评估脚本里又写一份否则标签错位会让你查很久都查不到原因。3.2 用BERT做七分类微调最小可用代码清洗之后就可以进入建模环节。用BERT微调已经是这个场景的默认打法我以transformers库为例给一个能跑的完整流程。常见的预训练模型选择是bert-base-chinese如果机器显存够换用更大模型也能提升一点但收益并不总值得那个时间成本。import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained( model_name, num_labelslen(label_map) ) def encode_samples(samples, tokenizer, max_len128): texts [clean_weibo_text(s[content]) for s in samples] labels [label_map[s[label]] for s in samples] encodings tokenizer( texts, paddingTrue, truncationTrue, max_lengthmax_len, return_tensorspt ) return encodings, torch.tensor(labels) train_encodings, train_labels encode_samples(train_samples, tokenizer)这里有几个参数我说一下max_length我设128微博文本普遍短128个token足够覆盖绝大多数样本设得越大训练速度越慢收益却不大。paddingTrue会在batch内补齐到最长文本truncationTrue会把过长文本截断。如果你的显卡内存比较紧张可以改成paddingmax_length统一补齐让训练更稳定。训练循环本身不用自己写直接走Trainerfrom transformers import Trainer, TrainingArguments training_args TrainingArguments( output_dir./checkpoints, learning_rate2e-5, per_device_train_batch_size16, num_train_epochs5, weight_decay0.01, warmup_ratio0.1, logging_steps50, eval_strategyepoch, save_strategyepoch, ) trainer Trainer( modelmodel, argstraining_args, train_dataset(train_encodings, train_labels), eval_dataset(dev_encodings, dev_labels), ) trainer.train()这段代码里train_dataset传的是(encodings, labels)元组Trainer会自动处理成torch数据集。eval_strategy建议设置成epoch每个epoch跑完在验证集上看一次效果便于判断什么时候开始过拟合。3.3 训练参数边界与收敛判断参数这块我给出自己常用的范围你可以在这个基础上调。学习率2e-5到5e-5之间算常规区间超过5e-5容易出现梯度震荡batch size看显卡显存入门级显卡用16大显存可以用32或64epoch数一般4到6轮就够跑太多轮模型会把训练集噪声也背下来。判断收敛我主要看两个东西验证集loss和宏F1。只盯着loss看容易误判因为模型在训练后期loss还可能缓慢下降但验证集F1已经开始波动甚至回落。训练中保存每个epoch的checkpoint结束后在checkpoint上遍历一遍选出验证集F1最高的那一个来做测试这个习惯帮我少踩了很多次“训完直接用最后一轮”的坑。4. 避坑指南四条踩过的坑与对应解法4.1 标签主观噪声有些样本的情绪边界连人都分不清现象训练完模型之后打开预测错误的样本看发现不少“错误”样本其实模型判断得更合理。比如一条微博写着“加班到十点终于回家了”原始标签标的是“sadness”但模型的输出是“neutral”。原因情绪标注本身就是主观行为。标注者看到“加班”联想到负面情绪读者看到“终于回家”觉得平淡。nlpcc2013评测数据里这类模糊样本量不小七分类边界重叠决定了不可能全部标对。解决我后来不再把训练集标签当成绝对正确而是把它当作弱监督信号。具体做法是先用全量数据训练一版模型在训练集上找出模型预测极不自信softmax概率接近均匀分布的样本人工检查一小批如果错误率太高就删掉这部分再做第二轮训练。这个两阶段训练法在这个数据集上提升了约1到2个点的宏F1。4.2 URL、HTML标签与用户清理不彻底现象模型训练时loss下降很慢最终验证集F1明显偏低。查看训练集文本后发现大量http://t.cn/xxxxx和lt;这样的残渣。原因老数据集里原始微博未经彻底清洗短链和HTML实体都留在文本里。BERT词表里没有URL片段这些整块token分词阶段会被切成一堆无意义的子词不仅浪费序列长度还引入了大量噪声。解决预处理阶段必须把这些内容清掉。我在clean_weibo_text函数里对URL、HTML实体、用户名做了三级处理并且强调在训练前把清洗后的内容重新存一份train_clean.jsonl避免每次训练前重复清洗。一个容易被忽略的细节用户名里可能包含英文字母或数字正则里要同时覆盖中英文范围。4.3 类别不平衡带来的假收敛现象训练loss平稳下降但测试集宏F1只有0.4左右准确率却有60%以上。细看各类别的精确率和召回率中性类特别高愤怒、恐惧这类低频类别几乎不预测。原因这个数据集的标签分布并不均匀中性样本占比往往最大低频类别样本很少。模型发现全预测中性可以拿到很高的准确率于是走捷径把minimize loss和maximize accuracy画了等号。解决从两个层面调整。一是在损失函数上做文章给低频类别更高的权重二是评估指标不要只看准确率重点看宏F1。权重的设置我一般用类别样本数的倒数做归一化再乘以类别总数避免权重尺度差距过大from sklearn.utils.class_weight import compute_class_weight import numpy as np labels np.array([label_map[s[label]] for s in train_samples]) class_weights compute_class_weight( class_weightbalanced, classesnp.unique(labels), ylabels, ) weight_dict {i: w for i, w in enumerate(class_weights)}这段代码用了sklearn的compute_class_weightbalanced模式自动按样本频率计算权重。用weight_dict传入损失函数后模型对低频类的偏置会得到一定纠正。不过也要注意权重拉得太大会导致高频类别召回率下降需要做一次网格搜索。4.4 训练集与测试集划分不当导致结果虚高现象某次实验在测试集上宏F1达到0.72比之前高了将近10个点很兴奋。但换了一批数据重新测试掉回了0.60附近。原因为了快速迭代我图省事按随机抽样做了切分。但随机划分引入了数据泄漏——同一用户的多条微博被同时分到训练集和测试集模型实际上“见过”了测试集用户的表达习惯分数虚高。解决重新做了按用户维度去重再切分的版本。具体流程是先把训练集样本按用户ID分组同一用户全部划入同一侧再做数据集划分。nlpcc2013官方的训练测试划分方式在不同渠道流传不一致复现时最好问清楚对方用的是哪种划分方式否则你报告的数字和别人的数字没有可比性。5. 评估与对比别让准确率骗了你5.1 评估指标宏F1才是硬指标微博情绪分析里准确率是一个极具欺骗性的指标。类别不平衡的情况下模型只要学会预测高频类别就能拿到不错的准确率但实际业务里低频类别往往才是最需要关注的。我评估时固定使用宏F1Macro-F1作为主指标也就是每一个类别的F1分别计算再取平均同时用分类报告看高频和低频各类的表现。from sklearn.metrics import classification_report, confusion_matrix predictions trainer.predict(test_dataset) pred_labels predictions.predictions.argmax(axis-1) print(classification_report( test_labels, pred_labels, labelslist(label_map.values()), target_nameslist(label_map.keys()) ))这段代码输出每一类的精确率、召回率、F1以及总体宏F1。输出结果我一般会存一份report.txt存档。分析报告时重点看两类样本量大但F1低的类别说明数据内部差异大样本量小且F1尤其低的类别说明模型几乎没有学到有效特征。5.2 对照基线不是所有模型都能打过传统方法用上BERT之后会形成一种错觉觉得它必然全面碾压传统方法实际并非如此。我在这份数据上做过几组对照节奏是先跑TF-IDF加朴素贝叶斯再跑BiLSTM最后跑BERT同一套预处理、同一套划分模型预处理宏F1参考训练时间TF-IDF 朴素贝叶斯结巴分词 清洗0.50左右分钟级Word2Vec BiLSTM结巴分词 清洗0.55左右小时级BERT-base清洗 字粒度tokenize0.63左右不等视GPU而定传统模型和BERT的差距确实存在但差距没有想象中大尤其当你只做三分类正面、负面、中性时TF-IDF加朴素贝叶斯跑出来的宏F1能达到0.65左右和基础BERT差距缩小到5个点以内。这个结论说明一件事在这份数据上清洗和预处理步骤对分数的贡献可能比换一个大模型更直接。5.3 混淆矩阵与错误分析怎么做分类报告只能告诉你哪类表现差混淆矩阵才能告诉你差在哪。我每次实验必画一张混淆矩阵去看具体的错误模式。import matplotlib.pyplot as plt import seaborn as sns cm confusion_matrix(test_labels, pred_labels) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelslist(label_map.keys()), yticklabelslist(label_map.keys())) plt.title(Confusion Matrix) plt.ylabel(True Label) plt.xlabel(Predicted Label) plt.show()从这张图上能读出几个常见模式真实标签为“惊讶”的样本大量被预测成“高兴”真实标签为“厌恶”的样本被预测成“愤怒”这两种混淆背后有语言学原因——惊讶和高兴在面部表情和用语上高度重叠厌恶和愤怒在中文表达上也经常连用。看清混淆方向之后再决定合并类别而不是盲目调参。比如业务对“惊讶”和“高兴”的区分没有硬性要求直接合并成“正面情绪”反而更稳。6. 进阶用法用噪声过滤和数据增强榨干这份语料基础微调和评估跑通后再往上提点需要从数据和训练策略两个方向扣细节。数据方向我用两类手段一种是对训练集做噪声过滤让模型在更干净的数据上学习另一种是做数据增强把低频类别的样本量撑大。噪声过滤的做法在第4.1节提过核心是用模型自身预测的软概率去识别可疑样本。不要只过滤低置信度样本我一般会加一个约束预测类别与原始标签不同且最大概率超过0.85的样本也值得人工检查因为高置信度但偏离原始标签的样本很可能是标注错误。某次检查中我发现一条微博内容写的是“新手机掉水里了无语”原始标签是“neutral”但模型高置信度预测为“sadness”。经过人工确认确实是原标注质量不高这条样本最终从训练集里剔除。数据增强方面简单的方法是词级替换。中文NLP里不像英文那样有成熟可靠的单词替换工具我常用的替代方案是随机删除、随机打乱顺序、以及用同义词词典替换情绪词。要注意的是情绪词是分类的关键特征无差别替换会破坏标签语义。比如把“高兴”替换成“难过”标签就乱了。所以我限定只对非情绪词做增强。再做一步更干净的增强把训练集中的中性样本和少数类样本结合起来用BERT回译生成新的微博变体经过人工抽检确认情绪倾向不变后加入训练集。在nlpcc2013这批数据上这个操作能把“恐惧”和“惊讶”两个低频类别的F1各提升3个点左右代价是要多花几十分钟做回译和数据检查。最后说一个我养成的习惯每次实验跑完固定把标签分布、模型预测分布、混淆矩阵、分类报告四个文件连同模型checkpoint一起存档。后来某次线上报告需要一个精确的数字我翻出了三个月前的存档五分钟之内定位到当时的实验配置和对应结果不用重跑一遍。从那以后每换一个数据集我都强制自己走一遍这个流程先统计数据再清洗文本再跑基线模型最后才上大模型。希望这份拆解能帮你在微博情绪分析这条路上少踩几个坑快速复现出一版可靠的结果。本文还有配套的精品资源点击获取