Attention出自NMT(神经网络机器翻译)以处理文本对齐问题,目前已经在各个领域发光发彩,玩出各种花样带出多少文章。而Attention的本质其实就是–加权重。
通用的NMT的架构如上图所示,其中会由两个Deep LSTM做encoder 和 decoder。( NMT大部分以Encoder-Decoder结构为基础结构,而且特别喜欢bidirectional,但它无法适应在线的场景,所以目前为止RNN系列在NLP领域中是淘汰趋势,基本上都可以用transformer做代替了)而文本对齐的问题是 对输入的一个句子对,这个句子对中相对应部分的映射,比如
- 每天都喜欢我居一点点
- I love juju a little bit every day
那么每个单词之间应该如何实现这种对应(特别是这种输入输出双方都不定长),即在翻译“every day”的时候,显然对于输入的句子“每天”的重要性相比于其他词的重要性是不能比的。那么对于不同词的重要性每一时刻都是动态的吗?那么究竟应该关注哪些时刻的encoder状态呢?而且关注的强度是多少呢?
于是可以构想一种打分机制,结合输入和正在预测的输出联合计算当前时刻的Attention:那么以前一时刻t-1的decoder状态和某个encoder状态为参数,输出得分,即在BiLSTM的基础上又额外算了一种权重:
c
=
s
c
o
r
e
(
h
t
−
1
,
h
s
′
)
c=score(h_{t-1},h'_s)
c=score(ht−1,hs′)然后利用c,在所有输入的上下文+已经预测的结果去预测下一时刻:
p
(
y
)
=
∏
t
=
1
T
p
(
y
t
∣
{
y
1
,
.
.
.
,
y
t
}
,
c
)
p(y)=\prod_{t=1}^{T}p(y_t | \{y_1,...,y_t\},c)
p(y)=t=1∏Tp(yt∣{y1,...,yt},c)
对于每个输入序列的词
x
t
x_t
xt,都有个中间隐层的解释向量
h
j
h_j
hj(
h
j
=
[
h
j
→
,
h
j
←
]
h_j=[ h_j→, h_j←]
hj=[hj→,hj←]包含了j和其前后信息),那么对当前预测词
y
t
y_t
yt的贡献权重α采用softmax方式计算(也被称为对齐权值(alignment weights)),即用来衡量某个词对当前预测词的匹配度。这样对所有输入序列中的词都通过h对注意力向量c作贡献,而且每一时刻都会做这样的动态计算,很简单,详细公式如上图。
- 目标:query —> key-value对的映射 以完成自动加权
- 1 Q与K进行相似度计算得到权重
- 2 softmax归一化
- 3 应用权重,和value进行加权求和便得到attention
pytorch官方示例:
class AttnDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
super(AttnDecoderRNN, self).__init__()
self.hidden_size = hidden_size
self.output_size = output_size
self.dropout_p = dropout_p
self.max_length = max_length
self.embedding = nn.Embedding(self.output_size, self.hidden_size)
self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
self.dropout = nn.Dropout(self.dropout_p)
self.gru = nn.GRU(self.hidden_size, self.hidden_size)
self.out = nn.Linear(self.hidden_size, self.output_size)
def forward(self, input, hidden, encoder_outputs):
embedded = self.embedding(input).view(1, 1, -1)
embedded = self.dropout(embedded)
#计算权重,cat->linear->softmax一步算完
#cat了Q K,然后让linear去自己学习权重再softmax
attn_weights = F.softmax(
self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
#算好的权重乘原向量
attn_applied = torch.bmm(attn_weights.unsqueeze(0),
encoder_outputs.unsqueeze(0))
#再用linear融合decoder和Attention结果
output = torch.cat((embedded[0], attn_applied[0]), 1)
output = self.attn_combine(output).unsqueeze(0)
#利用结果预测输出就可以了
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = F.log_softmax(self.out(output[0]), dim=1)
return output, hidden, attn_weights
def initHidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
常用Attention形式
用的比较多的主要有点积,通用,拼接,感知机等,形式如下:
Q
T
K
Q^TK
QTK
Q
T
W
a
K
Q^TW_aK
QTWaK
W
a
[
Q
,
K
]
W_a[Q, K]
Wa[Q,K]
v
a
T
t
a
n
h
(
W
a
Q
+
U
a
K
)
v_a^Ttanh(W_aQ+U_aK)
vaTtanh(WaQ+UaK)
其实还有一些加入正则,局部偏好,采样微分优化梯度等改进方法,以及其他小trick。
Attention变体
在实践应用中Attention已经被玩到emmm,很缤纷的程度了。
- Soft-Attention,Hard-Attention,不软不硬Attention(用hard确定范围,再用soft在窗口中分配)
- Mutil-Attention,Co-Attention,Cross-Attention,Hierarchical-Attention,Group-Attention,High-order-Attention
- Channel-wise-Attention,Spatial-Attention
- Bahdanau-Attention(https://arxiv.org/abs/1409.0473), Loung-Attention(https://arxiv.org/abs/1508.04025)
升级版:normed_Bahdanau-Attention,和scaled_Loung-Attention.(https://arxiv.org/pdf/1602.07868.pdf)。 - Monotonic-Attention
Attention in CNN
- 直接计算Attention
- 卷积完后计算Attention
- 加入附加信息计算Attention
- 全部结合起来计算
Transformer
但是,attention is all you need 。只要有attention本身就够了!不止是不要一些无关痛痒的进化,我们不需要RNN!(特别是RNN串行无法并行化,训练时间太长了),只需要 暴力算算算不要钱 Self-Attention,3种Attention,多套Attention 足矣!
self-attention
首先是self-attention(也被称为 scaled dot-product attention),相比普通的注意力,这里是“自”。普通的方法是别人跟自己家人算相似算权重,“自”则是自己家人相互算权重,即句子中词和词之间算权重。:
A
t
t
e
n
t
i
o
n
(
Q
,
K
,
V
)
=
s
o
f
t
m
a
x
(
Q
K
T
d
k
)
V
Attention(Q,K,V)= softmax(\frac{QK^T}{\sqrt{d_k}})V
Attention(Q,K,V)=softmax(dkQKT)Vdk是归一化系数,用来scaled。先看具体的计算方式如下图:
输入是‘thinking’和‘machines’的向量
X
1
X_1
X1和
X
2
X_2
X2,两者共享
W
Q
,
W
K
,
W
V
W^Q,W^K,W^V
WQ,WK,WV的映射矩阵得到query q,key k和value v,自注意力的自就在于这三个都是从自己的原向量得到的。然后对于权重计算,将
q
1
⋅
k
1
q_1\cdot k_1
q1⋅k1和
q
1
⋅
k
2
q_1\cdot k_2
q1⋅k2,再divide放缩之后softmax,即可以理解为要得到‘thinking’的向量,需要将自己的查询q跟所有其他词的向量进行一个相似度的比较,然后整个进行加权平均得到最后的权重,再乘value值即得到最后的表示
z
1
z_1
z1。
- 为什么算Q和K要内积?向量的内积以计算两个向量的相似度,即起到Q查询K的作用。
- Softmax怎么做?先exp放大明显一下,再归一化算分数。
- 为什么归一化系数?维度越大内积就越大,但不能就这样判断重要性(如10维的向量和100维的向量的结果,100维的值显然会更大),所以要Scala与维度数无关。
- 归一化d的另一种解释。加和Attention效果其实要好于点积(因为积的操作会比和大很多,softmax的结果会偏向梯度小区域),但是点积可以用矩阵来加速,为了保留点积所以需要除以一个系数尝试抵消这一点。
- 矩阵乘法实际上是所有词一起而不是一个一个的并行算。
- 多头直接拼接再FC降维。
- “自”在哪里?由于不使用RNN后,原先的隐层h直接变成word,整体看到就是自己通过与自己比较来算Attention,此时K,V是一样的,即self-attention。
#self-attention
def forward(self, q, k, v, mask=None):
attn = torch.bmm(q, k.transpose(1, 2))
attn = attn / self.temperature
if mask is not None:
attn = attn.masked_fill(mask, -np.inf) #要当前词要屏蔽掉其他的,而且设为负无穷之后softmax才是0
attn = self.softmax(attn)
attn = self.dropout(attn)
output = torch.bmm(attn, v)
return output, attn
Transformer一共使用了三种Attention分别是encoder的self-attention,decoder的mask self-attention,以及连接encoder和decoder之间的cross attention,如上图的模型结构。
- Encoder-Decoder层,Q来自先前的解码器,并且K和V来自Encoder的输出。属于两块地方的Cross部分,Encoder过来的是k和v,output是产生q,可以理解为目前已经输出的一些单词去查询当前输入的词最后翻译为我们想要得到的结果。
- Encoder中的Self-attention层。所有的K、V和Q来自同一个地方,都是Encoder中前一层的输出。
- Decoder中的Self-attention层。它不能计算所有位置,需要遮住decoder中向左的信息流以保持自回归属性。(目前只翻译出了一半的结果,那么输入只能有一部分而不是全部的输出)
而多套Attention是,上述三种都是multi-head attention,即把self-attention重复做多次如N=8(参数不共享,可并行),然后拼起来,以多套视角对数据进行操作: h e a d i = A t t e n t i o n ( Q W i Q , K W i K , W i V ) head_i=Attention(QW_i^Q,KW_i^K,W_i^V) headi=Attention(QWiQ,KWiK,WiV) M u l t i H e a d ( Q , K , V ) = c o n c a t ( h e a d 1 , . . . , h e a d h ) MultiHead(Q,K,V)=concat(head_1,...,head_h) MultiHead(Q,K,V)=concat(head1,...,headh)
其中Encoder和Decoder都有6个子层,每两个子层之间都使用了残差(Residual Connection,解决梯度问题) 和归一化(加快收敛),并dropout了(rate=0.1)再输出。 D r o p o u t ( L a y e r N o r m ( x + S u b l a y e r ( x ) ) ) Dropout(\mathrm{LayerNorm}(x + \mathrm{Sublayer}(x))) Dropout(LayerNorm(x+Sublayer(x)))
#MultiHeadAttention
def forward(self, q, k, v, mask=None):
#归一化系数,维度和多头数
d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
sz_b, len_q, _ = q.size()
sz_b, len_k, _ = k.size()
sz_b, len_v, _ = v.size()
#不是concat 每两个子层之间使用残差形式
residual = q
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
#这里把batch和分块数放在一起,便于使用bmm
q = q.permute(2, 0, 1, 3).contiguous().view(-1, len_q, d_k) # (n*b) x lq x dk
k = k.permute(2, 0, 1, 3).contiguous().view(-1, len_k, d_k) # (n*b) x lk x dk
v = v.permute(2, 0, 1, 3).contiguous().view(-1, len_v, d_v) # (n*b) x lv x dv
#多头
#Masked是考虑到输出Embedding会偏移一个位置
#错位:从前到后(LTR)预测下一个词,从后到前(RTL)预测前一个词
#确保预测时仅此时刻前的已知输出,而把后面不该看到的信息屏蔽掉(能看到就作弊了)
mask = mask.repeat(n_head, 1, 1) # (n*b) x .. x ..
output, attn = self.attention(q, k, v, mask=mask)
output = output.view(n_head, sz_b, len_q, d_v)
output = output.permute(1, 2, 0, 3).contiguous().view(sz_b, len_q, -1) # b x lq x (n*dv)
output = self.dropout(self.fc(output))
output = self.layer_norm(output + residual)
return output, attn
更多Transformer的细节源代码逐行注释:https://github.com/nakaizura/Source-Code-Notebook/tree/master/Transformer
Transformer运行动图:
一些训练trick
Soft Attention
hard Attention就是对于某些选定的区域是1,而其他直接为0,这显然不太好。soft软性注意力机制有两种:普通模式(Key=Value=X)和键值对模式(Key!=Value)。其选择的信息是所有输入信息在注意力
α
\alpha
α分布下的期望。
a
t
t
(
q
,
X
)
=
∑
i
=
1
N
α
i
X
i
att(q,X)=\sum_{i=1}^N \alpha_iX_i
att(q,X)=i=1∑NαiXi
MASK
Mask这个东西其实并不是在Transformer/BERT这里才使用,在RNN和Attention中都存在着应用,主要分为两种使用:处理非定长序列和防止标签泄露。
- 处理非定长序列。RNN理论上可以处理非定长的句子/序列等,但是在实践中,为了 batch 训练,一般会把不定长的序列 padding 到相同长度,再用 mask 去区分非 padding 部分和 padding 部分。这样可以使padding部分不处理计算或者计算loss。同样在 Attention 机制中,同样需要忽略 padding 部分的影响,self-attention 中,Q 和 K 在点积之后,需要先经过 mask 把想屏蔽的部分输出为负无穷后 再进行 softmax(如上面又插入过self-attention的代码)。
- 防止标签泄露。Transformer 是包括 Encoder和 Decoder的,Encoder中 self-attention 的 padding mask 外,而 Decoder 还需要防止标签泄露,即在 t 时刻不能看到 t 时刻之后的信息,因此在上述 padding mask 的基础上,还要加上 sequence mask,即一个按时刻的三角矩阵。 而BERT的mask是为了做Masked LM,和XLNet的mask是为了PLM(Permutation Language Modeling),都十分的巧妙,这里下一篇博文再详细整理。
Feed forward
为了得到更好的更抽象能力的向量而加的,而多个自注意力堆一起也是为了这种“深度”。
Skip connection
模仿残差。设计直觉上是至少不必原来差(做了深度学习抽象特征等一堆事之后并不能保证这个向量结果比原来好),另一方面也是帮助深度学习学习缓解梯度消失。
Layer normalization
Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据,尽量不使输入数据落在激活函数的饱和区。
h
t
=
f
[
g
σ
t
⋅
(
a
t
−
μ
t
)
+
b
]
h^t=f[\frac{g}{\sigma^t}\cdot(a^t-\mu^t)+b]
ht=f[σtg⋅(at−μt)+b]把普通BN用可学习的参数g和b进行一种可学习的缩放移动。
BatchNorm和LayerNorm的区别?
- BatchNorm — 为每一个小batch计算每一层的平均值和方差,即是所有样本的各个维度位置的归一化,以求梯度的“圆”化。
- LayerNorm — 独立计算每一层每一个样本的均值和方差,即归一某样本自己维度。
从LayerNorm的优点来看,它对于batch大小是健壮的,并且在样本级别而不是batch级别工作得更好。
(实际上BN后的输出,经过网络层后,仍然不再是归一化的了。然后不断BN,会使数据的偏差越来越大即Internal Covariate Shift,当网络在反向传播需要考虑到这些大的偏差,就迫使只能使用较小的学习率来防止梯度消失或者梯度爆炸,关于BN系列文末会再次统一整理一下备忘)
Label smoothing
也是一种soft方法,把绝对的0,1标签,变成
1
−
β
1-\beta
1−β,
β
\beta
β部分其他地方平分,如[0 1 0 0 0 0]变成[0.02 0.9 0.02 0.02 0.02 0.02]。另一方面如果训练数据存在误差(这很常见),通过这种表情平滑使用类权值来修正损失对健壮性都是很有好处的。
Noam learning rate schedule
学习率先直线上升,再指数衰减。
Encoder和Decoder的mask不同
- Encoder中没有Masked,而Decoder中需要使用Masked,因为在序列生成过程中,在 i 时刻,大于 i 的时刻都是未知的,只有小于 i 时刻的预测是一样的,因此需要做Mask来屏蔽,即保持部分的输出。
为什么Transformer可以代替RNN/CNN
RNN其实只比NN多一个前一时刻的向量,本质上仍然是“局部编码”,而它无法并行速度太慢,至于CNN…无法捕捉长距离。Self-Attention是图神经网络的一个特例,且已经可以考虑到前时刻的状态进行计算,“动态”地生成不同连接的权重,从而处理变长的信息序列。所以也因为RNN+word2ve的缺点1不能并行2层数太少3考虑不到语境,也就诞生了BERT等模型,这在下一篇文章进行整理。
为什么要位置信息?
另外由于Transformer不包含递归和卷积结构了,为了加强有效利用序列的顺序特征,会加入序列中各个Token间相对位置或绝对位置的信息(因为自注意力中每个词其实都会对整个序列加权,那么词在哪个位置都是一样的,这显然和实际句子有顺序是相悖的)。BERT一般使用不同频率的正弦和余弦函数Embedding:
P
E
(
p
o
s
,
2
i
)
=
s
i
n
(
p
o
s
/
1000
0
2
i
/
d
model
)
PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}})
PE(pos,2i)=sin(pos/100002i/dmodel)
P
E
(
p
o
s
,
2
i
+
1
)
=
c
o
s
(
p
o
s
/
1000
0
2
i
/
d
model
)
PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}})
PE(pos,2i+1)=cos(pos/100002i/dmodel)
其中pos是位置,i是维度,位置编码的每个维度都对应于一个正弦曲线.(容易学会Attend相对位置),在偶数位置用正弦,奇数位置用余弦,最后把这个positional encoding 与 embedding直接相加,再输入到下一层。
看公式可以明白sin后面的值是很小的,不管是sin还是cos的周期信号在第一递减,所以实际上也是位置越远权重越小。
(不用这种复杂的计算也是可以的,比如用随着与当前单词位置距离增大而权重减小等,但是把这种复杂的方法可视化还真的很数学之美。。。如上图,纵坐标是位置从0-50)
为什么位置信息是“+”而不是concat?
直接加进去难道不会找不到了吗?答案是位置信息加进去的时候也会乘W,而特征也是W,这种情况下其实是等同于concat了再整体W。
为什么要多头 multi-head
- 扩展了模型关注不同位置的能力
- 多组映射子空间
类似CNN多通道,从多个角度以增强信息,利于捕捉更丰富的特征(特别是自从Transformer逐渐日常化后,不同的Transformer所侧重的点确实有很大的不同)。而且,可以并行,时间效率上差别并不大。
Transformer的时间复杂度
LSTM是序列长度 x hidden2,Transformer是序列长度2 x hidden。当hidden大于序列长度时(往往都是这种情况),Transformer比LSTM要快很多。
如果序列长度超过512了怎么办?
用Transformer-XL或者Longformer。
Adam优化的局限性
虽然Adam有自适应的学习率有助于模型快速收敛,但结果的泛化能力学不如SGD。这可能是因为在初期学习率的设置上,太小了在训练初期的偏差会比较大,太大了有可能收敛不到最佳。(解决:可以用学习率预热。或者AdamW使用了L2正则,这样小的权重泛化性能会更好)
Transformer的优缺点
优点
- 每层的计算复杂度低.。LSTM的复杂度是:序列长度n x hidden²,Transformer的复杂度是:序列长度n² x hidden。当序列长度n小于表示维数时,self-attention层速度会很快。
- 利于并行。多套注意力之间互不干扰,一起计算节省时间。
- 模型可解释很高。注意力具有天生的解释能力。
缺点
- 对新出现的词表现不优。
- RNN图灵完备,而transformer不是。图灵完备是只要图灵机的算力足够,理论上可以近似任何值的算法。
Transformer做文本分类
文本分类不需要sq2sq,所以只使用Transformer编码器。即模型图左边的内容,得到一个分类概率就行。
class EncoderLayer(nn.Module):
def __init__(self, d_model, n_heads, p_drop, d_ff):
super(EncoderLayer, self).__init__()
self.mha = MultiHeadAttention(d_model, n_heads)
self.dropout1 = nn.Dropout(p_drop)
self.layernorm1 = nn.LayerNorm(d_model, eps=1e-6)
self.ffn = PositionWiseFeedForwardNetwork(d_model, d_ff)
self.dropout2 = nn.Dropout(p_drop)
self.layernorm2 = nn.LayerNorm(d_model, eps=1e-6)
#图左边的逻辑
def forward(self, inputs, attn_mask):
# |inputs| : (batch_size, seq_len, d_model)
# |attn_mask| : (batch_size, seq_len, seq_len)
attn_outputs, attn_weights = self.mha(inputs, inputs, inputs, attn_mask) #多头注意力
attn_outputs = self.dropout1(attn_outputs) #dropout
attn_outputs = self.layernorm1(inputs + attn_outputs) #层正则
# |attn_outputs| : (batch_size, seq_len(=q_len), d_model)
# |attn_weights| : (batch_size, n_heads, q_len, k_len)
ffn_outputs = self.ffn(attn_outputs) #前向
ffn_outputs = self.dropout2(ffn_outputs) #dropout
ffn_outputs = self.layernorm2(attn_outputs + ffn_outputs) #add+ln
# |ffn_outputs| : (batch_size, seq_len, d_model)
return ffn_outputs, attn_weights
完整代码:
code:https://github.com/lyeoni/nlp-tutorial/blob/master/text-classification-transformer/
Batch Normalization
Batch Normalization首先提出是由2015的Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift一文,从这个题目可以看到它的motivation是为了解决 Internal Covariate Shift。
- 内部协变转移(Internal Covariate Shift)是指神经网络在更新参数后各层输入的分布会发生变化,这使得后一层网络需要不停的适应这种分布变化,这便会降低网络的收敛速度。同时,这种变化的不断累积使模型容易陷入激活函数的饱和区,可能产生梯度消失/爆炸,从而给训练带来困难。所以在BN出现之前,较小的学习率和特定的权重初始化是必要的。
BN通过批归一化的操作,即对mini-batch的数据进行归一化为均值为 0、方差为 1 的正态分布,这就使得每一层神经网络的输入保持相同的分布,从而可以使用大学习率加速收敛,也不用特别设计权重初始化,Dropout,L2和weight decay也可以设置小甚至不用的。有放在激活函数前或者激活函数后两种方法,一般放在激活函数后比价常见。 x ′ ( k ) = x ( k ) − E [ x ( k ) ] V a r [ x ( k ) ] x^{'(k)}=\frac{x^{(k)}-E[x^{(k)}]}{\sqrt{Var[x^{(k)}]}} x′(k)=Var[x(k)]x(k)−E[x(k)]对mini-batch计算 的均值和方差就行了,其中k是特征的维度,即BN其实是对每个维度进行归一化的。
用的比较多的主要变体有:Batch Norm(BN)、Layer Norm(LN)、Instance Norm(IN)和 Group Norm(GN)。
- LN:在Transformer中就用到了,前面也提到过,BN主要受制有两点1.因为它是算当前 batch 的均值和方差,所以受制与batch size,但是size小了没意义,大了受硬件的影响(MoCo也主要是改进的这一点) 2.BN适合固定的网络如cnn,对于rnn的话由于句子长度不一样就不太好,所以那为什么不就直接对句子本身进行归一化呢?所以LN就是直接对样本本身进行归一化。
- IN:针对Channel的归一化,在处理图像时一般会有很多的通道,所以直接用每一个通道去计算均值和方差。
- GN:GN 主要是针对 batch 过小而导致统计值不准的缺点进行优化。即在这种情况下,把通道也拿来一起分组一起算,即分组数*通道数=特征数。
BN对应解决梯度问题性能优异,已经成为网络标配,但是在NIPS18’时被MIT打脸,文章自How Does Batch Normalization Help Optimization? 作者认为BN 优化训练的并不是因为缓解了 ICS(就算分布都归一化为0,1这些分布也不一定是相同的分布),而是和ResNet一样对目标函数空间增加了平滑约束loss landscape,从而使得利用更大的学习率获得更好的局部优解。
Transformer 的未来
- 模型效率。由于self-attention模块的计算,存储复杂度都很高,让Transformer在处理长序列数据时效率较低。主要的解决方法是对Attention做轻量化以及引入分治思想,博主已经整理过了,传送门。
- 模型泛化。Transformer没有像CNN引入归纳偏置,导致其在小规模数据集上难以训练。解决方法是引入结构偏置或正则项,在大数据集上进行预训练等等。
- 模型适应能力。这方面工作主要是将Transformer引入下游任务中。