深度学习入门学习笔记(六)

深度学习入门学习笔记(六)

与学习相关的技巧

本章将介绍神经网络的学习中的一些重要观点,主题涉及寻找最优权重参数的最优化方法、权重参数的初始值、超参数的设定方法等。此外,为了应对过拟合,本章还将介绍权值衰减、Dropout等正则化方法,并进行实现。

一、参数的更新

神经网络学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化。遗憾的是,神经网络的最优化问题非常难。这是因为参数空间非常复杂,无法轻易找到最优解。而且,在深度神经网络中,参数的数量非常庞大,导致最优化问题更加复杂。
在前面的学习中,为了找到最优参数,我们将参数的梯度(导数)作为线索。使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机梯度下降法,简称SGD

1. SGD

我们先来复习一下SGD。用数学式可以将SGD写成下面的式子。
W    ←    W    −    η ∂ L ∂ W W\;\leftarrow\;W\;-\;\eta\frac{\partial L}{\partial W} WWηWL
SGD是朝着梯度方向只前进一定距离的简单方法。现在,我们将SGD实现为一个python类。

class SGD:
    def __init__(self, lr=0.01)
        self.lr = lr

    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

这里,进行初始化时的参数lr表示learning rate(学习率)。这个学习率会保存为实例变量。此外,代码段中还定义了update(params, grads)方法,这个方法在SGD中会被反复调用。参数params和grads是字典型变量,按params[‘W1’]、grads[‘W1’]的形式,分别保存了权重参数和它们的梯度。
使用这个SGD类,可以按如下方式进行神经网络的参数的更新

network = TwoLayerNet(...)
optimizer = SGD()   # 使用随机梯度下降作为优化器

for i in range(10000):
    ···
    x_batch, t_batch = get_mini_batch(...) # mini-batch
    grads = network.gradient(x_batch, t_batch)
    params = network.params
    optimizer.update(params, grads)
    ···

参数的更新由optimizer负责完成。我们在这里需要做的只是将参数和梯度的信息传给optimizer。
像这样,通过单独实现进行最优化的类,功能的模块化变得更简单。比如,后面我们马上会实现另一个最优化方法Momentum,它同样会实现成拥有update(params, grads)这个共同方式的形式。这样一来,只需要将optimizer = SGD()这一语句换成optimizer = Momentum(),就可以从SGD切换为Momentum。

许多深度学习框架都实现了各种最优化方法,并且提供了可以简单切换这些方法的构造。

2. SGD的缺点

虽然SGD简单,并且容易实现,但是在解决某些问题时可能没有效率。也就是说,SGD的缺点是,如果函数的形状非均向(anisotropic),比如呈延伸状,搜索的路径就会非常低效。因此,我们需要比单纯朝梯度方向前进的SGD更聪明的方法。SGD低效的根本原因是,梯度的方向并没有指向最小值的方向
为了改进SGD的缺点,下面我们将介绍Momentum、AdaGrad、Adam这三种方法来取代SGD。

  • Momentum

Momentum是“动量”的意思,和物理有关。用数学式表示Momentum方法,如下所示。
ν    ←    α ν    − η ∂ L ∂ W \nu\;\leftarrow\;\alpha\nu\;-\eta\frac{\partial L}{\partial W} νανηWL
W    ←    W      +      ν W\;\leftarrow\;W\;\;+\;\;\nu WW+ν

这里新出现了一个变量v,对应物理上的速度。上式表示了物体在梯度方向上受力,在这个力的作用下,物体的速度增加这一物理法则。
Momentum方法给人的感觉就像是小球在地面上滚动。

在这里插入图片描述

上面的公式中有αv这一项。在物体不受任何力时,该项承担使物体逐渐减速的任务(α设定为0.9之类的值),对应物理上的地面摩擦或者空气阻力。下面是Momentum的代码实现:

class Momentum:
    """Momentum SGD"""
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zero_like(val)

        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
            params[key] += self.v[key]

实例变量v会保存物体的速度。初始化时,v中什么都不保存,但当第一次调用update()时,v会以字典型变量的形式保存与参数结构相同的数据。

在这里插入图片描述

