想象你是一家创业公司的 HR 总监。创始人 ROOT 把四位员工叫进会议室:产品经理“爱”、工程师“我”、运营“红茶”。你的任务,是给每个人都安排好直属上级,画出一张清晰的汇报结构图。这个过程,和“依存句法分析”要解决的问题一模一样:机器拿到一句话,要搞清楚每个词之间的主从支配关系,也就是“谁管着谁”。 怎么画这张图?公司里会有两套思路:一套是“稳妥型 HR”,先把所有员工都放进会议室,等人员凑齐了再逐个指定上级,这对应 Arc-Standard 转移系统;另一套是“急脾气 HR”,新员工一进门就马上连线,能挂靠就挂靠,这对应 Arc-Eager 转移系统。还有一套“总部规划委员会”,把公司所有可能的分区方式都推演一遍,记住每个子团队的最优方案,再合并出全局最优架构,这对应基于图的 Eisner 算法。 这篇文章就用“搭汇报线”的故事,把这两大流派掰开揉碎讲清楚。学完你就能明白,为什么有的解析器快如闪电,有的则追求全局最优,以及在实际 NLP 工程中,你该选哪一把刀。

岗位关联

掌握依存句法分析的三大算法,是你读懂 NLP 预处理明星组件(如 spaCy、HanLP、LTP)实现原理的钥匙,也能帮你为真实业务选择性价比最高的解析方案。

目录
  1. 可视化演示
  2. 第1节:任务概览——每个句子都有一张组织架构图
  3. 第2节:依存树结构——四条铁律
  4. 第3节:Arc-Standard——稳扎稳打的“先囤人后连线”
  5. 第4节:Arc-Eager——风风火火的“见人就连线”
  6. 第5节:Eisner 算法——全局最优的“规划委员会”
  7. 第6节:算法选型——你的公司该用哪一套?
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

句子:苹果发布新产品 Stack: Buffer: 依存树构建完成 依存句法分析可视化:展示转移系统和图方法的解析过程

第1节:任务概览——每个句子都有一张组织架构图

公司 HR 拿到一张员工清单,最核心的任务不是把名字背下来,而是搞清楚“谁向谁汇报”。NLP 里的依存句法分析也一样:给出一句话,我们要构建一棵“句法汇报树”,让每个词都清楚地知道自己的抬头是什么、直属上级是谁。这张图一旦画好,后面做信息抽取、情感分析、机器翻译时,机器就不会抓瞎。

把这棵树形式化来看:句子里的每个词都是树上的一个节点,词与词之间画一条带箭头的弧,表示“谁支配谁”。弧上还会贴一个关系标签,比如“主谓关系”“动宾关系”。所有结点再加上一个特殊的虚拟节点 ROOT(对应公司的创始人),整棵树的根就是它。ROOT 的存在很有用:它充当“总指挥”,让句子中那个核心谓语(比如“爱”)可以有一个合法的上级。

形式化说,一个依存句法结构可以写成有向图:

回到比喻里:HR 只需要在员工脑袋上方贴一张小纸条,写上“你的老板是谁、你们是什么关系”,整家公司的汇报体系就瞬间清晰了。依存句法分析要产出的,正是这样一套“贴纸条”的结果。

G=(V,A),A={(h,d,r):h,d∈V, r∈R}G = (V, A),\quad A = \{ (h, d, r) : h, d \in V,\ r \in \mathcal{R} \}
  1. 节点集合 V$V$ 是句子中全部词汇(token)的集合,通常还会额外加入一个虚拟的 ROOT 标记,代表整个句子的最高层。
    V={ROOT,w1,w2,…,wn}V = \{ \text{ROOT}, w_1, w_2, \dots, w_n \}
  2. 弧集合 A$A$ 是所有依存弧的集合。一条弧是一个三元组 $(h, d, r)$,其中 $h$ 是头词(老板),$d$ 是从属词(下属),$r$ 是句法关系标签。
    A={(h,d,r):h,d∈V, r∈R}A = \{ (h, d, r) : h, d \in V,\ r \in \mathcal{R} \}
  3. 关系标签集合 R$\mathcal{R}$ 是各类句法关系标签的集合,例如“主谓”“动宾”“定中”,它告诉我们这条弧代表什么语法意义。
    R={主谓,动宾,定中,… }\mathcal{R} = \{ \text{主谓}, \text{动宾}, \text{定中}, \dots \}

