一、神经网络的数学本质:线性变换与非线性激活的协同
在 PyTorch 中搭建神经网络,其本质是将数据通过一系列 线性层(如
nn.Linear
、nn.Conv2d
)和非线性激活函数(如nn.ReLU
) 进行变换组合。整个网络就是在模拟如下的数学结构:
y = XWᵀ + b
其中:
- X:是输入张量,
- W:是模型权重(可学习参数),
- b:是偏置项。
单纯的线性结构(即便多层堆叠)也只是一层线性映射。唯有引入激活函数,神经网络才能捕捉复杂的非线性关系。理解这点,是掌握 torch.nn
的第一步。
二、非线性激活函数:赋予网络表达力
神经网络的“智能”来自激活函数。它们在每一层之后对线性输出进行非线性变换,帮助模型捕捉模式、增强特征选择能力。
1. ReLU:线性整流单元(nn.ReLU
)
ReLU(Rectified Linear Unit)是最常用的激活函数之一。
规则:输入 > 0 时,输出 = 输入;否则输出为 0;
-
优势:
- 计算开销极低(仅为一次 max 操作);
- 输出稀疏,有利于泛化;
- 在深度网络中表现更稳定,缓解梯度消失;
-
应用场景:
- 常用于隐藏层的激活,尤其适用于深层神经网络。
2. Sigmoid:概率压缩(nn.Sigmoid
)
将输出值映射到 (0, 1) 区间:
- 用途:可作为二分类任务的输出层;
- 中性点:输入为 0 时输出为 0.5;
- 问题:在输入过大或过小时,梯度趋近于 0,导致梯度消失。
3. Tanh:对称 S 型激活(nn.Tanh
)
Tanh 函数将输入压缩至 (-1, 1):
- 优势:以 0 为中心,对称性优于 Sigmoid;
- 缺点:仍可能在极值区间出现梯度消失;
- 用途:LSTM/GRU 等循环结构中广泛使用。
4. Softmax & Softplus
nn.Softmax
:将向量映射为多分类概率分布,常用于多分类输出层;-
nn.Softplus
:ReLU 的平滑替代品,避免梯度爆炸,公式为:f(x) = log(1 + exp(x))
三、构建神经网络结构的核心层
神经网络之所以强大,离不开多种层之间的配合。在
torch.nn
中,构建网络主要依赖以下核心层:
1. 全连接层(nn.Linear
)
import torch
import torch.nn as nn
# 创建一个线性层:输入特征维度为 5,输出特征维度为 2
linear = nn.Linear(in_features=5, out_features=2)
# 输入一个形状为 [batch_size=3, input_dim=5] 的张量
x = torch.randn(3, 5)
# 前向传播
output = linear(x)
print("Output shape:", output.shape) # [3, 2]
作用:执行线性变换 y = xW^T + b
。这是神经网络中最基础的构建块,常用于:
- 分类模型的最后输出层;
- 中间特征融合;
- 回归问题建模。
你可以通过 .weight
和 .bias
获取层参数。
2. 卷积层:提取局部空间特征
PyTorch 提供了多维度的卷积层:
模块 | 适用对象 |
---|---|
nn.Conv1d |
序列/音频数据 |
nn.Conv2d |
图像数据 |
nn.Conv3d |
视频/医学图像 |
卷积层的核心参数包括:
参数 | 说明 |
---|---|
in_channels |
输入通道数(如 RGB 为 3); |
out_channels |
卷积核数量(即输出通道数); |
kernel_size |
卷积核尺寸; |
stride |
滑动步长; |
padding |
边缘填充; |
bias |
是否添加偏置。 |
示例:
import torch
import torch.nn as nn
# 定义一个二维卷积层
conv = nn.Conv2d(
in_channels=3, # 输入通道数,例如 RGB 图像是 3
out_channels=16, # 卷积核数量(输出通道数)
kernel_size=3, # 卷积核大小为 3x3
stride=1, # 步幅为 1
padding=1 # 保证输出大小和输入一样
)
# 输入一个 batch 的图像,尺寸为 (batch_size=4, channels=3, height=32, width=32)
x = torch.randn(4, 3, 32, 32)
# 前向传播
output = conv(x)
print("Output shape:", output.shape) # [4, 16, 32, 32]
→ 输出 16 个特征图,每个 3x3 卷积核在图像上滑动采样特征。
3. 池化层:下采样与特征提取
池化层用于减小特征图尺寸、降低计算量并提取区域代表特征。
常用的有:
-
nn.MaxPool2d
:最大池化(提取最显著特征); -
nn.AvgPool2d
:平均池化(特征平滑)。
nn.MaxPool2d(kernel_size=2, stride=2)
假设原图是 4×4,最大池化后会变为 2×2,区域内保留最大值。
4. 数据展平层:为全连接层准备数据
在卷积层之后,必须将输出展平才能输入到全连接层。
-
nn.Flatten(start_dim=1)
:自动将start_dim
之后的维度展平,适用于nn.Sequential
; -
.view(-1, shape)
:手动变换张量结构,用于自定义结构中。
:
.view()
操作要求输入张量在内存中是连续的。如果你之前对张量做过 .transpose()
或 .permute()
等操作,可能导致张量变为非连续,此时需要:
x = x.contiguous().view(batch_size, -1)
这样才能安全地将数据展平。
展平是连接 CNN 与全连接层的必要桥梁。
5. 转置卷积层:模型中的“放大镜”
在图像生成、超分辨率等任务中,我们需要将低分辨率特征图还原成高分辨率图像,这时就用到了转置卷积(Transposed Convolution),也称“反卷积”。
PyTorch 提供了如下模块:
模块 | 应用场景 |
---|---|
nn.ConvTranspose1d |
时间序列重构、上采样 |
nn.ConvTranspose2d |
图像上采样、图像生成 |
nn.ConvTranspose3d |
3D 数据恢复、重建 |
转置卷积的本质是将标准卷积中的“前向传播”反向操作,实现特征图尺寸的还原。常用于:
- AutoEncoder 的解码器部分;
- GAN 的生成器部分;
- 超分辨率模型中图像恢复。
四、构建模块组合:用 torch.nn
搭积木
为了构建复杂网络结构,PyTorch 提供了结构化的组合容器:
-
nn.Sequential
:顺序叠加模块:
import torch
import torch.nn as nn
# 使用 nn.Sequential 构建一个简单的全连接网络
model = nn.Sequential(
nn.Linear(20, 64),
nn.ReLU(),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, 10)
)
# 输入一个随机张量,模拟 batch size = 5,特征维度 = 20
x = torch.randn(5, 20)
output = model(x)
print("Output shape:", output.shape)
-
nn.ModuleList
:以列表形式存储多个层,适合动态结构;
import torch
import torch.nn as nn
import torch.nn.functional as F
class DynamicMLP(nn.Module):
def __init__(self, input_dim, hidden_dims, output_dim):
super(DynamicMLP, self).__init__()
self.layers = nn.ModuleList()
# 创建多层线性层
prev_dim = input_dim
for hidden_dim in hidden_dims:
self.layers.append(nn.Linear(prev_dim, hidden_dim))
prev_dim = hidden_dim
self.output_layer = nn.Linear(prev_dim, output_dim)
def forward(self, x):
for layer in self.layers:
x = F.relu(layer(x))
return self.output_layer(x)
# 用法示例:
model = DynamicMLP(input_dim=10, hidden_dims=[64, 128, 64], output_dim=3)
print(model)
-
nn.ModuleDict
:以字典形式组织模块,可方便地调用不同分支结构。
class MultiTaskNet(nn.Module):
def __init__(self, input_dim, shared_dim, task_dims):
super(MultiTaskNet, self).__init__()
self.shared_layer = nn.Linear(input_dim, shared_dim)
# 定义不同任务的输出头
self.task_heads = nn.ModuleDict({
task_name: nn.Linear(shared_dim, out_dim)
for task_name, out_dim in task_dims.items()
})
def forward(self, x):
shared = F.relu(self.shared_layer(x))
outputs = {}
for task_name, head in self.task_heads.items():
outputs[task_name] = head(shared)
return outputs
# 用法示例:
model = MultiTaskNet(input_dim=100, shared_dim=64, task_dims={
"classification": 10,
"regression": 1
})
print(model)
五、正则化层:稳定训练与提升泛化能力
深度神经网络虽然强大,但也容易出现训练不稳定、过拟合、收敛慢等问题。正则化层正是为了解决这些问题而存在。
正则化不是“提取特征”的功能层,而是帮助模型更好训练、更强泛化的重要手段。
1. 批量归一化(nn.BatchNorm2d
)
适用于图像数据的 2D 特征图。
import torch
import torch.nn as nn
# 定义一个 BatchNorm 层,输入通道数是 16(对应某一层卷积的输出)
bn = nn.BatchNorm2d(num_features=16)
# 输入:batch_size=4, channels=16, height=32, width=32
x = torch.randn(4, 16, 32, 32)
# 前向传播
output = bn(x)
print("Output shape:", output.shape) # [4, 16, 32, 32]
参数说明:
参数 | 说明 |
---|---|
num_features |
输入通道数(如卷积输出通道数); |
eps |
避免除零的小常数; |
momentum |
用于更新均值/方差的动量; |
affine |
是否学习缩放(γ)和偏移(β); |
track_running_stats |
是否追踪运行时均值/方差。 |
2. 层归一化(nn.LayerNorm
)
适用于 Transformer、RNN 等结构。
import torch
import torch.nn as nn
# 假设输入形状为 [batch_size, features],例如 [4, 10]
x = torch.randn(4, 10)
# 对每个样本的 10 个特征做 LayerNorm
layer_norm = nn.LayerNorm(normalized_shape=10)
output = layer_norm(x)
print("Output shape:", output.shape) # [4, 10]
参数说明:
参数 | 说明 |
---|---|
normalized_shape |
要归一化的维度; |
eps |
避免除零; |
elementwise_affine |
是否学习缩放和偏移。 |
3. 实例归一化(nn.InstanceNorm2d
)
多用于风格迁移任务,每个样本独立归一化。
import torch
import torch.nn as nn
# 创建 InstanceNorm2d 层,假设通道数为 3(如 RGB 图像)
inst_norm = nn.InstanceNorm2d(num_features=3)
# 输入张量形状:[batch_size=2, channels=3, height=32, width=32]
x = torch.randn(2, 3, 32, 32)
# 前向传播
output = inst_norm(x)
print("Output shape:", output.shape) # [2, 3, 32, 32]
参数说明:
参数 | 说明 |
---|---|
num_features |
通道数; |
affine |
是否添加可学习参数 γ 和 β; |
track_running_stats |
是否追踪均值方差。 |
4. 组归一化(nn.GroupNorm
)
适用于小批量训练时的归一化操作。
import torch
import torch.nn as nn
# 将通道分为 4 组,每组归一化;一共 8 个通道
group_norm = nn.GroupNorm(num_groups=4, num_channels=8)
# 输入:[batch_size=2, channels=8, height=32, width=32]
x = torch.randn(2, 8, 32, 32)
output = group_norm(x)
print("Output shape:", output.shape) # [2, 8, 32, 32]
参数说明:
参数 | 说明 |
---|---|
num_groups |
分组数(如 4 组); |
num_channels |
通道数(需被 num_groups 整除); |
eps |
防止除以 0; |
affine |
是否包含可学习参数。 |
5. Dropout(nn.Dropout
)
训练时随机将一部分神经元输出设为 0,以此降低模型对特定路径的依赖。
import torch
import torch.nn as nn
dropout = nn.Dropout(p=0.5) # 50% 的概率将元素置为 0
x = torch.randn(4, 10)
# 模拟训练阶段
dropout.train()
out_train = dropout(x)
# 模拟推理阶段(推理时不会随机丢弃)
dropout.eval()
out_eval = dropout(x)
print("Input:\n", x)
print("After dropout (train):\n", out_train)
print("After dropout (eval):\n", out_eval)
参数说明:
参数 | 说明 |
---|---|
p |
丢弃的概率; |
inplace |
是否原地修改输入数据。 |
六、循环神经网络:处理序列信息的利器
对于文本、语音、时间序列等输入长度可变、具有时序关系的任务,循环神经网络(RNN)系列提供了天然的建模能力。
1. 基本结构:nn.RNN
、nn.LSTM
、nn.GRU
-
nn.RNN
最简单,记忆能力弱。RNN 是可以在序列中“记住历史”的神经网络
它的基本结构是:每个时刻的输出不仅取决于当前输入,还取决于上一个隐藏状态。
理解为:记录历史 → 分析当前 → 推测未来
import torch
import torch.nn as nn
rnn = nn.RNN(input_size=10, hidden_size=20, num_layers=1, batch_first=True)
# 输入:batch_size=3,序列长度=5,特征维度=10
x = torch.randn(3, 5, 10)
output, h_n = rnn(x)
print("RNN Output shape:", output.shape) # [3, 5, 20]
print("RNN Last hidden state shape:", h_n.shape) # [1, 3, 20]
-
nn.LSTM
引入门控机制,能记住长期信息。它的目标是解决 RNN “记忆太短”的问题机制:
遗忘门:决定忘记哪些旧信息;
输入门:决定当前有哪些信息要写入内存
输出门:决定从内存输出哪些信息类比:
RNN 就像是你脑子在记东西,但过一会儿你会忘;
LSTM 是你有个小本子,把重要的写进去,写之前还会判断“这重要不?”、“以前那段可以忘了吗?”
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=1, batch_first=True)
x = torch.randn(3, 5, 10)
output, (h_n, c_n) = lstm(x)
print("LSTM Output shape:", output.shape) # [3, 5, 20]
print("LSTM Last hidden state:", h_n.shape) # [1, 3, 20]
print("LSTM Last cell state:", c_n.shape) # [1, 3, 20]
-
nn.GRU
GRU 是一种能记住长期信息、又计算更快、更简单的循环神经网络。 LSTM 的“轻量级版本”。- 机制:
更新门:决定当前隐藏状态要保留多少旧信息;
重置门:决定当前输入要结合多少旧的记忆 |
- 机制:
gru = nn.GRU(input_size=10, hidden_size=20, num_layers=1, batch_first=True)
x = torch.randn(3, 5, 10)
output, h_n = gru(x)
print("GRU Output shape:", output.shape) # [3, 5, 20]
print("GRU Last hidden state:", h_n.shape) # [1, 3, 20]
2. 通用参数说明:
参数 | 说明 |
---|---|
input_size |
每个时间步输入的特征维度; |
hidden_size |
隐藏状态维度; |
num_layers |
层数; |
batch_first |
输入是否为 (batch, seq, dim) ; |
bidirectional |
是否双向; |
dropout |
层间 Dropout; |
七、嵌入层
1. 嵌入层(nn.Embedding
)
用于将离散的 ID 映射为稠密向量,尤其在自然语言处理(NLP)中广泛使用。
import torch
import torch.nn as nn
# 创建一个词嵌入层,包含 100 个词,每个词表示为 16 维向量
embedding = nn.Embedding(num_embeddings=100, embedding_dim=16)
# 模拟一个输入:batch_size=4,每个样本有 5 个“词”的索引(int)
input_indices = torch.tensor([
[1, 3, 5, 2, 9],
[4, 7, 6, 8, 0],
[1, 1, 1, 1, 1],
[2, 4, 6, 8, 10]
])
# 获取嵌入向量
output = embedding(input_indices)
print("Output shape:", output.shape) # [4, 5, 16]
- 参数说明
参数名 | 含义 |
---|---|
num_embeddings |
嵌入表中总词数(通常是词表大小) |
embedding_dim |
每个词嵌入向量的维度 |
padding_idx |
如果指定该索引,其嵌入向量会是全 0,不参与训练 |
max_norm |
限制每个嵌入向量的最大范数(用于防止梯度爆炸) |
八、多头注意力机制与自注意力(Self-Attention)
多头注意力机制是 Transformer 的核心组成,其背后正是自注意力机制(Self-Attention)的具体实现。
1. 自注意力机制原理:
每个位置的表示依赖于整个序列中的所有位置(即全局信息);
-
通过计算 Query、Key、Value 的加权组合实现:
Attention(Q, K, V) = softmax(QKᵀ / √d_k) * V
公式理解
对于每个输入向量 𝑥𝑖,
我们生成三个向量:
Query(查询向量) → 要问别人;
Key(键) → 别人暴露的信息;
Value(值) → 别人的真实内容;
输入: ["猫", "在", "沙发", "上", "睡觉"]
↓
每个词生成 Q/K/V 向量
↓
Q 和所有 K 计算注意力权重
↓
加权求和 V 向量作为输出
- 通俗理解
Self-Attention 就像你在读一句话时,自动决定关注哪些词来更好理解当前词。
比如
句子 “我今天心情非常好!”
如果你要理解 “好” 这个词,它和“心情”关系更大,而不是 “我” 。
权重的划分
当前词(Query)
好
其他词(Key)
我、今天、心情、非常、好
权重
0.1、0.1、0.4、0.3、0.1
"心情" 的权重 0.4
2. nn.MultiheadAttention
使用示例:
它就是 多个 Self-Attention 并行运行,每个有自己的一套 Q/K/V 权重,然后把它们拼起来。
nn.MultiheadAttention
= 多个 Self-Attention + 拼接 + 投影,是 Self-Attention 的 增强版 + 实用封装,在 Transformer 中标准使用。
import torch
import torch.nn as nn
# 假设每个 token 的嵌入维度是 64,注意力头数是 4
mha = nn.MultiheadAttention(embed_dim=64, num_heads=4, batch_first=True)
# 输入是一个 batch 的序列:[batch_size=2, seq_len=5, embed_dim=64]
x = torch.randn(2, 5, 64) # 可以看作是 query = key = value
# 自注意力:Q = K = V = x
output, attn_weights = mha(x, x, x)
print("Output shape:", output.shape) # [2, 5, 64]
print("Attention weights shape:", attn_weights.shape) # [2, 4, 5, 5]:batch, heads, target_len, source_len
参数说明:
参数 | 说明 |
---|---|
embed_dim |
输入/输出的特征维度(比如 512),会被分成多个 head
|
num_heads |
注意力头的数量,要求能整除 embed_dim (每个 head = embed_dim / num_heads ) |
dropout=0.0 |
注意力权重的 dropout (默认 0.0) |
bias=True |
是否为 Q/K/V 添加偏置(默认 True) |
add_bias_kv=False |
是否为 K/V 添加额外的 bias(可选机制) |
add_zero_attn=False |
是否在 attention 前加一个全零向量,默认 False (用于特殊处理) |
kdim |
Key 的维度,如果与 embed_dim 不同需指定 |
vdim |
Value 的维度,如果与 embed_dim 不同需指定 |
batch_first=False |
如果为 True,则输入/输出为 (batch, seq, dim) 而不是 (seq, batch, dim) |
九、损失函数与优化器
1. 常见损失函数
损失函数(Loss Function) 是用来评估模型的预测值与真实值之间的差异。
损失越小,模型表现越好;优化器的目标就是最小化损失。
-
nn.MSELoss(y_pred, y_true) 回归任务,均方误差
- 特点:
适用于回归任务;对异常值敏感(因为平方放大误差);用于输出是连续值的情况。 - 参数:
y_pred
: 预测值
y_true
:真实值
- 特点:
import torch
import torch.nn as nn
# 创建 MSELoss 实例
criterion = nn.MSELoss()
# 模拟模型输出和真实标签
y_pred = torch.tensor([[2.5], [0.0], [2.1]], dtype=torch.float)
y_true = torch.tensor([[3.0], [0.0], [2.0]], dtype=torch.float)
# 计算 MSE 损失
loss = criterion(y_pred, y_true)
print("MSE Loss:", loss.item()) # 输出一个标量
-
nn.CrossEntropyLoss(y_pred, y_true) 交叉熵损失
- 特点:
分类任务首选;
输入必须是 未归一化的 logits。logits 是未经过激活函数的输出,值域在 (−∞,+∞);
标签格式是 整数(非 one-hot)
- 特点:
-
nn.NLLLoss
是用于分类任务的 负对数似然 损失函数,适用于模型输出是对数概率(log-probabilities)的情况- 参数:
weight
: 每类的损失权重,用于类别不平衡(比如第0类比第1类重要
ignore_index
: 忽略指定标签的样本(不参与 loss 计算)
reduction
: mean(默认,平均)/ sum / none(逐元素输出)
- 参数:
常见搭配
nn.NLLLoss() ⬅️ 搭配 ➡️ nn.LogSoftmax(dim=1)
或
nn.CrossEntropyLoss() ⬅️ 它等价于 nn.NLLLoss + nn.LogSoftmax
import torch
import torch.nn as nn
# 假设 batch_size=3,类别数=4
log_softmax = nn.LogSoftmax(dim=1)
nll_loss = nn.NLLLoss()
# 模拟模型输出 logits(还未 softmax)
logits = torch.tensor([[2.0, 0.5, 0.1, 0.2],
[0.1, 2.1, 0.3, 0.4],
[1.0, 0.2, 3.0, 0.3]])
# 类别标签(每个样本的正确类别索引)
targets = torch.tensor([0, 1, 2]) # 每个值在 0~3 之间
# 对 logits 做 log softmax
log_probs = log_softmax(logits)
# 计算 NLLLoss
loss = nll_loss(log_probs, targets)
print("NLL Loss:", loss.item())
2. 优化器(包括 SGD、Adam、Adagrad 等)
优化器是模型参数更新的关键,PyTorch 提供了丰富的优化器模块,均位于
torch.optim
中。
1)SGD(随机梯度下降)
import torch
import torch.nn as nn
import torch.optim as optim
# 一个简单的线性模型:y = wx + b
model = nn.Linear(1, 1)
# 均方误差损失
criterion = nn.MSELoss()
# SGD 优化器,学习率 0.01
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 模拟训练数据
x = torch.tensor([[1.0], [2.0], [3.0]])
y = torch.tensor([[2.0], [4.0], [6.0]]) # y = 2x
# 训练 100 步
for epoch in range(100):
# 前向传播
y_pred = model(x)
# 计算损失
loss = criterion(y_pred, y)
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}, Loss: {loss.item():.4f}")
-
lr
:学习率; -
momentum
:加速收敛并抑制震荡; -
weight_decay
:L2 正则化,防止过拟合。
2)Adam(自适应矩估计)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-8)
-
betas
:一阶和二阶矩估计的指数衰减率; -
eps
:防止除零。 - Adam 结合了 Momentum 和 RMSprop 的优点,默认参数下性能稳定。
3)Adagrad、RMSprop(适用于稀疏/非平稳目标)
optimizer = torch.optim.Adagrad(model.parameters(), lr=0.01)
optimizer = torch.optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99)
优化器通用用法:
optimizer.zero_grad() # 清空上一步的梯度信息
loss.backward() # 反向传播,计算当前梯度
optimizer.step() # 根据梯度更新参数
3. 学习率调度器(torch.optim.lr_scheduler
)
学习率调度器用于动态调整学习率,提升模型收敛效率,常配合优化器一起使用。
常见调度策略:
-
StepLR
: 每隔固定 epoch 衰减一次; -
MultiStepLR
: 在指定 epoch 点衰减; -
ExponentialLR
: 指数衰减; -
ReduceLROnPlateau
: 验证集无进展时自动衰减; -
CosineAnnealingLR
: 余弦衰减,适用于高性能模型。
示例:StepLR
每隔固定 epoch 让学习率乘以一个系数:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR
# 简单模型和损失函数
model = nn.Linear(10, 1)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 每 10 个 epoch 将 lr 乘以 0.1
scheduler = StepLR(optimizer, step_size=10, gamma=0.1)
# 假数据
x = torch.randn(32, 10)
y = torch.randn(32, 1)
# 训练 30 个 epoch
for epoch in range(30):
# 前向传播
y_pred = model(x)
loss = criterion(y_pred, y)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 学习率调度器更新
scheduler.step()
# 打印当前学习率
current_lr = scheduler.get_last_lr()[0]
print(f"Epoch {epoch}, Loss: {loss.item():.4f}, LR: {current_lr:.6f}")
调度器常在每个 epoch 结束后调用 .step()
,动态调整当前学习率。
- SGD 支持 Momentum(动量);
- Adam 具备自适应学习率能力,效果稳定,使用广泛。
十、ResNet 与残差连接:深层网络的训练利器
在深度神经网络中,随着网络层数的增加,模型训练会遇到梯度消失或梯度爆炸问题,导致性能下降。ResNet(Residual Network)通过引入“残差连接”结构,有效缓解了这一问题,极大推动了深度网络的发展。
1. 残差结构的思想
传统网络层直接学习从输入到输出的映射函数 H(x)
,而 ResNet 改为学习残差函数 F(x) = H(x) - x
,也就是说:
y = F(x) + x
这种设计思想让网络可以更容易地拟合恒等映射,尤其在一些层实际上无需学习变化时,直接“跳过”成为最优选择。
2. 残差连接的实现方式
残差连接是通过“快捷路径(skip connection)”将输入直接加到输出上,如:
out = self.block(x) # 普通卷积层或非线性变换
out += x # 加上原始输入,实现跳跃连接
return F.relu(out) # 激活函数
在 PyTorch 中实现一个基本残差块:
class ResidualBlock(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(in_channels),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(in_channels)
)
def forward(self, x):
return F.relu(self.block(x) + x)
注意:当输入和输出维度不一致时,需使用
nn.Conv2d
进行升维或降维匹配。
3. ResNet 的应用与演化
- ResNet 最初提出了 ResNet-18/34/50/101 等系列模型,通过堆叠多个残差块构成深层网络;
- 在图像分类、目标检测、图像分割等任务中广泛使用;
- 也衍生出 ResNeXt、SE-ResNet 等变种模型,提升表达能力。
通过残差连接,ResNet 实现了网络加深与训练稳定性的双赢,使得“百层网络”变为现实,其思想也被广泛应用于 NLP、CV 等各类模型设计中。
y = F(x) + x
- F(x) 是网络学习的“残差”,x 是原始输入;
- 避免梯度消失,使深层网络更易训练;
- 可堆叠至 100+ 层。
示例:
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
def forward(self, x):
# 把原始输入保存下来
identity = x
# conv → bn → relu :学习“变化函数” 𝐹(𝑥):它告诉模型“输入应该如何调整”
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
# 残差
# 把变化量加到原始输入上,得到最终输出 𝐻(𝑥)=𝐹(𝑥)+𝑥
out = identity + out
out = self.relu(out)
return out
十一、模型参数管理与微调
1. 冻结参数
冻结前面层,只训练后面层:
import torch
import torch.nn as nn
# 定义一个简单模型
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
self.feature_extractor = nn.Sequential(
nn.Linear(100, 50),
nn.ReLU()
)
self.classifier = nn.Linear(50, 10)
def forward(self, x):
x = self.feature_extractor(x)
return self.classifier(x)
model = MyModel()
# 冻结 feature_extractor 的参数
for param in model.feature_extractor.parameters():
param.requires_grad = False
# 只训练 classifier 的参数
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=0.001)
# 检查哪些参数被冻结
for name, param in model.named_parameters():
print(f"{name}: requires_grad = {param.requires_grad}")
2. 解冻
# 解冻 feature_extractor
for param in model.feature_extractor.parameters():
param.requires_grad = True
3. 获取模型参数名与结构
for name, param in model.named_parameters():
print(name, param.shape)
输出示例:
layer1.0.conv1.weight torch.Size([64, 3, 3, 3])
-
layer1
层组; ResNet 的第 1 个残差层组 -
.0
第几个block; 这个组里的第 0 个残差块(block) -
.conv1
第几个conv层; 该残差块的第一个卷积层 -
.weight
哪个参数(权重); 卷积核权重张量(不是 bias)