神经网络

Lecture 6 训练神经网络(上)

2019-01-30  本文已影响2人  HRain

目前我们已有的知识有:

接下来需要做的就是训练一个神经网络。主要有以下工作:

本课重点

1 激活函数

在全连接层或者卷积层,输入数据与权重相乘后累加的结果送给一个非线性函数,即激活函数(activation function)。每个激活函数的输入都是一个数字,然后对其进行某种固定的数学操作。

图1 神经元数学模型中的激活函数
下面是在实践中可能遇到的几种激活函数: 图2 常见的激活函数

Sigmoid函数

数学公式:\sigma(x) = 1 / (1 + e^{-x})

求导公式:\frac{d\sigma(x)}{dx} = \left( 1 - \sigma(x) \right) \sigma(x)(不小于0)

特点:把输入值“挤压”到0到1范围内

图像: 图3 sigmoid函数

Sigmoid函数把输入的实数值“挤压”到0到1范围内,很大的负数变成0,很大的正数变成1。历史上,sigmoid函数很常用,因为它对神经元的激活频率有良好的解释:从完全不激活(0)到假定最大频率处的完全饱和(saturated)的激活(1)。然而现在sigmoid函数已经很少使用了,因为它有三个主要缺点:

  1. Sigmoid函数饱和时使梯度消失:当神经元的激活在接近0或1处时(即门单元的输入过或过大时)会饱和:在这些区域,梯度几乎为0。在反向传播的时候,这个局部梯度要与整个损失函数关于该门单元输出的梯度相乘。因此,如果局部梯度非常小,那么相乘的结果也会接近零,这会“杀死”梯度,几乎就有没有信号通过神经元传到权重再到数据了。还有,为了防止饱和,必须对于权重矩阵初始化特别留意。比如,如果初始权重过大,那么大多数神经元将会饱和,导致网络就几乎不学习了。
  2. Sigmoid函数的输出不是零中心的:这个性质会导致神经网络后面层中的神经元得到的数据不是零中心的。这一情况将影响梯度下降的运作,因为如果输入神经元的数据总是正数(比如在\sigma(\sum_{i}w_ix_i+b)中每个输入x都有x > 0),那么关于w的梯度在反向传播的过程中,将会要么全部是正数,要么全部是负数(根据该Sigmoid门单元的回传梯度来定,回传梯度可正可负,而\frac{d\sigma}{dW}=X^T \cdot\sigma' 在X为正时恒为非负数)。这将会导致梯度下降权重更新时出现z字型的下降。该问题相对于上面的神经元饱和问题来说只是个小麻烦,没有那么严重。
  3. 指数型计算量比较大。

Tanh函数

数学公式:\tanh(x) = 2 \sigma(2x) -1

特点:将实数值压缩到[-1,1]之间

图像: 图4 tanh函数

和sigmoid神经元一样,它也存在饱和问题,但是和sigmoid神经元不同的是,它的输出是零中心的。因此,在实际操作中,tanh非线性函数比sigmoid非线性函数更受欢迎。注意tanh神经元是一个简单放大的sigmoid神经元。

ReLU函数

数学公式:f(x) = \max(0, x)

特点:一个关于0的阈值

图像:

图5 ReLU函数 优点:ReLU只有负半轴会饱和;节省计算资源,不含指数运算,只对一个矩阵进行阈值计算;更符合生物学观念;加速随机梯度下降的收敛,Krizhevsky论文指出比sigmoid和tanh函数快6倍之多,据称这是由它的线性,非饱和的公式导致的。

缺点:仍有一半会饱和;非零中心;训练时,ReLU单元比较脆弱并且可能“死掉”。举例来说,当一个很大的梯度流过ReLU的神经元的时候,由于梯度下降,可能会导致权重更新到一种特别的状态(比如大多数的w都小于0),在这种状态下神经元将无法被其他任何数据点再次激活。如果这种情况发生,那么从此所有流过这个神经元的梯度将都变成0。也就是说,这个ReLU单元在训练中将不可逆转的死亡,因为这导致了数据多样化的丢失。例如,如果学习率设置得太高(本来大多数大于0的w更新后都小于0了),可能会发现网络中40%的神经元都会死掉(在整个训练集中这些神经元都不会被激活)。通过合理设置学习率,这种情况的发生概率会降低。

Leaky ReLU

公式:f(x) = \mathbb{1}(x < 0) (\alpha x) + \mathbb{1}(x>=0) (x)\alpha是小常量

特点:解决“ReLU死亡”问题,x<0时给出一个很小的梯度值,比如0.01。

图像:

图6 Leaky ReLU,max(0.01x, x) 有些研究者的论文指出这个激活函数表现很不错,但是其效果并不是很稳定。Kaiming He等人在2015年发布的论文Delving Deep into Rectifiers中介绍了一种新方法PReLU,把负区间上的斜率当做每个神经元中的一个参数,然而无法确定该激活函数在不同任务中均有益处。

指数线性单元(Exponential Linear Units,ELU)

公式:f(x)=\begin{cases} x & if \space\space x>0 \\ \alpha(exp(x)-1) & otherwise \end{cases}

特点:介于ReLU和Leaky ReLU之间

图像: 图7 ELU

具有ReLU的所有优点,但是不包括计算量;介于ReLU和Leaky ReLU之间,有负饱和的问题,但是对噪声有较强的鲁棒性。

Maxout

公式:max(w_1^Tx+b_1, w_2^Tx + b_2)

特点:是对ReLU和leaky ReLU的一般化归纳

对于权重和数据的内积结果不再使用非线性函数,直接比较两个线性函数。ReLU和Leaky ReLU都是这个公式的特殊情况,比如ReLU就是当w_1=1,b1=0的时候。Maxout 拥有ReLU单元的所有优点(线性操作和不饱和),而没有它的缺点(死亡的ReLU单元)。然而和ReLU对比,它每个神经元的参数数量增加了一倍,这就导致整体参数的数量激增。

实际应用用ReLU函数。注意设置好学习率,或许可以监控你的网络中死亡的神经元占的比例。如果单元死亡问题困扰你,就试试Leaky ReLU或者Maxout,不要再用sigmoid了。也可以试试tanh,但是其效果应该不如ReLU或者Maxout。

2 数据预处理

关于数据预处理有3个常用的符号,数据矩阵X,假设其尺寸是[N x D]N是数据样本的数量,D是数据的维度)。

