Python3.7+Tensorflow2.0(TF2)实现Bilstm+mask-self-attention+CRF实现命名实体识别

一、他说的是对的

前几天看到一篇关于大连理工大学的研三学长的去世新闻,仔细看了他的遗书,很是泪目。他说同样的条件,做出的实验结果是不同的。
在训练我这个模型的时候,深深体会到了这个感受,有时候收敛,有时候无论怎么也不收敛。可能这个还容易解释一点,模型的很多参数是初始化的,不同的参数会跑到局部最you,模型陷在了一个局部最优点,出不去。
可能我这个模型的结构和参数都有问题,在训练过程中,损失最低也就是0.9+,然后别的一直就很高,都是三位数的损失,然后在测试集上训练的损失也很高,很是头疼的。
希望大家一起加油,马克思哲学告诉我们,前途是光明的,但道路是曲折的,事物总是螺旋上升,遵循否定之否定规律。好吧!!!不装了,不过马克思哲学是真的厉害噢,很佩服马克思的洞察力。愿逝者安息,下辈子成功做一只有人爱的猫猫。

二、研一入学自己踩的坑

感觉是自己导师给了自己机会,自己没有把握住,入学一个多月了,我一直都是在游离的状态,导师让我跟着一个博士师姐做项目,我觉得有时自己在逃避,自己乱学。其实,这些知识,是永远学不完的,只能以点带面,专注一个方向,接下来决定花时间好好帮助师姐做项目了--------方面级情感分析。导师也有很大的可能性让我开这个方向的题目。加油,是要好好看论文了。我真菜噢,课题组最清闲的人。

三、代码

3.1、创建参数文件,管理模型的全部参数Model_para.py

有些参数没有用到,自己改改吧

# -*- coding: utf-8 -*-
"""
Created on Wed Oct 14 20:24:48 2020

@author: DELL
"""

import argparse

class Hpara():
    parser = argparse.ArgumentParser()#构建一个参数管理对象
    
    parser.add_argument('--traindatapath',default='./data/train.csv',type=str)
    parser.add_argument('--testdatapath',default='./data/test.csv',type=str)
    
    parser.add_argument('--label2idpath',default='./data/label2id.json',type=str) 
    parser.add_argument('--word2idpath',default='./data/word2id.json',type=str) 
    parser.add_argument('--id2labelpath',default='./data/id2label.json',type=str) 
    parser.add_argument('--id2wordpath',default='./data/id2word.json',type=str) 
    
    parser.add_argument('--testlabel2idpath',default='./data/test_label2id.json',type=str) 
    parser.add_argument('--testword2idpath',default='./data/test_word2id.json',type=str) 
    parser.add_argument('--testid2labelpath',default='./data/test_id2label.json',type=str) 
    parser.add_argument('--testid2wordpath',default='./data/test_id2word.json',type=str)
       
    parser.add_argument('--attenion_drop_rate',default=0.2,type=float)
    parser.add_argument('--attention_size',default=400,type=int)
    parser.add_argument('--is_mask',default=True,type=bool)
    
    parser.add_argument('--max_sen_len',default=200,type=int)
    parser.add_argument('--train_nums',default=19717,type=int)
    parser.add_argument('--test_nums',default=200,type=int)
    parser.add_argument('--word2vector_dim',default=100,type=int)
    parser.add_argument('--hidden_dim',default=400,type=int)
    parser.add_argument('--token_nums',default=9275,type=int)
    parser.add_argument('--label_nums', default=14, type=int)
    parser.add_argument('--rnn_layers_nums',default=2,type=int)
    parser.add_argument('--is_training',default=True,type=bool)
    parser.add_argument('--drop_rate',default=0.1,type=float)    
    parser.add_argument('--learning_rate',default=0.005,type=float)
    parser.add_argument('--epochs',default=150,type=int)
    parser.add_argument('--cell_type',default='LSTM',type=str)
    parser.add_argument('--savepath',default='./check_point',type=str)
    parser.add_argument('--batch_size',default=50,type=int)
    

