Schnelle Matrixmanipulationsbibliothek für Elixir, implementiert in nativem C-Code mit hochoptimiertem CBLAS sgemm() für die Matrixmultiplikation.
Beispielsweise ist die vektorisierte lineare Regression etwa 13-mal schneller als die Single-Threaded-Implementierung von Octave.
Es ist außerdem speichereffizient, sodass Sie mit großen Matrizen arbeiten können, die etwa eine Milliarde Elemente groß sind.
Basierend auf Matrixcode von https://gitlab.com/sdwolfz/experimental/-/tree/master/exlearn
2015 MacBook Pro, 2,2 GHz Core i7, 16 GB RAM
Operationen werden an 3000×3000-Matrizen durchgeführt, die mit Zufallszahlen gefüllt sind.
Sie können Benchmarks aus dem Ordner /bench
mit python numpy_bench.py
und MIX_ENV=bench mix bench
ausführen.
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)
Eigentlich ein Abschlachten der Unschuldigen.
2015 MacBook Pro, 2,2 GHz Core i7, 16 GB RAM
Skalarprodukt von 500×500-Matrizen
Bibliothek | Operationen/Sek | Im Vergleich zu Matrex |
---|---|---|
Matrex | 674,70 | |
Matrix | 0,0923 | 7 312,62× langsamer |
Numexy | 0,0173 | 38 906,14× langsamer |
ExMatrix | 0,0129 | 52 327,40× langsamer |
Skalarprodukt von 3×3-Matrizen
Bibliothek | Operationen/Sek | Im Vergleich zu Matrex |
---|---|---|
Matrex | 3624,36 K | |
GraphMath | 1310,16 K | 2,77x langsamer |
Matrix | 372,58 K | 9,73x langsamer |
Numexy | 89,72 K | 40,40x langsamer |
ExMatrix | 35,76 K | 101,35x langsamer |
Transponierende 1000x1000-Matrix
Bibliothek | Operationen/Sek | Im Vergleich zu Matrex |
---|---|---|
Matrex | 428,69 | |
ExMatrix | 9.39 | 45,64× langsamer |
Matrix | 8.54 | 50,17× langsamer |
Numexy | 6,83 | 62,80× langsamer |
Vollständiges Beispiel der Matrex-Bibliothek bei der Arbeit: Lineare Regression auf MNIST-Ziffern (Jupyter-Notizbuch)
Matrex implementiert Inspect
-Protokoll und sieht in Ihrer Konsole gut aus:
Es kann sogar eine Heatmap Ihrer Matrix in der Konsole zeichnen! Hier ist eine Animation des logistischen Regressionstrainings mit der Matrex-Bibliothek und einigen Matrix-Heatmaps:
Das Paket kann installiert werden, indem Sie matrex
zu Ihrer Abhängigkeitsliste in mix.exs
hinzufügen:
def deps do
[
{ :matrex , "~> 0.6" }
]
end
Dank des Accelerate-Frameworks funktioniert alles sofort. Wenn ein Kompilierungsfehler auftritt
native/src/matrix_dot.c:5:10: fatal error: 'cblas.h' file not found
Stellen Sie dann sicher, dass die XCode-Befehlszeilentools installiert sind ( xcode-select --install
). Wenn der Fehler immer noch nicht behoben ist, führen Sie für MacOS Mojave open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg
aus, um /usr/include und /usr/lib wiederherzustellen.
Unter MacOS 10.15 kann dieser Fehler mit behoben werden
export CPATH=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/
oder mit
C_INCLUDE_PATH=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/Headers mix compile
Sie müssen wissenschaftliche Bibliotheken installieren, damit dieses Paket kompiliert werden kann:
> sudo apt-get install build-essential erlang-dev libatlas-base-dev
Es wird auf jeden Fall unter Windows funktionieren, wir benötigen jedoch ein Makefile und eine Installationsanleitung. Bitte tragen Sie bei.
Mithilfe der Umgebungsvariablen MATREX_BLAS
können Sie auswählen, mit welcher BLAS-Bibliothek eine Verknüpfung hergestellt werden soll. Es kann die Werte blas
(Standard), atlas
, openblas
oder noblas
annehmen.
Die letzte Option bedeutet, dass Sie C-Code ohne externe Abhängigkeiten kompilieren, sodass er überall mit einem C-Compiler-Ort funktionieren sollte:
$ mix clean
$ MATREX_BLAS=noblas mix compile
Das Zugriffsverhalten ist teilweise für Matrex implementiert, sodass Sie Folgendes tun können:
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
Oder auch:
iex > m [ 1 .. 2 ]
#Matrex[2×3]
┌ ┐
│ 8.0 1.0 6.0 │
│ 3.0 5.0 7.0 │
└ ┘
Es gibt auch mehrere Abkürzungen zum Abrufen der Matrixdimensionen:
iex > m [ :rows ]
3
iex > m [ :size ]
{ 3 , 3 }
Berechnung des Maximalwerts der gesamten Matrix:
iex > m [ :max ]
9.0
oder nur eine seiner Zeilen:
iex > m [ 2 ] [ :max ]
7.0
Berechnen des einsbasierten Index des maximalen Elements für die gesamte Matrix:
iex > m [ :argmax ]
8
und eine Zeile:
iex > m [ 2 ] [ :argmax ]
3
Matrex.Operators
-Modul definiert die mathematischen Kernel
-Operatoren (+, -, *, / <|>) neu und definiert einige praktische Funktionen, sodass Sie Berechnungscode auf natürlichere Weise schreiben können.
Es sollte mit großer Vorsicht verwendet werden. Wir empfehlen, es nur innerhalb bestimmter Funktionen und nur zur besseren Lesbarkeit zu verwenden, da die Verwendung von Matrex
-Modulfunktionen, insbesondere solchen, die zwei oder mehr Vorgänge bei einem Aufruf ausführen, zwei bis drei Mal schneller ist.
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
Dieselbe Funktion, codiert mit Modulmethodenaufrufen (2,5-mal schneller):
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 implementiert Enumerable
, sodass alle Arten von Enum
-Funktionen anwendbar sind:
iex > Enum . member? ( m , 2.0 )
true
iex > Enum . count ( m )
9
iex > Enum . sum ( m )
45
Für Funktionen, die sowohl in Enum
als auch in Matrex
vorhanden sind, wird die Verwendung der Matrex-Version bevorzugt, da diese normalerweise viel, viel schneller ist. Das heißt, für eine 1.000 x 1.000-Matrix sind Matrex.sum/1
und Matrex.to_list/1
438 bzw. 41 Mal schneller als ihre Enum
Gegenstücke.
Sie können die Matrix im nativen Binärdateiformat (besonders schnell) und im CSV-Format (langsam, insbesondere bei großen Matrizen) speichern/laden.
Das Matrex CSV-Format ist mit der GNU Octave CSV-Ausgabe kompatibel, sodass Sie es zum Datenaustausch zwischen zwei Systemen verwenden können.
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 │
└ ┘
Float-Sonderwerte wie :nan
und :inf
leben gut in Matrizen und können aus Dateien geladen und in Dateien gespeichert werden. Aber wenn sie in Elixir übernommen werden, werden sie in :nan
, :inf
und :neg_inf
-Atome übertragen, da BEAM keine Sonderwerte als gültige Floats akzeptiert.
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 )