# 📚 多层感知机（MLP）—— 从零理解神经网络（详细版）



# 🎯 本章学习目标

| 目标 | 说明 |
|------|------|
| **理解** | 什么是函数——机器学习的核心数学工具 |
| **理解** | 单层感知机为什么只能解决线性问题（含数学证明） |
| **理解** | 多层感知机如何解决非线性问题（数学原理） |
| **掌握** | 前向传播：数据如何在网络中流动（含矩阵维度分析） |
| **掌握** | 反向传播：网络如何从错误中学习（完整链式法则推导） |
| **掌握** | 用 NumPy 从零实现一个 MLP（不依赖任何框架） |
| **理解** | 激活函数、损失函数、学习率等核心概念（含求导过程） |

---

# 📖 第一部分：数学基础 —— 你必须知道的概念


## 1.1 什么是函数？

### 1.1.1 函数的定义

**函数**是一种对应规则：输入一个值，经过某种规则，输出一个值。

数学定义：
```
设 D 是一个非空数集，如果对于 D 中的每一个数 x，按照某种规则 f，
都能确定唯一一个数 y 与之对应，那么我们就说 f 是定义在 D 上的函数，
记作：

    y = f(x)

其中：
  - x 称为自变量（输入）
  - y 称为因变量（输出）
  - D 称为定义域（所有可能的输入）
  - f 称为对应规则（函数本身）
```

### 1.1.2 函数的直观理解

```
y = f(x) 可以想象为一个"加工机器"：

    输入 x  -->  [  f  ]  -->  输出 y

例子：
  - y = 2x + 1 是一个加工机器：输入 x，输出 2x+1
  - y = x^2   是一个加工机器：输入 x，输出 x^2
  - y = sin(x) 是一个加工机器：输入 x，输出 sin(x)
```

### 1.1.3 神经网络本质上是一个复杂的函数

```
传统函数：
  y = f(x) = 2x + 1

神经网络（想象）：
  y = f(x) = ReLU(W2 * ReLU(W1 * x + b1) + b2)
  
  这是一个多层嵌套的函数！
  x = 输入（比如一张图片的像素）
  y = 输出（比如图片是数字几的概率）
```

### 1.1.4 多元函数

神经网络处理的是多元函数，即有多个输入的函数：

```
z = f(x, y)  =  x + 2y

例子：
  输入：x = 3, y = 5
  输出：z = 3 + 2*5 = 13

在神经网络中：
  输入可能是 784 个数（x1, x2, x3, ..., x784）
  输出可能是 10 个数（y1, y2, ..., y10）
  
  z1 = f1(x1, x2, ..., x784)
  z2 = f2(x1, x2, ..., x784)
  ...
  z10 = f10(x1, x2, ..., x784)
```
---

## 1.2 导数 —— 变化的度量

### 1.2.1 导数的物理意义

导数描述的是"变化率"：

```
导数的定义：
    f'(x) = lim(h->0) [f(x+h) - f(x)] / h

含义：
  - 当 h 趋近于 0 时，差商 [f(x+h) - f(x)] / h 趋近于一个常数
  - 这个常数就是函数 f 在点 x 处的导数
  - 导数表示：在点 x 处，f 的值随着 x 的增加而变化的快慢
  
几何意义：切线的斜率
物理意义：瞬时变化率
```

### 1.2.2 常见函数的导数

```
1. 常数函数：f(x) = C
   f'(x) = 0
   
2. 幂函数：f(x) = x^n
   f'(x) = n * x^(n-1)
   
   例子：
     f(x) = x^2  ->  f'(x) = 2x
     f(x) = x^3  ->  f'(x) = 3x^2
   
3. 指数函数：f(x) = e^x
   f'(x) = e^x
   
4. 对数函数：f(x) = ln(x)
   f'(x) = 1/x

5. 三角函数：
   f(x) = sin(x)  ->  f'(x) = cos(x)
   f(x) = cos(x)  ->  f'(x) = -sin(x)
```

### 1.2.3 导数的运算法则

```
加法法则：[f(x) + g(x)]' = f'(x) + g'(x)

乘法法则：[f(x) * g(x)]' = f'(x) * g(x) + f(x) * g'(x)

除法法则：[f(x) / g(x)]' = [f'(x) * g(x) - f(x) * g'(x)] / g(x)^2

链式法则（最重要）：[f(g(x))]' = f'(g(x)) * g'(x)
```

### 1.2.4 链式法则详解

链式法则是理解反向传播的数学基础，必须深入理解！

```
如果 y = f(u)，且 u = g(x)
那么 y = f(g(x)) 是 x 的复合函数

链式法则：
    dy/dx = dy/du * du/dx

也可以写成：
    d/dx [f(g(x))] = f'(g(x)) * g'(x)

直观理解：
    dy/dx = dy/du * du/dx
      = (y 相对 u 的变化率) * (u 相对 x 的变化率)
```

**例子1**：
```
y = (2x + 1)^3

令 u = 2x + 1，则 y = u^3

dy/du = 3u^2 = 3(2x+1)^2
du/dx = 2

所以 dy/dx = 3u^2 * 2 = 6(2x+1)^2 = 6(2x+1)^2
```

**例子2**：
```
y = e^(3x)

令 u = 3x，则 y = e^u

dy/du = e^u = e^(3x)
du/dx = 3

所以 dy/dx = e^(3x) * 3 = 3e^(3x)
```

### 1.2.5 偏导数

当函数有多个自变量时，我们需要偏导数：

```
设 z = f(x, y) = x^2 + 3xy + y^2

对 x 的偏导数（把 y 看作常数）：
    ∂z/∂x = 2x + 3y

对 y 的偏导数（把 x 看作常数）：
    ∂z/∂y = 3x + 2y

例子：计算 f(1, 2) 的偏导数
    ∂z/∂x|(1,2) = 2*1 + 3*2 = 8
    ∂z/∂y|(1,2) = 3*1 + 2*2 = 7
```

