跳到主要内容

内容阅读

AI 底层工程阿新聊ai

神经网络入门:从感知机到多层网络,通用近似定理,以及为什么需要深层

本文迁移自 mindcarver/91ai · 原始位置 docs/machine learning/stage2 deep learning/01 neural network intro.md · 由 @阿新聊ai 整理。 <! 调研来源: 1. Cybenko (1989) 和 Hornik (1991) 的...

本文迁移自 mindcarver/91ai · 原始位置 docs/machine-learning/stage2-deep-learning/01-neural-network-intro.md · 由 @阿新聊ai 整理。

神经网络入门:从感知机到多层网络,通用近似定理,以及为什么需要深层

TL;DR: 感知机是神经网络的最小单元——一个线性函数加一个阈值判断。单层感知机只能解决线性可分问题(连 XOR 都搞不定),但把多层感知机堆叠起来就能逼近任何连续函数——这就是通用近似定理。然而浅层网络要逼近复杂函数可能需要指数级的神经元,而深层网络通过层级组合可以用少得多的参数达到同样的效果。现实世界的数据(图像、语言、声音)天然具有层级结构,这就是深度学习有效的原因。

为什么这很重要

1957 年,Frank Rosenblatt 发明了感知机(Perceptron),被认为是人工智能的起点之一。1969 年,Minsky 和 Papert 在《Perceptrons》一书中证明了单层感知机无法解决 XOR(异或)问题。这个结论导致了对神经网络的第一次寒冬——很多人认为神经网络是死胡同。

问题的本质是:单层感知机只能画一条直线来分开数据。如果数据不能用一条直线分开(像 XOR 那样),单层感知机就无能为力。

但解决方案其实很简单:再加一层。多层感知机(MLP)通过组合多个线性决策边界,可以逼近任意复杂的决策边界。1989 年,Cybenko 在数学上证明了这个直觉:带一个隐层的前馈网络可以逼近任意连续函数——通用近似定理(Universal Approximation Theorem,UAT)。

但通用近似定理有一个重要的限制:它只说"存在"这样一个网络,没说这个网络有多大。实际上,对于某些复杂函数,浅层网络可能需要指数级数量的神经元,而深层网络只需要多项式级的参数就能做到同样好。这就是为什么我们需要深层网络。

核心概念

感知机:最简单的神经元

感知机是最简单的神经网络,它做的事情可以用一句话概括:计算输入的加权和,如果超过阈值就输出 1,否则输出 0。

$$ \hat{y} = \begin{cases} 1 & \text{if } \sum_{i} w_i x_i + b > 0 \ 0 & \text{otherwise} \end{cases} $$

其中 $w_i$ 是权重(每个输入的重要性),$b$ 是偏置(阈值),$x_i$ 是输入。

日常类比:感知机就像一个做决定的人。他听取多个意见(输入),每个意见有不同的可信度(权重),然后做一个综合判断。如果综合得分超过某个标准(阈值),他就做出肯定的决定;否则拒绝。

感知机能做什么

  • AND 运算(x1 AND x2)
  • OR 运算(x1 OR x2)
  • 任何线性可分的分类问题

感知机不能做什么

  • XOR 运算(x1 XOR x2)——不能用一条直线分开
  • 任何非线性可分的问题
AND 运算(线性可分)       XOR 运算(非线性可分)

  x2                         x2
  ↑                          ↑
1 |  ×  ●                    1 |  ×  ●  ← 无法画一条直线
  |                          |              把 × 和 ● 分开
0 |  ×  ×                   0 |  ●  ×
  +——→ x1                    +——→ x1
   0   1                      0   1

● = 输出 1                  ● = 输出 1
× = 输出 0                  × = 输出 0

多层感知机(MLP)

多层感知机的解决方案是:让多个感知机先各自画一条线,然后用另一层感知机来组合这些线的结果。

对于 XOR 问题:

  • 第一层的感知机 A 学到:x1 OR x2(上面的线)
  • 第一层的感知机 B 学到:NOT (x1 AND x2)(下面的线)
  • 第二层的感知机 C 学到:A AND B

结果:C 的输出就是 x1 XOR x2。

多层感知机解决 XOR:

输入层    隐藏层    输出层
 x1 ──→ (感知机 A: OR) ──┐
    \                     ├──→ (感知机 C: AND) ──→ 输出
 x2 ──→ (感知机 B: NAND) ─┘

真值表:
x1  x2  | A(OR)  B(NAND) | C(A AND B)
0   0   |  0      1       |  0  ✓
0   1   |  1      1       |  1  ✓
1   0   |  1      1       |  1  ✓
1   1   |  1      0       |  0  ✓

这就是多层网络的核心思想:每一层学习一个更高级别的特征,层与层之间通过组合产生更复杂的功能。

通用近似定理

1989 年,Cybenko 证明了一个惊人的结果:

