Python和TensorFlow2实现ELMO(Embedding From Language Model)模型,并对源码做了一些改进

一、ELMO模型简介

1.1、模型概要

该模型主要是结合了字符卷积神经网络和双向LSTM网络。其中字符卷积网络是生成上下文无关的词向量表示,接着将该字符卷积神经网络的输出大小调整的LSTM需要的大小512(论文里面是这个)。再利用LSTM结构提取上下文相关的词向量表示。

在这里我想要介绍下这个完整的模型,花了我很多时间,看了无数博客和文章以及近2000行的论文源码才把这个模型彻底搞清楚。啊哈哈哈,也不能说彻底吧,我自己的理解肯定是有限的。希望各位能批评指正,大家一起进步

1.2 、字符卷积模块

卷积层的构成:

filters=[ [1, 32],[2, 32], [3, 64], [4, 128], [5, 256], [6, 512], [7, 512] ]

对这个filters二维列表里面的每个元素,比如[1,32],将使用大小为[1,1,1,32]的卷积核大小对输入大小为[batch_size,unroll_steps,max_word_len,char_vector_dim]的输入数据进行卷积,卷积核的第二个位置均为1,因为我们不对时间步维度进行卷积,如果这样,会造成单词的数量减少。
再比如对于filters列表的第四个元素[4,128],将生成一个大小为[1,1,4,128]的卷积核对输入数据进行卷积。

最重要的一点是这些卷积层都是并行的,不是串联。卷积层的输入数据都是同样的[batch_size,unroll_steps,max_word_len,char_vector_dim],不是将一层的卷积输出作为下一层的卷积输入。

对输入数据使用不同的卷积层作用之后,接着进行最大池化,池化之后的输出数据大小是[batch_size,unroll_steps,out_channel],这里的out_channel的取值就是上面的filters的32,32,64,128,256,512,512.因此不同的卷积和池化之后并行输出为[batch_size,unroll_steps,32],[batch_size,unroll_steps,32],[batch_size,unroll_steps,64],[batch_size,unroll_steps,128],[batch_size,unroll_steps,256],[batch_size,unroll_steps,512],[batch_size,unroll_steps,512]大小的数据。

接着将这不同大小的数据在第二个维度进行拼接生成大小为[batch_size,unroll_steps,32+32+64+128+256+512+512]的数据。

1.3 highway net高速公路层

这个不做介绍啦,很简单的,在网上看到说这个是残差连接的推广版,而且是比resnet优先发表的论文,但是效果好像没有残差连接效果好。具体我也没有深究,别的大佬这样说的,暂时先这样接受吧,以后再看。

1.4 Projection Layer投影层

由上面可以看出,卷积池化输出的数据大小为[batch_size,unroll_steps,1536],因为32+32+64+128+256+512+512=1536. 啊哈哈哈
那么就需要经过该层将数据大小调整为双向LSTM要求的大小[batch_size,unroll_steps,512].我是就是使用了一个Dense层来直接调整的。

1.5 LSTM模型

不想做过多介绍 看图
Python和TensorFlow2实现ELMO(Embedding From Language Model)模型,并对源码做了一些改进_第1张图片
该模型使用输入预测下一个单词。不如这句话:今天是国庆节和中秋节。我们可以使用“今天是国庆节”预测“天是国庆节和”,使用“天是国庆节和”预测“是国庆节和中”,使用“是国庆节和中”预测“国庆节和中秋”。

二、ELMO代码(代码我都加了注释)

首先是数据处理模块,没有源码处理的那么复杂,也是结合一点我自己的理解吧,有错误欢迎指正。

2.1、创建py文件ELMO_para.py

该文件主要用来存储模型的参数

import argparse

