矢量量化的C语言实现_numba从入门到精通(5)—强大的矢量化vectorize和guvectorize...

(jit和vectorize的参数总结在第6章里会写)

在机器学习的编程过程中,经常会涉及到很多复杂的循环,往往程序中最消耗时间的也是这部分代码,好在后来提出了向量化的概念,将循环转化为矢量计算,大大降低了运行的时间。

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")

9e14368eff53c115ff3f31eddd35799d.png

numpy中也是有ufunc函数的,并且有内置也支持自定义,不过自定义的情况下性能不如内置,毕竟内置是用c写的,速度上还是有差异的。详细的numpy的矢量化函数可见:

https://blog.csdn.net/qq_42413820/article/details/80743663​blog.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)

矢量量化的C语言实现_numba从入门到精通(5)—强大的矢量化vectorize和guvectorize..._第1张图片

因为这里的“trig”函数就是一个普通的函数,没有矢量化的功能无法对矩阵直接进行矢量化操作所以报错了。我们先用numpy的自定义ufunc来实现一下。

trig_numpy_ufunc = np.vectorize(trig, otypes=[np.float64])
%timeit trig_numpy_ufunc(a,b)

2997d405de0147872347277152769fb4.png

然后再用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)

5e5ecf56bb80278ea7fa2f19fb064700.png

速度上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)

(补充一下,numba的多线程的数量通过全局变量来设置,参数没有显式的让你设置调用核数的地方,见下:

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)

993fa014da0366441ff3124897f0b179.png
@jit
def make_a_copy(a):
    return gucopy(a)

%timeit make_a_copy(a)

a0ad1c6e4467f52d6732408ccc5bcd9d.png

因为嵌套调用反而速度下降了,所以把vectorize和jit联合使用并没有什么神奇的事情发生。

你可能感兴趣的:(矢量量化的C语言实现)