知识图谱与 LLM 的实战应用——命名实体歧义消解

14 阅读1小时+

本章涵盖

  • 将命名实体消歧与知识图谱技术结合起来
  • 从多个来源构建知识图谱
  • 执行高级分析

自然语言处理(NLP)技术在从非结构化数据中自动构建知识图谱(KG)的过程中发挥着关键作用。这个过程中的一项关键任务是命名实体识别(NER),它用于识别原始文本中与任务相关的命名实体提及。NER 会将这些实体归入预定义类别,例如人、组织、地点或疾病。尽管 NER 是构建 KG 的重要组成部分,但它并不能让我们在具体应用领域中对文本获得足够精确的理解。

7.1 从识别到消歧

设想你正在开发一个智能咨询系统(IAS),用于支持医疗领域利益相关者的工作。此类 IAS 的一个关键属性是交互性,也就是系统能够通过多轮交互与人类交换信息的能力。支持这种信息交换的能力包括:

  • 在自然语言中识别有意义的实体
  • 从不同知识源中检索关于这些实体的信息

仅靠 NER 推理并不能提供这些能力。举例来说,看看下面这段摘自欧洲疾病预防与控制中心(ECDC)[1] 每周公报的文字:

In the week of 13 April, Belize reported for the first time mosquito-borne Zika virus transmission. Update on the observed increase of congenital Zika syndrome and other neurological complications Microcephaly and other fetal malformations potentially associated with Zika virus infection.

在这段很短的文本中,“Zika” 一共出现了三次。然而,它的含义会随着上下文而变化。假设我们在这个特定任务上能够取得很高的准确率,那么一个 NER 模型会识别出:第一次出现指的是一种病毒;而第二次和第三次出现则指向疾病实体。那么,这两次疾病实体是否代表同一种疾病?表面上看似乎是的,因为它们都提到了 “Zika”。但在医学这类关键领域中,我们必须尽可能精确。对于某些用例来说,区分传统意义上的 Zika 疾病或感染(第三次提及)与其先天性形式(第二次提及)至关重要。对于领域专家来说,对这类术语进行消歧可能并不难,但一旦文档规模增大,这件事就会变得不切实际。

这些问题可以通过另一项 NLP 任务来解决:命名实体消歧[2](NED)。NED 的目标是:通过考察每次提及的上下文,并将该提及连接到知识库中的某个实体,从而自动消除 “Zika” 含义中的不确定性或歧义。这样的“基准实体”以及其参考知识库的一个例子,就是统一医学语言系统(UMLS)[3] 中的 Congenital Zika virus infection 实体。它与 Zika virusZika virus infection 这两个同样被 UMLS 定义的实体不同。图 7.1 展示了 UMLS 网站上的一个截图,其中给出了关于 Zika virus 实体的详细信息。

image.png

图 7.1 UMLS 网站截图,展示了 Zika Virus 实体的信息。这些信息包括名称、ID(概念唯一标识符,CUI)、语义类型,以及一组定义、上位概念和下位概念。

通过将原始文本中的 “Zika” 提及映射到实体,我们满足了 IAS 关于“识别有意义实体”的要求:在这里,它对应的是一个 UMLS 实体。但这还不是全部。通过这种映射,我们实际上也为满足 IAS 的第二个要求创造了条件,也就是从多个知识源中检索信息。以识别出的 UMLS 实体为起点,我们可以继续探索多个生物医学本体中与这些抽取实体相关的上下文知识。例如,医学本体允许我们检索示例中这些消歧实体之间的连接关系。在某些情况下,它们会显式定义非常直接的关系,例如规定 Zika virus 是这两种综合征的 CAUSATIVE_AGENT(致病因子) 。还有一些关系则没那么直接:例如,Campylobacter infectionAIDS 可以建立联系,因为前者会影响肠道,而后者有一种特定形式——AIDS with intestinal malabsorption——同样涉及肠道。

本章将演示如何把 NED 模型应用到医疗领域的文本内容中。基于我们与欧盟委员会卫生与食品安全总司(DG SANTE)合作项目中的实践经验,我们首先介绍 NED 模型的总体情况,并通过示例说明如何完成消歧以及与生物医学本体的互联。随后,我们将介绍流程中涉及的文档与本体。最后,我们将展示如何自动构建一个真实世界的 KG,以便从多份技术文档中抽取并整合信息,形成统一视图。

练习

这个练习将帮助你识别 NED 的不同视角。在 Zika 的例子中,我们已经看到:同一个术语会根据上下文指向不同实体。但反过来的情况也可能出现:在某些情况下,不同术语会指向同一个实体。你能找出一个例子吗?(提示:查看本章中提到的医学实体示例。)在建模这些情况时,KG 有什么用?

7.2 理解命名实体消歧

知识库在收集某一特定领域中实体的结构化表示方面扮演核心角色。NER 并不能消除已识别为命名实体的提及所带来的不确定性,因此我们需要将文本中的提及链接到参考知识库中的正确实体。这个链接步骤由 NED 系统完成,而 NED 系统通常包括三个主要阶段:

  • 候选选择(Candidate selection)
  • 候选排序(Candidate ranking)
  • 本体集成(Ontology integration)

图 7.2 展示了一个典型的基于 KG 的 NED 系统的思维模型。

image.png

图 7.2 NED 系统架构,包括三个主要阶段:候选选择、候选排序和本体集成

候选选择用于为识别出的命名实体提及找出最佳候选项。这个选择过程是在现有知识库上进行的,该知识库包含有用的结构化信息,从而能够精确地区分不同实体。候选排序则会基于上下文信息(即被识别实体周围的词语)为每个候选项打分。得分最高的实体就代表该提及的目标实体。

我们将使用一个基于 Python 的库 scispaCy [4] 来对命名实体进行消歧。scispaCy 中实现的模型可以识别命名实体、在知识库中选择候选项,并对这些候选项进行排序,以识别目标实体。下面的代码展示了一个使用 scispaCy 模型的 Python 脚本示例。

代码清单 7.1 使用 scispaCy 模型进行候选选择与排序

import spacy
from scispacy.linking import EntityLinker

nlp = spacy.load("en_core_sci_md")
nlp.add_pipe("scispacy_linker",
             config={"resolve_abbreviations": True, "linker_name": "umls"})

linker = nlp.get_pipe("scispacy_linker")
linker_dict = linker.kb.cui_to_entity

doc = nlp("""In the week of 13 April, Belize reported for the first time 
mosquito-borne Zika virus transmission. Update on the observed increase 
of congenital Zika syndrome and other neurological complications 
Microcephaly and other fetal malformations potentially associated with 
Zika virus infection.""")
for ent in doc.ents:
  if "Zika" in ent.text:
    print("Recognized entity:", ent.text, ent.start_char, ent.end_char)
  print("Ranked target candidates:")
  for kb_ent in ent._.kb_ents:
    print('-', linker_dict[kb_ent[0]][0], linker_dict[kb_ent[0]][1])

运行这段代码后,会得到一个相关候选项的有序列表。

代码清单 7.2 scispaCy 模型的候选选择与排序结果

Recognized entity: Zika virus 75 85
Ranked target candidates:
- C0318793 Zika Virus  #1
- C0276289 Zika Virus Infection
- C4687930 Zika Virus Antibody Measurement

Recognized entity: congenital Zika syndrome 135 159
Ranked target candidates:
- C4546023 Congenital Zika Syndrome   #2

Recognized entity: Zika virus infection 268 288
Ranked target candidates:
- C0276289 Zika Virus Infection   #3
- C0318793 Zika Virus
- C4687930 Zika Virus Antibody Measurement

#1 检测到的 “Zika Virus” 提及(字符位置 75–85)的目标实体
#2 检测到的 “Congenital Zika Syndrome” 提及(字符位置 135–159)的目标实体
#3 检测到的 “Zika Virus Infection” 提及(字符位置 268–288)的目标实体

每个识别出的实体都会关联一个候选列表,并依据 scispaCy 模型分数进行排序。每个提及的第一条结果就是模型给出的最佳结果。请注意,“Zika” 的每次提及最终都被关联到了不同的 UMLS 实体 ID。现在,我们就可以得到一段带注释的文本,其中每个被识别出来的实体都链接到了 UMLS 知识库。

在使用这些抽取信息之前,最后一步是本体集成:将领域本体中的知识整合进来,使抽取出的实体的结构化信息与上下文信息统一到一个 KG 中。UMLS 提供了来自多个来源的术语、分类和编码标准。这使得我们能够创建可互操作的生物医学信息系统,并且可以从另一个来源出发来访问和探索其中的信息。为了更好地理解 UMLS 是如何收集这些信息的,下面给出一个 UMLS 条目的示例。

代码清单 7.3 来自 UMLS 实体文件的示例条目

C0276289|ENG|S|L0388876|VC|S0517846|Y|A2985635|8552019|3928002||
SNOMEDCT_US|PT|3928002|Zika virus disease|9|N|256|

C0276289|ENG|P|L13115709|PF|S16069662|N|A27369917||C128423||
NCI|PT|C128423|Zika Virus Infection|0|N|256|

C0276289|ENG|S|L0392793|VW|S16069660|Y|A26676017||M000613823|D000071243|
MSH|ET|D000071243|Zika Fever|0|N|256|

我们高亮了本体集成中最重要的字段:从左到右,分别是实体 ID、本体,以及该本体中与该实体 ID 关联的名称。可以看到,在第一条记录中,表示 Zika Virus Infection 实体的 UMLS ID,被映射到了 SNOMEDCT_US [5] 本体中的 ID 3928002,该本体中为这一实体定义的一个名称是 Zika virus disease。SNOMEDCT_US,也就是 Systematized Nomenclature of Medicine(SNOMED) ,是最全面的多语言临床术语体系之一,包含超过 45 万个概念。它还提供了这些概念之间一套非常丰富的关系类型,包括从临床视角看很有价值的语义连接,例如 CAUSATIVE_AGENTFINDING_SITE。下面我们来看两个与 SNOMED 本体相关的文件样例:一个是 SNOMED 实体/关系描述文件,一个是边(或三元组)文件。先看描述文件样例。

代码清单 7.4 SNOMED 描述文件示例

84087010    20020131    1    900000000000207008    50471002
    en    900000000000013009    Zika virus    900000000000017005

8552019     20020131    1    900000000000207008    3928002
     en    900000000000013009    Zika virus disease    900000000000017005

367784012      20020131    1    900000000000207008    246075003
    en    900000000000013009    Causative agent    900000000000020002

你可以看到其中的 SNOMED ID 以及对应的名称。这个文件同时包含实体 ID 和实体之间关系的 ID。在这个样例里,第一条记录代表一个实体(Zika virus),第三条记录则代表一种关系(Causative agent)。

下面再看边文件中的一条记录。

代码清单 7.5 SNOMED 边文件示例

769900023    20020131    1    900000000000207008
    3928002          50471002    0    246075003    
900000000000011006    900000000000451002

高亮的值表示三元组元素,包括:表示 Zika virus disease 的源 ID(3928002)、表示 Zika virus 的目标 ID(50471002),以及表示 Causative agent 的关系 ID(246075003)。

通过引入外部本体,NED 模型的输出就成为了一个入口:既能探索和发现信息,也能构建 KG,使我们能够在统一视图中打通非结构化知识与结构化知识。图 7.3 展示了这样一个 KG 的直观示例,其中整合了前面提到的所有信息片段。

image.png

图 7.3 通过整合 scispaCy 模型处理后的文本信息、UMLS 和 SNOMED 本体所构建的示例 KG

我们的示例 KG 将使我们能够在一个真实世界场景中执行高级分析,这个场景与欧盟人源物质(SoHO)管理相关的医疗标准与法规定义有关。图 7.4 给出了一个思维模型,用于说明本章所描述的流程:从业务理解,到 KG 构建,再到查询。正如前几章讨论过的,这个思维模型可以看作是对 CRISP-DM 模型在 KG 场景下的一种具体化,它在第 2 章中已经引入。

image.png

图 7.4 描述 KG 构建流程的思维模型,作为 CRISP-DM 模型在本场景中的具体化:从理解业务目标,到定义对分析有用的 KG 查询

在介绍具体业务应用示例之前,我们先回顾一下第 6 章中介绍的 LLM 的机会与局限,并将其放到当前案例中来看。LLM 是 scispaCy 的一种现代替代方案,但在这个场景下,它们必须与 KG 技术结合,才能真正增加价值。

