Pytorch神经网络
机器学习 · 动手深度学习。来自 Daily Learning 的学习笔记。
import torch
from torch import nn
from torch.nn import functional as F
import torchinfo
层和块
自定义块
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
net = MLP()
X = torch.rand(size=(2, 20))
net(X).shape # nn.Module.__call__() 方法会调用 forward()
已保存输出
torch.Size([2, 10])
顺序块
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
net = MySequential(
nn.Linear(20, 256),
nn.ReLU(),
nn.Linear(256, 10),
)
net(X).shape
已保存输出
torch.Size([2, 10])
沐神这里不知道在玩啥,估计是想表达 pytorch 块的灵活性
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
net = nn.Sequential(nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 1))
访问参数
# 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]))]))也可以直接去层里拿参数
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
一次性访问所有参数(命名参数)
# 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 里找
net.state_dict()['2.bias'].data # 访问输出层的偏置
已保存输出
tensor([-0.1614])
从嵌套块收集参数
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
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)
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))
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.]])沐神在这又搞了个神秘初始化
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]])更暴力的方法
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.]])参数绑定
可以共享参数
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 的子类。可以把你自己定义的块作为层使用。
层通常指的是一个操作,例如线性变换、卷积、池化等,而块通常是由多个层组成的更复杂的结构,例如多层感知机、卷积神经网络等。
class CenteredLayer(nn.Module):
"""输入减去均值,使输出的均值为 0"""
def __init__(self):
super().__init__()
def forward(self, X):
return X - X.mean()
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))
已保存输出
tensor([-2., -1., 0., 1., 2.])
net = nn.Sequential(nn.Linear(8, 16), CenteredLayer())
# 输出的均值接近 0
net(torch.rand(4, 8)).mean()
已保存输出
tensor(-3.7253e-09, grad_fn=<MeanBackward0>)
自定参数层
只为层定义参数,再在 forward 用各种方式中使用它们
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
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)读写文件
加载和保存张量
存储和读取单个张量
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])
存储、读取张量列表
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.]))
写入或读取 字符串-> 张量 的字典
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)
# 创建一个 MLP 模型和输入
net = MLP()
X = torch.rand(size=(2, 20))
# 计算输出
Y = net(X)
# 保存模型参数
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]])保存整个模型对象(包括结构和参数)
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
# '!' 是 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 对象
# 默认 0 号 gpu
device = torch.device('cuda')
# 指定 gpu
device1 = torch.device('cuda:0')
device, device1
已保存输出
(device(type='cuda'), device(type='cuda', index=0))
torch.cuda 模块
# 查看是否有可用的 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>
定义比较稳健的获取设备方法
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
查询张量所在的设备
x = torch.FloatTensor([1, 2, 3])
x.device # 默认在 cpu 上
已保存输出
device(type='cpu')
将张量移动到 GPU
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))
同一个设备上的张量可以直接运算,结果也在同一个设备上
y + z
已保存输出
tensor([2., 4., 6.], device='cuda:0')
神经网络与 GPU
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>)
确认模型参数存储在同一个设备上
for param in net.parameters():
print(param.device)
已保存输出
cuda:0 cuda:0