定理(简化版):给定任意连续函数 $f: [0,1]^n \to [0,1]$ 和任意精度 $\epsilon > 0$,存在一个带单个隐藏层的前馈网络,使得对于所有 $x \in [0,1]^n$:

$$ |f(x) - \hat{f}(x)| < \epsilon $$

换句话说:一个隐藏层足够了。 只要隐藏层足够宽(有足够多的神经元),你可以逼近任何连续函数到任意精度。

但这里有一个巨大的"但是":定理说"存在"这样一个网络,但没告诉你:

  • 需要多少个神经元(可能是指数级的)
  • 能不能通过梯度下降学到这些权重
  • 实际需要多少训练数据

为什么需要深层网络

既然一个隐藏层就够了,为什么我们要用几十层甚至几百层的深度网络?

关键洞察来自 Telgarsky (2016) 的结果:存在一类函数,深度为 $O(k)$ 的网络可以用 $O(k)$ 个神经元表示,但深度为 $O(1)$ 的网络需要 $O(2^k)$ 个神经元。

换句话说,对于某些函数,深层网络比浅层网络指数级更高效。

直觉解释

考虑一个反复应用同一个简单操作的任务。比如判断一个 10 位二进制数的奇偶性。

浅层网络的方法:直接从 10 个输入映射到输出。需要学习所有 $2^{10} = 1024$ 种输入模式。

深层网络的方法:

  • 第 1 层:判断前 2 位的奇偶性 → 5 个中间结果
  • 第 2 层:合并相邻的结果 → 3 个中间结果
  • 第 3 层:继续合并 → 2 个结果
  • 第 4 层:最终合并 → 输出

每层只需要 $O(n)$ 个神经元,总共 $O(n \log n)$ 个参数。而浅层网络可能需要 $O(2^n)$ 个参数。

现实世界的类比

想象你要识别一张图片里是不是有猫。

浅层方法:直接从像素映射到"猫/非猫"。这需要记住所有可能的猫的像素组合——不同的姿势、颜色、大小、角度、光照……组合爆炸。

深层方法:

  • 第 1 层:检测边缘(水平线、垂直线、对角线)
  • 第 2 层:组合边缘检测简单形状(角、圆弧、交叉线)
  • 第 3 层:组合形状检测部件(眼睛、耳朵、爪子的轮廓)
  • 第 4 层:组合部件检测完整对象(猫的脸、猫的身体)

每一层都在前一层的输出上构建更高级的抽象。这种方式更高效,因为它重用了低层的计算

现实世界数据的层级结构

很多自然数据天然具有层级/组合结构:

数据类型 层级结构
图像 像素 → 边缘 → 纹理 → 部件 → 对象 → 场景
语言 字符 → 词 → 短语 → 句子 → 段落 → 文章
语音 声波 → 频谱 → 音素 → 音节 → 词 → 句子
音乐 音符 → 和弦 → 乐句 → 乐段 → 乐章

深度学习的成功,本质上是因为网络的结构匹配了数据的层级结构

工作原理(简化的心智模型)

神经网络是乐高积木

把神经网络想象成乐高积木。

单个神经元:一块最简单的乐高积木——一个小方块。它能做的事情很有限。

单层网络:把很多小方块排成一排。你可以用它们拼出一条直线(线性决策边界)。

多层网络:把一层积木叠在另一层上。每一层在上一层的输出上构建更复杂的结构。第一层拼出简单形状,第二层组合简单形状成复杂形状,第三层组合复杂形状成更复杂的结构。

关键洞察:深度比宽度更重要。100 块积木叠 10 层,比 1000 块积木铺 1 层能拼出更复杂的结构。因为每一层都在前面层的成果上构建,实现了计算的层级重用。

前向传播:从输入到输出

前向传播(Forward Propagation)是信息从输入层经过隐藏层传到输出层的过程。

对于一个 3 层网络(1 个输入层、1 个隐藏层、1 个输出层):

$$ \begin{aligned} \mathbf{h} &= \sigma(W_1 \mathbf{x} + \mathbf{b}_1) \quad \text{(隐藏层)}\ \hat{\mathbf{y}} &= \sigma(W_2 \mathbf{h} + \mathbf{b}_2) \quad \text{(输出层)} \end{aligned} $$

其中:

  • $\mathbf{x}$ 是输入向量
  • $W_1, \mathbf{b}_1$ 是第一层的权重和偏置
  • $\sigma$ 是激活函数(如 ReLU、Sigmoid)
  • $\mathbf{h}$ 是隐藏层的输出
  • $W_2, \mathbf{b}_2$ 是第二层的权重和偏置
  • $\hat{\mathbf{y}}$ 是网络的输出

矩阵视角

假设输入是 784 维(28×28 像素的图片),隐藏层有 128 个神经元,输出是 10 维(数字 0-9 的概率)。