第2节:依存树结构——四条铁律

一个合格的汇报结构,不是随便画几条线就行。想象你给公司画组织架构图,如果出现“我管你、你管他、他又管我”的循环,或者某个人同时有两个直属老板,第二天公司就会乱成一锅粥。依存句法树也一样,必须满足几条硬性规则,否则它就不是一棵合法的树。

首先,整棵树必须只有一个根,就是 ROOT,没有老板的人只能有一个。其次,除 ROOT 外的每一个词,必须有且仅有一个头部(老板),这叫“单亲约束”。再次,整棵树的弧的数量必须等于节点数减一,因为树是连通的,不能有环。最后还有一条更强的约束叫“投影性”:把弧画在句子下方,如果所有弧都不会跨越彼此,那这棵树就具有投影性。

∣A∣=∣V∣−1,indeg(r)=0, ∀w∈V∖{r}:indeg(w)=1|A| = |V| - 1, \qquad \text{indeg}(r) = 0,\ \forall w \in V \setminus \{r\}: \text{indeg}(w) = 1

1. 弧数等于节点数减一:$|A| = |V| - 1$ 是树的连通性保证。如果 $V$ 有 $n$ 个节点,恰好 $n-1$ 条弧,且彼此连通,那么结构一定没有环,是一棵树。

∣A∣=∣V∣−1|A| = |V| - 1

2. 唯一根节点:$\text{indeg}(r) = 0$ 表示根节点(ROOT)的入度为 0,即没有任何词支配它。它是整棵树的最高层。

indeg(r)=0\text{indeg}(r) = 0

3. 每个非根节点恰好一个老板:$\forall w \in V \setminus \{r\}: \text{indeg}(w) = 1$ 表示除 ROOT 外每个词都恰好有一条进入的弧,也就是只有一个头。

∀w∈V∖{r}:indeg(w)=1\forall w \in V \setminus \{r\}: \text{indeg}(w) = 1

投影性这条约束对算法影响很大:Arc-Standard 默认只能处理投影结构,Arc-Eager 也主要处理投影结构;而现实中的汉语、英语偶尔会有少量非投影现象(比如口语里的“把这本书,我读完了”),这就成了两套转移系统的隐性边界。

第3节:Arc-Standard——稳扎稳打的“先囤人后连线”

有一种 HR 做事特别谨慎:新员工到了以后,不让立刻指定汇报关系,而是全部先请进一间大会议室(栈)里站好,后面排队的候选人(缓冲区)继续等着。只有当栈里最靠近门口的两个人已经不会再被中间插进来的人打扰时,HR 才放心地宣布他们之间的上下级关系。这就是 Arc-Standard 的处事风格:不到火候不连线。

Arc-Standard 是一个经典的转移系统,它用一个栈维护已经出场的词,用一个缓冲区维护还没出场的词。每一步只做三件事之一:把缓冲区第一个人请进栈(SHIFT);让栈顶的人当老板,收编他左边那个当从属,然后让从属离开栈(LEFT-ARC);或者让次栈顶的人当老板,收编栈顶这个从属,然后让从属离开栈(RIGHT-ARC)。算法一直重复到栈里只剩 ROOT、缓冲区清空为止。

Arc-Standard 的聪明之处是它的“忍”:它不会在中间还隔着未处理词的时候就急着画弧,所以画出来的弧天然不交叉,保证投影性。代价是每个词都会被压栈再出栈,过程更有章法,但没法处理需要“跨过中间词”的语序。它的解码步数大约是 $2n$ 步,是一个线性时间的效率标兵。

