AI-02a5a4.神经网络-与学习相关的技巧-参数更新

发布于:2025-05-13 ⋅ 阅读:(13) ⋅ 点赞:(0)

参数的更新

神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化(optimization)。

而(深度)神经网络的参数空间非常大而复杂,无法轻易找到最优解。

AI-02a5a2.神经网络的学习中,为了找到最优参数,介绍了梯度法,将参数的梯度(导数)作为了线索。

使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机梯度下降法(stochastic gradient descent),简称SGD

在这里插入图片描述

SGD

式(6.1), W ← W − η ∂ L ∂ W W \leftarrow W - \eta\frac{\partial L}{\partial W} WWηWL
这里把需要更新的权重参数记为 W W W,把损失函数关于 W W W的梯度记为 ∂ L ∂ W \frac{\partial L}{\partial W} WL η \eta η表示学习率,实际上会取0.01或0.001这些事先决定好的值。式子中的 ← \leftarrow 表示用右边的值更新左边的值。

SGD是朝着梯度方向只前进一定距离的简单方法。

class SGD:

    """
    随机梯度下降法(Stochastic Gradient Descent)
    lr: 学习率
    """
    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]

在这里插入图片描述

缺点

如果函数的形状非均向(anisotropic),比如呈延伸状,搜索的路径就会非常低效。
SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。

Momentum

式(6.3), v ← α v − η ∂ L ∂ W v \leftarrow \alpha v - \eta \frac{\partial L}{\partial W} vαvηWL

式(6.4), W ← W + v W \leftarrow W + v WW+v
W W W表示要更新的权重参数, ∂ L ∂ W \frac{\partial L}{\partial W} WL表示损失函数关于 W W W的梯度, η \eta η表示学习率。 v v v对应物理上的速度。

式(6.3)表示了物体在梯度方向上受力,在这个力的作用下,物体的速度增加这一物理法则。
Momentum 方法感觉就像式小球在斜面上滚动,如图6.4所示。

在这里插入图片描述

式(6.3)中有 α v \alpha v αv这一项。在物体不受任何力时,该项承担使物体逐渐减速的任务( α \alpha α设定为0.9之类的值),对应物理上的地面摩擦或空气阻力。

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]

实例变量v会保存物体的速度。初始化时, v v v中什么都不保存,但当第一次调用update()时, v v v会以字典型变量的形式保存与参数结构相同的数据。剩余的代码部分就是将式(6.3)、式(6.4)写出来.

在这里插入图片描述

AdaGrad

在神经网络的学习中,学习率(数学式中记为η)的值很重要。学习率过小,会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能正确进行。

在关于学习率的有效技巧中,有一种被称为学习率衰减(learning rate decay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。

逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。而AdaGrad 进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。

AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习(AdaGrad的Ada来自英文单词Adaptive,即“适当的”的意思)。

式(6.5), h ← h + ∂ L ∂ W ⊙ ∂ L ∂ W h \leftarrow h + \frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W} hh+WLWL
式(6.6), W ← W − η 1 h ∂ L ∂ W W \leftarrow W - \eta \frac{1}{\sqrt{h}} \frac{\partial L}{\partial W} WWηh 1WL

W W W表示要更新的权重参数, ∂ L ∂ W \frac{\partial L}{\partial W} WL表示损失函数关于 W W W的梯度, η \eta η表示学习率。
变量 h h h ,如式(6.5)所示,它保存了以前的所有梯度值的平方和(式(6.5)中的 ⊙ \odot 表示对应矩阵元素的乘法)。然后,在更新参数时,通过乘以 1 h \frac{1}{\sqrt{h}} h 1,就可以调整学习的尺度。这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。

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)

这里需要注意的是,最后一行加上了微小值1e-7。这是为了防止当self.h[key]中有0时,将0用作除数的情况。这个微小值也可以设定为参数。

在这里插入图片描述

AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,更新的幅度就越小。实际上,如果无止境地学习,更新量就会变为 0,完全不再更新。为了改善这个问题,可以使用 RMSProp 方法。RMSProp方法并不是将过去所有的梯度一视同仁地相加,而是逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。这种操作从专业上讲,称为“指数移动平均”,呈指数函数式地减小过去的梯度的尺度。

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)

Adam

Adam(Adaptive Moment Estimation)是一种自适应学习率的优化算法,结合了 MomentumRMSprop 的思想。

初始化

  • 学习率:lr
  • 一阶矩衰减率:beta1(通常为 0.9)
  • 二阶矩衰减率:beta2(通常为 0.999)
  • 一阶矩变量:m(初始化为 0)
  • 二阶矩变量:v(初始化为 0)
  • 迭代次数:t(初始化为 0)

更新规则

对于每个参数 θ 和其梯度 g,Adam 的更新步骤如下:

  1. 计算一阶矩(动量)
    m t = β 1 ⋅ m t − 1 + ( 1 − β 1 ) ⋅ g t m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t mt=β1mt1+(1β1)gt

  2. 计算二阶矩(梯度平方的指数移动平均)
    v t = β 2 ⋅ v t − 1 + ( 1 − β 2 ) ⋅ g t 2 v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2 vt=β2vt1+(1β2)gt2

  3. 偏差校正
    由于 m t m_t mt v t v_t vt 在初始阶段偏向于 0,需要进行偏差校正:
    m ^ t = m t 1 − β 1 t \hat{m}_t = \frac{m_t}{1 - \beta_1^t} m^t=1β1tmt
    v ^ t = v t 1 − β 2 t \hat{v}_t = \frac{v_t}{1 - \beta_2^t} v^t=1β2tvt

  4. 更新参数
    θ t = θ t − 1 − lr ⋅ m ^ t v ^ t + ϵ \theta_t = \theta_{t-1} - \frac{\text{lr} \cdot \hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} θt=θt1v^t +ϵlrm^t
    其中,ϵ 是一个很小的常数(如 1e-7),用于防止分母为零。


η t = η 1 − β 2 t 1 − β 1 t \eta_t = \eta\frac{\sqrt{1-\beta_2^t}}{1-\beta_1^t} ηt=η1β1t1β2t
m ← m + ( 1 − β 1 ) ( ∂ L ∂ W − m ) m \leftarrow m + (1-\beta_1)(\frac{\partial L}{\partial W}-m) mm+(1β1)(WLm)
v ← v + ( 1 − β 2 ) ( ∂ L ∂ W ⊙ ∂ L ∂ W − v ) v \leftarrow v + (1-\beta_2)(\frac{\partial L}{\partial W}\odot\frac{\partial L}{\partial W}-v) vv+(1β2)(WLWLv)
W ← W − η t m v W \leftarrow W - \eta_t \frac{m}{\sqrt{v}} WWηtv m

W W W表示要更新的权重参数, ∂ L ∂ W \frac{\partial L}{\partial W} WL表示损失函数关于 W W W的梯度。
η \eta η 表示学习率,通常是0.001。
β 1 \beta_1 β1 是一阶矩(动量)的衰减率,通常为 0.9。
β 2 \beta_2 β2 是二阶矩(梯度平方的指数移动平均)的衰减率,通常为 0.999。
t t t 是当前迭代次数,从 1 开始计数。
1 − β 2 t \sqrt{1 - \beta_2^t} 1β2t 这是对二阶矩的偏差校正项,由于初始阶段 v v v 偏向于 0,需要通过 1 − β 2 t \sqrt{1 - \beta_2^t} 1β2t 进行校正。
1 − β 1 t 1 - \beta_1^t 1β1t 这是对一阶矩的偏差校正项,由于初始阶段 m m m 偏向于 0,需要通过 1 − β 1 t 1 - \beta_1^t 1β1t 进行校正。
η t \eta_t ηt 这是 自适应学习率,结合了初始学习率、一阶矩和二阶矩的偏差校正项,随着迭代次数 (t) 的增加, η t \eta_t ηt 会逐渐趋于稳定。

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] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            # self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)

            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)

            # unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            # unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            # params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