输入 x:     [784 × 1]
权重 W1:    [128 × 784]  → 隐藏层 h: [128 × 1]
权重 W2:    [10 × 128]   → 输出 y:  [10 × 1]

总参数量: 784×128 + 128 + 10×128 + 10 = 101,770

网络的几何理解

一个理解神经网络的强大方式是几何视角:

  1. 每一层的线性变换($W\mathbf{x} + \mathbf{b}$)做了两件事:旋转空间(通过 $W$)和平移空间(通过 $\mathbf{b}$)
  2. 激活函数(如 ReLU)对空间做了非线性扭曲(折叠、弯曲)
  3. 整个网络就是一系列旋转、平移和非线性扭曲的组合
原始数据空间          第 1 层变换后         第 2 层变换后

  × ×                    × ×                  × × ×
 × × ×   → 弯曲空间 →  × × ×   → 再弯曲 →  ● ● ●
● ● ●                  ● ● ●                 × 和 ● 分开了!

这就是为什么神经网络能处理非线性问题:通过层层的非线性变换,网络把原始空间"扭曲"成一个新的空间,在这个新空间里,数据变成线性可分的了。

工作原理(详细机制)

感知机的数学定义

感知机的数学模型:

$$ \hat{y} = \text{step}\left(\sum_{i=1}^{n} w_i x_i + b\right) = \text{step}(\mathbf{w}^T \mathbf{x} + b) $$

其中 step 函数是:

$$ \text{step}(z) = \begin{cases} 1 & z > 0 \ 0 & z \leq 0 \end{cases} $$

感知机的几何意义:$\mathbf{w}^T \mathbf{x} + b = 0$ 定义了输入空间中的一个超平面(在 2D 中是一条直线,在 3D 中是一个平面)。感知机的任务就是找到正确的超平面来分开数据。

多层感知机的完整定义

一个 $L$ 层的前馈网络定义为:

$$ \begin{aligned} \mathbf{h}^{(0)} &= \mathbf{x} \ \mathbf{h}^{(l)} &= \sigma\left(W^{(l)} \mathbf{h}^{(l-1)} + \mathbf{b}^{(l)}\right), \quad l = 1, \ldots, L-1 \ \hat{\mathbf{y}} &= \sigma_{\text{out}}\left(W^{(L)} \mathbf{h}^{(L-1)} + \mathbf{b}^{(L)}\right) \end{aligned} $$

其中:

  • $\mathbf{h}^{(l)}$ 是第 $l$ 层的输出
  • $W^{(l)}$ 是第 $l$ 层的权重矩阵
  • $\mathbf{b}^{(l)}$ 是第 $l$ 层的偏置向量
  • $\sigma$ 是隐藏层激活函数(通常是 ReLU)
  • $\sigma_{\text{out}}$ 是输出层激活函数(回归用恒等函数,二分类用 Sigmoid,多分类用 Softmax)

通用近似定理的形式化

定理(Cybenko, 1989; Hornik, 1991)

设 $\sigma$ 是一个非常数、有界、连续的激活函数。则对于任意连续函数 $f: [0,1]^n \to \mathbb{R}$ 和任意 $\epsilon > 0$,存在 $N \in \mathbb{N}$,权重 $\mathbf{w}_i \in \mathbb{R}^n$,$b_i, \alpha_i \in \mathbb{R}$($i = 1, \ldots, N$),使得:

$$ \left| f(\mathbf{x}) - \sum_{i=1}^{N} \alpha_i \sigma(\mathbf{w}_i^T \mathbf{x} + b_i) \right| < \epsilon, \quad \forall \mathbf{x} \in [0,1]^n $$

定理的含义

  • 右边就是单隐藏层网络的输出
  • 定理说:只要隐藏层足够宽($N$ 足够大),你可以逼近任何连续函数
  • 这是存在性定理:它说"存在"这样的网络,但不告诉你怎么找到它

UAT 的常见误解

误解 1:"UAT 说明我们只需要一个隐藏层就行了。" → 理论上是的,但实践中 $N$ 可能大到无法计算。深度网络用更少的参数达到同样的效果。

误解 2:"UAT 说明神经网络可以学到任何函数。" → UAT 只说"存在"一组权重可以逼近目标函数。能不能通过梯度下降学到这组权重,是另一个问题。

误解 3:"UAT 说明网络越大越好。" → 虽然理论上更大的网络可以逼近得更精确,但更大的网络也更容易过拟合,更难训练。

深度的指数优势

Telgarsky (2016) 的结果(简化版):

对于任意的 $k$,存在一个函数 $f_k: \mathbb{R} \to \mathbb{R}$,满足:

  • $f_k$ 可以被深度为 $O(k)$、每层宽度为 $O(1)$ 的网络精确计算
  • 但任何深度为 $O(\log k)$ 的网络至少需要 $O(2^k / k)$ 个神经元才能近似 $f_k$

