Transformer模型简要分析(上篇)(代码来源D2l)

Transformer模型简要分析(上篇)(代码来源D2l)

ps:学习过程中的记录,现抽时间将应答的transformer模型学习流程简单描述如下,能力有限,若有错误,还请不吝赐教 : )
代码运行环境:jupyter notebook

下一篇:Transformer模型简要分析(中篇)(代码来源D2l)

1.注意力提示

受试者基于非自主性提示自主性提示有选择地引导注意力的焦点。

  • 非自主性提示是基于环境中物体的突出性和易见性(封面艳丽的书更容易吸引受试者注意力)
  • 自主性提示指依赖于任务的意志提示(主观想读一本书)

2.查询,键,与值

是否包含自主性提示将注意力机制与全连接层或汇聚层区别开来。

  • 在注意力机制的背景下,自主性提示被称为查询(query)
  • 给定任何查询,注意力机制通过注意力汇聚(attention pooling)将选择引导至感官输入(sensory inputs,例如中间特征表示)
  • 注意力机制中,这些感官输入被称为值(value)

图片来源:https://zh-v2.d2l.ai/chapter_attention-mechanisms/attention-cues.htmlAlt

注意力机制通过注意力汇聚将查询(自主性提示)和键(非自主性提示)结合在一起,实现对值(感官输入)的选择倾向。

3.注意力可视化

考虑每个查询与相应的键值对的匹配关系,并以热值图展示
D2l代码:

import torch
from d2l import torch as d2l
#matrices的形状是 (要显示的行数,要显示的列数,查询的数目,键的数目)
#@save
def show_heatmaps(matrices, xlabel, ylabel, titles=None, figsize=(2.5, 2.5),
                  cmap='Reds'):
    """显示矩阵热图"""
    #在Jupyter Notebook中显示SVG(可缩放矢量图形)格式的图像
    d2l.use_svg_display()
    num_rows, num_cols = matrices.shape[0], matrices.shape[1]
    fig, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize,
                                 sharex=True, sharey=True, squeeze=False)
    for i, (row_axes, row_matrices) in enumerate(zip(axes, matrices)):
        for j, (ax, matrix) in enumerate(zip(row_axes, row_matrices)):
            pcm = ax.imshow(matrix.detach().numpy(), cmap=cmap)
            if i == num_rows - 1:
                ax.set_xlabel(xlabel)
            if j == 0:
                ax.set_ylabel(ylabel)
            if titles:
                ax.set_title(titles[j])
    fig.colorbar(pcm, ax=axes, shrink=0.6);
#测试如下
attention_weights = torch.eye(10).reshape((1, 1, 10, 10))
show_heatmaps(attention_weights, xlabel='Keys', ylabel='Queries')

在这里插入图片描述

4.Nadaraya-Watson 核回归

考虑一个具有注意力机制的机器学习例子,生成数据集 {(𝑥1,𝑦1),…,(𝑥𝑛,𝑦𝑛)},学习 f来预测任意新输入 𝑥 的输出 𝑦̂ =𝑓(𝑥),其中生成公式如下:
y i = 2 sin ⁡ ( x i ) + x i 0.8 + ϵ y_i = 2\sin(x_i) + x_i^{0.8} + \epsilon yi=2sin(xi)+xi0.8+ϵ
ϵ \epsilon ϵ服从均值为 0 0 0和标准差为 0.5 0.5 0.5的正态分布。

'''
author:d2l
resource:https://zh-v2.d2l.ai/chapter_attention-mechanisms/nadaraya-waston.html
'''
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)  # 测试样本数
#plot_kernel_reg绘制所有的训练样本(样本由圆圈表示), 不带噪声项的真实数据生成函数 𝑓(标记为“Truth”), 以及学习得到的预测函数(标记为“Pred”)
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);