3.2、创建数据处理文件data_utils.py

之前一个师姐说,拿到一个模型,需要改写的最多的就是数据处理部分,确实是这样,你拿到模型之后,别人的数据集和我们的一般是不一样的,那么我们首先要做的事,就是把自己的数据转化为这个模型需要的形式。
下面的代码就是,两个数据集不一样,那么处理的时候,就要写不同的程序处理的。

mport pandas as pd
import numpy as np
import json

from Model_para import Hpara
hp=Hpara()
parser = hp.parser
para = parser.parse_args(args=[])

def data_process(para):
    '''
    我想使用训练数据和测试数据里面的全部标签和文本,这样就不用在标签字典里面加入
    特殊字符<UKN>,也就词典包括所有词和符号,如果有需要,还要修改数据处理的部分的代码
    Parameters
    ----------
    para : parser 实例
        管理各种模型的参数,这样修改起来也简单方便.
        
    Returns
    -------
    返回训练数据以及保存各种中间变量.

    '''
    train_data=pd.read_csv(para.traindatapath,delimiter=' ')
    test_data=pd.read_csv(para.testdatapath,delimiter=' ')
    
    train_text=list(train_data['text'])
    train_label=list(train_data['label'])
    
    test_text=list(test_data['text'])
    test_label=list(test_data['label'])
    
    
    text_set=list(set(list(train_data['text'])+list(test_data['text'])))
    label_set=list(set(list(train_data['label'])+list(test_data['label'])))
    #词和标签转化为id
    word2id=dict(zip(text_set,range(1,len(text_set)+1)))
    label2id=dict(zip(label_set,range(1,len(label_set)+1)))
    #id转化为单词和标签
    id2word=dict(zip(range(1,len(text_set)+1),text_set))
    id2label=dict(zip(range(1,len(label_set)+1),label_set))
    
    padding='PAD'#加入填充的数值,因为我是使用全部的字典,就不考虑特殊字符UNK了,如果有需要也可以加
    label2id[padding]=0
    word2id[padding]=0
    id2label[0]=padding
    id2word[0]=padding
       
    def convert_to_array(text,label,word_vocab,label_vocab,para,training):
        #将数据转化为矩阵
        indics=[i for i,x in enumerate(text) if x=='。']#多少个句号就有多少句话,数据集有点大,就取前2000行吧
        indics.insert(0,-1)
        if training:
            nums=para.train_nums
            array=np.zeros([nums,para.max_sen_len],dtype=np.int32)
            array_label=np.zeros([nums,para.max_sen_len],dtype=np.int32)
        else:
            nums=para.test_nums 
            array=np.zeros([nums,para.max_sen_len],dtype=np.int32)
            array_label=np.zeros([nums,para.max_sen_len],dtype=np.int32)
            
        for i in range(1,nums+1):
            index=indics[i]
            last_index=indics[i-1]
            for j in range(index-last_index):
                if j>99:
                    break
                word=text[j+last_index+1]
                l=label[j+last_index+1]
                array[i-1,j]=word_vocab[word]
                array_label[i-1,j]=label_vocab[l]
                
        return array,array_label
    train_array,train_label_array=convert_to_array(train_text,train_label,word2id,label2id,para,True)
    test_array,test_label_array=convert_to_array(test_text,test_label,word2id,label2id,para,False)
    
    #下面将数据返回,并且保存词典
    #保存这几个词典
    if para.is_training:
        with open(para.label2idpath,'w') as f:
            json.dump(label2id,f)
        with open(para.word2idpath,'w') as f:
            json.dump(word2id,f)
        with open(para.id2labelpath,'w') as f:
            json.dump(id2label,f)
        with open(para.id2wordpath,'w') as f:
            json.dump(id2word,f)
    else:
        with open(para.testlabel2idpath,'w') as f:
            json.dump(label2id,f)
        with open(para.testword2idpath,'w') as f:
            json.dump(word2id,f)
        with open(para.testid2labelpath,'w') as f:
            json.dump(id2label,f)
        with open(para.testid2wordpath,'w') as f:
            json.dump(id2word,f)
    
    return train_array,train_label_array,test_array,test_label_array


