知识图谱构建全流程:数据采集、实体识别、关系抽取、存储可视化
说实话,很多朋友一上来就问我知识图谱怎么搭,脑子里全是图数据库、嵌套查询这些名词。但我得泼盆冷水——真正决定你图谱质量的,往往是这些看似基础的前三步。今天咱们就把这五个环节掰开揉碎了讲一遍。
我自己做过几个医疗领域的知识图谱项目,踩过的坑比代码行数还多。就拿COVID-19这个场景来说,文献数据乱、实体重叠多、关系类型复杂,每一步都有讲究。
3.1 数据采集——源头决定了天花板
数据源大致分三类:
- 结构化数据:像DrugBank、ClinicalTrials.gov这些DB导出的表格,字段清晰,拿来就能用。
- 半结构化数据:PubMed上的XML、JSON格式论文元数据,需要解析。
- 非结构化数据:PDF全文、新闻、临床试验报告,文本里全是宝藏。
我个人习惯把采集脚本写成可配置的Pipeline。你看这个例子:
# 采集PubMed文献摘要(伪代码)
import requests
from bs4 import BeautifulSoup
base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
params = {
"db": "pubmed",
"term": "COVID-19 AND drug",
"retmax": 1000
}
response = requests.get(base_url, params=params)
soup = BeautifulSoup(response.text, "xml")
id_list = [id_tag.text for id_tag in soup.find_all("Id")]
小技巧:采集PubMed时加个 usehistory=y 参数,能把检索结果暂存到服务器,分页取数据效率高不少。我最初不知道这个,每次重复请求被限速卡得头皮发麻。
曾经有一次我爬取ClinicalTrials.gov的XML,没注意接口的XML Schema半年更新了一次——结果解析器全崩了。所以记得给采集脚本做版本控制,每次运行前先校验Schema。
注意:别碰未经授权的医学数据库。许多期刊的全文受版权保护,只能爬公开摘要。另外,API要控制频率,别被封IP。
3.2 实体识别——把“药名”“基因”从文本里捞出来
实体识别(NER)是知识图谱的骨架。你想想看,一句话“瑞德西韦抑制SARS-CoV-2的RNA聚合酶”,你得认出“瑞德西韦”(药物)、“SARS-CoV-2”(病毒)、“RNA聚合酶”(靶点)。
常用的方法就两种:
- 基于规则:用正则匹配已知词表。适合专用术语明确的场景。
- 基于模型:用BERT、BioBERT这类预训练模型做序列标注。准确率高,但需要标注数据。
我推荐先用规则跑一遍召回,再用模型做精排,双轮过滤效果好。
举个实操例子,用SpaCy配合自定义管道做NER:
import spacy
from spacy.pipeline import EntityRuler
nlp = spacy.load("en_core_sci_sm")
ruler = nlp.add_pipe("entity_ruler", before="ner")
patterns = [
{"label": "DRUG", "pattern": "Remdesivir"},
{"label": "DRUG", "pattern": "Favipiravir"},
{"label": "VIRUS", "pattern": [{"LOWER": "sars"}, {"LOWER": "-"}, {"LOWER": "cov"}, {"LOWER": "-"}, {"LOWER": "2"}]}
]
ruler.add_patterns(patterns)
text = "Remdesivir shows efficacy against SARS-CoV-2."
doc = nlp(text)
for ent in doc.ents:
print(ent.text, ent.label_)
这里要注意:别让规则和模型冲突。我吃过亏,规则给“瑞德西韦”标了DRUG,模型又输出一个“瑞德西韦”标MISC,最后得合并或做投票。
关键点:实体归一化(Entity Linking)别忘。比如“SARS-CoV-2”“2019-nCoV”“COVID-19病毒”在文本里可能指同一个实体,你得把它们映射到标准ID(比如NCBI Taxonomy ID: 2697049)。这一步不做,图谱里全是别名,查都查不匀。
3.3 关系抽取——连点成线
实体识别完了,知识图谱还是散沙。你得把“药物-靶点”“药物-副作用”这些关系抽出来。
关系抽取分三种粒度:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 基于模板 | 写正则/规则匹配特定句式 | 少样本、关系类型固定 |
| 远程监督 | 利用知识库自动标注语料 | 大规模数据,但噪声多 |
| 端到端模型 | 用RE+NER联合训练 | 高质量标注充足时 |
我见过最坑的事:远程监督里把“A抑制B”和“A不被B抑制”标成同一种关系。后来我给每条关系加了个否定词检测模块,准确率从62%拉到81%。
下面是基于依存句法树的简单模板示例:
# 利用Stanford CoreNLP的依赖分析
def extract_interaction(sentence):
# 假设依赖树中"inhibit"是root
for token in doc:
if token.dep_ == "nsubj" and token.head.lemma_ == "inhibit":
drug = token.text
obj = [child for child in token.head.children if child.dep_ == "dobj"][0].text
return ("INHIBITS", drug, obj)
return None
当然,这只是个演示。实际生产中我建议用微调好的BioBERT或PubMedBERT做关系分类。把两个实体用 [E1] [E2] 标记后丢进去。比如:
Input: [CLS] [E1]Remdesivir[E1] inhibits [E2]SARS-CoV-2[E2] activity [SEP]
Output: INHIBITS (with probability 0.97)
为什么要用标记?因为模型需要知道哪两个词是候选实体对,不然它不知道要分类哪一对。
3.4 存储与可视化——让知识跑起来
图谱存哪儿?目前主流就两个选择:
- Neo4j:属性图模型,Cypher查询极方便。
- JanusGraph:支持超大规模图,需要HBase/Cassandra做后端。
我个人偏好Neo4j做原型快速验证。导入数据用LOAD CSV:
LOAD CSV WITH HEADERS FROM 'file:///drug_triples.csv' AS row
MERGE (d:Drug {name: row.drug})
MERGE (t:Target {name: row.target})
CREATE (d)-[:INHIBITS]->(t)
可视化方面,我习惯先在Neo4j Browser里跑个MATCH看看骚不骚(比如看看瑞德西韦关联了多少个蛋白)。
但为了课程演示,我们还是用轻量级的工具。下面这张SVG图展示了整个构建流程的核心逻辑:
嗯,这个流程看着简单,实际做起来每一步都有坑。比如实体识别阶段没有做统一ID映射,到关系阶段会出现“瑞德西韦”和“Remdesivir”两种写法,存进图里直接被当作两个节点。我后来加了个字典映射表,才把数据洗干净。
推荐工具链:数据采集用Scrapy + Requests + Selenium(动态页面);实体识别用SpaCy + BioBERT;关系抽取用微调Transformers;存储用Neo4j;可视化先跑Neo4j Browser,再用Gephi出图。
3.5 避坑总结
最后给几个我亲自踩过的坑:
- 关系类型设计太细:一开始我设了20+种关系,结果标注一致性只有40%。后来合并成12种核心关系,模型效果立竿见影。
- 别名没归一化:同药不同名导致图谱里重复节点。必须做名称消歧。
- 没做质量验证:存进图之前最好抽样100条三元组人工校验。如果关系准确率低于85%,别急着往下走。
- 忽略文档上下文:同一个句子里的实体有时隔了30个词,关系抽取模型要能捕捉长依赖——用Transformer比LSTM好太多。
说白了,知识图谱构建不是一次性的瀑布流,而是一个不断修正的循环。你今天跑通的pipeline,明天换一组数据可能就崩了。所以给你的代码加上足够的容错和日志。
嗯,这一章到这里。接下来去动手写你的第一个Pipeline吧!