☰
依存句法分析:从转移式到图式解析,读懂NLP的句法结构地基
2026/10/15 13:29:13 网站建设 项目流程

2019年刷某门经典NLP课程时,第五讲的标题是Linguistic Structure: Dependency Parsing,中文一般叫“依存句法分析”。当时我已经被词向量、注意力这些“现代”话题养刁了胃口,突然看到一讲在讲语法树,第一反应是:这都什么年代了,怎么还在学语言学。但把作业啃完之后发现,句法分析恰恰是很多上层任务绕不开的地基。你想做关系抽取、语义角色标注、机器翻译的词对齐,最后都会回到“谁是谁的核心、谁修饰谁”这个结构问题上来。这篇文章就是当时第五讲的复习笔记,按依赖树的表示、两类解析算法、神经网络改造、评估与落地细节来写,适合准备补NLP基础、做结构化文本理解的同学参考。

1. 为什么自然语言处理绕不开“语言结构”这一层

1.1 很多上层任务都在悄悄依赖“谁是谁的修饰”

如果你刚开始学NLP,可能会觉得句法分析是个“历史遗留问题”——毕竟现在大家都在搞大模型预训练,好像给足够多的数据就能自动把语法学会。但真正做任务时会发现,很多结构化输出并不是靠“猜”能稳定搞定的。

举一个最常见的例子:关系抽取。给你一句“总经理的秘书今天辞职了”,要抽“总经理”和“秘书”之间是否存在任职关系。如果只看词序,你会想当然地把“总经理”当成“秘书”的修饰,这没问题;但换成长句“总经理的秘书今天当着所有同事的面提出了辞职”,中间的插入成分一多,局部窗口就抓不住了。依存句法树直接告诉你,“秘书”是“总经理”的修饰中心,“辞职”又是整句的核心谓语,“秘书”和“辞职”之间是主体关系。有了这种结构,关系抽取的规则或模型就能把远距离的语义联系锁定住。

语义角色标注也一样。你要回答“谁对谁做了什么”,“谁”和“谁”常常不是顺序相邻的。比如“张三昨天在办公室当着所有人的面拒绝了那份合同”,从词袋视角看,“拒绝”和“合同”隔着十几个词,中间还有一堆状语和补语;但依存结构中“拒绝”和“合同”就是一条干净的宾语弧。机器翻译里的长句调序、问答里的论元抽取,本质上都要靠这种“谁修饰谁”的信息。所以第五讲开篇就花了很大篇幅强调:句法结构是很多中高层NLP任务的最短路径,绕开它不代表问题消失,只是把困难推迟给了下游。

1.2 短语结构树与依存树:同一句话的两种切法

NLP课程讲到语法,通常先给出两种表示:短语结构树和依存结构树。

短语结构树是自上而下的层次切分。句子S下面分NP和VP,NP再往下分成冠词、名词,VP里再嵌套动词短语。这种表示强调“成分”,一个句子被看成若干层级的词组包裹关系。它的好处是直观,传统的上下文无关文法就是建立在这套体系上的。但它也有麻烦:遇到语序自由的语言,比如德语、俄语、日语,主语宾语靠格标记区分而不是靠位置,短语结构树就会产生大量空类别和移位规则,工程上很难维护。

依存结构树则完全是另一套逻辑。它不给句子做“成分划分”,而是直接把词和词用一条有向弧连接起来:每条弧从head(头词)指向dependent(依存词),并带一个关系标签。比如“她喜欢红色的裙子”,短语结构是“喜欢”下面挂一个动宾短语;依存树则直接给出三条边:喜欢看作是句子的根,挂上“她”作为主语;再挂上“裙子”作为宾语;“红色的”挂在“裙子”下面做修饰。

ROOT | 喜欢 / \ 她 裙子 / 红色的

课程里特意强调,依存结构更贴近“中心词驱动”的语言学直觉:一个短语的核心决定它出现在哪里、以什么形态出现,其他词围绕这个核心进行扩展。因此依存弧能跨越很远的距离,也能自然处理语序自由的语言。这也解释了为什么从2019年的视角看,工业界的解析器几乎全部建立在依存结构上,而不是短语结构上。

