最近几天在研究BP神经网络网时,发现网上对它的介绍很多,但是在编程实现的时候,总感觉很多博客介绍BP的公式的时候没有介绍清楚,参考了不少博客还是感觉模棱两可,最后参考了周志华老师的《机器学习》这本书,再结合之前在网上看到的,自己用python实现了一个标准的BP网络。在这里记录一下学习过程。

1,BP神经网络

     在参考其他博客的时候,感觉都没有把BP网络的图以及变量表示清楚。导致我自己在看博客代码实现的时候,变量全部都搞混了。后来参考了周志华老师的《机器学习》感觉上面把变量定义的很清楚,在此参考。

1.1 正向传播

     BP的网络如下(这里直接拍照了 QAQ):
这里写图片描述
    图中定义了一个3层神经网络。输入层,隐藏层,输出层个数分别为 dql <script type="math/tex" id="MathJax-Element-269">d,q,l</script>。原始的输入为 xi <script type="math/tex" id="MathJax-Element-270">{x_i}</script>,每次输入d个变量,也可以理解为每次输入一个向量有d维。输入层与隐藏层之间的元素的节点的权值为 vih <script type="math/tex" id="MathJax-Element-271">v_{ih}</script>,隐藏层元素接收到的输入为 ah <script type="math/tex" id="MathJax-Element-272">a_h</script>, ah <script type="math/tex" id="MathJax-Element-273">a_h</script>的公式为 ah=divihxi <script type="math/tex" id="MathJax-Element-274">a_h = \sum_i^dv_{ih}x_i</script>,隐藏层的阀值表示为 γh <script type="math/tex" id="MathJax-Element-275">\gamma_h</script>。隐藏层输出值用 bh <script type="math/tex" id="MathJax-Element-276">b_h</script>表示,一般是 ah <script type="math/tex" id="MathJax-Element-277">a_h</script>减去阀值然后放入一个函数中。通常会选择s型函数,s型函数的定义为 f(x)=11ex <script type="math/tex" id="MathJax-Element-278">f(x)=\frac{1}{1-e^{-x}}</script>,s型函数的导数为 f,(x)=f(x)(1f(x)) <script type="math/tex" id="MathJax-Element-279">f^,(x)=f(x)*(1-f(x))</script>,之后在反向修改权值和阀值的时候会用到。隐藏层的输出 bn <script type="math/tex" id="MathJax-Element-280">b_n</script>的公式为 bh=f(ahγh) <script type="math/tex" id="MathJax-Element-281">b_h=f(a_h - \gamma_h)</script>。再从隐藏层到输出层时,每一隐藏层元素与输出层的元素之间的权值为 whj <script type="math/tex" id="MathJax-Element-282">w_{hj}</script>,每个输出层元素的输入为 βj <script type="math/tex" id="MathJax-Element-283">\beta_j</script>。公式为 βj=qhwhjbh <script type="math/tex" id="MathJax-Element-284">\beta_j=\sum_h^qw_{hj}b_h</script>。最后输出层的输出用 yj^ <script type="math/tex" id="MathJax-Element-285">\hat{y_j}</script>表示。每个输出层的元素也有一个阀值用 θj <script type="math/tex" id="MathJax-Element-286">\theta_j</script>表示。 yj^ <script type="math/tex" id="MathJax-Element-287"> \hat{y_j}</script>的公式为 yjˇ=f(βjθj) <script type="math/tex" id="MathJax-Element-288"> \check{y_j}=f(\beta_j-\theta_j)</script>。
    公式符号全部就介绍完了。是不是搞晕了。在这里总结一下:
s型函数: f(x)=f(x)(1f(x)) <script type="math/tex" id="MathJax-Element-289">f(x)=f(x)*(1-f(x))</script>,导数: f,(x)=f(x)(1f(x)) <script type="math/tex" id="MathJax-Element-290">f^,(x)=f(x)*(1-f(x))</script>
最初输入: xi <script type="math/tex" id="MathJax-Element-291">{x_i}</script>
输入层和隐藏层权值: vih <script type="math/tex" id="MathJax-Element-292">v_{ih}</script>
隐藏层输入: ah <script type="math/tex" id="MathJax-Element-293">a_h</script>,公式: ah=divihxi <script type="math/tex" id="MathJax-Element-294"> a_h= \sum_i^dv_{ih}x_i</script>
隐藏层阀值: γh <script type="math/tex" id="MathJax-Element-295">\gamma_h</script>
隐藏层输出: bh <script type="math/tex" id="MathJax-Element-296">b_h</script>,公式: bh=f(ahγh) <script type="math/tex" id="MathJax-Element-297">b_h=f(a_h - \gamma_h)</script>
隐藏层与输出层权值: whj <script type="math/tex" id="MathJax-Element-298">w_{hj}</script>
输出层输入: βj <script type="math/tex" id="MathJax-Element-299">\beta_j</script>,公式: βj=qhwhjbh <script type="math/tex" id="MathJax-Element-300">\beta_j=\sum_h^qw_{hj}b_h</script>
输出层阀值: θj <script type="math/tex" id="MathJax-Element-301">\theta_j</script>
输出层输出值: yj^ <script type="math/tex" id="MathJax-Element-302"> \hat{y_j}</script>,公式: yj^=f(βjθj) <script type="math/tex" id="MathJax-Element-303"> \hat{y_j}=f(\beta_j-\theta_j)</script>
当有一个输入 xi <script type="math/tex" id="MathJax-Element-304">{x_i}</script>的时候,先计算隐藏层输入 ah <script type="math/tex" id="MathJax-Element-305">a_h</script>。再计算隐藏层输出 bh <script type="math/tex" id="MathJax-Element-306">b_h</script>,然后计算输出层输入 βj <script type="math/tex" id="MathJax-Element-307">\beta_j</script>,最后计算得到输出值 yj <script type="math/tex" id="MathJax-Element-308">y_j</script>。正向传播的过程大致就是这样了。

