《从零构建大语言模型》学习笔记4,自注意力机制1


文章目录

  • 《从零构建大语言模型》学习笔记4,自注意力机制1
  • 前言
  • 一、实现一个简单的无训练权重的自注意力机制
  • 二、实现具有可训练权重的自注意力机制
    • 1. 分步计算注意力权重
    • 2.实现自注意力Python类
  • 三、将单头注意力扩展到多头注意力
  • 总结


前言

本书原项目地址:https://github.com/rasbt/LLMs-from-scratch
我们进入第三章,探讨自注意力机制——这是大语言模型的核心基础算法。自注意力中的"自"表示该机制能够分析输入序列内部不同位置之间的关联,动态计算注意力权重。它通过学习输入元素(如句子中的单词或图像中的像素)之间的相互关系和依赖模式来实现这一功能。


一、实现一个简单的无训练权重的自注意力机制

比如我们有6个词元的embeddings的向量,接下来看下怎么计算第二个词元与其它词元之间的注意力分数。

计算方法也很简单,就是把第二个词元向量分别与其它词元向量做点积,原理图如下:

点积后得到的就是每一个向量与第二向量的自注意分数,然后进行归一化就得到了注意力权重。
计算代码如下:

import torchinputs = torch.tensor([[0.43, 0.15, 0.89], # Your     (x^1)[0.55, 0.87, 0.66], # journey  (x^2)[0.57, 0.85, 0.64], # starts   (x^3)[0.22, 0.58, 0.33], # with     (x^4)[0.77, 0.25, 0.10], # one      (x^5)[0.05, 0.80, 0.55]] # step     (x^6)
)
#对于一句话中的每个单词定义了一个三维的向量query = inputs[1]  # 2nd input token is the queryattn_scores_2 = torch.empty(inputs.shape[0])
#建立一个未初始化的张量来记录注意力得分
for i, x_i in enumerate(inputs):attn_scores_2[i] = torch.dot(x_i, query) # 相似性度量计算attention分数# 从公式上看也就是点乘
print(attn_scores_2)attn_weights_2 = torch.softmax(attn_scores_2, dim=0)
print("Attention weights:", attn_weights_2)
print("Sum:", attn_weights_2.sum())
#用torch优化过的softmax对边缘值也挺友好的

然后把得到的注意力权重又分别与对应的向量相乘后再累加,就得到了上下文向量,这个就是我们最后要求的输出。

代码如下:

query = inputs[1] # 2nd input token is the query
context_vec_2 = torch.zeros(query.shape)
#创造一个内容的零向量
for i,x_i in enumerate(inputs):context_vec_2 += attn_weights_2[i]*x_i#把不同内容的向量+起来
print(context_vec_2)

以上就是实现一个简单的无训练权重的自注意力机制

二、实现具有可训练权重的自注意力机制

1. 分步计算注意力权重

上述注意力计算过程可拆解为三个步骤,每个步骤都需要使用词向量。为此,我们为每个步骤的词向量分别乘以可训练的参数矩阵(Wq、Wk、Wv),从而得到对应的query、key和value向量。这样计算过程就如下图所示:


同样使用上面的例子,先用第二个词元向量点乘对应的Wq矩阵得到q2,然后用q2去点乘其它所有词元的key,就得到对应词元的注意力分数,同样归一化后得到对应的注意力权重。
最后用对应的注意力权重与value想成后累加,就得到了上下文向量。当然应用可以训练的参数矩阵,所以后面可以根据上下文向量结果来训练参数矩阵。
实现代码如下:

x_2 = inputs[1] # second input element
d_in = inputs.shape[1] # the input embedding size, d=3
d_out = 2 # the output embedding size, d=2torch.manual_seed(123)
#固定随机种子确保可复现性W_query = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
W_key   = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
W_value = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
#初始化三个矩阵来存放
#不要求梯度降低了复杂度query_2 = x_2 @ W_query # _2 because it's with respect to the 2nd input element
key_2 = x_2 @ W_key 
value_2 = x_2 @ W_value
#点积计算
print(query_2)keys = inputs @ W_key 
values = inputs @ W_value
print("keys.shape:", keys.shape)
print("values.shape:", values.shape)
#中途检验下keys_2 = keys[1] # Python starts index at 0
attn_score_22 = query_2.dot(keys_2)
print(attn_score_22)attn_scores_2 = query_2 @ keys.T # All attention scores for given query
print(attn_scores_2)
#计算注意力跟query值d_k = keys.shape[1]
attn_weights_2 = torch.softmax(attn_scores_2 / d_k**0.5, dim=-1)
#压缩函数, 有利于储存与比较
print(attn_weights_2)context_vec_2 = attn_weights_2 @ values
print(context_vec_2)

2.实现自注意力Python类

把上面的过程集中到一个类里面,并且按照pytorch里面构建神经网络的方式来重写这个类,__init__函数是初始化一些参数,其中就包括用nn模块里面的线性层来初始化W_query,W_key ,W_value 这三个参数矩阵。forward函数是这个网络的前向运算过程,就是用初始化后的K,Q,V矩阵按照上面讲到的顺序进行矩阵相乘,最后得到上下文向量矩阵。
代码如下(示例):

class SelfAttention_v2(nn.Module):def __init__(self, d_in, d_out, qkv_bias=False):super().__init__()self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)self.W_key   = nn.Linear(d_in, d_out, bias=qkv_bias)self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)#权重初始化def forward(self, x):keys = self.W_key(x)queries = self.W_query(x)values = self.W_value(x)attn_scores = queries @ keys.T #Query跟Key的计算 得出初始的分数传递到后面进行归一化操作attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)context_vec = attn_weights @ values#直接基于注意力对于文本计算return context_vectorch.manual_seed(789)
sa_v2 = SelfAttention_v2(d_in, d_out)
print(sa_v2(inputs))

