• 反向传播(BP)算法理解以及Python实现


    全文参考《机器学习》-周志华中的5.3节-误差逆传播算法;整体思路一致,叙述方式有所不同;

    使用如上图所示的三层网络来讲述反向传播算法;

    首先需要明确一些概念,

    假设数据集(X={x^1, x^2, cdots, x^n}, Y={y^i, y^2, cdots, y^n}),反向传播算法使用数据集中的每一个样本执行前向传播,之后根据网络的输出与真实标签计算误差,利用误差进行反向传播,更新权重;

    使用一个样本((x, y)),其中(x=(x_1, x_2, cdots, x_d))

    输入层:

      有(d)个输入结点,对应着样本(x)(d)维特征,(x_i)表示输入层的第(i)个结点;

    隐藏层:

      有(q)个结点,(b_h)表示隐藏层的第(h)个结点;

    输出层:
      有(l)个输出结点,(y_j)表示输出层的第(j)个结点;

    权重矩阵:

      两个权重矩阵(V, W),分别是位于输入层和隐藏层之间的(Vin R^{d imes q}),其中(v_{ih})表示连接结点(x_i)与结点(b_h)之间的权重;以及位于隐藏层与输出层之间的(Win R^{q imes l}),其中(w_{hj})表示连接结点(b_h)与结点(y_j)的权重;

    激活函数:

      激活函数使用sigmoid函数;

    [f(x) = dfrac{1}{1+e^{-x}} ]

    其导数为:

    [f'(x) = f(x)(1-f(x)) ]

    其他:

      在隐藏层,结点(b_h)在执行激活函数前为(alpha_h),即隐藏层的输入;所以有:

    [alpha_h = sum_{i=1}^{d}v_{ih}x_i ]

    之后经过sigmoid函数:

    [b_h = sigmoid(alpha_h) ]

      在输出层,结点(y_j)在执行激活函数前为(eta_j),即输出层的输入;所以有:

    [eta_j = sum_{h=1}^{q}w_{hj}b_h ]

    之后经过sigmoid函数:

    [hat{y}_j = sigmoid(eta_j) ]

    前向传播

      所以,根据上面一系列的定义,前向传播的过程为:由输入层的结点((x_1, x_2, cdots, x_i, cdots, x_d)),利用权重矩阵(V)计算得到((alpha_1, alpha_2, cdots, alpha_h, cdots, alpha_q)),经过激活函数sigmoid得到((b_1, b_2, cdots, b_h, cdots, b_q)),这就得到了隐藏层的输出;之后,利用权重矩阵(W)计算得到((eta_1, eta_2, cdots, eta_j, cdots, eta_l)),经过激活函数sigmoid得到((hat{y}_1,hat{y}_1, cdots, hat{y}_j , cdots, hat{y}_l )),也就是最后的输出;

    步骤:

    **Step 1: **输入层(x in R^{1 imes d}),计算隐藏层输出(b = sigmoid(x imes V), quad bin R^{1 imes q})

    **Step 2: ** 输出层输出(hat{y} = sigmoid(b imes W), quad hat{y}in R^{1 imes l})

      注意,在前向传播的过程中,记录每一层的输出,为反向传播做准备,因此,需要保存的是(x, b, hat{y})

    前向传播还是比较简单的,下面来看反向传播吧;

    反向传播

      想一下为什么要有反向传播过程呢?其实目的就是为了更新我们网络中的参数,也就是上面我们所说的两个权重矩阵(V, W),那么如何来更新呢?

    《机器学习》周志华

    BP是一个迭代算法,在迭代的每一轮中采用广义的感知机学习规则对参数进行更新估计,任意参数v的更新估计式为:

    [v leftarrow v + Delta v ]

    BP算法基于梯度下降策略,以目标的负梯度方向对参数进行调整;

    我们如何来更新参数呢?也就是如何更新(V, W)这两个权重矩阵;以(W)中的某个参数(w_{hj})举例,更新它的方式如下:

    [w_{hj} leftarrow w_{hj} + Delta w_{hj} ]

    那么,如何计算(Delta w_{hj})的呢?计算如下:

    [Delta w_{hj} = -eta dfrac{partial{E}}{partial{w_{hj}}} ]

    其中,(E_k)表示误差,也就是网络的输出(hat{y})与真实标签(y)的均方误差;(eta)表示学习率;负号则表示沿着负梯度方向更新;

    [E = dfrac{1}{2}sum_{j=1}^{l}(hat{y}_j - y_j)^2 ]

    也就是说,我们想要对哪一个参数进行更新,则需要计算当前网络输出与真实标签的均方误差对该参数的偏导数,即(dfrac{partial{E}}{partial{w_{hj}}}),之后再利用学习率进行更新;

    在这个三层的网络结构中,有两个权重矩阵(V, W),我们该如何更新其中的每一个参数呢?

    就以权重矩阵(W)中的参数(w_{hj})来进行下面的解释,

    那么根据上面所叙述的,更新(w_{hj})得方式为:

    [w_{hj} leftarrow w_{hj} + Delta w_{hj} ]

    [Delta w_{hj} = -eta dfrac{partial{E}}{partial{w_{hj}}} ]

    那么如何来计算(dfrac{partial{E}}{partial{w_{hj}}})呢?

    这里就需要用到链式法则了,如果不熟悉的,建议查找再学习一下;

    [f(x) = g(x)+x ]

    [g(x) = h(x) + x^2 ]

    [h(x) = x^3 + 1 ]

    想一下是怎么求(dfrac{df(x)}{dx})的;

    如果对上文中讲述的网络结构,能够将其完整的呈现的在脑海中的话,对于下面的推导应该不会很困难。

    再回顾一遍前向传播:

    所以,根据上面一系列的定义,前向传播的过程为:由输入层的结点((x_1, x_2, cdots, x_i, cdots, x_d)),利用权重矩阵(V)计算得到((alpha_1, alpha_2, cdots, alpha_h, cdots, alpha_q)),经过激活函数sigmoid得到((b_1, b_2, cdots, b_h, cdots, b_q)),这就得到了隐藏层的输出;之后,利用权重矩阵(W)计算得到((eta_1, eta_2, cdots, eta_j, cdots, eta_l)),经过激活函数sigmoid得到((hat{y}_1,hat{y}_1, cdots, hat{y}_j , cdots, hat{y}_l )),也就是最后的输出;



    那么如何来计算(dfrac{partial{E}}{partial{w_{hj}}})呢?

    我们想一下在网络的均方误差(E)与参数(w_{hj})之间有哪些过程,也就是说需要想明白参数(w_{hj})是怎么对误差(E)产生影响的;

    (w_hj)是连接隐藏层结点(b_h)与输出层结点(hat{y}_j)的权重,因此过程是:(b_h ightarrow eta_j ightarrow hat{y}_j ightarrow E)

    那么根据链式法则就可以有:

    [dfrac{partial E}{partial w_{hj}} = dfrac{partial E}{partial hat{y}_j}dfrac{partial hat{y}_j}{partial eta_j} dfrac{partial eta_j}{partial w_{hj}} ]

    分别来求解(dfrac{partial E}{partial hat{y}_j}), (dfrac{partial hat{y}_j}{partial eta_j}), $ dfrac{partial eta_j}{partial w_{hj}}$这三项;

    (1)第一项:(dfrac{partial E}{partial hat{y}_j})

    想一下(E)(hat{y}_j)之间有什么关系,即:

    [E = dfrac{1}{2}sum_{j=1}^{l}(hat{y}_j-y_j)^2 = dfrac{1}{2}[(hat{y}_1-y_1)^2+cdots +(hat{y}_j-y_j)^2+cdots+(hat{y}_l-y_l)^2] ]

    那么,(E_k)(hat{y}_j)求偏导:

    [dfrac{partial E}{partial hat{y}_j} = -(hat{y}_j-y_j) ]

    (2)第二项:(dfrac{partial hat{y}_j}{partial eta_j})

    再想一下(hat{y}_j)(eta_j)之间有什么关系呢,即

    [hat{y}_j = sigmoid(eta_j) ]

    那么,(hat{y}_j)(eta_j)求偏导,即:

    [dfrac{partial hat{y}_j}{partial eta_j} = hat{y}_j(1-hat{y}_j) ]

    (3)第三项:$ dfrac{partial eta_j}{partial w_{hj}}$

    再想一下(eta_j)(w_{hj})之间又有什么关系呢,即:

    [eta_j = sum_{h=1}^{q}w_{hj}b_h= w_{1j}b_1 + cdots+w_{hj}b_h+cdots+w_{qj}b_q ]

    所以从上式中能够看清(eta_j)(w_{hj})之间的关系了吧,其实再想一下,(eta_j)是输出层的第(j)个结点,而(w_{hj})是连接隐藏层结点(b_h)与结点(eta_j)的权重;

    那么(eta_j)(w_{hj})的偏导数,即:

    [dfrac{partial eta_j}{partial w_{hj}} = b_h ]

    上面三个偏导数都求出来了,那么就有:

    [dfrac{partial E}{partial w_{hj}} = dfrac{partial E}{partial hat{y}_j}dfrac{partial hat{y}_j}{partial eta_j} dfrac{partial eta_j}{partial w_{hj}} =-(hat{y}_j-y_j)hat{y}_j(1-hat{y}_j)b_h ]

    那么更新参数(w_{hj})

    [w_{hj} leftarrow w_{hj}+Delta w_{hj} ]

    [Delta w_{hj} = -eta dfrac{partial E}{partial w_{hj}} = -eta(-(hat{y}_j-y_j)hat{y}_j(1-hat{y}_j)b_h) ]

    即:

    [w_{hj} = w_{hj}+Delta w_{hj} = w_{hj} -eta(-(hat{y}_j-y_j)hat{y}_j(1-hat{y}_j)b_h) ]

    从上式可以看出,想要对参数(w_{hj})进行更新,我们需要知道上一次更新后的参数值,输出层的第(j)个结点(hat{y}_j),以及隐藏层的第(h)个结点(b_h)其实想一下,也就是需要知道参数(w_{hj})连接的两个结点对应的输出;那么这里就提醒我们一点,在网络前向传播的时候需要记录每一层网络的输出,即经过sigmoid函数之后的结果;



    现在我们知道如何对权重矩阵(W)中的每一个参数(w_{hj})进行更新,那么如何对权重矩阵(V)中的参数(v_{ih})进行更新呢?其中,(v_{ih})是连接输入层结点(x_i)与隐藏层结点(b_h)之间的权重;

    同样是利用网络的输出误差(E_k)对参数(v_{ih})的偏导,即:

    [v_{ih} leftarrow v_{ih} + Delta v_{ih} ]

    [Delta v_{ih} = -eta dfrac{partial{E}}{partial{v_{ih}}} ]

    那么如何来计算(dfrac{partial{E}}{partial{v_{ih}}})呢?想一下是(E)(v_{ih})之间有什么关系,过程为:

    [v_{ih} ightarrow alpha_h ightarrow b_h ightarrow eta ightarrow hat{y} ightarrow E ]

    同样是利用链式求导法则,有:

    [dfrac{partial{E}}{partial{v_{ih}}} = dfrac{partial E}{partial b_h}dfrac{partial b_h}{partial alpha_h}dfrac{partial alpha_h}{partial v_{ih}} ]

    同样地,分别来求解(dfrac{partial E}{partial b_h}),(dfrac{partial b_h}{partial alpha_h}),(dfrac{partial alpha_h}{partial v_{ih}})这三项;

    (1)第一项:(dfrac{partial E}{partial b_h})

    与上述思路相同,想一下(E_k)(b_h)之间的关系,又可以分解为:

    [dfrac{partial E}{partial b_h} =sum_{j=1}^{l}dfrac{partial E}{partial eta_j} dfrac{partial eta_j}{partial b_h} ]

    其中,

    [dfrac{partial E}{partial eta_j} = dfrac{partial E}{partial hat{y}_j}dfrac{partial hat{y}_j}{partial eta_j} = -(hat{y}_j-y_j)hat{y}_j(1-hat{y}_j) ]

    另外,(dfrac{partial eta_j}{partial b_h}),想一下(eta_j)(b_h)的关系:

    [dfrac{partial eta_j}{partial b_h} = w_{hj} ]

    所以,就有:

    [dfrac{partial E}{partial b_h} =sum_{j=1}^{l}dfrac{partial E}{partial eta_j} dfrac{partial eta_j}{partial b_h} = sum_{j=1}^{l}-(hat{y}_j-y_j)hat{y}_j(1-hat{y}_j) w_{hj} ]

    (2)第二项:(dfrac{partial b_h}{partial alpha_h})

    同样地,(b_h)(alpha_h)之间的关系,有:

    [b_h = sigmoid(alpha_h) ]

    那么有:

    [dfrac{partial b_h}{partial alpha_h} = b_h (1-b_h) ]

    (3)第三项:(dfrac{partial alpha_h}{partial v_{ih}})

    同样地,(alpha_h)(v_{ih})之间的关系,有:

    [alpha_h = sum_{i=1}^{d}v_{ih}x_i= v_{1h}x_1 + cdots+v_{ih}x_i+cdots+v_{dh}x_d ]

    因此,(alpha_h)(v_{ih})的偏导数为:

    [dfrac{partial alpha_h}{partial v_{ih}} = x_i ]

    综合上面三项,有:

    [dfrac{partial{E}}{partial{v_{ih}}} = dfrac{partial E}{partial b_h}dfrac{partial b_h}{partial alpha_h}dfrac{partial alpha_h}{partial v_{ih}} = sum_{j=1}^{l}-(hat{y}_j-y_j)hat{y}_j(1-hat{y}_j) w_{hj} b_h (1-b_h) x_i ]



    我们来对比一下(dfrac{partial{E}}{partial{v_{ih}}})(dfrac{partial E}{partial w_{hj}}),两者分别为:

    [dfrac{partial E}{partial w_{hj}} =-(hat{y}_j-y_j)hat{y}_j(1-hat{y}_j)b_h ]

    [dfrac{partial{E}}{partial{v_{ih}}} = sum_{j=1}^{l}-(hat{y}_j-y_j)hat{y}_j(1-hat{y}_j) w_{hj} b_h (1-b_h) x_i ]

    稍微换一种形式,将负号放进去:

    [dfrac{partial E}{partial w_{hj}} =(y_j- hat{y}_j)hat{y}_j(1-hat{y}_j)b_h ]

    [dfrac{partial{E}}{partial{v_{ih}}} = sum_{j=1}^{l}(y_j - hat{y}_j)hat{y}_j(1-hat{y}_j) w_{hj} b_h (1-b_h) x_i ]

    这里我们是对单个参数(w_{hj}, v_{ih})进行更新,如何对(W, V)整体进行更新呢?

    我们再明确一下几个定义:
    (x)表示输入层的输出, (xin R^{1 imes d })

    (b)表示隐藏层的输出,(bin R^{1 imes q })

    (hat{y})表示输出层的输出,(hat{y}in R^{1 imes l})

    (sigmoid\_deriv())表示(sigmoid)的导数,(sigmoid\_deriv(hat{y}) = hat{y}(1-hat{y}))

    将输出层的输出与ground-truth之间的差值记为:(eroor = y-hat{y})

    可以得到

    [dfrac{partial E}{partial W} = b' cdot error cdot sigmoid\_deriv(hat{y}) ]

    [dfrac{partial E}{partial V}= x' cdot error cdot sigmoid\_deriv(hat{y}) cdot W' cdot sigmoid\_deriv(b) ]

    在反向传播的过程中,我们记:

    [D[0] =error cdot sigmoid\_deriv(hat{y}) ]

    [D[1]= error cdot sigmoid\_deriv(hat{y}) cdot W' cdot sigmoid\_deriv(b) ]

    当将每一个权重矩阵的(D[?])计算出来,得到一个列表后,再对所有的权重矩阵进行更新;之所以这样做,是为方便代码实现;

    Python实现前向传播与反向传播

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # @Time : 19-5-7
    
    """
    get started implementing backpropagation.
    """
    
    __author__ = 'Zhen Chen'
    
    # import the necessaty packages
    import numpy as np
    
    
    class NeuralNetwork:
        def __init__(self, layers, alpha=0.1):
            # 初始化权重矩阵、层数、学习率
            # 例如:layers=[2, 3, 2],表示输入层两个结点,隐藏层3个结点,输出层2个结点
            self.W = []
            self.layers = layers
            self.alpha = alpha
    
    		# 随机初始化权重矩阵,如果三层网络,则有两个权重矩阵;
            # 在初始化的时候,对每一层的结点数加1,用于初始化训练偏置的权重;
            # 由于输出层不需要增加结点,因此最后一个权重矩阵需要单独初始化;
            for i in np.arange(0, len(layers)-2):
                w = np.random.randn(layers[i] + 1, layers[i + 1] + 1)
                self.W.append(w / np.sqrt(layers[i]))
    
            # 初始化最后一个权重矩阵
            w = np.random.randn(layers[-2] + 1, layers[-1])
            self.W.append(w / np.sqrt(layers[-2]))
    
        def __repr__(self):
            # 输出网络结构
            return "NeuralNetwork: {}".format(
                "-".join(str(l) for l in self.layers)
            )
    
        def sigmoid(self, x):
            # sigmoid激活函数
            return 1.0 / (1 + np.exp(-x))
    
        def sigmoid_deriv(self, x):
            # sigmoid的导数
            return x * (1 - x)
    
        def fit(self, X, y, epochs=1000, display=100):
            # 训练网络
            # 对训练数据添加一维值为1的特征,用于同时训练偏置的权重
            X = np.c_[X, np.ones(X.shape[0])]
    
            # 迭代的epoch
            for epoch in np.arange(0, epochs):
                # 对数据集中每一个样本执行前向传播、反向传播、更新权重
                for (x, target) in zip(X, y):
                    self.fit_partial(x, target)
    
                # 打印输出
                if epoch == 0 or (epoch + 1) % display == 0:
                    loss = self.calculate_loss(X, y)
                    print("[INFO] epoch={}, loss={:.7f}".format(
                        epoch + 1, loss
                    ))
    
        def fit_partial(self, x, y):
            # 构造一个列表A,用于保存网络的每一层的输出,即经过激活函数的输出
            A = [np.atleast_2d(x)]
    
            # ---------- 前向传播 ----------
            # 对网络的每一层进行循环
            for layer in np.arange(0, len(self.W)):
                # 计算当前层的输出
                net = A[layer].dot(self.W[layer])
                out = self.sigmoid(net)
    
                # 添加到列表A
                A.append(out)
    
            # ---------- 反向传播 ----------
            # 计算error
            error = A[-1] - y
    
            # 计算最后一个权重矩阵的D[?]
            D = [error * self.sigmoid_deriv(A[-1])]
    
            # 计算前面的权重矩阵的D[?]
            for layer in np.arange(len(A)-2, 0, -1):
                # 参见上文推导的公式
                delta = D[-1].dot(self.W[layer].T)
                delta = delta * self.sigmoid_deriv(A[layer])
                D.append(delta)
    
            # 列表D是从后往前记录,下面更新权重矩阵的时候,是从输入层到输出层
            # 因此,在这里逆序
            D = D[::-1]
    
            # 迭代更新权重
            for layer in np.arange(0, len(self.W)):
                # 参考上文公式
                self.W[layer] += -self.alpha * A[layer].T.dot(D[layer])
    
        def predict(self, X, addBias=True):
            # 预测
            p = np.atleast_2d(X)
    
            # check to see if the bias column should be added
            if addBias:
                # insert a column of 1's as the last entry in the feature
                # matrix (bias)
                p = np.c_[p, np.ones((p.shape[0]))]
    
            # loop over our layers int the network
            for layer in np.arange(0, len(self.W)):
                # computing the output prediction is as simple as taking
                # the dot product between the current activation value 'p'
                # and the weight matrix associated wieth the current layer,
                # then passing this value through a nonlinear activation
                # function
                p = self.sigmoid(np.dot(p, self.W[layer]))
    
            # return the predicted value
            return p
    
        def calculate_loss(self, X, targets):
            # make predictions for the input data points then compute
            # the loss
            targets = np.atleast_2d(targets)
            predictions = self.predict(X, addBias=False)
            loss = 0.5 * np.sum((predictions - targets) ** 2)
    
            # return the loss
            return loss
    
    
    nn = NeuralNetwork([2, 2, 1])
    print(nn.__repr__())
    
  • 相关阅读:
    js实现深拷贝的几种方法
    禁止浏览器的默认行为 图片拖动 复制 剪切 右击
    祈祷奇迹,其实不如无尽的练习
    洛谷P4643 [国家集训队]阿狸和桃子的游戏 & 初赛心情
    Re:prime 关于质数的算法
    【洛谷有题】NOIP 2014 提高组初赛试题 订正 网络协议 检索/比较次数计算
    补码的快速计算
    2020洛谷初赛模拟 订正
    【洛谷有题】NOI 笔试题库(非初赛)订正
    树状数组小结
  • 原文地址:https://www.cnblogs.com/chenzhen0530/p/10830112.html
Copyright © 2020-2023  润新知