对应:Stanford CS224N Winter 2025 Lecture 4(Dependency Parsing);Winter 2026 作业 2 的编程部分;Manning 主讲的 Winter 2021 / 2023 版第 4 讲(Syntactic Structure and Dependency Parsing),录像在 YouTube 上。 配套代码:
code/lecture04/预计阅读时间:3.5–4.5 小时(第一遍通读 1.5 小时,第二遍手算转移序列、对照代码 2–3 小时)
0. 本讲导航
0.1 学完本讲,你应该能够
- 说明为什么理解句子需要知道它的结构,区分成分句法和依存句法。
- 举出介词短语附着、并列范围、修饰语范围等结构歧义的例子,解释为什么歧义数量随句子长度指数增长。
- 读懂依存树:中心词、依存词、依存标签、ROOT;判断一棵树是否合法、是否投射。
- 说出依存分析的几类方法,解释基于转移的方法为什么快。
- 手工执行 arc-standard 转移系统(SHIFT、LEFT-ARC、RIGHT-ARC),写出一个句子的完整转移序列;解释 oracle 如何从树库生成训练数据。
- 说明传统特征方法的三个问题(稀疏、不完整、计算慢)。
- 描述 Chen & Manning(2014)神经依存分析器的结构:48 个特征、嵌入、隐藏层、softmax。
- 计算 UAS 和 LAS;解释固定特征窗口的局限以及后来的改进方向。
0.2 先修知识
| 知识点 | 用在哪里 | 最低要求 |
|---|---|---|
| 第 3 讲 | 第 7、8 节 | 窗口分类器、softmax、交叉熵、PyTorch 训练循环 |
| 栈(stack) | 第 6 节 | 知道栈是”后进先出”:只能在栈顶放入和取出。不熟悉也没关系,第 6 节会解释 |
| 语法术语 | 全讲 | 不需要。主语、宾语、介词等概念出现时会用例子说明 |
0.3 本讲地图
第1节 为什么要分析句子结构?
第2节 两种观点:成分句法与依存句法
第3节 结构歧义:为什么分析难 ← 代码 01 演示 3
第4节 依存语法与依存树、树库 ← 代码 01
第5节 依存分析的几类方法
第6节 基于转移的分析:arc-standard 系统与 oracle ← 代码 02(本讲重点之一)
第7节 每一步怎么决策?传统的特征方法 ← 代码 03
第8节 神经依存分析器(Chen & Manning 2014) ← 代码 04(本讲重点之二)
第9节 评估:UAS 与 LAS
第10节 句法分析在大模型时代还有用吗?
第11–15节 小结、与作业的关系、练习、延伸阅读、下一讲预告
本讲的核心思路是一个漂亮的”化归”:把”找出一棵树”这个结构预测问题,变成”一连串分类”的问题。而分类正是第 3 讲刚刚学会的东西。
1. 为什么要分析句子结构?
人类用语言传达复杂的意思,靠的是把词组合成更大的单位:词组成短语,短语组成句子。要理解一个句子,必须知道哪些词和哪些词组合在一起、谁修饰谁。
一个例子(来自 Manning 的课件):
Scientists count whales from space.
是”科学家从太空中数鲸鱼”(from space 修饰 count,比如用卫星图像),还是”科学家数来自太空的鲸鱼”(from space 修饰 whales)?人一眼就知道是前者,但这需要世界知识:鲸鱼不会来自太空。这两种理解对应两种不同的句法结构。
句法结构在 NLP 中有很多直接用途:
- 信息抽取 / 关系抽取:从生物医学文献中抽取”蛋白质 A 与蛋白质 B 相互作用”这样的关系。依存树上,“interacts”连着两个蛋白质名的路径,比它们在句子中的线性距离可靠得多;
- 问答:“谁收购了谁?“需要知道动词的主语和宾语分别是什么;
- 语义角色标注、机器翻译中的语序调整、语法检查等。
在大语言模型出现之前,句法分析是几乎所有 NLP 管线中的一个基础模块。今天它不再总是显式地出现,但”模型内部是否学到了句法”仍然是研究的热点(第 10 节)。
2. 两种观点:成分句法与依存句法
2.1 成分句法(短语结构)
成分句法(constituency grammar,也叫短语结构语法,phrase-structure grammar)认为:句子由嵌套的短语(成分)组成。
- 词有词性:the(限定词 Det)、cat(名词 N)、by(介词 P)、large(形容词 Adj)、door(名词 N);
- 词组成短语:the cat 是一个名词短语(NP),by the large door 是一个介词短语(PP);
- 短语再组成更大的短语:the cat by the large door 也是一个名词短语。
这些组合规则可以写成上下文无关文法(CFG):
NP → Det (Adj)* N (PP)
PP → P NP
VP → V NP (PP)
S → NP VP
成分句法分析的结果是一棵短语结构树,叶子是词,内部节点是短语类别(NP、VP、PP……)。乔姆斯基的生成语法属于这个传统。
2.2 依存句法
依存句法(dependency grammar)不讲短语,只讲词与词之间的关系:每个词都依附于(depends on)另一个词,或者说被另一个词支配。
Look in the large crate in the kitchen by the door
- in the large crate 修饰 look(“往大箱子里看”)。按本讲采用的约定(第 4.2 节),短语中的实词 crate 依附于 look,介词 in 依附于 crate;
- the、large 修饰 crate,所以依附于 crate;
- in the kitchen 修饰 crate(“厨房里的那个箱子”),所以 kitchen 依附于 crate;
- by the door 修饰 kitchen 还是 crate?这又是一个歧义。
依存关系是不对称的:一端是中心词(head,也叫支配词,governor),另一端是依存词(dependent,也叫修饰词)。我们用一个箭头表示:本讲约定从中心词指向依存词(不同的书约定不同,这一点要注意)。箭头上通常还有一个标签,说明关系的类型(主语 nsubj、宾语 obj、限定词 det……)。
2.3 为什么 NLP 更常用依存句法?
两种表示可以互相转换(大致上),各有长处。近二十年来 NLP 中依存句法用得更多:
- 更直接地表达了”谁和谁有关系”:关系抽取、问答等应用要的正是这个;
- 对语序自由的语言更友好:俄语、捷克语、土耳其语等语言中,词序比较灵活,短语往往不连续,用短语结构表示很别扭,依存关系则不受影响;
- 分析算法简单高效(第 5、6 节);
- 有统一的多语言标准:Universal Dependencies(第 4.5 节)。
3. 结构歧义:为什么分析难
3.1 介词短语附着
最常见的结构歧义是介词短语附着(prepositional phrase attachment):一个介词短语可以修饰前面的动词,也可以修饰前面的某个名词。
| 句子 | 两种理解 |
|---|---|
| Scientists count whales from space | 从太空中数(修饰 count)/来自太空的鲸鱼(修饰 whales) |
| San Jose cops kill man with knife | 警察用刀杀了人/警察杀了一个持刀的人 |
| I saw the man with a telescope | 我用望远镜看到他/我看到一个拿着望远镜的人 |
| 我们讨论了关于环境的问题 | 中文里修饰语通常放在前面,这类歧义的形式不同,但”修饰谁”的问题同样存在 |
(前两个例子是 Manning 课件里的真实新闻标题。)
歧义的数量会爆炸。 如果一个动词和它的宾语后面跟着 个介词短语,每个介词短语都可以修饰动词、宾语,或者它前面任何一个介词短语里的名词——但修饰关系不能交叉(第 4.4 节的”投射性”)。满足这些条件的附着方式有多少种?
动手:代码
01_dependency_trees.py演示 3 暴力枚举了所有合法的附着方式:
介词短语个数 1 2 3 4 5 6 7 8 附着方式的数目 2 5 14 42 132 429 1430 4862 恰好是卡塔兰数 !
卡塔兰数 大约以 的速度增长。Manning 课件里有一个真实的句子:
The board approved [its acquisition] [by Royal Trustco Ltd.] [of Toronto] [for $27 a share] [at its monthly meeting].
它有 4 个介词短语,光是附着方式就有几十种可能,人却能一眼看出唯一合理的那种:by Royal Trustco 修饰 acquisition,of Toronto 修饰 Royal Trustco,for $27 a share 修饰 acquisition,at its monthly meeting 修饰 approved。
3.2 其他结构歧义
| 歧义类型 | 例子 | 两种理解 |
|---|---|---|
| 并列范围 | Shuttle veteran and longtime NASA executive Fred Gregory appointed to board | 一个人(既是航天老兵又是 NASA 高管的 Fred Gregory)/两个人 |
| 形容词修饰范围 | Students get first hand job experience | first-hand(亲身的)修饰 experience/另一种不雅的理解 |
| 动词短语附着 | Mutilated body washes up on Rio beach to be used for Olympics beach volleyball | to be used… 修饰 beach(这片海滩要用于奥运会)/修饰 body |
(这些也都是 Manning 课件里的真实新闻标题,正是因为歧义而显得滑稽。)中文的例子也很多,比如”我们三个人一组”(“我们”是三个人/每三个人一组)、“发现了敌人的哨兵”(哨兵属于敌人/发现了敌人和哨兵?——后者其实不太通顺,但计算机不知道)。
3.3 这对分析器意味着什么?
- 不可能枚举所有可能的结构再挑一个:数量是指数级的。
- 消歧需要词汇知识和世界知识:knife 是工具、cheese 是食物配料、鲸鱼不来自太空。
- 所以一个好的分析器需要:一个高效的搜索方法(第 5、6 节),加上一个懂词义的打分模型(第 7、8 节)。
4. 依存语法与依存树、树库
4.1 一点历史
依存的思想非常古老:公元前 5 世纪前后,古印度语法学家帕尼尼(Pāṇini)的梵语语法就是按词与词之间的关系来组织的;中世纪阿拉伯语法学家也有类似的分析。现代依存语法通常追溯到法国语言学家吕西安·泰尼埃(Lucien Tesnière)1959 年的著作。相比之下,短语结构语法是 20 世纪中叶才流行起来的。
4.2 一棵依存树
依存句法分析的输出是一棵依存树:句子中每个词恰好有一个中心词;整句话的中心(通常是主动词)依附于一个虚拟的 ROOT 节点。
动手:代码 01 演示 1 画出了两个只差一个词的句子:
┌──────────────punct───────────────┐
├────────────obl─────────────┐ │
┌─────root──────┬───obj────┐ ┌──case────┤ │
│ ┌─det─┬────┤ ┌─det─┤ │ ┌─det─┤ │
│ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
ROOT the chef cut the bread with a knife .
(每条折线从中心词出发,箭头 ▼ 指向依存词;chef 依附于 cut 的那条短弧没有写标签,它是 nsubj。)knife 是切的工具,依附于动词 cut(标签 obl);如果把 knife 换成 cheese,cheese 就依附于名词 bread(标签 nmod)。
也可以用表格表示(类似 CoNLL-U 格式):每行一个词,列出它的编号、词、词性、中心词的编号、依存标签。
| 编号 | 词 | 词性 | 中心词 | 标签 |
|---|---|---|---|---|
| 1 | the | DET | 2 | det |
| 2 | chef | NOUN | 3 | nsubj |
| 3 | cut | VERB | 0 | root |
| 4 | the | DET | 5 | det |
| 5 | bread | NOUN | 3 | obj |
| 6 | with | ADP | 8 | case |
| 7 | a | DET | 8 | det |
| 8 | knife | NOUN | 3 | obl |
| 9 | . | PUNCT | 3 | punct |
注意第 6 行:介词 with 依附于名词 knife,而不是反过来。这是 Universal Dependencies 的约定——实词做中心词,介词、限定词等虚词作为依存成分挂在实词上。这样做的好处是不同语言之间更一致(很多语言用格变化而不是介词来表达同样的意思)。
4.3 合法的依存树
一组依存关系构成一棵合法的依存树,需要满足:
- 每个词恰好有一个中心词(单一中心);
- 只有一个词依附于 ROOT;
- 无环且连通:从任何一个词出发,沿着中心词一路往上走,最终都能到达 ROOT。
这三条保证了它是一棵以 ROOT 为根的树。代码 01 演示 2 的 check_tree 实现了这些检查。
4.4 投射性
把所有依存弧画在句子上方,如果可以画得一条线都不交叉,这棵树就是投射的(projective)。等价地说:每个词和它的所有后代在句子里构成连续的一段。
大多数英语句子的依存树是投射的。非投射的结构主要来自”移位”,比如 Manning 课件里的例子:
a hearing is scheduled on the issue today.
on the issue 修饰的是 hearing(“关于这个议题的听证会”),但它被放到了动词后面,于是 hearing → issue 这条弧跨过了 scheduled,和 ROOT → scheduled 的弧交叉。
动手:代码 01 演示 2 画出了这个句子,可以看到交叉的弧;
crossing_arcs找出了 3 对交叉的弧。
在德语、荷兰语、捷克语等语序较自由的语言中,非投射结构更常见。第 6 节的 arc-standard 系统只能产生投射树,这是它的一个局限(代码 02 演示 4)。
4.5 树库
怎样得到一个依存分析器?一个办法是让语言学家手写语法规则(第 1 讲的规则时代)。1990 年代以来的主流做法是:先标注一批句子(树库),再从树库中学习。
最著名的依存树库项目是 Universal Dependencies(UD):用一套统一的标注规范,为一百多种语言标注了树库。
为什么”标注树库”比”写语法”更好?看起来标注树库比写一套语法慢多了,但它有很多好处:
- 可复用:同一个树库可以用来训练词性标注器、句法分析器、做语言学研究……;
- 覆盖面广:标注的是真实文本,包括各种语法书里没写到的现象,而不是语言学家头脑中的”理想语言”;
- 提供了频率信息:哪些结构常见、哪些罕见——这正是统计和神经方法需要的;
- 提供了评估标准:可以客观地比较不同的分析器。
动手:本讲的代码用
treebank.py按一套小语法生成了一个迷你树库(代码 01 演示 4):训练集 3000 句、开发集 500 句、测试集 500 句,平均每句 13.5 个词,13 种依存标签。介词短语的附着由词义决定:with + 工具(knife、fork……)修饰动词,with + 配料(cheese、sauce……)修饰食物名词,with + 衣饰(hat、scarf……)修饰人,in + 地点修饰动词,near + 地点修饰名词。所有树都是合法、投射的。它比真实的树库整齐得多,但足以展示分析器的所有关键部件。
5. 依存分析的几类方法
依存分析的任务:输入一个句子 ,为每个词 选一个中心词( 中的一个,0 是 ROOT)和一个标签,使结果是一棵(投射的)树。
做决策时,哪些信息有用?Manning 总结过几种:
- 词与词的亲和性:discussion → issues 是很合理的依存关系(“关于问题的讨论”);
- 依存距离:大多数依存关系发生在相邻或很近的词之间;
- 中间的内容:依存关系很少跨过动词或标点;
- 中心词的”配价”(valency):一个词通常在哪一侧带几个依存成分(限定词通常在名词左边、及物动词通常带一个宾语)。
主要的分析方法有:
| 方法 | 思路 | 代表 | 复杂度 |
|---|---|---|---|
| 动态规划 | 类似成分句法的 CKY 算法,自底向上合并 | Eisner(1996) | |
| 基于图 | 给所有可能的 条依存弧打分,求得分最高的生成树 | MST Parser(McDonald 等,2005);后来的 biaffine 分析器 | ~ |
| 约束满足 | 先列出所有可能的弧,再用语法约束逐步删掉不合法的 | Karlsson(1990) | — |
| 基于转移 | 从左到右读句子,每一步用分类器选一个动作,贪心地建树 | MaltParser(Nivre,2003 起) |
基于转移的方法最快:每个词只处理常数次,分析一个句子只需要 次分类。2014 年之前,它的准确率略低于基于图的方法;Chen & Manning(2014)用神经网络大幅提升了它的准确率,同时保持了速度。下面重点讲它。
6. 基于转移的分析:arc-standard 系统
6.1 状态与动作
基于转移的分析器像一个从左到右读句子的人,手边有三样东西:
- 栈 (stack):已经读过、但还在等待找齐依存成分的词。初始时只有 ROOT。
- 缓冲区 (buffer):还没读的词。初始时是整个句子。
- 弧集合 :已经确定的依存关系。初始时为空。
(“栈”是一种只能在顶端放入和取出的数据结构,像一摞盘子。我们记栈顶为 ,下面一个为 ;缓冲区的第一个词为 。)
arc-standard 系统有三种动作:
| 动作 | 条件 | 效果 |
|---|---|---|
| SHIFT | 缓冲区非空 | 把 从缓冲区移到栈顶 |
| LEFT-ARC() | 栈中至少两个词, 不是 ROOT | 加一条弧 ( 依附于 ),把 从栈中移除 |
| RIGHT-ARC() | 栈中至少两个词 | 加一条弧 ( 依附于 ),把 从栈中移除 |
形式化地写( 表示栈顶是 , 表示缓冲区开头是 ):
当缓冲区为空、栈里只剩 ROOT 时,分析结束, 就是结果。
记忆方法:LEFT-ARC 的箭头指向左边(依存词在左),RIGHT-ARC 的箭头指向右边(依存词在右);无论哪种,依存词都会被从栈中移除——它已经找到了自己的中心词,而且(在 arc-standard 中)它的依存成分也必须已经全部找齐了。
6.2 一个完整的例子:I ate fish
动手:代码
02_arc_standard.py演示 1(CS224N 课件的例子):
| 步 | 栈 | 缓冲区 | 动作 | 新增的弧 |
|---|---|---|---|---|
| 0 | [ROOT] | [I, ate, fish] | ||
| 1 | [ROOT, I] | [ate, fish] | SHIFT | |
| 2 | [ROOT, I, ate] | [fish] | SHIFT | |
| 3 | [ROOT, ate] | [fish] | LEFT-ARC(nsubj) | ate → I |
| 4 | [ROOT, ate, fish] | [] | SHIFT | |
| 5 | [ROOT, ate] | [] | RIGHT-ARC(obj) | ate → fish |
| 6 | [ROOT] | [] | RIGHT-ARC(root) | ROOT → ate |
3 个词,恰好 6 步。一般地, 个词的句子恰好需要 步:每个词被 SHIFT 一次(进栈),又被某个 ARC 移除一次(找到中心词)。所以分析的时间是线性的——这就是基于转移的方法快的原因。
6.3 Oracle:从树库生成训练数据
给定一个句子和它的标准依存树,怎样知道每一步”正确”的动作是什么?这由一个叫 oracle(神谕)的简单规则给出(设栈顶为 、下面一个为 ):
- 如果标准答案中 依附于 → LEFT-ARC;
- 否则,如果 依附于 ,并且 的所有依存成分都已经连上了 → RIGHT-ARC;
- 否则 → SHIFT。
第 2 条中的”并且”非常关键:RIGHT-ARC 会把 移出栈,之后就再也不能给它连依存成分了。
动手:代码 02 演示 2 比较了两个句子:
- she ate the pizza with the fork:栈是 [ROOT, ate, pizza] 时,pizza 没有别的依存成分了,立刻 RIGHT-ARC(obj);之后 fork 只能依附于 ate。
- she ate the pizza with the cheese:栈同样是 [ROOT, ate, pizza],但 cheese 还要依附于 pizza,所以先 SHIFT,把 pizza 留在栈里,等 cheese 连上之后再 RIGHT-ARC。
关键在于:做决策的那一刻,fork / cheese 还在缓冲区里( 的位置)。分析器必须”往前看”几个词,并且知道 fork 是工具、cheese 是配料。
Oracle 把树库变成了分类器的训练数据:对每个训练句子,从初始状态出发,每一步问 oracle 正确的动作是什么,记下 (状态, 正确动作),然后执行这个动作,直到结束。一个 个词的句子产生 个训练样本。
动手:代码 02 演示 3 在全部 4000 个句子上验证了 oracle:每棵树都被精确重建(中心词和标签都对),每个句子都恰好 步。一共 54,118 个词、108,236 步,其中 SHIFT 54,118 次、LEFT-ARC 35,932 次、RIGHT-ARC 18,186 次。动作一共 27 类:1 个 SHIFT + 13 个 LEFT-ARC(标签) + 13 个 RIGHT-ARC(标签)。
于是,训练一个依存分析器 = 训练一个 27 类的分类器:输入是分析器的状态,输出是下一步的动作。
6.4 局限:只能产生投射树
动手:代码 02 演示 4 用 oracle 分析非投射的 hearing 句子,在栈为 [ROOT, scheduled, issue, today, .]、缓冲区为空时卡住了:hearing 早已被移出栈,hearing → issue 这条弧再也连不上了。
原因是 arc-standard 只在栈顶两个词之间建弧,而非投射的弧要求两个不相邻的词”跨过”中间已经处理好的结构相连。解决办法有:加一个交换栈顶两个词的 SWAP 动作(Nivre,2009)、“伪投射”(先把非投射树变成投射树来训练,分析后再还原)、或者改用基于图的分析器。
6.5 不学习的基线
动手:代码 02 演示 5 用几种”不学习”的打分方式分析测试集:
打分方式 UAS LAS 作弊:直接用 oracle 1.000 1.000 随机选一个合法动作 0.134 0.010 每个词依附于左边的词 0.000 0.000 每个词依附于右边的词 0.311 0.102 (UAS、LAS 的定义见第 9 节。)“依附于右边的词”对了不少,“依附于左边的词”一个都不对:在我们的语法中(英语也大致如此),限定词、形容词、介词、主语的中心词都在右边,而依附于左边的成分(宾语、介词短语、标点)往往和中心词之间隔着别的词。
7. 每一步怎么决策?传统的特征方法
7.1 贪心的转移分析器
有了 oracle 生成的训练数据,分析器的工作流程就是:
状态 = 初始状态
while 状态还没结束:
动作 = 分类器(状态) # 只在合法动作中选得分最高的
状态 = 执行(状态, 动作)
这是贪心的:每一步只选当前看起来最好的动作,一旦选错就无法回头。Nivre 的 MaltParser(2003 年起)就是这样工作的,速度非常快,准确率只比当时最好的基于图的分析器略低。改进的方法有:用束搜索(beam search)同时保留几个最好的部分分析(第 6 讲会在机器翻译中详细讲束搜索);以及动态 oracle(Goldberg & Nivre,2012):训练时故意让分析器走一些错误的路径,教它”在走错之后怎样尽量挽回”。
7.2 指示特征 + 线性分类器
2014 年之前,这个分类器通常是一个线性分类器(感知机、支持向量机、逻辑回归),输入是大量人工设计的指示特征(indicator features)。每个特征是一个”是 / 否”的条件,写成字符串:
s1.w=pizza 栈顶的词是 pizza
s1.t=NOUN|b1.t=ADP 栈顶是名词、缓冲区第一个是介词
s1.w|b3.w=pizza|cheese 栈顶是 pizza、缓冲区第三个是 cheese
s2.t|s1.t|b1.t=VERB|NOUN|ADP 三个位置的词性组合
这些特征由特征模板批量生成:比如模板” 的词 + 的词”会为每一对可能的词生成一个特征。一个状态通常”点亮”几十个特征,而特征的种类可以有几十万到上百万个。
为什么需要那么多组合特征? 因为线性分类器本身不会组合信息:它只会把各个特征的权重加起来。“栈顶是 pizza 并且 是 cheese → 应该 SHIFT”这种交互关系,必须由人显式地写成一个组合特征。
平均感知机是训练这类模型最简单的方法之一:每个特征对每个动作有一个权重,动作的得分是被点亮特征的权重之和;预测错了,就把正确动作的相关权重 +1、错误动作的 −1。“平均”指最终使用训练过程中所有时刻权重的平均值,而不是最后一刻的权重,这能减少震荡、提高泛化。
动手:代码
03_feature_based_parser.py实现了这样一个分析器,用 35 个特征模板(其中好几个是我专门为介词短语附着设计的”往前看”模板,例如 的词 + 的词)。在 3000 句上训练 5 轮(在编写讲义的电脑上约 8 秒),一共产生了 17,458 种特征。测试集上 UAS 0.994、LAS 0.994。整体分数很高,因为这份树库的大部分结构非常规则;几乎所有错误都出在介词短语附着上(测试集准确率 0.945)。而且错误集中在一类情况:
做附着决策时,介词后面的名词…… 附着准确率 在特征窗口里(“with the cheese”,cheese 在 ) 0.996(263 个) 不在特征窗口里(“with the fresh cheese”,cheese 在 ) 0.910(387 个) 名词不在窗口里时,分析器只能根据动词、宾语等其他线索去猜。
动手:代码 03 演示 3 列出了权重最大的几个” 的词 + 的词”特征。最支持”立刻 RIGHT-ARC(obj)“的是 pie|hammer、pizza|stick、salad|telescope……(b3 是工具);最支持”先 SHIFT”的是 fish|honey、bread|cream、pizza|garlic……(b3 是配料)。模型记住的是一个个具体的词对:它不知道 cheese、sauce、cream 是”同一类东西”,每个词对都必须在训练数据里单独出现过,权重才能学好。
7.3 特征方法的三个问题
Chen & Manning(2014)总结了传统特征方法的三个问题:
- 稀疏:特征太多,大多数特征只出现过几次,权重估计不准。代码 03 演示 4:17,458 种特征中有 2,505 种(14%)在训练集中只出现过 1~2 次。
- 不完整:模板是人想出来的,难免漏掉有用的组合;而且测试时总会遇到训练时没见过的特征(词对),只能忽略。每加一个组合模板,特征数就成倍增长,稀疏问题更严重。
- 计算慢:每个状态都要拼几十个字符串、查几十次哈希表。论文报告,在传统分析器中 95% 以上的分析时间花在计算特征上。
这三个问题的根源是同一个:词是孤立的符号——第 1 讲朴素贝叶斯遇到的老问题,在句法分析中又出现了。解决办法也是同一个:用稠密的向量表示词。
8. 神经依存分析器(Chen & Manning 2014)
8.1 想法:把一切都变成稠密向量
Chen 和 Manning(2014)的想法很直接:
- 每个词用一个 维向量表示(词嵌入,可以用 word2vec / GloVe 初始化);
- 每个词性也用一个 维向量表示——NN(单数名词)和 NNS(复数名词)的向量会很接近;
- 每个依存标签也用一个 维向量表示——amod(形容词修饰)和 num(数词修饰)的向量会很接近。
然后从分析器的状态中取出若干个位置,把这些位置上的词、词性、标签的向量拼接起来,送进一个神经网络分类器。这和第 3 讲的窗口分类器几乎一模一样,区别只是”窗口”换成了”栈和缓冲区里的若干个位置”。
8.2 特征:48 个位置
论文使用的特征:
- 18 个词:栈顶 3 个词 ;缓冲区前 3 个词 ;栈顶两个词各自的最左、最右、第二左、第二右的依存成分 ,以及最左依存成分的最左依存成分 、最右的最右 ()。。
- 18 个词性:上面 18 个位置上的词性。
- 12 个依存标签:上面后 12 个位置(依存成分)与其中心词之间的、分析器已经预测出来的标签。
空位置用一个特殊的 <NULL> 向量,没见过的词用 <UNK>。
8.3 网络结构
几点说明:
- 三张嵌入表(词、词性、标签)各自独立,但同一张表被所有位置共享: 的词和 的词查的是同一张词嵌入表。拼接之后,隐藏层的权重 对每个位置是不同的——模型知道一个词出现在 还是 。
- 立方激活函数 :论文的解释是, 展开后含有 这样的乘积项,相当于自动地组合了三个特征——这正是第 7 节人工设计的组合特征所做的事。后来的工作(以及作业 2)发现 ReLU 同样好用。
- 训练:用 oracle 生成的 (状态, 动作) 样本,最小化交叉熵。论文还用了 dropout 和 L2 正则。分析时只在合法的动作中选得分最高的。
动手:代码
04_neural_parser.py演示 1:我们的词表有 96 个词、12 个词性、15 个标签(含特殊符号),27 种动作;嵌入 50 维,拼接后 2400 维;隐藏层 200 个单元。共 491,777 个参数,其中 480,000 个在隐藏层的权重矩阵里。
8.4 结果
论文在英语宾州树库上报告:UAS 约 92%,比当时的 MaltParser 等转移分析器高约 2 个百分点,与最好的基于图的分析器相当;速度约每秒 1000 句,比基于图的分析器快得多。它证明了”稠密表示 + 简单的神经网络”可以同时赢得准确率和速度。
动手:代码 04 演示 2 在我们的树库上训练了神经分析器(立方激活,8 轮,在编写讲义的电脑上约 30 秒),并和代码 03 的感知机比较(测试集):
分析器 UAS LAS 介词短语:名词在窗口内 介词短语:名词在窗口外 平均感知机(35 个特征模板) 0.994 0.994 0.996 0.910 神经分析器(立方激活) 0.995 0.995 1.000 0.920 演示 3 换了激活函数:ReLU 0.996、tanh 0.995(各只训练 1 次,零点几个百分点的差别在随机波动范围内)。
在这份规则、词汇量很小的玩具树库上,两种分析器都接近满分,差别不大——感知机的特征模板是我针对这份树库专门设计的。神经分析器的优势在真实数据上才明显:词汇量大、罕见词多、需要组合的特征多,人工模板很难面面俱到,而神经网络能从数据中自动学到需要的组合。
8.5 分析器学到了什么样的词向量?
动手:代码 04 演示 5 查看了分析器的词嵌入(随机初始化,只为了做好句法分析而训练):
fork → spoon(0.99), camera(0.99), knife(0.99), hammer(0.99), stick(0.99) cheese → garlic(0.99), sauce(0.98), cream(0.98), honey(0.98), mushrooms(0.98) hat → umbrella(0.99), scarf(0.98), beard(0.98), badge(0.98), coat(0.98) kitchen → hall(0.97), park(0.97), garden(0.96), market(0.95), library(0.95)工具、配料、衣饰、地点四类词:同类词平均余弦 0.974,异类词平均余弦 0.164。
没有任何人告诉分析器”fork 和 knife 是一类”,它只是为了把介词短语挂对,就自己学出了这样的词向量。同类词向量相近,学到的知识就能在同类词之间共享——这正是稠密特征相对于稀疏指示特征(每个词对都要单独学)的根本优势。这也再次印证了第 1 讲的分布假说:fork 和 knife 在句法上的”行为”相同,所以向量相同。
8.6 特征窗口的局限
演示 2 中两种分析器的错误都集中在”名词在窗口外”的介词短语上。是模型不够聪明,还是信息不够?
动手:代码 04 演示 4 把缓冲区的第 4、5 个词和词性也加进特征(共 52 个特征):
特征 UAS 介词短语:名词在窗口内 名词在窗口外 标准() 0.995 1.000 0.920 加上 、 1.000 1.000 1.000 (我们的语法中,介词短语最多有两个形容词,名词最远在 ,所以多看两个词就够了。)
演示 6 的错误分析也印证了这一点:错误的例子都是”with a happy knife""with the young hungry honey”这种名词被形容词推到窗口外的情况。
原来的错误不是因为模型不够聪明,而是特征窗口里根本没有做决策所需的信息。 但无论窗口开多大,总有更长的句子。真正的解决办法是让每个位置的表示都包含整句话的信息。后来的分析器正是这样做的:
- Kiperwasser & Goldberg(2016):先用双向 LSTM(第 6 讲)读完整个句子,每个词得到一个包含上下文信息的向量,分析器只用栈和缓冲区里少数几个位置的 LSTM 向量作为特征;
- Dozat & Manning(2017):基于图的 biaffine 分析器,也用双向 LSTM 编码句子,再给所有可能的依存弧打分;它在很长一段时间里是最准确的分析器之一,斯坦福的 Stanza 工具包就用它;
- Weiss 等(2015)、Andor 等(2016,Google 的 SyntaxNet):更大的网络、束搜索、全局归一化;
- 今天的分析器通常在预训练的 Transformer(如 BERT)之上做依存分析。
9. 评估:UAS 与 LAS
依存分析的标准评估指标非常简单:
- UAS(Unlabeled Attachment Score,无标签附着准确率):中心词预测正确的词所占的比例;
- LAS(Labeled Attachment Score,带标签附着准确率):中心词和依存标签都预测正确的词所占的比例。
显然 LAS ≤ UAS。
例子:句子 “ROOT She saw the video lecture”(5 个词),标准答案和预测如下:
| 词 | 标准中心词 | 标准标签 | 预测中心词 | 预测标签 | 中心词对? | 都对? |
|---|---|---|---|---|---|---|
| She | saw | nsubj | saw | nsubj | ✓ | ✓ |
| saw | ROOT | root | ROOT | root | ✓ | ✓ |
| the | lecture | det | video | det | ✗ | ✗ |
| video | lecture | compound | lecture | compound | ✓ | ✓ |
| lecture | saw | obj | saw | nsubj | ✓ | ✗ |
UAS = 4/5 = 80%,LAS = 3/5 = 60%。
两个实践中的细节:很多英文评估不统计标点(标点的中心词约定比较随意);而且 UAS / LAS 是按词平均的,长句子的权重更大。我们的代码统计了所有词(包括标点)。
10. 句法分析在大模型时代还有用吗?
今天的大语言模型不需要显式的句法分析也能很好地理解和生成语言。那么句法分析过时了吗?
模型内部学到了句法。 Hewitt 和 Manning(2019)提出了”结构探针”(structural probe):在 BERT 的隐藏向量上学一个线性变换,使得变换后两个词向量的距离近似等于它们在依存树上的距离。结果发现这样的线性变换确实存在——BERT 的表示空间中隐含地编码了依存树。这说明”预测词”这个训练目标迫使模型学会了句法(第 17 讲讨论可解释性时会再谈)。
显式的句法仍然有用:
- 语言学研究:Universal Dependencies 让跨语言的类型学研究成为可能;
- 低资源语言:数据很少时,句法结构是有用的先验;
- 信息抽取管线:很多工业系统(spaCy、Stanza 等工具包)仍然在用依存分析抽取实体关系;
- 可解释性与可控性:句法结构给出了人类能理解的中间表示。
更重要的是,本讲的思想贯穿了整门课:把结构预测拆成一连串局部决策、用 oracle 生成训练数据、贪心与束搜索的权衡、固定窗口特征的局限……这些在后面的序列模型、机器翻译、甚至大语言模型的生成中都会反复出现。
11. 本讲小结
- 理解句子需要知道结构:哪些词组合在一起、谁修饰谁。成分句法用嵌套的短语表示结构,依存句法用词与词之间的有向关系表示结构。
- 结构歧义无处不在(介词短语附着、并列范围、修饰语范围),其数量随句子长度指数增长(卡塔兰数),消歧需要词汇和世界知识。
- 依存树:每个词恰好一个中心词,只有一个词依附于 ROOT,无环连通;没有交叉弧的叫投射树。UD 约定实词做中心词。树库是现代分析器的基础。
- 基于转移的分析:栈 + 缓冲区 + 三种动作(SHIFT、LEFT-ARC、RIGHT-ARC), 个词恰好 步,线性时间。oracle 把树库变成 (状态, 动作) 训练样本,分析器 = 分类器。arc-standard 只能产生投射树。
- 传统特征方法:指示特征 + 线性分类器(感知机等),问题是稀疏、不完整、计算慢。
- Chen & Manning(2014):48 个位置(18 词 + 18 词性 + 12 标签)的嵌入拼接 → 隐藏层(立方 / ReLU)→ softmax。稠密向量让同类词共享知识,又快又准。
- 评估:UAS(中心词对)、LAS(中心词和标签都对)。
- 局限:固定的特征窗口看不到远处的词;后来的分析器用双向 LSTM / Transformer 编码整句话。
12. 与作业的关系
作业 2 的编程部分(Neural Transition-Based Dependency Parsing)几乎就是本讲第 6、8 节的内容(以官方当年发布的版本为准):
PartialParse:实现 arc-standard 的状态和三种动作(__init__、parse_step、parse)。注意作业中的动作不带标签(只算 UAS),栈和缓冲区里存的是词本身。对应代码parsing.py的Configuration。minibatch_parse:同时分析一批句子,每一轮把所有未完成的状态交给模型批量预测,再各自执行动作,完成的句子移出。对应parsing.py的parse_batch。ParserModel:用 PyTorch 实现 Chen & Manning 的网络(作业版本用 ReLU、只用词和词性特征)。作业通常要求你自己实现嵌入查表和线性层(不直接用nn.Embedding、nn.Linear),以加深理解。对应代码 04 的ParserModel。- 训练循环:Adam 优化器、交叉熵损失、在开发集上选最好的模型;要求在开发集上达到一定的 UAS。
- 书面题:手工写出一个句子的转移序列;说明一个 个词的句子需要多少步();分析几种错误类型(介词短语附着错误、动词短语附着错误、修饰语附着错误、并列附着错误)并给出修正——第 3 节的歧义类型正是为此准备的。
13. 练习
练习 1(手写转移序列)★:用 arc-standard 系统分析 “ROOT I parsed this sentence correctly”,其中 parsed → I(nsubj)、parsed → sentence(obj)、sentence → this(det)、parsed → correctly(advmod)、ROOT → parsed(root)。写出每一步的栈、缓冲区、动作和新增的弧。
参考答案
| 步 | 栈 | 缓冲区 | 动作 | 新增的弧 |
|---|---|---|---|---|
| 0 | [ROOT] | [I, parsed, this, sentence, correctly] | ||
| 1 | [ROOT, I] | [parsed, this, sentence, correctly] | SHIFT | |
| 2 | [ROOT, I, parsed] | [this, sentence, correctly] | SHIFT | |
| 3 | [ROOT, parsed] | [this, sentence, correctly] | LEFT-ARC | parsed → I |
| 4 | [ROOT, parsed, this] | [sentence, correctly] | SHIFT | |
| 5 | [ROOT, parsed, this, sentence] | [correctly] | SHIFT | |
| 6 | [ROOT, parsed, sentence] | [correctly] | LEFT-ARC | sentence → this |
| 7 | [ROOT, parsed] | [correctly] | RIGHT-ARC | parsed → sentence |
| 8 | [ROOT, parsed, correctly] | [] | SHIFT | |
| 9 | [ROOT, parsed] | [] | RIGHT-ARC | parsed → correctly |
| 10 | [ROOT] | [] | RIGHT-ARC | ROOT → parsed |
5 个词,10 步。这是作业 2 书面部分的一道经典题。
练习 2(步数):证明 arc-standard 系统分析一个 个词的句子恰好需要 步。
参考答案
每个词恰好被 SHIFT 一次(从缓冲区进入栈)。每次 LEFT-ARC 或 RIGHT-ARC 恰好把一个词从栈中移除(并给它确定中心词),而每个词恰好有一个中心词,所以 ARC 动作恰好执行 次(ROOT 从不被移除)。总步数 。
练习 3(oracle 的条件)★:oracle 的 RIGHT-ARC 规则要求” 的所有依存成分都已经连上了”。如果去掉这个条件,分析 “she ate the pizza with cheese”(cheese → pizza)时会发生什么?
参考答案
当栈为 [ROOT, ate, pizza] 时,pizza 确实依附于 ate,没有这个条件的 oracle 会立刻 RIGHT-ARC,把 pizza 移出栈。之后 with、cheese 进栈,cheese 的中心词 pizza 已经不在栈里了,pizza → cheese 这条弧永远连不上,cheese 最后只能被错误地连到 ate 上。
练习 4(投射性):判断下面的依存树是否投射:词 1..5,中心词分别是 [2, 0, 5, 2, 2](即 1→2、2→ROOT、3→5、4→2、5→2)。
参考答案
弧(写成左端点 < 右端点):(1,2)、(0,2)、(3,5)、(2,4)、(2,5)。检查 (2,4) 和 (3,5):,交叉!所以不是投射的。直观上,3 依附于 5,但 3 和 5 之间的 4 依附于 2,“跨出”了 5 的子树。可以用代码 01 的 crossing_arcs([-1, 2, 0, 5, 2, 2]) 验证。
练习 5(卡塔兰数):一个动词 + 宾语后面跟着 3 个介词短语,有多少种合法的附着方式?列出 时的全部 5 种。
参考答案
时有 种。 时(V 动词、N 宾语、P1、P2 两个介词短语里的名词):P1 可以挂到 V 或 N,P2 可以挂到 V、N 或 P1,一共 种组合。其中”P1→V、P2→N”不合法:弧 V—P1 和 N—P2 满足 V < N < P1 < P2,交叉了。剩下 5 种合法的:(1) P1→V,P2→V;(2) P1→N,P2→V;(3) P1→N,P2→N;(4) P1→V,P2→P1;(5) P1→N,P2→P1。
练习 6(UAS / LAS):一个 10 个词的句子,分析器有 2 个词的中心词错了,另外有 1 个词中心词对但标签错了。UAS 和 LAS 各是多少?
参考答案
UAS = 8/10 = 80%;LAS = 7/10 = 70%(中心词错的 2 个词无论标签如何都算错,再加上 1 个标签错的)。
练习 7(参数量):Chen & Manning 的分析器,词表 4 万个词、45 个词性、40 个标签,嵌入 50 维,隐藏层 200 个单元,动作 81 种(1 + 40 + 40)。估算各部分的参数量。哪部分最大?
参考答案
词嵌入 万;词性嵌入 ;标签嵌入 ;隐藏层 万;输出层 万。词嵌入最大(真实场景词表大);在我们的玩具树库里词表只有 96 个词,所以隐藏层最大(代码 04 演示 1)。
练习 8(立方激活函数)★:设隐藏单元 (省略偏置)。展开后有哪些项?解释为什么说立方函数”自动组合了特征”。
参考答案
展开后有 、、 等项,例如系数为 的 。如果 、、 分别来自” 的词"" 的词"" 的词”的嵌入维度,那么 这一项只有当三者同时取较大的值时才大——这正是” 是 X 且 是 Y 且 是 Z”这种三元组合特征的连续版本。线性函数 + ReLU 没有显式的乘积项,但多层或足够宽的网络同样能近似这种交互。
练习 9(动手):
(a) 在 treebank.py 中把介词短语里形容词的个数上限从 2 改成 4,重新运行代码 03、04。“名词在窗口外”的比例和准确率如何变化?代码 04 演示 4 中”加上 、“还能达到 1.000 吗?
(b) 在代码 04 中把词嵌入维度改成 10 和 100,结果如何变化?
(c) 把代码 04 的训练数据减少到 300 句,再和感知机(同样 300 句)比较。在数据少的时候,谁更好?
(d) 修改 parsing.py 中的 oracle,去掉 RIGHT-ARC 的”所有依存成分都已连上”条件,再运行代码 02 演示 3,看会发生什么。
练习 10(思考):第 8.6 节说”窗口总是有限的”。除了用双向 LSTM 编码整句话,你还能想到什么办法,让分析器在做介词短语附着决策时”看到”介词短语里的名词?(提示:想想能不能改变动作的设计,或者延迟决策。)
参考答案
一种思路是改用 arc-eager 转移系统(Nivre 2003):它的 RIGHT-ARC 在依存词刚进栈时就建弧、但不把依存词移出栈,另有一个 REDUCE 动作在之后弹出。在 arc-eager 中,宾语名词被连到动词上之后仍留在栈里,介词短语的名词可以在完全读入之后再决定挂到栈里的哪个词上。另一种思路是用基于图的分析器,它同时给所有可能的弧打分,不存在”必须在看到名词之前做决定”的问题。当然,最根本的还是让特征包含更多上下文。
14. 延伸阅读
- CS224N 课程笔记 Part IV(Dependency Parsing)。
- Chen & Manning,A Fast and Accurate Dependency Parser using Neural Networks(EMNLP 2014):本讲第 8 节,也是作业 2 的基础。
- Nivre,Incrementality in Deterministic Dependency Parsing(2004);Nivre,Algorithms for Deterministic Incremental Dependency Parsing(2008):转移系统的系统介绍。
- Kübler, McDonald & Nivre,Dependency Parsing(2009,综述性小书)。
- Jurafsky & Martin,Speech and Language Processing 第 3 版,第 18 章(Dependency Parsing)。
- Kiperwasser & Goldberg,Simple and Accurate Dependency Parsing Using Bidirectional LSTM Feature Representations(2016);Dozat & Manning,Deep Biaffine Attention for Neural Dependency Parsing(2017)。
- Hewitt & Manning,A Structural Probe for Finding Syntax in Word Representations(2019)。
- Universal Dependencies 项目主页(universaldependencies.org):标注规范和一百多种语言的树库。
15. 下一讲预告
本讲的分析器有一个明显的局限:特征窗口是固定的,看不到远处的词。下一讲(语言模型与循环神经网络)会处理一个更基本的问题:
- 什么是语言模型?为什么”预测下一个词”这么重要?
- n-gram 语言模型:用计数估计概率,以及它的稀疏性问题;
- 固定窗口的神经语言模型:和本讲、第 3 讲的窗口分类器同一个思路,问题也一样——窗口有限;
- 循环神经网络(RNN):用同一组参数一个词一个词地读下去,理论上能记住任意长的历史;
- 困惑度:怎样评估语言模型;以及 RNN 能不能真的学会长距离的依赖(主谓一致)。
下一讲之前,建议你:跑通 code/lecture04/ 下的 4 个脚本;不看讲义完成练习 1(这是作业 2 的必考题型);读 Chen & Manning(2014)的第 3 节。