当前位置: 首页 > news >正文

深度学习入门(五):学习相关的技巧

文章目录

  • 前言
  • 如何找到最优的参数
    • 随机梯度下降法(SGD)
      • 原理
      • 优缺点
    • Momentum
    • AdaGrad
    • Adam
    • 总结与对比
  • 权重初始值的设置
    • 全部设置为0可以吗?
    • 权重初始值对激活层分布的影响
  • 如何控制激活层的分布
    • batch normalization
    • batch normalization效果测试
  • 如何防止过拟合——正则化登场
    • 正则化方法一:权值衰减
    • 正则化方法二:dropout
      • 原理
      • 为什么dropout可以抑制过拟合
  • 超参数的验证

前言

本章我们将从:

  1. 如何找到最优的参数?(SGD、Momentum、AdaGrad、Adam)
  2. 参数/权重的初始值如何设置?(是否可以全部初始化为0?权重初始值对激活层分布的影响?)
  3. 怎么控制激活层的分布?(Batch Normalization)
  4. 怎么防止过拟合?(正则化:权值衰减、dropout)
  5. 如何高效寻找超参数?(随机采样 VS 网格搜索)

这5个问题入手,介绍一下学习相关的技巧。

如何找到最优的参数

神经网络学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化

随机梯度下降法(SGD)

原理

在梯度下降法一文中,我们已经学习过梯度下降法了。复习一下它的公式是:
w = w − η ∂ L ∂ w w = w - \eta\frac{\partial L}{\partial w} w=wηwL
啰嗦几句,因为梯度 ∂ L ∂ w \frac{\partial L}{\partial w} wL代表的是函数增长最快的方向,这里我们是希望损失函数的值最小,所以要沿着梯度的反方向走,因此是用减号。

用代码表示为:

class SGD:class __init__(self, lr=0.01):self.lr = lrdef update(self, params, grads):for key in params.keys():params[key] -= self.lr * grads[key]

实际调用时:

network = TwoLayerNet()
optimizer = SGD()for i in range(1000):x_batch, t_batch = get_mini_batch()grads = network.gradient(x_batch, t_batch)params = network.paramsoptimmizer.update(params, grads)

优缺点

  • 优点:简单、容易实现;
  • 缺点:在某些情况下搜索不高效,本质原因是损失函数值下降最快的方向 ≠ \neq =直接奔向最小值,它可能会产生震荡(在不同方向来回变换),从而导致收敛慢。

举个例子,假设我们想寻找下面这个函数的最小值:
f ( x , y ) = 1 20 x 2 + y 2 f(x,y) =\frac{1}{20} x^2+y^2 f(x,y)=201x2+y2
该函数的图像和等高线如下图:
在这里插入图片描述
该函数的梯度如下图所示(箭头的方向代表梯度的方向,箭头的长度代表梯度的模,即梯度向量的大小)。可以看到,该梯度的特征是:y轴方向大,x轴方向小
在这里插入图片描述
最终基于SGD搜索的效果如下,可以看到搜索过程呈“之”字型,这是一个相当低效的搜索路径。当函数的形状非均向,比如呈延伸状,搜索的效率就会非常低下。

为了解决上述的低效问题,我们接下来学习Momentum, AdaGrad和Adam三种方法,来取代SGD。

Momentum

先看一下数学公式:
v = α v − η ∂ L ∂ w w = w + v v = \alpha v- \eta\frac{\partial L}{\partial w}\\ w = w+v v=αvηwLw=w+v
和SGD的公式对比一下:
w = w − η ∂ L ∂ w w = w - \eta\frac{\partial L}{\partial w} w=wηwL
发现多了 v v v这一项。通过下面的代码理解一下 v v v的计算。初始化为0,保存了参数结构一样的数据,剩余的代码就是实现上面的数学公式。

class Momentum:def __init__(self, lr=0.1, momentum=0.9):self.lr = lrself.momentum = momentumself.v = Nonedef update(self, params, grads):if self.v is None:self.v = dict()for key, val in params.items():self.v[key] = np.zeros_like(val)for key, val in params.items():self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]params[key] += self.v[key]

看一下Momentum的效果如下图所示,可以发现“之”字型震荡的幅度变小了。怎么理解这个优化效果呢?(重要)

因为 v v v初始化为0,一开始 v v v保存的就是梯度的负方向 − η ∂ L ∂ w - \eta\frac{\partial L}{\partial w} ηwL。想想震荡过程的一个特点是,一会儿朝前一会儿往后,也就是说梯度的方向是相反的。那么在 v v v第一次更新的时候, v = α v − η ∂ L ∂ w v = \alpha v- \eta\frac{\partial L}{\partial w} v=αvηwL,式子的第一项是初始化的梯度,第二项当前的梯度(势必和之前方向相反),这时两者加和就出现了一个抵消的作用。

