返回介绍

Caffe 中的 SGD 的变种优化算法(1)

发布于 2025-02-25 23:04:58 字数 5854 浏览 0 评论 0 收藏 0

Caffe 中还有一个很重要的部分,那就是优化算法。在专栏的一开始,我们就从优化入手,聊了两个优化算法 - 随机梯度下降和动量(感谢众多知友指出我之前翻译的错误……)。下面我们沿着前面的步伐,继续前进。

写在前面

说实话设计优化算法和解释优化算法的合理性实际上是一个不太容易的事情。这里我们主要关注算法的实际效果,因此我们对于算法的理论推导就暂时放在一边了。而关于理论,我们只简单讲两句,不做过多的深入,重点是看算法在我们设计的场景下的表现。

非凸函数

之前在我们的算法演示中,我们展示的函数都是凸函数。凸函数有哪些好处,谁来讲一讲?

(侵删)

  1. 它通常有唯一的极值点。
  2. Hessian 矩阵正定。

而下面我们将展示一个函数,这个函数可没有这么多好的性质了:

f(x,y)=x^2-2x+100xy+10y^2+20y

在等高线图上,它的样子是这样的:

这里的配色采用绘图中的经典彩虹色,其中颜色越靠近蓝色表示函数值越大,颜色越靠近红色表示函数值越小。这和我们的直觉是相符的,一三象限的数字大,二四象限的数字小。

我们的第一个尝试就是从一个比较正常的位置出发 - (5,5)点,看看各种算法的效果

在这个新的环境下,我们的老算法 - 梯度下降,动量,Nesterov 算法会不会焕发第二春呢?

有请老队员上场

首先是梯度下降法,由于前面我们已经贴过代码了,这里我们就不再贴代码了,直接上结果:

res, x_arr = gd([5,5], 0.0008, g)
contour(X,Y,Z, x_arr)

为了保证算法经过 50 轮迭代不会超出我们限定的范围,我们调整了它的 learning rate。可以看出算法在迭代过程中先是冲向了局部最优点,也就是我们常说的鞍点,然后发现不对,调转头冲向了真正的优化点,祝它一路走好……

然后是动量算法:

res, x_arr = momentum([5, 5], 3e-4, g)
contour(X,Y,Z, x_arr)

一切顺利!不同的演员同样的剧本,演出来的效果还是不错的。

好了,到这里我们的老队员登场完毕,下面看看新队员了。

Adagrad

Adagrad 是一种自适应的梯度下降方法,它希望根据梯度的更新量调节梯度实际的影响值。如果一个变量的梯度更新量比较大,那么再后面的更新过程中我们会适当减少它的更新量,如果更新量比较小,那么我们可以适当地增加它的更新量。

它的参数更新公式如下所示:

x -= lr * \frac{g}{\sqrt{\sum{g^2}} + \varepsilon }

它的核心代码如下所示:

def adagrad(x_start, step, g, delta=1e-8):
    x = np.array(x_start, dtype='float64')
    sum_grad = np.zeros_like(x)
    for i in range(50):
        grad = g(x)
        sum_grad += grad * grad
        x -= step * grad / (np.sqrt(sum_grad) + delta)
        if abs(sum(grad)) < 1e-6:
            break;
    return x

我们直接来看看它的表现:

res, x_arr = adagrad([5, 5], 1.3, g)
contour(X,Y,Z, x_arr)

看上去和前面的算法没什么差别嘛……

Rmsprop

前面的 Adagrad 有一个很大的问题,那就是随着优化的进行,更新公式的分母项会变得越来越大。所以理论上更新量会越来越小,下面的算法 Rmsprop 就试图解决这个问题。在它的算法中,分母的梯度平方和不再随优化而递增,而是做加权平均:

G_{t+1}=\beta*G_{t}+(1-\beta)*g^2
x_{t+1} = x_t - lr * \frac{g}{\sqrt{G_{t+1}} + \varepsilon }

它的代码如下所示:

def rmsprop(x_start, step, g, rms_decay = 0.9, delta=1e-8):
    x = np.array(x_start, dtype='float64')
    sum_grad = np.zeros_like(x)
    passing_dot = [x.copy()]
    for i in range(50):
        grad = g(x)
        sum_grad = rms_decay * sum_grad + (1 - rms_decay) * grad * grad
        x -= step * grad / (np.sqrt(sum_grad) + delta)
        passing_dot.append(x.copy())        
        if abs(sum(grad)) < 1e-6:
            break;
    return x, passing_dot

我们同样来看看它的表现:

res, x_arr = rmsprop([5, 5], 0.3, g)
contour(X,Y,Z, x_arr)

看上去和前面的表现还是差不多的。

AdaDelta

Rmsprop 已经很好地解决了一部分的问题,但是在 AdaDelta 的眼中似乎还不够。在介绍它的论文中,作者详细阐述了 Adagrad 算法中得到的参数更新量的“单位”已经不太对了,于是给出了一堆公式用以证明我们需要增加一些计算来使得计算单位恢复正常,于是就有了下面的公式:

G_{t+1}=\beta*G_{t}+(1-\beta)*g^2
\delta_{t+1}=\sqrt{\frac{\Delta_t+\varepsilon}{G_{t+1}+ \varepsilon}  }*g
x_{t+1} = x_{t} - lr * \delta_{t+1}
\Delta_{t+1}=\beta*\Delta_t+(1-\beta)*\delta_{t+1}

具体的代码如下:

def adadelta(x_start, step, g, momentum = 0.9, delta=1e-1):
    x = np.array(x_start, dtype='float64')
    sum_grad = np.zeros_like(x)
    sum_diff = np.zeros_like(x)
    passing_dot = [x.copy()]
    for i in range(50):
        grad = g(x)
        sum_grad = momentum * sum_grad + (1 - momentum) * grad * grad
        diff = np.sqrt((sum_diff + delta) / (sum_grad + delta)) * grad
        x -= step * diff
        sum_diff = momentum * sum_diff + (1 - momentum) * (diff * diff)
        passing_dot.append(x.copy())
        if abs(sum(grad)) < 1e-6:
            break;
    return x, passing_dot

它的表现如下:

虽然上面算法的表现差不多,但是它们的 learning rate 还是有些不同的,感兴趣的童鞋可以换一些 learning rate 去尝试。

看上去我们并没有把算法讲完,也没有把实验做完……没错,留下尾巴,剩下的事情我们下回再说。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文