资讯详情

基于Keras-bert的BERT微调实战:多标签文本分类与阈值调优

📅 2026/10/5 7:28:17 | 华诺云谱 👁 阅读
基于Keras-bert的BERT微调实战:多标签文本分类与阈值调优
简介一套基于Keras和Keras-bert的文本多标签分类项目面向NLP学习与项目实践者演示如何对BERT进行微调来完成多标签分类任务。项目选用2020语言与智能技术竞赛事件抽取数据作为样例将事件抽取问题转化为多标签分类适合希望借助预训练模型解决实际标签场景的读者。压缩包共10个文件大小约1.01MB其中4个Python脚本分别负责模型训练、评估、预测与FGM对抗训练2个CSV文件为训练集和测试集另有中文BERT词表、依赖说明及README文档目录结构清晰。目前已有1634人学习。通过此项目可掌握Keras-bert调用流程、多标签分类的标签处理与评估细节同时了解FGM对抗训练如何增强模型鲁棒性是一份可复用性强的文本分类参考实现。1. 多标签文本分类的实战入口用 Keras-bert 对 BERT 做微调先把一个难点说清楚多标签文本分类是 NLP 项目里踩坑率最高的任务形态之一。拿 2020 语言与智能技术竞赛事件抽取任务来说一条新闻报道经常同时包含“产品发布”和“高管变动”两种事件如果拿单标签分类模型硬套模型会在标签共现样本上直接翻车。这个项目用 Keras 和 Keras-bert 实现文本多标签分类任务核心是加载中文 BERT 预训练权重做微调在[CLS]向量上接一个全连接层输出端用 sigmoid 激活做多标签预测。适合你的场景是需要快速跑通 BERT 微调、样本本身有标签共现、又不想切换到 transformers 全家桶的情况。下面我按文件结构、训练脚本、踩坑记录到阈值调优的顺序把整个跑通过程拆开。2. 把 BERT 微调搬到 Keras 生态Keras-bert 的运行机制与工程选型2.1 为什么选 Keras-bert 而不是 transformers如果你习惯管理层维护的 transformers 库第一次看到 Keras-bert 可能会觉得它够“裸”没有Trainer没有一键pipeline甚至文档都停留在几年前的风格。但恰恰是这种裸让它在多标签微调任务里更好控制。Keras-bert 本质上就是一组 Keras 层往外拼成一个完整 BERT 模型load_trained_model_from_checkpoint加载后直接返回 Keras 模型对象。这意味着你可以把它嵌进keras.Model、Sequential也可以随时借用 Keras 回调、自定义 loss、对抗训练这些成熟机制。transformers 在 TF 2.x 里也能做到这些但老项目里如果 TensorFlow 和 Keras 版本被锁住升降级往往会拖出更多兼容问题。我一般只在三种情况下优先选 Keras-bert一是手头代码本来就基于 Keras 写的迁移成本最低二是要频繁访问中间层权重做 FGM 这类对抗训练Keras-bert 的权重命名很规整好过滤三是离线环境里只给了 Keras 生态的依赖清单不想再引入 transformers 那套依赖树。踩坑方面要提一句这个库已经不活跃更新你在网上搜到的多数教程都是 TF 1.x Keras 2.3 时代的产物。拿最新版 TF 直接跑大概率会撞上兼容性问题这我在第 4 章里专门展开讲。2.2 中文 BERT 怎么被 Keras-bert 吃进去tokenizer 与 checkpoint 的依赖链项目里默认的中文预训练模型是chinese_L-12_H-768_A-12也就是 BERT-base 中文版12 个 Transformer 编码层、隐藏层 768 维、12 个注意力头参数量在 1.1 亿上下。这个模型文件名里的L-12_H-768_A-12就是它的三个关键结构参数训练脚本里配置的seq_len决定了输入序列长度不影响预训练权重结构。这个目录里vocab.txt只是词表负责把文本映射成 token id。Keras-bert 加载权重时实际需要三件套文件作用缺失后果bert_config.json模型结构配置包含层数、隐藏层维度、注意力头数模型无法初始化bert_model.ckpt预训练权重训练时从这里做微调起点模型加载直接报错vocab.txt中文词表Tokenizer 依赖它做切分tokenizer 无法初始化一段文本要喂给 BERT不能直接把字符串丢进去。Keras-bert 的Tokenizer会先按 WordPiece 机制切词中文基本是逐字切再把切好的 token 转成两个数组一个存 token id 序列input_ids一个存段落 id 序列segment_ids。第一个 token 固定是[CLS]句尾补[SEP]不足长度补[PAD]。多标签分类中我们最后只取[CLS]位置的输出向量作为整句话的语义表示。这里有个关键点训练和预测时必须使用同一套 tokenizer 和同一个seq_len否则 token 对齐就乱掉。我见过不少人训练时seq_len256预测时图省内存改成 128导致维度直接冲突或者静默截断后面第 4 章会讲这个坑。2.3 项目文件清单与职责边界解开压缩包后先不要急着跑model_train.py。我建议先对照文件清单过一遍明确每个脚本的输入和输出不然训练到一半发现数据文件路径不对心态容易崩。文件/目录职责model_train.py主训练脚本加载 BERT 权重、数据、训练并保存 h5 模型model_predict.py推理脚本加载训练好的 h5 模型做多标签预测model_evaluate.py评估脚本计算准确率、召回率、F1 等指标FGM.pyFast Gradient Method 对抗训练实现提升泛化能力data/train.csv训练语料格式一般是一列文本、一列多标签data/test.csv测试语料用于最终评估chinese_L-12_H-768_A-12/vocab.txt词表文件requirements.txt依赖版本锁定文件README.md使用说明单看文件布局这是个很标准的 Keras 微调项目数据、训练、评估、预测四个环节分离。model_train.py是核心入口model_predict.py和model_evaluate.py都依赖它产出的 h5 文件。你拿到压缩包后要特别确认一件事chinese_L-12_H-768_A-12目录下面是否只有vocab.txt。如果只有词表那bert_config.json和bert_model.ckpt需要自己去下载补全这是这个项目最容易卡住的第一步。3. 训练脚本实操从 train.csv 到 model_train.py 的参数体系3.1 数据格式与多标签编码项目用到的数据是 2020 语言与智能技术竞赛事件抽取任务里的那批语料。官方任务本身是个结构抽取任务需要抽事件类型、触发词、论元但如果你只想判断“这句话涉及哪些事件类型”就可以把它简化为一个多标签分类任务来建模。train.csv的常见格式是两列一列存文本一列存标签多个标签之间用符号隔开。先写一段数据预处理代码把标签集合收集起来再把每行样本转成 multi-hot 编码import pandas as pd import numpy as np df pd.read_csv(data/train.csv, encodingutf-8) df df.dropna(subset[text, label]) # 收集全部标签构造 id 映射 label_set set() for labels in df[label].str.split(,): label_set.update(labels) label2id {label: i for i, label in enumerate(label_set)} id2label {i: label for label, i in label2id.items()} num_labels len(label_set) print(f标签数量: {num_labels}) # 把每行的标签列表转成 multi-hot 向量 def encode_labels(label_str, label2idlabel2id, num_labelsnum_labels): vec np.zeros(num_labels, dtypenp.float32) for label in label_str.split(,): label label.strip() if label in label2id: vec[label2id[label]] 1.0 return vec df[label_vector] df[label].map(encode_labels) print(df.head(3))这段代码做的事很简单先遍历全部数据收集标签集合再为每个标签分配一个整数 id最后把每行标签串转成固定长度的 0/1 向量。注意label2id的构建一定要在全局数据上做不能只在一部分样本上做否则训练集和验证集会因为标签空间不一致直接崩掉。num_labels就是模型输出层的神经元个数。这个数字直接在model_train.py里被使用所以如果你换了自己的数据集标签数量变了这一处必须跟着改。3.2 model_train.py 的主干流程整个训练脚本的核心其实不长。加载 BERT 预训练模型、接上多标签输出层、用二分类交叉熵训练。下面是主干代码实际项目里文件结构可能稍有出入但骨架基本一致import numpy as np import keras from keras import layers, optimizers from keras_bert import load_trained_model_from_checkpoint, Tokenizer seq_len 128 config_path ./chinese_L-12_H-768_A-12/bert_config.json checkpoint_path ./chinese_L-12_H-768_A-12/bert_model.ckpt vocab_path ./chinese_L-12_H-768_A-12/vocab.txt tokenizer Tokenizer(vocab_path) # 加载预训练 BERTtrainingTrue 是关键 bert_model load_trained_model_from_checkpoint( config_path, checkpoint_path, seq_lenseq_len, trainingTrue ) # 两个输入token ids 和 segment ids x1_in layers.Input(shape(seq_len,), nameinput_indices) x2_in layers.Input(shape(seq_len,), nameinput_segments) x bert_model([x1_in, x2_in]) # 只取 [CLS] 向量作为整句表示 cls_out layers.Lambda(lambda t: t[:, 0, :], namecls_extract)(x) cls_out layers.Dropout(0.1)(cls_out) # 多标签输出层sigmoid 每个维度独立判断 pred layers.Dense(num_labels, activationsigmoid)(cls_out) model keras.models.Model([x1_in, x2_in], pred) model.compile( optimizeroptimizers.Adam(learning_rate2e-5), lossbinary_crossentropy ) model.summary()这里有两个必须理解的细节。第一load_trained_model_from_checkpoint里的trainingTrue表示加载后以训练模式运行模型批归一化和 Dropout 行为是训练模式如果这里写成False训练时前面的 BERT 层就不会正常更新甚至会出现验证集正常但训练集 loss 不降的诡异现象。第二Lambda(lambda t: t[:, 0, :])取的是第一个 token也就是[CLS]位置的向量。BERT 输入的每个 token 都对应一个 768 维输出整句输入会产生seq_len × 768的一个矩阵。分类任务里我们不需要每个 token 的输出只要这个代表全句语义的向量。输出层用 sigmoid 而不是 softmax这是多标签分类和单标签分类的本质区别。softmax 强制所有类别的概率之和为 1多标签场景下一句话可以同时属于多个事件类型几个概率同时接近 1 才是对的所以每个输出节点必须独立判断。3.3 参数怎么调batch_size、seq_len、学习率、warmupBERT 微调不像训练普通神经网络那样随意。预训练权重已经很接近任务需要的语义空间学习率开大一点就可能把学到的信息冲毁。项目里几个关键参数的合理范围如下参数常见范围调参说明seq_len128 / 256事件抽取数据通常句子不长128 足够如果文本是长段落可以上调到 256但显存占用会明显增加batch_size16 / 32显存够用就 32小于 16 时梯度噪声偏大收敛不稳定learning_rate1e-5 ~ 3e-5BERT 微调推荐范围超过 5e-5 容易掉点dropout0.1只加到分类头BERT 内部 keep 默认epochs3 ~ 5小数据集 3 轮基本够多了容易过拟合warmup前 10% 步数warmup 让学习率从小步长升到大步长前期不冲垮权重我一般把学习率固定在 2e-5batch_size 优先取 32如果显存不够先降 batch 而不是降seq_len。seq_len下调会直接截断文本丢掉事件触发的关键上下文对多标签分类影响很直接。每轮训练结束一定要保存模型权重文件。建议用 Keras 的ModelCheckpoint回调按验证集 F1 或 loss 保存最优权重别只保存最后一轮。多标签任务里最后一轮经常不是最优尤其是训练后期已经过拟合这点我在第 5 章还会提到。3.4 把 FGM 对抗训练接到微调流程里这个项目单独放了一个FGM.py说明作者是在认真做泛化能力不是简单跑通就完事。FGMFast Gradient Method的思路很直观在 embedding 层加上一个很小的扰动方向让模型在扰动后的样本上也算一次 loss相当于引入对抗性干扰让模型学得更稳。常见实现是把扰动加到 embedding 权重上前向算 loss反向更新一次梯度然后恢复原权重class FGM: def __init__(self, model): self.model model self.backup {} def attack(self, epsilon0.5, emb_nameembedding): for name, weight in self.model.trainable_weights: if emb_name in name: self.backup[name] weight.value() grad weight.gradient if grad is not None: weight.assign(weight - epsilon * grad / (np.linalg.norm(grad) 1e-12)) def restore(self): for name, weight in self.model.trainable_weights: if name in self.backup: weight.assign(self.backup[name]) self.backup.clear()训练循环里配合使用fgm FGM(model) for batch in data_generator: x_indices, x_segments, y_true batch # 正常训练一步 loss model.train_on_batch([x_indices, x_segments], y_true) # 加扰动再训练一步 fgm.attack(epsilon0.5) loss_adv model.train_on_batch([x_indices, x_segments], y_true) fgm.restore()epsilon0.5是扰动幅度值太大容易把权重带歪太小起不到对抗作用。实际操作中可以先从 0.3 试起观察验证集 F1 有没有提升没有明显提升就调小。需要注意emb_name的过滤规则要匹配 Keras-bert 里 embedding 层的真实命名我一般会在训练前打印model.trainable_weights看一下实际层名再去设置过滤条件而不是盲写embedding。4. BERT 微调避坑排查五个我在现场踩过的坑4.1 坑一加载预训练权重后训练loss 不降还掉点现象是model_train.py跑起来了第一轮训练 loss 从初始值开始不降反升验证集指标比随机预测还差。原因load_trained_model_from_checkpoint里的training参数没设成True。这个参数控制 BERT 内部的 Dropout 和 LayerNormalization 是训练模式还是推理模式如果训练时误用了trainingFalse整个模型像在推理模式下做训练梯度能反向传播但归一化统计量不会正确更新训练就废了。解决加载时显式传trainingTrue保存模型后推理时再传trainingFalse。我在model_predict.py里也会重新加载一遍模型结构保证推理模式正确。4.2 坑二验证集所有标签全预测成 0现象是训练几轮后 loss 已经降到 0.2 以下但把所有验证样本喂给模型输出概率都小于 0.5最终预测全是 0。原因多标签数据里正样本比例通常很低。事件抽取数据中某一种事件类型可能在 100 条里只有 5 条负样本太多了模型学到最后倾向于所有输出都向着 0 收敛0.5 这个默认阈值在很多类别上根本不可用。解决不要拘泥于 0.5 阈值。模型输出是连续概率值先保存概率矩阵再在验证集上做阈值网格搜索找一个能让 F1 最优的阈值。这个内容我在第 5 章会单独给一段代码。应急办法是给 loss 加class_weight把正样本权重调高但这只能缓解解决不了阈值适配问题。4.3 坑三keras_bert 和 Keras/TensorFlow 版本冲突compile 直接报错现象是执行到model.compile或者model keras.models.Model(...)时报错常见有AttributeError: module keras has no attribute layers或者TypeError: call() missing 1 required positional argument。原因keras_bert 最后一次活跃更新停留在 TF 1.x/2.0 时代后续 TF 版本调整内部 APIkeras.layers的导入路径和Model初始化方式都变了新版兼容直接断掉。解决严格按requirements.txt锁定的版本安装依赖。我这边最稳的组合是tensorflow1.15keras2.3.1keras_bert0.8.3。如果必须在 TF 2.x 下跑建议装tensorflow-gpu2.2左右的老版本并保证tf.keras和独立keras不要混用混用会出现模型参数放不到同一设备上的诡异问题。4.4 坑四预测结果整体错位类别排序完全不对现象是model_predict.py跑完输出概率和标签顺序对不上比如id2label明明是从train.csv构建的预测脚本里却用了另外一套索引。原因训练时构建了一次label2id预测时又重建了一套两套映射没有做序列化保存。一旦类别的枚举顺序改变同一个位置的输出对应的就不是同一个标签了。解决训练完把label2id和id2label存成 JSON 文件model_predict.py启动时直接读这个文件禁止二次重建。这个文件是所有下游推理的依赖丢了就只能重训。4.5 坑五chinese_L-12_H-768_A-12 目录缺少权重文件训练直接跑死现象是从压缩包解压后直接跑model_train.py报出FileNotFoundError: bert_model.ckpt或bert_config.json不存在的错误。原因项目压缩包里chinese_L-12_H-768_A-12目录下通常只放了vocab.txt因为模型权重文件太大不适合直接打包分享。补全权重文件这一步被很多人默认成了“项目自带”结果卡在第一步。解决先检查这个目录下有没有bert_model.ckpt、bert_config.json、vocab.txt三个文件。缺前两个就去下载完整的 BERT 中文预训练模型包解压后放回对应目录。下载完再看一眼bert_config.json里的hidden_size是不是 768跟model_train.py里seq_len无关但和 BERT 内部维度直接相关错了会在 load 时直接报 shape mismatch。5. 阈值调优与验证技巧把 model_evaluate.py 变成多标签决策器训完模型不代表事情结束了多标签分类的最后一步本来是调阈值。很多文本分类项目评估只看 accuracy但对多标签任务来说这指标没有意义如果一句话同时有 3 个真实标签模型预测出其中 2 个accuracy 会判它是错的但实际业务里能召回 2 个已经很有用。所以我习惯在model_evaluate.py里先算全概率矩阵再做阈值扫描。import numpy as np from sklearn.metrics import f1_score # y_true: one-hot 编码的真实标签shape 为 (n_samples, n_labels) # y_pred_prob: 模型输出的概率矩阵shape 与 y_true 相同 best_f1 0.0 best_threshold 0.5 for threshold in np.arange(0.25, 0.85, 0.05): y_pred_bin (y_pred_prob threshold).astype(int) micro_f1 f1_score(y_true, y_pred_bin, averagemicro) if micro_f1 best_f1: best_f1 micro_f1 best_threshold threshold print(f最优阈值: {best_threshold}, 最优 Micro-F1: {best_f1:.4f})averagemicro把所有类别样本汇总后计算 F1适合事件抽取这种各类别样本数差距悬殊的场景如果你的业务更关注每个事件类型都要独立可用可以同时看一眼averagemacro两者差异大的时候说明某几个类别被模型严重忽略了。还有一种容易被忽略的验证技巧分类阈值扫描结果不能只保存一次换训练轮次后阈值大概率会变。我通常把每轮的验证概率矩阵都存下来训练结束后统一做阈值搜索最后把最优阈值写进model_predict.py。基于概率阈值算出的结果再做一次事件类型维度的汇总分析比如哪些类型总是被同时预测出来哪些类型被漏掉这个分析能反过来指导你是否需要增加训练样本。顺便说一句model_evaluate.py输出的指标里如果 Macro-F1 和 Micro-F1 差距大于 0.2基本可以断定少数类被模型放弃了这时候优先考虑的核对该类别的训练样本量其次再考虑换阈值。数据层面没有修好阈值再扫都没有用。多标签分类里阈值和标签映射是我每次交付前必查的两项。从那以后我每次跑多标签项目都会强制走一遍完整流程先做全局阈值扫描再确认阈值持久化到推理脚本最后核对标签映射文件版本。规则听起来简单但它确实是这类项目最容易出错的地方。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