生物信息学与算法算法深度学习与神经网络

numpy实现一个简单的神经网络(python3)

2019-08-23  本文已影响1人  __倔强的贝吉塔

感知机(Perceptron)

一种简单的感知机结构如下图所示,由三个输入节点和一个输出节点构成,三个输入节点x1,x2,x3分别代表一个输入样本x的三个特征值;w1,w2,w3分别代表三个特征值对应的权重;b为偏置项;输出节点中的z和o分别代表线性变换后的输出值和非线性变换后的输出值。

image

\begin{cases}z = x_1*w_1+x_2*w_3+x_3*w_3+b\\o=f(z)\end{cases} \tag{1}

其中映射函数f为激活函数,下面列几个常见的激活函数:

函数名 函数表达式 导数
sigmoid f(z)=\dfrac{1}{1+e^{-z}} f(z)[1-f(z)]
tanh f(z)=\dfrac{e^z-e^{-z}}{e^z+e^{-z}} 1-f(z)^2
softmax f(z)=\dfrac{e^{z_i}}{\sum_{j=0}^n e^{z_j}} 经常用其构成的
损失函数的导数:
f(z_i)-t(i)~ [1]

神经网络(Neural Network)

神经网络基本结构

神经网络与感知机类似,但是它的节点更加复杂,下图是一个含有1层隐藏层的神经网络,也是一种最简单的神经网络,我们可以看到这个神经网络的输入层有2个节点,隐藏层有3个节点,输出层有1个节点。我们可以认为神经网络由多个感知机构成。我们以下图所示结构为例,实现一个可以进行数据分类的神经网络。


image

假设我们有N个样本,对于每一个样本来说,都有两个特征值,对于这样的每一个样本\textbf{x}(x_1,x_2)都满足公式2,公式中带小括号的上标代表神经网络的层数,w_{ij}为相邻两层两个节点之间的权重系数,其中的i代表前一层的第i个节点,j代表后一层的第j个节点。

\begin{cases}z^{(1)}_{1} = x_1*w^{(1)}_{11}+x_2*w^{(1)}_{21}+b^{(1)}_1,~h_1=f(z^{(1)}_{1})\\z^{(1)}_2 = x_1*w^{(1)}_{22}+x_2*w^{(1)}_{22}+b^{(1)}_2,~h_2=f(z^{(1)}_2)\\z^{(1)}_3 = x_1*w^{(1)}_{13}+x_2*w^{(1)}_{23}+b^{(1)}_3 ,~h_3=f(z^1_3)\\z^{(2) }= h_1*w^{(2) }_{1}+ h_2*w^{(2) }_{2}+ h_3*w^{(2) }_{3}+b^{(2)},~o=f(z^{(2)})\end{cases}\tag{2}
我们可以用矩阵形式改写公式2:
\begin{cases}Z_1=X\cdot W_1+B_1\\ H=f(Z_1)\\ Z_2=H\cdot W_2+B_2\\ \hat Y=f(Z_2)\end{cases}\tag{3}

公式2中X_{[N\times2]}为输入矩阵,B_{1~[N\times3]}为隐藏层偏置矩阵,W_{1~[2\times3]}为输入层到隐藏层的权重矩阵,W_{2~[3\times1]}为隐藏层到输出层的权重矩阵,B_{2~[N\times1]}为输出层偏置矩阵,\hat{Y}_{[N\times1]}为输出矩阵(结果预测矩阵),Z_{1}H矩阵维度为N\times3Z_{2}矩阵维度为N\times1

神经网络损失函数

我们这里用改写的方差公式作为神经网络预测分类结果的损失函数,正确的分类结果矩阵记为Y_{[N\times1]}
func = \dfrac{1}{2N}*\sum_{i=1}^N{(\hat Y-Y)^2}\tag{4}
根据梯度下降法,我们需要求损失函数func的梯度,梯度下降法的实现可以看这里。损失函数可以表示为func=f(X,W_1,W_2,B_1,B_2)的形式(类似地,Z_1=f(X,W_1,B_1)Z_2=f(Z_1,W_2,B_2)),由于W_1,W_2,B_1,B_2是我们需要训练的参数,所以我们需要分别求funcW_1,W_2,B_1,B_2的梯度(这里涉及到矩阵的求导,见附录)。

