一、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模型
不想做过多介绍 看图
该模型使用输入预测下一个单词。不如这句话:今天是国庆节和中秋节。我们可以使用“今天是国庆节”预测“天是国庆节和”,使用“天是国庆节和”预测“是国庆节和中”,使用“是国庆节和中”预测“国庆节和中秋”。
二、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['<unk>']=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,操作更方便哦