句法特征#

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', '年', '心理', '语言', '学会', '在', '广州', '召开', '。']
['欢迎', '大家', '参加', '工作坊', '!']