Julia: Tomarse la transposición de matriz en serio

Creado en 10 mar. 2017  ·  141Comentarios  ·  Fuente: JuliaLang/julia

Actualmente, transpose es recursivo. Esto es bastante intuitivo y conduce a esta desgracia:

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

Desde hace algún tiempo, le hemos estado diciendo a la gente que haga permutedims(A, (2,1)) lugar. Pero creo que todos sabemos, en el fondo, que eso es terrible. ¿Cómo llegamos aquí? Bien, está bastante bien entendido que uno quiere que el ctranspose o "adjunto" de una matriz de matrices sea recursivo. Un ejemplo motivador es que puede representar números complejos usando matrices 2x2 , en cuyo caso el "conjugado" de cada "elemento" (en realidad una matriz), es su adjunto como matriz; en otras palabras, si ctranspose es recursivo, entonces todo trabajos. Este es solo un ejemplo, pero generaliza.

El razonamiento parece haber sido el siguiente:

  1. ctranspose debe ser recursivo
  2. ctranspose == conj ∘ transpose == conj ∘ transpose
  3. transpose por lo tanto también debería ser recursivo

Creo que hay algunos problemas aquí:

  • No hay ninguna razón por la que ctranspose == conj ∘ transpose == conj ∘ transpose deba mantenerse, aunque el nombre hace que esto parezca casi inevitable.
  • El comportamiento de los conj elementwise operar en matrices es una especie de vestigio lamentable de Matlab y no es realmente una operación matemática justificables, tanto como exp operativo elementwise no es realmente matemáticamente correcto y lo expm hace sería una mejor definición.
  • En realidad, está tomando el conjugado (es decir, adjunto) de cada elemento que implica que ctranspose debería ser recursivo; en ausencia de conjugación, no hay una buena razón para que la transposición sea recursiva.

En consecuencia, propondría los siguientes cambios para remediar la situación:

  1. Cambiar el nombre de ctranspose (también conocido como ' ) a adjoint - eso es realmente lo que hace esta operación, y nos libera de la implicación de que debe ser equivalente a conj ∘ transpose .
  2. Depreciar vectorizado conj(A) en matrices a favor de conj.(A) .
  3. Agregue un argumento de palabra clave recur::Bool=true a adjoint (née ctranspose ) indicando si debe llamarse a sí mismo de forma recursiva. De forma predeterminada, lo hace.
  4. Agregue un argumento de palabra clave recur::Bool=false a transpose indicando si debe llamarse a sí mismo de forma recursiva. Por defecto, no es así.

Como mínimo, esto nos permitiría escribir lo siguiente:

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

Si podemos acortarlo más a A' depende de lo que queramos hacer con conj y adjoint de no números (o más específicamente, no reales, no -valores complejos).

[Este número es el segundo de una serie de ω₁ partes].

breaking decision linear algebra

Comentario más útil

(OT: ya estoy ansioso por "Tomarme en serio los 7 tensores", la próxima entrega de la exitosa miniserie de 6 partes ...)

Todos 141 comentarios

El sucesor lógico de un número anterior ... 👍

El comportamiento de conj que opera elementwise en matrices es una especie de remanente desafortunado de Matlab y no es realmente una operación matemáticamente justificable

Esto no es cierto en absoluto, y no es análogo a exp . Los espacios vectoriales complejos y sus conjugaciones son un concepto matemático perfectamente establecido. Véase también https://github.com/JuliaLang/julia/pull/19996#issuecomment -272312876

Los espacios vectoriales complejos, y su conjugación, son un concepto matemático perfectamente establecido.

A menos que me equivoque, la operación de conjugación matemática correcta en ese contexto es ctranspose lugar de conj (que era precisamente mi punto):

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

El problema de usar una palabra clave para recur es que, la última vez que verifiqué, hubo una gran penalización de rendimiento por usar palabras clave, lo cual es un problema si terminas llamando de forma recursiva a estas funciones con un caso base que termina en escalares.

Necesitamos solucionar el problema de rendimiento de las palabras clave en 1.0 de todos modos.

@StefanKarpinski , estás equivocado. Puede tener una conjugación compleja en un espacio vectorial sin tener adjuntos: los adjuntos son un concepto que requiere un espacio de Hilbert, etc., no solo un espacio vectorial complejo.

Además, incluso cuando tiene un espacio de Hilbert, el conjugado complejo es distinto del adjunto. por ejemplo, el conjugado de un vector de columna complejo en ℂⁿ es otro vector complejo, pero el adjunto es un operador lineal (un "vector de fila").

(¡La conjugación compleja no implica que puedas multiplicar conj(v)*v !)

Desactivar vectorizado conj es independiente del resto de la propuesta. ¿Puede proporcionar algunas referencias para la definición de conjugación compleja en un espacio vectorial?

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

(Este tratamiento es bastante formal; pero si busca en Google "matriz conjugada compleja" o "vector conjugado complejo", encontrará infinidad de usos).

Si el mapeo de un vector complejo al vector conjugado (lo que conj hace ahora) es una operación importante distinta de mapearlo a es un covector conjugado (lo que hace ' ), entonces ciertamente podemos mantener conj para expresar esa operación en vectores (y matrices, y matrices superiores, supongo). Eso proporciona una distinción entre conj y adjoint ya que estarían de acuerdo con los escalares pero se comportarían de manera diferente en las matrices.

En ese caso, ¿ conj(A) llamar a conj en cada elemento o llamar a adjoint ? La representación de números complejos como el ejemplo de matrices 2x2 sugeriría que conj(A) debería llamar a adjoint en cada elemento, en lugar de llamar a conj . Eso haría adjoint , conj y conj. todas operaciones diferentes:

  1. adjoint : intercambia i y j índices y mapea recursivamente adjoint sobre elementos.
  2. conj : mapear adjoint sobre elementos.
  3. conj. : mapear conj sobre elementos.

conj(A) debería llamar conj en cada elemento. Si representa números complejos mediante matrices de 2x2, tiene un espacio vectorial complejo diferente.

Por ejemplo, un uso común de la conjugación de vectores es en el análisis de valores propios de matrices reales : los valores propios y los vectores propios vienen en pares complejos conjugados. Ahora, suponga que tiene una matriz de bloques representada por una matriz 2d A de matrices 2x2 reales, actuando sobre vectores bloqueados v representados por matrices 1d de vectores de 2 componentes. Para cualquier valor propio λ de A con vector propio v , esperamos un segundo valor propio conj(λ) con vector propio conj(v) . Esto no funcionará si conj llama a adjoint recursiva.

(Tenga en cuenta que el adjunto, incluso para una matriz, puede ser diferente de un conjugado-transpuesta, porque el adjunto de un operador lineal definido de la manera más general depende también de la elección del producto interno. Hay muchas aplicaciones reales donde algunos tipo de producto interno ponderado es apropiado, en cuyo caso cambia la forma apropiada de tomar el adjunto de una matriz! Pero estoy de acuerdo en que, dado un Matrix , debemos tomar el adjunto correspondiente al producto interno estándar dado por dot(::Vector,::Vector) . Sin embargo, es muy posible que algunos tipos AbstractMatrix (u otros operadores lineales) quieran anular adjoint para hacer algo diferente).

En consecuencia, también hay cierta dificultad para definir un adjoint(A) algebraicamente razonable para, por ejemplo, matrices 3d, porque no hemos definido matrices 3d como operadores lineales (p. Ej., No hay una operación array3d * array2d incorporada) . ¿Quizás adjoint solo debería definirse de forma predeterminada para matrices escalares, 1d y 2d?

Actualización: Oh, bien: tampoco definimos ctranspose ahora para matrices 3d. Continua.

(OT: ya estoy ansioso por "Tomarme en serio los 7 tensores", la próxima entrega de la exitosa miniserie de 6 partes ...)

Si representa números complejos mediante matrices de 2x2, tiene un espacio vectorial complejo diferente.

No estoy siguiendo esto: la representación matricial 2x2 de números complejos debería comportarse exactamente como si tuviera escalares complejos como elementos. Yo pensaría, por ejemplo, que si definimos

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

y tenemos un vector complejo arbitrario v entonces queremos que esta identidad se mantenga:

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

Deletrearía más el ejemplo y haría una comparación con ' que se supone que ya se desplaza con m pero no puedo debido a https://github.com/JuliaLang/julia/ issues / 20979 , que rompe accidentalmente esa conmutación. Una vez que se solucione el error, m.(v)' == m.(v') se mantendrá, pero si te entiendo correctamente, @stevengj , ¿no debería conj(m.(v)) == m.(conj(v)) ?

conj(m(z)) debe == m(z) .

Su m(z) es un isomorfismo a números complejos bajo suma y multiplicación, pero no es el mismo objeto de otras formas para álgebra lineal, y no deberíamos pretender que es por conj . Por ejemplo, si z es un escalar complejo, entonces eigvals(z) == [z] , pero eigvals(m(z)) == [z, conj(z)] . Cuando el truco m(z) se extiende a matrices complejas, esta duplicación del espectro tiene consecuencias complicadas para, por ejemplo, métodos iterativos (ver, por ejemplo, este documento ).

Otra forma de decirlo es que z ya es un espacio vectorial complejo (un espacio vectorial sobre los números complejos), pero la matriz real 2x2 m(z) es un espacio vectorial sobre los números reales (usted puede multiplicar m(z) por un número real) ... si "complexifica" m(z) multiplicándolo por un número complejo, p m(z) * (2+3im) ej. no m(z) * m(2+3im) ) entonces obtener un espacio vectorial complejo diferente que ya no es isomorfo al espacio vectorial complejo original z .

Esta propuesta parece que producirá una mejora real: +1: Estoy pensando en el lado matemático:

  • según tengo entendido, la conjugación es una acción en el anillo (léase: números) que proporcionan los coeficientes para nuestro espacio / vectores vectoriales. Esta acción induce otra acción en el espacio vectorial (y en sus mapeos, léase: matrices), también llamada conjugación, por la linealidad de la construcción del espacio vectorial sobre el anillo. Por lo tanto, la conjugación funciona necesariamente mediante la aplicación de elementos a los coeficientes. Para Julia, eso significa que en el fondo para un vector v , conj(v) = conj.(v) , y es una elección de diseño si conj tiene métodos también para matrices o solo para escalares, los cuales parece estar bien? (El punto de Steven sobre el ejemplo de números complejos como matrices 2x2 es que este es un espacio vectorial cuyos coeficientes son formalmente / realmente reales, por lo que la conjugación no tiene ningún efecto aquí).
  • adjoint tiene un significado algebraico que es fundamental en muchos dominios importantes que involucran íntimamente al álgebra lineal (ver 1, 2 y 3 , todos con grandes aplicaciones también en física). Si se agrega adjoint , debería permitir argumentos de palabra clave para la acción en consideración; en un análisis funcional / de espacios vectoriales, esta acción generalmente es inducida por el producto interno, por lo tanto, el argumento de la palabra clave puede ser el formulario. Todo lo que esté diseñado para las transposiciones de Julia debería evitar entrar en conflicto con estas aplicaciones, así que creo que adjoint es un poco caro.

@felixrehren , no creo que utilice un argumento de palabra clave para especificar el producto interno que induce el adjunto. Creo que usaría un tipo diferente en su lugar, lo mismo que si quisiera cambiar el significado de dot por un vector.

Mi preferencia sería un poco más simple:

  • Mantenga conj tal cual (por elementos).
  • Haga que a' corresponda a adjoint(a) , y hágalo siempre recursivo (y, por lo tanto, falle para matrices de cadenas, etc., donde adjoint no está definido para los elementos).
  • Haga que a.' corresponda a transpose(a) , y que nunca sea recursivo (solo un permutedims ), y por lo tanto ignore el tipo de los elementos.

Realmente no veo un caso de uso para un adjoint no recursivo. Como una extensión, puedo imaginar casos de uso para un transpose recursivo, pero en cualquier aplicación en la que tenga que cambiar a.' a transpose(a, recur=true) parece tan fácil llamar a otra función transposerecursive(a) . Podríamos definir transposerecursive en Base, pero creo que la necesidad de esto será tan poco común que deberíamos esperar para ver si realmente surge.

Personalmente, creo que mantener esto simple será lo más sencillo para los usuarios (y para la implementación), y aún así será bastante defendible en el frente del álgebra lineal.

Hasta ahora (la mayoría) de nuestras rutinas de álgebra lineal están fuertemente arraigadas a estructuras de matriz estándar y al producto interno estándar. En cada ranura de su matriz o vector, coloca un elemento de su "campo". Argumentaré que para elementos de campos , nos preocupamos por + , * , etc., y conj , pero no transpose . Si su campo era una representación compleja especial que se parece un poco a una matriz real de 2x2 pero cambia por debajo de conj , entonces está bien, es una propiedad de conj . Si es solo un AbstractMatrix 2x2 real que no cambia bajo conj , entonces posiblemente esos elementos de una matriz no deberían cambiar bajo el adjunto ( @stevengj lo dijo mejor de lo que puedo describir - puede haber un isomorfismo en números complejos, pero eso no significa que se comporte igual en todos los sentidos).

De todos modos, el ejemplo complejo 2x2 me parece un poco como una pista falsa. La verdadera causa del comportamiento de la matriz recursiva fue un atajo para hacer álgebra lineal en matrices de bloques. ¿Por qué no tratamos este caso especial con el debido cuidado y simplificamos el sistema subyacente?

Entonces mi sugerencia "simplificada" sería:

  • Mantenga conj como está (o conviértalo en una vista por AbstractArray s)
  • Haga de a' una vista no recursiva tal que (a')[i,j] == conj(a[j,i])
  • Haga de a.' una vista no recursiva tal que (a.')[i,j] == a[j,i]
  • Introduzca un tipo BlockArray para manejar matrices de bloques y así sucesivamente (en Base , o posiblemente en un paquete bien soportado). Podría decirse que esto sería mucho más poderoso y flexible que un Matrix{Matrix} para este propósito, pero igualmente eficiente.

Creo que estas reglas serían lo suficientemente simples para que los usuarios las adoptaran y las siguieran.

PD: @StefanKarpinski Por razones prácticas, un argumento de palabra clave booleana para la recursividad no funcionará con las vistas para la transposición. El tipo de vista puede depender del valor booleano.

Además, lo mencioné en otra parte, pero lo agregaré aquí para completar: las vistas de transposición recursivas tienen la molesta propiedad de que el tipo de elemento puede cambiar en comparación con la matriz que está envolviendo. Por ejemplo, transpose(Vector{Vector}) -> RowVector{RowVector} . No he pensado en una forma de obtener este tipo de elemento por RowVector sin una penalización de tiempo de ejecución o invocando la inferencia para calcular el tipo de salida. Supongo que el comportamiento actual (invocando inferencias) no es deseable desde el punto de vista del lenguaje.

NB: tampoco hay nada que impida a los usuarios definir conj para devolver un tipo diferente, por lo que la vista ConjArray también sufre este problema, ya sea que la transposición sea recursiva o no.

@stevengj : usted señala que las matrices 2x2 "complejas" que son un espacio vectorial formalmente real en lugar de un espacio vectorial complejo tiene sentido para mí, pero ese punto me pone en duda la motivación original para el adjunto recursivo, lo que me lleva a preguntarme si La propuesta de @andyferris no sería mejor (transposición no recursiva y adjunto). Supongo que el hecho de que tanto el ejemplo complejo 2x2 como la representación de la matriz de bloques "quieren" que el adjunto sea recursivo es sugerente, pero dados sus comentarios sobre ese primer ejemplo, me pregunto si no hay otros casos en los que el adjunto no recursivo es más correcto / conveniente.

Si el adjunto no es recursivo, no es un adjunto. Simplemente está mal.

¿Puede dar un poco más de justificación para eso cuando tenga un momento?