流程分析:首先生成五十个训练样本对(x_train,y_train )其中x_train 采用随机生成的方式在0~5区间生成50个,然后再将x_train用于生成带有噪声的 y_train ,可以理解成使用50个带有噪声的(x_train,y_train )样本对来近似将生成分布空间进行了描述,故后续使用间距为0.1的从0~5开始的五十个x_test来构建真实的生成分布空间描述y_truth,在预测的环节只需要考虑x_test与价值对(x_train,y_train )的关系,进而得到预测输出,这种实现就是采用了注意力机制,即观察一个x_test中的查询x与(x_train,y_train )的关系——采用查询x与x_train的注意力权重乘以y_train再求和的方式来预测查询x的输出。

4.1平均汇聚

基于平均汇聚来计算所有训练样本输出值的平均值作为f的预测输出:
f ( x ) = 1 n ∑ i = 1 n y i f(x) = \frac{1}{n}\sum_{i=1}^n y_i f(x)=n1i=1nyi

# 创建一个大小为(2,3,4)的随机张量
x = torch.randn(2, 3, 4)
#print(x)
# 定义要重复的次数
repeats = torch.tensor([2, 4])
# 在第0维和第1维上分别重复两次和三次
y = torch.repeat_interleave(x, repeats, dim=0).repeat_interleave(repeats[1], dim=1)
# 输出结果的大小
#print(y)
#生成一个由y_train均值重复n_test次的张量y_hat
y_hat = torch.repeat_interleave(y_train.mean(), n_test)
plot_kernel_reg(y_hat)

plot_kernel_reg函数

黄色圆圈表明添加有噪声的训练样本,而蓝色truth线表明没有添加噪声的样本,将y_train.mean()重复五十次来作为每一次y的预测,可见效果并不好,为一条横线

4.2非参数注意力汇聚

平局汇聚直接使用了训练集y的均值作为预测,忽略了输入x与训练集中xi的关系,故Nadaraya (Nadaraya, 1964)和 Watson (Watson, 1964)提出了一个更好的想法, 根据输入的位置对输出进行加权:
f ( x ) = ∑ i = 1 n K ( x − x i ) ∑ j = 1 n K ( x − x j ) y i , f(x) = \sum_{i=1}^n \frac{K(x - x_i)}{\sum_{j=1}^n K(x - x_j)} y_i, f(x)=i=1nj=1nK(xxj)K(xxi)yi,

其中:K表示核函数,上述公式描述的估计器被称为 Nadaraya-Watson核回归(Nadaraya-Watson kernel regression)

受其启发,重写公式最后得到了我们的键值对注意力公式如下:
f ( x ) = ∑ i = 1 n α ( x , x i ) y i , f(x) = \sum_{i=1}^n \alpha(x, x_i) y_i, f(x)=i=1nα(x,xi)yi,

其中:𝑥是查询,(𝑥𝑖,𝑦𝑖)是键值对,其表示为yi的加权平均,即通过分配x与每一个xi的关系权重,并与yi相乘再相加得到查询x的输出。

对于任何查询,模型在所有键值对注意力权重都是一个有效的概率分布: 它们是非负的,并且总和为1。
考虑高斯核(Gaussian kernel):
K ( u ) = 1 2 π exp ⁡ ( − u 2 2 ) . K(u) = \frac{1}{\sqrt{2\pi}} \exp(-\frac{u^2}{2}). K(u)=2π 1exp(2u2).
带入该核到注意力重写公式中,可得:
f ( x ) = ∑ i = 1 n α ( x , x i ) y i = ∑ i = 1 n exp ⁡ ( − 1 2 ( x − x i ) 2 ) ∑ j = 1 n exp ⁡ ( − 1 2 ( x − x j ) 2 ) y i = ∑ i = 1 n s o f t m a x ( − 1 2 ( x − x i ) 2 ) y i . \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} f(x)=i=1nα(x,xi)yi=i=1nj=1nexp(21(xxj)2)exp(21(xxi)2)yi=i=1nsoftmax(21(xxi)2)yi.

考虑一个事实, 如果一个键 𝑥𝑖越是接近给定的查询 𝑥, 那么分配给这个键对应值𝑦𝑖的注意力权重就会越大, 也就“获得了更多的注意力”,由softmax函数决定,负二分之一决定了分子的指数部分始终小于一,故其最大的值是当且仅当查询x与xi相同时,为e的零次方为1。