class Hpara():
    parser = argparse.ArgumentParser()#构建一个参数管理对象
    filters=[
            [1, 32],
            [2, 32],
            [3, 64],
            [4, 128],
            [5, 256],
            [6, 512],
            [7, 512]
        ]
    
    nums=0
    for i in range(len(filters)):
        nums+=filters[i][1]
    
    parser.add_argument('--datapath',default='./data/test.txt',type=str)   
    parser.add_argument('--filters',default=filters,type=list)
    parser.add_argument('--n_filters',default=nums,type=int)
    parser.add_argument('--n_highway_layers',default=2,type=int)
    parser.add_argument('--model_dim',default=512,type=int)
    parser.add_argument('--max_sen_len',default=8,type=int)
    parser.add_argument('--max_word_len',default=50,type=int)
    parser.add_argument('--char_embedding_len',default=16,type=int)
    parser.add_argument('--drop_rate',default=0.2,type=float)
    parser.add_argument('--learning_rate',default=0.02,type=float)
    parser.add_argument('--vocab_size',default=74,type=int)
    parser.add_argument('--batch_size',default=2,type=int)
    parser.add_argument('--char_nums',default=259,type=int)
    parser.add_argument('--epochs',default=1,type=int)

2.2 创建py文件data_processing_modules.py

from tensorflow import keras 
import numpy as np

def Create_word_ids(datapath,sen_max_len,n):  #n是要控制循环的次数,来生成训练数据train_data和语言模型的标签target
    '''
    Parameters
    ----------
    datapath : str
        存储数据的路径.
    sen_max_len : int
        训练数据的长度.
    vocab_size: int
        词典大小
    n: int
        复制多少次训练数据

    Returns
    -------
    词典,训练数据,训练数据对应的标签target.
    '''

    f=open(datapath,'r',encoding='utf-8')
    lines=f.readlines()
    lines=[line.strip() for line in lines]#去除每行的换行符
    t = keras.preprocessing.text.Tokenizer()
    t.fit_on_texts(lines)
    word_index=t.word_index#生成字典
    l=len(word_index)
    #向字典里面添加特殊字符,这里只添加了一个特殊字符,因为我在数据集里面已经添加了句子的开始和结束特殊字符
    word_index['']=l+1
    
    
    whole_sens=' '.join(lines)
    whole_sens=whole_sens.split(' ')
    len_whole_sens=len(whole_sens)
    #构造训练数据和标签
    train_data=[]
    target=[]
    
    for i in range(len_whole_sens-sen_max_len):
        train_data.append(' '.join(whole_sens[i:i+sen_max_len]))
        target.append(' '.join(whole_sens[i+1:sen_max_len+i+1]))#将数据后移一位,构造标签,这个模型使用一个文本,然后预测下一个单词
        #比如 对于 ‘我今天吃了一个苹果’  可以使用‘我今天’作为一个训练数据,预测‘今天吃’。使用‘今天吃’预测‘天吃了’ 等等,上面这个循环就实现了这个
        
    #下面将训练数据复制n次
    train_data=train_data*n
    target=target*n
    
    #下面将句子都转化为对应id的形式
    train_data=t.texts_to_sequences(train_data)
    target=t.texts_to_sequences(target)
    train_data=keras.preprocessing.sequence.pad_sequences(train_data,maxlen=sen_max_len,padding='post')
    target=keras.preprocessing.sequence.pad_sequences(target,maxlen=sen_max_len,padding='post')
    return word_index,train_data,target


#上面已经完成将word转化为id的程序,接下面将单词转化为字符的utf-8编码的id
def Create_char_id_embedding(word_index,max_word_length):
    '''
    
    Parameters
    ----------
    word_index : dict
        词典,是单词和id 的对应关系.
    max_word_length : int
        因为单词的长度不一致,因而我们希望传入一个整数,来控制单词的长度.
    Returns
    -------
    一个二维矩阵,类似嵌入矩阵,可以将单词转化为对应的utf-8编码.
    
    '''
    
    bow=256 #单词的起始id  begin of word
    eow=257 #单词的结束id  end of word
    padding=258 #将单词转化为utf-8(0-255)编码的时候,不能使用0填充,因为0也是字符的ascii码

    bos=259 #句子的开始id  begin of sentence
    eos=260 #句子的结束id  end of sentence
    
    dict_len=len(word_index)+1#字典里面单词的个数
    word_embedding=np.ones([dict_len,max_word_length])*padding#都先初始化为填充的值
    #下面开始根据字典构造char_embedding矩阵
    for word,id in word_index.items():
        l=len(word)
        word=word.encode('utf-8','ignore')
        word_embedding[id][0]=bow
        for i in range(1,l+1):
            word_embedding[id][i]=word[i-1]
        word_embedding[id][l+1]=eow
        
    return word_embedding
       
