
简介本资源为基于BERT模型的中文文本情感二分类完整项目源码面向计算机相关专业正在做毕设的学生、需要项目实战练习的学习者也可作为课程设计或期末大作业参考。项目经导师指导并认可评审分98分属于高分设计项目帮助读者快速掌握预训练模型在中文情感分析任务中的落地流程。压缩包共40个文件约22.84MB包含15个Python源码文件、5个CSV数据集、4个XML配置、3个TXT与3个Markdown说明文档、2个Jupyter Notebook及BERT预训练权重相关文件覆盖数据处理、模型训练、特征提取与分类预测等环节。已有296人学习下载。读者可获得完整的BERT中文情感二分类实现方案包括训练与测试脚本、微博与疫情等多场景语料、项目说明文档及运行脚本便于理解模型微调、结果评估与工程目录组织适合作为自然语言处理方向的实战参考。1. 拆开这个 BERT 中文情感二分类包它到底能不能跑通你的毕设如果你正在做中文文本情感二分类的毕设或课程设计大概率会遇到一个尴尬局面用 LSTM 或 TextCNN 跑出来准确率卡在 85% 上下答辩时老师一句「为什么不用预训练模型」就把你问住了。这个包就是冲着这个场景来的——它把 Google 开源的chinese_L-12_H-768_A-12预训练权重、BERT 官方 TF 版代码、以及四份中文情感数据集weibo.csv、yiqing.csv、dev.csv、train.csv打包在一起配合run_classifier.py和modeling.py就能完成从数据读取到情感二分类的全流程。它适合两类人一是需要一份能跑通、能改、能写进论文的中文情感分类基线二是想搞明白 BERT 微调到底改了哪些层、bert_config.json里那些参数怎么影响显存和精度的实战学习者。评审 98 分不是重点重点是这份源码把「预训练权重 下游分类头」这条链路完整暴露出来了你能看到每一层张量的形状变化而不是调个 API 就完事。2. 环境与目录把 chinese_L-12_H-768_A-12 权重喂进 run_classifier.py2.1 为什么这个包选 TF 1.x 而不是 PyTorch打开requirements.txt和run_classifier.py的 import 段你会发现它依赖的是tensorflow而不是torchmodeling.py里全是tf.variable_scope、tf.layers这类 TF 1.x 风格的写法。这不是作者偷懒而是因为 Google 官方发布的chinese_L-12_H-768_A-12权重本身就是 TF checkpoint 格式bert_model.ckpt.data-00000-of-00001、.index、.meta三件套配套的run_classifier.py、optimization.py、tokenization.py也都是官方 TF 版代码。如果你硬要用 PyTorch 的transformers库加载得先做权重转换多一道工序。所以这个包的定位很明确用官方 TF 代码 官方中文权重跑通最原汁原味的 BERT 微调。常见做法是装 TF 1.15最后一个支持 1.x API 的版本Python 用 3.7因为 TF 1.15 对 3.8 支持不完整装的时候容易在tensorflow-estimator上翻车。2.2 目录结构里哪些文件是必须的哪些可以删这个包文件不少但真正跑分类任务的核心链路只有几个。我按「必须保留 / 可删 / 参考用」分一下文件/目录作用是否必须chinese_L-12_H-768_A-12/预训练权重 bert_config.jsonvocab.txt必须run_classifier.py分类任务主入口定义 Processor 和训练循环必须modeling.pyBERT 模型结构定义必须optimization.pyAdamWeightDecayOptimizer微调专用优化器必须tokenization.py中文按字切分 FullTokenizer必须data/下 csv训练/验证/测试数据必须run_pretraining.py、create_pretraining_data.py预训练用分类任务用不到可删run_squad.py、extract_features.py阅读理解/特征提取与本任务无关可删*.ipynb演示 notebook参考用bert_config.json里hidden_size768、num_hidden_layers12、num_attention_heads12、vocab_size21128这四个数字决定了模型参数量约 1.1 亿。vocab.txt是中文按字级别的词表21128 个 token所以中文不需要分词直接按字喂进去就行——这也是为什么tokenization.py里FullTokenizer对中文的处理是逐字切分。2.3 从零把环境跑起来的具体命令先建虚拟环境别在系统 Python 里装 TF 1.15否则和你机器上其他项目冲突的概率极高# 建一个 Python 3.7 的虚拟环境TF 1.15 对 3.7 最友好 conda create -n bert_cls python3.7 -y conda activate bert_cls # 装 TF 1.15指定 CPU 版即可GPU 版要匹配 CUDA 10.0 pip install tensorflow1.15.0 # 装其余依赖numpy 别装太新1.19 以上会和 TF 1.15 打架 pip install numpy1.18.5 pandas1.1.5 scikit-learn0.24.2装完验证一下 TF 能不能正常 import并且确认没有报FutureWarning之外的错误import tensorflow as tf print(tf.__version__) # 期望输出 1.15.0 # 关掉 TF 1.x 那堆冗余日志后面训练时清爽很多 import os os.environ[TF_CPP_MIN_LOG_LEVEL] 2TF_CPP_MIN_LOG_LEVEL这个环境变量建议在训练脚本开头就设TF 1.x 默认会把每个 op 的分配信息都打出来几千行日志里找 loss 曲线非常痛苦。设成2只保留 warning 和 error设成3连 warning 都屏蔽。2.4 数据格式与 Processor 的对应关系run_classifier.py里定义了一个DataProcessor基类你需要为情感二分类写一个子类实现get_train_examples、get_dev_examples、get_labels四个方法。这个包里data/下的 csv 通常是text,label两列label 是 0/1。常见做法是直接继承DataProcessor把 csv 读成InputExample列表import csv from run_classifier import DataProcessor, InputExample class SentimentProcessor(DataProcessor): def _read_csv(self, path): examples [] with open(path, r, encodingutf-8) as f: reader csv.reader(f) next(reader) # 跳过表头 for row in reader: if len(row) 2: continue examples.append(InputExample( guidNone, text_arow[0], text_bNone, labelrow[1])) return examples def get_train_examples(self, data_dir): return self._read_csv(f{data_dir}/train.csv) def get_dev_examples(self, data_dir): return self._read_csv(f{data_dir}/dev.csv) def get_labels(self): return [0, 1] # 二分类标签顺序要和 csv 里一致InputExample的text_a是主句text_b用于句对任务比如 NLI情感分类只填text_a。get_labels返回的列表顺序决定了label_map的映射如果 csv 里标签是「正面/负面」这种字符串这里就要对应改成[正面, 负面]否则convert_single_example里会抛KeyError。这个坑我在第一次跑的时候踩过日志里只报KeyError: 1不告诉你哪一行排查了半天。3. 微调参数怎么设max_seq_length、batch_size 与学习率的三角关系3.1 max_seq_length 不是越大越好run_classifier.py的main函数里有一堆tf.flags其中max_seq_length默认 128。中文按字切分后一条微博评论通常 3060 字128 完全够用。但如果你把max_seq_length调到 512显存占用会从约 4GB 涨到 12GB 以上因为 BERT 的注意力矩阵是seq_len × seq_len512 的平方是 128 的 16 倍。更关键的是超过 128 之后大部分位置都是[PAD]这些 padding 也参与注意力计算等于白烧显存。我的建议是先统计你数据里文本长度的 95 分位数然后取一个比它略大的 2 的幂次。比如 95 分位是 90 字就设 128是 200 字就设 256。# 统计训练集文本长度分布决定 max_seq_length import pandas as pd df pd.read_csv(data/train.csv) lengths df.iloc[:, 0].astype(str).str.len() print(lengths.describe(percentiles[0.5, 0.9, 0.95, 0.99])) # 如果 95 分位在 100 以内max_seq_length128 就是最优解3.2 batch_size 与学习率的联动BERT 微调的经典配置是batch_size32、learning_rate2e-5、num_train_epochs3。这三个数不是随便定的学习率太大比如 1e-3会让预训练权重被冲垮loss 直接飙到 nan太小比如 1e-6则 3 个 epoch 根本学不动验证集准确率纹丝不动。batch_size和learning_rate要联动——如果你显存不够只能开到 16学习率可以适当降到 1e-5因为小 batch 的梯度噪声更大大学习率容易震荡。# 单卡 CPU 训练命令GPU 把 use_tpu 保持 False 即可 python run_classifier.py \ --task_namesentiment \ --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./outputinit_checkpoint指向bert_model.ckpt时不要带.index或.data-00000-of-00001后缀TF 会自动找齐三件套。output_dir里会生成model.ckpt和eval_results.txt后者记录准确率、精确率、召回率、F1。如果do_train和do_eval同时为 true训练结束后会自动跑一次验证。3.3 学习率预热与衰减策略optimization.py里的AdamWeightDecayOptimizer实现了权重衰减但学习率预热是在run_classifier.py的create_optimizer里手动做的前 10% 的 step 线性升温之后线性衰减到 0。这个策略对 BERT 微调很重要因为一开始就用 2e-5 会让预训练好的 attention 权重被大幅更新反而破坏语言知识。如果你发现训练前 100 步 loss 下降很慢别急着调大学习率先确认预热有没有生效——看global_step和learning_rate的打印值前 10% 应该从接近 0 慢慢升到 2e-5。3.4 显存不够时的三个降级方案显存 OOM 是跑 BERT 最常见的翻车点。按优先级降级第一把max_seq_length从 128 降到 64显存直接省一半但要看你的文本长度分布是否允许第二把train_batch_size从 32 降到 16 或 8同时学习率降到 1e-5第三用gradient_accumulation模拟大 batch但官方 TF 代码没现成实现得自己改run_classifier.py里的梯度更新逻辑。我一般先试第一和第二个实在不行才动代码。4. 数据读取与中文分词tokenization.py 里那几个容易忽略的细节4.1 中文按字切分与 vocab.txt 的对应tokenization.py里的FullTokenizer对英文做 WordPiece对中文做逐字切分。vocab.txt里前 100 个是[PAD]、[UNK]、[CLS]、[SEP]、[MASK]这些特殊 token后面是常用汉字和英文子词。中文句子「今天心情很好」会被切成[CLS] 今 天 心 情 很 好 [SEP]每个字对应一个 id。如果遇到vocab.txt里没有的生僻字会映射到[UNK]所以如果你的数据里有大量网络新词或颜文字[UNK]比例会偏高影响效果。常见做法是统计一下[UNK]占比超过 5% 就要考虑扩充词表或做文本清洗。from tokenization import FullTokenizer tokenizer FullTokenizer(vocab_filechinese_L-12_H-768_A-12/vocab.txt) tokens tokenizer.tokenize(今天心情很好) print(tokens) # [今, 天, 心, 情, 很, 好] # 检查 UNK 比例 texts [今天心情很好, 这个产品太差了, ] for t in texts: toks tokenizer.tokenize(t) unk toks.count([UNK]) print(f{t} - UNK 数: {unk})4.2 convert_single_example 里的截断逻辑run_classifier.py的convert_single_example会把tokens_a和tokens_b拼起来加上[CLS]和[SEP]然后截断到max_seq_length。注意截断是从尾部截的如果text_a很长后面的内容会直接丢掉。对于情感分类关键情感词往往在句尾比如「但是质量很差」尾部截断可能把最关键的信息切掉。一个实用技巧是如果文本超长优先保留前max_seq_length - 2个 token但手动把最后 20 个字拼到前面或者做句子级切分后取多个片段分别预测再投票。4.3 标签映射与类别不平衡file_based_convert_examples_to_features会把label通过label_map转成 id。如果正负样本比例是 9:1模型会倾向于全预测多数类准确率看着有 90%但 F1 只有 0.47。这个包里yiqing.csv和weibo.csv的类别分布可能不同训练前一定要看一眼import pandas as pd for name in [train, dev, yiqing, weibo]: df pd.read_csv(fdata/{name}.csv) print(name, df.iloc[:, 1].value_counts(normalizeTrue).to_dict())如果发现不平衡可以在run_classifier.py的 loss 计算里给少数类加权或者用tf.data.Dataset的repeat和sample_from_datasets做重采样。官方代码没内置这些得自己加。5. 避坑与排查从 OOM 到 loss 不降的五个血泪记录5.1 现象训练一开始就 OOM报Resource exhausted: OOM when allocating tensor原因max_seq_length或batch_size设太大或者 GPU 上同时跑了别的进程。BERT base 在seq_len128、batch32时FP32 大约占 45GB 显存如果显卡只有 6GB加上 TF 的显存预分配策略很容易爆。解决先nvidia-smi看有没有残留进程然后按 3.4 节的顺序降max_seq_length和batch_size。另外可以在run_classifier.py开头加tf_config.gpu_options.allow_growth True让 TF 按需分配而不是一次性占满。5.2 现象loss 一直是 0.69 左右不降准确率 50%原因init_checkpoint路径写错或者bert_config.json和 checkpoint 不匹配导致 BERT 部分随机初始化等于从零训练一个 1.1 亿参数的模型3 个 epoch 根本不够。解决检查init_checkpoint是否指向bert_model.ckpt不带后缀并确认bert_config.json里的vocab_size和vocab.txt行数一致21128。可以在modeling.py的BertModel初始化后打印一下变量名确认bert/embeddings/word_embeddings被正确加载。5.3 现象验证集准确率比训练集还高原因数据泄露。train.csv和dev.csv可能有重复样本或者dev.csv是从train.csv里随机切出来的但没去重。解决训练前对train和dev做一次文本去重确保dev里的文本没在train里出现过。另外检查do_eval时用的eval_examples是不是真的来自dev.csv别不小心传了train.csv。5.4 现象预测结果全是同一个类别原因标签映射顺序错了或者get_labels返回的顺序和 csv 里的标签不一致。比如 csv 里 0 是负面、1 是正面但get_labels返回[1, 0]模型学到的概率会反过来。解决打印label_map确认映射关系并在预测后用np.argmax取 id 再映射回原始标签。5.5 现象tokenization.py报UnicodeDecodeError原因csv 文件编码不是 UTF-8可能是 GBK 或 UTF-8 with BOM。解决用chardet检测编码或者统一用pandas.read_csv(..., encodingutf-8-sig)读进来再存成 UTF-8。这个坑在 Windows 上尤其常见因为 Excel 导出的 csv 默认是 GBK。6. 进阶技巧用 predict 模式做单条推理与批量预测训练完拿到output/model.ckpt之后最实用的进阶用法是写一个独立的推理脚本不依赖run_classifier.py的训练循环。核心思路是重建BertModel和分类头加载 checkpoint然后对单条文本做convert_single_example并sess.run出 logits。下面是一个最小可用的推理脚本import tensorflow as tf from modeling import BertConfig, BertModel from tokenization import FullTokenizer from run_classifier import InputFeatures, convert_single_example # 1. 重建图结构必须和训练时一致 bert_config BertConfig.from_json_file(chinese_L-12_H-768_A-12/bert_config.json) input_ids tf.placeholder(tf.int32, [None, 128], nameinput_ids) input_mask tf.placeholder(tf.int32, [None, 128], nameinput_mask) segment_ids tf.placeholder(tf.int32, [None, 128], namesegment_ids) label_ids tf.placeholder(tf.int32, [None], namelabel_ids) model BertModel(configbert_config, is_trainingFalse, input_idsinput_ids, input_maskinput_mask, token_type_idssegment_ids) # 分类头取 [CLS] 位置的输出接一个全连接 output_weights tf.get_variable( output_weights, shape[2, bert_config.hidden_size], initializertf.truncated_normal_initializer(stddev0.02)) output_bias tf.get_variable(output_bias, shape[2], initializertf.zeros_initializer()) logits tf.matmul(model.get_pooled_output(), output_weights, transpose_bTrue) output_bias probs tf.nn.softmax(logits, axis-1) # 2. 加载训练好的 checkpoint saver tf.train.Saver() with tf.Session() as sess: saver.restore(sess, output/model.ckpt) tokenizer FullTokenizer(chinese_L-12_H-768_A-12/vocab.txt) # 3. 构造输入 text 这家餐厅的菜太难吃了不会再来了 tokens tokenizer.tokenize(text) tokens [[CLS]] tokens[:126] [[SEP]] ids tokenizer.convert_tokens_to_ids(tokens) mask [1] * len(ids) [0] * (128 - len(ids)) seg [0] * 128 ids ids [0] * (128 - len(ids)) # 4. 推理 p sess.run(probs, feed_dict{ input_ids: [ids], input_mask: [mask], segment_ids: [seg]}) print(f负面概率: {p[0][0]:.4f}, 正面概率: {p[0][1]:.4f})这段代码的关键点有三个第一is_trainingFalse必须设否则 dropout 会随机丢弃神经元每次预测结果都不一样第二get_pooled_output()返回的是[CLS]经过一层 tanh 后的向量形状[batch, 768]直接接全连接就是分类头第三saver.restore的路径要和训练时output_dir一致如果训练用了--do_train但没保存检查run_classifier.py里saver.save的调用位置。批量预测的话把单条文本换成列表循环构造input_ids即可但注意max_seq_length要统一短文本补[PAD]到 128。如果数据量大建议用tf.data.Dataset做 batch比手动循环快很多。我一般会在推理脚本里加一个--input_file参数读 csv 后逐行预测输出text, pred_label, prob三列方便后续做错误分析。从那以后我每次拿到一个新的 BERT 微调包都会先跑一遍train.csv的前 100 条确认 loss 在 10 步内开始下降再上全量数据——这个习惯帮我省下了至少三次通宵排查的时间。希望帮到你。本文还有配套的精品资源点击获取