41 votes

Multiplication matricielle en Clojure vs Numpy

Je travaille sur une application en Clojure qui doit multiplier de grandes matrices et je rencontre de gros problèmes de performance par rapport à une version Numpy identique. Numpy semble être capable de multiplier une matrice 1 000 000x23 par sa transposition en moins d'une seconde, alors que le code Clojure équivalent prend plus de six minutes (je peux imprimer la matrice résultante à partir de Numpy, donc il évalue bien tout).

Est-ce que je fais quelque chose de terriblement mal dans ce code Clojure ? Y a-t-il une astuce de Numpy que je peux essayer d'imiter ?

Voici le python :

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)

Et le 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))

MISE À JOUR : J'ai mis en œuvre le même benchmark en utilisant la fonction JBLAS et ont trouvé des améliorations massives, massives de la vitesse. Merci à tous pour leur contribution ! Il est temps d'envelopper cet aspirateur dans Clojure. Voici le nouveau code :

(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

32voto

Arthur Ulfeldt Points 45059

La version Python compile jusqu'à une boucle en C tandis que la version Clojure construit une nouvelle séquence intermédiaire pour chacun des appels à map dans ce code. Il est probable que la différence de performance que vous constatez provienne de la différence de structures de données.

Pour faire mieux que cela, vous pouvez jouer avec une bibliothèque telle que Incantateur ou écrivez votre propre version comme expliqué dans cette question SO . voir également celui-ci , néandertalien o nd4j . Si vous souhaitez vraiment rester dans le domaine des séquences pour conserver les propriétés d'évaluation paresseuses, etc. transitoires pour les calculs de la matrice interne

EDIT : j'ai oublié d'ajouter la première étape de l'accordage

28 votes

Pas en C ; une grande partie de BLAS et de LAPACK sont en Fortran. La version Python ne "compile" pas non plus une boucle en C ; elle utilise simplement une bibliothèque écrite en C/Fortran. Enfin, les algorithmes utilisés sont complètement différents. Il est plutôt improbable qu'une description de l'approche de Numpy ait le moindre rapport avec le code affiché par notre poster original.

1 votes

@pavpanchekha : également, l'algorithme sous-jacent de la multiplication matricielle BLAS/LAPACK n'est certainement pas celui, naïf, implémenté dans la fermeture ci-dessus (qui a une complexité O(n³) pour une matrice carrée). Voir fr.wikipedia.org/wiki/

0 votes

Comme il s'agit d'une excellente réponse, j'ai pris la liberté d'ajouter quelques options modernes supplémentaires.

27voto

littleidea Points 374

Numpy se lie à des routines BLAS/Lapack qui ont été optimisées pendant des décennies au niveau de l'architecture de la machine, tandis que Clojure implémente la multiplication de la manière la plus directe et naïve.

Chaque fois que vous avez des opérations matricielles/vectorielles non triviales à effectuer, vous devriez probablement vous lier à BLAS/LAPACK.

Le seul cas où cela ne sera pas plus rapide est celui des petites matrices issues de langages où les frais de traduction de la représentation des données entre le runtime du langage et LAPACK dépassent le temps passé à effectuer le calcul.

14voto

motus Points 131

Je viens de mettre en scène une petite fusillade entre Incantateur 1.3 et jBLAS 1.2.1. Voici le code :

(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)))))

Dans mon test, Incanter est constamment plus lent que jBLAS d'environ 45% dans une simple multiplication de matrice. Cependant, Incanter trans ne crée pas une nouvelle copie d'une matrice, et donc la fonction (.mmul (.transpose ab) ab) dans jBLAS prend deux fois plus de mémoire et est seulement 15% plus vite que (mmult (trans ai) ai) dans Incanter.

Étant donné la richesse des fonctionnalités d'Incanter (en particulier sa bibliothèque de traçage), je ne pense pas que je vais passer à jBLAS de sitôt. Néanmoins, j'aimerais bien voir une autre confrontation entre jBLAS et Parallel Colt, et peut-être que cela vaut la peine d'envisager de remplacer Parallel Colt par jBLAS dans Incanter :-)


EDIT : Voici les chiffres absolus (en msec.) que j'ai obtenus sur mon PC (plutôt lent) :

Incanter: 665.362452
   jBLAS: 459.311598
   numpy: 353.777885

Pour chaque bibliothèque, j'ai choisi le meilleur temps sur 20 passages, taille de la matrice 23x400000.

PS. Les résultats de Haskell hmatrix sont proches de ceux de numpy, mais je ne suis pas sûr de savoir comment l'évaluer correctement.

12voto

pavpanchekha Points 1348

