Bibliothèque de manipulation matricielle rapide pour Elixir implémentée en code natif C avec CBLAS sgemm() hautement optimisé utilisé pour la multiplication matricielle.
Par exemple, la régression linéaire vectorisée est environ 13 fois plus rapide que l’implémentation à thread unique d’Octave.
Il est également économe en mémoire, ce qui vous permet de travailler avec de grandes matrices, d'une taille d'environ un milliard d'éléments.
Basé sur le code matriciel de https://gitlab.com/sdwolfz/experimental/-/tree/master/exlearn
MacBook Pro 2015, Core i7 2,2 GHz, 16 Go de RAM
Les opérations sont effectuées sur des matrices 3000×3000 remplies de nombres aléatoires.
Vous pouvez exécuter des tests à partir du dossier /bench
avec les commandes python numpy_bench.py
et MIX_ENV=bench mix bench
.
benchmark iterations average time
logistic_cost() 1000 1.23 ms/op
np.divide(A, B) 100 15.43 ms/op
np.add(A, B) 100 14.62 ms/op
sigmoid(A) 50 93.28 ms/op
np.dot(A, B) 10 196.57 ms/op
benchmark iterations average time
logistic_cost() 1000 1.23 ms/op (on par)
divide(A, B) 200 7.32 ms/op (~ 2× faster)
add(A, B) 200 7.71 ms/op (~ 2× faster)
sigmoid(A) 50 71.47 ms/op (23% faster)
dot(A, B) 10 213.31 ms/op (8% slower)
En fait, c'est le massacre d'innocents.
MacBook Pro 2015, Core i7 2,2 GHz, 16 Go de RAM
Produit scalaire de matrices 500×500
Bibliothèque | Opérations/s | Comparé à Matrex |
---|---|---|
Matrex | 674,70 | |
Matrice | 0,0923 | 7 312,62× plus lent |
Numéroxy | 0,0173 | 38 906,14× plus lent |
ExMatrice | 0,0129 | 52 327,40× plus lent |
Produit scalaire de matrices 3×3
Bibliothèque | Opérations/s | Comparé à Matrex |
---|---|---|
Matrex | 3624,36K | |
GraphiqueMath | 1310,16 Ko | 2,77x plus lent |
Matrice | 372,58 Ko | 9,73x plus lent |
Numéroxy | 89,72 Ko | 40,40x plus lent |
ExMatrice | 35,76 Ko | 101,35x plus lent |
Transposition d'une matrice 1000x1000
Bibliothèque | Opérations/s | Comparé à Matrex |
---|---|---|
Matrex | 428,69 | |
ExMatrice | 9h39 | 45,64× plus lent |
Matrice | 8.54 | 50,17× plus lent |
Numéroxy | 6,83 | 62,80× plus lent |
Exemple complet de la bibliothèque Matrex au travail : régression linéaire sur les chiffres MNIST (carnet Jupyter)
Matrex implémente le protocole Inspect
et est joli dans votre console :
Il peut même dessiner une carte thermique de votre matrice en console ! Voici une animation d'entraînement à la régression logistique avec la bibliothèque Matrex et quelques cartes thermiques matricielles :
Le package peut être installé en ajoutant matrex
à votre liste de dépendances dans mix.exs
:
def deps do
[
{ :matrex , "~> 0.6" }
]
end
Tout fonctionne immédiatement, grâce au framework Accelerate. Si vous rencontrez une erreur de compilation
native/src/matrix_dot.c:5:10: fatal error: 'cblas.h' file not found
puis assurez-vous que les outils de ligne de commande XCode sont installés ( xcode-select --install
). Si l'erreur n'est toujours pas résolue, pour MacOS Mojave, exécutez open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg
pour restaurer /usr/include et /usr/lib.
Sur MacOS 10.15, cette erreur peut être résolue avec
export CPATH=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/
ou avec
C_INCLUDE_PATH=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/Headers mix compile
Vous devez installer des bibliothèques scientifiques pour que ce package compile :
> sudo apt-get install build-essential erlang-dev libatlas-base-dev
Cela fonctionnera certainement sous Windows, mais nous avons besoin d'un makefile et d'instructions d'installation. S'il vous plaît, contribuez.
À l'aide de la variable d'environnement MATREX_BLAS
vous pouvez choisir avec quelle bibliothèque BLAS établir un lien. Il peut prendre les valeurs blas
(la valeur par défaut), atlas
, openblas
ou noblas
.
La dernière option signifie que vous compilez du code C sans aucune dépendance externe, il devrait donc fonctionner n'importe où avec un emplacement de compilateur C :
$ mix clean
$ MATREX_BLAS=noblas mix compile
Le comportement d'accès est en partie implémenté pour Matrex, vous pouvez donc :
iex > m = Matrex . magic ( 3 )
#Matrex[3×3]
┌ ┐
│ 8.0 1.0 6.0 │
│ 3.0 5.0 7.0 │
│ 4.0 9.0 2.0 │
└ ┘
iex > m [ 2 ] [ 3 ]
7.0
Ou encore :
iex > m [ 1 .. 2 ]
#Matrex[2×3]
┌ ┐
│ 8.0 1.0 6.0 │
│ 3.0 5.0 7.0 │
└ ┘
Il existe également plusieurs raccourcis pour obtenir les dimensions de la matrice :
iex > m [ :rows ]
3
iex > m [ :size ]
{ 3 , 3 }
calculer la valeur maximale de toute la matrice :
iex > m [ :max ]
9.0
ou juste une de ses lignes :
iex > m [ 2 ] [ :max ]
7.0
calcul de l'indice de base un de l'élément maximum pour toute la matrice :
iex > m [ :argmax ]
8
et une ligne :
iex > m [ 2 ] [ :argmax ]
3
Le module Matrex.Operators
redéfinit les opérateurs mathématiques Kernel
(+, -, *, / <|>) et définit certaines fonctions pratiques, afin que vous puissiez écrire du code de calcul de manière plus naturelle.
Il doit être utilisé avec beaucoup de prudence. Nous suggérons de l'utiliser uniquement dans des fonctions spécifiques et uniquement pour une meilleure lisibilité, car l'utilisation des fonctions du module Matrex
, en particulier celles qui effectuent deux ou plusieurs opérations en un seul appel, est 2 à 3 fois plus rapide.
def lr_cost_fun_ops ( % Matrex { } = theta , { % Matrex { } = x , % Matrex { } = y , lambda } = _params )
when is_number ( lambda ) do
# Turn off original operators
import Kernel , except: [ -: 1 , +: 2 , -: 2 , *: 2 , /: 2 , <|>: 2 ]
import Matrex.Operators
import Matrex
m = y [ :rows ]
h = sigmoid ( x * theta )
l = ones ( size ( theta ) ) |> set ( 1 , 1 , 0.0 )
j = ( - t ( y ) * log ( h ) - t ( 1 - y ) * log ( 1 - h ) + lambda / 2 * t ( l ) * pow2 ( theta ) ) / m
grad = ( t ( x ) * ( h - y ) + ( theta <|> l ) * lambda ) / m
{ scalar ( j ) , grad }
end
La même fonction, codée avec des appels de méthodes de module (2,5 fois plus rapide) :
def lr_cost_fun ( % Matrex { } = theta , { % Matrex { } = x , % Matrex { } = y , lambda } = _params )
when is_number ( lambda ) do
m = y [ :rows ]
h = Matrex . dot_and_apply ( x , theta , :sigmoid )
l = Matrex . ones ( theta [ :rows ] , theta [ :cols ] ) |> Matrex . set ( 1 , 1 , 0 )
regularization =
Matrex . dot_tn ( l , Matrex . square ( theta ) )
|> Matrex . scalar ( )
|> Kernel . * ( lambda / ( 2 * m ) )
j =
y
|> Matrex . dot_tn ( Matrex . apply ( h , :log ) , - 1 )
|> Matrex . subtract (
Matrex . dot_tn (
Matrex . subtract ( 1 , y ) ,
Matrex . apply ( Matrex . subtract ( 1 , h ) , :log )
)
)
|> Matrex . scalar ( )
|> ( fn
:nan -> :nan
x -> x / m + regularization
end ) . ( )
grad =
x
|> Matrex . dot_tn ( Matrex . subtract ( h , y ) )
|> Matrex . add ( Matrex . multiply ( theta , l ) , 1.0 , lambda )
|> Matrex . divide ( m )
{ j , grad }
end
Matrex implémente Enumerable
, donc toutes sortes de fonctions Enum
sont applicables :
iex > Enum . member? ( m , 2.0 )
true
iex > Enum . count ( m )
9
iex > Enum . sum ( m )
45
Pour les fonctions qui existent à la fois dans Enum
et dans Matrex
il est préférable d'utiliser la version Matrex, car elle est généralement beaucoup, beaucoup plus rapide. Autrement dit, pour une matrice de 1 000 x 1 000 Matrex.sum/1
et Matrex.to_list/1
sont respectivement 438 et 41 fois plus rapides que leurs homologues Enum
.
Vous pouvez enregistrer/charger la matrice au format de fichier binaire natif (extra rapide) et CSV (lent, en particulier sur les grandes matrices).
Le format Matrex CSV est compatible avec la sortie GNU Octave CSV, vous pouvez donc l'utiliser pour échanger des données entre deux systèmes.
iex > Matrex . random ( 5 ) |> Matrex . save ( "rand.mtx" )
:ok
iex > Matrex . load ( "rand.mtx" )
#Matrex[5×5]
┌ ┐
│ 0.05624 0.78819 0.29995 0.25654 0.94082 │
│ 0.50225 0.22923 0.31941 0.3329 0.78058 │
│ 0.81769 0.66448 0.97414 0.08146 0.21654 │
│ 0.33411 0.59648 0.24786 0.27596 0.09082 │
│ 0.18673 0.18699 0.79753 0.08101 0.47516 │
└ ┘
iex > Matrex . magic ( 5 ) |> Matrex . divide ( Matrex . eye ( 5 ) ) |> Matrex . save ( "nan.csv" )
:ok
iex > Matrex . load ( "nan.csv" )
#Matrex[5×5]
┌ ┐
│ 16.0 ∞ ∞ ∞ ∞ │
│ ∞ 4.0 ∞ ∞ ∞ │
│ ∞ ∞ 12.0 ∞ ∞ │
│ ∞ ∞ ∞ 25.0 ∞ │
│ ∞ ∞ ∞ ∞ 8.0 │
└ ┘
Les valeurs spéciales flottantes, comme :nan
et :inf
vivent bien dans les matrices, peuvent être chargées et enregistrées dans des fichiers. Mais lorsqu'ils sont introduits dans Elixir, ils sont transférés vers les atomes :nan
, :inf
et :neg_inf
, car BEAM n'accepte pas les valeurs spéciales comme flottants valides.
iex > m = Matrex . eye ( 3 )
#Matrex[3×3]
┌ ┐
│ 1.0 0.0 0.0 │
│ 0.0 1.0 0.0 │
│ 0.0 0.0 1.0 │
└ ┘
iex > n = Matrex . divide ( m , Matrex . zeros ( 3 ) )
#Matrex[3×3]
┌ ┐
│ ∞ NaN NaN │
│ NaN ∞ NaN │
│ NaN NaN ∞ │
└ ┘
iex > n [ 1 ] [ 1 ]
:inf
iex > n [ 1 ] [ 2 ]
:nan
iex ( 166 ) > matrex_logo =
... ( 166 ) > "../emnist/emnist-letters-test-images-idx3-ubyte"
.. . ( 166 ) > |> Matrex . load ( :idx )
.. . ( 166 ) > |> Access . get ( 9601 .. 10200 )
.. . ( 166 ) > |> Matrex . list_of_rows ( )
.. . ( 166 ) > |> Enum . reduce ( fn x , sum -> add ( x , sum ) end )
.. . ( 166 ) > |> Matrex . reshape ( 28 , 28 )
.. . ( 166 ) > |> Matrex . transpose ( )
.. . ( 166 ) > |> Matrex . resize ( 2 )
.. . ( 166 ) > |> Matrex . heatmap ( :color24bit )