2. 依存语法的核心约定:head、标签、合法树约束

2.1 一条边的完整含义

如果把一句话的依存分析结果画出来,你看到的是一张“箭头图”。每条箭头表达的不只是“两个词有关系”,还包含两个信息:谁是head、谁是dependent,以及这个关系是什么类型。

判断head的一个实用标准是:dependent的形态由head决定。动词决定主语的格,介词决定宾语的格,形容词决定名词的性数。反过来,dependent是对head的语义补充或限定。比如“红色的裙子”里,“裙子”是head,“红色”是dependent,标签amod表示形容词修饰语;“吃苹果”里,“吃”是head,“苹果”是dependent,标签obj表示宾语。主语“我”的标签是nsubj,表示名词性主语。

课程里会要求你熟悉一套常见标签:nsubj、obj、obl(间接格论元)、amod(形容词修饰)、nmod(名词修饰)、advmod(副词修饰)、aux(助动词)、case(格标记)、det(限定词)等。这些标签不是随意定的,而是把“论元关系”和“修饰关系”混合在一起。比如nsubj、obj更接近语义角色里的施事和受事,但又不完全等价;amod、advmod则是纯粹的句法修饰。理解这一点很重要,后面做关系抽取时如果直接把nsubj当成语义角色“施事”用,会在被动句里踩坑。

2.2 一棵合法的树:根、入度、连通、投影性

一张“箭头图”并不自动等于一棵合法的依存树。课程给出了几个必须同时满足的约束:

  • 有且仅有一个根节点,通常叫ROOT,入度为0。
  • 除根节点外,每个词有且仅有一个head,也就是说每个节点入度恰好为1。
  • 整张图必须弱连通:从根出发,能沿着弧走到所有词。
  • 不能有环:如果A指向B,B又指向A,那就不构成树。

满足前三条之后,无环性可以由入度条件推出来,但课程里还是单独强调,因为初学阶段很容易画出“多个根”或“一个节点两个父亲”的非法结构。

还有一个更微妙的约束叫投影性,也叫projectivity。大意是:把句子按从左到右的顺序排开,如果从弧上画起来会出现交叉,那就是非投影结构。比如四个词按顺序ABCD,如果有一条弧从B指向D,同时另一条弧从C指向A,两条弧横跨交叉,这就不是投影树。一些语序灵活的语言里,远距离移动会产生非投影现象。课程的重点是:转移式解析器默认处理投影树比较方便,但现实中树库允许非投影标注,解析器必须能容忍或处理这种情况。后面图式解析里的MST解码就是解决非投影问题的关键途径。

2.3 通用标注体系:谁是head谁做标记

2019年这讲已经大量依赖“通用依存标注体系”(UD)了。UD的核心思想是跨语言统一标签:无论中文、英文、捷克语、土耳其语,同一类句法关系尽量用同一套关系标签,标注原则也尽量对齐。

UD里面最反直觉的一条原则是:内容词当head,功能词当dependent。典型例子是介词短语“on the table”,我们自然觉得“on”是短语核心、名词是依存,但在UD中“table”是head,“on”是它的case标记。类似的还有助动词:动词当head,“have”“will”挂在动词下面标aux;系动词“is”在UD里标cop,名词或形容词谓语的head甚至可以是表语本身。这个设计是为了让跨语言的句法结构更容易对齐,但也让很多习惯了传统语法的人感到别扭。

好在学这一讲不需要你背下每个标签,只需要理解标注原则。我当时记的一句话是:别用“自然语言的直觉”去套UD,用“谁决定句子主干结构”去套。动词决定主干,名词决定名词短语,形容词决定形容词短语,功能词统统降级为dependent。后面做解析器评测,用的树库就是这套标注,所以先搞清楚标签规则,后面才能读懂论文里的结果表。

3. 路线一:转移式解析(Transition-based Parsing)

3.1 Arc-standard三步走:Shift、Left-Arc、Right-Arc

转移式解析的基本假设是:解析一个句子可以看作“维护一个机器状态,不断执行动作,逐步拼出依存边”。状态通常由一个栈和一个缓冲区组成,再维护一个已建立的弧集合。