上图中,更新路径就像小球在碗中滚动一样。和SGD相比,我们发现“之”字形的“程度”减轻了。这是因为虽然x轴方向上受到的力非常小,但是一直在同一方向上受力,所以朝同一个方向会有一定的加速。反过来,虽然y轴方向上受到的力很大,但是因为交互地收到正方向和反方向的力,它们会互相抵消,所以y轴方向上的速度不稳定。因此,和SGD时的情形相比,可以更快地朝x轴方向靠近,减弱“之”字形地变动程度。

  • AdaGrad

在神经网络的学习中,学习率的值很重要。学习率过小,会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能正确进行。
在关于学习率的有效技巧中,有一种被称为学习率衰减的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
逐渐减小学习率的想法,相当于将“全体”参数的学习率一起降低。而AdaGrad进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。
AdaGrad会为参数的每个元素适当地调整学习率。
h    ←    h    +    ∂ L ∂ W    ⊙    ∂ L ∂ W h\;\leftarrow\;h\;+\;\frac{\partial L}{\partial W}\;\odot\;\frac{\partial L}{\partial W} hh+WLWL
W    ←    W      −    η 1 h ∂ L ∂ W W\;\leftarrow\;W\;\;-\;\eta\frac1{\sqrt h}\frac{\partial L}{\partial W} WWηh 1WL
和前面的SGD一样,W表示要更新的权重参数,η表示学习率。这里新出现了变量h,它保存了以前的所有梯度值的平方和。然后,在更新参数时,通过乘以1/sqrt(h),就可以调整学习的尺度。这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。

AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,更新的幅度就越小。实际上,如果无止境地学习,更新量就会变成0,完全不再更新。为了改善这个问题,可以使用RMSProp方法。RMSProp方法并不是将过去所有的梯度一视同仁地相加,二十逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。这种操作从专业上讲,称为“指数移动平均”,呈指数函数式地减小过去的梯度的尺度。

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None

    def update(self, params, grads):
        if self.h is None:
        self.h = {}
        for key, val in params.items():
            self.h[key] = np.zeros_like(val)

    for key in params.key():
        self.h[key] += grads[key] * grads[key]
        params[key] -= self.lr * grads[key] / np.sqrt(self.h[key] + 1e-7)

这里需要注意的是,最后一行加上了微小值1e-7。这是为了防止当self.h[key]中有0时,将0用作除数的情况。在很多深度学习框架中,这个微小值也可以设定为参数,但这里我们用的是1e-7这个固定值。
现在,我们试着使用AdaGrad解决优化问题,如下图所示:

在这里插入图片描述

由上图结果可知,函数的取值高效地向着最小值移动。由于y轴方向上的梯度较大,因此刚开始变动较大,但是后面会根据这个较大的变动按比例调整,减小更新的步伐。因此,y轴方向上的更新程度被削弱,“之”字形的变动程度有所衰减。

  • Adam

Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起就是Adam方法的基本思路。
Adam是2015年提出的新方法。它的理论有些复杂,直观地讲,就是融合Momentum和AdaGrad的方法。通过组合前面两个方法的优点,有望实现参数空间的高速搜索。此外,进行超参数的“偏置校正”也是Adam的特征。

class Adam:
    
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None

    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)

        self.iter += 1
        lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)

        for key in params.keys():
            # self.m[key] = self.bata1*self.m[key] + (1-self.beta1)*grads[key]
            # self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])

            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)

            # unbias m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            # unbias b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            # params[key] += self.lr * unbias m / (np.sqrt(unbias b) + 1e-7)

在这里插入图片描述

在上图中,基于Adam的更新过程就像小球在碗中滚动一样。虽然Momentum也有类似的移动,但是相比之下,Adam的小球左右摇晃的程度有所减轻。这得益于学习的更新程度被适当地调整了。

Adam会设置3个超参数。一个是学习率(论文中以α出现),另外两个是一次momentum系数β1和二次momentum系数β2。根据论文,标准的设定值是β1为0.9,β2为0.999。设置了这些值后,大多数情况下都能顺利运行。

3. 优化器的选择问题

到目前为止,我们已经学习了4中更新参数的方法。我们来比较一下这4仲方法。

