从CYK算法到知识图谱:手把手复现一个简易的汉语依存句法分析器
·
从CYK算法到知识图谱:手把手构建汉语依存句法分析器
在自然语言处理领域,句法分析是理解文本结构的基础环节。想象一下,当你阅读"小猫追逐毛线球"这句话时,大脑能自动识别"小猫"是动作发出者,"毛线球"是动作承受者——这正是句法分析要解决的问题。本文将带你用Python实现一个能自动分析中文句子结构的工具,从理论基础到完整代码实现,最终生成可视化的语义网络。
1. 环境准备与基础概念
在开始编码前,我们需要明确几个核心概念。依存语法认为句子成分间存在主从关系,比如"吃"支配"苹果";而短语结构语法则把句子分解为嵌套的组成成分,如[S [NP 我] [VP [V 吃] [NP 苹果]]]。这两种视角各有优势,我们的项目将结合它们的特点。
准备开发环境:
pip install stanfordnlp networkx matplotlib
注意:Stanford CoreNLP需要Java环境,建议下载最新版并设置环境变量。若使用transformers库,可安装:
pip install transformers pygraphviz
关键术语对照表:
| 术语 | 英文 | 说明 |
|---|---|---|
| 支配者 | Head | 在依存关系中处于支配地位的词 |
| 从属者 | Dependent | 依存关系中从属于支配者的词 |
| 价数 | Valence | 动词能支配的名词短语数量 |
2. 基于CYK算法的短语结构分析
CYK算法采用动态规划思想,其核心是填充一个三角矩阵。假设我们要分析"我喜欢编程"这个句子:
# 示例文法规则
grammar = {
'S': ['NP VP'],
'NP': ['我', '编程'],
'VP': ['V NP'],
'V': ['喜欢']
}
def cyk_parse(sentence, grammar):
words = sentence.split()
n = len(words)
# 初始化三角矩阵
table = [[set() for _ in range(n)] for _ in range(n)]
# 填充对角线(底层)
for i in range(n):
for lhs in grammar:
if words[i] in grammar[lhs]:
table[i][i].add(lhs)
# 自底向上填充
for length in range(2, n+1):
for i in range(n-length+1):
j = i+length-1
for k in range(i, j):
for lhs in grammar:
for rule in grammar[lhs]:
if len(rule.split()) == 2:
B, C = rule.split()
if B in table[i][k] and C in table[k+1][j]:
table[i][j].add(lhs)
return 'S' in table[0][n-1]
提示:实际应用中建议使用PCFG(概率上下文无关文法),它能通过概率解决歧义问题。例如"进口汽车"可能被错误分析为[VP 进口][NP 汽车],而PCFG会根据语料库统计选择更可能的[NP [Adj 进口][N 汽车]]结构。
常见问题排查:
- 若返回False,检查单词是否全部被文法规则覆盖
- 矩阵索引从0开始,注意Python的区间是左闭右开
- 复杂句子建议先进行分词和词性标注
3. 移进-归约依存分析实现
移进-归约算法通过栈和缓冲区模拟人类分析句子的过程。我们定义一个简化版的处理器:
class DependencyParser:
def __init__(self):
self.stack = []
self.buffer = []
self.relations = []
def parse(self, tokens):
self.buffer = list(tokens)
while len(self.buffer) > 0 or len(self.stack) > 1:
if self.shift_condition():
self.shift()
elif self.reduce_left_condition():
self.reduce_left()
else:
self.reduce_right()
return self.relations
def shift_condition(self):
return len(self.stack) < 2 or len(self.buffer) > 0
def reduce_left_condition(self):
# 简化的判断逻辑:栈顶两个词存在依存关系
return len(self.stack) >= 2 and random.random() > 0.5 # 实际应替换为模型预测
典型动作序列示例:
- SHIFT: 将"我"移入栈
- SHIFT: 将"喜欢"移入栈
- REDUCE_L: 建立"喜欢"→"我"的关系
- SHIFT: 将"编程"移入栈
- REDUCE_R: 建立"喜欢"→"编程"的关系
进阶技巧:使用神经网络预测动作:
from transformers import BertTokenizer, BertModel
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertModel.from_pretrained('bert-base-chinese')
def predict_action(stack, buffer):
inputs = tokenizer(stack[-2:]+buffer[:1], return_tensors='pt', padding=True)
outputs = model(**inputs)
# 添加分类头预测动作类型
...
4. 知识图谱可视化与实践优化
将分析结果转化为图结构,便于直观理解:
import networkx as nx
import matplotlib.pyplot as plt
def visualize_dependencies(relations):
G = nx.DiGraph()
for gov, dep in relations:
G.add_edge(gov, dep)
pos = nx.spring_layout(G)
nx.draw(G, pos, with_labels=True, node_size=2000)
plt.show()
实际项目中的优化策略:
- 缓存机制:对常见短语预存分析结果
- 并行处理:使用多线程处理长文本
- 增量学习:记录错误分析并反馈调整模型
# 性能优化示例:使用LRU缓存
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_parse(sentence):
return parser.parse(sentence)
处理复杂句子的技巧:
- 先拆分逗号分隔的从句
- 处理核心动词短语
- 最后整合修饰成分
- 对不确定的关系添加概率权重
5. 项目扩展与前沿探索
现代NLP系统已逐渐转向端到端的解决方案。你可以尝试:
- 用预训练模型增强分析器:
from transformers import pipeline
nlp = pipeline('dependency-parsing', model='ckiplab/bert-base-chinese-dependency-parsing')
result = nlp("自然语言处理很有趣")
- 结合语义角色标注:
def extract_semantic_roles(sentence):
# 识别施事、受事、时间等语义角色
...
- 构建领域特定优化版本:
- 法律文本:侧重长距离依存
- 医疗报告:关注实体间关系
- 社交媒体:处理非规范表达
在调试过程中发现,中文处理要特别注意:
- 量词结构:"三本书"应分析为[NP [NUM 三][CL 本][N 书]]
- 把字句:"把苹果吃了"中"把"需要特殊处理
- 离合词:"洗澡"可能被错误拆分为"洗"和"澡"
更多推荐

所有评论(0)