El adjunto de un vector debe ser un operador lineal mapeándolo a un escalar. Es decir, a'*a debe ser un escalar si a es un vector. Y esto induce un adjunto correspondiente en las matrices, ya que la propiedad definitoria es a'*A*a == (A'*a)'*a .

Si a es un vector de vectores, esto implica que a' == adjoint(a) debe ser recursivo.

OK, creo que sigo esto.

También tenemos productos internos recursivos:

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

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

Claramente, el "adjunto" o "dual" o lo que sea debería ser igualmente recursivo.

Creo que la pregunta central entonces es, ¿requerimos que a' * b == dot(a,b) para todos los vectores a , b ?

La alternativa es decir que ' no necesariamente devuelve el adjunto, es solo una operación de matriz que transpone los elementos y los pasa a través de conj . Esto resulta ser el adjunto de los elementos reales o complejos.

Hay una y solo una razón por la que tenemos un nombre especial y símbolos especiales para adjuntos en álgebra lineal, y esa es la relación con los productos internos. Esa es la razón por la que la "transposición conjugada" es una operación importante y la "rotación conjugada de matrices en 90 grados" no lo es. No tiene sentido tener algo que sea "solo una operación de matriz que intercambia filas y columnas y conjuga" si no está conectado a productos punto.

Se puede definir una relación similar entre transpose y un "producto escalar" no conjugado, lo que abogaría por una transposición recursiva. Sin embargo, los "productos punto" no conjugados para datos complejos, que en realidad no son productos internos, no aparecen con tanta frecuencia como los verdaderos productos internos; surgen de relaciones de bi-ortogonalidad cuando un operador está escrito en complejo forma simétrica (no hermitiana), y ni siquiera tienen una función de Julia incorporada o una terminología común en álgebra lineal, mientras que querer intercambiar filas y columnas de matrices arbitrarias no numéricas parece mucho más común, especialmente con la transmisión. Es por eso que puedo admitir que transpose no sea recursivo y deje adjoint recursivo.

Veo. Entonces, ¿cambiar el nombre de ' a adjoint sería parte del cambio, para que sea obvio que no es conj ∘ transpose ?

Lo que siempre me confundió con las matrices recursivas es: ¿qué es un escalar en este contexto? En todos los casos con los que estoy familiarizado, se dice que los elementos de un vector son escalares. Incluso en el caso de que un matemático escriba una estructura de bloque-vector / matriz en un trozo de papel, todavía sabemos que esto es solo una abreviatura de un vector / matriz más grande donde los elementos son probablemente números reales o complejos (es decir, BlockArray ). Espera que los escalares puedan multiplicarse por debajo de * , y el tipo de escalar no suele diferir entre el vector y su dual.

@andyferris , para un espacio vectorial general, los escalares son un anillo (números) por los que puedes multiplicar vectores. Puedo hacer 3 * [[1,2], [3,4]] , pero no puedo hacer [3,3] * [[1,2], [3,4]] . Entonces, incluso para Array{Array{Number}} , el tipo escalar correcto es Number .

De acuerdo, pero generalmente se dice que los elementos son (los mismos) escalares, ¿no?

Los tratamientos que he visto, de todos modos, comienzan con un anillo y construyen un espacio vectorial a partir de ellos. El anillo admite + , * , pero no he visto un tratamiento que requiera que admita adjoint , dot o lo que sea.

(Lo siento, solo estoy tratando de comprender el objeto matemático subyacente que estamos modelando).

Depende de lo que entiendas por "taquigrafía" y de lo que entiendas por "elementos".

Por ejemplo, es bastante común tener vectores de funciones de tamaño finito y "matrices" de operadores de dimensión infinita. por ejemplo, considere la siguiente forma de las ecuaciones macroscópicas de Maxwell :

image

En este caso, tenemos una matriz 2x2 de operadores lineales (por ejemplo, rizos) que actúan sobre vectores de 2 componentes cuyos "elementos" son campos vectoriales de 3 componentes. Estos son vectores sobre el campo escalar de números complejos. En cierto sentido, si profundiza lo suficiente, los "elementos" de los vectores son números complejos, componentes individuales de los campos en puntos individuales del espacio, pero eso es bastante confuso.

O quizás más precisamente, ¿no podemos usar siempre el anillo para describir los coeficientes del vector en alguna base (dada la naturaleza de matriz de las cosas en Julia, no estamos libres de bases)?

En cualquier caso, la consecuencia sigue siendo que los adjuntos tienen que ser recursivos, ¿no? No entiendo a qué punto estás llegando.

(Creo que podemos ir absolutamente libres de bases, ya que los objetos o los elementos de las matrices podrían ser expresiones simbólicas como SymPy o alguna otra estructura de datos que represente un objeto matemático abstracto, y el adjunto podría devolver otro objeto que, cuando se multiplica, calcula un integral, por ejemplo.)

Solo intento entender mejor, no hacer un punto en particular :)

Ciertamente, en lo anterior, el adjunto es recursivo. Me preguntaba si, por ejemplo, en lo anterior es mejor pensar en [E, H] como un BlockVector cuyos (¿infinitos?) Muchos elementos son complejos, o como un 2-vector cuyos elementos son campos vectoriales. Creo que en este caso, el enfoque BlockVector sería inviable.

Gracias de todos modos.

Entonces, tal vez pueda destilar mis pensamientos de esta forma:

Para un vector v , espero que length(v) describa la dimensión del espacio vectorial, es decir, el número de coeficientes escalares que necesito para describir (completamente) un elemento del espacio vectorial, y también que v[i] devuelve el i ésimo coeficiente escalar. Hasta la fecha, no he pensado en AbstractVector como un "vector abstracto", sino más bien como un objeto que contiene los coeficientes de un elemento de algún espacio vectorial dada alguna base que el programador conoce.

Parecía un modelo mental simple y útil, pero quizás sea demasiado restrictivo / poco práctico.

(EDITAR: Es restrictivo porque en Vector{T} , T debe comportarse como un escalar para que funcionen las operaciones de álgebra lineal).

pero no puedo hacer [3,3] * [[1,2], [3,4]]

Podríamos arreglar eso. No estoy seguro de si es una buena idea o no, pero ciertamente podría funcionar.

No estoy seguro de que sea deseable ... en este caso [3,3] realidad no es un escalar. (También ya tenemos la capacidad de hacer [3,3]' * [[1,2], [3,4]] , que realmente no sé cómo interpretar en un sentido de álgebra lineal).

Esto muestra un caso de esquina interesante: parece que estamos diciendo que v1' * v2 es el producto interno (y por lo tanto devuelve un "escalar"), pero [3,3]' * [[1,2], [3,4]] == [12, 18] . Un ejemplo artificial, supongo.

En ese caso, [3,3] es un escalar: los escalares son elementos en el anillo subyacente, y hay muchas buenas formas de convertir elementos de la forma [a,b] en un anillo (y así definir vectores / matrices sobre eso). El hecho de que estén escritos como vectores, o el hecho de que este anillo forme un espacio vectorial sobre otro anillo subyacente, no cambia eso. (¡Podrías tomar cualquier otra notación que pueda ocultar los vectores! O hacer que se vea totalmente diferente). Como se refería @andyferris , lo que es un escalar depende del contexto.

Formalmente, la recursividad no es parte del adjunto. En cambio, la conjugación de los elementos escalares hace esa parte del trabajo y, a menudo, si un escalar s se representa como una matriz, la conjugación de s implicará la transposición de la matriz que usamos para denotar eso. Pero ese no es siempre el caso, depende de la estructura dada por el usuario al anillo de escalares.

Creo que forzar la recursividad adjunta puede ser un compromiso práctico aceptable, típico de las aplicaciones de tipo matlab. Pero para mí, tomarlo muy en serio significaría adjuntar no recursivo y usar escalares escritos + despacho para permitir que conj trabaje la magia necesaria en cualquiera que sea la estructura del anillo subyacente.

En ese caso [3,3] es un escalar: los escalares son elementos en el anillo subyacente

Excepto que tal escalar no es un elemento del anillo definido por + , * . No es compatible con * ; No puedo hacer [3,3] * [3,3] .

Creo que forzar la recursividad adjunta puede ser un compromiso práctico aceptable, típico de las aplicaciones de tipo matlab. Pero para mí, tomarlo muy en serio significaría adjuntar no recursivo y usar escalares escritos + despacho para permitir que conj trabaje la magia necesaria en cualquiera que sea la estructura del anillo subyacente.

Estoy de acuerdo, si queremos hacer una comprensión práctica, esto está completamente bien y es lo que hemos hecho hasta ahora, pero me parece que los escalares subyacentes son los de la matriz completamente aplanada y eso es en lo que se define el álgebra lineal . Tenemos la tecnología para hacer un BlockVector y un BlockMatrix (y BlockDiagonal , etc.) que presentan una vista plana y eficiente de la matriz completamente expandida, por lo que el "super serio "El enfoque debería, como mínimo, ser factible. También creo que sería tan fácil de usar como el enfoque recursivo (algunos caracteres adicionales para escribir BlockMatrix([A B; C D]) a cambio de lo que podría ser un código semántico más agradable y potencialmente más legible, estoy seguro de que estos puntos están en debate). Sin embargo, sería más trabajo implementar todo eso.

@andyferris , tienes razón, no son un anillo porque * no está definido, pero supongo que estamos en la misma página que * podría definirse fácilmente para él, y hay muchas formas diferentes de hacerlo que le darán una estructura de anillo. Así que supongo que estamos en la misma página: escribir es la forma más extensible de resolver esto.

(Acerca de los escalares: ¡los escalares no son necesariamente los elementos de la estructura completamente aplanada! La razón es esta. Un espacio vectorial complejo unidimensional es unidimensional sobre los números complejos y bidimensional sobre los números reales; ninguna representación es especial. Los cuaterniones son bidimensionales sobre los números complejos. Los octoniones son bidimensionales sobre los cuartos, cuatridimensionales sobre los números complejos y 8-dimensionales sobre los reales. Pero ya no hay razón para insistir en que los octoniones son "8-dimensionales" que insistir en que sus escalares son los números reales no complejos; esas son elecciones no forzadas por la lógica. Es puramente una cuestión de representación, el usuario debe tener esta libertad ya que el álgebra lineal es la misma en cada caso. Y obtienes cadenas mucho más largas de tales espacios con anillos multipolinomiales [truncados], o extensiones algebraicas de campos numéricos, los cuales tienen aplicaciones vivas con matrices. Julia no tiene que ir tan lejos por la madriguera del conejo, pero nosotros Debe recordar que existe, para no bloquearlo. ¿Quizás un tema de discurso? :))

@felixrehren , un vector de vectores sobre un anillo R se entiende mejor como un espacio de suma directa, que también es un espacio vectorial sobre R. No tiene sentido llamar a los vectores en sí mismos "el anillo subyacente", porque en general no son un anillo. Ese razonamiento se aplica perfectamente a [[1,2], [3,4]] .

La conjugación y los adjuntos son normalmente conceptos separados; decir que "la conjugación de los elementos escalares" debería hacer el trabajo aquí me parece totalmente incorrecto - lo opuesto a "serio" - excepto en el caso especial que se indica a continuación.

Considere el caso de "vectores de columna de 2 componentes" (| u⟩, | v⟩) de dos elementos | u⟩ y | v⟩ en algún espacio de Hilbert H sobre algún anillo (digamos los números complejos ℂ), en notación de Dirac: "vectores de vectores". Este es un espacio de Hilbert de suma directa H⊕H con el producto interno natural ⟨(| u⟩, | v⟩), (| w⟩, | z⟩)⟩ = ⟨u | w⟩ + ⟨v | z⟩. Por tanto, el adjunto debe producir el operador lineal que consiste en el "vector fila" (⟨u | ⟨v |), cuyos elementos son operadores lineales: el adjunto es"recursivo" . Esto es totalmente diferente del conjugado complejo, que produce un elemento del mismo espacio de Hilbert H⊕H, inducido por la conjugación del anillo subyacente.

Está confundiendo el asunto al referirse a vectores sobre cuaterniones, etcétera. Si los "elementos" del vector son también elementos del anillo "complexificado" subyacente, entonces, por supuesto, el adjunto y el conjugado de estos elementos es lo mismo. Sin embargo, eso no es aplicable a todos los espacios de productos directos.

Dicho de otra manera, el tipo de objeto debe indicar dos datos diferentes :

  • El anillo escalar (de ahí el conjugado, que produce un elemento del mismo espacio).
  • El producto interior (de ahí el adjunto, que produce un elemento del espacio dual ).

Para Vector{T<:Number} , deberíamos leerlo como indicando que el anillo escalar es T (o complex(T) para el espacio vectorial complejo), y que el producto interno es el euclidiano habitual .

Si tiene un vector de vectores, entonces es un espacio de Hilbert de suma directa y el anillo es la promoción de los anillos escalares de los vectores individuales, y el producto escalar es la suma de los productos escalares de los elementos. (Si los escalares no se pueden promover o los productos escalares no se pueden sumar, entonces no es un espacio de vector / Hilbert en absoluto).

Si tiene un escalar T<:Number , entonces conj(x::T) == adjoint(x::T) .

Entonces, si intentas representar números complejos z::Complex{T} por las matrices 2x2 m(z)::Array{T,2} , entonces tu tipo indica un anillo diferente T y un producto interno diferente en comparación con z , y por lo tanto no debe esperar que conj o adjoint den resultados equivalentes.

Ya veo lo que estás diciendo @felixrehren. Si el escalar es real, entonces se puede pensar en un octerniano como un espacio vectorial de 8 dimensiones. Pero si el coeficiente escalar fuera un octerniano, entonces existe una base trivial de dimensión 1 que representa a todos los octernianos. No estoy seguro si esto es diferente a lo que dije (o lo que quise decir: sonríe :), si tenemos un AbstractVector{T1} podría ser isomorfo a un AbstractVector{T2} de diferente dimensionalidad ( length ). Pero T1 y T2 deben ser anillos bajo los operadores de Julia + y * (y el isomorfismo podría preservar el comportamiento de + pero no * , o el producto interno o adjunto).

@stevengj Siempre he pensado en la suma directa exactamente como mi BlockVector . Tiene dos bases incompletas (disjuntas). Dentro de cada base, es posible que pueda formar superposiciones como c₁ | X₁⟩ + c₂ | X₂⟩ en la primera base o c₃ | Y₁⟩ + c₄ | Y₂⟩ en la segunda. La "suma directa" representa el espacio de estados que se parecen a (c₁ | X₁⟩ + c₂ | X₂⟩) + (c₃ | Y₁⟩ + c₄ | Y₂⟩). Me parece que lo único especial que separa esto de una base de dimensión cuatro sobre los números complejos (es decir, estados como c₁ | X₁⟩ + c₂ | X₂⟩ + c₃ | Y₁⟩ + c₄ | Y₂⟩) son los corchetes - para mí, parece notorio; la suma directa es solo una forma conveniente de escribirlos en papel o con matrices en una computadora (y podría ser útil, por ejemplo, para permitirnos catalogar y aprovechar las simetrías, o dividir el problema (tal vez para paralelizarlo), etc. ). En este ejemplo, X⊕Y sigue siendo un espacio vectorial de cuatro dimensiones donde los escalares son complejos.

Esto es lo que me hace querer eltype(v) == Complex{...} y length(v) == 4 lugar de eltype(v) == Vector{Complex{...}} y length(v) == 2 . Me ayuda a ver y razonar sobre el anillo subyacente y el espacio vectorial. ¿No es también este espacio "aplanado" donde puede hacer una transposición "global" y conj por elementos para calcular el adjunto?

