2194 字
11 分钟
深度学习——神经网络超参数的处理(02)

概述#

本文建立在前一篇文章的基础上进行讨论,这一系列文章的本意是记录本人的学习历程,避免“后面学,前面忘”的窘境。

前文讨论到超参数,本文继续讨论如何处理上文的问题。具体而言,主要涉及到:

  • 对欠拟合、过拟合的处理
  • 正则化方法(Regularization)
    • L2L2正则化
    • Dropout正则化
  • 标准化方法(Normalization)
    • 梯度消失(Vanishing Gradient)
    • 梯度爆炸(Exploding Gradient)
    • 梯度检查(Gradient Checking)

神经网络训练数据#

深度学习的火爆离不开现如今的大量数据以及Tensorflow之类算力的加成buff,一般把数据分为训练集、开发集、测试集(Train/Dev/Test sets)。

数据集#

训练集就是训练神经网络的数据集,一般而言越多越好;开发集则一般用来调参,因此最终神经网络实际上是包含了训练集和开发集的信息,因此开发集上的测试结果并不是无偏的;测试集则用来测试训练好的神经网络,结果也是无偏的。

一般而言,当数据量较少时(<2000<2000),训练集、开发集、测试集的比例一般为6:2:26:2:2。这三个数据集中测试集可以没有,这样的话开发集实际上充当了测试集的功能。但由于上面提到的原因,开发集并不完全等价于测试集。

当数据量很大时,比如1,000,0001,000,000,这时,并不一定要按照6:2:26:2:2的比例来划分。由于开发集、测试集的功能,其并不需要很多的数据,因此给两者分配一定数量的数据可以完成目标就可以了。因此,可以按照98:1:198:1:1的比例来分配数据。

数据量#

当数据量很大时,神经网络将变得十分臃肿,进而拖慢训练速度。比如,当数据量为1,000,0001,000,000时,我们可以把整个数据集XX分为1000个小数据集XiX^{\\{i\\}},每个小数据集包含1000个数据。

当神经网络在XiX^{\\{i\\}}上完成一次训练,便进行一次参数更新:即原本要跑完1,000,0001,000,000条数据才会更新参数,现在每跑完一小部分数据就更新一次参数。在XiX^{\\{i\\}}上跑完一次,成为完成了一次epoch。

初始化参数#

这里的参数指W[l]W^{[l]}b[l]b^{[l]},运用“He initialization”可以更快的使模型收敛。具体而言,只需要在常规初始化的参数后面乘上2/n[l1]\sqrt{2/n^{[l-1]}}即可。用代码表示为:

parameters['W'+str(l)] = np.random.randn(layers_dims[l], layers_dims[l-1])*np.sqrt(2/layers_dims[l-1])
parameters['b'+str(l)] = np.zeros((layers_dims[l], 1))

欠拟合与过拟合#

前文提到,区分两者的关键是模型在训练集和测试集上的表现:

  • 欠拟合:训练集误差大,测试集误差较小
  • 过拟合:训练集误差小,测试集误差较大
  • 同时发生:训练集、测试集上误差均较大

处理欠拟合的方法有:

  • 增加神经网络深度
  • 调整超参数

处理过拟合的方法有:

  • 正则化
  • 标准化

L2L2正则化(Regularization)#

一般而言,避免过拟合的标准方法是L2L2正则化,它从改变成本函数开始。 正常的成本函数是:

J=1mi=1m(y(i)ln(a[L](i))+(1y(i))ln(1a[L](i)))J = -\frac1m\sum_{i=1}^{m}(y^{(i)}\ln(a^{[L](i)}) + (1-y^{(i)})\ln(1-a^{[L](i)}))

改变为:

J=1mi=1m(y(i)ln(a[L](i))+(1y(i))ln(1a[L](i)))+1mλ2lkjWk,j[l]2J = -\frac1m\sum_{i=1}^{m}(y^{(i)}\ln(a^{[L](i)})+(1-y^{(i)})\ln(1-a^{[L](i)})) + \frac1m\frac\lambda2\sum_l\sum_k\sum_jW_{k,j}^{[l]2}

这里λ\lambda成为正则化常数,是一个需要我们不断测试来选择最佳值的超参数。

进而,反向传播的计算公式也有所改变:

dW[l]=1mdZ[l]A[l1]T+λm×W[l] db[l]=1m_rowdZ[l]\begin{aligned} dW^{[l]} &= \frac1mdZ^{[l]}A^{[l-1]T} + \frac{\lambda}{m}\times W^{[l]} \\\ db^{[l]} &= \frac1m\sum\_{row}dZ^{[l]} \end{aligned}

Dropout#

Dropout也是常用的一种正则化手段,其在每次迭代过程中随机关闭一些神经节点,看起来不可思议,问题是它居然真的有用。

