程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

人工智能编程:如何完成神经网络模型的优化?

balukai 2025-03-29 15:07:30 文章精选 14 ℃

人工智能专栏限时打折



本文重点

在机器学习或者深度学习中,我们需要通过修改参数使得损失的数最小化(或最大化),优化算法就是一种调整模型参数更新的策略。

随机梯度下降算法

随机梯度下降优化算法,在pytorch中,我们使用如下命令就可以使用这个优化算法

optimzier = torch.optim.SGD(net.parameters(), 1e-2)

然后使用optimzier.step()进行梯度更新

如果我们自己实现,那就是

def sgd_update(parameters, lr):

for param in parameters:

param.data = param.data - lr * param.grad.data

然后我们就可以直接使用了

net.zero_grad()

loss.backward()

sgd_update(net.parameters(), 1) # 使用 1.0 的学习率

动量法

使用梯度下降法,每次都会朝着目标函数下降最快的方向,这也称为最速下降法。这种更新方法看似非常快,实际上存在一些问题。

假如我们的损失函数的等高线如图所示,可以想象成一个很扁的漏斗,这样在竖直方向上,梯度就非常大,在水平方向上,梯度就相对较小,所以我们在设置学习率的时候就不能设置太大,为了防止竖直方向上参数更新太过了,这样一个较小的学习率又导致了水平方向上参数在更新的时候太过于缓慢,所以就导致最终收敛起来非常慢。动量法的提出就是为了应对这个问题,我们梯度下降法做一个修改如下

其中 vivi 是当前速度,γγ 是动量参数,是一个小于 1的正数,ηη 是学习率,相当于每次在进行参数更新的时候,都会将之前的速度考虑进来,每个参数在各方向上的移动幅度不仅取决于当前的梯度,还取决于过去各个梯度在各个方向上是否一致,如果一个梯度一直沿着当前方向进行更新,那么每次更新的幅度就越来越大,如果一个梯度在一个方向上不断变化,那么其更新幅度就会被衰减,这样我们就可以使用一个较大的学习率,使得收敛更快,同时梯度比较大的方向就会因为动量的关系每次更新的幅度减少,如下图

本质上说,动量法就仿佛我们从高坡上推一个球,小球在向下滚动的过程中积累了动量,在途中也会变得越来越快,最后会达到一个峰值,对应于我们的算法中就是,动量项会沿着梯度指向方向相同的方向不断增大,对于梯度方向改变的方向逐渐减小,得到了更快的收敛速度以及更小的震荡。

def sgd_momentum(parameters, vs, lr, gamma):

for param, v in zip(parameters, vs):

v[:] = gamma * v + lr * param.grad.data

param.data = param.data – v

net.zero_grad()

loss.backward()

sgd_momentum(net.parameters(), vs, 1e-2, 0.9)

optimizer = torch.optim.SGD(net.parameters(), lr=1e-2, momentum=0.9) # 加动量

adagrad

SGD和momentum都是用固定的学习率,adagrad

这个优化算法被称为自适应学习率优化算法,有时候不同的参数梯度可能不一样,所以需要不同的学习率才能比较好的进行训练,但是这个事情又不能很好地被人为操作,所以 Adagrad 便能够帮助我们做这件事。

Adagrad 算法

Adagrad 的想法非常简答,在每次使用一个 batch size 的数据进行参数更新的时候,我们需要计算所有参数的梯度,那么其想法就是对于每个参数,初始化一个变量 s 为 0,然后每次将该参数的梯度平方求和累加到这个变量 s 上,然后在更新这个参数的时候,学习率就变为

这里的 ε 是为了数值稳定性而加上的,因为有可能 s 的值为 0,那么 0 出现在分母就会出现无穷大的情况,通常 εε 取 10-1010-10,这样不同的参数由于梯度不同,他们对应的 s 大小也就不同,所以上面的公式得到的学习率也就不同,这也就实现了自适应的学习率。

Adagrad 的核心想法就是,如果一个参数的梯度一直都非常大,那么其对应的学习率就变小一点,防止震荡,而一个参数的梯度一直都非常小,那么这个参数的学习率就变大一点,使得其能够更快地更新

Adagrad 也有一些问题,因为 s 不断累加梯度的平方,所以会越来越大,导致学习率在后期会变得较小,导致收敛乏力的情况,可能无法收敛到表较好的结果,当然后面有一个对其的改进,我们之后会讲到

下面我们来实现一下 Adagrad 的算法

def sgd_adagrad(parameters, sqrs, lr):

eps = 1e-10

for param, sqr in zip(parameters, sqrs):

sqr[:] = sqr + param.grad.data ** 2

div = lr / torch.sqrt(sqr + eps) * param.grad.data

param.data = param.data – div

optimizer = torch.optim.Adagrad(net.parameters(), lr=1e-2)

