pytorch深度学习实战:从全连接神经网络到卷积神经网络

1. 从全连接到卷积

1.1. 卷积的优势

在图像中,卷积对于全连接的优势:
img

  1. 局部操作

  2. 平移不变性

    相同的核以及核中的每个权重在整幅图像中被重用。回想一下自动求导,这意味着每个权重的使用都有一个跨越整个图像的历史值。因此,关于卷积权值的损失的导数包括来自整个图像的贡献。

  3. 模型的参数大幅减少

img

由上得,对于一层网络
卷积权重数量16*3*3*3=1728
全连接权重数量3*32*32=9216
-> 模型参数大幅减小

1.2. 卷积神经网络的工作原理

卷积神经网络的工作是估计连续层中的一组过滤器的卷积核,这些过滤器将把一个多通道图像转换成另一个多通道图像,其中不同的通道对应不同的特征,例如一个通道代表平均值,一个通道代表垂直边缘,等等。如下所示

img

1.3. padding的计算方法

3个关键参数:

参数 含义 常用符号
输入尺寸 图像/特征图的边长(比如32×32的图,边长=32) \(H_{in}\)/\(W_{in}\)
卷积核大小 卷积核的边长(比如3×3的核,边长=3) \(K\)
步长 卷积核每次滑动的距离(默认=1) \(S\)

则,卷积后输出尺寸的计算公式:

\[H_{out} = \frac{H_{in} + 2×padding - K}{S} + 1 \]

(宽度\(W\)的计算和高度\(H\)完全一样)

import torch.nn as nn

# 方式1:手动指定(3×3卷积核,padding=1,输出尺寸=输入尺寸)
conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1, stride=1)

# 方式2:自动计算(PyTorch 1.10+支持,推荐)
conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding="same", stride=1)

1.4. 看到更多特征

如何使用小卷积核看到更大范围(更大的感受野)的物体?
在一个卷积之后堆叠另一个卷积,同时在连续卷积之间对图像进行下采样(池化)。

img

第 1 组卷积核对一阶、低级特征的小邻域进行操作,而第 2 组卷积核则有效地对更宽的邻域进行操作,生成由先前特征组成的特征。

pool = nn.MaxPool2d(2) # 如果我们想把图像缩小一半,我们让核大小为 2
output = pool(img.unsqueeze(0)) # 增加一个 batch 维度
# 32*32 -> 16*16

因此,我们搭建模型:

model = nn.Sequential(
    nn.Conv2d(3, 16, kernel_size=3, padding=1),
    nn.Tanh(),
    nn.MaxPool2d(2),
    nn.Conv2d(16, 8, kernel_size=3, padding=1),
    nn.Tanh(),
    nn.MaxPool2d(2),
    # --- 这里原代码缺少了 Flatten 层 ---
    nn.Linear(8 * 8 * 8, 32),
    nn.Tanh(),
    nn.Linear(32, 2),
)

img

2. 自定义模型

然而,当我们想要构建模型来做更复杂的事情,而不仅仅是一层接着一层地应用时,我们需要放弃nn.Sequential 运算带来的灵活性。PyTorch 允许我们在模型中通过子类化 nn.Module 来进行任何运算。

  1. 至少需要定义一个 forward()方法,接收模块的输入并返回输出。
  2. 为了包含这些子模块,我们通常在构造函数__init__()中定义运算使用的其他模块,例如内置的卷积或自定义的模块,并将它们分配给 self 以便在 forward()方法中使用。同时,它们将在模块的整个生命周期中保存它们的参数。
PyTorch 如何自动跟踪子模块和参数

2.1. PyTorch 如何自动跟踪子模块和参数

PyTorch 里的 nn.Module 有一个非常聪明的机制:

  • 当你在自定义模型的 __init__ 函数里,把 nn.Conv2dnn.Linear 等层赋值为模型的顶级属性时(比如 self.conv1 = nn.Conv2d(...)),PyTorch 会自动把这些层识别为“子模块”。
  • 这些子模块的参数(权重、偏置)会被自动收集到模型的 parameters() 方法里,优化器可以直接通过 model.parameters() 获取所有可训练参数。