1.2 反向传播

    在正向过程计算完成后,然后就要通过误差的反向传播(error backpropagation)修改权值和阀值了。误差使用每次正向传播的输出值与真实值的平方差得到 E=12(yrealyj^)2 <script type="math/tex" id="MathJax-Element-309">E=\frac{1}{2}{(y_{real}- \hat{y_j})}^2</script>。从误差的公式以及之前的正向传播的定义可以将误差E看成是关于权值和阀值的函数,利用梯度下降的思想分别求出权值的梯度 Δwhj <script type="math/tex" id="MathJax-Element-310">\Delta w_{hj}</script>和阀值的梯度 Δθj <script type="math/tex" id="MathJax-Element-311">\Delta \theta_j</script>(这里以隐藏层和输出层的权值阀值为例),得出梯度下降的方向,然后新的权值 whj=whjηΔwhj <script type="math/tex" id="MathJax-Element-312">w_{hj}=w_{hj} - \eta\Delta w_{hj} </script>。新的阀值 θj=θjηΔθj <script type="math/tex" id="MathJax-Element-313">\theta_j=\theta_j - \eta\Delta \theta_j</script>。 η <script type="math/tex" id="MathJax-Element-314">\eta</script>为学习率,一般定为0.1。
这里以隐藏层与输出层的权值阀值为例,先求关于权值的偏导 Ekwhj <script type="math/tex" id="MathJax-Element-315">\frac{\partial E_k}{\partial w_{hj}}</script>,这里就直接上图了,《机器学习》书中给出了大致的推导过程,具体想要了解数学推导的同学,再看看其他的博客或书。
这里写图片描述
这里写图片描述
在此总结一下反向传播的各个参数:
隐藏层与输出层之间权值的梯度: Δwhj=ηgjbh <script type="math/tex" id="MathJax-Element-316">\Delta w_{hj}=\eta g_jb_h</script>
gj <script type="math/tex" id="MathJax-Element-317">g_j</script>的公式: gj=y^kj(1y^kj)(ykjy^kj) <script type="math/tex" id="MathJax-Element-318">g_j=\hat{y}^k_j(1-\hat{y}^k_j)({y}^k_j-\hat{y}^k_j)</script>
输出层阀值的梯度: Δθj=ηgj <script type="math/tex" id="MathJax-Element-319">\Delta \theta_j=-\eta g_j</script>
输入层与隐藏层之间的权值梯度: Δvih=ηehxi <script type="math/tex" id="MathJax-Element-320">\Delta v_ih=\eta e_hx_i</script>
eh <script type="math/tex" id="MathJax-Element-321">e_h</script>的公式: eh=bh(1bh)ljwhjgj <script type="math/tex" id="MathJax-Element-322">e_h=b_h(1-b_h)\sum_j^lw_{hj}g_j</script>
隐藏层的阀值梯度: Δγh=ηeh <script type="math/tex" id="MathJax-Element-323">\Delta \gamma_h=-\eta e_h</script>
至此BP的公式就介绍完了。

2,代码实现

    代码实现的过程中有些技巧,在参考的https://www.cnblogs.com/Finley/p/5946000.html博客中,博主没有考虑输出层的阀值,只是考虑了输入层的阀值。而在http://blog.csdn.net/acdreamers/article/details/44657439这篇博客中,博主使用的是c++写的。没有将权值用矩阵表示。完全是利用for循环写的QAQ。
所以我在实现自己的神经网络时,考虑了隐藏层阀值以及输出层阀值,并且利用权值矩阵将输入层与隐藏层以及隐藏层和输出层的权值和阀值用两个矩阵input_weights以及output_weights表示。
代码如下:

#! /usr/bin/python
# -*- encoding:utf8 -*-

import numpy as np


def rand(a, b):
    return (b - a) * np.random.random() + a

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))


