*注:本博客参考李宏毅老师2020年机器学习课程. 视频链接
学习速率的设置问题
从前面的实验来看,在模型训练过程中调整学习速率是一件十分麻烦的事情,主要表现在:
- lr设置过大:
- 模型难以得到最优的参数,出现微分“震荡”现象;
- 更新参数的值过大,导致更新后的参数剧烈膨胀,超出上限;
- lr设置过小:
- 参数更新缓慢,求得最佳模型的时间大幅增加;
- 偏导乘以lr过小导致根本无法更新参数;
观察实验结果可以得出一下结论:
- 由于一开始的模型参数太差,此时较大的lr有助于快速更新到与正确的参数相近的情形;
- 经过较多次迭代后,较小的lr有助于模型更精确地得到正确的参数,而不会出现“震荡”;
- 更高次项的参数更新速度应该慢于低次项,否则参数容易爆炸。
综上所述,虽然梯度下降算法实现简单,在实际使用过程中会出现很多问题。为了使得学习速率lr能够根据训练阶段和参数自动更新,引入了优化器。
Adagrad算法
该算法考虑到了上述情形,具有以下特点:
- 为每一个参数单独设置一个学习速率;
- 学习速率随训练周期而逐渐变小;
Adagrad算法调整学习速率可以用以下公式来表达:
w
t
+
1
=
w
t
−
η
t
σ
t
g
t
w^{t+1}=w^t-\frac{\eta^t}{\sigma^t}g^t
wt+1=wt−σtηtgt
其中
t
t
t表示第t个epoch,
η
t
{\eta^t}
ηt表示一个训练时间相关的函数,
σ
t
\sigma^t
σt表示过去所有导数值的均方根,即
σ
t
=
1
t
+
1
∑
i
=
0
t
g
i
2
\sigma^t=\sqrt{\frac{1}{t+1}\sum_{i=0}^{t}{{g^i}^2}}
σt=t+11i=0∑tgi2
g
t
g^t
gt表示导数值。
在Adagrad中,
η
t
=
η
t
+
1
\eta^t=\frac{\eta}{\sqrt{t+1}}
ηt=t+1η,
η
\eta
η是提前设置好的学习速率。
经过化简,上述式子可以简化为:
w
t
+
1
=
w
t
−
η
∑
i
=
0
t
g
i
2
g
t
w^{t+1}=w^t-\frac{\eta}{\sqrt{\sum_{i=0}^{t}{{g^i}^2}}}g^t
wt+1=wt−∑i=0tgi2ηgt
用python表示上述公式可以写成:
self.w -= d_w*lr/np.sqrt(self.sum_dw2)
以下附上实现代码,经过实验,改进后的算法不再会发生溢出,参数更新也不再会爆炸。
# Author Xiangrui Li
# Date 2021-07-01
import numpy as np
from matplotlib import pyplot as plt
class Model:
def __init__(self, level=1):
# 初始化w为一随机值,注意0次项b也包含在w中,且为w的第1项
self.w = np.random.rand(1, level+1)*0.1
self.name = "x^{}".format(level)
def get_result(self):
text = "y={:.2f}".format(self.w[0, 0])
for i in range(1, self.w.shape[1]):
if i > 1:
text += "+{:.2f}x^{}".format(self.w[0, i], i)
else:
text += "+{:.2f}x".format(self.w[0, i])
return text
def show(self, show_loss_figure=False):
"""
显示结果
"""
text = self.get_result()
print(text)
self.result = text
print("abs loss: {} ".format(self.log_loss[-1]))
if show_loss_figure and self.log_loss and len(self.log_loss) > 0:
plt.title(text)
plt.plot(self.log_loss)
plt.show()
def forward(self, xs):
"""
单步计算
"""
xs = self.__get_cal_metrix__(xs)
# y=w*x
return self.w.dot(xs)[0]
def __get_cal_metrix__(self, xs):
# 复制行保持与w行数相同
xx = np.tile(xs, (self.w.shape[1], 1))
# 第一行设为1确保改行计算后为b的值
xx[0, :] = 1
# 从第二行开始每行自乘一次得到x的n次方项
for i in range(1, xx.shape[0]):
xx[i, :] *= xx[i-1, :]
return xx
def __train_one_step__(self, xs, y0s, L2, lr):
"""
单步训练,使用Adagrad作为优化器
"""
# y=w*x
y1s = self.w.dot(xs)
# 计算损失
loss = y1s-y0s
# 计算每个训练数值的偏导值
d_w = loss * xs
# 行求和得到所有数据的偏导值之和
d_w = d_w.sum(axis=1)
# L2正则化
if L2 > 0:
d_w += self.w[0]*L2
self.sum_dw2 += d_w**2
# 更新参数
# 使用Adagrad算法动态调整更新速率
self.w -= d_w*lr/np.sqrt(self.sum_dw2)
return loss
def train(self, xs, ys, lr=1e-8, epoch=50000, L2=0):
"""
开始训练的函数
"""
self.sum_dw2 = 0
self.log_loss = []
xx = self.__get_cal_metrix__(xs)
# 开始训练
for i in range(epoch):
loss = self.__train_one_step__(xx, ys, L2, lr)
self.log_loss.append(np.abs(loss).sum())
data = np.array([x for x in range(1000)])
data_y = data*5+8+data**2*3
m = Model(2)
m.train(data, data_y, lr=1,L2=100)
m.show(show_loss_figure=True)
y=6.83+4.28x+3.00x^2
abs loss: 93669.72494314221
Adagrad算法原理解析
从Adagrad的计算式
w
t
+
1
=
w
t
−
η
∑
i
=
0
t
g
i
2
g
t
w^{t+1}=w^t-\frac{\eta}{\sqrt{\sum_{i=0}^{t}{{g^i}^2}}}g^t
wt+1=wt−∑i=0tgi2ηgt
中,我们可以得知以下事实:
- 由于学习速率乘以一项 g t g^t gt,因此如果某一次参数更新时计算的导数较大,则更新的值越大;
- 由于分母是所有偏导值的均方根,所以如果计算的导数较大,则更新的值越小。
很容易发现,上述两个特性是矛盾的,那为什么Adagrad要这么设置呢?
参数更新的最佳值
假设有函数
y
=
2
x
+
1
y=2x+1
y=2x+1,计算机使用函数
y
=
w
x
+
b
y=wx+b
y=wx+b来学习它。则使用均方差的损失函数可以表示为:
L
o
s
s
=
(
y
1
−
y
0
)
2
=
x
2
w
2
+
(
2
b
x
−
4
x
2
−
2
x
)
w
+
(
4
x
2
+
4
x
+
b
2
−
4
b
x
−
2
b
+
1
)
Loss=\left( y_1-y_0 \right)^2=x^2w^2+\left(2bx-4x^2-2x\right)w+\left(4x^2+4x+b^2-4bx-2b+1\right)
Loss=(y1−y0)2=x2w2+(2bx−4x2−2x)w+(4x2+4x+b2−4bx−2b+1)
b**2 - 4*b*x - 2*b + w**2*x**2 + w*(2*b*x - 4*x**2 - 2*x) + 4*x**2 + 4*x + 1
假定此时要更新w,则不妨设b=0,x=1,则 L o s s = A w 2 + B w + C = w 2 − 6 w + 9 Loss=Aw^2+Bw+C=w^2-6w+9 Loss=Aw2+Bw+C=w2−6w+9,画出loss对w的图像:
不难求出该图像的最低点在 w = − B 2 A = 3 w=-\frac{B}{2A}=3 w=−2AB=3时取到。假设现在 w = 10 w=10 w=10,那么按照梯度下降算法,求出该点处的导数为: g t = 2 A w + B = 2 w − 6 = 14 g^t=2Aw+B=2w-6=14 gt=2Aw+B=2w−6=14
如果使用Adagrad算法,参数更新应该为:
w
t
+
1
=
w
t
−
14
η
w^{t+1}=w^t-14\eta
wt+1=wt−14η
然而我们很容易求得,从w=10这个点到loss最小的点的横坐标距离为:
d
i
s
=
w
+
B
2
A
=
2
A
w
+
B
2
A
=
7
dis=w+\frac{B}{2A}=\frac{2Aw+B}{2A}=7
dis=w+2AB=2A2Aw+B=7
不难发现,dis的分子就是该点处的一阶导数,而分母就是该点处的二阶导数。
但是,由于实际情况下,二阶导数求导复杂,并且求解二阶导数需要二外的运算开销,因此往往不使用。那么有没有办法用其他计算近似地求得二阶导数的值呢?
这就是Adagrad所做的,它使用过去所有导数值的均方根来模拟二阶导数的值。
假设有两个函数:
y
=
5
x
2
y=5x^2
y=5x2和
y
=
x
2
y=x^2
y=x2,其一阶导数的绝对值的图像如下图:
如果分别在两函数一阶导数上取若干个点,求其均方差,假设取得的点足够多且均匀分布的话,那么二阶导数较高的函数,所得到的一阶导数的均方差也是更大的。从而可以通过一阶导数的均方差大小来比较二阶导数值的大小,达到使用一阶导数模拟二阶导数值的目的。