在机器学习的编程过程中,经常会涉及到很多复杂的循环,往往程序中最消耗时间的也是这部分代码,好在后来提出了向量化的概念,将循环转化为矢量计算,大大降低了运行的时间。
python中的numpy就是向量化的典型代表,粗浅的理解就是,cpu可以直接识别并且运行矩阵层面的运算,就好像如果我们人工去计算一堆行列矩阵的乘积之和,从循环的角度来看,我们要遍历两个矩阵的index,然后根据index遍历两个矩阵,各位元素相乘最后相加得到结果,但是从矩阵的角度出发,就是一个简单的矩阵乘法而已。具体可见下面的例子:
import time
a=np.random.rand(1000000) #随机得到一个1000000的数组
b=np.random.rand(1000000)
time_begin=time.time() #测量当前时间
#向量化版本
c=np.dot(a,b)
time_end=time.time()
print("Vectorized version:"+str(1000*(time_end-time_begin))+"ms")
#非向量化样本
c=0
time_begin1=time.time() #测量当前时间
for i in range(1000000):
c+=a[i]*b[i]
time_end2=time.time()
print("No-Vectorized version:"+str(1000*(time_end2-time_begin1))+"ms")
numpy中也是有ufunc函数的,并且有内置也支持自定义,不过自定义的情况下性能不如内置,毕竟内置是用c写的,速度上还是有差异的。详细的numpy的矢量化函数可见:
https://blog.csdn.net/qq_42413820/article/details/80743663blog.csdn.net总结的比较详细了。
下面我们来介绍numba中的矢量化。还是用一个例子来说明吧。
import numpy
import math
def trig(a, b):
return math.sin(a**2) * math.exp(b)
trig(1, 1)
a = numpy.ones((5,5))
b = numpy.ones((5,5))
trig(a, b)
因为这里的“trig”函数就是一个普通的函数,没有矢量化的功能无法对矩阵直接进行矢量化操作所以报错了。我们先用numpy的自定义ufunc来实现一下。
trig_numpy_ufunc = np.vectorize(trig, otypes=[np.float64])
%timeit trig_numpy_ufunc(a,b)
然后再用numba的ufunc尝试一下
from numba import vectorize
@vectorize
def trig_numba_ufunc(a, b):
return math.sin(a**2) * math.exp(b)
%timeit trig_numba_ufunc(a, b)
速度上numba相对于numpy的ufunc要提升了28.93倍。
numba.vectorze 支持静态定义数据的类型(不过个人感觉没有什么必要,因为速度上并不会因此而提升),并且实现并行也很方便。
from numba import vectorize
@vectorize('float64(float64, float64)',target='parallel')
def trig_numba_ufunc(a, b):
return math.sin(a**2) * math.exp(b)
%timeit trig_numba_ufunc(a, b)
外层的float64表示这个函数的返回值是float64类型,里面的两个float64表示参数类型是float64.
也可以设置多组参数数据类型,这样当你输入错误的数据类型的时候会自动给你报错:
@vectorize(['int32(int32, int32)',
'int64(int64, int64)',
'float32(float32, float32)',
'float64(float64, float64)'])
def trig(a, b):
return math.sin(a**2) * math.exp(b)
import numba
numba.config.NUMBA_NUM_THREADS=8
)
如果我们的输入参数不是数字而是一个数组类型的数据怎么办?"vectorize"只能支持单元素的函数,这个时候我们可以使用“guvectorize
”.
@guvectorize('int64[:], int64, int64[:]', '(n),()->(n)')
def g(x, y, result):
for i in range(x.shape[0]):
result[i] = x[i] + y
相对于vectorize来说可以支持数组的输入不过有一个坑就是,函数不能用return,函数类型类型与c中的“void”函数不能返回值,所以这里的输入输出通过参数中的 '(n),()->(n)' 这一项来指定,其中(n)表示1维切样本数为n的,‘()’表示无维则为单元素,“-》”左边代表输入,右边代表输出。
更复杂一些的情况:
@guvectorize('float64[:,:], float64[:,:], float64[:,:]',
'(m,n),(n,p)->(m,p)')
def matmul(A, B, C):
m, n = A.shape
n, p = B.shape
for i in range(m):
for j in range(p):
C[i, j] = 0
for k in range(n):
C[i, j] += A[i, k] * B[k, j]
a = numpy.random.random((500, 500))
out = matmul(a, a, numpy.zeros_like(a))
%timeit matmul(a, a, numpy.zeros_like(a))
这个时候我们不禁要产生疑问,jit和vectorize之间可以互相调用吗?如果可以的话会加速吗?
下面的例子见答案:
@guvectorize('float64[:,:], float64[:,:]', '(n,n)->(n,n)')
def gucopy(a, b):
I, J = a.shape
for i in range(I):
for j in range(J):
b[i, j] = a[i, j]
a = numpy.random.random((25,25))
%%timeit
b = gucopy(a)
@jit
def make_a_copy(a):
return gucopy(a)
%timeit make_a_copy(a)
因为嵌套调用反而速度下降了,所以把vectorize和jit联合使用并没有什么神奇的事情发生。