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

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

简要介绍:

首先,为了方便理解,可以想象成这样一个故事:“有一个性情古怪的探险家。他在广袤的干旱地带旅行,坚持寻找幽深的山谷,他的目标是要达到最深的谷底。他给自己制定了两个规定:一是不许看地图;二是把眼睛蒙上。因此,他并不知道最深的谷底在这个广袤的大地的何处。在如此严苛的条件下,冒险家如何才能找到“至深之地”呢?”

优化算法大体上可以分为两类:
1) 调整 学习率 , 使得优化更稳定
2) 梯度估计 修正, 优化训练速度

编程实现:


1. 被优化函数 :0.2*x_{1}^{2}+2*x_{2}^{2}

首先定义有关优化器方面的代码:

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. 被优化函数  :\frac{x^{2}}{20}+y^{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()


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博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值