具体的构造:考虑函数 $f(x) = \text{triangle}^{(k)}(x)$,即三角波函数反复嵌套 $k$ 次。

  • 这个函数有一个自然的层级结构:每一层做一次"折叠"
  • 深度为 $k$ 的网络可以通过逐层计算来精确表示这个函数
  • 浅层网络需要指数级的神经元,因为它必须"一次性"学会所有折叠

现实世界的对应

很多实际函数也有这种层级结构。比如图像识别:

  • 判断"两个像素是否构成一条边缘"→ 简单函数
  • 判断"两条边缘是否构成一个角"→ 组合上面的函数
  • 判断"几个角是否构成一只眼睛"→ 组合上面的函数
  • 判断"眼睛、耳朵、鼻子是否构成猫脸"→ 组合上面的函数

每一层都在做简单操作,但通过层级组合产生了非常复杂的功能。深层网络天然匹配这种结构。

为什么没有激活函数就不行

如果所有激活函数都是线性的($\sigma(x) = x$),那么不管你堆多少层,整个网络等价于一个单层线性变换。

证明:对于两层线性网络:

$$ \begin{aligned} \mathbf{h} &= W_1 \mathbf{x} + \mathbf{b}_1 \ \hat{\mathbf{y}} &= W_2 \mathbf{h} + \mathbf{b}_2 \ &= W_2(W_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}2 \ &= \underbrace{W_2 W_1}{W'} \mathbf{x} + \underbrace{W_2 \mathbf{b}_1 + \mathbf{b}2}{\mathbf{b}'} \end{aligned} $$

结果还是 $\hat{\mathbf{y}} = W' \mathbf{x} + \mathbf{b}'$,一个线性变换。不管堆多少层都是如此。

这就是为什么激活函数必须是非线性的。非线性激活函数让每一层都能对空间做"弯曲"和"折叠",从而产生复杂的决策边界。没有非线性,深层网络就退化成了单层线性模型。

容量与泛化

网络的容量(capacity)是它能表示的函数族的复杂程度。容量由两个因素决定:

  1. 宽度:每层神经元的数量
  2. 深度:层数

容量和泛化之间存在权衡:

  • 容量太小 → 欠拟合(模型无法表达目标函数)
  • 容量太大 → 过拟合(模型记住了训练数据的噪声)

但深度学习的一个反直觉现象是:有时候更大的模型反而泛化更好。 这和我们之前讨论的 Double Descent 现象一致——在越过插值阈值后,更大的模型反而测试误差更低。

代码示例

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons, make_circles
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import warnings
warnings.filterwarnings('ignore')

np.random.seed(42)

# ============================================================
# 1. 感知机实现
# ============================================================
print("=" * 70)
print("1. 感知机(Perceptron)")
print("=" * 70)

class Perceptron:
    """简单的感知机实现"""
    def __init__(self, lr=0.01, n_epochs=100):
        self.lr = lr
        self.n_epochs = n_epochs
    
    def fit(self, X, y):
        n_samples, n_features = X.shape
        self.w = np.zeros(n_features)
        self.b = 0
        
        for epoch in range(self.n_epochs):
            errors = 0
            for i in range(n_samples):
                y_pred = 1 if (np.dot(self.w, X[i]) + self.b) > 0 else 0
                error = y[i] - y_pred
                if error != 0:
                    self.w += self.lr * error * X[i]
                    self.b += self.lr * error
                    errors += 1
            if errors == 0:
                break
        return self
    
    def predict(self, X):
        return (np.dot(X, self.w) + self.b > 0).astype(int)

# AND 门
X_and = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_and = np.array([0, 0, 0, 1])

perceptron = Perceptron(lr=0.1, n_epochs=100)
perceptron.fit(X_and, y_and)
print(f"AND 门: {perceptron.predict(X_and)} (期望 [0, 0, 0, 1])")

# OR 门
y_or = np.array([0, 1, 1, 1])
perceptron.fit(X_and, y_or)
print(f"OR  门: {perceptron.predict(X_and)} (期望 [0, 1, 1, 1])")

# XOR 门
y_xor = np.array([0, 1, 1, 0])
perceptron.fit(X_and, y_xor)
xor_pred = perceptron.predict(X_and)
print(f"XOR 门: {xor_pred} (期望 [0, 1, 1, 0])")
print(f"XOR 正确率: {accuracy_score(y_xor, xor_pred):.2f} ← 感知机无法解决 XOR!")
print(f"\n原因: XOR 不是线性可分的。感知机只能画一条直线,无法分开 XOR 的两类。")

# ============================================================
# 2. 多层感知机实现
# ============================================================
print("\n" + "=" * 70)
print("2. 多层感知机(MLP)— 用 NumPy 从零实现")
print("=" * 70)

