《天然语言处理入门》12.依存句法分析--提取用户评论

笔记转载于GitHub项目https://github.com/NLP-LOVE/Introduction-NLPhtml

12. 依存句法分析

语法分析(syntactic parsing )是天然语言处理中一个重要的任务,其目标是分析句子的语法结构并将其表示为容易理解的结构(一般是树形结构)。同时,语法分析也是全部工具性NLP任务中较为高级、较为复杂的一种任务。 经过掌握语法分析的原理、实现和应用,咱们将在NLP工程师之路上跨越一道分水岭。 本章将会介绍短语结构树依存句法树两种语法形式,而且着重介绍依存句法分析的原理和实现。python

12.1 短语结构树

语言其实具有自顶而下的层级关系,固定数量的语法结构可以生成无数句子。好比,仅仅利用下列两个语法规律,咱们就可以生成全部名词短语。git

  • 名词短语能够由名词和名词短语组成。
  • 名词短语还能够由名词和名词组成。

例如,“上海+浦东+机场+航站楼”,因此,汉语中大部分句子均可以经过这样的语法来生成。github

在语言学中,这样的语法被称为上下文无关文法,它由以下组件构成:正则表达式

  • 终结符结合 Σ,好比汉语的一个词表。
  • 非终结符集合 V,好比“名词短语”“动词短语”等短语结构组成的集合。V 中至少包含一个特殊的非终结符,即句子符或初始符,计做 S。
  • 推到规则 R,即推到非终结符的一系列规则: V -> V U Σ。

基于上下文无关文法理论,咱们能够从 S 出发,逐步推导非终结符。一个非终结符至少产生一个下级符号,如此一层一层地递推下去,咱们就获得了一棵语法树。但在NLP中,咱们称其为短语结构树。也就是说,计算机科学中的术语“上下文无关文法”在语言学中被称做“短语结构语法”。算法

  1. 短语结构树api

    短语结构语法描述了如何自顶而下的生成一个句子,反过来,句子也能够用短语结构语法来递归的分解。层级结构实际上是一种树形结构,例如这句话“上海 浦东 开发 与 法制 建设 同步”,分解成以下图的短语结构树:框架

    这样的树形结构称为短语结构树,相应的语法称为*短语结构语法**或上下文无关文法。至于树中的字母下面开始介绍。机器学习

  2. 宾州树库和中文树库ide

    语言学家制定短语结构语法规范,将大量句子人工分解为树形结构,造成了一种语料库,称为树库( treebank )。常见的英文树库有宾州树库,相应地,中文领域有CTB。上图中叶子节点(词语)的上级节点为词性,词性是非终结符的一种,知足“词性生成词语”的推导规则。

    常见的标记以下:

    标记 释义
    IP-HLN 单句-标题
    NP-SBJ 名词短语-主语
    NP-PN 名词短语-代词
    NP 名词短语
    VP 动词短语

    可是因为短语结构语法比较复杂,相应句法分析器的准确率并不高,如今研究者绝大部分转向了另外一种语法形式。

12.2 依存句法树

不一样于短语结构树,依存句法树并不关注如何生成句子这种宏大的命题。依存句法树关注的是句子中词语之间的语法联系,而且将其约束为树形结构。

  1. 依存句法理论

    依存语法理论认为词与词之间存在主从关系,这是一种二元不等价的关系。在句子中,若是一个词修饰另外一个词,则称修饰词为从属词( dependent ),被修饰的词语称为支配词(head),二者之间的语法关系称为依存关系( dependency relation)。好比句子“大梦想”中形容词“大”与名词“梦想"之间的依存关系如图所示:

    图中的箭头方向由支配词指向从属词,这是可视化时的习惯。将一个句子中全部词语的依存关系以有向边的形式表示出来,就会获得一棵树,称为依存句法树( dependency parse tree)。好比句子“弱小的我也有大梦想”的依存句法树如图所示。

    现代依存语法中,语言学家 Robinson 对依存句法树提了 4 个约束性的公理。

    • 有且只有一个词语(ROOT,虚拟根节点,简称虚根)不依存于其余词语。
    • 除此以外全部单词必须依存于其余单词。
    • 每一个单词不能依存于多个单词。
    • 若是单词 A 依存于 B,那么位置处于 A 和 B 之间的单词 C 只能依存于 A、B 或 AB 之间的单词。

    这 4 条公理分别约束了依存句法树(图的特例)的根节点惟一性、 连通、无环和投射性( projective )。这些约束对语料库的标注以及依存句法分析器的设计奠基了基础。

  2. 中文依存句法树库

    目前最有名的开源自由的依存树库当属UD ( Universal Dependencies),它以“署名-非商业性使用-相同方式共享4.0”等相似协议免费向公众受权。UD是个跨语种的语法标注项目,一共有 200 多名贡献者为 70 多种语言标注了 100 多个树库。具体到中文,存在4个不一样领域的树库。本章选取其中规模最大的 UD_ Chinese GSD 做为示例。该树库的语种为繁体中文,将其转换为简体中文后,供你们下载使用。

    http://file.hankcs.com/corpus/chs-gsd-ud.zip

    该树库的格式为 CoNLL-U,这是一种以制表符分隔的表格格式。CoNLL-U 文件有10列,每行都是一个单词, 空白行表示句子结束。单元中的下划线 _ 表示空白, 结合其中一句样例,解释如表所示。

    词性标注集合依存关系标注集请参考 UD 的官方网站:

    http://niversaldependencies.org/guidelines.html

    另外一份著名的语料库依然是 CTB,只不过须要额外利用一些工具将短语结构树转换为依存句法树。读者能够直接下载转换后的 CTB 依存句法树库,其格式是相似于 CoNLl-U 的 CoNLL。

  3. 依存句法树的可视化

    工具以下:

    • 南京大学汤光超开发的 Dependency Viewer。导入 .conll 扩展名的树库文件便可。
    • brat 标注工具。

    可视化工具能够帮助咱们理解句法树的结构,比较句子之间的不一样。

