深度学习基石:从感知机到深度神经网络
深度学习基石:从感知机到深度神经网络
当我们谈论深度学习时,我们实际上在讨论一个看似简单却极其强大的思想:用层层堆叠的非线性变换,从原始数据中自动提取越来越抽象的表示。这个思想并非新生事物,其根源可以追溯到二十世纪五十年代的感知机。然而,真正让深度学习在二十一世纪大放异彩的,是数学理论的成熟、计算能力的飞跃以及海量数据的涌现。本章将带领读者深入深度学习的数学本质,从最基础的感知机出发,逐步构建起对深度神经网络的完整理解,并最终通过 PyTorch 亲手实现一个高效的训练循环。
从感知机到多层神经网络:历史的必然
感知机的诞生与局限
1958年,Frank Rosenblatt 提出了感知机,这是第一个能够学习的神经网络模型。感知机的数学表达简洁而优雅:
$$y = \sigma\left(\sum_{i=1}^{n} w_i x_i + b\right)$$
其中 $x_i$ 是输入特征,$w_i$ 是对应的权重,$b$ 是偏置项,$\sigma$ 是阶跃激活函数。感知机通过调整权重来学习线性决策边界,其更新规则如下:
$$w_i \leftarrow w_i + \eta (y_{true} - y_{pred}) x_i$$
然而,1969年,Minsky 和 Papert 在其著作《Perceptrons》中严格证明了感知机的一个致命缺陷:它无法学习非线性可分的问题,最经典的例子便是异或(XOR)问题。
```mermaid graph LR A[输入空间] --> B{线性可分?} B -->|是| C[感知机可解] B -->|否| D[感知机失效] D --> E[引入隐藏层] E --> F[多层感知机 MLP] F --> G[非线性变换] G --> H[任意决策边界] ```
这个证明曾让神经网络研究陷入长达十年的"AI寒冬"。但历史的吊诡之处在于,解决这个问题的方法其实早已存在——只需要在输入和输出之间加入一层或多层隐藏层,并引入非线性激活函数。
多层感知机的突破
多层感知机通过引入隐藏层打破了线性变换的局限。考虑一个具有单隐藏层的网络,其数学形式为:
$$\mathbf{h} = \sigma(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1)$$ $$\mathbf{y} = \sigma(\mathbf{W}_2 \mathbf{h} + \mathbf{b}_2)$$
关键在于,如果去掉激活函数 $\sigma$,整个网络将退化为单纯的线性变换:
$$\mathbf{y} = \mathbf{W}_2(\mathbf{W}_1 \mathbf{x}) = (\mathbf{W}_2 \mathbf{W}_1)\mathbf{x} = \mathbf{W}'\mathbf{x}$$
这告诉我们一个深刻的道理:非线性激活函数是深度学习的灵魂。正是这些看似简单的非线性函数,赋予了神经网络强大的表达能力。
> 💡 核心洞察:根据通用近似定理,一个具有足够宽隐藏层的单隐藏层神经网络,在合适的激活函数下,可以以任意精度逼近任何连续函数。这为深度学习的理论基础提供了坚实的保障。
张量:深度学习的数学语言
张量的本质与秩
在深度学习中,张量是数据的基本表示形式。张量可以理解为多维数组的推广,其秩决定了维度的数量:
| 秩 | 数学名称 | PyTorch 类型 | 典型应用 | |---|---------|-------------|---------| | 0 | 标量 | `Scalar` | 损失值、学习率 | | 1 | 向量 | `Vector` | 偏置项、词嵌入 | | 2 | 矩阵 | `Matrix` | 权重矩阵、灰度图像 | | 3 | 三阶张量 | `Tensor` | RGB 图像、时间序列 | | 4 | 四阶张量 | `Tensor` | 视频片段、批量图像 | | 5+ | 高阶张量 | `Tensor` | 复杂结构化数据 |
```python import torch
创建不同秩的张量
scalar = torch.tensor(3.14159) # 秩 0 vector = torch.tensor([1.0, 2.0, 3.0]) # 秩 1 matrix = torch.tensor([[1, 2], [3, 4]]) # 秩 2 tensor_3d = torch.randn(3, 224, 224) # 秩 3:3通道 224x224 图像 tensor_4d = torch.randn(32, 3, 224, 224) # 秩 4:批量32张RGB图像
print(f"Scalar shape: {scalar.shape}, dim: {scalar.dim()}") print(f"Vector shape: {vector.shape}, dim: {vector.dim()}") print(f"Matrix shape: {matrix.shape}, dim: {matrix.dim()}") print(f"3D Tensor shape: {tensor_3d.shape}, dim: {tensor_3d.dim()}") print(f"4D Tensor shape: {tensor_4d.shape}, dim: {tensor_4d.dim()}") ```
张量运算的几何直觉
理解张量运算的几何意义对于深入掌握深度学习至关重要。矩阵乘法 $\mathbf{Y} = \mathbf{X}\mathbf{W}^T$ 可以理解为对输入空间进行线性变换,包括旋转、缩放和投影。
$$[\mathbf{Y}]_{ij} = \sum_k [\mathbf{X}]_{ik} [\mathbf{W}^T]_{kj} = \sum_k [\mathbf{X}]_{ik} [\mathbf{W}]_{jk}$$
在神经网络中,这个操作的本质是将输入从原始特征空间映射到一个新的表示空间。每一层都在进行这样的空间变换,而非线性激活函数则在这些线性变换之间插入了弯曲和扭曲,使得整个网络能够描绘出任意复杂的决策边界。
```python
张量运算示例:批量矩阵乘法
batch_size, in_features, out_features = 32, 784, 256 X = torch.randn(batch_size, in_features) # 输入批量 W = torch.randn(out_features, in_features) # 权重矩阵 b = torch.randn(out_features) # 偏置向量
等价的两种写法
Y1 = torch.matmul(X, W.t()) + b # 显式转置 Y2 = X @ W.T + b # Pythonic 风格
print(f"Output shape: {Y1.shape}") # [32, 256] ```
广播机制:优雅的维度匹配
广播机制是张量运算中一个极其重要但常被忽视的概念。当两个形状不同的张量进行运算时,PyTorch 会自动扩展较小的张量以匹配较大的张量,而无需实际复制数据。
广播遵循两条规则: 1. 从最右边的维度开始向左对齐 2. 每个对应维度要么相等,要么其中一个为 1,要么其中一个不存在
```python
广播示例
A = torch.randn(3, 4) # 形状 [3, 4] B = torch.randn(4) # 形状 [4] C = A + B # 广播为 [3, 4] + [1, 4] -> [3, 4]
D = torch.randn(32, 1, 64) # 形状 [32, 1, 64] E = torch.randn(1, 16, 64) # 形状 [1, 16, 64] F = D + E # 广播为 [32, 16, 64]
print(f"A + B result shape: {C.shape}") print(f"D + E result shape: {F.shape}") ```
> ⚠️ 实践警告:广播虽然方便,但也容易隐藏 bug。当形状不符合预期时,广播可能产生意外的结果。建议在关键运算前后使用 `assert` 检查张量形状,或使用 `einsum` 进行显式的维度标注。
反向传播:深度学习的引擎
反向传播算法是训练深度神经网络的核心技术,其本质是高效计算损失函数对所有参数的梯度。理解反向传播需要从计算图和链式法则两个角度入手。
计算图:自动微分的基础
计算图是一种有向无环图(DAG),用于表示数学表达式中的计算依赖关系。每个节点代表一个操作或变量,边代表数据的流动方向。
```mermaid graph TD x[x] --> mul1[*] w[w] --> mul1 mul1 --> add1[+] b[b] --> add1 add1 --> sigmoid[σ] sigmoid --> y_hat[y_hat] y[y] --> loss[L] y_hat --> loss
style x fill:#e1f5fe style w fill:#fff3e0 style b fill:#fff3e0 style y fill:#e8f5e9 style y_hat fill:#fce4ec style loss fill:#ffcdd2 ```
PyTorch 使用动态计算图,即每次前向传播都会构建新的计算图。这种设计使得模型可以包含条件控制和循环,极大地提高了灵活性。
链式法则的数学推导
设神经网络的损失函数为 $L$,对于网络中的任意参数 $\theta$,我们需要计算 $\frac{\partial L}{\partial \theta}$。根据链式法则,如果 $y = f(x)$ 且 $z = g(y)$,则:
$$\frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \cdot \frac{\partial y}{\partial x}$$
考虑一个简单的三层网络,前向传播为:
$$\mathbf{z}_1 = \mathbf{W}_1 \mathbf{x} + \mathbf{b}_1$$ $$\mathbf{h}_1 = \text{ReLU}(\mathbf{z}_1)$$ $$\mathbf{z}_2 = \mathbf{W}_2 \mathbf{h}_1 + \mathbf{b}_2$$ $$\hat{\mathbf{y}} = \text{softmax}(\mathbf{z}_2)$$
对于交叉熵损失 $L = -\sum_i y_i \log(\hat{y}_i)$,反向传播从输出层开始:
$$\frac{\partial L}{\partial \mathbf{z}_2} = \hat{\mathbf{y}} - \mathbf{y}$$
这是一个极其优雅的结果:softmax 与交叉熵的组合产生的梯度就是简单的预测值与真实值之差。继续向后传播:
$$\frac{\partial L}{\partial \mathbf{W}_2} = \frac{\partial L}{\partial \mathbf{z}_2} \otimes \mathbf{h}_1^T$$ $$\frac{\partial L}{\partial \mathbf{h}_1} = \mathbf{W}_2^T \frac{\partial L}{\partial \mathbf{z}_2}$$
ReLU 的梯度需要特殊处理:
$$\frac{\partial L}{\partial \mathbf{z}_1} = \frac{\partial L}{\partial \mathbf{h}_1} \odot \mathbb{1}[\mathbf{z}_1 > 0]$$
其中 $\odot$ 表示逐元素乘法,$\mathbb{1}[\cdot]$ 是指示函数。这个特性——ReLU 在正区间的梯度恒为 1,在负区间梯度为 0——对解决梯度消失问题至关重要。
梯度流问题与解决方案
深度网络训练中最常见的问题之一是梯度消失或梯度爆炸。考虑一个具有 $L$ 层的深层网络,假设每层的激活导数为 $\sigma'$,则梯度在反向传播时会累积为:
$$\frac{\partial L}{\partial \mathbf{W}_1} \propto \prod_{l=1}^{L} \sigma'_l \cdot \mathbf{W}_l$$
如果 $|\sigma' \cdot \mathbf{W}| < 1$,梯度会指数级衰减;如果 $|\sigma' \cdot \mathbf{W}| > 1$,梯度会指数级增长。
```python import torch.nn as nn
演示梯度消失问题
class DeepNetwork(nn.Module): def __init__(self, depth, hidden_size, use_relu=True): super().__init__() layers = [] for _ in range(depth): layers.append(nn.Linear(hidden_size, hidden_size)) if use_relu: layers.append(nn.ReLU()) else: layers.append(nn.Sigmoid()) # 更容易梯度消失 self.network = nn.Sequential(*layers)
def forward(self, x): return self.network(x)
测试梯度流
def test_gradient_flow(model, input_size): x = torch.randn(1, input_size, requires_grad=True) y = model(x) loss = y.sum() loss.backward()
检查输入梯度
grad_norm = x.grad.norm().item() return grad_norm
ReLU vs Sigmoid 在深层网络中的梯度对比
model_relu = DeepNetwork(20, 64, use_relu=True) model_sigmoid = DeepNetwork(20, 64, use_relu=False)
print(f"ReLU network gradient norm: {test_gradient_flow(model_relu, 64):.6f}") print(f"Sigmoid network gradient norm: {test_gradient_flow(model_sigmoid, 64):.10f}") ```
激活函数:赋予网络非线性能力
激活函数的选择对网络性能有着深远的影响。理想的激活函数应该具备以下特性:非线性、易于求导、不饱和(避免梯度消失)、计算高效、零中心化。
Sigmoid 与 Tanh:历史的教训
Sigmoid 函数 $\sigma(x) = \frac{1}{1 + e^{-x}}$ 曾经是神经网络的标准选择。它的输出范围是 $(0, 1)$,天然适合表示概率。然而,它有两个致命缺陷:
1. 梯度饱和:当 $|x|$ 较大时,$\sigma'(x) \approx 0$,导致梯度消失 2. 非零中心:输出恒为正,会导致权重更新呈现"之字形"路径
Sigmoid 的导数为 $\sigma'(x) = \sigma(x)(1 - \sigma(x))$,最大值仅为 $0.25$。
Tanh 函数 $\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}$ 输出范围是 $(-1, 1)$,解决了零中心化问题,但梯度饱和问题依然存在。
$$\text{Sigmoid}(x) = \frac{1}{1 + e^{-x}}, \quad \text{Tanh}(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}$$
ReLU 及其变体:现代的标准选择
ReLU(Rectified Linear Unit)定义为 $f(x) = \max(0, x)$,它的出现是深度学习发展史上的一个里程碑:
- **优点**:计算极其简单(只需比较),正区间梯度恒为 1(完美解决梯度消失),稀疏激活(负输入产生零输出)
- **缺点**:负区间梯度为零("死亡 ReLU"问题),输出非零中心化
为了解决"死亡 ReLU"问题,研究者提出了多种变体:
Leaky ReLU: $$f(x) = \begin{cases} x & \text{if } x > 0 \\ \alpha x & \text{if } x \leq 0 \end{cases}$$
其中 $\alpha$ 是一个小常数(通常为 0.01)。
GELU(Gaussian Error Linear Unit): $$\text{GELU}(x) = x \cdot \Phi(x) = x \cdot P(X \leq x), \quad X \sim \mathcal{N}(0, 1)$$
GELU 在 Transformer 架构(如 BERT、GPT)中被广泛使用,可以看作是 ReLU 的平滑版本。
Swish: $$\text{Swish}(x) = x \cdot \sigma(\beta x)$$
当 $\beta \to \infty$ 时,Swish 退化为 ReLU;$\beta = 1$ 时,它就是