web-dev-qa-db-fra.com

État de dépréciation de la classe de matrice NumPy

Quel est le statut de la classe matrix dans NumPy?

On me dit toujours que je devrais utiliser plutôt la classe ndarray. Est-il utile/sûr d’utiliser la classe matrix dans le nouveau code que j’écris? Je ne comprends pas pourquoi je devrais plutôt utiliser ndarrays.

42
Andras Deak

tl; dr: la classe numpy.matrix devient obsolète. Certaines bibliothèques de haut niveau dépendent de la classe en tant que dépendance (la plus grande étant scipy.sparse), Ce qui empêche une désapprobation correcte à court terme de la classe, mais les utilisateurs sont vivement encouragés à utiliser le ndarray classe (généralement créée avec la fonction de commodité numpy.array ). Avec l'introduction de l'opérateur @ Pour la multiplication de matrices, de nombreux avantages relatifs des matrices ont été supprimés.

Pourquoi (pas) la classe de matrice?

numpy.matrix Est une sous-classe de numpy.ndarray. Il était initialement conçu pour une utilisation pratique dans les calculs impliquant une algèbre linéaire, mais il existe à la fois des limitations et des différences surprenantes dans leur comportement par rapport aux instances de la classe array plus générale. Exemples de différences fondamentales de comportement:

  • Formes: les tableaux peuvent avoir un nombre arbitraire de dimensions allant de 0 à l'infini (ou 32). Les matrices sont toujours bidimensionnelles. Curieusement, alors qu’une matrice ne peut pas être créée avec plus de dimensions, il est possible d’injecter des dimensions singleton dans une matrice pour obtenir techniquement une matrice multidimensionnelle: np.matrix(np.random.Rand(2,3))[None,...,None].shape == (1,2,3,1) ( pas que cela ait une importance pratique).
  • Indexation: les tableaux d'indexation peuvent vous donner des tableaux de toute taille en fonction de comment vous l'indexez . L'indexation des expressions sur les matrices vous donnera toujours une matrice. Cela signifie que arr[:,0] Et arr[0,:] Pour un tableau 2d vous donne un 1d ndarray, alors que mat[:,0] A la forme (N,1) Et mat[0,:] A la forme (1,M) Dans le cas d'un matrix.
  • Opérations arithmétiques: la principale raison de l’utilisation des matrices à l’époque était que les opérations arithmétiques (en particulier multiplication et puissance) sur les matrices effectuaient des opérations matricielles (multiplication matricielle et puissance matricielle). La même chose pour les tableaux se traduit par une multiplication et une puissance par élément. Par conséquent, mat1 * mat2 Est valide si mat1.shape[1] == mat2.shape[0], Mais arr1 * arr2 Est valide si arr1.shape == arr2.shape (Et bien sûr, le résultat signifie quelque chose de complètement différent). Aussi, étonnamment, mat1 / mat2 Exécute élément par élément division de deux matrices. Ce comportement est probablement hérité de ndarray mais n'a aucun sens pour les matrices, en particulier à la lumière de la signification de *.
  • Attributs spéciaux: les matrices ont quelques attributs pratiques en plus de ce que les tableaux ont: mat.A Et mat.A1 Sont des vues matricielles avec la même valeur que np.array(mat) et np.array(mat).ravel(), respectivement. mat.T Et mat.H Sont les transposés et les conjugués transposés (adjoints) de la matrice; arr.T Est le seul attribut de ce type qui existe pour la classe ndarray. Enfin, mat.I Est la matrice inverse de mat.

C'est assez facile d'écrire du code qui fonctionne soit pour ndarrays, soit pour les matrices. Mais quand il y a une chance que les deux classes doivent interagir en code, les choses commencent à devenir difficiles. En particulier, beaucoup de code pourrait fonctionne naturellement pour les sous-classes de ndarray, mais matrix est une sous-classe mal élevée qui peut facilement casser du code qui tente de compter sur la frappe de canard. Considérez l'exemple suivant en utilisant des tableaux et matrices de forme (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

L'ajout de tranches des deux objets est catastrophiquement différent selon la dimension le long de laquelle nous découpons. L'addition sur les matrices et les tableaux s'effectue élément par élément lorsque les formes sont identiques. Les deux premiers cas ci-dessus sont intuitifs: nous ajoutons deux tableaux (matrices), puis nous ajoutons deux lignes de chacun. Le dernier cas est vraiment surprenant: nous voulions probablement ajouter deux colonnes et nous nous sommes retrouvés avec une matrice. La raison en est bien sûr que arr[:,0] A une forme (3,) Compatible avec la forme (1,3), Mais que mat[:.0] A une forme (3,1). Les deux sont diffusion ensemble pour former (3,3).

Enfin, le principal avantage de la classe de matrice (à savoir la possibilité de formuler de manière concise des expressions de matrice complexes impliquant de nombreux produits de matrice) a été supprimé lorsque l'opérateur @ A été introduit dans python 3.5 , mis en œuvre pour la première fois dans numpy 1.1 . Comparez le calcul d’une forme quadratique 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

