Julia: Levando a transposição da matriz a sério

Criado em 10 mar. 2017  ·  141Comentários  ·  Fonte: JuliaLang/julia

Atualmente, transpose é recursivo. Isso é muito pouco intuitivo e leva a esta infelicidade:

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

Há algum tempo, dizemos às pessoas que façam permutedims(A, (2,1)) vez disso. Mas acho que todos nós sabemos, no fundo, isso é terrível. Como chegamos aqui? Bem, está bastante claro que se deseja que o ctranspose ou "adjunto" de uma matriz de matrizes seja recursivo. Um exemplo motivador é que você pode representar números complexos usando matrizes 2x2 , caso em que o "conjugado" de cada "elemento" (na verdade uma matriz), é seu adjunto como uma matriz - em outras palavras, se ctranspose é recursivo, então tudo trabalho. Este é apenas um exemplo, mas generaliza.

O raciocínio parece ter sido o seguinte:

  1. ctranspose deve ser recursivo
  2. ctranspose == conj ∘ transpose == conj ∘ transpose
  3. transpose portanto, também deve ser recursivo

Acho que existem alguns problemas aqui:

  • Não há nenhuma razão que ctranspose == conj ∘ transpose == conj ∘ transpose tenha que valer, embora o nome faça isso parecer quase inevitável.
  • O comportamento de conj operando elementwise em arrays é uma espécie de resquício infeliz do Matlab e não é realmente uma operação matematicamente justificável, assim como exp operando elementwise não é realmente matematicamente sólido e o que expm does seria uma definição melhor.
  • Na verdade, está pegando o conjugado (isto é, adjunto) de cada elemento que implica que ctranspose deve ser recursivo; na ausência de conjugação, não há uma boa razão para transpor ser recursiva.