Le code Numpy utilise des bibliothèques intégrées, écrites en Fortran au cours des dernières décennies et optimisées par les auteurs, votre fournisseur de processeur et votre distributeur de système d'exploitation (ainsi que par les gens de Numpy) pour une performance maximale. Vous venez de faire l'approche complètement directe et évidente de la multiplication de matrices. Il n'est pas surprenant, vraiment, que les performances diffèrent.

Mais si vous tenez absolument à le faire en Clojure, pensez à consulter le site de meilleurs algorithmes en utilisant des boucles directes par opposition à des fonctions d'ordre supérieur telles que reduce ou trouver une bibliothèque d'algèbre matricielle appropriée pour Java (je doute qu'il en existe de bonnes en Clojure, mais je ne sais pas vraiment) écrite par un mathématicien compétent.

Enfin, regardez comment écrire correctement et rapidement en Clojure. Utilisez les indications de type, exécutez un profileur sur votre code (surprise ! c'est votre fonction de produit de points qui consomme le plus de temps), et laissez tomber les fonctions de haut niveau dans vos boucles serrées.

4 votes

Une bonne librairie d'algèbre matricielle pour Java est juste une bonne librairie d'algèbre matricielle pour Clojure qui attend un wrapper.

3 votes

Tu l'as dit. J'ai trouvé JBLAS et je suis en train de l'emballer en ce moment.

9voto

Ben Mabey Points 517

Comme @littleidea et d'autres l'ont souligné, votre version de numpy utilise LAPACK/BLAS/ATLAS qui sera beaucoup plus rapide que tout ce que vous ferez en clojure puisqu'il a été finement mis au point depuis des années :).

Cela dit, le plus gros problème avec le code Clojure est qu'il utilise des doubles, comme des doubles encadrés. J'appelle cela le problème du "double paresseux" et je l'ai rencontré à plusieurs reprises au travail. À l'heure actuelle, même avec la version 1.3, les collections de Clojure ne sont pas adaptées aux primitives (vous pouvez créer un vecteur de primitives, mais cela ne vous sera d'aucune utilité puisque toutes les fonctions seq. finiront par les mettre en boîte ! Je dois également dire que les améliorations apportées aux primitives dans la version 1.3 sont plutôt sympathiques et finissent par aider nous ne sommes simplement pas encore à 100% en ce qui concerne le support des primitives dans les collections).

Pour effectuer tout type de calcul matriciel en clojure, vous devez vraiment utiliser des tableaux java ou, mieux encore, des bibliothèques de matrices. Incanter utilise parrelcolt mais vous devez faire attention aux fonctions d'Incanter que vous utilisez... car beaucoup d'entre elles rendent les matrices séquençables, ce qui finit par mettre en boîte les doubles et vous donne des performances similaires à celles que vous voyez actuellement. (BTW, j'ai ma propre configuration de wrappers parrelcolt que je pourrais publier si vous pensez qu'ils seraient utiles).

Afin d'utiliser les bibliothèques BLAS, vous avez deux options au pays de Java. Avec toutes ces options, vous devez payer une taxe JNA... toutes vos données doivent être copiées avant de pouvoir être traitées. Cette taxe a un sens lorsque vous effectuez des opérations liées au CPU, comme des décompositions de matrices, et dont le temps de traitement est plus long que le temps nécessaire pour copier les données. Pour des opérations plus simples avec de petites matrices, rester au pays de Java sera probablement plus rapide. Il vous suffit de faire quelques tests comme ceux que vous avez faits ci-dessus pour voir ce qui fonctionne le mieux pour vous.

Voici vos options pour utiliser BLAS à partir de java :

http://jblas.org/

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

Je dois préciser que parrelcolt utilise le projet netlib-java. Ce qui signifie, je crois, que si vous le configurez correctement, il utilisera BLAS. Cependant, je ne l'ai pas vérifié. Pour une explication sur les différences entre jblas et netlib-java, voir le fil de discussion que j'ai lancé à ce sujet sur la liste de diffusion de jblas :

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

Je dois également signaler la bibliothèque Universal Java Matrix Package :

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

Il englobe toutes les bibliothèques que j'ai mentionnées, et même plus ! Je n'ai pas trop regardé l'API pour savoir à quel point leur abstraction est perméable. Cela semble être un bon projet. J'ai fini par utiliser mes propres wrappers clojure parrelcolt puisqu'ils étaient assez rapides et que j'aimais bien l'API de Colt. (Colt utilise des objets fonctionnels, ce qui signifie que j'ai pu simplement passer des fonctions clojure sans trop de problèmes).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X