En regardant ce qui précède, on comprend clairement pourquoi la classe de matrice était largement préférée pour travailler avec l'algèbre linéaire: l'opérateur infixe * Rendait les expressions beaucoup moins verbeuses et beaucoup plus faciles à lire. Cependant, nous obtenons la même lisibilité avec l'opérateur @ En utilisant modern python et numpy. De plus, notez que le cas de la matrice nous donne une matrice de forme (1,1) qui doit techniquement être un scalaire, ce qui implique également que nous ne pouvons pas multiplier un vecteur de colonne avec ce "scalaire": (v_row * mat * v_row.T) * v_row.T dans l'exemple ci-dessus génère une erreur car les matrices de forme (1,1) et (3,1) Ne peut pas être multiplié dans cet ordre.

Par souci d’exhaustivité, il convient de noter que, bien que l’opérateur matmul corrige le scénario le plus courant dans lequel les ndarrays sont sous-optimaux par rapport aux matrices, il subsiste quelques lacunes dans le traitement élégant de l’algèbre linéaire à l’aide de ndarrays (bien que les préférable de s'en tenir à ce dernier). Un tel exemple est le pouvoir matriciel: mat ** 3 Est le troisième pouvoir matriciel d’une matrice (alors qu’il s’agit du cube élémentaire d’un ndarray). Malheureusement, numpy.linalg.matrix_power Est beaucoup plus détaillé. De plus, la multiplication de matrice sur place ne fonctionne que très bien pour la classe de matrice. En revanche, alors que PEP 465 et la grammaire python autorisent @= Comme affectation augmentée avec matmul, ceci n'est pas implémenté pour ndarrays à partir de numpy 1.15.

Historique de dépréciation

Compte tenu des complications susmentionnées concernant la classe matrix, il existe depuis longtemps des discussions sur son éventuelle dépréciation. L’introduction de l’opérateur d’infix @, Qui était une condition sine qua non de ce processus s’est produite en septembre 2015 . Malheureusement, les avantages de la classe de matrice des premiers jours ont eu pour effet que son utilisation s'est largement répandue. Il y a des bibliothèques qui dépendent de la classe de matrice (l'une des dépendantes les plus importantes est scipy.sparse qui utilise la sémantique numpy.matrix Et renvoie souvent des matrices lors de la densification), donc pleinement les décourager a toujours été problématique.

Déjà dans n sujet de la liste de diffusion de Numpy de 2009 j'ai trouvé des remarques telles que

numpy a été conçu pour des besoins informatiques généraux, pas pour une branche des mathématiques. Les tableaux de données sont très utiles pour beaucoup de choses. En revanche, Matlab, par exemple, a été conçu à l'origine pour être un système facile d'accès au progiciel d'algèbre linéaire. Personnellement, lorsque j’utilisais Matlab, j’ai trouvé cela très gênant - j’écrivais généralement des centaines de lignes de code qui n’avaient rien à voir avec l’algèbre linéaire, pour chaque ligne qui produisait des mathématiques matricielles. Donc, je préfère de loin la manière de Numpy - les lignes de code algèbre linéaire sont plus longues, plus gênantes, mais le reste est bien meilleur.

La classe Matrix est une exception: elle a été écrite pour fournir un moyen naturel d’exprimer l’algèbre linéaire. Cependant, les choses deviennent un peu délicates lorsque vous mélangez des matrices et des tableaux, et même lorsque vous vous en tenez à des matrices, il existe des confusions et des limitations - comment exprimez-vous un vecteur ligne/colonne? qu'est-ce que vous obtenez lorsque vous parcourez une matrice? etc.

Il y a eu beaucoup de discussions sur ces questions, beaucoup de bonnes idées, un peu de consensus sur la façon de l'améliorer, mais personne avec les compétences nécessaires pour le faire n'a la motivation suffisante pour le faire.

Celles-ci reflètent les avantages et les difficultés découlant de la classe matricielle. La suggestion la plus ancienne que j'ai pu trouver concernant la dépréciation est à partir de 2008 , bien que motivée en partie par un comportement non intuitif qui a changé depuis (en particulier, le découpage en tranches et l'itération sur une matrice entraînent des matrices (en lignes) comme attendre probablement). La suggestion montrait qu’il s’agissait d’un sujet très controversé et que les opérateurs infixes pour la multiplication matricielle étaient cruciaux.

La prochaine mention que je pourrais trouver date de 2014 s'est avérée être un fil [très fructueux. La discussion qui suit soulève la question de la gestion des sous-classes numpy en général, dont le thème général est encore très présent . Il y a aussi critique forte :

Ce qui a déclenché cette discussion (sur Github), c'est qu'il n'est pas possible d'écrire du code dactylographié qui fonctionne correctement pour:

  • ndarrays
  • matrices
  • scipy.sparse matrices clairsemées

La sémantique des trois est différente; scipy.sparse se situe quelque part entre les matrices et ndarrays, certaines choses fonctionnant de manière aléatoire comme des matrices et d’autres pas.

Avec un peu d’hyberbole ajouté, on pourrait dire que du point de vue du développeur, np.matrix fait et a déjà fait du mal simplement en existant, en déréglant les règles non énoncées de la sémantique ndarray en Python.