Consequentemente, eu proporia as seguintes mudanças para remediar a situação:

  1. Renomeie ctranspose (também conhecido ' ) para adjoint - isso é realmente o que esta operação faz e nos livra da implicação de que deve ser equivalente a conj ∘ transpose .
  2. Descontinue conj(A) vetorizados em matrizes em favor de conj.(A) .
  3. Adicione um argumento de palavra-chave recur::Bool=true a adjoint (née ctranspose ) indicando se ele deve chamar a si mesmo recursivamente. Por padrão, sim.
  4. Adicione um argumento de palavra-chave recur::Bool=false a transpose indicando se ele deve chamar a si mesmo recursivamente. Por padrão, isso não acontece.

No mínimo, isso nos permitiria escrever o seguinte:

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

Se poderíamos ou não encurtar ainda mais para A' depende do que queremos fazer com conj e adjoint de não-números (ou mais especificamente, não real, não valores -complex).

[Este problema é o segundo de uma série de ω₁ partes.]

breaking decision linear algebra

Comentários muito úteis

(OT: Já estou ansioso para "Levar 7 tensores a sério", o próximo capítulo da minissérie de 6 partes de grande sucesso ...)

Todos 141 comentários

O sucessor lógico de uma edição anterior ... 👍

O comportamento de conj operando elemento a elemento em matrizes é uma espécie de resquício infeliz do Matlab e não é realmente uma operação matematicamente justificável

Isso não é verdade de forma alguma e não é análogo a exp . Espaços vetoriais complexos e suas conjugações são um conceito matemático perfeitamente bem estabelecido. Veja também https://github.com/JuliaLang/julia/pull/19996#issuecomment -272312876

Espaços vetoriais complexos e sua conjugação são um conceito matemático perfeitamente bem estabelecido.

A menos que eu esteja enganado, a operação de conjugação matemática correta nesse contexto é ctranspose vez de conj (que é precisamente o que quero dizer):

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

O problema de usar uma palavra-chave para recur é que, da última vez que verifiquei, havia uma grande penalidade de desempenho para o uso de palavras-chave, o que é um problema se você acabar chamando recursivamente essas funções com um caso base que termina em escalares.

Precisamos corrigir o problema de desempenho de palavra-chave em 1.0 de qualquer maneira.

@StefanKarpinski , você está enganado. Você pode ter conjugação complexa em um espaço vetorial sem ter adjuntos - adjuntos são um conceito que requer um espaço de Hilbert etc., não apenas um espaço vetorial complexificado.

Além disso, mesmo quando você tem um espaço de Hilbert, o conjugado complexo é distinto do adjunto. por exemplo, o conjugado de um vetor de coluna complexo em ℂⁿ é outro vetor complexo, mas o adjunto é um operador linear (um "vetor linha").

(A conjugação complexa não significa que você pode multiplicar conj(v)*v !)

A descontinuação do vetorizado conj é independente do resto da proposta. Você pode fornecer algumas referências para a definição de conjugação complexa em um espaço vetorial?

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

(Este tratamento é bastante formal; mas se você pesquisar "matriz de conjugado complexo" ou "vetor de conjugado complexo" no Google, encontrará zilhões de usos.)

Se mapear um vetor complexo para o vetor conjugado (o que conj faz agora) é uma operação importante distinta do mapeamento para o covetor conjugado (o que ' faz), então certamente podemos manter conj para expressar essa operação em vetores (e matrizes e matrizes superiores, eu acho). Isso fornece uma distinção entre conj e adjoint já que eles concordariam em escalares, mas se comportariam de maneira diferente em matrizes.

Nesse caso, conj(A) chamar conj em cada elemento ou deve chamar adjoint ? A representação de números complexos como um exemplo de matrizes 2x2 sugere que conj(A) deve realmente chamar adjoint em cada elemento, em vez de chamar conj . Isso tornaria adjoint , conj e conj. todas as operações diferentes:

  1. adjoint : troque os índices i e j e mapeie recursivamente adjoint sobre os elementos.
  2. conj : mapear adjoint sobre os elementos.
  3. conj. : mapear conj sobre os elementos.

conj(A) deve chamar conj em cada elemento. Se você estiver representando números complexos por matrizes 2x2, terá um espaço vetorial complexificado diferente.

Por exemplo, um uso comum de conjugação de vetores é na análise de autovalores de matrizes reais : os autovalores e autovetores vêm em pares complexos-conjugados. Agora, suponha que você tenha uma matriz de bloco representada por uma matriz 2d A de matrizes 2x2 reais, agindo sobre vetores bloqueados v representada por uma matriz 1d de vetores de 2 componentes. Para qualquer autovalor λ de A com autovetor v , esperamos um segundo autovalor conj(λ) com autovetor conj(v) . Isso não funcionará se conj chamar adjoint recursivamente.

(Observe que o adjunto, mesmo para uma matriz, pode ser diferente de um conjugado-transposto, porque o adjunto de um operador linear definido da maneira mais geral depende também da escolha do produto interno. Existem muitas aplicações reais onde algumas tipo de produto interno ponderado é apropriado, caso em que a maneira apropriada de obter o adjunto de uma matriz muda! Mas eu concordo que, dado um Matrix , devemos tomar o adjunto correspondente ao produto interno padrão dado por dot(::Vector,::Vector) . No entanto, é inteiramente possível que alguns tipos de AbstractMatrix (ou outro operador linear) queiram substituir adjoint para fazer algo diferente.)

Correspondentemente, há também alguma dificuldade em definir um adjoint(A) algébricamente razoável para, por exemplo, arrays 3d, porque não definimos arrays 3d como operadores lineares (por exemplo, não há operação array3d * array2d embutida) . Talvez adjoint só deva ser definido por padrão para arrays escalares, 1d e 2d?

Atualização: Oh, bom: nós não definimos ctranspose agora para matrizes 3D também. Continue.

(OT: Já estou ansioso para "Levar 7 tensores a sério", o próximo capítulo da minissérie de 6 partes de grande sucesso ...)

Se você estiver representando números complexos por matrizes 2x2, terá um espaço vetorial complexificado diferente.

Não estou entendendo isso - a representação da matriz 2x2 de números complexos deve se comportar exatamente como se tivesse escalares complexos como elementos. Eu pensaria, por exemplo, se definirmos

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

e temos um vetor complexo arbitrário v então gostaríamos que essa identidade contivesse:

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

Eu explicaria mais o exemplo e faria uma comparação com ' que supostamente já se desloca com m mas não posso por causa de https://github.com/JuliaLang/julia/ edições / 20979 , que acidentalmente quebra essa comutação. Assim que o bug for corrigido, m.(v)' == m.(v') será mantido, mas se estou entendendo corretamente, @stevengj , então conj(m.(v)) == m.(conj(v)) não deveria?

conj(m(z)) deveria == m(z) .

Seu m(z) é um isomorfismo para números complexos sob adição e multiplicação, mas não é o mesmo objeto de outras maneiras para álgebra linear, e não devemos fingir que é para conj . Por exemplo, se z é um escalar complexo, então eigvals(z) == [z] , mas eigvals(m(z)) == [z, conj(z)] . Quando o truque m(z) é estendido a matrizes complexas, essa duplicação do espectro tem consequências complicadas para, por exemplo, métodos iterativos (veja, por exemplo, este artigo ).

Outra forma de colocar isso é que z já é um espaço vetorial complexo (um espaço vetorial sobre os números complexos), mas a matriz real 2x2 m(z) é um espaço vetorial sobre os números reais (você pode multiplicar m(z) por um número real) ... se você "complexificar" m(z) multiplicando por um número complexo, por exemplo m(z) * (2+3im) ( não m(z) * m(2+3im) ), então você obter um espaço vetorial complexo diferente não mais isomórfico ao espaço vetorial complexo original z .

Esta proposta parece que irá produzir uma melhoria real: +1: Estou pensando no lado matemático:

  • pelo que entendi, a conjugação é uma ação no anel (leia-se: números) que fornecem os coeficientes para o nosso espaço vetorial / vetores. Essa ação induz outra ação no espaço vetorial (e em seus mapeamentos, leia-se: matrizes), também chamada de conjugação, pela linearidade da construção do espaço vetorial sobre o anel. Conseqüentemente, a conjugação necessariamente funciona por aplicação elementar aos coeficientes. Para Julia, isso significa que, no fundo, para um vetor v , conj(v) = conj.(v) , é uma escolha de design se conj tem métodos também para matrizes ou apenas para escalares, ambos os quais parece ok? (O ponto de Steven sobre o exemplo de números complexos como matrizes 2x2 é que este é um espaço vetorial cujos coeficientes são formalmente / realmente reais, então a conjugação não tem efeito aqui.)
  • adjoint tem um significado algébrico que é fundamental em muitos domínios importantes que envolvem intimamente a álgebra linear (ver 1 e 2 e 3 , todos com grandes aplicações na física também). Se adjoint for adicionado, ele deve permitir argumentos de palavra-chave para a ação em consideração - em uma análise de espaços vetoriais / funcional, essa ação é tipicamente induzida pelo produto interno, portanto, o argumento de palavra-chave pode ser a forma. O que quer que seja projetado para as transposições de Julia deve evitar conflito com esses aplicativos, então acho que adjoint é um pouco caro?

@felixrehren , não acho que você usaria um argumento de palavra-chave para especificar o produto interno que induz o adjunto. Acho que você apenas usaria um tipo diferente, da mesma forma que se quisesse alterar o significado de dot para um vetor.

Minha preferência seria um pouco mais simples:

  • Mantenha conj no estado em que se encontra (elemento a elemento).
  • Faça a' corresponder a adjoint(a) , e torná-lo recursivo sempre (e portanto falhar para arrays de strings etc onde adjoint não está definido para os elementos).
  • Faça a.' corresponder a transpose(a) e torne-o nunca recursivo (apenas permutedims ) e, portanto, ignore o tipo dos elementos.

Eu realmente não vejo um caso de uso para adjoint não recursivo. Em resumo, posso imaginar casos de uso para um transpose recursivo, mas em qualquer aplicativo em que você precise alterar a.' para transpose(a, recur=true) , parece tão fácil chame outra função transposerecursive(a) . Poderíamos definir transposerecursive no Base, mas acho que a necessidade disso será tão rara que devemos esperar para ver se realmente surge.

Pessoalmente, acho que manter isso simples será mais direto para os usuários (e para a implementação) e ainda será bastante defensável na frente da álgebra linear.

Até agora (a maioria) de nossas rotinas de álgebra linear estão fortemente enraizadas em estruturas de array padrão e no produto interno padrão. Em cada slot de sua matriz ou vetor, você coloca um elemento de seu "campo". Argumentarei que, para elementos de campos , nos preocupamos com + , * , etc, e conj , mas não transpose . Se o seu campo era uma representação complexa especial que se parece um pouco com uma matriz real 2x2, mas muda em conj , então está tudo bem - é uma propriedade de conj . Se for apenas um AbstractMatrix 2x2 real que não muda em conj , então, indiscutivelmente, tais elementos de uma matriz não deveriam mudar sob o adjunto ( @stevengj disse isso melhor do que eu posso descrever - pode haver um isomorfismo para números complexos, mas isso não significa que se comporte da mesma forma em todas as formas).

De qualquer forma, o exemplo complexo 2x2 parece um pouco errado para mim. A causa real do comportamento recursivo da matriz foi como um atalho para fazer álgebra linear em matrizes de bloco. Por que não tratamos esse caso especial com o devido cuidado e simplificamos o sistema subjacente?

Portanto, minha sugestão "simplificada" seria:

  • Mantenha conj como está (ou torne-o uma visualização por AbstractArray s)
  • Faça a' uma visão não recursiva de forma que (a')[i,j] == conj(a[j,i])
  • Faça a.' uma visão não recursiva de forma que (a.')[i,j] == a[j,i]
  • Apresente um tipo BlockArray para lidar com matrizes de bloco e assim por diante (em Base , ou possivelmente em um pacote bem suportado). Indiscutivelmente, isso seria muito mais poderoso e flexível do que Matrix{Matrix} para essa finalidade, mas igualmente eficiente.

Acho que essas regras seriam simples o suficiente para os usuários adotarem e desenvolverem.

PS - @StefanKarpinski Por razões práticas, um argumento de palavra-chave booleana para recursão não funcionará com

Além disso, eu mencionei em outro lugar, mas vou adicioná-lo aqui para completar: visualizações de transposição recursiva têm a propriedade irritante de que o tipo de elemento pode mudar em comparação com a matriz que está envolvendo. Por exemplo, transpose(Vector{Vector}) -> RowVector{RowVector} . Não pensei em uma maneira de obter esse tipo de elemento para RowVector sem uma penalidade de tempo de execução ou invocando inferência para calcular o tipo de saída. Estou supondo que o comportamento atual (invocar inferência) é indesejável do ponto de vista da linguagem.

NB: também não há nada que impeça os usuários de definir conj para retornar um tipo diferente - então a visão ConjArray também sofre deste problema, seja a transposição recursiva ou não.

@stevengj - você aponta que matrizes 2x2 "complexas" sendo um espaço vetorial formalmente real em vez de um espaço vetorial complexo faz sentido para mim, mas então esse ponto me questiona a motivação original para adjunto recursivo, o que me leva a questionar se A proposta de @andyferris não seria melhor (transposta não recursiva e adjunto). Eu acho que o fato de que tanto o exemplo 2x2 complexo quanto a representação da matriz de bloco "deseja" que o adjunto seja recursivo é sugestivo, mas, dados seus comentários sobre o primeiro exemplo, tenho que me perguntar se não há outros casos em que o adjunto não recursivo é mais correto / conveniente.

Se o adjunto não for recursivo, não é um adjunto. É simplesmente errado.

Você pode dar um pouco mais de justificativa para isso quando tiver um momento?

O adjunto de um vetor deve ser um operador linear mapeando-o para um escalar. Ou seja, a'*a deve ser um escalar se a for um vetor. E isso induz um adjunto correspondente nas matrizes, uma vez que a propriedade definidora é a'*A*a == (A'*a)'*a .

Se a é um vetor de vetores, isso implica que a' == adjoint(a) deve ser recursivo.

OK, acho que entendi isso.

Também temos produtos internos recursivos:

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

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

Claramente, o "adjoint" ou "dual" ou o que quer que seja, deve ser recursivo da mesma forma.

Acho que a questão central é: exigimos que a' * b == dot(a,b) para todos os vetores a , b ?

A alternativa é dizer que ' não retorna necessariamente o adjunto - é apenas uma operação de array que transpõe os elementos e os passa por conj . Isso simplesmente acontece de ser o adjunto para os elementos reais ou complexos.

Só existe uma razão para termos um nome especial e símbolos especiais para adjuntos em álgebra linear: a relação com os produtos internos. Essa é a razão pela qual "transpor conjugada" é uma operação importante e "rotação conjugada de matrizes em 90 graus" não. Não faz sentido ter algo que seja "apenas uma operação de matriz que troca linhas e colunas e se conjuga" se não estiver conectado a produtos escalares.

Pode-se definir uma relação semelhante entre transpose e um "produto escalar" não conjugado, que argumentaria a favor de uma transposição recursiva. No entanto, "produtos escalares" não conjugados para dados complexos, que não são realmente produtos internos, não aparecem quase tão frequentemente como produtos internos verdadeiros - eles surgem de relações bi-ortogonalidades quando um operador é escrito em forma simétrica (não hermitiana) - e nem mesmo tem uma função Julia embutida ou uma terminologia comum em álgebra linear, ao passo que querer trocar linhas e colunas de matrizes não numéricas arbitrárias parece muito mais comum, especialmente com transmissão. É por isso que posso deixar transpose não recursivo enquanto deixo adjoint recursivo.

Eu vejo. Portanto, renomear ' para adjoint faria parte da mudança, para deixar claro que não é conj ∘ transpose ?

O que sempre me confundiu com matrizes recursivas é: o que é escalar neste contexto? Em todos os casos com os quais estou familiarizado, dizem que os elementos de um vetor são escalares. Mesmo no caso em que um matemático escreve uma estrutura de vetor de bloco / matriz em um pedaço de papel, ainda sabemos que isso é apenas uma abreviação de um vetor / matriz maior onde os elementos são provavelmente números reais ou complexos (ou seja, BlockArray ). Você espera que os escalares sejam capazes de se multiplicar em * , e o tipo de escalar normalmente não difere entre o vetor e seu dual.

@andyferris , para um espaço vetorial geral, os escalares são um anel (números) pelos quais você pode multiplicar vetores. Eu posso fazer 3 * [[1,2], [3,4]] , mas não posso fazer [3,3] * [[1,2], [3,4]] . Portanto, mesmo para Array{Array{Number}} , o tipo escalar correto é Number .

Concordo - mas geralmente se diz que os elementos são (os mesmos) escalares, não?

Os tratamentos que vi, de qualquer maneira, começam com um anel e constroem um espaço vetorial a partir deles. O anel suporta + , * , mas eu não vi um tratamento que exija que suporte adjoint , dot ou qualquer outra coisa.

(Desculpe, estou apenas tentando entender o objeto matemático subjacente que estamos modelando).

Depende do que você entende por "taquigrafia" e do que você entende por "elementos".

Por exemplo, é bastante comum ter vetores de funções de tamanho finito e "matrizes" de operadores de dimensão infinita. por exemplo, considere a seguinte forma das equações macroscópicas de Maxwell :

image

Neste caso, temos uma matriz 2x2 de operadores lineares (por exemplo, cachos) agindo em vetores de 2 componentes cujos "elementos" são campos de vetores de 3 componentes. Esses são vetores sobre o campo escalar de números complexos. Em certo sentido, se você detalhar o suficiente, os "elementos" dos vetores são números complexos - componentes individuais dos campos em pontos individuais no espaço - mas isso é bastante obscuro.

Ou talvez, mais precisamente, não podemos usar sempre o anel para descrever os coeficientes do vetor em alguma base (dada a natureza da matriz das coisas em Julia, não vamos ficar livres de bases)?

Em qualquer caso, a consequência ainda é que os adjoints têm que ser recursivos, não? Não entendo a que ponto você está chegando.

(Nós absolutamente podemos ir sem base, eu acho, uma vez que os objetos ou os elementos das matrizes poderiam ser expressões simbólicas ala SymPy ou alguma outra estrutura de dados representando um objeto matemático abstrato, e o adjunto poderia retornar outro objeto que, quando multiplicado, calcula um integral, por exemplo.)

Estou apenas tentando entender melhor, não fazendo um ponto em particular :)

Certamente, no caso anterior, o adjunto é recursivo. Eu queria saber se, por exemplo, acima, é melhor pensar em [E, H] como um BlockVector cujos (infinitamente?) Muitos elementos são complexos, ou como um vetor de 2 cujos elementos são campos de vetor. Acho que, neste caso, a abordagem BlockVector seria impraticável.

Enfim, obrigado.

Então, talvez eu possa destilar meus pensamentos desta forma:

Para um vetor v , eu meio que espero que length(v) descreva a dimensão do espaço vetorial, ou seja, o número de coeficientes escalares que preciso para (completamente) descrever um elemento do espaço vetorial, e também que v[i] retorna o coeficiente escalar i th. Até agora, não pensei em AbstractVector como um "vetor abstrato", mas sim como um objeto que contém os coeficientes de um elemento de algum espaço vetorial com base em alguma base conhecida pelo programador.

Parecia um modelo mental simples e útil, mas talvez seja muito restritivo / impraticável.

(EDITAR: é restritivo porque em Vector{T} , T deve se comportar como um escalar para que as operações de álgebra linear funcionem.)

mas eu não posso fazer [3,3] * [[1,2], [3,4]]

Podemos consertar isso. Não tenho certeza se é uma boa ideia ou não, mas certamente poderia funcionar.

Não tenho certeza se é desejável ... neste caso [3,3] na verdade não é um escalar. (Também já temos a capacidade de fazer [3,3]' * [[1,2], [3,4]] - o que realmente não sei como interpretar no sentido de álgebra linear).

Isso mostra um caso interessante: parece que estamos dizendo que v1' * v2 é o produto interno (e, portanto, retorna um "escalar"), mas [3,3]' * [[1,2], [3,4]] == [12, 18] . Um exemplo artificial, suponho.

Nesse caso [3,3] é um escalar: escalares são elementos no anel subjacente, e existem muitas boas maneiras de fazer elementos da forma [a,b] em um anel (definindo assim vetores / matrizes acima dele). O fato de eles serem escritos como vetores, ou o fato de que o próprio anel forma um espaço vetorial sobre outro anel subjacente, não muda isso. (Você poderia pegar qualquer outra notação que esconda os vetores! Ou faça com que pareça totalmente diferente.) Como @andyferris estava chegando, o que é escalar depende do contexto.

Formalmente, a recursão não faz parte do adjunto. Em vez disso, a conjugação dos elementos escalares faz essa parte do trabalho - e muitas vezes, se um escalar s é representado como uma matriz, a conjugação de s envolverá a transposição da matriz que usamos para denotar isto. Mas nem sempre é o caso, depende da estrutura fornecida pelo usuário para o anel de escalares.

Eu acho que forçar a recursão adjunta pode ser um compromisso prático OK, típico para aplicativos do tipo matlab. Mas para mim, levar isso muito a sério significaria adjunto não recursivo e usar escalares digitados + despacho para permitir que conj fizesse a mágica necessária em qualquer que seja a estrutura do anel subjacente.

Nesse caso, [3,3] é um escalar: escalares são elementos no anel subjacente

Exceto que tal escalar não é um elemento do anel definido por + , * . Não suporta * ; Não consigo fazer [3,3] * [3,3] .

Eu acho que forçar a recursão adjunta pode ser um compromisso prático OK, típico para aplicativos do tipo matlab. Mas para mim, levar isso muito a sério significaria adjunto não recursivo e usar escalares digitados + despacho para permitir que conj fizesse a mágica necessária em qualquer que seja a estrutura do anel subjacente.

Eu concordo, se quisermos fazer uma compreensão prática, está tudo bem e é o que fizemos até agora, mas parece-me que os escalares subjacentes são aqueles da matriz totalmente plana e é nisso que a álgebra linear é definida . Temos a tecnologia para fazer um BlockVector e um BlockMatrix (e BlockDiagonal , etc) que apresentam uma visão eficiente e nivelada do array totalmente expandido, portanto, o "super sério "abordagem deve, no mínimo, ser factível. Eu também acho que seria tão amigável quanto a abordagem recursiva (alguns caracteres extras para digitar BlockMatrix([A B; C D]) em troca do que poderia ser um código semântico melhor e potencialmente mais legível - tenho certeza que estes pontos estão em debate). No entanto, seria mais trabalhoso implementar tudo isso.

@andyferris, você está certo, eles não são um anel porque * não está definido - mas acho que estamos na mesma página que * facilmente poderia ser definido para ele, e há muitas maneiras diferentes de fazer isso que darão uma estrutura em anel. Portanto, suponho que estamos na mesma página: digitar é a maneira mais extensível de resolver isso.

(Sobre escalares: os escalares não são necessariamente os elementos da estrutura totalmente achatada! A razão é esta. Um espaço vetorial complexo unidimensional é unidimensional sobre os números complexos e bidimensional sobre os números reais; nenhuma representação é especial. Os quatérnions são bidimensionais sobre os números complexos. As octonions são bidimensionais sobre os quarternions, os quadridimensionais sobre os números complexos e os oito dimensionais sobre os reais. Mas não há mais razão para insistir que as octonions são "8 dimensionais" do que insistir que seus escalares são os números reais não complexos; essas são escolhas não forçadas pela lógica. É puramente uma questão de representação, o usuário deve ter essa liberdade, pois a álgebra linear é a mesma em cada caso. cadeias muito mais longas de tais espaços com anéis multipolinomiais [truncados] ou extensões algébricas de campos de números, ambos os quais têm aplicações vivas com matrizes. Julia não precisa ir tão longe na toca do coelho - mas nós deve se lembrar de que existe, para não bloqueá-lo. Talvez um tópico de discurso? :))

@felixrehren , um vetor de vetores sobre um anel R é mais bem entendido como um espaço de soma direta, que também é um espaço vetorial sobre R. Não faz sentido chamar os próprios vetores de "o anel subjacente", porque em geral eles não são um anel. Esse raciocínio se aplica perfeitamente a [[1,2], [3,4]] .

Conjugação e adjuntos são normalmente conceitos separados; dizer que "a conjugação dos elementos escalares" deve fazer o trabalho aqui me parece totalmente incorreto - o oposto de "sério" - exceto no caso especial observado abaixo.

Considere o caso de "vetores de coluna de 2 componentes" (| u⟩, | v⟩) de dois elementos | u⟩ e | v⟩ em algum espaço de Hilbert H sobre algum anel (digamos os números complexos ℂ), na notação de Dirac: "vetores de vetores." Este é um espaço de Hilbert de soma direta H⊕H com o produto interno natural ⟨(| u⟩, | v⟩), (| w⟩, | z⟩)⟩ = ⟨u | w⟩ + ⟨v | z⟩. O adjunto deve, portanto, produzir o operador linear que consiste no "vetor linha" (⟨u | ⟨v |), cujos elementos são eles próprios operadores lineares: o adjunto é"recursivo" . Isso é totalmente diferente do conjugado complexo, que produz um elemento do mesmo espaço de Hilbert H⊕H, induzido pela conjugação do anel subjacente.

Você está confundindo o assunto ao referir-se a vetores sobre quatérnios, etc. Se os "elementos" do vetor também são elementos do anel "complexificado" subjacente, então é claro que o adjunto e o conjugado desses elementos são a mesma coisa. No entanto, isso não se aplica a todos os espaços de produto direto.

Dito de outra forma, o tipo de objeto precisa indicar duas informações diferentes :

  • O anel escalar (daí o conjugado, que produz um elemento do mesmo espaço).
  • O produto interno (daí o adjunto, que produz um elemento do espaço dual ).

Para Vector{T<:Number} , devemos ler como indicando que o anel escalar é T (ou complex(T) para o espaço vetorial complexificado), e que o produto interno é o euclidiano usual .

Se você tem um vetor de vetores, então é um espaço de Hilbert de soma direta e o anel é a promoção dos anéis escalares dos vetores individuais, e o produto escalar é a soma dos produtos escalares dos elementos. (Se os escalares não podem ser promovidos ou os produtos escalares não podem ser somados, então não é um vetor / espaço de Hilbert.)

Se você tiver um escalar T<:Number , então conj(x::T) == adjoint(x::T) .

Então, se você tentar representar números complexos z::Complex{T} pelas matrizes 2x2 m(z)::Array{T,2} , então seu tipo indica um anel diferente T e um produto interno diferente em comparação com z e, portanto, você não deve esperar que conj ou adjoint forneçam resultados equivalentes.

Eu meio que entendo o que você está dizendo @felixrehren. Se o escalar for real, um octerniano pode ser considerado um espaço vetorial de 8 dimensões. Mas se o coeficiente escalar fosse um octerniano, então há uma base de dimensão 1 trivial que representa todos os octernos. Não tenho certeza se isso é diferente do que eu disse (ou do que eu quis dizer: sorria :), se tivermos um AbstractVector{T1} , pode ser isomórfico a um AbstractVector{T2} de uma dimensionalidade diferente ( length ). Mas T1 e T2 devem ser ambos anéis sob os operadores Julia + e * (e o isomorfismo pode preservar o comportamento de + mas não * , ou o produto interno ou adjunto).

@stevengj Sempre pensei na soma direta exatamente como meu BlockVector . Você tem duas bases incompletas (disjuntas). Dentro de cada base, você poderá formar superposições como c₁ | X₁⟩ + c₂ | X₂⟩ na primeira base ou c₃ | Y₁⟩ + c₄ | Y₂⟩ na segunda. A "soma direta" representa o espaço de estados que se parecem com (c₁ | X₁⟩ + c₂ | X₂⟩) + (c₃ | Y₁⟩ + c₄ | Y₂⟩). Parece-me que a única coisa especial que separa isso de uma base de dimensão quatro sobre os números complexos (ou seja, estados como c₁ | X₁⟩ + c₂ | X₂⟩ + c₃ | Y₁⟩ + c₄ | Y₂⟩) são os colchetes - para mim, parece notacional; a soma direta é apenas uma maneira conveniente de escrever isso no papel ou com matrizes em um computador (e pode ser útil, por exemplo, para nos deixar catalogar e aproveitar as simetrias, ou para dividir o problema (talvez para paralelizá-lo), etc. ) Neste exemplo, X⊕Y ainda é um espaço vetorial quadridimensional onde os escalares são complexos.

Isso é o que me faz querer eltype(v) == Complex{...} e length(v) == 4 vez de eltype(v) == Vector{Complex{...}} e length(v) == 2 . Isso me ajuda a ver e raciocinar sobre o anel subjacente e o espaço vetorial. Não é também este espaço "achatado" onde você pode fazer a transposição "global" e elementwise conj para calcular o adjunto?

(você recebeu duas postagens enquanto eu escrevia apenas uma, @stevengj : sorria :)

Claro, se preferirmos usar matrizes aninhadas como convenção para a soma direta (como fazemos agora) em vez de BlockArray , isso pode ser bom e consistente também! Podemos nos beneficiar de algumas funções extras para determinar o campo escalar (um tipo de eltipo recursivo) e qual pode ser a dimensionalidade aplainada efetiva (um tipo de tamanho recursivo) para tornar um pouco mais fácil raciocinar sobre que álgebra linear estamos fazendo.

Para ser claro, estou feliz com as duas abordagens e aprendi muito com essa discussão. Obrigado.

@andyferris , só porque dois espaços são isomórficos não significa que sejam o "mesmo" espaço, e discutir sobre qual deles é "realmente" o espaço vetorial (ou se eles são "realmente" o mesmo) é um atoleiro metafísico da qual nunca escaparemos. (Mas o caso de somas diretas de espaços vetoriais de dimensão infinita ilustra uma limitação do seu conceito "achatado" de uma soma direta como sendo "todos os elementos de um seguido por todos os elementos do outro".)

Novamente, não tenho certeza de qual é o seu ponto. Você está propondo seriamente que eltype(::Vector{Vector{Complex}}) em Julia deveria ser Complex , ou que length deveria retornar a soma dos comprimentos? Que todos em Julia deveriam ser forçados a adotar seu isomorfismo "achatado" para espaços de soma direta? Caso contrário, o adjoint deve ser recursivo.

E se você estiver "apenas tentando entender melhor, sem fazer um ponto específico", pode levá-lo para um fórum diferente? Esta questão é confusa o suficiente sem argumentos metafísicos sobre o significado dos espaços de soma direta.

só porque dois espaços são isomórficos não significa que eles sejam o "mesmo" espaço

Eu definitivamente não estava sugerindo isso.

Mais uma vez, não tenho certeza de qual é o seu ponto

Estou sugerindo seriamente que se você quiser fazer álgebra linear com um AbstractArray , então o eltype melhor ser um anel (suporte + , * e conj ) que exclui, por exemplo, eltypes de Vector porque [1,2] * [3,4] não funciona. E o length de um vetor representaria na verdade a dimensionalidade de sua álgebra linear. Sim, isso exclui espaços vetoriais infinitamente dimensionais, mas geralmente em Julia AbstractVector não pode ter tamanho infinito. Acho que isso tornaria mais fácil raciocinar sobre como implementar e usar a funcionalidade de álgebra linear em Julia. No entanto, os usuários teriam que denotar explicitamente uma soma direta por meio de BlockArray ou similar, em vez de usar estruturas de array aninhadas.

Esta é uma sugestão bastante interessante e tem desvantagens notáveis ​​(algumas que você mencionou), então não ficarei infeliz se dissermos "é uma boa ideia, mas não será prática". No entanto, também venho tentando apontar que a abordagem de matriz aninhada torna algumas coisas sobre a álgebra linear subjacente um pouco mais opacas.

Tudo isso parece diretamente relevante para o OP. Com uma abordagem aplainada forçada , abandonaríamos as transposições / adjuntos recursivos e, se eliminássemos as transposições / adjuntos recursivos, apenas estruturas planificadas seriam viáveis ​​para álgebra linear. Se não aplicamos uma abordagem nivelada, devemos manter um adjunto recursivo. Para mim, parecia uma decisão interligada.

Você está propondo seriamente que eltype(::Vector{Vector{Complex}}) em Julia deveria ser Complex , ou que length deveria retornar a soma dos comprimentos?

Não, desculpe, talvez o que eu escrevi lá não estivesse claro - eu estava apenas sugerindo que duas novas funções de conveniência para essa finalidade podem ser úteis em certas circunstâncias. Às vezes você pode querer zero ou one daquele anel, por exemplo.

Você está dizendo que todos em Julia deveriam ser forçados a adotar seu isomorfismo "achatado" para espaços de soma direta?

Eu estava sugerindo isso como uma possibilidade, sim. Não estou 100% convencido de que é a melhor ideia, mas acho que existem algumas vantagens (e algumas desvantagens).

Retornando ao reino das mudanças acionáveis ​​aqui, parece que a versão simplificada de @stevengj da minha proposta é o caminho a percorrer, ou seja:

  • Mantenha conj no estado em que se encontra (elemento a elemento).
  • Faça a' corresponder a adjoint(a) , e torná-lo recursivo sempre (e, portanto, falhar para arrays de strings etc onde adjoint não é definido para os elementos).
  • Faça a.' corresponder a transpose(a) e torne-o nunca recursivo (apenas permutedims ) e, portanto, ignore o tipo dos elementos.

Os principais "todos" reais que podem ser extraídos disso são:

  1. [] Renomeie ctranspose para adjoint .
  2. [] Altere transpose para não recursivo.
  3. [] Descubra como isso se encaixa nos vetores de linha.

Eu suspeito que depender de transposição recursiva é suficientemente raro que podemos apenas mudar isso no 1.0 e listá-lo como quebrando no NEWS. Alterar ctranspose para adjoint pode passar por uma depreciação normal (no entanto, fazemos isso para 1.0). Há mais alguma coisa que precisa ser feita?

@StefanKarpinski , também precisaremos fazer a alteração correspondente em RowVector . Uma possibilidade seria dividir RowVector em Transpose e Adjoint tipos para preguiçoso não recursivo transpose e preguiçoso recursivo adjoint , respectivamente, e para se livrar de Conj (preguiçoso conj ).

Mais algumas mudanças, pelo menos tangencialmente relacionadas

  • Alguns tipos de visualização para transpose e adjoint de AbstractMatrix
  • Tornar adjoint(::Diagonal) recursivo (sem dúvida, devemos corrigir este "bug" em Diagonal para transpose e ctranspose antes de Julia v0.6.0)
  • Use o tipo "escalar" adequado em coisas como: v = Vector{Vector{Float64}}(); dot(v,v) (atualmente um erro - ele tenta retornar zero(Vector{Float64}) )

OK, tenho tentado levar matrizes de bloco recursivas mais a sério e, para comprovar, também estou tentando melhorar o comportamento de Diagonal aqui (já existem alguns métodos que antecipam uma diagonal de bloco , como getindex , portanto, parece natural tornar a transposição recursiva neste caso).

Onde eu fico preso, e o que motivou diretamente todos os meus pontos de discussão acima, é como fazer operações de álgebra linear em tal estrutura, incluindo inv , det , expm , eig e assim por diante. Por exemplo, existe um método para det(::Diagonal{T}) where T que apenas obtém o produto de todos os elementos diagonais. Funciona brilhantemente para T <: Number e também funciona se todos os elementos forem matrizes quadradas do mesmo tamanho . (Claro, se eles são matrizes quadradas do mesmo tamanho, então os elementos formam um anel, e é tudo bastante sensato - também a transposição recursiva (editar: adjoint) é a coisa correta).

No entanto, se pensarmos em uma matriz de bloco diagonal geral Diagonal{Matrix{T}} , ou qualquer matriz de bloco Matrix{Matrix{T}} , então podemos geralmente falar de seu determinante como um escalar T . Ou seja, se o tamanho fosse (3 ⊕ 4) × (3 ⊕ 4) (uma matriz 2 × 2 com elementos diagonais que são matrizes de dimensões 3 × 3, 4 × 4 e elementos fora da diagonal correspondentes), det retornar o determinante da estrutura 7 × 7 "achatada", ou deveria simplesmente tentar multiplicar os elementos de 2 × 2 como eles são (e com erro, neste caso)?

(editar: eu alterei as dimensões acima para serem diferentes, o que deve evitar linguagem ambígua)

Não tenho problema em a' ser recursivo, mas pessoalmente acho a nova notação a.' extremamente confusa. Se você me mostrasse a sintaxe a.' eu diria, com base no meu modelo mental de Julia, que ela realiza transpose.(a) ou seja, transposição de elemento a elemento de a , e eu estaria completamente errado. Alternativamente, se você me mostrou que a' e a.' eram opções para uma transposição, e que uma delas também recursou elemento a elemento, eu diria que a.' deve ter o recursão elementwise, e eu estaria errado novamente.

Claro, meu modelo mental do que . significa está simplesmente errado neste caso. Mas eu suspeito que não sou o único que interpretaria essa sintaxe exatamente da maneira errada. Para esse fim, eu sugeriria usar algo diferente de .' para a transposição não recursiva.

@rdeits Receio que esta sintaxe derive do MATLAB. Isso se tornou um pouco infeliz, uma vez que a (maravilhosa) sintaxe de difusão ponto-chamada foi introduzida na v0.5.

Poderíamos forjar nosso próprio caminho separado do MATLAB e mudar isso - mas acredito que houve uma discussão separada em algum lugar sobre isso (alguém se lembra de onde?).

Ah, isso é lamentável. Obrigado!

Outra tarefa:

  • [] Corrija issymmetric e ishermitian para combinar transpose e adjoint - o primeiro de cada par sendo não recursivo, o último de cada par sendo recursivo.

A sintaxe do Matlab .' é definitivamente muito infeliz no contexto de nossa nova sintaxe . . Eu não seria contra mudar isso, mas precisamos de uma nova sintaxe para transpor, e não tenho certeza se temos uma disponível. Alguém tem alguma sugestão para transpor?

Vamos mover a discussão da sintaxe de transposição para aqui: https://github.com/JuliaLang/julia/issues/21037.

Não tenho nenhuma opinião forte sobre transpose / ctranspose / adjoint ser recursivo, mas prefiro não tratar A::Matrix{Matrix{T}} como matriz de bloco no sensação de um preguiçoso hvcat , que @andyferris parece implicar, pelo menos em parte. Isto é, se os elementos de A fossem todos matrizes quadradas do mesmo tamanho (isto é, formam um anel), eu esperaria que det(A) retornasse um quadrado desse tamanho novamente. Se eles forem retangulares ou de tamanhos diferentes, esperaria um erro.

Dito isso, um tipo de matriz de bloco / gato preguiçoso pode ser útil, mas deve ir até o fim e também definir, por exemplo, getindex para trabalhar nos dados achatados. Mas eu definitivamente não gostaria de ver este conceito absorvido por Matrix ou qualquer outro tipo de matriz existente como Diagonal .

Isso é um alívio, @martinholters. O que me deixou em pânico no início deste tópico foi a ideia de que deveríamos de alguma forma ser capazes de aplicar toda a estrutura de álgebra linear de Julia a Matrix{Matrix} para matrizes de bloco arbitrárias (onde as submatrizes têm tamanhos diferentes).

O que eu estava defendendo com o "achatamento" não é fazer nenhuma introspecção extravagante do que são os elementos, e apenas tratar os elementos por seus próprios méritos como elementos de um anel. No entanto, ctranspose / adjoint recursivo expande isso para permitir elementos que agem como operadores lineares, o que parece correto e útil. (O outro caso são os elementos de um vetor que agem como um vetor, onde um adjunto recursivo também está correto).

Para voltar um pouco mais - qual foi a motivação original para remover o comportamento não operacional padrão de transpose(x) = x e ctranpsose(x) = conj(x) ? Isso sempre me pareceu muito útil.

Para voltar um pouco mais - qual foi a motivação original para remover o comportamento não operacional padrão para transpor (x) = xe ctranpsose (x) = conj (x)? Isso sempre me pareceu muito útil.

Isso foi motivado por tipos de operadores lineares personalizados (que não podem ser subtipos de AbstractArray) que falharam em especializar ctranspose . Isso significa que eles herdaram o comportamento no-op errado de um fallback. Tentamos estruturar o despacho de forma que os substitutos nunca sejam silenciosamente incorretos (mas eles podem ter uma complexidade pessimista). https://github.com/JuliaLang/julia/issues/13171

Parece que temos duas opções - em ambas, transpose torna-se não recursivo, caso contrário:

  1. ctranspose não recursivo.
  2. ctranspose recursivo.

@stevengj , @jiahao , @andreasnoack - quais são suas preferências aqui? Outras?

Tenho pensado muito sobre isso desde a palestra de

Para mim, ainda acho que a álgebra linear deve ser definida em relação a um campo "escalar" como um tipo de elemento T . Se T for um campo (suporta + , * e conj (também - , / , .. .)) então não vejo por que os métodos em Base.LinAlg deveriam falhar.

OTOH é bastante comum (e válido) falar sobre pegar a transposta de uma matriz de bloco, por exemplo. Acho que podemos aprender com a teoria dos tipos "matemática" aqui, que por exemplo tenta lidar com declarações estranhas que surgem da discussão de conjuntos de conjuntos por ter conjuntos de escalares de "primeira ordem", conjuntos de escalares de "segunda ordem", terceiro ordenar "conjuntos de conjuntos de conjuntos de escalares e assim por diante. Acho que temos o mesmo problema (e oportunidade) aqui quando lidamos com arrays de arrays - podemos potencialmente usar o sistema de tipos de Julia para descrever arrays de "primeira ordem" de escalares "verdadeiros", arrays de "segunda ordem" de arrays de escalares, etc. . Acho que as longas discussões entre @stevengj , eu e outros resultaram do fato de que, sim, você pode criar um conjunto autoconsistente de operações em matrizes de "ordem" arbitrária e, nesta estrutura (c) transpor é clara e corretamente recursiva, mas, no entanto, você não precisa fazer isso. Como a palestra de Jiahao apontou claramente, linguagens de programação / frameworks fazem escolhas semânticas sobre distinguir escalares de matrizes (ou não), distinguir vetores de matrizes (ou não) e assim por diante, e eu acho que essa é uma escolha relacionada que podemos fazer.

Para mim, o ponto crucial aqui é que para fazer o array de "ordem arbitrária" funcionar, você precisa ensinar aos seus "escalares" algumas operações que normalmente são definidas apenas em vetores e matrizes. O método Julia atual que faz isso é transpose(x::Number) = x . No entanto, eu realmente preferiria que estendêssemos nossa distinção semântica entre arrays e escalares removendo esse método - e acho que pode ser bastante viável.

O próximo passo seria ensinar Base.LinAlg a diferença entre uma matriz de primeira ordem e uma matriz de segunda ordem e assim por diante. Pensei em duas opções viáveis, pode haver mais, realmente não sei:

  • Ter algum tipo de característica opt-in para "arrayness", de modo que transpose(::AbstractMatrix{T}) seja recursivo quando T é um AbstractMatOrVec , e não quando T é um Number e torná-lo de alguma forma configurável (opt-in, opt-out, qualquer outro). (De certa forma, isso era e atualmente é feito controlando o comportamento do método transpose para os elementos, mas acho que é mais limpo controlar o comportamento do método transpose método (externo ) array).
  • Alternativamente, afirme que a maioria de AbstractArray s incluindo Array s são de primeira ordem (seus elementos são campos escalares) e use um tipo AbstractArray (digamos NestedArray ) para agrupar matrizes e "marcar" que seus elementos devem ser tratados como matrizes, não como escalares. Tentei brincar um pouco com isso, mas parecia que a opção acima provavelmente é menos trabalhosa para os usuários.

Acho que Julia faz uma forte separação semântica entre arrays e escalares, e que a situação atual parece (para mim) um pouco inconsistente com isso, já que escalares tiveram que ser "ensinados" sobre a propriedade de array transpose . Um usuário pode dizer claramente que Matrix{Float64} é uma matriz de escalares (matriz de primeira ordem) e Matrix{Matrix{Float64}} é uma matriz de bloco (matriz de segunda ordem). Pode ser mais consistente para nós usar o sistema de tipos ou características para determinar se um array é de "primeira ordem" ou qualquer outra coisa. Eu também acho que a primeira opção que listei tornaria Julia mais amigável (para que eu possa fazer ["abc", "def"].' ) e ainda manter a flexibilidade para fazer alguma coisa padrão sensata / útil com matrizes de bloco.

Desculpe insistir tanto, mas depois de falar com @jiahao na JuliaCon, eu senti que poderíamos fazer um pouco melhor aqui. Tenho certeza de que podemos fazer as outras opções mencionadas por @StefanKarpinski acima "funcionarem", mas para mim estaria "funcionando" exatamente como a álgebra matricial / vetorial estava "funcionando" antes da introdução de RowVector .

O TLDR era - não faça ( c ) transpose ser recursivo ou não - deixe fazer as duas coisas (ou seja, ser configurável).

Esses são bons pontos, mas quero apenas salientar que quase todas (se não todas) as reclamações sobre (c)transpose serem recursivas estão relacionadas ao uso não matemático de ' e .' quando remodelar, por exemplo, Vector{String} e Vector{PyObject} em matrizes 1xn. É fácil corrigir tipo por tipo, mas posso ver que é irritante. No entanto, o pensamento era que a definição recursiva era a matematicamente correta e que "correta, mas irritante em muitos casos" era melhor do que "conveniente, mas errada em casos raros".

Uma possível solução poderia ser sua sugestão no primeiro marcador, ou seja, ter apenas transposições recursivas para elementos do tipo array. Acredito que T<:AbstractVecOrMat cobriria a maioria dos casos, alterando o status para "conveniente, mas errado em casos muito raros". O motivo pelo qual ainda pode estar errado é que alguns tipos do tipo operador não se encaixariam na categoria AbstractMatrix porque a interface AbstractArray é principalmente sobre semântica de array ( getindex ) , não álgebra linear.

@andyferris , o adjunto e o dual de um escalar estão perfeitamente bem definidos e ctranspose(x::Number) = conj(x) está correto.

Minha impressão é que a maioria dos usos de transpose são "não matemáticos" e a maioria dos usos de ctranspose (ou seja, onde o comportamento de conjugação é desejado) são matemáticos (uma vez que não há outra razão para conjugar ) Portanto, eu tenderia a ser a favor de transpose recursivos e ctranspose recursivos.

Pessoalmente, acho que tentar olhar para matrizes de bloco como aninhadas Arrays se torna complicado pelas razões aqui e talvez seja melhor ter um tipo dedicado à la https://github.com/KristofferC/BlockArrays.jl para esta.

Parece bom, @KristofferC.

Há um ponto muito válido que @stevengj faz acima, e é que às vezes seus elementos podem ser vetores ou operadores lineares no sentido matemático, mas não AbstractArray s. Definitivamente precisamos de uma maneira de fazer a transposição recursiva (c) para eles - não tenho certeza se você pensou sobre isso ou não, mas pensei em mencioná-lo.

Destaques de uma conversa slack / # linalg sobre este tópico. Recapitula alguns dos tópicos acima. Foca na semântica, evita a ortografia. (Obrigado a todos que participaram dessa conversa! :))

Existem três operações semanticamente distintas:
1) "adjunto matemático" (recursivo e preguiçoso)
2) "transposição matemática" (idealmente recursiva e preguiçosa)
3) "transposição estrutural" (idealmente não recursiva e? Ansiosa?)

