像搭积木一样搭神经网络

搭积木的时候,我们将不同类型的积木搭在一起。而对每一种类型的积木,又有多种变体可供选择。比如门框,既可以是文艺复兴风格,也可以是中式庭园风格。神经网络也一样,充满了各种各样的“积木”,以及“积木”的变体。学习神经网络,其实就是认“积木”的过程。

GitHub 项目地址:dl-tricks/note.ipynb

一、概述

1.1 从 MLP 说起

多层感知机 (MLP) 是非常简单的深度神经网络。麻雀虽小,五脏俱全。只要想想数据是如何在 MLP 中流动的,就能把深度学习中的各种组件想个七七八八。注意到,我们这里给神经网络中的“积木”一个比较正式的名称 —— 组件。

下图是一个 4 层感知机,左边是特征,右边是标签。训练开始时,样本数据先从左到右做 正向传播。待数据流到右侧,用 损失函数 计算损失。此时损失是一个标量,而最后一层的节点权重 $W$ 是一个矩阵,标量对矩阵的偏导是矩阵。优化器 会用大小合适的梯度矩阵,沿负梯度方向逐层反向更新权重 $W$。这样下一 批量 (batch) 数据进入网络时,正好能用上一轮更新后的参数做正向传播。

1.2 DataLoader

样本是有限的。为了榨干样本的最后一丝价值,一个样本往往要反复利用多次。这就要 DataLoader 上场了。DataLoader 把数据编排成一个个批量,并构建一个迭代器,使得每次调用它,都能从第一个批量开始遍历。

让我们来实现一个野生 DataLoader

import math
import torch

class DataLoader:
    def __init__(self, data: list, batch_size: int):
        self.i = 0
        self.batch_size = batch_size
        self.batch_num = math.floor(len(data) / batch_size)
        self._data = self.gen_batch(data)

    def gen_batch(self, data):
        lst = []
        s = self.batch_size
        for i in range(self.batch_num):
            start, end = s * i, s * (i + 1)
            X = torch.tensor([e[0] for e in data[start:end]])
            y = torch.tensor([e[1] for e in data[start:end]])
            lst.append((X, y))

        return lst

    def __iter__(self):
        self.i = 0
        return self

    def __next__(self):
        if self.i < len(self._data):
            self.i += 1
            return self._data[self.i - 1]
        else:
            raise StopIteration

为了让例子更加具体,我们假设有 2560 个样本。如果分成 10 个批量,则每批量有 256 个样本。

# 构造符合 f(a, b) = \frac{a^2 - b^2}{a^2 + b^2} + \epsilon 函数的样本生成
sample_num, batch_size = 2560, 10
X = [(random.random(), random.random()) for e in range(sample_num)]
y = map(lambda e: ((e[0]**2 - e[1]**2) / (e[0]**2 + e[1]**2)) + (random.random() / 100), X)

raw_data = list(zip(X, y))
# 输出一个批量的数据
for X, y in DataLoader(data=raw_data, batch_size=batch_size):
    print(f'X: {X}')
    print(f'y: {y}')
    break

看下图。在训练过程中,如果 10 个批量数据全训了一遍,就叫完成一个轮次 (epoch). 神经网络通常需要经过多个轮次的训练才会收敛。

Note: 为什么要做成批量?因为批量可以提高反向传播的效率。想了解更多细节,可以问 GPT:批量随机梯度下降比随机梯度下降好在哪儿?

1.3 神经元里发生了什么

让我们来看看每个神经元里发生了什么。

假定我们的样本有 10 个特征 (features),考虑各个层的维数:

  • 第一层的维数必须与样本特征数相同
  • 最后一层的维数必须与预测类别数相同
  • 中间隐藏层的维数比较自由,可以灵活地调整

假定一到四层的维数如下:

[Layer 0] 第一层维数为 10
[Layer 1] 第二层维数为 12
[Layer 2] 第三层维数为 12
[Layer 3] 第四层维数为 10

现在,我们来考虑一个样本做正向传播的情况:

第一层:

直接把样本的 10 个特征,分别填入 10 个神经元里就好了。

# 原始特征
features = ["张", "女性", 143.0, "国际贸易", 97.0, \
            88.5, 95.0, 79.0, 91.0, 70.0]

# 经过 encoder 编码后
features = [33, 1, 143.0, 1002, 97.0, \
            88.5, 95.0, 79.0, 91.0, 70.0]

# 把特征注入对应神经元
x_00, x_01, x_02, x_03, x_04, x_05, x_06, x_07, x_08, x_09 = *features

