En informática , la programación de matrices se refiere a soluciones que permiten aplicar operaciones a un conjunto completo de valores a la vez. Estas soluciones se utilizan habitualmente en entornos científicos y de ingeniería.
Los lenguajes de programación modernos que admiten la programación de matrices (también conocidos como lenguajes vectoriales o multidimensionales ) se han diseñado específicamente para generalizar operaciones en escalares para aplicarlas de forma transparente a vectores , matrices y matrices de dimensiones superiores. Estos incluyen APL , J , Fortran , MATLAB , Analytica , Octave , R , Cilk Plus , Julia , Perl Data Language (PDL) . En estos lenguajes, una operación que opera sobre arreglos completos puede denominarse operación vectorizada , [1] independientemente de si se ejecuta en un procesador vectorial , que implementa instrucciones vectoriales. Las primitivas de programación de matrices expresan de manera concisa ideas amplias sobre la manipulación de datos. El nivel de concisión puede ser dramático en ciertos casos: no es raro [ se necesita ejemplo ] encontrar frases ingeniosas en lenguaje de programación de matrices que requieren varias páginas de código orientado a objetos.
La idea fundamental detrás de la programación de matrices es que las operaciones se aplican a la vez a un conjunto completo de valores. Esto lo convierte en un modelo de programación de alto nivel , ya que permite al programador pensar y operar con agregados completos de datos, sin tener que recurrir a bucles explícitos de operaciones escalares individuales.
Kenneth E. Iverson describió el fundamento detrás de la programación de matrices (en realidad refiriéndose a APL) de la siguiente manera: [2]
la mayoría de los lenguajes de programación son decididamente inferiores a la notación matemática y se utilizan poco como herramientas de pensamiento en formas que serían consideradas significativas, por ejemplo, por un matemático aplicado.
La tesis es que las ventajas de ejecutabilidad y universalidad que se encuentran en los lenguajes de programación se pueden combinar efectivamente, en un único lenguaje coherente, con las ventajas que ofrece la notación matemática. Es importante distinguir la dificultad de describir y aprender una notación de la dificultad de dominar sus implicaciones. Por ejemplo, aprender las reglas para calcular un producto matricial es fácil, pero dominar sus implicaciones (como su asociatividad, su distributividad sobre la suma y su capacidad para representar funciones lineales y operaciones geométricas) es una cuestión diferente y mucho más difícil. .
De hecho, el carácter mismo sugestivo de una notación puede hacer que parezca más difícil de aprender debido a las muchas propiedades que sugiere para las exploraciones.
[...]
Los usuarios de computadoras y lenguajes de programación a menudo se preocupan principalmente por la eficiencia de la ejecución de los algoritmos y, por lo tanto, podrían descartar sumariamente muchos de los algoritmos presentados aquí. Tal desestimación sería miope, ya que una declaración clara de un algoritmo generalmente puede usarse como base a partir de la cual uno puede derivar fácilmente un algoritmo más eficiente.
La base detrás del pensamiento y la programación de matrices es encontrar y explotar las propiedades de los datos donde los elementos individuales son similares o adyacentes. A diferencia de la orientación a objetos, que implícitamente descompone los datos en sus partes constituyentes (o cantidades escalares ), la orientación de matrices busca agrupar datos y aplicar un manejo uniforme.
El rango de función es un concepto importante para los lenguajes de programación de matrices en general, por analogía con el rango de tensor en matemáticas: las funciones que operan con datos pueden clasificarse según el número de dimensiones en las que actúan. La multiplicación ordinaria, por ejemplo, es una función escalar porque opera con datos de dimensión cero (números individuales). La operación de producto cruzado es un ejemplo de función de rango vectorial porque opera con vectores, no con escalares. La multiplicación de matrices es un ejemplo de una función de 2 rangos, porque opera en objetos bidimensionales (matrices). Los operadores de colapso reducen la dimensionalidad de una matriz de datos de entrada en una o más dimensiones. Por ejemplo, la suma de elementos colapsa la matriz de entrada en 1 dimensión.
La programación de matrices se adapta muy bien a la paralelización implícita ; un tema de mucha investigación hoy en día. Además, Intel y las CPU compatibles desarrolladas y producidas después de 1997 contenían varias extensiones de conjuntos de instrucciones, comenzando con MMX y continuando hasta SSSE3 y 3DNow. , que incluyen capacidades rudimentarias de matriz SIMD . Esto ha continuado hasta la década de 2020 con conjuntos de instrucciones como AVX-512 , lo que convierte a las CPU modernas en procesadores vectoriales sofisticados. El procesamiento de matrices se diferencia del procesamiento paralelo en que un procesador físico realiza operaciones en un grupo de elementos simultáneamente, mientras que el procesamiento paralelo tiene como objetivo dividir un problema más grande en otros más pequeños ( MIMD ) para que numerosos procesadores los resuelvan poco a poco. Los procesadores con múltiples núcleos y GPU con miles de núcleos informáticos generales son comunes a partir de 2023.
Los ejemplos canónicos de lenguajes de programación de matrices son Fortran , APL y J. Otros incluyen: A+ , Analytica , Chapel , IDL , Julia , K , Klong, Q , MATLAB , GNU Octave , Scilab , FreeMat , PDL , R , S-Lang , SAC , Nial , ZPL , Futhark y TI-BASIC .
En lenguajes escalares como C y Pascal , las operaciones se aplican sólo a valores únicos, por lo que a + b expresa la suma de dos números. En tales lenguajes, agregar una matriz a otra requiere indexación y bucles, cuya codificación es tediosa.
para ( i = 0 ; i < n ; i ++ ) para ( j = 0 ; j < n ; j ++ ) a [ i ][ j ] += b [ i ][ j ];
En lenguajes basados en matrices, por ejemplo en Fortran, el bucle for anidado anterior se puede escribir en formato de matriz en una línea,
a = a + b
o alternativamente, para enfatizar la naturaleza de matriz de los objetos,
a (:,:) = a (:,:) + b (:,:)
Si bien los lenguajes escalares como C no tienen elementos de programación de matrices nativos como parte del lenguaje propiamente dicho, esto no significa que los programas escritos en estos lenguajes nunca aprovechen las técnicas subyacentes de vectorización (es decir, utilizar las instrucciones basadas en vectores de una CPU si tiene ellos o usando múltiples núcleos de CPU). Algunos compiladores de C como GCC en algunos niveles de optimización detectan y vectorizan secciones de código que su heurística determina que se beneficiarían de ello. Otro enfoque lo ofrece la API OpenMP , que permite paralelizar secciones de código aplicables aprovechando múltiples núcleos de CPU.
En los lenguajes de matrices, las operaciones se generalizan para aplicarse tanto a escalares como a matrices. Así, a + b expresa la suma de dos escalares si a y b son escalares, o la suma de dos matrices si son matrices.
Un lenguaje de matrices simplifica la programación, pero posiblemente a un costo conocido como penalización de abstracción . [3] [4] [5] Debido a que las adiciones se realizan de forma aislada del resto de la codificación, es posible que no produzcan el código óptimamente más eficiente . (Por ejemplo, es posible que posteriormente se encuentren adiciones de otros elementos de la misma matriz durante la misma ejecución, lo que provocaría búsquedas repetidas innecesarias). Incluso el compilador de optimización más sofisticado tendría dificultades extremas para amalgamar dos o más funciones aparentemente dispares que podrían aparecer en diferentes secciones o subrutinas del programa, aunque un programador podría hacer esto fácilmente, agregando sumas en la misma pasada sobre la matriz para minimizar la sobrecarga ).
El código C anterior pasaría a ser el siguiente en el lenguaje Ada , [6] que admite la sintaxis de programación de matrices.
A := A + B ;
APL utiliza símbolos Unicode de un solo carácter sin azúcar sintáctico.
A ← A + B
Esta operación funciona en matrices de cualquier rango (incluido el rango 0) y en un escalar y una matriz. Dyalog APL amplía el idioma original con asignaciones aumentadas :
A + ← B
Analytica proporciona la misma economía de expresión que Ada.
A := A + B;
Dartmouth BASIC tenía declaraciones MAT para manipulación de matrices y matrices en su tercera edición (1966).
DIM A ( 4 ), B ( 4 ), C ( 4 ) MAT A = 1 MAT B = 2 * A MAT C = A + B MAT IMPRIMIR A , B , C
El lenguaje de programación matricial de Stata , Mata, admite la programación de matrices. A continuación, ilustramos la suma, la multiplicación, la suma de una matriz y un escalar, la multiplicación elemento por elemento, el subíndice y una de las muchas funciones matriciales inversas de Mata.
. mata :: A = ( 1 , 2 , 3 ) \( 4 , 5 , 6 ): A 1 2 3 +-------------+ 1 | 1 2 3 | 2 | 4 5 6 | +------------+: B = ( 2 .. 4 ) \( 1 .. 3 ): B 1 2 3 +-------------+ 1 | 2 3 4 | 2 | 1 2 3 | +------------+: C = J ( 3 , 2 , 1 ) // Una matriz de unos de 3 por 2: C 1 2 +---------+ 1 | 1 1 | 2 | 1 1 | 3 | 1 1 | +---------+: D = A + B: D 1 2 3 +-------------+ 1 | 3 5 7 | 2 | 5 7 9 | +------------+: mi = A * C: mi 1 2 +-----------+ 1 | 6 6 | 2 | 15 15 | +-----------+: F = A: * B:F 1 2 3 +----------------+ 1 | 2 6 12 | 2 | 4 10 18 | +----------------+: GRAMO = MI : + 3: GRAMO 1 2 +-----------+ 1 | 9 9 | 2 | 18 18 | +-----------+: H = F[( 2 \ 1 ), ( 1 , 2 )] // Subíndice para obtener una submatriz de F y: // cambia las filas 1 y 2:H 1 2 +-----------+ 1 | 4 10 | 2 | 2 6 | +-----------+: I = invsym (F' * F) // Inversa generalizada (F*F^(-1)F=F) de a: // matriz semidefinida positiva simétrica: I[simétrico] 1 2 3 +-------------------------------------------+ 1 | 0 | 2 | 0 3,25 | 3 | 0-1,75 . _ _ 9444444444 | +-------------------------------------+: fin
La implementación en MATLAB permite la misma economía que permite el uso del lenguaje Fortran.
A = A + B ;
Una variante del lenguaje MATLAB es el lenguaje GNU Octave , que extiende el lenguaje original con asignaciones aumentadas:
A += B ;
Tanto MATLAB como GNU Octave admiten de forma nativa operaciones de álgebra lineal como la multiplicación de matrices, la inversión de matrices y la solución numérica de sistemas de ecuaciones lineales , incluso utilizando el pseudoinverso de Moore-Penrose . [7] [8]
El ejemplo de Nial del producto interno de dos matrices se puede implementar utilizando el operador de multiplicación de matrices nativo. Si a
es un vector fila de tamaño [1 n] y b
es un vector columna correspondiente de tamaño [n 1].
a*b;
Por el contrario, el producto de entrada se implementa como:
a .*b;
El producto interno entre dos matrices que tienen el mismo número de elementos se puede implementar con el operador auxiliar (:)
, que transforma una matriz dada en un vector columna, y el operador de transposición'
:
A(:)' * B(:);
El lenguaje de consulta rasdaman es un lenguaje de programación de matrices orientado a bases de datos. Por ejemplo, se podrían agregar dos matrices con la siguiente consulta:
SELECCIONE A + B DE A , B
El lenguaje R admite el paradigma de matriz de forma predeterminada. El siguiente ejemplo ilustra un proceso de multiplicación de dos matrices seguido de la suma de un escalar (que es, de hecho, un vector de un elemento) y un vector:
> A <- matriz ( 1 : 6 , nrow = 2 ) # !! esto tiene nrow=2 ... y A tiene 2 filas > A [,1] [,2] [,3] [1,] 1 3 5 [2,] 2 4 6 > B <- t ( matriz ( 6 : 1 , nrow = 2 ) ) # t() es un operador de transposición !! Esto tiene nrow=2 ... y B tiene 3 filas - - una clara contradicción con la definición de A > B [,1] [,2] [1,] 6 5 [2,] 4 3 [3,] 2 1 > C <- A %*% B > C [, 1] [,2] [1,] 28 19 [2,] 40 28 > D <- C + 1 > D [,1] [,2] [1,] 29 20 [2,] 41 29 > D + c ( 1 , 1 ) # c() crea un vector [,1] [,2] [1,] 30 21 [2,] 42 30
El operador de división por la izquierda de la matriz expresa de manera concisa algunas propiedades semánticas de las matrices. Como en el equivalente escalar, si el ( determinante del) coeficiente (matriz) A
no es nulo, entonces es posible resolver la ecuación (vectorial) A * x = b
multiplicando por la izquierda ambos lados por el inverso de A
: (tanto en lenguajes MATLAB como GNU Octave : ). Las siguientes afirmaciones matemáticas son válidas cuando se trata de una matriz cuadrada de rango completo :A−1
A^-1
A
A^-1 *(A * x)==A^-1 * (b)
(A^-1 * A)* x ==A^-1 * b
( asociatividad matriz-multiplicación )x = A^-1 * b
¿ Dónde ==
está el operador relacional de equivalencia ? Las declaraciones anteriores también son expresiones válidas de MATLAB si la tercera se ejecuta antes que las demás (las comparaciones numéricas pueden ser falsas debido a errores de redondeo).
Si el sistema está sobredeterminado (de modo que A
tiene más filas que columnas), el pseudoinverso (en lenguajes MATLAB y GNU Octave:) puede reemplazar al inverso , de la siguiente manera:A+
pinv(A)
A−1
pinv(A) *(A * x)==pinv(A) * (b)
(pinv(A) * A)* x ==pinv(A) * b
(asociatividad de multiplicación de matrices)x = pinv(A) * b
Sin embargo, estas soluciones no son ni las más concisas (por ejemplo, todavía sigue siendo necesario diferenciar notacionalmente los sistemas sobredeterminados) ni las más eficientes desde el punto de vista computacional. Este último punto es fácil de entender si se considera nuevamente el equivalente escalar a * x = b
, cuya solución x = a^-1 * b
requeriría dos operaciones en lugar de la más eficiente x = b / a
. El problema es que generalmente las multiplicaciones de matrices no son conmutativas ya que la extensión de la solución escalar al caso matricial requeriría:
(a * x)/ a ==b / a
(x * a)/ a ==b / a
(¡La conmutatividad no es válida para matrices!)x * (a / a)==b / a
(la asociatividad también es válida para matrices)x = b / a
El lenguaje MATLAB introduce el operador de división por la izquierda \
para mantener la parte esencial de la analogía con el caso escalar, simplificando así el razonamiento matemático y preservando la concisión:
A \ (A * x)==A \ b
(A \ A)* x ==A \ b
(la asociatividad también es válida para matrices, la conmutatividad ya no es necesaria)x = A \ b
Este no es solo un ejemplo de programación de matrices concisas desde el punto de vista de la codificación, sino también desde la perspectiva de la eficiencia computacional, que en varios lenguajes de programación de matrices se beneficia de bibliotecas de álgebra lineal bastante eficientes como ATLAS o LAPACK . [9]
Volviendo a la cita anterior de Iverson, la razón detrás de ella ahora debería ser evidente:
Es importante distinguir la dificultad de describir y aprender una notación de la dificultad de dominar sus implicaciones. Por ejemplo, aprender las reglas para calcular un producto matricial es fácil, pero dominar sus implicaciones (como su asociatividad, su distributividad sobre la suma y su capacidad para representar funciones lineales y operaciones geométricas) es una cuestión diferente y mucho más difícil. . De hecho, el carácter mismo sugestivo de una notación puede hacer que parezca más difícil de aprender debido a las muchas propiedades que sugiere para las exploraciones.
El uso de bibliotecas especializadas y eficientes para proporcionar abstracciones más concisas también es común en otros lenguajes de programación. En C++, varias bibliotecas de álgebra lineal explotan la capacidad del lenguaje para sobrecargar operadores . En algunos casos, una abstracción muy concisa en esos lenguajes está explícitamente influenciada por el paradigma de programación de matrices, como lo hace la biblioteca de extensión NumPy para las bibliotecas Python , Armadillo y Blitz++ . [10] [11]