《用Python进行自然语言处理》第8章 分析句子结构

1. 我们如何使用形式化语法来描述无限的句子集合的结构?
2. 我们如何使用句法树来表示句子结构?

3. 语法分析器如何分析一个句子并自动构建语法树?

8.1 一些语法困境

语言数据和无限可能性

#语言提供给我们的结构,看上去可以无限扩展句子
#文法的目的是给出一个明确的语言描述。

普遍存在的歧义

#让我们仔细看看短语 I shot an elephant in my pajamas 中的歧义。
#首先,我们需要定义一个简单的文法:
import nltk
groucho_grammar = nltk.CFG.fromstring("""
    S -> NP VP
    PP -> P NP
    NP -> Det N | Det N PP | 'I'
    VP -> V NP | VP PP
    Det -> 'an' | 'my'
    N -> 'elephant' | 'pajamas'
    V -> 'shot'
     P -> 'in'
    """)
#这个文法允许以两种方式分析句子,取决于介词短语 in my pajamas 是描述大象还是枪击事件。
sent = ['I', 'shot', 'an', 'elephant', 'in', 'my', 'pajamas']
parser = nltk.ChartParser(groucho_grammar)
for tree in parser.parse(sent):
    print(tree)
    tree.draw()

8.2 文法有什么用?

超越 n-grams

#学习文法的一个好处是,它提供了一个概念框架和词汇表能拼凑出这些直觉。
#在一个符合语法规则的句子中的词序列可以被一个更 小的序列替代而不会导致句子不符合语法规则。

8.3 上下文无关文法

一种简单的文法

# 首先,让我们看一个简单的上下文无关文法
# 按照惯例, 第一条生产式的左端是文法的开始符号,通常是 S,所有符合语法规则的树都必须有这个符 号作为它们的根标签。
# NLTK中,上下文无关文法定义在 nltk.grammar 模块。


#上下文无关文法
grammar1 = nltk.CFG.fromstring("""
  S -> NP VP
  VP -> V NP | V NP PP
  PP -> P NP
  V -> "saw" | "ate" | "walked"
  NP -> "John" | "Mary" | "Bob" | Det N | Det N PP
  Det -> "a" | "an" | "the" | "my"
  N -> "man" | "dog" | "cat" | "telescope" | "park"
  P -> "in" | "on" | "by" | "with"
  """)
sent = "Mary saw Bob".split()
rd_parser = nltk.RecursiveDescentParser(grammar1)
for tree in rd_parser.parse(sent):
    print(tree)
    tree.draw()
# S    句子      the man walked
# NP   名词短语   a dog
# VP   动词短语   saw a park
# PP   介词短语   with a telescope
# Det  限定词     the
# N    名词       dog 
# V    动词       walked
# P    介词       in

写你自己的文法

import nltk
nltk.download('Users')

def you_grammar(gram):
    sent = "Mary saw Bob".split()
    rd_parser = nltk.RecursiveDescentParser(gram)
    for tree in rd_parser.parse(sent):
        print(tree)
        tree.draw()
print(yours_grammar(grammar1))
#grammar1 = nltk.data.load('file:mygrammar.cfg')
#确保你的文件名后缀为.cfg,并且字符串'file:mygrammar.cfg中'间没有空格符

句法结构中的递归

#一个文法被认为是递归的,如果文法类型出现在产生式左侧也出现在右侧,
#如例 8-2 所 示。产生式 Nom -> Adj Nom(其中 Nom 是名词性的类别)包含 Nom 类型的直接递归, 
#而 S 上的间接递归来自于两个产生式的组合:S -> NP VP 与 VP -> V S。

#例8-2. 递归的上下文无关文法。
grammar2 = nltk.CFG.fromstring("""
  S  -> NP VP
  NP -> Det Nom | PropN
  Nom -> Adj Nom | N
  VP -> V Adj | V NP | V S | V NP PP
  PP -> P NP
  PropN -> 'Buster' | 'Chatterer' | 'Joe'
  Det -> 'the' | 'a'
  N -> 'bear' | 'squirrel' | 'tree' | 'fish' | 'log'
  Adj  -> 'angry' | 'frightened' |  'little' | 'tall'
  V ->  'chased'  | 'saw' | 'said' | 'thought' | 'was' | 'put'
  P -> 'on'
  """)
def yours_grammar(sent, gram):
    rd_parser = nltk.RecursiveDescentParser(gram)
    for tree in rd_parser.parse(sent):
        print(tree)
        tree.draw()
