PyTorch 深度学习专题【左扬精讲】—— torch.nn 模块:神经网络构建的核心框架
PyTorch 深度学习专题【左扬精讲】—— torch.nn 模块:神经网络构建的核心框架
在深度学习的世界里,PyTorch 已成为最受欢迎的框架之一,其灵活的动态计算图和直观的 API 设计让无数研究者爱不释手。而 torch.nn 模块,正是 PyTorch 中用于构建神经网络的 核心基础设施——从最基础的线性层到复杂的注意力机制,从损失函数到优化器接口,一切神经网络相关的能力都沉淀在这个模块中。
本文将系统性地剖析 torch.nn 模块的设计哲学、核心组件和使用要点。通过 What-How-Why 三段式讲解,让你彻底理解这个模块解决了什么问题、如何解决,以及为什么这样设计。
本文涉及的核心模块与概念:
├── torch.nn ← 神经网络构建核心模块
├── torch.nn.Module ← 所有网络层的基类
├── torch.nn.Parameter ← 可学习参数封装
├── torch.nn.Container
│ ├── nn.Sequential ← 顺序容器
│ ├── nn.ModuleList ← 动态模块列表
│ └── nn.ModuleDict ← 动态模块字典
├── torch.nn.Layers
│ ├── nn.Linear ← 全连接层
│ ├── nn.Conv1d/2d/3d ← 卷积层
│ ├── nn.LSTM/GRU ← 循环层
│ └── nn.Embedding ← 嵌入层
├── torch.nn.Activation ← 激活函数
├── torch.nn.Loss ← 损失函数
└── 正则化与归一化
├── nn.Dropout ← Dropout
├── nn.BatchNorm ← 批归一化
└── nn.LayerNorm ← 层归一化
学习重点:本文聚焦于 PyTorch 2.x 的 torch.nn 模块核心概念。通过本文,你将理解:(1) nn.Module 的设计哲学与参数管理机制;(2) 常见网络层的原理与适用场景;(3) 模型构建、训练与部署的最佳实践。
PyTorch 2.x torch.nn 神经网络 nn.Module 深度学习
一、torch.nn 模块概述
1.1 What — torch.nn 是什么?
torch.nn 是 PyTorch 官方提供的神经网络构建模块,它将深度学习中常用的网络组件封装为可复用的 Python 类。从功能上看,这个模块提供了三大类能力:
- 网络层(Layers):卷积层、池化层、全连接层、循环层等,它们是神经网络的基本 building block
- 损失函数(Loss Functions):MSELoss、CrossEntropyLoss 等,用于衡量模型预测与真实标签之间的差异
- 容器与工具:nn.Module 基类、nn.Sequential 顺序容器、参数初始化工具等
在 PyTorch 的模块层级中,torch.nn 位于 torch 的核心层级之下,专门负责神经网络模型的定义与组织。而 torch.nn.functional(常写作 F)则提供了这些组件的函数式接口,用于更细粒度的操作。
1.2 Why — 为什么需要 torch.nn?
为什么需要专门的神经网络构建模块?
深度学习模型的构建涉及大量重复性工作:参数的声明与管理、计算图的组织与构建、设备迁移(CPU/GPU)、序列化与反序列化等。如果没有统一的抽象层,开发者需要为每个模型重复编写这些基础设施代码,既繁琐又容易出错。
没有 torch.nn 会发生什么?
- 每次定义新模型都要手动管理 tensor 的创建、参数注册、梯度追踪
- 模型参数的设备迁移需要在每个 tensor 上单独调用 .to(device)
- 模型的序列化(保存/加载)需要开发者自己处理参数的映射关系
- 不同模型之间的代码复用性极差,每个项目都要"从零开始"
torch.nn 模块通过 nn.Module 基类,将这些通用能力抽象和封装,让开发者可以专注于模型本身的逻辑设计,而不用操心底层工程细节。
1.3 How — torch.nn 是如何设计的?
torch.nn 的设计哲学:组合优于继承,接口优于实现
nn.Module 是整个模块的核心基类,它采用组合模式——每个 Module 可以包含其他 Module 作为子模块。这种设计天然支持嵌套结构,让复杂网络的构建变得模块化、可组合。
torch.nn 的核心设计原则:
- 参数自动注册:通过 nn.Parameter 封装的参数会自动注册到模块的 parameters() 列表中
- 递归遍历:所有子模块的参数会被递归收集,支持任意嵌套结构
- 计算图整合:子模块的前向计算自动纳入整体计算图,支持自动求导
- 设备统一管理:通过 .to(device) 可以一次性迁移整个模型及其所有参数
import torch
import torch.nn as nn
# 示例:nn.Module 的组合设计
class SimpleNet(nn.Module):
def __init__(self):
super().__init__() # 调用父类构造函数
# 组合:线性层 + 激活函数作为子模块
self.linear = nn.Linear(10, 5) # 输入10维,输出5维
self.relu = nn.ReLU() # ReLU 激活
def forward(self, x):
# 前向传播:组合子模块的计算
x = self.linear(x)
x = self.relu(x)
return x
# 创建模型实例
model = SimpleNet()
print(model)
# 输出:SimpleNet(
# (linear): Linear(in_features=10, out_features=5, bias=True)
# (relu): ReLU()
# )
很多初学者对 nn.Linear(10, 5) 的两个参数感到困惑。本节用图解 + 数学 + 具象类比,帮你彻底吃透全连接层的维度变换机制。
一、一句话通俗解释
nn.Linear(10, 5) = 全连接层,它的工作本质是:
- 第一个参数 10:告诉网络"你的输入每个样本有 10 个数(10维特征)
- 第二个参数 5:告诉网络"处理完后,每个样本我要输出 5 个数(5维特征)"
👉 核心变换:原本每个样本是 10 维向量,经过全连接层,变成 5 维向量。这就是"10维→5维"的含义。
二、数学角度拆解:全连接层公式 y = xWᵀ + b
全连接层的本质是一次矩阵乘法加一次偏置加法:
┌─────────────────────────────────────────────────────────────────┐
│ y = x · Wᵀ + b │
│ │
│ 输入 x 权重 W 运算 输出 y │
│ [batch, 10] × [5, 10] = [batch, 5] [batch, 5] │
│ + [5] │
└─────────────────────────────────────────────────────────────────┘
逐项拆解:
- x:输入张量,形状 [batch_size, 10]。batch_size 是每批多少个样本,10 是每个样本多少个特征值
- W:权重矩阵,形状 [5, 10]。这是网络要学习的参数,第一维 5 匹配输出维度,第二维 10 匹配输入维度
- b:偏置向量,形状 [5]。同样是可学习参数,对输出每个维度加一个偏移
- y:输出张量,形状 [batch_size, 5]。每条样本从 10 维变成了 5 维
三、具象类比:10维→5维到底是什么感觉?
想象一下:
- 一个学生有 10 门课的成绩(语文、数学、英语 ... 共10个数字)
- 全连接层把 10 个分数综合成 5 个指标(逻辑思维分、表达能力分 ... 共5个数字)
- 这 5 个指标不是简单丢弃了 5 门课,而是用加权组合的方式,把 10 门课的信息压缩进 5 个综合维度里
所以:维度变换 ≠ 信息丢失,而是信息的重组与提炼。
四、结合代码走一遍完整数据流
import torch
import torch.nn as nn
model = SimpleNet()
# 造一批数据:2个样本,每个样本10维
x = torch.randn(2, 10) # shape: [2, 10]
print(f"输入 x 的形状: {x.shape}") # torch.Size([2, 10])
# ↑ batch=2(2条数据),每条10个特征
out = model(x)
print(f"输出 out 的形状: {out.shape}") # torch.Size([2, 5])
# ↑ 同样是2条数据,但每条变成了5个特征
# 打印模型结构,直观看维度
print(model)
# SimpleNet(
# (linear): Linear(in_features=10, out_features=5, bias=True)
# (relu): ReLU()
# )
# ↑ in=10 表示"我期待每个样本给我10个数"
# ↑ out=5 表示"处理完后我要输出5个数"
数据流的每一步:
Step 1: x = torch.randn(2, 10)
┌────────────────────────────────────────┐
│ 2条数据,每条10维 → shape = [2, 10] │
└────────────────────────────────────────┘
Step 2: self.linear(x) # 线性变换
┌────────────────────────────────────────┐
│ [2, 10] × [5, 10]ᵀ + [5] │
│ = [2, 5] │
│ 10维特征 → 5维特征 │
└────────────────────────────────────────┘
Step 3: self.relu(x) # 激活函数(不变维度)
┌────────────────────────────────────────┐
│ [2, 5] → [2, 5] (维度不变!) │
│ ReLU 负责把负数变成0,不改形状 │
└────────────────────────────────────────┘
Step 4: return x
┌────────────────────────────────────────┐
│ 最终输出:2条样本,每条5维 │
└────────────────────────────────────────┘
五、为什么要从10维映射到5维?三个核心原因
原因1:特征压缩与降维
原始 10 维特征可能有冗余或噪声。通过映射到更低维度,网络被迫提取最关键的信息,丢弃无关细节。
原因2:可学习的特征组合
这不是简单的"删掉5个数字",而是用权重矩阵做线性组合:每个输出维度都是 10 个输入维度的加权和(加偏置)。这意味着:
- 输出维度1 ≈ a₁×输入₁ + a₂×输入₂ + ... + a₁₀×输入₁₀
- 输出维度2 ≈ b₁×输入₁ + b₂×输入₂ + ... + b₁₀×输入₁₀
- ...以此类推
这些权重 (a₁, a₂...a₁₀), (b₁, b₂...b₁₀) 等都是通过反向传播自动学习的。
原因3:适配下游任务与计算效率
最终分类/回归任务可能只需要少量特征。5 维输出既保留了关键信息,又减少了后续层的参数量和计算量。
六、彻底厘清:最容易混淆的四个概念
混淆点1:维度 vs 批次
"10维"指的是单个样本的特征数量,和 batch_size 完全无关。batch=32、batch=128 都可以,只要每个样本是 10 维就行:
x1 = torch.randn(1, 10) # 1个样本,10维 → shape [1, 10]
x32 = torch.randn(32, 10) # 32个样本,每个10维 → shape [32, 10]
x1000 = torch.randn(1000, 10) # 1000个样本,每个10维 → shape [1000, 10]
# 三种情况都合法,Linear(10, 5) 都输出 [N, 5]
混淆点2:哪些操作会改变维度,哪些不会?
| 操作 | 是否改变维度 | 说明 |
|---|---|---|
| nn.Linear | ✅ 改变 | 通过权重矩阵投影改变维度 |
| nn.Conv2d | ✅ 改变(取决于 stride/padding) | 卷积核滑动改变 H×W |
| nn.ReLU / nn.Sigmoid | ❌ 不改变 | 逐元素操作,只改数值大小 |
| nn.MaxPool / nn.AvgPool | ✅ 改变(H×W维度) | 空间下采样 |
| nn.Flatten | ✅ 改变(多维→一维) | 展平操作 |
| view / reshape | ✅ 改变(形状重塑) | 重排维度,不改数据内容 |
混淆点3:Linear 的 bias 可以不要吗?
可以!在 nn.Linear(10, 5, bias=False) 中设置 bias=False,偏置向量就不会被创建。这常见于:
- 对标已有预训练权重(如 VGG 的某些层)
- 确信偏置不是必需的场景
混淆点4:10维输入,输出一定是5维吗?
是的,输出维度严格由第二个参数决定,和输入数据内容无关:
linear = nn.Linear(10, 5)
x_1000d = torch.randn(2, 1000) # 1000维输入 ❌ 会报错!
x_10d = torch.randn(2, 10) # 10维输入 ✅
x_5d = torch.randn(2, 5) # 5维输入 ❌ 会报错!
# Linear(10,5) 要求输入必须是 [*, 10],即最后一个维度必须是10
# 输出一定是 [*, 5],即最后一个维度一定是5
七、实战延伸:不同维度的组合场景
# 常见的维度变换链
class MLP(nn.Module):
def __init__(self):
super().__init__()
# 784维(28x28图像)→ 256维 → 128维 → 10维(分类)
self.net = nn.Sequential(
nn.Linear(784, 256), # 降维:784→256
nn.ReLU(),
nn.Linear(256, 128), # 降维:256→128
nn.ReLU(),
nn.Linear(128, 10) # 分类头:128→10
)
def forward(self, x):
return self.net(x)
# 验证维度变换
model = MLP()
x = torch.randn(4, 784) # 4个样本,784维特征
out = model(x)
print(out.shape) # torch.Size([4, 10])
# 维度流: [4, 784] → [4, 256] → [4, 128] → [4, 10]
本节要点:
- torch.nn 是 PyTorch 神经网络的构建核心,提供层、损失函数、容器等基础设施
- 通过 nn.Module 基类实现参数管理、设备迁移、序列化等通用能力
- 采用组合模式,支持嵌套结构,让复杂网络可模块化构建
二、nn.Module:神经网络的核心基类
2.1 What — nn.Module 是什么?
nn.Module 是 PyTorch 中所有神经网络模型的基类,它是整个 torch.nn 模块的核心设计。任何自定义的神经网络模型,都应该继承自 nn.Module。
从设计角度看,nn.Module 承担了五大核心职责:
| 职责 | 具体能力 | 解决的问题 |
|---|---|---|
| 结构化与模块化 | 支持嵌套子模块,递归遍历 | 复杂网络的组织与复用 |
| 参数管理 | 自动注册、收集参数 | 参数的统一管理与优化器绑定 |
| 设备管理 | .to(device) 统一迁移 | CPU/GPU 迁移的便捷操作 |
| 前向传播 | 统一的 forward() 接口 | 计算逻辑与调用方式的标准化 |
| 序列化 | state_dict() / load_state_dict() | 模型保存与加载 |
2.2 Why — 为什么需要 nn.Module?
nn.Module 的出现解决了什么问题?
在深度学习实践中,模型的复杂度不断提升:从早期的几层全连接网络,到如今动辄上百层的 Transformer。管理如此复杂的网络结构,如果没有统一的抽象层,工程难度将指数级上升。
没有 nn.Module 会发生什么?
- 手动维护 tensor 列表来存储参数:self.params = [W1, b1, W2, b2, ...]
- 手动遍历参数列表进行梯度清零:for p in self.params: p.grad = None
- 手动递归收集嵌套结构的参数:需要自己实现递归遍历逻辑
- 模型保存时手动序列化每个参数:需要自己管理参数名和顺序
nn.Module 通过自动化的机制,将这些重复性的"脏活累活"封装起来,让研究者可以专注于模型本身的创新。
2.3 How — nn.Module 的核心机制
2.3.1 参数注册机制
当你将一个 nn.Module 实例赋值给类属性时(如 self.linear = nn.Linear(...)),nn.Module 会自动将其注册为子模块,并递归收集其参数。
import torch
import torch.nn as nn
class MLP(nn.Module):
def __init__(self):
super().__init__()
# 当赋值给 self.xxx 时,自动注册为子模块
self.layer1 = nn.Linear(784, 256) # 注册:layer1.weight, layer1.bias
self.layer2 = nn.Linear(256, 10) # 注册:layer2.weight, layer2.bias
self.relu = nn.ReLU() # 注册:无参数(如 ReLU)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
return x
model = MLP()
# parameters() 会递归收集所有可学习参数
for name, param in model.named_parameters():
print(f"{name}: {param.shape}")
# 输出:
# layer1.weight: torch.Size([256, 784])
# layer1.bias: torch.Size([256])
# layer2.weight: torch.Size([10, 256])
# layer2.bias: torch.Size([10])
机制一:属性赋值自动注册
当你执行 self.layer = nn.Linear(...) 时,nn.Module.__setattr__ 会拦截这个赋值操作。如果检测到赋值为 nn.Module 或 nn.Parameter 实例,会自动将其加入内部管理结构(_modules 或 _parameters 字典)。
机制二:parameters() 的递归收集
parameters() 方法会递归遍历 _modules 字典,收集每个子模块的参数。参数名通过层级拼接形成,如 "block1.conv.weight"。
机制总结:2 个关键字典 + 1 个递归遍历
- 2 个关键字典:_parameters(直接参数)+ _modules(子模块)
- 1 个递归遍历:named_parameters() 深度优先遍历所有子模块
避坑提醒:
- 不要用普通 Python list 装 Module:self.layers = [nn.Linear(...), nn.Linear(...)] 不会自动注册,应该用 nn.ModuleList
- 注册顺序影响参数遍历顺序:同一层级按定义顺序遍历
2.3.2 forward 方法与计算图
forward() 方法是 nn.Module 的核心抽象接口,它定义了模型的前向计算逻辑。当你调用 model(x) 时,PyTorch 会自动调用 model.forward(x)。
import torch
import torch.nn as nn
class Net(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.bn = nn.BatchNorm2d(16)
self.relu = nn.ReLU()
self.fc = nn.Linear(16 * 32 * 32, 10)
# forward 方法定义了前向传播的计算逻辑
def forward(self, x):
x = self.conv(x) # 卷积
x = self.bn(x) # 批归一化
x = self.relu(x) # 激活
x = x.view(x.size(0), -1) # flatten
x = self.fc(x) # 全连接
return x
# 创建模型并执行前向传播
model = Net()
x = torch.randn(1, 3, 32, 32) # batch=1, channel=3, 32x32
output = model(x) # 等价于 model.forward(x)
# 自动构建计算图,支持反向传播
print(f"Output shape: {output.shape}") # torch.Size([1, 10])
注意:不要直接调用 model.forward(x),而应该使用 model(x)。前者不会触发 __call__ 中的钩子函数(hooks)和异常处理机制。
2.3.3 实例化模型与访问参数
import torch
import torch.nn as nn
# 定义一个简单的卷积网络
class ConvNet(nn.Module):
def __init__(self):
super().__init__()
# 卷积层:输入3通道,输出16通道,卷积核3x3
self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
self.bn1 = nn.BatchNorm2d(16)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.bn2 = nn.BatchNorm2d(32)
self.fc = nn.Linear(32 * 8 * 8, 10)
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2, 2)
def forward(self, x):
x = self.pool(self.relu(self.bn1(self.conv1(x))))
x = self.pool(self.relu(self.bn2(self.conv2(x))))
x = x.view(x.size(0), -1) # 展平
x = self.fc(x)
return x
model = ConvNet()
# ========== 访问参数的方法 ==========
# 1. 获取所有参数(tensor 列表)
params = list(model.parameters())
print(f"Total parameters: {len(params)}")
# 2. 获取带名称的参数(推荐)
for name, param in model.named_parameters():
print(f"{name}: {param.shape}, requires_grad={param.requires_grad}")
# 3. 访问特定层的参数
conv1_weight = model.conv1.weight # 等价于 model.state_dict()['conv1.weight']
print(f"Conv1 weight shape: {conv1_weight.shape}")
# 4. 访问 buffer(非梯度参数,如 BatchNorm 的 running_mean)
for name, buf in model.named_buffers():
print(f"Buffer {name}: {buf.shape}")
2.3.4 设备管理:.to 方法
.to(device) 方法是 nn.Module 提供的统一设备迁移接口,它会递归地将模型的所有参数和缓冲区迁移到指定设备。
import torch
import torch.nn as nn
model = ConvNet()
# ========== 设备管理 ==========
# 检查可用设备
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"CUDA device count: {torch.cuda.device_count()}")
# 迁移到 CPU(默认)
model_cpu = model.to('cpu')
# 迁移到 CUDA GPU(如果有)
if torch.cuda.is_available():
device = torch.device('cuda:0')
model_gpu = model.to(device)
print(f"Model is now on: {next(model_gpu.parameters()).device}")
# 或者更简洁的方式
model = model.to('cuda')
print(f"Model device: {next(model.parameters()).device}")
# 在 GPU 上创建输入,传入模型
x = torch.randn(1, 3, 32, 32).to('cuda')
output = model(x)
print(f"Output on: {output.device}")
小贴士:设备迁移是递归的,这意味着你只需要调用一次 model.to(device),模型的所有参数、缓冲区和子模块都会自动迁移。但注意,输入数据也需要手动迁移到同一设备:x = x.to(device)。
2.3.5 模型模式:train() 和 eval()
train() 和 eval() 方法用于切换模型的运行模式,它们主要影响以下行为:
- Dropout:train 模式启用 Dropout(随机丢弃神经元),eval 模式关闭
- BatchNorm:train 模式使用 batch 统计量更新 running stats,eval 模式使用固定的统计量
import torch
import torch.nn as nn
class NetWithDropout(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(100, 50)
self.dropout = nn.Dropout(p=0.5) # 50% 概率丢弃
self.fc2 = nn.Linear(50, 10)
self.bn = nn.BatchNorm1d(50)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.bn(x)
x = self.dropout(x) # 训练时丢弃,推理时不丢弃
x = self.fc2(x)
return x
model = NetWithDropout()
# ========== 训练模式 ==========
model.train() # 切换到训练模式
print(f"Training mode: dropout={model.dropout.training}") # True
# ========== 推理模式 ==========
model.eval() # 切换到评估模式
print(f"Eval mode: dropout={model.dropout.training}") # False
# eval 模式下,with torch.no_grad() 可以进一步禁用梯度计算
with torch.no_grad():
output = model(torch.randn(1, 100))
print(f"Output (inference): {output}")
本节要点:
- nn.Module 是所有神经网络的基类,提供五大核心能力:结构化、参数管理、设备管理、前向传播、序列化
- forward() 方法定义计算逻辑,通过 model(x) 调用
- .to(device) 统一迁移模型到指定设备
- train()/eval() 切换模式,影响 Dropout 和 BatchNorm 的行为
三、nn.Module 的子类:常用容器与容器类
3.1 nn.Sequential — 顺序容器
nn.Sequential 是最常用的容器类,它按照传入的顺序依次执行各个子模块。它的本质是:将多个层组合成一个"流水线",数据从第一层流入,经过层层处理,从最后一层流出。
import torch
import torch.nn as nn
# 方式一:直接传入层序列(按索引访问)
model = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=3, padding=1), # 卷积层
nn.BatchNorm2d(16), # 批归一化
nn.ReLU(), # 激活函数
nn.MaxPool2d(2, 2), # 池化层
nn.Conv2d(16, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1)), # 自适应池化
nn.Flatten(),
nn.Linear(32, 10)
)
# 按索引访问层:model[0] 是第一个卷积层
print(model[0]) # Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
print(model[0].weight.shape) # 访问第一个卷积层的权重
# 方式二:使用 OrderedDict 给层命名(按名称访问)
from collections import OrderedDict
model = nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(3, 16, 3, padding=1)),
('bn1', nn.BatchNorm2d(16)),
('relu1', nn.ReLU()),
('pool', nn.MaxPool2d(2, 2)),
('conv2', nn.Conv2d(16, 32, 3, padding=1)),
('bn2', nn.BatchNorm2d(32)),
('relu2', nn.ReLU()),
('gap', nn.AdaptiveAvgPool2d((1, 1))),
('flatten', nn.Flatten()),
('fc', nn.Linear(32, 10))
]))
# 前向传播
x = torch.randn(1, 3, 32, 32)
output = model(x)
print(f"Output shape: {output.shape}") # torch.Size([1, 10])
# 通过名称访问层(仅 OrderedDict 方式支持)
print(model.conv1) # 访问 conv1 层
print(model.bn1.weight) # 访问 bn1 的 weight 参数
3.2 nn.ModuleList 和 nn.ModuleDict
当需要动态组织多个层时,nn.ModuleList 和 nn.ModuleDict 提供了更灵活的组织方式。
import torch
import torch.nn as nn
# ========== nn.ModuleList:模块列表,通过索引访问 ==========
class DynamicMLP(nn.Module):
def __init__(self, num_layers, input_dim, hidden_dim, output_dim):
super().__init__()
# 创建多个隐藏层
self.layers = nn.ModuleList([
nn.Linear(input_dim if i == 0 else hidden_dim, hidden_dim)
for i in range(num_layers)
])
self.output_layer = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
for layer in self.layers:
x = torch.relu(layer(x))
return self.output_layer(x)
model = DynamicMLP(num_layers=5, input_dim=784, hidden_dim=256, output_dim=10)
# ========== nn.ModuleDict:模块字典,通过键访问 ==========
class FlexibleNet(nn.Module):
def __init__(self):
super().__init__()
self.encoders = nn.ModuleDict({
'cnn': nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1),
nn.ReLU()
),
'mlp': nn.Sequential(
nn.Linear(784, 256),
nn.ReLU()
)
})
self.classifier = nn.Linear(64 + 256, 10)
def forward(self, x, mode='cnn'):
if mode == 'cnn':
x = self.encoders['cnn'](x)
elif mode == 'mlp':
x = x.view(x.size(0), -1)
x = self.encoders['mlp'](x)
else:
# 双路编码
cnn_feat = self.encoders['cnn'](x)
mlp_feat = x.view(x.size(0), -1)
mlp_feat = self.encoders['mlp'](mlp_feat)
x = torch.cat([cnn_feat.flatten(1), mlp_feat], dim=1)
return self.classifier(x)
注意:必须使用 nn.ModuleList 和 nn.ModuleDict,而不是 Python 原生的 list 和 dict。只有前者才会正确注册子模块,后者的内容不会被 PyTorch 识别为模型参数。
3.3 自定义容器
你也可以继承 nn.Module 创建自定义容器,以适应特殊的模型结构需求。
import torch
import torch.nn as nn
# ========== 自定义残差容器 ==========
class ResidualBlock(nn.Module):
"""残差块:输出 = f(x) + x"""
def __init__(self, channels):
super().__init__()
self.conv = nn.Conv2d(channels, channels, 3, padding=1)
self.bn = nn.BatchNorm2d(channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
residual = x # 保存输入(恒等映射)
out = self.conv(x)
out = self.bn(out)
out = out + residual # 残差连接
out = self.relu(out)
return out
# ========== 自定义多分支容器 ==========
class MultiBranchNet(nn.Module):
"""多分支网络"""
def __init__(self, in_channels):
super().__init__()
# 三个不同深度的分支
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, 32, 1),
nn.ReLU()
)
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, 32, 1),
nn.Conv2d(32, 32, 3, padding=1),
nn.ReLU()
)
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, 32, 1),
nn.Conv2d(32, 32, 5, padding=2),
nn.ReLU()
)
self.fusion = nn.Conv2d(96, 64, 1) # 融合三个分支
def forward(self, x):
b1 = self.branch1(x)
b2 = self.branch2(x)
b3 = self.branch3(x)
# 通道维度拼接
fused = torch.cat([b1, b2, b3], dim=1)
return self.fusion(fused)
本节要点:
- nn.Sequential:顺序容器,适合线性堆叠的层
- nn.ModuleList:动态列表,适合层数需要程序控制的场景
- nn.ModuleDict:动态字典,适合需要按名称选择层的场景
- 可以继承 nn.Module 创建自定义容器
四、torch.nn 中的 Layers(网络层)
4.1 What — 什么是 Layers?
在 PyTorch 中,Layer(层)是神经网络的基本构建单元,每个层封装了一组参数和一个数学变换。从数据流的角度看:层接收输入张量,经过数学运算,输出变换后的张量。
4.2 Why — 为什么要封装成 Layer?
Layer 封装解决了什么问题?
神经网络的核心是数学运算的组合。如果每次构建网络都要从头编写矩阵乘法、激活函数、梯度计算等代码,不仅效率低下,还容易出错。将这些运算封装成标准化的"层",可以实现:
- 代码复用:一次定义,到处使用
- 语义明确:层名即功能,见名知意
- 参数管理:层的参数自动注册,便于优化器绑定
- 模块化设计:复杂网络由简单层组合而成
4.3 核心 Layers 一览
| 类别 | 层名称 | 一句话说明 | 典型应用场景 |
|---|---|---|---|
| 全连接层 | nn.Linear | 输入与权重的矩阵乘加变换 | MLP、分类器的最后一层 |
| nn.Bilinear | 双线性变换:y = x1·W·x2 + b | 推荐系统、注意力机制 | |
| 卷积层 | nn.Conv1d | 一维卷积,适合序列数据 | 文本分类、语音处理 |
| nn.Conv2d | 二维卷积,适合图像 | 图像分类、目标检测 | |
| nn.Conv3d | 三维卷积,适合视频/体数据 | 视频理解、3D 医学影像 | |
| nn.ConvTranspose2d | 转置卷积(上采样) | 语义分割、生成对抗网络 | |
| 循环层 | nn.RNN | 基础循环神经网络 | 简单序列建模(已较少使用) |
| nn.LSTM | 长短期记忆网络 | 机器翻译、文本生成 | |
| nn.GRU | 门控循环单元 | 语音识别、时间序列 | |
| nn.RNNCell | RNN 单步计算单元 | 自定义 RNN 逻辑 | |
| 嵌入层 | nn.Embedding | 离散ID映射为稠密向量 | NLP、推荐系统 |
| nn.EmbeddingBag | 嵌入的加权求和/平均 | TextCNN、句子嵌入 | |
| 池化层 | nn.MaxPool2d | 最大值池化,下采样 | 几乎所有 CNN |
| nn.AdaptiveAvgPool2d | 自适应池化,输出固定尺寸 | 特征提取网络的输出对齐 |
4.4 常见层详解
4.4.1 nn.Linear — 全连接层
nn.Linear 实现:y = x·W^T + b,即线性变换(也称仿射变换)。它是神经网络中最基础的层,几乎所有复杂网络都会用到它。
import torch
import torch.nn as nn
# 创建一个全连接层:输入特征10维,输出特征5维
linear = nn.Linear(in_features=10, out_features=5, bias=True)
# 输入:张量 (batch_size, 10)
x = torch.randn(32, 10) # batch=32, 10维输入
output = linear(x)
print(f"Input shape: {x.shape}") # torch.Size([32, 10])
print(f"Output shape: {output.shape}") # torch.Size([32, 5])
# 查看层参数
print(f"Weight shape: {linear.weight.shape}") # torch.Size([5, 10])
print(f"Bias shape: {linear.bias.shape}") # torch.Size([5])
4.4.2 nn.Conv2d — 二维卷积层
nn.Conv2d 是卷积神经网络(CNN)的核心,用于提取图像的局部空间特征。它通过滑动窗口的方式,将卷积核与输入进行局部连接,实现参数共享和局部感受野。
import torch
import torch.nn as nn
# 创建卷积层
# 输入:3通道(如RGB),输出:16通道,卷积核3x3
conv = nn.Conv2d(
in_channels=3, # 输入通道数
out_channels=16, # 输出通道数(卷积核数量)
kernel_size=3, # 卷积核尺寸
stride=1, # 步长(默认1)
padding=1, # 填充(保持尺寸可设为 kernel_size//2)
bias=True # 是否使用偏置
)
# 输入:张量 (batch, channels, height, width)
x = torch.randn(1, 3, 32, 32) # batch=1, 3通道, 32x32图像
output = conv(x)
print(f"Input shape: {x.shape}") # torch.Size([1, 3, 32, 32])
print(f"Output shape: {output.shape}") # torch.Size([1, 16, 32, 32])
print(f"卷积核数量: {conv.out_channels}") # 16
print(f"卷积核尺寸: {conv.kernel_size}") # (3, 3)
卷积层参数详解:
- in_channels:输入通道数,RGB图像为3,灰度图为1
- out_channels:输出通道数,即卷积核的数量,决定提取多少种特征
- kernel_size:卷积核尺寸,常用3x3、5x5、7x7
- stride:滑动步长,>1 时会减小输出尺寸(下采样)
- padding:边缘填充,控制输出尺寸
- groups:分组卷积,控制输入输出通道的连接方式
4.4.3 nn.LSTM — 长短期记忆网络
nn.LSTM 是处理序列数据的核心组件,通过门控机制解决传统 RNN 的梯度消失问题,能够学习长距离依赖关系。
import torch
import torch.nn as nn
# 创建 LSTM 层
lstm = nn.LSTM(
input_size=256, # 输入特征维度
hidden_size=512, # 隐藏状态维度
num_layers=2, # LSTM 层数
batch_first=True, # True: (batch, seq, feature),否则 (seq, batch, feature)
dropout=0.1, # 层间 Dropout
bidirectional=True # 双向 LSTM
)
# 输入序列
batch_size = 4
seq_len = 10
input_size = 256
# (batch, seq_len, input_size)
x = torch.randn(batch_size, seq_len, input_size)
# 前向传播
# output: (batch, seq_len, hidden_size * directions)
# h_n: (num_layers * directions, batch, hidden_size)
# c_n: (num_layers * directions, batch, hidden_size)
output, (h_n, c_n) = lstm(x)
print(f"Input shape: {x.shape}") # torch.Size([4, 10, 256])
print(f"Output shape: {output.shape}") # torch.Size([4, 10, 1024]) 1024=512*2(bidirectional)
print(f"Hidden shape: {h_n.shape}") # torch.Size([4, 4, 512]) 4=2*2(num_layers*bidirectional), 4=batch, 512=hidden
print(f"Cell shape: {c_n.shape}") # torch.Size([4, 4, 512]) 同 h_n
4.4.4 nn.Embedding — 嵌入层
nn.Embedding 将离散的整数索引(如词表中的单词ID)映射为连续的稠密向量,是 NLP 任务的基础组件。
import torch
import torch.nn as nn
# 创建嵌入层:词表大小10000,嵌入维度300
embedding = nn.Embedding(
num_embeddings=10000, # 词表大小(索引范围:0~9999)
embedding_dim=300, # 嵌入向量维度
padding_idx=0 # 填充索引(可选,该索引的向量全零)
)
# 输入:整数索引序列 (batch, seq_len)
# 常见场景:句子中每个词的ID
batch_size = 8
seq_len = 20
x = torch.randint(0, 10000, (batch_size, seq_len)) # 随机生成词索引
output = embedding(x)
print(f"Input shape: {x.shape}") # torch.Size([8, 20])
print(f"Output shape: {output.shape}") # torch.Size([8, 20, 300])
# 获取单个词的嵌入
word_id = torch.tensor([42]) # 单词ID
word_vec = embedding(word_id) # torch.Size([1, 300])
print(f"Single word embedding: {word_vec.shape}")
4.5 How — 如何选择合适的 Layer?
场景一:图像分类任务
典型结构:Conv2d + BatchNorm2d + ReLU + MaxPool2d 循环堆叠,最后用 AdaptiveAvgPool2d + Linear 输出分类结果。
# 图像分类网络结构示意
class ImageNet(nn.Module):
def __init__(self, num_classes=1000):
super().__init__()
self.features = nn.Sequential(
# Block 1: 224 -> 112
nn.Conv2d(3, 64, 7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(3, stride=2, padding=1),
# Block 2-4: 继续堆叠...
)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Linear(2048, num_classes)
场景二:NLP 文本分类
典型结构:Embedding + LSTM/Transformer + Linear。嵌入层将词ID转为向量,LSTM/Transformer 编码序列,最后 Linear 输出分类。
# 文本分类网络
class TextClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
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, text):
# text: (batch, seq_len)
embedded = self.embedding(text) # (batch, seq_len, embed_dim)
_, (hidden, _) = self.lstm(embedded)
# 双向 LSTM 的最后隐藏状态拼接
hidden = torch.cat([hidden[-2], hidden[-1]], dim=1)
return self.fc(hidden)
场景三:推荐系统 / 特征交叉
使用 Embedding 将用户/物品ID映射为向量,通过 Linear 或更复杂的交互层学习特征组合。
本节要点:
- Layer 是神经网络的基本构建单元,封装参数和数学变换
- 全连接层(Linear):通用变换,用于特征投影和分类
- 卷积层(Conv2d):提取局部空间特征,用于图像
- 循环层(LSTM):处理序列数据,捕获时序依赖
- 嵌入层(Embedding):离散ID转稠密向量,用于NLP
五、激活函数与损失函数
5.1 激活函数(Activation Functions)
激活函数为网络引入非线性,让网络能够学习复杂的模式。没有激活函数,多层网络等价于单层线性变换。
| 激活函数 | 公式 | 特点 | 适用场景 |
|---|---|---|---|
| nn.ReLU | max(0, x) | 计算高效,梯度稀疏 | 几乎所有场景(首选) |
| nn.LeakyReLU | x if x>0 else 0.01x | 负区间有梯度,避免"死神经元" | GAN、生成模型 |
| nn.Sigmoid | 1/(1+e^{-x}) | 输出0-1,易解释 | 二分类输出层 |
| nn.Tanh | (e^x - e^{-x})/(e^x + e^{-x}) | 输出-1~1,零中心 | LSTM门控、文本生成 |
| nn.Softmax | e^{x_i}/Σe^{x_j} | 多分类概率归一化 | 多分类输出层 |
import torch
import torch.nn as nn
import torch.nn.functional as F
# 使用方式一:作为 nn.Module 实例(可学习参数或有状态)
relu = nn.ReLU()
x = torch.randn(4, 4)
output = relu(x)
# 使用方式二:直接用 functional API(无状态,常用在 Sequential 中)
output = F.relu(x)
# Softmax 使用示例
# 多分类:最后一层 logits,dim=-1 表示在最后一个维度做 Softmax
logits = torch.randn(4, 10) # batch=4, 10个类别
probs = F.softmax(logits, dim=-1) # 转概率
print(f"Sum per row: {probs[0].sum().item()}") # ≈ 1.0
5.2 损失函数(Loss Functions)
损失函数衡量模型预测与真实标签之间的差异,是训练优化的目标。
| 损失函数 | 适用场景 | 一句话说明 |
|---|---|---|
| nn.MSELoss | 回归任务 | 预测值与真实值的均方误差 |
| nn.CrossEntropyLoss | 多分类 | 包含 Softmax + 交叉熵,一步到位 |
| nn.BCEWithLogitsLoss | 二分类、多标签 | Sigmoid + 二进制交叉熵,数值稳定 |
| nn.BCELoss | 二分类(已 logits 需先 Sigmoid) | 纯二进制交叉熵 |
| nn.L1Loss | 回归(对异常值鲁棒) | MAE,绝对值误差 |
| nn.SmoothL1Loss | 回归(如目标检测边框) | L1 和 L2 的平滑组合 |
import torch
import torch.nn as nn
# ========== 多分类:CrossEntropyLoss ==========
# 输入:原始 logits(未经 Softmax),形状 (batch, num_classes)
# 目标:类别索引,形状 (batch,)
criterion_ce = nn.CrossEntropyLoss()
logits = torch.randn(4, 10) # batch=4, 10个类别
targets = torch.tensor([3, 1, 7, 5]) # 真实类别索引
loss = criterion_ce(logits, targets)
print(f"CrossEntropy Loss: {loss.item():.4f}")
# ========== 二分类:BCEWithLogitsLoss ==========
# 输入:原始 logits,形状 (batch,)
# 目标:0/1 标签,形状 (batch,)
criterion_bce = nn.BCEWithLogitsLoss()
logits = torch.randn(4) # 二分类 logits
targets = torch.tensor([1., 0., 1., 0.])
loss = criterion_bce(logits, targets)
print(f"BCE Loss: {loss.item():.4f}")
# ========== 回归:MSELoss ==========
criterion_mse = nn.MSELoss()
predictions = torch.randn(4, 1) # 预测值
targets = torch.randn(4, 1) # 真实值
loss = criterion_mse(predictions, targets)
print(f"MSE Loss: {loss.item():.4f}")
注意:当你使用 nn.CrossEntropyLoss 时,不要在模型最后再加 nn.Softmax!因为 CrossEntropyLoss 内部已经包含了 Softmax 计算,外部再加会导致数值不稳定。
六、正则化与归一化
6.1 Dropout — 随机丢弃,防止过拟合
nn.Dropout 是深度学习中最常用的正则化技术之一。它的核心行为是:在训练阶段,按照概率 p 随机将一部分神经元的输出置为 0,从而强迫网络不能依赖少数神经元,必须学习更鲁棒、更冗余的特征表示。
其数学形式可简化为:
r ~ Bernoulli(1 - p) # 以概率 (1-p) 生成 0/1 掩码
output = input * r / (1-p) # 训练时掩码乘以输入,并缩放
# 推理时不使用掩码,也不缩放
- 训练时:随机“关掉”一部分神经元,输出被掩码遮住
- 推理/验证时:Dropout 关闭,所有神经元都参与计算,不缩放
问题背景:过拟合(overfitting)
神经网络在训练集上表现很好,但在测试集上表现明显变差,说明模型“记住了训练数据”,而不是学到了“可泛化的规律”。
为什么 Dropout 能缓解过拟合?
- 打破神经元共适应:如果没有 Dropout,某些神经元可能总是依赖另一些神经元一起工作。Dropout 让每次前向传播的网络结构都略有不同,迫使每个神经元都独立地学会有用的特征。
- 集成学习视角:每次 dropout 都相当于一个“随机子网络”,每次训练不同子网络;推理时使用全部神经元,可视为所有这些子网络的“几何平均”,起到 bagging 效果。
- 减少特征之间的复杂共适应:避免少数神经元把错误放大到整个网络,提高整体鲁棒性。
对比:不使用 Dropout 的风险
不使用 Dropout:
- 神经元容易形成依赖对,一个失效会导致另一个也失效
- 复杂共适应会让模型对训练集噪声过度敏感
- 测试集准确率通常低于训练集 5%~20%
使用 Dropout 后:
- 网络鲁棒性提升,对噪声更不敏感
- 测试集准确率更接近训练集,差距缩小
- 对深层网络尤其有效,越深收益越大
6.1.3 怎么做:常见用法与调参经验
基本用法:
self.dropout = nn.Dropout(p=0.5) # 50% 丢弃率
训练与推理模式:
model.train() # 训练模式:Dropout 生效,随机丢弃
model.eval() # 推理模式:Dropout 关闭,所有神经元参与
使用位置:
- 全连接层之后、激活函数之后
- 通常在“宽而深”的层后使用,如 `fc → ReLU → Dropout`
- 卷积层后也可用,但现代 CNN 更常用 BatchNorm
常见 p 值:
| 场景 | 推荐 p | 说明 |
|---|---|---|
| 全连接层 | 0.3 ~ 0.6 | 默认常用 0.5;参数越多,p 可稍高 |
| 卷积层 | 0.1 ~ 0.3 | 卷积层本身参数共享,过拟合风险相对低 |
| 输入层 | 0.1 ~ 0.2 | 仅用于噪声较大的输入 |
进阶:Inverted Dropout(PyTorch 实现方式)
PyTorch 在训练时做“反向缩放”:不是推理时放大,而是训练时除以 (1-p)。这样推理阶段无需做任何调整,代码更简洁:
训练时:output = input * mask / (1-p) # 期望值保持原量级
推理时:output = input # 直接使用,无需缩放
完整示例:
import torch
import torch.nn as nn
class DropoutNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.dropout1 = nn.Dropout(p=0.5) # 50% 丢弃率
self.fc2 = nn.Linear(256, 128)
self.dropout2 = nn.Dropout(p=0.3) # 30% 丢弃率
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.dropout1(x) # 训练时随机丢弃,推理时通过
x = torch.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x)
return x
model = DropoutNet()
# 训练时
model.train() # Dropout 生效
out = model(x)
# 推理时
model.eval() # Dropout 关闭
out = model(x)
6.1.4 常见误区
- 推理时忘记切 mode:始终调用 model.eval(),否则 Dropout 在推理时仍会随机丢弃神经元,导致结果不稳定
- Dropout 不是越大越好:p 过高会让网络欠拟合,p 过低则正则化不足,需要根据验证集调参
- BatchNorm 和 Dropout 不要随便堆叠:在 ResNet/Transformer 等结构中,两者可能冲突,需要谨慎设计
6.2 BatchNorm — 批归一化
nn.BatchNorm 是一类对 batch 维度进行均值方差归一化的层。它把当前 batch 内每个特征的分布“拉回”到均值 0、方差 1 的标准正态分布,从而稳定训练、加速收敛、缓解梯度消失/爆炸。
对每个特征 channel c,BatchNorm 执行:
μ_c = (1/N) Σ x_i,c # batch 均值
σ²_c = (1/N) Σ (x_i,c - μ_c)² # batch 方差
x̂_i,c = (x_i,c - μ_c) / sqrt(σ²_c + ε) # 归一化
y_i,c = γ_c · x̂_i,c + β_c # 可学习的缩放和平移
- μ, σ²:当前 batch 的统计量
- γ, β:可学习参数,让网络可以“undo”归一化,保留表达能力
- ε:极小值,防止除零
变体:
| 类名 | 输入形状 | 适用场景 |
|---|---|---|
| BatchNorm1d | [N, C] 或 [N, C, L] | 全连接层、1D 时序 |
| BatchNorm2d | [N, C, H, W] | 2D 图像 CNN |
| BatchNorm3d | [N, C, D, H, W] | 3D 视频/体数据 |
问题背景:Internal Covariate Shift(ICS)
在深层网络中,前面层的参数更新会导致后面层输入分布发生变化。这种“分布漂移”让后续层必须不断重新适应新的输入分布,导致训练缓慢、梯度不稳定。
BatchNorm 的四大收益:
- 稳定训练:把每层输入拉回到稳定的分布,减少梯度爆炸/消失
- 加速收敛:允许使用更大的学习率,epoch 更少即可收敛
- 降低对初始化的敏感度:不再需要精心调参初始权重
- 轻微正则化:batch 统计量带有噪声,带来类似 Dropout 的正则效果
直观理解:
没有 BatchNorm:
第 1 层输出: 均值 0.5,方差 4.2 → 第 2 层要适应这个分布
第 1 层更新后: 均值 -0.3,方差 1.1 → 第 2 层又要重新适应
每一层都在不断“搬家”,训练很慢
加入 BatchNorm 后:
第 1 层输出: 强制变成 均值 0,方差 1
第 2 层始终接收稳定分布,训练稳定且快速
6.2.3 怎么做:代码与常见用法
在 CNN 中:
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
self.bn1 = nn.BatchNorm2d(64) # num_features = 输出通道数
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2)
def forward(self, x):
x = self.pool(self.relu(self.bn1(self.conv1(x))))
return x
x = torch.randn(32, 3, 224, 224) # batch=32
out = model(x)
print(out.shape) # torch.Size([32, 64, 112, 112])
在 MLP 中:
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.bn1 = nn.BatchNorm1d(256)
self.fc2 = nn.Linear(256, 128)
self.bn2 = nn.BatchNorm1d(128)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.bn1(self.fc1(x)))
x = torch.relu(self.bn2(self.fc2(x)))
return self.fc3(x)
x = torch.randn(32, 784)
out = model(x)
print(out.shape) # torch.Size([32, 10])
运行时统计量:
bn2d = nn.BatchNorm2d(num_features=64)
x = torch.randn(32, 64, 32, 32)
x = bn2d(x)
print(f"Output shape: {x.shape}") # torch.Size([32, 64, 32, 32])
print(f"Running mean shape: {bn2d.running_mean.shape}") # torch.Size([64])
print(f"Running var shape: {bn2d.running_var.shape}") # torch.Size([64])
6.2.4 使用注意事项
- num_features 必须等于通道数:BatchNorm2d(64) 的 64 对应输入张量的 channel 维度
- batch 不能太小:如果 batch=1 或 2,均值/方差统计不可靠,BatchNorm 效果会变差
- 推理时使用滑动平均:PyTorch 在训练时维护 running_mean 和 running_var,推理时用它们代替当前 batch 统计量
- 顺序:通常是 Conv/Linear → BatchNorm → Activation,不要放反
- 与 LayerNorm 选择:BatchNorm 依赖 batch,适合 CNN;LayerNorm 不依赖 batch,适合 Transformer、小 batch、变长序列
BatchNorm vs LayerNorm:BatchNorm 在 batch 维度做归一化,适合 batch 较大且稳定的场景(如图像);LayerNorm 在特征维度做归一化,不依赖 batch,适合序列模型和 Transformer。
6.3 LayerNorm — 层归一化
nn.LayerNorm 是一种对单个样本内部的特征维度做归一化的技术。它不会跨样本统计 batch 信息,而是对每一个样本、每一个 token 的特征向量,单独计算均值与方差,并将其归一化到均值 0、方差 1 附近。
其数学表达可写成:
μ = (1/D) Σ x_i # 在当前样本的特征维度上求均值
σ² = (1/D) Σ (x_i - μ)² # 在当前样本的特征维度上求方差
x̂_i = (x_i - μ) / sqrt(σ² + ε) # 归一化
y_i = γ_i · x̂_i + β_i # 可学习的缩放和平移
- D:归一化维度的大小,通常由 normalized_shape 指定
- γ, β:可学习参数,让网络可以“撤销”或调整归一化后的分布
- 关键点:所有统计都在“单个样本内部”完成,不同样本之间互不影响
输入形状示例:
# 最后一维归一化:适用于 [N, seq_len, embed_dim]
ln = nn.LayerNorm(normalized_shape=512)
# 多个维度一起归一化:适用于 [N, C, H, W]
ln = nn.LayerNorm(normalized_shape=[256, 16, 16])
6.3.2 为什么要用 LayerNorm?
问题背景:BatchNorm 在哪些场景会失效?
- 小 batch 场景:batch 太小会导致均值/方差估计不准,BatchNorm 不稳定
- 变长序列:NLP 中 batch 内样本长度往往不同,难以统一做 batch 维归一化
- 在线/ continual 学习:推理时 batch=1,BatchNorm 的 running stats 不再可靠
- 强化学习:batch 不稳定,分布变化剧烈
LayerNorm 的四大收益:
- 不依赖 batch:每个样本独立归一化,batch=1 也能正常工作
- 适配变长序列:序列长度不影响归一化逻辑,天然适合 Transformer
- 稳定深层训练:减少层间分布漂移,让梯度传播更稳定
- 保留表达能力:通过 γ, β 可学习参数,网络仍然可以恢复任意分布
直观理解:
BatchNorm:把一批样本中同一特征拉到同一分布
→ 依赖 batch,batch 越小越不准
LayerNorm:把单个样本中不同特征拉到同一分布
→ 不依赖 batch,每个样本自己管自己
例如,对一个词向量 [0.2, -0.5, 1.3, ..., 0.8]:
BatchNorm 看:batch 里所有样本的第 1 维
LayerNorm 看:这个样本自己的 512 个维度
6.3.3 怎么做:代码与常见用法
基本用法:
import torch
import torch.nn as nn
# 对最后一维做归一化,embed_dim=512
ln = nn.LayerNorm(normalized_shape=512)
# 4D 输入:(batch, seq_len, embed_dim)
x = torch.randn(8, 30, 512) # batch=8,序列长度30,嵌入维度512
x = ln(x)
print(f"输入形状: {x.shape}") # torch.Size([8, 30, 512])
print(f"输出均值(应≈0): {x.mean(dim=-1).mean().item():.6f}")
print(f"输出方差(应≈1): {x.var(dim=-1).mean().item():.6f}")
在 Transformer / BERT 中使用:
import torch.nn as nn
class TransformerBlock(nn.Module):
def __init__(self, embed_dim=512, num_heads=8):
super().__init__()
self.attn = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
self.norm1 = nn.LayerNorm(embed_dim) # 注意力后的层归一化
self.ffn = nn.Sequential(
nn.Linear(embed_dim, embed_dim * 4),
nn.GELU(),
nn.Linear(embed_dim * 4, embed_dim),
)
self.norm2 = nn.LayerNorm(embed_dim) # FFN 后的层归一化
def forward(self, x):
# Pre-LN 结构(GPT 系列常用)
x = x + self.attn(self.norm1(x), x, x)[0] # 残差 + 注意力
x = x + self.ffn(self.norm2(x)) # 残差 + FFN
return x
对多维输入归一化:
# 图像输入 (N, C, H, W),同时对 C、H、W 归一化
ln = nn.LayerNorm(normalized_shape=[256, 16, 16])
x = torch.randn(4, 256, 16, 16)
x = ln(x)
# 输出形状不变:torch.Size([4, 256, 16, 16])
6.3.4 与 BatchNorm 的对比
| 对比项 | BatchNorm | LayerNorm |
|---|---|---|
| 统计维度 | batch 维度 | 特征维度 |
| 依赖 batch | 是,batch 太小效果差 | 否,batch=1 也可工作 |
| 典型场景 | CNN、大 batch 图像 | Transformer、NLP、小 batch |
| 训练/推理行为 | 训练用 batch 统计,推理用 running stats | 每次都是当前样本统计,无 running stats |
| 参数量 | 每个 channel 2 个参数 (γ, β) | 每个归一化维度 2 个参数 (γ, β) |
- normalized_shape 要与实际维度对齐:比如输入最后一维是 512,就设 LayerNorm(512)
- 训练和推理无需切换 mode:LayerNorm 没有 running stats,行为前后一致
- 放在残差之前还是之后?常见两种结构:Pre-LN(如 GPT、BERT)和 Post-LN(如原始 Transformer),两者训练稳定性不同
- 与 BatchNorm 不要随意混用:同一模块内通常只选一种归一化策略
本节要点:
- nn.Dropout:训练时随机丢弃神经元,防止过拟合
- nn.BatchNorm:batch 维度归一化,适合 CNN
- nn.LayerNorm:特征维度归一化,适合 Transformer
七、模型构建实战案例
7.1 案例一:LeNet-5 手写数字识别
LeNet-5 是深度学习领域的“Hello World”,由 Yann LeCun 等人在 1998 年提出,是最早成功商用的卷积神经网络。它用于识别手写数字 MNIST(28×28 灰度图),整体参数量仅约 6 万,但验证了“局部感受野 + 权值共享”的核心思想。
MNIST 的挑战在于:同一个人写“7”,可以有几百种形态;机器必须从像素级的原始输入中,抽象出“高层的曲线、转角、闭环”等特征,才能准确分类。
LeNet-5 的设计哲学是:先用卷积层提取局部特征(保留空间结构),再用全连接层做分类决策(压缩空间信息)。
结构拆解:逐层维度变化输入图像: (1, 32, 32) # 通道=1, 高=32, 宽=32
卷积层 C1:
Conv2d(1, 6, kernel=5, padding=2)
→ 特征图: (6, 28, 28) # 32-5+2*2 = 28 (无 padding 时公式: H_out = H_in - kernel + 2*pad)
池化层 S2:
MaxPool2d(2, 2)
→ 特征图: (6, 14, 14) # 28/2 = 14
卷积层 C3:
Conv2d(6, 16, kernel=5)
→ 特征图: (16, 10, 10) # 14-5+0 = 10
池化层 S4:
MaxPool2d(2, 2)
→ 特征图: (16, 5, 5) # 10/2 = 5
全连接层 F5:
Linear(16*5*5, 120)
→ 输出: (120,) # 展平后 400 维
全连接层 F6:
Linear(120, 84)
→ 输出: (84,)
全连接层 F7 (输出层):
Linear(84, 10)
→ 输出: (10,) # 10 个类别的 logits
核心代码逐行解析
class LeNet5(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
# 卷积层:提取局部特征,out_channels=6 表示输出 6 个特征图
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2)
self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
# 池化层:下采样,降低分辨率,减少计算量
self.pool = nn.MaxPool2d(2, 2)
# 全连接层:展平后做分类
# 16*5*5 = 400,对应最后一个池化层的总神经元数
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, num_classes)
self.relu = nn.ReLU() # 共享 ReLU 实例,节省内存
def forward(self, x):
# 卷积 + 池化:提取特征
x = self.pool(self.relu(self.conv1(x))) # (1,32,32) → (6,14,14)
x = self.pool(self.relu(self.conv2(x))) # (6,14,14) → (16,5,5)
# 展平:将 (16,5,5) → (400,)
x = x.view(x.size(0), -1)
# 全连接:分类
x = self.relu(self.fc1(x)) # (400,) → (120,)
x = self.relu(self.fc2(x)) # (120,) → (84,)
x = self.fc3(x) # (84,) → (10,) # 最后一层无激活,logits 给 CrossEntropyLoss
return x
维度计算关键点
- padding=2 的作用:输入 32×32,kernel=5 时,不 padding 会在第一层就把边缘信息丢掉;加 padding=2 可以让输出保持 28×28
- 为什么第二层卷积 kernel_size=5 无 padding:输入是 14×14,经过 14-5+1=10,不 padding 刚好是 10×10
- 16*5*5=400:卷积提取的空间特征在最后一个池化层保留为 5×5 的 16 个特征图,展平后就是 400 维向量
- 最后一层无激活:因为 CrossEntropyLoss 内部会做 Softmax,直接用 logits 更稳定
- MNIST 原图是 28×28,这里预处理时 padding 到 32×32;如果用 28×28,只需把第一个池化输出改为 (6,12,12)
- 原始 LeNet 用 sigmoid 激活,现代实现改用 ReLU,收敛更快
- 现代 MNIST 精度很容易超过 99%,LeNet 足够用
7.2 案例二:ResNet 残差块
ResNet(Residual Network) 由 He Kaiming 等人在 2015 年提出,是 ImageNet 比赛冠军模型。它的核心贡献是解决了深层网络退化问题:网络越深,训练误差和测试误差反而更高——这不是过拟合,而是优化困难。
退化问题的本质:非线性层越来越多,随机初始化的权重让每个残差分支的输出在训练初期趋近于零,于是深层网络退化为浅层网络的恒等映射表达能力,网络无法有效学习。
ResNet 的解法:恒等映射(Identity Mapping)
与其让网络直接学习底层→高层的完整映射 H(x),不如让网络学习残差 F(x) = H(x) - x。如果恒等映射是最优解,网络只需要让 F(x)=0;如果需要改造,网络同时学习 F(x) 和 x。两件事互不干扰。
普通网络: H(x) = F(x) # 要学会整个映射
残差网络: H(x) = F(x) + x # 只需要学会残差 F(x)
# x 是恒等映射的"捷径"(shortcut)
训练初期: F(x) ≈ 0 → H(x) ≈ x # 退化为浅层网络,可以正常训练
训练后期: F(x) 学习增量信息 # 超越浅层网络
残差块结构拆解
每个残差块包含两条路径:
- 残差路径(F(x)):两个 3×3 卷积 + BN + ReLU,负责学习增量
- 捷径路径(x):直接传递输入,如果尺寸不一致则用 1×1 卷积调整
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
# ---- 残差路径(主路径)----
# stride>1 时做下采样;bias=False 因为后面 BatchNorm 会做偏置
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
# ---- 捷径路径(shortcut)----
# 当尺寸不一致时(stride≠1 或通道数变化),需要调整 x 使其能与 F(x) 相加
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
# 1×1 卷积:既可以改变通道数,也可以改变分辨率
nn.Conv2d(in_channels, out_channels, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
residual = x # 保存输入(恒等映射)
out = self.conv1(x) # 第一个卷积
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out) # 第二个卷积
out = self.bn2(out)
out += self.shortcut(residual) # 关键:F(x) + x
out = self.relu(out)
return out
三种尺寸变化场景
| 场景 | 输入 | 输出 | Shortcut 处理 |
|---|---|---|---|
| 尺寸相同 | (N, 64, 32, 32) | (N, 64, 32, 32) | 直接相加,shortcut = 空 |
| 分辨率减半 | (N, 64, 32, 32) | (N, 64, 16, 16) | 1×1 conv + stride=2 |
| 通道数翻倍 | (N, 64, 32, 32) | (N, 128, 16, 16) | 1×1 conv + stride=2 + 通道扩维 |
class ResNet18(nn.Module):
def __init__(self, num_classes=1000):
super().__init__()
self.in_channels = 64
# 初始卷积层
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 残差块堆叠
self.layer1 = self._make_layer(out_channels=64, blocks=2, stride=1) # 64×56×56
self.layer2 = self._make_layer(out_channels=128, blocks=2, stride=2) # 128×28×28
self.layer3 = self._make_layer(out_channels=256, blocks=2, stride=2) # 256×14×14
self.layer4 = self._make_layer(out_channels=512, blocks=2, stride=2) # 512×7×7
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, num_classes)
def _make_layer(self, out_channels, blocks, stride):
layers = []
# 第一个 block 可能需要下采样
layers.append(ResidualBlock(self.in_channels, out_channels, stride))
self.in_channels = out_channels
# 后续 blocks 尺寸不变
for _ in range(1, blocks):
layers.append(ResidualBlock(out_channels, out_channels))
return nn.Sequential(*layers)
def forward(self, x):
x = self.maxpool(self.relu(self.bn1(self.conv1(x)))) # 224→112
x = self.layer1(x) # 112→56
x = self.layer2(x) # 56→28
x = self.layer3(x) # 28→14
x = self.layer4(x) # 14→7
x = self.avgpool(x) # 7→1
x = torch.flatten(x, 1)
return self.fc(x)
# ResNet-18/34/50/101/152 区别:残差块数量和每个 block 的卷积配置
# ResNet-18: [2,2,2,2] blocks,对应 18 层
# ResNet-50: [3,4,6,3] blocks,且用 1×1→3×3→1×1 的 bottleneck 结构
挂件点:残差连接的三大注意事项
- F(x) 和 x 必须形状一致才能相加:维度不匹配时用 1×1 卷积调整,这是最常见的 bug 来源
- 捷径路径不需要激活函数:恒等映射本身就是 identity,直接加结果更好
- ResNet 训练技巧:先用 lr=0.1, SGD+momentum=0.9, weight_decay=1e-4;现代也常用 AdamW 配合余弦退火
- BatchNorm 在哪里?残差路径里每个卷积后都有 BN,1×1 shortcut 里也有 BN
7.3 案例三:完整的训练流程
前面两个案例聚焦于模型定义,但深度学习真正重要的另一半是训练闭环——从数据到梯度更新,再到模型持久化。这是一个经典的 MLP(多层感知机)训练流程,覆盖了 PyTorch 项目中最核心的 7 个步骤:
- ① 定义模型
- ② 定义损失函数和优化器
- ③ 准备数据
- ④ 训练循环(前向→损失→梯度→更新)
- ⑤ 评估
- ⑥ 保存模型
- ⑦ 加载模型推理
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# ============================================================
# 步骤①:定义模型
# ============================================================
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
# nn.Sequential:按顺序堆叠层,最简洁的定义方式
self.net = nn.Sequential(
nn.Linear(784, 256), # 输入层:28×28=784 → 256
nn.ReLU(), # 激活函数
nn.Dropout(0.2), # 防止过拟合
nn.Linear(256, 128), # 隐藏层
nn.ReLU(), # 激活函数
nn.Linear(128, 10) # 输出层:10 个类别
)
# 注意:最后一层无激活!CrossEntropyLoss 内部包含 Softmax
def forward(self, x):
return self.net(x)
# 创建模型实例
model = SimpleNet()
print(f"模型结构:\n{model}")
print(f"参数量: {sum(p.numel() for p in model.parameters()):,}")
# ============================================================
# 步骤②:定义损失函数和优化器
# ============================================================
criterion = nn.CrossEntropyLoss() # 分类交叉熵损失(内置 softmax)
optimizer = optim.Adam(
model.parameters(),
lr=0.001, # 学习率
betas=(0.9, 0.999), # 动量估计
weight_decay=1e-4 # L2 正则化
)
# ============================================================
# 步骤③:准备数据(这里用随机数据演示,实际用真实数据集)
# ============================================================
X_train = torch.randn(1000, 784) # 1000 个样本,784 维
y_train = torch.randint(0, 10, (1000,)) # 1000 个标签
# TensorDataset:将特征和标签打包
train_dataset = TensorDataset(X_train, y_train)
# DataLoader:批量加载,自动 shuffle、打乱、重采样
train_loader = DataLoader(
train_dataset,
batch_size=32, # 每个 batch 32 个样本
shuffle=True, # 每个 epoch 打乱一次
num_workers=2, # 多进程加载(加速)
drop_last=False # 不丢弃最后不完整的 batch
)
# ============================================================
# 步骤④:训练循环
# ============================================================
model.train() # 开启训练模式:Dropout 生效,BatchNorm 用 batch 统计
num_epochs = 5
for epoch in range(num_epochs):
running_loss = 0.0 # 累计损失
correct = 0 # 预测正确数
total = 0 # 总样本数
for batch_idx, (inputs, targets) in enumerate(train_loader):
# ---- 前向传播 ----
outputs = model(inputs) # shape: (32, 10)
loss = criterion(outputs, targets)
# ---- 反向传播 ----
optimizer.zero_grad() # 梯度清零(必须!)
loss.backward() # 计算梯度
# ---- 参数更新 ----
optimizer.step() # 按梯度和学习率更新参数
# ---- 统计 ----
running_loss += loss.item()
_, predicted = outputs.max(1) # 取最大 logit 对应类别
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
# 每个 epoch 结束后打印一次平均指标
epoch_loss = running_loss / len(train_loader)
epoch_acc = 100. * correct / total
print(f"Epoch [{epoch+1}/{num_epochs}] "
f"Loss: {epoch_loss:.4f} Acc: {epoch_acc:.2f}%")
# ============================================================
# 步骤⑤:评估(验证/测试)
# ============================================================
model.eval() # 开启评估模式:Dropout 关闭,BatchNorm 用 running stats
with torch.no_grad(): # 推理时不需要梯度,节省显存和计算
test_input = torch.randn(1, 784)
output = model(test_input)
_, predicted = output.max(1)
print(f"预测类别: {predicted.item()}")
print(f"各类别概率: {torch.softmax(output, dim=1)}")
# ============================================================
# 步骤⑥:保存模型
# ============================================================
# 方式一:保存整个模型(包含结构,不推荐,跨版本可能不兼容)
torch.save(model, 'model_whole.pth')
# 方式二:保存参数(推荐,只保存权重,兼容性好)
torch.save(model.state_dict(), 'model_weights.pth')
# 方式三:保存完整检查点(推荐,包含优化器状态、epoch 等,方便断点续训)
checkpoint = {
'epoch': 5,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': epoch_loss,
}
torch.save(checkpoint, 'checkpoint.pth')
# ============================================================
# 步骤⑦:加载模型推理
# ============================================================
# 加载参数(需要先有模型结构)
model.load_state_dict(torch.load('model_weights.pth', weights_only=True))
model.eval()
# 加载检查点(恢复完整状态)
checkpoint = torch.load('checkpoint.pth', weights_only=False)
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
start_epoch = checkpoint['epoch']
print(f"从 epoch {start_epoch} 恢复训练")
训练流程关键点梳理
- model.train() vs model.eval():两者主要控制 Dropout、BatchNorm、BatchNorm 的行为差异;不切换不会报错,但结果会不稳定
- optimizer.zero_grad() 必须调用:PyTorch 默认梯度累积,不清零会导致梯度叠加更新错误
- with torch.no_grad():推理时禁止梯度计算,可节省约 30-50% 显存、显著加速
- loss.item() 而非 loss:loss 是计算图节点,item() 提取纯数值;打印 loss 时必须用 item(),否则显存泄漏
- CrossEntropyLoss 不在最后一层:该损失函数内部集成了 log_softmax,直接传 logits,不要手动 softmax
- drop_last:设为 True 可避免最后一个 batch 样本太少导致 BN 统计不稳定
| 保存方式 | 代码 | 优点 | 缺点 |
|---|---|---|---|
| 保存整个模型 | torch.save(model, 'xxx.pth') | 简单直接 | 不跨版本、不可移植、有安全风险 |
| 保存 state_dict(推荐) | torch.save(model.state_dict(), 'xxx.pth') | 轻量、跨版本、可移植 | 需要重建模型结构 |
| 保存完整检查点 | torch.save({'model': ..., 'opt': ..., 'epoch': ...}) | 支持断点续训 | 文件较大 |
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device) # 模型搬到 GPU
for inputs, targets in train_loader:
inputs = inputs.to(device) # 数据搬到 GPU
targets = targets.to(device)
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
optimizer.zero_grad()
本节要点:
- 模型定义:继承 nn.Module,实现 forward()
- 训练流程:前向传播 → 计算损失 → 梯度清零 → 反向传播 → 参数更新
- 模式切换:train() 训练模式,eval() 评估模式
- 模型持久化:state_dict() 保存参数,load_state_dict() 加载
八、FAQ(常见问题)
以下是关于 torch.nn 模块的 20 个常见问题,涵盖核心概念、实战技巧和避坑指南。
Q1. nn.Module 和 nn.functional 的区别是什么?
一句话结论:nn.Module 是有状态的(有参数),nn.functional 是无状态的(纯函数)。
nn.ReLU 是类实例,F.relu 是函数调用。推荐在 nn.Sequential 中使用 nn.ReLU(),因为它可以统一管理状态;在需要控制行为的地方(如 inplace=True),使用 F.relu(x, inplace=True)。
Q2. 为什么要在每个 batch 前调用 optimizer.zero_grad()?
一句话结论:PyTorch 默认会累积梯度,不清零会导致梯度计算错误。
每个 batch 的梯度需要独立计算,如果不清零,新梯度会加上之前的梯度,导致参数更新方向错误。正确的顺序是:zero_grad() → backward() → step()。
Q3. .to(device) 和 .cuda() 有什么区别?
一句话结论:.to(device) 更通用,支持任意设备;.cuda() 是简写,仅限 CUDA。
推荐使用 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'),然后 model.to(device),代码更灵活。
Q4. model.train() 和 model.eval() 的区别是什么?
一句话结论:训练模式 Dropout 生效、BatchNorm 使用 batch 统计量;推理模式则相反。
如果忘记切换到 eval(),Dropout 不会关闭,BatchNorm 不会使用固定的 running stats,导致推理结果随机。
Q5. torch.no_grad() 和 model.eval() 有什么关系?
一句话结论:model.eval() 切换模式,torch.no_grad() 禁用梯度。
两者常搭配使用:model.eval() 切换 BatchNorm/Dropout 行为,torch.no_grad() 禁用梯度计算以节省显存。
Q6. 为什么不直接调用 model.forward(x)?
一句话结论:因为 model(x) 会触发 __call__ 中的钩子函数和异常处理。
nn.Module.__call__ 会调用 forward,同时触发 _forward_hooks、_backward_hooks 等扩展机制。直接调用 forward 会绕过这些。
Q7. nn.Parameter 和 nn.Tensor 有什么区别?
一句话结论:nn.Parameter 是自动注册到模型的 nn.Tensor。
当你将 nn.Parameter 赋值给类属性时,它会自动注册为模型参数,被 parameters() 收集;而普通 Tensor 不会。
Q8. 如何查看模型参数数量?
一句话结论:使用 sum(p.numel() for p in model.parameters())。
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total: {total_params:,}, Trainable: {trainable_params:,}")
Q9. nn.Sequential 和 nn.ModuleList 哪个更好?
一句话结论:顺序结构用 nn.Sequential,动态/索引结构用 nn.ModuleList。
如果网络结构是固定顺序的,用 nn.Sequential 更简洁;如果需要动态添加层或按索引访问,用 nn.ModuleList。
Q10. 为什么输入数据要调用 .to(device)?
一句话结论:因为模型和数据必须在同一设备上才能计算。
如果模型在 GPU 上,数据在 CPU 上,会报错。将数据也迁移到 GPU:inputs = inputs.to(device)。
Q11. nn.BatchNorm 需要设置 track_running_stats 吗?
一句话结论:训练时 track_running_stats=True(默认),推理时使用 running stats。
如果 batch size 很小(如 1),可以设 track_running_stats=False,这样训练时会使用 batch 统计量而非 running stats。
Q12. 模型的 state_dict 是什么?
一句话结论:一个有序字典,键是参数名,值是参数张量。
model.state_dict() 返回模型所有参数的快照,常用于保存和加载模型。
Q13. 如何加载部分预训练参数?
一句话结论:加载时设置 strict=False,然后手动过滤。
pretrained_dict = torch.load('pretrained.pth')
model_dict = model.state_dict()
# 只加载匹配的参数
pretrained_dict = {k: v for k, v in pretrained_dict.items()
if k in model_dict and v.shape == model_dict[k].shape}
model_dict.update(pretrained_dict)
model.load_state_dict(model_dict)
Q14. 为什么 ReLU 要用 inplace=True?
一句话结论:节省显存,不需要额外分配内存存储结果。
inplace=True 直接在原张量上修改,但注意在计算梯度时会有影响,某些场景(如求导顺序敏感)不建议使用。
Q15. nn.CrossEntropyLoss 和 nn.NLLLoss 有什么区别?
一句话结论:CrossEntropyLoss = Softmax + NLLLoss。
CrossEntropyLoss 输入是原始 logits,内部先做 Softmax 再算 NLL;NLLLoss 输入是 Softmax 后的 log 概率。
Q16. 如何实现自定义损失函数?
一句话结论:继承 nn.Module 并实现 forward。
import torch
import torch.nn as nn
import torch.nn.functional as F
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, inputs, targets):
ce_loss = F.cross_entropy(inputs, targets, reduction='none')
pt = torch.exp(-ce_loss)
focal_loss = self.alpha * (1-pt)**self.gamma * ce_loss
return focal_loss.mean()
Q17. 为什么需要注册子模块?
一句话结论:注册后 PyTorch 会自动管理参数、梯度、设备迁移和序列化。
只有通过 self.xxx = nn.Module(...) 注册的子模块,其参数才会被 parameters() 收集、优化器绑定和 to() 迁移。
Q18. nn.Module 的 __init__ 中为什么要调用 super().__init__()?
一句话结论:初始化父类的内部状态,确保参数注册机制正常工作。
super().__init__() 会初始化 _parameters、_modules、_buffers 等字典,这些是参数管理的基石。
Q19. 如何实现模型权重初始化?
一句话结论:在 __init__ 或 reset_parameters 中使用 nn.init 模块。
def reset_parameters(self):
nn.init.xavier_uniform_(self.weight) # Xavier 初始化
if self.bias is not None:
nn.init.zeros_(self.bias)
Q20. 保存整个模型和只保存 state_dict 哪个更好?
一句话结论:推荐只保存 state_dict,更灵活、可移植。
保存整个模型会绑定模型类定义,更换代码后可能加载失败;保存 state_dict 只依赖参数名,兼容性更好。
FAQ 要点总结:
- nn.Module vs nn.functional:有状态 vs 无状态
- zero_grad() 必须调用,否则梯度累积
- train()/eval() + no_grad() 配合使用
- 优先使用 state_dict 保存模型
九、Roadmap 后续预告
后续学习路线:
- 深入 Layer:深入理解各类卷积变体(Depthwise、Separable、Transposed)、注意力机制(Self-Attention)
- 优化技巧:学习率调度(LR Scheduler)、梯度裁剪(Gradient Clipping)、混合精度训练(AMP)
- 高级模块:torch.nn.utils(参数剪枝、Spectral Norm)、torch.nn.parallel(分布式训练)
- 实战项目:图像分类(ResNet、EfficientNet)、NLP(BERT、GPT)、目标检测(YOLO)、语义分割(U-Net)
- 自动微分机制:torch.autograd 原理、计算图构建、梯度计算
- 优化器详解:torch.optim 中的 SGD、Adam、RMSprop 等

浙公网安备 33010602011771号