优化算法:优化的目标是网络模型中的参数(集合),损失函数L变量就是θ,其中L中的参数是整个训练集,换句话说,目标函数(损失函数)是通过整个训练集来确定的,训练集全集不同,则损失函数的图像也不同。
SGD又称为随机梯度下降算法 ,用于求解损失函数最小值,对于SGD而言,每次使用的损失函数只是通过这一个小批量的数据确定的,其函数图像与真实全集损失函数有所不同,所以其求解的梯度也含有一定的随机性,在鞍点或者局部最小值点的时候,震荡跳动,如果是mini-batch或者SGD,每次找到的梯度都是不同的,就会发生震荡,来回跳动。
SGD 一次只进行一次更新,就没有冗余,而且比较快,并且可以新增样本。
缺点:更新比较频繁,会造成 cost function 有严重的震荡。
可以对低频的参数做较大的更新,对高频的做较小的更新,也因此,对于稀疏的数据它的表现很好,很好地提高了 SGD 的鲁棒性
梯度更新
优点是减少了学习率的手动调节,超参数设定值:一般η选取0.01
缺点:
它的缺点是分母会不断积累,这样学习率就会收缩并最终会变得非常小。
RMSprop 为了解决 Adagrad 学习率急剧下降问题的,
梯度更新规则:
使用的是指数加权平均,旨在消除梯度下降中的摆动,与Momentum的效果一样,某一维度的导数比较大,则指数加权平均就大,某一维度的导数比较小,则其指数加权平均就小,这样就保证了各维度导数都在一个量级,进而减少了摆动。允许使用一个更大的学习率η)
加速 SGD, 并且抑制震荡
当我们将一个小球从山上滚下来时,没有阻力的话,它的动量会越来越大,但是如果遇到了阻力,速度就会变小。 加入的这一项,可以使得梯度方向不变的维度上速度变快,梯度方向有所改变的维度上的更新速度变慢,这样就可以加快收敛并减小震荡。
超参数设定值: 一般 γ 取值 0.9 左右。
缺点:
这种情况相当于小球从山上滚下来时是在盲目地沿着坡滚,如果它能具备一些先知,例如快要上坡时,就知道需要减速了的话,适应性会更好。
计算每个参数的自适应学习率的方法。相当于 RMSprop + Momentum
除了像 Adadelta 和 RMSprop 一样存储了过去梯度的平方 vt 的指数衰减平均值 ,也像 momentum 一样保持了过去梯度 mt 的指数衰减平均值
每个优化器实现代码
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): # SGD,实际是批量梯度下降
# 初始化方法,接受初始学习率 init_lr 和模型 model 作为参数
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.grads[key] 存储了对应参数的梯度信息
self.model.params[key] = self.model.params[key] - self.init_lr * self.model.grads[key]
class Adagrad(Optimizer):
# 初始化方法,接受初始学习率init_lr、模型model和一个小量epsilon作为参数
def __init__(self, init_lr, model, epsilon):
super(Adagrad, self).__init__(init_lr=init_lr, model=model)
# 初始化一个字典G,用于存储每个参数的历史梯度平方
self.G = {}
for key in self.model.params.keys():
self.G[key] = 0
# 存储epsilon,用于防止除数为0的情况
self.epsilon = epsilon
# adagrad方法,用于计算参数更新和梯度平方的更新
def adagrad(self, x, gradient_x, G, init_lr):
# 更新梯度平方的历史记录,将新的梯度平方加到原来的值上
G += gradient_x ** 2
# 根据Adagrad算法计算参数的更新量
x -= init_lr / torch.sqrt(G + self.epsilon) * gradient_x
return x, G
def step(self):
for key in self.model.params.keys():
# 调用adagrad方法进行参数更新和梯度平方的更新,并将结果存储回model.params和G字典中
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)
# 初始化一个字典G,用于存储每个参数的历史梯度平方的加权移动平均
self.G = {}
for key in self.model.params.keys():
self.G[key] = 0
# 存储衰减率beta和epsilon,其中epsilon用于防止除数为0的情况
self.beta = beta
self.epsilon = epsilon
# rmsprop方法,用于计算参数更新和梯度平方加权移动平均的更新
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):
# 初始化方法,接受初始学习率init_lr、模型model和动量系数rho作为参数
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 # 将每个参数的历史梯度的一阶矩初始化为0
self.G[key] = 0 # 将每个参数的历史梯度的二阶矩初始化为0
self.t = 1 # 初始化时间步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) # 对一阶矩进行正规化,使其除以(1 - beta1的t次方)
G_hat = G / (1 - self.beta2 ** t) # 对二阶矩进行正规化,使其除以(1 - 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')
# 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="b")
# plt.contour(X, Y, Z) # 绘制等高线
plt.contour(X, Y, Z, cmap='copper') # 颜色填充
# 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=10, color="r",
verticalalignment='top', horizontalalignment='center', fontstyle='normal')
'''
0, 10: 这是文本的坐标位置。0是x坐标,10是y坐标。
key + '\nlr=' + str(lr): 这是要添加到图上的文本字符串。key是一个变量,'\n'是一个换行符,将文本分成两行。lr是另一个变量,使用str()函数将其转换为字符串类型,以便与key连接。
fontsize=20: 设置文本的字体大小为10。
color="b": 设置文本的颜色为红色(red)。
verticalalignment='top': 设置文本的垂直对齐方式为顶部对齐。
horizontalalignment='center': 设置文本的水平对齐方式为居中对齐。
fontstyle='italic': 设置文本的字体样式为正常。
'''
plt.xlabel("x")
plt.ylabel("y")
plt.subplots_adjust(wspace=0, hspace=0) # 调整子图间距wspace: 水平间距,即子图之间的宽度间距。hspace: 垂直间距,即子图之间的高度间距。
plt.show()
SGD轨迹形成原因:
1、因为是随机梯度下降,SGD在每一步仅仅使用一个样本的梯度信息,这使得它对函数局部特性的估计可能不够准确。当函数具有非均向性(如一个维度上的变化远大于另一个维度)时,SGD可能会在最优解附近来回震荡,呈现出“之”字形的轨迹,
2、学习率设置不合适:如果学习率设置得过大,SGD可能会跳过最优解;如果学习率设置得过小,SGD可能会在最优解附近徘徊不前。当学习率的调整不恰当时,SGD的收敛轨迹就可能呈现出“之”字形。
Adagrad
y轴梯度较大,后面会根据前面较大的变动进行调整,减小更新的步伐,呈现稳定的向最优点收敛。
RMSprop
RMSprop因为逐渐遗忘过去的梯度,只被近期的梯度影响,在最初的时候会收敛的更快,变化幅度大。
Momentum
对以前的梯度做了指数加权平均,不会像原始的那样直接折线那么厉害,因为有之前速度影响,所以瞬间就折,而是会再向原来那个方向前进一段距离,也可以理解为对原始梯度做了一个平滑,然后再用来做梯度下降
Adam
前期收敛幅度较大,后期逐渐平稳,朝着最优点不断移动。Adam算法由于可以结合了动量法和 RMSprop 。
动量法
动量相当于给梯度一个惯性,使其能够在相同方向上持续更新。这可以帮助算法跳过局部最小值并加速收敛。动量系数是一个介于0和1之间的值,通常设置为0.9。该值越高,算法越趋向于保持之前的方向。在每次迭代中,算法会计算梯度,并将其与之前的梯度相加,产生一个新的梯度。它可以跳过局部最小值并加速收敛。另外,由于动量系数的引入,该算法具有一定的平滑性,这可以帮助防止过度拟合。
缺点:
它需要更多的内存来存储之前的梯度。此外,由于动量系数的引入,该算法可能会导致权重更新过多。这可以通过调整动量系数和学习率来解决。
Adam中使用一阶矩和二阶矩,一阶矩就是期望值,换句话说就是平均数,二阶矩方差
当 一阶矩大,且二阶矩大,梯度大且稳定
当一阶矩小,二阶矩很大,振荡,考虑成可能是向下更新到一个局部的波谷,又进行一波反弹
当 一阶矩 趋近于 0,且 二阶矩也趋近于 0 :梯度趋于零,可能达到局部最低点,也可能走到一个极度平缓的平地
如何选择优化算法深度学习——优化器算法Optimizer详解(BGD、SGD、MBGD、Momentum、NAG、Adagrad、Adadelta、RMSprop、Adam)-腾讯云开发者社区-腾讯云 (tencent.com)
如果数据是稀疏的,就用自适用方法,即 Adagrad, Adadelta, RMSprop, Adam。
RMSprop, Adadelta, Adam 在很多情况下的效果是相似的。
Adam 就是在 RMSprop 的基础上加了 bias-correction 和 momentum,
随着梯度变的稀疏,Adam 比 RMSprop 效果会好。
整体来讲,Adam 是最好的选择。
很多论文里都会用 SGD,没有 momentum 等。SGD 虽然能达到极小值,但是比其它算法用的时间长,而且可能会被困在鞍点。
如果需要更快的收敛,或者是训练更深更复杂的神经网络,需要用一种自适应的算法。
REF:
【23-24 秋学期】NNDL 作业12 优化算法2D可视化-CSDN博客
深度学习——优化器算法Optimizer详解(BGD、SGD、MBGD、Momentum、NAG、Adagrad、Adadelta、RMSprop、Adam)-腾讯云开发者社区-腾讯云 (tencent.com)
【23-24 秋学期】NNDL 作业12 优化算法2D可视化-CSDN博客
NNDL 作业12-优化算法2D可视化 [HBU]-CSDN博客
Adam Algorithm & First-order moment, Second moment_一阶矩估计-CSDN博客