Arc-standard系统只有三个动作,非常好记:

  • SHIFT:把缓冲区第一个词压入栈。
  • LEFT-ARC(label):建立一条从栈顶词指向栈内第二词的弧,然后弹出第二词。
  • RIGHT-ARC(label):建立一条从栈内第二词指向栈顶词的弧,然后弹出栈顶词。

我一开始总把LEFT-ARC和RIGHT-ARC搞混,后来自己写了个示意代码才记住。这里的命名不是看“谁在左边”,而是看“弧指向哪个方向”:LEFT-ARC建立一条向左跨的弧,RIGHT-ARC建立一条向右跨的弧。

def shift(stack, buffer): stack.append(buffer.pop(0)) def left_arc(stack, arcs, label): dep = stack.pop(-2) # 栈内第二词,位于栈顶左侧 head = stack[-1] # 栈顶 arcs.append((head, dep, label)) def right_arc(stack, arcs, label): dep = stack.pop() # 栈顶 head = stack[-1] # 弹掉栈顶后的新栈顶 arcs.append((head, dep, label))

用“我 吃 苹果”这个最短例子走一遍:

  1. 初始:stack=[ROOT],buffer=[我, 吃, 苹果]
  2. SHIFT:stack=[ROOT, 我],buffer=[吃, 苹果]
  3. SHIFT:stack=[ROOT, 我, 吃],buffer=[苹果]
  4. LEFT-ARC(nsubj):head=吃,dep=我,得到 吃→我;stack=[ROOT, 吃]
  5. SHIFT:stack=[ROOT, 吃, 苹果],buffer=[]
  6. RIGHT-ARC(obj):head=吃,dep=苹果,得到 吃→苹果;stack=[ROOT, 吃]
  7. RIGHT-ARC(root):head=ROOT,dep=吃,得到 ROOT→吃;stack=[ROOT]

整个过程是O(n)级别的动作数量,因为每个词至少压栈一次、弹栈一次。这也是转移式解析最大的工程优势:快,内存可控。

3.2 动作预测训练与贪心误差

转移式解析的核心问题变成了:给定当前栈、缓冲区和已有弧,下一步该执行哪个动作。2019年之前,主流做法是把当前配置中栈顶若干词、缓冲区首词、以及它们的词性、距离等手工特征抽出来,扔给一个分类器。教学里通常会讲经典的分类器+特征模板,但我个人认为神经化之后你不需要手工挖那么多特征,但“配置状态如何表达”这个概念依然重要。

训练转移式解析器有一个核心工具叫oracle。oracle知道gold树,负责告诉你“在当前状态下,gold树对应的合法动作是什么”。课程里常讲静态oracle:状态一旦偏离gold,后面就无法回到正确路径,训练数据本身会有不少噪声;更好的做法是动态oracle,它检查当前状态和gold树之间的误差,允许程序在已经犯错的情况下继续生成合理动作。

实际跑模型时会发现,贪心解析最大的问题是“一步错,步步错”。因为训练时每个动作都依赖前面的状态,如果模型在早期把某个词弹错栈了,后面的整个结构都会乱。解决思路有两种:一种是在推理时做beam search,保留多个候选状态;另一种是改用图式解析,一次性全局优化。第五讲在这里埋了个伏笔:等讲到图式方法时,你会明白转移式的快与图式的稳,其实是NLP中效率和精度的一次经典权衡。

3.3 一种常用变体:Arc-eager

Arc-standard不是唯一的转移系统。课程里一般还会提Arc-eager,中文常叫“急切式”或“早建边”。

Arc-eager和Arc-standard最大的区别是:它允许在缓冲区首词和栈顶之间直接建边,并且建立left-arc时不要求依存词已经在栈里稳稳待过,而是更早地把左向修饰关系确定下来。直观来说,Arc-standard比较“沉稳”,非要等一个词的所有右依存都有把握后才把它弹出去;Arc-eager比较“着急”,一有机会就先连线。

工业级解析器后来多数采用Arc-eager风格或它的变体,因为它每一步动作选择更贴近人的阅读习惯,对长句也更宽容。但课程里不会花太多时间对比两者,你只需要知道:转移式解析家族不是只有一套动作,具体用哪种取决于你要解析的语言、树的投影性要求、以及速度预期。我自己的建议是:初学阶段先吃透Arc-standard,因为动作最少、逻辑最干净;等需要优化性能时,再去看Arc-eager等变体的实现。

