一、 AlexNet

import torchfrom torch import nnfrom d2l import torch as d2l
net = nn.Sequential( # 这里使用一个11*11的更大窗口来捕捉对象。 # 同时,步幅为4,以减少输出的高度和宽度。 # 另外,输出通道的数目远大于LeNet nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2), # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数 nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2), # 使用三个连续的卷积层和较小的卷积窗口。 # 除了最后的卷积层,输出通道的数量进一步增加。 # 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度 nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(), nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2), nn.Flatten(), # 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合 nn.Linear(6400, 4096), nn.ReLU(), nn.Dropout(p=0.5), nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(p=0.5), # 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000 nn.Linear(4096, 10))

二、VGG
1. vgg块

import torchfrom torch import nnfrom d2l import torch as d2l
def vgg_block(num_convs, in_channels, out_channels): layers = [] for _ in range(num_convs): layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)) layers.append(nn.ReLU()) in_channels = out_channels layers.append(nn.MaxPool2d(kernel_size=2,stride=2)) return nn.Sequential(*layers)2.vgg网络

def vgg(conv_arch): conv_blks = [] in_channels = 1 # 卷积层部分 for (num_convs, out_channels) in conv_arch: conv_blks.append(vgg_block(num_convs, in_channels, out_channels)) in_channels = out_channels
return nn.Sequential( *conv_blks, nn.Flatten(), # 全连接层部分 nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 10))
net = vgg(conv_arch)三、GoogLeNet
1. Inception块

import torchfrom torch import nnfrom torch.nn import functional as Ffrom d2l import torch as d2l
class Inception(nn.Module): # c1--c4是每条路径的输出通道数 def __init__(self, in_channels, c1, c2, c3, c4, **kwargs): super(Inception, self).__init__(**kwargs) # 线路1,单1x1卷积层 self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1) # 线路2,1x1卷积层后接3x3卷积层 self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1) self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1) # 线路3,1x1卷积层后接5x5卷积层 self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1) self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2) # 线路4,3x3最大汇聚层后接1x1卷积层 self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1) self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)
def forward(self, x): p1 = F.relu(self.p1_1(x)) p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) p4 = F.relu(self.p4_2(self.p4_1(x))) # 在通道维度上连结输出 return torch.cat((p1, p2, p3, p4), dim=1)2. GoogLeNet 模型

b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1), nn.ReLU(), nn.Conv2d(64, 192, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32), Inception(256, 128, (128, 192), (32, 96), 64), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64), Inception(512, 160, (112, 224), (24, 64), 64), Inception(512, 128, (128, 256), (24, 64), 64), Inception(512, 112, (144, 288), (32, 64), 64), Inception(528, 256, (160, 320), (32, 128), 128), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128), Inception(832, 384, (192, 384), (48, 128), 128), nn.AdaptiveAvgPool2d((1,1)), nn.Flatten())
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))四、 ResNet
1. 残差块

import torchfrom torch import nnfrom torch.nn import functional as Ffrom d2l import torch as d2l
class Residual(nn.Module): #@save def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1): super().__init__() self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1, stride=strides) self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides) else: self.conv3 = None self.bn1 = nn.BatchNorm2d(num_channels) self.bn2 = nn.BatchNorm2d(num_channels)
def forward(self, X): Y = F.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) Y += X return F.relu(Y)2.残差网络

