bert-base-chinese微调WeiboSenti100k:中文情感分析完整实践
简介基于WeiboSenti100k微博情感数据集通过bert-base-chinese预训练模型微调的中文情感分析毕业设计源码包面向NLP方向的毕设学生、研究人员及入门开发者覆盖数据加载、模型训练、推理评估的完整实践链路也适合作为BERT中文文本分类任务的教学示例。压缩包共5个文件包含两个Python脚本训练脚本train.py、推理脚本inference.py、依赖清单requirements.txt、项目说明README.md以及微博情感数据集weibo_senti_100k.csv整体约9.73MB结构紧凑便于直接迁移使用。目前已有103人浏览学习。这套源码可直接作为毕设核心代码按README配置环境后即可运行微调并验证模型效果同时提供清晰的数据格式与训练推理流程有助于理解预训练模型的微调机制以及情感分类任务中的数据处理、参数调优和结果评估等关键环节。1. 微调中文情感分析这份 bert-base-chinese 源码包把 WeiboSenti100k 的完整链路包圆了做中文情感分析毕业设计绕不开bert-base-chinese和WeiboSenti100k这个组合前者是中文领域最常用的预训练权重后者是 10 万条带正负标签的微博语料两个凑在一起正好组成一套标准的中文情感分析微调任务。这份源码包的价值在于它把数据清洗、训练/验证切分、BERT 微调、评估、推理接口全部串成了一条能直接跑通的链路而不是给你一堆零散脚本自己去拼。我拆这个包的时候最直观的感受是它不像某些课程设计只丢一个训练ipynb而是把「项目说明 源码 数据整理脚本」放在一起照着 README 一步步执行就能得到可用的分类模型。对正在做毕业设计的学生、想快速上手 Hugging Face 微调流程的从业者都适用尤其是那些第一次接触 BERT 微调、需要一份完整可复现代码的人。2. 环境与数据台账先把 GPU、transformers 依赖和 WeiboSenti100k 核清再开始训练2.1 从项目包目录建立“能跑”的预期下载解压之后我习惯先用一条命令把整个目录结构打出来而不是直接点开训练脚本就跑。因为这类源码包最常见的翻车点不是代码逻辑而是 README 里写着要用某个版本的数据集结果本地路径根本对不上。unzip based_on_weibosenti100k_bert-base-chinese_finetune.zip -d ./weibo_senti cd ./weibo_senti tree -L 2你应该能在根目录看到类似data/、src/、README.md的结构。data/下通常放着已经整理好的 CSV 或 JSONL 格式语料src/下会有数据预处理脚本、训练脚本和推理脚本。先看 README 里对数据格式的约定再动手。这里说明一个判断要点如果包里的数据已经是切分好的train.csv和dev.csv说明作者已经帮你处理过数据泄漏的问题如果只有一份全量数据那你要自己完成分层切分。切分这件事直接决定你后面报告里的 F1 有没有说服力千万别随便random.shuffle两行就完事。2.2 环境核对命令与依赖版本选择微调bert-base-chinese并不需要特别新的 transformers 版本但版本太老会遇到Trainer参数不兼容的问题版本太新又可能跟 pytorch 版本冲突。我一般建议把依赖锁在一个已验证过的组合上比如transformers4.36.2、torch2.1.0。如果你的包带了requirements.txt直接用否则按下面的命令装一份最小依赖。python -m venv venv source venv/bin/activate pip install torch2.1.0 transformers4.36.2 datasets2.14.5 scikit-learn pandas tqdm如果你在 Windows 上跑source venv/bin/activate要换成venv\Scripts\activate。还有一点torch的 CUDA 版本要跟显卡驱动匹配。启动训练前先跑一遍nvidia-smi确认你本机的 CUDA 版本不是 10.x否则后面Trainer会静默退回 CPU训练速度慢十倍。数据集这一层我遇到过不少坑。datasets库可以直接加载weibo_senti_100k但很多时候项目包已经把它转换成了本地文件我的经验是优先用包内的本地数据避免训练和验证分布不一致也避免临时下载导致断网中断。2.3 数据落盘与种子固定不管数据是原始 json 还是 HuggingFace 格式先统一整理成一份带表头的 CSV同时固定随机种子。这样做的目的是让整个实验可复现你后续换学习率、换max_length做对比实验时前后两次训练用的是同一批验证集对比才有意义。import pandas as pd from sklearn.model_selection import train_test_split raw_df pd.read_csv(weibo_senti_100k.csv, encodingutf-8) raw_df raw_df[[label, text]] raw_df raw_df.dropna(subset[text]) train_df, dev_df train_test_split( raw_df, test_size0.1, stratifyraw_df[label], random_state42, ) train_df.to_csv(data/train.csv, indexFalse) dev_df.to_csv(data/dev.csv, indexFalse)代码里test_size0.1是在 10 万条里拿 1 万条做验证stratify按标签比例分层抽样保证正负样本在训练集和验证集里的比例一致。对分类任务来说如果直接随机切分极可能让某个标签在验证集里占比过高最后计算出的准确率看起来不错实际上模型在真实分布上很虚。数据落盘之后下一步就是按 BERT 的要求做清洗和 tokenization。这一步我单独用一章来写因为那里才是真正决定最终 F1 的地方。3. 数据与 Dataset 构建让 10 万条微博文本变成 BERT 能直接吃进内存的输入3.1 理解 WeiboSenti100k 的标签和字段分布打开数据先别急着训练第一步是统计标签分布和文本长度。BERT 的输入长度有上限bert-base-chinese默认最大 512微博文本虽然大多很短但偶尔会有超长文本和转发链这直接影响截断策略。import pandas as pd df pd.read_csv(data/train.csv) print(df[label].value_counts()) df[text_len] df[text].str.len() print(df[text_len].describe())WeiboSenti100k 的标签通常是0表示负面、1表示正面两者比例接近 1:1。如果你的包里标签字段是中文如正面/负面在预处理脚本里要统一转成0/1否则后面BertForSequenceClassification的num_labels会跟标签取值对不上。文本长度分布要重点看75%和max这两行。大部分微博在 30 到 120 字之间但max可能到几百甚至上千说明自动去转发链之前有大量冗余//用户名信息。这类转发链对情感判定有时有帮助原博文情感会传递但在大多数毕业设计场景里“retweet 结构”带来的噪声远大于信号我倾向于去掉。3.2 清洗逻辑去 URL、去转发标记但保留表情符号微博文本的清洗跟新闻语料不一样。新闻里遇到 URL 直接删掉就行但微博里大量情感其实寄托在表情符号上比如“[笑cry]”“[怒]”“[good]”这些带情绪的标记对 BERT 来说都是有价值的特征。所以清洗原则是URL 删掉、转发链删掉、连续空格合并表情符号保留交给 tokenizer 的 vocab 去处理。import re def clean_weibo_text(text: str) - str: # 去掉 http 链接包括 t.cn 短链 text re.sub(rhttp\S|https\S, , text) # 去掉转发链形如 //用户名: text re.sub(r//\S*?:?\s*, , text) # 去掉 用户名 和 话题两端的 #但保留话题词本身 text re.sub(r\S*?[\s。], , text) text re.sub(r#, , text) # 空白压缩 text re.sub(r\s, , text).strip() return text df[text] df[text].astype(str).apply(clean_weibo_text) df df[df[text].str.len() 0].reset_index(dropTrue)清洗之后一定要重新统计长度分布并过滤掉清洗后变成空字符串的样本。这类样本在某些包里可能占比 1% 左右直接放进训练集不会让模型崩溃但会让 loss 曲线出现短暂尖峰因为空文本经过 tokenizer 后只剩[CLS]和[SEP]模型没法学到一个稳定的表示。3.3 用 Dataset 和 DataCollator 统一模型输入接下来是把清洗后的 DataFrame 转换成 HuggingFaceDataset。这里有两种做法一种是在__getitem__里动态 tokenize节省内存但每个 epoch 都要重复计算另一种是先用map把整个数据集 tokenize 成input_ids/attention_mask/token_type_ids存进内存训练时只做 padding。内存够用就选第二种。bert-base-chinese的词表有两万多个 token10 万条微博按平均 80 字算tokenize 后大概占几百 MB 内存完全能接受。from datasets import Dataset from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) def tokenize_fn(examples): return tokenizer( examples[text], truncationTrue, max_length128, ) train_dataset Dataset.from_pandas(train_df[[text, label]]) train_dataset train_dataset.map(tokenize_fn, batchedTrue, remove_columns[text])这段代码的核心参数是max_length128。很多入门教程会写成 512但微博短文本大部分不超过 100 字把max_length拉到 512 只是白白增加计算量和显存占用并不会带来精度提升。我自己一般先用 128 跑通再用 256 做一组对比实验看验证集 F1 有没有明显变化再决定最终配置。最后一环是DataCollatorWithPadding。因为每个 batch 内文本长度不同需要把同一 batch 的输入 pad 到相同长度再送进模型。这个 collator 会让 padding 操作发生在每个 batch 内部速度比提前统一 pad 到 128 要快且不占多余显存。from transformers import DataCollatorWithPadding data_collator DataCollatorWithPadding(tokenizertokenizer)这里不用手动指定paddingmax_lengthDataCollatorWithPadding会自动按 batch 内最长样本补齐。注意如果你的模型输入用的token_type_idscollator 也会一并处理。到这里数据层面的准备工作基本结束可以进入训练环节了。4. Trainer 微调 bert-base-chinese全参微调取舍与一组能复现的参数表4.1 为什么选 bert-base-chinese全参微调而不是 LoRA很多读者看到大模型微调就想到 LoRA但在这个项目里我建议先做全参微调。原因是bert-base-chinese本身就是参数量约 1.02 亿的模型跟现在动辄几十亿的大模型不是一个量级微调一次在消费级显卡上只要几十分钟到一两个小时完全不需要用 LoRA 来省显存。LoRA 在千亿参数模型上很有价值但在 1 亿参数的中小模型上它会引入额外超参r、alpha、target_modules调参成本反而高于收益。如果项目包里默认是全参微调脚本没必要为了追热点改成 LoRA。不过如果你显卡显存只有 4Gper_device_train_batch_size会非常小这时候可以考虑只在attention层挂 LoRA。选bert-base-chinese而不是其他中文预训练模型主要原因是数据兼容性。WeiboSenti100k 是中文微博语料bert-base-chinese的中文词表覆盖了常见网络用语和表情符号词条微调收敛速度快换别的模型还得重新处理词表映射。4.2 TrainingArguments 关键参数说明这一组参数是我在这个数据集上调过几次之后相对稳定的配置直接抄问题不大但你要理解每个参数在干什么参数推荐值说明learning_rate3e-5BERT 微调常用区间是 2e-5 到 5e-5太高会导致灾难性遗忘num_train_epochs3微博情感分类任务简单3 个 epoch 足够多了会过拟合per_device_train_batch_size166G 显存可用 324G 显存降到 8weight_decay0.01对非 bias 和 LayerNorm 参数做权重衰减warmup_ratio0.1前 10% 步数线性升温稳定早期 losseval_strategyepoch每个 epoch 结束跑一次验证集load_best_model_at_endTrue训练结束自动加载最优 checkpointmetric_for_best_modeleval_f1按 F1 选最优模型而不是准确率eval_strategy是老版本evaluation_strategy改名来的如果你用的 transformers 版本比较老比如 4.20 之前要写evaluation_strategyepoch否则会直接报TypeError。这一条我在第 5 章避坑里还会展开。4.3 训练代码Trainer 的两个关键钩子下面这段代码是微调主体的核心。compute_metrics是Trainer的回调钩子每个 epoch 验证结束后自动调用用来计算准确率和 F1。from transformers import ( AutoTokenizer, BertForSequenceClassification, Trainer, TrainingArguments, ) from sklearn.metrics import accuracy_score, f1_score import numpy as np model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2, id2label{0: 负面, 1: 正面}, label2id{负面: 0, 正面: 1}, ) training_args TrainingArguments( output_dir./checkpoints, learning_rate3e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs3, weight_decay0.01, warmup_ratio0.1, eval_strategyepoch, save_strategyepoch, logging_steps200, load_best_model_at_endTrue, metric_for_best_modeleval_f1, save_total_limit2, report_to[], seed42, ) def compute_metrics(eval_pred): logits, labels eval_pred preds np.argmax(logits, axis-1) return { accuracy: accuracy_score(labels, preds), f1: f1_score(labels, preds, averagebinary), } trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetdev_dataset, data_collatordata_collator, tokenizertokenizer, compute_metricscompute_metrics, ) trainer.train()两个关键钩子分别是compute_metrics和data_collator。compute_metrics接收的是原始 logits 和 label所以必须在函数内部做argmax之后才能算指标data_collator则保证了每个 batch 在进模型前已经完成了 padding。这段代码里有个容易忽视的点report_to[]。Trainer默认会尝试连接wandb如果你机器上没配好 wandb 账号训练会在初始化阶段卡住或者频繁打印警告。加上report_to[]等于告诉 Trainer 不需要向任何外部平台上报训练日志只留在本地。如果你是第一次跑这个项目看到控制台输出Starting training之后 5 分钟都没有进度条大概率就是卡在 wandb 上了。训练结束后model会保留下最优权重但真正要用于推理还需要显式调用trainer.save_model()把模型配置和权重落盘到指定目录。5. 评估、推理与避坑把 F1 算对再交付一个能演示的中文情感分析接口5.1 评估脚本从 eval_f1 到完整报告Trainer 训练时打印的eval_f1只是模型在验证集上的单值指标毕业设计评测要求往往需要完整报告包括每一类的 precision、recall、f1 和混淆矩阵。这里单独写一个评估脚本来加载最优 checkpoint 并输出详细指标。from transformers import AutoModelForSequenceClassification, AutoTokenizer from sklearn.metrics import classification_report, confusion_matrix model AutoModelForSequenceClassification.from_pretrained(./checkpoints/checkpoint-5600, local_files_onlyTrue) tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) # dev_texts 和 dev_labels 从 dev.csv 加载 predictions [] for text in dev_texts: inputs tokenizer(text, truncationTrue, max_length128, return_tensorspt) logits model(**inputs).logits predictions.append(logits.argmax(-1).item()) print(classification_report(dev_labels, predictions, target_names[负面, 正面])) print(confusion_matrix(dev_labels, predictions))注意local_files_onlyTrue这个参数它强制从本地目录加载权重而不是再去联网检查远端更新。实测经验里这一步能避免不少因网络不稳定导致的偶发加载失败。5.2 看错误样本比看 loss 更有效评估完之后别急着打印一堆指标就收工。我一般会把预测错误的样本抽出来看重点看两类真实标签是负面但模型判成正面的样本以及模型预测分数接近 0.5 的样本。wrong_df dev_df.copy() wrong_df[pred] predictions wrong_df wrong_df[wrong_df[label] ! wrong_df[pred]] print(wrong_df.head(10).to_string())错误样本能告诉你数据清洗有没有残留问题。比如说如果错误样本里频繁出现表情符号说明模型虽然看到了表情但没学会把表情和整体情感联合起来判定如果错误样本集中在某类特定句式比如“不是很满意”这类双重否定那就说明 BERT 在这种语料上的句法理解还存在边界。毕业设计报告中写两三个 bad case 分析比堆一堆指标更像个真正做过实验的人。5.3 推理接口把模型封装成可演示的服务训练好的模型最终要能给别人演示。用 Flask 写一个最小接口是最常见的做法不依赖前端框架直接在浏览器里测试from flask import Flask, request, jsonify from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch app Flask(__name__) model AutoModelForSequenceClassification.from_pretrained(./best_model) tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) app.route(/predict, methods[POST]) def predict(): text request.json.get(text, ) inputs tokenizer(text, truncationTrue, max_length128, return_tensorspt) with torch.no_grad(): logits model(**inputs).logits pred int(logits.argmax(-1)) return jsonify({label: 正面 if pred 1 else 负面}) if __name__ __main__: app.run(host0.0.0.0, port5000)torch.no_grad()在推理时很重要它让模型不再保存中间变量用于反向传播显存占用降一大截推理速度也更快。如果有人问为什么接口里没有model.eval()我一般会补一句Trainer保存的 checkpoint 在加载时默认是推理模式但为了稳妥起见model.eval()还是应该加上原因是dropout层在训练和推理模式下行为不同。5.4 避坑记录五条你大概率会撞上的问题现象一训练卡在Starting training不前进。原因是Trainer尝试连接 wandb 或 huggingface Hub 进行同步网络或 token 配置有问题就会卡住。解决在TrainingArguments里加report_to[]并在AutoModel.from_pretrained时设置local_files_onlyTrue或离线模式环境变量HF_DATASETS_OFFLINE1。现象二训练能跑但 loss 不降验证 F1 一直徘徊在 0.5 左右。原因是标签映射错误比如标签从 0 开始还是从 1 开始没对齐。解决训练前打印train_dataset[0]和dev_dataset[0]确认label字段值只有 0 和 1然后再看model.config.num_labels是否等于 2。现象三老版本 transformers 报错TypeError: __init__() got an unexpected keyword argument eval_strategy。原因是 transformers 4.36 之后才把evaluation_strategy改名为eval_strategy。解决查一下你pip show transformers的版本老版本就改回evaluation_strategyepoch。现象四验证集准确率很高但拿新微博文本测试时表现很差。原因是微博文本里的标点和表情没被正确处理比如全角英文、[笑cry]里的方括号被清洗脚本误删。解决检查清洗函数是否把[]也当普通符号删掉了调整正则保留[...]包裹的表情标记。现象五显存明明够 8G但一训练就 OOM。原因是max_length512加上per_device_train_batch_size32的组合把序列长度和 batch size 同时拉满了。解决先缩 batch size 到 16再把max_length从 512 降到 128重新看显存占用你会发现 F1 几乎不变。6. 进阶把微调模型导出成 ONNX演示机上不再需要 PyTorch训练和评估跑通只是第一步真正让项目“能交付”的是把模型导出成推理格式。ONNX 的好处是可以在没有 PyTorch 环境的机器上跑也能接入 CPU 推理加速。毕业答辩时现场用一台没装深度学习框架的电脑做演示这是最稳的方案。导出过程比较直接。加载训练好的 checkpoint用torch.onnx.export导出同时固定输入输出的动态维度import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer model AutoModelForSequenceClassification.from_pretrained(./best_model) model.eval() tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) dummy_input tokenizer(今天心情不错, return_tensorspt) torch.onnx.export( model, tuple(dummy_input.values()), sentiment_model.onnx, input_names[input_ids, token_type_ids, attention_mask], output_names[logits], dynamic_axes{ input_ids: {0: batch_size, 1: seq_len}, token_type_ids: {0: batch_size, 1: seq_len}, attention_mask: {0: batch_size, 1: seq_len}, }, opset_version14, )导出后最好用onnxruntime做一次推理验证确认输出和 PyTorch 原模型一致。这里有个容易漏掉的细节dummy_input的 key 顺序必须和input_names一一对应否则导出时会把 token 类型和 mask 弄混。我习惯在部署目录里额外写一个infer_onnx.py里面只依赖onnxruntime和tokenizers不含 torch 和 transformers。这样演示机可以只装一个轻量 Python 环境避免了答辩现场装 torch 装到崩溃的情况。导出 ONNX 之后还需要对比一下原模型和 ONNX 模型在同一批样本上的预测结果。因为有些算子在不同 opset 版本下实现有细微差异如果前后预测不一致优先把opset_version降到 12 再试一次。从那以后我每次交付情感分析项目都会把「训练 → 评估 → ONNX 导出 → 无 torch 环境推理」强制走一遍。这个习惯帮我挡掉了不止一次突发状况。希望帮到你。本文还有配套的精品资源点击获取