本文目录导读:
依存分析(Dependency Parsing)的目标是分析句子中词语之间的语法依存关系,主谓关系”、“动宾关系”等,实现它的主流方法分为几个阶段,目前最常用的是基于神经网络的深度学习方法。
下面我将从核心概念、主流实现方法(传统 vs 深度学习)、具体工具和代码示例几个方面来详细说明。
核心概念
- 依存关系:句子中词与词之间的一种有向的、非对称的语法关系,一个词(依存词)依赖于另一个词(支配词/核心词)。
- 核心词:整个句子的“根节点”,通常是谓语动词。
- 依存弧:从支配词指向依存词的有向箭头。
- 每个依存弧上的标记,表示具体的关系类型(如
nsubj主语,dobj直接宾语)。
例子:句子“小明吃苹果。”
graph LR
root["ROOT"] -->|root| 吃
吃 -->|nsubj| 小明
吃 -->|dobj| 苹果
苹果 -->|punct| 。
在这个例子中,“吃”是ROOT(核心),“小明”是主语(依赖“吃”),“苹果”是宾语(依赖“吃”)。
主流实现方法
第一阶段:传统方法(基于规则与统计)
- 基于规则:人工编写语法规则(如“动词后的名词是宾语”)。缺点:覆盖性差,遇到复杂句子或新句型效果不好。
- 基于图的方法(如 Eisner 算法、CYK 算法):将依存分析看作寻找最大生成树问题,利用特征模板(如词性、距离等)计算每对词之间存在依存关系的概率。代表:MSTParser, MaltParser。
- 基于转移的方法(如 Nivre 算法):模拟从句子左侧扫描到右侧的过程,通过一系列“转移动作”(如 Shift、Reduce、Left-Arc、Right-Arc)逐步构建出依存弧。代表:MaltParser。
第二阶段:深度学习方法(当前主流)
当前几乎所有高效的依存分析器都基于神经网络,主要有两种范式:
基于图的方法
将问题转化为Biaffine(双仿射)分类问题。
- 输入:句子中的每个词及其词性(POS Tag)。
- 编码:使用双向LSTM(长短期记忆网络)或Transformer(如BERT)对输入序列进行编码,得到每个词的上下文相关向量表示。
- 打分:构建两个独立的MLP(多层感知机):
- 头词打分器:对每个词,预测它作为“支配词”的可能性。
- 依存词打分器:对每个词,预测它作为“依存词”的可能性。
- 双仿射(Biaffine)注意力:将头词和依存词的表示进行双线性变换,得到一个
n x n的分数矩阵。F[i][j]表示词 i 作为头词、词 j 作为依存词的分数。 - 解码:利用 Eisner算法或 Maximum Spanning Tree (MST) 算法,从分数矩阵中找出最高分的无环依存树(通常假设树是投影性投影的,即没有交叉的依存弧)。
- 关系分类:对每个预测出的依存弧,再做一个分类任务,判断其关系类型(如
nsubj)。
基于转移的方法
通过神经网络来预测转移动作序列。
- 状态表示:维护一个栈(Stack)、一个缓存(Buffer)和一个已构建的部分依存树结构,神经网络(如LSTM)将栈顶和缓存头的向量作为输入,编码当前状态。
- 动作预测:用Softmax分类器从上一步的向量预测下一个最可能的动作(Shift, Left-Arc, Right-Arc,以及带上具体关系标签)。
- 执行:执行预测的动作,更新状态。
- 循环:重复直到所有词都被处理完毕,得到完整的依存树。
目前性能最优的架构:基于图的深度双仿射(Deep Biaffine)解析器(如Dozat & Manning 2016模型)配合预训练语言模型(如BERT/ELMo)。
如何使用现有的工具实现?
你不需要自己从零实现神经网络,Python生态中有几个成熟、开箱即用的工具包,它们已经集成了最先进的依存分析模型。
推荐工具
- Stanza(推荐):
- 由斯坦福NLP小组开发,基于PyTorch,支持多种语言,其核心模型就是基于双仿射注意力和图方法的。
- 使用简单,准确率高。
- spaCy:
- 一个非常流行的工业级NLP库,集成了基于转移(Transition-based)的解析器。
- 优点:速度快,工程优化好,易于部署。
- 缺点:不支持直接输出解析树的完整结构(需要额外处理),自定义关系类型比较麻烦。
- HanLP:
由中国人开发,对中文有极佳的支持,集成了多种模型(包括基于Transformer的最新模型)。
代码示例(使用 Stanza)
import stanza
# 1. 下载并初始化英文模型(第一次运行会下载)
# 可以指定 'en' 或 'zh' 等
stanza.download('en')
nlp = stanza.Pipeline('en', processors='tokenize,pos,depparse')
# 2. 处理句子
doc = nlp("I shot an elephant in my pajamas.")
# 3. 提取依存分析结果
for sentence in doc.sentences:
print(f"句子: {sentence.text}")
for word in sentence.words:
# word.head 是支配词的ID (0表示根节点ROOT)
# word.id 是当前词的ID
# word.deprel 是依存关系标签
# word.text 是当前词
# sentence.words[word.head-1] 可以获取支配词(如果word.head > 0)
head_word = sentence.words[word.head - 1] if word.head > 0 else "ROOT"
print(f" {word.text} (ID={word.id}) --> {head_word} (ID={word.head}) | 关系: {word.deprel}")
输出示例:
句子: I shot an elephant in my pajamas.
I (ID=1) --> shot (ID=2) | 关系: nsubj
shot (ID=2) --> ROOT (ID=0) | 关系: root
an (ID=3) --> elephant (ID=4) | 关系: det
elephant (ID=4) --> shot (ID=2) | 关系: obj
in (ID=5) --> shot (ID=2) | 关系: prep
my (ID=6) --> pajamas (ID=7) | 关系: nmod:poss
pajamas (ID=7) --> in (ID=5) | 关系: pobj
. (ID=8) --> shot (ID=2) | 关系: punct
如何自己实现一个简单的(学习目的)?
如果想理解底层原理,可以尝试实现一个基于转移的线性模型(比如McDonald的MSTParser的简化版,或者基于简单特征的Nivre算法),核心步骤如下:
- 定义状态:
Stack,Buffer,Arcs。 - 定义特征模板:从状态中提取手工特征(如“栈顶的词是什么?”、“栈顶的词性是什么?”、“栈顶和缓存头的词对是什么?”)。
- 训练分类器:使用逻辑回归或感知机,以这些特征预测下一个动作(Shift, Left-Arc(rel), Right-Arc(rel))。
- 解码:在测试时,反复使用训练好的模型预测动作,执行动作,直到Buffer和Stack都为空。
- 如果你需要快速应用到生产环境:直接用 Stanza 或 spaCy,调用两行代码即可得到结果。
- 如果你想深入学习:研究双仿射注意力(Biaffine Attention) + 图解码(Eisner算法) 的论文(如Dozat & Manning 2016),然后尝试用PyTorch/TensorFlow复现一个,这是目前最主流的学术范式。