def data_process_ds1(para):
    train_data=pd.read_csv(para.traindatapath,delimiter='\t')
    test_data=pd.read_csv(para.testdatapath,delimiter='\t')
    
    train_data_no_nan=train_data.dropna()
    test_data_no_nan=test_data.dropna()
    
    train_text=list(train_data['text'])
    train_label=list(train_data['label'])
    test_text=list(test_data['text'])
    test_label=list(test_data['label'])
    
    word_vocab=list(set(list(train_data_no_nan['text'])+list(test_data_no_nan['text'])))
    label_vocab=list(set(list(train_data_no_nan['label'])+list(test_data_no_nan['label'])))
    
    #建立词典
    word2id=dict(zip(word_vocab,range(1,len(word_vocab)+1)))
    label2id=dict(zip(label_vocab,range(1,len(label_vocab)+1)))
    id2word=dict(zip(range(1,len(word_vocab)+1),word_vocab))
    id2label=dict(zip(range(1,len(label_vocab)+1),label_vocab))
    
    word2id['pad']=0
    label2id['pad']=0
    id2label[0]='pad'
    id2word[0]='pad'
    #下面开始找nan的位置
    def create_ds_arr(text,label,word2id,label2id,para):
        nan_ids=[i for i,t in enumerate(text) if str(t)=='nan']
        nan_ids.insert(0,-1)
        train_arr=np.zeros([len(nan_ids)-1,para.max_sen_len],dtype=np.int32)
        train_l=np.zeros([len(nan_ids)-1,para.max_sen_len],dtype=np.int32)
        
        for i in range(1,len(nan_ids)):
            index=nan_ids[i]
            last_index=nan_ids[i-1]
            for j in range(index-last_index-1):
                if j>=para.max_sen_len:
                    break
                word=text[j+last_index+1]
                l=label[j+last_index+1]
                train_arr[i-1,j]=word2id[word]
                train_l[i-1,j]=label2id[l]
                
        return train_arr,train_l
                
            
    t1,l1=create_ds_arr(train_text,train_label,word2id,label2id,para)
    t2,l2=create_ds_arr(test_text,test_label,word2id,label2id,para)
    
    
    return t1,l1,t2,l2

3.1、模型组件文件Model_modules.py

自己写了两种注意力机制的代码实现,带掩码的自注意力机制和带掩码的加性注意力。加性注意力的实现是有点问题的,因为原始论文实现的使用了编码器和解码器的隐向量,但是对于这个模型,相当于只有编码器的隐向量,如何实现加性注意力还需要灵活应用的。不知道你们有没有一些感觉,就是注意力机制,其实就是在考虑如何生成一组隐向量的权值,其实这个生成的方法是很多很多的,但是具体哪个效果好,还要自己设计实验来验证,所以,同志们,大胆的去胡思乱想吧,说不定自己设计的忘了的结果就很好。深度学习缺少严格健全的数学理论支撑,所以,计算机到底学到了什么,我们也不知道,可能,他们可以学习的知识系统呢,啊哈哈哈

# -*- coding: utf-8 -*-
"""
Created on Thu Oct 15 14:21:07 2020

@author: DELL
"""

import tensorflow as tf
from tensorflow.keras import layers
import tensorflow_addons as tfa