减均值(Mean Subtraction)

均值减法是数据预处理最常用的形式。它对数据中每个独立特征减去平均值,在每个维度上都将数据的中心都迁移到原点。在numpy中,该操作可以通过代码X -= np.mean(X, axis=0)实现。而对于图像,更常用的是对所有像素都减去一个值,可以用X -= np.mean(X)实现,也可以在3个颜色通道上分别操作。具体来讲,假如训练数据是50000张32x32x3的图片,第一种做法是减去均值图像,即将每张图片拉成长为3072的向量,50000x3072的矩阵按列求平均,得到一个含有3072个数的均值图像,训练集测试集验证集都要减去这个均值,AlexNet是这种方式;第二种做法是按照通道求平均,RGB三个通道每个通道一个均值,即每张图片的3072个数中,RGB各有32x32个数,要在50000x32x32个数中求一个通道的均值,最终的均值有3个数字,然后所有图片每个通道都要减去对应的通道均值,VGGNet是这种方式。

之所以执行减均值操作,是因为解决输入数据大多数都是正或者负的问题。虽然经过这种操作,数据变成零中心的,但是仍然只能第一层解决sigmoid非零均值的问题,后面会有更严重的问题。

归一化(Normalization)

归一化是指将数据的所有维度都归一化,使其数值范围都近似相等。有两种常用方法可以实现归一化。第一种是先对数据做零中心化(zero-centered)处理,然后每个维度都除以其标准差,实现代码为X /= np.std(X, axis=0)。第二种方法是对每个维度都做归一化,使得每个维度的最大和最小值是1和-1。这个预处理操作只有在确信不同的输入特征有不同的数值范围(或计量单位)时才有意义,但要注意预处理操作的重要性几乎等同于学习算法本身。在图像处理中,由于像素的数值范围几乎是一致的(都在0-255之间),所以进行这个额外的预处理步骤并不是很必要。

图8 原数据与减均值和归一化后的数据对比 左边:原始的2维输入数据。中间:在每个维度上都减去平均值后得到零中心化数据,现在数据云是以原点为中心的。右边:每个维度都除以其标准差来调整其数值范围,红色的线指出了数据各维度的数值范围。在中间的零中心化数据的数值范围不同,但在右边归一化数据中数值范围相同。

主成分分析(PCA)

这是另一种机器学习中比较常用的预处理形式,但在图像处理中基本不用。在这种处理中,先对数据进行零中心化处理,然后计算协方差矩阵,它展示了数据中的相关性结构。

# 假设输入数据矩阵X的尺寸为[N x D]
X -= np.mean(X, axis = 0) # 对数据进行零中心化(重要)
cov = np.dot(X.T, X) / X.shape[0] # 得到数据的协方差矩阵,DxD

数据协方差矩阵的第(i, j)个元素是数据第i个和第j个维度的协方差。具体来说,该矩阵的对角线上的元素是方差。还有,协方差矩阵是对称和半正定的。我们可以对数据协方差矩阵进行SVD(奇异值分解)运算。

U,S,V = np.linalg.svd(cov)

U的列是特征向量,S是装有奇异值的1维数组(因为cov是对称且半正定的,所以S中元素是特征值的平方)。为了去除数据相关性,将已经零中心化处理过的原始数据投影到特征基准上:

Xrot = np.dot(X,U) # 对数据去相关性

np.linalg.svd的一个良好性质是在它的返回值U中,特征向量是按照特征值的大小排列的。我们可以利用这个性质来对数据降维,只要使用前面的小部分特征向量,丢弃掉那些包含的数据没有方差的维度,这个操作也被称为 主成分分析(Principal Component Analysis 简称PCA)降维:

Xrot_reduced = np.dot(X, U[:,:100]) # Xrot_reduced 变成 [N x 100]

经过上面的操作,将原始的数据集的大小由[N x D]降到了[N x 100],留下了数据中包含最大方差的的100个维度。通常使用PCA降维过的数据训练线性分类器和神经网络会达到非常好的性能效果,同时还能节省时间和存储器空间。

有一问题是为什么使用协方差矩阵进行SVD分解而不是使用原X矩阵进行?其实都是可以的,只对数据X(可以不是方阵)进行SVD分解,做PCA降维(避免了求协方差矩阵)的话一般用到的是右奇异向量V,即V的前几列是需要的特征向量(注意np.linalg.svd返回的是V.T)。X是NxD,则U是NxN,V是DxD;而对协方差矩阵(DxD)做SVD分解用于PCA降维的话,可以随意取左右奇异向量U、V(都是DxD)之一,因为两个向量是一样的。

白化(Whitening)

最后一个在实践中会看见的变换是白化(whitening)。白化操作的输入是特征基准上的数据,然后对每个维度除以其特征值来对数值范围进行归一化。该变换的几何解释是:如果数据服从多变量的高斯分布,那么经过白化后,数据的分布将会是一个均值为零,且协方差相等的矩阵。该操作的代码如下:

# 对数据进行白化操作:
# 除以特征值 
Xwhite = Xrot / np.sqrt(S + 1e-5)

警告:夸大的噪声。注意分母中添加了1e-5(或一个更小的常量)来防止分母为0。该变换的一个缺陷是在变换的过程中可能会夸大数据中的噪声,这是因为它将所有维度都拉伸到相同的数值范围,这些维度中也包含了那些只有极少差异性(方差小)而大多是噪声的维度。在实际操作中,这个问题可以用更强的平滑来解决(例如:采用比1e-5更大的值)。


图9 PCA和白化

左边是二维的原始数据。中间:经过PCA操作的数据。可以看出数据首先是零中心的,然后变换到了数据协方差矩阵的基准轴上。这样就对数据进行了解相关(协方差矩阵变成对角阵)。右边:每个维度都被特征值调整数值范围,将数据协方差矩阵变为单位矩阵。从几何上看,就是对数据在各个方向上拉伸压缩,使之变成服从高斯分布的一个数据点分布。

我们可以使用CIFAR-10数据将这些变化可视化出来。 图10 特征、PCA和白化可视化

最左:一个用于演示的图片集合,含49张图片。
左二:3072个特征向量中的前144个。靠前面的特征向量解释了数据中大部分的方差。
第三张是49张经过了PCA降维处理的图片,只使用这里展示的这144个特征向量。为了让图片能够正常显示,需要将144维度重新变成基于像素基准的3072个数值。因为U是一个旋转,可以通过乘以U.transpose()[:144,:]来实现,然后将得到的3072个数值可视化。可以看见图像变得有点模糊了,然而,大多数信息还是保留了下来。
最右:将“白化”后的数据进行显示。其中144个维度中的方差都被压缩到了相同的数值范围。然后144个白化后的数值通过乘以U.transpose()[:144,:]转换到图像像素基准上。

实际应用

实际上在卷积神经网络中并不会采用PCA和白化,对数据进行零中心化操作还是非常重要的,对每个像素进行归一化也很常见。

注意:进行预处理很重要的一点是:任何预处理策略(比如数据均值)都只能在训练集数据上进行计算,然后再应用到验证集或者测试集上。一个常见的错误做法是先计算整个数据集图像的平均值然后每张图片都减去平均值,最后将整个数据集分成训练/验证/测试集。正确的做法是先分成训练/验证/测试集,只是从训练集中求图片平均值,然后各个集(训练/验证/测试集)中的图像再减去这个平均值。

3 权重初始化

开始训练网络之前,还需要初始化网络的参数。

全零初始化

对一个两层的全连接网络,如果输入给网络的所有参数都是0会怎样?这种做法是错误的。因为如果网络中的每个神经元都计算出同样的输出,然后它们就会在反向传播中计算出同样的梯度,从而进行同样的参数更新。换句话说,如果权重被初始化为同样的值,神经元之间就失去了不对称性的源头。

小随机数初始化

现在权重初始值要非常接近0又不能等于0。解决方法就是将权重初始化为很小的数值,以此来打破对称性。其思路是:如果神经元刚开始的时候是随机且不相等的,那么它们将计算出不同的更新,并将自身变成整个网络的不同部分。实现方法是:W = 0.01 * np.random.randn(D,H)。其中randn函数是基于零均值和标准差的一个高斯分布来生成随机数的。

