深度学习计算-层和块、参数管理、自定义层
层和块
单个神经网络
(1)接受一些输入;
(2)生成响应的标量输出;
(3)具有一组相关参数(parameters),更新这些参数可以优化某目标函数。
然后,当考虑具有多个输出的网络时,我们利用矢量化算法来描述整层神经元。像单个神经元一样,层:
(1)接受一组输入
(2)生成相应的输出
(3)由一组可调整参数描述。
当我们使用softmax回归时,一个单层本身就是模型。对于多层感知机而言,整个模型及其组成层都是这种架构。整个模型接受原始输入(特征),生成输出(预测),并包含⼀些参数(所有组成层的参数集合)。同样,每个单独的层接收输入(由前一层提供),生成输出(到下一层的输入),并且具有一组可调参数,这些参数根据从下一层反向传播的信号进行更新。事实证明,研究讨论"比单个层大"但"比整个模型小"的组件更有价值。
为了实现这些复杂的网络,我们引入了神经网络块的概念。块(block)可以描述单个层、由多个层组成的组
件或整个模型本身。使用块进行抽象的一个好处是可以将一些块组合成更大的组件,这一过程通常是递归的,如图所示。通过定义代码来按需生成任意复杂度的块,我们可以通过简洁的代码实现复杂的神经网络。
在构造自定义块之前,我们先回顾一下多层感知机的代码。下面的代码生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接隐藏层,然后是一个具有10个隐藏单元且不带激活函数的全连接输出层。
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.0904, 0.0188, 0.1643, -0.1694, -0.2669, 0.0727, -0.0780, -0.1643,
-0.0432, 0.1137],
[-0.0923, -0.1018, 0.1541, -0.0809, -0.1445, -0.0203, 0.0141, -0.0628,
0.0623, 0.1462]], grad_fn=<AddmmBackward0>)
在这个例子中,我们通过实例化nn.Sequential
来构建我们的模型,层的执行顺序是作为参数传递的。简而言之,nn.Sequential
定义了⼀种特殊的Module,即在PyTorch中表示一个块的类,它维护了⼀个由Module组成的有序列表。注意,两个全连接层都是Linear类的实例,Linear类本身就是Module的子类。我们通常通过net(X)调用我们的模型来获得模型的输出。这实际上是net.__call__(X)
的简写。这个前向传播函数非常简单:它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。
自定义块
在实现我们自定义块之前,我们简要总结一下每个块必须提供的基本功能。
-
将输入数据作为其前向传播函数的参数。
-
通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。
-
计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
-
存储和访问前向传播计算所需的参数。
-
根据需要初始化模型参数。
在下面的代码片段中,我们从零开始编写⼀个块。它包含⼀个多层感知机,其具有256个隐藏单元的隐藏层和⼀个10维输出层。下面的MLP类继承了表示块的类。我们的实现只需要提供我们自己的构造函数(Python中的__init__函数)和前向传播函数forward。
这个__init__要先定义好,然后这个前向传播forward在依次调用__init__中的函数。
class MLP(nn.Module):
# ⽤模型参数声明层。这⾥,我们声明两个全连接的层
def __init__(self):
# 调⽤MLP的⽗类Module的构造函数来执⾏必要的初始化。
# 这样,在类实例化时也可以指定其他函数参数,例如模型参数params
super().__init__()
self.hidden = nn.Linear(20, 256) # 隐藏层
self.out = nn.Linear(256, 10) # 输出层
# 定义模型的前向传播,即如何根据输⼊X返回所需的模型输出
def forward(self, X):
# 注意,这⾥我们使⽤ReLU的函数版本,其在nn.functional模块中定义
return self.out(F.relu(self.hidden(X)))
调用
net = MLP()
net(X)
print(net)
tensor([[-0.1011, -0.0037, -0.0747, 0.2348, 0.1905, 0.0327, -0.0759, -0.0561,
0.0168, 0.3603],
[ 0.0841, 0.0386, -0.1649, 0.2326, 0.0607, 0.0092, 0.0386, -0.1004,
0.1536, 0.3863]], grad_fn=<AddmmBackward0>)
MLP(
(hidden): Linear(in_features=20, out_features=256, bias=True)
(out): Linear(in_features=256, out_features=10, bias=True)
)
其实这个就等价于这个:
他是依次调用的,也可以写在一起:都是一样的效果
class MLP_1(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(20, 256)
self.out = nn.Linear(256, 10)
def forward(self, X):
x1=self.hidden(X)
x2=F.relu(x1)
y=self.out(x2)
return y
我们首先看一下前向传播函数,它以X作为输入,计算带有激活函数的隐藏表示,并输出其未规范化的输出值。
块的⼀个主要优点是它的多功能性。我们可以子类化块以创建层(如全连接层的类)、整个模型(如上面的MLP类)或具有中等复杂度的各种组件。我们在接下来的章节中充分利用了这种多功能性,比如在处理卷积神经网络时。
顺序块
现在我们可以更仔细地看看Sequential类是如何工作的,回想一下Sequential的设计是为了把其他模块串起来。为了构建我们自己的简化的MySequential,我们只需要定义两个关键函数:
- ⼀种将块逐个追加到列表中的函数;
- ⼀种前向传播函数,用于将输入按追加块的顺序传递给块组成的"链条"。
下面的MySequential类提供了与默认Sequential类相同的功能。
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for block in args:
# 这⾥,module是Module⼦类的⼀个实例。我们把它保存在'Module'类的成员
# 变量_modules中。_module的类型是OrderedDict
self._modules[block] = block
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X
当MySequential的前向传播函数被调用时,每个添加的块都按照它们被添加的顺序执行。现在可以使用我们的MySequential类重新实现多层感知机。
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
输出
tensor([[-0.0267, -0.1032, -0.0041, -0.2428, -0.0098, 0.0511, -0.0923, -0.0216,
0.1433, -0.0347],
[ 0.0360, -0.1570, 0.0925, -0.2573, 0.0442, 0.1462, -0.0525, -0.0429,
0.1582, -0.0249]], grad_fn=<AddmmBackward0>)
MySequential的用法与之前为Sequential类编写的代码相同,这里就是自己实现了一下这个Sequential.
在前向传播函数中执⾏代码
Sequential类使模型构造变得简单,允许我们组合新的架构,而不必定义自己的类。然而,并不是所有的架构都是简单的顺序架构。当需要更强的灵活性时,我们需要定义自己的块。例如,我们可能希望在前向传播函数中执行Python的控制流。此外,我们可能希望执行任意的数学运算,而不是简单地依赖预定义的神经网络层。
例如,我们需要一个计算函数的层,其中x是输入,w是参数,c是某个在优化过程中没有更新的指定常量。因此我们实现了⼀个FixedHiddenMLP类,如下所示:
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()
在这个FixedHiddenMLP模型中,我们实现了⼀个隐藏层,其权重(self.rand_weight)在实例化时被随机初始化,之后为常量。这个权重不是⼀个模型参数,因此它永远不会被反向传播更新。然后,神经网络将这个固定层的输出通过一个全连接层。
注意,在返回输出之前,模型做了⼀些不寻常的事情:它运行了一个while循环,在L1范数大于1的条件下,将
输出向量除以2,直到它满足条件为止。最后,模型返回了X中所有项的和。注意,此操作可能不会常用于在任何实际任务中,我们只展示如何将任意代码集成到神经网络计算的流程中。
net = FixedHiddenMLP()
net(X)
混合搭配各种组合块的方法
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)
参数管理
在选择了架构并设置了超参数后,我们就进入了训练阶段。此时,我们的目标是找到使损失函数最小化的模型参数值。经过训练后,我们将需要使用这些参数来做出未来的预测。此外,有时我们希望提取参数,以便在其他环境中复用它们,将模型保存下来,以便它可以在其他软件中执行,或者为了获得科学的理解而进行检查。
之前的介绍中,我们只依靠深度学习框架来完成训练的工作,而忽略了操作参数的具体细节。下面:
- 访问参数,用于调试、诊断和可视化;
- 参数初始化;
- 在不同模型组件间共享参数
参数访问
当通过Sequential类定义模型时,我们可以通过索引来访问模型的任意层,从0层开始,其中Relu也算一层就是没有参数。这就像模型是一个列表一样,每层的参数都在其属性中。其中属性有w和b,也就是weight和bias,Relu函数没有参数。
print(net[0].state_dict()) # Linear(4, 8) w[4,8] b[8]
print(net[1].state_dict()) # ReLU() 没有参数
print(net[2].state_dict())# Linear(8, 1) w[8,1] b[1]
我们访问其偏置也就是b:
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
参数是复合的对象,包含值、梯度和额外信息。这就是我们需要显式参数值的原因。除了值之外,我们还可以访问每个参数的梯度。在上面这个网络中,由于我们还没有调用反向传播,所以以参数的梯度处于初始状态。
net[2].weight.grad == None
# True
一次性访问所有参数
当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。当我们处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂,因为我们需要递归整个树来提取每个子块的参数。下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。
# 访问第一个层
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
# ('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
# 访问所有的层
print(*[(name, param.shape) for name, param in net.named_parameters()])
# ('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
这为我们提供了另一种访问网络参数的方式,如下所示。
net.state_dict()['2.bias'].data
# tensor([-0.2016])
从嵌套块收集参数
让我们看看,如果我们将多个块相互嵌套,参数命名约定是如何工作的。我们首先定义一个生成块的函数,然后将这些块组合到更大的块中。
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):
# 在这⾥嵌套了4个block1
net.add_module(f'block {i}', block1())
return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
# tensor([[0.0628],
# [0.0629]], grad_fn=<AddmmBackward0>)
设计了网络后,我们看看它是如何工作的。
print(rgnet)
因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。下面,我们访问第一个主要的块中、第而个子块的第一层的偏置项。
rgnet[0][1][0].bias.data
# tensor([-0.2539, 0.4913, 0.3029, -0.4799, 0.2022, 0.3146, 0.0601, 0.3757])
参数初始化
知道了如何访问参数后,现在我们看看如何正确地初始化参数。深度学习框架提供默认随机初始化,也允许我们创建自定义初始化方法,满足我们通过其他规则实现初始化权重。默认情况下,PyTorch会根据⼀个范围均匀地初始化权重和偏置矩阵,这个范围是根据输入和输出维度计算出的。PyTorch的nn.init
模块提供了多种预置初始化方法。
内置初始化
让我们首先调用内置的初始化器。下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量,且将偏置参数设置为0。
def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
net.apply(init_normal)
输出看看
net[0].weight.data[0], net[0].bias.data[0]
# (tensor([ 0.0116, 0.0025, -0.0048, 0.0073]), tensor(0.))
我们还可以将所有参数初始化为给定的常数,比如初始化为1。当然现实生活中不这么做。
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]
# (tensor([1., 1., 1., 1.]), tensor(0.))
我们还可以对某些块应用不同的初始化方法。例如,下面我们使用Xavier初始化方法初始化第一个神经网络层,然后将第三个神经网络层初始化为常量值42。
def xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
# tensor([-0.2295, -0.2428, 0.2164, 0.2420])
# tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
自定义初始化
在下面的例子中,我们使用以下的分布为任意权重参
数w定义初始化方法:
其中m.weight.data.abs() >= 5代表这个就是如果w的绝对值小于5返回0
def my_init(m):
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)
# m.weight.data.abs() >= 5这个就是绝对值小于5 返回0
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight[:2]
输出
Init weight torch.Size([8, 4])
Init weight torch.Size([1, 8])
tensor([[7.0674, 5.5314, 8.3402, -0.0000],
[9.1243, 0.0000, 0.0000, 7.1388]], grad_fn=<SliceBackward0>)
然后,我们也可以直接设置某一个参数
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
# tensor([42.0000, 6.5314, 9.3402, 1.0000])
参数绑定
有时我们希望在多个层间共享参数:我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared,
nn.ReLU(), nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同⼀个对象,⽽不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])
# tensor([True, True, True, True, True, True, True, True])
# tensor([True, True, True, True, True, True, True, True])
这个例子表明第三个和第五个神经网络层的参数是绑定的。它们不仅值相等,而且由相同的张量表示。因此,如果我们改变其中⼀个参数,另⼀个参数也会改变。这里有一个问题:当参数绑定时,梯度会发生什么情况?
答案是由于模型参数包含梯度,因此在反向传播期间第⼆个隐藏层(即第三个神经网络层)和第三个隐藏层
(即第五个神经网络层)的梯度会加在⼀起。
自定义层
深度学习成功背后的⼀个因素是神经网络的灵活性:我们可以用创造性的方式组合不同的层,从而设计出适用于各种任务的架构。
不带参数的层
首先,我们构造⼀个没有任何参数的自定义层。下⾯
的CenteredLayer类要从其输入中减去均值。要构建它,我们只需继承基础层类并实现前向传播功能。
class CenteredLayer(nn.Module):
def __init__(self):
super().__init__()
def forward(self, X):
# x每个元素减去均值
return X - X.mean()
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))
# 这个均值是3
# tensor([-2., -1., 0., 1., 2.])
现在,我们可以将层作为组件合并到更复杂的模型中。
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
作为额外的健全性检查,我们可以在向该网络发送随机数据后,检查均值是否为0。由于我们处理的是浮点数,因为存储精度的原因,我们仍然可能会看到⼀个非常小的非零数。
Y = net(torch.rand(4, 8))
Y.mean()
带参数的层
以上我们知道了如何定义简单的层,下面我们继续定义具有参数的层,这些参数可以通过训练进行调整。我们可以使用内置函数来创建参数,这些函数提供一些基本的管理功能。比如管理访问、初始化、共享、保存和加载模型参数。这样做的好处之一是:我们不需要为每个自定义层编写自定义的序列化程序。
现在,让我们实现自定义版本的全连接层。回想⼀下,该层需要两个参数,一个用于表示权重,另一个用于表示偏置项。在此实现中,我们使⽤修正线性单元作为激活函数。该层需要输入参数:in_units和units,分别表示输入数和输出数。
class MyLinear(nn.Module):
def __init__(self, in_units, units):
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)
接下来,我们实例化MyLinear类并访问其模型参数。
linear = MyLinear(5, 3)
linear.weight
我们可以使用自定义层直接执行前向传播计算。
linear(torch.rand(2, 5))
# tensor([[0.0450, 0.8390, 0.0000],
# [0.0000, 0.0000, 0.7711]])
我们还可以使用自定义层构建模型,就像使用内置的全连接层一样使用自定义层。
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!