python实现BP神经网络


title: python实现BP神经网络
date: 2019-10-29 21:54:21


文章目录

0. 前言

有幸,在软件可靠性课程的实验中,被要求实现BP神经网络模型。虽然,我觉得这门课程搭配这样的实验很无厘头,但正好趁这个机会,重新学习一下神经网络知识。学校的课程设计总归是不太令人满意的,但只要能学到有益的知识,就是赚到了。至于学分、绩点多少,就无关紧要了。

BP神经网络模型简介:

BP神经网络模型是1986年由Rumelhart和McClelland为首的科学家提出的概念,是一种按照误差逆向传播算法训练的多层前馈神经网络,是目前应用最广泛的神经网络。

原文及代码地址

1. 神经网络基本模型

1.1. 单神经元模型

python实现BP神经网络

其中,f(x)函数为神经元输出经过的激活函数

常见的激活函数有:

  • sigmoid函数

s i g m o i d ( x ) = 1 1 + e x p ( − x ) sigmoid(x) = \frac{ 1 }{ 1+exp(-x) } sigmoid(x)=1+exp(−x)1​

  • sgn函数(阶跃函数)

s g n ( x ) = { 1 , x ≥ 0 0 , x < 0 sgn(x)=\begin{cases} 1, & x\geq0 \\ 0, & x<0 \\ \end{cases} sgn(x)={1,0,​x≥0x<0​

  • ReLU(Rectified Linear Unit)函数

r e l u ( x ) = { x , x > 0 0 , x ≤ 0 relu(x)=\begin{cases} x, & x>0 \\ 0, & x\leq0 \\ \end{cases} relu(x)={x,0,​x>0x≤0​

值得注意的是,激活函数大多为非线性函数。原因在于:

线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无隐藏层的神经网络”。为了具体地(稍微直观地)理解这一点,我们来思考下面这个简单的例子。这里我们考虑把线性函数 h(x) = cx 作为激活函数,把y(x) = h(h(h(x)))的运算对应3层神经网络A。这个运算会进行y(x) = c × c × c × x的乘法运算,但是同样的处理可以由y(x) = ax(注意,a = c^3)这一次乘法运算(即没有隐藏层的神经网络)来表示。

1.2. 多层神经网络

python实现BP神经网络

当神经网络有多层时,中间的层称为中间层或隐藏层。隐藏层的输入为上一层的输出,隐藏层的输出为下一层的输入,对隐藏层的输出同样需使用激活函数。输入层则一般不需要经过激活函数。

值得注意的是,一个神经元的输出会传递到下一层的每个神经元上。

1.3. 神经网络学习过程

以感知机(由两层神经元组成)为例:

python实现BP神经网络

对于训练样例 ( X , y ) (X, y) (X,y),其中 X = { x 1 , x 2 } X=\{x_1, x_2\} X={x1​,x2​},当前神经网络的输出为 y ^ \hat{y} y^​。假定输出层的激活函数为阶跃函数,其数学推导为:

y ^ = f ( w 1 x 1 + w 2 x 2 − θ ) \hat{y} = f(w_1x_1 + w_2x_2 - \theta) y^​=f(w1​x1​+w2​x2​−θ)

将实际值 y y y与预测值 y ^ \hat{y} y^​进行数学比较,从而得出各权值 w i w_i wi​和阈值 θ \theta θ的误差,从而更新相应的权值和阈值:

Δ w i = η ( y − y ^ ) x i \Delta w_i = \eta(y - \hat{y})x_i Δwi​=η(y−y^​)xi​

w i ← w i + Δ w i w_i \leftarrow w_i + \Delta w_i wi​←wi​+Δwi​

其中, η ∈ ( 0 , 1 ) \eta \in (0,1) η∈(0,1),称为学习率。当 y ^ \hat{y} y^​与 y y y相等,或者之差足够小时,则可认定为训练成功。

2. BP误差反向传播算法

2.1. 算法推导

BP神经网络的数学推导过程相对简单,读者切不可望而却步。

更详细内容请参考西瓜书第5章——神经网络。

以三层神经网络为例:

python实现BP神经网络

:输入层到隐藏层的阈值为 γ h \gamma_h γh​,隐藏层到输出层的阈值为 θ j \theta_j θj​,激活函数 f ( x ) f(x) f(x)都为 S i g m o i d Sigmoid Sigmoid函数。

假定,对于一组样例 ( X k , Y k ) (X_k, Y_k) (Xk​,Yk​),神经网络输入为 X k = ( x 1 k , x 2 k , . . . , x d k ) X_k = (x_1^k, x_2^k,..., x_d^k) Xk​=(x1k​,x2k​,...,xdk​),输出为 Y ^ k = ( y ^ 1 k , y ^ 2 k , . . . , y ^ l k ) \hat{Y}_k = (\hat{y}_1^k, \hat{y}_2^k,..., \hat{y}_l^k) Y^k​=(y^​1k​,y^​2k​,...,y^​lk​)。

隐藏层输出为:

b h = f ( α h − γ h ) b_h = f(\alpha_h - \gamma_h) bh​=f(αh​−γh​)

输出层输出为:

y ^ j k = f ( β j − θ j ) \hat{y}_j^k = f(\beta_j - \theta_j) y^​jk​=f(βj​−θj​)

那么,神经网络在当前样例 ( X k , Y k ) (X_k, Y_k) (Xk​,Yk​)上均方误差为:

E k = 1 2 ∑ j = 1 l ( y ^ j k − y j k ) 2 E_k = \frac{1}{2}\sum_{j=1}^l(\hat{y}_j^k-y_j^k)^2 Ek​=21​j=1∑l​(y^​jk​−yjk​)2

根据均方误差结果,基于梯度下降策略,以目标的负梯度方向对隐层到输出层的权值参数 Δ w h j \Delta w_{hj} Δwhj​进行调整。给定学习率,有:

Δ w h j = − η ∂ E k ∂ w h j Δ w h j = − η ∂ E k ∂ y ^ j k ⋅ ∂ y ^ j k ∂ β j ⋅ ∂ β j ∂ w h j \begin{aligned} \Delta w_{hj} &= -\eta\frac{\partial E_k}{\partial w_{hj}} \\ \Delta w_{hj} &= -\eta\frac{\partial E_k}{\partial \hat{y}_j^k}\cdot\frac{\partial \hat{y}_j^k}{\partial \beta_j}\cdot\frac{\partial \beta_j}{\partial w_{hj}} \end{aligned} Δwhj​Δwhj​​=−η∂whj​∂Ek​​=−η∂y^​jk​∂Ek​​⋅∂βj​∂y^​jk​​⋅∂whj​∂βj​​​

显然:

∂ E k ∂ y ^ j k = y ^ j k − y j k \frac{\partial E_k}{\partial \hat{y}_j^k} = \hat{y}_j^k-y_j^k ∂y^​jk​∂Ek​​=y^​jk​−yjk​

根据图例中 β j \beta_j βj​的函数,又显然:

∂ β j ∂ w h j = b h \frac{\partial \beta_j}{\partial w_{hj}} = b_h ∂whj​∂βj​​=bh​

再根据 S i g m o i d Sigmoid Sigmoid函数的定义:

f ′ ( x ) = f ( x ) ( 1 − f ( x ) ) f^\prime(x) = f(x)(1-f(x)) f′(x)=f(x)(1−f(x))

则:

∂ y ^ j k ∂ β j = y ^ j k ( 1 − y ^ j k ) \frac{\partial \hat{y}_j^k}{\partial \beta_j} = \hat{y}_j^k(1-\hat{y}_j^k) ∂βj​∂y^​jk​​=y^​jk​(1−y^​jk​)

综上可得:

Δ w h j = − η ( y ^ j k − y j k ) y ^ j k ( 1 − y ^ j k ) b h \Delta w_{hj} = -\eta(\hat{y}_j^k-y_j^k)\hat{y}_j^k(1-\hat{y}_j^k)b_h Δwhj​=−η(y^​jk​−yjk​)y^​jk​(1−y^​jk​)bh​

令:

g j = y ^ j k ( y j k − y ^ j k ) ( 1 − y ^ j k ) g_j = \hat{y}_j^k(y_j^k-\hat{y}_j^k)(1-\hat{y}_j^k) gj​=y^​jk​(yjk​−y^​jk​)(1−y^​jk​)

最终:

Δ w h j = η g j b h \Delta w_{hj} = \eta g_j b_h Δwhj​=ηgj​bh​

进而,我们可以对隐藏层到输出层的阈值 θ j \theta_j θj​进行调整:

Δ θ j = − η ∂ E k ∂ θ j Δ θ j = − η ∂ E k ∂ y ^ j k ⋅ ∂ y ^ j k ∂ θ j Δ θ j = − η g j \begin{aligned} \Delta \theta_j &= -\eta\frac{\partial E_k}{\partial \theta_j} \\ \Delta \theta_j &= -\eta\frac{\partial E_k}{\partial \hat{y}_j^k}\cdot\frac{\partial \hat{y}_j^k}{\partial \theta_j} \\ \Delta \theta_j &= -\eta g_j \end{aligned} Δθj​Δθj​Δθj​​=−η∂θj​∂Ek​​=−η∂y^​jk​∂Ek​​⋅∂θj​∂y^​jk​​=−ηgj​​

同理,我们可以得到输入层到隐藏层的权值和阈值误差为:

Δ v i h = η e h x i Δ γ j = − η e h \begin{aligned} \Delta v_{ih} &= \eta e_h x_i \\ \Delta \gamma_j &= -\eta e_h \end{aligned} Δvih​Δγj​​=ηeh​xi​=−ηeh​​

其中:

e h = b h ( 1 − b h ) ∑ j = 1 l w h j g j e_h = b_h(1-b_h)\sum_{j=1}^l w_{hj}g_j eh​=bh​(1−bh​)j=1∑l​whj​gj​

2.2. 梯度下降的理解

何为梯度?

首先,它是一个向量。

其次,它的定义为:设可微函数 f ( x , y , z ) f(x,y,z) f(x,y,z),对于函数上的某一个点 P ( x , y , z ) P(x,y,z) P(x,y,z), { ∂ f ∂ x , ∂ f ∂ y , ∂ f ∂ z } \{\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}, \frac{\partial f}{\partial z}\} {∂x∂f​,∂y∂f​,∂z∂f​}则是该函数在 P P P点的梯度。

通俗来讲,函数某一点的梯度,就是该点的斜率,该点变化率最大的方向。而负梯度,则是该点能最快接近函数极小值的方向。

那么,何为梯度下降呢?

梯度下降则是,沿当前点的负梯度方向变化: x ← x − γ ∇ x \leftarrow x - \gamma \nabla x←x−γ∇,其中 γ \gamma γ为步长。如果步长足够小,则可以保证每一次迭代都在减小,但可能导致收敛太慢;如果步长太大,则不能保证每一次迭代都减少,也不能保证收敛。

以函数 f ( x ) = x 2 f(x) = x^2 f(x)=x2为例:

其梯度函数为 ∇ = 2 x \nabla = 2x ∇=2x。

点 p ( 1 , 1 ) p(1,1) p(1,1)处的梯度为 2 2 2。

设步长为0.2,点 p p p处进行梯度下降后,下一个点则为 ( 0.6 , 0.64 ) (0.6, 0.64) (0.6,0.64)。

在BP神经网络中,采用梯度下降则是为了以最快速度调整参数,将误差降到极小(此处涉及到极小与最小的数学问题,有兴趣者可以看看西瓜书)。

2.3. 算法步骤

python实现BP神经网络

2.4. 算法流程

python实现BP神经网络

2.5. 算法实现

编写一个三层神经网络的BP类,在构造函数中初始化神经网络:

'''
三层神经网络模型,包含:输入层、隐层、输出层
'''
class BP:
    '''
    构造函数,初始化三层神网络的各参数

    Args:
        x_count: 输入层神经元个数
        mid_count: 隐层神经元个数
        y_count: 输出层神经元个数
        eta: 学习率
        train_count: 最大训练次数
        precision: 误差精度
    '''
    def __init__(self, x_count, mid_count, y_count, eta=0.3, train_count=100, precision=0.00001):
        self.x_count = x_count
        self.mid_count = mid_count
        self.y_count = y_count
        self.eta = eta
        self.train_count = train_count
        self.precision = precision

        # 输入层到隐层的权值
        self.V = []
        for i in range(0, x_count):
            temp = []
            for j in range(0, mid_count):
                temp.append(2*random.random() - 1)
            self.V.append(temp)

        # 输入层到隐层的阈值
        self.gamma = []
        for i in range(0, mid_count):
            self.gamma.append(2*random.random() - 1)

        # 隐层到输出层的权值
        self.W = []
        for i in range(0, mid_count):
            temp = []
            for j in range(0, y_count):
                temp.append(2*random.random() - 1)
            self.W.append(temp)

        # 隐层到输出层的阈值
        self.beta = []
        for i in range(0, y_count):
            self.beta.append(2*random.random() - 1)

其次,在BP类中,编写一个训练神经网络的类方法:

    '''
    神经网络训练函数

    Args:
        X: 列表,输入数据
        Y: 列表,实际输出数据
    '''
    def train(self, X, Y):
        if len(X) != len(Y):
            print("Error: len(X) and len(Y) is unequal!!!")
            return

        for i in range(self.train_count):
            E = [] # 每一组数据的误差
            # 遍历每一组输入数据
            for j in range(len(X)):
                # 计算预测值
                y_predict, mid_output = self.compute_y(X[j])

                # 计算当前样例(组)的均方误差
                e = 0.0
                mid2y_g = [] # 隐层到输出层的梯度项
                for k in range(self.y_count):
                    # 计算输出层第k个神经元的误差
                    e += pow(y_predict[k] - Y[j][k], 2)
                E.append(e/2)

                # 计算隐层到输出层的梯度项
                mid2y_g = []
                for k in range(self.y_count):
                    # 计算输出层第k个神经元对应的,隐层到输出层的梯度项
                    mid2y_g.append(y_predict[k] * (1 - y_predict[k]) * (Y[j][k] - y_predict[k]))

                # 计算输入层到隐层的梯度项
                x2mid_g = []
                for k in range(self.mid_count):
                    temp = 0
                    for l in range(self.y_count):
                        temp += self.W[k][l] * mid2y_g[l]
                    # 计算隐层第k个神经元对应的,输入层到隐层的梯度项
                    x2mid_g.append(mid_output[k] * (1 - mid_output[k]) * temp)

                # 更新隐层到输出层的权值和阈值
                for k in range(self.mid_count):
                    for l in range(self.y_count):
                        self.W[k][l] += self.eta * mid2y_g[l] * mid_output[k]
                for k in range(self.y_count):
                    self.beta[k] -= self.eta * mid2y_g[k]

                # 更新输入层到隐层的权值和阈值
                for k in range(self.x_count):
                    for l in range(self.mid_count):
                        self.V[k][l] += self.eta * x2mid_g[l] * X[j][k]
                for k in range(self.mid_count):
                    self.gamma[k] -= self.eta * x2mid_g[k]

            # 计算累积误差
            E_sum = 0.0
            for e in E:
                E_sum += e
            E_sum /= len(E)
            print(E_sum)

            # 如果累计误差小于设定的误差精度,则停止训练
            if E_sum < self.precision:
                break

该函数用到的类方法如下:

    '''
    Sigmoid激活函数

    Args:
        x

    Returns:
        y: sigmoid(x)
    '''
    def sigmoid(self, x):
        return 1 / (1 + math.exp(-x))

    '''
    计算一组预测值

    Args:
        x: 列表,一组多元或一元的输入数据

    Returns:
        y: 列表,一组多元或一元的输出数据
        mid_output: 列表,隐层的输出数据
    '''
    def compute_y(self, x):
        # 计算隐层输入
        mid_input = []
        for i in range(self.mid_count):
            temp = 0
            for j in range(self.x_count):
                temp += self.V[j][i] * x[j]
            mid_input.append(temp)

        # 计算隐层输出
        mid_output = []
        for i in range(self.mid_count):
            mid_output.append(self.sigmoid(mid_input[i] - self.gamma[i]))

        # 计算输出层的输入
        y_input = []
        for i in range(self.y_count):
            temp = 0
            for j in range(self.mid_count):
                temp += self.W[j][i] * mid_output[j]
            y_input.append(temp)

        # 计算输出层的输出
        y = []
        for i in range(self.y_count):
            y.append(self.sigmoid(y_input[i] - self.beta[i]))

        return (y, mid_output)

最后,在BP类中,编写一个基于神经网络进行预测的类方法:

    '''
    神经网络预测函数

    Args:
        X: 列表,输入数据

    Returns:
        Y_predict: 列表,预测输出数据
    '''
    def predict(self, X):
        Y_predict = []
        for x in X:
            y_predict, _ = self.compute_y(x)
            Y_predict.append(y_predict)
        return Y_predict

2.6. 算法检验

2.6.1. 预测 y = x 2 y=x^2 y=x2模型

'''
预测 y=x^2 函数模型
'''
# 数据个数
data_count = 500

# 随机生成X数据
X = []
for i in range(data_count):
    X.append([2*random.random() - 1])

# 根据一元二次方程生成Y数据
Y = []
for i in range(data_count):
    noise = random.random() / 6 # 生成噪音,使数据更真实
    Y.append([pow(X[i][0], 2) + noise])

plt.scatter(X, Y, label='source data') # 原始数据

# 创建神经网络
bp = BP(x_count=1, mid_count=10, y_count=1, eta=0.3, train_count=1000, precision=0.00001)

# 未训练进行预测
Y_predict = bp.predict(X) # 预测
plt.scatter(X, Y_predict, label='predict firstly') # 显示预测数据

# 训练
bp.train(X, Y)

# 训练之后进行预测
Y_predict = bp.predict(X) # 预测
plt.scatter(X, Y_predict, label='predict finally') # 显示预测数据

plt.legend()
plt.show()

控制台输出每一轮训练后的累计误差如下:

python实现BP神经网络

显示的原数据与预测数据对比图如下:

python实现BP神经网络

2.6.2. 预测mnist手写数字图片数据集

'''
预测mnist数字图片数据集
'''
# 获取数据
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
# print(mnist.train.images.shape, mnist.train.labels.shape) # 训练集
# print(mnist.test.images.shape, mnist.test.labels.shape) # 测试集
# print(mnist.validation.images.shape, mnist.validation.labels.shape) # 验证集

# 取验证集中的一部分为训练数据,一部分为测试数据
X_train = mnist.validation.images[:100].tolist() # 将ndarray对象转换成列表
Y_train = mnist.validation.labels[:100].tolist()
X_test = mnist.validation.images[100:120].tolist()
Y_test = mnist.validation.labels[100:120].tolist()

# 创建神经网络,并用训练数据进行训练
bp = BP(x_count=784, mid_count=10, y_count=10, eta=0.3, train_count=100, precision=0.001)
bp.train(X_train, Y_train)

# 训练结束后,用测试数据进行预测
Y_predict = bp.predict(X_test)

# 显示预测结果
for i in range(len(Y_predict)):
    # 求一组预测输出数据中值最大的神经元位置
    max_pos = 0
    Max = 0
    for j in range(len(Y_predict[i])):
        if Y_predict[i][j] > Max:
            max_pos = j
            Max = Y_predict[i][j]

    image = X_test[i] # 获取测试集中对应的数据
    image = np.array(image).reshape(28, 28) # 将图像数据还原成28*28的分辨率,即28*28的数组
    plt.imshow(image)
    plt.title('predict is: {}, real is: {}'.format(max_pos, Y_test[i].index(1)))
    plt.ion()
    plt.pause(3)
    plt.close()

控制台输出每一轮训练后的累计误差如下:

python实现BP神经网络

挑选4张预测结果图片,如下:

python实现BP神经网络

3. 参考

  • 《机器学习》,周志华
上一篇:【笔记】简单的线性回归的实现以及向量化的实现


下一篇:10.5.4 利用sklearn搭建多层神经网络