机器学习笔记1-2:优化器

*注:本博客参考李宏毅老师2020年机器学习课程. 视频链接


学习速率的设置问题

从前面的实验来看,在模型训练过程中调整学习速率是一件十分麻烦的事情,主要表现在:

  • lr设置过大:
    • 模型难以得到最优的参数,出现微分“震荡”现象;
    • 更新参数的值过大,导致更新后的参数剧烈膨胀,超出上限;
  • lr设置过小:
    • 参数更新缓慢,求得最佳模型的时间大幅增加;
    • 偏导乘以lr过小导致根本无法更新参数;

观察实验结果可以得出一下结论:

  1. 由于一开始的模型参数太差,此时较大的lr有助于快速更新到与正确的参数相近的情形;
  2. 经过较多次迭代后,较小的lr有助于模型更精确地得到正确的参数,而不会出现“震荡”;
  3. 更高次项的参数更新速度应该慢于低次项,否则参数容易爆炸。

综上所述,虽然梯度下降算法实现简单,在实际使用过程中会出现很多问题。为了使得学习速率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=0tgi2
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=wti=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=wti=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=(y1y0)2=x2w2+(2bx4x22x)w+(4x2+4x+b24bx2b+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=w26w+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=2w6=14


如果使用Adagrad算法,参数更新应该为: w t + 1 = w t − 14 η w^{t+1}=w^t-14\eta wt+1=wt14η


然而我们很容易求得,从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,其一阶导数的绝对值的图像如下图:

如果分别在两函数一阶导数上取若干个点,求其均方差,假设取得的点足够多且均匀分布的话,那么二阶导数较高的函数,所得到的一阶导数的均方差也是更大的。从而可以通过一阶导数的均方差大小来比较二阶导数值的大小,达到使用一阶导数模拟二阶导数值的目的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值