[ML] 深蓝学院transformer第6节学习笔记

课前问题和部分解答

什么是注意力
怎么运用注意力的结果
为啥需要缩放
kqv

1.点积注意力

就是输入向量的一次点积运算,这里维度有点绕,详细写一下

x1 = torch.randn(2, 3, 4) # 形状(batch_size, seq_len1, feature_dim)
x2 = torch.randn(2, 5, 4) # 形状(batch_size, seq_len2, feature_dim)

创建两个例子,假设第一个维度表示批处理大小(batch_size),第二个维度表示序列长度(seq_len1和seq_len2),第三个维度表示特征维度(feature_dim)

raw_weights = torch.bmm(x1, x2.transpose(1, 2))

用bmm完成矩阵乘法,transpose(1, 2)用于交换张量x2的最后两个维度,为(2,4,5),使维度对齐。结果raw_weights的形状为(2, 3, 5),其中2是批处理大小,3是x1的序列长度,5是x2的序列长度

举一个例子解释一下bmm

import torch


matrix1 = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=torch.float32)
matrix2 = torch.tensor([[[2, 0], [1, 2]], [[-1, 1], [0, -1]]], dtype=torch.float32)

result = torch.bmm(matrix1, matrix2)

print(result)
#结果是:
tensor([[[ 4.,  4.],
         [10.,  8.]],
        [[-5., -1.],
         [-7., -1.]]])

以上两个例子形状为(2,2,2),这里举例使用对称矩阵,不涉及转置,结果也是(2,2,2),运算如下:
在这里插入图片描述

在这里插入图片描述
torch.bmm 将同一个批次中的每个矩阵的第一个矩阵与第二个矩阵进行矩阵乘法,并将结果存储在结果矩阵批次的相应的位置,它允许同时处理多个数据样本,从而提高了计算效率和并行性。在神经网络中的线性变换、注意力机制等操作中经常使用。

attention_weights = F.softmax(raw_weights, dim=2)

这一行代码应用了softmax激活函数函数,将raw_weights中的数值转换为在0到1之间,并且保证每一行的和为1。dim=2表示在第三个维度上进行softmax操作,即对每个seq_len2中的数值进行softmax。

attention_output = torch.bmm(attention_weights, x2)

具体维度为:attention_weights 的形状为2,3,5 (batch_size, seq_len1, seq_len2),而x2 的形状为2,5,4 (batch_size, seq_len2, feature_dim)。这产生了一个形状为 (batch_size, seq_len1, feature_dim) 的输出,得到的attention_output的形状为(2, 3, 4),它表示了经过注意力机制加权后的输出。

缩放点积注意力

如果特征维度大,加权和也就大,softmax激活函数在输入,(这里为:raw_weights )较大区间时候,会出现梯度消失

scaling_factor = x1.size(-1) ** 0.5
scaled_weights = raw_weights  / scaling_factor

就是简单的除特征维度的平方根

编码器解码器注意力

回顾第五节第三部分的内容 第五节

在原有类上加入注意力机制:
上面例子的x1:q在这里是解码器(Decoder)在各个时间步的隐藏状态(可以理解为解码过程中的信息向量)。
上面例子的x2:kv在这里是编码器(Encoder)输出的上下文向量(可以理解为整个输入序列的压缩表示)。而输出的上下文向量优势Enconder的隐藏状态。
让decoder去注意encoder

以下为老师的源代码,在第五节第三部分上加入注意力部分:这里主要给出不同提供的decoder部分和注意力类,并加入自己的理解

# 创建一个Attention类,用于计算注意力权重
class Attention(nn.Module):
    def __init__(self):
        super(Attention, self).__init__()

    def forward(self, decoder_context, encoder_context):
        # 计算decoder_context和encoder_context的点积,得到注意力分数
        scores = torch.matmul(decoder_context, encoder_context.transpose(-2, -1))
        # 归一化分数
        attn_weights = nn.functional.softmax(scores, dim=-1)
        # 将注意力权重乘以encoder_context,得到加权的上下文向量
        context = torch.matmul(attn_weights, encoder_context)
        return context, attn_weights
    



class DecoderWithAttention(nn.Module):
    def __init__(self, hidden_size, output_size):
        super(DecoderWithAttention, self).__init__()
        self.hidden_size = hidden_size # 设置隐藏层大小
        self.embedding = nn.Embedding(output_size, hidden_size) # 创建词嵌入层
        self.rnn = nn.RNN(hidden_size, hidden_size, batch_first=True) # 创建RNN层
        self.attention = Attention()  # 创建注意力层
        self.out = nn.Linear(2 * hidden_size, output_size)  # 修改线性输出层,考虑隐藏状态和上下文向量

    def forward(self, inputs, hidden, encoder_outputs):
        embedded = self.embedding(inputs)  # 将输入转换为嵌入向量
        rnn_output, hidden = self.rnn(embedded, hidden)  # 将嵌入向量输入RNN层并获取输出 
        context, attn_weights = self.attention(rnn_output, encoder_outputs)  # 计算注意力上下文向量
        output = torch.cat((rnn_output, context), dim=-1)  # 将上下文向量与解码器的输出拼接
        output = self.out(output)  # 使用线性层生成最终输出
        return output, hidden, attn_weights
  • 重点的不同点
    这里改变了decoder的线性层输入的维度,输入使用了cat拼接了rnn的输出和加入了权重化处理的context,

