实现二层神经网络反向传播

在这里插入图片描述

友情链接

用numpy构建多种损失函数
使用mnist进行神经网络演练
使用numpy实现简单二层神经网络
python实现梯度下降优化算法

前记

通过上面三篇博客的撰写,大概了解到了前馈神经网络的基本架构,并且完成了一个能够训练的简单二层神经网络。但是问题也非常的明显:采用解析法(即求导法则)进行神经网络训练,每更新两次输入数据并进行前馈计算,才能获取到一个权重或者偏置的导数,显然成本大的惊人,不适合大数据的实现方式,因此,通过学习了解到反向传播的相关机制以及实现方式,进行一下总结与分享。

正文

反向传播概述

通过反向传播,我们可以完成求导法则无法实现的高训练速度,虽然可能不如求导法则准确,但是只要比以上方法多几次次迭代后其准确率基本是一致的甚至超越前者,并且经过实际测试,其训练相同数据的速度大约快了600倍不止,因此足以看出孰优孰劣。
反向传播的实现方式可以从最简单的加减乘除说起,我们可以把任何比较困难的层次拆分为最简单的加减乘除的层次。我们通过前一层到后一层的加减乘除关系,来反向推导出传递给前面节点的权重为多少,通过求导的方式不断向前递进,最终得到目标节点如W,b的梯度,从而进行更新。
说白了,我们获得梯度的方式就是通过求导,反向传播巧妙的利用这一原理,从最后一个神经元向前传播梯度,最终得到所有节点的导数,其中多数导数需要用到矩阵的求偏导公式,因此数学复杂性确实有所增加,但是在这效率面前,不得不臣服…

加法、乘法的反向传播

借用一下图
加法传播从导数中可以简单理解:
y = x 1 + x 2 y = x_1+x_2 y=x1+x2
α y a x 1 = 1 \frac{αy}{ax_1}=1 ax1αy=1
α y a x 2 = 1 \frac{αy}{ax_2}=1 ax2αy=1
因此可以理解加法就是将后一个节点的导数原封不动的传给前面的两个分支节点。

乘法也是如此:
y = x 1 ∗ x 2 y = x_1*x_2 y=x1x2
α y a x 1 = x 2 \frac{αy}{ax_1}=x_2 ax1αy=x2
α y a x 2 = x 1 \frac{αy}{ax_2}=x_1 ax2αy=x1
即可以理解为两个前面的节点的x值互换并乘以传播过来的累积导数。

Relu层的反向传播

Relu层是神经网络激活函数里面最简单的一个函数:
借图
导数为:
借图
后面需要用到,所以实现贴一下:

class _Relu:
	def __init__(self):
		self.mask = None
	
	def forward(self, X):
		self.mask = X<=0
		y = X.copy()
		y[self.mask]=0
		return y
	
	def backward(self, dout):
		dout[self.mask] = 0
		return dout
Affine层的实现

Affine层中文释义为仿射层,实际就是计算
X ⋅ W + b X·W+b XW+b的层次,即是前一层神经元映射成后一层神经元的计算环节,其反向传播涉及到矩阵的偏导数计算:
Y = X ⋅ W + b Y = X·W+b Y=XW+b
α Y a X = α ( X ⋅ W + b ) a X = W T \frac{αY}{aX}=\frac{α(X·W+b)}{aX}=W^T aXαY=aXα(XW+b)=WT
α Y a W = α ( X ⋅ W + b ) a W = X T \frac{αY}{aW}=\frac{α(X·W+b)}{aW}=X^T aWαY=aWα(XW+b)=XT
如果连同前面传递下来的导数αL/αy一起计算的话,可以得到Affine层反向传播完整求导公式为:
α L α X = α L α Y ⋅ W T \frac{αL}{αX}=\frac{αL}{αY}·W^T αXαL=αYαLWT
α L α W = X T ⋅ α L α Y \frac{αL}{αW}=X^T·\frac{αL}{αY} αWαL=XTαYαL
α L α B = α L α Y 第 0 轴 之 和 \frac{αL}{αB}=\frac{αL}{αY}第0轴之和 αBαL=αYαL0
可能整个神经网络的实现有时候不太容易摸清头脑,但是一层神经元的计算总是简单的:

class _Affine:
	def __init__(self, W, b):
		self.W = W
		self.b = b
		self.dW = None
		self.db = None
		self.x = None
	
	def forward(self, X):
		self.x = X
		y = np.dot(X, self.W)+self.b
		return y
	
	def backward(self, dout):
		self.dW = np.dot(self.x.T,dout)
		dx = np.dot(dout, self.W.T)
		self.db = np.sum(dout, axis=0)
			
sigmoid层反向传播

α y α x = α ( 1 1 + e − x ) α x = − ( 1 1 + e − x ) 2 ⋅ e − x ⋅ ( − 1 ) = e − x ( 1 + e − x ) 2 = y ⋅ ( 1 − y ) \frac{αy}{αx} = \frac{α(\frac{1}{1+e^{-x}})}{αx}=-(\frac{1}{1+e^{-x}})^2·e^{-x}·(-1)=\frac{e^{-x}}{(1+e^{-x})^2}=y·(1-y) αxαy=αxα(1+ex1)=(1+ex1)2ex(1)=(1+ex)2ex=y(1y)
化简后相当简单,当然实现直接用最终的结果,一样的简单

class _sigmoid:
	def __init__(self):
		self.y = None
	
	def forward(self, X):
		self.y = 1/(1+np.exp(-X))
		return self.y
	
	def backward(self, dout):
		return np.dot(dout, self.y*(1-self.y))

注意点乘和普通乘法*的区别。

softmax与cross_entropy反向传播

