【23-24 秋学期】NNDL 作业12 优化算法2D可视化

简要介绍图中的优化算法,编程实现并2D可视化

1. 被优化函数 x^{2}

SGD:

from nndl.op import Op
import torch
import numpy as np
from matplotlib import pyplot as plt
 
from nndl.opitimizer import SimpleBatchGD  # 导入简单批量梯度下降优化器
 
# 被优化函数
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  # 更新参数字典中的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'])  # 计算梯度
 
 
# SGD梯度更新
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  # 返回参数更新轨迹和损失值
 
 
# 可视化
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])  # 初始化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)  # 保存可视化结果
        plt.show()
 
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.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')  # 训练模型并进行可视化

AdaGrad:

class Adagrad(Optimizer):
    def __init__(self, init_lr, model, epsilon):
        """
        Adagrad 优化器初始化
        输入:
            - init_lr: 初始学习率
            - model:模型,model.params存储模型参数值
            - epsilon:保持数值稳定性而设置的非常小的常数
        """
        super(Adagrad, self).__init__(init_lr=init_lr, model=model)
        self.G = {}  # 初始化梯度累积字典
        for key in self.model.params.keys():
            self.G[key] = 0  # 将每个参数的梯度累积值初始化为0
        self.epsilon = epsilon  # 设置epsilon用于数值稳定性
 
    def adagrad(self, x, gradient_x, G, init_lr):
        """
        adagrad算法更新参数,G为参数梯度平方的累计值。
        """
        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)  # 调用adagrad方法更新参数
 
torch.manual_seed(0)  # 设置随机种子
w = torch.tensor([0.2, 2])  # 初始权重
model = OptimizedFunction(w)  # 创建模型对象
opt = Adagrad(init_lr=0.5, model=model, epsilon=1e-7)  # 创建Adagrad优化器对象
train_and_plot_f(model, opt, epoch=50, fig_name='opti-vis-para2.pdf')  # 训练并可视化模型参数
plt.show()  # 显示可视化结果
RMSprop:
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)  # 调用rmsprop方法更新参数和G值
 
# 固定随机种子
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)  # 创建RMSprop优化器
train_and_plot_f(model, opt, epoch=50, fig_name='opti-vis-para3.pdf')  # 训练模型并可视化训练过程
Momentum:
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)
        
        # 初始化delta_x字典,用于存储每个参数的梯度的加权移动平均
        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():
            # 使用momentum函数更新每个参数的值,并计算梯度的加权移动平均
            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)

# 创建Momentum优化器对象
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')
Adam:
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')
    优化一下代码:
import torch

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 = {key: torch.zeros_like(value) for key, value in self.model.params.items()}
        self.G = {key: torch.zeros_like(value) for key, value in self.model.params.items()}
        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, value in self.model.params.items():
            self.model.params[key], self.G[key], self.M[key], self.t = self.adam(value,
                                                                                 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')

这里的优化主要包括以下几点:

  1. 在初始化Adam类时,使用字典推导式来初始化梯度平方和梯度的移动平均,避免了显式循环。
  2. adam方法中,使用inplace操作符-=、+=来更新参数和变量,减少了内存消耗。
  3. step方法中,使用items()方法直接迭代模型参数的键值对,简化了代码。
            
2. 被优化函数 x^{2}/20+y^{2} 

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):
    """该函数用于定义二维函数f(x, y)"""
    return x**2/20.0 + y**2

def df(x, y):
    """该函数用于定义二维函数f(x, y)的梯度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

# 遍历优化器字典,使用不同优化算法进行参数更新,并记录参数变化历史
for key in optimizers:
    optimizer = optimizers[key]
    lr = learningrate[idx-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)
    mask = Z > 7
    Z[mask] = 0
    plt.subplot(2, 3, idx)
    idx += 1
    plt.plot(x_history, y_history, 'o-', color="r")
    plt.contour(X, Y, Z, cmap='gray')
    plt.ylim(-10, 10)
    plt.xlim(-10, 10)
    plt.plot(0, 0, '+')
    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()

在代码中,首先定义了几种常用的优化算法类,包括随机梯度下降法(SGD)、Momentum、Nesterov's Accelerated Gradient、AdaGrad、RMSprop和Adam。然后,定义了一个二维函数f(x, y)和其梯度函数df(x, y)。

接下来,定义了初始位置init_pos,并初始化参数params和梯度grads。然后,定义了优化器字典optimizers,其中包含了不同的优化算法和对应的学习率。遍历优化器字典,使用每种优化算法进行参数更新,迭代30次,并记录参数的变化历史x_history和y_history。

3. 解释不同轨迹的形成原因

分析各个算法的优缺点

SGD(随机梯度下降)的轨迹形成原因是由于其在y轴方向上的更新变化较大,而在x轴方向上的变化较小。因为梯度具有随机性质,所以梯度搜索轨迹会表现出嘈杂的动荡现象,呈现之字形。

  • 优点:易于理解和实现,对于大规模数据集和高维度特征有效。
  • 缺点:收敛速度较慢且可能停留在局部最小值处。

AdaGrad算法的轨迹形成原因是函数的取值会高效地朝着最小值移动。初始阶段,由于y轴方向上的梯度较大,更新变化较大。随着时间的推移,根据之前较大的更新变动进行调整,减小了更新的步伐,导致y轴方向上的更新程度衰减,形成稳定的收敛路径。

  • 优点:能够自适应地调整学习率,适用于稀疏数据集。
  • 缺点:学习率会随着训练的进行变得过小,导致提前停止更新参数。

RMSprop算法的轨迹形成原因是相对于AdaGrad算法,RMSprop的轨迹图更加平缓和稳定,更有效地收敛到损失函数的最小值。然而,由于该算法逐渐遗忘过去的梯度,只受近期梯度的影响,在初始阶段会收敛得更快,变动幅度较大。

  • 优点:解决了AdaGrad学习率过早减小的问题,更适合非平稳目标函数。
  • 缺点:可能受到梯度消失或爆炸的影响,需要仔细调整学习率。

Momentum算法的轨迹形成原因是引入了动量的概念,当梯度方向不一致时会起到减速作用,增加了稳定性。与SGD相比,在x轴方向上受到的力较小,一直在同向受力,导致加速;而y轴方向上受到的力较大,交替正反向的力会互相抵消。

  • 优点:加速了收敛速度,提高了稳定性。
  • 缺点:可能会在某些情况下错过局部最优解,需要仔细调整动量参数。

Nesterov算法是对动量法的改进,不仅根据当前梯度调整位置,还根据当前动量在预期未来位置计算梯度。这样可以相应地调整更新,避免振荡现象,尤其是在具有陡峭峡谷的表面上,可以更快地收敛到最小值。

  • 优点:相比标准的动量法,更容易避免梯度更新的震荡。
  • 缺点:需要额外的计算步骤,有时会增加计算成本。

Adam算法的轨迹形成原因是由于结合了动量法和RMSprop算法,不仅可以自适应地调整学习率,收敛速度快,而且参数更新更加平稳。其收敛轨迹图通常呈直线或者前期收敛幅度较大,后期逐渐平稳,朝着最优点移动。

  • 优点:结合了动量和自适应学习率的优点,适用于大多数情况,并且对超参数的选择不太敏感。
  • 缺点:对一些非凸优化问题可能不够稳定,需要小心调整超参数。

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值