7.3 领域化 NED 与 LLM

前面我们讨论过:在文本内容中识别概念的能力,是支持决策的 IAS 所必需的一个关键特性。基于这一点,我们做了一个简单实验,用来测试基于 LLM 技术的代表性应用 ChatGPT 是否能够完成这一目标。

下面是我们基于当前示例定义的基本提示词:

“In the week of 13 April, Belize reported for the first time mosquito-borne Zika virus transmission. Update on the observed increase of congenital Zika syndrome and other neurological complications Microcephaly and other fetal malformations potentially associated with Zika virus infection.” Disambiguate all the medical entities you can detect.

ChatGPT 给出的输出如下:

  • Zika virus
  • Mosquito-borne transmission
  • Congenital Zika syndrome
  • Neurological complications
  • Microcephaly
  • Fetal malformations

注意 ChatGPT 基于的是一个持续演化中的生成式模型。因此,即使在类似测试和实验中,你也可能收到不同的回答。

正如你从这个初步结果中看到的,模型并没有完美识别出:第一次出现的 “Zika” 指的是病毒实体,而最后一次出现指的是病毒感染。考虑到医学领域的复杂性,我们需要对模型是否真正“理解”这些表达的含义有充分信心。从 NED 的角度看,我们必须检查这些实体是否被正确映射到了合适的知识库中。为测试这一点,我们又定义了一个额外提示:

Can you assign UMLS ids to these extracted entities?

得到的回答如下:

As an AI language model, I am not able to assign UMLS ids to extracted entities. However, UMLS Metathesaurus is a biomedical and health ontological resource that provides normalized names and mappings to concepts in various biomedical ontologies. It can be used to map extracted entities to their corresponding UMLS concepts and obtain their unique identifiers (UMLS ids). This requires domain-specific knowledge and expertise in utilizing the UMLS resources.

正如你所看到的,UMLS 知识库当前并没有被纳入 ChatGPT 模型之中。最后一句尤其关键:它表明,我们需要开发具备领域知识、并且对 UMLS 资源具有专业能力的模型。在本例中,我们将结合 NED 技术与 KG,构建一种同时具备这些能力、并且易于集成到 IAS 中的方法。

7.4 业务与领域理解

图 7.5 展示了业务理解这一步。我们的真实世界场景与 SoHO 管理中的标准与法规制定有关。SoHO 包括血液、组织、细胞和器官,这些物质可用于多种医学治疗。基于 SoHO 的治疗能够挽救生命(如输血)、改善生活质量(如肾移植),甚至帮助创造生命(如配子和体外受精)。在这个场景中,KG 技术能够通过诸如表示灵活性、以及将多源信息统一到一个视图中的能力来响应具体需求。

image.png

图 7.5 业务理解用于明确我们要解决的问题。这个阶段并不严格属于技术范畴,但却是后续步骤的基础。

7.4.1 背景

在医疗领域,一个关键问题是:接受输血、移植和辅助生殖治疗的患者安全。从捐献到最终应用于患者的全过程中,血液、组织和细胞(BTC)等成分都会在欧盟范围内用于治疗。图 7.6 展示了这一医疗领域的多个方面:从供体评估开始,可以分析多个维度,包括采集、质量标准、分发、可追溯性和生物警戒。

image.png

图 7.6 (中间)SoHO 供应链中的各个步骤。(左右两侧)这些阶段的特征。

BTC 领域依赖公民捐献,但在公共卫生危机(例如 COVID-19 大流行)期间,捐献可用性会显著下降。同时,BTC 的质量标准还必须随着新的 SoHO 采集、检测和处理方法而持续更新。新的风险和技术趋势会不断出现,因此需要一个能够“有效实施、面向未来、具备危机韧性且足够敏捷”的法律框架 [6],以持续提供适当要求。

2022 年,欧盟委员会(EC)发布了一项关于 SoHO 用于人体应用的标准和质量法规提案。该提案旨在确保接受 SoHO 治疗患者的安全,并保护他们免受可避免且可预测的风险。在评估的多个方案中,SoHO 相关利益方认为最佳选项是:基于 欧洲疾病预防与控制中心(ECDC)欧洲药品质量与卫生保健总署(EDQM) 等机构的专业能力,建立一个联合监管框架。这两家机构扮演互补角色:ECDC 通常发布与健康监测、健康威胁响应、新兴趋势以及 SoHO 安全相关的简短报告;EDQM 则发布更详细的指南,处理超出传染病传播风险之外的质量与安全问题,并为 SoHO 的采集、处理、储存和分发提供技术标准。能够从这两类文本中抽取有用信息和上下文细节,对于识别规则并快速更新法规、从而保障患者安全、保护其免受可避免风险至关重要。

7.4.2 用例定义

设想有一位卫生政策官员,需要识别与胰岛移植(也称为 islets of Langerhans)相关的具体指南和潜在风险,同时分析某一地区 Zika virus 的传播情况。正如我们将看到的,采用 NED 与 KG 技术可以支持这类工作。

概念搜索(CONCEPTUAL SEARCH)

概念搜索是一种检索方法,允许用户基于“意义”而非精确关键词来查找信息。它能够将不同表达统一到同一个实体上(例如 “pancreatic islets” 与 “islets of Langerhans”),也能区分名称相似但含义不同的实体。

基于结构化知识的搜索(STRUCTURED KNOWLEDGE-BASED SEARCH)

基于结构化知识的搜索,是利用领域本体中的形式化知识来检索文本中的信息。这些信息使我们能够在多份文档中的不同文本片段之间建立非平凡关系。例如,通过沿着本体路径导航,用户可以识别与糖尿病相关的不同疾病类型,检索提到这些疾病的所有文档,并获得与之相关文本内容的完整视图。

基于 KG 的可解释性与发现(KG-BASED INTERPRETABILITY AND DISCOVERY)

本体中形式化知识所定义的关系或路径,既可以反映文本内容中的关键含义(可解释性),也可以通过补充和丰富文本信息的连接来提供洞见(发现)。例如,从可解释性角度看,type 1 diabetes(T1D)islets of Langerhans 这两个实体会共现,因为前者是一种会影响后者的综合征。从发现角度看,像 AIDST1D 这样的疾病也可能共现,因为某些与 T1D 相关的病理过程会涉及免疫系统。

揭示新知识(UNCOVERING NEW KNOWLEDGE)

更有意思的情况会出现在:共现实体的相关知识尚未在本体中被形式化,但已经出现在 EDQM 指南中,或与该领域中的新发现有关。例如,pancreatic islets 这个实体会与 SoHO 管理相关的信息一起出现(见图 7.6)。此外,那些对传染病和非传染病异常共现关系感兴趣的 SoHO 相关方,例如 COVID-19diabetes mellitus,也可以在同一个基于图的视图中查阅 ECDC 公报,以理解某个成员国中的感染是否在增加,并据此决定是否需要阻止器官和组织的进口。

7.5 理解数据

在 SoHO 领域开发 IAS,需要把来自多个存储库的异构信息整合为统一来源,将非结构化数据语义结构化数据结合起来(见图 7.7)。本节概述了 EC 及其机构发布的文档,以及在此场景中采用的医学本体,例如 SNOMED 和 Human Phenotype Ontology(HPO)。

image.png

图 7.7 数据理解步骤使我们能够掌握可用信息的特征。在本示例场景中,可用数据包括 EC 及其机构发布的 SoHO 法规、报告和指南,以及 SNOMED 和 HPO 等医学本体。

7.5.1 非结构化数据

非结构化内容包括多种类型的文档:

  • BTC 领域的影响评估报告及相关法规提案
  • 利益相关方对法规提案立场的报告
  • EDQM 发布的 SoHO 管理指南和通讯
  • ECDC 发布的监测传染病进展的报告与公报

我们来看一下这些文档的特征,以便更清楚地理解:到底是哪类非结构化信息会被处理并转换为 KG。这也有助于你建立直觉,理解基于这些文档可以衍生出哪些潜在用例。

EC 提供的影响评估报告概述了欧盟 BTC 领域问题所处的政治与法律背景。它们汇集了修改既有立法的思路,突出诸如 BTC 供应中断等问题,讨论新疾病以及科技发展,提出改善 BTC 领域的目标,分析政策选项,并提出监管建议。

法规提案会由利益相关方进行评估,他们会提交立场文件、经验总结和一般性评论。例如,在一份立场文件中,国际干细胞研究学会(ISSCR)就未经证实的细胞疗法以及企业对临床疗效作出无依据声明的问题表达了关切。该文件建议,欧盟专家机构必须与其他监管机构协商,并使标准与国际规范保持一致,同时还应简化指导文件的征询流程,以鼓励对新提案提供反馈。

像 EDQM 发布的《Guide to the Quality and Safety of Tissues and Cells for Human Application》[7] 这样的文件,会向医疗专业人员提供技术指导。该指南给出了符合欧盟指令的最低标准、与当前科学认知一致的最佳实践、专家意见,以及国际项目的成果。

ECDC 的每周传染病威胁报告(CDTR)会汇总通过流行病情报活动收集到的、与欧盟重要传染病相关的全部数据。这些总结还会提供关于全球状况以及传染病流行病学变化的信息,而这些变化可能会对欧洲产生影响。

7.5.2 领域本体

第 3 章已经介绍了如何把本体作为整合不同来源的参考模式。在我们当前讨论的场景范围内,我们将使用 UMLS、SNOMED 和 HPO 本体。

统一医学语言系统(UMLS)

UMLS 是一个由多个生物医学领域受控词表组成的元词库(meta-thesaurus)。它提供了这些词表之间的映射结构,从而简化不同术语系统之间的转换。我们的代码示例使用的是 UMLS 2022AA 版本。

在本场景中,我们将使用以下两个文件:

  • MRCONSO.RRF —— 来自多个词表的生物医学实体列表。对于每个实体名称,它都包含该名称所属实体的实体 ID。
  • MRSTY.RRF —— 用于对 UMLS 实体进行分类的语义类型列表。

这些文件采用分隔符分隔值(DSV)格式,分隔符是竖线 |,因此它们可以像普通 CSV 文件一样处理。代码清单 7.6 和 7.7 分别展示了 MRCONSO.RRF 和 MRSTY.RRF 的样例。

代码清单 7.6 UMLS MRCONSO.RRF 文件示例

C0022131|ENG|P|L0022136|PF|S0054489|N|A2883106|130586015|78696007||
SNOMEDCT_US|SY|78696007|Islets of Langerhans|9|N|256|

C0022131|ENG|S|L7933100|PF|S9245679|Y|A15439829||76489||
FMA|SY|76489|Insulae pancreaticae|0|N|256|

C0022131|ENG|S|L0826072|PF|S0870037|N|A0928304||||
RCD|PT|Xa1Ij|Endocrine pancreatic structure|3|N|256|

---

C0011311|ENG|P|L0011312|VC|S0000287|Y|A2872183|63434017|38362002||
SNOMEDCT_US|SY|38362002|Dengue fever|9|N|256|

C0011311|ENG|S|L0286841|VO|S14576929|N|A24118377||M0005831|D003715|
MSH|PM|D003715|Break Bone Fever|0|N|256|

C0011311|ENG|S|L0294785|VW|S4069775|Y|A4402397||||
ICPC2ICD10ENG|PT|MTHU021113|dandy fever|3|N|256|

---

C0018681|ENG|S|L0290365|PF|S0362835|N|A2926207|41994011|25064002||
SNOMEDCT_US|SY|25064002|Cephalgia|9|N|2304|

C0018681|ENG|S|L1406212|VO|S1680379|Y|A1641924||M0009824|D006261|
MSH|PM|D006261|Cranial Pains|0|N||

C0018681|ENG|P|L0018681|PF|S0046854|N|A24679981|||HP:0002315|
HPO|PT|HP:0002315|Headache|0|N|256|

MRCONSO.RRF 聚合了与医学实体相关的本体、编码和实体名称信息。每一条记录都由该行第一列中的 UMLS ID 唯一标识;在 NED 中我们需要它,因为 scispaCy 模型返回的消歧结果正是使用这些 ID。代码清单 7.6 展示了与 Islets of Langerhans、Dengue fever、Cephalgia(头痛) 相关的若干行。

本体(ONTOLOGIES)

在这个示例中,与实体相关联的编码和名称来自以下来源:

接下来,这段清单展示了 Islets of Langerhans、Dengue FeverHeadache 在 UMLS 中是如何被分类的。

