写在前面
在机器学习的世界里,深度学习已经成为了推动各类智能系统发展的核心技术。从基础的多层感知机(MLP)到强大的卷积神经网络(CNN)如VGG、ResNet,再到能够生成数据的生成对抗网络(GAN),深度学习模型的演变为我们带来了前所未有的进展和机遇。
在我的《机器学习》专栏的其他博文里已经记录了一些基础模型和神经网络基础概念的内容。
在本篇博文中,我将分享一些经典且有影响力的深度学习模型。通过逐步深入了解这些模型的架构、工作原理、训练过程以及各自的创新点和火爆的原因,我们可以更好地掌握它们在实际应用中的优势和挑战,并由此或多或少提出自己的(小)创新点思路。这也是我在学习深度学习和《智能计算系统-从深度学习到大模型(第二版-陈云霁等)》第二/三章过程中对不同模型的总结与体会。我将在文章中配合来自这本教材的习题,分享一些相关的验证代码来展开这篇学习笔记博文。
暂不涉及具体的参数计算过程和方法的推导,本文只是对这些有名的模型做一个“了解性质”的介绍。由于本期博客简单的汇总了所有较为典型的模型、方便整理和大家对比各深度学习网络模型,故篇幅也显得略长。
目录
SENet (Squeeze-and-Excitation Networks)
R-CNN (Regions with CNN Features)
SSD (Single Shot Multibox Detector)
GAN (Generative Adversarial Networks)
变分自编码器(Variational Autoencoder, VAE)
RNN (Recurrent Neural Network)
Seq2Seq (Sequence-to-Sequence)
缩放点积注意力 (Scaled Dot-Product Attention)
GPT(Generative Pre-trained Transformer)
BERT(Bidirectional Encoder Representations from Transformers)
CLIP(Contrastive Language-Image Pre-training)
多层感知机(MLP)
多层感知机(MLP,Multilayer Perceptron) 是一种典型的前馈神经网络,它由多个层次的神经元组成,通常包括一个输入层、一个或多个隐藏层和一个输出层。MLP 是深度学习中最基础的神经网络之一,它是构建其他复杂神经网络(如卷积神经网络、循环神经网络等)的基础。
网络结构
- 输入层(Input Layer):接收外部数据,每个神经元代表一个特征。输入层的大小与特征数量一致。
- 隐藏层(Hidden Layer):通常包括多个神经元,用于进行信息处理和抽象。MLP 至少包含一个隐藏层,深度网络通常有多个隐藏层。
- 输出层(Output Layer):根据任务要求,输出层的神经元个数对应于分类任务的类别数或回归任务的目标值维度。
习题2.2 假设有一个只有1个隐层的多层感知机,其输入、隐层、输出层的神经元个数分别为33、512、10,那么这个多层感知机中总共有多少个参数是可以被训练的?
对于全连接层,参数包括:
- 权重(weights):形状为输入神经元数×隐藏层神经元数: 33×512=16896
- 偏置(biases):每个隐藏层神经元一个偏置,共 512
所以,输入层到隐藏层的参数总数为:16896+512=17408
隐藏层到输出层参数包括:
- 权重:形状为隐藏层神经元数×输出层神经元数: 512×10=5120
- 偏置:每个输出层神经元一个偏置,共 10
所以,隐藏层到输出层的参数总数为:5120+10=5130.
将两部分参数相加即得到整个网络的总可训练参数数:17408+5130=22538
验证代码
以下给出一段用PyTorch创建MLP:通过 nn.Module
、nn.Linear
和 torch.relu
的组合,计算出所有可训练参数的数量的示例代码
import torch
import torch.nn as nn
class SimpleMLP(nn.Module):
def __init__(self, input_dim=33, hidden_dim=512, output_dim=10):
super(SimpleMLP, self).__init__()
# 全连接层:input_dim -> hidden_dim
self.fc1 = nn.Linear(input_dim, hidden_dim)
# 全连接层:hidden_dim -> output_dim
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x
# 实例化模型
model = SimpleMLP()
# 计算总的可训练参数数量
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("Number of trainable parameters:", total_params)
# 如果想打印各层的参数形状,也可以这样做:
for name, param in model.named_parameters():
print(name, param.shape, param.numel())
Number of trainable parameters: 22538
fc1.weight torch.Size([512, 33]) 16896
fc1.bias torch.Size([512]) 512
fc2.weight torch.Size([10, 512]) 5120
fc2.bias torch.Size([10]) 10
数学公式
在 MLP 中,每一层的神经元都与上一层的神经元全连接。对于每个神经元,计算的过程可以表示为:
输入层到隐藏层(全连接)
对于第 i 个隐藏层神经元,它的输出是通过输入信号进行加权求和后,再加上偏置项,并通过激活函数得到的。具体来说,对于每个隐藏层神经元的计算过程为:
;
是从第 (l-1) 层到第 l 层的权重矩阵,形状为 (输入维度×输出维度)
是上一层的输出(或输入层的输入数据)
是偏置项
是激活函数(如 ReLU、Sigmoid、Tanh 等(具体可见我的《入门机器学习专栏-6.神经网络》)),它帮助神经网络捕捉非线性特征
隐藏层到输出层
输出层的计算与隐藏层相似,只是它最终输出的是网络的预测值或分类结果:
;
是从最后一个隐藏层到输出层的权重矩阵
是最后一个隐藏层的输出
是输出层的偏置项
是输出层的激活函数(对于分类任务常用 Softmax,对于回归任务常用线性激活)
最终使用损失函数(或目标函数)用于衡量模型预测值与实际值之间的差异(具体可见我的《入门机器学习专栏-6.神经网络》))。在 MLP 中,常见的损失函数包括:
- 均方误差(MSE):用于回归任务,计算公式为
- 交叉熵损失(Cross-Entropy Loss):用于分类任务,计算公式为
训练过程
MLP 的训练过程通常采用 梯度下降(Gradient Descent) 或其变种,如 Adam、SGD(随机梯度下降) 等优化算法。训练过程中会通过 反向传播(Backpropagation) 算法来计算每一层的梯度,并更新网络中的参数(权重和偏置)以最小化损失函数。(梯度的概念、优化算法等基础概念具体可见我的《入门机器学习专栏-6.神经网络》))
反向传播过程包括以下步骤:
- 前向传播(Forward Propagation):计算每一层的输出,并最终获得预测结果。
- 计算损失(Loss Calculation):使用损失函数计算模型预测值与实际值之间的误差。
- 反向传播(Backward Propagation):通过链式法则计算损失对每个参数的梯度。
- 参数更新(Parameter Update):使用优化算法更新网络中的权重和偏置。
习题2.3 反向传播中,神经元的梯度是如何计算的?权重是如何更新的?
1. 计算损失函数(Loss Function):首先,网络会根据输入数据进行前向传播,计算出每一层的输出,并最终得到网络的预测值。然后,通过损失函数计算出网络的预测值与真实值之间的误差。损失函数的值表示了网络当前预测值与目标值之间的差异,目标是最小化这个损失。
2. 计算梯度(反向传播):接下来,需要计算损失函数相对于网络中每个参数(权重和偏置)的偏导数,即梯度。通过反向传播算法,神经网络计算损失函数相对于每一层的权重和偏置的导数。反向传播的步骤如下:
-
链式法则:反向传播的核心是链式法则,它允许将误差从输出层传递回输入层,逐层计算每一层的梯度。
设输出层的损失为 L,第 l 层的权重为
,第 l 层的输入为
,那么梯度计算的过程:
- 计算输出层的梯度:损失函数 L 对输出层激活值
的偏导数。
- 逐层反向计算:使用链式法则从输出层逐层传递误差,计算每一层的权重和偏置的梯度。
- 计算输出层的梯度:损失函数 L 对输出层激活值
例如,对于第 l 层的权重 ,梯度计算为:
3. 权重更新(梯度下降):有了每一层参数的梯度后,就可以通过 梯度下降(Gradient Descent) 或其变种(如 Adam 优化器)来更新每一层的权重和偏置,目的是最小化损失函数。
权重的更新公式为:
其中:
是第 l 层的权重
- η 是学习率(learning rate),决定了每次更新的步长
是损失函数相对于权重的梯度
偏置的更新方法与权重类似:
4. 迭代过程:通过反向传播计算梯度并更新权重和偏置之后,模型会重新进行一次前向传播,计算新的预测结果,并再次计算损失。然后再通过反向传播更新权重。这个过程会反复进行,直到损失函数收敛到最小值或者达到预定的迭代次数。
防止过拟合的方法
1. 正则化(Regularization)通过在损失函数中加入额外的惩罚项,约束模型的复杂度,减少模型对训练数据的过度拟合。(具体可见我的《入门机器学习专栏-5.正则化技术》)
- L2正则化(Ridge Regression):在损失函数中加入权重的平方和惩罚项(
),使得模型的权重尽可能小,从而限制模型的复杂度。
- L1正则化(Lasso Regression):在损失函数中加入权重的绝对值和惩罚项(
),这不仅能避免过拟合,还可以产生稀疏的解(即部分权重变为零)。
2. 数据增强(Data Augmentation):通过对训练数据进行不同的变换(如旋转、翻转、缩放、裁剪等)来生成新的训练样本。这样做的目的是增加训练数据的多样性,帮助模型更好地泛化,减少对单一训练集的过度依赖。
- 对于图像数据,常见的增强方法包括旋转、翻转、裁剪、颜色变换等。
- 对于文本数据,可以进行同义词替换、数据噪声添加等方法。
3. 早停法(Early Stopping):在训练过程中监控模型在验证集上的性能,并在验证集的性能停止提升时提前终止训练的策略。这样可以防止模型在训练集上过度拟合。
- 在训练过程中,当验证集上的误差开始上升时,说明模型可能开始过拟合。此时,可以停止训练,避免继续学习那些过度拟合训练数据的细节。
- 早停法通过避免训练过程中的冗余步骤,减少了模型复杂度,从而有助于提高模型的泛化能力。
4. 交叉验证(Cross-Validation): 一种模型评估方法,它通过将数据集划分为多个子集,分别作为训练集和验证集来训练和验证模型,从而获得模型性能的更加可靠的评估。K折交叉验证是最常见的形式。
- 将数据集分为 K 个子集,每次使用 K−1 个子集作为训练集,剩余的子集作为验证集。
- 通过多次验证,避免模型对某一特定验证集的过拟合,得到模型在不同数据上的泛化能力的估计。
5. 剪枝(Pruning):针对决策树等模型的过拟合问题。决策树模型往往会随着深度增加而过拟合,剪枝技术通过去除决策树中不必要的分支来减少模型复杂度。
- 前剪枝:在树构建过程中,如果某一分支的增益小于某个阈值,便停止分裂。
- 后剪枝:先构建完全的树,然后通过删除一些低效的节点来简化树。
6. Dropout:是一种在神经网络中常用的正则化技术。它通过在训练过程中随机“丢弃”神经网络中的一部分神经元(即将它们的输出设为零),迫使网络在每一次迭代时都只能依赖于网络中的部分神经元,而不是整个网络。
- 在每一层的训练过程中,随机选择一部分神经元并将其“丢弃”。
- 通过这种方式,Dropout 能够减少神经元之间的相互依赖,强迫网络学习到更加鲁棒的特征,防止模型过度拟合训练数据。
7. 模型集成(Ensemble Methods):集成学习 是通过将多个模型的预测结果结合起来,以提高整体性能的方法。集成方法常见的有 Bagging、Boosting 和 Stacking 等。
- Bagging(例如随机森林):通过训练多个不同的数据子集上的独立模型,并将它们的预测结果进行平均或投票来减少过拟合。
- Boosting(例如梯度提升树):通过训练多个弱学习器,每个学习器修正前一个学习器的误差,从而增强模型的泛化能力。
- Stacking:将多个不同模型的输出结合,形成一个更强的集成模型。
8. 减少模型复杂度:减少模型的复杂度是防止过拟合的一个直观方法。对于神经网络来说,可以通过减少隐藏层的数量、每层神经元的数量来简化模型结构。
- 减少神经元数量:通过减少每一层的神经元数量,降低模型的容量,从而减少其对训练数据的过度拟合。
- 减少层数:降低网络的深度也可以帮助减少模型过拟合的风险。
9. 批标准化(Batch Normalization):是一种在训练过程中对每一层的输入进行标准化处理的方法,通过对每一批数据的输入进行归一化,使得数据在传递过程中保持稳定。BN 的作用之一是加速训练,并帮助模型避免过拟合。
- BN 通过缩放和平移的操作,确保每一层的输入保持在一个适当的范围内,避免了梯度消失或爆炸问题。
- BN 还能够引入一定的噪声,具有正则化效果,从而防止过拟合。
MLP的功能
MLP 广泛应用于以下任务:
- 分类任务:如手写数字识别、图像分类、文本分类等。
- 回归任务:如房价预测、股票价格预测等。
- 函数拟合:通过训练可以近似任意复杂的非线性函数。
- 特征学习:通过多层的隐藏层,MLP 可以学习到输入数据的高级抽象特征。
由于 MLP 是一种前馈神经网络,因此它只能通过已知的输入数据进行推理,且每次推理的计算时间较为固定,因此适用于多种任务和不同规模的数据。
适合图像分类的神经网络
BNN(Binary Neural Network)
网络结构
BNN(Binary Neural Network)是一种网络架构,旨在使用二进制权重和激活值来减少计算复杂度和内存占用。BNN的核心思想是将传统网络中的权重和激活值限制为+1和-1(或0和1),从而加速计算并减少存储需求。
卷积-池化结构
BNN的卷积层与标准卷积神经网络类似,区别在于权重和激活函数使用的是二值化的操作。传统的卷积操作在BNN中被替换为二进制乘法,从而大大提高计算速度。
原理公式
在传统神经网络中,卷积操作如下: 其中,xi是输入,wi是权重,b是偏置,y是输出。 在BNN中,权重和输入都经过二值化处理:
然后计算:
这种二进制乘法极大地加速了计算。
但BNN仍受限于复杂数据集/任务上很不理想的准确率和对特定硬件架构或软件框架的依赖,而模型压缩领域真正能应用于实处且算得上通用的技术仍然限于通道剪枝。
训练过程
BNN的训练过程与标准神经网络的训练过程类似,但有几个关键点需要注意:
- 二值化操作:输入数据和权重需要在训练过程中进行二值化。常见的做法是使用符号函数(sign function)将它们转换为+1和-1。
- 损失函数:通常使用交叉熵损失(Cross-Entropy Loss)来训练BNN,但由于二值化的引入,通常还需要采取一些近似方法来计算梯度。
训练步骤:
- 初始化权重:随机初始化权重,使用二值化技术将权重和输入限制为+1或-1。
- 正向传播:通过二值化输入和权重进行卷积计算。
- 损失计算:使用交叉熵损失函数计算预测值与标签之间的差异。
- 反向传播:通过反向传播计算梯度并更新权重。使用二值梯度估计来近似更新权重。
- 重复迭代:重复步骤2-4,直到模型收敛。
创新点
BNN的创新点在于使用二进制权重和激活值,从而在计算和存储上获得了显著的优势。这使得BNN特别适合于资源受限的设备,如嵌入式系统和移动端设备。
验证代码
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# BNN 网络模型
class BNN(nn.Module):
def __init__(self):
super(BNN, self).__init__()
self.layer1 = BNNLayer(3, 64, 3)
self.layer2 = BNNLayer(64, 128, 3)
self.fc = nn.Linear(128*32*32, 10)
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
# 数据加载器
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
trainset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=64, shuffle=True)
# 模型训练
model = BNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
for epoch in range(2): # 多次训练
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99: # 每100步输出一次损失值
print(f"Epoch {epoch+1}, Step {i+1}, Loss: {running_loss / 100}")
running_loss = 0.0
print("Finished Training")
AlexNet
网络结构
AlexNet是由Alex Krizhevsky等人在2012年提出的深度卷积神经网络(CNN),并在ImageNet挑战赛中取得了巨大成功。其网络结构包含8层(5个卷积层和3个全连接层)。AlexNet使用了ReLU激活函数、Dropout、局部响应归一化(LRN)和最大池化等技术。
卷积-池化结构
- 卷积层:使用了大卷积核(11×11、5×5),这是AlexNet的一大特点,能够有效提取图像的低级特征。
- 池化层:使用了最大池化(Max Pooling)(3×3)来减少特征图的尺寸。
- 创新点:AlexNet的创新之一是使用ReLU激活函数,这相比传统的Sigmoid激活函数具有更快的收敛速度和更好的梯度传播。
原理公式
假设输入数据为X,卷积核为W,偏置为b,激活函数为ReLU,则卷积操作的计算公式为:
这里的*代表卷积操作。
训练过程
AlexNet的训练过程主要包括:
- 数据增强:AlexNet使用了数据增强技术,如图像翻转、裁剪和颜色调整,以增加训练集的多样性。
- Dropout:Dropout被应用于全连接层,以防止过拟合。
- 学习率衰减:AlexNet采用了基于批次的随机梯度下降(SGD)优化器,并使用了学习率衰减策略。
训练步骤:
- 数据预处理:输入图像进行大小调整、标准化以及数据增强处理。
- 初始化网络:使用预训练的网络权重或随机初始化。
- 正向传播:通过卷积层、池化层和全连接层进行计算,得到预测结果。
- 损失计算:使用交叉熵损失函数进行损失计算。
- 反向传播:计算每一层的梯度,并通过SGD优化器更新参数。
- 迭代训练:重复步骤3-5,调整学习率并进行梯度更新。
创新点
- ReLU激活函数:加速训练,避免梯度消失。
- Dropout:防止过拟合,随机丢弃部分神经元。
- LRN:提高了模型的泛化能力。
- 数据增强:使用数据增强技术来增加训练数据的多样性。
验证代码
import torchvision.models as models
alexnet = models.alexnet(pretrained=True)
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2)
self.conv2 = nn.Conv2d(64, 192, kernel_size=5, padding=2)
self.conv3 = nn.Conv2d(192, 384, kernel_size=3, padding=1)
self.conv4 = nn.Conv2d(384, 256, kernel_size=3, padding=1)
self.conv5 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
self.fc1 = nn.Linear(256 * 6 * 6, 4096)
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, 10)
self.relu = nn.ReLU()
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2)
self.dropout = nn.Dropout(0.5)
def forward(self, x):
x = self.relu(self.conv1(x))
x = self.maxpool(x)
x = self.relu(self.conv2(x))
x = self.maxpool(x)
x = self.relu(self.conv3(x))
x = self.relu(self.conv4(x))
x = self.relu(self.conv5(x))
x = x.view(x.size(0), -1) # Flatten
x = self.dropout(self.relu(self.fc1(x)))
x = self.dropout(self.relu(self.fc2(x)))
x = self.fc3(x)
return x
# 数据加载器
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
trainset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=64, shuffle=True)
# 模型训练
model = AlexNet()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
for epoch in range(2): # 多次训练
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99: # 每100步输出一次损失值
print(f"Epoch {epoch+1}, Step {i+1}, Loss: {running_loss / 100}")
running_loss = 0.0
print("Finished Training")
VGG16
网络结构
VGG16由Simonyan和Zisserman提出,是一个非常经典的卷积神经网络。它的网络结构非常简单,特点是使用了许多相同大小的3x3卷积核,搭配2x2的最大池化层。
- 16层:13个卷积层和3个全连接层。
- 所有卷积层的过滤器大小为3x3,并且卷积层之间没有跳跃连接,网络非常深。
- 使用最大池化层来降低空间维度。
卷积-池化结构
- 卷积层:使用了多个连续的3x3卷积层,以增加感受野,同时保持较小的卷积核,避免了使用大的卷积核带来的计算和参数增多的问题。
- 池化层:使用2x2的最大池化,减少特征图的尺寸。
原理公式
卷积操作为:
其中,X 是输入,W 是卷积核,b 是偏置,ReLU为激活函数。池化操作使用最大池化:
最大池化通常是对3x3区域中的最大值进行池化。
训练过程
VGG16的训练过程与AlexNet类似,但有几点不同:
- 较深的网络:VGG16包含了更多的卷积层和全连接层,因此它的训练会比AlexNet更加复杂。
- 较小的卷积核:VGG16使用多个3x3的卷积核来代替较大的卷积核,从而降低了计算量。
训练步骤:
- 数据预处理:图像尺寸调整为224x224,进行归一化和数据增强。
- 初始化网络:使用VGG16模型的预训练权重,或者从头开始随机初始化。
- 正向传播:输入通过多个卷积层和池化层,最后通过全连接层输出。
- 损失计算:计算输出与标签之间的交叉熵损失。
- 反向传播:通过反向传播计算每层的梯度,使用SGD优化器进行权重更新。
- 学习率调整:使用学习率衰减来提高训练效果。
- 训练迭代:多轮迭代,直到模型收敛。
创新点
VGG16的创新点在于使用大量的相同大小的小卷积核(3x3)来代替更大的卷积核(例如5x5、7x7),从而减小了计算量,并且增加了网络的深度。
验证代码
import torchvision.models as models
vgg16 = models.vgg16(pretrained=True)
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
class VGG16(nn.Module):
def __init__(self):
super(VGG16, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 1000)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1) # Flatten
x = self.classifier(x)
return x
# 数据加载器和训练代码同AlexNet模型相同。
GoogLeNet(Inception v1)
网络结构