通俗类比
这就像你开了一家公司(模型),招聘了几个部门经理(子模块,如卷积层、全连接层)。公司会自动把所有经理的员工(参数)都纳入人事系统,你不需要一个个去登记。


2.1.1. 为什么子模块不能藏在列表或字典里

如果你把层放在普通的 listdict 里,比如:

self.layers = [nn.Conv2d(...), nn.Linear(...)] # 不能这样写!

PyTorch 就无法自动识别这些子模块,它们的参数也不会被 model.parameters() 收集到,优化器就无法更新这些参数。

解决方法

  • 用 PyTorch 提供的 nn.ModuleListnn.ModuleDict 来存放子模块,这样 PyTorch 就能正确跟踪它们。

2.1.2. parameters() 方法的作用

model.parameters() 会递归遍历所有子模块,把所有可训练参数(权重、偏置)收集成一个列表。

比如你定义了一个 ResNet 模型,调用 model.parameters() 会返回所有卷积层、全连接层的权重和偏置。优化器就是通过这个列表来更新模型参数的。


2.1.3. 为什么 forward 里直接调用 nn.ReLU() 不好

如果你在 forward 里直接写:

def forward(self, x):
    x = nn.ReLU()(x)  # 直接调用,没有注册为子模块
    return x

这样的层没有被注册为模型的子模块,而是每次 forward 时临时创建的。

  • 这些层如果没有可训练参数(比如 ReLUMaxPool2d),不会影响参数更新,但会让模型的结构变得不清晰。
  • 如果是有参数的层(比如 Conv2d),它们的参数不会被 model.parameters() 收集,导致优化器无法更新。

正确做法
把所有层都在 __init__ 里注册为子模块,即使是没有参数的层:

def __init__(self):
    self.relu = nn.ReLU()

def forward(self, x):
    x = self.relu(x)
    return x

或者用函数式API,见下节2.2


2.1.4. 总结

  1. 子模块注册规则:在 __init__ 里把层赋值为模型的顶级属性(如 self.conv1),PyTorch 会自动跟踪。
  2. 禁止藏在普通容器:不要用普通 list/dict 存子模块,要用 nn.ModuleList/nn.ModuleDict
  3. parameters() 的作用:自动收集所有子模块的可训练参数,供优化器使用。
  4. forward 里的层也要注册:即使是无参数层(如 ReLU),也建议在 __init__ 里注册,让模型结构更清晰。

2.2. 函数式 API 和模块化 API 的区别以及各自的用法

2.2.1. 什么是函数式 API?

在 PyTorch 里,函数式 API 就是 torch.nn.functional 下的一系列函数(比如 F.linearF.tanhF.max_pool2d)。

  • 它们没有内部状态,输出完全由输入参数决定。
  • 比如 F.linear(input, weight, bias),权重和偏置都需要你手动传进去,函数本身不会保存这些参数。
  • 这和 nn.Linear(模块化 API)完全不同:nn.Linear 会把权重和偏置作为自己的内部参数保存下来。

通俗类比

  • 函数式 API 就像“一次性计算器”:每次计算都要把所有数字(输入、权重、偏置)都给它,算完就扔,不保留任何数据。
  • 模块化 API 就像“带记忆的计算器”:会把常用的数字(权重、偏置)存起来,下次直接用,不用每次都重新输入。

2.2.2. 什么时候用函数式 API?

函数式 API 适合那些不需要保存参数和状态的操作,比如:

  • 激活函数:F.tanhF.relu
  • 池化操作:F.max_pool2d
  • 损失函数:F.cross_entropy

因为这些操作没有可训练的参数,用函数式 API 更轻量、更灵活。

例子对比

# 模块化 API(有参数,保存权重)
linear_layer = nn.Linear(10, 2)
output = linear_layer(input)

# 函数式 API(无参数,手动传权重)
weight = torch.randn(2, 10)
bias = torch.randn(2)
output = F.linear(input, weight, bias)

2.2.3. 为什么 nn.Linearnn.Conv2d 要用模块化 API?

因为这些层有可训练的参数(权重、偏置),需要在训练中不断更新。

  • 模块化 API 会自动保存这些参数,并且通过 model.parameters() 让优化器可以轻松找到它们。
  • 如果用函数式 API,你需要自己手动管理权重和偏置的更新,非常麻烦。

2.2.4. 代码的核心逻辑

你看到的 Net 类就是一个典型的混合使用方式:

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        # 有参数的层用模块化 API
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(16, 8, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(8 * 8 * 8, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        # 无参数的操作(激活、池化)用函数式 API
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2) # pool使用函数式 API
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = out.view(-1, 8 * 8 * 8)  # 自适应批处理维度
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out
  • __init__:用模块化 API 定义有参数的层(卷积、全连接),让 PyTorch 自动跟踪参数。
  • forward:用函数式 API 处理无参数的操作tanhmax_pool2d),让代码更简洁。

2.2.5. 为什么说“在 forward 里用 nn.ReLU() 不好”?(接2.1.3.)

如果在 forward 里直接写 nn.ReLU()(x),相当于每次前向传播都创建一个新的 ReLU 实例,既浪费资源,又让模型结构不清晰。

  • 正确做法:无参数层也建议在 __init__ 里注册为子模块(比如 self.relu = nn.ReLU()),或者直接用函数式 API(F.relu(x))。

2.2.6. 总结

类型 优点 适用场景
模块化 APInn.Linearnn.Conv2d 自动保存参数,优化器可直接获取 有可训练参数的层(卷积、全连接)
函数式 APIF.linearF.tanh 轻量、灵活,无内部状态 无参数的操作(激活、池化、损失计算)

简单来说:有参数的用模块化,没参数的用函数式,这样代码既清晰又高效。


2.3. 最终搭建的模型

在全连接网络的基础上,修改代码为卷积网络。修改部分如下:

img

img

点击查看完整代码
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets
from torchvision.transforms import ToTensor # PIL (0~255) -> Tensor (0.0~1.0)
from torchvision.transforms import Normalize # 归一化
from torchvision.transforms import Compose # 组合多个变换
import torch.nn.functional as F # 函数式 API

# -----------------------------------------------------------------------
# 加载数据
data_path = './data/CIFAR-10/'
transform = Compose([ToTensor(), Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))])
transformed_cifar10 = datasets.CIFAR10(data_path, train = True, download = True, transform = transform)
transformed_cifar10_val = datasets.CIFAR10(data_path, train = False, download = True, transform = transform)

# -----------------------------------------------------------------------
# 一个只有鸟和飞机的字数据集
label_map = {0: 0, 2: 1} # 0: 鸟, 1: 飞机
class_names = ['airplane', 'bird']
cifar2 = [(img, label_map[label]) for img, label in transformed_cifar10 if label in [0, 2]]
cifar2_val = [(img, label_map[label]) for img, label in transformed_cifar10_val if label in [0, 2]]

train_loader = torch.utils.data.DataLoader(cifar2, batch_size = 64, shuffle = True)
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size = 64, shuffle = False)
# -----------------------------------------------------------------------
# 搭建一个全连接网络
## 定义模型
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.act1 = nn.Tanh()
        self.conv2 = nn.Conv2d(16, 8, kernel_size=3, padding=1)
        self.act2 = nn.Tanh()
        self.fc1 = nn.Linear(8 * 8 * 8, 32)
        self.act3 = nn.Tanh()
        self.fc2 = nn.Linear(32, 2)

    def forward(self,x):
        out = F.max_pool2d(self.act1(self.conv1(x)), 2) # pool使用函数式 API
        out = F.max_pool2d(self.act2(self.conv2(out)), 2)
        out = out.view(-1, 8 * 8 * 8) # 自适应批处理维度
        out = self.act3(self.fc1(out))
        out = self.fc2(out)
        return out

model = Net()

learning_rate = 1e-2
optimizer = optim.SGD(model.parameters(), lr = learning_rate)

loss_fn = nn.CrossEntropyLoss()

