反向传播算法原理以及简单实现

11次阅读

共计 6617 个字符,预计需要花费 17 分钟才能阅读完成。

内容:bp 神经网络

bp 网络原理以及例题

主要思想:

  • 解决 adaline 网络无法进行线性划分以及异或的问题

本质:

  • 通过多层网络与多个节点将原来在低维度线性不可分的问题转换为高维度,有很大可能这个问题就成了线性可分问题了

主要难点:

  • 在 adaline 网络里,如果按照梯度下降法来进行节点连接权值的调整的话,是比较简单的,只需要将计算期望与样本期望比较一下就可以计算出误差,通过误差计算梯度也是很简单的事情,但是在 bp 网络中,因为涉及多隐层,中间层的误差不好计算,所以不能够直接计算局部梯度

解决方法:

  • 按照正常梯度下降调整权值的话,其公式应该是

    $\Delta\omega=\eta*\delta_j(n)*y_j(n)\\\Delta\omega: 调整的权值 \\\eta: 学习率 \\y_j(n): 神经元 j 的输出信号 \\\delta_j(n): 局部梯度 $

  • 问题的的关键在于求解局部梯度
  • 在输出层

    • $$\delta_j(n)=e_j(n)*\varphi^`(v_j(n))\\e: 误差 \\\varphi: 激活函数 \\v_j(n): 神经元输出 (非输出信号)$$
  • 在隐藏层,根据微分的链式求导法则,可以计算出

    • $$\delta_j(n)=\varphi^`(v_j(n))\sum_k\delta_k(n)w_{kj}(n)$$
    • 在这里需要注意,k=j+1,也就是 j 的下一层节点。
  • 在解决了参数调整的问题之后,bp 网络就可以分成两个阶段:

    • 前向阶段,算出样本期望
    • 后向阶段,根据输出层的误差调整各节点权重。

