Matrix Multiplication in Clojure vs Numpy
我正在Clojure中开发一个应用程序,该应用程序需要乘以大型矩阵,并且与同一个Numpy版本相比,遇到了一些大型性能问题。 Numpy似乎可以在一秒钟内通过其转置来乘以1,000,000x23矩阵,而等效的clojure代码则需要六分钟的时间。 (我可以从Numpy中打印出结果矩阵,因此肯定可以评估所有内容)。
我在此Clojure代码中是否做错了什么? 我可以尝试模仿Numpy的一些技巧吗?
这是python:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | (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包裹这种吸盘了。 这是新的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | (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 |
Python版本正在编译为C语言中的循环,而Clojure版本正在为此代码中映射的每个调用构建新的中间序列。您看到的性能差异很可能来自数据结构的差异。
要获得更好的效果,您可以使用类似Incanter的库,或者按照此SO问题中的说明编写自己的版本。另请参阅此人,穴居人或nd4j。如果您真的想保留序列以保留惰性评估属性等,那么可以通过查看内部矩阵计算的瞬变来获得真正的推动力
编辑:忘记添加调整Clojure的第一步,请打开"警告反射"
Numpy与BLAS / Lapack例程相关联,这些例程在机器体系结构级别上已进行了数十年的优化,而Clojure则以最直接,最幼稚的方式实现乘法。
每当您要执行非平凡的矩阵/矢量运算时,您都应该链接到BLAS / LAPACK。
唯一不会更快的时间是来自语言的小型矩阵,这些语言的语言在语言运行时和LAPACK之间转换数据表示的开销超过了计算所花费的时间。
我刚刚在Incanter 1.3和jBLAS 1.2.1之间上演了一次小型枪战。 这是代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 | (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))))) |
在我的测试中,在纯矩阵乘法中,Incanter始终比jBLAS慢约45%。 但是,Incanter
有了Incanter丰富的功能集(尤其是绘图库),我认为我不会很快转换到jBLAS。 尽管如此,我还是希望看到jBLAS和Parallel Colt之间的另一场枪战,也许值得考虑在Incanter中用jBLAS替换Parallel Colt吗? :-)
编辑:这是绝对数字(以毫秒为单位)。
1 2 3 | Incanter: 665.362452 jBLAS: 459.311598 numpy: 353.777885 |
对于每个库,我从20个运行中选择了最佳时间,矩阵大小为23x400000。
PS。 Haskell hmatrix结果接近numpy,但是我不确定如何正确对其进行基准测试。
Numpy代码使用过去几十年来用Fortran编写的内置库,并由作者,您的CPU供应商和您的OS发行商(以及Numpy员工)进行了优化,以实现最佳性能。您只是对矩阵乘法做了完全直接,显而易见的方法。确实,性能有所不同也就不足为奇了。
但是,如果您坚持在Clojure中执行此操作,请考虑查找更好的算法,使用直接循环而不是像
最后,查找如何正确编写快速Clojure。使用类型提示,在代码上运行事件探查器(惊奇!您乘积产品功能消耗的时间最多),并将高级功能放到紧密的循环中。
正如@littleidea和其他人指出的那样,您的Numpy版本正在使用LAPACK / BLAS / ATLAS,这比Clojure中的任何操作都要快得多,因为它已经进行了多年的微调。 :)
也就是说,Clojure代码的最大问题在于它正在使用Doubles,就像在盒装Doubles中一样。我将其称为"懒惰的双重问题",并且在工作中遇到了很多次。到目前为止,即使使用1.3,clojure的集合也不是原始友好的。 (您可以创建一个原语向量,但它对您没有任何帮助,因为所有的seq。函数最终都将它们装箱!我还应该说1.3中的原语改进非常好,最终对您有所帮助。集合中不是100%有WRT基本支持。)
在Clojure中进行任何形式的矩阵数学运算时,您确实需要使用Java数组,或者更好的是,使用矩阵库。 Incanter确实使用了parrelcolt,但是您需要小心使用哪些incanter函数...因为它们中的许多函数都使矩阵可排序,最终将双精度框装箱,使您获得与当前所见性能相似的性能。 (顺便说一句,我有我自己设置的parrelcolt包装器,如果您认为它们会有所帮助,则可以发布。)
为了使用BLAS库,您可以在java-land中选择两个选项。使用所有这些选项,您必须支付JNA税...必须先复制所有数据,然后才能对其进行处理。当您执行诸如矩阵分解之类的CPU约束操作并且其处理时间比复制数据所花费的时间更长时,这种税收是有意义的。对于使用较小矩阵的更简单操作,则停留在Java领域可能会更快。您只需要像上面一样进行一些测试,即可确定最适合您的方法。
这是从Java使用BLAS的选项:
http://jblas.org/
http://code.google.com/p/netlib-java/
- 以上版本的API:http://code.google.com/p/matrix-toolkits-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包装器,因为它们足够快,而且我实际上非常喜欢colt API。 (Colt使用函数对象,这意味着我可以轻松传递clojure函数!)
如果您想在Clojure中进行数字运算,我强烈建议您使用Incanter,而不要尝试滚动自己的矩阵函数或类似函数。
Incanter使用引擎盖下的平行小马驹,速度非常快。
编辑:
从2013年初开始,如果您想在Clojure中进行数字运算,我强烈建议您查看core.matrix
Numpy已针对线性代数进行了高度优化。当然,对于大型矩阵而言,大多数处理都是在本机C代码中进行的。
为了达到这种性能(假设它可以在Java中使用),您将不得不去除Clojure的大多数抽象:在大型矩阵上进行迭代时,请勿将map与匿名函数一起使用,添加类型提示以启用原始Java数组,等等。 。
最好的选择可能只是使用针对数字计算优化的现成Java库(http://math.nist.gov/javanumerics/或类似名称)。
对于您,我没有任何具体答案;只是一些建议。
IME,Clojure代码的性能应该非常接近Java(2或3X)。但是你必须努力。
仅在有意义的情况下使用map()。这意味着:如果您有一个特殊的问题,例如将两个矩阵相乘,则不要尝试对它进行map()运算,只需将矩阵相乘即可??。
我倾向于仅在具有语言意义的情况下才使用map()(即,如果程序确实比没有它的情况下更具可读性)。矩阵相乘非常明显,因此无法进行映射。
你的
佩德罗·福图尼(Pedro Fortuny)。