☰
基于改进BERT算法的专利实体抽取:石墨烯领域实战
2026/10/11 10:13:09 网站建设 项目流程

简介:这份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, flags=re.M) raw = re.sub(r'^\s*(权利要求书|说明书|附图说明).*$', '', raw, flags=re.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% 双标,算 Cohen's 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 领域继续预训练:用专利语料再喂一遍 MLM

DAPT 的核心是拿领域无标注语料继续跑 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"], truncation=True, max_length=256) tokenized = dataset.map(tokenize, batched=True, remove_columns=["text"]) collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm_probability=0.15) args = TrainingArguments( output_dir="./dapt_out", per_device_train_batch_size=16, learning_rate=5e-5, # 继续预训练用小学习率,避免灾难性遗忘 num_train_epochs=3, warmup_ratio=0.1, save_steps=500, logging_steps=100, fp16=True, ) trainer = Trainer(model=model, args=args, data_collator=collator, train_dataset=tokenized["train"]) trainer.train()

逻辑说明:mlm_probability=0.15是 BERT 原论文的默认值,领域语料不大时保持这个比例即可;learning_rate=5e-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_channels=128): super().__init__() self.bert = bert hidden = bert.config.hidden_size # 多尺度卷积捕捉实体边界 self.conv3 = nn.Conv1d(hidden, conv_channels, kernel_size=3, padding=1) self.conv5 = nn.Conv1d(hidden, conv_channels, kernel_size=5, padding=2) 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_mask=attention_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], dim=1).transpose(1, 2) feat = torch.cat([out, conv_feat], dim=-1) logits = self.classifier(self.dropout(feat)) return logits

逻辑说明:conv_channels=128是经验值,实体类型多可以加到 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_first=True) def forward(self, input_ids, attention_mask, labels=None): out = self.bert(input_ids, attention_mask=attention_mask).last_hidden_state emissions = self.classifier(out) if labels is not None: # 训练:返回负对数似然 loss = -self.crf(emissions, labels, mask=attention_mask.bool(), reduction="mean") return loss # 推理:Viterbi 解码 return self.crf.decode(emissions, mask=attention_mask.bool())

逻辑说明:mask必须传,否则 padding 位置会污染转移概率。batch_first=True要和数据加载器的维度对齐,这个参数写错是最常见的翻车点,报错信息还不直观。CRF 层会让训练慢 10% 左右,但推理时 Viterbi 是 O(n·L²),L 是标签数(这里 9 个),开销可忽略。

4. 训练配置与调参:从学习率到类别不均衡

4.1 分层学习率:BERT 层小、任务层大

微调时如果所有层用同一个学习率,要么 BERT 被冲坏,要么任务层学不动。我一般用分层学习率:BERT 主干 2e-5,新增的卷积层和分类头 1e-3,CRF 转移矩阵 1e-3。这样既保护预训练知识,又让新模块快速收敛。

from transformers import AdamW def build_optimizer(model, bert_lr=2e-5, head_lr=1e-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_decay=0.01)

逻辑说明:weight_decay=0.01是 BERT 微调的常规设置,对偏置和 LayerNorm 参数其实应该关掉,但影响很小,工程上常忽略。bert_lr=2e-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(weight=class_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, mask=batch["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 解码时没传 mask,padding 位置的发射分数参与了 Viterbi。解决:确认crf.decode和crf.forward都传了mask=attention_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, epsilon=1.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 = {}

逻辑说明:epsilon=1.0是扰动幅度,太大反而掉点,建议在 0.5 到 1.0 之间试。FGM 只在 embedding 层加扰动,训练开销增加约 30%。用法是正常前向反向得到梯度后调attack(),再前向反向一次,然后restore(),两次梯度累加更新。

第三步,鲁棒性测试。构造三类测试集:含错别字的、中英文混排的、超长文本的。石墨烯专利里「graphene」和「石墨烯」混用很常见,模型必须两种都能识别。我一般要求三类测试集上 F1 下降不超过 3 个点才算过关。

最后说个我自己的习惯:每次实验都固定随机种子,记录完整的配置(学习率、batch、epoch、数据版本),用表格管理。专利实体抽取这种任务,复现性比单次高分重要得多。我吃过亏,一次调出 0.89 的模型,结果配置没记全,重跑怎么都复现不出来,白白浪费一周。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询