
先别急着敲代码。做三元组抽取这几年我最大的感受是真正决定项目成败的往往不是模型选得有多新而是你在一开始有没有想清楚“做限定领域还是开放领域”。这个选择题做错了后面再换方案返工成本非常高。信息抽取里的三元组抽取简单说就是从非结构化文本中抽取出头实体关系尾实体这样的结构化知识。比如从“华为发布了搭载麒麟9000芯片的Mate 40手机”里抽取出华为发布Mate 40手机、Mate 40手机搭载麒麟9000芯片。但同样是做这件事限定领域和开放领域的思路、模型选型、代码实现完全不是一回事。这篇文章我就把两种路线的差异、各自的实操方案以及可以直接改来用的代码都摊开讲清楚希望能帮你少走点弯路。1. 先想清楚限定领域和开放领域的本质差异在哪1.1 为什么三元组是知识图谱的最小单元在做信息抽取之前得先搞清楚一个基础问题为什么知识图谱偏偏选三元组这种表示形式因为三元组天然具备可组合性。实体是节点关系是边一个三元组就是一条带方向的边。多个三元组拼在一起就能形成一张可推理、可查询的图结构。我举一个实际业务里的例子。假设你在做工业设备故障诊断的知识库从维修记录里抽出一条“轴承温度过高 导致 电机停机”。单独看这一条它只是一个事实但当你把几千条都抽出来拼接成图就能发现“润滑油不足 - 轴承温度过高 - 电机停机 - 产线中断”这样的因果链条这时候数据才真正变成可用的资产。三元组抽取就是整个信息抽取流程里最基础也最关键的一环后续的实体链接、知识融合、推理都是建立在它的输出之上的。1.2 限定领域关系集合固定重在“抽得准”限定领域三元组抽取指的是预先定义好一个封闭的schema关系集合模型只需要在这个集合内做抽取。典型的例子是金融领域的“公司-拥有-股权”、“人物-任职-企业”医疗领域的“药物-治疗-疾病”电商领域的“用户-购买-商品”。这种模式最大的特点就是关系类别有限通常几十个以内。这意味着你可以在相对少的数据量下训练出一个准确率很高的抽取模型。因为模型不需要发明新关系它只需要在给定的关系集合里做选择题。做限定领域方案核心目标是“抽得准”也就是precision优先。因为下游的知识图谱、业务系统对这个关系是否可靠非常敏感抽错了比抽漏了的后果更严重。1.3 开放领域关系不预设重在“抽得全”开放领域三元组抽取情况完全反过来。你不再预设关系集合模型需要从文本中自主发现实体之间的语义关系。关系可能是“位于”、“成立于”、“导致”、“治疗”……但也可以是“喜欢”、“讨厌”、“比……更便宜”、“与……有合作”几乎没有上限。开放抽取的最大难点在于你不知道模型会抽出来什么。有些人觉得开放抽取更方便不用费劲定义schema但真做起来会发现召回上去了、噪声也跟着上去了。模型抽出来的三元组里大量是“苹果 是 一种水果”这种废话或者是“他觉得 这个 好看”这种信息量极低的内容。开放领域的核心目标从“抽得准”变成了“抽得全筛得掉”你需要额外的过滤机制来处理噪声。1.4 路线选择的决策依据先看业务需求再看数据成本选哪条路线不是拍脑袋决定的。我给你一个比较实用判断框架判断维度偏向限定领域偏向开放领域下游用途知识图谱入库、业务规则触发、统计分析文档理解、搜索召回、自动摘要、知识发现关系范围业务上明确比如几十种固定关系无法穷举希望发现新关系数据成本能付出标注成本至少几千条标注语料几乎没有标注数据希望零样本起步容错要求高抽错会影响业务决策低允许噪声靠下游二次过滤迭代周期有较长的项目周期可以训练专用模型要快速验证希望开箱即用我的经验是如果你做的是企业内部的知识库尤其是金融、医疗、工业这些领域绝大多数场景走限定领域路线更稳妥。一来准确率高二来可解释性强出了问题容易追溯。开放领域更适合搜索引擎增强、舆情分析、文档初步理解这类对召回要求高但对精确度容忍度也高的场景。2. 限定领域实操从规则到BERT序列标注我推荐哪条路2.1 先用少量样例判断文本的规律性强不强在动手写模型之前记得先做一步抽出100条真实样本肉眼看一遍。这一步看似笨拙但极其关键。你需要在脑子里回答一个问题这些文本里的实体和关系是不是有比较固定的话术模式我做一个供应链风控项目时面对的是“XX公司向XX公司采购了XX货物”这种句式。我发现实体之间往往有固定的连接词“向”后面跟交易对手“采购”后面跟货物。这种强规律场景单纯用规则引擎或者基于词典正则的方式就能达到非常高的准确率完全不需要跑深度模型。反之如果你面对的文本是“我们这边初步考虑跟A公司聊聊B产品的代理但是价格还没谈拢可能也会看看C家的替代方案”这种口语化、信息糅杂的语料规则基本撑不住直接上深度模型更实际。2.2 规则方案成本最低的baseline别瞧不起它很多人一听到信息抽取就想到BERT觉得规则太Low。但真到了业务落地的时候正则依存句法分析能解决大量问题而且方便快速上线。我在很多项目里第一版都是规则方案打底用它把数据跑通、给业务方看效果同时攒下来一批有明确问题的case用于后续训练模型。规则方案的核心做法是三板斧触发词词典、连接词模式、句法约束。import re class RuleBasedExtractor: def __init__(self, entity_dict: dict, relation_patterns: list): entity_dict: {company: [华为, 小米], product: [Mate 40, 麒麟9000]} relation_patterns: [(发布, company, product), (搭载, product, chip)] self.entity_dict entity_dict self.relation_patterns relation_patterns def find_entities(self, text: str): found [] for etype, words in self.entity_dict.items(): for w in words: for m in re.finditer(w, text): found.append((m.start(), m.end(), w, etype)) # 按文本位置排序方便后面找关系 found.sort(keylambda x: x[0]) return found def extract(self, text: str): entities self.find_entities(text) triples [] # 对每个关系模式找触发词再找左右两边的实体 for trigger, e1_type, e2_type in self.relation_patterns: for m in re.finditer(trigger, text): head self._find_nearest_entity(entities, m.start(), e1_type, left) tail self._find_nearest_entity(entities, m.end(), e2_type, right) if head and tail: triples.append((head[2], trigger, tail[2])) return triples def _find_nearest_entity(self, entities, pos, etype, direction): if direction left: candidates [e for e in entities if e[3] etype and e[1] pos] return max(candidates, keylambda x: x[1]) if candidates else None else: candidates [e for e in entities if e[3] etype and e[0] pos] return min(candidates, keylambda x: x[0]) if candidates else None # 使用示例 extractor RuleBasedExtractor( entity_dict{ company: [华为, 小米, 苹果], product: [Mate 40, 麒麟9000, iPhone 13], chip: [麒麟9000, A15] }, relation_patterns[ (发布, company, product), (搭载, product, chip) ] ) text 华为发布了搭载麒麟9000芯片的Mate 40手机 print(extractor.extract(text))这种方案的好处是逻辑透明、改动方便。坏处也明显遇到宾语前置、从句嵌套、指代省略就抓瞎。所以我的建议是规则方案适合做baseline和冷启动别指望它做到90分但用它跑到60分能帮你明确问题边界这个性价比很高。2.3 基于序列标注的深度方案BERTSoftmax的细节如果规则方案撑不住下一步就是深度模型。限定领域的抽取模型方案我推荐直接做端到端的序列标注也就是BIO标注。给每个token标上B-头实体、I-头实体、B-尾实体、I-尾实体、O。关系怎么处理两种做法一种是在实体识别之后再单独做关系分类另一种是更优雅的CasRel方案用头实体作为条件去预测关系和尾实体。这里我先讲经典的做法也就是实体识别关系分类pipeline。基于BERT的实体识别模型代码核心结构是这样的import torch import torch.nn as nn from transformers import BertModel, BertTokenizer class BertNER(nn.Module): def __init__(self, pretrained_modelbert-base-chinese, num_labels9): super().__init__() self.bert BertModel.from_pretrained(pretrained_model) self.dropout nn.Dropout(0.3) self.classifier nn.Linear(self.bert.config.hidden_size, num_labels) # 这里用交叉熵损失忽略label中id为-100的位置 self.loss_fn nn.CrossEntropyLoss(ignore_index-100) def forward(self, input_ids, attention_mask, labelsNone): outputs self.bert(input_ids, attention_maskattention_mask) pooled outputs.last_hidden_state logits self.classifier(self.dropout(pooled)) loss None if labels is not None: # logits: (batch, seq_len, num_labels), labels: (batch, seq_len) loss self.loss_fn( logits.view(-1, logits.size(-1)), labels.view(-1) ) return {loss: loss, logits: logits}这里值得注意的点是ignore_index-100。训练数据里[CLS]、[SEP]和PAD这些特殊token位置我都不参与loss计算统一在构造label时设置为-100。否则模型会拼命去学习预测PAD的位置但对实际效果反而有害。在标注实体类型上我常用的是BIOES五标签体系比BIOBegin/Inside/Outside更细一点因为它多了EEnd和SSingle对实体边界的预测更精细尤其在实体名称较长或嵌套较常见的中文语料里E标签能辅助模型学会收尾。2.4 关系分类怎么做用BERT的CLS或者实体池化实体识别抽出了“华为”、“Mate 40手机”、“麒麟9000芯片”这样的实体后接下来就是判断两两之间有没有关系是什么关系。这一步我用的是关系分类模型。关系的判定我给你一个特别有用、代码也简单的做法将头实体和尾实体的位置做基于token的池化把两者的池化向量拼接起来再加关系分类头。class RelationClassifier(nn.Module): def __init__(self, pretrained_modelbert-base-chinese, num_relations20): super().__init__() self.bert BertModel.from_pretrained(pretrained_model) self.dropout nn.Dropout(0.3) self.classifier nn.Linear(self.bert.config.hidden_size * 2, num_relations) def forward(self, input_ids, attention_mask, head_pos, tail_pos, labelsNone): outputs self.bert(input_ids, attention_maskattention_mask) hidden outputs.last_hidden_state # head_pos, tail_pos: (batch, 2) 分别是实体的起止下标 batch_size hidden.size(0) head_vecs, tail_vecs [], [] for i in range(batch_size): hs, he head_pos[i] ts, te tail_pos[i] # 用mean pooling取实体片段向量 head_vec hidden[i, hs:he1].mean(dim0) tail_vec hidden[i, ts:te1].mean(dim0) head_vecs.append(head_vec) tail_vecs.append(tail_vec) head_vec torch.stack(head_vecs) tail_vec torch.stack(tail_vecs) logits self.classifier(self.dropout(torch.cat([head_vec, tail_vec], dim-1))) loss None if labels is not None: loss nn.CrossEntropyLoss()(logits, labels) return {loss: loss, logits: logits}为什么不用[CLS]向量而用实体片段的mean pooling因为[CLS]向量是整个句子的语义表示它包含的信息是全局的但你判断两个人/两个事物之间的关系更重要的是这两个实体自身的语义以及它们在句子中交互的上下文。把两个实体的向量拼起来相当于问题和答案都在模型面前模型更容易学出“谁作用于谁”。2.5 关系抽取联合训练方案CasRel思路简介P pipeline两阶段有个明显弊端——错误传播。实体识别阶段如果漏抽了实体关系分类阶段就不可能抽到这个关系实体识别阶段抽错了边界关系分类也会连带受罪。为了解决这个问题业界提出了联合抽取模型最经典的是CasRelA Cascade Binary Tagging Framework。CasRel的思路很有意思它把关系抽取建模成一个层级分类任务第一步从文本中找出所有头实体第二步给定一个头实体对每个预设关系分别判断文本中是否存在对应的尾实体。这样就把关系建模成和头实体相关的函数而不是全局分类。class CasRel(nn.Module): def __init__(self, config): super().__init__() self.bert BertModel.from_pretrained(config[pretrained_model]) hidden_size self.bert.config.hidden_size self.num_relations config[num_relations] # 头实体识别用一个二分类 self.head_entity_cls nn.Linear(hidden_size, 2) # 尾实体识别对每个关系识别尾实体的起始位置和结束位置 self.tail_entity_start_cls nn.Linear(hidden_size * 2, 1) self.tail_entity_end_cls nn.Linear(hidden_size * 2, 1) def forward(self, input_ids, attention_mask, head_posNone, relation_labelNone, tail_posNone): outputs self.bert(input_ids, attention_maskattention_mask) hidden outputs.last_hidden_state # (batch, seq_len, hidden) # 1. 头实体识别序列标注二分类BIO head_logits self.head_entity_cls(hidden) # 2. 尾实体识别根据头实体的向量拼接上下文特征 # 实际训练时取头实体向量作为条件 if head_pos is not None: hs, he head_pos head_vec hidden[:, hs:he1, :].mean(dim1) # (batch, hidden) head_vec head_vec.unsqueeze(1).expand(-1, hidden.size(1), -1) tail_input torch.cat([hidden, head_vec], dim-1) tail_start_logits self.tail_entity_start_cls(tail_input).squeeze(-1) tail_end_logits self.tail_entity_end_cls(tail_input).squeeze(-1) # ... loss计算省略这种级联方式能显著减少错误传播在关系种类多、实体重叠情况多的场景下效果比pipeline要好。代价是训练逻辑更复杂需要构造“头实体-关系-尾实体”的样本对我对团队新人的要求是先跑通pipeline再看联合模型不然很容易在数据处理上栽跟头。3. 开放领域难在哪实用做法有哪些3.1 开放抽取的经典框架基于依赖树的关系发现开放领域三元组抽取这个概念最早可以追溯到斯坦福的OpenIE工作。它的核心思想是不做关系分类而是通过句法分析把句子中的主谓宾结构抽取出来作为三元组的骨架。比如“华为发布了Mate 40”通过依存句法能分析出“华为”是主语“发布”是谓语“Mate 40”是宾语天然就是一个三元组。在中文场景里你如果不想上深度学习模型一个退而求其次的实用方案是用spaCy或LTP做依存句法分析然后提取主语-谓语-宾语路径。我贴一个基于spaCy的简化版做法import spacy nlp spacy.load(zh_core_web_trf) def openie_by_dependency(text: str): doc nlp(text) triples [] for token in doc: # ROOT一般是核心谓语动词 if token.dep_ ROOT: subj None obj None for child in token.children: if child.dep_ in (nsubj, nsubjpass, top): subj child.text if child.dep_ in (dobj, attr, oprd, pobj): obj child.text if subj and obj: triples.append((subj, token.text, obj)) return triples这里头的坑是依存句法分析本身比词性标注要难得多长难句、口语化表达、多谓语句式都会让分析结果偏掉。实际跑一套下来能用的三元组可能只有四成。所以它更适合做“召回候选”后续必须配合清洗逻辑。3.2 基于生成式模型的开抽取效果更稳代价是速度如果说OpenIE是传统方案那这两年真正让开放信息抽取变好用起来的是预训练生成模型。以PaddleNLP的UIE为代表的通用信息抽取框架把实体识别、关系抽取、事件抽取统一成Text-to-Structure生成任务。你给它一段文本和一个提示词它直接生成结构化的抽取结果。UIE做开放抽取最简单的使用方式是给模型一个空提示让模型自己判断什么关系有意义。它的底层逻辑是构建了一个大规模的通用抽取模型对“实体-关系-实体”结构的理解已经内化到了参数里所以能做到零样本抽取。使用方式如下from pprint import pprint from paddlenlp import Taskflow # 开放领域抽取不指定schema让模型自己找关系 ie Taskflow(information_extraction, schema[]) result ie(华为发布了搭载麒麟9000芯片的Mate 40手机) pprint(result)你可能会问schema[]是什么意思意思是告诉模型我不帮你限定要抽什么关系你自己看着办。输出结果里会有一大堆三元组噪声也会比较多。这里的实操心得是开放抽取的输出不能直接用得设置一个置信度阈值配合一套领域相关性的过滤规则。3.3 开放领域的关系归一化你绕不过去的一步我前面说过开放抽取最麻烦的是“抽得全”之后还得“筛得掉”。这里的关系归一化是关键环节。模型可能抽出来“位于”、“坐落于”、“地处”三种关系它们语义上完全一样但在字面上是不同的。如果你直接入库知识图谱里会出现三条关系边查询时还得做别名映射麻烦得很。我的做法是开放抽取后再加一层「关系聚类」把所有抽取出的关系动词用预训练embedding编码然后跑一个层次聚类人工审核聚类中心给每个类族指定一个规范化名称。这套流程在维护成本和抽取效果之间算是比较平衡的方案。4. 代码实战一个可落地的限定领域三元组抽取实现4.1 项目背景与整体架构为了让你能把整套技术路线串起来我这边给一个实际的代码框架。项目背景是做一个简单的**“科技新闻行业图谱”**需要从新闻标题和正文中抽取公司发布产品、产品搭载技术两类关系。文本来源是几十篇科技公司新闻稿属于典型的限定领域。整体架构是数据准备BIO标注- BERT序列标注模型实体抽取 - 基于实体对的规则判定或关系分类模型关系抽取 - 三元组组装与过滤。4.2 数据准备标注格式和代码实现做序列标注数据准备是最容易出错的部分也是很多同学代码写得好但效果出不来的原因。我这边用的方式是BIOES标签实体类型有Company、Product、Tech三种。每个token对应的标签比如B-Company、I-Product、E-Tech、S-Company、O。我给你一个标注文件转训练样本的例子注意这里面的核心逻辑除了token本身还要生成对应的label id序列并且[CLS]、[SEP]位置的label要设为-100。import json from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) def load_bio_data(path): samples [] with open(path, encodingutf-8) as f: for line in f: line line.strip() if not line: continue # 每一行格式: token\t标签前一行是text当前行是labels # 这里简化假设每个样本是一个json: {text: ..., labels: [B-Company, ...]} item json.loads(line) tokens list(item[text]) labels item[labels] assert len(tokens) len(labels) samples.append((tokens, labels)) return samples def encode_sample(tokens, labels, max_len128): # 加上[CLS] 和 [SEP] tokens [[CLS]] tokens[:max_len-2] [[SEP]] labels [O] labels[:max_len-2] [O] input_ids tokenizer.convert_tokens_to_ids(tokens) attention_mask [1] * len(input_ids) # 把标签转为id特殊标记(map label - id)这里省略 label_ids [label_to_id.get(lb, 0) for lb in labels] # 关键把[CLS]和[SEP]位置的loss权重设为-100不让模型去学 label_ids[0] -100 label_ids[-1] -100 return input_ids, attention_mask, label_ids这里花大笔墨强调标签对齐是因为我在面试和带人时发现很多新人的代码是全流程能跑通的但是效果不稳定一查原因十有八九是标签在截断、padding、特殊token处理时错位了。这个环节最值得慢下来写清楚。4.3 训练流程BERTCRF的一个轻量级替代关于序列标注模型上面我给了BERTSoftmax的代码。但在实际项目里我通常会给输出层加一个CRF条件随机场因为它能建模标签之间的转移关系。比如B-Company后面跟着I-Technology几乎不会出现这种标签转移约束对边界识别帮助很大。这里给一个轻量的CRF实现思路class LinearChainCRF(nn.Module): def __init__(self, num_labels): super().__init__() self.num_labels num_labels # 转移矩阵transitions[i][j] 表示从i转移到j的得分 self.transitions nn.Parameter(torch.randn(num_labels, num_labels)) # 设置START和END标签的转移约束 self.start_transitions nn.Parameter(torch.randn(num_labels)) self.end_transitions nn.Parameter(torch.randn(num_labels))实际训练时CRF的前向计算要算整个路径的得分这里我不展开公式了。重点说结论CRF能提升3~5个点的实体F1值尤其是在标签边界模糊、实体类别不平衡的场景下收益明显。代价是训练速度稍慢推理时如果要viterbi解码时间复杂度是O(seq_len * num_labels^2)不过对于128长度的序列来说这个开销可以忽略。4.4 推理脚本从模型输出到最终三元组模型训练完推理阶段要做的事情是拿到每个token的BIOES标签把实体片段组装起来然后判断相邻实体之间的关系最后按置信度过滤。def decode_entities(token_list, tag_list): entities [] cur_entity None for idx, tag in enumerate(tag_list): if tag.startswith(B-): if cur_entity: entities.append(cur_entity) etype tag.split(-)[1] cur_entity {type: etype, start: idx, end: idx, tokens: [token_list[idx]]} elif tag.startswith(I-) and cur_entity and cur_entity[type] tag.split(-)[1]: cur_entity[end] idx cur_entity[tokens].append(token_list[idx]) elif tag.startswith(E-) and cur_entity and cur_entity[type] tag.split(-)[1]: cur_entity[end] idx cur_entity[tokens].append(token_list[idx]) entities.append(cur_entity) cur_entity None elif tag O: if cur_entity: entities.append(cur_entity) cur_entity None if cur_entity: entities.append(cur_entity) return entities拿到实体后关系抽取我用最简单的距离规则两个实体之间如果存在“发布了”、“搭载了”这样的触发词且距离小于阈值就认为它们存在对应关系。这个规则看起来粗暴但在限定领域的结构化文本里效果够用而且可解释性非常好。等case积累到一定量再把规则升级成前面说的关系分类模型。5. 从限定走向开放一种渐进式融合策略5.1 限定模型的输出可以作为开放抽取的种子很多人会在限定领域和开放领域之间反复摇摆其实两者不是对立关系完全可以做成一条流水线。我的建议是先用限定模型把高频、核心的关系抽出来保证准确率再对“漏网之鱼”做开放抽取作为召回补充两条路线的结果做融合去重。比如做科技新闻图谱已知公司发布产品这条关系很重要那就用限定模型死磕它保证一篇新闻里的此类关系都能抽准。同时稿子里可能还提到“某某专家表示”、“某某机构预测”这种隐含关系限定模型没覆盖到这时再用开放模型补充一段抽取结果经过人工复审后可以沉淀为新关系模板反馈回限定模型的schema里。5.2 用置信度和实体类型过滤开放抽取的噪声开放抽取结果能不能用关键看过滤。我的过滤规则一般是三层的第一层置信度过滤。所有模型都会输出一个概率分数这个阈值设多高合适我的经验是宁高勿低开放抽取场景下阈值设在0.7甚至0.8都不过分因为低置信度的候选里噪声占比太高。第二层实体类型过滤。开放抽取出来的实体五花八门但你关心的是“公司”“产品”“人”“技术”这类实体其他词性的实体比如形容词、动词短语直接丢掉。这需要接一个实体类型分类器或者用规则判断实体片段的首尾词性。第三层关系动词过滤。像“是”、“有”、“在”这种泛化关系的三元组信息量太低。我通常会维护一个弱关系词表这些词抽出来的三元组直接进回收站。6. 常见问题与排查技巧实录6.1 标注数据不一致导致的模型震荡做限定领域抽取时模型效果不稳十有八九是标注不一致。最典型的问题标注员对“公司全称vs简称”的标注边界不统一。比如“华为技术有限公司”有人标“华为技术有限”有人标“华为技术有限公司”还有人只标“华为”。标签不一致会导致模型在实体边界上摇摆不定F1值就是上不去。解决办法是写一份标注规范把实体边界判定规则定义清楚比如“公司实体一律包含‘公司/有限/集团’等后缀词除非原文单独出现了简称形式”。然后随机抽10%标注结果做一致性检验Kappa系数低于0.8就需要重新对齐规范。6.2 数据类别不平衡关系样本悬殊怎么办限定领域里关系的分布往往极不均衡。“公司-发布-产品”这种核心关系可能占80%的样本而“公司-合作-公司”这种关系只有5%。如果不做处理模型会严重偏向高频关系低频关系基本抽不出来。我的做法是在数据采样阶段对低频关系做“句子级过采样”复制那些包含低频关系的句子让它在训练时出现得更频繁。同时在loss上给低频关系类别加权重让模型在梯度更新时更关注这些类别。但权重不能加得太大我试过把低频关系权重加到5倍以上模型开始过拟合真实场景反而掉点。6.3 实体嵌套和重叠问题怎么破中文领域的实体重叠特别常见。比如“中华全国工商业联合会”这个整体是一个组织实体但里面的“中华”也能单独看成一个地名。在BIOES序列标注框架下模型只能输出一层标签嵌套实体的内部结构就丢了。应对办法有两个方向一是改用多标签序列标注每个token可以属于多个实体类型二是用Span-based的抽取方式先枚举所有可能的实体片段再做分类。后者在实现上更复杂但效果更好。如果你做的是限定领域的非嵌套实体用BIOES就够了不要往复杂了搞。6.4 文本长度和截断策略BERT类模型最长输入通常是512个token。长文本直接截断会丢失实体和关系信息这在抽取任务里是硬伤。我一般会先做句子级切分把长文本切成多个句子然后在句子级别上做抽取最后把结果合并。这样做的好处是每个句子的语义完整性高实体之间的距离近关系分类更容易。如果长文本里确实存在跨句关系从句A的实体和从句B的实体有联系我会在上游加一个实体链接步骤把不同句子中的同名实体关联起来再做关系推断。6.5 代码调试踩坑记录再分享几个实战里容易踩的代码坑第一个transfomers库版本升级后BERT输出的last_hidden_state的维度没变但是tokenizer的convert_tokens_to_ids在某些新版本里对未登录字的处理方式变了导致token和label对应不上。解决办法是统一用模型自带的tokenizer不要自己用jieba分词后再转。第二个GPU显存不足时很多人会减小batch size但batch size太小比如2会导致模型收敛慢、效果差。我的做法是先用小batch size跑通代码然后梯度累积用更大的有效batch size训练。第三个预测阶段和训练阶段的文本预处理必须完全一致。比如训练时做了全角转半角、统一小写化预测时忘了做输入分布一变化效果立马崩给你看。这个细节我吃过不少亏。结尾给做三元组抽取的你几句实在话做了这么多抽取项目我最深的一点体会是技术方案的迭代速度很快但业务问题的本质没变。限定领域和开放领域的区别说到底不是一个在A方案一个在B方案而是同一个抽取问题在不同的约束条件下的不同解。如果你手上的是特定业务场景、关系清晰的数据沉下心做好限定模型的标注和调优效果比什么都强如果你做的是探索性分析、不知道里面有什么关系那就先跑开放抽取当作侦察兵用召回换认知再用标注沉淀下来反哺限定模型。代码部分我分享的都是能直接跑通的最简实现你可以在这个基础上往自己的场景扩展。最后再送一个小技巧无论用哪种方案一定把“抽取失败”的case攒下来哪怕是人工看一眼也能帮你快速发现数据里的规律性问题。这比调一堆超参数带来的收益来得直接得多。