☰
bert-base-chinese微调WeiboSenti100k:中文情感分析完整实践
2026/10/7 21:31:13 网站建设 项目流程

简介:基于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 版本冲突。我一般建议把依赖锁在一个已验证过的组合上,比如transformers==4.36.2、torch==2.1.0。如果你的包带了requirements.txt,直接用;否则按下面的命令装一份最小依赖。

python -m venv venv source venv/bin/activate pip install torch==2.1.0 transformers==4.36.2 datasets==2.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", encoding="utf-8") raw_df = raw_df[["label", "text"]] raw_df = raw_df.dropna(subset=["text"]) train_df, dev_df = train_test_split( raw_df, test_size=0.1, stratify=raw_df["label"], random_state=42, ) train_df.to_csv("data/train.csv", index=False) dev_df.to_csv("data/dev.csv", index=False)

代码里test_size=0.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(r"http\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(drop=True)

清洗之后一定要重新统计长度分布,并过滤掉清洗后变成空字符串的样本。这类样本在某些包里可能占比 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的词表有两万多个 token,10 万条微博按平均 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"], truncation=True, max_length=128, ) train_dataset = Dataset.from_pandas(train_df[["text", "label"]]) train_dataset = train_dataset.map(tokenize_fn, batched=True, remove_columns=["text"])

这段代码的核心参数是max_length=128。很多入门教程会写成 512,但微博短文本大部分不超过 100 字,把max_length拉到 512 只是白白增加计算量和显存占用,并不会带来精度提升。我自己一般先用 128 跑通,再用 256 做一组对比实验,看验证集 F1 有没有明显变化再决定最终配置。

最后一环是DataCollatorWithPadding。因为每个 batch 内文本长度不同,需要把同一 batch 的输入 pad 到相同长度再送进模型。这个 collator 会让 padding 操作发生在每个 batch 内部,速度比提前统一 pad 到 128 要快,且不占多余显存。

from transformers import DataCollatorWithPadding data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

这里不用手动指定padding="max_length",DataCollatorWithPadding会自动按 batch 内最长样本补齐。注意如果你的模型输入用的token_type_ids,collator 也会一并处理。到这里,数据层面的准备工作基本结束,可以进入训练环节了。

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。不过如果你显卡显存只有 4G,per_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 显存可用 32,4G 显存降到 8
weight_decay0.01对非 bias 和 LayerNorm 参数做权重衰减
warmup_ratio0.1前 10% 步数线性升温,稳定早期 loss
eval_strategyepoch每个 epoch 结束跑一次验证集
load_best_model_at_endTrue训练结束自动加载最优 checkpoint
metric_for_best_modeleval_f1按 F1 选最优模型,而不是准确率

eval_strategy是老版本evaluation_strategy改名来的,如果你用的 transformers 版本比较老(比如 4.20 之前),要写evaluation_strategy="epoch",否则会直接报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_labels=2, id2label={0: "负面", 1: "正面"}, label2id={"负面": 0, "正面": 1}, ) training_args = TrainingArguments( output_dir="./checkpoints", learning_rate=3e-5, per_device_train_batch_size=16, per_device_eval_batch_size=32, num_train_epochs=3, weight_decay=0.01, warmup_ratio=0.1, eval_strategy="epoch", save_strategy="epoch", logging_steps=200, load_best_model_at_end=True, metric_for_best_model="eval_f1", save_total_limit=2, report_to=[], seed=42, ) 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, average="binary"), } trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=dev_dataset, data_collator=data_collator, tokenizer=tokenizer, compute_metrics=compute_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_only=True) tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") # dev_texts 和 dev_labels 从 dev.csv 加载 predictions = [] for text in dev_texts: inputs = tokenizer(text, truncation=True, max_length=128, return_tensors="pt") 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_only=True这个参数,它强制从本地目录加载权重而不是再去联网检查远端更新。实测经验里,这一步能避免不少因网络不稳定导致的偶发加载失败。

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, truncation=True, max_length=128, return_tensors="pt") 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(host="0.0.0.0", port=5000)

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_only=True或离线模式环境变量HF_DATASETS_OFFLINE=1。

现象二:训练能跑但 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_strategy="epoch"。

现象四:验证集准确率很高,但拿新微博文本测试时表现很差。
原因是微博文本里的标点和表情没被正确处理,比如全角英文、[笑cry]里的方括号被清洗脚本误删。解决:检查清洗函数是否把[]也当普通符号删掉了,调整正则保留[...]包裹的表情标记。

现象五:显存明明够 8G,但一训练就 OOM。
原因是max_length=512加上per_device_train_batch_size=32的组合,把序列长度和 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_tensors="pt") 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_version=14, )

导出后最好用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 环境推理」强制走一遍。这个习惯帮我挡掉了不止一次突发状况。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询