神经网络基础篇:向量化(Vectorization)

发布时间 2023-11-02 10:27:25作者: Oten

向量化

向量化是非常基础的去除代码中for循环的艺术,在深度学习安全领域、深度学习实践中,会经常发现自己训练大数据集,因为深度学习算法处理大数据集效果很棒,所以的代码运行速度非常重要,否则如果在大数据集上,代码可能花费很长时间去运行,将要等待非常长的时间去得到结果。所以在深度学习领域,运行向量化是一个关键的技巧,让举个栗子说明什么是向量化。

在逻辑回归中需要去计算\(z={{w}^{T}}x+b\)\(w\)\(x\)都是列向量。如果有很多的特征那么就会有一个非常大的向量,所以\(w\in {{\mathbb{R}}^{{{n}_{x}}}}\) , \(x\in{{\mathbb{R}}^{{{n}_{x}}}}\),所以如果想使用非向量化方法去计算\({{w}^{T}}x\),需要用如下方式(python

z=0
for i in range(n_x):
    z += w[i]*x[i]
z += b

这是一个非向量化的实现,会发现这真的很慢,作为一个对比,向量化实现将会非常直接计算\({{w}^{T}}x\),代码如下:

z=np.dot(w,x)+b

这是向量化计算\({{w}^{T}}x\)的方法,将会发现这个非常快

让用一个小例子说明一下,在的将会写一些代码(以下是在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上面实现,这里只有CPUCPUGPU都有并行化的指令,他们有时候会叫做SIMD指令,这个代表了一个单独指令多维数据,这个的基础意义是,如果使用了built-in函数,像np.function或者并不要求实现循环的函数,它可以让python的充分利用并行化计算,这是事实在GPUCPU上面计算,GPU更加擅长SIMD计算,但是CPU事实上也不是太差,可能没有GPU那么擅长吧。接下来,将看到向量化怎么能够加速的代码,经验法则是,无论什么时候,避免使用明确的for循环。

以下代码及运行结果截图:

向量化的更多例子(More Examples of Vectorization)

从上面例子中,知道了怎样通过numpy内置函数和避开显式的循环(loop)的方式进行向量化,从而有效提高代码速度。

经验提醒,当在写神经网络程序时,或者在写逻辑(logistic)回归,或者其他神经网络模型时,应该避免写循环(loop)语句。虽然有时写循环(loop)是不可避免的,但是可以使用比如numpy的内置函数或者其他办法去计算。当这样使用后,程序效率总是快于循环(loop)

让看另外一个例子。如果想计算向量\(u=Av\),这时矩阵乘法定义为,矩阵乘法的定义就是:\(u_{i} =\sum_{j}^{}{A_{\text{ij}}v_{i}}\),这取决于怎么定义\(u_{i}\)值。同样使用非向量化实现,\(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\)\(v_1\)\(e\)\(v_2\),一直到\(e\)\(v_n\)次方。这里是非向量化的实现方式,首先初始化了向量\(u=np.zeros(n,1)\),并且通过循环依次计算每个元素。但事实证明可以通过pythonnumpy内置函数,帮助计算这样的单个函数。所以会引入import numpy as np,执行 \(u=np.exp(v)\) 命令。注意到,在之前有循环的代码中,这里仅用了一行代码,向量\(v\)作为输入,\(u\)作为输出。已经知道为什么需要循环,并且通过右边代码实现,效率会明显的快于循环方式。

事实上,numpy库有很多向量函数。比如 u=np.log是计算对数函数(\(log\))、 np.abs() 计算数据的绝对值、np.maximum(v, 0) 按元素计算\(v\)中每个元素和和0相比的最大值,v**2 代表获得元素 \(v\) 每个值的平方、 1/v 获取 \(v\) 中每个元素的倒数等等。所以当想写循环时候,检查numpy是否存在类似的内置函数,从而避免使用循环(loop)方式。

那么,将刚才所学到的内容,运用在逻辑回归的梯度下降上,看看是否能简化两个计算过程中的某一步。这是逻辑回归的求导代码,有两层循环。在这例子有\(n\)个特征值。如果有超过两个特征时,需要循环 \(dw_1\)\(dw_2\)\(dw_3\) 等等。所以 \(j\) 的实际值是1、2 和 \(n_x\),就是想要更新的值。所以想要消除第二循环,在这一行,这样就不用初始化 \(dw_1\)\(dw_2\) 都等于0。去掉这些,而是定义 \(dw\) 为一个向量,设置 \(u=np.zeros(n(x),1)\)。定义了一个\(x\)行的一维向量,从而替代循环。仅仅使用了一个向量操作 \(dw=dw+x^{(i)}dz^{(i)}\) 。最后,得到 \(dw=dw/m\) 。现在通过将两层循环转成一层循环,仍然还有这个循环训练样本。

希望这个博客有给读者一点向量化感觉,减少一层循环使代码更快,但事实证明能做得更好。所以在下个博客,将进一步的讲解逻辑回归,将会看到更好的监督学习结果。在训练中不需要使用任何 for 循环,也可以写出代码去运行整个训练集。到此为止一切都好。