( apareciste en dos publicaciones mientras yo escribía solo una,

Por supuesto, si preferimos usar matrices anidadas como convención para la suma directa (como lo hacemos ahora) en lugar de BlockArray , ¡esto también puede ser agradable y consistente! Podríamos beneficiarnos de algunas funciones adicionales para determinar el campo escalar (una especie de eltype recursivo) y cuál podría ser la dimensionalidad aplanada efectiva (una especie de tamaño recursivo) para que sea un poco más fácil razonar sobre qué álgebra lineal estamos haciendo.

Para ser claros, estoy contento con ambos enfoques y he aprendido mucho de esta discusión. Gracias.

@andyferris , solo porque dos espacios son isomórficos no significa que sean el "mismo" espacio, y discutir sobre cuál de ellos es "realmente" el espacio vectorial (o si son "realmente" lo mismo) es un atolladero metafísico de la que nunca escaparemos. (Pero el caso de sumas directas de espacios vectoriales de dimensión infinita ilustra una limitación de su concepto "aplanado" de una suma directa como "todos los elementos de uno seguidos por todos los elementos del otro").

Una vez más, no estoy seguro de cuál es su punto. ¿Estás proponiendo seriamente que eltype(::Vector{Vector{Complex}}) en Julia debería ser Complex , o que length debería devolver la suma de las longitudes? ¿Que todos en Julia deberían verse obligados a adoptar su isomorfismo "plano" para los espacios de suma directa? Si no es así, el adjunto debe ser recursivo.

Y si está "simplemente tratando de entender mejor, sin hacer un punto en particular", ¿puede llevarlo a un foro diferente? Este tema es lo suficientemente confuso sin argumentos metafísicos sobre el significado de los espacios de suma directa.

el hecho de que dos espacios sean isomórficos no significa que sean el "mismo" espacio

Definitivamente no estaba sugiriendo eso, en absoluto.

De nuevo, no estoy seguro de cuál es tu punto.

Estoy sugiriendo seriamente que si quieres hacer álgebra lineal con AbstractArray , entonces eltype será mejor un anillo (soporte + , * y conj ) que descarta, por ejemplo, los tipos de Vector porque [1,2] * [3,4] no funciona. Y el length de un vector en realidad representaría la dimensionalidad de su álgebra lineal. Sí, esto descarta los espacios vectoriales de dimensión infinita, pero generalmente en Julia AbstractVector no puede tener un tamaño infinito. Creo que esto facilitaría el razonamiento sobre cómo implementar y usar la funcionalidad de álgebra lineal en Julia. Sin embargo, los usuarios tendrían que indicar explícitamente una suma directa a través de BlockArray o similar en lugar de usar estructuras de matriz anidadas.

Esta es una sugerencia bastante rompedora y tiene desventajas notables (algunas de las cuales ha mencionado), por lo que no me sentiré infeliz si decimos "es una buena idea, pero no va a ser práctica". Sin embargo, también he intentado señalar que el enfoque de matriz anidada hace que algunas cosas sobre el álgebra lineal subyacente sean un poco más opacas.

Todo esto parece directamente relevante para el PO. Con un enfoque aplanado forzado nos desharíamos de las transposiciones / adjuntas recursivas, y si abandonáramos la transposición / adjuntas recursivas, solo las estructuras aplanadas podrían ser viables para el álgebra lineal. Si no aplicamos un enfoque plano, debemos mantener un adjunto recursivo. Para mí, parecía ser una decisión vinculada.

¿Estás proponiendo seriamente que eltype(::Vector{Vector{Complex}}) en Julia debería ser Complex , o que length debería devolver la suma de las longitudes?

No, lo siento, tal vez lo que escribí allí no estaba claro; simplemente estaba sugiriendo que dos nuevas funciones de conveniencia para este propósito podrían ser útiles en ciertas circunstancias. A veces, es posible que desee el zero o one de ese anillo, por ejemplo.

¿Está diciendo que todos en Julia deberían verse obligados a adoptar su isomorfismo "plano" para los espacios de suma directa?

Estaba sugiriendo eso como una posibilidad, sí. No estoy 100% convencido de que sea la mejor idea, pero creo que hay algunas ventajas (y algunas desventajas).

Volviendo al ámbito de los cambios procesables aquí, parece que la versión simplificada de @stevengj de mi propuesta es el camino a seguir aquí, es decir:

  • Mantenga conj tal cual (por elementos).
  • Haga que a' corresponda a adjoint(a) , y hágalo siempre recursivo (y, por lo tanto, falle para matrices de cadenas, etc. donde adjoint no está definido para los elementos).
  • Haga que a.' corresponda a transpose(a) , y que nunca sea recursivo (solo un permutedims ), y por lo tanto ignore el tipo de los elementos.

Los principales "todos" reales que se pueden extraer de esto son:

  1. [] Cambie el nombre de ctranspose a adjoint .
  2. [] Cambie transpose para que no sea recursivo.
  3. [] Averigüe cómo encaja esto con los vectores de fila.

Sospecho que confiar en la transposición recursiva es lo suficientemente raro como para que podamos cambiar esto en 1.0 y listarlo como ruptura en NEWS. Cambiar ctranspose a adjoint puede pasar por una depreciación normal (sin embargo, hacemos esto para 1.0). ¿Hay algo más que deba hacerse?

@StefanKarpinski , también necesitaremos hacer el cambio correspondiente a RowVector . Una posibilidad sería dividir RowVector en Transpose y Adjoint tipos para transpose perezoso no recursivo y adjoint recursivo perezoso, respectivamente, y deshacerse de Conj (perezoso conj ).

Algunos cambios más, al menos tangencialmente relacionados

  • Algunos tipos de vista por transpose y adjoint de AbstractMatrix
  • Hacer que adjoint(::Diagonal) recursivo (posiblemente deberíamos arreglar este "error" en Diagonal para transpose y ctranspose antes de Julia v0.6.0)
  • Utilice el tipo "escalar" adecuado en cosas como: v = Vector{Vector{Float64}}(); dot(v,v) (actualmente es un error; intenta devolver zero(Vector{Float64}) )

De acuerdo, he estado tratando de tomar las matrices de bloques recursivas más en serio, y para respaldar esto, también estoy tratando de mejorar el comportamiento de Diagonal aquí (ya hay algunos métodos que anticipan una diagonal de bloque estructura, como getindex , por lo que parece natural hacer que la transposición sea recursiva en este caso).

Donde me quedo atascado, y lo que motivó directamente todos mis puntos de discusión anteriores, es cómo hacer operaciones de álgebra lineal en tal estructura, incluyendo inv , det , expm , eig , y así sucesivamente. Por ejemplo, existe un método para det(::Diagonal{T}) where T que solo toma el producto de todos los elementos diagonales. Funciona de forma brillante por T <: Number , y también funciona si todos los elementos son matrices cuadradas del mismo tamaño . (Por supuesto, si son matrices cuadradas del mismo tamaño, entonces los elementos forman un anillo, y todo es bastante sensato - también la transposición recursiva (editar: adjunto) es lo correcto).

Sin embargo, si pensamos en una matriz diagonal de bloques general Diagonal{Matrix{T}} , o cualquier matriz de bloques Matrix{Matrix{T}} , entonces generalmente podemos hablar de su determinante como un escalar T . Es decir, si el tamaño fuera (3 ⊕ 4) × (3 ⊕ 4) (una matriz de 2 × 2 con elementos diagonales que son matrices de dimensiones 3 × 3, 4 × 4 y elementos fuera de la diagonal coincidentes), debería det devolver el determinante de la estructura "aplanada" de 7 × 7, ¿o debería simplemente intentar multiplicar los elementos de 2 × 2 tal como están (y el error, en este caso)?

(editar: cambié las dimensiones anteriores para que sean todas diferentes, lo que debería evitar un lenguaje ambiguo)

No tengo ningún problema con que a' sea ​​recursivo, pero personalmente encontraría la nueva notación a.' extremadamente confusa. Si me mostraras la sintaxis a.' te diría, basándome en mi modelo mental de Julia, que realiza transpose.(a) es decir, una transposición por elementos de a , y estaría completamente incorrecto. Alternativamente, si me mostrara que a' y a.' eran ambas opciones para una transposición, y que una de ellas también se repite por elementos, le diría que a.' debe tener la recursividad elementwise, y nuevamente estaría equivocado.

Por supuesto, mi modelo mental de lo que significa . es simplemente incorrecto en este caso. Pero sospecho que no soy el único que interpretaría esa sintaxis exactamente de la manera incorrecta. Con ese fin, sugeriría usar algo distinto a .' para la transposición no recursiva.

@rdeits Me temo que esta sintaxis se deriva de MATLAB. Esto se volvió un poco desafortunado una vez que se introdujo la (bastante maravillosa) sintaxis de transmisión de dot-call para la versión 0.5.

Podríamos forjar nuestro propio camino separado de MATLAB y cambiar esto, pero creo que hubo una discusión separada en algún lugar sobre eso (¿alguien recuerda dónde?).

Ah, eso es lamentable. ¡Gracias!

Otro todo:

  • [] Arregle issymmetric y ishermitian para que coincidan con transpose y adjoint - el primero de cada par no es recursivo, el último de cada par es recursivo.

La sintaxis de Matlab .' es definitivamente bastante desafortunada en el contexto de nuestra nueva sintaxis . . No estaría en contra de cambiar esto, pero luego necesitamos una nueva sintaxis para la transposición, y no estoy seguro de que tengamos una disponible. ¿Alguien tiene alguna sugerencia para la transposición?

Pasemos la discusión de la sintaxis de transposición aquí: https://github.com/JuliaLang/julia/issues/21037.

No tengo una opinión sólida acerca de que transpose / ctranspose / adjoint sea ​​recursivo, pero prefiero no tratar A::Matrix{Matrix{T}} como una matriz de bloques en el sentido de un hvcat perezoso , que A fueran todos matrices cuadradas del mismo tamaño (es decir, formaran un anillo), esperaría que det(A) devolviera un cuadrado de ese tamaño nuevamente. Si son rectangulares o de diferentes tamaños, esperaría un error.

Dicho esto, un tipo de matriz de bloques / gato perezoso podría ser útil, pero debería ir hasta el final y también definir, por ejemplo, getindex para trabajar en los datos aplanados. Pero definitivamente no me gustaría que este concepto fuera absorbido por Matrix o cualquier otro tipo de matriz existente como Diagonal .

Esto es un alivio, @martinholters. Lo que me entró en pánico anteriormente en este hilo fue la idea de que de alguna manera deberíamos poder aplicar todo el marco de álgebra lineal de Julia a Matrix{Matrix} para matrices de bloques arbitrarias (donde las submatrices tienen diferentes tamaños).

Lo que estaba argumentando con el "aplanamiento" no es hacer una introspección elegante de lo que son los elementos, y simplemente tratar los elementos por sus propios méritos como elementos de un anillo. Sin embargo, el recursivo ctranspose / adjoint expande esto para permitir elementos que actúan como operadores lineales, lo que parece correcto y útil. (El otro caso son elementos de un vector que actúan como un vector, donde un adjunto recursivo también es correcto).

Para volver un poco más atrás, ¿cuál fue la motivación original para eliminar el comportamiento predeterminado de no operación para transpose(x) = x y ctranpsose(x) = conj(x) ? Siempre me parecieron bastante útiles.

Para volver un poco más atrás, ¿cuál fue la motivación original para eliminar el comportamiento de no operación predeterminado para transpose (x) = x y ctranpsose (x) = conj (x)? Siempre me parecieron bastante útiles.

Eso fue motivado por tipos de operadores lineales personalizados (que no pueden ser subtipos de AbstractArray) que no se especializaron ctranspose . Esto significó que heredaron el comportamiento incorrecto sin operación de una reserva. Hemos intentado estructurar el envío de modo que las alternativas nunca sean silenciosamente incorrectas (pero pueden tener una complejidad pesimista). https://github.com/JuliaLang/julia/issues/13171

Parece que tenemos dos opciones: en ambas, transpose vuelve no recursivo, de lo contrario:

  1. No recursivo ctranspose .
  2. ctranspose recursivo.

@stevengj , @jiahao , @andreasnoack : ¿cuáles son sus preferencias aquí? ¿Otros?

He estado pensando mucho en esto desde la charla JuliaCon 2017 de @jiahao.

Para mí, todavía siento que el álgebra lineal debería definirse con respecto a un campo "escalar" como un tipo de elemento T . Si T es un campo (admite + , * y conj (también - , / , .. .)) entonces no veo por qué los métodos en Base.LinAlg deberían fallar.

OTOH es bastante común (y válido) hablar de tomar la transposición de una matriz de bloques, por ejemplo. Creo que podemos aprender aquí de la teoría de tipos "matemática", que, por ejemplo, trata de lidiar con enunciados extraños que surgen de la discusión de conjuntos de conjuntos teniendo conjuntos de escalares de "primer orden", conjuntos de conjuntos de escalares de "segundo orden", orden "conjuntos de conjuntos de conjuntos de escalares, y así sucesivamente. Creo que tenemos el mismo problema (y oportunidad) aquí cuando tratamos con matrices de matrices: potencialmente podemos usar el sistema de tipos de Julia para describir matrices de "primer orden" de escalares "verdaderos", matrices de "segundo orden" de matrices de escalares, etc. . Creo que las largas discusiones entre @stevengj , yo y otros, resultaron del hecho de que, sí, se puede llegar a un conjunto de operaciones autoconsistentes en matrices de "orden" arbitrario, y en este marco (c) transpose es clara y correctamente recursiva, pero no es necesario que lo haga. Como señaló claramente la charla de Jiahao, los lenguajes / marcos de programación toman decisiones semánticas sobre distinguir escalares de matrices (o no), distinguir vectores de matrices (o no) y demás, y creo que esta es una elección relacionada que podemos hacer.

Para mí, el punto crucial aquí es que para hacer que la matriz de "orden arbitrario" funcione, tienes que enseñar a tus "escalares" algunas operaciones que normalmente solo se definen en vectores y matrices. El método actual de Julia que hace esto es transpose(x::Number) = x . Sin embargo, realmente preferiría que extendiéramos nuestra distinción semántica entre matrices y escalares eliminando este método, y creo que puede ser bastante factible.

El siguiente paso sería enseñar Base.LinAlg la diferencia entre una matriz de primer orden y una matriz de segundo orden y así sucesivamente. He pensado en dos opciones viables, puede que haya más, realmente no lo sé:

  • Tener algún tipo de rasgo opt-in para "arreglo", de modo que transpose(::AbstractMatrix{T}) sea ​​recursivo cuando T es un AbstractMatOrVec , y no cuando T es un Number , y hágalo de alguna manera configurable (opt-in, opt-out, lo que sea) de lo contrario. (De alguna manera esto se hizo y se hace actualmente controlando el comportamiento del método transpose para los elementos, pero creo que es más limpio controlar el comportamiento del método transpose del ( ) matriz).
  • Alternativamente, afirme que la mayoría de AbstractArray s, incluidos Array s, son de primer orden (sus elementos son campos escalares), y use un tipo AbstractArray (digamos NestedArray ) para envolver matrices y "marcar" que sus elementos deben tratarse como matrices, no como escalares. Intenté jugar un poco con esto, pero parecía que la opción anterior probablemente sea una carga menor para los usuarios.

Siento que Julia hace una fuerte separación semántica entre matrices y escalares, y que la situación actual me parece (a mí) un poco inconsistente con eso, ya que los escalares tenían que ser "enseñados" sobre la propiedad de la matriz transpose . Un usuario puede decir claramente que Matrix{Float64} es una matriz de escalares (matriz de primer orden) y Matrix{Matrix{Float64}} es una matriz de bloques (matriz de segundo orden). Puede ser más consistente para nosotros usar el sistema de tipos o los rasgos para determinar si una matriz es de "primer orden" o lo que sea. También creo que la primera opción que enumeré haría que Julia sea más fácil de usar (para que pueda hacer ["abc", "def"].' ) y al mismo tiempo conservar la flexibilidad para hacer algo predeterminado sensato / útil con matrices de bloques.

Perdón por insistir tanto, pero después de hablar con @StefanKarpinski arriba "funcionen", pero para mí sería "funcionando" al igual que el álgebra matricial / vectorial "funcionaba" antes de la introducción de RowVector .

El TLDR fue: no haga que ( c ) transpose sea ​​recursivo o no, deje que haga ambas cosas (es decir, que sea configurable).

Esos son buenos puntos, pero solo quiero señalar que casi todas (si no todas) las quejas acerca de que (c)transpose son recursivas están relacionadas con el uso no matemático de ' y .' al remodelar, por ejemplo, Vector{String} y Vector{PyObject} en matrices 1xn. Es fácil de arreglar tipo por tipo, pero puedo ver que es molesto. Sin embargo, se pensaba que la definición recursiva era la matemáticamente correcta y que "correcta pero molesta en muchos casos" era mejor que "conveniente pero incorrecta en casos raros".

Una posible solución podría ser su sugerencia en la primera viñeta, es decir, solo tener transposiciones recursivas para elementos tipo matriz. Creo que T<:AbstractVecOrMat cubriría la mayoría de los casos cambiando el estado a "conveniente pero incorrecto en casos muy raros". La razón por la que todavía podría estar mal es que algunos tipos de operadores no encajarían en la categoría AbstractMatrix porque la interfaz AbstractArray trata principalmente de semántica de matriz ( getindex ) , no álgebra lineal.

@andyferris , el adjunto y el dual de un escalar están perfectamente bien definidos, y ctranspose(x::Number) = conj(x) es correcto.

Mi sensación es que la mayoría de los usos de transpose son "no matemáticos", y la mayoría de los usos de ctranspose (es decir, usos donde se desea el comportamiento de conjugación) son matemáticos (ya que no hay otra razón para conjugar ). Por lo tanto, tendería a estar a favor de transpose recursivos y ctranspose recursivos.

Personalmente, creo que tratar de ver las matrices de bloques como Arrays anidado se vuelve complicado por las razones aquí y quizás sea mejor tener un tipo dedicado a la https://github.com/KristofferC/BlockArrays.jl para esta.

Se ve bien, @KristofferC.

Hay un punto muy válido que @stevengj hace arriba, y es que a veces sus elementos pueden ser, por ejemplo, vectores u operadores lineales en el sentido matemático, pero no AbstractArray s. Definitivamente necesitamos una forma de hacer transposición recursiva (c) para estos, no estoy seguro de si has pensado en esto o no, pero pensé en mencionarlo.

Aspectos destacados de una conversación floja / # linalg sobre este tema. Recapitula algunos de los hilos anteriores. Se centra en la semántica, evita la ortografía. (¡Gracias a todos los que participaron en esa conversación! :))

Existen tres operaciones semánticamente distintas:
1) "adjunto matemático" (recursivo y perezoso)
2) "transposición matemática" (idealmente recursiva y perezosa)
3) "transposición estructural" (idealmente no recursiva y? Ansiosa?)