12.3 依存句法分析

依存句法分析( dependency parsing )指的是分析句子的依存语法的一种中高级 NLP任务,其输人一般是词语和词性,输出则是一棵依存句法树。 本节介绍实现依存句法分析的两种宏观方法,以及依存句法分析的评价指标。

  1. 基于图的依存句法分析

    正如树是图的特例同样,依存句法树实际上是彻底图的一个子图。若是为彻底图中的每条边是否属于句法树的可能性打分,而后就能够利用 Prim 之类的算法找出最大生成树( MST )做为依存句法树了。这样将整棵树的分数分解( factorize )为每条边上的分数之和,而后在图上搜索最优解的方法统称为基于图的算法。

    在传统机器学习时代,基于图的依存句法分析器每每面临运行开销大的问题。这是因为传统机器学习所依赖的特征过于稀疏,训练算法须要在整个图上进行全局的结构化预测等。考虑到这些问题,另外一种基于转移的路线在传统机器学习框架下显得更加实用。

  2. 基于转移的依存句法分析

    咱们以“人 吃 鱼”这个句子为例子,手动构建依存句法树。

    • 从“吃”连线到“人”创建依存关系,主谓关系。
    • 从“吃”连线到“鱼”创建依存关系,动宾关系。

    如此,咱们将一棵依存句法树的构建过程表示为两个动做。若是机器学习模型可以根据句子的某些特征准确地预测这些动做,那么计算机就可以根据这些动做拼装出正确的依存句法树了。这种拼装动做称为转移( transition),而这类算法统称为基于转移的依存句法分析

12.4 基于转移的依存句法分析

  1. Arc-Eager 转移系统

    一个转移系统 S 由 4 个部件构成: S = (C,T,Cs,Ct),其中:

    • C 是系统状态的集合
    • T 是全部可执行的转移动做的集合。
    • Cs 是一个初始化函数
    • Ct 为一系列终止状态,系统进入该状态后便可停机输出最终的动做序列。

    而系统状态又由 3 元祖构成: C = (σ,β,A) 其中:

    • σ 为一个存储单词的栈。
    • β 为存储单词的队列
    • A 为已肯定的依存弧的集合。

    Arc-Eager 转移系统的转移动做集合详见下表:

    动做名称 条件 解释
    Shift 队列 β 非空 将队首单词 i 压栈
    LeftArc 栈顶单词 i 没有支配词 将栈顶单词 i 的支配词设为队首单词 j,即 i 做为 j 的子节点
    RightArc 队首单词 j 没有支配词 将队首单词 j 的支配词设为栈顶单词 i,即 j 做为 i 的子节点
    Reduce 栈顶单词 i 已有支配词 将栈顶单词 i 出栈

    对于上面的“人 吃 鱼”案例,Arc-Eager 的执行步骤以下:

    装填编号 σ 转移动做 β A
    0 [] 初始化 [人,吃,鱼,虚根] {}
    1 [人] Shift [吃,鱼,虚根] {}
    2 [] LeftArc(主谓) [吃,鱼,虚根] \(\{人\xleftarrow{主谓}吃\}\)
    3 [吃] Shift [鱼,虚根] \(\{人\xleftarrow{主谓}吃\}\)
    4 [吃,鱼] RightArc(动宾) [虚根] \(\{人\xleftarrow{主谓}吃,吃\xrightarrow{动宾}鱼\}\)
    5 [吃] Reduce [虚根] \(\{人\xleftarrow{主谓}吃,吃\xrightarrow{动宾}鱼\}\)
    6 [] LeftArc(核心) [虚根] \(\{人\xleftarrow{主谓}吃,吃\xrightarrow{动宾}鱼,吃\xleftarrow{核心}虚根\}\)

    此时集合 A 中的依存弧为一颗依存句法树。

  2. 训练原理

    对基于转移的依存句法分析器而言,它学习和预测的对象是一系列转移动做。然而依存句法树库是一棵树,并非现成的转移动做序列。这时候就须要一个算法将语料库中的依存句法树转移为正确地转移动做序列。

    这里可使用感知机进行训练获得转移动做序列,原理详见:

    5. 感知机分类与序列标注

    训练句法分析器时,结构化感知机算法迭代式的优化线性模型,目标是使其将最高的分值赋予可抵达正确句法树的转移序列。

    训练分为如下几个步骤:

    • 读入一个训练样本,提取特征,建立 ArcEager 的初始状态 c。
    • 若 c 不是终止状态,反复进行转移序列,修正参数。
    • 算法终止,返回返回模型参数 w。

