目录
本章主题涉及寻找最优权重参数的最优化方法、权重参数的初始值、超参数的设定方法等
1. 参数的更新
- 神经网络的学习的目的就是找到使损失函数的值尽可能小的参数
- 这是寻找最优参数的问题,这个过程叫做最优化(optimization)
SGD
-
前几章我们采用的方法为随机梯度下降法(SGD, stochastic gradient descent)
-
公式:
- W为需要更新的权重参数,
为损失函数关于W的梯度
-
实现为一个Python类
class SGD: def __init__(self, lr=0.01): self.lr = lr def update(self, params, grads): for key in params.key(): params[key] -= self.lr * grads[key]
-
缺点:解决某些问题时可能没有效率
- 如果函数的形状非均向(anisotropic),比如延伸状,搜索路径会非常低效
Momentum
-
Momentum是“动量”的意思,和物理有关
-
公式:
(1)
(2)
表示学习率,
对应物理上的速度
- (1)表示了物体在梯度方向上受力,速度增加,其中
表示物体不受力时,物体逐渐减速,对应物理上的摩擦或空气阻力
-
实现
import numpy as np class Momentum: def __init__(self, lr=0.01, momentum=0.9): self.lr = lr self.momentum = momentum self.v = None def update(self, params, grads): # v保存物体的速度,初始化时,什么都不保存,但第一次调用update()时 # v会以字典型变量的形式保存与参数结构相同的数据 if self.v is None: self.v = {} for key, val in params.items(): self.v[key] = np.zeros_like(val) # 公式的实现 for key in params.keys(): self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] params[key] += self.v[key]
AdaGrad
- 神经网络的学习中,学习率的值很重要:过小,会导致学习花费过多时间;过大,会导致学习发散而不能正确进行
- 学习率衰减(learning rate decay):随着学习的进行,使学习率逐渐减小。一开始多学,然后逐渐少学。
- AdaGrad:为参数的每个元素适当地调整学习率,与此同时进行学习
- AdaGrad会记录过去所有梯度的平方和,因此随着学习越深入,更新的幅度就越小
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.keys():
self.h[key] += grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
Adam
- Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐,Adam方法融合了这两种思想
class Adam:
"""Adam (<http://arxiv.org/abs/1412.6980v8>)"""
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.beta1*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
#unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
#params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)
总结
- 4种方法各有特点,都有擅长解决和不擅长解决的问题,很多研究中至今仍在使用SGD
2. 权重的初始值
设定什么样的权重初始值,经常关系到神经网络的学习能否成功
可以将权重初始值设为0吗
- 权值衰减(weight decay):是一种以减少权重参数的值为目的的进行学习的方法。它可以抑制过拟合、提高泛化能力
- 如果想减小权重的值,一开始就将初始值设为较小的值才是正途
- 前面我们使用了
0.01*np.random.randn(10, 100)
这样由高斯分布生成的值乘以0,01后得到的值
- 前面我们使用了
- 为什么不能将权重初始值设为0,或者严格地说,为什么不能将权重初始值设处一样的值?
- 因为在误差反向传播法中,所有的权重都会进行相同的更新
- 权重被更新为相同的值,并拥有了对策(重复)的值
- 这使得神经网络拥有许多不同权重的意义丧失了
- 因此为了瓦解权重的对称结构,必须随机生成初始值
隐藏层的激活值的分布
假设神经网络有5层,每层有100个神经元,然后用高斯分布随机生成1000个数据作为输入数据,激活函数使用sigmoid函数,node_num为神经元的个数
-
使用标准差为1的高斯分布
W = np.random.randn(node_num, node_num) * 1
- 各层的激活值呈偏向0和1的分布
- 在sigmoid函数中,随着输出不断靠近0或1,它的导数值逐渐接近0,因此会造成反向传播中梯度的值不断变小,最后消失,即梯度消失(gradient vanishing)
-
使用标准差为0.01的高斯分布
W = np.random.randn(node_num, node_num) * 0.01
- 各层的激活值呈集中在0.5附近的分布
- 多个神经元都输出几乎相同的值,那么100个神经元可以由1个神经元来表达基本相同的事情,因此激活值的分布有所偏向,会出现“表现力受限”的问题
💡 因此,我们要求各层激活值的分布要有适当的广度,这样通过在各层间传递多样性的数据,神经网络可以进行高效的学习
-
使用Xavier初始值(在一般的深度学习框架中,已被作为标准使用)
node_num = n # 前一层的节点数 W = np.random.randn(node_num, node_num) * np.sqrt(1 / node_num)
- 如果前一层的结点数是n,则初始值使用标准差为
的高斯分布
- 越是后面的层,图像变得越歪斜,但是呈现了比之前更有广度的分布
- 如果使用tanh函数代替sigmoid函数,歪斜的问题可以得到改善
- 如果前一层的结点数是n,则初始值使用标准差为
ReLU的权重初始值
-
上述介绍的Xavier是以激活函数是线性函数为前提而推导出来的。但当激活函数使用ReLUctant时,使用“He初始值”
-
He初始值:当前一层的节点数为n时,He初始值使用标准差为
的高斯分布。和Xavier相比,因为ReLU的负值区域的值为0,为了使它更有广度,所以需要2倍的系数
node_num = n # 前一层的节点数 W = np.random.randn(node_num, node_num) * np.sqrt(2 / node_num)
总结
- 激活函数为sigmoid或tanh等S型曲线函数时,权重初始值使用Xavier初始值
- 激活函数为ReLU时,初始值的赋值方式使用He初始值
3. Batch Normalization
为了使各层拥有适当的广度,Batch Norm方法可以“强制性”地调整激活值的分布,减小数据分布的偏向
-
概念:以进行学习时的mini-batch为单位,按nini-batch进行正规化,就是进行是数据分布的均值为0,方差为1的正规化,然后对正规化后的数据进行缩放和平移的变换
-
实现
class BatchNormalization: """ <http://arxiv.org/abs/1502.03167> """ def __init__(self, gamma, beta, momentum=0.9, running_mean=None, running_var=None): self.gamma = gamma self.beta = beta self.momentum = momentum self.input_shape = None # Conv层的情况下为4维,全连接层的情况下为2维 # 测试时使用的平均值和方差 self.running_mean = running_mean self.running_var = running_var # backward时使用的中间数据 self.batch_size = None self.xc = None self.std = None self.dgamma = None self.dbeta = None def forward(self, x, train_flg=True): self.input_shape = x.shape if x.ndim != 2: N, C, H, W = x.shape x = x.reshape(N, -1) out = self.__forward(x, train_flg) return out.reshape(*self.input_shape) def __forward(self, x, train_flg): if self.running_mean is None: N, D = x.shape self.running_mean = np.zeros(D) self.running_var = np.zeros(D) if train_flg: mu = x.mean(axis=0) xc = x - mu var = np.mean(xc**2, axis=0) std = np.sqrt(var + 10e-7) xn = xc / std self.batch_size = x.shape[0] self.xc = xc self.xn = xn self.std = std self.running_mean = self.momentum * self.running_mean + (1-self.momentum) * mu self.running_var = self.momentum * self.running_var + (1-self.momentum) * var else: xc = x - self.running_mean xn = xc / ((np.sqrt(self.running_var + 10e-7))) out = self.gamma * xn + self.beta return out def backward(self, dout): if dout.ndim != 2: N, C, H, W = dout.shape dout = dout.reshape(N, -1) dx = self.__backward(dout) dx = dx.reshape(*self.input_shape) return dx def __backward(self, dout): dbeta = dout.sum(axis=0) dgamma = np.sum(self.xn * dout, axis=0) dxn = self.gamma * dout dxc = dxn / self.std dstd = -np.sum((dxn * self.xc) / (self.std * self.std), axis=0) dvar = 0.5 * dstd / self.std dxc += (2.0 / self.batch_size) * self.xc * dvar dmu = np.sum(dxc, axis=0) dx = dxc - dmu / self.batch_size self.dgamma = dgamma self.dbeta = dbeta return dx
-
优点:
- 可以使学习快速进行(增大学习率)
- 不那么依赖初始值
- 抑制过拟合
-
评估:几乎所有的情况下,使用Batch Norm时学习进行得更快;不使用时,如果不赋予一个尺度好的初始值,学习将完全无法进行
4. 正则化
过拟合
指只能拟合训练数据,但不能很好的拟合不包含在训练数据中的其他数据的状态
-
产生原因:
- 模型拥有大量参数、表现力强
- 训练数据小
-
现象:
- 训练数据的识别精度几乎为100%
- 但测试数据离100%的识别精度差距较大
权值衰减
权值衰减是一直以来经常被使用的一种抑制过拟合的方法,通过在学习的过程中对大的权值来进行惩罚,来抑制过拟合
-
神经网络的学习目的是减小损失函数的值,假如给损失函数加上权重的平方范数,可以抑制权重变大
-
将权重记为$W$,平方范数的权值衰减就是
,
是控制正则化强度的超参数,值越大,对大的权值施加的惩罚就越重,1/2 是用于求导结果变成
-
实现:
weight_decay = 0 for idx in range(1, self.hidden_layer_num + 2): W = self.params['W' + str(idx)] weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2) return self.last_layer.forward(y, t) + weight_decay
-
使用权值衰减后的结果:
- 训练数据和测试数据的精度差距变小,说明过拟合受到了抑制
- 但是训练数据的识别精度没有到达100%
Dropout
权值衰减实现简单,但是难以应对复杂的网络模型,这时要使用Dropout
-
Dropout是一种在学习过程中随即删除神经元的方法,被删除的神经元不再进行信号的传递
- 训练时,每传递一次数据,就会随机选择要删除的神经元
- 测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出
-
实现
class Dropout: def __init__(self, dropout_ratio=0.5): self.dropout_ratio = dropout_ratio self.mask = None def forward(self, x, train_flg=True): if train_flg: # mask随机生成和 x 形状相同的数组,并将值比 dropout_ratio 大的元素设为True self.mask = np.random.rand(*x.shape) > self.dropout_ratio # 被删除的神经元值为False,不会被传递 return x * self.mask else: return x * (1.0 - self.dropout_ratio) # 反向传播与ReLU相同 def backward(self, dout): return dout * self.mask
- 正向传播时传递了信号的神经元,反向传播时按原样传递信号
- 正向传播时没有传递信号的神经元,反向传播时信号将停在那里
集成学习
机器学习中经常使用集成学习,就是指让多个模型单独进行学习,推理时再取出多个模型的输出的平均值,可以提高识别精度
- 用神经网络的语境来说,比如准备5个结构相同或类似的网络,分别进行学习,测试时,以这5个网络的输出的平均值作为答案
- 集成学习与Dropout有密切的关系,Dropout通过随即删除神经元,从而每一次都让不同的模型进行学习,推理时通过对神经网络的输出乘以删除比例,可以取得模型的平均值
- 也就是说Dropout将集成学习的效果通过一个网络实现了
5. 超参数的验证
神经网络中除了权重和偏置等参数,超参数也经常出现,比如各层神经元的数量、batch大小、参数更新时的学习率或权值衰减。以下将介绍尽可能高效寻找超参数的值的方法
验证数据
调整超参数时,必须使用超参数专用的确认数据,即验证数据
💡 训练数据用于参数的学习,验证数据用于超参数的性能评估,测试数据最后确认泛化能力
-
有的数据集会事先分成训练数据、验证数据、测试数据三部分,有的需要自己分割
-
例如MNIST数据集,可以从训练数据中事先分割20%作为验证数据
def shuffle_dataset(x, t): """打乱数据集 Parameters ---------- x : 训练数据 t : 监督数据 Returns ------- x, t : 打乱的训练数据和监督数据 """ permutation = np.random.permutation(x.shape[0]) x = x[permutation,:] if x.ndim == 2 else x[permutation,:,:,:] t = t[permutation] return x, t (x_train, t_train), (x_test, t_test) = load_mnist() # 打乱训练数据 x_train, t_train = shuffle_dataset(x_train, t_train) # 分割验证数据 validation_rate = 0.20 validation_num = int(x_train.shape[0] * validation_rate) x_val = x_train[:validation_num] t_val = t_train[:validation_num] x_train = x_train[validation_num:] t_train = t_train[validation_num:]
超参数的最优化
- 步骤0
- 设定超参数的范围
- 步骤1
- 从设定的超参数范围中随机采样
- 步骤2
- 使用步骤1中采样到的超参数的值进行学习,通过验证数据评估识别精度(将epoch设置得很小)
- 因为深度学习需要很长时间,使用要尽早放弃那些不符合逻辑的超参数,我们需要减少学习的epoch,缩短一次评估所需的时间
- 步骤3
- 重复步骤1和步骤2(100次等),根据识别精度的结果,缩小超参数的范围
- 在缩小到一定程度后,从中选出一个值
这里介绍的方法是实践性的,还有贝叶斯最优化