Bert微调实现微博情感分析:从数据清洗到分类头训练

Bert微调实现微博情感分析:从数据清洗到分类头训练 简介基于Bert实现情感分析和文本分类任务的Python源码包面向计算机科学、人工智能、数据科学等专业的在校学生与开发者尤其适合作为毕业设计、课程设计或期末大作业的参考项目。该资源共10个文件包括5个Python脚本、4个文本说明文件及1个CSV数据集压缩包大小30.97MB。其中Python脚本覆盖数据预处理、模型训练与推理流程文本文件包含项目说明与运行指导CSV数据集提供可用的微博情感语料有助于快速跑通全流程。项目代码经功能验证稳定可靠并已获得指导导师认可评审分数达98分同时具备良好拓展空间读者可基于现有框架进行二次开发和实验对比。目前已有518人浏览学习适合入门自然语言处理并在实际任务中实践Bert模型。1. 从一条微博到情感标签Bert微调在文本分类上的落地路径手头有 10 万条带情感标签的微博评论想直接拿 Bert 做文本分类很多人不是倒在模型结构上而是倒在数据格式不统一、标签读进来是字符串、训练完权重不知道落盘在哪。这套基于 Bert 实现情感分析和文本分类的工程把数据转换、微调训练和运行入口拆成了三个独立模块配合 weibo_senti_100k.csv 中文数据集能在本地显卡上把原始文本到正负情感标签的完整链路复现出来。它适合正在做课程设计或期末大作业的学生需要一份能讲清楚原理的 python 源码作为起点也适合想快速验证微调流程的工程师借这个工程看清数据处理和训练脚本的边界。我按 data_to_csv、csv_reader、train、main 四个文件的顺序把全流程重走了一遍数据侧先讲。2. 微博评论数据集清洗与特征列构造data_to_csv 与 csv_reader 的数据流设计2.1 读懂 weibo_senti_100k 的列结构与标签含义weibo_senti_100k.csv 是中文情感分析里常用的公开文本语料来源于微博短文本规模约 10 万条。从命名上能看出它分成正负两类对应情感二分类的极性标签。项目根目录下保留了 data/triple.txt 和 data/summary.txt 两个辅助文件前者通常是带噪声的原始抓取片段后者是统计摘要它们不是训练主料而是给数据转换脚本做校验或扩展用的。真正参与训练的输入应该是 csv 文件里的 label 和 text 两个核心字段。提示拿到任何新数据集先统计标签分布再谈训练。经验上情感数据集如果正负比例偏差超过 4:1直接微调 Bert 容易让验证集指标虚高换到线下真实场景里会明显回落。我对字段的重构习惯是label 取 int 类型0 表示负向1 表示正向text 保留原始文本不去除表情符号因为微博语料里表情和标点本身就是强情感信号。分词工作交给 Bert 自己的 tokenizer不要在这一步做 jieba 分词否则会把 Bert 预训练词表的语义切碎。这个选择和 Bert 的 WordPiece 机制有关放到第 3 章细说。2.2 data_to_csv.py把零散文本转成标准训练集data_to_csv.py 的角色是把目录下分散的文本文件统一成单张表。项目里它最可能的输入是 data 目录下的 triple.txt 这类文本输出是 weibo_senti_100k.csv。常见做法是用 tab 或逗号分隔一行一条样本第一列是标签第二列是文本。核心转换逻辑通常长这样import pandas as pd def convert_txt_to_csv(src_path: str, dst_path: str, sep: str \t) - None: rows [] with open(src_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue parts line.split(sep) if len(parts) 2: continue label int(parts[0]) text sep.join(parts[1:]) # 文本里可能包含分隔符 rows.append({label: label, text: text}) df pd.DataFrame(rows, columns[label, text]) df.to_csv(dst_path, indexFalse, encodingutf-8)这段代码有三个容易被忽略的点。第一parts[1:]用 join 重新拼起来是因为微博文本里可能自带 tab 或逗号直接取parts[1]会把后面的内容丢掉第二label 提前转成 int避免后续在 DataFrame 里反复做字符串到数值的转换第三encodingutf-8必须显式指定Windows 默认编码会读乱中文。转换前后的字段可以对照这张表。阶段文件名字段构成用途原始data/triple.txt标签与文本以分隔符混杂便于人工抽查中间weibo_senti_100k.csvlabel, text 两列直接喂给训练脚本如果 csv 已经存在我一般会把这段脚本当作格式修复器来用读到报错或列数不对时回到原始文本按上面的逻辑重建一次保证 csv_reader 永远面对同一套 schema。2.3 csv_reader.py封装数据读取与训练/验证切分csv_reader.py 是训练脚本和 csv 文件之间的唯一入口。它不关心文件从哪来只负责把 label 和 text 装成模型能迭代的形式。我通常会在数据读取处做三件事列名校验、标签数值确认、以及按比例切出验证集。切分时注意用分层抽样避免某一个类别在验证集里消失。import pandas as pd from sklearn.model_selection import train_test_split def read_split_dataset(csv_path: str, val_ratio: float 0.1, seed: int 42): df pd.read_csv(csv_path, encodingutf-8) assert {label, text}.issubset(df.columns), csv 必须包含 label 和 text 列 df[label] df[label].astype(int) train_df, val_df train_test_split( df, test_sizeval_ratio, stratifydf[label], random_stateseed ) return train_df, val_df这里的stratifydf[label]是按标签比例做分层切分的关键参数。当前数据集正负样本较均衡时随机切分偶尔也能用但后续替换成多分类或多标签语料时分层切分几乎就是必需品。seed 固定下来实验才可复现这是训练脚本里最容易被忽略的工程细节。如果不想依赖 sklearn也可以手动按 label 分组后取下标但分层逻辑会多写十几行没必要。提示验证集比例建议设在 8%15%。情感分析任务文本短、噪声大验证集太小会让早停判断剧烈抖动太大则浪费微调样本。3. BertForSequenceClassification 的分类头原理与 train.py 微调实现3.1 分类头的构成为什么直接用现成模型而不手写全连接层Bert 微调做文本分类核心是 Hugging Face transformers 库里的BertForSequenceClassification。这个类由两部分组成底层的 BertModel 和顶层的分类头。分类头本质是一个Linear(hidden_size, num_labels)输入的hidden_size在 bert-base-chinese 里是 768num_labels在二分类场景里是 2。它的输入不是整条序列的每个 token 向量而是取[CLS]位置经过池化后的表示。from transformers import BertForSequenceClassification, BertConfig config BertConfig.from_pretrained( bert-base-chinese, num_labels2, hidden_dropout_prob0.1, attention_probs_dropout_prob0.1, ) model BertForSequenceClassification.from_pretrained( bert-base-chinese, configconfig )为什么不手写一个全连接层接在 BertModel 后面因为BertForSequenceClassification内部还包含了 dropout、分类头权重初始化以及 loss 计算逻辑。你用裸的 BertModel 加nn.Linear也能跑但需要自己处理 label 的 dtype、忽略 padding 位置的 loss以及训练和推理时 dropout 状态的切换。这些细节在工程里出了问题很难用肉眼发现。直接复用这个类的代价是把输出维度定死在了num_labels这是一种值得接受的设计取舍。3.1.1 冻结参数的边界微调时要不要冻结 Bert 底层参数取决于数据量和算力。10 万条微博语料对 Bert 来说属于中等规模我通常不冻结任何层让底层词向量和注意力参数都参与更新。只有两种情况我会考虑冻结一是只有几千条标注数据冻结底部 6 层可以显著降低过拟合二是显存不够时gradient_checkpointing比冻结参数更值得优先尝试代价是训练时间大约上涨 20%。3.2 Tokenizer中文分词、attention_mask 与 max_len 控制中文进入 Bert 之前必须先过BertTokenizer。它用 WordPiece 把句子拆成子词单元这部手机很好用会被切成[[CLS], 这, 部, 手, 机, 很, 好, 用, [SEP]]再映射成 id。注意这里没有 jieba 参与因为预训练阶段就是这么切的换分词器等于把模型扔回未训练状态。from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) def encode_example(text: str, max_len: int 128): encoded tokenizer( text, max_lengthmax_len, paddingmax_length, # 所有样本统一长度便于批处理 truncationTrue, # 超长文本截断而不是报错 return_tensorspt, ) return encoded[input_ids], encoded[attention_mask]paddingmax_length让同 batch 的样本形状完全一致truncationTrue处理超长文本二者组合是文本分类任务的标准配置。max_len 的选择直接影响准确率和资源占用微博评论中位数约 3060 字max_len128 基本够用如果换成商品评论或新闻长文本我会把 max_len 放到 256但 batch_size 要相应减半。attention_mask 的作用是让模型知道哪些位置是真实 token、哪些是 padding训练时 loss 不会把 padding 位置的误差算进去。3.3 train.py 训练循环warmup、学习率与梯度累积train.py 承担的是整个项目的核心算力逻辑。情感分析微调的参数取值有相对固定的经验区间先把一份能跑通的配置列出来再解释每个参数调整时应该观察什么指标。参数推荐值说明learning_rate2e-5大于 5e-5 容易训练震荡num_train_epochs310 万条数据 3 轮足够收敛per_device_train_batch_size1632显存 6G 用 1612G 可上 32max_seq_length128超过 256 后收益明显递减warmup_ratio0.1前 10% 步数线性预热weight_decay0.01对非 bias 和 LayerNorm 参数生效训练循环如果用 transformers 的Trainer上面的参数可以直接传进TrainingArguments。但 train.py 里更常见的是手写循环方便在每一步后面打印真实 loss 和梯度范数。下面这段是简化后的训练循环骨架from transformers import AdamW, get_linear_schedule_with_warmup def train_one_epoch(model, dataloader, optimizer, scheduler, device): model.train() total_loss 0.0 for step, batch in enumerate(dataloader): input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) outputs model( input_idsinput_ids, attention_maskattention_mask, labelslabels, ) loss outputs.loss loss.backward() # 梯度累积每 4 步做一次参数更新 if (step 1) % 4 0: torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step() optimizer.zero_grad() total_loss loss.item() return total_loss / len(dataloader)这个循环里有三个值得展开的细节。第一梯度累积让每 4 步更新一次参数等效于把 batch_size 翻 4 倍显存不变的情况下能模拟更大的 batch代价是训练时间变长但 Bert 里只有 LayerNorm不受累积步数影响。第二clip_grad_norm_(max_norm1.0)把梯度范数限制在 1.0 以内这是微调 Bert 的默认安全操作能避免个别难样本把参数一步推远。第三scheduler.step()必须在optimizer.step()之后调用线性调度的步数由总步数和 warmup_ratio 共同决定。4. main.py 编排下的端到端训练依赖、显存与 loss 曲线的工程排错4.1 requirements.txt 依赖视角transformers 与 torch 的版本关系项目根目录的 requirements.txt 锁定了运行环境文档.txt 里通常也会强调先装依赖再跑训练。这类工程的依赖集中在 pandas、scikit-learn、torch、transformers 上。版本之间最容易出问题的是 transformers 和 torch 的匹配老版本 transformers 加载新格式权重时会提示重映射新版则对 Python 版本有下限要求。我建议严格按 requirements.txt 安装因为手动补包最容易漏掉tokenizers这个隐式依赖。pip install -r requirements.txt python -c import torch, transformers; print(torch.__version__, transformers.__version__)第二条命令在安装后立即执行作用是验证核心库能否在同一个进程里正常导入。如果 transformers 报缺 tokenizers 或版本不匹配先升级 tokenizers 再升级 transformers不要反过来。GPU 环境下还要确认torch.cuda.is_available()返回 True否则代码能跑但会退化成 CPU 训练10 万条数据在纯 CPU 上跑 3 轮可能要十几个小时这个时间成本在课程设计答辩前尤其致命。4.2 main.py 串联全流程参数解析、训练入口与模型保存main.py 是这个工程的调度层。它不写模型逻辑只做三件事解析命令行参数、按参数调用数据模块和 train 模块、把训练产物落到指定路径。用 argparse 组织参数的好处是跑实验时不用改代码不同 batch 和 lr 的组合直接用命令切换即可。import argparse def parse_args(): parser argparse.ArgumentParser(descriptionBert sentiment analysis) parser.add_argument(--data, defaultweibo_senti_100k.csv) parser.add_argument(--epochs, typeint, default3) parser.add_argument(--batch_size, typeint, default32) parser.add_argument(--lr, typefloat, default2e-5) parser.add_argument(--max_len, typeint, default128) parser.add_argument(--output_dir, defaultcheckpoints) return parser.parse_args() if __name__ __main__: args parse_args() train_df, val_df read_split_dataset(args.data) # 构造 dataloader 并调用 train_one_epoch # 每轮保存 model.save_pretrained(f{args.output_dir}/epoch_{epoch})这种结构的可维护性在于数据读取、训练逻辑、参数解析三者没有互相 import 的环。后续要换数据集或换模型只需要替换 main.py 里对应的函数调用。参数说明上--epochs控制训练轮数课程设计中先把 2 轮跑通验证流程再把 3 轮作为最终结果--output_dir每轮单独建目录避免中途中断后权重全部覆盖找不到历史版本。把 lr 和 batch 写进目录名比如checkpoints_lr2e5_bs32比任何实验记录表都可靠。4.3 训练中的三个高频问题与排查手段微调 Bert 的报错高度集中在显存和序列长度两个维度上很少有人在这里遇到模型结构本身的 bug。把情感分析的场景换成任意文本分类任务以下这些问题同样成立。结合 train.py 训练循环和 main.py 的参数入口我按出现频率整理成一张排查表表里的处理方式都是先在参数层解决不轻易改动模型结构。现象直接原因处理方式CUDA out of memorybatch 太大或 max_len 过长batch 减半或 max_len 从 256 降到 128loss 稳定在 0.69 不降类别不平衡或 lr 过大检查样本比例lr 降为 1e-5 重跑验证集准确率波动超过 5%验证集太小或 seed 不固定增大 val_ratio 到 0.15固定 seed训练很快但准确率全为多数类数据切分未分层改用stratifylabel做切分4.3.1 OOM 时的最小改动策略显存不足是最常见的硬性约束。优先动 max_len 而不是动 batch把 128 降到 96对微博短文本的指标几乎无损但显存占用显著下降。如果必须保留 128再动 batch32 改 16梯度累积步数从 4 改成 8等效 batch 仍然能维持。这两步都无效时最后考虑model.gradient_checkpointing_enable()这是以时间换显存的正规做法。4.3.2 loss 曲线的正常形态情感分析微调的 loss 在第 1 轮内通常会从 0.7 附近快速回落到 0.3 以下第 2 轮斜率变平第 3 轮基本收敛。如果第 1 轮结束 loss 还在 0.5 以上优先怀疑 lr 过大或数据标签反了。我习惯把每步的 loss 写入列表训练结束后画一条曲线曲线呈阶梯状下降是正常的因为梯度累积让 loss 每 4 步才更新一次参数如果曲线出现周期性尖峰说明某些 batch 里混入了异常长文本或错误标签需要回头检查预处理环节。5. 单任务分类的边界扩展句对输入、多标签改造与 ONNX 导出5.1 用句对输入把推理流程封装成可复用函数训练完成后真正要上线或答辩演示的是一个输入文本、输出情感标签的推理函数而不是脱离生产的训练脚本。常见做法是让 tokenizer 和 model 只暴露 predict 这一个接口既支持命令行 demo也方便单元测试单独跑通。def predict(text: str, model, tokenizer, device, max_len128): encoded tokenizer(text, max_lengthmax_len, paddingmax_length, truncationTrue, return_tensorspt) encoded {k: v.to(device) for k, v in encoded.items()} model.eval() # 关闭 dropout保证推理结果稳定 with torch.no_grad(): logits model(**encoded).logits pred int(logits.argmax(dim-1).item()) return 正向 if pred 1 else 负向model.eval()和torch.no_grad()缺一不可前者把 dropout 关掉后者不构建计算图。对于单条短文本的推理断开计算图能省掉一次反向传播的内存开销。如果要从命令行批量体验可以在 main.py 里加一个--predict分支读入文件逐行预测并把结果写回带标签的列这样训练和推理共用同一套编码逻辑一致性更好。5.2 从二分类到多标签的改动清单把情感二分类扩成多标签时改动点集中在三处。第一处是num_labels从 2 改成目标类别数比如高兴、愤怒、悲伤、中性四分类第二处是标签解码逻辑如果多个标签可以同时命中就要把argmax换成sigmoid加阈值第三处是 lossBertForSequenceClassification在 labels 维度大于 1 时会自动切换成多标签 loss前提是标签填成 one-hot 矩阵。模型结构不用动train.py 和预处理模块可以原样复用。5.3 模型导出用 ONNX 做轻量级推理如果想把模型接进 Web 服务PyTorch 的动态图推理在服务端不是最优选择。用torch.onnx.export把微调后的 Bert 导出成 ONNX 静态图推理延迟通常能下降 30% 以上且服务端不需要装完整 torch 依赖。dummy_input { input_ids: torch.ones(1, 128, dtypetorch.long), attention_mask: torch.ones(1, 128, dtypetorch.long), } torch.onnx.export( model, (dummy_input[input_ids], dummy_input[attention_mask]), bert_senta.onnx, opset_version13, input_names[input_ids, attention_mask], output_names[logits], dynamic_axes{input_ids: {0: batch}, attention_mask: {0: batch}}, )dynamic_axes允许推理时 batch 大小不固定这是服务化部署的常规设定句长固定为 128 是刻意选择句长动态化会让推理引擎做额外内存对齐收益有限。导出完成后用 onnxruntime 加载和 PyTorch 原输出对比误差在 1e-4 以内就可以放心切换。多标签或多情感类别的扩展也可以在这条导出链路里进行模型结构不变变的只是分类头的输出维度。本文还有配套的精品资源点击获取