---

## 1.3 线性代数基础

### 1.3.1 向量

**向量**是一串数字的有序排列：

```
向量 a = [a1, a2, ..., an] 称为 n 维向量

例子：
  速度 v = [vx, vy, vz] = [3, 4, 0] 是一个三维向量
  像素 p = [255, 128, 64, ...] 是一个 784 维向量
  
向量的维度：向量中元素的个数

向量运算：
  加法：[1,2] + [3,4] = [4,6]
  数乘：2 * [1,2] = [2,4]
  点积：[1,2] · [3,4] = 1*3 + 2*4 = 11
```

**向量的点积（内积）**：
```
向量 a = [a1, a2, ..., an]
向量 b = [b1, b2, ..., bn]

点积 a · b = a1*b1 + a2*b2 + ... + an*bn = Σ(ai*bi)

几何意义：
  a · b = |a| * |b| * cos(θ)
  
  其中 |a| 是向量 a 的长度，θ 是两个向量的夹角
  
当两个向量垂直时（θ = 90°），cos(90°) = 0，点积为 0
```

### 1.3.2 矩阵

**矩阵**是一个数字的矩形表格：

```
矩阵 A = [[a11, a12, ..., a1n],
          [a21, a22, ..., a2n],
          ...
          [am1, am2, ..., amn]]

称为 m 行 n 列的矩阵，记作 A(m x n)

例子：
  图片（28x28）是一个 28 行 28 列的矩阵：
  
  A = [[0,   128, 255, ..., 0],
       [255, 0,   128, ..., 45],
       ...
       [45,  100, 0,   ..., 78]]
```

### 1.3.3 矩阵乘法

这是神经网络最核心的运算！

```
矩阵 A (m x n) 与矩阵 B (n x k) 的乘积 C (m x k)：

    C = A @ B

其中 C 的每个元素：
    C[i,j] = Σ(A[i,:] * B[:,j]) = A[i,1]*B[1,j] + A[i,2]*B[2,j] + ... + A[i,n]*B[n,j]
```

**例子**：
```
A (2x3) = [[1, 2, 3],     B (3x2) = [[7, 8],
           [4, 5, 6]]                  [9, 10],
                                      [11, 12]]

C = A @ B (2x2):

C[0,0] = 1*7 + 2*9 + 3*11 = 7 + 18 + 33 = 58
C[0,1] = 1*8 + 2*10 + 3*12 = 8 + 20 + 36 = 64
C[1,0] = 4*7 + 5*9 + 6*11 = 28 + 45 + 66 = 139
C[1,1] = 4*8 + 5*10 + 6*12 = 32 + 50 + 72 = 154

C = [[58, 64],
     [139, 154]]
```

### 1.3.4 神经网络中的矩阵运算

```
输入向量 x (784,) 
权重矩阵 W (784 x 128)
偏置向量 b (128,)

计算：z = x @ W + b

x 是 1 行 784 列的矩阵 (1 x 784)
W 是 784 行 128 列的矩阵 (784 x 128)
x @ W 是 1 行 128 列的矩阵 (1 x 128)
z 是 1 行 128 列的矩阵 (1 x 128)
b 是 1 行 128 列的矩阵 (1 x 128)，与 z 相加

批量计算时（batch_size = 64）：
X (64 x 784) @ W (784 x 128) = Z (64 x 128)
```

### 1.3.5 矩阵的转置

```
矩阵 A 的转置 A^T：行列互换

A (m x n) -> A^T (n x m)

例子：
A = [[1, 2, 3],     A^T = [[1, 4],
     [4, 5, 6]]              [2, 5],
                             [3, 6]]

重要公式：
  (A @ B)^T = B^T @ A^T
  
在反向传播中经常用到！
```

---

# 📖 第二部分：生物神经网络与人工神经元

## 2.1 生物大脑中的神经元

### 2.1.1 神经元的结构

```
                        树突（接收信号）
                        /   |   \
                   ╱      ╱     ╲     ╲
              ┌─────────────────────────┐
              │         细胞体           │
              │    （处理输入，产生输出）   │
              └─────────────────────────┘
                            │
                            │ 轴突
                            │
                       突触（连接到其他神经元）
```

**各部分功能**：
- **树突**：接收来自其他神经元的信号
- **细胞体**：汇总所有输入，如果超过阈值则产生信号
- **轴突**：传输细胞体产生的信号
- **突触**：神经元之间的连接点，决定信号传递的"强度"

### 2.1.2 神经元的工作原理

```
简化模型：
  1. 多个神经元的信号通过树突传入
  2. 每个输入信号有一个"权重"（突触强度）
  3. 细胞体把所有信号加权求和
  4. 如果总和 > 阈值，神经元激活，输出信号
  5. 信号通过轴突传给其他神经元

数学表示：
  y = 激活函数(Σ wi*xi - θ)
  
  其中：
    xi = 第 i 个输入信号
    wi = 第 i 个突触的权重（权重越大，该信号越重要）
    θ = 阈值（决定神经元何时激活）
    激活函数 = 把总和转换成输出
```

### 2.1.3 生活中的类比：部门开会投票

```
想象一个部门开会决策：

输入（每个人的意见）：
  - 员工 A 的意见（重要性 0.8）
  - 员工 B 的意见（重要性 0.5）
  - 员工 C 的意见（重要性 0.9）

汇总计算：
  总分 = 0.8*意见A + 0.5*意见B + 0.9*意见C

决策：
  如果 总分 > 阈值（比如 1.5）
  则 部门决定"通过"
  否则 部门决定"不通过"

这就是人工神经网络的原理！
```

