1. PyTorch神经网络构建完全指南:从零到一的深度学习实战
深度学习已经成为现代人工智能的核心技术,而PyTorch作为当前最受欢迎的深度学习框架之一,以其动态计算图和直观的API设计赢得了广大研究者和开发者的青睐。本文将带你从零开始,系统掌握使用PyTorch构建神经网络的完整知识体系。
作为一名长期使用PyTorch进行研究和开发的从业者,我深知学习深度学习框架的痛点所在。很多人一开始就被各种概念和代码细节所困扰,无法建立起完整的知识体系。本文将从最基础的张量操作讲起,逐步深入到复杂的网络架构设计,不仅告诉你"怎么做",更会解释"为什么这样做"。
无论你是刚接触深度学习的新手,还是有一定基础希望系统提升PyTorch技能的开发者,这篇文章都将为你提供实用的指导和深入的见解。我们将通过大量精心设计的代码示例和实战项目,帮助你建立起对PyTorch神经网络的完整认知。
2. 神经网络基础:从张量到模块的完整认知
2.1 PyTorch张量:深度学习的基石
2.1.1 张量的本质与特性
在PyTorch中,张量(Tensor)是最基本的数据结构,可以理解为多维数组的扩展。但与普通的数组不同,PyTorch张量具有以下三个核心特性:
-
维度(Dimension):决定数据的组织结构。例如:
- 0维张量:标量
- 1维张量:向量
- 2维张量:矩阵
- 更高维张量:图像数据(通常是3维或4维)
-
数据类型(dtype):决定数值的精度和类型。常见的有:
- torch.float32: 单精度浮点数
- torch.float64: 双精度浮点数
- torch.int32: 32位整数
- torch.bool: 布尔类型
-
设备(device):决定计算位置,可以是CPU或GPU。这是PyTorch能够利用GPU加速计算的关键。
python复制import torch
# 创建不同特性的张量示例
scalar = torch.tensor(3.14) # 0维,float32类型,默认设备(CPU)
vector = torch.tensor([1, 2, 3], dtype=torch.float64) # 1维,float64类型
matrix = torch.randn(3, 3, device='cuda') # 2维,随机值,GPU设备
2.1.2 张量的高效操作技巧
在实际应用中,如何高效地操作张量是提升代码性能的关键。以下是一些实用技巧:
- 向量化操作:尽量避免Python循环,使用PyTorch内置的向量化操作
python复制# 不推荐:使用Python循环
result = torch.zeros(1000)
for i in range(1000):
result[i] = a[i] + b[i]
# 推荐:向量化操作
result = a + b # 快几个数量级
- 广播机制:理解并合理利用广播规则可以减少内存占用
python复制# 标量与张量相加会自动广播
a = torch.ones(3, 3)
b = 1.0
c = a + b # b会被广播成与a相同形状
# 形状兼容的张量也可以广播
x = torch.ones(5, 3, 4)
y = torch.ones(3, 1)
z = x + y # y会被广播为(1,3,1)然后(5,3,4)
- 内存共享与复制:理解视图(view)和复制(clone)的区别
python复制a = torch.randn(3, 3)
b = a.view(9) # 视图,共享内存
c = a.clone() # 完全复制,不共享内存
a[0,0] = 10
print(b[0]) # 输出10,因为共享内存
print(c[0,0]) # 不变,因为是独立副本
注意:在使用view()时,必须确保张量在内存中是连续的(contiguous)。如果不确定,可以先调用contiguous()方法。
2.1.3 张量与NumPy的互操作
PyTorch与NumPy可以方便地相互转换,这使得我们可以利用NumPy丰富的生态系统:
python复制import numpy as np
# NumPy数组转PyTorch张量
np_array = np.random.rand(3, 3)
torch_tensor = torch.from_numpy(np_array)
# PyTorch张量转NumPy数组
torch_tensor = torch.randn(3, 3)
np_array = torch_tensor.numpy()
# 注意:GPU张量需要先移动到CPU才能转换为NumPy
gpu_tensor = torch.randn(3, 3, device='cuda')
cpu_tensor = gpu_tensor.cpu()
np_array = cpu_tensor.numpy()
2.2 nn.Module:神经网络的基础构建块
2.2.1 Module的核心设计理念
nn.Module是PyTorch中所有神经网络模块的基类,它体现了PyTorch的几个重要设计哲学:
- 封装性:将相关参数和计算逻辑封装在模块内部,对外暴露清晰的接口
- 层次性:支持模块的嵌套,可以构建复杂的网络结构
- 状态管理:自动管理参数的保存、加载和设备转移
- 计算图构建:通过forward()方法动态构建计算图
2.2.2 基础Module实现示例
让我们从一个最简单的全连接网络开始:
python复制import torch.nn as nn
import torch.nn.functional as F
class BasicNeuralNetwork(nn.Module):
"""基础神经网络示例"""
def __init__(self, input_size, hidden_size, output_size):
super().__init__() # 必须调用父类初始化
# 定义网络层
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, hidden_size)
self.fc3 = nn.Linear(hidden_size, output_size)
self.relu = nn.ReLU()
def forward(self, x):
# 定义前向传播逻辑
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
x = self.relu(x)
x = self.fc3(x)
return x
在这个示例中:
__init__方法定义了网络的所有层和组件forward方法定义了数据如何通过这些层流动nn.Linear是全连接层,执行线性变换:y = xW^T + bnn.ReLU是激活函数,引入非线性:ReLU(x) = max(0, x)
2.2.3 参数管理与初始化
正确的参数初始化对神经网络的训练至关重要。不同的激活函数需要配合不同的初始化策略:
python复制def initialize_weights(model):
"""权重初始化函数"""
for m in model.modules():
if isinstance(m, nn.Linear):
# 对于ReLU激活函数,推荐使用He初始化
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
# 偏置初始化为0
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Conv2d):
# 卷积层同样使用He初始化
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
# BatchNorm层权重初始化为1,偏置为0
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
# 应用初始化
model = BasicNeuralNetwork(784, 256, 10)
initialize_weights(model)
2.2.4 模型保存与加载
在实际项目中,我们需要保存训练好的模型以便后续使用或继续训练:
python复制# 保存整个模型(包括结构和参数)
torch.save(model, 'model.pth')
# 加载整个模型
loaded_model = torch.load('model.pth')
# 仅保存模型参数(推荐方式,更灵活)
torch.save(model.state_dict(), 'params.pth')
# 加载模型参数(需要先创建相同结构的模型)
new_model = BasicNeuralNetwork(784, 256, 10)
new_model.load_state_dict(torch.load('params.pth'))
2.2.5 实战:MNIST分类器
让我们构建一个实际的MNIST手写数字分类器:
python复制class MNISTClassifier(nn.Module):
def __init__(self):
super().__init__()
# 特征提取部分
self.features = nn.Sequential(
nn.Linear(784, 512),
nn.ReLU(),
nn.Dropout(0.3), # Dropout防止过拟合
nn.Linear(512, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 128),
nn.ReLU(),
)
# 分类器部分
self.classifier = nn.Linear(128, 10)
def forward(self, x):
x = x.view(x.size(0), -1) # 将图像展平
x = self.features(x)
x = self.classifier(x)
return x
这个分类器有几个关键设计点:
- 使用Sequential组织简单的层序列
- 添加Dropout层防止过拟合
- 将网络分为特征提取和分类器两部分,结构更清晰
- 在forward中处理输入形状转换
3. 构建神经网络:从全连接到卷积
3.1 全连接网络(MLP)的深入解析
3.1.1 MLP的结构特点
多层感知机(MLP)是最基础的前馈神经网络,由输入层、多个隐藏层和输出层组成。虽然结构简单,但MLP是理解神经网络工作原理的最佳起点。
MLP的核心特点包括:
- 全连接:每个神经元与下一层的所有神经元连接
- 前馈结构:信息单向流动,无循环连接
- 非线性变换:通过激活函数引入非线性
3.1.2 动态构建MLP的实现
我们可以设计一个更灵活的MLP实现,允许动态指定隐藏层大小:
python复制class DynamicMLP(nn.Module):
def __init__(self, input_dim, hidden_dims, output_dim=1):
super().__init__()
layers = []
prev_dim = input_dim
# 动态添加隐藏层
for i, hidden_dim in enumerate(hidden_dims):
layers.append(nn.Linear(prev_dim, hidden_dim))
layers.append(nn.ReLU())
# 除了最后一层外都添加Dropout
if i < len(hidden_dims) - 1:
layers.append(nn.Dropout(0.2))
prev_dim = hidden_dim
# 输出层
layers.append(nn.Linear(prev_dim, output_dim))
self.network = nn.Sequential(*layers)
def forward(self, x):
if x.dim() > 2: # 如果输入是多维数据(如图像)
x = x.view(x.size(0), -1) # 展平
return self.network(x)
这个实现允许我们灵活地创建不同深度的MLP:
python复制# 创建一个5层MLP
mlp = DynamicMLP(input_dim=784, hidden_dims=[512, 256, 128, 64], output_dim=10)
3.1.3 解决梯度问题的技巧
深度MLP容易遇到梯度消失或梯度爆炸问题,以下是几种解决方案:
- 批归一化(BatchNorm):稳定训练,加速收敛
python复制class MLPWithBN(nn.Module):
def __init__(self, input_dim, hidden_dims):
super().__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.extend([
nn.Linear(prev_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(0.2)
])
prev_dim = hidden_dim
self.network = nn.Sequential(*layers)
- 残差连接(Residual Connection):缓解梯度消失
python复制class ResidualBlock(nn.Module):
def __init__(self, dim):
super().__init__()
self.linear = nn.Linear(dim, dim)
self.bn = nn.BatchNorm1d(dim)
def forward(self, x):
identity = x
out = self.linear(x)
out = self.bn(out)
out = F.relu(out)
out = out + identity # 残差连接
return out
- 合适的初始化策略:
- ReLU激活函数:使用He初始化(
kaiming_normal_) - Tanh/Sigmoid激活函数:使用Xavier初始化(
xavier_normal_)
3.1.4 MLP的典型应用场景
虽然MLP看起来简单,但在许多场景下仍然非常有效:
- 结构化数据分类/回归:如房价预测、客户流失预测等
- 简单图像分类:如MNIST等简单数据集
- 神经网络的最后分类/回归层:通常作为CNN/RNN等网络的输出层
3.2 卷积神经网络(CNN)的构建与实践
3.2.1 CNN的核心思想
卷积神经网络是处理图像、视频等网格数据的标准架构,其三大核心思想是:
- 局部连接:每个神经元只连接输入的一小部分区域(感受野)
- 权重共享:相同卷积核在整个输入上滑动,大大减少参数量
- 池化操作:降低空间维度,增加平移不变性
3.2.2 基础CNN实现
让我们实现一个经典的CNN结构:
python复制class BasicCNN(nn.Module):
def __init__(self, in_channels=3, num_classes=10):
super().__init__()
# 特征提取部分
self.features = nn.Sequential(
nn.Conv2d(in_channels, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
)
# 分类器部分
self.classifier = nn.Sequential(
nn.Linear(128 * 8 * 8, 256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256, num_classes)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1) # 展平
x = self.classifier(x)
return x
这个CNN包含几个关键设计:
- 使用3x3卷积核,保持空间分辨率(padding=1)
- 每两个卷积层后接一个最大池化层,逐步降低分辨率
- 使用BatchNorm加速收敛并稳定训练
- 最后使用全连接层进行分类
3.2.3 现代CNN架构:残差网络
残差网络(ResNet)通过引入跳跃连接解决了深层网络训练困难的问题:
python复制class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
# 主路径
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1)
self.bn2 = nn.BatchNorm2d(out_channels)
# 快捷连接
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 1, stride),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = self.shortcut(x)
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += identity # 残差连接
out = F.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=10):
super().__init__()
self.in_channels = 64
self.conv1 = nn.Conv2d(3, 64, 7, 2, 3)
self.bn1 = nn.BatchNorm2d(64)
self.maxpool = nn.MaxPool2d(3, 2, 1)
self.layer1 = self._make_layer(block, 64, layers[0], stride=1)
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, num_classes)
def _make_layer(self, block, out_channels, blocks, stride=1):
layers = []
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels
for _ in range(1, blocks):
layers.append(block(out_channels, out_channels))
return nn.Sequential(*layers)
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
使用示例:
python复制def resnet18(num_classes=10):
return ResNet(ResidualBlock, [2, 2, 2, 2], num_classes)
model = resnet18()
3.2.4 CNN可视化与理解
理解CNN内部工作机制的一个好方法是可视化其学习到的特征:
python复制def visualize_feature_maps(model, image):
# 注册hook来获取中间层输出
features = []
def hook(module, input, output):
features.append(output.detach())
# 选择要可视化的层
target_layer = model.features[0] # 第一个卷积层
handle = target_layer.register_forward_hook(hook)
# 前向传播
model.eval()
with torch.no_grad():
_ = model(image.unsqueeze(0))
# 移除hook
handle.remove()
# 可视化特征图
feature_maps = features[0][0] # 取第一个样本的特征图
plt.figure(figsize=(12, 6))
for i in range(min(16, feature_maps.size(0))): # 最多显示16个特征图
plt.subplot(4, 4, i+1)
plt.imshow(feature_maps[i].cpu(), cmap='viridis')
plt.axis('off')
plt.show()
这种方法可以帮助我们理解CNN每一层学习到了什么样的特征,从底层的边缘、纹理到高层的语义特征。
3.3 循环神经网络(RNN)与序列建模
3.3.1 RNN的基本原理
循环神经网络是处理序列数据的标准架构,特别适合文本、时间序列、语音等任务。RNN的核心思想是:
- 时间展开:沿时间步展开网络
- 隐藏状态:携带历史信息
- 参数共享:不同时间步共享参数
3.3.2 基础RNN实现
python复制class BasicRNN(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.rnn = nn.RNN(embed_dim, hidden_dim, batch_first=True)
self.fc = nn.Linear(hidden_dim, num_classes)
def forward(self, x):
# x形状: (batch_size, seq_len)
embedded = self.embedding(x) # (batch_size, seq_len, embed_dim)
output, hidden = self.rnn(embedded)
# 取最后一个时间步的输出
last_output = output[:, -1, :]
output = self.fc(last_output)
return output
这个基础RNN有几个关键组件:
- Embedding层:将离散的token转换为连续的向量表示
- RNN层:处理序列数据,输出每个时间步的隐藏状态
- 全连接层:将最后一个时间步的隐藏状态映射到输出空间
3.3.3 LSTM与GRU:解决长程依赖问题
基础RNN存在梯度消失问题,难以学习长序列中的依赖关系。LSTM和GRU通过门控机制解决了这个问题:
python复制class LSTMModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers, num_classes):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers,
batch_first=True, dropout=0.3 if num_layers>1 else 0)
self.fc = nn.Linear(hidden_dim, num_classes)
def forward(self, x, lengths=None):
embedded = self.embedding(x)
if lengths is not None:
# 处理变长序列
packed = nn.utils.rnn.pack_padded_sequence(
embedded, lengths.cpu(), batch_first=True, enforce_sorted=False)
packed_output, (hidden, cell) = self.lstm(packed)
output, _ = nn.utils.rnn.pad_packed_sequence(packed_output, batch_first=True)
else:
output, (hidden, cell) = self.lstm(embedded)
last_output = output[:, -1, :]
output = self.fc(last_output)
return output
LSTM的关键改进是引入了三个门控机制:
- 输入门:控制新信息的流入
- 遗忘门:控制旧信息的遗忘
- 输出门:控制输出的信息
GRU是LSTM的简化版本,只有两个门:
- 重置门:控制历史信息的忽略程度
- 更新门:控制新信息与历史信息的混合比例
3.3.4 双向RNN与注意力机制
双向RNN可以同时利用过去和未来的上下文信息:
python复制class BiLSTM(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True,
bidirectional=True)
self.fc = nn.Linear(hidden_dim * 2, num_classes) # 双向需要2倍维度
def forward(self, x):
embedded = self.embedding(x)
output, (hidden, cell) = self.lstm(embedded)
# 拼接最后两个方向的隐藏状态
hidden = torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1)
output = self.fc(hidden)
return output
注意力机制可以动态地关注输入序列的不同部分:
python复制class Attention(nn.Module):
def __init__(self, hidden_dim):
super().__init__()
self.attention = nn.Linear(hidden_dim, 1)
def forward(self, rnn_output):
# rnn_output形状: (batch_size, seq_len, hidden_dim)
attention_weights = torch.softmax(
self.attention(rnn_output).squeeze(2), dim=1)
# 加权求和
context = torch.bmm(attention_weights.unsqueeze(1), rnn_output).squeeze(1)
return context, attention_weights
3.3.5 RNN的典型应用场景
RNN及其变体在序列数据处理中表现出色:
- 文本分类:情感分析、主题分类
- 序列标注:命名实体识别、词性标注
- 序列生成:机器翻译、文本摘要
- 时间序列预测:股票预测、天气预测
4. 自动微分系统:理解PyTorch的核心
4.1 Autograd基础原理
4.1.1 计算图与自动微分
PyTorch的自动微分系统(Autograd)是其核心特性之一。它通过构建动态计算图来自动计算梯度:
- 前向传播:记录所有执行的操作,构建计算图
- 反向传播:从输出开始,根据链式法则计算梯度
- 梯度累积:梯度累积在叶节点的grad属性中
python复制# 自动微分示例
x = torch.tensor(2.0, requires_grad=True)
w = torch.tensor(3.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)
# 前向计算
y = w * x + b
# 反向传播
y.backward()
# 查看梯度
print(f"∂y/∂x = {x.grad}") # 3.0
print(f"∂y/∂w = {w.grad}") # 2.0
print(f"∂y/∂b = {b.grad}") # 1.0
4.1.2 梯度控制技巧
在实际应用中,我们需要精细控制梯度计算:
- 梯度清零:防止梯度累积
python复制optimizer.zero_grad() # 训练循环中每次迭代前调用
- 阻止梯度跟踪:减少内存消耗
python复制with torch.no_grad():
# 这里的计算不会被跟踪
y = model(x)
- 分离张量:从计算图中分离
python复制y = model(x)
z = y.detach() # z不再有梯度信息
- 保留梯度:非叶节点的梯度默认会被释放
python复制y = model(x)
y.retain_grad() # 保留y的梯度
4.1.3 高阶导数
PyTorch支持高阶导数计算,通过设置create_graph=True:
python复制x = torch.tensor(2.0, requires_grad=True)
y = x ** 3
# 一阶导数
dy_dx = torch.autograd.grad(y, x, create_graph=True)[0]
# 二阶导数
d2y_dx2 = torch.autograd.grad(dy_dx, x)[0]
print(f"一阶导数: {dy_dx.item()}") # 12.0
print(f"二阶导数: {d2y_dx2.item()}") # 12.0
4.2 自定义自动微分函数
4.2.1 自定义Function的实现
PyTorch允许我们自定义前向和反向传播函数,这对于实现特殊操作或优化性能非常有用:
python复制class CustomSigmoid(torch.autograd.Function):
@staticmethod
def forward(ctx, input):
output = 1 / (1 + torch.exp(-input))
ctx.save_for_backward(output) # 保存反向传播需要的信息
return output
@staticmethod
def backward(ctx, grad_output):
output, = ctx.saved_tensors
grad_input = grad_output * output * (1 - output) # sigmoid的导数
return grad_input
# 使用自定义函数
x = torch.randn(4, requires_grad=True)
y = CustomSigmoid.apply(x)
loss = y.sum()
loss.backward()
4.2.2 梯度检查
自定义函数的反向传播实现可能有误,PyTorch提供了梯度检查工具:
python复制from torch.autograd import gradcheck
# 创建输入
input = torch.randn(3, 3, dtype=torch.double, requires_grad=True)
# 检查梯度计算是否正确
test = gradcheck(CustomSigmoid.apply, (input,), eps=1e-6, atol=1e-4)
print("梯度检查:", test) # 应该返回True
4.2.3 性能优化技巧
自定义函数可以用于性能优化,例如实现融合操作:
python复制class FusedBiasActivation(torch.autograd.Function):
@staticmethod
def forward(ctx, input, bias):
ctx.save_for_backward(input)
output = input + bias.unsqueeze(0)
output = torch.relu(output)
return output
@staticmethod
def backward(ctx, grad_output):
input, = ctx.saved_tensors
grad_input = grad_output.clone()
grad_input[input + bias.unsqueeze(0) <= 0] = 0 # ReLU的导数
grad_bias = grad_input.sum(0)
return grad_input, grad_bias
# 使用融合操作
def fused_bias_relu(x, b):
return FusedBiasActivation.apply(x, b)
这种融合操作可以减少内存访问和中间结果的存储,提高性能。
5. 训练优化与调试技巧
5.1 损失函数选择
5.1.1 常见损失函数
不同的任务需要不同的损失函数:
-
回归任务:
- MSE (均方误差):
nn.MSELoss() - MAE (平均绝对误差):
nn.L1Loss() - Huber损失:
nn.SmoothL1Loss()
- MSE (均方误差):
-
分类任务:
- 交叉熵损失:
nn.CrossEntropyLoss() - 二元交叉熵:
nn.BCELoss() - 带logits的二元交叉熵:
nn.BCEWithLogitsLoss()
- 交叉熵损失:
-
特殊任务:
- 对比损失:
nn.ContrastiveLoss() - Triplet损失:
nn.TripletMarginLoss() - Focal Loss:处理类别不平衡
- 对比损失:
5.1.2 自定义损失函数
实现一个Focal Loss来处理类别不平衡问题:
python复制class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
super().__init__()
self.alpha = alpha
self.gamma = gamma
self.reduction = reduction
def forward(self, inputs, targets):
# 计算交叉熵
ce_loss = F.cross_entropy(inputs, targets, reduction='none')
# 计算概率
pt = torch.exp(-ce_loss)
# 计算Focal Loss
focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
if self.reduction == 'mean':
return focal_loss.mean()
elif self.reduction == 'sum':
return focal_loss.sum()
else:
return focal_loss
5.1.3 多任务学习损失
在多任务学习中,我们需要平衡不同任务的损失:
python复制class MultiTaskLoss(nn.Module):
def __init__(self, task_num):
super().__init__()
self.log_vars = nn.Parameter(torch.zeros(task_num))
def forward(self, *losses):
total_loss = 0
for i, loss in enumerate(losses):
precision = torch.exp(-self.log_vars[i])
total_loss += precision * loss + self.log_vars[i]
return total_loss
这种方法可以自动学习不同任务损失的相对权重。
5.2 优化器选择与配置
5.2.1 常见优化器比较
PyTorch提供了多种优化器:
-
SGD:
torch.optim.SGD- 优点:简单,泛化性能好
- 缺点:需要仔细调参
- 适用:计算机视觉任务
-
Adam:
torch.optim.Adam- 优点:自适应学习率,收敛快
- 缺点:可能泛化稍差
- 适用:自然语言处理任务
-
AdamW:
torch.optim.AdamW- 改进:正确处理权重衰减
- 适用:Transformer等现代架构
5.2.2 分层学习率
不同层可能需要不同的学习率:
python复制# 为不同层设置不同学习率
param_groups = [
{'params': model.features.parameters(), 'lr': 0.001},
{'params': model.classifier.parameters(), 'lr': 0.01}
]
optimizer = torch.optim.Adam(param_groups)
5.2.3 优化器配置技巧
- 学习率预热:
python复制def warmup_scheduler(optimizer, warmup_steps):
def lr_lambda(step):
if step < warmup_steps:
return float(step) / float(max(1, warmup_steps))
return 1.0
return torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
- 梯度裁剪:防止梯度爆炸
python复制torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
- 权重衰减:L2正则化
python复制optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)
5.3 学习率调度策略
5.3.1 常见调度器
- StepLR:固定步长衰减
python复制scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
- MultiStepLR:多步长衰减
python复制scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer,
milestones=[30, 80],
gamma=0.1)
- CosineAnnealingLR:余弦退火
python复制scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
- ReduceLROnPlateau:基于指标调整
python复制scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer,
mode='min',
factor=0.5,