Estado de desaprobación de la clase de matriz NumPy

¿Cuál es el estado de la clase de matrix en NumPy?

Me siguen ndarray que debo usar la clase ndarray lugar. ¿Vale la pena / seguro usar la clase de matrix en el nuevo código que escribo? No entiendo por qué debería usar ndarray s en su lugar.

tl; dr: la clase numpy.matrix está en desuso. Hay algunas bibliotecas de alto perfil que dependen de la clase como una dependencia (la más grande es scipy.sparse ) que dificulta la desaprobación adecuada a corto plazo de la clase, pero se recomienda a los usuarios que usen la clase ndarray (generalmente creada usando el numpy.array conveniencia numpy.array ) en su lugar. Con la introducción del operador @ para la multiplicación de matrices, se han eliminado muchas de las ventajas relativas de las matrices.

¿Por qué (no) la clase matriz?

numpy.matrix es una subclase de numpy.ndarray . Originalmente fue diseñado para un uso conveniente en los cálculos que involucran álgebra lineal, pero existen limitaciones y diferencias sorprendentes en la forma en que se comportan en comparación con las instancias de la clase de matriz más general. Ejemplos de diferencias fundamentales en el comportamiento:

  • Formas: las matrices pueden tener un número arbitrario de dimensiones que van desde 0 hasta el infinito (o 32). Las matrices son siempre bidimensionales. Por extraño que parezca, si bien no se puede crear una matriz con más dimensiones, es posible inyectar dimensiones singleton en una matriz para terminar con una matriz multidimensional técnicamente: np.matrix(np.random.rand(2,3))[None,...,None].shape == (1,2,3,1) (no es que esto tenga alguna importancia práctica).
  • Indexación: las matrices de indexación pueden proporcionarle matrices de cualquier tamaño, dependiendo de cómo lo indexe . Las expresiones de indexación en matrices siempre te darán una matriz. Esto significa que tanto arr[:,0] como arr[0,:] para una matriz 2d le dan un ndarray 1d, mientras que mat[:,0] tiene forma (N,1) y mat[0,:] tiene forma (1,M) en caso de una matrix .
  • Operaciones aritméticas: la razón principal para usar matrices en los viejos tiempos era que las operaciones aritméticas (en particular, la multiplicación y la potencia) en las matrices realizan operaciones matriciales (multiplicación de la matriz y potencia de la matriz). Lo mismo para las matrices da como resultado la multiplicación y la potencia elementales. En consecuencia, mat1 * mat2 es válido si mat1.shape[1] == mat2.shape[0] , pero arr1 * arr2 es válido si arr1.shape == arr2.shape (y, por supuesto, el resultado significa algo completamente diferente). Además, sorprendentemente, mat1 / mat2 realiza una división elemental de dos matrices. Este comportamiento probablemente se hereda de ndarray pero no tiene sentido para las matrices, especialmente a la luz del significado de * .
  • Atributos especiales: las matrices tienen algunos atributos útiles además de lo que tienen los arreglos: mat.A y mat.A1 son vistas de arreglo con el mismo valor que np.array(mat) y np.array(mat).ravel() , respectivamente . mat.T y mat.H son la transposición y la transposición conjugada (adjunta) de la matriz; arr.T es el único atributo que existe para la clase ndarray . Finalmente, mat.I es la matriz inversa de mat .

Es bastante fácil escribir código que funcione tanto para ndarrays como para matrices. Pero cuando existe la posibilidad de que las dos clases tengan que interactuar en código, las cosas comienzan a ser difíciles. En particular, una gran cantidad de código podría funcionar naturalmente para las subclases de ndarray , pero la matrix es una subclase de mal comportamiento que puede romper fácilmente el código que trata de basarse en la tipificación de pato. Considere el siguiente ejemplo utilizando matrices y matrices de forma (3,4) :

 import numpy as np shape = (3, 4) arr = np.arange(np.prod(shape)).reshape(shape) # ndarray mat = np.matrix(arr) # same data in a matrix print((arr + mat).shape) # (3, 4), makes sense print((arr[0,:] + mat[0,:]).shape) # (1, 4), makes sense print((arr[:,0] + mat[:,0]).shape) # (3, 3), surprising 

Agregar segmentos de los dos objetos es catastróficamente diferente según la dimensión a lo largo de la cual cortamos. La adición de ambas matrices y matrices ocurre de manera elemental cuando las formas son las mismas. Los primeros dos casos de lo anterior son intuitivos: agregamos dos matrices (matrices), luego agregamos dos filas de cada una. El último caso es realmente sorprendente: probablemente quisimos agregar dos columnas y terminamos con una matriz. Por supuesto, la razón es que arr[:,0] tiene forma (3,) que es compatible con la forma (1,3) , pero mat[:.0] tiene forma (3,1) . Los dos se emiten juntos para dar forma (3,3) .