代码中通过一个由0和1为元素构成的矩阵来实现神经节点的开关。

Dl = np.random.rand(Al.shape[0], Al.shape[1]) #随机产生0-1间的数字
Dl = (Dl < keep_prob) #keep_prob是保持概率,小于这个数的将被设置为1
Al *= Dl
Al /= keep_prob

规范化(Normalization)#

也称为标准化,是对输入数据进行的处理:把输入数据的均值变为0,方差变为1。

X=XXˉ X=XVar(X)\begin{aligned} X &= X - \bar{X} \\\ X &= \frac{X}{\sqrt{Var(X)}} \end{aligned}

经过这样的处理后,模型收敛速度更快。

梯度检测(Gradient Checking)#

根据导数的定义,即

f(x)=limϵ0f(x+ϵ)f(x)ϵf'(x) = \lim_{\epsilon\rightarrow0}\frac{f(x+\epsilon)-f(x)}{\epsilon}

进而赋予ϵ\epsilon一个很小的值来近似计算倒数来比较。

算法优化#

数据量#

这里是指前文提到的,当数据量很大时,把整体数据XX分成一些小数据集XiX^{\\{i\\}},在小数据集上训练数据。

动量法(Momentum)#

动量法可以纠正梯度下降法的frustration。

初始化动量#

动量vv的维度应该和参数相同,具体而言:

def initialize_velocity(parameters):
"""
参数:
parameters -- 包含参数W和b的python字典
Wl = parameters['W'+str(l)]
bl = parameters['b'+str(l)]
返回值:
v -- 包含计算好的动量的python字典
dWl的动量 = v['dW'+str(l)]
dbl的动量 = v['db'+str(l)]
"""
L = len(parameters) // 2
v = {}
for l in range(L):
v['dW'+str(l+1)] = np.zeros(parameters['W'+str(l+1)].shape)
v['db'+str(l+1)] = np.zeros(parameters['b'+str(l+1)].shape)
return v

更新参数W,b#

更新参数WWbb的规则为:

