从零开始学习CNN系列——(三)梯度下降法的深度探索

从零开始学习CNN系列——(三)梯度下降法的深度探索

    从零开始学习CNN系列,是本人学习李宏毅老师的深度学习课、cs231n及吴恩达老师的深度学习课后总结的适合无基础小白入门深度学习的教程,会跟随本人的炼丹水平不断更改,如有错误,请多加指正。

    此小节,我们将对上一小节讲到的梯度下降法(Graident Descent)做进一步的探索,讲解常见的几种梯度下降法变种,并用代码加以实践。

1、Adagrad

    此方法的核心思想是自适应动态调节学习率(learning rate),运用同样思想的梯度下降法有很多种,Adagrad只是其中一种。再介绍Adagrad具体内容前,我们先来了解这个方法提出的问题背景。

    对于Loss曲线如上图的情况,分别选择极大(very large)、较大(large)、合适(just make)、较小(small)的学习率,我们得到了左图的四种结果:

  • 极大(very Large): 学习率过大,直接跳过目标点,损失(Loss)一直降不下去,达不到目标点。
  • 较大(large): 学习率较大,迭代一定次数后,损失(Loss)在两个点中循环跳跃,无论迭代多少次,最终都错过目标点。
  • 合适(just make): 学习率刚好合适,迭代一定次数后,刚好到达目标点。
  • 较小(small): 学习率较小,虽然能到达目标点,但是迭代时间过长。

    以上即是上一节讲过的“原始“的梯度下降法存在的问题,它提醒我们要想达到较好的训练结果,学习率不能是常数,而应该是能动态改变的。如往常的文章一样,读者可以先自己思考:如果是你,会怎样解决这个问题?
    我相信,大多数人能够想到的一种简单方案是——一开始,我们离目标点很远,应该步子迈大点,即选择较大的学习率;随着迭代次数增多,逐渐靠近目标点,应该放慢步伐,即选择较小的学习率,以防错过目标点。 这个方案的专业名称叫做Vanilla Gradient Descent,数学表达式为:
$$
w^{t+1} = w^{t}-\eta^{t} g^{t}
$$
$$
\eta^{t}=\frac{\eta}{\sqrt{t+1}},g^{t}=\frac{\partial L\left(\theta^{t}\right)}{\partial w}
$$
$$
其中t为迭代次数,\eta^{t}为学习率,g^{t}为梯度
$$
    但这个方案的核心问题是对于每个参数,学习率的变化是一样的,而稍微思考一下,对于不同的参数,最优的学习率变化必定有差异,即最好的方案是对于不同的参数,我们能够选择不同的动态学习率。Adagrad就是基于这种考虑的一种方案,数学表达式为:
$$
w^{t+1} = w^{t}-\frac{\eta^{t}}{\sigma^{t}} g^{t}
$$
$$
\eta^{t}=\frac{\eta}{\sqrt{t+1}},g^{t}=\frac{\partial L\left(\theta^{t}\right)}{\partial w},\sigma^{t}=\sqrt{\frac{1}{t+1} \sum_{i=0}^{t}\left(g^{i}\right)^{2}}
$$
进一步化简可得:
$$
w^{t+1} \leftarrow w^{t}-\frac{\eta}{\sqrt{\sum_{i=0}^{t}\left(g^{i}\right)^{2}}} g^{t}
$$
    关于Adagrad的合理性的解释,个人不是很同意李老师的解释,感觉有点牵强。以现有的知识水平,个人认为Adagrad综合了:迭代次数越多,学习率需要逐渐降低若前面的迭代中的平均梯度偏小(或偏大),学习率也应当较大(或较小)两种情况的考量。但从数学上解释,还不能完全理解,以后如果有更深的理解会回来更新。
    炼丹可不能纸上谈兵,代码实践才是王道,我们将上篇的代码稍微做个更改以实现Adagrad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import matplotlib.pyplot as plt
