关于python:Clojure与Numpy中的矩阵乘法

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 trans函数不会创建矩阵的新副本,因此jBLAS中的(.mmul (.transpose ab) ab)占用的内存是两倍,并且仅比Incanter中的(mmult (trans ai) ai)快15%。

有了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中执行此操作,请考虑查找更好的算法,使用直接循环而不是像reduce这样的高阶函数,或者为Java找到合适的矩阵代数库(我怀疑在Java中有好的代数库) 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)。