sent1 = "the angry bear chased the frightened little squirrel".split()
sent2 = "Chatterer said Buster thought the tree was tall".split()
print("sent1: ")
print(yours_grammar(sent1, grammar2))
print("sent2: ")
print(yours_grammar(sent2, grammar2))

8.4 上下文无关文法分析

#分析器根据文法产生式处理输入的句子,并建立一个或多个符合文法的组成结构。
#文法 是一个格式良好的声明规范——它实际上只是一个字符串,而不是程序。
#分析器是文法的解 释程序。它搜索符合文法的所有树的空间找出一棵边缘有所需句子的树。


# 分析器允许使用一组测试句子评估一个文法,帮助语言学家发现在他们的文法分析中存 在的错误。
# 分析器可以作为心理语言处理模型,帮助解释人类处理某些句法结构的困难。

递归下降分析

#一种最简单的分析器将一个文法作为如何将一个高层次的目标分解成几个低层次的子 目标的规范来解释。

#递归下降分析器在上述过程中建立分析树。带着最初的目标(找到一个 S),创建 S 根 节点。
#随着上述过程使用文法的产生式递归扩展,分析树不断向下延伸(故名为递归下降)
#我们可以在图形化示范 nltk.app.rdparser()中看到这个过程
nltk.app.rdparser()

#NLTK 提供了一个递归下降分析器:
rd_parser = nltk.RecursiveDescentParser(grammar1)
sent = "Mary saw a dog".split()
for tree in rd_parser.parse(sent):
    print(tree)
    
    
#递归下降分析有三个主要的缺点。
#首先,左递归产生式,如:NP -> NP PP,会进入死循环。
#第二,分析器浪费了很多时间处理不符合输入句子的词和结构。
#第三,回溯过程中可 能会丢弃分析过的成分,它们将需要在之后再次重建。
#例如:从VP -> V NP上回溯将放 弃为 NP 创建的子树。
#如果分析器之后处理 VP -> V NP PP,那么 NP 子树必须重新创建。


#递归下降分析是一种自上而下分析。

移进-归约分析


#种简单的自下而上分析器是移进-归约分析器。
#尝试找到对应文法生产式右侧的词和短语的序列,用左侧的替换它们,直到整个 句子归约为一个 S。
nltk.app.srparser()
#移进-归约分析器的六个阶段:分析器一开始把输入的第一个词转移到堆栈;
#一旦堆 栈顶端的项目与一个文法产生式的右侧匹配,就可以将它们用那个产生式的左侧替换;
#当所 有输入都被使用过且堆栈中只有剩余一个项目 S 时,分析成功。


#NLTK 中提供了 ShiftReduceParser(),移进-归约分析器的一个简单的实现。
sr_parse = nltk.ShiftReduceParser(grammar1)
sent = 'Mary saw a dog'.split()
for tree in sr_parse.parse(sent):
    print(tree)

左角落分析器

#递归下降分析器的问题之一是当它遇到一个左递归产生式时,会进入无限循环。
#这是因 为它盲目应用文法产生式而不考虑实际输入的句子。
#左角落分析器是我们已经看到的自下而 上与自上而下方法的混合体。

#左角落分析器 是一个带自下而上过滤的自上而下的分析器。

符合语句规则的子串表

#运用动态 规划算法设计技术分析问题
#动态规划存储中间结果,并在 适当的时候重用它们,能显著提高效率。

#这种技术可以应用到句法分析,使我们能够存储分析任务的部分解决方案,
#然后在必要的时候查找它们,直到达到最终解决方案。这种分析方法被称为图表分析。

#动态规划使我们能够只建立一次 PP in my pajamas。
#第一次我们建立时就把它存入一 个表格中,然后在我们需要作为对象 NP 或更高的 VP 的组成部分用到它时我们就查找表格。 
#这个表格被称为符合语法规则的子串表 或简称为 WFST。



#使用符合语句规则的子串表的接收器。
def init_wfst(tokens, grammar):
    numtokens = len(tokens)
    wfst = [[None for i in range(numtokens+1)] for j in range(numtokens+1)]
    for i in range(numtokens):
        productions = grammar.productions(rhs=tokens[i])
        wfst[i][i+1] = productions[0].lhs()
    return wfst

