目录
[1️⃣ 谁负责"自动创建和管理参数"?](#1️⃣ 谁负责“自动创建和管理参数”?)
[使用 nn:参数自动生成并注册](#使用 nn:参数自动生成并注册)
[使用 functional:参数你自己来](#使用 functional:参数你自己来)
[2️⃣ 谁负责"运行状态"(buffer)?](#2️⃣ 谁负责“运行状态”(buffer)?)
[BatchNorm 的区别](#BatchNorm 的区别)
[Dropout 也是一样的逻辑](#Dropout 也是一样的逻辑)
[3️⃣ 组合方式不同:谁更适合"搭结构",谁更适合"插操作"?](#3️⃣ 组合方式不同:谁更适合“搭结构”,谁更适合“插操作”?)
[用 nn 搭网络,结构一眼就懂](#用 nn 搭网络,结构一眼就懂)
[用 F,灵活度更高](#用 F,灵活度更高)
[1️⃣ 大多数 nn.Module 的 forward(),内部都会调用 F](#1️⃣ 大多数 nn.Module 的 forward(),内部都会调用 F)
[2️⃣ 很多 F 本身也是"算子组合"](#2️⃣ 很多 F 本身也是“算子组合”)
[✅ 优先用 nn 的场景](#✅ 优先用 nn 的场景)
[✅ 更偏向用 F 的场景](#✅ 更偏向用 F 的场景)
很多人刚学 PyTorch 时都会有一个疑惑:
为什么有的地方用
nn.ReLU(),有的地方却写
F.relu(x)?它们是不是重复设计了?
结论先给出来一句话版本:
torch.nn和torch.nn.functional的关系,可以理解为:
👉「带状态的积木」 vs 「纯函数工具箱」
下面我们慢慢拆开讲。
一句话抓住本质区别
在 PyTorch 里,这两者分工非常明确:
-
torch.nn-
提供的是 Module(模块 / 层)
-
通常 自带参数、状态
-
参数会被自动注册,能被 optimizer 更新
-
能进
state_dict(),能保存 / 加载 /.to(device) -
非常适合 搭网络结构
-
-
torch.nn.functional(通常写作F)-
提供的是 函数式接口
-
本身 不保存参数、不保存状态
-
你传什么,它就算什么
-
非常适合在
forward()里做 临时计算
-
一句更直观的比喻:
-
nn:已经封装好的"层级积木" -
F:一把"算子瑞士军刀"
用法上的核心差异(配代码)
1️⃣ 谁负责"自动创建和管理参数"?
这是最关键的差别。
使用 nn:参数自动生成并注册
python
import torch.nn as nn
conv = nn.Conv2d(3, 16, 3)
# conv.weight / conv.bias 已经存在
# 并且会出现在 model.parameters() 里
你什么都不用操心:
-
参数自动创建
-
optimizer 会自动更新
-
保存模型时会进
state_dict()
使用 functional:参数你自己来
python
import torch.nn.functional as F
y = F.conv2d(x, weight, bias, stride=1, padding=0)
这里的 weight、bias:
-
PyTorch 不会帮你创建
-
也不会自动注册
-
更不会自动更新
所以经验法则很简单:
-
你想要一个"层" → 用
nn -
你只想"算一下" → 用
F
2️⃣ 谁负责"运行状态"(buffer)?
这在 BatchNorm / Dropout 上特别明显。
BatchNorm 的区别
-
nn.BatchNorm2d-
内部有
running_mean / running_var -
自动跟随
model.train()/model.eval() -
这些统计量会进
state_dict()
-
-
F.batch_norm-
你要自己把均值、方差传进去
-
还要自己决定
training=True/False
-
Dropout 也是一样的逻辑
python
# nn 版本
self.drop = nn.Dropout(p=0.5)
# functional 版本
x = F.dropout(x, p=0.5, training=self.training)
nn.Dropout 会自动"看懂"当前模型是在训练还是推理
F.dropout 则需要你显式告诉它
这也是为什么:
有状态的层,优先用
nn,更安全
3️⃣ 组合方式不同:谁更适合"搭结构",谁更适合"插操作"?
用 nn 搭网络,结构一眼就懂
python
class M(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(128, 64)
self.act = nn.ReLU()
def forward(self, x):
return self.act(self.fc(x))
这种写法:
-
结构清晰
-
方便调试
-
非常适合主干网络
用 F,灵活度更高
python
def forward(self, x):
x = self.fc(x)
x = F.relu(x)
x = F.dropout(x, p=0.5, training=self.training)
return x
这里你可以:
-
临时加个操作
-
不定义成员变量
-
做更复杂的控制逻辑
这在 自监督、损失函数、mask 操作 里非常常见。
从源码角度看:它们其实是"父子关系"
这是很多人不知道、但非常重要的一点。
1️⃣ 大多数 nn.Module 的 forward(),内部都会调用 F
也就是说:
nn.Module = 参数 / 状态管理 + 调用 functional 做计算
概念上大致是这样:
-
nn.ReLU.forward(x)→
F.relu(x, inplace=self.inplace) -
nn.Linear.forward(x)→
F.linear(x, self.weight, self.bias) -
nn.Conv2d.forward(x)→
F.conv2d(x, self.weight, self.bias, ...)
所以真正的分工是:
-
torch.nn-
管参数
-
管状态
-
管保存 / 加载 / 设备迁移
-
-
torch.nn.functional- 负责 真正的数值计算
再往下,F 的实现通常会继续调用:
-
aten算子 -
最终落到 C++ / CUDA kernel
2️⃣ 很多 F 本身也是"算子组合"
例如:
-
F.relu→
aten::relu -
F.linear→
addmm等底层算子 -
F.cross_entropy→
log_softmax + nll_loss(不同版本可能融合优化)
整体调用链常常是:
nn.Module.forward
↓
torch.nn.functional.xxx
↓
aten / torch.ops
↓
C++ / CUDA kernel
如果你翻源码,经常能看到 forward() 里直接调用 F.xxx。
实战中该怎么选?
一个非常实用的判断标准是:
✅ 优先用 nn 的场景
-
正常搭网络结构(Conv / Linear / BN / Embedding / RNN)
-
希望:
-
自动收参数
-
自动保存加载
-
自动处理 train / eval 行为
-
-
多卡训练、模型复用
这是 99% 模型代码的主流写法。
✅ 更偏向用 F 的场景
-
自定义
forward -
自监督 / 对比学习 / 医疗分割里的各种 loss、mask、相似度
-
权重共享、动态权重(meta-learning、hypernetwork、LoRA)
-
不想把一个操作"升级成层",只是临时用一下
一个非常容易踩的坑
不要用 functional 去"硬写"有状态的层,尤其是:
-
BatchNorm
-
Dropout
典型问题是:
-
忘了传
training=self.training -
推理时还在 dropout
-
BN 统计行为不对
如果你不是刻意要手动控制行为:
直接用
nn.BatchNorm / nn.Dropout,更稳、更不容易出 bug
小结一句话版
-
torch.nn:负责结构、参数、状态 -
torch.nn.functional:负责计算本身 -
多数情况下:nn 外壳 + F 内核
理解了这一点,你在读源码、写自定义模型、调试诡异训练行为时,思路会清晰很多。