1533 lines
35 KiB
Markdown
1533 lines
35 KiB
Markdown
# 📚 多层感知机(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
|
||
|
||
---
|
||
|
||
> **记住**:神经网络的核心是"前向传播 + 反向传播 + 梯度下降",通过不断调整权重来最小化损失!
|