代码清单 7.7 UMLS MRSTY.RRF 文件示例

C0022131|T023|A1.2.3.1|Body Part, Organ, or Organ Component|AT19674993|256|
C0011311|T047|B2.2.1.2.1|Disease or Syndrome|AT41932582|256|
C0018681|T184|A2.2.2|Sign or Symptom|AT17639733|256|

对于 MRCONSO.RRF 中的每条记录,MRSTY.RRF 文件会提供一个语义类型编码及其名称。在这里,Body Part, Organ, or Organ Component(T023)Disease or Syndrome(T047)Sign or Symptom(T184) ,分别对应于 Islets of Langerhans、Dengue Fever 和 Cephalgia 的语义类型。

医学系统化命名法(SNOMED)

SNOMED 包含超过 45 万个概念,以及这些概念之间的关系类型。它通过 UMLS 免费许可提供,可以从 www.nlm.nih.gov/healthit/sn… 下载。我们使用的是 2022 年 9 月 1 日发布的 SNOMED 版本

在本场景中,我们将使用以下两个文件:

  • sct2_Description_Full-en_US1000124_20220901.txt —— 包含所有实体名称(及别名)以及三元组文件中关系对应的名称,后者用于定义实体之间的关系
  • sct2_Relationship_Full_US1000124_20220901.txt —— 包含一组三元组(以及其他元数据),用于定义所有 SNOMED 实体之间的关系。每个实体与关系都用数值编码标识。

这些文件采用 TSV 格式。代码清单 7.8 和 7.9 展示了其样例。

代码清单 7.8 SNOMED 描述文件示例

130586015    20020131    1    900000000000207008    78696007    
en    900000000000013009    Islets of Langerhans    900000000000017005
63434017    20020131    1    900000000000207008    38362002    
en    900000000000013009    Dengue fever    900000000000017005
41993017    20020131    1    900000000000207008    25064002    
en    900000000000013009    Cephalalgia    900000000000020002

在这里我们可以看到:UMLS 最初就是从这些源数据中生成出来的。SNOMED 条目中包含 Islets of Langerhans、Dengue fever 和 Cephalalgia 各自的编码与名称。下一段清单展示了:它们在语义关系中可以作为源实体,也可以作为目标实体。

代码清单 7.9 SNOMED 关系文件示例

169174023    20020131    1    900000000000207008    360555004    
78696007    0    116680003    900000000000011006    900000000000451002

182243021    20020131    1    900000000000207008    20927009    
38362002    0    116680003    900000000000011006    900000000000451002

424787021    20020131    1    900000000000207008    54012000    
25064002    0    116680003    900000000000011006    900000000000451002

在这个例子中,Islets of Langerhans、Dengue feverCephalalgia 都是某个 IS_A 关系(ID 为 116680003)的目标实体。而对应的源实体分别是 Endocrine pancreas cell(ID 360555004)Dengue hemorrhagic fever(ID 20927009)Posttraumatic headache(ID 54012000)

人类表型本体(HPO)

HPO 本体 [8] 以一个名为 hpo.owl 的 RDF/XML 文件发布(purl.obolibrary.org/obo/hp.owl)。其中包含关于表型异常的标准化信息。下面的代码清单展示了文件中与 T1D 相关的一个子集;为提升可读性,数据已从 RDF/XML 序列化为 Turtle(Terse RDF Triple Language)。

代码清单 7.10 hpo.owl 中的 T1D 详情

obo:HP_0100651 a owl:Class ;  #1
    rdfs:label "Type I diabetes mellitus" ^^xsd:string ; 
    obo:IAO_0000115 "A chronic condition in which the pancreas produces 
        little or no insulin..." ^^xsd:string ;   #2
    oboInOwl:created_by "doelkens"^^xsd:string ;   #3
    oboInOwl:creation_date "2010-12-29T06:37:55Z"^^xsd:string ;
    oboInOwl:hasDbXref "MSH:D003922"^^xsd:string,  #4
        "SNOMEDCT_US:46635009" ^^xsd:string,
        "UMLS:C0011854" ^^xsd:string ;
    oboInOwl:hasExactSynonym "Diabetes mellitus Type I"^^xsd:string,
        "Juvenile diabetes mellitus" ^^xsd:string,
        "Type 1 diabetes",
        "Type I diabetes";
    oboInOwl:hasRelatedSynonym "Insulin-dependent diabetes 
        mellitus"^^xsd:string ;
    oboInOwl:id "HP:0100651"^^xsd:string ;
    rdfs:comment "The onset of type 1 diabetes is typically during 
        adolescence..." ^^xsd:string ;
    rdfs:subClassOf obo:HP_0000819 .   #5

#1 将 T1D(URI 为 obo:HP_0100651)定义为一个本体类
#2 用自然语言描述该疾病
#3 与该条目作者(“doelkens”)相关的元数据
#4 指向 T1D 的外部数据源 ID
#5 将 T1D 定义为 URI 为 obo:HP_0000819 的表型特征的子类,该 URI 对应 diabetes mellitus

7.6 构建 SoHO 知识图谱

构建 KG,并在这一“事实来源”之上开发用例,需要经过以下步骤:

  • 定义 KG 模式(schema)
  • 处理并导入文档
  • 对医学实体进行消歧并导入
  • 处理、加载并映射本体
  • 生成共现关系

图 7.8 展示了构建 KG 时,数据准备与导入的关键步骤。

image.png

图 7.8 数据准备以及 KG 的创建和更新,是其中非常关键的技术步骤。数据准备步骤负责处理现有数据,而 KG 创建/更新步骤则利用处理后的输出,生成供分析阶段使用的数据库。

在本节中,此时你有两种推进方式。第一种是从零开始,按上述每个步骤完整构建 KG。另一种是:如果你的目标是先理解构建 KG 的主要思想,然后把重点放在用例(7.7 节)上,那么你可以直接从 KG 的一个中间版本开始,该版本已经包含了使用 scispaCy 处理过的文档。在这种情况下,你可以先阅读 7.6.1 节,然后跳到 7.6.4 节,加载本体并将它们的节点映射到抽取出的医学实体。用于构建 KG 的完整代码结合了 Python 脚本和 Cypher 查询,并已提供在本书代码仓库中。

7.6.1 定义模式

定义模式是一个理论步骤,它为我们的数据定义了一个基于图的模型,这将帮助我们完成 7.7 节中的用例。图 7.9 展示了我们将要构建的 KG 中主要组成部分(节点和关系)的模式。

image.png

图 7.9 KG 模式,其中包括相关的节点标签和关系类型,以支持多种类型的分析

导入的文档会以图结构的形式组织起来:通过连接 File 节点及其相关的 Page 节点。每个 Page 都会连接到一组 EntityMention 节点,这些节点定义了文本中所有被识别出来的实体提及。接着,这些 EntityMention 节点会被消歧,并连接到 MedicalEntity 节点。DISAMBIGUATED_TO 关系让我们能够表示这样两类情况:一方面,同一个字符串的提及可以指向不同实体;另一方面,不同字符串形式的表达也可以指向同一个实体。比如,正如前面看到的,术语 “Zika” 可以指代多个实体;而 “AIDS” 和 “Acquired Immunodeficiency Syndrome” 则是同一个实体的两种不同表达。MENTIONS_ENTITY 关系把 Page 节点与消歧后的 MedicalEntity 节点连接起来。

其余的节点和关系则用于将抽取出的 MedicalEntity 映射到 SnomedEntity、HpoEntityHpoDiseaseEntity。为了表示这种映射,我们定义了以下关系:IS_SNOMED_ENTITY、IS_HPO_ENTITYIS_DISEASE_ENTITY。现在模式已经定义完成,我们可以开始导入数据了。

7.6.2 处理并导入文档

在这个阶段,我们会导入文档,并将相关内容组织成基于图的结构。出于我们的目标,大部分可用文档都以 PDF 或 DOCx 格式发布。因此,在将数据加载进 Neo4j 之前,我们先使用 Amazon Textract OCR 服务提取原始内容,并对结果进行处理。为了重建完整文本,我们编写了 Python 脚本来处理不同结构的文档,比如单栏和双栏文档。图 7.10 展示了这一准备阶段的关键内容,即如何从 PDF 和 DOCx 文档中重建完整文本。

注意 Amazon Textract(aws.amazon.com/textract/)是 AWS 提供的一项机器学习服务,可自动从扫描文档中提取文本、手写内容和数据。

image.png

图 7.10 我们使用 Amazon Textract 从 SoHO 文档中提取文本。

你需要在本地机器上运行文本处理脚本,以重建页面的完整文本,然后再将结果加载到 KG 中。完整代码实现已在本书的代码仓库中提供。代码清单 7.11 展示了将原始文本或处理后文档导入 Neo4j,并将这些信息组织为图结构的 Cypher 查询。

代码清单 7.11 将文本内容加载到 Neo4j

[...]
class DocsImporter:
[...]

  def set_constraints(self):
    queries = ["CREATE FULLTEXT INDEX pageText FOR (n:Page) ON EACH [n.text]"]

    for q in queries:
      self.connection.query(q, db=self.db)

  def load_docs(self):
    with open(self.docs_file) as json_file:
      docs = json.load(json_file)

    query = """
      MERGE (f:File {id: $name})
      SET f.type = $type, f.path = $name
      WITH f

      UNWIND $pages as page
      MERGE (p:Page {id: replace($name, '.pdf', '') + '_' + page.page_idx})
      SET p.page_idx = page.page_idx,
          p.text = page.text

      MERGE (f)-[:CONTAINS_PAGE]->(p)
      """

    for i in tqdm(docs):
      name = i['name']
      type = i['type']
      pages = i['pages']
      self.connection.query(query, 
                            parameters={'name': name,
                                   'type': type,
                                   'pages': pages},
                       db=self.db)

当我们完成这个过程后,文件和页面文本就都被加载到了 KG 中。

7.6.3 对医学实体进行消歧并导入

下一阶段是:通过直接处理 OCR 生成的 JSON 文件中的文档来抽取并消歧医学实体,把结果存入 Python 字典,再将这些结果加载到 Neo4j。下面给出了这一处理结果的一个样例。

代码清单 7.12 文档处理生成的 Python 字典