class RNN_layer(layers.Layer):
    
    def __init__(self,para):
        super().__init__(self)
        self.para=para
        self.fw_lstm1=layers.LSTM(self.para.hidden_dim, return_sequences = True, go_backwards= False, dropout = self.para.drop_rate, name = "fwd_lstm")
        self.bw_lstm1=layers.LSTM(self.para.hidden_dim, return_sequences = True, go_backwards= True, dropout = self.para.drop_rate, name = "bwd_lstm")
        self.bilstm1=layers.Bidirectional(merge_mode = "concat", layer = self.fw_lstm1, backward_layer = self.bw_lstm1, name = "bilstm")
        self.fw_lstm2=layers.LSTM(self.para.hidden_dim, return_sequences = True, go_backwards= False, dropout = self.para.drop_rate, name = "fwd_lstm")
        self.bw_lstm2=layers.LSTM(self.para.hidden_dim, return_sequences = True, go_backwards= True, dropout = self.para.drop_rate, name = "bwd_lstm")
        self.bilstm2=layers.Bidirectional(merge_mode = "concat", layer = self.fw_lstm2, backward_layer = self.bw_lstm2, name = "bilstm")
    def call(self,inputs):
        outputs=self.bilstm1(inputs,training=self.para.is_training)
        #
        return outputs
    
#自注意力机制层
class Self_Attention_Layer(layers.Layer):
    def __init__(self,para):
        super().__init__(self)
        self.para=para
        self.dense_Q=layers.Dense(self.para.attention_size,use_bias=False,trainable=True,kernel_initializer=tf.keras.initializers.GlorotNormal())
        self.dense_K=layers.Dense(self.para.attention_size,use_bias=False,trainable=True,kernel_initializer=tf.keras.initializers.GlorotNormal())
        self.dense_V=layers.Dense(self.para.attention_size,use_bias=False,trainable=True,kernel_initializer=tf.keras.initializers.GlorotNormal())
        self.dropout=layers.Dropout(self.para.attenion_drop_rate,name='attenion_drop')
        self.softmax=layers.Softmax()
        
    def call(self,inputs,sen_len):
        #就算QKV
        Q=self.dense_Q(inputs)
        K=self.dense_K(inputs)
        V=self.dense_V(inputs)
        
        #下面开始做注意力机制,如果使用mask操作,还要用到句子的长度,不使用mask操作会简单很多
        QK=tf.matmul(Q,tf.transpose(K,[0,2,1]))#现在QK的大小是[batch_size,max_sen_len,max_sen_len]
        if self.para.is_mask:
            #接下来实现带有mask操作的自注意力机制,之前尝试使用句子的长度来做mask没有弄成,现在再次尝试
            mask=tf.sequence_mask(sen_len,maxlen=self.para.max_sen_len)
            mask=tf.expand_dims(mask,1)#mask主要是将填充的地方的权值设置的非常小,这样在加权的时候就会是填充的单词起到作用了
            mask=tf.tile(mask,[1,tf.shape(QK)[1],1])#现在有了mask矩阵,下面开始将pading的单词的权重是设置的很小
            padding_val=-2**32
            QK=tf.where(mask,QK,tf.ones_like(QK)*padding_val)/tf.sqrt(tf.cast(self.para.hidden_dim,dtype=tf.float32))#采用的是缩放的点积
            QK=self.softmax(QK)
            Z=tf.matmul(QK,V)           
        else:
            #不使用mask操作,还要有个缩放因子
            QK=self.softmax(QK/tf.sqrt(self.para.hidden_dim))
            #softmax之后就是加权求和输出z,很简单的矩阵乘法
            Z=tf.matmul(QK,V)#使用这个矩阵乘法之后,默认在最后两个维度进行做乘法,也就是加权求和了
        return Z