Situación actual: "adjunto matemático" se asigna a ctranspose , "transposición matemática" a transpose y "transposición estructural" a permutedims(C, (2, 1)) para C bidimensionales y reshape(C, 1, length(C)) por C unidimensionales. El problema: la "transposición estructural" es una operación común, y la historia de permutedims / reshape es algo confusa / antinatural / molesta en la práctica.

Cómo surgió eso: Anteriormente, la "transposición estructural" se combinó con "adjunto matemático" / "transposición matemática" a través de alternativas genéricas no operativas como transpose(x::Any) = x , ctranspose(x::Any) = conj(x) y conj(x::Any) = x . Estos fallbacks hicieron que [c]transpose y los operadores de sufijo asociados ' / .' sirvieran para "transposición estructural" en la mayoría de los casos. Excelente. Pero también hicieron que las operaciones que involucraban [c]transpose en algunos tipos numéricos definidos por el usuario fallaran silenciosamente (devolvieron resultados incorrectos) sin la definición de [c]transpose especializaciones para esos tipos. Ay. Por lo tanto, se eliminaron esos fallos genéricos sin operación, dando lugar a la situación actual.

La pregunta es qué hacer ahora.

Resultado ideal: proporciona un encantamiento unificado, natural y compacto para una "transposición estructural". Admite al mismo tiempo adjunto y transposición matemáticos semánticamente correctos. Evite la introducción de casos complicados en el uso o la implementación.

Existen dos amplias propuestas:

(1) Proporcionar adjunto matemático, transposición matemática y transposición estructural como tres operaciones sintáctica y semánticamente distintas. Ventajas: Hace que todo funcione, separa claramente los conceptos y evita los complicados casos de esquina. Desventajas: Tres operaciones para explicar e implementar.

(2) Calzar las tres operaciones en dos. Existen tres formas de esta propuesta:

(2a) Hacer transpose semánticamente "transposición estructural", que también sirve como "transposición matemática" en casos comunes. Ventajas: dos operaciones. Funciona como se esperaba para contenedores con elementos escalares sin ambigüedades. Desventajas: Cualquiera que espere una semántica de "transposición matemática" recibirá silenciosamente resultados incorrectos en objetos más complejos que contenedores con elementos escalares sin ambigüedades. Si el usuario detecta ese problema, lograr la semántica de "transposición matemática" requiere definir nuevos tipos y / o métodos.

(2b) Introduzca un rasgo que indique la "matemática" de un tipo. Al aplicar adjunto / transponer a un objeto, si los tipos de contenedor / elemento son "mathy", recurse; si no, no repita. Ventajas: dos operaciones. Podría cubrir una variedad de casos comunes. Desventajas: Sufre el mismo problema que las fallas genéricas [c]transpose operación, es decir, podría producir silenciosamente resultados incorrectos para tipos numéricos definidos por el usuario que carecen de las definiciones de rasgos necesarias. No permite la aplicación de transposición estructural a un tipo "mathy" o transposición matemática a un tipo "no mathy". No está claro cómo decidir si un objeto es "mathy" en todos los casos. (Por ejemplo, ¿qué pasa si el tipo de elemento es abstracto? ¿La decisión recursiva / no recursiva es en tiempo de ejecución y por elemento, o en tiempo de ejecución y requiere un primer barrido sobre todos los elementos del contenedor para verificar su tipo colectivo?

(2c) Mantenga adjoint ( ctranspose ) "adjunto matemático" y transpose "transposición matemática", e introduzca métodos especializados (no alternativos genéricos) para adjoint / transpose para tipos escalares no numéricos (por ejemplo, adjoint(s::AbstractString) = s ). Ventajas: dos operaciones. Semántica matemática correcta. Desventajas: Requiere una definición por partes de adjoint / transpose para tipos no numéricos, lo que impide la programación genérica. No permite la aplicación de transposición estructural a tipos "mathy".

Las propuestas (1) y (2a) gozaron de un apoyo sustancialmente más amplio que (2b) y (2c).

Encuentre más discusión en # 19344, # 21037, # 13171 y slack / # linalg. ¡Mejor!

¡Gracias por la bonita reseña!

Me gustaría poner otra posibilidad sobre la mesa que iría bien con la opción 1: tener diferentes tipos de contenedores para matrices matemáticas y datos tabulares. Entonces, el significado de A' podría decidirse por el tipo de A (nota: no su tipo de elemento como se discutió anteriormente). Los contras aquí son que esto podría requerir una gran cantidad de convert s entre los dos (¿no?) Y, por supuesto, sería muy perjudicial. Es cierto que soy más que escéptico sobre si los beneficios justificarían esta interrupción, pero aún así quería mencionarlo.

Gracias, excelente reseña. Voto por (1).

Gran redacción. Tenga en cuenta que para (1) la ortografía podría ser:

  • Adjunto: a' (recursivo, perezoso)
  • Transposición matemática: conj(a') (recursiva, perezosa)
  • Transposición estructural: a.' (no recursiva, ansiosa)

Por lo tanto, no sería necesario introducir ningún operador nuevo: la transposición matemática sería solo una composición (perezosa y, por lo tanto, eficiente) de adjunto y conjugación. El mayor problema es que hace dos operadores de apariencia similar, es decir, el sufijo ' y .' , bastante distintos semánticamente. Sin embargo, diría que en la mayoría de los códigos genéricos correctos, si alguien ha usado ' o .' es un indicador 99% exacto de si querían decir "dame el adjunto de esta cosa" o "intercambiar las dimensiones de esta cosa ". Además, en los casos en los que alguien haya usado ' y realmente haya querido decir "intercambiar las dimensiones de esta cosa", su código ya sería incorrecto para cualquier matriz con elementos cuyo adjunto escalar no sea trivial, por ejemplo, números complejos. En los pocos casos restantes en los que alguien en realidad quiso decir "dame el conjugado del adjunto de esto", yo diría que escribir conj(a') hace que el significado sea mucho más claro, ya que en la práctica la gente usa a.' para significa "intercambiar las dimensiones de a ".

Queda por determinar todos los tipos genéricos subyacentes que necesitaríamos para esto, pero @andyferris y @andreasnoack ya tienen algunas ideas al respecto y parece posible.

Pensé que también debería actualizar, ya que esta discusión continuó en otro lugar. Admito que había una cosa (¿obvia?) Sobre esta propuesta que me había perdido por completo y era que asumí que continuaríamos usando .' para álgebra lineal, pero debería haberme dado cuenta de que este no es el caso. !

Por ejemplo, vector.' ya no necesitará ser un RowVector (ya que RowVector es un concepto de álgebra lineal, nuestro primer intento en un vector "dual") - simplemente puede ser un Matrix . Cuando quiero la "transposición no conjugada" en álgebra lineal, lo que realmente estoy haciendo es tomar conj(adjoint(a)) , y eso es lo que usaremos y recomendaremos a todos los usuarios de álgebra lineal que usen (hasta ahora tengo tenía un "mal" hábito de larga data desde MATLAB de usar a.' lugar de a' para transponer cualquier matriz (o vector) que sabía que era real, cuando lo que realmente quería era el adjoint (o dual): el cambio de nombre será de gran ayuda aquí).

También señalaré brevemente que esto deja abierto un espacio interesante. Anteriormente, vector' y vector.' tenían que satisfacer las necesidades duales de hacer una "transposición de datos" y tomar algo como el vector dual. Ahora que .' es para manipular tamaños de matriz y ' es un concepto de álgebra lineal, es posible que podamos cambiar RowVector en 1D DualVector o algo más enteramente. (Quizás no deberíamos discutir esto aquí; si alguien tiene ganas de esto, hagamos un tema aparte).

Finalmente, copiaré un plan de acción propuesto de Slack:

1) mueva transpose de LinAlg a Base y agregue advertencias (solo 0.7) sobre que ya no haga un RowVector o sea recursivo (si eso es posible)
2) renombrar ctranspose a adjoint todas partes
3) asegúrese de que Vector , ConjVector y RowVector funcionen con combinaciones de ' y conj y * , posiblemente renombrar RowVector . (aquí también hacemos conj(vector) perezosos).
4) introduzca una matriz perezosa adjunta que también interactúe bien con conj y ConjMatrix
5) eliminar A_mul_Bc etc. Cambiar el nombre de A_mul_B! a mul! (o *! ).
6) beneficio

@stevengj escribió:

@andyferris , el adjunto y el dual de un escalar están perfectamente bien definidos, y ctranspose(x::Number) = conj(x) es correcto.

Para que conste, definitivamente estoy de acuerdo con esto.

Mi sensación es que la mayoría de los usos de transpose son "no matemáticos", y la mayoría de los usos de ctranspose (...) son matemáticos

Entonces la idea será formalizar esto: todos los usos de transpose vuelven "no mathy" y el único uso de adjoint será "mathy".

Por ejemplo, vector.' ya no necesitará ser un RowVector (dado que RowVector es un concepto de álgebra lineal, nuestro primer intento en un vector "dual") - simplemente puede ser un Matrix .

Sin embargo, todavía queremos que .' sea ​​vago. p X .= f.(x, y.') ej.,

Aquí hay una pregunta: ¿Cómo deberíamos implementar issymmetric(::AbstractMatrix{<:AbstractMatrix}) ? ¿Sería una verificación no recursiva para coincidir con transpose ? Estoy mirando la implementación; asume que los elementos pueden transpose . OTOH, parece bastante válido comprobar si issymmetric(::Matrix{String}) ...

Actualmente no es recursivo si está envuelto en Symmetric cierto

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

Sí, estoy trabajando en la creación de un PR para esto y hay muchos de estos tipos de inconsistencias. (Me encontré con ese no hace mucho mientras buscaba cada instancia de transpose , adjoint y conj en el código de matriz).

A menos que se indique lo contrario, implementaré un comportamiento tal que issymmetric(a) == (a == a.') y ishermitian(a) == (a == a') . Estos parecen bastante intuitivos en sí mismos y AFAICT el uso existente de issymmetric es para los tipos de elementos Number (o de lo contrario, con frecuencia tiene otros errores / suposiciones que no tienen mucho sentido para las matrices anidadas) .

(La implementación alternativa es issymmetric(a) == (a == conj(adjoint(a))) ... no es tan "bonita" ni funcionará para matrices de "datos" (de String , etc.), pero coincide en matrices de Number )

A menos que se indique lo contrario, implementaré un comportamiento tal que issymmetric(a) == (a == a.') y ishermitian(a) == (a == a')

Me parece la solución correcta.

Actualización: Cambié de opinión. Simétrico es probablemente principalmente para álgebra lineal

Solo una pequeña sugerencia: a menudo es útil hacer una suma sobre el producto de dos vectores ( dotu ), y x.’y devolver un escalar es una forma conveniente de hacerlo. Entonces yo estaría a favor de no devolver un Matrix de transpose(::Vector)

Sí, será un RowVector por lo que debería obtener un escalar. (Tenga en cuenta que la transposición no será recursiva).

Perdón por el comentario posiblemente tangencial, pero muchas personas en este hilo siguen hablando de un "anillo de escalares del espacio vectorial". Por definición, los escalares de un espacio vectorial deben formar un campo, no cualquier anillo. Una estructura algebraica que es muy similar a un espacio vectorial pero cuyos escalares solo forman un anillo en lugar de un campo se llama "módulo", pero creo que los módulos son un poco demasiado esotéricos para ser manejados en la Base ... er. .. módulo.

Dado que admitimos matrices de enteros, admitimos módulos de manera efectiva, no solo espacios vectoriales. Por supuesto, también podríamos ver las matrices de enteros como incrustadas en matrices de punto flotante de forma conductual, por lo que son una representación parcial de un espacio vectorial. Pero también podemos crear matrices de enteros modulares (por ejemplo), y con un módulo no primo estaríamos trabajando con un anillo que no está integrado de forma natural en ningún campo. En resumen, realmente queremos considerar módulos en general en lugar de solo espacios vectoriales, pero no creo que haya ninguna diferencia significativa para nuestros propósitos (generalmente solo estamos hablando de + y * ) por lo que "espacio vectorial" sirve como una abreviatura más familiar de "módulo" para nuestros propósitos.

Sí, Julia ciertamente admite (y debería) soportar operaciones algebraicas en módulos generales, así como en espacios vectoriales verdaderos. Pero según tengo entendido, la filosofía general de la comunidad es que las funciones en Base deben diseñarse con "rutinas de álgebra lineal genérica 'ordinarias'" en mente, así que, por ejemplo, los cálculos exactos de álgebra lineal en matrices enteras no No pertenecen a Base , por lo que al tomar decisiones de diseño fundamentales, debemos asumir que los escalares forman un campo. (Aunque, como dijiste, prácticamente hablando, realmente no importa).

Publicación cruzada https://github.com/JuliaLang/julia/pull/23424#issuecomment -346678279

Aprecio mucho el esfuerzo de mover los números 5332 y 20978 hacia adelante que representa esta solicitud de extracción, y estoy de acuerdo con la mayor parte del plan asociado. Me gustaría mucho estar también a bordo con las opciones de terminología y el impacto posterior que avanza esta solicitud de extracción, y por lo tanto, trato de convencerme a mí mismo de que esas opciones producen el mejor conjunto de compensaciones disponibles.

Cada vez que trato de convencerme a mí mismo de esa posición, encallo en el mismo conjunto de dudas. Uno de esos recelos es la complejidad de implementación sustancial que esta elección impone a LinAlg : la combinación de transposición y cambio de matriz obliga a LinAlg a manejar ambos, lo que requiere que LinAlg soporte un conjunto mucho mayor combinaciones de tipos en operaciones comunes.

Para ilustrar, considere mul(A, B) donde A y B son desnudos, envueltos en adjuntos, envueltos en transposición o envueltos en matriz Matrix s. Sin combinar transposición y cambio de matriz, A puede ser un Matrix , un Matrix envuelto en adjunto, o un Matrix envuelto en transposición (e igualmente B ). Así que mul(A, B) necesitan soporte para nueve combinaciones de tipos. Pero al combinar transposición y cambio de matriz, A puede ser un Matrix , un Matrix envuelto en adjunto, un Matrix envuelto en transposición, o una matriz- envuelto Matrix (y también B ). Así que ahora mul(A, B) necesitan soporte para dieciséis combinaciones de tipos.

Este problema se agrava exponencialmente con la cantidad de argumentos. Por ejemplo, sin la combinación mul!(C, A, B) necesita admitir veintisiete combinaciones de tipos, mientras que con la combinación mul!(C, A, B) necesita admitir sesenta y cuatro combinaciones de tipos. Y, por supuesto, agregar Vector sy tipos de matriz / operador no Matrix a la mezcla complica aún más las cosas.

Parece que vale la pena considerar este efecto secundario antes de seguir adelante con este cambio. ¡Mejor!

Esta publicación consolida / revisa la discusión reciente sobre github, slack y triage, y analiza los posibles caminos a seguir. (El sucesor espiritual de https://github.com/JuliaLang/julia/issues/20978#issuecomment-315902532 nació de pensar en cómo llegar a 1.0).

Están en juego tres operaciones semánticamente distintas:

  • adjunto (algebraico lineal, recursivo, idealmente perezoso por defecto)
  • transponer (algebraico lineal, recursivo, idealmente perezoso por defecto)
  • array-flip (abstract-array-ic, no recursivo, idealmente? perezoso? por defecto)

Estado de estas operaciones en el maestro

  • Adjoint se llama adjoint / ' (pero está ansioso aparte de ' -expresiones que involucran especialmente bajas a llamadas A[c|t]_(mul|rdiv|ldiv)_B[c|t][!] , que evitan adjuntos / transposiciones ansiosos intermedios) .

  • La transposición se llama transpose / .' (con la misma advertencia que adjoint ).

  • Array-flip se llama permutedims(C, (2, 1)) para C bidimensionales y reshape(C, 1, length(C)) para C unidimensionales.