import numpy as np
x_data = [338.,333.,328.,207.,226.,25.,179.,60.,208.,606.]
y_data = [640.,633.,619.,393.,428.,27.,193.,66.,226.,1591.]
#y_data = b + w*x_data
x = np.arange(-200,-100,1)#bias的集合
y = np.arange(-5,5,0.1)#weight的集合
Z = np.zeros((len(x),len(y)))
X,Y = np.meshgrid(x,y)
#bia与weight集合随意两个搭配,组成function set
for i in range(len(x)):
for j in range(len(y)):
b = x[i]
w = y[j]
Z[j][i] = 0
for n in range(len(x_data)):
Z[j][i] = Z[j][i] + (y_data[n]-b-w*x_data[n])**2#L(w,b)
Z[j][i] = Z[j][i]/len(x_data)#实质上就是平均均方误差,L(w,b)/length
#梯度下降法起点(b0,w0)
b = -120
w = -4
lr = 1
b_lr = 0#learning rate
w_lr = 0
iteration = 100000
b_history = [b]
w_history = [w]
b_sum_grad = 0#参数b的梯度的平方和
w_sum_grad = 0#参数w的梯度的平方和
#iterations,这里迭代一万次,用尽量多的次数来接近理论上的谷点
for i in range(iteration):
b_grad = 0.0
w_grad = 0.0
for n in range(len(x_data)):
b_grad = b_grad - 2.0*(y_data[n]-b-w*x_data[n])*1.0#dL/db
w_grad = w_grad - 2.0*(y_data[n]-b-w*x_data[n])*x_data[n]#dL/dw
b_sum_grad = b_sum_grad + b_grad**2
w_sum_grad = w_sum_grad + w_grad**2
#(b,w)迭代
b_lr = lr/np.sqrt(b_sum_grad)
w_lr = lr/np.sqrt(w_sum_grad)
b = b - b_lr*b_grad
w = w - w_lr*w_grad

#存储(b,w)的历史值,方便后面绘制轨迹
b_history.append(b)
w_history.append(w)

#plot the figure
plt.contourf(X,Y,Z,50,alpha=0.5,cmap=plt.get_cmap('jet'))#等高线函数
plt.plot([-188.4],[2.67],'x',ms=12,markeredgewidth=3,color='orange')#[-188.4][2.67]是最终找到的谷点
plt.plot(b_history,w_history,'o-',ms=3,lw=1.5,color='black')
plt.xlim(-200,-100)
plt.ylim(-5,5)
plt.xlabel(r'$b$',fontsize=16)
plt.ylabel(r'$w$',fontsize=16)
plt.show()
print("over!")

    效果还是挺显著的,见下图。

2、Stochastic Gradient Descent(随机梯度下降法)

    对于随机梯度下降法,这里我们引用博主楼燚航的理解:

    相对于原始的随机梯度下降法,可以看到多了随机两个字,随机也就是说用样本中的一个例子来近似所有的样本,来调整θ,因而随机梯度下降是会带来一定的问题,因为计算得到的并不是准确的一个梯度,容易陷入到局部最优解中。

    下图是原始梯度下降法随机梯度下降法的公式对比:
$$
L=\sum_{n}\left(\hat{y}^{n}-\left(b+\sum w_{i} x_{i}^{n}\right)\right)^{2},\theta^{i}=\theta^{i-1}-\eta \nabla L\left(\theta^{i-1}\right),原始梯度下降法
$$
$$
L^{n}=\left(\hat{y}^{n}-\left(b+\sum w_{i} x_{i}^{n}\right)\right)^{2},\theta^{i}=\theta^{i-1}-\eta \nabla L^{n}\left(\theta^{i-1}\right),随机梯度下降法
$$
其中,$\nabla$是指梯度,$L^{n}$表达式中的$n$是随机的,意为从所有数据中随机抽出一组,利用这组数据的损失(loss)代表全局损失(loss)。
    可以很清楚的看到,随机梯度下降法的确是利用所有样本中的一个例子来近似所有样本,相信也会有读者对其合理性产生质疑,我的理解是:随机梯度下降法是为了减少计算量,尝试将所有超参数向着局部损失减小的方向进行,而局部损失非常小的点可能恰是全局最优点——因为全局最优时,局部损失其实也就必定很小。
    在实际应用中,随机梯度下降法因为迭代速度非常快,大受欢迎,最大的缺点就是最终收敛的解不一定是全局最优解。下面我们继续对上一节的代码进行更改,测试下随机梯度下降法的实际表现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import matplotlib.pyplot as plt
