Julia: Prendre au sérieux les transpositions matricielles

Créé le 10 mars 2017  ·  141Commentaires  ·  Source: JuliaLang/julia

Actuellement, transpose est récursif. Ceci est assez peu intuitif et conduit à ce malheur:

julia> A = [randstring(3) for i=1:3, j=1:4]
3×4 Array{String,2}:
 "J00"  "oaT"  "JGS"  "Gjs"
 "Ad9"  "vkM"  "QAF"  "UBF"
 "RSa"  "znD"  "WxF"  "0kV"

julia> A.'
ERROR: MethodError: no method matching transpose(::String)
Closest candidates are:
  transpose(::BitArray{2}) at linalg/bitarray.jl:265
  transpose(::Number) at number.jl:100
  transpose(::RowVector{T,CV} where CV<:(ConjArray{T,1,V} where V<:(AbstractArray{T,1} where T) where T) where T) at linalg/rowvector.jl:80
  ...
Stacktrace:
 [1] transpose_f!(::Base.#transpose, ::Array{String,2}, ::Array{String,2}) at ./linalg/transpose.jl:54
 [2] transpose(::Array{String,2}) at ./linalg/transpose.jl:121

Depuis un certain temps, nous disons aux gens de faire permutedims(A, (2,1)) place. Mais je pense que nous savons tous, au fond, que c'est terrible. Comment on est venu ici? Eh bien, il est assez bien entendu que l'on veut que le ctranspose ou "adjoint" d'une matrice de matrices soit récursif. Un exemple motivant est que vous pouvez représenter des nombres complexes en utilisant des matrices 2x2 , auquel cas le "conjugué" de chaque "élément" (en fait une matrice), est son adjoint en tant que matrice - en d'autres termes, si ctranspose est récursif, alors tout travaux. Ceci n'est qu'un exemple mais cela généralise.

Le raisonnement semble avoir été le suivant:

  1. ctranspose doit être récursif
  2. ctranspose == conj ∘ transpose == conj ∘ transpose
  3. transpose devrait donc aussi être récursif

Je pense qu'il y a quelques problèmes ici:

  • Il n'y a aucune raison pour laquelle ctranspose == conj ∘ transpose == conj ∘ transpose doit tenir, bien que le nom rend cela presque inévitable.
  • Le comportement de conj fonctionnant par élément sur les tableaux est une sorte de séquelle malheureuse de Matlab et n'est pas vraiment une opération mathématiquement justifiable, tout comme exp opération par élément n'est pas vraiment mathématique et ce que expm serait une meilleure définition.
  • Il prend en fait le conjugué (ie adjoint) de chaque élément qui implique que ctranspose devrait être récursif; en l'absence de conjugaison, il n'y a pas de bonne raison pour que la transposition soit récursive.

En conséquence, je proposerais les changements suivants pour remédier à la situation:

  1. Renommez ctranspose (aka ' ) en adjoint - c'est vraiment ce que fait cette opération, et cela nous libère de l'implication qu'elle doit être équivalente à conj ∘ transpose .
  2. Déprécier vectorisé conj(A) sur les tableaux au profit de conj.(A) .
  3. Ajoutez un argument de mot-clé recur::Bool=true à adjoint (née ctranspose ) indiquant s'il doit s'appeler récursivement. Par défaut, c'est le cas.
  4. Ajoutez un argument de mot-clé recur::Bool=false à transpose indiquant s'il doit s'appeler récursivement. Par défaut, ce n'est pas le cas.

Au minimum, cela nous permettrait d'écrire ce qui suit:

julia> A.'
4×3 Array{String,2}:
 "J00"  "Ad9"  "RSa"
 "oaT"  "vkM"  "znD"
 "JGS"  "QAF"  "WxF"
 "Gjs"  "UBF"  "0kV"

Que nous puissions ou non réduire cela à A' dépend de ce que nous voulons faire avec conj et adjoint de non-nombres (ou plus spécifiquement, non-réels, non -valeurs complexes).

[Ce numéro est le deuxième d'une série de ω₁ parties.]

breaking decision linear algebra

Commentaire le plus utile

(OT: J'ai déjà hâte de "Prendre les 7 tenseurs au sérieux", le prochain opus de la mini-série à succès en 6 parties ...)

Tous les 141 commentaires

Le successeur logique d'un numéro précédent ... 👍

Le comportement de conj fonctionnant par élément sur les tableaux est une sorte de séquelle malheureuse de Matlab et n'est pas vraiment une opération mathématiquement justifiable

Ce n'est pas du tout vrai, et pas du tout analogue à exp . Les espaces vectoriels complexes et leurs conjugaisons sont un concept mathématique parfaitement bien établi. Voir aussi https://github.com/JuliaLang/julia/pull/19996#issuecomment -272312876

Les espaces vectoriels complexes et leur conjugaison sont un concept mathématique parfaitement bien établi.

Sauf erreur de ma part, l'opération de conjugaison mathématique correcte dans ce contexte est ctranspose plutôt que conj (ce qui était précisément mon point):

julia> v = rand(3) + rand(3)*im
3-element Array{Complex{Float64},1}:
 0.0647959+0.289528im
  0.420534+0.338313im
  0.690841+0.150667im

julia> v'v
0.879291582684847 + 0.0im

julia> conj(v)*v
ERROR: DimensionMismatch("Cannot multiply two vectors")
Stacktrace:
 [1] *(::Array{Complex{Float64},1}, ::Array{Complex{Float64},1}) at ./linalg/rowvector.jl:180

Le problème avec l'utilisation d'un mot-clé pour recur est que, la dernière fois que j'ai vérifié, il y avait une grosse pénalité de performance pour l'utilisation de mots-clés, ce qui est un problème si vous finissez par appeler ces fonctions de manière récursive avec un cas de base qui se termine sur scalaires.

Nous devons de toute façon résoudre le problème de performance des mots clés dans la version 1.0.

@StefanKarpinski , vous vous trompez. Vous pouvez avoir une conjugaison complexe dans un espace vectoriel sans avoir de joint - les adjoints sont un concept qui nécessite un espace de Hilbert, etc., pas seulement un espace vectoriel complexifié.

De plus, même lorsque vous avez un espace de Hilbert, le conjugué complexe est distinct de l'adjoint. par exemple, le conjugué d'un vecteur colonne complexe dans ℂⁿ est un autre vecteur complexe, mais l'adjoint est un opérateur linéaire (un "vecteur ligne").

(La conjugaison complexe n'implique pas que vous pouvez multiplier conj(v)*v !)

La dépréciation vectorisée de conj est indépendante du reste de la proposition. Pouvez-vous fournir quelques références pour la définition de la conjugaison complexe dans un espace vectoriel?

https://en.wikipedia.org/wiki/Complexification#Complex_conjugation

(Ce traitement est plutôt formel; mais si vous recherchez sur Google "matrice conjuguée complexe" ou "vecteur conjugué complexe", vous trouverez des millions d'usages.)

Si le mappage d'un vecteur complexe au vecteur conjugué (ce que fait conj maintenant) est une opération importante distincte de son mappage sur un covecteur conjugué (ce que fait ' ), alors nous pouvons certainement garder conj pour exprimer cette opération sur des vecteurs (et des matrices, et des tableaux supérieurs, je suppose). Cela fournit une distinction entre conj et adjoint puisqu'ils seraient d'accord sur les scalaires mais se comporteraient différemment sur les tableaux.

Dans ce cas, est-ce que conj(A) appeler conj sur chaque élément ou appeler adjoint ? La représentation des nombres complexes sous forme d'exemple de matrices 2x2 suggérerait que conj(A) devrait en fait appeler adjoint sur chaque élément, plutôt que d'appeler conj . Cela ferait adjoint , conj et conj. toutes les opérations différentes:

  1. adjoint : swap i et j index et mapper récursivement adjoint sur les éléments.
  2. conj : map adjoint sur les éléments.
  3. conj. : map conj sur les éléments.

conj(A) devrait appeler conj sur chaque élément. Si vous représentez des nombres complexes par des matrices 2x2, vous avez un espace vectoriel complexifié différent.

Par exemple, une utilisation courante de la conjugaison de vecteurs est dans l' analyse des valeurs propres de matrices réelles : les valeurs propres et les vecteurs propres viennent en paires complexes-conjugués. Maintenant, supposons que vous ayez une matrice de blocs représentée par un tableau 2d A de matrices 2x2 réelles, agissant sur des vecteurs bloqués v représentés par des tableaux 1d de vecteurs à 2 composants. Pour toute valeur propre λ de A avec un vecteur propre v , nous attendons une deuxième valeur propre conj(λ) avec le vecteur propre conj(v) . Cela ne fonctionnera pas si conj appelle adjoint récursivement.

(Notez que l'adjoint, même pour une matrice, peut être différent d'un conjugué-transposé, car l' adjoint d'un opérateur linéaire défini de la manière la plus générale dépend aussi du choix du produit interne. Il existe de nombreuses applications réelles où certaines type de produit interne pondéré est approprié, auquel cas la manière appropriée de prendre l'adjoint d'une matrice change! Mais je suis d'accord que, étant donné un Matrix , nous devrions prendre l'adjoint correspondant au produit interne standard donné par dot(::Vector,::Vector) . Cependant, il est tout à fait possible que certains types AbstractMatrix (ou d'autres types d'opérateurs linéaires) veuillent remplacer adjoint pour faire quelque chose de différent.)

De même, il est également difficile de définir un adjoint(A) algébriquement raisonnable pour, par exemple, des tableaux 3D, car nous n'avons pas défini les tableaux 3D comme des opérateurs linéaires (par exemple, il n'y a pas d'opération array3d * array2d intégrée) . Peut-être que adjoint ne devrait être défini par défaut que pour les tableaux scalaires, 1d et 2d?

Mise à jour: Oh, bien: nous ne définissons pas non plus ctranspose pour les tableaux 3D. Continuer.

(OT: J'ai déjà hâte de "Prendre les 7 tenseurs au sérieux", le prochain opus de la mini-série à succès en 6 parties ...)

Si vous représentez des nombres complexes par des matrices 2x2, vous avez un espace vectoriel complexifié différent.

Je ne suis pas cela - la représentation matricielle 2x2 des nombres complexes devrait se comporter exactement comme si des scalaires complexes étaient des éléments. Je penserais, par exemple, que si nous définissons

m(z::Complex) = [z.re -z.im; z.im z.re]

et nous avons un vecteur complexe arbitraire v alors nous voudrions que cette identité tienne:

conj(m.(v)) == m.(conj(v))

Je voudrais préciser l'exemple et faire une comparaison avec ' qui est censé déjà faire la navette avec m mais je ne peux pas à cause de https://github.com/JuliaLang/julia/ issues / 20979 , qui rompt accidentellement cette commutation. Une fois ce bug corrigé, m.(v)' == m.(v') tiendra, mais si je vous comprends bien, @stevengj , alors conj(m.(v)) == m.(conj(v)) ne devrait pas?

conj(m(z)) devrait == m(z) .

Votre m(z) est un isomorphisme de nombres complexes sous addition et multiplication, mais ce n'est pas le même objet à d'autres égards pour l'algèbre linéaire, et nous ne devrions pas prétendre que c'est pour conj . Par exemple, si z est un scalaire complexe, alors eigvals(z) == [z] , mais eigvals(m(z)) == [z, conj(z)] . Lorsque l'astuce m(z) est étendue à des matrices complexes, ce doublement du spectre a des conséquences délicates par exemple pour les méthodes itératives (voir par exemple cet article ).

Une autre façon de le dire est que z est déjà un espace vectoriel complexe (un espace vectoriel sur les nombres complexes), mais la matrice réelle 2x2 m(z) est un espace vectoriel sur les nombres réels (vous peut multiplier m(z) par un nombre réel)… si vous "complexifiez" m(z) en le multipliant par un nombre complexe, par exemple m(z) * (2+3im) (et non m(z) * m(2+3im) ) alors vous obtenir un espace vectoriel complexe différent qui n'est plus isomorphe à l'espace vectoriel complexe d'origine z .

Cette proposition semble apporter une réelle amélioration: +1: je pense au côté mathématique:

  • si je comprends bien, la conjugaison est une action sur l'anneau (lire: nombres) qui fournit les coefficients pour notre espace vectoriel / vecteurs. Cette action induit une autre action sur l'espace vectoriel (et sur ses mappages, lire: matrices), également appelée conjugaison, par la linéarité de la construction de l'espace vectoriel sur l'anneau. Par conséquent, la conjugaison fonctionne nécessairement par application élémentaire aux coefficients. Pour Julia, cela signifie qu'au fond pour un vecteur v , conj(v) = conj.(v) , et c'est un choix de conception si conj a également des méthodes pour les tableaux ou uniquement pour les scalaires, les deux semble ok? (Le point de Steven à propos de l'exemple des nombres complexes comme matrices 2x2 est qu'il s'agit d'un espace vectoriel dont les coefficients sont formellement / réellement réels, donc la conjugaison n'a aucun effet ici.)
  • adjoint a une signification algébrique fondamentale dans de nombreux domaines importants impliquant intimement l'algèbre linéaire (voir 1 et 2 et 3 , tous avec de grandes applications en physique aussi). Si adjoint est ajouté, cela devrait autoriser les arguments de mot-clé pour l' action considérée - dans une analyse d'espaces vectoriels / fonctionnelle, cette action est généralement induite par le produit interne, par conséquent l'argument de mot-clé peut être la forme à la place. Tout ce qui est conçu pour les transpositions de Julia devrait éviter d'entrer en conflit avec ces applications, donc je pense que adjoint est un peu trop chargé?

@felixrehren , je ne pense pas que vous utiliseriez un argument de mot-clé pour spécifier le produit interne qui induit l'adjoint. Je pense que vous utiliseriez simplement un type différent à la place, comme si vous vouliez changer la signification de dot pour un vecteur.

Ma préférence serait un peu plus simple:

  • Gardez conj tel quel (élément par élément).
  • Faire correspondre a' à adjoint(a) , et le rendre récursif toujours (et donc échouer pour les tableaux de chaînes, etc. où adjoint n'est pas défini pour les éléments).
  • Faites correspondre a.' à transpose(a) , et ne le rendez jamais récursif (juste un permutedims ), et donc ignorez le type des éléments.

Je ne vois vraiment pas de cas d'utilisation pour un adjoint non récursif. En guise d'étirement, je peux en quelque sorte imaginer des cas d'utilisation pour un transpose récursif, mais dans toute application où vous devez changer a.' en transpose(a, recur=true) il semble tout aussi simple appeler une autre fonction transposerecursive(a) . Nous pourrions définir transposerecursive dans Base, mais je pense que le besoin de cela sera si rare que nous devrions attendre de voir si cela se produit réellement.

Personnellement, je pense que garder cela simple sera le plus simple pour les utilisateurs (et pour la mise en œuvre), et reste tout à fait défendable sur le front de l'algèbre linéaire.

Jusqu'à présent (la plupart) de nos routines d'algèbre linéaire sont assez fortement enracinées dans les structures de tableau standard et le produit interne standard. Dans chaque slot de votre matrice ou vecteur, vous mettez un élément de votre "champ". Je dirai que pour les éléments de champs , nous nous soucions de + , * , etc, et conj , mais pas transpose . Si votre champ était une représentation complexe spéciale qui ressemble un peu à une matrice réelle 2x2 mais change sous conj , alors c'est OK - c'est une propriété de conj . Si c'est juste un réel 2x2 AbstractMatrix qui ne change pas sous conj , alors on peut soutenir que de tels éléments d'une matrice ne devraient pas changer sous l'adjoint ( @stevengj l'a dit mieux que je ne peux le décrire - il peut y avoir un isomorphisme des nombres complexes mais cela ne veut pas dire qu'il se comporte de la même manière de toutes les manières).

Quoi qu'il en soit, l'exemple complexe 2x2 me semble un peu un hareng rouge. La vraie cause du comportement de la matrice récursive était un raccourci pour faire de l'algèbre linéaire sur des matrices de blocs. Pourquoi ne pas traiter ce cas particulier avec soin et simplifier le système sous-jacent?

Donc, ma suggestion "simplifiée" serait:

  • Gardez conj tel quel (ou créez-en une vue pour AbstractArray s)
  • Faire de a' une vue non récursive telle que (a')[i,j] == conj(a[j,i])
  • Faire de a.' une vue non récursive telle que (a.')[i,j] == a[j,i]
  • Introduisez un type BlockArray pour gérer les blocs-matrices et ainsi de suite (dans Base , ou éventuellement dans un package bien pris en charge). Sans doute, ce serait beaucoup plus puissant et flexible qu'un Matrix{Matrix} à cette fin, mais tout aussi efficace.

Je pense que ces règles seraient assez simples pour que les utilisateurs les adoptent et les exploitent.

PS - @StefanKarpinski Pour des raisons pratiques, un argument mot-clé booléen pour la récursivité ne fonctionnera pas avec les vues pour la transposition. Le type de vue peut dépendre de la valeur booléenne.

Aussi, j'ai mentionné ailleurs, mais je l'ajouterai ici pour être complet: les vues de transposition récursives ont la propriété ennuyeuse que le type d'élément puisse changer par rapport au tableau qu'il encapsule. Par exemple, transpose(Vector{Vector}) -> RowVector{RowVector} . Je n'ai pas pensé à un moyen d'obtenir ce type d'élément pour RowVector sans pénalité d'exécution ou en invoquant l'inférence pour calculer le type de sortie. Je suppose que le comportement actuel (invoquer l'inférence) n'est pas souhaitable du point de vue du langage.

NB: rien n'empêche non plus les utilisateurs de définir conj pour renvoyer un type différent - donc la vue ConjArray souffre également de ce problème, que la transposition soit récursive ou non.

@stevengj - vous faites remarquer que les matrices 2x2 "complexes" étant un espace vectoriel formellement réel plutôt qu'un espace vectoriel complexe a du sens pour moi, mais alors ce point me remet en question la motivation originale de l'adjoint récursif, ce qui me conduit à me demander si La proposition de @andyferris ne serait pas meilleure (transposition et adjoint non récursifs). Je suppose que le fait que l'exemple 2x2 complexe et la représentation de matrice de blocs "veulent" que l'adjoint soit récursif est suggestif, mais étant donné vos commentaires sur ce premier exemple, je dois me demander s'il n'y a pas d'autres cas où l'adjoint non récursif est plus correct / pratique.

Si l'adjoint n'est pas récursif, ce n'est pas un adjoint. C'est juste faux.

Pouvez-vous donner un peu plus de justification pour cela lorsque vous avez un moment?

L'adjoint d'un vecteur doit être un opérateur linéaire le mappant à un scalaire. Autrement dit, a'*a doit être un scalaire si a est un vecteur. Et cela induit un adjoint correspondant sur les matrices, puisque la propriété de définition est a'*A*a == (A'*a)'*a .

Si a est un vecteur de vecteurs, cela implique que a' == adjoint(a) doit être récursif.

OK, je pense que je suis ça.

Nous avons également des produits internes récursifs:

julia> norm([[3,4]])
5.0

julia> dot([[3,4]], [[3,4]])
25

Clairement, le "adjoint" ou le "dual" ou quoi que ce soit devrait être récursif de la même manière.

Je pense que la question centrale est donc de savoir si nous avons besoin de a' * b == dot(a,b) pour tous les vecteurs a , b ?

L'alternative est de dire que ' ne retourne pas nécessairement l'adjoint - c'est juste une opération de tableau qui transpose les éléments et les passe par conj . C'est juste l'adjoint pour les éléments réels ou complexes.

Il y a une et une seule raison pour laquelle nous avons un nom spécial et des symboles spéciaux pour les adjoints en algèbre linéaire, et c'est la relation avec les produits internes. C'est la raison pour laquelle la "transposition conjuguée" est une opération importante et "la rotation conjuguée des matrices de 90 degrés" ne l'est pas. Il est inutile d'avoir quelque chose qui est « juste une opération de tableau que les swaps lignes et des colonnes et des conjugués » si elle n'est pas connecté aux produits point.

On peut définir une relation similaire entre transpose et un "produit scalaire" non conjugué, ce qui plaiderait pour une transposition récursive. Cependant, les «produits scalaires» non conjugués pour des données complexes, qui ne sont pas du tout des produits internes, n'apparaissent pas presque aussi souvent que de vrais produits internes - ils proviennent de relations de bi-orthogonalité lorsqu'un opérateur est écrit en complexe forme symétrique (pas hermitienne) - et n'ont même pas de fonction Julia intégrée ou de terminologie commune en algèbre linéaire, alors que vouloir échanger des lignes et des colonnes de tableaux non numériques arbitraires semble beaucoup plus courant, en particulier avec la diffusion. C'est pourquoi je peux supporter de rendre transpose non récursif tout en laissant adjoint récursif.

Je vois. Donc, renommer ' en adjoint ferait partie du changement, pour montrer clairement que ce n'est pas conj ∘ transpose ?

Ce qui m'a toujours confondu avec les tableaux récursifs est: qu'est-ce qu'un scalaire dans ce contexte? Dans tous les cas que je connais, on dit que les éléments d'un vecteur sont des scalaires. Même dans le cas où un mathématicien écrit une structure bloc-vecteur / matrice sur un bout de papier, nous savons toujours que ce n'est qu'un raccourci pour un vecteur / matrice plus grand où les éléments sont probablement des nombres réels ou complexes (ie le BlockArray ). Vous vous attendez à ce que les scalaires puissent se multiplier sous * , et le type du scalaire ne diffère généralement pas entre le vecteur et son dual.

@andyferris , pour un espace vectoriel général, les scalaires sont un anneau (nombres) par lequel vous pouvez multiplier les vecteurs. Je peux faire 3 * [[1,2], [3,4]] , mais je ne peux pas faire [3,3] * [[1,2], [3,4]] . Ainsi, même pour Array{Array{Number}} , le type scalaire correct est Number .

D'accord - mais on dit généralement que les éléments sont (les mêmes) scalaires, non?

Les traitements que j'ai vus, de toute façon, commencent par un anneau et en construisent un espace vectoriel. L'anneau prend en charge + , * , mais je n'ai pas vu de traitement qui l'exige pour prendre en charge adjoint , dot , ou autre.

(Désolé, j'essaie juste de comprendre l'objet mathématique sous-jacent que nous modélisons).

Cela dépend de ce que vous entendez par «sténographie» et de ce que vous entendez par «éléments».

Par exemple, il est assez courant d'avoir des vecteurs de fonctions de taille finie et des «matrices» d'opérateurs de dimension infinie. Par exemple, considérons la forme suivante des équations macroscopiques de Maxwell :

image

Dans ce cas, nous avons une matrice 2x2 d'opérateurs linéaires (par exemple des boucles) agissant sur des vecteurs à 2 composantes dont les «éléments» sont des champs de vecteurs à 3 composantes. Ce sont des vecteurs sur le champ scalaire des nombres complexes. Dans un certain sens, si vous explorez suffisamment en profondeur les «éléments» des vecteurs sont des nombres complexes - des composants individuels des champs en des points individuels dans l'espace - mais c'est plutôt trouble.

Ou peut-être plus précisément, ne pouvons-nous pas toujours utiliser l'anneau pour décrire les coefficients du vecteur dans une certaine base (étant donné la nature de tableau des choses dans Julia, nous n'allons pas sans base)?

Dans tous les cas, la conséquence est toujours que les adjoints doivent être récursifs, non? Je ne comprends pas à quel point vous en venez.

(Nous pouvons absolument aller sans base, je pense, puisque les objets ou les éléments des tableaux pourraient être des expressions symboliques comme SymPy ou une autre structure de données représentant un objet mathématique abstrait, et l'adjoint pourrait renvoyer un autre objet qui, lorsqu'il est multiplié, calcule un intégrale, par exemple.)

J'essaie juste de mieux comprendre, sans faire valoir un point particulier :)

Certes, dans ce qui précède, l'adjoint est récursif. Je me demandais si, par exemple, dans ce qui précède, il était préférable de penser à [E, H] comme un BlockVector dont (infiniment?) De nombreux éléments sont complexes, ou comme un 2-vecteur dont les éléments sont des champs vectoriels. Je pense que dans ce cas, l'approche BlockVector serait irréalisable.

Bref, merci.

Alors, je peux peut-être distiller mes pensées sous cette forme:

Pour un vecteur v , je m'attends un peu à ce que length(v) décrive la dimension de l'espace vectoriel, c'est-à-dire le nombre de coefficients scalaires dont j'ai besoin pour décrire (complètement) un élément de l'espace vectoriel, et aussi que v[i] renvoie le coefficient scalaire i e. À ce jour, je n'ai pas pensé à AbstractVector comme un "vecteur abstrait", mais plutôt comme un objet qui contient les coefficients d'un élément d'un espace vectoriel étant donné une base que le programmeur connaît.

Cela semblait être un modèle mental simple et utile, mais c'est peut-être trop restrictif / peu pratique.

(EDIT: C'est restrictif car dans Vector{T} , T doit se comporter comme un scalaire pour que les opérations d'algèbre linéaire fonctionnent.)

mais je ne peux pas faire [3,3] * [[1,2], [3,4]]

Nous pourrions corriger cela. Je ne sais pas si c'est une bonne idée ou non, mais cela pourrait certainement fonctionner.

Je ne suis pas sûr que ce soit souhaitable ... dans ce cas, [3,3] n'est en fait pas un scalaire. (Nous avons également déjà la possibilité de faire [3,3]' * [[1,2], [3,4]] - que je ne sais vraiment pas interpréter dans un sens d'algèbre linéaire).

Cela montre un cas de coin intéressant: nous semblons dire que v1' * v2 est le produit interne (et renvoie donc un "scalaire"), mais [3,3]' * [[1,2], [3,4]] == [12, 18] . Un exemple artificiel, je suppose.

Dans ce cas, [3,3] est un scalaire: les scalaires sont des éléments de l'anneau sous-jacent, et il existe de nombreuses bonnes façons de transformer des éléments de la forme [a,b] en anneau (et ainsi de définir des vecteurs / matrices Au dessus de). Le fait qu'ils soient écrits comme des vecteurs, ou le fait que cet anneau lui-même forme un espace vectoriel sur un autre anneau sous-jacent, ne change rien à cela. (Vous pouvez prendre n'importe quelle autre notation qui pourrait masquer les vecteurs! Ou lui donner un aspect totalement différent.) Comme @andyferris y arrivait , ce qu'est un scalaire dépend du contexte.

Formellement, la récursivité ne fait pas partie de l'adjoint. Au lieu de cela, la conjugaison des éléments scalaires fait cette partie du travail - et souvent, si un scalaire s est représenté comme une matrice, la conjugaison de s impliquera de transposer la matrice que nous utilisons pour désigner il. Mais ce n'est pas toujours le cas, cela dépend de la structure donnée par l'utilisateur à l'anneau de scalaires.

Je pense que forcer la récursion adjointe peut être un bon compromis pratique, typique des applications de type matlab. Mais pour moi, le prendre très au sérieux signifierait un adjoint non récursif et utiliser des scalaires typés + dispatch pour laisser conj travailler la magie nécessaire sur quelle que soit la structure de l'anneau sous-jacent.

Dans ce cas [3,3] est un scalaire: les scalaires sont des éléments dans l'anneau sous-jacent

Sauf qu'un tel scalaire n'est pas un élément de l'anneau défini par + , * . Il ne prend pas en charge * ; Je ne peux pas faire [3,3] * [3,3] .

Je pense que forcer la récursion adjointe peut être un bon compromis pratique, typique des applications de type matlab. Mais pour moi, le prendre très au sérieux signifierait un adjoint non récursif et utiliser des scalaires typés + dispatch pour laisser conj travailler la magie nécessaire sur quelle que soit la structure de l'anneau sous-jacent.

Je suis d'accord, si nous voulons faire une compréhension pratique, c'est tout à fait bien et c'est ce que nous avons fait jusqu'à présent, mais il me semble que les scalaires sous-jacents sont ceux du tableau entièrement aplati et c'est sur quoi l'algèbre linéaire est définie . Nous avons la technologie pour faire un BlockVector et un BlockMatrix (et BlockDiagonal , etc.) qui présentent une vue efficace et aplatie du tableau entièrement étendu, donc le "super sérieux "l'approche devrait, au minimum, être faisable. Je pense aussi que ce serait tout aussi convivial que l'approche récursive (quelques caractères supplémentaires à taper BlockMatrix([A B; C D]) en échange de ce qui pourrait être un code sémantique plus agréable et potentiellement plus lisible - je suis sûr que ceux-ci points sont à débattre). Cependant, il serait plus difficile de mettre en œuvre tout cela.

@andyferris vous avez raison, ce n'est pas un anneau car * n'est pas défini - mais je suppose que nous sommes sur la même longueur d'onde que * pourrait facilement être défini pour cela, et il existe de nombreuses façons de le faire qui donneront une structure en anneau. Je suppose donc que nous sommes sur la même longueur d'onde: la saisie est le moyen le plus extensible de résoudre ce problème.

(À propos des scalaires: les scalaires ne sont

@felixrehren , un vecteur de vecteurs sur un anneau R est mieux compris comme un espace à somme directe, qui est également un espace vectoriel sur R. Il est absurde d'appeler les vecteurs eux-mêmes «l'anneau sous-jacent», car en général ils ne sont pas un bague. Ce raisonnement s'applique parfaitement à [[1,2], [3,4]] .

La conjugaison et les adjoints sont normalement des concepts séparés; dire que "la conjugaison des éléments scalaires" devrait faire le travail ici me semble totalement incorrect - le contraire de "sérieux" - sauf dans le cas particulier noté ci-dessous.

Considérons le cas des "vecteurs colonnes à 2 composants" (| u⟩, | v⟩) de deux éléments | u⟩ et | v⟩ dans un espace de Hilbert H sur un anneau (disons les nombres complexes ℂ), en notation Dirac: «vecteurs de vecteurs». C'est un espace de Hilbert à somme directe H⊕H avec le produit intérieur naturel ⟨(| u⟩, | v⟩), (| w⟩, | z⟩)⟩ = ⟨u | w⟩ + ⟨v | z⟩. L'adjoint doit donc produire l'opérateur linéaire constitué du "vecteur ligne" (⟨u | ⟨v |), dont les éléments sont eux-mêmes des opérateurs linéaires: l'adjoint est"récursif" . Ceci est totalement différent du conjugué complexe, qui produit un élément du même espace de Hilbert H⊕H, induit par la conjugaison de l'anneau sous-jacent.

Vous confondez les choses en vous référant à des vecteurs sur quaternions, etc. Si les "éléments" du vecteur sont aussi des éléments de l'anneau "complexifié" sous-jacent, alors bien sûr l'adjoint et le conjugué de ces éléments est la même chose. Cependant, cela ne s'applique pas à tous les espaces de produits directs.

En d'autres termes, le type d'objet doit indiquer deux informations différentes :

  • L'anneau scalaire (d'où le conjugué, qui produit un élément du même espace).
  • Le produit interne (d'où l'adjoint, qui produit un élément de l'espace dual ).

Pour Vector{T<:Number} , nous devrions le lire comme indiquant que l'anneau scalaire est T (ou complex(T) pour l'espace vectoriel complexifié), et que le produit interne est le produit euclidien habituel .

Si vous avez un vecteur de vecteurs, alors c'est un espace de Hilbert à somme directe et l'anneau est la promotion des anneaux scalaires des vecteurs individuels, et le produit scalaire est la somme des produits scalaires des éléments. (Si les scalaires ne peuvent pas être promus ou si les produits scalaires ne peuvent pas être additionnés, alors ce n'est pas du tout un espace vectoriel / Hilbert.)

Si vous avez un scalaire T<:Number , alors conj(x::T) == adjoint(x::T) .

Donc, si vous essayez de représenter des nombres complexes z::Complex{T} par les matrices 2x2 m(z)::Array{T,2} , votre type indique à la fois un anneau différent T et un produit intérieur différent par rapport à z , et par conséquent vous ne devriez pas vous attendre à ce que conj ou adjoint donnent des résultats équivalents.

Je vois un peu ce que vous dites @felixrehren. Si le scalaire est réel, alors un octernien peut être considéré comme un espace vectoriel à 8 dimensions. Mais si le coefficient scalaire était un octernien, alors il existe une base de dimension 1 triviale qui représente tous les octerniens. Je ne sais pas si c'est différent de ce que j'ai dit (ou de ce que je voulais dire: sourire :), si nous avons un AbstractVector{T1} il pourrait être isomorphe à un AbstractVector{T2} d'une dimension différente ( length ). Mais T1 et T2 devraient tous deux être des anneaux sous les opérateurs Julia + et * (et l'isomorphisme pourrait préserver le comportement de + mais pas * , ni le produit interne ou adjoint).

@stevengj J'ai toujours pensé à la somme directe exactement comme mon BlockVector . Vous avez deux bases incomplètes (disjointes). Dans chaque base, vous pourriez être capable de former des superpositions comme c₁ | X₁⟩ + c₂ | X₂⟩ dans la première base ou c₃ | Y₁⟩ + c₄ | Y₂⟩ dans la seconde. La "somme directe" représente l'espace des états qui ressemblent à (c₁ | X₁⟩ + c₂ | X₂⟩) + (c₃ | Y₁⟩ + c₄ | Y₂⟩). Il me semble que la seule chose spéciale qui sépare cela d'une base de dimension quatre sur les nombres complexes (c'est-à-dire des états comme c₁ | X₁⟩ + c₂ | X₂⟩ + c₃ | Y₁⟩ + c₄ | Y₂⟩) sont les crochets - pour moi, cela semble notionnel; la somme directe est juste un moyen pratique de les écrire sur papier ou avec des tableaux sur un ordinateur (et pourrait être utile, par exemple, pour nous laisser cataloguer et tirer parti des symétries, ou pour diviser le problème (peut-être pour le paralléliser), etc. ). Dans cet exemple, X⊕Y est toujours un espace vectoriel à quatre dimensions où les scalaires sont complexes.

C'est ce qui me donne envie de eltype(v) == Complex{...} et length(v) == 4 plutôt que eltype(v) == Vector{Complex{...}} et length(v) == 2 . Cela m'aide à voir et à raisonner sur l'anneau sous-jacent et l'espace vectoriel. N'est-ce pas aussi cet espace "aplati" où l'on peut faire une transposition "globale" et par élément conj pour calculer l'adjoint?

(vous avez écrit deux articles pendant que j'écrivais un seul, @stevengj : sourire :)

Bien sûr, si nous préférons utiliser des tableaux imbriqués comme convention pour la somme directe (comme nous le faisons maintenant) plutôt que BlockArray , cela peut aussi être agréable et cohérent! Nous pourrions bénéficier de quelques fonctions supplémentaires pour déterminer le champ scalaire (une sorte d'éltype récursif) et ce que pourrait être la dimensionnalité aplatie effective (une sorte de taille récursive) pour qu'il soit un peu facile de raisonner sur l'algèbre linéaire que nous faisons.

Pour être clair, je suis satisfait des deux approches et j'ai beaucoup appris de cette discussion. Merci.

@andyferris , ce n'est pas parce que deux espaces sont isomorphes qu'ils sont le "même" espace, et se demander lequel d'entre eux est "vraiment" l'espace vectoriel (ou s'ils sont "vraiment" les mêmes) est un bourbier métaphysique dont nous n'échapperons jamais. (Mais le cas des sommes directes d'espaces vectoriels de dimension infinie illustre une limitation de votre concept "aplati" d'une somme directe comme étant "tous les éléments de l'un suivis de tous les éléments de l'autre".)

Encore une fois, je ne suis pas sûr de votre argument. Proposez-vous sérieusement que eltype(::Vector{Vector{Complex}}) dans Julia soit Complex , ou que length renvoie la somme des longueurs? Que tout le monde en Julia devrait être forcé d'adopter votre isomorphisme «aplati» pour les espaces à somme directe? Sinon, alors adjoint doit être récursif.

Et si vous «essayez simplement de mieux comprendre, sans faire valoir un point particulier», pouvez-vous le présenter à un autre forum? Ce problème est assez déroutant sans arguments métaphysiques sur la signification des espaces à somme directe.

ce n'est pas parce que deux espaces sont isomorphes qu'ils sont le "même" espace

Je ne le suggérais certainement pas du tout.

Encore une fois, je ne suis pas sûr de votre point de vue

Je suggère sérieusement que si vous voulez faire de l'algèbre linéaire avec un AbstractArray , alors le eltype serait mieux un anneau (support + , * et conj ) qui exclut par exemple les eltypes de Vector parce que [1,2] * [3,4] ne fonctionne pas. Et le length d'un vecteur représenterait en fait la dimensionnalité de votre algèbre linéaire. Oui, cela exclut les espaces vectoriels infiniment dimensionnels, mais généralement dans Julia AbstractVector ne peut pas être de taille infinie. Je pense que cela faciliterait le raisonnement sur la façon d'implémenter et d'utiliser la fonctionnalité d'algèbre linéaire sur Julia. Cependant, les utilisateurs devraient désigner explicitement une somme directe via un BlockArray ou similaire plutôt que d'utiliser des structures de tableaux imbriqués.

C'est une suggestion assez cassante, et a des inconvénients notables (certains que vous avez mentionnés), donc je ne serai pas malheureux si nous disons "c'est une bonne idée, mais ce ne sera pas pratique". Cependant, j'ai également essayé de souligner que l'approche des tableaux imbriqués rend certaines choses concernant l'algèbre linéaire sous-jacente un peu plus opaque.

Tout cela semble directement pertinent pour le PO. Avec une approche aplatie forcée , nous abandonnerions les transposés / adjoints récursifs, et si nous abandonnions les transposés / adjoints récursifs, seules les structures aplaties pourraient être viables pour l'algèbre linéaire. Si nous n'appliquons pas une approche aplatie, nous devons garder un adjoint récursif. Pour moi, cela semblait être une décision liée.

Proposez-vous sérieusement que eltype(::Vector{Vector{Complex}}) dans Julia soit Complex , ou que length renvoie la somme des longueurs?

Non, désolé, peut-être que ce que j'ai écrit n'était pas clair - je suggérais simplement que deux nouvelles fonctions pratiques à cet effet pourraient être utiles dans certaines circonstances. Parfois, vous voudrez peut-être le zero ou one de cet anneau, par exemple.

Êtes-vous en train de dire que tout le monde en Julia devrait être forcé d'adopter votre isomorphisme «aplati» pour les espaces à somme directe?

Je suggérais cela comme une possibilité, oui. Je ne suis pas convaincu à 100% que ce soit la meilleure idée, mais j'estime qu'il y a des avantages (et des inconvénients).

Revenant ici au domaine des changements exploitables , il semble que la version simplifiée de

  • Gardez conj tel quel (élément par élément).
  • Faire correspondre a' à adjoint(a) , et le rendre récursif toujours (et donc échouer pour les tableaux de chaînes, etc. où adjoint n'est pas défini pour les éléments).
  • Faites correspondre a.' à transpose(a) , et ne le rendez jamais récursif (juste un permutedims ), et donc ignorez le type des éléments.

Les principaux "todos" réels qui peuvent en être extraits sont:

  1. [] Renommez ctranspose en adjoint .
  2. [] Remplacez transpose par non récursif.
  3. [] Déterminez comment cela s'intègre aux vecteurs de ligne.

Je soupçonne que s'appuyer sur la transposition récursive est suffisamment rare pour que nous puissions simplement changer cela dans 1.0 et le lister comme une rupture dans NEWS. Changer ctranspose en adjoint peut passer par une obsolescence normale (cependant nous le faisons pour 1.0). Y a-t-il autre chose à faire?

@StefanKarpinski , nous devrons également apporter le changement correspondant à RowVector . Une possibilité serait de diviser respectivement RowVector en types Transpose et Adjoint pour transpose paresseux non récursifs et récursifs paresseux adjoint , et se débarrasser de Conj (lazy conj ).

Quelques changements supplémentaires au moins tangentiellement liés

  • Certains types de vues pour transpose et adjoint sur AbstractMatrix
  • Rendre adjoint(::Diagonal) récursif (sans doute nous devrions corriger ce "bug" dans Diagonal pour transpose et ctranspose avant Julia v0.6.0)
  • Utilisez le type "scalaire" approprié dans des choses comme: v = Vector{Vector{Float64}}(); dot(v,v) (actuellement une erreur - il essaie de retourner zero(Vector{Float64}) )

OK, j'ai essayé de prendre plus au sérieux les blocs-matrices récursifs, et pour sauvegarder cela, j'essaie également d'améliorer le comportement de Diagonal ici (il existe déjà des méthodes qui anticipent une diagonale de bloc structure, comme getindex , il semble donc naturel de rendre la transposition récursive dans ce cas).

Là où je suis bloqué, et ce qui a directement motivé tous mes points de discussion ci-dessus, c'est comment faire des opérations d'algèbre linéaire sur une telle structure, y compris inv , det , expm , eig , et ainsi de suite. Par exemple, il existe une méthode pour det(::Diagonal{T}) where T qui prend juste le produit de tous les éléments diagonaux. Fonctionne à merveille pour T <: Number , et cela fonctionne également si tous les éléments sont des matrices carrées de même taille . (Bien sûr, si ce sont des matrices carrées de même taille, alors les éléments forment un anneau, et c'est tout à fait raisonnable - également la transposition récursive (edit: adjoint) est la bonne chose).

Cependant, si nous pensons à une matrice bloc-diagonale générale Diagonal{Matrix{T}} , ou à toute matrice bloc Matrix{Matrix{T}} , alors nous pouvons généralement parler de son déterminant comme un scalaire T . C'est-à-dire que si la taille était (3 ⊕ 4) × (3 ⊕ 4) (une matrice 2 × 2 avec des éléments diagonaux qui sont des matrices de dimensions 3 × 3, 4 × 4 et des éléments hors diagonale correspondants), devrait det renvoyer le déterminant de la structure 7 × 7 «aplatie», ou devrait-il simplement essayer de multiplier les éléments du 2 × 2 tels quels (et sans erreur, dans ce cas)?

(modifier: j'ai changé les dimensions ci-dessus pour qu'elles soient toutes différentes, ce qui devrait éviter un langage ambigu)

Je n'ai pas de problème avec le fait que a' soit récursif, mais personnellement, je trouverais la nouvelle notation a.' extrêmement déroutante. Si vous me montriez la syntaxe a.' je vous dirais, sur la base de mon modèle mental de Julia, qu'il effectue transpose.(a) ie transposition élémentaire de a , et je serais complètement faux. Alternativement, si vous me montriez que a' et a.' étaient les deux options pour une transposition, et que l'une d'elles était également récursée par élément, je vous dirais que a.' doit avoir récursion élémentaire, et je me trompe encore une fois.

Bien sûr, mon modèle mental de ce que signifie . est tout simplement faux dans ce cas. Mais je suppose que je ne suis pas le seul à interpréter cette syntaxe exactement de la mauvaise manière. À cette fin, je suggérerais d'utiliser autre chose que .' pour la transposition non récursive.

@rdeits J'ai bien peur que cette syntaxe dérive de MATLAB. Cela est devenu un peu malheureux une fois que la (assez merveilleuse) syntaxe de diffusion dot-call a été introduite pour la v0.5.

Nous pourrions forger notre propre chemin séparé de MATLAB et changer cela - mais je crois qu'il y a eu une discussion séparée quelque part à ce sujet (n'importe qui se souvient où?).

Ah, c'est malheureux. Merci!

Un autre todo:

  • [] Corrigez issymmetric et ishermitian pour faire correspondre transpose et adjoint - le premier de chaque paire étant non récursif, le dernier de chaque paire étant récursif.

La syntaxe Matlab .' est certainement assez malheureuse dans le contexte de notre nouvelle syntaxe . . Je ne serais pas contre le changement, mais nous avons besoin d'une nouvelle syntaxe pour la transposition, et je ne suis pas sûr que nous en ayons une disponible. Quelqu'un a des suggestions de transposition?

Déplaçons la discussion sur la syntaxe de transposition ici: https://github.com/JuliaLang/julia/issues/21037.

Je n'ai pas d'opinion tranchée sur le fait que transpose / ctranspose / adjoint est récursif, mais je préfère ne pas traiter A::Matrix{Matrix{T}} comme une matrice de blocs dans le sens d'un paresseux hvcat , ce que @andyferris semble impliquer au moins en partie. Autrement dit, si les éléments de A étaient tous des matrices carrées de la même taille (c'est-à-dire forment un anneau), je m'attendrais à ce que det(A) renvoie à nouveau un carré de cette taille. S'ils sont rectangulaires ou de tailles différentes, je m'attendrais à une erreur.

Cela dit, un type block matrix / lazy cat pourrait être utile, mais il devrait aller jusqu'au bout et définir par exemple getindex pour travailler sur les données aplaties. Mais je n'aimerais certainement pas voir ce concept absorbé par Matrix ou tout autre des types de matrice existants comme Diagonal .

C'est soulageant, @martinholters. Ce dont je paniquais plus tôt dans ce fil de discussion était l'idée que nous devrions d'une manière ou d'une autre être en mesure d'appliquer l'ensemble du cadre d'algèbre linéaire de Julia à Matrix{Matrix} pour des matrices de blocs arbitraires (où les sous-matrices ont des tailles différentes).

Ce que je disais avec l '«aplatissement», ce n'est pas de faire une introspection fantaisiste de ce que sont les éléments, et de traiter simplement les éléments sur leurs propres mérites comme des éléments d'un anneau. Cependant, récursif ctranspose / adjoint étend cela pour permettre des éléments qui agissent comme des opérateurs linéaires, ce qui semble correct et utile. (L'autre cas concerne les éléments d'un vecteur qui agissent comme un vecteur, où un adjoint récursif est également correct).

Pour revenir un peu plus loin, quelle était la motivation initiale pour supprimer le comportement de non-opération par défaut pour transpose(x) = x et ctranpsose(x) = conj(x) ? Celles-ci m'ont toujours semblé très utiles.

Pour revenir un peu plus loin, quelle était la motivation initiale pour supprimer le comportement de non-opération par défaut pour transpose (x) = x et ctranpsose (x) = conj (x)? Celles-ci m'ont toujours semblé très utiles.

Cela était motivé par des types d'opérateurs linéaires personnalisés (qui ne peuvent pas être des sous-types de AbstractArray) qui n'ont pas réussi à se spécialiser ctranspose . Cela signifiait qu'ils héritaient du mauvais comportement de non-opération d'un repli. Nous avons essayé de structurer la répartition de telle sorte que les solutions de secours ne soient jamais silencieusement incorrectes (mais elles peuvent avoir une complexité pessimiste). https://github.com/JuliaLang/julia/issues/13171

Nous semblons avoir deux options - dans les deux, transpose devient non récursif, sinon:

  1. Non récursif ctranspose .
  2. Récursif ctranspose .

@stevengj , @jiahao , @andreasnoack - quelles sont vos préférences ici? Autres?

J'y ai beaucoup réfléchi depuis la conférence JuliaCon 2017 de @jiahao.

Pour moi, je pense toujours que l'algèbre linéaire devrait être définie par rapport à un champ "scalaire" comme un type d'élément T . Si T est un champ (prend en charge + , * et conj (également - , / , .. .)) alors je ne vois pas pourquoi les méthodes de Base.LinAlg devraient échouer.

OTOH il est assez courant (et valable) de parler de la transposition d'une matrice de blocs, par exemple. Je pense que nous pouvons apprendre de la théorie des types "mathématique" ici, qui essaie par exemple de traiter d'étranges déclarations résultant de la discussion d'ensembles d'ensembles en ayant des ensembles de scalaires du "premier ordre", des ensembles d'ensembles de scalaires de "second ordre", "troisième order "des ensembles d'ensembles d'ensembles de scalaires, et ainsi de suite. Je pense que nous avons le même problème (et l'opportunité) ici lorsqu'il s'agit de tableaux de tableaux - nous pouvons potentiellement utiliser le système de types de Julia pour décrire des tableaux de "premier ordre" de "vrais" scalaires, des tableaux de "second ordre" de tableaux de scalaires, etc. . Je pense que les longues discussions entre @stevengj , moi-même et d’autres ont résulté du fait que, oui, vous pouvez trouver un ensemble d’opérations auto-cohérent sur des tableaux «d’ordre» arbitraire, et dans ce cadre (c) transposer est clairement et correctement récursif, mais vous n'êtes pas obligé de le faire. Comme le discours de Jiahao l'a clairement souligné, les langages / frameworks de programmation font des choix sémantiques pour distinguer les scalaires des tableaux (ou non), distinguer les vecteurs des matrices (ou non) et ainsi de suite, et je pense que c'est un choix connexe que nous pouvons faire.

Pour moi, le point crucial ici est que pour faire fonctionner le tableau "d'ordre arbitraire", vous devez enseigner à vos "scalaires" certaines opérations qui ne sont normalement définies que sur des vecteurs et des matrices. La méthode Julia actuelle qui fait cela est transpose(x::Number) = x . Cependant, je préférerais vraiment que nous étendions notre distinction sémantique entre les tableaux et les scalaires en supprimant cette méthode - et je pense que cela peut être tout à fait faisable.

L'étape suivante consisterait à enseigner à Base.LinAlg la différence entre une matrice du premier ordre et une matrice du second ordre et ainsi de suite. J'ai pensé à deux options viables, il y en a peut-être plus, je ne sais vraiment pas:

  • Avoir une sorte de trait opt-in pour "arrayness", de sorte que transpose(::AbstractMatrix{T}) soit récursif quand T est un AbstractMatOrVec , et pas quand T est un Number , et rendez-le configurable (opt-in, opt-out, peu importe) sinon. (D'une certaine manière, cela a été et est fait actuellement en contrôlant le comportement de la méthode transpose pour les éléments, mais je pense qu'il est plus propre de contrôler plutôt le comportement de la méthode transpose du (externe ) tableau).
  • Vous pouvez également affirmer que la plupart des AbstractArray y compris les Array sont de premier ordre (leurs éléments sont des champs scalaires), et utilisent un type AbstractArray (disons NestedArray ) pour envelopper les tableaux et "marquer" que leurs éléments doivent être traités comme des tableaux et non comme des scalaires. J'ai essayé de jouer un peu avec cela, mais il semblait que l'option ci-dessus était probablement moins lourde pour les utilisateurs.

J'ai le sentiment que Julia fait une forte séparation sémantique entre les tableaux et les scalaires, et que la situation actuelle me semble un peu incompatible avec cela, puisque les scalaires devaient être "enseignés" sur la propriété du tableau transpose . Un utilisateur peut clairement dire que Matrix{Float64} est une matrice de scalaires (tableau du premier ordre) et Matrix{Matrix{Float64}} est une matrice de blocs (tableau du second ordre). Il peut être plus cohérent pour nous d'utiliser le système de types ou les traits pour déterminer si un tableau est du «premier ordre» ou autre. Je pense aussi que la première option que j'ai énumérée rendrait Julia plus conviviale (donc je peux faire ["abc", "def"].' ) tout en conservant la flexibilité de faire une chose par défaut raisonnable / utile avec des matrices de blocs.

Désolé de persister si longtemps, mais après avoir parlé avec @StefanKarpinski ci-dessus «fonctionnent», mais pour moi, cela «fonctionnerait» exactement comme l'algèbre matricielle / vectorielle «fonctionnait» avant l'introduction de RowVector .

Le TLDR était - ne faites pas ( c ) transpose récursif ou non - laissez-le faire les deux (c'est-à-dire être configurable).

Ce sont de bons points, mais je tiens simplement à souligner que presque toutes (sinon toutes) les plaintes concernant la récursivité de (c)transpose sont liées à l'utilisation non mathématique de ' et .' lors du remodelage, par exemple Vector{String} et Vector{PyObject} en matrices 1xn. Il est facile de corriger type par type mais je peux voir que c'est ennuyeux. Cependant, on pensait que la définition récursive était la définition mathématiquement correcte et que "correct mais ennuyeux dans de nombreux cas" était mieux que "pratique mais faux dans de rares cas".

Une solution possible pourrait être votre suggestion dans la première puce, c'est-à-dire de n'avoir que des transpositions récursives pour les éléments de type tableau. Je crois que T<:AbstractVecOrMat couvrirait la plupart des cas en changeant le statut en "pratique mais faux dans de très rares cas". La raison pour laquelle cela pourrait encore être faux est que certains types de type opérateur ne rentrent pas dans la catégorie AbstractMatrix car l'interface AbstractArray concerne principalement la sémantique des tableaux ( getindex ) , pas de l'algèbre linéaire.

@andyferris , l'adjoint et le dual d'un scalaire sont parfaitement bien définis, et ctranspose(x::Number) = conj(x) est correct.

Mon sentiment est que la plupart des utilisations de transpose sont "non-mathy", et la plupart des utilisations de ctranspose (c'est-à-dire les utilisations où le comportement de conjugaison est souhaité) sont mathématiques (car il n'y a pas d'autre raison de conjuguer ). Par conséquent, j'aurais tendance à être en faveur des transpose récursifs et des ctranspose non récursifs.

Personnellement, je pense qu'essayer de regarder les tableaux de blocs comme imbriqués Arrays devient compliqué pour les raisons ici et il est peut-être préférable d'avoir un type dédié à la https://github.com/KristofferC/BlockArrays.jl pour ce.

Ça a l'air sympa, @KristofferC.

Il y a un point très valable que @stevengj fait ci-dessus, et c'est que parfois vos éléments peuvent par exemple être des vecteurs ou des opérateurs linéaires au sens mathématique, mais pas AbstractArray s. Nous avons certainement besoin d'un moyen de faire une transposition récursive (c) pour ceux-ci - je ne sais pas si vous y avez pensé ou non, mais j'ai pensé le mentionner.

Faits saillants d'une conversation slack / # linalg sur ce sujet. Récapitule certains des fils ci-dessus. Se concentre sur la sémantique, évite l'orthographe. (Merci à tous ceux qui ont participé à cette conversation! :))

Trois opérations sémantiquement distinctes existent:
1) "adjoint mathématique" (récursif et paresseux)
2) "transposition mathématique" (idéalement récursive et paresseuse)
3) "transposition structurelle" (idéalement non récursive et? Impatiente?)

