一、层和块

1.1 什么是块(module . Block)

在最基础的神经网络中,我们是一层一层计算的:输入层 -> 隐藏层 -> 输出层。
但是,现在的网络太深了(比如ResNet有152层),如果按“层”来写代码,太过于复杂。

于是,深度学习框架引入了一个抽象概念:块(在PyTorch中叫 nn.Module)

块”就像是乐高积木:

  1. 单层(比如一个nn.Linear)是一个小积木块。
  2. 多层组合(比如 线性层 + 激活函数 + 线性层)拼在一起,可以封装成一个大一点的积木块。
  3. 整个神经网络,其实就是把所有积木块拼在一起形成的一个超大积木块。

块的三个核心能力:

  1. 接收输入,产生输出(前向传播 forward)。
  2. 保存参数(比如权重 w 和偏置 b)。
  3. 能计算梯度并更新参数(反向传播 backward,但感谢深度学习框架,这一步自动完成,不需要我们写)。

代码示例:

import torch
from torch import nn
from torch.nn import functional as F

net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

X = torch.rand(2, 20)
net(X)

输出:

tensor([[ 0.1492,  0.0410, -0.0095, -0.0028, -0.0883,  0.0542,  0.2303, -0.0324,
         -0.1823, -0.0405],
        [ 0.2362,  0.0605, -0.0275,  0.0723,  0.1714,  0.1456,  0.1556, -0.0457,
         -0.0717, -0.1022]], grad_fn=<AddmmBackward0>)

这里是实例化了多层感知机的层,然后每次调用正向传播函数时调用这些层。

1.2 自定义块示例

我们手写一个块来实现前面示例的功能:

import torch
from torch import nn
from torch.nn import functional as F

# 1. 继承 nn.Module:告诉PyTorch,我是一个块
class MLP(nn.Module):
    
    # 2. __init__ 函数:定义参数和层
    def __init__(self):
        # 调用父类的初始化,给这块注册各种内部功能(比如梯度追踪)
        super().__init__() 
        
        # 准备两个两个全连接层
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)     # 输出层

    # 3. forward 函数:定义前向传播逻辑
    def forward(self, X):
        # X 经过隐藏层 -> 经过 ReLU 激活 -> 经过输出层 -> 返回结果
        return self.out(F.relu(self.hidden(X)))

1.3 什么是Sequential

前面的 nn.Sequential… 一行代码达到我们手写的十多行代码的功能。

其实,Sequential 本质上就是官方写好的一个特殊的“块”

它的内部逻辑非常简单,我们手写一个MySequential:

class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for i, module in enumerate(args):
            # module 是 Module 子类的一个实例
            # _modules 是一个 OrderedDict
            self._modules[str(i)] = module

    def forward(self, X):
        # OrderedDict 保证了按照成员添加的顺序遍历
        for block in self._modules.values():
            X = block(X)
        return X

net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
  • 其实就是按顺序保存我们给定的块
  • 然后前向传播的时候按顺序调用

输出:

tensor([[ 0.2768,  0.0146,  0.0942,  0.0807, -0.2026, -0.0390, -0.1409, -0.0476,
          0.0255,  0.1032],
        [ 0.2284,  0.0940,  0.2281,  0.0921, -0.1149, -0.1638, -0.2174, -0.1115,
          0.0052,  0.1253]], grad_fn=<AddmmBackward0>)

1.4 为什么要自定义块?

如果只是串联网络,用 nn.Sequential 就够了。为什么还要辛辛苦苦写 class?

事实上,深度学习的研究通常需要非常规的设计。

class FixedHiddenMLP(nn.Module):
    def __init__(self):
        super().__init__()
        # 不计算梯度的随机权重参数。因此其在训练期间保持不变
        self.rand_weight = torch.rand((20, 20), requires_grad=False)
        self.linear = nn.Linear(20, 20)

    def forward(self, X):
        X = self.linear(X)
        # 使用创建的常量参数以及relu和mm函数
        X = F.relu(torch.mm(X, self.rand_weight) + 1)
        # 复用全连接层。这相当于两个全连接层共享参数
        X = self.linear(X)
        # 控制流
        while X.abs().sum() > 1:
            X /= 2
        return X.sum()
  • 这里我们生成了一个随机矩阵,并设置 requires_grad=False。这意味着在训练时,它的值是定死的,不会随着梯度下降而改变。这在某些场景中非常有用。
  • 我们还在前向传播中加入了 while 控制流

