支持形态和词嵌入的成分语法解析器
项目描述
word-mover-grammar
此包实现了一个具有相当灵活的终结符匹配的上下文无关语法解析器。支持的匹配模式有
- 标准精确匹配(例如在NLTK中);
- 正则表达式匹配(例如在Lark中);
- 词元匹配(例如在Yandex Alice中);
- 词嵌入匹配(没有已知的Python实现)。
此包的使命是能够轻松创建针对各种NLU问题的自定义语法,例如句子分类或提取语义槽。
它被称为“词移动语法”,因为它就像词移动距离一样,将词嵌入应用于句子模板,但以一种更结构化的方式。
目录
安装
pip install word-mover-grammar
基本解析
WMG生产规则可以使用以下语法的文本文件来描述
- 小写标记表示终结符,大写标记表示非终结符。您还可以在非终结符前加
$
符号,或将终结符放入单括号中。 - 生产式的左右两侧可以使用
:
或->
标记分隔。 - 同一生产式的不同右侧可以使用
|
符号或使用换行符后跟几个空格来分隔。在后一种情况下,每个RHS可以加前缀-
,这使得格式与YAML兼容。 - 单行注释可以从
#
符号开始。
下面的代码片段显示了如何创建一个简单的语法和解析器
import word_mover_grammar as wmg
rules = """
S : NP VP
NP: N | A NP
VP: V | VP NP | VP PP
PP: P NP
N: fruit | flies | bananas
A: fruit
V: like | flies | are
P: like
"""
grammar = wmg.text_to_grammar.load_granet(rules)
parser = wmg.earley.EarleyParser(grammar, root_symbol='S')
主要推理方法是parser.parse(tokens)
,其中tokens
是一个字符串列表。此方法返回一个ParseResult
对象,该对象存储解析树。
result = parser.parse('bananas are fruit'.split())
print(result.success)
for tree in result.iter_trees():
wmg.earley.print_tree(tree, result.final_state)
print('=======')
以上代码的输出如下。解析器正确地推断出句子 "bananas are fruit" 由名词短语 "bananas" 和动词短语 "are fruit" 组成,而动词短语 "are fruit" 又由动词 "are" 和名词 "fruit" 组成。
True
| . |
| S |
| NP | VP |
| N | VP | NP |
| bananas | V | N |
| | are | fruit |
| bananas | are | fruit |
=======
如果短语无法解析,result.success
将为 False
- 例如这里
result = parser.parse('bananas bananas bananas'.split())
print(result.success) # False
歧义短语
某些短语可以以多种方式解析。在这种情况下,result.success
仍将为 True
,但树的数量将超过一个。
result = parser.parse('fruit flies like bananas'.split())
print(result.success)
for tree in result.iter_trees():
wmg.earley.print_tree(tree, result.final_state)
print('=======')
上述短语可以有两种理解方式
- 那种特别的昆虫喜欢香蕉;
- 某些水果的飞行方式与香蕉相似。解析结果对两种解释都有树。
| . |
| S |
| NP | VP |
| N | VP | PP |
| fruit | V | P | NP |
| | flies | like | N |
| | | | bananas |
| fruit | flies | like | bananas |
=======
| . |
| S |
| NP | VP |
| A | NP | VP | NP |
| fruit | N | V | N |
| | flies | like | bananas |
| fruit | flies | like | bananas |
=======
不精确匹配
默认情况下,WMG 只使用标记的精确匹配。然而,可以通过特殊指令激活几种更多匹配方式。
%w2v
:如果词嵌入的点积高于阈值(默认为0.5
),则认为单词相等。如果使用此模式,解析器构造函数需要额外的参数w2v
- 一个将单词转换为向量的可调用对象。%lemma
:如果至少有一些词的正常形式相同,则认为单词相等。如果使用此模式,解析器构造函数需要额外的参数lemmer
- 一个将单词转换为正常形式列表的可调用对象。%regex
:如果单词可以通过正则表达式解析,则匹配单词。%exact
:只有当单词完全相同时,才认为单词相等。
如果在非终端内部插入指令,则该指令在非终端结束前有效。如果非终端外部插入指令,则该指令在下一个非终端外部的指令之前有效,但在非终端内可以临时覆盖。
以下代码显示了为简单俄语语法进行不精确匹配的示例。
grammar = wmg.text_to_grammar.load_granet("""
root:
включи $What $Where
$What:
%w2v
свет | кондиционер
%regex
.+[аеиюя]т[ое]р
$Where:
в $Room
на $Room
$Room:
%lemma
ванна | кухня | спальня
""")
作为lemmer,我们可以使用pymorphy2
from pymorphy2 import MorphAnalyzer
analyzer = MorphAnalyzer()
def lemmer(text):
return [p.normal_form for p in analyzer.parse(text)]
对于嵌入,我们可以使用压缩的FastText模型
import compress_fasttext
small_model = compress_fasttext.models.CompressedFastTextKeyedVectors.load(
'https://github.com/avidale/compress-fasttext/releases/download/v0.0.1/ft_freqprune_100K_20K_pq_100.bin'
)
small_model.init_sims()
def w2v(text):
return small_model.word_vec(text, use_norm=True)
解析器结合了上述所有对象
parser = wmg.earley.EarleyParser(grammar, w2v=w2v, w2v_threshold=0.5, lemmer=lemmer)
以下短语包含一个OOV词 пылесос
,但其嵌入接近于 вентилятор
,因此匹配成功。另一个问题是,спальне
不等于 спальня
,但它们的正常形式相同,因此匹配是可能的。
tokens = 'включи пылесос в спальне'.split()
result = parser.parse(tokens)
print(result.success)
for tree in result.iter_trees():
wmg.earley.print_tree(tree, result.final_state, w=16)
print('=======')
输出如下
True
| . |
| root |
| включи | $What | $Where |
| | кондиционер | в | $Room |
| | | | спальня |
| включи | пылесос | в | спальне |
=======
形式和槽
在对话系统中,短语通常被视为 形式 - 信息容器。每个有意义的信息片段都可以存储在类型化的 槽 中。你可以把它们想象成正则表达式中的 命名组,或者扩展的 命名实体。
在WMG中,每个槽都与某些非终端符号相关联。这种关联可以在与生成规则相同的文件中配置。
import word_mover_grammar as wmg
cfg = """
root:
turn the $What $Where on
turn on the $What $Where
$What: light | conditioner
$Where: in the $Room
$Room: bathroom | kitchen | bedroom | living room
slots:
what:
source: $What
room:
source: $Room
"""
grammar = wmg.text_to_grammar.load_granet(cfg)
parser = wmg.earley.EarleyParser(grammar)
result = parser.parse('turn on the light in the living room'.split())
print(result.slots)
结果将是一个 Yandex兼容 的槽名称到短语中找到的槽的映射。
{'what': {'type': 'string', 'value': 'light', 'text': 'light', 'tokens': {'start': 3, 'end': 4}},
'room': {'type': 'string', 'value': 'living room', 'text': 'living room', 'tokens': {'start': 6, 'end': 8}}}
几点注意事项
- 目前,在歧义短语中,槽只从第一个解析树中提取。如果您想从一个任意的树中提取槽,可以调用
result.extract_slots(tree)
。 - 每个槽只填充一次。如果非终端在短语中多次出现,相应的槽将用第一次出现的内容填充。
未来计划
将来,我们计划以以下方式增强库
- 到和从NLTK语法的转换
- 支持量词和括号
- 概率解析
- 从解析树中提取意图和槽
- 完全兼容Yandex Alice语法
- 你想知道什么!