# -----------------------------------------------------------------------
## 训练分类器
## 书本P185~P187, 代码逐句详解
def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
    for epoch in range(1, n_epochs + 1):
        loss_train = 0.0
        for imgs, label in train_loader:
            outputs = model(imgs)
            loss = loss_fn(outputs, label)

            optimizer.zero_grad() # 归零上一轮的梯度
            loss.backward() # 更新这一轮的梯度
            optimizer.step() # 根据梯度更新参数

            loss_train += loss.item()

        if epoch % 10 == 0 or epoch == 1:
            print(f'{epoch} Epoch, Training Loss: {format(loss_train / len(train_loader))}')

training_loop(
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader
)
# -----------------------------------------------------------------------
## 验证分类器
def validate(model, train_loader, val_loader):
    for name, loader in [("train", train_loader), ("val", val_loader)]:
        n_correct = 0
        n_total = 0

        with torch.no_grad():
            for imgs, label in loader:
                out = model(imgs)
                _, predicted = torch.max(out, dim = 1)
                n_correct += int((predicted == label).sum())
                n_total += imgs.shape[0]

        print(f"{name} Accuracy: {n_correct / n_total}")

validate(model, train_loader, val_loader)

输出:

1 Epoch, Training Loss: 0.5591110106866071
10 Epoch, Training Loss: 0.3240322130880538
20 Epoch, Training Loss: 0.28498813206223167
30 Epoch, Training Loss: 0.26027746427400855
40 Epoch, Training Loss: 0.24314728122987564
50 Epoch, Training Loss: 0.23136532102610655
60 Epoch, Training Loss: 0.218075687433504
70 Epoch, Training Loss: 0.20048291872071614
80 Epoch, Training Loss: 0.19081769541949983
90 Epoch, Training Loss: 0.17507808528555807
100 Epoch, Training Loss: 0.1622462912349944
train Accuracy: 0.9368
val Accuracy: 0.8995

2.4. GPU 训练

在GPU训练, 需要修改如下部分:

# 检查GPU设备
# -----------------------------------------------------------------------
# 选择设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')

# -----------------------------------------------------------------------
# 将模型移动到GPU
model = Net().to(device=device)# <- GPU

# -----------------------------------------------------------------------
# 将dataloader中的内容移动到cpu
def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
    for epoch in range(1, n_epochs + 1):
        loss_train = 0.0
        for imgs, label in train_loader:
            imgs = imgs.to(device=device)  # <- GPU
            label = label.to(device=device)
            # ...

# -----------------------------------------------------------------------
## 验证分类器
def validate(model, train_loader, val_loader):
    for name, loader in [("train", train_loader), ("val", val_loader)]:
        n_correct = 0
        n_total = 0

        with torch.no_grad():
            for imgs, label in loader:
                imgs = imgs.to(device=device) # <- GPU
                label = label.to(device=device) # <- GPU

2.5. 保存与加载模型

2.5.1. 保存模型

torch.save(model.state_dict(), './data/cifar2_model.pt')

2.5.2. 加载模型

load_model = Net()
load_model.load_state_dict(torch.load('./data/cifar2_model.pt'))

注意当加载权重时,需要指示pytorch覆盖设备信息:

load_model = Net().to(device=device)
load_model.load_state_dict(torch.load('./data/cifar2_model.pt'), 
                            map_location=device)  # <- 注意此处

原因:

加载网络权重时有一点儿复杂:PyTorch 将尝试将权重加载到保存它的同一设备上。也就是说,GPU 上的权重将恢复到 GPU 上。由于我们不知道是否需要相同的设备,我们有 2 个选择:在保存之前将网络移动到 CPU,或者在恢复后将其移回。在加载权重时,指示 PyTorch 覆盖设备信息会更简洁一些。这是通过将 map_location 关键字参数传递给 torch.load 来实现的

3. 改进模型

3.1. 增加内存容量:宽度