12.5 依存句法分析 API

  1. 训练模型

    本节使用的语料库是 CTB8.0,运行代码的时候会自动下载语料库: train_parser.py

    https://github.com/NLP-LOVE/Introduction-NLP/tree/master/code/ch12/train_parser.py

    训练时间比较长,结果以下:

    1 人 人 N NN _ 2 nsubj _ _
    2 吃 吃 V VV _ 0 ROOT _ _
    3 鱼 鱼 N NN _ 2 dobj _ _
    UAS=83.3% LAS=81.0%
  2. 标准化评测

    给定两棵树,一棵树为标准答案(来自测试集),一棵树为预测结果,评测的目标是衡量这两棵树的差别。若是将树的节点编号,拆解为依存弧并分别存入两个集合 A ( 标准答案)和 B (预测结果),则能够利用分类任务的 F1 评价指标。

    依存句法分析任务采用的评测指标为 UAS (unlabeled atachment score) 和 LAS (labeled attachment score ),分别对应忽略标签和包括标签的 F1 值。以 LAS 为例,具体计算方式以下:
    \[ P=\frac{|A\cap B|}{|B|}\\ R=\frac{|A\cap B|}{|A|}\\ LAS=\frac{2*P*R}{P+R} \]
    UAS 的计算也是同理,只不过将每条依存弧上的标签去掉后放人集合参与运算便可。相较于 LAS, UAS 仅仅衡量支配词的预测准确率,不衡量依存关系的准确率,通常分数更高。

    在上面的训练模型中已经作了评测

    UAS=83.3% LAS=81.0%

    这个分数说明,在测试集上有 83% 的支配词被准确预测,有 81% 的依存弧被准确预测。

12.6 案例: 基于依存句法分析的意见抽取

其实许多人都有一个疑问,依存句法分析究竟能够用来干什么。本节就来利用依存句法分析实现一个意见抽取的例子,提取下列商品评论中的属性和买家评价。

电池很是棒,机身不长,长的是待机,可是屏幕分辨率不高。

为了提取“电池”“机身”“待机”和“分辨率”所对应的意见,朴素的处理方式是在分司和词性标注以后编写正则表达式,提取名词后面的形容词。然而正则表达式没法处理“长的是待机”这样句式灵活的例子。

这时就能够对这句话进行依存句法分析,分析代码以下:

from pyhanlp import *

CoNLLSentence = JClass('com.hankcs.hanlp.corpus.dependency.CoNll.CoNLLSentence')
CoNLLWord = JClass('com.hankcs.hanlp.corpus.dependency.CoNll.CoNLLWord')
IDependencyParser = JClass('com.hankcs.hanlp.dependency.IDependencyParser')
KBeamArcEagerDependencyParser = JClass('com.hankcs.hanlp.dependency.perceptron.parser.KBeamArcEagerDependencyParser')

parser = KBeamArcEagerDependencyParser()
tree = parser.parse("电池很是棒,机身不长,长的是待机,可是屏幕分辨率不高。")
print(tree)

运行结果以下:

