深度学习系列二:反向传播算法解析与简单示例

概述

上一章主要描述了线性回归与逻辑回归的概念与区别,在模型优化方面带过了,本篇着重记录下深度学习中常用来作参数优化的反向传播算法,并结合一个简单的例子阐明。
首先需要明确的是,任何一个神经网络都可以看作是一个关于其输入的函数,这个函数接受一系列值作为其输入,并输出一个或者多个结果,如下:
( y 0 , y 1 . . . y m ) = f ( x 1 , x 2 . . . x n ) (y_0,y_1...y_m)=f(x_1,x_2...x_n) (y0,y1...ym)=f(x1,x2...xn)
简化为向量形式:
Y = f ( X ) Y=f(X) Y=f(X)
其中m,n均可以为1或者任意值,此外,此函数中还包含一系列参数用于调整该模型的性能,因此该函数可以写成如下形式:
( y 0 , y 1 . . . y m ) = f w 1 , w 2 . . . w k ( x 1 , x 2 . . . x n ) (y_0,y_1...y_m)=f_{w_1,w_2...w_k}(x_1,x_2...x_n) (y0,y1...ym)=fw1,w2...wk(x1,x2...xn)
简化形式:
Y = f ( X , W ) Y=f(X,W) Y=f(X,W)
例如常用的线性模型:
Y = W X + B Y=WX+B Y=WX+B
因此,可以认为任意网络都仅由三个要素组成:输入、输出、参数。一般而言,输入是采集到的数据例如图像、语音等等,转化为数据作为输入,经过网络中与网络参数一系列的计算得到输出,显然一个未经训练的网络输出只是一系列没有规律的值,而网络训练的过程,实际就是采集一系列已知的输入和输出真值,将输入喂入网络,生成一个输出,并将网络的输出与采集的输出真值对比,以其差值在网络中进行反向传播来优化网络参数的过程。

实现

  • 损失函数
    损失函数的作用就是衡量网络输出与输出真值的差距,可以认为,当该差距越小时,网络输出与输出真值越接近,也越能够反应该组数据的规律,不同的网络会因为各种原因选用不同的损失函数,这里不做深入探究,损失函数可以简化如下:
    l o s s = C ( f w 1 , w 2 . . . w k ( x 1 , x 2 . . . x n ) , y t r u t h ) loss=C(f_{w_1,w_2...w_k}(x_1,x_2...x_n),y_{truth}) loss=C(fw1,w2...wk(x1,x2...xn),ytruth)
    简化:
    l o s s = C ( f ( X , W ) , Y t r u t h ) loss=C(f(X,W),Y_{truth}) loss=C(f(X,W),Ytruth)
  • 梯度下降
    因此可以认为网络训练的过程就是调整参数使loss不断趋于极小值的过程,有许多算法可以实现,这里主要介绍下常用的梯度下降法,其细节也有很多文章讲解,这里也不多作介绍,对于以上loss函数,在训练过程中,由于输入 x 1 , x 2 . . . x n x_1,x_2...x_n x1,x2...xn y t r u t h y_{truth} ytruth为提前采集到的数据,可以认为是已知值,将参数 w 1 , w 2 . . . w k w_1,w_2...w_k w1,w2...wk视为自变量,loss为因变量,其梯度如下:
    ( ∂ C ∂ w 1 , ∂ C ∂ w 2 , . . . ∂ C ∂ w k ) (\frac{\partial C}{\partial w_1},\frac{\partial C}{\partial w_2},...\frac{\partial C}{\partial w_k}) (w1C,w2C,...wkC)
    加入学习率如下:
    ( Δ w 1 , Δ w 2 . . . Δ w k , ) = η ( ∂ C ∂ w 1 , ∂ C ∂ w 2 , . . . ∂ C ∂ w k ) (\Delta w_1,\Delta w_2...\Delta w_k,)=\eta(\frac{\partial C}{\partial w_1},\frac{\partial C}{\partial w_2},...\frac{\partial C}{\partial w_k}) (Δw1,Δw2...Δwk,)=η(w1C,w2C,...wkC)
    因此,参数优化如下:
    w 1 ′ = w 1 − Δ w 1 w 2 ′ = w 2 − Δ w 2 . . . w k ′ = w 1 − Δ w k w'_1=w_1-\Delta w_1\\ w'_2=w_2-\Delta w_2\\ ...\\ w'_k=w_1-\Delta w_k\\ w1=w1Δw1w2=w2Δw2...wk=w1Δwk