#加性注意力机制,该注意力机制好像是首先应用在seq2seq模型里面的,需要使用到编码器和解码器这两个部分的向量,
#但是对于这个LSTM+attention+crf实现命名实体识别模型的,由于没有解码器,因为隐向量只有一部分,如何做到
#活学活用是一件挺难的事。   ,下面是我根据我自己的理解写的,可能有不对的地方。
class additive_attention_layer(layers.Layer):
    '''
    至于为什么要这么去实现加性attention机制,我也不清楚。因为深度学习本来就是解释性特别的低
    我只是在想如何计算一组权重,这种权重可以根据不同的计算方式得到,但是到底是好是坏谁也不知道,
    因而我就根据网上的一些文章,自己去尝试实现一下。对不对估计还需要大佬的指教,模型的好坏只能靠实验结果确定
    '''
    def __init__(self,para):
        super().__init__(self)
        self.para=para
        self.dense=layers.Dense(self.para.attention_size,trainable=True,activation='tanh')#这个是需要一个激活函数的
        self.dropout=layers.Dropout(rate=self.para.attenion_drop_rate)
        self.softmax=layers.Softmax()
        
    def build(self,input_shape):
        #我想使用这个权值矩阵将经过全连接层作用之后的输出的大小[batch_size,max_len,attention_size]
        #调正为[batch_size,maxlen,maxlen],就和自注意力机制层是一样的
        self.attention_u=self.add_weight(name='atten_u',shape=(self.para.attention_size,self.para.max_sen_len),initializer = tf.random_uniform_initializer(), dtype = "float32", trainable = True)
        super.build(input_shape)
    def call(self,inputs,sen_len):
        '''
        Parameters
        ----------
        inputs : tensor
            循环神经网络的输出.
        sen_len : tensor
            每个batch里面句子的长度,用来实现mask操作.

        Returns
        -------
        返回加权之后的隐向量.

        '''
        alpha=self.dense(inputs)
        if self.para.is_mask:
            alpha=tf.matmul(alpha,self.attention_u)
            mask=tf.sequence_mask(sen_len,maxlen=self.para.max_sen_len)
            mask=tf.expand_dims(mask,1)
            mask=tf.tile(mask,[1,tf.shape(alpha)[1]],1)#[batch_size,maxlen,maxlen]
            padding_val=-2**22
            alpha=tf.where(mask,alpha,tf.ones_like(alpha)*padding_val)
            alpha=self.softmax(alpha)
            Z=tf.matmul(alpha,inputs)
        else:
            alpha=tf.matmul(alpha,self.attention_u)#将alpha的大小由[batch_size,max_len,attention_size]调整为大小为[batch_size,maxlen,maxlen]
            alpha=self.softmax(alpha)
            Z=tf.matmul(alpha,inputs)#将权值与隐向量相乘做为新的隐向量
        return self.dropout(Z,training=self.para.is_training)
  
class Crf_layer(layers.Layer):
    def __init__(self,para):
        super().__init__(self)
        self.para=para
        self.dense=layers.Dense(self.para.label_nums,use_bias=False,trainable=True,kernel_initializer=tf.keras.initializers.GlorotNormal())
        
    def call(self,inputs,targets,lens):
        '''
        inputs是经过attention层之后的输出,这里还要将输入的大小[batch_size,maxlen,attention_dim]调整为
        [batch_size,maxlen,label_nums]
        '''
        out=self.dense(inputs)#调整大小为[batch_size,maxlen,nums_label]
        self.log_likelihood,self.tran_paras=tfa.text.crf_log_likelihood(out, targets, lens)
        self.batch_pred_sequence,self.batch_viterbi_score=tfa.text.crf_decode(out,self.tran_paras,lens)
        self.loss=tf.reduce_sum(-self.log_likelihood)
        
        return self.loss,self.batch_pred_sequence

3.4、创建模型训练和测试文件Model.py

# -*- coding: utf-8 -*-
"""
Created on Fri Oct 16 12:09:22 2020

@author: DELL
"""
import tensorflow as tf
import os
import numpy as np
from tensorflow.keras import layers
from tqdm import tqdm
import matplotlib.pyplot as plt

from Model_modules import RNN_layer,Self_Attention_Layer,Crf_layer
from data_utils import data_process,data_process_ds1
from Model_para import Hpara