Situação atual: "adjunto matemático" mapeia para ctranspose , "transposição matemática" para transpose e "transposição estrutural" para permutedims(C, (2, 1)) para C bidimensional e reshape(C, 1, length(C)) para C unidimensionais. O problema: "transposição estrutural" é uma operação comum, e a história permutedims / reshape é um tanto confusa / não natural / irritante na prática.

Como isso surgiu: anteriormente, "transposição estrutural" era confundida com "adjunto matemático" / "transposição matemática" por meio de alternativas não operacionais genéricas como transpose(x::Any) = x , ctranspose(x::Any) = conj(x) e conj(x::Any) = x . Esses fallbacks fizeram [c]transpose e os operadores postfix associados ' / .' servirem para "transposição estrutural" na maioria dos casos. Ótimo. Mas eles também fizeram operações envolvendo [c]transpose em alguns tipos numéricos definidos pelo usuário falharem silenciosamente (retornando resultados incorretos) sem definição de [c]transpose especializações para esses tipos. Ai. Conseqüentemente, esses substitutos não operacionais genéricos foram removidos, gerando a situação presente.

A questão é o que fazer agora.

Resultado ideal: forneça um encantamento unificado, natural e compacto para "transposição estrutural". Simultaneamente apóie adjunto matemático semanticamente correto e transponha. Evite introduzir casos complicados em uso ou implementação.