4. 路线二:图式解析(Graph-based Parsing)

4.1 Arc-factored打分:把解析变成带约束的最优化

图式解析的逻辑和转移式完全不同。转移式是“走一步看一步”,图式则是“把所有候选弧一次性打分,然后从整棵树的得分最大化解码”。

所谓arc-factored模型,假设整棵树的得分等于每条弧得分的累加。对每个可能的弧(i, j, label),模型给出一个分数,然后解析器在所有满足树约束的结构里找一个总分最高的。这里的“树约束”就是第2节说的:单根、入度为1、连通、无环。

这个设定听起来简单,但好处很大:训练时不需要像转移式那样对齐动作序列,只需要把gold树里的正例弧和负例弧对比,让正例弧得分高、负例弧得分低;推理时也能使用全局解码,不会因为一个早期错误导致连锁反应。缺点也很明显:完全枚举所有树的复杂度太高,必须靠算法优化解码。

课程里讲的经典解码有几个关键点。如果假设输出是投影树,可以用Eisner算法做动态规划;如果允许非投影树,就得用最大有向生成树算法,也就是Chu-Liu-Edmonds。2019年的神经图式解析器,解码仍然沿用这些方法。

4.2 投影假设下的Eisner解码与非投影场景的MST

Eisner算法我当年学的时候晕了很久,后来发现它本质上是一个“带根方向的span DP”。

它的核心思想是:把句子从左到右切分成连续区间span,每个区间对应一棵局部树,且这棵树必须“挂在区间边界上”。合并两个相邻span时,如果它们的根都在边界,就可以通过一条跨边把两个半棵树连接起来,形成更大的span。这样自底向上合并,最后覆盖整个句子,同时保证所有弧不交叉。复杂度是O(n³),比朴素的指数级枚举实用得多。

为什么不能用普通的无向最小生成树算法?因为普通MST处理的是无向图,而依存树有方向要求(每个节点只有一个父亲,还有唯一根)。Chu-Liu-Edmonds算法处理有向图,专门找有根、入度限制的最大生成树,适用于非投影树。如果树库里出现大量交叉弧,图式解析会比转移式表现更好。

我当时把这两种解码方式各手动推了一个小例子,收获很大。推Eisner时,重点是观察“半树只有一个边界根”这个约束;推Chu-Liu-Edmonds时,重点观察“如果成环,要沿着环调整最小代价边”。课程不要求你徒手实现这些算法,但读懂它们能帮你理解为什么图式解析器的训练目标和解码输出不一定完全一致。

4.3 两条路线怎么选:一张对比表

转移式和图式常常被放在一起对比。我用课程笔记里的方式总结成下面这张表:

对比维度转移式解析图式解析
建树方式动作序列逐步构造所有候选弧打分后全局解码
时间复杂度约O(n)Eisner约O(n³),MST约O(n²)
推理方式贪心,可配合beam search全局最优,但受模型得分影响
特征表达容易利用当前配置等局部上下文传统上偏向arc-factored简单特征
非投影能力需要扩展或预处理可以直接处理非投影结构
工程速度快,适合大规模实时解析相对慢,但GPU优化后也可接受

在2019年前后,两条路线都已经神经化,精度差异在大多数标准树库上已经缩小到1到2个点。真正选择时,课程给的判断标准是:如果任务需要极低延时,转移式更合适;如果追求极端准确率、不介意多花一点计算,图式更稳。后来的很多工业系统干脆把两者融合,先用图式打分再走转移式约束,这是后话。

5. 神经依存解析器:从词向量到Deep Biaffine

5.1 BiLSTM上下文编码:解决“同一个词在不同语境下的句法角色”

到了2019年这讲,手工特征已经不是主角,取而代之的是神经网络编码器。课程强调的第一步是把每个词的表示从“静态词向量”升级成“上下文相关表示”。

