-多模态大模型-第二节
多模态大模型–第二节
专栏:
个人主页:
书接上次的第一节 到了CLIP与残差网部分
一. CLIP架构
1. 残差网络结构
*几代表几个层
conv卷积层—抓取图像的信息
pool池化层—把抓取好的图像压缩变小(额外介绍)
卷积层一般是 3x3 4x4 1x1(作用卷积网络改变图像尺寸 改变图像的数量,为了挤压数据)
从输入到输出尺寸变小
BottleNeck代码(一定要手搓一遍 很重要!!!)
class Bottleneck(nn.Module):
# 定义扩展倍数,用于决定输出通道数的扩展比例
expansion = 4
def __init__(self, inplanes, planes, stride=1):
"""
Bottleneck 模块的初始化方法。
参数:
- inplanes: 输入通道数。
- planes: 每一层卷积的基础通道数,最终会扩展为 planes * expansion。
- stride: 步幅,控制空间分辨率的缩小(默认值为1)。
"""
super().__init__()
# 第1个卷积层,使用 1x1 卷积,用于减少计算量
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes) # 批归一化
self.relu1 = nn.ReLU(inplace=True) # 激活函数,inplace=True 节省内存
# 第2个卷积层,使用 3x3 卷积,用于提取特征
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes) # 批归一化
self.relu2 = nn.ReLU(inplace=True) # 激活函数
# 如果 stride > 1,使用 AvgPool2d 进行空间降采样,否则使用 Identity(保持不变)
self.avgpool = nn.AvgPool2d(stride) if stride > 1 else nn.Identity()
# 第3个卷积层,使用 1x1 卷积,用于扩展通道数至 planes * expansion
self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion) # 批归一化
self.relu3 = nn.ReLU(inplace=True) # 激活函数
# 如果输入和输出的通道数或分辨率不同,则需要通过 downsample 进行调整
self.downsample = None
self.stride = stride
if stride > 1 or inplanes != planes * Bottleneck.expansion:
# downsample 模块,用于调整 shortcut 的形状,使其与主路径的输出形状匹配
self.downsample = nn.Sequential(OrderedDict([
# 使用 AvgPool2d 进行空间降采样(仅在 stride > 1 时)
("-1", nn.AvgPool2d(stride)),
# 1x1 卷积调整通道数
("0", nn.Conv2d(inplanes, planes * self.expansion, kernel_size=1, stride=1, bias=False)),
# 批归一化
("1", nn.BatchNorm2d(planes * self.expansion))
]))
def forward(self, x: torch.Tensor):
"""
前向传播函数。
参数:
- x: 输入张量,形状为 (batch_size, inplanes, H, W)
返回值:
- 输出张量,形状为 (batch_size, planes * expansion, H / stride, W / stride)
"""
identity = x # 保存输入作为 shortcut(残差连接的输入)
# 主路径:第1个卷积 + BN + ReLU
out = self.relu1(self.bn1(self.conv1(x)))
# 主路径:第2个卷积 + BN + ReLU
out = self.relu2(self.bn2(self.conv2(out)))
# 如果 stride > 1,进行空间降采样
out = self.avgpool(out)
# 主路径:第3个卷积 + BN
out = self.bn3(self.conv3(out))
# 如果 downsample 不为 None,则对 identity 进行调整
if self.downsample is not None:
identity = self.downsample(x)
# 残差连接:将主路径的输出和 shortcut 相加
out += identity
# 激活函数
out = self.relu3(out)
return out
由于这个残差网络比较简单,大多数的读者应该都会 就不进行详细解释了 说实话不是很重点
2.全局池化注意力机制—这是一个注意力机制层—并且完成了Projection(此处笔者可能解释不是很清楚,日后定单独发文解释)
Attention Pooling
- Attention Pooling 是 CLIP 的 Modified ResNet 架构中用来替代传统全局平均池化(Global Average Pooling, GAP)的方法、同时对整个 CLIP 来说也是重要的向量压缩层。在经过了 stem 和多个 Residual Layers(layer1-layer4)的特征提取后,网络会输出一个形状为**(batch_size,channels,height,width)**的特征图,Attention Pooling 会将这个特征图整合为一个全局特征向量,表示整幅图像的特征、从而方便后续的投影。
- Attention Pooling 的主要任务是将二维的特征图 (batch_size, channels, height, width) 转换为一个全局特征向量 (batch_size, embed_dim)。
- (100,256,H,W )==>AP==>(100, d_model) d_model就相当于是代表了之前的 256,H,W
- 和传统的全局平均池化相比,注意力池化可以对特征图上的每个位置(像素点)的重要性进行加权、这样生成的全局特征向量能够更好地捕获空间上的信息以及全局上下文关系。
(bs, C, H, W) H W一般都相等
class AttentionPool2d(nn.Module):
def __init__(self, spacial_dim: int, embed_dim: int, num_heads: int, output_dim: int = None):
"""
初始化 Attention Pooling 模块。
参数:
- spacial_dim (int): 输入特征图的空间维度(即特征图宽高的大小,假设特征图是正方形)。
- embed_dim (int): 输入特征的嵌入维度(通道数)。
- num_heads (int): 多头注意力的头数。
- output_dim (int, optional): 输出特征的目标维度。如果未指定,则默认为 embed_dim。
"""
super().__init__()
# 位置嵌入 (Positional Embedding),初始化为随机值,大小为 (spacial_dim^2 + 1, embed_dim)
# 额外的 "+1" 是为全局 token 预留的位置。
self.positional_embedding = nn.Parameter(torch.randn(spacial_dim ** 2 + 1, embed_dim) / embed_dim ** 0.5)
# Query、Key、Value 的线性投影
self.k_proj = nn.Linear(embed_dim, embed_dim)
self.q_proj = nn.Linear(embed_dim, embed_dim)
self.v_proj = nn.Linear(embed_dim, embed_dim)
# 输出投影,将嵌入维度从 embed_dim 调整为 output_dim
self.c_proj = nn.Linear(embed_dim, output_dim or embed_dim)
# 多头注意力的头数
self.num_heads = num_heads
def forward(self, x):
"""
前向传播函数,执行 Attention Pooling 操作。
参数:
- x (Tensor): 输入特征图,形状为 (batch_size, channels, height, width)。
返回:
- Tensor: 输出的全局特征向量,形状为 (batch_size, output_dim)。
"""
# 1. 展平特征图并转置
# 原始形状:x -> (batch_size, channels, height, width)
# 转换后:x -> (height * width, batch_size, channels)
x = x.flatten(start_dim=2).permute(2, 0, 1)
# 2. 添加全局 token
# 全局 token 是整幅特征图的均值,用于捕获全局信息。
# `x.mean(dim=0, keepdim=True)` 的形状为 (1, batch_size, channels)
# 添加到特征图的前面,新的 x 形状为 (height * width + 1, batch_size, channels)
x = torch.cat([x.mean(dim=0, keepdim=True), x], dim=0)
# 3. 添加位置嵌入
# self.positional_embedding 的形状为 (height * width + 1, embed_dim)
# 将位置嵌入添加到特征图上,每个位置都会加上对应的嵌入值。
x = x + self.positional_embedding[:, None, :].to(x.dtype)
# 4. 执行多头注意力
# 注意力操作的参数:
# - query=x[:1]: 使用全局 token 作为 Query,形状为 (1, batch_size, embed_dim)。
# - key=x: 使用整个特征图(包括全局 token)作为 Key,形状为 (height * width + 1, batch_size, embed_dim)。
# - value=x: 同上,使用整个特征图作为 Value。
# 注意力机制的输出形状为 (1, batch_size, embed_dim),即仅保留全局 token 的注意力输出。
x, _ = F.multi_head_attention_forward(
query=x[:1], # 全局 token 作为 Query
key=x, # 整个特征图作为 Key
value=x, # 整个特征图作为 Value
embed_dim_to_check=x.shape[-1], # 嵌入维度
num_heads=self.num_heads, # 注意力头的数量
q_proj_weight=self.q_proj.weight, # Query 的投影权重
k_proj_weight=self.k_proj.weight, # Key 的投影权重
v_proj_weight=self.v_proj.weight, # Value 的投影权重
in_proj_weight=None, # 不使用合并的投影权重
in_proj_bias=torch.cat([self.q_proj.bias, self.k_proj.bias, self.v_proj.bias]), # Query, Key, Value 的偏置
bias_k=None, # Key 的额外偏置(未使用)
bias_v=None, # Value 的额外偏置(未使用)
add_zero_attn=False, # 不添加额外的全零注意力头
dropout_p=0, # 不使用 Dropout
out_proj_weight=self.c_proj.weight, # 输出投影的权重
out_proj_bias=self.c_proj.bias, # 输出投影的偏置
use_separate_proj_weight=True, # 使用单独的 Query, Key, Value 投影权重
training=self.training, # 根据当前模式确定是否训练
need_weights=False # 不需要返回注意力权重
)
# 5. 移除第一个维度
# 注意力输出的形状为 (1, batch_size, embed_dim),去掉第一个维度后,变为 (batch_size, embed_dim)。
return x.squeeze(0)
注意力计算 Q K V
0
0-1
0-2
………
0-14这样的信息在文字序列里面,不断的进行预测
但是对于图像数据 16个像素点 用注意力机制压缩到一行里面去,图像转化为注意力机制能够接受的对象 把宽度和高度+1 多出的一行 作为注意力机制的信息 ,必须由(bs, channels, heights, width)—> (bs, output_dim)
注意力池化流程
- 输入:
- 特征图(batch_size, channels, height, width)。
- 特征图被展平到(spatial_dim, batch_size, embed_dim)以适配注意力机制。
- 处理步骤:
- Flatten 特征图:
- 将特征图展平(flatten),从(N, C, H, W)变为(HW, N, C),即空间位置作为序列。
- 添加全局 token:
- 为每个图像添加一个全局 token,代表全局的上下文信息。
- 位置嵌入 (Positional Embedding):
- 为每个位置加上位置信息,保留空间结构。
- 多头注意力机制:
- 使用多头注意力,计算每个位置的重要性权重,并整合为全局特征向量。
- 输出变换:
- 通过线性投影层调整最终输出的维度(output_dim)。
- Flatten 特征图:
- 输出:
- 一个全局特征向量,表示整幅图像的特征。
完整的残差网实现
class ModifiedResNet(nn.Module):
"""
一个基于 ResNet 的改进版本,包含以下变化:
- 起始部分使用了 3 层卷积(stem),而不是原始 ResNet 的单层卷积,同时将 max pool 替换为 avg pool。
- 对于 stride > 1 的卷积层,使用了反锯齿(anti-aliasing)的卷积方式,即在卷积前添加了 avg pool。
- 最终的全局池化层替换为 QKV 注意力池化层,而不是传统的 average pool。
"""
def __init__(self, layers, output_dim, heads, input_resolution=224, width=64):
"""
初始化函数。
参数:
- layers: 每个阶段的残差块数量(如 [3, 4, 6, 3] 表示每个阶段的残差块数)。
- output_dim: 输出特征的维度。
- heads: 注意力池化的头数。
- input_resolution: 输入图像的分辨率(默认 224)。
- width: 初始通道宽度(默认 64)。
"""
super().__init__()
self.output_dim = output_dim
self.input_resolution = input_resolution
# 初始化阶段(stem):3 层卷积代替原来的单层卷积
self.conv1 = nn.Conv2d(3, width // 2, kernel_size=3, stride=2, padding=1, bias=False) # 输入通道为3,输出为 width//2
self.bn1 = nn.BatchNorm2d(width // 2)
self.relu1 = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(width // 2, width // 2, kernel_size=3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(width // 2)
self.relu2 = nn.ReLU(inplace=True)
self.conv3 = nn.Conv2d(width // 2, width, kernel_size=3, padding=1, bias=False) # 输出为 width
self.bn3 = nn.BatchNorm2d(width)
self.relu3 = nn.ReLU(inplace=True)
self.avgpool = nn.AvgPool2d(2) # 平均池化,用于下采样
# 残差层
self._inplanes = width # 初始输入通道数,用于动态调整残差块的输入
self.layer1 = self._make_layer(width, layers[0]) # 第一阶段残差块
self.layer2 = self._make_layer(width * 2, layers[1], stride=2) # 第二阶段残差块,输出通道翻倍
self.layer3 = self._make_layer(width * 4, layers[2], stride=2) # 第三阶段残差块
self.layer4 = self._make_layer(width * 8, layers[3], stride=2) # 第四阶段残差块
# 注意力池化层
embed_dim = width * 32 # ResNet 的特征维度,等于最后一个阶段的输出通道数
self.attnpool = AttentionPool2d(input_resolution // 32, embed_dim, heads, output_dim) # QKV 注意力池化
def _make_layer(self, planes, blocks, stride=1):
"""
创建一个残差模块(由多个 Bottleneck 组成)。
参数:
- planes: 当前阶段的通道数。
- blocks: 残差块数量。
- stride: 当前阶段的步幅。
返回值:
- 一个由多个残差块组成的序列模块。
"""
layers = [Bottleneck(self._inplanes, planes, stride)] # 第一个残差块,可能需要调整步幅
self._inplanes = planes * Bottleneck.expansion # 更新通道数
for _ in range(1, blocks):
layers.append(Bottleneck(self._inplanes, planes)) # 后续残差块保持步幅为 1
return nn.Sequential(*layers) # 返回残差块的序列
def forward(self, x):
"""
前向传播函数。
参数:
- x: 输入张量,形状为 (batch_size, channels, height, width)。
返回值:
- x: 输出张量,经过特征提取和注意力池化后。
"""
def stem(x):
"""
起始阶段(stem):连续 3 层卷积 + 平均池化。
参数:
- x: 输入张量。
返回值:
- x: 经过 stem 阶段后的特征张量。
"""
x = self.relu1(self.bn1(self.conv1(x))) # 第1层卷积
x = self.relu2(self.bn2(self.conv2(x))) # 第2层卷积
x = self.relu3(self.bn3(self.conv3(x))) # 第3层卷积
x = self.avgpool(x) # 平均池化
return x
x = x.type(self.conv1.weight.dtype) # 将输入类型调整为卷积权重的类型(支持混合精度训练)
x = stem(x) # 经过起始阶段
x = self.layer1(x) # 第一阶段残差层
x = self.layer2(x) # 第二阶段残差层
x = self.layer3(x) # 第三阶段残差层
x = self.layer4(x) # 第四阶段残差层
x = self.attnpool(x) # 最后通过注意力池化
return x
# 实例化模型
model = ModifiedResNet(layers=[3, 4, 6, 3], output_dim=512, heads=8)
# 输入示例数据
input_data = torch.randn(10, 3, 224, 224) # (batch_size, channels, height, width)
output = model(input_data)
print("输出形状:", output.shape)
3. Vit (Vision transformer)
如何把四维的图像数据 变为transformer能够处理的三维数据信息,简单来说就是降维 但是降维的过程并不容易。
这个是图片的四维数据(bs, c(每种像素有多少维度 一般值为3), w, h)
在此基础上进行切为小块(bs, c, patch_nums, patch_size, patch_size)每个分片一个向量
再次进行转化为每个分片 (bs, patch_nums, c * patch_size*patch_size)
c * patch_size*patch_size ==> d_model
(bs, patch_nums, d_model) 这个叫做分片patch Embedding
就这个过程不一样 其余的和transformer保持一致
Vision Transformer (ViT) 是一种将 Transformer 架构应用于计算机视觉任务的深度学习模型,旨在利用 Transformer 的全局建模能力处理图像数据。与传统的卷积神经网络(CNN)不同,ViT 不依赖卷积操作,而是通过将图像分割为固定大小的子块(Patch)并将这些子块视为序列输入,结合自注意力机制对全局特征进行建模。ViT 的主要特点在于,它能够直接处理图像的全局上下文关系,尤其在大规模数据集上(如 ImageNet 或 LAION)展现了极高的性能。
ViT 如何将图像信息转变为 Transformer 能够读取的序列?
在 ViT 中,负责转变信息的结构叫做 Patch Embedding。Patch Embedding 的任务是将图像转化为适合 Transformer 输入的嵌入序列。具体来说,ViT 会将输入图像按照固定大小P×P分割成若干个不重叠的子块(Patch),每个 Patch 被展平为一维向量,形状为P的平方⋅C,其中C是图像的通道数。随后,使用一个线性投影层(通常是一个全连接层)将每个 Patch 的一维向量投影到D维的特征空间中(D是 Transformer 的隐藏层维度),生成对应的 Patch Embedding
Patch Embedding
线性层适合用来投影向量(4*4,c)==> 16c 直接从二维抽成一维
代码patch embedding
import torch
import torch.nn as nn
class PatchEmbedding(nn.Module):
def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
"""
初始化 PatchEmbedding 类,用于将输入图像划分为若干 Patch,并投影到固定的嵌入维度。
参数:
- img_size: 输入图像的分辨率(默认为 224)。
- patch_size: 每个 Patch 的尺寸(默认为 16x16)。
- in_channels: 输入图像的通道数(默认为 3,即 RGB 图像)。
- embed_dim: 嵌入维度(Transformer 的输入特征维度,默认为 768)。
"""
super().__init__()
self.img_size = img_size # 保存输入图像的尺寸
self.patch_size = patch_size # 保存每个 Patch 的尺寸
self.num_patches = (img_size // patch_size) * (img_size // patch_size)
# 计算图像被划分的总 Patch 数量 = (图像宽度上的 Patch 数) * (图像高度上的 Patch 数)
self.patch_dim = patch_size * patch_size * in_channels
# 每个 Patch 的维度 = patch_size * patch_size * 通道数(即展平后的特征数)
# 定义一个线性层,将每个 Patch 的原始特征(patch_dim)投影到 Transformer 的嵌入维度(embed_dim)
self.projection = nn.Linear(self.patch_dim, embed_dim)
# 初始化一个可学习的参数,用于位置编码,形状为 (1, num_patches, embed_dim)
self.position_embedding = nn.Parameter(torch.randn(1, self.num_patches, embed_dim))
def forward(self, x):
"""
前向传播函数,将输入图像转换为 Patch 嵌入,并添加位置编码。
参数:
- x: 输入张量,形状为 (batch_size, in_channels, height, width)。
返回值:
- embeddings: 包含位置编码的 Patch 嵌入,形状为 (batch_size, num_patches, embed_dim)。
"""
# 获取输入张量的批量大小、通道数、高度和宽度
B, C, H, W = x.shape
# 确保输入图像的分辨率与定义的 img_size 一致
assert H == self.img_size and W == self.img_size, "Input size must match img_size"
# 将输入图像划分为 Patch,使用 unfold 函数:
# unfold(2, patch_size, patch_size) 在高度维度上以 patch_size 的窗口滑动并提取小块;
# unfold(3, patch_size, patch_size) 在宽度维度上以 patch_size 的窗口滑动并提取小块。
patches = x.unfold(2, self.patch_size, self.patch_size).unfold(3, self.patch_size, self.patch_size)
# contiguous() 确保数据在内存中是连续的;
# view 将张量重塑为 (B, num_patches, patch_dim),
# 其中每个 Patch 被展平为长度为 patch_dim 的向量。
patches = patches.contiguous().view(B, -1, self.patch_dim)
# 使用线性层将每个 Patch 的特征投影到嵌入维度(embed_dim),
# 输出的形状为 (B, num_patches, embed_dim)。
embeddings = self.projection(patches)
# 添加位置编码,位置编码的形状为 (1, num_patches, embed_dim),
# 会广播到 batch 的每个样本。
embeddings += self.position_embedding
# 返回添加了位置编码的 Patch 嵌入
return embeddings
更加高效的做法,采用卷积层来完成这个过程
只需要让卷积和等于步长的大小即可实现分片,剩下的被舍弃
- 更高的计算效率 使用卷积操作可以有效利用硬件加速:
- 并行计算:卷积操作在现代深度学习框架(如 PyTorch、TensorFlow)中得到了高度优化,可以充分利用 GPU 的并行计算能力,而展平每个 Patch 的传统方式需要额外的显式循环操作,效率较低。
- 少数据搬移:传统方式需要将图像数据重新组织成 [num_patches, patch_size・patch_size・channels],这涉及到大量的数据搬移。而卷积操作直接在输入图像上执行,无需显式展平,减少了数据搬移的开销。
- 自然的空间感知 卷积操作天生具有局部感知能力:
- 卷积核会在每个 Patch 内进行加权求和,相当于提取 Patch 的局部特征,而不是简单的展平数据。
- 如果需要,卷积还可以在 Patch 中实现更高级的特征提取(比如使用非线性激活函数或更深的卷积层),相比于简单的 Linear 投影更加灵活。
- 内存消耗更少 卷积操作直接作用于原始的高维图像输入,而传统方法中展平操作会生成一个高维度的中间结果:
- 在传统实现中,每个 16 × 16 的 Patch 会被展平成长度为 16 × 16 × 3 = 768 的向量。如果图像分辨率较高,Patch 较多,会导致内存占用增加。
- 使用 Conv2d 的实现直接输出 [batch_size, width, grid_height, grid_width],避免了显式展平产生的中间结果,从而节省内存。
- 代码简洁性与可扩展性 卷积的实现直接将 Patch 的划分和投影合并成一个操作:
- 简洁性:Conv2d 的 kernel_size 和 stride 参数天然对应 Patch 的大小和步幅,代码更加直观、简洁,省去了展平和逐个 Patch 投影的步骤。
- 可扩展性:如果需要更复杂的处理,可以直接在卷积层中添加更多的非线性操作(如激活函数)或深层网络结构,而不是依赖额外的模块。
- 支持任意大小的输入图像
- 传统方法:要求图像分辨率是 Patch 大小的整数倍(否则划分出的 Patch 数量不一致),而且需要显式处理 Padding。
- 卷积方法:Conv2d 中的 padding 参数能够自动处理边界情况,使得网络支持任意大小的输入图像。
- 与预训练 CNN 特征结合的潜力 在实际应用中,我们可能会用预训练的 CNN 特征作为 Transformer 的输入:
- 卷积操作本质上是 CNN 的核心,因此可以方便地与预训练模型(如 ResNet、EfficientNet)的特征对接。
- 这种方式可以充分利用卷积的特性提取更高质量的 Patch 特征。
- 灵活的 Patch 特征生成
- 传统方法:展平的 Patch 中,每个像素值直接作为输入,无法动态调整 Patch 内部的特征。
- 卷积方法:通过设计卷积核(如不同大小、深度或非线性处理),可以灵活生成更复杂的 Patch 特征。例如:
- 使用更大的卷积核捕获跨越多个像素的上下文关系;
- 使用多层卷积构造更丰富的层次化 Patch 表示
- 提前降维,减少计算量 如果输入图像分辨率较高(如 4K 图像),直接展平后的序列长度会非常长,导致 Transformer 的计算量急剧增加。而卷积方法可以在切片的同时降低通道数,从而有效减少计算量。
Vit和普通的transformer有什么区别(除了patch embedding)?
为了保留图像中 Patch 的位置信息,VIT 会在 Patch Embedding 中加入二维位置编码(Positional Encoding),以向模型提供顺序信息。这种位置编码可以是可学习的参数,或者基于正弦和余弦的固定位置编码。最终,所有 Patch Embedding 和位置编码的功能结果都组成一个序列。通过这种方式,VIT 将原始图像处理成了一个序列结构,充分利用了 Transformer 在自然语言处理任务中增长的序列建模能力,从而在图像分类、目标检测等视觉任务中实现了优异的性能。
除此之外,VIT支持类别的嵌入。类别的嵌入是一个 learnable 参数,直接添加到序列的开头,用于提取全局图像特征,就类似于我们在Bert当中所插入的(CLS)、这个token/patch用于收集全局信息。
同时,VIT所需要的数据量会比普通Transformer大很多。VIT被认为是一个图像的架构而不是文本的架构,因此VIT需要构建入Patch,而普通Transformer需要的输入是Token。
常规的Vit和CLIP的VIT之间有什么差别?
VIT全套代码
class LayerNorm(nn.LayerNorm):
"""
LayerNorm 的子类,用于处理 fp16(半精度浮点数)。
"""
def forward(self, x: torch.Tensor):
# 保存原始数据类型
orig_type = x.dtype
# 将输入转换为 float32 类型,执行 LayerNorm 操作
ret = super().forward(x.type(torch.float32))
# 恢复原始数据类型
return ret.type(orig_type)
class QuickGELU(nn.Module):
"""
实现 QuickGELU 激活函数,比标准 GELU 更简单且计算效率更高。
"""
def forward(self, x: torch.Tensor):
# QuickGELU 激活函数公式:x * sigmoid(1.702 * x)
return x * torch.sigmoid(1.702 * x)
class ResidualAttentionBlock(nn.Module):
"""
实现一个残差注意力模块,包括多头注意力机制和前馈网络(MLP)。
"""
def __init__(self, d_model: int, n_head: int, attn_mask: torch.Tensor = None):
"""
初始化函数。
参数:
- d_model: 模型的隐藏层维度(即特征向量的维度)。
- n_head: 多头注意力的头数。
- attn_mask: 注意力掩码,可选。
"""
super().__init__()
# 多头注意力模块
self.attn = nn.MultiheadAttention(d_model, n_head)
# 第一个 LayerNorm
self.ln_1 = LayerNorm(d_model)
# 前馈网络(MLP),包括线性层、QuickGELU 激活和另一层线性变换
self.mlp = nn.Sequential(OrderedDict([
("c_fc", nn.Linear(d_model, d_model * 4)), # 扩展特征维度 4 倍
("gelu", QuickGELU()), # QuickGELU 激活
("c_proj", nn.Linear(d_model * 4, d_model)) # 恢复到原始特征维度
]))
# 第二个 LayerNorm
self.ln_2 = LayerNorm(d_model)
# 注意力掩码
self.attn_mask = attn_mask
def attention(self, x: torch.Tensor):
"""
计算多头注意力。
"""
# 如果存在注意力掩码,将其转换为与输入相同的数据类型和设备
self.attn_mask = self.attn_mask.to(dtype=x.dtype, device=x.device) if self.attn_mask is not None else None
# 调用多头注意力,返回注意力结果(不需要权重)
return self.attn(x, x, x, need_weights=False, attn_mask=self.attn_mask)[0]
def forward(self, x: torch.Tensor):
"""
前向传播。
"""
# 1. 多头注意力:先进行 LayerNorm,再计算注意力,并与输入 x 相加(残差连接)
x = x + self.attention(self.ln_1(x))
# 2. 前馈网络:先进行 LayerNorm,再通过 MLP,并与输入 x 相加(残差连接)
x = x + self.mlp(self.ln_2(x))
return x
class Transformer(nn.Module):
"""
基于 ResidualAttentionBlock 的 Transformer 模块。
"""
def __init__(self, width: int, layers: int, heads: int, attn_mask: torch.Tensor = None):
"""
初始化函数。
参数:
- width: 模型的隐藏层维度。
- layers: Transformer 层数。
- heads: 多头注意力的头数。
- attn_mask: 注意力掩码。
"""
super().__init__()
self.width = width
self.layers = layers
# 堆叠多个 ResidualAttentionBlock,构成完整的 Transformer
self.resblocks = nn.Sequential(*[ResidualAttentionBlock(width, heads, attn_mask) for _ in range(layers)])
def forward(self, x: torch.Tensor):
"""
前向传播。
"""
return self.resblocks(x)
class VisionTransformer(nn.Module):
"""
实现 Vision Transformer,用于处理图像输入。
"""
def __init__(self, input_resolution: int, patch_size: int, width: int, layers: int, heads: int, output_dim: int):
"""
初始化函数。
参数:
- input_resolution: 输入图像的分辨率(如 224)。
- patch_size: 图像切片的尺寸(如 16x16)。
- width: Transformer 的隐藏层维度。
- layers: Transformer 层数。
- heads: 多头注意力的头数。
- output_dim: 输出特征维度。
"""
super().__init__()
self.input_resolution = input_resolution
self.output_dim = output_dim
# 图像切片卷积,将输入图像分割为若干 patch,并映射到 width 维度
self.conv1 = nn.Conv2d(in_channels=3, out_channels=width, kernel_size=patch_size, stride=patch_size, bias=False)
# 类别嵌入,用于标识全局图像特征
scale = width ** -0.5
self.class_embedding = nn.Parameter(scale * torch.randn(width))
# 位置嵌入,用于编码每个 patch 的位置
self.positional_embedding = nn.Parameter(scale * torch.randn((input_resolution // patch_size) ** 2 + 1, width))
# 输入的 LayerNorm
self.ln_pre = LayerNorm(width)
# Transformer 编码器
self.transformer = Transformer(width, layers, heads)
# 输出的 LayerNorm 和投影层
self.ln_post = LayerNorm(width)
self.proj = nn.Parameter(scale * torch.randn(width, output_dim))
def forward(self, x: torch.Tensor):
"""
前向传播。
参数:
- x: 输入图像张量,形状为 (batch_size, 3, H, W)。
返回值:
- x: 输出特征,形状为 (batch_size, output_dim)。
"""
# 图像切片,将输入分割为 patch 并映射到特征维度
x = self.conv1(x) # 形状 = [batch_size, width, grid, grid]
# 将每个 patch 展平,并调整为 [batch_size, num_patches, width]
x = x.reshape(x.shape[0], x.shape[1], -1) # 形状 = [batch_size, width, grid ** 2]
x = x.permute(0, 2, 1) # 调整为 [batch_size, num_patches, width]
# 添加类别嵌入
x = torch.cat([self.class_embedding.to(x.dtype) + torch.zeros(x.shape[0], 1, x.shape[-1], dtype=x.dtype, device=x.device), x], dim=1) # 形状 = [batch_size, num_patches + 1, width]
# 加上位置嵌入
x = x + self.positional_embedding.to(x.dtype)
# 输入的 LayerNorm
x = self.ln_pre(x)
# Transformer 编码器
x = x.permute(1, 0, 2) # 调整为 [num_patches + 1, batch_size, width]
x = self.transformer(x)
x = x.permute(1, 0, 2) # 调整回 [batch_size, num_patches + 1, width]
# 输出特征仅取第一个(全局图像特征)
x = self.ln_post(x[:, 0, :])
# 如果存在投影层,将其映射到输出维度
if self.proj is not None:
x = x @ self.proj
return x
# 调整参数以减少内存占用
input_resolution = 224 # 分辨率
width = 512 # 隐藏层维度
layers = 6 # Transformer 层数
heads = 8 # 注意力头数
output_dim = 512 # 输出特征维度
batch_size = 10 # batch大小
# 重新定义 VisionTransformer 模型
vit = VisionTransformer(
input_resolution=input_resolution,
patch_size=16,
width=width,
layers=layers,
heads=heads,
output_dim=output_dim
)
# 假设输入图像数据 (10,3,224,224)
input_data = torch.randn(batch_size, 3, input_resolution, input_resolution)
# 通过模型前向传播
output = vit(input_data)
# 打印输出的形状
output.shape
4. 文本编码器—经典transformer架构(大家应该都会就简略解释了)
在 CLIP 中,Transformer 作为文本编码器,其结构与标准的 Transformer 中的编码器结构高度相似,由**嵌入层(Embedding Layer)、多层的自注意力模块(Self-Attention)、前馈网络(Feed-Forward Network, FFN)和归一化(LayerNorm)**组成。
首先,输入文本通过嵌入层,将每个词映射为固定维度的向量,同时添加可学习的位置编码,以保留序列的位置信息。随后,这些嵌入通过多层 Transformer 模块,每层由多头自注意力(Multi-Head Attention)和 FFN 组成,并在每个子模块后加上残差连接(Residual Connection)和 LayerNorm。Transformer 的作用是逐层捕获文本序列中的上下文关系和全局语义特征。最终,文本的全局语义特征通过特定的标记(例如 [EOS] token)的表示提取,并投影到与图像特征一致的嵌入空间中,用于多模态匹配。这种结构使 CLIP 能够高效地对齐图像和文本的语义信息,实现跨模态理解。
5. CLIP的预训练
训练有两种:
有监督训练 ==> 真实的标签(我有数据、同时我也有数据所对应的标准答案)
让算法输出它的答案,和真实的答案来进行对比
构建损失函数(真实值 - 预测值),我们通过最小化损失函数(最小化真实值与预测值之间的差异)
来迭代我们神经网络的权重、从而影响神经网络输出
必须有标签
无监督训练 ==> 无法提供如此大量的有标签的数据
很多任务天生就是没有标签
预训练 ==> 预训练都是无监督的(没有标签的)
- 当前预训练大概率是无监督的,那输入给模型的数据是什么呢?
- 在无监督的情况下,当前模型计算的是什么损失?怎么样来迭代权重的?
现在我们有了图像信息和文本信息,但一个多模态模型如何将这两类信息整合起来呢?就像文字架构预训练目标是创造出 “能够理解基础文字信息”、多模态架构的预训练目标就是 “能够理解基础文字和基础图像信息”。因此,CLIP 的预训练流程是让架构本身能够理解图像和文本的流程。CLIP 通过学习图像和文字的匹配关系,成功地将两种模态的信息(图像和文本)映射到同一个语义空间,这使得它能够理解并关联图像和文本,从而实现强大的多模态任务能力,比如跨模态检索、分类、生成等。
在 CLIP 中,计算余弦相似度和匹配图像与文本特征的过程可以概括为:
- 输入图 - 文对数据集作为初始数据、各自提取图像和文本特征。
- 通过归一化、点积等流程,计算图像和文本的配对余弦相似度。
- 实现对比学习(Contrastive Learning)、使用对比损失作为损失函数,对比损失越高说明匹配越不精准 ——
基于损失函数来调整相似度矩阵,调整的方向是让配对样本的余弦相似度尽可能接近 1,而非配对样本尽可能接近 0。
由于要推动相似度矩阵变得更精准、因此损失函数也会优化图像编码结果和文字编码结果(也就是优化图像编码器和文字编码器的权重),如果样本是配对的、那拉进两种编码结果在语义空间中更接近,如果样本不是配对的、则将样本的编码结果在语义空间中分离。 - 最终,将图像和文本的语义表示统一到一个多模态的共享空间中、使得图像和文字能够被精准理解,例如通过输入文本检索相关图像,或者通过图像查找相关文本描述,实现高效的跨模态搜索和匹配。(但是注意它没有文生图的功能!!!)