机器读句子,第一件事不是懂意思,是拆结构。用一叠「谁由谁组成」的规则,一句话能拆成一棵树。 麻烦在于:同一串词常常能拆出不止一棵树。而且词一多,树的数量按卡特兰数往上翻。 这一节把这两件事都量出来。
这一节是第 10 章的转向。前面四节都在讲「把一串数变成另一串数」, 从这里开始,处理的对象变成「有结构的符号串」。 它也是 7.1 无监督学习的一个对照: 那套办法从数据里统计规律,这套办法是语法学家手写的规则。两条路后来合到了一起。
一、文法就是一串规则
先搭一个最小的中文文法。七条规则,八成的句子都能拆:
| 规则 | 读法 |
|---|---|
| S → NP VP | 句子=名词短语加动词短语 |
| NP → Det N | 名词短语=限定词加名词 |
| NP → N | 单个名词也算名词短语 |
| NP → NP PP | 名词短语后面还能挂介词短语 |
| VP → V NP | 动词短语=动词加宾语 |
| VP → VP PP | 动词短语后面也能挂介词短语 |
| PP → P NP | 介词短语=介词加名词短语 |
| 注意 NP 和 VP 的规则都是递归的:NP 里套 PP,PP 里又套 NP。 | |
| 递归让七条规则能描述无穷多句话,也埋下了下一段的麻烦。 | |
| 词典负责把词映射到词性。张三、猫、狗、望远镜都是 N,那只 是 Det,追、看见 是 V,用 是 P。 | |
| 一句话能不能解析,就是问:从第 1 个词到最后一个词,能不能推出一个 S。 |
二、一步步试:回溯解析
最直白的算法就是逐个断点试。要问「从第 4 个词到第 6 个词能不能当 NP」,就试遍每一种断开方式。 「张三 追 那只 猫」只有一棵树:
S
NP
N
张三
VP
V
追
NP
Det
那只
N
猫
这棵树把「谁在做什么」摆明了:结构就是意义。同一个意思换个说法,树就换了形状; 可只要树没变,核心关系就没变。 这就是句法分析的全部野心:把「意思」装进「结构」里。
三、同一串词,两棵树
换一句。「张三 看见 那只 狗 用 望远镜」——两棵树。
| 「用望远镜」挂在哪 | 意思 |
|---|---|
| 动词短语上 | 拿望远镜去看(修饰「看见」) |
| 名词短语上 | 被看见的是「带着望远镜的狗」 |
| 两棵树的词性序列一模一样,差别只在层次。 | |
| 这就是句法歧义:语法只说结构合法不合法,它不知道狗是什么。 | |
| 人靠常识一眼排除掉第二种。 | |
| 而机器要解决这个问题,就得请出语义知识——那正是第 12 章要讲的东西。 |
四、句子一长,树的数量按卡特兰数翻
在基础句后面一条一条加介词短语,把树全数出来:
| 词数 | 解析树棵数 | 不记忆的调用次数 | 记忆后 |
|---|---|---|---|
| 4 | 1 | 38 | 30 |
| 6 | 2 | 217 | 92 |
| 8 | 5 | 1101 | 205 |
| 10 | 14 | 5421 | 391 |
| 12 | 42 | 26614 | 690 |
| 树的棵数是 1、2、5、14、42——这个数列叫卡特兰数。它随词数指数增长,这就是长句歧义难处理的原因。 | |||
| 更要紧的是最后一列。同样把这棵树全找出来,不记忆要两万六千次调用,记忆只要六百九十次。 | |||
| 差了三十八倍,而结果完全一样。 | |||
| 原因很朴素:回溯会反复重算同一个子问题。「第 3 到第 5 个词能不能是 NP」被问了几十遍, | |||
| 每次从头再算。把答案记进一张表,就是动态规划。 |
五、这一节的位置
把三件事串起来:文法定义结构,回溯找结构,歧义说明结构不够用。 文法是人写的,这是它的长处也是它的短处。长处是精确、可解释; 短处是写不全——真实语言里的例外,写规则的人永远追不上。 后来的统计方法(11.2)换了个思路:不写规则,改从语料里数概率。 两种思路在 11.3 会合到一起——用概率给每种结构打分,再挑最高的那棵。
六、动手 Demo:一句话能画出几棵树,就看文法给不给它机会
四段:文法长什么样、一句话的树、歧义句的两棵树、树的数量与算法代价。 第 3 段和第 4 段是一对:一个说「能有多解」,一个说「多解有多贵」。 解析器是手写的回溯,没用什么库。树的缩进图直接打印出来, 这样「层次」这件事是看得见的,不是抽象的。
def go(cat, i, j): # 从第 i 个词到第 j 个词,能不能当 cat
key = (cat, i, j)
if key in memo:
return memo[key] # 问过就查表,别重算
out = []
if j - i == 1 and tags[i] == cat:
out.append((cat, [WORDS[i]])) # 一个词自己就是一个成分
for rhs in RULES.get(cat, []):
if len(rhs) == 1: # 一元规则:NP -> N 这种
if rhs[0] != cat:
for c in go(rhs[0], i, j):
out.append((cat, [c])) # 本层节点要套回去
continue
if j - i < 2: # 二元规则至少要覆盖两个词
continue
for k in range(i + 1, j): # 在哪里断开,逐个试
for a in go(rhs[0], i, k):
for b in go(rhs[1], k, j):
out.append((cat, [a, b]))
memo[key] = out
return out
实跑输出:
--------------------------------------------------------------------
一句话能画出几棵树,就看文法给不给它机会
--------------------------------------------------------------------
==============================================================
[1] 文法就是一串「谁由谁组成」的规则
七条规则:
S -> NP VP
NP -> Det N
NP -> N
NP -> NP PP
VP -> V NP
VP -> VP PP
PP -> P NP
词典把词映射到词性:
张三/N 猫/N 狗/N 望远镜/N
那只/Det 追/V 看见/V 用/P
**NP 是名词短语,VP 是动词短语,PP 是介词短语,S 是句子。**
**注意 NP 和 VP 的规则都是递归的——NP 里还能套 PP,PP 里还能套 NP。**
**递归是这套办法能描述无穷多句话的原因,也是下一段麻烦的来源。**
[2] 给一句话,枚举它能怎么拆
句子:「张三 追 那只 猫」
词性: N V Det N
解析树的棵数:1
S
NP
N
张三
VP
V
追
NP
Det
那只
N
猫
**树是从上往下读的:S 由 NP 和 VP 组成,NP 由「张三」一个词充当。**
**VP 又由动词和宾语组成——这棵树把「谁在做什么」摆明了:结构就是意义。**
**这棵树把「谁在做什么」摆明了:结构就是意义。**
[3] 换个句子,树就分叉了
句子:「张三 看见 那只 狗 用 望远镜」
词性: N V Det N P N
解析树的棵数:2
**这几棵树的读法不一样,区别全在「用望远镜」挂在哪:**
第 1 棵(挂在名词短语上):
S
NP
N
张三
VP
V
看见
NP
NP
Det
那只
N
狗
PP
P
用
NP
N
望远镜
第 2 棵(挂在动词短语上):
S
NP
N
张三
VP
VP
V
看见
NP
Det
那只
N
狗
PP
P
用
NP
N
望远镜
挂在动词短语上:**「用望远镜」修饰「看见」这个动作**——拿望远镜去看。
挂在名词短语上:**「用望远镜」修饰「那只狗」**——被看见的是「带着望远镜的狗」。
**同一串词,同一个词性序列,两种意思。这就是句法歧义。**
**人靠常识一眼就能排除第二种(狗不太可能被望远镜修饰),**
**可文法本身只管结构合不合法,它不知道狗是什么。**
[4] 句子一长,树的数量就爆炸
在「张三 看见 那只 狗」后面一条一条加介词短语,
把解析树全部枚举出来,同时数一遍「算」的代价:
词数 解析树棵数 不记忆的调用次数 记忆后
4 1 38 30
6 2 217 92
8 5 1101 205
10 14 5421 391
12 42 26614 690
**树的棵数是 1、2、5、14、42——这个数列叫卡特兰数。**
**每多加一条短语,能拆出来的结构就多一批,长句的歧义就是这么来的。**
**再看调用次数那一列:不记忆的一路往上冲,记忆的慢得多。**
**因为回溯会反复重算同一个子问题——第 3 到第 5 个词能不能是 NP,被问了几十遍。**
**把答案记下来,就是动态规划。11.3 要讲的 CYK 算法,本质就是这张记忆表。**
四段连起来:**文法告诉你句子能怎么拆**;**一句话常常能拆出不止一棵树**;
**词一多树就指数增长**;**而重复的子问题让「把中间结果记住」变成必需。**
第 2 段那棵树要竖着读。每一层缩进代表一层结构, NP 底下是 N,N 底下才是具体的词——结构是夹在词和句之间的那一层。 第 3 段那两棵树值得对着看:叶子顺序完全相同,只是挂的层不同。 这就是「结构的差别可以不带一个字的差别」。 第 4 段那张表的第三列最有说服力:38、217、1101、5421、26614。 而第四列是 30、92、205、391、690。同一个答案,两条代价曲线差着数量级。 最后那句「CYK 本质就是这张记忆表」不是比喻。11.3 会把这张表画出来, 你会看到它就是一个二维数组。
30 秒实验
- 把第 3 段的句子改成「用 望远镜 张三 看见 那只 狗」(介词短语挪到句首),重跑。 树会只剩一棵——位置一换,能挂的地方就少了。
- 往文法里加一条
VP → V NP PP,重跑第 3 段。 树的数量会变——规则多一条,可能的层次就多一批,这也说明「文法写多写少」直接影响歧义。 - 把第 4 段的句子加到 16 个词,重跑。 不记忆那一列会涨到几十万——指数增长在前几步看着温和,再走两步就上天了。
常见误区
- 以为「解析就是找一棵树」。真实句子常常有几十棵树, 解析器真正要做的是「给这些树排序」,挑出最可能的那棵。
- 以为「歧义是句子写得不好」。歧义是语言的常态, Demo 第 3 段那句话,母语者天天在说,一点不别扭。
- 以为「文法规则写得越细越好」。规则越多,覆盖越窄; 真实语料里的口语、错别字、网络新词,规则永远追不上。
- 以为「记忆化只是优化,不影响结果」。对, 不影响结果——可它把代价从指数压到多项式,这就从「不可用」变成了「可用」。
自测三问(点开对答案)
1. 为什么「张三 看见 那只 狗 用 望远镜」会有两棵树? —— 因为**介词短语可以挂在两个不同的层次上**:挂在 VP 上(VP → VP PP), **「用望远镜」修饰「看见」;或者挂在宾语的名词短语上(NP → NP PP),修饰「狗」。** 两棵树的词性序列完全一样,**差别只在挂的层次。** 2. 为什么树的数量是 1、2、5、14、42? —— 因为**每加一条介词短语,就多一个「挂在哪一层」的选择,而这些选择会互相组合**。 这个数列叫**卡特兰数**,是「n 对括号有多少种合法嵌套」的答案。 **长句歧义之所以难,就是因为候选数量按这个速度涨。** 3. 记忆化为什么能把代价压下来? —— 因为**回溯会把同一个子问题反复重算**。 Demo 第 4 段:12 个词时**不记忆要 26614 次调用,记忆只要 690 次**,结果完全一样。 **子问题的种类数是多项式的(三个下标),所以记住之后总代价也就是多项式的。**下一节换一条路:不写规则,改数概率。 看看一个词的意思能不能只从「它跟谁一起出现」里学出来。 这条路不需要语法学家,只需要一大堆文本。
- ☐ (本节完,下一节 11.2 知识获取和统计语义学)