SHIFT:(σ,b∣β)→(σ∣b,β)\text{SHIFT}: (\sigma, b\mid\beta) \to (\sigma\mid b, \beta)
LEFT-ARC:(σ∣s2∣s1,β)→(σ∣s1,β), 加入弧 s1→s2\text{LEFT-ARC}: (\sigma\mid s_2\mid s_1, \beta) \to (\sigma\mid s_1, \beta),\ \text{加入弧 } s_1 \to s_2
RIGHT-ARC:(σ∣s2∣s1,β)→(σ∣s2,β), 加入弧 s2→s1\text{RIGHT-ARC}: (\sigma\mid s_2\mid s_1, \beta) \to (\sigma\mid s_2, \beta),\ \text{加入弧 } s_2 \to s_1
  1. SHIFT 入栈缓冲区最左侧的待处理词 $b$ 被移入栈顶,写法 $\beta = b \mid \beta'$ 表示 $b$ 是缓冲区的第一个元素,出队后剩下 $\beta'$。
    SHIFT:(σ,b∣β)→(σ∣b,β)\text{SHIFT}: (\sigma, b\mid\beta) \to (\sigma\mid b, \beta)
  2. LEFT-ARC 建立左弧栈顶 $s_1$ 当老板,次栈顶 $s_2$ 当从属。新增弧 $s_1 \to s_2$,随后从属词 $s_2$ 出栈,栈顶保持为 $s_1$。
    LEFT-ARC:(σ∣s2∣s1,β)→(σ∣s1,β), 加入弧 s1→s2\text{LEFT-ARC}: (\sigma\mid s_2\mid s_1, \beta) \to (\sigma\mid s_1, \beta),\ \text{加入弧 } s_1 \to s_2
  3. RIGHT-ARC 建立右弧次栈顶 $s_2$ 当老板,栈顶 $s_1$ 当从属。新增弧 $s_2 \to s_1$,随后从属词 $s_1$ 出栈,次栈顶 $s_2$ 成为新的栈顶。
    RIGHT-ARC:(σ∣s2∣s1,β)→(σ∣s2,β), 加入弧 s2→s1\text{RIGHT-ARC}: (\sigma\mid s_2\mid s_1, \beta) \to (\sigma\mid s_2, \beta),\ \text{加入弧 } s_2 \to s_1

第4节:Arc-Eager——风风火火的“见人就连线”

另一类 HR 完全相反,急性子:新员工刚从缓冲区露面,她立刻就想办法建立汇报关系,能连就连,绝不拖延。比如她是 HR 专员“爱”的直属部下,刚一见到“爱”出现在缓冲区,就马上把“爱”和自己的上级线连好。这种风格虽然快,但也容易出问题:可能连线之后发现,后面还有人需要这个人来管,所以这套系统多了一个动作叫 REDUCE(归约),专门用来把“已经找到老板、不再被需要”的人从栈里请出去。

Arc-Eager 的动作比 Arc-Standard 多一个,一共四类:SHIFT、LEFT-ARC、RIGHT-ARC、REDUCE。它最大的不同是,弧不一定只在栈内两个人之间建立,而是可以连接“栈顶”和“缓冲区第一个”这两个人。因此在句子读入到一半时,整个汇报结构就已经飞速成型了。

回到“我 爱 红茶”的例子上:Arc-Eager 会先让“爱”刚出现在缓冲区时,就把栈里的“我”收编;然后 ROOT 立刻任命“爱”为核心负责人;最后“爱”再把缓冲区里的“红茶”纳入麾下。整个过程一气呵成,不需要像 Arc-Standard 那样把所有人都闷在会议室里憋到最后。也正因为它这么“急”,所以需要 REDUCE 来兜底清理。

