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() 会:
- 把维度从
[H, W, C]转为[C, H, W]。 - 把整数像素值从 $[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。自定义模型通常要:
- 在
__init__中定义有状态的层。 - 在
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()
zero_grad()清空上一批累积的梯度。- 前向传播计算 logits 和损失。
backward()根据计算图求梯度,并把梯度累积到参数的.grad。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 网络的两部分
分类网络可粗略分为:
- 特征提取器:把原始像素映射到更容易线性分类的语义特征空间。
- 分类头:根据高层特征输出目标类别分数。
源数据集上学到的低层边缘、纹理等特征通常更通用;越靠近输出的高层特征越依赖原任务和标签。因此迁移到新数据集时,原分类头往往不能直接使用,而特征提取部分可能仍然有效。
0.18.2 微调流程
- 在大型源数据集上得到预训练模型。
- 用预训练权重初始化目标模型,而不是随机初始化全部参数。
- 把最后的输出层替换为适合目标类别数的新层。
- 在目标数据集上训练,可先冻结底部部分层,再逐步解冻。
- 通常使用更小学习率、更少训练轮次和更强正则化。
示例:
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。
- 标准更新顺序是清梯度、前向、计算损失、反向传播、参数更新。
- 训练与测试必须切换模型模式;保存参数字典通常比保存完整对象更便于迁移。
- 数据增强通过保持语义的随机变换提高多样性;迁移学习利用预训练特征,再针对目标任务替换和微调分类头。