← 返回学习笔记

Lecture 0:PyTorch 总结

408 学习笔记:Lecture 0:PyTorch 总结。保留原有知识内容、公式与图示;有对应原件时提供查看和下载入口。

本页目录

描述:本节系统整理 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。
  • 标准更新顺序是清梯度、前向、计算损失、反向传播、参数更新。
  • 训练与测试必须切换模型模式;保存参数字典通常比保存完整对象更便于迁移。
  • 数据增强通过保持语义的随机变换提高多样性;迁移学习利用预训练特征,再针对目标任务替换和微调分类头。