scipy.sparse稀疏矩陣內積點乘--效率優化!

在使用scipy和numpy做數據計算時,感覺運行速度較慢,但是程序已經到了使用多數計算使用內積運算地步了,真的不知道該如何優化。如果能夠優化下內積運算該有多好啊,奔着這個目標,希望能夠寫一篇文章盤點各種內積優化方法,也算是貢獻自己的微薄之力。

開篇我寫兩點自己經驗,拋磚引玉,希望大家多多提意見。由於自己對對於Scipy和Numpy熟悉度不夠,所以有不正確的地方,還請大家多多斧正。

在說我的優化之前,先囉嗦下:scipy.sparse的矩陣包中,牽扯到矩陣運算,矩陣的格式優選csr_matrix和csc_matrix。不然速度肯定慢的你懷疑人生。

特別說明1本文的實驗在ipython或者jupyter環境進行,時間消耗測試使用的是“%timeit”命令,Scipy版本爲“0.19.1”。

特別說明2在程序中,很多非計算操作,如:list轉稀疏矩陣、矩陣轉置、矩陣拼接和矩陣更新等,由於它們具有內存操作,所以時間代價相當昂貴,並且可以提前處理,所以在測量時間消耗時,無需將他們的時間消耗也計算在內。在性能優化中,有兩條原則相當重要:減少內存操作和減少CPU命令數。更多詳情查看《Python高性能編程》第6章。

特別說明3如果你是計算專業的在讀生,那麼學好《計算機架構導論》、《操作系統》、《數據結構》、《離散數學》。前兩本書讓你在硬件和操作系統層次明白編程語言的特性,配上一些相關書籍,你會很快明白爲什麼會快,爲什麼會慢,爲什麼有些語言風格會快,有些則慢。後兩本則告訴你如何優化你的算法,好比:現在從山北到山南,你可以從北山腳爬到山頂再到南山腳,也可以圍着山跑,從北山腳跑到南山腳。當然,這些書的用處,絕不僅於此,它也是科班生與培訓班生的區別。計算機學生,不僅僅是學好幾門編程語言和數據結構那麼簡單,。

一、大小矩陣內積運算

當兩個規模相當的矩陣做內積時,選擇CSC或CSR並沒有太大差別,時間效果相當。但是當爲一大一小矩陣時,就有一些技巧,可以節約時間。假設B爲大矩陣,S爲小矩陣。
- 當CSR格式時,S×B速度較快,與B×S相比節約了一半時間。
- 當CSC格式時,B×S速度較快,與S×B相比節約一半時間。
上述兩種方法,時間相近,不分伯仲之間。

以下是我的計算例子。

import scipy.sparse as sp

def is_csr_instance(mtx):
    if isinstance(mtx, sp.csr_matrix):
        return True
    else:
        return False

def is_csc_instance(mtx):
    if isinstance(mtx, sp.csc_matrix):
        return True
    else:
        return False

a_mtx = sp.csc_matrix([[1., 1., 3.]*120])
mtx = sp.csc_matrix([[1., 0., 0.]*120]*30000)

is_csc_instance(a_mtx), is_csc_instance(mtx)

mtx.shape, a_mtx.shape

mtx_T = mtx.T
mtx_T = mtx_T.tocsc()

print is_csc_instance(mtx_T), is_csr_instance(mtx_T)

print u"\n\ncsc little×big"
print type(a_mtx), type(mtx_T)
print a_mtx.shape, mtx_T.shape
%timeit c = a_mtx.dot(mtx_T)

print u"\n\ncsr little×big"
a_mtx_r = a_mtx.tocsr()
mtx_T_r = mtx_T.tocsr()
print type(a_mtx_r), type(mtx_T_r)
print a_mtx_r.shape, mtx_T_r.shape
%timeit c = a_mtx_r.dot(mtx_T_r)

a_mtx_T = a_mtx.T
a_mtx_T = a_mtx_T.tocsc()
mtx_T.shape, a_mtx_T.shape

