Attention出自NMT(神经网络机器翻译)以处理文本对齐问题,目前已经在各个领域发光发彩,玩出各种花样带出多少文章。而Attention的本质其实就是–加权重。
通用的NMT的架构如上图所示,其中会由两个Deep LSTM做encoder 和 decoder。( NMT大部分以Encoder-Decoder结构为基础结构,而且特别喜欢bidirectional,但它无法适应在线的场景,所以目前为止RNN系列在NLP领域中是淘汰趋势,基本上都可以用transformer做代替了)而文本对齐的问题是 对输入的一个句子对,这个句子对中相对应部分的映射,比如
那么每个单词之间应该如何实现这种对应(特别是这种输入输出双方都不定长),即在翻译“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作贡献,而且每一时刻都会做这样的动态计算,很简单,详细公式如上图。
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,很缤纷的程度了。
Attention in CNN
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。
#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)
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,如上图的模型结构。
而多套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
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的区别?
从LayerNorm的优点来看,它对于batch大小是健壮的,并且在样本级别而不是batch级别工作得更好。
(实际上BN后的输出,经过网络层后,仍然不再是归一化的了。然后不断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不同
为什么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)
为什么要多头 multi-head
类似CNN多通道,从多个角度以增强信息,利于捕捉更丰富的特征(特别是自从Transformer逐渐日常化后,不同的Transformer所侧重的点确实有很大的不同)。而且,可以并行,时间效率上差别并不大。
Transformer的时间复杂度
LSTM是序列长度 x hidden2,Transformer是序列长度2 x hidden。当hidden大于序列长度时(往往都是这种情况),Transformer比LSTM要快很多。
Adam优化的局限性
虽然Adam有自适应的学习率有助于模型快速收敛,但结果的泛化能力学不如SGD。这可能是因为在初期学习率的设置上,太小了在训练初期的偏差会比较大,太大了有可能收敛不到最佳。(解决:可以用学习率预热。或者AdamW使用了L2正则,这样小的权重泛化性能会更好)
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/