Transformer

不会NLP的人记录的Transformer笔记。

Seq2Seq

普通Seq2Seq

以翻译来说,就是中文通过编码器得到了一个上下文编码向量,然后通过解码器就可以翻译为英文

编码器和解码器在早期是可以获取时序信息的RNN模块,后来引入了LSTM【输入/遗忘/输出门】、GRU【LSTM的变种,更新/重置门】模块【通过门来控制选择保留的特征】

缺点

  • 上下文编码向量是固定长度的,如果长度过长,会导致不合理
  • 当时间过长,仍可能会出现信息丢失的问题

基于Attention的Seq2Seq

增加注意力机制,通过对输入的自适应加权,聚焦到关键信息。

流程

编码

大概流程就是将Query(向量)和Key(和Q长度相同的向量)计算相似性,再通过softmax得到相似性的概率权重分布。对应权重value(和Q长度相同的向量),最后相加,就可以得到包含注意力的attention值输出。

看到的一个例子:

  • 假设世界上所有小吃都可以被标签化,例如微辣、特辣、变态辣、微甜、有嚼劲….,总共有1000个标签,现在我想要吃的小吃是[微辣、微甜、有嚼劲],这就是我的Query,有没有一个微辣的、微甜的且有嚼劲的食物呀?
  • 每个店铺卖的东西不一样,但是肯定可以被标签化,例如第一家小吃被标签化后是[微辣、微咸],第二家小吃被标签化后是[特辣、微臭、特咸],第二家小吃被标签化后是[特辣、微甜、特咸、有嚼劲],其余店铺都可以被标签化,每个店铺的标签就是Keys,但是每家店铺由于卖的东西不一样,单品种类也不一样,所以被标签化后每一家的标签List不一样长【但是描述这个特征的维度必须相同】
  • Values就是每家店铺对应的单品,例如第一家小吃的Values是[烤羊肉串、炒花生]
  • 将Query和所有的Keys进行一一比对,相当于计算相似性,此时就可以知道我想买的小吃和每一家店铺的匹配情况,最后有了匹配列表,就可以去店铺里面买东西了(Values和相似性加权求和)。最终的情况可能是,我在第一家店铺买了烤羊肉串,然后在第10家店铺买了个玉米,最后在第15家店铺买了个烤面筋。最终去不同店铺买不同东西的过程就是权重和Values加权求和过程。
1
2
3
4
5
6
7
# 假设q是(1,N,512),N就是最大标签化后的list长度,k是(1,M,512),M可以等于N,也可以不相等
# (1,N,512) x (1,512,M)-->(1,N,M)
attn = torch.matmul(q, k.transpose(2, 3))
# softmax转化为概率,输出(1,N,M),表示q中每个n和每个m的相关性
attn=F.softmax(attn, dim=-1)
# (1,N,M) x (1,M,512)-->(1,N,512),V和k的shape相同
output = torch.matmul(attn, v)

解码

  • 将第一个开启解码标志<START>(也就是Q)与编码器的每一个时间步的隐含状态(一系列Key和Value)进行点乘计算相似性得到每一时间步的相似性分数
  • 通过softmax转化为概率分布
  • 将概率分布和对应位置向量进行加权求和得到新的上下文向量
  • 将上下文向量输入解码器中进行解码输出

缺点

  • 不管是采用RNN、LSTM还是GRU都不利于并行训练和推理,因为相关算法只能从左向右依次计算或者从右向左依次计算
  • 长依赖信息丢失问题,顺序计算过程中信息会丢失,虽然LSTM号称有缓解,但是无法彻底解决

基于transformer的Seq2Seq

Attention

解决的问题

  • 前馈网络和循环网络虽然具有很强的能力,但是因为
    • 需要记住很多“信息”,使得模型变得更复杂,同时计算能力限制了它的继续发展
    • 虽然局部连接、权重共享以及Pooling,让神经网络更加简单,但是信息的“记忆”能力并不高
    • 两者都是局部编码:CNN基于N-gram,RNN由于梯度消失问题只能建立短距离依赖
  • 全连接网络虽然可以获取远距离的信息交互,但是无法处理变长的输入序列;

优势

  • 允许解码器以不同程度的权重利用编码器的所有信息,可以绕过瓶颈
  • 注意力分布可以反映解码器关注什么,只选择一些关键信息,减少模型的复杂度,可解释性强
  • 【自注意力】可以动态地生成不同连接的权重,处理变长的信息序列

分类

聚焦式Focus

是一种自上而下的有意识的注意力,主动注意【依赖于任务】

显著性saliency-based