SHIFT:(σ,i∣β)→(σ∣i,β)\text{SHIFT}: (\sigma, i\mid\beta) \to (\sigma\mid i, \beta)
LEFT-ARC:(σ∣j, i∣β)→(σ, i∣β), 加入弧 i→j\text{LEFT-ARC}: (\sigma\mid j,\ i\mid\beta) \to (\sigma,\ i\mid\beta),\ \text{加入弧 } i \to j
RIGHT-ARC:(σ∣j, i∣β)→(σ∣j∣i, β), 加入弧 j→i\text{RIGHT-ARC}: (\sigma\mid j,\ i\mid\beta) \to (\sigma\mid j\mid i,\ \beta),\ \text{加入弧 } j \to i
REDUCE:(σ∣j, β)→(σ, β)\text{REDUCE}: (\sigma\mid j,\ \beta) \to (\sigma,\ \beta)
  1. SHIFT 入栈缓冲区第一个词 $i$ 进入栈顶,和 Arc-Standard 的 SHIFT 含义一致。
    SHIFT:(σ,i∣β)→(σ∣i,β)\text{SHIFT}: (\sigma, i\mid\beta) \to (\sigma\mid i, \beta)
  2. LEFT-ARC 左弧缓冲区第一个词 $i$ 当老板,栈顶 $j$ 当从属。新增弧 $i \to j$,从属 $j$ 离开栈,但老板 $i$ 仍然留在缓冲区等后续处理。
    LEFT-ARC:(σ∣j, i∣β)→(σ, i∣β), 加入弧 i→j\text{LEFT-ARC}: (\sigma\mid j,\ i\mid\beta) \to (\sigma,\ i\mid\beta),\ \text{加入弧 } i \to j
  3. RIGHT-ARC 右弧栈顶 $j$ 当老板,缓冲区第一个 $i$ 当从属。新增弧 $j \to i$,然后 $i$ 入栈,但它已经被标记为“有老板了”。
    RIGHT-ARC:(σ∣j, i∣β)→(σ∣j∣i, β), 加入弧 j→i\text{RIGHT-ARC}: (\sigma\mid j,\ i\mid\beta) \to (\sigma\mid j\mid i,\ \beta),\ \text{加入弧 } j \to i
  4. REDUCE 归约如果栈顶 $j$ 已经找到老板,并且不再需要它等待新下属,就把它从栈里移除,相当于给它的任务画上句号。
    REDUCE:(σ∣j, β)→(σ, β)\text{REDUCE}: (\sigma\mid j,\ \beta) \to (\sigma,\ \beta)

第5节:Eisner 算法——全局最优的“规划委员会”

如果说前两个 HR 是现场临场发挥,那么 Eisner 算法就是总部派来的“规划委员会”。它不着急立刻拍板任何一条汇报线,而是拿到完整句子后,把所有可能的分区方式都推演一遍:把句子切成左、右两个连续小区间,分别算出每个区间内部最优的组织方案,再把两个区间的最优方案合并,加上一条跨区间的汇报弧。从小到大,慢慢拼出整棵最优树。

Eisner 属于“基于图”的解析路线:它先让一个打分模型给每个候选弧打一个“默契分”(score),再用动态规划找出总分数最高的那棵树的组合方式。动态规划在这里像个备忘录,把每个小区间的最优分数记下来,避免重复计算。这正是它和贪心转移系统最本质的差别:转移系统边走边决策,只保证当前步最优;Eisner 却保证全局最优。

Eisner 之所以高效,是因为它把指数级的树组合压缩成了多项式级别:每个括号(子区间)只需记住“最左当老板”和“最右当老板”两张表,合并时只考虑一个分裂点。最终它能拿到全局最优的依赖树,而不是一个“差不多就行”的贪心解。

Ci,jL=max⁡i≤k<j(Ci,kL+Ck+1,jR+score(i,j))C_{i,j}^{L} = \max_{i \le k < j} \left( C_{i,k}^{L} + C_{k+1,j}^{R} + \text{score}(i, j) \right)
Ci,jR=max⁡i≤k<j(Ci,kL+Ck+1,jR+score(j,i))C_{i,j}^{R} = \max_{i \le k < j} \left( C_{i,k}^{L} + C_{k+1,j}^{R} + \text{score}(j, i) \right)
  1. 左头子表 C^L$C_{i,j}^{L}$ 表示区间 $[i, j]$ 内依存树的最优分数,并且这个区间的最左词 $i$ 是整棵子树的老板(头)。递推时尝试一个分裂点 $k$,将区间拆成 $[i,k]$ 和 $[k+1,j]$。
    Ci,jL=max⁡i≤k<j(Ci,kL+Ck+1,jR+score(i,j))C_{i,j}^{L} = \max_{i \le k < j} \left( C_{i,k}^{L} + C_{k+1,j}^{R} + \text{score}(i, j) \right)
  2. 右头子表 C^R$C_{i,j}^{R}$ 表示区间 $[i, j]$ 的最优分数,并且最右词 $j$ 是老板。递推时同样在 $k$ 处二分,然后加上弧 $j \to i$ 的分数。
    Ci,jR=max⁡i≤k<j(Ci,kL+Ck+1,jR+score(j,i))C_{i,j}^{R} = \max_{i \le k < j} \left( C_{i,k}^{L} + C_{k+1,j}^{R} + \text{score}(j, i) \right)
  3. 初始化与复杂度长度为 1 的区间不需要任何弧,分数为 0;算法从小区间逐步扩展到大区间,最终在 $C_{0,n-1}^{L}$ 或 $C_{0,n-1}^{R}$ 中挑一个作为全局最优。
    Ci,iL=Ci,iR=0C_{i,i}^{L} = C_{i,i}^{R} = 0