{vdW[l]=βvdW[l]+(1β)dW[l] W[l]=W[l]αvdW[l]\begin{cases} v_{dW^{[l]}} = \beta v_{dW^{[l]}} + (1-\beta)dW^{[l]} \\\ W^{[l]} = W^{[l]} - \alpha v_{dW^{[l]}} \end{cases}{vdb[l]=βvdb[l]+(1β)db[l] b[l]=b[l]αvdb[l]\begin{cases} v_{db^{[l]}} = \beta v_{db^{[l]}} + (1-\beta)db^{[l]} \\\ b^{[l]} = b^{[l]} - \alpha v_{db^{[l]}} \end{cases}

对应的代码为:

def update(parameters, grads, v, beta, learning_rate):
"""
参数:
parameters -- Wl = parameters['W'+str(l)]
bl = parameters['b'+str(l)]
grads -- dWl = grads['dW'+str(l)]
dbl = grads['db'+str(l)]
beta -- 常量,动量法的一个超参数
learning_rate -- 常量,学习速率
返回值:
parameters -- 同上,更新后的参数
v -- 保存计算好的动量值的字典
"""
L = len(parameters) // 2
for l in range(L):
v['dW'+str(l+1)] = beta * v['dW'+str(l+1)] + (1 - beta) * grads['dW'+str(l+1)]
v['db'+str(l+1)] = beta * v['db'+str(l+1)] + (1 - beta) * grads['db'+str(l+1)]
parameters['W'+str(l+1)] -= learning_rate * v['dW'+str(l+1)]
parameters['b'+str(l+1)] -= learning_rate * v['db'+str(l+1)]
return parameters, v

一般而言,β\beta的值一般选取0.8到0.999之间的某个数,β=0.9\beta = 0.9是一个常用的数。

Adam优化法#

Adam法结合了动量法和RMSProp,是效率较高的一种优化方法。 其数学表示如下:

{vdW[l]=β1vdW[l]+(1β1)dW[l] vdW[l]corrected=vdW[l]1(β1)t sdW[l]=β2sdW[l]+(1β2)(dW[l])2 sdW[l]corrected=sdW[l]1(β2)t W[l]=W[l]αvdW[l]correctedsdW[l]corrected+ϵ\begin{cases} v_{dW^{[l]}} = \beta_1v_{dW^{[l]}} + (1-\beta_1)dW^{[l]} \\\ v_{dW^{[l]}}^{corrected} = \frac{v_{dW^{[l]}}}{1 - (\beta_1)^t} \\\ s_{dW^{[l]}} = \beta_2s_{dW^{[l]}} + (1-\beta_2)(dW^{[l]})^2 \\\ s_{dW^{[l]}}^{corrected} = \frac{s_{dW^{[l]}}}{1 - (\beta_2)^t} \\\ W^{[l]} = W^{[l]} - \alpha \frac{v_{dW^{[l]}}^{corrected}}{\sqrt{s_{dW^{[l]}}^{corrected}}+\epsilon} \end{cases}

这里:

  • tt表示Adam算法的运行次数
  • β1\beta_1β2\beta_2是两个超参数
  • ϵ\epsilon是一个非常小的常数,其主要目的是防止分母部分为零

代码如下:

def update_parameters(parameters, grads, v, s, t, learning_rate=0.01, beta1=0.9, beta2=0.999, epsilon=1e-8):
"""
输入:
parameters -- 包含W和b的字典
grads -- 包含梯度的字典
v -- Adam法的动量部分一
s -- Adam法的栋梁部分二
t -- Adam法的执行次数
返回值:
parameters -- 调整后的参数字典
v, b -- 同上
"""
L = len(parameters) // 2
v_corrected = {}
s_corrected = {}
for l in range(L):
v['dW'+str(l+1)] = beta1 * v['dW'+str(l+1)] + (1 - beta1) * grads['dW'+str(l+1)]
v['db'+str(l+1)] = beta1 * v['db'+str(l+1)] + (1 - beta1) * grads['bd'+str(l+1)]
v_corrected['dW'+str(l+1)] = v['dW'+str(l+1)] / (1 - np.power(beta1, t))
v_corrected['db'+str(l+1)] = v['db'+str(l+1)] / (1 - np.power(beta1, t))
s['dW'+str(l+1)] = beta2 * s['dW'+str(l+1)] + (1 - beta2) * np.power(grads['dW'+str(l+1)], 2)
s['db'+str(l+1)] = beta2 * s['db'+str(l+1)] + (1 - beta2) * np.power(grads['db'+str(l+1)], 2)
s_corrected['dW'+str(l+1)] = s['dW'+str(l+1)] / (1 - np.power(beta2, t))
s_corrected['db'+str(l+1)] = s['db'+str(l+1)] / (1 - np.power(beta2, t))
parameters['dW'+str(l+1)] -= learning_rate * v_corrected['dW'+str(l+1)] / (np.sqrt(s_corrected['dW'+str(l+1)]) + epsilon)
parameters['db'+str(l+1)] -= learning_rate * v_corrected['db'+str(l+1)] / (np.sqrt(s_corrected['db'+str(l+1)]) + epsilon)
return parameters, v, s

超参数的调整#

超参数#

常见的超参数按重要性分类:

  • 重要性No.1
    • 学习速率α\alpha
  • 重要性No.2
    • 动量法参数β\beta,常用β=0.9\beta=0.9
    • 隐藏层的神经单元数量
    • 每个小的数据集XiX^{\\{i\\}}包含的数据量,多为2的倍数,如64,256
  • 重要性No.3
    • 神经网络的深度
    • 学习速率递减参数
  • 不那么重要
    • Adam优化法参数,β1=0.9,,β2=0.999,,ϵ=108\beta_1=0.9, , \beta_2=0.999, , \epsilon = 10^{-8}

调参的一般方法#

随机选取参数进行训练并观察结果是一般方法,要注意的是不能简单的取随机数。

比如学习速率α\alpha的范围一般取α(0.001,0.1)\alpha \in (0.001, 0.1),但是,下面这样的做法是不可取的:

alpha = np.random.rand() * 0099 + 0.001

下面的方法是可取的:

beta = -1 - np.random.rand() * 2
alpha = np.power(10, beta)

Batch Norm#

BN是一种与之前的规范会类似的方法。前文说到的规范化是针对输入的,这里则是针对所有的Z[l]Z^{[l]}做的处理。

多种类分类器(Multi-class Classification)#

Softmax回归#

与之前的二元分类器类似,当有n[L]n^{[L]}个种类时,这里的输出是一个(n[L],1)(n^{[L]}, 1)矩阵。具体而言:

Z[L]=[...] Ztemp[L]=eZ[L] A[L]=Ztemp[L]Ztemp[L]\begin{aligned} Z^{[L]} &= [...] \\\ Z_{temp}^{[L]} &= e^{Z^{[L]}} \\\ A^{[L]} &= \frac{Z_{temp}^{[L]}}{\sum Z_{temp}^{[L]}} \end{aligned}

Softmax是与Hardmax相对的,Hardmax是直接把Z[L]Z^{[L]}中最大的输出直接赋值为1,其余赋值为0。因该方法简单直接,因而被赋予Hard之名,Softmax较为柔和。

Tensorflow#

Tensorflow作为Google的开源机器学习框架,可以很方便的帮助我们部署机器学习的内容,尤其是其可以自动计算Backward Propagation部分。

深度学习——神经网络超参数的处理(02)
https://blog.xiaobaizhang.top/posts/deep-02/
作者
张小白
发布于
2020-01-05
许可协议
CC BY-NC-SA 4.0