第 3 章:激活函数
这是理论篇中推导最密集的一章。请耐心读完——每一个推导步骤都很重要,后面的反向传播会直接用到这些结论。
3.1 为什么需要激活函数?
先看一个问题。假设没有激活函数,网络的计算变成:
第一层:a1 = X @ W1 + b1
第二层:输出 = a1 @ W2 + b2
= (X @ W1 + b1) @ W2 + b2
= X @ (W1 @ W2) + b1 @ W2 + b2
= X @ W_new + b_new展开后,多层网络等价于一个单层网络(W_new = W1 @ W2 是另一个矩阵)!
无论叠多少层,最终都是 输出 = X @ 某矩阵 + 某向量,这是线性变换,表达能力极其有限,无法拟合复杂的非线性关系。
激活函数的作用: 在每层之后引入非线性,使多层网络能够拟合任意复杂的函数。这是神经网络强大能力的根源。
3.2 Sigmoid 函数
3.2.1 公式
代码:1.0 / (1.0 + np.exp(-x))3.2.2 图像与性质
σ(x)
1.0 | ____----
| ----
0.5 | ----
| ----
0.0 |----____
+------+------+------+------+------+------→ x
-4 -2 0 2 4性质:
- 输出范围:(0, 1) — 永远在 0 和 1 之间
σ(0) = 0.5— x=0 时输出正好是 0.5- x 很大时 → 趋近于 1;x 很小时 → 趋近于 0
- 函数连续可微 — 可以求导(反向传播的前提)
3.2.3 Sigmoid 的导数——完整推导
目标: 求
设
这里
先算分子中的
(因为
代入:
现在进行关键的化简。把分子分母分别处理:
把分子和分母拆开:
注意第一项正是
(分子加减 1,拆开分数)
所以:
这个结论极其优雅:Sigmoid 的导数可以用它自己的输出值来表示!
在代码中,如果 z = sigmoid(x) 已经算好了,那么导数就是:
def sigmoid_grad(z): # z 是 sigmoid 的输出(不是输入!)
return z * (1.0 - z)不需要重新计算 sigmoid,直接用已经算好的 z 就行——这在反向传播中节省了一次前向传播计算。
3.2.4 数值验证
取
取
注意:当
3.3 ReLU 函数
3.3.1 公式
代码:np.maximum(0, x)3.3.2 图像与性质
ReLU(x)
| /
3 | /
2 | /
1 | /
0 +----------+------→ x
-4 -2 0 1 2 4
↑
负数都变0性质:
:直接输出 (梯度 = 1,信号完整传递) :输出 0(梯度 = 0,信号截断) - 计算极其简单,速度快
3.3.3 ReLU 的导数——完整推导
ReLU 是分段函数,分段求导:
当
当
当
严格来说在
所以:
def relu_grad(x): # x 是 ReLU 的输入(原始线性激活值)
return (x > 0).astype(float)
# (x > 0) 生成布尔数组:x>0的位置是True(1.0),其他是False(0.0)注意: ReLU 的导数需要传入输入值(判断正负),而 Sigmoid 的导数需要传入输出值。
3.3.4 为什么 ReLU 比 Sigmoid 好?
| 特性 | Sigmoid | ReLU |
|---|---|---|
| 正区间梯度 | < 0.25(随 |x| 增大而减小) | 恒为 1 |
| 计算复杂度 | 需要计算 | 只需比较大小(极快) |
| 梯度消失 | 严重(深层网络) | 正区间不会消失 |
| 死亡问题 | 无 | 有(Dead ReLU) |
本项目使用的是 Sigmoid(隐藏层),主要是为了教学目的(求导公式更有趣),实际深度学习项目中 ReLU 及其变种更常用。
3.4 Softmax 函数
3.4.1 为什么输出层用 Softmax?
到目前为止,网络的输出 a2 = z1 @ W2 + b2 是一个普通的实数向量:
a2 = [-2.1, 0.5, 3.2, -0.8, 1.1, 0.3, -1.5, 8.7, 0.2, -0.4]
↑ ↑ ↑ ↑ ↑
是0? 是1? 是2? 是6? 是7?这些数字可以是任意实数,有正有负,总和也不是 1。我们没法直接说"是7的概率是 8.7"——因为概率必须在 [0,1] 之间,且所有类别的概率之和必须等于 1。
Softmax 就是把任意实数向量转换为合法概率分布的函数。
3.4.2 公式
对于向量
直觉: 先对每个元素取指数(把所有数变成正数),然后除以总和(归一化到1)。
3.4.3 一个具体例子
输入 a2 = [-2.1, 0.5, 3.2] (简化成3类)
步骤1:取指数
e^(-2.1) ≈ 0.122
e^(0.5) ≈ 1.649
e^(3.2) ≈ 24.533
步骤2:求总和
总和 = 0.122 + 1.649 + 24.533 = 26.304
步骤3:各自除以总和
y₁ = 0.122 / 26.304 ≈ 0.005 (0.5%)
y₂ = 1.649 / 26.304 ≈ 0.063 (6.3%)
y₃ = 24.533 / 26.304 ≈ 0.933 (93.3%)
验证:0.005 + 0.063 + 0.933 = 1.001 ≈ 1 ✓最大的输入值(3.2)经过 softmax 之后得到了最大的概率(93.3%)。Softmax 会放大差距(因为指数函数增长很快)。
3.4.4 数值稳定性问题与解决方案
直接计算 softmax 会遇到一个问题:当 a 中有很大的数时,e^a 会溢出(变成 inf):
import numpy as np
np.exp(1000) # → inf(溢出!)解决方案: 利用 softmax 的一个数学性质——减去任意常数 C 不改变结果:
证明:
分子分母都乘以
实践中取 C = max(a): 这样所有的指数
def softmax(x):
if x.ndim == 2: # 批量(矩阵)情况
x = x.T
x = x - np.max(x, axis=0) # 每列减去该列最大值
y = np.exp(x) / np.sum(np.exp(x), axis=0)
return y.T
# 单样本(向量)情况
x = x - np.max(x) # 减去最大值,防止溢出
return np.exp(x) / np.sum(np.exp(x))3.4.5 为什么 Softmax 的导数不需要单独实现?
Softmax 的 Jacobian 矩阵(完整的导数矩阵)推导很复杂(因为每个输出都依赖所有输入)。
但有一个数学奇迹:当 Softmax 与交叉熵损失函数组合使用时,组合的导数极为简洁。
这个推导在第 6 章(反向传播)会详细展示。这就是为什么 functions.py 里没有 softmax_grad 函数。
3.5 三种激活函数对比
| 激活函数 | 公式 | 用在哪里 | 梯度消失? |
|---|---|---|---|
| Sigmoid | 隐藏层(本项目) | 有(深层时) | |
| ReLU | 隐藏层(现代网络首选) | 正区间无 | |
| Softmax | 多分类输出层 | 不单独使用 |
3.6 小结
| 函数 | 公式 | 导数 |
|---|---|---|
| Sigmoid | z*(1-z) | |
| ReLU | 1 if x>0 else 0 | |
| Softmax | 与交叉熵联合求导(见第6章) |
下一章,把激活函数串联起来,走一遍完整的前向传播,带着详细的维度标注。
← 第 2 章 | 返回目录 | 第 4 章:前向传播 →