深度学习计算-层和块、参数管理、自定义层

 

层和块

单个神经网络
(1)接受一些输入;
(2)生成响应的标量输出;
(3)具有一组相关参数(parameters),更新这些参数可以优化某目标函数。
然后,当考虑具有多个输出的网络时,我们利用矢量化算法来描述整层神经元。像单个神经元一样,层:
(1)接受一组输入
(2)生成相应的输出
(3)由一组可调整参数描述。
当我们使用softmax回归时,一个单层本身就是模型。对于多层感知机而言,整个模型及其组成层都是这种架构。整个模型接受原始输入(特征),生成输出(预测),并包含⼀些参数(所有组成层的参数集合)。同样,每个单独的层接收输入(由前一层提供),生成输出(到下一层的输入),并且具有一组可调参数,这些参数根据从下一层反向传播的信号进行更新。事实证明,研究讨论"比单个层大"但"比整个模型小"的组件更有价值。
为了实现这些复杂的网络,我们引入了神经网络的概念。块(block)可以描述单个层、由多个层组成的组
件或整个模型本身。使用块进行抽象的一个好处是可以将一些块组合成更大的组件,这一过程通常是递归的,如图所示。通过定义代码来按需生成任意复杂度的块,我们可以通过简洁的代码实现复杂的神经网络。

image
在构造自定义块之前,我们先回顾一下多层感知机的代码。下面的代码生成一个网络,其中包含一个具有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>)

image
在这个例子中,我们通过实例化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)
)

image
其实这个就等价于这个:
image
他是依次调用的,也可以写在一起:都是一样的效果

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

image
我们首先看一下前向传播函数,它以X作为输入,计算带有激活函数的隐藏表示,并输出其未规范化的输出值。
块的⼀个主要优点是它的多功能性。我们可以子类化块以创建层(如全连接层的类)、整个模型(如上面的MLP类)或具有中等复杂度的各种组件。我们在接下来的章节中充分利用了这种多功能性,比如在处理卷积神经网络时。

顺序块

现在我们可以更仔细地看看Sequential类是如何工作的,回想一下Sequential的设计是为了把其他模块串起来。为了构建我们自己的简化的MySequential,我们只需要定义两个关键函数:

  1. ⼀种将块逐个追加到列表中的函数;
  2. ⼀种前向传播函数,用于将输入按追加块的顺序传递给块组成的"链条"。
    下面的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的控制流。此外,我们可能希望执行任意的数学运算,而不是简单地依赖预定义的神经网络层。
例如,我们需要一个计算函数f(x,w)=c·wx的层,其中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)

image

混合搭配各种组合块的方法

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)

image

参数管理

在选择了架构并设置了超参数后,我们就进入了训练阶段。此时,我们的目标是找到使损失函数最小化的模型参数值。经过训练后,我们将需要使用这些参数来做出未来的预测。此外,有时我们希望提取参数,以便在其他环境中复用它们,将模型保存下来,以便它可以在其他软件中执行,或者为了获得科学的理解而进行检查。
之前的介绍中,我们只依靠深度学习框架来完成训练的工作,而忽略了操作参数的具体细节。下面:

  • 访问参数,用于调试、诊断和可视化;
  • 参数初始化;
  • 在不同模型组件间共享参数

参数访问

当通过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]

image
我们访问其偏置也就是b:

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)

image
参数是复合的对象,包含值、梯度和额外信息。这就是我们需要显式参数值的原因。除了值之外,我们还可以访问每个参数的梯度。在上面这个网络中,由于我们还没有调用反向传播,所以以参数的梯度处于初始状态。

net[2].weight.grad == None

# True

image

一次性访问所有参数

当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。当我们处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂,因为我们需要递归整个树来提取每个子块的参数。下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。

# 访问第一个层
print(*[(name, param.shape) for name, param in net[0].named_parameters()])

# ('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))

image

# 访问所有的层
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]))

image
这为我们提供了另一种访问网络参数的方式,如下所示。

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>)

image
设计了网络后,我们看看它是如何工作的。

print(rgnet)

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

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

# tensor([-0.2539, 0.4913, 0.3029, -0.4799, 0.2022, 0.3146, 0.0601, 0.3757])

image

参数初始化

知道了如何访问参数后,现在我们看看如何正确地初始化参数。深度学习框架提供默认随机初始化,也允许我们创建自定义初始化方法,满足我们通过其他规则实现初始化权重。默认情况下,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.))

image
我们还可以将所有参数初始化为给定的常数,比如初始化为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定义初始化方法:
image
其中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>)

image

然后,我们也可以直接设置某一个参数

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])

image

参数绑定

有时我们希望在多个层间共享参数:我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。

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])

image
这个例子表明第三个和第五个神经网络层的参数是绑定的。它们不仅值相等,而且由相同的张量表示。因此,如果我们改变其中⼀个参数,另⼀个参数也会改变。这里有一个问题:当参数绑定时,梯度会发生什么情况?
答案是由于模型参数包含梯度,因此在反向传播期间第⼆个隐藏层(即第三个神经网络层)和第三个隐藏层
(即第五个神经网络层)的梯度会加在⼀起。

自定义层

深度学习成功背后的⼀个因素是神经网络的灵活性:我们可以用创造性的方式组合不同的层,从而设计出适用于各种任务的架构。

不带参数的层

首先,我们构造⼀个没有任何参数的自定义层。下⾯
的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.])

image

现在,我们可以将层作为组件合并到更复杂的模型中。

net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())

作为额外的健全性检查,我们可以在向该网络发送随机数据后,检查均值是否为0。由于我们处理的是浮点数,因为存储精度的原因,我们仍然可能会看到⼀个非常小的非零数。

Y = net(torch.rand(4, 8))
Y.mean()

image

带参数的层

以上我们知道了如何定义简单的层,下面我们继续定义具有参数的层,这些参数可以通过训练进行调整。我们可以使用内置函数来创建参数,这些函数提供一些基本的管理功能。比如管理访问、初始化、共享、保存和加载模型参数。这样做的好处之一是:我们不需要为每个自定义层编写自定义的序列化程序。
现在,让我们实现自定义版本的全连接层。回想⼀下,该层需要两个参数,一个用于表示权重,另一个用于表示偏置项。在此实现中,我们使⽤修正线性单元作为激活函数。该层需要输入参数: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

image
我们可以使用自定义层直接执行前向传播计算。

linear(torch.rand(2, 5))

# tensor([[0.0450, 0.8390, 0.0000],
#        [0.0000, 0.0000, 0.7711]])

image
我们还可以使用自定义层构建模型,就像使用内置的全连接层一样使用自定义层。

net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))

image

posted @   lipu123  阅读(111)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示