3. 神经网络基础:计算方法及其python实现
在前一节笔记中,我们大概了解了神经网络的数学原理——它是如何判断一张图是不是“猫图”,以及它是如何实现炫酷的“学习、训练”过程的。
那么我们想要在计算机中实现它,应该怎么做呢?
// 如果看不懂就先复习高等数学的导数及偏导数、链式法则部分,线性代数的多元方程矩阵计算部分。
之前我们知道了,“猫图识别”的过程,其实就是以图片为输入,经过逻辑回归计算后,将其进行二元分类,最后输出其是“猫图”概率的一个过程。
这其中有一系列的参数(WT和b)参与,而这些参数正是决定神经网络能否准确识别图片的关键。
由此我们又找到了一个名叫成本函数的函数,来评价这些参数的效果,从而通过凸优化,通过增加输入图片的数量,让神经网络不断进行优化,找到最佳的参数组。这就是神经网络“训练”的过程。
为了更深入的理解这个过程,在这里我们要引出一个概念:
一切神经网络算法,都是按照前向传播(Forward propagation)或者反向传播(Back propagation)算法进行计算的。
1、前向传播(Forward propagation)和反向传播(Back propagation)
让我们从一个简单的例子来理解什么是前向传播,什么是反向传播。
假设我们要计算这样的一个函数:
要计算这个函数,实际上我们要进行三个步骤:
通过流程图可以看出,我们完成了这样的一个计算:
它实际上就是我们的输入,通过计算,得到输出的过程,它从前向后顺序进行,将变量a、b、c组合成了函数u、v,然后再组成我们最终需要的函数J。因此这个过程被称作“前向传播(Forward propagation)”过程。
我们的“猫图识别”程序也是同样的过程,只不过它的输入是一个/一组图片,不像上例那样只有a、b、c那么简单而已。
其中,我们最后得到的输出y帽的逻辑回归函数,就像最后的J = 3v = 3(a + bc)一样,是一个由输入经过计算的函数。
但是我们最终要用梯度下降法来优化我们的算法。而在梯度下降法中,需要对每个输入变量求偏导数,才能算出其梯度:
为了方便说明,我们再回到上面J = 3(a + bc)的例子中。
在这个例子中,我们如果想求J对b的偏导数(a和c同理),从而计算出梯度,根据链式法则,我们需要这样做。
1、求出J对v的导数。
2、求出v对u的偏导数(因为v不仅是由u,也是由a构成的)。
3、求出u对b的偏导数。
这是一个从后往前的,通过链式法则不断求导的过程,因此这个过程被称作“反向传播(Back propagation)”过程。
所以可以看出,我们之前笔记中,给定一个输入x,通过逻辑回归进行二元分类,得到输出结果y帽,就是一个前向传播(Forward propagation)过程。而通过梯度下降法,对成本函数进行优化从而“训练”神经网络的过程,就是反向传播(Back propagation)过程。
回到“猫图识别”问题,我们在逻辑回归中,具体又是怎样实现梯度下降优化的呢?
2、逻辑回归中的梯度下降法的计算
回顾一下逻辑回归的数学原理,有三个核心公式:
其中:
它们都是维度为nx的向量。
公式1、2是用于预测的逻辑回归公式,我们可以给它一个输入x,进而得到输出y帽,来判断图片是不是“猫图”,而公式3则用于评价我们的逻辑回归给出的输出有“多么正确”,让我们知道得出的结果与实际结果的误差。
当然它不能凭空预测,需要先“学习”什么是“猫图”,因此我们需要先给它一个训练集,这时候训练集中每个输入的图片x(i)是固定的,我们要改变和调整的是参数wT和b,最终找到一组wT和b让我们输出的y帽与实际的结果尽可能的贴近。
而调整参数wT和b的依据就是梯度下降法,像小球滚入山谷那样,找到参数wT和b的最优解。
这一过程的流程图是这样的:
1、我们需要先选取一组初始的参数wT0和b0,作为滚入山谷的小球的起点。
我们已经有了用于训练的样本数量为m的训练集,我们先从第一个样本开始,以它作为输入x(1)
它已经是固定的了,而我们接下来几步要关注的是参数wT和b。先用它们求出z。
2、再由z求出“猫图”的预测概率值y帽,为了方便描述,我们令y帽 = a。
3、当然我们随意选取的初始点wT0和b0是不合适的,我们需要将a(即y帽)与实际值y进行比较,计算损失函数L(a,y),衡量我们预测的误差。
这一过程,是逻辑回归中的一个前向传播过程。只要一步一步计算就可以了,比较容易理解。
4、这时我们得到了初始点wT0和b0处,含有m个样本的训练集中,第一个样本x(1)的损失函数L(a(1),y(1))(有点绕),但是我们需要总结的是整个训练集中所有样本的规律,因此我们需要从第一个样本开始,重复上面的过程,直到计算出从x(1)到x(m)每个样本的损失函数。
然后就能计算出在训练集的全局损失,也就是每个样本的损失函数的累加平均,即成本函数:
对于成本函数的计算,我们有一个很方便计算的巧妙方法,我把它放在下一步的梯度下降中一起说明。
5、接下来,我们就要计算出成本函数的梯度,进而通过梯度下降法,在初始的wT0和b0处找到我们下一步的方向,来跨出逼近最优解的第一步,到达wT1和b1。
正常情况下,我们要求J(wT , b)对w1,w2,...,wn,b的偏导数,进而求出梯度,但是我们在这里不这么做。
成本函数作为损失函数的累加平均,它对于参数组w1、w2 、...、wn、b的偏导数,就等于损失函数L(a,y)对参数组w1、w2 、...、wn、b的偏导数的累加平均(为什么?)。以对w1求偏导为例:
所以我们只要分别求出第一个样本x(1)的损失函数L(a(1),y(1))对w1、w2 、...、wn、b的偏导数,然后再累加后除以样本数m作平均就可以了。
这时我们开始了我们的一个反向传播过程,依旧以w1为例,从流程图来看,我们的步骤是这样的:
- 先求损失函数L(a,y)对a=σ(z)的导数,记做da:
- 再求a=σ(z)对z的导数,从而的得到损失函数对z的导数,记做dz:
- 最后求出z对w1的偏导数,从而得出损失函数对w1的偏导数,记做dw1:
因为dz = a - y是一个很好计算的量,x1是我们输入的样本,a是我们输入样本x1后得到的输出,而y就是真实值,因此我们只计算到这里就能求出损失函数对w1的偏导数的值了。对于dw2,...,dwn,db也同理可以求出:
这时我们就可以求出第一个输入样本x(1),对参数w1,w2,...,wn,b的偏导数:
对于后面的第2,3,...,i,...,m个样本,也是同样的公式,只是上角标(1)的变化而已。
还记得我们要求的成本函数的偏导数公式吗?
它是对全部m个样本的偏导数的累加平均,所以接下来我们要重复这个过程,直到求出从第一个样本到第m个样本,所有m个样本的损失函数对参数w1,w2,...,wn,b的偏导数,然后分别累加再除以样本数m,作累加平均:
进而得到在初始的wT0和b0处,成本函数的梯度是:
求出梯度,我们就知道了我们下一步的方向,然后就要更新我们的参数wT和b变成wT1和b1,移动到下一点处。
6、重复上面1~5的过程,不断的更新参数wT和b,直到达到最优解。
现在我们已经基本理清了“猫图识别”的思路,以及具体实现的步骤,那么该如何在python上实现它呢?
//对于这门课,在学到这里为止,个人认为其核心永远是数学。但是在大量数据的运算时,人力是无法去计算的,所以代码只是为了能够让我们利用计算机的运算能力来计算出结果的工具而已。大概这也是为什么Andrew Ng说深度学习是一个随着近年计算机硬件性能提升才重新火起来的课题。而选用python,可能就是因为作为一个工具,它比其他语言更简单易学,容易使用吧,工具永远只是工具,哪种工具能更好的为我们实现目标创造条件,我们就选用哪种。18/12/26。//
3、逻辑回归以及梯度下降法的python实现
在上一节,我们用6步来利用逻辑回归进行了二元分类,然后再用梯度下降法进行了优化。接下来要做的就是在python上,把这6步逐步实现。
我们以输入是含有m个如
的样本的样本集为例,方便说明。
- 第1步:初始化权重(参数wT和b),计算z = wTx + b
python代码:
x = [3, 4] # 假设的输入,实际的输入x是训练集图片,不由我们自己定义
w_1 = 0.01; w_2 = 0.01 # 初始化w不能为0
w = [w_1, w_2];
b = 1; J = 0; dw_1 = 0; dw_2 = 0; db = 0 # 初始化各权重及参数
for i in range(len(x)): # 遍历两个向量的所有元素,计算w^T*x
z = w[i] * x[i]
z = z + b # 计算z = w^T*x + b
print ("z = w^T*x + b = " + str(z)) # 检查计算出的z
输出结果:
z = w^T*x + b = 1.04
注意这里的权重不能全部初始为0,比如将wT和b全部初始为0则会造成z = w^T*x + b = 0,导致所有神经元输出均相同,使整个网络无效。具体的初始化方法可以参照下面的链接:
- 第2步:由计算出的z,计算其sigmoid函数的输出a(即y帽)。
python代码:
x = [3, 4] # 假设的输入,实际的输入x是训练集图片,不由我们自己定义
w_1 = 0.01; w_2 = 0.01
w = [w_1, w_2];
b = 1; J = 0; dw_1 = 0; dw_2 = 0; db = 0 # 初始化各权重及参数
for i in range(len(x)): # 遍历两个向量的所有元素,计算w^T*x
z = w[i] * x[i]
z = z + b # 计算z = w^T*x + b
print("z = w^T*x + b = " + str(z)) # 检查计算出的z
import numpy as np # 我们需要利用numpy中的函数来计算e的x次幂
def sigmoid(x): # 定义sigmoid函数,返回函数值
y = 1.0 / (1.0 + np.exp(-x))
return y
a = sigmoid(z)
print("a = sigmoid(z) = " + str(a)) # 检查计算出的a
输出结果:
z = w^T*x + b = 1.04
a = sigmoid(z) = 0.7388500060842489
- 第3步:计算出损失函数L(a,y)。
python代码:
x = [3, 4] # 假设的输入,实际的输入x是训练集图片,不由我们自己定义
y = [1] #假设第一个样本的标记
w_1 = 0.01; w_2 = 0.01
w = [w_1, w_2];
b = 1; J = 0; dw_1 = 0; dw_2 = 0; db = 0 # 初始化各权重及参数
for i in range(len(x)): # 遍历两个向量的所有元素,计算w^T*x
z = w[i] * x[i]
z = z + b # 计算z = w^T*x + b
print("z = w^T*x + b = " + str(z)) # 检查计算出的z
import numpy as np
def sigmoid(x): # 定义sigmoid函数,返回函数值
y = 1.0 / (1.0 + np.exp(-x))
return y
a = sigmoid(z)
print("a = sigmoid(z) = " + str(a)) # 检查计算出的a
l = -(y[0] * np.log(a)) + (1-y[0]) * np.log(1-a) # 计算损失函数
print("L(a,y) = " + str(l)) # 检查计算出的L(a,y)
输出结果:
z = w^T*x + b = 1.04
a = sigmoid(z) = 0.7388500060842489
L(a,y) = 0.30266034739773895
- 第4步:遍历所有m个样本,直到计算出所有样本的损失函数,并计算出成本函数。
这里我稍微调整了一下上面代码的顺序,让代码易读一些。
python代码:
import numpy as np
def sigmoid(x): # 定义sigmoid函数,返回函数值
y = 1.0 / (1.0 + np.exp(-x))
return y
X = [[2, 3],
[3, 4],
[1, 5]] # 三个样本的样本集
y = [1, 0, 1] #三个样本对应的标签
w_1 = 0.01; w_2 = 0.01 #初始化w
w = [w_1, w_2];
b = 1; J = 0; dw_1 = 0; dw_2 = 0; db = 0 # 初始化各权重及参数
for i in range(len(X)): #遍历样本集
x = X[i] # 其中第i个样本
for j in range(len(x)): #计算z = w*x + b
z = w[j] * x[j]
z = z + b
a = sigmoid(z) #计算逻辑回归
print("a" + str(i + 1) + " = " + str(a))
L = -((y[i] * np.log(a)) + (1-y[i]) * np.log(1-a)) #计算损失函数
print("L" + str(i + 1) + " = " + str(L))
J = J + L #累加损失函数
dz = a - y[i]
dw_1 = dw_1 + x[0] * dz # 累加dw_1
dw_2 = dw_2 + x[1] * dz # 累加dw_2
# ... #
# 输入的样本x维度是多少,就有多少个dw和x[]
db = db + dz # 累加db
J = J / len(X) # 累加后的平均
dw_1 = dw_1 / len(X)
dw_2 = dw_2 / len(X)
db = db / len(X)
至此我们得到了:
接下来我们就可以通过计算梯度,进而进行优化了。
- 第5步:梯度下降
现在我们需要向最优点方向,跨出第一步,更新参数wT和b。
来回顾一下这一步的数学原理:
这里我们需要设置一个合适的学习速率(待填坑)。
python代码:
w_1 = w_1 - learning_rate * dw_1 # 更新参数
w_2 = w_2 - learning_rate * dw_2
b = b - learning_rate * b
这样我们的参数就向着最优解的方向移动了一步。
接下来我们需要用更新后的参数,重复上面的步骤,计算出损失函数,如果我们的计算没有错误,此时我们的损失函数应该比之前小了,也就是说我们的模型给出的预测值,更贴近真实值了。接下来只需要不断的重复这一步,直到逼近最优解,得到效果最好的模型。
现在我们已经理清了这个最简单的“猫图识别”神经网络的数学原理和程序代码,但是这代码之中还有一个问题:
import numpy as np
def sigmoid(x): # 定义sigmoid函数,返回函数值
y = 1.0 / (1.0 + np.exp(-x))
return y
X = [[2, 3],
[3, 4],
[1, 5]] # 三个样本的样本集
y = [1, 0, 1] #三个样本对应的标签
w_1 = 0.01; w_2 = 0.01 #初始化w
w = [w_1, w_2];
b = 1; J = 0; dw_1 = 0; dw_2 = 0; db = 0 # 初始化各权重及参数
for i in range(len(X)): #遍历样本集
x = X[i] # 其中第i个样本
for j in range(len(x)): #计算z = w*x + b
z = w[j] * x[j]
z = z + b
a = sigmoid(z) #计算逻辑回归
print("a" + str(i + 1) + " = " + str(a))
L = -((y[i] * np.log(a)) + (1-y[i]) * np.log(1-a)) #计算损失函数
print("L" + str(i + 1) + " = " + str(L))
J = J + L #累加损失函数
dz = a - y[i]
dw_1 = dw_1 + x[0] * dz # 累加dw_1
dw_2 = dw_2 + x[1] * dz # 累加dw_2
# ... #
# 输入的样本x维度是多少,就有多少个dw和x[]
db = db + dz # 累加db
J = J / len(X) # 累加后的平均
dw_1 = dw_1 / len(X)
dw_2 = dw_2 / len(X)
db = db / len(X)
这个代码中我们用了两个for循环,分别用来来遍历我们的输入样本集,以及样本集中某一个样本的所有分量。
上面的例子中,样本集中只有三个样本,每个样本也只有2维,因此很好计算。但是在实际的项目中,样本集数量很可能是成千上万的,而每个样本图片的维数也可能万级别的,显然这样会让这两个for循环的循环次数变得非常多,从而导致计算缓慢。
而神经网络就是在有大量的训练集数据时才有很好的表现,因此在训练大数据集时,计算速度往往非常重要。
那么有没有一种办法,可以摆脱for循环的方式,更高效的进行计算呢?
4、向量化(Vectorization)
什么是向量化?我们先举一个小例子:比如我们要计算z = wT * x + b,其中wT和x都是nx维向量。
如果我们用传统的非向量化方法,我们需要这样做:
z = 0
for i in len(x):
z = w[i] * x[i]
z = z + b
这个循环一共需要计算nx次。
之前我们为了计算sigmoid函数中的ex,引用过一个库:numpy。
这个库其实主要用来做向量计算,因此我们可以用numpy库的函数,以向量化的方式,这样来计算上面的for循环。
import numpy as np
z = np.dot(w,x) # 将w向量与x向量相乘
这样我们的代码就大大简化了,并且计算速度也得到了极大的提升。
来看一个非向量化计算与向量化计算对比的实例:
# 向量化方法
import numpy as np
import time
a = np.random.rand(1000000) # 随机生成维数为1,000,000的数组
b = np.random.rand(1000000)
tic = time.time() # 记录计算用时
c = np.dot(a,b)
toc = time.time()
print(c) # 输出结果证明两种方法效果相同
print("Vectorized version: " + str(1000*(toc - tic)) + " ms.")
# 非向量化方法
c = 0
tic = time.time()
for i in range(1000000):
c += a[i] * b[i]
toc = time.time()
print(c)
print("For loop: " + str(1000*(toc - tic)) + " ms.")
输出结果:
249870.39907406908
Vectorized version: 15.636205673217773 ms.
249870.39907406823
For loop: 750.1156330108643 ms.
可以看出两种方法计算出的结果相同(最后几位是随机数字,为什么?),但是用时却可能有几十到几百倍的差距。
因为当我们使用for循环时,计算机只能一个一个进行顺序计算,而用向量化方法进行计算时,计算机可以多个数据同时并行运算,因此速度被大大提升了。
向量化的方法可以在我们计算大量数据组成的数组或者向量时,节省非常多的时间。

浙公网安备 33010602011771号