注意的是,Nadaraya-Watson核回归是一个非参数模型,这也是本小节非参数的注意力汇聚(nonparametric attention pooling)模型的由来。
代码如下:

#根据公式可知,其具有两个n,故此处要求n_test要与n_train维数相同(均为50)
# X_repeat的形状:(n_test,n_train),
# 每一行都包含着相同的测试输入(例如:同样的查询)
#print(x_test)
#print(x_train)
#此处将x_test中的每一个元素重复50次,然后使用reshape来改写格式成(50*50),其中一行表示一个查询x的50次重复
X_repeat = x_test.repeat_interleave(n_train).reshape((-1, n_train))
#print(X_repeat.shape)
#print(X_repeat - x_train)
#X_repeat - x_train对应了取一个查询x(x_test的扩展),即X_repeat - x_train的每一行的元素都是一个预测f(x)的输入𝑥−𝑥𝑖
# x_train包含着键。attention_weights的形状:(n_test,n_train),
# 每一行都包含着要在给定的每个查询的值(y_train)之间分配的注意力权重
#运算得到每一个查询x的softmax,后面将把这个attention_weights(50*50)的矩阵与y_train(50*1)做矩阵运算得到一个f(X)的预测输出(50*1)
attention_weights = nn.functional.softmax(-(X_repeat - x_train)**2 / 2, dim=1)
# y_hat的每个元素都是值的加权平均值,其中的权重是注意力权重
y_hat = torch.matmul(attention_weights, y_train)
print(attention_weights.shape,y_train.shape)
plot_kernel_reg(y_hat)

在这里插入图片描述

可见附带了有关键值对(xi,yi)的与查询x的关系之后,我们的函数f每一次的预测有了一个明显的走势,其形状近似于truth的形状,证明我们的非参数的注意力汇聚机制可以在平均汇聚的基础上得到一个提升。

其中,注意力的权重展示如下:

#attention_weights.unsqueeze(0).unsqueeze(0)表示对attention_weights增加两个维度变成(1*1*50*50)
#固定Sorted testing inputs时(即固定一个查询x),可以看到当x与Sorted training inputs的值相近时,得到的热值最燃(最亮)
d2l.show_heatmaps(attention_weights.unsqueeze(0).unsqueeze(0),
                  xlabel='Sorted training inputs',
                  ylabel='Sorted testing inputs')

在这里插入图片描述
可见:“查询-键”对越接近, 注意力汇聚的[注意力权重]就越高。

4.3带参数的注意力汇聚

非参数的Nadaraya-Watson核回归具有一致性(consistency)的优点: 如果有足够的数据,此模型会收敛到最优结果。 尽管如此,我们还是可以轻松地将可学习的参数集成到注意力汇聚中。(引用来至D2L)
改写之前的公式,附带上可学习参数w如下:
f ( x ) = ∑ i = 1 n α ( x , x i ) y i = ∑ i = 1 n exp ⁡ ( − 1 2 ( ( x − x i ) w ) 2 ) ∑ j = 1 n exp ⁡ ( − 1 2 ( ( x − x j ) w ) 2 ) y i = ∑ i = 1 n s o f t m a x ( − 1 2 ( ( x − x i ) w ) 2 ) y i . \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} f(x)=i=1nα(x,xi)yi=i=1nj=1nexp(21((xxj)w)2)exp(21((xxi)w)2)yi=i=1nsoftmax(21((xxi)w)2)yi.

4.3.1小批量矩阵乘法

考虑批量矩阵的乘法效率,设第一个小批量数据包含 𝑛个矩阵 𝐗1,…,𝐗𝑛 ,形状为 𝑎×𝑏,第二个小批量包含 𝑛个矩阵 𝐘1,…,𝐘𝑛,形状为 𝑏×𝑐。它们的批量矩阵乘法得到 𝑛个矩阵 𝐗1𝐘1,…,𝐗𝑛𝐘𝑛,形状为 𝑎×𝑐。因此,[假定两个张量的形状分别是(𝑛,𝑎,𝑏)和 (𝑛,𝑏,𝑐), 它们的批量矩阵乘法输出的形状为 (𝑛,𝑎,𝑐)]。(引用来自D2l)

