Pytorch深度学习实践-03-[Gradient Descent]

Date: 2021-12-18

Repositity: Gitee

0. 梯度及梯度方向

Zhihu: 如何直观形象地理解方向导数与梯度以及它们之间的关系?

梯度:一个矢量,其方向上的方向导数最大,其大小正好是此最大方向导数,对应的方向即增长最快的方向。更详细的理解,参考上述知乎链接。这里通过一个代码看一下梯度的方向和大小:

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)=x0limxf(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=1N(ωxy^)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=1N(xnωyn)2=N1n=1Nω(xnωyn)2=N1n=1N2xn(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=1N2xn(xnωyn)(5)
如果loss function 训练出现上扬,可能是学习率太大,当然也可能是其他原因。

注意:为了更好的观察loss的趋势,可以使用加权均值去对loss平滑。

4. 随机梯度下降

通常我们训练模型,直接使用梯度下降方法的情况几乎没有,而是使用其衍生版本:随机梯度下降。

可以从发现上一节我们代码中权重参数更新时,实际使用的是全局的平均Cost,而不是单个样本的损失。随机梯度下降则是从所有样本集中随机选择一个样本,计算单个样本的损失进行梯度下降。为什么要这么做?

因为我们拿到的样本实际上是存在噪声的,那么在梯度下降中可能就存在一个动量使其可以突破鞍点(鞍点区域),在这神经网络训练中已被证明。但是全部单个操作会导致代码不好利用硬件的并行特性,因为每次迭代在时序上存在依赖关系的。

因此,mini-batch 出场:将训练样本随机拆成多个小的 batch,这么做有什么意义?

因为,在实际训练中,假设我迭代全部数据 n n n 轮,每一轮有 m m mmini-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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值