双向最大匹配和实体标注:你以为我只能分词?

栏目: IT技术 · 发布时间: 4年前

双向最大匹配和实体标注:你以为我只能分词?

一 :内容预告

前几天,我看了叉烧同学写的文章:

NLP.TM[29] | ner自动化打标方法

文章介绍了一种实体自动打标的方法: 实体词典+最大逆向匹配

啥叫实体打标呢?

举叉烧同学文章里的例子,有一句话:宫保鸡丁和红烧牛肉哪个好吃, 我们需要给句子中的两个实体 [宫保鸡丁, 红烧牛肉] 打上 Food 标签,非实体打上 O 标签。

做法是,首先准备一个实体词典,实体词典包含这两个实体在内的食物名,然后用这个词典和最大逆向匹配算法,给实体打标签。

打好标签的句子,可以直接作为实体提取的最终结果,也可以作为训练实体识别模型的样本。

用于构造样本时,打标签的结果如下:

""" 1:句子 """

宫保鸡丁和红烧牛肉哪个好吃

""" 2:标注结果 """
宫 B-Food
保 I-Food
鸡 I-Food
丁 I-Food
和 O
红 B-Food
烧 I-Food
牛 I-Food
肉 I-Food
哪 O
个 O
好 O
吃 O

我之前也做过实体自动打标签的活,不过用的方法是: 实体词典+jieba词性标注

jieba词性标注应该用到了双向最大匹配,所以其实我之前做的,就是掉包实现了以上打标的方法。

这次准备了一个医疗实体词典(带标签)和若干医疗文本,用 python 实现双向最大匹配算法,并结合实体词典进行实体自动打标,作为训练实体识别模型的样本。

同时也整理了 实体词典+jieba词性标注 进行实体自动打标的方法。

代码已上传github地址:

https://github.com/DengYangyong/medical_entity_recognize

本文主要关注以下四方面的内容:

  • 双向最大匹配算法的思路

  • 双向最大匹配算法的代码实现

  • 实体词典+双向最大匹配算法做实体打标

  • 实体词典+jieba词性标注做实体打标

二:双向最大匹配算法

前向最大匹配算法和后向最大匹配算法是基于规则(词典)的分词方法,二者按照一定的规则结合使用,就是双向最大匹配算法。

双向最大匹配算法不仅可以用于分词,也可以用于序列标注,速度快且准确率高,不会分出奇怪的词或实体,但也难以正确处理未登录词。

由于完全依赖词典,通用性不强,所以比较适用于处理具体领域的任务,比如医疗领域。

01

前向最大匹配算法

首先拿到一个词典,比如实体词典,每一行是一个实体和对应的标签:

""" 词典(包含:实体和标签) """

肾抗针,DRU
肾囊肿,DIS
肾区,REG
肾上腺皮质功能减退症,DIS
肾性高血压,DIS
肾性贫血,DIS
肾血管,ORG
肾脏,ORG
伴脓性渗出,NBP
生理反射存在,NBP

然后计算词典中实体的最大长度,作为截取句子片段的最大长度。

为什么叫最大匹配呢?

对句子进 行分词和标注的一个 原则是: 单个词(实体)的长度尽可能大。 所以会先按最大长度从句子中截取片段,去词典中匹配,匹配中了,就切分出来。

按最大长度匹配不中,那么最大长度减一,再去截取句子片段,去词典中匹配,直到匹配中,或者截取的长度减小至一。

为什么叫前向呢?

前向的意思是,从句子中截取片段是从左向右进行的。

更多的细节,来看一个例子。

患者的电子病历上写着:我最近双下肢疼痛,我该咋办。

句子中有个两个实体,出现在实体词典中:双下肢疼痛、疼痛。

句子的长度为14(包括标点符号),实体词典中的最大长度为10。

第一轮遍历未匹配中,把第一个字切分出来:[我]。

双向最大匹配和实体标注:你以为我只能分词?

第二轮遍历未匹配中,把第二个字切分出来:[我,最]。

双向最大匹配和实体标注:你以为我只能分词?

第四轮遍历,匹配中了[双下肢疼痛],就把这个实体从句子中剔除,拿剩下的句子继续匹配。

双向最大匹配和实体标注:你以为我只能分词?

最终的实体打标结果为:

可以看到,[疼痛] 这个实体没有被匹配出来,因为[双下肢疼痛]的粒度更大,表达的含义更明确,这也是最大匹配的意义。

前向最大匹配的python实现如下:

#coding:utf-8
import pandas as pd

class PsegMax:
def __init__(self, dict_path):
self.entity_dict, self.max_len = self.load_entity(dict_path)

def load_entity(self, dict_path):
"""
加载实体词典
"""


entity_list = []
max_len = 0

