第十一章:信息提取和知识图谱(基础)
本章涵盖
- 从文本中提取命名实体
- 使用依存解析理解句子的结构
- 将依存树转换为知识(事实)
- 从文本构建知识图谱
在上一章(第十章)中,你学会了如何使用大型变压器生成聪明的听起来像单词。但单独的语言模型只是通过预测下一个对你来说听起来合理的单词来进行欺骗。直到你为它们提供有关世界的事实和知识之前,你的 AI 才能推理真实世界。在第二章中,你学会了如何做到这一点,但当时你并不知道。你能够标记代币及其在句子意义中的逻辑角色(依存树)。这种老式的代币标记算法是为了给你的生成式语言模型(AI)提供关于真实世界的知识。本章的目标是教你的机器人理解它所读的内容。然后,你将理解放入一个旨在存储知识的灵活数据结构中,称为知识图谱。然后,你的机器人可以利用这些知识做出决策,并就世界发表聪明的言论。
正确地将文本解析为实体,并发现它们之间的关系,这是你从文本中提取事实的方法。 知识图谱,也称为知识数据库(知识库)或语义网络,是一个将知识存储为概念之间关系的数据库。虽然你可以使用关系型数据库存储关系和概念,但有时使用图数据结构更为合适。图中的节点将是实体,而边将是这些实体之间的关系。
你可以在图 11.1 中看到一个知识图谱示例。
图 11.1 知识图谱示例
每个你提取的事实都会在图的节点之间创建一个新的连接 - 或者,可能会创建新的节点。这使得你可以使用诸如 GraphQL、Cypher 甚至 SQL 的查询语言来询问关系。
然后,你的算法可以对文本进行事实检查,不仅限于人类编写的文本,还包括你的 NLP 管道或 AI 生成的文本。最后,你的 AI 算法将能够自省,让你知道它们告诉你的内容是否实际上有些真实的相似之处。
你的 AI 可以使用知识图谱来填补大型语言模型中的 常识知识 差距,也许有点符合关于 LLMs 和 AI 的炒作。这是你需要创建真正 AI 所需的 NLP 链中缺失的环节。你可以使用知识图谱来以编程方式生成合理的文本,因为它基于你的数据库中的事实。你甚至可以推断出关于世界的新事实或 逻辑推理,这些事实尚未包含在你的知识库中。
当人们谈论前向传播或使用深度学习模型进行预测时,你可能会听说过“推理”的概念。深度学习语言模型利用统计学来估计或猜测你输入的文本中的下一个词。深度学习研究人员希望有一天,神经网络能够达到与自然人类推理和思考世界的能力相匹配。但这是不可能的,因为单词并不包含机器需要处理的关于世界的全部知识,以做出事实正确的推断。因此,你将使用一种经过验证和可靠的逻辑推理方法,称为“符号推理”。
如果你熟悉编译器的概念,那么你可能会想到依赖树作为解析树或抽象语法树(AST)。AST 定义了机器语言表达式或程序的逻辑。你将使用自然语言依赖树来提取自然语言文本中的逻辑关系。这种逻辑将帮助你 基于事实 地对统计深度学习模型进行推理,使它们能够做更多不仅仅是像之前章节中那样做统计学上的“猜测”世界的工作。
11.1 推理
一旦你有了一个知识图谱,你的聊天机器人和 AI 代理就会有一种以可解释的方式正确推理世界的方法。如果你能从你的深度学习模型生成的文本中提取事实,你可以检查该文本是否与你在知识图谱中收集的知识一致。当你维护一个知识图谱并使用它来对生成的文本中的事实和推理进行双重检查时,这被称为 推理。当你将语言模型与关于世界的一些基本事实联系起来时,你就在为其打下基础。
推理也可以以其他方式使你的 NLP 流程受益。在算法的推理部分使用知识图谱可以释放你的语言模型,让它做它最擅长的事情——生成合理的、符合语法的文本。因此,你可以微调你的语言模型,让它具有你想要的语气,而不是试图构建一个假装理解和推理世界的变色龙。你的知识图谱可以被设计成只包含你希望你的 AI 理解的世界的事实——无论是你心目中的真实世界的事实还是你正在创建的某个虚构世界的事实。通过将推理与语言分离,你可以创建一个既听起来正确又 是 正确的 NLP 流程。
在提及这个接地过程时,还有一些其他术语经常被使用。有时它被称为符号推理,与机器学习模型的概率推理相对。一阶逻辑是符号推理的一种系统。这是在数据和处理能力不足以支持机器学习和深度学习之前构建专家系统和定理证明器的首选方法。它也被称为 Good Old Fashioned AI 或 GOFAI,发音为“高菲”。随着研究人员试图构建我们可以依赖于做出重要决策的普遍智能系统,GOFAI 重新流行起来。
将您的自然语言处理(NLP)流水线接地的另一个优势是,您可以使用知识库中的事实来解释其推理过程。如果您要求一个未接地的语言模型解释为什么会说出不合理的话,它只会继续为自己(和您)挖一个越来越深的坑,通过编造越来越多的无稽理由。在前几章中,您已经看到了这一点,当语言模型自信地产生(虚构)不存在但合理的引用和虚构人物来解释他们的胡言乱语的来源时。创造一个您可以信任的人工智能的关键是在其下放置一个理性的地板,使用知识图谱。这个接地过程中的第一个,也许是最重要的算法是知识提取。
11.1.1 传统方法:模式匹配的信息提取
在这一章中,我们还将回顾您在前几章中看到的方法,比如正则表达式。为什么要回到硬编码(手动组成的)正则表达式和模式?因为您对自然语言处理的统计或数据驱动方法有限制。您希望您的机器学习流水线能够做一些基本的事情,比如回答逻辑问题或执行根据自然语言指令安排会议。而机器学习在这方面效果不佳。
另外,正如您在这里所看到的,您可以定义一组紧凑的条件检查(正则表达式),以从自然语言字符串中提取关键信息。它可以适用于广泛的问题。
模式匹配(以及正则表达式)仍然是信息提取及相关任务的最先进方法。
先说正事吧。让我们开始知识提取和接地之旅吧!但在处理您的文档之前,我们必须覆盖一个重要步骤,以生成适当的输入到您的知识提取流水线。我们需要将文本分解成较小的单元。
11.2 先解决重要问题:将您的文本分割成句子
在您开始从原始文本中提取知识之前,您需要将其分解为管道可以处理的块。文档“分块”对于创建关于文档的半结构化数据非常有用,这样可以更容易地搜索、过滤和对信息检索的文档进行排序。而对于信息提取,如果您正在提取关系以构建知识库,如 NELL 或 Freebase(稍后将更详细介绍),则需要将其分成可能包含一个或两个事实的部分。当您将自然语言文本分解为有意义的片段时,这称为分割。生成的片段可以是短语、句子、引用、段落,甚至是长文档的整个部分。
对于大多数信息提取问题来说,句子是最常见的块。句子通常用几个符号(“。”、“?”、“!”或换行符)标点。而语法上正确的英语句子必须包含一个主语(名词)和一个动词,这意味着它们通常至少有一个值得提取的事实。句子通常是自包含的意义包,大部分信息不太依赖于前文来传达。
除了促进信息提取外,您还可以将其中一些陈述和句子标记为对话的一部分或适合对话中的回复。使用句子分段器(sentencizer)可以让您在更长的文本(如书籍)上训练您的聊天机器人。选择这些书籍可以使您的聊天机器人比纯粹在 Twitter 流或 IRC 聊天上训练它更具文学性和智能风格。而且这些书籍为您的聊天机器人提供了一个更广泛的训练文档集,以建立关于世界的常识知识。
句子分割是信息提取管道的第一步。它有助于将事实与其他事实隔离开来。大多数句子表达一个单一的连贯思想。而且许多这些思想都是关于现实世界中的真实事物。最重要的是,所有的自然语言都有句子或逻辑上连贯的文本部分。并且所有语言都有一个广泛共享的生成它们的过程(一组语法“规则”或习惯)。
但是,分割文本并识别句子边界比你想象的要棘手。例如,在英语中,没有单个标点符号或字符序列总是标记句子的结束。
11.2.1 为什么 split(‘.!?’) 不能起作用?
甚至即使是人类读者在以下每个引号内找到适当的句子边界也可能有困难。以下是大多数人类可能会尝试拆分为多个句子的一些示例句子:
她喊道:“就在这里!”但我仍然在寻找一个句子的边界。
我目瞪口呆地盯着,像“我是怎么到这里的?”、“我在哪里?”、“我还活着吗?”在屏幕上飞来飞去。
作者写道:“‘我不认为它是有意识的。’ 图灵说。”
即使是人类读者也会很难在每个引号和嵌套引号以及故事中找到合适的句子边界。
更多关于句子分割的“边缘情况”可以在 tm-town.com 上找到。^([2])
技术文本特别难以分割成句子,因为工程师、科学家和数学家倾向于使用句号和感叹号来表示除了句子结束以外的很多事情。当我们试图找出本书中的句子边界时,我们不得不手动纠正了几个提取出的句子。
如果我们写英语像发电报一样,每个句子的结尾都有一个“STOP”或独特的标点符号。但由于我们不是这样做的,你将需要比 split('.!?') 更复杂的自然语言处理。希望你已经在脑海中想象出了一个解决方案。如果是这样,那么它可能基于你在本书中使用过的两种 NLP 方法之一:
- 手动编程算法(正则表达式和模式匹配)
- 统计模型(基于数据的模型或机器学习)
我们将使用句子分割问题来重新审视这两种方法,向你展示如何使用正则表达式以及更先进的方法来找到句子边界。你将使用本书的文本作为训练和测试集来展示一些挑战。幸运的是,你没有在句子内部插入换行符,像报纸栏目布局中那样手动“换行”文本。否则,问题会更加困难。事实上,这本书的大部分源文本,都是以 ASCIIdoc 格式编写的,带有“老式”的句子分隔符(每个句子结束后有两个空格),或者每个句子都在单独的一行上。这样做是为了让我们能够将这本书用作你的分段器的训练和测试集。
11.2.2 使用正则表达式进行句子分割
正则表达式只是一种简写的方式,用来表达字符串中寻找字符模式的“如果…则…”规则(正则语法规则)。正如我们在第一章和第二章中提到的,正则表达式(正则语法)是一种特别简洁的方式,用来指定有限状态机的结构。
任何形式的语法都可以被机器用两种方式使用:
- 用于识别与该语法匹配的内容
- 生成一个新的符号序列
你不仅可以用模式(正则表达式)从自然语言中提取信息,还可以用它们生成与该模式匹配的字符串!如果你需要生成符合正则表达式的示例字符串,可以查看 rstr(缩写为“随机字符串”)包。^([3])这里是你的一些信息提取模式。
这种形式化的语法和有限状态机方法对模式匹配还有其他一些令人惊叹的特点。真正的有限状态机保证在有限步骤内最终停止(停机)。所以如果你使用正则表达式作为你的模式匹配器,你知道你总是会得到关于你是否在你的字符串中找到了一个匹配的答案。它永远不会陷入永久循环……只要你不 “作弊” 并在你的正则表达式中使用向前看或向后看。而且因为正则表达式是确定性的,它总是返回匹配或非匹配。它永远不会给你不到 100% 的置信度或匹配的概率。
所以你会坚持使用不需要这些 “向后看” 或 “向前看” 的正则表达式。你会确保你的正则表达式匹配器处理每个字符,并且仅在它匹配时向前移动到下一个字符 - 就像一个严格的列车售票员走过座位检查票一样。如果没有,售票员会停下来宣布有问题,不匹配,并且他拒绝继续前进,或者向你前后看直到他解决问题。对于列车乘客或严格的正则表达式来说,没有 “回头” 或 “重做”。
我们的正则表达式或有限状态机在这种情况下只有一个目的:识别句子边界。
如果你搜索句子分割器,^([4]) 你可能会被指向旨在捕捉最常见句子边界的各种正则表达式。以下是一些结合和增强以给你一个快速、通用的句子分割器的正则表达式。
以下正则表达式将适用于一些 “正常” 句子。
>>> re.split(r'[!.?]+[\s$]+', ... "Hello World.... Are you there?!?! I'm going to Mars!") ['Hello World', 'Are you there', "I'm going to Mars!"]
不幸的是,这种 re.split 方法会吞噬掉句子终止符。注意一下 “Hello World” 结尾的省略号和句号在返回列表中消失了吗?分割器只在它是文档或字符串中的最后一个字符时才返回句子终止符。一个假设你的句子将以空白结束的正则表达式确实可以很好地忽略双重嵌套引号中的句号的伎俩,但:
>>> re.split( ... r'[!.?]+[\s$]+', ... "The author wrote \"'It isn't conscious.' Turing said.\"") ['The author wrote "\'It isn\'t conscious.\' Turing said."']
看到返回的列表只包含一个句子而不会弄乱引号内的引用吗?不幸的是,这个正则表达式模式也会忽略引号中终止实际句子的句号,因此任何以引号结尾的句子都将与随后的句子连接起来。如果后续的信息提取步骤依赖于准确的句子分割,这可能会降低其准确性。
那么文本消息和带有缩写文本、非正式标点和表情符号的 tweets 呢?匆忙的人类会将句子挤在一起,句号周围没有空格。以下正则表达式可以处理具有字母的短信消息中的句号,并且它会安全地跳过数值:
>>> re.split(r'(?<!\d)\.|\.(?!\d)', "I went to GT.You?") ['I went to GT', 'You?']
即使将这两个正则表达式组合成一个像r'?这样的怪物也不足以让所有的句子都正确。如果你解析了本章手稿的 AciiDoc 文本,它会犯几个错误。你需要在正则表达式模式中添加更多的“向前查找”和“向后查找”来提高其作为句子分割器的准确性。你已经被警告了!
如果查找所有边缘情况并为其设计规则感觉繁琐,那是因为确实如此。句子分割的更好方法是使用在标记句子集上训练过的机器学习算法。通常情况下,逻辑回归或单层神经网络(感知器)就足够了。有几个包含这样的统计模型,你可以用来改进你的句子分割器。SpaCy 和 Punkt(在 NLTK 中)都有很好的句子分割器。你可以猜猜我们使用哪一个。
SpaCy 在默认解析器管道中内置了一个句子分割器,这是你在关键任务应用中的最佳选择。它几乎总是最准确、最健壮、性能最好的选择。以下是如何使用 spaCy 将文本分割成句子:
>>> import spacy >>> nlp = spacy.load('en_core_web_md') >>> doc = nlp("Are you an M.D. Dr. Gebru? either way you are brilliant.") >>> [s.text for s in doc.sents] ['Are you an M.D. Dr. Gebru?', 'either way you are brilliant.']
SpaCy 的准确性依赖于依赖解析。依赖解析器识别每个词在句子图中如何依赖于其他词,就像你在文法学校(小学)学到的那样。拥有这种依赖结构以及令牌嵌入帮助 SpaCy 句子分割器准确处理模糊的标点和大写字母。但所有这些复杂性都需要处理能力和时间。当你只处理几个句子时,速度并不重要,但如果你想要解析本书第九章的 AsciiDoc 手稿呢?
>>> from nlpia2.text_processing.extractors import extract_lines >>> t0 = time.time(); lines = extract_lines( ... 9, nlp=nlp); t1=time.time() # #1 >>> t1 - t0 15.98... >>> t0 = time.time(); lines = extract_lines(9, nlp=None); t1=time.time() >>> t1 - t0 0.022...
哇,这真是太慢了!SpaCy 比正则表达式慢了大约 700 倍。如果你有数百万个文档而不只是这一章的文本,那么你可能需要做一些不同的事情。例如,在一个医疗记录解析项目中,我们需要切换到正则表达式标记器和句子分割器。正则表达式解析器将我们的处理时间从几周缩短到几天,但也降低了我们 NLP 管道的准确性。
SpaCy 现在(截至 2023 年)已经满足了我们对定制化的需求。SpaCy 现在允许你启用或禁用任何你喜欢的管道部分。它还有一个统计句子分段器,不依赖于 SpaCy 管道的其他元素,比如词嵌入和命名实体识别器。当你想加速你的 SpaCy NLP 管道时,你可以移除所有你不需要的元素,然后只添加你想要的管道元素回来。
首先,检查 spacy NLP 管道的pipeline属性,查看默认值中包含什么。然后使用exclude关键字参数来load清理管道。
>>> nlp.pipeline [('tok2vec', <spacy.pipeline.tok2vec.Tok2Vec at 0x...>), ('tagger', <spacy.pipeline.tagger.Tagger at 0x7...>), ('parser', <spacy.pipeline.dep_parser.DependencyParser at 0x...>), ('attribute_ruler', <spacy.pipeline.attributeruler.AttributeRuler at 0x...>), ('lemmatizer', <spacy.lang.en.lemmatizer.EnglishLemmatizer at 0x...>), ('ner', <spacy.pipeline.ner.EntityRecognizer at 0x...>)] >>> nlp = spacy.load("en_core_web_md", exclude=[ ... 'tok2vec', 'parser', 'lemmatizer', # #1 ... 'ner', 'tagger', 'attribute_ruler']) >>> nlp.pipeline # #2 []
现在,您已经清理了管道,现在可以添加回所需的重要部分。在本章的快速运行中,您的 NLP 管道只需要senter管道元素。senter管道是统计句子分割器。
>>> nlp.enable_pipe('senter') >>> nlp.pipeline [('senter', <spacy.pipeline.senter.SentenceRecognizer at 0x...>)] >>> t0 = time.time(); lines2 = extract_lines(nlp=nlp); t1=time.time() >>> t1 - t0 2.3...
这是一个重要的时间节省器-在 8 核 i7 笔记本电脑上为 2.3 秒与 11.5 秒。统计句子分割器比完整的 spaCy 管道快约 5 倍。正则表达式方法仍将快得多,但统计句子分割器将更准确。您可以通过比较句子列表估算这两种算法的准确性,以查看它们是否产生了相同的分割。这不会告诉你哪种方法正在正确地分段特定的文本行,但至少你会看到两个 spaCy 管道什么时候达成一致。
>>> df_md = pd.DataFrame(lines) # #1 >>> df_fast = pd.DataFrame(lines2) # #2 >>> (df_md['sents_spacy'][df_md.is_body] ... == df_fast['sents_spacy'][df_fast.is_body] ... ).sum() / df_md.is_body.sum() 0.93
因此,该书约 93%的句子通过慢速和快速管道进行分段。请查看一些示例分段,以确定哪种方法适合您的用例。
>>> df_md['sents_spacy'][df_md.is_body] 37 [_Transformers_ are changing the world.] ... >>> df_fast['sents_spacy'][df_fast.is_body] 37 [_, Transformers_ are changing the world.] ...
看起来以前导下划线字符(_)开头的句子对于更快的统计分割器要困难一些。因此,您在解析 Markdown 或 AsciiDoc 文本文件时可能需要使用完整的 spacy 模型。如果统计分割器没有接受过类似文本的训练,那么格式字符会使它混淆。
11.2.3 句子语义
现在,您的文本已被分割成包含离散事实的句子,您已经准备好开始在知识图谱中将这些事实提取出来并给它们构建结构。要开始,创建第九章所有句子的 BERT 嵌入热力图。
>>> import pandas as pd >>> url = 'https://gitlab.com/tangibleai/nlpia2/-/raw/main/' >>> url += 'src/nlpia2/data/nlpia_lines.csv' # #1 >>> df = pd.read_csv(url, index_col=0) >>> df9 = df[df.chapter == 9].copy() >>> df9.shape (2028, 24)
看看这个 DataFrame,它具有包含每行文本标签的列。您可以使用这些标签来过滤掉不想处理的行。
>>> pd.options.display.max_colwidth=25 >>> df9[['text', 'is_title', 'is_body', 'is_bullet']] text is_title is_body is_bullet 19057 = Stackable deep lear... True False False ... ... ... ... ... 21080 * By keeping the inpu... False False True 21081 * Transformers combin... False False True 21082 * The GPT transformer... False False True 21083 * Despite being more ... False False True 21084 * If you chose a pret... False False True
现在,您可以使用’is_body’标记来处理手稿正文内的所有句子。这些行应该包含大部分完整的句子,以便您可以语义地将它们与其他语句进行比较,以查看我们有多经常说类似的话的热力图。现在您已经了解了像 BERT 这样的转换器,可以使用它来为您提供比 SpaCy 创建的更有意义的文本表示。
>>> texts = df9.text[df9.is_body] >>> texts.shape (672,) >>> from sentence_transformers import SentenceTransformer >>> minibert = SentenceTransformer('all-MiniLM-L12-v2') >>> vecs = minibert.encode(list(texts)) >>> vecs.shape (672, 384)
MiniLM 模型是一个经过优化和“蒸馏”的通用 BERT 转换器。它提供高精度和速度,并且从 Hugging Face 下载不需要很长时间。现在,您有 689 个文本段落(主要是单个句子)。MiniLM 语言模型已将它们嵌入到 384 维向量空间中。正如您在第六章中了解的那样,嵌入向量语义相似度计算使用归一化点积。
>>> from numpy.linalg import norm >>> dfe = pd.DataFrame([list(v / norm(v)) for v in vecs]) >>> cos_sim = dfe.values.dot(dfe.values.T) >>> cos_sim.shape (672, 672)
现在你有一个方阵,每个文本段和它的 BERT 嵌入向量有一行和一列。矩阵的每个单元格中的值包含该嵌入向量对之间的余弦相似度。如果用文本段的前几个字符标记列和行,那将使得用热图解释所有这些数据更容易。
>>> labels = list(texts.str[:14].values) >>> cos_sim = pd.DataFrame(cos_sim, columns=labels, index=labels) This chapter c _Transformers_ ... A personalized This chapter c 1.000000 0.187846 ... 0.073603 _Transformers_ 0.187846 1.000000 ... -0.010858 The increased 0.149517 0.735687 ... 0.064736 ... ... ... ... ... So here's a qu 0.124551 0.151740 ... 0.418388 And even if yo 0.093767 0.080934 ... 0.522452 A personalized 0.073603 -0.010858 ... 1.000000
通常,余弦相似度在零到一之间变化,大多数值都小于 0.85(85%),除非它们是说基本相同事情的句子。因此,85%将是识别可能被合并或重新措辞以提高本书写作质量的冗余语句的良好阈值。这是这些余弦相似度值的热图。^([10])
>>> import seaborn as sns >>> from matplotlib import pyplot as plt >>> sns.heatmap(cos_sim) <Axes: > >>> plt.xticks(rotation=-35, ha='left') >>> plt.show(block=False)
在第九章大约 60%的位置似乎只有一个小小的白热相似度方块,也许在以“Epoch: 13…”开头的那行附近。这行对应于变压器训练运行的输出文本,因此自然语言模型会将这些机器生成的行视为语义上相似是不奇怪的。毕竟,BERT 语言模型只是对你说“对我来说这都是希腊语。”手稿标记行为自然语言或软件块的正则表达式工作得不是很好。如果你改进了nlpia2.text_processing.extractors中的正则表达式,你可以让你的热图跳过这些不相关的代码行。而且 AsciiDoc 文件是结构化数据,所以它们应该是机器可读的,不需要任何正则表达式的猜测…如果只有一个用于解析 AsciiDoc 文本的最新的 Python 库。^([12])
这是第三章文本的另一个热图。你在这里看到了什么有趣的东西吗?
注意跨越整个章节的巨大深红色十字(打印时为灰色)?这意味着该十字中间的文本与该章节中的所有其他文本非常不同。你能猜到为什么吗?那个部分包含以“Ernqnov…”开头的一个句子,这是“Python 禅”的加密行(import this)。而那个位置的小小白色矩形表明该加密诗的每一行与其附近的行非常相似。
语义热图是在文本数据中找到结构的一种方式,但如果你想从文本中创造知识,你需要更进一步。你的下一步是利用句子的向量表示来创建实体之间的连接“图”。现实世界中的实体是通过事实相关的。我们对世界的心理模型是一个信念网络或知识图 - 所有你知道一些事情的东西(实体)之间的连接的网络。
自然语言处理实战第二版(MEAP)(六)(2)https://developer.aliyun.com/article/1519690