深度学习 Transformer 笔记

本文最后更新于:2023年11月17日 上午

Attention

概念

  • 随意线索
    • 有意识有目的的
  • 非随意线索
    • 非意识到

注意力机制

  • 卷积、全链接、池化层斗志考虑不随意线索

  • 注意力机制则显示地考虑随意线索

    • 随意线索被称为Query
    • 每个输入都是一个值(value)和不随意线索(key)的对
    • 通过注意力池化层来偏向性地选择某些输入
  • Query (key) = values

  • 非参注意力池化层(不要学参数)

    • 给定数据 \((x_i,y_i),i=1,...n\)
    • 平均池化
      • 最简单
      • \(f(x)=\frac{1}{n}\sum_iy_i\)
    • Nadaraya-Watson核回归
      • 更好的方案
      • \(f(x) = \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i,\)
      • 给定一个\(x\) (query),计算最近的\(x_j\),选出\(x_j\)对应的\(y_i\)\(f(x)\)
      • 高斯核\(K(u) = \frac{1}{\sqrt{2\pi}} \exp(-\frac{u^2}{2}).\)
      • \(\begin{split}\begin{aligned} f(x) &=\sum_{i=1}^n \alpha(x, x_i) y_i\\ &= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}(x - x_i)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}(x - x_j)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}(x - x_i)^2\right) y_i. \end{aligned}\end{split}\)
  • 参数化的注意力机制

    • 在之前的基础上,引入可以学习的\(w\)
    • \(\begin{split}\begin{aligned}f(x) &= \sum_{i=1}^n \alpha(x, x_i) y_i \\&= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}((x - x_i)w)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}((x - x_j)w)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}((x - x_i)w)^2\right) y_i.\end{aligned}\end{split}\)

高维度

  • 假设有一个查询\(\mathbf{q} \in \mathbb{R}^q\)\(m\) 个“键-值”对\((\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)\) , 其中\(\mathbf{k}_i \in \mathbb{R}^k\)\(\mathbf{v}_i \in \mathbb{R}^v\)。 注意力汇聚函数就被表示成值的加权和:

\[f(\mathbf{q}, (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)) = \sum_{i=1}^m \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i \in \mathbb{R}^v\]

\[\alpha(\mathbf{q}, \mathbf{k}_i) = \mathrm{softmax}(a(\mathbf{q}, \mathbf{k}_i)) = \frac{\exp(a(\mathbf{q}, \mathbf{k}_i))}{\sum_{j=1}^m \exp(a(\mathbf{q}, \mathbf{k}_j))} \in \mathbb{R}.\]

  • \(\alpha\):计算权重的函数
  • \(a\):计算\(q,k_j\)相关度的函数(可以是距离)

注意力函数设计

Additive Attention

  • 加性注意力

\[a(\mathbf q, \mathbf k) = \mathbf w_v^\top \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R},\]

  • 其中可学习的参数是\(\mathbf W_q\in\mathbb R^{h\times q}\)\(\mathbf W_k\in\mathbb R^{h\times k}\)\(\mathbf w_v\in\mathbb R^{h}\)。将查询和键连结起来后输入到一个多层感知机(MLP)中, 感知机包含一个隐藏层,其隐藏单元数是一个超参数。 通过使用作为激活函数,并且禁用偏置项。

  • keyquery拼起来,放到一个隐藏层中去计算

  • 好处:

    • keyvalue可以是任意长度

Scale Dot-Product Attention

  • 如果querykey都是同样的长度\(d\),

  • \(\mathbf Q\in\mathbb R^{n\times d}\),\(\mathbf K\in\mathbb R^{m\times d}\)

  • \(a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}.\)

  • 向量化版本

    • nQuerymkeyvalue
    • \(Q\in\mathbb R^{n\times d},K\in\mathbb R^{m\times d},V\in\mathbb R^{m\times v}\)
    • 注意力分数:\(a(Q,K)=QK^T/\sqrt{d}\in \mathbb R^{n\times m}\),第i行表示第iQuery的权重
    • 注意力池化:\(f=softmax(a(Q,K))V\in\mathbb R^{n\times v}\)

代码

注意力机制

1
2
3
import torch
from torch import nn
from d2l import torch as d2l
1
2
3
4
5
6
7
8
9
10
11
n_train = 50  # 训练样本数
x_train, _ = torch.sort(torch.rand(n_train) * 5) # 排序后的训练样本

