将分词器与NLTK的语法和解析器组合在一起

27 投票
3 回答
9883 浏览
提问于 2025-04-16 10:59

我正在学习《NLTK》这本书,但我遇到了一些问题,感觉有些事情应该是构建一个不错的语法的自然第一步。

我的目标是为特定的文本语料库建立一个语法。

(最初的问题是:我应该从头开始写语法,还是从一个预定义的语法开始?如果我应该从其他语法开始,哪个适合英语的入门呢?)

假设我有以下这个简单的语法:

simple_grammar = nltk.parse_cfg("""
S -> NP VP
PP -> P NP
NP -> Det N | Det N PP
VP -> V NP | VP PP
Det -> 'a' | 'A'
N -> 'car' | 'door'
V -> 'has'
P -> 'in' | 'for'
 """);

这个语法可以解析一个非常简单的句子,比如:

parser = nltk.ChartParser(simple_grammar)
trees = parser.nbest_parse("A car has a door")

现在我想扩展这个语法,以处理包含其他名词和动词的句子。我该如何将这些名词和动词添加到我的语法中,而不需要在语法中手动定义它们呢?

例如,假设我想解析句子“A car has wheels”。我知道提供的分词器可以神奇地识别出哪些词是动词、名词等等。我该如何利用分词器的输出告诉语法“wheels”是一个名词呢?

3 个回答

11

解析(Parsing)是个复杂的问题,很多地方可能会出错!

你需要(至少)三个部分,一个是分词器(tokenizer),一个是标注器(tagger),最后是解析器(parser)。

首先,你需要把连续的文本分割成一个个小单元,这个过程叫做分词。最简单的方式就是根据空格把输入的字符串切开,但如果你要解析更复杂的文本,还得处理数字和标点符号,这可不是件简单的事。例如,句子结尾的句号通常不算在它后面的单词里,但表示缩写的句号通常是算在内的。

当你得到一个输入单元的列表后,可以用标注器来判断每个单词的词性(POS),并用它来消除输入标签序列的歧义。这有两个主要好处:首先,它加快了解析速度,因为我们不再需要考虑那些由模糊单词引发的不同假设,因为词性标注器已经处理过了。其次,它改善了对未知单词的处理,也就是那些不在你语法规则里的单词,标注器也会给这些单词分配一个标签(希望是正确的)。把解析器和标注器结合起来是很常见的做法。

词性标签将构成你语法中的预终结符(pre-terminals)。预终结符是产生式的左侧,而右侧只有终结符。比如 N -> "house",V -> "jump" 等等。N 和 V 就是预终结符。通常情况下,语法会有语法性,左右两边都是非终结符的产生式,以及一个非终结符对应一个终结符的词汇产生式。这在语言学上大多数时候是有道理的,而且大多数上下文无关文法(CFG)解析器要求语法以这种形式存在。不过,你可以通过从右侧有非终结符的终结符创建“虚拟产生式”来以这种方式表示任何CFG。

如果你想在语法中对词性标签进行更细致或更粗略的区分,可能需要在词性标签和预终结符之间建立某种映射。然后你可以用标注器的结果来初始化图表,也就是在每个输入单元上填充适当类别的被动项。可惜我不太了解NTLK,但我相信有简单的方法可以做到这一点。当图表初始化后,解析可以正常进行,任何解析树(包括单词)也可以按常规方式提取。

不过,在大多数实际应用中,你会发现解析器可能会返回几种不同的分析结果,因为自然语言是高度模糊的。我不知道你想解析的文本库是什么样的,但如果它像自然语言,你可能需要构建某种解析选择模型,这需要一个树库(treebank),也就是一组解析树,数量从几百到几千不等,这取决于你的语法和你需要的结果准确性。有了这个树库,就可以自动推导出一个对应的概率上下文无关文法(PCFG)。然后可以用这个PCFG作为简单的模型来对解析树进行排序。

这一切都是需要自己做很多工作的。你打算用解析结果做什么?你有没有看过NTLK或其他包,比如StanfordParser或BerkeleyParser?

13

我知道这已经是一年后的事了,但我想分享一些我的想法。

我正在做一个项目,需要把很多不同的句子标注上词性。然后我按照StompChicken的建议,从元组(单词,标签)中提取标签,并把这些标签当作“终端”(也就是我们在创建一个完全标注的句子时的底部节点)。

不过,这样做并不能满足我想在名词短语中标记主名词的需求,因为我无法把主名词“单词”引入语法中,因为语法里只有标签。

所以我做了一个改变,使用(单词,标签)元组来创建一个标签字典,把所有带有该标签的单词作为这个标签的值。然后我把这个字典打印到屏幕上或保存到grammar.cfg(上下文无关语法)文件中。

我用来打印的格式与通过加载语法文件设置解析器非常匹配(parser = nltk.load_parser('grammar.cfg'))。它生成的其中一行看起来像这样:

VBG -> "fencing" | "bonging" | "amounting" | "living" ... 还有30多个单词...

现在我的语法中有实际的单词作为终端,并且分配了与nltk.tag_pos相同的标签。

希望这能帮助到其他想要自动标注大量语料库,并且仍然希望在他们的语法中保留实际单词作为终端的人。

import nltk
from collections import defaultdict

tag_dict = defaultdict(list)

...
    """ (Looping through sentences) """

        # Tag
        tagged_sent = nltk.pos_tag(tokens)

        # Put tags and words into the dictionary
        for word, tag in tagged_sent:
            if tag not in tag_dict:
                tag_dict[tag].append(word)
            elif word not in tag_dict.get(tag):
                tag_dict[tag].append(word)

# Printing to screen
for tag, words in tag_dict.items():
    print tag, "->",
    first_word = True
    for word in words:
        if first_word:
            print "\"" + word + "\"",
            first_word = False
        else:
            print "| \"" + word + "\"",
    print ''
16

你可以对你的文本使用一个词性标注工具,然后调整你的语法规则,让它根据词性标签来工作,而不是直接用单词。

> text = nltk.word_tokenize("A car has a door")
['A', 'car', 'has', 'a', 'door']

> tagged_text = nltk.pos_tag(text)
[('A', 'DT'), ('car', 'NN'), ('has', 'VBZ'), ('a', 'DT'), ('door', 'NN')]

> pos_tags = [pos for (token,pos) in nltk.pos_tag(text)]
['DT', 'NN', 'VBZ', 'DT', 'NN']

> simple_grammar = nltk.CFG.fromstring("""
  S -> NP VP
  PP -> P NP
  NP -> Det N | Det N PP
  VP -> V NP | VP PP
  Det -> 'DT'
  N -> 'NN'
  V -> 'VBZ'
  P -> 'PP'
  """)

> parser = nltk.ChartParser(simple_grammar)
> tree = parser.parse(pos_tags)

撰写回答