1.残差
随着网络变深,会出现梯度消散或者梯度爆炸问题,这个可以通过下面的BN和LN解决,将激活函数换为ReLu,使用Xaiver初始化等解决。
但增加深度的另一个问题就是网络的退化问题(degradation),即随着网络的怎加,网络性能会越来越差,直接表现在训练集上的准确率饱和甚至下降。
做法:
网络的每一层是为了学习一个恒等映射网络,如果直接去拟合一个恒等映射函数,比较困难。但是,把网络设计成,这就转化成了要学习一个残差函数的问题,这样,只要,拟合残差函数更加容易。
残差有效的解释:
简要来讲,残差使得映射对输入的变化更加敏感.
2.Layer normalization
Batch Normalization
- 2.1 用BN的原因
说起normalizaiton,需要先说起Batch Normalization。Batch Normalization的提出主要是为了解决深度神经网络训练过程中的Internal Covariate Shift现象,即在深度神经网络,随着网络深度加深,训练就越困难,收敛越慢。在前一层参数变化时,每层的输入分布也随之变化,进而上层的网络需要不停地去适应这些分布变化,使得我们的模型训练变得困难。
- 2.2 BN实现方法
所以,提出白化的方案,白化在机器学习中通常用来规范数据分布。它的作用是:
- 使得输入特征分布具有相同的均值与方差。
其中PCA白化保证所有特征分布均值为0方差为1。
2. 去除特征之间的相关性。
但是白化具有两个问题:
- 计算复杂度比较高。
- 白化过程改变了网络每一层的分布。这样就无法学习到数据原始的分布信息了。
为了解决上面两个问题,提出了Batch Normalization:
在batch数据的每个特征进行独立的规范化:
上图公式是计算第维特征(也就是第j个神经元结点)计算的结果。
同时,BN中引入两个可学习的参数和,这两个参数的引用是为了恢复数据本身的数据表达,对规范后的数据进行线性映射,
通过上述定义,使得:
- 即用更加简化的方式来对数据进行规范化,使得第 层的输入每个特征的分布均值为0,方差为1
- 在一定程度上保证了输入数据的表达能力。
- 2.3 引入Batch Normalization带来的好处:
- BN使得深度网络中每层的输入数据的分布更加稳定,加快模型收敛速度。
- BN使得模型对网络中的参数不那么敏感,简化调参过程,使得网络学习更加稳定。
- BN允许网络使用饱和性激活函数(入sigmoid,tanh等),避免输入落到梯度非饱和区(导数近似0),缓解梯度消失问题。
- BN具有一定的正则化效果。
- 2.4 BN存在的问题
- 每次在batch_size上计算均值、方差,如果batch_size过小,则计算的均值和方差不足以代表整个数据分布。
- 如果batch_size太大,会超出内存容量。需要跑更多的epoch,导致总训练时间变长;会直接固定梯度下降的方向,导致很难更新。
- 2.5 在测试阶段怎么进行Batch Normalization?
利用BN训练好模型后,我们保留每一个batch的训练数据在网络中每一层的均值和方差:,。在测试阶段,我们使用整个样本的统计量对测试数据进行归一化,具体来说使用均值与方差的无偏估计:
Layer normalization
针对BN不适用于深度不固定的网络(sequence长度不一致,如RNN),LN对深度网络的某一层的所有神经元的输入做normalization操作。
假设输入tensor的shape为:[m,H,W,C],其中C表示通道channel,对于每个通道:
然后对于每个通道,引入和,保持数据原始特征:
LN中同层神经元的输入拥有相同的均值和方差,不同的输入样本有不同的均值和方差。
LN的一个优势是不需要批训练,在单条数据内部就能归一化。
下面这个图可以形象的体现BN和LN的区别:
BN是在一个batch上计算均值和方差。
而LN是在每个样本上计算均值和方差。
transformer 为什么使用 layer normalization,而不是其他的归一化方法?
深度学习中正则化作用:”通过把一部分不重要的复杂信息损失掉,以此降低拟合难度和过拟合的风险,从而加速模型收敛“。Normalization就是让分布稳定下来,即降低各数据维度的方差。
不同正则化方法的区别只是操作维度的不同,即选择损失信息的维度不同。
选择什么样的归一化方法,取决于你关注哪部分信息。如果某个维度的信息差异性很重要,需要被拟合,那就不要在那个维度进行归一化。
所以,在NLP任务中,不同batch样本的信息关联度不大,而且由于不同的句子长度不同,强行在batch维度做归一化,会损失不同样本之间的信息差异性。所以,不用BN而用LN,只在句子内部维度上的做归一化。可以认为,在NLP任务中,一个样本内部维度之间时关联的,所以,在信息归一化时,对样本内部差异进行一些损失,反而能降低方差。
3.Mask
Mask表示掩码,是对某些值或者位置进行掩盖,是其在参数更新时不产生效果。Transformer中有两处mask,padding mask和sequence mask。
padding mask
因为模型的输入需要固定维度,所以对于一些较短的句子,需要对齐,通常是在序列后面填充0。但是,这些填充的位置,实际上是没有意义的,在做attention时不应该考虑这些位置。
所以,具体的做法,在这些位置上加上一个非常大的负数,这样,经过softmax,这些位置的概率就接近0了。
代码层面,是通过一个张量(pad_mask)来决定(值为false)那个位置需要mask掉。
sequence mask
decoder在解码t时刻的token时,应该只能使用t时刻之前的信息,因为你不能使用未来信息(t之后)来预测当前结果,所以要想办法把t时刻之后的信息隐藏掉。
具体做法,产生一个上三角矩阵,上三角全是1,下三角全是0,对角线也是0。1表示需要被隐藏。
4.transformer中self-attention和multi-head的代码实现
1.计算注意力权重
def scaled_dot_product_attention(q, k, v, mask):
"""
计算注意力权重。
要求:q,k,v,前面维度需要保持一样。
k,v必须有匹配的倒数第二个维度,即:seq_len_k=seq_len_v
:param q: shape(...,seq_len_q,depth)
:param k: shape(...,seq_len_k,depth)
:param v: shape(...,seq_len_v,depth_v)
:param mask: shape(...,seq_len_q,depth),默认为None
:return: 输出,注意力权重
"""
multul_qk = tf.matmul(q, k, transpose_b=True)
# 缩放 matmul_qk
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = multul_qk / tf.math.sqrt(dk)
# 将mask加入到缩放的张量上
if mask is not None:
scaled_attention_logits += (mask * -1e9)
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=1) # (..., seq_len_q, seq_len_k)
output = tf.matmul(attention_weights, v) # (..., seq_len_q, depth_v)
return output, attention_weights
复制代码
2.多头的实现
多头注意力
- 线性层拆分成多头
- 每一个头执行按比缩放的点积注意力,scaled_dot_product_attention
- 多头拼接一起
- 最后过一层线性层
说明:
- 多头机制是先拆分成多份,再拼接回来。而不是输入维度不变,执行多次scaled_dot_product_attention,最后再缩放到原本维度。
- 分拆后,每个头部的维度减少,因此总的计算成本与有全部维度的单头计算相同。
- 多头只需拆最后一维!
class MultiHeadAttention(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
assert d_model % self.num_heads == 0
self.depth = d_model // self.num_heads
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)
self.dense = tf.keras.layers.Dense(d_model)
def split_heads(self, x, batch_size):
"""
分拆最后一个维度, d_model -> (num_heads, depth),
:param x: (batch_size, seq_len, d_model)
:param batch_size:
:return: 转置结果使得形状为 (batch_size, num_heads, seq_len, depth)
"""
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
# shape(batch_size, seq_len, num_head, depth)
return tf.transpose(x, perm=[0, 2, 1, 3])
def call(self, v, k, q, mask):
"""
执行多头机制
:param v: (batch_size, seq_len_q, d_model)
:param k:
:param q:
:param mask:
:return:
output: (batch_size, seq_len_q, d_model)
attention_weights: (batch_size, num_heads, seq_len_q, depth)
"""
batch_size = tf.shape(q)[0]
q = self.wq(q) # (batch_size, seq_len_q, d_model)
k = self.wk(k) # (batch_size, seq_len, d_model)
v = self.wv(v) # (batch_size, seq_len, d_model)
# 拆分成多头,其中d_model = num_heads * depth
q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_q, depth)
v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_q, depth)
# 多个头的QKV共同执行scaled_dot_product_attention
scaled_attention, attention_weights = scaled_dot_product_attention(
q, k, v, mask)
# scaled_attention.shape == (batch_size, num_heads, seq_len_q, depth)
# attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k)
# 个头的注意力输出连接起来(用tf.transpose 和 tf.reshape)
scaled_attention = tf.transpose(scaled_attention,
perm=[0, 2, 1, 3]) # (batch_size, seq_len_q, num_heads, depth)
concat_attention = tf.reshape(scaled_attention, (batch_size, -1, self.d_model))
# concat_attention: (batch_size, seq_len_q, d_model)
# 放入最后的 Dense 层
output = self.dense(concat_attention)
return output, attention_weights
复制代码
5.为什么BERT用可训练的position mebedding而不是cos-sin编码的position-embedding?
Why did BERT use learned position embedding rather than sinusoidal position encoding?
6. transformer中的根号dk
公式: