
简介这是一份关于基于预训练模型进行多标签专利分类研究的完整技术文档面向自然语言处理、文本挖掘及专利情报分析方向的学生和研究人员。文档重点围绕IPC专利分类体系针对大规模专利文献快速、准确识别语义信息并赋予多个分类号这一难题给出了基于BERT、RoBERTa、RBT3三种预训练模型的微调方案并采用Sigmoid激活函数与BCEWithLogitsLoss损失函数构建分类模型以准确率和Micro-F1作为评价指标。内容涵盖研究背景、相关技术综述、数据集构建、高频标签筛选、小类粒度分类实验设计与结果对比实验数据达到准确率91.2%、Micro-F1值71.7%。资源包含1个docx文档压缩包大小仅413KB便于快速下载阅读。目前已有279人学习适合需要了解预训练模型在多标签专利分类中应用方法、希望复现实验或撰写相关论文的人员参考。1. 专利分类不是简单的文本分类多标签才是真正的难点做专利分类的团队拿 BERT 直接跑起来之后通常会遇到同一件事效果看起来过得去但一到验证集上就发现分类结果总是少标了类或者把父子层级完全混淆。原因不在预训练模型的精度而在任务设计——专利分类从来不是单一标签的文本分类而是一个天然的、强层次约束的多标签分类问题。一篇专利可以同时属于多个技术领域例如一件关于“基于神经网络的图像压缩方法”的专利可能在 IPC 分类号上既属于 H04N图像通信又属于 G06N计算机系统。更麻烦的是标签本身是一棵深度五层的树叶子节点数量超过七万标注体系还是人工维护的。用单一模型预测一个最可能的分类号只是早期检索系统的思路它在现代专利预审、技术导航和竞争对手分析场景里明显不够用。本文就顺着“预训练模型——多标签框架——专利分类”这条链讲清楚为什么这么做、最小可行的训练代码长什么样、以及真正上线时会卡在哪几个参数上。2. 多标签分类任务的定义与预训练模型选型2.1 从“一个标签”到“多个标签”分类头与损失函数的改变多标签分类multi-label classification和多类别分类multi-class classification在数学上是两个不同的问题。多类别分类用 softmax 做归一化最后输出的是一个概率分布在所有类别上概率之和等于 1而多标签分类给每个标签独立的二分类判断正负标签之间互不排斥一份文本可以被赋予任意多个标签。落到模型结构上区别只在于分类头的激活函数和损失函数计算方式多类别softmax CrossEntropyLoss多标签sigmoid BCEWithLogitsLossBCEWithLogitsLoss内部把 sigmoid 和二分类交叉熵合并在一起计算数值上比分开写更加稳定。具体到 PyTorch 代码就是下面的差异import torch import torch.nn as nn # 多标签分类头的核心差异 class MultiLabelClassifier(nn.Module): def __init__(self, encoder_dim: int, num_labels: int): super().__init__() self.fc nn.Linear(encoder_dim, num_labels) def forward(self, pooler_output): # 关键没有 softmax logits self.fc(pooler_output) return logits # 训练时 logits model(input_ids) loss_fn nn.BCEWithLogitsLoss() loss loss_fn(logits, labels.float()) # labels 是 multi-hot 向量多标签场景下的“预测”并不是简单地取 argmax而是对每个标签独立判断“是或否”。这个判断依赖于阈值阈值可以统一设为 0.5更常见的做法是去验证集上根据 F1 值搜索一个最优阈值。2.2 中文专利场景下的预训练模型选择专利文本是典型的长文档说明书动不动就是几千到几万字而且充满了特殊表达权利要求书的固定句式、化合物名称、数值范围、公式符号。这些特点决定了预训练模型不能随便选。BERT 基础版本BERT-base12 层、768 维对通用领域文本表现良好但在中文专利文本上训练出来的词表对“权利要求”“技术方案”“其特征在于”这类高度重复的段落结构不够敏感。中文专利场景里常用的几个方向按照优先级排序大概是这样的模型结构适用性说明RoBERTa-wwm-ext全词掩码哈工大讯飞版中文文本的细粒度语义更好处理专利说明书段落效果稳定BERT-base-Chinese字级别 BERT适合用于基线对比照搬需配合行业预训练Legal-BERT / Patent-BERT领域预训练语料英文场景效果优秀中文不直接可用需要自己在专利语料上继续预训练继续预训练后的领域模型通用模型 专利语料 MLM在检索、分类、抽取任务上综合收益最明显这里特别说明一点所谓“继续预训练”不是从头训练而是在通用 RoBERTa 模型的基础上用领域语料继续做 MLM 训练通常几千步就有明显效果成本可控。对于专利这种专业术语密度极高的语料这几乎是一个必做项。2.3 预训练模型的参数固定策略freeze 与 fine-tune多标签专利分类的训练策略选择核心是两个问题是否冻结底层参数以及学习率怎么和冻结策略配合。标注数据量大于 1 万条时一般直接 full fine-tune。标注数据少于 3000 条时冻结 BERT 底层的 Embedding 和前几层只训练高层和分类头。学习率上BERT 部分用 2e-5 到 3e-5分类头用 1e-4——分类头是随机初始化的需要更大学习率。深层 Transformer 的底层编码的是通用语法信息顶层更接近任务相关的语义这一点在专利分类中表现得很明显冻结底层能减少过拟合但也会让领域预训练的收益打折扣。实际做法可以是“先全量训练两个 epoch再解冻所有层训一个 epoch”这种方式在工业项目里成本不高效果却比单纯 freeze 或全量训练都好。3. 用 roberta-wwm 搭建多标签专利分类最小实现3.1 数据预处理IPC 分类号展开与标签矩阵构建专利分类的标签构造和普通文本分类有一个非常大的不同标签本身是层级化的。IPC 分类号由“部—大类—小类—大组—小组”构成例如G06F 17/30中的G06是“计算推算或计数”大类的子类。多标签分类中父亲标签和子标签往往是同时正确的——一篇属于G06F 17/30的专利通常也属于G06F。所以数据的处理方式通常是两种路线只预测叶子节点所有标签取最细粒度训练时只输出叶子节点预测全部节点叶子节点和父节点都作为独立标签预测结果天然带层级过滤。第二种方案在推理时有一个后置约束如果某个父节点没有被预测出来那么它的所有子节点也必须被抑制掉。我一般建议直接预测全部节点因为这种做法的精度更高而且漏标的容错性更好。标签展开的代码实现如下import pandas as pd from sklearn.preprocessing import MultiLabelBinarizer # 原始数据每行是一个专利文本 一个 IPC 标签 # 同一个专利的多个标签分布在多行需要先聚合 df pd.read_csv(patent_samples.csv) grouped df.groupby(patent_id)[ipc_label].apply(list).reset_index() mlb MultiLabelBinarizer() label_matrix mlb.fit_transform(grouped[ipc_label]) print(标签总数:, len(mlb.classes_)) print(label_matrix.shape) # (样本数, 标签数)MultiLabelBinarizer会把标签集体转成一个 multi-hot 矩阵训练时每一行就是一个样本的标签向量标签之间互不排斥。这里的标签数量少则几十多则几千随着类别数上升类别不平衡的问题会变得严重这个在后续章节会专门处理。3.2 tokenization长文本截断与滑窗策略专利说明书长度远超 BERT 的 512 token 上限。最常见的方案是直接截断前 512 token虽然简单但在专利分类任务上损失巨大——很多专利的核心技术方案描述在说明书的中后段。工程上可行的做法是分段 池化把长文本切分成多个 512 token 的片段彼此之间有 128 token 的重叠每个片段单独过预训练模型得到[CLS]表示对多个片段的[CLS]做 max-pooling 或 mean-pooling 得到整个文档的表示。from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(hfl/chinese-roberta-wwm-ext) def split_long_text(text: str, max_len: int 512, stride: int 128): tokens tokenizer.tokenize(text) segments [] for i in range(0, len(tokens), max_len - stride): seg tokens[i : i max_len - 2] # 加上 [CLS] 和 [SEP] segments.append(tokenizer.build_inputs_with_special_tokens(seg)) if i max_len - 2 len(tokens): break return segments截断长度和滑窗步长是两个需要按实际数据分布的统计特征去调的参数。步长太长会丢失片段之间的上下文衔接太短则计算量成倍增长。经验值是步长取 128一片专利平均拆 3 到 5 段。如果预算足够也可以换成 Longformer 或 BigBird 这类长文本模型但从效果性价比来看滑窗 RoBERTa 仍然是最常见、最容易复现的落地方案。3.3 训练脚本BCE Loss 与验证指标训练部分直接用 HuggingFace 的Trainer是效率最高的方式但多标签任务需要覆写两个地方模型的num_labels和损失计算。示例如下from transformers import ( AutoModelForSequenceClassification, TrainingArguments, Trainer, ) import numpy as np from sklearn.metrics import f1_score, accuracy_score model AutoModelForSequenceClassification.from_pretrained( hfl/chinese-roberta-wwm-ext, num_labelslabel_matrix.shape[1], problem_typemulti_label_classification, # 关键参数 ) training_args TrainingArguments( output_dir./checkpoints, num_train_epochs5, per_device_train_batch_size8, per_device_eval_batch_size16, learning_rate3e-5, warmup_ratio0.1, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelmicro_f1, ) def compute_metrics(eval_pred): logits, labels eval_pred # 阈值设为 0.5后续可以针对每个标签调优 preds (logits 0.5).astype(int) micro_f1 f1_score(labels, preds, averagemicro) macro_f1 f1_score(labels, preds, averagemacro) return {micro_f1: micro_f1, macro_f1: macro_f1}problem_typemulti_label_classification是 HuggingFace 在AutoModelForSequenceClassification中提供的一个便捷入口设置后模型内部自动使用BCEWithLogitsLoss省去自己写分类头的麻烦。微平均 F1 受到高频类别的主导宏平均 F1 对低频类别更敏感。专利分类因为类别极其不平衡两个指标都必须关注micro_f1代表整体判断能力macro_f1代表低频类的识别能力。3.4 阈值调优直接决定最终效果没做过多标签分类的人最容易忽略的环节就是阈值。模型的输出是 logits经过 sigmoid 变成 0 到 1 之间的概率但很少有一套数据在 0.5 处能拿到最好的 F1。专利分类中因为类目定义精细不同类别的最优阈值差异很大高频类别的概率普遍偏高低频类别的概率普遍偏低统一用 0.5 会导致高频类误报增多、低频类大量漏报。正确的做法是在验证集上搜索全局最优阈值或者更精细一点为每个标签搜索独立阈值from sklearn.metrics import f1_score best_threshold 0.5 best_score 0.0 for threshold in np.arange(0.3, 0.7, 0.05): preds (valid_probs threshold).astype(int) score f1_score(valid_labels, preds, averagemicro) if score best_score: best_score score best_threshold threshold print(fbest threshold: {best_threshold:.2f}, micro-F1: {best_score:.4f})全局阈值搜索的范围一般放在 0.3 到 0.7 之间超出这个范围说明模型置信度严重偏移问题大概率出在样本标注质量或类别不平衡上。独立阈值的代价是需要在验证集上对每个标签分别搜索专利分类有几百个类别时不建议做容易过拟合验证集。4. 专利分类中的类别不平衡与层次约束4.1 标签长尾分布的处理方法专利分类的标签分布是非典型的长尾。少数几个大类占据了绝大多数样本——例如电通信技术、计算、医学卫生领域的专利量级远高于其他领域而大量小组分类号的样本只有个位数。直接训练会带来两个问题高频类别的 loss 主导梯度更新低频类别的表示学不好验证时会发现微平均指标不错但宏观指标惨不忍睹。处理方法从数据层面到模型层面都有。最简单的方案是给损失函数加类别权重权重与标签频率成反比import torch def compute_class_weight(label_matrix: np.ndarray) - torch.Tensor: freq label_matrix.sum(axis0) # 避免除零 weight 1.0 / (freq 1e-6) # 归一化防止权重过大导致训练震荡 weight weight / weight.mean() return torch.tensor(weight, dtypetorch.float32) class_weight compute_class_weight(label_matrix) loss_fn torch.nn.BCEWithLogitsLoss(pos_weightclass_weight)用pos_weight时有一个细节它不是简单的“少样本给更大权重”而是调高“正样本”这一侧的梯度贡献等于变相惩罚漏报。权重设置太激进会让模型偏向于预测正标签需要在验证集上反复观察精确率和召回率之间的变化。数据增强也是处理长尾的有效手段。对低频类别的专利文本做回译back translation即中文翻译成英文再翻译回中文能够在不改变语义的前提下产生新的训练句对。这种方法的成本相对较高但专利文本本身结构程式化回译的收益比其他领域的文本更明显。4.2 层次约束让模型输出符合 IPC 树结构多标签分类模型本身并不知道 IPC 分类号之间的父子关系它有可能预测出“文档属于G06F但又不属于它的子类”这种明显违反树结构的结果。推理时候选集的过滤逻辑是标准做法先拿到模型预测出的所有叶子节点标签检查每个叶子节点的父节点是否也在预测结果中如果父节点没有被预测到将父节点强制设为预测结果如果某个父节点被预测但其子节点一个都没被预测到则可以保留父节点不去强推子节点。def apply_hierarchy_filter(pred_labels: set, ipc_tree: dict) - set: ipc_tree: {子节点: 父节点} 的映射 result set(pred_labels) for child in pred_labels: parent ipc_tree.get(child) if parent is None: continue if parent not in result: result.add(parent) return result后处理规则虽然看起来简单但在验证集上的涨分幅度通常在 1 到 3 个百分点特别是在粗粒度分类的评测指标上提升明显。这个提升并不意味着模型的语义理解更强而是因为约束消除了“结构非法”的输出。4.3 验证集划分不要随机切分专利数据最后一个在专利分类里特别容易踩坑的地方是数据划分。如果直接按行随机切分训练集和验证集同一个专利的多个分类号片段会被分到两边导致验证指标虚高——模型看似见到了文档其实已经“见过”它的一部分了。正确的划分方式是按专利 ID 切分直接保证同一个专利的所有片段都在同一侧from sklearn.model_selection import train_test_split # 先按专利 ID 去重切分 unique_ids grouped[patent_id].unique() train_ids, valid_ids train_test_split(unique_ids, test_size0.2, random_state42) train_mask grouped[patent_id].isin(train_ids) valid_mask grouped[patent_id].isin(valid_ids) train_data grouped[train_mask] valid_data grouped[valid_mask]另外一个被低估的问题是所跨时间的专利数据分布漂移。专利分类的标注体系是逐年更新的新分类号的引入和旧分类号的废弃经常发生。如果要用历史数据做训练、当前数据做验证必须先检查分类号体系是否一致否则会出现“验证集里有一个标签训练集完全没有”的情况。这类错误不会让程序报错但会静默拉低指标。5. 蒸馏、继续预训练与推理加速的进阶技巧5.1 教师网络与专利文本的继续预训练组合从上线的角度来说直接部署一个 RoBERTa-base 做在线推理的问题是延迟和吞吐。一个完整的专利分类服务单篇专利要被切成好几段每段都要过一遍 Transformer整体耗时往往超过百毫秒。线上服务通常需要在效果和速度之间再找一层平衡。一种很常见的组合做法是先把通用模型在专利语料上继续预训练然后蒸馏到一个 smaller 模型上。继续预训练的语料不需要标注数据直接从公开的专利数据中抽出说明书和权利要求书即可。在 10 万篇专利上做 MLM 继续预训练大概几千步就能完成。蒸馏阶段用的是“教师网络的软标签”import torch.nn.functional as F def distill_loss(teacher_logits, student_logits, labels, temperature3.0, alpha0.7): # 软标签损失KL 散度 soft_targets F.log_softmax(student_logits / temperature, dim-1) soft_labels F.softmax(teacher_logits / temperature, dim-1) kd_loss F.kl_div(soft_targets, soft_labels, reductionbatchmean) * (temperature**2) # 硬标签损失多标签 BCE hard_loss F.binary_cross_entropy_with_logits(student_logits, labels) return alpha * kd_loss (1 - alpha) * hard_loss温度系数和解耦系数是两个重要的超参数。温度越高教师网络给出的软标签分布越平滑对负标签的抑制作用越弱专利分类的标签区分度很强温度太高反而会让低频类别和无关类别混在一起。5.2 推理加速裁剪序列长度与 ONNX 导出对于长文本专利推理耗时的大头在序列长度。滑窗策略下一篇文章往往要跑 3 到 5 次前向计算预处理阶段可以通过统计训练集的 token 长度分布来定制合适的截断长度——如果能覆盖 90% 的段落而不截断就不必一直默认使用 512。量化逐步开启也是一个思路FP16 半精度推理在 V100 或 T4 上能带来 60% 左右的加速但 batch 中如果混有超长序列加速比会被拉低。ONNX 导出是性能问题的常规解法python -m transformers.onnx \ --modelhfl/chinese-roberta-wwm-ext \ --featuresequence-classification \ onnx/patent_classifier/ONNX Runtime 配合CUDAExecutionProvider运行时的优势在于线程控制更细对多路并发请求的服务端推理提升非常明显。优化时要特别注意序列的可变长度支持如果每篇文本都做 padding 到固定长度那么 ONNX 带来的收益会被浪费掉。5.3 验证线上效果观察误报与漏报的分布模型上线后的效果验证和离线指标不同。线下评测看的是 F1线上需要看 false positive 和 false negative 的实际分布。多标签分类系统的输出是每个标签的独立概率你在线上可以把阈值放宽一点用于召回再把高置信度样本交给人工复核形成“人机协同”的闭环。我个人在项目中通常记录三个监控指标每个标签的平均预测概率分布观察是否存在某个标签几乎不会被激活每日新增预测标签集合和 IPC 分类号新增对照判断是否有体系变化预测结果中父标签缺失子标签的比例这个比例一旦突增往往意味着数据分布漂移。最后一个指标实际上是把离线做的层次约束搬到了线上监控——模型如果连续多天输出“父标签存在但所有子标签不存在”的比例持续走高那么说明它正在从细粒度判断退化成粗粒度判断这种退化用 F1 不一定能及时看出来。关于多标签专利分类最后的建议是预训练模型决定效果的上限数据划分决定评估的可靠性阈值和层次约束决定上线的可用性。先把这三件事理顺再谈用什么模型。本文还有配套的精品资源点击获取