我们还可以嵌套使用:

#@tab pytorch
class NestMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
                                 nn.Linear(64, 32), nn.ReLU())
        self.linear = nn.Linear(32, 16)

    def forward(self, X):
        return self.linear(self.net(X))

chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)

输出

tensor(-0.1805, grad_fn=<SumBackward0>)

二、参数管理

怎么去查看、修改、初始化和共享神经网络的参数?

2.1 参数访问

假设我们建了一个简单的两层网络:

import torch
from torch import nn

# 0层: Linear(4,8)
# 1层: ReLU()
# 2层: Linear(8,1)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))

访问特定层的参数

nn.Sequential 就像一个列表(List)。想看哪一层,直接用索引去拿。

  • net[0] 就是第一个全连接层。
  • net[1] 是 ReLU 激活函数(这层没有参数)。
  • net[2] 是第二个全连接层。

怎么看具体数值?

在 PyTorch 中,层的参数存在 .weight 和 .bias 里。它们不是简单的数字,而是包含了很多附加信息(比如梯度)的特殊对象。

  • 拿具体数值:用 .data
  • 拿梯度(导数):用 .grad(没训练前它是空的/None)
# 拿到第二个全连接层的偏置(Bias)的实际数值
print(net[2].bias.data)

输出:

tensor([-0.3049])

一次性看光所有参数(极其常用)

  1. net.state_dict() (状态字典)
    它会把模型里所有的参数打包成一个字典(Dictionary)。保存模型、加载模型,其实存的就是这个 state_dict。

    • net.state_dict()
      

      输出:

      OrderedDict([('0.weight',
                    tensor([[-0.0374,  0.3565, -0.1123, -0.0794],
                            [ 0.2829, -0.2780, -0.2709, -0.2521],
                            [-0.3619, -0.1657, -0.0441,  0.3440],
                            [-0.4781, -0.4682, -0.2878, -0.3242],
                            [-0.1087, -0.3281, -0.2288, -0.0621],
                            [-0.2360, -0.0401, -0.4945, -0.4236],
                            [-0.2741,  0.0395,  0.3928,  0.3536],
                            [-0.4630, -0.1073,  0.2529, -0.2306]])),
                   ('0.bias',
                    tensor([ 0.2064, -0.1857, -0.3083, -0.4128, -0.4388,  0.1397, -0.4284,  0.0706])),
                   ('2.weight',
                    tensor([[ 0.3486, -0.2258, -0.3285, -0.0370,  0.3190,  0.2794, -0.1570,  0.2787]])),
                   ('2.bias', tensor([-0.3049]))])
      
  2. net.named_parameters() (命名参数)
    用 for 循环遍历所有的参数名和参数张量(Tensor),常用于打印调试:

    • for name, param in net.named_parameters():
          print(name, param.shape)
      

      输出:

      0.weight torch.Size([8, 4])
      0.bias torch.Size([8])
      2.weight torch.Size([1, 8])
      2.bias torch.Size([1])
      

    嵌套块的参数

    #@tab pytorch
    def block1():
        return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                             nn.Linear(8, 4), nn.ReLU())
    
    def block2():
        net = nn.Sequential()
        for i in range(4):
            # 在这里嵌套
            net.add_module(f'block {i}', block1())
        return net
    
    rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
    X = torch.rand(2, 4)
    rgnet(X)
    

    输出:

    tensor([[0.2044],
            [0.2042]], grad_fn=<AddmmBackward0>)
    
    print(rgnet)
    

    输出:

    Sequential(
      (0): Sequential(
        (block 0): Sequential(
          (0): Linear(in_features=4, out_features=8, bias=True)
          (1): ReLU()
          (2): Linear(in_features=8, out_features=4, bias=True)
          (3): ReLU()
        )
        (block 1): Sequential(
          (0): Linear(in_features=4, out_features=8, bias=True)
          (1): ReLU()
          (2): Linear(in_features=8, out_features=4, bias=True)
          (3): ReLU()
        )
        (block 2): Sequential(
          (0): Linear(in_features=4, out_features=8, bias=True)
          (1): ReLU()
          (2): Linear(in_features=8, out_features=4, bias=True)
          (3): ReLU()
        )
        (block 3): Sequential(
          (0): Linear(in_features=4, out_features=8, bias=True)
          (1): ReLU()
          (2): Linear(in_features=8, out_features=4, bias=True)
          (3): ReLU()
        )
      )
      (1): Linear(in_features=4, out_features=1, bias=True)
    )
    
    

    因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。
    下面,我们访问第一个主要的块中、第二个子块的第一层的偏置项。