这个类还需进一步优化,需要引入掩码的概念。掩码的作用是遮盖注意力权重矩阵的特定部分,通常是对矩阵的上三角部分进行处理。因为在实际推理过程中,模型需要预测后续未知的词元,所以在训练阶段就要通过掩码将未来的权重信息遮盖掉,让模型学会对未知信息的合理拟合。如果不这样做,可能会导致模型过快收敛。原理如下图:

代码很简单,就是给权重矩阵乘一个三角矩阵:

context_length = attn_scores.shape[0]
mask_simple = torch.tril(torch.ones(context_length, context_length))
#Mask矩阵,直接保留Diagonal下部分的,上部分掩盖掉
print(mask_simple)masked_simple = attn_weights*mask_simple
print(masked_simple)
#简单的效果图row_sums = masked_simple.sum(dim=-1, keepdim=True)
masked_simple_norm = masked_simple / row_sums
print(masked_simple_norm)
#掩码之后的softmax

记得掩码后,要重新归一化。
为了加大训练难点,还会把卷积网络中比较成熟的drop层也引用进来,就是随机丢弃权重矩阵中的一些权重。

实现代码如下:

torch.manual_seed(123)
dropout = torch.nn.Dropout(0.5) 
# dropout rate of 50%丢包率doge
example = torch.ones(6, 6) 
# create a matrix of ones满的6*6矩阵被1包圆了print(dropout(example))
#输出需要被放大相应的倍数,为了维持恒定torch.manual_seed(123)
print(dropout(attn_weights))

把以上两个技巧应用到类里面后,重写的代码如下:

class CausalAttention(nn.Module):def __init__(self, d_in, d_out, context_length,dropout, qkv_bias=False):#初始化定义网络结构和参数super().__init__()self.d_out = d_outself.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)self.W_key   = nn.Linear(d_in, d_out, bias=qkv_bias)self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)self.dropout = nn.Dropout(dropout) # Newself.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1)) # New#定义QKV并对进行dropout防止过拟合#注册mask向量,对未来进行负无穷的拟合def forward(self, x):b, num_tokens, d_in = x.shape # New batch dimension b#提取batch的大小、token的数量、跟宽度keys = self.W_key(x)queries = self.W_query(x)values = self.W_value(x)#进行运算计算attn_scores = queries @ keys.transpose(1, 2) # Changed transpose#通过点积来计算attention的数值attn_scores.masked_fill_(  # New, _ ops are in-placeself.mask.bool()[:num_tokens, :num_tokens], -torch.inf)  # `:num_tokens` to account for cases where the number of tokens in the batch is smaller than the supported context_sizeattn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1## 缩放因子 √d,用于稳定梯度)#在时间顺序上进行mask确保信息不会被泄露attn_weights = self.dropout(attn_weights) # New#防止过拟合的dropout处理方式context_vec = attn_weights @ values# 根据注意力权重计算上下文向量return context_vectorch.manual_seed(123)
context_length = batch.shape[1]
ca = CausalAttention(d_in, d_out, context_length, 0.0)context_vecs = ca(batch)print(context_vecs)
print("context_vecs.shape:", context_vecs.shape)

三、将单头注意力扩展到多头注意力

在卷积神经网络(CNN)中,通常采用不同尺寸的卷积核(如3×3、5×5等)来捕获图像的多尺度特征。这些不同尺寸卷积核提取的特征图经过通道维度的拼接(concat)后,能形成更全面的特征表示。类似地,注意力网络通过初始化多组q、k、v参数来获取不同的上下文向量并进行合并,这种机制被称为多头注意力。

比较简单的实现方式是,使用for循环做多次单注意力计算,然后再拼接就可以了,代码如下:

class MultiHeadAttentionWrapper(nn.Module):def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):super().__init__() #多个实例,每个都是一个头self.heads = nn.ModuleList([CausalAttention(d_in, d_out, context_length, dropout, qkv_bias) for _ in range(num_heads)])def forward(self, x):return torch.cat([head(x) for head in self.heads], dim=-1)#模型的训练torch.manual_seed(123)context_length = batch.shape[1] # This is the number of tokens
d_in, d_out = 3, 2
mha = MultiHeadAttentionWrapper(d_in, d_out, context_length, 0.0, num_heads=2
)context_vecs = mha(batch)print(context_vecs)
print("context_vecs.shape:", context_vecs.shape)

然而,上述方法的效率较低,主要体现在以下两个方面:首先,这种方法需要重复进行n次参数初始化和前向传播计算(n代表注意力头的数量),导致计算资源的浪费;其次,多个独立的参数矩阵会导致内存访问不连续,降低缓存命中率。更常见且高效的做法是在模型初始化阶段就进行维度扩展。最后代码如下:

class MultiHeadAttention(nn.Module):def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):super().__init__()assert (d_out % num_heads == 0), \"d_out must be divisible by num_heads"#确保是可以被整除的self.d_out = d_outself.num_heads = num_headsself.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim#初始化头的维度、数量self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)self.out_proj = nn.Linear(d_out, d_out)  # Linear layer to combine head outputs#头的输出结合线性层self.dropout = nn.Dropout(dropout)#进行dropout防止过拟合self.register_buffer("mask",torch.triu(torch.ones(context_length, context_length),diagonal=1))# 上三角掩码,确保因果性def forward(self, x):b, num_tokens, d_in = x.shapekeys = self.W_key(x) # Shape: (b, num_tokens, d_out)queries = self.W_query(x)values = self.W_value(x)#把输出的维度拆成头*头大小# We implicitly split the matrix by adding a `num_heads` dimension# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)keys = keys.view(b, num_tokens, self.num_heads, self.head_dim) values = values.view(b, num_tokens, self.num_heads, self.head_dim)queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)#转制维度,听说是为了更好的计算注意力# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)keys = keys.transpose(1, 2)queries = queries.transpose(1, 2)values = values.transpose(1, 2)# 计算缩放点积注意力# Compute scaled dot-product attention (aka self-attention) with a causal maskattn_scores = queries @ keys.transpose(2, 3)  # Dot product for each head# 将掩码缩减到当前 token 数量,并转换为布尔型# 进而实现动态遮蔽,所以不用另开好几个数组mask_bool = self.mask.bool()[:num_tokens, :num_tokens]# 遮蔽矩阵# Use the mask to fill attention scoresattn_scores.masked_fill_(mask_bool, -torch.inf)#归一化attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)attn_weights = self.dropout(attn_weights)# Shape: (b, num_tokens, num_heads, head_dim)context_vec = (attn_weights @ values).transpose(1, 2) #头的合并# Combine heads, where self.d_out = self.num_heads * self.head_dim#对上下文向量的形状进行调整,确保输出的形状context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)context_vec = self.out_proj(context_vec) # optional projectionreturn context_vectorch.manual_seed(123)batch_size, context_length, d_in = batch.shape
d_out = 2
mha = MultiHeadAttention(d_in, d_out, context_length, 0.0, num_heads=2)context_vecs = mha(batch)print(context_vecs)
print("context_vecs.shape:", context_vecs.shape)

