多模态基础--CNN和ViT
1.概述
Transformer 架构在自然语言处理上面已经占据不可替代的地位。根据 Transformer 架构的特点,Transformer 可以处理任何序列数据,不仅适用于文本,还适用于图像。与卷积神经网络相比,Transformer模型也能够取得出色的结果。
2. 卷积神经网络
卷积神经网络(Convolutional Neural Network,CNN)常被用于图像识别、语音识别等各种场合。它在计算机视觉领域表现尤为出色,广泛应用于图像分类、目标检测、图像分割等任务。
CNN 的能力主要基于卷积层(Convolution层)、池化层(Pooling层)和残差网络(Residual Network,ResNet),其中残差网络成为当代人工智能最重要的基础之一。下图是一个 CNN 的结构:

2.1 卷积层
卷积层用于提取输入数据的局部特征。相比于全连接层需要将输入拉平为1维数据,卷积层能够保存空间位置信息。
2.1.1 卷积计算
卷积层对数据进行卷积运算,卷积运算相当于图像处理中的滤波器运算。在3维数据的卷积运算中,输入数据的通道数和卷积核的通道数须设为相同的值。当有多个通道时,会按通道进行输入数据和卷积核的卷积运算,并将结果相加得到输出数据。



2.2 池化层
池化层缩小长、宽方向上的空间来进行降维,能够缩减模型的大小并提高计算速度。和卷积层不同,池化层没有要学习的参数,并且池化运算按通道独立进行,经过池化运算后数据的通道数不会发生变化。池化的另一个特点是对微小偏差具有鲁棒性,数据发生微小偏差时,池化可能会返回相同的结果。池化层有Max池化(计算窗口内的最大值)、Average池化(平均池化,计算窗口内的平均值)等。

2.3 填充和步幅
在进行卷积层或者池化层之前,有时要向输入数据的周围填入固定的数据(比如0),这称为填充(padding)。应用卷积核或者池化的位置间隔称之为步幅(stride)。
2.4 残差网络
2015年由微软团队(何恺明等人)提出,比之前的网络具有更深的结构。为了解决深度网络的梯度消失问题,ResNet引入了“残差连接”(或者“跳跃连接”),这种网络结构也被称为“残差网络”(Residual Network,ResNet)。ResNet有效地解决了深度网络中的梯度消失和梯度爆炸问题,在加深层的同时,提高了网络性能。

2.5 代码实践手写数字识别
import time
from pathlib import Path
import torch
import torchvision.transforms as T
from torch import nn, optim, utils
from torch.utils.tensorboard import SummaryWriter
from torchvision.datasets.mnist import MNIST
# 基础配置
ROOT_DIR = Path(__file__).parent.parent
device = 'cuda' if torch.cuda.is_available() else 'cpu'
log_dir = ROOT_DIR / 'ch08_cnn'/'logs'
# 超参数配置
epochs = 10
batch_size = 128
lr = 0.001
# 加载数据集
transform = T.Compose([T.ToTensor()])
train_set = MNIST(
root="./datasets", train=True, download=True, transform=transform
)
test_set = MNIST(
root="./datasets", train=False, download=True, transform=transform
)
train_loader = utils.data.DataLoader(train_set, shuffle=True, batch_size=batch_size)
test_loader = utils.data.DataLoader(test_set, shuffle=False, batch_size=batch_size)
# 初始化模型
model = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(), # 拉平
nn.Linear(400, 120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.Sigmoid(),
nn.Linear(84, 10),
).to(device=device)
# 初始化损失函数
loss = nn.CrossEntropyLoss()
# 初始化优化器
optimizer = optim.Adam(model.parameters(), lr=lr)
# 开始训练
with SummaryWriter(log_dir=str(log_dir / time.strftime('%Y-%m-%d_%H-%M-%S'))) as writer:
for epoch in range(epochs):
training_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
optimizer.zero_grad()
loss_v = loss(outputs, labels)
loss_v.backward()
optimizer.step()
training_loss += loss_v.item()
writer.add_scalar('loss', training_loss, epoch + 1)
print(
f'Epoch {epoch + 1}/{epochs} loss: {training_loss / len(train_loader):.3f}')
# 开始测试
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'\n预测准确率: {100 * correct // total} %')

3. Vision Transformer

3.1 补丁嵌入(Patch Embedding)
创建Vision Transformer 的第一步是将输入图像拆分为补丁,并创建这些补丁的线性嵌入序列。我们能够通过使用 PyTorch 的 Conv2d 方法来实现这一点。Conv2d 方法获取输入图像,将它们拆分为补丁,并提供大小等于 d_model 的线性投影。通过将kernel_size 和步幅设置为补丁大小,确保补丁大小正确且没有重叠。

