从零构建农业知识图谱:本体建模、NER与关系抽取全流程实践
2026/9/17 18:07:52 网站建设 项目流程

简介:面向智慧农业的知识图谱项目AgriKG,源自上海农业农村大数据共享服务平台建设课题,由华东师范大学数据科学与工程学院构建,旨在基于碎片化农业大数据实现农业领域信息检索、命名实体识别、关系抽取、智能问答与辅助决策。压缩包内包含完整项目代码与数据,共462个文件,涵盖Python脚本(114个,用于实体识别、关系抽取及后端逻辑)、JavaScript与前端页面(88个JS、31个HTML、27个CSS,呈现可视化界面)、JSON/CSV数据文件(17个JSON、16个CSV,存放农业知识数据)、文本与文档(56个TXT、13个MD)等,包体约349.79MB。项目虽已停止维护,代码仅供参考,但数据可免费用于学术等非商业用途,配套DASFAA 2019论文有助于理解整体设计思路。目前已有1452人学习下载,适合NLP、知识图谱及智慧农业方向的研究者参考其工程实现与数据组织方式。

1. AgriKG 要解决的信息检索难题,不是搜不到,而是搜不准

同样是“水稻得了稻瘟病怎么办”,地方植保站存的是 Word 表格,期刊论文里是完整的发病规律和防治方案,农户微信群里的说法可能是“稻叶上长灰色斑点”。把这些不同形态、不同术语的信息统一检索出来,再给出一份可执行的用药建议,传统关键词搜索做不到,原因是“稻瘟病”“叶瘟”“稻热病”指向的是同一个实体。农业知识图谱(AgriKG)就是为这类场景落地的数据组织方式:先用命名实体识别把非结构化文本里的领域名词对齐成统一实体,再用关系抽取把“作物—病害—农药”之间的语义连接抽出来,存成图结构,最后通过智能问答和辅助决策把知识返回给使用者。适合正在做智慧农业、农业信息检索或者垂直领域知识图谱的工程师——本文只讨论你自己能从零搭起来的那一套实现路径。

2. 农业知识图谱的本体建模与图存储:先画关系,再做抽取

2.1 农业本体设计的最小闭环:实体和关系不能拍脑袋定

知识图谱构建有一个常见误区:拿到一批农业语料就立刻跑命名实体识别,指望模型把结构“吐”出来。实际工程上顺序是反的,先把要支持的高频问句列出来,反推实体类型和关系集合,再去组织 NLP 流程。

农业领域的实体并不复杂,但需要明确边界。作物、品种、病害、虫害、天敌、农药、肥料、土壤、气象这类是实体;品种具有抗性、农药具有剂量和安全间隔期,这是属性。真正决定图谱检索价值的是一组关系,最少只要六类就能覆盖绝大多数农业问句。

实体类型典型属性在图谱中的角色
Crop(作物)学名、别名、类别问题主体
Disease(病害)病原、发病条件需要防治的对象
Pest(虫害)拉丁名、为害部位需要防治的对象
Pesticide(农药)剂量、安全间隔期防治手段
AgronomicOp(农事操作)时间、要点栽培指导

关系集合建议按“闭环”设计。比如作物和病害之间建立 HAS_DISEASE,病害和农药之间建立 CONTROLLED_BY,作物和品种之间建立 HAS_VARIETY。这样用户问“水稻得稻瘟病打什么药”,图谱才能沿 Crop → Disease → Pesticide 这条路径把答案找出来。如果只有实体没有关系边,图谱就退化成一张属性表,辅助决策也无从谈起。

实际项目中我一般会在白板上画一张闭环图:作物感染病虫害,病虫害被农药控制,农事操作影响作物生长,土壤与气象影响病虫害发生。这个循环里的每条边都对应至少一个用户高频问句,没有用户问句支持的关系类型,第一版先不建。

2.2 用 Cypher 把本体落进 Neo4j:命名规范与约束先行

图谱存储选 Neo4j 是出于查询便捷性的考虑,Cypher 对路径检索的表达能力强于 SQL 多层 JOIN。建库第一步不是灌数据,而是立约束。下面这段 CQL 可以直接在建库阶段执行:

CREATE CONSTRAINT crop_name_unique IF NOT EXISTS FOR (c:Crop) REQUIRE c.name IS UNIQUE; CREATE CONSTRAINT pest_name_unique IF NOT EXISTS FOR (p:Pest) REQUIRE p.name IS UNIQUE; CREATE INDEX IF NOT EXISTS FOR (d:Disease) ON (d.name);

约束的作用是保证后面 MERGE 不产生重复节点。注意这里唯一键选择的是 name 字段,那么写入前必须先把“玉米”“苞米”“棒子”这类别名归一到同名,否则同名异写会绕过约束产生两个节点。索引给 Disease 等高频实体类型单独建,查询走点查时性能差一个数量级。