总结

以上是关于注意力机制的讲解,重点在于理解Q、K、V三个参数。我一直在思考为什么是三个参数,是否能构造更多参数。从理论上看,增加更多参数是可行的,但从数学角度来说,过多的线性相乘参数可能对后续求导没有实质性帮助。此外,采用query、key、value的命名方式也使其含义更加直观易懂。在撰写过程中,已假设大家具备卷积神经网络和PyTorch的基础知识。若有任何表述不清或理解有误之处,欢迎随时指正交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/92833.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/92833.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/92833.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

昇思+昇腾开发板+DeepSeek模型推理和性能优化

昇思昇腾开发板DeepSeek模型推理和性能优化 模型推理 流程: 权重加载 -> 启动推理 -> 效果比较与调优 -> 性能测试 -> 性能优化 权重加载 如微调章节介绍,最终的模型包含两部分:base model 和 LoRA adapter,其中base …

未给任务“Fody.WeavingTask”的必需参数“IntermediateDir”赋值。 WpfTreeView

c#专栏记录: 报错 未给任务“Fody.WeavingTask”的必需参数“IntermediateDir”赋值。 WpfTreeView 生成 解决办法 清理和重新生成项目 完成上述配置后,尝试执行以下步骤: 清理项目:删除 bin 和 obj 文件夹。 重新生成项目&…

[Linux]学习笔记系列 -- [arm][lib]

文章目录arch/arm/lib/delay.cregister_current_timer_delay 注册当前定时器延迟read_current_timer 读取当前定时器drivers/clocksource/timer-stm32.cstm32_clocksource_init STM32 平台上初始化时钟源https://github.com/wdfk-prog/linux-study arch/arm/lib/delay.c regis…

harbor仓库搭建(配置https)

目录 1. 环境准备 2. 配置https的原因 3. 生成ca证书 4. 搭建harbor仓库 5. 访问harbor 6. 修改加密算法 1. 环境准备 需要提前安装docker和docker-compose,harbor仓库版本越新,对应的docker和docker-compose版本越新。 主机IP192.168.48.19dock…

C++多线程服务器

C多线程服务器 因为自己同时在看多本书,之前看过《TCP/IP 网络编程》一书,其中有一个自己编写一个多线程服务器的例子,于是就把代码直接抄了一变。 在学习网络编程前需要先了解网络的7层模型。 具体代码如下: 服务器端&#xff1a…

【Pandas】常用数据处理技巧