class MLP:
    """从零实现的多层感知机"""
    def __init__(self, layer_sizes, lr=0.1, activation='relu'):
        self.layer_sizes = layer_sizes
        self.lr = lr
        self.activation = activation
        self.n_layers = len(layer_sizes) - 1
        
        # 初始化权重(Xavier 初始化)
        self.weights = []
        self.biases = []
        for i in range(self.n_layers):
            scale = np.sqrt(2.0 / (layer_sizes[i] + layer_sizes[i+1]))
            W = np.random.randn(layer_sizes[i], layer_sizes[i+1]) * scale
            b = np.zeros((1, layer_sizes[i+1]))
            self.weights.append(W)
            self.biases.append(b)
    
    def _activate(self, z):
        if self.activation == 'relu':
            return np.maximum(0, z)
        elif self.activation == 'sigmoid':
            return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
    
    def _activate_derivative(self, z):
        if self.activation == 'relu':
            return (z > 0).astype(float)
        elif self.activation == 'sigmoid':
            s = self._activate(z)
            return s * (1 - s)
    
    def _softmax(self, z):
        exp_z = np.exp(z - np.max(z, axis=1, keepdims=True))
        return exp_z / np.sum(exp_z, axis=1, keepdims=True)
    
    def forward(self, X):
        """前向传播"""
        self.activations = [X]
        self.z_values = []
        
        h = X
        for i in range(self.n_layers - 1):
            z = h @ self.weights[i] + self.biases[i]
            self.z_values.append(z)
            h = self._activate(z)
            self.activations.append(h)
        
        # 输出层
        z = h @ self.weights[-1] + self.biases[-1]
        self.z_values.append(z)
        
        if self.weights[-1].shape[1] == 1:
            output = self._activate(z)  # 二分类用 sigmoid
        else:
            output = self._softmax(z)   # 多分类用 softmax
        
        self.activations.append(output)
        return output
    
    def backward(self, y):
        """反向传播"""
        m = y.shape[0]
        gradients_w = [None] * self.n_layers
        gradients_b = [None] * self.n_layers
        
        # 输出层梯度
        delta = self.activations[-1] - y
        
        for i in range(self.n_layers - 1, -1, -1):
            gradients_w[i] = self.activations[i].T @ delta / m
            gradients_b[i] = np.mean(delta, axis=0, keepdims=True)
            
            if i > 0:
                delta = (delta @ self.weights[i].T) * self._activate_derivative(self.z_values[i-1])
        
        return gradients_w, gradients_b
    
    def fit(self, X, y, epochs=1000, verbose=True):
        """训练"""
        for epoch in range(epochs):
            output = self.forward(X)
            grad_w, grad_b = self.backward(y)
            
            for i in range(self.n_layers):
                self.weights[i] -= self.lr * grad_w[i]
                self.biases[i] -= self.lr * grad_b[i]
            
            if verbose and (epoch + 1) % 200 == 0:
                loss = -np.mean(y * np.log(output + 1e-8) + (1 - y) * np.log(1 - output + 1e-8))
                pred = (output > 0.5).astype(int)
                acc = accuracy_score(y.flatten(), pred.flatten())
                print(f"  Epoch {epoch+1}: loss={loss:.4f}, accuracy={acc:.4f}")
    
    def predict(self, X):
        output = self.forward(X)
        return (output > 0.5).astype(int)

# 用 MLP 解决 XOR
print("\n训练 MLP 解决 XOR 问题:")
mlp = MLP(layer_sizes=[2, 4, 1], lr=0.5, activation='sigmoid')
y_xor_2d = y_xor.reshape(-1, 1).astype(float)
mlp.fit(X_and.astype(float), y_xor_2d, epochs=2000)

xor_pred_mlp = mlp.predict(X_and.astype(float))
print(f"\nXOR 预测: {xor_pred_mlp.flatten()}")
print(f"XOR 正确率: {accuracy_score(y_xor, xor_pred_mlp.flatten()):.2f}")
print("→ MLP 成功解决了 XOR 问题!")

# ============================================================
# 3. 决策边界可视化
# ============================================================
print("\n" + "=" * 70)
print("3. 决策边界可视化")
print("=" * 70)

# 生成非线性数据集
datasets = {
    'Moons': make_moons(n_samples=500, noise=0.15, random_state=42),
    'Circles': make_circles(n_samples=500, noise=0.1, factor=0.5, random_state=42),
    'XOR-like': (np.random.randn(500, 2), (np.logical_xor(
        np.random.randn(500) > 0, np.random.randn(500) > 0)).astype(int))
}

fig, axes = plt.subplots(2, 3, figsize=(18, 10))