Situation actuelle: "adjoint mathématique" correspond à ctranspose , "transposition mathématique" à transpose , et "transposition structurelle" à permutedims(C, (2, 1)) pour deux dimensions C et reshape(C, 1, length(C)) pour un dixième C . Le problème: la "transposition structurelle" est une opération courante, et l'histoire de permutedims / reshape est quelque peu déroutante / non naturelle / ennuyeuse en pratique.

Comment cela s'est produit: Auparavant, la "transposition structurelle" était confondue avec "l'adjoint mathématique" / "transposition mathématique" via des solutions de remplacement génériques sans opération comme transpose(x::Any) = x , ctranspose(x::Any) = conj(x) et conj(x::Any) = x . Ces solutions de secours ont fait que [c]transpose et les opérateurs postfix associés ' / .' servent à la "transposition structurelle" dans la plupart des cas. Génial. Mais ils ont également fait des opérations impliquant [c]transpose sur certains types numériques définis par l'utilisateur échouent silencieusement (retournent des résultats incorrects) sans définition des spécialisations [c]transpose pour ces types. Aie. Par conséquent, ces solutions de secours génériques sans opération ont été supprimées, ce qui a abouti à la situation actuelle.

La question est de savoir quoi faire maintenant.

Résultat idéal: Fournir une incantation unifiée, naturelle et compacte pour la «transposition structurelle». Soutenir simultanément l'adjoint mathématique sémantiquement correct et la transposition. Évitez d'introduire des cas de coin délicats lors de l'utilisation ou de la mise en œuvre.