一. 数据读取 1.pd.to_csv & pd.read_csv 细节: 1.pd.read_csv 需要 ignore_index True or ,index_col0 否则会有列Unnamed0 2.pickle具有更快的读取速度,与更小的体积。 读取前N行(若不需获取所有数据) pd.read_csv(…

Docker Compose 部署高可用 MongoDB 副本集集群(含 Keepalived + HAProxy 负载均衡)

Docker Compose 部署高可用 MongoDB 副本集集群(含 Keepalived HAProxy 负载均衡)背景与目标📋 环境规划**服务器信息****软件版本**部署步骤1. 创建目录结构2、生成 keyFile(三台机器内容必须一致)3. 准备 Keepalive…

MySQL(189)如何分析MySQL的锁等待问题?

分析MySQL的锁等待问题有助于发现和解决数据库性能瓶颈。锁等待问题通常会导致数据库响应时间变长,影响系统的整体性能。以下是详细深入的方法和代码示例,帮助你分析和解决MySQL的锁等待问题。 一、锁的类型和概念 在MySQL中,主要有以下几种锁…

26.Scikit-learn实战:机器学习的工具箱

Scikit-learn实战:机器学习的工具箱 🎯 前言:机器学习界的"宜家家具" 还记得第一次逛宜家的感受吗?琳琅满目的家具,每一件都有详细的说明书,组装简单,样式统一,关键是—…

wordpress文章摘要调用的3种方法

以下是WordPress文章摘要的3种调用方法: 1. 使用the_excerpt()函数 这是WordPress自带的函数,用于调用文章摘要。如果文章有手动填写的摘要,则会显示手动摘要;如果没有手动摘要,WordPress会自动从文章内容中提取前55个单词作为摘…

java excel转图片常用的几种方法

十分想念顺店杂可。。。在 Java 中实现 Excel 转图片,常用的方法主要分为两类:使用商业库(简单高效但可能收费)和使用开源库组合(免费但实现复杂)。以下是几种常用方案及实现思路:一、使用商业库…

QT项目 -仿QQ音乐的音乐播放器(第五节)

目录 一、CommonPage界⾯设置和显示 二、自定义ListItemBox 三、支持hover效果 四、自定义VolumeTool 五、界面设置 六、页面创建及弹出 七、绘制三角 一、CommonPage界面设置和显示 void CommonPage::setCommonPageUI(const QString &title, const QString &imag…

wstool和git submodule优劣势对比

wstool 和 git submodule 都可以用来管理项目中的外部源代码依赖,但它们的设计理念、工作流程和适用场景有很大不同。 我们来深入对比一下它们的优势和劣势。 核心理念比喻 git submodule:像是在你的汽车设计图纸中,直接嵌入了另一家公司&…

六、RuoYi-Cloud-Plus OSS文件上传配置

1.前面我们完成了RuoYi-Cloud-Plus 部署及启动,此刻已经可以正常访问。 前面文章的专栏内容在这,感兴趣可以看看。 https://blog.csdn.net/weixin_42868605/category_13023920.html 2.但现在虽然已经启动成功,但有很多功能我们依旧用不了&a…

达梦数据库日常运维命令

查询数据库表空间数据文件使用大小限制DECLARE K INT:(SELECT cast(PAGE()/1024 as varchar)); BEGIN SELECTF."PATH" 数据文件 ,F.CLIENT_PATH,G.NAME 所属表空间,F.MAX_SIZE||M 文件扩展限制,(CASE F.AUTO_EXTEND WHEN 1 THEN 是 ELSE 否 END) 文件…

使用线性降维方法进行数据降维

在数据科学与机器学习的领域中,维度灾难问题经常导致模型的性能下降。线性降维方法是一种常见的技术,用于在保留尽可能多的原始数据特征的同时,减少数据集的维度。这些方法通过将高维数据映射到低维空间来减少特征数量,从而加速模…

OpenCV图像裁剪与 ROI 操作

在图像处理领域,ROI(Region of Interest)区域感兴趣操作是非常基础而重要的一环。无论是进行目标检测、图像分割,还是简单的图像处理,都离不开对图像某一区域的选取与处理。本文将结合 OpenCV 的 C 接口,详…

关于AI应用案例计算机视觉、自然语言处理、推荐系统和生成式AI四大领域的详细技术分析。

一、计算机视觉应用:实时物体检测 案例描述:使用YOLOv8模型实现实时物体检测系统,应用于安防监控场景。 1. 代码示例(Python) python from ultralytics import YOLO import cv2# 加载预训练模型 model YOLO("…

各个网络层拥有的协议简写

OSI 七层模型(从下到上分别为物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)是网络通信的经典理论框架,每层都有其核心功能和对应的协议。以下是各层的主要协议列举:1. 物理层(Physical Layer&#xff…

django基于Python的设计师作品平台的数据可视化系统设计与实现

django基于Python的设计师作品平台的数据可视化系统设计与实现