Manejo de matrices en clojure con core.matrix

Hace unas semanas escribí una introducción sobre Machine Learning usando numl, una librería para C#. Al empezar a profundizar en el tema, he visto que hay bastantes algoritmos de aprendizaje automático que se basan en el uso de matrices, y aunque hay lenguajes muy apropiados para eso, como matlab u octave (con el que ando también entretenido), no he podido resistirme a ver cómo se podría trabajar con matrices en clojure, el lenguaje que se ha convertido en mi lenguaje por defecto para jugar y experimentar.

OJO: si no te interesan clojure, ni las matrices, ni el álgebra lineal, éste es un momento excelente para que dejes de leer el post y te dediques a cosas más entretenidas.

clojure.core.matrix

La librería de referencia en clojure para trabajar con matrices es core.matrix. Pese a su nombre, no está incluida «de serie» en clojure, sino que se instala como cualquier otra dependencia.

core.matrix expone un API (que veremos dentro de poco) para trabajar con matrices y vectores. Aunque matemáticamente podemos incluir los vectores como un caso particular de matrices, core.matrix separa ambos tipos porque a nivel de programación hay escenarios en los que resulta más cómodo tratar los vectores de manera especial. En cualquier caso, en este post nos centraremos en el manejo de matrices.

Una característica interesante de core.matrix es que lo que realmente expone es un API «de alto nivel» que puede trabajar con distintas implementaciones de matrices.

Por defecto incluye algunas implementaciones básicas, como la basada en persistent-vectors de clojure, pero se pueden emplear otras implementaciones como vectorz-clj, basada en vectorz, una de las implementaciones más rápidas de matrices sobre la JVM, o clatrix, una librería con parte nativa que ofrece un rendimiento aún mayor para matrices grandes.

A lo largo de este post usaremos la implementación de vectorz-clj, por aquello de ahorrarnos tener una dependencia nativa y de paso evitar ciertos bugs en la implementación por defecto de core.matrix.

Cómo usar core.matrix

Lo primero que necesitamos es incluir los paquetes adecuados, en este caso el paquete principal de core.matrix, y el de la implementación de vectorz-clj. Podemos añadirlos a nuestro project.clj:

(defproject my-project
  ...
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [net.mikera/core.matrix "0.34.0"]
                 [net.mikera/vectorz-clj "0.29.0"]])

A partir de aquí los referenciamos y podemos empezar a jugar:

(ns my-namespace
  (:require [clojure.core.matrix :as m]))

Existe la opción de referenciar también el espacio de nombres clojure.core.matrix.operators, que redefine los operadores aritméticos típicos (+, -, *, /, etc.), pero personalmente prefiero hacer explícito que estoy usando operaciones operaciones sobre matrices.

A continuación, establecemos la implementación concreta de matrices que queremos usar y ya estamos listos para empezar a trastear:

(m/set-current-implementation :vectorz)

Podemos crear matrices a partir de vectores anidados, o con algunas funciones predefinidas:

;; Una matriz arbitraria
(def M (m/matrix [[1 2 3]
                [4 5 6]
                [7 8 9]]))

;; Matriz identidad 4x4
(def I4 (m/identity 4)) 

;; Matriz de 3x2 con 0 en todas las filas y columnas
(def Z3x2 (m/zero-matrix 3 x)) 

Podemos obtener y establecer los valores de una matriz (que por defecto es inmutable, así que al establecer devuelve una copia de la matriz original):

(m/get M 1 2) 
;; => 6

(m/set M 0 0 21) 
;; => [[21 2 3]
;; =>  [ 4 5 6]
;; =>  [ 7 8 9]]

A partir de una matriz, podemos obtener un parte:

;; Nos quedamos a partir de la fila 0 con 1 fila 
;; y a partir de la columna 1 con 2 columnas
(m/submatrix M 0 1 1 2) 
;; => [[2 3]]

;; Nos quedamos con las filas de la 0 y 2, y con todas las columnas
(m/select M [0 2] :all)
;; => [[1 2 3]
;; =>  [7 8 9]]

Por supuesto, podemos operar con matrices, tanto entre ellas como con escalares usando m/add, m/sub, m/mul (multiplicación entre matriz y escalar o multiplicaciones de matrices elemento a elemento), m/mmul (multiplicación típica de matrices), m/div, etc.

Cuando operamos con matrices, sus dimensiones tienen que ser compatibles. Si operamos con una matriz y un escalar, se realiza la operación entre el escalar y cada elemento de la matriz:

(m/add (m/matrix [[1 2] 
                [1 2]]) 
      (m/matrix [[5 5] 
                [7 7]]))
;; => operación con matrices:
;; => [[6 7]
;; =>  [8 9]]

(m/add (m/matrix [[1 2] 
                [1 2]]) 
       3)
;; => operación con matriz y escalar
;; => [[4 5]
;; =>  [4 5]]

Además de combinar matrices y escalares, podemos combinar matrices cuyas dimensiones no son compatibles a priori, pero que pueden convertirse en compatibles según las reglas de expansión (broadcasting) de core.matrix. Así, si sumamos una matriz de 3×3 con un vector fila 1×3, core.matrix sumará el vector a cada una de las filas de la matriz original.

Junto a estas operaciones aritméticas básicas, tenemos operaciones específicas sobre matrices, como m/transpose para obtener la matriz transpuesta, m/det para obtener el determinante, o m/inverse para obtener la inversa de una matriz.

Al ser clojure un lenguaje funcional, no podían faltar los equivalentes a map, reduce y compañía, que nos permiten aplicar estas operaciones a todos los elementos de una matriz como si de una secuencia se tratasen. En geneneral, estas funciones se denominan igual que sus equivalentes para secuencias, pero con el prefijo e (de element wise). Por ejemplo, tenemos e/map o e/reduce.

El API de core.matrix tiene un estilo muy funcional, haciendo un uso intensivo de matrices inmutables, pero a veces puede ser necesario utilizar estructuras de datos mutables por motivos de rendimiento. Si la implementación subyacente lo permite (y vectorz-clj lo hace), se puede emplear el equivalente mutable de (casi) todas las funciones que hemos visto ahora ahora. Estas versiones mutables tiene el sufijo ! y modifican la matriz que reciben como parámetro:

;; En su versión inmutable, set devuelve una copia de la matriz original
(m/set M 0 0 21)
;; La versión mutable modifica la matriz original
(m/set! M 0 0 21)

Resumen

Esta mini introducción de core.matrix nos permite hacernos una idea del tipo de operaciones que podemos hacer con esta librería. Si estás acostumbrado (como casi todos nosotros) a desarrollar las típicas aplicaciones de línea de negocio, todo esto de las matrices te parecerá una tontería inútil, pero hay bastantes campos en los que tienen su aplicación, y contar con una librería que nos ayude a implementar algoritmos basados en matrices resulta muy importante cuando empiezas a tocarlos.

2 comentarios en “Manejo de matrices en clojure con core.matrix

  1. O está más inactiva, o tiene peor SEO, pero no la encontré cuando estuve buscando.

    Tiene pinta de incluir más cosas que core.matrix. Le echaré un vistazo porque la parte de visualización me interesa.

    Gracias!

Comentarios cerrados.