## 目录
[3. 二元语法与中文分词](#3-二元语法与中文分词)
- [3.1 语言模型](#31-语言模型)
- [3.2 中文分词语料库](#32-中文分词语料库)
- [3.3 训练与预测](#33-训练与预测)
- [3.4 HanLP分词与用户词典的集成](#34-hanlp分词与用户词典的集成)
- [3.5 二元语法与词典分词比较](#35-二元语法与词典分词比较)

## 3. 二元语法与中文分词

上一章中我们实现了块儿不准的词典分词，词典分词无法消歧。给定两种分词结果“商品 和服 务”以及“商品 和 服务”，词典分词不知道哪种更加合理。

我们人类确知道第二种更加合理，只因为我们从小到大接触的都是第二种分词，出现的次数多，所以我们判定第二种是正确地选择。这就是利用了**统计自然语言处理**。统计自然语言处理的核心话题之一，就是如何利用统计手法对语言建模，这一章讲的就是二元语法的统计语言模型。



### 3.1 语言模型

1. **什么是语言模型**

   **模型**指的是对事物的数学抽象，那么**语言模型**指的就是对语言现象的数学抽象。准确的讲，给定一个句子 w，语言模型就是计算句子的出现概率 p(w) 的模型，而统计的对象就是人工标注而成的语料库。

   假设构建如下的小型语料库：

   ```
   商品 和 服务
   商品 和服 物美价廉
   服务 和 货币
   ```

   每个句子出现的概率都是 1/3，这就是语言模型。然而 p(w) 的计算非常难：句子数量无穷无尽，无法枚举。即便是大型语料库，也只能“枚举”有限的数百万个句子。实际遇到的句子大部分都在语料库之外，意味着它们的概率都被当作0，这种现象被称为**数据稀疏**。

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_18-22-57.png)

   然而随着句子长度的增大，语言模型会遇到如下两个问题。

   - **数据稀疏**，指的是长度越大的句子越难出现，可能统计不到频次，导致 ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_17-57-48.png)，比如 p(商品 和 货币)=0。
- **计算代价大**，k 越大，需要存储的 p 就越多，即便用上字典树索引，依然代价不菲。
   

   
2. **马尔可夫链与二元语法**

   为了解决以上两个问题，需要使用**马尔可夫假设**来简化语言模型，给定时间线上有一串事件顺序发生，假设每个事件的发生概率只取决于前一个事件，那么这串事件构成的因果链被称作**马尔可夫链**。

   在语言模型中，第 t 个事件指的是 Wt 作为第 t 个单词出现。也就是说，每个单词出现的概率只取决于前一个单词：

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_17-59-12.png)

   

   基于此假设，式子一下子变短了不少，此时的语言模型称为**二元语法模型**：

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_10-57-30.png)

   

   由于语料库中二元连续的重复程度要高于整个句子的重要程度，所以缓解了数据稀疏的问题，另外二元连续的总数量远远小于句子的数量，存储和查询也得到了解决。

   

3. **n元语法**

   利用类似的思路，可以得到**n元语法**的定义：每个单词的概率仅取决于该单词之前的 n 个单词：

   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_18-1-47.png)

   

   特别地，当 n=1 时的 n 元语法称为**一元语法** ( unigram);当 n=3 时的 n 元语法称为**三元语法**(tigam); n≥4时数据稀疏和计算代价又变得显著起来，实际工程中几乎不使用。

   

4. **数据稀疏与平滑策略**

   对于 n 元语法模型，n 越大，数据稀疏问题越严峻。比如上述语料库中“商品 货币”的频次就为0。一个自然而然的解决方案就是利用低阶 n 元语法平滑高阶 n 元语法，所谓**平滑**，就是字面上的意思：使 n 元语法频次的折线平滑为曲线。最简单的一种是线性插值法：
   
   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_11-17-22.png)
   
   

   其中，λ∈(0,1)为常数平滑因子。通俗理解，线性插值就是劫富济贫的税赋制度，其中的 λ 就是个人所得税的税率。![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_18-11-13.png) 是税前所得，P(Wt) 是社会福利。 通过缴税，高收人(高概率)二元语法的一部分收人 (概率)被移动到社会福利中。而零收入(语料库统计不到频次)的一元语法能够从社会福利中取得点低保金， 不至于饿死。低保金的额度与二元语法挣钱潜力成正比:二元语法中第二个词词频越高，它未来被统计到的概率也应该越高，因此它应该多拿一点。
   
   类似地，一元语法也可以通过线性插值来平滑：
   
   ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_11-21-45.png)
   
   
   
   其中，N 是语料库总词频。



### 3.2 中文分词语料库

语言模型只是一个函数的骨架，函数的参数需要在语料库上统计才能得到。为了满足实际工程需要，一个质量高、分量足的语料库必不可少。以下是常用的语料库：

- 《人民日报》语料库 PKU
- 微软亚洲研究院语料库 MSR
- 香港城市大学 CITYU(繁体)
- 台湾中央研究院 AS(繁体)

