向量化(Vectorization)
向量化是非常基础的去除代码中for循环的艺术,在深度学习安全领域、深度学习实践中,你会经常发现自己训练大数据集,因为深度学习算法处理大数据集效果很棒,所以你的代码运行速度非常重要,否则如果在大数据集上,你的代码可能花费很长时间去运行,你将要等待非常长的时间去得到结果。所以在深度学习领域,运行向量化是一个关键的技巧,让我们举个栗子说明什么是向量化。
在逻辑回归中你需要去计算z=wTx+b ,w 、x 都是列向量。如果你有很多的特征那么就会有一个非常大的向量,所以w∈Rnx , x∈Rnx ,所以如果你想使用非向量化方法去计算wTx ,你需要用如下方式(python)
z=0
for i in range(n_x)
z+=w[i]*x[i]
z+=b
这是一个非向量化的实现,你会发现这真的很慢,作为一个对比,向量化实现将会非常直接计算wTx ,代码如下:
z=np.dot(w,x)+b
这是向量化计算wTx 的方法,你将会发现这个非常快
让我们用一个小例子说明一下,在我的我将会写一些代码(以下为教授在他的Jupyter notebook上写的Python代码,)
import numpy as np #导入numpy库
a = np.array([1,2,3,4]) #创建一个数据a
print(a)
# [1 2 3 4]
import time #导入时间库
a = np.random.rand(1000000)
b = np.random.rand(1000000) #通过round随机得到两个一百万维度的数组
tic = time.time() #现在测量一下当前时间
#向量化的版本
c = np.dot(a,b)
toc = time.time()
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”)#打印for循环的版本的时间
返回值见图。
在两个方法中,向量化和非向量化计算了相同的值,如你所见,向量化版本花费了1.5毫秒,非向量化版本的for循环花费了大约几乎500毫秒,非向量化版本多花费了300倍时间。所以在这个例子中,仅仅是向量化你的代码,就会运行300倍快。这意味着如果向量化方法需要花费一分钟去运行的数据,for循环将会花费5个小时去运行。
一句话总结,以上都是再说和for循环相比,向量化可以快速得到结果。
你可能听过很多类似如下的话,“大规模的深度学习使用了GPU或者图像处理单元实现”,但是我做的所有的案例都是在jupyter notebook上面实现,这里只有CPU,CPU和GPU都有并行化的指令,他们有时候会叫做SIMD指令,这个代表了一个单独指令多维数据,这个的基础意义是,如果你使用了built-in函数,像np.function或者并不要求你实现循环的函数,它可以让python的充分利用并行化计算,这是事实在GPU和CPU上面计算,GPU更加擅长SIMD计算,但是CPU事实上也不是太差,可能没有GPU那么擅长吧。接下来的视频中,你将看到向量化怎么能够加速你的代码,经验法则是,无论什么时候,避免使用明确的for循环。
以下代码及运行结果截图:
向量化的更多例子(More Examples of Vectorization)
从上节视频中,你知道了怎样通过numpy内置函数和避开显式的循环(loop)的方式进行向量化,从而有效提高代码速度。
经验提醒我,当我们在写神经网络程序时,或者在写逻辑(logistic)回归,或者其他神经网络模型时,应该避免写循环(loop)语句。虽然有时写循环(loop)是不可避免的,但是我们可以使用比如numpy的内置函数或者其他办法去计算。当你这样使用后,程序效率总是快于循环(loop)。
让我们看另外一个例子。如果你想计算向量u=Av ,这时矩阵乘法定义为,矩阵乘法的定义就是:ui=jAijvi ,这取决于你怎么定义ui 值。同样使用非向量化实现,u=np.zeros(n,1) , 并且通过两层循环for(i):for(j): ,得到u[i]=u[i]+A[i][j]*v[j] 。现在就有了i 和 j 的两层循环,这就是非向量化。向量化方式就可以用u=np.dot(A,v) ,右边这种向量化实现方式,消除了两层循环使得代码运行速度更快。
下面通过另一个例子继续了解向量化。如果你已经有一个向量v ,并且想要对向量v 的每个元素做指数操作,得到向量u 等于e 的v1 ,e 的v2 ,一直到e 的vn 次方。这里是非向量化的实现方式,首先你初始化了向量u=np.zeros(n,1) ,并且通过循环依次计算每个元素。但事实证明可以通过python的numpy内置函数,帮助你计算这样的单个函数。所以我会引入import numpy as np,执行 u=np.exp(v) 命令。注意到,在之前有循环的代码中,这里仅用了一行代码,向量v 作为输入,u 作为输出。你已经知道为什么需要循环,并且通过右边代码实现,效率会明显的快于循环方式。
事实上,numpy库有很多向量函数。比如 u=np.log是计算对数函数(log )、 np.abs() 是计算数据的绝对值、np.maximum() 计算元素y 中的最大值,你也可以 np.maximum(v,0) 、 v**2 代表获得元素 y 每个值得平方、 1v 获取元素 y 的倒数等等。所以当你想写循环时候,检查numpy是否存在类似的内置函数,从而避免使用循环(loop)方式。
那么,将刚才所学到的内容,运用在逻辑回归的梯度下降上,看看我们是否能简化两个计算过程中的某一步。这是我们逻辑回归的求导代码,有两层循环。在这例子我们有n 个特征值。如果你有超过两个特征时,需要循环 dw1 、dw2 、dw3 等等。所以 j 的实际值是1、2 和 nx ,就是你想要更新的值。所以我们想要消除第二循环,在这一行,这样我们就不用初始化 dw1 , dw2 都等于0。去掉这些,而是定义 dw 为一个向量,设置 u=np.zeros(n(x),1) 。定义了一个x 行的一维向量,从而替代循环。我们仅仅使用了一个向量操作 dw=dw+x(i)dz(i) 。最后,我们得到 dw=dw/m 。现在我们通过将两层循环转成一层循环,我们仍然还有这个循环训练样本。
希望这个视频给了你一点向量化感觉,减少一层循环使你代码更快,但事实证明我们能做得更好。所以在下个视频,我们将进一步的讲解逻辑回归,你将会看到更好的监督学习结果。在训练中不需要使用任何 for 循环,你也可以写出代码去运行整个训练集。到此为止一切都好,让我们看下一个视频。
向量化逻辑回归(Vectorizing Logistic Regression)
我们已经讨论过向量化是如何显著加速你的代码,在本次视频中我们将讨论如何实现逻辑回归的向量化计算。这样就能处理整个数据集,甚至不会用一个明确的for循环就能实现对于整个数据集梯度下降算法的优化。我对这项技术感到非常激动,并且当我们后面谈到神经网络时同样也不会用到一个明确的 for 循环。
让我们开始吧,首先我们回顾一下逻辑回归的前向传播步骤。所以,如果你有 m 个训练样本,然后对第一个样本进行预测,你需要这样计算。计算 z ,我正在使用这个熟悉的公式 z(1)=wTx(1)+b 。然后计算激活函数 a(1)=σ(z(1)) ,计算第一个样本的预测值 y 。
对第二个样本进行预测,你需要计算 z(2)=wTx(2)+b , a(2)=σ(z(2)) 。
对第三个样本进行预测,你需要计算 z(3)=wTx(3)+b , a(3)=σ(z(3)) ,依次类推。如果你有 m 个训练样本,你可能需要这样做 m 次,可以看出,为了完成前向传播步骤,即对我们的 m 个样本都计算出预测值。有一个办法可以并且不需要任何一个明确的for循环。让我们来看一下你该怎样做。
首先,回忆一下我们曾经定义了一个矩阵 X 作为你的训练输入,(如下图中蓝色 X )像这样在不同的列中堆积在一起。这是一个 nx 行 m 列的矩阵。我现在将它写为Python numpy的形式 (nx,m) ,这只是表示 X 是一个 nx 乘以 m 的矩阵 Rnx×m 。
现在我首先想做的是告诉你该如何在一个步骤中计算 z1 、 z2 、z3 等等。实际上,只用了一行代码。所以,我打算先构建一个 1×m 的矩阵,实际上它是一个行向量,同时我准备计算 z(1) , z(2) ……一直到 z(m) ,所有值都是在同一时间内完成。结果发现它可以表达为 w 的转置乘以大写矩阵 x 然后加上向量 [bb...b] , ([z(1)z(2)...z(m)]=wT+[bb...b]) 。[bb...b] 是一个 1×m 的向量或者 1×m 的矩阵或者是一个 m 维的行向量。所以希望你熟悉矩阵乘法,你会发现的 w 转置乘以 x(1) , x(2) 一直到 x(m) 。所以 w 转置可以是一个行向量。所以第一项 wTX 将计算 w 的转置乘以 x(1) , w 转置乘以x(2) 等等。然后我们加上第二项 [bb...b] ,你最终将 b 加到了每个元素上。所以你最终得到了另一个 1×m 的向量, [z(1)z(2)...z(m)]=wTX+[bb...b]=[wTx(1)+b,wTx(2)+b...wTx(m)+b] 。
wTx(1)+b 这是第一个元素,wTx(2)+b 这是第二个元素, wTx(m)+b 这是第 m 个元素。
如果你参照上面的定义,第一个元素恰好是 z(1) 的定义,第二个元素恰好是 z(2) 的定义,等等。所以,因为X 是一次获得的,当你得到你的训练样本,一个一个横向堆积起来,这里我将 [z(1)z(2)...z(m)] 定义为大写的 Z ,你用小写 z 表示并将它们横向排在一起。所以当你将不同训练样本对应的小写 x 横向堆积在一起时得到大写变量 X 并且将小写变量也用相同方法处理,将它们横向堆积起来,你就得到大写变量 Z 。结果发现,为了计算 WTX+[bb...b] ,numpy命令是Z=np.dot(w.T,X)+b 。这里在Python中有一个巧妙的地方,这里 b 是一个实数,或者你可以说是一个 1×1 矩阵,只是一个普通的实数。但是当你将这个向量加上这个实数时,Python自动把这个实数 b 扩展成一个 1×m 的行向量。所以这种情况下的操作似乎有点不可思议,它在Python中被称作广播(brosdcasting),目前你不用对此感到顾虑,我们将在下一个视频中进行进一步的讲解。话说回来它只用一行代码,用这一行代码,你可以计算大写的 Z ,而大写 Z 是一个包含所有小写z(1) 到 z(m) 的 1×m 的矩阵。这就是 Z 的内容,关于变量 a 又是如何呢?
我们接下来要做的就是找到一个同时计算 [a(1)a(2)...a(m)] 的方法。就像把小写 x 堆积起来得到大写 X 和横向堆积小写 z 得到大写 Z 一样,堆积小写变量 a 将形成一个新的变量,我们将它定义为大写 A 。在编程作业中,你将看到怎样用一个向量在sigmoid函数中进行计算。所以sigmoid函数中输入大写 Z 作为变量并且非常高效地输出大写 A 。你将在编程作业中看到它的细节。
总结一下,在这张幻灯片中我们已经看到,不需要for循环,利用 m 个训练样本一次性计算出小写 z 和小写 a ,用一行代码即可完成。
Z = np.dot(w.T,X) + b
这一行代码:A=[a(1)a(2)...a(m)]=σ(Z) ,通过恰当地运用σ 一次性计算所有 a 。这就是在同一时间内你如何完成一个所有 m 个训练样本的前向传播向量化计算。
概括一下,你刚刚看到如何利用向量化在同一时间内高效地计算所有的激活函数的所有 a 值。接下来,可以证明,你也可以利用向量化高效地计算反向传播并以此来计算梯度。让我们在下一个视频中看该如何实现。
向量化 logistic 回归的梯度输出(Vectorizing Logistic Regression's Gradient)
如何向量化计算的同时,对整个训练集预测结果a ,这是我们之前已经讨论过的内容。在本次视频中我们将学习如何向量化地计算m 个训练数据的梯度,本次视频的重点是如何同时计算 m 个数据的梯度,并且实现一个非常高效的逻辑回归算法(Logistic Regression)。
之前我们在讲梯度计算的时候,列举过几个例子, dz(1)=a(1)-y(1) ,dz(2)=a(2)-y(2) ……等等一系列类似公式。现在,对 m 个训练数据做同样的运算,我们可以定义一个新的变量 dZ=[dz(1),dz(2)...dz(m)] ,所有的 dz 变量横向排列,因此,dZ 是一个 1×m 的矩阵,或者说,一个 m 维行向量。在之前的幻灯片中,我们已经知道如何计算A ,即 [a(1),a(2)...a(m)] ,我们需要找到这样的一个行向量 Y=[y(1)y(2)...y(m)] ,由此,我们可以这样计算 dZ=A-Y=[a(1)-y(1)a(2)-y(2)...a(m)-y(m)] ,不难发现第一个元素就是 dz(1) ,第二个元素就是 dz(2) ……所以我们现在仅需一行代码,就可以同时完成这所有的计算。
在之前的实现中,我们已经去掉了一个for循环,但我们仍有一个遍历训练集的循环,如下所示:
dw=0
dw+=x(1)*dz(1)
dw+=x(2) *dz(2)
………….
dw+=x(m)*dz(m)
dw=dwm
db=0
db+=dz(1)
db+=dz(2)
………….
db+=dz(m)
db=dbm
上述(伪)代码就是我们在之前实现中做的,我们已经去掉了一个for循环,但用上述方法计算 dw 仍然需要一个循环遍历训练集,我们现在要做的就是将其向量化!
首先我们来看 db ,不难发现 db=1mi=1mdz(i) , 之前的讲解中,我们知道所有的dzi) 已经组成一个行向量 dZ 了,所以在Python中,我们很容易地想到db=1m*np.sum(dZ) ;接下来看dw ,我们先写出它的公式 dw=1m*X*dzT 其中,X 是一个行向量。因此展开后 dw=1m*(x(1)dz(1)+x(2)dz(2)+...+xmdzm) 。因此我们可以仅用两行代码进行计算:db=1m*np.sum(dZ) , dw=1m*X*dzT 。这样,我们就避免了在训练集上使用for循环。
现在,让我们回顾一下,看看我们之前怎么实现的逻辑回归,可以发现,没有向量化是非常低效的,如下图所示代码:
我们的目标是不使用for循环,而是向量,我们可以这么做:
Z=wTX+b=np.dot(w.T,X)+b
A=σ(Z)
dZ=A-Y
dw=1m*X*dzT
db=1m*np.sum(dZ)
w:=w-a*dw
b:=b-a*db
现在我们利用前五个公式完成了前向和后向传播,也实现了对所有训练样本进行预测和求导,再利用后两个公式,梯度下降更新参数。我们的目的是不使用for循环,所以我们就通过一次迭代实现一次梯度下降,但如果你希望多次迭代进行梯度下降,那么仍然需要for循环,放在最外层。不过我们还是觉得一次迭代就进行一次梯度下降,避免使用任何循环比较舒服一些。
最后,我们得到了一个高度向量化的、非常高效的逻辑回归的梯度下降算法,我们将在下次讨论Python中的Broadcasting技术。