Deux grandes propositions existent:

(1) Fournir l'adjoint mathématique, la transposition mathématique et la transposition structurelle sous forme de trois opérations syntaxiquement et sémantiquement distinctes. Inconvénients: fait tout fonctionner, sépare proprement les concepts et évite les cas de coin délicats. Inconvénients: Trois opérations à expliquer et à mettre en œuvre.

(2) Shoe-horn les trois opérations en deux. Trois formes de cette proposition existent:

(2a) Faire de transpose sémantiquement "transposée structurelle", servant également de "transposition mathématique" dans les cas courants. Inconvénients: deux opérations. Fonctionne comme prévu pour les conteneurs avec des éléments scalaires sans ambiguïté. Inconvénients: Quiconque s'attend à une sémantique de «transposition mathématique» recevra silencieusement des résultats incorrects sur des objets plus complexes que des conteneurs avec des éléments scalaires sans ambiguïté. Si l'utilisateur détecte ce problème, obtenir une sémantique de «transposition mathématique» nécessite de définir de nouveaux types et / ou méthodes.

(2b) Introduisez un trait indiquant la «mathématique» d'un type. Lors de l'application de l'adjoint / transposition à un objet, si les types conteneur / élément sont "mathy", recurse; sinon, ne rentrez pas. Inconvénients: deux opérations. Pourrait couvrir une variété de cas courants. Inconvénients: Souffre du même problème que les solutions [c]transpose secours génériques no-op

