动手实现深度神经网络5 神经网络的优化之参数更新优化
在之前的神经网络中,我们都是使用随机梯度下降法SGD对参数进行更新的,然而,虽然SGD实现简单易于理解,但是,它在解决某些问题时可能很没有效率。
SGD的缺点
首先我们来看一个例子:
这个函数的图像和等高线如下:
它的各点处的梯度如下图:
可以看到,在各个点的梯度在x方向上的分量很小,也就是说在根据梯度更新参数时,只会向x方向移动很少的举例,这样显然要花费很长时间才能到达最小值处(中间)。
下面的基于SGD的最优化的更新路径也证实了上面的推测:
可以看到,SGD优化的路径呈“之”字形朝最小值(0, 0)移动,效率非常低。
optimizer
在介绍对其他更新参数的方法之前,我们先来把“像SGD一样负责参数修改的代码”封装为一个类,这些类统称为optimizer(负责优化的人),这些类都会实现一个update方法用于“根据梯度修改参数”。
新建一个optimizer.py文件,把SGD以及之后要介绍地其他类都放在这个文件里面。
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]
Momentum
Momentum引入了新的变量v ,用于表示速度 ,它把梯度 理解为物体受到的力 ,还有新变量momentum 用于表示“不管物体受不受力,都会作用于物体上的空气阻力 ”。
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):
if self.v == None:
self.v = {}
for k, v in params.items():
self.v[k] = np.zeros_like(v)
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
简单来说就是:在每一次更新参数时,速度v会受到“阻力momentum”和“作用力grads”的影响而改变,而参数则会根据速度v的值改变。
Momentum方法给人的感觉就像是小球在地面上滚动一样,下图是基于Momentum的最优化的更新路径:
AdaGrad
在神经网络的学习中,学习率(η)的值很重要。学习率过小, 会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能 正确进行。 在关于学习率的有效技巧中,有一种被称为学习率衰减 (learning rate decay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多” 学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
AdaGrad进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值 ,具体如何做到定制呢?AdaGrad引入一个变量h,记录每个参数对应的以前的所有梯度值的平方和,在更新参数时,通过乘以1/h0.5 ,就可以调整学习的尺度,这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小。
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)
下图是基于AdaGrad的最优化的更新路径:
RMSProp
AdaGrad听起来很美好,但是仍然存在问题:
“AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,h值越大,更新的幅度就越小。实际上,如果无止境地学习,更新量就会变为 0, 完全不再更新。”
为了解决这一问题,我们引入RMSProp,它会逐渐“忘记”很早之前的梯度,避免h值过大。
class RMSProp:
def __init__(self, lr=0.01, decay_rate = 0.99):
self.lr = lr
self.decay_rate = decay_rate
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] *= self.decay_rate
self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
Adam
Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参 数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起,就是Adam的基本思想。当然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] += (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)
几种方式的比较
我们以前面自己手动实现的两层神经网络手写数字识别为例,比较一下这五种方法。
(x_train,t_train),(x_test,t_test)=load_mnist(normalize=True)
train_size=x_train.shape[0]
batch_size=128
max_iterations=2000
optimizers={}
optimizers['SGD']=SGD()
optimizers['Momentum']=Momentum()
optimizers['AdaGrad']=AdaGrad()
optimizers['RMSProp']=RMSProp()
optimizers['Adam']=Adam()
networks={}
train_loss={}
for key in optimizers.keys():
networks[key]=MyTwoLayerNet(input_size=784,hidden_size_1=100,output_size=10)
train_loss[key]=[]
for i in range(max_iterations):
batch_mask=np.random.choice(train_size,batch_size)
x_batch=x_train[batch_mask]
t_batch=t_train[batch_mask]
for key in optimizers.keys():
grads=networks[key].gradient(x_batch,t_batch)
optimizers[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 optimizers.keys():
loss = networks[key].loss(x_batch, t_batch)
print(key + ":" + str(loss))
markers = {"SGD": "o", "Momentum": "x", "AdaGrad": "s","RMSProp":".", "Adam": "D"}
x = np.arange(max_iterations)
for key in optimizers.keys():
plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
plt.show()
比较结果是:
可以看到,与SGD相比,其他几种方法学习得更快。当然,这只是在我自己设计的神经网络上的表现,不同的网络结构,学习率等超参数的设置等都会比较实验结果。
上面介绍了几种方法,那么用哪种方法好呢?很遗憾,目前并不存在能在所有问题中都表现良好的方法。这几种方法各有各的特点,都有各自擅长解决的问题和不擅长解决的问题 。
|