当前位置: 首页 > 科技观察

用Keras+LSTM+CRF练习命名实体识别NER

时间:2023-03-18 23:41:08 科技观察

文本分割、词性标注和命名实体识别都是自然语言处理领域非常基础的任务。它们的准确性决定了下游任务的准确性。其实在这之前我并没有真正接触过命名实体识别的工作。虽然在读研究生的时候断断续续的参与过这样的项目,但是毕业后总觉得自己了解的不多。最近想重新捡起来,以练习为主的学习手段。系统地理解、学习和实践命名实体识别的任务。在如今的各种应用中,几乎不可能说哪些任务不会有深度学习的影子。很多子任务的发展历程惊人的相似。起初,大部分研究和应用都集中在机器学习领域。学习模型的开发也得到了广泛的应用。命名实体识别等序列标注任务也不例外。基于LSTM+CRF的深度学习实体识别已有相关研究,但与我之前的工作有所不同。方向不一致,所以一直没花太多时间关注。最近刚好在学NER。在之前的相关文章中,我实践了简单的基于机器学习方法的命名实体识别。在这里,它基于深度学习模型。实现NER。命名实体识别是一个序列标记任务。事实上,它更像是一个分类任务。NER是识别一段文本中预定义的实体类型。NER是序列标注问题,所以他们的数据标注方法也遵循序列标注问题,主要是BIO和BIOES。这里直接介绍BIOES。如果您了解BIOES,您将掌握BIO。先列出BIOES代表什么:B,也就是Begin,表示开头I,也就是Intermediate,表示中间E,也就是End,表示结尾S,也就是Single,表示单个字符O,也就是表示Other,用于标记不相关的字符例如,对于下面的句子:YaoMingplayedinthegymnasiumofHarbinInstitute,标记结果为:YaoMingplayedinthegymnasiumofHarbinInstituteB-PERE-PEROB-ORGI-ORGI-ORGI-ORGI-ORGI-ORGE-ORGB-LOCI-LOCE-LOCOOO的简单回顾到此结束,接下来进入本文的实践部分,首先是数据集部分,数据集是从网上获取的,简单看一下样本数据,如下图:train_data部分的样本数据如下图所示:当O希望O希望O项目O项目O救援O帮助O百万O孩子O孩子们O成长O成长O崛起O来O,O科学O教导O繁荣O国家O繁荣O自然人O成功O风O时间O,O今天O日O有O收藏O收藏O价值O价值O书O你O没有O买O,明天O日O会O打电话O你O后悔O不O在开始哦!Otest_data的部分样本数据如下:highOholdOpatriotismOnationalismOprincipleOandOsocietyOsocietyOprincipleOprincipleOtwoOsurfaceOflagObannerO,OunityOunityOwholeObodyOformationO社员O用O与O所O联结O与O归O华侨O、O华侨O亲属O、O宣扬O爱O国O革命O命运O光荣O光荣O传统O传统O,O是O统一O一O祖国O国O振兴O中华B-LOC华I-LOC和O努力O奋斗O奋斗O;O简单了解训练集和测试集的数据结构后,就可以进行后续的数据处理,主要目的是生成特征数据。核心代码实现如下:withopen('test_data.txt',encoding='utf-8')asf:test_data_list=[one.strip().split('\t')foroneinf.readlines()ifone.strip()]withopen('train_data.txt',encoding='utf-8')asf:train_data_list=[one.strip().split('\t')foroneinf.readlines()ifone.strip()]char_list=[one[0]foroneintest_data_list]+[one[0]foroneintrain_data_list]label_list=[one[-1]foroneintest_data_list]+[one[-1]foroneintrain_data_list]print('char_list_length:',len(char_list))print('label_list_length:',len(label_list))print('char_num:',len(list(set(char_list))))print('label_num:',len(list(set(label_list))))char_count,label_count={},{}#字符频率统计foroneinchar_list:ifoneinchar_count:char_count[one]+=1else:char_count[one]=1foroneinlabel_list:ifoneinlabel_count:label_count[one]+=1else:label_count[one]=1#按频率降序排序sortedsorted_char=sorted(char_count.items(),key=lambdae:e[1],reverse=True)sortedsorted_label=sorted(label_count.items(),key=lambdae:e[1],reverse=True)#Character-id映射关系构建char_map_dict={}label_map_dict={}foriinrange(len(sorted_char)):char_map_dict[sorted_char[i][0]]=ichar_map_dict[str(i)]=sorted_char[i][0]foriinrange(len(sorted_label)):label_map_dict[sorted_label[i][0]]=ilabel_map_dict[str(i)]=sorted_label[i][0]#结果存储withopen('charMap.json','w')asf:f.write(json.dumps(char_map_dict))withopen('labelMap.json','w')asf:f.write(json.dumps(label_map_dict))代码实现很清晰,关键其中一些也有相应的注释内容,这里就不多解释了。核心思想是将字符或标签类别数据映射到相应的索引数据。这里我没有设置频率的过滤阈值,有些实现会过滤掉只出现一次的数据,这个可以根据自己的需要修改charMap数据样例如下:labelMap数据样例如下:上面的映射数据生成后,可以对原始文本数据进行转换计算,然后生成我们需要的特征数据。核心代码实现如下:X_train,y_train,X_test,y_test=[],[],[],[]#trainingdatasetforiinrange(len(trainData)):one_sample=[one.strip().split('\t')foroneintrainData[i]]char_list=[O[0]forOinone_sample]label_list=[O[1]forOinone_sample]char_vec=[char_map_dict[char_list[v]]forvinrange(len(char_list))]label_vec=[label_map_dict[label_list[l]]forlinrange(len(label_list))]X_train.append(char_vec)y_train.append(label_vec)#测试数据集foriinrange(len(testData)):one_sample=[one.strip().split('\t')foroneintestData[i]]char_list=[O[0]forOinone_sample]label_list=[O[1]forOinone_sample]char_vec=[char_map_dict[char_list[v]]forvinrange(len(char_list))]label_vec=[label_map_dict[label_list[l]]forlinrange(len(label_list))]X_test.append(char_vec)y_test.append(label_vec)feature={}feature['X_train'],feature['y_train']=X_train,y_trainfeature['X_test'],feature['y_test']=X_test,y_test#resultstoragewithopen('feature.json','w')asf:f.write(json.dumps(feature))到这里就得到了我们需要的特征数据,并且划分了测试集数据和训练集数据接下来,您可以构建模型。这里为了简化实现,我使用了Keras框架。与原有的Tensorflow框架相比,上手门槛更低。核心代码实现如下:#loaddatasetwithopen('feature.json')asf:F=json.load(f)X_train,X_test,y_train,y_test=F['X_train'],F['X_test'],F['y_train'],F['y_test']#数据对齐操作X_train=pad_sequences(X_train,maxlen=max_len,value=0)y_train=pad_sequences(y_train,maxlen=max_len,value=-1)y_train=np.expand_dims(y_train,2)X_test=pad_sequences(X_test,maxlen=max_len,value=0)y_test=pad_sequences(y_test,maxlen=max_len,value=-1)y_test=np.expand_dims(y_test,2)#模型初始化,训练ifnotos.path.exists(saveDir):os.makedirs(saveDir)#modelinitializationmodel=Sequential()model.add(Embedding(voc_size,256,mask_zero=True))model.add(Bidirectional(LSTM(128,return_sequences=True)))model.add(Dropout(rate=0.5))model.add(Dense(tag_size))crf=CRF(tag_size,sparse_target=True)model.add(crf)model.summary()model.compile('adam',loss=crf.loss_function,metrics=[crf.accuracy])#Trainingfittinghistory=model.fit(X_train,y_train,batch_size=100,epochs=500,validation_data=[X_test,y_test])模型.save(saveDir+'model.h5')#模型结构可视化)plt.plot(history.history['acc'])plt.plot(history.history['val_acc'])plt.title('modelaccuracy')plt.ylabel('accuracy')plt.xlabel('epochs')plt.legend(['train','test'],loc='upperleft')plt.savefig(saveDir+'train_validation_acc.png')plt.clf()plt.plot(history.history['loss'])plt.plot(history.history['val_loss'])plt.title('modelloss')plt.ylabel('loss')plt.xlabel('epochs')plt.legend(['train','test'],loc='upperleft')plt.savefig(saveDir+'train_validation_loss.png')scores=model.evaluate(X_test,y_test,verbose=0)print("精度:%.2f%%"%(分数[1]*100))模型model_json=model.to_json()withopen(saveDir+'structure.json','w')asf:f.write(model_json)model.save_weights(saveDir+'weight.h5')print('===finish====')训练完成后,structure目录的文件结构为如下:模型结构图如下:训练过程中的accuracy曲线如下:训练过程中的loss值曲线如下:由于训练计算资源比较大,时间比较长,我这里我们简单设置20次迭代计算。您可以根据自己的实际情况设置更高或更低的迭代次数,以实现不同的需求。简单预测示例如下:到这里,本文的实践就结束了,后面有时间继续深入研究,希望对你有所帮助,祝你工作顺利,学业有成!