神经网络与深度学习二:神经网络的基础编程

1 二分分类

当你要构建一个神经网络,有些技巧是相当重要的,例如m个样本的训练集,你可能习惯性地使用一个for循环来遍历这m个样本,但事实上,实现一个神经网络如果你要遍历整个训练集并不需要直接使用for循环。还有就是神经网络的计算过程中通常有一个正向过程或者叫正向传播步骤,接着会有一个反向步骤,也叫反向传播步骤,下面会介绍为什么神经网络的计算过程可以分为前向传播和反向传播两个分开的过程,接下来会用logistic回归来阐述以便于更好的理解

logistic回归是一个用于二分分类的算法,下面看一个例子

image-20220404172627820

计算机保存一张图片,要保存三个独立矩阵,分别对应图片中的红绿蓝三个颜色通道,如果输入的图片是64×64像素的,就有三个64×64的矩阵,分别对应图片中的红绿蓝三种像素的亮度,为了方便表示,这里用三个小矩阵(它们是5×4的并不是64×64)

要把这些像素亮度值放进一个特征向量中,就要把这些像素值都提出来放入一个特征向量x,为了把这些像素值取出放入特征向量,就要像下面这样定义一个特征向量x以表示这张图片,我们把所有的像素值都取出来例如255,231这些,最后得到一个很长的特征向量,把图片中的红绿蓝像素强度值都列出来,如果图片上64×64,那么向量x的总维度就是64×64×3=12288,用 n x n_x nx来表示输入的特征向量的维度,有时为了简洁会直接使用小写的n

在二分类问题中 目标是训练出一个分类器它以图片的特征向量x作为输入,预测输出的结果标签y是1还是0也就是预测图片中是否有猫
一 个 单 独 的 样 本 : ( x , y ) x 是 n 维 的 特 征 向 量 : x ∈ R n x 标 签 y 的 值 为 0 或 1 : y ∈ { 0 , 1 } m 个 训 练 样 本 : { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x m , y m ) } 一个单独的样本:(x,y) \\ x是n维的特征向量:x\isin R^{n_x} \\ 标签y的值为0或1: y \isin \{0,1\}\\ m个训练样本:\{(x^1,y^1),(x^2,y^2),...,(x^m,y^m)\} :(x,y)xn:xRnxy01y{0,1}m:{(x1,y1),(x2,y2),...,(xm,ym)}
最后用更紧凑的符号表示训练集

注意有时候矩阵X定义为,训练样本作为行向量堆叠,而不是这样列向量堆叠

但是构建神经网络时,用第一种约定方式会让构建过程简单很多

现在X是以一个 n x n_x nx×m矩阵,当你用python时X.shape会输出矩阵的维度( n x n_x nx,m)

这就是如何将训练样本,即输入x用矩阵表示。 那么输出标签y呢

同样为了方便构建一个神经网络将y也放到列中

2 logistic回归

已知输入特征向量x可能是一张图,你希望识别出这是不是猫图,你需要一个算法可以给出一个预测值, y ^ \hat{y} y^

就是你对y的预测,更正式的说 y ^ \hat{y} y^是一个概率,当输入特征满足条件时y就是1

换句话说如果x是图片,你希望 y ^ \hat{y} y^可以告诉你这是一张猫图的概率,已知logistic的参数是w和b

你可以直接做线性回归,但这不是一个非常好的二元分类算法,因为你希望 y ^ \hat{y} y^的是y=1的概率,所以 y ^ \hat{y} y^应该介于0和1之间。但实际上这很难实现,因为 y ^ = w t x + b \hat{y}=w^tx+b y^=wtx+b可能比1大得多 或者甚至是负值这样的概率是没意义的,你希望概率介于0和1之间,所以在logistic回归里我们的输出变成了sigmoid函数作用到这个量上

sigmoid函数的图像是

z= w t x + b w^tx+b wtx+b

可以看出当z是一个很大的数时(趋于无穷大),函数值趋于1,z是一个很大得负数时(趋于负无穷),函数值趋于0。所以当你实现logistic回归时你要做得就是就是学习参数w和b,所以 y ^ \hat{y} y^变成了比较好得估计,对y=1的比较好的估计。在继续之前我们讲讲符号约定

当我们对神经网络编程时,我们通常会把w和b分开,这里b对应一个拦截器,在一些其他地方你们可能看见过不同的表示,在一些符号约定中,你定义一个额外的特征向量叫 x 0 x_0 x0等于1,所以x就是 x ∈ R n x + 1 x\isin R^{n_x+1} xRnx+1维的向量,然后你将 y ^ \hat{y} y^定义为 y ^ = σ ( θ T x ) \hat{y}= \sigma(\theta^Tx) y^=σ(θTx),你有一个向量参数 θ \theta θ