---

## 2.2 人工神经元（感知机）

### 2.2.1 感知机的数学模型

**感知机（Perceptron）**是最简单的人工神经元模型，由 Frank Rosenblatt 于 1958 年提出。

```
感知机的数学模型：

输入：x1, x2, ..., xn（n 个输入）
权重：w1, w2, ..., wn（每个输入对应一个权重）
偏置：b（阈值，取负号后称为偏置）

计算：
  z = x1*w1 + x2*w2 + ... + xn*wn + b
  
  用向量形式：z = x · w + b
  
  其中 x = [x1, x2, ..., xn]，w = [w1, w2, ..., wn]

激活函数：
  如果 z > 0，则 y = 1（激活）
  如果 z <= 0，则 y = 0（不激活）
  
  这叫"阶跃函数"：
  y = { 1, if z > 0
      { 0, if z <= 0
```

### 2.2.2 感知机的向量形式

```
输入向量：x = [x1, x2, ..., xn]  (1 x n)
权重向量：w = [w1, w2, ..., wn]  (1 x n)

加权求和：z = x · w + b = Σ(xi * wi) + b

这就是两个向量的点积再加上偏置！
```

### 2.2.3 用代码实现感知机

```python
import numpy as np

class Perceptron:
    """单个感知机（人工神经元）"""
    
    def __init__(self, input_size):
        """
        初始化感知机
        input_size: 输入的维度（有多少个输入）
        """
        # 初始化权重为小的随机数
        self.w = np.random.randn(input_size) * 0.01
        # 初始化偏置为 0
        self.b = 0
    
    def forward(self, x):
        """
        前向传播：计算感知机的输出
        x: 输入向量，比如 (784,)
        """
        # 计算加权求和：z = x · w + b
        z = np.dot(x, self.w) + self.b
        
        # 阶跃激活函数
        if z > 0:
            return 1
        else:
            return 0
    
    def __call__(self, x):
        """让对象可以像函数一样调用"""
        return self.forward(x)
```

### 2.2.4 感知机的几何意义

```
感知机本质上是一个超平面：

z = x · w + b = 0

这个方程定义了一个 n 维空间中的超平面（n-1 维子空间）

对于二维输入（x1, x2）：
  w1*x1 + w2*x2 + b = 0  是一条直线
  
对于三维输入（x1, x2, x3）：
  w1*x1 + w2*x2 + w3*x3 + b = 0 是一个平面

感知机的作用：
  - 把输入空间分成两部分
  - 一边输出 1，一边输出 0
```

---

# 📖 第三部分：单层感知机的局限——数学证明

## 3.1 感知机可以解决什么问题？

### 3.1.1 AND 门（与门）

```
AND 门的真值表：
  输入 A  输入 B  输出
  ─────────────────
    0      0     0
    0      1     0
    1      0     0
    1      1     1   <- 只有两个都是 1 时才输出 1

我们可以用感知机实现 AND 门：

设 w1 = 0.5, w2 = 0.5, b = -0.7

验证：
  A=0, B=0: z = 0*0.5 + 0*0.5 - 0.7 = -0.7 <= 0 -> y=0 [OK]
  A=0, B=1: z = 0*0.5 + 1*0.5 - 0.7 = -0.2 <= 0 -> y=0 [OK]
  A=1, B=0: z = 1*0.5 + 0*0.5 - 0.7 = -0.2 <= 0 -> y=0 [OK]
  A=1, B=1: z = 1*0.5 + 1*0.5 - 0.7 = 0.3 > 0 -> y=1 [OK]

几何解释：
  在二维坐标系中，直线 0.5*x1 + 0.5*x2 - 0.7 = 0
  把 (0,0), (0,1), (1,0) 分到一边（输出 0）
  把 (1,1) 分到另一边（输出 1）
```

### 3.1.2 OR 门（或门）

```
OR 门的真值表：
  输入 A  输入 B  输出
  ─────────────────
    0      0     0
    0      1     1
    1      0     1
    1      1     1   <- 至少有一个是 1 时输出 1

用感知机实现：w1 = 0.5, w2 = 0.5, b = -0.3

验证：
  A=0, B=0: z = 0*0.5 + 0*0.5 - 0.3 = -0.3 <= 0 -> y=0 [OK]
  A=0, B=1: z = 0*0.5 + 1*0.5 - 0.3 = 0.2 > 0 -> y=1 [OK]
  A=1, B=0: z = 1*0.5 + 0*0.5 - 0.3 = 0.2 > 0 -> y=1 [OK]
  A=1, B=1: z = 1*0.5 + 1*0.5 - 0.3 = 0.7 > 0 -> y=1 [OK]
```

### 3.1.3 NOT 门（非门）

```
NOT 门的真值表：
  输入 A  输出
  ─────────
    0     1
    1     0

用感知机实现：w1 = -0.5, b = 0.3

验证：
  A=0: z = 0*(-0.5) + 0.3 = 0.3 > 0 -> y=1 [OK]
  A=1: z = 1*(-0.5) + 0.3 = -0.2 <= 0 -> y=0 [OK]
```

---

## 3.2 XOR 门——感知机的致命缺陷

### 3.2.1 XOR 的定义

```
XOR（异或）门的真值表：
  输入 A  输入 B  输出
  ─────────────────
    0      0     0
    0      1     1
    1      0     1
    1      1     0   <- 两者不一样时输出 1

特点：相同输出 0，不同输出 1
```

### 3.2.2 尝试用感知机实现 XOR