为什么静态词向量不够?因为“苹果”在“他喜欢吃苹果”和“苹果公司发布了新品”里的句法角色完全不同:前者是动词宾语,后者是名词短语的修饰核心。静态word embedding是一个固定向量,无法区分这两种语境。BiLSTM的思路很简单:每个词从左到右和从右到左分别编码,再把两个方向的隐藏状态拼接起来。这样“苹果”向量的每个维度都包含了它所在句子的上下文信息,下游打分器再做“谁依赖谁”的判断时,就有足够的上下文线索。

你可以把BiLSTM理解成给每个词重新写了份“简历”:简历里除了这个词本身,还写清了它左边出现什么、右边出现什么。以第五讲当时的知识点,BiLSTM已经是标准做法;后来的预训练语言模型则是把这份简历做得更厚实,但底层的“上下文编码再打分”结构并没有变。

5.2 Biaffine打分:为什么值得用双线性替代MLP

2019年这讲最核心的模型更新,我印象最深的是Deep Biaffine Attention。它解决了“两个词的表示如何打分”的问题。

传统做法是把head表示和dependent表示拼接起来,过一个MLP,输出得分。拼接+MLP理论上没问题,但缺点是两个表示只能在最后的非线性层才开始交互,要想表达复杂的头-依存关系,需要较大隐层。Biaffine直接用一个双线性函数计算得分:

score(head i, dep j) = u_i^T W u_j + U u_i + V u_j + b

这个公式里,W是一个二维矩阵,能够同时捕捉u_i和u_j的交叉项,比纯点积灵活;加上后面的线性项又保留了单侧信息。对GPU来说,它可以用矩阵乘法一次性算出所有n×n个候选弧的得分,非常高效。

当时我看到这个设计的第一反应是:这不就是给每条弧做了一个“专门适配”的打分器吗?是的。更关键的是,Biaffine被打分器用于两个任务:一个预测是否存在弧,即head index;另一个用另一组参数预测关系标签。这种“先找弧,再标标签”的两阶段结构,后来成了标准图式神经解析器的雏形。

5.3 2019年前后的标准架构与我在复现时的体会

综合课程内容,神经依存解析器的整体架构可以分成四层:

  1. 输入层:词向量拼接词性向量,也可以加上形态特征。
  2. 编码层:BiLSTM把句子编码成上下文表示。
  3. 打分层:通过两套MLP分别得到dep表示和head表示,再用Biaffine计算弧得分和标签得分。
  4. 解码层:对弧得分矩阵执行Eisner或Chu-Liu-Edmonds解码,得到最终树。

我当时按照这个结构复现了一个小型模型,踩过两个坑。第一个坑是标签得分和弧得分必须分开处理,如果用同一个打分器输出弧又要输出标签,模型容易把“有弧”和“弧标签”糊在一起。第二个坑是解码时不能直接把每个词取最高分的弧,因为很容易得到多个根或成环;必须在解码阶段用全局约束,这一点远比在训练阶段做复杂后处理有效。

还有一个小体会:在小数据集上,Encoder和Scorer的宽度不用太大,但BiLSTM的层数和dropout对结果影响非常大。课程里给的标准baseline,一般两层BiLSTM、隐藏维度400到600就够;再往上加,精度提升很慢,训练时间却暴增。如果你想快速验证整个pipeline,先把维度缩到128,跑通再放大也不迟。

6. 评估指标、工具链和工程落地中的细节

6.1 UAS/LAS到底在算什么

解析器最常用的指标是UAS和LAS。UAS是无标签依存准确率:只比较模型预测的head index是否和gold一致,不管关系标签对不对。LAS是带标签依存准确率:head和label必须同时正确才算对。

举一个简单例子。假设gold树是:

ROOT → 吃 吃 → 我 吃 → 苹果

共三个非根节点:我、吃?这里需要小心:ROOT→吃这条边,dependent是吃,所以三个dependent分别是我、吃、苹果。如果模型预测ROOT→我、ROOT→吃、吃→苹果,那么“我”的head错了,“吃”的head对了,“苹果”的head对了,UAS就是2/3。如果其中“吃→苹果”标签标成了obl而不是obj,那“苹果”即使head对了,在LAS里也判错,LAS就会更低于UAS。

