如何使用numpy搭建双隐层神经网络?看这一篇文章就够用了

在阅读本文之前,请确保你有一定的神经网络基础(具体介绍见西瓜书)。

本文采用的是标准的BP算法,即每次仅针对一个样例更新权重和阈值。

本文将搭建用于分类的双隐层BP神经网络

内容

一、理论部分

2.使用步骤

1.引入库

2.读入数据

总结

一、理论部分

1.1 正向计算

符号 说明

设我们的双隐层BP神经网络有m个输入神经元,n个输出神经元,第一个隐层有p个隐层神经元,第二个隐层有q个隐层神经元。

  • 权重:第i个输入神经元到第j个第一个隐层的神经元的权重记为w1_{ij}, 第i个第一个隐层的神经元到第j个第二个隐层的神经元的权重记为w2_{ij},第i个第二个隐层的神经元到第j个输出神经元的权重记为w3_{ij} ;
  • 阈值:i第一隐藏层神经元的阈值记为\theta1_ij第二隐藏层神经元的阈值记为\theta2_jk输出神经元的阈值记为\theta3_k
  • Input:第k个输入神经元接收到的输入记为x_kk第一个隐藏层的神经元接收到的输入值为\alpha_kk第二个隐藏层的神经元接收到的输入输入值为\gamma_k ,第k个输出神经元接收到的输入值为\beta_k
  • 输出:j第一个隐藏层神经元的输出记为a_jj第二个隐藏层神经元的输出记为b_jj输出神经元的输出记为\widehat{y}_j

x = (x_1, x_2, \cdots,x_n)^T,激活函数为\sigma(x),我们定义:

\sigma(x) = (\sigma(x_1), \sigma(x_2),\cdots,\sigma(x_n))^T

公式推导

{\bf\alpha}=\begin{bmatrix} \alpha_1\\ \alpha_2\\ \vdots\\ \alpha_p \end{bmatrix}_{p\times1},W1=\begin{bmatrix} w1_{11} & w1_{21}&\cdots &w1_{m1} \\ w1_{12}&w1_{22} &\cdots &w1_{m2} \\ \vdots &\vdots & \ddots &\vdots \\ w1_{1p}&w1_{2p} &\cdots &w1_{mp} \end{bmatrix}_{p\times m},x=\begin{bmatrix} x_1\\ x_2\\ \vdots\\ x_m \end{bmatrix}_{m\times1}

然后有

\large \alpha = W1 x

再设a=(a_1, a_2, \cdots,a_p)^T\theta1=(\theta1_1,\theta1_2, \cdots,\theta1_p)^T,因此a= \sigma(\alpha- \theta1)=\sigma(W1x - \theta1)

类似地,W2_{q\times p}是第一个隐藏层和第二个隐藏层之间的权重矩阵,我们有

\gamma=W2a, \quad b=\sigma(\gamma - \theta2)

再有b=\sigma(W2\sigma(W1x - \theta1)-\theta2)

最后,W3_{n\times q}是第二个隐藏层和输出层之间的权重矩阵,并且有

\beta = W3b,\quad \widehat{y} = \sigma(\beta-\theta3)

再有\widehat{y}=\sigma(W3\sigma(W2\sigma(W1x - \theta1)-\theta2)-\theta3)

综上所述,我们成功地得到了神经网络的输出。

1.2 反向传播

对于给定的例子(x,y),神经网络的输出是\widehat{y} = \sigma(\beta - \theta3),所以误差是

E=\left \| \widehat{y} - y \right \|^2= \sum_{j =1}^n(\widehat{y}- y_j)^2

我们采用梯度下降策略来最小化误差,即

w3_{ij} :=w3_{ij}-\eta\frac{\partial E}{\partial w3_{ij}},\theta3:=\theta3-\eta\frac{\partial E}{\partial \theta3_j}

