python numpy log_Cython教程与代码之——Cython_tutorial之cython和numpy以及pandas

前面我们探讨的都是一些比较简单的应用情况,下面来介绍一下我们更常用的优化场景。

一般在数据挖掘的工程中我们最常用的两个python库,numpy和pandas,下面我们来学习一下怎么使用cython来优化numpy和pandas的代码。

纯python版

%load_ext Cython
import numpy as np


def shannon_entropy_py(p_x):
    return - np.sum(p_x * np.log(p_x))

(补充一下这里的参数-a,很nice的使用参数,具体的可以显示代码中可优化的部分:

v2-00edf3b6468351858777262f9e1b61ee_b.jpg

图中的黄色部分表示这部分代码都是全部或者部分用到了python的解释器,所以这部分代码是拖慢程序运行速度的罪魁祸首,我们需要在这部分上进行cython的改造。)

首先,最简单也最有效的改造方式,把python中涉及到变量的部分 用cython定义一遍,这里就要用到 cimport numpy as cnp了,numpy给cython留了调用的c-level的接口,使用cimport可以在cython中导入c模块从而通过调用c模块来加快程序运行的速度。

numpy真是个好东西!

我们这里把函数的参数进行类型声明,“cnp.ndarray p_x” (和c完全一样的变量声明方式)

%%cython 

import numpy as np
cimport numpy as cnp

def shannon_entropy_cy(cnp.ndarray p_x):
    return - np.sum(p_x * np.log(p_x))

比较一下二者的运行速度区别:

v2-83ffa6de03d8d580547a2c5951a46557_b.jpg

辣鸡,才提升了这么一点。

接下来在上一部的cython定义里面,我们把np.log改为从c库中导入(没错,使用cimport允许你在cython中导入标准的c函数)

%%cython 

cimport numpy as cnp
from libc.math cimport log as clog

def shannon_entropy_v1(cnp.ndarray p_x):
    cdef double res = 0.0
    cdef int n = p_x.shape[0]
    cdef int i
    for i in range(n):
        res += p_x[i] * clog(p_x[i])
    return -res

v2-60675e90be476c4364d76e3a3d6ce6b9_b.png

wtf???为什么速度慢了这么多????

这里实际上涉及到矢量化的概念,矢量化的大概的含义就是将一些比较复杂的运算转化为张量(比如二维条件下的矩阵运算)的运算,最典型的例子就是通过矩阵乘的方法代替了for循环,比如,针对两个5*5的数组进行求和操作,如果使用for循环我们需要循环一大圈,但是从矩阵的角度出发就方便多了,对应位置的元素相加即可。虽然这二者听起来运算的目的是一样的,但是在cpu上运行的效率是不可比拟的。

为了比较,我们把循环的写法写一遍然后计算一下时间:

def shannon_entropy_v1(p_x):
    res=0.0
    n = p_x.shape[0]
    for i in range(n):
        res += p_x[i] * np.log(p_x[i])
    return -res
%%timeit
shannon_entropy_v1(pmf)

v2-0eded948a0a7670b9e883da33f6513b6_b.png

下面我们来使用神器的numpy数据类型的指定。相对于上面再上面的那个cython实现,下面的代码仅仅改变了cnp.ndarray[double],从原来的cnp.ndarray改成了cnp.ndarray[double],

%%cython 

cimport numpy as cnp
from libc.math cimport log as clog

def shannon_entropy_v2(cnp.ndarray[double] p_x):
    cdef double res = 0.0
    cdef int n = p_x.shape[0]
    cdef int i
    for i in range(n):
        res += p_x[i] * clog(p_x[i])
    return -res

v2-f7c67b29eea6d6c74e853854877f3020_b.png

可以看到,速度比矢量化的numpy版还要快5倍左右!相对于python版本的循环函数快了161.73倍。

在上面的基础上,我们继续努力!

%%cython -a

cimport cython
cimport numpy as cnp
from libc.math cimport log