θ 0 \theta_0 θ0扮演的就是b的角色,而 θ 1 \theta_1 θ1 θ n \theta_n θn的作用和w一样。事实上当你实现你的神经网络时,把w和b看作独立的参数可能更好。

好了接下来为了求出w和b你需要定义一个损失函数

3 logistic回归损失函数

image-20220409161947879

这里用上标(i)指明数据 x ( 1 ) x^{(1)} x(1)就是第一个样本,你可以这样定义损失函数

image-20220409162348160

但是通常在logistic回归中 大家都不这么做,因为当你学习这些参数的时候你会发现之后讨论的优化问题会变成非凸的,最后会得到很多个局部最优解,梯度下降法可能找不到全局最优值

如果你不理解这个问题也没关系,后面会进行详细的讲解,这个直观的理解就是我们通过定义这个损失函数L来衡量你的预测输出值 y ^ \hat{y} y^和y的实际值有多接近,误差平方看起来似乎是一个合理的选择,但用这个的话梯度下降法就不太好用,在logistic回归中我们会定义一个不同的损失函数,它起着与误差平方相似的作用,这会给我们一个凸的优化问题,在后面的文章中就会看到它很容易去做优化。在logistic回归中我们用的就是

image-20220409171331359

为什么这个损失函数能起作用?记得如果我们使用误差平方越小越好,对于这个logistic回归的损失函数同样的我们也想让它尽可能地小,为了更好的理解为什么它能起作用,让我们来看两个例子

在第一个例子中我们说y=1时,第二项就等于0,也就是说y=1时你想要 − l o g y ^ -log\hat{y} logy^尽可能的小,这意味着想让 l o g y ^ log\hat{y} logy^尽可能的大这样就意味着你想要 y ^ \hat{y} y^够大,因为 y ^ \hat{y} y^是由sigmoid函数得到得永远不会大于1,也就是说如果y=1时,你会想让 y ^ \hat{y} y^尽可能地大,但它永远不会大于1,所以你要让 y ^ \hat{y} y^接近1。

另一种情况如果y=0,那么第一项等于0,想让 − l o g ( 1 − y ^ ) -log(1-\hat{y}) log(1y^)足够得小,就是让 l o g ( 1 − y ^ ) log(1-\hat{y}) log(1y^)足够得大,也就是让 y ^ \hat{y} y^尽可能得小,也就是 y ^ \hat{y} y^接近0

image-20220409172222354

有很多函数都可以达到这个效果如果y=1我们让 y ^ \hat{y} y^尽可能得大,如果y=0让 y ^ \hat{y} y^尽可能的小。后面的文章中会解释为什么在logistic回归中要用这个形式的损失函数。最后说一下损失函数是在单个训练样本中定义的,它衡量了在单个训练样本上的表现,下面定义一个cost function它衡量的是在全体训练样本上的表现

image-20220409174555546

你刚刚看到了logistic回归算法的过程以及训练样本的损失函数还有和参数相关的cost function结果表明logistic回归可以被看作一个非常小的神经网络

4 梯度下降法

在上面的文章学习了logistic回归模型,也知道了损失函数(loss function)是衡量单一训练样例的效果,你还知道了成本函数(cost function)用于衡量参数w和b的效果,在全部训练集上来衡量。接下来就讨论如何使用梯度下降法来训练或学习训练集上的参数w和b

image-20220410101014437

第一行是我们熟悉的logistic回归算法,第二行是cost function(成本函数) J,J是参数w和b的函数,它被定义为平均值即1/m的损失函数之和,损失函数可以衡量你的算法的效果,每一个训练样例都输出 y ^ ( i ) \hat{y}^{(i)} y^(i)把它和真实标签 y ( i ) y^{(i)} y(i)比较。成本函数衡量了参数w和b在训练集上的效果,要习得合适的参数w和b,我们很自然的想要找到使成本函数J(w,b)尽可能小的w和b,下面来看看梯度下降法

这个图中的横轴表示空间参数w和b,在实践中w可以是更高维的,但为了方便绘图我们让w是一个实数,b也是一个实数,成本函数J(w,b)是在水平轴w和b上的曲面,曲面的高度表示了J(w,b)在某一点的值,我们所想要做的就是找到这样的w和b其对应的成本函数J值是最小值

