
简介这份PDF文档面向自然语言处理与专利分析方向的研究者、研究生及技术开发人员聚焦如何利用改进BERT算法提升专利实体抽取的效率与准确性解决传统串行抽取方式在专利新颖性判断中的局限。文档以石墨烯制备技术为案例系统讲解基于改进BERT-BiLSTM-CRF的命名实体识别算法以及融合注意力机制与句法语义特征的实体关系抽取算法并延伸至知识图谱构建与专利技术动态演化分析等应用场景。资源包内含1个PDF文件大小约1.55MB内容涵盖摘要、关键词、算法原理、数值实验与结论等完整章节结构规范便于按主题研读。目前已有141人学习下载。读者可从中获取两种改进算法的设计思路与实验验证方法理解中文特征与句法语义特征结合的具体路径并借助石墨烯案例掌握专利文本自动抽取、实体关系识别及技术演化趋势分析的可复用研究框架适合作为论文写作与算法复现的参考材料。1. 专利实体抽取为什么要盯上石墨烯这个窄领域去年帮某实验室做专利情报分析时我踩过一个典型的坑拿通用 NER 模型直接跑石墨烯专利摘要把「化学气相沉积」识别成「化学」领域词、把「CVD」漏掉、把「氧化石墨烯」切成「氧化」和「石墨烯」两个实体。人工抽 200 篇做校验F1 只有 0.61根本没法用。问题不在模型不行而在通用预训练语料里石墨烯相关的专利文本太少领域词表几乎空白。这就是「基于改进 BERT 算法的专利实体抽取」要解决的事在 BERT 的基础上做领域适配和结构改进把专利文本里的材料、工艺、性能参数、设备这几类实体稳定抽出来。石墨烯只是切入场景——它专利量大、术语密集、中英文混排严重是检验方案鲁棒性的好样本。适合谁看做专利情报、材料领域知识图谱、企业技术尽调的工程师以及想把 BERT 微调落地到垂直领域的 NLP 从业者。下面按「数据怎么造 → 模型怎么改 → 怎么训 → 坑在哪 → 怎么验」的顺序讲透。2. 石墨烯专利语料怎么造从原始 PDF 到 BIO 标注2.1 先想清楚要抽哪几类实体实体体系定不下来后面标注全是返工。我一般按石墨烯专利的技术结构划四类材料实体石墨烯、氧化石墨烯、碳纳米管复合物、工艺实体化学气相沉积、Hummers 法、旋涂、性能实体载流子迁移率、比表面积、拉伸强度、设备实体管式炉、旋涂仪、拉曼光谱仪。这四类覆盖了专利权利要求书里 90% 以上的关键技术信息。标注规范要写死边界规则否则不同标注员对「氧化石墨烯薄膜」这种嵌套实体会给出不同答案。我的做法是整体优先即「氧化石墨烯薄膜」整体标为材料实体不拆成「氧化石墨烯」「薄膜」。规则写进标注手册标注前先做 50 条试标对齐。2.2 从 PDF 到纯文本的清洗脚本专利 PDF 的坑在于双栏排版、页眉页脚、公式图片混排。直接复制粘贴会得到一堆乱序文本。下面是我常用的清洗流程基于 pdfplumber 和正则import pdfplumber import re def extract_patent_text(pdf_path): full_text [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: # 按栏切分专利常见双栏布局 left page.crop((0, 0, page.width / 2, page.height)) right page.crop((page.width / 2, 0, page.width, page.height)) for col in [left, right]: txt col.extract_text() or full_text.append(txt) raw \n.join(full_text) # 去掉页眉页脚常见模式为纯数字行或含权利要求书的行 raw re.sub(r^\s*\d\s*$, , raw, flagsre.M) raw re.sub(r^\s*(权利要求书|说明书|附图说明).*$, , raw, flagsre.M) # 合并被硬换行切断的句子 raw re.sub(r(?[\u4e00-\u9fa5])\n(?[\u4e00-\u9fa5]), , raw) # 压缩多余空白 raw re.sub(r[ \t], , raw) return raw.strip()逻辑说明先按页面中线切左右栏避免跨栏串行再用正则清页眉页脚关键一步是把中文之间的硬换行去掉否则一句「氧化石墨烯的制备方法」会被切成三行标注时容易漏标。参数上page.width / 2对 A4 双栏基本够用如果遇到三栏排版需要改成三等分。清洗完建议人工抽 20 篇核对重点看公式区域是否混入乱码。2.3 标注格式与一致性校验标注输出用 BIO 格式每行「字符\t标签」句子之间空行分隔。标签集B-MAT/I-MAT、B-PRO/I-PRO、B-PER/I-PER、B-DEV/I-DEV、O。标注完必须做一致性校验我一般抽 10% 双标算 Cohens kappa低于 0.85 就回去改规范重新标。# 统计各实体类型数量检查是否严重不均衡 awk -F\t {print $2} train.bio | grep -v ^O$ | sed s/^[BI]-// | sort | uniq -c | sort -rn如果某类实体少于 200 个微调时基本学不动需要针对性补充该类专利或做数据增强。这一步不做后面模型表现差你都不知道是算法问题还是数据问题。3. 改进 BERT 的三个着力点领域适配、实体边界、标签依赖3.1 为什么原生 BERT 在专利上会掉点原生 BERT 的中文预训练语料以新闻、百科为主专利文本的句式长句、被动、嵌套限定和词汇分布差异很大。直接微调会出现两个现象一是领域词被切成子词后语义漂移比如「石墨烯」在原生词表里可能被切成「石」「墨」「烯」三个 token二是长实体边界识别差「化学气相沉积法」经常只标出「化学气相沉积」。改进方向有三个我按性价比排序领域继续预训练DAPT 实体边界增强 标签依赖建模。DAPT 是投入产出比最高的用几万篇石墨烯专利做 MLM 继续预训练领域词的表征立刻改善。3.2 领域继续预训练用专利语料再喂一遍 MLMDAPT 的核心是拿领域无标注语料继续跑 BERT 的掩码语言模型任务。不需要标签几万篇专利摘要加权利要求书就够。下面是基于 transformers 的继续预训练脚本骨架from transformers import BertTokenizer, BertForMaskedLM, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments from datasets import load_dataset tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForMaskedLM.from_pretrained(bert-base-chinese) dataset load_dataset(text, data_files{train: graphene_patents.txt}) def tokenize(examples): return tokenizer(examples[text], truncationTrue, max_length256) tokenized dataset.map(tokenize, batchedTrue, remove_columns[text]) collator DataCollatorForLanguageModeling(tokenizertokenizer, mlm_probability0.15) args TrainingArguments( output_dir./dapt_out, per_device_train_batch_size16, learning_rate5e-5, # 继续预训练用小学习率避免灾难性遗忘 num_train_epochs3, warmup_ratio0.1, save_steps500, logging_steps100, fp16True, ) trainer Trainer(modelmodel, argsargs, data_collatorcollator, train_datasettokenized[train]) trainer.train()逻辑说明mlm_probability0.15是 BERT 原论文的默认值领域语料不大时保持这个比例即可learning_rate5e-5比从头预训练小一个量级目的是在适配领域的同时不把通用语言知识冲掉。训练完把 encoder 权重存下来后面接分类头时加载这个权重而不是原生 BERT。参数上如果显存吃紧把max_length降到 128、batch 降到 8效果损失有限。3.3 实体边界增强给分类头加一层字符级卷积专利实体普遍偏长纯 token 级分类对边界不敏感。我的做法是在 BERT 输出后接一个字符级卷积层用 3、5 两种卷积核捕捉相邻 token 的边界信号再拼回原维度送 CRF。这样「化学气相沉积法」的尾字「法」更容易被正确标为 I-PRO 而不是 O。import torch import torch.nn as nn class BoundaryEnhancedBERT(nn.Module): def __init__(self, bert, num_labels, conv_channels128): super().__init__() self.bert bert hidden bert.config.hidden_size # 多尺度卷积捕捉实体边界 self.conv3 nn.Conv1d(hidden, conv_channels, kernel_size3, padding1) self.conv5 nn.Conv1d(hidden, conv_channels, kernel_size5, padding2) self.classifier nn.Linear(hidden conv_channels * 2, num_labels) self.dropout nn.Dropout(0.1) def forward(self, input_ids, attention_mask): out self.bert(input_ids, attention_maskattention_mask).last_hidden_state # 转成 (batch, hidden, seq) 供 Conv1d 使用 x out.transpose(1, 2) c3 torch.relu(self.conv3(x)) c5 torch.relu(self.conv5(x)) conv_feat torch.cat([c3, c5], dim1).transpose(1, 2) feat torch.cat([out, conv_feat], dim-1) logits self.classifier(self.dropout(feat)) return logits逻辑说明conv_channels128是经验值实体类型多可以加到 256卷积核选 3 和 5 是因为中文实体平均长度在 3 到 6 字之间。注意padding要设成kernel_size // 2才能保持序列长度不变否则和 BERT 输出对不齐。这一层带来的参数量很小推理开销增加不到 5%。3.4 标签依赖CRF 层为什么不能省BIO 标签之间有硬约束比如 I-MAT 前面必须是 B-MAT 或 I-MAT不能直接跟 O。纯 softmax 分类头会输出非法序列。接一层 CRF 能把这种转移约束学进去实测在石墨烯测试集上能涨 1.5 到 2 个点 F1。CRF 的转移矩阵是可学习的训练时用负对数似然解码用 Viterbi。from torchcrf import CRF class BERT_CRF(nn.Module): def __init__(self, bert, num_labels): super().__init__() self.bert bert self.num_labels num_labels self.classifier nn.Linear(bert.config.hidden_size, num_labels) self.crf CRF(num_labels, batch_firstTrue) def forward(self, input_ids, attention_mask, labelsNone): out self.bert(input_ids, attention_maskattention_mask).last_hidden_state emissions self.classifier(out) if labels is not None: # 训练返回负对数似然 loss -self.crf(emissions, labels, maskattention_mask.bool(), reductionmean) return loss # 推理Viterbi 解码 return self.crf.decode(emissions, maskattention_mask.bool())逻辑说明mask必须传否则 padding 位置会污染转移概率。batch_firstTrue要和数据加载器的维度对齐这个参数写错是最常见的翻车点报错信息还不直观。CRF 层会让训练慢 10% 左右但推理时 Viterbi 是 O(n·L²)L 是标签数这里 9 个开销可忽略。4. 训练配置与调参从学习率到类别不均衡4.1 分层学习率BERT 层小、任务层大微调时如果所有层用同一个学习率要么 BERT 被冲坏要么任务层学不动。我一般用分层学习率BERT 主干 2e-5新增的卷积层和分类头 1e-3CRF 转移矩阵 1e-3。这样既保护预训练知识又让新模块快速收敛。from transformers import AdamW def build_optimizer(model, bert_lr2e-5, head_lr1e-3): bert_params, head_params [], [] for name, param in model.named_parameters(): if bert in name: bert_params.append(param) else: head_params.append(param) return AdamW([ {params: bert_params, lr: bert_lr}, {params: head_params, lr: head_lr}, ], weight_decay0.01)逻辑说明weight_decay0.01是 BERT 微调的常规设置对偏置和 LayerNorm 参数其实应该关掉但影响很小工程上常忽略。bert_lr2e-5是微调的安全区间DAPT 之后可以降到 1e-5因为领域知识已经灌进去了不需要大改。4.2 类别不均衡设备实体少怎么办石墨烯专利里设备实体天然比材料实体少可能差 5 倍以上。直接训会让模型偏向多数类。两个办法一是损失加权按类别频率的倒数给权重二是在 CRF 的发射分数上做温度缩放。我一般先用加权损失简单有效。import torch def compute_class_weights(label_list, num_labels): counts torch.zeros(num_labels) for labels in label_list: for l in labels: counts[l] 1 # 频率倒数归一化到均值为 1 weights 1.0 / (counts 1e-6) weights weights / weights.mean() return weights # 在损失里使用 loss_fct nn.CrossEntropyLoss(weightclass_weights)逻辑说明1e-6防止除零归一化到均值 1 是为了不改变整体损失量级否则学习率要重新调。注意 CRF 的负对数似然不能直接接 CrossEntropyLoss 的 weight 参数需要在发射分数上手动加权或者改用 focal loss 的思路。如果设备实体实在太少低于 100 个优先补数据而不是调损失。4.3 早停与模型选择别只看 loss专利实体抽取的评估指标是实体级 F1不是 token 级准确率。训练时每个 epoch 在验证集上算实体级 F1用早停保留最佳 checkpoint。我见过太多人盯着 loss 下降就以为模型在变好结果实体 F1 早就开始掉了。from seqeval.metrics import f1_score def evaluate(model, dataloader, id2label): model.eval() preds, trues [], [] with torch.no_grad(): for batch in dataloader: logits model(batch[input_ids], batch[attention_mask]) pred_labels model.crf.decode(logits, maskbatch[attention_mask].bool()) for p, t, m in zip(pred_labels, batch[labels], batch[attention_mask]): length m.sum().item() preds.append([id2label[i] for i in p[:length]]) trues.append([id2label[i] for i in t[:length].tolist()]) return f1_score(trues, preds)逻辑说明seqeval算的是实体级 F1会把「B-MAT I-MAT」整体当一个实体比对比 token 级准确率严格得多。length截断是为了去掉 padding 部分否则 O 标签会拉高分数造成虚高。早停 patience 设 3 到 5 个 epoch专利数据一般 10 个 epoch 内收敛。5. 避坑与排查五个真实踩过的坑5.1 坑一DAPT 学习率太大通用能力被冲掉现象继续预训练后模型在石墨烯测试集上确实涨了但在其他材料专利上反而比原生 BERT 差。原因学习率用了 1e-4接近从头预训练的量级把通用语言知识覆盖了。解决DAPT 学习率压到 5e-5 以下训练轮数控制在 3 轮以内并且用验证集监控通用任务比如跑一下原始 BERT 的 MLM 准确率是否明显下降。5.2 坑二标注边界规则不统一模型学出矛盾现象训练 loss 震荡验证 F1 上不去看预测结果发现同一类实体有时带「法」字有时不带。原因标注时对「化学气相沉积法」和「化学气相沉积」两种写法处理不一致。解决回到标注规范明确「工艺方法名带方法后缀的一律包含后缀」重新对齐标注双标 kappa 低于 0.85 的批次全部返工。5.3 坑三CRF 的 mask 传错padding 污染转移矩阵现象模型在短句上表现正常长句尾部实体识别乱掉。原因CRF 解码时没传 maskpadding 位置的发射分数参与了 Viterbi。解决确认crf.decode和crf.forward都传了maskattention_mask.bool()并且 attention_mask 的 1/0 和实际 token 对齐。这个坑排查起来费时间建议写个单元测试固定住。5.4 坑四实体级评估用了 token 级指标虚高 10 个点现象报告里 F1 有 0.92实际人工抽检只有 0.80。原因评估脚本用的是 token 级准确率O 标签占 90% 以上把分数拉高了。解决统一用 seqeval 的实体级 F1并且报告里区分「严格匹配」和「宽松匹配」两种口径严格匹配要求边界和类型都对。5.5 坑五推理时 batch 内序列长度差异大显存爆掉现象单条推理没问题批量推理到第 30 条左右 OOM。原因专利文本长度差异极大短的 50 字长的 2000 字padding 到最长序列导致显存爆炸。解决推理时按长度分桶同一 batch 内长度接近或者用动态 padding每个 batch padding 到该 batch 最大值而不是全局最大值。生产环境建议加长度上限截断超过 512 的部分滑窗处理。6. 验证与进阶把 F1 从 0.80 推到 0.88 的几个技巧模型训完只是开始真正决定能不能上生产的是验证环节和几个细节优化。我一般分三步走先做错误分析定位瓶颈再针对性加技巧最后做鲁棒性测试。第一步错误分析。把验证集上的错误按类型分边界错误、类型混淆、漏标、多标。石墨烯场景里最常见的错误是「材料-工艺」混淆比如「氧化石墨烯制备」被整体标成材料。定位到这类错误后针对性补充边界样本比盲目加数据有效得多。第二步几个实测有效的技巧。一是实体词典后处理把领域词表石墨烯、CVD、Hummers 等做成词典对模型输出做规则校正边界错误能降 30% 左右。二是多模型投票用不同随机种子训 3 个模型推理时对实体做投票F1 能涨 1 到 1.5 个点代价是推理成本翻三倍。三是对抗训练在 embedding 层加微小扰动FGM提升鲁棒性对中英文混排的专利文本尤其有效。# FGM 对抗训练核心片段 class FGM: def __init__(self, model, epsilon1.0): self.model model self.epsilon epsilon self.backup {} def attack(self): for name, param in self.model.named_parameters(): if param.requires_grad and embedding in name: self.backup[name] param.data.clone() norm torch.norm(param.grad) if norm ! 0: r_at self.epsilon * param.grad / norm param.data.add_(r_at) def restore(self): for name, param in self.model.named_parameters(): if name in self.backup: param.data self.backup[name] self.backup {}逻辑说明epsilon1.0是扰动幅度太大反而掉点建议在 0.5 到 1.0 之间试。FGM 只在 embedding 层加扰动训练开销增加约 30%。用法是正常前向反向得到梯度后调attack()再前向反向一次然后restore()两次梯度累加更新。第三步鲁棒性测试。构造三类测试集含错别字的、中英文混排的、超长文本的。石墨烯专利里「graphene」和「石墨烯」混用很常见模型必须两种都能识别。我一般要求三类测试集上 F1 下降不超过 3 个点才算过关。最后说个我自己的习惯每次实验都固定随机种子记录完整的配置学习率、batch、epoch、数据版本用表格管理。专利实体抽取这种任务复现性比单次高分重要得多。我吃过亏一次调出 0.89 的模型结果配置没记全重跑怎么都复现不出来白白浪费一周。希望帮到你。本文还有配套的精品资源点击获取