BERT多标签专利分类实践:IPC标签筛选与微调
发布时间:2026/9/17 13:45:40 锦皓数字建站

简介一份基于预训练模型的多标签专利分类研究文档面向自然语言处理与专利文本挖掘方向的研究者系统阐述如何利用BERT、RoBERTa和RBT3预训练模型解决大规模专利自动分类问题。文档将分类粒度细化到IPC“小类”级别并通过高频标签筛选提升模型效果最终准确率达91.2%、Micro-F1值达71.7%实验设计完整。资源包仅有1个docx文件大小413KB内容涵盖数据集构建思路、模型微调策略、BCEWithLogitsLoss损失函数与Sigmoid激活函数的选择、评价指标设定及实验对比分析等核心环节适合作为多标签分类课题的参考案例或研究模板。该资源已被279人学习浏览可用于快速了解预训练模型在专利分类场景的落地方法。1. 从IPC分类号到多标签任务预训练模型凭什么能接住专利文本做过专利分析的人都知道IPC分类号不是一道单选题。一份专利申请可以同时落进G06F电数字数据处理和Y02E温室气体减排技术也可能横跨H04L与H04W两个通信大类。2020年国内专利申请量超过519万件审查员需要在七万多个IPC组别里做判断单靠人工阅读摘要和权利要求书效率和质量都难以兼顾。传统机器学习做法是先做词频统计、再上SVM或随机森林但专利文本里大量同义表达和长距离语义依赖让这类方法的准确率卡在瓶颈上。本文拆解的项目就是把BERT这类预训练模型搬到多标签专利分类场景标签取IPC分类号前4位小类级别使用Sigmoid做多标签激活、BCEWithLogitsLoss做损失函数在百万级中文专利语料上微调。最终通过高频标签筛选准确率做到91.2%Micro-F1到71.7%。这套方案值得借鉴的地方在于它把大而全的IPC体系压缩成可训练的标签空间同时给出了数据清洗、标签筛选、模型选型和评估的完整链路对做文本多标签分类的工程师来说有直接参考价值尤其是标签长尾和类别不均衡这两个老问题。2. 数据构建与标签压缩IPC前4位怎么变成训练目标2.1 为什么选IPC前4位而不是完整分类号IPC分类体系有部、大类、小类、大组、小组五级。部只有8个大类131个小类646个小组则超过七万个。如果直接用小组做标签绝大多数标签下样本数可能只有个位数预训练模型再强也学不出有效特征。用部或大类做标签又太粗分类结果对审查员的参考价值有限。论文选择前4位即小类级别是精度与数据密度之间的折中。具体来说一个IPC分类号如G06F17/30前4位是G06F其中G代表部、06代表大类、F代表小类。这样646个小类就是候选标签集合。但实际操作中从Google Patent抓取的中文专利不同小类下的文档数量差异极大。头部标签如G06F有数万篇文本尾部标签可能只有几十篇。直接用全部646个标签训练模型会把尾部标签几乎全部预测为负例Micro-F1被拖垮。2.2 数据集构建与高频标签筛选项目从Google Patent采集了2018至2021年的中文专利保留标题、摘要和分类号信息共103万份文档构成CNPatents-Large另取约40万份构成CNPatents-Small。训练集与测试集按82切分。每篇专利的标签是分类号前4位去重后的集合一篇专利可以对应多个小类标签。清洗流程中有一个关键操作统计每个标签下的样本数保留排名前30的标签作为高频标签集合其余标签对应的样本从训练集中剔除。注意这里是按标签筛不是按文档筛也就是说一篇文档如果有多个标签其中只要有一个属于高频标签集合这篇文档就保留但标签列表只保留高频部分。筛选后CNPatents-Large剩下68.5万篇CNPatents-Small剩下31.4万篇。import pandas as pd from collections import Counter # 假设 raw_data 是 DataFrame包含 title, abstract, ipc_list 三列 # ipc_list 是字符串形式的列表如 [G06F, H04L, Y02E] def filter_high_freq_labels(df, top_k30, min_count100): # 统计每个标签的文档数 label_counter Counter() for labels in df[ipc_list]: label_set set(labels.strip([]).replace(, ).split(, )) label_counter.update(label_set) # 筛选文档数 min_count 且排名前 top_k 的标签 freq_labels [label for label, cnt in label_counter.most_common(top_k) if cnt min_count] # 只保留包含高频标签的文档并将标签列表裁剪为高频部分 def keep_high_freq(labels): label_set set(labels.strip([]).replace(, ).split(, )) return list(label_set set(freq_labels)) df[filtered_labels] df[ipc_list].apply(keep_high_freq) df df[df[filtered_labels].map(len) 0] return df, freq_labels df_train, freq_labels filter_high_freq_labels(raw_data) print(f高频标签数量: {len(freq_labels)}) print(f筛选后样本数: {len(df_train)})这段代码做的事情是先遍历所有文档的标签集合做计数取数量排名前30且超过100篇的标签作为保留集合。然后逐篇检查文档的标签是否有交集有则保留并把标签列表裁剪成交集部分。要注意filtered_labels为空的行必须丢弃否则模型会拿到空标签样本BCEWithLogitsLoss计算时所有维度都是0相当于告诉模型这篇文档不属于任何类别会引入噪声。筛选后的标签分布在CNPatents-Large上呈现明显的长尾G06F有43183篇G06K有26263篇而尾部如G02B只有6189篇头部与尾部相差约7倍。这个比例虽然还是不均衡但至少每个标签都有足够样本让模型学到可泛化的特征。相比之下不筛选直接训练时大量标签下样本数不足100模型在这些类别上的预测几乎等于随机猜测。2.3 文本截断与序列长度控制专利摘要通常比新闻标题长得多BERT类模型默认最大序列长度是512。项目把标题和摘要拼接后统一截断到200个token即MAX_LEN200。这里有一个细节截断策略不是从头截而是保留开头和结尾。因为专利摘要的开头通常交代技术领域结尾说明技术效果中间是具体实现方案。如果直接从头截断200个token可能丢失技术效果信息如果只保留前128和后72信息完整性更好。from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(hfl/chinese-bert-wwm-ext) def truncate_text(text, max_len200): tokens tokenizer.tokenize(text) if len(tokens) max_len - 2: # 预留 [CLS] 和 [SEP] return tokens # 保留开头 3/4 和结尾 1/4 head_len int((max_len - 2) * 0.75) tail_len (max_len - 2) - head_len return tokens[:head_len] tokens[-tail_len:] sample_tokens truncate_text(本发明公开了一种基于深度学习的文本分类方法...) print(tokenizer.convert_tokens_to_ids([[CLS]] sample_tokens [[SEP]]))实际项目中我习惯再加一层判断如果截断后结尾处的token不是完整词边界就往前多截几个字符。中文BERT以字为粒度这个问题不明显但如果后续换成英文专利数据就需要处理WordPiece的##前缀问题。此外200这个长度不是拍脑袋定的是权衡了显存和效果后的选择。RTX 2080 Ti上batch size为16时MAX_LEN200能跑起来如果提到512batch size要降到8甚至更小训练时间翻倍但准确率提升有限。3. 模型微调与多标签输出层设计3.1 BERT、RoBERTa、RBT3的选型逻辑项目选用了哈工大讯飞联合实验室发布的三个中文预训练模型BERT-wwm-ext、RoBERTa-wwm-ext和RBT3。这里有个容易混淆的地方RoBERTa-wwm-ext不是Facebook原版RoBERTa的中文版而是用RoBERTa的训练策略动态Masking、去掉NSP、更大batch重新训练的BERT模型。RBT3则是用RoBERTa-wwm-ext的前3层Transformer初始化再继续训练100万步得到的轻量模型。选这三个模型的用意是形成对照BERT-wwm-ext代表标准全词遮盖预训练方案RoBERTa-wwm-ext代表改良训练策略RBT3代表轻量级蒸馏路线。参数量上RBT3只有约3层Transformer推理速度快得多但效果通常会打折。从实验数据看RBT3在CNPatents-Large上的准确率比BERT-wwm-ext低1.3个百分点Micro-F1低3个百分点换取的是近3倍的推理速度提升。如果分类系统要上线做实时预测RBT3是可考虑的降级方案。3.2 Sigmoid与BCEWithLogitsLoss的配合逻辑多标签分类与单标签分类的本质区别在于输出层概率分布。Softmax强制所有类别概率和为1适合互斥类别多标签任务中一篇专利可以同时属于G06F和Y02E需要每个类别独立判断是与否。Sigmoid函数对每个输出维度独立计算概率输出向量的各维之和不一定等于1正好满足需求。损失函数这里有一个工程细节值得展开。很多初学者会用nn.Sigmoid()加nn.BCELoss()的组合即先算Sigmoid再算交叉熵。但项目用的是BCEWithLogitsLoss这个类把Sigmoid和BCELoss合并成一个函数。合并的数学结果是等价的数值稳定性却差别很大。BCEWithLogitsLoss内部使用log-sum-exp技巧处理数值下溢避免Sigmoid输出的极值接近0或1在对数运算时产生inf或NaN。尤其当模型对某个样本的预测置信度极高时纯Sigmoid加BCELoss的梯度可能消失BCEWithLogitsLoss则稳定得多。import torch import torch.nn as nn class MultiLabelPatentClassifier(nn.Module): def __init__(self, model_namehfl/chinese-bert-wwm-ext, num_labels30): super().__init__() from transformers import BertModel self.bert BertModel.from_pretrained(model_name) self.dropout nn.Dropout(0.3) self.classifier nn.Linear(self.bert.config.hidden_size, num_labels) self.loss_fn nn.BCEWithLogitsLoss() def forward(self, input_ids, attention_mask, labelsNone): outputs self.bert(input_idsinput_ids, attention_maskattention_mask) pooled outputs.pooler_output # [batch, 768] logits self.classifier(self.dropout(pooled)) # [batch, 30] if labels is not None: loss self.loss_fn(logits, labels.float()) return loss, logits return logits训练时不需要在forward里显式加SigmoidBCEWithLogitsLoss的输入是未经过激活的logits。预测时则要过Sigmoid拿到概率再用一个阈值通常是0.5决定每个类别是正例还是负例。阈值0.5不是最优选择后面会讲到如何调。3.3 训练参数与优化器配置项目给出了一组经过验证的参数MAX_LEN200TRAIN_BATCH_SIZE16VALID_BATCH_SIZE16EPOCHS3LEARNING_RATE1e-5。数据量在30万到60万级别时3个epoch足够收敛再多容易过拟合。学习率1e-5是BERT微调的标准起点BERT原论文使用的就是2e-5到5e-5区间但那是英文任务。中文专利文本更长、标签更多1e-5更稳妥。需要注意模型名称与优化器的匹配。RBT3在预训练阶段使用的是AdamW优化器微调时继续用AdamW没有问题。BERT-wwm-ext在预训练时用的是LAMB优化器用于大batch训练微调阶段用AdamW反而更常见因为任务规模小LAMB在大batch下有优势小batch下与AdamW差距不明显。关于batch size与学习率的联动这里有个原则batch size翻倍学习率通常也要相应上调。但BERT微调对学习率非常敏感从16调到32时1e-5可能会让loss震荡。稳妥做法是保持学习率不变增加梯度累积步数来模拟更大batch。from transformers import AdamW, get_linear_schedule_with_warmup optimizer AdamW(model.parameters(), lr1e-5, weight_decay0.01) total_steps len(train_dataloader) * 3 scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(total_steps * 0.1), num_training_stepstotal_steps )warmup步数设置为总步数的10%前10%的step里学习率从0线性增到1e-5让模型从预训练权重出发时不会因为初始梯度异常而震荡。weight_decay0.01是BERT微调的常见配置只对权重矩阵生效不对bias和LayerNorm参数生效AdamW内部已经处理好了这个区分。4. 实验对比与高频标签筛选的实际收益4.1 未筛选时的基线表现先看不做高频标签筛选的结果。在CNPatents-Large上BERT-wwm-ext准确率0.659、Micro-F1 0.597在CNPatents-Small上准确率0.756、Micro-F1却只有0.506。这个反差值得解读CNPatents-Small数据量更小、标签相对更集中准确率自然更高但Micro-F1反而更低说明模型在部分标签上完全失效——所有测试样本在该标签上的预测都是错的拉低了Micro-F1。直观理解是Micro-F1是对每个样本的每个标签计算TP、FP、FN后汇总再求precision和recall。如果某个标签在测试集中有样本但模型从不预测该标签那这个标签的recall为0TP为0Micro-F1被明显拖低。未筛选的646个标签里大量是这种低频标签模型训练时没见过足够正样本测试时当然预测不出来。4.2 高频标签筛选后的提升幅度筛选后效果非常显著。在CNPatents-Large上BERT-wwm-ext准确率从0.659提升到0.862Micro-F1从0.597提升到0.717。在CNPatents-Small上准确率冲到0.912Micro-F1到0.693。综合看高频标签筛选贡献了约20个百分点的准确率提升和12个百分点的Micro-F1提升。三个模型的排序也值得注意。在CNPatents-Large上BERT-wwm-ext与RoBERTa-wwm-ext几乎持平0.862 vs 0.863RBT3略低但差距不大。在CNPatents-Small上RoBERTa-wwm-ext的Micro-F1反而最高0.696说明数据量小时RoBERTa-wwm-ext的动态Masking策略更有优势。如果只看准确率三个模型差距不超过2个百分点考虑到RBT3推理速度快3倍生产环境选RBT3是省算力的合理选择。RoBERTa-wwm-ext相对BERT-wwm-ext的提升在这个任务上没有体现出来可能因为专利文本的领域特殊性削弱了通用预训练策略的差异。4.3 标签数与样本量的权衡高频标签筛选本质上是在精度与召回之间做交换丢弃低频标签模型在头部标签上的性能大幅提升但代价是模型无法覆盖全部小类。在实际的专利分类系统里这通常不是问题——头部30个标签覆盖了大多数专利申请低频标签可以走规则或人工兜底流程。更细致的做法是分层处理。比如把标签分成三档样本数大于10000的头部标签、1000到10000的中部标签、小于1000的尾部标签。头部标签用模型预测中部标签用模型预测但阈值调高比如0.6尾部标签直接用基于关键词的规则匹配或者交给人工。这样既保住了头部性能又不至于完全丧失对长尾类别的识别能力。def predict_with_threshold(logits, head_thresh0.5, tail_thresh0.6): probs torch.sigmoid(logits) preds (probs head_thresh).int() # 对低频标签提高阈值减少误报 for i, is_tail in enumerate(tail_label_flags): if is_tail: preds[:, i] (probs[:, i] tail_thresh).int() return preds这个阈值调整技巧在测试集上能再提升1到2个点的Micro-F1代价是低频标签的recall会下降。具体阈值怎么定可以通过在验证集上搜索一组最优值实现遍历head_thresh在0.3到0.7、tail_thresh在0.5到0.9的网格组合用Micro-F1做选择标准。这类后处理优化空间通常在2个百分点左右值得花时间做。5. 从准确率到工程落地阈值校准与批次推理实验报告给出了91.2%准确率和71.7% Micro-F1但上线前还有几个细节要处理。首先是预测阈值。0.5是Sigmoid输出的默认分界但对多标签任务不一定最优。专利分类中误报把不相关的分类号预测为正和漏报漏掉真实分类号的代价不同阈值应该向减少代价更高的错误方向偏移。def find_best_threshold(model, val_dataloader, label_freq_dict): from sklearn.metrics import f1_score best_thresh, best_f1 0.5, 0.0 for thresh in [i * 0.05 for i in range(6, 11)]: # 0.3 ~ 0.5 all_preds, all_labels [], [] model.eval() with torch.no_grad(): for batch in val_dataloader: logits model(batch[input_ids], batch[attention_mask]) probs torch.sigmoid(logits) preds (probs thresh).int().cpu().numpy() all_preds.extend(preds) all_labels.extend(batch[labels].cpu().numpy()) f1 f1_score(all_labels, all_preds, averagemicro) if f1 best_f1: best_thresh, best_f1 thresh, f1 return best_thresh, best_f1这个方法对每个候选阈值算一次Micro-F1选最优值。注意要根据验证集做不是测试集否则会有信息泄漏。实际项目中还可以分标签设置阈值正样本占比高的标签适当降阈值占比低的标签升阈值效果比全局阈值更好。推理阶段的批量处理同样要优化。BERT类模型对序列长度敏感200 token的输入如果按整batch送入GPU利用率较高但显存占用大如果混入大量短文本可以按长度排序后动态batching。这块的收益在线下评测时看不出来上线后对吞吐量影响明显。另外一个容易被忽略的细节是attention_mask的设置。专利标题和摘要拼接后padding部分必须mask掉否则模型会吸收无效的padding向量污染[CLS]的语义表示。很多人直接tokenizer(..., paddingTrue, truncationTrue)一把梭tokenizer会帮你生成mask但如果手动拼batch很容易漏掉这一步。最后提一个面向后续扩展的方向论文里提到的粒度是前4位小类但审查员真正需要的是大组甚至小组级别的分类号。从前4位往第5、6位推进时标签数量会从几十个暴涨到几千个每个标签的正样本数量骤降。这时候需要引入层次分类结构先在小类级别做粗分类再在各小类内部用独立的模型或分类器细分到大组。预训练模型在这个两级架构中仍然可以作为文本编码器复用第一级的特征可以拼接第二级的标签嵌入做联合训练。这条路走得通但工程复杂度比单模型高出不少需要独立的样本管理机制来维护各个层级的标签体系。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。