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

感知机(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函数与交叉熵

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容