\frac{\partial E}{\partial w3_{ij}}=\frac{\partial E}{\partial \widehat{y}_j}\frac{\partial \widehat{y}_j}{\partial \beta_{j}}\frac{\partial \beta_{j}}{\partial w3_{ij}}= (\widehat{y}_j - y_j)\sigma^{'}(\beta_j - \theta3_j)b_i

g3_j = -(\widehat{y}_j - y_j)\sigma^{'}(\beta_j - \theta3_j),则我们知道\Delta w3_{ij} = \eta g3_j b_i

另一方面

\frac{\partial E}{\partial \theta3}_j = \frac{\partial E}{\partial \widehat{y}}_j\frac{\partial \widehat{y}_j}{\partial \theta3}_j=(\widehat{y}-y_j)(-\sigma^{'}(\beta_j - \theta3_j))=g3_j

因此\Delta \theta3_j = -\eta g3_j

\Delta W = \eta gb^T, \Delta \theta = -\eta g,则得到权重和阈值的更新公式:

W3:= W3 + \Delta W,\quad \quad\theta 3 = \theta3 + \Delta \theta

此外,对于另外的两个权重矩阵以及阈值和梯度项,我们可以根据以上过程以及链式法则推导得到(注:激活函数采用Sigmoid函数):

g_2 = W_1^Tg_3b(1-b) \quad \quad g_1 = W_2^Tg_2a(1-a)

同时,我们还可以得到其他权重和阈值的更新公式:

W_2 := W_2 + \eta g_2a^T, \quad \quad \theta_2 := \theta_2 +-\eta g_2

W_1 := W_1 +\eta g_1x^T, \quad \quad \theta_1 = \theta_1 + -\eta g_1

二、实现部分

2.1 算法伪码

基于以上推导过程,为了方便我们后续的编程实现,我们可以尝试编写算法的伪代码:

输入:训练集D, 学习率\eta

过程 :

1.随机初始化W1,W2,W3,\theta1, \theta2, \theta3

2.repeat

3.for all(x,y)\in Ddo

4.        计算输入和输出\alpha, a, \gamma, b, \beta,\widehat{y}

5.        计算梯度项g1, g2, g3

6.        计算改变量\Delta W1, \Delta W2,\Delta W3,\Delta \theta1 , \Delta \theta2 ,\Delta \theta3

7.        更新W1,W2,W3,\theta1,\theta2, \theta3

8.end for

9.untilE< \epsilon或迭代次数达到某个值

输出:训练好的神经网络

经过了以上的推导,想必大家更希望看到一种更为简洁的方式来表示神经网络的输出过程,以及各个变量所对应的位置,于是我们可以将双隐层按如下简化:

如何使用numpy搭建双隐层神经网络?看这一篇文章就够用了如何使用numpy搭建双隐层神经网络?看这一篇文章就够用了

2.2 算法实现

2.2.1初始化

首先我们需要创建一个神经网络类:

import numpy as np


class Neural_Network():
    """定义一个神经网络类"""
    def __init__(self):
        pass

接下来我们要对神经网络类需要初始化的数据做一个分析,显然,输入层的结点数m,隐层结点数p和q,输出层的结点数n都是需要初始化的,它们决定了神经网络的结构。

另外根据算法伪代码我们可以看到需要初始化\eta,W_1,W_2,W_3,\theta_1,\theta_2,\theta_3,注意权重矩阵和阈值向量是由前面的节点参数决定的,剩下的参数需要我们手动输入

当然,容忍度tol,最大迭代次数max_iter,也需要人工输入,这两个参数决定了我们的神经网络进行训练的时间和次数。

def __init__(self, X, y, hiddennodes1, hiddennodes2, learningrate=0.01, tol=1e-3, max_iter=1000):
       
        self.X, self.y = X, y
        
        self.classes = np.unique(self.y)
        #结点数
        self.ins, self.hns1, self.hns2, self.ons = len(X[0]), hiddennodes1, hiddennodes2, len(self.classes)
        #学习率
        self.lr = learningrate
        #容忍度
        self.tol = tol
        #最大迭代次数
        self.max_iter = max_iter

权重矩阵和阈值向量都是在(0,1)范围内初始化:

        #权重矩阵
        self.W1 = np.random.rand(self.hns1, self.ins)
        self.W2 = np.random.rand(self.hns2, self.hns1)
        self.W3 = np.random.rand(self.ons, self.hns2)
        #阈值向量
        self.theta1 = np.random.rand(self.hns1)
        self.theta2 = np.random.rand(self.hns2)
        self.theta3 = np.random.rand(self.ons)

当然,还有激活函数,我们用Python的匿名函数来做初始化:

#Sigmoid 
self.sigmoid = lambda x: 1 / (1 + np.power(np.e, -x))

到此为止,我们的_init()_函数算是完成了 :

def __init__(self, X, y, hiddennodes1, hiddennodes2, learningrate=0.01, tol=1e-3, max_iter=1000):
       
        self.X, self.y = X, y
        
        self.classes = np.unique(self.y)
        self.ins, self.hns1, self.hns2, self.ons = len(X[0]), hiddennodes1, hiddennodes2, len(self.classes)
        self.lr = learningrate
        self.tol = tol
        self.max_iter = max_iter
        
        self.W1 = np.random.rand(self.hns1, self.ins)
        self.W2 = np.random.rand(self.hns2, self.hns1)
        self.W3 = np.random.rand(self.ons, self.hns2)
        
        self.theta1 = np.random.rand(self.hns1)
        self.theta2 = np.random.rand(self.hns2)
        self.theta3 = np.random.rand(self.ons)
        
        self.sigmoid = lambda x: 1 / (1 + np.power(np.e, -x))

2.2.2 正向传播

我们需要定义一个函数来实现正向的传播,该函数需要一个样本(x,y)作为输入,我们用(inputs, targets)来表示,输出的\widehat{y}用outputs来表示。

当然,我们还需要计算误差E,用error来表示。

 def propagate(self, inputs, targets):
        
        #计算输入和输出
        alpha = np.dot(self.W1, inputs)
        a = self.sigmoid(alpha - self.theta1)
        gamma = np.dot(self.W2, a)
        b = self.sigmoid(gamma - self.theta2)
        beta = np.dot(self.W3, b)
        outputs = self.sigmoid(beta - self.theta3)

        #计算误差
        error = np.linalg.norm(targets - outputs, ord=2) ** 2 / 2
        
        return alpha, a, gamma, b, beta, outputs, error

2.2.3 反向传播

反向传播需要先计算三个梯度项g_1,g_2,g_3,然后更新参数。可以看出我们需要以下参数

x,y, \alpha,a,\gamma,b,\beta,\widehat{y},因此我们定义了一个反向传播函数:

    def back_propagate(self, inputs, targets, alpha, a, gamma, b, beta, outputs):
        
        #计算三个梯度项
        g3 = (targets - outputs) * outputs * (np.ones(len(outputs)) - outputs)
        g2 = np.dot(self.W3.T, g3) * b * (np.ones(len(b)) - b)
        g1 = np.dot(self.W2.T, g2) * a * (np.ones(len(a)) - a)
        
        #更新参数
        self.W3 += self.lr * np.outer(g3, b)
        self.theta3 += -self.lr * g3
        self.W2 += self.lr * np.outer(g2, a)
        self.theta2 += -self.lr * g2
        self.W1 += self.lr * np.outer(g1, inputs)
        self.theta1 += -self.lr * g1

2.2.4 训练

训练需要传入数据集,假设数据集的表示形式为 X , y 。

我们需要对传入的数据集做一些处理。在遍历数据集时,X[i] 是向量,即inputs,y[i] 是向量对应的标签,它仅仅是一个数,而非像targets这样的向量。

那么如何将 y[i] 处理成targets呢?

考虑像手写数字识别这样的十分类任务,对应的神经网络的输出层有十个神经元,每个神经元对应着一个数字。例如,当我们输入一张 5 的图片时,5 对应的神经元的输出值应该是最高的,其他神经元的输出值应该是最低的。

因为激活函数是Sigmoid函数,它的输出区间为(0,1)。我们将输出层神经元按照其对应的数字从小到大进行排列,从而输出层的情况应当十分接近下面这个样子:
[0,0,0,0,0,1,0,0,0,0]

y 中包含了所有向量的标签,若要判断我们面临的问题是一个几分类问题,我们需要对 y 进行去重,再将结果从小到大进行排序:

self.classes = np.unique(self.y)

获取targets的步骤如下:

 inputs, targets = self.X[idx], np.zeros(len(self.classes))
 targets[list(self.classes).index(self.y[idx])] = 1

正如我们前面提到的,当且仅当满足以下两个条件之一时,训练才会停止:

  • 迭代收敛,即E<\in
  • 迭代次数达到最大值,即训练了 max_iter 次

如果第一个条件先达到,则我们应立刻停止训练;如果第二个条件达到,说明我们训练了max_iter次也没有收敛,此时应当抛出一个错误。

 def train(self):
        
        #初始时未收敛
        convergence = False
        
        #开始迭代 max_iter 次
        for _ in range(self.max_iter):
            #遍历数据集
            for idx in range(len(self.X)):
                inputs, targets = self.X[idx], np.zeros(len(self.classes))
                targets[list(self.classes).index(self.y[idx])] = 1
                
                #正向计算
                alpha, a, gamma, b, beta, outputs, error = self.propagate(inputs, targets)
                #若误差不超过容忍度,则判定收敛
                if error <= self.tol:
                    convergence = True
                    break
                else:
                    # 反向传播
                    self.back_propagate(inputs, targets, alpha, a, gamma, b, beta, outputs)
            if convergence:
                break
        
        if not convergence:
            raise RuntimeError('神经网络未收敛,请调大max_iter的值')

2.2.5 预测

训练好的神经网络应该能对单个样本完成分类,我们选择outputs中输出最高的神经元对应的类别标签作为我们对样本的标签:

    def predict(self, sample):
        
        alpha = np.dot(self.W1, sample)
        a = self.sigmoid(alpha - self.theta1)
        gamma = np.dot(self.W2, a)
        b = self.sigmoid(gamma - self.theta2)
        beta = np.dot(self.W3, b)
        outputs = self.sigmoid(beta - self.theta3)
        
        #直接输出最高的神经元在outputs中的索引  
        return self.classes[idx = list(outputs).index(max(outputs))]

完整的双隐藏层神经网络代码如下:

import numpy as np


class Neural_Network():
    """定义一个神经网络类"""
    def __init__(self, X, y, hiddennodes1, hiddennodes2, learningrate=0.01, tol=1e-3, max_iter=1000):
       
        self.X, self.y = X, y
        
        self.classes = np.unique(self.y)
        self.ins, self.hns1, self.hns2, self.ons = len(X[0]), hiddennodes1, hiddennodes2, len(self.classes)
        self.lr = learningrate
        self.tol = tol
        self.max_iter = max_iter
        
        self.W1 = np.random.rand(self.hns1, self.ins)
        self.W2 = np.random.rand(self.hns2, self.hns1)
        self.W3 = np.random.rand(self.ons, self.hns2)
        
        self.theta1 = np.random.rand(self.hns1)
        self.theta2 = np.random.rand(self.hns2)
        self.theta3 = np.random.rand(self.ons)
        
        self.sigmoid = lambda x: 1 / (1 + np.power(np.e, -x))
            
    def propagate(self, inputs, targets):
        
        alpha = np.dot(self.W1, inputs)
        a = self.sigmoid(alpha - self.theta1)
        gamma = np.dot(self.W2, a)
        b = self.sigmoid(gamma - self.theta2)
        beta = np.dot(self.W3, b)
        outputs = self.sigmoid(beta - self.theta3)
        
        error = np.linalg.norm(targets - outputs, ord=2) ** 2 / 2
        
        return alpha, a, gamma, b, beta, outputs, error
        
    def back_propagate(self, inputs, targets, alpha, a, gamma, b, beta, outputs):
        
        g3 = (targets - outputs) * outputs * (np.ones(len(outputs)) - outputs)
        g2 = np.dot(self.W3.T, g3) * b * (np.ones(len(b)) - b)
        g1 = np.dot(self.W2.T, g2) * a * (np.ones(len(a)) - a)
        
        self.W3 += self.lr * np.outer(g3, b)
        self.theta3 += -self.lr * g3
        self.W2 += self.lr * np.outer(g2, a)
        self.theta2 += -self.lr * g2
        self.W1 += self.lr * np.outer(g1, inputs)
        self.theta1 += -self.lr * g1
        
    def train(self):
        
        convergence = False
    
        for _ in range(self.max_iter):
            for idx in range(len(self.X)):
                inputs, targets = self.X[idx], np.zeros(len(self.classes))
                targets[list(self.classes).index(self.y[idx])] = 1
                
                alpha, a, gamma, b, beta, outputs, error = self.propagate(inputs, targets)
                
                if error <= self.tol:
                    convergence = True
                    break
                else:
                    self.back_propagate(inputs, targets, alpha, a, gamma, b, beta, outputs)
            if convergence:
                break
        
        if not convergence:
            raise RuntimeError('神经网络未收敛,请调大max_iter的值')
        
    def predict(self, sample):
        
        alpha = np.dot(self.W1, sample)
        a = self.sigmoid(alpha - self.theta1)
        gamma = np.dot(self.W2, a)
        b = self.sigmoid(gamma - self.theta2)
        beta = np.dot(self.W3, b)
        outputs = self.sigmoid(beta - self.theta3)
        

        return self.classes[idx = list(outputs).index(max(outputs))]

我们将其保存在tnn.py里面。

3.实战模拟

在此,我们仅用sklearn中的鸢尾花数据集来训练并测试神经网络

完整代码如下:

from dnn import Neural_Network #导入我们的神经网络
from sklearn.model_selection import train_test_split  #划分数据集和测试集
from sklearn.metrics import accuracy_score            #计算分类准确率
from sklearn.datasets import load_iris                #鸢尾花数据集
from sklearn.preprocessing import MinMaxScaler        #数据预处理
import numpy as np


def test(bpnn, X_test, y_test):
    y_pred = [bpnn.predict(X_test[i]) for i in range(len(X_test))]
    return accuracy_score(y_test, y_pred)


X, y = load_iris(return_X_y=True)
scaler = MinMaxScaler().fit(X)
X_scaled = scaler.transform(X)
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42)

bpnn = Neural_Network(X_train, y_train, 10, 8, max_iter=3000)#将第一个隐层神经元数据设为10,将第二个隐层神经元的数目设为8
bpnn.train()

print(test(bpnn, X_test, y_test))
#0.9666666666666667

最后我们得到神经网络在测试集上的分类准确率达96.666666…%

尽管后续的调参采用了多个数据,但是分类准确率均为96.666…%,可能双隐层神经网络在小数据规模的数据集上分类效果并没有单隐层的神经网络好。也欢迎各位大佬在评论区提供改进建议以及分享不同思路。

Reference

[1]机器学习. 周志华

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2022年3月21日 下午4:18
下一篇 2022年3月21日 下午4:41

相关推荐