{'id': 'sample_dataset-PublicUse/ECDC Documents/west nile virus/EU-summary
-report-trends-sources-zoonoses-2013_120',
'ents': [{'sentenceIndex': 0,
   'value': 'zoonoses',
   'lemma': 'zoonosis',
   'label': 'ENTITY',
   'beginCharacter': 60,
   'endCharacter': 68,
   'selected_ned_id': 'C0043528',
   'selected_ned_name': 'Zoonoses',
   'selected_ned_definition': 'Diseases of non-human animals that may be
   transmitted to HUMANS or may be transmitted from humans to non-human
   animals.',
   'selected_ned_aliases': ['Zoonotic Disease',
    'Zoonosis, NOS',
    'Zoonoses',...],
   'selected_ned_types_id': ['T047'],
   'selected_ned_types': ['Disease or Syndrome']...
}

这些结果保存了与抽取实体相关的详细信息,包括句子索引,以及通过起始字符和结束字符定位的提及位置。除了实体类型和别名等信息之外,这些细节还将被用于执行高级查询,正如 7.7 节中会讨论的那样。现在我们就可以把这些结果加载到 Neo4j 中了。

代码清单 7.13 加载 NED 数据

[...]
class NLPImporter(BaseImporter):
[...]

  def load_nlp_res(self):
    print("Loading data into Neo4j...")
    with open(self.file,'rb') as file:
    processed_pages = pickle.load(file)

    query = """
            UNWIND $rows as item

            MATCH (page:Page)   #1
            WHERE page.id = item.id AND NOT page:NEDProcessed
            SET page:NEDProcessed
            WITH page, item

            UNWIND item.ents as entity
            MERGE (
              mention:EntityMention {
                name_normalized: toLower(
                  apoc.text.join(
                    apoc.text.split(trim(entity.value), "\s+"), " "
                  )
                )
              }
            )   #2
            ON CREATE SET 
              mention.name = apoc.text.join(
                apoc.text.split(trim(entity.value), "\s+"), " "
             )
            MERGE 
  (page)-[s:MENTIONS_MENTION {from_model: "ned"}]->(mention)   #3

            ON CREATE SET s.start_chars= [entity.beginCharacter], 
                        s.end_chars= [entity.endCharacter], 
                        s.sentence_index = [entity.sentenceIndex],
                        s.type = toLower(entity.label)
            ON MATCH SET s.start_chars = s.start_chars + entity.beginCharacter,
                        s.end_chars = s.end_chars + entity.endCharacter, 
                        s.sentence_index = s.sentence_index +
                        entity.sentenceIndex
            WITH page, mention, entity     

            FOREACH(medical in entity |
            MERGE (dis:MedicalEntity {id: medical.selected_ned_id})  #4
            ON CREATE SET dis.name=
            apoc.text.join(apoc.text.split(trim(medical.selected_ned_name),
            "\s+"), " "),
                        dis.type_id = medical.selected_ned_types_id, 
                        dis.types = medical.selected_ned_types,
                        dis.type = medical.selected_ned_types[0],
                        dis.original_mention = medical.value, 
                        dis.definition = medical.selected_ned_definition, 
                        dis.aliases = medical.selected_ned_aliases,
                        dis.start_chars= [entity.beginCharacter], 
                        dis.end_chars= [entity.endCharacter], 
                        dis.sentence_index = [entity.sentenceIndex]  #5
            ON MATCH SET dis.start_chars = dis.start_chars +
            entity.beginCharacter, 
                        dis.end_chars = dis.end_chars + entity.endCharacter

            MERGE (mention)-[r:DISAMBIGUATED_TO]->(dis)  #6
            SET r.confidence = medical.selected_ned.confidence

            MERGE (page)-[t:MENTIONS_ENTITY]->(dis)  #7
            ON CREATE SET t.sentence_index = [medical.sentenceIndex]
            ON MATCH SET t.sentence_index = t.sentence_index +
            medical.sentenceIndex)  

            """

        self.load_in_batch(query, processed_pages, len(processed_pages),
   chunk_size=1)

#1 匹配尚未处理过的页面节点
#2 创建实体提及节点,并将其连接到页面
#3 为页面与提及之间的关系添加属性,包括起止字符位置
#4 合并由 scispaCy 工具抽取出的医学实体节点
#5 为医学实体节点添加属性,包括别名和语义类型
#6 将医学实体节点连接到实体提及节点
#7 将医学实体节点连接到页面节点

这个查询会把 scispaCy 的处理结果存入 Neo4j。它首先创建 EntityMention 节点,并把这些节点连接到 Page 节点。接着创建 MedicalEntity 节点,用 UMLS 数据丰富它们,并把这些医学实体连接到 EntityMentionPage 节点。正如我们前面提到的,我们在图中同时保留了 EntityMention 和消歧后的 MedicalEntity,以提高数据表示的灵活性。

7.6.4 处理、加载并映射本体

在这个阶段,我们把 UMLS、SNOMED 和 HPO 本体加载到 KG 中。UMLS 充当访问多个本体中具体信息的入口。因此,我们首先加载 SNOMEDHPO,然后再把其中的每个实体映射到 UMLS。

导入 SNOMED

下面的代码清单会根据 sct2_Relationship_Full_US1000124_20220901.txt 在 Neo4j 中创建节点和关系。

代码清单 7.14 导入 SNOMED:加载关系

[...]
class SnomedRelationshipsImporter(BaseImporter): #1
  [...]  
  def set_constraints(self):  #2
    queries = [
        (
            "CREATE CONSTRAINT IF NOT EXISTS FOR (n:SnomedEntity) "
            "REQUIRE n.id IS UNIQUE"
        ),
        (
            "CREATE INDEX snomedNodeName IF NOT EXISTS "
            "FOR (n:SnomedEntity) ON (n.name)"
        ),
        (
            "CREATE INDEX snomedRelationId IF NOT EXISTS "
            "FOR ()-[r:SNOMED_RELATION]-() ON (r.id)"
        ),
        (
            "CREATE INDEX snomedRelationType IF NOT EXISTS "
            "FOR ()-[r:SNOMED_RELATION]-() ON (r.type)"
        ),
        (
            "CREATE INDEX snomedRelationUmls IF NOT EXISTS "
            "FOR ()-[r:SNOMED_RELATION]-() ON (r.umls)"
        ),
    ]

    for q in queries:
      self.connection.query(q, db=self.db)

  def import_snomed_rels(self): #3
    query = """
       UNWIND $batch as item
       MERGE (e1:SnomedEntity {id: item.sourceId})
       MERGE (e2:SnomedEntity {id: item.destinationId})
       MERGE (e1)-[:SNOMED_RELATION {id: item.typeId}]->(e2)
       FOREACH(ignoreMe IN CASE WHEN item.typeId = '116680003' 
       THEN [true] ELSE [] END|
         MERGE (e1)-[:SNOMED_IS_A]->(e2)  #4
       )
       """

size = self.get_csv_size(snomedRels_file)  #5
self.batch_store(snomed_rels_query, self.get_rows(snomedRels_file),
size=size) #6

#1 继承 BaseImporter 类,该类包含基础导入功能
#2 为 SNOMED 实体及其属性定义约束和索引
#3 使用参数化查询导入 SNOMED 关系
#4 创建 SNOMED_IS_A 关系类型,用于追踪实体之间的层次连接
#5 获取文件大小(BaseImporter 类中的默认实现)
#6 按批次加载 SNOMED 数据(实现也在 BaseImporter 类中)

SNOMED 包含数百种关系。为了让图模式尽可能简洁,我们决定只创建一种统一的 SNOMED_RELATION 关系,并把关系名称存储在其 type 属性中。在代码清单 7.14 中,我们另外创建了一个 SNOMED_IS_A 关系,用于定义实体之间的层级连接;稍后你会看到,这样做有利于把信息从根节点传播到叶子节点。

现在,节点和关系都已经导入,图的基本结构也已经成形,接下来我们需要进一步丰富它:为节点和关系补充名称与别名。下面这个类会从 sct2_Description_Full-en_US1000124_20220901.txt 中提取这些信息。

代码清单 7.15 导入 SNOMED:加载名称和别名

[...]
class SnomedNamesImporter(BaseImporter):
  [...]

  def import_snomed_names(self, snomedNames_file):
        snomed_names_concepts_query = """
        UNWIND $batch as item
        MATCH (e1:SnomedEntity)
        -[r:SNOMED_RELATION {id: item.conceptId}]->
        (e2:SnomedEntity)
        WHERE item.conceptId <> '116680003' AND r.id = item.conceptId 
        SET r.type = CASE 
                WHEN r.type IS NULL THEN item.termAsType
                ELSE r.type END,  #1
            r.aliases = CASE 
                WHEN item.termAsType IN r.aliases THEN r.aliases
                ELSE coalesce(r.aliases,[]) + item.termAsType END  #2
        """

        snomed_names_entities_query = """
        UNWIND $batch as item
        MATCH (e:SnomedEntity {id: item.conceptId})
        SET e.name = CASE 
                WHEN e.name IS NULL THEN item.term
                ELSE e.name END,  #3
            e.aliases = CASE 
                WHEN item.term in e.aliases THEN  e.aliases
                ELSE coalesce(e.aliases, []) + item.term END  #4
        """
        size = self.get_csv_size(snomedNames_file)
        self.batch_store(
          snomed_names_concepts_query,
          self.get_rows(snomedNames_file),
         size=size)
        self.batch_store(
          snomed_names_entities_query,
          self.get_rows(snomedNames_file),
          size=size)

[...]

#1 将关系名称写入 type 属性
#2 为关系添加别名
#3 添加节点名称
#4 添加节点别名

这个类会为图中已加载的节点和关系补充名称与别名。下一步则是:将信息从根节点传播到所有子节点,一直传到叶子节点。SNOMED 的第一层节点表示医学领域中的原型实体,例如疾病、身体结构、物质和事件。这些根节点定义了 SNOMED 中实体的语义类型。然而,这些信息在我们导入的原始数据中是隐含的,因为对于其余实体,原始数据只提供名称和别名。因此,我们需要一种机制,把这种语义信息沿着本体的树形结构传递下去。这样一来,我们就能轻松识别某个深层实体到底是疾病、产品,还是其他类型。图 7.11 对这个传播机制作了说明。

image.png

图 7.11 SNOMED 本体层次结构示例。借助这一层次结构,位于更深层级的节点(例如 Ecallantide 和 Retinopathy associated with AIDS),就可以继承第一层节点所代表的原型类别信息(例如 Pharmaceutical product 和 Disease)。

下面的代码清单会沿着层次结构检索所有节点,并将第一层节点的信息传播到更深层节点。

代码清单 7.16 导入 SNOMED:从第一层节点传播标签

[...]
class SnomedLabelPropagator():    
[...]  

  def get_rows(self):
    propagation_query = """
      MATCH p=(n:SnomedEntity)<-[:SNOMED_IS_A]-(m:SnomedEntity) 
      WHERE n.id= "138875005" // Root node
      WITH distinct m as first_node

      CALL apoc.path.expandConfig(first_node, { #1
              relationshipFilter: '<SNOMED_IS_A',
                minLevel: 1,
                maxLevel: -1,
                uniqueness: 'RELATIONSHIP_GLOBAL'
            }) yield path

     UNWIND nodes(path) as other_level  #2
     WITH first_node, collect(DISTINCT other_level) as uniques
             UNWIND uniques as unique_other_level
             WITH first_node,unique_other_level
             WHERE not first_node.name in coalesce(unique_other_level.type,[])

             RETURN unique_other_level.id as id, first_node.name as label #3
            """

   with self._driver.session(database=self._database) as session:
     result = session.run(query=propagation_query)
     for record in iter(result):
        yield dict(record)
[...]

#1 使用 expandConfig 设置层次遍历规则
#2 获取该遍历过程所访问到的节点
#3 返回所有被赋予特定类型的节点

SNOMED_IS_A 关系通过实体之间的层次连接,使语义类型能够沿着树形结构传播。

导入 HPO

下面的代码清单概述了导入 HPO 本体的主要步骤。更多细节,包括适当索引的创建,在第 3 章中已有讨论,并已包含在本书仓库中的 Cypher 代码里。首先,我们使用 Neosemantics 插件导入 HPO 本体。

代码清单 7.17 导入 HPO:加载本体

CALL n10s.rdf.import.fetch("http://purl.obolibrary.org/obo/hp.owl","RDF/XML");

HPO 本体以 RDF/XML 格式提供,并被转换成 Neo4j 图结构,以便用 Cypher 进行处理。

接下来,我们为表示表型特征实体的节点添加 HpoEntity 标签。

代码清单 7.18 导入 HPO:为表型特征添加 HpoEntity 标签

MATCH (n:Resource)
WHERE n.uri STARTS WITH "http://purl.obolibrary.org/obo/HP"
SET n:HpoEntity,
 n.id = coalesce(n.id, replace(apoc.text.replace(n.uri,'(.*)obo/',''),'_', ':'));

现在我们还需要处理注释文件,该文件描述了表型特征与疾病之间的连接。为了让这一步更高效,我们使用下面的查询,将疾病实体预先加载为图节点。

代码清单 7.19 导入 HPO:创建 HpoDiseaseEntity 节点

LOAD CSV FROM 'https://github.com/obophenotype/human-phenotype-
ontology/releases/latest/download/phenotype.hpoa' AS row
FIELDTERMINATOR '\t'
WITH row
SKIP 5
MERGE (dis:Resource:HpoDiseaseEntity {id: row[0]})
ON CREATE SET dis.label = row[1];

下一步是建立表型特征节点与疾病节点之间的关系。

代码清单 7.20 导入 HPO:在 HpoEntity 与 HpoDiseaseEntity 之间建立关系

LOAD CSV FROM 'https://github.com/obophenotype/human-phenotype-ontology/
releases/latest/download/phenotype.hpoa' AS row
FIELDTERMINATOR '\t'
WITH row
SKIP 5
MATCH (dis:HpoDiseaseEntity)
WHERE dis.id = row[0]
MATCH (phe:HpoEntity)
WHERE phe.id = row[3]
MERGE (dis)-[:HAS_PHENOTYPIC_FEATURE]->(phe)

下面的查询则建立了:从文本中抽取并消歧后的实体,与本体节点之间的连接。

代码清单 7.21 通过 UMLS 集成 SNOMED

MATCH (m:MedicalEntity)
WITH m
MATCH (d:SnomedEntity)
WHERE m.id in d.umls_ids
WITH m, d
MERGE (m)-[:IS_SNOMED_ENTITY]->(d)

对于 HPO,我们也执行类似的过程(完整细节请参考完整代码)。下面这个查询把 MedicalEntity 节点与 HPO 注释文件中的 HpoDiseaseEntity 节点连接起来。

代码清单 7.22 连接 MedicalEntity 与 HpoDiseaseEntity 节点

MATCH (m:MedicalEntity)
WITH m
MATCH (d:HpoDiseaseEntity)
WHERE m.id in d.umls_ids 
WITH m, d
MERGE (m)-[:IS_DISEASE_ENTITY]->(d)

在下一节中,我们将为位于同一句中的实体生成共现关系。

7.6.5 生成实体共现关系

识别文本中医学实体的共现,是实现高级用例的基础步骤,因为这些用例需要把文本中的非结构化知识与领域本体中的结构化知识结合起来。代码清单 7.23 中的查询,会在同一句中被识别出的医学实体之间创建一种新的关系,称为 COOCCURR

定义 共现(Co-occurrence)指的是:将 Page 节点投影到 Entity 节点上所形成的关系。

代码清单 7.23 在句子级别创建共现关系

CALL apoc.periodic.iterate(
  "MATCH (n:Page) WHERE exists( (n)-[:MENTIONS_ENTITY]->(:MedicalEntity) )
  RETURN n", 
  "MATCH (n)-[r:MENTIONS_ENTITY]->(m:MedicalEntity)
  WITH n, r.sentence_index as sentences, m 
  UNWIND sentences as sentence
  WITH n, sentence, collect(distinct m) as entities

  UNWIND range(0, size(entities)-2) as i
  UNWIND range(i+1, size(entities)-1) as j

  WITH n, sentence, entities, i, j
  MATCH (m1) WHERE id(m1) = id(entities[i])
  MATCH (m2) WHERE id(m2) = id(entities[j])

  WITH n, sentence, entities, i, j, m1, m2
  MERGE (m1)-[s:COOCCURR]-(m2)
  ON CREATE SET s.count = 1, 
       s.sentences = [sentence]
  ON MATCH SET s.count = s.count + 1, 
   s.sentences = s.sentences + sentence", 
{batchSize: 50})

这个查询会在 KG 中生成超过 25,000 条关系,把位于同一句中的医学实体之间的连接显式表示出来。正如接下来会讨论的那样,对这些共现实体在本体中的连接关系进行分析,可以帮助我们发现与这些实体相关的既有知识,以及尚未被生物医学本体正式建立的新信息。

7.7 基于 KG 的用例

本节中的代码示例将展示:我们如何结合 KG 与 NED 来处理以下用例:

  • 概念搜索
  • 基于结构化知识的搜索
  • 基于 KG 的可解释性与发现
  • 揭示新知识

图 7.12 展示了与本应用领域用例相关的分析步骤。

image.png

图 7.12 在生成 KG 之后,我们就可以围绕定义好的用例执行分析,以解决业务问题,并基于 KG 提供解决方案。

注意 由于环境差异、Neo4j 索引行为、数据管道导入顺序,以及 scispaCy 实体抽取中的非确定性因素,你的查询结果可能会与本章展示的结果略有差异。

7.7.1 概念搜索

概念搜索的核心能力在于:把不同表达但意义相同的术语统一起来返回给用户,或者把表面相似、但实际上指向不同实体的术语区分开来。执行概念搜索可以帮助我们扩大或聚焦对特定文档及其相关文本片段的检索范围。图 7.13 对概念搜索与传统全文搜索做了一个高层对比。

image.png

图 7.13 传统全文搜索与概念搜索的差异。全文搜索直接在文档集合上执行;而概念搜索会先借助本体信息扩展搜索空间,再去搜索文档。

我们先从传统全文搜索出发,观察概念搜索的效果。下面这个查询尝试检索所有提到 “breakbone fever” 的文档。

代码清单 7.24 以 “breakbone fever” 为输入字符串的全文搜索查询

CALL db.index.fulltext.queryNodes("PageText", "breakbone fever")
YIELD node, score
WITH node as p, score as score
MATCH (f:File)-[:CONTAINS_PAGE]->(p)
RETURN f.id as `File ID`, p.page_idx as `Page index`, score as Score
LIMIT 5

表 7.1 展示了按分值排序的全文搜索结果。第一列是文档路径,第二列是文档中的页码索引,第三列则是全文搜索算法计算出的得分。

表 7.1 对 “breakbone fever” 进行全文搜索所返回的前五页

File IDPage indexScore
sample_dataset-PublicUse/ECDC Documents/west nile virus/emerging-vector-borne-diseases_annual-epidemiological-report-2014.pdf52.12
sample_dataset-PublicUse/ECDC Documents/west nile virus/emerging-vector-borne-diseases_annual-epidemiological-report-2014.pdf42.09
sample_dataset-PublicUse/ECDC Documents/zika virus/Communicable-disease-threats-report-26-mar-2016.pdf102.08
sample_dataset-PublicUse/ECDC Documents/ebola/communicable-disease-threats-report-13-19-mar-2016.pdf92.08
sample_dataset-PublicUse/ECDC Documents/zika virus/Communicable-disease-threats-report-26-mar-2016.pdf102.08

这种搜索方式是利用模糊逻辑在文档集合中识别关键词。在本例中,“breakbone” 这个词实际上从未在文本中出现过,但 “fever” 却会在多个高分文档中频繁出现。然而,我们真正感兴趣的是:识别提到这一特定疾病的文档,或者进一步说,识别提到导致该疾病的病原体的文档。

在 UMLS 知识库中,编码 C0011311 标识的正是这个实体,而它可能以多种形式出现,例如 “dengue fever”“dungero”“dandy fever” 。因此,我们接下来运行下面的查询,用来检索所有被 scispaCy 模型识别出来、并且其别名中包含 “breakbone fever” 的医学实体。

代码清单 7.25 查找别名为 “breakbone fever” 的实体提及

MATCH (f:File)-[:CONTAINS_PAGE]->(p)
      -[r:MENTIONS_MENTION]->(m)-[:DISAMBIGUATED_TO]->(e)
WHERE "breakbone fever" IN [x IN e.aliases | toLower(x)]  #1

UNWIND range(0, size(r.start_chars) - 1) AS mention  #2
WITH f, p, e, m, r, mention
RETURN DISTINCT
  f.id AS `File ID`,
  p.page_idx AS `Page index`,
  apoc.text.join(
    collect(
      substring(
        p.text,
        apoc.coll.max([r.start_chars[mention] - 100, 0]),  #3
        r.end_chars[mention] - r.start_chars[mention] + 200  #4
      )
    )[0..3],  #5
    '\n\n'
  ) AS `Mention contexts`,
  size(collect(m.name)) AS `Number of mentions`
ORDER BY `Number of mentions` DESC
LIMIT 5

#1 通过检查别名列表中是否存在小写形式的 "breakbone fever" 来过滤节点
#2 遍历起始字符列表中的每个提及索引,以处理单页中多次提及的情况
#3 通过从起始位置向前回溯 100 个字符来计算截取起点,同时保证不小于 0
#4 通过“结束位置 - 起始位置 + 200”来确定截取长度,以保留更多上下文
#5 将每个文件-页面组合中收集到的上下文片段限制为前三条,以保持输出简洁

表 7.2 展示了通过概念搜索得到的高分页面,以及其中一段提到了表示 breakbone fever 的 UMLS 实体的示例段落,结果按提及次数排序。

表 7.2 以 “breakbone fever” 作为实体别名进行概念搜索得到的高分页面

File IDPage indexMention contextNumber of mentions
sample_dataset-PublicUse/ECDC Documents/hepatitis-a/communicable-disease-threats-report-feb-24-2018.pdf11“[…] In 2017, Cambodia reported over 3,200 suspected dengue cases.”22
sample_dataset-PublicUse/ECDC Documents/ebola/communicable-disease-threats-report-17%20may-2014.pdf11“[…] Singapore has reported more than 1000 dengue cases nationally from January to April this year, which is 15 per cent fewer cases compared with […]”20
sample_dataset-PublicUse/ECDC Documents/ebola/Communicable-disease-threats-report-19-jul-2014.pdf12“[…] An epidemic of dengue fever in Malaysia has now infected nearly 47,000 people, which is more than double the number of cases […]”20
sample_dataset-PublicUse/ECDC Documents/ebola/communicable-disease-threats-report-21-jun-2014.pdf13“[…] Cuba has recorded 67 imported cases of dengue fever up to 8 June, according to media quoting the Cuban government.”20
sample_dataset-PublicUse/ECDC Documents/west nile virus/communicable-disease-threats-report-21-jun-2014.pdf12“[…] Oceania: As of 13 June 2014, 1 762 suspected dengue cases have been reported in Solomon Islands since January 2014.”19

将这些结果与全文搜索的结果相比,我们会发现,最相关的页面完全不同。现在我们来进一步扩展概念搜索查询,查看完整结果集(去掉 LIMIT 子句)。在概念搜索结果中,全文搜索的第一条结果排在第 19 位。该文档中包含 17 次对该 UMLS 实体的提及,而概念搜索中得分最高的结果则包含 22 次对 C0011311 这个 UMLS 实体的提及。

在我们的应用领域中,采用概念搜索会带来非常显著的效果。与全文搜索相比,我们不仅能够检索到更准确、更细粒度的信息,而且除了文档路径和页码之外,还可以定位提到该实体的具体文本片段,以及该实体在页面中的出现次数。因此,概念搜索还支持其他能力,进一步增强检索结果的可解释性。由于 NED 模型能够定位实体在文本中的具体位置,我们甚至还可以进行精确的调试测试,以识别模型在哪些情况下没有达到预期效果。

概念搜索还能够增强这样一些场景中的搜索效果:某个词或表达在不同上下文中有不同含义。比如,“islands” 一词通常表示“岛屿”,但在表达 “islands of Langerhans” 时,它的语义上下文就完全不同了。传统的全文搜索很可能会返回大量与“被水包围的陆地”有关的无关结果。而借助概念搜索机制,我们可以把这些无关结果过滤掉,并扩大搜索范围,把 “pancreatic islets” 这样的其他表达形式也纳入进来,以捕获更多相关内容。正如下一节将要讨论的,概念搜索的核心思想还可以通过把页面与那些在本体关系上互相关联的医学实体连接起来,进一步扩展。

7.7.2 基于结构化知识的搜索

使用基于结构化知识的搜索,我们可以借助领域本体中已经形式化的知识,从文本中检索信息,并在多份文档中的不同文本片段之间建立非平凡关系。我们已经看到,借助这种方式,我们可以把指向同一个概念的信息聚合起来,例如 dengue feverpancreatic islets,不受歧义或多名称表达的影响。不过,如果我们进一步利用嵌入在 SNOMED 这类本体中的知识,就可以基于本体关系,把非结构化内容进行聚合和关联。举例来说,考虑到像 islets of Langerhans 这样的细胞捐献很重要,那么聚合所有提到“会影响这些细胞、从而危及捐献过程的疾病”的文本就会很有价值。图 7.14 展示了概念搜索查询与基于结构化知识的搜索查询之间的思维模型差异。

image.png

图 7.14 概念搜索与基于结构化知识的搜索之间的差异。在这里,搜索过程会涉及那些通过本体关系与搜索查询中实体相连的医学实体。从 “islets of Langerhans” 出发,系统会进一步搜索 “diabetes type 1” 和 “hyperglycaemia” 等会影响胰岛的疾病。

下面这个查询会检索提到“可能影响 islets of Langerhans 的疾病”的文本片段。

代码清单 7.26 获取关于会影响 islets of Langerhans 的疾病的文本

MATCH (m1:MedicalEntity)-[:IS_SNOMED_ENTITY]->(s1:SnomedEntity)
<-[r1:SNOMED_RELATION]-(s2:SnomedEntity)
<-[:IS_SNOMED_ENTITY]-(e:MedicalEntity)
WHERE m1.name = "Islets of Langerhans" AND r1.type = "FINDING_SITE"  #1
WITH e
MATCH path = (f:File)-[:CONTAINS_PAGE]->(p)
-[r:MENTIONS_MENTION]->(m)-[:DISAMBIGUATED_TO]->(e)
UNWIND range(0, size(r.start_chars) - 1) AS mention  #2

WITH f, p, r, e, mention,
     apoc.coll.max([r.start_chars[mention] - 100, 0]) AS start,  #3
     apoc.coll.min([r.end_chars[mention] + 100, size(p.text)]) AS end  #4
WITH f, p, r, e, mention, substring(p.text, start, end - start) AS context  #5
WITH f, p,
     collect(DISTINCT e.name) AS `Mentioned entities`,  #6
     collect(context)[0..3] AS contexts  #7
RETURN DISTINCT
  f.id AS `File ID`,
  p.page_idx AS `Page index`,
  `Mentioned entities`,
  apoc.text.join(contexts, '\n\n') AS `Mention contexts`
ORDER BY size(`Mentioned entities`) DESC
LIMIT 5

#1 过滤出通过 FINDING_SITE 关系与 “Islets of Langerhans” 相连的 SNOMED 实体
#2 使用 start_chars 中的索引遍历每个提及实例,以支持单个关系中的多次提及
#3 计算上下文提取的起始位置,向前保留 100 个字符且不低于 0
#4 计算结束位置,向后多保留 100 个字符,但不超过页面文本长度
#5 从页面文本中截取上述范围内的子串作为上下文窗口
#6 收集页面中被提及医学实体名称的去重列表
#7 将收集到的上下文窗口限制为前三条

表 7.3 展示了该查询结果中的一个子集。

表 7.3 关于会影响 islets of Langerhans 的疾病的文本内容

File IDPage indexMentioned entitiesMention contexts
sample_dataset-PublicUse/EDQM Documents/BTC/guide-to-the-quality-and-safety-of-organs-for-transplantation-7th-edition.PDF137Diabetes Mellitus, Metabolic Syndrome X“Metabolic syndrome, arterial hypertension, diabetes mellitus, albuminuria (see below) and other chronic kidney diseases or systemic disease […]”
sample_dataset-PublicUse/EDQM Documents/BTC/guide-to-the-quality-and-safety-of-organs-for-transplantation-7th-edition.PDF144Hyperglycemia, Diabetes Mellitus, Non-Insulin-Dependent“[…] Many patients with severe head trauma become hyperglycaemic and require insulin therapy, despite normal pancreatic function and no history of diabetes. […] On the other hand, manifestation of diabetes mellitus type II is possible at an age of over 50-65 years.”

在代码清单 7.26 中,我们扩展了概念搜索的逻辑:输入不再只是单个实体,而是一组来自本体的实体集合,再把这些实体相关的文本细节检索出来。其中最有意思的结果之一来自《Guide to the Quality and Safety of Organs for Transplantation, 7th ed.》第 144 页,也就是表 7.3 第 4 列所展示的内容。该页提到了 Hyperglycemia,而它通过 FINDING_SITE 关系与 Islets of Langerhans 实体显式相连,而且出现于一个关于头部创伤潜在供体管理的上下文中。这句话所在章节专门讨论的是胰腺捐献中的供体维护方案。这样的关键信息,是不可能通过概念搜索机制直接检索出来的。但借助 Hyperglycemia 这个医学实体与我们搜索起点(Islets of Langerhans)之间的连接,我们成功获取了这条信息。其他结果还提到了不同形式的 diabetes mellitus,而这在胰岛捐献场景中尤其重要。

基于结构化知识的搜索还可以通过遍历本体中的长路径来实现。我们再来看 Zika virus 的例子。该病毒可以被归类为一种 Togavirus(披膜病毒) ,并通过吸血节肢动物(尤其是昆虫和蛛形纲动物)传播给人类。假设我们感兴趣的是:文档中提到的、所有由 Togavirus 引起的疾病。我们可以沿着 SNOMED 本体中多个 CAUSATIVE_AGENT 关系拼接出的路径进行探索,并利用结果去获取所需文档;图 7.15 给出了一个示例。

image.png

图 7.15 只包含 CAUSATIVE_AGENT 关系的 SNOMED 本体路径。遍历这一关系子集,对于识别由同类病毒传播的疾病尤其有用。

我们可以将搜索扩展到诸如 Yellow fever、Rift valley feverRubella 等实体——它们与搜索查询中指定的实体(Zika virus)并没有直接连接。代码清单 7.27 展示了如何利用这些复杂路径来检索相关文档。对于我们的应用领域来说,这个例子尤其有吸引力,因为它使我们能够基于“共同病毒类型”自动检索提到相似疾病的文档。

代码清单 7.27 获取提到由 Togavirus 引起疾病的文档

MATCH (m1:MedicalEntity)-[:IS_SNOMED_ENTITY]->(s1:SnomedEntity)
-[r1:SNOMED_RELATION*3..3]-(s2:SnomedEntity)
      <-[:IS_SNOMED_ENTITY]-(e:MedicalEntity)  #1
WHERE m1.name = "Zika Virus" 
AND all(x IN r1 WHERE x.type = "CAUSATIVE_AGENT")  #2
WITH DISTINCT e

MATCH path = (f:File)-[:CONTAINS_PAGE]->(p)
-[r:MENTIONS_MENTION]->(m)-[:DISAMBIGUATED_TO]->(e)
WITH f, e, collect(p.page_idx) AS pages_list  #3
RETURN DISTINCT
  f.id AS `File ID`,
  pages_list,
  collect(DISTINCT e.name) AS `Mentioned entity`
ORDER BY size(`Mentioned entity`) DESC
LIMIT 5

#1 沿着三跳 SNOMED_RELATION 遍历,以发现间接相关的医学实体
#2 过滤出所有三条关系都为 CAUSATIVE_AGENT 的路径,以保证语义一致性
#3 收集所有提及该实体的页面索引

该查询的结果列在表 7.4 中。

表 7.4 提到由 Togavirus 引起疾病的文档及页面

File IDPagesMentioned entities
sample_dataset-PublicUse/ECDC Documents/west nile virus/TER-Mosquito-surveillance-guidelines.pdf10Murray valley encephalitis, Japanese Encephalitis, Venezuelan equine encephalomyelitis, Encephalomyelitis, Eastern Equine, Ross river virus infection
sample_dataset-PublicUse/ECDC Documents/ebola/ebola-preparedness-belgium.pdf38Yellow Fever, Rift Valley Fever, West Nile Fever, Dengue Fever, Chikungunya Fever
sample_dataset-PublicUse/ECDC Documents/west nile virus/communicable-disease-threats-report-18-august-2019-updated-26-august-2019.pdf1Rubella, Yellow Fever, Japanese Encephalitis, Dengue Fever, Chikungunya Fever
sample_dataset-PublicUse/ECDC Documents/ebola/communicable-disease-threats-report-18-august-2019-updated-26-august-2019.pdf1Rubella, Yellow Fever, Japanese Encephalitis, Dengue Fever, Chikungunya Fever
sample_dataset-PublicUse/ECDC Documents/west nile virus/communicable-disease-threats-report-15-december-2018.pdf1Yellow Fever, Dengue Fever, Mosquito-Borne Diseases, Chikungunya Fever

这个用例对于自动检索相关信息非常有用,因为它利用了本体中的连接关系。以搜索字符串中提到的实体为起点,我们可以在多段文本之间导航,找到那些提到与该搜索实体有关联实体的内容,从而为 IAS 系统中的用户体验开辟新的增强方式。

7.7.3 基于 KG 的可解释性与发现

从实体在同一句中的共现出发,基于 KG 的可解释性与发现使我们能够分析:这些实体在本体中是如何相连的,以及这种连接的本质是什么。在某些情况下,本体中的连接恰好反映了这些实体为何会在某个句子中共现(可解释性)。这种分析还可以把共现实体与本体进行校验。另一些情况下,本体中的连接则会补充新的知识,扩展句子本身所提供的信息(发现)。

为了更好地理解这一用例的价值,来看代码清单 7.28,它摘取了《Proposal for a Directive of the European Parliament and of the Council Setting Standards of Quality and Safety for the Collection, Testing, Processing, Storage, and Distribution of Human Blood and Blood Components and Amending Council Directive 89/381/EEC》这份文件中的若干句子。在该文件中,“AIDS” 和 “Hepatitis” 共现了两次。

代码清单 7.28 AIDS 与 Hepatitis 共现的句子

SENTENCE 1: The reasons why they should not donate which put recipients at 
risk, such as unsafe sexual behaviour, HIV/ AIDS, hepatitis, drug 
addiction and the use and abuse of drugs;

[...]

SENTENCE 2: Infectious diseases persons suffering or having suffered from
- Babesiosis
- Hepatitis B (HBsAg confirmed positive)
- Hepatitis C
- Hepatitis, infectious (of unexplained aetiology)
- HIV/AIDS

“AIDS” 和 “Hepatitis” 会在这些句子中彼此靠近地出现,是因为它们都代表了捐献过程中的一种风险因素:它们都是传染性疾病。这一信息在 SNOMED 中已经被直接编码,并通过不同的本体路径表达出来。下面的代码清单展示了其中一部分。

代码清单 7.29 连接这些实体的路径:可解释性视角

(AIDS)-[:PATHOLOGICAL_PROCESS]->(Infectious disease)
<-[:DUE_TO]-(Hepatitis due to infection)
-[:IS_A]->(Inflammatory disorder of liver)

(AIDS)-[:PATHOLOGICAL_PROCESS]->(Infectious disease)
<-[:DUE_TO]-(Viral hepatitis)
-[:IS_A]->(Inflammatory disorder of liver)

这些本体路径定义了这两个实体之间的连接,并编码了 “AIDS” 和 “Hepatitis” 共现的含义(在 SNOMED 中,后者被标注为 Inflammatory disorder of liver)。我们也借此把抽取出的实体与本体进行了校验,从而理解为什么这些实体会在同一句中被抽取出来。

还有一些类型的 SNOMED 路径,则揭示了 AIDS 与 hepatitis 之间更有意思的连接。比如,下面清单中的第一条路径表明:AIDS 与一种被称为 Hepatomegaly associated with AIDS 的疾病相关,而该疾病影响肝脏。第二条则显示,一种特殊形式的肝炎 —— Lupus hepatitis —— 与 AIDS 一样都涉及免疫系统。

代码清单 7.30 连接这些实体的路径:发现视角

(AIDS)<-[:ASSOCIATED_WITH]-(Hepatomegaly associated with AIDS (disorder))
-[:FINDING_SITE]->(Liver)
<-[:FINDING_SITE]-(Inflammatory disorder of liver)

(AIDS)-[:HAS_DEFINITIONAL_MANIFESTATION]->(Immune system finding)
<-[:HAS_DEFINITIONAL_MANIFESTATION]-(Lupus hepatitis)
-[:IS_A]->(Inflammatory disorder of liver)

本体不仅帮助我们理解一对医学实体为何会在句子中共同出现,也让我们能够发现与这对实体相关的新细节。图 7.16 展示了如何将领域本体集成到从自然语言内容构建出的 KG 中,其中用 DengueZika virus 作为共现示例。

image.png

图 7.16 描述基于 KG 的可解释性与发现过程的思维模型。文本中共现的实体,可以通过 SNOMED 本体中的多条路径相连。

下面我们进一步深入细节,看看如何在 SoHO 领域中应用实际方案,通过一个真实 KG 来支持可解释性与发现。下面的查询会检索与 “Zika virus” 共现频率最高的前五种实体类型。

代码清单 7.31 检索与 “Zika virus” 共现最多的实体类型

MATCH (m1:MedicalEntity)-[r:COOCCURR]-(m2:MedicalEntity)
WHERE m1.id= "C0318793"
RETURN m2.type as `Entity Type`, count(m2.type) as `Number of co-occurrences` 
ORDER BY count(m2.type) DESC
LIMIT 5

这个查询通过统计共现实体类型的数量,识别最相关的几类。结果见表 7.5。

表 7.5 与 “Zika virus” 共现最多的实体类型

EntityNumber of co-occurrences
Geographic Area255
Qualitative Concept132
Disease or Syndrome125
Functional Concept106
Finding98

结果中出现最多的是 Geographic Area,这是因为 “Zika virus” 常常出现在报告传染病传播情况的公报中。其他类别,例如 Qualitative ConceptFunctional Concept,包含的是一些较宽泛的概念,在本例中大多数时候并不重要。因此,我们把重点放在与之共现的 Disease or Syndrome 类型实体上。下面的查询会找到这些共现实体,以及抽取该共现关系时对应的文本片段。

代码清单 7.32 获取带上下文的共现疾病实体

MATCH (n1:MedicalEntity)-[r:COOCCURR]-(n2:MedicalEntity)
WHERE n1.id = "C0318793" AND n2.type = "Disease or Syndrome"  #1
WITH n1, r, n2
ORDER BY r.count DESC
MATCH (f:File)-[:CONTAINS_PAGE]->(p:Page)
      -[r1:MENTIONS_MENTION]->(m1)-[:DISAMBIGUATED_TO]->(n1),
      (p)-[r2:MENTIONS_MENTION]->(m2)-[:DISAMBIGUATED_TO]->(n2)
WHERE r1.sentence_index = r2.sentence_index  #2
WITH f, p, r1, r2, n2
RETURN DISTINCT
  f.id AS `File ID`,
  p.page_idx AS `Page index`,
  n2.id AS `Co-occurring entity id`,
  n2.name AS `Co-occurring entity name`,
  CASE
    WHEN r1.end_chars[0] > r2.start_chars[0]
    THEN substring(p.text, r2.start_chars[0], 
    r1.end_chars[0] - r2.start_chars[0])  #3
    ELSE substring(p.text, r1.start_chars[0], 
    r2.end_chars[0] - r1.start_chars[0])  #4
  END AS `Mentions context`

#1 过滤出与 "C0318793"(Zika virus)共现、且类型限定为 Disease or Syndrome 的实体
#2 确保两个实体是在页面中的同一句里被提及
#3 截取覆盖两个实体提及区间的文本片段
#4 保证上下文窗口总是从较早字符位置延伸到较晚字符位置

表 7.6 给出了其中一部分结果;你可以运行代码清单 7.32 来分析完整结果集。

表 7.6 与 “Zika virus” 共现的疾病实体及其上下文

File IDPage indexCo-occurring entity IDCo-occurring entity nameMentions context
sample_dataset-PublicUse/ECDC Documents/zika virus/Communicable-disease-threats-report-23-apr-2016.pdf8C0009450Communicable Diseases“COMMUNICABLE DISEASE THREATS REPORT Countries and territories with reported confirmed autochthonous cases of Zika virus”
sample_dataset-PublicUse/ECDC Documents/ebola/Communicable-disease-threats-report-01-jul-2017.pdf17C0009450Communicable Diseases“COMMUNICABLE DISEASE THREATS REPORT Zika: No mosquito-borne Zika virus”
sample_dataset-PublicUse/ECDC Documents/zika virus/Questions and answers about Zika virus disease.pdf4C0276289Zika Virus Infection“Zika virus disease Risk of transmission in Europe What is the risk to Europe? The main risk to the EU is related to an infected traveller introducing the virus to an area with mosquitoes that are able to transmit Zika virus”
sample_dataset-PublicUse/ECDC Documents/west nile virus/Communicable-disease-threats-report-2-jul-2016.pdf1C0018378Guillain-Barre Syndrome“Zika virus is a cause of microcephaly and Guillain-Barre syndrome”
sample_dataset-PublicUse/EDQM Documents/BTC/optimal-use-of-clotting-factors-and-platelets-european-symposium-proceedings-wildbad-kreuth-initiative-iv-freising-germany.PDF300C0011311, C0008055Dengue Fever, Chikungunya Fever“chikungunya and dengue virus infections in France and overseas departements, and emerging pathogens such as Zika virus”

Zika virusCommunicable Diseases(UMLS ID C0009450) 在同一句中出现了很多次。这是因为很多已导入的公报具有类似结构,例如 “COMMUNICABLE DISEASE THREATS REPORT Zika: No mosquito-borne Zika virus […]”。但其他一些共现例子则更有助于支持可解释性与发现。例如,Zika virus 经常与以下实体共现:Zika Virus Infection(UMLS ID C0276289)Dengue Fever(UMLS ID C0011311)Chikungunya Fever(UMLS ID C0008055)

Zika virus 与 Zika Virus Infection 的共现是可以理解的。而且这两个实体之间的语义连接已经直接存在于 SNOMED 本体中。下面的查询会利用相应的 SNOMED ID 和标签,展示 Zika virus disease(SNOMED ID 3928002)Zika virus(SNOMED ID 50471002) 之间的 SNOMED 连接。

代码清单 7.33 获取 Zika virus disease 与 Zika virus 之间的路径

MATCH (s1), (s2)
WHERE s1.id = "3928002" AND s2.id = "50471002"
WITH s1, s2, allShortestPaths((s1)-[:SNOMED_RELATION*1..8]-(s2)) AS paths  #1
UNWIND paths AS path
WITH relationships(path) AS path_edges,
     nodes(path) AS path_nodes
WITH 
  [n IN path_nodes | n.name] AS node_names,
  [r IN path_edges | COALESCE(r.type, 'IS_A')] AS rel_types,  #2
  [n IN path_edges | startnode(n).name] AS rel_starts  #3
WITH [i IN range(0, size(node_names) - 1) |
  CASE
    WHEN i = size(node_names) - 1
    THEN '(' + node_names[i] + ')'  #4
    WHEN node_names[i] = rel_starts[i]
    THEN '(' + node_names[i] + ')' + '-[:' + rel_types[i] + ']->'  #5
    ELSE '(' + node_names[i] + ')' + '<-[:' + rel_types[i] + ']-'  #6
  END
] AS string_paths
RETURN DISTINCT apoc.text.join(string_paths, '') AS `Extracted paths`

#1 查找两个 SNOMED 概念节点之间的最短路径(最多 8 跳)
#2 使用关系类型(默认值为 IS_A)以确保标注合法
#3 获取每条关系的起始节点名称,以确定关系方向
#4 完成路径字符串的结尾
#5 表示该关系从当前节点向外发出
#6 表示该关系从后一个节点指向当前节点

这个查询会以一种格式化后的方式,返回 Zika virus 与 Zika virus disease 之间的所有最短路径。结果如下。

代码清单 7.34 连接 Zika virus disease 与 Zika virus 的路径

(Zika virus disease)-[:CAUSATIVE_AGENT]->(Zika virus)

在这里,SNOMED 本体提供了一个非常清晰的关系,用来“解释”这种共现的含义。这个连接反映了:在临床和流行病学文本中,疾病及其致病因子之间在语言上和概念上天然接近。在我们的场景中,我们提供了一种图增强信息,把为什么这些共现实体可以被验证这件事显式表达了出来。

如表 7.6 所示,Zika virus 这个医学实体还会与另一种疾病 Dengue Fever 共现。我们可以通过运行代码清单 7.33 中的查询,并把 WHERE 子句替换为 s1.id="38362002" AND s2.id="50471002",来探索这两个实体之间的本体路径。结果如下。

代码清单 7.35 连接 Dengue 与 Zika virus 的路径

(Dengue)-[:IS_A]->(Disease due to Flavivirus)
<-[:IS_A]-(Zika virus disease)-[:CAUSATIVE_AGENT]->(Zika virus)

(Dengue)-[:IS_A]->(Mosquito-borne flavivirus fever)
<-[:IS_A]-(Zika virus disease)-[:CAUSATIVE_AGENT]->(Zika virus)

(Dengue)-[:IS_A]->(Viral disease)
<-[:IS_A]-(Zika virus disease)-[:CAUSATIVE_AGENT]->(Zika virus)

(Dengue)-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]->(Infectious process 
(qualifier value))
<-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]-(Congenital Zika virus infection)
-[:CAUSATIVE_AGENT]->(Zika virus) 