前例中,x轴方向受到的力虽然非常小,但是一直稳定的在受力,一直朝着同一个方向,慢慢会形成一个加速度的感觉~y轴方向受到的力虽然非常大,但是在来回震荡,一会儿朝前一会儿朝后,会相互抵消。因此和SGD相比,momentum可以更快的朝x轴的方向靠近,并减弱y轴方向的震荡,即“之”字震动的幅度。

回到Momentum这个词本身,它是动量的意思。它就像一个小球在斜面上滚动,小球是有惯性的,就算在某个方向上的受力有突然急剧的变化,它也不会立刻大幅的偏离轨道,而是“考虑”过去的方向和速度。

在这里插入图片描述

AdaGrad

Momentum是针对SGD震荡的缺点,引入了“惯性”减少了衰减。参数的更新中还有一个参数很重要,那就是学习率 η \eta η。学习率太大,可能导致学习太发散,不能正确学习;学习率太小,可能导致学习的太慢,收敛需要很长时间。

一种常用的技巧是「学习率衰减」,即在开始的时候多学,在后面的时候少学,先看公式:
h = h + ∂ L ∂ w ⊙ ∂ L ∂ w w = w − η ∗ 1 h ∂ L ∂ w h=h+\frac{\partial L}{\partial w} \odot \frac{\partial L}{\partial w}\\ w=w-\eta*\frac{1}{\sqrt{h}}\frac{\partial L}{\partial w} h=h+wLwLw=wηh 1wL
上式的 h h h存放了过去所有梯度值的平方和,最后更新参数时,如果过去更新的梯度平方和越大,则本次学习率越小。

class Adam:def __init__(self, lr=0.1, momentum=0.9):self.lr = lrself.h = Nonedef update(self, params, grads):if self.h is None:self.h = dict()for key, val in params.items():self.h[key] = np.zeros_like(val)for key, val in params.items():self.h[key] += grads[key]*grads[key]params[key] -= self.lr * grads[key] * 1/np.sqrt(self.h[key]+1e-7)

代码里最后一行加了一个 1 e − 7 1e-7 1e7,这是为了避免分母为0的时候出现报错。

下图为用AdaGrad求解的图示:
在这里插入图片描述
可以看到"之"字震荡的问题减弱了很多。这是因为前期学习的时候,y轴方向的梯度较大,那么在学习率衰减的作用下,后面会减小这个更新的步伐,因此,y轴方向上更新的程度被减弱。

Adam

如果结合Momentum的思路和AdaGrad的思路在一起会怎么样呢?Adam就是这样一种方法。我读的这本书没有对它进行展开说明,但有一张图可以显示Adam的效果:

在这里插入图片描述

总结与对比

注意,目前不存在在所有问题中都表现最好的方法。

在这里插入图片描述

权重初始值的设置

全部设置为0可以吗?

先说结论,不可以。

假设我们有一个2层的神经网络,如果初始权重为0,那么正向传播时,第二层的输入全部为0,通过下图回顾一下乘法节点和加法节点的反向传播,如果第二层正向传播的输入全部是相同的值,那么反向传播时,所有的权重都会被更新为相同的值(下图的 x x x y y y都相等,那么反向传播时,值也一样),这使得神经网络拥有许多不同权重的意义丧失了
在这里插入图片描述
因此,不仅是全部设置为0不可以,全部设置为一个相同的非零数也不可以,因为同样会有权重相同的问题。

为了瓦解“权重均一化” / “权重的对称结构”,必须随机生成初始权重

权重初始值对激活层分布的影响

这里作者做了一个实验,通过改变权重初始值的分布,观察激活层的分布,代码如下:

import numpy as np
import matplotlib.pyplot as pltdef sigmoid(x):return 1 / (1 + np.exp(-x))x = np.random.randn(1000, 100) # 1000个数据
node_num = 100        # 各隐藏层的节点(神经元)数
hidden_layer_size = 5 # 隐藏层有5层
activations = {}      # 激活值的结果保存在这里for i in range(hidden_layer_size):if i != 0:x = activations[i-1]w = np.random.randn(node_num, node_num) * 1 # 生成一个随机权重矩阵 w,其维度为 node_num × node_num(即行数和列数都等于 node_num),服从标准正态分布,最后的1控制的是标准差。z = np.dot(x, w)a = sigmoid(z)   # sigmoid函数activations[i] = a

然后可视化:

# 绘制直方图
for i, a in activations.items():plt.subplot(1, len(activations), i+1)plt.title(str(i+1) + "-layer")plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

最后得到下图:可以看到这是一个偏向0和1的分布。回想一下sigmoid的图像是S型曲线,随着输出不断靠近0或1,导数不断的趋近于0。因此,偏向0和1的分布会导致反向传播过程中,梯度不断减小,最后消失。这个问题称为梯度消失(gradient vanishing)