第6节:算法选型——你的公司该用哪一套?

现在你已经见过三位风格迥异的 HR:Arc-Standard 稳、Arc-Eager 快、Eisner 精。可到了真实业务中,你不能全都要,得看场景说话。假设你的聊天机器人要对每一条用户消息实时做句法解析,延迟每多 1 毫秒用户都能感觉到,这时候 Arc-Eager 这种线性解码速度就非常吃香。反过来,如果你是离线分析一整个语料库,要求尽可能高的准确率,那 Eisner 的全局最优就值得那 O(n³) 的等待。

从形式化角度来说,转移系统的解码时间随句子长度线性增长,Eisner 则是立方增长。句子短时二者差别不大,句子一长,差距就非常显著。现代工业界常用方案是:用神经网络(如 BiLSTM、Transformer)给每一步打分,然后接一个像 Arc-Eager 这样的转移解码器,在保证速度的同时把准确率拉得很高;而基于图的 Eisner 则常常被当作“重武器”,用在学术评测、离线标注等高精度场景。

回到公司的比喻里:创业公司几十号人,HR 现场连线完全够用;可到了集团总部要规划上万人的组织架构,不派一个精算委员会全局推演,迟早会出大乱子。理解了这层本质,你就不会被“哪个算法更先进”这种话术迷惑,而是会冷静地问一句:我的业务到底更在乎速度,还是更在乎最终报表的准确率?

Ttransition=O(n),TEisner=O(n3)T_{\text{transition}} = \mathcal{O}(n), \qquad T_{\text{Eisner}} = \mathcal{O}(n^{3})
  1. 转移系统线性时间$T_{\text{transition}} = \mathcal{O}(n)$ 表示步数随句子长度 $n$ 线性增长。每个词最多经历一次 SHIFT、一次 REDUCE,再加一到两次建弧,所以总步数约 $2n$。
    Ttransition=O(n)T_{\text{transition}} = \mathcal{O}(n)
  2. Eisner 立方时间$T_{\text{Eisner}} = \mathcal{O}(n^{3})$ 表示要做三重循环:枚举区间起点、枚举区间终点、枚举区间内分裂点,所以是 $n^3$ 级别的计算量。
    TEisner=O(n3)T_{\text{Eisner}} = \mathcal{O}(n^{3})
  3. 空间开销对比Eisner 需要为 $\mathcal{O}(n^{2})$ 个区间各存两张表,转移系统则只存一个栈和几个指针,空间开销几乎是常数级。
    SEisner=O(n2)S_{\text{Eisner}} = \mathcal{O}(n^{2})

AI 应用场景

场景一:金融文本的信息抽取

阶段:NLP 预处理与分析

解决的问题:传统关键词正则抽取无法识别“A公司向B公司支付违约金”里到底谁是付款方,谁是收款方。用依存句法分析找到动词“支付”的主语和宾语,就能准确抽出“付款方-金额-收款方”三元组。

具体案例:某金融舆情系统每天处理数万篇研报。系统先用 Arc-Eager 解析器快速得到“ROOT→支付→A公司/B公司”的依存弧,再按“主谓关系”和“动宾关系”抽取结构化字段,准确率从正则方法的 62% 提升到 89%。由于请求量大,选 Arc-Eager 而不是 Eisner,保证单句延迟小于 5ms。

场景二:智能客服的意图与否定关系识别

阶段:对话理解中的语义解析

