基于Bert的情感分析与文本分类实战:从环境搭建到模型部署 简介这是一份基于Bert的中文情感分析与文本分类实战资源适合计算机、人工智能、数据科学等专业学生用于入门进阶、课程设计或毕业设计。压缩包共43个文件以Python脚本为主辅以JSON配置、XML数据、PNG可视化图、CSV表格及Markdown说明文档整体42.14MB目录按data、model、train、crawler、GUI、processing、sentiment、topic等模块划分从数据爬取、清洗、建模到可视化展示形成完整链路。目前已有398人学习下载。内容包含可运行的情感分析器与文本分类器、训练好的模型、数据集及详细项目说明并附带GUI界面与主题建模模块方便直观查看分类结果同时提供shell训练脚本可快速复现Bert微调流程。资源既适合在校生直接用作毕设演示也支持在此基础上二次开发是理解深度学习NLP任务的实用材料。1. 这个Bert项目zip值不值得下载情感分析与文本分类的真实收益先说一个我反复遇到的场景业务方丢来几万条客服工单要求自动判断用户情绪是愤怒、平静还是满意另一边商品评论要归类到物流、质量、售后等类别。传统做法是堆关键词和规则上线后准确率勉强到70%但新话术一多召回率掉得飞快。换到“基于Bert实现情感分析和文本分类任务python源码数据集项目说明”这套方案本质是利用预训练模型已经学会的语言知识在少量标注数据上做精调不需要从零训练网络就能把准确率和F1推到可用水平。这个zip的核心构成很清晰python源码负责数据预处理、模型训练、评估和推理数据集用来做精调项目说明文档解释环境配置、参数含义和注意事项。它适合两类人——一类是有标注数据、想快速把预训练模型落到业务里的开发者另一类是课程设计或者内部系统Demo需要一套完整可跑的参考实现。但要说句实在话Bert不是装上就灵。同样一份代码数据处理干不干净、学习率给得对不对跑出来的结果可能差10个点。这篇笔记按我自己的落地习惯拆解这套方案的完整链路从环境准备、数据格式、训练脚本到上线前的阈值策略把能复现的步骤和容易翻车的坑一次讲清楚。2. 跑通项目前先搭环境依赖清单、中文Bert权重与数据集格式核对拿到zip先别急着把代码跑起来先做三件事锁依赖版本、确认权重文件完整、核对数据格式。这三件事每件都是坑单独拎出来说。2.1 先把Python环境和依赖锁死transformers版本与pytorch的对应关系我见过太多人“pip install transformers”一把梭结果torch还是旧版import阶段就报错。Bert相关代码对torch和transformers的版本耦合很敏感最稳的做法是新建独立虚拟环境先把torch装好再装transformers。conda create -n bert_cls python3.9 -y conda activate bert_cls pip install torch2.0.1 pip install transformers4.35.0 datasets2.15.0 pip install tokenizers0.15.0 scikit-learn1.3.2 pip install pandas numpy tqdm为什么先装torch再装transformers因为transformers安装时会做依赖检查读取torch版本信息如果torch后装可能被解析成不兼容的版本组合。torch 2.0.1搭配transformers 4.35.0是我验证过比较稳的一套你按自己的显卡驱动微调即可。datasets库不是必装但如果zip里的数据是json格式想用load_dataset统一加载装上会省事。如果你的机器没有独立显卡也能跑通最小闭环只是慢。把batch_size调小到8先用几十条样本验证代码路径确认没问题再上GPU资源。CPU上跑完整数据集确实受罪但用来排错非常高效。import torch device torch.device(cuda if torch.cuda.is_available() else cpu) print(use, device)2.2 中文Bert权重选型本地目录优先于在线下载处理中文的情感分析和文本分类不能用英文预训练权重。英文分词器面对中文文本时经常把一个汉字切碎或识别成unkF1直接掉一大截。正确做法是使用中文预训练权重并把权重一次性下载到本地目录代码里用本地路径加载。权重目录需要核对三件套config.json是模型结构配置pytorch_model.bin是权重文件vocab.txt是词表。如果额外有tokenizer_config.json和special_tokens_map.json也一起保留。加载失败大多数情况是少文件先检查目录再怀疑代码。from transformers import BertTokenizer, BertForSequenceClassification model_dir /data/pretrained/bert-base-chinese tokenizer BertTokenizer.from_pretrained(model_dir) model BertForSequenceClassification.from_pretrained( model_dir, num_labels3 ) print(model.config.num_hidden_layers)from_pretrained既接受本地路径也接受一个模型标识字符串后者会自动尝试在线拉取。内网环境或网络不稳定时在线拉取很容易断掉所以我的习惯是先把权重下到本地再改成路径加载。打印num_hidden_layers是为了确认权重真的加载进来了如果输出是12说明Bert base结构正常加载如果加载失败模型会走默认初始化后续训练loss半天降不下去。另外如果拿到的是TensorFlow版本权重直接加载会报错。常见做法是用转换脚本把ckpt转成pytorch格式转完再做一次加载验证看到模型结构正常输出再继续训练。2.3 数据集长什么样CSV两列结构与标签编码规则我自己的项目习惯是把训练数据统一整理成两列CSVtext是输入文本label是标注字符串。情感分析里label一般是正、负、中性三分类文本分类里label就是业务类别名称比如物流、质量、售后。textlabel快递比预期晚了两天差评负客服态度很好问题解决了正商品还行但包装有点旧中性import pandas as pd import json train_df pd.read_csv(data/train.csv, encodingutf-8) print(train_df[label].value_counts()) label2id {label: i for i, label in enumerate(sorted(train_df[label].unique()))} with open(label2id.json, w, encodingutf-8) as f: json.dump(label2id, f, ensure_asciiFalse, indent2) print(label2id)value_counts先看标签分布如果某一类样本特别少后面训练时要做类别权重处理。label2id这个映射必须持久化保存训练、验证、推理共用同一份避免下次启动程序重新排序导致预测结果串位。encodingutf-8必须写很多Excel导出的CSV是gbk编码不指定的话pandas读进来中文乱码后续模型表现为loss正常但F1一直上不去。数据清洗也有讲究。情感分析场景里不要做停用词过滤中文里“不是不行”“太不好”这类表达一旦去掉否定词语义直接反转。Bert自己能学会这些组合我们只需要清理URL、HTML标签这类无意义噪声。import re def clean_text(text): text re.sub(rhttp\S|www\.\S, , text) text re.sub(r[^], , text) text re.sub(r[ \t], , text) return text.strip() train_df[text] train_df[text].astype(str).apply(clean_text)数据划分建议固定随机种子做分层抽样from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( train_df[text].tolist(), train_df[label].tolist(), test_size0.1, stratifytrain_df[label].tolist(), random_state42 )stratify按原始标签比例抽样在不均衡数据下必须传random_state固定后多次运行划分结果一致这是后面能复现实验结果的前提。3. 最小训练闭环从文本预处理到情感分析模型出分的完整脚本环境、权重、数据三件套就绪后开始跑通最小训练闭环。把训练代码写成一个train.py先确保能出结果再逐步调参优化。3.1 用Tokenizer把文本变成input_ids、attention_mask与token_type_idsBert不能直接读字符串。输入侧要先切词、映射词表id同时告诉模型哪些位置是真实文本、哪些位置是padding补齐的。这些都是Tokenizer一次完成的。def encode_texts(texts, tokenizer, max_length128): encoded tokenizer( texts, truncationTrue, paddingmax_length, max_lengthmax_length, return_tensorspt, ) return encodedencoded里包含三个关键张量input_ids是词表下标attention_mask是有效位置标记padding位置为0token_type_ids在单句分类任务里基本全是0属于预留字段。truncationTrue表示超长截断paddingmax_length让同一batch内的样本对齐到相同长度否则DataLoader拼不成矩阵。max_length是最值得调的一个参数。中文几乎一个字对应一个token一句话评论128够用工单或长文本建议设256。设太长有两个副作用显存上涨、训练变慢而收益到256以后明显放缓。提示调max_length之前先统计训练集的文本长度分布。如果90%的样本在100个token以内设128足够没必要拉高。3.2 用TensorDataset和DataLoader组织批量数据编码完成后把数据包成PyTorch标准的数据集和数据加载器import torch from torch.utils.data import DataLoader, TensorDataset train_enc encode_texts(X_train, tokenizer) train_dataset TensorDataset( train_enc[input_ids], train_enc[attention_mask], torch.tensor([label2id[y] for y in y_train], dtypetorch.long), ) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue)TensorDataset把三个张量打包DataLoader按batch_size切分并打乱。shuffleTrue很关键如果数据按标签排序后不打乱连续几个batch都是同一类样本模型梯度会偏向这一类别训练曲线像锯齿。batch_size16是我在8GB显存显卡上比较稳的选择。显存不够报CUDA out of memory时先降到8不要先降max_length。batch_size越小梯度噪声越大必要时配合后面第5章的梯度累积方案。3.3 加载模型与配置训练超参情感分析三分类任务模型层面只需要指定num_labels3from transformers import BertForSequenceClassification, AdamW, get_linear_schedule_with_warmup model BertForSequenceClassification.from_pretrained( /data/pretrained/bert-base-chinese, num_labelslen(label2id), ).to(device) optimizer AdamW(model.parameters(), lr3e-5, weight_decay0.01) total_steps len(train_loader) * 3 scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(total_steps * 0.1), num_training_stepstotal_steps, )BertForSequenceClassification本身是“Bert主体分类头”的完整结构训练时直接传labels它会自动计算CrossEntropyLoss不需要手写。AdamW是Bert精调最常用的优化器weight_decay0.01是常规选择。参数常用值说明learning_rate2e-5 ~ 5e-5精调学习率必须小1e-4很容易震荡warmup_ratio0.1前10%的step学习率从0线性升到设定值weight_decay0.01对权重做L2正则缓解过拟合epochs3小数据量2-3个epoch足够多了会过拟合max_grad_norm1.0梯度裁剪防止梯度爆炸为什么Bert不能用CNN训练时常用的0.01学习率因为预训练权重已经在海量语料上收敛大学习率会一次性把学到的语义知识冲掉这也是很多新人跑Bert效果差的原因。3.4 训练循环与验证评估训练循环代码量不大但细节多from tqdm.auto import tqdm model.train() for epoch in range(3): loop tqdm(train_loader, descfepoch {epoch}) for step, batch in enumerate(loop): input_ids, attention_mask, labels [x.to(device) for x in batch] 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() optimizer.zero_grad() loop.set_postfix(lossloss.item())梯度裁剪放在backward之后、step之前防止异常梯度把参数带偏。scheduler.step必须在optimizer.step之后调用顺序反了学习率变化和参数更新会错位一个step。打印的loss是每个batch的瞬时值波动正常要看整体趋势还是得看验证集。验证环节用sklearn的classification_reportfrom sklearn.metrics import classification_report model.eval() all_preds, all_labels [], [] with torch.no_grad(): for batch in DataLoader(val_dataset, batch_size16, shuffleFalse): input_ids, attention_mask, labels [x.to(device) for x in batch] logits model(input_ids, attention_maskattention_mask).logits preds torch.argmax(logits, dim-1) all_preds.extend(preds.cpu().tolist()) all_labels.extend(labels.cpu().tolist()) print(classification_report(all_labels, all_preds, target_nameslist(label2id.keys())))验证时务必model.eval()否则dropout还在工作同一段文本预测结果会变来变去。torch.no_grad()省显存也避免构建反向计算图。评估报告里重点看每个类别的F1而不是整体accuracy。三分类样本不均衡时accuracy很容易骗人。4. 从情感分析扩展到多分类文本任务模型改动与数据策略的统一情感分析和文本分类在Bert精调框架下其实是同一个模型、同一套训练脚本差别只在num_labels和数据标注。这一章讲清楚替换点以及对应的数据策略。4.1 只改num_labels不需要重写模型情感分析通常3个label文本分类可能是10类甚至20类。BertForSequenceClassification内部是768维向量接一个分类头分类头的神经元个数等于num_labels。从3类换到20类唯一变化是分类头矩阵的维度模型主体不需要改。# 情感分析设置成3文本分类设置成业务类别数 num_labels 20 model BertForSequenceClassification.from_pretrained( /data/pretrained/bert-base-chinese, num_labelsnum_labels, )有一个容易踩的坑当num_labels1时BertForSequenceClassification会把任务当作回归loss变成MSE。所以二分类任务也要设num_labels2不能设1。如果任务是多标签分类那不能用CrossEntropy要换成sigmoid加BCE这是另一套逻辑。数据量小或硬件资源紧张时可以冻结底层只精调上层for name, param in model.bert.named_parameters(): if name.startswith(encoder.layer.0) or name.startswith(encoder.layer.1): param.requires_grad False for name, param in model.bert.embeddings.named_parameters(): param.requires_grad False冻结Embedding层和最低两层能省一部分显存、减少过拟合代价是少学到下游任务的底层特征。数据量只有几千条时这个操作比全量训练更稳。4.2 长文本超过512个token截断、滑窗和内容主次Bert的position embedding上限是512输入的token数量不能超过512。业务文本经常有几百上千字直接截断会丢关键信息。三种策略各有适用场景策略做法优点缺点直接去尾保留前512实现最简单尾部结论容易丢头尾拼接取前256后256头尾信息都能保留中段内容丢失滑窗平均分窗预测后融合完整覆盖全文推理耗时增加我处理长文本工单时常用滑窗平均代码大致这样def predict_long_text(model, tokenizer, text, max_len480, stride160): tokens tokenizer.encode(text, add_special_tokensTrue) windows [] for start in range(0, len(tokens), stride): window tokens[start:start max_len] if len(window) 32: continue windows.append(torch.tensor([window]).to(device)) logits_list [] model.eval() with torch.no_grad(): for w in windows: attention_mask (w ! tokenizer.pad_token_id).long() out model(input_idsw, attention_maskattention_mask).logits logits_list.append(out) avg_logits torch.mean(torch.cat(logits_list), dim0) return torch.softmax(avg_logits, dim-1)stride是每次滑动的步长窗口之间有重叠能让断点两边的语义被两个窗口各看一次缓解“关键信息恰好在截断处被一分为二”的问题。最后对logits取平均而不是简单投票投票会把少数窗口的极端预测放大。max_len设480而不是512是为了给CLS和SEP两个特殊token留空间。stride160表示相邻窗口有320个token重叠覆盖率高但推理量增加如果在意速度可以调到stride240。4.3 类别不均衡时计算类别权重别盲目过采样文本分类里“退费”“投诉”这类样本通常很少直接训练模型会把它归到多数类。我一般不做SMOTE文本过采样很容易让模型背下重复句子。更常用的是给loss加类别权重import numpy as np from sklearn.utils.class_weight import compute_class_weight from torch import nn class_weights compute_class_weight( balanced, classesnp.unique(y_train), yy_train, ) class_weights torch.tensor(class_weights, dtypetorch.float32).to(device) logits model(input_ids, attention_maskattention_mask).logits loss_fn nn.CrossEntropyLoss(weightclass_weights) loss loss_fn(logits, labels)compute_class_weight的balanced模式按样本数倒数归一化多数类权重小于1少数类权重大于1。这时不能用Bert自带的outputs.loss要手动把logits和labels传给CrossEntropyLoss。加了类别权重后少数类召回会上升但精确率可能略降。最终评估要盯macro-F1不能只看整体accuracy——整体accuracy 90%以上但少数类全错时模型线上完全不可用。5. Bert训练避坑指南loss异常、过拟合、显存不足与复现性问题排查Bert精调不像传统模型那样“多训几轮就好”它的坑很集中。下面这几条是我在实际项目里反复踩过的按现象、原因、解决整理。5.1 loss不下降或者越训越高先查学习率与数据顺序现象训练到第1个epoch结束loss还在2.0附近不动继续训练数值甚至升高。原因通常是三个一是学习率设到了1e-4或更大预训练权重被冲坏二是DataLoader的shuffleFalse同类样本连续出现梯度方向偏置三是标签本身噪声高正负标注反了。分类器随机初始化时loss大约在ln(num_labels)附近三分类就是1.1左右。如果loss一直停在2.0先怀疑输入输出没对上。解决先把学习率降到3e-5以内并加10%的warmup确认shuffleTrue再随机抽20条样本人工过一遍标注。注意打印的是每个batch的瞬时loss波动正常要看每个epoch结束后验证集的表现。有时候loss看着没降验证F1其实在缓慢上升这种情况先别急着调参。5.2 训练很好、验证很差过拟合的三个信号现象训练集F1到0.98验证集只有0.82验证loss在第2个epoch之后反弹。原因Bert参数量上亿几千条小样本足够它把训练集背下来。这也说明为什么epochs要控制在3以内。解决的三个手段按性价比排序先做early stopping看验证loss连续两个epoch不降就停再把weight_decay从0.01提到0.05最后考虑把dropout调大修改配置后重新加载模型。model.config.hidden_dropout_prob 0.2 model.config.attention_probs_dropout_prob 0.2dropout加大后训练loss会偏高但验证集通常会明显改善。注意不要同时把两个dropout都调到0.3以上否则模型会欠拟合。5.3 显存不够降batch_size后性能下滑用梯度累积和混合精度找回现象8GB显存跑batch_size32直接CUDA OOM降到4以后能跑但F1掉了好几个点。原因batch_size太小梯度噪声大优化方向不稳定尤其在分类头刚随机初始化时非常明显。解决用梯度累积模拟大batch。每4个batch累加一次梯度再更新参数等效batch_size翻4倍accum_steps 4 scaler torch.cuda.amp.GradScaler() for step, batch in enumerate(train_loader): input_ids, attention_mask, labels [x.to(device) for x in batch] with torch.cuda.amp.autocast(): outputs model(input_ids, attention_maskattention_mask, labelslabels) loss outputs.loss / accum_steps scaler.scale(loss).backward() if (step 1) % accum_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad() scheduler.step()loss除以accum_steps是为了让累积梯度量级和等效大batch一致。amp混合精度把大部分计算降到fp16显存能省30%到40%代价是偶尔数值不稳定。如果训练中出现loss为NaN先怀疑amp查一下CUDA版本和卡型号不行就关掉amp只用梯度累积。5.4 结果复现不了随机种子没固定现象同一个脚本连续跑两次验证集F1差2到3个点。原因GPU运算本身有非确定性train_test_split和DataLoader的shuffle也没固定随机状态。解决训练脚本最前面加固定种子函数并关掉cudnn的自动搜索import random import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed()cudnn.benchmarkTrue会动态选择卷积算法虽然快但不同运行间可能选择不同实现。set_seed要放在数据划分之前否则划分结果都不一样。需要说明完全锁死GPU行为并不容易同一份代码在不同型号显卡上的结果仍可能有小差异这属于正常范围。5.5 推理结果忽高忽低忘记model.eval()现象同一个样本线上预测时正负类别经常变监控里精确率忽上忽下。原因训练完直接进入预测模型还在train模式dropout在随机丢弃神经元LayerNorm也在用训练期的统计行为输出自然不稳定。解决推理入口统一加model.eval()和torch.no_grad()model.eval() with torch.no_grad(): logits model(input_ids, attention_maskattention_mask).logits这个坑几乎每个人都会踩一次而且线上很难排查因为看起来像“模型飘了”。先把eval模式补齐再做数据漂移分析这个顺序不能反。6. 把模型推到线上置信度阈值与业务兜底规则6.1 先找低置信度区域别用0.5一刀切模型输出的是每个类别的概率argmax只是选出最大概率的类别但没告诉你这个概率到底有多高。如果一条样本的最大概率只有0.4另一条是0.98两者都进自动分类显然不合适。我的做法是在验证集上统计最大概率的分布把低置信度样本单独筛出来max_probs, preds [], [] model.eval() with torch.no_grad(): for batch in val_loader: input_ids, attention_mask, labels [x.to(device) for x in batch] probs torch.softmax(model(input_ids, attention_maskattention_mask).logits, dim-1) max_prob, pred torch.max(probs, dim-1) max_probs.extend(max_prob.cpu().tolist()) preds.extend(pred.cpu().tolist()) for thr in [0.5, 0.55, 0.6, 0.65, 0.7]: mask [p thr for p in max_probs] keep_preds [p for p, m in zip(preds, mask) if m] keep_labels [l for l, m in zip(all_labels, mask) if m] if keep_labels: f1 f1_score(keep_labels, keep_preds, averagemacro) print(fthr {thr}: 进入自动分类 {sum(mask)} 条, macro-f1 {f1:.4f})阈值提高后自动分类的样本数减少但F1通常会上升。选阈值不是越高越好要跟业务能承担的人工处理量一起权衡。常见区间在0.6到0.7之间但具体数值必须从验证集跑出来不能拍脑袋。6.2 低置信度走人工兜底再回流精调低于阈值的样本不要硬分类设置一个“待人工”出口。业务上“拒识”比“错识”便宜错分会直接破坏信任拒识只是多一次人工点击。后续怎么做增量更新常见做法是每1到2周把人工确认过的新样本收集起来叠加一部分旧数据在现有模型上继续精调两轮学习率降到2e-5epochs设2。我在某个模拟项目X里照这个方式迭代上线头两周需要人工审核30%一个季度后降到15%模型保持稳定水位。“迭代”这件事不需要等模型百分百完美再上先上线、再回流、再更新比憋一个大版本更靠谱。6.3 保存产物时把模型、分词器和标签映射放同一个目录模型文件、label2id.json、tokenizer目录、训练参数和评估报告应该放同一个带时间戳的目录。这个习惯一开始容易被忽略直到某天预测脚本换机器跑label顺序对不上才后悔。保存代码很简单model.save_pretrained(outputs/bert_cls_20250101) tokenizer.save_pretrained(outputs/bert_cls_20250101)save_pretrained会把模型结构和权重一起保存之后用from_pretrained直接加载不需要重新做标签映射。当初我把label2id.json单独放在项目根目录后来换机器跑预测那个文件没带过去预测结果的类别全串了位。从那以后我把模型、tokenizer、label映射放进同一个目录作为一套完整产物。这份经验也分享给你希望帮到你。本文还有配套的精品资源点击获取