# please run pip install sklearn-crfsuite in your environment
#导入数据分析所需的基础包 import pandas as pd import numpy as np import os #包tqdm是用来对可迭代对象执行时生成一个进度条用以监视程序运行过程 from tqdm import tqdm #导入训练集测试集划分的包 from sklearn.model_selection import train_test_split #导入CRF模型所需的包 from sklearn_crfsuite import CRF #导入模型评估所需的包 from sklearn_crfsuite import metrics
读取数据
数据保存在data/目录下,data/目录下共有四个文件夹,分别对应四种医学情景:出院情况、病史特点、诊疗过程和一般项目。每个文件夹下保存了该情景下的电子病历。包括两类文件:'xxx-yyy.txtoriginal.txt'和'xxx-yyy.txt'。'xxx-yyy.txtoriginal.txt'保存了xxx情境下第yyy号病历的病历文本,保存在txt的第一行中。'xxx-yyy.txt'为其对应的标签数据。
数据中共包含5种实体:治疗、身体部位、疾病和诊断、症状和体征、检查和检验。
#读取一个病历文本数据,并查看其内容。 with open('data/病史特点/病史特点-39.txtoriginal.txt','r', encoding='utf-8') as f: content = f.read().strip() print(content)
1、患者缘于1小时前被他人打伤头面部,鼻背部,左胸部,伤后头晕头痛,心慌气短,鼻背部疼痛,左胸部疼痛,双膝部右手右肘疼痛,自来我院。血压130/80mmHg。神清语利,扶入诊室,查体合作。头枕部约4.0厘米5.0厘米软组织红肿,触痛。鼻背部肿胀明显,触痛。左胸部可见约3.0厘米3.0厘米软组织红肿,触痛。双膝关节活动受限,双膝、右手背散在挫伤痕。右肘部可见皮肤挫伤痕。神经系统查体未见异常。辅助检查:头颅、鼻骨、全腹部CT:未见明显异常;肋骨三维重建:左侧第7肋骨骨折。右手正斜位X光:未见明显异常;右肘、双膝正侧位X光:未见明显异常;下颌骨X光:未见异常。 主因头面部鼻背部,左胸部外伤1小时来院。入院查体:血压130/80mmHg。神清语利,扶入诊室,查体合作。头枕部约4.0厘米5.0厘米软组织红肿,触痛。鼻背部肿胀明显,触痛。左胸部可见约3.0厘米3.0厘米软组织红肿,触痛。双膝关节活动受限,双膝、右手背散在挫伤痕。右肘部可见皮肤挫伤痕。神经系统查体未见异常。辅助检查:头颅、鼻骨、全腹部CT:未见明显异常;肋骨三维重建:左侧第7肋骨骨折。右手正斜位X光:未见明显异常;右肘、双膝正侧位X光:未见明显异常;下颌骨X光:未见异常。
#读取上述病历对应的标签数据 with open('data/一般项目/一般项目-39.txt','r', encoding='utf-8') as f: content_label = f.read().strip() print(content_label)
背部 0 1 身体部位 左胸部 3 5 身体部位
可以看出,标签文件的数据格式为每行对应一个实体,每行格式为“实体内容 实体在文本中的开始位置 实体在文本中的结束位置 实体类别”。如第一行表示content[21:24]对应的便是'右髋部',为身体部位实体类别。
数据标注
实体识别的数据标注方式主要有BIOES和BIO两种,详细的介绍参考实验手册。这里为使标注类别不至于太多,我们采用BIO方式。即将实体部分的第一个汉字标注为B,实体的其他部分的汉字标注为I,非实体部分标注为O。
将5种实体类别治疗、身体部位、疾病和诊断、症状和体征、检查和检验分别标记为TREATMENT、BODY、DISEASES、SIGNS、EXAMINATIONS。
则标记时,如:若为治疗类别的实体的第一个汉字,则将其标注为B-TREATMENT,该实体其他字标记为I-TREATMENT。
label_dict = {'治疗':'TREATMENT', '身体部位':'BODY', '疾病和诊断':'DISEASES', '症状和体征':'SIGNS', '检查和检验':'EXAMINATIONS'} def sentence2BIOlabel(sentence,label_from_file): ''' 返回句子sentence的BIO标注列表 入参: sentence:一个句子,字符串类别 label_from_file:该句子对应的标签,格式为直接从txt文件中读出的格式,形如上文中的content_label 出参: sentence_label:该句子的BIO标签。一个列表,列表的第i项为第i个汉字对应的标签 ''' #初始的sentence_label每个标签均定义为'O'。之后会修改其中实体部分的标签。 sentence_label = ['O']*len(sentence) if label_from_file=='': return sentence_label #line为label_from_file中每一行的数据,对应一个实体的信息。格式为“实体内容 实体在文本中的开始位置 实体在文本中的结束位置 实体类别” for line in label_from_file.split('\n'): #entity_info中保存了单个实体的信息 entity_info = line.strip().split('\t') start_index = int(entity_info[1]) #实体在文本中的开始位置 end_index = int(entity_info[2]) #实体在文本中的结束位置 entity_label = label_dict[entity_info[3]] #实体标签类别 #为实体的第一个汉字标记为B-xx sentence_label[start_index] = 'B-'+entity_label #为实体中的其他汉字标记为I-xx for i in range(start_index+1,end_index+1): sentence_label[i] = 'I-'+entity_label return sentence_label
#以上文中的content和content_label为例查看sentence2BIOlabel函数的使用方法 #返回上文中content对应的BIO标签并输出 sentence_label_tmp = sentence2BIOlabel(content,content_label) print(sentence_label_tmp)
['B-BODY', 'I-BODY', 'O', 'B-BODY', 'I-BODY', 'I-BODY', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']
#输出content中每个汉字与BIO标签的对应关系 for i in range(len(content)): print(content[i],sentence_label_tmp[i])
1 B-BODY 、 I-BODY 患 O 者 B-BODY 缘 I-BODY 于 I-BODY 1 O 小 O 时 O 前 O 被 O 他 O 人 O 打 O 伤 O 头 O 面 O 部 O , O 鼻 O 背 O 部 O , O 左 O 胸 O 部 O , O 伤 O 后 O 头 O 晕 O 头 O 痛 O , O 心 O 慌 O 气 O 短 O , O 鼻 O 背 O 部 O 疼 O 痛 O , O 左 O 胸 O 部 O 疼 O 痛 O , O 双 O 膝 O 部 O 右 O 手 O 右 O 肘 O 疼 O 痛 O , O 自 O 来 O 我 O 院 O 。 O 血 O 压 O 1 O 3 O 0 O / O 8 O 0 O m O m O H O g O 。 O 神 O 清 O 语 O 利 O , O 扶 O 入 O 诊 O 室 O , O 查 O 体 O 合 O 作 O 。 O 头 O 枕 O 部 O 约 O 4 O . O 0 O 厘 O 米 O 5 O . O 0 O 厘 O 米 O 软 O 组 O 织 O 红 O 肿 O , O 触 O 痛 O 。 O 鼻 O 背 O 部 O 肿 O 胀 O 明 O 显 O , O 触 O 痛 O 。 O 左 O 胸 O 部 O 可 O 见 O 约 O 3 O . O 0 O 厘 O 米 O 3 O . O 0 O 厘 O 米 O 软 O 组 O 织 O 红 O 肿 O , O 触 O 痛 O 。 O 双 O 膝 O 关 O 节 O 活 O 动 O 受 O 限 O , O 双 O 膝 O 、 O 右 O 手 O 背 O 散 O 在 O 挫 O 伤 O 痕 O 。 O 右 O 肘 O 部 O 可 O 见 O 皮 O 肤 O 挫 O 伤 O 痕 O 。 O 神 O 经 O 系 O 统 O 查 O 体 O 未 O 见 O 异 O 常 O 。 O 辅 O 助 O 检 O 查 O : O 头 O 颅 O 、 O 鼻 O 骨 O 、 O 全 O 腹 O 部 O C O T O : O 未 O 见 O 明 O 显 O 异 O 常 O ; O 肋 O 骨 O 三 O 维 O 重 O 建 O : O 左 O 侧 O 第 O 7 O 肋 O 骨 O 骨 O 折 O 。 O 右 O 手 O 正 O 斜 O 位 O X O 光 O : O 未 O 见 O 明 O 显 O 异 O 常 O ; O 右 O 肘 O 、 O 双 O 膝 O 正 O 侧 O 位 O X O 光 O : O 未 O 见 O 明 O 显 O 异 O 常 O ; O 下 O 颌 O 骨 O X O 光 O : O 未 O 见 O 异 O 常 O 。 O O 主 O 因 O 头 O 面 O 部 O 鼻 O 背 O 部 O , O 左 O 胸 O 部 O 外 O 伤 O 1 O 小 O 时 O 来 O 院 O 。 O 入 O 院 O 查 O 体 O : O 血 O 压 O 1 O 3 O 0 O / O 8 O 0 O m O m O H O g O 。 O 神 O 清 O 语 O 利 O , O 扶 O 入 O 诊 O 室 O , O 查 O 体 O 合 O 作 O 。 O 头 O 枕 O 部 O 约 O 4 O . O 0 O 厘 O 米 O 5 O . O 0 O 厘 O 米 O 软 O 组 O 织 O 红 O 肿 O , O 触 O 痛 O 。 O 鼻 O 背 O 部 O 肿 O 胀 O 明 O 显 O , O 触 O 痛 O 。 O 左 O 胸 O 部 O 可 O 见 O 约 O 3 O . O 0 O 厘 O 米 O 3 O . O 0 O 厘 O 米 O 软 O 组 O 织 O 红 O 肿 O , O 触 O 痛 O 。 O 双 O 膝 O 关 O 节 O 活 O 动 O 受 O 限 O , O 双 O 膝 O 、 O 右 O 手 O 背 O 散 O 在 O 挫 O 伤 O 痕 O 。 O 右 O 肘 O 部 O 可 O 见 O 皮 O 肤 O 挫 O 伤 O 痕 O 。 O 神 O 经 O 系 O 统 O 查 O 体 O 未 O 见 O 异 O 常 O 。 O 辅 O 助 O 检 O 查 O : O 头 O 颅 O 、 O 鼻 O 骨 O 、 O 全 O 腹 O 部 O C O T O : O 未 O 见 O 明 O 显 O 异 O 常 O ; O 肋 O 骨 O 三 O 维 O 重 O 建 O : O 左 O 侧 O 第 O 7 O 肋 O 骨 O 骨 O 折 O 。 O 右 O 手 O 正 O 斜 O 位 O X O 光 O : O 未 O 见 O 明 O 显 O 异 O 常 O ; O 右 O 肘 O 、 O 双 O 膝 O 正 O 侧 O 位 O X O 光 O : O 未 O 见 O 明 O 显 O 异 O 常 O ; O 下 O 颌 O 骨 O X O 光 O : O 未 O 见 O 异 O 常 O 。 O
#将数据集中每个样本读进来并将其保存在sentence_list中,将每个样本对应的BIO标签保存在label_list中 #sentence_list的格式为[第一个句子,第二个句子,第三个句子,...,第n个句子] #label_list的格式为[第一个句子对应的BIO标注列表,第二个句子对应的BIO标注列表,第三个句子对应的BIO标注列表,...,第n个句子对应的BIO标注列表] ##在这里输入你的代码 import os sentence_list = [] label_list = [] text = "" for folder in ["病史特点", "出院情况", "一般项目", "诊疗过程"]: for i in range(1, 1000): try: with open(f"./data/{folder}/{folder}-{i}.txt",'r', encoding='utf-8') as f: label = f.read().strip() with open(f"./data/{folder}/{folder}-{i}.txtoriginal.txt",'r', encoding='utf-8') as f: text = f.read().strip() sentence_label_tmp = sentence2BIOlabel(text,label) text = [_ for _ in text] assert len(sentence_label_tmp) == len(text) sentence_list.append("".join(text)) label_list.append(sentence_label_tmp) except IndexError and FileNotFoundError: pass ##结束你的代码
文本特征工程
要使用CRF算法对每个字进行标注,就需要获取每个字对应的特征。就需要对文本进行特征工程,这一部分就是构建一句话中每个字的特征。
#中文分词时最常用的包是jieba。但我们本次的数据集是专门针对医疗领域的,这里选用了一个在细分领域上表现更好的库pkuseg import pkuseg #将model_name设置为'medicine'以加载医疗领域的模型。第一次执行此代码时会自动下载医疗领域对应的模型,这可能需要一些时间。 #设置postag为True会在分词的同时进行词性标注 seg = pkuseg.pkuseg(model_name='medicine',postag=True)
#pkuseg包使用示例 seg.cut('发病原因为右髋部摔伤后疼痛肿胀')
[('发病', 'vn'), ('原因', 'n'), ('为', 'v'), ('右髋部', 'n'), ('摔伤', 'v'), ('后', 'f'), ('疼痛', 'a'), ('肿胀', 'v')]
可以看出,pkuseg包对医疗方面的文本有较好的分词效果。seg.cut(文本)的输出格式为[(第一个词,第一个词的词性),(第二个词,第二个词的词性),...,(第n个词,第n个词的词性)]。稍后在构建每个字的特征时我们会用到pkuseg的分词功能。
#加载医学的专业词汇词库THUOCL_medical.txt。这一文件是从https://github.com/thunlp/THUOCL中下载而来。 #文件中每行的格式为:医学名词 词频 #读取文件 with open('THUOCL_medical.txt', 'r', encoding='utf-8') as f: medical_words = f.read().strip() #获取医疗词汇表 medical_words_list = [words.strip().split('\t')[0] for words in medical_words.split('\n')]
#医疗词汇表示例,这一词汇表在我们构建特征时会用到。 medical_words_list[:10]
['精神', '医院', '检查', '死亡', '恢复', '意识', '医疗', '治疗', '卫生', '患者']
进行完上述准备工作后,我们接下来正式来构造特征。
def word2feature(sentence,i): ''' 返回句子sentence中第i个汉字的一些简单的特征 入参: sentence:待处理的句子 i:会返回第i个汉字的一些简单的特征 出参: simple_feature:由一些简单的特征所组成的字典,字典的键为特征名,值为特征值 ''' simple_feature = {} simple_feature['word'] = sentence[i] #当前字 simple_feature['pre_word'] = sentence[i-1] if i>0 else 'start' #前一个字 simple_feature['after_word'] = sentence[i+1] if i<len(sentence)-1 else 'end' #后一个字 #接下来加入当前字的Bi-gram特征,即前一个字+当前字、当前字+后一个字,并将特征分别命名为'pre_word_word'和'word_after_word' ##在这里输入你的代码 simple_feature['pre_word_word'] = sentence[i-1: i+1] if i > 0 else sentence[i] simple_feature['word_after_word'] = sentence[i: i+2] if i < len(sentence)-1 else sentence[i] ##结束你的代码 #加入一个偏置项 simple_feature['bias'] = 1 return simple_feature
def sentence2feature(sentence): ''' 在word2feature定义的简单特征的基础上,增加一些复杂的特征,并返回句子中每个字对应的特征字典所组成的列表 入参: sentence:待处理的句子 出参: sentence_feature_list:句子中每个字对应的特征字典所组成的列表。格式为:[第一个字的特征字典,第二个字的特征字典,...,第n个字的特征字典] ''' sentence_feature_list = [word2feature(sentence,i) for i in range(len(sentence))] #为每个字增加一些复杂的特征 #增加当前字在分词后所在的词,该词的词性,该词的上一个词,该词的下一个词,该词是否为医疗专业词汇,该字是否为该词的第一个字 word_index = 0 #指向字的指针,会逐步往后移动,其作用在之后可以看到 #使用pkuseg对句子进行分词 sentence_cut = seg.cut(sentence) #这里为和字进行区分,使用大写的WORD来表示词,小写的word来表示字 for i,(WORD,nominal) in enumerate(sentence_cut): # print(f"WORD: {WORD}") # print(f"nominal: {nominal}") for j in range(word_index,word_index+len(WORD)): # print(f"sentence_feature_list[{j}]: {sentence_feature_list[j]}") sentence_feature_list[j]['WORD'] = WORD #当前字在分词后所在的词 sentence_feature_list[j]['nominal'] = nominal #该词的词性 sentence_feature_list[j]['pre_WORD'] = sentence_cut[i-1][0] if i>0 else 'START' #该词的上一个词 sentence_feature_list[j]['after_WORD'] = sentence_cut[i+1][0] if i<len(sentence_cut)-1 else 'END' #该词的下一个词 sentence_feature_list[j]['is_medicalwords'] = 1 if WORD in medical_words_list else 0 #该词是否为医学专业词汇 #加入一个特征'is_first'表示当前字是否为其所属词的第一个字,是则将该特征值记为1,否则记为0 ##在这里输入你的代码 sentence_feature_list[j]['is_first'] = 1 if sentence_feature_list[j]["word"] == WORD[0] else 0 ##结束你的代码 word_index = word_index+len(WORD) #更新word_index的值 return sentence_feature_list
#获取sentence_list中每句话中每个字对应的特征,并将其保存在feature_list中 #使用tqdm函数来输出一个进度条,以监控代码的运行过程 feature_list = [sentence2feature(sentence) for sentence in tqdm(sentence_list)]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1198/1198 [00:58<00:00, 20.50it/s]
CRF模型搭建
#首先对数据划分训练集和测试集 x_train,x_test,y_train,y_test = train_test_split(feature_list, label_list, test_size=0.3, random_state=2020)
#搭建一个CRF模型 crf = CRF( algorithm='lbfgs', #训练算法 c1=0.1, #L1正则化系数 c2=0.1, #L2正则化系数 max_iterations=100, #优化算法的最大迭代次数 all_possible_transitions=False ) #使用crf模型对训练集进行训练 crf.fit(x_train,y_train)
C:\Users\avaws\anaconda3\envs\pkuseg\lib\site-packages\sklearn\base.py:213: FutureWarning: From version 0.24, get_params will raise an AttributeError if a parameter cannot be retrieved as an instance attribute. Previously it would return None. FutureWarning)
def predict(sentence): ''' 输出CRF预测的一个句子的BIO标注 入参: sentence:待处理的句子 出参: sent_bio:一个字典,字典的键为句子中的汉字,值为其对应的BIO标注 ''' #提示:按照获取输入句子的特征、获取crf的预测值、获取以句子中汉字为键,对应的BIO标注为值的字典 的步骤进行 #提示:crf.predict_single(待预测句子的特征) 可用来预测单个句子的BIO标注,返回值为每个字的BIO标注列表 ##在这里输入你的代码 sent_bio = crf.predict_single(sentence2feature(sentence)) ##结束你的代码 return sent_bio
#使用predict函数对一个句子进行预测 predict('这是由于耳膜损伤导致的')
['O', 'O', 'O', 'O', 'B-BODY', 'I-BODY', 'O', 'O', 'O', 'O', 'O']
可以看出CRF模型能够有效的识别出这句话中的实体,接下来我们用CRF模型对我们的测试集进行预测。
#获取测试集的预测值 y_pred = crf.predict(x_test)
模型评估
使用sklearn_crfsuite中自带的metrics包可对模型进行有效的评估
#获取crf模型的全部标签 labels = list(crf.classes_) #由于标签O过多,而我们对其他标签更感兴趣。为了解决这个问题,我们标签O移除。 labels.remove('O') #查看除'O'外的全部标签 labels
['B-TREATMENT', 'I-TREATMENT', 'B-BODY', 'I-BODY', 'B-SIGNS', 'I-SIGNS', 'B-EXAMINATIONS', 'I-EXAMINATIONS', 'B-DISEASES', 'I-DISEASES']
#计算除O之外的所有标签计算的平均F1分数。 metrics.flat_f1_score(y_test, y_pred, average='micro', labels=labels)
0.937281513469052
#查看每个类别的预测情况 print(metrics.flat_classification_report( y_test, y_pred, labels=labels, digits=3 ))
C:\Users\avaws\anaconda3\envs\pkuseg\lib\site-packages\sklearn\utils\validation.py:71: FutureWarning: Pass labels=['B-TREATMENT', 'I-TREATMENT', 'B-BODY', 'I-BODY', 'B-SIGNS', 'I-SIGNS', 'B-EXAMINATIONS', 'I-EXAMINATIONS', 'B-DISEASES', 'I-DISEASES'] as keyword args. From version 0.25 passing these as positional arguments will result in an error FutureWarning)
BiLSTM-CRF
CRF模型还可与BiLSTM模型结合来解决实体识别问题,这样的好处是BiLSTM可以自动获取文本的特征,我们便不需要自己去定义特征,不需要再进行文本特征工程部分。
由于BiLSTM-CRF的代码过于冗长,且实现这一代码并不是我们的重点,而仅做展示之用。所以我们把BiLSTM-CRF模型的实现细节均在BiLSTM_CRF.py中实现。这里仅展示部分关键部分。
! conda install pytorch torchvision torchaudio cudatoolkit=11.3 -c pytorch
Collecting package metadata (current_repodata.json): ...working... done Solving environment: ...working... done # All requested packages already installed.
from BiLSTM_CRF import *
C:\Users\avaws\anaconda3\envs\pkuseg\lib\site-packages\tqdm\auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
#将word映射到id word2id = word_to_id(sentence_list) #将label映射到id tag2id = tag_to_id(label_list)
#查看label与id的映射关系
tag2id
{'O': 0, 'B-BODY': 1, 'I-BODY': 2, 'B-SIGNS': 3, 'I-SIGNS': 4, 'B-EXAMINATIONS': 5, 'I-EXAMINATIONS': 6, 'B-DISEASES': 7, 'I-DISEASES': 8, 'B-TREATMENT': 9, 'I-TREATMENT': 10, '<unk>': 11, '<pad>': 12, '<start>': 13, '<end>': 14}
LSTM模型训练的时候需要在word2id和tag2id加入PAD和UNK,如果是加了CRF的lstm还要加入<start>和<end> (解码的时候需要用到)。word2id的格式与tag2id的格式类似。
#按照与CRF模型划分训练集测试集时相同的比例和相同的随机数种子对sentence_list与label_list划分训练集合测试集 x_train_lstmcrf,x_test_lstmcrf,y_train_lstmcrf,y_test_lstmcrf = train_test_split(sentence_list, label_list, test_size=0.3, random_state=2020)
#为每句话后加入一个"<end>" token x_train_lstmcrf,y_train_lstmcrf = prepocess_data_for_lstmcrf(x_train_lstmcrf,y_train_lstmcrf) x_test_lstmcrf,y_test_lstmcrf = prepocess_data_for_lstmcrf(x_test_lstmcrf,y_test_lstmcrf,test=True)
#搭建一个BiLSTM_CRF模型 model = BiLSTM_CRF_Model(vocab_size=len(word2id),out_size=len(tag2id),batch_size=64, epochs=30) #在训练集上进行训练 model.train(x_train_lstmcrf,y_train_lstmcrf,word2id,tag2id)
Epoch 1, step/total_step: 10/14 71.43% Loss:679.0421 Epoch 1, Val Loss:301.0062 Epoch 2, step/total_step: 10/14 71.43% Loss:374.4472 Epoch 2, Val Loss:230.5597 Epoch 3, step/total_step: 10/14 71.43% Loss:303.9551 Epoch 3, Val Loss:205.2301 Epoch 4, step/total_step: 10/14 71.43% Loss:269.2586 Epoch 4, Val Loss:172.2326 Epoch 5, step/total_step: 10/14 71.43% Loss:224.5888 Epoch 5, Val Loss:140.8520 Epoch 6, step/total_step: 10/14 71.43% Loss:183.4506 Epoch 6, Val Loss:116.7093 Epoch 7, step/total_step: 10/14 71.43% Loss:152.1754 Epoch 7, Val Loss:105.2365 Epoch 8, step/total_step: 10/14 71.43% Loss:133.0295 Epoch 8, Val Loss:85.2563 Epoch 9, step/total_step: 10/14 71.43% Loss:109.7353 Epoch 9, Val Loss:72.5094 Epoch 10, step/total_step: 10/14 71.43% Loss:93.3250 Epoch 10, Val Loss:63.0916 Epoch 11, step/total_step: 10/14 71.43% Loss:81.4246 Epoch 11, Val Loss:56.4978 Epoch 12, step/total_step: 10/14 71.43% Loss:72.7384 Epoch 12, Val Loss:50.4667 Epoch 13, step/total_step: 10/14 71.43% Loss:65.0488 Epoch 13, Val Loss:45.2442 Epoch 14, step/total_step: 10/14 71.43% Loss:58.7878 Epoch 14, Val Loss:41.5584 Epoch 15, step/total_step: 10/14 71.43% Loss:53.7732 Epoch 15, Val Loss:37.7015 Epoch 16, step/total_step: 10/14 71.43% Loss:49.1189 Epoch 16, Val Loss:34.6484 Epoch 17, step/total_step: 10/14 71.43% Loss:45.5345 Epoch 17, Val Loss:32.9121 Epoch 18, step/total_step: 10/14 71.43% Loss:42.7169 Epoch 18, Val Loss:30.8214 Epoch 19, step/total_step: 10/14 71.43% Loss:40.0796 Epoch 19, Val Loss:28.5754 Epoch 20, step/total_step: 10/14 71.43% Loss:37.5324 Epoch 20, Val Loss:27.1824 Epoch 21, step/total_step: 10/14 71.43% Loss:35.6576 Epoch 21, Val Loss:25.6989 Epoch 22, step/total_step: 10/14 71.43% Loss:33.6447 Epoch 22, Val Loss:24.4878 Epoch 23, step/total_step: 10/14 71.43% Loss:31.8999 Epoch 23, Val Loss:22.9804 Epoch 24, step/total_step: 10/14 71.43% Loss:30.4300 Epoch 24, Val Loss:22.2672 Epoch 25, step/total_step: 10/14 71.43% Loss:29.0533 Epoch 25, Val Loss:20.9311 Epoch 26, step/total_step: 10/14 71.43% Loss:27.6675 Epoch 26, Val Loss:20.0889 Epoch 27, step/total_step: 10/14 71.43% Loss:26.3884 Epoch 27, Val Loss:19.4638 Epoch 28, step/total_step: 10/14 71.43% Loss:25.4839 Epoch 28, Val Loss:18.4383 Epoch 29, step/total_step: 10/14 71.43% Loss:24.1868 Epoch 29, Val Loss:18.2865 Epoch 30, step/total_step: 10/14 71.43% Loss:23.7794 Epoch 30, Val Loss:17.5620
#获取测试集的预测值 y_pred_lstmcrf, _ = model.test(x_test_lstmcrf,y_test_lstmcrf,word2id,tag2id)
C:\Users\avaws\anaconda3\envs\pkuseg\lib\site-packages\torch\nn\modules\rnn.py:695: UserWarning: RNN module weights are not part of single contiguous chunk of memory. This means they need to be compacted at every call, possibly greatly increasing memory usage. To compact weights again call flatten_parameters(). (Triggered internally at ..\aten\src\ATen\native\cudnn\RNN.cpp:925.) self.num_layers, self.dropout, self.training, self.bidirectional)
#计算BiLSTM-CRF模型除O之外的所有标签计算的平均F1分数。 metrics.flat_f1_score(y_test_lstmcrf, y_pred_lstmcrf, average='micro', labels=labels)
0.9282667117701228
#查看BiLSTM-CRF模型每个类别的预测情况 print(metrics.flat_classification_report( y_test_lstmcrf, y_pred_lstmcrf, labels=labels, digits=3 ))
C:\Users\avaws\anaconda3\envs\pkuseg\lib\site-packages\sklearn\utils\validation.py:71: FutureWarning: Pass labels=['B-TREATMENT', 'I-TREATMENT', 'B-BODY', 'I-BODY', 'B-SIGNS', 'I-SIGNS', 'B-EXAMINATIONS', 'I-EXAMINATIONS', 'B-DISEASES', 'I-DISEASES'] as keyword args. From version 0.25 passing these as positional arguments will result in an error FutureWarning)