从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)
相关推荐
明天好,会的2 小时前
分形生成实验:在有限上下文中构建可组合的强类型单元
人工智能
week_泽2 小时前
6、OpenCV SURF特征检测笔记
人工智能·笔记·opencv
AI即插即用2 小时前
即插即用系列 | CVPR 2025 DICMP:基于深度信息辅助的图像去雾与深度估计双任务协同互促网络
图像处理·人工智能·深度学习·神经网络·计算机视觉·视觉检测
Coder_Boy_2 小时前
基于SpringAI的智能平台基座开发-(五)
java·人工智能·spring boot·langchain·springai
AI即插即用2 小时前
即插即用系列 | WACV 2024 CSAM:面向各向异性医学图像分割的 2.5D 跨切片注意力模块
图像处理·人工智能·深度学习·神经网络·目标检测·计算机视觉·视觉检测
今夕资源网2 小时前
仙宫云自动抢算力工具可后台运行,仙宫云自动抢卡,仙宫云自动抢显卡,AI云平台抢算力
人工智能·后台·仙宫云·抢算力·抢显卡·抢gpu
小小工匠2 小时前
LLM - AgentScope + Mem0 搭建实战可用的 AI Agent 记忆系统
人工智能·mem0·agentscope
LucianaiB2 小时前
【基于昇腾平台的CodeLlama实践:从环境搭建到高效开发】
运维·人工智能·性能优化
工藤学编程2 小时前
零基础学AI大模型之LangChain Tool工具
人工智能·langchain