简介:这是一个基于BERT模型的中文文本情感分类完整项目,专为计算机相关专业毕业生及有实战需求的学习者准备,适用于毕业设计、课程设计或期末大作业。项目包含模型训练、特征提取、微调、优化等全套Python源码,并配套微博情感分类数据集、预训练BERT模型压缩包、shell运行脚本及说明文档,下载后可直接运行并作为毕设核心参考。整个资源包共37个文件,以Python脚本和shell脚本为主,另含CSV数据文件、pkl模型文件、TXT/MD说明文档等,大小约383.81MB,结构清晰便于查阅。目前已有1070人下载学习,适合具备Python与深度学习基础、希望快速搭建情感分类系统的读者。通过该项目可掌握BERT模型在中文NLP任务中的加载、微调与预测流程,同时可直接复用微博情感分类数据与调试好的代码,节省从零搭建的时间。
1. 拿 BERT 做中文情感分类:为什么这个项目值得当毕设基线
做中文文本情感分类的毕业设计,最不缺的就是各种“开箱即用”的 Demo,但真正能跑通、能出指标、能写进论文里的反而少。这个项目给的是一个完整的 BERT 中文情感分类闭环:从微博语料预处理、划分训练测试集,到加载chinese_L-12_H-768_A-12预训练模型微调,再到输出预测结果,整条链路都在。它不搞花活,就是标准 BERT 在单文本二分类上的落地姿势,适合正在做毕设、需要一份能稳定运行且能说清楚原理的实战项目的同学。我把它拆了一遍,把数据格式、参数配置、训练脚本的坑和对应解法都整理在下面,照着复现基本不会卡壳。
我当时拿到压缩包的第一反应是先看文件清单,确认是不是“缺胳膊少腿”的残包。解压后 BERT 官方代码的完整结构、预训练权重压缩包、标注好的 CSV 数据都在,这就值得往下走了。
2. 项目文件结构与数据格式:先把家底盘清楚
2.1 压缩包里的角色分工
这个项目的目录并不复杂,但每个文件承担的角色差别很大。我建议拿到压缩包后先别急着跑,花十分钟把结构过一遍。
核心文件有这么几组:
modeling.py、optimization.py、tokenization.py:BERT 的模型结构、优化器和分词器实现,这三个文件是整个项目的地基。modeling.py里定义了 Transformer 的 Encoder 结构、Multi-Head Attention、LayerNorm 这些关键组件,微调时我们改的是它的上层,一般不动这个文件内部实现。run_classifier.py:微调入口脚本,文本分类任务的训练、验证、预测全在它里面,是平时打交道最多的文件。chinese_L-12_H-768_A-12.zip:谷歌发布的中文 BERT 预训练权重压缩包,解压后里面是bert_config.json、vocab.txt、bert_model.ckpt.*三件套。weibo_senti_100k.csv、train.csv、dev.csv、test.csv:训练和评估用的原始数据集与划分结果。divide.py:数据划分脚本,把原始语料拆成训练、开发、测试三份。run.sh、bertsvr.sh:分别是微调启动脚本和服务化推理脚本,后面对应svr目录下的部署代码。
2.2 数据集格式与dev.csv的实际长什么样
weibo_senti_100k.csv是原始的微博情感语料,大概 10 万条级别,每行一条数据。下载后先确认一下编码格式,常见的是utf-8或gbk,用文本编辑器打开看一眼就行。标注规则一般是label, text,label 为 0 或 1,对应负面和正面情感。
dev.csv是划分后的开发集,格式和训练集完全一致,看一下首行就能确认。我打开后看到的结构是:
label,text 1,【心情好】今天天气真不错,出去走了一圈 0,加班到十一点,地铁都末班了,心累注意这个文件不带表头也可以跑,关键在于run_classifier.py读取数据时用的是tf.gfile.Open加逐行解析的方式,它不依赖 pandas,所以 CSV 的分隔符和字段顺序必须严格和代码里DataProcessor的解析逻辑一致。
2.3 预处理脚本divide.py做了什么
divide.py负责把原始语料打乱后按比例拆成 train、dev、test 三个文件。常见的拆分比例是 8:1:1,但脚本里具体比例要打开确认,我看到的版本支持通过参数指定。如果你要换自己的数据集,这个脚本是第一个要改的。
import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv('weibo_senti_100k.csv', encoding='utf-8') train, tmp = train_test_split(df, test_size=0.2, random_state=42) dev, test = train_test_split(tmp, test_size=0.5, random_state=42) train.to_csv('train.csv', index=False, encoding='utf-8') dev.to_csv('dev.csv', index=False, encoding='utf-8') test.to_csv('test.csv', index=False, encoding='utf-8')这段代码的关键在random_state,固定成同一个值才能保证每次运行划分结果一致,否则不同次运行得到的训练集不同,论文里的指标就没法复现。实际使用中我会刻意检查一下拆分后的类别分布,防止因为原始数据有序排列导致某个类别在某份文件中占比失衡。
3. BERT 中文分类的核心机制:模型在微调时到底改了什么
3.1 从预训练到微调:分类头是怎么加上去的
BERT 在预训练阶段学的是通用的语言表示,到文本分类任务时,需要在其输出层上面接一个分类头,再拿标注数据做有监督训练,这个过程叫微调(Fine-tuning)。
具体到这个项目,run_classifier.py中定义了一个TextClassifier类,它把modeling.py里的BertModel输出的pooled_output(也就是[CLS]位置对应的向量)接了一层全连接,映射到类别数量个 logits 上。训练时更新的是全部参数,不只是新加的分类层,这是 BERT 微调和“只训顶层”的常规做法的关键差异。
pooled_output的维度是[batch_size, hidden_size],在chinese_L-12_H-768_A-12里hidden_size是 768。二分类任务最终输出的 logits 维度是[batch_size, 2],接 softmax 后取概率最大的下标作为预测类别。
3.2 中文分词:为什么是字级别而不是词级别
chinese_L-12_H-768_A-12用的是vocab.txt里约 2.1 万个中文字符和常用符号做切分,也就是字级别(Character-level)的 Tokenizer。中文里“结婚”和“婚前”这种词,切出来是“结”“婚”“婚”“前”,模型通过注意力机制自己学习字与字之间的组合关系。
这一点对文本预处理有直接影响:你不需要做繁简转换、分词、去停用词这些传统 NLP 流水线操作,只需要把原始文本按字符处理,交给tokenization.py里的FullTokenizer即可。
3.3 三条输入序列:input_ids、input_mask、segment_ids
BERT 的输入不是一行文本,而是三个等长的整数序列,run_classifier.py里convert_single_example函数做的就是这件事。
假设我们有一条文本“今天天气不错”,调用分词器后变成 token 序列,再套上[CLS]和[SEP],最终得到:
tokens = ["[CLS]", "今", "天", "天", "气", "不", "错", "[SEP]"] input_ids = [101, 1762, 1921, 1921, 3694, 679, 743, 102] input_mask = [1, 1, 1, 1, 1, 1, 1, 1] segment_ids = [0, 0, 0, 0, 0, 0, 0, 0]然后 padding 到max_seq_length长度。input_mask区分真实 token 和 padding 位置,注意力机制会忽略 mask 为 0 的位置;segment_ids在单文本分类中全是 0。
vocab.txt里[CLS]对应的 id 是 101,[SEP]对应 102,[PAD]对应 0,这些值在任何中文 BERT 权重里是一致的。如果你在代码里看到 101、102 这些数字,不要当作魔法值,它们是词表里固定的特殊符号。
3.4 为什么说这是分类任务的“最标准姿势”
很多竞赛方案会在 BERT 上再接 BiLSTM 或注意力机制,但单纯做情感二分类,[CLS]向量直接接全连接往往就已经足够。原因在于 BERT 最后一层的[CLS]向量已经经过 12 层 Transformer 的信息聚合,语义表征能力足够强,强行加复杂结构反而容易过拟合。
我一般建议毕设第一版就跑这个最朴素的版本,拿到一个稳定的 baseline。如果后面需要提升指标,再在pooled_output后面加 Dropout 和多层全连接,而不是一开始就上复杂结构。这个项目的代码正好保留了从简单到复杂都方便改写的接口设计。
4. 从零跑通微调:训练脚本参数解读与完整流程
4.1 解压与目录准备
找到chinese_L-12_H-768_A-12.zip,解压到项目根目录下的chinese_L-12_H-768_A-12/文件夹。解压后确认目录里有bert_config.json、vocab.txt、bert_model.ckpt.meta、bert_model.ckpt.index、bert_model.ckpt.data-00000-of-00001这五个文件。
cd ChineseSentimentAnalysiswithBERT 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-12/ ls chinese_L-12_H-768_A-12/注意核对bert_model.ckpt三个后缀文件是否齐全,少了任何一个都无法加载预训练权重。ckpt.meta存图结构,ckpt.index存参数索引,ckpt.data-*存实际权重值,这三个文件要放在同一目录下且文件名前缀一致。
4.2 微调启动脚本run.sh的逐参数拆解
run.sh是整个项目最核心的启动入口,我基于实际运行情况整理一份可直接使用的版本,按 GPU 显存 6G 到 11G 的中等配置来设:
export BERT_BASE_DIR=./chinese_L-12_H-768_A-12 export DATA_DIR=./data export OUTPUT_DIR=./output python run_classifier.py \ --task_name=setiment \ --do_train=true \ --do_eval=true \ --do_predict=true \ --data_dir=$DATA_DIR \ --vocab_file=$BERT_BASE_DIR/vocab.txt \ --bert_config_file=$BERT_BASE_DIR/bert_config.json \ --init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \ --max_seq_length=128 \ --train_batch_size=32 \ --learning_rate=2e-5 \ --num_train_epochs=3.0 \ --output_dir=$OUTPUT_DIR关键参数的含义和调试经验:
task_name:要和run_classifier.py里processors字典的键对应。代码里自定义处理器时注册的名字是什么,这里就要写什么。如果写错,会直接报KeyError。do_train、do_eval、do_predict三个开关可以组合,比如先只训练不预测,训练完后再把do_train改称 false 单独预测。首次跑三者全开没问题。max_seq_length:最大序列长度,BERT 会把超过这个长度的文本截断。微博文本一般比较短,128 够用。如果做长文本分类,可以考虑 256,但训练时间和显存占用会同步上升。train_batch_size:单步训练的样本数。显存 6G 以下建议改成 16 或 8,同时注意如果减小 batch,学习率也可以适当下调,否则 loss 曲线会震荡。learning_rate:微调阶段 BERT 惯用的学习率在 2e-5 到 5e-5 之间,比从零训练小一个数量级以上。学习率太大容易把预训练权重冲坏,出现 loss 不降反升。num_train_epochs:训练轮数,常见范围为 2 到 4。小数据集上轮数太多会过拟合,可以对比 dev set 的准确率选最优轮数。
4.3 运行后的预期输出与指标怎么看
训练启动后会在终端打印类似下面的日志:
***** Running training ***** Num examples = 80000 Batch size = 32 Num steps = 7500Num steps的计算公式是:样本数乘以轮数除以 batch size 后向上取整。上面例子里 80000 个样本、32 的 batch、3 个 epoch,算出来就是 7500 步。
训练过程中每 100 步会打印一次 loss,观察它的趋势。BERT 微调在训练初期 loss 通常在 0.6 到 0.7 附近,然后逐步下降到 0.2 到 0.3 左右,这属于正常节奏。如果 loss 从开始就卡在 0.69 不动,多半是数据处理出了问题,比如正负样本标签反了,或者所有样本的输入都一样。
训练结束后,output_dir下会生成model.ckpt-*系列文件和eval_results.txt,后者内容形如:
eval_accuracy = 0.9821 eval_loss = 0.0987情感二分类任务在微博语料上跑到 95% 以上的准确率是正常水平。如果明显低于这个数,先检查dev.csv的标注质量,或训练集和验证集是否存在重叠。
4.4 用训练好的模型做预测
预测阶段要把do_predict设为 true,同时把init_checkpoint指向训练产物:
python run_classifier.py \ --task_name=setiment \ --do_predict=true \ --data_dir=$DATA_DIR \ --vocab_file=$BERT_BASE_DIR/vocab.txt \ --bert_config_file=$BERT_BASE_DIR/bert_config.json \ --init_checkpoint=$OUTPUT_DIR/model.ckpt-7500 \ --max_seq_length=128 \ --output_dir=$OUTPUT_DIR预测结果会写到output_dir/test_results.tsv,每行两个浮点数,对应负面和正面的概率。一般取第二列大于 0.5 为正类,小于等于 0.5 为负类,也可以用 argmax 取最大概率对应的列下标,两者在二分类下等价。
4.5 依赖安装与版本匹配
requirements.txt里写的是项目运行所需的基础库。BERT 官方代码依赖 TensorFlow 1.x,这一点特别容易踩坑,TensorFlow 2.x 直接跑会报module 'tensorflow' has no attribute 'gfile'这类错误。
常见的处理方案是创建一个独立的虚拟环境,然后安装 TensorFlow 1.15 版本:
python -m venv bertenv source bertenv/bin/activate pip install tensorflow==1.15 pip install -r requirements.txt如果你只有 TensorFlow 2.x 的环境,也可以利用tf.compat.v1的兼容层,但需要在代码头部加入若干兼容性设置,改动面较大。对这个项目来说,装一个 1.15 的虚拟环境是最省事的路线。
5. 避坑实录:我在复现过程中遇到的五个实际问题
5.1 报错:ModuleNotFoundError: No module named 'tensorflow.contrib'
- 现象:运行
run_classifier.py时,import 阶段就报错找不到tensorflow.contrib。 - 原因:TensorFlow 2.0 及以上版本移除了
contrib模块,而 BERT 官方代码大量依赖tf.contrib里的 API。 - 解决:创建一个 Python 3.6 或 3.7 的虚拟环境,安装
tensorflow==1.15。如果受限于 conda 环境切换不便,也可以考虑用tensorflow.compat.v1模式,但 1.x 环境的兼容性最好。
5.2 报错:KeyError: 'setiment'或ValueError: Task not found
- 现象:启动训练时提示任务名找不到。
- 原因:
--task_name的参数和run_classifier.py里注册的处理器名字对不上。项目自定义的处理器可能注册为"sentiment",而你传的是"setiment",或者反过来。 - 解决:打开
run_classifier.py,找到processors = {"sentiment": SentimentProcessor()}之类的字典定义,把--task_name改成字典里实际存在的键名,保持完全一致,区分大小写。
5.3 训练时 GPU 显存溢出(OOM)
- 现象:程序运行后不久报
ResourceExhaustedError,提示显存不足。 - 原因:
train_batch_size和max_seq_length设置的组合超出 GPU 显存容量。BERT-base 的显存占用和这两个参数近似线性相关。 - 解决:先把
train_batch_size从 32 降到 16,如果还溢出就降到 8。同时可以把max_seq_length从 128 降到 64 试一下,但要注意文本截断带来的信息损失。如果使用 GPU 服务器,可以通过nvidia-smi实时监控显存占用。
5.4 预测结果全是一个类别
- 现象:训练完成后,预测文件里的概率分布几乎都是同一个类别的概率更高,模型完全失效。
- 原因:最常见的原因是训练数据里正负样本严重不均衡,负样本数量远超正样本,模型学会了“全预测为多数类”这种偷懒策略。另一个可能原因是
dev.csv和train.csv数据划分时没有打乱,导致训练集里只有一个类别。 - 解决:用
divide.py重新划分数据,并检查每个文件里的类别分布。我的做法是在划分前先执行一次df['label'].value_counts(),确认原始数据正负样本比例在 4:1 到 1:4 之间再继续往下走。
5.5 CSV 文件中文乱码或读取行数异常
- 现象:用 Excel 打开
dev.csv中文显示乱码,或模型读取数据时报解析错误。 - 原因:文件保存时用了
utf-8编码,但 Excel 默认按ANSI或GBK打开。或者反过来,数据的实际编码和run_classifier.py里tf.gfile.Open读取时指定的编码不一致。 - 解决:用文本编辑器(如 VS Code)打开 CSV 文件,确认右下角显示的编码格式。如果代码里没指定编码,默认按系统区域设置解析,中文 Windows 下是
GBK。我的习惯是在divide.py输出时统一加encoding='utf-8',同时在run_classifier.py的读取处也用 UTF-8,彻底告别乱码问题。
6. 进阶玩法:从二分类扩展到多分类与快速验证
项目默认是情感二分类,但毕设答辩环节经常会被问“能不能做多分类”。这个改动其实不复杂,核心就三处,我可以直接给出来参考。
第一处是数据标注。把label从 0/1 改成 0/1/2 或多个类别,比如把微博情感分成“喜悦、愤怒、悲伤、中性”四类。第二处是run_classifier.py里的SentimentProcessor,把get_labels()函数的返回值从["0", "1"]改成["0", "1", "2", "3"]。第三处是模型输出的 logits 维度会自动变成类别数,不需要额外改网络结构。
def get_labels(self): """多分类时按需求返回全部类别""" return ["0", "1", "2", "3"]如果想把模型部署成 HTTP 接口,项目里的svr目录和bertsvr.sh就是预留的服务化方案。基本流程是:先freeze_graph.py把训练好的 checkpoint 冻结成单个 pb 文件,再用 TensorFlow Serving 或 Flask 封装预测函数。冻结命令大致如下:
python freeze_graph.py \ --bert_config_file=./chinese_L-12_H-768_A-12/bert_config.json \ --init_checkpoint=./output/model.ckpt-7500 \ --output_dir=./serving_model快速验证模型效果有一个很实用的技巧:直接用训练时生成的dev.csv抽取前 20 条,手动构造一个sample_text.txt,每行一句话,然后用extract_features.py提取[CLS]向量,看不同类别样本的向量距离。如果同类样本的向量明显聚在一起,说明模型学到的语义区分度足够好。
python extract_features.py \ --input_file=./sample_text.txt \ --output_file=./output_features.jsonl \ --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-7500 \ --layers=-1这个验证方式不依赖标签,只依赖模型内部的语义表示,用来排查“模型是不是在瞎猜”特别有效。
我自己的习惯是拿到这类项目后,先跑通默认配置,再刻意改坏一处参数看报错现象,比如把task_name改错、把batch_size调大,确保自己对错误模式有感知。从那以后我每次接手新的 BERT 项目,都会强制走一遍这套流程:确认数据分布、核对处理器注册名、检查预训练权重三件套、最后才是启动训练,这四步做完基本不会翻车。希望这份拆解能帮你少走弯路,顺利把毕设跑起来。
本文还有配套的精品资源,点击获取