接着写入第一批本体实例:

MERGE (c:Crop {name: '水稻'}) SET c.latinName = 'Oryza sativa', c.category = '粮食作物' MERGE (p:Pest {name: '二化螟'}) SET p.feedingPart = '茎部' MERGE (c)-[:HAS_PEST {certainty: 0.97, source: '地方植保手册'}]->(p);

MERGE 会先按 name 查约束,命中即返回现有节点,未命中才创建。SET 用来补属性,避免重复执行脚本时覆盖已有信息。关系上的 certainty 和 source 两个属性建议一定保留:前者在多源数据冲突时做取舍依据,后者保证辅助决策里每个答案都能回溯到原始文献或手册,这是农业知识图谱和通用百科图谱的一大区别。

2.3 批量导入三元组:LOAD CSV 的顺序和避坑方式

手工 MERGE 只适合验证阶段。真实语料抽出的三元组一般有几十万条,统一走 CSV 导入。Neo4j 的 import 目录下放好边表文件后执行:

LOAD CSV WITH HEADERS FROM 'file:///agri_kg_edges.csv' AS row MATCH (head:Crop {name: row.head}) MATCH (tail:Disease {name: row.tail}) MERGE (head)-[:HAS_DISEASE {source: row.source, certainty: toFloat(row.certainty)}]->(tail);

这里有个顺序问题:必须先用 MATCH 锁定头尾实体,再 MERGE 关系。如果跳过 MATCH 直接用 MERGE 关系,会在实体不存在时把缺失节点一并创建出来,后续检查数据时很难排查。csv 里的一行如果匹配不到实体,这一条边会被跳过,所以边表入库前一定要先确认实体表完整。导入完成后用路径查询验证一下连通性:

MATCH path = (:Crop {name: '水稻'})-[:HAS_PEST]->(:Pest) RETURN path LIMIT 10;

查结果时多用 LIMIT,尤其是网页端 Neo4j Browser,数据量大时全量 RETURN 很容易让浏览器卡死。这一步验证的不是“有没有数据”,而是“关系方向对不对”。很多项目实体抽取做得不错,但边方向建反了,导致问答阶段查不到路径。

3. 农业命名实体识别:低成本词典召回与NER微调的组合方案

3.1 农业实体为什么不能直接用通用NER

通用命名实体识别在新闻语料上表现很好,但放到农业领域会出现三个具体问题:一是别名分散,“玉米”“苞米”“棒子”代表同一作物,通用模型不会把三者映射到统一实体;二是嵌套实体多,“水稻条纹叶枯病”可以拆成“水稻”和“条纹叶枯病”两个实体,前者是作物名后者是病害名,但文本中紧挨着出现,BIOES 标签体系下容易互相干扰;三是新品种、新农药不断冒出来,语料里出现“龙粳31”“稻花香2号”这类不在预训练词表里的词,分词器一拆就碎。

所以在 AgriKG 项目里,推荐顺序是先上词典召回拿到确定性的收益,再用序列标注模型去覆盖未见词和口语化表达。词典不需要一开始做全,把本体表里已有的实体名和常见别名输进去即可,后边边抽边补。

3.2 用 Aho-Corasick 做农业词典精准召回

Python 里做词典匹配用 pyahocorasick,它把词典构造成 AC 自动机,匹配速度和处理词条数量关系不大。核心代码:

import pyahocorasick from typing import List, Tuple agri_dict = [ ("水稻", "Crop"), ("玉米", "Crop"), ("稻瘟病", "Disease"), ("二化螟", "Pest"), ("高粱条螟", "Pest"), ] automaton = pyahocorasick.Automaton() for idx, (name, etype) in enumerate(agri_dict): automaton.add_word(name, (idx, etype)) automaton.make_automaton() def entity_recall(text: str) -> List[Tuple[int, int, str, str]]: results = [] for end_idx, (idx, etype) in automaton.iter(text): start = end_idx - len(agri_dict[idx][0]) + 1 results.append((start, end_idx + 1, agri_dict[idx][0], etype)) return results

讲一下两个关键点。make_automaton 之后不能再 add_word,所以要扩充词典只能重新构建自动机,词典量大时可以考虑把实体表放数据库,启动时加载进内存再构建。iter 返回的 end_idx 是匹配词最后一个字符的索引,所以 start 要回退一个词长,这里很多人会错写成 end_idx - len + 0,导致后续把标签切到错误边界上。

词典召回的结果还需要做一个最长匹配去重。例如句子“水稻条纹叶枯病”会同时命中“水稻”和“条纹叶枯病”,这是期望结果;但如果词典里有“稻瘟病”和“水稻稻瘟病”,需要优先保留较长的那一个,标准做法是把重叠区间按长度排序,保留最长且后处理的实体标注。

