一个最简单的神经网络

神经网络基础

本文是基于我在阅读《神经网络与深度学习》一书之后自己的一些总结,书中举的第一个MNIST的例子的神经网络就已经很复杂了,对于没有机器学习的基础的读者话可能得花很长时间理解,于是我从里面扣了最简单的基本结构先来讲讲(不包含反向传播)

目前我们在神经网络的神经元主要是S型感知元,但在介绍这个之前,我想先介绍一下感知器,你可以理解为这是一个最简单的神经元,并且书中提到了一个非常有意思的东西是我特别想和读者分享的

感知器

首先我们来了解一下感知器:

感知器是一个人工神经元,可以接受一个或多个二进制输入(0或者1),并通过计算权重w以及偏置b来输出一个二进制结果

![image-20211010024705473](03 一个最简单的神经网络.assets/image-20211010024705473.png)

计算式如下:

![image-20211010030153582](03 一个最简单的神经网络.assets/image-20211010030153582.png)

这里偏置起到阈值(threshold)的作用(b = -threshold)很好理解吧。

重点来了假设我们有一个如下图所示的感知器,它对两个输入的权重为-2,偏置为3(其他合理值都可以)

![image-20211010030510628](03 一个最简单的神经网络.assets/image-20211010030510628.png)

我们的输入输出表为

x1 x2 output
0 0 1
0 1 0
1 0 0
1 1 1

所以说,我们通过感知器实现了一个与非门(NAND)!我们可以也就是说我们可以感知器网络实现任何电路,感知器和我们的电子设备一样强大,这是非常令人激动的

![image-20211010031818692](03 一个最简单的神经网络.assets/image-20211010031818692.png)

一个实现二进制加法的例子(我记得我大一的时候老师还讲过这个让我们画)

S型神经元

其实S型神经元和感知器的区别并不大,只是能接受和输出的值变为了[0, 1]这个区间了,而不是仅仅是0或者1了。这样我们就可以进行运算了

![image-20211010024705473](03 一个最简单的神经网络.assets/image-20211010024705473.png)

以上图感知器为例,如果我们将输入x转化为形状[1, 3]的单行矩阵,将感知器对x1, x2, x3的权重相加w转化为形状[3, 1]的单列矩阵,将输入分别计算权重相加再加上偏置,就得到了唯一的输出了。并且计算式我们就可以简单的写为output = w * x + b 了(题外话:因为python的numpy包可以快速的处理矩阵运算,并且在将来处理大量数据时可以借助GPU的算力来加速计算,所以学会转化成矩阵运算的思维是很重要的)

Sigmoid函数

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

很简单啦,就是将值转化为(0,1)区间内

至于为什么要用这个,它比越阶函数的好处是它处处可导,导函数是y' = y * (1-y)并且他的形式很简单,我就不多介绍了

搭建神经网络

我直接先把代码po出来了 (代码解释我用注释写在代码里了)

import numpy as np
from icecream import ic  # 这个用来调试代码用的,可以用print()代替ic()。但我个人比较喜欢用这个,对输出结果的排版会舒服很多


def sigmoid(x):
    return 1 / (1 + np.exp(-x))  # 前面讲过了


class Network(object):
    def __init__(self, size: tuple):
        """
        初始化神经网络
        :param size:神经网络的大小
        """
        self.size = size
        self.num_layers = len(size)  # 神经网络的层数,存储起来方便之后在做梯度下降时的运算
        self.w = [np.random.randn(j, i) for i, j in zip(self.size[:-1], self.size[1:])]  # 注意这里传入的是(j, i)这样方便计算,而且在print出来后是一行对应一个节点,里面的值是该节点分别对前一个结点的权重
        self.b = [np.random.randn(i) for i in self.size[1:]]  # 生成每一个节点的偏置
        # np.random.randn的意思是用随机正态分布取值
        # 因为第一层节点是输入层,所以它们没有对前一层的权重和偏置

    def feedforward(self, z):
        """
        前向传播
        :param z:data
        :return:result
        """
        x = z
        for w, b in zip(self.w, self.b):  # 用zip函数将每一层的w和b绑定在一起,方便逐层传播下去
            x = sigmoid(np.dot(w, x) + b)  # 计算式ouput = w * x + b外面再包一层sigmoid将值转化为(0, 1)区间
        return x

测试的结果(因为权重和偏置都是随机生成的,并没有用反向传播做梯度下降,所以结果是随机的,我随便用了一次结果来举例)

if __name__ == '__main__':
    net = Network((2, 3, 4, 5))  # 这里是一个四层的神经网络,接收两个数据,经过两层的隐藏层后,根据权重和偏置输出5个值
    ic(net.feedforward((32, 55)))
    
    
# output
ic| net.feedforward((32, 55)): array([0.93004331, 0.57397579, 0.73598641, 0.08318392, 0.41371676])

如果我们这个神经网络是用于分类,比如:给物体分成5类中的一个,我们传入这个物体的两个特定特征,将得到分别它是各个类的可能性

结果可以看出,我们如果将(32,55)传入这个神经网络中,在本次结果中很有可能是第一类(0.9300433),不太可能是第四类(0.08318392),可以自己在最后的结果加一个阈值判断

lambda p: 1 if p > threshold else 0

最后

其实原书(至少我手上的这个版本)的演示代码是python2版本,而且有很多奇怪的numpy用法,对MINIST手写识别的代码我已经跟着打了一遍,打算看看能不能改进一下里面的糟糕numpy用法再发出来,先写一小段简单的部分出来(其实已经很长了,剩下的要挨着挨着讲完要多长。。)

还有就是最近要忙着赶作业,和补之前基本没听的正课(。。),所以即使改完了也暂时不会写成文章发出来,感兴趣的话可以自己在下载电子书来看,非常不错。