(Dengue)-[:CAUSATIVE_AGENT]->(Arthropod-borne organism)
<-[:CAUSATIVE_AGENT]-(Zika virus disease)
-[:CAUSATIVE_AGENT]->(Zika virus)

(Dengue)-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]->(Infectious process 
(qualifier value))
<-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]-(Zika virus disease)
-[:CAUSATIVE_AGENT]->(Zika virus)

(Dengue)-[:PATHOLOGICAL_PROCESS]->(Infectious disease)
<-[:PATHOLOGICAL_PROCESS]-(Zika virus disease)
-[:CAUSATIVE_AGENT]->(Zika virus)

(Dengue)-[:CAUSATIVE_AGENT]->(Togavirus)
<-[:CAUSATIVE_AGENT]-(Zika virus disease)
-[:CAUSATIVE_AGENT]->(Zika virus)

有多条路径把 DengueZika virus 连接起来,其中包括 Viral diseaseInfectious disease 这样的关键实体,它们帮助我们解释为什么这些疾病会在同一个句子中共现(参见表 7.6 的最后一行)。此外,像 Disease due to Flavivirus、TogavirusMosquito-borne flavivirus fever 这样的实体,也为我们揭示了 Dengue 与 Zika virus 之间是如何连接的,而不依赖于它们究竟是在什么句子里被提到。

