Gradient Descent

Gradient Descent

Tags
Machine Learning
Published
February 22, 2017
Author
yanbc

Gradient Descent

假设我们有一个函数,我们需要找到它的最小值。
其中x,y是数据集,是常量;w,b是变量,那么C函数的偏差值可以表示为:
同时C函数的 Gradient Vector 为:
如果我们可以使
那么可以实现 C 一直保持向下减小:
因为括号里是两个平方的和,永远不小于零,所以只要学习率 η 是一个小的正数,ΔC 就一定小于等于零。这就是梯度下降名字的由来:每一步都沿着梯度的反方向,也就是下降最快的方向,走一小步。把式(1)和式(2)写成参数的更新规则就是
有两点需要注意:
  • η 叫做学习率(learning rate)。上面 ΔC 的公式只是一阶近似,只有在 Δw 和 Δb 足够小的时候才成立。η 太大,近似不再成立,C 可能不降反升,甚至发散;η 太小,C 虽然一直在降,但降得非常慢。
  • 在神经网络里,C 一般是所有训练样本损失的平均值,。每走一步都要把全部样本算一遍太慢了,所以实际中通常每次只随机取一小批样本(mini-batch)来估算梯度,这就是随机梯度下降(Stochastic Gradient Descent,SGD)。

Back-propagation

那么,到底怎么去求式(1)和式(2)呢?这就需要依赖 Back-propagation 了。实际上,Back-propagation 就是一种快速求Gradient的方法。
在神经网络中,式(1)和式(2)里的 w 和 b 就是网络里所有的权重和偏置,动辄成千上万个。如果对每一个参数都单独用数值方法求偏导,每个参数都要跑一次前向传播,根本算不过来。
先约定一下记号(和 Michael Nielsen 的 Neural Networks and Deep Learning 一致):
  • :第 l−1 层第 k 个神经元到第 l 层第 j 个神经元的权重
  • :第 l 层第 j 个神经元的偏置
  • :第 l 层第 j 个神经元的加权输入
  • :第 l 层第 j 个神经元的输出(激活值),σ 是激活函数,比如 sigmoid
  • L 表示输出层,⊙ 表示两个向量逐元素相乘(Hadamard product)
我们定义一个值,称之为 error:
Back-propagation 的核心就是说,只要我们可以计算出 error 值,我们就可以通过下面的简单计算得出每一个参数的 Gradient:
这四条公式的直观含义是:
  • BP1:输出层的 error 由两部分相乘得到。 表示损失对这个输出有多敏感, 表示这个神经元的输出对它的输入有多敏感。比如用二次损失 时,,就是预测值和标签的差。
  • BP2:知道了第 l+1 层的 error,就可以通过权重矩阵的转置把它“传回”第 l 层,再乘上第 l 层的 。这一步就是“反向传播”名字的由来。
  • BP3:偏置的梯度就等于 error 本身。
  • BP4:权重的梯度等于这条连接输入端的激活值乘以输出端的 error。所以输入激活值很小(接近 0)的连接,它的权重也学得很慢。
BP1 和 BP2 都只是链式法则的应用。以 BP1 为例,因为 ,所以
BP2 也一样,只不过 会通过下一层的每一个 影响 C,所以要对 j 求和:
写成向量形式就是 BP2。BP3 和 BP4 的推导也类似,留给有兴趣的同学自己试试。

算法步骤

把这四条公式和梯度下降串起来,一次训练迭代是这样的:
  1. 前向传播:输入 x,逐层计算并记下每一层的 和
  1. 输出层 error:用 BP1 计算
  1. 反向传播:用 BP2 从第 L−1 层往回一直算到第 2 层,得到每一层的
  1. 求梯度:用 BP3 和 BP4 得到每一个 和
  1. 更新参数:用式(1)和式(2)更新所有的 w 和 b,然后回到第 1 步
第 1 步和第 3 步都只需要把网络遍历一次。也就是说,只花大约两次前向传播的计算量,就能得到全部参数的梯度,这就是它“快”的地方。

代码例子

下面用 numpy 实现一个 3-4-2 的小网络(sigmoid 激活,二次损失),先用数值梯度验证反向传播算出来的梯度是对的,再跑 1000 步梯度下降:
import numpy as np def sigmoid(z): return 1 / (1 + np.exp(-z)) def sigmoid_prime(z): s = sigmoid(z) return s * (1 - s) def cost(weights, biases, x, y): a = x for w, b in zip(weights, biases): a = sigmoid(w @ a + b) return 0.5 * np.sum((a - y) ** 2) def backprop(weights, biases, x, y): # 前向传播,记下每一层的 z 和 a a = x activations = [a] zs = [] for w, b in zip(weights, biases): z = w @ a + b a = sigmoid(z) zs.append(z) activations.append(a) grad_w = [None] * len(weights) grad_b = [None] * len(biases) # BP1:输出层的 error(二次损失,dC/da = a - y) delta = (activations[-1] - y) * sigmoid_prime(zs[-1]) grad_b[-1] = delta # BP3 grad_w[-1] = delta @ activations[-2].T # BP4 # BP2:把 error 一层一层往回传 for l in range(2, len(weights) + 1): delta = (weights[-l + 1].T @ delta) * sigmoid_prime(zs[-l]) grad_b[-l] = delta # BP3 grad_w[-l] = delta @ activations[-l - 1].T # BP4 return grad_w, grad_b rng = np.random.default_rng(0) sizes = [3, 4, 2] # 输入层 3 个神经元,隐藏层 4 个,输出层 2 个 weights = [rng.standard_normal((n, m)) for m, n in zip(sizes[:-1], sizes[1:])] biases = [rng.standard_normal((n, 1)) for n in sizes[1:]] x = rng.standard_normal((3, 1)) y = np.array([[0.0], [1.0]]) grad_w, grad_b = backprop(weights, biases, x, y) # 数值梯度:(C(w + eps) - C(w - eps)) / (2 * eps) eps = 1e-6 weights[0][1, 2] += eps c_plus = cost(weights, biases, x, y) weights[0][1, 2] -= 2 * eps c_minus = cost(weights, biases, x, y) weights[0][1, 2] += eps print("backprop: ", grad_w[0][1, 2]) print("numerical:", (c_plus - c_minus) / (2 * eps)) eta = 0.5 for step in range(1001): grad_w, grad_b = backprop(weights, biases, x, y) weights = [w - eta * gw for w, gw in zip(weights, grad_w)] # 式(1) biases = [b - eta * gb for b, gb in zip(biases, grad_b)] # 式(2) if step % 200 == 0: print(f"step {step:4d} C = {cost(weights, biases, x, y):.6f}")
输出:
backprop: -0.0009394054515286938 numerical: -0.0009394054423239595 step 0 C = 0.090742 step 200 C = 0.001705 step 400 C = 0.000799 step 600 C = 0.000515 step 800 C = 0.000378 step 1000 C = 0.000298
反向传播和数值方法算出来的梯度前八位有效数字都一样,说明公式实现是对的。之后的梯度下降过程中,C 也一直在往下降。

总结

  • 梯度下降:沿着梯度的反方向更新参数,。只要学习率 η 足够小,每一步都能让 C 变小。
  • 反向传播:一种高效计算梯度的方法。先前向传播一次,再把 error 从输出层往回传一次,就能得到所有参数的梯度。
  • 两者合起来,就是训练神经网络最基本的套路。PyTorch、TensorFlow 这些框架里的自动求导(autograd),本质上就是把反向传播推广到任意的计算图上。

References