## 目录
- [12. 依存句法分析](#12-依存句法分析)
- [12.1 短语结构树](#121-短语结构树)
- [12.2 依存句法树](#122-依存句法树)
- [12.3 依存句法分析](#123-依存句法分析)
- [12.4 基于转移的依存句法分析](#124-基于转移的依存句法分析)
- [12.5 依存句法分析 API](#125-依存句法分析-api)
- [12.6 案例: 基于依存句法分析的意见抽取](#126-案例-基于依存句法分析的意见抽取)

## 12. 依存句法分析

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



### 12.1 短语结构树

语言其实具备自顶而下的层级关系，固定数量的语法结构能够生成无数句子。比如，仅仅利用下列两个语法规律，我们就能够生成所有名词短语。

- 名词短语可以由名词和名词短语组成。
- 名词短语还可以由名词和名词组成。

例如，“上海+浦东+机场+航站楼”，所以，汉语中大部分句子都可以通过这样的语法来生成。

在语言学中，这样的语法被称为**上下文无关文法**，它由如下组件构成:

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

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

1. **短语结构树**

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

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-14_16-27-56.png)

   

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

   

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

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

   常见的标记如下:

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

   但是由于短语结构语法比较复杂，相应句法分析器的准确率并不高，现在研究者绝大部分转向了另一种语法形式。



### 12.2 依存句法树

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

1. **依存句法理论**

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

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-14_17-55-50.png)

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

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-14_17-58-48.png)

   

   现代依存语法中，语言学家 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](http://file.hankcs.com/corpus/chs-gsd-ud.zip)

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

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-14_18-37-41.png)

   

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

   [http://niversaldependencies.org/guidelines.html](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 的执行步骤如下:

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-18_17-10-42.png)
   
   
   
   此时集合 A 中的依存弧为一颗依存句法树。
   
   
   
2. **训练原理**

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

   这里可以使用感知机进行训练得到转移动作序列，原理详见:

   [5. 感知机分类与序列标注](https://mp.weixin.qq.com/s/HXkeXDS9krQ8z0XDL6f-9A)

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

   **训练分为以下几个步骤**:

   - 读入一个训练样本，提取特征，创建 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](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 为例，具体计算方式如下:
   
   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-18_17-12-27.gif)
   
   UAS 的计算也是同理，只不过将每条依存弧上的标签去掉后放人集合参与运算即可。相较于 LAS, UAS 仅仅衡量支配词的预测准确率，不衡量依存关系的准确率，一般分数更高。 
   
   在上面的训练模型中已经做了评测

   ```
UAS=83.3% LAS=81.0%
   ```
   
   这个分数说明，在测试集上有 83% 的支配词被准确预测，有 81% 的依存弧被准确预测。



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

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

> 电池非常棒，机身不长，长的是待机，但是屏幕分辨率不高。

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

这时就可以对这句话进行依存句法分析，分析代码如下:

```python
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	_	_
```



进行可视化后:

![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-18_16-43-14.png)



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

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

   ```python
   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，于是我们只需检查形容词是否存在否定修饰的支配词即可。于是得出第二版算法:

   ```python
   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. 接下来思考如何提取“待机”的意见，“待机”与“长”之间的公共父节点为“是”，于是我们得到第三版算法如下:

   ```python
   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 个属性被完整正确地提取出来了，读者可以尝试搜集更多的句子，通过分析句法结构总结更多的提取规则。








