深入理解神经网络中的反向传播过程

本文转自
作者:Charlotte77
出处:http://www.cnblogs.com/charlotte77/

最近几天在看深度学习的东西,对于神经网络之前了解过一点,但一直搞不懂具体,云里雾里的感觉,看了这个博主的文章终于弄懂了,讲得很清楚,细节都很到位(但是到自己会推理整个公式加理解整个代码也用了三天时间呢)同时参考https://blog.csdn.net/u014162133/article/details/81181194这个文章总算弄明白了整个多层感知机结构。

下面是参考上述两篇文章自己重新整理的内容


神经网络结构

image.png

两层结构的叫感知器,多层结构叫感知机,也称为神经网络。

上图是典型的三层神经网络的基本构成。LayerL1是输入层,LayerL3是输出层,中间LayerL2是隐含层(除了首尾的输入输出层,中间都是隐含层,所以隐含层有多个)

这里只做简单介绍,如果完全不懂神经网络结构,可以参考Poll写的笔记:[Mechine Learning & Algorithm] 神经网络基础

神经网络作用
神经网络的作用就是我们预先给它大量的数据(包含输入和输出)来进行训练,训练完成后,我们希望它对于将来的真实环境的输入也能给出一个令我们满意的输出。这里不知道怎么训练的先看下文,后面再做总结。

神经网络传播过程
假设,你有这样一个网络层:

image.png

第一层是输入层,包含两个神经元i1,i2,和截距项b1;第二层是隐含层,包含两个神经元h1,h2和截距项b2,第三层是输出o1,o2,每条线上标的wi是层与层之间连接的权重,激活函数我们默认为sigmoid函数。

这里为什么要加截距项,可以参考文章 神经网络中w,b参数的作用(为何需要偏置b的解释)
简单来说就是如果没有偏置b的话,所有的线性分割线都是经过原点的,但是现实问题并不会都是经过原点线性可分的,情况更复杂。

为什么要有激活函数,它表示神经元的输入和输出之间具有的某种函数关系,只有输入超过一定标准时才会产生输出。sigmoid函数是最常用的默认的激活函数。

现在对神经网络结构赋上初值,如下图:


image.png

 其中,

  • 输入数据 i1=0.05,i2=0.10;
  • 初始权重 w1=0.15,w2=0.20,w3=0.25,w4=0.30;w5=0.40,w6=0.45,w7=0.50,w8=0.55
  • 输出数据 o1=0.01,o2=0.99;

目标:给出输入数据i1,i2(0.05和0.10),使输出尽可能与原始输出o1,o2(0.01和0.99)接近。

前向传播

前向传播也可以叫作前馈或者正向传播,就是指给神经网络的输入一层一层向前计算输出,最终得到一个输出。(指向输出层的方向为向前)

1.输入层---->隐含层:
计算神经元h1的输入加权和:

image

神经元h1的输出o1:(此处用的激活函数为sigmoid函数):

image

同理,可计算出神经元h2的输出o2:

image

2.隐含层---->输出层:

计算输出层神经元o1和o2的值:

image
image

至此前向传播的过程就结束了,我们得到输出值为[0.75136079 , 0.772928465],与实际值[0.01 , 0.99]相差还很远。所以要尽可能地减少误差。

误差怎么算?
误差也就是神经网络中所称为的损失函数(或称代价函数、Loss函数)。

现假设神经网络输出的真实结果记为fi,期望结果记为yi。使用数学工具中的MAE(Mean Absolute Error,平均绝对误差)
image.png

或MSE(Mean Squared Error,均方误差)
image.png

还有其他的方法函数。Loss值越大,说明神经网络的输出结果越远离我们的期望,所以要尽可能地使Loss值越小。可以发现x(输入)是固定的,yi(期望结果)也是固定的,那么实际上影响Loss的只有w和b,所以最重要的任务也就是寻找w和b使得Loss最小。

而寻找合适的w和b就是最最重要的反向传播过程,现在我们对误差进行反向传播,更新权值w,重新计算输出。

反向传播