(2c) Gardez adjoint ( ctranspose ) "adjoint mathématique" et transpose "transposition mathématique", et introduisez des méthodes spécialisées (pas génériques de secours) pour adjoint / transpose pour les types scalaires non numériques (par exemple adjoint(s::AbstractString) = s ). Inconvénients: deux opérations. Corrigez la sémantique mathématique. Inconvénients: nécessite une définition fragmentaire de adjoint / transpose pour les types non numériques, ce qui empêche la programmation générique. Ne permet pas l'application de la transposition structurelle aux types "mathy".

Les propositions (1) et (2a) ont bénéficié d'un soutien nettement plus large que (2b) et (2c).

Veuillez trouver plus de discussion dans # 19344, # 21037, # 13171 et slack / # linalg. Meilleur!

Merci pour la belle rédaction!

Je voudrais mettre une autre possibilité sur le tableau qui irait bien avec l'option 1: avoir différents types de conteneurs pour les matrices mathématiques et les données tabulaires. Ensuite, la signification de A' pourrait être décidée par le type de A (note: pas son type d'élément comme indiqué ci-dessus). Les inconvénients ici sont que cela pourrait nécessiter beaucoup de convert s entre les deux (n'est-ce pas?) Et, bien sûr, serait très perturbateur. Certes, je suis plus que sceptique quant à savoir si les avantages justifieraient cette perturbation, mais je voulais tout de même le mentionner.

Merci, excellente rédaction. Je vote pour (1).

Grande rédaction. Notez que pour (1) les orthographes pourraient être:

  • Adjoint: a' (récursif, paresseux)
  • Transposition mathématique: conj(a') (récursif, paresseux)
  • Transposition structurelle: a.' (non récursif, désireux)

Il n'y aurait donc pas nécessairement besoin d'introduire un nouvel opérateur - la transposition mathématique serait juste une composition (paresseuse et donc efficace) d'adjoint et de conjugaison. Le plus gros problème est qu'il crée deux opérateurs d'apparence similaire, à savoir postfix ' et .' , assez distincts sémantiquement. Cependant, je dirais que dans le code générique le plus correct, le fait que quelqu'un ait utilisé ' ou .' est un indicateur précis à 99% pour savoir s'il voulait dire "donne-moi l'adjoint de cette chose" ou "échange les dimensions de cette chose ". De plus, dans les cas où quelqu'un a utilisé ' et signifiait en fait "permuter les dimensions de cette chose", leur code serait déjà incorrect pour toute matrice avec des éléments dont l'adjoint scalaire n'est pas trivial, par exemple les nombres complexes. Dans les quelques cas restants où quelqu'un voulait vraiment dire "donnez-moi le conjugué de l'adjoint de ceci", je dirais qu'écrire conj(a') rend ce sens beaucoup plus clair puisque dans la pratique les gens utilisent effectivement a.' pour signifie "permuter les dimensions de a ".

Il reste à déterminer tous les types génériques sous-jacents dont nous aurions besoin pour cela, mais @andyferris et @andreasnoack ont déjà quelques réflexions à ce sujet et cela semble possible.

J'ai pensé que je devrais aussi mettre à jour, puisque cette discussion s'est poursuivie ailleurs. J'avoue qu'il y avait une chose (évidente?) À propos de cette proposition que j'avais complètement ratée, c'est que je supposais que nous continuerions à utiliser .' pour l'algèbre linéaire, mais j'aurais dû réaliser que ce n'est pas le cas !

Par exemple, vector.' n'aura plus besoin d'être un RowVector (puisque RowVector est un concept d'algèbre linéaire, notre première tentative de vecteur "dual") - il peut simplement être un Matrix . Quand je veux la "transposition non conjuguée" en algèbre linéaire, ce que je fais vraiment , c'est prendre conj(adjoint(a)) , et c'est ce que nous utiliserons et recommanderons à tous les utilisateurs d'algèbre linéaire d'utiliser (jusqu'à présent, j'ai avait une «mauvaise» habitude de longue date depuis MATLAB d'utiliser simplement a.' plutôt que a' pour transposer n'importe quelle matrice (ou vecteur) que je savais réelle, alors que ce que je voulais vraiment était le adjoint (ou double) - le changement de nom sera d'une grande aide ici).

Je ferai également remarquer brièvement que cela laisse un espace intéressant. Auparavant, vector' et vector.' devaient satisfaire le double besoin de faire une "transposition de données" et de prendre quelque chose comme le vecteur dual. Maintenant que .' sert à manipuler les tailles de tableaux et que ' est un concept d'algèbre linéaire, nous pourrions peut-être changer RowVector en 1D DualVector ou autre chose entièrement. (Peut-être que nous ne devrions pas en discuter ici - si quelqu'un a envie de cela, faisons un autre problème.)

Enfin, je vais copier une proposition de plan d'action de Slack:

1) déplacez transpose de LinAlg à Base et ajoutez des depwarns (0.7 seulement) à ce sujet ne faisant plus un RowVector ou étant récursif (si c'est possible)
2) renommer ctranspose en adjoint partout
3) assurez-vous que Vector , ConjVector et RowVector fonctionnent avec des combinaisons de ' et conj et * , éventuellement renommer RowVector . (ici nous rendons également conj(vector) paresseux).
4) introduire une matrice adjointe paresseuse qui interagit également bien avec conj et ConjMatrix
5) supprimer A_mul_Bc etc. Renommez A_mul_B! en mul! (ou *! ).
6) profit

@stevengj a écrit:

@andyferris , l'adjoint et le dual d'un scalaire sont parfaitement bien définis, et ctranspose(x::Number) = conj(x) est correct.

Pour mémoire, je suis tout à fait d'accord avec cela.

Mon sentiment est que la plupart des utilisations de transpose sont "non-mathy", et la plupart des utilisations de ctranspose (...) sont mathy

Donc l'idée sera de formaliser ceci: toutes les utilisations de transpose deviendront "non-mathy" et la seule utilisation de adjoint sera "mathy".

Par exemple, vector.' n'aura plus besoin d'être un RowVector (puisque RowVector est un concept d'algèbre linéaire, notre première tentative de vecteur "dual") - il peut simplement être un Matrix .

Cependant, nous voulons toujours que .' soit paresseux. par exemple, X .= f.(x, y.') doit toujours être non alloué.

Voici une question: comment devrions-nous implémenter issymmetric(::AbstractMatrix{<:AbstractMatrix}) ? Serait-ce un chèque non récursif, pour correspondre à transpose ? Je regarde la mise en œuvre; il suppose que les éléments peuvent transpose . OTOH il semble tout à fait valide de vérifier si issymmetric(::Matrix{String}) ...

Il n'est actuellement pas récursif s'il est enveloppé dans Symmetric btw

julia> A = [rand(2, 2) for i in 1:2, j in 1:2]; A[1, 2] = A[2, 1]; As = Symmetric(A);

julia> issymmetric(A)
false

julia> issymmetric(As)
true

julia> A == As
true