Existem duas propostas amplas:

(1) Fornecer adjunto matemático, transposição matemática e transposição estrutural como três operações sintática e semanticamente distintas. Vantagens: Faz tudo funcionar, separa conceitos de forma clara e evita casos complicados. Desvantagens: Três operações para explicar e implementar.

(2) Calce as três operações em duas. Existem três formas desta proposta:

(2a) Faça transpose semanticamente "transposta estrutural", servindo também como "transposta matemática" em casos comuns. Positivo: Duas operações. Funciona conforme o esperado para contêineres com elementos escalares inequivocamente. Desvantagens: qualquer pessoa que espere uma semântica de "transposição matemática" receberá silenciosamente resultados incorretos em objetos mais complexos do que contêineres com elementos escalares inequívocos. Se o usuário perceber esse problema, alcançar a semântica de "transposição matemática" requer a definição de novos tipos e / ou métodos.

(2b) Apresente um traço indicando a "matemática" de um tipo. Ao aplicar adjoint / transpor a um objeto, se os tipos de contêiner / elemento forem "matemáticos", recurse; se não, não recurse. Positivo: Duas operações. Pode abranger uma variedade de casos comuns. Desvantagens: sofre do mesmo problema que os substitutos genéricos no-op [c]transpose , ou seja, podem produzir resultados incorretos silenciosamente para tipos numéricos definidos pelo usuário sem as definições de características necessárias. Não permite a aplicação de transposição estrutural para um tipo "matemático" ou transposta matemática para um tipo "não matemático". Não está claro como decidir se um objeto é "matemático" em todos os casos. (Por exemplo, e se o tipo de elemento for abstrato? A decisão recursiva / não recursiva é então runtime e elementwise, ou runtime e requer uma primeira varredura sobre todos os elementos no contêiner para verificar seu tipo coletivo?)

(2c) Mantenha adjoint ( ctranspose ) "adjunto matemático" e transpose "transposição matemática" e introduza métodos especializados (fallback não genérico) para adjoint / transpose para tipos escalares não numéricos (por exemplo, adjoint(s::AbstractString) = s ). Positivo: Duas operações. Semântica matemática correta. Desvantagens: Requer definição fragmentada de adjoint / transpose para tipos não numéricos, impedindo a programação genérica. Não permite a aplicação de transposição estrutural para tipos "matemáticos".

As propostas (1) e (2a) tiveram um apoio substancialmente mais amplo do que (2b) e (2c).

Encontre mais discussão em # 19344, # 21037, # 13171 e slack / # linalg. Melhor!

Obrigado pelo bom artigo!

Eu gostaria de colocar outra possibilidade na mesa que iria bem com a opção 1: Ter diferentes tipos de contêineres para matrizes matemáticas e dados tabulares. Então, o significado de A' poderia ser decidido pelo tipo de A (nota: não é o tipo de elemento discutido acima). Os contras aqui são que isso pode exigir muitos convert s entre os dois (não é?) E, claro, seria altamente prejudicial. Admito que estou mais do que cético se os benefícios justificariam essa interrupção, mas ainda assim gostaria de mencioná-lo.

Obrigado, excelente redação. Eu voto em (1).

Ótimo escrever. Observe que para (1) as grafias podem ser:

  • Adjunto: a' (recursivo, preguiçoso)
  • Transposição matemática: conj(a') (recursiva, preguiçosa)
  • Transposição estrutural: a.' (não recursiva, ansiosa)

Portanto, não seria necessário introduzir nenhum novo operador - a transposição matemática seria apenas uma composição (preguiçosa e, portanto, eficiente) de adjunto e conjugação. O maior problema é que ele torna dois operadores de aparência semelhante, isto é, o postfix ' e .' , semanticamente distintos. No entanto, eu diria que, no código genérico mais correto, se alguém usou ' ou .' é um indicador 99% preciso de se eles queriam dizer "me dê o adjunto desta coisa" ou "troca as dimensões dessa coisa ". Além disso, nos casos em que alguém usou ' e realmente quis dizer "trocar as dimensões desta coisa", seu código já estaria incorreto para qualquer matriz com elementos cujo escalar adjunto não seja trivial, por exemplo, números complexos. Nos poucos casos restantes onde alguém realmente quis dizer "dê-me o conjugado do adjunto deste", eu diria que escrever conj(a') torna esse significado muito mais claro, pois na prática as pessoas realmente usam a.' para significa "trocar as dimensões de a ".

Trabalhar todos os tipos genéricos subjacentes que precisaríamos para isso ainda está para ser determinado, mas @andyferris e @andreasnoack já têm algumas idéias sobre o assunto e parece possível.

Achei que também deveria atualizar, já que essa discussão continuou em outro lugar. Admito que havia uma coisa (óbvia?) Sobre essa proposta que eu havia esquecido completamente que presumi que continuaríamos a usar .' para álgebra linear, mas eu deveria ter percebido que esse não é o caso !

Por exemplo, vector.' não precisará mais ser RowVector (já que RowVector é um conceito de álgebra linear, nossa primeira tentativa de um vetor "duplo") - pode simplesmente seja um Matrix . Quando eu quero a "transposição não conjugada" na álgebra linear, o que estou realmente fazendo é pegar conj(adjoint(a)) , e é isso que vamos usar e recomendar que todos os usuários de álgebra linear usem (até agora eu tenho tinha um hábito "ruim" de longa data desde o MATLAB de usar apenas a.' vez de a' para transpor qualquer matriz (ou vetor) que eu sabia ser real, quando o que eu realmente queria era adjoint (ou dual) - a mudança no nome será uma grande ajuda aqui).

Também vou apontar brevemente que isso deixa em aberto um espaço interessante. Anteriormente, vector' e vector.' tinham que satisfazer as necessidades duplas de fazer uma "transposição de dados" e tomar algo como o vetor dual. Agora que .' é para manipular tamanhos de array e ' é um conceito de álgebra linear, podemos ser capazes de transformar RowVector em 1D DualVector ou outra coisa inteiramente. (talvez não devêssemos discutir isso aqui - se alguém tem apetite para isso, vamos fazer uma questão separada).

Por fim, copiarei um plano de ação proposto do Slack:

1) mova transpose de LinAlg para Base e adicione alertas (apenas 0,7) sobre ele não mais gerar RowVector ou ser recursivo (se for possível)
2) renomear ctranspose para adjoint qualquer lugar
3) certifique-se de que Vector , ConjVector e RowVector trabalham com combinações de ' e conj e * , possivelmente renomear RowVector . (aqui também tornamos conj(vector) preguiçoso).
4) introduzir um adjunto de matriz preguiçoso que também interage bem com conj e ConjMatrix
5) remova A_mul_Bc etc. Renomeie A_mul_B! para mul! (ou *! ).
6) lucro

@stevengj escreveu:

@andyferris , o adjunto e o dual de um escalar estão perfeitamente bem definidos e ctranspose(x::Number) = conj(x) está correto.

Para registro, eu definitivamente concordo com isso.

Minha impressão é que a maioria dos usos de transpor são "não matemáticos", e a maioria dos usos de ctranspor (...) são matemáticos

Portanto, a ideia será formalizar isto: todos os usos de transpose tornam-se "não matemáticos" e o único uso de adjoint será "matemático".

Por exemplo, vector.' não precisará mais ser RowVector (uma vez que RowVector é um conceito de álgebra linear, nossa primeira tentativa de um vetor "duplo") - pode simplesmente seja um Matrix .

No entanto, ainda queremos que .' seja preguiçoso. por exemplo, X .= f.(x, y.') ainda deve estar sem alocação.

Aqui está uma pergunta: Como devemos implementar issymmetric(::AbstractMatrix{<:AbstractMatrix}) ? Seria uma verificação não recursiva, para corresponder a transpose ? Estou olhando para a implementação; ele assume que os elementos podem transpose . OTOH parece bastante válido verificar se issymmetric(::Matrix{String}) ...

Atualmente não é recursivo se envolvido em 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

Sim, estou trabalhando na criação de um RP para isso e há muitos desses tipos de inconsistências. (Eu encontrei aquele não há muito tempo, enquanto procurava por cada instância de transpose , adjoint e conj no código da matriz).

A menos que seja instruído de outra forma, implementarei o comportamento de issymmetric(a) == (a == a.') e ishermitian(a) == (a == a') . Estes parecem bastante intuitivos em si mesmos e AFAICT do uso existente de issymmetric é para Number tipos de elemento (ou então freqüentemente tem outros erros / suposições que não farão muito sentido para matrizes aninhadas) .

(A implementação alternativa é issymmetric(a) == (a == conj(adjoint(a))) ... não é tão "bonita" nem funcionará para matrizes de "dados" (de String , etc), mas coincide com matrizes de Number )

A menos que seja instruído de outra forma, implementarei um comportamento de forma que issymmetric(a) == (a == a.') e ishermitian(a) == (a == a')

Parece ser a solução certa para mim.

Atualização: Mudei de ideia. Simétrico é provavelmente principalmente para álgebra linear

Apenas uma pequena sugestão: geralmente é útil fazer uma soma sobre o produto de dois vetores ( dotu ) e x.’y retornar um escalar é uma maneira conveniente de fazer isso. Portanto, eu seria a favor de não devolver Matrix de transpose(::Vector)

Sim, será um RowVector então você deve obter um escalar. (Observe que a transposição não será recursiva).

Desculpe pelo comentário possivelmente tangencial, mas muitas pessoas neste tópico continuam falando sobre um "anel de escalares do espaço vetorial". Por definição, os escalares de um espaço vetorial devem formar um campo, não qualquer anel. Uma estrutura algébrica que é muito semelhante a um espaço vetorial, mas cujos escalares formam apenas um anel em vez de um campo é chamada de "módulo", mas acho que os módulos são um pouco esotéricos demais para serem manipulados na Base ... er. .. módulo.

Como oferecemos suporte a matrizes inteiras, oferecemos suporte efetivo a módulos, não apenas a espaços vetoriais. Claro, também poderíamos ver arrays inteiros como incorporados em arrays de ponto flutuante comportamentalmente, então eles são uma representação parcial de um espaço vetorial. Mas também podemos criar matrizes de inteiros modulares (por exemplo), e com um módulo não primo estaríamos trabalhando com um anel que não está naturalmente embutido em nenhum campo. Resumindo, queremos realmente considerar os módulos em geral, em vez de apenas espaços vetoriais, mas não acho que haja qualquer diferença significativa para nossos propósitos (geralmente estamos falando apenas de + e * ) então "espaço vetorial" serve como uma abreviação mais familiar para "módulo" para nossos propósitos.

Sim, Julia certamente oferece (e deve) suportar operações algébricas em módulos gerais, bem como em espaços vetoriais verdadeiros. Mas, pelo que entendi, a filosofia geral da comunidade é que as funções em Base devem ser projetadas com "rotinas de álgebra linear genérica 'comum" em mente - então, por exemplo, cálculos de álgebra linear exata em matrizes inteiras não não pertence a Base - portanto, ao tomar decisões de design fundamentais, devemos apenas assumir que os escalares formam um campo. (Embora, como você disse, praticamente falando, isso realmente não importa.)

Crossposting https://github.com/JuliaLang/julia/pull/23424#issuecomment -346678279

Agradeço muito o esforço para mover os números 5332 e 20978 adiante que esta solicitação pull representa, e estou a bordo da maior parte do plano associado. Eu também gostaria de estar de acordo com as opções de terminologia e o impacto posterior que essa solicitação de pull avança e, assim, tentar regularmente me convencer de que essas escolhas geram o melhor conjunto de compensações disponível.

Cada vez que tento me convencer dessa posição, fico encalhado no mesmo conjunto de dúvidas. Uma dessas dúvidas é a complexidade de implementação substancial que esta escolha impõe a LinAlg : transpor e inverter array força LinAlg para lidar com ambos, exigindo que LinAlg suporte um conjunto muito maior combinações de tipo em operações comuns.

Para ilustrar, considere mul(A, B) onde A e B são nus, embalados por adjoint, embalados por transposição ou virados por array Matrix s. Sem conflating transposição e gama-flip, A pode ser um Matrix , um embrulhado-adjuntas Matrix , ou uma transposição embrulhado Matrix (e igualmente B ). Portanto, mul(A, B) precisa do suporte de nove combinações de tipo. Mas conflating transposição e gama-flip, A pode ser um Matrix , um embrulhado-adjuntas Matrix , um embrulhado-transposta Matrix , ou um array- flip-wrap Matrix (e da mesma forma B ). Portanto, agora mul(A, B) precisa suportar dezesseis combinações de tipos.

Esse problema piora exponencialmente com o número de argumentos. Por exemplo, sem conflação mul!(C, A, B) precisa suportar vinte e sete combinações de tipo, enquanto com conflação mul!(C, A, B) precisa suportar sessenta e quatro combinações de tipo. E, claro, adicionar Vector s e não- Matrix tipos de matriz / operador à mistura complica ainda mais as coisas.

Parece que vale a pena considerar esse efeito colateral antes de prosseguir com essa mudança. Melhor!

