ARTICLE DETAIL

资讯详情

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

BERT情感分析实战:从中文语料到模型微调的完整指南

BERT情感分析实战:从中文语料到模型微调的完整指南 简介基于BERT的中文情感分析项目包面向具备基础Python知识、希望系统掌握预训练模型微调流程的NLP学习者和开发者完整提供了从模型加载、数据预处理、训练评估到推理应用的主要流程。包内共35个文件、446KB包含8个Python源文件、10个Markdown说明文档、7个txt配置或依赖清单以及json、yml、gif等辅助资源兼顾代码、文档与演示素材。目前已有452人学习浏览。资源围绕6.4.4-1-main组织提供算法示例、自测练习与扩展练习三个模块算法示例包含带注释的train与predict脚本及演示动画帮助直观理解BERT的调用方式自测练习配有test_case.py和说明文档可检验掌握程度扩展练习与requirements依赖清单则支持继续探索。由于模型较大读者可借助说明文档调参并配合GPU环境运行。1. 把 BERT 情感分析代码拿到手第一件事不是跑是看它怎么组织你费劲下载了一份“Python 基于 BERT 的情感分析”压缩包如果直接双击 README 就开跑大概率会在数据路径和依赖上卡半小时。这份资源的核心是1_算法示例里的train.py和predict.py——一个负责把标注好的中文语料训练成情感分类模型一个负责对新文本输出正面或负面结论中间的2_算法演示、3_自测练习、4_扩展练习是配套的验证与上手材料。我用它复现了一遍从训练到推理的完整流程最直接的体感是BERT 本身没什么黑匣子复杂的是数据怎么喂进去、参数怎么设、以及哪些环节最容易翻车。如果你正在做电商评论分析、客服工单分类或者社交媒体舆情监控又不想从零搭 Transformer这份项目能帮你省下大量造轮子的时间。2. 先把原理立住双向编码为什么适合情感分析以及这个包怎么分工2.1 为什么是 BERT 而不是词向量情感分析本质上是一个文本分类任务给一段话判断它是正向还是负向有时加一个中性。早年做这类任务主流方案是 TF-IDF 或 Word2Vec 把句子变成向量再喂给 SVM、朴素贝叶斯或者浅层神经网络。那个路线最大的问题在于词向量是静态的“挺好的”和“好”在向量空间里各待各的不考虑上下文而中文里“这手机续航真不错”和“这手机真不错续航除外”这两句话词表面相近情感却可能完全不同。BERT 解决这个问题的办法是预训练阶段用 Masked Language ModelMLM任务——随机遮住句子里的一个词让模型去猜。为了猜对它必须同时看这个词左边的词和右边的词这就是“双向编码”的含义。这种训练方式让 BERT 学到了真正依赖上下文的语义表示所以拿到情感分析这样的小任务时不需要从零学语法和语义只需要在预训练参数上做微调fine-tuning。用代码说就是下面这行from transformers import BertForSequenceClassification, BertTokenizer model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2 ) tokenizer BertTokenizer.from_pretrained(bert-base-chinese)逻辑说明BertForSequenceClassification是 Hugging Face Transformers 库给分类任务专门封装好的类它在 BERT 的最后一层输出之后接了一个线性分类头num_labels2表示二分类。用bert-base-chinese这个预训练权重是因为它是在中文语料上训练出来的分词粒度是字不用额外装 jieba。如果你用的是英文模型处理中文文本分词器会把每个汉字切碎模型效果会明显变差这个后文会专门讲。2.2 项目包结构每个目录对应一条学习路径解压之后目录树和我在开头列举的一致核心是这五个部分目录 / 文件作用怎么用1_算法示例含train.py、predict.py和对应 README主代码训练和预测都在这2_算法演示demo.gif加说明文档先看动图知道跑通后长什么样3_自测练习test_case.py加 README给你准备好的测试用例验证环境装没装对4_扩展练习额外练习材料改参数、换数据集的进阶操作requirements.txt依赖清单装环境用省得一个个 pip install我建议的学习顺序是先看2_算法演示里的 GIF对输出结果有个预期然后跑3_自测练习/test_case.py确认环境没问题再进1_算法示例看训练和预测代码最后用4_扩展练习里的方向去改模型。项目根目录还有.gitignore、.gitpod.yml、.vscode/settings.json这些是工程配置不影响算法逻辑但用 VS Code 打开项目时格式化风格会保持一致。2.3 环境准备装好依赖再动手别让 pip 拖后腿跑这个项目之前先把 Python 环境准备好。这个包的核心依赖是 Transformers 和 PyTorch缺少任何一个import阶段就会报错。安装命令如下pip install transformers torch pandas numpy scikit-learn参数说明transformers负责加载 BERT 模型和分词器torch是底层深度学习框架pandas和numpy用来读数据和做数值运算scikit-learn用于评估指标计算和数据集划分。如果你用的是装有 NVIDIA 显卡的机器建议先单独安装对应 CUDA 版本的 PyTorch再装其余依赖这样训练速度会快很多。验证环境是否装好可以直接跑一行 Pythonpython -c from transformers import BertTokenizer; print(BertTokenizer.from_pretrained(bert-base-chinese))能正常打印出分词器对象说明 Transformers 库和网络下载通道都正常。这里有个很常见的坑bert-base-chinese首次使用时需要从 Hugging Face 下载预训练模型大约 400MB网络不稳定时容易中断。下载失败时程序会卡在进度条上看起来像死机实际上是在重试。解决办法是设置镜像源或者在网络稳定的时段先手动下载到本地缓存目录。3. 数据预处理与训练管线从原始文本到模型输入的五步转换3.1 BERT 到底在接收什么格式的输入和传统深度学习模型不一样BERT 的输入不是一串文本而是三个对齐的张量input_idstoken 对应的 ID、attention_mask哪些位置是真实文本、哪些是填充、token_type_ids区分句子 A 和句子 B。这个格式由分词器一件生成不需要手写。看一下实际代码from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) text 这家店的奶茶特别好喝 encoded tokenizer.encode_plus( text, add_special_tokensTrue, max_length64, paddingmax_length, truncationTrue, return_tensorspt, return_token_type_idsTrue, return_attention_maskTrue, ) print(encoded.keys()) print(encoded[input_ids])逻辑说明encode_plus是分词器最常用的编码方法它把中文句子切分成 token再映射成数字 ID。add_special_tokensTrue会在句子开头加上[CLS]、结尾加上[SEP]这两个特殊 token 对分类任务至关重要——[CLS]位置的最终隐藏层向量会被当作整个句子的语义表示送到分类头里做预测。max_length64设置了序列长度上限paddingmax_length把不足 64 的位置补 0truncationTrue把超过 64 的部分截掉。你可能要问max_length设多少合适这个参数直接影响训练速度和效果。max_length太小长句子后半段的信息被截断模型看不到关键情感词太大显存占用成倍上升训练时间拉长。64是一个比较稳妥的起步值如果你的数据是商品评论、微博这类短文本64 基本够用如果是长文评论或段落级文本建议改成128甚至256。注意一点训练和预测时必须用同一个max_length否则模型看到的输入分布不一致预测效果会失真。3.2 从原始 CSV 到 PyTorch Dataset拿到带标签的语料一般是 CSV 文件两列text和label需要先做切分再包装成 PyTorch 可以迭代的数据集。下面是一份可以直接套用的数据处理代码import pandas as pd from sklearn.model_selection import train_test_split from torch.utils.data import Dataset, DataLoader class SentimentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len64): self.texts texts self.labels labels self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text str(self.texts[idx]) label int(self.labels[idx]) encoded self.tokenizer.encode_plus( text, max_lengthself.max_len, paddingmax_length, truncationTrue, return_tensorspt, ) return { input_ids: encoded[input_ids].squeeze(0), attention_mask: encoded[attention_mask].squeeze(0), label: torch.tensor(label, dtypetorch.long), } # 读取数据 df pd.read_csv(sentiment_data.csv) train_df, val_df train_test_split(df, test_size0.2, random_state42) train_dataset SentimentDataset( textstrain_df[text].values, labelstrain_df[label].values, tokenizertokenizer, ) val_dataset SentimentDataset( textsval_df[text].values, labelsval_df[label].values, tokenizertokenizer, ) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue) val_loader DataLoader(val_dataset, batch_size16, shuffleFalse)逻辑说明SentimentDataset继承自Dataset核心在__getitem__——每次取一条样本实时调用分词器编码并返回三个张量。这里用squeeze(0)是因为encode_plus返回的张量默认带一个 batch 维度单个样本不需要它压缩掉才能正确喂给模型。train_test_split用 8:2 比例切分数据random_state42保证每次切分结果一致方便复现。补一个细节分词是在__getitem__里做的这意味着每个 epoch 都会把所有文本重新编码一遍。数据量小没事几万条以上就会明显拖慢训练。更好的做法是提前把所有文本一次性编码好用input_ids和attention_mask直接构建 Dataset缺点是占用内存更大。训练脚本里用哪种方式看 README 里的数据规模我建议小数据用上面的写法大数据改预编码。3.3 数据预处理的三条检查清单数据格式这块是新手最容易出错的地方我一般会按三条顺序检查第一标签必须是整数不能是字符串“正面/负面”直接进模型。label列如果是中文要做一次映射{负面: 0, 正面: 1}或者反过来。映射关系一旦定了训练、评估、预测都要保持一致否则准确率高得离谱但实际应用时全预测反。第二tokenizer所在的语言版本必须和文本一致中文文本用bert-base-chinese英文文本用bert-base-uncased或bert-base-cased。第三attention_mask不要手动置零。部分人为了“省显存”手动把 padding 位置的 mask 改成 0这本身没错因为分词器默认就是这么做的但如果你同时又把max_length改小了旧数据里已经编码好的张量长度对不上DataLoader会直接报维度不匹配的错。遇到这类报错重新跑一遍预处理就好。4. 训练与评估实操两个脚本的完整闭环4.1 训练脚本 train.py 的完整解读这个项目里最值得逐行读的就是1_算法示例/train.py。它做的事情可以拆成五步加载模型、配置优化器、定义数据加载器、跑训练循环、保存权重。核心代码如下import torch from transformers import BertForSequenceClassification, AdamW from torch.nn import CrossEntropyLoss from tqdm import tqdm model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2, ) model.train() optimizer AdamW(model.parameters(), lr2e-5) loss_fn CrossEntropyLoss() EPOCHS 3 for epoch in range(EPOCHS): total_loss 0 for batch in tqdm(train_loader, descfEpoch {epoch 1}): input_ids batch[input_ids] attention_mask batch[attention_mask] labels batch[label] optimizer.zero_grad() outputs model( input_idsinput_ids, attention_maskattention_mask, labelslabels, ) loss outputs.loss total_loss loss.item() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() print(fEpoch {epoch 1} loss: {total_loss / len(train_loader):.4f}) torch.save(model.state_dict(), bert_sentiment.pt)逻辑说明AdamW是 BERT 微调最常用的优化器lr2e-5这个学习率是经过大量实验验证的推荐值——BERT 的预训练参数已经收敛得很好微调时学习率不能太大否则会破坏已学到的语义表示。model的前向输出outputs.loss是模型内部算好的交叉熵损失因为传入labels时模型会自动计算省去了手动传给loss_fn的一步。clip_grad_norm_是对梯度做裁剪防止梯度爆炸max_norm1.0是经验值。训练循环本身不复杂但有几个细节决定了最终效果optimizer.zero_grad()必须在每个 batch 前调用否则梯度会累加model.train()和后面的model.eval()要配对使用前者开启 dropout 和 BatchNorm 训练模式后者关闭保存权重用model.state_dict()而不是整个model对象这样加载时更灵活不受 Transformers 库版本影响。4.2 评估阶段别只看 loss要看精确率和召回率训练过程中如果只盯 loss很容易被误导。loss 下降只说明模型在拟合训练集不保证泛化能力。跑验证集评估是必须的而且要看分类指标from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score model.eval() all_preds [] all_labels [] with torch.no_grad(): for batch in val_loader: input_ids batch[input_ids] attention_mask batch[attention_mask] labels batch[label] outputs model(input_idsinput_ids, attention_maskattention_mask) logits outputs.logits preds torch.argmax(logits, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(准确率:, accuracy_score(all_labels, all_preds)) print(精确率:, precision_score(all_labels, all_preds)) print(召回率:, recall_score(all_labels, all_preds)) print(F1:, f1_score(all_labels, all_preds))逻辑说明验证阶段必须用torch.no_grad()包裹关闭梯度计算一方面省显存另一方面防止反向传播改变模型权重。torch.argmax(logits, dim1)取分类头输出概率最大的类别作为预测结果。如果你的数据集正负样本不均衡准确率没有参考价值比如 90% 的样本是正面模型全预测正面也有 90% 准确率这时候必须看召回率和 F1。4.3 自测脚本和预测脚本怎么配合3_自测练习/test_case.py的作用是验证环境不是训练模型。它一般会加载一个已经训练好的权重文件对几条预设文本做预测并比较结果。你可以在跑完train.py之后用test_case.py来检验自己训出来的模型权重能否通过基础测试。predict.py则是实际使用的推理脚本它的核心代码很短from transformers import BertForSequenceClassification, BertTokenizer model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels2, ) model.load_state_dict(torch.load(bert_sentiment.pt)) model.eval() tokenizer BertTokenizer.from_pretrained(bert-base-chinese) text 这家店的奶茶特别好喝 encoded tokenizer.encode_plus( text, max_length64, paddingmax_length, truncationTrue, return_tensorspt, ) with torch.no_grad(): outputs model(**encoded) logits outputs.logits pred torch.argmax(logits, dim1).item() print(预测标签:, pred)逻辑说明这里有个容易踩的问题——load_state_dict之前必须先实例化一个结构完全相同的模型也就是说num_labels、预训练版本都要和训练时一致否则加载权重会报 key 不匹配。训练时如果你训的是英文 BERT预测时却加载中文模型权重基本全对不上模型预测结果会是错的。model(**encoded)里的**是把input_ids、attention_mask、token_type_ids三个键值对展开成关键字参数传进去这是 Transformers 库常见写法比手动逐个传参更简洁。5. 避坑指南BERT 实战中最容易翻车的五个问题5.1 模型预测结果全部偏向某一个类别现象训练后 loss 正常下降但预测时所有文本都被判成“正面”负面评论一条都识别不出来。原因数据集严重不平衡负样本占比太小模型“学”到的策略是全部输出多数类。另一个常见原因是batch_size太大而max_length太短模型根本没看到少数类的有效特征。解决先统计训练集里各个类别的数量确认是否平衡。不平衡就先做重采样把少数类复制几遍或者用class_weight给少数类更高的损失权重。在BertForSequenceClassification里可以这样设置from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( class_weightbalanced, classesnp.array([0, 1]), ytrain_df[label].values, ) weights torch.tensor(class_weights, dtypetorch.float) loss_fn CrossEntropyLoss(weightweights)逻辑说明compute_class_weight会根据每个类别的样本量自动计算权重样本少的类别权重高样本多的类别权重低。这样训练时模型对少数类的误判会有更大的损失惩罚迫使它认真学少数类的特征。5.2 显存不够batch_size 调到 4 还是 OOM现象CUDA out of memory训练第一个 batch 就崩。很多人把batch_size从 32 降到 16、再降到 8仍然报错。原因BERT 的显存占用不只是看 batch_size还看max_length。序列长度 64 和 256 的显存消耗差距接近四倍。如果机器是 8GB 显存max_length256、batch_size16确实扛不住。解决优先缩短max_length把 256 降到 128其次才是调小 batch_size。如果两条都不能改用梯度累积gradient accumulation每几个 batch 更新一次权重效果等价于大 batch 训练accumulation_steps 4 optimizer.zero_grad() for step, batch in enumerate(train_loader): outputs model(**batch) loss outputs.loss / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()参数说明accumulation_steps4表示攒够 4 个 batch 的梯度再更新一次权重效果上近似于把 batch_size 扩大 4 倍但显存占用不变。5.3 分词器用错中文字被切得稀碎现象模型能跑loss 也下降但预测结果跟随机猜差不多打印input_ids发现中文字符全变成了[UNK]。原因加载的模型是bert-base-uncased英文版分词器不认识中文字符所有汉字都映射到未知 token。代码里写了BertTokenizer.from_pretrained(bert-base-uncased)但数据是中文。解决全项目统一用bert-base-chinese。训练脚本、预测脚本、自测脚本都要对齐。检查方法是打印一段中文的分词结果print(tokenizer.tokenize(这家店的奶茶特别好喝))如果输出是[这, 家, 店, 的, 奶, 茶, 特, 别, 好, 喝]说明分词器正常如果看到[UNK]就是语言版本不匹配。5.4 test_case.py 直接跑报 ModuleNotFoundError现象解压后直接运行python test_case.py报ModuleNotFoundError: No module named transformers或者找不到bert_sentiment.pt文件。原因两种常见情况。第一Python 环境没装依赖这个包在 README 里写了requirements.txt但没有自动安装脚本。第二test_case.py里写的是相对路径默认当前工作目录必须是项目根目录在别的目录下执行就会找不到权重文件。解决先装依赖再跑pip install -r requirements.txt然后在项目根目录下执行测试脚本不要从其他路径调用。如果是从 IDE 运行确认工作目录working directory已经设置成项目所在文件夹。5.5 微调后效果还不如随机森林现象花了几个小时训练验证集准确率只有 60%跟 TF-IDF 加逻辑回归跑出来的结果差不多甚至更差。原因最常见的是学习率设太大比如直接用lr1e-3。BERT 微调的合理范围是2e-5到5e-5比常规深度学习模型小两个数量级。学习率过大预训练学好的语义表示被前几个 step 冲坏。另一个原因是训练轮数太少BERT 微调通常需要 3 个 epoch 左右只跑 1 个 epoch 往往欠拟合。解决把lr改回2e-5EPOCHS设成 3。如果还不行检查训练集样本量——BERT 微调虽然比从零训练省数据但几千条以下效果确实不如传统方法稳定。这个情况没有捷径要么补数据要么改用更小的模型比如distilbert-base-chinese降低过拟合风险。6. 部署推理与验证让模型真正可用的几个进阶技巧模型训练完工作只完成一半。真正把模型用起来推理阶段的细节决定了体验。先说效率问题训练时model.train()推理时必须换成model.eval()并且用torch.no_grad()包裹。很多人忘了这步推理速度慢不说结果还带随机性——dropout 在训练模式会随机丢弃神经元同一个句子预测两次可能得到不同结论。下面是一个批量推理的写法def predict_batch(model, tokenizer, texts, max_len64): model.eval() encoded tokenizer( texts, max_lengthmax_len, paddingTrue, truncationTrue, return_tensorspt, ) with torch.no_grad(): logits model(**encoded).logits preds torch.argmax(logits, dim1).tolist() return preds逻辑说明tokenizer直接传一个文本列表进去内部会自动做 padding 和 truncation不需要手动写循环。paddingTrue会把同一个 batch 里的短文本补齐到一样长配合attention_mask使用模型不会把 padding 位置当成有效文本。批量推理比单条循环快很多因为 GPU 擅长并行处理多条样本。然后是模型保存的版本问题。train.py里保存的是model.state_dict()加载时必须先重新实例化一个结构相同的模型。这个方案的好处是权重文件小、跨 Transformers 版本兼容性好。但副作用是num_labels、tokenizer版本这类配置信息不会跟着权重走换机器时容易漏配。我现在的习惯是训练完不仅存权重还顺手存一个config.json记录model_name、max_length、num_labels、label_map这些关键参数加载时先读配置再建模型{ model_name: bert-base-chinese, max_length: 64, num_labels: 2, label_map: {0: 负面, 1: 正面} }验证模型效果有一个很实用的土办法找 20 条完全没参与训练的新数据人工标注好情感再让模型预测数一下对了几条。不要用验证集里的数据因为模型见过它们数字会虚高。我从那以后每次训练完都会顺手跑一遍这个盲测同时把max_length、lr、batch_size写进 config连同模型一起归档——下次要复现或调参时不用再从代码里翻参数。这份项目包适合当作你的第一个 BERT 实战入口把train.py跑通、把数据换成你自己的业务语料情感分析的基本功也就立住了。希望帮到你。本文还有配套的精品资源点击获取
返回列表