2012-01-17 101 views
39

我正在研究Clojure中的一個應用程序,該應用程序需要乘以大型矩陣,並且遇到了與相同的Numpy版本相比的一些大型性能問題。 Numpy似乎能夠在一秒之內通過轉置乘以一個1,000,000x23的矩陣,而相同的clojure代碼則需要六分鐘。 (我可以打印Numpy生成的矩陣,所以它絕對評估一切)。Clojure與Numpy的矩陣乘法

我在這個Clojure代碼中做了一些非常錯誤的事情嗎?我可以嘗試模仿Numpy的一些技巧嗎?

這裏的蟒蛇:

import numpy as np 

def test_my_mult(n): 
    A = np.random.rand(n*23).reshape(n,23) 
    At = A.T 

    t0 = time.time() 
    res = np.dot(A.T, A) 
    print time.time() - t0 
    print np.shape(res) 

    return res 

# Example (returns a 23x23 matrix): 
# >>> results = test_my_mult(1000000) 
# 
# 0.906938076019 
# (23, 23) 

而且Clojure的:

(defn feature-vec [n] 
    (map (partial cons 1) 
     (for [x (range n)] 
     (take 22 (repeatedly rand))))) 

(defn dot-product [x y] 
    (reduce + (map * x y))) 

(defn transpose 
    "returns the transposition of a `coll` of vectors" 
    [coll] 
    (apply map vector coll)) 

(defn matrix-mult 
    [mat1 mat2] 
    (let [row-mult (fn [mat row] 
        (map (partial dot-product row) 
         (transpose mat)))] 
    (map (partial row-mult mat2) 
     mat1))) 

(defn test-my-mult 
    [n afn] 
    (let [xs (feature-vec n) 
     xst (transpose xs)] 
    (time (dorun (afn xst xs))))) 

;; Example (yields a 23x23 matrix): 
;; (test-my-mult 1000 i/mmult) => "Elapsed time: 32.626 msecs" 
;; (test-my-mult 10000 i/mmult) => "Elapsed time: 628.841 msecs" 

;; (test-my-mult 1000 matrix-mult) => "Elapsed time: 14.748 msecs" 
;; (test-my-mult 10000 matrix-mult) => "Elapsed time: 434.128 msecs" 
;; (test-my-mult 1000000 matrix-mult) => "Elapsed time: 375751.999 msecs" 


;; Test from wikipedia 
;; (def A [[14 9 3] [2 11 15] [0 12 17] [5 2 3]]) 
;; (def B [[12 25] [9 10] [8 5]]) 

;; user> (matrix-mult A B) 
;; ((273 455) (243 235) (244 205) (102 160)) 

更新:我實現並採用JBLAS庫相同的基準,並發現大量的,大規模的速度提升。感謝大家的投入!有時間把這個吸盤包裝在Clojure中。下面是新的代碼:

(import '[org.jblas FloatMatrix]) 

(defn feature-vec [n] 
    (FloatMatrix. 
    (into-array (for [x (range n)] 
       (float-array (cons 1 (take 22 (repeatedly rand)))))))) 

(defn test-mult [n] 
    (let [xs (feature-vec n) 
     xst (.transpose xs)] 
    (time (let [result (.mmul xst xs)] 
      [(.rows result) 
      (.columns result)])))) 

;; user> (test-mult 10000) 
;; "Elapsed time: 6.99 msecs" 
;; [23 23] 

;; user> (test-mult 100000) 
;; "Elapsed time: 43.88 msecs" 
;; [23 23] 

;; user> (test-mult 1000000) 
;; "Elapsed time: 383.439 msecs" 
;; [23 23] 

(defn matrix-stream [rows cols] 
    (repeatedly #(FloatMatrix/randn rows cols))) 

(defn square-benchmark 
    "Times the multiplication of a square matrix." 
    [n] 
    (let [[a b c] (matrix-stream n n)] 
    (time (.mmuli a b c)) 
    nil)) 

;; forma.matrix.jblas> (square-benchmark 10) 
;; "Elapsed time: 0.113 msecs" 
;; nil 
;; forma.matrix.jblas> (square-benchmark 100) 
;; "Elapsed time: 0.548 msecs" 
;; nil 
;; forma.matrix.jblas> (square-benchmark 1000) 
;; "Elapsed time: 107.555 msecs" 
;; nil 
;; forma.matrix.jblas> (square-benchmark 2000) 
;; "Elapsed time: 793.022 msecs" 
;; nil 

回答

31

Python的版本被編譯到在C環,而Clojure的版本,建立一個新的中間序列的每個呼叫的這個代碼映射。您看到的性能差異很可能來自數據結構的差異。

爲了獲得更好的效果,您可以玩像Incanter這樣的圖書館,或者按照this SO question中所述編寫自己的版本。另見this one。如果你真的想留在序列,以保持懶惰的評估屬性等,那麼你可以通過查看transients進行內部矩陣計算來獲得真正的提升

編輯:忘記添加調整clojure的第一步,打開「反射警告」

+0

啊,這些鏈接真的很有幫助。我會試着看看我能從一些矩陣包中得到什麼。 – 2012-01-17 19:18:48

+28

不是C;大部分BLAS和LAPACK都是Fortran。 Python版本也不是「編譯」成C中的循環;它只是使用C/Fortran編寫的庫。最後,使用的算法是完全不同的。對Numpy的方法的描述完全與我們的原始海報顯示的代碼相關的可能性不大。 – pavpanchekha 2012-01-17 21:28:26

+1

@pavpanchekha:另外,BLAS/LAPACK矩陣乘法的基本算法當然不是在上面的閉包中實現的天真的算法(其具有O(n 3)複雜性(對於方陣)。參見http://en.wikipedia.org /維基/ Matrix_multiplication#Algorithms_for_efficient_matrix_multiplication – 2012-01-28 18:04:51

4

Numpy針對線性代數進行了高度優化。當然對於大型矩陣來說,大多數處理都在本地C代碼中。

爲了與此性能相匹配(假設它可能在Java中),您將不得不剝去Clojure的大部分抽象:在迭代大型矩陣時不要使用具有匿名函數的map,添加類型提示以啓用raw Java數組等

也許是最好的選擇就是使用現成的Java庫,用於數值計算(http://math.nist.gov/javanumerics/或相似)進行了優化。

+14

其實,numpy的甚至沒有做C中的計算,必須的。它只是調用'BLAS'程序。如果你有'MKL'或'安裝ATLAS',(如果numpy的配置爲使用它們),它會調用他們的'BLAS'例程來進行矩陣乘法,其中一部分基本上是手動調整的程序集。無論如何,我的觀點是numpy(如果配置正確)會擊敗OP寫入的clojure例子在C中,所以最好的選擇可能更像是Incanter等等(正如你所建議的,我只是漫無目的) – 2012-01-17 19:40:05

+2

Incanter使用平行小馬,它也可以利用MKL/ATLAS – dnolen 2012-01-17 19:46:30

0

我沒有任何具體的答案給你;只是一些建議。

  1. 使用分析器找出其中的時間被消耗
  2. 警告上反射和使用類型提示在需要的地方
  3. 您可能不得不放棄一些高層次的結構和去使用loop-recur來排除最後一盎司的性能

IME,Clojure代碼應該與Java非常接近(2或3X)。但你必須努力。

12

Numpy代碼使用內置庫,在過去的幾十年中由Fortran編寫,並由作者,您的CPU供應商和您的OS分銷商(以及Numpy人員)進行優化,以實現最佳性能。你只是做了矩陣乘法的完全直接的,明顯的方法。實際上,性能不同並不令人驚訝。

但是,如果你在用Clojure做insistant,考慮仰視better algorithms,採用直接循環相對於高階功能,如reduce,或者找到一個Java的正確矩陣代數庫(我懷疑有好的在Clojure中,但我真的不知道)由一位稱職的數學家撰寫。

最後,看看如何正確寫出快速Clojure。使用類型提示,在你的代碼上運行一個分析器(驚喜!你的產品功能使用最多的時間),並將高級功能放在緊密循環中。

+4

對於Java來說,一個好的矩陣代數庫僅僅是Clojure等待包裝器的一個很好的矩陣代數庫。 – 2012-01-17 20:30:12

+3

你說的。我找到了JBLAS,並且正在努力將它包裝起來。 – 2012-01-17 21:24:52

25

numpy的被鏈接到已在機器架構的電平數十年優化而Clojure是在最簡單的和幼稚的方式執行乘法BLAS/LAPACK例程。

你有不平凡的矩陣/矢量操作來執行任何時候,你應該鏈接到BLAS/LAPACK。

唯一的一次,這將不會是快是從語言,其中翻譯語言運行庫和LAPACK之間的數據表示的開銷超過花費的時間計算的小矩陣。

-3

只有使用map()纔有意義。這意味着:如果你有一個特定的問題,例如乘以兩個矩陣,不要試圖映射()它,只需乘以矩陣。

我傾向於使用地圖()只有當它使語言意義(即,如果該程序真的比沒有它更具有可讀性)。乘法矩陣是如此明顯的循環,映射它是沒有意義的。

你的。

Pedro Fortuny。

9

作爲@littleidea和其他人指出,你的numpy版本正在使用LAPACK/BLAS/ATLAS,它比你在clojure中做的任何事都快得多,因爲它已經被多年精心調整過了。 :)

也就是說用Clojure的代碼最大的問題是,它是用雙打,如盒裝雙打。我稱之爲「懶惰的雙重」問題,並且我在工作中遇到過很多次。到目前爲止,即使是1.3,clojure的系列也不是原始的友好。 (你可以創建一個基元向量,但它不會幫助你,因爲所有的seq。函數最終都會將它們裝箱!我還應該說1.3中的原始改進相當不錯,並最終幫助我們不是100%有在收藏WRT原始的支持。)

當用Clojure做任何類型的矩陣數學,你真的需要使用Java數組或者更好的是,矩陣庫。 Incanter確實使用parrelcolt,但您需要小心使用什麼樣的incanter功能......因爲它們中的很多使得矩陣變得可用,最終將雙打裝箱給予您與當前所看到的類似的性能。 (順便說一句,我有我自己的設置parrelcolt包裝,我可以釋放,如果你認爲他們會有所幫助。)

爲了使用BLAS庫,你在java-land中有幾個選項。有了所有這些選項,您必須支付JNA納稅......您的所有數據都必須先進行復制,然後才能進行處理。如果您正在進行CPU綁定操作(如矩陣分解),並且其處理時間比複製數據需要的時間更長,則此稅務是有意義的。對於使用小矩陣的簡單操作,留在java-land中的速度可能會更快。你只需要做一些測試,就像你上面所做的那樣,看看最適合你的是什麼。

這裏是您使用BLAS從Java選項:

http://jblas.org/

http://code.google.com/p/netlib-java/

我要指出的是parrelcolt使用NETLIB -java項目。這意味着,我相信,如果你正確地設置它將使用BLAS。但是,我沒有驗證這一點。有關jblas和NETLIB的Java之間的區別交代看到這個線程我開始它jblas的郵件列表上:

http://groups.google.com/group/jblas-users/browse_thread/thread/c9b3867572331aa5

我還要指出通用的Java矩陣包庫:

http://sourceforge.net/projects/ujmp/

它包裝了我提到的所有庫,然後一些!我沒有看太多的API,但知道它們的抽象是多麼的漏洞。這似乎是一個很好的項目。我已經結束了使用我自己的parrelcolt clojure包裝,因爲他們足夠快,我其實很喜歡小馬API。 (Colt使用函數對象,這意味着我只能通過clojure函數而沒有什麼麻煩!)

5

如果你想在Clojure中使用數值,我強烈推薦使用Incanter而不是試圖推出自己的矩陣函數等等。

煽動者在引擎蓋下使用Parallel Colt,這非常快。

編輯:

截至2013年年初,如果你想要做Clojure中NUMERICS我強烈建議檢查出core.matrix

14

我剛剛上演Incanter 1.3和1.2 jBLAS之間的小槍戰。 1。下面的代碼:

(ns ml-class.experiments.mmult 
    [:use [incanter core]] 
    [:import [org.jblas DoubleMatrix]]) 

(defn -main [m] 
    (let [n 23 m (Integer/parseInt m) 
     ai (matrix (vec (double-array (* m n) (repeatedly rand))) n) 
     ab (DoubleMatrix/rand m n) 
     ti (copy (trans ai)) 
     tb (.transpose ab)] 
    (dotimes [i 20] 
     (print "Incanter: ") (time (mmult ti ai)) 
     (print " jBLAS: ") (time (.mmul tb ab))))) 

在我的測試,咒術持續高於jBLAS約45%純矩陣乘法慢。然而,Incanter trans函數不會創建矩陣的新副本,因此jBLAS中的(.mmul (.transpose ab) ab)需要兩倍的內存,並且只比Incanter中的(mmult (trans ai) ai)快012%。

鑑於甕器豐富的功能集(尤其是繪圖庫),我不認爲我會很快切換到jBLAS。儘管如此,我還是希望看到jBLAS和Parallel Colt之間的又一次槍戰,也許值得考慮用Incantter中的jBLAS替換Parallel Colt?:-)


編輯:這裏是絕對數字(以毫秒爲單位)。我得到了我的(相當慢)郵編:

Incanter: 665.362452 
    jBLAS: 459.311598 
    numpy: 353.777885 

對於每個庫,我選擇了最好的超過20次運行,矩陣大小23x400000。

PS。哈斯克爾hmatrix結果接近numpy,但我不知道如何正確地進行基準測試。