def f(x):
return 2 * torch.sin(x) + x**0.8

y_train = f(x_train) + torch.normal(0.0, 0.5, (n_train,)) # 训练样本的输出
x_test = torch.arange(0, 5, 0.1) # 测试样本
y_truth = f(x_test) # 测试样本的真实输出
n_test = len(x_test) # 测试样本数
n_test
1
2
3
4
def plot_kernel_reg(y_hat):
d2l.plot(x_test, [y_truth, y_hat], 'x', 'y', legend=['Truth', 'Pred'],
xlim=[0, 5], ylim=[-1, 5])
d2l.plt.plot(x_train, y_train, 'o', alpha=0.5);
1
2
y_hat = torch.repeat_interleave(y_train.mean(), n_test)
plot_kernel_reg(y_hat)
1
2
3
4
5
6
7
8
9
# X_repeat的形状:(n_test,n_train),
# 每一行都包含着相同的测试输入(例如:同样的查询)
X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train))
# x_train包含着键。attention_weights的形状:(n_test,n_train),
# 每一行都包含着要在给定的每个查询的值(y_train)之间分配的注意力权重
attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, dim=1)
# y_hat的每个元素都是值的加权平均值,其中的权重是注意力权重
y_hat = torch.matmul(attention_weights, y_train)
plot_kernel_reg(y_hat)
1
2
3
d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0),
xlabel='Sorted training inputs',
ylabel='Sorted testing inputs')
1
2
3
4
X = torch.ones((2, 1, 4))
Y = torch.ones((2, 4, 6))
torch.bmm(X, Y).shape
# torch.Size([2, 1, 6])

1
2
3
4
5
weights = torch.ones((2, 10)) * 0.1
values = torch.arange(20.0).reshape((2, 10))
torch.bmm(weights.unsqueeze(1), values.unsqueeze(-1))
#tensor([[[ 4.5000]],
#[[14.5000]]])

1
2
3
4
5
6
7
8
9
10
11
12
13
class NWKernelRegression(nn.Module):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.w = nn.Parameter(torch.rand((1,), requires_grad=True))

def forward(self, queries, keys, values):
# queries和attention_weights的形状为(查询个数,“键-值”对个数)
queries = queries.repeat_interleave(keys.shape[1]).reshape((-1, keys.shape[1]))
self.attention_weights = nn.functional.softmax(
-((queries - keys) * self.w)**2 / 2, dim=1)
# values的形状为(查询个数,“键-值”对个数)
return torch.bmm(self.attention_weights.unsqueeze(1),
values.unsqueeze(-1)).reshape(-1)
1
2
3
4
5
6
7
8
# X_tile的形状:(n_train,n_train),每一行都包含着相同的训练输入
X_tile = x_train.repeat((n_train, 1))
# Y_tile的形状:(n_train,n_train),每一行都包含着相同的训练输出
Y_tile = y_train.repeat((n_train, 1))
# keys的形状:('n_train','n_train'-1)
keys = X_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
# values的形状:('n_train','n_train'-1)
values = Y_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
1
2
3
4
5
6
7
8
9
10
11
12
net = NWKernelRegression()
loss = nn.MSELoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=0.5)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, 5])

for epoch in range(5):
trainer.zero_grad()
l = loss(net(x_train, keys, values), y_train)
l.sum().backward()
trainer.step()
print(f'epoch {epoch + 1}, loss {float(l.sum()):.6f}')
animator.add(epoch + 1, float(l.sum()))
1
2
3
4
5
6
7
# keys的形状:(n_test,n_train),每一行包含着相同的训练输入(例如,相同的键)
keys = x_train.repeat((n_test, 1))
# value的形状:(n_test,n_train)
values = y_train.repeat((n_test, 1))
y_hat = net(x_test, keys, values).unsqueeze(1).detach()
plot_kernel_reg(y_hat)

1
2
3
d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0),
xlabel='Sorted training inputs',
ylabel='Sorted testing inputs')

注意力函数

加性注意力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#@save
class AdditiveAttention(nn.Module):
"""加性注意力"""
def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
super(AdditiveAttention, self).__init__(**kwargs)
self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
self.w_v = nn.Linear(num_hiddens, 1, bias=False)
self.dropout = nn.Dropout(dropout)