第二层:

第二层第 i 个神经元的值 $x_{1i}$,可以看作是由第一层神经元的值 $x_{0i} (i \in [0, 9])$ 经过 线性变换 -> 加偏置项 -> 过激活函数 得到的。

$x_{1i}$ 可由下式得出:

$$ x_{1i} = ReLU (W_{0i} X_0 + b_{0i}) $$

其中:

符号 描述
$ReLU$ 是激活函数
$W_{0i}$ 第 2 层神经元 i 上的可学习参数,是长为 10 的一维向量
$X_0$ 第 1 层所有神经元 x 值 concat 成的向量,是长为 10 的一维向量
$b_{0i}$ 第 2 层神经元 i 上的可学习偏置,是标量
$x_{1i}$ 第 2 层神经元 i 上的值,是标量(对于最后一层,它就是输出值 $\hat{y}_{i}$

为了更具象地理解,不妨认为神经元 $i$ 里包了三个参数:

  • $W_{0i}$: 可学习的线性变换参数
  • $b_{0i}$: 可学习的偏置
  • $x_{1i}$: 神经元的值

注意到前两项 $W_{0i}$$b_{0i}$ 下标的第一个数字是 0。但是认为它们在 Layer 0(第一层)不是个好主意。我倾向于认为 $W_{0i}$, $b_{0i}$$x_{1i}$ 高度相关,可以把它们当作神经元 $i$ 上的参数。

下图揭示了这种关系:$X_0$ 是来自上一层的 “外来者”,而 $W_{0i}$$b_{0i}$ 更像本层的 “原住民”。

描述信息

如果:

  • 将第二层所有向量 $W: W_{00}, W_{01}, ... ... W_{09}$ concat 起来得到矩阵 $W_0$
  • 将第二层所有偏置 $b: b_{00}, b_{10}, ... ... b_{0,12}$ concat 起来得到向量 $b_0$
  • 将第一层所有神经元上的值 concat 起来得到 $X_0$
  • 将第二层所有神经元上的值 concat 起来得到 $X_1$

那么第二层上的非线形变换可以写作:

$$ X_1 = ReLU (W_0 X_0 + b_0) $$

好崩溃,画个图把 Lucid 免费额度用完了 (´;ω;`)

二、激活函数

激活函数为深度神经网络引入了非线性性,让神经网络具有拟合任意函数的能力,使其拥有找到非凸优化问题最优解的能力。

2.1 Sigmoid

  • 表达式:$ sigmoid (x) = \frac{1}{1 + e^{-x}} $
  • 值域:$(0, 1)$
  • 特性:导数存在且处处可微。但在输入值很大或很小的情况下,梯度接近于零,导致梯度消失的问题。且输出不是零均值,可能会影响下一层的收敛速度。

2.2 Tanh

  • 表达式:$ \tanh(x) = \frac{1 - e^{-2x}}{1 + e^{-2x}} $
  • 值域:$(-1, 1)$
  • 特性:与 Sigmoid 函数相似,存在梯度消失问题。但相比 Sigmoid 函数,输出值更接近于零均值,有助于加快收敛速度。

2.3 ReLU

  • 表达式:$ ReLU (x) = \max(0, x) $
  • 值域:$[0, +\infty)$
  • 特性:简单高效,计算速度快。当输入为正数时,梯度为 1,可避免梯度消失问题;当输入为负数时,梯度为 0,意味着不再激活,即神经元死亡。输出不是归一化的,可能需要额外的规范化技术。

2.4 Softmax

  • 表达式:$\operatorname{softmax}(\mathbf{X})_{i j}=\frac{\exp \left(\mathbf{X}_{i j}\right)}{\sum_k \exp \left(\mathbf{X}_{i k}\right)}$
  • 值域:输出是一个概率分布,所有元素和为 1
  • 特性:可将任意实数向量转化为概率分布向量,常用于分类模型输出层的激活函数。

三、损失函数

5.1 交叉熵损失

交叉熵损失(Cross Entropy Loss)

5.2 均方误差

均方误差(Mean Squared Error, MSE)

四优化器

4.1 SGD

4.2 Adam

五、链式求导

六、正则化技术

6.1 权重衰减

6.2 dropout

七、优化策略(梯度更新策略)

7.1 梯度裁剪

7.2 学习率调度

7.3 量化

八、归一化技术

8.1 批量归一化

8.2 层归一化

九、推理和可视化

ONNX + Netron