# 避免模型中的硬编码
class Net(nn.Module):
    def __init__(self, n_chans1 = 32): # 16 -> 32 增加内存容量:宽度,但是容易过拟合
        super().__init__()
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.act1 = nn.Tanh()
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3, padding=1)
        self.act2 = nn.Tanh()
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
        self.act3 = nn.Tanh()
        self.fc2 = nn.Linear(32, 2)

    def forward(self,x):
        out = F.max_pool2d(self.act1(self.conv1(x)), 2) # pool使用函数式 API
        out = F.max_pool2d(self.act2(self.conv2(out)), 2)
        out = out.view(-1, 8 * 8 * n_chans1 // 2) # 自适应批处理维度
        out = self.act3(self.fc1(out))
        out = self.fc2(out)
        return out

3.2. 帮助模型收敛和泛化:正则化

训练模型涉及 2 个关键步骤:一是优化,当我们需要减少训练集上的损失时;二是泛化,当模型不仅要处理训练集,还要处理以前没有见过的数据,如验证集时。旨在简化这 2 个步骤的数学工具有时被归入正则化的标签之下。

3.2.1. 检查参数:权重惩罚

作用:稳定泛化,防止过拟合。

避免关注几个特征,让模型更关注通用特征。
给模型加 “紧箍咒”,参数不能变得太大(避免过拟合)。

# weight_decay实现
optimizer2 = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)  # λ=1e-4

3.2.2. 不太依赖单一输入:Dropout

作用:防止过拟合

将网络每轮训练迭代中的神经元随机部分清零。

在 PyTorch 中,我们可以通过在非线性激活与后面的线性或卷积模块之间添加一个nn.Dropout 模块在模型中实现 Dropout。作为一个参数,我们需要指定输入归零的概率。如果是卷积,我们将使用专门的 nn.Dropout2d 或者 nn.Dropout3d,将输入的所有通道归零.

需要区分model.train()和model.eval()