Finalmente, la mayor ventaja de la clase de matriz (es decir, la posibilidad de formular de manera concisa expresiones de matriz complicadas que involucran una gran cantidad de productos de matriz) se eliminó cuando se introdujo el operador @ matmul en Python 3.5 , implementado por primera vez en el número 1.10 . Compara el cálculo de una forma cuadrática simple:

 v = np.random.rand(3); v_row = np.matrix(v) arr = np.random.rand(3,3); mat = np.matrix(arr) print(v.dot(arr.dot(v))) # pre-matmul style # 0.713447037658556, yours will vary print(v_row * mat * v_row.T) # pre-matmul matrix style # [[0.71344704]] print(v @ arr @ v) # matmul style # 0.713447037658556 

Viendo lo anterior, está claro por qué la clase de matriz fue ampliamente preferida para trabajar con álgebra lineal: el operador infijo * hizo que las expresiones fueran mucho menos detalladas y mucho más fáciles de leer. Sin embargo, obtenemos la misma legibilidad con el operador @ utilizando python y numpy modernos. Además, tenga en cuenta que el caso de matriz nos da una matriz de forma (1,1) que técnicamente debería ser un escalar. Esto también implica que no podemos multiplicar un vector de columna con este “escalar”: (v_row * mat * v_row.T) * v_row.T en el ejemplo anterior genera un error porque las matrices con forma (1,1) y (3,1) no se puede multiplicar en este orden.

Por razones de integridad, se debe tener en cuenta que si bien el operador de matmul resuelve el escenario más común en el que los ndarrays son subóptimos en comparación con las matrices, todavía hay algunas deficiencias en el manejo del álgebra lineal con elegancia utilizando ndarrays (aunque la gente todavía tiende a creer que, en general, preferible atenerse a este último). Un ejemplo de ello es la potencia de matriz: mat ** 3 es la tercera potencia de matriz adecuada de una matriz (mientras que es el cubo elementwise de un ndarray). Desafortunadamente, numpy.linalg.matrix_power es bastante más detallado. Además, la multiplicación de matrices in situ solo funciona bien para la clase de matrices. En contraste, si bien PEP 465 y la gramática de python permiten @= como una asignación aumentada con matmul, esto no se implementa para ndarrays a partir del número 1.15.

Historia de la deprecacion

Teniendo en cuenta las complicaciones anteriores relacionadas con la clase de matrix , se han mantenido discusiones recurrentes sobre su posible desaprobación durante mucho tiempo. La introducción del operador @ infix, que era un gran requisito previo para este proceso, tuvo lugar en septiembre de 2015 . Desafortunadamente, las ventajas de la clase de matriz en días anteriores significaron que su uso se extendió ampliamente. Hay bibliotecas que dependen de la clase de matriz (una de las dependientes más importantes es scipy.sparse que utiliza la semántica numpy.matrix y, a menudo, devuelve matrices al densificar), por lo que su eliminación total siempre ha sido problemática.

Ya en un hilo de la lista de correo numpy de 2009 encontré comentarios como

numpy fue diseñado para necesidades computacionales de propósito general, no para una twig de matemáticas. Los nd-arrays son muy útiles para muchas cosas. En contraste, Matlab, por ejemplo, fue diseñado originalmente para ser un frente fácil para el paquete de álgebra lineal. Personalmente, cuando utilicé Matlab, lo encontré muy incómodo: por lo general escribía cientos de líneas de código que no tenían nada que ver con el álgebra lineal, por cada pocas líneas que realmente hicieran matrices matriciales. Así que prefiero la manera de numpy: las líneas de código de álgebra lineal son más complicadas, pero el rest es mucho mejor.

La clase Matrix es la excepción a esto: se escribió para proporcionar una forma natural de express el álgebra lineal. Sin embargo, las cosas se complican un poco cuando se mezclan matrices y matrices, e incluso cuando se adhieren a las matrices hay confusiones y limitaciones. ¿Cómo se expresa una fila frente a un vector de columna? ¿Qué obtienes cuando recorres una matriz? etc.

Ha habido mucha discusión sobre estos temas, muchas buenas ideas, un poco de consenso sobre cómo mejorarlo, pero nadie con la habilidad para hacerlo tiene la motivación suficiente para hacerlo.

Estos reflejan los beneficios y dificultades que surgen de la clase matriz. La primera sugerencia de desaprobación que pude encontrar es de 2008 , aunque está motivada en parte por un comportamiento poco intuitivo que ha cambiado desde entonces (en particular, cortar e iterar sobre una matriz resultará en matrices (filas) como es de esperar). La sugerencia mostró que este es un tema altamente controvertido y que los operadores de infijo para la multiplicación de matrices son cruciales.

La siguiente mención que pude encontrar es de 2014, que resultó ser un hilo muy fructífero. La discusión subsiguiente plantea la cuestión del manejo de subclases numpy en general, cuyo tema general todavía está muy sobre la mesa . También hay fuertes críticas :

Lo que provocó esta discusión (en Github) es que no es posible escribir código de tipo pato que funcione correctamente para:

  • ndarrays
  • matrices
  • scipy.sparse matrices dispersas

La semántica de los tres es diferente; scipy.sparse está en algún lugar entre matrices y ndarrays con algunas cosas que funcionan aleatoriamente como matrices y otras que no.

