Lecture 3 · 浅层神经网络:隐藏层、激活函数与反向传播 · Shallow Neural Networks

Deep Learning · Lecture 03。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:Coursera Deep Learning Specialization C1M3 “Shallow Neural Networks”(Neural Networks Overview、Neural Network Representation、Computing a Neural Network’s Output、Vectorizing Across Multiple Examples、Explanation for Vectorized Implementation、Activation Functions、Why do you need Non-Linear Activation Functions?、Derivatives of Activation Functions、Gradient Descent for Neural Networks、Backpropagation Intuition、Random Initialization);编程作业 Planar Data Classification with One Hidden Layer。在 Fall 2025 的课程表里,C1M3 和第 3 次课堂讲座(10/7,Full Cycle of a DL Project)在同一周,那次课堂讲座的内容放在本讲义第 11 讲。 依据说明:Coursera 的视频和编程作业只对注册学员开放,不能公开引用。本讲按 Andrew Ng 在 C1M3 里广为人知的内容来写:记号、公式和推导顺序,“花朵”数据的生成公式,以及作业里”隐藏单元数 1–50”的实验设计。作业的数据生成器照原公式用 numpy 重写,具体的点和原作业不同,所以数字也不同。激活函数的历史与性质依据原始论文(§13)。所有实验数字都来自配套代码。 配套代码 Code:code/lecture03/(numpy;5 个脚本 + 3 个工具模块。01、02 约 1 秒,04 约 27 秒,03、05 各约 45–50 秒) 预计阅读时间:3 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

学完这一讲,你应该能够:

  1. 用 Andrew Ng 的记号(上标 [l][l] 表示层、上标 (i)(i) 表示样本、下标 ii 表示层内第几个单元)写出一个两层网络,说出每个量的形状,并解释为什么”一个隐藏层的网络”叫两层网络。
  2. 从”一个样本、一个单元”的公式出发,推出向量化的前向传播 Z[1]=W[1]X+b[1]Z^{[1]} = W^{[1]}X + b^{[1]}……,并说清楚为什么把样本按列堆起来以后公式不变。
  3. 画出 sigmoid、tanh、ReLU、Leaky ReLU 的形状,推导它们的导数,说出各自的优缺点,以及隐藏层和输出层分别该怎么选。
  4. 证明并用实验验证:没有非线性激活,网络叠多少层都等于一层线性函数。线性激活的两层网络就是逻辑回归。
  5. 独立推导两层网络反向传播的 6 个公式,做维度检查,并会用数值梯度检查自己的实现。
  6. 解释为什么不能把权重初始化成 0(对称性),以及为什么初始权重要小(饱和)。
  7. 看懂隐藏层大小怎样影响决策边界,认出过拟合(overfitting)的苗头。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面几讲再展开 Previewed only
第 1 讲:神经元 = 线性 + 非线性;训练四步(前向 → 损失 → 反向 → 更新);X∈Rnx×mX \in \mathbb{R}^{n_x \times m} 按列堆样本LL 层网络的通用实现、“为什么要深”(第 4 讲)
第 2 讲:sigmoid 输出 + 交叉熵损失;计算图与链式法则;逻辑回归的反向传播 dZ=A−YdZ = A - Y、dw=1mX dZ⊤dw = \frac1m X\,dZ^\top;向量化与广播Xavier / He 初始化的推导;梯度检验的系统用法(第 7 讲)
矩阵乘法、转置;求导的链式法则偏差与方差、正则化、用验证集选超参数(第 6 讲)
mini-batch、Adam(第 8 讲);多分类的 softmax 输出层(第 9 讲)

第 1 讲的 tiny_nn.py 是一个能处理任意层数的”黑盒”。本讲只看一个隐藏层的情形,把这个黑盒里的每一个公式摊开、推导、用实验验证。配套的 shallow_nn.py 从零实现了这个网络,函数划分和 Coursera 编程作业一样;代码 02 会验证它和 tiny_nn 算出的梯度完全一致。

0.3 本讲地图 Map

§1  为什么需要隐藏层:花朵数据上,逻辑回归 ≈ 抛硬币,4 个 tanh 单元 ≈ 94%      ← 代码 01 演示 1–2
§2  网络的表示:记号、形状、每个隐藏单元在做什么                                ← 代码 01 演示 3–4
§3  向量化:从一个样本到 m 个样本,为什么公式不变                                ← 代码 01 演示 3
§4  激活函数:sigmoid / tanh / ReLU / Leaky ReLU 的形状、导数与选择               ← 代码 03 演示 1、3
§5  为什么必须有非线性:线性激活的两层网络 = 逻辑回归                            ← 代码 03 演示 2
§6  梯度下降与反向传播:6 个公式的推导、维度检查、梯度检验                        ← 代码 02
§7  随机初始化:对称性,以及为什么要"小"                                        ← 代码 04
§8  隐藏层大小:决策边界、Bayes 上限、过拟合的苗头                               ← 代码 05
§9  拼起来:一个完整的训练流程(nn_model)                                       ← shallow_nn.py
§10–14  小结、常见误区、练习、延伸阅读、下一讲预告

0.4 记号 Notation(沿用第 1 讲 §0.4,本讲新增)

符号含义例子
ai[1]a^{[1]}_i第 1 层第 ii 个单元的激活值(下标 = 层内第几个单元)a2[1]a^{[1]}_2:第 2 个隐藏单元的输出
a[1](i)a^{[1]{}(i)}第 ii 个样本在第 1 层的激活值(一个列向量)方括号是层,圆括号是样本
wi[1]∈Rnxw^{[1]}_i \in \mathbb{R}^{n_x}第 1 层第 ii 个单元的权重(列向量);W[1]W^{[1]} 的第 ii 行是 wi[1]⊤w^{[1]\top}_i
nh=n[1]n_h = n^{[1]}隐藏单元个数花朵数据上常用 nh=4n_h = 4
g[l]g^{[l]}第 ll 层的激活函数;g′g' 是它的导数g[1]=tanh⁡g^{[1]} = \tanh,g[2]=σg^{[2]} = \sigma
dW[l],db[l],dZ[l],dA[l]dW^{[l]}, db^{[l]}, dZ^{[l]}, dA^{[l]}Ng 的简写:dW[l]dW^{[l]} 表示 ∂J/∂W[l]\partial J / \partial W^{[l]},其余类推和对应的量形状相同
∗*逐元素乘法(numpy 里的 *),区别于矩阵乘法(numpy 里的 @)W[2]⊤dZ[2]∗g′(Z[1])W^{[2]\top}dZ^{[2]} * g'(Z^{[1]})

数层数:输入层记作第 0 层(a[0]=xa^{[0]} = x),不计入层数。所以”输入 → 一个隐藏层 → 输出层”是一个两层网络(2-layer neural network),L=2L = 2。代码里写作 [n_x, n_h, 1]。

0.5 关于数字 · About the numbers

讲义里的实验数字都来自配套代码的实际输出(固定随机种子;每个演示用自己的随机数生成器;同一台机器上重复运行逐字相同,本讲没有计时类的数字)。数据是按 Coursera 作业的公式重新生成的,所以和原作业的数字不同。原作业报告的训练集准确率大约是:逻辑回归 47%,4 个隐藏单元 90%;我们这里是 51.0% 和 94.0%,趋势一致。代码 05 的准确率打印 4 位小数,其他脚本打印 3 位。有几处结果和我写代码前的预想不一样,讲义按实际结果来写,这些情况列在代码 README 的”关于实验结果”里。


1. 为什么需要一个隐藏层 · Why a hidden layer?

1.1 从第 2 讲出发

第 2 讲推导了逻辑回归:一个样本 xx 进来,算 z=w⊤x+bz = w^\top x + b,再算 a=σ(z)a = \sigma(z) 作为”是正类的概率”;损失用交叉熵;反向传播时有一个漂亮的结果 dz=a−ydz = a - y;mm 个样本一起算时是 dZ=A−YdZ = A - Y、dw=1mX dZ⊤dw = \frac{1}{m} X\,dZ^\top、db=1m∑idz(i)db = \frac1m \sum_i dz^{(i)}。第 2 讲还强调:逻辑回归就是一个没有隐藏层的神经网络,只有一个 sigmoid 单元。

逻辑回归的决策边界是 w⊤x+b=0w^\top x + b = 0,在二维平面上是一条直线(高维时是一个超平面)。直线一侧判为 1,另一侧判为 0。那么问题来了:如果两类数据不能用一条直线分开呢?

1.2 花朵数据

Coursera 的编程作业用了一个专门为此设计的数据集,因为点排成一朵花的形状,大家叫它”花朵”数据(load_planar_dataset)。生成方法是这样的(planar_data.py):每类 200 个点,对第 jj 类(j=0,1j = 0, 1):

t=linspace(3.12 j, 3.12 (j+1), 200)+0.2 ϵ1,r=4sin⁡(4t)+0.2 ϵ2,x=(rsin⁡t, rcos⁡t)t = \text{linspace}(3.12\,j,\ 3.12\,(j+1),\ 200) + 0.2\,\epsilon_1,\qquad r = 4\sin(4t) + 0.2\,\epsilon_2,\qquad x = (r \sin t,\ r\cos t)

其中 ϵ1,ϵ2\epsilon_1, \epsilon_2 是标准正态噪声。tt 是角度,rr 是半径。sin⁡(4t)\sin(4t) 让半径随角度起伏,形成”花瓣”。rr 可以是负数,负半径相当于把点翻到原点的另一侧。正因为这个翻转,两类的花瓣交替出现:第 0 类本来只占 t∈[0,3.12]t \in [0, 3.12] 的”上半圈”,但有一半花瓣被翻到了另一半。两个 0.2 的噪声让花瓣有了宽度,也让两类在花心附近和花瓣边缘互相重叠。所以这个数据集没有 100% 正确的分类器(§8.2 会算出最好能做到多少)。

仔细算一下还会发现一个细节(代码 01 演示 4 用到它):8 片花瓣每片占 45°,但有两处是相邻的两片同色,所以绕原点一圈,颜色只变 6 次,而不是 8 次。

我们用这个生成器生成 400 个训练样本(和原作业一样),再用另一个种子独立生成 2000 个测试样本。测试集取 2000 个是为了让测试准确率更稳:400 个样本时,准确率本身就有约 ±1.5 个百分点的随机误差。

1.3 逻辑回归 vs 4 个隐藏单元

代码 01 演示 1–2 在花朵数据上训练了两个模型,都用学习率 1.2、10000 步全批量梯度下降(作业的设定):

模型参数个数训练准确率测试准确率
逻辑回归 [2, 1]30.5100.501
两层网络 [2, 4, 1],tanh 隐藏层170.9400.906

终端字符画(plot_tools.py)画出了两个模型的决策边界。背景 ' ' 表示模型预测第 0 类,'.' 表示预测第 1 类;o、x 是两类训练样本,大写的 O、X 是分错的点:

逻辑回归:训练 51.0%                         4 个 tanh 隐藏单元:训练 94.0%
+----------------------------------------+   +----------------------------------------+
|                                        |   |                                        |
|           oooX          oo             |   |           oooX          oo           ..|
|           oooXXo      oooo             |   |..         oooXXo      oooo         ....|
|            o  oo    oo   oo            |   |....        o  oo    oo   oo      ......|
|                 oo  o    o             |   |.......          oo  o    o    .........|
|    X        o    o oX   o        XX    |   |....x....    o    o oX   o   .....xx....|
|  XXXX X XXX oo   Xoo   ooXX XX XXX XX  |   |..xxxx.x.xxx oo   Xoo   ooXx.xx.xxx.xx..|
|    X       X Xo   Xo   * X      XXX X  |   |....x.......x.Xo   Xo   *.x......xxx.x..|
|     X XX      Xoo X o*X      XXXX  ....|   |.....x.xx......xOo X o*x......xxxx......|
|          X   XXXoX*oXXXXx.x.x..........|   |..........x...xxxOx*Oxxxxx.x.x..........|
|         ooooooO.x.OxxOO.O.OOOO.........|   |         ooooooo X OxXoo o oooo         |
|    OOOO......OO*x......O.....O.OO......|   |    oooo      oo*x..... o     o oo      |
|...O.O.....O.OOx...O...x.OOO.......O....|   |   o o     o oOx...O...x.Ooo       o    |
|..OOOOOOO.OO..x...x.x...x..OOO.OOOOOO...|   |  ooooooo oo..x...x.x...x..Ooo oooooo   |
|....................x....xx.............|   |          ..........x....xx....         |
|............x....xO..x....x.............|   |        ....x....xO..x....x......       |
|............x....x....x....x............|   |     .......x....x....x....x........    |
|...........xxxxx.......x.xxx............|   |   ........xxxxx.......x.xxx..........  |
|...........xxxxx........xxxx............|   | ..........xxxxx........xxxx............|
|........................................|   |........................................|
+----------------------------------------+   +----------------------------------------+

左边满屏的大写字母:逻辑回归学到的边界是 0.049 x1−0.267 x2−0.010=00.049\,x_1 - 0.267\,x_2 - 0.010 = 0,一条几乎水平的直线。花瓣绕着中心交替排列,任何一条直线两侧都是两类各占一半,所以 51% 并不是梯度下降没收敛,而是模型本身只能画直线。这种”模型太简单、再多数据和训练也没用”的情况叫 high bias(偏差大),第 6 讲会系统地诊断它。

右边只多了 4 个隐藏单元(参数从 3 个变成 17 个),'.' 区域就被切成了几块扇形,大致贴合了花瓣,训练准确率 94.0%,测试准确率 90.6%。

训练过程(每 1000 步记录一次,节选):

步数cost JJ训练准确率测试准确率
00.69320.3200.301
10000.21540.9300.909
50000.18780.9400.905
100000.17990.9400.906

第 0 步的 cost 是 0.6932≈ln⁡20.6932 \approx \ln 2:权重很小时网络对每个样本都输出约 0.5,“完全没把握”时的交叉熵就是 ln⁡2\ln 2。前 1000 步就学会了大部分东西,之后 cost 还在慢慢降,准确率基本不动。

一个隐藏层到底多做了什么? 这是 §2 的问题。先给答案:每个隐藏单元本身就是一个”小逻辑回归”,各自画一条直线;输出层再把这几条直线的判断组合起来。4 条直线组合起来,就能围出直线画不出的形状。


2. 网络的表示 · Neural network representation