def Create_char_Vector(dim):
    '''
    随机生成一个每个字符的vector  比如 a--->[22,55,....],根据上面那个方法,这里其实是
    a对应的ascii码97转化为[22,55,....],输入一个batch的句子,最终生成的数据是[batch_size,time_steps,max_word_len,max_char_vector_len]
    ,然后对这个四维数据进行卷积操作之后调整为LSTM需要数据维度大小[batch_size,time_steps,dim]
    

    Parameters
    ----------
    dim : int
        生成字符嵌入的维度.

    Returns
    -------
    一个大小为259*dim的矩阵.
    259是因为utf-8编码有256位字符因为是8位2进制,再加上bow,eow和padding,所以总共259个
    这是我根据我自己理解弄的,可能和别的代码不太一样

    '''
    return np.random.normal(0,1,size=[259,dim])

2.3、创建py文件Model_modules.py

import tensorflow as tf
from tensorflow.keras import layers

class Highway_layers(layers.Layer):
    '''
    构造ELMO模型里面的高速公路层
    filters': [
            [1, 32],
            [2, 32],
            [3, 64],
            [4, 128],
            [5, 256],
            [6, 512],
            [7, 512]
        ]
    
    '''
    def __init__(self,n_filters):
        super().__init__(self)
        self.carrygate_dense=layers.Dense(n_filters,activation='sigmoid')
        self.transform_gate_dense=layers.Dense(n_filters, activation='relu')
        
    def call(self,inputs):
        '''
        我看网上是这个是残差连接的一般形式,但是却没有残差连接有效
        '''
        carrygate=self.carrygate_dense(inputs)
        transformgate=self.transform_gate_dense(inputs)
        
        return carrygate*transformgate+(1.0-carrygate)*inputs
    
#下面是投影层
class ProjectionLayer(layers.Layer):
    '''
    将数据输出为LSTM要求的大小,最终是[batch_size,time_steps,dim=512]
    '''
    
    def __init__(self,lstm_dim=512):
        super().__init__(self)
        self.dense=layers.Dense(lstm_dim,activation='relu')
        
    def call(self,inputs):
        return self.dense(inputs)
    
    
class All_Con_MP_Layers(layers.Layer):
    '''
    该类主要用来做卷积和最大池化操作,并且将七个卷积层经过池化后的输出在最后一个维度拼接起来,最终的输出的大小是
    [batchsize,time_steps,32+32+64+128+256+512+512]的矩阵,然后经过高速公路层和投影层,将矩阵的大小调整为LSTM的
    需求的大小,其实也就是为每个单词生成了一个维度为512的嵌入表示,不过这个嵌入表示是上下文无关的,然后输入给双向LSTM,
    生成上下文相关的词向量
    '''
    
    def __init__(self,filters):
        super().__init__(self)
        
        self.ConvLayers=[layers.Conv2D(num,kernel_size=[1,width]) for i,(width, num) in enumerate(filters)]
        self.MaxPoolLayers=[layers.MaxPool2D(pool_size=(1,50-width+1),strides=(1, 1), padding='valid') for i,(width,num) in enumerate(filters)]
        
    def call(self,inputs):
        conout=[conlayer(inputs) for conlayer in self.ConvLayers]
        mpout=[]
        for i in range(len(conout)):
            mpout.append(tf.squeeze(self.MaxPoolLayers[i](conout[i]),axis=2))#使用maxpooling作用并且在第三个维度也就是axis=2压缩张量,经过池化之后的第二个维度的大小是1
            
        #下面在axis=2粘接张量
        out=mpout[0]
        for i in range(1,len(mpout)):
            out=tf.concat([out,mpout[i]], axis=2)
        return out