def sigmoid_derivative(x):
    return x * (1 - x)


class BP:
    def __init__(self, layer, iter, max_error):
        self.input_n = layer[0]  # 输入层的节点个数 d
        self.hidden_n = layer[1]  # 隐藏层的节点个数 q
        self.output_n = layer[2]  # 输出层的节点个数 l
        self.gj = []
        self.eh = []
        self.input_weights = []   # 输入层与隐藏层的权值矩阵
        self.output_weights = []  # 隐藏层与输出层的权值矩阵
        self.iter = iter          # 最大迭代次数
        self.max_error = max_error  # 停止的误差范围

        # for i in range(self.input_n + 1):
        #     tmp = []
        #     for j in range(self.hidden_n):
        #         tmp.append(rand(-0.2, 0.2))
        #     self.input_weights.append(tmp)
        #
        # for i in range(self.hidden_n + 1):
        #     tmp = []
        #     for j in range(self.output_n):
        #         tmp.append(rand(-0.2, 0.2))
        #     self.output_weights.append(tmp)
        # self.input_weights = np.array(self.input_weights)
        # self.output_weights = np.array(self.output_weights)

        # 初始化一个(d+1) * q的矩阵,多加的1是将隐藏层的阀值加入到矩阵运算中
        self.input_weights = np.random.random((self.input_n + 1, self.hidden_n))
        # 初始话一个(q+1) * l的矩阵,多加的1是将输出层的阀值加入到矩阵中简化计算
        self.output_weights = np.random.random((self.hidden_n + 1, self.output_n))

        self.gj = np.zeros(layer[2])
        self.eh = np.zeros(layer[1])

    #  正向传播与反向传播
    def forword_backword(self, xj, y, learning_rate=0.1):
        xj = np.array(xj)
        y = np.array(y)
        input = np.ones((1, xj.shape[0] + 1))
        input[:, :-1] = xj
        x = input
        # ah = np.dot(x, self.input_weights)
        ah = x.dot(self.input_weights)
        bh = sigmoid(ah)

        input = np.ones((1, self.hidden_n + 1))
        input[:, :-1] = bh
        bh = input

        bj = np.dot(bh, self.output_weights)
        yj = sigmoid(bj)

        error = yj - y
        self.gj = error * sigmoid_derivative(yj)

        # wg = np.dot(self.output_weights, self.gj)

        wg = np.dot(self.gj, self.output_weights.T)
        wg1 = 0.0
        for i in range(len(wg[0]) - 1):
            wg1 += wg[0][i]
        self.eh = bh * (1 - bh) * wg1
        self.eh = self.eh[:, :-1]

        #  更新输出层权值w,因为权值矩阵的最后一行表示的是阀值多以循环只到倒数第二行
        for i in range(self.output_weights.shape[0] - 1):
            for j in range(self.output_weights.shape[1]):
                self.output_weights[i][j] -= learning_rate * self.gj[0][j] * bh[0][i]

        #  更新输出层阀值b,权值矩阵的最后一行表示的是阀值
        for j in range(self.output_weights.shape[1]):
            self.output_weights[-1][j] -= learning_rate * self.gj[0][j]

        #  更新输入层权值w
        for i in range(self.input_weights.shape[0] - 1):
            for j in range(self.input_weights.shape[1]):
                self.input_weights[i][j] -= learning_rate * self.eh[0][j] * xj[i]

        # 更新输入层阀值b
        for j in range(self.input_weights.shape[1]):
            self.input_weights[-1][j] -= learning_rate * self.eh[0][j]
        return error

    def fit(self, X, y):

        for i in range(self.iter):
            error = 0.0
            for j in range(len(X)):
                error += self.forword_backword(X[j], y[j])
            error = error.sum()
            if abs(error) <= self.max_error:
                break

    def predict(self, x_test):
        x_test = np.array(x_test)
        tmp = np.ones((x_test.shape[0], self.input_n + 1))
        tmp[:, :-1] = x_test
        x_test = tmp
        an = np.dot(x_test, self.input_weights)
        bh = sigmoid(an)
        #  多加的1用来与阀值相乘
        tmp = np.ones((bh.shape[0], bh.shape[1] + 1))
        tmp[:, : -1] = bh
        bh = tmp
        bj = np.dot(bh, self.output_weights)
        yj = sigmoid(bj)
        print yj
        return yj

if __name__ == '__main__':
    #  指定神经网络输入层,隐藏层,输出层的元素个数
    layer = [2, 4, 1]
    X = [
            [1, 1],
            [2, 2],
            [1, 2],
            [1, -1],
            [2, 0],
            [2, -1]
        ]
    y = [[0], [0], [0], [1], [1], [1]]
    # x_test = [[2, 3],
    #           [2, 2]]
    #  设置最大的迭代次数,以及最大误差值
    bp = BP(layer, 10000, 0.0001)
    bp.fit(X, y)
    bp.predict(X)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