RMSProp算法

前面我们提到了 Adagrad 算法有一个问题,就是学习率分母上的变量 s 不断被累加增大,最后会导致学习率除以一个比较大的数之后变得非常小,这不利于我们找到最后的最优解,所以 RMSProp 的提出就是为了解决这个问题。

RMSProp 算法P

RMSProp 仍然会使用梯度的平方量,不同于 Adagrad,其会使用一个指数加权移动平均来计算这个 s,也就是

这里 g 表示当前求出的参数梯度,然后最终更新和 Adagrad 是一样的,学习率变成了

这里 αα 是一个移动平均的系数,也是因为这个系数,导致了 RMSProp 和 Adagrad 不同的地方,这个系数使得 RMSProp 更新到后期累加的梯度平方较小,从而保证 s 不会太大,也就使得模型后期依然能够找到比较优的结果

def rmsprop(parameters, sqrs, lr, alpha):

eps = 1e-10

for param, sqr in zip(parameters, sqrs):

sqr[:] = alpha * sqr + (1 - alpha) * param.grad.data ** 2

div = lr / torch.sqrt(sqr + eps) * param.grad.data

param.data = param.data – div

optimizer = torch.optim.RMSprop(net.parameters(), lr=1e-3, alpha=0.9)

optimizer = torch.optim.SGD(model.parameters() , lr=0.01,momentum=0.9)

这样我们就将模型的参数作为需要更新的参数传入优化器,设定学习率lr是 0.01 ,动量momentum是 0.9 的随机梯度下降,在优化之前需要先将梯度归零,即 optimizer.zeros() , 然后通过 loss.backward()反向传播,自动求导得到每个参数的梯度,最后只需要 optimizer. step ()就可以通过梯度做一步参数更新。

Adam

Adam 是一个结合了动量法和 RMSProp 的优化算法,其结合了两者的优点。

def adam(parameters, vs, sqrs, lr, t, beta1=0.9, beta2=0.999):

eps = 1e-8

for param, v, sqr in zip(parameters, vs, sqrs):

v[:] = beta1 * v + (1 - beta1) * param.grad.data

sqr[:] = beta2 * sqr + (1 - beta2) * param.grad.data ** 2

v_hat = v / (1 - beta1 ** t)

s_hat = sqr / (1 - beta2 ** t)

param.data = param.data - lr * v_hat / torch.sqrt(s_hat + eps)


可以看到使用 adam 算法 loss 能够更快更好地收敛,但是一定要小心学习率的设定,使用自适应的算法一般需要更小的学习率

optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)

AdadeltaP

Adadelta 算是 Adagrad 法的延伸,它跟 RMSProp 一样,都是为了解决 Adagrad 中学习率不断减小的问题,RMSProp 是通过移动加权平均的方式,而 Adadelta 也是一种方法,有趣的是,它并不需要学习率这个参数。


def adadelta(parameters, sqrs, deltas, rho):

eps = 1e-6

for param, sqr, delta in zip(parameters, sqrs, deltas):

sqr[:] = rho * sqr + (1 - rho) * param.grad.data ** 2

cur_delta = torch.sqrt(delta + eps) / torch.sqrt(sqr + eps) * param.grad.data

delta[:] = rho * delta + (1 - rho) * cur_delta ** 2

param.data = param.data - cur_delta

optimizer = torch.optim.Adadelta(net.parameters(), rho=0.9)

w.data = w.data - 0.1 * w.grad.data

b.data = b.data - 0.1 * b.grad.data

这个就是参数更新的步骤,这样写太麻烦了,如果我们的参数很多,比如有 100 个,那么我们需要写 100 行来更新参数,为了方便,我们可以写成一个函数来更新,其实 PyTorch 已经为我们封装了一个函数来做这件事,这就是 PyTorch 中的优化器 torch.optim

使用 torch.optim 需要另外一个数据类型,就是 nn.Parameter,这个本质上和 Variable 是一样的,只不过 nn.Parameter 默认是要求梯度的,而 Variable 默认是不求梯度的。

使用 torch.optim.SGD 可以使用梯度下降法来更新参数,PyTorch 中的优化器有更多的优化算法,在本章后面的课程我们会更加详细的介绍

将参数 w 和 b 放到 torch.optim.SGD 中之后,说明一下学习率的大小,就可以使用 optimizer.step() 来更新参数了,比如下面我们将参数传入优化器,学习率设置为 1.0

比如optimizer = torch.optim.SGD([w, b], lr=1.)

可以看到使用优化器之后更新参数非常简单,只需要在自动求导loss.backward()之前使用optimizer.zero_grad() 来归 0 梯度,然后使用 optimizer.step()来更新参数就可以了,非常简便

Tags:

最近发表
标签列表