class Model_NER(tf.keras.Model):
    def __init__(self,para):
        super().__init__(self)
        self.para=para
        self.embeddinglayer=layers.Embedding(input_dim=self.para.token_nums,output_dim=self.para.word2vector_dim,input_length=self.para.max_sen_len, name = "embeding")       
        self.rnn_layer=RNN_layer(self.para)
        self.atte_layer=Self_Attention_Layer(self.para)
        self.crf_layer=Crf_layer(self.para)
    
    def call(self,traintext,label):
        #计算一下长度
        traintext=tf.convert_to_tensor(traintext)
        label=tf.convert_to_tensor(label)
        self.lens=tf.reduce_sum(tf.sign(traintext),axis=-1)
        self.out=self.embeddinglayer(traintext)
        self.out=self.rnn_layer(self.out)
        self.out=self.atte_layer(self.out,self.lens)
        self.loss,self.batch_pred_seq=self.crf_layer(self.out,label,self.lens)
        
        return self.loss,self.batch_pred_seq
 

       
def batch_iter(x, y, batch_size = 16):
    data_len = len(x)
    num_batch = (data_len + batch_size - 1) // batch_size#获取的是
    indices = np.random.permutation(np.arange(data_len))#随机打乱下标
    x_shuff = x[indices]
    y_shuff = y[indices]#打乱数据
    
    for i in range(num_batch):#按照batchsize取数据
        start_offset = i*batch_size #开始下标
        end_offset = min(start_offset + batch_size, data_len)#一个batch的结束下标
        yield i, num_batch, x_shuff[start_offset:end_offset], y_shuff[start_offset:end_offset]#yield是产生第i个batch,输出总的batch数,以及每个batch的训练数据和标签       
       
def train_and_test(para):
    model=Model_NER(para)
    if not os.path.exists(para.savepath):
        print('make model savepath----')
        os.makedirs(para.savepath)
    else:
        print('load model weight----')
        model.load_weights(os.path.join(para.savepath,'ckpt'))
    
    optimizer=tf.keras.optimizers.RMSprop(lr=para.learning_rate,rho=0.9, epsilon=1e-06)
    
    def train_step(x,y):
        with tf.GradientTape() as tape:
            loss,batch_pred_seq=model(x,y)
        gradients=tape.gradient(loss,model.trainable_variables)
        optimizer.apply_gradients(zip(gradients,model.trainable_variables))
        return loss,batch_pred_seq
    
    #加载数据集
    train_arr,train_l,test_arr,test_l=data_process_ds1(para)
    for e in tqdm(range(para.epochs)):
        loss_epoch=0
        for i,num_batch,x,y in tqdm(batch_iter(train_arr, train_l,para.batch_size)):
            loss_step,pred_step=train_step(x,y)
            loss_epoch+=loss_step
            if (i+1) % 5==0:
                print('\n第 %d epoch 的第%d步的损失是%f\n'%(e,i+1,loss_step))
        print('\n第 %d 个epoch的平均损失是%f\n'%(e+1,loss_epoch/num_batch))
    #保存模型
    model.save_weights(os.path.join(para.savepath,'ckpt'))
    #下面对模型进行测试
    pred_loss=[]
    for i,num_batch,test_x,test_y in tqdm(batch_iter(test_arr, test_l)):
        loss,pred=model(test_x,test_y)
        pred_loss.append(loss)
    plt.plot(pred_loss)
    plt.xlabel('steps')
    plt.ylabel('loss')
        
if __name__=='__main__':
    hp=Hpara()
    parser = hp.parser
    para = parser.parse_args(args=[])
    train_and_test(para)

3.5、全部代码

链接:https://pan.baidu.com/s/1cKMJnq4_CIoDJM6XMnTSPQ
提取码:a0l7
复制这段内容后打开百度网盘手机App,操作更方便哦

  • 9
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值