Los temas relevantes

  1. 5332: La reducción especial a A[c|t]_(mul|rdiv|ldiv)_B[c|t] y la colección combinatoria asociada de nombres de métodos deberían desaparecer en 1.0. La eliminación de esa reducción especial / los nombres de los métodos asociados requiere un adjunto diferido y una transposición.

  2. 13171: Adjoint (née ctranspose ) y la transposición se combinaron anteriormente con array-flip a través de alternativas genéricas no operativas como transpose(x::Any) = x , ctranspose(x::Any) = conj(x) y conj(x::Any) = x . Estas alternativas hicieron que las operaciones que involucraban [c]transpose en algunos tipos numéricos definidos por el usuario fallaran silenciosamente (devolvían resultados incorrectos) sin especializaciones [c]transpose para esos tipos. Devolver silenciosamente resultados incorrectos es una mala noticia, por lo que se eliminaron esos fallos (# 17075). No introducir fallos más silenciosos sería genial.

  3. 17075, # 17374, # 19205: La eliminación de los fallbacks anteriores hizo que el cambio de matriz fuera menos conveniente. Y junto con (disculpas, mi culpa) advertencias de desaprobación asociadas menos que excelentes, esa eliminación (¿transitoriamente?) Causó quejas. Un encantamiento más conveniente para array-flip sería bueno.

  4. 21037: Eliminar la ahora confusa sintaxis .' para 1.0 sería maravilloso. La reducción especial mencionada anteriormente debe eliminarse para permitir este cambio.

Objetivos de diseño para resolver estos problemas

Elimine la reducción especial y los nombres de métodos asociados, evite la introducción de fallas silenciosas, proporcione encantamientos intuitivos y convenientes para la transposición y el cambio de matriz, y elimine .' . Consiga lo anterior con una mínima complejidad y rotura adicionales.

Propuestas de diseño

El campo se ha reducido a dos propuestas de diseño:

  1. Llame al adjunto adjoint , transponga conjadjoint ("conjugue adjunto"), y haga girar matriz transpose . adjoint y conjadjoint viven en LinAlg , y transpose viven en Base .

  2. Llame al adjunto adjoint , transponga transpose y cambie la matriz flip . adjoint y transpose viven en LinAlg , y flip viven en Base .

A primera vista, estas propuestas parecen solo superficialmente diferentes. Pero un examen más detenido revela profundas diferencias prácticas. Evitando la discusión de los méritos relativamente superficiales de estos esquemas de nomenclatura, echemos un vistazo a esas profundas diferencias prácticas.

Diferencias, vista de alto nivel

  1. Complejidad:

    La propuesta uno, al llamar a array-flip transpose , fuerza a LinAlg a manejar array-flip además de transponer y adjuntar. En consecuencia, LinAlg debe expandir sustancialmente el conjunto de combinaciones de tipos soportadas por operaciones comunes; https://github.com/JuliaLang/julia/pull/23424#issuecomment -346678279 ilustra esta complejidad adicional a modo de ejemplo, y la siguiente discusión afirma implícitamente la existencia de esa complejidad adicional.

    La propuesta dos requiere que LinAlg soporte solo transposición y adjunto, lo que LinAlg hace ahora.

  2. Rotura:

    La propuesta uno cambia la semántica básica de las operaciones existentes: transpose convierte en array-flip en lugar de transponer, y todas las funciones relacionadas con transpose deben cambiar en consecuencia. (Por ejemplo, todas las operaciones de multiplicación y división izquierda / derecha en LinAlg asociadas con el nombre transpose requerirían una revisión semántica). Dependiendo de cómo se realice este cambio, este cambio causa una rotura silenciosa dondequiera que se confíe en la semántica presente (a propósito o inadvertidamente).

    La propuesta dos conserva la semántica básica de todas las operaciones existentes.

  3. Acoplamiento:

    La propuesta uno trae un concepto algebraico lineal (transposición) a Base , y un concepto de matriz abstracta (matriz-volteo) a LinAlg , acoplando fuertemente Base y LinAlg .

    La propuesta dos separa limpiamente las cosas de matriz abstracta y álgebra lineal, permitiendo que la primera viva únicamente en la base y la última únicamente en LinAlg , sin un nuevo acoplamiento.

  4. Fallo silencioso versus ruidoso:

    La propuesta uno conduce a un resultado silenciosamente incorrecto cuando uno llama a transpose esperando semántica de transposición (pero en su lugar obtiene semántica de cambio de matriz).

    El análogo bajo la propuesta dos está llamando transpose en una matriz no numérica que espera una semántica de cambio de matriz. En este caso, transpose puede arrojar un error que apunte al usuario a flip .

  5. .' : El argumento principal para no depreciar .' es la longitud de transpose . La propuesta uno reemplaza .' con los nombres transpose y conjadjoint , lo que no mejora esa situación. En contraste, la propuesta dos proporciona los nombres flip y transpose , mejorando esa situación.

Diferencias en los caminos a 1.0

¿Qué implica llegar a 1.0 en cada propuesta? El camino a 1.0 es más simple con la propuesta dos, así que comencemos por ahí.

El camino a 1.0 bajo la propuesta dos

  1. Introduzca tipos de envoltura adjunta perezosa y transposición en LinAlg , digamos Adjoint y Transpose . Introduzca los métodos mul[!] / ldiv[!] / rdiv[!] que se envían en esos tipos de envoltura y que contengan el código de métodos A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] . Vuelva a implementar los últimos métodos como hijos cortos de los primeros.

    Este paso no rompe nada e inmediatamente permite la eliminación de la reducción especial y la desaprobación de .' :

  2. Elimine la reducción especial que produce A[c|t]_{mul|ldiv|rdiv}_B[c|t] , en lugar de reducir simplemente ' / .' a Adjoint / Transpose ; expresiones anteriormente especialmente reducidas que producen llamadas A[c|t]_{mul|ldiv|rdiv}_B[c|t] luego se convierten en llamadas equivalentes mul / ldiv / rdiv . Retirar A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] a los métodos mul[!] / ldiv[!] / rdiv[!] . Depreciar .' .

    Estos pasos se pueden realizar en 0.7. Solo rompen dos cosas: (1) El código que depende de la reducción especial para alcanzar los métodos A[c|t]_{mul|ldiv|rdiv}_B[c|t] para tipos que no son Base / LinAlg se romperá. Dicho código emitirá explícitamente MethodError s indicando a qué rinde la nueva reducción / hacia qué necesita migrar el código roto. (2) El código que se basa en ' s / .' s aislados que se comportan estrictamente con ansiedad se romperá. El modo de falla común también debe ser explícito MethodError s. A su alrededor, la rotura es limitada y ruidosa.

    Y eso es todo para los cambios estrictamente necesarios para 1.0.

    En este punto, Adjoint(A) / Transpose(A) produciría un adjunto y una transposición perezosos, y adjoint(A) / transpose(A) produciría un adjunto y una transposición ansiosos. Los últimos nombres podrían permanecer indefinidamente o, si se desea, quedar en desuso a alguna otra ortografía en 0.7, por ejemplo eagereval(Adjoint(A)) / eagereval(Transpose(A)) modulo ortográfico de eagereval o eageradjoint(A) / eagertranspose(A) . En el caso de depreciación, adjoint / transpose podrían reutilizarse en 1.0 (aunque con Adjoint(A) / Transpose(A) alrededor, no estoy seguro de si eso sería necesario).

    Finalmente...

  3. Introduzca flip y / o Flip en Base . Al ser una característica adicional, este cambio puede ocurrir en 1.x si es necesario.

El camino a 1.0 bajo la propuesta uno

¿Y la propuesta uno? A continuación se describen dos posibles caminos. El primer camino consolida cambios en 0,7 pero implica una ruptura silenciosa. El segundo camino evita la rotura silenciosa, pero implica más cambios 0,7-> 1,0. Ambos esquemas evolucionan continuamente el código base; los equivalentes menos continuos podrían consolidar / evitar algún trabajo / abandono, pero probablemente serían más desafiantes y propensos a errores. El trabajo en curso aparentemente sigue el primer camino.

Primer camino bajo la propuesta uno (con rotura silenciosa)
  1. Cambie la semántica de transpose de transponer a array-flip, y necesariamente también la semántica de todas las funciones relacionadas con transpose . Por ejemplo, todos los métodos A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] que comienzan At_... o terminan ..._Bt[!] potencialmente necesitan una revisión semántica. También debe cambiar, por ejemplo, las definiciones y el comportamiento de Symmetric / issymmetric . Mueva transpose a Base .

    Estos cambios se romperían silenciosa y ampliamente.

  2. Introduzca conjadjoint en LinAlg . Este paso requiere restaurar todos los métodos tocados en el paso anterior, pero en su forma semántica original, y ahora con diferentes nombres asociados con conjadjoint (digamos Aca_... y ..._Bca[!] nombres) . También requiere agregar métodos para las combinaciones de tipos adicionales que admiten simultáneamente array-flip (ahora transpose ), transponer (ahora conjadjoint ) y adjunto en LinAlg requiere (por ejemplo, el ca variantes entre A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] ).

  3. Introduzca adjunto perezoso y transposición (llamado conjadjoint ) en LinAlg , digamos Adjoint y ConjAdjoint . Introduzca un tipo de contenedor de cambio de matriz perezoso (llamado transpose ) en Base , digamos Transpose . Introduzca los métodos mul[!] / ldiv[!] / rdiv[!] que se envíen en esos tipos de envoltura y que contengan el código de métodos A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] . Vuelva a implementar los últimos métodos como hijos cortos de los primeros.

  4. Elimine la reducción especial que produce A[c|t]_{mul|ldiv|rdiv}_B[c|t] , en lugar de reducir simplemente ' / .' a Adjoint / Transpose ; expresiones anteriormente especialmente reducidas que producían llamadas A[c|t]_{mul|ldiv|rdiv}_B[c|t] luego se convierten en llamadas equivalentes mul / ldiv / rdiv (aunque recuerde que la semántica habrá cambiado silenciosamente). Retirar A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] a los métodos mul[!] / ldiv[!] / rdiv[!] . De manera similar, elimine los métodos Aca_... / ...Bca[!] recientemente introducidos a favor de los mul[!] / ldiv[!] / rdiv[!] . Depreciar .' .

Estos cambios tendrían que ir en 0,7. Los cambios semánticos en las operaciones existentes producirían una ruptura amplia y silenciosa. La eliminación especial del descenso produciría la misma rotura cerrada y ruidosa descrita anteriormente.

En este punto Adjoint(A) / Transpose(A) / ConjAdjoint(A) producirían, respectivamente, adjunto perezoso, cambio de matriz y transposición, y adjoint(A) / transpose(A) / conjadjoint(A) produciría, respectivamente, adjunto ansioso, cambio de matriz y transposición. Los últimos nombres podrían permanecer indefinidamente o, si se desea, ser obsoletos para alguna otra ortografía también en 0.7 (ref. Arriba).

Se podría introducir ConjAdjoint antes en este proceso para consolidar / evitar algo de trabajo / abandono, aunque es probable que ese enfoque sea más desafiante y propenso a errores.

Segundo camino bajo la propuesta uno (evitando roturas silenciosas)
  1. Introduzca ansiosos conjadjoint en LinAlg . Migre todas las funciones y métodos asociados actualmente con transpose (incluidos, por ejemplo, A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] métodos que involucran t ) a conjadjoint y nombres derivados. Haga que todos los nombres relacionados con transpose hijos cortos de los nuevos conjadjoint . Despreciar todos los transpose -relacionado nombres a conjadjoint equivalentes.

  2. Introduzca los tipos de envoltorio adjunto perezoso y transposición (llamado conjadjoint ) en LinAlg , digamos Adjoint y ConjAdjoint . Introduzca los métodos mul[!] / ldiv[!] / rdiv[!] que se envíen en esos tipos de envoltura y que contengan el código de métodos A[c|ca]_{mul|ldiv|rdiv}_B[c|ca][!] . Vuelva a implementar los últimos métodos como hijos cortos de los primeros.

  3. Introduzca un tipo de envoltura de arrastre perezoso en Base , llamado Transpose (algo confuso, ya que simultáneamente transpose está obsoleto a conjadjoint con semántica de transposición). Agregue todos los métodos generales necesarios para que array-flip ( Transpose ) funcione en Base . Luego agregue métodos a LinAlg para todas las combinaciones de tipos adicionales que soportan simultáneamente array-flip (ahora Transpose , pero no transpose ), transponer (ahora conjadjoint y ConjAdjoint ), y adjunto en LinAlg requiere (por ejemplo, los mul[!] / rdiv[!] / ldiv[!] equivalentes de A[c|t|ca]_{mul|ldiv|rdiv}_B[c|t|ca][!] que no existen actualmente).

  4. Elimine la reducción especial que produce A[c|t]_{mul|ldiv|rdiv}_B[c|t] , en lugar de reducir simplemente ' / .' a Adjoint / Transpose ; expresiones anteriormente especialmente reducidas que producían llamadas A[c|t]_{mul|ldiv|rdiv}_B[c|t] luego se convierten en llamadas equivalentes mul / ldiv / rdiv . Retirar A[c|t]_{mul|ldiv|rdiv}_B[c|t][!] a los métodos mul[!] / ldiv[!] / rdiv[!] . Depreciar .' .

Los cambios anteriores deberían ir en 0,7. Sin rotura silenciosa, solo la rotura ruidosa de la eliminación de descenso especial como se describe anteriormente.

En este punto Adjoint(A) / Transpose(A) / ConjAdjoint(A) producirían respectivamente adjunto diferido, cambio de matriz y transposición. adjoint(A) / conjadjoint(A) producirían respectivamente un adjunto y una transposición ansiosos. transpose(A) quedaría obsoleto a conjadjoint ; transpose podría reutilizarse en 1.0 si se desea. adjoint podría permanecer indefinidamente o quedar obsoleto a favor de otra ortografía en 0.7. Otra forma de escribir conjadjoint podría ir directamente a 0.7.

Uno podría consolidar un poco los pasos 1 y 2, evitando algo de trabajo / abandono, aunque es probable que ese enfoque sea más desafiante y propenso a errores.

#

¡Gracias por leer!

Gracias por la gran escritura. En general, también estoy de acuerdo con la propuesta 2, con una razón adicional de que el adjoint conjugado no es en absoluto una terminología estándar y personalmente lo encuentro muy confuso (ya que adjoint a veces se usa como terminología para la transposición simple en ciertas ramas de las matemáticas, y el adjetivo adicional conjugate parece implicar que tiene lugar la conjugación).

Intenté leer las elaboradas discusiones anteriores, pero no pude ver en qué punto se decidió / motivó que un transpose matemático debería ser recursivo. Esto pareció ser el resultado de una discusión privada (en algún momento entre el 14 y el 18 de julio), después de lo cual de repente se introdujo una transposición matemática y estructural.
@ Sacha0 lo describe como

Cómo surgió eso: anteriormente, la "transposición estructural" se combinaba con "adjunto matemático" / "transposición matemática"

Estoy de acuerdo en que la transposición (estructural) se combinó con "adjunto", pero aquí aparece la "transposición matemática" de la nada. En aras de la integridad / autosuficiencia, ¿podría ese concepto y por qué debería ser recursivo ser brevemente reiterado / resumido?

En relación con eso, creo que nadie está usando realmente transpose en el sentido matemático abstracto, como una operación en mapas lineales , por la simple razón de que la transposición de una matriz sería un objeto que no actúa sobre vectores pero en RowVector s, es decir, mapearía RowVector a RowVector . Dada la falta de argumentación para la transposición recursiva, realmente no veo ninguna distinción entre la transposición de matriz simple que se define típicamente como un concepto (dependiente de la base) y la operación flip recientemente propuesta.

Gracias @ Sacha0 por el gran escrito. Estoy a favor de la propuesta 2 (Llamar adjunto adjoint , transponer transpose y cambiar matriz flip . adjoint y transpose en vivo en LinAlg , y flip vive en Base .). Para mí, esto parece dar el mejor resultado final (que debería ser la principal preocupación), y también permite una forma más limpia para v1.0. Estoy de acuerdo con sus puntos anteriores, así que no los repetiré, pero aquí hay algunos comentarios adicionales.