Oui, je travaille sur la création d'un PR pour cela et il y a beaucoup de ces types d'incohérences. (Je suis tombé sur celui-là il n'y a pas longtemps en recherchant chaque instance de transpose , adjoint et conj dans le code du tableau).

Sauf indication contraire, j'implémenterai un comportement tel que issymmetric(a) == (a == a.') et ishermitian(a) == (a == a') . Ceux-ci semblent assez intuitifs en eux-mêmes et l'utilisation existante AFAICT de issymmetric est pour les types d'éléments Number (ou bien a fréquemment d'autres erreurs / hypothèses qui n'auront pas beaucoup de sens pour les tableaux imbriqués) .

(L'implémentation alternative est issymmetric(a) == (a == conj(adjoint(a))) ... pas aussi "jolie" ni ne fonctionnera pour les tableaux de "données" (de String , etc), mais elle coïncide sur des tableaux de Number )

Sauf indication contraire, je vais implémenter un comportement tel que issymmetric(a) == (a == a.') et ishermitian(a) == (a == a')

Cela me semble être la bonne solution.

Mise à jour: j'ai changé d'avis. Symmetric est probablement principalement pour l'algèbre linéaire

Juste une petite suggestion: il est souvent utile de faire une somme sur le produit de deux vecteurs ( dotu ), et x.’y renvoyer un scalaire est un moyen pratique de le faire. Je serais donc en faveur de ne pas retourner un Matrix de transpose(::Vector)

Oui, ce sera un RowVector donc vous devriez obtenir un scalaire. (Notez que la transposition ne sera pas récursive).

Désolé pour le commentaire éventuellement tangentiel, mais beaucoup de gens dans ce fil continuent de parler d'un "anneau de scalaires d'un espace vectoriel". Par définition, les scalaires d'un espace vectoriel doivent former un champ, pas n'importe quel anneau. Une structure algébrique qui ressemble beaucoup à un espace vectoriel mais dont les scalaires ne forment qu'un anneau plutôt qu'un champ s'appelle un "module", mais je pense que les modules sont un peu trop ésotériques pour être manipulés dans la Base ... euh. .. module.

Puisque nous prenons en charge les tableaux d'entiers, nous prenons en charge efficacement les modules, pas seulement les espaces vectoriels. Bien sûr, nous pourrions également voir les tableaux d'entiers comme incorporés dans des tableaux à virgule flottante de manière comportementale, ils sont donc une représentation partielle d'un espace vectoriel. Mais nous pouvons également créer des tableaux d' entiers modulaires (par exemple), et avec un module non premier, nous travaillerions avec un anneau qui n'est naturellement intégré dans aucun champ. En bref, nous voulons en fait considérer les modules en général plutôt que simplement les espaces vectoriels, mais je ne pense pas qu'il y ait de différence significative pour nos besoins (nous ne parlons généralement que de + et * ) donc "espace vectoriel" sert de raccourci plus familier pour "module" pour nos besoins.

Oui, Julia supporte (et devrait) certainement les opérations algébriques sur les modules généraux ainsi que sur les vrais espaces vectoriels. Mais si je comprends bien, la philosophie générale de la communauté est que les fonctions dans Base doivent être conçues avec "des routines d'algèbre linéaire générique" ordinaire "à l'esprit - donc, par exemple, les calculs d'algèbre linéaire exact sur des matrices entières don 'n'appartient pas à Base - donc lorsque nous prenons des décisions de conception fondamentales, nous devrions simplement supposer que les scalaires forment un champ. (Bien que, comme vous l'avez dit, en pratique, cela n'a pas vraiment d'importance.)

Postage croisé https://github.com/JuliaLang/julia/pull/23424#issuecomment -346678279

J'apprécie beaucoup les efforts déployés pour faire avancer les numéros 5332 et 20978 que représente cette demande de tirage, et je suis d'accord avec la plupart des plans associés. J'aimerais beaucoup être également d'accord avec les choix de terminologie et l'impact en aval que cette pull request avance, et j'essaie donc régulièrement de me convaincre que ces choix donnent le meilleur ensemble de compromis disponibles.

Chaque fois que j'essaie de me convaincre de cette position, je m'échoue sur le même ensemble de doutes. L'un de ces doutes est la complexité considérable de l'implémentation que ce choix impose à LinAlg : La transposition conflictuelle et le retournement de tableau obligent LinAlg à gérer les deux, nécessitant LinAlg de combinaisons de types dans les opérations courantes.

Pour illustrer, considérons mul(A, B)A et B sont nus, enveloppés par un adjoint, par transposition ou par retournement de tableau Matrix s. Sans confondre transpose et array-flip, A peut être un Matrix , un Matrix enveloppé par un adjoint, ou un Matrix (et de même B ). Donc mul(A, B) doit prendre en charge neuf combinaisons de types. Mais en confondant transpose et array-flip, A peut être un Matrix , un Matrix enveloppé par un adjoint, un Matrix enveloppé par une transposition, ou un tableau- flip-wrapped Matrix (et de même B ). Alors maintenant, mul(A, B) doit prendre en charge seize combinaisons de types.

Ce problème s'aggrave de façon exponentielle avec le nombre d'arguments. Par exemple, sans conflation mul!(C, A, B) besoin de prendre en charge vingt-sept combinaisons de types, alors qu'avec la conflation mul!(C, A, B) faut prendre en charge soixante-quatre combinaisons de types. Et bien sûr, ajouter des types Vector s et non- Matrix matrice / opérateur au mélange complique encore les choses.

Cet effet secondaire semble intéressant d'être pris en compte avant d'aller de l'avant avec ce changement. Meilleur!

Cet article consolide / passe en revue les discussions récentes sur github, slack et triage, et analyse les pistes possibles. (Le successeur spirituel de https://github.com/JuliaLang/julia/issues/20978#issuecomment-315902532 est né de la réflexion sur la façon d'arriver à 1.0.)

Trois opérations sémantiquement distinctes sont en jeu:

  • adjoint (algébrique linéaire, récursif, idéalement paresseux par défaut)
  • transposer (algébrique linéaire, récursif, idéalement paresseux par défaut)
  • array-flip (abstract-array-ic, non récursif, idéalement? paresseux? par défaut)

État de ces opérations sur le maître

  • Adjoint est appelé adjoint / ' (mais est impatient à part les expressions ' -involving spécialement abaissées à A[c|t]_(mul|rdiv|ldiv)_B[c|t][!] appels, ce qui évite les adjoints / transposés intermédiaires désireux) .

  • La transposition s'appelle transpose / .' (avec la même mise en garde que adjoint ).

  • Array-flip est appelé permutedims(C, (2, 1)) pour deux dimensions C et reshape(C, 1, length(C)) pour unidimensionnel C .

Les enjeux pertinents

  1. 5332: L'abaissement spécial à A[c|t]_(mul|rdiv|ldiv)_B[c|t] et la collection combinatoire associée de noms de méthodes devraient disparaître de 1.0. La suppression de cet abaissement spécial / des noms de méthodes associés nécessite un adjoint paresseux et une transposition.

  2. 13171: Adjoint (née ctranspose ) et transposée étaient autrefois confondus avec array-flip via des solutions de secours génériques sans opération comme transpose(x::Any) = x , ctranspose(x::Any) = conj(x) et conj(x::Any) = x . Ces solutions de secours ont fait échouer les opérations impliquant [c]transpose sur certains types numériques définis par l'utilisateur (renvoyer des résultats incorrects) en l'absence [c]transpose spécialisations

  3. 17075, # 17374, # 19205: la suppression des solutions de secours précédentes a rendu le retournement de tableau moins pratique. Et couplé avec (mes excuses, ma faute) des avertissements de dépréciation associés moins que grands, cette suppression (transitoire?) A provoqué des plaintes. Une incantation plus pratique pour array-flip serait bien.

  4. 21037: Supprimer la syntaxe désormais déroutante .' pour 1.0 serait très agréable. L'abaissement spécial mentionné ci-dessus doit être retiré pour permettre ce changement.

Objectifs de conception pour résoudre ces problèmes

Supprimez la réduction spéciale et les noms de méthodes associés, évitez d'introduire des échecs silencieux, fournissez des incantations intuitives et pratiques pour la transposition et le retournement de tableau, et supprimez .' . Réalisez ce qui précède avec un minimum de complexité et de casse supplémentaires.

Propositions de design

Le domaine a été réduit à deux propositions de conception:

  1. Appelez adjoint adjoint , transposez conjadjoint ("conjugué adjoint"), et array-flip transpose . adjoint et conjadjoint vivent dans LinAlg , et transpose vit dans Base .

  2. Appelez adjoint adjoint , transposez transpose , et array-flip flip . adjoint et transpose vivent dans LinAlg et flip vivent dans Base .

À première vue, ces propositions ne semblent que superficiellement différentes. Mais un examen plus approfondi révèle de profondes différences pratiques. En évitant de discuter des mérites superficiels relatifs de ces schémas de dénomination, jetons un coup d'œil à ces différences pratiques profondes.

Différences, vue de haut niveau

  1. Complexité:

    La première proposition, en appelant array-flip transpose , force LinAlg à gérer array-flip en plus de transpose et adjoint. Par conséquent, LinAlg doit considérablement étendre l'ensemble des combinaisons de types prises en charge par les opérations courantes; https://github.com/JuliaLang/julia/pull/23424#issuecomment -346678279 illustre cette complexité supplémentaire à titre d'exemple, et la discussion suivante affirme implicitement l'existence de cette complexité supplémentaire.

    La deuxième proposition nécessite la prise en charge de LinAlg uniquement pour la transposition et l'adjoint, ce que fait maintenant LinAlg .

  2. Rupture:

    La première proposition modifie la sémantique de base des opérations existantes: transpose devient un retournement de tableau plutôt qu'une transposition, et toutes les fonctionnalités liées à transpose doivent changer en conséquence. (Par exemple, toutes les opérations de multiplication et de division gauche / droite dans LinAlg associées au nom transpose nécessiteraient une révision sémantique.) Selon la manière dont ce changement est réalisé, ce changement provoque une rupture silencieuse partout où la sémantique actuelle est invoquée (intentionnellement ou par inadvertance).

    La deuxième proposition conserve la sémantique de base de toutes les opérations existantes.

  3. Couplage:

    La première proposition apporte un concept algébrique linéaire (transposer) en Base , et un concept de tableau abstrait (array-flip) en LinAlg , couplant fortement Base et LinAlg .

    La proposition deux sépare clairement les éléments tableau abstrait et algèbre linéaire, permettant au premier de vivre uniquement en base et le second uniquement en LinAlg , sans nouveau couplage.

  4. Échec silencieux ou bruyant:

    La première proposition conduit à un résultat silencieusement incorrect quand on appelle transpose attendant une sémantique de transposition (mais obtient à la place une sémantique de retournement de tableau).

    L'analogue de la proposition 2 appelle transpose sur un tableau non numérique attendant une sémantique de retournement de tableau. Dans ce cas, transpose peut renvoyer une erreur pointant utilement l'utilisateur vers flip .

  5. .' : L'argument principal pour ne pas déprécier .' est la longueur de transpose . La première proposition remplace .' par les noms transpose et conjadjoint , ce qui n'améliore pas cette situation. En revanche, la deuxième proposition fournit les noms flip et transpose , améliorant ainsi cette situation.

Différences dans les chemins vers 1.0

Que faut-il pour arriver à 1.0 pour chaque proposition? Le chemin vers la version 1.0 est plus simple dans le cadre de la deuxième proposition, alors commençons par là.

Le chemin vers 1.0 sous la proposition deux

  1. Introduisez les types de wrapper paresseux adjoint et transpose dans LinAlg , disons Adjoint et Transpose . Introduisez les méthodes mul[!] / ldiv[!] / rdiv[!] répartissant sur ces types de wrapper et contenant le code des méthodes A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] correspondant. Réimplémentez les dernières méthodes en tant que petits enfants des premières méthodes.

    Cette étape ne rompt rien et permet immédiatement à la fois de supprimer l'abaissement spécial et la dépréciation de .' :

  2. Supprimez l'abaissement spécial qui donne A[c|t]_{mul|ldiv|rdiv}_B[c|t] , mais abaissez simplement ' / .' à Adjoint / Transpose ; les expressions autrefois spécialement abaissées produisant des appels A[c|t]_{mul|ldiv|rdiv}_B[c|t] deviennent alors équivalentes mul / ldiv / rdiv appels. Dépréciez A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] aux méthodes mul[!] / ldiv[!] / rdiv[!] . Obsolète .' .

    Ces étapes pourraient être effectuées en 0.7. Ils ne cassent que deux choses: (1) Le code reposant sur la réduction spéciale pour atteindre les méthodes A[c|t]_{mul|ldiv|rdiv}_B[c|t] pour les types non- Base / LinAlg cassera. Un tel code émettra des MethodError explicites indiquant ce que le nouvel abaissement produit / vers quoi le code cassé doit migrer. (2) Le code reposant sur des ' s / .' isolés se comportant strictement avec empressement se cassera. Le mode d'échec commun doit également être explicite MethodError s. Tout autour, la casse est confinée et bruyante.

    Et c'est tout pour les changements strictement nécessaires pour la version 1.0.

    À ce stade, Adjoint(A) / Transpose(A) donnerait un adjoint et une transposition paresseux, et adjoint(A) / transpose(A) donnerait un adjoint et une transposition impatients. Ces derniers noms pourraient rester indéfiniment ou, si vous le souhaitez, être obsolètes pour une autre orthographe dans 0.7, par exemple eagereval(Adjoint(A)) / eagereval(Transpose(A)) modulo orthographe de eagereval ou eageradjoint(A) / eagertranspose(A) . Dans le cas de la dépréciation, adjoint / transpose pourrait alors être réutilisé dans 1.0 (bien qu'avec Adjoint(A) / Transpose(A) autour, je ne suis pas certain que ce serait nécessaire).

    Finalement...

  3. Introduisez flip et / ou Flip en Base . Étant un ajout de fonctionnalité, ce changement peut se produire dans 1.x si nécessaire.

Le chemin vers 1.0 sous la première proposition

Et la première proposition? Le tableau ci-dessous décrit deux chemins possibles. Le premier chemin consolide les changements en 0.7 mais implique une rupture silencieuse. Le deuxième chemin évite la rupture silencieuse, mais implique plus de changement 0.7-> 1.0. Les deux contours font évoluer continuellement la base de code; des équivalents moins continus pourraient consolider / éviter un certain travail / désabonnement, mais seraient probablement plus difficiles et sujets aux erreurs. Le travail en cours suit apparemment le premier chemin.

Premier chemin sous la proposition 1 (avec rupture silencieuse)
  1. Changez la sémantique de transpose de transpose en array-flip, et nécessairement aussi la sémantique de toutes les fonctionnalités liées à transpose . Par exemple, toutes les méthodes A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] commençant At_... ou se terminant ..._Bt[!] potentiellement besoin d'une révision sémantique. Il faut également changer par exemple les définitions et le comportement de Symmetric / issymmetric . Déplacez transpose lui-même dans Base .

    Ces changements seraient silencieusement et largement interrompus.

  2. Introduisez conjadjoint dans LinAlg . Cette étape nécessite de restaurer toutes les méthodes touchées à l'étape précédente, mais dans leur forme sémantique d'origine, et maintenant avec des noms différents associés à conjadjoint (disons Aca_... et ..._Bca[!] noms) . Nécessite également l'ajout de méthodes pour les combinaisons de types supplémentaires qui prennent simultanément en charge le retournement de tableau (maintenant transpose ), la transposition (maintenant conjadjoint ) et l'adjoint dans LinAlg nécessite (par exemple le ca variantes parmi A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] ).

  3. Introduisez l'adjoint paresseux et transposez (appelé conjadjoint ) dans LinAlg , dites Adjoint et ConjAdjoint . Introduisez un type de wrapper paresseux array-flip (appelé transpose ) dans Base , disons Transpose . Introduisez les méthodes mul[!] / ldiv[!] / rdiv[!] répartissant sur ces types de wrapper et contenant le code des méthodes A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] correspondant. Réimplémentez les dernières méthodes en tant que petits enfants des premières méthodes.

  4. Supprimez l'abaissement spécial qui donne A[c|t]_{mul|ldiv|rdiv}_B[c|t] , mais abaissez simplement ' / .' à Adjoint / Transpose ; les expressions autrefois spécialement abaissées produisant des appels A[c|t]_{mul|ldiv|rdiv}_B[c|t] deviennent alors équivalentes mul / ldiv / rdiv appels (mais rappelez-vous que la sémantique aura silencieusement changé). Obsolète A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] aux méthodes mul[!] / ldiv[!] / rdiv[!] . De même, supprimez les méthodes Aca_... / ...Bca[!] récemment introduites en faveur des équivalents mul[!] / ldiv[!] / rdiv[!] . Obsolète .' .

Ces changements devraient entrer en 0.7. Les modifications sémantiques des opérations existantes entraîneraient une rupture large et silencieuse. Le retrait spécial de l'abaissement produirait le même bris confiné et bruyant décrit ci-dessus.

À ce stade, Adjoint(A) / Transpose(A) / ConjAdjoint(A) donnerait respectivement un adjoint paresseux, un tableau-flip et une transposition, et adjoint(A) / transpose(A) / conjadjoint(A) donnerait respectivement un adjoint impatient, un retournement de tableau et une transposition. Ces derniers noms pourraient rester indéfiniment ou, si vous le souhaitez, être déconseillés à une autre orthographe également en 0.7 (réf. Ci-dessus).

On pourrait introduire ConjAdjoint plus tôt dans ce processus pour consolider / éviter certains travaux / churn, bien que cette approche soit plus difficile et sujette aux erreurs.

Deuxième chemin sous la première proposition (éviter la rupture silencieuse)
  1. Introduisez impatient conjadjoint dans LinAlg . Migrez toutes les fonctionnalités et méthodes actuellement associées à transpose (y compris par exemple les méthodes A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] impliquant t ) vers conjadjoint et les noms dérivés. Faites en sorte que tous les noms relatifs à transpose soient des enfants courts des nouveaux équivalents conjadjoint . Désapprouver tous les transpose concernant la PI noms à conjadjoint équivalents.

  2. Introduisez les types de wrapper lazy adjoint et transpose (appelés conjadjoint ) dans LinAlg , disons Adjoint et ConjAdjoint . Introduisez les méthodes mul[!] / ldiv[!] / rdiv[!] répartissant sur ces types de wrapper et contenant le code des méthodes A[c|ca]_{mul|ldiv|rdiv}_B[c|ca][!] correspondant. Réimplémentez les dernières méthodes en tant que petits enfants des premières méthodes.

  3. Introduisez un type de wrapper à retournement de tableau paresseux dans Base , appelé Transpose (quelque peu déroutant, car simultanément transpose est déconseillé à conjadjoint avec une sémantique de transposition). Ajoutez toutes les méthodes générales nécessaires pour que array-flip ( Transpose ) fonctionne à Base . Ensuite, ajoutez des méthodes à LinAlg pour toutes les combinaisons de types supplémentaires qui supportent simultanément le retournement de tableau (maintenant Transpose , mais pas transpose ), transposez (maintenant conjadjoint et ConjAdjoint ), et adjoint dans LinAlg requiert (par exemple, les mul[!] / rdiv[!] / ldiv[!] équivalents de A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] qui n'existent pas actuellement).

  4. Supprimez l'abaissement spécial qui donne A[c|t]_{mul|ldiv|rdiv}_B[c|t] , mais abaissez simplement ' / .' à Adjoint / Transpose ; les expressions précédemment spécialement abaissées produisant des appels A[c|t]_{mul|ldiv|rdiv}_B[c|t] deviennent alors équivalentes mul / ldiv / rdiv appels. Obsolète A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] aux méthodes mul[!] / ldiv[!] / rdiv[!] . Obsolète .' .

Les changements précédents devraient aller dans 0.7. Aucune rupture silencieuse, seulement la rupture bruyante de la suppression spéciale de l'abaissement comme décrit ci-dessus.

À ce stade, Adjoint(A) / Transpose(A) / ConjAdjoint(A) donnerait respectivement un adjoint paresseux, un retournement de tableau et une transposition. adjoint(A) / conjadjoint(A) donnerait respectivement un adjoint et une transposition enthousiastes. transpose(A) deviendrait conjadjoint ; transpose pourrait être réutilisé en 1.0 si vous le souhaitez. adjoint pourrait rester indéfiniment ou être obsolète au profit d'une autre orthographe en 0.7. Une autre façon d'épeler conjadjoint pourrait aller directement dans 0.7.

On pourrait un peu consolider les étapes 1 et 2, en évitant un certain travail / désabonnement, bien que cette approche soit plus difficile et sujette aux erreurs.

#

Merci d'avoir lu!

Merci pour l'excellente rédaction. Je suis généralement également d'accord avec la proposition 2, avec comme raison supplémentaire que conjugate adjoint n'est pas du tout une terminologie standard et je trouve personnellement cela très déroutant (puisque adjoint est parfois utilisé comme terminologie pour transposer simple dans certaines branches des mathématiques, et l'adjectif supplémentaire conjugué semble impliquer que la conjugaison a lieu).

J'ai essayé de lire les discussions élaborées ci-dessus, mais je n'ai pas réussi à voir à quel point il avait été décidé / motivé qu'un transpose mathématique devait être récursif. Cela semblait découler d'une discussion privée (quelque part entre le 14 et le 18 juillet), après quoi soudainement une transposition mathématique et structurelle a été introduite.
@ Sacha0 le décrit comme

Comment cela s'est produit: Auparavant, «transposition structurelle» était confondue avec «adjoint mathématique» / «transposition mathématique»

Je suis d'accord que la transposition (structurelle) a été confondue avec «adjoint», mais ici «transposition mathématique» apparaît de nulle part? Par souci d'exhaustivité / d'autonomie, ce concept et pourquoi il devrait être récursif pourraient-ils être brièvement réitérés / résumés?

Par rapport à cela, je pense que personne n'utilise vraiment transpose au sens mathématique abstrait, comme une opération sur des cartes linéaires , pour la simple raison que la transposition d'une matrice serait un objet qui n'agit pas sur vecteurs mais sur RowVector s, c'est-à-dire qu'il mapperait RowVector à RowVector . Étant donné le manque d'argumentation pour la transposition récursive, je ne vois vraiment aucune distinction entre la transposition matricielle simple qui est généralement définie comme un concept (dépendant de la base), et l'opération flip nouvellement proposée.

Merci @ Sacha0 pour la bonne rédaction. Je préfère la proposition 2 (Call adjoint adjoint , transposer transpose , et array-flip flip . adjoint et transpose vivre dans LinAlg et flip vit dans Base .). Pour moi, cela semble donner le meilleur résultat final (qui devrait être la principale préoccupation), et permet également un moyen plus propre pour la v1.0. Je suis d'accord avec vos points ci-dessus, je ne les répéterai donc pas, mais voici quelques commentaires supplémentaires.

Un argument en faveur de transpose non récursif est que la syntaxe .' est très pratique et peut donc être utilisée pour par exemple Matrix{String} . En supposant que la syntaxe disparaît (# 21037) et qu'il faut utiliser transpose(A) au lieu de A.' , alors je pense qu'une fonction flip serait un ajout bienvenu (plus court et plus clair que transpose(A) , et certainement mieux que permutedims(A, (2,1)) ).

Le découplage entre LinAlg et Base que donne la proposition 2 est également très agréable. Non seulement puisque LinAlg doit seulement se soucier de la façon de gérer Transpose et Adjoint , mais aussi qu'il est clair que nous considérons transpose et adjoint pour être LinAlg opérations, et flip pour être une chose AbstractMatrix générique qui ne fait que retourner les dimensions d'une matrice.

Enfin, je n'ai pas vu beaucoup de plaintes à propos de transpose(::Matrix{String}) etc. ne fonctionnant pas. Est-ce vraiment un cas d'utilisation courant? Dans la plupart des cas, il devrait être préférable de construire la matrice inversée depuis le début de toute façon.

Ce schéma de dénomination (proposition 2) pourrait en effet être meilleur. Adjoint et transposer sont des termes assez classiques en algèbre linéaire, et c'est moins cassant ...

