中文情感分析实战:WeiboSenti100k与BERT微调工程指南 简介这份资源是一套面向计算机、人工智能及相关专业学生的中文情感分析实战方案以WeiboSenti100k微博评论语料为基础结合预训练语言模型BERT进行针对性微调可用于课程设计、学期项目或毕业设计参考帮助学习者打通从文本预处理、模型构建到训练评估的完整链路。压缩包共7个文件约19.44MB包含Python训练与推理脚本、CSV格式的微博情感数据集、Markdown说明文档及依赖配置等覆盖数据、代码与文档三类核心内容便于快速复现实验。目前已有29人学习。方案在学术评审中获得98分并获指导教师认可读者可据此掌握BERT微调流程、情感分类评估方法及常见调参排错思路形成从理论到实践的系统知识框架。1. 中文情感分析落地从 WeiboSenti100k 到 BERT 微调的真实工程路径中文情感分析这件事看起来门槛不高但真到业务里跑起来翻车点往往不在模型结构而在数据格式、标签分布和推理后处理上。我最近拆了一份基于 BERT 微调与 WeiboSenti100k 数据集的中文情感分析系统源码整体思路很清晰用预训练中文 BERT 做底座在十万级微博情感标注数据上做微调最后封装成可复用的推理接口。它解决的核心问题是——让中小团队不用从零标注数据、不用自己搭训练框架就能拿到一个在中文短文本情感分类上可用的基线系统。适合谁一是需要快速验证情感分析产品形态的开发者二是想学习 BERT 微调完整流程的学生或转行者三是手里有垂直语料但缺一个靠谱起点做继续预训练或领域适配的工程师。这份源码的价值不在于模型多新而在于它把数据加载、分词对齐、训练循环、评估指标和推理导出串成了一条能跑通的链路省去了大量拼装时间。2. 拆解源码结构WeiboSenti100k 数据流与 BERT 微调入口2.1 数据集长什么样字段、标签与分布陷阱WeiboSenti100k 从名字就能看出规模十万条量级的中文微博文本标注为情感极性。常见做法是二分类正面/负面部分版本会保留中性或细粒度情绪但这份源码按二分类处理。数据文件通常是 TSV 或 CSV核心字段就两个text和label。text是原始微博内容长度参差不齐短的几个字长的可能带话题标签和 信息label一般是 0/1 或 1/0 映射到负面/正面。这里第一个坑是标签分布。微博情感数据天然不均衡负面样本往往偏多或者某些话题下正面扎堆。源码里如果直接按默认比例切分训练集和验证集验证集准确率会虚高。我一般会先跑一遍统计import pandas as pd # 假设数据文件为 weibosenti100k.tsv无表头列顺序为 text, label df pd.read_csv(weibosenti100k.tsv, sep\t, headerNone, names[text, label]) # 查看标签分布 print(df[label].value_counts(normalizeTrue)) # 查看文本长度分布决定 max_seq_length df[char_len] df[text].astype(str).apply(len) print(df[char_len].describe(percentiles[0.5, 0.9, 0.95, 0.99]))逻辑说明value_counts(normalizeTrue)给出各类占比如果某一类低于 30%训练时就要考虑加权损失或重采样。char_len的 95 分位数直接决定max_seq_length设多少设大了浪费显存设小了截断关键情感词。参数上中文 BERT 的 tokenizer 对单字切分较细一般 128 能覆盖绝大多数微博文本如果 99 分位超过 128再考虑 256。2.2 模型加载与微调头设计为什么选 BERT 而不是 LSTM源码用bert-base-chinese作为预训练权重这是最稳妥的选择。中文 BERT 在通用语料上预训练过对微博这种口语化、带网络用语的文本比从零训练的词向量加 LSTM 强不少。LSTM 方案在长距离依赖和上下文表征上吃亏而 BERT 的注意力机制能捕捉“虽然……但是……”这类转折结构里的情感极性。微调头部分源码在[CLS]向量后接一个 dropout 加全连接层输出二分类 logits。这是标准做法但要注意[CLS]向量是否经过 pooler。HuggingFace 的BertForSequenceClassification默认用 pooler 输出如果自己写训练循环别漏掉pooler_output和dropout。from transformers import BertTokenizer, BertForSequenceClassification import torch.nn as nn model_name bert-base-chinese tokenizer BertTokenizer.from_pretrained(model_name) # 二分类num_labels2 model BertForSequenceClassification.from_pretrained(model_name, num_labels2) # 如果显存紧张可以冻结底层参数只训练最后几层 # for name, param in model.bert.named_parameters(): # if layer.10 not in name and layer.11 not in name and pooler not in name: # param.requires_grad False逻辑说明num_labels2决定分类头输出维度。冻结底层是显存不够时的常见做法但会损失一些领域适配能力微博语料和通用语料差异不算极端冻结太多反而欠拟合。我一般先全量微调跑不动再逐层解冻。学习率设 2e-5 到 5e-5 之间BERT 微调对学习率敏感太大容易灾难性遗忘太小收敛慢。2.3 训练循环与评估损失、优化器与指标选择源码的训练循环用 AdamW 优化器带权重衰减学习率线性预热加衰减。这是 BERT 微调的标准配置。损失函数用交叉熵如果标签不均衡可以传class_weights。from transformers import AdamW, get_linear_schedule_with_warmup import torch device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) optimizer AdamW(model.parameters(), lr3e-5, weight_decay0.01) epochs 3 total_steps len(train_loader) * epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(0.1 * total_steps), num_training_stepstotal_steps ) # 如果标签不均衡计算类别权重 # from sklearn.utils.class_weight import compute_class_weight # weights compute_class_weight(balanced, classesnp.array([0,1]), ytrain_labels) # criterion nn.CrossEntropyLoss(weighttorch.tensor(weights, dtypetorch.float).to(device)) criterion nn.CrossEntropyLoss() for epoch in range(epochs): model.train() for batch in train_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[label].to(device) optimizer.zero_grad() outputs model(input_ids, attention_maskattention_mask, labelslabels) loss outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step()逻辑说明get_linear_schedule_with_warmup的前 10% 步数做预热防止初期梯度震荡。clip_grad_norm_设 1.0 是防止梯度爆炸微博文本里偶尔有超长样本不加裁剪容易跑飞。评估指标别只看准确率F1 更可靠尤其是类别不均衡时。源码里如果只打印 accuracy建议自己补一个classification_report。3. 从训练到推理模型导出、分词对齐与批量预测3.1 保存与加载state_dict 还是完整模型训练完保存模型有两种方式存state_dict或存整个模型。源码如果用的是 HuggingFace 的save_pretrained会同时保存配置和权重加载时直接from_pretrained即可。如果自己写torch.save(model.state_dict())加载时必须先实例化同结构模型再load_state_dict否则键名对不上。# 方式一HuggingFace 标准保存 model.save_pretrained(./saved_model) tokenizer.save_pretrained(./saved_model) # 加载 from transformers import BertForSequenceClassification, BertTokenizer model BertForSequenceClassification.from_pretrained(./saved_model) tokenizer BertTokenizer.from_pretrained(./saved_model)逻辑说明save_pretrained会把config.json、pytorch_model.bin和 tokenizer 相关文件一起存下来部署时整个目录拷走就行。注意 tokenizer 的vocab.txt必须和训练时一致换 tokenizer 会导致 token id 错位预测结果直接乱掉。3.2 推理封装处理原始微博文本的四个细节推理阶段最容易出问题的是文本预处理。微博文本里混着 URL、表情符号、话题标签、 提及训练时如果没清洗推理时也别清洗保持一致。但如果训练时清洗了推理时漏掉分布偏移会让模型表现下降。import torch import torch.nn.functional as F def predict(text, model, tokenizer, max_len128): model.eval() # 编码注意 truncation 和 padding inputs tokenizer( text, return_tensorspt, truncationTrue, paddingmax_length, max_lengthmax_len ) inputs {k: v.to(model.device) for k, v in inputs.items()} with torch.no_grad(): outputs model(**inputs) probs F.softmax(outputs.logits, dim-1) pred torch.argmax(probs, dim-1).item() return pred, probs[0][pred].item()逻辑说明paddingmax_length保证 batch 内长度一致单条推理时也可以用paddingTrue动态补齐。softmax后的概率值可以当置信度用但别直接当校准概率BERT 微调后的输出往往偏自信低置信度样本建议人工复核。max_len必须和训练时一致训练用 128 推理用 256位置编码对不上结果会漂。3.3 批量推理与性能取舍线上服务如果 QPS 不高逐条推理够用。如果要批量注意显存和延迟的平衡。batch size 设 16 或 32 通常能压满 GPU再大收益递减。CPU 推理的话用 ONNX Runtime 或量化能提速不少但这份源码没带这些属于进阶优化。def batch_predict(texts, model, tokenizer, batch_size32, max_len128): model.eval() results [] for i in range(0, len(texts), batch_size): batch_texts texts[i:ibatch_size] inputs tokenizer( batch_texts, return_tensorspt, truncationTrue, paddingmax_length, max_lengthmax_len ) inputs {k: v.to(model.device) for k, v in inputs.items()} with torch.no_grad(): outputs model(**inputs) preds torch.argmax(outputs.logits, dim-1).cpu().numpy() results.extend(preds.tolist()) return results逻辑说明批量推理时paddingmax_length会浪费一些计算但实现简单。如果追求极致可以按长度分桶再动态 padding代码复杂度会上升。batch_size根据显存调OOM 就减半。4. 避坑与排查微调中文 BERT 时最容易翻车的五个点4.1 现象训练 loss 正常下降验证集准确率不动原因学习率太大导致模型在预训练权重附近震荡或者 tokenizer 和模型不匹配。常见于换了中文 BERT 变体但没换对应 tokenizer。解决先把学习率降到 2e-5 试一个 epoch确认 tokenizer 的vocab.txt和模型配置里的vocab_size一致。打印一下tokenizer.convert_tokens_to_ids([CLS])看是不是 101不是就说明 tokenizer 错了。4.2 现象推理时所有样本都预测成同一类原因标签映射反了或者训练时label列被当成字符串模型学了个常数。也有可能是attention_mask没传padding 位置参与了注意力计算。解决检查训练数据里label的 dtype确保是 int。推理时确认attention_mask传入了。如果还不行拿几条训练集样本做推理看能不能复现训练时的预测不能就是保存加载环节出了问题。4.3 现象显存溢出batch size 降到 1 还是 OOM原因max_seq_length设太大或者模型没冻结底层全量微调 12 层 BERT 在 8G 显存上确实吃力。解决先把max_seq_length降到 64 试跑确认能跑通再往上加。或者用梯度累积batch_size8累积 4 步等效 32。再不行就冻结底层 8 层只训最后 4 层加分类头。4.4 现象验证集 F1 很高上线后效果差原因验证集和线上数据分布不一致。WeiboSenti100k 是微博语料如果线上是电商评论或客服对话领域偏移会导致效果打折。解决拿一批线上真实数据做人工标注至少几百条做一次领域内评估。如果差距大用线上数据继续微调几个 epoch学习率调小到 1e-5。4.5 现象模型保存后重新加载预测结果和保存前不一致原因保存时用了model.state_dict()加载时模型结构有细微差异比如 dropout 层位置不同或者分类头初始化方式变了。解决统一用save_pretrained和from_pretrained。如果必须用state_dict保存前打印model.state_dict().keys()加载后对比键名和形状确保完全一致。5. 进阶技巧用置信度阈值和温度缩放提升线上可用性模型训完只是起点线上真正好用还得做后处理。BERT 微调后的 softmax 概率往往偏极端正面样本动辄 0.99负面也类似这会导致低置信度样本被忽略。我一般会加一个温度缩放把 logits 除以一个温度系数再 softmax让概率分布更平滑方便设阈值。import torch.nn.functional as F def predict_with_temperature(text, model, tokenizer, temperature1.5, max_len128): model.eval() inputs tokenizer(text, return_tensorspt, truncationTrue, paddingmax_length, max_lengthmax_len) inputs {k: v.to(model.device) for k, v in inputs.items()} with torch.no_grad(): logits model(**inputs).logits # 温度缩放 scaled_logits logits / temperature probs F.softmax(scaled_logits, dim-1) pred torch.argmax(probs, dim-1).item() confidence probs[0][pred].item() return pred, confidence逻辑说明temperature大于 1 会让分布更平缓小于 1 更尖锐。1.5 到 2.0 之间比较常用。设好温度后可以定一个置信度阈值比如 0.7低于这个值的样本走人工或规则兜底。这样线上准确率能明显提升代价是覆盖率下降具体阈值看业务容忍度。另一个技巧是拿验证集做阈值搜索画一条置信度-准确率曲线找准确率拐点。我习惯在验证集上跑一遍把样本按置信度分桶看每个桶的准确率然后决定阈值设多少。这个流程走一遍比拍脑袋设 0.9 靠谱得多。从那以后我每次微调完 BERT 做分类都强制走一遍温度缩放和阈值搜索不然线上总有一批模棱两可的样本在捣乱。希望帮到你。本文还有配套的精品资源点击获取