目录
词性标注
实体名称标准化
实体关系标准化和提取
单词模式
文本分割
断句
断句的方式
使用正则表达式进行断句
词性标注
词性(POS)标注可以使用语言模型来完成,这个语言模型包含词及其所有可能词性组成的字典。然后,该模型可以使用已经正确标注好词性的句子进行训练,从而识别由该字典中其他词组成的新句子中所有词的词性。NLTK和spaCy都具备词性标注功能。这里使用spaCy,因为它更快,更精确:
import spacy
en_model=spacy.load('en_core_web_md')
sentence=("In 1541 Desoto wrote in his journal that the Pascagoula people ranged as far north as the confluence of the Leaf and Chickasawhay rivers at 30.4,-88.5.")
parsed_sent=en_model(sentence)
print(parsed_sent.ents)
print(' '.join(['{}_{}'.format(tok,tok.tag_)for tok in parsed_sent]))
这里spaCy一开始没有识别出经纬度对中的经度,后来使用了“OntoNotes 5”词性标注标签体系。
要构建知识图谱,需要确定哪些对象(名词短语)应该配对。我们想把日期“1554年3月15日”与命名实体Desoto配对。然后可以解析这两个字符串(名词短语)以指向我们知识库中的对象。这里可以将1554年3月15日转换为规范化的datetime.date对象
spaCy解析的句子还包含嵌套字典表示的依存树。同时,spacy.displacy可以生成可缩放的矢量图形SVG字符串(或完整的HTML页面),然后在浏览器中以图像的方式查看。上述可视化方式可以帮助我们找到通过依存树创建用于关系提取的标签模式的方法:
from spacy.displacy import render
sentence="In 1541 Desoto wrote in his journal that the Pascagoula."
parsed_sent=en_model(sentence)
with open('pascagoula.html','w') as f:
f.write(render(docs=parsed_sent,page=True,options=dict(compact=True)))
上述短句的依存树表明,名称短语“the Pascagoula”是主语“Desoto”的“met”关系的宾语。这两个名词都被标注为专有名词:
要为spacy.matcher.Matcher创建词性和词属性的模式,以表格形式列出所有的词条标签会很有帮助,下面是一些辅助函数,会使上述过程更容易:
import pandas as pd
from collections import OrderedDict
def token_dict(token):
return OrderedDict(ORTH=token.orth_,LEMMA=token.lemma_,POS=token.pos_,TAG=token.tag_,DEP=token.dep_)
def doc_dataframe(doc):
return pd.DataFrame([token_dict(tok) for tok in doc])
print(doc_dataframe(en_model("In his journal that the Pascagoula.")))
从上例中,可以看到POS或TAG特征值组成的序列构成了一个正确的模式。如果我们查找人与组织的“has-met”关系,我们可能希望引入诸如“PROPN met PROPN”、“PROPN met the PROPN”、“PROPN met with the PROPN”等模式。我们可以单独指定每个模式,或者在专有名词之间尝试使用“任何词”加上*会?操作符的模式来捕获它们:
'PROPN ANYWORD? met ANYWORD?ANYWORD? PROPN'
spaCy中的模式比上述伪代码更强大更灵活,因此必须更加详细的阐述我们想要匹配的词的特征。在spaCy的模式规范中,我们使用字典为每个词或词条去捕获想要匹配的所有标签:
pattern=[{'TAG':'NNP','OP':'+'},
{'IS_ALPHA':True,'OP':'*'},
{'LEMMA':'meet'},
{'IS_ALPHA':True,'OP':'*'},
{'TAG':'NNP','OP':'+'}]
然后,可以从解析的句子中提取想要的带标签的词条:
from spacy.matcher import Matcher
doc=en_model("In 1541 Desoto met the Pascagoula.")
matcher=Matcher(en_model.vocab)
matcher.add('met',patterns=[pattern])
m=matcher(doc)
print(m)
通过上述模式就可以从原始句子中提取一个匹配项。下面看对于类似句子的效果:
doc=en_model("October 24: Lewis and Clark met their first Mandan Chief, Big White.")
m=matcher(doc)[0]
print(m)
print(doc[m[1]:m[2]])
我们需要再添加一个模式,,允许动词在主语和宾语名词之后出现:
doc=en_model("On 11 October 1986, Gorbachev and Reagan met at a house.")
pattern=[{'TAG':'NNP','OP':'+'},
{'LEMMA':'and'},
{'TAG':'NNP','OP':'+'},
{'IS_ALPHA':True,'OP':'*'},
{'LEMMA':'meet'}
]
matcher.add('met',[pattern])
m=matcher(doc)
print(m)
print(doc[m[-1][1]:m[-1][2]])
现在得到了实体和关系。我们甚至可以构建一个对中间动词(“met”)的限制更少、对两侧的人祸组织的名称限制更严格的模式。这样做可能帮助我们识别出更多类似的动词,这些动词也表示一个人或组织和另一个人或组织相遇,例如动词“knows”,甚至包括被动短语,然后我们可以基于这些新的动词给两侧新的专有名词添加关系。
对于如何偏离初始关系模式的原本含义,这被称为语义漂移。幸运的是,spaCy在对被解析文档中的词打标签时,不仅包含词性和依存树信息,还提供了Word2vec词向量。我们可以利用该向量来避免动词和任何一侧的专有名词的连接关系偏离初始模式的原本含义太远。
实体名称标准化
实体的标准化表示通常是一个字符串,即使对于日期之类的数字信息也是如此。日期的标准化ISO格式为“1541-01-01”。实体的标准化表示使我们的知识库能够将图谱中在同一天世界上发生的所有不同事情连接到同一节点(实体)。
我们对其他类型的命名实体也采用标准化,更正单词的拼写,并尝试处理物体、动物、人物、地点等名称的歧义。特别是对于代词或依赖上下文的其他“名称”,标准化命名实体和解决歧义问题通常也被成为共指消解或指代消解。命名实体的标准化确保拼写和命名实体不会产生混淆的、有冗余的名称,从而污染命名实体表。
例如,“Desoto”可能至少以5种不同的方式在特定文档中出现:
- “de Soto”;
- “Hernando de Soto”;
- “Hernando de Soto(约1496/1497-1542),相西班牙征服者”;
- https://.../.../Hernando de Soto(一个URL);
- 著名历史人物数据库的数字ID。
类似的,标准化算法可以选择上述任何一种形式。知识图谱应该以相同的方式对每种实体进行标准化,以防止同一类型的多个不同实体使用了相同的名称。我们不希望多个人的名字都指向同一个人。更重要的是,标准化应该一以贯之的使用——无论是在向知识库写入新的事实,还是在读取或者查询知识库时都应如此。
如果打算在填充知识库之后更改标准化的方法,那么应该“迁移”或更新知识库中已有实体的数据,以符合新的标准化模式。用于存储知识图谱或知识库的无模式数据库(键值存储),也会受到关系数据库迁移的影响。毕竟,无模式数据库实际上就是关系数据库的接口封装器。
实体标准化之后,还需要“is-a”关系将它们连接到实体类别,这些实体类别定义了实体的类别或类型。因为每个实体可以具有多个“is-a”关系,所以这些“is-a”关系可以被认为是标签。类似于人名或词性标签,如果想要在知识库中使用日期和其他离散数字对象,也需要对其进行标准化。
实体关系标准化和提取
现在需要一种标准化实体关系的方法,从而确定实体之间的关系类型。通过标准化,我们可以找到日期和人之间的所有生日关系,或历史事件发生的日期,类似与“Hernando de Soto”和“Pascagoula people”相遇的时间。我们需要编写算法来选择上述关系中的正确标签。
此外,这些关系可以采用层次化的命名方式,例如“发生于/近似地”和“发生于/精确地”,从而让我们采用特定的关系或者关系类别。还可以使用一个数字属性来标记这些关系的“置信度”、概率、权重或者标准化频率(类似于词项/单词的TF-IDF)。每次从新文本中提取的事实证实了知识库中存在的事实或该事实矛盾时,都可以调整这些置信度的值。
现在需要一种方法来匹配可以找到这些关系的模式。
单词模式
单词模式就像正则表达式一样,但它用于单词而不是字符。我们使用单词类,而不使用字符类。例如,我们不是通过匹配小写字符,而是通过单词模式判定方法来匹配所有的单数名词(词性标注为“NN”)。这往往通过机器学习来实现。一些种子句利用从句子中提取的正确关系(事实)来进行标注,然后可以通过词性标注模式查找类似的句子,即使句子中的主语词和宾语词甚至关系有所变化。
无论先要匹配多少个模式,都可以使用spaCy包以两种不同的方式在O(1)(常数时间)时间内匹配这些模式:
- 用于任何单词/标签序列模式的PhraseMatcher;
- 用于词性标签序列模式的Matcher。
为了确保在新句子中找到的新关系真正类似于原始的种子(例子)关系,我们通常需要新句子中的主语词、关系词和宾语词的含义与种子句子中的类似。实现上述目标的最好方法是使用词义的向量表示。词向量有助于尽可能减少语义漂移的发生。
使用单词和短语的语义向量表示使自动信息提取精确到能够自动构建大型知识库。不过仍然需要人工监督和管理来处理自然语言文本中的大量歧义。
文本分割
文档“组块”有助于创建关于文档的半结构化数据,从而让文档在信息检索场景中更加容易搜索、过滤和排序。对于信息提取,如果从中提取关系以创建知识库(如NELL或Freebase),则需要将文档拆分成可能包含一到两个事实的多个部分。我们把自然语言文本划分为有意义的各部分的过程,称为文本分割。得到的分割结果可以是短语、句子、引文、段落甚至是长文档的整个章节。
对于大多数信息提取问题,句子是最常见的块。句子之间通常使用一些符号(.、?、\、!或换行符)作为标点。语法正确的英文句子必须包含一个主语(名词)和一个动词,这意味着它们之间通常至少有一个关系或事实值得提取。句子的意义通常是自包含的,不会过多依赖前面的文本来传达句子的大部分信息。
幸运的是,包含英语在内的大多数语言都有句子的概念,即一个单独的语句,包含一个主语和一个表达了某些内容的动词。对于NLP知识提取流水线,句子正是所需要的文本块。对于聊天机器人流水线,我们的目标是将文档划分成句子和语句。
除了便于信息提取,我们还可以将其中一些语句和句子进行标记,然后作为对话的一部分或者对话中的适当回复。通过短句,可以在较长的文本上训练聊天机器人。相比于单纯在聊天记录上训练,选择合适的数据可以使聊天机器人具有更文艺、智慧的风格。这些书籍使聊天机器人可以使用范围更广的训练文档,从而获得关于世界的常识性知识。
断句
断句通常是信息提取流水线的第一步,它有助于将事实彼此隔离,以便于可以在“The Babel fish cost $42.42 cents for the stamp”这个字符串中,将正确的价格与正确的事物相关联。上述字符串是表明断句很难的一个很好的例子——中间的句号可以被解释为小数点或句号结束符。
我们从文档中提取的最简单的“信息”是包含逻辑性连贯语句的词序列。在自然语言文档中,重要性排在词之后的是句子。句子包含了关于世界的逻辑连贯语句。这些语句包含我们要从文本中提取的信息。句子描述事实的时候,常常描述事物之间的关系以及世界运行的原理,因此我们可以基于句子进行知识提取。句子通常用来解释过去的某个时间、地点,事情是怎么发生的,一般会怎么发展,或者将来怎么发展。因此,还应该能够用句子作为知道,提取有关时间、地点、人、甚至事件或任务序列的事实。而且,最重要的是,所有自然语言都有句子或某种逻辑连贯的文本部分,并且所有语言都有一个广泛认同的步骤来生成它们(一套语法规则或习惯)。
但是断句即识别句子边界,是很复杂的。例如在英语中没有哪个标点符号或字符序列可以始终标记句子的结尾。
断句的方式
即使是人也可能无法在每个句子中找到合适的句子边界。对于一些疑难例子,错误率可能都接近100%。
即使文档特别难以断句,因为对工程师、科学家和数学家来说,句子和感叹号除了可以用来表示句子结尾外,还会被用来表示很多其他内容,对于这些情况,需要一些更复杂的NLP方法,而不仅仅是split('.!?)。这些方法分别是:
- 手动编程算法(正则表达式和模式匹配);
- 统计模型(基于数据的模型或机器学习)。
使用正则表达式进行断句
正则表达式知识描述“if...then”规则树(正则语法规则)的简写方法,用于查找字符串中的字符模式。正则表达式(正则语法)是指定有限状态机规则的一种特别简明的方法,使用正则表达式或有限状态机只有一个目的:识别句子边界。
有一些搜索断句工具,它们通过组合和增强提供快速、通用的断句表达式。以下正则表达式适用于一些“正常”句子:
import re
print(re.split(r'[!.?]+[ $]',"Hello World.... Are you there?!?! I'm going to Mars!"))
但是上面的re.split方法消耗了句子的分隔符,只有该分隔符是文档或者字符串的最后一个字符时才会被保留。但该方法确实正确地忽略了双层嵌套引号中句号的问题:
print(re.split(r'[!.?] ',"The author wrote \"'I don't think it's conscious.'Turing said."))
但是该方法也忽略了引号中真实句子的分隔符。这可能是好事也是坏事,取决于断句之后的信息提取步骤:
print(re.split(r'[!.?] ',"The author wrote \"'I don't think it's conscious.'Turing said.\" But I stopped readin."))
缩写文本的效果呢?有时人们着急会把句子写到一起,句号周围没有留空。以下正则表达式职能处理在任何一侧都有字母的短消息中的句号,并且它可以安全地跳过数值:
print(re.split(r'(?<!\d)\.|\.(?!\d)',"I went to GT.You?"))
即使合并上面两个正则表达式,也无法在nlpia.data的疑难测试用例中获得较好的效果:
from nlpia.data.loaders import get_data
regex=re.compile(r'(?<!\d)\.|\.(?!\d)|([!.?]+)[ $]+')
examples=get_data('sentence_tm_town')
wrong=[]
for i,(challenge,text,sents) in enumerate(examples):
if tuple(regex.split(text))!=tuple(sents):
print('wrong{}:{}{}'.format(i,text[:50],'...'if len(text)>50 else ''))
wrong=wrong+[i]
print(len(wrong),len(examples))
必须添加“前向环视”和“后向环视”来提高正则表达式断句工具的精确性。更好的断句方法是使用在标记好的句子集合上训练的机器学习算法(通常是单层神经网络或对率回归)。有些软件包含这样的模型,大家可以使用它来改进断句工具:
- DetectorMorse;
- spaCy;
- SyntaxNet;
- NLTK;
- StandfordCoreNLP。
对于大多数关键任务的应用程序,可以使用spaCy断句工具(内置于解析器中)。spaCy依赖少,并且在精确率和速度方面与其他工具相当。纯Python实现中,Kyle Gorman的DetectorMorse也是一个不错的选择。