En informática , la programación matricial se refiere a soluciones que permiten la aplicación de operaciones a un conjunto completo de valores a la vez. Estas soluciones se utilizan comúnmente 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 ) han sido diseñados específicamente para generalizar operaciones en escalares para aplicar 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 en matrices completas 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 forma concisa ideas amplias sobre la manipulación de datos. El nivel de concisión puede ser dramático en ciertos casos: no es raro [ ejemplo necesario ] encontrar lenguajes de programación de matrices de una sola línea 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 de inmediato a un conjunto completo de valores. Esto la convierte en un modelo de programación de alto nivel , ya que permite al programador pensar y operar sobre agregados completos de datos, sin tener que recurrir a bucles explícitos de operaciones escalares individuales.
Kenneth E. Iverson describió la lógica 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, digamos, un matemático aplicado.
La tesis es que las ventajas de la ejecutabilidad y la universalidad que se encuentran en los lenguajes de programación se pueden combinar eficazmente, en un único lenguaje coherente, con las ventajas que ofrece la notación matemática. Es importante distinguir la dificultad de describir y aprender un fragmento de notación de la dificultad de dominar sus implicaciones. Por ejemplo, aprender las reglas para calcular un producto de matrices 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, la propia sugestión de una notación puede hacer que parezca más difícil de aprender debido a las numerosas propiedades que sugiere para las exploraciones.
[...]
Los usuarios de ordenadores y lenguajes de programación suelen preocuparse principalmente por la eficiencia de ejecución de los algoritmos y, por lo tanto, podrían descartar de plano muchos de los algoritmos presentados aquí. Tal descarte sería una falta de visión, ya que una declaración clara de un algoritmo suele utilizarse como base a partir de la cual se puede derivar fácilmente un algoritmo más eficiente.
La base de la programación y el pensamiento de matrices es encontrar y explotar las propiedades de los datos en los que los elementos individuales son similares o adyacentes. A diferencia de la orientación a objetos, que descompone implícitamente los datos en sus partes constituyentes (o cantidades escalares ), la orientación de matrices busca agrupar los 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 tensorial en matemáticas: las funciones que operan sobre datos pueden clasificarse por el número de dimensiones sobre las que actúan. La multiplicación ordinaria, por ejemplo, es una función de rango escalar porque opera sobre datos de dimensión cero (números individuales). La operación de producto vectorial es un ejemplo de una función de rango vectorial porque opera sobre vectores, no escalares. La multiplicación de matrices es un ejemplo de una función de dos rangos, porque opera sobre 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 los elementos colapsa la matriz de entrada en una dimensión.
La programación de matrices es muy adecuada para la paralelización implícita , un tema de mucha investigación en la actualidad. Además, las CPU de Intel y compatibles desarrolladas y producidas después de 1997 contenían varias extensiones de conjuntos de instrucciones, comenzando por MMX y continuando con SSSE3 y 3DNow!, que incluyen capacidades rudimentarias de matriz SIMD . Esto ha continuado en 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 sean resueltos por partes por numerosos procesadores. Los procesadores con múltiples núcleos y las GPU con miles de núcleos de computación general 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 , Perl Data Language (PDL), R , Raku , S-Lang , SAC , Nial , ZPL , Futhark y TI-BASIC .
En lenguajes escalares como C y Pascal , las operaciones se aplican solo a valores únicos, por lo que a + b expresa la suma de dos números. En dichos lenguajes, sumar 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 matricial de los objetos,
a (:,:) = a (:,:) + b (:,:)
Si bien los lenguajes escalares como C no tienen elementos de programación de matriz 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 las tiene o mediante el uso de 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 sus heurísticas determinan que se beneficiarían de ello. Otro enfoque lo proporciona 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 que se apliquen tanto a escalares como a matrices. Por lo tanto, 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 matriz simplifica la programación, pero posiblemente a un costo conocido como la 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 más eficiente de forma óptima . (Por ejemplo, es posible que se encuentren adiciones de otros elementos de la misma matriz posteriormente durante la misma ejecución, lo que causa búsquedas repetidas innecesarias). Incluso el compilador de optimización más sofisticado tendría dificultades extremadamente para fusionar dos o más funciones aparentemente dispares que podrían aparecer en diferentes secciones del programa o subrutinas, 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 se convertiría en el siguiente en el lenguaje Ada , [6] que admite la sintaxis de programación matricial.
A := A + B ;
APL utiliza símbolos Unicode de un solo carácter sin ningún tipo de complejidad sintáctica.
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 extiende el lenguaje original con asignaciones aumentadas :
A + ← B
Analytica ofrece la misma economía de expresión que Ada.
A := A + B;
Dartmouth BASIC tenía declaraciones MAT para la manipulación de matrices y arreglos en su tercera edición (1966).
DIMENSIONES A ( 4 ), B ( 4 ), C ( 4 ) MAT A = 1 MAT B = 2 * A MAT C = A + B MAT IMPRIMIR A , B , C
Mata, el lenguaje de programación matricial de Stata , 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, la subexpresión y una de las muchas funciones de matriz inversa 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 | +------------+:E = A * C:yo 1 2 +-----------+ 1 | 6 6 | 2 | 15 15 | +-----------+:F=A: * B:Yo 1 2 3 +----------------+ 1 | 2 6 12 | 2 | 4 10 18 | +----------------+:G = E : + 3:G 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 una: // 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 la pseudoinversa 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 de fila de tamaño [1 n] y b
es un vector de 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 seguida de la adición de un escalar (que, de hecho, es un vector de un elemento) y un vector:
> A <- matrix ( 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 ( matrix ( 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
Raku admite el paradigma de matriz a través de sus metaoperadores. [9] El siguiente ejemplo demuestra la adición de matrices @a y @b utilizando el operador Hyper junto con el operador plus.
[ 0 ] > mi @a = [[ 1 , 1 ],[ 2 , 2 ],[ 3 , 3 ]];[[ 1 1 ] [ 2 2 ] [ 3 3 ]][ 1 ] > mi @b = [[ 4 , 4 ],[ 5 , 5 ],[ 6 , 6 ]];[[ 4 4 ] [ 5 5 ] [ 6 6 ]][ 2 ] > @a »+« @b ;[[ 5 5 ] [ 7 7 ] [ 9 9 ]]
El operador de división por la izquierda de matrices expresa de forma 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 la inversa de A
: (tanto en los lenguajes MATLAB como GNU Octave: ). Las siguientes afirmaciones matemáticas se cumplen cuando es 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 de multiplicación de matrices )x = A^-1 * b
donde ==
es el operador relacional de equivalencia . Las sentencias 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, es decir, A
tiene más filas que columnas, la pseudoinversa (en los lenguajes MATLAB y GNU Octave: ) puede reemplazar a la inversa , 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 las más concisas (por ejemplo, sigue siendo necesario diferenciar mediante notación los sistemas sobredeterminados) ni las más eficientes computacionalmente. Este último punto es fácil de entender cuando se considera nuevamente el equivalente escalar a * x = b
, para el cual la solución x = a^-1 * b
requeriría dos operaciones en lugar de la más eficiente x = b / a
. El problema es que, en general, 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 las matrices!)x * (a / a)==b / a
(La asociatividad también es válida para las matrices)x = b / a
El lenguaje MATLAB introduce el operador de división 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 las matrices, la conmutatividad ya no es necesaria)x = A \ b
Este no es solo un ejemplo de programación de matrices concisa 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 . [10]
Volviendo a la cita anterior de Iverson, el fundamento de la misma debería resultar 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 de matrices 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 sugerente de una notación puede hacer que parezca más difícil de aprender debido a las muchas propiedades que sugiere para la exploración.
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á influenciada explícitamente por el paradigma de programación de matrices, como lo hacen la biblioteca de extensión NumPy para Python y las bibliotecas Armadillo y Blitz++ . [11] [12]