""" 实体词典: {'肾抗针': 'DRU', '肾囊肿': 'DIS', '肾区': 'REG', '肾上腺皮质功能减退症': 'DIS', ...} """
df = pd.read_csv(dict_path,header=None,names=["entity","tag"])
entity_dict = {entity.strip(): tag.strip() for entity,tag in df.values.tolist()}

""" 计算词典中实体的最大长度 """
df["len"] = df["entity"].apply(lambda x:len(x))
max_len = max(df["len"])

return entity_dict, max_len


def max_forward_seg(self, sent):
"""
前向最大匹配实体标注
"""

words_pos_seg = []
sent_len = len(sent)

while sent_len > 0:

""" 如果句子长度小于实体最大长度,则切分的最大长度为句子长度 """
max_len = min(sent_len,self.max_len)

""" 从左向右截取max_len个字符,去词典中匹配 """
sub_sent = sent[:max_len]

while max_len > 0:

""" 如果切分的词在实体词典中,那就是切出来的实体 """
if sub_sent in self.entity_dict:
tag = self.entity_dict[sub_sent]
words_pos_seg.append((sub_sent,tag))
break

elif max_len == 1:

""" 如果没有匹配上,那就把单个字切出来,标签为O """
tag = "O"
words_pos_seg.append((sub_sent,tag))
break

else:

""" 如果没有匹配上,又还没剩最后一个字,就去掉右边的字,继续循环 """
max_len -= 1
sub_sent = sub_sent[:max_len]

""" 把分出来的词(实体或单个字)去掉,继续切分剩下的句子 """
sent = sent[max_len:]
sent_len -= max_len

return words_pos_seg

02

后向最大匹配算法

后向的意思是,从句子中截取片段是从右往左进行的。

还是来看上面的那个例子。

第一轮遍历未匹配中,把最后一个字切分出来:[。]。

双向最大匹配和实体标注:你以为我只能分词?

第二轮遍历未匹配中,把倒数第二个字切分出来:[办,。]。

双向最大匹配和实体标注:你以为我只能分词?

直到第七轮遍历, 匹配中了[双下肢疼痛],就把这个实体从句子中剔除,拿剩下的句子继续匹配。

双向最大匹配和实体标注:你以为我只能分词?

最终的实体打标结果和前向最大匹配的结果一致。

这个例子没反映出前向最大匹配和后向最大匹配的差别,感觉在实体标注这一块,二者的结果差别不大。

有资料统计,在分词时,仅使用前向最大匹配的错误率为 1/169,而使用后向最大匹配的错误率为 1/245,可见使用后向最大匹配可以提高分词或标注的准确率。

后向最大匹配的python实现如下:

#coding:utf-8
import pandas as pd

class PsegMax:
def __init__(self, dict_path):
self.entity_dict, self.max_len = self.load_entity(dict_path)

def load_entity(self, dict_path):
"""
加载实体词典
"""


def max_forward_seg(self, sent):
"""
前向最大匹配实体标注
"""


def max_backward_seg(self, sent):
"""
后向最大匹配实体标注
"""


words_pos_seg = []
sent_len = len(sent)

while sent_len > 0:

""" 如果句子长度小于实体最大长度,则切分的最大长度为句子长度 """
max_len = min(sent_len,self.max_len)

""" 从右向左截取max_len个字符,去词典中匹配 """
sub_sent = sent[-max_len:]

while max_len > 0:

""" 如果切分的词在实体词典中,那就是切出来的实体 """
if sub_sent in self.entity_dict:
tag = self.entity_dict[sub_sent]
words_pos_seg.append((sub_sent,tag))
break

elif max_len == 1:

""" 如果没有匹配上,那就把单个字切出来,标签为O """
tag = "O"
words_pos_seg.append((sub_sent,tag))
break

else:

""" 如果没有匹配上,又还没剩最后一个字,就去掉右边的字,继续循环 """
max_len -= 1
sub_sent = sub_sent[-max_len:]

""" 把分出来的词(实体或单个字)去掉,继续切分剩下的句子 """
sent = sent[:-max_len]
sent_len -= max_len

""" 把切分的结果反转 """
return words_pos_seg[::-1]

03

双向最大匹配算法

双向最大匹配就是,将前向最大匹配的切分结果,和后向最大匹配的结果进行比较,按一定的规则选择其一作为最终的结果。

按照什么规则来选择呢?

(1)如果前向和后向切分结果的词数不同,则取词数较少的那个。

(2)如果词数相同 :

  • 切分结果相同,则返回任意一个;

  • 切分结果不同,则返回单字较少的那个。

网上看到一个非常好的例子,前向和后向切分后,词数相同,结果不同,返回单字少的那个结果(例子中为后向最大匹配)。

双向最大匹配和实体标注:你以为我只能分词?

双向最大匹配的python实现如下:

#coding:utf-8
import pandas as pd