Esta postagem consolida / analisa discussões recentes sobre github, folga e triagem e analisa possíveis caminhos a seguir. (O sucessor espiritual de https://github.com/JuliaLang/julia/issues/20978#issuecomment-315902532 nasceu pensando em como chegar a 1.0.)

Três operações semanticamente distintas estão em questão:

  • adjunto (algébrico linear, recursivo, idealmente preguiçoso por padrão)
  • transpor (algébrico linear, recursivo, idealmente preguiçoso por padrão)
  • array-flip (abstract-array-ic, não recursivo, idealmente? preguiçoso? por padrão)

Status dessas operações no mestre

  • Adjoint é chamado de adjoint / ' (mas é ansioso além de ' -expressões envolventes especialmente reduzidas para A[c|t]_(mul|rdiv|ldiv)_B[c|t][!] chamadas, que evitam adjoints / transposes ansiosas intermediárias) .

  • A transposição é chamada de transpose / .' (com a mesma ressalva de adjoint ).

  • Array-flip é chamado de permutedims(C, (2, 1)) para C bidimensional e reshape(C, 1, length(C)) para C unidimensional.

As questões relevantes

  1. 5332: A redução especial para A[c|t]_(mul|rdiv|ldiv)_B[c|t] e a coleção combinatória associada de nomes de métodos devem desaparecer em 1.0. Remover esse abaixamento especial / os nomes de métodos associados requer adjunto e transposição preguiçosos.

  2. 13171: Adjoint (née ctranspose ) e transpose foram anteriormente confundidos com array-flip via fallbacks não operacionais genéricos como transpose(x::Any) = x , ctranspose(x::Any) = conj(x) e conj(x::Any) = x . Esses fallbacks fizeram as operações envolvendo [c]transpose em alguns tipos numéricos definidos pelo usuário falharem silenciosamente (retornando resultados incorretos) na ausência de [c]transpose especializações para esses tipos. O retorno silencioso de resultados incorretos é uma má notícia, portanto, esses substitutos foram removidos (# 17075). Não introduzir mais falhas silenciosas seria ótimo.

  3. 17075, # 17374, # 19205: A remoção dos fallbacks anteriores tornou o array-flip menos conveniente. E junto com (desculpas, minha culpa) menos que ótimos avisos de depreciação associados, essa remoção (temporariamente?) Causou reclamações. Um encantamento mais conveniente para a inversão do array seria bom.

  4. 21037: Remover a agora confusa sintaxe .' para 1.0 seria ótimo. O rebaixamento especial mencionado acima deve ser removido para permitir essa alteração.

Objetivos de design para resolver esses problemas

Remova o abaixamento especial e os nomes de método associados, evite a introdução de falhas silenciosas, forneça encantamentos intuitivos e convenientes para transpor e inverter o array e remover .' . Alcance o anterior com o mínimo de complexidade adicional e quebra.

Propostas de design

O campo foi reduzido a duas propostas de design:

  1. Chame adjoint adjoint , transponha conjadjoint ("conjugate adjoint") e inverta o array transpose . adjoint e conjadjoint vivem em LinAlg , e transpose vivem em Base .

  2. Chame o adjunto adjoint , transponha transpose e inverta o array flip . adjoint e transpose vivem em LinAlg , e flip vivem em Base .

À primeira vista, essas propostas parecem apenas superficialmente diferentes. Mas um exame mais aprofundado revela profundas diferenças práticas. Evitando a discussão dos méritos superficiais relativos desses esquemas de nomenclatura, vamos dar uma olhada nessas profundas diferenças práticas.

Diferenças, visão de alto nível

  1. Complexidade:

    A proposta um, ao chamar array-flip transpose , força LinAlg a lidar com array-flip, além de transpor e adjunto. Conseqüentemente, LinAlg deve expandir substancialmente o conjunto de combinações de tipo suportadas por operações comuns; https://github.com/JuliaLang/julia/pull/23424#issuecomment -346678279 ilustra essa complexidade adicional por meio de exemplo, e a discussão a seguir afirma implicitamente a existência dessa complexidade adicional.

    A proposta dois requer suporte de LinAlg apenas transpor e adjunto, o que LinAlg exige agora.

  2. Quebra:

    A proposta um altera a semântica básica das operações existentes: transpose torna-se inverter array em vez de transpor, e todas as funcionalidades relacionadas a transpose devem ser alteradas de acordo. (Por exemplo, todas as operações de multiplicação e divisão esquerda / direita em LinAlg associadas ao nome transpose exigiriam revisão semântica.) Dependendo de como essa mudança é realizada, essa mudança causa uma quebra silenciosa onde quer que a semântica presente seja invocada (propositalmente ou inadvertidamente).

    A proposta dois retém a semântica básica de todas as operações existentes.

  3. Acoplamento:

    A proposta um traz um conceito algébrico linear (transpor) em Base e um conceito de matriz abstrata (inverter matriz) em LinAlg , acoplando fortemente Base e LinAlg .

    A proposta dois separa claramente as coisas de array abstrato e álgebra linear, permitindo que o primeiro viva apenas na base e o último apenas em LinAlg , sem nenhum novo acoplamento.

  4. Falha silenciosa vs. alta:

    A proposta um leva a um resultado silenciosamente incorreto quando se chama transpose esperando a semântica de transposição (mas, em vez disso, obtém a semântica de inversão de matriz).

    O análogo na proposta dois está chamando transpose em uma matriz não numérica esperando semântica de inversão de matriz. Nesse caso, transpose pode gerar um erro apontando o usuário para flip .

  5. .' : O argumento principal para não depreciar .' é o comprimento de transpose . A proposta um substitui .' pelos nomes transpose e conjadjoint , o que não melhora essa situação. Em contraste, a proposta dois fornece os nomes flip e transpose , melhorando essa situação.

Diferenças nos caminhos para 1.0

O que chegar a 1.0 leva em cada proposta? O caminho para 1.0 é mais simples na proposta dois, então vamos começar por aí.

O caminho para 1.0 na proposta dois

  1. Introduza os tipos de wrapper adjunto e transposto preguiçosos em LinAlg , digamos Adjoint e Transpose . Introduza mul[!] / ldiv[!] / rdiv[!] métodos despachando nesses tipos de invólucro e contendo o código de métodos A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] . Reimplemente os últimos métodos como pequenos filhos dos métodos anteriores.

    Esta etapa não quebra nada e ativa imediatamente a remoção da redução especial e a depreciação de .' :

  2. Remova a redução especial que rende A[c|t]_{mul|ldiv|rdiv}_B[c|t] , em vez de simplesmente reduzir ' / .' para Adjoint / Transpose ; expressões anteriormente especialmente rebaixadas gerando A[c|t]_{mul|ldiv|rdiv}_B[c|t] chamadas e, em vez disso, tornam-se mul / ldiv / rdiv chamadas equivalentes. Substituir A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] para os métodos mul[!] / ldiv[!] / rdiv[!] . Suspensão de .' .

    Essas etapas podem ser feitas em 0,7. Eles quebram apenas duas coisas: (1) O código que depende do abaixamento especial para atingir A[c|t]_{mul|ldiv|rdiv}_B[c|t] métodos para tipos não Base / LinAlg será quebrado. Esse código emitirá MethodError s explícitos, indicando para que os novos rendimentos de redução / para onde o código quebrado precisa ser migrado. (2) O código que depende de ' s / .' s isolados se comportando estritamente ansiosamente será quebrado. O modo de falha comum também deve ser MethodError s explícito. Ao redor, a quebra é confinada e alta.

    E é isso para as alterações estritamente necessárias para 1.0.

    Nesse ponto, Adjoint(A) / Transpose(A) produziria adjunto preguiçoso e transporia, e adjoint(A) / transpose(A) resultaria em adjunto e transposição preguiçosos. Os últimos nomes podem permanecer indefinidamente ou, se desejado, ser substituídos por outra grafia em 0.7, por exemplo eagereval(Adjoint(A)) / eagereval(Transpose(A)) modulo spelling of eagereval ou eageradjoint(A) / eagertranspose(A) . No caso de suspensão de uso, adjoint / transpose poderia então ser reaproveitado em 1.0 (embora com Adjoint(A) / Transpose(A) ao redor, não tenho certeza se isso seria necessário).

    Finalmente...

  3. Apresente flip e / ou Flip em Base . Por ser um acréscimo de recurso, essa alteração pode acontecer no 1.x se necessário.

O caminho para 1.0 na proposta um

Que tal a proposta um? A seguir descreve dois caminhos possíveis. O primeiro caminho consolida mudanças em 0,7, mas envolve quebra silenciosa. O segundo caminho evita a quebra silenciosa, mas envolve mais mudanças 0,7-> 1,0. Ambos os contornos evoluem continuamente a base de código; equivalentes menos contínuos podem consolidar / evitar algum trabalho / rotatividade, mas provavelmente seriam mais desafiadores e sujeitos a erros. O trabalho em andamento aparentemente segue o primeiro caminho.

Primeiro caminho sob proposta um (com quebra silenciosa)
  1. Altere a semântica de transpose de transpose para array-flip e, necessariamente, também a semântica de todas as funcionalidades relacionadas a transpose . Por exemplo, todos os métodos A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] começando com At_... ou terminando ..._Bt[!] potencialmente precisam de revisão semântica. Também deve alterar, por exemplo, as definições e o comportamento de Symmetric / issymmetric . Mova transpose em si para Base .

    Essas mudanças estariam silenciosa e amplamente interrompendo.

  2. Apresente conjadjoint em LinAlg . Esta etapa requer a restauração de todos os métodos tocados na etapa anterior, mas em sua forma semântica original, e agora com nomes diferentes associados a conjadjoint (digamos Aca_... e ..._Bca[!] nomes) . Também requer a adição de métodos para as combinações de tipo adicionais que suportam simultaneamente array-flip (agora transpose ), transpor (agora conjadjoint ) e adjunto em LinAlg requer (por exemplo o ca variantes entre A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] ).

  3. Apresente o adjunto preguiçoso e transponha (chamado conjadjoint ) em LinAlg , digamos Adjoint e ConjAdjoint . Introduza um tipo de wrapper de array-flip lento (chamado transpose ) em Base , digamos Transpose . Apresente mul[!] / ldiv[!] / rdiv[!] métodos despachados nesses tipos de invólucro e contendo o código de métodos A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] . Reimplemente os últimos métodos como pequenos filhos dos métodos anteriores.

  4. Remova a redução especial que rende A[c|t]_{mul|ldiv|rdiv}_B[c|t] , em vez de simplesmente reduzir ' / .' para Adjoint / Transpose ; expressões anteriormente especialmente rebaixadas que rendem A[c|t]_{mul|ldiv|rdiv}_B[c|t] chamadas e, em vez disso, tornam-se mul / ldiv / rdiv chamadas equivalentes (lembre-se de que a semântica terá mudado silenciosamente). Substituir A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] para os métodos mul[!] / ldiv[!] / rdiv[!] . De forma semelhante, remova os métodos Aca_... / ...Bca[!] introduzidos recentemente em favor de mul[!] / ldiv[!] / rdiv[!] equivalentes. Descontinue .' .

Essas mudanças precisariam ir em 0,7. As mudanças semânticas nas operações existentes resultariam em uma quebra ampla e silenciosa. A remoção de rebaixamento especial produziria a mesma quebra confinada e barulhenta descrita acima.

Neste ponto Adjoint(A) / Transpose(A) / ConjAdjoint(A) produziria respectivamente adjunto preguiçoso, virada de matriz e transpor, e adjoint(A) / transpose(A) / conjadjoint(A) resultaria respectivamente em adjoint, array-flip e transpor ansiosos. Os últimos nomes podem permanecer indefinidamente ou, se desejado, ser substituídos por alguma outra grafia também em 0.7 (ref. Acima).

Pode-se introduzir ConjAdjoint no início deste processo para consolidar / evitar algum trabalho / rotatividade, embora seja provável que essa abordagem seja mais desafiadora e sujeita a erros.

Segundo caminho sob a proposta um (evitando quebra silenciosa)
  1. Apresente conjadjoint ansiosos em LinAlg . Migre todas as funcionalidades e métodos atualmente associados a transpose (incluindo, por exemplo, A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] métodos envolvendo t ) para conjadjoint e nomes derivados. Torne todos os nomes relacionados a transpose filhos curtos dos novos conjadjoint equivalentes. Substituir todos os nomes relacionados a transpose para conjadjoint equivalentes.

  2. Apresente o adjunto preguiçoso e transponha (chamados conjadjoint ) tipos de invólucro em LinAlg , digamos Adjoint e ConjAdjoint . Introduza mul[!] / ldiv[!] / rdiv[!] métodos despachando nesses tipos de invólucro e contendo o código de métodos A[c|ca]_{mul|ldiv|rdiv}_B[c|ca][!] . Reimplemente os últimos métodos como pequenos filhos dos métodos anteriores.

  3. Apresente um tipo de wrapper array-flip lento em Base , chamado Transpose (um tanto confuso, já que transpose está obsoleto para conjadjoint com semântica de transposição). Adicione todos os métodos gerais necessários para a inversão de matriz ( Transpose ) para funcionar em Base . Em seguida, adicione métodos a LinAlg para todas as combinações de tipo adicionais que suportam simultaneamente a inversão de matriz (agora Transpose , mas não transpose ), transpor (agora conjadjoint e ConjAdjoint ), e adjunto em LinAlg requer (por exemplo, os mul[!] / rdiv[!] / ldiv[!] equivalentes de A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] que não existem atualmente).

  4. Remova a redução especial que rende A[c|t]_{mul|ldiv|rdiv}_B[c|t] , em vez de simplesmente reduzir ' / .' para Adjoint / Transpose ; expressões anteriormente especialmente rebaixadas rendendo A[c|t]_{mul|ldiv|rdiv}_B[c|t] chamadas e, em vez disso, tornam-se mul / ldiv / rdiv chamadas equivalentes. Desprezar A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] para os métodos mul[!] / ldiv[!] / rdiv[!] correspondentes correspondentes. Suspensão de .' .

As alterações anteriores precisariam ir em 0,7. Sem quebra silenciosa, apenas a quebra barulhenta da remoção de rebaixamento especial conforme descrito acima.

Neste ponto, Adjoint(A) / Transpose(A) / ConjAdjoint(A) produziria, respectivamente, adjunto preguiçoso, inversão de matriz e transposição. adjoint(A) / conjadjoint(A) renderia, respectivamente, adjunto e transposição ansiosos. transpose(A) seria preterido para conjadjoint ; transpose poderia ser reaproveitado em 1.0, se desejado. adjoint pode permanecer indefinidamente ou ser preterido em favor de outra grafia em 0.7. Outra forma de soletrar conjadjoint poderia ir diretamente para 0,7.

É possível consolidar um pouco as etapas 1 e 2, evitando algum trabalho / rotatividade, embora seja provável que essa abordagem seja mais desafiadora e propensa a erros.

#

Obrigado por ler!

Obrigado pelo ótimo artigo. Eu geralmente também concordo com a proposta 2, com a razão adicional de que conjugado adjunto não é a terminologia padrão e eu pessoalmente acho muito confuso (uma vez que adjoint é algumas vezes usado como terminologia para transpor em certos ramos da matemática, e o adjetivo conjugado extra parece implicar que ocorre a conjugação).

Tentei ler as elaboradas discussões acima, mas não consegui ver em que ponto foi decidido / motivado que um transpose matemático deveria ser recursivo. Isso pareceu resultar de alguma discussão particular (em algum lugar entre 14 e 18 de julho), após a qual de repente uma transposição matemática e estrutural foi introduzida.
@ Sacha0 o descreve como

Como isso surgiu: anteriormente, "transposição estrutural" era confundida com "adjunto matemático" / "transposição matemática"

Concordo que a transposição (estrutural) foi confundida com "adjunta", mas aqui a "transposição matemática" aparece do nada? Para completude / autocontenção, esse conceito e por que deveria ser recursivo poderia ser brevemente reiterado / resumido?

Em relação a isso, acho que ninguém está realmente usando transpose no sentido matemático abstrato, como uma operação em mapas lineares , pela simples razão de que a transposição de uma matriz seria um objeto que não atua sobre vetores, mas em RowVector s, ou seja, mapearia RowVector a RowVector . Dada a falta de argumentação para a transposta recursiva, não vejo realmente nenhuma distinção entre a transposta de matriz simples que é normalmente definida como um conceito (dependente da base) e a operação flip recentemente proposta.