例题

  • 现在我们面对长这样的网络

    • 简单分析一下网络,三个输入节点,其中一个偏置,两层三节点隐藏层,其中都有一个偏置,一个输出节点。
    • 参数说明各层权重以及输入:

      $$
      \begin{align*}
      X=\left[
      \begin{matrix}
      x_{0} \\
      x_{1} \\
      x_{2} \\
      \end{matrix}
      \right] \
      W^0=\left[
      \begin{matrix}
      w^0_{10} & w^0_{20}\\
      w^0_{11} & w^0_{21}\\
      w^0_{21} & w^0_{22}
      \end{matrix}
      \right]\ \\
      W^1=\left[
      \begin{matrix}
      w^1_{10} & w^1_{20}\\
      w^1_{11} & w^1_{21}\\
      w^1_{21} & w^1_{22}
      \end{matrix}
      \right]\
      W^2=\left[
      \begin{matrix}
      w^2_{10}\\
      w^2_{11}\\
      w^2_{21}
      \end{matrix}
      \right]\
      \end{align*}
      $$

    • 前向过程

      $$
      X^T*W^0=V^0|_{2*1}\quad

      V^0 = \left[Y^0_0,V^0_1,V^0_2\right]^T\quad

      Y^0|_{3*1} = \varphi(V^0)\quad

      Y^0*W^1 = V^1|_{2*1}\\

      $$

      $$

      Y^1 = \left[Y^1_0,V^1_1,V^1_2\right]^T\quad Y^1|_{3*1} = \varphi(V^1)\quad

      Y^1*W^2 = V^2|_{1*1}\quad

      Y^2|_{1*1} = \varphi(V^2)
      $$

      • 对于 V^0, 加入一个偏置 Y^0_0, 形成紫色层的输入
      • 算出的样本期望就是 $Y^2$, 就是网络的总输出
    • 后向过程

      • 输出层 (橙色层前面的权重)

    $$
    \delta^2|_{1*1}=e*\varphi^`(v^2)\quad
    \Delta\omega^2|_{3*1}=\eta*\delta^2*y^1(n)\quad
    w^2 = w^2+\Delta w^2
    $$

    • 第二隐层 (紫色层)

      $$
      \delta^1|_{2*1}=\frac{\varphi^`(v^1)}{_{1*1}}\sum_k\delta^2_k(n)w^2(n)\\=\frac{\varphi^`(v^1)}{_{1*1}}*\left[
      \begin{matrix}
      \delta^2*w^2_{01}\\
      \delta^2*w^2_{02}
      \end{matrix}
      \right]\\
      $$

      $$
      \\\Delta\omega^1|_{3*1}=\eta*\frac{y^0(n)}{_{3*3}}*\frac{\delta^1}{_{1*2}}^T\quad
      w^1 = w^1+\frac{y^0(n)^T}{_{3*1}}
      $$

      • 因为偏置不应该算入调整权值变化量的公式中,所以省略了偏置的权重
    • 输入层 (绿色层)

      $$
      \delta^0|_{2*1}=\frac{\varphi^`(v^0)}{_{2*1}}\sum_k\delta^1_k(n)w^1(n)\\=\left[
      \begin{matrix}
      \varphi^`(v^0_1)(\delta^1_1*w^1_{11}+\delta^1_2*w^1_{12})\\
      \varphi^`(v^0)(\delta^1_1*w^1_{21}+\delta^1_2*w^1_{22}\\
      \end{matrix}
      \right]|_{2*1}\\
      $$

      $$
      \\\Delta\omega^0|_{3*1}=\eta*\frac{X}{_{3*1}}*\frac{\delta^0}{_{1*2}}^T\quad~~~~
      w^1 = w^1+\frac{X}{_{3*1}}
      $$

    • 对于一组输入来说,这就已经调整过一次了

    bp 网络编程实现

    网络结构与数据

    • 数据类型是双月的数据,输入层三个,其中加入一个为 1 的偏置
    • 第一隐层,也就是输入层后的一层,节点是 20 个,第二隐层节点数 10 个
    • 输出层为一个

    工作流程:

    from graphviz import Digraph
    dot = Digraph(comment='The Round Table')
    dot.node('A',"生成数据")
    dot.node('B',"参数确定 / 权值确定")
    dot.node('C','网络构建 / 参数调整')
    dot.node('D','决策面绘制')
    
    dot.edges(["AB","BC","CD"])
    dot

    1]()

    实验步骤

    导入相关包并生成数据

    import numpy as np
    import matplotlib.pyplot as plt
    import pandas as pd
    import random
    # 数据生成
    """最后生成 train_data 变量存储数据"""
    
    
    def halfmoon(rad, width, d, n_samp):
        ''' 生成半月数据
        @param  rad:    半径
        @param  width:  宽度
        @param  d:      距离
        @param  n_samp: 数量
        '''
        if n_samp % 2 != 0:  # 确保数量是双数
            n_samp += 1
    
        data = np.zeros((3, n_samp))
        # 生成 0 矩阵, 生成 3 行 n_samp 列的的矩阵
        aa = np.random.random((2, int(n_samp/2)))
        radius = (rad-width/2) + width*aa[0, :]
        theta = np.pi*aa[1, :]
    
        x = radius*np.cos(theta)
        y = radius*np.sin(theta)
        label = np.ones((1, len(x)))         # label for Class 1
    
        x1 = radius*np.cos(-theta) + rad  # 在 x 基础之上向右移动 rad 个单位
        y1 = radius*np.sin(-theta) + d      # 在 y 取相反数的基础之上向下移动 d 个单位
        label1 = 0*np.ones((1, len(x1)))     # label for Class 2
    
        data[0, :] = np.concatenate([x, x1])
        data[1, :] = np.concatenate([y, y1])
        data[2, :] = np.concatenate([label, label1], axis=1)
        # 合并数据
        return data
    
    
    dataNum = 1000
    data = halfmoon(10, 5, 5, dataNum)
    pos_data = data[:, 0: int(dataNum/2)]
    neg_data = data[:, int(dataNum/2):dataNum]
    plt.figure()
    plt.scatter(pos_data[0, :], pos_data[1, :], c="b", s=10)
    plt.scatter(neg_data[0, :], neg_data[1, :], c="r", s=10)
    plt.show()
    train_data = []
    test_data = []
    tmp = []
    i = 0
    for i in range(1000):
        tmp.append(i)
    random.shuffle(tmp)
    for i in range(len(tmp)):
        train_data.append(data[:, tmp[i]])
    train_data = np.array(train_data).T

    确定应该使用的参数

    • 包括二个隐藏层各层的节点数,输入层的节点数,学习率。
    • 生成每一层的随机权重
    # 参数确定·
    inp_num = 3  # 输入层节点数
    out_num = 1  # 输出层节点数
    hid_num_1 = 20  # 第一隐层有 20 个节点
    hid_num_2 = 10  # 第二隐层有 10 个节点
    w1 = 0.2*np.random.random((inp_num, hid_num_1))   # 初始化输入层权矩阵
    w2 = 0.2*np.random.random((hid_num_1, hid_num_2))  # 初始化第一隐层的权矩阵
    w3 = 0.2*np.random.random((hid_num_2, out_num))  # 初始化第二隐层的权矩阵
    inp_lrate = 0.3             # 输入层权值学习率
    hid_lrate = 0.3             # 隐层学权值习率
    out_lrate = 0.3             # 输出层学习率
    a = 1                       # sigmoid 函数参数 

    激活函数与误差函数的定义

    def get_act(a, x):  # 激活函数
        act_vec = []
        for i in x:
            act_vec.append((1/(1+math.exp(-1*a*i))))
        act_vec = np.array(act_vec)
        return act_vec
    
    
    def get_err(e):  # 误差函数
        return 0.5*np.dot(e, e)

    学习过程

    import math
    '''循环使用生成的 1000 个节点,直到误差函数小于设定的阈值'''
    err_store = []
    have = True
    while(have):
        e_store = []
        for count in range(0, 1000):
            t_label = np.zeros(out_num)  # 输出结果存储
            # 前向过程
            tmp = []
            tmp.append(train_data[0][count])
            tmp.append(train_data[1][count])
            tmp.append(1)
        #     三个输入
            hid_value_1 = np.dot(np.array(tmp), w1)  # hid_value_1 = v1
            hid_act_1 = get_act(a, hid_value_1)  # hid_act_1 = y1
            hid_value_2 = np.dot(hid_act_1, w2)  # hid_value_2 = v2
            hid_act_2 = get_act(a, hid_value_2)  # hid_act_2 = y2
            hid_value_3 = np.dot(hid_act_2, w3)  # 输出层的输出值
            tmp_1 = []
            tmp_1.append(hid_value_3)
            out_act = get_act(a, tmp_1)   # 输出层的激活函数值 / 最终的计算结果
    
            # 后向过程
            e = train_data[2][count] - out_act  # 误差计算
            e_store.append(get_err(e))  # 存储误差函数
            out_delta = a * e * out_act * (1-out_act)  # 输出层误差梯度
            hid_delta_2 = a * hid_act_2 * \
                (1-hid_act_2)*np.dot(w3, out_delta)  # 第二隐层的误差梯度
            hid_delta_1 = a * hid_act_1 * \
                (1-hid_act_1)*np.dot(w2, hid_delta_2)  # 第一隐层的误差梯度
            for i in range(0, out_num):
                w3[:, i] += out_lrate * out_delta * hid_act_2
            for i in range(0, hid_num_2):
                w2[:, i] += hid_lrate * hid_delta_2[i] * hid_act_1
            for i in range(0, hid_num_1):
                w1[:, i] += inp_lrate * hid_delta_1[i] * np.array(tmp)
        err_sum = 0
        for item in e_store:
            err_sum += item
        err_store.append(err_sum)
        if(err_sum < 0.1): # 0.1 是设置的误差阈值
            have = False

    误差曲面绘制

    x = []
    for item in range(len(err_store)):
        x.append(item)
    plt.figure()
    plt.scatter(x,err_store)
    <matplotlib.collections.PathCollection at 0x1b04ea014c0>
    
    
    
    

    • 可以看到在一开始的回合之中,误差其实是非常大的,在 10 回合左右的时候下降速度最快,但是在 80 回合左右以及 120 回合左右会出现反弹

    决策面绘制

    x = []
    y = []
    for i in range(-15, 25):
        x.append(i/1)
    for i in range(-8, 13):
        y.append(i/1)
    
    green_x = []
    green_y = []
    red_x = []
    red_y = []
    for i in x:
        for j in y:
            tmp = [i, j, 1]
            hid_value_1 = np.dot(np.array(tmp), w1)  # hid_value_1 = v1
            hid_act_1 = get_act(a, hid_value_1)  # hid_act_1 = y1
            hid_value_2 = np.dot(hid_act_1, w2)  # hid_value_2 = v2
            hid_act_2 = get_act(a, hid_value_2)  # hid_act_2 = y2
            hid_value_3 = np.dot(hid_act_2, w3)  # 输出层的输出值
            tmp_1 = []
            tmp_1.append(hid_value_3)
            out_act = get_act(a, tmp_1)   # 输出层的激活函数值 / 最终的计算结果
            if(out_act > 0.5):
                green_x.append(i)
                green_y.append(j)
            else:
                red_x.append(i)
                red_y.append(j)
    
    
    plt.figure()
    plt.scatter(pos_data[0, :], pos_data[1, :], c="b", s=10)
    plt.scatter(neg_data[0, :], neg_data[1, :], c="b", s=10)
    plt.scatter(green_x, green_y, c="g", s=10)
    plt.scatter(red_x, red_y, c="r", s=10)
    plt.show()

    • 从决策面中可以看出,分类的效果还是比较明显的

    实验总结

    遇到的问题以及解决方法

    • 在计算后向过程的时候,因为对算法理解不清晰,计算结果的表面现象就是矩阵的规模对不上,深层次的就是公式还不理解

      • 其实我感觉在做这类型的网络时候,基础还是数学,在编程之前还是应该要把数学公式严格的推导下来,这样子才不会在编程上犯错误
    • 对编程语言本身理解还是应该要加强,多看文档多尝试
    • 在没有设置网络学习的停止条件的时候,我尝试使用固定的回合去学习,发现会有一些比较好玩的情况。
      • 我认为之所以会出现这个情况,就是因为没有设置恰当的停止规则,导致学习不完全所导致的。
      • 遇到这个情况后,我开始思考了一下添加进入停止规则,这才减少了误差

    心得体会

    • bp 神经网络可以实现非线性的分类,但是本身需要多次迭代学习,现在手上的数据集点很小,才 1000 个数据,就需要学习接近 150 个回合,如果说再大量一点的数据集可能需要很长的学习周期。
    • 神经网络学习的停止规则还是很重要。

    参考资料

    代码:核心步骤是参考以下网站的~~~~
    github

    正文完
     0