| 语料库 | 字符数 | 词语种数 | 总词频 | 平均词长 |
| ------ | ------ | -------- | ------ | -------- |
| PKU    | 183万  | 6万      | 111万  | 1.6      |
| MSR    | 405万  | 9万      | 237万  | 1.7      |
| AS     | 837万  | 14万     | 545万  | 1.5      |
| CITYU  | 240万  | 7万      | 146万  | 1.7      |

一般采用MSR作为分词语料的首选，有以下原因：

- 标注一致性上MSR要优于PKU。
- 切分颗粒度上MSR要优于PKU，MSR的机构名称不予切分，而PKU拆开。
- MSR中姓名作为一个整体，更符合习惯。
- MSR量级是PKU的两倍。



### 3.3 训练与预测

训练指的是统计二元语法频次以及一元语法频次，有了频次，通过极大似然估计以及平滑策略，我们就可以估计任意句子的概率分布，即得到了语言模型。这里以二元语法为例：

这里我们选用上面自己构造的小型语料库：[data/dictionnary/my_cws_corpus.txt](https://github.com/NLP-LOVE/Introduction-NLP/blob/master/data/dictionnary/my_cws_corpus.txt)

代码请见：[code/ch03/ngram_segment.py](https://github.com/NLP-LOVE/Introduction-NLP/blob/master/code/ch03/my_cws_corpus.txt)

步骤如下：

- 加载语料库文件并进行词频统计。

- 对词频文件生成**词网**

  词网指的是句子中所有一元语法构成的网状结构，是HanLP工程上的概念。比如“商品和服务”这个句子，我们将句子中所有单词找出来，起始位置(offset)相同的单词写作一行：

  ```
  0:[ ]
  1:[商品]
  2:[]
  3:[和,和服]
  4:[服务]
  5:[务]
  6:[ ]
  ```

  其中收尾(行0和行6)分别对应起始和末尾。词网必须保证从起点出发的所有路径都会连通到钟点房。

  词网有一个极佳的**性质**：那就是第 i 行的词语 w 与第 i+len(w) 行的所有词语相连都能构成二元语法。

  ![](https://github.com/NLP-LOVE/Introduction-NLP/raw/master/img/2020-2-5_15-13-17.png)

- 词图上的**维特比算法**

  上述词图每条边以二元语法的概率作为距离，那么中文分词任务转换为有向无环图上的最长路径问题。再通过将浮点数乘法转化为负对数之间的加法，相应的最长路径转化为负对数的最短路径。使用维特比算法求解。

  这里仅作一下简述，详细过程参考书本第三章。

该模型代码输入是句子“货币和服务”，得到结果如下：

```
[' ', '货币', '和', '服务', ' ']
```

结果正确，可见我们的二元语法模型具备一定的泛化能力。



### 3.4 HanLP分词与用户词典的集成

词典往往廉价易得，资源丰富，利用统计模型的消歧能力，辅以用户词典处理新词，是提高分词器准确率的有效方式。HanLP支持 2 档用户词典优先级：

- **低优先级**：分词器首先在不考虑用户词典的情况下由统计模型预测分词结果，最后将该结果按照用户词典合并。默认低优先级。
- **高优先级**：分词器优先考虑用户词典，但具体实现由分词器子类自行决定。

**HanLP分词器简洁版**：

```python
from pyhanlp import *

ViterbiSegment = SafeJClass('com.hankcs.hanlp.seg.Viterbi.ViterbiSegment')

segment = ViterbiSegment()
sentence = "社会摇摆简称社会摇"
segment.enableCustomDictionary(False)
print("不挂载词典：", segment.seg(sentence))
CustomDictionary.insert("社会摇", "nz 100")
segment.enableCustomDictionary(True)
print("低优先级词典：", segment.seg(sentence))
segment.enableCustomDictionaryForcing(True)
print("高优先级词典：", segment.seg(sentence))
```

输出：

```
不挂载词典： [社会/n, 摇摆/v, 简称/v, 社会/n, 摇/v]
低优先级词典： [社会/n, 摇摆/v, 简称/v, 社会摇/nz]
高优先级词典： [社会摇/nz, 摆/v, 简称/v, 社会摇/nz]
```

可见，用户词典的高优先级未必是件好事，HanLP中的用户词典默认低优先级，做项目时请读者在理解上述说明的情况下根据实际需求自行开启高优先级。



### 3.5 二元语法与词典分词比较

按照NLP任务的一般流程，我们已经完成了语料标注和模型训练，现在来比较一下二元语法和词典分词的评测：

| 算法     | P     | R     | F1    | R(oov) | R(IV) |
| -------- | ----- | ----- | ----- | ------ | ----- |
| 最长匹配 | 89.41 | 94.64 | 91.95 | 2.58   | 97.14 |
| 二元语法 | 92.38 | 96.70 | 94.49 | 2.58   | 99.26 |

相较于词典分词，二元语法在精确度、召回率及IV召回率上全面胜出，最终F1值提高了 2.5%，成绩的提高主要受惠于消歧能力的提高。然而 OOV 召回依然是 n 元语法模型的硬伤，我们需要更强大的统计模型。