@cython.boundscheck(False)
@cython.wraparound(False)
def shannon_entropy_v3(cnp.ndarray[double] p_x):
    cdef double res = 0.0
    cdef int n = p_x.shape[0]
    cdef int i
    for i in range(n):
        res += p_x[i] * log(p_x[i])
    return -res

%%timeit
shannon_entropy_v3(pmf)

v2-f65f25d6a826d91b5e4ac245bb6cd8f4_b.jpg

可以看到,在取消了边界检查和环绕检查之后速度又提升了一丢丢。

我们再来看看有没有办法针对于循环做一个简单的优化,重新生成一个更大的pmf来比较:

from scipy.stats import poisson
poi = poisson(10.0)
n = 10000000
pmf = poi.pmf(np.arange(n))

原来的最快的方式耗时:

v2-90495a8282f6ed6a5819e6eab5e4e3d5_b.jpg

我们针对循环体做了一些改变,具体参照:

https://stackoverflow.com/questions/21382180/cython-pure-c-loop-optimization​stackoverflow.com
%%cython -a

cimport cython
cimport numpy as cnp
from libc.math cimport log

@cython.boundscheck(False)
@cython.wraparound(False)
def shannon_entropy_v3(cnp.ndarray[double] p_x):
    cdef double res = 0.0
    cdef int n = p_x.shape[0]
    cdef int i
    for i from 0<=i<n:
        res += p_x[i] * log(p_x[i])
    return -res

v2-c938d7b4ac5d8b0de090ec9681d4f4c7_b.jpg

可以看到,改变了循环体的结构不使用python的range之后运行的效率又提升了不少。具体原因可以参考上面的stack overflow的讨论。

对于pandas,tutorial给出的建议是,用numpy代替pandas,因为pandas是在numpy的基础上进行的高级的封装,所以,不可避免的要带来一定的性能的损失。因此建议在使用pandas的场合都替换为numpy,一方面使用numpy在大部分场景下都可以直接加速,一方面也便于扩展,julia、cython、numba都是可以直接和numpy进行交互的。


介绍完了函数,该介绍一些cython中的类如何来定义了。其实也挺简单的。

下面是纯python的类定义。

class PyLCG(object):
    
    def __init__(self, a=1664525, c=1013904223, m=2**3, seed=0):
        self.a = a
        self.c = c
        if m <= 0:
            raise ValueError("m must be > 0, given {}".format(m))
        self.m = m
        # The RNG state.
        self.x = seed
        
    def _advance(self):
        r = self.x
        self.x = (self.a * self.x + self.c) % self.m
        return r
        
    def randint(self, size=None):
        if size is None:
            return self._advance()
        return np.asarray([self._advance() for _ in range(size)])

v2-1b39c2f58ae15443460d56128435bc5b_b.jpg
时间花销为6.21ms

然后是cython下的类定义:可以看到比较特别的地方在于:

(1)cython下的类在初始化__init__的时候用的是__cinit__

(2)__cinit__中的参数要提前在_cinit_之间进行声明否则类无法直接识别

(3)针对与如何在cython中定义list的问题,google上给出很多解决方案,不过对于存放纯数字的list来说,numpy的c api不失为一个很方便的选择,或者如果有可能,在cython中使用numcpp?(最快的速度应该是使用c的malloc来开辟动态数组,或者c++的vector等方式,当然如果通过list来append dict,tuple之类的会比较麻烦这里暂不赘述,因为我还没有搞清楚,c和c++的语法知识我需要足够的时间来慢慢研究),我觉得list、dict、tuple这些python的数据类型要在cython中通过纯c或c++的方式表达出来也是比较难,这一块后续单独写一篇总结的文章放到当前的专栏下,因为涉及到的东西实在太多了。

%%cython -a

import numpy as np
cimport numpy as cnp
cimport cython

