查看全课大纲(第 77 / 82 节)
- 1.函数与数据泛化
- 2.向量:从几何直觉到高维空间
- 3.矩阵与张量
- 4.特征值与特征向量:矩阵变换的不动方向
- 5.凸函数:从向量求导的矩阵记号到好优化的函数形状
- 6.概率与统计:机器学习的数学基础
- 7.独立性与随机变量:贝叶斯公式与二项分布
- 8.期望、方差与协方差:随机变量的数字画像
- 9.联合概率:当一条轴不够用的时候
- 10.概率分布(一):从伯努利到伽马函数
- 11.概率分布(二):贝塔、泊松、正态与两个贝叶斯故事
- 12.机器学习的模型:找输入与输出的关系
- 13.机器学习的应用
- 14.过拟合
- 15.机器学习的分类
- 16.有监督学习:从房价预测说起
- 17.损失函数
- 18.梯度下降(上):更新规则与学习速率
- 19.梯度下降(2)
- 20.牛顿方法
- 21.KNN简介
- 22.距离测算
- 23.朴素贝叶斯
- 24.朴素贝叶斯的实际应用
- 25.信息熵:决策树构建的度量基础
- 26.信息增益与决策树的构建
- 27.ID3算法
- 28.决策树节点的划分
- 29.CART树
- 30.Bagging
- 31.Boosting:自适应提升算法
- 32.Gradient Boosting
- 33.线性模型:回归与分类的同一副骨架
- 34.最小二乘:把最优参数直接解出来
- 35.最小二乘法的几何与概率视角
- 36.极大似然:给最小二乘一个概率出身
- 37.逻辑回归(1):把线性模型接到分类上
- 38.逻辑回归(2):损失函数与多分类
- 39.---- 手造数据: 5 个样本, 2 个特征; x2 几乎 = 2*x1 (偏差手工指定, 无随机) ----
- 40.讲师例: 向量 "23014" -- 三把范数尺子量同一个向量
- 41.优化方法(1):坐标下降与前向选择
- 42.优化方法(2):前向策略与最小角回归
- 43.拉格朗日乘子法(1):从无约束到等式约束
- 44.拉格朗日乘子法(2):等式约束下的平行条件与拉格朗日函数
- 45.拉格朗日乘子法(3):不等式约束与 KKT 条件
- 46.最大间隔:把分类问题变成优化问题
- 47.拉格朗日对偶问题:把 w 和 b 全部换成 α
- 48.支持向量:边界上的少数派
- 49.SVM 计算过程:一笔到底算完,再走进核方法
- 50.贝叶斯公式:概率即信念
- 51.隐含变量和概率模型:给看不见的原因建模
- 52.EM 算法:期望与最大化的迭代
- 53.琴生不等式:EM 的数学钥匙
- 54.三个硬币问题:EM 的复盘与衔接
- 55.高斯混合模型:用隐变量解释双峰数据
- 56.隐马尔可夫模型
- 57.隐马尔可夫模型(2):前向算法与动态混合模型
- 58.隐马尔可夫模型在 NLP 中的运用
- 59.EM 算法:把「为什么收敛」推到底
- 60.HMM 在 NLP 中的运用:从分词到机器翻译
- 61.HMM 参数估计:Baum-Welch 的三块拉格朗日
- 62.高维下的高斯模型
- 63.主题模型:从词袋到隐含主题
- 64.Dirichlet 分布(1)
- 65.Dirichlet 分布(2)
- 66.统计学习内容回顾
- 67.K-means 算法
- 68.PCA 降维
- 69.SVD 降维
- 70.从聚类到流形
- 71.局部线性嵌入
- 72.随机邻域嵌入:SNE 与 t-SNE
- 73.无参模型:让数据自己说话
- 74.高斯过程:函数空间里的正态分布
- 75.高斯过程(2):噪声、正则化与核的线性组合
- 76.神经网络简介:从感知器到自编码器
- 77.反向传播:把误差一层层传回去
- 78.卷积神经网络(CNN):让特征自己学出来
- 79.循环神经网络(RNN):给网络装上记忆
- 80.深度学习回顾:静态一类,动态一类
- 81.增强学习(1):蒙特卡洛、树搜索与 Q-learning
- 82.强化学习(2):从乌鸦吃核桃到井字棋的 value 回传
反向传播:把误差一层层传回去
约 23 分钟
小象实战讲义 · 机器学习算法精讲
上一节结尾我们停在门口:构造损失函数时要「连续」,这样误差才能不断地传递回来。本节就讲这件事的正片——反向传播(Back Propagation):神经网络里最重要的算法,是「得到训练神经网络」最重要的一块。很多人对它有不同解释,讲师开场就说:我原先也有特别长的公式给大家解释这件事,直到后来发现,其实有更好的例子——所以本节的路线不是推公式,而是两个算例:一个只有加法和乘法,一个带 sigmoid;一步一步把梯度乘回去,每一层的数都手算可验。
💡 核心导读
- 要调的是谁:损失 最小 → 每个参数 的梯度 → 梯度下降(4.3 节老朋友); 实际是大矩阵,更新的是其中每个 ——「统一方式表述」而已。
- 链式法则是全部:,一层层乘回去(4.4 节「先外后内」的老规矩)。
- 算例一 :加法和乘法——恰好就是神经网络前半部分(线性组合)的全部构成;绿色正向算值,红色反向算导数。
- 算例二 sigmoid 神经元:本节主戏——从 出发, 每个权重一个梯度,每个数字手算可验。
- sigmoid 是一个「门」:——整块一起算与一步一步算,一模一样。
- 大图景:10 个参数就是 10 维空间——每个方向都要有梯度,迭代循环找 loss 的最低点。
要调整谁:从 E 到每一个 w
先把问题摆正。我们整个神经网络是线性、非线性叠加的形式(上一节),给定输入、得到输出 ,我们期望的是: 与 之间误差的平方和最小——这就是要构造的损失函数 (4.2 节的老朋友)。
要使 最小,需要调整谁?这么多参数里,比如我们要求关于 的——调整 ;对 ,我们要调整 ……其实任何参数的调整,都可以用以前学过的梯度下降(4.3 节):给 随机一个值,它下一个值就等于当前值,加上学习速率 乘上 的梯度——这个梯度是关于 的梯度。(一个记号提醒:4.3 节的最小化更新是「往负梯度方向走」的减号 ;本节的算例里 还只是网络输出、没挂上要最小化的 ,讲师板书随手用了加号演示「梯度进更新式」的机械形态,数值以板书为准。)
这里有个几乎必被问到的问题,讲师自己替大家问了:「 实际上是一个大的矩阵,难道矩阵同时更新吗?」——不是这样。你真正更新的时候,更新的是 里边的每一个 :每个分量各自加上 乘上自己的梯度。每次更新,只是我们用统一的方式表述而已。
所以最关键的问题在于:求出来误差和前边需要调整的参数之间的梯度关系。这该怎么做?
链式法则:先复习,再用
先给大家求一个最简单的。如果大家还记得 ——想求它关于 的导数,等于多少?
也可以换一种描述方式:先求 对 的导数,乘上 对 的导数——这就是链式法则,复合函数求导的重要部分(4.4 节讲梯度下降时用过,先外后内)。讲师的原话很实在:「如果大家还记得的话——不记得也得没办法,也得要记住我们已经讲过这些事情。」
层数再多也一样:如果有一个 是 的函数,那就 对 求导、 对 求导、 再对 求导,这么连成一条链。而我们这个神经网络要做的,就是这样一个反向传导的过程。
算例一:
最简单的复合函数构成就是加法和乘法——当然,这也是我们神经网络前一部分(线性组合)的全部内容。设:
先正向算。引入一个中间变量 ,把「」变成单独一个变量:;再 ,非常容易计算。为什么要引入 ?因为这样一拆,每一步都容易求导: 对 求导、对 求导,每个都等于 1; 变成 和 的乘积,对 求导等于 、对 求导等于 。
再反向算。想求 对 的偏导,用链式原则:
对 求导等于 , 是 ,所以第一部分是 ; 对 求导等于 1——乘上 1,最后等于 。把这个 用红色写在 旁边: 对 的传递导数是 。当然, 对 的偏导也是 ( 对 的导数同样是 1)。但 对 的导数就不一样了:等于 ,而 的输入值是 3——所以它的导数是 3。
# 例 1: f = (x + y) * z —— 前向(绿)与反向(红)
x, y, z = -2, 5, -4
# --- 前向: 绿色, 正面计算 ---
q = x + y # 引入中间变量 q, 把 x+y 变成单独一个变量
f = q * z
print(f"[前向/绿] q = x + y = {q}")
print(f"[前向/绿] f = q * z = {f}")
# --- 反向: 红色, 从输出往回乘 ---
df_dq = z # f = q*z, 对 q 求导得 z
df_dz = q # f = q*z, 对 z 求导得 q
dq_dx = 1 # q = x+y, 对 x 求导得 1
dq_dy = 1
print(f"[反向/红] df/dx = df/dq * dq/dx = {df_dq} * {dq_dx} = {df_dq * dq_dx}")
print(f"[反向/红] df/dy = df/dq * dq/dy = {df_dq} * {dq_dy} = {df_dq * dq_dy}")
print(f"[反向/红] df/dz = {df_dz}")[前向/绿] q = x + y = 3
[前向/绿] f = q * z = -12
[反向/红] df/dx = df/dq * dq/dx = -4 * 1 = -4
[反向/红] df/dy = df/dq * dq/dy = -4 * 1 = -4
[反向/红] df/dz = 3讲师给这套双色记法定了名:绿色是我们输入值正面的计算,红色是最后输出的结果对于每一个变量的导数。那这些导数有什么用?——以后会看到:导数是用来调整所有这些数的。这个例子特别简单,我猜大家没有什么问题。
顺路:激活函数的动物园
往下走之前,讲师顺路把激活函数盘了一遍。整个神经网络,就是先通过刚才描述的线性组合(加和乘的关系),之后通过一个非线性的激活函数——这个激活函数:
- 有的时候用 sigmoid。它最大的问题是:输入跑到很远的两端时,梯度就非常小了;
- 有的时候用 tanh,会好一些;
- 但最好是 rectifier(修正线性,即现在常说的 ReLU):保持大于 0 的时候,它的导数就是一个 1——一直是这样一个特别好的值,不会减小;
- 还有一种 softplus,是去逼近 rectifier 的另一个比较连续的激活函数,这个方向都是 OK 的。
所以整个过程就是两个方向:绿色叫 activation——把这些值 输进去,激活出 ;红色就是求它的 gradient——对 求导、 对 、 对 ,一路乘回去。整个反向传播,走的就是红色这条路。
算例二:sigmoid 神经元,一步一步乘回去(主戏)
现在看经过 sigmoid 这个激活函数时,整个运算该怎么做。这个例子跟我们以前的记号稍稍有点区别——讲师原话:「这个也没办法,是别人写的。」线性组合是 ,其中 就当作偏置 。讲师还预防了一问:「大家不用纠结说我们这整个 presentation 怎么又变了?一样的道理,没什么区别。」——它描述的正是上一节那个 sigmoid 函数、也就是逻辑回归(8.5 节):给定的输入配上系数 ,做成线性组合,经过 的负的线性组合,构造出 sigmoid,它的输出就是我们最终激活出来的输出。
值从哪来:初始化
假设给定的值是:。这些值哪来的?—— 肯定知道, 就是我们正常的输入; 呢,随机先取一些值,就像在最开始做一个初始化一样。
前向(绿):从 1 到 0.73
,,它俩一加为正 4;正 4 再加上 ,合起来为 1——所以括号里头得出的就是一个正 1。前面还有一个负号,乘上 变成 ; 的负一次幂是 0.37(讲师先顺口说了个 0.73、当场纠正——0.73 是最终输出,别混);0.37 再加 1,整个分母是 1.37;分子是 1 的时候,最后得出来的结果——最后一个绿色的 0.73。讲师对这个前向很有底气:「大家不用纠结这个数对不对,一定是对的,你不信大家可以算一下。」
反向(红)之前:四个求导基本功
在做整个 back propagation 之前,需要对每一个子函数求导数。比较难的有这么几个(其实都是基本功):
- 求导,它的导数是它自己;
- 倍乘上 ,导数是 ——相当于常数乘 ;
- 分之一就是 ,再求导,变成 ;
- ,一个常数对 求导,最后等于 1。
反向(红):每一步都是「本段导数 × 上游传来的导数」
第一块,永远是一:这是 对 自己求导,返回的第一部分永远是 1。
过 :它的导数是 ,而 之前输入的是 1.37——所以这一段的导数是 ,乘上上游的 1,从这步返回去的就是 −0.53。
过 :加一个常数,导数还是 1——问大家 −0.53 跟加一比应该有多少?不用问,1 乘上 −0.53,还是 −0.53。
过 exp:exp 这边输入的是 ,导数是它自己 ;乘上边传过来的 −0.53,一乘是 ,最后得到 −0.2。到这里讲师把原则钉死了一句话:「每次都是求这一阶段的导数,乘上上一部分传来的导数。」——链式法则的双层版, 再多几层就再多乘几段,完全一样。
过 :−0.2 乘上 的导数(就是 本身),负负得正,变成 0.2。
加法扇出:0.2 遇到加法节点,导数都是 1,所以两条分支各得 0.2——还记得刚才 那个形式吗?它传导的是 ,只要一加和,两支就都是 。同一个道理。
到权重了:我们关心 这一块。 这一段,对 求导得到的是 ; 乘上面传来的 0.2,等于 −0.2。(讲师在这里把前向的绿色值和反向的红色梯度看混了一眼——「这边呢是 6——啊,对,不是 6,是 0.2,Sorry」——前向是 6,反向是 0.2,两套数字别串色。)同样道理:
- 对 求导,得到的是 , 0.4;
- 对 求导,得到的是 ,−0.4;
- 对 求导,得到的是 ,−0.6;
- 偏置那一路( 乘的输入是常数 1),梯度就是 0.2。
这样的结果都算出来了。把整个前向与反向跑成代码——每一步对应板书的一个数字:
import math
# 例 2: sigmoid 神经元(权重为随机初始化后给定的值)
w0, x0 = 2, -1
w1, x1 = -3, -2
w2 = -3 # 偏置
# --- 前向: 绿色, 一步一步 ---
s = w0 * x0 + w1 * x1 + w2 # 线性组合 = 1
neg = -s # = -1
e_neg = math.exp(neg) # = 0.37
denom = 1 + e_neg # = 1.37
f = 1 / denom # = 0.73
print("[前向/绿]")
print(f" 线性组合 s = 2*(-1) + (-3)*(-2) + (-3) = {s}")
print(f" -s = {neg}, e^(-s) = {e_neg:.2f}, 1+e^(-s) = {denom:.2f}")
print(f" f = 1/(1+e^(-s)) = {f:.2f}")
print()
# --- 反向: 红色, 每步 = 本段导数 * 上游传来的导数 ---
print("[反向/红]")
g = 1.0 # f 对 f 自己求导, 起点永远是 1
print(f" 起点 f/f = {g:.0f}")
g = g * (-1.0 / denom ** 2) # 1/x 的导数 -1/x^2, 此处 x=1.37
print(f" 过 1/x 乘 -1/{denom:.2f}^2 = {-1.0 / denom ** 2:.2f} -> {g:.2f}")
g = g * 1.0 # 加常数 1, 导数是 1
print(f" 过 +1 乘 1 -> {g:.2f}")
g = g * e_neg # exp 的导数是它自己, 此处 = 0.37
print(f" 过 exp 乘 e^(-s) = {e_neg:.2f} -> {g:.2f}")
g = g * (-1.0) # 乘 (-1)
print(f" 过 *(-1) 乘 -1 -> {g:.2f}")
# 加法节点: 梯度原样分给每个加数, 再各自乘本地梯度
print(f" 加法扇出: 梯度 {g:.1f} 原样分给 w0*x0 与 w1*x1 两支")
dw0, dx0 = g * x0, g * w0
dw1, dx1 = g * x1, g * w1
dw2 = g * 1.0
print(f" df/dw0 = {g:.1f} * x0 = {dw0:.1f} df/dx0 = {g:.1f} * w0 = {dx0:.1f}")
print(f" df/dw1 = {g:.1f} * x1 = {dw1:.1f} df/dx1 = {g:.1f} * w1 = {dx1:.1f}")
print(f" df/dw2 = {g:.1f} * 1 = {dw2:.1f} (偏置那一路乘的输入是常数 1)")
print()
# --- gate 捷径: 把 sigmoid 整块当一个门, sigma' = f*(1-f) ---
gate = f * (1 - f)
print(f"[gate 捷径] sigma'(s) = f*(1-f) = {f:.2f} * {1 - f:.2f} = {gate:.2f}")
print(f"[对照] 逐步链乘到 s 处 = {g:.2f}, 与 gate 捷径 {gate:.2f} 一模一样")[前向/绿]
线性组合 s = 2*(-1) + (-3)*(-2) + (-3) = 1
-s = -1, e^(-s) = 0.37, 1+e^(-s) = 1.37
f = 1/(1+e^(-s)) = 0.73
[反向/红]
起点 f/f = 1
过 1/x 乘 -1/1.37^2 = -0.53 -> -0.53
过 +1 乘 1 -> -0.53
过 exp 乘 e^(-s) = 0.37 -> -0.20
过 *(-1) 乘 -1 -> 0.20
加法扇出: 梯度 0.2 原样分给 w0*x0 与 w1*x1 两支
df/dw0 = 0.2 * x0 = -0.2 df/dx0 = 0.2 * w0 = 0.4
df/dw1 = 0.2 * x1 = -0.4 df/dx1 = 0.2 * w1 = -0.6
df/dw2 = 0.2 * 1 = 0.2 (偏置那一路乘的输入是常数 1)
[gate 捷径] sigma'(s) = f*(1-f) = 0.73 * 0.27 = 0.20
[对照] 逐步链乘到 s 处 = 0.20, 与 gate 捷径 0.20 一模一样拿梯度去更新
我们举一个例子,关心 ,最后得出一个 ,这是什么意思?最开始给定的 等于 2。下一次需要调整的时候,下一阶段的 就等于这一时刻的 ,加上学习速率 乘上 的梯度。假设 (随便举的例子):
所以下一次更新的时候, 就会变成 1.98(讲师口播说成了「0.98」,是口误—— 是 1.98)。同样的道理,你可以不断地更新 、,所有的这些。
那 E 在哪里
当然,我们其实最后真正更新优化的,不是 这个函数本身。 这个函数,需要跟它得到的训练数据期望的 之间做一个差,才是我们的 。我只是举了个特别简单的例子:输入第一个数据 ,它可能有一个期望的 ,但我们这没有一个大家期望的 ——就算有的话也没什么。有一个期望的 的话, 是一个常数: 对 求导,就等于 对 求导、乘上 对 求导——而 对 求导是一个常数,没有变化。所以链上多乘一层而已,还是刚才的梯度在传。这就是通过一个具体算例告诉大家:back propagation 是怎么去做的。而且刚才只传导了一层——下面看两层怎么传导。
把 sigmoid 当成一个「门」
在这之前,还有一个重点:也许有的时候,我们需要把整个一块变成单独的一个区域。比如说,sigmoid 这一整块可以当成是一个大的 gate(门)——它里边是一个 sigmoid 函数。sigmoid 函数求导自己有特别多的优点:
求导之后,等于「一减去它本身,乘上它本身」。所以我们可以把整个这一大块看成一个复合函数:它输出的结果是 0.73,那么返回梯度直接就 0.73 乘上 ,直接算出 0.2——不需要一步一步地回去算(上面代码块最后一行的对照:逐步链 0.20,gate 捷径 0.20,一模一样)。一步一步回去算和整个一起算,结果是完全相同的——只是这里边有个特殊的性质而已。这个「门」的思想,就是工程上把一整块运算打包、只留进出两个接口的做法。
两层怎么传导
再往后走:两层的时候,这件事情该怎么算?跟着思路走——
是输出, 是一个 sigmoid 函数; 等于所有的线性组合——每个节点接一个三维的系数; 是这一块: 乘上 那边的线性组合。回过头来做的是 对 求导。一层层展开:
拆开看每一步。对 这个具体某一个权重求导的时候,我们只关心它的 —— 乘上 ,其他节点的部分并不关心。把 提出去,剩下对 求 的导数:又是一个复合函数求导—— 对 求导(这就简单了,是 sigmoid 的导数,一减它本身), 再对 求导(线性的,把 解析成 的形式,得出 )。所以最后: 乘上一个 sigmoid 的导数,前面再乘 ——你会发现这个也并不特别难算。
那我们想再多算一层呢?比如输出这部分叫 :——就像 一样,我们就多了一层而已。把所有前面的结果,都再乘上一个 对 的导数(又一个 sigmoid 的门),就可以把 这一层的误差,直接导到 里边。所以就是这么一步一步、一层一层地往前推算。
把这套公式跑成数字(讲义设定的小网络,正好就是「2 个节点、各接 3 维输入、汇聚成一个 、再过一层 sigmoid」),并用数值梯度对账——把 扰动一点点,看 实际变多少,与公式比一比:
import math
def sig(z):
return 1.0 / (1.0 + math.exp(-z))
def dsig(v):
"""sigmoid 的门: 已知输出 v, 导数就是 v*(1-v)"""
return v * (1 - v)
# 两层小网络(讲义设定): 第一层 2 个节点各接 3 维输入, 汇聚成 S, 再过一层 sigmoid 输出 y
x = [1.0, 2.0, -1.0] # 3 维输入
W = [[1.0, 0.0, -1.0], # 节点 1 的三个权重
[0.0, 1.0, 1.0]] # 节点 2 的三个权重
b = [0.0, 0.0]
u = [1.0, 2.0] # 汇聚权重: S = u1*a1 + u2*a2
def forward(W, b, u):
a = []
for i in range(2):
z = sum(W[i][j] * x[j] for j in range(3)) + b[i]
a.append(sig(z))
S = u[0] * a[0] + u[1] * a[1]
y = sig(S)
return a, S, y
a, S, y = forward(W, b, u)
print(f"前向: a = [{a[0]:.4f}, {a[1]:.4f}], S = {S:.4f}, y = {y:.4f}")
print()
# 公式: dy/dw_ij = sigma'(S) * u_i * sigma'(z_i) * x_j —— 一层层乘回去
print("公式 dy/dw_ij = sigma'(S) * u_i * sigma'(z_i) * x_j:")
for i in range(2):
for j in range(3):
g = dsig(y) * u[i] * dsig(a[i]) * x[j]
print(f" dy/dw_{i+1}{j+1} = {dsig(y):.4f} * {u[i]:.1f} * {dsig(a[i]):.4f} * {x[j]:+.1f} = {g:+.4f}")
print()
# 对账: 数值梯度(把 w_ij 扰动一点点, 看 y 实际变多少)与公式一致
eps = 1e-6
print("数值对账(扰动 eps=1e-6, 中心差分):")
for i in range(2):
for j in range(3):
Wp = [row[:] for row in W]
Wp[i][j] += eps
Wm = [row[:] for row in W]
Wm[i][j] -= eps
num = (forward(Wp, b, u)[2] - forward(Wm, b, u)[2]) / (2 * eps)
g = dsig(y) * u[i] * dsig(a[i]) * x[j]
print(f" w_{i+1}{j+1}: 公式 {g:+.6f} 数值 {num:+.6f}")前向: a = [0.8808, 0.7311], S = 2.3429, y = 0.9124
公式 dy/dw_ij = sigma'(S) * u_i * sigma'(z_i) * x_j:
dy/dw_11 = 0.0800 * 1.0 * 0.1050 * +1.0 = +0.0084
dy/dw_12 = 0.0800 * 1.0 * 0.1050 * +2.0 = +0.0168
dy/dw_13 = 0.0800 * 1.0 * 0.1050 * -1.0 = -0.0084
dy/dw_21 = 0.0800 * 2.0 * 0.1966 * +1.0 = +0.0314
dy/dw_22 = 0.0800 * 2.0 * 0.1966 * +2.0 = +0.0629
dy/dw_23 = 0.0800 * 2.0 * 0.1966 * -1.0 = -0.0314
数值对账(扰动 eps=1e-6, 中心差分):
w_11: 公式 +0.008394 数值 +0.008394
w_12: 公式 +0.016789 数值 +0.016789
w_13: 公式 -0.008394 数值 -0.008394
w_21: 公式 +0.031439 数值 +0.031439
w_22: 公式 +0.062878 数值 +0.062878
w_23: 公式 -0.031439 数值 -0.031439读这张表的两层结构。第一层(公式那六行): 与 只差一个符号——因为节点 1 的两个权重一个接 、一个接 ,同一个节点、不同的输入,梯度只在最后乘 那一步分道;而 比 大一倍——输入翻倍( 对 )。第二层(对账那六行):公式的梯度与「真把权重拨动一丁点、看输出实际变多少」到小数点后六位完全一致——一步步乘回去的链,就是真导数。这就是反向传播的全部底气。
大图景:10 维空间里找最低点
总之一句话:就是一个复合函数求导。这个导数代表的是:神经网络有向量的输入、有特别多复杂的系数(权重),给它构造了一个特别高维空间的、要被优化的函数。这个函数的参数特别多——大家想想上面这个小网络里有几个参数?第一层 个权重,再加 2 个偏置是 8 个;再把汇聚的 2 个 算上,一共 10 个参数——就是 10 维空间里边构造的一个需要被优化的函数。你要在 10 个维度上不断地变化,就很难找,而且也不知道怎么搜索——我们需要知道每一个方向上的梯度。这 10 维空间的每一个轴都往某一个方向驱动、往某一个方向移动,大家不断地迭代和循环,找到这 10 维空间里函数的最优点,或者说它的最低点——loss function 最低的情况。所以这个呢,就是我们在做的:反向传播。
📝 动手练一练
- 乘法节点的对称性:算例二里, 而 ; 而 。观察每一对 与 的梯度,说说它们之间「互为对方的什么」?再回答:训练的时候我们更新哪一个、不更新哪一个,为什么?
👉 点击查看参考答案
这个乘法节点上,对 求导得 、对 求导得 ——两侧的梯度互为对方的输入值:、。训练时只更新 (参数): 是模型的旋钮,梯度告诉每个旋钮往哪边拧; 是给定的数据,不是旋钮——数据不动,模型动。
- gate 捷径与「两端变小」:用 手算三个导数:、、(提示:,,)。观察输出越极端、导数越小的规律,然后回答:为什么 rectifier 没有这个烦恼?
👉 点击查看参考答案
;;。输出越靠近 0 或 1(输入跑向两端),导数越小——梯度在两端「熄火」,这就是讲师说的 sigmoid 最大的问题(层数一多,小梯度连乘会越乘越小)。rectifier 在大于 0 的区域导数恒等于 1——不管跑多远都不减小,所以没有这个烦恼。
本章小结
反向传播 = 复合函数求导的链式法则在神经网络上的落地。本节走了三步。第一步,摆正问题:损失 最小 → 要每个参数 的梯度 → 梯度下降; 是大矩阵但更新的是每个分量,「统一表述」而已。第二步,两个算例: 建立直觉(绿色正向算值、红色反向算导数,中间变量把复合拆成每步可导的小段);sigmoid 神经元是主戏——从 出发,(过 )、(过 )、(过 exp)、(过 )、加法扇出,最后每个权重拿到自己的梯度(: 、: 、: 、: )——每一步都是「本段导数 × 上游传来的导数」;拿 一乘一加, 从 2 更新到 1.98。第三步,工程化与图景:sigmoid 整块是一个「门」,,一步出导数、与逐步链一模一样;两层网络 ,数值对账六位小数一致;而真正的 只是在链上多乘一层( 是常数)。收尾的大图景:10 个参数就是 10 维空间——每个方向都要有梯度,迭代循环,找 loss function 的最低点。
📋 行动清单
- 口述原则:对着算例二的七个数字(1 → −0.53 → −0.53 → −0.2 → 0.2 → 各权重梯度),不看笔记把「每一步 = 本段导数 × 上游传来的导数」讲一遍。
- 手推两层公式:在纸上推出 ,再与代码块的六行数字对账。
- 数参数:给自己画一个「3 维输入、2 节点、1 汇聚输出」的小网络,数一数一共几个参数(10 个)——那就是几维的优化空间。
- 对照激活函数:用练一练第 2 题的三个数(0.25、0.105、0.018)说清「sigmoid 两端梯度熄火」,再讲 rectifier 为什么不怕(导数恒 1)。
—— 小象教研组
配套资料 · 直接下载
课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。
- 下载第19章课件:神经网络(BP/CNN/RNN)