Un argumento a favor del transpose no recursivo es que la sintaxis .' es muy conveniente y, por lo tanto, puede usarse para, por ejemplo, Matrix{String} . Suponiendo que la sintaxis desaparezca (# 21037) y uno tenga que usar transpose(A) lugar de A.' , entonces creo que una función flip sería una adición bienvenida (más corta y más claro que transpose(A) , y definitivamente mejor que permutedims(A, (2,1)) ).

El desacoplamiento entre LinAlg y Base que da la propuesta 2 también es muy bueno. No solo porque LinAlg solo necesita preocuparse por cómo manejar Transpose y Adjoint , sino que también deja en claro que consideramos transpose y adjoint para ser LinAlg operaciones, y flip para ser una cosa AbstractMatrix genérica que simplemente invierte las dimensiones de una matriz.

Por último, no he visto muchas quejas acerca de que transpose(::Matrix{String}) etc. no funciona. ¿Es realmente un caso de uso común? En la mayoría de los casos, debería ser mejor construir la matriz invertida desde el principio de todos modos.

Este esquema de denominación (propuesta 2) podría ser mejor. Adjoint y transpose son términos bastante estándar en álgebra lineal, y se rompen menos ...

Pero no pudo ver en qué momento se decidió / motivó que una transposición matemática debería ser recursiva

@Jutho Es el mismo argumento que el adjunto (el adjunto de un número complejo es su conjugado (dado que los números complejos son espacios vectoriales válidos de rango uno y operadores lineales, se aplican el producto interno y los conceptos adjuntos), y el adjunto de una matriz de bloques es recursivo ...). La gente puede querer hacer álgebra lineal con matrices reales anidadas, por ejemplo. Todavía veo mucho código usando .' para el "adjunto de una matriz o vector real", y también solía hacer esto. Aquí, ' no solo sería válido sino que también sería más genérico (trabajando para matrices que podrían tener un valor complejo y / o anidar). El uso más genuino de la transposición recursiva en matrices complejas anidadas ocurre cuando sus ecuaciones le dan conj(adjoint(a)) , que es relativamente más raro pero aún sucede (es lo suficientemente raro que estoy feliz si no hay una función asociada con él - en el En las discusiones anteriores y en otros lugares, varias personas comenzaron a llamarlo de diferentes maneras, como "transposición matemática", elegí conjadoint , pero nada de eso es una gran terminología en mi opinión, excepto tal vez transpose sí). En álgebra lineal, la operación no recursiva que reordena los bloques de una matriz de bloques pero no hace nada a los bloques en sí, generalmente no aparece.

Por último, no he visto muchas quejas acerca de que transpose(::Matrix{String}) etc. no funcionan. ¿Es realmente un caso de uso común? En la mayoría de los casos, debería ser mejor construir la matriz invertida desde el principio de todos modos.

Creo que este tipo de cosas es bastante deseable con las operaciones de transmisión de Julia. Por ejemplo, es bastante común querer tomar el producto externo (cartesiano) de algunos datos y quizás mapearlo a través de una función. Entonces, en lugar de algo como map(f, product(a, b)) podemos usar broadcast(f, a, transpose(b)) o simplemente f.(a, b.') . Es mucho poder en pocos personajes.

Mis pensamientos: para evitar agregar aún más nombres de funciones a este espacio (es decir, flip ), me pregunto si podríamos tener un valor predeterminado para la permutación en permutedims(a) = permutedims(a, (2,1)) . Desafortunadamente, esto no es tan corto como flip , pero significativamente menos desagradable que el formulario completo permutedims(a, (2,1)) (y tiene el efecto secundario de enseñar a los usuarios sobre la función más general).

( @ Sacha0 Muchas gracias por su comentario aquí. FYI en la elección entre Adjoint y Transpose envoltorios, o RowVector + MappedArray + lo que sea para volteado matrices como se planeó anteriormente (tal vez solo PermutedDimsArray ), todavía tiendo a favorecer lo último ...)

El uso más genuino de la transposición recursiva en matrices complejas anidadas ocurre cuando sus ecuaciones le dan conj(adjoint(a))

Una vez que obtenga conj(adjoint(a)) en sus ecuaciones, ¿cómo sabe que en realidad es conj que desea aplicar a sus entradas de matriz y tal vez no adjoint , es decir, tal vez realmente desee adjoint.(adjoint(a)) .

Esto probablemente hará que me baneen / expulsen, pero no soy un gran fanático de toda la idea recursiva. Tampoco me opongo necesariamente, simplemente no creo que haya una razón matemáticamente rigurosa para esto, ya que los vectores y operadores / matrices no se definen de forma recursiva, se definen sobre campos de escalares (y por anillos de extensión si lo desea para trabajar también con módulos en lugar de espacios vectoriales). Y entonces Base.LinAlg debería funcionar en ese caso. El argumento principal

El adjunto de un vector debe ser un operador lineal mapeándolo a un escalar. Es decir, a '* a debe ser un escalar si a es un vector.

es incompatible con el funcionamiento de Julia Base: haga a=[rand(2,2),rand(2,2)] y observe a'*a . No es un escalar. Entonces, ¿es a una matriz ahora, porque es un vector lleno de matrices? Lo anterior es solo un escalar si de hecho tenía la intención de usar el anillo de matrices 2x2 como escalares sobre los que definió un módulo. Pero en esos contextos, no me queda claro que el resultado anterior sea el esperado. De todos modos, el producto interno euclidiano rara vez se usa en el contexto de módulos, etc., por lo que no creo que Base deba molestarse en definir el comportamiento correcto para él.

Entonces, realmente, aunque la declaración anterior podría ser un intento de extender la motivación más allá de interpretar matrices llenas de matrices como matrices de bloques, al final creo que la interpretación de la matriz de bloques fue la única motivación verdadera para hacer adjoint y ahora incluso transpose (que nunca se usa en el sentido matemático pero siempre en el sentido de índices de matriz invertida) recursivo en primer lugar. Si define un nuevo tipo de campo escalar usted mismo, es una carga extraña tener que definir adjoint y transpose además de conj , antes de poder usarlo en una matriz.

Entonces, ¿por qué, con un sistema de tipos tan poderoso como el de Julia, no solo tener un tipo dedicado para matrices de bloques? Y así, jugando al abogado del diablo:

  • ¿Cuántas personas están realmente usando / confiando en el comportamiento recursivo actual de adjunto para cualquier trabajo práctico?
  • ¿Hay otros lenguajes que utilicen un enfoque tan recursivo? (Matlab, usando celdas de matrices, no lo hace)

Como dije, no necesariamente me opongo, solo cuestiono la validez (incluso después de haber leído toda la discusión anterior), especialmente para el caso transpose .

Esto probablemente hará que me expulsen

Me doy cuenta de que esto es una broma, pero tener una opinión impopular no hará que nadie sea prohibido. Solo el mal comportamiento que se prolonga durante un período prolongado puede llevar a la prohibición. Incluso entonces la prohibición no es

Simplemente no creo que haya una razón matemáticamente rigurosa para esto, ya que los vectores y operadores / matrices no se definen de forma recursiva, se definen sobre campos de escalares (y por anillos de extensión si desea trabajar también con módulos en lugar de espacios vectoriales ).

Esta afirmación no tiene ningún sentido para mí. Un vector de vectores, o un vector de funciones, o un vector de matrices, todavía forma un espacio vectorial (sobre un campo escalar subyacente) con + y * scalar definidos recursivamente, y de manera similar forma un espacio de producto interior con el producto interior definido de forma recursiva. La idea de que los vectores sólo pueden ser "columnas de escalares" desafía tanto las definiciones como el uso común en matemáticas, física e ingeniería.

Haga a=[rand(2,2),rand(2,2)] y observe a'*a . No es un escalar.

En este caso, el "anillo escalar" de a es el anillo de matrices 2x2. Así que este es un problema lingüístico, no un problema con la forma en que funciona el adjunto.

Discutir sobre cómo debería funcionar a'*a partir de cómo funciona actualmente (o no funciona) en Julia parece bastante circular.

Si define un nuevo tipo de campo escalar usted mismo, es una carga extraña que necesite definir adjoint y transpose además de conj , antes de poder usarlo en una matriz

Creo que esta carga es más estética que práctica. Si está de acuerdo con convertirlo en un subtipo de Number , no tiene que definir nada e incluso si cree que Number no es lo correcto para usted, las definiciones son tan triviales que agregarlos no es una carga.

Entonces, ¿por qué, con un sistema de tipos tan poderoso como el de Julia, no solo tener un tipo dedicado para matrices de bloques?

https://github.com/KristofferC/BlockArrays.jl/ Los colaboradores son bienvenidos :)

Mis disculpas por la broma de la "prohibición". Esta será mi última reacción para no descarrilar más esta discusión. Está claro que el adjunto recursivo (pero tal vez no la transposición) está resuelto, y no estoy tratando de deshacer esa decisión. Solo estoy tratando de señalar algunas inconsistencias.

@StefanKarpinski : El ejemplo del anillo escalar está exactamente en contradicción con la recursividad: ¿son las matrices 2x2 en sí mismas los escalares, o la definición es recursiva y el tipo Number subyacente es el escalar?

En el primer caso, en realidad tiene un módulo en lugar de un espacio vectorial y probablemente depende del caso de uso si desea conj o adjoint en esos 'escalares', si es que lo desea. utilizando productos internos y adjuntos en absoluto.

Estoy bien con aceptar la última definición. Estoy usando elementos arbitrarios en subespacios invariantes de grupo de productos tensoriales graduados de espacios super vectoriales en mi trabajo, por lo que no estoy realmente confinado a columnas de números en mi definición de vectores. Pero los vectores de matrices, son solo vectores puros o también deberían heredar algún comportamiento de matriz. En el primer caso, dot(v,w) debería producir un escalar, probablemente llamando de forma recursiva a vecdot en sus elementos. En el último caso, al menos vecdot(v,w) debería producir un escalar actuando de forma recursiva. Por lo tanto, sería una solución mínima para ser coherente con el adjunto recursivo.

Pero parece más simple no tener un transpose recursivo y permitir que se use también para aplicaciones no matemáticas, ya que creo que incluso en las ecuaciones matriciales típicas tiene muy poco que ver con la transposición matemática real de un mapa lineal.

Creo que dot debería llamar a dot recursivamente, no vecdot , por lo que sería un error para un vector de matrices ya que no definimos un producto interno canónico para matrices.

Siempre me sorprende que la transposición sea recursiva ... y no puedo imaginar que nadie no lo sea. (Supongo que las opiniones sobre la "transposición de un mapa lineal" en el artículo de Wikipedia se han triplicado al menos debido a esta discusión).

Yo también he encontrado a menudo las definiciones recursivas inquietantes y, en general, he compartido la opinión expresada anteriormente por @Jutho (pero, de nuevo, hemos tenido una formación muy similar).

Creo que he descubierto la inconsistencia que me ha estado molestando aquí: seguimos usando anillos y campos como la incrustación de matrices 2x2 de números complejos como ejemplo aquí, pero en realidad no funciona ni motiva la transposición recursiva (y adjunto ).

Dejame explicar. Las cosas con las que estoy ampliamente de acuerdo

  1. Los escalares son operadores lineales de rango 1 válidos. Dado el poderoso sistema de tipos de Julia, no hay ninguna razón por la que LinAlg conceptos como adjoint no se apliquen, por ejemplo, deberíamos definir adjoint(z::Complex) = conj(z) . (Más allá de los vectores y matrices de escalares, los conceptos de LinAlg también pueden extenderse (por parte de los usuarios, supongo) a otros objetos - @stevengj mencionado, por ejemplo, espacios vectoriales de tamaño infinito (espacios de Hilbert)).
  2. Deberíamos poder tratar de alguna manera con escalares con diferentes representaciones. El ejemplo prototípico aquí es un número complejo z = x + y*im se puede modelar como Z = x*[1 0; 0 1] + y*[0 1; -1 0] y las operaciones + , - , * , / y \ se conservan en este isomorfismo. (Sin embargo, tenga en cuenta que conj(z) va a adjoint(Z) / transpose(Z) / flip(Z) - más sobre eso más adelante).
  3. Las matrices de bloques deberían ser posibles de alguna manera (el enfoque actual se basa en adjoint recursivos de forma predeterminada, etc.).

Parece razonable que Base.LinAlg sea ​​compatible con 1 y 2, pero IMO 3 solo debería hacerse en Base si encaja de forma natural (de lo contrario, tendería a ceder a paquetes externos como https: / /github.com/KristofferC/BlockArrays.jl).

Ahora me doy cuenta de que hemos estado combinando 2 y 3 y esto conduce a algunas inconsistencias ( @Jutho también lo señaló). A continuación, deseo mostrar que 3. el uso recursivo de adjoint y otras operaciones según las matrices de bloques no nos da 2. La forma más sencilla es mediante el ejemplo. Definamos una matriz 2x2 de números complejos como m = [z1 z2; z3 z4] , la representación isomórfica M = [Z1 Z2; Z3 Z4] , y una matriz de bloques 2x2 estándar b = [m1 m2; m3 m4] donde m1 etc son cuadrados de igual tamaño matrices de Number . Las respuestas semánticamente correctas para operaciones comunes se enumeran a continuación:

| operación | z | Z | m | M | b |
| - | - | - | - | - | - |
| + , - , * | recursivo | recursivo | recursivo | recursivo | recursivo |
| conj | conj(z) | Z' o Z.' o flip(Z) | conj.(m) | adjoint.(M) (o transpose.(M) ) | conj.(b) |
| adjoint | conj(z) | Z' o Z.' o flip(Z) | flip(conj.(m)) o recursivo | flip(transpose.(m)) o recursivo | recursivo |
| trace | z | Z | z1 + z4 | Z1 + Z4 | trace(m1) + trace(m4) |
| det | z | Z | z1*z4 - z2*z3 | Z1*Z3 - Z2*Z3 | det(m1) * det(m4 - m2*inv(m1)*m3) (si m1 invertible, consulte, por ejemplo, Wikipedia ) |

Teniendo en cuenta operaciones como trace y det que devuelven escalares, creo que está bastante claro que nuestro sistema de tipo Julia para LinAlg no podría manejar nuestra incrustación de matriz 2x2 de Complex de manera "automática", infiriendo lo que queremos decir con "escalar" en un momento determinado. Un claro ejemplo es trace(Z) donde Z = [1 0; 0 1] es 2 , mientras que esta es nuestra representación de 1 . De manera similar para rank(Z) .

Una forma de recuperar la coherencia es definir explícitamente nuestra representación 2x2 como escalar, por ejemplo, subtipificando Number como se muestra a continuación:

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

Con esta definición, los métodos genéricos LinAlg probablemente funcionarán bien.

La conclusión que saco de esto: recursivo adjoint es una conveniencia para las matrices de bloques y los vectores de vectores. No debe ser motivada por la corrección matemática para los tipos de "escalar" matrices de 2x2 etiqueté Z anteriormente. Lo veo como nuestra elección si admitimos matrices de bloques o no de forma predeterminada, con los siguientes pros y contras:

Pros

  • Comodidad para los usuarios de arreglos de bloques
  • Los vectores de vectores son espacios vectoriales válidos, y las matrices de bloques son operadores lineales válidos, bajo + , * , conj , etc. Si es posible hacer de esto un isomorfismo natural (a diferencia del ejemplo de Z anterior, que requiere CNumber ), ¿por qué no?

Contras

  • Complejidad adicional significativa para nuestra implementación de LinAlg (que potencialmente podría vivir en otro paquete).
  • Es un poco difícil (pero no imposible) admitir cosas como eig(block_matrix) . Si decimos que LinAlg admite eig y LinAlg admite matrices de bloques, entonces considero que esto es un error hasta que se solucione. Dada la gran cantidad de funcionalidad proporcionada por LinAlg , es difícil ver que alguna vez estaremos "terminados".
  • Inconvenientes para los usuarios de datos que desean utilizar operaciones como transpose de forma no recursiva,

Para mí, la pregunta es: ¿elegimos decir que de forma predeterminada LinAlg espera que los elementos de AbstractArray s sean "escalares" (subtipos o tipificaciones de pato de Number ) y traspasar la complejidad de las matrices de bloques a paquetes externos? ¿O aceptamos la complejidad en Base y LinAlg ?

El plan hasta hace un día había sido el último.

@ Sacha0 He estado intentando completar el trabajo RowVector para admitir los cambios aquí (los anteriores: no recursivos transpose , RowVector cadenas de soporte y otros datos) y ahora me pregunto qué tenías en mente en términos de diseño.

De las discusiones recientes, veo estos casos con una suposición de lo que podría desearse

| | Vector | Matrix |
| - | - | - |
| adjoint | RowVector con recursivo adjoint | AdjointMatrix o TransposedMatrix con adjoint recursivos |
| transpose | RowVector con transpose recursivos | TransposeMatrix con transpose recursivos |
| flip | una copia o PermutedDimsArray ? | una copia o PermutedDimsArray ? |
| conj de AbstractArray | perezoso o ansioso? | perezoso o ansioso? |
| conj de RowVector o TransposedMatrix | perezoso | perezoso |

(Lo anterior intenta separar las preocupaciones del álgebra lineal de las dimensiones de permutación de matrices de datos).

Entonces, algunas preguntas básicas para despegarme:

  • ¿Estamos haciendo transpose recursivos o no? ¿Qué pasa con adjoint ?
  • Si es así, ¿continuaremos asumiendo conj(transpose(array)) == adjoint(array) ?
  • Parece que al menos algunas conjugaciones complejas serán flojas, para soportar, por ejemplo, todas las operaciones actuales de BLAS sin copias adicionales. ¿Hacemos que conj perezosos para todas las matrices?
  • Si introducimos flip , ¿es perezoso o ansioso?

Para su información, probé un enfoque de baja fricción para "voltear" las dimensiones de una matriz en # 24839, usando una forma más corta para permutedims .

Estoy totalmente a favor de la propuesta de @ Sacha0 2. No hemos resuelto por completo los fundamentos teóricos de los adjuntos recursivos y no recursivos, pero independientemente de eso: cómo es ahora ha resultado ser útil, al menos para mí , y quizás no sea aconsejable un cambio de último minuto. La transposición debe seguir al adjunto (= conj∘adjoint ) en este sentido, si es necesario.

FWIW, Mathematica no hace recursivas Transpose ni ConjugateTranspose :

untitled

Intenté leer las elaboradas discusiones anteriores, pero no pude ver en qué punto se decidió / motivó que una transposición matemática debería ser recursiva. [...] En aras de la integridad / autosuficiencia, ¿podría ese concepto y por qué debería ser recursivo ser brevemente reiterado / resumido?

Entiendo y aprecio este sentimiento y me gustaría abordarlo en la medida en que el tiempo lo permita. Lamentablemente, explicar de manera accesible y lúcida por qué el adjunto y la transposición deben ser recursivos por definición es, en el mejor de los casos, un desafío y probablemente no sea posible en breve. Los informes coherentes y completos como el anterior requieren una enorme cantidad de tiempo para construir. Siendo poco tiempo, en el transcurso del día intentaré abordar parte de la confusión en las pocas publicaciones anteriores respondiendo a puntos / preguntas particulares allí; por favor tengan paciencia conmigo mientras lo hago poco a poco :). ¡Mejor!