五、注意力机制
还是不理解QKV对应没有注意力机制下的哪些输入?参数?卷积核?还是训练数据?还是指什么
如果经过的QKV完全一样那么和直接连接下一层有什么区别?如何选区Wq,Wk,Wv或者最后如何对数据应用这个变化以实现注意力的效果?
这个问题直击自注意力机制(Self-Attention)的灵魂。很多初学者都会困惑:既然 Q、K、V 都是同一个输入 变来的,那折腾这一大圈,跟直接把 喂给下一层(比如一个全连接层或卷积层)到底有什么本质区别?
我们分两个层面来彻底解开这个疑惑。
一、 如果经过的 QKV 都一样,和直接连接下一层有什么区别?
假设我们正在处理一段自动驾驶场景中的数据,输入数据 是一个序列,包含了主车、行人 A、车辆 B 在过去几秒的状态特征。
1. 直接连接下一层(例如全连接层 MLP)的做法:
如果直接把 连到下一层,公式是 。
这就好比让主车、行人 A、车辆 B 各自拿着一个通用的公式,独立地去升级自己的特征。主车只看主车的数据,行人 A 只看行人 A 的数据。它们之间是没有信息交流的(除非你用很大感受野的卷积或循环网络硬把它们揉在一起)。
2. 注意力机制的做法(引入 Q、K、V):
虽然 Q、K、V 的源头都是 ,但通过注意力计算,它打破了 Token 之间的孤岛。
主车生成的 会去和行人 A 生成的 计算点积。这就意味着,主车在更新自己的特征时,主动“看”了一眼行人 A,并根据行人 A 的重要程度(注意力权重),把行人 A 的部分特征()融合到了自己身上。
3. 为什么不能干脆省掉权重,让 Q = K = V = X?
如果我们不乘任何权重矩阵,直接拿原始特征 自己跟自己算内积(即 ),会发生什么?
此时,两个实体之间的“注意力打分”,完全取决于它们物理特征的相似度。比如,两辆同向行驶、速度相同的车,内积肯定最大。但实际上,一辆正在直行的主车,最需要关注的往往不是和它同向行驶的车,而是垂直方向突然冲出来的行人。
如果不引入权重矩阵,模型就永远被困在“物理相似度”里。
二、 如何选取 ?如何对数据应用这个变化?
这三个矩阵 不是由人类手动选取的,也不是预先设定好的规则。它们和神经网络里其他的参数一样,在模型一开始被随机初始化,然后在模型训练的过程中,通过计算损失函数(比如轨迹预测的偏差)并使用反向传播算法,自动学习和更新出来的。
模型通过大量的训练数据,自己摸索出了一套规律:把 乘上特定的 和 ,就能把特征映射到一个“意图匹配空间”。在这个空间里,“准备直行的车(Q)”和“横穿的行人(K)”的内积会变得极其巨大,从而触发警报(分配高注意力权重)。
为了让你清楚数据在代码层面是如何一步步发生变化的,这里是它最底层的数学执行流:
第一步:特征映射(生成 Q, K, V)
输入数据 (假设形状是 [序列长度, 特征维度])分别并行地乘以三个不同的权重矩阵:
此时,原始数据 就拥有了三种不同的“身份”。
第二步:计算注意力原始得分(打分)
拿所有的查询去和所有的键计算点积相似度:
这个算出来的矩阵,就是一个 的打分表,记录了场景中每一个实体对其他所有实体的关注度得分。
第三步:缩放与概率归一化(Softmax)
因为随着特征维度的增大,点积的结果会爆炸,导致梯度消失,所以需要除以维度的一个缩放因子 。然后通过 Softmax 将得分转化为加和为 1 的概率权重:
这就是最终的注意力权重矩阵(Attention Map)。如果主车对行人 A 的权重是 0.9,对车辆 B 的权重是 0.1,就说明此时主车 90% 的精力都在防备行人。
第四步:加权信息提取(输出)
最后,拿这个权重矩阵去乘以对应的值 :
得到的 就是经过注意力机制洗礼后的全新特征。此时, 中的主车特征,已经不再仅仅是它自己的历史轨迹了,而是“包含了行人 A 潜在危险信息的综合特征”。这个 就会被直接送入下一层(比如前馈神经网络)进行最终的预测。
目前常用的注意力机制有哪些?有哪些异同?
目前常用的注意力机制主要分为三大流派:Transformer 家族(主宰大模型与多模态)、CNN 家族(主宰传统视觉特征提取),以及为了解决极长序列算力瓶颈而诞生的高效底层变体。
我们可以把注意力机制想象成不同风格的“找重点策略”。以下是主流机制的通俗拆解与异同对比:
1. Transformer 家族:基于 QKV 的序列玩家
这是目前大语言模型(如 ChatGPT)和前沿视觉模型(如 Sora)的最底层基石。
-
自注意力机制 (Self-Attention)
-
通俗表达: 内部消化。句子里的每一个词,都去观察同一个句子里的其他词,看看谁和自己关系最紧密。
-
技术差异: Q、K、V 全部来自同一个输入序列。它打破了传统 RNN 只能顺序计算的限制,实现了全局视野和并行计算。
-
-
交叉注意力 (Cross-Attention)
-
通俗表达: 跨界寻找。拿着问题(Query)去另一本书(Key 和 Value 的来源)里找答案。
-
技术差异: Q 来自输入 A(例如文本提示),而 K 和 V 来自输入 B(例如图像特征)。这是多模态模型能够将不同模态“对齐”并产生互动的核心组件。
-
-
多头注意力 (Multi-Head Attention)
-
通俗表达: 多个评审团从不同角度打分。第一组评审专门看语法,第二组看情感,第三组看逻辑。
-
技术差异: 将原本的 Q、K、V 拆分成多个“头”,让它们并行执行独立的注意力计算,最后再拼接起来。这极大地提升了模型捕捉多维度复杂特征的能力。
-
2. CNN 家族:基于特征图的视觉玩家
这类机制通常不使用繁琐的 QKV 点积,而是通过对卷积出来的空间或通道特征图进行权重缩放来起作用。
-
通道注意力 (Channel Attention, 如 SE-Net)
-
通俗表达: 解决“看什么”的问题。在成百上千个特征通道里,给“边缘特征”或“颜色特征”所在的通道打分。
-
技术差异: 通过全局池化和全连接层,学习出一个针对通道的一维权重向量,直接乘回原本的特征图上。
-
-
空间注意力 (Spatial Attention)
-
通俗表达: 解决“看哪里”的问题。在一张图里,把目标物体的区域点亮,把无用的背景变暗。
-
技术差异: 在通道维度上进行池化压缩,学习出一个二维的空间权重掩码(Mask)。
-
-
混合注意力 (如 CBAM)
- 通俗表达: 既挑重点特征,又看关键区域。综合了上述两者的优势,是许多经典目标检测算法的轻量级增强标配。
3. 高效注意力:算力与显存的优化者
标准自注意力机制最大的痛点是 的计算和内存复杂度:当输入序列的长度 翻倍时,计算量会变成 4 倍。为了让模型能处理几十万字的长文本,诞生了以下变体:
-
稀疏注意力 (Sparse Attention, 如 Longformer / BigBird)
-
通俗表达: 不再要求每个人都互相认识,只和自己周围的邻居,或者几个固定的“村长”(全局点)交流。
-
技术差异: 强行把稠密的注意力计算矩阵变稀疏,忽略掉大部分远距离的无效点积。这属于算法层面的近似优化。
-
-
FlashAttention (现代大模型标配)
-
通俗表达: 不改变最终的答案,但彻底优化了显卡内部的“搬砖路线”,让数据跑得极快。
-
技术差异: 它是一种硬件感知 (Hardware-aware) 的底层优化。通过分块计算 (Tiling) 和重计算技巧,大幅减少了 GPU SRAM 与高带宽内存 (HBM) 之间缓慢的数据读写操作。它的计算结果与标准注意力完全等价(没有精度损失),但速度飞升且极度节省显存。
-
正在加载评论...
链上评论区