```
假设存在感知机使得 XOR 成立，即存在 w1, w2, b 使得：

  当 (A,B) = (0,0): w1*0 + w2*0 + b <= 0
  当 (A,B) = (0,1): w1*0 + w2*1 + b > 0
  当 (A,B) = (1,0): w1*1 + w2*0 + b > 0
  当 (A,B) = (1,1): w1*1 + w2*1 + b <= 0

从第一个条件：b <= 0
从第二个条件：w2 + b > 0
从第三个条件：w1 + b > 0
从第四个条件：w1 + w2 + b <= 0

把 b <= 0 代入第二个：w2 + b > 0 -> w2 > -b >= 0 -> w2 > 0
把 b <= 0 代入第三个：w1 + b > 0 -> w1 > -b >= 0 -> w1 > 0

所以 w1 > 0, w2 > 0, b <= 0

但第四个条件 w1 + w2 + b <= 0
由于 w1 > 0, w2 > 0, b <= 0
如果 b <= 0 但非常接近 0，比如 b = -ε
那么 w1 + w2 - ε <= 0 -> w1 + w2 <= ε

这意味着 w1 和 w2 必须非常小，接近 0
但根据 w1 > 0, w2 > 0 且 w1 + w2 <= ε
这不可能同时满足第二个 w1 + b > 0 -> w1 - ε > 0 -> w1 > ε
和第三个 w2 - ε > 0 -> w2 > ε

所以 w1 + w2 > 2ε，与 w1 + w2 <= ε 矛盾！

因此：感知机无法实现 XOR！
```

### 3.2.3 XOR 的几何解释

```
AND 门（线性可分）：
    
    B
    ^     1 |  *
    |       |  
    |       |  
    +----------> A
    0      0  
    
    可以用一条直线（超平面）把 *（输出1）和 。（输出0）分开

OR 门（线性可分）：
    
    B
    ^     1 |  *  *
    |       |  
    |       |  
    +----------> A
    0      0  
    
    可以用一条直线把 *（输出1）和 。（输出0）分开

XOR 门（线性不可分）：
    
    B
    ^     1 |  *   .
    |  .   |     *  
    |       |      
    +----------> A
    0      0  
    
    *（输出1）在对角线：(0,1) 和 (1,0)
    .（输出0）在对角线：(0,0) 和 (1,1)
    
    无法用一条直线把 * 和 . 分开！
    必须用两条直线（折线）才能分开
```

### 3.2.4 线性可分与线性不可分

```
定义：

  一组数据点 {x(i)} 被称为线性可分的，如果存在一个超平面
  w · x + b = 0 能够把所有正类点分到一边，所有负类点分到另一边。
  
  换句话说：
    如果存在 w, b 使得：
      对于所有正类：w · x + b > 0
      对于所有负类：w · x + b <= 0
    那么这组数据是线性可分的。

AND, OR, NOT 都是线性可分的
XOR 不是线性可分的
```

---

# 📖 第四部分：多层感知机（MLP）—— 解决非线性问题

## 4.1 多层的思想

### 4.1.1 为什么多层可以解决 XOR？

```
单层感知机的局限：
  - 只能画一条直线（一个超平面）
  - 只能解决线性可分问题

多层感知机的能力：
  - 第一层：学多条直线（多个超平面）
  - 第二层：综合第一层的结果，画出更复杂的边界

XOR 的多层解决方案：

  输入层 (x1, x2)
      |
      v
  隐藏层：两个感知机 h1, h2
      |
      v
  输出层：综合判断
  
  具体：
    h1 = AND(NOT x1, x2)  <- 在区域 (0,1) 输出 1
    h2 = AND(x1, NOT x2)  <- 在区域 (1,0) 输出 1
    y = OR(h1, h2)        <- 任何一个为 1 就输出 1
```

### 4.1.2 多层的几何解释

```
单层：一条直线把空间分成两部分

多层：多条直线组合，把空间分成多个区域

XOR 的解决过程：
  
  第一步：两个隐藏单元各自画一条直线
    h1: w1=[-1,1], b=0.5 -> 识别 (0,1) 区域
    h2: w2=[1,-1], b=0.5 -> 识别 (1,0) 区域
  
  第二步：输出层综合
    y = OR(h1, h2)
  
  效果：把原本无法用一条直线分开的问题，
        转换成两步：先画两条线，再综合
```

---

## 4.2 MLP 的网络结构

### 4.2.1 本项目的 MLP 结构

```
网络结构: 784 -> 128 -> 10

输入层：
  - 784 个神经元（对应 28x28 图片的 784 个像素值）
  - 输入向量 x：形状 (batch_size, 784)

隐藏层：
  - 128 个神经元
  - 每个神经元：z1[i] = x @ W1[:,i] + b1[i]
  - 激活后：a1[i] = ReLU(z1[i])

输出层：
  - 10 个神经元（对应数字 0-9）
  - 每个神经元输出一个"分数"
  - 通过 Softmax 转换成概率
```

### 4.2.2 网络的矩阵表示

```
输入：X (batch_size, 784)

第一层（输入 -> 隐藏）：
  Z1 = X @ W1 + b1
  W1 形状: (784, 128)
  b1 形状: (128,)
  Z1 形状: (batch_size, 128)
  
  A1 = ReLU(Z1)

第二层（隐藏 -> 输出）：
  Z2 = A1 @ W2 + b2
  W2 形状: (128, 10)
  b2 形状: (10,)
  Z2 形状: (batch_size, 10)
  
  A2 = Softmax(Z2)  # 最终概率输出
```

### 4.2.3 参数量计算

```
总参数量：

  W1: 784 × 128 = 100,352 个
  b1: 128 个
  
  W2: 128 × 10 = 1,280 个
  b2: 10 个
  
  总计：101,770 个参数

这就是为什么深度学习需要大量参数！
参数量决定了网络能学多复杂的模式。
```

---

## 4.3 激活函数

### 4.3.1 为什么需要激活函数？