rgnet[0][1][0].bias.data

输出:

tensor([-0.1853,  0.4081,  0.2124,  0.4229, -0.4867, -0.0189, -0.3486,  0.1886])

2.2 参数初始化

为什么要初始化?
如果不手动设置,PyTorch 会用一套默认的随机算法给权重赋值。但有时候我们需要特定的开局(权重太大会爆炸,太小会消失)。

怎么一次性把我们想要的规则应用到所有的层呢?PyTorch 用了一个非常优雅的方法:.apply() 方法

内置初始化

假设我们想把所有的 nn.Linear 层的**权重(Weight)**变成均值为0、标准差为0.01的正态分布,**偏置(Bias)**全设为0。

写一个辅助函数然后让网络 apply:

def init_normal(m):
    # m 代表网络中的某一个模块(层)
    # 如果这个模块是全连接层 (Linear)
    if type(m) == nn.Linear:
        # 修改权重为正态分布 (注意后面有个下划线 '_',在PyTorch里代表直接原地修改数据)
        nn.init.normal_(m.weight, mean=0, std=0.01)
        # 修改偏置为 0
        nn.init.zeros_(m.bias)

# 让网络 net 里的每一个层都被 init_normal 检查和处理一下
net.apply(init_normal)

自定义初始化

def my_init(m):
    if type(m) == nn.Linear:
        # 1. 先全部设为 -10 到 10 的均匀分布
        nn.init.uniform_(m.weight, -10, 10)
        # 2. 算出一个条件:绝对值是不是 >= 5 (返回 True(1) 或 False(0))
        mask = m.weight.data.abs() >= 5
        # 3. 两个矩阵相乘。绝对值小于5的乘以0就变0了,大于等于5的保留原值。
        m.weight.data *= mask

参数绑定

什么是参数绑定(参数共享)?
正常情况下,每一层都有自己独立的权重。但有时为了节省显存,或者由于某种算法设计要求(比如处理文本序列的RNN/Transformer),我们希望网络里不同的两层,共用同一套权重

怎么做?
在定义网络前,先把这个特殊的层实例化出来赋给一个变量,然后把这个变量塞进网络两次

# 1. 先打造一个名为 shared 的专属“公用零件”
shared = nn.Linear(8, 8)

# 2. 把这个公用零件,塞进流水线的第二步和第四步
net = nn.Sequential(
    nn.Linear(4, 8), nn.ReLU(),
    shared,          nn.ReLU(),  # 第一次用 shared
    shared,          nn.ReLU(),  # 第二次用 shared
    nn.Linear(8, 1)
)

共享参数时,梯度怎么算?

想象一下:shared 这个层,在数据正向传播时,参与了两次计算。
那么反向传播求导数(梯度)时会发生什么?

  • 第一次使用 shared :权重改动量是 ΔW1\Delta W_1ΔW1
  • 第二次使用 shared :权重改动量是 ΔW2\Delta W_2ΔW2
  • 最终结果: PyTorch 会自动把这两次的梯度相加,也就是总梯度 =ΔW1+ΔW2= \Delta W_1 + \Delta W_2=ΔW1+ΔW2

这就是为什么前面提到参数不仅包含值,还包含梯度。共享参数时,更新的幅度是所有使用了该参数的层所计算出的梯度的累加和

三、自定义层

不带参数的层

有时候,我们需要在网络中插入一步纯数学运算,比如“把输入的数据减去它们的平均值”。这个操作本身是不需要学习任何权重(没有 和b)的。

import torch
from torch import nn

class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__() 

    # 前向传播
    def forward(self, X):
        return X - X.mean()  # 把 X 减去它的均值

带参数的层

class MyLinear(nn.Module):
    # in_units: 输入特征维度; units: 输出特征维度
    def __init__(self, in_units, units):
        super().__init__()
        # 1. 创建权重 W,并用 nn.Parameter 声明它是可训练的参数!
        self.weight = nn.Parameter(torch.randn(in_units, units))
        # 2. 创建偏置 b,同样声明为可训练参数
        self.bias = nn.Parameter(torch.randn(units,))
        
    def forward(self, X):
        # 3. 矩阵乘法:X 乘以 W,再加上 b
        linear = torch.matmul(X, self.weight) + self.bias
        # 4. 经过 ReLU 激活函数后输出
        return torch.nn.functional.relu(linear)
  • 注意 self.weight 初始化不用 nn.Parameter 的话,pytorch会认为这只是一个普通矩阵
