Biblioteca de manipulación rápida de matrices para Elixir implementada en código nativo C con CBLAS sgemm() altamente optimizado utilizado para la multiplicación de matrices.
Por ejemplo, la regresión lineal vectorizada es aproximadamente 13 veces más rápida que la implementación de un solo subproceso de Octave.
También es eficiente en cuanto a memoria, por lo que puede trabajar con matrices grandes, de alrededor de mil millones de elementos.
Basado en código matricial de https://gitlab.com/sdwolfz/experimental/-/tree/master/exlearn
MacBook Pro 2015, núcleo i7 de 2,2 GHz, 16 GB de RAM
Las operaciones se realizan en matrices de 3000 × 3000 llenas de números aleatorios.
Puede ejecutar pruebas comparativas desde la carpeta /bench
con los comandos python numpy_bench.py
y 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 realidad, masacre de inocentes.
MacBook Pro 2015, núcleo i7 de 2,2 GHz, 16 GB de RAM
Producto escalar de matrices de 500 × 500
Biblioteca | Operaciones/seg | Comparado con Matrex |
---|---|---|
matrex | 674,70 | |
Matriz | 0.0923 | 7 312,62× más lento |
numexi | 0.0173 | 38 906,14× más lento |
exmatriz | 0.0129 | 52 327,40× más lento |
Producto escalar de matrices de 3 × 3
Biblioteca | Operaciones/seg | Comparado con Matrex |
---|---|---|
matrex | 3624,36 kilos | |
GraficaMatemáticas | 1310,16 mil | 2,77 veces más lento |
Matriz | 372,58 kilos | 9,73 veces más lento |
numexi | 89,72 mil | 40,40 veces más lento |
exmatriz | 35,76 mil | 101,35 veces más lento |
Transposición de matriz de 1000x1000
Biblioteca | Operaciones/seg | Comparado con Matrex |
---|---|---|
matrex | 428,69 | |
exmatriz | 9.39 | 45,64 veces más lento |
Matriz | 8.54 | 50,17 veces más lento |
numexi | 6.83 | 62,80 veces más lento |
Ejemplo completo de la biblioteca Matrex en funcionamiento: regresión lineal en dígitos MNIST (cuaderno Jupyter)
Matrex implementa el protocolo Inspect
y se ve bien en su consola:
¡Incluso puede dibujar un mapa de calor de tu matriz en la consola! Aquí hay una animación del entrenamiento de regresión logística con la biblioteca Matrex y algunos mapas de calor matriciales:
El paquete se puede instalar agregando matrex
a su lista de dependencias en mix.exs
:
def deps do
[
{ :matrex , "~> 0.6" }
]
end
Todo funciona desde el primer momento, gracias al marco Accelerate. Si encuentra un error de compilación
native/src/matrix_dot.c:5:10: fatal error: 'cblas.h' file not found
luego asegúrese de que las herramientas de línea de comandos de XCode estén instaladas ( xcode-select --install
). Si el error aún no se resuelve, para MacOS Mojave, ejecute open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg
para restaurar /usr/include y /usr/lib.
En MacOS 10.15 este error se puede solucionar con
export CPATH=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/
o con
C_INCLUDE_PATH=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/Headers mix compile
Necesita instalar bibliotecas científicas para que este paquete se pueda compilar:
> sudo apt-get install build-essential erlang-dev libatlas-base-dev
Definitivamente funcionará en Windows, pero necesitamos un archivo MAKE e instrucciones de instalación. Por favor, contribuye.
Con la ayuda de la variable de entorno MATREX_BLAS
puede elegir con qué biblioteca BLAS vincular. Puede tomar valores blas
(el predeterminado), atlas
, openblas
o noblas
.
La última opción significa que compilas código C sin dependencias externas, por lo que debería funcionar en cualquier lugar con un compilador de C:
$ mix clean
$ MATREX_BLAS=noblas mix compile
El comportamiento de acceso está parcialmente implementado para Matrex, por lo que puede hacer:
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
O incluso:
iex > m [ 1 .. 2 ]
#Matrex[2×3]
┌ ┐
│ 8.0 1.0 6.0 │
│ 3.0 5.0 7.0 │
└ ┘
También hay varios atajos para obtener las dimensiones de la matriz:
iex > m [ :rows ]
3
iex > m [ :size ]
{ 3 , 3 }
calcular el valor máximo de toda la matriz:
iex > m [ :max ]
9.0
o solo una de sus filas:
iex > m [ 2 ] [ :max ]
7.0
calcular el índice de base uno del elemento máximo para toda la matriz:
iex > m [ :argmax ]
8
y una fila:
iex > m [ 2 ] [ :argmax ]
3
El módulo Matrex.Operators
redefine los operadores matemáticos Kernel
(+, -, *, / <|>) y define algunas funciones convenientes, para que pueda escribir código de cálculo de una manera más natural.
Debe usarse con mucha precaución. Sugerimos usarlo solo dentro de funciones específicas y solo para aumentar la legibilidad, porque el uso de funciones del módulo Matrex
, especialmente aquellas que realizan dos o más operaciones en una llamada, son 2 o 3 veces más rápidos.
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 misma función, codificada con llamadas a métodos de módulo (2,5 veces más rápido):
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 implementa Enumerable
, por lo que son aplicables todo tipo de funciones Enum
:
iex > Enum . member? ( m , 2.0 )
true
iex > Enum . count ( m )
9
iex > Enum . sum ( m )
45
Para funciones que existen tanto en Enum
como en Matrex
, se prefiere usar la versión Matrex, porque suele ser mucho, mucho más rápida. Es decir, para matrices de 1 000 x 1 000 Matrex.sum/1
y Matrex.to_list/1
son 438 y 41 veces más rápidos, respectivamente, que sus homólogos Enum
.
Puede guardar/cargar matrices con formato de archivo binario nativo (muy rápido) y CSV (lento, especialmente en matrices grandes).
El formato Matrex CSV es compatible con la salida GNU Octave CSV, por lo que puede usarlo para intercambiar datos entre dos sistemas.
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 │
└ ┘
Los valores flotantes especiales, como :nan
y :inf
viven bien dentro de matrices, se pueden cargar y guardar en archivos. Pero al introducirlos en Elixir, se transfieren a los átomos :nan
, :inf
y :neg_inf
, porque BEAM no acepta valores especiales como flotantes válidos.
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 )