可以看到成本函数J是一个凸函数,像这样的一个大碗,因此这是一个凸函数,看起来和下面的函数不一样,它是非凸的有很多不同的局部最优解

因此,这里定义的成本函数J(w,b)是凸的,这是我们使用这个特殊的成本函数J进行逻辑回归的一个重要原因之一,为了找到更好的参数值,我们要做的就是用某初始值初始化w和b

对于logistic回归而言任意的初始化方法都有效,通常用0进行初始化,随机初始化也有效,但对于logistic回归我们通常不这样做,但是因为函数是凸的,无论在哪里初始化都应该达到同一点或者大致相同的点。梯度下降法要做的就是从初始点开始朝最陡的下坡方向走一步,在梯度下降一步后或许在那里停下因为他正试图沿着最快下降的方向向下走,这是梯度下降的一次迭代。两次迭代或许就可以到达J最小的地方或者三次等等,隐藏在图上的曲线很有希望收敛到这个全局最优解或接近全局最优解

以上阐述了梯度下降法,接下来看一些细节,为了更好的说明,让我们来看一些函数,你希望最小化J(w),函数可能看起来像这样,为了方便画图先忽略b,用一维曲线代替多维曲线

我们将重复执行以下更新操作

用dw作为导数的变量名, α \alpha α表示学习率,学习率可以控制每一次迭代或者梯度下降法中的步长,我们之后会讨论如何选择学习率 α \alpha α。随机选择一个w,对于的J(w)如下图

导数是函数在这一点的斜率, w = w − α d w w=w-\alpha dw w=wαdw,向左移动,w逐渐减小

如果w很小, w = w − α d w w=w-\alpha dw w=wαdw 导数为负值,向右移动,w逐渐增大

无论你的初始化位置是在左边还是右边梯度下降法会朝着全局最小值的方向移动。如果你不熟悉积分和导数的概念,别担心,下面会介绍,让你对导数和微分有直观的认识。在logistic回归中你的cost function是一个含有w,b的函数,在这种情况下 梯度下降的内循环就是

你需要一直重复计算,更新w,b

在微积分中,当自变量是两个及以上时,会使用偏导数符号,其实就是只对其中的一个变量求导,把另一个当做常量

我们通常用变量dw表示J对w的导数,变量db表示J对b的导数

5 导数

下面看一个函数

a=2时,f(a)=6,现在稍微改变一点a

其实导数就是在这一点的斜率
Δ y Δ x \frac{\varDelta y}{\varDelta x} ΔxΔy

6 计算图

可以说神经网络的计算都是按照前向或反向传播来实现的,首先计算出神经网络的输出,接着进行一个反向传输的操作,后者我们用来计算出对于的梯度或导数,这个流程图解释了为什么用这样的方式实现。

先看一个简单的例子J(a,b,c)=3(a+bc) 我们可以分三步计算

假设a=5,b=3,c=2那么计算图就是

可以看出通过从左到右的一个过程你可以计算出J的值,在接下来我们会看到为了计算导数,从右到左的这个过程

这是用于计算导数最自然的方法,下面就看看这个从右到左的计算过程

7 使用计算图求导

首先求出J对v的导数,显然是3

那我们就做完了一步反向传播,接下来那么dJ/da是多少呢?

其实就是微积分里的链式法则,如果a影响到v影响到J,那么你让a增大时,J的变化量就是当你改变a时v的变化量乘以改变v时J的变化量。

可以显然看出dv/da=1,代入之前计算的dJ/dv,那么dJ/da=3×1=3

当你编程实现反向传播时通常会有一个最终输出值是你要关心的,最终的输出变量你真正想要关心或者说优化的,在这种情况下最终的输出变量是J,在很多情况下你需要计算输出变量的导数,对各种中间变量比如a,b,c,u,v的导数,当你编程实现的时候变量名一般是将dJ/dv 命名为dv,dJ/da命名为da,因为你一直在对dJ求导

到目前为止我们一直在往回传播,并计算出dv=3,dv的定义是就是一个变量名在代码里是dJ/dv,da=3  da是代码里的变量名其实代表的是dJ/da的值。

下面继续计算导数

dJ/du=3,根据dJ/du可以求出dJ/db的值

image-20220412195017589

最终计算出所有的导数

神经网络与深度学习二:神经网络的基础编程_第1张图片

对于这个例子,当计算所有的导数时最有效率的方法就是从右到左计算,跟着这个红色箭头走,特别是当我们第一次计算对v的导数时之后在计算对a的导数就可以用到对v的导数,然后对u的导数,可以帮忙计算b,c的导数