评测时还要注意几个约定:是否把标点排除,是否把根节点计入。很多论文为了好看会报告“排除标点”的指标,这不算造假,但你在复现或对比时一定要看评估脚本是否一致。另一个细节是树库里可能存在多词token或空节点,token对齐稍有差池,得分表就会失真,这是实验里最容易踩的坑。

6.2 工程里常见的坑:分词语粒度和token对齐

真实项目里跑依存解析,一半以上的问题出在预处理,而不是解析模型本身。

第一坑是分词。中文依存解析器依赖分词结果,分词的颗粒度不同,句法结构就不同:“南京市/长江/大桥”和“南京市/长江大桥”出来的head关系完全不一样。英文看似不用分词,但连字符、缩写展开、大小写都会干扰词法分析。我的经验是:先用固定规则做一次分词和后处理,不要频繁切换分词工具,否则模型会懵。

第二坑是token对齐。如果你要评估一个模型,gold树库里的token边界必须和预测器的token边界一致。很多开源树库带有额外的多词token,比如法语“du”展开成“de le”,中文“把”可能被拆成基础token,这些在对比时最容易造成“错位+1”的连续错误。我在一个某跨语言项目里曾经因为没处理多词token,LAS直接掉了十几个点,后来才发现问题不在解析器,而在数据对齐。遇到这种情况,先写一个脚本打印gold和pred的token序列,肉眼检查到底差在哪。

第三坑是领域漂移。训练集是新闻文本,模型在对话、口语、社交媒体文本上性能会剧烈下降。口语里成分省略多、插入语频繁,句子边界也很难判断。如果项目里必须处理口语,建议先做句子切分,再引入该领域的少量标注数据进行微调,比强行优化解析模型本身更划算。

6.3 迷你对比实验:转移式与图式在上手后的差距

为了把课程知识落到代码里,我在一个开源中文树库上做过小实验:抽样100条句子,分别用转移式神经分类器和一个图式Biaffine模型做对比,统一排除标点。

结果大概是这样:

模型UASLAS
转移式+词窗口MLP84.781.2
转移式+BiLSTM88.985.4
图式+BiLSTM+Biaffine90.387.1

这个结果不是排行榜级的,因为数据集小、迭代轮数也少,但趋势和课程里讲的一致:BiLSTM上下文编码比“词窗口MLP”带来了巨大提升;在同一种编码器下,图式+全局解码相对转移式的优势大约稳定在1到2个点。

我还做了一个有趣观察:转移式在短句上的错误多数集中在“介词归属”,比如“对于这个问题”里的“对于”到底挂到“问题”还是挂到主谓结构上;图式在长句上的错误则更多来自“弧标签混淆”,比如把obl标成nsubj。需要的技术手段往往不同,前者要更好的上下文编码,后者要更丰富的标签先验。

6.4 给后来者的一条学习路径

如果你是从零开始补这块,我的建议是不要一上来就看论文代码,而是按下面顺序走:

  1. 先徒手用Arc-standard推三个短句子,把Shift、Left-Arc、Right-Arc的动作序列写出来,顺便和gold树对照。
  2. 自己实现一个简单oracle,给定gold树,输出每一步合法动作;这一步能让你彻底理解转移式训练数据是怎么来的。
  3. 再理解Eisner算法的plot图和半树概念,笔记里画一下span怎么合并。
  4. 最后再看神经模型,重点理解“编码层→打分层→解码层”的分工,不要被一堆花哨模块带跑。

如果时间紧张,至少要完成第1步和第3步。这两个地方一旦通了,后面读论文里“训练设置”“解码细节”都会顺畅很多。这也是我刷完第五讲之后最大的收获:句法分析不是一个“读一遍就懂”的知识点,它是那种必须手动推一遍、跑一遍才能真正内化的技术。

最后分享一个实际经验:我后来在某个结构化抽取项目里,费了很大劲调关系抽取模型,效果一直卡住,后来把句子先过一遍依存分析,把nsubj、obj、nmod这些弧当成弱特征输进去,效果立刻涨了一截。不要神化句法分析,也不要只看论文里那零点几个点的差距,它在真实任务里往往是最稳定、最可解释的那块支点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询