2.1 输入层、隐藏层、输出层

   输入层(第 0 层)        隐藏层(第 1 层)              输出层(第 2 层)

        x₁ ─────────┬──────→ ○  a₁[1] ─┐
              ╲   ╱ ├──────→ ○  a₂[1] ─┤
               ╲ ╱  │                   ├──────→ ○  a[2] = ŷ
               ╱ ╲  ├──────→ ○  a₃[1] ─┤
        x₂ ───╱───╲─┴──────→ ○  a₄[1] ─┘
                   (每个输入连到每个隐藏单元:全连接)
  • 输入层就是 xx 本身,记作 a[0]=xa^{[0]} = x。它不做任何计算,所以不算一层。
  • 隐藏层(hidden layer):“隐藏”的意思是:训练集里有 xx 也有 yy,但没有隐藏单元”应该输出什么”的标准答案。它们的值由网络自己在训练中决定(第 1 讲 §2.3 的”自动学出中间特征”)。
  • 输出层输出 y^=a[2]\hat y = a^{[2]}。二分类时它是一个 sigmoid 单元。

2.2 每个圆圈做两件事

Andrew Ng 讲”Computing a Neural Network’s Output”时,先只看一个圆圈。每个圆圈都做和逻辑回归一模一样的两件事:

z=w⊤x+b(线性部分),a=g(z)(激活函数)z = w^\top x + b \quad\text{(线性部分)},\qquad a = g(z) \quad\text{(激活函数)}

隐藏层的 4 个单元各有自己的 ww 和 bb,各自算一遍:

z1[1]=w1[1]⊤x+b1[1],  a1[1]=g(z1[1]);…z4[1]=w4[1]⊤x+b4[1],  a4[1]=g(z4[1])z^{[1]}_1 = w^{[1]\top}_1 x + b^{[1]}_1,\ \ a^{[1]}_1 = g(z^{[1]}_1);\qquad \dots \qquad z^{[1]}_4 = w^{[1]\top}_4 x + b^{[1]}_4,\ \ a^{[1]}_4 = g(z^{[1]}_4)

写 4 遍太啰嗦。把 4 个 wi[1]⊤w^{[1]\top}_i 竖着叠成一个矩阵的 4 行,4 个 bi[1]b^{[1]}_i 叠成一个列向量:

[z1[1]z2[1]z3[1]z4[1]]⏟z[1]: (4,1)=[−w1[1]⊤−−w2[1]⊤−−w3[1]⊤−−w4[1]⊤−]⏟W[1]: (4,2)[x1x2]⏟x: (2,1)+[b1[1]b2[1]b3[1]b4[1]]⏟b[1]: (4,1)\underbrace{\begin{bmatrix} z^{[1]}_1 \\ z^{[1]}_2 \\ z^{[1]}_3 \\ z^{[1]}_4 \end{bmatrix}}_{z^{[1]}:\ (4,1)} = \underbrace{\begin{bmatrix} - & w^{[1]\top}_1 & - \\ - & w^{[1]\top}_2 & - \\ - & w^{[1]\top}_3 & - \\ - & w^{[1]\top}_4 & - \end{bmatrix}}_{W^{[1]}:\ (4,2)} \underbrace{\begin{bmatrix} x_1 \\ x_2 \end{bmatrix}}_{x:\ (2,1)} + \underbrace{\begin{bmatrix} b^{[1]}_1 \\ b^{[1]}_2 \\ b^{[1]}_3 \\ b^{[1]}_4 \end{bmatrix}}_{b^{[1]}:\ (4,1)}

于是一个样本的前向传播只要四行:

z[1]=W[1]x+b[1],a[1]=g[1](z[1]),z[2]=W[2]a[1]+b[2],a[2]=σ(z[2])z^{[1]} = W^{[1]} x + b^{[1]},\quad a^{[1]} = g^{[1]}(z^{[1]}),\quad z^{[2]} = W^{[2]} a^{[1]} + b^{[2]},\quad a^{[2]} = \sigma(z^{[2]})

后两行就是”以 a[1]a^{[1]} 为输入的逻辑回归”。所以一个两层网络 = 4 个并排的逻辑回归(隐藏层)+ 一个以它们的输出为输入的逻辑回归(输出层)。区别只在于:隐藏层的 4 个”逻辑回归”没有自己的标签,它们的参数由输出层的误差反向传回来决定(§6)。

代码 01 演示 3 的函数 forward_loop_units 就是照这个”每个圆圈做两件事”的方式,用两重 for 循环一个单元、一个样本地算,结果和矩阵写法一样(§3.4)。

2.3 形状

代码 01 演示 3 打印了花朵数据上(nx=2n_x = 2、nh=4n_h = 4、m=400m = 400)每个量的形状:

量实际形状一般形状说明
X=A[0]X = A^{[0]}(2, 400)(nx,m)(n_x, m)每列一个样本
W[1]W^{[1]}(4, 2)(n[1],nx)(n^{[1]}, n_x)第 ii 行 = 第 ii 个隐藏单元的 wi⊤w^\top_i
b[1]b^{[1]}(4, 1)(n[1],1)(n^{[1]}, 1)广播到 mm 列
Z[1],A[1]Z^{[1]}, A^{[1]}(4, 400)(n[1],m)(n^{[1]}, m)行 = 隐藏单元,列 = 样本
W[2]W^{[2]}(1, 4)(n[2],n[1])(n^{[2]}, n^{[1]})输出单元对 4 个隐藏单元的权重
b[2]b^{[2]}(1, 1)(n[2],1)(n^{[2]}, 1)
Z[2],A[2]Z^{[2]}, A^{[2]}(1, 400)(n[2],m)(n^{[2]}, m)每个样本一个概率

规律只有一条:W[l]W^{[l]} 的形状是(本层单元数, 上一层单元数)。参数个数是 nhnx+nh+nh+1n_h n_x + n_h + n_h + 1,nh=4n_h = 4 时 8+4+4+1=178 + 4 + 4 + 1 = 17。

2.4 每个隐藏单元在做什么

隐藏单元算的是 tanh⁡(w1x1+w2x2+b)\tanh(w_1 x_1 + w_2 x_2 + b):在直线 w1x1+w2x2+b=0w_1x_1 + w_2x_2 + b = 0 的一侧接近 +1,另一侧接近 −1,越过直线时平滑地翻转。可以把它想成一个**“软开关”**。∥w∥\|w\| 越大,翻转得越陡,越接近一个硬开关。

代码 01 演示 4 把训练好的 4 个单元逐个拿出来看:

单元直线 w1x1+w2x2+b=0w_1x_1 + w_2x_2 + b = 0离原点输出权重 W[2]W^{[2]}∣a∣>0.9\lvert a\rvert > 0.9 的样本
#0−11.14 x1−14.20 x2+0.19=0-11.14\,x_1 - 14.20\,x_2 + 0.19 = 00.017.7092.8%
#1+10.30 x1−11.70 x2−0.08=0+10.30\,x_1 - 11.70\,x_2 - 0.08 = 00.005.5993.5%
#2+0.28 x1+7.88 x2+0.01=0+0.28\,x_1 + 7.88\,x_2 + 0.01 = 00.0011.8890.0%
#3+0.07 x1+0.16 x2−0.48=0+0.07\,x_1 + 0.16\,x_2 - 0.48 = 02.72−3.330.0%
单元 #0:a > 0 处为 '.'  单元 #1:a > 0 处为 '.'  单元 #2:a > 0 处为 '.'  单元 #3:a > 0 处为 '.'
+--------------------+  +--------------------+  +--------------------+  +--------------------+
|                    |  |                    |  |....................|  |    ................|
|.                   |  |                 ...|  |....................|  |         ...........|
|....                |  |               .....|  |....................|  |              ......|
|......              |  |             .......|  |....................|  |                  ..|
|.........           |  |           .........|  |....................|  |                    |
|...........         |  |         ...........|  |                    |  |                    |
|..............      |  |      ..............|  |                    |  |                    |
|................    |  |    ................|  |                    |  |                    |
|................... |  |  ..................|  |                    |  |                    |
|....................|  |....................|  |                    |  |                    |
+--------------------+  +--------------------+  +--------------------+  +--------------------+

