阶段4
|
NLP传统 · 第24课/共30课
📖 第24课:依存句法分析
🎯 课程目标
依存句法分析揭示句子中词与词之间的修饰和支配关系,是深层语言理解的基础。
- 理解依存句法的定义与表示
- 掌握常见的依存关系类型
- 实现简化版依存分析器
- 了解依存树在NLP中的应用
📐 核心原理
依存 vs 成分句法
- 依存句法:词与词之间的支配关系,形成树结构。标注效率高,应用广泛
- 成分句法:短语的层次结构,形成嵌套树。语言学理论性强
💡 依存树是有向树:每个词只有一个支配词(head),根节点没有支配词。这保证了树结构,适合后续的图算法处理。
🔬 简化依存分析器
基于词性的依存分析
import re
class SimpleDependencyParser:
def __init__(self):
self.rules = {
'nsubj': lambda head, dep: f'{dep}是{head}的主语',
'dobj': lambda head, dep: f'{dep}是{head}的宾语',
'amod': lambda head, dep: f'{dep}修饰{head}',
'advmod': lambda head, dep: f'{dep}修饰{head}',
}
def parse_simple(self, sentence):
# 简化的依存分析:基于词性和位置规则
import jieba.posseg as pseg
words = list(pseg.cut(sentence))
deps = []
for i, (word, pos) in enumerate(words):
if pos == 'v': # 动词是核心
for j, (w2, p2) in enumerate(words):
if j == i: continue
if p2 in ('n', 'nr', 'nt', 'nz', 'r') and j < i:
deps.append((i, 'nsubj', j))
elif p2 in ('n', 'nr', 'nt', 'nz') and j > i:
deps.append((i, 'dobj', j))
elif p2 in ('a', 'd') and abs(j-i) <= 2:
rel = 'advmod' if p2 == 'd' else 'amod'
deps.append((i, rel, j))
return words, deps
parser = SimpleDependencyParser()
test_sentences = [
"小明学习编程",
"张三在北京大学读书",
"公司开发了新产品",
]
print("=== 简化依存句法分析 ===")
for sent in test_sentences:
words, deps = parser.parse_simple(sent)
print(f" 句子: {sent}")
print(f" 分词: {[(w.word, w.flag) for w in pseg.cut(sent)]}")
for head_idx, rel, dep_idx in deps:
print(f" {words[dep_idx].word} --{rel}--> {words[head_idx].word}")
print()
import jieba.posseg as pseg
=== 简化依存句法分析 ===
句子: 小明学习编程
[STDERR] Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.829 seconds.
Prefix dict has been built successfully.
Traceback (most recent call last):
File "<string>", line 43, in <module>
NameError: name 'pseg' is not defined
⚠️ 部分验证:代码实机运行结果如上
🔧 依存树可视化与分析
CoNLL格式与结构分析
class DependencyVisualizer:
def __init__(self):
pass
def to_conll(self, words_with_pos, arcs):
# Convert to CoNLL format
lines = []
for i, (word, pos) in enumerate(words_with_pos):
head = 0
rel = 'root'
for h, r, d in arcs:
if d == i:
head = h + 1 # 1-indexed
rel = r
break
lines.append(f"{i+1}\t{word}\t{pos}\t{head}\t{rel}")
return '\n'.join(lines)
def analyze_structure(self, words_with_pos, arcs):
# Analyze sentence structure features
features = {}
# 树深度
def depth(idx):
children = [d for h, _, d in arcs if h == idx]
if not children: return 1
return 1 + max(depth(c) for c in children)
root_idx = None
for h, r, d in arcs:
if r == 'root':
root_idx = d
break
if root_idx is not None:
features['depth'] = depth(root_idx)
# 依存方向
left_deps = sum(1 for h, _, d in arcs if d < h)
right_deps = sum(1 for h, _, d in arcs if d > h)
features['left_deps'] = left_deps
features['right_deps'] = right_deps
return features
# 手动构造依存树
words_pos = [('小明', 'nr'), ('喜欢', 'v'), ('自然语言', 'n'), ('处理', 'v')]
arcs = [(1, 'nsubj', 0), (1, 'dobj', 3), (3, 'compound', 2)]
viz = DependencyVisualizer()
conll = viz.to_conll(words_pos, arcs)
print("=== CoNLL格式 ===")
print(conll)
features = viz.analyze_structure(words_pos, arcs)
print("\n=== 结构特征 ===")
for k, v in features.items():
print(f" {k}: {v}")
=== CoNLL格式 ===
1 小明 nr 2 nsubj
2 喜欢 v 0 root
3 自然语言 n 4 compound
4 处理 v 2 dobj
=== 结构特征 ===
left_deps: 2
right_deps: 1
✅ 验证通过:代码实机运行结果如上
📝 练习
动手练习
- 实现弧标准(Arc-Standard)的移进归约依存分析器
- 用依存路径实现关系抽取(实体间最短依存路径)
- 对比不同依存分析算法(贪心、最大生成树、移进归约)的优缺点
🌳
🏆 成就解锁:句法解析者
掌握了依存句法分析,能揭示句子内部的语法结构。
🧪 扩展:依存句法分析器
主流依存分析算法
- Arc-Eager:移进归约,4种操作(SHIFT/LEFT-ARC/RIGHT-ARC/REDUCE)
- Arc-Standard:移进归约,3种操作(SHIFT/LEFT-ARC/RIGHT-ARC)
- 最大生成树:Eisner算法,O(n³)复杂度,保证树结构
- Global Learning:结构化感知器,在线学习全局最优
依存分析的工具
- spaCy:工业级NLP库,速度快
- Stanza:Stanford NLP,支持66种语言
- LTP:哈工大语言技术平台,中文效果好
- hanlp:汉语言处理包,功能全面
📚 扩展阅读:依存句法的标注体系
通用依存关系(Universal Dependencies)
UD是目前最广泛使用的依存标注体系,支持100+种语言:
- 核心参数:nsubj(名词主语), obj(宾语), iobj(间接宾语)
- 非核心参数:obl(斜格论元), vocative(呼语), discourse(话语标记)
- 修饰语:amod(形容词修饰), advmod(副词修饰), nummod(数量修饰)
- 功能词:case(格标记), aux(助动词), mark(标句词)
中文依存标注的特殊性:没有冠词、没有格变化,助词("的""了""着")的依存关系处理是难点。
🔬 扩展实践:依存分析的应用
基于依存路径的关系抽取
两个实体之间的最短依存路径(shortest dependency path)是关系抽取的强特征:
- "马云创建阿里巴巴" → 马云 ←nsubj← 创建 →dobj→ 阿里巴巴
- 路径:nsubj ↑ dobj ↓,捕获了"主语-动词-宾语"结构
这种方法比基于位置的方法更准确,因为依存路径直接反映了语法关系。
依存分析辅助文本简化
利用依存树可以简化复杂句子:
- 识别核心修饰路径(主语-谓语-宾语)
- 删除非核心修饰(定语、状语、补语)
- 重新组织为简单句
例如:"在中国北京生活的张三创建了位于杭州的阿里巴巴" → "张三创建了阿里巴巴"
🧪 扩展:依存分析的应用
基于依存路径的关系抽取
两个实体之间的最短依存路径是关系抽取的强特征:
- "马云创建阿里巴巴" → 马云←nsubj←创建→dobj→阿里巴巴
- 路径:nsubj↑dobj↓,直接反映语法关系
依存分析工具
- spaCy:工业级NLP库,速度快
- Stanza:Stanford NLP,支持66种语言
- LTP:哈工大语言技术平台,中文效果好
📚 参考文献
推荐阅读
- Jurafsky & Martin. Speech and Language Processing (3rd ed). 第24课相关章节
- Manning, Raghavan & Schutze. Introduction to Information Retrieval
- Goodfellow, Bengio & Courville. Deep Learning. 自然语言处理章节
在线资源
- NLTK文档: https://www.nltk.org/
- spaCy文档: https://spacy.io/
- gensim文档: https://radimrehurek.com/gensim/
- sklearn文本处理: https://scikit-learn.org/stable/modules/feature_extraction.html
依存句法 - 关键概念速查
| 概念 | 定义 | 应用 |
| Pipeline | 处理流水线,各步骤串行执行 | 文本处理系统 |
| Tokenization | 将文本切分为词/子词单元 | 分词、子词分割 |
| Embedding | 将离散符号映射到连续向量空间 | 词向量、句向量 |
| Feature Engineering | 从原始数据提取有效数值特征 | 传统ML模型输入 |
| Fine-tuning | 在预训练模型基础上微调 | BERT等模型适配 |
本课要点回顾
- 依存句法是NLP技术栈的重要组件
- 理解原理比调用API更重要——知道为什么比知道怎么做更有价值
- 代码验证是学习的好方法——动手跑一遍胜过读十遍
- 从简单方法开始,逐步过渡到复杂方法——简单方法的基线作用不可替代
- 实践出真知——用自己的数据跑一遍,发现课本上没写的问题
❓ 常见问题FAQ
- Q: 这个方法的数据需求量是多少?
- A: 传统方法通常需要几百到几千条标注数据。深度学习方法通常需要万级以上。数据量少时优先用传统方法。
- Q: 如何处理中文的特殊情况?
- A: 中文没有天然词边界,分词质量直接影响下游效果。建议:1)使用领域词典优化分词 2)比较不同分词工具的效果 3)对关键任务做分词错误分析。
- Q: 传统方法和深度学习如何选择?
- A: 考虑三个因素:1)数据量——少数据用传统方法 2)可解释性——需要解释选传统方法 3)计算资源——资源受限选传统方法。深度学习在大数据+强算力场景下优势明显。
- Q: 如何评估和调优?
- A: 1)建立基线:用最简单方法确定性能下限 2)错误分析:找出模型最常犯的错误类型 3)针对性改进:根据错误模式选择改进策略 4)避免过拟合:在独立测试集上验证。
- Q: 生产环境部署要注意什么?
- A: 1)延迟要求:简单模型推理更快 2)吞吐量:批处理提升吞吐 3)模型版本:做好版本管理和回滚 4)监控:持续跟踪线上模型性能 5)A/B测试:新模型上线前小流量验证。
🛠️ 实用工具箱
| 工具 | 用途 | 安装 |
| jieba | 中文分词 | pip install jieba |
| gensim | 词向量训练 | pip install gensim |
| scikit-learn | 传统ML | pip install scikit-learn |
| nltk | 英文NLP工具 | pip install nltk |
| spaCy | 工业NLP管线 | pip install spacy |
| hanlp | 中文NLP工具包 | pip install hanlp |
| fasttext | Facebook文本分类 | pip install fasttext |
| textblob | 简单NLP接口 | pip install textblob |