quot;的依存关系(即 $(w_j)$ 是父节点,$(w_i)$ 是子节点),然后**弹出 $(w_i)$**。 3. **RIGHT-ARC(r)** - 在栈顶记作 $(w_j)$,其下方记作 $(w_i)$。建立"$(w_i)$ ←(r)—$(w_j)quot;的依存关系(即 $(w_i)$ 是父节点,$(w_j)$ 是子节点),然后**弹出 $(w_j)$**。 > **注意:** 有些资料中对 LEFT-ARC/RIGHT-ARC 的方向描述可能略有差异,核心是"谁是 head,谁是 dependent"以及"移除哪个元素"。这里我们沿用与本示例相一致的版本:**Left-Arc** 去掉下方元素,**Right-Arc** 去掉顶部元素,并在弧 (arc) 中标明谁是父、谁是子。 ## 场景设置 - **初始状态**: - 栈:只含 `[ROOT]` - 缓冲区:`[He, read, a, book]` - 弧集 A={}A = \{\}(空) 我们一步步演示 8 次动作(Transition),最终得到完整依存树。 ### Step 0:初始状态 - **动作**:无(刚开始) - **Stack**:ROOTROOT - **Buffer**:He,read,a,bookHe, read, a, book - **Arcs**:∅\varnothing **还没有任何依存关系**,因此如果勉强用"树"来画,就是所有词都彼此独立,还没有边 (arc): ```Java Step 0: +---------+ | Stack | (top在最右) +---------+ | ROOT | +---------+ +------------------+ | Buffer | +------------------+ | He, read, a, book (front在左) +------------------+ arcs = {} (所有词彼此独立, 并无连接) ROOT He read a book ``` --- ### Step 1:SHIFT - **动作**:SHIFT - **效果**:把 "He" 从缓冲区前端移到栈顶 - **Arcs**:仍然是 ∅\varnothing ASCII "树"依然空空如也,因为 SHIFT 并不会产生依存关系: ```ascii Step 1: SHIFT Stack: [ROOT, He] ^^^^^ ^^^ bottom top Buffer: [read, a, book] arcs = {} ROOT He read a book (依然没有任何弧) ``` --- ### Step 2:SHIFT - **动作**:SHIFT - **效果**:把 "read" 从缓冲区前端移到栈顶 - **Arcs**:仍然是 ∅\varnothing 依然没有新弧: ```java Step 2: SHIFT Stack: [ROOT, He, read] top Buffer: [a, book] arcs = {} ROOT He read a book ``` --- ### Step 3:LEFT-ARC(nsubj) - **动作**:LEFT-ARC(nsubj) - **栈顶**是 "read",下方是 "He" => 建立 `(read ←(nsubj)— He)` 并弹出 "He" - **Arcs**:{(read,nsubj,He)}\{(read, nsubj, He)\} 现在有了第一条弧:**"read" 是父节点,"He" 是它的主语**。 用 ASCII 画"部分树"如下: ```Java Step 3: LEFT-ARC(nsubj) Stack: [ROOT, read] top Buffer: [a, book] arcs = { (read, nsubj, He) } 目前可视化为: read / He (nsubj) (还没与 ROOT、a、book 等连接) ``` --- ### Step 4:SHIFT - **动作**:SHIFT - **效果**:把 "a" 从缓冲区前端移到栈顶 - **Arcs**:仍是 { (read, nsubj, He) } 部分树不变: ```Java Step 4: SHIFT Stack: [ROOT, read, a] Buffer: [book] arcs = { (read, nsubj, He) } read / He (nsubj) 剩余词: ROOT, a, book (无连接) ``` --- ### Step 5:SHIFT - **动作**:SHIFT - **效果**:把 "book" 从缓冲区前端移到栈顶 - **Arcs**:仍是 { (read, nsubj, He) } 依然没有新的弧: ```Java Step 5: SHIFT Stack: [ROOT, read, book] Buffer: [] arcs = { (read, nsubj, He) } read / He (nsubj) 剩余词: ROOT, a, book (无连接) ``` --- ### Step 6:LEFT-ARC(det) - **动作**:LEFT-ARC(det) - **栈顶**是 "book",下方是 "a" => 建立 `(book ←(det)— a)` 并弹出 "a" - **Arcs**:{ (read, nsubj, He), (book, det, a) } 现在产生第二条弧:**"book" 是父节点,"a" 是修饰它的限定词 (det)**。 所以我们有两棵小树: ```Java Step 6: LEFT-ARC(det) Stack: [ROOT, read] Buffer: [] arcs = { (read, nsubj, He), (book, det, a) } 部分树1: read / He (nsubj) 部分树2: book / a (det) ( 还未与 ROOT 相连 ) ``` --- ### Step 7:RIGHT-ARC(obj) - **动作**:RIGHT-ARC(obj) - **栈顶**是 "book",下方是 "read" => 建立 `(read ←(obj)— book)` 并弹出 "book" - **Arcs**:{ (read, nsubj, He), (book, det, a), (read, obj, book) } 现在我们把"read" 和"book"连起来,意味着**"read" 是父节点,"book" 是它的宾语 (obj)**。于是两棵小树合二为一: ```Java Step 7: RIGHT-ARC(obj) Stack: [ROOT, read] Buffer: [] arcs = { (read, nsubj, He), (book, det, a), (read, obj, book) } 合并树: read / \ (nsubj)/ \(obj) He book / a (det) 尚未和 ROOT 相连 ``` --- ### Step 8:RIGHT-ARC(root) - **动作**:RIGHT-ARC(root) - **栈顶**是 "read",下方是 "ROOT" => 建立 `(ROOT ←(root)— read)` 并弹出 "read" - **Arcs**:{ (ROOT, root, read), (read, nsubj, He), (book, det, a), (read, obj, book) } 这一步把"ROOT"与"read"连起来,表示"read"是全句的根 (root)。 **最终**树形结构: ```Java Step 8: RIGHT-ARC(root) Stack: [ROOT] Buffer: [] arcs = { (ROOT, root, read), (read, nsubj, He), (read, obj, book), (book, det, a) } 完整依存树 (包含 ROOT) 如下: ROOT \ (root) read / \ (nsubj) (obj) He book / a (det) ``` 很多时候我们在可视化时会把 `ROOT` 省略,直接把 "read" 当成可视化的**最高节点**: ```Java read (root) / \ (nsubj)He (obj)book / a (det) ``` 这样就是一个标准的依存树: - 谓语动词 "read" 在根位置; - "He" 是主语 (nsubj); - "book" 是宾语 (obj); - "a" 依存于 "book" (det)。 --- ## 总结 通过上面**每一步都给出 arcs 和对应的"小树"**,可以清晰地看到: 1. **初始 (Arcs = {}) 时**:所有单词都"各自独立"。 2. **每个 SHIFT** 只是在栈与缓冲区之间移动单词,并不产生弧。 3. **每个 LEFT-ARC/RIGHT-ARC** 会在 `Arcs` 里多加入一个依存关系,并从栈中弹出相应单词。 4. **最终** 当缓冲区为空、栈只剩 `ROOT`,依存关系 `Arcs` 中就包含整句依存树。 这样就实现了**从无到有**、**一步步**添加依存弧的"转移式依存解析"过程。 ## 完整依存关系 最终得到的依存关系集合为: ```Java { (ROOT, root, read), # read 是整句核心动词 (read, nsubj, He), # read 的主语是 He (read, obj, book), # read 的宾语是 book (book, det, a) # book 的修饰语是冠词 a } ``` 如果把这些关系画成依存树(简写省略 w0、w1 等编号),可以这样理解: ```Java ROOT ↓ (root) read ------------> He (nsubj) \ \ (obj) book ---> a (det) ``` 或更常见的写法是把 `ROOT` 省去,只说 `read` 是根节点: ```Java read (root) / \ (nsubj) / \ (obj) He book | (det) a ``` > 由此可见,我们**一步步**地用 SHIFT / LEFT-ARC / RIGHT-ARC 这些动作,配合栈与缓冲区的状态变化,最终构建出该句子的依存树。 --- ### 小结 - **初始**:栈只有 `ROOT`,缓冲区里是句子所有单词,弧集为空。 - **每步**:根据当前栈顶、次顶、缓冲区等上下文信息,选择一个动作(如 SHIFT 或 LEFT-ARC/RIGHT-ARC + 关系标签)。 - **结束**:当缓冲区清空、栈只剩 `ROOT` 时,弧集里就包含了完整的依存关系,解析完成。 在神经网络实现中,系统会对当前状态 (σ,β,A)(\sigma, \beta, A) 提取特征(单词向量、POS、已建关系等),用模型来**预测**最优动作(transition)和依存关系标签,从而实现自动、贪心地构建依存树。上述 ASCII 步骤演示的正是"转移式依存解析"的核心逻辑。 ### 神经网络 - **输入特征改用嵌入向量** (embedding)。 - 例如,把当前栈顶 2-3 个单词、缓冲区前面 2-3 个单词等,都变成对应的词向量。 - 同时可能还会把它们的词性、已经建立好的依存标签等也映射成 embedding。 - 把所有这些向量**拼接 (concatenate)** 在一起,作为神经网络的输入。 - **神经网络计算** - 常见做法:输入层 → 若干隐藏层 (多层感知器 MLP) → 输出层 (softmax)。 - 输出层会给出对三个动作 (Shift / Left-Arc / Right-Arc) 以及其对应依存标签的概率分布。 - 训练时,用交叉熵损失 (cross entropy) + 反向传播 (backpropagation) 来更新网络参数,包括嵌入矩阵和全连接层。 - **贪心解码** - 在真实解析时,我们从初始状态开始,**每一步都选置信度最高的动作**,直到到达终止状态,从而得到依存树。