import numpy as np
import random
x_data = [338.,333.,328.,207.,226.,25.,179.,60.,208.,606.]
y_data = [640.,633.,619.,393.,428.,27.,193.,66.,226.,1591.]
#y_data = b + w*x_data
x = np.arange(-200,-100,1)#bias的集合
y = np.arange(-5,5,0.1)#weight的集合
Z = np.zeros((len(x),len(y)))
X,Y = np.meshgrid(x,y)
#bia与weight集合随意两个搭配,组成function set
for i in range(len(x)):
for j in range(len(y)):
b = x[i]
w = y[j]
Z[j][i] = 0
for n in range(len(x_data)):
Z[j][i] = Z[j][i] + (y_data[n]-b-w*x_data[n])**2#L(w,b)
Z[j][i] = Z[j][i]/len(x_data)#实质上就是平均均方误差,L(w,b)/length
#梯度下降法起点(b0,w0)
b = -120
w = -4
lr = 0.000001
iteration = 0
b_history = [b]
w_history = [w]
loss = 10**6#总损失
threshold = 10**5+2000#最大允许误差
max_iters = 1000000#最大迭代次数
#iterations,这里迭代一万次,用尽量多的次数来接近理论上的谷点
while(loss > threshold):
loss = 0
b_grad = 0.0
w_grad = 0.0
n = random.randint(0,9)
b_grad = b_grad - 2.0*(y_data[n]-b-w*x_data[n])*1.0#dL/db
w_grad = w_grad - 2.0*(y_data[n]-b-w*x_data[n])*x_data[n]#dL/dw
b = b - lr*b_grad
w = w - lr*w_grad
#存储(b,w)的历史值,方便后面绘制轨迹
b_history.append(b)
w_history.append(w)
#计算全局损失,以判断是否应该终止迭代
for n in range(len(x_data)):
y_pre = b + w*x_data[n]
loss = loss + (y_data[n]-y_pre)**2
loss = loss/len(x_data)
iteration = iteration + 1
if iteration > max_iters:
break

#plot the figure
print("iterations:%d"%(iteration))
plt.contourf(X,Y,Z,50,alpha=0.5,cmap=plt.get_cmap('jet'))#等高线函数
plt.plot([-188.4],[2.67],'x',ms=12,markeredgewidth=3,color='orange')#[-188.4][2.67]是最终找到的谷点
plt.plot(b_history,w_history,'o-',ms=3,lw=1.5,color='black')
plt.xlim(-200,-100)
plt.ylim(-5,5)
plt.xlabel(r'$b$',fontsize=16)
plt.ylabel(r'$w$',fontsize=16)
plt.show()

输出:iterations = 4861428

    因为阈值没有设置为和目标点误差完全一样,所以运行后只到达了目标点附近。从结果图,可以明显看出,随机梯度下降法并没有想象中那么给力,还是有一些缺陷,比如迭代过程中波动严重,这是因为每次迭代时,算法不一定按照全局最优方向进行,所以间接导致迭代次数增加。不过,运算速度确实快,迭代了4861428次,总时间也没比Adagrad的100000次少多少。