在这里插入图片描述

对比总结

在这里插入图片描述

以 手 写 数 字 识 别 为 例,比 较 前 面 介 绍 的 SGD、Momentum、AdaGrad、Adam这4种方法,并确认不同的方法在学习进展上有多大程度的差异。

一个5层神经网络为对象,其中每层有100个神经元。激活函数使用的是ReLU。结果如下图所示,
在这里插入图片描述

算法 梯度计算方式 学习率调整机制 核心优势
SGD 当前梯度 固定学习率 简单直观,适合凸优化问题
动量法 当前梯度 + 历史梯度累积 固定学习率 加速收敛,缓解震荡
RMSProp 当前梯度 基于历史梯度平方自适应调整 自适应学习率,适合非凸问题
Adam 当前梯度 + 一阶动量 结合一阶、二阶动量自适应调整 综合性能优,低超参数敏感度

multi_layer_net.py

# coding: utf-8

import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
from collections import OrderedDict
from common.layers import *
from common.gradient import numerical_gradient
  

class MultiLayerNet:

    """全连接的多层神经网络
    
    Parameters
    ----------
    input_size : 输入大小(MNIST的情况下为784)
    hidden_size_list : 隐藏层的神经元数量的列表(e.g. [100, 100, 100])
    output_size : 输出大小(MNIST的情况下为10)
    activation : 'relu' or 'sigmoid'
    weight_init_std : 指定权重的标准差(e.g. 0.01)
        指定'relu'或'he'的情况下设定“He的初始值”
        指定'sigmoid'或'xavier'的情况下设定“Xavier的初始值”
    weight_decay_lambda : Weight Decay(L2范数)的强度
    """
    def __init__(self, input_size, hidden_size_list, output_size,
                 activation='relu', weight_init_std='relu', weight_decay_lambda=0):
        self.input_size = input_size
        self.output_size = output_size
        self.hidden_size_list = hidden_size_list
        self.hidden_layer_num = len(hidden_size_list)
        self.weight_decay_lambda = weight_decay_lambda
        self.params = {}
  
        # 初始化权重
        self.__init_weight(weight_init_std)

        # 生成层
        activation_layer = {'sigmoid': Sigmoid, 'relu': Relu}
        self.layers = OrderedDict()

        for idx in range(1, self.hidden_layer_num+1):
            self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)], self.params['b' + str(idx)])
            self.layers['Activation_function' + str(idx)] = activation_layer[activation]()

        idx = self.hidden_layer_num + 1
        self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)], self.params['b' + str(idx)])

        self.last_layer = SoftmaxWithLoss()

  

    def __init_weight(self, weight_init_std):

        """设定权重的初始值
        
        Parameters
        ----------
        weight_init_std : 指定权重的标准差(e.g. 0.01)
            指定'relu'或'he'的情况下设定“He的初始值”
            指定'sigmoid'或'xavier'的情况下设定“Xavier的初始值”
        """

        all_size_list = [self.input_size] + self.hidden_size_list + [self.output_size]
        for idx in range(1, len(all_size_list)):
            scale = weight_init_std
            if str(weight_init_std).lower() in ('relu', 'he'):
                scale = np.sqrt(2.0 / all_size_list[idx - 1])  # 使用ReLU的情况下推荐的初始值
            elif str(weight_init_std).lower() in ('sigmoid', 'xavier'):
                scale = np.sqrt(1.0 / all_size_list[idx - 1])  # 使用sigmoid的情况下推荐的初始值
  
            self.params['W' + str(idx)] = scale * np.random.randn(all_size_list[idx-1], all_size_list[idx])
            self.params['b' + str(idx)] = np.zeros(all_size_list[idx])

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x

    def loss(self, x, t):
        """求损失函数
        Parameters
        ----------
        x : 输入数据
        t : 教师标签

        Returns
        -------
        损失函数的值
        """
        y = self.predict(x)
        weight_decay = 0
        for idx in range(1, self.hidden_layer_num + 2):
            W = self.params['W' + str(idx)]
            weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)
        return self.last_layer.forward(y, t) + weight_decay

    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    def numerical_gradient(self, x, t):
        """求梯度(数值微分)
        Parameters
        ----------
        x : 输入数据
        t : 教师标签
        
        Returns
        -------
        具有各层的梯度的字典变量
            grads['W1']、grads['W2']、...是各层的权重
            grads['b1']、grads['b2']、...是各层的偏置
        """
        loss_W = lambda W: self.loss(x, t)
        grads = {}
        for idx in range(1, self.hidden_layer_num+2):
            grads['W' + str(idx)] = numerical_gradient(loss_W, self.params['W' + str(idx)])
            grads['b' + str(idx)] = numerical_gradient(loss_W, self.params['b' + str(idx)])

        return grads

    def gradient(self, x, t):
        """求梯度(误差反向传播法)
        Parameters
        ----------
        x : 输入数据
        t : 教师标签

        Returns
        -------
        具有各层的梯度的字典变量
            grads['W1']、grads['W2']、...是各层的权重
            grads['b1']、grads['b2']、...是各层的偏置
        """
        # forward
        self.loss(x, t)
        
        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()

        for layer in layers:
            dout = layer.backward(dout)

        # 设定
        grads = {}
        for idx in range(1, self.hidden_layer_num+2):
            grads['W' + str(idx)] = self.layers['Affine' + str(idx)].dW + self.weight_decay_lambda * self.layers['Affine' + str(idx)].W
            grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db

        return grads

