Cython 初探及關於性能提升的初步討論

Cython 可以通過強類型 大大加快Python代碼的運行效率:
簡單例子:
pure python
def f(x):
return x ** 2 - x 


def intergrate_f(a, b, N):
s = 0
dx = (b - a)/ N
for i in range(N):
s += f(a + i * dx)


return s * dx
 
print intergrate_f(-2.5, 10.5, 10000000)



Cython:
def f(double x):
return x ** 2 - x 


def intergrate_f(double a, double b, int N):
cdef int i 
cdef double s, dx 
s = 0 
dx = (b-a)/N 
for i in range(N):
s += f(a + i * dx)
return s * dx




將上述代碼用Setup編譯成Python包後 調用執行,
當使用pure Python 時耗時4.5s Cython耗時0.96s


這裏cdef 是c相應類型的定義
這裏還提供了對於異常的問題,當定義強類型函數時,一般需要指定exception,
值 當返回相應的exception值時說明產生了異常,否則Cython對於特殊的cdef返回值函數
是不具備異常提醒機制的。


Cython 用於提升np.ndarray相關的性能的方法在於指定數據類型
cdef np.ndarray 及對ndarray下標訪問時 如指定下標cdef int i
應該使用轉型<unsigned int> i 代替i進行下標訪問。


使用Cython 可以大大加快Python代碼運行
這一般限於對於底層的一些C類型,而不是所有情況,例如對於一般的np.ndarray
這種已經使用C進行了較大優化的數組,這種情況是有限的,
而且對於諸如一些for循環, 當循環次數不夠多時,Cython的加速也是捉襟見肘。
而且由於引入了新的類型及變量會導致運行時間更長,
有時候速度與內存是不可兼得的,
比如要使用一個Bagging算法(統計中的BootStrap)需要初始化抽樣數組,
這時如果利用生成器對於每一個樣本逐個生成,並進行後續處理,
會在佔用較小內存的情況下完成任務,但速度不佳,
但也可以選擇先生成所有抽樣下標樣本,這樣會消耗較多的內存,但速度應當是更快的。


舉一個例子,下面的代碼是用來產生Bagging樣本的pure python代碼:
def generate_bagging_sample_index(X, sample_num, sample_size):
ListRequire = []
for i in range(sample_num):
ListRequire.append(np.random.randint(X.shape[1], size = sample_size))


return np.array(ListRequire)


def generate_bagging_sample_array(X, sample_num, sample_size):
sample_indexs_array = generate_bagging_sample_index(X, sample_num, sample_size)
for sample_indexs in sample_indexs_array:
yield X[sample_indexs].copy()


def Bagging_Test_Simple_Func(X, factor_max_bound = 20, sample_num = 10, sample_size = 100, disp = False, Type = "forward_stepwise", itertimes = None):
factor_dict = dict()


def change_eigen_ratio_sample_order(eigen_ratio_sample):
array_require_T = np.empty(shape = eigen_ratio_sample.T.shape)
for i in range(eigen_ratio_sample.T.shape[0]):
array_require_T[i] = list(reversed(list(eigen_ratio_sample.T[i])))


return (array_require_T.T).copy()


def generate_forward_stepwise_eigen_ratio_sample(X_bagging_sample):
eigen_ratio_sample = None


for i in range(factor_max_bound ,sample_size):
eigen_v = np.abs(list(reversed(list(eigvalsh(np.dot(X_bagging_sample[ :i + 1], X_bagging_sample[ :i + 1].T)))))) ** 0.5


if disp:
print (eigen_v[:-1] / eigen_v[1:])[: factor_max_bound]
print np.argmax((eigen_v[:-1] / eigen_v[1:])[: factor_max_bound])


if type(eigen_ratio_sample) == type(None):
eigen_ratio_sample = np.array((eigen_v[:-1] / eigen_v[1:])[: factor_max_bound]).reshape(1, -1)
else:
eigen_ratio_sample = np.append(eigen_ratio_sample, np.array((eigen_v[:-1] / eigen_v[1:])[: factor_max_bound]).reshape(1, -1), axis = 0)


return eigen_ratio_sample




這些函數的作用僅僅是生成樣本及計算特徵值的比,但當考慮性能採用Cython進行改進
會得到一個非常不具有可讀性的版本:
def generate_bagging_sample_index(np.ndarray X, int sample_num, int sample_size):
cdef np.ndarray ListRequire = np.empty(shape = (sample_num, sample_size)), random_list


cdef int i, j


for i in range(sample_num):
random_list = np.random.randint(X.shape[1], size = sample_size)
for j in range(sample_size):
ListRequire[<unsigned int>i][<unsigned int>j] = random_list[<unsigned int>j]


return ListRequire


def generate_bagging_sample_array(np.ndarray X, int sample_num, int sample_size):
cdef np.ndarray sample_indexs_array = generate_bagging_sample_index(X, sample_num, sample_size)
cdef int i


for i in range(sample_indexs_array.shape[0]):
yield X[np.array(sample_indexs_array[<unsigned int>i], dtype = np.int)].copy()


def Bagging_Test_Simple_C_Func(np.ndarray X, int factor_max_bound = 20, int sample_num = 10, int sample_size = 100, disp = False, Type = "forward_stepwise", int itertimes = 0):
factor_dict = dict()


def change_eigen_ratio_sample_order(eigen_ratio_sample):
cdef np.ndarray array_require_T = np.empty(shape = eigen_ratio_sample.T.shape)


cdef int i


for i in range(eigen_ratio_sample.T.shape[0]):
array_require_T[<unsigned int>i] = list(reversed(list(eigen_ratio_sample.T[<unsigned int>i])))


return (array_require_T.T).copy()


def generate_forward_stepwise_eigen_ratio_sample(X_bagging_sample):
cdef np.ndarray eigen_ratio_sample = np.empty(shape = (sample_size - factor_max_bound, factor_max_bound)), ratio_list, eigen_v


cdef int i, j


for i in range(factor_max_bound ,sample_size):
eigen_v = np.abs(list(reversed(list(eigvalsh(np.dot(X_bagging_sample[ :<unsigned int>i + 1], X_bagging_sample[ :<unsigned int>i + 1].T)))))) ** 0.5


ratio_list = np.array((eigen_v[:-1] / eigen_v[1:])[: factor_max_bound])
for j in range(ratio_list.shape[0]):
eigen_ratio_sample[<unsigned int>i - factor_max_bound][<unsigned int>j] = ratio_list[<unsigned int>j]


return eigen_ratio_sample




在測試這兩個程序時會發現,Cython比前者慢(不會慢太多),這與直接將前者進行Cython編譯
的結果是近乎相同的,當數據量不夠大時後者完敗。
所以Bagging的性能消耗本質仍是內存與速度的取捨,而非編譯語言。








發佈了28 篇原創文章 · 獲贊 23 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章