多模态基础--CNN和ViT

举报
剑指南天 发表于 2026/05/29 14:07:56 2026/05/29
【摘要】 根据 Transformer 架构的特点,Transformer 可以处理任何序列数据,不仅适用于文本,还适用于图像。与卷积神经网络相比,Transformer模型也能够取得出色的结果。

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)

        # QK的点积
        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  # 训练10epoch
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 模型的输入,可以使模型同时具有理解并融合多种不同类型信息的能力

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。