print "\n\ncsc big×little"
print type(mtx), type(a_mtx_T)
print mtx.shape, a_mtx_T.shape
%timeit c = mtx.dot(a_mtx_T)

print "\n\ncsr big×little"
mtx = mtx.tocsr()
a_mtx_T = a_mtx_T.tocsr()
print type(mtx), type(a_mtx_T)
print mtx.shape, a_mtx_T.shape
%timeit c = mtx.dot(a_mtx_T)

輸出如下:

csc little×big
<class 'scipy.sparse.csc.csc_matrix'> <class 'scipy.sparse.csc.csc_matrix'>
(1, 360) (360, 30000)
100 loops, best of 3: 17.4 ms per loop


csr little×big
<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'>
(1, 360) (360, 30000)
100 loops, best of 3: 8.13 ms per loop


csc big×little
<class 'scipy.sparse.csc.csc_matrix'> <class 'scipy.sparse.csc.csc_matrix'>
(30000, 360) (360, 1)
100 loops, best of 3: 8.31 ms per loop


csr big×little
<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'>
(30000, 360) (360, 1)
100 loops, best of 3: 17.6 ms per loop

二、多矩陣內積優化

不好意思,這條優化有時有效有時無效,所以暫時不要完全相信,歡迎各位對此條多提意見。

當有多個矩陣進行內積計算時,可以通過矩陣拼接將多次內積計算合併爲一次節約時間。時間優化效果與矩陣的中需要計算的非零數據次數成反比,需要計算的次數越多,節約的時間越少。假設稀疏矩陣中,非零元素隨機出現,那麼需要計算的非零數據次數非常少,所以有近似結論:矩陣越稀疏,需要計算的非零數據越少,節約的時間越多。矩陣稠密度是非零元素個數與矩陣總元素數的比值。

本實驗有兩個組,對照組爲一個1×N與一個M×N的矩陣做四次內積,實驗組爲一個1×4N的矩陣與一個M×4N的矩陣做一次內積。實驗分3次:例1,例2和例3:
- 例1中,兩個矩陣稠密度爲100%,對照組時間消耗略高。
- 例2中,兩個矩陣稠密度爲33.34%,對照組時間較高。
- 例3中,兩個矩陣稠密度分別爲16.7%和8.3%,對照組時間消耗明顯很高。

實驗公共代碼

import scipy.sparse as sp

def quadra_dot(a_mtx, b_mtx):
    a = a_mtx * b_mtx
    b = a_mtx * b_mtx
    c = a_mtx * b_mtx
    d = a_mtx * b_mtx

def uni_dot(a_mtx, b_mtx):
    a = a_mtx * b_mtx

def density(mtx):
    non_zeros_numbers = len(mtx.data) * 1.0
    m, n = mtx.shape
    print non_zeros_numbers / (m*n)

例1

a_mtx = sp.csr_matrix([[2.23, 1.56, 3.47]*120]*300)
mtx = sp.csr_matrix([[1.07, 2.19, 3.12]*120]*30000)

print(u"對照組:")
b_mtx = mtx.T
b_mtx = b_mtx.tocsr()

print type(a_mtx), type(b_mtx), a_mtx.shape, b_mtx.shape
# 測試時間消耗
%timeit quadra_dot(a_mtx, b_mtx)

print(u"實驗組:")
c_mtx = sp.vstack((b_mtx, b_mtx))
c_mtx = sp.vstack((c_mtx, b_mtx))
c_mtx = sp.vstack((c_mtx, b_mtx))

a_mtx = sp.hstack((a_mtx, a_mtx))
a_mtx = sp.hstack((a_mtx, a_mtx))

c_mtx = c_mtx.tocsr()
a_mtx = a_mtx.tocsr()

print type(a_mtx), type(c_mtx), a_mtx.shape, c_mtx.shape
%timeit uni_dot(a_mtx, c_mtx)

例1輸出:

對照組:
<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'> (300, 360) (360, 30000)
1 loop, best of 3: 29.8 s per loop

實驗組:
<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'> (300, 1440) (1440, 30000)
1 loop, best of 3: 28 s per loop

例2