简单示例与代码

以一个简单逻辑回归为例,其网络结构如下:

w011
w012
w022
w021
w111
w121
sigmoid
z01
z11
z12
z02
z21
a

表达式如下:
Z 1 = W 1 X 1 + B 1 Z 2 = W 2 Z 1 + B 2 Z^1=W_1X_1+B_1\\ Z^2=W_2Z^1+B_2 Z1=W1X1+B1Z2=W2Z1+B2
其激活函数为sigmoid:
σ ( z ) = 1 1 + e − z \sigma(z)=\frac{1}{1+e^{-z}} σ(z)=1+ez1
a = σ ( z 1 2 ) a=\sigma(z_1^2) a=σ(z12)
使用交叉熵作为损失函数:
C = y l a b e l l o g ( a ) + ( 1 − y l a b e l ) l o g ( 1 − a ) C=y_{label}log(a)+(1-y_{label})log(1-a) C=ylabellog(a)+(1ylabel)log(1a)
于是有:
∂ C ∂ a = y l a b e l a − 1 − y l a b e l 1 − a   ∂ a ∂ z 1 2 = a ( 1 − a )   ∂ z j l ∂ w i j ( l − 1 ) = z i ( l − 1 )   ∂ z j l ∂ b j ( l − 1 ) = 1 \frac{\partial C}{\partial a}=\frac{y_{label}}{a}-\frac{1-y_{label}}{1-a}\\ \ \\ \frac{\partial a}{\partial z_1^2}=a(1-a)\\ \ \\ \frac{\partial z_j^l}{\partial w^{(l-1)}_{ij}}=z^{(l-1)}_i\\ \ \\ \frac{\partial z_j^l}{\partial b^{(l-1)}_j}=1\\ aC=aylabel1a1ylabel z12a=a(1a) wij(l1)zjl=zi(l1) bj(l1)zjl=1
以第1层参数更新为例:
∂ C ∂ w 11 1 = ∂ C ∂ a ∗ ∂ a ∂ z 1 2 ∗ ∂ z 1 2 ∂ w 11 1 = ∂ C ∂ z 1 2 ∗ z 1 1   ∂ C ∂ b 1 1 = ∂ C ∂ a ∗ ∂ a ∂ z 1 2 ∗ ∂ z 1 2 ∂ b 1 1 = ∂ C ∂ z 1 2 ∗ 1 \frac{\partial C}{\partial w_{11}^1}=\frac{\partial C}{\partial a}*\frac{\partial a}{\partial z_1^2}*\frac{\partial z_1^2}{\partial w^1_{11}}=\frac{\partial C}{\partial z_1^2}*z^1_1\\ \ \\ \frac{\partial C}{\partial b^{1}_1}=\frac{\partial C}{\partial a}*\frac{\partial a}{\partial z_1^2}*\frac{\partial z_1^2}{\partial b^{1}_1}=\frac{\partial C}{\partial z_1^2}*1 w111C=aCz12aw111z12=z12Cz11 b11C=aCz12ab11z12=z12C1
写成矩阵形式,令 δ l \delta^l δl为C对l层z偏导的向量,即 δ l = ∂ C ∂ z l \delta^l=\frac{\partial C}{\partial z^l} δl=zlC,于是可以推导得到以下公式:
δ l = W T δ l + 1   ∂ C ∂ w l = δ l + 1 ∗ Z l T   ∂ C ∂ w l = δ l + 1 \delta^l=W^T\delta^{l+1}\\ \ \\ \frac{\partial C}{\partial w^l}=\delta^{l+1}*Z^{lT}\\ \ \\ \frac{\partial C}{\partial w^l}=\delta^{l+1} δl=WTδl+1 wlC=δl+1ZlT wlC=δl+1
由以上两个公式可以得知,在反向传播过程中,可以将 δ l + 1 \delta^{l+1} δl+1视为第 l l l层的输入, δ l \delta^{l} δl为输出并作为 l − 1 l-1 l1层的输入继续传播, δ l + 1 \delta^{l+1} δl+1结合前向传播中得到的 z l z^l zl的转置可以得到损失函数对于该层权重和偏移参数的偏导数,以此来更新该层的参数。
代码如下:

import numpy as np
from abc import ABC, abstractmethod

sigmoid = np.vectorize(lambda z: 1 / (1 + np.exp(-z)))


