ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于WeiboSenti100k微调BERT的中文情感分析实战

基于WeiboSenti100k微调BERT的中文情感分析实战 简介面向高校毕业设计、课程设计与软件工程实践的中文情感分析实战资源基于WeiboSenti100k数据集对bert-base-chinese预训练模型进行微调覆盖数据清洗、格式转换、模型构建、训练评估与推理预测完整流程适合NLP初学者及有一定基础的开发者系统学习。压缩包共5个文件包含2个Python脚本train.py训练脚本与inference.py推理脚本、1个微博情感数据集CSV、1个README项目说明文档和1个requirements依赖清单整体大小约9.73MB结构轻量、目录清晰便于快速下载与部署。已有196人浏览学习资源实用性得到初步认可。源码提供可直接运行的训练与推理代码并附数据预处理思路和模型调参建议通过实际操作可掌握BERT中文文本分类的核心方法理解从数据加载到模型部署的全链路为后续开展NLP相关课题或工程实践打下扎实基础。1. 基于WeiboSenti100k微调bert-base-chinese这套源码解决什么问题中文情感分析在落地时最常遇到的场景不是工整的电商评论而是微博这种短文本——口语化严重、表情符号随处可见、长度参差不齐、反讽和暗语频繁出现拿通用分词加词频统计的旧办法去处理效果往往差到无法上线。基于 WeiboSenti100k 数据集对 bert-base-chinese 做微调本质上是让一个已经懂中文语法的通用预训练模型在约 10 万条带正负标签的微博语料上继续训练几个 epoch让它从“懂语言”过渡到“懂情绪”。这个方向的价值在于给第一次接触中文情感分析的人一条完整可复现的路径也给做舆情监控、内容审核、评论运营的团队一个可直接换数据的基线。整条链路包括环境配置、数据清洗、BERT 标准的 tokenization、模型微调、模型部署与效果展示我按这条链路逐个环节展开。2. 为什么是 WeiboSenti100k 和 bert-base-chinese数据与模型的双向适配2.1 WeiboSenti100k 数据画像三个影响训练结果的特点我拿到任何一份情感数据集不管它是微博还是商品评论第一件事永远是打开看分布而不是急着跑模型。WeiboSenti100k 是公开的中文微博情感二分类数据集100k 指的是约 10 万条文本标签是人工标注的正负二分类。先用一段最小检查代码把数据看明白import pandas as pd df pd.read_csv(weibo_senti_100k.csv) print(样本数:, df.shape[0]) print(标签分布:, df[label].value_counts().to_dict()) df[text_len] df[text].astype(str).str.len() print(文本长度统计:, df[text_len].describe())这段代码用 pandas 读取 CSV把样本数、标签分布、文本长度分布一次性打印出来。标签分布决定后面要不要做类别平衡长度分布决定 tokenizer 的 max_length 设多少这两件事都直接影响训练效果。我看完数据之后发现微博文本有三个明显特征必须处理。第一个是短。大多数微博在几十个字的量级超过 128 个 token 的是少数这意味着 tokenizer 的 max_length 往 128 设就够用不用像长文档分类那样拉到 512省显存也省时间。第二个是噪声多。URL、用户、话题标签、表情符号混在一起如果不在清洗阶段做过滤BERT 的注意力机制会把权重分散到这些和情感无关的位置上。第三个是情绪表达含蓄。反讽、反问、调侃在微博里非常常见词表匹配那套完全应付不来这也是为什么必须用预训练模型而不是词频方法的原因。这三个特征直接决定了后面的代码怎么写短文本决定序列长度噪声决定清洗函数含蓄表达决定必须做全参数微调而不是只训练一个分类头。如果你打算把这份数据换成自己的业务数据下载下来之后先把同样这三样看清楚样本总量、正负比例、平均长度。这三个数字能帮你提前判断是按原样训练还是需要做数据增强或类别平衡。2.2 bert-base-chinese 为什么是微调落地最稳的基座选型时我对比过几类方案。第一类是 word2vec 或 fastText 词向量加 LSTM实现简单但上下文表征能力弱遇到反讽基本无能为力。第二类是通用的多语言 BERT效果比词向量好但词表切分不如纯中文模型细腻。第三类就是 bert-base-chinese它是 Google 官方发布的简体中文 BERT 预训练模型字级切分不需要 jieba 分词从源头上避免了分词错误向后续任务传播。字级切分的意思是模型把每个汉字当作一个 token遇到一个没见过的网络新词也能通过字的组合推断出大致含义这对微博这种新词频出的场景非常关键。在中文情感分析这个任务上bert-base-chinese 不一定是最强模型但一定是最稳定的选择。transformers 库对它的支持非常完整加载权重、调用 tokenizer、保存模型全是标准 API社区里关于它的经验帖也最多遇到问题不愁搜不到解决方案。模型参数量在 1 亿出头单张 6GB 显存就能跑起来对团队硬件门槛很低。如果项目上线后想换更大的中文预训练模型代码基本不用动只换模型名就行。做这类中文任务我一般会把 bert-base-chinese 当作默认基座不是因为它的指标最高而是因为它能让我把精力放在数据和业务上而不是折腾环境。2.3 全参数微调 vs 冻结训练10 万条数据足够做什么这里必须把“微调”这个概念讲透。BERT 预训练阶段通过 masked language model 学会了词和词之间的共现关系但它不知道“正面”“负面”是什么概念。情感分类任务要求模型把每个 token 的语义向量压缩成一个二分类判断常规做法是在 BERT 顶部接一个分类头用 [CLS] token 的输出向量做全连接映射到 2 个 logits。常见误区是只训练分类头、把 BERT 主体冻结。冻结训练在数据量极少、比如只有几千条时有防过拟合的作用但在 WeiboSenti100k 这种 10 万条量级的数据下全参数微调效果明显更好。全参数微调让每一层注意力权重都参与梯度更新模型会逐渐把注意力集中在和情感强相关的字词上“喜欢”“太棒了”和“讨厌”“垃圾”会激活不同的注意力头。这和近期大家讨论的 LoRA 微调是两个路线LoRA 通过低秩矩阵注入减少可训练参数量降低训练成本这里做的是标准的全参数微调虽然费一点显存但在 10 万条数据规模下能把模型潜力压得更透。如果你后续要在更大的中文大模型微调任务上做实验这个先看数据、再定基座、最后选微调方式的思路是通用的。3. 环境配置与数据预处理把微博文本整理成 BERT 能读的格式3.1 环境配置torch、transformers、datasets 的版本搭配环境问题通常是新手卡住的第一关。我常用的组合是 Python 3.9、PyTorch 2.1、transformers 4.36、datasets 2.16这套组合在 CUDA 11.8 的 GPU 机器上能稳定跑通。安装命令如下conda create -n sentiment python3.9 conda activate sentiment pip install torch2.1.0 --index-url https://download.pytorch.org/whl/cu118 pip install transformers4.36.2 datasets2.16.1 scikit-learn pandas注意 torch 的安装参数指定 CUDA 11.8 的 wheel 源确保 torch 自带配套的 CUDA 算子。transformers 4.36 的 Trainer API 支持 fp16、梯度累积、学习率调度这些训练参数足够覆盖这个任务。如果之后你想在这条代码上接 LoRA 微调可以在后面加装 peft 库它和 transformers 的 Trainer 是兼容的改几行配置就能切换。安装完后建议顺手设置一个环境变量把预训练模型的下载缓存挪到大磁盘目录。HuggingFace 的缓存默认放在 home 目录下系统盘小的机器很容易被几个模型文件塞满之后 from_pretrained 会报一个不明所以的 OSError。命令是 export TRANSFORMERS_CACHE/你的磁盘路径写进 .bashrc 或 .zshrc 里避免每次重设。这条经验是我帮同事排查过两次之后养成的习惯磁盘写满导致的报错很容易被误判成环境损坏。3.2 数据清洗URL、用户、话题标签和表情符号怎么处理清洗这一步我一般写成一个独立函数处理四类噪声URL、用户、话题标签、多余空白。表情符号特意留着不删笑脸和哭脸本身就是情感信号删掉等于丢特征。import re def clean_weibo_text(text: str) - str: text re.sub(rhttp\S, url , text) # URL 替换为占位符 text re.sub(r\S, user , text) # 用户 替换为占位符 text re.sub(r#(.?)#, r\1, text) # 话题标签去掉#保留正文 text re.sub(r\s, , text).strip() # 压缩连续空白 return text df[clean_text] df[text].astype(str).apply(clean_weibo_text)这个函数的执行顺序是固定的。先处理 URL 和 用户把它们替换成占位符而不是直接删除目的是保持句子的结构完整让 BERT 还能理解文本的语法骨架。话题标签的井号去掉保留标签正文因为“#心疼自己#”里的“心疼”有明确的情感倾向。最后压缩空白防止 tokenizer 把多个空格当成奇怪的 token。正则替换的顺序有讲究先替换 URL 再替换 用户否则 符号和 URL 里的斜杠会互相干扰导致两个正则表达式都匹配不干净。关于表情符号有一类特殊的处理方式当 BERT 的词表里没有某个表情时tokenizer 会输出 [UNK]。遇到这种情况我建议把常见表情做一次映射比如将 映射为文本“哭”将 映射为“笑哭”让模型能学到明确的语义而不是丢给 [UNK] 让注意力机制猜测。这一步不是必须的但对微博数据有实际提升我实测下来准确率能涨一两个点。3.3 tokenizer 与 Dataset 封装max_length 设多少padding 用哪种数据清洗完下一步是把文本转成 BERT 能吃的 input_ids 和 attention_mask。这里用 AutoTokenizer 加载 bert-base-chinese 的字典按批次做编码from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) def encode_fn(texts, labels): enc tokenizer( texts, truncationTrue, paddingTrue, max_length128, return_tensorspt, ) return { input_ids: enc[input_ids], attention_mask: enc[attention_mask], labels: labels, }三个关键参数解释一下。max_length 取 128对应前面数据画像里微博文本偏短的事实超过 128 的部分被截断极大省显存。truncationTrue 保证长文本不报错。paddingTrue 是动态 padding每个 batch 内填充到当前 batch 的最长长度而不是固定填充到 128在文本长短不一的微博数据上能省掉不少无效计算。如果你用的是 datasets 库的 map 函数记得加上 batchedTrue否则 map 会逐条调用 tokenizer动态 padding 就失效了。封装成 Dataset 之后还要做训练集和验证集的划分。常见做法是用 sklearn 的 train_test_split固定 random_state保证每次跑出来的数据划分一致这是复现训练结果的前提。划完以后打印一下两边的标签分布确认和原始分布基本一致再进训练环节。4. 模型微调实战用 Trainer 跑通 bert-base-chinese 训练4.1 最小可跑的训练脚本模型和数据都准备好了直接用 transformers 的 Trainer API 做微调。我不建议新手一开始就手写训练循环Trainer 内部已经处理好了梯度累积、学习率预热、best checkpoint 保存这些细节先把训练跑通再回去看源码反而更有收获。from transformers import AutoModelForSequenceClassification, Trainer, TrainingArguments model AutoModelForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2, ) training_args TrainingArguments( output_dir./results, num_train_epochs3, per_device_train_batch_size32, per_device_eval_batch_size64, learning_rate2e-5, weight_decay0.01, logging_dir./logs, logging_steps500, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modeleval_loss, fp16True, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, ) trainer.train()这段代码的核心在 TrainingArguments 的几组参数。learning_rate2e-5 是 BERT 全参数微调的经典取值比这个大一个数量级容易在第一个 epoch 就把预训练权重冲坏。weight_decay0.01 对除 bias 和 LayerNorm 之外的参数做 L2 正则能有效减缓过拟合。evaluation_strategy 和 save_strategy 都设为 epoch每个 epoch 结束同时做评估和保存。load_best_model_at_endTrue 会在训练结束时自动回载验证集上最好的 checkpoint这个是给后续推理用的别漏。fp16 建议在支持半精度计算的 GPU 上打开显存占用大约能降三分之一速度提升近一倍。如果你的机器是 CPU 训练这个参数必须改成 False否则会报精度类型不支持的错。训练过程中日志里会有 train_loss 和 eval_losseval_loss 如果连续两个 epoch 不再下降就该考虑早停或调低学习率了。4.2 别只盯 loss把准确率、精确率、召回率一起打出来很多第一次做微调的人习惯只盯着训练 loss 看这是不够的。交叉熵 loss 下降不代表模型在正负两个类上都有能力。为了看清真实情况我在 Trainer 里挂一个自定义评估函数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) acc accuracy_score(labels, preds) p, r, f1, _ precision_recall_fscore_support( labels, preds, averagebinary, pos_label1 ) return { accuracy: acc, precision: p, recall: r, f1: f1, } trainer Trainer( ..., compute_metricscompute_metrics, )这里我把正类 label1 的精确率、召回率和 F1 单独拎出来。原因是二分类里如果负样本居多模型可能靠全预测负类拿到一个还不错的 accuracy但放到真实舆情场景中这个模型对正面微博完全没有识别能力。F1 是精确率和召回率的调和平均能同时惩罚漏报和误报是情感分类任务里比 accuracy 更值得关注的指标。验证集上的这四个数字每个 epoch 结束后都会打印哪一版 checkpoint 更可靠一眼就能看出来。如果你后续要对比不同预训练模型的微调效果我建议把这几项指标连同数据规模、训练耗时一起记在一个表格里。做技术选型时光看准确率容易踩坑把精确率和召回率放在一起看才能知道模型在业务里到底好不好用。4.3 显存不够怎么调batch size、梯度累积和 fp16 的组合如果你的 GPU 只有 6GB 或 8GBbatch size 32 大概率撑不住。我的调整顺序是先降 batch size 到 16 或 8再开梯度累积弥补小 batch 带来的梯度噪声最后开 fp16。在 TrainingArguments 里加一行 gradient_accumulation_steps2意思是模型每处理 2 个 batch 才更新一次参数。batch size 16 配合梯度累积步数 2等效更新强度接近 batch size 32但单次前向传播的显存占用只有一半。注意梯度累积会增加一些显存暂存开销但整体远小于直接开大 batch。这个调参思路和目前大家在大模型微调、包括在 LLaMA-Factory 这类工具上压显存的做法是相通的先降 batch 再累积梯度训练速度慢一点但不至于中断。下表是我在 8GB 显存机器上验证可用的几组搭配可以参考后按你的显卡调整显卡显存batch sizegradient_accumulation_stepsfp166GB84开8GB162开12GB321开16GB322开这个表不是硬性规定max_length 如果从 128 调到 256显存占用会涨一截batch size 可能要再往下降。训练慢一点没关系优先保住不 OOM模型能完整跑完几个 epoch 比什么都重要。5. 避坑手册bert-base-chinese 微调查错与常见问题的 5 条记录5.1 现象训练 loss 不降反升我第一次跑这个项目时训练 loss 不但没降反而从 0.7 涨到 0.95这是典型的不收敛症状。原因排查后发现最常见的原因就是学习率过大。BERT 微调的学习率一般就在 2e-5 到 5e-5 之间如果沿用其他任务里常用的 1e-4AdamW 的更新步长太大预训练权重会被快速覆盖模型丢掉原有的语言能力。另一个常见原因是数据没有预先 shuffle模型每一轮都按固定顺序见到同样的正负样本序列优化过程形成震荡loss 很难平稳下降。解决方法是先用默认的 2e-5 重跑同时把梯度累积步数补上让优化步更稳。如果还不行给学习率调度器加上 warmup前 10% 的训练步数线性升温让模型从预训练权重平滑过渡到任务权重。这个组合能解决九成以上的 loss 不降问题。5.2 现象显存溢出 OOM运行到第二个 training step 时报错 RuntimeError: CUDA out of memory。原因是 batch size 和 max_length 组合超出了显存上限。6GB 显存开 batch size 32 加 max_length 128激活值直接吃满。还有一个隐形杀手是固定 padding。如果用 paddingmax_length 把每条文本都填到 128等于按最长的计算量跑短文本也占满序列长度显存和计算都白费。解决方法是把 batch size 降到 16padding 改成动态打开 fp16。这三步做完还爆就把 max_length 降到 100。微博文本长度超过 100 的样本很少截断后对效果影响可以忽略。实在不行再考虑 gradient checkpointing用时间换空间但这个选项会拖慢训练速度能不用就不用。5.3 现象训练 loss 很低但验证集准确率一直在 50%训练结束时 loss 已经到 0.15验证集准确率却还在 50% 附近像是在抛硬币。这类情况的根因往往不是数据泄露而是标签类型错了。有一次我发现数据集封装时 label 是 float 类型Trainer 把它当成了回归任务模型输出从一个分类 logits 向量变成一个标量交叉熵计算和验证逻辑完全错位。模型在训练集上把 float label 拟合得很好但验证时 argmax 一个标量毫无意义准确率自然卡在 50%。解决办法是训练前检查数据集特征类型打印 train_dataset.features确认 labels 是 Value(dtypeint64)。如果不是用 cast_column 强制转换然后重新封装数据集。训练前花一分钟检查数据类型能省掉半天排查时间。5.4 现象推理阶段预测结果全是同一个类别训练完保存 checkpoint加载模型跑推理输入什么文本都预测成“负面”或者都预测成“正面”。原因常见有两个。一个是 num_labels 设置错误。如果 from_pretrained 时 num_labels1模型输出的是单 logit相当于在做回归而不是分类argmax 恒等于 0预测就全部落在同一个类别上。另一个是训练集类别严重偏斜模型学到的最优策略是全部预测多数类。WeiboSenti100k 本身正负接近 1:1但如果你换上自己的行业数据首先要看类别比例。解决方法是训练前用 value_counts 检查分布分布偏了就做加权采样或类别权重。推理脚本里确认 num_labels2并且用 softmax 归一化后再取概率最大的类别作为预测结果。这两个位置是整条链路上最容易出问题的环节检查一遍总能找到原因。5.5 现象GPU 利用率低训练速度比预期慢很多nvidia-smi 里 GPU-Util 只有 20% 左右训练一个 epoch 要将近一小时。原因往往是数据加载管线卡了瓶颈。清洗函数如果用 Python 正则逐行处理并且清洗过程放在训练循环里CPU 处理不过来GPU 只能空转等数据。另一个常见原因是 DataLoader 的 num_workers 设成 0数据从主进程一张张地送往 GPU速度完全提不起来。解决方法是把清洗提前到训练前一次性处理完存成新文件训练时只做 tokenizer 编码。在数据集构造阶段给 DataLoader 指定 num_workers4 或 8让多个子进程预取数据GPU 就不会再空等。这个小优化往往能把训练速度提升两到三倍属于性价比最高的一处调整。6. 预测与效果验证用小批量推理检查模型的真实边界训练和评估都通过后最后一步的关键是把模型从 Trainer 里拿出来以部署时的状态做推理。我习惯先准备 10 条没进过训练集的微博覆盖正面、负面、中性、反讽几种情况跑一次小批量推理import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification model_path ./results/checkpoint-3000 tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained(model_path) model.eval() samples [ 今天天气真好出去走了一圈心情特别舒畅, 这个外卖等了一个半小时还没到差评, 电影特效不错但剧情拖沓得让人想睡觉, 笑了这种方案居然也能被通过, ] inputs tokenizer( samples, paddingTrue, truncationTrue, max_length128, return_tensorspt, ) with torch.no_grad(): logits model(**inputs).logits probs torch.softmax(logits, dim-1) preds torch.argmax(logits, dim-1) print(probs)推理代码里有几个细节值得提醒。model.eval() 必须先于 torch.no_grad() 执行因为 eval 模式决定 dropout 是否生效no_grad 决定是否构建计算图两者负责的事情不一样漏掉任何一个都可能导致部署结果和训练时不一致。softmax 得到的概率是模型对“正面”的置信度它可以直接用于业务判断。我自己的习惯是把 0.4 到 0.6 这个区间的样本单独导出来人工复核因为这部分样本往往包含反讽、双重否定和语境依赖的隐喻模型在这个区间的判断只是统计意义上的把握不算可靠结论。把这条人工复核规则写进部署逻辑比直接信任预测结果上线更稳妥。希望这套从数据到部署的思路能帮到你。跑通之后把清洗规则和超参数换成你自己的数据再试一次你会发现剩下的问题基本都是数据侧的模型反而不用瞎折腾。本文还有配套的精品资源点击获取
返回列表