Obrigado @ Sacha0 pelo ótimo artigo. Sou a favor da proposta 2 (chame o adjunto adjoint , transponha transpose e inverta a matriz flip . adjoint e transpose viva em LinAlg e flip vivem em Base .). Para mim, isso parece dar o melhor resultado final (que deve ser a principal preocupação) e também permite uma maneira mais limpa para a v1.0. Eu concordo com seus pontos acima, então não vou repeti-los, mas aqui estão alguns comentários extras.

Um argumento a favor de transpose não recursivo é que a sintaxe .' é muito conveniente e pode, portanto, ser usada para, por exemplo, Matrix{String} . Assumindo que a sintaxe está indo embora (# 21037) e que se deve usar transpose(A) vez de A.' , então acho que uma função flip seria uma adição bem-vinda (mais curta e mais claro do que transpose(A) , e definitivamente mais agradável do que permutedims(A, (2,1)) ).

O desacoplamento entre LinAlg e Base que a proposta 2 dá também é muito bom. Não só porque LinAlg só precisa se preocupar em como lidar com Transpose e Adjoint , mas também que deixa claro que consideramos transpose e adjoint para ser LinAlg operações, e flip para ser uma coisa genérica AbstractMatrix que simplesmente inverte as dimensões de uma matriz.

Por último, não tenho visto muitas reclamações sobre transpose(::Matrix{String}) etc não funcionando. É realmente um caso de uso comum? Na maioria dos casos, deve ser melhor construir a matriz invertida desde o início.

Este esquema de nomenclatura (proposta 2) pode realmente ser melhor. Adjunto e transpor são termos bastante comuns em álgebra linear, e é menos quebrável ...

Mas não consegui ver em que ponto foi decidido / motivado que uma transposta matemática deveria ser recursiva

@Jutho É o mesmo argumento que o adjunto (o adjunto de um número complexo é seu conjugado (uma vez que os números complexos são espaços vetoriais válidos de primeira posição e operadores lineares, o produto interno e os conceitos adjunto se aplicam) e o adjunto de uma matriz de bloco é recursivo ...). As pessoas podem querer fazer álgebra linear com matrizes reais aninhadas, por exemplo. Ainda vejo muitos códigos usando .' para o "adjunto de uma matriz ou vetor real" e também costumava fazer isso. Aqui, ' não seria apenas válido, mas também mais genérico (trabalhando para arrays que podem ter valores complexos e / ou aninhados). O uso mais genuíno de transposição recursiva em matrizes complexas aninhadas acontece quando suas equações fornecem conj(adjoint(a)) , o que é relativamente mais raro, mas ainda acontece (é raro o suficiente para ficar feliz se não houver função associada a ele - no discussões acima e em outros lugares várias pessoas começaram a chamá-lo de coisas diferentes como "transposição matemática", eu escolhi conjadoint , mas nada disso é uma ótima terminologia IMO, exceto talvez transpose si). Na álgebra linear, a operação não recursiva que reorganiza os blocos de uma matriz de bloco, mas não faz nada com os próprios blocos, geralmente não aparece.

Por último, não tenho visto muitas reclamações sobre transpose(::Matrix{String}) etc não funcionando. É realmente um caso de uso comum? Na maioria dos casos, deve ser melhor construir a matriz invertida desde o início.

Acho que esse tipo de coisa é bastante desejável nas operações de transmissão de Julia. Por exemplo, é muito comum querer pegar o produto externo (cartesiano) de alguns dados e talvez mapeá-lo por meio de uma função. Portanto, em vez de algo como map(f, product(a, b)) , podemos usar broadcast(f, a, transpose(b)) ou simplesmente f.(a, b.') . É muito poder em alguns personagens.

Minha opinião: para evitar adicionar ainda mais nomes de função a este espaço (ou seja, flip ), estou pensando se poderíamos ter um valor padrão para a permutação em permutedims(a) = permutedims(a, (2,1)) . Infelizmente, isso não é tão curto quanto flip , mas significativamente menos desagradável do que o formulário permutedims(a, (2,1)) completo (e tem o efeito colateral de ensinar aos usuários a função mais geral).

( @ Sacha0 Muito obrigado por seu artigo aqui. FYI na escolha entre Adjoint e Transpose wrappers, ou RowVector + MappedArray + qualquer coisa para invertido matrizes conforme planejado anteriormente (talvez apenas PermutedDimsArray ), ainda tendo a favorecer o último ...)

O uso mais genuíno de transposição recursiva em matrizes complexas aninhadas acontece quando suas equações fornecem conj(adjoint(a))

Depois de obter conj(adjoint(a)) em suas equações, como você sabe que é realmente conj que você deseja aplicar às entradas da matriz e talvez não adjoint , ou seja, talvez você realmente queira adjoint.(adjoint(a)) .

Isso provavelmente fará com que eu seja banido / expulso, mas não sou um grande fã dessa ideia recursiva. Eu também não sou necessariamente contra, eu só não acho que haja uma razão matematicamente rigorosa para isso, já que vetores e operadores / matrizes não são definidos recursivamente, eles são definidos sobre campos de escalares (e por anéis de extensão, se você quiser para trabalhar também com módulos em vez de espaços vetoriais). E então Base.LinAlg deve funcionar nesse caso. O argumento principal

O adjunto de um vetor deve ser um operador linear mapeando-o para um escalar. Ou seja, a '* a deve ser um escalar se a for um vetor.

é incompatível com o funcionamento do Julia Base: Faça a=[rand(2,2),rand(2,2)] e observe a'*a . Não é um escalar. Então a uma matriz agora, porque é um vetor preenchido com matrizes? O acima é apenas um escalar se você realmente pretendia usar o anel de matrizes 2x2 como escalares sobre os quais definiu um módulo. Mas, nesses contextos, não está claro para mim que o resultado acima seja o pretendido. De qualquer forma, o produto interno euclidiano raramente é usado no contexto de módulos, etc., então não acho que o Base deva se preocupar em definir o comportamento correto para ele.

Então, realmente, embora a afirmação acima possa ser uma tentativa de estender a motivação além da interpretação de matrizes preenchidas com matrizes como matrizes de bloco, no final eu acho que a interpretação da matriz de bloco foi a única verdadeira motivação para fazer adjoint e agora mesmo transpose (que nunca é usado no sentido matemático, mas sempre no sentido dos índices de matriz invertida) recursivo em primeiro lugar. Se você mesmo definir um novo tipo de campo escalar, é apenas um fardo estranho que você precise definir adjoint e transpose para ele, além de conj , antes de poder usá-lo em uma matriz.

Então, por que, com um sistema de tipos tão poderoso como em Julia, não basta simplesmente ter um tipo dedicado para matrizes de bloco. E assim, bancando o advogado do diabo:

  • Quantas pessoas estão realmente usando / contando com o comportamento recursivo atual de adjoint para qualquer trabalho prático?
  • Existem outras linguagens usando essa abordagem recursiva? (Matlab, usando células de matrizes, não)

Como disse, não sou necessariamente contra, apenas questionando a validade (mesmo depois de ter lido toda a discussão acima), especialmente para o caso transpose .

Isso provavelmente fará com que eu seja banido / expulso

Eu sei que isso é uma piada, mas ter uma opinião impopular de forma alguma fará com que ninguém seja banido. Somente o mau comportamento que dura um longo período pode levar ao banimento. Mesmo assim, a proibição não é

Eu só não acho que haja uma razão matematicamente rigorosa para isso, já que vetores e operadores / matrizes não são definidos recursivamente, eles são definidos sobre campos de escalares (e por extensão de anéis se você quiser trabalhar também com módulos em vez de espaços vetoriais )

Esta afirmação não faz sentido para mim. Um vetor de vetores, ou um vetor de funções, ou um vetor de matrizes, ainda forma um espaço vetorial (sobre um campo escalar subjacente) com + e * scalar definidos recursivamente e de forma semelhante se forma um espaço de produto interno com o produto interno definido recursivamente. A ideia de que os vetores só podem ser "colunas de escalares" desafia as definições e o uso comum em matemática, física e engenharia.

Faça a=[rand(2,2),rand(2,2)] e observe a'*a . Não é um escalar.

Neste caso, o "anel escalar" de a é o anel de matrizes 2x2. Portanto, este é um problema linguístico, não um problema com a maneira como o adjoint funciona.

Discutir sobre como a'*a deveria funcionar a partir de como atualmente funciona (ou não funciona) em Julia parece um tanto circular.

Se você mesmo definir um novo tipo de campo escalar, é apenas uma carga estranha que você precisa definir adjoint e transpose para ele, além de conj , antes de poder usá-lo em uma matriz

Acho que esse fardo é mais estético do que prático. Se você concordar em torná-lo um subtipo de Number você não precisa definir nada e mesmo se você acreditar que Number não é a coisa certa para você, as definições são tão triviais que adicioná-los dificilmente é um fardo.

Então, por que, com um sistema de tipos tão poderoso como em Julia, não basta simplesmente ter um tipo dedicado para matrizes de bloco.

https://github.com/KristofferC/BlockArrays.jl/ Colaboradores bem-vindos :)

Minhas desculpas pela piada do "banimento". Esta será minha última reação para não atrapalhar a discussão. É claro que o adjunto recursivo (mas talvez não transposto) está resolvido, e não estou tentando desfazer essa decisão. Estou apenas tentando apontar algumas inconsistências.

@StefanKarpinski : O exemplo do anel escalar está exatamente em contradição com a recursão: as próprias matrizes 2x2 são escalares ou a definição é recursiva e o tipo Number subjacente é escalar?

No primeiro caso, você realmente tem um módulo em vez de um espaço vetorial e provavelmente depende do caso de uso se você deseja conj ou adjoint nesses 'escalares', se você quiser usando produtos internos e adjuntos em tudo.

Estou bem em aceitar a última definição. Estou usando elementos arbitrários em subespaços invariantes de grupo de produtos tensores graduados de espaços supervetoriais em meu trabalho, então não estou realmente confinado a colunas de números em minha definição de vetores. Mas os vetores de matrizes são apenas vetores puros ou deveriam também herdar algum comportamento de matriz. No primeiro caso, dot(v,w) deve produzir um escalar, provavelmente chamando vecdot recursivamente em seus elementos. No último caso, pelo menos vecdot(v,w) deve produzir um escalar agindo recursivamente. Portanto, essa seria uma correção mínima para ser consistente com o adjunto recursivo.

Mas parece mais simples não ter um transpose recursivo e apenas permitir que ele seja usado para aplicações não matemáticas também, uma vez que acho que mesmo em equações matriciais típicas, tem muito pouco a ver com a transposição matemática real de um mapa linear.

Eu acho que dot deve chamar dot recursivamente, não vecdot , então seria um erro para um vetor de matrizes, uma vez que não definimos um produto interno canônico para matrizes.

Sempre fico surpreso que transpor seja recursivo ... e não consigo imaginar que alguém não seja. (Estou supondo que as visualizações sobre o artigo da Wikipedia “transpor de um mapa linear” pelo menos triplicaram por causa desta discussão.)

Eu também muitas vezes achei as definições recursivas perturbadoras e geralmente compartilho da visão expressa acima por @Jutho (mas, novamente, tivemos um treinamento muito semelhante).

Acho que descobri a inconsistência que está me incomodando aqui - continuamos usando anéis e campos como a incorporação de matriz 2x2 de números complexos como um exemplo aqui, mas isso não funciona nem motiva a transposição recursiva (e adjunta )

Deixe-me explicar. As coisas com as quais concordo amplamente

  1. Os escalares são operadores lineares de classificação 1 válidos. Dado o poderoso sistema de tipos de Julia, não há razão para que LinAlg conceitos como adjoint não se apliquem, por exemplo, devemos definir adjoint(z::Complex) = conj(z) . (Além de vetores e matrizes de escalares, LinAlg conceitos também podem ser estendidos (por usuários, eu presumo) para outros objetos - @stevengj mencionado, por exemplo, espaços vetoriais de tamanho infinito (espaços de Hilbert)).
  2. Devemos ser capazes de lidar de alguma forma com escalares com diferentes representações. O exemplo prototípico aqui é um número complexo z = x + y*im pode ser modelado como Z = x*[1 0; 0 1] + y*[0 1; -1 0] e as operações + , - , * , / e \ são preservados neste isomorfismo. (Observe, entretanto, que conj(z) vai para adjoint(Z) / transpose(Z) / flip(Z) - mais sobre isso depois).
  3. As matrizes de blocos devem ser possíveis de alguma forma (a abordagem atual depende de adjoint recursivos por padrão, etc).

Parece razoável que Base.LinAlg seja compatível com 1 e 2, mas IMO 3 só deve ser feito em Base se encaixar naturalmente (caso contrário, eu tenderia a adiar para pacotes externos como https: / /github.com/KristofferC/BlockArrays.jl).

Agora percebo que estivemos combinando 2 e 3 e isso leva a algumas inconsistências ( @Jutho também apontou isso). A seguir, desejo mostrar que 3. o uso recursivo de adjoint e outras operações como matrizes de bloco não nos dá 2. A maneira mais fácil é pelo exemplo. Vamos definir uma matriz 2x2 de números complexos como m = [z1 z2; z3 z4] , a representação isomórfica M = [Z1 Z2; Z3 Z4] e uma matriz de bloco 2x2 padrão b = [m1 m2; m3 m4] onde m1 etc são quadrados de mesmo tamanho matrizes de Number . As respostas semanticamente corretas para operações comuns estão listadas abaixo:

| operação | z | Z | m | M | b |
| - | - | - | - | - | - |
| + , - , * | recursiva | recursiva | recursiva | recursiva | recursiva |
| 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 recursivo | flip(transpose.(m)) ou recursivo | recursiva |
| 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) (se m1 invertível, veja por exemplo Wikipedia ) |

Considerando operações como trace e det que retornam escalares, acho que está bem claro que nosso sistema de tipo Julia para LinAlg não poderia lidar com nossa incorporação de matriz 2x2 de Complex de forma "automática", inferindo o que queremos dizer com "escalar" em um determinado momento. Um exemplo claro é trace(Z) onde Z = [1 0; 0 1] é 2 , enquanto esta é a nossa representação de 1 . Da mesma forma para rank(Z) .

Uma maneira de recuperar a consistência é definir explicitamente nossa representação 2x2 como escalar, por exemplo, subtipando Number como abaixo:

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))

Com essas definições, métodos genéricos LinAlg provavelmente funcionarão bem.

A conclusão que tiro disso: recursiva adjoint é uma conveniência para matrizes de blocos e vetores de vetores. Não deve ser motivada pela exatidão matemática para os tipos de "escalar" matrizes 2x2 I rotulados Z acima. Eu vejo como nossa escolha se apoiamos matrizes de bloco ou não por padrão, com os seguintes prós e contras:

Prós

  • Conveniência para usuários de block array
  • Vetores de vetores são espaços vetoriais válidos e matrizes de bloco são operadores lineares válidos, sob + , * , conj , etc. Se for possível fazer disso um isomorfismo natural (ao contrário do Z exemplo acima, que requer CNumber ), então por que não?

Contras

  • Complexidade adicional significativa para nossa implementação de LinAlg (que poderia residir em outro pacote).
  • É um pouco difícil (mas não impossível) suportar coisas como eig(block_matrix) . Se dissermos que LinAlg suporta eig e LinAlg suporta matrizes de bloco, então considero isso um bug até que seja corrigido. Dada a grande quantidade de funcionalidade fornecida por LinAlg , é difícil ver que algum dia estaremos "acabados".
  • Inconveniente de usuários de dados que desejam usar operações como transpose de uma forma não recursiva,

Para mim, a questão é - escolhemos dizer que, por padrão, LinAlg espera que os elementos de AbstractArray s sejam "escalares" (subtipos ou digitação de Number ) e alocar a complexidade dos arrays de blocos em pacotes externos? Ou abraçamos a complexidade em Base e LinAlg ?

O plano até um dia atrás era o último.

@ Sacha0 Tenho tentado concluir o trabalho RowVector para suportar as mudanças aqui (as anteriores: transpose não recursivos, RowVector strings de suporte e outros dados) e agora estou me perguntando o que você tem em mente em termos de design.

