基于BERT的中文情感分类实战:源码拆解与避坑指南
简介基于BERT的中文文本情感分类完整项目包面向计算机相关专业正在做毕业设计或课程设计的学生以及需要项目实战练习的深度学习入门者核心解决中文文本情感分类任务从数据处理到模型训练、评估的完整落地问题。包内共37个文件、总大小约383.81MB以16个Python脚本为主干覆盖BERT模型定义、分词、优化器、分类器、预训练等关键模块另含4个Shell训练/推理脚本、4个CSV标注数据集、2个pkl模型文件以及使用说明和依赖清单。数据方面提供微博情感语料和train/dev/test划分可直接用于训练与验证配套的divide.py等脚本能完成自定义数据切分模型参数文件则让复现与二次开发更便捷。整个项目经过调试、结构清晰下载后即可运行既能满足毕业设计完整度要求又可作为学习BERT文本分类的练手项目。目前已有1070人学习下载。1. 中文文本情感分类这套BERT源码包为什么值得拆做情感分析毕设最怕的不是模型选型而是数据、代码、预训练权重三样东西要自己凑齐。这份项目源码把三者打包在了一起Google 官方中文 BERT 预训练模型12 层、768 维、微博情感语料 10 万条、完整的 BERT 微调脚本解压之后就能把「训练→评估→预测」整条链路跑通。它面向的就是计算机相关专业正在做毕业设计、课程设计或期末大作业的人不是玩具 demo跑完会有真实准确率能直接写进论文实验结果。下面我会按自己实际拆这类项目的顺序来讲先看文件结构再跑训练最后把坑和迁移方法讲透新手能跟着步骤走熟手也能直接跳到参数和避坑部分看重点。2. 项目文件地图核心脚本、预训练权重与微博数据构成拿到 zip 第一件事不是急着 pip install而是先搞清楚包里哪些文件是主线、哪些是官方仓库自带的冗余脚本。这个包的主干非常清晰预训练权重、微调脚本、数据、服务脚本四个层次分得很开。2.1 按功能拆文件哪些必须碰、哪些可以直接忽略我先按功能把文件分个类这样后面跑的时候不容易迷路。文件/目录作用优先级chinese_L-12_H-768_A-12.zip中文 BERT 预训练模型压缩包解压后有 bert_config.json、bert_model.ckpt、vocab.txt必须run_classifier.py分类任务主入口负责 load 数据、fine-tune、eval、predict必须modeling.py / modeling_adam.pyBERT 模型图结构定义两个文件是原版和带 Adam 的变体二选一tokenization.py中文分词器按字切分文本并映射到 vocab id必须optimization.py / optimization_adam.pyAdamWeightDecay 优化器实现二选一divide.py把 weibo_senti_100k.csv 切分成 train/dev/test必须data/划分好的 train.csv、dev.csv、test.csv必须weibo_senti_100k.csv微博情感原始语料约 10 万条可选run.sh / runadam.sh一键训练脚本两个对应不同优化器方案可选bertsvr.sh / svr把训练好的模型封装成服务供外部调用可选extract_features.py提取句子的 BERT 向量可选freeze_graph.py / freezegraph.sh冻结模型为 pb 文件用于离线部署可选run_pretraining.py / create_pretraining_data.pyBERT 预训练脚本做分类用不到忽略run_squad.py阅读理解任务示例忽略如果你只交毕设核心链路就是四条divide.py 切数据、run_classifier.py 训练、run_classifier.py 评估、run_classifier.py 预测。run_pretraining.py 和 run_squad.py 是 Google 原始仓库自带的示例不跑预训练的话碰都不用碰。modeling_adam.py 和 optimization_adam.py 这两个文件值得多说一句。原版 BERT 把优化器单独放在 optimization.py 里这个项目作者把 Adam 相关逻辑做进了 model 和 optimizer 两个变体文件里目的通常是为了更灵活地控制权重更新过程。常见做法是如果你不需要动优化器直接用原版两个文件就够如果训练时想调整 Adam 的 epsilon 或权重衰减策略再去碰优化器变体。不要在没搞清楚差异前两个文件混着 import容易出现变量定义冲突。2.2 微博100k数据格式与train/dev/test划分逻辑weibo_senti_100k.csv 是公开的微博情感语料约 10 万条短文本每条包含一个情感标签和微博正文。数据是这份资源比多数纯代码仓库值钱的地方很多毕业设计卡在找不到合规模、够量的中文文本数据这个包直接解决了。这种微博语料常见的列格式是标签和文本标签通常用 0、1 表示负向和正向。文本里经常带话题、用户、表情符号和 URL这些噪声在预处理阶段就要处理掉不然后面 BERT 分词时会把噪声也编码进语义。我一般会先把正文里的 用户名 和 http 链接整体替换成特殊占位符性格 polarity 判断时这步能减少无效 token。divide.py 做的事就是把原始 CSV 打乱顺序后按比例切分。常见切法是训练集 70%、开发集 20%、测试集 10%。如果你自己重新划分我建议 dev 稍微留大一点微调 BERT 时要用 dev 上的准确率判断是否过拟合dev 太少容易遇到「训练集某个 batch 把 acc 拉得很高、一上 dev 就崩」的假象。数据处理完data 目录里会得到 train.csv、dev.csv、test.csv 三个文件其实都是标准 TSV。BERT 的 run_classifier.py 读取数据时用 \t 做分隔符第一列是 label第二列是 text_a这是踩坑高发区后面避坑章会专门讲。2.3 BERT fine-tuning原理情感分类为什么只需加一层分类器BERT 的预训练阶段在大规模中文语料上做 Masked Language Model 和 Next Sentence Prediction 两个任务训练出来的不是分类器而是一组能编码中文上下文语义的权重。情感分类这类下游任务只需要在 BERT 顶层加一个全连接层加 softmax用标注好的情感数据做微调把预训练权重和新增分类层一起反向传播更新。为什么可以这样因为预训练模型已经学会了「中文句子里的词和词怎么互相影响」这类通用知识情感极性判断本质上是在这个知识基础上追加一个简单的线性映射。这也解释了为什么微调时学习率不能太大——预训练权重已经是很好的解学习率超过 5e-5 容易把原有语义空间冲乱出现训练集 loss 降、dev 准确率反而下降的翻车现象。对中文来说 BERT 还有一个隐性福利它的分词器是字符级切分vocab.txt 里两万多个 token 覆盖常用汉字和少量多字词块不需要像传统 NLP 流水线那样先做 jieba 分词。你输入「今天的饭真好吃」tokenizer 会把它拆成「今 天 的 饭 真 好 吃」这样的单字序列再映射成 id。这就省掉了一个预处理模块。BERT 对输入长度也有硬上限位置编码最多支持 512 个 token。新浪微博正文一般不超过 140 字取 max_seq_length128 足够覆盖绝大多数样本这也是这个项目里默认值合理的直接原因。## 3. 跑通训练从环境准备到run_classifier参数对照理论清楚了开始动手。这一章我按实际顺序写环境、数据、训练命令、输出产物、服务部署。3.1 环境准备Python版本、TensorFlow版本与依赖安装这个项目用的是原始 Google BERT 的 TensorFlow 实现对应的是 TF 1.x。我实测过的稳定组合是 Python 3.6 或 3.7 配上 TensorFlow 1.15Python 3.8 以上容易出现 tf.contrib 模块被移除导致的 import 报错。requirements.txt 里一般包含 tensorflow、numpy、six 这类基础依赖。我习惯先建一个干净的虚拟环境再装conda create -n bert-emo python3.7 -y conda activate bert-emo pip install -r requirements.txt逻辑说明创建 Python 3.7 的独立环境避免和系统其他项目的包冲突。装依赖时如果网络慢可以用国内 pip 镜像源加速但要注意镜像源里的 tensorflow 1.15 版本是完整的不影响本地运行。装完之后先验证一下python -c import tensorflow as tf; print(tf.__version__)能打印出 1.15.x 就说明环境对路。这一步值得做很多后面报的 import 错误其实都是环境版本问题提前排查能省一晚上的时间。3.2 数据预处理divide.py切分与TSV格式检查进到项目主目录后先跑数据切分。包里的 weibo_senti_100k.csv 是完整语料data 目录下可能已经有切好的文件但为了可复现我会自己再切一遍。cd ChineseSentimentAnalysiswithBERT主-master # 切分原始语料生成 train.csv / dev.csv / test.csv python divide.py逻辑说明divide.py 内部会把 10 万条数据打乱后按比例写到 data 目录。注意这个名字在主目录下运行不要把它放进 data 目录里跑否则输出路径会嵌套。切完后检查格式这一步非常关键提前暴露问题能省掉训练到一半才发现数据错位的痛苦。我会用 head 命令看前几行head -n 5 data/train.csv期望输出是每行两列用 tab 分隔第一列是 0 或 1第二列是一段中文文本。如果看到 CSV 逗号分隔或者第一行是表头那就要先处理掉。BERT 的 DataProcessor 是按 TSV 格式解析的列数不对会直接读崩。# 如果文件带了 header先去掉第一行再继续 tail -n 2 data/train.csv data/train.tmp mv data/train.tmp data/train.csv这段是经典处理手法tail 从第 2 行开始输出重定向到新文件再覆盖原文件。凡是之后自己换数据集都要走一遍这道检查工序。3.3 训练命令与核心参数对照表与调整建议环境数据都就位后解压预训练权重然后跑训练mkdir -p chinese_L-12_H-768_A-12 unzip chinese_L-12_H-768_A-12.zip -d chinese_L-12_H-768_A-12python run_classifier.py \ --task_nameweibo \ --do_traintrue \ --do_evaltrue \ --data_dir./data \ --vocab_file./chinese_L-12_H-768_A-12/vocab.txt \ --bert_config_file./chinese_L-12_H-768_A-12/bert_config.json \ --init_checkpoint./chinese_L-12_H-768_A-12/bert_model.ckpt \ --max_seq_length128 \ --train_batch_size32 \ --learning_rate2e-5 \ --num_train_epochs3.0 \ --output_dir./outputtask_name 要对应 run_classifier.py 里 processors 字典里已有的 key跑之前打开文件搜一下 processors确认这个项目自定义的任务名是 weibo 还是别的别直接照抄。do_traintrue 和 do_evaltrue 放在一起效果是训练完自动在 dev 集上做评估省一次命令。init_checkpoint 指向的是预训练权重这个路径错了后面准确率大概率只有一半属于必查项。output_dir 是训练产物输出目录建议每次实验换一个带参数标识的目录名比如 output_seq128_bs32方便对比结果。核心参数对照表参数含义建议值备注max_seq_length输入最大 token 数128微博短文 128 够用长文本任务提到 256 以上train_batch_size每步训练样本数32显存不够就降到 16优先级低于 seq_lengthlearning_rate微调学习率2e-5超过 5e-5 容易破坏预训练权重num_train_epochs训练轮数3.0数据量大时 3-5 轮足够太多会过拟合微调任务里学习率 2e-5 是 BERT 常见做法的经验值。官方文档和大量复现实验都集中在 2e-5 到 5e-5 区间低于 1e-5 收敛太慢高于 5e-5 容易把预训练学到的语义冲没这两种情况我在自己机器上都遇到过。3.4 训练产物与预测checkpoint、eval_results与test_results训练结束后output 目录会生成几类产物文件内容model.ckpt-XXXX模型权重每一步保存一次checkpoint指向最新 ckpt 的指针文件eval_results.txtdev 集上的准确率、losstest_results.tsv测试集每个样本的预测概率先看 eval_results.txt里面会写 eval_accuracy微博情感二分类上这个值在 0.90 上下浮动是正常的。如果只有 0.5 左右基本可以断定预训练权重没加载对直接回查 init_checkpoint。模型结构、任务难度、数据噪声都会影响最终准确率不在意在论文里写具体数字写「在 dev 集上达到 0.9X」加上 loss 下降曲线就是完整实验结果。做预测时把 init_checkpoint 换成训练产物并且只开 do_predictpython run_classifier.py \ --task_nameweibo \ --do_predicttrue \ --data_dir./data \ --vocab_file./chinese_L-12-H-768_A-12/vocab.txt \ --bert_config_file./chinese_L-12-H-768_A-12/bert_config.json \ --init_checkpoint./output/model.ckpt-4500 \ --max_seq_length128 \ --output_dir./outputinit_checkpoint 是 output 目录里最新保存的步数具体数字打开 output/checkpoint 文件一看便知。这个阶段不再需要 pre-training 的 bert_model.ckpt指向训练后的权重才有正确结果这是新手最容易翻车的地方之一。predict 跑完test_results.tsv 每行两个小数分别代表负向概率和正向概率取大的一列作为预测标签。3.5 把模型变成服务bertsvr.sh的部署思路项目里的 bertsvr.sh 走的是另一条路用 BERT 服务框架把预训练模型加载起来提供向量接口。启动以后可以拿一句新文本直接调接口拿句向量再对接自己的分类层做实时预测。# 启动服务终端 1 bert-serving-start -model_dir ./chinese_L-12_H-768_A-12 -num_worker1 # 调用服务终端 2from bert_serving.client import BertClient bc BertClient() texts [这家店味道不错, 这家店味道不行] vectors bc.encode(texts) print(vectors.shape)逻辑说明bert-serving-start 把 BERT 权重加载进内存监听端口。BertClient 连上后把文本编码成向量每个句子输出一个 768 维表示。如果你只需要句子向量而不需要端到端分类这种方案最省事但它不包含情感分类层向量拿到手后还要自己接一个逻辑回归或其他分类器这里要区分开。4. 避坑指南六个高频报错与排查顺序这章是血泪经验汇总。以下每一条我都见过不止一个人卡住按「现象 → 原因 → 解决」来写排查时也可以按这个顺序走。4.1 GPU显存爆炸batch_size和max_seq_length怎么取舍现象run_classifier.py 跑到第一个 session.run 时报 ResourceExhaustedError提示 Allocator GPU_0_bfc ran out of memory。原因8GB 显存加载 BERT-Base 后剩余显存撑不住 max_seq_length128、train_batch_size32 的组合。BERT 的注意力矩阵占用和 seq_length 成平方关系128 和 64 的差别是四倍。解决把 train_batch_size 降到 16如果还报 OOM 就把 max_seq_length 降到 64。微博文本短64 丢的信息很少。如果两者都不够改用 CPU 跑代价是训练时间变长但能出结果。不要两个参数同时硬顶我之前在一张 6GB 卡上把 batch 调到 8、seq 降到 32 才跑稳优先级永远是先保 seq_length再保 batch_size。4.2 数据列对不上表头、分隔符与空行现象训练启动后报错 ValueError: Input 1 of node ... should be float32 tensor with shape [?], but encountered shape 或 IndexError: list index out of range。原因读 TSV 时 line 是按 \t 切分的列表如果原始文件是逗号分隔、带了表头、或者存在空行line[0] 和 line[1] 就取不到预期值。解决先对三个文件都跑一遍awk -F \t {print NF} data/train.csv | sort -u看每行列数是否一致。理想输出只包含一个数字就是 2。如果出现 1 或 3说明有行列数不对基本是文本内部混入了多余的 tab 或换行。可以写个小脚本把多余 tab 替换成空格或者用我前面说的 tail 去表头操作处理完再训练。4.3 准确率在50%徘徊init_checkpoint加载失败现象训练过程不报错loss 也在下降但 eval 准确率始终在 0.5 附近跟随机猜测差不多。原因init_checkpoint 路径写错或文件不完整BERT 实际是用随机权重做的训练。没有预训练权重的 BERT-Base 堆一堆随机参数小数据量下学不出语义。解决检查 init_checkpoint 是否指向 chinese_L-12_H-768_A-12/bert_model.ckpt注意不是 .index 也不是 .data 文件名。打开 chinese_L-12_H-768_A-12 目录确认三个文件都在bert_model.ckpt.meta、bert_model.ckpt.index、bert_model.ckpt.data-00000-of-00001。缺任何一个都补全再跑。另外训练日志里如果出现 Warning: Could not find old checkpoint 或类似提示就是路径错了别往下跑。4.4 TensorFlow 1.15与CUDA版本冲突现象import tensorflow 后报 Could not load dynamic library libcudnn.so.7 或模块里没有 contrib 属性。原因TF 1.15 对应 CUDA 10.0 和 cuDNN 7新机器装的高版本 CUDA 和 TF 1.15 不兼容。另外 TF 1.15 如果安装包是 cpu 版本则不会报这个错但只能 CPU 跑。解决优先用 conda 建环境然后安装 cudatoolkit10.0 cudnn7再装 tensorflow-gpu1.15。或者直接用 CPU 版本设CUDA_VISIBLE_DEVICES环境变量跑数据只有 10 万条时训练时间可接受能保证结果正确性优先。我自己的习惯是毕设阶段只要不追求速度干脆 CPU 训练省掉一堆 CUDA 环境问题。4.5 输出乱码标签错位编码和多余字符现象预测结果 test_results.tsv 里标签和文本对不上或者中文文本出现乱码eval 准确率异常低。原因第一种情况是文本里有 \t 或换行导致 BERT 解析时列错位。第二种情况是文件编码不是 UTF-8常见于直接在 Windows 记事本里编辑过数据。这两种情况都会让标签跟噪声绑定模型学到的是错误 pattern。解决用 chardet 或 file 命令检测 CSV 编码统一转成 UTF-8用 python 脚本把文本里的 \t 替换成空格。清洗规则是空行删掉标签列只能有 0 或 1超过两个类别要检查是否混入了噪声标签。做完之后重新跑一遍数据检查命令再训练。数据清洗在 NLP 项目里作用很大是值得多花半小时的步骤。5. 迁移到自有数据Processor改造与四个验证步骤最后这章讲透怎么把这份源码变成自己的项目。很多毕设要求不只用现成微博数据老板可能让你换个领域比如电商评论或社交平台评论。这个项目的代码结构对换场景是友好的。5.1 新增Processor把自己数据集接进run_classifierrun_classifier.py 里最核心的扩展点是 DataProcessor 子类。你想要换数据集只需要照葫芦画瓢写一个新的 Processor然后在文件末尾的 processors 字典里注册一下。# run_classifier.py 中仿照原有 Processor 写一个新类 class EcommerceCommentProcessor(DataProcessor): 电商评论情感二分类,标签 0 负面, 1 正面 def get_train_examples(self, data_dir): lines self._read_tsv(os.path.join(data_dir, train.tsv)) return self._create_examples(lines, train) def get_dev_examples(self, data_dir): lines self._read_tsv(os.path.join(data_dir, dev.tsv)) return self._create_examples(lines, dev) def get_test_examples(self, data_dir): lines self._read_tsv(os.path.join(data_dir, test.tsv)) return self._create_examples(lines, test) def get_labels(self): return [0, 1] def _create_examples(self, lines, set_type): examples [] for (i, line) in enumerate(lines): if len(line) 2: continue guid %s-%s % (set_type, i) text_a tokenization.convert_to_unicode(line[1].strip()) label tokenization.convert_to_unicode(line[0].strip()) examples.append(InputExample(guidguid, text_atext_a, labellabel)) return examples逻辑说明get_train_examples 从 data_dir 读 train.tsv_create_examples 里按label\ttext的列顺序构造成 InputExample。text_b 留空是因为情感分类是单句任务不需要句子对输入。get_labels 返回的是标签字符串列表BERT 会把它们转成 id。注册处一般是 run_classifier.py 的 main 函数附近加一行映射processors { weibo: WeiboProcessor, ecommerce: EcommerceCommentProcessor, }之后训练命令里把--task_nameecommerce就可以换到你的数据集了。这时只需要保证你的数据文件命名和路径与 Processor 里一致跑起来几乎不用改其他代码。5.2 四个验证步骤确认模型是真提升而不是过拟合换数据后光看训练集 loss 下降不够我一般会在训练完强制走一套验证流程。第一步看 dev 上的 eval_accuracy 和训练集 accuracy 差多少。差 3 个点以内正常差 5 个点以上说明过拟合需要加大数据量或降低 epoch。第二步看 test_results.tsv 里两个类的预测概率分布。如果所有样本都集中输出 0.9 以上或 0.1 以下模型置信度过高需要检查数据里是否有明显的重复样本干扰。第三步对比一个 baseline。方法是用同样的数据跑一个 TF-IDF 加逻辑回归的分类器把它的准确率和 BERT 的放一起。这个对比不仅证明 BERT 的有效性也是论文实验章节常见的套路能直接搬过去用。第四步做 5 折交叉验证。数据量在 10 万条时可以直接跑用五份的平均准确率和方差作为最终结果。这个数字比单次划分更有说服力也是毕设答辩时被问到「你的结果可靠吗」时最好的回应。从那次之后我每次拿到一套 BERT 分类项目第一件事都是跑一遍 dev 评估确认 checkpoint 加载成功再动训练参数第二件事就是为自定义数据写好 Processor 再跑迁移。这两步做顺了项目的可复现性和说服力都会好很多希望这份笔记帮到你。本文还有配套的精品资源点击获取