for col, (name, (X_data, y_data)) in enumerate(datasets.items()):
    # 感知机
    perc = Perceptron(lr=0.1, n_epochs=100)
    perc.fit(X_data, y_data)
    perc_acc = accuracy_score(y_data, perc.predict(X_data))
    
    # MLP
    y_2d = y_data.reshape(-1, 1).astype(float)
    mlp = MLP(layer_sizes=[2, 16, 8, 1], lr=0.1, activation='relu')
    mlp.fit(X_data, y_2d, epochs=2000, verbose=False)
    mlp_acc = accuracy_score(y_data, mlp.predict(X_data).flatten())
    
    # 可视化
    h = 0.05
    x_min, x_max = X_data[:, 0].min() - 1, X_data[:, 0].max() + 1
    y_min, y_max = X_data[:, 1].min() - 1, X_data[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
    
    for row, (model, title, acc) in enumerate([
        (perc, f'感知机 (acc={perc_acc:.2f})', perc_acc),
        (mlp, f'MLP (acc={mlp_acc:.2f})', mlp_acc)
    ]):
        ax = axes[row, col]
        if row == 0:
            Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
        else:
            Z = model.predict(np.c_[xx.ravel(), yy.ravel()].astype(float)).reshape(xx.shape)
        
        ax.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
        ax.scatter(X_data[:, 0], X_data[:, 1], c=y_data, cmap='coolwarm', edgecolors='k', s=20)
        ax.set_title(f'{name} - {title}')
        if row == 0:
            ax.set_xlabel('(单层感知机只能画直线)')
        else:
            ax.set_xlabel('(MLP 可以画复杂边界)')

axes[0, 0].set_ylabel('感知机')
axes[1, 0].set_ylabel('MLP (3层)')
plt.tight_layout()
plt.savefig('perceptron_vs_mlp.png', dpi=150)
plt.show()
print("感知机 vs MLP 决策边界可视化已保存")

# ============================================================
# 4. 深度 vs 宽度实验
# ============================================================
print("\n" + "=" * 70)
print("4. 深度 vs 宽度实验")
print("=" * 70)

from sklearn.neural_network import MLPClassifier

X_deep, y_deep = make_moons(n_samples=2000, noise=0.2, random_state=42)
X_tr, X_te, y_tr, y_te = train_test_split(X_deep, y_deep, test_size=0.2, random_state=42)

print(f"{'架构':&#x3C;35} {'参数量':&#x3C;10} {'测试准确率':&#x3C;12}")
print("-" * 60)

configs = [
    # 浅层 + 宽
    ([8], '浅层 8神经元'),
    ([32], '浅层 32神经元'),
    ([128], '浅层 128神经元'),
    ([512], '浅层 512神经元'),
    ([2048], '浅层 2048神经元'),
    # 深层 + 窄
    ([8]*2, '2层 × 8神经元'),
    ([8]*4, '4层 × 8神经元'),
    ([8]*8, '8层 × 8神经元'),
    ([16]*2, '2层 × 16神经元'),
    ([16]*4, '4层 × 16神经元'),
    ([32]*2, '2层 × 32神经元'),
    ([64]*2, '2层 × 64神经元'),
]

for layers, name in configs:
    clf = MLPClassifier(hidden_layer_sizes=layers, max_iter=2000, 
                         random_state=42, early_stopping=True)
    clf.fit(X_tr, y_tr)
    acc = accuracy_score(y_te, clf.predict(X_te))
    
    # 计算参数量
    n_params = 0
    prev = 2
    for l in (layers if isinstance(layers, tuple) else [layers]):
        n_params += prev * l + l
        prev = l
    n_params += prev * 1 + 1  # 输出层
    
    print(f"  {name:&#x3C;33} {n_params:&#x3C;10d} {acc:&#x3C;12.4f}")

# 预期输出类似:
# 架构                                 参数量      测试准确率
# ------------------------------------------------------------
#   浅层 8神经元                       33          0.8350
#   浅层 32神经元                      129         0.8600
#   浅层 128神经元                     513         0.9150
#   浅层 512神经元                     2049        0.9200
#   浅层 2048神经元                    8193        0.9250
#   2层 × 8神经元                     105         0.8600
#   4层 × 8神经元                     241         0.9100
#   8层 × 8神经元                     441         0.9300
#   2层 × 16神经元                    337         0.9150
#   4层 × 16神经元                    753         0.9350
#   2层 × 32神经元                    1249        0.9250
#   2层 × 64神经元                    4737        0.9300

print("\n观察:")
print("  1. 深层网络在相同参数量下通常比浅层网络表现更好")
print("  2. 参数量增加带来的边际收益递减")
print("  3. 太深的网络(8层×8)可能因为梯度消失而训练困难")

# ============================================================
# 5. 通用近似定理演示
# ============================================================
print("\n" + "=" * 70)
print("5. 通用近似定理演示:用网络逼近各种函数")
print("=" * 70)

# 要逼近的目标函数
target_functions = {
    'sin(2πx)': lambda x: np.sin(2 * np.pi * x),
    '|x - 0.5|': lambda x: np.abs(x - 0.5),
    '阶梯函数': lambda x: (x > 0.3).astype(float) + (x > 0.7).astype(float) * 0.5,
    '锯齿波': lambda x: 2 * np.abs(2 * (x - np.floor(x + 0.5)))
}

x_fit = np.linspace(0, 1, 200).reshape(-1, 1)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes = axes.flatten()

for idx, (name, func) in enumerate(target_functions.items()):
    y_fit = func(x_fit).ravel()
    
    # 用不同宽度的网络逼近
    for n_neurons in [4, 16, 64, 256]:
        from sklearn.neural_network import MLPRegressor
        mlp = MLPRegressor(hidden_layer_sizes=[n_neurons], activation='relu',
                           max_iter=5000, random_state=42, solver='adam', 
                           learning_rate_init=0.01)
        mlp.fit(x_fit, y_fit)
        y_pred = mlp.predict(x_fit)
        mse = np.mean((y_fit - y_pred) ** 2)
        axes[idx].plot(x_fit, y_pred, '--', alpha=0.7, 
                      label=f'{n_neurons} neurons (MSE={mse:.4f})')
    
    axes[idx].plot(x_fit, y_fit, 'k-', linewidth=2, label='目标函数')
    axes[idx].set_title(f'逼近 {name}')
    axes[idx].legend(fontsize=8)
    axes[idx].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('universal_approximation.png', dpi=150)
plt.show()
print("通用近似定理演示已保存")
print("→ 随着神经元数量增加,网络越来越精确地逼近目标函数")
print("→ 但注意:某些函数(如阶梯函数)需要更多神经元才能精确逼近")

# ============================================================
# 6. 网络架构选择指南
# ============================================================
print("\n" + "=" * 70)
print("6. 网络架构选择指南")
print("=" * 70)

guide = """
╔════════════════════════════════════════════════════════════════════════╗
║                     神经网络架构选择指南                               ║
╠════════════════════════════════════════════════════════════════════════╣
║                                                                      ║
║  隐藏层数量(深度)选择:                                             ║
║  ├── 0 层:线性模型(逻辑回归、线性回归)                             ║
║  ├── 1 层:简单非线性问题(XOR、浅层模式)                            ║
║  ├── 2-3 层:中等复杂度问题(结构化数据、简单图像)                    ║
║  └── 4+ 层:复杂问题(需要层级特征提取)                              ║
║                                                                      ║
║  每层神经元数量(宽度)选择:                                         ║
║  ├── 经验法则:每层 2/3 × (输入维度 + 输出维度) 到 2 × 输入维度       ║
║  ├── 逐层递减:如 [512, 256, 128, 64](常见模式)                    ║
║  └── 不要太少(欠拟合)也不要太多(过拟合+计算浪费)                  ║
║                                                                      ║
║  实用建议:                                                           ║
║  1. 从小网络开始,逐步增大直到验证集性能不再提升                      ║
║  2. 先加宽度,再加深度                                               ║
║  3. 使用 dropout / batch normalization 防止过拟合                     ║
║  4. 深度 > 宽度(大多数情况下)                                      ║
║  5. 残差连接可以帮助训练更深的网络                                    ║
║                                                                      ║
╚════════════════════════════════════════════════════════════════════════╝
"""
print(guide)

真实案例

案例 1:ImageNet 竞赛中的网络深度演进

ImageNet 图像分类竞赛的历史是网络深度不断增长的历史:

年份 模型 层数 Top-5 错误率 关键创新
2012 AlexNet 8 15.3% ReLU, Dropout, GPU 训练
2014 VGGNet 19 7.3% 小卷积核 (3×3),更深
2014 GoogLeNet 22 6.7% Inception 模块,多尺度
2015 ResNet 152 3.6% 残差连接,解决梯度消失
2017 SENet 152+ 2.3% 注意力机制

从 8 层到 152 层,错误率从 15.3% 降到 3.6%(人类大约 5%)。深度是性能提升的主要驱动力

但直接把网络做深是不行的。VGGNet 之后的很多尝试表明,简单地堆叠更多层反而让性能变差(退化问题,不是过拟合而是训练不收敛)。直到何恺明发明了残差连接(ResNet),才能有效地训练 100+ 层的网络。

案例 2:AlphaGo 中的网络架构

AlphaGo 使用两个深度网络:

  1. 策略网络(Policy Network):13 层 CNN,输入是棋盘状态,输出是每个位置的落子概率。负责"下一步该下在哪里"。

  2. 价值网络(Value Network):类似的架构,但输出一个标量——当前局面的胜率估计。负责"当前局面谁更有优势"。

为什么需要深层?因为围棋的策略是高度组合的:

  • 第 1-4 层:检测基本的棋形(如"眼"、"虎口")
  • 第 5-8 层:组合棋形检测局部战术
  • 第 9-13 层:组合战术理解全局战略

浅层网络无法有效地表示这种层级策略。

案例 3:NLP 中的深度——从 Word2Vec 到 Transformer

自然语言处理中的网络深度也经历了显著的增长:

  • Word2Vec (2013):单层,学习词的向量表示
  • LSTM (1997, 广泛应用 2014-2017):通常 2-4 层
  • ELMo (2018):2 层双向 LSTM
  • BERT-Base (2018):12 层 Transformer,1.1 亿参数
  • GPT-3 (2020):96 层 Transformer,1750 亿参数
  • GPT-4 (2023):估计 100+ 层,参数量未公开

语言的理解天然是层级的:字符 → 词 → 短语 → 句子 → 段落。深层 Transformer 通过自注意力机制在每一层建立词与词之间的关系,逐层构建更高级的语言理解。

权衡取舍以及何时不该使用

深层网络的代价

训练困难:梯度消失/爆炸使得深层网络难以训练。虽然 ResNet、BatchNorm 等技术缓解了这个问题,但训练 100+ 层的网络仍然需要大量经验和技巧。

计算资源:深层网络需要更多的计算和内存。训练 GPT-3 需要几千块 GPU 运行几周。

数据需求:深层网络有更多参数需要学习,通常需要更多训练数据。在小数据集上,简单的模型可能更好。

可解释性差:深层网络的决策过程很难解释。对于需要可解释性的场景(如医疗诊断),浅层模型或决策树可能更合适。

何时不该用深度学习

  1. 数据量小(< 1000 样本)。深度学习需要大量数据来学习参数。小数据集上,线性模型或树模型通常更好。

  2. 结构化/表格数据。对于特征工程做得好的结构化数据,XGBoost 等梯度提升方法通常比神经网络表现更好且更快。

  3. 需要可解释性。如果你的业务场景需要解释"为什么模型做了这个预测",深度学习不是好选择。

  4. 推理延迟要求高。在嵌入式设备或实时系统中,深层网络的推理延迟可能不可接受。

  5. 数据没有层级结构。如果你的数据是低维的、简单的关系,线性模型就够了。不需要杀鸡用牛刀。

常见的错误做法

错误 1:上来就用深度学习

看到问题就用神经网络,而不先尝试简单模型。正确做法是:先建立线性模型基线,再尝试树模型,只有在这两者都不够好时才考虑深度学习。

错误 2:盲目增加深度

网络不收敛就加层,结果更不收敛。应该先检查学习率、数据质量、梯度流等问题。有时候问题不是深度不够,而是训练方法不对。

错误 3:忽视网络规模和数据量的匹配

100 万参数的网络用 1000 条数据训练——严重过拟合。参数量应该和数据量匹配。经验法则:训练数据量至少应该是参数量的 5-10 倍。

关键要点

  1. 感知机是神经网络的基本单元,它计算输入的加权和并通过激活函数输出。单层感知机只能解决线性可分问题。

  2. 多层感知机通过组合多个感知机,可以逼近任意复杂的决策边界。通用近似定理保证了这一点——但需要的神经元数量可能很大。

  3. 深度比宽度更重要。对于具有层级结构的函数(包括大多数现实世界的数据),深层网络可以用指数级更少的参数来表示相同的函数。

  4. 非线性激活函数是多层网络有效的关键。没有非线性激活,不管堆多少层都等价于一个线性变换。

  5. 现实世界的数据天然具有层级结构(像素→边缘→形状→部件→对象),这解释了为什么深度学习有效——网络的结构匹配了数据的结构。

  6. 通用近似定理是理论保证,不是实践指南。它告诉你"存在"一个网络可以逼近目标函数,但不告诉你这个网络有多大,也不告诉你怎么找到它。

  7. 深度学习的适用场景:数据量大、数据有层级结构(图像、文本、语音)、性能比可解释性更重要。对于小数据或结构化数据,传统方法可能更好。

延伸阅读

  1. Cybenko, G. (1989). "Approximation by superpositions of a sigmoidal function." Mathematics of Control, Signals and Systems, 2(4), 303-314. — 通用近似定理的原始论文

  2. Hornik, K. (1991). "Approximation capabilities of multilayer feedforward networks." Neural Networks, 4(2), 251-257. — UAT 的更一般化证明

  3. Telgarsky, M. (2016). "Benefits of depth in neural networks." COLT. — 证明了深层网络相对于浅层网络的指数优势

  4. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, Chapter 6. — 深度前馈网络的系统介绍

  5. Nielsen, M. (2015). Neural Networks and Deep Learning. 免费在线教材,从直觉到数学的完整路径

  6. He, K., Zhang, X., Ren, S., & Sun, J. (2016). "Deep Residual Learning for Image Recognition." CVPR. — ResNet,解决了深层网络的训练问题

评论

0
登录后可以参与评论和讨论。
💬

还没有评论

欢迎留下第一条评论,帮助这篇内容更快形成讨论。