机器学习 / A LEAF IN THE GARDEN

Pytorch神经网络

机器学习 · 动手深度学习。来自 Daily Learning 的学习笔记。

代码 2 · In [1]
import torch
from torch import nn
from torch.nn import functional as F
import torchinfo

层和块

自定义块

代码 5 · In [2]
class MLP(nn.Module):
    def __init__(self):
        # 父类(nn.Module)的初始化
        super().__init__()
        # 网络里的层
        self.hidden = nn.Linear(20, 256)
        self.out = nn.Linear(256, 10)

    def forward(self, X):
        # 把 X 一层一层向前传
        X = self.hidden(X)
        X = F.relu(X)
        X = self.out(X)
        return X
代码 6 · In [3]
net = MLP()
X = torch.rand(size=(2, 20))
net(X).shape # nn.Module.__call__() 方法会调用 forward()
已保存输出
torch.Size([2, 10])

顺序块

代码 8 · In [4]
class MySequential(nn.Module):
    """模仿 nn.Sequential 的实现"""
    def __init__(self, *args): # *args 解包元组
        super().__init__()
        for block in args:
            # nn.Module 的 _modules 属性是一个有序字典,存储了所有子模块
            self._modules[block] = block


    def forward(self, X):
        # _modules.values() 返回一个有序字典的值的迭代器
        for block in self._modules.values():
            X = block(X)
        return X
代码 9 · In [5]
net = MySequential(
    nn.Linear(20, 256),
    nn.ReLU(),
    nn.Linear(256, 10),
)
net(X).shape
已保存输出
torch.Size([2, 10])

沐神这里不知道在玩啥,估计是想表达 pytorch 块的灵活性

代码 11 · In [6]
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) # 使用线性层
        X = F.relu(torch.mm(X, self.rand_weight) + 1) # 相当于一个不能训练的线性层,加上 relu
        X = self.linear(X) # 再使用线性层
        # 这里是最诡异的,一直除 2 直到输出张量的绝对值和小于等于 1
        while X.abs().sum() > 1:
            X /= 2
        # 然后返回输出张量的和
        return X.sum()

参数管理

做一个单隐藏层的 MLP

代码 14 · In [7]
net = nn.Sequential(nn.Linear(4, 8),
                    nn.ReLU(),
                    nn.Linear(8, 1))

访问参数

代码 16 · In [8]
# nn.Sequential 类似于 list,可以下标访问。
    # net[2] 是最后一层 nn.Linear(8, 1)
# state_dict() 返回一个有序字典,存储所有的参数
    # 这里有 weights 和 bias 两个参数张量
net[2].state_dict()
已保存输出
OrderedDict([('weight',
              tensor([[ 0.2640, -0.2366, -0.0655, -0.2188, -0.1823,  0.1116,  0.1881,  0.0392]])),
             ('bias', tensor([-0.1614]))])

)也可以直接去层里拿参数

代码 18 · In [9]
print(net[2].weight.type, '\n') # 参数类型是 Parameter 对象
print(net[2].weight, '\n') # 访问参数对象(有张量和梯度)
print(net[2].weight.data, '\n') # 访问张量
print(net[2].weight.grad) # 访问梯度(如果没有反向传播过,梯度是 None)
已保存输出
<built-in method type of Parameter object at 0x7f43876e0e60> 

Parameter containing: tensor([[ 0.2640, -0.2366, -0.0655, -0.2188, -0.1823, 0.1116, 0.1881, 0.0392]], requires_grad=True)

tensor([[ 0.2640, -0.2366, -0.0655, -0.2188, -0.1823, 0.1116, 0.1881, 0.0392]])

None

一次性访问所有参数(命名参数)

代码 20 · In [10]
# named_parameters() 返回所有参数的 (name, parameter) 对
# 第一层的 name 和 param
print(*[(name, param.shape) for name, param in net[0].named_parameters()], '\n')
# 所有参数的 name 和 param,前面加了层的名字
print(*[(name, param.shape) for name, param in net.named_parameters()])
已保存输出
('weight', torch.Size([8, 4])) ('bias', torch.Size([8])) 

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

根据名字,也可以从 state_dict 里找

代码 22 · In [11]
net.state_dict()['2.bias'].data # 访问输出层的偏置
已保存输出
tensor([-0.1614])