问题

  • 这里也依靠了线性层的参数,进行反向梯度传播学习参数,来配合完成使用context嘛?还是只起到整定维度的作用???
  • context, attn_weights = self.attention(rnn_output, encoder_outputs) # 计算注意力上下文向量 为啥用的是这俩不是hidden嘛
 self.attention = Attention()  # 创建注意力层
        self.out = nn.Linear(2 * hidden_size, output_size)  # 修改线性输出层,考虑隐藏状态和上下文向量
context, attn_weights = self.attention(rnn_output, encoder_outputs)  # 计算注意力上下文向量
output = torch.cat((rnn_output, context), dim=-1)  # 将上下文向量与解码器的输出拼接
output = self.out(output)  # 使用线性层生成最终输出
return output, hidden, attn_weights
  • 新的模型类
    对比第五节可以发现 以前不需要编码器的输出,现在需要了
class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder):
        super(Seq2Seq, self).__init__()
        # 初始化编码器和解码器
        self.encoder = encoder
        self.decoder = decoder
    # 定义前向传播函数
    def forward(self, source, hidden, target):
        # 将输入序列通过编码器并获取输出和隐藏状态
        encoder_output, encoder_hidden = self.encoder(source, hidden)
        # 将编码器的隐藏状态传递给解码器作为初始隐藏状态
        decoder_hidden = encoder_hidden
        # 将目标序列通过解码器并获取输出 -  此处更新解码器调用
        decoder_output, _, attn_weights = self.decoder(target, decoder_hidden, encoder_output) 
        return decoder_output, attn_weights

Seq2Seq 的model.forward(self, source, hidden, target):在实际使用时是传入的形参为model(encoder_input, hidden, decoder_input)
decoder.forward(self, inputs, hidden, encoder_outputs)在实际使用时是传入self.decoder(target, decoder_hidden, encoder_output)

qkv

  • x2对x1注意,则那么x2为查询对象q,x1是k和v
  • key为上面例子中第一次点积,分数
  • value为第二次点积,有分数的加权和。
    在这里插入图片描述
    在这里插入图片描述
    decoder注意encoder,我理解decoder是q,encoder的context(enconder的hiddenstate)是kv
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/d38dbfe2e7d146e3ba3986735ef0a6e7.png

一次完整的qkv示例如下,借用了老师的代码

# 1. 创建 Query、Key 和 Value 张量
q = torch.randn(2, 3, 4) # 形状(batch_size, seq_len1, feature_dim)
k = torch.randn(2, 4, 4) # 形状(batch_size, seq_len2, feature_dim)
v = torch.randn(2, 4, 4) # 形状(batch_size, seq_len2, feature_dim)

# 2. 计算点积,得到原始权重,形状为(batch_size, seq_len1, seq_len2)
raw_weights = torch.matmul(q, k.transpose(1, 2))
print("原始权重:", raw_weights)

# 3. 将原始权重进行缩放(可选)
scaling_factor = q.size(-1) ** 0.5
scaled_weights = raw_weights / scaling_factor
print("缩放后权重:", scaled_weights)

# 4. 应用Softmax函数,使结果的值在 0 和 1 之间,且每一行的和为 1
attention_weights = F.softmax(scaled_weights, dim=-1)
print("注意力权重:", attention_weights)

# 5. 与Value相乘,得到注意力分布的加权和, 形状为(batch_size, seq_len1, feature_dim)
attention_outputs = torch.matmul(attention_weights, v)
print("注意力输出:", attention_outputs)

自注意力机制

自己对自己注意,即qkv是同一个输入序列,建立不同位子之间的互相注意

在这里插入图片描述
老师代码给出了两种计算自注意力的代码,这两种方法在核心思想上是相同的,都是通过计算查询(Q)和键(K)之间的相似度得到原始权重,然后使用 softmax 函数进行归一化,最后将注意力权重应用于值(V)向量以得到最终的自注意力加权和。
主要的不同点在于原始权重的计算方式和缩放的处理上:
第一种方法:


raw_weights = torch.bmm(x, x.transpose(1, 2))
attn_weights = F.softmax(raw_weights, dim=2)
attn_outputs = torch.bmm(attn_weights, x)

在这种方法中,原始权重是通过输入张量 x 与其转置的点积计算得到的。这里没有显式地定义查询(Q)、键(K)、值(V)的线性变换,而是直接使用输入张量作为 Q、K、V。

第二种方法:
Q = linear_q(x)
K = linear_k(x)
V = linear_v(x)
raw_weights = torch.bmm(Q, K.transpose(1, 2))
scale_factor = K.size(-1) ** 0.5
scaled_weights = raw_weights / scale_factor
attn_weights = F.softmax(scaled_weights, dim=2)
attn_outputs = torch.bmm(attn_weights, V)