Nas discussões recentes, vejo esses casos com um palpite do que pode ser desejado

| | Vector | Matrix |
| - | - | - |
| adjoint | RowVector com adjoint recursivo | AdjointMatrix ou TransposedMatrix com adjoint recursivo |
| transpose | RowVector com transpose recursivo | TransposeMatrix com transpose recursivo |
| flip | uma cópia ou PermutedDimsArray ? | uma cópia ou PermutedDimsArray ? |
| conj de AbstractArray | preguiçoso ou ansioso? | preguiçoso ou ansioso? |
| conj de RowVector ou TransposedMatrix | preguiçoso | preguiçoso |

(O acima tenta separar as preocupações da álgebra linear de permutar dimensões de matrizes de dados.)

Então, algumas perguntas básicas para me desvencilhar:

  • Estamos fazendo transpose recursivos ou não? E quanto a adjoint ?
  • Em caso afirmativo, continuaremos assumindo conj(transpose(array)) == adjoint(array) ?
  • Parece que pelo menos algumas conjugações complexas serão preguiçosas, por exemplo, para suportar todas as operações BLAS atuais sem cópias extras. Tornamos conj preguiçosos para todos os arrays?
  • Se introduzirmos flip , é preguiçoso ou ansioso?

Para sua informação, tentei uma abordagem de baixo atrito para "inverter" as dimensões de uma matriz em # 24839, usando uma forma mais curta para permutedims .

Eu sou fortemente a favor da proposta 2 de @ Sacha0 . Nós não conj∘adjoint ) a este respeito, se for necessário.

FWIW, o Mathematica não faz Transpose recursivo nem ConjugateTranspose :

untitled

Tentei ler as elaboradas discussões acima, mas não consegui ver em que ponto foi decidido / motivado que uma transposição matemática deveria ser recursiva. [...] Para ser completo / autocontido, esse conceito e por que deveria ser recursivo poderia ser brevemente reiterado / resumido?

Compreendo e aprecio este sentimento e gostaria de abordá-lo na medida em que o tempo permitir. Explicar de forma acessível e lúcida por que adjoint e transpose devem ser recursivos por definição é, na melhor das hipóteses, desafiador e provavelmente não é possível em resumo. Redações coerentes e abrangentes como as acima levam muito tempo para serem elaboradas. Sendo o tempo curto, ao longo do dia, tentarei, em vez disso, resolver algumas das confusões nas postagens anteriores, respondendo a pontos / questões particulares nelas; por favor, tenha paciência comigo enquanto eu faço isso aos poucos :). Melhor!

explicando por que adjunto ... deve ser recursivo por definição ...

Acho que todos que contribuíram para esta discussão concordam que adjoint(A) deve ser recursivo. O único ponto de disputa é se transpose(A) também deve ser recursiva (e se uma nova função flip(A) deve ser introduzida para transposição não recorrente).

Acho que todos que contribuíram para esta discussão concordam que adjoint (A) deve ser recursivo.

Consulte, por exemplo, https://github.com/JuliaLang/julia/issues/20978#issuecomment-347777577 e comentários anteriores :). Melhor!

O argumento para adjunto recursivo é bastante básico para as definições. dot(x,y) deve ser um produto interno, ou seja, produzir um escalar, e a definição de adjunto é dot(A'*x, y) == dot(x, A*y) . A recursão (para dot e adjoint ) segue a partir dessas duas propriedades.

(A relação com os produtos escalares é toda a razão pela qual adjunto e transpor são operações importantes na álgebra linear. É por isso que temos um nome para eles, e não para outras operações como matrizes rotativas em 90 graus ( rot90 no Matlab ).)

Observe que um problema em usar flip para transposição não recursiva é que ambos Matlab e Numpy usam flip para o que chamamos de flipdim .

Acho que ninguém está realmente usando transpor no sentido matemático abstrato, como uma operação em mapas lineares, pela simples razão de que a transposição de uma matriz seria um objeto que não atua em vetores, mas sim em RowVector, ou seja, mapearia RowVector para RowVector.

Mas parece mais simples não ter uma transposta recursiva e apenas permitir que ela seja usada também para aplicações não matemáticas, uma vez que acho que mesmo em equações matriciais típicas tem muito pouco a ver com a transposta matemática real de um mapa linear.

transpor (que nunca é usado no sentido matemático, mas sempre no sentido de índices de matriz flip)

Isso vai parecer um pouco complicado, então tenha paciência comigo :).

adjoint Julia refere-se especificamente ao adjunto hermitiano . Em geral, para U e V espaços vetoriais normados completos (espaços de Banach) com respectivos espaços duais U * e V , e para o mapa linear A: U -> V, então o adjunto de A, tipicamente denotado A , é um mapa linear A *: V * -> U * . Ou seja, em geral, o adjunto é um mapa linear entre espaços duais, da mesma forma que em geral a transposta A ^ t é um mapa linear entre espaços duais, conforme mencionado acima. Então, como podemos reconciliar essas definições com a noção familiar de adjunto hermitiano? :)

A resposta reside na estrutura adicional dos espaços em que se trabalha normalmente, nomeadamente os espaços de produto interior completos (espaços de Hilbert). Um produto interno induz uma norma, portanto, um espaço de produto interno completo (Hilbert) é um espaço normatizado completo (Banach) e, portanto, suporta o conceito de adjunto (Hermitiano). Aqui está a chave: tendo um produto interno em vez de apenas uma norma, aplica-se um dos mais belos teoremas da álgebra linear, a saber, o teorema da representação de Riesz. Em poucas palavras, o teorema da representação de Riesz afirma que um espaço de Hilbert é naturalmente isomórfico ao seu espaço dual. Consequentemente, ao trabalhar com espaços de Hilbert, geralmente identificamos os espaços e seus duais e abandonamos a distinção. Fazer essa identificação é como você chega à noção familiar de adjunto hermitiano como A *: V -> U em vez de A *: V * -> U * .

E a mesma identificação é geralmente feita para transpor ao considerar espaços de Hilbert, tais que também A ^ t: V -> U, gerando a noção familiar de transpor. Portanto, para esclarecer, sim, a noção comum de transpor é a noção geral de transpor aplicada ao ambiente mais familiar (Hilbert), assim como a noção comum de adjunto hermitiano é a noção geral de adjunto aplicada a esse ambiente. Melhor!

Desculpas por bater em um cavalo morto, mas pensei em resumir brevemente as questões de confusão matemática de uma maneira diferente. O ponto principal de desacordo é como interpretar matematicamente um objeto Vector{T} quando T não é apenas um subtipo de Number sem subestrutura semelhante a um array.

Uma escola de pensamento aceita a afirmação de

um vetor de vetores sobre um anel R é melhor entendido como um espaço de soma direta, que também é um espaço vetorial sobre R.

Módulo de certas sutilezas sobre espaços vetoriais de dimensão infinita, etc., isso basicamente significa apenas que devemos pensar em vetores de vetores como vetores de bloco. Portanto, um Vector{Vector{T<:Number}} deve ser mentalmente "nivelado" em um simples Vector{T} . Dentro deste paradigma, operadores lineares que são representados como matrizes de matrizes devem ser pensados ​​da mesma forma como matrizes de blocos, e adjoint certamente devem ser recursivos, assim como transpose se estivermos usando a palavra sentido matemático . Por favor, corrija-me se eu estiver errado, mas acredito que as pessoas que têm esse ponto de vista pensam que algo como Vector{Matrix{T}} não tem uma interpretação natural o suficiente para que devamos projetar para isso. (Em particular, não devemos simplesmente assumir que o Matrix interno é uma representação de matriz de um número complexo, porque como @stevengj disse,

Se você estiver representando números complexos por matrizes 2x2, terá um espaço vetorial complexificado diferente.

)

A outra escola de pensamento é que Vector{T} sempre deve ser pensado como uma representação de um vetor em um espaço vetorial abstrato sobre escalares (no sentido de álgebra linear da palavra) do tipo T , independentemente do tipo T . Nesse paradigma, Vector{Vector{T'}} não deve ser pensado como um elemento de um espaço de soma direta, mas sim como um vetor sobre os escalares Vector{T'} . Nesse caso, transpose(Matrix{T}) não deve ser recursivo, mas simplesmente inverter a matriz externa. Um problema com esta interpretação é que para que os elementos do tipo T formem um anel válido de escalares, deve haver uma noção bem definida de adição (comutativa) e de multiplicação. Para um vetor como Vector{Vector{T'}} , precisaríamos de uma regra para multiplicar dois "escalares" Vector{T'} em outro Vector{T'} . Embora alguém certamente possa inventar essa regra (por exemplo, multipicação elementar, que resume a questão de como multiplicar T' ), não existe uma maneira natural universal de fazer isso. Outro problema é como adjoint funcionaria sob esta interpretação. O adjoint de Hermit é definido apenas em operadores lineares sobre um espaço de Hilbert, cujo campo escalar por definição deve ser os números reais ou complexos. Então, se quisermos aplicar adjoint a uma matriz Matrix{T} , devemos assumir que T é alguma representação dos números complexos (ou números reais, mas vou apenas manter com complexo porque esse caso é mais sutil). Nesta interpretação, adjoint não deve ser recursivo, mas deve inverter a matriz externa e então aplicar conjugate . Mas há mais problemas aqui, porque a ação correta de conjugate(T) depende da natureza da representação. Se for a representação 2x2 descrita na Wikipedia, então conjugate deve inverter a matriz. Mas, pelas razões descritas acima, conjugate definitivamente nem sempre deve inverter as matrizes. Portanto, essa abordagem seria um pouco complicada de implementar.

Aqui estão minhas próprias idéias: não há uma resposta "objetivamente correta" para saber se transpose deve ser recursivo quando aplicado a matrizes cujos elementos têm uma subestrutura complicada. Depende de como exatamente o usuário está escolhendo representar sua estrutura algébrica abstrata em Julia. No entanto, suportar anéis completamente arbitrários de escalares parece que seria muito difícil, então eu acho que para fins práticos, não devemos tentar ser tão ambiciosos e devemos esquecer a matemática esotérica de módulos em vez de anéis fora do padrão. Devemos certamente ter uma função em Base (com sintaxe mais simples do que permutedims(A, (2,1)) ) que não tem nada a ver com o conceito de álgebra linear de transposição e simplesmente vira matrizes e não faz nada recursivo, independentemente de ser chamado transpose ou flip ou o quê. Seria bom se adjoint e uma função de transposição separada (possivelmente com um nome diferente) em LinAlg fossem recursivos, porque então eles poderiam lidar com vetores / matrizes de bloco e a implementação simples da soma direta como vetores de vetores, mas isso não é exigido pela "correção matemática objetiva", e seria bom tomar essa decisão puramente com base na facilidade de implementação.

Apesar da minha promessa anterior de não comentar mais, infelizmente tenho que responder a esta.

O adjunto de Julia se refere especificamente ao adjunto de Hermit. Em geral, para U e V espaços vetoriais normados completos (espaços de Banach) com respectivos espaços duais U * e V , e para o mapa linear A: U -> V, então o adjunto Hermitiano de A, tipicamente denotado A , é um mapa linear A *: V * -> U *. Ou seja, em geral o adjunto hermitiano é um mapa linear entre espaços duais, assim como em geral a transposta A ^ t é um mapa linear entre espaços duais, conforme mencionado acima. Então, como você reconcilia essas definições com a noção familiar de adjunto hermitiano? :)

Realmente isso não é verdade. O que você está descrevendo aqui é realmente a transposta, mas (como já mencionei), em alguns campos isso é referido como o adjunto (sem Hermitiano) e denotado como A ^ t ou A ^ * (nunca A ^ adaga). Na verdade, ele se estende muito além dos espaços vetoriais e, na teoria das categorias, tal conceito existe em qualquer categoria monoidal (por exemplo, a categoria Cob de variedades orientadas n-dimensionais com cobordismos como mapas lineares), onde é referido como parceiro adjacente (em fato, pode haver dois diferentes A e A , já que o espaço dual esquerdo e direito não são necessariamente o mesmo). Mas observe, isso nunca envolve conjugação complexa. Os elementos de V * são de fato os mapas lineares f: V-> Escalar, e para um mapa linear A: U-> V e um vetor v de U, temos f (Av) = (A ^ tf) (v) . Visto que a ação de f não envolve conjugação complexa, nem a definição de A ^ t.

A resposta está na estrutura adicional possuída pelos espaços em que você normalmente trabalha, ou seja, espaços de produtos internos completos (espaços de Hilbert). Um produto interno induz uma norma, portanto, um espaço de produto interno completo (Hilbert) é um espaço normatizado completo (Banach) e, portanto, suporta o conceito de adjunto hermitiano. Aqui está a chave: tendo um produto interno em vez de apenas uma norma, aplica-se um dos mais belos teoremas da álgebra linear, a saber, o teorema da representação de Riesz. Em poucas palavras, o teorema da representação de Riesz afirma que um espaço de Hilbert é naturalmente isomórfico ao seu espaço dual. Consequentemente, ao trabalhar com espaços de Hilbert, geralmente identificamos os espaços e seus duais e abandonamos a distinção. Fazer essa identificação é como você chega à noção familiar de adjunto hermitiano como A *: V -> U em vez de A *: V * -> U *.

Novamente, não acho que isso seja totalmente correto. Em espaços de produto interno, o produto interno é uma forma sesquilinear dot de conj (V) x V -> Escalar (com conj (V) o espaço vetorial conjugado). Isso permite, de fato, estabelecer um mapa de V a V * (ou tecnicamente de conj (V) a V *), que é de fato o teorema da representação de Riesz. No entanto, não precisamos disso para introduzir o adjunto hermitiano. Na verdade, o produto interno dot é suficiente, e o adjunto Hermitiano de um mapa linear A é tal que
dot(w, Av) = dot(A' w, v) . Isso envolve conjugação complexa.

Realmente isso não é verdade. O que você está descrevendo aqui é realmente a transposta, mas (como já mencionei), em alguns campos isso é referido como o adjunto (sem Hermitiano) e denotado como A ^ t ou A ^ * (nunca A ^ adaga). [...]

@Jutho , veja, por exemplo, a página da

Talvez haja inconsistências entre os diferentes campos da matemática, mas:
https://en.wikipedia.org/wiki/Transpose_of_a_linear_map
e em particular
https://en.wikipedia.org/wiki/Transpose_of_a_linear_map#Relation_to_the_Hermitian_adjoint
e inúmeras referências na teoria das categorias, por exemplo
https://arxiv.org/pdf/0908.3347v1.pdf

https://en.wikipedia.org/wiki/Transpose_of_a_linear_map
e em particular
https://en.wikipedia.org/wiki/Transpose_of_a_linear_map#Relation_to_the_Hermitian_adjoint

@Jutho , não vejo nenhuma inconsistência entre aquela seção da página e as definições fornecidas na página que eu vinculei acima, nem vejo qualquer inconsistência com o que postei acima. Melhor!

Também irei assinar a proposta de @ Sacha0 2. Por enquanto, também estou bem com um argumento de 1 permutedims ; Acho que é melhor do que flip .

@ Sacha0 , então temos uma maneira diferente de interpretar isso. Eu li isso como
Para um determinado A: U-> V,
transpor (A) = dual (A) = (às vezes também) adjunto (A): V * -> U *
adjunto hermitiano (A) = punhal (A) = (normalmente apenas) adjunto (A): V-> U
e a relação entre ambos é obtida exatamente usando o mapa do espaço para o espaço dual (ie Riesz ...), que envolve conjugação complexa. Conseqüentemente, o adjunto hermitiano envolve conjugação, mas a transposição não.

Se você também deseja chamar o primeiro de hermitiano de adjunto, o que você chama de transpor? Você realmente não definiu o que estava em sua descrição, apenas mencionou

Adjunto Hermitiano de A, tipicamente denotado A , é um mapa linear A : V * -> U *. Ou seja, em geral o adjunto hermitiano é um mapa linear entre espaços duais, assim como em geral a transposta A ^ t é um mapa linear entre espaços duais

Portanto, transpor e Hermitian são adjuntos, então, duas maneiras diferentes de transformar A: U-> V em um mapa de V -> U ? Eu realmente ficaria feliz em discutir mais sobre isso, mas acho melhor fazermos isso em outro lugar. Mas, realmente, entre em contato comigo, pois estou muito interessado em aprender mais sobre isso.

Veja também http://staff.um.edu.mt/jmus1/banach.pdf para uma referência que adjunto, como usado no contexto de espaços de Banach, é realmente transposto, e não adjunto de Hermit (em particular é linear e não antilinear transformação). A Wikipedia (e outras referências) estão realmente combinando esses dois conceitos, usando a noção de adjunto hermitiano em espaços de Hilbert como uma motivação para uma definição generalizada de adjunto em espaços de Banach. Porém, este último é realmente transposto (e não precisa de um produto interno, nem de uma norma). Mas essa é a transposição de que eu estava falando, que ninguém está realmente usando em código de computador.

Para Julia Base: Não me oponho à conjugação hermitiana recursiva; Concordo que muitas vezes será a coisa certa a fazer. Não tenho certeza se o Base deve tentar fazer coisas inteligentes quando o tipo de elemento não é Number . Mesmo com T é um número, não há suporte no Base para o uso muito mais comum de produtos internos não euclidianos (e definições modificadas associadas de adjunto), nem acho que deveria haver. Acho que a principal motivação foram as matrizes de bloco, mas acho que um tipo de propósito especial (na Base ou em um pacote) é muito mais Juliano e, como @andyferris também mencionou, não é como todo o resto de LinAlg apóia essa noção de matrizes de bloco, mesmo coisas simples como inv (sem falar em fatorações de matrizes, etc.).

Mas se a conjugação hermitiana recursiva veio para ficar (por mim tudo bem), então acho que para consistência dot e vecdot deveriam agir recursivamente nos elementos. Atualmente, este não é o caso: dot chama x'y nos elementos (que não é o mesmo quando os elementos são matrizes) e vecdot chama dot nos elementos. Portanto, para um vetor de matrizes, não há realmente nenhuma maneira de obter um resultado escalar. Eu ficaria feliz em preparar um PR se as pessoas concordarem que a implementação atual não é realmente inconsistente com adjoint recursivo.

Quanto a transpose , parece mais simples torná-lo não recursivo e também permitir que seja usado por aqueles que não trabalham com dados numéricos. Acho que a maioria das pessoas que trabalham com matrizes conhece o termo transpose e o procurará. Ainda há conj(adjoint()) para aqueles que precisam de um transpose recursivo.

Triage acha que @ Sacha0 deve seguir em frente com a proposta 2 para que possamos experimentá-la.

Eu concordo totalmente com @ttparker que adjoint recursivo é uma escolha, e não a única opção matematicamente consistente. Por exemplo, podemos simplesmente afirmar:

1 - a LinAlg , um AbstractVector v é um vetor length(v) -dimensional com pesos básicos (escalares) v[1] , v[2] , ..., v[length(v)] .

(e da mesma forma por AbstractMatrix ).

Essa seria provavelmente a suposição que muitas pessoas fariam vindo de outras bibliotecas, e ter uma definição tão simples de vetores de base, classificação, etc., ajuda a manter a implementação simples. (Muitos provavelmente diriam que a álgebra linear numérica é viável de implementar em um computador precisamente porque temos uma boa base para trabalhar.)

Nossa abordagem atual é mais parecida com:

2 - a LinAlg , um AbstractVector v é uma soma direta de length(v) vetores abstratos. Também incluímos definições suficientes em tipos escalares como Number modo que para LinAlg eles sejam operadores / vetores lineares unidimensionais válidos.

e da mesma forma para matrizes (de bloco). Isso é muito mais generalizado do que as implementações de álgebra linear em MATLAB, numpy, eigen, etc, e é um reflexo do poderoso sistema de tipo / despacho de Julia que isso é ainda viável.

A razão geral para eu ver a opção 2 como desejável é novamente que o sistema de tipo / envio de Julia nos permite ter um objetivo muito mais amplo, que vagamente é assim:

3 - Em LinAlg , estamos tentando criar uma interface genérica de álgebra linear que funcione para objetos que satisfaçam a linearidade (etc) sob + , * , conj (etc), tratando tais objetos como operadores lineares / membros de um espaço de Hilbert / o que for apropriado.

O que é um objetivo muito legal (certamente muito além de qualquer outra linguagem de programação / biblioteca que eu conheça), motiva completamente adjoint e 2 recursivos (porque + , * e conj são recursivos) e é por isso que a proposta 2 de @ Sacha0 e a decisão de triagem são uma boa escolha :)

Eu realmente ficaria feliz em discutir mais sobre isso, mas acho melhor fazermos isso em outro lugar. Mas, realmente, entre em contato comigo, pois estou muito interessado em aprender mais sobre isso.

Saúde, vamos fazer! Estou ansioso para conversar mais offline :). Melhor!

