简介:一份基于预训练模型的多标签专利分类研究文档,面向自然语言处理与专利文本挖掘方向的研究者,系统阐述如何利用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。训练集与测试集按8:2切分。每篇专利的标签是分类号前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_k=30, min_count=100): # 统计每个标签的文档数 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_LEN=200。这里有一个细节:截断策略不是从头截,而是保留开头和结尾。因为专利摘要的开头通常交代技术领域,结尾说明技术效果,中间是具体实现方案。如果直接从头截断200个token,可能丢失技术效果信息;如果只保留前128和后72,信息完整性更好。
from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained("hfl/chinese-bert-wwm-ext") def truncate_text(text, max_len=200): 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_LEN=200能跑起来;如果提到512,batch 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_name="hfl/chinese-bert-wwm-ext", num_labels=30): 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, labels=None): outputs = self.bert(input_ids=input_ids, attention_mask=attention_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里显式加Sigmoid,BCEWithLogitsLoss的输入是未经过激活的logits。预测时则要过Sigmoid拿到概率,再用一个阈值(通常是0.5)决定每个类别是正例还是负例。阈值0.5不是最优选择,后面会讲到如何调。
3.3 训练参数与优化器配置
项目给出了一组经过验证的参数:MAX_LEN=200,TRAIN_BATCH_SIZE=16,VALID_BATCH_SIZE=16,EPOCHS=3,LEARNING_RATE=1e-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(), lr=1e-5, weight_decay=0.01) total_steps = len(train_dataloader) * 3 scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=int(total_steps * 0.1), num_training_steps=total_steps )warmup步数设置为总步数的10%,前10%的step里学习率从0线性增到1e-5,让模型从预训练权重出发时不会因为初始梯度异常而震荡。weight_decay=0.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为0,TP为0,Micro-F1被明显拖低。未筛选的646个标签里大量是这种低频标签,模型训练时没见过足够正样本,测试时当然预测不出来。
4.2 高频标签筛选后的提升幅度
筛选后效果非常显著。在CNPatents-Large上,BERT-wwm-ext准确率从0.659提升到0.862,Micro-F1从0.597提升到0.717。在CNPatents-Small上,准确率冲到0.912,Micro-F1到0.693。综合看,高频标签筛选贡献了约20个百分点的准确率提升和12个百分点的Micro-F1提升。
三个模型的排序也值得注意。在CNPatents-Large上,BERT-wwm-ext与RoBERTa-wwm-ext几乎持平(0.862 vs 0.863),RBT3略低但差距不大。在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_thresh=0.5, tail_thresh=0.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, average='micro') 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(..., padding=True, truncation=True)一把梭,tokenizer会帮你生成mask,但如果手动拼batch,很容易漏掉这一步。
最后提一个面向后续扩展的方向:论文里提到的粒度是前4位小类,但审查员真正需要的是大组甚至小组级别的分类号。从前4位往第5、6位推进时,标签数量会从几十个暴涨到几千个,每个标签的正样本数量骤降。这时候需要引入层次分类结构,先在小类级别做粗分类,再在各小类内部用独立的模型或分类器细分到大组。预训练模型在这个两级架构中仍然可以作为文本编码器复用,第一级的特征可以拼接第二级的标签嵌入做联合训练。这条路走得通,但工程复杂度比单模型高出不少,需要独立的样本管理机制来维护各个层级的标签体系。
本文还有配套的精品资源,点击获取