基于规则的匹配
文档说明
版本
✔️ ver_1.0.1
概述
查找短语和单词(tokens)以及实体匹配
相比于在原始文本上使用正则表达式匹配,spaCy
的基于规则的匹配器引擎和组件不仅可以找到所需的单词和短语,还可以让您访问文档中的词及其相互关系。这意味着您可以轻松访问和分析周围的词,将跨度(span)合并为单个词或加入到doc.ents
中的命名实体中。
我应该使用规则还是训练一个模型?
对于复杂的任务,通常最好是训练一个基于统计的命名实体识别模型。但是,统计模型需要训练数据,因此在许多情况下,基于规则的方法更实用。在项目初始阶段尤其如此:你可以使用基于规则的方法作为数据收集过程的一部分,以帮助您“引导”统计模型。
如果您有一些示例并且希望您的系统能够基于这些示例进行泛化(generalize),那么就应该训练模型。如果在当前上下文有线索,它会更加有效。例如,如果您尝试检测人名或公司名称,您的应用程序可能会受益于统计命名实体识别模型。
如果你想在数据中查找有限数量的示例,那么基于规则的系统就是一个很好的选择,或者你可以轻松找到一个规则或者正则表达式来准确的表达一个模式 。例如,你可以通过基于规则的方法很好地处理国家名称、
IP
地址或URL
等。
您还可以将这两种方法结合使用,可以使用规则改进统计模型,以处理非常具体的情况并提高准确性。有关详细信息,请参阅基于规则的实体识别部分。
如何选择:token匹配器(Matcher) vs 短语匹配器?
如果你已经拥有一个大型术语列表或一个由单个或多个token短语组成的地名录,此时你想找到一个确切的实例,那么PhraseMacher
就非常有用。 从spaCy v2.1.0
开始,您还可以通过LOWER
属性进行快速且不区分大小写的匹配。
Matcher
没有PhraseMatcher
那么快,因为他会比较所有token的属性。但是,它允许你使用模型预测的词汇属性和语言特征为正在寻找的tokens编写一个非常抽象的表示。例如,你可以查找一个跟在“love”或“like”动词后的名册,或可选限定符以及一个不少于10个字符的token后的名词,
Token-based
匹配
spaCy
自带规则匹配引擎Matcher
, 对tokens的操作类似于正则表达式。规则可以引用token
注释(例如text
或tag_
的token,或者如IS_PUNCT
的标志)。规则匹配器还允许您传入自定义回调以处理匹配项 - 例如,可以合并实体和应用自定义标签。您还可以将模式与实体ID
相关联,以允许进行一些基本的实体链接或消歧。要匹配大型术语列表,您可以使用 PhraseMatcher
,它接受Doc
对象作为匹配模式。
添加模式
假设我们想让spaCy
找到三个token
的组合:
- 小写形式与“hello”匹配的token,例如“Hello”或“HELLO”。
is_punct
标记设置为True的标记,即任何标点符号。- 小写形式与“world”匹配的标记,例如“World”或“WORLD”。
[{"LOWER": "hello"}, {"IS_PUNCT": True}, {"LOWER": "world"}]
[{"LOWER": "hello"}, {"IS_PUNCT": True}, {"LOWER": "world"}]
! ! ! 重要的提示
在编写模式时,记住每个字典代表一个token。如果spaCy
的分词处理与模式中定义的token
不匹配,则该模式不会产生任何结果。在开发复杂模式时,请确保根据spaCy
的标记化检查示例:doc = nlp("A complex-example,!") print([token.text for token in doc])
doc = nlp("A complex-example,!") print([token.text for token in doc])
首先,我们用一个词汇来初始化Matcher
。匹配器必须始终与其操作的文档共享相同的词汇。我们现在可以调用matcher.add()
并传入一个ID
和一个模式列表作为参数。
# `spaCy v 3.0 · Python 3 基于 Binder`
import spacy
from spacy.matcher import Matcher
nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab)
# Add match ID "HelloWorld" with no callback and one pattern
pattern = [{"LOWER": "hello"}, {"IS_PUNCT": True}, {"LOWER": "world"}]
# 将会匹配到“hello”+“标点”+“world”
matcher.add("HelloWorld", [pattern])
doc = nlp("Hello, world! Hello world!")
matches = matcher(doc)
for match_id, start, end in matches:
string_id = nlp.vocab.strings[match_id] # Get string representation
span = doc[start:end] # The matched span
print(match_id, string_id, start, end, span.text)
# `spaCy v 3.0 · Python 3 基于 Binder`
import spacy
from spacy.matcher import Matcher
nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab)
# Add match ID "HelloWorld" with no callback and one pattern
pattern = [{"LOWER": "hello"}, {"IS_PUNCT": True}, {"LOWER": "world"}]
# 将会匹配到“hello”+“标点”+“world”
matcher.add("HelloWorld", [pattern])
doc = nlp("Hello, world! Hello world!")
matches = matcher(doc)
for match_id, start, end in matches:
string_id = nlp.vocab.strings[match_id] # Get string representation
span = doc[start:end] # The matched span
print(match_id, string_id, start, end, span.text)
结果:15578876784678163569 HelloWorld 0 3 Hello, world
匹配器返回一个(match_id, start, end)元组的列表——在本例中就是[('15578876784678163569', 0, 3)],它映射到我们原始文档的doc[0:3]
。该match_id
是字符串ID的“HelloWorld”的散列值的。要获取字符串值,你可以在StringStore中查询。
for match_id, start, end in matches:
string_id = nlp.vocab.strings[match_id] # 'HelloWorld'
span = doc[start:end] # The matched span
for match_id, start, end in matches:
string_id = nlp.vocab.strings[match_id] # 'HelloWorld'
span = doc[start:end] # The matched span
或者,我们还可以选择添加多个模式,例如,还可以匹配“hello”和“world”之间没有标点符号的序列:
patterns = [
[{"LOWER": "hello"}, {"IS_PUNCT": True}, {"LOWER": "world"}],
[{"LOWER": "hello"}, {"LOWER": "world"}]
]
matcher.add("HelloWorld", patterns)
patterns = [
[{"LOWER": "hello"}, {"IS_PUNCT": True}, {"LOWER": "world"}],
[{"LOWER": "hello"}, {"LOWER": "world"}]
]
matcher.add("HelloWorld", patterns)
默认情况下,匹配器将只返回匹配项而不执行任何其他操作,例如合并实体或分配标签。这完全取决于您,你可以通过向add()
函数传入不同的回调函数作为on_match
参数来单独定义每个模式。这很有用,因为它允许您编写完全自定义和模式特定逻辑。例如,您可能希望将一些模式合并为一个token
,同时为其他模式类型添加实体标签。这个时候你不必为每个进程创建不同的匹配器。
可用的token属性
可用的Token模式键对应于许多Token
属性。基于规则的匹配支持的属性有:
ATTRIBUTE属性 | 描述 |
---|---|
ORTH | 准确的Token逐字文本. TYPE:str |
TEXT | 准确的Token逐字文本. TYPE:str |
LOWER | Token文本的小写形式.TYPE:str |
LENGTH | Token文本的长度.TYPE:int |
IS_ALPHA, IS_ASCII, IS_DIGIT | Token文本由字母字符、ASCII 字符、数字组成.TYPE:bool |
IS_LOWER, IS_UPPER, IS_TITLE | Token文本是小写、大写、标题.TYPE:bool |
IS_PUNCT, IS_SPACE, IS_STOP | Token文本是标点符号、空格、停止词.TYPE:bool |
IS_SENT_START | Token是句子的开始.TYPE:bool |
LIKE_NUM, LIKE_URL, LIKE_EMAIL | Token文本类似于数字、URL、电子邮件.TYPE:bool |
SPACY | Token有一个结尾空格.TYPE:bool |
POS, TAG, MORPH, DEP, LEMMA, SHAPE | token的简单和扩展词性标签、形态分析、依赖标签、词根、形状(shape)。请注意,这些属性的值区分大小写。有关可用词性标签和依赖项标签的列表,请参阅注释规范。 TYPE:str |
ENT_TYPE | Token的实体标签。 TYPE:str |
_ | 自定义扩展属性的属性. TYPE:Dict[str, Any] |
OP | 运算符和量词,限定Token模式的匹配次数。TYPE:str |
属性名称区分大小写吗? 不区分。spaCy会进行名称规范化处理,
{"LOWER": "text"}
和{"lower": "text"}
会得到相同的结果。使用大写版本主要是一种约定,以明确这些属性是“特殊的”并且不完全映射到像Token.lower和Token.lower_这样的标记属性。
为什么不支持所有Token属性?
spaCy
无法提供对所有属性的访问,因为Matcher循环遍历Cython
数据,而不是Python
对象。在匹配器内部,我们处理TokenC struct
– 我们并没有Token
实例。这意味着所有引用计算属性的属性都无法访问。
大写的属性名称类似于LOWER
或IS_PUNCT
引用自spacy.attrs
枚举表。它们被传递到一个本质上是一个case/switch
语句的函数中,以确定要返回的结构字段。相同的属性标识符还被用在Doc.to_array
,以及代码中需要描述此类字段的其他一些地方。
提示:试用交互式匹配器 匹配器演示
该交互式匹配器(Matcher)能够通过交互式创建token模式来测试基于规则的Matcher
。每个Token可以设置多个属性,如文本值、词性标记或布尔标志。基于token的视图可以让你看到spaCy是如何处理您的文本的,以及为什么可以成功匹配,或者为什么无法匹配。
扩展模式语法和属性
Token模式也可以映射到属性字典,而不一定是单个值。例如,指定词根的值是值列表的一部分,或设置最小字符长度。可用的属性如下表:
# Matches "love cats" or "likes flowers"
pattern1 = [{"LEMMA": {"IN": ["like", "love"]}},
{"POS": "NOUN"}]
# Matches tokens of length >= 10
pattern2 = [{"LENGTH": {">=": 10}}]
# Match based on morph attributes
pattern3 = [{"MORPH": {"IS_SUBSET": ["Number=Sing", "Gender=Neut"]}}]
# "", "Number=Sing" and "Number=Sing|Gender=Neut" will match as subsets
# "Number=Plur|Gender=Neut" will not match
# "Number=Sing|Gender=Neut|Polite=Infm" will not match because it's a superset
# Matches "love cats" or "likes flowers"
pattern1 = [{"LEMMA": {"IN": ["like", "love"]}},
{"POS": "NOUN"}]
# Matches tokens of length >= 10
pattern2 = [{"LENGTH": {">=": 10}}]
# Match based on morph attributes
pattern3 = [{"MORPH": {"IS_SUBSET": ["Number=Sing", "Gender=Neut"]}}]
# "", "Number=Sing" and "Number=Sing|Gender=Neut" will match as subsets
# "Number=Plur|Gender=Neut" will not match
# "Number=Sing|Gender=Neut|Polite=Infm" will not match because it's a superset
属性 | 描述 |
---|---|
IN | 属性值是列表的成员。TYPE:Any |
NOT_IN | 属性值不是列表的成员。TYPE:Any |
IS_SUBSET | 属性值(用于MORPH 或自定义列表属性)是列表的子集。TYPE:Any |
IS_SUPERSET | 属性值(用于MORPH 或自定义列表属性)是列表的超集。TYPE:Any |
INTERSECTS | 属性值(用于MORPH 或自定义列表属性)与列表具有非空交集。 |
Any | |
==, >=, <=, >, < | 属性值等于、大于或等于、小于或等于、大于或小于。TYPE:Union[int, float] |
常用表达式
在某些情况下,仅匹配token和token属性是不够的——例如,你可能希望匹配一个单词的不同拼写,而想为每个拼写添加新模式。
pattern = [{"TEXT": {"REGEX": "^[Uu](\.?|nited)$"}},
{"TEXT": {"REGEX": "^[Ss](\.?|tates)$"}},
{"LOWER": "president"}]
pattern = [{"TEXT": {"REGEX": "^[Uu](\.?|nited)$"}},
{"TEXT": {"REGEX": "^[Ss](\.?|tates)$"}},
{"LOWER": "president"}]
REGEX
操作允许你为任何属性定义规则,包括自定义属性。它始终需要应用于像TEXT
,LOWER
或TAG
属性之中:
# 匹配token文本的不同拼写
pattern = [{"TEXT": {"REGEX": "deff?in[ia]tely"}}]
# 匹配词性标记以“V”开头的的token
pattern = [{"TAG": {"REGEX": "^V"}}]
# 匹配自定义的正则表达式
pattern = [{"_": {"country": {"REGEX": "^[Uu](nited|\.?) ?[Ss](tates|\.?)$"}}}]
# 匹配token文本的不同拼写
pattern = [{"TEXT": {"REGEX": "deff?in[ia]tely"}}]
# 匹配词性标记以“V”开头的的token
pattern = [{"TAG": {"REGEX": "^V"}}]
# 匹配自定义的正则表达式
pattern = [{"_": {"country": {"REGEX": "^[Uu](nited|\.?) ?[Ss](tates|\.?)$"}}}]
! ! ! 重要的提示
使用REGEX
运算符时,请记住它对单个token而不是整个文本进行操作。每个表达式都将匹配一个token。如果您需要对整个文本进行匹配,请参阅有关对整个文本进行正则表达式匹配的详细信息 。
全文匹配正则表达式
如果您的表达式适用于多个token,一个简单的解决方案是使用re.finditer
匹配doc.text
并使用Doc.char_span
方法从匹配的字符索引中创建一个Span
。如果匹配的字符没有映射到一个或多个有效Token,则Doc.char_span
返回None
。
什么是有效的token序列? 在该示例中,表达式将匹配"US"在"USA"。但是,"USA"是单个标记,而
Span
对象是Token的序列。所以 "US"不能是它自己的span,因为它没有在token边界上结束。
import spacy
import re
nlp = spacy.load("en_core_web_sm")
doc = nlp("The United States of America (USA) are commonly known as the United States (U.S. or US) or America.")
expression = r"[Uu](nited|\.?) ?[Ss](tates|\.?)"
for match in re.finditer(expression, doc.text):
start, end = match.span()
span = doc.char_span(start, end)
# This is a Span object or None if match doesn't map to valid token sequence
if span is not None:
print("Found match:", span.text)
import spacy
import re
nlp = spacy.load("en_core_web_sm")
doc = nlp("The United States of America (USA) are commonly known as the United States (U.S. or US) or America.")
expression = r"[Uu](nited|\.?) ?[Ss](tates|\.?)"
for match in re.finditer(expression, doc.text):
start, end = match.span()
span = doc.char_span(start, end)
# This is a Span object or None if match doesn't map to valid token sequence
if span is not None:
print("Found match:", span.text)
结果
Found match: United States
Found match: United States
Found match: U.S.
Found match: US
Found match: United States
Found match: United States
Found match: U.S.
Found match: US
如何将
match
扩展为有效的令牌序列?
在某些情况下,您可能希望将匹配扩展到最近的token边界,你可以为"USA"创建一个span,即使只有子串"US"会被匹配。您可以使用文档中tokens的字符偏移量Token.idx)来计算它。这样你就可以创建一个列表包含有效的token开始和结束边界,并给你留下一个基础的算法问题:给定一个数字,找到下一个最小数(token开始)或下一个最大数(token结束)。这将是最接近有效token的边界。
有很多实现方法,最直接的方法是在
Doc
中创建一个基于字符的字典表,索引到所在的token(token中包含该字符)中。它易于编写且不易出错,并为您提供恒定的查找时间:您只需为每个Doc
创建一次词典。
chars_to_tokens = {}
for token in doc:
for i in range(token.idx, token.idx + len(token.text)):
chars_to_tokens[i] = token.i
chars_to_tokens = {}
for token in doc:
for i in range(token.idx, token.idx + len(token.text)):
chars_to_tokens[i] = token.i
然后,您可以在给定位置查找字符,并获取该字符所属的相应token的索引。你的span就是doc[token_start:token_end]。 如果字符不在dict
中,则意味着它是(white)空格token被拆分的。不过,这希望不应该发生,因为这意味着您的正则表达式正在生成包含前导或后续空格的匹配项。
span = doc.char_span(start, end)
if span is not None:
print("Found match:", span.text)
else:
start_token = chars_to_tokens.get(start)
end_token = chars_to_tokens.get(end)
if start_token is not None and end_token is not None:
span = doc[start_token:end_token + 1]
print("Found closest match:", span.text)
span = doc.char_span(start, end)
if span is not None:
print("Found match:", span.text)
else:
start_token = chars_to_tokens.get(start)
end_token = chars_to_tokens.get(end)
if start_token is not None and end_token is not None:
span = doc[start_token:end_token + 1]
print("Found closest match:", span.text)
运算符和量词
匹配器还允许您使用指定为'OP'
键的量词。量词让您定义要匹配的token序列,例如一个或多个标点符号,或指定可选token。请注意,不能使用嵌套或作用域量词——你可以使用on_match
回调来实现。
OP运算符 | 描述 |
---|---|
! | 否定模式,要求它精确匹配 0 次。 |
? | 使模式可选,允许它匹配 0 次或 1 次 |
+ | 要求模式匹配 1 次或多次。 |
* | 允许模式匹配零次或多次。 |
pattern = [{"LOWER": "hello"},
{"IS_PUNCT": True, "OP": "?"}]
pattern = [{"LOWER": "hello"},
{"IS_PUNCT": True, "OP": "?"}]
使用通配符token模式
token属性提供了许多选项来编写高度定制化的模式,同时你还可以使用空字典{}作为匹配任何token的通配符。如果你知道要匹配的内容的上下文但对特定标记及其字符知之甚少,这将非常有用。例如,假设您试图从您的数据中提取用户名。您只知道它们是一个“username:{username}”列表。名称本身可以包含任何字符,但不能包含空格——因此您会知道它将作为一个标记处理。
[{"ORTH": "User"}, {"ORTH": "name"}, {"ORTH": ":"}, {}]
[{"ORTH": "User"}, {"ORTH": "name"}, {"ORTH": ":"}, {}]
验证和调试模式(patterns)
可以通过选项validate=True
来验证Matcher
的模式。这在开发过程中进行调试很有用,特别是用于捕获不受支持的属性。
import spacy
from spacy.matcher import Matcher
nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab, validate=True)
# Add match ID "HelloWorld" with unsupported attribute CASEINSENSITIVE
pattern = [{"LOWER": "hello"}, {"IS_PUNCT": True}, {"CASEINSENSITIVE": "world"}]
matcher.add("HelloWorld", [pattern])
# 🚨 Raises an error:
# MatchPatternError: Invalid token patterns for matcher rule 'HelloWorld'
# Pattern 0:
# - [pattern -> 2 -> CASEINSENSITIVE] extra fields not permitted
import spacy
from spacy.matcher import Matcher
nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab, validate=True)
# Add match ID "HelloWorld" with unsupported attribute CASEINSENSITIVE
pattern = [{"LOWER": "hello"}, {"IS_PUNCT": True}, {"CASEINSENSITIVE": "world"}]
matcher.add("HelloWorld", [pattern])
# 🚨 Raises an error:
# MatchPatternError: Invalid token patterns for matcher rule 'HelloWorld'
# Pattern 0:
# - [pattern -> 2 -> CASEINSENSITIVE] extra fields not permitted
添加 on_match 规则
转到更现实的示例,假设您正在处理大量博客文章,并且您希望匹配所有提及的“Google I/O”(spaCy将其分解为(tokenization)['Google', 'I', '/', 'O'])。为安全起见,您只匹配大写版本,避免匹配诸如“Google i/o”之类的短语。
from spacy.lang.en import English
from spacy.matcher import Matcher
from spacy.tokens import Span
nlp = English()
matcher = Matcher(nlp.vocab)
def add_event_ent(matcher, doc, i, matches):
# Get the current match and create tuple of entity label, start and end.
# Append entity to the doc's entity. (Don't overwrite doc.ents!)
match_id, start, end = matches[i]
entity = Span(doc, start, end, label="EVENT")
doc.ents += (entity,)
print(entity.text)
pattern = [{"ORTH": "Google"}, {"ORTH": "I"}, {"ORTH": "/"}, {"ORTH": "O"}]
matcher.add("GoogleIO", [pattern], on_match=add_event_ent)
doc = nlp("This is a text about Google I/O")
matches = matcher(doc)
from spacy.lang.en import English
from spacy.matcher import Matcher
from spacy.tokens import Span
nlp = English()
matcher = Matcher(nlp.vocab)
def add_event_ent(matcher, doc, i, matches):
# Get the current match and create tuple of entity label, start and end.
# Append entity to the doc's entity. (Don't overwrite doc.ents!)
match_id, start, end = matches[i]
entity = Span(doc, start, end, label="EVENT")
doc.ents += (entity,)
print(entity.text)
pattern = [{"ORTH": "Google"}, {"ORTH": "I"}, {"ORTH": "/"}, {"ORTH": "O"}]
matcher.add("GoogleIO", [pattern], on_match=add_event_ent)
doc = nlp("This is a text about Google I/O")
matches = matcher(doc)
SpaCy
提供了内置的EntityRuler
实现了非常相似的逻辑.它还负责处理重叠的匹配项,否则您必须自己处理。
提示: 可视化匹配
在处理实体时,你可以使用displaCy
来快速从Doc
中创建一个NER
可视化,生成一个HTML文件:
from spacy import displacy
html = displacy.render(doc, style="ent", page=True, options={"ents": ["EVENT"]})
from spacy import displacy
html = displacy.render(doc, style="ent", page=True, options={"ents": ["EVENT"]})
更多信息和示例,请参阅可视化spaCy使用指南。
现在可以在文档上调用匹配器了。模式将按照它们在文本中出现的顺序进行匹配。然后匹配器将遍历匹配项,查找匹配到的匹配ID的回调,并调用它。
doc = nlp(YOUR_TEXT_HERE)
matcher(doc)
doc = nlp(YOUR_TEXT_HERE)
matcher(doc)
当回调被调用时,它被传递四个参数:匹配器本身、文档、当前匹配的位置和匹配的总列表。这允许您编写整个匹配短语集的回调,以便您可以以任何您喜欢的方式解决重叠和其他冲突。
Argument | 描述 |
---|---|
matcher | 匹配器实例。Type:Matcher |
doc | 使用匹配器的文档。Type:Doc |
i | 当前匹配项的索引 ( matches[i])。Type:int |
matches | 描述匹配的元组(match_id, start, end)列表。匹配元组描述跨度doc[start:end]。Type:List[Tuple[int, int int]] |
从匹配中创建spans
一个非常常见的用例时从返回的匹配中创建Span
对象。通过访问每个匹配项的start
和end
标记使这变得简单,你可以使用它来构造一个带有可选标签的新span。从 spaCy v3.0 开始,您还可以在调用匹配器的时候设置as_spans=True
来返回一个Span对象列表,其中使用match_id
作为span
标签。
import spacy
from spacy.matcher import Matcher
from spacy.tokens import Span
nlp = spacy.blank("en")
matcher = Matcher(nlp.vocab)
matcher.add("PERSON", [[{"lower": "barack"}, {"lower": "obama"}]])
doc = nlp("Barack Obama was the 44th president of the United States")
# 1. Return (match_id, start, end) tuples
matches = matcher(doc)
for match_id, start, end in matches:
# Create the matched span and assign the match_id as a label
span = Span(doc, start, end, label=match_id)
print(span.text, span.label_)
# 2. Return Span objects directly
matches = matcher(doc, as_spans=True)
for span in matches:
print(span.text, span.label_)
import spacy
from spacy.matcher import Matcher
from spacy.tokens import Span
nlp = spacy.blank("en")
matcher = Matcher(nlp.vocab)
matcher.add("PERSON", [[{"lower": "barack"}, {"lower": "obama"}]])
doc = nlp("Barack Obama was the 44th president of the United States")
# 1. Return (match_id, start, end) tuples
matches = matcher(doc)
for match_id, start, end in matches:
# Create the matched span and assign the match_id as a label
span = Span(doc, start, end, label=match_id)
print(span.text, span.label_)
# 2. Return Span objects directly
matches = matcher(doc, as_spans=True)
for span in matches:
print(span.text, span.label_)
使用自定义管道组件
假设您的数据还包含一些令人讨厌的预处理工作,例如其中的HTML
换行符(例如<br>
或<BR/>
)。为了使您的文本更易于分析,您希望将它们合并为一个toke
n并标记它们,以确保您以后可以忽略它们。理想情况下,这应该在您处理文本时自动完成。你可以在Doc
对象中添加自定义管道组件。
import spacy
from spacy.language import Language
from spacy.matcher import Matcher
from spacy.tokens import Token
# 这里使用了一个组件工厂,他直接返回一个组件对象
@Language.factory("html_merger")
def create_bad_html_merger(nlp, name):
return BadHTMLMerger(nlp.vocab)
class BadHTMLMerger:
def __init__(self, vocab):
patterns = [
[{"ORTH": "<"}, {"LOWER": "br"}, {"ORTH": ">"}],
[{"ORTH": "<"}, {"LOWER": "br/"}, {"ORTH": ">"}],
]
# 注册一个新的token扩展来标记上述不规范的HTML
Token.set_extension("bad_html", default=False)
self.matcher = Matcher(vocab)
self.matcher.add("BAD_HTML", patterns)
def __call__(self, doc):
# 当Doc访问该组件时,该方法将被调用
matches = self.matcher(doc)
spans = [] # Collect the matched spans here
for match_id, start, end in matches:
spans.append(doc[start:end])
with doc.retokenize() as retokenizer:
for span in spans:
retokenizer.merge(span)
for token in span:
token._.bad_html = True # Mark token as bad HTML
return doc
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("html_merger", last=True) # Add component to the pipeline
doc = nlp("Hello<br>world! <br/> This is a test.")
for token in doc:
print(token.text, token._.bad_html)
import spacy
from spacy.language import Language
from spacy.matcher import Matcher
from spacy.tokens import Token
# 这里使用了一个组件工厂,他直接返回一个组件对象
@Language.factory("html_merger")
def create_bad_html_merger(nlp, name):
return BadHTMLMerger(nlp.vocab)
class BadHTMLMerger:
def __init__(self, vocab):
patterns = [
[{"ORTH": "<"}, {"LOWER": "br"}, {"ORTH": ">"}],
[{"ORTH": "<"}, {"LOWER": "br/"}, {"ORTH": ">"}],
]
# 注册一个新的token扩展来标记上述不规范的HTML
Token.set_extension("bad_html", default=False)
self.matcher = Matcher(vocab)
self.matcher.add("BAD_HTML", patterns)
def __call__(self, doc):
# 当Doc访问该组件时,该方法将被调用
matches = self.matcher(doc)
spans = [] # Collect the matched spans here
for match_id, start, end in matches:
spans.append(doc[start:end])
with doc.retokenize() as retokenizer:
for span in spans:
retokenizer.merge(span)
for token in span:
token._.bad_html = True # Mark token as bad HTML
return doc
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("html_merger", last=True) # Add component to the pipeline
doc = nlp("Hello<br>world! <br/> This is a test.")
for token in doc:
print(token.text, token._.bad_html)
除了将模式硬编码到组件中,你还可以使用包含模式JSON文件,只需在构建管道时指定该文件路径。这样就可以根据你的应用在不同的模式中复用组件。当你使用nlp.add_pipe
将组件加入到管道中,并使用以下方式传递配置参数:
@Language.factory("html_merger", default_config={"path": None})
def create_bad_html_merger(nlp, name, path):
return BadHTMLMerger(nlp, path=path)
nlp.add_pipe("html_merger", config={"path": "/path/to/patterns.json"})
@Language.factory("html_merger", default_config={"path": None})
def create_bad_html_merger(nlp, name, path):
return BadHTMLMerger(nlp, path=path)
nlp.add_pipe("html_merger", config={"path": "/path/to/patterns.json"})
📖处理管道 有关如何创建自定义管道组件和扩展属性的更多详细信息和示例,请参阅使用指南。
示例:使用语言注释(linguistic annotations)
假设您正在分析用户评论,并且想要了解人们对Facebook
的看法。您想首先查找“Facebook is”
或“Facebook was”
之后的形容词。这显然是一个非常基本的解决方案,但它会很快,且是了解数据内容的好方法。您的模式可能如下所示:
[{"LOWER": "facebook"}, {"LEMMA": "be"}, {"POS": "ADV", "OP": "*"}, {"POS": "ADJ"}]
[{"LOWER": "facebook"}, {"LEMMA": "be"}, {"POS": "ADV", "OP": "*"}, {"POS": "ADJ"}]
这将匹配小写形式为“facebook
”的token
(如Facebook
、facebook
或FACEBOOK
),后跟词干为“be”
的token
(例如,is、was 或's),后跟可选副词,后跟形容词。此处使用的语言注释特别有用,因为您可以告诉 spaCy
匹配“Facebook's annoying”
,而不是“Facebook’s annoying ads”
。可选副词确保您不会错过带有增强词的形容词,例如“pretty awful”或“very nice”。
要快速概览结果,您可以收集包含匹配项的所有句子,并使用displaCy visualizer
呈现它们 。在回调函数,你就可以访问每个匹配结果的start
和end
,以及对应的Doc
。这使您可以确定包含匹配项的句子,doc[start:end].sent
,并计算句子中匹配范围的开始和结束。在displaCy
的“手动”模式下可以传入包含要呈现的文本和实体的字典列表。
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy import displacy
from spacy.matcher import Matcher
nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab)
matched_sents = [] # Collect data of matched sentences to be visualized
def collect_sents(matcher, doc, i, matches):
match_id, start, end = matches[i]
span = doc[start:end] # Matched span
sent = span.sent # Sentence containing matched span
# Append mock entity for match in displaCy style to matched_sents
# get the match span by ofsetting the start and end of the span with the
# start and end of the sentence in the doc
match_ents = [{
"start": span.start_char - sent.start_char,
"end": span.end_char - sent.start_char,
"label": "MATCH",
}]
matched_sents.append({"text": sent.text, "ents": match_ents})
pattern = [{"LOWER": "facebook"}, {"LEMMA": "be"}, {"POS": "ADV", "OP": "*"},
{"POS": "ADJ"}]
matcher.add("FacebookIs", [pattern], on_match=collect_sents) # add pattern
doc = nlp("I'd say that Facebook is evil. – Facebook is pretty cool, right?")
matches = matcher(doc)
# Serve visualization of sentences containing match with displaCy
# set manual=True to make displaCy render straight from a dictionary
# (if you're not running the code within a Jupyer environment, you can
# use displacy.serve instead)
displacy.render(matched_sents, style="ent", manual=True)
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy import displacy
from spacy.matcher import Matcher
nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab)
matched_sents = [] # Collect data of matched sentences to be visualized
def collect_sents(matcher, doc, i, matches):
match_id, start, end = matches[i]
span = doc[start:end] # Matched span
sent = span.sent # Sentence containing matched span
# Append mock entity for match in displaCy style to matched_sents
# get the match span by ofsetting the start and end of the span with the
# start and end of the sentence in the doc
match_ents = [{
"start": span.start_char - sent.start_char,
"end": span.end_char - sent.start_char,
"label": "MATCH",
}]
matched_sents.append({"text": sent.text, "ents": match_ents})
pattern = [{"LOWER": "facebook"}, {"LEMMA": "be"}, {"POS": "ADV", "OP": "*"},
{"POS": "ADJ"}]
matcher.add("FacebookIs", [pattern], on_match=collect_sents) # add pattern
doc = nlp("I'd say that Facebook is evil. – Facebook is pretty cool, right?")
matches = matcher(doc)
# Serve visualization of sentences containing match with displaCy
# set manual=True to make displaCy render straight from a dictionary
# (if you're not running the code within a Jupyer environment, you can
# use displacy.serve instead)
displacy.render(matched_sents, style="ent", manual=True)
示例:电话号码
电话号码可以有许多不同的格式,匹配它们通常很棘手。在标记化(tokenization)过程中,spaCy
将保持数字序列完整,并且仅在空格和标点符号处拆分。这意味着您的匹配模式必须关注定长的数字序列,并被特定的标点符号包围 - 取决于国家惯例。
IS_DIGIT
标志在这里没什么用,因为它没有告诉我们任何有关长度的信息。但是,您可以使用SHAPE
标志,每个标志d
代表一个数字(最多4
个数字/字符):
[{"ORTH": "("}, {"SHAPE": "ddd"}, {"ORTH": ")"}, {"SHAPE": "dddd"},
{"ORTH": "-", "OP": "?"}, {"SHAPE": "dddd"}]
[{"ORTH": "("}, {"SHAPE": "ddd"}, {"ORTH": ")"}, {"SHAPE": "dddd"},
{"ORTH": "-", "OP": "?"}, {"SHAPE": "dddd"}]
这将匹配(123) 4567 8901
或(123) 4567-8901
格式的电话号码。要同时匹配(123) 456 789
等格式,您可以添加新的模型模式,使用'ddd'
代替'dddd'
。通过对某些值进行硬编码,您只能匹配某些特定国家/地区的电话号码。例如,这是一个匹配德国国际电话格式
的最常见的 模式:
[{"ORTH": "+"}, {"ORTH": "49"}, {"ORTH": "(", "OP": "?"}, {"SHAPE": "dddd"},
{"ORTH": ")", "OP": "?"}, {"SHAPE": "dddd", "LENGTH": 6}]
[{"ORTH": "+"}, {"ORTH": "49"}, {"ORTH": "(", "OP": "?"}, {"SHAPE": "dddd"},
{"ORTH": ")", "OP": "?"}, {"SHAPE": "dddd", "LENGTH": 6}]
根据您的应用程序需要匹配的格式,创建一组规则比训练模型更好用。它将产生可预测的结果,更容易修改和扩展,并且不需要任何训练数据,只需要一组测试用例。
import spacy
from spacy.matcher import Matcher
nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab)
pattern = [{"ORTH": "("}, {"SHAPE": "ddd"}, {"ORTH": ")"}, {"SHAPE": "ddd"},
{"ORTH": "-", "OP": "?"}, {"SHAPE": "ddd"}]
matcher.add("PHONE_NUMBER", [pattern])
doc = nlp("Call me at (123) 456 789 or (123) 893 789!")
print([t.text for t in doc])
matches = matcher(doc)
for match_id, start, end in matches:
span = doc[start:end]
print(span.text)
import spacy
from spacy.matcher import Matcher
nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab)
pattern = [{"ORTH": "("}, {"SHAPE": "ddd"}, {"ORTH": ")"}, {"SHAPE": "ddd"},
{"ORTH": "-", "OP": "?"}, {"SHAPE": "ddd"}]
matcher.add("PHONE_NUMBER", [pattern])
doc = nlp("Call me at (123) 456 789 or (123) 893 789!")
print([t.text for t in doc])
matches = matcher(doc)
for match_id, start, end in matches:
span = doc[start:end]
print(span.text)
输出
['Call', 'me', 'at', '(', '123', ')', '456', '789', 'or', '(', '123', ')', '893', '789', '!']
(123) 456 789
(123) 893 789
['Call', 'me', 'at', '(', '123', ')', '456', '789', 'or', '(', '123', ')', '893', '789', '!']
(123) 456 789
(123) 893 789
示例:社交媒体上的标签和表情符号
这部分textacy
也有可以处理的方法
高效的词组匹配
如果您需要匹配大型术语列表,您仍然可以使用PhraseMatcher
并创建Doc
对象,相比于使用token
模式总体上效率更高。Doc
模式可以包含单个或多个token
。
添加短语(phrase)模式
import spacy
from spacy.matcher import PhraseMatcher
nlp = spacy.load("en_core_web_sm")
matcher = PhraseMatcher(nlp.vocab)
terms = ["Barack Obama", "Angela Merkel", "Washington, D.C."]
# Only run nlp.make_doc to speed things up
patterns = [nlp.make_doc(text) for text in terms]
matcher.add("TerminologyList", patterns)
doc = nlp("German Chancellor Angela Merkel and US President Barack Obama "
"converse in the Oval Office inside the White House in Washington, D.C.")
matches = matcher(doc)
for match_id, start, end in matches:
span = doc[start:end]
print(span.text)
import spacy
from spacy.matcher import PhraseMatcher
nlp = spacy.load("en_core_web_sm")
matcher = PhraseMatcher(nlp.vocab)
terms = ["Barack Obama", "Angela Merkel", "Washington, D.C."]
# Only run nlp.make_doc to speed things up
patterns = [nlp.make_doc(text) for text in terms]
matcher.add("TerminologyList", patterns)
doc = nlp("German Chancellor Angela Merkel and US President Barack Obama "
"converse in the Oval Office inside the White House in Washington, D.C.")
matches = matcher(doc)
for match_id, start, end in matches:
span = doc[start:end]
print(span.text)
输出
Angela Merkel
Barack Obama
Washington, D.C.
Angela Merkel
Barack Obama
Washington, D.C.
spaCy
同时处理模式和要匹配的文本,因此您不必担心特定的标记化(tokenization),例如,您可以简单地传入nlp("Washington, D.C.")
而不必编写复杂的token
模式去覆盖确切标记化(tokenization)的的术语。
!关于创建模式的重要说明 要创建模式,必须使用
nlp
对象处理每个短语。如果您加载了经过训练的管道,那么在循环或列表中执行此操作则低效且缓慢。如果您只需要标记化(tokenization)和词法属性,则可以使用nlp.make_doc
,它只会运行标记器(tokenizer)。要获得额外的速度提升,您还可以使用nlp.tokenizer.pipe
方法,它将文本作为流处理。
- patterns = [nlp(term) for term in LOTS_OF_TERMS]
+ patterns = [nlp.make_doc(term) for term in LOTS_OF_TERMS]
+ patterns = list(nlp.tokenizer.pipe(LOTS_OF_TERMS))
- patterns = [nlp(term) for term in LOTS_OF_TERMS]
+ patterns = [nlp.make_doc(term) for term in LOTS_OF_TERMS]
+ patterns = list(nlp.tokenizer.pipe(LOTS_OF_TERMS))
匹配其他令牌属性
这里会介绍几种匹配属性通过attr
参数进行传递,如:attr="LOWER"
,attr="SHAPE"
等。
默认情况下,PhraseMatcher
将逐字匹配token
文本,例如Token.text
。通过在初始化时设置attr
参数,您可以更改匹配时应使用的token属性。例如,使用属性LOWER
可以匹配Token.lower
并创建不区分大小写的匹配模式:
from spacy.lang.en import English
from spacy.matcher import PhraseMatcher
nlp = English()
matcher = PhraseMatcher(nlp.vocab, attr="LOWER")
patterns = [nlp.make_doc(name) for name in ["Angela Merkel", "Barack Obama"]]
matcher.add("Names", patterns)
doc = nlp("angela merkel and us president barack Obama")
for match_id, start, end in matcher(doc):
print("Matched based on lowercase token text:", doc[start:end])
from spacy.lang.en import English
from spacy.matcher import PhraseMatcher
nlp = English()
matcher = PhraseMatcher(nlp.vocab, attr="LOWER")
patterns = [nlp.make_doc(name) for name in ["Angela Merkel", "Barack Obama"]]
matcher.add("Names", patterns)
doc = nlp("angela merkel and us president barack Obama")
for match_id, start, end in matcher(doc):
print("Matched based on lowercase token text:", doc[start:end])
结果
Matched based on lowercase token text: angela merkel
Matched based on lowercase token text: barack Obama
Matched based on lowercase token text: angela merkel
Matched based on lowercase token text: barack Obama
!关于创建模式的重要说明 这里的例子使用
nlp.make_doc
方法在不运行任何其他管道组件的情况下尽可能高效地创建Doc
对象模式。如果要匹配的token
属性由管道组件设置,请确保在创建模式时管道组件是运行状态。例如,要匹配POS
或者LEMMA
,模式Doc
对象需要具有由tagger
或morphologizer
设置的词性标签。您可以在模式文本上调用nlp
对象来取代nlp.make_doc
,或使用nlp.select_pipes
有选择地禁用组件。
另一个可能的用例是根据形状匹配数字标记,例如IP
地址。这意味着您不必担心这些字符串将如何被标记化(tokenized),并且您将能够根据少量示例找到token和token组合。在这里,我们在形状ddd.d.d.d
和ddd.ddd.d.d
上进行匹配:
from spacy.lang.en import English
from spacy.matcher import PhraseMatcher
nlp = English()
matcher = PhraseMatcher(nlp.vocab, attr="SHAPE")
matcher.add("IP", [nlp("127.0.0.1"), nlp("127.127.0.0")])
doc = nlp("Often the router will have an IP address such as 192.12.1.1 or 192.168.2.1.")
for match_id, start, end in matcher(doc):
print("Matched based on token shape:", doc[start:end])
from spacy.lang.en import English
from spacy.matcher import PhraseMatcher
nlp = English()
matcher = PhraseMatcher(nlp.vocab, attr="SHAPE")
matcher.add("IP", [nlp("127.0.0.1"), nlp("127.127.0.0")])
doc = nlp("Often the router will have an IP address such as 192.12.1.1 or 192.168.2.1.")
for match_id, start, end in matcher(doc):
print("Matched based on token shape:", doc[start:end])
结果,可以看到这里的192.12.1.1
没有匹配到
Matched based on token shape: 192.168.2.1
Matched based on token shape: 192.168.2.1
理论上,这同样适用于像POS
属性。 例如,nlp("I like cats")
基于其词性标签匹配的模式将返回匹配“I love dogs”。您还可以匹配布尔标志,例如IS_PUNCT
匹配具有与模式相同的标点符号和非标点符号序列的短语。但这很容易让人感到困惑,并且与编写一两个token
模式相比没有太大优势。
依赖匹配器 V 3.0需求模型
DependencyMatcher
允许您使用Semgrex
运算符解析依赖项匹配模式。它需要一个包含解析器(parser)的模型,例如 DependencyParser
。Matcher
模式中DependencyMatcher
模式在依赖项解析中匹配tokens并指定它们之间的关系,而不是定义一个相邻token
的列表。
from spacy.matcher import DependencyMatcher
# "[subject] ... initially founded"
pattern = [
# anchor token: founded
{
"RIGHT_ID": "founded",
"RIGHT_ATTRS": {"ORTH": "founded"}
},
# founded -> subject
{
"LEFT_ID": "founded",
"REL_OP": ">",
"RIGHT_ID": "subject",
"RIGHT_ATTRS": {"DEP": "nsubj"}
},
# "founded" follows "initially"
{
"LEFT_ID": "founded",
"REL_OP": ";",
"RIGHT_ID": "initially",
"RIGHT_ATTRS": {"ORTH": "initially"}
}
]
matcher = DependencyMatcher(nlp.vocab)
matcher.add("FOUNDED", [pattern])
matches = matcher(doc)
from spacy.matcher import DependencyMatcher
# "[subject] ... initially founded"
pattern = [
# anchor token: founded
{
"RIGHT_ID": "founded",
"RIGHT_ATTRS": {"ORTH": "founded"}
},
# founded -> subject
{
"LEFT_ID": "founded",
"REL_OP": ">",
"RIGHT_ID": "subject",
"RIGHT_ATTRS": {"DEP": "nsubj"}
},
# "founded" follows "initially"
{
"LEFT_ID": "founded",
"REL_OP": ";",
"RIGHT_ID": "initially",
"RIGHT_ATTRS": {"ORTH": "initially"}
}
]
matcher = DependencyMatcher(nlp.vocab)
matcher.add("FOUNDED", [pattern])
matches = matcher(doc)
添加到依赖匹配器的模式由字典列表组成,每个字典描述要匹配的token及其 与模式中现有token的关系。除了第一个字典,只使用RIGHT_ID
和RIGHT_ATTRS
定义一个锚token
,其他模式都应该有以下键:
名称 | 描述 |
---|---|
LEFT_ID | 关系中左侧节点的名称,已在较早的节点中定义。类型:str |
REL_OP | 描述两个节点关系的运算符。类型:str |
RIGHT_ID | 关系中右侧节点的唯一名称。类型:str |
RIGHT_ATTRS | 要匹配右侧节点的token 属性,格式与基于常规token 的Matcher 提供相同的模式。类型:Dict[str, Any] |
添加到模式的每个附加token
都通过关系REL_OP
链接到现有token``LEFT_ID
。新token
被赋予名称RIGHT_ID
并由属性RIGHT_ATTRS
进行描述。
!重要的提示
因为唯一标记名称LEFT_ID
和RIGHT_ID
用于标识token
,所以模式中字典的顺序很重要:token名称需要在模式中的一个字典中定义LEFT_ID
,然后才能在另一个字典中作为RIGHT_ID
使用。
依赖匹配运算符
DependencyMatcher
支持以下运算符,其中大部分运算符直接来自Semgrex
:
标记(SYMBOL) | 描述 |
---|---|
A < B | A是B的直接依赖项。 |
A > B | A是B的直接负责人。 |
A << B | A是链中依赖B,跟随dep → head 路径的依赖项。 |
A >> B | A是B跟随 head → dep 路径的链中的头部。 |
A . B | A紧接在B之前,例如A.i == B.i - 1,并且两者都在同一个依赖树中。 |
A .* B | A在B之前,即A.i < B.i,两者都在同一个依赖树中(不在 Semgrex 中)。 |
A ; B | A紧随B后,即A.i == B.i + 1,两者都在同一个依赖树中(不在 Semgrex 中)。 |
A ;* B | A跟随B,即A.i > B.i,并且两者都在同一个依赖树中(不在 Semgrex 中)。 |
A $+ B | B是A的右直接兄弟,即A和B具有相同的父级 和A.i == B.i - 1。 |
A $- B | B是A 的左直接兄弟,即A和B具有相同的父级和A.i == B.i + 1。 |
A $++ B | B是A的右兄弟,即A和B具有相同的父级 和A.i < B.i。 |
A $-- B | B是A的左兄弟,即A和B具有相同的父级和A.i > B.i。 |
设计依赖匹配模式
假设我们想找到描述谁创立了什么样的公司的句子:
- 史密斯于 2005 年创立了一家医疗保健公司。
- 威廉姆斯最初于 1987 年创立了一家保险公司。
- Lee 是一位经验丰富的 CEO,他创立了两家 AI 初创公司。
“Smith founded a healthcare company”的依赖解析显示了我们想要匹配的关系类型和tokens
:
可视化解析 displacy可视化器允许您呈现Doc对象及其依赖项解析和词性标签:
import spacy
from spacy import displacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Smith founded a healthcare company")
displacy.serve(doc)
import spacy
from spacy import displacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Smith founded a healthcare company")
displacy.serve(doc)
我们感兴趣的关系是:
- 创始人是带有文本
founded
的token的主语(nsubj
) - 公司是founded的谓语(
dobj
) - 公司类型可以是形容词(
amod
,上面未显示)或复合词(compound
)
第一步是为模式选择一个锚token
。因为它是依赖解析的根,这里founded
是一个不错的选择。当所有依赖关系运算符都从头指向子节点时,通常更容易构造模式。在这个例子中,我们将只使用>
,它将一个头连接到一个直接依赖,例如head > child
。
最简单的依赖匹配器模式识别并命名树中的单个token
:
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy.matcher import DependencyMatcher
nlp = spacy.load("en_core_web_sm")
matcher = DependencyMatcher(nlp.vocab)
pattern = [
{
"RIGHT_ID": "anchor_founded", # unique name
"RIGHT_ATTRS": {"ORTH": "founded"} # token pattern for "founded"
}
]
matcher.add("FOUNDED", [pattern])
doc = nlp("Smith founded two companies.")
matches = matcher(doc)
print(matches) # [(4851363122962674176, [1])]
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy.matcher import DependencyMatcher
nlp = spacy.load("en_core_web_sm")
matcher = DependencyMatcher(nlp.vocab)
pattern = [
{
"RIGHT_ID": "anchor_founded", # unique name
"RIGHT_ATTRS": {"ORTH": "founded"} # token pattern for "founded"
}
]
matcher.add("FOUNDED", [pattern])
doc = nlp("Smith founded two companies.")
matches = matcher(doc)
print(matches) # [(4851363122962674176, [1])]
现在我们有了一个命名的锚标记(anchor_founded
),我们可以将创始人(founder)添加为具有依赖标签的founded
直接依赖( >):nsubj
步骤 1
pattern = [
{
"RIGHT_ID": "anchor_founded",
"RIGHT_ATTRS": {"ORTH": "founded"}
},
{
"LEFT_ID": "anchor_founded",
"REL_OP": ">",
"RIGHT_ID": "founded_subject",
"RIGHT_ATTRS": {"DEP": "nsubj"},
}
# ...
]
pattern = [
{
"RIGHT_ID": "anchor_founded",
"RIGHT_ATTRS": {"ORTH": "founded"}
},
{
"LEFT_ID": "anchor_founded",
"REL_OP": ">",
"RIGHT_ID": "founded_subject",
"RIGHT_ATTRS": {"DEP": "nsubj"},
}
# ...
]
以相同的方式添加直接对象 (dobj) :
步骤 2
pattern = [
#...
{
"LEFT_ID": "anchor_founded",
"REL_OP": ">",
"RIGHT_ID": "founded_object",
"RIGHT_ATTRS": {"DEP": "dobj"},
}
# ...
]
pattern = [
#...
{
"LEFT_ID": "anchor_founded",
"REL_OP": ">",
"RIGHT_ID": "founded_object",
"RIGHT_ATTRS": {"DEP": "dobj"},
}
# ...
]
添加主语(subject)和谓语(object)tokens后,他们在RIGHT_ID
键中就有了名称,这个名称可以是任何唯一的字符串,例如founded_subject
。然后这些名称可以作为LEFT_ID
使用,链接新的tokens到模式中。模式的最后一部分,我们将指定founded_object
token应该有一个修饰器(modifier),修饰器依赖关系amod
或compound
:
步骤 3
pattern = [
# ...
{
"LEFT_ID": "founded_object",
"REL_OP": ">",
"RIGHT_ID": "founded_object_modifier",
"RIGHT_ATTRS": {"DEP": {"IN": ["amod", "compound"]}},
}
]
pattern = [
# ...
{
"LEFT_ID": "founded_object",
"REL_OP": ">",
"RIGHT_ID": "founded_object_modifier",
"RIGHT_ATTRS": {"DEP": {"IN": ["amod", "compound"]}},
}
]
您可以将创建依赖匹配器模式的过程想象为一个链条, 首先在左侧定义一个锚toke通过使用关系运算符在右侧一个接一个地链接token来构建模式。要创建有效(valid)模式,每个新token都需要链接到其左侧的现有标记。这个例子中的founded
,一个token可以链接到其右侧的多个令牌:
完整的模式组合在一起,如下例所示:
import spacy
from spacy.matcher import DependencyMatcher
nlp = spacy.load("en_core_web_sm")
matcher = DependencyMatcher(nlp.vocab)
pattern = [
{
"RIGHT_ID": "anchor_founded",
"RIGHT_ATTRS": {"ORTH": "founded"}
},
{
"LEFT_ID": "anchor_founded",
"REL_OP": ">",
"RIGHT_ID": "founded_subject",
"RIGHT_ATTRS": {"DEP": "nsubj"},
},
{
"LEFT_ID": "anchor_founded",
"REL_OP": ">",
"RIGHT_ID": "founded_object",
"RIGHT_ATTRS": {"DEP": "dobj"},
},
{
"LEFT_ID": "founded_object",
"REL_OP": ">",
"RIGHT_ID": "founded_object_modifier",
"RIGHT_ATTRS": {"DEP": {"IN": ["amod", "compound"]}},
}
]
matcher.add("FOUNDED", [pattern])
doc = nlp("Lee, an experienced CEO, has founded two AI startups.")
matches = matcher(doc)
print(matches) # [(4851363122962674176, [6, 0, 10, 9])]
# Each token_id corresponds to one pattern dict
match_id, token_ids = matches[0]
for i in range(len(token_ids)):
print(pattern[i]["RIGHT_ID"] + ":", doc[token_ids[i]].text)
import spacy
from spacy.matcher import DependencyMatcher
nlp = spacy.load("en_core_web_sm")
matcher = DependencyMatcher(nlp.vocab)
pattern = [
{
"RIGHT_ID": "anchor_founded",
"RIGHT_ATTRS": {"ORTH": "founded"}
},
{
"LEFT_ID": "anchor_founded",
"REL_OP": ">",
"RIGHT_ID": "founded_subject",
"RIGHT_ATTRS": {"DEP": "nsubj"},
},
{
"LEFT_ID": "anchor_founded",
"REL_OP": ">",
"RIGHT_ID": "founded_object",
"RIGHT_ATTRS": {"DEP": "dobj"},
},
{
"LEFT_ID": "founded_object",
"REL_OP": ">",
"RIGHT_ID": "founded_object_modifier",
"RIGHT_ATTRS": {"DEP": {"IN": ["amod", "compound"]}},
}
]
matcher.add("FOUNDED", [pattern])
doc = nlp("Lee, an experienced CEO, has founded two AI startups.")
matches = matcher(doc)
print(matches) # [(4851363122962674176, [6, 0, 10, 9])]
# Each token_id corresponds to one pattern dict
match_id, token_ids = matches[0]
for i in range(len(token_ids)):
print(pattern[i]["RIGHT_ID"] + ":", doc[token_ids[i]].text)
! ! 关于速度的重要说明 依赖匹配器(dependency matcher)可能会变慢,当token模式可以潜在的匹配句子中的很多tokens或者关系运算符允许依赖解析中的长路径时,例如
<<
、>>
、.*
、;*.
。 要提高匹配器速度,请尝试使您的令牌模式和运算符尽可能具体。例如,如果可能的话,使用>
代替>>
并使用包含依赖标签和其他token属性的token模式,而不要使用{}
匹配句子中任意token。
Entity Ruler 基于规则的实体识别
基于规则的实体识别
EntityRuler
(实体规则)组件,让你可以基于模式字典添加命名实体,从而可以轻松地将基于规则的命名实体识别和统计命名实体识别结合起来,以使管道更加强大。
实体模式
实体模式的字典具有两个键:其中"label"
指定了实体匹配成功后分配的标签; "pattern"
指定匹配模式。实体规则接受两种类型的模式:
- 短语模式用于精确字符串匹配
{"label": "ORG", "pattern": "Apple"}
{"label": "ORG", "pattern": "Apple"}
- Token模式
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}]}
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}]}
使用实体规则
EntityRuler
是一个管道组件,通常通过nlp.add_pipe
方法添加。当你在一个文本对象上调用nlp
对象时,它将找到doc
中的匹配项,使用指定的模式标签作为实体标签,并将他们作为实体添加到doc.ents
中。如果有匹配重叠,那么模式匹配将优先匹配尽量多的token。如果他们恰巧又长度相同,则选择在Doc
中最早出现的匹配作为结果。
# spacy v3.0 python 3
from spacy.lang.en import English
nlp = English()
ruler = nlp.add_pipe("entity_ruler")
patterns = [{"label": "ORG", "pattern": "Apple"},
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}]}]
ruler.add_patterns(patterns)
doc = nlp("Apple is opening its first big office in San Francisco.")
print([(ent.text, ent.label_) for ent in doc.ents])
# spacy v3.0 python 3
from spacy.lang.en import English
nlp = English()
ruler = nlp.add_pipe("entity_ruler")
patterns = [{"label": "ORG", "pattern": "Apple"},
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}]}]
ruler.add_patterns(patterns)
doc = nlp("Apple is opening its first big office in San Francisco.")
print([(ent.text, ent.label_) for ent in doc.ents])
结果
[('Apple', 'ORG'), ('San Francisco', 'GPE')]
[('Apple', 'ORG'), ('San Francisco', 'GPE')]
实体规则旨在通过与现用的管道组件集成来增强命名实体识别器。如果在ner
组件之前添加,则实体识别器将基于现有的实体跨度并围绕它调整预测。很多情况下,这都可以显著的提升预测准确性。如果在ner
组件之后添加,如果他们在现有模型预测的实体没有重叠,则实体规则只会在doc.ents
添加实体跨度(spans)。你可以在初始化时设置overwrite_ents=True
来覆盖重叠实体。
# spacy v3.0 python 3
import spacy
nlp = spacy.load("en_core_web_sm")
ruler = nlp.add_pipe("entity_ruler")
patterns = [{"label": "ORG", "pattern": "MyCorp Inc."}]
ruler.add_patterns(patterns)
doc = nlp("MyCorp Inc. is a company in the U.S.")
print([(ent.text, ent.label_) for ent in doc.ents])
# spacy v3.0 python 3
import spacy
nlp = spacy.load("en_core_web_sm")
ruler = nlp.add_pipe("entity_ruler")
patterns = [{"label": "ORG", "pattern": "MyCorp Inc."}]
ruler.add_patterns(patterns)
doc = nlp("MyCorp Inc. is a company in the U.S.")
print([(ent.text, ent.label_) for ent in doc.ents])
验证和调试EntityRuler
模式
实体规则通过设置"validate"
来验证模型,具体参见验证和调试模式
ruler = nlp.add_pipe("entity_ruler", config={"validate": True})
ruler = nlp.add_pipe("entity_ruler", config={"validate": True})
给模式添加 ID
EntityRuler
可以为每个模式添加id
属性。使用id
属性后就允许将多个模式关联到同一实体。
# spacy v3.0 python 3
from spacy.lang.en import English
nlp = English()
ruler = nlp.add_pipe("entity_ruler")
patterns = [{"label": "ORG", "pattern": "Apple", "id": "apple"},
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}], "id": "san-francisco"},
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "fran"}], "id": "san-francisco"}]
ruler.add_patterns(patterns)
doc1 = nlp("Apple is opening its first big office in San Francisco.")
print([(ent.text, ent.label_, ent.ent_id_) for ent in doc1.ents])
doc2 = nlp("Apple is opening its first big office in San Fran.")
print([(ent.text, ent.label_, ent.ent_id_) for ent in doc2.ents])
# spacy v3.0 python 3
from spacy.lang.en import English
nlp = English()
ruler = nlp.add_pipe("entity_ruler")
patterns = [{"label": "ORG", "pattern": "Apple", "id": "apple"},
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}], "id": "san-francisco"},
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "fran"}], "id": "san-francisco"}]
ruler.add_patterns(patterns)
doc1 = nlp("Apple is opening its first big office in San Francisco.")
print([(ent.text, ent.label_, ent.ent_id_) for ent in doc1.ents])
doc2 = nlp("Apple is opening its first big office in San Fran.")
print([(ent.text, ent.label_, ent.ent_id_) for ent in doc2.ents])
结果
[('Apple', 'ORG', 'apple'), ('San Francisco', 'GPE', 'san-francisco')]
[('Apple', 'ORG', 'apple'), ('San Fran', 'GPE', 'san-francisco')]
[('Apple', 'ORG', 'apple'), ('San Francisco', 'GPE', 'san-francisco')]
[('Apple', 'ORG', 'apple'), ('San Fran', 'GPE', 'san-francisco')]
如果该id
属性包含在EntityRuler
模式中,ent_id_
属性将被设置为匹配到的实体所设置的id
。因此,在上面的示例中,很容易识别出“San Francisco”
和“San Fran”
是同一个实体。
使用模式文件
你可用使用to_disk
和from_disk
方法把模式保存到JSONL
文件并从JSONL
文件中加载模式,文件中每行包含一个模式对象。PATTERNS.JSONL
文件格式如下:
{"label": "ORG", "pattern": "Apple"}
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}]}
{"label": "ORG", "pattern": "Apple"}
{"label": "GPE", "pattern": [{"LOWER": "san"}, {"LOWER": "francisco"}]}
保存、加载模式
ruler.to_disk("./patterns.jsonl")
new_ruler = nlp.add_pipe("entity_ruler").from_disk("./patterns.jsonl")
ruler.to_disk("./patterns.jsonl")
new_ruler = nlp.add_pipe("entity_ruler").from_disk("./patterns.jsonl")
与 Prodigy 集成 如果您使用
Prodigy
注释工具,您可能会通过引导命名实体和文本分类标签来识别这些模式文件。EntityRuler
的模式遵循相同的语法,因此您可以在spaCy
中使用现有的Prodigy
模式文件,反之亦然。
当您保存一个nlp
对象时,而管中添加了EntityRuler
,其模式(patterns)会自动导出到管道目录中(此时导出的是一个包含管道其他内容的文件夹,其中不止包含实体规则模式)。
nlp = spacy.load("en_core_web_sm")
ruler = nlp.add_pipe("entity_ruler")
ruler.add_patterns([{"label": "ORG", "pattern": "Apple"}])
nlp.to_disk("/path/to/pipeline")
nlp = spacy.load("en_core_web_sm")
ruler = nlp.add_pipe("entity_ruler")
ruler.add_patterns([{"label": "ORG", "pattern": "Apple"}])
nlp.to_disk("/path/to/pipeline")
保存的管道在config.cfg
中包含了"entity_ruler"
并且管道目录包含一个entityruler.jsonl
文件,这就是模式文件。当您重新加载管道时,所有管道组件都将被恢复和反序列化-包括实体规则。这使你可以发布包含二进制权重和规则的管道包。
使用大量的短语模式
当使用大量短语模式(大约 > 10000)时,了解add_patterns
实体规则是如何工作的就变的很重要。对于每个短语模式(phrase pattern),如果您尝试在现有管道的末尾添加EntityRuler
,那么EntityRuler
将调用nlp
对象来构造一个doc
对象。例如,现在有一个POS
标记器(tagger),我们希望能够准确匹配基于POS签名模式。在这种情况下,您需要为实体规则设置"phrase_matcher_attr": "POS"
。
在大型列表中运行完整的语言管道时,大量短语模式可能需要更长的时间。从spaCy v2.2.4
开始add_patterns
函数已被重构为在所有短语模式上使用nlp.pipe
,从而分别以5,000-100,000
个短语模式实现约10-20
倍的加速。但即使有了这种加速(但特别是如果您使用的是旧版本),add_patterns
功能仍可能需要很长时间。一个简单的解决办法是在添加短语模式时禁用其他语言管道。
ruler = nlp.add_pipe("entity_ruler")
patterns = [{"label": "TEST", "pattern": str(i)} for i in range(100000)]
with nlp.select_pipes(enable="tagger"):
ruler.add_patterns(patterns)
ruler = nlp.add_pipe("entity_ruler")
patterns = [{"label": "TEST", "pattern": str(i)} for i in range(100000)]
with nlp.select_pipes(enable="tagger"):
ruler.add_patterns(patterns)
规则与模型结合
您可以通过多种方式组合统计和基于规则的组件。通过为特定标记预设标签、实体或句子边界,基于规则的组件可用于提高统计模型的准确性。统计模型通常会尊重这些预设注释,这有时会提高其他决策的准确性。您还可以在统计模型之后使用基于规则的组件来纠正常见错误。最后,基于规则的组件可以引用统计模型设置的属性,以实现更抽象的逻辑。
示例:扩展命名实体
当使用已训练的命名实体识别模型从文本中提取信息时,您可能会发现预测的span
仅包含了您正在寻找的实体的一部分。这通常出现在统计模型错误地预测实体时。其他情况下,如果在原始训练语料库中定义实体类型的方式与您的应用程序所需的不匹配,就会发生这种情况。
语料库来自哪里 用于从头开始训练管道的语料库通常来自学术界。它们包含各种来源的文本,具有由人工注释者手动标记的语言特征(遵循一组特定的准则)。然后将语料库与评估数据一起分发,因此其他研究人员可以对他们的算法进行基准(benchmark)测试,每个人都可以报告该语料数据的数字。然而,大多数应用程序需要在没有任何语料信息的情况下进行学习。
例如,spaCy
的English pipelines
语料库将PERSON
实体定义为仅包含人名,而没有像“Mr.” or “Dr“这样的标题。这是有道理的,因为它更容易将实体类型解析回知识库。但是,如果您的应用程序需要包含title
的全名,该怎么办?
# spaCy v3.0 · Python 3 · via Binder
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Dr. Alex Smith chaired first board meeting of Acme Corp Inc.")
print([(ent.text, ent.label_) for ent in doc.ents])
# spaCy v3.0 · Python 3 · via Binder
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Dr. Alex Smith chaired first board meeting of Acme Corp Inc.")
print([(ent.text, ent.label_) for ent in doc.ents])
结果
[('Alex Smith', 'PERSON'), ('first', 'ORDINAL'), ('Acme Corp Inc.', 'ORG')]
[('Alex Smith', 'PERSON'), ('first', 'ORDINAL'), ('Acme Corp Inc.', 'ORG')]
虽然您可以尝试通过更多包含title
的span
示例来更新模型,这样来教导模型重新定义PERSON
实体,但这可能不是最有效的方法。现有模型训练了超过200
万个单词,因此为了彻底改变实体类型的定义,您可能需要大量训练示例。但是,如果您已经拥有预测的实体,则可以使用基于规则的方法来检查它们是否带有title
,如果有,则将实体span
扩大一个token
。毕竟,此示例中的所有title
的共同点是,如果它们出现,它们就会会出现在person
实体之前的前一个token
中。
from spacy.language import Language
from spacy.tokens import Span
@Language.component("expand_person_entities")
def expand_person_entities(doc):
new_ents = []
for ent in doc.ents:
# 检查token为PERSON且不是第一个token(第一个意味着前面没有其他token了)
if ent.label_ == "PERSON" and ent.start != 0:
prev_token = doc[ent.start - 1]
if prev_token.text in ("Dr", "Dr.", "Mr", "Mr.", "Ms", "Ms."):
new_ent = Span(doc, ent.start - 1, ent.end, label=ent.label)
new_ents.append(new_ent)
else:
new_ents.append(ent)
else:
new_ents.append(ent)
doc.ents = new_ents
return doc
from spacy.language import Language
from spacy.tokens import Span
@Language.component("expand_person_entities")
def expand_person_entities(doc):
new_ents = []
for ent in doc.ents:
# 检查token为PERSON且不是第一个token(第一个意味着前面没有其他token了)
if ent.label_ == "PERSON" and ent.start != 0:
prev_token = doc[ent.start - 1]
if prev_token.text in ("Dr", "Dr.", "Mr", "Mr.", "Ms", "Ms."):
new_ent = Span(doc, ent.start - 1, ent.end, label=ent.label)
new_ents.append(new_ent)
else:
new_ents.append(ent)
else:
new_ents.append(ent)
doc.ents = new_ents
return doc
上面的函数接受一个Doc
对象,修改doc.ents
并返回Doc
对象。 使用@Language.component
装饰器,我们可以将其注册为管道组件,以便在处理文本时自动运行。我们可以用nlp.add_pipe
将其添加到当前管道中。
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy.language import Language
from spacy.tokens import Span
nlp = spacy.load("en_core_web_sm")
@Language.component("expand_person_entities")
def expand_person_entities(doc):
new_ents = []
for ent in doc.ents:
if ent.label_ == "PERSON" and ent.start != 0:
prev_token = doc[ent.start - 1]
if prev_token.text in ("Dr", "Dr.", "Mr", "Mr.", "Ms", "Ms."):
new_ent = Span(doc, ent.start - 1, ent.end, label=ent.label)
new_ents.append(new_ent)
else:
new_ents.append(ent)
doc.ents = new_ents
return doc
# 在ner组件之后添加该组件
nlp.add_pipe("expand_person_entities", after="ner")
doc = nlp("Dr. Alex Smith chaired first board meeting of Acme Corp Inc.")
print([(ent.text, ent.label_) for ent in doc.ents])
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy.language import Language
from spacy.tokens import Span
nlp = spacy.load("en_core_web_sm")
@Language.component("expand_person_entities")
def expand_person_entities(doc):
new_ents = []
for ent in doc.ents:
if ent.label_ == "PERSON" and ent.start != 0:
prev_token = doc[ent.start - 1]
if prev_token.text in ("Dr", "Dr.", "Mr", "Mr.", "Ms", "Ms."):
new_ent = Span(doc, ent.start - 1, ent.end, label=ent.label)
new_ents.append(new_ent)
else:
new_ents.append(ent)
doc.ents = new_ents
return doc
# 在ner组件之后添加该组件
nlp.add_pipe("expand_person_entities", after="ner")
doc = nlp("Dr. Alex Smith chaired first board meeting of Acme Corp Inc.")
print([(ent.text, ent.label_) for ent in doc.ents])
另一种方法是使用扩展属性, 例如._.person_title
并将其添加到Span
对象(doc.ents
中包含实体跨度)。这里的优点是实体文本保持不变,仍然可以用于在知识库中查找名称。以下函数接受一个Span
对象,如果是PERSON
实体则检查前一个token
,如果找到title
则返回title
。Span.doc
属性使我们可以轻松访问span
的父文档。
def get_person_title(span):
if span.label_ == "PERSON" and span.start != 0:
prev_token = span.doc[span.start - 1]
if prev_token.text in ("Dr", "Dr.", "Mr", "Mr.", "Ms", "Ms."):
return prev_token.text
def get_person_title(span):
if span.label_ == "PERSON" and span.start != 0:
prev_token = span.doc[span.start - 1]
if prev_token.text in ("Dr", "Dr.", "Mr", "Mr.", "Ms", "Ms."):
return prev_token.text
现在我们可以使用Span.set_extension
方法添加自定义扩展属性"person_title"
,使用get_person_title
作为getter
函数。
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy.tokens import Span
nlp = spacy.load("en_core_web_sm")
def get_person_title(span):
if span.label_ == "PERSON" and span.start != 0:
prev_token = span.doc[span.start - 1]
if prev_token.text in ("Dr", "Dr.", "Mr", "Mr.", "Ms", "Ms."):
return prev_token.text
# 注册'person_title'作为span 扩展
Span.set_extension("person_title", getter=get_person_title)
doc = nlp("Dr Alex Smith chaired first board meeting of Acme Corp Inc.")
print([(ent.text, ent.label_, ent._.person_title) for ent in doc.ents])
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy.tokens import Span
nlp = spacy.load("en_core_web_sm")
def get_person_title(span):
if span.label_ == "PERSON" and span.start != 0:
prev_token = span.doc[span.start - 1]
if prev_token.text in ("Dr", "Dr.", "Mr", "Mr.", "Ms", "Ms."):
return prev_token.text
# 注册'person_title'作为span 扩展
Span.set_extension("person_title", getter=get_person_title)
doc = nlp("Dr Alex Smith chaired first board meeting of Acme Corp Inc.")
print([(ent.text, ent.label_, ent._.person_title) for ent in doc.ents])
结果,‘Alex Smith'
有一个 'Dr'
title,这样我们在在处理文本时可以对其进行一定的拼接,例如Dr Alex Smith
。
[('Alex Smith', 'PERSON', 'Dr'), ('first', 'ORDINAL', None), ('Acme Corp Inc.', 'ORG', None)]
[('Alex Smith', 'PERSON', 'Dr'), ('first', 'ORDINAL', None), ('Acme Corp Inc.', 'ORG', None)]
示例:使用实体、词性标记和依赖解析
语言特征 这个例子广泛使用了词性标注、依赖属性以及相关的
Doc
,Token
和Span
方法。有关这方面的介绍,请参阅语言特征指南。有关标签(labels)方案的详细信息,另请参阅模型目录。
假设您要解析专业传记并提取人名和公司名称,区分该公司是当前工作的公司,还是以前工作的公司。一种方法是尝试训练一个命名实体识别器来预测CURRENT_ORG
和PREVIOUS_ORG
但这种区别非常细微,实体识别器可能难以学习。本质上“Acme Corp Inc.”
无关“当前”或“以前”。
但是,句子的句法有一些非常重要的线索:我们可以检查诸如“work”之类的触发词,它们是过去时还是现在时,是否附有公司名称以及人是否是主语(subject)。所有这些信息都在词性标注和依赖解析中可用。
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Alex Smith worked at Acme Corp Inc.")
print([(ent.text, ent.label_) for ent in doc.ents])
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Alex Smith worked at Acme Corp Inc.")
print([(ent.text, ent.label_) for ent in doc.ents])
结果
[('Alex Smith', 'PERSON'), ('Acme Corp Inc.', 'ORG')]
[('Alex Smith', 'PERSON'), ('Acme Corp Inc.', 'ORG')]
nsubj: 名义主题。
prep: 介词。
pobj: 介词宾语。
NNP: 专有名词,单数。
VBD: 动词,过去式。
IN: 连词、从属或介词。
nsubj: 名义主题。
prep: 介词。
pobj: 介词宾语。
NNP: 专有名词,单数。
VBD: 动词,过去式。
IN: 连词、从属或介词。
spacy.displacy
可视化options={'fine_grained': True}
以输出细粒度的词性标签,即Token.tag_
在这个例子中,“worked”是句子的根,是一个过去时态动词。它的主语是工作的人“Alex Smith”。“at Acme Corp Inc.” 是一个附加在动词“工作”上的介词短语。为了提取这种关系,我们可以首先查看预测的PERSON
实体,找到它们的头部并检查它们是否附加到诸如“work”之类的触发词上。接下来,我们可以检查附加到头部的介词短语以及它们是否包含ORG
实体。最后,要确定公司隶属关系是否是当前的(current),我们可以检查头部的词性标注。
person_entities = [ent for ent in doc.ents if ent.label_ == "PERSON"]
for ent in person_entities:
# Because the entity is a span, we need to use its root token. The head
# is the syntactic governor of the person, e.g. the verb
head = ent.root.head
if head.lemma_ == "work":
# Check if the children contain a preposition
preps = [token for token in head.children if token.dep_ == "prep"]
for prep in preps:
# Check if tokens part of ORG entities are in the preposition's
# children, e.g. at -> Acme Corp Inc.
orgs = [token for token in prep.children if token.ent_type_ == "ORG"]
# If the verb is in past tense, the company was a previous company
print({"person": ent, "orgs": orgs, "past": head.tag_ == "VBD"})
person_entities = [ent for ent in doc.ents if ent.label_ == "PERSON"]
for ent in person_entities:
# Because the entity is a span, we need to use its root token. The head
# is the syntactic governor of the person, e.g. the verb
head = ent.root.head
if head.lemma_ == "work":
# Check if the children contain a preposition
preps = [token for token in head.children if token.dep_ == "prep"]
for prep in preps:
# Check if tokens part of ORG entities are in the preposition's
# children, e.g. at -> Acme Corp Inc.
orgs = [token for token in prep.children if token.ent_type_ == "ORG"]
# If the verb is in past tense, the company was a previous company
print({"person": ent, "orgs": orgs, "past": head.tag_ == "VBD"})
要在处理文本时自动应用此逻辑,我们可以将其作为自定义管道组件添加到nlp
对象中。上述逻辑还期望将实体合并为单个tokens
。spaCy
附带一个方便的内置程序merge_entities
来处理这个问题。除了打印结果之外,您还可以将其写入 实体上的自定义属性Span
——例如._.orgs
、._.prev_orgs
或者._.current_orgs
。
合并实体
# 内部实现逻辑,实体使用‘Doc.retokenize(https://spacy.io/api/doc#retokenize)上下文管理器:
with doc.retokenize() as retokenize:
for ent in doc.ents:
retokenizer.merge(ent)
# 内部实现逻辑,实体使用‘Doc.retokenize(https://spacy.io/api/doc#retokenize)上下文管理器:
with doc.retokenize() as retokenize:
for ent in doc.ents:
retokenizer.merge(ent)
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy.language import Language
from spacy import displacy
nlp = spacy.load("en_core_web_sm")
@Language.component("extract_person_orgs")
def extract_person_orgs(doc):
person_entities = [ent for ent in doc.ents if ent.label_ == "PERSON"]
for ent in person_entities:
head = ent.root.head
if head.lemma_ == "work":
preps = [token for token in head.children if token.dep_ == "prep"]
for prep in preps:
orgs = [token for token in prep.children if token.ent_type_ == "ORG"]
print({'person': ent, 'orgs': orgs, 'past': head.tag_ == "VBD"})
return doc
# To make the entities easier to work with, we'll merge them into single tokens
nlp.add_pipe("merge_entities")
nlp.add_pipe("extract_person_orgs")
doc = nlp("Alex Smith worked at Acme Corp Inc.")
# If you're not in a Jupyter / IPython environment, use displacy.serve
displacy.render(doc, options={"fine_grained": True})
# spaCy v3.0 · Python 3 · via Binder
import spacy
from spacy.language import Language
from spacy import displacy
nlp = spacy.load("en_core_web_sm")
@Language.component("extract_person_orgs")
def extract_person_orgs(doc):
person_entities = [ent for ent in doc.ents if ent.label_ == "PERSON"]
for ent in person_entities:
head = ent.root.head
if head.lemma_ == "work":
preps = [token for token in head.children if token.dep_ == "prep"]
for prep in preps:
orgs = [token for token in prep.children if token.ent_type_ == "ORG"]
print({'person': ent, 'orgs': orgs, 'past': head.tag_ == "VBD"})
return doc
# To make the entities easier to work with, we'll merge them into single tokens
nlp.add_pipe("merge_entities")
nlp.add_pipe("extract_person_orgs")
doc = nlp("Alex Smith worked at Acme Corp Inc.")
# If you're not in a Jupyter / IPython environment, use displacy.serve
displacy.render(doc, options={"fine_grained": True})
如果您更改上面的句子结构,例如更改为“was working”,您会注意到我们当前的逻辑失败并且无法正确地将公司检测为过去的组织。那是因为词根是分词,时态信息在附加的助动词“was”中:
为了解决这个问题,我们可以调整规则用以检查上述结构:
@Language.component("extract_person_orgs")
def extract_person_orgs(doc):
person_entities = [ent for ent in doc.ents if ent.label_ == "PERSON"]
for ent in person_entities:
head = ent.root.head
if head.lemma_ == "work":
preps = [token for token in head.children if token.dep_ == "prep"]
for prep in preps:
orgs = [t for t in prep.children if t.ent_type_ == "ORG"]
aux = [token for token in head.children if token.dep_ == "aux"]
past_aux = any(t.tag_ == "VBD" for t in aux)
past = head.tag_ == "VBD" or head.tag_ == "VBG" and past_aux
print({'person': ent, 'orgs': orgs, 'past': past})
return doc
@Language.component("extract_person_orgs")
def extract_person_orgs(doc):
person_entities = [ent for ent in doc.ents if ent.label_ == "PERSON"]
for ent in person_entities:
head = ent.root.head
if head.lemma_ == "work":
preps = [token for token in head.children if token.dep_ == "prep"]
for prep in preps:
orgs = [t for t in prep.children if t.ent_type_ == "ORG"]
aux = [token for token in head.children if token.dep_ == "aux"]
past_aux = any(t.tag_ == "VBD" for t in aux)
past = head.tag_ == "VBD" or head.tag_ == "VBG" and past_aux
print({'person': ent, 'orgs': orgs, 'past': past})
return doc
在您最终的基于规则的系统中,您最终可能会使用几种不同的代码路径来涵盖数据中出现的构造类型。