optimizer_compare_naive.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from common.optimizer import *

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

optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["AdaGrad"] = AdaGrad(lr=1.5)
optimizers["Adam"] = Adam(lr=0.3)

idx = 1

for key in optimizers:
    optimizer = optimizers[key]
    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, 2, idx)
    idx += 1
    plt.plot(x_history, y_history, 'o-', color="red")
    plt.contour(X, Y, Z)
    plt.ylim(-10, 10)
    plt.xlim(-10, 10)
    plt.plot(0, 0, '+')
    #colorbar()
    #spring()
    plt.title(key)
    plt.xlabel("x")
    plt.ylabel("y")
plt.show()

optimizer_compare_mnist.py

# coding: utf-8
import os
import sys
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import *

# 0:读入MNIST数据==========

(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)

train_size = x_train.shape[0]

batch_size = 128

max_iterations = 2000

# 1:进行实验的设置==========
optimizers = {}
optimizers['SGD'] = SGD()
optimizers['Momentum'] = Momentum()
optimizers['AdaGrad'] = AdaGrad()
optimizers['Adam'] = Adam()
# optimizers['RMSprop'] = RMSprop()

networks = {}
train_loss = {}

for key in optimizers.keys():
    networks[key] = MultiLayerNet(
        input_size=784, hidden_size_list=[100, 100, 100, 100],
        output_size=10)
    train_loss[key] = []    

# 2:开始训练==========
for i in range(max_iterations):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    for key in optimizers.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimizers[key].update(networks[key].params, grads)
        loss = networks[key].loss(x_batch, t_batch)
        train_loss[key].append(loss)

    if i % 100 == 0:
        print( "===========" + "iteration:" + str(i) + "===========")
        for key in optimizers.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))

# 3.绘制图形==========
markers = {"SGD": "o", "Momentum": "x", "AdaGrad": "s", "Adam": "D"}
# markers = {"SGD": "o", "Momentum": "x", "RMSprop": "s", "Adam": "D"}
x = np.arange(max_iterations)
for key in optimizers.keys():
    plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
plt.show()

网站公告

今日签到

点亮在社区的每一天
去签到