小随机数初始化在简单的网络中效果比较好,但是网络结构比较深的情况不一定会得到好的结果。比如一个10层的全连接网络,每层500个神经元,使用tanh激活函数,用小随机数初始化。代码与输出图像如下:

import numpy as np
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

# 假设一些高斯分布单元
D = np.random.randn(1000, 500)
hidden_layer_sizes = [500]*10  # 隐藏层尺寸都是500,10层
nonlinearities = ['tanh']*len(hidden_layer_sizes)  # 非线性函数都是用tanh函数

act = {'relu': lambda x: np.maximum(0, x), 'tanh': lambda x: np.tanh(x)}
Hs = {}
for i in range(len(hidden_layer_sizes)):
    X = D if i == 0 else Hs[i-1]  # 当前隐藏层的输入
    fan_in = X.shape[1]
    fan_out = hidden_layer_sizes[i]
    W = np.random.randn(fan_in, fan_out) * 0.01  # 权重初始化

    H = np.dot(X, W)  # 得到当前层输出
    H = act[nonlinearities[i]](H)  # 激活函数
    Hs[i] = H  # 保存当前层的结果并作为下层的输入

# 观察每一层的分布
print('输入层的均值:%f 方差:%f'% (np.mean(D), np.std(D)))
layer_means = [np.mean(H) for i,H in Hs.items()]
layer_stds = [np.std(H) for i,H in Hs.items()]
for i,H in Hs.items():
    print('隐藏层%d的均值:%f 方差:%f' % (i+1, layer_means[i], layer_stds[i]))

# 画图
plt.figure()
plt.subplot(121)
plt.plot(list(Hs.keys()), layer_means, 'ob-')
plt.title('layer mean')
plt.subplot(122)
plt.plot(Hs.keys(), layer_stds, 'or-')
plt.title('layer std')

# 绘制分布图
plt.figure()
for i,H in Hs.items():
    plt.subplot(1, len(Hs), i+1)
    plt.hist(H.ravel(), 30, range=(-1,1))

plt.show()
图11 小权重每层输出均值方差与分布

可以看到只有第一层的输出均值方差比较好,输出接近高斯分布,后面几层均值方差基本为0。这样导致的后果是正向传播的激活值基本为0,反向传播时就会计算出非常小的梯度(因权重的梯度就是层的输入,输入接近0,梯度接近0),参数基本不会更新。

现在假如上面的例子不用小随机数,即W = np.random.randn(fan_in, fan_out) * 1,此时会怎样呢? 此时,由于权重较大并且使用的tanh函数,所有神经元都会饱和,输出为+1或-1,梯度为0。如下图所示,均值在0附近波动,方差较大在0.98附近波动,神经元输出大多为+1或-1。

图12 大权重每层输出

Xavier/He初始化(校准方差)

上面的分析看到,权重过小网络崩溃,权重过大网络饱和,所以都在研究出一种合理的初始化方式。一种很好的经验是使用Xavier初始化:
W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in)
这是Glorot等在2010年发表的论文。这样就保证了网络中所有神经元起始时有近似同样的输出分布。实践经验证明,这样做可以提高收敛的速度。原理如下:

假设神经元的权重w与输入x的内积为s = \sum_i^n w_i x_i,这是还没有进行非线性激活函数运算之前的原始数值。此时s的方差:
\begin{align} \text{Var}(s) &= \text{Var}(\sum_i^n w_ix_i) \\ &= \sum_i^n \text{Var}(w_ix_i) \\ &= \sum_i^n [E(w_i)]^2\text{Var}(x_i) + E[(x_i)]^2\text{Var}(w_i) + \text{Var}(x_i)\text{Var}(w_i) \\ &= \sum_i^n \text{Var}(x_i)\text{Var}(w_i) \\ &= n \text{Var}(w) \text{Var}(x) \end{align}
前三步使用的是方差的性质(累加性、独立变量相乘);第三步中,假设输入和权重的均值都是0,即E[x_i] = E[w_i] = 0,但是ReLU函数中均值应该是正数。在最后一步,我们假设所有的w_i,x_i都服从同样的分布。从这个推导过程我们可以看见,如果想要s有和输入x一样的方差,那么在初始化的时候必须保证每个权重w的方差是1/n。又因为对于一个随机变量X和标量a,有\text{Var}(aX) = a^2\text{Var}(X),这就说明可以让w基于标准高斯分布(方差为1)取样,然后乘以a = \sqrt{1/n},即\text{Var}( \sqrt{1/n}\cdot w) = 1/n\text{Var}(w)=1/n,此时就能保证\text{Var}(s) =\text{Var}(x)。代码为:W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in),其中fan_in就是上文的n。(不过作者在论文中推荐的是:
W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in + fan_out),使\text{Var}(w) = 2/(n_{in} + n_{out}),其中n_{in}, n_{out}是前一层和后一层中单元的个数,这是基于妥协和对反向传播中梯度的分析得出的结论。)
输出结果为:

图13 校准方差后的输出 图上可以看出,后面几层的输入输出分布很接近高斯分布。

但是使用ReLU函数这种关系会被打破,同样w使用单位高斯并且校准方差,然而使用ReLU函数后每层会消除一半的神经元(置0),结果会使方差每次减半,会有越来越多的神经元失活,输出为0的神经元越来越多。如下图所示:

图14 将tanh换成relu函数,其他不变 解决方法是W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in/2)。因为每次有一半的神经元失活,校准时除2即可,这样得到的结果会比较好。这是2015年何凯明的论文Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification提到的方法,这个形式是神经网络算法使用ReLU神经元时的当前最佳推荐。结果如下: 图15 使用ReLU函数校准后的输出

稀疏初始化

另一个处理非标定方差的方法是将所有权重矩阵设为0,但是为了打破对称性,每个神经元都同下一层固定数目的神经元随机连接(其权重数值由一个小的高斯分布生成)。一个比较典型的连接数目是10个。

偏置(biases)的初始化:通常将偏置初始化为0。

实际应用

合适的初始化设置仍然是现在比较活跃的研究领域,经典的论文有:

当前的推荐是使用ReLU激活函数,并且使用w = np.random.randn(n) * sqrt(2.0/n)来进行权重初始化,n是上一层神经元的个数,这是何凯明的论文得出的结论,也称作He初始化。

4 批量归一化(Batch Normalization)

概述

批量归一化是loffe和Szegedy最近才提出的方法,该方法一定程度解决了如何合理初始化神经网络这个棘手问题,其做法是让激活数据在训练开始前通过一个网络,网络处理数据使其服从标准高斯分布。因为归一化是一个简单可求导的操作,所以上述思路是可行的。在实现层面,应用这个技巧通常意味着全连接层(或者是卷积层,后续会讲)与激活函数之间添加一个BatchNorm层。在神经网络中使用批量归一化已经变得非常常见,在实践中使用了批量归一化的网络对于不好的初始值有更强的鲁棒性。

原理

具体来说,我们希望每一层网络的输入都近似符合标准高斯分布, 考虑有N个激活数据的小批量输入,每个输入x有D维,即x = (x^{(1)} ...x^{(d)}),那么对这个小批量数据的每个维度进行归一化,使符合单位高斯分布,应用下面的公式:
\hat{x}^{(k)} =\frac{x^{(k)}-\text{E}[x^{(k)}]}{\sqrt{\text{Var}[x^{(k)}]}}

但是我们并不确定是否要给非线性函数比如tanh输入单位高斯数据,批量归一化会把输入限制在非线性函数的线性区域,有时候我们并不想没有一点饱和,所以希望能控制饱和程度,即在归一化完成后,用两个参数去缩放和平移归一化后的激活数据:
y^{(k)} = \gamma ^{(k)}\hat{x} ^{(k)}+\beta ^{(k)}这两个参数可以在网络中学习,并且能实现我们想要的效果。的确 ,通过设置:\gamma ^{(k)}=\sqrt{\text{Var}[x^{(k)}]}, \beta ^{(k)}=\text{E}[x^{(k)}]可以恢复原始激活数据,如果这样做的确最优的话。现在网络有了为了让网络达到较好的训练效果而去学习控制让tanh具有更高或更低饱和程度的能力。

在批量归一化设置中,每个步骤都基于整个训练集,我们将使用整个训练集来归一化激活数据。 然而,当使用随机优化时,这是不切实际的。 因此,我们进行一个简化:由于我们在SGD中使用小批量,每个小批量都可以得到激活数据的均值和方差的估计。 这样,用于归一化的数据完全可以参与梯度反向传播。

批量归一化的思想:考虑一个尺寸为m的小批量B。由于归一化被独立地应用于激活数据x的每个维度,因此让我们关注特定激活数据维度x(k)并且为了清楚起见省略k。 在小批量中共有m个这种激活数据维度x(k)\text{B} ={x_{1...m}}

归一化后的值为:\hat{x}_{1...m}

线性转化后的值为:y_{1...m}

这种线性转化是批量归一化转化\text{BN}_{γ,β} : x_{1...m} → y_{1...m}

于是,我们的小批量激活数据\text{B} ={x_{1...m}} 通过BN层,有两个参数需要学习:γ,β. (\epsilon是为了维持数值稳定在小批量方差上添加的小常数)。 该BN层的输出为:\{y_i=\text{BN}_{γ,β}(x_i)\},i=1...m,该层的计算有:

优势

注意:测试时不使用小批量中计算的均值和方差, 相反,使用训练期间激活数据的一个固定的经验均值,例如可以使用在训练期间的平均值作为估计。