调用 flatten 方法将补丁列和补丁行维度组合成一个补丁维度,从而得到 (B, d_model, P) 的形状。
最后使用转置方法切换 d_model 和补丁维度,得到 (B, P, d_model) 的形状。

3.2 位置编码补足补丁嵌入的空间信息,作为 Transformer 的输入。
3.3 类别对应的 token
编码器模型的代表是 BERT 模型。参考 BERT 单句分类任务的处理方式,使用 [CLS] 的输出向量,经过线性层用于情感极性判断、语法可接受性判断等。

3.4 Transformer 编码器
Transformer 编码器由两个子层组成:第一个子层执行多头注意力,第二个子层包含MLP。残差连接与层归一化用于缓解模型训练中的梯度消失、收敛困难等问题,对于Transformer能够堆叠多层至关重要。
3.5 代码实践手写数字识别
import torch
import time
import torch.nn as nn
import torchvision.transforms as T
from torch.optim import Adam
from torchvision.datasets.mnist import MNIST
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from pathlib import Path
import numpy as np
# 补丁嵌入
class PatchEmbedding(nn.Module):
def __init__(self,d_model,patch_size,n_channels):
super().__init__()
self.d_model = d_model # 卷积核的数量或者输入的channel数量
self.patch_size = patch_size # 卷积核的形状或者将要切分的块的形状; 应该保证图像能被切分成整块
self.n_channels = n_channels # 输入图像的通道数
self.linear_project = nn.Conv2d(self.n_channels,self.d_model,kernel_size=self.patch_size,stride=self.patch_size)
# B: 批次大小 C: 通道数量 H: 图像高度 W: 图像宽度 P_col: 补丁的列 P_row: 补丁的行
def forward(self, x):
# (B, C, H, W) -> (B, d_model, P_col, P_row)
x = self.linear_project(x)
x = x.flatten(2) # (B, d_model, P_col, P_row) -> (B, d_model, P)
x = x.transpose(1, 2) # (B, d_model, P) -> (B, P, d_model)
return x
# 位置编码
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_seq_length):
super().__init__()
# 类别token
self.cls_token = nn.Parameter(torch.randn(1, 1, d_model))
# 初始化编码矩阵 (max_len, d_model)
pe = torch.zeros(size=(max_seq_length, d_model))
# 当前词在序列中的位置 (max_len, 1)
pos = torch.arange(0, max_seq_length).unsqueeze(1)
# 表示公式中2i (d_model/2, )
_2i = torch.arange(0, d_model, 2)
# 计算10000**(2i/d_model) (d_model/2, )
div_term = torch.pow(10000, (_2i / d_model))
# 按奇偶数维度计算位置编码值 (max_len, d_model)
pe[:, 0::2] = torch.sin(pos / div_term)
if d_model % 2 == 1:
_2i1 = torch.arange(0, d_model-1, 2)
div_term = torch.pow(10000, (_2i1 / d_model))
pe[:, 1::2] = torch.cos(pos / div_term)
self.register_buffer("pe", pe)
def forward(self, x):
# 为批次中的每张图片分配一个类别token
tokens_batch = self.cls_token.expand(x.size()[0], -1, -1)
# 将类别token添加到每个图像的补丁嵌入数组的开头
x = torch.cat((tokens_batch, x), dim=1)
# 将位置编码添加到嵌入中
x = x + self.pe
return x
# 注意力头
class AttentionHead(nn.Module):
def __init__(self, d_model, head_size):
super().__init__()
self.head_size = head_size
self.query = nn.Linear(d_model, head_size)
self.key = nn.Linear(d_model, head_size)
self.value = nn.Linear(d_model, head_size)
def forward(self, x):
# 计算Q, K, V
Q = self.query(x)
K = self.key(x)
V = self.value(x)
# Q和K的点积
attention = Q @ K.transpose(-2, -1)
# 缩放
attention = attention / (self.head_size ** 0.5)
attention = torch.softmax(attention, dim=-1)
attention = attention @ V
return attention
# 多注意头
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
self.head_size = d_model // n_heads
self.W_o = nn.Linear(d_model, d_model)
self.heads = nn.ModuleList([AttentionHead(d_model, self.head_size) for _ in range(n_heads)])
def forward(self, x):
# 拼接多个注意力头
out = torch.cat([head(x) for head in self.heads], dim=-1)
out = self.W_o(out)
return out
# 多注意头 + 全连接层 + 层归一化和残差链接
class TransformerEncoder(nn.Module):
def __init__(self, d_model, n_heads, r_mlp=4):
super().__init__()
self.d_model = d_model
self.n_heads = n_heads
# 层归一化
self.ln1 = nn.LayerNorm(d_model)
# 多头注意力
self.mha = MultiHeadAttention(d_model, n_heads)
# 层归一化
self.ln2 = nn.LayerNorm(d_model)
# MLP
self.mlp = nn.Sequential(
nn.Linear(d_model, d_model * r_mlp),
nn.GELU(),
nn.Linear(d_model * r_mlp, d_model)
)
def forward(self, x):
# 第一次层归一化之后的残差
out = x + self.mha(self.ln1(x))
# 第二次层归一化之后的残差
out = out + self.mlp(self.ln2(out))
return out
class VisionTransformer(nn.Module):
def __init__(self,d_model,n_classes,img_size,patch_size,n_channels,n_heads,n_layers):
super().__init__()
assert img_size[0] % patch_size[0] == 0 and img_size[1] % patch_size[1] == 0, "img_size 必须能被 patch_size 整除"
assert d_model % n_heads == 0, "d_model 必须能被 n_heads 整除"
self.d_model = d_model # 模型维度,嵌入的维度(宽度)
self.n_classes = n_classes # 类别的数量
self.img_size = img_size # 图片大小
self.patch_size = patch_size # 补丁大小
self.n_channels = n_channels # 通道数
self.n_heads = n_heads # 注意力头的数量
# 补丁的数量 = (32x32) // (4x4)
self.n_patches = (self.img_size[0] * self.img_size[1]) // (self.patch_size[0] * self.patch_size[1])
# 序列的长度 = 1(分类token) + 补丁的数量
self.max_seq_length = self.n_patches + 1
# 补丁嵌入
self.patch_embedding = PatchEmbedding(
self.d_model,
self.patch_size,
self.n_channels
)
# 位置编码
self.positional_encoding = PositionalEncoding(
self.d_model,
self.max_seq_length
)
self.transformer_encoder = nn.Sequential(*[
TransformerEncoder(self.d_model, self.n_heads)
for _ in range(n_layers)
])
# 用于分类的MLP
self.classifier = nn.Sequential(
nn.Linear(self.d_model, self.n_classes),
nn.Softmax(dim=-1)
)
def forward(self, images):
# 将图片转换成补丁的嵌入(embedding)
x = self.patch_embedding(images)
# 添加位置编码
x = self.positional_encoding(x)
# 编码
x = self.transformer_encoder(x)
# 分类的线性层
x = self.classifier(x[:, 0])
return x
# 基础配置
ROOT_DIR = Path(__file__).parent.parent
device = 'cuda' if torch.cuda.is_available() else 'cpu'
log_dir = ROOT_DIR / 'logs'
# 超参数配置
d_model = 12 # 嵌入的维度12
n_classes = 10 # 类别数量为10
img_size = (32, 32) # 图片大小为32x32
patch_size = (16, 16) # 补丁的大小是16x16
n_channels = 1 # 灰度图片通道数量为1
n_heads = 4 # 4个注意力头
n_layers = 3 # 3层编码器
batch_size = 128 # 每个批次128张图片
epochs = 10 # 训练10个epoch
alpha = 0.005 # 学习率5e-3
# 准备数据
transform = T.Compose([T.Resize(img_size),T.ToTensor()])
train_set = MNIST(
root="./datasets", train=True, download=True, transform=transform
)
test_set = MNIST(
root="./datasets", train=False, download=True, transform=transform
)
train_loader = DataLoader(train_set, shuffle=True, batch_size=batch_size)
test_loader = DataLoader(test_set, shuffle=False, batch_size=batch_size)
# 初始化模型
ViT = VisionTransformer(
d_model,
n_classes,
img_size,
patch_size,
n_channels,
n_heads,
n_layers
).to(device)
# 初始化优化器
optimizer = Adam(ViT.parameters(), lr=alpha)
# 初始化损失函数
loss = nn.CrossEntropyLoss()
# 开始训练
with SummaryWriter(log_dir=str(log_dir / time.strftime('%Y-%m-%d_%H-%M-%S'))) as writer:
for epoch in range(epochs):
training_loss = 0.0
for i, data in enumerate(train_loader, 0):
# 取出图像和对应的标签
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = ViT(inputs)
# 交叉熵损失
loss_v = loss(outputs, labels)
loss_v.backward()
optimizer.step()
training_loss += loss_v.item()
writer.add_scalar('loss', training_loss, epoch + 1)
print(
f'Epoch {epoch + 1}/{epochs} loss: {training_loss / len(train_loader):.3f}')
correct = 0
total = 0
# 开始测试
with torch.no_grad():
for data in test_loader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = ViT(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'\n预测准确率: {100 * correct // total} %')

4. 总结:Vision Transformer 证明将文本,图像,音频等经过处理 Embedding 作为 Transformer 模型的输入,可以使模型同时具有理解并融合多种不同类型信息的能力。
- 点赞
- 收藏
- 关注作者
评论(0)