1.计算总误差

总误差:(square error)

image

但是有两个输出,所以分别计算o1和o2的误差,总误差为两者之和:

image
image
image

2.隐含层---->输出层的权值更新:

以权重参数w5为例,如果我们想知道w5对整体误差产生了多少影响,可以用整体误差对w5求偏导求出:(链式法则)

image

下面的图可以更直观的看清楚误差是怎样反向传播的:

image

现在我们来分别计算每个式子的值:

计算
image
image

计算
image.png
image

(这一步实际上就是对sigmoid函数求导,比较简单,可以自己推导一下)

计算
image
image

最后三者相乘:

image

这样我们就计算出整体误差E(total)对w5的偏导值。

回过头来再看看上面的公式,我们发现:

image

为了表达方便,用
image

来表示输出层的误差:

image

因此,整体误差E(total)对w5的偏导公式可以写成:

image

如果输出层误差计为负的话,也可以写成:

image

最后我们来更新w5的值:

image

(其中,
image

是学习速率,这里我们取0.5)

同理,可更新w6,w7,w8:

image.png

3.隐含层---->隐含层的权值更新:

方法其实与上面说的差不多,但是有个地方需要变一下,在上文计算总误差对w5的偏导时,是从out(o1)---->net(o1)---->w5,但是在隐含层之间的权值更新时,是out(h1)---->net(h1)---->w1,而out(h1)会接受E(o1)和E(o2)两个地方传来的误差,所以这个地方两个都要计算。

image

计算
image

image

先计算
image

image
image
image
image.png

同理,计算出:

image

两者相加得到总值:

image.png

再计算
image

image

再计算
image
image

最后,三者相乘:

image

为了简化公式,用sigma(h1)表示隐含层单元h1的误差:

image

最后,更新w1的权值:

image

同理,额可更新w2,w3,w4的权值:

image

至此误差反向传播法完成。然后我们再用更新的权值重新正向再反向计算,不停地迭代,得到使误差最小的w和b。

在这个例子中第一次迭代之后,总误差E(total)由0.298371109下降至0.291027924。迭代10000次后,总误差为0.000035085,输出为[0.015912196,0.984065734],而原输入为[0.01,0.99],证明效果还是不错的。

总结
基于上面的知识,我们现在可以总结出训练一个神经网络的全流程:

  • 初始化神经网络,对每个神经元的w和b赋予随机值;
  • 输入训练样本集合,对于每个样本,将输入给到神经网络的输入层,进行一次正向传播得到输出层各个神经元的输出值;
  • 求出输出层的误差,再通过反向传播算法,向后求出每一层(的每个神经元)的误差
  • 通过误差可以得出每个神经元的∂C/∂w、∂C/∂b,再乘上负的学习率(-η),就得到了Δw、Δb,将每个神经元的w和b更新为 w+Δw、b+Δb;

所谓的训练就是不断地重复上述过程,从而找到合适的参数w和b,使输入经计算后能在真实环境中得到理想的输出。

python实现代码:

import random
import math

#   参数解释:
#   "pd_":偏导的前缀
#   "d_":倒数的前缀
#   "w_ho":隐含层到输出层的权重系数索引
#   "w_ih":输入层到隐含层的权重系数索引

class Neuron:
    def __init__(self, bias):
        self.bias = bias
        self.weights = []

    # 前向传播:从前向后得到实际输出值

    # 计算输入层到隐含层或隐含层到输出层的net值
    def calculate_total_net_input(self):
        total = 0
        for i in range(len(self.inputs)):
            total += self.inputs[i] * self.weights[i]
        return total + self.bias

    # sigmoid函数 由net值变为out值
    def squash(self, total_net_input):
        return 1 / (1 + math.exp(- total_net_input))

    # 计算神经元的out值
    def calculate_output(self, inputs):
        self.inputs = inputs
        self.output = self.squash(self.calculate_total_net_input())
        return self.output

    # 反向传播:从后向前对误差进行反向传播,更新权重,重新计算输出

    # 每一个神经元的总误差是由平方差公式计算的
    def calculate_error(self, target_output):
        return 0.5 * (target_output - self.output) ** 2

    # ∂Etotal/∂out
    def calculate_pd_error_wrt_output(self, target_output):
        return -(target_output - self.output)

    # ∂out/∂net
    def calculate_pd_total_net_input_wrt_input(self):
        return self.output * (1 - self.output)

    # ∂net/∂wi
    def calculate_pd_total_net_input_wrt_weight(self, index):
        return self.inputs[index]

    # (∂Etotal/∂out) * (∂out/∂net)
    def calculate_pd_error_wrt_total_net_input(self, target_output):
        return self.calculate_pd_error_wrt_output(target_output) * self.calculate_pd_total_net_input_wrt_input()