在这种方法中,首先定义了三个线性层 linear_q、linear_k、linear_v,分别用于将输入张量 x 转换为查询(Q)、键(K)、值(V)向量。然后,通过这些转换后的向量计算 Q 和 K 之间的原始权重,并进行了缩放。最后,通过 softmax 函数得到注意力权重,应用于值向量 V。
在第二种方法中,通过线性层进行了显式的 Q、K、V 的计算和缩放,这使得模型更加灵活,可以学习适合任务的表示。而第一种方法直接使用输入张量作为 Q、K、V,可能对于简单任务足够,但在复杂任务中,通过学习参数化的线性变换可能更为有效。

多头注意力机制

在这里插入图片描述
这里和自注意力基本相同,老师定义的分割函数一带而过,但是我研究了好久

# 创建一个形状为 (batch_size, seq_len, feature_dim) 的张量 x
x = torch.randn(2, 3, 4) # (batch_size=2, seq_len=3, feature_dim=4)

# 定义头数和每个头的维度
num_heads = 2
head_dim = 2
# feature_dim 必须是 num_heads * head_dim 的整数倍
assert x.size(-1) == num_heads * head_dim

# 定义线性层用于将 x 转换为 Q, K, V 向量
linear_q = torch.nn.Linear(4, 4)
linear_k = torch.nn.Linear(4, 4)
linear_v = torch.nn.Linear(4, 4)
# 通过线性层计算 Q, K, V
Q = linear_q(x) # (batch_size=2, seq_len=3, Q_dim=4)
K = linear_k(x) # (batch_size=2, seq_len=3, K_dim=4)
V = linear_v(x) # (batch_size=2, seq_len=3, V_dim=4)

我在这里有一个问题:为什么线型变换后维度还是2,3,4
输入 x 的形状为 (batch_size=2, seq_len=3, feature_dim=4)。线性层(例如 linear_q, linear_k, 和 linear_v)都被定义为从维度4映射到维度4,即 torch.nn.Linear(4, 4)。所以,当输入数据经过这些线性层时,它的最后一个维度(特征维度)并没有发生变化,因为输入和输出维度都是4。

但线性变换的关键不在于它是否改变了数据的维度,而在于它如何改变了数据中的值。在这种情况下,虽然输入和输出的形状相同,但它们的值可能已经根据线性层的权重和偏差进行了调整。

如果我定义的是:

linear_q = torch.nn.Linear(4, 8)
linear_k = torch.nn.Linear(4, 8)
linear_v = torch.nn.Linear(4, 8)

那么结果会是:(线性变换不会改变 batch_size 和 seq_len 的长度,它只改变了特征维度,更详细的说:线性变换是一个线性操作,它独立地应用于输入数据的每个元素,而不会改变它们的数量或顺序)


    Q:(batch_size=2, seq_len=3, Q_dim=8)
    K:(batch_size=2, seq_len=3, K_dim=8)
    V:(batch_size=2, seq_len=3, V_dim=8)

定义分割

# 将 Q, K, V 分割成 num_heads 个头
def split_heads(tensor, num_heads):
    batch_size, seq_len, feature_dim = tensor.size()
    head_dim = feature_dim // num_heads
    return tensor.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2)
Q = split_heads(Q, num_heads) # (batch_size=2, num_heads=2, seq_len=3, head_dim=2)
K = split_heads(K, num_heads) # (batch_size=2, num_heads=2, seq_len=3, head_dim=2)
V = split_heads(V, num_heads) # (batch_size=2, num_heads=2, seq_len=3, head_dim=2)

head_dim = feature_dim // num_heads#特征维度需要被平均分为多个头
而后使用 split_heads 函数将这些向量分割成 num_heads 个头,其中 Q、K 和 V 各自的形状变为 (batch_size, num_heads, seq_len, head_dim)

# 计算 Q 和 K 的点积,作为相似度分数,也就是自注意力原始权重
raw_weights = torch.matmul(Q, K.transpose(-2, -1)) # (batch_size=2, num_heads=2, seq_len=3, seq_len=3)

变换-2,-1,Q 和 K 的维度为 (batch_size, num_heads, seq_len, Q_dim) 和 (batch_size, num_heads, seq_len, K_dim),进行转置操作后,它们的维度变为 (batch_size, num_heads, seq_len, K_dim) 和 (batch_size, num_heads, K_dim, seq_len),以确保正确的点积计算

# 将所有头的结果拼接起来
def combine_heads(tensor, num_heads):
    batch_size, num_heads, seq_len, head_dim = tensor.size()
    feature_dim = num_heads * head_dim
    return tensor.transpose(1, 2).contiguous().view(batch_size, seq_len, feature_dim)
attn_outputs = combine_heads(attn_outputs, num_heads)  # (batch_size=2, seq_len=3, feature_dim=4)

随后定义的是拼接操作,看起来有点绕,但功能很好理解,还原了原tensor的维度。

完整的代码不po了

下一次更新第7节,只打算完成最后的两个大作业

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值