解决的问题:用户说“我不是想取消订单,只是改地址”,机器如果只做关键词匹配,很可能被“取消订单”带偏。用依存分析能看出“取消”的从属是“订单”,但否定词“不是”的依存指向“想”,从而判断真正意图是“修改地址”。

具体案例:某电商智能客服系统接入 HanLP 解析结果后,否定句的意图识别准确率提升约 18%。解析器采用 Arc-Standard,原因是客服语句普遍较短且符合投影性,同时部署在 CPU 上对延迟敏感。

场景三:机器翻译的语序调整

阶段:翻译前源语言分析

解决的问题:中英翻译时,汉语“昨天我在公园看见了小红”,如果逐字直译,英文语序会乱。借助依存树找到核心动词“看见”,再按目标语言规则重新排列所有从属词,就能生成通顺译文。

具体案例:某翻译团队在统计机器翻译时代使用 Eisner 算法对源语言做全局最优依存解析,作为调序模型的输入。虽然离线训练可以接受 O(n³) 的开销,但上线阶段仍切换为转移系统保证翻译速度。

Python 演示

python77 行
# -*- coding: utf-8 -*-"""简易 Arc-Eager 依存解析器演示:用一份手写得分表模拟神经网络的打分层,再用“贪心动作选择”逐步建立依存树。""" def score_arc(words, h, d):    """返回弧 h->d 的分数,分数越高越倾向建立。"""    rules = {        ('爱', '我'): 9,        ('爱', '红茶'): 8,        ('ROOT', '爱'): 7,        ('我', '爱'): 2,        ('红茶', '爱'): 1,    }    return rules.get((words[h], words[d]), 0) def arc_eager_parse(words):    n = len(words)    heads = [-1] * n            # heads[i] = i 的头部词下标    has_head = [False] * n      # 该词是否已经找到了头    stack = [0]                 # ROOT 下标=0 先进栈    buf = list(range(1, n))     # 缓冲区:待入栈的词下标     while buf or len(stack) > 1:        if not stack:            stack.append(buf.pop(0))            continue        s = stack[-1]           # 栈顶词        b = buf[0] if buf else -1         best, best_act = -1e9, None         # 尝试 LEFT-ARC:缓冲区第一个 b 当头部,栈顶 s 当从属        if b != -1 and not has_head[s]:            val = score_arc(words, b, s)            if val > best:                best, best_act = val, ('left',)         # 尝试 RIGHT-ARC:栈顶 s 当头部,缓冲区第一个 b 当从属        if b != -1 and not has_head[b]:            val = score_arc(words, s, b)            if val > best:                best, best_act = val, ('right',)         # 尝试 REDUCE:栈顶 s 已找到头,移除        if has_head[s]:            if 0 > best:                best, best_act = 0, ('reduce',)         # 尝试 SHIFT:把缓冲区第一个 b 压入栈        if b != -1 and best_act is None:            best_act = ('shift',)         # 执行动作        if best_act[0] == 'left':            heads[s] = b            has_head[s] = True            stack.pop()        elif best_act[0] == 'right':            heads[b] = s            has_head[b] = True            stack.append(b)            buf.pop(0)        elif best_act[0] == 'reduce':            stack.pop()        else:            stack.append(b)            buf.pop(0)     return heads if __name__ == '__main__':    words = ['ROOT', '我', '爱', '红茶']    heads = arc_eager_parse(words)    for i in range(1, len(words)):        print('{0} 的头是 {1}'.format(words[i], words[heads[i]]))
目的:这是一个完整可运行的 Arc-Eager 贪心依存解析器,用一张手写分数表模拟模型打分,演示“谁向谁汇报”逐步建立的过程。
  1. rules = {('爱', '我'): 9, ...}手写“默契分”表,代表每个候选弧的得分,用于模拟神经网络预测结果。
  2. has_head = [False] * n记录每个词是否已经有了老板,这是 Arc-Eager 判断 REDUCE 能否执行的关键状态。
  3. stack = [0]栈中先放入 ROOT(下标 0),它是整个解析过程的起点。
  4. buf = list(range(1, n))缓冲区保存所有还未进入会议室的员工下标,按顺序等待处理。
  5. if b != -1 and not has_head[s]:LEFT-ARC 的合法条件:缓冲区有人,并且栈顶还没有老板,才能被新的缓冲区词收编。
  6. heads[s] = b执行左边弧:把栈顶 s 的头部记为 b,也就是“b 是 s 的老板”。
  7. stack.append(b); buf.pop(0)执行右边弧后,缓冲区第一个词 b 成为新栈顶(它是被收编的下属),并从缓冲区移除。
  8. while buf or len(stack) > 1:只要缓冲区还有人,或栈里除了 ROOT 之外还有员工,解析就需要继续。