class PsegMax:
def __init__(self, dict_path):
self.entity_dict, self.max_len = self.load_entity(dict_path)

def load_entity(self, dict_path):

def max_forward_seg(self, sent):

def max_backward_seg(self, sent):

def max_biward_seg(self, sent):
"""
双向最大匹配实体标注
"""


""" 1: 前向和后向的切分结果 """
words_psg_fw = self.max_forward_seg(sent)
words_psg_bw = self.max_backward_seg(sent)

""" 2: 前向和后向的词数 """
words_fw_size = len(words_psg_fw)
words_bw_size = len(words_psg_bw)

""" 3: 前向和后向的词数,则取词数较少的那个 """
if words_fw_size < words_bw_size: return words_psg_fw

if words_fw_size > words_bw_size: return words_psg_bw

""" 4: 结果相同,可返回任意一个 """
if words_psg_fw == words_psg_bw: return words_psg_fw

""" 5: 结果不同,返回单字较少的那个 """
fw_single = sum([1 for i in range(words_fw_size) if len(words_psg_fw[i][0])==1])
bw_single = sum([1 for i in range(words_fw_size) if len(words_psg_bw[i][0])==1])

if fw_single < bw_single: return words_psg_fw
else: return words_psg_bw


if __name__ == "__main__":

dict_path = "medical_ner_dict.csv"
text = "我最近双下肢疼痛,我该咋办。"

psg = PsegMax(dict_path)
words_psg = psg.max_biward_seg(text)

print(words_psg)

最终的实体打标结果为:

三:实体自动标注

接下来就两种方法来做实体自动打标,构造训练实体识别模型的样本。

一是用:实体词典+双向最大匹配,

二是用:实体词典+jieba词性标注。

01

jieba加载词典

医疗实体词典的格式如下,每一行是实体词和对应的标签:

肾抗针,DRU
肾囊肿,DIS
肾区,REG
肾上腺皮质功能减退症,DIS
肾性高血压,DIS
肾性贫血,DIS
肾血管,ORG
肾脏,ORG
伴脓性渗出,NBP
生理反射存在,NBP

未标注的电子病历内容如下:

患者精神状况好,无发热,诉右髋部疼痛,饮食差,二便正常,查体:神清,各项生命体征平稳,心肺腹查体未见异常。右髋部压痛,右下肢皮牵引固定好,无松动,右足背动脉搏动好,足趾感觉运动正常。

首先导入必要的包,max_seg.py 就是上面写好的双向最大匹配算法。

#encoding=utf8
import os,jieba,csv,random,re
import jieba.posseg as psg
from max_seg import PsegMax

""" 医疗实体词典, 每一行类似:(视力减退,SYM) """
dict_path = "medical_ner_dict.csv"
psgMax = PsegMax(dict_path)

c_root = os.getcwd() + os.sep + "source_data" + os.sep

""" 实体类别 """
biaoji = set(['DIS', 'SYM', 'SGN', 'TES', 'DRU', 'SUR', 'PRE', 'PT', 'Dur', 'TP', 'REG', 'ORG', 'AT', 'PSB', 'DEG', 'FW','CL'])

""" 句子结尾符号,表示如果是句末,则换行 """
fuhao = set(['。','?','?','!','!'])

然后把医疗实体和标签加载到jieba中,同时为了保证由多个词组成的实体不被切开,我们为实体设置较高的权重。

def add_entity(dict_path):
"""
把实体字典加载到jieba里,
实体作为分词后的词,
实体标记作为词性
"""


dics = csv.reader(open(dict_path,'r',encoding='utf8'))

for row in dics:

if len(row)==2:
jieba.add_word(row[0].strip(),tag=row[1].strip())

""" 保证由多个词组成的实体词,不被切分开 """
jieba.suggest_freq(row[0].strip())

我们可以选择用jieba还是双向最大匹配来标注。

def sentence_seg(sentence,mode="jieba"):
"""
1: 实体词典+jieba词性标注。mode="jieba"
2: 实体词典+双向最大匹配。mode="max_seg"
"""


if mode == "jieba": return psg.cut(sentence)
if mode == "max_seg": return psgMax.max_biward_seg(sentence)

我们来看实体标注(词性标注)的效果:

sentence = "我最近双下肢疼痛。"

""" 1: 不加词典的jieba词性标注 """
[pair('我', 'r'), pair('最近', 'f'), pair('双下肢', 'n'), pair('疼痛', 'n'), pair('。', 'x')]

""" 2: 加词典的jieba词性标注 """
[pair('我', 'r'), pair('最近', 'f'), pair('双下肢疼痛', 'SYM'), pair('。', 'x')]

""" 3: 双向最大匹配的标注 """
[('我', 'O'), ('最', 'O'), ('近', 'O'), ('双下肢疼痛', 'SYM'), ('。', 'O')]