在这里插入图片描述

在这里插入图片描述

如果我们修改一下权重初始值的分布会怎样呢?将上面的标准差从1改为0.01试试看:

# w = np.random.randn(node_num, node_num) * 1 # 生成一个随机权重矩阵 w,其维度为 node_num × node_num(即行数和列数都等于 node_num),服从标准正态分布,最后的1控制的是标准差。
w = np.random.randn(node_num, node_num) * 0.01

得到下图:现在的分布集中在0.5附近;

  • 好消息:因为分布不再集中在0和1附近,所以不会再出现梯度消失的问题了;
  • 坏消息激活值的分布有所偏向,意味着神经网络的表现力会受限。因为如果不同的神经元都输出相近的值,那许多神经元也就没有存在的意义了。比如100个神经元输出的结果都基本相同,那么和使用1个神经元来表达,基本是同样的事情。
    在这里插入图片描述
    为了使各层激活值呈现具有相同广度的分布,Xavier Glorot等人推导了一些比较好用的初始权重的分布,俗称“Xavier初始值”:
node_num = 100 # 前一层的节点数
w = np.random.randn(node_num, node_num) / np.sqrt(node_num)

从公式可以看到,前一层的节点越多,要设定的目标节点的初始权重就越小。
在这里插入图片描述
最终分布的表现:
在这里插入图片描述

激活函数不同,适配的初始权重分布也不同,作者总结了一下,当激活函数使用ReLU时,权重初始值使用He初始值,当激活函数为sigmoid或tanh等S型曲线函数时,初始值使用Xavier初始值。这是目前的最佳实践

如何控制激活层的分布

上面介绍了通过调整权重的初始值,可以使得各层激活值的分布拥有适当的广度,从而使得网络有充分的表现力。那么如果不通过权重初始值的调整,而是强制的“控制激活值的分布”会如何呢?Batch normalization就是基于这样的思路诞生的!

batch normalization

如下图所示,batch norm通过对神经网络中插入对数据分布进行正规化的层,称为batch norm层,使得激活值分布具有相当的广度。
在这里插入图片描述
具体而言,是在学习时以mini-batch为单位,按mini-batch进行正规化(使数据分布的均值为0,方差为1),数学公式如下:

m m m表示一个mini-batch里的输入数据数量,首先求了mini-batch里输入数据的均值,然后求了方差,最后对每个数字进行了正规化计算。最后一个式子里分母有个 ϵ \epsilon ϵ,是一个极小的值,为了防止分母为0的情况。
在这里插入图片描述
还没有结束,接着batch norm会对正规化后的数据进行平移和缩放,
这里的 γ \gamma γ β \beta β是参数,一开始 γ = 1 \gamma=1 γ=1 β = 0 \beta=0 β=0,然后再通过学习调整到合适的值。

batch normalization效果测试

最后作者做了一个实验,如下图所示,实现代表加入了batch normalization,虚线表示没有batch normalization。几乎所有的情况下都是使用了batch normalization后学习的更快。同时也观测到,在某些不适用batch normalization的情况下,如果不设置一个合适的权重初始值,学习将无法正常进行并且,加入batch normalization后,对权重初始值变得健壮(不那么依赖权重初始值)

在这里插入图片描述

如何防止过拟合——正则化登场

第一次看到正则化这个词觉得非常抽象,完全联想不到过拟合,去问了一下GPT,才稍微能理解一点为什么取这个名字了。

正则化是从英语单词"regularization"翻译过来的,有“规则化”的意思,核心思想是通过引入某项规则,使得系统或解变得更加“规范”,更稳定,即防止过度复杂化(过拟合)。姑且这么理解吧~

正则化方法一:权值衰减

很多过拟合的原因是权重参数过大,因此权值衰减的思路应运而生。它的核心思想是,对大的权重进行惩罚,抑制过拟合~