```
如果网络只有线性变换：
  Z1 = X @ W1 + b1
  Z2 = A1 @ W2 + b2
  
  那么整个网络仍然是线性的！
  
  Z2 = (X @ W1 + b1) @ W2 + b2
     = X @ (W1 @ W2) + (b1 @ W2 + b2)
     = X @ W + b
  
  这等价于一个单层网络！
  
所以：必须加入非线性激活函数，才能让多层网络有意义！
```

### 4.3.2 ReLU 激活函数

```
ReLU(x) = max(0, x) = { x, if x > 0
                        0, if x <= 0 }

函数图像：
    y
    |       /
    |      /
    |     /
    |    /
    +----------- x
    0
  
数学性质：
  - 正半轴：导数 = 1
  - 负半轴：导数 = 0
  - 在 x=0 处：不可导（但实际使用中可以任意选择一边）

导数（求导过程）：
  当 x > 0 时，ReLU(x) = x，所以 d/dx ReLU(x) = 1
  当 x < 0 时，ReLU(x) = 0，所以 d/dx ReLU(x) = 0
  当 x = 0 时，导数不存在，但在实践中常定义为 0 或 1
  
ReLU'(x) = { 1, if x > 0
           { 0, if x <= 0
```

### 4.3.3 Softmax 激活函数

```
Softmax 把一个向量转换成概率分布

对于向量 z = [z1, z2, ..., zk]：

  softmax(zi) = exp(zi) / Σj exp(zj)

特点：
  1. 每个 softmax(zi) > 0（因为 exp 永远是正数）
  2. Σi softmax(zi) = 1（因为分母是所有分子的和）
  
所以 softmax 的输出是一个合法的概率分布！

数值稳定技巧：
  由于 exp 可能溢出，通常用以下技巧：
  
  softmax(zi) = exp(zi - C) / Σj exp(zj - C)
  
  其中 C = max(z)，这样所有指数都不会太大
```

**Softmax 的求导**：

```
设 y_i = softmax(z_i) = exp(z_i) / Σj exp(z_j)

当 i == j 时（对角元素）：
  ∂y_i/∂z_i = [exp(z_i) * Σj exp(z_j) - exp(z_i) * exp(z_i)] / (Σj exp(z_j))^2
            = exp(z_i)(Σj exp(z_j) - exp(z_i)) / (Σj exp(z_j))^2
            = exp(z_i) / Σj exp(z_j) * (Σj exp(z_j) - exp(z_i)) / Σj exp(z_j)
            = y_i * (1 - y_i)

当 i != j 时（非对角元素）：
  ∂y_i/∂z_j = [0 * Σj exp(z_j) - exp(z_i) * exp(z_j)] / (Σj exp(z_j))^2
            = -exp(z_i) * exp(z_j) / (Σj exp(z_j))^2
            = -y_i * y_j

总结：
  ∂y_i/∂z_j = { y_i * (1 - y_i),  if i == j
             { -y_i * y_j,      if i != j
```

---

# 📖 第五部分：前向传播 —— 详细数学推导

## 5.1 前向传播的完整流程

```
输入：x (784,)

第一步：输入层 -> 隐藏层
  z1 = x @ W1 + b1
  a1 = ReLU(z1)

第二步：隐藏层 -> 输出层
  z2 = a1 @ W2 + b2
  y = softmax(z2)
```

### 5.1.1 第一步：输入层 -> 隐藏层

```
给定：
  x: (784,)  # 一张图片的像素值
  W1: (784, 128)  # 权重矩阵
  b1: (128,)  # 偏置向量

计算 z1：
  z1[i] = Σj x[j] * W1[j,i] + b1[i]
  
  z1 的形状: (128,)
  
  直观理解：每个隐藏神经元 i 接收所有输入的加权求和

计算 a1（ReLU 激活）：
  a1[i] = max(0, z1[i])
  
  a1 的形状: (128,)

批量计算（batch_size = N）：
  X: (N, 784)
  Z1 = X @ W1 + b1: (N, 128)
  A1 = ReLU(Z1): (N, 128)
```

### 5.1.2 第二步：隐藏层 -> 输出层

```
给定：
  a1: (128,)  # 隐藏层激活
  W2: (128, 10)  # 权重矩阵
  b2: (10,)  # 偏置向量

计算 z2：
  z2[i] = Σj a1[j] * W2
  a1[j] * W2[j,i] + b2[i]
  
  z2 的形状: (10,)
  
  直观理解：每个输出神经元 i 综合所有隐藏层的激活

计算 y（Softmax）：
  y[i] = softmax(z2)[i] = exp(z2[i]) / Σj exp(z2[j])
  
  y 的形状: (10,)  # 10 个数字的概率分布

批量计算（batch_size = N）：
  A1: (N, 128)
  Z2 = A1 @ W2 + b2: (N, 10)
  Y = Softmax(Z2): (N, 10)
```

---

## 5.2 前向传播的 NumPy 实现

```python
import numpy as np

class MLP:
    def __init__(self, input_size=784, hidden_size=128, output_size=10):
        # Xavier 初始化
        self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)
        self.b1 = np.zeros(hidden_size)
        self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size)
        self.b2 = np.zeros(output_size)
    
    def relu(self, x):
        return np.maximum(0, x)
    
    def softmax(self, x):
        # 数值稳定技巧
        x_shifted = x - np.max(x, axis=1, keepdims=True)
        exp_x = np.exp(x_shifted)
        return exp_x / np.sum(exp_x, axis=1, keepdims=True)
    
    def forward(self, X):
        """
        前向传播
        
        X: (batch_size, input_size) 输入图片
        返回: (batch_size, output_size) 每个类的概率
        """
        # 第一层
        self.z1 = X @ self.W1 + self.b1        # (batch, 784) @ (784, 128) = (batch, 128)
        self.a1 = self.relu(self.z1)          # (batch, 128)
        
        # 第二层
        self.z2 = self.a1 @ self.W2 + self.b2 # (batch, 128) @ (128, 10) = (batch, 10)
        self.probs = self.softmax(self.z2)    # (batch, 10)
        
        return self.probs
```