1   电池  电池  N   NN  _   3   nsubj   _   _
2   很是  很是  A   AD  _   3   advmod  _   _
3   棒   棒   V   VA  _   0   ROOT    _   _
4   ,   ,   P   PU  _   3   punct   _   _
5   机身  机身  N   NN  _   7   nsubj   _   _
6   不   不   A   AD  _   7   neg _   _
7   长   长   V   VA  _   3   conj    _   _
8   ,   ,   P   PU  _   7   punct   _   _
9   长   长   V   VA  _   11  top _   _
10  的   的   D   DEC _   9   cpm _   _
11  是   是   V   VC  _   7   conj    _   _
12  待机  待机  N   NN  _   11  attr    _   _
13  ,   ,   P   PU  _   3   punct   _   _
14  可是  可是  A   AD  _   18  advmod  _   _
15  屏幕  屏幕  N   NN  _   16  nn  _   _
16  分辨率 分辨率 N   NN  _   18  nsubj   _   _
17  不   不   A   AD  _   18  neg _   _
18  高   高   V   VA  _   3   conj    _   _
19  。   。   P   PU  _   3   punct   _   _

进行可视化后:

仔细观察,不难发现“电池”与“棒”、“机身”与“长”、“分辨率”与“高”之间的依存关系都是 nsubj (名词性主语)。

  1. 利用这一规律, 不难写出初版遍历算法, 也就是用个for 循环去遍历树中的每一个节点。对于算法遍历树中的每个词语, 若是其词性为名词且做为某个形容词的名词性主语,则认为该名词是属性,而形容词是意见。运行代码以下:

    def extactOpinion1(tree):
        for word in tree.iterator():
            if word.POSTAG == "NN" and word.DEPREL == "nsubj":
                print("%s = %s" % (word.LEMMA, word.HEAD.LEMMA))
    
    print("初版")
    extactOpinion1(tree)

    结果以下:

    初版
    电池 = 棒
    机身 = 长
    分辨率 = 高
  2. 虽然的确提取出了一些意见,然然后两个都是错误的。这一版算法存在的问题之一是没有考虑到“机身不长””“分辨率不高"等否认修饰关系。否认修饰关系在依存句法中的标记为 neg,因而咱们只需检查形容词是否存在否认修饰的支配词便可。因而得出第二版算法:

    def extactOpinion2(tree):
        for word in tree.iterator():
            if word.POSTAG == "NN" and word.DEPREL == "nsubj":
                if tree.findChildren(word.HEAD, "neg").isEmpty():
                    print("%s = %s" % (word.LEMMA, word.HEAD.LEMMA))
                else:
                    print("%s = 不%s" % (word.LEMMA, word.HEAD.LEMMA))
    
    print("第二版")
    extactOpinion2(tree)

    结果以下:

    第二版
    电池 = 棒
    机身 = 不长
    分辨率 = 不高
  3. 接下来思考如何提取“待机”的意见,“待机”与“长”之间的公共父节点为“是”,因而咱们获得第三版算法以下:

    def extactOpinion3(tree):
        for word in tree.iterator():
            if word.POSTAG == "NN":
    
                 # 检测名词词语的依存弧是不是“属性关系”,
                # 若是是,则寻找支配词的子节点中的主题词
                # 以该主题词做为名词的意见。
                if word.DEPREL == "nsubj":  # ①属性
    
                    if tree.findChildren(word.HEAD, "neg").isEmpty():
                        print("%s = %s" % (word.LEMMA, word.HEAD.LEMMA))
                    else:
                        print("%s = 不%s" % (word.LEMMA, word.HEAD.LEMMA))
                elif word.DEPREL == "attr":
                    top = tree.findChildren(word.HEAD, "top")  # ②主题
    
                    if not top.isEmpty():
                        print("%s = %s" % (word.LEMMA, top.get(0).LEMMA))
    
    print("第三版")
    extactOpinion3(tree)

    结果以下:

    第三版
    电池 = 棒
    机身 = 不长
    待机 = 长
    分辨率 = 不高

至此,4 个属性被完整正确地提取出来了,读者能够尝试搜集更多的句子,经过分析句法结构总结更多的提取规则。

12.7 GitHub

HanLP何晗--《天然语言处理入门》笔记:

https://github.com/NLP-LOVE/Introduction-NLP

项目持续更新中......

目录


章节
第 1 章:新手上路
第 2 章:词典分词
第 3 章:二元语法与中文分词
第 4 章:隐马尔可夫模型与序列标注
第 5 章:感知机分类与序列标注
第 6 章:条件随机场与序列标注
第 7 章:词性标注
第 8 章:命名实体识别
第 9 章:信息抽取
第 10 章:文本聚类
第 11 章:文本分类
第 12 章:依存句法分析
第 13 章:深度学习与天然语言处理
相关文章
相关标签/搜索