# 完美融入 PyTorch 生态, 我们可以把它和其它层混着用
net = nn.Sequential(
    MyLinear(64, 8), # 第一层是我们自己手写的
    MyLinear(8, 1)   # 第二层也是我们自己手写的
)

X = torch.rand(2, 64)
print(net(X)) # 成功运行正向传播!

输出:

tensor([[7.6324],
        [9.6935]], grad_fn=<ReluBackward0>)

四、读写文件

4.1 基础数据的读写(保存张量 Tensor)

PyTorch 提供了极其简单的两个函数:

  • 存文件:torch.save(数据, ‘文件名’)
  • 读文件:torch.load(‘文件名’)

可以把多个张量放进一个**列表(List)或者字典(Dictionary)**里,一次性全存进去

import torch

x = torch.arange(4)
y = torch.zeros(4)

# 1. 存入字典
mydict = {'x_data': x, 'y_data': y}
torch.save(mydict, 'my_tensor_file.pt') 

# 2. 从硬盘读回内存 (加上 weights_only=True 开启安全模式)
loaded_dict = torch.load('my_tensor_file.pt', weights_only=True)

print(loaded_dict['x_data']) # 完美打印 [0, 1, 2, 3]

4.2 保存和加载神经网络

在深度学习中,我们通常只保存参数(权重的数值),而不保存网络的代码架构

保存模型

  • state_dict()(状态字典)。它是一个 Python 字典,里面把所有层的名字和对应的权重张量一一对应好了。
import torch
from torch import nn
from torch.nn import functional as F

net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

# 1. 提取所有参数组成的字典
params_dict = net.state_dict()

# 2. 把这个字典存到硬盘上(通常命名为 .params, .pt 或 .pth)
torch.save(params_dict, 'mlp.params')

加载模型

clone_net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

loaded_params = torch.load('mlp.params', weights_only=True)

clone_net.load_state_dict(loaded_params)

clone_net.eval()

为什么要写 clone_net.eval()?因为有些特殊的层,比如 Dropout 层和 BatchNorm 层,在“训练状态”和“测试状态”下的行为是不一样的。读档通常是为了做预测,所以要调成 eval 测试模式。)

五、GPU使用

5.1 查看 GPU 状态

!nvidia-smi

5.2 在代码里指定设备(Device)

import torch

# 默认通常是 cuda:0

with torch.cuda.device(0):
    # 此处的操作将在 cuda:0 上进行
    x = torch.ones(10).cuda() 
    print(f"Tensor is on: {x.device}") # 输出: cuda:0

# 离开 with 块后,回复为默认或之前的设备
y = torch.ones(10).cuda()
print(f"Tensor is on: {y.device}") # 输出: cuda:0 (或其他默认)

输出:

Tensor is on: cuda:0
Tensor is on: cuda:0

5.3 查询可用gpu的数目

torch.cuda.device_count()

输出:

1

5.4 更为安全的使用方式

def try_gpu(i = 0):
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')
try_gpu(2)

输出:

device(type='cpu')
def try_all_gpus():
    devices = [
        torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())]
    return devices if devices else [torch.device('cpu')]
try_all_gpus()

输出:

[device(type='cuda', index=0)]

5.5 查询张量所在的设备

x = torch.tensor([1, 2, 3])
x.device

输出:

device(type='cpu')

5.6 存储在GPU上

X = torch.ones(2, 3, device=try_gpu())
X

输出:

tensor([[1., 1., 1.],
        [1., 1., 1.]], device='cuda:0')
X.device

输出:

device(type='cuda', index=0)

如果两个张量在不同的device上,计算时候需要指定设备:

X = torch.ones(2, 3, device=try_gpu(0))
Y = torch.ones(2, 3, device=try_gpu(1))
X.device, Y.device

输出:

(device(type='cuda', index=0), device(type='cpu'))
Y.cuda(0) + X

输出:

tensor([[2., 2., 2.],
        [2., 2., 2.]], device='cuda:0')

5.7 神经网络与GPU

from torch import nn
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device = try_gpu())
X = torch.ones(3,device = try_gpu())
net(X)

输出:

tensor([-0.8211], device='cuda:0', grad_fn=<ViewBackward0>)

5.8 确认模型参数存储在同一个GPU上

net[0].weight.data.device

输出:

device(type='cuda', index=0)

更多推荐