3.3 用 BERT-CRF 序列标注兜底未见词

词典召回覆盖不了新出现的品种名和农户口语缩写,这一层交给序列标注。把语料切分成字符级 token,标注 BIOES 标签。例如:

水 B-Crop 稻 I-Crop 条 B-Disease 纹 I-Disease 叶 I-Disease 枯 I-Disease 病 E-Disease

使用 BIOES 的原因在于它强制要求 B 和 E 成对出现,对嵌套实体边界的约束比 BIO 强。模型加载和训练参数的经验值如下:

from transformers import AutoModelForTokenClassification, AutoTokenizer, TrainingArguments label_list = ["O", "B-Crop", "I-Crop", "B-Disease", "I-Disease", "B-Pest", "I-Pest"] model = AutoModelForTokenClassification.from_pretrained( "bert-base-chinese", num_labels=len(label_list) ) tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") training_args = TrainingArguments( output_dir="./agri_ner_model", learning_rate=2e-5, per_device_train_batch_size=32, per_device_eval_batch_size=64, num_train_epochs=5, evaluation_strategy="epoch", save_strategy="epoch", )

几个参数的调整逻辑:学习率从 5e-5 降到 2e-5,是防止小语料上微调 BERT 时标签层波动过大,农业标注数据通常只有几千句,不属于大数据场景;max_length 建议固定 128,不要贪长——农业论文句子动辄上百字,直接截断会切断实体边界,需要在预处理阶段按句号、分号切句再进模型;epoch 数设为 5 但配合早停,看验证集 F1 不升就停,训练 10 个以上 epoch 模型会过拟合到语料里的文献格式,对口语问句的泛化不升反降。

3.4 实体归一化:识别之后必须做别名映射

模型识别出来的“苞米”“玉米棒子”都要归一到 canonical name 才能匹配图里的唯一约束。用一张别名表做映射:

ENTITY_ALIAS = { "苞米": "玉米", "玉米棒子": "玉米", "稻热病": "稻瘟病", } def normalize(name: str) -> str: return ENTITY_ALIAS.get(name, name)

归一化发生在实体识别和关系抽取之间。不在这一步做,后边关系抽取抽出的三元组会出现“苞米—HAS_DISEASE—大斑病”,和“玉米—HAS_DISEASE—大斑病”两条记录,图谱里形成两个节点,查询时按“玉米”永远搜不到“苞米”的数据。别名表从哪来?第一版用人工整理,跑起来之后每天从搜索日志里挖掘未命中的实体名,按编辑距离自动生成候选别名,人工确认后再追加。

4. 农业关系抽取:从触发词模板到远程监督回灌

4.1 关系集合是有限且可控的

关系抽取不是开放式的“抽取所有可能关系”,而是在第 2 章定义的有限关系集合里做分类。对 AgriKG 第一版来说,关系类型控制在 6 到 8 个就够了:

关系头实体尾实体示例三元组
HAS_DISEASE作物病害(水稻, HAS_DISEASE, 稻瘟病)
HAS_PEST作物虫害(水稻, HAS_PEST, 二化螟)
CONTROLLED_BY病害/虫害农药(稻瘟病, CONTROLLED_BY, 三环唑)
HAS_VARIETY品种作物(龙粳31, HAS_VARIETY, 水稻)
PREFERRED_SOIL作物土壤(水稻, PREFERRED_SOIL, 酸性土)
AFFECTED_BY_METEO病虫害气象条件(稻瘟病, AFFECTED_BY_METEO, 阴雨高湿)

关系越少,远程监督的样本质量越好控制。每定义一个关系,先确认它对应的问句确实存在,否则这张表会越滚越大,最后变成标注噩梦。

4.2 触发词加正则模板——最少代码跑通关系抽取

对农业技术文献这种句式相对固定的文本,触发词模板是投入产出比最高的方案。下面的代码直接复用第 3 章的实体识别结果:

import re PATTERNS = [ (r"(?P<crop>.{2,20})(?:感染|发生|患上)(?P<disease>.{2,20})", "HAS_DISEASE"), (r"(?P<disease>.{2,20})(?:可(?:用|以)|使用|喷施)(?P<pesticide>.{2,20})(?:防治|治疗)", "CONTROLLED_BY"), ] def extract_triples(text: str, entities: list) -> list: triples = [] for pattern, rel_type in PATTERNS: for m in pattern.finditer(text): head = m.groupdict().get("crop") or m.groupdict().get("disease") tail = m.groupdict().get("disease") or m.groupdict().get("pesticide") if head in entities and tail in entities: triples.append((head, rel_type, tail)) return triples

触发词是关系抽取里最重要的参数。农业文献写到防治方案时,习惯句式是“XX 可用 XX 防治”,所以 CONTROLLED_BY 的模板里把“可用”“使用”“喷施”作为触发词,并且把“防治”“治疗”放在农药后面做闭合标记。这个模板能覆盖标准文献里七成左右的防治关系,剩下的分散在长难句、被动句里,交给下一节。