class Net(nn.Module):
    def __init__(self, n_chans1 = 32):
        super().__init__()
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.conv1_dropout = nn.Dropout2d(p=0.4)
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3, padding=1)
        self.conv2_dropout = nn.Dropout2d(p=0.4)
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self,x):
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
        out = self.conv1_dropout(out)
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = self.conv2_dropout(out)
        out = out.view(-1, 8 * 8 * n_chans1 // 2)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out

3.2.3. 保存激活检查:批量归一化

作用:提高学习率,减少训练对初始化的依赖,并充当正则化器,提出了一种替代 Dropout 的方法。主要思想是将输入重新调整到网络的激活状态,从而使小批量具有一定的理想分布。

需要区分model.train()和model.eval()

class NetBatchNorm(nn.Module): 
    def __init__(self, n_chans1=32): 
        super().__init__() 
        self.n_chans1 = n_chans1 
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1) 
        self.conv1_batchnorm = nn.BatchNorm2d(num_features=n_chans1) 
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3, 
        padding=1) 
        self.conv2_batchnorm = nn.BatchNorm2d(num_features=n_chans1 // 2) 
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32) 
        self.fc2 = nn.Linear(32, 2) 

    def forward(self, x): 
        out = self.conv1_batchnorm(self.conv1(x)) 
        out = F.max_pool2d(torch.tanh(out), 2) 
        out = self.conv2_batchnorm(self.conv2(out)) 
        out = F.max_pool2d(torch.tanh(out), 2) 
        out = out.view(-1, 8 * 8 * self.n_chans1 // 2) 
        out = torch.tanh(self.fc1(out)) 
        out = self.fc2(out) 
        return out

3.3. 更复杂的结构:深度

3.3.1. 跳跃连接

深度带来挑战:
1.梯度消失/爆炸。反向传播的梯度会在逐层传递中不断缩小,导致浅层的参数几乎无法更新。
2.参数数量增加,容易过拟合

跳跃连接在反向传播时,梯度可以通过跳跃连接直接传递到浅层:

img

class NetRes(nn.Module): 
    def __init__(self, n_chans1=32): 
        super().__init__() 
        self.n_chans1 = n_chans1 
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1) 
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3, 
        padding=1) 
        self.conv3 = nn.Conv2d(n_chans1 // 2, n_chans1 // 2, 
        kernel_size=3, padding=1) 
        self.fc1 = nn.Linear(4 * 4 * n_chans1 // 2, 32) 
        self.fc2 = nn.Linear(32, 2) 

    def forward(self, x): 
        out = F.max_pool2d(torch.relu(self.conv1(x)), 2) 
        out = F.max_pool2d(torch.relu(self.conv2(out)), 2) 
        out1 = out 
        out = F.max_pool2d(torch.relu(self.conv3(out)) + out1, 2) 
        out = out.view(-1, 4 * 4 * self.n_chans1 // 2) 
        out = torch.relu(self.fc1(out)) 
        out = self.fc2(out) 
        return out

3.3.2. 用跳跃连接建立一个非常深的模型

接着,我们可以用跳跃连接建立一个非常深的模型:

# 模块子类,为一个块提供运算,
class ResBlock(nn.Module):
    def __init__(self, n_chans):
        super().__init__()
        self.conv = nn.Conv2d(n_chans, n_chans, kernel_size=3, padding=1, bias=False)
        self.batch_norm = nn.BatchNorm2d(num_features=n_chans) 
        torch.nn.init.kaiming_normal_(self.conv.weight, nonlinearity='relu') 
        torch.nn.init.constant_(self.batch_norm.weight, 0.5) 
        torch.nn.init.zeros_(self.batch_norm.bias)

    def forward(self, x): 
        out = self.conv(x) 
        out = self.batch_norm(out) 
        out = torch.relu(out) 
        return out + x

# 网络模型
class Net(nn.Module): 
    def __init__(self, n_chans1=32, n_blocks=10): 
        super().__init__() 
        self.n_chans1 = n_chans1 
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1) 

        # - ResBlock(n_chans=n_chans1):创建一个残差块实例(比如通道数为 32 的 ResBlock);
        # - [ResBlock(...)]:把这一个实例放进列表,得到 [ResBlock实例];
        # - n_blocks * [...]:把列表重复 n_blocks 次(比如 n_blocks=10 时,得到包含 10 个 ResBlock 实例的列表 [ResBlock, ResBlock, ..., ResBlock])
        # - *:解包列表,把列表元素变成 nn.Sequential 的独立参数
        self.resblocks = nn.Sequential( 
        *(n_blocks * [ResBlock(n_chans=n_chans1)]))  # <- 

        self.fc1 = nn.Linear(8 * 8 * n_chans1, 32) 
        self.fc2 = nn.Linear(32, 2) 
    def forward(self, x): 
        out = F.max_pool2d(torch.relu(self.conv1(x)), 2) 
        out = self.resblocks(out)  # <-
        out = F.max_pool2d(out, 2) 
        out = out.view(-1, 8 * 8 * self.n_chans1) 
        out = torch.relu(self.fc1(out)) 
        out = self.fc2(out) 
        return out

4. 本文最终代码

点击展开
# 更深的卷积网络(利用跳跃连接)
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets
from torchvision.transforms import ToTensor # PIL (0~255) -> Tensor (0.0~1.0)
from torchvision.transforms import Normalize # 归一化
from torchvision.transforms import Compose # 组合多个变换
import torch.nn.functional as F # 函数式 API

# -----------------------------------------------------------------------
# 加载数据
data_path = './data/CIFAR-10/'
transform = Compose([ToTensor(), Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))])
transformed_cifar10 = datasets.CIFAR10(data_path, train = True, download = True, transform = transform)
transformed_cifar10_val = datasets.CIFAR10(data_path, train = False, download = True, transform = transform)

# -----------------------------------------------------------------------
# 一个只有鸟和飞机的字数据集
label_map = {0: 0, 2: 1} # 0: 鸟, 1: 飞机
class_names = ['airplane', 'bird']
cifar2 = [(img, label_map[label]) for img, label in transformed_cifar10 if label in [0, 2]]
cifar2_val = [(img, label_map[label]) for img, label in transformed_cifar10_val if label in [0, 2]]

train_loader = torch.utils.data.DataLoader(cifar2, batch_size = 64, shuffle = True)
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size = 64, shuffle = False)

