神经网络模型优化 - 梯度

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

之前的模型中我们实现了一个二层神经网络,准确度还行,但是训练的时间极长,我们在调参数之前先看看能不能对它的训练速度进行一个提升。

在这个模型中我们使用了梯度下降法,并使用数值方法近似求损失函数关于各个参数的偏导数,一个很直接的想法就是将数值方法换成 基于计算图理论的前后传播方法。这个方法可以有效提高求导数的效率,目前很多成熟的模型求导都是用的这种方法。

因为我们的模型比较简单,所以我们就不怎么改动之前的代码的结构直接修改 gradient 函数。(其实在使用计算图的时候更适合将每一个运算拆开,分别组成模块,使用的时候只需要像积木一样组装就好了,这是由计算图的性质决定的)

先上源码:

    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}

        batch_num = x.shape[0]

        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)

        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)

        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

使用计算图的话就需要一个向前传播,然后保存好相关计算数据,接着向后传播计算梯度。

向前传播很好办,就是之前的预测函数,因为我们在向后传播的时候要用到向前传播的数据,所以不要直接用 predict 函数,还是要在这里面写一遍,

向后传播的时候需要知道一些运算的向后传播导数计算方法,具体的推导过程在这里就不写了,直接给出结果:可以参考https://blog.csdn.net/oBrightLamp/article/details/84333111

对于 softmax with loss 层反向传播的是 softmax 层的输出和监督标签的差分,也就是 (y_1-t_1,y_2-t_2,y_3-t_3)

对于 Affine 层(批处理版本)可以参考下图:


其实 Affine 的意思是仿射变换,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间。
具体来说,一个集合 X 的放射变换为:

f(x) =Y= Wx + B, ~~x\in X

直接看反向传播,首先最右端传过来一个 Y 例如大小为 (N,3)N 表示批的大小。这里面的表示方法其实是 numpy 的表示法,但是它的意义和矩阵理论中的一样,但是需要注意的是,numpy 里面的矩阵每一行作为一条信息。例如 np.array([ [1,2,3], [1,2,3] ]) 的大小为 (2,3)

继续向前传播,遇到了一个加法运算,对于加法运算,直接把上层结果 \frac{\partial L}{\partial Y} 传递过去,但是还有一个问题,在正向传播的时候,偏置被加到了每一个数据上面,所以各个数据反向传播的值需要汇总一下作为偏置的梯度。

再看另一个方向,另一个方向是一个点乘,这个没什么问题就是乘法的运算图。交换后相乘向下传播。

最后是 sigmoid 层的反向传播,还记得当时在刚接触深度学习的时候老师跟我们讲 sigmoid 说为什么使用这个函数,有一个很重要的原因就是它好求导,当时没什么感觉,直到了解了它的反向传播才理解了这句话,
直接,上层下来一个 \frac{\partial L}{\partial y} 输出 \frac{\partial L}{\partial y}y(1-y)
为了代码的简洁,我单独写了一个函数,其实在这个地方不单独写也没事

def sigmoid_grad(x):
    return (1.0 - sigmoid(x)) * sigmoid(x)

调用的时候传入 a1 然后乘上 da1 就可以。
再看代码就会感觉代码的含义还是很明确的。这样这个求梯度的函数就写完啦,

从时间上来看明显是误差反向传播的方法更快,但是实现起来比较复杂容易出错,我们可以比较一下数值微分和误差反向传播计算的微分,看看是否一致,因为计算机计算精度的原因,一般来说是不一致的,但是不会相差太多,

这种操作被称为梯度确认

我们来测试一下我们写的代码

# coding: utf-8
import numpy as np
from mnist import load_mnist
from two_layer_net import TwoLayerNet

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

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

x_batch = x_train[:3]
t_batch = t_train[:3]

grad_numerical = network.numerical_gradient(x_batch, t_batch)
grad_backprop = network.gradient(x_batch, t_batch)

for key in grad_numerical.keys():
    diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
    print(key + ":" + str(diff))
    

从结果上来看确实差的不多:基本上都是在 10^{-7} 以下的数量级。

上一篇下一篇

猜你喜欢

热点阅读