简要介绍图中的优化算法,编程实现并2D可视化
1. 被优化函数
1.1 SGD
SGD(Stochastic Gradient Descent,随机梯度下降)是一种常见的优化算法,用于训练神经网络等机器学习模型。它的核心思想是在每次迭代中随机选择一个样本来计算梯度,并更新模型参数。
具体来说,SGD的过程可以分为以下几个步骤:
-
随机选择一个样本。
-
计算该样本的梯度。
-
根据梯度更新模型参数。
-
重复以上步骤,直到达到预定的迭代次数或其他停止条件。
在实际应用中,SGD通常会结合一些技巧来提高其效率和稳定性,例如:
-
学习率衰减:随着迭代次数的增加,逐渐降低学习率,使得模型参数更新更为平稳。
-
动量方法:加入动量项,使得模型参数更新具有历史信息,可以减少更新的震荡。
-
批量更新:将多个样本组成一个批次,计算平均梯度后再更新模型参数,可以减少噪声的影响,使得更新更为稳定。
将被优化函数实现为OptimizedFunction算子,其forward方法是Sphere函数的前向计算,backward方法则计算被优化函数对xx的偏导。代码实现如下:
from NNDL 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'])
小批量梯度下降优化器 复用3.1.4.3节定义的梯度下降优化器SimpleBatchGD。按照梯度下降的梯度更新公式进行梯度更新。
训练函数 定义一个简易的训练函数,记录梯度下降过程中每轮的参数和损失。代码实现如下:
import copy
def train_f(model, optimizer, x_init, 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
可视化函数 定义一个Visualization类,用于绘制的更新轨迹。代码实现如下:
import numpy as np
import matplotlib.pyplot as plt
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'])
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')
ax.plot(0, 'r*', markersize=18, color='#fefefe')
ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_xlim((-2, 5))
ax.set_ylim((-2, 5))
plt.savefig(fig_name)
定义train_and_plot_f函数,调用train_f和Visualization,训练模型并可视化参数更新轨迹。代码实现如下:
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)
模型训练与可视化 指定Sphere函数中ww的值,实例化被优化函数,通过小批量梯度下降法更新参数,并可视化的更新轨迹。
# 固定随机种子
torch.manual_seed(0)
w = torch.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')
1.2 AdaGrad
AdaGrad算法(Adaptive Gradient Algorithm,自适应梯度算法)是借鉴正则化的思想,每次迭代时自适应地调整每个参数的学习率。在第次迭代时,先计算每个参数梯度平方的累计值。
其中⊙为按元素乘积,是第次迭代时的梯度。
其中是初始的学习率,是为了保持数值稳定性而设置的非常小的常数,一般取值到。此外,这里的开平方、除、加运算都是按元素进行的操作。
from NNDL import *
import torch
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)
# 固定随机种子
torch.manual_seed(0)
w = torch.tensor([0.2, 2])
model = OptimizedFunction(w)
opt = Adagrad(init_lr=0.5, model=model, epsilon=1e-7)
train_and_plot_f(model, opt, epoch=50, fig_name='opti-vis-para2.pdf')
1.3RMSprop
RMSprop(Root Mean Square Propagation,均方根传播)是一种常见的优化算法,用于训练神经网络等机器学习模型。它的核心思想是对梯度进行加权平均,以调整学习率,并适应不同参数的变化范围。
具体来说,RMSprop的过程可以分为以下几个步骤:
-
计算梯度的平方。
-
对梯度的平方进行指数加权移动平均,得到平均的梯度平方。
-
根据平均的梯度平方调整学习率,使得参数更新更为平稳。
-
根据调整后的学习率更新模型参数。
在实际应用中,RMSprop通常会结合一些技巧来提高其效率和稳定性,例如:
-
学习率衰减:随着迭代次数的增加,逐渐降低学习率,使得模型参数更新更为平稳。
-
剪裁梯度:限制梯度的范围,使得更新更为稳定。
from NNDL import *
import torch
class RMSprop(Optimizer):
def __init__(self, init_lr, model, beta, epsilon):
"""
RMSprop优化器初始化
输入:
- init_lr:初始学习率
- model:模型,model.params存储模型参数值
- 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)
# 固定随机种子
torch.manual_seed(0)
w = torch.tensor([0.2, 2])
model = OptimizedFunction(w)
opt = RMSprop(init_lr=0.1, model=model, beta=0.9, epsilon=1e-7)
train_and_plot_f(model, opt, epoch=50, fig_name='opti-vis-para3.pdf')
1.4 Momentum
动量(Momentum)是一种优化算法,用于训练神经网络和其他机器学习模型。它的目标是加速收敛,并且在梯度更新时具有一定的惯性。
动量算法基于以下原理:在参数更新时,不仅要考虑当前的梯度信息,还要考虑之前的梯度更新方向。这可以帮助算法在参数空间中更好地探索,并加速收敛过程。
具体来说,动量算法引入一个动量项,用于累积之前梯度的方向信息。算法的步骤如下:
-
初始化参数和动量项。
-
计算当前的梯度。
-
更新动量项:将上一次的动量项乘以一个衰减因子(通常为0.9或0.99),并加上当前的梯度。
-
根据动量项来更新参数:将动量项的方向作为参数更新的方向,并乘以一个学习率。
-
重复以上步骤,直到达到预定的迭代次数或其他停止条件。
from NNDL import *
import torch
class Momentum(Optimizer):
def __init__(self, init_lr, model, rho):
"""
Momentum优化器初始化
输入:
- init_lr:初始学习率
- model:模型,model.params存储模型参数值
- 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)
# 固定随机种子
torch.manual_seed(0)
w = torch.tensor([0.2, 2])
model = OptimizedFunction(w)
opt = Momentum(init_lr=0.01, model=model, rho=0.9)
train_and_plot_f(model, opt, epoch=50, fig_name='opti-vis-para4.pdf')
1.5 Adam
Adam(Adaptive Moment Estimation,自适应矩估计)是一种自适应的优化算法,用于训练神经网络等机器学习模型。它的核心思想是结合了动量和RMSprop两种优化算法的优点,以实现更快速、更准确的梯度下降。
Adam算法通过计算梯度的一阶矩估计和二阶矩估计,来自适应地调整每个参数的学习率。在Adam中,每个参数都有一个自适应的学习率,根据历史梯度的一阶矩估计和二阶矩估计进行计算。
具体来说,Adam算法的步骤如下:
-
初始化模型参数和一阶矩、二阶矩的估计值。
-
计算梯度,并更新一阶矩和二阶矩的估计值。
-
对一阶矩和二阶矩的估计值进行偏差校正。
-
根据偏差校正后的一阶矩和二阶矩的估计值,计算每个参数的自适应学习率。
-
根据自适应学习率更新模型参数。
-
重复以上步骤,直到达到预定的迭代次数或其他停止条件。
from NNDL import *
import torch
class Adam(Optimizer):
def __init__(self, init_lr, model, beta1, beta2, epsilon):
"""
Adam优化器初始化
输入:
- init_lr:初始学习率
- model:模型,model.params存储模型参数值
- 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):
"""
adam算法更新参数
输入:
- 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)
# 固定随机种子
torch.manual_seed(0)
w = torch.tensor([0.2, 2])
model = OptimizedFunction(w)
opt = Adam(init_lr=0.2, model=model, beta1=0.9, beta2=0.99, epsilon=1e-7)
train_and_plot_f(model, opt, epoch=20, fig_name='opti-vis-para5.pdf')
附件:
NNDL.py
from torch.utils.data import Dataset,DataLoader
import torch
import os
import torch.nn as nn
import torch.nn.functional as F
from abc import abstractmethod
class DigitSumDataset(Dataset):
def __init__(self, data):
self.data = data
def __getitem__(self, idx):
example = self.data[idx]
seq = torch.tensor(example[0], dtype=torch.int64)
label = torch.tensor(example[1], dtype=torch.int64)
return seq, label
def __len__(self):
return len(self.data)
# 加载数据
def load_data(data_path):
# 加载训练集
train_examples = []
train_path = os.path.join(data_path, "train.txt")
with open(train_path, "r", encoding="utf-8") as f:
for line in f.readlines():
# 解析一行数据,将其处理为数字序列seq和标签label
items = line.strip().split("\t")
seq = [int(i) for i in items[0].split(" ")]
label = int(items[1])
train_examples.append((seq, label))
# 加载验证集
dev_examples = []
dev_path = os.path.join(data_path, "dev.txt")
with open(dev_path, "r", encoding="utf-8") as f:
for line in f.readlines():
# 解析一行数据,将其处理为数字序列seq和标签label
items = line.strip().split("\t")
seq = [int(i) for i in items[0].split(" ")]
label = int(items[1])
dev_examples.append((seq, label))
# 加载测试集
test_examples = []
test_path = os.path.join(data_path, "test.txt")
with open(test_path, "r", encoding="utf-8") as f:
for line in f.readlines():
# 解析一行数据,将其处理为数字序列seq和标签label
items = line.strip().split("\t")
seq = [int(i) for i in items[0].split(" ")]
label = int(items[1])
test_examples.append((seq, label))
return train_examples, dev_examples, test_examples
class Embedding(nn.Module):
def __init__(self, num_embeddings, embedding_dim):
super(Embedding, self).__init__()
self.W = nn.init.xavier_uniform_(torch.empty(num_embeddings, embedding_dim),gain=1.0)
def forward(self, inputs):
# 根据索引获取对应词向量
embs = self.W[inputs]
return embs
emb_layer = Embedding(10, 5)
inputs = torch.tensor([0, 1, 2, 3])
emb_layer(inputs)
# 基于RNN实现数字预测的模型
class Model_RNN4SeqClass(nn.Module):
def __init__(self, model, num_digits, input_size, hidden_size, num_classes):
super(Model_RNN4SeqClass, self).__init__()
# 传入实例化的RNN层,例如SRN
self.rnn_model = model
# 词典大小
self.num_digits = num_digits
# 嵌入向量的维度
self.input_size = input_size
# 定义Embedding层
self.embedding = Embedding(num_digits, input_size)
# 定义线性层
self.linear = nn.Linear(hidden_size, num_classes)
def forward(self, inputs):
# 将数字序列映射为相应向量
inputs_emb = self.embedding(inputs)
# 调用RNN模型
hidden_state = self.rnn_model(inputs_emb)
# 使用最后一个时刻的状态进行数字预测
logits = self.linear(hidden_state)
return logits
class RunnerV3(object):
def __init__(self, model, optimizer, loss_fn, metric, **kwargs):
self.model = model
self.optimizer = optimizer
self.loss_fn = loss_fn
self.metric = metric # 只用于计算评价指标
# 记录训练过程中的评价指标变化情况
self.dev_scores = []
# 记录训练过程中的损失函数变化情况
self.train_epoch_losses = [] # 一个epoch记录一次loss
self.train_step_losses = [] # 一个step记录一次loss
self.dev_losses = []
# 记录全局最优指标
self.best_score = 0
def train(self, train_loader, dev_loader=None, **kwargs):
# 将模型切换为训练模式
self.model.train()
# 传入训练轮数,如果没有传入值则默认为0
num_epochs = kwargs.get("num_epochs", 0)
# 传入log打印频率,如果没有传入值则默认为100
log_steps = kwargs.get("log_steps", 100)
# 评价频率
eval_steps = kwargs.get("eval_steps", 0)
# 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams"
save_path = kwargs.get("save_path", "best_model.pdparams")
custom_print_log = kwargs.get("custom_print_log", None)
# 训练总的步数
num_training_steps = num_epochs * len(train_loader)
if eval_steps:
if self.metric is None:
raise RuntimeError('Error: Metric can not be None!')
if dev_loader is None:
raise RuntimeError('Error: dev_loader can not be None!')
# 运行的step数目
global_step = 0
# 进行num_epochs轮训练
for epoch in range(num_epochs):
# 用于统计训练集的损失
total_loss = 0
for step, data in enumerate(train_loader):
X, y = data
# 获取模型预测
logits = self.model(X)
loss = self.loss_fn(logits, y.long()) # 默认求mean
total_loss += loss
# 训练过程中,每个step的loss进行保存
self.train_step_losses.append((global_step, loss.item()))
if log_steps and global_step % log_steps == 0:
print(
f"[Train] epoch: {epoch}/{num_epochs}, step: {global_step}/{num_training_steps}, loss: {loss.item():.5f}")
# 梯度反向传播,计算每个参数的梯度值
loss.backward()
if custom_print_log:
custom_print_log(self)
# 小批量梯度下降进行参数更新
self.optimizer.step()
# 梯度归零
self.optimizer.zero_grad()
# 判断是否需要评价
if eval_steps > 0 and global_step > 0 and \
(global_step % eval_steps == 0 or global_step == (num_training_steps - 1)):
dev_score, dev_loss = self.evaluate(dev_loader, global_step=global_step)
print(f"[Evaluate] dev score: {dev_score:.5f}, dev loss: {dev_loss:.5f}")
# 将模型切换为训练模式
self.model.train()
# 如果当前指标为最优指标,保存该模型
if dev_score > self.best_score:
self.save_model(save_path)
print(
f"[Evaluate] best accuracy performence has been updated: {self.best_score:.5f} --> {dev_score:.5f}")
self.best_score = dev_score
global_step += 1
# 当前epoch 训练loss累计值
trn_loss = (total_loss / len(train_loader)).item()
# epoch粒度的训练loss保存
self.train_epoch_losses.append(trn_loss)
print("[Train] Training done!")
# 模型评估阶段,使用'torch.no_grad()'控制不计算和存储梯度
@torch.no_grad()
def evaluate(self, dev_loader, **kwargs):
assert self.metric is not None
# 将模型设置为评估模式
self.model.eval()
global_step = kwargs.get("global_step", -1)
# 用于统计训练集的损失
total_loss = 0
# 重置评价
self.metric.reset()
# 遍历验证集每个批次
for batch_id, data in enumerate(dev_loader):
X, y = data
# 计算模型输出
logits = self.model(X)
# 计算损失函数
loss = self.loss_fn(logits, y.long()).item()
# 累积损失
total_loss += loss
# 累积评价
self.metric.update(logits, y)
dev_loss = (total_loss / len(dev_loader))
dev_score = self.metric.accumulate()
# 记录验证集loss
if global_step != -1:
self.dev_losses.append((global_step, dev_loss))
self.dev_scores.append(dev_score)
return dev_score, dev_loss
# 模型评估阶段,使用'torch.no_grad()'控制不计算和存储梯度
@torch.no_grad()
def predict(self, x, **kwargs):
# 将模型设置为评估模式
self.model.eval()
# 运行模型前向计算,得到预测值
logits = self.model(x)
return logits
def save_model(self, save_path):
torch.save(self.model.state_dict(), save_path)
def load_model(self, model_path):
state_dict = torch.load(model_path)
self.model.load_state_dict(state_dict)
class Accuracy():
def __init__(self, is_logist=True):
# 用于统计正确的样本个数
self.num_correct = 0
# 用于统计样本的总数
self.num_count = 0
self.is_logist = is_logist
def update(self, outputs, labels):
# 判断是二分类任务还是多分类任务,shape[1]=1时为二分类任务,shape[1]>1时为多分类任务
if outputs.shape[1] == 1: # 二分类
outputs = torch.squeeze(outputs, dim=-1)
if self.is_logist:
# logist判断是否大于0
preds = torch.tensor((outputs >= 0), dtype=torch.float32)
else:
# 如果不是logist,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
preds = torch.tensor((outputs >= 0.5), dtype=torch.float32)
else:
# 多分类时,使用'torch.argmax'计算最大元素索引作为类别
preds = torch.argmax(outputs, dim=1)
# 获取本批数据中预测正确的样本个数
labels = torch.squeeze(labels, dim=-1)
batch_correct = torch.sum(torch.tensor(preds == labels, dtype=torch.float32)).cpu().numpy()
batch_count = len(labels)
# 更新num_correct 和 num_count
self.num_correct += batch_correct
self.num_count += batch_count
def accumulate(self):
# 使用累计的数据,计算总的指标
if self.num_count == 0:
return 0
return self.num_correct / self.num_count
def reset(self):
# 重置正确的数目和总数
self.num_correct = 0
self.num_count = 0
def name(self):
return "Accuracy"
# SRN模型
class SRN(nn.Module):
def __init__(self, input_size, hidden_size, W_attr=None, U_attr=None, b_attr=None):
super(SRN, self).__init__()
# 嵌入向量的维度
self.input_size = input_size
# 隐状态的维度
self.hidden_size = hidden_size
# 定义模型参数W,其shape为 input_size x hidden_size
if W_attr == None:
W = torch.zeros(size=[input_size, hidden_size], dtype=torch.float32)
else:
W = torch.tensor(W_attr, dtype=torch.float32)
self.W = torch.nn.Parameter(W)
# 定义模型参数U,其shape为hidden_size x hidden_size
if U_attr == None:
U = torch.zeros(size=[hidden_size, hidden_size], dtype=torch.float32)
else:
U = torch.tensor(U_attr, dtype=torch.float32)
self.U = torch.nn.Parameter(U)
# 定义模型参数b,其shape为 1 x hidden_size
if b_attr == None:
b = torch.zeros(size=[1, hidden_size], dtype=torch.float32)
else:
b = torch.tensor(b_attr, dtype=torch.float32)
self.b = torch.nn.Parameter(b)
# 初始化向量
def init_state(self, batch_size):
hidden_state = torch.zeros(size=[batch_size, self.hidden_size], dtype=torch.float32)
return hidden_state
# 定义前向计算
def forward(self, inputs, hidden_state=None):
# inputs: 输入数据, 其shape为batch_size x seq_len x input_size
batch_size, seq_len, input_size = inputs.shape
# 初始化起始状态的隐向量, 其shape为 batch_size x hidden_size
if hidden_state is None:
hidden_state = self.init_state(batch_size)
# 循环执行RNN计算
for step in range(seq_len):
# 获取当前时刻的输入数据step_input, 其shape为 batch_size x input_size
step_input = inputs[:, step, :]
# 获取当前时刻的隐状态向量hidden_state, 其shape为 batch_size x hidden_size
hidden_state = F.tanh(torch.matmul(step_input, self.W) + torch.matmul(hidden_state, self.U) + self.b)
return hidden_state
class Op(object):
def __init__(self):
pass
def __call__(self, inputs):
return self.forward(torch.as_tensor(inputs, dtype=torch.float32))
def forward(self, inputs):
raise NotImplementedError
def backward(self, inputs):
raise NotImplementedError
class OptimizedFunction(Op):
def __init__(self, w):
super(OptimizedFunction, self).__init__()
self.w = torch.as_tensor(w, dtype=torch.float32)
self.params = {'x': torch.as_tensor(0, dtype=torch.float32)}
self.grads = {'x': torch.as_tensor(0, dtype=torch.float32)}
def forward(self, x):
self.params['x'] = x
return torch.matmul(self.w.T, torch.square(self.params['x']))
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):
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']
return torch.as_tensor(all_x), losses
import numpy as np
from matplotlib import pyplot as plt
class Visualization(object):
def __init__(self):
x1 = np.arange(-5, 5, 0.1)
x2 = np.arange(-5, 5, 0.1)
x1, x2 = np.meshgrid(x1, x2)
self.init_x = torch.as_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(1, 0)),
colors=['#e4007f', '#f19ec2', '#e86096', '#eb7aaa', '#f6c8dc', '#f5f5f5', '#000000'])
c = ax.contour(self.init_x[0], self.init_x[1], model(self.init_x.transpose(1, 0)), colors='black')
cbar = fig.colorbar(cp)
ax.plot(x[:, 0], x[:, 1], '-o', color='#000000')
ax.plot(0, 'r*', markersize=18, color='#fefefe')
ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_xlim((-2, 5))
ax.set_ylim((-2, 5))
plt.savefig(fig_name)
def train_and_plot_f(model, optimizer, epoch, fig_name):
x_init = torch.as_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)
losses = np.array(losses)
# 展示x1、x2的更新轨迹
vis = Visualization()
vis.plot_2d(model, x, fig_name)
def train_f(model, optimizer, x_init, 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']
return torch.as_tensor(all_x), losses
# 优化器基类
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]
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.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(30):
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
# 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='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')
plt.xlabel("x")
plt.ylabel("y")
plt.subplots_adjust(wspace=0, hspace=0) # 调整子图间距
plt.show()
3. 解释不同轨迹的形成原因
分析各个算法的优缺点
REF:图灵社区-图书 (ituring.com.cn)
深度学习入门:基于Python的理论与实现
3.1 SGD
轨迹形成原因:SGD收敛轨迹呈“之”字形,是因为y方向变化很大,x方向变化很小,随机收敛只能迂回往复地寻找,效率很低。单纯的朝着梯度方向,使得在函数的形状非均向时,只能反复的寻找。
优点:
-
计算开销小: SGD每次只使用一个样本进行参数更新,因此在大规模数据集上可以减少计算开销和内存消耗。
-
适用于大规模数据集: 由于其计算效率高,SGD特别适用于大规模数据集和高维特征空间的问题。
-
易于实现: SGD算法本身比较简单,易于实现和调试。
缺点:
-
收敛速度相对较慢: 由于每次只利用一个样本进行参数更新,SGD的收敛速度相对较慢,尤其在面对凸函数以外的其他函数时,可能会陷入局部最优解。
-
抖动: SGD容易受到数据的噪声影响,可能会导致参数更新的抖动。
-
需调整学习率: 学习率的选择对SGD算法的性能影响较大,需要进行仔细的调参。
-
不利于处理稀疏数据: 对于稀疏数据,SGD可能需要更多的迭代才能达到收敛。
3.2 AdaGrad
轨迹形成原因:函数的取值高效地向着最小值移动。 由于y轴方向上的梯度较大,因此刚开始变动较大,但是后面会根据前面较大的变动进行调整,减小更新的步伐,导致y轴方向上的更新程度被减弱,“之”字形的变动程度衰减,呈现稳定的向最优点收敛。
优点:
-
自适应学习率: AdaGrad算法可以自适应地调整每个参数的学习率,以反映该参数在训练过程中的历史梯度信息。这使得AdaGrad算法能够更好地处理不同参数之间的差异,从而提高了训练效率和模型的性能。
-
适用于稀疏数据: AdaGrad算法对于稀疏数据的处理效果比较好,因为它能够根据每个参数的历史梯度信息自适应地调整学习率,从而更好地处理那些只有少量非零特征的数据。
-
易于实现: AdaGrad算法本身比较简单,易于实现和调试。
-
泛化性能好: 由于AdaGrad算法能够根据每个参数的历史梯度信息自适应地调整学习率,因此具有一定的正则化效果,能够提高模型的泛化性能。
缺点:
-
学习率下降太快: 由于历史梯度平方和是逐渐增加的,而学习率是逐渐减小的,因此随着迭代次数的增加,学习率会下降得越来越快,可能会导致算法过早收敛。
-
不适用于非凸函数: 对于非凸函数,AdaGrad算法可能会陷入局部最优解。
3.3 RMSprop
轨迹形成原因:RMSprop算法的轨迹图与AdaGrad相比,RMSprop的轨迹到后期表现出更加平缓和稳定的学习率变化,从而更有效地收敛到损失函数的最小值。但是由于该算法会逐渐遗忘过去的梯度,只被近期的梯度所影响,在最初的时候会收敛的更快,变化幅度大.
优点:
-
自适应学习率: RMSprop算法可以自适应地调整每个参数的学习率,以反映该参数在训练过程中的历史梯度信息。与AdaGrad算法不同的是,RMSprop算法使用指数加权移动平均来计算历史梯度平方和,因此能够更好地控制学习率的下降速度,从而避免了AdaGrad算法学习率下降过快的问题。
-
适用于非凸函数: RMSprop算法相对于AdaGrad算法更适合处理非凸函数,因为它使用指数加权移动平均来计算历史梯度平方和,可以更好地控制学习率的下降速度,从而减缓算法陷入局部最优解的风险。
-
易于实现: RMSprop算法本身比较简单,易于实现和调试。
-
泛化性能好: 由于RMSprop算法能够根据每个参数的历史梯度信息自适应地调整学习率,因此具有一定的正则化效果,能够提高模型的泛化性能。
缺点:
-
需要调节超参数: RMSprop算法需要调节一些超参数,如学习率和指数加权移动平均的衰减率等,对于不同的数据集和模型可能需要不同的超参数设置,因此需要一些经验和调试。
-
可能会出现震荡: 由于指数加权移动平均只考虑了过去一段时间内的平方梯度,因此对于某些变化剧烈的情况可能会出现震荡现象,即学习率快速变化。
3.4Momentum
轨迹形成原因:该算法的收敛路径以一种有所抑制的振荡模式接近最小值。动量法是梯度估计修正算法,引入了动量的概念,当梯度方向不一致时,会起到减速作用,增加稳定性。
优点:
-
加速收敛: 动量项可以在更新参数时积累之前梯度的方向信息,有助于在参数空间中跨过局部极小值,加速收敛速度。
-
减少震荡: 动量可以减少训练过程中的震荡,因为它可以在一定程度上抑制参数更新的方差,使更新方向更加稳定。
-
适用于高曲率的情况: 在高曲率的方向上,动量可以帮助算法快速前进,避免陷入局部最小值。
-
容易跳出局部最小值: 动量项有助于克服局部最小值,帮助算法更有可能找到全局最小值。
缺点:
-
可能会超调(overshoot): 当学习率设置过大或者动量项过大时,动量算法可能会导致参数更新过度,在某些情况下甚至会超出最优值范围。
-
需要调优学习率和动量参数: 动量法需要调节学习率和动量参数,对于不同的数据集和模型可能需要不同的参数设置,需要一定的经验和调试。
-
可能受到噪声影响: 动量项的积累可能受到噪声的干扰,尤其是在一些噪声较大的情况下,可能会影响算法的效果。
3.5Nesterov
轨迹形成原因:该算法是对动量法的改进,不仅仅根据当前梯度调整位置,而是根据当前动量在预期的未来位置计算梯度。所以,算法可以相应地调整更新,避免在使用梯度下降时可能出现的振荡,特别是当表面具有陡峭的峡谷时,可能会导致更快地收敛到最小值。图中的轨迹呈现出更加平滑、更有方向性的路径朝向最优点。
优点:
-
更快的收敛速度: Nesterov动量相比标准动量能够更快地收敛到最优解,尤其在参数空间中出现弯曲的情况下,Nesterov动量通常能够更快地找到最优解。
-
减少参数更新的振荡: 与标准动量相比,Nesterov动量能够更好地抑制参数更新的振荡,使得参数更新更加稳定。
-
更好的参数更新估计: Nesterov动量通过提前估计下一步参数的位置,能够更准确地指导参数更新,因此有助于更好地指引搜索方向。
-
更容易跳出局部最小值: Nesterov动量在跳出局部最小值方面相对于标准动量表现更好,有助于全局搜索。
缺点:
-
需要调节学习率和动量参数: 与标准动量一样,Nesterov动量同样需要调节学习率和动量参数,对于不同的数据集和模型可能需要不同的参数设置,需要一定的经验和调试。
-
计算复杂度略高: 相对于标准动量,Nesterov动量需要多进行一次参数的预估计算,因此在一些情况下会带来略微的计算复杂度增加。
3.6Adam
轨迹形成原因:Adam的收敛轨迹图和其他的相比,明显要稳定,基本上是呈直线,或者前期收敛幅度较大,后期逐渐平稳,朝着最优点不断移动。Adam算法由于可以结合了动量法和 RMSprop 算法,不仅何以自适应调整学习率,收敛速度快,并且参数更新更加平稳。
优点:
-
自适应学习率: Adam算法使用了自适应学习率,可以根据参数的历史梯度信息自动调整学习率。这使得算法在不同参数更新的方向上能够更加准确地控制学习步长,提高收敛速度。
-
快速收敛: Adam算法通常能够快速收敛到最优解,尤其在大规模数据集和复杂模型中表现良好。
-
对稀疏梯度适应性强: 在处理稀疏梯度时,Adam算法相比其他优化算法(如Adagrad)的效果更好,能够更好地适应不同特征的更新要求。
-
不需要手动调节学习率参数: Adam算法在很大程度上减轻了学习率的调节负担,相对于传统的梯度下降算法更加方便使用。
缺点:
-
对小批量样本敏感: Adam算法对小批量样本比较敏感,因为它利用了每个样本的梯度估计来更新参数。因此,在处理小批量样本时,可能会导致参数更新的不稳定性。
-
对超参数敏感: Adam算法的效果受到超参数(如学习率、动量项等)的影响较大,需要仔细调节超参数以获得最佳性能。
-
内存消耗较大: Adam算法需要存储每个参数的梯度和动量的平方,这可能会占用较多的内存空间,尤其是在处理大规模模型时。