Bom resumo, Andy! :)

Totalmente de acordo, Andy, pelo menos por adjoint (que foi o assunto do seu comentário).

No entanto, um apelo final para um transpose não recursivo, antes que eu me cale para sempre (espero).
Vejo uma transposta não recursiva com as seguintes vantagens:

  • Pode ser usado por todas as pessoas que trabalham com matrizes, mesmo contendo dados não numéricos. É também assim que eles provavelmente conhecerão esta operação e a procurarão em outras linguagens e na matemática básica extrapolada para seu caso de uso não numérico
  • Não há necessidade de escrever código extra para que o tipo preguiçoso flip ou PermutedDimsArray interaja com LinAlg . E se eu tiver uma matriz numérica que virei em vez de transpor; ainda poderei multiplicá-lo por outras matrizes (de preferência usando BLAS)?

  • Com um transpose recursivo e um adjoint recursivo, podemos facilmente ter um adjunto não recursivo como conj(transpose(a)) e uma transposição recursiva conj(adjoint(a)) . E ainda tudo vai interagir bem com LinAlg .

Então, quais são as desvantagens. Não vejo nenhum. Eu ainda mantenho meu ponto de que realmente ninguém está usando transpose em seu sentido matemático. Mas, em vez de tentar argumentar mais, alguém pode me dar um exemplo concreto em que uma transposição recursiva é necessária ou útil, e onde realmente é uma transposição matemática? Isso exclui qualquer exemplo onde você realmente pretendeu usar adjoint mas está apenas tendo números reais. Portanto, uma aplicação em que existe uma razão matemática para transpor um vetor ou matriz preenchida com mais matrizes que são, por sua vez, de valores complexos.

Posso dizer que pelo menos o Mathematica (que seria de esperar que tivesse dedicado bastante atenção a isso) não faz transposição recursiva:

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

EDITAR: Opa, isso também foi comentado acima, desculpe

Então, estou confuso. Parecia haver um consenso bastante sólido de que transpose deveria ser tornado não recursivo - por exemplo, 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 e https://github.com/JuliaLang/julia/pull/23424. Então @ Sacha0 deu duas propostas, uma das quais deixaria a transposição recursiva, mas introduziria uma função flip não recursiva, que obteve forte suporte apesar (pelo que eu posso dizer) de não ter sido realmente levantada como uma possibilidade antes . Então @JeffBezanson sugeriu que não precisamos de flip afinal se dermos a permutedims um segundo argumento padrão, que também obteve um forte suporte.

Portanto, agora o consenso parece ser que as únicas mudanças reais em transpose devem ser "nos bastidores": em relação à redução especial e avaliação preguiçosa vs. rápida, que o usuário final típico provavelmente não saberá ou se importará . As únicas mudanças realmente visíveis são essencialmente mudanças de grafia (depreciando .' e dando a permutedims um segundo argumento padrão).

Portanto, o consenso da comunidade parece ter mudado quase 180 graus em um tempo muito curto (na época da postagem de

Eu esqueci se alguém sugeriu isso, mas poderíamos apenas tornar transpose(::AbstractMatrix{AbstractMatrix}) (e possivelmente transpose(::AbstractMatrix{AbstractVector}) também) recursivo e transpose não recursivo de outra forma? Parece que cobriria todas as bases e não consigo pensar em nenhum outro caso de uso em que você queira que tranpose seja recursivo.

Portanto, o consenso da comunidade parece ter mudado quase 180 graus em um tempo muito curto (na época da postagem de

Se eu fosse tão eloqüente 😄. O que você está vendo é que o consenso não foi realmente formado. Em vez disso, (1) os participantes que favorecem o status quo, mas se retiraram da discussão devido ao atrito, voltaram para expressar uma opinião; e (2) outras partes que não haviam considerado o que o afastamento do status quo acarretaria na prática (e como isso pode influenciar as considerações de liberação) formaram uma opinião mais forte a favor do status quo e expressaram essa opinião.

Considere que esta discussão está em andamento de uma forma ou de outra no github desde 2014, e provavelmente antes offline. Para participantes de longo prazo, essas discussões se tornam exaustivas e cíclicas. Havendo trabalho significativo a fazer além de envolver-se nesta discussão - como escrever código, que é mais agradável - o resultado é o desgaste entre os participantes de longo prazo. Consequentemente, a conversa parece desequilibrada durante um período ou outro. Pessoalmente, estou quase no limite de atrito, então vou me concentrar em escrever código agora, em vez de continuar a me envolver nesta discussão. Obrigado a todos e melhor! :)

Vou dar um pequeno voto a favor da transposição e ctransposição não recursiva para AbstractArrays, com ambas sendo recursivas em AbstractArray {T} onde T <: AbstractArray.

Eu concordo que o comportamento recursivo é 'correto' em alguns casos, e vejo a questão de como podemos alcançar o comportamento correto com o mínimo de surpresa para aqueles que usam e desenvolvem pacotes.
Nesta proposta, o comportamento de transposição recursiva para tipos personalizados é opt-in: você opt-in tornando seu tipo um AbstractArray ou definindo o método apropriado
Base.transpose(AbstractArray{MyType}) ou Base.transpose(AbstractArray{T}) where T<: MyAbstractType .
Acho que a estratégia de digitação de pato para transposições recursivas (apenas recorrendo sem perguntar) produz algumas surpresas, conforme documentado acima. Se você introduzir ctranspose e adjoint distintos, ou propostas mais complicadas como conjadjoint e flip, os usuários os encontrarão e tentarão usá-los, e os mantenedores dos pacotes tentarão oferecer suporte a todos.

Como um exemplo de algo que seria difícil de suportar nas novas propostas: normal, transpose, ctranspose e conj arrays todos devem ser capazes de ter visualizações (ou avaliação preguiçosa) que funcionam com as visualizações ReshapedArray e SubArray. (Não estou certo se eles produzem visualizações por padrão ou apenas ao usar @view .) Isso se conecta ao trabalho na redução de A*_mul_B* e nas chamadas BLAS de nível inferior com sinalizadores 'N' 'T' e 'C' para matrizes densas, como foi observado em outro lugar. Isso seria mais fácil de raciocinar se eles tratassem normal , transpose , ctranspose e conj
em pé de igualdade. Observe que o próprio BLAS suporta apenas 'N' para normal, 'T' para transpor e 'C' para ctranspor e não tem flag para conj, o que eu acho um erro.

Finalmente, para consistência com Arrays e remodelações dimensionais superiores, acredito que a generalização apropriada de transpor e ctranspor é inverter todas as dimensões, ou seja
transpor (A :: Array {T, 3}) = permutados (A, (3, 2, 1)).

Felicidades!

Eu aprecio muito as pessoas que estão fazendo o trabalho. O que foi discutido com muito comprimento são adjuntos / transposições vetoriais (mas nunca o aspecto recursivo disso), até que @andyferris intensificou e implementou isso, e funciona maravilhosamente bem. Da mesma forma, também aprecio muito o redesenho em andamento dos construtores de matriz. Perfeito para tudo isso.

Dito isto, a transposição de matriz e adjoint / ctranspose nunca teve muita discussão, especialmente não o aspecto recursivo dela, que foi quase silenciosamente introduzido em https://github.com/JuliaLang/julia/pull/7244 com matrizes de bloco de motivação única . Vários motivos e motivações para adjuntos recursivos foram dados (após os fatos), e a maioria das pessoas pode concordar que essa é uma boa (mas não a única) escolha. No entanto, o Transpose não possui uma única motivação ou caso de uso real.

Há algumas coisas separadas acontecendo nessas discussões, e acontece agora que precisamos de um plano que possa ser implementado rapidamente.

  • Discutimos se matrizes de blocos de suporte (e estruturas mais exóticas) vale a pena em LinAlg . As opções de implementação são: nenhuma coisa recursiva (exceto + , * e conj porque essa é a natureza das funções genéricas em Julia), tudo recursivo (o status quo), ou tentar algum tipo de verificação de tipo ou característica para saber se um elemento deve fazer álgebra linear recursiva ou ser tratado como escalar.
  • Queremos uma boa maneira de os usuários permutarem as dimensões de uma matriz 2D de dados. Temos transpose , flip não recursiva, sintaxe abreviada permutedims (esse PR foi enviado primeiro puramente porque é o menor número de caracteres para implementar e provavelmente faz sentido até se fizermos outra coisa também), algum tipo de verificação de tipo ou característica para saber se um elemento deve fazer transposição recursiva (talvez até mesmo reintroduzindo transpose(x::Any) = x ...).
  • O analisador Julia tem um comportamento estranho como x' * y -> Ac_mul_B(x, y) que é uma verruga, que idealmente não existirá na v1.0. Isso não foi visto como viável até que possamos suportar BLAS rápido (sem cópias extras) sem ele, portanto, matriz preguiçosa transposta e adjunta.
  • O código em LinAlg é muito grande e foi desenvolvido ao longo de vários anos. Muitas coisas como a multiplicação de matrizes poderiam ser refatoradas para serem mais amigáveis ​​aos traços, talvez com um sistema de despacho mais parecido com o novo broadcast . Acho que é aqui que podemos facilitar o envio das matrizes certas (estou pensando em PermuteDimsArray de visualizações conjugadas remodeladas e ajdointed de matrizes strided) para o BLAS. No entanto, isso não tornará a v1.0 e também estamos tentando evitar regressões de desempenho sem tornar o código muito pior. Como Sacha apontou (e estou descobrindo), ter visões de transposição com uma ampla gama de comportamentos nos elementos (adjunto recursivo, transposição recursiva, conjugação, nada) cria complexidade adicional e um monte de novos métodos para manter as coisas funcionando como estão está.

Se pensarmos na v1.0 como algo que estabiliza a linguagem, então, em alguns sentidos, a maior prioridade para fazer uma mudança no comportamento é a terceira. Eu diria: a linguagem (incluindo o analisador) deve ser mais estável, seguida por Base , seguida por stdlib (que pode ou não incluir LinAlg , mas acho que quase definitivamente incluirá BLAS , Sparse , etc. um dia). É uma mudança que realmente não afeta os usuários (principalmente desenvolvedores de bibliotecas), então eu não ficaria surpreso se as opiniões das pessoas fossem diferentes aqui.

Ponto em Andy! :)

Acho que a única coisa que resta a fazer aqui é tornar adjoint e transpose preguiçosos por padrão?

Isso pode ser fechado agora?

A seguir: "Levando as transposições escalares a sério"

Mas, falando sério, podemos ter uma boa interface para especificar as diferentes transpostas 3D e multiplicações de tensores que são usadas nos solucionadores de PDE? Um pouco sério, mas não tenho certeza se conseguiria ser o OP para a próxima iteração dessa loucura.

não

:)

podemos ter uma boa interface para especificar os diferentes transposes 3D e multiplicações de tensores que são usados ​​em solvers PDE

Definitivamente parece um bom assunto para um pacote.

uma boa interface para especificar as diferentes transposições 3D e multiplicações de tensores

O TensorOperations.jl não faz o que você precisa aqui? (Observe que, neste nível, "uma boa interface" significa algo como um diagrama de rede de tensor, que é um pouco desafiador para escrever em código de forma mais sucinta do que a sintaxe de TensorOperations ).

Sim, TensorOperations.jl parece bom. Eu estava brincando, mas consegui o que precisava 👍.

Esta página foi útil?
0 / 5 - 0 avaliações