从0到1,构建自己的全连接神经网络

文章目录

  • [1、torch.nn 和 nn.Module](#1、torch.nn 和 nn.Module)
  • 2、summary
  • [3、summary 使用](#3、summary 使用)
  • 4、nn.Module.named_parameters
  • [5、nn.Module.named_parameters() 和 nn.Module.parameters()](#5、nn.Module.named_parameters() 和 nn.Module.parameters())
  • [6、通常不需要手动 初始化输出层 参数](#6、通常不需要手动 初始化输出层 参数)
  • [7、构建自己的神经网络模型 - 代码](#7、构建自己的神经网络模型 - 代码)

1、torch.nn 和 nn.Module

一、torch.nn 是什么?

torch.nn(通常简写为 nn)是 PyTorch 中用于构建神经网络的核心模块

  • 它提供了:
    • 网络层(Layers) :如 nn.Linear(全连接层)、nn.Conv2d(卷积层)、nn.BatchNorm1d(批归一化)等;
    • 激活函数 :虽然常用激活函数在 torch 中(如 torch.relu),但 nn 也提供类形式(如 nn.ReLU());
    • 损失函数 :如 nn.CrossEntropyLoss
    • 初始化方法 :通过 nn.init 子模块;
    • 容器类 :如 nn.Sequential,用于快速搭建网络。

✅ 简单说:nn 就是 PyTorch 的"神经网络工具箱"


二、nn.Module 是什么?

nn.Module所有神经网络模块的基类(父类)

你写的每一个自定义模型(比如 ModelDemo),都必须继承自 nn.Module

🔑 nn.Module 的核心作用:

  1. 自动管理参数(Parameters)

    • 当你在 __init__ 中创建 nn.Linear 等层时,这些层内部的 weightbias自动注册为模型的可学习参数
    • 调用 model.parameters() 就能获取所有待优化的参数,供优化器(如 Adam)使用。
  2. 支持前向传播(Forward Pass)

    • 你只需实现 forward() 方法,PyTorch 会自动处理反向传播(通过自动微分机制)。
  3. 支持 GPU 加速、保存/加载、嵌套结构等高级功能

    • 比如调用 model.to('cuda') 可将整个模型移到 GPU;
    • 模型可以像搭积木一样嵌套(一个 Module 可包含多个子 Module)。

三、结合代码逐部分解析

python 复制代码
class ModelDemo(nn.Module):  # ← 继承 nn.Module,这是必须的!

✅ 这行表示:ModelDemo 是一个神经网络模型,具备 nn.Module 的所有能力。


  1. __init__ 构造方法:搭建网络结构
python 复制代码
def __init__(self):
    super().__init__()  # ← 必须调用父类初始化!否则 nn.Module 功能失效
    
# 或者

def __init__(self, *args, **kwargs):  
    super().__init__(*args, **kwargs)  # ← 必须调用父类初始化!否则 nn.Module 功能失效

📌 super().__init__() 会初始化 nn.Module 内部的状态(如参数注册器、子模块容器等),绝对不能省略

python 复制代码
self.linear1 = nn.Linear(in_features=3, out_features=3)
self.linear2 = nn.Linear(in_features=3, out_features=2)
self.output  = nn.Linear(in_features=2, out_features=2)
  • nn.Linear(a, b) 表示一个全连接层 (也叫仿射变换):
    • 输入维度:a
    • 输出维度:b
    • 内部自动创建权重矩阵(形状 [b, a])和偏置向量(长度 b

💡 注意:self.linear1 被赋值为 self 的属性,这样 PyTorch 才能"看到"它,并把它当作子模块管理。


  1. 参数初始化
python 复制代码
nn.init.xavier_normal_(tensor=self.linear1.weight)
nn.init.zeros_(tensor=self.linear1.bias)
...
  • 这些是对刚创建的层的权重和偏置进行手动初始化
  • 因为 nn.Linear 默认已有初始化(Kaiming),但老师想演示如何自定义。

✅ 初始化只在模型创建时执行一次,训练过程中权重会被优化器更新。


  1. forward 方法:定义数据流向
python 复制代码
def forward(self, x):
    x = torch.sigmoid(self.linear1(x))
    x = torch.relu(self.linear2(x))
    x = torch.softmax(self.output(x), dim=-1)
    return x
  • forward模型的计算逻辑 ,描述输入 x 如何一步步变成输出。
  • 每次调用 model(input) 时,PyTorch 会自动调用 forward()
  • 使用了三种激活函数:
    • sigmoid:常用于二分类输出或门控机制;
    • relu:现代网络主流激活函数;
    • softmax:多分类问题的输出层,将 logits 转为概率分布。

📌 dim=-1 表示对最后一个维度(即每个样本的类别维度)做 softmax,确保每行概率和为 1。


四、如何使用这个模型?

python 复制代码
# 1. 创建模型实例
model = ModelDemo()

# 2. 准备输入数据(假设 batch_size=4,每个样本3个特征)
x = torch.randn(4, 3)

# 3. 前向传播,得到预测
y_pred = model(x)  # 自动调用 forward()

print(y_pred.shape)  # → torch.Size([4, 2]),4个样本,2类概率

五、关键总结

概念 说明
nn PyTorch 神经网络模块库,提供层、损失、初始化等工具
nn.Module 所有模型的基类,负责参数管理、设备迁移、嵌套等
super().__init__() 必须调用,初始化父类状态
self.xxx = nn.Layer(...) 将层作为属性保存,PyTorch 才能识别并管理它
forward() 定义前向计算流程,方法名固定,不能改
model(input) 实际调用的是 forward(input)

💡 记住一句话 : "在 __init__ 里搭积木,在 forward 里走流程。"

2、summary

📘 PyTorch 模型结构可视化工具:summary(来自 torchsummary

  1. 是什么?

summary 是 Python 第三方库 torchsummary 提供的一个函数,用于打印 PyTorch 神经网络的结构摘要(model summary) ,功能类似于 Keras 中的 model.summary()

✅ 主要用途:快速查看模型每层的输入/输出形状、参数数量、总参数量等信息。

  1. 为什么需要它?

在 PyTorch 中,模型结构不像 Keras 那样自动显示。当你构建复杂网络时,容易出现:

  • 维度不匹配
  • 参数量估算错误
  • 层连接逻辑不清

summary 能帮你:

  • 🔍 调试网络结构
  • 📊 检查张量形状传递是否正确
  • 🧮 统计可训练参数总数
  • 📝 教学或文档展示模型架构
  1. 安装方法
bash 复制代码
pip install torchsummary

⚠️ 注意:不要与 torchinfo 混淆。torchsummary 轻量简单,适合全连接、基础 CNN;torchinfo 功能更强,支持更复杂的模块。

  1. 基本用法

✅ 正确语法:

python 复制代码
from torchsummary import summary

summary(model, input_size)     # input_size 是元组类型, 不能直接是int

参数说明:

参数 类型 说明
model nn.Module PyTorch 模型实例
input_size tuple 单个样本的输入形状(不含 batch 维度)

示例:

python 复制代码
# 全连接网络:输入是 3 维特征向量
summary(my_model, input_size=(3,))

# CNN:输入是 (通道, 高, 宽)
summary(cnn_model, input_size=(3, 32, 32))

❗ 关键:input_size 不能包含 batch_size

非常好的问题!你观察得很仔细 👏。

torchsummary.summary 的函数签名确实是:

python 复制代码
def summary(model, input_size, batch_size=-1, device="cuda"):

summary 四个参数详解

参数 默认值 作用 是否常用 说明
model --- 要分析的 PyTorch 模型(nn.Module 实例) ✅ 必填 核心参数
input_size --- 单个样本的输入形状(不含 batch 维度) ✅ 必填 如 全连接(3,) 如 CNN(3, 224, 224)
batch_size -1 用于构建 dummy input 的 batch 大小 ⚠️ 可选 默认 -1 表示自动推断或设为 2
device "cuda" 模型和 dummy input 放在哪种设备上运行 ⚠️ 可选 若无 GPU 需改为 "cpu"

  1. modelinput_size:必填,核心
  • 这两个是你必须提供的。
  • input_size 永远不包含 batch 维度 ,这是 torchsummary 的设计约定。

input_size:

写法 含义 适用模型
(3,) 每个样本是 3 维向量 全连接网络(MLP)
(3, 224, 224) 每个样本是 3 通道、224×224 的图像 CNN(如 ResNet, VGG)
(1, 28, 28) 每个样本是 1 通道、28×28 的灰度图 CNN(MNIST)
(50, 128) 每个样本是 50 个 token,每个 128 维 RNN / Transformer

✅ 正确示例:

python 复制代码
summary(model, (3,))          # 全连接
summary(model, (3, 32, 32))   # CNN
  1. batch_size:通常不用管,但有时需指定

默认行为:

  • batch_size=-1(默认),torchsummary内部使用 batch_size=2 构造 dummy input。
  • 为什么是 2?因为有些层(如 BatchNorm)在 batch_size=1 时会报错,所以用 2 更安全。

什么时候需要手动设置?

  • 你想看特定 batch size 下的输出形状 (虽然 Output Shape 中 batch 维度显示为 -1,但内部计算依赖实际 batch)
  • 调试与 batch 相关的层(如 BatchNorm、某些自定义层)

✅ 示例:

python 复制代码
summary(model, (3,), batch_size=8)   # 明确使用 batch_size=8

💡 但对全连接网络,batch_size 几乎不影响结构,可忽略。

  1. device非常重要!尤其在没有 GPU 时

默认值陷阱:

  • device="cuda" 是默认值,如果你的机器没有 GPU 或 PyTorch 未启用 CUDA,会直接报错!

常见错误:

python 复制代码
# 在 CPU 环境下运行
summary(model, (3,))
# 报错: RuntimeError: Attempting to deserialize object on a CUDA device ...

✅ 正确做法:

  • 始终根据你的环境显式指定 device
python 复制代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
summary(model, (3,), device=device)

或者简单点:

python 复制代码
summary(model, (3,), device="cpu")   # 如果你确定用 CPU

🌟 强烈建议 :除非你明确使用 GPU,否则一律写 device="cpu",避免莫名其妙的 CUDA 错误。

🔍 实际源码逻辑简析(简化版)

python 复制代码
def summary(model, input_size, batch_size=-1, device="cuda"):
    if batch_size == -1:
        batch_size = 2  # 安全起见用 2

    # 构造 dummy input: shape = (batch_size, *input_size)
    x = torch.rand((batch_size, *input_size), device=device)

    # 将模型移到对应设备
    model.to(device)

    # 前向传播并记录每层输出
    ...

所以:

  • batch_size 决定了 dummy input 的第一维
  • device 决定了模型和数据放在哪里运行

✅ 最佳实践总结

场景 推荐写法
普通 CPU 调试(最常见) summary(model, (3,), device="cpu")
GPU 环境且模型已在 GPU summary(model, (3,), device="cuda")
不确定是否有 GPU
python 复制代码
device = "cuda" if torch.cuda.is_available() else "cpu"
summary(model, (3,), device=device)

其他参数不是"不重要",而是"有合理默认值,在基础使用中可省略"。但在实际项目中,尤其是跨设备调试时,`device` 参数至关重要!

# 推荐写法(安全、清晰)
summary(model=my_model, input_size=(3,), device="cpu")
  1. 输出解读

运行后会打印类似如下内容:

复制代码
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Linear-1                   [-1, 64]             192
            ReLU-2                     [-1, 64]               0
            Linear-3                   [-1, 10]             650
================================================================
Total params: 842		   # 总参数个数
Trainable params: 842       # 可训练参数
Non-trainable params: 0     # 不可训练参数
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
----------------------------------------------------------------

各字段含义:

字段 说明
Layer (type) 层的名称和类型(如 Linear, Conv2d
Output Shape 该层输出的张量形状,-1 表示 batch 维度(可变)
Param # 该层的可训练参数数量(权重 + 偏置)
Total params 模型总参数量
Trainable params 可训练参数数量(通常等于总参数)
MB 估算 输入、中间激活、参数占用的内存(粗略估计)
  1. 常见错误与注意事项

❌ 错误1:input_size 包含 batch 维度

python 复制代码
# 错误!
summary(model, input_size=(5, 3))   # 报错或解析错误

✅ 正确:

python 复制代码
summary(model, input_size=(3,))     # 只写特征维度

❌ 错误2:未实现 forward 方法

  • 如果模型没有正确实现 forwardsummary 无法追踪计算图,会报错。

❌ 错误3:使用了不支持的层(如自定义复杂模块)

  • torchsummary 对标准层(Linear, Conv2d, ReLU 等)支持良好,但对某些动态结构(如循环、条件分支)可能失效。
  1. 替代方案:torchinfo(推荐进阶使用)

如果你需要更强大的功能(如支持 RNN、Transformer、自定义模块),推荐使用 torchinfo

python 复制代码
# 安装
pip install torchinfo

# 使用(注意:这里 input_size 可以带 batch)
from torchinfo import summary
summary(model, input_size=(16, 3))  # batch_size=16, features=3

优势:

  • 支持更多层类型
  • 可指定 batch_size
  • 输出更美观,支持 Colab/Jupyter
  • 显示每层的 MACs(计算量)
  1. 总结对比表
特性 torchsummary torchinfo
安装简单 ✅ 是 ✅ 是
支持标准层 ✅ 是 ✅ 是
支持复杂结构 ❌ 有限 ✅ 强大
input_size 是否含 batch ❌ 不含 ✅ 可含
内存/MACs 估算 ⚠️ 粗略 ✅ 精细
推荐场景 初学者、全连接/CNN 基础模型 进阶用户、复杂模型
  1. 最佳实践建议

  2. 初学阶段 :用 torchsummary 快速验证网络维度。

  3. input_size 永远只写单样本形状 ,如 (3,)(3, 224, 224)

  4. 配合 print(model) 使用

    • print(model):看层定义
    • summary(model, ...):看数据流和参数
  5. 遇到报错先检查 forward 是否正确实现

🌟 一句话记住

summary 是 PyTorch 的"Keras-style model summary",让你一眼看清模型长什么样!

3、summary 使用

只要你调用:

python 复制代码
summary(model=my_model, input_size=(3,))    # input_size 是元组类型, 不能直接是int

它就会自动打印出模型的结构摘要(summary)到控制台 ,不需要你手动 print,也不需要额外操作。

太好了!你已经掌握了 summary(model, input_size=(3,)) 的正确写法 👏。

现在我们来聚焦问题:

"我知道怎么设置了,但到底该怎么使用它?在什么场景下用?输出怎么看?有什么实际价值?"

✅ 一、什么时候用 summary

场景 说明
🧪 刚写完模型结构 快速验证每层维度是否连得上(避免 forward 报错)
🔍 调试维度错误 比如 mat1 and mat2 shapes cannot be multiplied 时,先看 summary 确认形状传递
📊 汇报/教学展示 向别人展示你的模型有多少层、多少参数
🧮 估算模型大小 查看 Total params,判断是否能在手机/嵌入式设备部署
📝 写论文或文档 把 summary 输出贴到附录里,说明模型复杂度

💡 最佳时机 :定义完 __init__forward 后,立刻调用一次 summary

✅ 二、怎么正确使用?(完整代码模板)

python 复制代码
import torch
import torch.nn as nn
from torchsummary import summary

class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear1 = nn.Linear(3, 64)
        self.linear2 = nn.Linear(64, 10)

    def forward(self, x):
        x = torch.relu(self.linear1(x))
        x = self.linear2(x)
        return x

# === 使用 summary 的标准流程 ===
if __name__ == "__main__":
    model = MyModel()
    
    # 推荐:显式指定 device,避免 CUDA 错误
    device = "cpu"  # 或 "cuda" if torch.cuda.is_available() else "cpu"
    model.to(device)
    
    # 调用 summary:input_size 是单个样本的形状(不含 batch)
    summary(model, input_size=(3,), device=device)

✅ 输出示例:

复制代码
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Linear-1                   [-1, 64]             256
            Linear-2                   [-1, 10]             650
================================================================
Total params: 906
Trainable params: 906
Non-trainable params: 0
...

✅ 三、如何解读输出?(重点!)

以全连接网络为例:

python 复制代码
nn.Linear(in_features=3, out_features=3)  # 权重: 3×3=9, 偏置(每个输出对应一个偏置): 3 → 共12  
nn.Linear(3, 2)                           # 权重: 2×3=6, 偏置: 2 → 共8
nn.Linear(2, 2)                           # 权重: 2×2=4, 偏置: 2 → 共6

summary 输出会显示:

Layer Output Shape Param #
Linear-1 [-1, 3] 12
Linear-2 [-1, 2] 8
Linear-3 [-1, 2] 6
  • [-1, 3]-1 表示任意 batch size,3 是该层输出特征数
  • Param # = in_features × out_features + out_features(权重 + 偏置)

Linear-1 为什么参数是12个:

输入3个,输出3个,3 x 3 = 9,每个输出都有一个偏置,所以 9 + 3 = 12

你可以快速核对

  • 第一层输出是不是 3?→ 是
  • 总参数是不是 12+8+6=26?→ 是

如果数字对不上,说明模型定义有误!

✅ 四、实际价值举例

例1:发现维度错误

你误写成:

python 复制代码
self.linear2 = nn.Linear(4, 2)  # 应该是 3→2,但写了 4→2

summary 会报错:

复制代码
RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x3 and 4x2)

→ 你立刻知道:上一层输出是 3,但这一层期望输入是 4,不匹配!

例2:控制模型大小

你想部署到手机,要求参数 < 1000。
summary 显示 Total params: 906 → ✅ 符合要求。

例3:教学演示

学生问:"这个网络中间层输出是多少维?"

你直接展示 summary → 一目了然。

✅ 五、注意事项(避坑指南)

问题 解决方案
报 CUDA 错误 device="cpu"
报维度不匹配 检查 input_size 是否和第一层 in_features 一致
自定义层不显示 torchsummary 对非标准层支持有限,可改用 torchinfo
BatchNorm 报错 默认 batch_size=2 通常能避免,若仍报错可设 batch_size=8

✅ 六、一句话总结使用流程

定义模型 → 移到设备 → 调用 summary(model, input_size=(特征维度,), device="cpu") → 核对输出形状和参数量

🌟 最终建议

把下面这行代码当作模型开发的"标配",每次写完模型就跑一次:

python 复制代码
summary(model, input_size=(你的输入特征维度,), device="cpu")

它花不了 1 秒钟,却能帮你省下 10 分钟 debug 时间!

4、nn.Module.named_parameters

📘 PyTorch 模型参数遍历利器:named_parameters()

  1. 是什么?

named_parameters()torch.nn.Module 类提供的一个生成器方法(generator) ,用于遍历模型中所有可学习(可训练)参数(parameters)及其名称

✅ 返回形式:(name: str, param: torch.Tensor) 的迭代器

✅ 仅包含 requires_grad=True 的参数(即参与反向传播和优化的权重与偏置)

  1. 为什么需要它?

在深度学习中,我们经常需要:

  • 查看模型有哪些参数
  • 检查参数是否正确初始化
  • 冻结某些层(迁移学习)
  • 自定义参数更新逻辑
  • 调试梯度是否流动

named_parameters() 正是实现这些操作的标准入口

  1. 基本语法
python 复制代码
for name, param in model.named_parameters():
    print(f"参数名: {name}")
    print(f"参数值:\n{param}")
    print(f"形状: {param.shape}")
    print(f"是否参与梯度计算: {param.requires_grad}\n")

返回内容说明:

变量 类型 含义
name str 参数的层级命名路径(由模块结构自动生成)
param torch.Tensor 实际的参数张量(如权重矩阵、偏置向量)
  1. 命名规则(name 从哪来?)

PyTorch 根据你在 __init__ 中定义的子模块变量名自动构建参数名:

示例模型:

python 复制代码
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 8)      # 变量名:fc1
        self.classifier = nn.Linear(8, 2)  # 变量名:classifier

对应的 named_parameters() 输出:

复制代码
fc1.weight
fc1.bias
classifier.weight
classifier.bias

🔍 规则:<模块变量名>.weight<模块变量名>.bias

嵌套模块示例:

python 复制代码
self.backbone = nn.Sequential(
    nn.Linear(10, 5),
    nn.ReLU(),
    nn.Linear(5, 2)
)

→ 参数名会变成:

复制代码
backbone.0.weight
backbone.0.bias
backbone.2.weight
backbone.2.bias

(因为 Sequential 中第 0 层和第 2 层是 Linear

  1. 完整代码示例
python 复制代码
import torch
import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear1 = nn.Linear(3, 4)
        self.linear2 = nn.Linear(4, 2)

    def forward(self, x):
        return self.linear2(torch.relu(self.linear1(x)))

model = MyModel()

# 遍历所有可训练参数
for name, param in model.named_parameters():
    print(f"name -> {name}")
    print(f"shape -> {param.shape}")
    print(f"param ->\n{param}\n")

输出片段:

复制代码
name -> linear1.weight
shape -> torch.Size([4, 3])
param ->
tensor([[ 0.123, -0.456,  0.789],
        [ ... ],
        [ ... ],
        [ ... ]], requires_grad=True)

name -> linear1.bias
shape -> torch.Size([4])
...

name -> linear2.weight
...
  1. 核心特点
特性 说明
只包含可训练参数 不包括 BatchNorm 的 running_meannum_batches_tracked 等 buffer
自动递归遍历所有子模块 无论多深的嵌套结构都能覆盖
保留参数梯度信息 param.requires_grad 默认为 True
不包含非 Parameter 的张量 如普通 nn.Parameter 以外的属性不会被包含

💡 如果你需要所有状态(包括 buffer) ,请使用 model.state_dict()

  1. 典型应用场景

✅ 场景 1:调试参数初始化

python 复制代码
# 检查权重是否真的被 Xavier 初始化了
for name, param in model.named_parameters():
    if 'weight' in name:
        print(f"{name} std: {param.std().item():.4f}")

✅ 场景 2:冻结部分层(迁移学习)

python 复制代码
for name, param in model.named_parameters():
    if 'linear1' in name:
        param.requires_grad = False  # 冻结第一层

✅ 场景 3:自定义参数初始化

python 复制代码
for name, param in model.named_parameters():
    if 'bias' in name:
        nn.init.zeros_(param)
    elif 'weight' in name and 'linear2' in name:
        nn.init.kaiming_normal_(param)

✅ 场景 4:查看哪些参数会被优化器更新

python 复制代码
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
print("优化器将更新以下参数:")
for name, _ in model.named_parameters():
    if any(param is _ for param in optimizer.param_groups[0]['params']):
        print(name)

✅ 场景 5:统计总参数量

python 复制代码
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_params}, 可训练: {trainable_params}")
  1. 与其他方法对比
方法 是否含名字 是否含 buffer 是否可训练 用途
model.parameters() 传给优化器
model.named_parameters() 调试、操作特定参数
model.state_dict() ✅(dict key) ✅ + ❌ 保存/加载模型
model.named_modules() --- --- 遍历所有子模块

📌 记住

  • parameters() 给优化器
  • named_parameters() 调试和精细控制
  • state_dict() 保存模型
  1. 常见误区

❌ 误区 1:认为它包含所有模型内部张量

→ 错!只包含 nn.Parameter 类型的可训练参数。

❌ 误区 2:修改 param 就能改变模型行为

→ 要小心!直接赋值 param = new_tensor 无效 ,应使用 param.data = new_tensornn.init

✅ 正确修改方式:

python 复制代码
# ❌ 无效(只是局部变量重新绑定)
param = torch.zeros_like(param)

# ✅ 有效(原地修改张量数据)
param.data = torch.zeros_like(param)
# 或
nn.init.zeros_(param)
  1. 总结

named_parameters() 是 PyTorch 中访问模型可训练参数的"黄金标准"

  • 🔹 返回 (name, param) 对,名字反映模块结构
  • 🔹 仅包含 weightbias 等可学习参数
  • 🔹 广泛用于:初始化检查、层冻结、参数分析、自定义训练逻辑
  • 🔹 与 parameters()state_dict() 互补,各司其职

🌟 一句话口诀

"想看模型里哪些数字会变?named_parameters() 帮你全看见!"

5、nn.Module.named_parameters() 和 nn.Module.parameters()

🧠 核心区别:返回值结构

方法 返回类型 返回内容 用途
model.parameters() 生成器(Generator) 仅包含参数值(torch.nn.Parameter对象) 简单获取所有参数
model.named_parameters() 生成器(Generator) 每个元素是(参数名, 参数值)元组 获取带名称的参数,用于精细控制

简单来说:

  • parameters():只给你参数"值",就像给你一堆书,但没有标签
  • named_parameters():给你参数"值"和"标签",就像给你每本书都贴上了标签

🔍 详细对比分析

  1. 返回值的结构

parameters() 返回什么:

python 复制代码
for param in model.parameters():
    print(param.shape)  # 仅显示参数形状,没有名称

输出示例:

复制代码
torch.Size([64, 128])
torch.Size([64])
torch.Size([10, 64])
torch.Size([10])

named_parameters() 返回什么:

python 复制代码
for name, param in model.named_parameters():
    print(f"{name}: {param.shape}")

输出示例:

复制代码
fc1.weight: torch.Size([64, 128])
fc1.bias: torch.Size([64])
fc2.weight: torch.Size([10, 64])
fc2.bias: torch.Size([10])
  1. 深入理解:PyTorch内部机制

在PyTorch中,nn.Module类内部维护了几个关键数据结构:

  • _parameters: 一个字典,保存模型中所有参数,键是参数名,值是参数
  • _modules: 保存所有子模块
  • _buffers: 保存模型中的缓冲区(如BatchNorm中的running_mean)

当你调用model.parameters()时,它会遍历_parameters字典并返回所有参数值。

当你调用model.named_parameters()时,它会遍历_parameters字典并返回(name, param)元组。

  1. 为什么需要两个方法?

parameters():当不需要知道参数名称时,可以快速获取所有参数。例如,如果你只是想统计参数数量:

python 复制代码
# 统计模型总参数数量
total_params = sum(p.numel() for p in model.parameters())
print(f"总参数数量: {total_params}")

named_parameters():当需要基于参数名称进行操作时,比如为不同层设置不同学习率、冻结特定层等。这是深度学习中非常常见的需求。

🛠️ 实际应用场景详解

  1. 为不同层设置不同学习率(分层优化)

这是named_parameters()最常用的应用场景之一,特别适合在迁移学习和复杂模型中:

python 复制代码
# 为不同层设置不同学习率
optimizer = torch.optim.Adam([
    {"params": [p for n, p in model.named_parameters() if "conv" in n], "lr": 0.0001},
    {"params": [p for n, p in model.named_parameters() if "fc" in n], "lr": 0.001}
])

为什么这很重要?

  • 卷积层(conv)通常学习率设置得低一些,因为它们是特征提取层,需要稳定
  • 全连接层(fc)可以学习得快一些,因为它们是分类层,需要更快适应新任务
  1. 冻结特定层(迁移学习)

在迁移学习中,我们经常想冻结预训练模型的底层,只训练顶层:

python 复制代码
# 冻结卷积层,只训练全连接层
for name, param in model.named_parameters():
    if "conv" in name:  # 假设conv是卷积层
        param.requires_grad = False

为什么这很重要?

  • 预训练模型的底层已经学习了通用特征(如边缘、纹理)
  • 我们不需要重新学习这些特征,只需微调顶层以适应新任务
  • 可以大大减少计算量和训练时间
  1. 参数检查和调试

在训练过程中,检查是否有参数未更新,这有助于排查梯度消失/爆炸问题:

python 复制代码
# 检查未更新的参数
unupdated = []
for name, param in model.named_parameters():
    if param.grad is None:
        unupdated.append(name)
print(f"未更新参数: {unupdated}")

为什么这很重要?

  • 如果某些参数的梯度是None,说明它们没有被更新
  • 可能是学习率太低、参数被冻结或模型结构有问题
  • 有助于快速定位和解决问题
  1. 参数可视化和分析

在模型分析中,我们经常需要知道特定层的参数分布:

python 复制代码
# 打印特定层的参数
for name, param in model.named_parameters():
    if "fc1" in name:
        print(f"fc1参数: {param.data}")
        print(f"fc1梯度: {param.grad}")

为什么这很重要?

  • 了解模型内部参数的变化
  • 分析模型学习过程
  • 为模型优化提供依据

🧪 实际案例:ResNet模型中的应用

以ResNet18为例,看看named_parameters()如何工作:

python 复制代码
import torchvision.models as models
model = models.resnet18()

# 打印部分参数名
for name, param in model.named_parameters():
    print(f"参数名: {name}")
    if "layer1" in name:
        print("  层1的参数...")
    if "layer4" in name:
        print("  层4的参数...")
    if "fc" in name:
        print("  全连接层参数...")
    if "bn" in name:
        print("  批归一化层参数...")
    if "weight" in name:
        print("  权重参数...")
    print("-" * 50)

输出片段:

复制代码
参数名: conv1.weight
  权重参数...
--------------------------------------------------
参数名: conv1.bias
  权重参数...
--------------------------------------------------
参数名: bn1.weight
  批归一化层参数...
--------------------------------------------------
参数名: bn1.bias
  批归一化层参数...
--------------------------------------------------
参数名: layer1.0.conv1.weight
  层1的参数...
  权重参数...
--------------------------------------------------
参数名: layer1.0.conv1.bias
  层1的参数...
  权重参数...
--------------------------------------------------
...
参数名: fc.weight
  全连接层参数...
  权重参数...
--------------------------------------------------

⚠️ 常见错误与注意事项

  1. 混淆参数名和参数值
python 复制代码
# 错误示例:试图从parameters()中获取名称
for name, param in model.parameters():
    print(name, param)  # 这会报错,因为parameters()不返回名称

# 正确做法:使用named_parameters()
for name, param in model.named_parameters():
    print(name, param)
  1. 忘记使用list()转换生成器
python 复制代码
# 错误:直接打印生成器
print(model.parameters())  # 输出:<generator object Module.parameters at 0x7f8c0c2b0d60>

# 正确:转换为列表
print(list(model.parameters()))  # 显示所有参数
  1. 冻结参数后忘记重置优化器
python 复制代码
# 冻结参数
for name, param in model.named_parameters():
    if "conv" in name:
        param.requires_grad = False

# 重要:需要在训练前重置优化器
optimizer.zero_grad()  # 重置梯度
  1. 误以为named_parameters()返回所有参数

named_parameters()只返回可训练参数(requires_grad=True的参数),不包括冻结的参数。

🔗 与其他相关方法的对比

方法 返回内容 用途 与named_parameters()/parameters()的关系
state_dict() 字典,包含模型所有参数和缓冲区 保存和加载模型 与参数方法不同,返回的是字典
modules() 模型中所有模块的生成器 遍历所有模块 不返回参数,返回模块
named_modules() (name, module)元组的生成器 遍历所有模块,带名称 不返回参数,返回模块
children() 模型直接子模块的生成器 遍历直接子模块 不返回参数,返回子模块
named_children() (name, module)元组的生成器 遍历直接子模块,带名称 不返回参数,返回子模块

💡 为什么PyTorch要设计这两个方法?

想象一下,你有一个巨大的书架(模型),里面有很多书(参数):

  • parameters():就像你把所有书都拿了出来,但没有标签,你得一个一个翻才能知道哪本是《深度学习入门》
  • named_parameters():就像你给每本书都贴了标签,你可以直接说"我要《深度学习入门》",立刻找到

在深度学习中,模型通常由多个层组成,每个层都有自己的参数。在训练过程中,我们可能需要:

  • 为不同层设置不同的学习率
  • 冻结某些层(如预训练模型的底层)
  • 检查特定层的梯度是否正常

没有named_parameters(),我们就无法区分这些参数,也就无法进行这些精细控制。

🌟 实用技巧

  1. 为不同层设置学习率的高级用法
python 复制代码
# 为不同层设置不同学习率,同时指定权重衰减
optimizer = torch.optim.Adam([
    {"params": [p for n, p in model.named_parameters() if "conv" in n], "lr": 0.0001, "weight_decay": 1e-5},
    {"params": [p for n, p in model.named_parameters() if "fc" in n], "lr": 0.001, "weight_decay": 1e-4}
])
  1. 冻结特定层并保留梯度
python 复制代码
# 冻结特定层,但保留梯度用于其他操作
for name, param in model.named_parameters():
    if "conv" in name:
        param.requires_grad = False
        # 但可以为这些参数保留梯度,以便在需要时重新启用
        # 例如,用于梯度裁剪或检查
  1. 打印模型参数的统计信息
python 复制代码
# 打印模型参数的统计信息
for name, param in model.named_parameters():
    if param.requires_grad:
        print(f"{name}:")
        print(f"  形状: {param.shape}")
        print(f"  均值: {param.data.mean().item()}")
        print(f"  标准差: {param.data.std().item()}")
        print(f"  最小值: {param.data.min().item()}")
        print(f"  最大值: {param.data.max().item()}")
        print("-" * 50)

📚 为什么这个知识点如此重要?

作为深度学习实践者,你经常会遇到以下场景:

  1. 迁移学习 :你需要冻结预训练模型的底层,只训练顶层。没有named_parameters(),这几乎不可能实现。

  2. 模型调试 :当模型不收敛时,你需要检查哪些参数没有更新。named_parameters()是定位问题的关键。

  3. 模型优化:为不同层设置不同学习率可以显著提高训练效率和模型性能。

  4. 模型压缩 :在模型压缩中,我们需要对特定层进行特殊处理,named_parameters()提供了必要的接口。

💬 最后总结

  • model.parameters():简单获取所有参数,适合需要快速获取参数的场景。
  • model.named_parameters():获取带名称的参数,适合需要基于参数名称进行精细控制的场景。

核心区别named_parameters()提供参数名称,让你能区分不同层的参数,而parameters()只提供参数值,无法区分。

在实际应用中,named_parameters()parameters()更常用,特别是在需要对不同层进行不同处理时。如果你只做简单的模型训练,可能用不到named_parameters();但如果你要进行迁移学习、模型微调或深度调试,named_parameters()几乎是必备的工具。

6、通常不需要手动 初始化输出层 参数

为什么前两层(linear1, linear2)都手动初始化了权重和偏置,而输出层 self.output = nn.Linear(2, 2) 却没有初始化?是不是"不需要设置权重"?

答案是:

不是"不需要",而是"你没写,但它其实已经被自动初始化了"

PyTorch 的 nn.Linear 在创建时会自动进行默认初始化

下面我来为你彻底讲清楚。

✅ 一、nn.Linear 默认会初始化!

当你写:

python 复制代码
self.output = nn.Linear(in_features=2, out_features=2)

PyTorch 内部会自动调用默认的初始化方法

  • 权重(weight) :从均匀分布 \\mathcal{U}(-\\sqrt{k}, \\sqrt{k}) 中采样
    其中 k = 1 / \\text{in_features}
  • 偏置(bias) :初始化为全 0(如果 bias=True,默认就是)

这是 PyTorch 的标准行为,源码中定义在 Linear.reset_parameters() 方法里。

所以 output并不是"没初始化" ,而是用了 PyTorch 默认初始化

🔍 二、对比:你手动初始化 vs 默认初始化

初始化方式 权重分布 偏置
linear1 手动:xavier_normal_ 正态分布,适合 Sigmoid/Tanh 全 0
linear2 手动:kaiming_normal_(ReLU) 正态分布,适合 ReLU 全 0
output 默认初始化 均匀分布(范围小) 全 0

✅ 所以 output 层是有初始值的,只是你没显式打印出来。

🧪 三、验证:打印 output 层的参数

在代码末尾加上:

python 复制代码
print("输出层权重:\n", my_model.output.weight)
print("输出层偏置:\n", my_model.output.bias)

你会看到类似:

复制代码
输出层权重:
 Parameter containing:
 tensor([[ 0.4231, -0.5123],
         [-0.3312,  0.6789]], requires_grad=True)

输出层偏置:
 Parameter containing:
 tensor([0., 0.], requires_grad=True)

→ 权重不是零,也不是 NaN,说明已经初始化了

❓ 四、那要不要手动初始化输出层?

这取决于你的任务和激活函数!

情况 1:输出层后接 softmax(多分类)

  • 通常不需要特殊初始化
  • 默认初始化已经足够好
  • 因为 softmax 对输入尺度不敏感(会归一化)

✅ 所以代码是合理的:可以不手动初始化输出层

情况 2:输出层后接其他激活(如 sigmoid 用于二分类)

  • 有时为了避免初始输出集中在 0 或 1,会微调偏置(比如设为 small negative value)
  • 但一般也不需要改权重

情况 3:你追求极致控制 or 复现实验

  • 可以统一初始化所有层,保持一致性

例如:

python 复制代码
# 统一初始化所有 Linear 层
for layer in [self.linear1, self.linear2, self.output]:
    nn.init.xavier_normal_(layer.weight)
    nn.init.zeros_(layer.bias)

⚠️ 五、常见误区

❌ 误区:"不初始化 = 权重为 0"

→ 错!不手动初始化 ≠ 权重为 0,而是用 PyTorch 默认初始化(非零、有随机性)。

❌ 误区:"输出层不用管,随便初始化"

→ 虽然默认初始化通常够用,但在某些任务中(如不平衡分类),调整输出层偏置能加速收敛。

✅ 六、最佳实践建议

场景 建议
教学/简单实验 可以只初始化隐藏层,输出层用默认(如你当前代码)✅
复现论文/严格控制实验 所有层统一初始化(包括输出层)
多分类任务(softmax) 输出层通常无需特殊处理
二分类(sigmoid)且正样本极少 可设 output.bias = torch.tensor([-2.0]) 避免初始预测全为 1

🌟 总结

self.output = nn.Linear(2, 2) 是会被自动初始化的!

你没写初始化代码 ≠ 没初始化,而是用了 PyTorch 的默认初始化策略

  • ✅ 默认初始化对输出层通常是合适的
  • ✅ 代码没有问题,可以正常训练
  • ✅ 如果你想更严谨,也可以手动初始化输出层(非必须)

7、构建自己的神经网络模型 - 代码

python 复制代码
import torch
import torch.nn as nn
from torchsummary import summary

class MyModel(nn.Module):
    # 定义 __init__ 方法, 构建神经网络
    def __init__(self, *args, **kwargs):
        # 必须调用父类的 __init__ 方法
        super().__init__(*args, **kwargs)

        # 创建隐藏层1
        self.linear1 = nn.Linear(in_features=3, out_features=3)   # 3个输入, 3个输出, 权重矩阵是 (输出, 输入), 即 [out_features, in_features]

        # 隐藏层1参数初始化
        nn.init.xavier_normal_(tensor=self.linear1.weight)    # 正态化的Xavier初始化
        nn.init.zeros_(tensor=self.linear1.bias)
        print(f'隐藏层1的权重: \n{self.linear1.weight}')
        print(f'隐藏层1的偏置: \n{self.linear1.bias}')
        # 隐藏层1的权重:(每次运行结果都不一样)
        # Parameter containing:
        # tensor([[ 0.5180,  0.0963,  0.1452],
        #         [-0.1293,  0.2209,  0.3645],
        #         [-1.1118,  0.3499, -0.6002]], requires_grad=True)
        # 隐藏层1的偏置:
        # Parameter containing:
        # tensor([0., 0., 0.], requires_grad=True)

        # 创建隐藏层2
        # 全连接神经网络: 第N层的每个神经元和第N-1层的所有神经元相连(这就是Fully Connected的含义)
        self.linear2 = nn.Linear(in_features=3, out_features=2)   # 3个输入(需要和上一层的输出相同), 权重矩阵 (2, 3)

        # 隐藏层2参数初始化  【 nonlinearity  n.非线性 】
        nn.init.kaiming_normal_(tensor=self.linear2.weight, nonlinearity='relu')   # 正态分布的he(Kaiming He et al. 2015)初始化
        nn.init.zeros_(tensor=self.linear2.bias)
        print(f'隐藏层2的权重: \n{self.linear2.weight}')
        print(f'隐藏层2的偏置: \n{self.linear2.bias}')
        # 隐藏层2的权重:
        # Parameter containing:
        # tensor([[-0.3379, -0.4480, -0.9356],
        #         [ 0.1274,  0.7152, -0.4393]], requires_grad=True)
        # 隐藏层2的偏置:
        # Parameter containing:
        # tensor([0., 0.], requires_grad=True)

        # 创建输出层(使用默认初始化即可)
        self.output = nn.Linear(in_features=2, out_features=2)


    # 重写 forward, 定义数据流向: 定义前向传播方法, 得到预测值 y
    def forward(self, x):     # x: 输入样本
        z = self.linear1(x)           # 第1层计算: 加权求和,  z 表示线性输出(加权和)
        x = torch.sigmoid(input=z)    # 第1层计算: 使用激活函数, 得到非线性

        z = self.linear2(x)           # 第2层计算: 加权求和
        x = torch.relu(input=z)            # 第2层计算: 使用激活函数, 得到非线性

        z = self.output(x)                   # 第3层计算: 加权求和
        y_predict = torch.softmax(z, dim=1)  # 第3层计算: 输出层计算, 假设为多分类问题,

        return y_predict


if __name__ == '__main__':
    my_model = MyModel()     # 创建神经网络对象
    print(f'神经网络对象: \n{my_model}')
    # MyModel(
    #   (linear1): Linear(in_features=3, out_features=3, bias=True)
    #   (linear2): Linear(in_features=3, out_features=2, bias=True)
    #   (output): Linear(in_features=2, out_features=2, bias=True)
    # )

    # 生成随机样本: 第1层的输入是3, 所以必须是3列
    data = torch.randn(size=(5, 3))    # 从 标准正态分布 N(0,1) (均值为0,方差为1)中生成张量。
    print(f'data.shape = {data.shape}')
    print(f'data.requires_grad = {data.requires_grad}')
    print(f'data: \n{data}')
    # data.shape = torch.Size([5, 3])
    # data.requires_grad = False
    # data:
    # tensor([[ 0.6058, -0.8497, -1.7114],
    #         [-0.0886,  1.7651,  0.4772],
    #         [ 1.2658,  0.9205, -1.2388],
    #         [ 1.9676, -0.1317,  1.5285],
    #         [-0.3718,  0.8589, -1.4751]])

    # 调用神经网络模型对象进行模型训练、预测
    y_predict = my_model(data)   # 自动调用 forward 函数(因为实现了 __call__ 函数)
    print(f'y_predict: \n{y_predict}')
    # tensor([[0.7149, 0.2851],
    #         [0.7452, 0.2548],
    #         [0.7055, 0.2945],
    #         [0.7025, 0.2975],
    #         [0.7185, 0.2815]], grad_fn=<SoftmaxBackward0>)

    print('*' * 22)
    # input_size: 单个样本的输入形状(不含 batch 维度)
    # 调用 summary 会自动打印, 不需要额外的 print
    summary(model=my_model, input_size=(3, ))
    # ----------------------------------------------------------------
    #         Layer (type)               Output Shape         Param #
    # ================================================================
    #             Linear-1                    [-1, 3]              12
    #             Linear-2                    [-1, 2]               8
    #             Linear-3                    [-1, 2]               6
    # ================================================================
    # Total params: 26
    # Trainable params: 26
    # Non-trainable params: 0
    # ----------------------------------------------------------------
    # Input size (MB): 0.00
    # Forward/backward pass size (MB): 0.00
    # Params size (MB): 0.00
    # Estimated Total Size (MB): 0.00
    # ----------------------------------------------------------------

    print('*' * 22)
    # 查看模型参数
    for name, param in my_model.named_parameters():
        print('name->', name)
        print('param->', param)
相关推荐
风象南17 小时前
Claude Code这个隐藏技能,让我告别PPT焦虑
人工智能·后端
Mintopia18 小时前
OpenClaw 对软件行业产生的影响
人工智能
陈广亮18 小时前
构建具有长期记忆的 AI Agent:从设计模式到生产实践
人工智能
会写代码的柯基犬18 小时前
DeepSeek vs Kimi vs Qwen —— AI 生成俄罗斯方块代码效果横评
人工智能·llm
Mintopia19 小时前
OpenClaw 是什么?为什么节后热度如此之高?
人工智能
爱可生开源社区19 小时前
DBA 的未来?八位行业先锋的年度圆桌讨论
人工智能·dba
叁两1 天前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
前端付豪1 天前
LangChain记忆:通过Memory记住上次的对话细节
人工智能·python·langchain
strayCat232551 天前
Clawdbot 源码解读 7: 扩展机制
人工智能·开源