ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

限定领域与开放领域三元组抽取:技术路线与实战指南

限定领域与开放领域三元组抽取:技术路线与实战指南 做知识图谱的人八成都被非结构化文本喂数据这件事折磨过。数据库里一堆表格好歹能映射但扔过来几百篇新闻稿、病历描述、法院文书你能做的第一件事就是把里面的实体和关系捞出来整理成 (头实体, 关系, 尾实体) 这种三元组。三元组抽取在信息抽取里是绕不开的核心任务往上接知识图谱的构建和推理往下接问答、检索、推荐。而动手之前最重要的一次选择就是搞清楚你面对的是固定 schema 的限定领域抽取还是关系不固定、靠模型自由发挥的开放领域抽取。这两条路线的数据、模型、评测方式几乎是两套打法选错了后面都是白费功夫。这篇文章我会把两条路线的思路拆开讲配合可以直接跑的代码适合正在做知识图谱、搜索引擎或者刚入门 NLP 抽取任务的朋友参考。1. 内容整体设计与思路拆解1.1 先想清楚一个问题你手里的任务到底能不能限定 schema在动手写代码之前要先定义问题。我自己踩过不少坑把这个步骤省了后面就是灾难。限定领域的定义是所有的实体类型和关系类型都在一个封闭集合内例如“人物、公司、职位、地点、时间”这类关系是“任职于、成立、位于、出生于”。这种场景一般出现在垂直行业比如招聘信息抽取、商品属性抽取、病历信息抽取。因为领域窄、关系固定你完全可以把关系列表写死在配置里让模型在有限的选择里做判断。而开放领域没有固定的关系集合。比如你让模型从“乔布斯创立的苹果发布了 iPhone”里面抽取任意有意义的三元组模型可能要抽出“(乔布斯, 创立, 苹果)”、“(苹果, 发布, iPhone)”也可能抽出“(乔布斯, 是, 苹果创始人)”这种更口语化的表达。关系是开放、不可穷举的今天出现“抗癌”明天出现“获得专利”后天出现“被制裁”你没法提前把 schema 定死。判断依据其实很简单如果你能枚举出关系集合而且这些关系很长时间不会变就选限定领域的监督学习如果关系经常变甚至用户每天给的查询 schema 都不一样就选开放领域的生成式抽取。我见过很多项目明明业务场景是固定的那几种关系却非要用大模型做开放抽取结果效果好是好可每个月光 API 费用就让人肉疼延迟还动不动两秒起属于典型的大炮打蚊子。1.2 限定 vs 开放两套完全不同的技术栈这两条路线不只是模型不同从数据到评测几乎完全另一套体系。我用一个表格把关键差异列出来对比维度限定领域开放领域关系集合固定、可枚举开放、动态变化典型方法序列标注、联合抽取模型、规则依赖生成式抽取、指令微调、大模型 ICL数据标注按固定标签集标注实体和关系文本 查询 schema 作为输入输出三元组准确率高垂直场景可到 95%中等依赖模型语言理解能力可解释性高每个决策都有对应标签一般生成结果有时无法溯源算力需求相对低甚至 CPU 能跑相对高生成式模型开销更大从技术原理上理解这件事会更清楚为什么会有这种分水岭。限定领域本质上是一个分类问题给定一段文本和一个候选实体对判断它们之间是否存在预定义关系。分类问题的好处是边界清晰模型学的是“判别边界”所以数据充足的情况下效果非常稳。开放领域本质上是一个生成问题模型要理解“用户想要哪种关系”然后从文本中把对应的实体组合起来并组织成三元组。这要求模型具备更强的抽象能力和语义泛化能力所以通常得靠大模型或生成式训练才能扛住。我做知识图谱项目快十年了两条路线都走过。说句实在的如果业务环境允许你把关系限定到 20 个以内我从来不推荐一上来就走开放抽取。因为开放抽取的输出自由度太高下游入库时还要再做关系对齐、实体消歧整体链路会复杂很多。限定领域更可控规则也清晰哪怕效果不够至少你知道问题出在哪个环节。1.3 为什么我不建议一上来就上大模型很多新人看到开放领域就直接选大模型其实不对。我理解这种心理大模型即插即用给一段 prompt 就出结果看起来最省事。但省事是省在开发初期后面的成本全跑到运维和生产阶段了。大模型做抽取有几个绕不开的问题。第一是延迟尤其在中文长文本上输入 token 一多自回归生成的时间会明显拉长线上服务往往扛不住。第二是成本无论是 API 计费还是自建 GPU 集群都比本地小模型贵一个量级。第三是不确定性同样的输入换一个时间跑结果可能就不一样这对知识图谱入库来说很难受——昨天入库一个三元组今天同一个实体又抽出来一个不同的关系数据一致性会出问题。所以我的建议是先用规则加小型模型搭一个基线把数据逻辑跑通再评估要不要升级到大模型。很多时候你会发现一个基于依存句法的规则脚本加上一个微调过的 BERT 序列标注模型已经能覆盖大部分业务场景。大模型只是工具箱里的最后一块拼图不是唯一的解。2. 核心细节解析与实操要点2.1 限定领域抽取的三种常见实现路线限定领域虽然概念简单但实现路线差别不小。我拆开讲一下各自的适用场景方便你判断哪种更适合你的数据。规则式抽取是代价最低的一种本质是拿正则表达式或模板去文本里套。比如抽取“出生于”关系可以写([\u4e00-\u9fa5]{2,4})出生于([\d]{4}年)之类的模板。它的优点是快、零标注、可解释性强缺点也非常明显中文的自然语言表达太灵活“出生于”可以说成“生在北京”、“生于1985年”、“老家在山东”规则一多维护起来就是灾难。我一般用规则式做冷启动先抽一批数据看看有哪些典型表达为后面的模型方案提供参考。Pipeline 方式是先用实体识别模型把候选实体找出来再对实体对一一做关系分类。这种方式实现起来最容易因为实体识别和关系分类都是成熟的分类任务可以分别用单独的模型训练。但它的缺陷是误差会累积实体识别漏了一个实体后面的关系分类再强也白搭。另外如果文本中候选实体对很多两两组合做关系分类的计算量会爆炸。联合抽取模型是目前限定领域效果最好的路线代表性工作有 CasRel、TPLinker 等。联合抽取的核心思路是让模型同时预测实体边界和实体间的关系共享底层编码避免了 pipeline 的误差累积问题。代价是模型结构复杂训练调参门槛高对标注数据的质量也很敏感。如果你的团队有算法工程师数据量也够联合抽取是首选如果只是个人项目想快速验证用 pipeline 就够了。2.2 开放领域抽取的两种主流玩法开放领域抽取我把它归成两类一类是微调生成模型另一类是大模型 In-Context Learning。微调生成模型的典型代表是百度开源的 UIE它把抽取任务统一成了“文本 指令”到“结构输出”的序列到序列问题。训练时给模型文本和一段 schema 描述模型输出对应结构。这类模型的好处是经过特定领域微调后效果稳定可以本地部署不需要每次都写长篇 prompt。缺点是训练数据难搞需要整理成“指令-输出”对而且要持续维护。大模型 In-Context Learning 玩法更灵活核心是把当前要抽取的关系列表作为指令模板让模型直接输出结构化内容。比如给模型这样的 prompt请从以下文本中抽取所有三元组关系类型包括创始人、产品、总部地点。输出格式为 (头实体, 关系, 尾实体)。对于少量数据、快速验证的场景这种方法很管用不用标注一条数据就能跑起来。我自己的经验是如果关系 schema 以周为单位变化大模型 ICL 是最优解因为改 prompt 成本极低如果关系稳定、数据量大、要求高准确率那还是微调一个 UIE 或者序列到序列模型靠谱。因为大模型在“关系非常多”的场景下容易漏抽而且输出格式越复杂出错率越高。2.3 数据与标签设计最容易翻车的隐藏环节很多人把注意力全放在模型选型上却忽略了数据标注和 schema 设计结果模型怎么调都上不去。我在这里把几个高频翻车点说一下。实体类型定义要小心粒度。比如你做人物资料抽取实体类型是“人物”就够了还是需要区分“艺人”“科学家”“企业家”粒度太粗关系分类会失去很多语义粒度太细标注难度和模型学习难度都会增加。我的建议是先用粗粒度跑基线确认有需要在细分。关系集合要保证互斥且语义清晰。“任职于”和“供职于”这俩关系如果不做归一化标注人员都会疯掉。你在 schema 阶段就要把同义关系合并并给每条关系写一个示例。比如“出生于”“出生地”“毕业于”“毕业院校”示例可以让标注者少犯选择题错误。还有负样本处理。限定领域关系抽取时很多候选实体对之间根本没有关系但很多人在标注时只标了正例没有标负例。模型学了一堆“有关系的模式”上线后面对大量无关系实体对就会疯狂误报。所以标注数据里一定要包含明显没有关系的实体对作为负样本让模型学会拒绝。开放领域的数据设计又不太一样。核心是思考“查询 schema 怎么表达”。UIE 这类模型的输入里schema 起着语法层面的控制作用schema 描述得越清楚输出越规范。比如“找出所有公司”和“找出文本中的机构实体”这两种 schema 写法效果差距可能很大。我习惯在 schema 描述里包含实体类型的语义提示而不只是一个空泛的名词。3. 实操过程与核心环节实现3.1 环境准备与最小依赖先说环境这一块。以下代码我都基于 Python 3.9用到的核心库是transformers、spacy、torch。装好之后下载中文模型即可整条链路 CPU 也能跑只是速度慢一些。pip install transformers torch spacy python -m spacy download zh_core_web_sm如果你想跑后面生成式模型那段建议至少准备 8G 以上内存有 GPU 更好。没有 GPU 也没关系flan-t5-small这种模型在 CPU 上跑个短句子还是能出结果的就是生成速度感人一次可能要十几秒。3.2 限定领域实战基于规则和依存句法的三元组抽取很多人一听规则就嗤之以鼻但在垂直场景浓度很高的文本里规则能发挥奇效。比如招聘 JD、简历、公告这种形式化文本句子结构相对规范用依存句法抽主谓宾效率非常高。我拿一个例子演示输入一句话通过 spaCy 的依存解析核心动词再找出主语和宾语拼成一个三元组。import spacy nlp spacy.load(zh_core_web_sm) def extract_svo(text): doc nlp(text) triples [] for token in doc: if token.dep_ ROOT and token.pos_ VERB: subject None objects [] for child in token.children: if child.dep_ in (nsubj, nsubjpass) and subject is None: subject child.text elif child.dep_ in (dobj, attr, dative): objects.append(child.text) if subject and objects: for obj in objects: triples.append((subject, token.lemma_, obj)) return triples text 乔布斯创立了苹果公司后来发布了iPhone。 for t in extract_svo(text): print(t)这里有几个坑要提醒。第一spaCy 中文模型的依存标注并不是百分百准确尤其遇到长难句“ROOT”可能落在非核心动词上。第二抽取出来的三元组里关系词直接用了动词原文比如“创立了”但实际业务通常需要映射成标准关系名。你可以加一个动词到关系的映射表比如{创立了: 创始人, 创立: 创始人, 成立: 成立时间}这一步属于关系归一化在后面的问题排查里我会详细展开。这个脚本最大的价值不是拿去做生产系统而是让你花十分钟就能在真实数据上跑出一个粗糙基线看看句法结构大概长什么样高频关系动词有哪些。有了这个基线下面选择模型方案会更有底气。3.3 开放领域实战基于生成模型的灵活抽取下面进入开放领域。我以 T5 家族为例因为它结构简单、开源生态好CPU 也能跑。代码逻辑是先把文本和期望抽取的 schema 拼成一段 prompt让模型生成三元组结果。为了保证表达统一我在 prompt 里明确要求输出“头实体, 关系, 尾实体”的 CSV 格式这样后处理解析容易很多。from transformers import AutoTokenizer, AutoModelForSeq2SeqLM model_name google/flan-t5-small tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSeq2SeqLM.from_pretrained(model_name) def extract_open_triples(text, schemas): schema_text 、.join(schemas) prompt ( f请从下面文本中抽取所有关系三元组 f关系类型只能是{schema_text}。\n f输出格式为每行一个三元组头实体,关系,尾实体\n\n f文本{text}\n\n f结果 ) inputs tokenizer(prompt, return_tensorspt, truncationTrue, max_length512) outputs model.generate( **inputs, max_new_tokens256, do_sampleFalse, num_beams4, ) result tokenizer.decode(outputs[0], skip_special_tokensTrue) triples [] for line in result.strip().splitlines(): parts [p.strip() for p in line.split(,)] if len(parts) 3: triples.append(tuple(parts)) return triples text 小米公司由雷军创立总部位于北京主要产品是小米手机。 schemas [创始人, 总部地点, 产品] print(extract_open_triples(text, schemas))这里有几个参数值得解释。do_sampleFalse加上num_beams4是让模型在生成时走确定性更大的 beam search而不是随机采样。开放抽取场景下我强烈建议关闭采样因为线上服务需要结果可复现否则每次生成的实体边界都可能不一样。max_new_tokens设成 256 是因为三元组列表长度通常不会太长但也不能设太短否则长文本里多个三元组会被截断。这个方案的优点是把关系列表做成函数参数每次调用都可以动态传新的 schema非常适合关系频繁变化的场景。缺点也很明显flan-t5-small的能力有限遇到复杂语义或者嵌套句式输出质量会拉胯。实际项目里我一般会换用flan-t5-large或者更专业的 UIE 模型但代码流程是差不多的只需要把模型目录换一下。3.4 把抽取服务封装成一个可调用的接口模型写完总得上线我这里用一个简单的 FastAPI 例子说明怎么把抽取函数封装成服务。注意这个小服务不追求高并发适合内部工具或者个人项目直接用。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ExtractRequest(BaseModel): text: str schemas: list[str] mode: str open app.post(/extract) def extract_endpoint(req: ExtractRequest): if req.mode open: triples extract_open_triples(req.text, req.schemas) else: triples extract_svo(req.text) return {triples: triples}启动方式用uvicorn main:app --reload就行。这里不展开生产级工程细节但我要提一个很容易被忽略的问题接口层一定要加超时控制和错误兜底。生成式模型的推理时间受文本长度影响很大如果上游文本突然变长服务可能迟迟不返回导致调用方大量超时重试进一步拖垮服务。我的做法是在接口层先对输入文本做长度截断或者在 FastAPI 里配一个超时中间件避免单个请求把进程拖死。3.5 效果评估别只看准确率还要看抽取错误类型三元组抽取的评测比一般分类任务复杂因为错误有几种完全不同的形态。我们经常用的是人工抽检加错误类型归类。常见错误类型至少有三种实体边界错误比如“苹果公司”被抽成“苹果”关系判断错误比如把“任职于”抽成“创立”幻觉错误模型自己编造了原文没有的三元组。如果你只算准确率召回率这些错误混在一起你根本不知道模型短板在哪。我的习惯是抽 100 条结果人工逐条标记错误类型然后按类型统计占比。如果实体边界错误占大头就去优化 NER如果幻觉多就要检查 prompt 或者降低模型采样温度如果关系混淆多就要检查关系集合定义和训练数据的区分度。开放领域评测更麻烦一点因为没有一个固定的 ground truth。我的做法是先让两个人分别抽相同文本计算抽取结果的一致性类似 Inter-Annotator Agreement不一致的地方就是模型和学习目标有歧义的地方。这一步看起来很费人力但对判断数据质量非常关键。4. 常见问题与排查技巧实录4.1 规则式抽取对长难句失效怎么办用依存句法做规则抽取时最头疼的就是并列结构、嵌套从句和插入语。比如“乔布斯这位苹果公司的创始人在 1976 年创立了这家公司”spaCy 很容易把“创始人”当成核心名词而不是“创立”的关联成分。我的临时解法是分层处理先用规则把句子按标点拆成短句然后对每个短句单独做依存分析最后再合并结果。虽然拆句会损失一些跨分句的关联但单个短句的抽取准确率会明显提升。如果拆句后依然抽不准我建议别死磕规则直接切到序列标注或生成式方案性价比更高。4.2 生成式模型输出 JSON 解析失败生成式模型就像个不太靠谱的实习生你让它输出 JSON它偶尔会给你来个“JSON:\n{...}”或者带一堆解释文字。我在项目里碰到最离谱的一次模型输出里混进了“抱歉我无法回答”这种话直接把解析器打崩了。我的处理方式分两层。第一层是在 prompt 里做硬性约束比如明确写“只输出 JSON不要包含任何解释”。第二层是解析时做容错处理先剥掉首尾空白和多余字符再用正则直接抓{...}片段来解析或者干脆像刚才的代码示例一样不让模型输出复杂 JSON只输出简单的逗号分隔文本。很多时候降低输出结构的复杂度比让模型学会精确输出 JSON 容易得多。如果你必须用复杂 JSON可以尝试让模型按格式化模板输出比如每个字段占一行解析时按行还原。4.3 中文长文本的滑窗切分策略开放领域抽取时如果文本太长模型可能漏掉尾部内容因为注意力被前面的信息占满了。直接截断又可能切断实体和关系的关键上下文。最靠谱的做法是滑窗切分让相邻窗口之间有重叠。窗口大小我一般设为 256 到 512 个字符重叠 50 个字符左右。切分之后每个窗口独立抽取三元组最后再合并去重。合并时要以标准化后的实体作为 key避免“苹果”和“苹果公司”被当成两个实体。重叠区域如果抽出了重复三元组保留其中一条即可。4.4 关系名不统一引发的抽取混乱这个坑在限定领域和开放领域都会出现。规则抽取抽出来“创立了”生成模型抽出来“创建”下游知识图谱入库时这两个会被当作两个不同的关系图谱结构直接炸掉。我的方案是维护一个关系归一化词典。把所有能表达“创建、成立、创办”这类语义的动词统一映射到标准关系名“创始人”或“成立时间”。归一化可以在抽取之后单独做一个后处理函数也可以用规则批量替换。这个词典需要根据线上数据持续补充通常我一个月会更新一次把新出现的同义表达加进去。没有这一步你的三元组系统越跑越脏。4.5 常见问题速查表问题现象可能原因排查方向实体边界多字或少字NER 模型训练数据标注不一致抽检标注质量统一实体边界规则大量幻觉三元组生成模型采样随机性过高设置 do_sampleFalse降低 temperature开放领域漏抽尾部实体输入超过模型有效长度改用滑窗切分并做重叠关系名称混杂没有做关系归一化维护关系同义词映射词典限定领域误报率高缺少负样本在训练数据中增加负例实体对长难句抽不出内容依存句法解析出错先拆句再抽取或切换成生成式方案这张表基本上覆盖了我日常问诊的大部分情况。当然每个项目的具体错误类型分布都不一样还是得按前面说的抽检方法先定位再对症下药。最后聊一个我个人的习惯。现在我做新的抽取项目不管预期多复杂都会先用规则加依存句法脚本跑一遍基线拿一批真实数据人工看十分钟把高频的错误类型列出来再来决定后面的模型方案。这个习惯帮我省过很多次返工也避免了一上来就投入大量标注成本。如果你在动手前想试试自己的数据适不适合开放领域抽取一个小技巧是先拿一二十条文本手动写一遍三元组再统计一下这些关系里有多少是重复的。重复率低得离谱那就是开放领域没跑了尽早把生成式方案的链路搭起来才是正事。
返回列表