从嵌套块收集参数

代码 24 · In [12]
def block1():
    """返回一个 Sequential 块"""
    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):
        # add_module() 方法可以给 Sequential 块添加子模块。传入名字和模块
        net.add_module(f'block {i}', block1())
    return net
代码 25 · In [13]
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
torchinfo.summary(rgnet, input_size=(2, 4)) # 打印网络结构
已保存输出
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
Sequential                               [2, 1]                    --
├─Sequential: 1-1                        [2, 4]                    --
│    └─Sequential: 2-1                   [2, 4]                    --
│    │    └─Linear: 3-1                  [2, 8]                    40
│    │    └─ReLU: 3-2                    [2, 8]                    --
│    │    └─Linear: 3-3                  [2, 4]                    36
│    │    └─ReLU: 3-4                    [2, 4]                    --
│    └─Sequential: 2-2                   [2, 4]                    --
│    │    └─Linear: 3-5                  [2, 8]                    40
│    │    └─ReLU: 3-6                    [2, 8]                    --
│    │    └─Linear: 3-7                  [2, 4]                    36
│    │    └─ReLU: 3-8                    [2, 4]                    --
│    └─Sequential: 2-3                   [2, 4]                    --
│    │    └─Linear: 3-9                  [2, 8]                    40
│    │    └─ReLU: 3-10                   [2, 8]                    --
│    │    └─Linear: 3-11                 [2, 4]                    36
│    │    └─ReLU: 3-12                   [2, 4]                    --
│    └─Sequential: 2-4                   [2, 4]                    --
│    │    └─Linear: 3-13                 [2, 8]                    40
│    │    └─ReLU: 3-14                   [2, 8]                    --
│    │    └─Linear: 3-15                 [2, 4]                    36
│    │    └─ReLU: 3-16                   [2, 4]                    --
├─Linear: 1-2                            [2, 1]                    5
==========================================================================================
Total params: 309
Trainable params: 309
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 0.00
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
==========================================================================================

参数初始化

内置初始化(nn.init)

代码 28 · In [14]
def init_normal(m: nn.Module):
    """初始化权重为正态分布,偏置为 0"""
    # 只初始化线性层
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, mean=0, std=0.01)
        nn.init.zeros_(m.bias)
# apply() 方法会递归地将 init_normal 应用于 net 的每一层
net.apply(init_normal)
# 第一层的权重,应该是 8x4 的张量,均值接近 0,标准差接近 0.01
net[0].weight.data.mean(), net[0].weight.data.std()
已保存输出
(tensor(0.0010), tensor(0.0105))
代码 29 · In [15]
def init_constant(m: nn.Module):
    """初始化权重为常数 1"""
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 1)
net.apply(init_constant)
net[0].weight.data
已保存输出
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]])

沐神在这又搞了个神秘初始化

代码 31 · In [16]
def my_init(m: nn.Module):
    """50 年内没人能看懂他在干啥"""
    if type(m) == nn.Linear:
        # 打印层的名字和权重形状
        print("Init", *[(name, param.shape) for name, param in m.named_parameters()][0])
        nn.init.uniform_(m.weight, -10, 10)
        # 只保留绝对值大于等于 5 的权重,其余置为 0
        m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight.data
已保存输出
Init weight torch.Size([8, 4])
Init weight torch.Size([1, 8])
已保存输出
tensor([[-0.0000,  0.0000, -7.6377,  8.0257],
        [-0.0000, -0.0000, -0.0000, -7.1209],
        [-0.0000, -5.6221,  0.0000, -0.0000],
        [-0.0000,  0.0000, -5.2305,  0.0000],
        [-0.0000, -6.5059,  9.4995, -0.0000],
        [-6.5164,  5.3369, -8.5269,  0.0000],
        [ 7.3583,  0.0000,  6.8810, -9.8931],
        [ 6.0195, -9.3518, -0.0000,  0.0000]])

更暴力的方法

代码 33 · In [17]
net[0].weight.data[:] = 42 # 所有权重设为 42
net[0].weight.data[0] = 114 # 第一行权重设为 114
net[0].weight.data[0, 0] = 1 # 第一个权重设为 1