---

# 📖 第六部分：反向传播 —— 完整数学推导

> 这是神经网络最核心的部分，包含了完整的链式法则推导。

## 6.1 损失函数

### 6.1.1 交叉熵损失函数

```
对于多分类问题，我们使用交叉熵损失（Cross-Entropy Loss）

设：
  y_true: 真实标签的 One-Hot 编码 (10,)  # 比如数字 5：[0,0,0,0,0,1,0,0,0,0]
  y_pred: 网络预测的概率分布 (10,)  # 比如：[0.1, 0.05, 0.02, 0.03, 0.1, 0.6, 0.02, 0.05, 0.02, 0.01]

交叉熵损失：
  L = -Σ y_true[i] * log(y_pred[i])

由于 y_true 是 One-Hot 编码，只有一个元素是 1，其他都是 0
所以 L = -log(y_pred[真实类别])

例子：
  真实标签是 5，y_true[5] = 1
  网络预测 y_pred[5] = 0.6
  L = -log(0.6) = 0.51

  网络预测 y_pred[5] = 0.9
  L = -log(0.9) = 0.11
  
  预测越准确，损失越小！
```

### 6.1.2 交叉熵的求导

```
设 L = -Σ y_true[i] * log(y_pred[i])

对 y_pred[i] 求偏导：
  ∂L/∂y_pred[i] = -y_true[i] / y_pred[i]

但是我们实际上是对 z2（Softmax 的输入）求导，而不是对 y_pred。

设 y_pred = softmax(z2)

我们需要求：∂L/∂z2[i]
```

---

## 6.2 反向传播的数学推导

### 6.2.1 输出层梯度

```
已知：
  L = -Σ y_true[j] * log(y_pred[j])
  y_pred[j] = softmax(z2)[j] = exp(z2[j]) / Σk exp(z2[k])

我们需要求：∂L/∂z2[i]

根据链式法则：
  ∂L/∂z2[i] = Σj ∂L/∂y_pred[j] * ∂y_pred[j]/∂z2[i]
            = Σj (-y_true[j] / y_pred[j]) * ∂y_pred[j]/∂z2[i]

根据 Softmax 的求导结果：
  ∂y_i/∂z_j = { y_i * (1 - y_i),  if i == j
             { -y_i * y_j,      if i != j

所以：
  ∂L/∂z2[i] = Σj (-y_true[j] / y_pred[j]) * ∂y_pred[j]/∂z2[i]
            = (-y_true[i] / y_pred[i]) * y_pred[i] * (1 - y_pred[i])
              + Σ_{j≠i} (-y_true[j] / y_pred[j]) * (-y_pred[i] * y_pred[j])
            
            = -y_true[i] * (1 - y_pred[i]) + Σ_{j≠i} y_true[j] * y_pred[i]
            
            = -y_true[i] + y_true[i] * y_pred[i] + Σ_{j≠i} y_true[j] * y_pred[i]
            
            = -y_true[i] + Σj y_true[j] * y_pred[i]
            
            = -y_true[i] + y_pred[i] * Σj y_true[j]
            
            = -y_true[i] + y_pred[i] * 1  (因为 y_true 是 One-Hot，和为1)
            
            = y_pred[i] - y_true[i]

这就是著名的结论：Softmax + Cross-Entropy 的梯度 = y_pred - y_true！

d_z2 = y_pred - y_true
```

### 6.2.2 第二层权重梯度

```
现在我们已经知道 d_z2 = y_pred - y_true

需要求：
  ∂L/∂W2[i,j]
  ∂L/∂b2[i]

根据链式法则：
  L 是 z2 的函数，z2 是 W2, b2 的函数
  
  z2[j] = Σk a1[k] * W2[k,j] + b2[j]
  
  ∂z2[j]/∂W2[i,j] = a1[i]  # 只有 W2[i,j] 影响 z2[j]，系数是 a1[i]
  ∂z2[j]/∂b2[j] = 1

所以：
  ∂L/∂W2[i,j] = Σj ∂L/∂z2[j] * ∂z2[j]/∂W2[i,j]
              = Σj d_z2[j] * a1[i]
              = a1[i] * Σj d_z2[j]
              = a1[i] * d_z2[j]
              
  向量形式：
    d_W2 = a1.T @ d_z2  # (128, 10)
  
  ∂L/∂b2[i] = Σj ∂L/∂z2[j] * ∂z2[j]/∂b2[i]
            = d_z2[i]
            
  向量形式：
    d_b2 = d_z2  # (10,)
```

### 6.2.3 隐藏层梯度

```
现在需要把梯度反向传播到隐藏层。

我们需要求：
  ∂L/∂a1[i]  # 损失对隐藏层激活的梯度
  ∂L/∂z1[i]  # 损失对 ReLU 输入的梯度

第一步：求 ∂L/∂a1

根据前向传播：
  z2[j] = Σk a1[k] * W2[k,j] + b2[j]
  
所以：
  ∂z2[j]/∂a1[k] = W2[k,j]
  
  ∂L/∂a1[k] = Σj ∂L/∂z2[j] * ∂z2[j]/∂a1[k]
            = Σj d_z2[j] * W2[k,j]
            = Σj W2[k,j] * d_z2[j]
            
  向量形式：
    d_a1 = d_z2 @ self.W2.T  # (batch, 128)

第二步：求 ∂L/∂z1

根据前向传播：
  a1[i] = ReLU(z1[i])
  
ReLU 的导数：
  ∂a1[i]/∂z1[i] = 1, if z1[i] > 0
                 = 0, if z1[i] <= 0
  
所以：
  ∂L/∂z1[i] = ∂L/∂a1[i] * ∂a1[i]/∂z1[i]
            = d_a1[i] * ReLU'(z1[i])
            
  向量形式：
    d_z1 = d_a1 * (z1 > 0).astype(float)
```