Mais je n'ai pas vu à quel point il a été décidé / motivé qu'une transposition mathématique soit récursive

@Jutho C'est le même argument que l'adjoint (l'adjoint d'un nombre complexe est son conjugué (puisque les nombres complexes sont des espaces vectoriels valides de rang un et des opérateurs linéaires, le produit interne et les concepts adjoints s'appliquent), et l'adjoint d'une matrice de blocs est récursif ...). Les gens peuvent vouloir faire de l'algèbre linéaire avec des matrices imbriquées et réelles, par exemple. Je vois encore beaucoup de code utilisant .' pour "l'adjoint d'une matrice ou d'un vecteur réel", et j'ai aussi l'habitude de le faire. Ici, ' serait non seulement valide mais aussi plus générique (fonctionnant pour des tableaux qui pourraient être complexes et / ou imbriqués). Une utilisation plus authentique de la transposition récursive sur des matrices complexes imbriquées se produit lorsque vos équations vous donnent conj(adjoint(a)) , ce qui est relativement plus rare mais se produit quand même (c'est assez rare pour que je sois heureux s'il n'y a pas de fonction associée - dans le discussions ci-dessus et ailleurs diverses personnes ont commencé à l'appeler des choses différentes comme "transposition mathématique", j'ai choisi conjadoint , mais rien de tout cela n'est une excellente terminologie IMO, sauf peut-être transpose lui-même). En algèbre linéaire, l'opération non récursive qui réorganise les blocs d'une matrice de blocs mais ne fait rien aux blocs eux-mêmes n'apparaît généralement pas.

Enfin, je n'ai pas vu beaucoup de plaintes à propos de transpose(::Matrix{String}) etc. ne fonctionnant pas. Est-ce vraiment un cas d'utilisation courant? Dans la plupart des cas, il devrait être préférable de construire la matrice inversée depuis le début de toute façon.

Je pense que ce genre de chose est tout à fait souhaitable avec les opérations de radiodiffusion de Julia. Par exemple, il est assez courant de vouloir prendre le produit externe (cartésien) de certaines données et peut-être de le mapper via une fonction. Donc, au lieu de quelque chose comme map(f, product(a, b)) nous pouvons utiliser broadcast(f, a, transpose(b)) ou simplement f.(a, b.') . C'est beaucoup de puissance en quelques personnages.

Mes pensées: pour éviter d'ajouter encore plus de noms de fonctions à cet espace (ie flip ), je me demande si nous pourrions avoir une valeur par défaut pour la permutation dans permutedims(a) = permutedims(a, (2,1)) . Malheureusement, ce n'est pas aussi court que flip , mais nettement moins odieux que le formulaire complet permutedims(a, (2,1)) (et a pour effet secondaire d'enseigner aux utilisateurs la fonction plus générale).

( @ Sacha0 Merci beaucoup pour votre écriture ici. FYI dans le choix entre Adjoint et Transpose wrappers, ou RowVector + MappedArray + peu importe si matrices comme prévu précédemment (peut-être juste PermutedDimsArray ), j'ai toujours tendance à privilégier ce dernier ...)

Une utilisation plus authentique de la transposition récursive sur des matrices complexes imbriquées se produit lorsque vos équations vous donnent conj(adjoint(a))

Une fois que vous avez obtenu conj(adjoint(a)) dans vos équations, comment savez-vous que c'est en fait conj que vous vouliez appliquer à vos entrées de matrice et peut-être pas adjoint , c'est-à-dire peut-être que vous voulez réellement adjoint.(adjoint(a)) .

Cela me fera probablement bannir / expulser, mais je ne suis pas un grand fan de toute l'idée récursive. Je n'y suis pas forcément opposé non plus, je ne pense tout simplement pas qu'il y ait une raison mathématiquement rigoureuse à cela, car les vecteurs et les opérateurs / matrices ne sont pas définis de manière récursive, ils sont définis sur des champs de scalaires (et par extension des anneaux si vous voulez pour travailler également avec des modules au lieu d'espaces vectoriels). Et donc Base.LinAlg devrait fonctionner dans ce cas. L'argument principal

L'adjoint d'un vecteur doit être un opérateur linéaire le mappant à un scalaire. Autrement dit, a '* a doit être un scalaire si a est un vecteur.

est incompatible avec le fonctionnement de Julia Base: Faites a=[rand(2,2),rand(2,2)] et observez a'*a . Ce n'est pas un scalaire. Alors a -il maintenant une matrice, parce que c'est un vecteur rempli de matrices? Ce qui précède n'est qu'un scalaire si en effet vous avez l'intention d'utiliser l'anneau de matrices 2x2 comme scalaires sur lesquels vous avez défini un module. Mais dans ces contextes, il n'est pas clair pour moi que le résultat ci-dessus soit celui visé. Le produit interne euclidien est de toute façon rarement utilisé dans le contexte des modules, etc., donc je ne pense pas que Base devrait même se soucier de définir un comportement correct pour cela.

Donc vraiment, même si la déclaration ci-dessus pourrait être une tentative d'étendre la motivation au-delà de l'interprétation des matrices remplies de matrices comme des matrices de blocs, en fin de compte, je pense que l'interprétation de la matrice de blocs était la seule vraie motivation pour faire adjoint et maintenant même transpose (qui n'est jamais utilisé dans le sens mathématique mais toujours dans le sens des index de matrice de retournement) récursif en premier lieu. Si vous définissez vous-même un nouveau type de champ scalaire, c'est juste un fardeau étrange que vous devez définir adjoint et transpose pour lui en plus de conj , avant de pouvoir l'utiliser dans une matrice.

Alors pourquoi, avec un système de types aussi puissant que dans Julia, ne pas simplement avoir un type dédié pour les matrices de blocs. Et ainsi, jouant l'avocat du diable:

  • Combien de personnes utilisent / se fient réellement au comportement récursif actuel de l'adjoint pour tout travail pratique?
  • Existe-t-il d'autres langages utilisant une telle approche récursive? (Matlab, utilisant des cellules de matrices, ne le fait pas)

Comme dit, je ne suis pas forcément opposé, je remets simplement en question la validité (même après avoir lu toute la discussion ci-dessus), en particulier pour le cas transpose .

Cela me fera probablement bannir / expulser

Je me rends compte que c'est une blague, mais avoir une opinion impopulaire ne fera absolument pas interdire personne. Seuls les mauvais comportements qui durent pendant une longue période peuvent conduire à une interdiction. Même dans ce cas, l'interdiction n'est pas permanente .

Je ne pense tout simplement pas qu'il y ait une raison mathématiquement rigoureuse à cela, car les vecteurs et les opérateurs / matrices ne sont pas définis de manière récursive, ils sont définis sur des champs de scalaires (et par extension des anneaux si vous souhaitez également travailler avec des modules au lieu d'espaces vectoriels ).

Cette déclaration n'a aucun sens pour moi. Un vecteur de vecteurs, ou un vecteur de fonctions, ou un vecteur de matrices, forme toujours un espace vectoriel (sur un champ scalaire sous-jacent) avec + et * scalar définis récursivement, et de même il se forme un espace de produit intérieur avec le produit intérieur défini de manière récursive. L'idée que les vecteurs ne peuvent être que des «colonnes de scalaires» défie à la fois les définitions et l'usage courant en mathématiques, en physique et en ingénierie.

Faites a=[rand(2,2),rand(2,2)] et observez a'*a . Ce n'est pas un scalaire.

Dans ce cas, "l'anneau scalaire" de a est l'anneau de matrices 2x2. C'est donc un problème linguistique, pas un problème avec le fonctionnement de l'adjoint.

Se disputer sur la façon dont a'*a devrait fonctionner à partir de la façon dont cela fonctionne actuellement (ou ne fonctionne pas) dans Julia semble plutôt circulaire.

Si vous définissez vous-même un nouveau type de champ scalaire, c'est juste un fardeau étrange que vous devez définir adjoint et transpose pour lui en plus de conj , avant de pouvoir l'utiliser dans une matrice

Je pense que ce fardeau est plus esthétique que pratique. Si vous êtes d'accord pour en faire un sous-type de Number vous n'avez rien à définir et même si vous pensez que Number n'est pas la bonne chose pour vous, les définitions sont si triviales que les ajouter n'est guère un fardeau.

Alors pourquoi, avec un système de types aussi puissant que dans Julia, ne pas simplement avoir un type dédié pour les matrices de blocs.

https://github.com/KristofferC/BlockArrays.jl/ Bienvenue aux contributeurs :)

Mes excuses pour la blague «interdiction». Ce sera ma dernière réaction afin de ne pas faire dérailler davantage cette discussion. Il est clair que l'adjoint récursif (mais peut-être pas transposé) est réglé, et je n'essaye pas d'annuler cette décision. J'essaie simplement de souligner certaines incohérences.

@StefanKarpinski : L'exemple de l'anneau scalaire est exactement en contradiction avec la récursivité: les matrices 2x2 sont-elles elles-mêmes les scalaires, ou la définition est-elle récursive et le sous-jacent Number le scalaire?

Dans le premier cas, vous avez en fait un module au lieu d'un espace vectoriel et cela dépend probablement du cas d'utilisation si vous voulez conj ou adjoint sur ces 'scalaires', si vous le feriez même en utilisant des produits intérieurs et des joints du tout.

Je suis d'accord avec cette dernière définition. J'utilise des éléments arbitraires dans des sous-espaces invariants de groupe de produits tensoriels gradués d'espaces super vectoriels dans mon travail, donc je ne suis pas vraiment confiné aux colonnes de nombres dans ma définition des vecteurs. Mais les vecteurs de matrices ne sont-ils que de purs vecteurs ou devraient-ils également hériter d'un comportement de matrice. Dans le premier cas, dot(v,w) devrait produire un scalaire, probablement en appelant récursivement vecdot sur ses éléments. Dans ce dernier cas, au moins vecdot(v,w) devrait produire un scalaire en agissant récursivement. Ce serait donc un correctif minimal pour être cohérent avec l'adjoint récursif.

Mais il semble plus simple de ne pas avoir de transpose récursif et de l'autoriser également à être utilisé pour des applications non mathématiques, car je pense que même dans les équations matricielles typiques, cela a très peu à voir avec la transposition mathématique réelle d'une carte linéaire.

Je pense que dot devrait appeler dot récursivement, pas vecdot , donc cela ferait une erreur pour un vecteur de matrices puisque nous ne définissons pas de produit interne canonique pour les matrices.

Je suis toujours surpris que la transposition soit récursive .. et je ne peux pas imaginer que personne ne le soit. (Je suppose que les vues sur l'article de Wikipédia "transposer une carte linéaire" ont au moins triplé à cause de cette discussion.)

J'ai moi aussi souvent trouvé les définitions récursives troublantes, et j'ai généralement partagé le point de vue exprimé ci-dessus par @Jutho (mais là encore, nous avons eu une formation très similaire).

Je pense avoir compris l'incohérence qui me dérangeait ici - nous continuons à utiliser des anneaux et des champs tels que l'incorporation de matrice 2x2 de nombres complexes à titre d'exemple ici, mais cela ne fonctionne pas réellement ni ne motive la transposition récursive (et adjoint ).

Laisse-moi expliquer. Les choses avec lesquelles je suis globalement d'accord

  1. Les scalaires sont des opérateurs linéaires valides de rang 1. Étant donné le puissant système de types de Julia, il n'y a aucune raison pour que des concepts LinAlg comme adjoint ne s'appliquent pas, par exemple nous devrions définir adjoint(z::Complex) = conj(z) . (Au-delà des vecteurs et des matrices de scalaires, les concepts LinAlg peuvent également être étendus (par les utilisateurs, je présume) à d'autres objets - @stevengj a mentionné par exemple les espaces vectoriels de taille infinie (espaces de Hilbert)).
  2. Nous devrions pouvoir en quelque sorte traiter des scalaires avec des représentations différentes. L'exemple prototypique ici est un nombre complexe z = x + y*im peut être modélisé comme Z = x*[1 0; 0 1] + y*[0 1; -1 0] et les opérations + , - , * , / et \ sont préservés dans cet isomorphisme. (Notez cependant que conj(z) va à adjoint(Z) / transpose(Z) / flip(Z) - plus à ce sujet plus tard).
  3. Les matrices de blocs devraient être possibles d'une manière ou d'une autre (l'approche actuelle repose sur la récursive adjoint par défaut, etc.).

Il semble raisonnable que Base.LinAlg soit compatible avec 1 et 2, mais IMO 3 ne devrait être fait qu'en Base si cela s'intègre naturellement (sinon, j'aurais tendance à m'en remettre à des packages externes comme https: / /github.com/KristofferC/BlockArrays.jl).

Je réalise maintenant que nous avons confondu 2 et 3 et cela conduit à des incohérences ( @Jutho l' a adjoint opérations et autres par des matrices de bloc ne nous donne pas 2. La façon la plus simple est par exemple. Définissons une matrice 2x2 de nombres complexes comme m = [z1 z2; z3 z4] , la représentation isomorphe M = [Z1 Z2; Z3 Z4] , et une matrice de blocs 2x2 standard b = [m1 m2; m3 m4]m1 etc sont carrés de même taille matrices de Number . Les réponses sémantiquement correctes pour les opérations courantes sont répertoriées ci-dessous:

| opération | z | Z | m | M | b |
| - | - | - | - | - | - |
| + , - , * | récursif | récursif | récursif | récursif | récursif |
| conj | conj(z) | Z' ou Z.' ou flip(Z) | conj.(m) | adjoint.(M) (ou transpose.(M) ) | conj.(b) |
| adjoint | conj(z) | Z' ou Z.' ou flip(Z) | flip(conj.(m)) ou récursif | flip(transpose.(m)) ou récursif | récursif |
| trace | z | Z | z1 + z4 | Z1 + Z4 | trace(m1) + trace(m4) |
| det | z | Z | z1*z4 - z2*z3 | Z1*Z3 - Z2*Z3 | det(m1) * det(m4 - m2*inv(m1)*m3) (si m1 inversible, voir par exemple Wikipedia ) |

Compte tenu des opérations comme trace et det qui renvoient des scalaires, je pense qu'il est assez clair que notre système de type Julia pour LinAlg ne pourrait pas gérer notre intégration de matrice 2x2 de Complex de manière "automatique", en déduisant ce que nous entendons par "scalaire" à un moment donné. Un exemple frappant est trace(Z)Z = [1 0; 0 1] est 2 , alors que c'est notre représentation de 1 . De même pour rank(Z) .

Une façon de récupérer de la cohérence est de définir explicitement notre représentation 2x2 comme scalaire, par exemple en sous-typant Number comme ci-dessous:

struct CNumber{T <: Real} <: Number
    m::Matrix{T}
end
CNumber(x::Real, y::Real) = CNumber([x y; -y x])

+(c1::CNumber, c2::CNumber) = CNumber(c1.m + c2.m)
-(c1::CNumber, c2::CNumber) = CNumber(c1.m - c2.m)
*(c1::CNumber, c2::CNumber) = CNumber(c1.m * c2.m)
/(c1::CNumber, c2::CNumber) = CNumber(c1.m / c2.m)
\(c1::CNumber, c2::CNumber) = CNumber(c1.m \ c2.m)
conj(c::CNumber) = CNumber(transpose(c.m))
zero(c::CNumber{T}) where {T} = CNumber(zero(T), zero(T))
one(c::CNumber{T}) where {T} = CNumber(one(T), one(T))

Avec ces définitions, les méthodes génériques LinAlg fonctionneront probablement très bien.

La conclusion que j'en tire: récursive adjoint est une commodité pour les matrices de blocs et les vecteurs de vecteurs. Cela ne doit Z ci-dessus. Je considère que c'est notre choix que nous prenions en charge les matrices de blocs ou non par défaut, avec les avantages et les inconvénients suivants:

Avantages

  • Commodité pour les utilisateurs de la baie de blocs
  • Les vecteurs de vecteurs sont des espaces vectoriels valides, et les matrices de blocs sont des opérateurs linéaires valides, sous + , * , conj , etc. S'il est possible d'en faire un isomorphisme naturel (contrairement à l'exemple Z ci-dessus, qui nécessite CNumber ), alors pourquoi pas?

Les inconvénients

  • Complexité supplémentaire significative à notre implémentation de LinAlg (qui pourrait potentiellement vivre dans un autre package).
  • C'est un peu difficile (mais pas impossible) de supporter des choses comme eig(block_matrix) . Si nous disons que LinAlg prend en charge eig et LinAlg prend en charge les matrices de blocs, alors je considère cela comme un bogue jusqu'à ce qu'il soit corrigé. Compte tenu de la grande quantité de fonctionnalités fournies par LinAlg il est difficile de voir que nous serons un jour «finis».
  • Inconvénient des utilisateurs de données souhaitant utiliser des opérations comme transpose de manière non récursive,

Pour moi, la question est - choisissons-nous de dire que par défaut LinAlg s'attend à ce que les éléments de AbstractArray s soient "scalaires" (sous-types ou types de canard de Number ) et délimiter la complexité des tableaux de blocs vers des packages externes? Ou acceptons-nous la complexité de Base et LinAlg ?

Le plan jusqu'à il y a un jour était le dernier.

@ Sacha0 J'ai essayé de terminer le travail RowVector nécessaire pour prendre en charge les changements ici (les plus anciens: non récursifs transpose , RowVector chaînes de support et autres données) et maintenant je me demande ce que vous aviez en tête en termes de design.

D'après les discussions récentes, je vois ces cas avec une estimation de ce qui pourrait être souhaité

| | Vecteur | Matrix |
| - | - | - |
| adjoint | RowVector avec récursif adjoint | AdjointMatrix ou TransposedMatrix avec récursif adjoint |
| transpose | RowVector avec récursif transpose | TransposeMatrix avec récursif transpose |
| flip | une copie ou PermutedDimsArray ? | une copie ou PermutedDimsArray ? |
| conj de AbstractArray | paresseux ou impatient? | paresseux ou impatient? |
| conj sur RowVector ou TransposedMatrix | paresseux | paresseux |

(Les tentatives ci-dessus pour séparer les préoccupations d'algèbre linéaire de la permutation des dimensions des tableaux de données.)

Alors quelques questions de base pour me décoller:

  • Faisons-nous des transpose récursifs ou pas? Et pour adjoint ?
  • Si tel est le cas, continuerons-nous à supposer conj(transpose(array)) == adjoint(array) ?
  • Il semble qu'au moins certaines conjugaisons complexes seront paresseuses, pour prendre en charge par exemple toutes les opérations BLAS actuelles sans copies supplémentaires. Rendons-nous conj paresseux pour tous les tableaux?
  • Si nous introduisons flip , est-ce paresseux ou impatient?

Pour info, j'ai essayé une approche à faible friction pour "inverser" les dimensions d'une matrice à # 24839, en utilisant une forme plus courte pour permutedims .

Je suis fortement en faveur de la proposition 2 de @ Sacha0 . Nous n'avons pas entièrement trié les fondements théoriques de l'adjoint récursif vs non récursif, mais indépendamment de cela: comment il est maintenant s'est avéré utile, du moins pour moi , et un changement de dernière minute loin de cela est peut-être mal conseillé. La transposition doit suivre adjoint (= conj∘adjoint ) à cet égard, si cela est nécessaire.

FWIW, Mathematica ne fait pas de Transpose récursif ni ConjugateTranspose :

untitled

J'ai essayé de lire les discussions élaborées ci-dessus, mais je n'ai pas vu à quel point il était décidé / motivé qu'une transposition mathématique devait être récursive. [...] Par souci d'exhaustivité / d'autonomie, ce concept et pourquoi il devrait être récursif pourraient-ils être brièvement réitérés / résumés?

Je comprends et apprécie ce sentiment et je voudrais y répondre dans la mesure où le temps le permet. Expliquer de manière accessible et lucide pourquoi l'adjoint et la transposition devraient être récursifs par définition est malheureusement au mieux difficile et probablement pas possible en bref. Des écrits cohérents et complets comme celui ci-dessus prennent énormément de temps à construire. Le temps étant court, au cours de la journée, j'essaierai plutôt de résoudre une partie de la confusion dans les quelques articles précédents en répondant à des points / questions particuliers; s'il vous plaît soyez avec moi pendant que je le fais au coup par coup :). Meilleur!

expliquant pourquoi adjoint ... devrait être récursif par définition ...

Tout le monde qui a contribué à cette discussion est d'accord pour dire que adjoint(A) devrait être récursif. Le seul point de litige est de savoir si transpose(A) doit également être récursif (et si une nouvelle fonction flip(A) doit être introduite pour une transposition non récurrente).

Je suis quasiment tout le monde qui a contribué à cette discussion est d'accord pour dire que l'adjoint (A) devrait être récursif.

Voir, par exemple, https://github.com/JuliaLang/julia/issues/20978#issuecomment-347777577 et commentaires précédents :). Meilleur!

L'argument pour l'adjoint récursif est assez basique pour les définitions. dot(x,y) doit être un produit interne, c'est-à-dire produire un scalaire, et la définition de adjoint est que dot(A'*x, y) == dot(x, A*y) . La récursivité (pour dot et adjoint ) découle de ces deux propriétés.

(La relation avec les produits scalaires est la raison pour laquelle l'adjoint et la transposition sont des opérations importantes en algèbre linéaire. C'est pourquoi nous avons un nom pour eux, et pas pour d'autres opérations comme la rotation des matrices de 90 degrés ( rot90 dans Matlab ).)

Notez qu'un problème avec l'utilisation de flip pour une transposition non récursive est que Matlab et Numpy utilisent flip pour ce que nous appelons flipdim .

Je pense que personne n'utilise vraiment la transposition au sens mathématique abstrait, comme opération sur des cartes linéaires, pour la simple raison que la transposition d'une matrice serait un objet qui n'agirait pas sur des vecteurs mais sur des RowVectors, c'est-à-dire qu'elle mapperait RowVector à RowVector.

Mais il semble plus simple de ne pas avoir de transposée récursive et de lui permettre également d'être utilisé pour des applications non mathématiques, car je pense que même dans les équations matricielles typiques, cela a très peu à voir avec la transposition mathématique réelle d'une carte linéaire.

transposer (qui n'est jamais utilisé dans le sens mathématique mais toujours dans le sens des index flip matrix)

Cela semblera un peu détourné, alors soyez patient :).

Le adjoint Julia se réfère spécifiquement à l' adjoint hermitien . En général, pour U et V des espaces vectoriels normés complets (espaces de Banach) avec des espaces doubles respectifs U * et V , et pour l'application linéaire A: U -> V, alors l'adjoint de A, typiquement noté A , est une application linéaire A *: V * -> U * . Autrement dit, en général l'adjoint est une application linéaire entre des espaces doubles, tout comme en général la transposée A ^ t est une application linéaire entre des espaces doubles comme mentionné ci-dessus. Alors, comment concilier ces définitions avec la notion familière d'adjoint hermitien? :)

La réponse réside dans la structure supplémentaire que possèdent les espaces dans lesquels on travaille généralement, à savoir les espaces de produits intérieurs complets (espaces de Hilbert). Un produit interne induit une norme, donc un espace de produit interne complet (Hilbert) est un espace normé complet (Banach), et soutient ainsi le concept d'adjoint (hermitien). Voici la clé: ayant un produit interne plutôt qu'une simple norme, l'un des plus beaux théorèmes de l'algèbre linéaire s'applique, à savoir le théorème de représentation de Riesz. En un mot, le théorème de représentation de Riesz déclare qu'un espace de Hilbert est naturellement isomorphe à son espace dual. Par conséquent, lorsque nous travaillons avec des espaces de Hilbert, nous identifions généralement les espaces et leurs duaux et abandonnons la distinction. Faire cette identification est la façon dont vous arrivez à la notion familière d'adjoint hermitien comme A *: V -> U plutôt que A *: V * -> U * .

Et la même identification est généralement faite pour la transposition lorsqu'on considère les espaces de Hilbert, de telle sorte qu'également A ^ t: V -> U, ce qui donne la notion familière de transposition. Donc pour clarifier, oui, la notion commune de transposée est la notion générale de transposée appliquée au cadre le plus familier (Hilbert), tout comme la notion commune d'adjoint hermitien est la notion générale d'adjoint appliquée à ce cadre. Meilleur!

Toutes mes excuses pour avoir battu un cheval mort, mais j'ai pensé résumer brièvement les problèmes de confusion mathématique d'une manière différente. Le principal point de désaccord est de savoir comment interpréter mathématiquement un objet Vector{T} quand T n'est pas simplement un sous-type de Number sans sous-structure de type tableau.

Une école de pensée accepte l' affirmation de

un vecteur de vecteurs sur un anneau R est mieux compris comme un espace à somme directe, qui est également un espace vectoriel sur R.

Modulo certaines subtilités concernant les espaces vectoriels de dimension infinie, etc., cela signifie simplement que nous devrions considérer les vecteurs de vecteurs comme des vecteurs de blocs. Donc un Vector{Vector{T<:Number}} devrait être mentalement "aplati" en un simple Vector{T} . Au sein de ce paradigme, les opérateurs linéaires qui sont représentés comme des matrices de matrices devraient également être considérées comme des matrices de blocs, et adjoint devrait certainement être récursive, tout comme transpose si nous utilisons le mot dans la sens mathématique . S'il vous plaît, corrigez-moi si je me trompe, mais je crois que les gens qui adoptent ce point de vue pensent que quelque chose comme Vector{Matrix{T}} n'a pas d'interprétation assez naturelle que nous devrions concevoir pour cela. (En particulier, nous ne devrions pas simplement supposer que l'intérieur Matrix est une représentation matricielle d'un nombre complexe, car comme @stevengj l'a dit,

Si vous représentez des nombres complexes par des matrices 2x2, vous avez un espace vectoriel complexifié différent.

)

L'autre école de pensée est qu'un Vector{T} doit toujours être pensé comme une représentation d'un vecteur dans un espace vectoriel abstrait sur des scalaires (au sens de l'algèbre linéaire du mot) de type T , quel que soit le type T . Dans ce paradigme, un Vector{Vector{T'}} ne doit pas être considéré comme un élément d'un espace de somme directe, mais plutôt comme un vecteur sur les scalaires Vector{T'} . Dans ce cas, transpose(Matrix{T}) ne doit T forment un anneau valide de scalaires, il doit y avoir une notion bien définie d'addition (commutative) et de multiplication. Pour un vecteur comme Vector{Vector{T'}} , nous aurions besoin d'une règle pour multiplier deux "scalaires" Vector{T'} en un autre Vector{T'} . Bien que l'on puisse certainement trouver une telle règle (par exemple, la multiplication élémentaire, qui résume le problème à la façon de multiplier T' ), il n'y a pas de moyen naturel universel de le faire. Un autre problème est de savoir comment adjoint fonctionnerait avec cette interprétation. L' adjoint hermitien n'est défini que sur des opérateurs linéaires sur un espace de Hilbert, dont le champ scalaire par définition doit être soit des nombres réels, soit des nombres complexes. Donc, si nous voulons appliquer adjoint à une matrice Matrix{T} , nous devons supposer que T est une représentation des nombres complexes (ou des nombres réels, mais je vais juste m'en tenir avec complexe car ce cas est plus subtil). Dans cette interprétation, adjoint ne devrait conjugate . Mais il y a plus de problèmes ici, car l'action correcte de conjugate(T) dépend de la nature de la représentation. Si c'est la représentation 2x2 décrite sur Wikipédia, alors conjugate devrait retourner la matrice. Mais pour les raisons décrites ci-dessus, conjugate ne devrait certainement pas toujours retourner les matrices. Cette approche serait donc un peu compliquée à mettre en œuvre.

Voici mes propres pensées: il n'y a pas de réponse "objectivement correcte" pour savoir si transpose doit être récursif lorsqu'il est appliqué à des tableaux dont les éléments ont une sous-structure encore plus compliquée. Cela dépend de la façon dont l'utilisateur choisit exactement pour représenter sa structure algébrique abstraite dans Julia. Néanmoins, supporter des anneaux complètement arbitraires de scalaires semble être très difficile, donc je pense que pour des raisons pratiques, nous ne devrions pas essayer d'être aussi ambitieux et devrions oublier les mathématiques ésotériques des modules sur des anneaux non standard. Nous devrions certainement avoir une fonction dans Base (avec une syntaxe plus simple que permutedims(A, (2,1)) ) qui n'a rien à voir avec le concept d'algèbre linéaire de transposition et qui retourne simplement les matrices et ne fait rien de récursif, que ce soit appelé transpose ou flip ou quoi. Ce serait bien si adjoint et une fonction de transposition séparée (éventuellement avec un nom différent) dans LinAlg étaient récursifs, car ils pourraient alors gérer les vecteurs de blocs / matrices et l'implémentation simple de la somme directe comme vecteurs de vecteurs, mais cela n'est pas exigé par «l'exactitude mathématique objective», et il serait bien de prendre cette décision uniquement pour des raisons de facilité de mise en œuvre.

Malgré ma promesse précédente de ne plus commenter, je dois malheureusement répondre à celle-ci.

L'adjoint de Julia se réfère spécifiquement à l'adjoint hermitien. En général, pour U et V des espaces vectoriels normés complets (espaces de Banach) avec des espaces doubles respectifs U * et V , et pour l'application linéaire A: U -> V, alors l'adjoint hermitien de A, typiquement noté A , est une application linéaire A *: V * -> U *. Autrement dit, en général l'adjoint hermitien est une application linéaire entre des espaces doubles, tout comme en général la transposée A ^ t est une application linéaire entre des espaces doubles comme mentionné ci-dessus. Alors, comment conciliez-vous ces définitions avec la notion familière d'adjoint hermitien? :)

Ce n'est vraiment pas vrai. Ce que vous décrivez ici est vraiment la transposition, mais (comme je l'ai déjà mentionné), dans certains domaines, cela est appelé l'adjoint (sans hermitien) et noté A ^ t ou A ^ * (jamais A ^ dagger). En fait, il s'étend bien au-delà des espaces vectoriels, et dans la théorie des catégories, un tel concept existe dans n'importe quelle catégorie monoïdale (par exemple, la catégorie Cob des variétés orientées à n dimensions avec des cobordismes comme des cartes linéaires), où il est appelé compagnon adjoint (dans en fait, il peut y en avoir deux différents A et A , puisque les doubles espaces gauche et droit ne sont pas nécessairement les mêmes). Mais notez que cela n'implique jamais de conjugaison complexe. Les éléments de V * sont en effet les applications linéaires f: V-> Scalaire, et pour une application linéaire A: U-> V et un vecteur v de U, on a f (Av) = (A ^ tf) (v) . Puisque l'action de f n'implique pas de conjugaison complexe, la définition de A ^ t non plus.

La réponse réside dans la structure supplémentaire que possèdent les espaces dans lesquels vous travaillez généralement, à savoir les espaces de produits internes complets (espaces de Hilbert). Un produit interne induit une norme, donc un espace de produit interne complet (Hilbert) est un espace normé complet (Banach), et soutient ainsi le concept d'adjoint hermitien. Voici la clé: ayant un produit interne plutôt qu'une simple norme, l'un des plus beaux théorèmes de l'algèbre linéaire s'applique, à savoir le théorème de représentation de Riesz. En un mot, le théorème de représentation de Riesz déclare qu'un espace de Hilbert est naturellement isomorphe à son espace dual. Par conséquent, lorsque nous travaillons avec des espaces de Hilbert, nous identifions généralement les espaces et leurs duaux et abandonnons la distinction. Faire cette identification est la façon dont vous arrivez à la notion familière d'adjoint hermitien comme A *: V -> U plutôt que A *: V * -> U *.

Encore une fois, je ne pense pas que ce soit tout à fait correct. Dans les espaces de produits internes, le produit interne est une forme sesquilinéaire dot de conj (V) x V -> Scalaire (avec conj (V) l'espace vectoriel conjugué). Cela permet en effet d'établir une application de V vers V * (ou techniquement de conj (V) vers V *), qui est bien le théorème de représentation de Riesz. Cependant, nous n'en avons pas besoin pour introduire l'adjoint hermitien. En réalité, le produit interne dot est lui-même suffisant, et l'adjoint hermitien d'une application linéaire A est tel que
dot(w, Av) = dot(A' w, v) . Cela implique une conjugaison complexe.

Ce n'est vraiment pas vrai. Ce que vous décrivez ici est vraiment la transposition, mais (comme je l'ai déjà mentionné), dans certains domaines, cela est appelé l'adjoint (sans hermitien) et noté A ^ t ou A ^ * (jamais A ^ dagger). [...]

@Jutho , veuillez consulter, par exemple, la page Wikipedia sur l'adjoint hermitien .

Il y a peut-être des incohérences entre les différents domaines des mathématiques, mais:
https://en.wikipedia.org/wiki/Transpose_of_a_linear_map
et en particulier
https://en.wikipedia.org/wiki/Transpose_of_a_linear_map#Relation_to_the_Hermitian_adjoint
et d'innombrables références dans la théorie des catégories, par exemple
https://arxiv.org/pdf/0908.3347v1.pdf

https://en.wikipedia.org/wiki/Transpose_of_a_linear_map
et en particulier
https://en.wikipedia.org/wiki/Transpose_of_a_linear_map#Relation_to_the_Hermitian_adjoint

@Jutho , je ne vois aucune incohérence entre cette section de page et les définitions données sur la page que j'ai liée ci-dessus, et je ne vois aucune incohérence avec ce que j'ai publié ci-dessus. Meilleur!

Je vais aussi signer la proposition 2 de @ Sacha0 . Je suis d'accord avec un argument à permutedims pour le moment également; Je pense que c'est mieux que flip .

@ Sacha0 , alors nous avons une manière différente d'interpréter cela. Je lis ceci comme
Pour un A donné: U-> V,
transpose (A) = dual (A) = (parfois aussi) adjoint (A): V * -> U *
adjoint hermitien (A) = poignard (A) = (typiquement juste) adjoint (A): V-> U
et la relation entre les deux est exactement obtenue en utilisant la carte de l'espace à l'espace dual (ie Riesz ...), qui implique une conjugaison complexe. Par conséquent, l'adjoint hermitien implique la conjugaison, la transposition ne le fait pas.

Si vous voulez aussi appeler le premier hermitien adjoint, alors qu'est-ce que vous appelez transposer? Vous n'avez pas vraiment défini ce que c'était dans votre description, vous venez de mentionner

L'adjoint hermitien de A, typiquement noté A , est une application linéaire A : V * -> U *. Autrement dit, en général l'adjoint hermitien est une application linéaire entre des espaces doubles, tout comme en général la transposée A ^ t est une application linéaire entre des espaces doubles

Donc transposée et adjointe hermitienne sont-elles alors deux manières différentes de transformer A: U-> V en une application de V -> U ? Je serais vraiment heureux d'en discuter davantage à ce sujet, mais je suppose que nous ferions mieux de le faire ailleurs. Mais vraiment, contactez-moi car je suis vraiment très intéressé d'en savoir plus à ce sujet.

Voir aussi http://staff.um.edu.mt/jmus1/banach.pdf pour une référence selon laquelle l'adjoint tel qu'utilisé dans le contexte des espaces de Banach est vraiment transposable, et non hermitien adjoint (en particulier c'est un linéaire et non un antilinéaire transformation). Wikipedia (et d'autres références) confondent vraiment ces deux concepts, en utilisant la notion d'adjoint hermitien dans les espaces de Hilbert comme motivation pour une définition généralisée d'adjoint dans les espaces de Banach. Cependant, cette dernière est vraiment transposée (et n'a pas besoin de produit interne, ni de norme). Mais c'est la transposition dont je parlais, que personne n'utilise vraiment dans le code informatique.

Pour Julia Base: je ne suis pas opposé à la conjugaison hermitienne récursive; Je conviens que ce sera souvent la bonne chose à faire. Je ne suis pas sûr que Base doive essayer de faire des choses intelligentes lorsque le type d'élément n'est pas Number . Même avec T is number, il n'y a pas de support dans Base pour l'utilisation beaucoup plus courante des produits internes non euclidiens (et des définitions modifiées associées d'adjoint), et je ne pense pas qu'il devrait y en avoir. Je pense donc que la motivation principale était les matrices de blocs, mais là je pense juste qu'un type à usage spécial (dans Base ou dans un package) est beaucoup plus Julian, et, comme @andyferris l'a également mentionné, ce n'est pas comme tout le reste de LinAlg prend en charge cette notion de matrices de blocs, même des choses simples comme inv (sans parler des factorisations matricielles, etc.).

Mais si la conjugaison hermitienne récursive est là pour rester (très bien pour moi), alors je pense que pour la cohérence dot et vecdot devraient agir récursivement sur les éléments. Actuellement, ce n'est pas le cas: dot appelle x'y sur les éléments (ce qui n'est pas la même chose lorsque les éléments sont des matrices) et vecdot appelle dot sur les éléments. Donc, pour un vecteur de matrices, il n'y a en fait aucun moyen d'obtenir un résultat scalaire. Je serais heureux de préparer un PR si les gens conviennent que la mise en œuvre actuelle n'est pas vraiment incompatible avec le adjoint récursif.

Quant à transpose , il semble juste plus simple de le rendre non récursif et de le laisser également être utilisé par ceux qui ne fonctionnent pas avec des données numériques. Je pense que la plupart des gens qui travaillent avec des matrices connaissent le terme transpose et vont le chercher. Il y a encore conj(adjoint()) pour ceux qui ont besoin d'un transpose récursif.

Triage pense que @ Sacha0 devrait aller de l'avant avec la proposition 2 afin que nous puissions l'essayer.

Je suis tout à fait d'accord avec @ttparker pour

1 - à LinAlg , un AbstractVector v est un vecteur length(v) -dimensionnel avec des poids de base (scalaires) v[1] , v[2] , ..., v[length(v)] .

(et de même pour AbstractMatrix ).

Ce serait probablement l'hypothèse que beaucoup de gens feraient en venant d'autres bibliothèques, et avoir une définition aussi simple des vecteurs de base, des rangs, etc., permet de simplifier l'implémentation. (Beaucoup diraient probablement que l'algèbre linéaire numérique est faisable à implémenter sur un ordinateur précisément parce que nous avons une bonne base pour travailler.)

Notre approche actuelle ressemble plus à:

2 - à LinAlg , un AbstractVector v est une somme directe de length(v) vecteurs abstraits. Nous incluons également suffisamment de définitions sur les types scalaires comme Number pour que pour LinAlg ce soient des opérateurs / vecteurs linéaires unidimensionnels valides.

et de même pour les matrices (blocs). C'est beaucoup plus généralisé que les implémentations d'algèbre linéaire dans MATLAB, numpy, eigen, etc., et c'est le reflet du puissant système de type / répartition de Julia que cela est même faisable.

La raison principale pour laquelle je considère l'option 2 comme étant souhaitable est encore une fois que le système de type / répartition de Julia nous permet d'avoir un objectif beaucoup plus large, qui va vaguement comme:

3 - Dans LinAlg , nous essayons de créer une interface d'algèbre linéaire générique qui fonctionne pour les objets qui satisfont la linéarité (etc) sous + , * , conj (etc), traitant ces objets comme des opérateurs linéaires / membres d'un espace de Hilbert / tout ce qui est approprié.

Ce qui est un objectif vraiment cool (sûrement bien au-delà de tout autre langage de programmation / bibliothèque que je connaisse), motive complètement les récursifs adjoint et 2 (car + , * et conj sont eux-mêmes récursifs) et c'est pourquoi la proposition 2 de @ Sacha0 et la décision de triage sont un bon choix :)

Je serais vraiment heureux d'en discuter davantage à ce sujet, mais je suppose que nous ferions mieux de le faire ailleurs. Mais vraiment, contactez-moi car je suis vraiment très intéressé d'en savoir plus à ce sujet.

Cheers, faisons! J'ai hâte de discuter davantage hors ligne :). Meilleur!

Beau résumé Andy! :)

Andy entièrement d'accord, au moins pour adjoint (qui était le sujet de votre commentaire).

Cependant, un dernier plaidoyer pour un transpose non récursif, avant que je ne me tienne à jamais (espérons-le).
Je vois une transposition non récursive ayant les avantages suivants:

  • Il peut être utilisé par toutes les personnes travaillant avec des matrices, même contenant des données non numériques. C'est aussi ainsi qu'ils connaîtront probablement cette opération et la rechercheront, à partir d'autres langages et des mathématiques de base extrapolées à leur cas d'utilisation non numérique.
  • Pas besoin d'écrire du code supplémentaire pour que le type paresseux flip ou PermutedDimsArray interagisse avec LinAlg . Et si j'avais une matrice numérique que j'ai retournée au lieu de transposer; pourrai-je encore le multiplier avec d'autres matrices (de préférence en utilisant BLAS)?

  • Avec un transpose non récursif et un adjoint non récursif, on peut facilement avoir un adjoint non récursif comme conj(transpose(a)) et une transposée récursive conj(adjoint(a)) . Et toujours, tout interagira bien avec LinAlg .

Alors, quels sont les inconvénients. Je n'en vois aucun. Je maintiens toujours mon point de vue selon lequel personne n'utilise transpose dans son sens mathématique. Mais au lieu d'essayer d'argumenter plus loin, quelqu'un peut-il me donner un exemple concret où une transposée récursive est nécessaire ou utile, et où est-ce vraiment une transposition mathématique? Cela exclut tout exemple où vous avez réellement l'intention d'utiliser adjoint mais que vous n'avez que des nombres réels. Donc une application où il y a une raison mathématique de transposer un vecteur ou une matrice remplie de plusieurs matrices qui sont elles-mêmes des valeurs complexes.

Je peux dire qu'au moins Mathematica (dont on s'attendrait à y avoir consacré beaucoup de réflexion) ne fait pas de transposition récursive:

A = Array[1 &, {2, 3, 4, 5}];
Dimensions[A]  # returns {2, 3, 4, 5}
Dimensions[Transpose[A]] # returns {3, 2, 4, 5}

EDIT: Oups, cela a également été commenté ci-dessus, désolé

Donc je suis confus. Il semblait y avoir un consensus assez solide sur le fait que transpose devrait être rendu non récursif - par exemple https://github.com/JuliaLang/julia/issues/20978#issuecomment -285865225, https://github.com/ JuliaLang / julia / issues / 20978 # issuecomment -285942526, https://github.com/JuliaLang/julia/issues/20978#issuecomment -285993057, https://github.com/JuliaLang/julia/issues/20978#issuecomment - 348464449 et https://github.com/JuliaLang/julia/pull/23424. Puis @ Sacha0 a donné deux propositions, dont l'une laisserait la transposition récursive mais introduirait une fonction flip non récursive, qui a obtenu un fort soutien malgré (pour autant que je sache) qu'elle n'ait pas vraiment été soulevée comme une possibilité auparavant . Ensuite, @JeffBezanson a suggéré que nous n'avons pas besoin de flip après tout si nous donnons à permutedims un deuxième argument par défaut, qui a également un fort soutien.

Alors maintenant, le consensus semble être que les seuls changements réels apportés à transpose devraient être "en coulisses": en ce qui concerne la réduction spéciale et l'évaluation paresseuse ou impatiente, que l'utilisateur final typique ne connaîtra probablement pas ou ne se souciera probablement pas . Les seuls changements vraiment visibles sont essentiellement des changements d'orthographe (déprécier .' et donner à permutedims un deuxième argument par défaut).

Le consensus de la communauté semble donc avoir changé de près de 180 degrés en très peu de temps (à peu près au moment de la publication de

J'oublie si quelqu'un a suggéré cela, mais pourrions-nous simplement rendre transpose(::AbstractMatrix{AbstractMatrix}) (et peut-être transpose(::AbstractMatrix{AbstractVector}) aussi) récursif et transpose non récursif autrement? Cela semble couvrir toutes les bases et je ne peux penser à aucun autre cas d'utilisation où vous voudriez que tranpose soit récursif.

Le consensus communautaire semble donc avoir changé de près de 180 degrés en très peu de temps (à peu près au moment du post # 20978 de @ Sacha0 (commentaire)). Le message de Sacha était-il si éloquent qu'il a changé d'avis pour tout le monde? (C'est bien si c'est le cas, je veux juste comprendre pourquoi nous semblons avancer sur une voie qui, il y a quelques jours à peine, nous semblions tous convenir que ce n'était pas la bonne.)

Si seulement j'étais si éloquent 😄. Ce que vous voyez, c'est que le consensus ne s'est pas réellement formé. Au contraire, (1) les participants qui étaient favorables au statu quo, mais s'étaient retirés de la discussion en raison de l'attrition, sont revenus pour exprimer une opinion; et (2) d'autres parties qui n'avaient pas envisagé ce que l'abandon du statu quo impliquerait dans la pratique (et comment cela pourrait jouer avec les considérations de libération) ont formé une opinion plus forte en faveur du statu quo et ont exprimé cette opinion.

Veuillez noter que cette discussion est en cours sous une forme ou une autre sur github depuis 2014, et probablement plus tôt hors ligne. Pour les participants de longue date, de telles discussions deviennent épuisantes et cycliques. Il y a un travail significatif à faire autre que de s'engager dans cette discussion - comme l'écriture de code, qui est plus agréable - le résultat est l'attrition parmi ces participants de longue date. Par conséquent, la conversation semble déséquilibrée pendant une période ou une autre. Personnellement, je suis à peu près à ce seuil d'attrition, donc je vais me concentrer sur l'écriture de code maintenant plutôt que de continuer à m'engager dans cette discussion. Merci à tous et meilleur! :)

Je vais voter légèrement en faveur de la transposition et du ctranspose non récursifs pour AbstractArrays, les deux étant récursifs sur AbstractArray {T} où T <: AbstractArray.

Je suis d'accord que le comportement récursif est «correct» dans certains cas, et je vois la question comme comment pouvons-nous obtenir le comportement correct avec le moins de surprise pour ceux qui utilisent et développent des paquets.
Dans cette proposition, le comportement de transposition récursif pour les types personnalisés est opt-in: vous vous inscrivez en faisant de votre type un AbstractArray ou en définissant la méthode appropriée
Base.transpose(AbstractArray{MyType}) ou Base.transpose(AbstractArray{T}) where T<: MyAbstractType .
Je pense que la stratégie de typage de canard pour les transpositions récursives (juste récursive sans demander) produit quelques surprises comme documenté ci-dessus. Si vous introduisez des propositions ctranspose et adjointe distinctes, ou des propositions plus compliquées comme conjadjoint et flip, les utilisateurs les rencontreront et essaieront de les utiliser, et les responsables du paquet essaieront de les supporter tous.

Comme exemple de quelque chose qui serait difficile à supporter dans le cadre des nouvelles propositions: les tableaux normaux, transposés, ctransposés et conjoints devraient tous pouvoir avoir des vues (ou une évaluation paresseuse) qui interagissent avec les vues ReshapedArray et SubArray. (Je ne sais pas si ceux-ci produisent des vues par défaut ou uniquement lors de l'utilisation de @view .) Cela se connecte au travail sur la A*_mul_B* et sur les appels BLAS de niveau inférieur avec les indicateurs 'N' «T» et «C» pour les tableaux denses, comme cela a été noté ailleurs. Celles-ci seraient plus faciles à raisonner si elles traitaient normal , transpose , ctranspose et conj
sur un pied d'égalité. Notez que BLAS lui-même ne prend en charge que «N» pour normal, «T» pour transpose et «C» pour ctranspose et n'a pas de drapeau pour conj, ce qui est à mon avis une erreur.

Enfin, par souci de cohérence avec les tableaux de dimensions supérieures et les remodelages, je crois que la généralisation appropriée de transpose et ctranspose consiste à inverser toutes les dimensions, c'est-à-dire
transpose (A :: Array {T, 3}) = permutés (A, (3, 2, 1)).

À votre santé!

J'apprécie beaucoup les gens qui font réellement le travail. Ce qui a été discuté trop longuement, ce sont les vecteurs adjoints / transposés (mais jamais l'aspect récursif de celui-ci), jusqu'à ce que

Cela étant dit, la transposition de matrice et l'adjoint / ctranspose n'ont jamais fait l'objet de beaucoup de discussions, surtout pas l'aspect récursif de celui-ci, qui a été presque silencieusement introduit dans https://github.com/JuliaLang/julia/pull/7244 avec des matrices de blocs de motivation uniques . Diverses raisons et motivations pour l'adjoint récursif ont été données (après les faits), et la plupart des gens peuvent s'accorder sur le fait qu'il s'agit d'un bon (mais pas le seul) choix. Cependant, la transposition manque même d'une seule motivation ou d'un cas d'utilisation réel.

Il y a quelques choses distinctes dans ces discussions, et il arrive maintenant que nous ayons besoin d'un plan qui puisse être mis en œuvre rapidement.

  • Nous avons discuté de la pertinence de prendre en charge les matrices de blocs (et les structures plus exotiques) dans LinAlg . Les choix d'implémentation sont: pas de trucs récursifs du tout (sauf + , * et conj car c'est la nature des fonctions génériques dans Julia), tout récursif (le statu quo), ou essayer une sorte de contrôle de type ou de trait pour savoir si un élément doit faire de l'algèbre linéaire récursive ou être traité comme scalaire.
  • Nous voulons un moyen agréable pour les utilisateurs de permuter les dimensions d'un tableau 2D de données. Nous avons permutedims syntaxe non récursive transpose , flip , raccourcie permutedims (ce PR a été soumis en premier uniquement parce que c'est le moins de caractères à implémenter, et a probablement du sens même si nous faisons autre chose également), une sorte de vérification de type ou de trait pour savoir si un élément doit faire une transposition récursive (peut-être même réintroduire transpose(x::Any) = x ...).
  • L'analyseur Julia a un comportement étrange comme x' * y -> Ac_mul_B(x, y) qui est un peu une verrue, qui n'existera idéalement pas dans la v1.0. Cela n'a pas été considéré comme faisable jusqu'à ce que nous puissions prendre en charge BLAS rapide (pas de copies supplémentaires) sans lui, donc transposition de matrice paresseuse et adjoint.
  • Le code dans LinAlg est assez volumineux et construit sur plusieurs années. Beaucoup de choses comme la multiplication matricielle pourraient être remodelées pour être plus respectueuses des traits, peut-être avec un système de répartition ressemblant davantage au nouveau broadcast . Je pense que c'est là que nous pouvons faciliter l'envoi des bons tableaux (je pense à PermuteDimsArray de vues ajdointes remodelées conjuguées de matrices strided) à BLAS. Cependant, cela ne rendra pas la v1.0 et nous essayons également d'éviter les régressions de performances sans aggraver le code. Comme Sacha l'a souligné (et je découvre) avoir des vues transposées avec un large éventail de comportements sur les éléments (adjoint récursif, transposition récursive, conjugaison, rien) rend plus complexe et un tas de nouvelles méthodes pour faire fonctionner les choses comme elles sont.

Si nous pensons que la v1.0 stabilise quelque peu le langage, alors à certains égards, la plus grande priorité pour faire un changement de comportement est la troisième. Je dirais: le langage (y compris l'analyseur) devrait être le plus stable, suivi de Base , suivi de stdlib (qui peut ou non inclure LinAlg , mais je pense que presque certainement inclura BLAS , Sparse , etc. un jour). C'est un changement qui n'affecte pas vraiment les utilisateurs (principalement les développeurs de bibliothèques), donc je ne serais pas surpris si les opinions des gens diffèrent ici.

Spot sur Andy! :)

Je pense que la seule chose à faire ici est de rendre adjoint et transpose paresseux par défaut?

Cela peut-il être fermé maintenant?

Prochaine étape: "Prendre au sérieux les transpositions scalaires"

Mais sérieusement, pouvons-nous avoir une bonne interface pour spécifier les différentes transpositions 3D et multiplications de tenseur qui sont utilisées dans les solveurs PDE? Un peu sérieux, mais je ne sais pas si je pourrais supporter d'être l'OP à la prochaine itération de cette folie.

non

:)

peut-on avoir une bonne interface pour spécifier les différentes transpositions 3D et multiplications de tenseur qui sont utilisées dans les solveurs PDE

Cela semble définitivement être un bon sujet pour un package.

une bonne interface pour spécifier les différentes transpositions 3D et multiplications tenseur

TensorOperations.jl ne fait- TensorOperations ).

Ouais, TensorOperations.jl a l'air bien. Je plaisantais un peu, mais j'ai obtenu ce dont j'avais besoin 👍.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

TotalVerb picture TotalVerb  ·  3Commentaires

manor picture manor  ·  3Commentaires

omus picture omus  ·  3Commentaires

ararslan picture ararslan  ·  3Commentaires

tkoolen picture tkoolen  ·  3Commentaires