<< Attention is all you need >>阅读笔记
Transformer模型打破了传统的RNN的时序限制,引入自注意力机制,可以更好的对句子进行编码,关于模型的理论部分已经有很多介绍了,本文主要介绍transformer的各个部分在实现过程中遇到的问题,并展示具体的实现代码。Transformer模型的结构如下图所示:
实验环境:
- tensorflow == 1.13
- python == 3.6
实验细节:
1.输入层编码
-
无论是编码器还是解码器,输入层编码=word embedding + position embedding
-
word embedding可以是预训练好的词向量,也可以是随机初始化的向量
-
position embedding是对应的位置编码,transformer模型打破了时序的限制,
可以进行并行计算,这样虽然提升了训练速度,但是如果仅利用词向量进行编码,不同位置的相同词的编码是一样,显然这样做是不合理的,因为在文本中不同位置的词往往对语义有不同的影响。因此,在该模型中引入了位置编码。 -
位置编码的具体实现:
def get_position(seq_length, dim):
"""pe是一个seq_length*dim的矩阵,表明句子中每个单词每个维度的位置编码
位置编码在句子中每个位置每个维度的编码一致
pe是一个句子的位置编码"""
pe = np.zeros((seq_length, dim))
for i in range(seq_length):
for j in range(dim):
if j % 2 == 0:
pe[i][j] = math.sin(i/(10000**(j/dim)))
else:
pe[i][j] = math.cos(i/(10000**((j-1)/dim)))
tf.logging.info("*********************the position matrix have been created!!")
tf.logging.info(pe)
return pe
2.多头注意力层
- 注意力机制认为句子中每个单词对句子的编码的贡献(权重)不同,利用
每个句子中每个单词之间的相似性作为权重,进行加权平均。 - 具体实现过程:
- 利用输入层的输入生成q,k,v(相当于添加一个全连接层)
- 划分为多层注意力(增加不同语义空间)
- 考虑padding那部分的权重影响,padding主要包括输入本身mask的部分和句子mask的部分(解码器理论上t时刻的输出只能看到t时刻以前的结果,因此需要将t时刻以后的词语mask)
- transformer模型中主要包括三种注意力:
- 编码器-编码器自注意力: 只需要考虑句子本身mask的那部分
- 编码器-解码器注意力:需要考虑句子本身mask的部分和解码器中t时刻以后的部分
- 解码器-解码器注意力:只需要考虑句子本身的mask部分
- mask的技巧:利用一个特别小的数代替该位置的数值,这样在进行归一(softmax)的时候,得到的权重几乎趋近于0.
- 具体实现过程:
def multi_attention(inputs, masks, num_head, type, encode_output=None):
"""
inputs: [batch_size,seq_length,num_units]
masks: [batch_size,seq_length]
num_head: the number of head in model'
type:encode,decode,encode-decode三种注意力类型
return: [batch_size,seq_length,num_units]
"""
min_num = -2**32 + 1
emb_dim = inputs.get_shape().as_list()
print("*********emb_dim: ", emb_dim)
# batch_size = emb_dim[0]
seq_length = emb_dim[1]
dim = emb_dim[2]
if dim % num_head:
raise ValueError('please make the dim/d_head is the int!! ')
# 1.初始化q,k,v矩阵,相当于添加一个全连接层,将输入的维度由[batch_size,seq_length,dim]变为[batch_size,seq_length,dim//num_head]
q = tf.layers.dense(inputs, units=dim) # 全连接层,权重共享,对每个单词进行全连接,实现时所有位置单词是并行的
k = tf.layers.dense(inputs, units=dim)
v = tf.layers.dense(inputs, units=dim)
tf.logging.info(q)
# 只有编码器-解码器的权重的查询值与key,value不一样
if type == 'encode-decode':
k = encode_output # 此时k和v是encode的输出,q是decode的输入
v = encode_output
# 切分为多头,q_:[batch_size*num_head,seq_length,dim/num_head]
q_ = tf.concat(tf.split(q, num_head, axis=2), axis=0)
k_ = tf.concat(tf.split(k, num_head, axis=2), axis=0)
v_ = tf.concat(tf.split(v, num_head, axis=2), axis=0)
tf.logging.info(q_)
# scale q_
depth = dim // num_head
q_ *= (depth ** -0.5)
# 计算q与k的相似度,sim:[batch_size*num_head,seq_length,seq_length]
sim = tf.matmul(q_, k_, transpose_b=True)
sim = sim / (k_.get_shape().as_list()[-1]**0.5)
# 利用mask乘以一个特别小的数,避免填补的值对权重的影响。mask有padding mask和seq mask两种
# 1.padding mask是句子长度不到固定长度的填补标记
masks = tf.to_float(masks)
pad_mask = tf.tile(tf.expand_dims(masks, axis=1), [1, seq_length, 1]) # [batch_size, seq_length, seq_length]
multi_pad_mask = tf.tile(pad_mask, [num_head, 1, 1]) # [batch_size*num_head, seq_length, seq_length]
tf.logging.info(multi_pad_mask)
# 2.seq mask是解码器中需要把t时刻后的单词进行覆盖,生成一个下三角
seq_mask = tf.ones_like(masks, dtype=tf.float32)
seq_mask = tf.linalg.LinearOperatorLowerTriangular(seq_mask).to_dense() # 将张量转换为下三角矩阵
multi_seq_mask = tf.tile(tf.expand_dims(seq_mask, axis=1), [1, seq_length, 1])
multi_seq_mask = tf.tile(multi_seq_mask, [num_head, 1, 1])
# encode只需要padding mask, decode需要padding mask 和 seq
padding = tf.ones_like(sim) * min_num # 创造一个与sim具有相同形状的张量,元素全为最小数
if type == 'encode' or type == 'encode-decode':
sim = tf.where(tf.equal(multi_pad_mask, True), sim, padding) # 如果为真,就保留权重,否则利用最小数代替
elif type == 'decode':
decode_mask = multi_pad_mask * multi_seq_mask
sim = tf.where(tf.equal(decode_mask, True), sim, padding)
else:
raise ValueError("please input encode, decode or encode-decode!!!")
# 归一化,确保权重之和为1
sim = tf.nn.softmax(sim)
# dropout, 添加
if mode == 'train':
sim = tf.layers.dropout(sim, rate=0.3, training=True)
# 输出output
output = tf.matmul(sim, v_) # 输出output:[batch_size*num_head,seq_length,dim//num_head]
output = tf.concat(tf.split(output, num_head, axis=0), axis=2) # 输出output:[batch_size,seq_length,dim]
# linear, 多头注意层的最后输出需要添加一个线性变换
output = tf.layers.dense(output, units=dim, use_bias=False)
return output
3.layer norm
-
layer norm:固定一层神经元的均值和方差
-
为什么要进行layer norm?
神经网络的权值高度依赖于前一层的输出,当这些输出高度相关时,难以快速收敛。
-
计算公式:
- eplsion = 1e-8
- norm = (input - mean) / ((var + eplsion)**0.5)
- output = alpha * norm + beta
-
具体实现过程:
def layer_norm(inputs, eplsion=1e-8):
"""
layer norm
inputs: [batch_size,seq_length,num_units]
eplsion:防止分母为0
return: [batch_size,seq_length,num_units]
"""
para = inputs.get_shape().as_list()[-1]
alpha = tf.Variable(tf.ones(para))
beta = tf.Variable(tf.zeros(para))
mean, var = tf.nn.moments(inputs, axes=[-1], keep_dims=True)
norm = (inputs - mean) / ((var + eplsion) ** 0.5)
# 数组中*是按照对应位置相乘的
output = alpha * norm + beta # 相当于alpha乘以[batch_size, seq_length], 然后乘以norm:[batch_size,seq_length,dim]
return output
4.残差连接
- 计算公式:output = input + output(innput)
- 确保反向传播求偏导时,梯度至少为1
5.全连接层(FFN)
- FFN层相当于一个Relu激活层和一个线性激活层或两个大小为1的一维卷积层
- 计算公式: FFN(x) = w_2*(max(0, w_1*x+b_1)) + b_2
- 具体实现过程:
def forward_feed(inputs, num_units_list):
"""前馈全连接层FFN(x) = w_2*(max(0, w_1*x+b_1)) + b_2
相当于两个大小为1的一维卷积层,二者选择一种方式实现即可
inputs = word Embedding + position Embedding , [batch_size,seq_length,word_dim]
num_units_list: contains two int number ,for example: [2048, 512]
return: [batch_size,seq_length,word_dim]
"""
# 1.全连接层实现
inner = tf.layers.dense(inputs, units=num_units_list[0], activation=tf.nn.relu)
output = tf.layers.dense(inner, units=num_units_list[1]) # 没有激活函数就是线性激活函数层
# 2.卷积层实现
# 相当于 [batch_size*sequence_length,num_units]*[num_units,ffn_dim],在reshape成[batch_size,sequence_length,num_units]
# paras = {"inputs": inputs, "kernel_size": 1, "filter": num_units_list[0], "activation": tf.nn.relu}
# inner = tf.layers.conv1d(**paras)
# paras = {"inputs": inner, "kernel_size": 1, "filter": num_units_list[1], "activation": tf.nn.relu}
# output = tf.layers.conv1d(**output)
return output
6.标签平滑
- 未经过平滑的标签在进行训练时,容易过拟合,为了增强模型的泛化能力,对标签进行平滑处理
- 计算公式:output = (1-alpha) * input + alpha * (1/k),alpha一般取0.1,k表示类别的数目
- 具体实现过程:
def label_smoothing(inputs, epsilon=0.1):
"""
标签平滑
"""
v = inputs.get_shape().as_list()[-1]
output = (1-epsilon)*inputs + epsilon / v
return output
常用函数
- tf.tile(): 复制张量
- tf.equal(): 逐个元素判断是否相等
- tf.where(condition, x, y): condition是bool类型,与x,y具有相同的形状。
如果condition中元素为True,利用x中对应位置的元素替换,否则,利用y中对应位置的元素替换 - tf.expand_dims: 扩展张量维度,把(2,3,4)扩展为(2,1,3,4)
- tf.shape():获取变量的维度信息,(包括变量的维度为None)
参考链接
阅读transformer论文时借鉴了许多大神的博客,这里就不一一列举了。