suivie d'une discussion intéressante sur les futurs possibles des matrices. Même sans opérateur @ À ce moment-là, on pense beaucoup à la désapprobation de la classe de matrice et à la manière dont elle pourrait affecter les utilisateurs en aval. Autant que je sache, cette discussion a directement conduit à la création de PEP 465, qui introduit Matmul.

début 2015 :

À mon avis, une version "fixe" de np.matrix ne devrait (1) pas être une sous-classe de np.ndarray et (2) exister dans une bibliothèque tierce non numpy elle-même.

Je ne pense pas qu'il soit vraiment possible de fixer np.matrix dans son état actuel en tant que sous-classe ndarray, mais même une classe à matrice fixe n'appartient pas à numpy, qui a des cycles de publication trop longs et des garanties de compatibilité pour l'expérimentation - sans oublier que la simple existence de la classe de matrice dans numpy induit en erreur les nouveaux utilisateurs.

Une fois que l'opérateur @ Était disponible depuis un certain temps la discussion sur la dépréciation refait surface , relancer le sujet à propos de la relation entre dépréciation de matrice et scipy.sparse.

Finalement, la première action pour déconseiller numpy.matrix A été prise à la fin novembre 2017 . En ce qui concerne les personnes à charge de la classe:

Comment la communauté gérerait-elle les sous-classes de la matrice scipy.sparse? Ceux-ci sont encore d'usage courant.

Ils ne vont nulle part avant un certain temps (jusqu'à ce que les ndarrays clairsemés se matérialisent au moins). Par conséquent, np.matrix doit être déplacé et non supprimé.

( source ) et

bien que je veuille me débarrasser de np.matrix autant que quiconque, le faire de sitôt serait vraiment perturbateur.

  • Il y a des tonnes de petits scripts écrits par des gens qui ne savaient pas mieux; nous voulons qu'ils apprennent à ne pas utiliser np.matrix mais casser tous leurs scripts est une façon pénible de le faire

  • Il y a des projets majeurs comme scikit-learn qui n'ont tout simplement aucune alternative à l'utilisation de np.matrix, à cause de scipy.sparse.

Donc, je pense que la voie à suivre est quelque chose comme:

  • Maintenant ou chaque fois que quelqu'un établit un PR: émettez un PendingDeprecationWarning dans np.matrix .__ init__ (sauf si cela tue les performances de scikit-learn et de vos amis) et placez un gros message d'avertissement en haut de la documentation. L'idée ici est de ne pas casser le code de quiconque, mais de commencer à faire passer le message que nous ne pensons certainement pas que quiconque devrait utiliser cela s'il a une alternative.

  • Après avoir trouvé une alternative à scipy.sparse: augmentez les avertissements, éventuellement jusqu’à FutureWarning afin que les scripts existants ne se cassent pas, mais reçoivent des avertissements bruyants.

  • Finalement, si nous pensons que cela réduira les coûts de maintenance: divisez-le en un sous-paquet

( source ).

Status Quo

À compter de mai 2018 (numpy 1.15, pertinent demande d'extraction et commit ), la classe de classe de documents matriciels contient la note suivante:

Il n'est plus recommandé d'utiliser cette classe, même pour l'algèbre linéaire. Utilisez plutôt des tableaux classiques. La classe peut être supprimée dans le futur.

Et en même temps, un PendingDeprecationWarning a été ajouté à matrix.__new__. Malheureusement, les avertissements de dépréciation sont (presque toujours) désactivés par défaut , donc la plupart des utilisateurs finaux de numpy ne verront pas cet indice puissant.

Enfin, la feuille de route numpy à compter de novembre 2018 mentionne plusieurs sujets connexes comme l'un des " les tâches et fonctionnalités [la communauté numpy] investiront des ressources dans":

Certaines choses à l'intérieur de NumPy ne correspondent pas vraiment à la portée de NumPy.

  • Un système backend pour numpy.fft (pour que par exemple fft-mkl n’ait pas besoin de monkeypatch numpy)
  • Réécrivez les tableaux masqués pour qu'ils ne soient pas une sous-classe ndarray - peut-être dans un projet séparé?
  • MaskedArray en tant que type de tableau de canard, et/ou
  • dtypes qui supportent les valeurs manquantes
  • Rédigez une stratégie sur la manière de traiter le chevauchement entre numpy et scipy pour linalg et fft (et le mettre en œuvre).
  • Deprecate np.matrix

Il est probable que cet état reste aussi longtemps que de grandes bibliothèques/de nombreux utilisateurs (et en particulier scipy.sparse) S'appuient sur la classe matrix. Cependant, il y a discussion en cours pour déplacer scipy.sparse De dépendre de quelque chose d'autre, tel que pydata/sparse . Indépendamment de l'évolution du processus de dépréciation, les utilisateurs doivent utiliser la classe ndarray dans le nouveau code et, si possible, utiliser un code plus ancien. Finalement, la classe de matrice finira probablement dans un paquet séparé pour supprimer certaines des charges causées par son existence dans sa forme actuelle.

57
Andras Deak