2A. Reporte escrito. Experimentos y análisis de estructuras de datos.
Author
José Alberto Villegas Díaz Disciplina.
Lista de cambios:
Sin cambios. Esta entrega se calificó con la nota más alta.
Introducción
En este reporte, se lleva a cabo un análisis exhaustivo de dos algoritmos de álgebra lineal aplicados sobre matrices: la multiplicación de matrices y la eliminación gaussiana.El objetivo principal de este reporte es implementar y analizar el desempeño de ambos algoritmos al trabajar con matrices aleatorias de tamaño \(nxn\) para valores de \(n=100,300, 1000\). Para evaluar de manera rigurosa su eficiencia, se compararán los resultados en términos del número de operaciones requeridas y el tiempo real de ejecución para cada algoritmo. La elección de estos parámetros permitirá observar cómo cada uno de estos algoritmos escala con el tamaño de las matrices, proporcionando así una visión clara de sus ventajas y limitaciones bajo diferentes condiciones.
Algoritmo para multiplicación de matrices aleatorias de tamaño \(n×n\)
En primera instancia crearemos dos funciones, una para la generación de matrices aleatorias de tamaño \(nxn\) y otra implementando un algoritmo convencional para la multiplicación de las mismas.
La definición dada por Grossman & Flores (2012), de producto de matrices establece:
Sea \(A = (a_{ij})\) una matriz de dimensión \(m \times n\), y sea \(B = (b_{ij})\) una matriz de dimensión \(n \times p\). Entonces el producto de \(A\) y \(B\) es una matriz \(C = (c_{ij})\) de dimensión \(m \times p\), en donde
\[
c_{ij} = (\text{renglón } i \text{ de } A) \cdot (\text{columna } j \text{ de } B)
\]
Es decir, el elemento \(c_{ij}\) de \(AB\) es el producto punto del renglón \(i\) de \(A\) y la columna \(j\) de \(B\). Si esto se extiende, se obtiene
Si el número de columnas de \(A\) es igual al número de renglones de \(B\), entonces se dice que \(A\) y \(B\) son compatibles bajo la multiplicación.
Bajo ese esquema teórico se realizara el algoritmo.
Definición y prueba del algoritmo
# Función para matrices aleatorias de tamaño n x nfunctiongenerar_matriz_aleatoria(n)returnrand(n, n) # Matriz con números aleatorios entre 0 y 1end# Función en la que se implementa un algoritmo convencional para el producto de matricesfunctionmultiplicar_matrices(A, B) n =size(A, 1) #A y B son de tamaño n x n C =zeros(n, n) # Matriz resultado en la que se asignarán los valores de cij operaciones =0# Contador de operaciones# Algoritmo de multiplicación de matrices convencionalfor i in1:nfor j in1:n suma =0# Variable para la suma en la posición (i, j)for k in1:n suma += A[i, k] * B[k, j] # Multiplicación y suma operaciones +=1# Contamos cada multiplicaciónend C[i, j] = suma # Asignamos el valor a la matriz resultadoendendreturn C, operaciones # Retorna la matriz resultado y el número de operacionesend
multiplicar_matrices (generic function with 1 method)
Una vez definidas las funciones podemos realizar una validación simple para matrices pequeñas de orden 3x3.
# Validación de uso con matrices 3x3n =3# Tamaño de la matrizA =generar_matriz_aleatoria(n)B =generar_matriz_aleatoria(n)# Multiplicación de matricesC, operaciones =multiplicar_matrices(A, B)# Mostrar el resultadoprintln("Matriz A:")println(A)println("Matriz B:")println(B)println("Matriz C (Resultado):")println(C)# Mostrar el número de operaciones realizadasprintln("Número de operaciones realizadas: $operaciones")
Los algoritmos mostraron un correcto funcionamiento por lo que se procederá a evaluar para los distintos valores de \(n\). Nótese que para esta validación se imprimieron las matrices, esto no es viable en los siguientes casos debido al tamaño de las entradas.
Evaluación para \(n=100\)
# Validación de uso con matrices 100x100n =100# Tamaño de la matrizA =generar_matriz_aleatoria(n)B =generar_matriz_aleatoria(n)# Multiplicación de matricesC, operaciones =multiplicar_matrices(A, B)# Mostrar el número de operaciones realizadasprintln("Número de operaciones realizadas: $operaciones")
Número de operaciones realizadas: 1000000
Se puede observar que el número de operaciones realizadas cuando \(n=100\) es de 1,000,000 es decir \(100^3\)
usingBenchmarkTools@btimemultiplicar_matrices(A, B)
# Validación de uso con matrices 100x100n =300# Tamaño de la matrizA =generar_matriz_aleatoria(n)B =generar_matriz_aleatoria(n)# Multiplicación de matricesC, operaciones =multiplicar_matrices(A, B)# Mostrar el número de operaciones realizadasprintln("Número de operaciones realizadas: $operaciones")
Número de operaciones realizadas: 27000000
Se puede observar que el número de operaciones realizadas cuando \(n=300\) es de 27,000,000 es decir \(300^3\)
usingBenchmarkTools@btimemultiplicar_matrices(A, B)
Podemos observar que el tiempo en el que incrementó fue apróximadamente de un orden más respecto de \(n=100\) y el número de asignaciones permanece constante
Evaluación para \(n=1000\)
# Validación de uso con matrices 1000x1000n =1000# Tamaño de la matrizA =generar_matriz_aleatoria(n)B =generar_matriz_aleatoria(n)# Multiplicación de matricesC, operaciones =multiplicar_matrices(A, B)# Mostrar el número de operaciones realizadasprintln("Número de operaciones realizadas: $operaciones")
Número de operaciones realizadas: 1000000000
Se puede observar que el número de operaciones realizadas cuando \(n=1000\) es de 1,000,000,000 es decir \(1000^3\)
usingBenchmarkTools@btimemultiplicar_matrices(A, B)
Podemos observar que el tiempo en el que incrementó fue apróximadamente de un orden más respecto de \(n=300\) y el número de asignaciones permanece constante
Experimento para matrices dispersas
usingSparseArraysA =sprand(1000, 1000, 0.01) # Matriz 1000x1000 con 1% de elementos no cerosB =sprand(1000, 1000, 0.01)# Multiplicación de matricesC, operaciones =multiplicar_matrices(A, B)# Mostrar el número de operaciones realizadasprintln("Número de operaciones realizadas: $operaciones")
Número de operaciones realizadas: 1000000000
usingBenchmarkTools@btimemultiplicar_matrices(A, B)
Se observa que para \(n=1000\) el tiempo de ejecución incrementa en un orden. Las asignaciones son constantes
Algortimo para Eliminación gaussiana / Gauss-Jordan para matrices aleatorias de tamaño \(n×n\)
Definición y prueba del algoritmo
En algebra lineal una de las aplicaciones del método de Gauss Jordan es su uso para encontrar la inversa de una matriz invertible \(nxn\), para lo cual el primer paso es agregar una matriz identidad a la derecha de la matriz inverible en forma de matriz aumentada y resolver el sistema por el método de Gauss Jordan. De acuerdo a Grossman & Flores (2012), el método para realizar Gauss-Jordan con pivoteo consiste en:
Escribir el sistema en la forma de matriz aumentada. De la primer columna con componentes diferentes de cero (denominada columna pivote), seleccione la componente con el valor absoluto. Esta componente se denomina pivote:
Reacomodar los renglones para mover el pivote hasta arriba:
Dividir el primer renglón entre el pivote:
Sumar múltiplos del primer renglón a los otros renglones para hacer cero todas las componentes de la columna pivote:
Tapar el primer renglón y realice los pasos 1 al 4 en la submatriz que resulta:
Continuar de esta manera hasta que la matriz esté en la forma escalonada por renglones.
Resolver el sistema
Bajo este esquema se implementará el algoritmo.
Definición y prueba del algoritmo
usingLinearAlgebra# Crear una matriz aleatoria invertible de tamaño n x nfunctiongenerar_matriz_invertible(n)whiletrue A =rand(n, n) # Generamos una matriz aleatoriaifdet(A) !=0# Verificamos que sea invertiblereturn Aendendendfunctiongauss_jordan(A) n =size(A, 1) # Tamaño de la matriz (nxn) operaciones =0# Contador de operaciones# Convertimos A en una matriz aumentada con la identidad (para calcular la inversa) A =hcat(A, Matrix(I, n, n)) # Agregamos la matriz identidad a la derecha para expresarla en forma de matriz aumentada# Eliminación hacia adelantefor i in1:n# Pivoteo parcial (si es necesario) max =argmax(abs.(A[i:n, i])) + (i -1) # Encuentra el máximo en la columnaif max != i A[i, :], A[max, :] = A[max, :], A[i, :] # Intercambia filas operaciones += n # Contamos el intercambio de filasend# Normalizar el pivote a 1 pivote = A[i, i] A[i, :] /= pivote operaciones += n # División de toda la fila# Eliminación de los elementos debajo y arriba del pivotefor j in1:nif j != i # Evitar la fila del pivote factor = A[j, i] A[j, :] -= factor * A[i, :] operaciones += n # Contamos la operación de eliminaciónendendend# La parte derecha de A ahora es la matriz inversareturn A[:, n+1:end], operacionesend
gauss_jordan (generic function with 1 method)
Una vez definidas las funciones podemos realizar una validación simple para una matriz pequeña de orden 3x3.
# Prueba con una matriz 3x3n =3A =generar_matriz_invertible(n)# Aplicamos Gauss-Jordan para encontrar la inversaA_inv, operaciones =gauss_jordan(A)# Mostrar resultadosprintln("Matriz A:")println(A)println("Matriz Inversa A^-1:")println(A_inv)println("Número de operaciones realizadas: $operaciones")
Se corrobora que el algoritmo funciona correctamente. También desde este momento podemos observar que el número de operaciones realizadas para \(n=3\) es de 27, es decir \(3^3\)
Evaluación para \(n=100\)
# Experimento para n=100n =100A =generar_matriz_invertible(n)A_inv, operaciones =gauss_jordan(A)println("Número de operaciones realizadas: $operaciones")
Número de operaciones realizadas: 1009600
Se puede observar que el número de operaciones realizadas cuando \(n=100\) es de apróximadamente 1,000,000 es decir \(100^3\)
El tiempo de ejecución aumenta aproximadamente en dos órdenes respecto a \(n=300\) las asignaciones incrementan en un orden
Experimento para matrices dispersas
Bajor la misma lógica que se evaluó este algoritmo se generará una matriz dispersa tamaño \(nxn\) que sea invertible.
usingSparseArrays, LinearAlgebrafunctiongenerar_matriz_invertible_dispersada(n::Int, densidad::Float64)# Primero generamos una matriz dispersa aleatoria de tamaño n x n A =sprandn(n, n, 0.01)# Hacemos la matriz simétrica A = A + A^T para asegurar que sea simétrica A = A +transpose(A)# Añadimos una constante al diagonal para asegurar que sea positiva definida# Esto ayuda a evitar que tenga autovalores no positivosfor i in1:n A[i,i] += n # Incrementamos la diagonal para garantizar la positividadend# A es ahora simétrica y positiva definida, y como es dispersa, debería ser invertiblereturn Aend
generar_matriz_invertible_dispersada (generic function with 1 method)
# Experimento para n=1000n =1000A =generar_matriz_invertible(n)A_inv, operaciones =gauss_jordan(A)println("Número de operaciones realizadas: $operaciones")
Número de operaciones realizadas: 1000987000
Observarmos que el número de operaciones continua siendo aproximadamente \(n^3\)
No se notó una diferencia significativa en tiempo de ejecución ni en asignaciones
Discusión y conclusiones
Del experimento podemos concluir que el orden de crecimiento para el algoritmo del método tradicional de multiplicación de matrices como para el algoritmo de eliminación gaussiana es de \(O(n^3)\). Este resultado es previsible ya que está bien documentado. Sin embargo, se debe apuntar que para los algoritmos de multiplicación de matrices el mínimo exponente para multiplicar matrices \(nxn\) oscila entre 2 y 3 (Anderson & Barman, 2009) y al día de hoy existen múltiples algoritmos, de un orden menor al tradicional, siendo uno de los más conocidos el algoritmo de Strassen (1969) de orden \(O(n^{2.81})\), el cual se basa en el método de “Divide y vencerás” (Cormen et al. 2022). Sin embargo estos algoritmos van más allá del alcance de este reporte.
Si bien ambos algoritmos poseen un orden de crecimiento igual, el algoritmo de eliminación gaussiana, plantea una problemática adicional que es el número de asignaciones, estas son muy altas lo que se interpreta como que Julia está creando múltiples copias de matrices, mientras que en el algoritmo de multiplicación de matrices se aprecia un número de asignaciones constante. Esta discrepacia en los algoritmos afecta directamente los tiempos de ejecución los costos en memoria para el algoritmo de eliminación gaussiana.
Finalmente el experimento haciendo uso de matrices dispersas demostró que el tiempo de ejecución fue mayor para una misma entrada \(n=1000\), para el algoritmo de multiplicación de matrices, mientras que para el algoritmo de eliminación gaussiana no demostró diferencias significativas.
¿Cuál es el impacto de acceder los elementos contiguos en memoria de una matriz?
El acceso a los elementos contiguos en memoria de una matriz podría tener un impacto significativo en los tiempos de ejecución de los algoritmos.Cuando accedes a elementos contiguos en memoria (por ejemplo, en un orden fila-por-fila o columna-por-columna), se puede aprovechar la memoria caché, para acceder a estos y que no tengan que ser cargados desde la memoria principal, que es más lenta.
¿Qué cambiarías si utilizas matrices dispersas? ¿Cuáles serían los costos?
Como se pudo corroborar en los experimentos el uso de matrices dispersas se comportó distinto para cada algoritmo, para el de multiplicación incrementó los tiempos de ejecución, mientras que para el de eliminación gaussiana no tuvo diferencias considerables para \(n=1000\)
Referencias
Anderson, M., & Barman, S. (2009, diciembre 6). The Coppersmith-Winograd Matrix Multiplication Algorithm.
Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford (2022). Introduction to Algorithms (2nd ed.). MIT Press.
Grossman, S. I., & Flores, J. J. (2012). Álgebra lineal (7a ed.). McGraw-Hill.