Date
:2021-12-18
Repositity
: Gitee
0. 梯度及梯度方向
梯度:一个矢量,其方向上的方向导数最大,其大小正好是此最大方向导数,对应的方向即增长最快的方向。更详细的理解,参考上述知乎链接。这里通过一个代码看一下梯度的方向和大小:
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
"""
- Ref: https://www.zhihu.com/question/36301367
"""
def f(x, y):
return x ** 2 + y ** 2
def grad_x(f, x, y):
h = 1e-4
return (f(x + h / 2, y) - f(x - h / 2, y)) / h
def grad_y(f, x, y):
h = 1e-4
return (f(x, y + h / 2) - f(x, y - h / 2)) / h
def numerical_gradient(f, P):
grad = np.zeros_like(P)
for i in range(P[0].size):
grad[0][i] = grad_x(f, P[0][i], P[1][i])
grad[1][i] = grad_y(f, P[0][i], P[1][i])
return grad
x = np.arange(-4, 4, 0.25)
y = np.arange(-4, 4, 0.25)
ax_x, ax_y = np.meshgrid(x, y)
""" draw f(x) to 3d surface """
fig = plt.figure()
ax = Axes3D(fig)
func = ax_x ** 2 + ax_y ** 2
ax.plot_surface(ax_x, ax_y, func, cmap=matplotlib.cm.coolwarm)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('f(x)')
plt.show()
X, Y = np.meshgrid(x, y)
# flatten X, Y to 1D vector
X = X.flatten()
Y = Y.flatten()
# cal grad_x and grad_y
grad = numerical_gradient(f, np.array([X, Y]))
# Plot a 2D field of arrows.
plt.quiver(X, Y, grad[0], grad[1])
plt.xlim([-2, 2])
plt.ylim([-2, 2])
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.show()
看一下
f
(
x
)
=
x
2
+
y
2
f(x) = x^2 + y^2
f(x)=x2+y2对应的3D视图和梯度投影到(x, y)
平面的梯度方向,如下图所示:
可以发现梯度和方向符合预期,梯度方向即增长最快的方向。
2. 梯度下降
这里先看维基百科上一个很形象的比喻,如下:
一个人(假设是登山队员能走最快的路线)被困在山上,正试图下山(试图找到全局最小值),但是山上有大雾,能见度极低。因此,下山的最快路径是不可见的,所以他们必须使用本地信息来找到最小值。他可以使用梯度下降的方法,该方法包括查看当前位置的山坡陡度,然后向下降最陡的方向(即下坡)前进。如果他们试图找到山顶(即最大值),那么他们将朝着最陡峭的上升方向(即上坡)前进。使用这种方法,他们最终会找到下山的路,或者可能卡在某个洞里(即局部最小值或鞍点)或者一个湖泊。然而,山的陡度不是通过简单的观察就能立即明显看出,而是需要一个复杂的仪器来测量,而这个人目前恰好拥有。使用仪器测量山的陡度需要相当长的时间,因此如果他们想在日落之前下山,应尽量减少使用仪器。困难在于选择测量山坡陡度的频率,以免偏离最佳路线。
在这个类比中,人代表算法,下山的路径代表算法将探索的参数设置序列。山坡的陡度表示该点误差面的斜率。用于测量陡度的仪器是微分(误差面的斜率可以通过在该点处的平方误差函数的导数来计算)。他们选择行进的方向与该点的误差表面的梯度对齐。他们在进行另一次测量之前所经过的时间是步长。
回归课程,老师在本节中先是回顾了上一节的内容,然后结合cost function
对梯度方向和迭代做出说明。
f
(
x
)
=
lim
△
x
→
0
f
(
x
+
△
x
)
−
f
(
x
)
△
x
(1)
f(x) = \lim_{\triangle{x}\rightarrow{0}}\frac{f(x+\triangle{x}) - f(x)}{\triangle{x}} \tag{1}
f(x)=△x→0lim△xf(x+△x)−f(x)(1)
注意:
△
x
>
0
\triangle{x} > 0
△x>0 ,随着
x
x
x 增加
f
(
x
)
f(x)
f(x)增加(
f
(
x
)
>
0
f(x) > 0
f(x)>0),即导数正方向,导数是正的;反之是负导数反向,即下降最快的方向。故要想下降最快,则使用负梯度方向:
−
∂
c
o
s
t
∂
ω
-\frac{\partial{cost}}{\partial{\omega}}
−∂ω∂cost。
因此,有权重更新公式:
ω
=
ω
−
α
∂
c
o
s
t
∂
ω
(2)
\omega = \omega - \alpha\frac{\partial{cost}}{\partial{\omega}} \tag{2}
ω=ω−α∂ω∂cost(2)
其中,
α
\alpha
α 是学习率,通常指每次向前走的距离。可以发现每一次迭代都是朝着最快下降的方向,这就是典型的贪心算法思想。
查看课件可知。中间串讲的两个内容拿出来说一下:
- 为什么没有使用分治:
DNN
的参数量太大,且实际的模型其局部最小值可能会导致错误; - 梯度下降只能找到局部最优,无法保证找到全局最优。
深度神经网络中往往局部最优点较少,但是会存在鞍点:一维函数对应的梯度为0的点。这个相比局部最优是更需要解决的点。PyTorch
优化器中集成了大量的迭代方案,后续对其进行展开。
3. 损失函数偏导数推导
损失函数(MSE
):
c
o
s
t
=
1
N
∑
i
=
1
N
(
ω
∗
x
−
y
^
)
2
(3)
cost = \frac{1}{N}\sum_{i=1}^{N}(\omega * x - \hat{y})^2 \tag{3}
cost=N1i=1∑N(ω∗x−y^)2(3)
求导:
∂ c o s t ( ω ) ∂ ω = ∂ ∂ ω 1 N ∑ n = 1 N ( x n ∗ ω − y n ) 2 = 1 N ∑ n = 1 N ∂ ∂ ω ( x n ∗ ω − y n ) 2 = 1 N ∑ n = 1 N 2 ⋅ x n ⋅ ( x n ∗ ω − y n ) (4) \begin{aligned} \frac{\partial{cost(\omega)}}{\partial{\omega}} &= \frac{\partial}{\partial{\omega}}\frac{1}{N}\sum_{n=1}^{N}{(x_n * \omega - y_n)^2} \\ &= \frac{1}{N}\sum_{n=1}^{N}\frac{\partial}{\partial{\omega}}{(x_n * \omega - y_n)^2} \tag{4} \\ &= \frac{1}{N}\sum_{n=1}^{N}2\cdot x_n \cdot (x_n * \omega - y_n) \end{aligned} ∂ω∂cost(ω)=∂ω∂N1n=1∑N(xn∗ω−yn)2=N1n=1∑N∂ω∂(xn∗ω−yn)2=N1n=1∑N2⋅xn⋅(xn∗ω−yn)(4)
则前节的权重参数更新公式为:
ω
=
ω
−
α
⋅
1
N
∑
n
=
1
N
2
⋅
x
n
⋅
(
x
n
∗
ω
−
y
n
)
(5)
\omega = \omega - \alpha\cdot\frac{1}{N}\sum_{n=1}^{N}2\cdot x_n \cdot (x_n * \omega - y_n) \tag{5}
ω=ω−α⋅N1n=1∑N2⋅xn⋅(xn∗ω−yn)(5)
如果loss function
训练出现上扬,可能是学习率太大,当然也可能是其他原因。
注意:为了更好的观察loss
的趋势,可以使用加权均值去对loss
平滑。
4. 随机梯度下降
通常我们训练模型,直接使用梯度下降方法的情况几乎没有,而是使用其衍生版本:随机梯度下降。
可以从发现上一节我们代码中权重参数更新时,实际使用的是全局的平均Cost
,而不是单个样本的损失。随机梯度下降则是从所有样本集中随机选择一个样本,计算单个样本的损失进行梯度下降。为什么要这么做?
因为我们拿到的样本实际上是存在噪声的,那么在梯度下降中可能就存在一个动量使其可以突破鞍点(鞍点区域),在这神经网络训练中已被证明。但是全部单个操作会导致代码不好利用硬件的并行特性,因为每次迭代在时序上存在依赖关系的。
因此,mini-batch
出场:将训练样本随机拆成多个小的 batch
,这么做有什么意义?
因为,在实际训练中,假设我迭代全部数据
n
n
n 轮,每一轮有
m
m
m 个mini-batch
,那么我随机从中间抽取1个mini-batch
,如果我的
n
n
n 足够大,那么基本满足了随机性需求,保证在遇到鞍点时也能概率滑过去,同时还保证了计算的效率问题。
5. 代码实现
""" Use SGD """
x_data = [1., 2., 3., 4.]
y_data = [2., 4., 6., 8.]
w = 1.0
def forward(x):
return x * w
def loss_func(x, y):
y_pred = forward(x)
return (y_pred - y) ** 2
def gradient(x, y):
return 2 * x * (x * w - y)
print('Predict (before training) input: {}, foward: {}'.format(5, forward(5)))
for epoch in range(100):
for x, y in zip(x_data, y_data):
grad = gradient(x, y)
w = w - 0.01 * grad
print('\t x: {}, y: {}, grad: {}'.format(x, y, grad))
loss = loss_func(x, y)
print('Progress: epoch={}, w={}, loss={}'.format(epoch + 1, w, loss))
print('Predict (after training) input: {}, forward: {}'.format(5, forward(5)))
预测结果:
Progress: epoch=100, w=1.9999999999999998, loss=7.888609052210118e-31
Predict (after training) input: 5, forward: 9.999999999999998