正则模板的局限要认清:它只能处理同一短句内的实体对,跨句关联如“水稻在本田期易感病。稻瘟病在多雨年份流行”就抽不出来。解决路径是加一层依存句法分析,把“易感”的 nsubj 和 obj 对应到实体,但维护成本高,建议先做远程监督。

4.3 远程监督:用种子三元组自动生成训练语料

远程监督的思路是跨句级的:已知图谱里有三元组 (稻瘟病, CONTROLLED_BY, 三环唑),现在拿这条三元组去匹配语料库里所有同时包含“稻瘟病”和“三环唑”的句子,把这些句子全部标记为 CONTROLLED_BY 的正样本,送入关系分类模型训练。实现步骤分为三步。

第一步,构建种子三元组文件,可以从植保手册、农药登记数据库里的结构化数据直接转换。第二步,对每个句子做实体召回,打上 BIOES 标签,然后按实体对检索匹配句子。第三步,训练一个句子级别的关系分类器,输入是“头实体 + [SEP] + 尾实体 + [SEP] + 整句文本”,输出是关系类别或 none。

远程监督有一个要处理的经典问题:一句包含“水稻”和“稻瘟病”的文本,可能只是在讲栽培历史,并没有直接表达“感染”关系,却被正样本误标。常见处理是降低置信度——把远程监督自动标注的数据称为候选正样本,交由规则或人工抽检,置信度低于阈值的句子不进训练集。使用大语言模型做候选样本预筛选也有效,但筛完仍需保留可解释的触发词证据,否则样本质量难以审计。

4.4 三元组冲突消解:同一实体对多条关系的取舍

从多个来源抽取三元组时,同一实体对可能出现多条相同类型关系,有的置信度 0.95,有的只有 0.6。需要做一次去重合并:

MATCH (a:Crop {name: '水稻'})-[r:HAS_DISEASE]->(b:Disease) WITH a, b, collect(r) AS rels WHERE size(rels) > 1 UNWIND rels AS r WITH a, b, rels, r ORDER BY r.certainty DESC WITH a, b, collect(r)[1..] AS to_delete UNWIND to_delete AS r DELETE r

这段 Cypher 的执行顺序是:先找出水稻所有指向同一病害且类型相同的边,按 certainty 降序排列,切掉第一条最可信的,把剩余低置信度关系删除。注意 DELETE 前先把 to_delete 收集完整再执行,不要在 UNWIND 过程中直接删,否则列表遍历时元素变化会漏删。生产环境更稳妥的做法是不物理删除,而是给每条边增加 status 字段标记 active/inactive,查询时过滤,这样误删还能找回。

5. 农业智能问答与辅助决策:意图模板加参数化Cypher的实现

5.1 问句意图与查询模板映射

知识图谱问答层的核心不是大模型,而是把用户问句稳定映射到图查询。先把意图类型收敛成十来类,例如病害防治、虫害防治、品种推荐、栽培要点、农资用法。用户输入“水稻出穗期遇到阴雨还得了稻瘟病该咋办”,先抽作物实体“水稻”、病害实体“稻瘟病”,意图分类判定为控制方案查询,再映射到预定义查询模板。

5.2 参数化 Cypher 模板:避免字符串拼查询

意图模板确定后,用 Neo4j Python Driver 传参数执行:

MATCH (c:Crop {name: $cropName})-[:HAS_DISEASE]->(d:Disease {name: $diseaseName}) MATCH (d)-[:CONTROLLED_BY]->(p:Pesticide) RETURN p.name AS pesticide, p.dosage AS dosage, p.safetyInterval AS safetyInterval

参数化查询有两个好处:一是防止用户输入文本拼进 Cypher 造成注入,二是利用 Neo4j 的查询计划缓存。cropName 和 diseaseName 必须传归一化后的名称,否则用户说“苞米”而库里只有“玉米”,查询直接返回空。问答接口层做一层实体链接,把前端识别出的实体别名映射到 canonical name 再传入模板。

5.3 用负样本路径做回归验证

一个容易被忽略的验收手段是构造负路径测试:故意查询不存在的疾病或错配的作物—病害组合,验证图谱是否返回了不该返回的结果。例如玉米不会感染稻瘟病,那么查询“玉米—稻瘟病”的关系路径必须返回空。把这些恒空查询写进 CI 脚本,每次图谱数据更新后自动跑一遍。负路径测试能检测出实体归一化错误造成的误关联,也能暴露远程监督样本把实体的错误关系灌进图里,这两类问题在农业知识图谱里比抽取缺失更致命——因为用户搜不到会换词再搜,搜到错的对决策的破坏无法估计。

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

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

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

立即咨询