句法特征#
import re
# 数据处理及可视化
import pandas as pd
# 自然语言处理
import hanlp
## workshop中的例子,研究中一般会把标点去掉,但是这里保留了标点,模型也是能够解析标点的
Hanlp = hanlp.load(hanlp.pretrained.mtl.CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH) # 选择使用的模型
doc = Hanlp('欢迎大家参加工作坊!', tasks=['dep', 'con']) # 在tasks中选择需要的任务,如果不设置就进行所有任务(运行起来会慢一点)
doc.pretty_print()
Dep Tre
───────
┌┬──┬──
││ └─►
│└─►┌──
│ └─►
└─────►
Tok
───
欢迎
大家
参加
工作坊
!
Relat
─────
root
dobj
dep
dobj
punct
Tok
───
欢迎
大家
参加
工作坊
!
P 3 4 5 6 7
───────────────────────────────────────
_──────────────────────────┐
_───────────────────►NP────┤
_──────────┐ ├►VP ───┐
_───►NP ───┴►VP ────►IP ───┘ ├►IP
_──────────────────────────────────┘
1 成分句法#
成分句法输出得到的是一个树结构的数据,可以看作一个嵌套的列表。我们可以:
访问句法树的一些属性
转换为括号表示法,计算括号数量
访问句法树的子树
Hanlp = hanlp.load(hanlp.pretrained.mtl.CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH)
doc = Hanlp('欢迎大家参加工作坊!')
tree = doc['con']
# 叶结点的位置
for i in range(len(tree.leaves())):
print(tree.leaf_treeposition(i))
(0, 0, 0, 0)
(0, 0, 1, 0, 0)
(0, 0, 2, 0, 0, 0)
(0, 0, 2, 0, 1, 0, 0)
(0, 1, 0)
tree[0, 0, 1, 0, 0]
'大家'
# 转为括号表示法
bracket_form = tree.pformat().replace ('\n', '').replace(' ', '') # 去掉换行和空格
bracket_form
'(TOP(IP(VP(VV欢迎)(NP(PN大家))(IP(VP(VV参加)(NP(NN工作坊)))))(PU!)))'
# 转换为Chomsky Normal Form,可以用tree.un_chomsky_normal_form()转换回来
tree.chomsky_normal_form()
bracket_form = tree.pformat().replace ('\n', '').replace(' ', '')
print(bracket_form)
(TOP(IP(VP(VV欢迎)(VP|<NP-IP>(NP(PN大家))(IP(VP(VV参加)(NP(NN工作坊))))))(PU!)))
# 输出中有些节点只派生出一支,是冗余的(例如最外层的TOP根结点只派生出IP,以及句子中的IP只派生出VP),可以选择压缩节点
tree.collapse_unary(collapseRoot=True, joinChar='|') # 压缩冗余节点,压缩的节点用|来表示
bracket_form = tree.pformat().replace ('\n', '').replace(' ', '')
bracket_form
'(TOP|IP(VP(VV欢迎)(VP|<NP-IP>(NP(PN大家))(IP|VP(VV参加)(NP(NN工作坊)))))(PU!))'
# 计算括号表示法中每个词的括号数
bracket_clean= re.sub("([^()])", "", bracket_form) # 只保留括号
print(bracket_clean)
# 计算左括号数
left_bracket = [len(re.findall("\(", i)) for i in bracket_clean]
left_bracket_count = []
for i in left_bracket:
if len(left_bracket_count) == 0 or (i == 1 and j != 1):
left_bracket_count.append(1)
elif i == 1 and j == 1:
left_bracket_count[-1] += 1
j = i
print("左括号数:", left_bracket_count)
# 计算右括号数
right_bracket = [len(re.findall("\)", i)) for i in bracket_clean]
right_bracket_count = []; j = 0
for i in right_bracket:
if i == 1 and j != 1:
right_bracket_count.append(1)
elif i == 1 and j == 1:
right_bracket_count[-1] += 1
j = i
print("右括号数:", right_bracket_count)
# 可以保存为 dataframe 进行进一步的句法特征分析
df_bracket = pd.DataFrame([tree.leaves(), left_bracket_count, right_bracket_count]).T
df_bracket.columns = ['word', 'left_bracket', 'right_bracket']
# df_bracket.to_csv('bracket.csv', index=False) # 保存为csv文件
df_bracket
((()((())(()(()))))())
左括号数: [3, 3, 2, 2, 1]
右括号数: [1, 2, 1, 5, 2]
| word | left_bracket | right_bracket | |
|---|---|---|---|
| 0 | 欢迎 | 3 | 1 |
| 1 | 大家 | 3 | 2 |
| 2 | 参加 | 2 | 1 |
| 3 | 工作坊 | 2 | 5 |
| 4 | ! | 1 | 2 |
# 句法树的属性
print("Terminal nodes:", tree.leaves())
print("Tree depth:", tree.height())
print("Tree productions:", tree.productions())
print("Part of Speech:", tree.pos())
Terminal nodes: ['欢迎', '大家', '参加', '工作坊', '!']
Tree depth: 7
Tree productions: [TOP|IP -> VP PU, VP -> VV VP|<NP-IP>, VV -> '欢迎', VP|<NP-IP> -> NP IP|VP, NP -> PN, PN -> '大家', IP|VP -> VV NP, VV -> '参加', NP -> NN, NN -> '工作坊', PU -> '!']
Part of Speech: [('欢迎', 'VV'), ('大家', 'PN'), ('参加', 'VV'), ('工作坊', 'NN'), ('!', 'PU')]
# 句法树的嵌套结构
for i in tree.subtrees(): # 根据Tree productions,遍历所有的子树,每一棵子树都是一个Tree对象,可以进行之前相同的操作
print(i)
(TOP|IP
(VP
(VV 欢迎)
(VP|<NP-IP> (NP (PN 大家)) (IP|VP (VV 参加) (NP (NN 工作坊)))))
(PU !))
(VP (VV 欢迎) (VP|<NP-IP> (NP (PN 大家)) (IP|VP (VV 参加) (NP (NN 工作坊)))))
(VV 欢迎)
(VP|<NP-IP> (NP (PN 大家)) (IP|VP (VV 参加) (NP (NN 工作坊))))
(NP (PN 大家))
(PN 大家)
(IP|VP (VV 参加) (NP (NN 工作坊)))
(VV 参加)
(NP (NN 工作坊))
(NN 工作坊)
(PU !)
# 通过索引访问句法树的子树
treepositions = tree.treepositions() # 所有节点的索引
treepositions
[(),
(0,),
(0, 0),
(0, 0, 0),
(0, 1),
(0, 1, 0),
(0, 1, 0, 0),
(0, 1, 0, 0, 0),
(0, 1, 1),
(0, 1, 1, 0),
(0, 1, 1, 0, 0),
(0, 1, 1, 1),
(0, 1, 1, 1, 0),
(0, 1, 1, 1, 0, 0),
(1,),
(1, 0)]
for i in treepositions: # 遍历所有节点
print(tree[i])
(TOP|IP
(VP
(VV 欢迎)
(VP|<NP-IP> (NP (PN 大家)) (IP|VP (VV 参加) (NP (NN 工作坊)))))
(PU !))
(VP (VV 欢迎) (VP|<NP-IP> (NP (PN 大家)) (IP|VP (VV 参加) (NP (NN 工作坊)))))
(VV 欢迎)
欢迎
(VP|<NP-IP> (NP (PN 大家)) (IP|VP (VV 参加) (NP (NN 工作坊))))
(NP (PN 大家))
(PN 大家)
大家
(IP|VP (VV 参加) (NP (NN 工作坊)))
(VV 参加)
参加
(NP (NN 工作坊))
(NN 工作坊)
工作坊
(PU !)
!
2 依存句法#
依存句法的数据结构更加简单,为一个列表
[(head, relation), ... ]。列表中第\(i\)个值中包括了它的核心词的位置以及它与核心词之间的依存关系
Hanlp = hanlp.load(hanlp.pretrained.mtl.CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH)
doc = Hanlp('欢迎大家参加工作坊!')
doc['dep']
[(0, 'root'), (1, 'dobj'), (1, 'dep'), (3, 'dobj'), (1, 'punct')]
# 可以保存为 dataframe 进行进一步的句法特征分析
df_dep = pd.DataFrame(doc['dep'], columns=['head', 'rel'])
df_dep['word'] = doc['tok/fine']
df_dep = df_dep[['word', 'head', 'rel']]
df_dep
| word | head | rel | |
|---|---|---|---|
| 0 | 欢迎 | 0 | root |
| 1 | 大家 | 1 | dobj |
| 2 | 参加 | 1 | dep |
| 3 | 工作坊 | 3 | dobj |
| 4 | ! | 1 | punct |
3 批量操作#
只需要将要处理的句子放在list中,一起进行特征抽取即可。这对所有特征都适用,不仅是句法特征。
sentences = ['2023年心理语言学会在广州召开。', '欢迎大家参加工作坊!']
docs = Hanlp(sentences)
docs.pretty_print()
Dep Tree
────────
┌─►
┌───►└──
│ ┌──►
│ │┌─►
│┌─►└┴──
││┌─►┌──
│││ └─►
└┴┴──┬──
└─►
Toke
────
2023
年
心理
语言
学会
在
广州
召开
。
Relati
──────
nummod
nsubj
nn
nn
nsubj
prep
pobj
root
punct
Po
──
NT
M
NN
NN
NN
P
NR
VV
PU
Toke
────
2023
年
心理
语言
学会
在
广州
召开
。
NER Type
────────────
───►DATE
───►LOCATION
Toke
────
2023
年
心理
语言
学会
在
广州
召开
。
SRL PA1
────────────
◄─┐
◄─┴►ARGM-TMP
◄─┐
├►ARG1
◄─┘
◄─┐
◄─┴►ARGM-LOC
╟──►PRED
Toke
────
2023
年
心理
语言
学会
在
广州
召开
。
Po 3 4 5 6
────────────────────────────────
NT──────────┐
M ───►CLP ──┴►QP ───┐
NN──┐ ├►NP ───┐
NN ├────────►NP ───┘ │
NN──┘ │
P ──────────┐ ├►IP
NR───►NP ───┴►PP ───┐ │
VV───────────►VP ───┴►VP────┤
PU──────────────────────────┘
Dep Tre
───────
┌┬──┬──
││ └─►
│└─►┌──
│ └─►
└─────►
Tok
───
欢迎
大家
参加
工作坊
!
Relat
─────
root
dobj
dep
dobj
punct
Po
──
VV
PN
VV
NN
PU
Tok
───
欢迎
大家
参加
工作坊
!
SRL PA1
────────
╟──►PRED
───►ARG1
◄─┐
◄─┴►ARG2
Tok
───
欢迎
大家
参加
工作坊
!
SRL PA2
────────
╟──►PRED
───►ARG1
Tok
───
欢迎
大家
参加
工作坊
!
Po 3 4 5 6 7
────────────────────────────────────────
VV──────────────────────────┐
PN───────────────────►NP────┤
VV──────────┐ ├►VP ───┐
NN───►NP ───┴►VP ────►IP ───┘ ├►IP
PU──────────────────────────────────┘
# 提取出来的特征直接索引即可
print("句子数量为:", docs.count_sentences())
for i in range(docs.count_sentences()):
print(docs['tok/fine'][i])
句子数量为: 2
['2023', '年', '心理', '语言', '学会', '在', '广州', '召开', '。']
['欢迎', '大家', '参加', '工作坊', '!']