Files
task-3-3-2-MLP/README.md

1533 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📚 多层感知机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, ..., xnn 个输入)
权重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 接收所有输入的加权求和
计算 a1ReLU 激活):
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 综合所有隐藏层的激活
计算 ySoftmax
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[真实类别])
例子:
真实标签是 5y_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]
但是我们实际上是对 z2Softmax 的输入)求导,而不是对 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
---
> **记住**:神经网络的核心是"前向传播 + 反向传播 + 梯度下降",通过不断调整权重来最小化损失!