a_mtx = sp.csr_matrix([[2.23, 1.56, 3.47]*120]*300)
mtx = sp.csr_matrix([[1.07, 2.19, 3.12]*120]*30000)
density(a_mtx)
density(mtx)

# 代碼與例1的對應部分相同,不在重複
...

例2輸出:

density 0.3333
density 0.3333
對照組:
<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'> (300, 360) (360, 30000)
1 loop, best of 3: 9.06 s per loop

實驗組:
<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'> (300, 1440) (1440, 30000)
1 loop, best of 3: 8.85 s per loop

例3

a_mtx = sp.csr_matrix([[0., 0., 0., 0., 13.23, 0., 0., 0., 1.32, 0., 0., 0., 0., 0., 0., 0., 13.23, 0., 0., 0., 1.32, 0., 0., 0.]*5]*300)
mtx = sp.csr_matrix([[1.07, 0., 0., 0., 1.30, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.0, 0., 0., 0.]*5]*30000)
density(a_mtx)
density(mtx)

# 代碼與例1的對應部分相同,不在重複
...

例2輸出:

density 0.166666
density 0.083333
對照組:
<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'> (300, 120) (120, 30000)
1 loop, best of 3: 559 ms per loop

實驗組:
<class 'scipy.sparse.csr.csr_matrix'> <class 'scipy.sparse.csr.csr_matrix'> (300, 480) (480, 30000)
1 loop, best of 3: 374 ms per loop

稀疏矩陣歸一化和轉置,影響矩陣計算性能

這章是對稀疏矩陣來說的,稠密矩陣的請自行略過!
首先,相同格式的稀疏矩陣做點乘速度很快,不同格式速度很慢,CSR與CSR矩陣點乘速度快,CSC與CSC矩陣點乘速度快。某些情況下在點乘計算前,需要進行歸一化操作,比如計算cosine相似度,需要對兩個稀疏矩陣分別做行歸一化和列歸一化,或者轉置。在進行歸一化或者轉置後,矩陣的格式可能會發生改變,那麼拿着結果直接做點乘,速度就會很慢。

這裏使用的是sklearn.preprocessing.normalize函數進行歸一化的。對於稀疏矩陣,行歸一化的返回值是CSR矩陣,列歸一化的返回值是CSC矩陣(實驗結果見下面代碼);之所以這麼這麼做,是爲了提高計算速度,同時也降低計算難度,sklearn的做法是:如果是sparse矩陣,當是行歸一化時,就將原始矩陣轉爲CSR格式,這樣就可以對矩陣的data(data是sparse.csr_matrix的一個屬性)中的每行的元素,進行快速歸一化。當列歸一化時,轉爲CSC矩陣,然後對data中的列元素進行快速歸一化。如果你不明白爲什麼如此操作的好處,請參看稀疏矩陣壓縮原理

轉置操作輸入CSR矩陣返回CSC矩,陣輸入CSC矩陣返回CSR矩陣。至於轉置爲何也會改變矩陣格式,答案也是速度快,編碼簡單,爲什麼呢?自己動手計算一下吧。

import scipy.sp
import sklearn.preprocessing as pp

# 1. CSR矩陣
a = sp.csr_matrix([[1,2,3,4], [32,4,0,0]])
type(a)    # 輸出 --> scipy.sparse.csr.csr_matrix

b = pp.normalize(a, axis=1)  # 行歸一化
type(b)    # 輸出 --> scipy.sparse.csr.csr_matrix

b = pp.normalize(a, axis=0)  # 列歸一化
type(b)    # 輸出 --> scipy.sparse.csr.csc_matrix


a = sp.csr_matrix([[1,2,3,4], [32,4,0,0]])
type(a)    # 輸出 --> scipy.sparse.csr.csr_matrix

import sklearn.preprocessing as pp
b = pp.normalize(a, axis=1)  # 行歸一化
type(b)    # 輸出 --> scipy.sparse.csr.csr_matrix

b = pp.normalize(a, axis=0)  # 列歸一化
type(b)    # 輸出 --> scipy.sparse.csr.csc_matrix
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章