这种验证过程也可以扩展到其他生物医学本体,包括 HPO。例如,下面这个查询的结果展示了:如何系统地抽取并验证 表型特征(如 Renal cell carcinoma、Leukemia)与相关的疾病实体(如 von Hippel-Lindau syndrome、Colorectal cancer、RETINOBLASTOMA)之间的共现模式。该方法使我们能够基于文档级证据,识别在临床上有意义的表型-疾病关联。

代码清单 7.36 查找与表型特征共现的 HPO 疾病实体

MATCH (d:HpoDiseaseEntity)<-[:IS_DISEASE_ENTITY]-(n1:MedicalEntity)
      -[r:COOCCURR]-(n2:MedicalEntity)-[:IS_HPO_ENTITY]->(h:HpoEntity)
WHERE EXISTS((d)-[:HAS_PHENOTYPIC_FEATURE]->(h))
 AND n2.id in ["C0007134", "C0376545"]  #1
WITH n1, n2, d, h

MATCH (f:File)-[:CONTAINS_PAGE]->(p:Page)
      -[r1:MENTIONS_MENTION]->(m1)-[:DISAMBIGUATED_TO]->(n1),
      (p)-[r2:MENTIONS_MENTION]->(m2)-[:DISAMBIGUATED_TO]->(n2)