net[0].weight.data
已保存输出
tensor([[  1., 114., 114., 114.],
        [ 42.,  42.,  42.,  42.],
        [ 42.,  42.,  42.,  42.],
        [ 42.,  42.,  42.,  42.],
        [ 42.,  42.,  42.,  42.],
        [ 42.,  42.,  42.,  42.],
        [ 42.,  42.,  42.,  42.],
        [ 42.,  42.,  42.,  42.]])

参数绑定

可以共享参数

代码 35 · In [18]
shared = nn.Linear(2, 2)
# 让第二层、第三层共享同一套参数
net = nn.Sequential(nn.Linear(4, 2),
                    nn.ReLU(),
                    shared,
                    nn.ReLU(),
                    shared,
                    nn.ReLU(),
                    nn.Linear(2, 1))
# 两层的权重是同一个张量
net[2].weight.data, net[4].weight.data
已保存输出
(tensor([[-0.6689,  0.5408],
         [ 0.5281, -0.1695]]),
 tensor([[-0.6689,  0.5408],
         [ 0.5281, -0.1695]]))

自定义层

块作为层

层和块其实没有本质区别,都是 nn.Module 的子类。可以把你自己定义的块作为层使用。

层通常指的是一个操作,例如线性变换、卷积、池化等,而块通常是由多个层组成的更复杂的结构,例如多层感知机、卷积神经网络等。

代码 39 · In [19]
class CenteredLayer(nn.Module):
    """输入减去均值,使输出的均值为 0"""
    def __init__(self):
        super().__init__()

    def forward(self, X):
        return X - X.mean()
代码 40 · In [20]
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))
已保存输出
tensor([-2., -1.,  0.,  1.,  2.])
代码 41 · In [21]
net = nn.Sequential(nn.Linear(8, 16), CenteredLayer())
# 输出的均值接近 0
net(torch.rand(4, 8)).mean()
已保存输出
tensor(-3.7253e-09, grad_fn=<MeanBackward0>)

自定参数层

只为层定义参数,再在 forward 用各种方式中使用它们

代码 43 · In [22]
class MyLinear(nn.Module):
    """自定义线性层"""
    def __init__(self, in_features, out_features):
        super().__init__()
        # 定义权重和偏置参数,Parameter 对象有名字和梯度
        self.weight = nn.Parameter(torch.rand(in_features, out_features))
        self.bias = nn.Parameter(torch.zeros(out_features))

    def forward(self, X):
        # 使用矩阵乘法和加法实现线性变换
        return torch.mm(X, self.weight) + self.bias
代码 44 · In [23]
dense = MyLinear(5, 3)
dense.weight
已保存输出
Parameter containing:
tensor([[0.6897, 0.7300, 0.2204],
        [0.1985, 0.9655, 0.8790],
        [0.4186, 0.1495, 0.2961],
        [0.1977, 0.5166, 0.6448],
        [0.5205, 0.9787, 0.2212]], requires_grad=True)

读写文件

加载和保存张量

存储和读取单个张量

代码 48 · In [24]
x = torch.arange(4) # arange(4) 创建从 0 到 3 的张量
torch.save(x, './data/tensor/x-file') # 保存张量到文件
x2 = torch.load('./data/tensor/x-file') # 从文件加载张量
x2
已保存输出
tensor([0, 1, 2, 3])

存储、读取张量列表

代码 50 · In [25]
y = torch.zeros(4)
torch.save([x, y], './data/tensor/xy-list') # 保存张量列表
x2, y2 = torch.load('./data/tensor/xy-list') # 从文件加载张量列表
x2, y2
已保存输出
(tensor([0, 1, 2, 3]), tensor([0., 0., 0., 0.]))

写入或读取 字符串-> 张量 的字典

代码 52 · In [26]
mydict = {'x': x, 'y': y}
torch.save(mydict, './data/tensor/mydict') # 保存字典
mydict2 = torch.load('./data/tensor/mydict') # 从文件加载字典
mydict2
已保存输出
{'x': tensor([0, 1, 2, 3]), 'y': tensor([0., 0., 0., 0.])}

加载和保存模型

保存模型的参数(state_dict)