class Layer(ABC):
    @abstractmethod
    def front_pro(self, inputs):
        pass

    @abstractmethod
    def back_pro(self, output_der):
        pass

    @abstractmethod
    def update(self, length, eta):
        pass


class DenseLayer(Layer):
    def __init__(self, *shape):
        self._weights = 0.1 * np.random.randn(*shape)
        self._bias = np.random.randn(shape[0], 1)
        self._temp_weight_delta = np.zeros_like(self._weights)
        self._temp_bias_delta = np.zeros_like(self._bias)
        self._input = np.array((shape[-1], 1))
        self._output = np.array((shape[0], 1))

    def front_pro(self, inputs):
        self._input = inputs
        # print(self._weights.shape)
        # print(np.array(inputs).shape)
        self._output = np.dot(self._weights, inputs) + self._bias
        # self._output = np.swapaxes(self._output, 1, 0)
        # print(self._output.shape)
        return self._output

    def back_pro(self, output_der):
        weight_delta = np.dot(output_der, self._input.transpose())
        self._temp_weight_delta += weight_delta
        bias_delta = output_der
        self._temp_bias_delta += bias_delta
        return np.dot(self._weights.transpose(), output_der)

    def update(self, length, eta):
        self._weights += self._temp_weight_delta / length * eta
        self._bias += self._temp_bias_delta / length * eta
        self._temp_weight_delta = np.zeros_like(self._weights)
        self._temp_bias_delta = np.zeros_like(self._bias)


class SigmoidLayer(Layer):
    def __init__(self, last_layer=False):
        self._output = np.array([])
        self._last_layer = last_layer

    def front_pro(self, inputs):
        self._output = sigmoid(inputs)
        return self._output

    def back_pro(self, output_der):
        if self._last_layer:
            return output_der
        return output_der * self._output * (1 - self._output)

    def update(self, length, eta):
        pass


class Network(object):
    # loss func: y*log(a)+(1-y)*log(1-a)

    def __init__(self):
        self._z = np.ndarray((1,))
        self._output = np.ndarray((1,))
        self._weights = np.ndarray((1,))
        self._bias = np.ndarray((1,))
        self._layers = []

    def add_layer(self, layer):
        self._layers.append(layer)

    def front_pro(self, inputs):
        output = []
        for input in inputs:
            for layer in self._layers:
                input = layer.front_pro(input)
            output.append(input)
        return output

    def back_pro(self, output_der):
        for layer in reversed(self._layers):
            output_der = layer.back_pro(output_der)

    def cross_entropy_der(self, output, label):
        return label / output - (1 - label) / (1 - output)

    def train(self, data_input, data_label, eta):
        for x, y in zip(data_input, data_label):
            x = np.expand_dims(x, 0)
            output = self.front_pro(x)
            output = output[0]
            # output_der = self.cross_entropy_der(output, y)
            output_der = y - output
            self.back_pro(output_der)
        for layer in self._layers:
            layer.update(len(data_input), eta)


def main():
    network = Network()
    network.add_layer(DenseLayer(2, 2))
    network.add_layer(DenseLayer(1, 2))
    network.add_layer(SigmoidLayer(last_layer=True))
    input_generator = lambda x: 5 * x + 20
    inputs = []
    for i in range(10):
        if i < 5:
            inputs.append(np.array([[i], [input_generator(i) + 20]]))
        else:
            inputs.append(np.array([[i], [input_generator(i) - 20]]))
    y = np.array([1] * 5 + [0] * 5).reshape((10, 1))

    print(np.array(network.front_pro(inputs)).reshape(10,))
    for j in range(60):
        for i in range(5):
            network.train(inputs[i:i + 2], y[i:i + 2], 0.05)

    print(np.array(network.front_pro(inputs)).reshape(10,))


if __name__ == '__main__':
    main()

以上仅使用了10组简单的数据来训练,如下:
[[ 0 40] [ 1 45] [ 2 50] [ 3 55] [ 4 60] [ 5 25] [ 6 30] [ 7 35] [ 8 40] [ 9 45]]
对应label如下:
[1 1 1 1 1 0 0 0 0 0]

训练前网络输出结果:
[0.70424612 0.7159282 0.7273275 0.73843649 0.74924892 0.66834236
0.68079983 0.6930043 0.70494438 0.71660998]

训练后:
[0.99848587 0.99706174 0.99430577 0.98899335 0.97883024 0.03070642
0.01604001 0.00831864 0.00429797 0.00221627]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值