WHERE r1.sentence_index = r2.sentence_index  #2
WITH f, p, r1, r2, n1, n2, d, h

RETURN DISTINCT
  f.id AS `File name`,
  p.page_idx AS `Page index`,
  h.label AS `HpoEntity name`,
  d.label AS `HpoDiseaseEntity name`,
  CASE
    WHEN r1.end_chars[0] > r2.start_chars[0]
    THEN substring(p.text, r2.start_chars[0], r1.end_chars[0] 
    - r2.start_chars[0])  #3
    ELSE substring(p.text, r1.start_chars[0], r2.end_chars[0] 
    - r1.start_chars[0])  #4
  END AS `Mentions context`

#1 选择这样一类医学实体对:n1 与某个固定子集中的表型实体共现,并且 n1 所对应的疾病 d 具有该表型 h
#2 只保留那些出现在同一句中的提及
#3 抽取周围文本,形成共现片段
#4 确保文本抽取顺序正确

与前面的示例不同,这里我们明确要求:一个医学实体必须是疾病,另一个必须是收录在 HPO 注释文件中的表型特征。这个查询的结果列在表 7.7 中。

表 7.7 与表型特征共现的 HPO 疾病实体

File IDPage indexHPO nameHpoDisease nameMentions context
sample_dataset-PublicUse/EDQM Documents/BTC/guide-to-the-quality-and-safety-of-organs-for-transplantation-7th-edition.PDF275Renal cell carcinomavon Hippel-Lindau syndrome“[…] von Hippel-Lindau syndrome could be considered (preferably for the heart donation) when inappropriate risks associated with RCC […]”
sample_dataset-PublicUse/EDQM Documents/BTC/guide-to-the-quality-and-safety-of-organs-for-transplantation-7th-edition.PDF228Renal cell carcinomaColorectal cancer“[…] RCCs, two neuro-endocrine carcinomas, one breast cancer, one colorectal cancer […]”
sample_dataset-PublicUse/BTC key docs/CELEX_52002PC0319_EN_TXT.pdf12LeukemiaRETINOBLASTOMA“[…] retinoblastoma, melanoma of the anterior pole, haematological neoplasm […]”

这个用例对于分析人员具有现实意义,因为他们可以从共现的实体类型中提炼出有价值的模式。例如,在多个疾病共同出现的情况下,我们可以进一步验证到底是哪些本体路径把这些疾病连接起来。另一方面,如果共现实体之间缺乏有意义的本体路径,那就意味着:这里可能存在尚未被生物医学本体编码的新知识。下一节我们就讨论这种情况。