# -----------------------------------------------------------------------
# 选择设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')

# -----------------------------------------------------------------------
# 搭建一个全连接网络
## 定义模型
class ResBlock(nn.Module):
    def __init__(self, n_chans):
        super().__init__()
        self.conv = nn.Conv2d(n_chans, n_chans, kernel_size=3, padding=1, bias=False)
        self.batch_norm = nn.BatchNorm2d(num_features=n_chans) 
        torch.nn.init.kaiming_normal_(self.conv.weight, nonlinearity='relu') 
        torch.nn.init.constant_(self.batch_norm.weight, 0.5) 
        torch.nn.init.zeros_(self.batch_norm.bias)

    def forward(self, x): 
        out = self.conv(x) 
        out = self.batch_norm(out) 
        out = torch.relu(out) 
        return out + x

class Net(nn.Module): 
    def __init__(self, n_chans1=32, n_blocks=10): 
        super().__init__() 
        self.n_chans1 = n_chans1 
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1) 
        self.resblocks = nn.Sequential( 
        *(n_blocks * [ResBlock(n_chans=n_chans1)])) 
        self.fc1 = nn.Linear(8 * 8 * n_chans1, 32) 
        self.fc2 = nn.Linear(32, 2) 
    def forward(self, x): 
        out = F.max_pool2d(torch.relu(self.conv1(x)), 2) 
        out = self.resblocks(out) 
        out = F.max_pool2d(out, 2) 
        out = out.view(-1, 8 * 8 * self.n_chans1) 
        out = torch.relu(self.fc1(out)) 
        out = self.fc2(out) 
        return out

model = Net().to(device=device)
model.parameters()
for p in model.parameters():
    print(p.shape)
    p.numel()

learning_rate = 3e-3
optimizer = optim.SGD(model.parameters(), lr = learning_rate)

loss_fn = nn.CrossEntropyLoss()

# -----------------------------------------------------------------------
## 训练分类器
## 书本P185~P187, 代码逐句详解
def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
    for epoch in range(1, n_epochs + 1):
        loss_train = 0.0
        for imgs, label in train_loader:
            imgs = imgs.to(device=device)
            label = label.to(device=device)
            outputs = model(imgs)
            loss = loss_fn(outputs, label)

            optimizer.zero_grad() # 归零上一轮的梯度
            loss.backward() # 更新这一轮的梯度
            optimizer.step() # 根据梯度更新参数

            loss_train += loss.item()

        if epoch % 10 == 0 or epoch == 1:
            print(f'{epoch} Epoch, Training Loss: {format(loss_train / len(train_loader))}')

training_loop(
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader
)
# -----------------------------------------------------------------------
## 验证分类器
def validate(model, train_loader, val_loader):
    for name, loader in [("train", train_loader), ("val", val_loader)]:
        n_correct = 0
        n_total = 0

        with torch.no_grad():
            for imgs, label in loader:
                imgs = imgs.to(device=device)
                label = label.to(device=device)
                out = model(imgs)
                _, predicted = torch.max(out, dim = 1)
                n_correct += int((predicted == label).sum())
                n_total += imgs.shape[0]

        print(f"{name} Accuracy: {n_correct / n_total}")

validate(model, train_loader, val_loader)


输出:

Using device: cuda
1 Epoch, Training Loss: 0.49166663057485205
10 Epoch, Training Loss: 0.2634233754531593
20 Epoch, Training Loss: 0.1906170040891049
30 Epoch, Training Loss: 0.12040648637873352
40 Epoch, Training Loss: 0.06903127147252583
50 Epoch, Training Loss: 0.03015419604422845
60 Epoch, Training Loss: 0.015214779083528052
70 Epoch, Training Loss: 0.010205398788260427
80 Epoch, Training Loss: 0.005108015554672356
90 Epoch, Training Loss: 0.0037311426546294125
100 Epoch, Training Loss: 0.0026776732909893556
train Accuracy: 1.0
val Accuracy: 0.8835
posted @ 2026-01-20 22:04  榴红八色鸫  阅读(4)  评论(0)    收藏  举报