基于句法依存树的信息抽取

信息抽取是一个常见的nlp任务,为经常一起提到的知识图谱的基础。

这里有一份比较好的知识图谱入门资料:
Mining Knowledge Graphs from Text

信息抽取分为有监督和无监督方法。实际中监督信息往往是缺失的,所以本文主要提无监督方法。

在无监督方法中,一个广泛采用的工具是句法依存树,或者叫句法解析树(Dependency Tree)。

可视化工具:

工作中总结出来的一种基于句法解析树的信息提取的一般方法(尚未完善):

  1. 从CONLL格式的句法依存解析结果,生成具备孩子节点和父节点索引的树,并获取根结点root_id;
  2. 自上而下递归的进行解析,孩子节点的解析结果上传到父节点进行汇总;
    1. 如果当前节点为叶子节点(无孩子节点),判断当前节点类型,返回dict,上传给父节点;
    2. 如果当前词为动词(一般句子的核心是动词,一般方法也都是从动词开始扩展)或者用户指定的目标词,根据制定的规则提取指定关系类型的孩子,如定中关系、状中关系、动宾关系、主谓关系等,这里需要注意的是并列关系,有可能是补充,有可能是同级并列;
    3. 如果当前词非动词,先判断其所属信息类型,若能判断,关系向孩子传递,如不能判断,等待孩子节点上传的结果;
    4. 合并所有孩子的信息,按原始句子顺序排序
  3. 修正后处理

以提取文本中,关于道路信息的(时间、原因、地点、时间)四元组为例,输入句子:

黄石高速:因交通管制,晋州站、辛集站、藁城东站双向关闭。因沧州服务区附近K81处黄骅方向发生交通事故,沧州服务区附近K81处黄骅方向车辆缓慢通行约4公里

句法解析树:

1	黄石	黄石	nh	nr	_	3	SBV	_	_
2	高速	高速	d	d	_	3	ADV	_	_
3	:	:	v	v	_	0	HED	_	_
4	因	因	p	p	_	14	ADV	_	_
5	交通管制	交通管制	v	v	_	4	POB	_	_
6	,	,	wp	w	_	4	WP	_	_
7	晋州站	晋州站	ns	ns	_	14	SBV	_	_
8	、	、	wp	w	_	9	WP	_	_
9	辛集站	辛集站	n	n	_	7	COO	_	_
10	、	、	wp	w	_	12	WP	_	_
11	藁城	藁城	ns	ns	_	12	ATT	_	_
12	东站	东站	n	n	_	7	COO	_	_
13	双向	双向	d	d	_	14	ADV	_	_
14	关闭	关闭	v	v	_	3	COO	_	_
15	。	。	wp	w	_	3	WP	_	_
16	因	因	p	p	_	24	ADV	_	_
17	沧州	沧州	ns	ns	_	18	ATT	_	_
18	服务区	服务区	n	n	_	19	ATT	_	_
19	附近	附近	nd	f	_	21	ATT	_	_
20	K81	K81	ws	nx	_	21	ATT	_	_
21	处	处	n	n	_	22	ATT	_	_
22	黄骅	黄骅	ns	ns	_	23	ATT	_	_
23	方向	方向	n	n	_	16	POB	_	_
24	发生	发生	v	v	_	3	COO	_	_
25	交通事故	交通事故	n	n	_	24	VOB	_	_
26	,	,	wp	w	_	24	WP	_	_
27	沧州	沧州	ns	ns	_	28	ATT	_	_
28	服务区	服务区	n	n	_	29	ATT	_	_
29	附近	附近	nd	f	_	31	ATT	_	_
30	K81	K81	ws	nx	_	31	ATT	_	_
31	处	处	n	n	_	34	ATT	_	_
32	黄骅	黄骅	ns	ns	_	33	ATT	_	_
33	方向	方向	n	n	_	34	ATT	_	_
34	车辆	车辆	n	n	_	36	SBV	_	_
35	缓慢	缓慢	a	ad	_	36	ADV	_	_
36	通行	通行	v	v	_	24	COO	_	_
37	约	约	d	d	_	38	ATT	_	_
38	4	4	m	m	_	39	ATT	_	_
39	公里	公里	q	q	_	36	CMP	_	_
40	。	。	wp	w	_	3	WP	_	_


核心代码如下:

class WordBean(object):
   ''' 扩展conllword,存储父节点与孩子节点索引 '''

       def __init__(self):
           self.lemma = None
           self.postag = None
           self.relation = None
           self.head_id = None
           self.flag = True  # 是否还处于树中标志位,已合并的ATT将置为False
           self.child = []
   
       def set_word(self, conll_word):
           self.lemma = conll_word.LEMMA
           self.postag = conll_word.POSTAG
           self.relation = conll_word.DEPREL
           self.head_id = conll_word.HEAD.ID - 1
   
       def add_child(self, child_id, child_relation):
           self.child.append((child_id, child_relation))
       
   
   def extract_entity_tuple(self, dependency_tree, seed_id, head_type=''):
       '''自上而下解析树'''
       res_entity_tuple_list = []
       res_dict = dict()
       res_dict['time'] = []
       res_dict['reason'] = []
       res_dict['place'] = []
       res_dict['status'] = []
       # 判断是否是叶子节点
       if len(dependency_tree[seed_id].child) == 0:
           if dependency_tree[seed_id].lemma in self.status_set:
               res_dict['status'].append((seed_id, dependency_tree[seed_id].lemma))
           elif self.check_is_time(dependency_tree, seed_id):
               res_dict['time'].append((seed_id, dependency_tree[seed_id].lemma))
           elif head_type == 'reason':
               res_dict['reason'].append((seed_id, dependency_tree[seed_id].lemma))
           elif head_type == 'time':
               res_dict['time'].append((seed_id, dependency_tree[seed_id].lemma))
           else:
               if not dependency_tree[seed_id].lemma in self.discard_word_set:
                   res_dict['place'].append((seed_id, dependency_tree[seed_id].lemma))
           res_entity_tuple_list.append(res_dict)
           return res_entity_tuple_list

       # 非叶子节点需要向下递归解析
       if dependency_tree[seed_id].lemma in self.status_set:
           # 当前节点为状态节点
           status_merge_list = []
           for c_id, c_relation in dependency_tree[seed_id].child:
               child_bean = dependency_tree[c_id]
               if c_relation in ('COO', '并列关系'):
                   # 假设状态下不存在嵌套状态,有视为补充
                   if dependency_tree[c_id].postag == 'v' and len(dependency_tree[c_id].child) == 0:
                       res_dict['status'].append((c_id, dependency_tree[c_id].lemma))
                   else:
                       child_dict_list = self.extract_entity_tuple(dependency_tree, c_id)
                       for child_dict in child_dict_list:
                           self.merge_two_tuple_dict(res_dict, child_dict)
               elif c_relation in ('ADV', '状中结构'):
                   # 处理状中结构
                   if child_bean.lemma in ('因', '受', '由于'):
                       # 处理原因
                       child_dict_list = self.extract_entity_tuple(dependency_tree, c_id)
                       for child_dict in child_dict_list:
                           self.merge_two_tuple_dict(res_dict, child_dict)
                   elif child_bean.lemma == '处' or child_bean.postag == 'p':
                       child_dict_list = self.extract_entity_tuple(dependency_tree, c_id)
                       for child_dict in child_dict_list:
                           self.merge_two_tuple_dict(res_dict, child_dict)
                   elif child_bean.postag in ('a', 'ad', 'd'):
                       self.merge_att(dependency_tree, c_id)
                       status_merge_list.append(c_id)
                   elif self.check_is_time(dependency_tree, c_id):
                       self.merge_att(dependency_tree, c_id)
                       res_dict['time'].append((c_id, dependency_tree[c_id].lemma))
               elif c_relation in ('POB', '介宾关系') and 
                       child_bean.lemma in ('因', '受', '由于'):
                   self.merge_att(dependency_tree, c_id)
                   res_dict['reason'].append((c_id, dependency_tree[c_id].lemma))
               elif c_relation in ('CMP', '动补结构'):
                   self.merge_att(dependency_tree, c_id)
                   status_merge_list.append(c_id)
               elif c_relation in ('SBV', '主谓关系'):
                   # 处理主谓关系,解析具体地点
                   child_dict_list = self.extract_entity_tuple(dependency_tree, c_id)
                   for child_dict in child_dict_list:
                       self.merge_two_tuple_dict(res_dict, child_dict)
               elif c_relation in ('VOB', '动宾关系'):
                   if len(dependency_tree[c_id].child) == 0:
                       res_dict['status'].append((c_id, dependency_tree[c_id].lemma))
                   else:
                       child_dict_list = self.extract_entity_tuple(dependency_tree, c_id)
                       for child_dict in child_dict_list:
                           self.merge_two_tuple_dict(res_dict, child_dict)

           status_buffer = []
           status_merge_list.append(seed_id)
           status_merge_list.sort()
           for id in status_merge_list:
               status_buffer.append(dependency_tree[id].lemma)
           res_dict['status'].append((seed_id, ''.join(status_buffer)))

           res_entity_tuple_list.append(res_dict)
           return res_entity_tuple_list
       else:
           # 当前节点为非状态节点
           pre_head_type = head_type
           if self.check_is_time(dependency_tree, seed_id):
               # 为时间节点
               head_type = 'time'
               res_dict['time'].append((seed_id, dependency_tree[seed_id].lemma))
           elif dependency_tree[seed_id].lemma in ('因', '受', '由于') or head_type == 'reason':
               # 为原因节点
               head_type = 'reason'
               res_dict['reason'].append((seed_id, dependency_tree[seed_id].lemma))
           else:
               if not dependency_tree[seed_id].lemma in self.discard_word_set:
                   res_dict['place'].append((seed_id, dependency_tree[seed_id].lemma))
           child_dict_list = []
           coo_list = []
           for c_id, c_relation in dependency_tree[seed_id].child:
               # if c_relation in ('COO', '并列关系'):
               #     coo_list.append(c_id)
               if c_relation in ('WP', '标点符号'):
                   continue
               else:
                   if head_type == 'reason' and not pre_head_type == 'reason':
                       if c_relation in ('POB', '介宾关系'):
                           child_dict_list.extend(self.extract_entity_tuple(dependency_tree, c_id, head_type))
                           if dependency_tree[c_id].lemma in self.status_set:
                               res_dict['reason'].append((c_id, dependency_tree[c_id].lemma))
                   else:
                       child_dict_list.extend(self.extract_entity_tuple(dependency_tree, c_id, head_type))

           # 先合并非状态
           status_dict_list = []
           for child_dict in child_dict_list:
               if len(child_dict['status']) > 0:
                   status_dict_list.append(child_dict)
               else:
                   self.merge_two_tuple_dict(res_dict, child_dict)

           # 再合并存在状态的
           if len(status_dict_list) == 0:
               res_entity_tuple_list.append(res_dict)
           else:
               for child_dict in status_dict_list:
                   # tmp_dict = res_dict.copy()
                   tmp_dict = copy.deepcopy(res_dict)
                   self.merge_two_tuple_dict(tmp_dict, child_dict)
                   res_entity_tuple_list.append(tmp_dict)

           return res_entity_tuple_list


   def extract_information(self, line):
       segs = self.nlp_tokenizer.seg(line)
       # fix segs
       self.fix_seged_postag(segs)
       conll_words = self.parser.parse(segs).getWordArray()
       dependency_tree, root_id = self.construct_dependency_tree(conll_words)

       res_entity.append(self.extract_entity_tuple(dependency_tree, i))
       res_entity = self.extract_entity_tuple(dependency_tree, root_id)

       # print entity tuples
       for entity in res_entity:
           entity['time'].sort()
           entity['place'].sort()
           entity['reason'].sort()
           entity['status'].sort()
           self.fix_entity_tuple_dict(entity)
           print(entity)
       return res_entity

实验结果:

# 输入句子
'黄石高速:因交通管制,晋州站、辛集站、藁城东站双向关闭。因沧州服务区附近K81处黄骅方向发生交通事故,沧州服务区附近K81处黄骅方向车辆缓慢通行约4公里。'


# 信息提取结果
{'time': [], 'reason': ['因交通管制'], 'place': ['黄石高速:, 晋州站, 辛集站, 藁城东站'], 'status': ['交通管制, 双向关闭']}
{'time': [], 'reason': ['因沧州服务区附近K81处黄骅方向'], 'place': ['黄石高速:, 发生交通事故, 沧州服务区附近K81处黄骅方向'], 'status': ['缓慢通行约4公里']}

可以看到,第一句的解析没问题,但是第二句原因的解析边界出错。当前的解析方法仍然比较依赖于句法依存树的准确性,实体的边界的准确性不够,也是需要改进的地方。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!