简要介绍图中的优化算法,编程实现并2D可视化
简要介绍:
首先,为了方便理解,可以想象成这样一个故事:“有一个性情古怪的探险家。他在广袤的干旱地带旅行,坚持寻找幽深的山谷,他的目标是要达到最深的谷底。他给自己制定了两个规定:一是不许看地图;二是把眼睛蒙上。因此,他并不知道最深的谷底在这个广袤的大地的何处。在如此严苛的条件下,冒险家如何才能找到“至深之地”呢?”
编程实现:
1. 被优化函数 :
首先定义有关优化器方面的代码:
import torch
from abc import abstractmethod
class Op(object):
def __init__(self):
pass
def __call__(self, inputs):
return self.forward(inputs)
# 输入:张量inputs
# 输出:张量outputs
def forward(self, inputs):
# return outputs
raise NotImplementedError
# 输入:最终输出对outputs的梯度outputs_grads
# 输出:最终输出对inputs的梯度inputs_grads
def backward(self, outputs_grads):
# return inputs_grads
raise NotImplementedError
# 优化器基类
class Optimizer(object):
def __init__(self, init_lr, model):
"""
优化器类初始化
"""
# 初始化学习率,用于参数更新的计算
self.init_lr = init_lr
# 指定优化器需要优化的模型
self.model = model
@abstractmethod
def step(self):
"""
定义每次迭代如何更新参数
"""
pass
class SimpleBatchGD(Optimizer):
def __init__(self, init_lr, model):
super(SimpleBatchGD, self).__init__(init_lr=init_lr, model=model)
def step(self):
# 参数更新
if isinstance(self.model.params, dict):
for key in self.model.params.keys():
self.model.params[key] = self.model.params[key] - self.init_lr * self.model.grads[key]
class Adagrad(Optimizer):
def __init__(self, init_lr, model, epsilon):
super(Adagrad, self).__init__(init_lr=init_lr, model=model)
self.G = {}
for key in self.model.params.keys():
self.G[key] = 0
self.epsilon = epsilon
def adagrad(self, x, gradient_x, G, init_lr):
G += gradient_x ** 2
x -= init_lr / torch.sqrt(G + self.epsilon) * gradient_x
return x, G
def step(self):
for key in self.model.params.keys():
self.model.params[key], self.G[key] = self.adagrad(self.model.params[key],
self.model.grads[key],
self.G[key],
self.init_lr)
class RMSprop(Optimizer):
def __init__(self, init_lr, model, beta, epsilon):
super(RMSprop, self).__init__(init_lr=init_lr, model=model)
self.G = {}
for key in self.model.params.keys():
self.G[key] = 0
self.beta = beta
self.epsilon = epsilon
def rmsprop(self, x, gradient_x, G, init_lr):
"""
rmsprop算法更新参数,G为迭代梯度平方的加权移动平均
"""
G = self.beta * G + (1 - self.beta) * gradient_x ** 2
x -= init_lr / torch.sqrt(G + self.epsilon) * gradient_x
return x, G
def step(self):
"""参数更新"""
for key in self.model.params.keys():
self.model.params[key], self.G[key] = self.rmsprop(self.model.params[key],
self.model.grads[key],
self.G[key],
self.init_lr)
class Momentum(Optimizer):
def __init__(self, init_lr, model, rho):
super(Momentum, self).__init__(init_lr=init_lr, model=model)
self.delta_x = {}
for key in self.model.params.keys():
self.delta_x[key] = 0
self.rho = rho
def momentum(self, x, gradient_x, delta_x, init_lr):
"""
momentum算法更新参数,delta_x为梯度的加权移动平均
"""
delta_x = self.rho * delta_x - init_lr * gradient_x
x += delta_x
return x, delta_x
def step(self):
"""参数更新"""
for key in self.model.params.keys():
self.model.params[key], self.delta_x[key] = self.momentum(self.model.params[key],
self.model.grads[key],
self.delta_x[key],
self.init_lr)
class Adam(Optimizer):
def __init__(self, init_lr, model, beta1, beta2, epsilon):
super(Adam, self).__init__(init_lr=init_lr, model=model)
self.beta1 = beta1
self.beta2 = beta2
self.epsilon = epsilon
self.M, self.G = {}, {}
for key in self.model.params.keys():
self.M[key] = 0
self.G[key] = 0
self.t = 1
def adam(self, x, gradient_x, G, M, t, init_lr):
M = self.beta1 * M + (1 - self.beta1) * gradient_x
G = self.beta2 * G + (1 - self.beta2) * gradient_x ** 2
M_hat = M / (1 - self.beta1 ** t)
G_hat = G / (1 - self.beta2 ** t)
t += 1
x -= init_lr / torch.sqrt(G_hat + self.epsilon) * M_hat
return x, G, M, t
def step(self):
"""参数更新"""
for key in self.model.params.keys():
self.model.params[key], self.G[key], self.M[key], self.t = self.adam(self.model.params[key],
self.model.grads[key],
self.G[key],
self.M[key],
self.t,
self.init_lr)
接着根据定义的模型,选择不同优化器进行训练并进行可视化:
# coding=gbk
from Op import *
import torch
import numpy as np
from matplotlib import pyplot as plt
class OptimizedFunction(Op):
def __init__(self, w):
super(OptimizedFunction, self).__init__()
self.w = w
self.params = {'x': 0}
self.grads = {'x': 0}
def forward(self, x):
self.params['x'] = x
return torch.matmul(self.w.T, torch.tensor(torch.square(self.params['x']), dtype=torch.float32))
def backward(self):
self.grads['x'] = 2 * torch.multiply(self.w.T, self.params['x'])
import copy
def train_f(model, optimizer, x_init, epoch):
"""
训练函数
输入:
- model:被优化函数
- optimizer:优化器
- x_init:x初始值
- epoch:训练回合数
"""
x = x_init
all_x = []
losses = []
for i in range(epoch):
all_x.append(copy.copy(x.numpy()))
loss = model(x)
losses.append(loss)
model.backward()
optimizer.step()
x = model.params['x']
#print(all_x)
return torch.tensor(all_x), losses
class Visualization(object):
def __init__(self):
"""
初始化可视化类
"""
# 只画出参数x1和x2在区间[-5, 5]的曲线部分
x1 = np.arange(-5, 5, 0.1)
x2 = np.arange(-5, 5, 0.1)
x1, x2 = np.meshgrid(x1, x2)
self.init_x = torch.tensor([x1, x2])
def plot_2d(self, model, x, fig_name):
"""
可视化参数更新轨迹
"""
fig, ax = plt.subplots(figsize=(10, 6))
cp = ax.contourf(self.init_x[0], self.init_x[1], model(self.init_x.transpose(0, 1)),
colors=['#e4007f', '#f19ec2', '#e86096', '#eb7aaa', '#f6c8dc', '#f5f5f5', '#000000'])
'''
这行代码使用contourf方法绘制了一个填充的等高线图。
self.init_x[0]和self.init_x[1]是x1和x2的网格点。
model(self.init_x.transpose([1, 0, 2]))表示将self.init_x转置后传递给模型函数,得到每个网格点的值。
colors=['#e4007f', ...]定义了等高线填充的颜色。
'''
c = ax.contour(self.init_x[0], self.init_x[1], model(self.init_x.transpose(0, 1)), colors='black')
#绘制了等高线的轮廓线,其颜色为黑色
cbar = fig.colorbar(cp)#创建了一个颜色条,与填充的等高线图相关联
ax.plot(x[:, 0], x[:, 1], '-o', color='#000000')#不断更新的x,y绘制点,连成线
ax.plot(0, 'r*', markersize=18, color='#fefefe')#标记0,0点
ax.set_xlabel('$x1$')#标签名称
ax.set_ylabel('$x2$')
ax.set_xlim((-2, 5))#标签范围
ax.set_ylim((-2, 5))
plt.savefig(fig_name)
plt.show()
import numpy as np
def train_and_plot_f(model, optimizer, epoch, fig_name):
"""
训练模型并可视化参数更新轨迹
"""
# 设置x的初始值
x_init = torch.tensor([3, 4], dtype=torch.float32)#初始值在这确定的
# print('x1 initiate: {}, x2 initiate: {}'.format(x_init[0].numpy(), x_init[1].numpy()))
x, losses = train_f(model, optimizer, x_init, epoch)
# print(x)
losses = np.array(losses)
# 展示x1、x2的更新轨迹
vis = Visualization()
vis.plot_2d(model, x, fig_name)
# 固定随机种子
torch.seed()
w = torch.as_tensor([0.2, 2])
model = OptimizedFunction(w)
opt = SimpleBatchGD(init_lr=0.2, model=model)
train_and_plot_f(model, opt, epoch=20, fig_name='opti-vis-para.pdf')
# 固定随机种子
torch.seed()
w = torch.as_tensor([0.2, 2])
model2 = OptimizedFunction(w)
opt2 = Adagrad(init_lr=0.5, model=model2, epsilon=1e-7)
train_and_plot_f(model2, opt2, epoch=50, fig_name='opti-vis-para2.pdf')
torch.seed()
w = torch.as_tensor([0.2, 2])
model3 = OptimizedFunction(w)
opt3 = RMSprop(init_lr=0.1, model=model3, beta=0.9, epsilon=1e-7)
train_and_plot_f(model3, opt3, epoch=50, fig_name='opti-vis-para3.pdf')
# 固定随机种子
torch.seed()
w = torch.as_tensor([0.2, 2])
model4 = OptimizedFunction(w)
opt4 = Momentum(init_lr=0.01, model=model4, rho=0.9)
train_and_plot_f(model4, opt4, epoch=50, fig_name='opti-vis-para4.pdf')
# 固定随机种子
torch.seed()
w = torch.as_tensor([0.2, 2])
model5 = OptimizedFunction(w)
opt5 = Adam(init_lr=0.2, model=model5, beta1=0.9, beta2=0.99, epsilon=1e-7)
train_and_plot_f(model5, opt5, epoch=20, fig_name='opti-vis-para5.pdf')
SGD Adagrad
RMSprop Momentum
Adam
2. 被优化函数 :
# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
class SGD:
"""随机梯度下降法(Stochastic Gradient Descent)"""
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
class Momentum:
"""Momentum SGD"""
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
class Nesterov:
"""Nesterov's Accelerated Gradient (http://arxiv.org/abs/1212.0901)"""
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] *= self.momentum
self.v[key] -= self.lr * grads[key]
params[key] += self.momentum * self.momentum * self.v[key]
params[key] -= (1 + self.momentum) * self.lr * grads[key]
class AdaGrad:
"""AdaGrad"""
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] += grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
class RMSprop:
"""RMSprop"""
def __init__(self, lr=0.01, decay_rate=0.99):
self.lr = lr
self.decay_rate = decay_rate
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] *= self.decay_rate
self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
class Adam:
"""Adam (http://arxiv.org/abs/1412.6980v8)"""
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0
self.m = None
self.v = None
def update(self, params, grads):
if self.m is None:
self.m, self.v = {}, {}
for key, val in params.items():
self.m[key] = np.zeros_like(val)
self.v[key] = np.zeros_like(val)
self.iter += 1
lr_t = self.lr * np.sqrt(1.0 - self.beta2 ** self.iter) / (1.0 - self.beta1 ** self.iter)
for key in params.keys():
self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])#梯度估计 加一个减一个正好不变,所以“+=”后面self.m[key]也可使用(1 - self.beta1)
self.v[key] += (1 - self.beta2) * (grads[key] ** 2 - self.v[key])#学习率
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
def f(x, y):
return x ** 2 / 20.0 + y ** 2
def df(x, y):
return x / 10.0, 2.0 * y
init_pos = (-7.0, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0
learningrate = [0.9, 0.3, 0.3, 0.6, 0.6, 0.6, 0.6]
optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=learningrate[0])
optimizers["Momentum"] = Momentum(lr=learningrate[1])
optimizers["Nesterov"] = Nesterov(lr=learningrate[2])
optimizers["AdaGrad"] = AdaGrad(lr=learningrate[3])
optimizers["RMSprop"] = RMSprop(lr=learningrate[4])
optimizers["Adam"] = Adam(lr=learningrate[5])
idx = 1
id_lr = 0
for key in optimizers:
optimizer = optimizers[key]
lr = learningrate[id_lr]
id_lr = id_lr + 1
x_history = []
y_history = []
params['x'], params['y'] = init_pos[0], init_pos[1]
for i in range(50):
x_history.append(params['x'])
y_history.append(params['y'])
grads['x'], grads['y'] = df(params['x'], params['y'])
optimizer.update(params, grads)
x = np.arange(-10, 10, 0.01)
y = np.arange(-5, 5, 0.01)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)
# for simple contour line
mask = Z > 7
Z[mask] = 0
#将数组Z中所有大于7的值都设置为0
# plot
plt.subplot(2, 3, idx)
idx += 1
plt.plot(x_history, y_history, 'o-', color="r")
# plt.contour(X, Y, Z) # 绘制等高线
plt.contour(X, Y, Z, cmap='Greens') # 颜色填充
#plt.contour()函数用于绘制等高线图
#X 和 Y 是网格点的x和y坐标。
#Z 是每个网格点上的值。
#cmap 是一个可选参数,cmap='gray' 指定了颜色映射为灰度。这意味着等高线图将使用灰度颜色来表示不同的高度或值。
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.plot(0, 0, '+')
# plt.axis('off')
# plt.title(key+'\nlr='+str(lr), fontstyle='italic')
plt.text(0, 10, key + '\nlr=' + str(lr), fontsize=20, color="b",
verticalalignment='top', horizontalalignment='center', fontstyle='italic')
'''
0, 10: 这是文本的坐标位置。0是x坐标,10是y坐标。
key + '\nlr=' + str(lr): 这是要添加到图上的文本字符串。key是一个变量,'\n'是一个换行符,将文本分成两行。lr是另一个变量,使用str()函数将其转换为字符串类型,以便与key连接。
fontsize=20: 设置文本的字体大小为20。
color="b": 设置文本的颜色为蓝色(blue)。
verticalalignment='top': 设置文本的垂直对齐方式为顶部对齐。
horizontalalignment='center': 设置文本的水平对齐方式为居中对齐。
fontstyle='italic': 设置文本的字体样式为斜体。
'''
plt.xlabel("x")
plt.ylabel("y")
plt.subplots_adjust(wspace=0, hspace=0) # 调整子图间距wspace: 水平间距,即子图之间的宽度间距。hspace: 垂直间距,即子图之间的高度间距。
plt.show()
![](https://i-blog.csdnimg.cn/blog_migrate/1c171ffe4fda71b0fd31355ca56ac5e6.png)
3. 解释不同轨迹的形成原因
SGD:
尝试一下不同的步长,(确实是0.9更好一点啊)y方向跳过最低点跳的也不太远,x方向走的也不太慢
在y方向梯度大,x方向梯度小,如果步子迈的太大,x方向可能接近最低点,但是y方向可能一下子迈过低谷直接迈到更高的地方,有的甚至比之前还高(看步长是1时),步长太小,x方向走的就慢了,达到最优值会更慢,在步长是0.9时,效果不错,最终到达了最优值(0,0)
该函数处在一个类似于倾斜向右的山谷的形状的情况
需要经过反反复复的上上下下才可以最终找到谷底。在上述的情况下,SGD的性能非常差,这就导致了模型的训练非常慢。
动量法
发现让他训练30轮看不出啥来,结果调了一下轮数,请看只训练了九轮的结果:
只用了九轮就能到达最优值,可见比SGD优化了很多。
下面是SGD的九轮:
在参数更新的过程中,如果更新前的速度与梯度方向一致,则更新的数值更大,如果不一致,则更新的数值会比SGD时更小。(个人理解,加入第一次他往南走,结果走的太偏南了,错过了最低点,下次往北走,这两次方向不一样,都想往最优处更新,说明上次肯定走多了,避免这一次往北走还是走多,就往南拉他一把,即加上之前的动量(南)。关于东西方向,一直是往东,这样每次往东走,上一次的往东走的动量还会继续推他一把,导致他走得更快,到最优值速度更快。
动量的作用就是加速收敛,减少震荡。动量通过考虑过去梯度的平均值来调整参数的更新步长和方向。
Nesterov算法:
Nesterov动量法在迭代过程中能够更偏向
理想状态下降路径。
其中
- 蓝色曲线表示理想状态下的下降路径;
- 其中红色实线表示梯度下降法的更新方向;蓝色实线表示历史梯度构成的冲量信息;
- 其中红色虚线表示未来时刻梯度下降法的更新方向,与
描述的
红色实线之间存在一丢丢的偏移,不是平行的~。
Nesterov动量法在迭代过程中能够更偏向
理想状态下降路径。
AdaGrad:
学习率衰减是一个优化技巧,使模型一开始多学,慢慢的少学,可以有效避免模型在学到最优值附件反复横跳。AdaGrad进一步发展了这个思想,针对一个一个的参数进行学习率更新。
通俗来讲就是,知道它y的方向上梯度大,那学习率就减小一点,省的再跳过了最优。随着训练的进行,不断逼近最优值,到了最优值附近转悠,避免又错过最优,必须要谨慎一点,步子更慢一点,随着梯度的累加,学习率就减少,步子会变小,正好合适。
RMSProp:
但是这样会出现一个问题,随着学习的深入,h的值会越来越大,直至使得学习率的值趋于0。
RMSProp方法并不会将前面所有的梯度都一视同仁的相加起来,而是使用逐渐遗忘过去的操作,将做加法时将新的梯度信息看的更重要。这种操作叫做“指数移动平均”。
只训练了5轮:可以看到有好几组就能到达最优值,可见效果很好
Adam算法:Adam算法 ≈ 动量法 + RMSprop
将动量加入 RMSProp 最直观的方法是将动量应用于缩放后的梯度。结合缩放的动量使用没有明确的理论动机。其次,Adam 包括偏置修正,修正从原点初始化的一阶矩(动量项)和(非中心的)二阶矩的估计。RMSProp 也采用了(非中心的)二阶矩估计,然而缺失了修正因子。因此,不像 Adam,RMSProp 二阶矩估计可能在训练初期有很高的偏置。 Adam 通常被认为对超参数的选择相当鲁棒,尽管学习率有时需要从建议的默认修改。
虽然但是,感觉这个问题里,Adam的又是没有体现出来,感觉收敛速度不如RMSProp。
自己的理解:
SGD总是需要经过反反复复的上上下下才可以最终找到谷底。在上述的情况下,SGD的性能非常差,这就导致了模型的训练非常慢。针对该问题,总的来说就是动量法改变的是梯度的方向,为避免y方向走的步子很大而x方向的步子很小的问题,y方向拽着他一点,x方向推着他一点,(这个操作让动量来)去使方向更加偏x一点。Nesterov动量法在迭代过程中能够更偏向
理想状态下降路径。关于学习率调整的算法,AdaGrad算法刚开始步子迈的大点(为了快点到最优),越来越往后离最优值越近时要更加谨慎,步子更小点,省得跨过它。上面这个算法可能出现,学习率减少到0,还没到最优值呢就动不了是不行的,所以出现了RMSProp算法,滑动着只记一部分梯度平方,使用逐渐遗忘过去的操作,将做加法时将新的梯度信息看的更重要。这种操作叫做“指数移动平均”。最后关于Adam算法,将梯度缩放和偏置修正相结合,很稳定。
那么用哪种方法好呢?非常遗憾,(目前)并不存在能在所有问题中都表现良好的方法。这4种方法各有各的特点,都有各自擅长解决的问题和不擅长解决的问题。 很多研究中至今仍在使用SGD。Momentum和AdaGrad也是值得一试的方法。最近,很多研究人员和技术人员都喜欢用Adam。参考:HBU-NNDL 作业11:优化算法比较_adam不收敛-CSDN博客
总结:
参考
https://aistudio.baidu.com/projectdetail/7313541
https://blog.csdn.net/m0_57215376/article/details/128183431?spm=1001.2014.3001.5502
https://blog.csdn.net/zhenjiteng/article/details/128227639?spm=1001.2014.3001.5502
优化算法到底如何工作?深度学习中的几个经典优化算法的原理解析 - 知乎 (zhihu.com)
深度学习基础-优化算法详解 - 知乎 (zhihu.com)
深度学习笔记之优化算法(四)Nesterov动量方法的简单认识_nesterov算法-CSDN博客
【23-24 秋学期】NNDL 作业12 优化算法2D可视化-CSDN博客
【NNDL 作业】优化算法比较 增加 RMSprop、Nesterov_随着优化的进展,需要调整γ吗?rmsprop算法习题-CSDN博客