def forward(self, queries, keys, values, valid_lens):
queries, keys = self.W_q(queries), self.W_k(keys)
# 在维度扩展后,
# queries的形状:(batch_size,查询的个数,1,num_hidden)
# key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
# 使用广播方式进行求和
features = queries.unsqueeze(2) + keys.unsqueeze(1)
features = torch.tanh(features)
# self.w_v仅有一个输出,因此从形状中移除最后那个维度。
# scores的形状:(batch_size,查询的个数,“键-值”对的个数)
scores = self.w_v(features).squeeze(-1)
self.attention_weights = masked_softmax(scores, valid_lens)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
return torch.bmm(self.dropout(self.attention_weights), values)
缩放点乘积注意力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#@save
class DotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, dropout, **kwargs):
super(DotProductAttention, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)

# queries的形状:(batch_size,查询的个数,d)
# keys的形状:(batch_size,“键-值”对的个数,d)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
# valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
def forward(self, queries, keys, values, valid_lens=None):
d = queries.shape[-1]
# 设置transpose_b=True为了交换keys的最后两个维度
scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
self.attention_weights = masked_softmax(scores, valid_lens)
return torch.bmm(self.dropout(self.attention_weights), values)

总结

  • 几个概念

    • Query、Key、Value
    • \(Attention(Query,Source) = \sum_{i=1}^{L_x}Similarity(Query,Key_i)*Value_i\)
      • 简单地说就是计算QueryKey的相似程度作为对应Value的权重进行加权
  • 计算阶段

    • 根据QueryKey计算权重系数

      • 根据QueryKey计算两者的相似性或相关性
      • 对第一阶段的原始分值进行归一化处理
    • 根据权重系数对value进行加权求和

      img
  • 注意力分数是querykey的相似度,注意力全中是分数softmax的结果

  • 两种常见的注意力分数计算方式

    • querykey合并起来进入一个单输出单隐藏层的MLP
    • 直接将querykey做内积

加入Attention的Seq2Seq

核心

  • 传统的Seq2Seq
    • 用上一层的Hidden状态预测下一个
  • Attention的Seq2Seq
    • encoder层建立了输入和输出对应隐状态的Key和Value Pair
    • decoder层用上一时刻的输出作为Query去拿这次输出应该对应的隐状态
    • Key和Value是对RNN编码器的每一个词的输出
    • Query是解码器对上一个词的预测输出
image-20230805141434888

总结

  • Seq2Seq通过隐状态在编码器和解码器中传递信息
  • 注意力机制可以根据解码器RNN的输入来匹配到合适的编码器RNN的输出来更有效的传递信息

代码

1
2
3
4
5
6
7
8
9
# 带有注意力机制的编码器基本接口

class AttentionDecoder(d2l.Decoder):
def __init__(self, **kwargs):
super(AttentionDecoder,self).__init__(**kwargs)

@property
def attention_weights(self):
raise NoImplementedError
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
# 带有Bahdanau注意力的循环神经网络解码器

class Seq2SeqAttentionDecoder(AttentionDecoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
super(Seq2SeqAttentionDecoder,self).__init__(**kwargs):
# Query,Key,Value都一样,可以用自乘和自加注意力。这里用自加,方便学习到参数
self.attention = d2l.AdditiveAttention(num_hiddens, num_hiddens, num_hiddens, dropout)
# 其他定义和普通Seq2Seq一样
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,dropout=dropout)
self.dense = nn.Linear(num_hiddens, vocab_size)

def init_state(self, enc_outputs, enc_valid_lens, **args):
# encoder输出,最后一次输出的隐藏层状态
outputs, hidden_state = enc_outputs
return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)

def forward(self, X, state):
enc_outputs, hidden_state, enc_valid_lens = state
X = self.embedding(X).permute(1, 0 ,2)
outputs, self._attention_weights = [],[]
for x in X:
# 拿到上一个输出的隐状态
query = torch.unsequeeze(hidden_state=[-1], dim=1)
# 核心 计算context 每一步根据Query重新计算Context
context = self.attention(query, enc_outputs, enc_outputs, enc_valid_lens)

x = torch.cat((context,torch.unsqueeze(x, dim=1)),dim=-1)

out, hidden_state = self.rnn(x.permute(1, 0,2),hidden_sate)
outputs.append(out)



深度学习 Transformer 笔记
https://anonymouslosty.ink/2023/07/31/深度学习 Transformer 笔记/
作者
Ling yi
发布于
2023年7月31日
更新于
2023年11月17日
许可协议