GoogLeNet由Szegedy等人提出,采用了非常独特的Inception模块,它通过将不同大小的卷积核和池化层并行使用来提取特征。
- Inception模块:通过不同的卷积核大小(如1x1、3x3、5x5)和池化层(如3x3最大池化)进行并行计算,然后将结果拼接在一起。
- 卷积层:GoogLeNet使用了1x1卷积核来减少通道数,从而降低计算量。
- 深度监督:在网络中间加入了额外的输出层,帮助训练时更好地优化。
卷积-池化结构
- Inception模块:不同大小的卷积核并行进行特征提取。
- 池化层:采用最大池化来降低维度。
原理公式
在Inception模块中,我们会对输入进行多个卷积操作和池化操作:
然后将这些输出拼接:
训练过程
GoogLeNet的训练过程较为复杂,因为它包含多个并行的Inception模块:
- Inception模块:在每个模块中使用多个不同尺寸的卷积核进行并行计算,然后将其输出拼接。
- 辅助分类器:GoogLeNet使用了中间层的辅助分类器来帮助梯度传播,提升训练速度。
训练步骤:
- 数据预处理:图像尺寸调整为224x224,进行归一化和数据增强。
- 初始化网络:使用预训练权重或随机初始化。
- 正向传播:通过多个并行的Inception模块和辅助分类器计算输出。
- 损失计算:计算输出与标签之间的交叉熵损失。
- 反向传播:通过反向传播计算梯度,并通过SGD优化器更新网络参数。
- 辅助分类器:在中间层加入辅助分类器,促进网络训练。
- 训练迭代:进行多轮训练,直到模型收敛。
创新点
- Inception模块:通过不同尺寸的卷积核并行计算来提取多尺度特征。
- 1x1卷积:通过1x1卷积核降低网络的计算复杂度和参数量。
验证代码
import torchvision.models as models
googlenet = models.googlenet(pretrained=True)
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
class InceptionModule(nn.Module):
def __init__(self, in_channels, out1x1, out3x3, out5x5, out_pool):
super(InceptionModule, self).__init__()
self.conv1x1 = nn.Conv2d(in_channels, out1x1, kernel_size=1)
self.conv3x3 = nn.Conv2d(in_channels, out3x3, kernel_size=3, padding=1)
self.conv5x5 = nn.Conv2d(in_channels, out5x5, kernel_size=5, padding=2)
self.pool = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.pool_conv = nn.Conv2d(in_channels, out_pool, kernel_size=1)
def forward(self, x):
conv1x1_out = self.conv1x1(x)
conv3x3_out = self.conv3x3(x)
conv5x5_out = self.conv5x5(x)
pool_out = self.pool(x)
pool_out = self.pool_conv(pool_out)
return torch.cat([conv1x1_out, conv3x3_out, conv5x5_out, pool_out], 1)
class GoogLeNet(nn.Module):
def __init__(self):
super(GoogLeNet, self).__init__()
self.inception1 = InceptionModule(3, 64, 128, 32, 32)
self.fc = nn.Linear(512, 10) # 简化版输出10个类别
def forward(self, x):
x = self.inception1(x)
x = x.view(x.size(0), -1) # Flatten
x = self.fc(x)
return x
# 数据加载器和训练代码同VGG16模型相同。
ResNet (Residual Networks)
网络结构
ResNet由He等人提出,它的核心创新是残差连接(Residual Connections),这使得网络可以训练更深的层次而不会出现梯度消失问题。ResNet的网络通过使用跳跃连接(skip connections)将输入信号直接传递到后续层,这大大提高了网络的训练效率。
- 残差块:每个残差块由两个或更多卷积层和一个跳跃连接(短路连接)组成,跳跃连接直接将输入添加到输出上。
- 深度:ResNet通过使用这些残差块来堆叠网络层,能够构建非常深的网络,通常有18、34、50、101、152层。
习题3.2 请计算AlexNet、VGG19、ResNet152三个网络中的神经元数目,以及可训练的参数数目。
- 输入层
- 神经元数量:输入图像的像素数量(H*W*C)
- 参数数量:0
- 卷积层
- 神经元数量:输出特征图的像素数量(Hout*Wout*Cout)
- 参数数量:(K_height×K_width×Input Channels+1)×Output Channels
其中,
K_height
和K_width
是卷积核的高和宽,Input Channels
是输入特征图的深度,Output Channels
是输出特征图的深度,+1
是为了计算偏置项。
- 神经元数量:输出特征图的像素数量(Hout*Wout*Cout)
- 全连接层
- 神经元数量:等于输出节点的数量
- 参数数量:P_fc=(Input Features+1)×Output Features
其中,
Input Features
是输入到该层的节点数,Output Features
是该层的输出节点数,+1
是为了计算偏置项。
卷积-池化结构
- 卷积层:ResNet中的卷积层通常使用3x3卷积。
- 池化层:使用2x2最大池化。
原理公式
残差块的输出计算公式为:
其中, 是残差块中的卷积操作,x 是跳跃连接传递的输入信号。公式中,
计算的是残差,而不是直接输出。
训练过程
ResNet的训练过程核心在于使用残差连接:
- 残差连接:每个残差块都有一个跳跃连接,将输入直接加到输出上,有助于解决深层网络中的梯度消失问题。
- Batch Normalization:每个卷积层后都使用Batch Normalization来加速训练和稳定网络。
训练步骤:
- 数据预处理:调整输入图像大小并进行归一化。
- 初始化网络:使用预训练权重或随机初始化。
- 正向传播:每层通过残差块计算输出。
- 损失计算:使用交叉熵损失函数计算预测值与标签之间的差异。
- 反向传播:计算梯度,并通过优化器更新网络权重。
- 训练迭代:多轮训练,直到模型收敛。
创新点
- 残差连接:通过加上跳跃连接,网络可以更深,而不会出现梯度消失或梯度爆炸的问题。
- 深度网络:ResNet可以训练极深的网络,如ResNet152。
验证代码
import torchvision.models as models
resnet = models.resnet50(pretrained=True)
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x)
out = self.relu(out)
return out
class ResNet50(nn.Module):
def __init__(self):
super(ResNet50, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(64, 128, 2)
self.layer2 = self._make_layer(128, 256, 2, stride=2)
self.layer3 = self._make_layer(256, 512, 2, stride=2)
self.layer4 = self._make_layer(512, 1024, 2, stride=2)
self.fc = nn.Linear(1024, 10)
def _make_layer(self, in_channels, out_channels, blocks, stride=1):
layers = []
layers.append(ResidualBlock(in_channels, out_channels, stride))
for _ in range(1, blocks):
layers.append(ResidualBlock(out_channels, out_channels))
return nn.Sequential(*layers)
def forward(self, x):
x = self.relu(self.bn1(self.conv1(x)))
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = x.view(x.size(0), -1) # Flatten
x = self.fc(x)
return x
# 数据加载器和训练代码同VGG16模型相同。
SENet (Squeeze-and-Excitation Networks)
网络结构
SENet的核心思想是自适应地调整通道的权重,通过“压缩”模块来捕捉通道之间的依赖关系,从而优化特征的表示。SE模块由两部分组成:Squeeze 和 Excitation。
- Squeeze:通过全局平均池化操作对特征进行压缩,得到每个通道的全局信息。
- Excitation:使用一个全连接网络来学习通道的权重,并对通道进行重新加权。
卷积-池化结构
- 卷积层:标准的卷积层,SE模块嵌入在卷积网络中。
- 池化层:通常使用全局平均池化。
原理公式
Squeeze阶段:对每个通道进行全局平均池化:
Excitation阶段:通过一个全连接层学习通道权重:
然后使用学习到的权重对通道进行加权:y=s⋅x
训练过程
SENet通过Squeeze-and-Excitation模块自适应调整通道的权重:
- Squeeze-and-Excitation模块:通过全局平均池化计算通道间的关系,并通过全连接网络生成每个通道的权重。
- 损失函数:使用交叉熵损失函数进行训练。
训练步骤:
- 数据预处理:输入图像进行尺寸调整、归一化等预处理。
- 初始化网络:使用预训练权重或随机初始化。
- 正向传播:每个卷积块后都加入SE模块来调整通道的权重。
- 损失计算:计算输出与标签之间的损失。
- 反向传播:计算梯度并更新网络参数。
- 训练迭代:使用SGD优化器进行训练。
创新点
- Squeeze-and-Excitation模块:增强了网络对通道信息的关注,提升了模型的表现。
验证代码
import torchvision.models as models
senet = models.senet154(pretrained=True)
import torch
import torch.nn as nn
class SEBlock(nn.Module):
def __init__(self, in_channels, reduction=16):
super(SEBlock, self).__init__()
self.fc1 = nn.Linear(in_channels, in_channels // reduction)
self.fc2 = nn.Linear(in_channels // reduction, in_channels)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
batch_size, channels, _, _ = x.size()
z = x.view(batch_size, channels, -1).mean(dim=2)
z = self.fc1(z)
z = self.fc2(z)
z = self.sigmoid(z).view(batch_size, channels, 1, 1)
return x * z
class SENet(nn.Module):
def __init__(self):
super(SENet, self).__init__()
self.conv = nn.Conv2d(3, 64, kernel_size=3, padding=1)
self.se_block = SEBlock(64)
self.fc = nn.Linear(64, 10)
def forward(self, x):
x = self.conv(x)
x = self.se_block(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
# 数据加载器和训练代码同VGG16模型相同。
适合图像检测的神经网络
习题3.4
简述错误率与loU、mAP关系
错误率(Error Rate)、IoU(Intersection over Union,倾斜度)和mAP(mean Average Precision,均衡平均精度)是评估模型性能时常用的三个指标,特别是在目标检测和分割任务中,它们之间有一定的关系,但也各自衡量不同的方面。
-
错误率(Error Rate):指的是模型预测错误的比例,通常用公式表示为:
在目标检测或分类任务中,错误率较低通常表示模型的准确性较高。
-
IoU(Intersection over Union):是衡量两个区域重叠程度的指标,常用于目标检测任务中,表示预测框与真实框的重叠部分与它们并集的比值。其公式为:
IoU的值范围为[0, 1],值越大表示预测越准确。通常,IoU大于某个阈值(例如0.5)时,认为该预测是正确的。
-
mAP(mean Average Precision):是目标检测任务中的一个重要评估指标,反映了模型在各个类别上的平均精度。mAP是通过计算不同召回率下的精度(Precision),然后求其平均值。通常mAP与IoU结合使用,IoU阈值一般设定为0.5、0.75等不同的值。
错误率与IoU、mAP的关系:
-
IoU与错误率:高IoU值通常意味着模型能够准确地定位目标,预测框和真实框重叠较大,因此错误率较低。反之,低IoU值意味着预测框与真实框之间的重叠较小,错误率较高。
-
mAP与错误率:mAP考虑的是不同IoU阈值下的精度和召回率,较高的mAP通常意味着较低的错误率,因为模型在不同置信度阈值下能较好地检测出目标并进行准确分类。错误率高通常会导致mAP降低。
总结来说,错误率较低时,通常意味着IoU较高,进而mAP较高,因为模型能够更准确地检测和分类目标。mAP通过计算不同阈值下的精度-召回率曲线面积的平均值,综合考虑了不同IoU阈值下的性能。
U-Net
网络结构
U-Net最初是为医学图像分割任务设计的,它是一种对称的编码器-解码器结构。U-Net的创新点在于它引入了跳跃连接,将编码器中的特征直接传递到解码器,帮助解码器恢复空间分辨率。
- 编码器:由一系列卷积和池化层组成,用于提取特征。
- 解码器:通过反卷积层逐步恢复空间分辨率。
- 跳跃连接:直接将编码器的特征图连接到解码器中,对分割任务特别有效。
卷积-池化结构
- 卷积层:通常使用3x3卷积。
- 池化层:使用最大池化层进行下采样。
原理公式
假设编码器的输出为 x,解码器通过反卷积将其恢复为原始尺寸:y=UpConv(x)(反卷积)
跳跃连接直接将编码器输出的特征图与解码器的输入相加:
训练过程
- 跳跃连接:跳跃连接是U-Net的关键,帮助解码器恢复图像的细节。
- 损失函数:通常使用交叉熵或Dice系数作为损失函数来评估图像分割的效果。
训练步骤:
- 数据预处理:输入图像进行归一化和尺寸调整。
- 初始化网络:使用随机初始化权重。
- 正向传播:通过编码器提取特征,并通过解码器恢复图像。
- 损失计算:使用交叉熵或Dice系数计算预测结果与真实标签之间的损失。
- 反向传播:计算梯度并更新权重。
- 训练迭代:训练过程中可以使用数据增强来提高模型泛化能力。
创新点
- 跳跃连接:使得U-Net可以在分割过程中更好地恢复图像的细节。
验证代码
import torchvision.models.segmentation as models
unet = models.segmentation.deeplabv3_resnet101(pretrained=True)
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
class UNet(nn.Module):
def __init__(self):
super(UNet, self).__init__()
self.encoder = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(2, stride=2)
)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(64, 64, kernel_size=2, stride=2),
nn.ReLU(inplace=True),
nn.Conv2d(64, 3, kernel_size=1)
)
def forward(self, x):
x1 = self.encoder(x)
x2 = self.decoder(x1)
return x2
# 数据加载器和训练代码同VGG16模型相同。
R-CNN (Regions with CNN Features)
R-CNN是目标检测的开创性方法之一,它结合了传统的选择性搜索(Selective Search)方法与卷积神经网络(CNN)来进行目标检测。R-CNN的主要步骤包括生成候选区域、特征提取、分类和回归。其关键创新在于将CNN应用到候选区域的特征提取中。然而,它的缺点是计算量非常大,因为每个候选区域都要单独传入CNN进行处理。
算法流程
-
候选区域生成:
- 使用选择性搜索方法(Selective Search)生成候选区域(Region Proposals),这些区域可能包含目标物体。
- 选择性搜索通过多种方式(颜色、纹理、大小等)将图像分割成不同的区域,然后合并相似的区域来生成候选框。
-
特征提取:
- 对于每个候选区域,使用预训练的CNN(如AlexNet、VGG等)提取图像特征。
- 每个候选区域会被缩放成统一大小,以便于输入到CNN中进行特征提取。
-
SVM分类:
- 使用支持向量机(SVM)对提取的CNN特征进行分类,判断候选区域是否属于某个物体类别。
- 对于每个候选框,R-CNN输出一个类别标签和相应的置信度。
-
边界框回归:
- 使用线性回归对候选框的位置进行微调,以获得更精确的边界框位置。
公式
- 候选区域生成:选择性搜索通过合并相似区域来生成候选框,选择性搜索的得分函数可通过:
其中,α,β,γ 是权重,用来衡量候选区域的相似性。
- SVM分类:对每个候选区域的特征,SVM分类器输出类别 y 和置信度 p(y∣x):
- 边界框回归:对于每个候选框
, 回归模型预测新的边界框坐标:
其中, 为回归模型的预测值。
验证代码
import torch
import torch.nn as nn
from torchvision import models, transforms
from PIL import Image
import numpy as np
# 加载预训练的ResNet模型(作为R-CNN的特征提取器)
resnet = models.resnet50(pretrained=True)
resnet = nn.Sequential(*list(resnet.children())[:-1]) # 去掉最后的全连接层
# 假设有一个候选区域(这里简化为一个固定大小的裁剪区域)
def get_candidate_region(image_path, size=(224, 224)):
image = Image.open(image_path)
image = image.resize(size)
return np.array(image)
# 图像预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
# 模拟候选区域并提取特征
image_path = "sample_image.jpg"
candidate_region = get_candidate_region(image_path)
candidate_region = transform(candidate_region).unsqueeze(0) # 增加batch维度
# 特征提取
with torch.no_grad():
features = resnet(candidate_region)
# 输出提取的特征
print(features.shape)
# 注意:R-CNN还需要使用SVM分类器和边界框回归器进行后处理,这里做简化。
YOLO (You Only Look Once)
YOLO是另一种重要的目标检测方法,区别于R-CNN的是,YOLO将目标检测作为一个回归问题进行处理。YOLO通过一个单一的神经网络,直接从整个图像中预测目标的类别和边界框。由于不需要生成候选区域,YOLO比R-CNN速度更快。
算法流程
-
图像分块:
- 将输入图像分成S×S个网格,每个网格负责预测该区域内的目标。
-
边界框回归:
- 每个网格预测固定数量的边界框,每个框包含坐标 (x,y,w,h) 和置信度(confidence)分数,该置信度表示框中包含目标的概率。
- YOLO网络会预测每个框的类别概率分布。
-
类别预测:
- 每个网格还会预测每个类的概率,这些概率结合置信度来确定最终的分类结果。
-
非极大值抑制:
- YOLO使用非极大值抑制(NMS)来去除重叠过多的框,只保留具有最高置信度的框。
公式
- 边界框回归:对于每个边界框,YOLO网络预测 x,y 是框中心相对于网格的坐标,w,h 是框的宽高,且通过sigmoid函数输出置信度:
- 类别预测:每个网格预测 C 类的概率:
, 其中,yi 是类别i的得分。
- 最终损失函数: YOLO的损失函数包括三部分:
验证代码
import torch
from torchvision import models, transforms
from PIL import Image, ImageDraw
# 加载预训练的YOLOv5模型
yolo = models.detection.yolov5(pretrained=True)
yolo.eval()
# 图像预处理
transform = transforms.Compose([
transforms.Resize((640, 640)), # YOLO输入尺寸
transforms.ToTensor(),
])
# 加载图像
image_path = "sample_image.jpg"
image = Image.open(image_path)
image_tensor = transform(image).unsqueeze(0) # 增加batch维度
# 模型推理
with torch.no_grad():
predictions = yolo(image_tensor)
# 获取预测结果
boxes = predictions[0]['boxes'] # 边界框坐标
labels = predictions[0]['labels'] # 类别标签
scores = predictions[0]['scores'] # 置信度
# 画出检测结果
draw = ImageDraw.Draw(image)
for box, score in zip(boxes, scores):
if score > 0.5: # 设定置信度阈值
draw.rectangle(box.tolist(), outline="red", width=3)
image.show() # 显示检测后的图像
SSD (Single Shot Multibox Detector)
SSD是另一个单阶段目标检测算法,它与YOLO类似,也通过一个单一的神经网络进行目标检测。与YOLO不同的是,SSD在多个尺度上进行检测,允许它在不同的图像区域中进行不同大小的目标检测。,SSD相比YOLO具有更强的检测能力,尤其在小目标检测方面更为优秀。
算法流程
- 特征图生成:
- SSD使用一个基础网络(如VGG16)作为特征提取器,并从中间层提取多个特征图。
- 多尺度检测:
- SSD在不同的特征图上进行目标检测,每个特征图对应不同的感受野和目标大小。
- 边界框回归和类别预测:
- SSD对每个特征图位置预测固定数量的边界框,且每个框预测类别和位置。
- 非极大值抑制:
- 类似于YOLO,SSD也使用非极大值抑制来去除重复的框,只保留最优框。
公式
- 边界框回归:SSD的边界框回归与YOLO相似,预测每个框的坐标和置信度:
-
多尺度预测:SSD会在不同的特征图尺度上进行预测,每个尺度都有不同的边界框和类别预测。
-
损失函数: SSD的损失函数包括位置损失和分类损失:
其中, 是位置回归损失,
是分类损失。
验证代码
import torch
from torchvision import models, transforms
from PIL import Image, ImageDraw
# 加载预训练的SSD模型
ssd = models.detection.ssdlite320_mobilenet_v3_large(pretrained=True)
ssd.eval()
# 图像预处理
transform = transforms.Compose([
transforms.ToTensor(),
])
# 加载图像
image_path = "sample_image.jpg"
image = Image.open(image_path)
image_tensor = transform(image).unsqueeze(0) # 增加batch维度
# 模型推理
with torch.no_grad():
predictions = ssd(image_tensor)
# 获取预测结果
boxes = predictions[0]['boxes'] # 边界框坐标
labels = predictions[0]['labels'] # 类别标签
scores = predictions[0]['scores'] # 置信度
# 画出检测结果
draw = ImageDraw.Draw(image)
for box, score in zip(boxes, scores):
if score > 0.5: # 设定置信度阈值
draw.rectangle(box.tolist(), outline="red", width=3)
image.show() # 显示检测后的图像
适合图像生成的神经网络
GAN (Generative Adversarial Networks)
网络结构
GAN由生成器(Generator)和判别器(Discriminator)组成,两者通过对抗训练互相提升性能。生成器的任务是生成逼真的数据,而判别器的任务是判断输入数据是否为真实数据。
- 生成器:通常使用反卷积(转置卷积)生成新的样本。
- 判别器:使用标准卷积层来判断输入数据是真实的还是生成的。
原理公式
生成器和判别器之间的对抗性损失公式为:
其中,D是判别器,G是生成器,x是真实样本,z是随机噪声。
训练过程
GAN的训练由生成器和判别器组成,两者通过对抗训练来优化:
- 生成器:生成逼真的数据,尽量使判别器无法区分真假数据。
- 判别器:判断输入数据是来自真实数据还是生成器生成的数据。
训练步骤:
- 初始化网络:分别初始化生成器和判别器的网络。
- 生成器训练:生成器生成假数据并计算损失,优化器更新生成器参数。
- 判别器训练:判别器通过真实数据和生成的数据进行训练,优化器更新判别器参数。
- 交替训练:交替进行生成器和判别器的训练,直到达到对抗平衡。
创新点
- 对抗训练:生成器和判别器的对抗训练使得生成模型能够生成更加逼真的数据。
验证代码
import torch
import torch.nn as nn
import torch.optim as optim
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.fc = nn.Linear(100, 256)
self.deconv = nn.ConvTranspose2d(256, 1, kernel_size=4, stride=2, padding=1)
def forward(self, x):
x = self.fc(x)
x = x.view(x.size(0), 256, 1, 1)
x = self.deconv(x)
return x
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.conv = nn.Conv2d(1, 256, kernel_size=4, stride=2, padding=1)
self.fc = nn.Linear(256 * 7 * 7, 1)
def forward(self, x):
x = self.conv(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
# 使用Adam优化器训练生成器和判别器
PixelRNN与PixelCNN
目标:生成高质量的图像,逐像素建模图像的联合概率分布 p(x)p(x),其中 xx 是图像的所有像素。
核心思想:
- 将图像生成视为序列生成问题,按顺序预测每个像素(通常从左到右、从上到下)。
- 使用自回归模型(Autoregressive Model),即当前像素的概率依赖于之前生成的像素。
PixelRNN
- 递归神经网络(RNN):利用 LSTM 或 GRU 按行或对角线顺序建模像素依赖关系。
- 两种扫描顺序:
- Row LSTM:逐行处理,每个像素依赖当前行上方和左侧的上下文。
- Diagonal BiLSTM:按对角线顺序处理,覆盖更广的上下文区域。
- 每个点都需要重新训练一个新模型,一个点一个点产生,训练速度慢。
- 优点:能捕获长距离像素依赖(得益于RNN的记忆能力)。
PixelCNN
- 卷积神经网络(CNN):使用掩码卷积(Masked Convolution)限制上下文范围,确保仅依赖已生成的像素。
- 掩码类型:
- Mask Type A:中心像素被掩码(预测时不可见)。
- Mask Type B:中心像素可见(用于后续层的输入)。
- 优点:训练速度快(可部分并行化)。
- 缺点:感受野受限(需多层堆叠才能捕获全局依赖)。
import torch
import torch.nn as nn
import torch.nn.functional as F
class MaskedConv2d(nn.Conv2d):
"""掩码卷积层(Type B)"""
def __init__(self, in_channels, out_channels, kernel_size, stride=1):
super().__init__(in_channels, out_channels, kernel_size, stride,
padding=kernel_size//2)
# 创建掩码:中心及右侧像素为1,其余为0
mask = torch.ones_like(self.weight)
_, _, h, w = mask.shape
mask[:, :, h//2, w//2+1:] = 0 # 右侧掩码
mask[:, :, h//2+1:] = 0 # 下方掩码
self.register_buffer('mask', mask)
def forward(self, x):
self.weight.data *= self.mask # 应用掩码
return super().forward(x)
class PixelCNN(nn.Module):
def __init__(self, input_channels=3, hidden_dim=64, num_layers=7):
super().__init__()
layers = [MaskedConv2d(input_channels, hidden_dim, 7)]
for _ in range(num_layers - 1):
layers.extend([
nn.ReLU(),
MaskedConv2d(hidden_dim, hidden_dim, 3)
])
self.net = nn.Sequential(*layers)
self.out = nn.Conv2d(hidden_dim, 256 * input_channels, 1) # 输出256色分布
def forward(self, x):
x = self.net(x)
return self.out(x).view(x.shape[0], 256, 3, 32, 32) # 假设输入为32x32图像
# 使用示例
model = PixelCNN()
x = torch.randn(2, 3, 32, 32) # 模拟输入图像
logits = model(x) # 输出每个像素的256色概率分布
自编码器AE与变分自编码器VAE
参考:【深度视觉】第十三章:生成网络1——PixelRNN/CNN、VAE-CSDN博客
AE(详见机器学习专栏6.2篇)根本就称不上是一个生成模型!因为必须要输入A或者输入B,才能生成狗1或者狗2。即使我们训练100万张狗,也只是确定的这100万个数字,只能生成对应的100万只狗!也就是latent space永远是一个有限的、离散的空间。假如我们给解码器输入C,而C不在latent space上,此时我们会发现解码器给我们返回的图片不仅模糊而且还是乱码的。
期望的生成模型应该是输入C点能给我生成一张既像狗1又像狗2的狗图,或输入一个任意的随机数,解码器能生成一只狗,至于这只狗长啥样,随意都可,反正都是狗就可以了。这才是真正的生成模型!于是,人们转向研究如何让latent space变成一个连续的、无限的空间。
- 降噪自编码器:一个变种,隐藏空间变为连续的了。
变分自编码器(Variational Autoencoder, VAE)
核心思想
- 目标:学习数据的概率分布,生成新的样本。
- 关键改进:
- 潜在变量
z
是随机变量(服从高斯分布)。 - 编码器输出分布的参数(均值
μ
和方差σ²
),而非固定值。 - 训练目标:最大化证据下界(ELBO),平衡重构损失和分布正则化。
- 潜在变量
数学表达
VAE学的就是一个高维高斯分布,它就是一个混合高斯模型(Gaussian Mixture Model,GMM) ,就是对高维的数据进行概率表示的一个模型,就是多个高斯分布函数的线性组合。
VAE编码器的输出有2个,一个是输出均值,一个是输出log var。
class VAE(nn.Module):
def __init__(self, input_dim=784, latent_dim=32):
super().__init__()
# 编码器
self.fc1 = nn.Linear(input_dim, 256)
self.fc_mu = nn.Linear(256, latent_dim) # 均值
self.fc_logvar = nn.Linear(256, latent_dim) # 对数方差
# 解码器
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 256),
nn.ReLU(),
nn.Linear(256, input_dim),
nn.Sigmoid()
)
def encode(self, x):
h = torch.relu(self.fc1(x))
return self.fc_mu(h), self.fc_logvar(h) # μ, log(σ²)
def reparameterize(self, mu, logvar):
std = torch.exp(0.5 * logvar) # σ
eps = torch.randn_like(std) # 噪声 ε ~ N(0,1)
return mu + eps * std # z = μ + εσ
def forward(self, x):
mu, logvar = self.encode(x)
z = self.reparameterize(mu, logvar)
x_recon = self.decoder(z)
return x_recon, mu, logvar
# 损失函数
def vae_loss(x_recon, x, mu, logvar):
recon_loss = nn.functional.binary_cross_entropy(x_recon, x, reduction='sum')
kl_div = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return recon_loss + kl_div
AE 与 VAE 的对比
特性 | 自编码器(AE) | 变分自编码器(VAE) |
---|---|---|
潜在变量 | 确定性向量 | 随机变量(高斯分布) |
训练目标 | 最小化重构损失 | 最大化ELBO(重构+KL散度) |
生成能力 | 无 | 可通过采样生成新样本 |
潜在空间 | 可能不连续 | 连续且平滑 |
数学基础 | 无概率模型 | 概率生成模型(变分推断) |
典型应用 | 降维、去噪 | 生成、插值、异常检测 |
扩散模型(Diffusion Models)
原理
扩散模型是近年来提出的一类生成模型,通过模拟反向扩散过程来生成数据。扩散模型通过逐步加入噪声并反向去噪来生成高质量的图像。
- 正向扩散:将数据逐渐加入噪声。
- 反向扩散:通过学习如何去噪来生成样本。
这些模型需要较长的训练时间,但通常能生成非常高质量的图像。
训练过程
扩散模型通过模拟正向和反向的扩散过程来训练:
- 正向扩散:逐步将噪声加入到数据中。
- 反向扩散:通过反向扩散过程生成数据。
训练步骤:
- 初始化数据:通过正向扩散逐步添加噪声,直到数据变成纯噪声。
- 网络训练:训练网络预测每一时刻的噪声,并进行反向扩散。
- 损失计算:使用L2损失计算预测噪声与真实噪声之间的差异。
- 反向传播:计算梯度并更新网络权重。
- 生成过程:训练完成后,利用反向扩散过程生成样本。
适合文本/语音处理的循环神经网络
RNN (Recurrent Neural Network)
思路
RNN是一种用于处理序列数据的神经网络结构,能够处理时间序列或语言模型等任务。与传统的前馈神经网络不同,RNN在计算过程中会将前一个时刻的输出(或隐藏状态)作为当前时刻的输入之一,从而具有“记忆”能力。这使得RNN在处理序列数据时能够捕捉时间依赖性。
RNN的基本思路是对输入序列的每个时间步逐个进行处理,同时保持一个隐藏状态,该状态在时间步之间进行更新。每个时间步的计算不仅依赖于当前输入,还依赖于上一个时刻的状态(记忆)。
结构
RNN的基本结构可以表示为一个循环结构,其中每个时刻的隐藏状态 都是由前一时刻的隐藏状态
和当前时刻的输入
计算得到:
其中:ht 是时刻 t 的隐藏状态,Wh 和 Wx 是权重矩阵,xt 是当前时刻的输入,b 是偏置项,f 是激活函数。
RNN的输出 yt 是隐藏状态的函数:
其中 Wy 和 by 分别是输出层的权重和偏置。
计算过程
- 输入数据:将输入数据逐步传入RNN,并在每个时刻计算隐藏状态。
- 状态更新:每个时刻的隐藏状态依赖于当前输入和上一时刻的隐藏状态。
- 输出生成:通过输出层计算每个时刻的输出。
优缺点
- 优点:RNN能够处理序列数据,并且能够记住输入数据的历史信息。
- 缺点:RNN存在梯度消失和梯度爆炸的问题,尤其是在处理长序列时,无法有效捕捉长期依赖关系。
验证代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# 超参数
input_size = 10 # 输入特征维度
hidden_size = 20 # 隐藏层的维度
output_size = 1 # 输出维度(回归问题)
seq_length = 5 # 序列长度
batch_size = 32 # 批次大小
num_epochs = 10 # 训练轮数
# 构建一个简单的RNN模型
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# x: (batch_size, seq_length, input_size)
h0 = torch.zeros(1, x.size(0), hidden_size).to(x.device) # 初始化隐藏状态
out, _ = self.rnn(x, h0) # 输出的最后时刻的隐藏状态
out = self.fc(out[:, -1, :]) # 只取序列的最后一个时间步的输出
return out
# 数据集准备
x_data = torch.randn(1000, seq_length, input_size)
y_data = torch.randn(1000, output_size)
dataset = TensorDataset(x_data, y_data)
train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# 模型、损失函数和优化器
model = SimpleRNN(input_size, hidden_size, output_size)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练过程
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for inputs, labels in train_loader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}')
LSTM (Long Short-Term Memory)
思路
LSTM是为了解决RNN中梯度消失和梯度爆炸问题而提出的一种特殊类型的RNN。LSTM通过引入门控机制,使得网络能够选择性地“记住”或“遗忘”信息,从而能够捕捉更长时间范围内的依赖关系。
LSTM的结构包括三个主要的门:
- 遗忘门(Forget Gate):决定哪些信息应该被丢弃。
- 输入门(Input Gate):决定哪些新信息需要被添加到记忆中。
- 输出门(Output Gate):决定最终输出哪些信息。
结构
LSTM的计算过程与RNN相似,但在每个时刻,LSTM会计算三个门的值,利用这些门来控制信息的流动。
LSTM的更新过程可以表示为:
- 遗忘门:决定丢弃上一时刻的哪些信息:
- 输入门:决定哪些新信息需要添加到当前的记忆中:
- 更新记忆单元:结合遗忘门和输入门来更新记忆:
- 输出门:决定当前时刻的输出:
计算过程
- 输入数据:输入序列数据。
- 状态更新:根据输入和前一时刻的状态,计算当前时刻的状态。
- 输出生成:通过输出门计算当前时刻的输出。
优缺点
- 优点:LSTM能够更好地捕捉长期依赖关系,避免了RNN的梯度消失问题。
- 缺点:LSTM相较于普通RNN计算量较大,训练时间较长。
验证代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# 超参数
input_size = 10 # 输入特征维度
hidden_size = 20 # 隐藏层的维度
output_size = 1 # 输出维度(回归问题)
seq_length = 5 # 序列长度
batch_size = 32 # 批次大小
num_epochs = 10 # 训练轮数
# 构建一个简单的LSTM模型
class SimpleLSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleLSTM, self).__init__()
self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# x: (batch_size, seq_length, input_size)
h0 = torch.zeros(1, x.size(0), hidden_size).to(x.device) # 初始化隐藏状态
c0 = torch.zeros(1, x.size(0), hidden_size).to(x.device) # 初始化细胞状态
out, _ = self.lstm(x, (h0, c0)) # 输出的最后时刻的隐藏状态和细胞状态
out = self.fc(out[:, -1, :]) # 只取序列的最后一个时间步的输出
return out
# 数据集准备
x_data = torch.randn(1000, seq_length, input_size)
y_data = torch.randn(1000, output_size)
dataset = TensorDataset(x_data, y_data)
train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# 模型、损失函数和优化器
model = SimpleLSTM(input_size, hidden_size, output_size)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练过程
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for inputs, labels in train_loader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}')
GRU (Gated Recurrent Unit)
思路
GRU是对LSTM的一种简化版本,它通过减少门的数量来提高计算效率,同时仍然能够保持捕捉长期依赖的能力。GRU合并了LSTM中的输入门和遗忘门,只有两个门:重置门(Reset Gate)和更新门(Update Gate)。
结构
GRU的更新过程包括两个重要的门:
- 重置门:决定如何结合当前输入和前一时刻的隐藏状态:
- 更新门:决定当前时刻的隐藏状态应该包含多少来自前一时刻的状态:
- 计算最终的隐藏状态:
- 计算最终的隐藏状态:
计算过程
- 输入数据:输入序列数据。
- 状态更新:利用重置门和更新门来更新当前时刻的隐藏状态。
- 输出生成:计算并更新当前时刻的隐藏状态。
优缺点
- 优点:GRU比LSTM更简单、计算更高效,且在许多任务上表现良好。
- 缺点:虽然GRU在性能上有时优于LSTM,但在一些复杂任务中,LSTM可能仍然表现更好。
验证代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# 超参数
input_size = 10 # 输入特征维度
hidden_size = 20 # 隐藏层的维度
output_size = 1 # 输出维度(回归问题)
seq_length = 5 # 序列长度
batch_size = 32 # 批次大小
num_epochs = 10 # 训练轮数
# 构建一个简单的GRU模型
class SimpleGRU(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleGRU, self).__init__()
self.gru = nn.GRU(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# x: (batch_size, seq_length, input_size)
h0 = torch.zeros(1, x.size(0), hidden_size).to(x.device) # 初始化隐藏状态
out, _ = self.gru(x, h0) # 输出的最后时刻的隐藏状态
out = self.fc(out[:, -1, :]) # 只取序列的最后一个时间步的输出
return out
# 数据集准备
x_data = torch.randn(1000, seq_length, input_size)
y_data = torch.randn(1000, output_size)
dataset = TensorDataset(x_data, y_data)
train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# 模型、损失函数和优化器
model = SimpleGRU(input_size, hidden_size, output_size)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练过程
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for inputs, labels in train_loader:
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}')
大模型
Seq2Seq (Sequence-to-Sequence)
结构与思路
Seq2Seq是一种用于处理输入和输出序列长度不一致的任务的模型,尤其在机器翻译、文本生成等任务中非常常见。Seq2Seq模型通常由编码器(Encoder)和解码器(Decoder)组成。
- 编码器(Encoder):编码器的任务是接收输入序列并将其映射到一个固定长度的上下文向量(也叫隐状态)。传统的Seq2Seq采用RNN、LSTM或GRU作为编码器,输出的最后一层的隐藏状态作为整个输入序列的表示。
- 解码器(Decoder):解码器的任务是基于编码器的输出生成目标序列。解码器通常会使用RNN、LSTM或GRU,并且在生成时使用上一个时刻的输出作为当前时刻的输入。
计算过程
-
编码阶段:
- 输入序列的每个元素通过编码器逐步处理,更新隐藏状态
。
- 编码器最终输出的隐藏状态
作为上下文向量,传递给解码器。
- 输入序列的每个元素通过编码器逐步处理,更新隐藏状态
-
解码阶段:
- 解码器以上下文向量
作为初始状态,并通过逐步生成目标序列中的每个元素。
- 每个时刻的输出
是基于上一时刻的输出和当前时刻的隐藏状态计算得到。
- 解码器以上下文向量
注意力机制 (Attention Mechanism)
注意力机制的核心思想是为输入序列中的不同部分分配不同的注意力权重,从而让模型能够在不同时间步关注输入序列中的不同部分。这种机制在机器翻译、图像生成等任务中起到了至关重要的作用。
注意力机制其实就是一组权重值,加权给原始特征以达到“更关注”的效果。
缩放点积注意力 (Scaled Dot-Product Attention)
-
思路:缩放点积注意力通过计算输入的查询(Query)、键(Key)和值(Value)之间的相似度来决定每个位置的注意力权重。计算公式如下:
其中:Q 是查询矩阵(Query),K 是键矩阵(Key),V 是值矩阵(Value),dk 是键的维度,T是转置,缩放因子用于避免点积结果过大。
-
计算过程:
- 计算查询和键的点积:
- 缩放:将点积结果除以
- 应用Softmax得到注意力权重。
- 使用权重对值矩阵进行加权求和,得到最终的输出。
- 计算查询和键的点积:
自注意力(Self-Attention)
-
思路:自注意力机制是指同一序列的不同部分之间相互影响和关联,使用查询、键和值均来自同一输入序列。自注意力的计算过程与缩放点积注意力相同,只是查询、键和值来自同一序列。
-
计算过程:
- 计算输入序列中每个元素的查询、键和值。
- 通过查询与键的点积计算注意力权重。
- 对输入的值进行加权求和,得到新的表示。
多头注意力(Multi-Head Attention)
-
思路:多头注意力通过并行计算多个注意力头,捕捉不同的注意力模式。每个头独立地进行注意力计算,然后将所有头的输出连接起来并进行线性变换。
-
计算过程:
- 将输入的查询、键和值通过不同的线性变换分别映射到多个子空间。
- 对每个子空间进行缩放点积注意力计算。
- 将所有头的输出拼接起来并通过线性层进行变换。
示例:
CBAM模块:
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from tqdm import tqdm
import matplotlib.pyplot as plt
class CBAM(nn.Module):
def __init__(self, in_channels, reduction=16, kernel_size=7):
super().__init__()
# 通道注意力
self.ca = ChannelAttention(in_channels, reduction)
# 空间注意力
self.sa = SpatialAttention(kernel_size)
def forward(self, x):
x = self.ca(x) # 先通道注意力
x = self.sa(x) # 再空间注意力
return x
class ChannelAttention(nn.Module):
def __init__(self, in_channels, reduction=16):
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1) # 全局平均池化
self.max_pool = nn.AdaptiveMaxPool2d(1) # 全局最大池化
self.fc = nn.Sequential(
nn.Linear(in_channels, in_channels // reduction), # 降维
nn.ReLU(),
nn.Linear(in_channels // reduction, in_channels) # 恢复维度
)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
b, c, _, _ = x.size()
avg_out = self.fc(self.avg_pool(x).view(b, c)) # [B,C] → 全连接
max_out = self.fc(self.max_pool(x).view(b, c)) # [B,C] → 全连接
out = avg_out + max_out # 融合两种池化结果
return x * self.sigmoid(out).view(b, c, 1, 1) # 通道加权
class SpatialAttention(nn.Module):
def __init__(self, kernel_size=7):
super().__init__()
self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size//2)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avg_out = torch.mean(x, dim=1, keepdim=True) # 通道平均 [B,1,H,W]
max_out, _ = torch.max(x, dim=1, keepdim=True) # 通道最大 [B,1,H,W]
concat = torch.cat([avg_out, max_out], dim=1) # [B,2,H,W]
attn = self.sigmoid(self.conv(concat)) # [B,1,H,W]
return x * attn # 空间加权
class CNNWithCBAM(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU()
)
self.cbam1 = CBAM(32) # 在第一个卷积后加入CBAM
self.conv2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2)
)
self.cbam2 = CBAM(64) # 在第二个卷积后加入CBAM
self.fc = nn.Linear(64 * 16 * 16, num_classes) # CIFAR-10的尺寸是32x32,经过MaxPool2d(2)后为16x16
def forward(self, x):
x = self.conv1(x)
x = self.cbam1(x)
x = self.conv2(x)
x = self.cbam2(x)
x = x.view(x.size(0), -1) # 展平
x = self.fc(x)
return x
# 数据增强和归一化
transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 加载CIFAR-10数据集
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_set, batch_size=64, shuffle=True)
test_loader = DataLoader(test_set, batch_size=64, shuffle=False)
# 训练函数
def train_model(model, train_loader, test_loader, epochs=10, lr=0.001):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=lr)
train_losses, test_accs = [], []
for epoch in range(epochs):
model.train()
running_loss = 0.0
for images, labels in tqdm(train_loader, desc=f"Epoch {epoch+1}/{epochs}"):
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
train_loss = running_loss / len(train_loader)
train_losses.append(train_loss)
# 测试集准确率
model.eval()
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()
test_acc = 100 * correct / total
test_accs.append(test_acc)
print(f"Epoch {epoch+1}, Loss: {train_loss:.4f}, Test Acc: {test_acc:.2f}%")
# 绘制训练曲线
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(train_losses, label='Train Loss')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(test_accs, label='Test Accuracy')
plt.legend()
plt.show()
return model
model = CNNWithCBAM(num_classes=10)
model = train_model(model, train_loader, test_loader, epochs=10, lr=0.001)
torch.save(model.state_dict(), "cnn_with_cbam.pth")
print("Model saved to cnn_with_cbam.pth")
# 推理函数
def load_and_predict(model_path, test_image):
model = CNNWithCBAM(num_classes=10)
model.load_state_dict(torch.load(model_path))
model.eval()
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 假设test_image是PIL图像或numpy数组
input_tensor = transform(test_image).unsqueeze(0) # 增加batch维度
with torch.no_grad():
output = model(input_tensor)
_, predicted = torch.max(output, 1)
class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck']
print(f"Predicted class: {class_names[predicted.item()]}")
# 示例:从测试集中取一张图片进行预测
test_image, _ = test_set[0] # 实际使用时替换为自定义图像
load_and_predict("cnn_with_cbam.pth", test_image)
- 通道注意力的双线性层:降维减少计算量,升维恢复通道数,中间加入非线性。
- 双池化(平均+最大):捕捉不同统计特性(整体响应 vs 显著特征)。
- 空间注意力的通道压缩:沿通道维度聚合信息,突出空间重要性。
- CBAM的设计哲学:先通道("用哪些特征"),后空间("看哪里"),层次化注意力。
当前这个空间注意力仅关注--自动学习到的高激活区域:如果某些位置在多个通道上具有较高的平均值或最大值(如物体的边缘、纹理等),这些位置会获得更高权重。
若要指定关注区域,则可通过设置掩码(mask)引导注意力,“1”表示需要关注的区域。
def forward(self, x, mask=None):
avg_out = torch.mean(x, dim=1, keepdim=True)
max_out, _ = torch.max(x, dim=1, keepdim=True)
concat = torch.cat([avg_out, max_out], dim=1)
attn = self.sigmoid(self.conv(concat))
if mask is not None: # 外部传入掩码
mask = mask.to(x.device).unsqueeze(0).unsqueeze(0) # [1,1,H,W]
attn = attn * mask # 强制关注掩码区域
return x * attn
Transformer
Vaswani, A. "Attention is all you need." Advances in Neural Information Processing Systems (2017).
结构与思路
Transformer是一个完全基于注意力机制的模型,避免了传统RNN和LSTM的递归结构。Transformer由编码器和解码器组成,每个部分都包括多个注意力层和前馈网络。Transformer的优势在于它并行计算和高效处理长序列。
编码器-解码器结构
-
编码器:包含多层自注意力(Self-Attention)和前馈神经网络(Feed-Forward Neural Network)层。每层都有一个自注意力模块和一个前馈网络模块。
-
解码器:解码器在每层中有两个注意力模块:一个是自注意力,另一个是编码器-解码器注意力(Cross-Attention)。解码器还包括前馈神经网络。
计算过程
-
编码器:输入序列通过多层的自注意力模块和前馈网络逐步处理,最终输出上下文向量。
-
解码器:解码器在每层通过自注意力、编码器-解码器注意力和前馈网络生成输出,逐步生成目标序列。
多头注意力
Transformer使用多头注意力来同时捕捉不同的表示,每个头独立计算注意力,最后将它们拼接并通过线性变换得到最终的输出。
习题3.11 简述transformer中的三种注意力机制有什么相同点和不同点?
相同点:
- 全局信息建模:三种注意力机制都用于捕捉输入序列中各个位置之间的关系,能够通过序列中各个位置的交互来增强模型的表示能力。
- 加权求和:它们都依赖于通过加权求和来整合信息,权重通过注意力机制计算得出。
不同点:
- 自注意力机制关注序列中各位置之间的关系,捕捉全局信息。
- 多头注意力机制是多次应用自注意力机制,通过并行化使得模型能从不同子空间中捕捉多样的信息。
- 加权注意力机制通过计算点积相似度来得出注意力权重,并在其基础上加权求和,缩放操作避免数值问题。
NLP大模型
GPT(Generative Pre-trained Transformer)
-
结构与思路:GPT是一个基于Transformer解码器的语言生成模型。其核心思想是利用大规模无监督预训练来学习语言的分布式表示,然后通过微调来进行下游任务的适配。
-
计算过程:
- 预训练:通过自回归的方式进行预训练,预测下一个词。每个时刻,GPT使用之前的单词预测下一个单词。
- 微调:通过监督学习对模型进行微调,使其适应特定的任务(如文本生成、问答等)。
-
生成式任务:GPT是一种生成式模型,可以根据上下文生成自然语言文本。
BERT(Bidirectional Encoder Representations from Transformers)
-
结构与思路:BERT是一个基于Transformer编码器的模型,核心思想是通过双向上下文建模来捕捉词语的上下文信息。BERT是一个预训练-微调模型,在大规模文本数据上预训练,然后通过微调来适应不同的NLP任务。
-
计算过程:
- 预训练:
- 掩蔽语言模型(Masked Language Model,MLM):BERT通过随机掩蔽输入中的一些词,并训练模型预测这些被掩蔽的词。
- 下一句预测(Next Sentence Prediction,NSP):预测两个句子是否是连续的,从而加强句子间关系的理解。
- 微调:将预训练后的BERT模型微调到具体的任务中(如分类、问答等)。
- 预训练:
-
表征式任务:BERT是一种表征式模型,提供强大的语言理解能力,广泛应用于各种NLP任务。
CLIP(Contrastive Language-Image Pre-training)
CLIP模型是一种基于对比学习的多模态预训练模型,由OpenAI于2021年提出,旨在实现图像与文本的跨模态语义对齐。
- 主对角线:正样本
- 其余:负样本
总结
深度学习作为机器学习领域的重要分支,已成为推动各类智能系统发展的核心技术。随着技术的不断演进,深度学习模型经历了从基础的多层感知机(MLP)到复杂的卷积神经网络(CNN),再到具有强大生成能力的生成对抗网络(GAN)的发展。在这一过程中,各种网络架构的创新和优化极大地推动了图像识别、自然语言处理、语音识别等领域的突破性进展。
多层感知机(MLP)是最基本的神经网络之一,广泛用于分类和回归任务。通过前馈的方式,MLP通过多个隐藏层逐步抽象数据的特征,从而实现对输入数据的预测。尽管其结构简单,但MLP是理解更复杂神经网络(如卷积神经网络和循环神经网络)的基础。通过学习每一层的权重和偏置,MLP能够从输入中提取有用的信息,进行函数拟合和特征学习,从而实现目标任务。其训练过程采用梯度下降算法和反向传播机制,不断调整模型参数以最小化损失函数,最终获得准确的预测结果。
在目标检测领域,R-CNN、YOLO和SSD等模型通过不同的架构和创新方法,推动了检测技术的发展。R-CNN通过生成候选区域并使用CNN提取特征,在目标检测中取得了初步的成功。然而,它在速度上存在瓶颈,因为每个候选框都需要单独通过CNN进行处理。YOLO则通过一个单一的神经网络同时处理目标检测的分类和边界框回归,具备了实时检测的能力,其速度远快于R-CNN。SSD采用了多尺度的特征图,在不同尺度上进行检测,提高了对大小目标的检测能力。三者在目标检测中各具优势,从候选区域生成到多尺度检测,再到快速推理,逐步推动了实时目标检测技术的发展。
深度学习在推动人工智能发展的过程中,涌现了许多具有里程碑意义的网络架构,如 BNN (Binary Neural Network)、AlexNet 和 ResNet 等。BNN 通过将权重和激活值二值化,显著减少了计算和内存开销,适用于资源受限的环境。AlexNet 通过引入ReLU激活、LRN、Dropout等技术,在2012年ImageNet比赛中取得突破,推动了深度学习在计算机视觉中的应用。ResNet 则通过残差连接解决了深度网络训练中的梯度消失问题,使得可以训练更深的网络,从而提升了视觉任务的表现。
这些网络的创新不断推动了AI领域的进步,从加速计算到解决训练瓶颈,为后续模型如 Transformer、BERT 和 GPT 等奠定了基础。它们在不同应用场景中的成功,证明了深度学习在复杂任务中展现出的强大潜力和适应性。
在序列到序列的任务中,Seq2Seq模型提供了有效的解决方案,尤其是在机器翻译等任务中。通过编码器和解码器的结构,Seq2Seq能够处理输入和输出长度不一致的情况,然而其在捕捉长距离依赖时面临挑战。此时,注意力机制的引入大大改善了模型的表现,特别是在Transformer架构中,注意力机制被用来并行处理序列数据,避免了传统RNN和LSTM的计算瓶颈。Transformer通过完全基于注意力机制的架构,避免了递归计算的限制,具有显著的并行计算优势,成为当前许多NLP任务的基础。
在自然语言处理(NLP)领域,GPT和BERT等大规模预训练模型的出现,标志着深度学习技术在文本理解和生成方面的巨大进步。GPT通过自回归的方式生成文本,能够根据输入的上下文生成流畅的自然语言。而BERT则通过双向上下文建模,在预训练阶段通过掩蔽语言模型和下一句预测任务,学习到了丰富的语言表示,极大提升了下游任务的性能。两者的成功验证了预训练和微调的有效性,使得大规模预训练模型成为NLP任务的主流。
综上所述,深度学习模型的演变从传统的多层感知机到现代的Transformer和大规模预训练模型,不仅提高了任务的准确性,也极大地拓展了应用的范围。每个模型在其特定领域的创新和优势,都为推动人工智能技术的发展贡献了力量。在不断的学习和实践中,我们能够深入理解这些模型的架构和原理,并在实际应用中不断提出新的思路与创新。