赞
踩
本文总结一下ANN参数更新的各种方法
一些可能用到的数学公式数学公式
我们在以前的章节中使用随机梯度下降法(SGD)更新参数,在此,我们将提出其他的优化参数的方法,并指出SGD的一些缺点。
我们通过一个故事理解一下寻找最优参数的过程:
在这样苛刻的条件下,地面坡度显得尤为重要,通过脚底感受地面的倾斜状况,就是SGD的策略。勇敢 的探险家心里可能想着只要重复这一策略,总有一天可以到达“至深之地”。
1.1.2SGD
随机梯度下降法可以写成如下的式子
我们来实现一个名为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]
如果大家学习过前面的章节,这段代码很容易理解。
update函数会不断被调用来更新参数。
我们可以按如下方式训练神经网络(伪代码,不能运行)
network = TwoLayerNet(...)
optimizer = SGD()
for i in range(10000):
...
x_batch, t_batch = get_mini_batch(...) # mini-batch
grads = network.gradient(x_batch, t_batch)
params = network.params
optimizer.update(params, grads)
这里optimizer表示“进行最优化的人”的意思,由它负责更新参数。
如此,功能模块就会变得很简单。
1.1.3 SGD的缺点
SGD很简单并且容易实现,但是在解决某些问题时可能没有效率,我们来举个例子,求出下面函数最小值。
我们可以通过matplotlib来认识一下这个函数。代码如下
import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.mplot3d import axes3d def f(x,y): return 0.05*(x**2)+y**2 #线框图 fig = plt.figure() ax = plt.axes(projection='3d') #X = np.linspace(-10,10,256) #Y = np.linspace(-10,10,256) X = np.linspace(-10,10) Y = np.linspace(-10,10) print(f(0,0)) X, Y = np.meshgrid(X, Y)#生成网格点坐标矩阵。 Z = f(X,Y) plt.xlabel("x") plt.ylabel("y") ax.set_zlabel("z") ax.plot_wireframe(X, Y, Z, color='black') ax.set_title('wireframe') #等高线图 fig2 = plt.figure() level = np.array([0.0,1,2,3]) C = plt.contour(X, Y, Z,levels=level) plt.clabel(C,inline=True,fontsize=10) plt.contour(X,Y,Z) plt.xlabel("x") plt.ylabel("y") plt.show()
不难从两个图片看出,Z值关于x和y都是向0点下降的,不过y的下降梯度要更大。
接下来我们用图来表示一下梯度。代码如下,求梯度的函数大家在前面的章节已经很熟悉了
import numpy as np import matplotlib.pyplot as plt from deeplearning.fuction import numerical_gradient def f(x): return np.sum(0.05*x[0]**2+x[1]**2) x = np.arange(-10,10) y = np.arange(-5,6) X,Y = np.meshgrid(x,y) X = X.flatten() Y = Y.flatten() a = np.array([X, Y]) grad = numerical_gradient(f,a) plt.figure() plt.quiver(X, Y, -grad[0], -grad[1], angles="xy", color="#666666") # ,headwidth=10,scale=40,color="#444444") plt.xlim([-10, 10]) plt.ylim([-5, 5]) plt.xlabel('x0') plt.ylabel('x1') plt.grid() plt.draw() plt.show()
那么,接下来我们用SGD来更新参数,首先实现一下SGD的optimizer。
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]
import numpy as np import matplotlib.pyplot as plt from optimizer import SGD def f(x, y): return x**2 / 20.0 + y**2 def df(x, y): #直接把导数函数写出来,省略求梯度了 return x / 10.0, 2.0*y init_pos = (-7.0, 2.0) params = {} params['x'], params['y'] = init_pos[0], init_pos[1] grads = {} grads['x'], grads['y'] = 0, 0 x_history = [] y_history = [] optimizer = SGD(0.95) for i in range(30): x_history.append(params['x']) y_history.append(params['y']) grads['x'], grads['y'] = df(params['x'], params['y']) optimizer.update(params,grads) for i in range(len(x_history)): print(x_history[i],y_history[i]) x = np.arange(-10,10,0.01) y = np.arange(-5, 5, 0.01) X,Y = np.meshgrid(x,y) Z = f(X,Y) mask = Z > 7 Z[mask] = 0 plt.plot(x_history, y_history, 'o-', color="red") plt.contour(X, Y, Z) plt.ylim(-10, 10) plt.xlim(-10, 10) plt.plot(0, 0, '+') plt.xlabel("x") plt.ylabel("y") plt.title("SGD") plt.show()
在图中,SGD呈“之”字形移动。这是一个相当低效的路径。也就是说, SGD的缺点是,如果函数的形状非均向(anisotropic),比如呈延伸状,搜索 的路径就会非常低效。这段代码中间有一段循环打印,我们直接看打印的数据。
-7.0 2.0
-6.335 -1.7999999999999998
-5.733175 1.6199999999999997
-5.188523375 -1.4579999999999997
-4.695613654375 1.3121999999999998
-4.249530357209375 -1.18098
-3.8458249732744845 1.0628819999999997
-3.4804716008134085 -0.9565937999999994
-3.1498267987361346 0.8609344199999993
-2.8505932528562017 -0.7748409779999994
-2.5797868938348625 0.6973568801999994
-2.3347071389205505 -0.6276211921799995
-2.112909960723098 0.5648590729619996
-1.9121835144544037 -0.5083731656657995
-1.7305260805812355 0.4575358490992195
-1.566126102926018 -0.41178226418929753
-1.4173441231480464 0.37060403777036777
-1.282696431448982 -0.333543633993331
-1.1608402704613288 0.3001892705939978
-1.0505604447675025 -0.270170343534598
-0.9507572025145898 0.2431533091811382
-0.8604352682757038 -0.21883797826302437
-0.778693917789512 0.19695418043672192
-0.7047179955995084 -0.17725876239304972
-0.6377697860175552 0.15953288615374472
-0.5771816563458875 -0.14357959753837024
-0.5223493989930281 0.12922163778453322
-0.4727262060886904 -0.11629947400607987
-0.42781721651026483 0.10466952660547188
-0.3871745809417897 -0.09420257394492468
Process finished with exit code 0
很容易看出来y的值虽然在趋近于0但是它在正负之间反复横跳,x的值在缓慢的趋近于0。
竖直方向上,梯度就非常大,在水平方向上,梯度就相对较小,所以我们在设置学习率的时候就不能设置太大,为了防止竖直方向上参数更新太过了,这样一个较小的学习率又导致了水平方向上参数在更新的时候太过于缓慢,所以就导致最终收敛起来非常慢。
因此,我们需要比单纯朝梯度方向前进的SGD更聪 明的方法。SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。
Momentum是“动量”的意思,和物理有关。数学式子如下
新变量v对应物理上的速度。就像小球在地面上滚动一样
大家可以思考一下,小球还没到最低点时速度会越来越快,经过最低点后,速度会越来越慢上升,然后速度越来越快的下降,重复这个过程直到最低点时停下。
分析一下上面的式子,α是一个动量参数,我们通常把它设置为小于1的正数,例如0.9。v是上一次更新所加的参数。其他的我们很熟悉了
举个例子,假如上述小球在最低点的x值为0,左方向x值为负,右方向x值为正。初始值,x值为-3,v为0,学习率 * 梯度=-1,(假设α=0.9)。
第一次更新参数,v=0.9 * 0-(-1)=1,x = x+1=-2。
第二次更新参数,假设学习率*梯度仍是-1:v =0.9 * 1-(-1)= 1.9,x=x+v=-0.1
第三次更新参数,假设学习率*梯度仍是-1:v=0.9 * 1.9 = 1.71,x=x+v = 1.61
第四次更新参数,(方向相反了)学习率*梯度=1:v = 0.9 *1.71 - 1=0.539,x = x+v=3.149
我们可以明显看出来,如果方向不变,v的值会逐步增大,也就是更新幅度会越来越大,如果方向变了,v的值会变小,更新幅度会变小。相当于每次在进行参数更新的时候,都会将之前的速度考虑进来,每个参数在各方向上的移动幅度不仅取决于当前的梯度,还取决于过去各个梯度在各个方向上是否一致,如果一个梯度一直沿着当前方向进行更新,那么每次更新的幅度就越来越大,如果一个梯度在一个方向上不断变化,那么其更新幅度就会被衰减,这样我们就可以使用一个较大的学习率,使得收敛更快,同时梯度比较大的方向就会因为动量的关系每次更新的幅度减少。(这段黑字引自这里,我觉得说得很明白)
下面我们来说一下代码:
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 is None: #初始化v为一个和参数params一样的结构,元素都为0
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]
然后我们来看一下Momentum的最优化更新路径,代码和SGD的代码一样,只不过把optimizer=Momentum(0.1),然后换一下plt.title(“Momentum”)就好了,图形如下。为了看清楚一点,我给放大了
很明显Momentum要更加的“圆滑”。
神经网络的学习中,学习率(数学式中记为η)的值很重要。学习率过小, 会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能 正确进行。
在关于学习率的有效技巧中,有一种被称为学习率衰减(learning rate decay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多” 学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习 (AdaGrad的Ada来自英文单词Adaptive,即“适当的”的意思)。下面,让 我们用数学式表示AdaGrad的更新方法。
与SGD相比,多了一个h(圆圈表示对应矩阵元素的乘法,不是点积)。h存了以前的所有梯度值的平方和。然后,在更新参数时,通过乘以 ,就可以调整学习的尺度。这意味着, 参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说, 可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。
AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,更新 的幅度就越小。实际上,如果无止境地学习,更新量就会变为 0, 完全不再更新。为了改善这个问题,可以使用 RMSProp 方法。 RMSProp方法并不是将过去所有的梯度一视同仁地相加,而是逐渐 地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。 这种操作从专业上讲,称为“指数移动平均”,呈指数函数式地减小 过去的梯度的尺度。
下面我们来用代码实现一下AdaDrad:
class AdaGrad:
def __init__(self,lr = 0.01):
self.lr = lr
self.h = None
def update(self,params,grads):
if self.h is None:#初始化h
self.h={}
for key,val in params.items():
self.h[key] = np.zeros_like(val)
for key in params:
self.h[key] += grads[key]*grads[key]
params[key] -= self.lr*grads[key]/(np.sqrt(self.h[key])+1e-7)
#1e-7防止0被用作分母
同Momentum画图一样,我们画一下它的路径优化图:
效果是不是很棒!函数的取值高效地向着最小值移动。由于y轴方 向上的梯度较大,因此刚开始变动较大,但是后面会根据这个较大的变动按 比例进行调整,减小更新的步伐。因此,y轴方向上的更新程度被减弱,“之” 字形的变动程度有所衰减。
由于书上没有讲解,建议大家直接看吴恩达的deeplearning关于指数加权平均的部分,一共半个小时吧,应该没人讲的更好了,所以大家有理论不懂得地方推荐看他的视频。提醒大家注意视频中讲解β=0.9的时候是十天平均温度,β=0.98的时候是50天平均温度,大家要理解这是为什么。指数加权平均又叫指数移动平均,而移动的精髓就在此处。给大家个链接吧,因为b站吴恩达课程太多了,从p63开始。指数加权平均
了解理论之后直接用代码实现,和AdaGrad只有一部分不同。
class RMSprop: """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)
此处用的lr=0.1,书中代码lr=0.01,但是0.01绘出来的图离中心点太远了,收敛的不够,下降率也就是α=0.98。比上面的AdaGrad圆滑了一点。
学习过计算机网络或者操作系统的了解,我们设计算法的时候最喜欢缝合怪了,就是把两种算法结合在一起,同时具备两者的优点。
Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参 数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起会怎么样
Adam是2015年提出的新方法。它的理论有些复杂,直观地讲,就是融 合了Momentum和AdaGrad的方法。通过组合前面两个方法的优点,有望 实现参数空间的高效搜索。此外,进行超参数的“偏置校正”也是Adam的特征。吴恩达课程中RMSprop之后就讲Adam了,建议直接去看它的这部分理论课,7分钟左右,然后回来看代码。
class Adam: def __init__(self, lr=0.001, beta1=0.9, beta2=0.999): self.lr = lr self.beta1 = beta1 #动量法参数 self.beta2 = beta2#RMSprop参数 self.iter = 0 self.m = None #动量法的 self.v = None #RMSprop的 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) #偏差修正lr_t 把学习率和修正计算在一起了 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)
大家可以观察到这和吴恩达讲的不太一样,在计算mt和vt时都多减了一个mt-1,vt-1,下面注释的两行和吴恩达讲的一样,分别画出这两种计算方法的图如下
有没有发现一模一样,所以我猜测书上的办法可能是又修正了一个什么误差我们没学习到吧,不过我们按照吴恩达老师讲的就可以了,毕竟也没看出两者有什么差别。唯一不好理解的是在代码中,学习率和两个修正乘在一起了,大家用笔推一下就能理解了。三四行的公式而已。
上面我们介绍了SGD、Momentum、AdaGrad、Adam,RMSprop这5种方法,那 么用哪种方法好呢?非常遗憾,(目前)并不存在能在所有问题中都表现良好 的方法。这4种方法各有各的特点,都有各自擅长解决的问题和不擅长解决 的问题。 很多研究中至今仍在使用SGD。Momentum和AdaGrad也是值得一试 的方法。最近,很多研究人员和技术人员都喜欢用Adam。再以后的学习中主要使用SGD和Adam。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。