import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pylot as plt
from collections import OrderedDict
from common.optimizer import *

def f(x, y):
    return x**2 / 20.0 + y**2

def df(x, y):
    return x / 10.0, 2.0*y

init_pos = (-7.0, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0

optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["Adam"] = Adam(lr=0.3)

idx = 1

for key in optimizers:
    optimizer = optimizers[key]
    x_history = []
    y_history = []
    params['x'], params['y'] = init_pos[0], init_pos[1]

    for i in range(30):
        x_history.append(params['x'])
        y_history.append(params['y'])

        grads['x'], grads['y'] = df(params['x'], params['y'])
        optimizer.update(params, grads)

    x = np.arange(-10, 10, 0.01)
    y = np.arange(-5, 5, 0.01)

    # 生成网格点坐标矩阵
    X, Y = np.meshgrid(x, y)
    Z = f(X, Y)

    # for simple contour line
    mask = Z > 7
    Z[mask] = 0  # 当z大于0时为true,设置为0

    # plot
    plt.subplot(2, 2, idx)  # 两行两列,索引值为1的图
    idx += 1
    plt.plot(x_history, y_history, 'o-', color="red")
    plt.contour(X, Y, Z)  # 绘制等高线 contour和contourf都是画三维等高线图的,不同点在于contour()是绘制轮廓线,contourf()会填充轮廓
    plt.ylim(-10, 10)
    plt.xlim(-10, 10)
    plt.plot(0, 0, '+')
    #colorbar()
    #spring
    plt.title(key)
    plt.xlabel("x")
    plt.ylabel("y")

plt.show()


运行结果为:

在这里插入图片描述

根据上图所示,使用的优化方法不同参数更新的路径也不同。只看这个图的话,AdaGrad似乎是最好的,不过也要注意,结果会根据要解决的问题而变。并且,很显然,超参数(学习率等)的设定值不同,结果也会发生变化。目前并不存在能在所有问题中都表现良好的方法。这4种方法各有各的特点,都有各自擅长解决的问题和不擅长解决的问题。不过,越来越多的研究和技术人员喜欢用Adam优化。

4. 基于MNIST数据集的更新方法的比较

以手写数字识别为例,比较前面介绍的SGD、Momentum、AdaGrad、Adam这4仲方法,并确定不同的方法在学习进展上有多大程度的差异。
这个实验以一个5层神经网络为对象,其中每层有100个神经元。激活函数使用的是ReLU。
代码如下:

import os
import sys
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import *


# 0:读入MNIST数据集
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)

train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000

# 1:进行实验的设置
optimizers = {}
optimizer['SGD'] = SGD()
optimizer['Momentum'] = Momentum()
optimizer['AdaGrad'] = AdaGrad()
optimizer['Adam'] = Adam()
#optimizer['RMSprop'] = RMSprop()

networks = {}
train_loss = {}
for key in optimizer.keys():
    networks[key] = MultiLayerNet(
        input_size=784, hidden_size_list=[100, 100, 100, 100]
        output_size=10)
    trian_loss[key] = []

# 2:开始训练
for i in range(max_iterations):
    batch_mask = np.random.choice(trian_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    for key in optimziers.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimziers[key].update(networks[key].params, grads)

        loss = networks[key].loss(x_batch, t_batch)
        train_loss[key].append(loss)

    if i % 100 == 0:\
        print("===========" + "iteration:" + str(i) + "===========")
        for key in optimziers.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))

# 3.绘制图形
markers = {"SGD":"o", "Momentum":"x", "AdaGrad":"s", "Adam":"D"}
x = np.arange(max_iterations)
for key in optimizers.keys():
    plt.plot(x, smooth_curve(trian_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
plt.show()

运行结果如下:

在这里插入图片描述

从上图结果可知,与SGD相比,其他3种方法学习的更快,而且速度基本相同,仔细看的话,AdaGrad的学习进行得稍微快一点。这个实验需要注意的是,实验结果会随学习率等超参数、神经网络得结构(深度为几层等)得不同而发生变化。不过,一般而言,与SGD先比,其他方法学习得更快,有时最终得识别精度也更高。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值