一个计算流程图就是正向的或者说从左到右的计算来计算成本函数J你可能需要优化的函数然后反向从右到左计算导数

8 logistic回归中的梯度下降法

本节将讨论怎样计算偏导数来实现logistic回归的梯度下降法,回想一下logistic回归的公式

假设样本只有两个特征x1和x2,为了计算z我们需要输入参数w1,w2,b还有样本特征值x1和x2

因此在logistic回归中我们需要做的是变化参数w和b的值来最小化损失函数,我们已经描述过前向传播在单个训练样本上计算损失函数,现在让我们讨论怎样向后计算偏导数

现在向后传播的最后一步计算看看w和b需要如何变化

image-20220412203332134

因此关于单个样本的梯度下降法你所需要做的就是这些事情,然后跟新参数w1,w2,b

现在你已经知道了怎样计算导数 并且实现了单个训练样本的logistic回归的梯度下降法,但是训练logistic回归模型不仅仅只有一个训练样本而是由m个样本的整个训练集

9 m个样本的梯度下降法

前面已经介绍了单个样本的情况

神经网络与深度学习二:神经网络的基础编程_第2张图片

全局成本函数是一个求和,事实上是1到m项损失函数和的平均,它表明全局成本函数对w1的导数也同样是各项损失函数对w1的导数的平均,这会得到全局梯度值,你能够把它直接应用到梯度下降算法中

神经网络与深度学习二:神经网络的基础编程_第3张图片

初始化后使用for循环计算

随着这些计算,你已经计算了损失函数J对各个参数w1,w2,b的导数,我们使用dw1,dw2和db作为累加器,所以在这些计算后dw1等于你的全局成本函数对w1的导数,对dw2和db也是一样

完成这些后进行一步梯度下降

但这样计算有两个缺点,当应用在这里的时候就是说应用此方法到logistic回归,你需要编写两个for循环,第一个for循环是遍历m个样本的循环,第二个循环是遍历所有特征的for循环,在这个例子中我们只用两个特征,但如果你有更多的特征,你开始编写你的dw1 dw2类似的计算dw3一直到dwn,你需要一个for循环遍历所有的n个特征。当你应用深度学习算法你会发现在代码中显示地使用for循环会使算法很低效,同时在深度学习领域会有越来越大地数据集所以能够应用你的算法完全不用显示for循环的话会是重要的,会帮助你处理更大的数据集,有一门向量化技术帮助你的代码摆脱这些显示的for循环

10 向量化

向量化通常是消除你的代码中显示for循环语句的艺术,下面看一个例子

image-20220413164031227

左侧就是非向量化的运算,右侧是向量运算,在python中np.dot表示矩阵乘法

import numpy as np
import time
a=np.random.rand(1000000)
b=np.random.rand(1000000)
tic=time.time()
c=np.dot(a,b)
toc=time.time()
print("运算结果%f" % c)
print("vector:{}".format((toc-tic)*1000))

c_1=0
tic=time.time()
for i in range(1000000):
    c_1+=a[i]*b[i]
toc=time.time()
print("运算结果%f" % c_1)
print("for loop:{}".format((toc-tic)*1000))

image-20220413170700547

不难看出向量运算速度快的多,近100倍的速度。你可能听说过可扩展深度学习是在GPU上做的,GPU也叫图像处理单元,但是我做的所有案例都是在jupyter notebook上实现的,这里只有CPU,CPU和GPU都有并行化的指令有时会叫做SIMD指令意思是单指令多数据流,这个词的意思是如果你使用了这样的内置函数np.dot或者其他可以让你去掉显式for循环的函数,这样python的能够充分利用并行化去更快的计算,这点都GPU和CPU上面的计算都是成立的,GPU更加擅长SIMD计算,但CPU事实上也不是太差,可能没有GPU那么擅长吧。你们见识到了向量化可以加速你的代码,经验法则是只要有其他的可能 就不要使用显式的for循环

11 向量化的更多例子

如果你想计算一个向量u作为矩阵A和另一个向量v的乘积,矩阵乘法的定义就是

运算就是

再看另一个例子

事实上numpy库有很多向量值函数,

np.log 会逐个元素计算log

np.abs 会计算绝对值

np.maximum(v,0) 计算所有元素中的最大值,求出v中所有元素和0之间相比的最大值

v**2 v中每个元素的平方

1/v 导数