复习一下,神经网络的学习是使得损失函数的值最小,这时,如果给损失函数加上权重的平方范数(L2范数,权值衰减也可称为L2正则化 1 2 λ w 2 \frac{1}{2}\lambda w^2 21λw2(这里的 1 2 \frac{1}{2} 21是用于将求导结果调整为 λ w \lambda w λw的调整用常量),这样权重越大,惩罚就越大。

λ \lambda λ是用于调整惩罚程度的项,越大则惩罚的越重。

直观看一下测试的效果:

下图是过拟合的典型表现:
过拟合case
下图是加了权值衰减后的变化:可以看到,虽然训练数据和测试数据的识别精度仍然有一定差距,但对比上图,gap已经小了很多,且训练数据的精度不再是100%了,这说明过拟合受到了抑制
在这里插入图片描述

正则化方法二:dropout

原理

当神经网络变得很复杂时,权值衰减抑制过拟合的效果就变得十分有限了,这种情况下一般采用dropout方法。它的核心思想是,在训练时,随机筛选并删除一些神经元,被删除的神经元不再传递信号

如下图所示:

  • 训练时,每传递一次数据,都要随机选择删除的神经元;
  • 测试时,虽然会传递所有神经元的信号,但是对于每一个神经元的输出,都要乘以它训练时被删除的概率;
    在这里插入图片描述

看一下代码实现:正向传播时,如果是做训练,则通过self.mask以False的形式保存要删除的神经元,然后选择性的传输神经元数据;如果是做测试,则所有的神经元数据都传播,但是都会乘以一个被删除的比例;反向传播时,被删除的神经元将会停止传播。

class dropout:def __init__(self, dropout_ratio=0.5):self.dropout_ratio = dropout_ratioself.mask = Nonedef forward(self, x, train_flag=True):if train_falg:self.mask = np.random.rand(*x.shape) > self.dropout_ratioreturn x * self.maskelse:return x * (1-self.dropout_ratio)def backward(self, dout):return dout * self.mask

核心看下面这行代码的逻辑:它以False的形式保存了要删除的神经元。

self.mask = np.random.rand(*x.shape) > self.dropout_ratio

最终看一下实验结果:

在这里插入图片描述

可以看到训练精度和测试精度的gap同样变小了,而且训练的精度同样没有达到100%。这说明即使是表现力强的网络(该测试用的是7层网络,每层有100个神经元),也可以被抑制过拟合。

为什么dropout可以抑制过拟合

训练时随机删除神经元的操作,使得每次都在用不同的模型进行学习。而推理时,通过对每个神经元的输出乘以被删除的比例,相当于对不同模型的结果取了平均值。

这与机器学习中集成学习的思想(训练多个学习器,推理时再取多个学习器结果的平均值)不谋而合。

也就是说,dropout将集成学习的效果(模拟地)通过一个网络实现了。

超参数的验证

神经网络中,除了权重、偏置这类希望模型自动学习的参数外,剩下的就是超参数,是需要我们人工指定的,比如各层神经元的数量、batch大小、更新时的学习率、权值衰减系数、dropout rate等。

这一小节作者介绍了高效寻找超参数的方法,我先提炼2个核心注意的点:
1. 验证超参数不能直接在训练数据上验证,而是要在单独的验证数据上测试;
2. 超参数寻优时,与网格搜索这类有规律的搜索相比,随机采样的方式往往更好。

具体寻优的步骤:

  1. 设定超参数的寻优范围;
  2. 从设定的超参数范围中,随机采样;
  3. 使用上面的到的超参数进行学习,在验证数据上评估识别精度;
  4. 通过识别精度的结果,缩小测试范围。

这里第2步为了节省计算资源,提升搜索效率,通常设置较小的epoch(1个epoch = 模型完整的看完一个训练集,通常一个模型要训练几十个甚至上百个epoch,才能收敛至较好的性能)。

相关文章:

  • 蓝桥杯 18. 积木
  • 基于yolov11的打电话玩手机检测系统python源码+pytorch模型+评估指标曲线+精美GUI界面
  • 防止交叉验证中的数据泄露:提升模型在实际环境中的性能
  • React状态管理
  • 攻防世界-php伪协议和文件包含
  • 先滤波再降采样 还是 先降采样再滤波
  • JavaSE核心知识点01基础语法01-02(基本数据类型、运算符、运算符优先级)
  • 国产海光DCU及超算平台深度解析
  • Vue项目安全实践指南:从输入验证到状态管理的全方位防护
  • 笔记本电脑升级计划(2017———2025)
  • Springclound常用五大组件及其使用原理
  • [人机交互]理解与概念化交互
  • ARM介绍及其体系结构
  • Linux55yum源配置、本机yum源备份,本机yum源配置,网络Yum源配置,自建yum源仓库
  • SpringMVC 框架核心知识点详解与实战
  • 哈希算法、搜索算法与二分查找算法在 C# 中的实现与应用
  • 多语言笔记系列:Polyglot Notebooks 中使用 xUnit 单元测试
  • 数据结构实验8.1:图的基本操作
  • 第16章 监控和排除日志记录错误
  • 1. 设计哲学与核心价值
  • 同为“东部重要中心城市”后交出首份季报:宁杭苏表现如何?
  • 商务部新闻发言人就中美经贸高层会谈答记者问
  • 中俄合拍电影《红丝绸》将于今年9月在中国上映
  • 金沙记忆|元谋龙街渡:是起点也是终点
  • 国内外数十支搜救犬队伍齐聚三明,进行废墟搜救等实战
  • 伊朗公布新型弹道导弹,“萨德”系统无法拦截