是一种自下而上的有意识的注意力,被动注意【由外界刺激驱动的注意,可以和任务无关】

Max-Pooling

门控Gating机制

几个类别

Soft attention

所有输入信息在注意力分布下的期望

  • Q和所有K分别计算 【注意力打分机制】

    • 加性模型

    • 点积模型

    • 缩放点积模型

    • 双线性模型

  • 对相似性采用softmax转换为概率分布【注意力概率分布】,表示第i个信息受关注的程度

  • 将概率分布和V进行一一对应相乘【加权平均】

    • 普通模式:Key=Value

    • 键值对模式:Key!= Value

  • 最后相加得到新的和Q一样长的输出向量

Hard attention

只关注到某一个位置上的信息;一般用强化学习来训练

  • 选取最高概率的输入信息
  • 在注意力分布式上随机采样

缺点

  • 最大采样或随机采样导致最终的损失函数与注意力分布之间的函数关系不可导,无法使用反向传播。

Self-attention

主要是输入的不同,QKV都是同一个输入序列的不同线性表示;整体可以表示为:【都是矩阵的乘法操作,可以用GPU加速】

因此时间复杂度为,因为(n, d)*(d, n) -> n^2d, softmax(n, n) -> n^2, (n, n)*(n,d) -> n^2d

  • 自注意力机制的QKV是通过线性变换得到的,X是输入序列;self体现在Q是对自身的一个输入变换【传统的attention中,Q来自于外部】。

  • 通常使用缩放点积作为注意力打分函数(d是输入X的维度)

  • 用softmax得到概率分布

  • 加权平均得到注意力值:

One-head attention

算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class OneHeadAttention(nn.Module):
''' Scaled Dot-Product Attention '''
def __init__(self, temperature, attn_dropout=0.1):
super().__init__()
self.temperature = temperature
self.dropout = nn.Dropout(attn_dropout)
def forward(self, q, k, v, mask=None):
# self.temperature是论文中的d_k ** 0.5,防止梯度过大
# QxK/sqrt(dk)
attn = torch.matmul(q / self.temperature, k.transpose(2, 3))
if mask is not None:
# 屏蔽不想要的输出
attn = attn.masked_fill(mask == 0, -1e9)
# softmax+dropout
attn = self.dropout(F.softmax(attn, dim=-1))
# 概率分布xV
output = torch.matmul(attn, v)
return output, attn

Multi-headed attention

利用多个查询来平行地计算从输入信息中选取多个信息。每个注意力关注输入信息的不同部分,然后再拼接

特点
  • 类似于分组操作,分别得到输出后,将结果concat
  • 加入了残差设计和层归一化,防止梯度消失,加快收敛
优点
  • 扩展了模型专注于不同位置的能力
  • 给出了注意力层多个“表示子空间”,对于“多头”注意力机制,有多个Query/Key/Value权重矩阵集
算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class MultiHeadAttention(nn.Module):
''' Multi-Head Attention module '''
# n_head头的个数,默认是8
# d_model编码向量长度,例如上文的512
# d_k, d_v的值一般会设置为 n_head * d_k=d_model,
# 此时concat后正好和原始输入一样,当然不相同也可以,因为后面有fc层
# 相当于将可学习矩阵分成独立的n_head份
def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
super().__init__()
# 假设n_head=8,d_k=64
self.n_head = n_head
self.d_k = d_k
self.d_v = d_v
# d_model输入向量,n_head * d_k输出向量
# 可学习W^Q,W^K,W^V矩阵参数初始化
self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
# 最后的输出维度变换操作
self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
# 单头自注意力
self.attention = OneHeadAttention(temperature=d_k ** 0.5)
self.dropout = nn.Dropout(dropout)
# 层归一化
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, q, k, v, mask=None):
# 假设qkv输入是(b,100,512),100是训练每个样本最大单词个数
# 一般qkv相等,即自注意力
residual = q
# 将输入x和可学习矩阵相乘,得到(b,100,512)输出
# 其中512的含义其实是8x64,8个head,每个head的可学习矩阵为64维度
# q的输出是(b,100,8,64),kv也是一样
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)
# 变成(b,8,100,64),方便后面计算,也就是8个头单独计算
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1) # For head axis broadcasting.
# 输出q是(b,8,100,64),维持不变,内部计算流程是:
# q*k转置,除以d_k ** 0.5,输出维度是b,8,100,100即单词和单词直接的相似性
# 对最后一个维度进行softmax操作得到b,8,100,100
# 最后乘上V,得到b,8,100,64输出
q, attn = self.attention(q, k, v, mask=mask)
# b,100,8,64-->b,100,512
q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
q = self.dropout(self.fc(q))
# 残差计算
q += residual
# 层归一化,在512维度计算均值和方差,进行层归一化
q = self.layer_norm(q)
return q, attn