每当你想写一个for循环时看看可不可以调用numpy用内置函数计算而不是for循环,下面看看怎么运用到logistic

image-20220413175455828

x ( i ) x^{(i)} x(i)是一个向量

通过向量化消除了第二个循环,其实还可以进一步的向量化,一个for循环都不用写

12 向量化logistic回归

我们已经讨论过向量化是如何显著地加速你的代码,现在我们将会讨论向量化是如何实现在logistic回归的上面,这样就能同时处理整个训练集,来实现梯度下降法的一步迭代,针对整个训练集的一步迭代不需要使用任何显式for循环。

回忆一下之前X表示的样本,z1到zm的计算就可以一步完成

神经网络与深度学习二:神经网络的基础编程_第4张图片

在python中的广播机制,只需要在向量的最后加一个b(实数),python会自动把实数b扩展成一个1×m的行向量

神经网络与深度学习二:神经网络的基础编程_第5张图片

然后计算a

image-20220413181853862

你只用了两行代码就完成了m个样本的输出a的计算,所以这就是正向传播一步迭代的向量化实现同时处理m个训练样本。接下来你会发现同样可以用向量化来高效地计算反向传播,并以此来计算梯度

13 向量化logistic回归的梯度输出

基于刚才得到的A,Z的定义,dZ可以表示为

神经网络与深度学习二:神经网络的基础编程_第6张图片

虽然我们之前去掉了一个for循环,但是还有一个for循环遍历训练集,让我们继续下面的操作把它们向量化

神经网络与深度学习二:神经网络的基础编程_第7张图片

这样就求出了所有的dw

注意到我们没有在训练集上使用for循环,现在你可以计算参数的更新

神经网络与深度学习二:神经网络的基础编程_第8张图片

你完成了正向传播和反向传播,确实实现了对所有的训练样本进行预测和求导,而且没有使用一个for循环,然后梯度下降更新参数 w = w − ∗ α d w w=w- * \alpha dw w=wαdw 其中 α \alpha α是学习率, b = b − ∗ α d b b=b- * \alpha db b=bαdb

这样我们就实现了logistic回归的梯度下降的一次迭代,之前说过我们应该尽量避免显式的使用for循环,但如果希望进行多次迭代梯度下降那么你依然需要for循环,如果你要求1000次导数进行梯度下降你或许仍旧需要一个for循环

14 python中的广播

广播也是一种手段可以使你的python代码执行的更快,详情可以看我之前的文章点击此处

此外python中的广播机制有时会出现一些难以调试的bug

a=np.random.randn(5)
print(a)
print(a.shape)           # 秩为1的数组,既不是行向量也不是列向量

image-20220413203902160

print(a.T)

image-20220413203929089

print(np.dot(a,a.T))                    # 本来应该是一个矩阵

image-20220413203954948

尽量不要用上面的这种格式m

a=np.random.randn(5,1)
print(a)

神经网络与深度学习二:神经网络的基础编程_第9张图片

print(a.T)

image-20220413204120185

print(np.dot(a,a.T))

神经网络与深度学习二:神经网络的基础编程_第10张图片

神经网络与深度学习二:神经网络的基础编程_第11张图片

如果我们在代码中做了很多的事情,所以我不太确定一个向量的具体维度是多少,我通常会assert()这样一个声明

确保这是一个向量,这些assert执行起来很快,可以随意插入,仔细检查你的矩阵和数组的维度,不要害怕调用reshape来确保你的矩阵或向量是你需要的维度

15 (选修)logistic损失函数的解释

在之前给出了logistic回归的成本函数的表达式,在这里将会给出一个简洁的证明来说明 logistic回归成本函数的表达式为什么是这种形式

可以把上面的两个式子合成一个

image-20220413210324813

因为

神经网络与深度学习二:神经网络的基础编程_第12张图片

也就是下面,log是严格单调递增的,所以最大化logp(y|x)等价于最大化p(y|x)

image-20220413210609352

而这就是我们之前提到的成本函数的负值,前面有一个负号的原因就是当你训练学习算法时希望算法输出值的概率是最大的,然而在logistic回归中我们需要最小化损失函数,因此最小化损失函数就是最大化log(p(y|x)),这是单个样本的损失函数。那么m个样本的成本函数怎么表示

image-20220413212938300

总结一下,为了最小化成本函数J(w,b),我们从logistic回归模型的最大似然估计角度出发,假设训练集中的样本都是独立同分布的条件下。

你可能感兴趣的:(深度学习,深度学习,神经网络,人工智能)