常见误区

误区都是分析句子结构,依存句法和成分句法(短语结构)应该差不多吧?我见很多解析器都叫 parser,以为它们输出的是一样东西。

正解两者完全不同。短语结构分析生成的是嵌套的“成分树”,像洋葱一样一层套一层,比如“我”和“爱红茶”先组成一个小成分,再往上拼成整个句子;而依存句法分析生成的是“汇报关系树”,只关心每个词的头是谁,是一个扁平的有向图。可以用公司比喻来区分:成分树是“部门套部门”的组织图,依存树是“一个人只向另一个人汇报”的直线关系图。

为何常见:因为两者在教科书里都叫“句法分析”,且很多工具库同时提供两套接口,初学者容易混淆输出格式。

误区Arc-Eager 动作多、看起来更灵活,还能边读边解析,肯定全面碾压 Arc-Standard。

正解两者都能解析投影性句子,Arc-Eager 的优势主要在于更早建立弧、在流式场景下更自然,但它需要额外的 REDUCE 来管理状态;Arc-Standard 的结构更简洁,实现更直观,在很多评测中两者结合神经网络后准确率几乎持平。选型不能只看“看起来更先进”,而要看状态管理复杂度和业务场景。

为何常见:因为 Eager 多了一个动作,给人“功能更多”的直觉,实际上多出的动作只是对状态的一种兜底机制,并不代表更强的建模能力。

误区Eisner 也是用动态规划来找“近似解”,所以得到的不一定是最优树。

正解恰恰相反,Eisner 是精确的动态规划算法。只要给每个候选弧的分数是定好的,Eisner 能从指数级的树空间里找出全局分数最高的那棵树,不是近似而是精确。图解析里的“贪婪近似”通常指 MST 算法中的某些剪枝变体,而不是 Eisner。

为何常见:因为很多讲解析的教程把 Eisner 放在“近似推理”章节,加上它 O(n³) 复杂度和近似算法一样有性能折衷,给人留下“不精确”的印象。

误区既然转移系统是 O(n) 线性复杂度,那让它解析长句子应该也很快。

正解转移系统每走一步都要调用一次模型打分(比如神经网络前向计算),实际耗时是“步数 × 单步模型耗时”。句子变长,步数线性增加,但单步模型输入也可能变长,导致整体并非严格线性。另外,Arc-Standard 和 Arc-Eager 遇到无法处理的非投影结构时会退化甚至失败,长句更容易触发这类情况。

为何常见:人们容易只盯着大 O 复杂度里的 n,忽略了常数因子和模型的单步开销。

面试题

Q1. 依存句法分析输出的结果是什么?

单选题 · easy

  • A.嵌套短语树
  • B.依存有向树← 正确答案
  • C.词性标注序列
  • D.词语共现矩阵

答案:B。依存句法分析输出的是从 ROOT 出发、每个词都指向其头词的有向树;短语结构树是成分分析的结果,词性标注和共现矩阵都不是句法树。

Q2. 一棵合法的依存树中,非根节点的入度是多少?

单选题 · easy

  • A.零
  • B.一← 正确答案
  • C.二
  • D.任意

答案:B。除根节点外,每个词必须有且仅有一个头词,所以入度恰好为 1。入度为 0 的只能是 ROOT,入度为 2 则违反单亲约束,无法构成合法树。

Q3. Arc-Standard 转移系统包含以下哪组动作?

单选题 · easy

  • A.SHIFT、LEFT-ARC、RIGHT-ARC← 正确答案
  • B.SHIFT、LEFT-ARC、REDUCE
  • C.SHIFT、RIGHT-ARC、REDUCE
  • D.LEFT-ARC、RIGHT-ARC、REDUCE

