搭建最简单的神经网络———从单层到多层
1.单层神经网络
1.1 介绍
我们在前面的课题中介绍了什么是感知器模型,还介绍了感知器模型的训练方法,已经损失函数和激活函数是什么。现在我们可以着手开始搭建自己的神经网络了。其实所谓神经网络就是多个感知器组合成的网络,如果只有一列就是单层,有多列就是多层,现在我们先从最简单的单层神经网络开始介绍。
1.2 单层神经网络的结构
上图就是一个单层神经网络的结构,当然或许你看到的是好几层,不只是三层,那下面我就来一一介绍一下每一层的作用。
第一层:第一层叫做输入层,就是,故名思意,输入层就是我们前面一章提到的感知器的输入部分,可以是图片的像素,也可以是电线杆的粗细和麻雀的数量等。
第二层:第二层叫做隐含层,它就是我们之前见到的感知器的中间部分,只不过这里变成了多个感知器,它这里放的就是我们的激活函数它的作用就是处理前面过来的输入,把它变成进行输出。在这里也就可以说明ReLU函数为什么可以逼近任何图形,这是因为每一个神经元(就是隐含层的小圆圈),里面的激活函数都是不一样的,所以许多个不同的ReLU在一起合成的图形就是一个非线性的图形,之所以不直接用线性模型是因为它不具有筛选能力。为什么是叫“隐含层”呢,因为作为程序员的我们,在程序运行起来的时候,只能看见输入和输出的结果,比如丢进去一张图片,出来一个模型,但是看不到中间的处理的结果,是不知道激活函数操作的每一个具体过程的,所以这一层就像是被隐藏了一样。
第三层:第三层读者肯定是可以猜到,就叫输出层,最后的圆圈和Y都是输出层,最后的圆圈把前面隐含层的输出整理一下(可以用一个激活函数来整合),变成最终的输出y。它和感知器的输出部分是一样。
我们可以看到,其实整个网络结构里面最复杂的部分,也是起到关键作用的部分就是隐含层,因为隐含层只有一层,所以这个网络也就叫做单层神经网络。(部分的人也叫它三层或者两层(两层是因为输出层也可能涉及激活函数)神经网络,我参考的资料还是以隐含层的数量来决定名称,读者可以根据自己的喜好来取名,名字不重要,重要的是知识能否被掌握)
1.3 单层神经网络的参数
其实神经网络的参数是什么,我们在之前已经学习过,就是权值和偏置数。每一个激活函数在接受输入的时候,他们接受的输入是加上了权值和偏执数的输入,所以在把输入放入激活函数中前,需要把其对应的和定义好,就如下图所示。
对上图的说明:
(1)隐含层和输出层都有激活函数,所以它们都需要设置好权值和偏置数。
(2)的n就代表他来自第几层。
(3)的n则是从隐含层开始,因为隐含层才开始有n。
(4)的i代表了它是该层的第几个单元。
(5)的代表的是它是下一层哪个节点的输入,代表它是上一层的哪个节点的输出(如果学过离散数学,应该知道i其实代表着这是前一个节点的第几出度,j代表着这是后一节点的第几入度),这样有一个规范以后,可以方便我们后续的学习(它和参数矩阵化后的坐标一致,后面会细讲)。
1.4 单层神经网络的向前传播
上一章我们学过感知器模型的向后传播,就是把输入的结果通过激活函数处理,之后再计算误差,计算新的权值传递回来。其中,把输入的结果往前丢给激活函数处理,激活函数处理以后又往前丢出输出,这就是一个向前传播。后面我们把输出处理以后,得到新的权值,把这个新的权值丢回最开始的地方,这就是向后传播。
其实单层神经网络向前还是向后的原理和感知器没有任何区别,为了进一步加深影响,我们还是从数学公式的角度来细讲一下什么是向前传播。如果你已经理解上面的那段话,那完全可以不看这里的内容。我们用刚才的图为例:
第一次向前传播为,从输入层向隐含层的传播,输入了,输出为:
第二次向前传播为把向前传播到输出层单元处理,输出最终结果:
当然,有时候输出不仅仅是一个,可以像第一次向前传播一样,列多个公式。
2. 训练单层神经网络的方法
2.1 介绍
我们在第一部分已经搭建了一个单层的神经网络模型,现在需要学习的就是如何去训练它,本部分十分的重要,因为这个训练方法在神经网络中是通用的,你学会了,你就可以去做自己的神经网络了。
2.2 梯度下降算法
梯度下降算法很关键,也很常用,是一个非常好用的算法。但是这个算法我们之前已经介绍过,就是我们之前说的训练感知器模型的方法。我们来用现有知识描述一下,并且附上一个测试代码描述:
(1)首先,我们假设我们的目标函数是,但是我们目前并且不知道这个z长这样样子,我们只知道输入的,输出的。我们知道有一个和一个,所以,我们的目标就是把这个函数猜测出来。我们先定义一下这个函数,记为funz:
def funcz(x,w,b):
"""
@Description:z的公式
"""
return w*x+b
(2)第二步,我们需要选择一个激活函数,这里我们选择Sigmoid函数
Sigmoid 的代码如下,需要用到numpy库,没有的在cmd里面pip install就行:
import numpy as np
def sigmoid(z):
y = 1.0/(1+np.exp(-z))
return y
(3)第三步,我们需要选择一个损失函数,这里我们选择使用均方差损失函数:
代码如下:
def mse(y,ey):
"""
@Description:y是输出的结果,ey是数学期望
"""
return ((y-ey)**2)/2
(4)第四步,也是最关键的一步,求,也就是求。
我们先求一个对的偏导数,根据复合函数求导法(也可根据链式求导法)可得:
对的求导,我们之前已经记过Sigmoid求导结果:
最后对求的偏导是很简单的:
把结果一到结果三代入公式一里面,可以得到最终结果,这也就是对以sigmoid为激活函数,用均方差为损失函数对求偏导的结果,看不懂没有关系,背下来即可:
这里给出其代码,我们就叫它delossw,用户可自己扩展到m组:
def delossw(y, ey,x):
return (y-ey)*y(1-y)*x
我们可以很容易的把它扩展至有(也就是有多个输入,那也就要有多个输出):
这里可能会有人有疑惑,因为我们发现它好像和无关,那岂不是如果有多个,每一个的变化都是一样的?其实不是这样的,因为我这里的例子只有一个,但是在实际情况下,至少都有2个,比如,可以看到不同的旁边的是不一样的,而这个公式是与有关的,所以不同的结果会不一样。同时这里的不是只这样,而是说有多组比如,所以对应的还是同一个。如果有多个,那么对每一个都要求一次偏导。
我们求对的偏导数,我们如法炮制可以得到:
我们不难发现,公式二和公式一几乎没有区别,唯一区别在于最后是,
我们只需要求一下:
所以,对求偏导的结果为:
代码如下,我们叫它delossb:
def delossb(y, ey):
return (y-ey)*y*(1-y)
扩展到m个输入(输出),也就有m个b:
这里有的人会认为delossw就是delossb在乘以x,但是要注意的是,仅仅在只有一组输如和输出时是这样的,如果有m组,那就不成立了,要分别写代码,因为:
(5)接下来,我们只需要计算和的变化值就可以了,公式如下:
代码如下,因为是向后传播,所以叫goback,学习率我设了0.9,因为发现运行的时候太慢了,但是设的太大可能导致无法收敛,所以要自己慢慢调整,后面会细讲:
def goback(w,b,x,y,ey):
"""
:param w: 当前的w
:param b: 当前的b
:return: 新的wn,bn
"""
a = 0.9 # 学习率
db = delossb(y,ey) # b的变化值
dw = db*x # w的变化值,注意,只有单输入输出时可以这么写
wn = w - a*dw # 新的w
bn = b - a*db # 新的b
return wn,bn
(6)到了最后,我们就可以开始训练了,训练过程很简单,每次都计算一下损失函数loss,如果太大了就继续训练,直到loss可以到能接受的范围,代码如下和一开始可以是任意值:
def net(w=1, b=1):
while 1:
"""神经网络模型,可以先输入一组w和b"""
z = funcz(1,w,b)
y = sigmoid(z)
ey = sigmoid(9)
loss = (mse(y, ey))
print(loss)
if loss>=0.00000001:
w,b = goback(w,b,x,y,ey)
else:
print(w,b)
break
注意,如果你用的语言也是python,那千万别用递归的写法,如果递归会出现超过最大递归上限的错误(因为我们需要递归上千万次,但是这个超过python最大栈的阈值了),而且尾递归优化无效,这是因为python编译器没有进行尾递归优化(听不懂也没有关系,记得别递归就行,真的想知道就百度一下,我用网上找到装饰器也没有解决这个问题,所以直接放弃),如果用while循环则没有任何问题。
运行结果如下:精度是0.00000001时,它把数据逼近到都是4.1和4.1
这是因为我把x设为了1,所以公式一和公式二结果是一样的,如果我们把x换为2,ey变成12,结果会好很多,5.5和3.2不太准确的原因是只有一个输入,所以有多个结果:
所以处理回归问题,尽可能别让输入为1。
整体代码如下,可以复制去pycharm上运行,感受一下:
@FileName:Simple_net.py
@Description:一个简单的神经网络模型:我们假设目前有一个公式是 z = 3x+6,将使用梯度下降的方法来获得这个公式
@Author:段鹏浩
@Time:2023/3/2 22:27
"""
import numpy as np
import sys
# sys.setrecursionlimit(1000000) # 尝试用它解决递归问题,失败了
def funcz(x,w,b):
"""
@Description:z的公式
"""
return w*x+b
# 我们目前已经知道的输入有:
x = 2
# 那么输出就是:
ey = 12
# 我们选择Sigmoid函数作为我们的激活函数,Sigmoid函数可以这么定义
def sigmoid(z):
y = 1.0/(1+np.exp(-z))
return y
# 同时,我们选择均方差函数作为我们的损失函数,均方差函数如此定义:
def mse(y,ey):
"""
@Description:y是输出的结果,ey是数学期望\n
"""
return ((y-ey)**2)/2
# 对mse函数进行w求导可以得到,最后的偏导结果为:
def delossw(y, ey,x):
return (y-ey)*y(1-y)*x
# 对mse函数的b求偏导。可以得到:
def delossb(y, ey):
return (y-ey)*y*(1-y)
# 所以我们可以得出反向传播函数:
def goback(w,b,x,y,ey):
"""
:param w: 当前的w
:param b: 当前的b
:return: 新的wn,bn
"""
a = 0.9 # 学习率
db = delossb(y,ey) # b的变化值
dw = db*x # w的变化值
wn = w - a*dw # 新的w
bn = b - a*db # 新的b
return wn,bn
def net(w=1, b=1):
while 1:
"""神经网络模型,可以先输入一组w和b"""
z = funcz(1,w,b)
y = sigmoid(z)
ey = sigmoid(12)
loss = (mse(y, ey))
print(loss)
if loss>=0.00000001: # 比较一下看看精度是否达标
w,b = goback(w,b,x,y,ey)
else:
print(w,b)
break
if __name__ == "__main__":
net(1,1)
当然,这个代码是有缺陷的,因为它只能从低往高逼近,如果一开始和设置的太大(大于5),代码就不能工作了,这是因为我们前面提到的Sigmoid函数具有数值较大时发生梯度消失的特性,后面我们将学习如何解决这样的情况。
3.反向传播算法
3.1 介绍
反向传播算法的思想是很重要的,所以即便你在上面的内容中已经认为自己十分理解反向传播的思想,也一定要在这里再巩固一下,因为无论是面试还是工作甚至是做研究,都会提到它。我们还是以刚刚的单层神经网络为例子:
在这个例子中我们以sigmoid函数为激活函数,用交叉熵函数当损失函数,我们的目标是把误差传递回去,用来更新各个权值。我们把误差记为,上标代表它在第几层,下标代表它是该层的哪一个节点传回来的误差。现在我们来一层层地分析一下。
3.2 输出层
输出层的误差只有一个节点,所以误差记为,所以可以得到公式,其实就是上面的梯度下降:
为什么是这样的呢,其实是用梯度下降推导来的:
我们来一一算一下交叉熵函数的导数,很好求:
之后的Sigmoid函数的导数我们是记得的:
最后:
所以:
我们可以看到一开始,在隐含层和输出层之间的参数并不是而是,所以在这里的规则是,到后面输入层和隐含层之间的参数才变成,这时的规则才是
这里我们可以发现,使用交叉熵和sigmoid配合的好处就是,我们只需要使用减法就能得到的就可以完成梯度下降的计算,不需要求导。
3.3 更新隐含层和输出层的权值
更新的话为了方便,直接使用,来对每一个权值都进行更新,需要用到的就是隐含层的各个y值,是学习率:
3.4 计算出隐含层各个节点的误差
更新好隐含层和输出层各个节点的权值以后,就可以把误差继续往后传递,用到的是刚刚更新好的权值(因为已经修正过一遍,传下去的时候误差会减小一些,如果先用原本的和输出层的误差相乘,那么误差会被拉大)和输出层传过来的误差相乘,公式如下,这样,我们就计算出了隐含层每个节点的误差值:
3.5 更新输入层和隐含层之间的权值
输入层和隐含层之间的权值的更新方法和前面更新隐含层和输出层之间权值的方法一样:
这是和相连的权值:
这是和相连的权值:
3.6 误差的反向传播总结
(1)计算输出层的误差;
(2)更新隐含层和输出层之间的权值:;
(3)用刚刚更新的权值和误差来计算隐含层的误差:;
(4)用隐含层的误差来更新输入层和隐含层之间的权值:;
4.简单的多层神经网络
4.1 什么是多层神经网络
在前面,我们介绍了单层神经网络,以及如何训练这个单层的神经网络,我们可以在前面的代码中看到,单层的神经网络的训练精度不是那么高,即便是设置精度到0.000001,训练200000万次,结果也不是那么好,而且关键的是sigmoid还会出现梯度消失问题,以及之前提到的ReLU函数是否有效的问题,都可用多层神经网络解决,因为一旦搭建了多层神经网络,就生成了更加复杂的数学结构,足以模拟任何的图形。其实多层神经网络的原理很简单,就是隐含层数目的增加,图形如下:
一般而言,神经网络的层数,也就是隐含层越多,这个网络的精度也就越高,但是也就越复杂,代码写起来也就更困难,因为参数实在是太多了,我们可以用参数向量化的方法来规范参数的使用。同时,当神经网络的隐含层大于等于三层的时候,我们将其称之为深度学习。
4.2 什么是矩阵
首先,如果你没有学过线性代数,那么我们在学习向量参数化前,需要首先了解一下什么是矩阵。在数学中,矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合。由 m × n 个数aij排成的m行n列的数表称为m行n列的矩阵,简称m × n矩阵,记作A:
这m×n 个数称为矩阵A的元素,简称为元,数aij位于矩阵A的第i行第j列,称为矩阵A的(i,j)元,以数 aij为(i,j)元的矩阵可记为(aij)或(aij)m × n,m×n矩阵A也记作Amn。元素是实数的矩阵称为实矩阵,元素是复数的矩阵称为复矩阵。而行数与列数都等于n的矩阵称为n阶矩阵或n阶方阵 。
4.3 矩阵基本运算法则:
现在我们有一个矩阵A一个矩阵B
(1)加法:A+B,同位置相加就行,没有的加0
(2)减法:和加法相同
(3)数乘:如果是一个实数,那么:
(4)相乘:两个矩阵的乘法仅当第一个矩阵A的行的长度和另一个矩阵B的列的长度相等时才能定义,因为矩阵相乘就是用A的每一行乘以B的每一列对应元素,然后求和。如A是m×n矩阵和B是n×p矩阵,它们的乘积C是一个m×p矩阵。例如:A是一个2x3(两行三列)的矩阵,B是一个3x2(三行两列)的矩阵,可以得到一个2x2的矩阵:
4.4 参数向量化
在掌握了什么是矩阵,以及矩阵的基本运算法则之后,我们开始学习参数的向量化。首先,向量就是有方向的量,可以理解为一维的矩阵,比如(x,y,z),x,y,z代表空间中的一条从原点发出的射线,它是有方向的。所以,我们也把一维的矩阵(不管是横的还是竖着的),叫做向量,不管是矩阵还是向量,在计算机里面就是数组,所以很好计算。我们将从向前传播和向后传播两个方面学习和理解参数的向量化。我们以n层神经网络,且忽略偏置数b为例,并且用sigmoid函数作为隐含层和输出层的激活函数(且假设输入都大于0),用交叉熵函数作为损失函数。用来参数化的例子,还是原来这张图:
1.向前传播:
(1)从输入到隐含层:
我们可以把和,用矩阵的形式列出来:
我们可以看到,只需要让就可以得到上面的公式中的数值,4x2和2x1的矩阵,可以得到一个4x1的矩阵,也就是一维数组,我们只需要遍历该数组,一一放入激活函数中即可。
(2)从隐含层到其他隐含层:
(3)从隐含层到输出层:
其实下面两个也是一样的,把上面的换成对应的就可以。
2.反向传播:
反向传播也特别简单,因为就三个公式:
(1)计算输出层误差
如果y有多个,可以形成一个向量,那么也写成一个向量就可以,可以写多个重复的。
(2)计算隐含层误差
这里插个题外话,为了防止弄混,这里申明一下,正向传播时,是拿和计算的,反向传播时,是拿和计算的。
于是我们只需要把和的数值排列成矩阵,然后进行矩阵乘法就行。
(4)权值更新
其中,不仅涉及到矩阵的乘法,还有矩阵的数乘运算,参考上面即可。
4.5 Python如何操作矩阵
我们用一个例子来描述矩阵的创建,以及各种运算。
目前我们有两个矩阵:
(1)创建矩阵,这时候我们需要用到numpy库
import numpy as np
之后我们创建A和B,用到的是numpy库的mat函数,注意,传入的必须是一个矩阵,所以[]不能漏,而且只能传入一个,所以是数组嵌套数组的形式:
"""
@FileName:matrix.py
@Description:矩阵的基本运算
@Author:段鹏浩
@Time:2023/3/5 14:31
"""
import numpy as np
A = np.mat([[1,3,5],[7,9,11]])
B = np.mat([[2,4],[6,8],[10,12]])
if __name__ == "__main__":
print(A)
print("")
print(B)
输出结果如下:
(2)矩阵加法:
直接加就行,需注意,这里做加法,两个矩阵规模必须一样,不然会报错,你只能手动补零,让两个一样
"""
@FileName:matrix.py
@Description:矩阵的基本运算
@Author:段鹏浩
@Time:2023/3/5 14:31
"""
import numpy as np
A = np.mat([[1,3,5],[7,9,11]])
B = np.mat([[2,4],[6,8],[10,12]])
C=A+A
if __name__ == "__main__":
print(C)
结果是:
(3)矩阵乘法:和加法一样的
"""
@FileName:matrix.py
@Description:矩阵的基本运算
@Author:段鹏浩
@Time:2023/3/5 14:31
"""
import numpy as np
A = np.mat([[1,3,5],[7,9,11]])
B = np.mat([[2,4],[6,8],[10,12]])
C=A-A
if __name__ == "__main__":
print(C)
结果如下:
(4)数乘运算:直接乘就行,注意:没有除法
"""
@FileName:matrix.py
@Description:矩阵的基本运算
@Author:段鹏浩
@Time:2023/3/5 14:31
"""
import numpy as np
A = np.mat([[1,3,5],[7,9,11]])
B = np.mat([[2,4],[6,8],[10,12]])
C=3*A
if __name__ == "__main__":
print(C)
结果如下:
(5)矩阵乘法:
也是直接乘就行,特别方便,当然,还有一种dot函数的用法,结果是一样的:
"""
@FileName:matrix.py
@Description:矩阵的基本运算
@Author:段鹏浩
@Time:2023/3/5 14:31
"""
import numpy as np
A = np.mat([[1,3,5],[7,9,11]])
B = np.mat([[2,4],[6,8],[10,12]])
C=A*B
D=np.dot(A,B)
if __name__ == "__main__":
print(C)
print("")
print(D)
结果如下:
(6)矩阵的另一种表示法,更加常用,是array函数的用法,但是做乘法只能用dot的方法:
"""
@FileName:matrix.py
@Description:矩阵的基本运算
@Author:段鹏浩
@Time:2023/3/5 14:31
"""
import numpy as np
A = np.array([[1,3,5],[7,9,11]])
B = np.array([[2,4],[6,8],[10,12]])
D=np.dot(A,B)
E=A+A
F=A-A
G=3*A
if __name__ == "__main__":
print(D)
print(E)
print(F)
print(G)
运行结果如下:
5.本章总结
在这一章,我们学习了单层神经网络和多层神经网络的结构,学习了梯度下降和反向传播的思想,并且写了一个简单的神经网络代码。同时还学习了参数向量法,以及如何进行矩阵的运算代码。下一章,我们将学习更复杂的多层神经网络----卷积神经网络的各种知识。