深度学习keras深度学习模型深度学习与神经网络

神经网络的优化器

2019-08-12  本文已影响0人  Vector_Wan

我们知道,神经网络的学习的目的就是寻找合适的参数,使得损失函数的值尽可能小。解决这个问题的过程为称为最优化。解决这个问题使用的算法叫做优化器

1. SGD

在前面我们实现的神经网络中所使用的优化方法是随机梯度下降法(Stachastic gradient desent 简称 SGD)。SGD 的想法就是沿着梯度的方向前进一定距离。用数学的语言来描述的话可以写成下式:

W \leftarrow W - \eta \frac{\partial L}{\partial W}

这里面,W 表示需要更新的权重,\frac{\partial L}{\partial W} 表示损失函数关于 W 的梯度(准确点来说这是一个 Jacobian 矩阵),\eta 表示学习率,\leftarrow 表示使用右边的值更新左边的值。下面我们先给出一个 python 实现:

class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr
        
    def update(self, params, grades):
        for key in params.keys():
            params[key] -= self.lr * grades[key]

将优化器实现为一个类是一个很好的做法, python 是动态语言的原因,我们在实现的时候只要类里面都有 update 方法,解释器就会正常执行。例如我们可以将之前的学习过程写成这样:重点在于加了 # 号的那几行。

# 导入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

# 全局变量
train_loss_list = []
train_acc_list = []
test_acc_list = []

# 超参数
iter_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
iter_per_epoch = max(train_size / batch_size, 1)

# 生成模型
net_work = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
optimizer = SGD(learning_rate)   ########

# 模型的训练
for i in range(iter_num):
    # 获取 mini_batch
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 计算梯度
    grads = net_work.gradient(x_batch, t_batch)

    # 更新参数
    params = net_work.params
    optimizer.update(params, grads)  ##########

    # 记录学习过程
    if i % iter_per_epoch == 0:
        loss = net_work.loss(x_train, t_train)
        train_loss_list.append(loss)
        train_acc = net_work.accuracy(x_train, t_train)
        train_acc_list.append(train_acc)
        test_acc = net_work.accuracy(x_test, t_test)
        test_acc_list.append(test_acc)
        print('运行中... 损失:{} 训练集准确度:{}  测试集准确度:{}'.format(loss, train_acc, test_acc))

SGD 的优点就是简单,容易实现。但是其缺点就是低效,因为有的时候梯度的方向并没有指向最小值的方向。低效的原因有两大方面:

下面的方法就全是针对这两大方面对 SGD 进行改进。

2. Momentum

这种方法主要是为了解决第一种情况,当函数呈延伸状的情况下,梯度指向了谷底,而不是直接指向了最低点,函数值在学习过程中会来回震荡,但是向最低点移动的却很小。

上面的叙述中提到了两个方向,一个是纵向的震动,一个是横向的向最低点移动,如下图,如果我们可以避免或者减少震荡,加快横向的向最低点移动,那么就加快了学习。

实际上震荡是不可避免的,所以我们只能考虑减轻震荡。

我们还是先看一下数学描述:

v_t \leftarrow \alpha v_{t - 1} - \eta \frac{\partial L}{\partial W} W \leftarrow W + v_t

Momentum 在 SGD 的基础上引入了一个变量 - 速度 v 和一个超参数 - 指数衰减平均 \alpha

我们分别来看一下他们的含义。为了便于理解,我们先将 \alpha 去掉,或者说是设为 1。首先将 v_0 初始化为零矩阵,然后进行第一次迭代,v_1 保存的就是上一次的梯度,如果方向没改变,再一次迭代的时候梯度会被累加,加快学习。如果方向改变了(符号发生了变化),那么就减少了这次的学习,这样其实就实现了我们的目的:使得梯度方向不变的维度上速度变快,梯度方向有所改变的维度上的更新速度变慢,这样就可以加快收敛并减小震荡

接下来我们再来看一下超参数 \alpha ,它是描述之前梯度对现在影响的参数。\alpha 越大表示之前梯度对现在的影响越大。\alpha 一般被设定为 0.9。

说了这么多理论,我们还是要实现一下这个优化器:

class Momentum:
    def __init__(self, lr=0.01, alpha=0.9):
        self.lr = lr
        self.alpha = alpha
        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.alpha*self.v[key] - self.lr*grads[key]
                params[key] += self.v[key]

如果想使用 Momentum ,只需要将上面代码中的 optimizer = SGD(learning_rate) 修改为 optimizer = Momentum(learning_rate) 即可。
保持其他参数不变,训练的结果是:

SGD: 损失:0.5450568343946544 训练集准确度:0.8639  测试集准确度:0.8672
Momentum: 损失:0.1830601846034292 训练集准确度:0.94755  测试集准确度:0.9444

所以总结一句,优点:使得梯度方向不变的维度上速度变快,梯度方向有所改变的维度上的更新速度变慢,可以加快收敛并减小震荡。
但是也有缺点:这种方法相当于小球从山上滚下来时是在盲目地沿着坡滚,如果它能具备一些先知,例如快要上坡时,就知道需要减速了的话,适应性会更好。根据这个改进的优化器叫做:NAG(Nesterov Accelerated Gradient)

3. AdaGrad

这种方法主要是为了解决 SGD 遇到鞍点或者极小值点后学习变慢的问题。我们知道超参数学习率是一个很重要的参数,不同的参数对学习结果的影响很大,如果设置的值较小,会导致学习花费较多的时间,学习率大了就会导致学习发散而不能正常的进行。所以我们可以考虑避免人为的介入,根据需要让程序自己动态地设置学习率。例如对于遇到鞍点的情况,参数变化很小,基本不会改变,那么这个方法就会设置一个较大的学习率,跨过鞍点。

在神经网络中有一种方法经常被使用:学习率衰减方法(learning rate decay),也就是说随着学习的进行,使学习率逐渐减少。AdaGrade 进一步发展了这个想法,它会为参数的每一个元素设当的调整学习率。

我们还是看一下数学的描述:

h \leftarrow h + \frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W} W \leftarrow W - \eta \frac{1}{\sqrt h}\frac{\partial L}{\partial W}

这里新出现了一个变量 h ,它保存了之前所有梯度的平方和,在更新参数的时候通过乘以 \frac{1}{\sqrt h} 就可以调整学习的尺度。
我们还是尝试实现它:

class 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)

结果为:

AdaGrad: 损失:0.20811779864341037 训练集准确度:0.9411  测试集准确度:0.936

AdaGrad 的优点是可以动态的调整学习率,
缺点是 AdaGrad 会记录过去所有的梯度平方和,最后有可能不再更新,
针对这个问题有一个方法叫做 RMSProp 进行了优化。

4. Adam

Adam 直观的来讲就是融合了 Momentum 和 AdaGrad 方法,详细可以参考原版论文
详细回头再更,,

class Adam:
    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)

最后看两个动态的优化过程,图片来自网络,侵删。
第一个是存在鞍点和局部极小值的情况。



第二个是损失函数呈延伸状的情况。


上一篇 下一篇

猜你喜欢

热点阅读