bert-base-chinese微调中文情感分析:从数据清洗到推理部署

发布时间:2026/9/14 1:50:10
bert-base-chinese微调中文情感分析:从数据清洗到推理部署
简介针对中文情感分析任务这套基于WeiboSenti100k数据集与bert-base-chinese预训练模型的微调资源包适合NLP初学者、毕业设计及课程设计学生使用。压缩包9.73MB共5个文件包括训练与推理两个Python脚本、微博情感标注csv、requirements依赖文件以及Markdown项目说明完整呈现数据加载、模型构建、训练调优、验证评估和预测应用流程。已有195人学习下载可作为快速上手BERT中文情感分类的参考示例。通过阅读说明文档并运行源码可掌握transformers库加载中文BERT的方法理解情感分类任务的数据预处理与微调技巧并利用训练好的模型对微博文本进行情感极性预测。同时项目结构清晰代码注释和依赖列表便于复现实验为后续延伸学习文本分类、舆情分析等方向打下基础。1. 中文情感分析项目拿 WeiboSenti100k 微调 bert-base-chinese值在哪一个经常被忽略的事实是用 bert-base-chinese 在 WeiboSenti100k 上做中文情感分析微调单卡十几分钟就能跑完一个 epoch却能完整覆盖「数据清洗—分词—微调—评估—部署」全链路。这个标题拆开是三件事WeiboSenti100k 是带正负标签的微博短文本数据集规模接近 12 万条bert-base-chinese 是面向中文的预训练模型字粒度输入参数量约 1 亿微调则是把通用的语言理解能力对齐到情感分类这个具体任务上。它适合两类人一是想搞懂 BERT 微调到底改了哪些参数、loss 和 F1 怎么联动的新手二是要做舆情分析、评论打分但不想从 word2vec 开始造轮子的工程师。数据是公开的模型是公开的源码结构也相对固定真正的差异往往体现在数据清洗和验证划分上。2. 先把 WeiboSenti100k 和 bert-base-chinese 的边界摸清2.1 WeiboSenti100k 的数据长什么样label 到底代表什么WeiboSenti100k 在 Hugging Face 上可以直接通过数据集名加载常见字段是text和labellabel 取值为 0 或 1分别对应负面和正面情感。注意这里没有中性标签所以模型输出天然是二分类不要在中途引入「中性」这类第三类期望会破坏训练目标的闭合性。这个数据集的几个明显特征文本长度短大多数在 140 字以内口语化严重掺杂大量 用户名、URL、表情符号和网络流行语标签是粗粒度的人工标注存在标注歧义比如「我也是醉了」这种反讽文本在不同标注者眼里可能完全是相反的情感。还有一点容易被忽略数据里存在重复或近似重复的文本直接随机划分训练集和验证集可能出现同一句话同时落在两边的泄漏情况。2.1.1 数据样例速览textlabel期待已久的电影终于上映了太震撼1快递一周都没到客服也不理人0呵呵说好的补货呢0第一眼看起来标签没问题但注意「呵呵」这种词在微博语境里基本是负面而在通用语料里可能是中性的。这提示我们微调的本质是让 bert-base-chinese 重新校准对这个数据分布的情感判断而不是从零学习中文。2.2 bert-base-chinese 的输入约束bert-base-chinese 是字粒度模型输入不是分词后的词序列而是每个汉字作为一个 token配合[CLS]和[SEP]。最大输入长度是 512 个 token但微博短文本平均长度远低于这个值所以这里的约束更多是「别超长」而不是「越长越好」。用 Transformers 加载时AutoModelForSequenceClassification会自动替换掉预训练模型顶部的 MLM 头部换成随机初始化的分类层。分类层的宽度由num_labels决定。这里有个新手常踩的坑只加载AutoModel然后自己接分类头等于放弃了from_pretrained里已经处理好的权重映射完全没必要。直接指定num_labels2即可。2.3 用 datasets 一次性加载并做划分我一般会在项目一开始就把数据加载、清洗、划分写成一个独立脚本而不是在训练脚本里反复执行。这样每次实验的数据版本是确定的复现时不会因为某次清洗逻辑改动导致结果对不上。from datasets import load_dataset import re # 加载公开数据集首次执行会下载到本地缓存 ds load_dataset(weibo_senti_100k)load_dataset返回的DatasetDict通常只有一个train分片。也就是说验证集需要自己划分。常见做法是用train_test_split按 9:1 切分并固定随机种子。# 按 9:1 划分训练集和验证集固定种子保证可复现 split ds[train].train_test_split(test_size0.1, seed42) train_ds split[train] eval_ds split[test]这里只是随机划分还没有处理重复文本的问题。如果希望验证集指标更真实建议先按文本内容做一次全量去重再执行划分seen set() def dedup(example): key example[text].strip() if key in seen: return False seen.add(key) return True ds_dedup ds[train].filter(dedup)去重之后再做train_test_split能明显降低验证集 F1 虚高的情况。微博里大量转发、复读会让同一句话出现在多个样本里不过滤的话训练集和验证集的分布会高度重合评估结果会偏乐观。2.4 预处理只做必要清洗不要过度加工微博文本噪音多但 bert-base-chinese 对噪音的容忍度比想象中高因为它按字切分错别字和网络词都能被拆成单字进入模型。过度清洗反而可能引入不一致训练时把 URL 删了预测时 URL 还在分布就对不上了。我一般只做三类处理去掉 URL、去掉 用户名、统一常见全角标点。表情符号和 emoji 保留因为这些在微博情感表达里是有信息量的。def clean_weibo(text: str) - str: # 去掉 URL保留为空格避免被切成大量 UNK text re.sub(rhttps?://\S|www\.\S, , text) # 去掉 用户名这类提及对象对情感判断没有帮助 text re.sub(r\S, , text) # 全角标点转半角减少 token 碎片 text text.translate(str.maketrans(。【】, ,.!?()[])) # 压缩连续空白 text re.sub(r\s, , text).strip() return text # 在 map 阶段把清洗逻辑应用到所有样本 train_ds train_ds.map(lambda x: {text: clean_weibo(x[text])}) eval_ds eval_ds.map(lambda x: {text: clean_weibo(x[text])})注意清洗函数必须同时在训练集和验证集上执行不能在训练集上做了而在预测阶段漏掉。很多线上效果和离线验证对不上的原因就在这种不对称处理上。3. 用 Transformers 跑通 bert-base-chinese 微调的最小闭环3.1 环境与依赖版本怎么锁这个项目依赖不多核心是transformers、datasets、torch评估用scikit-learn。如果你的显存比较紧张accelerate也建议装上Trainer 底层依赖它。pip install transformers4.40 datasets2.19 torch2.1 scikit-learn accelerate有两个版本相关的点需要提前说明。第一Trainer的评估时机参数在较新版本里叫eval_strategy老版本叫evaluation_strategy按你实际安装的 transformers 版本选择否则会报 unexpected keyword。第二train_test_split的stratify_by_column参数在 datasets 新版本才有老版本需要手动分层但 WeiboSenti100k 正负样本基本均衡普通随机划分就够用。3.2 加载模型、tokenizer 与标签映射标签映射不只是一个显示用的字典它会写进模型配置文件。保存模型后用pipeline加载时输出的 label 名称就是从这里来的。from transformers import AutoTokenizer, AutoModelForSequenceClassification model_id bert-base-chinese # id2label 和 label2id 必须互相对应 id2label {0: 负面, 1: 正面} label2id {负面: 0, 正面: 1} tokenizer AutoTokenizer.from_pretrained(model_id) model AutoModelForSequenceClassification.from_pretrained( model_id, num_labels2, id2labelid2label, label2idlabel2id, )num_labels2会触发分类头替换id2label让模型输出可读标签。很多教程不设置这两个字典训练没问题但后续推理时 label 只会显示 0 和 1排查线上问题时非常不方便。3.3 训练参数照着这张表改就行中国微博情感分类是一个典型的短文本二分类任务参数选择有比较成熟的区间。训练轮数不建议超过 3 轮因为 bert-base-chinese 是通用中文模型在 12 万条数据上微调第 2、3 轮就已经能收敛再多就进入过拟合区间。参数推荐值说明learning_rate2e-5BERT 微调安全区间是 1e-5 到 5e-5超过 5e-5 容易震荡per_device_train_batch_size32显存不够时降到 16不要直接动学习率per_device_eval_batch_size64验证集不需要反向传播可以大一些num_train_epochs312 万条数据 3 轮足够再训练开始过拟合warmup_ratio0.1前 10% 的 step 学习率线性上升稳定训练初期weight_decay0.01AdamW 的权重衰减对分类头和小模型都有效max_length128微博短文 128 覆盖绝大多数样本512 会浪费显存fp16True半精度训练V100 及以上显著加速学习率是整个训练里最敏感的参数其他参数可以在一定范围内浮动。如果你只改一个参数优先检查学习率。3.4 用 Trainer 组织训练循环与保存Trainer 封装了训练循环、梯度累积、日志和保存逻辑不需要手写for epoch in range(...)。下面的代码是完整可跑的。from transformers import Trainer, TrainingArguments, DataCollatorWithPadding # DataCollatorWithPadding 在 batch 内动态 pad比全局 pad 到 max_length 省算力 data_collator DataCollatorWithPadding(tokenizertokenizer) args TrainingArguments( output_dir./weibo_bert_ckpt, learning_rate2e-5, per_device_train_batch_size32, per_device_eval_batch_size64, num_train_epochs3, weight_decay0.01, warmup_ratio0.1, eval_strategyepoch, # 老版本 transformers 用 evaluation_strategyepoch save_strategyepoch, logging_steps200, fp16True, load_best_model_at_endTrue, metric_for_best_modelf1, save_total_limit2, ) trainer Trainer( modelmodel, argsargs, train_datasettrain_ds, eval_dataseteval_ds, tokenizertokenizer, data_collatordata_collator, compute_metricscompute_metrics, ) trainer.train() trainer.save_model(./weibo_bert_final)几个容易被忽略的点save_strategy必须和eval_strategy保持一致否则load_best_model_at_end会找不到对应的 checkpointsave_total_limit2控制只保留最近两个 checkpoint避免磁盘被训练中间产物塞满save_model会把 tokenizer 和 config 一并写进目录后续加载只需要传这一个路径。3.4.1 评估函数必须计算 F1而不是只看准确率metric_for_best_modelf1要求compute_metrics返回的字典里包含f1这个键。情感分类正负样本即使均衡准确率也会掩盖部分类别的劣势F1 能同时反映精确率和召回率。import numpy as np from sklearn.metrics import accuracy_score, precision_recall_fscore_support def compute_metrics(eval_pred): logits, labels eval_pred preds np.argmax(logits, axis-1) precision, recall, f1, _ precision_recall_fscore_support(labels, preds, averagebinary) acc accuracy_score(labels, preds) return {acc: acc, precision: precision, recall: recall, f1: f1}averagebinary会把 label 为 1 的类别当成正类也就是「正面情感」。如果你的业务更关心负面识别可以在后面把正类换成 0或者用averagemacro看两个类别的平均表现。3.5 推理加载 checkpoint而不是重新训练训练结束后./weibo_bert_final目录里包含模型权重、tokenizer 配置和 label 映射。用pipeline可以直接做单条预测。from transformers import pipeline # 从目录加载不需要重新指定模型名和标签映射 clf pipeline(text-classification, model./weibo_bert_final, tokenizer./weibo_bert_final) result clf(这家店的火锅底料太咸了但是服务态度很好) print(result) # [{label: 负面, score: 0.93}]注意这种带转折的句子模型大概率判负面因为「太咸了」的情感强度高于「服务态度很好」。这不算模型错误而是微博情感标注本身的倾向负面词对整体判断的影响更大。这个现象在后续坏例分析里会反复出现。4. 微调过程中的典型故障与效果诊断4.1 loss 不降或震荡先查学习率再看 batch size训练 loss 在前几百步不降是正常的因为分类头是随机初始化的warmup 阶段学习率还在爬升。但如果一个 epoch 结束后 loss 仍然在 0.7 附近波动大概率是学习率设置过高模型在损失曲面边缘震荡。另一个容易被忽略的点是 batch size 太小导致的梯度噪声。微博文本长度差异大如果 batch size 只有 8每个 batch 的梯度方向可能差异很大表现为 loss 曲线锯齿明显。优先把 batch size 提到 16 或 32如果显存不够用gradient_accumulation_steps2模拟更大的 batch。注意梯度累积不会减少训练时间但能稳定梯度。4.2 验证集 F1 上不去重复文本泄漏和标注歧义如果你发现训练集 acc 到了 98% 以上验证集 F1 却卡在 88% 左右首先怀疑数据划分泄漏。WeiboSenti100k 是从微博抓取的大量转发和复读导致同一个文本出现多次。随机划分时训练集和验证集可能各出现一次相同文本模型相当于「见过」验证集样本。解决方法在第 2 章已经给出先按文本去重再划分。如果去重后 F1 只下降了一两个点说明原划分确实把重复样本也算进了评估指标虚高如果下降幅度很大那说明模型对重复文本的「记忆」远超对情感的「理解」这时候要考虑增加数据多样性而不是继续调参。4.3 中文文本的预处理坑UNK 堆积和全角标点bert-base-chinese 词表覆盖常用汉字没问题但 URL 会被切成[UNK]或碎片 token一条含 URL 的微博可能产生十几个无意义 token。表情符号同理词表里没有对应字。过度清洗的坏处上面说过这里补充一个更隐蔽的问题全角数字和字母会被 tokenizer 转成半角再切但全角标点会占 token 位。判断自己的预处理是否合理有一个简单方法训练前随机抽 100 条文本用 tokenizer 编码后打印input_ids对应的 token 序列肉眼检查 UNK 占比和断句是否合理。如果 UNK 超过 2%先做清洗再训练。对于微博场景URL 和 用户名替换成空格通常就够不建议引入[URL]这类特殊 token因为 tokenizer 不认识它反而会产生更多碎片。4.4 用 F1 和混淆矩阵评估而不是只看 accuracy二分类准确率在正负样本均衡时能到 95% 以上但业务上更关心的是负面识别率。混淆矩阵能直接看出模型是把负面误判成正面多还是反过来。from sklearn.metrics import confusion_matrix # predict 返回 PredictionOutputpredictions 是 logits pred_out trainer.predict(eval_ds) y_pred np.argmax(pred_out.predictions, axis-1) y_true pred_out.label_ids cm confusion_matrix(y_true, y_pred) print(cm) # 输出格式: [[真负面 判负面, 真负面 判正面], # [真正面 判负面, 真正面 判正面]]如果cm[0][1]明显高于cm[1][0]说明模型倾向于把负面判成正面。这在微博场景很常见因为大量负面表达是反讽或带表情的软性吐槽字面上没有强烈负面词。此时可以考虑调整预测阈值把判定为负面的概率阈值从 0.5 降到 0.45换召回率的提升。4.5 显存不够时的降级路线老显卡 8GB 显存跑 bert-base-chinese 完全够但 batch size 32 可能会爆显存。降级顺序建议是把max_length从 512 降到 128微博短文本几乎无损per_device_train_batch_size降到 16开gradient_accumulation_steps2开启gradient_checkpointingTrue用训练时间换显存最后才考虑换更小的模型比如bert-base-chinese换成albert-chinese-tiny显存问题大概率在前两步就解决了不需要一上来就换模型。5. 从「能跑」到「能上线」坏例分析、阈值与 LoRA 变体5.1 收集低置信度样本而不是只看准确率模型在测试集上的 F1 只能告诉你平均水平但上线前真正要处理的是那批「介于正负之间」的样本。我一般会写一个脚本把预测概率落在 0.45 到 0.55 之间的样本全部导出逐条人工看。import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer model AutoModelForSequenceClassification.from_pretrained(./weibo_bert_final) tokenizer AutoTokenizer.from_pretrained(./weibo_bert_final) model.eval() texts [为什么还不发货, 手感不错但颜色太土了] inputs tokenizer(texts, paddingTrue, truncationTrue, max_length128, return_tensorspt) with torch.no_grad(): probs torch.softmax(model(**inputs).logits, dim-1) for t, p in zip(texts, probs): neg, pos p.tolist() print(f{t} | 负面{neg:.3f} 正面{pos:.3f} | 置信度{max(neg, pos):.3f})低置信度区间往往集中了反讽、转折和网络新词。如果你的业务对这些样本有强需求比如舆情系统必须识别出「呵呵」「我也是醉了」这一类的负面表达那单靠 bert-base-chinese 微调是不够的需要补充这几类样本做二次标注再继续训练。5.2 阈值调整让模型偏向你要的那个类别二分类默认取概率大于 0.5 的类别但业务上有时希望负面识别更激进。在验证集上计算不同阈值下的 precision 和 recall选择业务可接受的平衡点。常见做法是只调负面类的阈值保持正面阈值不变。这个调整不需要重新训练在推理阶段加一个判断分支即可。5.3 想省显存或保留基座模型LoRA 微调的改法如果你的机器显存不足或者想保留一个通用的 bert-base-chinese 基座、按业务场景加载多个微调副本可以改用 LoRA。LoRA 只训练注入的低秩矩阵可训练参数量约为全量微调的 1% 到 2%显存占用显著下降同时在该数据量下也能拿到和全参数微调接近的 F1。这也是常说的 SFT 微调里的一种低成本实现。from peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, lora_alpha16, target_modules[query, value], lora_dropout0.1, ) model get_peft_model(model, lora_config)之后仍然用 Trainer 训练args里除了学习率可以适当提高到 3e-5 外其余保持不变。训练结束后model.save_pretrained只保存 LoRA 权重推理时需要先加载基座模型再加载 adapter。全参数微调在这个数据量下已经完全够用LoRA 更多是工程选型而不是效果提升手段。当你把低置信度样本导出、把阈值调到业务区间、再把训练好的模型替换进在线服务后这个项目的价值才真正显现。本文还有配套的精品资源点击获取