这张表比准确率更能说明网络学到了什么:

  1. 3 个单元(#0、#1、#2)是几乎穿过原点的直线,权重很大,90% 以上的样本上”开关拨到了底”。花朵的花瓣从中心辐射出去,有用的分界线自然也从中心出发。
  2. 3 条过原点的直线把平面切成 6 个扇形,正好对应 §1.2 说的”绕一圈颜色变 6 次”。
  3. 单元 #3 几乎没被用上:权重小得多、从不饱和,在整个数据范围内输出只在 −0.84 到 0.22 之间平缓变化,是一个”斜坡”而不是”开关”。4 个单元里有 1 个是多余的。这预告了 §8 的结果:nh=3n_h = 3 就够了。
  4. 输出层在”投票”:y^=σ(7.70 a0+5.59 a1+11.88 a2−3.33 a3−1.22)\hat y = \sigma(7.70\,a_0 + 5.59\,a_1 + 11.88\,a_2 - 3.33\,a_3 - 1.22)。每跨过一条直线,就有一个开关从 −1 翻到 +1(或反过来),投票结果随之改变,于是拼出了扇形相间的边界。

没有人告诉网络”要用过原点的直线”或者”要切成 6 个扇形”。这是梯度下降为了降低 cost 自己找到的中间特征,和第 1 讲 §2.3 里网络自己学出 walkability 是同一件事,只不过这里能直接画出来看。


3. 向量化:从一个样本到 m 个样本 · Vectorizing across examples

3.1 对样本循环

§2.2 的四行公式只处理一个样本。mm 个样本最直接的写法是循环:

for i = 1 to m:
    z[1](i) = W[1] x(i) + b[1]
    a[1](i) = g(z[1](i))
    z[2](i) = W[2] a[1](i) + b[2]
    a[2](i) = σ(z[2](i))

第 1 讲 §4.4 和第 2 讲都讲过,Python 循环比矩阵运算慢几百到几千倍。我们想要一次算完所有样本。

3.2 把样本横着堆起来

把 mm 个样本的列向量横着排成矩阵(第 1 讲的约定),中间结果也一样横着排:

X=[x(1) x(2)⋯x(m)],Z[1]=[z[1](1) z[1](2)⋯z[1](m)],A[1]=[a[1](1) ⋯ a[1](m)]X = \big[x^{(1)}\ x^{(2)} \cdots x^{(m)}\big],\quad Z^{[1]} = \big[z^{[1]{}(1)}\ z^{[1]{}(2)} \cdots z^{[1]{}(m)}\big],\quad A^{[1]} = \big[a^{[1]{}(1)}\ \cdots\ a^{[1]{}(m)}\big]

于是四行公式变成:

 Z[1]=W[1]X+b[1],A[1]=g[1](Z[1]),Z[2]=W[2]A[1]+b[2],A[2]=σ(Z[2]) \boxed{\ Z^{[1]} = W^{[1]} X + b^{[1]},\qquad A^{[1]} = g^{[1]}(Z^{[1]}),\qquad Z^{[2]} = W^{[2]} A^{[1]} + b^{[2]},\qquad A^{[2]} = \sigma(Z^{[2]})\ }

和单个样本的公式一模一样,只是小写换成了大写。在矩阵 Z[1]Z^{[1]}、A[1]A^{[1]} 里:

  • 横着走(换列)= 换样本:第 jj 列是第 jj 个样本在所有隐藏单元上的值;
  • 竖着走(换行)= 换隐藏单元:第 ii 行是第 ii 个隐藏单元对所有样本的输出。

代码 01 演示 3 打印了一个例子:A[1]A^{[1]} 的第 7 列是 [−1,−1,1,0.1309][-1, -1, 1, 0.1309],即第 7 个样本在 4 个隐藏单元上的输出(竖着读);第 0 行的前 5 个是 [−1,−1,−1,1,−1][-1, -1, -1, 1, -1],即第 0 个单元对前 5 个样本的输出(横着读)。

3.3 为什么公式不变?

这是 Ng 的视频 “Explanation for Vectorized Implementation” 要回答的问题。为了简单,先假设 b[1]=0b^{[1]} = 0。矩阵乘法可以按列拆开看:

W[1]X=W[1][x(1) x(2)⋯x(m)]=[W[1]x(1)  W[1]x(2)⋯W[1]x(m)]=[z[1](1) ⋯ z[1](m)]=Z[1]W^{[1]} X = W^{[1]} \big[x^{(1)}\ x^{(2)} \cdots x^{(m)}\big] = \big[W^{[1]}x^{(1)}\ \ W^{[1]}x^{(2)} \cdots W^{[1]}x^{(m)}\big] = \big[z^{[1]{}(1)}\ \cdots\ z^{[1]{}(m)}\big] = Z^{[1]}

W[1]XW^{[1]}X 的第 jj 列只和 XX 的第 jj 列有关,恰好就是第 jj 个样本的 z[1](j)z^{[1]{}(j)}。再加上 b[1]b^{[1]}:它的形状是 (nh,1)(n_h, 1),加到 (nh,m)(n_h, m) 的矩阵上时,numpy 把它复制到每一列(广播),等于给每个样本都加上同一个 b[1]b^{[1]}。激活函数是逐元素的,不在乎有几列。第二层同理。

所以向量化不是近似,它和逐个样本算是同一个计算,只是把循环交给了 numpy 底层的线性代数库。

3.4 代码验证

代码 01 演示 3 用三种写法算同一个 A[2]A^{[2]}:

写法和矩阵写法的最大差别
(a) 对样本循环 × 对隐藏单元循环(两重 for)4.4e−16
(b) 对样本循环,单元用矩阵 W[1]x(j)+b[1]W^{[1]}x^{(j)} + b^{[1]}4.4e−16
(c) 一次矩阵乘法 W[1]X+b[1]W^{[1]}X + b^{[1]}—

4.4e−16 是浮点数的舍入误差(加法的顺序不同,最后一位可能不同),数学上三者完全相等。


4. 激活函数 · Activation functions

到目前为止隐藏层用的都是 tanh。为什么不用 sigmoid?为什么现代网络大多用 ReLU?输出层又该用什么?

4.1 四个候选

名字公式输出范围一句话
sigmoidσ(z)=11+e−z\sigma(z) = \dfrac{1}{1 + e^{-z}}(0, 1)可以解释成概率;二分类输出层的标准选择
tanhtanh⁡(z)=ez−e−zez+e−z\tanh(z) = \dfrac{e^z - e^{-z}}{e^z + e^{-z}}(−1, 1)sigmoid 平移、拉伸后的版本,以 0 为中心
ReLUmax⁡(0,z)\max(0, z)[0, ∞)正半轴是恒等函数,负半轴是 0;现代网络的默认选择
Leaky ReLUmax⁡(0.01z, z)\max(0.01z,\ z)(−∞, ∞)负半轴留一个很小的斜率

四个激活函数(实线)和它们的导数(虚线)

(这张图由 03_activation_functions.py 生成;没装 matplotlib 时脚本只打印下面的表。)

代码 03 演示 1 在一排 zz 上算出了取值和导数(节选):

zz−4−1014
σ(z)\sigma(z)0.0180.2690.5000.7310.982
σ′(z)\sigma'(z)0.0180.1970.2500.1970.018
tanh⁡(z)\tanh(z)−0.999−0.7620.0000.7620.999
tanh⁡′(z)\tanh'(z)0.0010.4201.0000.4200.001
ReLU(z)(z)0.0000.0000.0001.0004.000
ReLU′(z)'(z)0.0000.0000.0001.0001.000
Leaky ReLU′(z)'(z)0.0100.0100.0101.0001.000

4.2 导数的推导 · Derivatives of activation functions

反向传播要用到 g′(z)g'(z)(§6),所以先把四个导数推出来。

sigmoid。σ(z)=(1+e−z)−1\sigma(z) = (1 + e^{-z})^{-1},用链式法则:

σ′(z)=−(1+e−z)−2⋅(−e−z)=11+e−z⋅e−z1+e−z=σ(z)(1−σ(z))\sigma'(z) = -(1 + e^{-z})^{-2} \cdot (-e^{-z}) = \frac{1}{1 + e^{-z}} \cdot \frac{e^{-z}}{1 + e^{-z}} = \sigma(z)\big(1 - \sigma(z)\big)

最后一步用了 e−z1+e−z=1−11+e−z\frac{e^{-z}}{1 + e^{-z}} = 1 - \frac{1}{1 + e^{-z}}。记 a=σ(z)a = \sigma(z),则 σ′(z)=a(1−a)\sigma'(z) = a(1 - a)。a(1−a)a(1-a) 在 a=1/2a = 1/2(即 z=0z = 0)时最大,等于 1/4。

tanh。用商的求导法则,记 u=ez−e−zu = e^z - e^{-z}、v=ez+e−zv = e^z + e^{-z},注意 u′=vu' = v、v′=uv' = u:

tanh⁡′(z)=u′v−uv′v2=v2−u2v2=1−tanh⁡2(z)\tanh'(z) = \frac{u'v - uv'}{v^2} = \frac{v^2 - u^2}{v^2} = 1 - \tanh^2(z)

记 a=tanh⁡(z)a = \tanh(z),则 tanh⁡′(z)=1−a2\tanh'(z) = 1 - a^2,在 z=0z = 0 处最大,等于 1。

ReLU:z>0z > 0 时导数是 1,z<0z < 0 时是 0。Leaky ReLU:z>0z > 0 时是 1,z<0z < 0 时是 0.01。

两个实现细节:

  • sigmoid 和 tanh 的导数都能用前向时已经算好的 aa 表示。反向传播时不用再算一次指数,直接用缓存里的 A[1]A^{[1]}:dZ1 = dA1 * (1 - A1**2)。这也是它们好用的原因之一。
  • ReLU 在 z=0z = 0 处严格来说不可导(左导数 0、右导数 1)。实践中随便约定一边就行(代码里归到 0),因为浮点数恰好等于 0 的概率几乎为零,对训练没有影响。数学上这叫取一个”次梯度”(subgradient)。

代码 03 演示 1 用数值导数 [g(z+h)−g(z−h)]/(2h)[g(z+h) - g(z-h)]/(2h) 在 [−6,6][-6, 6] 上核对了这四个公式,最大误差分别是 8.5e−11、7.4e−11、1.4e−10、1.4e−10(跳过了 z=0z = 0)。

4.3 为什么隐藏层里 tanh 通常比 sigmoid 好

Ng 在视频里说得很直接:隐藏层几乎总是用 tanh 比 sigmoid 好,sigmoid 只留给二分类的输出层。理由有两个,第三点是一个常被忽略的事实。

① 导数大 4 倍。 σ′\sigma' 最大 0.25,tanh⁡′\tanh' 最大 1。反向传播时梯度要乘上 g′(z)g'(z)(§6.2),用 sigmoid 时每穿过一层至少打 4 折。第 1 讲代码 04 已经看到,在 5 层 sigmoid 网络里这会让第 1 层的梯度只剩输出层的约 1/2000。

② 以 0 为中心(zero-centered)。 当 zz 来自标准正态分布时,代码 03 演示 1 算出的平均输出是:tanh 0.000,sigmoid 0.500,ReLU 0.400,Leaky ReLU 0.396。为什么”平均值在 0 附近”重要?看下一层的权重梯度。单个样本对 W[2]W^{[2]} 的梯度是

dW[2](i)=dz[2](i)⋅a[1](i)⊤dW^{[2]{}(i)} = dz^{[2]{}(i)} \cdot a^{[1]{}(i)\top}

dz[2](i)dz^{[2]{}(i)} 是一个数,a[1](i)a^{[1]{}(i)} 是一个向量。如果 a[1]a^{[1]} 的分量全是正的(sigmoid 的输出在 (0, 1)),那么这个样本给 W[2]W^{[2]} 的每个分量的梯度符号都一样:要么全部往上推,要么全部往下推。想让一个权重变大、另一个变小,就只能靠不同样本之间互相抵消,走”之”字形的路线。这个论证最早出自 LeCun 等人的 Efficient BackProp(1998)。代码 03 演示 1 在花朵数据上直接数了一下:

隐藏层激活一个样本给 W[2]W^{[2]} 的 4 个梯度分量全部同号的样本比例
sigmoid100.0%
tanh2.2%
ReLU100.0%
Leaky ReLU2.2%

(Leaky ReLU 的负输出 0.01z0.01z 虽然小,符号却是负的,所以”全部同号”的比例也低。但它的平均值是 0.396,离零中心还远。这个统计只看符号,不看大小。)

③ 两者能表示的函数完全一样。 有一个恒等式:

σ(z)=1+tanh⁡(z/2)2\sigma(z) = \frac{1 + \tanh(z/2)}{2}

所以任何 sigmoid 隐藏层的网络,都可以精确地改写成 tanh 隐藏层的网络:把 W[1],b[1],W[2]W^{[1]}, b^{[1]}, W^{[2]} 减半,b[2]b^{[2]} 加上 ∑jWj[2]/2\sum_j W^{[2]}_j / 2(练习 3)。代码 03 演示 3 末尾验证了,改写前后的输出最大差别是 2.2e−16。这说明 sigmoid 和 tanh 的差别不在”能表示什么”,而在”梯度下降走得顺不顺”。代码 03 演示 3 的实验(§4.6)也是这样:两者最终准确率几乎一样,tanh 只是快得多。

4.4 为什么 ReLU 成了默认选择

sigmoid 和 tanh 有一个共同的毛病:饱和(saturation)。代码 03 演示 1:∣z∣>4.59|z| > 4.59 时 σ′(z)<0.01\sigma'(z) < 0.01,∣z∣>3.00|z| > 3.00 时 tanh⁡′(z)<0.01\tanh'(z) < 0.01。zz 一旦跑到这些区域,“推一下几乎没反应”,梯度传不回去,学习就停滞了。

ReLU 在 z>0z > 0 时导数恒等于 1,不会饱和。它的好处:

  1. 梯度不衰减:正半轴上梯度原样通过,深层网络里不会一层层被打折(第 1 讲代码 04:ReLU 深网在很宽的学习率范围内 100–200 步就学会了,sigmoid 深网只在很窄的范围内能训练);
  2. 计算便宜:一个比较和一次取最大值,没有指数运算;
  3. 稀疏:负半轴的输出恰好是 0,一部分单元对某个样本”完全不激活”。

它也有缺点:

  • dead ReLU:如果某个单元对所有训练样本都有 z<0z < 0,它的梯度永远是 0,再也学不动了(第 1 讲练习 1(d) 已经见过这个现象)。学习率太大或者初始化不好时容易出现。Leaky ReLU 在负半轴保留 0.01 的斜率,就是为了让这些单元还能”活过来”。它由 Maas、Hannun 和 Ng(2013)在语音识别网络里提出。
  • 不是零中心的:§4.3 的”全部同号”问题它也有(100%),但它照样是最常用的激活函数。可见零中心是一个加分项,不是决定性的。

历史上,ReLU 在 2010–2012 年间流行起来:Nair & Hinton(2010)、Glorot、Bordes & Bengio(2011)展示了它在深层网络里好训练;AlexNet(2012)报告用 ReLU 的卷积网络在 CIFAR-10 上达到 25% 训练误差的速度是 tanh 的约 6 倍。今天的 Transformer 大多用 ReLU 的平滑变体,比如 GELU(BERT、GPT 系列)或 SwiGLU(LLaMA 等)。

4.5 输出层用什么

输出层的激活函数由输出 yy 是什么决定,和隐藏层的选择是两回事:

任务输出层激活例子
二分类,y∈{0,1}y \in \{0, 1\}sigmoid猫 / 非猫、本讲的花朵
回归,yy 可正可负线性(g(z)=zg(z) = z)预测温度变化
回归,y≥0y \ge 0线性或 ReLU房价(第 1 讲)
多分类softmax第 9 讲

这几乎是唯一应该用线性激活的地方。隐藏层用线性激活会怎样?那是 §5 的主题。

4.6 实验:同一个任务上,谁学得快?

代码 03 演示 3 用五种隐藏层激活,在三个数据集(花朵;§8 会用到的同心圆、两个月牙)上各训练一个 [2, 20, 1] 网络。学习率 1.2,10000 步,每种组合 3 个初始化种子(2、3、4)。“达到门槛”指训练准确率第一次达到:花朵 85%、同心圆 75%、月牙 95%(明显好于逻辑回归、接近该数据集能达到的水平)。

(a) 作业的设定:W=0.01×W = 0.01 \times 随机数

达到门槛的步数(3 个种子)花朵(≥85%)同心圆(≥75%)月牙(≥95%)
linear—/—/——/—/——/—/—
sigmoid300/400/4004500/4500/4400—/—/9200
tanh100/100/1001100/1000/10001800/1800/1700
ReLU600/400/—200/100/200—/—/—
Leaky ReLU400/300/—200/100/200—/—/—
最终测试准确率(3 个种子平均,括号里是范围)花朵同心圆月牙
linear0.5010.4830.868
sigmoid0.908(0.902–0.911)0.7830.906(0.879–0.961)
tanh0.902(0.896–0.907)0.7830.963
ReLU0.820(0.783–0.855)0.7890.882
Leaky ReLU0.808(0.765–0.845)0.7890.882

(b) 只换初始化:WW 的标准差改成 1(对 2 个输入的 ReLU 层,这正好是第 7 讲 He 初始化给出的 2/nx\sqrt{2/n_x})

花朵同心圆月牙
ReLU:步数 / 测试准确率100/200/200,0.835100/100/100,0.766100/100/100,0.963
Leaky ReLU100/100/200,0.837100/100/100,0.767100/100/100,0.960
tanh100/100/100,0.895100/100/100,0.763100/100/100,0.964

读这两张表时,我原本预想的是”ReLU 最快、准确率不输 tanh”。实际结果更复杂:

  1. linear 永远达不到门槛,三个数据集上都等于逻辑回归(§5)。非线性是必需品。
  2. sigmoid vs tanh:最终准确率几乎一样(花朵 0.908 vs 0.902),这和 §4.3 的第 ③ 点吻合:两者能表示的函数相同。差别在速度:同样的学习率下,tanh 达到门槛的步数少得多(花朵中位数 100 vs 400,同心圆 1000 vs 4500)。sigmoid 在月牙上还有 2 个种子停在了 0.88 左右的差解上。
  3. ReLU / Leaky ReLU 的表现取决于数据集和初始化:同心圆上它们最快(200 步)。但在作业的 0.01 初始化下,花朵和月牙上它们停在明显更差的解(测试 0.820、0.882,tanh 是 0.902、0.963)。换成更大的初始化(表 b)后,月牙上 ReLU 追平了 tanh(0.963 vs 0.964),花朵上仍然落后(0.835 vs 0.895)。我在写代码的原型阶段还试过学习率 0.1 和 0.3、训练 30000 步,花朵上 ReLU 也一直落后。

为什么花朵上 tanh 更好?一个可能的解释(我没有专门做实验验证):§2.4 看到,花朵需要的是几条过原点的”开关”,每个开关的输出和点离原点多远无关。tanh 单元饱和以后正好是这样的有界开关;ReLU 单元却是一个随距离线性增长的”斜坡”,离中心越远输出越大,要拼出”扇形”就别扭得多。初始化太小对 ReLU 尤其不利,这一点第 7 讲会从”信号的方差”角度解释。

所以”隐藏层默认用 ReLU”是针对深层网络的经验法则:它的主要优势是梯度穿过很多层时不衰减、计算便宜,这些在只有一个隐藏层、输入只有 2 维的小问题上都不重要。Ng 自己的建议也不是”永远用 ReLU”,而是:拿不准的时候,几种都试一试,在验证集上比较(第 6 讲讲怎样建立验证集)。

4.7 选择建议

位置建议理由
隐藏层(默认)ReLU不饱和、便宜,深层网络好训练
隐藏层(ReLU 出现很多 dead unit 时)Leaky ReLU负半轴也有梯度
隐藏层(浅层网络、小问题)tanh 也值得一试本讲的花朵、月牙上 tanh 更好(§4.6)
隐藏层几乎不用 sigmoid导数小、不以 0 为中心;能表示的函数和 tanh 一样,却学得更慢
隐藏层不用线性等于没有这一层(§5)
输出层由 yy 决定二分类 sigmoid、回归线性、多分类 softmax(§4.5)

5. 为什么必须有非线性 · Why non-linear activations?

5.1 线性的叠加还是线性

第 1 讲 §2.4 用一行公式说明过:两层没有激活函数的线性变换合起来还是一个线性变换。现在把它说完整。设隐藏层用线性激活 g[1](z)=zg^{[1]}(z) = z:

a[1]=W[1]x+b[1],z[2]=W[2]a[1]+b[2]=W[2]W[1]⏟W′ x+W[2]b[1]+b[2]⏟b′a^{[1]} = W^{[1]}x + b^{[1]},\qquad z^{[2]} = W^{[2]}a^{[1]} + b^{[2]} = \underbrace{W^{[2]}W^{[1]}}_{W'}\,x + \underbrace{W^{[2]}b^{[1]} + b^{[2]}}_{b'}

W′W' 的形状是 (1,nh)(nh,nx)=(1,nx)(1, n_h)(n_h, n_x) = (1, n_x),和逻辑回归的 w⊤w^\top 一样。所以

y^=σ(W′x+b′)\hat y = \sigma(W'x + b')

这就是一个逻辑回归。不管 nhn_h 是 4 还是 4000,不管叠多少个线性层(用归纳法:每多一层,就多乘一个矩阵、多加一个向量,结果仍是”矩阵 × x + 向量”),整个网络能表示的函数都和一个线性层(再加一个输出激活)完全相同。隐藏层的意义就在于它的非线性。

5.2 实验:线性激活的两层网络 = 逻辑回归

代码 03 演示 2 把隐藏层激活换成 g(z)=zg(z) = z,用和 §1.3 一样的设定训练:

模型参数个数训练准确率测试准确率最终 cost测试集上和逻辑回归预测相同
逻辑回归 [2, 1]30.5100.5010.6571100.0%
线性激活 [2, 4, 1]170.5100.5010.6571100.0%
线性激活 [2, 50, 1]2010.5100.5010.6571100.0%

再把训练好的 [2, 4, 1] “压扁”:

  • 网络:W′=W[2]W[1]=[0.049,−0.267]W' = W^{[2]}W^{[1]} = [0.049, -0.267],b′=W[2]b[1]+b[2]=−0.010b' = W^{[2]}b^{[1]} + b^{[2]} = -0.010;
  • 单独训练的逻辑回归:w⊤=[0.049,−0.267]w^\top = [0.049, -0.267],b=−0.010b = -0.010;
  • 网络输出 A[2]A^{[2]} 和 σ(W′x+b′)\sigma(W'x + b') 在测试集上的最大差别:1.1e−16。

线性激活的网络不只是”准确率和逻辑回归差不多”:它的决策边界就是逻辑回归那条直线(代码里的字符画两张图都是 §1.3 左边那张),参数压扁以后和逻辑回归在 3 位小数上相同。[2, 50, 1] 多出来的 198 个参数一个都没用上,只是把同一个线性函数用更啰嗦的方式写了一遍。

我原本以为,换成"W[2]W[1]W^{[2]}W^{[1]}"这种乘积形式的参数化以后(它对参数不再是凸的),梯度下降可能停在别的地方。实际上它找到的正是逻辑回归的那个最优解。

5.3 线性激活什么时候可以用

  • 回归任务的输出层(§4.5):输出本来就是一个实数,不需要再压缩。
  • 隐藏层几乎从不用线性激活。偶尔会看到不带激活函数的线性层,比如把向量投影到另一个维度(第 17 讲 Transformer 里的 Q,K,VQ, K, V 投影),但它们前后总有别的非线性(比如 softmax 注意力),整体仍是非线性的。

6. 梯度下降与反向传播 · Gradient descent & backpropagation

这是本讲的核心:两层网络的梯度到底怎么算。

6.1 要算什么

参数:W[1] (nh,nx)W^{[1]}\ (n_h, n_x),b[1] (nh,1)b^{[1]}\ (n_h, 1),W[2] (1,nh)W^{[2]}\ (1, n_h),b[2] (1,1)b^{[2]}\ (1, 1)。Cost:

J(W[1],b[1],W[2],b[2])=1m∑i=1mL(a[2](i),y(i)),L(a,y)=−ylog⁡a−(1−y)log⁡(1−a)J(W^{[1]}, b^{[1]}, W^{[2]}, b^{[2]}) = \frac{1}{m}\sum_{i=1}^m \mathcal{L}\big(a^{[2]{}(i)}, y^{(i)}\big),\qquad \mathcal{L}(a, y) = -y\log a - (1-y)\log(1-a)

梯度下降(Ng 的 “Gradient Descent for Neural Networks”):

随机初始化参数(§7)
重复:
    前向传播:算出所有样本的预测 A[2]
    反向传播:算出 dW[1], db[1], dW[2], db[2]
    更新:W[1] ← W[1] − α dW[1],b[1] ← b[1] − α db[1],W[2]、b[2] 同理

前向传播 §3 已经有了。下面推导反向传播:先推一个样本的,再向量化。

6.2 一个样本的推导

计算图(第 2 讲的工具):

x ──┐
W[1]─┼─→ z[1] = W[1]x + b[1] ─→ a[1] = g(z[1]) ──┐
b[1]─┘                                          W[2]─┼─→ z[2] = W[2]a[1] + b[2] ─→ a[2] = σ(z[2]) ─→ L(a[2], y)
                                                b[2]─┘
     ←─────────── 反向传播:从右往左,每一步乘上"局部导数" ───────────

从最右边往左走,每一步只用链式法则:

第 ① 步:dz[2]dz^{[2]}。 第 2 讲推导过 sigmoid + 交叉熵的组合:∂L∂a=−ya+1−y1−a\frac{\partial \mathcal L}{\partial a} = -\frac{y}{a} + \frac{1-y}{1-a},乘上 σ′(z)=a(1−a)\sigma'(z) = a(1-a),化简后

dz[2]=a[2]−ydz^{[2]} = a^{[2]} - y

第 ② 步:dW[2]dW^{[2]}、db[2]db^{[2]}。 z[2]=∑jWj[2] aj[1]+b[2]z^{[2]} = \sum_j W^{[2]}_{j}\, a^{[1]}_j + b^{[2]}(W[2]W^{[2]} 只有一行,Wj[2]W^{[2]}_j 是它的第 jj 个元素)。z[2]z^{[2]} 对 Wj[2]W^{[2]}_j 的偏导是 aj[1]a^{[1]}_j,所以

dWj[2]=dz[2] aj[1]⟹dW[2]=dz[2] a[1]⊤(1,nh),db[2]=dz[2]dW^{[2]}_j = dz^{[2]}\, a^{[1]}_j \quad\Longrightarrow\quad dW^{[2]} = dz^{[2]}\, a^{[1]\top} \quad (1, n_h),\qquad db^{[2]} = dz^{[2]}

和逻辑回归的 dw=x dzdw = x\,dz 完全一样,只是”输入”换成了 a[1]a^{[1]}。(W[2]W^{[2]} 是行向量,所以写成 dz a⊤dz\,a^\top 而不是 a dza\,dz。)

第 ③ 步:把误差传回隐藏层,da[1]da^{[1]}。 z[2]z^{[2]} 对 aj[1]a^{[1]}_j 的偏导是 Wj[2]W^{[2]}_j,所以

daj[1]=Wj[2] dz[2]⟹da[1]=W[2]⊤dz[2](nh,1)da^{[1]}_j = W^{[2]}_j\, dz^{[2]} \quad\Longrightarrow\quad da^{[1]} = W^{[2]\top} dz^{[2]} \quad (n_h, 1)

这一步是反向传播和逻辑回归真正不同的地方:输出的误差按输出权重分配给每个隐藏单元。输出权重大的单元,对 z[2]z^{[2]} 的影响大,分到的”责任”也大;输出权重是负的单元,被要求往反方向变。

第 ④ 步:穿过激活函数,dz[1]dz^{[1]}。 aj[1]=g(zj[1])a^{[1]}_j = g(z^{[1]}_j),每个单元只依赖自己的 zz,所以是逐元素相乘:

dz[1]=W[2]⊤dz[2]∗g[1]′(z[1])(nh,1)dz^{[1]} = W^{[2]\top} dz^{[2]} * g^{[1]\prime}(z^{[1]}) \quad (n_h, 1)

g′g' 在这里起”阀门”的作用:tanh 单元饱和时 g′≈0g' \approx 0,不管输出层传回多大的误差,这个单元都几乎收不到梯度。§4.4 的”饱和”、§7.4 的”初始化太大”,问题都出在这里。

第 ⑤ 步:dW[1]dW^{[1]}、db[1]db^{[1]}。 和第 ② 步一样,只是输入换成 xx:

dW[1]=dz[1] x⊤(nh,nx),db[1]=dz[1]dW^{[1]} = dz^{[1]}\, x^\top \quad (n_h, n_x),\qquad db^{[1]} = dz^{[1]}

这一步是一个外积:dWjk[1]=dzj[1]⋅xkdW^{[1]}_{jk} = dz^{[1]}_j \cdot x_k,即”第 jj 个单元该改多少” × “第 kk 个输入有多大”。

6.3 向量化:m 个样本的 6 个公式

把样本按列堆起来,对 JJ(mm 个损失的平均)求导:

dZ[2]=A[2]−Y(1,m)(1)dW[2]=1m dZ[2]A[1]⊤(1,nh)(2)db[2]=1m np.sum(dZ[2],axis=1,keepdims=True)(1,1)(3)dZ[1]=W[2]⊤dZ[2]∗g[1]′(Z[1])(nh,m)(4)dW[1]=1m dZ[1]X⊤(nh,nx)(5)db[1]=1m np.sum(dZ[1],axis=1,keepdims=True)(nh,1)(6)\boxed{ \begin{aligned} dZ^{[2]} &= A^{[2]} - Y && (1, m) && (1)\\ dW^{[2]} &= \tfrac{1}{m}\, dZ^{[2]} A^{[1]\top} && (1, n_h) && (2)\\ db^{[2]} &= \tfrac{1}{m}\, \texttt{np.sum}(dZ^{[2]}, \texttt{axis=1}, \texttt{keepdims=True}) && (1, 1) && (3)\\ dZ^{[1]} &= W^{[2]\top} dZ^{[2]} * g^{[1]\prime}(Z^{[1]}) && (n_h, m) && (4)\\ dW^{[1]} &= \tfrac{1}{m}\, dZ^{[1]} X^\top && (n_h, n_x) && (5)\\ db^{[1]} &= \tfrac{1}{m}\, \texttt{np.sum}(dZ^{[1]}, \texttt{axis=1}, \texttt{keepdims=True}) && (n_h, 1) && (6) \end{aligned}}

shallow_nn.py 的 backward_propagation 就是这 6 行,旁边标着”公式 (1)–(6)“。几个值得想清楚的地方:

  • 1/m1/m 放在哪里? Ng 的约定是:dZdZ 存的是”每个样本自己的”∂L(i)/∂z(i)\partial\mathcal{L}^{(i)}/\partial z^{(i)},不带 1/m1/m;1/m1/m 只在算 dWdW、dbdb 时出现,因为 JJ 是 mm 个样本的平均。
  • 矩阵乘法自动完成了对样本的求和。 dW[1]dW^{[1]} 本该是 1m∑idz[1](i)x(i)⊤\frac1m\sum_i dz^{[1]{}(i)} x^{(i)\top}。按”列 × 行”展开矩阵乘法,dZ[1]X⊤=∑idZ:,i[1] X:,i⊤dZ^{[1]}X^\top = \sum_i dZ^{[1]}_{:,i}\, X_{:,i}^\top,正好就是这个求和。dbdb 没有可以乘的输入,所以显式地 np.sum(..., axis=1)。
  • keepdims=True 让 dbdb 保持 (n,1)(n, 1) 的二维形状。忘了它会怎样,见 §6.4。
  • 公式 (4) 里的 ∗* 是逐元素乘法:W[2]⊤dZ[2]W^{[2]\top}dZ^{[2]} 是 (nh,1)(1,m)=(nh,m)(n_h, 1)(1, m) = (n_h, m),g′(Z[1])g'(Z^{[1]}) 也是 (nh,m)(n_h, m),两者对应位置相乘。tanh 时 g′(Z[1])=1−A[1]2g'(Z^{[1]}) = 1 - A^{[1]2},用缓存里的 A[1]A^{[1]} 直接算。

把这 6 行和 §1.1 回顾的逻辑回归对比:公式 (1)–(3) 就是”输入为 A[1]A^{[1]} 的逻辑回归”;公式 (5)–(6) 就是”误差为 dZ[1]dZ^{[1]} 的逻辑回归”;新东西只有公式 (4),它把误差从输出层传回了隐藏层。第 4 讲会看到,LL 层网络无非是把公式 (4)–(6) 重复 L−1L-1 次。

6.4 维度检查 · Getting the dimensions right

Ng 在 “Backpropagation Intuition” 里说,实现反向传播时最容易出错的就是维度。规则只有两条:

dWdW 和 WW 同形状,dZdZ 和 ZZ 同形状(dbdb 和 bb 同形状)。

代码 02 演示 2 在花朵数据上逐个检查:

前向的量形状反向的量形状
W[1]W^{[1]}(4, 2)dW[1]dW^{[1]}(4, 2)
b[1]b^{[1]}(4, 1)db[1]db^{[1]}(4, 1)
Z[1]Z^{[1]}(4, 400)dZ[1]dZ^{[1]}(4, 400)
W[2]W^{[2]}(1, 4)dW[2]dW^{[2]}(1, 4)
b[2]b^{[2]}(1, 1)db[2]db^{[2]}(1, 1)
Z[2]Z^{[2]}(1, 400)dZ[2]dZ^{[2]}(1, 400)

形状还能告诉你转置放在哪里:dW[2]dW^{[2]} 必须是 (1,nh)(1, n_h),手里有 dZ[2] (1,m)dZ^{[2]}\ (1, m) 和 A[1] (nh,m)A^{[1]}\ (n_h, m),唯一能凑出 (1,nh)(1, n_h) 的乘法就是 dZ[2]A[1]⊤dZ^{[2]}A^{[1]\top}。

演示 2 还故意制造了两个形状 bug:

  1. 把公式 (4) 的 ∗* 写成矩阵乘法 @:(4,400)(4, 400) @ (4,400)(4, 400) 维度对不上,numpy 立刻报错。这是”幸运”的 bug。
  2. 求 db[1]db^{[1]} 时忘了 keepdims=True:db[1]db^{[1]} 变成形状 (4,)(4,) 的一维数组,b[1]−α db[1]b^{[1]} - \alpha\,db^{[1]} 被悄悄广播成 (4,4)(4, 4),更新这一步不报错。错误要到下一次前向传播 Z[1]=W[1]X+b[1]Z^{[1]} = W^{[1]}X + b^{[1]} 才暴露,而报错信息指向的是前向传播函数,离真正写错的那一行很远。更糟的是 b[2]b^{[2]} 碰巧没事((1,1)−(1,)=(1,1)(1,1) - (1,) = (1,1)),这种”有时对、有时错”的 bug 最难查。这是第 1 讲 §3.5 的 rank-1 数组陷阱在反向传播里的样子。

6.5 手算一遍

公式推完了,用一个能手算的例子把每个数都过一遍。代码 02 演示 1:2 个输入、2 个 tanh 隐藏单元、1 个样本。x=(1,0.5)⊤x = (1, 0.5)^\top,y=1y = 1,W[1]=[1−10.50.5]W^{[1]} = \begin{bmatrix} 1 & -1 \\ 0.5 & 0.5 \end{bmatrix},b[1]=0b^{[1]} = 0,W[2]=[1,−2]W^{[2]} = [1, -2],b[2]=0b^{[2]} = 0。(m=1m = 1,所以没有 1/m1/m。)

公式数值
前向z[1]=W[1]x+b[1]z^{[1]} = W^{[1]}x + b^{[1]}(0.5000, 0.7500)(0.5000,\ 0.7500)
a[1]=tanh⁡(z[1])a^{[1]} = \tanh(z^{[1]})(0.4621, 0.6351)(0.4621,\ 0.6351)
z[2]=W[2]a[1]+b[2]z^{[2]} = W^{[2]}a^{[1]} + b^{[2]}−0.8082-0.8082
a[2]=σ(z[2])a^{[2]} = \sigma(z^{[2]})0.30830.3083
L=−log⁡a[2]\mathcal{L} = -\log a^{[2]}1.17681.1768
反向dz[2]=a[2]−ydz^{[2]} = a^{[2]} - y−0.6917-0.6917
dW[2]=dz[2]a[1]⊤dW^{[2]} = dz^{[2]}a^{[1]\top}(−0.3197, −0.4393)(-0.3197,\ -0.4393)
da[1]=W[2]⊤dz[2]da^{[1]} = W^{[2]\top}dz^{[2]}(−0.6917, +1.3834)(-0.6917,\ +1.3834)
g′(z[1])=1−a[1]2g'(z^{[1]}) = 1 - a^{[1]2}(0.7864, 0.5966)(0.7864,\ 0.5966)
dz[1]=da[1]∗g′(z[1])dz^{[1]} = da^{[1]} * g'(z^{[1]})(−0.5440, +0.8253)(-0.5440,\ +0.8253)
dW[1]=dz[1]x⊤dW^{[1]} = dz^{[1]}x^\top[−0.5440−0.2720+0.8253+0.4127]\begin{bmatrix} -0.5440 & -0.2720 \\ +0.8253 & +0.4127 \end{bmatrix}

逐行读一下,这就是 “Backpropagation Intuition” 想传达的东西:

  • 网络预测 0.3083,真实答案是 1,预测偏低。dz[2]=−0.6917<0dz^{[2]} = -0.6917 < 0 表示”z[2]z^{[2]} 应该变大”(梯度下降沿负梯度方向走)。
  • da[1]da^{[1]} 把误差按输出权重分回去:单元 0 的输出权重是 +1,它被要求”变大”(−0.6917-0.6917);单元 1 的输出权重是 −2,它反而被要求”变小”(+1.3834+1.3834),而且责任是单元 0 的两倍。
  • 再乘上 g′g':单元 1 的 zz 更大、更接近饱和,斜率只有 0.5966(单元 0 是 0.7864),所以它分到的梯度被打了更大的折扣。
  • 数值导数逐项核对,四个参数的相对误差都不超过 4.4e−10。用学习率 0.5 走一步,y^\hat y 从 0.3083 升到 0.8311,损失从 1.1768 降到 0.1850,方向是对的。

6.6 梯度检验:点到为止 · Gradient checking (preview)

推导和代码都可能出错,而反向传播的 bug 往往不报错:网络照样训练,只是学得慢或者学得差。最可靠的检查办法是数值梯度:把某个参数推一下,看 JJ 变多少。

∂J∂θ≈J(θ+ε)−J(θ−ε)2ε,相对误差=∥gnum−gana∥∥gnum∥+∥gana∥\frac{\partial J}{\partial \theta} \approx \frac{J(\theta + \varepsilon) - J(\theta - \varepsilon)}{2\varepsilon},\qquad \text{相对误差} = \frac{\|g_\text{num} - g_\text{ana}\|}{\|g_\text{num}\| + \|g_\text{ana}\|}

用双边差分是因为它的误差是 O(ε2)O(\varepsilon^2),单边差分 [J(θ+ε)−J(θ)]/ε[J(\theta+\varepsilon) - J(\theta)]/\varepsilon 的误差是 O(ε)O(\varepsilon)。Ng 的经验标准:相对误差约 1e−7 很好;约 1e−5 大概没问题,但要再看看;约 1e−3 很可能有 bug。(第 7 讲会系统地讲梯度检验的用法和注意事项,比如不要和 dropout 一起用。)

代码 02 演示 3 用 gradient_check 检查了正确的实现和三个故意写错的版本:

版本刚初始化(W≈0.01W \approx 0.01)训练 1000 步之后训练后各参数的相对误差 W[1]/b[1]/W[2]/b[2]W^{[1]} / b^{[1]} / W^{[2]} / b^{[2]}
正确的版本1.4e−071.6e−094e−09 / 6e−10 / 9e−09 / 4e−11
A:dZ[1]dZ^{[1]} 忘了乘 g′(Z[1])g'(Z^{[1]})8.7e−049.1e−011e+00 / 1e+00 / 9e−09 / 4e−11
B:tanh′ 写成 1−Z[1]21 - Z^{[1]2}4.5e−061.0e+001e+00 / 1e+00 / 9e−09 / 4e−11
C:dW[1]dW^{[1]} 忘了除以 mm8.4e−017.8e−019e−01 / 6e−10 / 9e−09 / 4e−11

(五种激活函数的正确实现,训练后的相对误差在 1.6e−09 到 9.0e−09 之间。)

两个收获:

  1. 按参数分开算相对误差,能定位 bug。 三个 bug 的 W[2],b[2]W^{[2]}, b^{[2]} 梯度都是对的,错误只出现在 W[1]W^{[1]}(和 b[1]b^{[1]})上;bug C 只动了 dW[1]dW^{[1]},所以 b[1]b^{[1]} 那一项也是对的。
  2. 一个陷阱:在刚初始化的参数上,有的 bug 会躲过去。 bug B 的相对误差只有 4.5e−06,按经验标准会被当成”大概没问题”;bug A 是 8.7e−04,处在”值得担心”的边缘,也容易被忽视。原因是 W≈0.01W \approx 0.01 时 Z[1]≈0Z^{[1]} \approx 0,而 tanh⁡′(0)=1\tanh'(0) = 1、1−02=11 - 0^2 = 1:在 0 附近,漏乘 g′g'、把 AA 写成 ZZ 几乎没有区别。训练 1000 步、参数离开 0 附近之后,它们就原形毕露了。这正是 Ng 的建议:梯度检验在随机初始化后做一次,训练一段时间后再做一次。

6.7 和第 1 讲的黑盒对答案

代码 02 演示 4 把同一组参数分别交给本讲的 shallow_nn 和第 1 讲的 tiny_nn:前向输出完全相同(差别 0),四个梯度的最大差别是 0 到 2.8e−17。第 1 讲当作黑盒的 tiny_nn.backward,在一个隐藏层的情况下算的就是这 6 个公式,只是它用一个从最后一层往前走的 for 循环来处理任意层数(第 4 讲拆开这个循环)。


7. 随机初始化 · Random initialization

逻辑回归可以把 ww、bb 都初始化成 0(第 2 讲就是这样做的,代码里的 logistic_regression 也是),因为它只有一个单元。神经网络为什么不行?

7.1 对称性论证

假设把 W[1]W^{[1]} 和 W[2]W^{[2]} 都初始化成 0。更一般地,假设某一步时:

  • W[1]W^{[1]} 的各行相同、b[1]b^{[1]} 的各分量相同(所有隐藏单元的参数一样);
  • W[2]W^{[2]} 的各分量相同(输出层对所有隐藏单元一视同仁)。

那么:

  1. 对每个样本,所有隐藏单元的 zj[1]z^{[1]}_j 相同 → aj[1]a^{[1]}_j 相同,即 A[1]A^{[1]} 的各行相同;
  2. dW[2]=1mdZ[2]A[1]⊤dW^{[2]} = \frac1m dZ^{[2]}A^{[1]\top} 的各分量相同;
  3. W[2]⊤dZ[2]W^{[2]\top}dZ^{[2]} 的各行相同(W[2]W^{[2]} 各分量相同),g′(Z[1])g'(Z^{[1]}) 的各行相同 → dZ[1]dZ^{[1]} 的各行相同 → dW[1]dW^{[1]} 的各行相同、db[1]db^{[1]} 的各分量相同;
  4. 更新以后,上面两个条件仍然成立。

由归纳法,这些条件永远成立:nhn_h 个隐藏单元从头到尾计算同一个函数,等于只有 1 个隐藏单元,再大的隐藏层也是浪费。这叫对称性问题(symmetry problem)。随机初始化 WW 的作用就是打破对称(symmetry breaking):从第 0 步起,每个单元就和别的单元不同,梯度也不同,它们会朝不同的方向发展,学出不同的特征。

bb 可以初始化成 0:只要 W[1]W^{[1]} 的各行不同,各单元就已经不同了。

7.2 实验

代码 04 演示 1 在花朵数据上用 4 个隐藏单元、学习率 1.2、10000 步,比较四种初始化:

初始化第 0 步 ∥dW[1]∥\lVert dW^{[1]}\rVert第 0 步 ∥dW[2]∥\lVert dW^{[2]}\rVert训练后 W[1]W^{[1]} 各行最大差别训练准确率测试准确率
全 0(tanh)0.0e+000.0e+000.00e+000.5000.500
全 0(sigmoid)0.0e+000.0e+000.00e+000.5000.500
WW 全是 0.5,b=0b = 0(tanh)1.9e−016.4e−010.00e+000.6800.688
0.01×0.01 \times 随机数(tanh)3.3e−037.2e−032.21e+010.9400.906
对照:只有 1 个隐藏单元0.7130.668

每一行都值得看:

  • 全 0 + tanh:一步也不动。 tanh⁡(0)=0\tanh(0) = 0,所以 A[1]=0A^{[1]} = 0 → dW[2]=0dW^{[2]} = 0;又因为 W[2]=0W^{[2]} = 0 → dZ[1]=0dZ^{[1]} = 0 → dW[1]=0dW^{[1]} = 0。所有梯度恰好为 0,10000 步后所有参数仍是 0,网络永远输出 0.5。这是一个鞍点(saddle point):梯度为 0,但离最优解很远。

  • 全 0 + sigmoid:也一步不动,但原因不同。 σ(0)=0.5≠0\sigma(0) = 0.5 \ne 0,按理 W[2]W^{[2]} 能收到梯度 1mdZ[2]A[1]⊤=0.5⋅mean(A[2]−Y)\frac1m dZ^{[2]}A^{[1]\top} = 0.5 \cdot \text{mean}(A^{[2]} - Y)。但花朵数据两类各 200 个,第 0 步 mean(0.5−Y)=0.5−0.5=0\text{mean}(0.5 - Y) = 0.5 - 0.5 = 0,于是所有梯度又恰好为 0。(我原以为这里会看到”各行相同但不为 0”,实际结果是全卡死。类别不平衡时会怎样,见练习 6。)

  • WW 全是 0.5:能动,但 4 个单元永远一样。 训练 10000 步之后的 W[1]W^{[1]}:

                  W 全是 0.5                                  0.01 × 随机数
      单元 #0     [ +4.9984  +5.3033]  b=-0.2082              [-11.1400 -14.1951]  b=+0.1901
      单元 #1     [ +4.9984  +5.3033]  b=-0.2082              [+10.2964 -11.6952]  b=-0.0762
      单元 #2     [ +4.9984  +5.3033]  b=-0.2082              [ +0.2797  +7.8789]  b=+0.0062
      单元 #3     [ +4.9984  +5.3033]  b=-0.2082              [ +0.0676  +0.1623]  b=-0.4779
      W2          [-0.185 -0.185 -0.185 -0.185]               [ 7.6983  5.5939 11.877  -3.3257]

    左边 4 行逐位相同,W[2]W^{[2]} 的 4 个分量也相同。这个网络实际上只有 1 个隐藏单元,准确率 0.680 也和真正只有 1 个隐藏单元的网络(0.713)差不多。两者停在了不同的局部解,所以不完全相等。

  • 随机初始化:4 行各不相同,就是 §2.4 分析过的那个网络。

7.3 对称是一个”不稳定的平衡”

如果初始值几乎对称,只差一点点呢?代码 04 演示 2 在 ”WW 全是 0.5” 上加了 1e−8 或 1e−4 的随机扰动:

步数无扰动(精确对称)扰动 1e−08扰动 1e−04
00.0e+00(0.323)1.2e−08(0.323)1.2e−04(0.323)
1000.0e+00(0.675)3.0e−07(0.675)3.0e−03(0.675)
10000.0e+00(0.675)1.9e−03(0.675)6.1e+00(0.915)
30000.0e+00(0.680)4.1e+00(0.715)1.3e+01(0.932)
100000.0e+00(0.680)1.1e+01(0.730)2.2e+01(0.943)

(表中是 W[1]W^{[1]} 各行之间的最大差别,括号里是训练准确率。)

  • 精确对称时,差别永远是精确的 0。在计算机里,相同的输入做相同的浮点运算,结果逐位相同。§7.1 的归纳法在浮点数下也严格成立。
  • 一旦有一点点不同,差别会指数式地长大:1e−8 在 1000 步后变成 1.9e−03。对称点像一支立在笔尖上的铅笔:理论上能立住,稍有偏差就会倒。
  • 但”会倒”不等于”倒得好”:1e−8 的扰动花了几千步才把单元分开,最后只有 0.730;1e−4 的扰动 1000 步内就分开了,最后 0.943,和正常的随机初始化差不多。实践中没有人这样微调,直接用随机数初始化就行。

7.4 为什么初始权重要”小”

Ng 的作业用的是 W1 = np.random.randn(n_h, n_x) * 0.01。为什么乘 0.01,而不是 1 或 100?

如果 WW 很大,z=Wx+bz = Wx + b 就很大,tanh / sigmoid 一开始就饱和:g′(z)≈0g'(z) \approx 0,梯度传不过去(§6.2 第 ④ 步的”阀门”几乎关死)。代码 04 演示 3 把尺度从 0.001 扫到 100,每个尺度 10 个种子:

scale初始时 ∣a∣>0.99\lvert a\rvert > 0.99 的比例初始时平均 g′(z)g'(z)初始 cost10 次里达到 90% 训练准确率平均测试准确率
0.0010.0%1.0000.6910 / 100.910
0.010.0%0.9990.6910 / 100.911
0.10.0%0.9280.6910 / 100.908
130.3%0.3550.878 / 100.860
1086.1%0.0626.074 / 100.768
10098.1%0.00812.561 / 100.681
  • scale ≤ 0.1:几乎没有饱和,初始 cost ≈ ln 2(“什么都不确定”),10 次全部成功。对一个隐藏层的网络,0.001、0.01、0.1 差别很小,作业用 0.01 是合理的。
  • scale = 10、100:86%、98% 的(单元, 样本)组合一开始就饱和,平均斜率只有 0.062、0.008。初始 cost 高达 6.07、12.56:输出层的 sigmoid 也饱和了,网络一开始就”非常自信地错了”。10 次里只有 4 次、1 次能达到 90%,平均测试准确率掉到 0.681。
  • 机制:饱和的单元对绝大多数样本”开关已经拨到底”,只能从少数靠近它那条直线的样本里学习,很容易停在差的解上。注意大初始化不是每次都失败(scale = 10 时有 4 次成功),它是让训练变得不可靠。这在实践中同样糟糕:你没法分辨失败是因为想法不对,还是因为运气不好。

7.5 预告:深层网络需要”刚刚好”的尺度

既然小一点更好,是不是越小越好?对一个隐藏层,是的。但在很深的网络里,每一层都把信号乘上一个很小的数(W≈0.01W \approx 0.01),几十层之后前向的激活值和反向的梯度都会缩到几乎为 0;反过来,尺度太大又会一层层爆炸。第 7 讲会推导”刚刚好”的尺度:让每一层输出的方差保持不变,得到 Xavier 初始化(Var(W)=1/n[l−1]\text{Var}(W) = 1/n^{[l-1]},适合 tanh)和 He 初始化(Var(W)=2/n[l−1]\text{Var}(W) = 2/n^{[l-1]},适合 ReLU)。§4.6 的表 (b) 已经让你看到,换成 He 尺度后,ReLU 在月牙数据上从 0.882 升到了 0.963。


8. 隐藏层大小 · Hidden layer size

8.1 从 1 到 50 个隐藏单元

隐藏单元数 nhn_h 是我们自己选的。作业的最后一部分就是把 nhn_h 从 1 调到 50,看决策边界怎样变化。代码 05 演示 1(tanh,学习率 1.2,10000 步,种子 2 / 3 / 4):

模型参数训练准确率(3 个种子)测试准确率(3 个种子)测试平均训练 − 测试
逻辑回归30.51000.50100.50100.0090
nh=1n_h = 150.7125 / 0.7125 / 0.71250.6680 / 0.6680 / 0.66800.66800.0445
nh=2n_h = 290.6925 / 0.6925 / 0.69250.6610 / 0.6610 / 0.66100.66100.0315
nh=3n_h = 3130.9400 / 0.9400 / 0.94000.9075 / 0.9075 / 0.90750.90750.0325
nh=4n_h = 4170.9400 / 0.9400 / 0.94000.9065 / 0.9065 / 0.90700.90670.0333
nh=5n_h = 5210.9475 / 0.9400 / 0.94500.8985 / 0.9055 / 0.90700.90370.0405
nh=20n_h = 20810.9625 / 0.9650 / 0.96500.9020 / 0.8965 / 0.90750.90200.0622
nh=50n_h = 502010.9650 / 0.9750 / 0.96500.8890 / 0.8890 / 0.90000.89270.0757
Bayes 最优(§8.2)—0.93750.93750.93750.0000

花朵数据:隐藏单元数与决策边界

(这张图由 05_hidden_layer_size.py 生成,对应种子 2;终端里打印的是同样内容的字符画。)

从表和图里能读出四件事:

  1. nh=1,2n_h = 1, 2 不够:1 条或 2 条”软直线”最多把平面切成 2 块或 4 块,而花朵绕一圈要变 6 次颜色。它们的训练准确率 0.71、0.69,比逻辑回归好,但远远不够。
  2. nh=3n_h = 3 是一个台阶:训练准确率跳到 0.9400。3 条过原点的直线正好切出 6 个扇形(§2.4)。图里 nh=3n_h = 3 和 nh=4n_h = 4 的边界几乎一模一样,nh=4n_h = 4 多出的那个单元没被用上。(原作业里也是从 3 个单元开始跳到 90% 左右。)
  3. 之后测试准确率不再上升:nh=3n_h = 3 到 50 的测试平均都在 0.8927–0.9075 之间,差别和种子之间的波动差不多大。最好的反而是最小的 nh=3n_h = 3。
  4. 训练准确率却在继续上升:nh=20n_h = 20、50 到了 0.96–0.975,“训练 − 测试”的差距从 0.0325 扩大到 0.0757。图里 nh=20n_h = 20、50 的边界在花心附近和上方多出了几个细小的”尖刺”,是为了包住个别训练点画出来的。

注意第 1 点里 nh=2n_h = 2 比 nh=1n_h = 1 还略差(训练 0.6925 vs 0.7125):两个单元的网络停在了一个局部解上,并没有用好多出来的那条直线。小网络对初始化和优化都更敏感,第 1 讲 §2.2 的 [5, 2, 1] 也是这样。

8.2 一把尺子:Bayes 最优分类器

测试准确率停在 0.90 左右,是不是已经到顶了?真实任务里我们无法回答这个问题,但花朵数据是我们自己生成的,知道真相。

已知生成过程时,可以算出每个点属于每一类的真实概率 p(y=1∣x)p(y = 1 \mid x)。“哪一类概率大就猜哪一类”的分类器叫 Bayes 最优分类器,它的期望准确率是任何模型都不可能超过的上限(第 10 讲会把”上限对应的错误”叫做 Bayes error,真实任务里常用人类水平来近似它)。planar_data.py 的 class_likelihoods 推导了花朵的真实后验:给定一个点 xx,能生成它的只有 t=θ+kπt = \theta + k\pi、r=(−1)k∣x∣r = (-1)^k|x| 这几组角度和半径(θ\theta 是 xx 的极角),把它们的概率加起来即可。我用另一种方法(在几百万个样本上按极坐标分格、数每一格里哪一类多)独立核对过这个公式,两者给出的上限一致。

在 20000 个新样本上,花朵的 Bayes 上限约 0.944;在我们那 2000 个测试样本上是 0.9375。对照 §8.1 的表:

  • nh=3n_h = 3 的网络在训练集上已经和 Bayes 一样准(0.9400 vs 0.9375),到了测试集上却差了 0.0300(0.9075 vs 0.9375)。网络的边界是照着这 400 个点画的,换一批点就没那么准了。这段差距加隐藏单元补不上(nh=20n_h = 20、50 的测试反而更低),要靠更多数据或者正则化。第 6 讲把这种”训练好、测试差”叫做 variance(方差)。
  • nh=20n_h = 20、50 在训练集上比 Bayes 还准(高出 0.0267、0.0308)。比”上帝视角”还准,只能说明它们在拟合这 400 个点里的噪声。

图的最后一张是 Bayes 最优分类器的边界:6 个干净的扇形,没有任何尖刺。(它下方有一条竖直的细缝,那里没有任何数据,怎么分都无所谓。)

8.3 换两个数据集

“3 个单元就够”是花朵数据的特点,还是普遍规律?代码 05 演示 2 在同心圆(Coursera 作业的附加数据集 noisy_circles,用 numpy 仿照 make_circles(factor=0.5, noise=0.3) 重写)和两个月牙(仿照 make_moons(noise=0.2))上重复了一遍(3 个种子的平均):

逻辑回归nh=1n_h=1nh=2n_h=2nh=3n_h=3nh=5n_h=5nh=20n_h=20nh=50n_h=50Bayes 最优
同心圆(测试)0.48300.58450.67700.76950.77880.78300.77530.7920
同心圆(训练)0.51500.60250.70500.78250.78170.78170.78580.7775
月牙(测试)0.86750.86820.86730.96350.96270.96280.96430.9735
月牙(训练)0.87750.87750.87500.97250.97330.97330.97330.9675
  • 同心圆:一条直线对一个圈无能为力(0.4830)。nhn_h 从 1 到 3,测试准确率逐步上升到 0.7695(3 条直线可以围出一个三角形,粗略地”圈住”内圈),之后基本持平。这个数据集噪声很大(两圈的间距 0.5,噪声标准差 0.3),Bayes 上限在测试集上只有 0.7920,大量的点本来就落在对方的地盘里。nh≥3n_h \ge 3 的网络离上限只差一两个百分点。
  • 两个月牙:逻辑回归已经有 0.8675,因为月牙的主体在两侧,一条斜线能分对大部分。nh=1,2n_h = 1, 2 几乎没有提高,nh=3n_h = 3 跳到 0.9635,接近上限 0.9735。要把两个月牙的尖端也分对,边界得弯成 S 形。

三个数据集都有一个”够用”的大小(这里碰巧都是 3 左右),之后测试准确率不再明显提高。够用的大小取决于决策边界的形状有多复杂,而这个事先通常不知道。

8.4 过拟合的苗头

400 个训练样本时,大网络只是”略微”过拟合。数据更少时呢?代码 05 演示 3 从 400 个训练样本里固定地挑出 80 个(这 80 个点上 Bayes 最优分类器的准确率是 0.9500):

nhn_h参数训练准确率(80 个,3 个种子)测试准确率(2000 个)测试准确率的最高点 @ 第几步
3130.9625 / 0.9625 / 0.96250.8610 / 0.8610 / 0.86100.8920@300 / 0.8915@300 / 0.8920@300
5210.9500 / 0.9875 / 0.98750.8375 / 0.8300 / 0.83000.8840@300 / 0.8810@500 / 0.8795@300
20811.0000 / 1.0000 / 1.00000.8120 / 0.7950 / 0.81650.8765@300 / 0.8735@300 / 0.8690@400
502011.0000 / 1.0000 / 1.00000.8275 / 0.8225 / 0.81300.8725@300 / 0.8730@300 / 0.8715@300
n_h = 3:训练 0.9625,测试 0.8610            n_h = 50:训练 1.0000,测试 0.8275
+----------------------------------------+   +----------------------------------------+
|                                       .|   |                                       .|
|            oo                       ...|   |            oo                      ....|
|.          o o  o                  .....|   |.          o o  o        ..       ...   |
|....        o         o    o    ........|   |....        o         o... o            |
|......                        ..........|   |......                ...      .........|
|.........    o    o oX   o  ............|   |.........    o    o ox.. o  ............|
|...x.......         o   o....x....x..x..|   |...x........        o.. o ...x....x..x..|
|............x.      o  ...x.......xx....|   |............x.      O. ...x.......xx....|
|.......xx.......   X ...........x.......|   |.......xx.......   x............x.......|
|................x.x*O......X X          |   |................x.x*o .....x.x..........|
|...      oo       .xx  o    o o         |   |         oo     ...xx  o    o o  .......|
|                .x.....                 |   |                .x  ....                |
|     o        O....O......              |   |     o        o.   o ....               |
|   o o  o  ................O      oo    |   |   o o  o     .     .......o      oo    |
|         ......................         |   |            ...    ...........          |
|       ..............x....x......       |   |      ...............x....x......       |
|    .............x....x.............    |   |   ..............x....x.............    |
|  ..........x..x.........xxx..........  |   |............x..x.........xxx..........  |
|............xxx............x............|   |............xxx............x............|
|........................................|   |........................................|
+----------------------------------------+   +----------------------------------------+
  • nh=20n_h = 20、50 把训练集背了下来:训练准确率 1.0000,比 Bayes 在这 80 个点上的 0.9500 还高,连落在”对方花瓣”里的噪声点都分对了。但测试准确率平均只有 0.8078、0.8210,比 nh=3n_h = 3(0.8610)还低。右边的字符画里,nh=50n_h = 50 为了包住个别训练点画出了一些”飞地”和弯折(比如右上角和中间偏上的几块 '.'),这些地方在测试集上大多是错的。这就是过拟合:模型的灵活性被用来记住训练集的偶然细节,而不是学到真正的规律。
  • 测试准确率在训练早期就到了最高点(第 300–500 步),之后一路下降到最终值。“在验证集上表现最好的时候停下来”叫 early stopping(第 6 讲)。
  • 数据越少,大网络越容易过拟合:同样的 nh=50n_h = 50,400 个训练样本时训练 − 测试的差距是 0.0757,80 个时变成 0.1790。

反过来,数据多了会怎样?演示 3 的最后一张表(种子 2,同一个测试集):

训练样本数nh=3n_h = 3 训练 / 测试nh=50n_h = 50 训练 / 测试Bayes 在训练集上
800.9625 / 0.86101.0000 / 0.82750.9500
4000.9400 / 0.90750.9650 / 0.88900.9375
2000(另外生成)0.9330 / 0.92250.9370 / 0.92250.9465

训练样本加到 2000 个时,nh=50n_h = 50 的训练准确率降到了 Bayes 之下(不再拟合噪声),训练 − 测试的差距缩小到 0.0145,测试准确率升到 0.9225,和 nh=3n_h = 3 一样好,大网络不再吃亏。两个网络的测试准确率也都比 400 个样本时高了,向 Bayes 上限又靠近了一步。

8.5 小结:nhn_h 是一个超参数

nhn_h 太小(这里是 1、2),训练准确率就上不去(high bias);“够用”之后测试准确率不再上升,再大就开始拟合噪声,数据越少越明显(high variance)。隐藏单元数、激活函数、学习率、迭代次数、初始化尺度这些由你决定、不由梯度下降学出来的量叫超参数(hyperparameters),以区别于 WW、bb 这些参数(第 4 讲)。选超参数要靠验证集,不能看测试集(第 6 讲)。实践中更常见的做法不是精确地找”刚好够用”的大小,而是网络稍微大一点,再用正则化控制过拟合:第 1 讲 §2.2 的 [5, 2, 1] 和本讲的 nh=2n_h = 2 都提醒过,太小的网络还容易卡在差的解上。


9. 拼起来:一个完整的训练流程 · Putting it together

Coursera 作业把一个隐藏层的网络拆成几个小函数,shallow_nn.py 照搬了这个结构。把它们串起来,就是第 1 讲 §2.1 的”训练四步”:

nn_model(X, Y, n_h):
    n_x, n_h, n_y = layer_sizes(X, Y, n_h)                       # 网络结构
    params = initialize_parameters(n_x, n_h, n_y, rng)           # §7:W 随机且小,b = 0
    重复 num_iterations 次:
        A2, cache = forward_propagation(X, params)               # §3:前向
        cost = compute_cost(A2, Y)                               # 交叉熵(第 2 讲)
        grads = backward_propagation(params, cache, X, Y)        # §6:6 个公式
        params = update_parameters(params, grads, learning_rate) # 梯度下降一步
    return params

predict(params, X) = (A2 > 0.5)

两个实现细节值得记住:compute_cost 先把 A[2]A^{[2]} 裁剪到 [10−12,1−10−12][10^{-12}, 1 - 10^{-12}] 再取 log(大初始化时 sigmoid 会饱和到恰好 0 或 1);nn_model 每一步都检查 cost 是否是有限的数,因为矩阵乘法里产生的 inf / nan 不会触发 numpy 的警告(第 1 讲踩过的坑)。函数和讲义各节的对应关系见代码 README。


10. 小结 · Summary

  • 两层网络 = 并排的几个逻辑回归(隐藏层)+ 一个以它们的输出为输入的逻辑回归(输出层)。记号:[l][l] 是层,(i)(i) 是样本,下标 ii 是层内的单元;输入层不算一层。
  • 前向传播:Z[1]=W[1]X+b[1]Z^{[1]} = W^{[1]}X + b^{[1]},A[1]=g(Z[1])A^{[1]} = g(Z^{[1]}),Z[2]=W[2]A[1]+b[2]Z^{[2]} = W^{[2]}A^{[1]} + b^{[2]},A[2]=σ(Z[2])A^{[2]} = \sigma(Z^{[2]})。样本按列堆叠后公式不变,因为 WXWX 的第 jj 列就是 Wx(j)Wx^{(j)}(代码 01:三种写法差 4.4e−16)。
  • 隐藏单元在花朵上学成了”过原点的直线开关”:4 个单元里 3 个在干活,切出 6 个扇形;输出层给它们投票。逻辑回归 0.510 / 0.501 → 4 个 tanh 单元 0.940 / 0.906。
  • 激活函数:σ′=a(1−a)≤1/4\sigma' = a(1-a) \le 1/4,tanh⁡′=1−a2≤1\tanh' = 1 - a^2 \le 1,ReLU′∈{0,1}' \in \{0, 1\}。tanh 在隐藏层通常优于 sigmoid(导数大、零中心;两者能表示的函数完全相同,差别只在训练速度)。ReLU 是深层网络的默认选择;但在本讲的小问题上,tanh 在花朵、月牙上更好(代码 03),所以拿不准时要在验证集上比较。输出层由 yy 决定:二分类用 sigmoid,回归用线性。
  • 必须有非线性:线性激活的两层网络就是逻辑回归。准确率、cost 都相同,压扁后的 W[2]W[1]W^{[2]}W^{[1]} 和逻辑回归的 ww 在 3 位小数上一致。
  • 反向传播 6 个公式:dZ[2]=A[2]−YdZ^{[2]} = A^{[2]} - Y;dW[2]=1mdZ[2]A[1]⊤dW^{[2]} = \frac1m dZ^{[2]}A^{[1]\top};db[2]=1m∑dZ[2]db^{[2]} = \frac1m\sum dZ^{[2]};dZ[1]=W[2]⊤dZ[2]∗g′(Z[1])dZ^{[1]} = W^{[2]\top}dZ^{[2]} * g'(Z^{[1]});dW[1]=1mdZ[1]X⊤dW^{[1]} = \frac1m dZ^{[1]}X^\top;db[1]=1m∑dZ[1]db^{[1]} = \frac1m\sum dZ^{[1]}。新东西只有第 4 个:误差按输出权重分回隐藏单元,再经 g′g' 这个”阀门”。维度规则:dWdW 和 WW 同形状,dZdZ 和 ZZ 同形状。
  • 梯度检验:双边差分,正确实现的相对误差在 1e−9 量级;按参数分开算能定位 bug;在 W≈0.01W \approx 0.01 时有的 bug 会躲过去,训练一段时间后要再查一次。
  • 初始化:全 0 → 对称性(全 0 + tanh 时梯度恰好为 0,一步不动;常数初始化时 4 个单元永远相同);随机数打破对称,bb 可以是 0。尺度太大 → 饱和,scale = 100 时 98% 饱和、10 次只有 1 次成功。对一个隐藏层,0.01 很好;深层网络需要 Xavier / He(第 7 讲)。
  • 隐藏层大小:花朵上 nh=3n_h = 3 是台阶,之后测试准确率停在 0.89–0.91,比 Bayes 上限(0.9375)低 0.03,这段差距是方差,加单元补不上。大网络在训练集上超过 Bayes,说明在拟合噪声;80 个训练样本时过拟合很明显(训练 1.0000、测试 0.82),2000 个时大网络不再吃亏。

11. 常见误区 · Common pitfalls

  1. “一个隐藏层的网络是一层网络。” 按 Ng 的数法,输入层是第 0 层、不算数,有一个隐藏层的网络是两层网络(L=2L = 2)。读论文和代码时注意别人用的是哪种数法。
  2. “矩阵写法是对循环写法的近似。” 两者是同一个计算(§3.3),差别只在浮点舍入(4.4e−16)。如果两者结果差得多,说明你的某个形状错了。
  3. “sigmoid 和 tanh 差不多,用哪个都行。” 它们能表示的函数确实相同,但在隐藏层里 tanh 学得快得多(代码 03:花朵上同学习率下 100 vs 400 步,同心圆上 1000 vs 4500 步)。sigmoid 只留给二分类的输出层。
  4. “ReLU 在任何问题上都最好。” 它是深层网络的默认选择,理由是梯度不衰减、计算便宜。在一个隐藏层、低维输入的小问题上不一定(代码 03:花朵上 ReLU 0.820 / 0.835,tanh 0.902 / 0.895)。不确定时在验证集上比较。
  5. “多加几个线性层,网络就更强。” 线性层叠多少都等于一层(§5)。没有非线性激活,[2, 50, 1] 和逻辑回归一模一样。
  6. “dZ1 = W2.T @ dZ2 @ g'(Z1)。” 公式 (4) 里是逐元素乘法 *,不是矩阵乘法。求 db 时忘了 keepdims=True 更危险:不报错,bb 悄悄变成矩阵(§6.4)。
  7. “梯度检验通过了,反向传播就一定对。” 在 W≈0.01W \approx 0.01 的初始参数上,漏乘 g′g' 这样的 bug 可能只给出 1e−6 到 1e−3 的相对误差(§6.6)。训练一段时间后要再查一次。
  8. “把权重初始化成 0 只是训练慢一点。” 不是慢,是根本学不出不同的隐藏单元:全 0 + tanh 时梯度恰好为 0,一步不动;常数初始化时 nhn_h 个单元永远是同一个单元(§7.2)。注意逻辑回归可以零初始化,因为它只有一个单元。
  9. “初始权重越大,网络’起步’越有力。” 正相反:大权重让 tanh / sigmoid 饱和,梯度几乎为 0,初始 cost 也很高(scale = 100 时 12.56),训练变得不可靠(§7.4)。
  10. “隐藏单元越多越好。” 花朵上 nh=3n_h = 3 之后测试准确率不再上升,nh=50n_h = 50 反而最差(0.8927);训练集越小,大网络过拟合越严重(§8)。不过答案也取决于数据量:2000 个训练样本时 nh=50n_h = 50 和 nh=3n_h = 3 一样好。

12. 练习 · Exercises

练习 1(形状与参数个数)。一个两层网络:nx=3n_x = 3,nh=5n_h = 5,输出 1 个单元,m=100m = 100 个样本。写出 W[1],b[1],Z[1],A[1],W[2],b[2],Z[2],dZ[1],dW[1],db[2]W^{[1]}, b^{[1]}, Z^{[1]}, A^{[1]}, W^{[2]}, b^{[2]}, Z^{[2]}, dZ^{[1]}, dW^{[1]}, db^{[2]} 的形状,以及参数总数。

参考答案

W[1]W^{[1]}:(5, 3);b[1]b^{[1]}:(5, 1);Z[1],A[1]Z^{[1]}, A^{[1]}:(5, 100);W[2]W^{[2]}:(1, 5);b[2]b^{[2]}:(1, 1);Z[2]Z^{[2]}:(1, 100);dZ[1]dZ^{[1]}:(5, 100)(和 Z[1]Z^{[1]} 相同);dW[1]dW^{[1]}:(5, 3)(和 W[1]W^{[1]} 相同);db[2]db^{[2]}:(1, 1)。

参数总数 5×3+5+5×1+1=265 \times 3 + 5 + 5 \times 1 + 1 = 26。注意 mm 不影响参数个数:参数是”每个样本共用的”,mm 只出现在 ZZ、AA、dZdZ 的列数里。

练习 2(手算一遍前向和反向)。tanh 隐藏层,一个样本:x=(2,−1)⊤x = (2, -1)^\top,y=0y = 0,W[1]=[0.51−10.5]W^{[1]} = \begin{bmatrix} 0.5 & 1 \\ -1 & 0.5 \end{bmatrix},b[1]=0b^{[1]} = 0,W[2]=[1,1]W^{[2]} = [1, 1],b[2]=0b^{[2]} = 0。 (a) 算出 z[1],a[1],z[2],a[2]z^{[1]}, a^{[1]}, z^{[2]}, a^{[2]} 和损失 L\mathcal{L}。(b) 算出 dz[2],dW[2],dz[1],dW[1]dz^{[2]}, dW^{[2]}, dz^{[1]}, dW^{[1]}。(c) 两个隐藏单元里,哪个的 W[1]W^{[1]} 那一行梯度大?为什么?单元 1 的输出权重 W1[2]W^{[2]}_1 的梯度为什么是 0? 提示:tanh⁡(2.5)≈0.9866\tanh(2.5) \approx 0.9866,σ(−0.9866)≈0.2716\sigma(-0.9866) \approx 0.2716。可以用 shallow_nn.forward_propagation 和 backward_propagation 验证。

参考答案

(a) z[1]=(0.5⋅2+1⋅(−1), −1⋅2+0.5⋅(−1))=(0,−2.5)z^{[1]} = (0.5 \cdot 2 + 1 \cdot (-1),\ -1 \cdot 2 + 0.5 \cdot (-1)) = (0, -2.5);a[1]=(0,−0.9866)a^{[1]} = (0, -0.9866);z[2]=0−0.9866=−0.9866z^{[2]} = 0 - 0.9866 = -0.9866;a[2]=0.2716a^{[2]} = 0.2716;y=0y = 0,所以 L=−log⁡(1−0.2716)≈0.3169\mathcal{L} = -\log(1 - 0.2716) \approx 0.3169。

(b) dz[2]=0.2716dz^{[2]} = 0.2716(预测偏高,z[2]z^{[2]} 应该变小)。dW[2]=dz[2]a[1]⊤=(0,−0.2679)dW^{[2]} = dz^{[2]}a^{[1]\top} = (0, -0.2679)。da[1]=W[2]⊤dz[2]=(0.2716,0.2716)da^{[1]} = W^{[2]\top}dz^{[2]} = (0.2716, 0.2716);g′=1−a[1]2=(1,0.0266)g' = 1 - a^{[1]2} = (1, 0.0266);dz[1]=(0.2716,0.00722)dz^{[1]} = (0.2716, 0.00722)。dW[1]=dz[1]x⊤=[0.5432−0.27160.01444−0.00722]dW^{[1]} = dz^{[1]}x^\top = \begin{bmatrix} 0.5432 & -0.2716 \\ 0.01444 & -0.00722 \end{bmatrix}。

(c) 单元 1(第一行)的梯度大得多。两个单元从输出层分到的误差一样(da=0.2716da = 0.2716),但单元 2 的 z=−2.5z = -2.5 已经饱和,tanh⁡′(−2.5)=0.0266\tanh'(-2.5) = 0.0266,阀门几乎关死,它的梯度只有单元 1 的约 1/38。单元 1 的 z=0z = 0,斜率最大(1)。而 W1[2]W^{[2]}_1 的梯度是 dz[2]⋅a1[1]=0.2716×0=0dz^{[2]} \cdot a^{[1]}_1 = 0.2716 \times 0 = 0:单元 1 现在输出 0,改变它的输出权重对预测没有任何影响。

练习 3(sigmoid 网络 = tanh 网络)。(a) 证明 σ(z)=1+tanh⁡(z/2)2\sigma(z) = \frac{1 + \tanh(z/2)}{2}。(b) 一个 sigmoid 隐藏层的网络参数是 (W[1],b[1],W[2],b[2])(W^{[1]}, b^{[1]}, W^{[2]}, b^{[2]})。证明 tanh 隐藏层、参数为 (W[1]/2, b[1]/2, W[2]/2, b[2]+∑jWj[2]/2)(W^{[1]}/2,\ b^{[1]}/2,\ W^{[2]}/2,\ b^{[2]} + \sum_j W^{[2]}_j/2) 的网络对任何输入都给出相同的输出。(c) 既然两者能表示的函数相同,为什么代码 03 里 tanh 学得更快?

参考答案

(a) tanh⁡(z/2)=ez/2−e−z/2ez/2+e−z/2\tanh(z/2) = \frac{e^{z/2} - e^{-z/2}}{e^{z/2} + e^{-z/2}},分子分母同乘 e−z/2e^{-z/2} 得 1−e−z1+e−z\frac{1 - e^{-z}}{1 + e^{-z}}。于是 1+tanh⁡(z/2)2=12⋅(1+e−z)+(1−e−z)1+e−z=11+e−z=σ(z)\frac{1 + \tanh(z/2)}{2} = \frac12 \cdot \frac{(1 + e^{-z}) + (1 - e^{-z})}{1 + e^{-z}} = \frac{1}{1 + e^{-z}} = \sigma(z)。

(b) sigmoid 网络的隐藏层输出 a=σ(W[1]x+b[1])=12+12tanh⁡(W[1]2x+b[1]2)a = \sigma(W^{[1]}x + b^{[1]}) = \frac12 + \frac12\tanh\big(\tfrac{W^{[1]}}{2}x + \tfrac{b^{[1]}}{2}\big)。代入输出层:z[2]=W[2]a+b[2]=W[2]2tanh⁡(W[1]2x+b[1]2)+(b[2]+12∑jWj[2])z^{[2]} = W^{[2]}a + b^{[2]} = \tfrac{W^{[2]}}{2}\tanh\big(\tfrac{W^{[1]}}{2}x + \tfrac{b^{[1]}}{2}\big) + \big(b^{[2]} + \tfrac12\sum_j W^{[2]}_j\big),正是 tanh 网络在新参数下的 z[2]z^{[2]}。代码 03 演示 3 末尾验证了,输出的最大差别是 2.2e−16。

(c) 梯度下降不是在”函数空间”里走,而是在参数空间里走。同一个函数在两种参数化下,梯度的大小和方向不同:sigmoid 的导数最大 0.25,输出不以 0 为中心(§4.3)。直观地说,sigmoid 网络相当于”同一个 tanh 网络,但隐藏层的参数被缩放了一半、输出又多了一个偏移”,同样的学习率在它身上走得更慢、更曲折。可以自己验证:把 03_activation_functions.py 里 sigmoid 的学习率调大(比如 2.5 倍),看达到门槛的步数是否接近 tanh。具体数字以你运行的结果为准。

练习 4(输出层用什么)。下面每个任务,输出层用什么激活函数?隐藏层呢?(a) 根据病人的化验指标判断是否患病;(b) 预测明天的气温(摄氏度,可能是负数);(c) 预测一套房子的价格;(d) 预测一张图片里有几个人。

参考答案

隐藏层:四个任务都可以默认用 ReLU(网络小、输入维度低时 tanh 也值得一试,§4.6)。输出层由 yy 决定: (a) 二分类 → sigmoid,损失用交叉熵。 (b) 实数,可正可负 → 线性,损失用均方误差(第 1 讲)。如果用 sigmoid 或 ReLU,就表示不了负的温度。 (c) 非负实数 → 线性或 ReLU(Ng 在视频里举的就是这个例子)。用 ReLU 能保证不预测出负价格,但要小心:如果输出单元的 zz 一直是负的,它会”死掉”、梯度为 0。 (d) 非负整数(计数)→ 常见做法是线性 / ReLU 输出加均方误差,或者把它当成多分类(0 人、1 人、……,softmax,第 9 讲),或者用泊松回归那样的输出(指数函数)。这个问题没有唯一答案,“输出层和损失函数怎样设计”是第 5 讲的主题。

练习 5(对称性,变形版)。把 W[1]W^{[1]} 随机初始化(0.01 × 随机数),但 W[2]W^{[2]} 初始化成 0。还有对称性问题吗?第 0 步的 dW[1]dW^{[1]} 是多少?先推理,再改代码验证(在 nn_model 之外自己写一个训练循环,或者在 initialize_parameters 之后把 params["W2"] 设成 0)。

参考答案

没有对称性问题。第 0 步时 W[2]=0W^{[2]} = 0,所以 dZ[1]=W[2]⊤dZ[2]∗g′=0dZ^{[1]} = W^{[2]\top}dZ^{[2]} * g' = 0,dW[1]=0dW^{[1]} = 0:隐藏层这一步不动。但 dW[2]=1mdZ[2]A[1]⊤dW^{[2]} = \frac1m dZ^{[2]}A^{[1]\top} 不是 0,而且因为 W[1]W^{[1]} 的各行不同、A[1]A^{[1]} 的各行不同,dW[2]dW^{[2]} 的 4 个分量也各不相同。更新一步后 W[2]W^{[2]} 的分量就各不相同了,从第 1 步起 dW[1]dW^{[1]} 也不再是 0,而且各行不同。

我验证的结果(花朵数据、种子 2、学习率 1.2):第 0 步 ∥dW[1]∥=0\|dW^{[1]}\| = 0、∥dW[2]∥=0.0072\|dW^{[2]}\| = 0.0072;第 1 步后 W[2]=[0.0018,0.0076,−0.0027,−0.0027]W^{[2]} = [0.0018, 0.0076, -0.0027, -0.0027],∥dW[1]∥=0.0024\|dW^{[1]}\| = 0.0024;10000 步后训练 / 测试准确率 0.940 / 0.907,和正常初始化一样。打破对称只需要某一层是随机的,而且要能让各单元收到不同的梯度。W[1]W^{[1]} 随机就够了。

练习 6(不平衡数据上的零初始化)。代码 04 里全 0 + sigmoid 一步不动,原因是两类正好各一半。现在只保留花朵训练集里第 0 类的全部 200 个和第 1 类的前 100 个(keep = np.concatenate([np.arange(200), np.arange(200, 300)])),用全 0 + sigmoid、4 个隐藏单元训练 10000 步。先预测:(a) 参数会动吗?(b) 训练后 W[1]W^{[1]} 的 4 行一样吗?(c) 准确率大概和什么网络差不多?然后运行验证。

参考答案

(a) 会。第 0 步 A[1]=0.5A^{[1]} = 0.5,A[2]=0.5A^{[2]} = 0.5,mean(A[2]−Y)=0.5−1/3≠0\text{mean}(A^{[2]} - Y) = 0.5 - 1/3 \ne 0,所以 db[2]db^{[2]} 和 dW[2]dW^{[2]} 不为 0(dW[2]dW^{[2]} 的 4 个分量相同)。下一步 W[2]≠0W^{[2]} \ne 0,dW[1]dW^{[1]} 也就不为 0 了。

(b) 一样。§7.1 的归纳法条件(W[1]W^{[1]} 各行相同、b[1]b^{[1]} 各分量相同、W[2]W^{[2]} 各分量相同)在第 0 步成立,所以永远成立。我运行的结果:10000 步后 W[1]W^{[1]} 的 4 行都是 [−6.2574,6.1907][-6.2574, 6.1907],W[2]=[−0.7324,−0.7324,−0.7324,−0.7324]W^{[2]} = [-0.7324, -0.7324, -0.7324, -0.7324]。

(c) 和只有 1 个隐藏单元的网络差不多:训练准确率 0.7367;同样的数据上 1 个 sigmoid 隐藏单元(随机初始化)是 0.7467。这批数据里第 0 类占 2/3,所以”全部猜第 0 类”就有 0.667,网络只比它好一点。

练习 7(梯度检验的陷阱)。代码 02 的 bug B 把 tanh 的导数写成了 1−z21 - z^2。(a) 分别在 z=0.04z = 0.04、0.50.5、11、22 处比较 1−z21 - z^2 和正确的 1−tanh⁡2z1 - \tanh^2 z。(b) 用 (a) 解释为什么 bug B 在 W≈0.01W \approx 0.01 时几乎查不出来、训练后却错得很离谱。(c) 你会怎样设计梯度检验,避免这类问题?

参考答案

(a) z=0.04z = 0.04:0.9984 vs 0.9984(前 4 位相同);z=0.5z = 0.5:0.75 vs 0.7864;z=1z = 1:0 vs 0.4200;z=2z = 2:−3 vs 0.0707(符号都反了)。

(b) W≈0.01W \approx 0.01 时花朵数据的 ∣z[1]∣|z^{[1]}| 只有百分之几,1−z21 - z^2 和 1−tanh⁡2z1 - \tanh^2 z 几乎相等,所以相对误差只有 4.5e−06。训练后 ∣z∣|z| 常常大于 1,两者天差地别,甚至符号相反,相对误差变成 1.0。

(c) Ng 的建议:在随机初始化后检查一次,训练一段时间后再检查一次。也可以直接用较大的随机参数(比如 shallow_nn.py 的自检用 scale = 1.0)或者若干组不同的参数检查,让 zz 覆盖激活函数的各个区域(包括饱和区)。

练习 8(直线能把平面切成几块)。nhn_h 个 tanh 单元饱和以后,近似于 nhn_h 条直线的”开关”。(a) nn 条过同一点的直线最多把平面分成几块?(b) nn 条一般位置的直线(没有两条平行、没有三条交于一点)呢?(c) 用 (a) 解释为什么花朵数据上 nh=2n_h = 2 不够、nh=3n_h = 3 够了。(d) 区域数够了,是不是就一定能分对?

参考答案

(a) 2n2n 块(nn 条过原点的直线把一圈切成 2n2n 个扇形)。 (b) 1+n+n(n−1)21 + n + \frac{n(n-1)}{2} 块。用归纳法:第 kk 条直线和前 k−1k-1 条各交于一点,被分成 kk 段,每段把一个区域一分为二,所以多出 kk 块。1+(1+2+⋯+n)=1+n+n(n−1)/21 + (1 + 2 + \dots + n) = 1 + n + n(n-1)/2。n=3n = 3 时是 7 块。 (c) 花朵绕一圈颜色变 6 次,需要 6 个扇形。2 条过原点的直线只有 4 个扇形,3 条正好 6 个。所以 nh=2n_h = 2 的训练准确率卡在 0.69,nh=3n_h = 3 跳到 0.94。 (d) 不一定。输出层只能做”加权投票再过 sigmoid”,即各开关读数的线性组合,不是任意的”区域 → 类别”查表。比如用 3 条过原点的直线时,6 个扇形的类别必须能由 sign(v1s1+v2s2+v3s3+c)\text{sign}(v_1 s_1 + v_2 s_2 + v_3 s_3 + c) 表示(sj=±1s_j = \pm 1),花朵恰好满足(§2.4 的网络就找到了这样的 vv)。一般情况下,区域数只是必要条件。第 4 讲会看到,有些函数(比如 parity)用一个隐藏层需要非常多的单元,加深网络才能高效表示。

练习 9(改代码,先预测再运行)。把 05_hidden_layer_size.py 演示 3 里的 [:80] 改成 [:200](训练样本从 80 个变成 200 个)。先预测:nh=3n_h = 3 和 nh=50n_h = 50 的训练准确率、测试准确率大概在什么范围?和 80 个、400 个时比,“训练 − 测试”的差距怎样变化?然后运行验证。

参考答案

预测:两种网络的测试准确率都应该介于 80 个和 400 个训练样本之间;nh=50n_h = 50 的训练准确率仍然很高(接近 1),但不一定全对;差距介于 0.18 和 0.08 之间。

我运行的结果(3 个种子):nh=3n_h = 3 训练 0.945、测试 0.9015(3 个种子相同);nh=50n_h = 50 训练 0.995 / 0.985 / 0.990,测试 0.865 / 0.877 / 0.872,差距约 0.12。趋势和预测一致:数据越多,大网络的过拟合越轻,但 200 个样本时 nh=50n_h = 50 仍明显不如 nh=3n_h = 3。(演示 3 里其他行的数字也会变,因为它们都用这 200 个样本。)


13. 延伸阅读 · Further reading

课程材料

  • Coursera Deep Learning Specialization,C1M3 “Shallow Neural Networks”(Andrew Ng)及编程作业 “Planar Data Classification with One Hidden Layer”:本讲的主要依据。
  • Stanford CS231n 课程笔记 “Neural Networks Part 1: Setting up the Architecture”(激活函数的比较)和 “Optimization 2: Backpropagation, Intuitions”(反向传播的直觉)。
  • Michael Nielsen, Neural Networks and Deep Learning(免费在线书),第 2 章 “How the backpropagation algorithm works”。

教材

  • Goodfellow, Bengio & Courville (2016). Deep Learning,第 6 章 “Deep Feedforward Networks”(6.3 隐藏单元,6.5 反向传播)。

激活函数与初始化的原始论文

  • LeCun, Bottou, Orr & Müller (1998). Efficient BackProp. ——为什么输入和激活要以 0 为中心、为什么对称的 tanh 型函数优于 logistic sigmoid(§4.3)。
  • Glorot & Bengio (2010). Understanding the difficulty of training deep feedforward neural networks. AISTATS. ——sigmoid 在深层网络里的饱和问题;Xavier 初始化(第 7 讲)。
  • Nair & Hinton (2010). Rectified linear units improve restricted Boltzmann machines. ICML.
  • Glorot, Bordes & Bengio (2011). Deep sparse rectifier neural networks. AISTATS.
  • Krizhevsky, Sutskever & Hinton (2012). ImageNet classification with deep convolutional neural networks. NeurIPS. ——ReLU 比 tanh 快约 6 倍的那张图。
  • Maas, Hannun & Ng (2013). Rectifier nonlinearities improve neural network acoustic models. ICML Workshop on Deep Learning for Audio, Speech and Language Processing. ——Leaky ReLU。
  • He, Zhang, Ren & Sun (2015). Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification. ICCV. ——He 初始化与 PReLU(第 7 讲)。
  • Hendrycks & Gimpel (2016). Gaussian Error Linear Units (GELUs). ——BERT、GPT 系列使用的激活函数。

反向传播

  • Rumelhart, Hinton & Williams (1986). Learning representations by back-propagating errors. Nature.

14. 下一讲预告 · Next lecture

第 4 讲:深层神经网络——L 层网络的前向与反向传播。 本讲把一个隐藏层的网络完全拆开了。下一讲把它推广到任意层数:

  • 本讲的 6 个公式里,(4)–(6) 会变成一个可以重复 L−1L-1 次的”模块”:linear_forward / activation_forward / linear_backward / activation_backward,再加一个 cache;
  • §2.3 的形状规则 W[l]:(n[l],n[l−1])W^{[l]}: (n^{[l]}, n^{[l-1]}) 会变成一条自动检查维度的工具;
  • 为什么要”深”:练习 8 留下的问题会变成一个实验,在同样的参数量下比较深而窄、浅而宽的网络(parity 函数);
  • §8.5 提到的”参数 vs 超参数”会正式定义。

到第 4 讲结束时,第 1 讲 tiny_nn.py 里 forward 和 backward 那两个循环,你就能一行一行地写出来了。

← 返回课程目录