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

本文转自
作者: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))
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容