\begin{cases} \dfrac{\partial func}{\partial W_2}=\dfrac{\partial func}{\partial Z_2}*\dfrac{\partial Z_2}{\partial W_2 } = \left( \dfrac{1}{N}*\sum_{i=1}^N{(\hat Y-Y)}\right)*\left(Z_1^T\cdot f'(Z_1,W_2,B_2) \right)\\ \\ \dfrac{\partial func}{\partial B_2}=\dfrac{\partial func}{\partial Z_2}*\dfrac{\partial Z_2}{\partial B_2 } = \left( \dfrac{1}{N}*\sum_{i=1}^N{(\hat Y-Y)}\right)* f'(Z_1,W_2,B_2) \\ \\ \dfrac{\partial func}{\partial W_1}=\dfrac{\partial func}{\partial Z_2}*\dfrac{\partial Z_2}{\partial Z_1 }*\dfrac{\partial Z_1}{\partial W_1 } = \left( \dfrac{1}{N}*\sum_{i=1}^N{(\hat Y-Y)}\right)*\left(f'(Z_1,W_2,B_2)\cdot W_2^T \right)*\left(X^T\cdot f'(X,W_1,B_1) \right)\\ \\ \dfrac{\partial func}{\partial B_1}=\dfrac{\partial func}{\partial Z_2}*\dfrac{\partial Z_2}{\partial Z_1 }*\dfrac{\partial Z_1}{\partial B_1 } = \left( \dfrac{1}{N}*\sum_{i=1}^N{(\hat Y-Y)}\right)*\left(f'(Z_1,W_2,B_2)\cdot W_2^T \right)*f'(X,W_1,B_1) \\ \end{cases}\tag{5}

根据梯度下降法,我们在求完梯度以后,需要更新我们的参数值,这里以W_1为例:
W_1 =W_1 - \eta*\dfrac{\partial func}{\partial W_1}\tag{6}
由公式6可以看出,W_1的梯度矩阵应该与W_1维度相同,即\frac{\partial func}{\partial Z_2}_{[1\times1]}*\frac{\partial Z_2}{\partial Z_1 }_{[N\times1]\cdot[3\times1]}*\frac{\partial Z_1}{\partial W_1 }_{[2\times N]\cdot[N\times3]}W_{1~[2\times3]}维度相同,因此N应该为1。所以我们在编程时应该一个样本一个样本的训练,而不是N个样本一起训练。当N=1时,公式5可以简化为:
\begin{cases} \dfrac{\partial func}{\partial W_2}=\dfrac{\partial func}{\partial Z_2}*\dfrac{\partial Z_2}{\partial W_2 } = (\hat Y-Y)*\left(Z_1^T* f'(Z_1,W_2,B_2) \right)\\ \\ \dfrac{\partial func}{\partial B_2}=\dfrac{\partial func}{\partial Z_2}*\dfrac{\partial Z_2}{\partial B_2 } = (\hat Y-Y)* f'(Z_1,W_2,B_2) \\ \\ \dfrac{\partial func}{\partial W_1}=\dfrac{\partial func}{\partial Z_2}*\dfrac{\partial Z_2}{\partial Z_1 }*\dfrac{\partial Z_1}{\partial W_1 } = (\hat Y-Y)*\left(f'(Z_1,W_2,B_2)* W_2^T \right)*\left(X^T\cdot f'(X,W_1,B_1) \right)\\ \\ \dfrac{\partial func}{\partial B_1}=\dfrac{\partial func}{\partial Z_2}*\dfrac{\partial Z_2}{\partial Z_1 }*\dfrac{\partial Z_1}{\partial B_1 } = (\hat Y-Y)*\left(f'(Z_1,W_2,B_2)* W_2^T \right)*f'(X,W_1,B_1) \\ \end{cases}\tag{7}
按照上述思路进行编程,这里隐藏层激活函数选择sigmoid函数,输出层激活函数选择tanh函数,得到分类结果的错误率为0.01~0.06,当隐藏层和输出层激活函数都选择tanh函数时,错误率更低。下图为错误率为0.025时的分类结果图。我们可以看到图中有5个数据点分类错误。

image

局限性

由于我们是一个样本一个样本训练的,所以我们得到的参数也是和这些样本一一对应的,因此这个模型无法画出决策边界,也无法预测新的数据,预测新的数据好像是应该对训练好的参数进行插值,但是我看别人没有那么做的,可能这样不大好。

附录

神经网络代码

# -*- encoding=utf-8 -*-
__Author__ = "stubborn vegeta"

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from matplotlib.colors import ListedColormap

class neuralNetwork(object):
    def __init__(self, X, Y, inputLayer, outputLayer, hiddenLayer=3,learningRate=0.01, epochs=10):
        """
        learningRate:学习率
        epochs:训练次数
        inputLayer:输入层节点数
        hiddenLayer:隐藏层节点数
        outputLayer:输出层节点数
        """
        self.learningRate = learningRate
        self.epochs = epochs
        self.inputLayer = inputLayer
        self.hiddenLayer = hiddenLayer
        self.outputLayer = outputLayer
        self.X = X
        self.Y = Y
        self.lenX,_ = np.shape(self.X)
        s=np.random.seed(0)
        # W1:输入层与隐藏层之间的权重;W2:隐藏层与输出层之间的权重;B1:隐藏层各节点的偏置项;B2:输出层各节点的偏置项
        self.W1 = np.array(np.random.random([self.inputLayer, self.hiddenLayer])*0.5)        #2*3
        self.B1 = np.array(np.random.random([self.lenX,self.hiddenLayer])*0.5)               #200*3
        self.W2 = np.array(np.random.random([self.hiddenLayer, self.outputLayer])*0.5)       #3*1
        self.B2 = np.array(np.random.random([self.lenX,self.outputLayer])*0.5)               #200*1

    def activationFunction(self, funcName:str, X):
        """
        激活函数
        sigmoid: 1/1+e^(-z)
        tanh: [e^z-e^(-z)]/[e^z+e^(-z)]
        softmax: e^zi/sum(e^j)
        """
        switch = {
                "sigmoid": 1/(1+np.exp(-X)),
                "tanh": np.tanh(X), 
                # "softmax": np.exp(X-np.max(X))/np.sum(np.exp(X-np.max(X)), axis=0)
                }
        return switch[funcName]

    def activationFunctionGrad(self, funcName:str, X):
        """
        激活函数的导数
        """
        switch = {
                "sigmoid": np.exp(-X)/(1+np.exp(-X))**2,
                "tanh": 1-(np.tanh(X)**2),
                # "softmax": np.exp(X-np.max(X))/np.sum(np.exp(X-np.max(X)), axis=0)
                }
        return switch[funcName]

    def train(self, funcNameH:str, funcNameO:str):
        """
        funcNameH: 隐藏层激活函数
        funcNameO: 输出层激活函数
        """
        for i in range(0,self.epochs):
            j = np.random.randint(self.lenX)
            x = np.array([self.X[j]])
            y = np.array([self.Y[j]])
            b1 = np.array([self.B1[j]])
            b2 = np.array([self.B2[j]])
            # 前向传播
            zHidden = x.dot(self.W1)+b1
            z1 = self.activationFunction(funcNameH, zHidden)  #1*3
            zOutput = z1.dot(self.W2)+b2
            z2 = self.activationFunction(funcNameO, zOutput)  #1*1 

            # 反向传播
            dW2 = (z2-y)*(z1.T*self.activationFunctionGrad(funcNameO,zOutput))
            db2 = (z2-y)*self.activationFunctionGrad(funcNameO,zOutput)
            dW1 = (z2-y)*(self.activationFunctionGrad(funcNameO,zOutput)*self.W2.T)*(x.T.dot(self.activationFunctionGrad(funcNameH,zHidden)))
            db1 = (z2-y)*(self.activationFunctionGrad(funcNameO,zOutput)*self.W2.T)*self.activationFunctionGrad(funcNameH,zHidden)

            #更新参数
            self.W2 -= self.learningRate*dW2
            self.B2[j] -= self.learningRate*db2[0]
            self.W1 -= self.learningRate*dW1
            self.B1[j] -= self.learningRate*db1[0]
        return 0

    def predict(self, xNewData, funcNameH:str, funcNameO:str):
        X = xNewData                                         #200*2
        N,_ = np.shape(X)
        yPredict = []
        for j in range(0,N):    
            x = np.array([X[j]])
            b1 = np.array([self.B1[j]])
            b2 = np.array([self.B2[j]])
            # 前向传播
            zHidden = x.dot(self.W1)+b1
            z1 = self.activationFunction(funcNameH, zHidden)  #1*3
            zOutput = z1.dot(self.W2)+b2
            z2 = self.activationFunction(funcNameO, zOutput)  #1*1 
            z2 = 1 if z2>0.5 else 0
            yPredict.append(z2)
        return yPredict,N


if __name__ == "__main__":
    X,Y = datasets.make_moons(200, noise=0.15)
    neural_network = neuralNetwork (X=X, Y=Y, learningRate=0.2, epochs=1000, inputLayer=2, hiddenLayer=3, outputLayer=1)
    funcNameH = "sigmoid"
    funcNameO = "tanh"
    neural_network.train(funcNameH=funcNameH,funcNameO=funcNameO)       
    yPredict,N = neural_network.predict(xNewData=X,funcNameH=funcNameH,funcNameO=funcNameO)
    print("错误率:", sum((Y-yPredict)**2)/N)
    colormap = ListedColormap(['royalblue','forestgreen'])              # 用colormap中的颜色表示分类结果
    plt.subplot(1,2,1)
    plt.scatter(X[:,0],X[:,1],s=40, c=Y, cmap=colormap)
    plt.xlabel("x")
    plt.ylabel("y")
    plt.title("Standard data")
    plt.subplot(1,2,2)
    plt.scatter(X[:,0],X[:,1],s=40, c=yPredict, cmap=colormap)
    plt.xlabel("x")
    plt.ylabel("y")
    plt.title("Predicted data")
    plt.show()

感知机结构图代码

digraph network{
edge[fontname="Monaco"]
node[fontname="Monaco"]
rankdir=LR
b[shape=plaintext] 
x1->"z|o"[label=w1]
x2->"z|o"[label=w2]
x3->"z|o"[label=w3]
b->"z|o"
{rank=same;b;"z|o"}
}

神经网络结构图代码

digraph network{
    edge[fontname="Monaco"]
    node[fontname="Monaco",shape=circle]
    rankdir=LR

    subgraph cluster_1{
        color = white
        fontname="Monaco"
        x1,x2;
        label = "Input Layer";
    }
    subgraph cluster_2{
        color = white
        fontname="Monaco"
        h3,h1,h2;
        label = "Hidden Layer";
    }
    subgraph cluster_3{
        // rank=same
        color = white
        fontname="Monaco"
        o;
        label = "Output Layer";
    }
    x1->h1
    x1->h2
    x1->h3
    x2->h1
    x2->h2
    x2->h3
    rank=same;h1;h2;h3
    h1->o
    h2->o
    h3->o       
}

矩阵求导公式

Y=A\cdot X~\Longrightarrow~ \dfrac{dY}{dX}=A^T Y=X\cdot A~\Longrightarrow~ \dfrac{dY}{dX}=A^T
Y=X^T\cdot A~\Longrightarrow~ \dfrac{dY}{dX}=A Y=A\cdot X~\Longrightarrow~ \dfrac{dY}{dX^T}=A
\dfrac{dX^T}{dX}=I \dfrac{dX}{dX^T}=I

  1. Softmax函数与交叉熵

上一篇下一篇

猜你喜欢

热点阅读