class LSTM_Layers(layers.Layer):
    '''
    该类主要用来实现双向LSTM层,并且定义三个参数来将不同的LSTM层输出的隐向量结合起来
    论文中的是直接定义了一个维度为3的隐含层权值,我觉得这样是不合理的,我认为应该是权值应该是随
    不同的句子而发生变化的,因而我这里这定义了一个Dense layer,激活函数使用softmax来输出一个[batch_size,time_steps,3]
    这样做的目的就是输出的权值可以根据不同的句子发生变化。
    '''
    def __init__(self,dim,drop_rate,vocab_size):
        super().__init__(self)
        #下面定义所需要的LSTM层
        self.Lstm_fw_layers1=layers.LSTM(dim,return_sequences=True,go_backwards= False, dropout = drop_rate)
        self.Lstm_bw_layers1=layers.LSTM(dim,return_sequences=True,go_backwards= True, dropout = drop_rate)
        self.Lstm_fw_layers2=layers.LSTM(dim,return_sequences=True,go_backwards= False, dropout = drop_rate)
        self.Lstm_bw_layers2=layers.LSTM(dim,return_sequences=True,go_backwards= True, dropout = drop_rate)
        self.layers_weights=layers.Dense(3, activation='softmax')
        self.outlayer=layers.Dense(vocab_size+1,activation='softmax')
        
    def call(self,inputs):
        self.bilstm1=layers.Bidirectional(merge_mode = "sum", layer =self.Lstm_fw_layers1, backward_layer =self.Lstm_bw_layers1)
        self.bilstm2=layers.Bidirectional(merge_mode = "sum", layer =self.Lstm_fw_layers2, backward_layer =self.Lstm_bw_layers2)
        
        h1=self.bilstm1(inputs)
        h2=self.bilstm2(h1)
        
        #下面计算权重,在这里我选择了将两个隐层和一个输入inputs相加在输入进dense层来计算各层每个隐层和输入的权重
        w=self.layers_weights(inputs+h1+h2)
        w=tf.expand_dims(w, axis=2)
        out=tf.concat([tf.expand_dims(inputs, axis=2),tf.expand_dims(h1, axis=2),tf.expand_dims(h2, axis=2)],axis=2)
        out=tf.squeeze(tf.matmul(w,out),axis=2) 
        
        out=self.outlayer(out)
        
        return out

2.4、创建py文件ELMO_Model.py

import tensorflow as tf
from tensorflow.keras import layers
from Model_modules import Highway_layers,ProjectionLayer,All_Con_MP_Layers,LSTM_Layers

class ELMO(tf.keras.Model):
    def __init__(self,para,word_to_char_ids_matrix,char_ids_to_vector_matrix):
        '''
        该类来搭建完整的ELMO
        Parameters
        ----------
        para: 一个参数收纳器,用来存储下面的参数
        
        n_highway_layers : int
            进行多少次高速公路层.
        n_filters : int
            所有卷积输出通道数加起来.
        model_dim : int
            输入进LSTM的词向量的维度大小.
        filters : 2d-list
            存储卷积的核大小和输出的通道数.
        drop_rate : float
            丢弃率.
        vocab_size : int
            字典大小.

        Returns
        -------
        [batch_size,max_sen_len,vocab_size+1]是预测的每个词的概率.

        '''
        super().__init__(self)
        #将word转化为字符编码
        self.word_embedding=layers.Embedding(input_dim=para.vocab_size+1, output_dim=para.max_word_len, input_length=para.max_sen_len, weights=[word_to_char_ids_matrix],trainable=False)
        #下面这个嵌入矩阵是将字符id表示为嵌入向量,是可以训练的,因为我是随机初始化的
        self.char_embedding=layers.Embedding(input_dim=para.char_nums, output_dim=para.char_embedding_len, input_length=para.max_word_len,weights=[char_ids_to_vector_matrix],trainable=True)
        
        self.HighWayLayers=[Highway_layers(para.n_filters) for i in range(para.n_highway_layers)]
        self.Projection=ProjectionLayer(para.model_dim)
        self.con=All_Con_MP_Layers(para.filters)
        self.lstm=LSTM_Layers(para.model_dim,para.drop_rate,para.vocab_size)
        
    def call(self,inputs):
        
        out=self.word_embedding(inputs)
        out=self.char_embedding(out)
        out=self.con(out)
        for i in range(len(self.HighWayLayers)):
            out=self.HighWayLayers[i](out)
        out=self.Projection(out)
        out=self.lstm(out)
        
        return out

2.5、创建py文件Train.py

from ELMO_para import Hpara
import numpy as np
hp=Hpara()
parser = hp.parser
para = parser.parse_args()
import tensorflow as tf

from data_processing_modules import Create_word_ids,Create_char_id_embedding,Create_char_Vector
from ELMO_Model import ELMO

