训练与优化
损失函数与反向传播
损失函数能够衡量 神经网络输出与目标值之间的误差 ,同时为反向传播提供依据,计算梯度来优化网络中的参数。
torch.nn.L1Loss
计算所有预测值与真实值之间的绝对差。参数为 reduction
:
'none'
:不对损失进行任何求和或平均,返回每个元素的损失。'mean'
:对损失进行平均,默认选项。'sum'
:对所有样本的损失进行求和。
python
import torch
input = torch.tensor([1, 2, 3], dtype=torch.float32)
target = torch.tensor([1, 3, 5], dtype=torch.float32)
loss = torch.nn.L1Loss(reduction="none")
res = loss(input, target)
print(res)
# tensor([0., 1., 2.])
loss = torch.nn.L1Loss(reduction="mean")
res = loss(input, target)
print(res)
# tensor(1.)
loss = torch.nn.L1Loss(reduction="sum")
res = loss(input, target)
print(res)
# tensor(3.)
torch.nn.MSELoss
计算每个样本的预测值与真实值之间的差距的平方,参数为 reduction
。
python
import torch
input = torch.tensor([1, 2, 3], dtype=torch.float32)
target = torch.tensor([1, 3, 5], dtype=torch.float32)
loss = torch.nn.MSELoss(reduction="none")
res = loss(input, target)
print(res)
# tensor([0., 1., 4.])
loss = torch.nn.MSELoss(reduction="mean")
res = loss(input, target)
print(res)
# tensor(1.6667)
loss = torch.nn.MSELoss(reduction="sum")
res = loss(input, target)
print(res)
# tensor(5.)
torch.nn.CrossEntropyLoss
计算实际类别分布 与预测类别分布 之间的差异。输入 input
为预测的类别得分(不是概率),维度为 (N,C)
,其中 N 是样本数量,C 是类别数量,每个样本是一个未经过softmax 的类别得分 。真实标签索引 target
维度为 (N)
,每个标签是一个整数,表示该样本的真实类别索引。
CrossEntropyLoss
会自动计算 input 的 softmax ,然后根据交叉熵公式计算每个样本的损失。
python
import torch
from torch import nn
# 2个样本,3个类别的得分
input = torch.tensor([[1, 2, 3], [1, 2, 3]], dtype=torch.float32)
# 真实标签:第1个样本属于类别2,第2个样本属于类别1
target = torch.tensor([2, 1])
loss = nn.CrossEntropyLoss()
res = loss(input, target)
print(res)
# tensor(0.9076)
如果数据集中的类别不平衡,可以通过 weight
参数对每个类别的损失进行加权。这样可以让模型在训练时更加关注某些类别。
python
import torch
from torch import nn
# 2个样本,3个类别的得分
input = torch.tensor([[1, 2, 3], [1, 2, 3]], dtype=torch.float32)
# 真实标签:第1个样本属于类别2,第2个样本属于类别1
target = torch.tensor([2, 1])
# 类别0权重为1,类别1权重为2,类别2权重为0.5
weight = torch.tensor([1.0, 2.0, 0.5])
loss = nn.CrossEntropyLoss(weight)
res = loss(input, target)
print(res)
# tensor(1.2076)
当计算出损失函数后,便可计算出每一个节点参数的梯度 ,从而进行反向传播,只需要加上一行:
python
result_loss.backward()
训练与推理
在 PyTorch 中,神经网络的 train()
和 eval()
模式控制着 Batch Normalization 和 Dropout 这两类层的行为,确保模型在训练和推理(测试)时的表现一致。
model.train()
负责启动 BN 和 Dropout 层的训练模式。BatchNorm 会计算当前批次的均值和方差,用于归一化数据,这些均值和方差会随着训练逐步更新。Dropout 会随机丢弃一部分神经元,以减少过拟合。
model.eval()
负责关闭训练模式,进入推理模式,确保计算的均值、方差、Dropout 影响不会波动,保证结果稳定。计算归一化时,会使用训练期间学到的全局均值和方差 ,而不是当前批次的统计量。也不再随机丢弃神经元,而是使用完整的网络进行预测。
在训练的时候,还需要关闭梯度计算 ,减少内存占用,加速推理。因为推理时不需要计算梯度,不需要 backward()
进行反向传播。
python
with torch.no_grad():
output = model(input)
在 train()
模式下,PyTorch 默认存储计算图 ,以支持 backward()
计算梯度torch.no_grad()
关闭计算图,避免存储不必要的梯度信息,减少显存占用。
训练模式
python
model.train() # 训练模式(启用 BatchNorm 统计 和 Dropout)
for data in dataloader:
optimizer.zero_grad()
output = model(data)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
推理模式
python
model.eval() # 进入推理模式
with torch.no_grad(): # 关闭梯度计算
output = model(input)
优化器
优化器利用通过反向传播计算得到的梯度来更新模型参数,从而减小损失函数值,提升模型的性能。
在每次训练过程中,首先使用 optimizer.zero_grad()
清零上一步的梯度,然后通过 loss.backward()
执行反向传播,计算当前模型参数的梯度,最后使用 optimizer.step()
根据梯度更新模型参数。
**SGD(随机梯度下降)**是基本的梯度下降法,每次更新一个小批量的数据(mini-batch)参数,需要调整学习率(lr)和可能的动量(momentum)等超参数。
Adam、Adagrad、Adadelta、RMSProp 是不同的优化算法,每种算法有不同的超参数调整方法,Adam
自适应调整学习率,Adagrad
适用于稀疏数据,Adadelta
主要针对自适应学习率的调整。
学习速率不能太大(太大模型训练不稳定)也不能太小(太小模型训练慢),一般建议先采用较大学习速率,后采用较小学习速率。
优化器构造方法:
python
# SGD(Stochastic Gradient Descent) 随机梯度下降
# 模型参数、学习速率、动量
**optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)**
优化器调用方法:
python
for input, target in dataset:
optimizer.zero_grad() # 清空梯度
output = model(input)
res= loss(output, target) # 计算损失函数
res.backward() # 反向传播计算梯度
optimizer.step() # 根据梯度优化参数
以 CIFAR-10 数据集为例:
python
import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
# 加载数据集
dataset = torchvision.datasets.CIFAR10(root="Dataset", train=False,
transform=torchvision.transforms.ToTensor(), download=False)
# 批量加载数据
dataloader = DataLoader(dataset, batch_size=64, shuffle=True)
writer = SummaryWriter("logs")
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
self.model1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(kernel_size=2),
nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(kernel_size=2),
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(kernel_size=2),
nn.Flatten(),
nn.Linear(in_features=1024, out_features=64),
nn.Linear(in_features=64, out_features=10)
)
def forward(self, x):
x = self.model1(x)
return x
model = Model()
# 定义损失函数
loss = torch.nn.CrossEntropyLoss()
# 定义优化器
**optimizer = torch.optim.SGD(model.parameters(), lr=0.01)**
# 训练 20 个 epoch
for epoch in range(20):
totalloss = 0.0
for data in dataloader:
optimizer.zero_grad() # 清空梯度
imgs, targets = data
outputs = model(imgs)
lossres = loss(outputs, targets) # 计算损失
totalloss = totalloss + lossres # 累加损失
lossres.backward() # 反向传播计算梯度
optimizer.step() # 更新模型参数
print("Epoch{} : {}".format(epoch, totalloss))
# 写入 TensorBoard
writer.add_scalar("train_loss", totalloss, epoch)
writer.close()
如果模型在训练时过早出现 nan
或损失不收敛,可以尝试调整学习率,使用更小的学习率或更高级的优化器(如 Adam)。
预训练模型
PyTorch 主要提供搭建神经网络的核心工具,TorchVision
提供了一系列预训练模型、标准数据集(如 ImageNet、CIFAR-10 等)和图像变换工具(transforms)。预训练模型(如 VGG16)在 ImageNet 数据集上已经训练好,可以直接使用或者在此基础上微调。
VGG16 是一种经典的卷积神经网络,主要用于图像分类任务。VGG16 由多层卷积层、池化层和全连接层组成,features 部分用于提取图像特征,classifier 部分用于分类,最终输出1000个类别。
python
torchvision.models.vgg16(weights, progress)
progess
代表是否显示下载进度条,默认 True
,表示在下载权重时显示进度条。
weights
是预训练权重,默认为 None
不加载预训练模型。权重 VGG16_Weights.IMAGENET1K_V1
适用于分类任务 ,基于 ImageNet 训练,包含完整的分类器(classifier
层),VGG16_Weights.DEFAULT
等同于 VGG16_Weights.IMAGENET1K_V1
。
python
import torchvision
# 无预训练权重(随机初始化参数)
vgg16_false = torchvision.models.vgg16(weights=None)
# 使用 ImageNet 预训练参数
vgg16_true = torchvision.models.vgg16(
weights=torchvision.models.VGG16_Weights.IMAGENET1K_V1)
# 默认使用 ImageNet 预训练权重
vgg16_default = torchvision.models.vgg16(
weights=torchvision.models.VGG16_Weights.DEFAULT)
但是 VGG16 对于图像输入有严格要求,输入维度必须是 224 × 224 224 \times 224 224×224 。
python
# 图像预处理(按 VGG16 需要的格式)
transform = transforms.Compose([
transforms.Resize(256), # 先缩放到 256
transforms.CenterCrop(224), # 再中心裁剪到 224
transforms.ToTensor(),
# 归一化
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
权重 VGG16_Weights.IMAGENET1K_FEATURES
用于特征提取 ,不包含 classifier
部分权重,只能提取特征,不能进行分类(只是不包含预训练的分类器权重,并没有移除分类器层 )。适用于迁移学习 ,可以用 features
层进行特征提取。
python
import torchvision
vgg16_feature = torchvision.models.vgg16(
weights=torchvision.models.VGG16_Weights.IMAGENET1K_FEATURES)
VGG Model Structure
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
......
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
分类任务
python
import torch
from PIL import Image
from torchvision import models, transforms
# 加载 VGG16 预训练模型
model = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1)
# 定义图像预处理步骤
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
img_path = r"Dataset/airplane.png"
img = Image.open(img_path)
input = transform(img)
# 添加 batch 维度
input = torch.reshape(input, (1, 3, 224, 224))
# 进入推理模式
model.eval()
# 前向传播
with torch.no_grad():
output = model(input)
# 获取预测类别索引
predicted_class = torch.argmax(output)
# 获取 ImageNet 1000 类的类别名称
classes = models.VGG16_Weights.IMAGENET1K_V1.meta["categories"]
print(classes[predicted_class])
迁移学习微调模型
如果要迁移到 CIFAR-10 的分类任务,需要修改最后一层:
python
from torch import nn
from torchvision import models
# 加载 VGG16 预训练模型
model = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1)
# 修改 classifier 部分(改为 10 类)
**model.classifier[6]** = nn.Linear(in_features=4096, out_features=10)
或者添加新层:
python
model.classifier.add_module("7", nn.Linear(in_features=1000, out_features=10))
如果只训练最后一层 ,可以冻结前面的参数:
python
for param in model.features.parameters():
param.requires_grad = False # 冻结 features 部分(不更新)
这样可以 保留原有的卷积特征,仅微调分类层,提高训练效率。