Biblioteca rápida de manipulação de matrizes para Elixir implementada em código nativo C com CBLAS sgemm() altamente otimizado usado para multiplicação de matrizes.
Por exemplo, a regressão linear vetorizada é cerca de 13 vezes mais rápida do que a implementação de thread único do Octave.
Também é eficiente em termos de memória, para que você possa trabalhar com matrizes grandes, com tamanho de cerca de bilhões de elementos.
Baseado no código da matriz de https://gitlab.com/sdwolfz/experimental/-/tree/master/exlearn
MacBook Pro 2015, Core i7 de 2,2 GHz, 16 GB de RAM
As operações são realizadas em matrizes 3000×3000 preenchidas com números aleatórios.
Você pode executar benchmarks a partir da pasta /bench
com os comandos python numpy_bench.py
e 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)
Massacre de inocentes, na verdade.
MacBook Pro 2015, Core i7 de 2,2 GHz, 16 GB de RAM
Produto escalar de matrizes 500×500
Biblioteca | Operações/s | Comparado com Matrex |
---|---|---|
Matrex | 674,70 | |
Matriz | 0,0923 | 7 312,62× mais lento |
Numexy | 0,0173 | 38 906,14× mais lento |
ExMatriz | 0,0129 | 52 327,40× mais lento |
Produto escalar de matrizes 3×3
Biblioteca | Operações/s | Comparado com Matrex |
---|---|---|
Matrex | 3624,36K | |
GráficoMath | 1310,16K | 2,77x mais lento |
Matriz | 372,58K | 9,73x mais lento |
Numexy | 89,72K | 40,40x mais lento |
ExMatriz | 35,76K | 101,35x mais lento |
Transpondo matriz 1000x1000
Biblioteca | Operações/s | Comparado com Matrex |
---|---|---|
Matrex | 428,69 | |
ExMatriz | 9h39 | 45,64× mais lento |
Matriz | 8,54 | 50,17× mais lento |
Numexy | 6,83 | 62,80× mais lento |
Exemplo completo da biblioteca Matrex em funcionamento: regressão linear em dígitos MNIST (caderno Jupyter)
Matrex implementa o protocolo Inspect
e fica bem em seu console:
Pode até desenhar um mapa de calor da sua matriz no console! Aqui está uma animação do treinamento de regressão logística com a biblioteca Matrex e alguns mapas de calor de matriz:
O pacote pode ser instalado adicionando matrex
à sua lista de dependências em mix.exs
:
def deps do
[
{ :matrex , "~> 0.6" }
]
end
Tudo funciona imediatamente, graças à estrutura Accelerate. Se você encontrar um erro de compilação
native/src/matrix_dot.c:5:10: fatal error: 'cblas.h' file not found
em seguida, certifique-se de que as ferramentas de linha de comando do XCode estejam instaladas ( xcode-select --install
). Se o erro ainda não for resolvido, para MacOS Mojave, execute open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg
para restaurar /usr/include e /usr/lib.
No MacOS 10.15 este erro pode ser resolvido com
export CPATH=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/
ou com
C_INCLUDE_PATH=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Accelerate.framework/Frameworks/vecLib.framework/Headers mix compile
Você precisa instalar bibliotecas científicas para que este pacote seja compilado:
> sudo apt-get install build-essential erlang-dev libatlas-base-dev
Definitivamente funcionará no Windows, mas precisamos de um makefile e instruções de instalação. Por favor, contribua.
Com a ajuda da variável de ambiente MATREX_BLAS
você pode escolher com qual biblioteca BLAS vincular. Pode assumir valores blas
(o padrão), atlas
, openblas
ou noblas
.
A última opção significa que você compila o código C sem quaisquer dependências externas, portanto, ele deve funcionar em qualquer lugar com um compilador C:
$ mix clean
$ MATREX_BLAS=noblas mix compile
O comportamento de acesso é parcialmente implementado para Matrex, então você pode fazer:
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 ainda:
iex > m [ 1 .. 2 ]
#Matrex[2×3]
┌ ┐
│ 8.0 1.0 6.0 │
│ 3.0 5.0 7.0 │
└ ┘
Existem também vários atalhos para obter dimensões da matriz:
iex > m [ :rows ]
3
iex > m [ :size ]
{ 3 , 3 }
calculando o valor máximo de toda a matriz:
iex > m [ :max ]
9.0
ou apenas uma de suas linhas:
iex > m [ 2 ] [ :max ]
7.0
calculando o índice baseado em um do elemento máximo para toda a matriz:
iex > m [ :argmax ]
8
e uma linha:
iex > m [ 2 ] [ :argmax ]
3
O módulo Matrex.Operators
redefine os operadores matemáticos Kernel
(+, -, *, / <|>) e define algumas funções convenientes, para que você possa escrever códigos de cálculos de maneira mais natural.
Deve ser usado com muito cuidado. Sugerimos usá-lo apenas dentro de funções específicas e apenas para maior legibilidade, pois o uso de funções do módulo Matrex
, especialmente aquelas que fazem duas ou mais operações em uma chamada, são 2 a 3 vezes mais 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
A mesma função, codificada com chamadas de métodos de módulo (2,5 vezes mais rápida):
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
, portanto, todos os tipos de funções Enum
são aplicáveis:
iex > Enum . member? ( m , 2.0 )
true
iex > Enum . count ( m )
9
iex > Enum . sum ( m )
45
Para funções que existem tanto no Enum
quanto no Matrex
é preferível usar a versão Matrex, porque geralmente é muito, muito mais rápida. Ou seja, para matrizes 1.000 x 1.000 Matrex.sum/1
e Matrex.to_list/1
são 438 e 41 vezes mais rápidas, respectivamente, do que suas contrapartes Enum
.
Você pode salvar/carregar matrizes com formato de arquivo binário nativo (extra rápido) e CSV (lento, especialmente em matrizes grandes).
O formato Matrex CSV é compatível com a saída GNU Octave CSV, portanto você pode usá-lo para trocar dados entre dois 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 │
└ ┘
Valores especiais flutuantes, como :nan
e :inf
funcionam bem dentro de matrizes, podem ser carregados e salvos em arquivos. Mas ao colocá-los no Elixir eles são transferidos para os átomos :nan
, :inf
e :neg_inf
, porque o BEAM não aceita valores especiais como flutuadores 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 )