Si se agrega algo de inteligencia, se podría decir que, desde el punto de vista del desarrollador, np.matrix está haciendo y ya ha hecho el mal por el hecho de desordenar las reglas no declaradas de la semántica ndarray en Python.

seguido de una gran cantidad de discusiones valiosas sobre los posibles futuros de las matrices. Incluso con el operador no @ en el momento, se piensa mucho en la desaprobación de la clase de matriz y en cómo podría afectar a los usuarios en sentido descendente. Por lo que puedo decir, esta discusión ha conducido directamente a la creación de PEP 465 introduciendo matmul.

A principios de 2015 :

En mi opinión, una versión “fija” de np.matrix no debería (1) ser una subclase np.ndarray y (2) existir en una biblioteca de terceros, no en sí misma.

No creo que sea realmente factible corregir np.matrix en su estado actual como una subclase ndarray, pero incluso una clase de matriz fija no pertenece realmente al número, que tiene ciclos de liberación demasiado largos y garantías de compatibilidad para la experimentación. Por no mencionar que la mera existencia de la clase de matriz en números hace que los nuevos usuarios se extravíen.

Una vez que el operador @ estuvo disponible durante un tiempo, volvió a surgir la discusión sobre la depreciación , volviendo a plantear el tema sobre la relación entre la depreciación de la matriz y scipy.sparse .

Finalmente, la primera acción para desaprobar numpy.matrix se tomó a fines de noviembre de 2017 . Respecto a los dependientes de la clase:

¿Cómo manejaría la comunidad las subclases de matriz scipy.sparse? Estos son todavía de uso común.

No irán a ningún lado durante bastante tiempo (hasta que las ndarrays dispersas se materialicen al menos). Por lo tanto, np.matrix necesita ser movido, no eliminado.

( fuente ) y

Aunque quiero deshacerme de np.matrix tanto como cualquiera, hacerlo en cualquier momento pronto sería realmente perjudicial.

  • Hay toneladas de pequeños guiones escritos por personas que no sabían mejor; queremos que aprendan a no usar np.matrix pero romper todos sus scripts es una forma dolorosa de hacerlo

  • Hay proyectos importantes como scikit-learn que simplemente no tienen alternativa al uso de np.matrix, debido a scipy.sparse.

Así que creo que el camino a seguir es algo como:

  • Ahora o cada vez que alguien reúna un PR: emita un PendingDeprecationWarning en np.matrix .__ init__ (a menos que elimine el rendimiento de scikit-learn y amigos), y coloque un gran cuadro de advertencia en la parte superior de los documentos. La idea aquí es no romper realmente el código de nadie, pero comenzar a transmitir el mensaje de que definitivamente no creemos que nadie debería usar esto si tienen alguna alternativa.

  • Después hay una alternativa a scipy.sparse: boost las advertencias, posiblemente hasta FutureWarning para que los scripts existentes no se rompan pero sí reciban advertencias ruidosas

  • Eventualmente, si creemos que reducirá los costos de mantenimiento: divídalo en un subpaquete

( fuente ).

Status quo

A partir de mayo de 2018 (número 1.15, solicitud de extracción relevante y confirmación ), la cadena de documentación de la clase de matriz contiene la siguiente nota:

Ya no se recomienda usar esta clase, incluso para el álgebra lineal. En su lugar, utilice matrices regulares. La clase puede ser eliminada en el futuro.

Y, al mismo tiempo, se ha agregado un PendingDeprecationWarning a la matrix.__new__ . Desafortunadamente, las advertencias de desaprobación están (casi siempre) silenciadas de manera predeterminada , por lo que la mayoría de los usuarios finales de numpy no verán este fuerte indicio.

Finalmente, la hoja de ruta numpy a partir de noviembre de 2018 menciona múltiples temas relacionados como una de las ” tareas y características [la comunidad numpy] en la que se invertirán recursos “:

Algunas cosas dentro de NumPy en realidad no coinciden con el scope de NumPy.

  • Un sistema backend para numpy.fft (de modo que, por ejemplo, fft-mkl no necesita monkeypatch numpy)
  • Reescriba los arreglos enmascarados para que no sean una subclase de ndarray, ¿quizás en un proyecto separado?
  • MaskedArray como un tipo duck-array, y / o
  • Dtypes que soportan valores perdidos
  • Escriba una estrategia sobre cómo lidiar con la superposición entre números y scipy para linalg y fft (e implementarlo).
  • Depredar np.matrix

Es probable que este estado se mantenga mientras las bibliotecas más grandes / muchos usuarios (y en particular scipy.sparse ) confíen en la clase de matriz. Sin embargo, hay una discusión en curso para mover scipy.sparse para depender de otra cosa, como pydata/sparse . Independientemente de los desarrollos del proceso de desaprobación, los usuarios deben usar la clase ndarray en un nuevo código y, de ser posible, portar un código más antiguo. Eventualmente, la clase de matriz probablemente terminará en un paquete separado para eliminar algunas de las cargas causadas por su existencia en su forma actual.