def complete_wfst(wfst, tokens, grammar, trace=False):
    index = dict((p.rhs(), p.lhs()) for p in grammar.productions())
    numtokens = len(tokens)
    for span in range(2, numtokens+1):
        for start in range(numtokens+1-span):
            end = start + span
            for mid in range(start+1, end):
                nt1, nt2 = wfst[start][mid], wfst[mid][end]
                if nt1 and nt2 and (nt1,nt2) in index:
                    wfst[start][end] = index[(nt1,nt2)]
                    if trace:
                        print("[%s] %3s [%s] %3s [%s] ==> [%s] %3s [%s]" % \
                        (start, nt1, mid, nt2, end, start, index[(nt1,nt2)], end))
    return wfst

def display(wfst, tokens):
    print('\nWFST ' + ' '.join(("%-4d" % i) for i in range(1, len(wfst))))
    for i in range(len(wfst)-1):
        print("%d   " % i, end=" ")
        for j in range(1, len(wfst)):
            print("%-4s" % (wfst[i][j] or '.'), end=" ")
        print()
tokens = "I shot an elephant in my pajamas".split()
wfst0 = init_wfst(tokens, groucho_grammar)
display(wfst0, tokens)
wfst1 = complete_wfst(wfst0, tokens, groucho_grammar)
display(wfst1, tokens)
wfst1 = complete_wfst(wfst0, tokens, groucho_grammar, trace=True)

8.5 依存关系和依存文法

#短语结构文法是关于词和词序列如何结合起来形成句子成分的。
#一个独特的和互补的方式,依存文法,集中关注的是词与其他词之间的关系。
#依存关系是一个中心词与它的依赖之间的二元对称关系。
#一个句子的中心词通常是动词,所有其他词要么依赖于中心词,要么依 赖路径与它联通。

#依存关系表示是一个加标签的有向图,其中节点是词汇项,加标签的弧表示依赖关系,从中心词到依赖。


#下面是 NLTK 为依存文法编码的一种方式 ——注意它只能捕捉依存关系信息,不能指 定依存关系类型:
groucho_dep_grammar = nltk.DependencyGrammar.fromstring("""
    'shot' -> 'I' | 'elephant' | 'in'
    'elephant' -> 'an' | 'in'
    'in' -> 'pajamas'
    'pajamas' -> 'my'
    """)
print(groucho_dep_grammar)

#依存关系图是一个投影,当所有的词都按线性顺序书写 ,边可以在词上绘制而不会交叉。 
#这等于是说一个词及其所有后代依赖(依赖及其依赖的依赖,等等)在句子中形成一个连续 的词序列
pdp = nltk.ProjectiveDependencyParser(groucho_dep_grammar)
sent = 'I shot an elephant in my pajamas'.split()
trees = pdp.parse(sent)
for tree in trees:
    print(tree)
    tree.draw()

配价与词汇


#动词和它们的依赖
#依赖 ADJ、NP、PP 和 S 通常被称为各自动词的补语,什么动词可以和什么补语一起出现 具有很强的约束。

#在依存文法的传统中,在表 8-3 中的动词被认为具有不同的配价。配价限制不仅适用于 动词,也适用于其他类的中心词。
#介词短语、形容词和副词通常充当修饰语。与补充不同修饰语是可选的,经常可以进行 迭代,不会像补语那样被中心词选择。

扩大规模

#到目前为止,我们只考虑了“玩具文法”,演示分析的关键环节的少量的文法。
#但有一个明显的问题就是这种做法是否可以扩大到覆盖自然语言的大型语料库。


#很难将文法模块 化,每部分文法可以独立开发。
#反过来这意味着,在一个语言学家团队中分配编写文法的任 务是很困难的。

# 另一个困难是当文法扩展到包括更加广泛的成分时,适用于任何一个句子的分析的数量也相应增加。
# 换句话说,歧义随着覆盖而增加。

8.6 文法开发

树库和文法

#corpus 模块定义了树库语料的阅读器,其中包含了宾州树库语料的 10%的样本。
from nltk.corpus import treebank
t = treebank.parsed_sents('wsj_0001.mrg')[0]
print(t)
#我们可以利用这些数据来帮助开发一个文法。


#搜索树库找出句子的补语。
def filter(tree):
    child_nodes = [child.label() for child in tree if isinstance(child, nltk.Tree)]
    return (tree.label() == 'VP') and ('S' in child_nodes)
from nltk.corpus import treebank
print([subtree for tree in treebank.parsed_sents() for subtree in tree.subtrees(filter)][1])