softmax与cross_entropy是相生相连,有softmax,其损失函数必定为交叉熵损失函数,那么我们可以把这一部分合并为一层,不过这一部分的反向传播比较复杂,需要多多推导理解,下面贴一下反向传播图:
借鉴图
看到结果便可以发现这一部分的反向传播的最终梯度为y1 -t1,即预测值与标签值的差值,这一部分表示出了实际值与预测值之间的差距,前面的全部神经元的梯度都以此为基础目标,通过不断减小这个差值来完成训练。以此这里为神经网络反向传播奠定了非常重要的基础。
下面为相关实现:
(softmax与cross_entropy_error可以参考前面文章)

class _SoftmaxWithLoss:
	def __init__(self):
		self.y = None
		self.t = None
		self.loss = None
	
	def forward(self, X, t):
		self.y = soft_max(X)
		self.t = t
		self.loss = cross_entropy_error(X, t)
		return loss
	def backward(self, dout=1):
		batch_size = self.t.shape[0]
		return dout*(self.y-self.t)/batch_size
	

这里的dout=1的原因是这里是神经网络的最后一层,所以需要为反向传播的起点设定一个初始值。

实现反向传播神经网络

这里主要对比上一篇的二层神经网络进行讲解,我们只需要改动一部分设置,就可以实现一个高效的二层神经网络:

步骤
  1. 更改init函数,通过Relu激活函数、Affine仿射层类来实现简单神经网络
  2. 更改predict函数,调用Affine,Relu的forward完成前向计算
  3. 更改gradient梯度计算函数,利用反向传播来获取梯度

其他与上一篇博客一致,这里不多赘述。

init函数更改
    def __init__(self, input_layer_size, hidden_layer_size, output_layer_size, weight_std=0.01):
        self.params = {"W1": weight_std * np.random.randn(input_layer_size, hidden_layer_size),
                       "W2": weight_std * np.random.randn(hidden_layer_size, output_layer_size),
                       "b1": np.zeros(hidden_layer_size),
                       "b2": np.zeros(output_layer_size)}

        self.layers = OrderedDict()
        self.layers["Affine1"] = _Affine(self.params["W1"], self.params["b1"])
        self.layers["Relu"] = _Relu()
        self.layers["Affine2"] = _Affine(self.params["W2"], self.params["b2"])
        self.last_layer = _SoftmaxWithLoss()

这里用到了OrderedList,是为了保证其中的层与层之间保持顺序,这非常重要。
而最后一层为last_layer,即是SoftmaxWithLoss层。

predict函数更改
    def predict(self, X):
        y = X
        for layer in self.layers.values():
            y = layer.forward(y)
        return y
gradient函数更改
    def gradient(self, X, labels):
        # forward
        self.loss(X, labels)
        # backward
        back_y = self.last_layer.backward()
        # 更改为list格式,方便进行迭代与翻转
        layer_list = list(self.layers.values())
        #将层倒转,为了反向进行传递
        layer_list.reverse()
        for back_layer in layer_list:
            back_y = back_layer.backward(back_y)
		# 获取各个参数的梯度
        grads = {"W1": self.layers["Affine1"].dW,
                 "b1": self.layers["Affine1"].db,
                 "W2": self.layers["Affine2"].dW,
                 "b2": self.layers["Affine2"].db}

        return grads

完成更改后,我们就可以实现神经网络的训练与预测工作了!
我们依旧使用mnist库的数据,对其进行训练和预测,这里我把main函数内容贴一下(仍然是mini-batch思想):

(x_train, y_train), (x_test, y_test) = load_mnist(normalize=True, flatten=True, one_hot_label=True)
    train_acc_list = []
    test_acc_list = []
    net = TwoLayerNetwork(x_train.shape[1], 100, 10)
    # 抽取大小
    batch_size = 100
    # 学习率
    learning_rate = 0.05
    # 迭代次数
    iter_times = 20000
    # 计算batch需要迭代多少次才能基本完成训练集的抽取
    epoch = max(x_train.shape[0]/batch_size, 1)
    loss_list = list()

    for i in range(iter_times):
        x_batch_num = np.random.choice(x_train.shape[0], batch_size)
        x_batch = x_train[x_batch_num]
        y_batch = y_train[x_batch_num]

        net.gradient_descent(x_batch, y_batch)
        loss_list.append(net.last_layer.loss)

        if i % epoch == 0:
            train_acc = net.get_accuracy(x_train, y_train)
            test_acc = net.get_accuracy(x_test, y_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)
            print("训练集预测准确度:"+str(train_acc)+" 测试集预测准确度:"+str(test_acc))

    plt.figure(111)
    x_array = np.arange(iter_times)
    x_array_1 = np.arange(iter_times/epoch)
    plt.plot(x_array, np.array(loss_list))
    plt.figure(222)
    plt.plot(x_array_1, np.array(test_acc_list))
    plt.show()

测试了多组学习率,迭代20000次基本都保持在94.3%的测试集正确率,如果迭代更多次应该会更改准确,但是迭代次数过多可能会出现过拟合的现象,因此94%的正确率已经非常符合我们的预期了。
这里没有用sigmoid做激活函数的原因是由于sigmoid的计算量相对Relu比较大,因此选用简单的Relu激活函数,速度会快很多。
然后展示一下准确率上升曲线以及损失波动曲线:
准确率随训练轮数上升趋势
损失波动曲线
可以看到,损失波动还是比较大,20000次迭代应该还是不够的,可以更加精确。
迭代到40000次准确率达到了96%,而损失波动也降低到了0.3左右的水平(如下图),继续训练可能还会减小,但是多迭代了20000次只提高了2%的准确率,显然继续迭代是没有必要的了。
40000迭代损失波动

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值