最后一句话总结:批量归一化可以理解为在网络的每一层之前都做预处理,将输入数据转化为单位高斯数据或者进行平移伸缩,只是这种操作以另一种方式与网络集成在了一起。

层归一化(Layer Normalization)

事实证明,批量归一化能使网络更容易训练,但是对批量的大小有依赖性,批量太小效果不好,批量太大又受到硬件的限制。所以在对输入批量大小具有上限的复杂网络中不太有用。目前已经提出了几种批量归一化的替代方案来缓解这个问题,其中一个就是层归一化。 我们不再对这个小批量进行归一化,而是对特征向量进行归一化。 换句话说,当使用层归一化时,基于该特征向量内的所有项的总和来归一化对应于单个数据点。层归一化测试与训练的行为相同,都是计算每个样本的归一。可用于循环神经网络。

卷积神经网络中

图16 CNN中各种归一化示意图对比

5 监控学习过程

  1. 使用小参数进行初始化,使正则损失为0,确保得到的损失值与期望一致。例如,对于一个输入CIFAR-10的Softmax分类器,一般期望它的初始损失值是2.302,这是因为初始时预计每个类别的概率是0.1(因为有10个类别),然后Softmax损失值正确分类的负对数概率-ln(0.1) = 2.302。对于多类SVM,假设所有的边界都被越过(因为所有的分值都近似为零),所以损失值是9(因为对于每个错误分类,边界值是1)。如果没看到这些损失值,那么初始化中就可能有问题;
  2. 提高正则化强度,损失值会变大;
def init_two_layer_model(input_size, hidden_size, output_size):
    model = {}
    model["W1"] = 0.0001 * np.random.randn(input_size, hidden_size)
    model['b1'] = np.zeros(hidden_size)
    model['W2'] = 0.0001 * np.random.randn(hidden_size, output_size)
    model['b2'] = np.zeros(output_size)
    return model

model = init_two_layer_model(32*32*3, 50, 10)
loss, grad = two_layer_net(X_train, model, y_train, 0)  # 0没有正则损失
print(loss)
  1. 对小数据子集过拟合。这一步很重要,在整个数据集进行训练之前,尝试在一个很小的数据集上进行训练(比如20个数据),然后确保能到达0的损失值。此时让正则化强度为0,不然它会阻止得到0的损失。除非能通过这一个正常性检查,不然进行整个数据集训练是没有意义的。但是注意,能对小数据集进行过拟合依然有可能存在不正确的实现。比如,因为某些错误,数据点的特征是随机的,这样算法也可能对小数据进行过拟合,但是在整个数据集上跑算法的时候,就没有任何泛化能力。
model = init_two_layer_model(32*32*3, 50, 10)
trainer = ClassifierTrainer()
X_tiny = X_train[:20]   # 选前20个作为样本
y_tiny = y_train[:20]
best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny, 
                                  model, two_layer_net, verbose=True,
                                  num_epochs=200, reg=0.0, update='sgd',
                                  learning_rate=1e-3, learning_rate_decay=1,
                                  sample_batchs=False)

最终损失为0,正确率能达到1.0,效果不错。

理论上将进行梯度检查很简单,就是简单地把解析梯度和数值计算梯度进行比较。然而从实际操作层面上来说,这个过程更加复杂且容易出错。下面是一些常用的技巧:

  1. 使用中心化公式。在使用有限差值近似来计算数值梯度的时候,常见的公式是:\frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h}其中h是一个很小的数字,在实践中近似为1e-5。但是在实践中证明,使用中心化公式效果更好:\frac{df(x)}{dx} = \frac{f(x + h) - f(x - h)}{2h}该公式在检查梯度的每个维度的时候,会要求计算两次损失函数(所以计算资源的耗费也是两倍),但是梯度的近似值会准确很多。

  2. 使用相对误差来比较。数值梯度f'_n和解析梯度f'_a的绝对误差并不能准确的表明二者的差距,应当使用相对误差。\frac{\mid f'_a - f'_n \mid}{\max(\mid f'_a \mid, \mid f'_n \mid)}在实践中:相对误差大于1e-2通常就意味着梯度可能出错;小于1e-7才是比较好的结果。但是网络的深度越深,相对误差就越高。所以对于一个10层网络,1e-2的相对误差值可能就行,因为误差一直在累积。相反,如果一个可微函数的相对误差值是1e-2,那么通常说明梯度实现不正确。

  3. 使用双精度。一个常见的错误是使用单精度浮点数来进行梯度检查,这样会导致即使梯度实现正确,相对误差值也会很高(比如1e-2)。

  4. 保持在浮点数的有效范围。把原始的解析梯度和数值梯度数据打印出来,确保用来比较的数字的值不是过小。

  5. 注意目标函数的不可导点(kinks)。在进行梯度检查时,一个导致不准确的原因是不可导点问题。不可导点是指目标函数不可导的部分,由ReLU函数、SVM损失、Maxout神经元等引入。考虑当x=-1e-6时,对ReLU函数进行梯度检查。因为x<0,所以解析梯度在该点的梯度为0。然而,在这里数值梯度会突然计算出一个非零的梯度值,因为f(x+h)可能越过了不可导点(例如:如果h>1e-6),导致了一个非零的结果。解决这个问题的有效方法是使用少量数据点。这样不可导点会减少,并且如果梯度检查对2-3个数据点都有效,那么基本上对整个批量数据也是没问题的。

  6. 谨慎设置h。并不是越小越好,如果无法进行梯度检查,可以试试试试将h调到1e-4或者1e-6。

  7. 在操作的特性模式中梯度检查。为了安全起见,最好让网络学习(“预热”)一小段时间,等到损失函数开始下降的之后再进行梯度检查。在第一次迭代就进行梯度检查的危险就在于,此时可能正处在不正常的边界情况,从而掩盖了梯度没有正确实现的事实。

  8. 关闭正则损失。推荐先关掉正则化对数据损失做单独检查,然后对正则化做单独检查,防止正则化损失吞没掉数据损失。

