动手学人工智能-深度学习计算4-自定义神经网络层

103 阅读4分钟

深度学习的成功背后有一个重要因素:神经网络的灵活性。我们可以通过创造性的方式组合不同的层,设计出适用于各种任务的架构。随着深度学习的广泛应用,研究人员发明了专门用于处理图像、文本、序列数据,甚至进行动态规划的层。尽管深度学习框架提供了许多标准层,但有时我们会遇到需要自己定义新层的情况,这时自定义层就显得尤为重要。

本文将介绍如何构建自定义层,包括不带参数的层和带参数的层。

1. 不带参数的层

1.1 自定义不带参数的层

首先,我们来看如何定义一个不带参数的自定义层。通常情况下,这种层不需要训练过程中的可学习参数,只有一些简单的数学操作。例如,我们可以构造一个“居中层”(CenteredLayer),它会从输入数据中减去其均值。

为了构建这个层,我们只需要继承PyTorch中的基础层类 nn.Module,并实现其中的 forward 方法。forward 方法定义了输入数据如何通过该层进行计算。

import torch
from torch import nn
from torch.nn import functional as F


class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, X):
        return X - X.mean()  # 将输入数据减去其均值

在上面的代码中,CenteredLayer 类定义了一个简单的操作:计算输入张量的均值,并将其从输入中减去。接下来,我们可以验证这个层是否按预期工作。

layer = CenteredLayer()
output = layer(torch.FloatTensor([1, 2, 3, 4, 5]))
print(output)

输出将会是:

tensor([-2., -1.,  0.,  1.,  2.])

如上所示,输入张量减去了均值(33),得到的结果是一个以 00 为中心的数据。

1.2 将自定义层与其他层结合

我们可以将 CenteredLayer 作为一个组件,加入到更复杂的神经网络模型中。例如,下面是一个包含 CenteredLayer 的简单神经网络:

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

为了验证 CenteredLayer 的效果,我们可以向网络输入随机数据,并检查输出的均值是否接近 00

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

输出可能是一个非常小的数,例如:

tensor(5.5879e-09, grad_fn=<MeanBackward0>)

2. 带参数的层

2.1 定义带参数的层

有时我们需要在自定义层中使用可训练的参数,例如权重偏置。在这种情况下,我们可以通过 nn.Parameter 来创建这些参数,使其在训练过程中能够更新。下面我们通过定义一个自定义的全连接层(MyLinear)来演示如何定义带参数的层。

MyLinear 类需要两个参数:输入单元数 in_units 和输出单元数 units。该层将使用矩阵乘法来计算输出,并通过修正线性单元(ReLU)作为激活函数。

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)  # 使用ReLU激活函数

在上面的代码中,self.weightself.bias 是自定义层的参数。通过 nn.Parameter 创建的参数会在训练过程中自动更新。

2.2 实例化并使用带参数的层

现在我们实例化MyLinear类,并访问其参数:

linear = MyLinear(5, 3)
print(linear.weight)

输出将会显示初始化的权重矩阵,例如:

Parameter containing:
tensor([[-0.1846,  0.2456, -1.2720],
        [ 0.7003,  0.7950,  0.8391],
        [-0.3997,  0.7286, -0.1981],
        [-0.6434, -0.1121,  1.9229],
        [-0.3844, -1.7084, -1.0740]], requires_grad=True)

接着,我们可以使用自定义的 MyLinear 层进行前向计算:

output = linear(torch.randn(2, 5))
print(output)

输出将是一个经过线性变换和ReLU激活后的结果。

tensor([[0.1596, 0.8452, 0.0000],
        [0.0000, 2.3463, 1.2967]])

2.3 使用自定义层构建更复杂的网络

我们可以像使用内置的层一样,使用自定义层来构建更复杂的网络。例如,下面是一个包含两个 MyLinear 层的网络:

net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
output = net(torch.randn(2, 64))
print(output)

输出将是一个通过两层全连接层计算得到的结果。

tensor([[2.9771],
        [0.0000]])

3. 小结

自定义层为我们提供了灵活性,使我们能够根据任务的需求设计新的层。通过继承深度学习框架提供的基础层类,我们可以非常方便地实现不带参数或带参数的自定义层。自定义层不仅可以包含常见的数学操作,还可以处理特殊的计算需求,甚至能够支持训练过程中的参数优化。

通过定义自定义层,我们可以将不同的层组合在一起,构建出更加复杂和高效的神经网络架构。无论是简单的数学操作,还是复杂的前向传播计算,自定义层都能帮助我们实现更加灵活的模型设计。