一、核心概念与组件梳理
(一)神经网络核心组件
神经网络的构建与训练依赖四大核心组件,各组件分工明确、协同工作,构成完整的模型运行体系:
组件 | 核心功能 |
---|---|
层(Layer) | 神经网络的基本结构单元,负责对输入张量进行数据变换(如卷积、线性变换等),实现特征提取与转换。 |
模型(Model) | 由多个层按特定逻辑组合而成的网络结构,是完成任务(分类、回归等)的核心载体。 |
损失函数 | 定义参数学习的目标函数,用于量化模型预测值(Y')与真实值(Y)之间的差异,是参数优化的依据。 |
优化器 | 实现损失函数最小化的算法,通过调整模型可学习参数(如权重、偏置),使模型性能逐步提升。 |
组件间的协同流程为:输入数据(X)经层的变换后得到预测值(Y'),损失函数计算 Y' 与真实值(Y)的差异,优化器根据损失值更新层的权重参数,最终实现模型性能迭代。
(二)PyTorch 核心构建工具
PyTorch 提供nn.Module
与nn.functional
两大工具集用于构建神经网络,二者功能互补但使用方式存在显著差异。
1. 工具特性对比
对比维度 | nn.Module | nn.functional |
---|---|---|
本质 | 面向对象的模块基类 | 纯函数集合 |
典型应用 | 卷积层、全连接层、Dropout 层、损失函数等 | 激活函数、池化层等 |
语法格式 | nn.Xxx(参数) (需实例化后调用) | nn.functional.xxx(输入, 参数) (直接调用函数) |
参数管理 | 自动定义和管理 weight、bias 等可学习参数 | 需手动定义和传入 weight、bias,无自动管理机制 |
与容器兼容性 | 可与nn.Sequential 等模型容器无缝结合 | 无法与模型容器结合,不利于结构化构建 |
状态切换 | Dropout 等模块可通过model.eval() 自动切换状态 | 需手动控制训练 / 测试状态,无自动切换功能 |
2. 适用场景总结
- 优先使用
nn.Module
:构建包含可学习参数的层(如nn.Linear
、nn.Conv2d
)、需要状态管理的模块(如nn.Dropout
),或需通过容器组织的复杂模型。 - 辅助使用
nn.functional
:调用无参数的功能性操作(如F.relu
、F.max_pool2d
),尤其在forward
方法中进行简单张量变换时更便捷。
二、模型构建方法详解
PyTorch 提供三种主流模型构建方式,分别适用于不同复杂度与灵活性需求的场景,核心均围绕nn.Module
基类展开。
(一)继承 nn.Module 基类构建模型
这是最灵活的构建方式,需自定义网络层定义与前向传播逻辑,适用于复杂结构的模型设计。
代码实现与注释
python
运行
import torch
from torch import nn
import torch.nn.functional as F
# 定义模型类,必须继承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__()
# 1. 展平层:将二维输入(如28x28图像)转为一维张量
self.flatten = nn.Flatten()
# 2. 全连接层1:输入维度in_dim,输出维度n_hidden_1
self.linear1 = nn.Linear(in_dim, n_hidden_1)
# 3. 批归一化层1:对全连接层1的输出进行归一化,加速训练
self.bn1 = nn.BatchNorm1d(n_hidden_1)
# 4. 全连接层2:输入维度n_hidden_1,输出维度n_hidden_2
self.linear2 = nn.Linear(n_hidden_1, n_hidden_2)
# 5. 批归一化层2:对全连接层2的输出进行归一化
self.bn2 = nn.BatchNorm1d(n_hidden_2)
# 6. 输出层:输入维度n_hidden_2,输出维度out_dim(对应分类类别数)
self.out = nn.Linear(n_hidden_2, out_dim)
# 前向传播方法:定义数据流过网络的计算逻辑,必须实现
def forward(self, x):
# 第一步:展平输入张量
x = self.flatten(x)
# 第二步:全连接层1 -> 批归一化 -> ReLU激活
x = self.linear1(x)
x = self.bn1(x)
x = F.relu(x)
# 第三步:全连接层2 -> 批归一化 -> ReLU激活
x = self.linear2(x)
x = self.bn2(x)
x = F.relu(x)
# 第四步:输出层 -> Softmax激活(将输出转为概率分布,dim=1表示按样本维度计算)
x = self.out(x)
x = F.softmax(x, dim=1)
return x
# 超参数定义(以MNIST手写数字识别为例)
in_dim = 28 * 28 # 输入维度:28x28图像展平后为784
n_hidden_1 = 300 # 第一个隐藏层神经元数
n_hidden_2 = 100 # 第二个隐藏层神经元数
out_dim = 10 # 输出维度: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)
)
(二)使用 nn.Sequential 按层顺序构建模型
nn.Sequential
是 PyTorch 提供的顺序容器,可按顺序封装多个层,自动实现前向传播(按添加顺序执行),适用于结构简单的线性网络。该方法支持三种实现方式:
1. 利用可变参数构建(无层名称)
python
运行
# 定义超参数(与上文一致)
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), # 全连接层1
nn.BatchNorm1d(n_hidden_1), # 批归一化层1
nn.ReLU(), # ReLU激活层
nn.Linear(n_hidden_1, n_hidden_2), # 全连接层2
nn.BatchNorm1d(n_hidden_2), # 批归一化层2
nn.ReLU(), # ReLU激活层
nn.Linear(n_hidden_2, out_dim), # 输出层
nn.Softmax(dim=1) # Softmax激活层
)
# 打印模型结构
print(Seq_arg)
2. 使用 add_module 方法构建(指定层名称)
python
运行
# 初始化空的Sequential容器
Seq_module = nn.Sequential()
# 调用add_module添加层,第一个参数为层名称,第二个参数为层实例
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 构建(有序字典指定层名称)
python
运行
from collections import OrderedDict
# 利用OrderedDict存储层名称与层实例的映射,保证顺序
layer_dict = 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))
])
# 传入OrderedDict构建模型
Seq_ordered = nn.Sequential(layer_dict)
# 打印模型结构
print(Seq_ordered)
三种方式的共性与差异
- 共性:均按顺序执行前向传播,最终模型功能一致。
- 差异:可变参数方式最简单但无法指定层名称;
add_module
与OrderedDict
方式可指定层名称,便于调试时定位层结构,其中OrderedDict
更适合批量定义层。
(三)继承 nn.Module 并结合模型容器构建
当模型结构复杂(如包含多个子模块)时,可继承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() # 独立展平层
# 子模块1:全连接层1 + 批归一化层1
self.layer1 = nn.Sequential(
nn.Linear(in_dim, n_hidden_1),
nn.BatchNorm1d(n_hidden_1)
)
# 子模块2:全连接层2 + 批归一化层2
self.layer2 = nn.Sequential(
nn.Linear(n_hidden_1, n_hidden_2),
nn.BatchNorm1d(n_hidden_2)
)
# 子模块3:输出层
self.out = nn.Sequential(nn.Linear(n_hidden_2, out_dim))
def forward(self, x):
x = self.flatten(x)
# 子模块1输出经ReLU激活
x = F.relu(self.layer1(x))
# 子模块2输出经ReLU激活
x = F.relu(self.layer2(x))
# 输出层经Softmax激活
x = F.softmax(self.out(x), dim=1)
return x
# 实例化模型
model_lay = Model_lay(in_dim, n_hidden_1, n_hidden_2, out_dim)
print(model_lay)
2. 结合 nn.ModuleList 容器
nn.ModuleList
是存储层的列表容器,支持索引访问,适用于需要动态调整层数量的场景:
python
运行
class Model_lst(nn.Module):
def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
super(Model_lst, self).__init__()
# 用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):
# 遍历ModuleList中的层,按顺序执行前向传播
for layer in self.layers:
x = layer(x)
return x
# 实例化模型
model_lst = Model_lst(in_dim, n_hidden_1, n_hidden_2, out_dim)
print(model_lst)
3. 结合 nn.ModuleDict 容器
nn.ModuleDict
是存储层的字典容器,通过键值对访问层,适用于需要动态选择层执行顺序的场景:
python
运行
class Model_dict(nn.Module):
def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
super(Model_dict, self).__init__()
# 用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(in_dim, n_hidden_1, n_hidden_2, out_dim)
print(model_dict)
三、自定义网络模块实践
对于复杂网络(如 ResNet),需自定义基础模块(如残差块),再组合成完整模型。以下以 ResNet 的核心组件 —— 残差块为例,说明自定义模块的实现方法。
(一)基础残差块(RestNetBasicBlock)
适用于输入与输出维度一致的场景,直接将输入与模块输出相加:
python
运行
import torch
import torch.nn as nn
from torch.nn import functional as F
class RestNetBasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride):
super(RestNetBasicBlock, self).__init__()
# 第一个3x3卷积层:保持通道数与分辨率不变(stride=1)
self.conv1 = nn.Conv2d(
in_channels, out_channels,
kernel_size=3, stride=stride, padding=1
)
self.bn1 = nn.BatchNorm2d(out_channels) # 批归一化层1
# 第二个3x3卷积层:进一步提取特征
self.conv2 = nn.Conv2d(
out_channels, out_channels,
kernel_size=3, stride=stride, padding=1
)
self.bn2 = nn.BatchNorm2d(out_channels) # 批归一化层2
def forward(self, x):
# 模块内部前向传播
output = self.conv1(x)
output = F.relu(self.bn1(output)) # 卷积1 -> 归一化 -> ReLU
output = self.conv2(output)
output = self.bn2(output) # 卷积2 -> 归一化
# 残差连接:输入x与模块输出相加,再经ReLU激活
return F.relu(x + output)
(二)下采样残差块(RestNetDownBlock)
适用于输入与输出维度不一致的场景(如通道数增加、分辨率降低),通过 1×1 卷积调整输入维度后再相加:
python
运行
class RestNetDownBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride):
super(RestNetDownBlock, self).__init__()
# 第一个3x3卷积层:stride[0]控制分辨率下采样
self.conv1 = nn.Conv2d(
in_channels, out_channels,
kernel_size=3, stride=stride[0], padding=1
)
self.bn1 = nn.BatchNorm2d(out_channels) # 批归一化层1
# 第二个3x3卷积层:stride[1]保持分辨率不变
self.conv2 = nn.Conv2d(
out_channels, out_channels,
kernel_size=3, stride=stride[1], padding=1
)
self.bn2 = nn.BatchNorm2d(out_channels) # 批归一化层2
# 1×1卷积层:调整输入x的通道数与分辨率,使其与输出维度一致
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):
# 先通过1×1卷积调整输入维度
extra_x = self.extra(x)
# 模块内部前向传播
output = self.conv1(x)
output = F.relu(self.bn1(output)) # 卷积1 -> 归一化 -> ReLU
output = self.conv2(output)
output = self.bn2(output) # 卷积2 -> 归一化
# 残差连接:调整后的输入与模块输出相加,再经ReLU激活
return F.relu(extra_x + output)
(三)组合模块构建 ResNet18
将基础残差块与下采样残差块按 ResNet18 的结构组合,形成完整网络:
python
运行
class RestNet18(nn.Module):
def __init__(self):
super(RestNet18, self).__init__()
# 初始卷积层:3通道输入→64通道,7x7卷积下采样
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) # 最大池化下采样
# 第一层:2个基础残差块(64通道→64通道,无下采样)
self.layer1 = nn.Sequential(
RestNetBasicBlock(64, 64, 1),
RestNetBasicBlock(64, 64, 1)
)
# 第二层:1个下采样残差块(64→128)+1个基础残差块
self.layer2 = nn.Sequential(
RestNetDownBlock(64, 128, [2, 1]),
RestNetBasicBlock(128, 128, 1)
)
# 第三层:1个下采样残差块(128→256)+1个基础残差块
self.layer3 = nn.Sequential(
RestNetDownBlock(128, 256, [2, 1]),
RestNetBasicBlock(256, 256, 1)
)
# 第四层:1个下采样残差块(256→512)+1个基础残差块
self.layer4 = nn.Sequential(
RestNetDownBlock(256, 512, [2, 1]),
RestNetBasicBlock(512, 512, 1)
)
self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1, 1)) # 自适应平均池化→1x1特征图
self.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_size, 512)
out = self.fc(out)
return out
四、模型训练流程
模型构建完成后,需经过标准化的训练流程实现参数优化,核心步骤包括:
1. 加载预处理数据集
通过 PyTorch 的torchvision
等工具加载数据集,并进行预处理(如归一化、数据增强):
python
运行
import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader
# 数据预处理:归一化到[0,1]并转为张量
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
# 加载MNIST训练集与测试集
train_dataset = torchvision.datasets.MNIST(
root='./data', train=True, download=True, transform=transform
)
test_dataset = torchvision.datasets.MNIST(
root='./data', train=False, download=True, transform=transform
)
# 构建数据加载器(批量读取数据)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
2. 定义损失函数与优化器
根据任务类型选择损失函数,结合优化器实现参数更新:
python
运行
# 定义损失函数:交叉熵损失(适用于分类任务)
criterion = nn.CrossEntropyLoss()
# 定义优化器:Adam优化器,学习率0.001
optimizer = torch.optim.Adam(model_seq.parameters(), lr=0.001)
3. 循环训练与验证
交替进行训练(更新参数)与验证(评估性能),记录模型表现:
python
运行
epochs = 5 # 训练轮数
for epoch in range(epochs):
# 训练阶段:模型设为训练模式
model_seq.train()
train_loss = 0.0
for images, labels in train_loader:
# 前向传播:计算预测值
outputs = model_seq(images)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播与参数更新
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播计算梯度
optimizer.step() # 优化器更新参数
# 累加训练损失
train_loss += loss.item() * images.size(0)
# 计算平均训练损失
train_loss = train_loss / len(train_loader.dataset)
print(f'Epoch [{epoch+1}/{epochs}], Train Loss: {train_loss:.4f}')
# 验证阶段:模型设为评估模式
model_seq.eval()
correct = 0
total = 0
with torch.no_grad(): # 关闭梯度计算,节省资源
for images, labels in test_loader:
outputs = model_seq(images)
_, predicted = torch.max(outputs.data, 1) # 取概率最大的类别
total += labels.size(0)
correct += (predicted == labels).sum().item()
# 计算验证准确率
val_acc = 100 * correct / total
print(f'Validation Accuracy: {val_acc:.2f}%')
4. 可视化结果
通过matplotlib
等工具可视化训练损失与验证准确率,分析模型收敛情况:
python
运行
import matplotlib.pyplot as plt
# 假设已记录各轮次的损失与准确率
epochs_list = list(range(1, epochs+1))
train_losses = [1.2, 0.8, 0.5, 0.3, 0.2] # 示例数据
val_accs = [60, 75, 85, 92, 95] # 示例数据
# 绘制损失曲线
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs_list, train_losses, 'b-', label='Train Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Curve')
plt.legend()
# 绘制准确率曲线
plt.subplot(1, 2, 2)
plt.plot(epochs_list, val_accs, 'r-', label='Validation Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy (%)')
plt.title('Validation Accuracy Curve')
plt.legend()
plt.tight_layout()
plt.show()
五、关键总结与注意事项
- 工具选择原则:
nn.Module
用于可学习参数模块与状态管理,nn.functional
用于无参数功能操作,二者结合可兼顾灵活性与简洁性。 - 模型构建范式:简单线性模型优先用
nn.Sequential
;复杂模型需继承nn.Module
并自定义forward
方法;多子模块场景结合ModuleList
/ModuleDict
实现结构化管理。 - 残差连接核心:当输入与输出维度不一致时,必须通过 1×1 卷积调整维度,否则无法实现残差相加。
- 训练关键操作:训练前需调用
model.train()
开启 dropout/batchnorm 等训练模式,验证前需调用model.eval()
切换为评估模式,且验证时用torch.no_grad()
关闭梯度计算。
通过以上内容的学习与实践,可掌握 PyTorch 构建与训练神经网络的核心能力,为复杂任务(图像识别、自然语言处理等)的模型开发奠定基础。