best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
                                  model, two_layer_net, verbose=True,
                                  num_epochs=10, reg=0.000001, update='sgd',
                                  learning_rate=1e-6, learning_rate_decay=1,
                                  sample_batchs=False)
学习率为1x10-6时,损失下降缓慢,说明学习速率过小。但是正确率却迅速增大到0.2左右,是因为我们使用的时softmax损失,虽然得分分布仍然比较分散,那么正确分类的损失还是比较接近,但是参数在朝正确的方向细微的更新,导致准确率可能会发生突变,因为我们选的是最大的准确率。

现在把学习率设为另一个极端:106,结果会发生损失爆炸:

NaN通常意味着学习率过高,导致损失过大。设为3x10-3时仍然爆炸,一个比较合理的范围是[10-5, 10-3]。

训练过程中的数值跟踪

跟踪损失函数

训练期间第一个要跟踪的数值就是损失值,它在前向传播时对每个独立的批数据进行计算。

在下面的图表中,x轴通常都是表示周期(epochs)单位,该单位衡量了在训练中每个样本数据都被观察过的次数的期望(一个epoch意味着每个样本数据都被观察过了一次)。相较于迭代次数(iterations),一般更倾向跟踪epoch,这是因为迭代次数与数据的批尺寸(batchsize)有关,而批尺寸的设置又可以是任意的。比如一共有1000个训练样本,每次SGD使用的小批量是10个样本,一次迭代指的是用这10个样本训练一次,而1000个样本都被使用过一次才是一次epoch,即这1000个样本全部被训练过一次需要100次iterations,一次epoch。

下图展示的是损失值随时间的变化,曲线形状会给出学习率设置的情况:

图17 损失函数与学习率的关系 左图展示了不同的学习率的效果。过低的学习率导致算法的改善是线性的。高一些的学习率会看起来呈几何指数下降,更高的学习率会让损失值很快下降,但是接着就停在一个不好的损失值上(绿线)。这是因为最优化的“能量”太大,参数随机震荡,不能最优化到一个很好的点上。过高的学习率又会导致损失爆炸。
右图显示了一个典型的随时间变化的损失函数值,在CIFAR-10数据集上面训练了一个小的网络,这个损失函数值曲线看起来比较合理(虽然可能学习率有点小,但是很难说),而且指出了批数据的数量可能有点太小(因为损失值的噪音很大)。损失值的震荡程度和批尺寸(batch size)有关,当批尺寸为1,震荡会相对较大。当批尺寸就是整个数据集时震荡就会最小,因为每个梯度更新都是单调地优化损失函数(除非学习率设置得过高)。 下图这种开始损失不变,然后开始学习的情况,说明初始值设置的不合理。 图18 初值不合理的损失函数曲线
跟踪训练集和验证集准确率
在训练分类器的时候,需要跟踪的第二重要的数值是验证集和训练集的准确率。这个图表能够展现知道模型过拟合的程度: 图19 训练集和验证集准确率

训练集准确率和验证集准确率间的间距指明了模型过拟合的程度。在图中,蓝色的验证集曲线比训练集准确率低了很多,这就说明模型有很强的过拟合。遇到这种情况,就应该增大正则化强度(更强的L2权重惩罚,更多的随机失活等)或收集更多的数据。另一种可能就是验证集曲线和训练集曲线很接近,这种情况说明模型容量还不够大:应该通过增加参数数量让模型容量更大些。

跟踪权重更新比例

