1.残差
随着网络变深,会出现梯度消散或者梯度爆炸问题,这个可以通过下面的BN和LN解决,将激活函数换为ReLu,使用Xaiver初始化等解决。
但增加深度的另一个问题就是网络的退化问题(degradation),即随着网络深度的增加,网络性能会越来越差,直接表现在训练集上的准确率饱和甚至下降。
设想一下,如果一个特别深的网路,模型已经在某一层得到了最佳情况,剩下层应该要做的尽量保证网路的特征,在后面层自动学成恒等映射(identity mapping) 的形式,换句话说,相对于浅层网络更深的网络至少不会有更差的效果。
因此,为了让网络的学习一个恒等映射网络,如果直接去拟合一个恒等映射函数,比较困难。
但是,把网络设计成, 还是关于x的函数,实际上是对于x的补充或者修正。
这样就把从根据x映射成一个新的y转为了根据x求x和y之间的差距,也就是
这就转化成了要学习一个残差函数的问题,这样,只要让,拟合残差函数更加容易。
残差有效的解释:
简要来讲,残差使得映射对输入的变化更加敏感.
2.Layer normalization
Batch Normalization
- 2.1 用BN的原因
说起normalizaiton,需要先说起Batch Normalization。Batch Normalization的提出主要是为了解决深度神经网络训练过程中的Internal Covariate Shift现象,即在深度神经网络,随着网络深度加深,训练就越困难,收敛越慢。在前一层参数变化时,每层的输入分布也随之变化,进而上层的网络需要不停地去适应这些分布变化,使得我们的模型训练变得困难。
- 2.2 BN实现方法
所以,提出白化的方案,白化在机器学习中通常用来规范数据分布。它的作用是:
- 使得输入特征分布具有相同的均值与方差。 其中PCA白化保证所有特征分布均值为0方差为1。
- 去除特征之间的相关性。
但是白化具有两个问题:
- 计算复杂度比较高。
- 白化过程改变了网络每一层的分布。这样就无法学习到数据原始的分布信息了。
为了解决上面两个问题,提出了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?
在测试预测阶段,就没有batch的概念,也无法计算batch的均值和方差。当模型训练好之后,BN层的两个参数就确定下来了。
因此,在测试阶段计算BN时,我们使用训练阶段的全部batch的统计量对测试数据进行归一化。具体来说均值使用全部训练batch的均值的平均,方差使用全部训练batch方差的无偏估计:
也就是说,在预测阶段,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任务中,一个样本内部维度之间是关联的,所以,在信息归一化时,对样本内部差异进行一些损失,反而能降低方差。
-
Transformer中使用BN表现不好的原因是CV和NLP数据特征的不同,对于NLP任务,前向和反向传播中,batch的统计量及其梯度都不太稳定。即在NLP任务上(IWSLT14)batch的均值和方差一直震荡,偏离全局的running statistics,而CV任务也相对稳定。From《PowerNorm: Rethinking Batch Normalization in Transformers》
-
对于NLP data来说,batch上去做归一化是没啥意义的,因为不同句子的同一位置的分布大概率是不同的。
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的代码实现
class SelfAttention(nn.Module):
def __init__(self, config):
self.num_heads = config.num_heads
self.hidden_size = config.hidden_size # 中间层
self.depth = self.hidden_size // self.num_heads
self.all_head_size = self.depth * self.num_heads
#初始化q,k,v矩阵
self.query = nn.Linear(config.hidden_size, self.all_head_size)
self.key = nn.Linear(config.hidden_size, self.all_head_size)
self.value = nn.Linear(config.hidden_size, self.all_head_size)
self.dropout = nn.Dropout(config.drop_prob)
def _split_and_permute(x):
# x:[bs, len, hidden_dim] -> [bs, num_head, len, dpeth]
bs, len, hd = x.size()
size = (bs, len, self.num_heads, self.depth)
x = x.view(*size)
x = x.permute(0, 2, 1, 3)
return x
def forward(q_inputs, k_input, attention_mask):
# inputs: [bs, q_len, hidden_size]
# attention_mask: [bs, 1, q_len, len_k]
# 生成qkv矩阵
Q = self.query(q_inputs)
K = self.key(k_input)
V = self.value(k_input)
#按头分割和转换
Q = self._split_and_permute(Q) # [bs, num_heads, q_len, depth]
K = self._split_and_permute(K) # [bs, num_heads, k_len, depth]
V = self._split_and_permute(V) # [bs, num_heads, v_len, depth]
# 计算在key上的attention概率分布
attention_score = torch.matmul(Q/math.sqrt(self.depth), K.permute(0, 1, 3, 2))
attention_score = attention_score + attention_mask # [bs, num_heads, q_len, k_len]
attention_score = nn.Softmax(dim=-1)(attention_score)
attention_score = self.dropout(attention_score)
# 计算在value上的结果(context_vector)
context_vector = torch.matmul(attention_score, V) #[bs, num_heads,q_len,depth]
# shape转回去
# tensor经过转置后数据的内存地址不连续导致的,所以在view之前,只要使用了transpose()和permute()这两个函数一定要contiguous()
context_vector = context_vector.permute(0, 2, 1,3).continue()
new_cv_shape = context_vector.size()[:2] + (self.all_head_size)
context_vector = context_vector.view(new_cv_shape)
return context_vector
5.为什么BERT用可训练的position mebedding而不是cos-sin编码的position-embedding?
Why did BERT use learned position embedding rather than sinusoidal position encoding?
6. transformer中的根号dk
公式:
论文中解释:Q和K的点乘结果很大,这会将softmax函数的梯度push到很小的区域,这不利于训练。而scale会缓解这种情况。
以数组为例子,两个长度为len、均值为0、方差为1的时数据相乘会产生一个长度为len、均值为0、方差为len的数组。而方差大会导致给softmax的输入推向正无穷和负无穷(更多),这是梯度会无限趋近于0,不利于训练。因此,将结果数据除以,可以让结果数组方差为1(参考中有证明),这样有利于训练的收敛。
7. 为什么 Bert 的三个 Embedding 可以进行相加?
8. Transformer中位置向量,为什么sin和cos可以表示距离?
首先要明白Transformer中为什么需要位置向量,Transformer这种存粹的attention模型是无法捕获输入位置信息的,而token的输入位置信息又是非常重要的特征,因此需要给这类模型输入额外加入位置编码。
位置编码的要求与sin和cos可以表示距离的原因:
- 位置编码要有一定的值域范围(有界),要不然不同位置的位置编码数值差距过大,在与字嵌入合并后可能出现特征在数值上的倾斜。而sin和cos的值域在[-1,1]之间,满足这个要求。
- 位置编码需要体现同一个单词在不同位置的区别,需要体现先后顺序,并且更重要的,在一定范围内的编码差异不应该依赖于文本长度,具有一定不变性。如果我们使用绝对位置信息,在较短文本中相邻token位置编码差异与在较长文本中一样了,这显然不合适。实际上,我们只需要关注相对位置信息,只需要关注一定范围内的相对次序位置编码差异就行了,因此,我们需要一个有界的周期性函数,使用波长来控制值域的变化差异。 而sin和cos就是一种很好的周期性函数。
- 位置编码需要能体现相对位置信息。根据三角函数性质:, 。这表明位置可以表示成位置和位置的向量组合,这提供了表达相对位置信息的可能性。
Transformer中位置向量公式:
表示输入token的位置,表示位置向量的维度,即生成高维响亮,是交替使用sin和cos函数。
位置编码是一个高维的向量,需要在每个维度都使用sin/cos函数计算位置特征。并且,在不同维度上应该用不同的函数刻画位置编码,这样高纬的便是空间才有意义。
9.Post-LN 和 Pre-LN
Post和Pre是相对于残差的位置来说的,见下图:
图a就是原始的Transformer模式,LN在残差之后做,称为Post-LN。其中addition表示残差,所以一层Layer是有两个地方有残差处理的。
而图b表示Pre-LN,即LN在计算Multi-head Attention和FFN之前做。
关于两者的表现区别:“Pre-LN”对梯度下降更加友好,收敛更快,更易于超参优化,但其性能总差于“Post-LN”。
10. self-attention的时间复杂度和空间复杂度?
时间复杂度:
self-attention包含三个部分计算:相似度矩阵计算,计算softmax,计算对V的加权平均,分开来说:
相似度矩阵计算:看作(n,d)与(d,n)的矩阵相乘,得到一个(n,n)的概率矩阵。矩阵相乘的时间复杂度:,需要计算nn次的d维向量点积,而维度为d的两向量点积,需要计算d次乘法和d次加法,复杂度为。
softmax: 对数组中第i个元素,这个元素的softmax值计算公式为:,因此计算一个(n,n)概率矩阵的softmax的时间复杂度为: .
计算对V的加权平均:加权平均看作(n,n)的概率矩阵与(n,d)矩阵相乘,得到一个(n,d)的矩阵。时间复杂度为:,需要计算nd次的n维向量点积。
综上,self-attention的时间复杂度为:。如果是多头,复杂度为:
空间复杂度:
11. Bert/Transformer参数估计
以base的Bert模型为例子:layer=12,hidden_size=768,vocab_size=30522,hidden_size=768,max_position_embeddings=512,token_type_embeddings=2 :
- 词向量参数: 输入有三部分嵌入表示,维度都相同。
这部分参数量为:(30522 + 512 + 2 ) * 768 。 - Multi-Heads Attention层参数:self-attention输入shape为[bs, len, hidden_size]的特征向量,先乘以q,k,v三个线性映射层得到Q,K,V向量(维度不变仍然是[bs, len, hidden_size]),然后根据num_heads在hidden_size维度切分成多头,在每个头上计算self-attention操作。所以这里参数数量:768 * 768 * 3。在Multi-Head attention的最后,又加了一层线性变换,参数量: 768 * 768 .
这部分参数量为:(768 * 768 * 3 + 768 * 768) * layer。 - 全连接层参数:BERT中通常使用两层,feature_out通常为4 * hidden_size=3072。
这部分参数量为: (768 * 3072 + 3072 * 768) * layer。 - LayerNorm层(都是Post-LN):BERT中有三个地方使用了LN,1.三个embedding相加之后。2.多头注意力之后。3.全连接层之后。根据LN的原理,LN层的参数就是和,与特征向量的维度相同。
这部分 参数量:768 * 2 + (768 * 2 * 2) * layer。
12. Transformer中为什么使用不同的K 和 Q, 为什么不能使用同一个值?
- 增加了表达能力。Slef-attention的解释,K和Q的点乘,在计算softmax是为了得到一个归一化的attention_score矩阵,用来对V进行提纯。使用不同的,,生成Q、K和V,可以理解为在不同空间上的投影,增加了表达能力,提高了泛化能力。
- 打破对称性。如果只使用同一个值,那得到的attention_score就是一个对称矩阵。但是在实际问题上,两个token,A对B重要,B对A不一定重要。
- 如果另Q=K,那么大概率会得到一个类似单位矩阵的attention_score矩阵,这样self-attention就退化成一个point-wise线性映射了。
13.self-attention 是如何解决长距离依赖问题?
-
长距离依赖是什么?
对于序列问题,第t时刻的输出依赖t之前时刻的输入,当t逐渐增大,前面的信息很难被学习到,很难建立长距离依赖关系。 -
为什么CNN和RNN无法解决长距离依赖问题?
CNN:CNN主要采取卷积核的方式来捕获token之间的关联信息,可以看作类似n-gram的局部编码方式,n相当于卷积核的宽度。n-gram是局部编码,即使堆积多层卷积,也很难与远距离token建立联系。
RNN:RNN处理序列是一个一个token顺序处理,本质上是一种循环的学习方式。随着时间推移,会出现梯度消失或梯度爆炸问题,因为RNN计算包含多次非线性激活函数,会有大量的梯度连乘。因此无法建立长距离依赖信息。
LSTM: 通过引入门的机制来选择性记忆一些重要信息,门上几乎没有非线性激活函数,所以梯度消失问题减轻很多。这在一定程度上缓解了长距离以来问题,但是当序列大于300,这个问题还是很严重。 -
self-attention如何解决的?
是直接在完整序列上学习重点位置。在self-attention计算中,对于query中每个token,需要与key句子中所有token直接进行点乘计算归一化的attention_score,以获得所有key对于当前query的score,然后再与所有value计算加权和进行提纯。
14.Transformer 如何并行化的?
Encoder端: RNN之所以不支持并行,是因为它天生是一个时序结构,循环计算,只能每个时刻依次计算。而Transformer可以一次处理整个序列,计算attention_score直接使用向量相乘,无序按照时序依次迭代计算。
Decoder端:1.teacher force。 2. self-attention masked。
15. Transformer的问题和缺陷有哪些?
- Transformer不能很好的处理超长输入问题。理论上self-attention可以关联到任意的距离的词,但实际中,由于计算资源有限,会限制输入序列长度,超过的部分会丢失。
- Transformer缺少conditional computaion。就是在encoder的过程中,每个token的计算都是相同的,但实际上对于一句话中,不同token信息量是不同的,因此对每个token应用相同的计算量,这样显然是低效的,这也导致序列长度和计算资源的限制。
- Transformer时间复杂度和空间复杂度过大。
解决方案:
-
对长文本分段处理,但是建模要解决context fragmentation(上下文碎片化),因为分段并不是根据语义边界,有可能相连的句子被分割了。采用Transformer-XL,采用Segment-level Recurrence来解决,思路是,对当前segment进行处理时,利用前面segment缓存的对应layer的隐向量序列。注意,上一个segment的所有隐向量值参与前向计算,不进行反向传播。
16. 关于Tokenizer
BERT中封装了三种tokenizer:
BasicTokenizer: 清理特殊字符和空格.
WordpieceTokenizer:Subword模型.
FullTokenizer: 先调用BasicTokenizer,再调用WordpieceTokenizer。
三种Subword模型: BPE,WordPiece,ULM:
-
BPE(Bype Pair Encoding):
把整个预料切分成字符级别,统计频率,再根据频率将n-gram进行合并构造词典。
a. 先使用预分词器完成初步切分,比如简单的基于空格和规则。
b. 分词后,统计每个词的词频,比如: ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5) .
c. 拆分成最小单元,建立基础词汇表:["b", "g", "h", "n", "p", "s", "u"]
d. 分别考虑2-gram,3-gram的字符组合,把高频的相邻n-gram合并加入到词汇表中。直到词汇表得到预定大小。
e. 则最终词汇表= 基础词汇表 + 合并词汇 + ''。分词过程: 得到subword的词表,按照长度从大到小排序。对单词,依次遍历词表来匹配子词。最终没匹配的用''表示。
-
WordPiece :
基本思路与BPE相似,区别在于合并的策略,BPE是对高频的相邻子词合并,而WordPiece是选择能够提升语言模型概率的相邻字词合并。
在切分之后,假设句子由n个子词组成,根据经典语言模型假设,每个token是独立的,这句话的语言模型似然值等于所有子词概率的乘积:假设要把相邻x,y位置子词合并,则合并之后的句子的似然变化为:
其实就是合并最大互信息的相邻子词,这个统计量就表示x,y这两个子词在语料中相邻方式出现的概率比较大。
-
ULM(Unigram Language Model):
也是基于语言模型来构建词表。
参考:
zhuanlan.zhihu.com/p/330494442
[参考:]
结合代码浅析Transformer
The Illustrated Transformer
动手推导Self-Attention
Batch Normalization原理与实战
聊聊 Transformer
batch normalization与 layer normalization区别
transformer中的根号dk解释