02

样本自动标注

这次一共有100篇电子病历,我们按7:2:1的比例划分为训练集、验证集和测试集。

def split_dataset():
"""
划分数据集,按照7:2:1的比例
"""


file_all = []
for file in os.listdir(c_root):
if "txtoriginal.txt" in file:
file_all.append(file)

random.seed(10)
random.shuffle(file_all)

num = len(file_all)
train_files = file_all[: int(num * 0.7)]
dev_files = file_all[int(num * 0.7):int(num * 0.9)]
test_files = file_all[int(num * 0.9):]

return train_files,dev_files,test_files

然后进行样本自动标注,采用的实体标注格式为BIO。

BIO格式就是说,对于实体词,第一个字标注为B,其他的字标注为I;对于非实体词,每个字都标注为O。

还有一个小细节是句与句之间要留空格。

例子如下:

我 O
最 O
近 O
双 B-SYM
下 I-SYM
肢 I-SYM
疼 I-SYM
痛 I-SYM
。 O

怎 O
么 O
办 O
? O

我们把每个字的标注结果,作为一行,写入到文件中。

代码实现如下:

def auto_label(files, data_type, mode="jieba"):
"""
不是实体,则标记为O,
如果是句号等划分句子的符号,则再加换行符,
是实体,则标记为BI。
"""


writer = open("example.%s" % data_type,"w",encoding="utf8")

for file in files:
fp = open(c_root+file,'r',encoding='utf8')

for line in fp:

""" 按词性分词 """
words = sentence_seg(line,mode)
for word,pos in words:
word,pos = word.strip(), pos.strip()
if not (word and pos):
continue

""" 如果词性不是实体的标记,则打上O标记 """
if pos not in biaoji:

for char in word:
string = char + ' ' + 'O' + '\n'

""" 在句子的结尾换行 """
if char in fuhao:
string += '\n'

writer.write(string)

else:

""" 如果词性是实体的标记,则打上BI标记"""
begin = 0
for char in word:

if begin == 0:
begin += 1
string = char + ' ' + 'B-' + pos + '\n'

else:
string = char + ' ' + 'I-' + pos + '\n'

writer.write(string)

writer.close()

def main():

""" 1: 加载实体词和标记到jieba """
add_entity(dict_path)

""" 2: 划分数据集 """
trains, devs, tests = split_dataset()

""" 3: 自动标注样本 """
for files, data_type in zip([trains,devs,tests],["train","dev","test"]):
auto_label(files, data_type,mode="max_seg")

if __name__ == "__main__":

main()

标注好的样本的部分内容如下:

部 O
分 O
脱 O
痂 O
。 O

双 B-ORG
侧 I-ORG
瞳 I-ORG
孔 I-ORG
正 O
大 O
等 O
圆 O

哈哈,感觉很多人都是用医疗领域的数据来学习实体识别,是因为在其他领域做实体识别的效果不好吗?

五月份要认真整理一下实体识别的模型,不然就是个假NLPer。

祝大家五一节快乐啊!

推荐阅读

AINLP年度阅读收藏清单

太赞了!Springer面向公众开放电子书籍,附65本数学、编程、机器学习、深度学习、数据挖掘、数据科学等书籍链接及打包下载

深度学习如何入门?这本“蒲公英书”再适合不过了!豆瓣评分9.5!【文末双彩蛋!】

斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用

数学之美中盛赞的 Michael Collins 教授,他的NLP课程要不要收藏?

自动作诗机&藏头诗生成器:五言、七言、绝句、律诗全了

From Word Embeddings To Document Distances 阅读笔记

模型压缩实践系列之——bert-of-theseus,一个非常亲民的bert压缩方法

这门斯坦福大学自然语言处理经典入门课,我放到B站了

可解释性论文阅读笔记1-Tree Regularization

征稿启示 | 稿费+GPU算力+星球嘉宾一个都不少

关于AINLP

AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。

双向最大匹配和实体标注:你以为我只能分词?


以上所述就是小编给大家介绍的《双向最大匹配和实体标注:你以为我只能分词?》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Pragmatic Programmer

The Pragmatic Programmer

Andrew Hunt、David Thomas / Addison-Wesley Professional / 1999-10-30 / USD 49.99

本书直击编程陈地,穿过了软件开发中日益增长的规范和技术藩篱,对核心过程进行了审视――即根据需求,创建用户乐于接受的、可工作和易维护的代码。本书包含的内容从个人责任到职业发展,直至保持代码灵活和易于改编重用的架构技术。从本书中将学到防止软件变质、消除复制知识的陷阱、编写灵活、动态和易适应的代码、避免出现相同的设计、用契约、断言和异常对代码进行防护等内容。一起来看看 《The Pragmatic Programmer》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换