代码 55 · In [27]
# 创建一个 MLP 模型和输入
net = MLP()
X = torch.rand(size=(2, 20))
# 计算输出
Y = net(X)
代码 56 · In [28]
# 保存模型参数
torch.save(net.state_dict(), './models/mlp.params')
# 加载模型参数
clone = MLP()
# torch.load() 返回一个参数字典,再用 load_state_dict() 方法加载到模型中
clone.load_state_dict(torch.load('./models/mlp.params'))
# 验证加载的模型参数是否正确
Y_clone = clone(X)
Y_clone == Y
已保存输出
tensor([[True, True, True, True, True, True, True, True, True, True],
        [True, True, True, True, True, True, True, True, True, True]])

保存整个模型对象(包括结构和参数)

代码 58 · In [29]
torch.save(net, './models/mlp.model') # 保存整个模型
clone = torch.load('./models/mlp.model', weights_only=False) # 加载整个模型
Y_clone = clone(X)
Y_clone == Y
已保存输出
tensor([[True, True, True, True, True, True, True, True, True, True],
        [True, True, True, True, True, True, True, True, True, True]])

GPU

这里主要讲 cuda。mps 之类的另说

确认是否有gpu

代码 61 · In [40]
# '!' 是 jupyter notebook 的魔法命令,用于在 notebook 中执行 shell 命令
# nvidia-smi 是 NVIDIA 提供的命令行工具,用于查看 GPU 的状态和信息
!nvidia-smi
已保存输出
Wed Aug 19 23:16:27 2026       
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 595.71.05              Driver Version: 595.71.05      CUDA Version: 13.2     |
+-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 4090        On  |   00000000:36:00.0 Off |                  Off |
| 30%   30C    P8             18W /  450W |     490MiB /  24564MiB |      0%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | 0 N/A N/A 1779 C …nvs/daily_learning/bin/python 480MiB | +-----------------------------------------------------------------------------------------+

计算设备

device 对象

代码 64 · In [41]
# 默认 0 号 gpu
device = torch.device('cuda')
# 指定 gpu
device1 = torch.device('cuda:0')
device, device1
已保存输出
(device(type='cuda'), device(type='cuda', index=0))

torch.cuda 模块

代码 66 · In [48]
# 查看是否有可用的 GPU
print(torch.cuda.is_available())
# 查看可用的 GPU 数量
print(torch.cuda.device_count())
# 查看当前默认的 GPU 设备索引
print(torch.cuda.current_device())
# 查看当前默认的 GPU 设备名称
print(torch.cuda.get_device_name(torch.cuda.current_device()))
# 设置当前 GPU 设备
print(torch.cuda.device('cuda:0'))
已保存输出
True
1
0
NVIDIA GeForce RTX 4090
<torch.cuda.device object at 0x7f445acbabd0>

定义比较稳健的获取设备方法

代码 68 · In [43]
def try_gpu(i:int = 0):
    """如果存在,则返回 gpu(i),否则返回 cpu()"""
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

def try_all_gpus():
    """返回所有可用的 gpu 设备列表,没有则返回 cpu"""
    gpus = [torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())]
    return gpus if gpus else [torch.device('cpu')]

try_gpu(), try_all_gpus()
已保存输出
(device(type='cuda', index=0), [device(type='cuda', index=0)])

张量与GPU

查询张量所在的设备

代码 71 · In [44]
x = torch.FloatTensor([1, 2, 3])
x.device # 默认在 cpu 上
已保存输出
device(type='cpu')

将张量移动到 GPU

代码 73 · In [45]
y = x.to(try_gpu()) # 张量拷贝到 GPU 上
z = x.cuda(0) # 也可以直接用 cuda() 方法
y.device, z.device
已保存输出
(device(type='cuda', index=0), device(type='cuda', index=0))

同一个设备上的张量可以直接运算,结果也在同一个设备上

代码 75 · In [46]
y + z
已保存输出
tensor([2., 4., 6.], device='cuda:0')

神经网络与 GPU

代码 77 · In [49]
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu()) # 将模型移动到 GPU 上

net(y)
已保存输出
tensor([-0.2463], device='cuda:0', grad_fn=<ViewBackward0>)

确认模型参数存储在同一个设备上

代码 79 · In [50]
for param in net.parameters():
    print(param.device)
已保存输出
cuda:0
cuda:0