PyTorch 神经网络模型构建与训练笔记(2)
一、核心工具回顾:nn.Module vs nn.functional
在正式构建模型前,需明确 PyTorch 中两个核心工具的区别 —— 二者均用于实现网络组件,但设计逻辑与适用场景不同,是后续模型构建的基础。
对比维度 | nn.Module | nn.functional |
---|---|---|
本质 | 类(需实例化),继承自nn.Module | 纯函数(直接调用) |
参数管理 | 自动定义、管理weight /bias 等可学习参数 | 需手动定义、传入weight /bias ,无自动管理 |
状态切换(如 Dropout) | 调用model.eval() 后自动切换测试模式 | 需手动传入training=True/False ,无自动切换 |
兼容性 | 可与nn.Sequential 等模型容器结合 | 无法与nn.Sequential 结合,复用性差 |
适用场景 | 卷积层(nn.Conv2d )、全连接层(nn.Linear )等带参数的组件 | 激活函数(F.relu )、池化层(F.max_pool2d )等无参数的组件 |
二、三种模型构建方法
PyTorch 提供多种模型构建方式,可根据网络复杂度与灵活性需求选择,核心是 “层的组织与数据流定义”。
方法 1:直接继承 nn.Module 基类(最灵活)
适用于复杂网络(需自定义数据流逻辑),核心是重写__init__
(定义层)与forward
(定义前向传播数据流)两个方法。
代码示例(手写数字分类网络)
python
import torch
from torch import nn
import torch.nn.functional as F# 1. 定义模型类,继承nn.Module
class Model_Seq(nn.Module):def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):super(Model_Seq, self).__init__() # 调用父类构造函数# 定义网络层(可学习参数由nn.Module自动管理)self.flatten = nn.Flatten() # 展平层:将28×28图像转为784维向量self.linear1 = nn.Linear(in_dim, n_hidden_1) # 全连接层1:输入→隐藏层1self.bn1 = nn.BatchNorm1d(n_hidden_1) # 批量归一化层1:加速训练self.linear2 = nn.Linear(n_hidden_1, n_hidden_2) # 全连接层2:隐藏层1→隐藏层2self.bn2 = nn.BatchNorm1d(n_hidden_2) # 批量归一化层2self.out = nn.Linear(n_hidden_2, out_dim) # 输出层:隐藏层2→10分类(0-9)# 2. 定义前向传播:数据如何流经各层def forward(self, x):x = self.flatten(x) # 展平:[batch,1,28,28]→[batch,784]x = self.linear1(x) # 全连接1x = self.bn1(x) # 批量归一化1x = F.relu(x) # 激活函数(用nn.functional)x = self.linear2(x) # 全连接2x = self.bn2(x) # 批量归一化2x = F.relu(x) # 激活函数x = self.out(x) # 输出层x = F.softmax(x, dim=1) # 分类概率归一化(dim=1:按样本维度)return x# 3. 实例化模型(超参数:输入维度784=28×28,隐藏层1=300,隐藏层2=100,输出=10)
in_dim, n_hidden_1, n_hidden_2, out_dim = 28*28, 300, 100, 10
model_seq = Model_Seq(in_dim, n_hidden_1, n_hidden_2, out_dim)
print(model_seq) # 打印模型结构,可查看各层参数
运行结果(模型结构)
plaintext
Model_Seq((flatten): Flatten(start_dim=1, end_dim=-1)(linear1): Linear(in_features=784, out_features=300, bias=True)(bn1): BatchNorm1d(300, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)(linear2): Linear(in_features=300, out_features=100, bias=True)(bn2): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)(out): Linear(in_features=100, out_features=10, bias=True)
)
方法 2:使用 nn.Sequential 按层顺序构建(最简单)
适用于层顺序固定、无复杂分支的网络(如简单全连接、基础 CNN),无需重写forward
(自动按顺序执行),支持三种实现方式。
方式 1:可变参数法(无层名,简洁)
直接将层作为参数传入nn.Sequential
,缺点是无法给层命名,不利于调试。
python
import torch
from torch import nn# 超参数(与方法1一致)
in_dim, n_hidden_1, n_hidden_2, out_dim = 28*28, 300, 100, 10# 按顺序定义层
Seq_arg = nn.Sequential(nn.Flatten(), # 展平nn.Linear(in_dim, n_hidden_1), # 全连接1nn.BatchNorm1d(n_hidden_1), # 批量归一化1nn.ReLU(), # 激活函数(用nn.Module的ReLU类)nn.Linear(n_hidden_1, n_hidden_2), # 全连接2nn.BatchNorm1d(n_hidden_2), # 批量归一化2nn.ReLU(), # 激活函数nn.Linear(n_hidden_2, out_dim), # 输出层nn.Softmax(dim=1) # 概率归一化
)print(Seq_arg) # 层名默认以0、1、2...编号
方式 2:add_module 法(手动指定层名)
通过add_module("层名", 层实例)
手动添加层,可自定义层名,便于后续访问特定层。
python
Seq_module = nn.Sequential()
# 逐个添加层并命名
Seq_module.add_module("flatten", nn.Flatten())
Seq_module.add_module("linear1", nn.Linear(in_dim, n_hidden_1))
Seq_module.add_module("bn1", nn.BatchNorm1d(n_hidden_1))
Seq_module.add_module("relu1", nn.ReLU())
Seq_module.add_module("linear2", nn.Linear(n_hidden_1, n_hidden_2))
Seq_module.add_module("bn2", nn.BatchNorm1d(n_hidden_2))
Seq_module.add_module("relu2", nn.ReLU())
Seq_module.add_module("out", nn.Linear(n_hidden_2, out_dim))
Seq_module.add_module("softmax", nn.Softmax(dim=1))print(Seq_module) # 层名与自定义一致
方式 3:OrderedDict 法(有序字典指定层名)
借助collections.OrderedDict
(有序字典),一次性定义 “层名 - 层实例” 映射,既有序又有命名。
python
from collections import OrderedDict# 用OrderedDict包装层名与层实例
Seq_ordered = nn.Sequential(OrderedDict([("flatten", nn.Flatten()),("linear1", nn.Linear(in_dim, n_hidden_1)),("bn1", nn.BatchNorm1d(n_hidden_1)),("relu1", nn.ReLU()),("linear2", nn.Linear(n_hidden_1, n_hidden_2)),("bn2", nn.BatchNorm1d(n_hidden_2)),("relu2", nn.ReLU()),("out", nn.Linear(n_hidden_2, out_dim)),("softmax", nn.Softmax(dim=1))
]))print(Seq_ordered) # 效果与add_module一致
方法 3:继承 nn.Module + 模型容器(灵活 + 模块化)
适用于复杂网络(含分支、多模块) ,通过nn.Sequential
/nn.ModuleList
/nn.ModuleDict
等容器将层 “模块化打包”,提升代码可读性。
容器 1:nn.Sequential(模块内顺序固定)
将多个层打包为一个 “子模块”,子模块内自动按顺序执行。
python
class Model_lay(nn.Module):def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):super(Model_lay, self).__init__()self.flatten = nn.Flatten()# 用nn.Sequential打包“全连接+批量归一化”为子模块self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden_1), nn.BatchNorm1d(n_hidden_1))self.layer2 = nn.Sequential(nn.Linear(n_hidden_1, n_hidden_2), nn.BatchNorm1d(n_hidden_2))self.out = nn.Sequential(nn.Linear(n_hidden_2, out_dim))def forward(self, x):x = self.flatten(x)x = F.relu(self.layer1(x)) # 子模块1 + 激活x = F.relu(self.layer2(x)) # 子模块2 + 激活x = F.softmax(self.out(x), dim=1) # 输出子模块 + 概率归一化return x# 实例化
model_lay = Model_lay(28*28, 300, 100, 10)
print(model_lay) # 子模块内的层以0、1编号
容器 2:nn.ModuleList(模块列表,灵活遍历)
将层存储为 “列表”,需在forward
中手动循环遍历执行,适用于层数量不确定的场景。
python
class Model_lst(nn.Module):def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):super(Model_lst, self).__init__()# 用nn.ModuleList包装层列表self.layers = nn.ModuleList([nn.Flatten(),nn.Linear(in_dim, n_hidden_1),nn.BatchNorm1d(n_hidden_1),nn.ReLU(),nn.Linear(n_hidden_1, n_hidden_2),nn.BatchNorm1d(n_hidden_2),nn.ReLU(),nn.Linear(n_hidden_2, out_dim),nn.Softmax(dim=1)])def forward(self, x):# 手动循环遍历层列表for layer in self.layers:x = layer(x)return x# 实例化
model_lst = Model_lst(28*28, 300, 100, 10)
print(model_lst) # 层以列表索引0-8编号
容器 3:nn.ModuleDict(模块字典,按键调用)
将层存储为 “字典”(键 = 层名,值 = 层实例),需在forward
中按预设顺序调用,适用于需动态选择层的场景。
python
class Model_dict(nn.Module):def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):super(Model_dict, self).__init__()# 用nn.ModuleDict包装层字典self.layers_dict = nn.ModuleDict({"flatten": nn.Flatten(),"linear1": nn.Linear(in_dim, n_hidden_1),"bn1": nn.BatchNorm1d(n_hidden_1),"relu": nn.ReLU(),"linear2": nn.Linear(n_hidden_1, n_hidden_2),"bn2": nn.BatchNorm1d(n_hidden_2),"out": nn.Linear(n_hidden_2, out_dim),"softmax": nn.Softmax(dim=1)})def forward(self, x):# 预设层执行顺序(按键调用)layer_order = ["flatten", "linear1", "bn1", "relu", "linear2", "bn2", "relu", "out", "softmax"]for layer_name in layer_order:x = self.layers_dict[layer_name](x)return x# 实例化
model_dict = Model_dict(28*28, 300, 100, 10)
print(model_dict) # 层以字典键命名
三、自定义网络模块:残差块与 ResNet18
当基础层无法满足复杂网络需求时(如深层网络的梯度消失问题),需自定义模块。以残差块(Residual Block) 为例,其核心是 “残差连接”(输入直接加到输出),是 ResNet 系列网络的基础。
1. 基础残差块:RestNetBasicBlock(输入输出形状一致)
适用于输入与输出通道数、分辨率相同的场景,直接将输入x
与卷积后的输出相加,缓解梯度消失。
python
import torch
import torch.nn as nn
import torch.nn.functional as Fclass RestNetBasicBlock(nn.Module):def __init__(self, in_channels, out_channels, stride):super(RestNetBasicBlock, self).__init__()# 残差块核心:2个3×3卷积+批量归一化self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)self.bn1 = nn.BatchNorm2d(out_channels)self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1)self.bn2 = nn.BatchNorm2d(out_channels)def forward(self, x):output = self.conv1(x) # 卷积1output = F.relu(self.bn1(output)) # 批量归一化1 + 激活output = self.conv2(output) # 卷积2output = self.bn2(output) # 批量归一化2return F.relu(x + output) # 残差连接:输入x + 输出 → 激活
2. 下采样残差块:RestNetDownBlock(调整形状一致)
适用于输入与输出通道数 / 分辨率不同的场景,需通过 1×1 卷积(extra
模块)调整输入x
的形状,确保与输出可相加。
python
class RestNetDownBlock(nn.Module):def __init__(self, in_channels, out_channels, stride):super(RestNetDownBlock, self).__init__()# 主路径:2个3×3卷积(stride[0]控制分辨率下采样)self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride[0], padding=1)self.bn1 = nn.BatchNorm2d(out_channels)self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride[1], padding=1)self.bn2 = nn.BatchNorm2d(out_channels)# 残差路径:1×1卷积调整输入形状(通道数+分辨率)self.extra = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride[0], padding=0),nn.BatchNorm2d(out_channels))def forward(self, x):extra_x = self.extra(x) # 1×1卷积调整输入形状output = self.conv1(x) # 主路径卷积1output = F.relu(self.bn1(output)) # 批量归一化1 + 激活output = self.conv2(output) # 主路径卷积2output = self.bn2(output) # 批量归一化2return F.relu(extra_x + output) # 调整后的输入 + 主路径输出 → 激活
3. 组合残差块:ResNet18 网络
ResNet18 由 “1 个初始卷积层 + 4 个残差块组 + 平均池化 + 全连接层” 组成,通过堆叠上述两种残差块实现深层结构。
python
class RestNet18(nn.Module):def __init__(self):super(RestNet18, self).__init__()# 初始卷积层(将输入3通道→64通道,分辨率下采样)self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)self.bn1 = nn.BatchNorm2d(64)self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 进一步下采样# 4个残差块组(每组含2个残差块)self.layer1 = nn.Sequential(RestNetBasicBlock(64, 64, 1), RestNetBasicBlock(64, 64, 1)) # 无下采样self.layer2 = nn.Sequential(RestNetDownBlock(64, 128, [2, 1]), RestNetBasicBlock(128, 128, 1)) # 下采样→128通道self.layer3 = nn.Sequential(RestNetDownBlock(128, 256, [2, 1]), RestNetBasicBlock(256, 256, 1)) # 下采样→256通道self.layer4 = nn.Sequential(RestNetDownBlock(256, 512, [2, 1]), RestNetBasicBlock(512, 512, 1)) # 下采样→512通道# 分类头(平均池化→展平→全连接)self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1, 1)) # 自适应池化:无论输入尺寸,输出1×1self.fc = nn.Linear(512, 10) # 512通道→10分类def forward(self, x):# 初始卷积+池化out = self.conv1(x)out = self.bn1(out)out = self.maxpool(out)# 残差块组out = self.layer1(out)out = self.layer2(out)out = self.layer3(out)out = self.layer4(out)# 分类头out = self.avgpool(out)out = out.reshape(x.shape[0], -1) # 展平:[batch,512,1,1]→[batch,512]out = self.fc(out)return out
四、模型训练的完整流程
构建完模型后,需通过 “数据加载→损失定义→优化器选择→训练 / 验证→可视化” 完成端到端训练,核心是 “前向传播算损失→反向传播求梯度→优化器更新参数” 的循环。
步骤 1:加载预处理数据集
使用torchvision.datasets
加载公开数据集(如 MNIST、CIFAR-10),并通过DataLoader
实现批量加载与预处理(如归一化、数据增强)。
python
import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader# 1. 预处理:ToTensor(转为张量)+ 归一化(均值=0.5,标准差=0.5)
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])# 2. 加载CIFAR-10数据集(训练集+测试集)
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)# 3. 批量加载(shuffle=True:训练集打乱,提升泛化性)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=2)
步骤 2:定义损失函数与优化器
- 损失函数:根据任务选择(分类任务用
CrossEntropyLoss
,回归任务用MSELoss
)。 - 优化器:常用
SGD
(随机梯度下降)或Adam
,需指定学习率(lr)等超参数。
python
import torch.optim as optim# 1. 实例化模型(ResNet18)
model = RestNet18()
# 2. 定义损失函数(分类任务:交叉熵损失)
criterion = nn.CrossEntropyLoss()
# 3. 定义优化器(SGD,学习率0.001,动量0.9)
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
步骤 3:循环训练模型(Epoch 级 + Batch 级)
- Epoch:遍历整个训练集一次为 1 个 Epoch。
- Batch:每次训练用一个 Batch 的数据更新参数,减少内存占用与训练波动。
python
num_epochs = 10 # 总训练轮次for epoch in range(num_epochs):running_loss = 0.0 # 记录当前Epoch的累计损失# 切换为训练模式(启用Dropout、BatchNorm更新)model.train()# 遍历训练集的每个Batchfor i, data in enumerate(train_loader, 0):# 1. 读取Batch数据(输入图像x,标签labels)inputs, labels = data# 2. 梯度清零(避免累计梯度)optimizer.zero_grad()# 3. 前向传播:计算模型输出outputs = model(inputs)# 4. 计算损失loss = criterion(outputs, labels)# 5. 反向传播:求梯度loss.backward()# 6. 优化器更新参数optimizer.step()# 7. 记录损失running_loss += loss.item()# 每200个Batch打印一次损失if i % 200 == 199:print(f'[{epoch + 1}, {i + 1}] loss: {running_loss / 200:.3f}')running_loss = 0.0print('Finished Training')
步骤 4:循环测试 / 验证模型
训练后需在测试集上评估模型性能(如准确率),需切换为eval
模式(禁用 Dropout、固定 BatchNorm 参数),并禁用梯度计算(节省内存)。
python
correct = 0 # 正确预测的样本数
total = 0 # 总样本数
# 切换为测试模式(禁用Dropout、BatchNorm不更新)
model.eval()# 禁用梯度计算(测试阶段无需求梯度)
with torch.no_grad():# 遍历测试集的每个Batchfor data in test_loader:images, labels = data# 前向传播计算输出outputs = model(images)# 取输出概率最大的类别作为预测结果_, predicted = torch.max(outputs.data, 1)# 累计总样本数与正确样本数total += labels.size(0)correct += (predicted == labels).sum().item()# 计算测试准确率
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total} %')
步骤 5:可视化结果
通过matplotlib
绘制 “训练损失曲线” 与 “测试准确率曲线”,直观观察模型训练趋势(如是否过拟合、是否收敛)。
python
import matplotlib.pyplot as plt
import numpy as np# 假设已记录每个Epoch的训练损失与测试准确率
train_losses = [0.8, 0.5, 0.3, 0.2, 0.15, 0.12, 0.1, 0.09, 0.08, 0.07] # 示例数据
test_accs = [65, 75, 82, 85, 87, 89, 90, 91, 92, 92.5] # 示例数据# 绘制训练损失曲线
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(range(1, num_epochs+1), train_losses, 'b-', label='Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Curve')
plt.legend()# 绘制测试准确率曲线
plt.subplot(1, 2, 2)
plt.plot(range(1, num_epochs+1), test_accs, 'r-', label='Test Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy (%)')
plt.title('Test Accuracy Curve')
plt.legend()plt.show()
五、总结
- 模型构建:PyTorch 提供三种核心方式,可根据复杂度选择 —— 简单网络用
nn.Sequential
,复杂网络用 “继承nn.Module
+ 容器”,灵活度最高。 - 自定义模块:针对深层网络需求(如梯度消失),可通过自定义模块(如残差块)扩展基础组件,是构建 ResNet 等经典网络的关键。
- 训练流程:遵循 “数据加载→损失 / 优化器定义→训练→测试→可视化” 的标准化流程,核心是 “前向 - 反向 - 更新” 的参数迭代循环,需注意
train()
/eval()
模式切换与梯度管理。