Transformer

优点

  • 可解释性强
  • 并行处理,速度快

结构

1
2
3
4
5
6
7
8
Decoder:
Encoder: ^
^ Feed Forward
| |
Feed Forward ----> Encoder-Decoder Attention
| |
Self-Attention Self-Attention
| |

Input

单词嵌入向量和位置编码向量相加就是编码器的输入了(batch, M, 512)

Embedding

Word2Vec

Positional encoding

transformer内部没有类似RNN的循环结构,没有捕捉顺序序列的能力,positional encoding补充了这个缺陷。

  • 网络自动学习

    self.pos_embedding = nn.Parameter(torch.randn(batch, N, 512)); 其中N是N个位置,512是用来表示坐标特征的维度

  • 自定义

    • 直接将绝对坐标编码为向量
    • sin-cos规则:将向量的512维度切分为奇数行和偶数行;奇数行采用cos函数编码、偶数行采用sin函数编码;按照原始行号拼接
      • 由于以及,因此编码后的可以用来表示;这样网络可以较好地学习相对位置信息

Encoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class EncoderLayer(nn.Module):
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(EncoderLayer, self).__init__()
self.self_attention = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(self, enc_input, slf_attn_mask=None):
# Q K V是同一个,自注意力
# enc_input来自源单词嵌入向量或者前一个编码器输出
enc_output, enc_slf_attn = self.self_attention(
enc_input, enc_input, enc_input, mask=slf_attn_mask)
enc_output = self.pos_ffn(enc_output)
return enc_output, enc_slf_attn
class Encoder(nn.Module):
def __init__(
self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, dropout=0.1, n_position=200):
# 词嵌入向量生成(用单词表的信息)
self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx)
# 位置编码
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
# n个编码器层
self.layer_stack = nn.ModuleList([
EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
# 层归一化
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, src_seq, src_mask, return_attns=False):
# 对输入序列进行词嵌入,加上位置编码
enc_output = self.dropout(self.position_enc(self.src_word_emb(src_seq)))
enc_output = self.layer_norm(enc_output)
# 作为编码器层输入
for enc_layer in self.layer_stack:
enc_output, _ = enc_layer(enc_output, slf_attn_mask=src_mask)
return enc_output

Self-attention

QKV来自同一个输入的attention操作就是自注意力。softmax(Q*K^T/\sqrt(d_k))*v=z

Feed Foward

进行空间变换,引入了非线性激活函数,增加了模型的表现能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PositionwiseFeedForward(nn.Module):
''' A two-feed-forward-layer module '''
def __init__(self, d_in, d_hid, dropout=0.1):
super().__init__()
# 两个fc层,对最后的512维度进行变换
self.w_1 = nn.Linear(d_in, d_hid) # position-wise
self.w_2 = nn.Linear(d_hid, d_in) # position-wise
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
residual = x
x = self.w_2(F.relu(self.w_1(x)))
x = self.dropout(x)
x += residual
x = self.layer_norm(x)
return xs

Decoder

解码器的输入不仅包括最后一个后一个编码器的输出,还有前面所有解码器的输入【对于第一个解码器,则还需要额外的输出嵌入向量——目标单词的嵌入向量】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class DecoderLayer(nn.Module):
''' Compose with three layers '''
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(DecoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(
self, dec_input, enc_output,
slf_attn_mask=None, dec_enc_attn_mask=None):
# 标准的自注意力,QKV=dec_input来自目标单词嵌入或者前一个解码器输出
dec_output, dec_slf_attn = self.slf_attn(
dec_input, dec_input, dec_input, mask=slf_attn_mask)
# KV来自最后一个编码层输出enc_output,Q来自带有mask的self.slf_attn输出
dec_output, dec_enc_attn = self.enc_attn(
dec_output, enc_output, enc_output, mask=dec_enc_attn_mask)
dec_output = self.pos_ffn(dec_output)
return dec_output, dec_slf_attn, dec_enc_attn
class Decoder(nn.Module):
def __init__(
self, n_trg_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, n_position=200, dropout=0.1):
# 目标单词嵌入
self.trg_word_emb = nn.Embedding(n_trg_vocab, d_word_vec, padding_idx=pad_idx)
# 位置嵌入向量
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
# n个解码器
self.layer_stack = nn.ModuleList([
DecoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
# 层归一化
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=False):
# 目标单词嵌入+位置编码
dec_output = self.dropout(self.position_enc(self.trg_word_emb(trg_seq)))
dec_output = self.layer_norm(dec_output)
# 遍历每个解码器
for dec_layer in self.layer_stack:
# 需要输入3个信息:目标单词嵌入+位置编码、最后一个编码器输出enc_output
# 和dec_enc_attn_mask,解码时候不能看到未来单词信息
dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
return dec_output

Postion Embedding

与编码器的不同:

  • 解码器的位置嵌入是在进行目标单词嵌入之前;而编码器是在输入单词嵌入之后
  • 解码器需要右移一位,新增加的一个位置采用定义好的标志位BOS_WORD代替【开始解码标志】

Feed Forward

由于想要并行运算,但是我们在第3个编码器的时候,只可以用前两个编码器的输出,而不可以使用后面编码器的结果,否则就是一种作弊的现象。因此用mask遮住了。

屏蔽部分在编码器中已经实现了。

循环解码过程:

  • 第一次解码:输入BOS_WORD单词嵌入向量,假设是(1,256),而编码器输出始终不变是(100,256),那么第一次解码过程是(1,256)+位置编码作为解码器输入,解码输出是(1,256),经过fc层(参数shape是(256,10000))变成(1,10000),10000是单词总数,此时就可以解码得到第一个单词i;
  • 接着将BOS_WORD和i都进行嵌入,得到(2,256)输入,同样运行,输出是(2,256),经过fc是(2,10000),此时不需要第一个维度输出只需要[-1,10000]既可以解码第二个单词,后面就一直迭代直到输出结束解码标注。

Classification Layer

1
2
3
self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False) # 目标单词编码
dec_output, *_ = self.model.decoder(trg_seq, trg_mask, enc_output, src_mask) # 解码
return F.softmax(self.model.trg_word_prj(dec_output), dim=-1) # 选择出对应的特征

与CV的结合

分类

An Image is Worth 16x16 Words:Transformers for Image Recognition at Scale

特点
  • 彻底抛弃CNN,RNN
  • 需要大量的数据进行训练,才会超过CNN,但是对应的训练时间也会增长很多
  • 应用transformer的可解释性强
结构
  • 将图片分块,切分为固定的patch个数

  • 将patches拉成序列(flatten),采用了爱因斯坦表达式(用einops库的rearrange实现)

    1
    x = rearrange(img, 'b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=p, p2=p)
    • 输入为(batch, 3, h, w)
    • 分为n个patch,那么每个patch就是h/sqrt(n), w/sqrt(n))像素
    • rearrange操作即先变成(batch, 3, sqrt(n)*h/sqrt(n), sqrt(n)*w/sqrt(n)),最后变成(batch, n, h*w/n*3)
    • 可以看作有n个单词向量,每个向量由h*w/n*3个元素编码表示
  • 对每一个单词向量的表示进行降维

    1
    2
    self.patch_to_embedding = nn.Linear(patch_dim, dim)
    x = self.patch_to_embedding(x)
  • 在flatten前增加一个开启解码的标志,这个标志是可学习的【在NLP中是固定的】

    1
    2
    3
    4
    self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
    cls_tokens = repeat(self.cls_token, '() n d -> b n d', b=b)
    # 额外追加token,变成(batch, n+1, dim)
    x = torch.cat((cls_tokens, x), dim=1)
    • 因为这篇论文在编码之后就进行分类,没有解码器,因此在这一部分增加了解码的一部分操作
  • 上述是一个图像内容的编码,接下来还需要对位置信息进行编码;在论文中设置为了可学习的;相邻位置具有相近的位置编码变量

    1
    2
    # 加一同样是因为增加了解码标志
    self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
  • 将patch嵌入向量和位置编码向量相加后作为编码器的输入

    1
    2
    3
    x += self.pos_embedding[:, :(n + 1)]
    x = self.dropout(x)
    self.transformer = Transformer(dim, depth, heads, mlp_dim, dropout)
  • 编码器的输出结果,传入FC分类器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    self.mlp_head = nn.Sequential(
    nn.LayerNorm(dim),
    nn.Linear(dim, mlp_dim),
    nn.GELU(),
    nn.Dropout(dropout),
    nn.Linear(mlp_dim, num_classes)
    )
    # 65个输出里面只需要第0个输出进行后续分类即可
    self.mlp_head(x[:, 0])

OD

DETR

见博文OD with new tech.

CS224N

转载请注明出处,谢谢。

愿 我是你的小太阳

买糖果去喽