← Back to Learning Notes

Lecture 0:PyTorch 总结

Chinese-language Computer Science 408 note: Lecture 0:PyTorch 总结. Original source available where applicable.

On this page

This note is currently available in Chinese. The original wording, formulas and diagrams are preserved to avoid an unverified technical translation.

描述:本节系统整理 PyTorch 的环境安装、帮助系统、Dataset 与 DataLoader、TensorBoard、Tensor 与图像变换、常用神经网络层、CIFAR-10 网络、损失函数、反向传播、模型训练与保存、数据增强以及迁移学习。源课件中的过时接口、拼写错误和数值错误已按当前 PyTorch 语义纠正。

0.1 环境与安装

0.1.1 为什么使用虚拟环境

虚拟环境可以把不同项目的 Python、PyTorch 和依赖版本彼此隔离。例如,一个旧项目可能依赖较早版本,另一个项目需要较新版本;分别建立环境可以避免包版本冲突。

使用 Conda 创建并进入环境:

conda create -n pytorch python=3.11
conda activate pytorch
  • 源课件以 Python 3.6 为例,这是旧版本示范,不应机械照搬。
  • 实际安装时,应在 PyTorch 官方安装页面按操作系统、包管理器和 CUDA 版本生成命令。
  • 只有 NVIDIA 显卡驱动、CUDA 运行时与 PyTorch 构建版本相互兼容时,GPU 才能正常使用。

安装后检查:

import torch

print(torch.__version__)
print(torch.cuda.is_available())

torch.cuda.is_available() 返回 True 表示当前 PyTorch 可以使用 CUDA;返回 False 不一定表示安装失败,也可能是没有可用 NVIDIA GPU、驱动不匹配或安装了 CPU 版本。

0.1.2 dir 与 help

学习陌生模块时,两个内置函数很实用:

import torch

print(dir(torch))
print(dir(torch.cuda))
help(torch.cuda.is_available)
  • dir(obj):查看对象可访问的属性和方法,即“工具箱里有什么”。
  • help(obj):查看函数或类的说明、参数和返回值,即“工具怎么用”。
  • 以双下划线开头和结尾的名称通常是 Python 特殊方法,例如 __len__、__getitem__。

0.2 数据抽象:Dataset 与 DataLoader

0.2.1 二者分工

组件 主要职责 核心问题
Dataset 定义样本存在哪里、如何按索引读取、共有多少个样本 “数据是什么”
DataLoader 按批次取样、是否打乱、多进程加载、如何拼接样本 “数据怎样送入模型”

地图式数据集通常实现:

from torch.utils.data import Dataset

class MyDataset(Dataset):
    def __len__(self):
        ...

    def __getitem__(self, index):
        ...
  • __len__() 返回样本总数。
  • __getitem__(index) 返回第 index 个样本,通常形式为 (data, label)。

0.2.2 自定义图片数据集

假设目录结构为:

dataset/
└─ train/
   ├─ ants/
   └─ bees/

可按类别目录构造数据集:

from pathlib import Path

from PIL import Image
from torch.utils.data import Dataset


class ImageFolderDataset(Dataset):
    def __init__(self, root_dir, label_dir, transform=None):
        self.root_dir = Path(root_dir)
        self.label_dir = label_dir
        self.transform = transform
        self.image_paths = sorted((self.root_dir / label_dir).iterdir())

    def __len__(self):
        return len(self.image_paths)

    def __getitem__(self, index):
        image = Image.open(self.image_paths[index]).convert("RGB")
        if self.transform is not None:
            image = self.transform(image)
        return image, self.label_dir


ants_dataset = ImageFolderDataset("dataset/train", "ants")
bees_dataset = ImageFolderDataset("dataset/train", "bees")
train_dataset = ants_dataset + bees_dataset
  • Image.open(...) 得到 PIL 图像对象;训练彩色网络时通常显式转为 RGB。
  • 两个 Dataset 使用 + 连接时会形成 ConcatDataset,索引会依次覆盖两个子数据集。
  • 实际分类训练通常把标签映射为整数,如 ants -> 0、bees -> 1。

0.2.3 DataLoader 的关键参数

from torch.utils.data import DataLoader

loader = DataLoader(
    dataset=train_dataset,
    batch_size=4,
    shuffle=False,
    num_workers=0,
    drop_last=False,
)
参数 含义
dataset 被加载的数据集
batch_size 每批样本数
shuffle 每轮是否重新打乱索引,默认 False
sampler 自定义采样策略;通常不能与 shuffle 同时设置
batch_sampler 直接产生一批索引,与 batch_size、shuffle 等参数互斥
num_workers 加载子进程数量;0 表示在主进程加载
collate_fn 把一批样本拼成批张量的规则
pin_memory 是否把张量放入页锁定内存,GPU 传输时可能更快
drop_last 样本数不能整除批大小时,是否丢弃最后一个不完整批次

使用:

for images, targets in loader:
    print(images.shape)
    print(targets)
  • batch_size=4 表示一次取出并拼接四个样本。
  • 正确类名是 DataLoader,正确参数名是 num_workers。
  • DataLoader 默认不会随机抓取;训练集若需要每轮打乱,应显式设置 shuffle=True。

0.3 TensorBoard 可视化

TensorBoard 可记录图像、标量损失和其他训练信息。

import numpy as np
from PIL import Image
from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter("logs")

image = Image.open("example.jpg").convert("RGB")
image_array = np.asarray(image)
writer.add_image("example", image_array, global_step=0, dataformats="HWC")

for step in range(100):
    writer.add_scalar("y=2x", 2 * step, step)

writer.close()

然后在相应环境中启动:

tensorboard --logdir=logs
  • NumPy/PIL 图像通常是 [H, W, C],所以要声明 dataformats="HWC"。
  • PyTorch 图像张量通常是 [C, H, W],可直接使用默认的 CHW。
  • global_step 用来表示训练步数,使同一标签下的数据按时间排列。
  • 写入完成后应调用 close(),也可使用 with SummaryWriter(...) as writer:。

0.4 Tensor 与 transforms

0.4.1 Tensor 的含义

Tensor 是多维数组,是 PyTorch 中存储数据、参数和中间结果的基本对象。

主要属性包括:

  • shape:各维大小。
  • dtype:元素类型。
  • device:位于 CPU 还是 GPU。
  • requires_grad:是否需要自动追踪梯度。
  • grad:反向传播后累积的梯度,通常只在叶子张量上直接保存。

因此,Tensor 不是“专门包装反向传播参数的对象”;普通输入数据也可以是 Tensor,只有需要时才启用梯度追踪。

0.4.2 ToTensor

from PIL import Image
from torchvision import transforms

image = Image.open("example.jpg").convert("RGB")
to_tensor = transforms.ToTensor()
image_tensor = to_tensor(image)

print(image_tensor.shape)

对常见的 8 位 PIL/NumPy 图像,ToTensor() 会:

  1. 把维度从 [H, W, C] 转为 [C, H, W]。
  2. 把整数像素值从 $[0,255]$ 缩放为浮点数 $[0,1]$。

0.4.3 Compose

Compose 按顺序组合多个变换:

transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
])

image_tensor = transform(image)

每个变换的输出类型和形状必须能作为下一个变换的输入。

0.4.4 Normalize

逐通道标准化公式为:

$$ y_c=\frac{x_c-\mu_c}{\sigma_c}. $$

normalize = transforms.Normalize(
    mean=(0.5, 0.5, 0.5),
    std=(0.5, 0.5, 0.5),
)

normalized = normalize(image_tensor)
  • mean 和 std 的元素个数要与通道数一致。
  • 若输入原本在 $[0,1]$,使用均值和标准差均为 0.5 会把数据大致映射到 $[-1,1]$。

0.4.5 Resize

fixed = transforms.Resize((512, 512))
keep_ratio = transforms.Resize(512)
  • Resize((h, w)) 使用“高、宽”顺序,把输出固定为指定尺寸。
  • Resize(size) 传入单个整数时,通常把较短边缩放到该值,并保持纵横比。

0.5 torchvision 数据集与 CIFAR-10

torchvision.datasets 提供常见视觉数据集,CIFAR10 的重要参数包括:

参数 含义
root 数据集保存目录
train True 为训练集,False 为测试集
transform 对输入图像执行的变换
target_transform 对标签执行的变换
download 本地缺失时是否下载
import torchvision
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms

dataset_transform = transforms.Compose([
    transforms.ToTensor(),
])

train_set = torchvision.datasets.CIFAR10(
    root="./dataset",
    train=True,
    transform=dataset_transform,
    download=True,
)

test_set = torchvision.datasets.CIFAR10(
    root="./dataset",
    train=False,
    transform=dataset_transform,
    download=True,
)

writer = SummaryWriter("logs/cifar10")
for index in range(10):
    image, target = test_set[index]
    writer.add_image("test_set", image, index)
writer.close()
  • CIFAR-10 彩色图像的形状为 [3, 32, 32]。
  • 标签是 0 到 9 的十个类别编号。

0.6 用 nn.Module 搭建网络

所有神经网络模块的基类是 torch.nn.Module。自定义模型通常要:

  1. 在 __init__ 中定义有状态的层。
  2. 在 forward 中定义一次前向传播的数据流。
import torch
from torch import nn


class AddOne(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, x):
        return x + 1


model = AddOne()
x = torch.tensor(1.0)
print(model(x))
  • super().__init__() 会初始化父类,使子模块和参数能够被正确注册。
  • 应调用 model(x),不要直接调用 model.forward(x);前者还会执行钩子等 Module 机制。

0.7 二维卷积层

0.7.1 主要参数

nn.Conv2d(
    in_channels,
    out_channels,
    kernel_size,
    stride=1,
    padding=0,
    dilation=1,
    groups=1,
    bias=True,
)
参数 含义
in_channels 输入通道数
out_channels 输出通道数,也等于卷积核组数对应的输出特征图数
kernel_size 卷积核大小,可为整数或二元组
stride 卷积步幅
padding 输入边缘填充
dilation 卷积核元素间隔,即空洞率
groups 输入、输出通道的分组数;二者都必须能被 groups 整除,每个输出通道只连接 $C_{in}/groups$ 个输入通道
bias 是否为每个输出通道学习偏置

分组卷积的权重参数量为

$$ C_{out}\frac{C_{in}}{groups}K_hK_w, $$

若启用偏置再加 $C_{out}$。groups=1 是普通卷积;groups=C_{in} 且输出通道为输入通道的整数倍时是深度卷积的一般形式。

输入高为 $H_{in}$,卷积核高为 $K_h$,步幅为 $S_h$,填充为 $P_h$,空洞率为 $D_h$ 时:

$$ H_{out}

\left\lfloor \frac{H_{in}+2P_h-D_h(K_h-1)-1}{S_h} +1 \right\rfloor. $$

宽度同理。

0.7.2 通道与形状示例

源课件图示中:

  • 输入为单通道 $5\times5$ 图像。
  • 使用两个 $3\times3$ 卷积核,stride=1、padding=0。
  • 因而输出有两个通道,每个通道大小为 $3\times3$。

PyTorch 示例:

from torch import nn


class ConvNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 6, kernel_size=3, stride=1, padding=0)

    def forward(self, x):
        return self.conv(x)

若输入批次形状为 [N, 3, 32, 32],输出形状为 [N, 6, 30, 30]。为了把输出特征图写入 TensorBoard,可将通道按图像批次重新组织,但不能随意把 6 个通道强行解释成 RGB;应选择通道、做网格或使用合适的可视化方式。

0.8 池化层

池化在每个输入通道上独立执行,通常不含可学习参数,因此默认不会改变通道数。

0.8.1 常见类型

  • MaxPool1d/2d/3d:窗口内取最大值,保留最强响应。
  • AvgPool1d/2d/3d:窗口内取平均值。
  • MaxUnpool1d/2d/3d:利用最大池化时保存的索引做部分逆过程,不等价于完整恢复原输入。

池化可以降低空间尺寸和计算量,并让特征对小范围位置变化更稳定,但也会丢失精确位置信息。

0.8.2 MaxPool2d 参数

pool = nn.MaxPool2d(
    kernel_size=3,
    stride=2,
    padding=0,
    dilation=1,
    ceil_mode=False,
)
  • stride 未指定时默认等于 kernel_size。
  • dilation 控制池化窗口内采样点间距。
  • ceil_mode=False 时输出尺寸按下取整计算;设为 True 时允许按上取整方式覆盖边缘窗口。

0.9 非线性激活

若网络只堆叠线性变换,整体仍然等价于一次线性变换;激活函数为网络引入非线性表达能力。

0.9.1 ReLU

$$ \operatorname{ReLU}(x)=\max(0,x). $$

relu = nn.ReLU(inplace=False)
output = relu(input_tensor)
  • 负数被截断为 $0$,正数保持不变。
  • inplace=False 会创建新输出,原输入不被覆盖。
  • inplace=True 会原地修改输入,可能节省一部分内存,但也可能破坏后续计算或自动求导所需的值。

0.9.2 Sigmoid

$$ \sigma(x)=\frac{1}{1+e^{-x}}. $$

Sigmoid 把输入映射到 $(0,1)$。它常用于二分类输出或门控结构,但绝对值较大时梯度容易饱和。

0.10 其他常用网络层

0.10.1 归一化层

课件把这一部分写作“正则化层”,更准确的名称是归一化层。

  • BatchNorm1d/2d/3d:按批次统计量归一化,并学习缩放和平移参数。
  • GroupNorm:把通道分组后归一化,对小批次训练更稳定。
  • 归一化常能改善数值尺度、加速和稳定训练,但不等同于损失函数中的参数正则化。
batch_norm = nn.BatchNorm2d(100)
x = torch.randn(20, 100, 35, 45)
y = batch_norm(x)

0.10.2 循环层

  • nn.RNN:基础循环神经网络。
  • nn.LSTM:通过门控和记忆单元缓解长期依赖问题。
  • nn.GRU:参数比 LSTM 更简化的门控循环单元。
  • RNNCell/LSTMCell/GRUCell:单步单元,需要手动组织时间循环。

这些层可处理文本、语音和其他序列,不只用于文字识别。

0.10.3 Transformer

  • nn.Transformer:完整编码器-解码器结构。
  • nn.TransformerEncoder:多个编码器层的堆叠。
  • nn.TransformerDecoder:多个解码器层的堆叠。
  • 编码器层通常包含自注意力和前馈网络;解码器层还包含面向编码器输出的交叉注意力。

0.10.4 线性层

线性层执行:

$$ \boldsymbol{y}=\boldsymbol{x}W^{\mathrm T}+\boldsymbol{b}. $$

linear = nn.Linear(in_features=196608, out_features=10, bias=True)
flat = torch.flatten(images, start_dim=1)
logits = linear(flat)
  • in_features 必须等于每个样本展平后的特征数。
  • torch.flatten(images, start_dim=1) 保留批次维,避免把整个批次揉成一个向量。

0.10.5 Dropout

Dropout 在训练阶段按概率随机把部分元素置零,以降低特征之间的共适应并缓解过拟合。

  • nn.Dropout:按元素丢弃。
  • nn.Dropout2d:常对二维特征图按通道丢弃。
  • nn.Dropout3d:常对三维特征图按通道丢弃。

在 model.eval() 模式下,Dropout 会关闭随机丢弃;因此训练和测试必须正确切换模式。

0.11 Concat 与 Add

假设两个特征图分别为:

$$ X_1\in\mathbb R^{C_1\times H\times W}, \qquad X_2\in\mathbb R^{C_2\times H\times W}. $$

0.11.1 拼接

沿通道维拼接:

output = torch.cat((x1, x2), dim=1)

得到:

$$ \operatorname{shape}(output)=(C_1+C_2,H,W). $$

  • 除拼接维外,其余维度必须一致。
  • 在带批次的 [N, C, H, W] 张量中,通道维是 dim=1。

0.11.2 相加

output = x1 + x2
  • 逐元素相加通常要求形状相同;若形状不同,只能在符合广播规则且语义合理时相加。
  • 相加不增加通道数,是残差连接中的常见操作。

0.12 CIFAR-10 卷积网络示例

0.12.1 形状流

对 [N, 3, 32, 32] 输入,课件网络的形状变化为:

层 输出形状
Conv2d(3,32,5,padding=2) [N,32,32,32]
MaxPool2d(2) [N,32,16,16]
Conv2d(32,32,5,padding=2) [N,32,16,16]
MaxPool2d(2) [N,32,8,8]
Conv2d(32,64,5,padding=2) [N,64,8,8]
MaxPool2d(2) [N,64,4,4]
Flatten() [N,1024]
Linear(1024,64) [N,64]
Linear(64,10) [N,10]

最后的 [N,10] 是十个类别的原始分数,即 logits,不是已经归一化的概率。

0.12.2 正确实现

import torch
from torch import nn


class CifarNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.model = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=5, padding=2),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 32, kernel_size=5, padding=2),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, kernel_size=5, padding=2),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Flatten(),
            nn.Linear(64 * 4 * 4, 64),
            nn.ReLU(),
            nn.Linear(64, 10),
        )

    def forward(self, x):
        return self.model(x)


model = CifarNet()
inputs = torch.ones((64, 3, 32, 32))
outputs = model(inputs)
print(outputs.shape)  # torch.Size([64, 10])
  • 源课件的序列代码漏写多个逗号,并在 forward 中把 self.model1 误写成 self.model。
  • 课件结构图展示卷积和池化过程;实际网络通常在卷积和隐藏线性层后加入非线性激活,否则表达能力受限。

0.13 损失函数与反向传播

0.13.1 L1 损失

平均绝对误差:

$$ L_{L1}=\frac{1}{n}\sum_{i=1}^{n}|x_i-y_i|. $$

若

$$ X=(1,2,3),\qquad Y=(1,2,5), $$

则

$$ L_{L1}=\frac{0+0+2}{3}=\frac{2}{3}\approx0.667. $$

源课件写成 0.6,计算错误。

loss_fn = nn.L1Loss()
loss = loss_fn(inputs, targets)

0.13.2 均方误差

$$ L_{MSE}=\frac{1}{n}\sum_{i=1}^{n}(x_i-y_i)^2. $$

同一示例中:

$$ L_{MSE}=\frac{0+0+2^2}{3}=\frac{4}{3}\approx1.333. $$

正确类名是 nn.MSELoss,不是 MESLoss。

0.13.3 交叉熵损失

对单个样本,设模型输出 logits 为 $z_1,\ldots,z_C$,真实类别为 $y$,交叉熵为:

$$ L(z,y)

-\log\frac{e^{z_y}}{\sum_{j=1}^{C}e^{z_j}}

-z_y+\log\sum_{j=1}^{C}e^{z_j}. $$

loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(logits, targets)
  • CrossEntropyLoss 直接接收未经过 Softmax 的 logits。
  • 多分类时,logits 常为 [N,C],整型 targets 常为 [N],每个值在 $[0,C-1]$。
  • 不应先手动做 Softmax 再传入 CrossEntropyLoss。

0.14 优化器与训练顺序

常用优化器:

optimizer_sgd = torch.optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,
)

optimizer_adam = torch.optim.Adam(
    model.parameters(),
    lr=1e-4,
)

一次标准参数更新顺序:

optimizer.zero_grad()
logits = model(images)
loss = loss_fn(logits, targets)
loss.backward()
optimizer.step()
  1. zero_grad() 清空上一批累积的梯度。
  2. 前向传播计算 logits 和损失。
  3. backward() 根据计算图求梯度,并把梯度累积到参数的 .grad。
  4. step() 使用当前梯度更新参数。

若有意做梯度累积,可以不在每个小批次都清零;普通训练则应按上述顺序处理。

0.15 完整训练与测试循环

下面把课件中分散且缩进错误的代码整理为完整流程:

import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms


train_data = torchvision.datasets.CIFAR10(
    root="../data",
    train=True,
    transform=transforms.ToTensor(),
    download=True,
)
test_data = torchvision.datasets.CIFAR10(
    root="../data",
    train=False,
    transform=transforms.ToTensor(),
    download=True,
)

print(f"训练数据集长度:{len(train_data)}")
print(f"测试数据集长度:{len(test_data)}")

train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
test_loader = DataLoader(test_data, batch_size=64, shuffle=False)

model = CifarNet()
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
writer = SummaryWriter("../logs_train")

total_train_step = 0
total_test_step = 0
epochs = 10

for epoch in range(epochs):
    print(f"第 {epoch + 1} 轮训练开始")
    model.train()

    for images, targets in train_loader:
        logits = model(images)
        loss = loss_fn(logits, targets)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_train_step += 1
        if total_train_step % 100 == 0:
            print(f"训练次数:{total_train_step},Loss:{loss.item():.6f}")
            writer.add_scalar("train_loss", loss.item(), total_train_step)

    model.eval()
    total_test_loss = 0.0
    total_correct = 0

    with torch.no_grad():
        for images, targets in test_loader:
            logits = model(images)
            loss = loss_fn(logits, targets)
            total_test_loss += loss.item() * images.size(0)
            total_correct += (logits.argmax(dim=1) == targets).sum().item()

    total_test_step += 1
    accuracy = total_correct / len(test_data)
    mean_test_loss = total_test_loss / len(test_data)
    print(f"测试集平均 Loss:{mean_test_loss:.6f}")
    print(f"测试集准确率:{accuracy:.4%}")
    writer.add_scalar("test_loss", mean_test_loss, total_test_step)
    writer.add_scalar("test_accuracy", accuracy, total_test_step)

    torch.save(model.state_dict(), f"cifar_net_{epoch + 1}.pth")
    print("模型已保存")

writer.close()

这里假设 loss_fn 使用默认的 reduction="mean"。loss.item() 是当前批次的平均损失,必须先乘批大小再累加,最后除以测试样本数;直接把各批均值相加会让最后一个小批次与完整批次权重相同,既不是全样本总损失,也不是规范的样本平均。

0.15.1 训练与测试阶段区别

训练 测试/验证
model.train() model.eval()
需要反向传播 使用 torch.no_grad()
Dropout 随机丢弃 Dropout 关闭
BatchNorm 更新统计量 BatchNorm 使用已保存统计量
执行 optimizer.step() 不更新参数

0.16 模型保存与加载

0.16.1 保存整个模型

torch.save(model, "model_full.pth")
loaded_model = torch.load("model_full.pth", weights_only=False)
  • 同时保存结构和参数,使用简单。
  • 加载依赖原类定义和运行环境,跨项目和跨版本可移植性较差。
  • 只加载可信来源的完整模型文件,反序列化不可信文件有安全风险。

0.16.2 保存参数字典

官方更常推荐保存 state_dict:

torch.save(model.state_dict(), "model_weights.pth")

loaded_model = CifarNet()
state_dict = torch.load("model_weights.pth", weights_only=True)
loaded_model.load_state_dict(state_dict)
loaded_model.eval()
  • 必须先创建相同结构的模型,再加载参数。
  • load_state_dict 接收参数字典,不能直接把文件名传给它。

使用 torchvision 模型时,当前接口可写为:

from torchvision.models import vgg16

model = vgg16(weights=None)
torch.save(model.state_dict(), "vgg16_weights.pth")

restored = vgg16(weights=None)
restored.load_state_dict(
    torch.load("vgg16_weights.pth", weights_only=True)
)

0.17 数据增强

数据增强通过对已有样本做保持语义的随机变换,增加训练数据多样性,从而改善模型泛化。

0.17.1 常见方式

  • 翻转:水平翻转、在语义允许时垂直翻转。
  • 裁剪:随机位置、随机面积和随机纵横比,再缩放到固定尺寸。
  • 颜色:随机调整亮度、对比度、饱和度和色调。
  • 其他模态:例如语音加入背景噪声。

增强通常在线随机生成,不必把所有增强结果预先保存到磁盘。

0.17.2 图像增强示例

from torchvision import transforms

train_transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomResizedCrop(
        size=(200, 200),
        scale=(0.1, 1.0),
        ratio=(0.5, 2.0),
    ),
    transforms.ColorJitter(
        brightness=0.5,
        contrast=0.5,
        saturation=0.5,
        hue=0.2,
    ),
    transforms.ToTensor(),
])

test_transform = transforms.Compose([
    transforms.Resize((200, 200)),
    transforms.ToTensor(),
])
  • scale 表示裁剪区域占原图面积的比例范围。
  • ratio 表示裁剪区域的宽高比范围。
  • 增强必须保持标签语义。例如建筑、文字或有明确上下方向的图像通常不能随意垂直翻转。
  • 随机增强只用于训练集;测试集应使用确定性预处理,保证评估可重复。

0.18 迁移学习与微调

0.18.1 网络的两部分

分类网络可粗略分为:

  1. 特征提取器:把原始像素映射到更容易线性分类的语义特征空间。
  2. 分类头:根据高层特征输出目标类别分数。

源数据集上学到的低层边缘、纹理等特征通常更通用;越靠近输出的高层特征越依赖原任务和标签。因此迁移到新数据集时,原分类头往往不能直接使用,而特征提取部分可能仍然有效。

0.18.2 微调流程

  1. 在大型源数据集上得到预训练模型。
  2. 用预训练权重初始化目标模型,而不是随机初始化全部参数。
  3. 把最后的输出层替换为适合目标类别数的新层。
  4. 在目标数据集上训练,可先冻结底部部分层,再逐步解冻。
  5. 通常使用更小学习率、更少训练轮次和更强正则化。

示例:

from torch import nn
from torchvision.models import ResNet18_Weights, resnet18

model = resnet18(weights=ResNet18_Weights.DEFAULT)

for parameter in model.parameters():
    parameter.requires_grad = False

in_features = model.fc.in_features
model.fc = nn.Linear(in_features, 2)

optimizer = torch.optim.SGD(model.fc.parameters(), lr=1e-3)

若目标数据足够多或与源数据差异较大,可进一步解冻后部网络层,并给预训练层设置比新分类头更小的学习率。

0.18.3 效果取决于什么

  • 预训练模型质量。
  • 源数据与目标数据的相似程度。
  • 目标数据规模。
  • 冻结范围、学习率和训练轮数。

当源数据集更大、更丰富且与目标任务相关时,微调通常比完全随机初始化收敛更快、精度更高;但这不是对所有任务的无条件保证。

0.19 易错点

  • PyTorch 类名区分大小写:DataLoader、Dataset、MSELoss、Conv2d、MaxPool2d。
  • DataLoader 参数是 num_workers;默认 shuffle=False,不是默认随机抓取。
  • Resize((h,w)) 的二元组顺序是高、宽。
  • Tensor 是通用多维数组,只有 requires_grad=True 等条件满足时才追踪梯度。
  • ToTensor 对常见 8 位图像还会把数值从 $[0,255]$ 缩放到 $[0,1]$。
  • BatchNorm 是归一化层,不应简单等同于参数正则化。
  • torch.flatten(images, start_dim=1) 要保留批次维。
  • [64,10] 表示 64 个样本各有 10 个类别分数,不是 10 个概率;需要概率时再使用 Softmax。
  • L1 示例的正确结果是 $2/3\approx0.667$,不是 $0.6$。
  • CrossEntropyLoss 直接接收 logits,不要提前做 Softmax。
  • 梯度默认累积,普通训练每次更新前要调用 optimizer.zero_grad()。
  • 测试阶段要同时使用 model.eval() 和 torch.no_grad();二者作用不同,不能互相替代。
  • load_state_dict 的参数是已加载的状态字典,不是文件路径字符串。
  • 数据增强要保持类别语义,测试集不应使用随机增强。
  • 迁移学习通常要替换最后分类层;是否冻结底层应根据数据量和任务相似度决定。

相关知识

0.20 本节复盘

  • 虚拟环境用于隔离项目依赖;安装后应核对 PyTorch 版本和 CUDA 可用性。
  • dir 查看对象成员,help 查看接口说明。
  • Dataset 定义样本及索引规则,DataLoader 决定批处理、打乱和并行加载方式。
  • Tensor 是 PyTorch 的多维数组;transforms 按输入、输出和作用组合图像预处理。
  • nn.Module 通过 __init__ 注册层,通过 forward 定义前向计算。
  • 卷积提取局部特征,池化压缩空间尺度,激活函数提供非线性;归一化、循环、Transformer、线性和 Dropout 层各有不同作用。
  • Concat 增加指定维度大小,Add 做逐元素融合;二者对形状的要求不同。
  • CIFAR-10 示例从 [N,3,32,32] 逐步变为 [N,10] logits,最后维度对应类别数。
  • L1、MSE 和交叉熵适用于不同任务;交叉熵直接接收 logits。
  • 标准更新顺序是清梯度、前向、计算损失、反向传播、参数更新。
  • 训练与测试必须切换模型模式;保存参数字典通常比保存完整对象更便于迁移。
  • 数据增强通过保持语义的随机变换提高多样性;迁移学习利用预训练特征,再针对目标任务替换和微调分类头。