def Create_whole_model_and_train(para):
    
    wordindex,traindata,target=Create_word_ids(para.datapath,para.max_sen_len,2)
    word_embedding=Create_char_id_embedding(wordindex,para.max_word_len)
    char_embedding=Create_char_Vector(para.char_embedding_len)
    model=ELMO(para,word_embedding,char_embedding) 
    optimizer = tf.keras.optimizers.Adam(0.01)#优化器adam
    loss_fn = tf.keras.losses.SparseCategoricalCrossentropy() #求损失的方法
    accuracy_metric = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')#准确率指标
    
    def batch_iter(x, y, batch_size = 2):#这个函数可以好好看看,确实不错的
        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_step(input_x, input_y):#训练一步
    
        with tf.GradientTape() as tape:
            raw_prob = model(input_x)#输出的是模型的预测值,调用了model类的call方法,输入的每个标签的概率,过了softmax函数
            #tf.print("raw_prob", raw_prob)
            pred_loss = loss_fn(input_y, raw_prob)#计算预测损失函数
      
        gradients = tape.gradient(pred_loss, model.trainable_variables)#对损失函数以及可以训练的参数进行跟新
        optimizer.apply_gradients(zip(gradients, model.trainable_variables))#应用梯度,这里会可以更新的参数应用梯度,进行参数更新
        # Update the metrics
        accuracy_metric.update_state(input_y, raw_prob)#计算准确率
        return raw_prob
    for i in range(para.epochs):
         batch_train = batch_iter(traindata,target, batch_size = para.batch_size)
         accuracy_metric.reset_states()
         for batch_no, batch_tot, data_x, data_y in batch_train:#第几个batch,总的batch,以及训练数据和标签
             predict_prob = train_step(data_x, data_y)  #对数据集分好batch之后,进行一部训练
    
    
if __name__=='__main__':
    Create_whole_model_and_train(para)

上述代码还有很多不完整之处,比如测试,评估,模型保存与加载都没写,用的数据集也很小,我的电脑实在是扛不住,望大家理解。穷人不配深度学习。

三、改进之处

上面的代码我已经对源码做了改进,我看源码里面是在将LSTM隐含层的加权输出作为词向量时,只是简单设置了三个参数用来训练,我认为这里应该权重是和不同的句子相关的,于是我将权重设置为inputs的函数,经过softmax输出权值,这会随不同的句子输入而改变LSTM隐含层的权值大小。当然这个改进完全可能来自我对该模型的不熟悉之处,如果有大佬知道,十分欢迎批评指正,万分感谢。

四、一个小疑问

在看很多文章的时候,看到很多人都在问,既然这个词向量是动态的,比如apple的词嵌入,在不同句子里面是不一样的,那么,我将该模型用于下游任务时,该使用哪个词嵌入呢??
其实我觉得应该是这样理解:当用于下游任务,一个单词的嵌入表示是和你当前输入的句子是有关的,句子的不同,会影响句法和语义的不同。这就会造成同一个单词的嵌入表示不同。比如‘i want to eat an apple’和‘apple is reall delicious’这两句话,语义和语法都不同,那么生成的apple的词嵌入也是不一样的,底层的LSTM会捕捉句法信息,高层的LSTM会捕捉语义信息。

五、参考文献

https://arxiv.org/pdf/1802.05365.pdf
https://github.com/horizonheart/ELMO Elmo的注释版本
https://arxiv.org/abs/1509.01626
https://github.com/horizonheart/ELMO
https://blog.csdn.net/liuchonge/article/details/70947995
https://www.zhihu.com/question/279426970/answer/614880515
https://zhuanlan.zhihu.com/p/51679783
https://blog.csdn.net/linchuhai/article/details/97170541
https://blog.csdn.net/jeryjeryjery/article/details/80839291
https://blog.csdn.net/jeryjeryjery/article/details/81183433
https://blog.csdn.net/weixin_44081621/article/details/86649821
https://jozeelin.github.io/2019/07/25/ELMo/
https://www.cnblogs.com/jiangxinyang/p/10235054.html

最后祝大家中秋节和国庆节快乐,也祝福天津大学125周年啦,有幸成为天大人,希望越来越好。大家也加油!!!!

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

你可能感兴趣的:(笔记,自然语言处理,tensorflow,深度学习)