像搭积木一样学深度学习:从层到块的进阶构建

引言

在深度学习的早期,模型往往很简单:输入数据,经过几个神经元,产生输出。但随着技术的发展,我们现在的模型(如ResNet、Transformer)动辄包含数百甚至数千层。如果还像搭积木一样一层一层地手动连接,代码将变得极其冗余且难以维护。

为了解决这个问题,现代深度学习框架(如PyTorch、TensorFlow、MXNet、PaddlePaddle)引入了一个核心概念——块(Block)

本文将带你深入理解“层”与“块”的区别,以及如何通过自定义块,像搭积木一样构建复杂的神经网络。


1. 从“层”到“块”:抽象的力量

在最初接触神经网络时,我们关注的是层(Layer)。一个层通常具备三个特征:

  1. 接受输入数据。
  2. 生成输出数据。
  3. 拥有一组可调整的参数(Parameters)。

当我们使用 softmax 回归或多层感知机(MLP)时,其实整个模型和单个层遵循着相同的架构。然而,随着模型复杂度的增加(例如在计算机视觉中大名鼎鼎的ResNet-152),我们需要一种更高层次的抽象。

这就是**块(Block)**的用武之地。块是一个比单层大但比整个模型小的组件。它是一个通用的容器,可以是:

  • 单个层(如全连接层)。
  • 一组层的集合(如ResNet中的残差块)。
  • 整个模型本身

通过“块”的概念,我们可以递归地组合:用块构建更大的块,最终形成复杂的模型。

2. 代码实战:PyTorch中的自定义块

在PyTorch中,所有的神经网络模块都继承自 nn.Module。要构建自定义块,我们需要掌握三个关键步骤:继承基类声明层定义前向传播

2.1 基础自定义:多层感知机 (MLP)

这是最简单的自定义块形式。我们构建一个包含隐藏层和输出层的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch
from torch import nn
from torch.nn import functional as F

class MLP(nn.Module):
# 1. 继承基类 nn.Module
def __init__(self):
super().__init__()
# 2. 在构造函数中声明层
self.hidden = nn.Linear(20, 256) # 隐藏层
self.out = nn.Linear(256, 10) # 输出层

# 3. 在 forward 中定义数据流动逻辑
def forward(self, X):
return self.out(F.relu(self.hidden(X)))

# --- 使用示例 ---
net = MLP()
X = torch.rand(2, 20) # 模拟输入数据
print(net(X))

2.2 进阶自定义:包含常数参数与控制流

标准的 Sequential 容器只能处理简单的线性层叠。如果我们要实现更复杂的逻辑(如常数参数、Python控制流),必须自定义 forward 函数。

文档中提到的 FixedHiddenMLP 是一个绝佳的例子,它展示了以下高级特性:

  • 常数参数:定义一个在训练中不更新的随机权重。
  • 参数共享:同一个层被复用两次。
  • 控制流:在神经网络中嵌入 while 循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
# 1. 使用常数参数进行矩阵运算
X = F.relu(torch.mm(X, self.rand_weight) + 1)
# 2. 参数共享:再次使用同一个层
X = self.linear(X)
# 3. 控制流:任意的 Python 逻辑
while X.abs().sum() > 1:
X /= 2
return X.sum()

3. 深入底层:为什么不能只用 Python 列表?

你可能会好奇,像 nn.Sequential 这样的容器内部是如何工作的?为什么我们不能简单地用一个 Python 列表来存储层?

让我们通过实现一个简化的 MySequential 来揭示其中的秘密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
# 关键点:不能直接用 self.modules = list(args)
# 必须利用 nn.Module 的机制注册子模块
for idx, module in enumerate(args):
self._modules[str(idx)] = module # PyTorch 使用 OrderedDict 存储

def forward(self, X):
# 遍历存储的模块
for block in self._modules.values():
X = block(X)
return X

# 使用方式与 nn.Sequential 一致
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
print(net(X))

核心原理:
普通的 Python 列表(List)无法被 PyTorch 的自动求导和参数管理机制自动发现。PyTorch 的 nn.Module 内部使用了一个特殊的有序字典(OrderedDict)属性(_modules)来注册所有的子块。只有注册到这里的模块,框架才会自动帮你初始化参数、计算梯度并管理它们的设备(CPU/GPU)迁移。

4. 组合与嵌套:构建复杂的“混合模型”

块的最大优势在于组合性。我们可以像俄罗斯套娃一样,将块嵌套在块中。例如,我们可以将之前定义的 NestMLP(包含嵌套序列的块)和 FixedHiddenMLP(包含复杂逻辑的块)组合在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
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())
print(chimera(X))

5. 性能考量:Python 的瓶颈

在享受 Python 代码的灵活性时,我们也要注意性能问题。深度学习训练主要在 GPU 上进行,而 Python 代码(如 while 循环)运行在 CPU 上。由于 Python 的全局解释器锁(GIL),这可能导致 GPU 等待 CPU,造成效率瓶颈。

解决方案:
现代框架提供了图模式(Graph Mode)混合式编程(Hybridization)。例如,PyTorch 的 @torch.jit.script 装饰器或 TensorFlow 的 @tf.function 可以将 Python 代码编译成高效的计算图,从而绕过 Python 解释器,直接在硬件上高速运行。

结语

通过理解“块”的概念,你已经掌握了深度学习框架的核心设计哲学。从简单的层叠到复杂的自定义逻辑,再到模块的嵌套组合,这种抽象能力让你能够复现甚至发明最先进的神经网络架构(如 ResNet、Transformer 等)。

记住,构建神经网络不仅仅是堆叠层,更是设计能够处理数据流的逻辑块。