explicando por qué el adjunto ... debería ser recursivo por definición ...

Casi todos los que han contribuido a esta discusión están de acuerdo en que adjoint(A) debería ser recursivo. El único punto de disputa es si transpose(A) también debe ser recursivo (y si se debe introducir una nueva función flip(A) para transposición no recurrente).

Casi todos los que han contribuido a esta discusión están de acuerdo en que el adjunto (A) debería ser recursivo.

Consulte, por ejemplo, https://github.com/JuliaLang/julia/issues/20978#issuecomment-347777577 y comentarios anteriores :). ¡Mejor!

El argumento del adjunto recursivo es bastante básico para las definiciones. dot(x,y) debe ser un producto interno, es decir, producir un escalar, y la definición de adjunto es que dot(A'*x, y) == dot(x, A*y) . La recursividad (para dot y adjoint ) se deriva de estas dos propiedades.

(La relación con los productos escalares es la razón por la cual adjunto y transponer son operaciones importantes en álgebra lineal. Es por eso que tenemos un nombre para ellos, y no para otras operaciones como rotar matrices en 90 grados ( rot90 en Matlab ).)

Tenga en cuenta que un problema con el uso de flip para transposición no recursiva es que tanto Matlab como Numpy usan flip para lo que llamamos flipdim .

Creo que nadie está realmente usando transposición en el sentido matemático abstracto, como una operación en mapas lineales, por la simple razón de que la transposición de una matriz sería un objeto que no actúa sobre vectores sino sobre RowVectors, es decir, mapearía RowVector a RowVector.

Pero parece más simple no tener una transposición recursiva y permitir que se use también para aplicaciones no matemáticas, ya que creo que incluso en las ecuaciones matriciales típicas tiene muy poco que ver con la transposición matemática real de un mapa lineal.

transponer (que nunca se usa en el sentido matemático, pero siempre en el sentido de índices de matriz invertida)

Esto parecerá un poco indirecto, así que tengan paciencia conmigo :).

El adjoint Julia se refiere específicamente al adjunto hermitiano . En general, para U y V espacios vectoriales normalizados completos (espacios de Banach) con espacios duales respectivos U * y V , y para el mapa lineal A: U -> V, entonces el adjunto de A, típicamente denotado A , es un mapa lineal A *: V * -> U * . Es decir, en general, el adjunto es un mapa lineal entre espacios duales, al igual que en general la transposición A ^ t es un mapa lineal entre espacios duales como se mencionó anteriormente. Entonces, ¿cómo conciliar estas definiciones con la noción familiar de adjunto hermitiano? :)

La respuesta radica en la estructura adicional que poseen los espacios en los que normalmente se trabaja, es decir, espacios de producto internos completos (espacios de Hilbert). Un producto interno induce una norma, por lo que un espacio de producto interno completo (Hilbert) es un espacio normado completo (Banach) y, por lo tanto, apoya el concepto de adjunto (hermitiano). Aquí está la clave: al tener un producto interno en lugar de una simple norma, se aplica uno de los teoremas más hermosos del álgebra lineal, a saber, el teorema de representación de Riesz. En pocas palabras, el teorema de representación de Riesz establece que un espacio de Hilbert es naturalmente isomorfo a su espacio dual. En consecuencia, cuando trabajamos con espacios de Hilbert, generalmente identificamos los espacios y sus duales y descartamos la distinción. Hacer esa identificación es cómo se llega a la noción familiar de adjunto hermitiano como A *: V -> U en lugar de A *: V * -> U * .

Y la misma identificación se hace generalmente para la transposición cuando se consideran los espacios de Hilbert, de modo que también A ^ t: V -> U, lo que produce la noción familiar de transposición. Entonces, para aclarar, sí, la noción común de transposición es la noción general de transposición aplicada al entorno más familiar (Hilbert), al igual que la noción común de adjunto hermitiano es la noción general de adjunto aplicada a ese entorno. ¡Mejor!

Disculpas por vencer a un caballo muerto, pero pensé que resumiría brevemente los problemas de la confusión matemática de una manera diferente. El punto clave del desacuerdo es cómo interpretar matemáticamente un objeto Vector{T} cuando T no es solo un subtipo de Number sin una subestructura similar a una matriz.

Una escuela de pensamiento acepta la afirmación de

un vector de vectores sobre un anillo R se entiende mejor como un espacio de suma directa, que también es un espacio vectorial sobre R.

Modulo ciertas sutilezas con respecto a espacios vectoriales de dimensión infinita, etc., esto básicamente significa que deberíamos pensar en vectores de vectores como vectores de bloque. Por lo tanto, un Vector{Vector{T<:Number}} debe "aplanarse" mentalmente en un Vector{T} simple. Dentro de este paradigma, los operadores lineales que se representan como matrices de matrices deben considerarse de manera similar como matrices de bloques, y adjoint ciertamente deben ser recursivos, al igual que transpose si estamos usando la palabra en sentido matemático . Por favor, corrígeme si me equivoco, pero creo que las personas que adoptan este punto de vista piensan que algo como Vector{Matrix{T}} no tiene una interpretación lo suficientemente natural como para que debamos diseñarlo. (En particular, no debemos simplemente asumir que el interior Matrix es una representación matricial de un número complejo, porque como dijo @stevengj ,

Si representa números complejos mediante matrices de 2x2, tiene un espacio vectorial complejo diferente.

)

La otra escuela de pensamiento es que un Vector{T} siempre debe pensarse en una representación de un vector en un espacio vectorial abstracto sobre escalares (en el sentido de álgebra lineal de la palabra) de tipo T , independientemente del tipo T . En este paradigma, un Vector{Vector{T'}} no debe considerarse como un elemento de un espacio de suma directa, sino como un vector sobre los escalares Vector{T'} . En este caso, transpose(Matrix{T}) no debería ser recursivo, sino simplemente invertir la matriz exterior. Un problema con esta interpretación es que para que los elementos de tipo T formen un anillo válido de escalares, debe haber una noción bien definida de suma (conmutativa) y de multiplicación. Para un vector como Vector{Vector{T'}} , necesitaríamos una regla para multiplicar dos "escalares" Vector{T'} en otro Vector{T'} . Si bien uno ciertamente podría llegar a una regla de este tipo (por ejemplo, la multiplicación por elementos, que reduce el problema a cómo multiplicar T' ), no existe una forma natural universal de hacerlo. Otro problema es cómo funcionaría adjoint bajo esta interpretación. El adjunto hermitiano solo se define en operadores lineales sobre un espacio de Hilbert, cuyo campo escalar por definición debe ser los números reales o complejos. Entonces, si queremos aplicar adjoint a una matriz Matrix{T} , debemos asumir que T es una representación de los números complejos (o números reales, pero me limitaré con complejo porque ese caso es más sutil). En esta interpretación, adjoint no debe ser recursivo sino que debe voltear la matriz externa y luego aplicar conjugate . Pero hay más problemas aquí, porque la acción correcta de conjugate(T) depende de la naturaleza de la representación. Si es la representación 2x2 descrita en Wikipedia, entonces conjugate debería voltear la matriz. Pero por las razones descritas anteriormente, conjugate definitivamente no siempre debería invertir las matrices. Así que este enfoque sería un poco complicado de implementar.

Aquí están mis propios pensamientos: no hay una respuesta "objetivamente correcta" en cuanto a si transpose debería ser recursivo cuando se aplica a matrices cuyos elementos tienen una subestructura aún más complicada. Depende de cómo el usuario elija exactamente representar su estructura algebraica abstracta en Julia. Sin embargo, soportar anillos de escalares completamente arbitrarios parece ser muy difícil, así que creo que, por razones prácticas, no deberíamos intentar ser tan ambiciosos y deberíamos olvidarnos de las matemáticas esotéricas de los módulos en lugar de los anillos no estándar. Ciertamente deberíamos tener una función en Base (con una sintaxis más simple que permutedims(A, (2,1)) ) que no tiene nada que ver con el concepto de transposición del álgebra lineal y simplemente invierte matrices y no hace nada recursivo, independientemente de si es llamado transpose o flip o qué. Sería bueno si adjoint y una función de transposición separada (posiblemente con un nombre diferente) en LinAlg fueran recursivas, porque entonces podrían manejar vectores / matrices de bloques y la implementación simple de la suma directa como vectores de vectores, pero esto no es requerido por la "corrección matemática objetiva", y estaría bien tomar esa decisión simplemente por motivos de facilidad de implementación.

A pesar de mi promesa anterior de no comentar más, desafortunadamente tengo que responder a esta.

El adjunto de Julia se refiere específicamente al adjunto hermitiano. En general, para U y V espacios vectoriales normalizados completos (espacios de Banach) con espacios duales respectivos U * y V , y para el mapa lineal A: U -> V, entonces el adjunto hermitiano de A, típicamente denotado A , es un mapa lineal A *: V * -> U *. Es decir, en general, el adjunto hermitiano es un mapa lineal entre espacios duales, al igual que en general la transposición A ^ t es un mapa lineal entre espacios duales como se mencionó anteriormente. Entonces, ¿cómo reconcilia estas definiciones con la noción familiar de adjunto hermitiano? :)

Realmente eso no es cierto. Lo que está describiendo aquí es realmente la transposición, pero (como ya mencioné), en algunos campos esto se denomina adjunto (sin hermitiano) y se denota como A ^ t o A ^ * (nunca A ^ daga). De hecho, se extiende mucho más allá de los espacios vectoriales, y en la teoría de categorías tal concepto existe en cualquier categoría monoidal (por ejemplo, la categoría Cob de variedades orientadas n-dimensionales con cobordismos como mapas lineales), donde se lo conoce como compañero adjunto (en De hecho, puede haber dos diferentes A y A , ya que el espacio dual izquierdo y derecho no son necesariamente iguales). Pero tenga en cuenta que esto nunca implica una conjugación compleja. Los elementos de V * son de hecho los mapas lineales f: V-> Escalar, y para un mapa lineal A: U-> V y un vector v de U, tenemos f (Av) = (A ^ tf) (v) . Dado que la acción de f no implica una conjugación compleja, tampoco la definición de A ^ t.

La respuesta radica en la estructura adicional que poseen los espacios en los que normalmente trabaja, es decir, espacios de productos internos completos (espacios de Hilbert). Un producto interno induce una norma, por lo que un espacio de producto interno completo (Hilbert) es un espacio normado completo (Banach) y, por lo tanto, apoya el concepto de adjunto hermitiano. Aquí está la clave: al tener un producto interno en lugar de una simple norma, se aplica uno de los teoremas más hermosos del álgebra lineal, a saber, el teorema de representación de Riesz. En pocas palabras, el teorema de representación de Riesz establece que un espacio de Hilbert es naturalmente isomorfo a su espacio dual. En consecuencia, cuando trabajamos con espacios de Hilbert, generalmente identificamos los espacios y sus duales y descartamos la distinción. Hacer esa identificación es cómo se llega a la noción familiar de adjunto hermitiano como A *: V -> U en lugar de A *: V * -> U *.

Una vez más, no creo que sea del todo correcto. En los espacios de producto interno, el producto interno es una forma sesquilínea dot de conj (V) x V -> Escalar (con conj (V) el espacio vectorial conjugado). Esto permite de hecho establecer un mapa de V a V * (o técnicamente de conj (V) a V *), que es de hecho el teorema de representación de Riesz. Sin embargo, no necesitamos eso para introducir adjunto hermitiano. Realmente, el producto interno dot es suficiente en sí mismo, y el adjunto hermitiano de un mapa lineal A es tal que
dot(w, Av) = dot(A' w, v) . Esto implica una conjugación compleja.

Realmente eso no es cierto. Lo que está describiendo aquí es realmente la transposición, pero (como ya mencioné), en algunos campos esto se denomina adjunto (sin hermitiano) y se denota como A ^ t o A ^ * (nunca A ^ daga). [...]

@Jutho , consulte, por ejemplo, la página de Wikipedia sobre el anexo hermitiano .

Tal vez haya inconsistencias entre diferentes campos de las matemáticas, pero:
https://en.wikipedia.org/wiki/Transpose_of_a_linear_map
y en particular
https://en.wikipedia.org/wiki/Transpose_of_a_linear_map#Relation_to_the_Hermitian_adjoint
e incontables referencias en la teoría de categorías, por ejemplo
https://arxiv.org/pdf/0908.3347v1.pdf

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

@Jutho , no veo ninguna inconsistencia entre esa sección de la página y las definiciones dadas en la página que vinculé arriba, ni veo ninguna inconsistencia con lo que publiqué arriba. ¡Mejor!

También firmaré la propuesta de permutedims por ahora también; Creo que es mejor que flip .

@ Sacha0 , entonces tenemos una forma diferente de interpretar eso. Leí esto como
Para un A dado: U-> V,
transpose (A) = dual (A) = (a veces también) adjoint (A): V * -> U *
adjunto hermitiano (A) = daga (A) = (típicamente solo) adjunto (A): V-> U
y la relación entre ambos se obtiene exactamente utilizando el mapa del espacio al espacio dual (es decir, Riesz ...), que implica una conjugación compleja. Por lo tanto, adjunto hermitiano implica conjugación, transponer no.

Si también quiere llamar adjunto hermitiano al primero, ¿cómo se llama transponer? Realmente no definiste qué era eso en tu descripción, solo mencionaste

El adjunto hermitiano de A, típicamente denotado A , es un mapa lineal A : V * -> U *. Es decir, en general el adjunto hermitiano es un mapa lineal entre espacios duales, al igual que en general la transposición A ^ t es un mapa lineal entre espacios duales

Entonces, ¿la transposición y el hermitiano son dos formas diferentes de transformar A: U-> V en un mapa de V -> U ? Realmente estaría feliz de discutir más sobre esto, pero creo que será mejor que lo hagamos en otro lugar. Pero en serio, contácteme ya que en realidad estoy muy interesado en aprender más sobre esto.

También consulte http://staff.um.edu.mt/jmus1/banach.pdf para obtener una referencia de que adjunto, como se usa en el contexto de los espacios de Banach, es realmente una transposición, y no un adjunto hermitiano (en particular, es un lineal y no un antilineal transformación). Wikipedia (y otras referencias) están realmente fusionando estos dos conceptos, utilizando la noción de adjunto hermitiano en los espacios de Hilbert como motivación para una definición generalizada de adjunto en los espacios de Banach. Sin embargo, esta última es realmente transpuesta (y no necesita un producto interno, ni una norma). Pero esa es la transposición de la que estaba hablando, que nadie está usando realmente en código de computadora.

Para Julia Base: no me opongo a la conjugación hermitiana recursiva; Estoy de acuerdo en que a menudo será lo correcto. Simplemente no estoy seguro de que Base deba intentar hacer cosas inteligentes cuando el tipo de elemento no es Number . Incluso con T es un número, no hay soporte en Base para el uso mucho más común de productos internos no euclidianos (y definiciones modificadas asociadas de adjunto), ni creo que debería haberlo. Así que creo que la motivación principal fueron las matrices de bloques, pero creo que un tipo de propósito especial (en Base o en un paquete) es mucho más juliano y, como también mencionó @andyferris , no es como el resto de LinAlg apoya esa noción de matrices de bloques, incluso cosas simples como inv (y mucho menos factorizaciones de matrices, etc.).

Pero si la conjugación hermitiana recursiva está aquí para quedarse (estoy bien), entonces creo que, por coherencia, dot y vecdot deberían actuar recursivamente sobre los elementos. Actualmente, este no es el caso: dot llama x'y a los elementos (que no es lo mismo cuando los elementos son matrices) y vecdot llama dot en los elementos. Entonces, para un vector de matrices, en realidad no hay forma de obtener un resultado escalar. Estaría feliz de preparar un PR si la gente está de acuerdo en que la implementación actual no es realmente inconsistente con adjoint recursivos.

En cuanto a transpose , parece más sencillo hacerlo no recursivo y también permitir que lo utilicen aquellos que no trabajan con datos numéricos. Creo que la mayoría de las personas que trabajan con matrices conocen el término transpose y lo buscarán. Todavía hay conj(adjoint()) para aquellos que necesitan un transpose recursivo.

Triage cree que @ Sacha0 debería seguir adelante con la propuesta 2 para que podamos probarla.

Estoy totalmente de acuerdo con @ttparker en que el adjunto recursivo es una opción, y no la única opción matemáticamente consistente. Por ejemplo, podríamos simplemente decir:

1 - a LinAlg , un AbstractVector v es un vector length(v) -dimensional con pesos básicos (escalares) v[1] , v[2] , ..., v[length(v)] .

(y de manera similar por AbstractMatrix ).

Esta sería probablemente la suposición que muchas personas harían provenientes de otras bibliotecas, y tener una definición tan simple de vectores base, rango, etc., ayuda a mantener la implementación simple. (Muchos probablemente dirían que el álgebra lineal numérica es factible de implementar en una computadora precisamente porque tenemos una buena base para trabajar).

Nuestro enfoque actual es más parecido a:

2 - a LinAlg , un AbstractVector v es una suma directa de length(v) vectores abstractos. También incluimos suficientes definiciones en tipos escalares como Number para que hasta LinAlg sean operadores / vectores lineales unidimensionales válidos.

y de manera similar para las matrices (de bloques). Esto es mucho más generalizado que las implementaciones de álgebra lineal en MATLAB, numpy, eigen, etc., y es un reflejo del poderoso sistema de tipo / despacho de Julia que esto es incluso factible.

La razón principal por la que veo la opción 2 como deseable es nuevamente que el sistema de tipo / envío de Julia nos permite tener un objetivo mucho más amplio, que vagamente es como:

3 - En LinAlg , estamos intentando crear una interfaz de álgebra lineal genérica que funcione para objetos que satisfagan la linealidad (etc.) en + , * , conj (etc.), tratando tales objetos como operadores lineales / miembros de un espacio de Hilbert / lo que sea apropiado.

Lo cual es un objetivo realmente genial (seguramente mucho más allá de cualquier otro lenguaje / biblioteca de programación que yo sepa), motiva completamente recursivo adjoint y 2 (porque + , * y conj son en sí mismos recursivos) y es por eso que la propuesta 2 de una buena opción :)

Realmente estaría feliz de discutir más sobre esto, pero creo que será mejor que lo hagamos en otro lugar. Pero en serio, contácteme ya que en realidad estoy muy interesado en aprender más sobre esto.

¡Salud, hagámoslo! Espero poder seguir charlando sin conexión :). ¡Mejor!

