ARTICLE DETAIL

资讯详情

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

限定领域与开放领域信息抽取实战:从依存句法到生成式模型

限定领域与开放领域信息抽取实战:从依存句法到生成式模型 信息抽取这个方向我这两年算是踩了不少坑。前阵子接了个客户项目要把几百份行业研报里的公司、产品、融资关系抽成三元组最后做成知识图谱。刚开始团队习惯性想直接上生成式大模型结果发现研报里大量句子其实就那几种固定写法比如“XX公司推出YY产品”“YY产品面向ZZ领域”关系类型高度重复。换个客户又完全反过来要求从开放语料里抽任意实体之间的任意关系规则完全写不出来。所以“限定领域”和“开放领域”这两条技术路线根本不冲突难的是怎么判断自己手里到底属于哪一类以及对应的工程方案怎么设计。这篇博文我就把手里的实操代码和踩坑记录整理出来内容包括两类问题的判断标准、限定领域怎么用词典加依存句法零标注快速落地、开放领域怎么用生成式模型做端到端抽取、以及最后评测和上线阶段真正会拖垮你的几个细节。适合刚入门信息抽取的算法工程师也适合那些需要自己搭知识图谱管道、但又不想被模型细节绕晕的同学。1. 限定领域和开放领域先判断你手里的活属于哪一类很多教程会把“限定领域”和“开放领域”理解成“模型大小”的区别或者“要不要用深度模型”的区别。其实最核心的判断依据只有一个关系类别集合是不是封闭的。限定领域抽取指的是在进入抽取之前你已经有一份明确的关系列表。比如人物库要抽“出生地、毕业院校、职业”电商库要抽“产品价格、发货地、品牌方”。实体类型和关系类型都写死在配置里模型或者规则的任务不是“发明新关系”而是在给定集合里做选择。开放领域抽取则完全反过来。你面对的是新闻、研报、合同、科研论文这种开放语料事先根本不知道会出现什么关系。可能今天看到“A收购了B”明天看到“C缺乏D”后天看到“E对F产生了抑制作用”。这种情况下没法枚举关系也没法写触发词表必须让模型自己判断“哪些词之间存在有意义的关联、这个关联叫什么”。我用一个生活化类比来说就是限定领域像给你一张空白表格你要把姓名、年龄、电话填到对应列里开放领域像语文阅读理解的开放式问答你得自己概括这段话在说什么并且提炼出主语、谓词、宾语。放在具体项目里判断标准可以进一步拆成几张清单。如果满足以下条件基本可以走限定领域路线业务方形成了标准化的schema关系列表页能背下来语料句式相对固定比如都是“XX推出了XX”“XX成立于XX年”标注成本有限团队短期内拿不出高质量的人工标注数据系统上线后要求结果可控、可解释出错了能快速定位。如果满足以下条件大概率需要走开放领域路线语料来源复杂句子表达方式千变万化业务没有既定关系体系希望先抽取再沉淀关系实体类型也不固定甚至希望模型帮我们发现新的实体类别能接受一定比例的“臆测”或“幻觉”后续靠人工筛选。这两条路线在技术实现上差异很大我整理了一张表方便对照维度限定领域开放领域关系集合封闭、可枚举开放、不可穷举实体范围通常限定在若干类别不限制尽量广泛标注成本低到中可借助规则生成伪数据高需要大量标注或依赖预训练能力可解释性高规则逻辑透明低模型决策不透明典型技术路线词典 规则 依存句法 / 序列标注生成式预训练模型 / 大规模分类适合场景垂直行业、结构化知识库构建开放域知识挖掘、研究探索做完这层判断大部分项目其实不会走极端。实际业务里往往是“限定为主、开放辅助”。比如金融研报抽取核心关系如“公司-股东-持股比例”是限定的但研报里偶尔会出现一两句关于“企业战略合作”的关系描述规则覆盖不到这时候可以再加一个开放抽取模块做兜底。所以别把两条路线对立起来它们是互补的。2. 限定领域怎么落地词典、规则和依存句法不标一条数据也能用限定领域最常见的误区是一上来就训一个命名实体识别和关系分类模型。训练模型不是不行但要对齐标签、做数据清洗、迭代版本周期太长。更好的做法是先做一套“词典加规则”的基线系统半天时间就能出第一版效果之后再把模型加进来这样才能量化模型到底比规则好多少。2.1 三个组件各司其职限定领域抽取我的实现思路分成三层第一层是实体识别。先建立一个领域词典把可能出现的实体名和别名收录进去。词典匹配不到的时候再用词性过滤兜底比如把“名词/专有名词”单独拎出来与预定义实体类型做模糊匹配。第二层是触发词匹配。每个预定义关系可以挂一组触发词。比如“创始人”这个关系可以挂“创立了”“创办了”“创建了”“成立了”“出生地”可以挂“出生于”“生于”“出生在”。这一步的作用是找到关系的候选位置。第三层是依存句法分析。找到触发词之后直接按正则去两头抓名词很容易抓错因为中文的语序和修饰语太多。更可靠的办法是拿触发词作为动词核心去看它的主语和宾语。依存句法分析提供的就是“谁支配谁、谁修饰谁”的结构信息。我自己用的是哈工大LTP接口简单分词、词性、依存句法一把梭。安装方式很直接pip install ltp首次使用会下载模型文件大约几百兆建议在网络稳定的环境提前跑一次。2.2 核心抽取代码假设我们要从研报里抽取三组关系产品发布关系、公司成立关系、资本投资关系。核心代码可以这样写from ltp import LTP ltp LTP() TRIGGER_WORDS { 发布: [发布, 推出, 上线, 发布了, 推出了], 成立: [成立, 创立, 创办, 组建, 设立了], 投资: [投资, 入股, 融资, 领投, 跟投], } def extract_by_dependency(sentence): # LTP统一输入一个句子list这里只传单句 seg, hidden ltp.seg([sentence]) pos ltp.pos(hidden) dep ltp.dep(hidden) tokens seg[0] postags pos[0] arcs dep[0] # arcs里每个元素有head和relation字段 spo_list [] for idx, trigger in enumerate(tokens): rel None for rel_name, words in TRIGGER_WORDS.items(): if trigger in words: rel rel_name break if rel is None: continue subj, obj None, None for j, arc in enumerate(arcs): if arc.head idx 1: # 依存弧指向当前触发词 if arc.relation SBV: # 主谓关系 subj tokens[j] elif arc.relation in (VOB, FOB, DBL): obj tokens[j] if subj and obj: spo_list.append([subj, rel, obj]) return spo_list if __name__ __main__: text 华为公司在2020年发布了鸿蒙系统并投资了多家芯片企业。 for spo in extract_by_dependency(text): print(spo)这段代码的思路是遍历句子里的每个词如果它命中了触发词表再看它的依存弧上有没有主语和宾语。LTP的词性标注体系中动词标记以v开头但这里直接查触发词表更可控不会把“成立”和“成果”搞混。实际跑一遍会看到类似输出[华为公司, 发布, 鸿蒙系统] [华为公司, 投资, 芯片企业]如果你自己想复现需要注意两个地方。第一LTP的seg接口接收的是一个列表即使只有一句话也要用[text]包一层不然返回结构会不一样。第二依存弧的head下标从1开始0表示根节点所以判断时要用idx 1对齐。头一回跑通拿到的结果就是两个正确的三元组那种成就感是直接调现成模型接口体会不到的。2.3 从“能跑”到“稳定”的规则扩展上面的代码只是骨架。真实场景里句子会带修饰语、状语、补语直接取SBV和VOB经常取到光秃秃的代词或空壳短语。我实际生产里加了三个处理实体边界扩展。只取“华为”“鸿蒙”这种裸词是不够的知识图谱里需要完整实体名。做法是拿到subj/obj的起止位置之后向前后扩展把定语部分ATT关系也吸收进来。比如“华为公司”里的“公司”“国内领先的芯片企业”里的“国内领先的”。触发词冲突消解。同一个句子可能命中多个触发词比如“该公司发布了应用程序并获得了大量投资”这里“发布”和“投资”同时出现如果分别独立匹配会把主语“该公司”复制到两个三元组里这没问题。但如果是“华为公司成立于1987年主要研发通信设备”你希望抽出“华为公司-成立-1987年”而不希望把“通信设备”也当成宾语。解决办法是根据触发词的词性以及相邻触发词的语义类别做过滤。被动句处理。中文被动句虽然不像英文频率高但合同文本里常见“该技术被华为采用”。依存关系中被动句的主语和宾语跟主动句是反的单纯的SBVVOB规则会抽反。我习惯在规则里额外判断如果出现“被”字结构则交换subj和obj。这些规则每个看起来都很粗糙但叠加起来效果相当能打。我做过一次对比在一个垂直领域的2000句测试集上纯规则方案的F1大概在0.72左右加上实体边界扩展和被动句处理能提升到0.82。这些规则不依赖任何标注数据对冷启动项目来说价值非常大。3. 开放领域怎么落地把三元组抽取当成生成任务来做开放领域就没有捷径可走了规则方案在这里会快速失效。因为关系集合是开放的触发词表无法维护句式变化又极其丰富传统依存句法在长句和复合句上很容易崩。这种情况下应该把三元组抽取转换成文本生成问题输入一段文本输出结构化的“主体|关系|客体”模型自己决定头部实体、尾部实体以及它们之间的关系。3.1 生成式抽取的思想传统抽取是“看词类别”生成式抽取是“让模型写答案”。比如输入“苹果公司的创始人是史蒂夫·乔布斯”我们希望模型直接生成苹果公司 | 创始人 | 史蒂夫·乔布斯模型本质上是在做条件文本生成只是生成的格式被约束成了我们约定的SPO结构。因为有预训练模型强大的语义理解能力即使训练数据里没见过“乔布斯于1976年与沃兹尼亚克共同创立了苹果”这种表达它也能通过语义相似性把“共同创立了”映射到“创始人”关系上。现在工业界落地比较多的实现方案是PaddleNLP的UIE模型。UIE把抽取任务统一成“从文本中寻找符合schema的span”可以用一段schema灵活控制抽取目标所以既适用限定领域也可以用非常宽泛的schema强行做开放抽取。对开放领域schema可以设置成[实体, 关系]这种通用结构模型会尽量挖掘文本中潜在的三元组。3.2 直接使用预训练模型跑一个Demo如果你不想自己训模型PaddleNLP的Taskflow接口一行代码就能跑起来from paddlenlp import Taskflow # 开放schema不限制具体关系让模型自由抽取 schema [实体, 关系] ie Taskflow(information_extraction, schemaschema) results ie(苹果公司的创始人是史蒂夫·乔布斯总部位于美国加利福尼亚州库比蒂诺。) for res in results: print(res)输出大概是这样的结构[ {实体: [{text: 苹果公司, start: 0, end: 4}], 关系: [{text: 创始人, subject: 苹果公司, object: 史蒂夫·乔布斯}]}, {实体: [{text: 库比蒂诺, start: 18, end: 22}], 关系: [{text: 总部地点, subject: 苹果公司, object: 美国加利福尼亚州库比蒂诺}]} ]注意schema传成“实体”和“关系”是一个比较粗暴的开放策略模型确实会努力识别但同时也容易抽出一堆意义不大的关系比如“位于”“是”“属于”这种万能谓词。实际我建议还是按业务关心的关系类型给一个候选集合哪怕20个、30个边界仍然比真正的全开放可控得多。3.3 如果要自己训练数据怎么准备直接使用预训练模型是快速验证的方式。要追求更高准确率还是要用领域数据微调。这里的数据格式我建议统一成jsonlines每行一个样例{text: 苹果公司的创始人是史蒂夫·乔布斯总部位于美国加利福尼亚州库比蒂诺。, spo_list: [{predicate: 创始人, subject: 苹果公司, object: 史蒂夫·乔布斯}, {predicate: 总部地点, subject: 苹果公司, object: 美国加利福尼亚州库比蒂诺}]} {text: 清华大学成立于1911年坐落于北京西北郊。, spo_list: [{predicate: 成立时间, subject: 清华大学, object: 1911年}, {predicate: 所在地, subject: 清华大学, object: 北京西北郊}]}采集和标注的时候有三个要点别偷懒关系名要归一化。标注员如果一会儿标“出生于”一会儿标“出生在”一会儿标“出生年月”模型会被搞晕。最好在标注规范里定死每条关系的标准名让模型只在这套标准名里做选择。实体边界宁长勿短。像“美国加利福尼亚州库比蒂诺”这种带层级的地名刚开始标的时候很容易只标“库比蒂诺”或“加利福尼亚州”导致模型学到的边界不稳定。标注规范里应该明确“完整行政地名”还是“最小地名”。负样本要有。只给正样本模型会倾向于从所有句子里都硬抽三元组甚至编造不存在的实体关系。需要采集一批“没有目标关系”的文本标注成spo_list: []让模型学会拒识。有了数据之后如果用的是PaddleNLP UIE微调可以参考官方脚本。核心部分就是把上面的jsonlines读进去构造成模型需要的样本格式然后跑训练。如果用的是一般的生成式模型比如T5、ChatGLM、Qwen系列需要把三元组拼成模型输入input_text 苹果公司的创始人是史蒂夫·乔布斯总部位于美国加利福尼亚州库比蒂诺。 output_text 苹果公司 | 创始人 | 史蒂夫·乔布斯苹果公司 | 总部地点 | 美国加利福尼亚州库比蒂诺这种“text2text”的做法灵活性极高代价是推理速度比分类模型慢得多。我测试过用T5-small在CPU上推理单句平均延迟约300ms左右如果换到7B级别的模型单句延迟会涨到1秒以上对实时服务不太友好。生产环境一般会用一个小的生成模型做初筛再用规则和大模型结合的方式保证效果。3.4 推理时怎么把生成结果解析回三元组生成式模型返回的是字符串还需要一个解析函数把它转成结构化的SPO列表。解析逻辑重点是分句分隔符和字段分隔符的统一。我通常约定用分号分隔多个三元组用竖线分隔S、P、Odef parse_generated_triples(text): triples [] for item in text.split(): parts [p.strip() for p in item.split(|)] if len(parts) 3 and all(parts): triples.append({subject: parts[0], predicate: parts[1], object: parts[2]}) return triples这套解析逻辑看着简单但实际坑不少。模型生成时经常把“|”错生成“丨”或“I”分号可能生成成中文分号或英文分号空格数量也不可控。所以模型输出必须做字符归一化全角标点转半角、竖线类字符统一、小写转大写。我在代码里加了一个normalize函数来解决这个问题实践中解析错误率从15%直接降到3%以内。4. 两份代码都跑通之后用评测和坏case把模型打回原形代码能跑只是第一步真正的挑战做题评测。三元组抽取这个任务的评测比普通文本分类麻烦得多因为一个三元组有三个组成部分任何一个错位都算失败。4.1 评测指标怎么定我常用的指标是标准精确率、召回率、F1但提前要定义什么叫“对”。一条预测三元组和一条标准三元组相比完全匹配subject、predicate、object三者完全一致才算正确。这是最严格的匹配策略。部分匹配如果实体边界差一个字算半对或者不得分。这个需要看业务容忍度。实际评测中实体边界错误占了预测错误的很大比例。比如标准实体是“美国加利福尼亚州库比蒂诺”模型预测成“库比蒂诺”或“加利福尼亚州库比蒂诺”。如果严格按完全匹配这类结果全算错指标可能只有0.6几如果用宽松匹配指标能到0.8以上。所以我建议每次跑评测两个指标都算一遍方便定位问题到底出在实体边界还是在关系分类上。计算F1时有几个细节值得注意。一个句子如果抽出了重复三元组先去重再算不存在于标准答案里的三元组一律算假阳性多条候选三元组置信度相同时优先保留主语和宾语更长的因为长实体通常信息量更大。4.2 限定方案的典型坏case与排查方法限定领域规则方案的坏case主要集中在三个位置第一触发词重叠导致关系误判。比如“该公司成立了一个研发中心并发布了新产品”这里“成立”是触发词但它的宾语不是时间而是“研发中心”最后抽成“该公司-成立-研发中心”。这是规则方案最常见的错误。解决办法是给每个触发词维护一个“宾语语义类型”配置文件比如“成立”这个触发词如果宾语是时间类型构成“成立时间”关系如果是机构类型构成“设立机构”关系。需要给宾语加一个实体类型分类器或者用词性和语义词典粗判。第二实体嵌套。例如“北京大学人民医院”词典里既有“北京大学”又有“北京大学人民医院”匹配时会优先抽成“北京大学”丢掉了完整实体。这个问题需要引入最长匹配优先策略同时配合上下文判断。对知识图谱来说实体丢失比实体多一块更致命。第三依存句法错误。长句和并列句里LTP的输出经常是错的。比如“华为发布了鸿蒙系统小米发布了澎湃OS”依存分析可能把“小米”挂到前一个句子的“发布”下面。排查时先把长句按标点或连接词切分成短句再逐句抽取正确率会明显回升。4.3 开放方案的典型坏case与排查方法开放方案的问题类型不太一样最突出的是“幻觉”。模型生成了“苹果公司-依赖-华为公司”这种原文里完全没有的三元组因为它觉得两个实体之间“应该有”关系。幻觉的根源在于生成式模型过度自信尤其在零样本场景下没有负样本约束。关系名不统一也是重灾区。模型可能一会儿生成“创始人是”一会儿生成“创立了”一会儿生成“创始人”即使你在训练数据里规定了一致的关系名在开放schema下还是难以完全约束。可以从两个方向改进一是推理时加入schema引导在prompt里明确提示“只输出以下关系名列表中的关系不要自创”二是在后处理时把得到的关系名做一个字符串相似度聚类映射到标准关系名上。排查这类坏case时我的思路是先看实体对不对再看关系对不对。如果实体边界大范围错误问题多半出在训练数据标注一致性和模型解码策略如果实体基本正确但关系名称五花八门多半是关系schema设计不够小、prompt约束不够强。这个排查链路的先后顺序非常关键直接对模型动手调参往往是浪费时间的开始。4.4 坏case处理流程总结我把排查流程整理成四步每次都按这个顺序走抽样统计抽200条预测错误的样本按错误类型分类统计占比分布。定位成分对每类错误确认是subject、predicate、object的哪一个成分错了。追溯机制判断是词典问题、触发词问题、句法问题还是模型生成问题。专项优化一次只针对占比最大的错误类型优化不要试图同时修所有问题。这个流程听着简单但实际执行很考验耐心。我见过不少团队一开始就想着换更大的模型结果坏case换了一批又一批问题没解决成本和延迟反而上来了。先做统计分析再决定用规则修还是用模型修这个习惯非常关键。5. 项目落地时最容易翻车的四个细节以及我的应对最后把工程实践里容易踩的坑集中讲一下。这些细节在很大程度上决定了模型效果能不能变成真正的产品价值。5.1 文本清洗别让无关字符破坏句法结构进抽取管道之前的数据往往带着各种全角半角符号、HTML标签、URL、乱码字符。依存句法分析器对异常字符非常敏感。比如全角括号和半角括号混在一起LTP可能把括号切成一个独立token结果导致括号位置的依存关系乱成一团主谓宾全部错位。我的清洗策略是先做Unicode规范化统一全角半角再删除无用的URL和标签但注意不要对句子做重度停用词过滤因为“被”“由”“把”这些虚词在依存句法里承担着重要的结构作用删了之后句法结构直接断裂。这一点跟做文本分类时的预处理习惯完全相反特别容易踩坑。5.2 长文分句别把整个段落喂给分析器很多短文档几句读完但研报、合同、论文经常一句话几十个字甚至上百字。LTP这种分析器在长句上的准确率会明显下降而且消耗的时间会翻好几倍。所以我在管道里加了分句逻辑先按句号、问号、感叹号分句再把超过60个字的句子按逗号进一步切分。切分时注意保留主语信息比如“华为发布了鸿蒙系统该系统支持多设备协同”应该切成“华为发布了鸿蒙系统”和“该系统支持多设备协同”而不是把“该系统”单独扔了。5.3 实体对齐与回填抽取结果不是终点抽取出来的三元组如果只是打印出来看看那确实没什么问题。但一旦要导入知识图谱你会发现“苹果公司”和“Apple Inc.”和“苹果”都指向同一个实体“华为”和“HUAWEI”也是同一个。不做实体对齐图谱里就会充满重复节点查询质量一塌糊涂。我的经验是抽取结果先落到一个中间表做一步实体融合按名称、别名、官网、统一社会信用代码等属性做匹配。如果没有这类属性就用字符串相似度加人工抽检。实体对齐这一步虽然不是抽取本身但直接决定了下游图谱能不能用。宁可前期多做一点也别等图谱上线后再清洗。5.4 部署形态与结果去重规则吃CPU模型吃GPU限定领域的词典加依存句法方案纯CPU就能跑吞吐量看机器性能单核每秒能处理2到5句话。生成式方案则几乎必须用GPU尤其是超过1B参数的模型CPU推理会让用户等到怀疑人生。所以工程上最好不要把两种方案混在一台机器上部署我习惯把它们拆成两个独立的抽取服务通过消息队列串联。结果合并也有讲究。同一篇文档里的句子经常会重复表达同一个三元组比如第一句说“苹果公司的创始人是乔布斯”最后一句又说“乔布斯是苹果的联合创始人”。语义上这两个是同一个三元组但字面上却完全不一样。在合并阶段我会按主语和客体的归一化结果加关系名做一次去重保留置信度最高的一条。这个后处理步骤虽然简单却能显著提升图谱数据的整洁度。最后再分享一个小技巧不管限定还是开放抽取出来的SPO列表统一封装成同样的JSON结构这样下游不管是写CSV、入Neo4j还是导入Elasticsearch只需要写一个适配器而不是为每套方案单独定制接口。我踩过好几次“方案换了下游链路全部重写”的坑统一的SPO结构是避免这个坑的最简单办法。
返回列表