动手学人工智能-深度学习计算1-层和块

291 阅读5分钟

在构建神经网络时,我们通常会涉及到 层(layer)和块(block) 的概念。理解这些概念对于构建复杂的神经网络模型至关重要。让我们从最基础的层开始,逐步引入块的概念,并通过实例加深理解。

多层.svg

1. 什么是层?

在神经网络的基本结构中,每一层都有特定的功能。首先,我们来看一个简单的神经网络,它只有一个输出:

  • 输入:神经网络接收一些输入数据。
  • 输出:网络产生相应的输出(例如分类结果)。
  • 参数:每一层都有一组可调整的参数,这些参数在训练过程中会被优化。

以一个简单的线性模型为例,模型有一个输入和一个输出,这个过程会根据网络中层的结构来定义。如果我们增加更多的层,神经网络会变得更加复杂,每一层都会接收前一层的输出作为输入,并生成新的输出,最终形成整个网络的预测结果。

2. 块的概念

当神经网络变得越来越复杂时,单一的层可能无法满足需求。因此,我们引入了 块(block) 的概念。块可以是一个单独的层、多个层的组合,甚至是整个模型。块的主要优势是它能将多个层封装在一起,形成一个高层次的模块。

块的定义通常由类(class)来表示,它包含了:

  • 前向传播函数:决定如何将输入转换为输出。
  • 参数:存储神经网络中需要调整的参数。
  • 反向传播函数:用于计算梯度,并进行参数更新。

块的好处在于,它可以将更复杂的网络结构进行抽象,并以更简洁的方式管理和实现神经网络。

3. 自定义块

为了更好地理解块的工作原理,接下来我们通过实现一个自定义块来加深理解。首先,我们需要定义一个简单的多层感知机(MLP)模型,并将其封装成一个块。以下是使用PyTorch实现的代码:

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


class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)  # 输出层

    def forward(self, X):
        """前向传播计算"""
        return self.out(F.relu(self.hidden(X)))


# 实例化并测试网络
net = MLP()
X = torch.rand(2, 20)

print(X.shape)  # torch.Size([2, 20])

print(net(X))

"""
tensor([[ 0.0155, -0.1311, -0.0113, -0.3112,  0.0482,  0.1557, -0.2504,  0.0271,
         -0.1276, -0.0866],
        [ 0.0543, -0.1713, -0.0039, -0.2339,  0.0922,  0.1267, -0.2425,  0.0483,
         -0.0868, -0.1099]], grad_fn=<AddmmBackward0>)
"""
  • torch.nn.functional 模块(通常简称为F)是PyTorch中的一个重要子模块,它提供了许多深度学习中常用的函数和操作,尤其是一些与神经网络层相关的函数,比如激活函数、损失函数、卷积操作等。它与PyTorch的torch模块的不同之处在于,torch.nn.functional 中的函数并不直接创建新的Tensor对象,而是直接对Tensor进行操作,无需创建额外的类或对象。

  • torch.rand 是PyTorch中用于生成随机数的函数,它会返回一个指定形状的Tensor,其元素值在 [0, 1) 之间均匀分布。这个函数可以用于初始化神经网络中的权重,或者用于需要随机输入的其他场景。

在这个例子中,MLP类定义了一个具有256个神经元的隐藏层和一个输出层。forward函数实现了数据流经隐藏层并应用激活函数(ReLU)后,再流经输出层。

4. 使用顺序块(Sequential)

在PyTorch中,Sequential类用于将多个层按顺序组合成一个模型。我们可以使用它来简化模型的构建。以下是一个简化的多层感知机模型,利用Sequential类将层按顺序排列:

net = nn.Sequential(
    nn.Linear(20, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
)

X = torch.rand(2, 20)
print(X)
"""
tensor([[0.3957, 0.5312, 0.8056, 0.3749, 0.3407, 0.2381, 0.9757, 0.5841, 0.6490,
         0.1689, 0.2499, 0.2440, 0.0397, 0.7120, 0.8425, 0.2046, 0.1753, 0.4144,
         0.7518, 0.6320],
        [0.9160, 0.1758, 0.8196, 0.2785, 0.4392, 0.7909, 0.8659, 0.0298, 0.1440,
         0.9179, 0.3337, 0.4030, 0.5683, 0.0869, 0.7739, 0.7054, 0.8481, 0.7669,
         0.7208, 0.5828]])
"""
print(net(X))
"""
tensor([[-0.0696, -0.0993, -0.0359, -0.3313,  0.1255,  0.1074, -0.1138,  0.1102,
         -0.0300, -0.0882],
        [ 0.0118, -0.0319, -0.0921, -0.3144,  0.0938,  0.0856, -0.1590,  0.1882,
         -0.0626, -0.1599]], grad_fn=<AddmmBackward0>)
"""

在这个示例中,Sequential类将多个层连接起来,创建了一个简单的神经网络模型。在调用net(X)时,输入数据会依次通过每一层。

5. 自定义顺序块

我们也可以自定义一个类似Sequential的类,将多个层按顺序连接起来。以下是一个简单的实现:

class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for idx, module in enumerate(args):
            self._modules[str(idx)] = module

    def forward(self, X):
        for block in self._modules.values():
            X = block(X)
        return X


# 使用自定义的Sequential实现
net = MySequential(
    nn.Linear(20, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
)
pprint(net._modules)
"""
OrderedDict([('0', Linear(in_features=20, out_features=256, bias=True)),
             ('1', ReLU()),
             ('2', Linear(in_features=256, out_features=10, bias=True))])
"""
X = torch.rand(2, 20)

print(net(X))

"""
tensor([[ 0.1490,  0.1756, -0.0927, -0.1953,  0.1193,  0.1560,  0.0340, -0.1926,
          0.2319, -0.4672],
        [-0.0448,  0.0712, -0.0696, -0.1076,  0.0780,  0.0239, -0.1082, -0.2056,
          0.1978, -0.3106]], grad_fn=<AddmmBackward0>)
"""

MySequential类的设计思想与Sequential类似,只是我们通过自定义的方式实现了层的顺序连接。

6. 灵活的前向传播

有时,我们需要在网络的前向传播过程中执行一些控制流或者更复杂的计算。在这种情况下,定义自定义的块会更灵活。例如,以下是一个包含常数参数的网络块示例:

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)
        X = self.linear(X)
        while X.abs().sum() > 1:
            print("X.abs().sum() =", X.abs().sum())
            X /= 2
        print("X.abs().sum() =", X.abs().sum())
        return X.sum()


# 测试固定权重的网络
net = FixedHiddenMLP()
X = torch.rand(2, 20)
print(net(X))
"""
X.abs().sum() = tensor(12.0223, grad_fn=<SumBackward0>)
X.abs().sum() = tensor(6.0112, grad_fn=<SumBackward0>)
X.abs().sum() = tensor(3.0056, grad_fn=<SumBackward0>)
X.abs().sum() = tensor(1.5028, grad_fn=<SumBackward0>)
X.abs().sum() = tensor(0.7514, grad_fn=<SumBackward0>)
tensor(0.2706, grad_fn=<SumBackward0>)
"""

在这个示例中,rand_weight 是一个固定的常量参数,不会在训练过程中更新。这个模型展示了如何在前向传播过程中添加一些常规的控制流逻辑。

小结

  • 层(Layer) 是神经网络中的基本构建单元,每一层接收输入并生成输出。
  • 块(Block) 是由多个层组成的更高层次的组件,它可以封装一个或多个层,并定义如何将输入转换为输出。
  • 使用 顺序块(Sequential) 可以方便地将多个层连接起来,形成一个简单的神经网络。
  • 自定义块 允许我们在前向传播过程中加入更复杂的控制流和计算。

通过使用块和层的组合,我们可以方便地构建和管理复杂的神经网络模型。理解这些概念将有助于你在更高级的深度学习任务中实现更灵活和高效的模型。