答案:A。Arc-Standard 只有三种动作:SHIFT(入栈)、LEFT-ARC(栈顶当老板)、RIGHT-ARC(次栈顶当老板)。REDUCE 是 Arc-Eager 特有的清理动作。

Q4. 在 Arc-Eager 中,多出来的、Arc-Standard 没有的动作是?

单选题 · easy

  • A.SHIFT
  • B.LEFT-ARC
  • C.REDUCE← 正确答案
  • D.RIGHT-ARC

答案:C。Arc-Eager 因为“见人就连线”,很多词已经找到老板但还留在栈中,所以需要 REDUCE 将它们清理出栈。Arc-Standard 建弧时从属词会立刻出栈,不需要这个动作。

Q5. Arc-Eager 执行 LEFT-ARC 时,充当老板(头词)的是?

单选题 · mid

  • A.缓冲区首词← 正确答案
  • B.栈顶位置
  • C.栈中次顶
  • D.ROOT节点

答案:A。Arc-Eager 的 LEFT-ARC 让缓冲区第一个词当老板,把栈顶词收编为从属并弹出栈。栈顶只当从属,不是老板;ROOT 只有在 RIGHT-ARC 中才可能当老板。

Q6. Eisner 算法的时间复杂度是多少?

单选题 · mid

  • A.O(n)
  • B.O(n²)
  • C.O(n³)← 正确答案
  • D.O(2ⁿ)

答案:C。Eisner 对区间长度、区间起点和分裂点各做一层循环,因此时间复杂度是 O(n³)。O(n) 是转移系统的解码复杂度,O(n²) 是 Eisner 的空间复杂度,O(2ⁿ) 是暴力枚举。

Q7. 对于投影性约束,下列说法正确的是?

单选题 · mid

  • A.只有 Arc-Standard 要求投影
  • B.转移系统默认只建不交叉弧← 正确答案
  • C.所有句子都必须投影
  • D.投影树必然有多个根

答案:B。Arc-Standard 和 Arc-Eager 都默认处理投影结构,它们在建弧时不会产生交叉弧。现实中存在少量非投影句子,因此解析器遇到这类句子时会产生错误,而不是“所有句子都必须投影”。

Q8. 关于 Eisner 算法,下列描述最准确的是?

单选题 · mid

  • A.贪心近似算法
  • B.精确动态规划← 正确答案
  • C.线性递归解析
  • D.无监督聚类算法

答案:B。Eisner 是全局精确的动态规划算法,它枚举所有分裂点并记忆子区间最优分数,最终得到全局最优解,不是贪心近似。它的复杂度是 O(n³),也不是线性。

Q9. 关于转移系统解析,下列说法正确的有?

多选题 · mid

  • A.解码只需线性步数← 正确答案
  • B.可用神经网络打分← 正确答案
  • C.保证全局最优
  • D.易于增量式处理← 正确答案

答案:ABD。转移系统每步做一次局部决策,解码步数为 O(n),且动作选择可以用神经网络打分;但由于贪心,它不保证全局最优。增量处理是它的天然优势,所以 A、B、D 正确。

Q10. 关于 Eisner 算法递归中的两个子表,下列说法正确的有?

多选题 · hard

  • A.C^L 表示最左词当头← 正确答案
  • B.C^R 表示最右词当头← 正确答案
  • C.分裂点 k 必须是中间点
  • D.合并时添加跨区间的弧← 正确答案

答案:ABD。C^L 和 C^R 分别表示头在区间最左和最右的子问题最优分数,分裂点 k 可以是区间内任意位置,不要求一定在正中。合并时确实要添加一条跨左右子区间的弧,所以选 A、B、D。

Q11. 工程上解析长文档时,通常优先选择转移系统的核心原因是?

单选题 · hard

  • A.能处理非投影语序
  • B.解码速度快延迟低← 正确答案
  • C.模型训练不需要数据
  • D.完全不受句子长度影响

答案:B。转移系统解码步数随句子长度线性增长,且每一步只需局部打分,延迟远低于 Eisner 的 O(n³) 全局搜索。它并非完全不受长度影响,也不保证处理非投影语序,更不能不训练数据。