Skip to content

第 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 公式

σ(x)=11+ex
代码: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 的导数——完整推导

目标:dσdx

f(x)=11+ex,我们用商的求导法则

(uv)=uvuvv2

这里 u=1v=1+ex,所以:

f(x)=0(1+ex)1(1+ex)(1+ex)2

先算分子中的 (1+ex)

(1+ex)=0+(1)ex=ex

(因为 ddxex=ex(1) ,链式法则)

代入:

f(x)=1(ex)(1+ex)2=ex(1+ex)2

现在进行关键的化简。把分子分母分别处理:

f(x)=ex(1+ex)2

把分子和分母拆开:

=11+exex1+ex

注意第一项正是 σ(x),再处理第二项:

ex1+ex=(1+ex)11+ex=111+ex=1σ(x)

(分子加减 1,拆开分数)

所以:

f(x)=σ(x)(1σ(x))

这个结论极其优雅:Sigmoid 的导数可以用它自己的输出值来表示!

在代码中,如果 z = sigmoid(x) 已经算好了,那么导数就是:

python
def sigmoid_grad(z):  # z 是 sigmoid 的输出(不是输入!)
    return z * (1.0 - z)

不需要重新计算 sigmoid,直接用已经算好的 z 就行——这在反向传播中节省了一次前向传播计算。

3.2.4 数值验证

x=0

  • σ(0)=11+e0=11+1=0.5
  • σ(0)=0.5×(10.5)=0.5×0.5=0.25

x=2

  • σ(2)=11+e211+0.1350.880
  • σ(2)0.880×(10.880)0.880×0.1200.105

注意:当 x 很大(接近 1)或很小(接近 0)时,导数趋近于 0。这就是梯度消失问题——在深层网络中,信号通过多个 Sigmoid 层后会越来越弱,几乎无法传播。这是 ReLU 出现的原因。


3.3 ReLU 函数

3.3.1 公式

ReLU(x)=max(0,x)
代码:np.maximum(0, x)

3.3.2 图像与性质

ReLU(x)
     |             /
   3 |            /
   2 |           /
   1 |          /
   0 +----------+------→ x
    -4  -2   0  1  2  4

     负数都变0

性质:

  • x>0:直接输出 x(梯度 = 1,信号完整传递)
  • x0:输出 0(梯度 = 0,信号截断)
  • 计算极其简单,速度快

3.3.3 ReLU 的导数——完整推导

ReLU 是分段函数,分段求导:

x>0 时:

ReLU(x)=xddxReLU(x)=1

x<0 时:

ReLU(x)=0ddxReLU(x)=0

x=0 时:

严格来说在 x=0 处不可导(左导数=0,右导数=1),但实际代码中约定等于 0(或 1),影响几乎可以忽略。

所以:

ReLU(x)={1x>00x0
python
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 好?

特性SigmoidReLU
正区间梯度< 0.25(随 |x| 增大而减小)恒为 1
计算复杂度需要计算 ex(较慢)只需比较大小(极快)
梯度消失严重(深层网络)正区间不会消失
死亡问题有(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 公式

对于向量 a=[a1,a2,...,aK](K=10个类别):

softmax(ai)=eaij=1Keaj

直觉: 先对每个元素取指数(把所有数变成正数),然后除以总和(归一化到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):

python
import numpy as np
np.exp(1000)   # → inf(溢出!)

解决方案: 利用 softmax 的一个数学性质——减去任意常数 C 不改变结果:

softmax(ai)=eaiCjeajC

证明:

eaiCjeajC=eaieCjeajeC=eaieCeCjeaj=eaijeaj

分子分母都乘以 eCeC 约分消掉,结果不变。

实践中取 C = max(a): 这样所有的指数 eaimax(a) 的底数 aimax(a)0,所以结果在 (0,1] 之间,永远不会溢出

python
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 三种激活函数对比

激活函数公式用在哪里梯度消失?
Sigmoid11+ex隐藏层(本项目)有(深层时)
ReLUmax(0,x)隐藏层(现代网络首选)正区间无
Softmaxexiexj多分类输出层不单独使用

3.6 小结

函数公式导数
Sigmoidσ(x)=11+exσ(x)(1σ(x)) = z*(1-z)
ReLUmax(0,x)1 if x>0 else 0
Softmaxexiexj与交叉熵联合求导(见第6章)

下一章,把激活函数串联起来,走一遍完整的前向传播,带着详细的维度标注。


← 第 2 章 | 返回目录 | 第 4 章:前向传播 →

基于 Kaggle MNIST 数据集,使用纯 numpy 从零实现