### 6.2.4 第一层权重梯度

```
现在求损失对第一层权重的梯度。

我们需要求：
  ∂L/∂W1[i,j]
  ∂L/∂b1[i]

第一步：求 ∂L/∂z1

我们已经知道：
  d_z1[i] = ∂L/∂z1[i]

第二步：求 ∂L/∂W1[i,j]

根据前向传播：
  z1[j] = Σk x[k] * W1[k,j] + b1[j]
  
  ∂z1[j]/∂W1[i,j] = x[i]
  
  ∂L/∂W1[i,j] = Σj ∂L/∂z1[j] * ∂z1[j]/∂W1[i,j]
              = Σj d_z1[j] * x[i]
              = x[i] * Σj d_z1[j]
              
  向量形式：
    d_W1 = X.T @ d_z1  # (784, 128)

第三步：求 ∂L/∂b1

  ∂L/∂b1[i] = Σj ∂L/∂z1[j] * ∂z1[j]/∂b1[i]
            = d_z1[i]
            
  向量形式：
    d_b1 = d_z1  # (128,)
```

---

## 6.3 梯度更新

```
得到所有梯度后，用梯度下降更新权重：

  W1 = W1 - learning_rate * d_W1
  b1 = b1 - learning_rate * d_b1
  W2 = W2 - learning_rate * d_W2
  b2 = b2 - learning_rate * d_b2

注意：这里梯度已经除以了 batch_size（因为损失是平均的）
```

---

## 6.4 完整反向传播代码

```python
def backward(self, X, y_true):
    """
    反向传播：计算梯度并更新权重
    
    参数：
      X: (batch_size, 784) 输入图片
      y_true: (batch_size, 10) One-Hot 真实标签
    """
    batch_size = X.shape[0]
    
    # ===== 第1步：输出层梯度 =====
    # Softmax + CrossEntropy 的组合梯度：d_z2 = y_pred - y_true
    d_z2 = self.probs - y_true  # (batch_size, 10)
    
    # ===== 第2步：第二层权重梯度 =====
    d_W2 = self.a1.T @ d_z2  # (128, 10)
    d_b2 = np.sum(d_z2, axis=0)  # (10,)
    
    # ===== 第3步：隐藏层梯度 =====
    d_a1 = d_z2 @ self.W2.T  # (batch_size, 128)
    d_z1 = d_a1 * (self.z1 > 0).astype(float)  # ReLU 导数
    
    # ===== 第4步：第一层权重梯度 =====
    d_W1 = X.T @ d_z1  # (784, 128)
    d_b1 = np.sum(d_z1, axis=0)  # (128,)
    
    # ===== 第5步：梯度裁剪（防止梯度爆炸） =====
    max_grad = 1.0
    for dW in [d_W1, d_W2]:
        np.clip(dW, -max_grad, max_grad, out=dW)
    for db in [d_b1, d_b2]:
        np.clip(db, -max_grad, max_grad, out=db)
    
    # ===== 第6步：梯度下降更新权重 =====
    self.W1 -= self.lr * d_W1 / batch_size
    self.b1 -= self.lr * d_b1 / batch_size
    self.W2 -= self.lr * d_W2 / batch_size
    self.b2 -= self.lr * d_b2 / batch_size
```

---

# 📖 第七部分：训练流程

## 7.1 完整训练循环

```python
def fit(self, X_train, y_train, epochs=50, batch_size=64):
    """
    训练 MLP
    
    参数：
      X_train: (N, 784) 训练图片
      y_train: (N, 10) 训练标签（One-Hot）
      epochs: 训练轮数
      batch_size: 每批图片数
    """
    N = len(X_train)
    num_batches = (N + batch_size - 1) // batch_size
    
    for epoch in range(epochs):
        # 打乱数据
        indices = np.random.permutation(N)
        X_shuffled = X_train[indices]
        y_shuffled = y_train[indices]
        
        epoch_loss = 0
        
        # 批训练
        for batch_idx in range(num_batches):
            start = batch_idx * batch_size
            end = min(start + batch_size, N)
            X_batch = X_shuffled[start:end]
            y_batch = y_shuffled[start:end]
            
            # 前向传播
            probs = self.forward(X_batch)
            
            # 反向传播
            self.backward(X_batch, y_batch)
            
            # 计算损失
            loss = self.cross_entropy_loss(probs, y_batch)
            epoch_loss += loss
        
        # 打印进度
        if (epoch + 1) % 5 == 0:
            acc = self.accuracy(X_train, y_train)
            print(f"Epoch {epoch+1:3d}/{epochs} | Loss: {epoch_loss/num_batches:.4f} | 准确率: {acc:.4f}")
    
    def cross_entropy_loss(self, probs, y_true):
        """交叉熵损失"""
        # 取真实类别的概率
        correct_probs = probs[np.arange(len(y_true)), y_true.argmax(axis=1)]
        # 避免 log(0)
        loss = -np.mean(np.log(np.clip(correct_probs, 1e-10, 1.0)))
        return loss
    
    def accuracy(self, X, y):
        """计算准确率"""
        if len(y.shape) > 1:
            y = np.argmax(y, axis=1)
        predictions = np.argmax(self.forward(X), axis=1)
        return np.mean(predictions == y)
```

---

## 7.2 超参数详解