Buen resumen Andy! :)

Andy totalmente de acuerdo, al menos por adjoint (que fue el tema de tu comentario).

Sin embargo, una última súplica por un transpose no recursivo, antes de que me calme para siempre (con suerte).
Veo una transposición no recursiva que tiene las siguientes ventajas:

  • Puede ser utilizado por todas las personas que trabajan con matrices, incluso si contienen datos no numéricos. Así es como probablemente conocerán esta operación y la buscarán, desde otros lenguajes y desde matemáticas básicas extrapoladas a su caso de uso no numérico.
  • No es necesario escribir código adicional para que el tipo perezoso flip o PermutedDimsArray interactúe con LinAlg . ¿Qué pasa si tengo una matriz numérica que volteé en lugar de transponer? ¿Aún podré multiplicarlo con otras matrices (preferiblemente usando BLAS)?

  • Con un transpose recursivo y un adjoint recursivo, podemos tener fácilmente un adjunto no recursivo como conj(transpose(a)) y una transposición recursiva conj(adjoint(a)) . Y aún así, todo interactuará muy bien con LinAlg .

Entonces, cuales son las desventajas. No veo ninguno. Sigo manteniendo mi punto de que realmente nadie está usando transpose en su sentido matemático. Pero en lugar de intentar argumentar más, ¿alguien puede darme un ejemplo concreto en el que se necesite o sea útil una transposición recursiva, y dónde realmente es una tanspuesta matemática? Esto excluye cualquier ejemplo en el que realmente pretendiera usar adjoint pero solo tenga números reales. Entonces, una aplicación donde hay una razón matemática para transponer un vector o matriz llena con más matrices que en sí mismas tienen un valor complejo.

Puedo decir que al menos Mathematica (que uno esperaría haber dedicado una reflexión considerable a esto) no hace transposición recursiva:

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

EDITAR: Vaya, esto también se comentó anteriormente, lo siento

Entonces estoy confundido. Parecía haber un consenso bastante sólido de que transpose debería hacerse no recursivo, por ejemplo, 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 y https://github.com/JuliaLang/julia/pull/23424. Luego @ Sacha0 dio dos propuestas, una de las cuales dejaría la transposición recursiva pero introduciría una función flip no recursiva, que obtuvo un fuerte apoyo a pesar de (que yo sepa) no haber sido planteada como una posibilidad antes. . Entonces @JeffBezanson sugirió que no necesitamos flip después de todo si le damos a permutedims un segundo argumento predeterminado, que también obtuvo un fuerte apoyo.

Así que ahora el consenso parece ser que los únicos cambios reales en transpose deberían ser "entre bastidores": con respecto a la reducción especial y la evaluación perezosa versus ansiosa, que el usuario final típico probablemente no sepa ni le importe . Los únicos cambios realmente visibles son esencialmente cambios de ortografía (depreciando .' y dando permutedims un segundo argumento predeterminado).

Entonces, el consenso de la comunidad parece haber cambiado casi 180 grados en muy poco tiempo (alrededor del momento de la publicación de

Olvidé si alguien sugirió esto, pero ¿podríamos hacer transpose(::AbstractMatrix{AbstractMatrix}) (y posiblemente transpose(::AbstractMatrix{AbstractVector}) también) recursivo y transpose no recursivo de lo contrario? Parece que cubriría todas las bases y no puedo pensar en ningún otro caso de uso en el que quieras que tranpose sea ​​recursivo.

Así que el consenso de la comunidad parece haber cambiado casi 180 grados en muy poco tiempo (alrededor del momento de la publicación de

Si tan solo fuera tan elocuente 😄. Lo que está viendo es que en realidad no se había formado consenso. Más bien, (1) los participantes que están a favor del status quo, pero que se habían retirado de la discusión debido al desgaste, volvieron a expresar una opinión; y (2) otras partes que no habían considerado lo que implicaría en la práctica alejarse del status quo (y cómo eso podría jugar con las consideraciones de liberación) formaron una opinión más fuerte a favor del status quo y expresaron esa opinión.

Tenga en cuenta que esta discusión ha estado en curso de una forma u otra en github desde 2014, y probablemente antes sin conexión. Para los participantes a largo plazo, estas discusiones se vuelven agotadoras y cíclicas. Al haber un trabajo significativo que hacer además de participar en esta discusión, como escribir código, que es más agradable, el resultado es el desgaste entre los participantes a largo plazo. En consecuencia, la conversación parece desequilibrada durante un período u otro. Personalmente, estoy en ese umbral de desgaste, así que me voy a centrar en escribir código ahora en lugar de seguir participando en esta discusión. Gracias a todos y lo mejor! :)

Daré un pequeño voto a favor de la transposición no recursiva y ctranspose para AbstractArrays, siendo ambos recursivos en AbstractArray {T} donde T <: AbstractArray.

Estoy de acuerdo en que el comportamiento recursivo es 'correcto' en algunos casos, y veo la pregunta como cómo logramos el comportamiento correcto con la menor cantidad de sorpresa para aquellos que usan y desarrollan paquetes.
En esta propuesta, el comportamiento de transposición recursiva para tipos personalizados es opcional: usted opta por hacer que su tipo sea un AbstractArray o definiendo el método apropiado
Base.transpose(AbstractArray{MyType}) o Base.transpose(AbstractArray{T}) where T<: MyAbstractType .
Creo que la estrategia de tipeo de pato para transposiciones recursivas (simplemente recurrir sin preguntar) produce algunas sorpresas como se documenta anteriormente. Si introduce ctranspose y adjoint distintos, o propuestas más complicadas como conjadjoint y flip, los usuarios los encontrarán e intentarán usarlos, y los mantenedores de paquetes intentarán admitirlos a todos.

Como ejemplo de algo que sería difícil de soportar bajo las nuevas propuestas: los arreglos normal, transpose, ctranspose y conj deberían poder tener vistas (o evaluación diferida) que interoperen con las vistas ReshapedArray y SubArray. (Soy agnóstico sobre si estos producen vistas de forma predeterminada o solo cuando se usa @view .) Esto se conecta al trabajo en la reducción de A*_mul_B* y en las llamadas BLAS de nivel inferior con banderas 'N', 'T' y 'C' para matrices densas, como se ha señalado en otra parte. Sería más fácil razonar si trataran a normal , transpose , ctranspose y conj
en pie de igualdad. Tenga en cuenta que BLAS en sí solo admite 'N' para normal, 'T' para transposición y 'C' para ctranspose y no tiene una bandera para conj, lo que creo que es un error.

Finalmente, por coherencia con arreglos y remodelaciones de dimensiones superiores, creo que la generalización adecuada de transposición y ctransposición es invertir todas las dimensiones, es decir
transponer (A :: Array {T, 3}) = permutedims (A, (3, 2, 1)).

¡Salud!

Aprecio mucho a la gente que realmente hace el trabajo. Lo que se ha discutido con demasiada longitud son los adjuntos / transposición de vectores (pero nunca el aspecto recursivo), hasta que @andyferris intensificó e implementó esto, y funciona maravillosamente bien. Del mismo modo, también aprecio mucho el rediseño continuo de los constructores de matrices. Aprobado por todo eso.

Dicho esto, la transposición de matriz y adjunta / ctransposición nunca tuvieron mucha discusión, especialmente no el aspecto recursivo de la misma, que se introdujo casi en silencio en https://github.com/JuliaLang/julia/pull/7244 con matrices de bloques de motivación única . Se han dado varias razones y motivaciones para el adjunto recursivo (después de los hechos), y la mayoría de las personas pueden estar de acuerdo en que es una buena opción (pero no la única). Sin embargo, Transpose carece de una sola motivación o caso de uso real.

Hay algunas cosas distintas que están sucediendo en estas discusiones, y sucede ahora que necesitamos un plan que se pueda implementar rápidamente.

  • Hemos discutido si vale la pena admitir matrices de bloques (y estructuras más exóticas) en LinAlg . Las opciones de implementación son: nada recursivo (excepto + , * y conj porque esa es la naturaleza de las funciones genéricas en Julia), todo recursivo (el status quo), o intentar algún tipo de verificación de tipo o rasgo para determinar si un elemento debe hacer álgebra lineal recursiva o ser tratado como escalar.
  • Queremos una forma agradable para que los usuarios permuten las dimensiones de una matriz de datos 2D. Tenemos una sintaxis transpose , flip no recursiva, abreviada permutedims (ese PR se envió primero simplemente porque es la menor cantidad de caracteres para implementar, y probablemente tenga sentido incluso si también hacemos otra cosa), algún tipo de verificación de tipo o rasgo para determinar si un elemento debería realizar una transposición recursiva (quizás incluso reintroduciendo transpose(x::Any) = x ...)
  • El analizador de Julia tiene un comportamiento extraño como x' * y -> Ac_mul_B(x, y) que es un poco una verruga, que idealmente no existirá en v1.0. Esto no se ha visto como factible hasta que podamos admitir BLAS rápido (sin copias adicionales) sin él, por lo tanto, transposición de matriz perezosa y adjunta.
  • El código en LinAlg es bastante grande y se desarrolló durante varios años. Muchas cosas como la multiplicación de matrices podrían refactorizarse para que sean más amigables con los rasgos, tal vez con un sistema de envío más parecido al nuevo broadcast . Creo que aquí es donde podemos facilitar el envío de las matrices correctas (estoy pensando en PermuteDimsArray de vistas conjugadas remodeladas y modificadas de matrices estriadas) a BLAS. Sin embargo, esto no hará que v1.0 y también estamos tratando de evitar regresiones de rendimiento sin empeorar el código. Como señaló Sacha (y estoy descubriendo) tener vistas de transposición con una amplia gama de comportamientos en los elementos (adjunto recursivo, transposición recursiva, conjugación, nada) genera una complejidad adicional y un montón de nuevos métodos para mantener las cosas funcionando como están son.

Si pensamos en la v1.0 como algo que estabiliza el lenguaje, entonces, en algunos sentidos, la mayor prioridad para hacer un cambio de comportamiento es la tercera. Yo diría: el idioma (incluido el analizador) debería ser más estable, seguido de Base , seguido de stdlib (que puede incluir o no LinAlg , pero creo que casi definitivamente incluirá BLAS , Sparse , etc un día). Es un cambio que realmente no afecta a los usuarios (principalmente a los desarrolladores de bibliotecas), por lo que no me sorprendería que las opiniones de la gente difieran aquí.

¡Detecta a Andy! :)

Creo que lo único que queda por hacer aquí es hacer que adjoint y transpose perezosos por defecto.

¿Se puede cerrar esto ahora?

A continuación: "Tomarse en serio las transposiciones escalares"

Pero en serio, ¿podemos tener una buena interfaz para especificar las diferentes transposiciones 3D y multiplicaciones de tensor que se utilizan en los solucionadores de PDE? Algo en serio, pero no estoy seguro de poder manejar ser el OP en la próxima iteración de esta locura.

No

:)

¿Podemos tener una buena interfaz para especificar las diferentes transposiciones 3D y multiplicaciones de tensor que se utilizan en los solucionadores de PDE?

Definitivamente parece un buen tema para un paquete.

una buena interfaz para especificar las diferentes transposiciones 3D y multiplicaciones de tensor

¿ TensorOperations.jl no hace lo que necesita aquí? (Tenga en cuenta que en este nivel "una buena interfaz" significa algo así como un diagrama de red tensorial, que es un poco difícil de escribir en código de manera más sucinta que la sintaxis de TensorOperations ).

Sí, TensorOperations.jl se ve bien. Estaba bromeando un poco, pero obtuve lo que necesitaba 👍.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

omus picture omus  ·  3Comentarios

StefanKarpinski picture StefanKarpinski  ·  3Comentarios

iamed2 picture iamed2  ·  3Comentarios

tkoolen picture tkoolen  ·  3Comentarios

felixrehren picture felixrehren  ·  3Comentarios