最后一个应该跟踪的量是权重中更新值的数量和全部值的数量之间的比例。注意:是更新的,而不是原始梯度(比如,在普通sgd中就是梯度乘以学习率)。需要对每个参数集的更新比例进行单独的计算和跟踪。一个经验性的结论是这个比例应该在1e-3左右。如果更低,说明学习率可能太小,如果更高,说明学习率可能太高。下面是具体例子:

# 假设参数向量为W,其梯度向量为dW
param_scale = np.linalg.norm(W.ravel())  # ravel将多维数组转化成一维;
                                         # np.linalg.norm默认求L2范式
update = -learning_rate*dW # 简单SGD更新
update_scale = np.linalg.norm(update.ravel())
W += update # 实际更新
print update_scale / param_scale # 要得到1e-3左右

第一层可视化

如果数据是图像像素数据,那么把第一层特征可视化会有帮助:

图20 第一层特征可视化 左图中的特征充满了噪音,这暗示了网络可能出现了问题:网络没有收敛,学习率设置不恰当,正则化惩罚的权重过低。右图的特征不错,平滑,干净而且种类繁多,说明训练过程进行良好。

6 超参数调优

那么如何具体进行超参数优化呢?常需要设置的超参数有三个:

下面介绍几个常用的策略:

  1. 比起交叉验证最好使用一个验证集。在大多数情况下,一个尺寸合理的验证集可以让代码更简单,不需要用几个数据集来交叉验证。

  2. 分散初值,几次周期(epoch)。(关于epoch和迭代(iteration)的区别,下文有说明。)选择几个非常分散的数值,然后使用几次epoch去学习。经过几次epoch,基本就能发现哪些数值较好哪些不好。比如很快就nan(往往超过初始损失3倍就可以认为是nan,就可以结束训练。),或者没有反应,然后进行调整。

  3. 过程搜索:从粗到细;epoch次数也逐渐增加。发现比较好的区间后,就可以精细搜索,epoch次数更多,运行时间更长。比如之前的网络,每次进行5次epoch,对较好的区间进行搜索,找到准确率比较高的值,然后进一步精确查找。注意,需要在对数尺度上进行超参数搜索,也就是说,我们从标准分布中随机生成了一个实数,然后让它成为10的次数。对于正则化强度,可以采用同样的策略。直观地说,这是因为学习率和正则化强度都对于训练的动态进程有乘的效果。例如:当学习率是0.001的时候,如果对其固定地增加0.01,那么对于学习进程会有很大影响。然而当学习率是10的时候,影响就微乎其微了。这就是因为学习率乘以了计算出的梯度。因此,比起加上或者减少某些值,思考学习率的范围是乘以或者除以某些值更加自然。但是有一些参数(比如随机失活)还是在原始尺度上进行搜索。

max_count = 100
for count in range(max_count):
    reg = 10**uniform(-5, 5)  # random模块的函数uniform,会在-5~5范围内随机选择一个实数
                              # reg在10^-5~10^5之间取值,指数函数
    lr = 10**uniform(-3, -6)

    model = init_two_layer_model(32 * 32 * 3, 50, 10)
    trainer = ClassifierTrainer()
    best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
                                      model, two_layer_net, verbose=False,
                                      num_epochs=5, reg=reg, update='momentum',
                                      learning_rate=lr, learning_rate_decay=0.9,
                                      sample_batchs=True, batch_size=100)
比较好的结果在红框中,学习率在10-4左右,正则强度在10-4~10-1左右,需要进一步精细搜索。修改代码:
max_count = 100
for count in range(max_count):
    reg = 10**uniform(-4, 0)
    lr = 10**uniform(-3, -4)
有一个相对较好的准确率:53%.但是这里却有一个问题,这些比较高的准确率都是学习率在10-4附近,也就是说都在我们设置的区间边缘,或许10-5或10-6有更好的结果。所以在设置区间的时候,要把较好的值放在区间中间,而不是区间边缘。
  1. 随机搜索优于网格搜索。Bergstra和Bengio在文章Random Search for Hyper-Parameter Optimization中说“随机选择比网格化的选择更加有效”,而且在实践中也更容易实现。通常,有些超参数比其余的更重要,通过随机搜索,而不是网格化的搜索,可以让你更精确地发现那些比较重要的超参数的好数值。
图21 网格搜索与随机搜索

上图中绿色函数部分是比较重要的参数影响,黄色是不重要的参数影响,同样取9个点,如果采用均匀采样就会错过很多重要的点,随机搜索就不会。

接下来要讲的学习率衰减方案、更新类型、正则化、以及网络结构(深度、尺寸)等都需要超参数调优。

总结

  1. 激活函数选择折叶函数;
  2. 数据预处理采用减均值;
  3. 权重初始化采用Xavier或He初始化;
  4. 使用批量归一化;
  5. 梯度检查;合理性检查;跟踪损失函数、准确率、更新比例等;
  6. 超参数调优采用随机搜索,对数间隔,不断细化范围,增加epoch.
上一篇下一篇

猜你喜欢

热点阅读