weights = torch.ones((2, 10)) * 0.1
values = torch.arange(20.0).reshape((2, 10))
#print(weights.unsqueeze(1).shape)
#print(values.unsqueeze(-1).shape)
#添加上维度使之能够完成张量乘法操作
#torch.Size([2, 1, 10])
#torch.Size([2, 10, 1])
torch.bmm(weights.unsqueeze(1), values.unsqueeze(-1))
4.3.2带参数注意力汇聚模型定义
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)
4.3.3训练

接下来,[将训练数据集变换为键和值]用于训练注意力模型。 在带参数的注意力汇聚模型中, 任何一个训练样本的输入都会和除自己以外的所有训练样本的“键-值”对进行计算(去除自相关性), 从而得到其对应的预测输出。 通过与其他样本计算注意力,模型可以学习到每个样本对于整体任务的重要程度,并且可以根据不同样本之间的相似性和相关性进行加权。这种全局上下文信息的交互使模型能够更好地捕捉到数据之间的依赖关系和语义信息,从而提高模型的性能和泛化能力。

总结来说,通过与除自己以外的所有训练样本进行注意力计算,带参数的注意力汇聚模型可以从全局角度整合信息,对于每个样本进行更准确的建模和预测。这种全局信息交互对于解决复杂的序列建模和语义理解任务非常有价值。(引用来自D2l)

# 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))
print('掩蔽矩阵:',(1 - torch.eye(n_train)).type(torch.bool))
#此处使用掩蔽矩阵将X_tile切割成('n_train','n_train'-1)形状,去掉了与自己相关的那一项(即对角线上的)
# keys的形状:('n_train','n_train'-1)
keys = X_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))
print('X_tile:',X_tile)
print('keys:',keys)
# values的形状:('n_train','n_train'-1)
values = Y_tile[(1 - torch.eye(n_train)).type(torch.bool)].reshape((n_train, -1))

掩蔽矩阵: tensor([[False, True, True, …, True, True, True],
[ True, False, True, …, True, True, True],
[ True, True, False, …, True, True, True],
…,
[ True, True, True, …, False, True, True],
[ True, True, True, …, True, False, True],
[ True, True, True, …, True, True, False]])

X_tile: tensor([[0.0218, 0.0588, 0.1359, …, 4.7913, 4.8449, 4.9028],
[0.0218, 0.0588, 0.1359, …, 4.7913, 4.8449, 4.9028],
[0.0218, 0.0588, 0.1359, …, 4.7913, 4.8449, 4.9028],
…,
[0.0218, 0.0588, 0.1359, …, 4.7913, 4.8449, 4.9028],
[0.0218, 0.0588, 0.1359, …, 4.7913, 4.8449, 4.9028],
[0.0218, 0.0588, 0.1359, …, 4.7913, 4.8449, 4.9028]])

keys: tensor([[0.0588, 0.1359, 0.1771, …, 4.7913, 4.8449, 4.9028],
[0.0218, 0.1359, 0.1771, …, 4.7913, 4.8449, 4.9028],
[0.0218, 0.0588, 0.1771, …, 4.7913, 4.8449, 4.9028],
…,
[0.0218, 0.0588, 0.1359, …, 4.7117, 4.8449, 4.9028],
[0.0218, 0.0588, 0.1359, …, 4.7117, 4.7913, 4.9028],
[0.0218, 0.0588, 0.1359, …, 4.7117, 4.7913, 4.8449]])

训练过程使用平方损失函数和随机梯度下降

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])
#每一个训练epoch遍历一次所有的训练集
for epoch in range(5):
	#清除上一次epoch积累的梯度
    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()))

在这里插入图片描述
接下来,用训练好的模型进行测试集拟合匹配,代码如下:

