中文分词实战——基于jieba动态加载字典和调整词频的电子病历分词
分词是自然语言处理中最基本的一个任务,这篇小文章不介绍相关的理论,而是介绍一个电子病历分词的小实践。
开源的分词工具中,我用过的有jieba、hnlp和stanfordnlp,感觉jieba无论安装和使用都比较便捷,拓展性也比较好。是不是直接调用开源的分词工具,就可以得到比较好的分词效果呢?答案当然是否定的。尤其是在专业性较强的领域,比如医疗行业,往往需要通过加载相关领域的字典、自定义字典和正则表达式匹配等方式,才能得到较好的分词效果。
这次我就通过一个电子病历分词的小实践,分析在具体的分词任务中会踩哪些坑,然后如何运用jieba的动态加载字典功能和正则表达式,得到更好的分词结果。
代码和数据可以去我的Github主页下载:https://github.com/DengYangyong/medical_segment
一、原始文本数据
原始文本是100篇txt格式的电子病历文档,其中一篇文档的内容是这样的:
医疗文本中有一些文言文,比如神志清、神清、情况可等,对分词任务造成一定的困难。
二、使用jieba直接分词
首先使用jieba直接对100篇电子病历进行分词(jieba的基本使用可以看它的github页面:https://github.com/fxsjy/jieba )。在jieba.cut()中,选择精准模式(cut_all=False),同时不使用HMM模型进行分词,因为我在尝试使用HMM模式时,切出了一些没见过也不合理的新词。
#-*- coding=utf8 -*-
import jieba,os
#定义一个分词的函数
def word_seg(sentence):
#使用jieba进行分词,选择精准模式,关闭HMM模式,返回一个list:['患者','精神',...]
str_list = list(jieba.cut(sentence, cut_all=False, HMM=False))
result = " ".join(str_list)
return result if __name__=="__main__":
# 100篇电子病历文档所在的目录
c_root = os.getcwd()+os.sep+"source_data"+os.sep
fout = open("cut_direct.txt","w",encoding="utf8") for file in os.listdir(path=c_root):
if "txtoriginal.txt" in file:
fp = open(c_root+file,"r",encoding="utf8")
for line in fp.readlines():
if line.strip() :
result = word_seg(line)
fout.write(result+'\n')
fp.close() fout.close()
下面是其中两篇电子病历的分词结果,可以看到,有很多医疗行业的词语没有切分正确,比如“双肺呼吸音清”、“湿性罗音”、“尿管”、“胃肠型及蠕动波” 、“反跳痛”等,所以下一步加载医疗行业的字典,希望能改善这个情况。
三、加载行业专属字典
最近在看一个电子病历命名实体识别项目的代码,这个项目使用加载字典的方式进行命名实体识别,里面有一个字典,恰好可以用在这里做分词。
这个字典是一个csv文件,有17713个医疗行业的命名实体,主要包括疾病名称、诊断方法、症状、治疗措施这几类,字典里每一行有两个元素,一个是命名实体(如肾性高血压);第二个是相应的符号标注(如DIS,即disease),类似于词性标注。
import csv
dic = csv.reader(open("DICT_NOW.csv","r",encoding='utf8'))
dic_list = list(dic) # 打印出字典的前五行内容
print("字典的内容是这样的:\n\n",a[:])
print("\n字典的长度为:",len(dic_list))
字典的内容是这样的: [['肾抗针', 'DRU'], ['肾囊肿', 'DIS'], ['肾区', 'REG'], ['肾上腺皮质功能减退症', 'DIS'], ['肾性高血压', 'DIS']] 字典的长度为: 17713
然后把这个字典加载到jieba里:
#-*- coding=utf8 -*-
import jieba,os,csv def add_dict():
dic = csv.reader(open("DICT_NOW.csv","r",encoding='utf8'))
# row: ['肾抗针', 'DRU']
for row in dic:
if len(row) ==2:
#把字典加进去
jieba.add_word(row[0].strip(),tag=row[1].strip())
#需要调整词频,确保它的词频足够高,能够被分出来。
#比如双肾区,如果在jiaba原有的字典中,双肾的频率是400,区的频率是500,而双肾区的频率是100,那么即使加入字典,也会被分成“双肾/区”
jieba.suggest_freq(row[0].strip(),tune=True) def word_seg(sentence): str_list = list(jieba.cut(sentence, cut_all=False, HMM=False))
result = " ".join(str_list)
return result if __name__=="__main__": add_dict()
c_root = os.getcwd()+os.sep+"source_data"+os.sep
fout = open("cut_dict.txt","w",encoding="utf8") for file in os.listdir(path=c_root):
if "txtoriginal.txt" in file:
fp = open(c_root+file,"r",encoding="utf8")
for line in fp.readlines():
if line.strip():
result = word_seg(line)
fout.write(result+'\n')
fp.close() fout.close()
分词完毕后,再看看前两篇病历的分词结果。可以看到,“双肺呼吸音清”,“湿性罗音”分词正确,可是还有一大堆词没有切分正确:尿管、胃肠型及蠕动波、反跳痛、肌紧张、皮牵引等等。可见一方面这个字典太小,只有17713个医疗词语,另一方面这是命名实体的字典,倾向于识别疾病名词、症状、检查手段和治疗措施这类命名实体,一般的身体部位、医疗用具并没有包含在内。
当然,尽管从这两篇病历来看,加载命名实体识别字典的效果不是太明显,但是查看100篇病历的分词结果,效果还是有较大的提升。既然命名实体识别的字典太小,那没办法,对于一些在病历中经常出现的词,可以通过自定义字典的方式进行正确分词。
四、自定义字典、正则表达式匹配和停用词
自定义字典就是把肠鸣音、皮牵引、胃肠型及蠕动波等没有正确切分的词语汇总,做成一个文件,然后加载到jieba当中去。代码实现起来比较简单,打开一个txt文件,每一行放一个词,然后保存文件即可,麻烦在于要手动挑选出这些专业词汇。100篇病历太多了,我简单做个尝试,从前10篇病历中大致找了些没有被正确切分的词做成字典,有下面这些:
听诊区 胃肠型及蠕动波 膀胱区 干湿性罗音 移动性浊音 皮牵引 反跳痛 肌紧张 肠鸣音 肋脊角 双肾区 查体 二便 痂皮 律齐 听诊区 发绀 湿罗音 外口 膝腱反射 巴彬斯基氏征 克尼格氏征 气过水声
我在查看100篇病历的分词结果时,发现其实除了这些比较规范的词外,还有一些与数字相关的词没有被正确切分(当然这也带有一定主观性,我有些强迫症),比如小数被 “.” 号分开,“%” 号与数字分开,* ^ 与数字分开。所以我想构造正则表达式,把正确的词匹配出来,然后作为字典加载到jieba中去。下面是一些我认为没有切分正确的词:
心率 62 次 / 分 ;右 下肢 直 腿 抬高 试验 阳性 50 度; 右手 拇指 可见 一 约 1 * 1cm 皮肤 挫伤 ;血常规 : 红细胞 6 . 05 * 10 ^ 12 / L , 血红蛋白 180 . 00g / L; 尿蛋白 + 2 红细胞 + 2;查 体 : T : 36 . 8 ℃ , 精神 可;中性 细胞 比率 77 . 6 % , 淋巴细胞 比率 19 . 6 % , 中 值 细胞 比率 2 . 8 % 。
正则表达式的代码如下,使用python的re模块。
先看p1,r'\d+[次度]'用来匹配“80次”、“50度” 这两种表示心率、温度的常见的词,\d+表示匹配一个或多个数字,[次度]表示数字后的字是次或者度。
然后看p2,用来匹配6.05,10^12,77.6%这类的数据,[a-zA-Z0-9+]+ 表示字母数字或者+号,然后匹配一次或多次。[\.^]* 中,表示匹配.或^号0次或1次,\是转义符号,因为后面的.本身就是正则表达式中的特殊字符,而在这里是表示小数点。[A-Za-z0-9%(℃)]+含义比较明确,然后(?![次度])是负前向查找,当后面的字不是次或度时就匹配,避免去匹配p1应该匹配的内容。当然其实这个表达式有很多问题,但是暂时想不出其他的了。
p1 = re.compile(r'\d+[次度]').findall(line)
p2 = re.compile(r'([a-zA-Z0-9+]+[\.^]*[A-Za-z0-9%(℃)]+(?![次度]))').findall(line)
所有匹配到的词如下所示,正则表达式刚入门还是小弱,费了老大的劲,还是匹配到了很多不想要的东西。先这样,看到的人欢迎拍砖。
" ".join([word.strip()for word in open("regex_dict.txt",'r',encoding='utf8').readlines()])
'80次 4次 62次 1cm 78次 BP130 80mmhg 80次 4次 4700ml CT 80次 4次 6.05 10^12 180.00g 0.550L 6.4fL +2 +2 25 38 HP 62.0U 100.00U 50度 76次 36.8C 84次 20次 74次 4次 3000ml 36.8C Fr20 3次 36.8℃ 84次 4次 140 90mmHg 4次 3cm 4X3cm CT 64次 4次 CT 90次 3mm 76次 4次 2.0cm 3.0 2.5cm 1.5 1.0cm 4次 36.4℃ 64次 3次 98次 300ml 12cm 74次 36.3C 80次 18次 80次 4次 37.0C 4次 12cm 76次 4次 Murphy CT 7.40 10^9 77.6% 19.6% 2.8% 3.49 10^12 99.00g 0.302L 78次 18次 36.5℃ BP130 80 mmHg 36.4℃ 80次 4次 4次 37℃ 82次 22次 82次 4次 3250ml 37℃ BP 149 97mmHg 100% 6cm 10cm 15cm 5cm 3cm 3cm 1.5 8cm 92次 Bp129 75mmHg 4次 72 4次 80次 4次 76次 4次 12 42 12 42 12cm 79次 37.0C 80次 4次 70次 4次 3次 72次 5度 100度 5度 105度 10 10 75次 18次 72次 T36.4℃ P7 R1 BP130 85mmhg'
第三步是去掉以标点符号为主的停用词(停用词是诸如“的、地、得”这些没有实际含义的词以及标的符号特殊符号等)。我尝试使用从网上下载的中文停用词表,挺齐全,有2000多个停用词,可是使用后发现结果不好,因为它会把 类似“无”,“可”这种词去掉,于是“无头晕”只剩下了“头晕”,“精神可”只剩下了“精神”,要么意思反了,要么不知所云。
所以我自己弄了个简单的停用词表,只去掉 “ ,。!?:、)( ” 这些符号。
完整的代码如下:
#-*- coding=utf8 -*-
import jieba,os,csv,re
import jieba.posseg as pseg def add_dict():
# 导入自定义字典,这是在检查分词结果后自己创建的字典
jieba.load_userdict("userdict.txt")
dict1 = open("userdict.txt","r",encoding='utf8')
#需要调整自定义词的词频,确保它的词频足够高,能够被分出来。
#比如双肾区,如果在jiaba原有的字典中,双肾的频率是400,区的频率是500,而双肾区的频率是100,那么即使加入字典,也会被分成“双肾/区”
[jieba.suggest_freq(line.strip(), tune=True) for line in dict1] #加载命名实体识别字典
dic2 = csv.reader(open("DICT_NOW.csv","r",encoding='utf8'))
for row in dic2:
if len(row) ==2:
jieba.add_word(row[0].strip(),tag=row[1].strip())
jieba.suggest_freq(row[0].strip(),tune=True) # 用正则表达式匹配到的词,作为字典
fout_regex = open('regex_dict.txt','w',encoding='utf8')
for file in os.listdir(path=c_root):
if "txtoriginal.txt" in file:
fp = open(c_root+file,"r",encoding="utf8")
for line in fp.readlines():
if line.strip() :
#正则表达式匹配
p1 = re.compile(r'\d+[次度]').findall(line)
p2 = re.compile(r'([a-zA-Z0-9+]+[\.^]*[A-Za-z0-9%(℃)]+(?![次度]))').findall(line)
p_merge = p1+p2
for word in p_merge:
jieba.add_word(word.strip())
jieba.suggest_freq(word.strip(),tune=True)
fout_regex.write(word+'\n')
35 fp.close()
fout_regex.close() # 用停用词表过滤掉停用词
def stop_words():
# "ChineseStopWords.txt"是非常全的停用词表,然后效果不好。
#stopwords = [word.strip() for word in open("ChineseStopWords.txt","r",encoding='utf-8').readlines()]
stopwords = [word.strip() for word in open("stop_words.txt","r",encoding='utf-8').readlines()]
return stopwords # 进行分词
def word_seg(sentence): str_list = list(jieba.cut(sentence, cut_all=False, HMM=False))
str_list = [word.strip() for word in str_list if word not in stopwords]
result = " ".join(str_list)
return result if __name__=="__main__": add_dict()
stopwords = stop_words()
c_root = os.getcwd()+os.sep+"source_data"+os.sep
fout = open("cut_stopwords.txt","w",encoding="utf8") for file in os.listdir(path=c_root):
if "txtoriginal.txt" in file:
fp = open(c_root+file,"r",encoding="utf8")
for line in fp.readlines():
if line.strip() :
result = word_seg(line)
fout.write(result+'\n\n')
fp.close() fout.close()
部分分词结果如下,可以看到,像80次、6.05,+2这种格式的数据分词正确,但是我发现尽管能已经匹配到了1*1,10^12,36.4℃这些词,但是加载进去后用jieba分词还是不能正确划分,看来在jieba中,* ^ / 这些符号是必须要被切开的。
五、小结
这一个简单的电子病历分词实践到此告一段落,说说两点体会吧。
一是分词有基于字典的方法,基于规则的方法和基于机器学习的方法,尽管机器学习的方法听起来高大上,但可能会切出一些没见过的词,而基于字典的方法看上去比较土,其实更好用,准确性更高。
二是医疗行业的自然语言处理非常不好做,这些电子病历、医疗文献是英文、现代文和文言文的混合体,医生写病历、处方有时喜欢文言体(简洁省事),各种疾病、治疗手段的专业术语又非常多,每种疾病又有很多主流和非主流的名称,给分词、实体识别这些任务造成了很大麻烦。
中文分词实战——基于jieba动态加载字典和调整词频的电子病历分词的更多相关文章
- python jieba分词(结巴分词)、提取词,加载词,修改词频,定义词库 -转载
转载请注明出处 “结巴”中文分词:做最好的 Python 中文分词组件,分词模块jieba,它是python比较好用的分词模块, 支持中文简体,繁体分词,还支持自定义词库. jieba的分词,提取关 ...
- Asp.Net Core 项目实战之权限管理系统(8) 功能菜单的动态加载
0 Asp.Net Core 项目实战之权限管理系统(0) 无中生有 1 Asp.Net Core 项目实战之权限管理系统(1) 使用AdminLTE搭建前端 2 Asp.Net Core 项目实战之 ...
- AngularJS进阶(三十九)基于项目实战解析ng启动加载过程
基于项目实战解析ng启动加载过程 前言 在AngularJS项目开发过程中,自己将遇到的问题进行了整理.回过头来总结一下angular的启动过程. 下面以实际项目为例进行简要讲解. 1.载入ng库 2 ...
- vue-element-admin实战 | 第二篇: 最小改动接入后台实现根据权限动态加载菜单
一. 前言 本篇基于 有来商城 youlai-mall微服务项目,通过对vue-element-admin的权限菜单模块理解个性定制其后台接口,实现对vue-element-admin工程几乎不做改动 ...
- 【Android编程实战】源码级免杀_Dex动态加载技术_Metasploit安卓载荷傀儡机代码复现
/文章作者:MG193.7 CNBLOG博客ID:ALDYS4 QQ:3496925334/ 在读者阅读本文章前,建议先阅读笔者之前写的一篇对安卓载荷的分析文章 [逆向&编程实战]Metasp ...
- 爬虫再探实战(四)———爬取动态加载页面——请求json
还是上次的那个网站,就是它.现在尝试用另一种办法——直接请求json文件,来获取要抓取的信息. 第一步,检查元素,看图如下: 过滤出JS文件,并找出包含要抓取信息的js文件,之后就是构造request ...
- 爬虫再探实战(三)———爬取动态加载页面——selenium
自学python爬虫也快半年了,在目前看来,我面临着三个待解决的爬虫技术方面的问题:动态加载,多线程并发抓取,模拟登陆.目前正在不断学习相关知识.下面简单写一下用selenium处理动态加载页面相关的 ...
- OSGI动态加载删除Service bundle
OSGi模块化框架是很早就出来的一个插件化框架,最早Eclipse用它而出名,但这些年也没有大热虽然OSGi已经发布了版本1到版本5.现在用的最多的,也是本文讲述基于的是Equinox的OSGi实现, ...
- Android动态加载框架汇总
几种动态加载的比较 1.Tinker 用途:热修复 GitHub地址:https://github.com/Tencent/tinker/ 使用:http://www.jianshu.com/p/f6 ...
随机推荐
- python运行js
安装 pip install PyExecJS # 需要注意, 包的名称:PyExecJS 简单使用 import execjs execjs.eval("new Date") 返 ...
- Docker常用名称
#查看容器ID(containedId) $docker ps -a #删除容器 $docker rm containedId #停止运行的容器 $docker stop containedId #修 ...
- java 自定义的注解有什么作用
转自https://zhidao.baidu.com/question/1668622526729638507.html 自定义注解,可以应用到反射中,比如自己写个小框架. 如实现实体类某些属性不自动 ...
- pingo--util.go 源码阅读
:] } var _letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-&qu ...
- CopyOnWriteArrayList简介
CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList.所有可变操作都是通过对底层数组进行一次新的复制来实现. CopyOnWriteAr ...
- Semaphore简介
Semaphore简介 Semaphore是并发包中提供的用于控制某资源同时被访问的个数 操作系统的信号量是个很重要的概念,在进程控制方面都有应用.Java 并发库 的Semaphore 可以很轻松完 ...
- BZOJ_1085_[SCOI2005]骑士精神_IDDFS
BZOJ_1085_[SCOI2005]骑士精神_DFS Description 在一个5×5的棋盘上有12个白色的骑士和12个黑色的骑士, 且有一个空位.在任何时候一个骑士都能按照骑 士的走法(它可 ...
- BZOJ_4443_[Scoi2015]小凸玩矩阵_二分+二分图匹配
BZOJ_4443_[Scoi2015]小凸玩矩阵_二分+二分图匹配 Description 小凸和小方是好朋友,小方给小凸一个N*M(N<=M)的矩阵A,要求小秃从其中选出N个数,其中任意两个 ...
- 【开源】OSharpNS,轻量级.net core快速开发框架发布
OSharpNS简介 OSharp Framework with .NetStandard2.0(OSharpNS)是OSharp的以.NetStandard2.0为目标框架,在AspNetCore的 ...
- CBC 字节反转攻击
一.CBC 简介 现代密码体制 现代密码中的加密体制一般分为对称加密体制(Symmetric Key Encryption)和非对称加密体制(Asymmetric Key Encryption).对称 ...