#NLTK 语料库也收集了中央研究院树库语料,包括 10000 句已分析的句子,来自现代汉 语中央研究院平衡语料库。
#让我们加载并显示这个语料库中的一棵树。
print(nltk.corpus.sinica_treebank.parsed_sents()[3450].draw())

有害的歧义

grammar = nltk.CFG.fromstring("""
     S -> NP V NP
     NP -> NP Sbar
     Sbar -> NP V
     NP -> 'fish'
     V -> 'fish'
     """)
tokens = ['fish'] * 5
cp = nltk.ChartParser(grammar)
for tree in cp.parse(tokens):
    print(tree)
#随着句子长度增加到(3,5,7,...),我们得到的分析树的数量是:1; 2; 5; 14; 42; 132; 429; 1,430; 4,862; 16,796; 58,786; 208,012; .... 

加权文法

#处理歧义是开发广泛覆盖的分析器的主要挑战。
#图表分析器提高 了计算一个句子的多个分析的效率,但它们仍然因可能的分析的数量过多而不堪重负。
#加权 文法和概率分析算法为这些问题提供了一个有效的解决方案。



#宾州树库样本中give和gave的用法。
def give(t):
    return t.label() == 'VP' and len(t) > 2 and t[1].label() == 'NP'\
           and (t[2].label() == 'PP-DTV' or t[2].label() == 'NP')\
           and ('give' in t[0].leaves() or 'gave' in t[0].leaves())
def sent(t):
    return ' '.join(token for token in t.leaves() if token[0] not in '*-0')
def print_node(t, width):
        output = "%s %s: %s / %s: %s" %\
            (sent(t[0]), t[1].label(), sent(t[1]), t[2].label(), sent(t[2]))
        if len(output) > width:
            output = output[:width] + "..."
        print(output)
        
        
for tree in nltk.corpus.treebank.parsed_sents():
    for t in tree.subtrees(give):
        print_node(t, 72)
        
        
#概率上下文无关文法(probabilistic context-free grammar,PCFG)是一种上下文无关文 法,
#它的每一个产生式关联一个概率。它会产生与相应的上下文无关文法相同的文本解析, 并给每个解析分配一个概率。
#PCFG 产生的一个解析的概率仅仅是它用到的产生式的概率的乘积。

grammar = nltk.PCFG.fromstring("""
    S    -> NP VP              [1.0]
    VP   -> TV NP              [0.4]
    VP   -> IV                 [0.3]
    VP   -> DatV NP NP         [0.3]
    TV   -> 'saw'              [1.0]
    IV   -> 'ate'              [1.0]
    DatV -> 'gave'             [1.0]
    NP   -> 'telescopes'       [0.8]
    NP   -> 'Jack'             [0.2]
    """)
print(grammar)


viterbi_parser = nltk.ViterbiParser(grammar)
for tree in viterbi_parser.parse(['Jack', 'saw', 'telescopes']):
    print(tree)

8.7 小结


句子都有内部组织结构,可以用一棵树表示。组成结构的显著特点是:递归、中心词、 补语和修饰语。

文法是一个潜在的无限的句子集合的一个紧凑的特性;我们说,一棵树是符合语法规则 的或文法树授权一棵树。文法是用于描述一个给定的短语是否可以被分配一个特定的成分或依赖结构的一种形 式化模型。

给定一组句法类别,上下文无关文法使用一组生产式表示某类型 A 的短语如何能够被 分析成较小的序列α1 ... αn。

依存文法使用产生式指定给定的中心词的依赖是什么。

当一个句子有一个以上的文法分析就产生句法歧义(如介词短语附着歧义)。

分析器是一个过程,为符合语法规则的句子寻找一个或多个相应的树。

一个简单的自上而下分析器是递归下降分析器,在文法产生式的帮助下递归扩展开始符号(通常是 S),尝试匹配输入的句子。这个分析器并不能处理左递归产生式(如:NP -> NP PP)。它盲目扩充类别而不检查它们是否与输入字符串兼容的方式效率低下, 而且会重复扩充同样的非终结符然后丢弃结果。

一个简单的自下而上的分析器是移位-规约分析器,它把输入移到一个堆栈中,并尝试匹配堆栈顶部的项目和文法产生式右边的部分。这个分析器不能保证为输入找到一个有 效的解析,即使它确实存在,它建立子结构而不检查它是否与全部文法一致。

阅读更多
个人分类: 自然语言处理
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