class NeuronLayer:
    def __init__(self, num_neurons, bias):

        # 同一层的神经元共享一个截距项b
        self.bias = bias if bias else random.random()

        self.neurons = []
        for i in range(num_neurons):
            self.neurons.append(Neuron(self.bias))

    # 检查神经元、权重及偏置系数
    def inspect(self):
        print('Neurons:', len(self.neurons))
        for n in range(len(self.neurons)):
            print(' Neuron:', n)
            for w in range(len(self.neurons[n].weights)):
                print(' Weights:', self.neurons[n].weights[w])
            print(' Bias:', self.bias)

    # 前馈 计算每层各神经元的out值(一层一层向前计算输出,最终得到一个输出,这就是正向传播)
    def feed_forward(self, inputs):
        outputs = []
        for neuron in self.neurons:
            outputs.append(neuron.calculate_output(inputs))
        return outputs

    # # 
    # def get_outputs(self):
    #     outputs = []
    #     for neuron in self.neurons:
    #         outputs.append(neuron.output)
    #     return outputs


class NeuralNetwork:
    LEARNING_RATE = 0.5

    def __init__(self, num_inputs, num_hidden, num_outputs, hidden_layer_weights=None, hidden_layer_bias=None, output_layer_weights=None, output_layer_bias=None):
        self.num_inputs = num_inputs

        self.hidden_layer = NeuronLayer(num_hidden, hidden_layer_bias)
        self.output_layer = NeuronLayer(num_outputs, output_layer_bias)

        self.init_weights_from_inputs_to_hidden_layer_neurons(hidden_layer_weights)
        self.init_weights_from_hidden_layer_neurons_to_output_layer_neurons(output_layer_weights)

    # 初始化及更新输入层到隐含层的权重值
    def init_weights_from_inputs_to_hidden_layer_neurons(self, hidden_layer_weights):
        weight_num = 0
        for h in range(len(self.hidden_layer.neurons)):
            for i in range(self.num_inputs):
                if not hidden_layer_weights:
                    self.hidden_layer.neurons[h].weights.append(random.random())
                else:
                    self.hidden_layer.neurons[h].weights.append(hidden_layer_weights[weight_num])
                weight_num += 1

    # 初始化及更新隐含层到输出层的权重值
    def init_weights_from_hidden_layer_neurons_to_output_layer_neurons(self, output_layer_weights):
        weight_num = 0
        for o in range(len(self.output_layer.neurons)):
            for h in range(len(self.hidden_layer.neurons)):
                if not output_layer_weights:
                    self.output_layer.neurons[o].weights.append(random.random())
                else:
                    self.output_layer.neurons[o].weights.append(output_layer_weights[weight_num])
                weight_num += 1

    # 检查各系数
    def inspect(self):
        print("---------------------")
        print(' * Inputs:{}'.format(self.num_inputs))
        print("---------------------")
        print("Hidden Layer")
        self.hidden_layer.inspect()
        print("---------------------")
        self.output_layer.inspect()
        print("---------------------")

    # 前馈 更新输出层的out值
    def feed_forward(self, inputs):
        hidden_layer_outputs = self.hidden_layer.feed_forward(inputs)
        return self.output_layer.feed_forward(hidden_layer_outputs)

    # 训练
    def train(self, training_inputs, training_outputs):
        self.feed_forward(training_inputs)

        # 1.输出神经元的值
        pd_errors_wrt_output_neuron_total_net_input = [0] * len(self.output_layer.neurons)
        for o in range(len(self.output_layer.neurons)):

            # (∂Etotal/∂out) * (∂out/∂net)
            pd_errors_wrt_output_neuron_total_net_input[o] = self.output_layer.neurons[o].calculate_pd_error_wrt_total_net_input(training_outputs[o])


        # 2.隐含神经元的值
        pd_errors_wrt_hidden_neuron_total_net_input = [0] * len(self.hidden_layer.neurons)
        for h in range(len(self.hidden_layer.neurons)):

            # dE/dyⱼ = Σ ∂E/∂zⱼ * ∂z/∂yⱼ = Σ ∂E/∂zⱼ * wᵢⱼ
            d_error_wrt_hidden_neuron_output = 0
            for o in range(len(self.output_layer.neurons)):
                # 在隐含层之间的权值更新时,out(h1)会接受E(o1)和E(o2)两个地方传来的误差 ∂Etotal = ∂Eo1 + ∂Eo2     ∂Eo1/∂outh1=(∂o1/∂neto1)*(∂neto1/∂outh1)=(∂o1/∂outo1)*(∂outo1/∂neto1)*(∂neto1/∂outh1)=(∂Etotal/∂neto1)*w5
                d_error_wrt_hidden_neuron_output += pd_errors_wrt_output_neuron_total_net_input[o] * self.output_layer.neurons[o].weights[h]

            pd_errors_wrt_hidden_neuron_total_net_input[h] = d_error_wrt_hidden_neuron_output * self.hidden_layer.neurons[h].calculate_pd_total_net_input_wrt_input()

        # 3.更新输出层权重系数
        for o in range(len(self.output_layer.neurons)):
            for w_ho in range(len(self.output_layer.neurons[o].weights)):
                # ∂Etotal/∂wi
                pd_error_wrt_weight = pd_errors_wrt_output_neuron_total_net_input[o] * self.output_layer.neurons[
                    o].calculate_pd_total_net_input_wrt_weight(w_ho)

                # wi = wi - α * ∂Eⱼ/∂wᵢ
                self.output_layer.neurons[o].weights[w_ho] -= self.LEARNING_RATE * pd_error_wrt_weight


        # 4.更新隐含层的权重系数
        for h in range(len(self.hidden_layer.neurons)):
            for w_ih in range(len(self.hidden_layer.neurons[h].weights)):

                # ∂Etotal/∂wi
                pd_error_wrt_weight = pd_errors_wrt_hidden_neuron_total_net_input[h] * self.hidden_layer.neurons[h].calculate_pd_total_net_input_wrt_weight(w_ih)

                # wi = wi - α * ∂Eⱼ/∂wᵢ
                self.hidden_layer.neurons[h].weights[w_ih] -= self.LEARNING_RATE * pd_error_wrt_weight


    # 计算更新后的总误差
    def calculate_total_error(self, training_sets):
        total_error = 0
        for t in range(len(training_sets)):
            training_inputs, training_outputs = training_sets[t]
            self.feed_forward(training_inputs)
            for o in range(len(training_outputs)):
                total_error += self.output_layer.neurons[o].calculate_error(training_outputs[o])
        return total_error

# 实例化
# 各层神经元系数、权重及偏置
nn = NeuralNetwork(2, 2, 2, hidden_layer_weights=[0.15, 0.2, 0.25, 0.3], hidden_layer_bias=0.35, output_layer_weights=[0.4, 0.45, 0.5, 0.55], output_layer_bias=0.6)
# 训练10000次
for i in range(10000):
    # 输入层值、原始输出层值
    nn.train([0.05, 0.1], [0.01, 0.09])
    # round()函数取四舍五入,round(n,9)中9代表保留几位小数
    print(i, round(nn.calculate_total_error([[[0.05, 0.1], [0.01, 0.09]]]), 9))

你可能感兴趣的:(深入理解神经网络中的反向传播过程)