3、Momentum(动量法)

    Momentum在中文里被译为动量,即高中物理所学的那个动量。Momentum梯度下降法的核心思想也正是基于动量(或者说惯性)的原理而来的,个人找到的对该方法原理最直观的解释如下(链接:https://blog.csdn.net/tsyccnh/article/details/76270707):

如果把梯度下降法想象成一个小球从山坡到山谷的过程,那么一般梯度下降法的小球是这样移动的:从A点开始,计算当前A点的坡度,沿着坡度最大的方向走一段路,停下到B。在B点再看一看周围坡度最大的地方,沿着这个坡度方向走一段路,再停下。确切的来说,这并不像一个球,更像是一个正在下山的盲人,每走一步都要停下来,用拐杖来来探探四周的路,再走一步停下来,周而复始,直到走到山谷。而一个真正的小球要比这聪明多了,从A点滚动到B点的时候,小球带有一定的初速度,在当前初速度下继续加速下降,小球会越滚越快,更快的奔向谷底。 momentum 动量法就是模拟这一过程来加速神经网络的优化的。

    让我们从最简单的情况开始——只有一个参数w需要学习,即上图所示的情况。此时的Loss曲线是简单的曲线,Loss关于w的梯度方向是沿x方向的一维向量,只能向前或者向后:
$$
\nabla L = \frac{\partial L}{\partial w}
$$
一般的梯度下降法会按如下方式移动:
$$
w^t = w^{t-1} - \eta^{t-1}\nabla L(w^{t-1})
$$
而Momentum梯度下降法则会考虑小球从上一个位置移动到当前位置残留的动量(这里的动量实际上就是上一个位置的负梯度向量,也即小球剩余的水平速度):
$$
\begin{array}{l}
v^{t}=\mu v^{t-1}-\eta^{t-1} \nabla L\left(w^{t-1}\right) \
w^{t}=w^{t-1}+v^{t} \
其中v^t代表第t次更新参数积累的动量,也即第t次参数更新的方向; \
v^0 = 0, 即起始位置的动量为0; \
0 \le \mu \le 1是人工可调节的常数,一般为0.9; \
t \ge 1 ;\
\end{array}
$$
可能一眼看上去不好理解,我们可以一步步来:
$$
\begin{array}{l}
v^{1}=\mu v^{0}-\eta^{0} \nabla L\left(w^{0}\right) = -\eta^{0} \nabla L\left(w^{0}\right) \
w^{1}=w^{0}+v^{1} \
v^{2}=\mu v^{1}-\eta^{1} \nabla L\left(w^{1}\right) \
w^{2}=w^{1}+v^{2} \

\end{array}
$$
    结合上图进行理解,t = 1时,小球从起始点开始移动到第二个位置,由于是起始点,小球并无初速度,因此第一次移动积累的动量为0,迭代方式和普通梯度下降法一致;t=2时,小球从第二个位置开始移动到第三个位置,这时观察$v^2$ 可以发现多了个$\mu v^1$ ,从物理学角度,这可以理解为动量或者说惯性的作用,因为小球从起始点移动到第二个位置时还保留着水平速度,在进行第二次移动时,Momentum梯度下降法考虑了这个速度,将其和当前位置的梯度下降方向进行结合,以此来作为小球最终的行进方向。这点正和本节开头阐述的思想一致。
    其实抛却“动量”这个对非物理专业可能比较难懂的概念,Momentum梯度下降法中的动量$v^t$可以做如下的简化理解:

$v^t$可以简单理解为,在进行第t次参数更新的时候,将第t-1次参数更新的方向与第t次计算得到的负梯度向量进行矢量叠加(原本的参数更新方向),并作为第t次参数更新的实际方向。

    上面小球的例子只考虑了一个参数的简单情况,对于多个参数,原理也同理,因为无论有多少个参数,梯度都是空间中的向量,其表达方式不会变,这里引用博客中一张非常直观的图来进行理解。

对于本文,图中的梯度下降方向应该理解为负梯度向量方向,
momentum的公式不止一种

    相对于一般的梯度下降法,Momentum梯度下降法最显著的优点就是不容易卡在局部最优点,因为当处于局部最优点的时候,由于“动量”的作用,“小球”会冲出局部最优点。实际运用中,其一般是结合SGD一起使用的。
    最后当然是代码实践环节,参考了下代码,也是来源于上面的博客。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import matplotlib.pyplot as plt
import numpy as np
import random
from mpl_toolkits.mplot3d import Axes3D
x_data = [338.,333.,328.,207.,226.,25.,179.,60.,208.,606.]
y_data = [640.,633.,619.,393.,428.,27.,193.,66.,226.,1591.]
#y_data = b + w*x_data
x = np.arange(-200,-100,1)#bias的集合
y = np.arange(-5,5,0.1)#weight的集合
Z = np.zeros((len(x),len(y)))
X,Y = np.meshgrid(x,y)
#bia与weight集合随意两个搭配,组成function set
for i in range(len(x)):
for j in range(len(y)):
b = x[i]
w = y[j]
#matplotlib绘制3D图时都是默认按列优先的顺序进行绘制,一般都要进行转置
#这里用Z[j][i]其实就是提前转置
Z[j][i] = 0
for n in range(len(x_data)):
Z[j][i] = Z[j][i] + (y_data[n]-b-w*x_data[n])**2#L(w,b)
Z[j][i] = Z[j][i]/len(x_data)#实质上就是平均均方误差,L(w,b)/length
#梯度下降法起点(b0,w0)
b = -120
w = -4
lr = 0.0000001
b_history = [b]
w_history = [w]
loss = 10**6#总损失
threshold = 10**5+2000#最大允许误差
max_iters = 10000#最大迭代次数
gamma = 0.7
#动量
vb = 0
vw = 0
#plot the figure
fig = plt.figure(1)
fig.suptitle('learning rate: %.6f method:momentum'%(lr))
#绘制三维曲面图
ax = fig.add_subplot(1,2,1,projection='3d')
ax.plot_surface(X,Y,Z,cmap='rainbow')
#绘制等高线
plt.subplot(1,2,2)
#填充等高线曲面
plt.contourf(X,Y,Z,50,alpha=0.5,cmap=plt.get_cmap('jet'))#等高线函数
plt.xlim(-200,-100)
plt.ylim(-5,5)
plt.xlabel(r'$b$',fontsize=16)
plt.ylabel(r'$w$',fontsize=16)
plt.plot([-188.4],[2.67],'x',ms=12,markeredgewidth=3,color='orange')#[-188.4][2.67]是最终找到的谷点

plt.ion()
#iterations,这里迭代一万次,用尽量多的次数来接近理论上的谷点
for iteration in range(1,max_iters+1):
loss = 0
b_grad = 0.0
w_grad = 0.0
for n in range(0,len(x_data)):
b_grad = b_grad - 2.0*(y_data[n]-b-w*x_data[n])*1.0#dL/db
w_grad = w_grad - 2.0*(y_data[n]-b-w*x_data[n])*x_data[n]#dL/dw
vb = gamma*vb - lr*b_grad
vw = gamma*vw - lr*w_grad
b = b + vb
w = w + vw
#存储(b,w)的历史值,方便后面绘制轨迹
b_history.append(b)
w_history.append(w)
#计算全局损失,以判断是否应该终止迭代
for n in range(len(x_data)):
y_pre = b + w*x_data[n]
loss = loss + (y_data[n]-y_pre)**2
loss = loss/len(x_data)#前面Z[j][i]也除了len(x_data),做统一处理
#绘制三维图面图上的动态轨迹
ax.scatter(b,w,loss,s=10,color='black')
#绘制等高线图上的动态轨迹
plt.subplot(1,2,2)
plt.scatter(b,w,s=5,color='black')
plt.plot(b_history,w_history,'o-',ms=3,lw=1.5,color='black')
plt.show()
plt.pause(0.01)
print("iterations:%d, loss:%d"%(iteration,loss))
plt.show()
plt.pause(999999999)

    很不幸,由于没有对数据做归一化,最终“小球”就是不断在谷底来回”徘徊“,始终到不了终点。但确实可以非常直观地看到,当learning rate和$\mu$设置得稍微大一些,“小球”直接冲出谷底,绝不回头,不信可以试试,代码运行结果是动态的!

learing rate = 0.0000001, 动量因子为0.7

小结

    本小节主要讲了两种梯度下降法的优化算法,当然还有Adam、NAG、Momentum等其它优化算法,以后用到的时候会滚回来更新。

  • Adagrad算法通过针对不同的参数,动态选择不同的学习率,来优化原始梯度下降法。
  • Stochastic Gradient Descent(随机梯度下降法) 算法通过随机选取一组样本中的一个样本,来近似代表所有样本,利用其损失函数梯度代替全局损失函数梯度来更新超参数,以获得更快的学习速度,但容易陷入局部最优且迭代过程波动严重。
  • Momentum 算法通过模拟现实世界中物理运动的动量(惯性)作用,在每次更新参数时,将上一次参数更新的方向与当前点的负梯度下降方向进行矢量相加,作为最终的参数更新方向。由于“动量”的作用,该算法可以有效解决一般算法容易陷入“局部最优点”的缺点。

评论


当我沿着一条路走下去的时候,心里总想着另一条路上的事。这种时候,我心里很乱。放声大哭从一个梦境进入另一个梦境,这是每个人都有的奢望。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×