
简介这份资源是面向高校学生与NLP入门者的中文情感分析实战项目围绕WeiboSenti100k数据集与bert-base-chinese预训练模型展开可用于毕业设计、课程设计或软件工程实践帮助读者掌握从数据预处理到模型微调的完整流程。压缩包共5个文件包含2个Python脚本训练与推理、1个CSV数据集、1个TXT依赖清单和1个Markdown项目说明整体约9.73MB结构精简便于快速跑通。项目覆盖数据清洗、分词与特殊标记处理、分类层构建、交叉熵损失与Adam优化器训练、准确率与F1等指标评估以及新微博文本的情感极性预测能帮助读者理解预训练模型在下游任务中的迁移方式。目前已有196人学习适合希望积累深度学习与自然语言处理项目经验、锻炼工程实现能力的读者参考。1. 从 WeiboSenti100k 说起中文情感分析微调到底在做什么如果你手头有一批中文用户评论、弹幕或者客服对话想快速判断每条文本的情绪极性最省事的路径不是从零训练模型而是拿一个已经见过海量中文语料的预训练模型在标注好的情感数据上做一轮微调。WeiboSenti100k 就是这样一个标注数据集规模在十万条量级文本来自微博场景口语化重、表情符号多、反讽和缩写混杂比标准书面语更接近真实业务里的脏数据。bert-base-chinese 则是中文预训练模型的经典基线参数量适中单卡就能跑适合作为中文情感分析任务的第一版方案。这套组合解决的核心问题是用可接受的算力成本把通用中文语义理解能力迁移到情感极性分类上让模型学会区分正面、负面和中性表达。适合谁适合手里有标注数据、想快速验证情感分析可行性、又不想被大模型推理成本拖住的工程师和算法初学者。2. 数据与模型选型WeiboSenti100k 配 bert-base-chinese 的理由2.1 为什么是 WeiboSenti100k 而不是通用情感语料中文情感分析的数据集不少ChnSentiCorp、NLPCC 情感分析评测数据、在线评论语料各有侧重。WeiboSenti100k 的特点是场景集中、标注粒度明确、文本长度偏短。微博文本通常在几十个字以内情感表达直接但噪声也大话题标签、提及、表情符号、网络缩写都会干扰模型。选它的理由很实际——如果你的业务场景是短文本情感判断比如评论审核、舆情初筛、客服工单优先级排序微博语料的分布比长文本影评语料更接近你的线上数据。拿到数据后第一件事不是直接喂模型而是做分布检查。常见做法是用 pandas 统计标签分布和文本长度分布确认有没有严重类别不平衡。如果负面样本只有正面的三分之一训练时就需要考虑加权损失或者重采样否则模型会倾向于预测多数类。import pandas as pd # 假设数据已整理为 csv包含 text 和 label 两列 df pd.read_csv(weibosenti100k.csv) # 标签分布 print(df[label].value_counts(normalizeTrue)) # 文本长度分布按字符数 df[char_len] df[text].astype(str).str.len() print(df[char_len].describe(percentiles[0.5, 0.9, 0.95, 0.99])) # 检查空值和异常标签 print(df.isnull().sum()) print(df[label].unique())这段代码的逻辑是先看类别比例再看长度分位数。参数上重点关注 95 分位和 99 分位的字符数如果 99 分位超过 200 字就要考虑截断长度设到 128 还是 256。bert-base-chinese 的最大位置编码是 512但短文本任务里设 128 通常够用能省显存和计算时间。标签列要确认是 0/1/2 这样的整数编码还是正面/负面/中文字符串后续训练脚本里需要统一映射。2.2 bert-base-chinese 的微调方式与输出层改造bert-base-chinese 本身是掩码语言模型输出的是每个 token 的上下文向量不能直接做分类。微调时需要在 [CLS] 位置的输出向量后面接一个分类头。分类头通常是一个线性层输入维度 768输出维度等于类别数。如果做三分类情感分析就是 768 到 3 的映射。常见做法是用 HuggingFace 的 transformers 库加载模型和分词器然后自定义一个继承自 BertPreTrainedModel 的类或者直接用 BertForSequenceClassification。后者已经把分类头封装好了只需要指定 num_labels。对于大多数场景直接用它就够了不需要自己写分类头。from transformers import BertTokenizer, BertForSequenceClassification model_name bert-base-chinese tokenizer BertTokenizer.from_pretrained(model_name) model BertForSequenceClassification.from_pretrained( model_name, num_labels3, # 正面、负面、中性 hidden_dropout_prob0.1, attention_probs_dropout_prob0.1 ) # 查看分类头参数 print(model.classifier)这里的关键参数是 num_labels必须和你的标签体系一致。dropout 概率在微调时通常保持 0.1如果数据量小、过拟合明显可以调到 0.2 或 0.3。注意 bert-base-chinese 的分词器对中文是按字切分不是按词所以不需要额外做分词直接传原始文本即可。但微博文本里的表情符号和特殊字符需要提前清理否则会被当成未知字符。2.3 训练参数怎么设学习率、批次大小和轮数微调 BERT 类模型时学习率是最敏感的参数。常见做法是设 2e-5 到 5e-5 之间太大容易破坏预训练权重太小收敛慢。批次大小受显存限制单卡 8GB 显存下序列长度 128、批次大小 16 通常能跑起来。如果显存不够可以用梯度累积模拟更大的批次。轮数方面WeiboSenti100k 十万条数据三分类任务通常 3 到 5 轮就能收敛。判断依据是验证集上的 F1 分数不再提升而不是训练损失降到多低。训练损失持续下降但验证 F1 下降就是过拟合的信号需要早停或者加正则。from transformers import TrainingArguments, Trainer training_args TrainingArguments( output_dir./bert_senti_ckpt, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs4, weight_decay0.01, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, logging_steps100, fp16True # 有 GPU 时开启混合精度 )weight_decay 设 0.01 是 BERT 微调的常规值对分类头和小部分预训练权重做正则。fp16 在支持 Tensor Core 的 GPU 上能明显提速但要注意有些操作在 fp16 下会溢出如果训练出现 NaN先关掉 fp16 排查。evaluation_strategy 设成 epoch 表示每轮结束评估一次配合 load_best_model_at_end 自动保留验证集 F1 最高的检查点。3. 从原始数据到可训练格式清洗、划分与 Tokenize3.1 微博文本清洗的四个必做步骤微博文本的噪声比标准语料大得多直接 tokenize 会把大量无效字符带进模型。清洗不是可选项是必做项。常见做法分四步去 URL、去 提及、处理表情符号、统一全半角。URL 对情感判断没有贡献反而会引入噪声提及是用户互动标记和情感极性无关表情符号有时携带情感信号比如哭脸和笑脸但直接保留原始 emoji 会被分词器切成未知字符更好的做法是映射成文字描述比如 [微笑]、[哭泣]。import re def clean_weibo_text(text): # 去 URL text re.sub(rhttp[s]?://\S, , text) # 去 提及 text re.sub(r[\w\u4e00-\u9fa5], , text) # 去话题标签符号保留标签内容 text re.sub(r#(.?)#, r\1, text) # 全角转半角 text text.replace( , ) # 去多余空白 text re.sub(r\s, , text).strip() return text df[clean_text] df[text].astype(str).apply(clean_weibo_text)这段清洗逻辑里话题标签的处理需要根据任务决定。如果话题名本身和情感相关比如 #吐槽#保留内容有帮助如果话题名是中性的事件名保留反而引入噪声。我一般会先保留训练一版看效果如果验证集表现不好再考虑去掉。全角转半角主要针对空格和标点中文文本里全角空格很常见不处理会被当成有效字符。3.2 训练集、验证集、测试集的划分比例与分层采样十万条数据常见划分是 8:1:1即训练集八万、验证集一万、测试集一万。但情感分析任务里类别分布往往不均衡随机划分可能导致验证集里某个类别样本过少评估指标波动大。更稳妥的做法是分层采样保证每个子集的类别比例和原始数据一致。from sklearn.model_selection import train_test_split # 先分训练集和临时集 train_df, temp_df train_test_split( df, test_size0.2, stratifydf[label], random_state42 ) # 临时集再分验证集和测试集 val_df, test_df train_test_split( temp_df, test_size0.5, stratifytemp_df[label], random_state42 ) print(train_df[label].value_counts(normalizeTrue)) print(val_df[label].value_counts(normalizeTrue))stratify 参数是关键它保证划分后每个子集的标签比例和原始数据一致。random_state 固定后结果可复现方便对比不同模型的实验。如果数据量特别大比如超过百万条可以适当降低验证集和测试集比例但十万条量级下 1:1 是合理的。3.3 用 BertTokenizer 做批量编码与动态填充Tokenize 环节最容易翻车的地方是填充策略。如果全部按最大长度 512 填充短文本会浪费大量计算如果按批次内最大长度动态填充能显著提速。HuggingFace 的 DataCollatorWithPadding 就是做这件事的它会在每个批次里把序列填充到该批次的最长长度而不是全局最大长度。from transformers import DataCollatorWithPadding from datasets import Dataset def tokenize_function(examples): return tokenizer( examples[clean_text], truncationTrue, max_length128, paddingFalse # 交给 collator 动态填充 ) train_dataset Dataset.from_pandas(train_df[[clean_text, label]]) val_dataset Dataset.from_pandas(val_df[[clean_text, label]]) train_dataset train_dataset.map(tokenize_function, batchedTrue) val_dataset val_dataset.map(tokenize_function, batchedTrue) data_collator DataCollatorWithPadding(tokenizertokenizer)max_length 设 128 是基于前面长度分布检查的结果覆盖 95% 以上的样本。truncation 开启后超长文本会被截断截断方向默认是从右往左对情感分析来说关键情感词通常在开头或结尾如果发现截断后效果下降可以尝试只保留前 128 个字符或者后 128 个字符做对比。padding 设 False 是为了让 collator 动态处理避免全局填充带来的显存浪费。4. 训练、评估与推理把微调流程跑通4.1 定义评估指标准确率不够要看 F1情感分析任务里准确率在类别不均衡时会失真。比如负面样本只占 10%模型全预测正面也能拿到 90% 准确率但实际没有识别出任何负面情感。所以评估指标要用宏平均 F1 或者加权 F1。宏平均 F1 对每个类别一视同仁适合关注少数类召回的场景加权 F1 按类别样本数加权更接近整体表现。import numpy as np from sklearn.metrics import f1_score, accuracy_score def compute_metrics(eval_pred): logits, labels eval_pred preds np.argmax(logits, axis-1) return { accuracy: accuracy_score(labels, preds), f1_macro: f1_score(labels, preds, averagemacro), f1_weighted: f1_score(labels, preds, averageweighted) }这个函数传给 Trainer 后每轮评估会自动计算并记录。重点关注 f1_macro如果它和 accuracy 差距很大说明类别不均衡问题严重需要考虑加权损失或者调整采样策略。logits 是模型输出的原始分数argmax 后得到预测类别和 labels 对比计算指标。4.2 用 Trainer 跑一轮完整微调Trainer 把训练循环、梯度裁剪、学习率调度、检查点保存都封装好了适合快速跑通基线。需要传入模型、训练参数、训练集、验证集、数据整理器和评估函数。from transformers import Trainer trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset, data_collatordata_collator, compute_metricscompute_metrics ) trainer.train() # 保存最终模型和分词器 trainer.save_model(./bert_senti_final) tokenizer.save_pretrained(./bert_senti_final)训练过程中注意观察日志里的 loss 和评估指标。如果训练 loss 正常下降但验证 loss 上升就是过拟合需要减少轮数或者增大 dropout。如果 loss 一直不降先检查学习率是不是太大或者数据标签有没有映射错误。保存模型时把分词器一起存推理时才能保证编码方式一致。4.3 推理脚本单条文本和批量预测训练完的模型要能实际用起来推理脚本是最后一步。单条预测适合调试和演示批量预测适合离线处理大量文本。推理时要注意模型要切换到 eval 模式并且用 torch.no_grad() 关闭梯度计算否则显存占用会飙升。import torch from transformers import BertTokenizer, BertForSequenceClassification model_path ./bert_senti_final tokenizer BertTokenizer.from_pretrained(model_path) model BertForSequenceClassification.from_pretrained(model_path) model.eval() label_map {0: 负面, 1: 中性, 2: 正面} def predict(text): inputs tokenizer( text, return_tensorspt, truncationTrue, max_length128 ) with torch.no_grad(): outputs model(**inputs) pred torch.argmax(outputs.logits, dim-1).item() return label_map[pred] print(predict(这个产品真的太差了用了一次就坏)) print(predict(还行吧没什么特别的感觉))推理时的 max_length 必须和训练时一致否则位置编码会对不上。label_map 要和训练时的标签编码对应如果训练时用的是 0/1/2推理时就不能搞错顺序。批量预测时可以把多条文本组成列表传给 tokenizer它会自动做 padding然后一次前向传播得到所有结果比逐条预测快得多。5. 避坑与排查微调中文情感模型时最容易翻车的五个地方5.1 标签映射错位导致模型学反现象训练 loss 正常下降但推理时正面文本被预测成负面负面被预测成正面F1 却不算太低。原因标签编码和 label_map 不一致。比如训练时 0 是正面、1 是负面推理时 label_map 写成了 0 是负面、1 是正面。解决在数据预处理阶段就把标签映射固定下来写成一个常量字典训练和推理共用同一个映射。训练前打印几条样本的原始标签和编码后标签人工核对一遍。5.2 序列截断把关键情感词切掉现象短文本预测准确率还行但稍长一点的文本效果明显下降尤其是情感词出现在末尾的样本。原因max_length 设得太小或者截断方向不对。BERT 默认从右边截断如果情感词在末尾就被切掉了。解决先统计文本长度分布把 max_length 设到覆盖 95% 以上样本。如果显存不够可以尝试只保留前 N 个字符或后 N 个字符做对比实验。微博文本通常情感词靠前但也不绝对用验证集实测最可靠。5.3 学习率过大导致预训练权重被破坏现象训练初期 loss 剧烈震荡甚至出现 NaN验证集指标始终上不去。原因学习率设成了 1e-3 或更大对 BERT 微调来说太大了预训练学到的语言知识被快速覆盖。解决把学习率降到 2e-5 到 5e-5 之间配合 warmup 策略让模型在前几百步慢慢适应。如果已经出现 NaN重新加载预训练权重从头训练不要接着崩掉的检查点继续。5.4 类别不均衡导致少数类召回极低现象整体准确率看着不错但负面样本的召回率只有 30% 左右大量负面被预测成中性或正面。原因训练集里负面样本占比过低模型倾向于预测多数类来降低整体损失。解决在损失函数里加类别权重权重和类别频率成反比。或者对少数类做过采样复制样本直到各类别数量接近。也可以在评估时重点关注宏平均 F1而不是准确率。5.5 分词器和模型不匹配导致输入错乱现象推理结果完全随机和输入文本的情感倾向没有关联。原因训练时用的分词器和推理时加载的分词器不是同一个或者词表版本不一致。解决保存模型时把 tokenizer 一起保存到同一个目录推理时从同一目录加载。不要训练用一个分词器、推理用另一个。如果换了分词器必须重新训练模型因为词表变了embedding 层对不上。6. 进阶技巧用对抗验证和置信度过滤提升线上表现微调跑通只是第一步真正上线时还会遇到训练集和线上数据分布不一致的问题。我一般会做两件事对抗验证和置信度过滤。对抗验证的做法是把训练集和线上采样的一批无标注数据混在一起训练一个二分类器判断每条样本来自训练集还是线上。如果分类器很容易区分说明两者分布差异大需要补充线上风格的标注数据。具体操作是给训练集样本打标签 0线上样本打标签 1用同样的 BERT 结构做二分类看 AUC 和特征重要性。如果 AUC 超过 0.8分布差异就值得警惕了。# 对抗验证的数据构造示意 adv_train train_df[[clean_text]].copy() adv_train[domain] 0 adv_online online_df[[clean_text]].copy() adv_online[domain] 1 adv_df pd.concat([adv_train, adv_online], ignore_indexTrue) # 后续用同样的 tokenize 和 Trainer 流程做二分类置信度过滤是推理阶段的技巧。模型输出的 softmax 概率如果最高类概率低于某个阈值比如 0.6说明模型对这条样本没把握可以转人工审核或者丢弃。这样能减少低质量预测对下游业务的影响。阈值设多少要看业务容忍度我一般会在验证集上画一条置信度-准确率曲线找到准确率开始明显下降的拐点作为阈值。还有一个实用技巧是分层采样做小样本实验。十万条数据全量微调一轮要不少时间调参阶段可以先抽一万条做快速实验确认学习率和批次大小合理后再上全量。这样一轮实验从几小时缩短到几十分钟迭代效率高很多。但要注意小样本实验的最优参数不一定完全适用于全量最终还是要全量验证一遍。最后说个血泪经验不要等到训练完才检查数据。我踩过一次坑训练了四个小时评估时发现验证集里混进了训练集的样本F1 虚高到 0.95实际线上只有 0.7。后来养成习惯划分完数据集先做一次去重检查确认训练集和验证集没有重叠文本。这个检查花不了几分钟但能省掉几小时的后悔药。希望帮到你。本文还有配套的精品资源点击获取