深度学习计算
一、层和块
1.1 什么是块(module . Block)
在最基础的神经网络中,我们是一层一层计算的:输入层 -> 隐藏层 -> 输出层。
但是,现在的网络太深了(比如ResNet有152层),如果按“层”来写代码,太过于复杂。
于是,深度学习框架引入了一个抽象概念:块(在PyTorch中叫 nn.Module)。
块”就像是乐高积木:
- 单层(比如一个nn.Linear)是一个小积木块。
- 多层组合(比如 线性层 + 激活函数 + 线性层)拼在一起,可以封装成一个大一点的积木块。
- 整个神经网络,其实就是把所有积木块拼在一起形成的一个超大积木块。
块的三个核心能力:
- 接收输入,产生输出(前向传播 forward)。
- 保存参数(比如权重 w 和偏置 b)。
- 能计算梯度并更新参数(反向传播 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])
一次性看光所有参数(极其常用)
-
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]))])
-
-
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)
更多推荐
所有评论(0)