# Creates a new extension type: https://docs.python.org/3/extending/newtypes.html
cdef class CyLCG:
    
    # We declare the compile-time types of our *instance* attributes here.
    # This is similar to C++ class declaration syntax.
    cdef long a, c, m, x
    
    
    # Special Cython-defined initializer.
    # Called before __init__ to initialize all C-level attributes.
    def __cinit__(self, long a=1664525, long c=1013904223, long m=2**3, long seed=0):
        self.a = a
        self.c = c
        if m <= 0:
            raise ValueError("m must be > 0, given {}".format(m))
        self.m = m
        self.x = seed
    
    # cdef / cpdef methods are supported
    @cython.cdivision(True)
    cdef long _advance(self):
        cdef long r = self.x
        self.x = (self.a * self.x + self.c) % self.m
        return r
    
    # Regular def method
    @cython.boundscheck(False)
    @cython.wraparound(False)
    def randint(self, size=None):
        cdef long r
        if size is None:
            # Call to self._advance() here is efficient and at the C level.
            r = self._advance()
            return r
        cdef long[:] a = np.empty((size,), dtype='i4')
        cdef int i
        cdef int n = int(size)
        for i in range(n):
            a[i] = self._advance()
        return np.asarray(a)

v2-4722c5ef81be746f8f0d55d89c752aa2_b.png

速度提高了42.53倍。

除此之后,使用cython定义的类的内存占用也要远小于python下的类:

### Pure-python内存占用

*`PyLCG()`对象本身

*实例`__dict__`

*和实例`__dict__`中的每个键/值

python下的类确实多了不少属性,后面会在《python高性能编程》的专栏里添加一篇关于python中的数据的属性的删减的问题。

import sys
(sys.getsizeof(pyrng) # the object itself
 + sys.getsizeof(pyrng.__dict__)  # the instance __dict__
 + sum(sys.getsizeof(k) + sys.getsizeof(v) for k, v in pyrng.__dict__.items())) # k/v memory use

v2-414f51ec4db9d189c845f83ea41180b6_b.png

而cython下定义的类型仅包含了这个类本身所占用的内存。

(sys.getsizeof(CyLCG()) # the object itself
 + 4 * 8) # The 4 8-byte longs (a, c, m, x)

v2-128464cefcbe0680ae60c05a6e5d397c_b.jpg

可以看到两个类属性上的差距。(CyLCG中cdef的_advance在python中是不可见的)

v2-a690b96d10b41409bed87ef7fa23529d_b.jpg

同时根据上图我们可以知道,凡是在cython定义的类中用cdef定义的方法和属性,在python中都是不可见的,

v2-f1d30f0d5598d8fe6e535c9e02146f8d_b.jpg

同时也不可以给这个类创造新的属性(而这一切在python中都是可以实现的)

这个时候我们可以使用cython中的 cdef public和cdef readonly来避免这个问题(很遗憾只能使得属性变得可见,cdef的方法无法变得可见只能通过别的手段比如cpdef或者是def中的嵌套cdef的方式)

%%cython -a

import numpy as np
cimport cython

cdef class CyLCGOpen:
    
    cdef public long x
    cdef readonly long a, c, m
    
    def __cinit__(self, long a=1664525, long c=1013904223, long m=2**32, long seed=0):
        self.a = a
        self.c = c
        if m <= 0:
            raise ValueError("m must be > 0, given {}".format(m))
        self.m = m
        self.x = seed
        
    # cdef / cpdef methods are supported
    @cython.cdivision(True)        
    cdef  long _advance(self):
        cdef long r = self.x
        self.x = (self.a * self.x + self.c) % self.m
        return r
    
    # Regular def method
    @cython.boundscheck(False)
    @cython.wraparound(False)
    def randint(self, size=None):
        cdef long r
        if size is None:
            # Call to self._advance() here is efficient and at the C level.
            r = self._advance()
            return r
        cdef long[::1] a = np.empty((size,), dtype='i8')
        cdef int i
        cdef int n = int(size)
        for i in range(n):
            a[i] = self._advance()
        return np.asarray(a)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值