| 超参数 | 默认值 | 说明 | 调参建议 |
|--------|--------|------|---------|
| **hidden_size** | 128 | 隐藏层神经元数 | 多→能力强但慢，少→快但弱 |
| **learning_rate** | 0.1 | 学习率（每步走多远） | 大→震荡，小→太慢 |
| **epochs** | 50 | 训练轮数 | 多→过拟合，少→欠拟合 |
| **batch_size** | 64 | 每批图片数 | 大→稳定，小→泛化好 |

---

# 📖 第八部分：MNIST 数据集

## 8.1 MNIST 数据集介绍

```
MNIST = Modified National Institute of Standards and Technology
        美国国家标准与技术研究院改良数据集

数据集信息：
  - 70,000 张图片（60,000 训练 + 10,000 测试）
  - 每张图片 28x28 像素，灰度图
  - 10 类（数字 0-9）
  - 来自 250 个不同人的手写样本
```

## 8.2 数据预处理

```python
def load_and_preprocess():
    """加载并预处理 MNIST 数据"""
    # 1. 加载数据（使用 sklearn）
    from sklearn.datasets import fetch_openml
    mnist = fetch_openml('mnist_784', version=1)
    X, y = mnist.data, mnist.target
    
    # 2. 转换为 float32 并归一化到 [0, 1]
    X = X.values.astype(np.float32) / 255.0
    
    # 3. 标签转为 One-Hot
    y = y.astype(int)
    num_classes = 10
    y_onehot = np.zeros((len(y), num_classes))
    y_onehot[np.arange(len(y)), y] = 1
    
    return X, y_onehot
```

---

# 📖 第九部分：梯度消失与梯度爆炸

## 9.1 什么是梯度消失？

```
在反向传播过程中，梯度可能会变得非常小（接近 0）

原因：
  - 链式法则的乘法效应
  - 深层网络中，梯度逐层相乘
  - 如果每层的梯度都小于 1，深层的梯度会指数衰减

例子：
  假设每层的梯度都是 0.5
  第 1 层：梯度 = 0.5
  第 5 层：梯度 = 0.5^5 = 0.03
  第 10 层：梯度 = 0.5^10 = 0.001
  
  深层的权重几乎学不到东西！
```

## 9.2 什么是梯度爆炸？

```
与梯度消失相反，梯度可能会变得非常大

原因：
  - 每层的梯度大于 1
  - 深层网络中，梯度指数增长

例子：
  假设每层的梯度都是 2
  第 1 层：梯度 = 2
  第 5 层：梯度 = 2^5 = 32
  第 10 层：梯度 = 2^10 = 1024
  
  权重更新过大，网络不稳定！
```

## 9.3 解决方案

```
梯度裁剪：
  d_W = clip(d_W, -max_grad, max_grad)
  
  本项目使用这种方法，效果良好
  
ReLU 激活函数：
  - 正半轴梯度为 1，不会衰减
  - 缓解梯度消失问题

Xavier/He 初始化：
  - 让梯度在传播过程中保持合理范围
  - 本项目使用 Xavier 初始化
```

---

# 📖 第十部分：总结与公式速查

## 10.1 核心公式汇总

| 公式 | 说明 |
|------|------|
| `z1 = X @ W1 + b1` | 第一层线性变换 |
| `a1 = ReLU(z1)` | 第一层激活 |
| `z2 = a1 @ W2 + b2` | 第二层线性变换 |
| `y = softmax(z2)` | Softmax 概率输出 |
| `L = -Σ y_true * log(y_pred)` | 交叉熵损失 |
| `d_z2 = y_pred - y_true` | Softmax+CE 梯度 |
| `d_W2 = a1.T @ d_z2` | 第二层权重梯度 |
| `d_z1 = d_a1 * (z1 > 0)` | ReLU 梯度 |
| `d_W1 = X.T @ d_z1` | 第一层权重梯度 |
| `W = W - lr * d_W` | 梯度下降更新 |

## 10.2 训练流程图

```
输入数据 X
    |
    v
前向传播 -> 保存中间结果 (z1, a1, z2, probs)
    |
    v
计算损失 L = -Σ y_true * log(probs)
    |
    v
反向传播（从后往前）：
  d_z2 = probs - y_true
  d_W2 = a1.T @ d_z2
  d_z1 = (d_z2 @ W2.T) * ReLU'(z1)
  d_W1 = X.T @ d_z1
    |
    v
梯度下降更新权重
    |
    v
重复，直到损失足够小
```

---

# 📝 课后练习

## 练习1：手算前向传播

已知：
```
x = [1, 0, 1]  # 输入
W1 = [[1, 0], [0, 1], [1, 1]]  # (3, 2)
b1 = [0, 0]
W2 = [[1, 2], [3, 4]]  # (2, 2)
b2 = [0, 0]
```

计算：
1. z1 = x @ W1 + b1
2. a1 = ReLU(z1)
3. z2 = a1 @ W2 + b2
4. y = softmax(z2)

## 练习2：验证 XOR 的多层解决方案

用两层感知机实现 XOR：
- 隐藏层：h1 = AND(NOT x1, x2), h2 = AND(x1, NOT x2)
- 输出层：y = OR(h1, h2)

验证对所有 4 种输入都正确。

## 练习3：理解梯度消失

如果每层的 ReLU 输出有一半是 0（因为输入小于 0），那么 10 层之后：
- 第 10 层的梯度大约是第 1 层的多少？
- 这说明什么问题？

## 练习4：调参实验

修改 config.py 中的超参数，记录变化：
- hidden_size: 64 vs 256
- learning_rate: 0.01 vs 1.0
- epochs: 20 vs 100

---

> **记住**：神经网络的核心是"前向传播 + 反向传播 + 梯度下降"，通过不断调整权重来最小化损失！