# keys的形状:(n_test,n_train),每一行包含着相同的训练输入(例如,相同的键)
keys = x_train.repeat((n_test, 1))
# value的形状:(n_test,n_train)
values = y_train.repeat((n_test, 1))
#代码首先通过 net(x_test, keys, values) 将这三个输入传入模型 net 中进行计算,
#得到输出结果。然后使用 unsqueeze(1) 在结果的维度1上增加一个维度,
#这可以将结果从形状为 (batch_size, ...)  
#变为 (batch_size, 1, ...),
#其中 batch_size 表示批处理的样本数量。最后使用 detach() 方法将结果断开梯度连接,
#即将结果的梯度信息设置为不可训练,这一步通常用于提高计算效率或防止梯度传播到之前的部分。
#总的来说,代码 net(x_test, keys, values).unsqueeze(1).detach() 的作用是对输入进行模型计算,并
#对计算结果进行维度变换和断开梯度连接。具体的目的和用途需要根据上下文来确定。

#此处使用x_test, keys, values来计算net,其中训练阶段去掉了某训练查询x与其自身的对应位置元素,即keys的形状:('n_train','n_train'-1)
#此处在训练好的net(具有w参数)基础上使用测试集,来考察每一个测试查询与其他样本的关系(包括自身),即keys的形状:('n_train','n_train')
print(keys.shape)
y_hat = net(x_test, keys, values).unsqueeze(1).detach()
plot_kernel_reg(y_hat)

在这里插入图片描述
结果显示,我们的预测曲线十分的不光滑,现给出相应的注意力热值图如下:

print(net.attention_weights.unsqueeze(0).unsqueeze(0).shape)
d2l.show_heatmaps(net.attention_weights.unsqueeze(0).unsqueeze(0),
                  xlabel='Sorted training inputs',
                  ylabel='Sorted testing inputs')

在这里插入图片描述
与非参数的注意力汇聚模型相比, 带参数的模型加入可学习的参数后, [曲线在注意力权重较大的区域变得更不平滑]

很好理解,在应用了参数的注意力机制之后,学习的参数针对注意力权重较大的地方具有更高的关注度,故当出现符合注意力权重匹配的查询时,模型“显得很高兴将会剧烈地跳跃“。

4.3.4反思

在带参数的注意力汇聚实验中,学习得到的参数 𝑤 的价值体现在以下几个方面:

模型性能提升:通过训练得到最优的参数 𝑤,可以使带参数的注意力汇聚模型在特定任务上表现出最佳性能。参数 𝑤 的调整可以使模型更好地对输入数据进行建模、捕捉数据之间的关系,并更准确地进行预测或分类。因此,学习得到的参数 𝑤 可以直接影响模型的性能,提高预测精度或任务完成度。

模型泛化能力:合适的参数 𝑤 可以提高模型的泛化能力,使其在未见过的数据上也能表现较好的性能。通过在训练集上进行参数学习,模型可以学习到数据的普遍规律和特征,从而更好地应对类似的新数据。学习得到的参数 𝑤 能够在一定程度上保持模型对于未知数据的适应性,增强模型的泛化能力。

知识表示与可解释性:学习得到的参数 𝑤 反映了模型对输入数据的理解和表示方式。通过分析参数 𝑤,可以了解模型对于不同输入特征或信息的重要性和关联度。这对于解释模型的决策过程、理解模型是如何将注意力集中在不同的位置或实体上具有重要意义。参数 𝑤 的价值体现在其帮助我们更好地理解模型内部的工作机制和知识表示方式。
总结来说,学习得到的参数 𝑤 在带参数的注意力汇聚实验中的价值体现在模型性能提升、泛化能力增强以及对模型工作机制的解释与理解上。这些参数能够指导模型在特定任务上更好地学习和推断,从而为解决复杂的实际问题提供支持。

下一篇:Transformer模型简要分析(中篇)(代码来源D2l)

PS:感谢《动手学深度学习DIVE INTO DEEP LEARNING》提供的开源线上代码和阅读教程,在此对一切为了开源事业造福人类的工作人员们表示衷心的感谢!本文可随意分享,您的支持将是作者最大的更新动力,分享转载请备注来源,行文仓促,若有错误,还请不吝赐教,谢谢!

作者:寒秋夜未央

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值