7.7.4 揭示新知识

在一个因研究进展而快速变化的领域中,文本中的一部分知识尚未被固化进医学本体。因此,出现在同一句中的实体,在领域本体中未必能够找到直接对应的可解释性或发现路径。

换句话说,我们不只是可以用本体来丰富 KG 中的信息,也可以反过来利用从文本构建出的 KG 去建议新的事实,以便将来纳入本体中。这个用例使我们能够识别文本中的新模式,而这些模式在医学领域中可能很有价值。为了识别这类模式,我们先运行下面这个查询,回顾一下与 Zika virus 共现最多的疾病。

代码清单 7.37 查找文本中与 “Zika virus” 共现最多的疾病

MATCH (n1:MedicalEntity)-[r:COOCCURR]-(n2:MedicalEntity)
WHERE n1.id= "C0318793" and n2.type = "Disease or Syndrome"
RETURN n2.name, r.count
ORDER BY r.count DESC
LIMIT 5

结果见表 7.8。

表 7.8 与 Zika virus 共现最多的疾病

Disease entityOccurrences
Communicable Diseases606
Zika Virus Infection520
Dengue Fever206
Guillain-Barre Syndrome195
Chikungunya Fever155

在这些结果中,有多个疾病与 Zika virus 实体相关。其中,Guillain-Barre Syndrome 尤其值得注意,因为与其他结果不同,它并不是一种虫媒传播疾病。这使得它与 Zika virus 的共同出现显得有些出人意料,但同时在临床上又非常重要,这提示它可能代表的是一种并发症,而不仅仅是地区流行病共存。

因此,我们有必要进一步查看那些同时提到 Zika virusGuillain-Barre syndrome 的文本内容。我们可以复用代码清单 7.32 中的查询,只需要把第一个 WHERE 子句替换为:

n1.id= "C0318793" AND n2.id = "C0018378"

它们分别是 Zika virus 和 Guillain-Barre syndrome 的 UMLS ID。(同时,我们也从结果中删除了 “Co-occurring entity id” 和 “Co-occurring entity name” 两列,因为在这个场景中它们没有价值。)在清理掉重复结果之后,结果如表 7.9 所示。

表 7.9 同时提到 Zika virus 与 Guillain-Barre syndrome 的文本

File IDPage indexMentions context
sample_dataset-PublicUse/ECDC Documents/ebola/communicable-disease-threats-report--20-feb-2016.pdf3“Zika and potential complications published on 12 February 2016, six countries have reported an increase in the incidence of cases of microcephaly (Brazil) and Guillain-Barre syndrome”
sample_dataset-PublicUse/ECDC Documents/zika virus/zika-virus-rapid-risk-assessment-11-april-2016.docx.pdf2“Zika virus epidemic, health services and practitioners should be alerted to the possible occurrence of neurological syndromes (Guillain-Barre syndrome)”
sample_dataset-PublicUse/ECDC Documents/ebola/communicable-disease-threats-report-16-june-2019.pdf3“Guillain-Barre is known to be triggered by bacterial infections, respiratory viruses, entero-viruses, and arboviruses such as dengue and Zika”

这些结果表明,Zika virusGuillain-Barre syndrome 的诱发因素之一。因此,我们有理由期待在像 SNOMED 这样的本体中能找到类似这样的事实:

Guillain-Barre syndrome-[:CAUSATIVE_AGENT]->Zika virus

下面我们来验证这一点。我们运行代码清单 7.33 中的查询,但把 WHERE 子句中的 Zika virus disease(SNOMED ID 3928002) 替换为 Infectious neuronitis(这是 Guillain-Barre syndrome 在 SNOMED 中的术语;SNOMED ID 为 40956001)。下一个清单展示了部分结果:SNOMED 本体中确实存在多条路径连接这些实体,但大多数路径都并不代表真正有意义的连接。

代码清单 7.38 连接 Zika virus 与 Infectious neuronitis 的路径

(Zika virus)<-[:CAUSATIVE_AGENT]-(Congenital Zika virus infection)
-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]->(Infectious process (qualifier value))
<-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]-(Reactivation of hepatitis B viral hepatitis (disorder))
-[:ASSOCIATED_MORPHOLOGY]->(Inflammation)
<-[:ASSOCIATED_MORPHOLOGY]-(Infectious neuronitis)

(Zika virus)<-[:CAUSATIVE_AGENT]-(Zika virus disease)
-[:CAUSATIVE_AGENT]->(Togavirus)
<-[:CAUSATIVE_AGENT]-(Mosquito-borne flavivirus encephalitis)
-[:ASSOCIATED_MORPHOLOGY]->(Inflammation)
<-[:ASSOCIATED_MORPHOLOGY]-(Infectious neuronitis)

在第一条路径中,Zika virus 能连接到 Infectious neuronitis,是因为 Zika virus 是 Congenital Zika virus infection 的致病因子,而后者又被视为一种 Infectious process(感染过程) 。问题在于,这个 Infectious process(qualifier value) 节点会连接到大量其他传染性过程,例如 Reactivation of hepatitis B viral hepatitis (disorder) 。而这个疾病又与 Inflammation(炎症) 相连,而炎症也是 Infectious neuronitis 的一种形态学表现。代码清单 7.38 中第二条路径的推理过程也类似。之所以会返回这些路径,是因为它们经过了像 Infectious process(qualifier value)Inflammation 这样的枢纽节点(hub nodes) ,这些节点与大量感染过程相关的节点都相连。

一种解决办法是:过滤掉包含这些枢纽节点的路径。为此,我们可以使用 Neo4j Graph Data Science Library(GDS) 提供的图算法工具,它可以计算图中节点的中心性。要运行这个算法,第一步是创建一个图投影,把我们分析所需的节点和关系集合投影出来。在这个例子中,我们想聚焦于 SNOMED 本体图,因此可以运行下面的代码。

代码清单 7.39 在 SNOMED 本体上创建投影

CALL gds.graph.project(
  'snomedGraph',
  'SnomedEntity',
  'SNOMED_RELATION'
)
YIELD
  graphName AS graph,
  relationshipProjection AS knowsProjection,
  nodeCount AS nodes,
  relationshipCount AS rels;

在生成投影之后,我们就可以以流式模式运行度数(degree)算法,找出连接度最高的节点,并利用这个结果过滤掉那些对我们的目的没有帮助的路径。

代码清单 7.40 过滤枢纽节点

CALL gds.degree.stream('snomedGraph')
YIELD nodeId, score
WITH gds.util.asNode(nodeId).name AS name, score AS degree
ORDER BY degree DESC
LIMIT 350
WITH collect(name) AS hub_nodes  #1

MATCH (s1), (s2)
WHERE s1.id = "3928002" AND s2.id = "40956001"
WITH s1, 
     s2,
     allShortestPaths((s1)-[:SNOMED_RELATION*1..8]-(s2)) AS paths,
    hub_nodes  #2
UNWIND paths AS path
WITH relationships(path) AS path_edges,
nodes(path) AS path_nodes,
hub_nodes
WITH 
[n IN path_nodes | n.name] AS node_names,
[r IN path_edges | COALESCE(r.type, 'IS_A')] AS rel_types,  #3
[n IN path_edges | startnode(n).name] AS rel_starts,  #4
hub_nodes
WHERE NOT any(x IN node_names WHERE x IN hub_nodes)  #5
WITH [i IN range(0, size(node_names) - 1) |
CASE
WHEN i = size(node_names) - 1
THEN '(' + node_names[i] + ')'  #6
WHEN node_names[i] = rel_starts[i]
THEN '(' + node_names[i] + ')' + '-[:' + rel_types[i] + ']->'  #7
ELSE '(' + node_names[i] + ')' + '<-[:' + rel_types[i] + ']-'  #8
END
] AS string_paths
RETURN DISTINCT apoc.text.join(string_paths, '') AS `Extracted paths`

#1 收集连接度最高的前 350 个高中心性节点
#2 查找两个 SNOMED 概念节点之间所有最短路径(1–8 跳)
#3 如果关系类型缺失,则默认使用 IS_A
#4 获取每条关系的源节点名称,以确定最终字符串中的方向
#5 排除任何经过枢纽节点的路径
#6 标记路径结束
#7 渲染从当前节点指向下一个节点的前向关系
#8 渲染从下一个节点指向当前节点的后向关系

这个查询把节点度计算相关路径识别结合在一起。凡是包含高度节点的路径,都会从结果中被排除。经过这一过滤后,结果如下一段清单所示。

代码清单 7.41 修订后的 Zika virus disease 与 Infectious neuronitis 连接路径

(Zika virus disease)-[:IS_A]->(Viral disease)
<-[:IS_A]-(Erythema multiforme caused by virus (disorder))
-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]->(AI – autoimmune)
<-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]-(Infectious neuronitis)

(Zika virus disease)-[:IS_A]->(Disease due to Flavivirus)
<-[:IS_A]-(Acute hepatitis C)
-[:IS_A]->(Acute inflammatory disorder)
<-[:IS_A]-(Infectious neuronitis)

(Zika virus disease)<-[:ASSOCIATED_FINDING]-(Presumptive recent Zika negative)
-[:IS_A]->(Context-dependent finding (context-dependent category))
<-[:IS_A]-(History of Guillain Barre syndrome (situation))
-[:ASSOCIATED_FINDING]->(Infectious neuronitis)

(Zika virus disease)-[:IS_A]->(Viral disease)
<-[:IS_A]-(Common cold)-[:IS_A]->(Acute inflammatory disorder)
<-[:IS_A]-(Infectious neuronitis)

(Zika virus disease)-[:IS_A]->(Viral disease)
<-[:IS_A]-(VHF – Viral haemorrhagic fever)
-[:IS_A]->(Acute inflammatory disorder)
<-[:IS_A]-(Infectious neuronitis)

(Zika virus disease)<-[:ASSOCIATED_FINDING]-(Presumptive recent Zika positive)
-[:IS_A]->(Context-dependent finding (context-dependent category))
<-[:IS_A]-(History of Guillain Barre syndrome (situation))
-[:ASSOCIATED_FINDING]->(Infectious neuronitis)

(Zika virus disease)-[:IS_A]->(Viral disease)
<-[:TEMPORALLY_FOLLOWS]-(Transient paroxysmal cold hemoglobinuria)
-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]->(AI – autoimmune)
<-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]-(Infectious neuronitis)

(Zika virus disease)-[:IS_A]->(Viral disease)
<-[:DUE_TO]-(Erythema multiforme caused by virus (disorder))
-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]->(AI – autoimmune)
<-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]-(Infectious neuronitis)

(Zika virus disease)-[:IS_A]->(Viral disease)
<-[:AFTER]-(Transient paroxysmal cold hemoglobinuria)
-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]->(AI – autoimmune)
<-[:PATHOLOGICAL_PROCESS_(ATTRIBUTE)]-(Infectious neuronitis)

这一次,我们只得到了 9 条结果,而原始查询曾返回 11,185 条路径。不过,这些结果中仍然包含 Viral disease 这样的泛化实体,它会把 Zika virus disease 与随机的病毒性疾病连接起来,而这些疾病又可以进一步把 Zika virus disease 连到 Infectious neuronitis。换句话说,在 SNOMED 本体中,并没有编码这两个实体之间的直接连接。这正是一个典型例子:KG 中定义的实体共现,未来可能可以反过来用于丰富领域本体,形成一种良性循环。

如果进一步从文本中抽取出有意义的关系,这种机制还可以被指数级增强。在这种方法里,起点不再只是共现,而是那些从文本中抽取出来、比共现关系更具体的关系。见图 7.17。

image.png

图 7.17 描述“揭示新知识”过程的思维模型。在这个例子中,“Zika virus” 和 “Guillain-Barre syndrome” 多次出现在同一句中,但医学本体并没有提供它们之间任何有意义的连接。

小结

  • 命名实体消歧(NED)使我们能够把文本中提到的实体连接到参考知识库。
  • 将 NED 与知识图谱技术结合起来,可以为关键领域中的高级服务开发打开新的可能性。
  • 构建 KG 需要多个步骤,包括:定义模式、导入文档、对文档中的实体进行消歧、集成领域本体、映射抽取出的实体,以及为位于同一句中的实体生成共现关系。
  • 在 KG 之上可以执行高级分析,以支持应用领域中的不同用例:概念搜索、基于结构化知识的搜索、基于 KG 的可解释性与发现,以及揭示新知识。