Reporte escrito. Experimentos y análisis de estructuras de datos
Alumno: Isaac Hernández Ramírez
Introducción:
La práctica de algoritmos para el manejo de matrices que haremos es fundamental en diversas áreas de la computación numérica, se compararon tres algoritmos clave: la multiplicación de matrices, la eliminación Gaussiana y el método de Gauss-Jordan, se evaluaron el número de operaciones y el tiempo de ejecución en matrices de diferentes tamaños (𝑛=100,300,1000), con el objetivo de analizar la eficiencia computacional de cada método. En términos de implementación eficiente, el manejo adecuado de las estructuras de datos y la organización de los accesos a memoria juegan un papel clave en el rendimiento computacional (Cormen et al., 2022, Cap. 10).
La multiplicación de matrices es una operación de complejidad 𝑂(𝑛3), aunque existen variantes más eficientes como el algoritmo de Strassen, que reduce la complejidad a aproximadamente 𝑂(𝑛2.81)(Cormen et al., 2022, Cap. 28). Sin embargo, para resolver sistemas de ecuaciones, la eliminación Gaussiana y el método de Gauss-Jordan son estrategias más eficientes en términos de costo computacional. En este sentido, la elección del algoritmo adecuado depende del tipo de problema que se quiera resolver y de la estructura de los datos.
Además, en la optimización de algoritmos matriciales, la organización de los accesos a memoria es determinante. Esto es particularmente relevante en arquitecturas modernas de cómputo, donde el acceso contiguo a memoria puede minimizar la latencia y maximizar la velocidad de procesamiento (Cormen et al., 2022, Cap. 22).
Importamos librerías necesarias para hacer el ejercicio
Importamos numpy (para las operaciones con matrices)
Importamos time (para medir el tiempo de ejecución)
Importamos tqdm (Esto es opcional, es solo para ver el avance de la ejecución)
Definimos la función para el caso de multiplicación de Matrices
Asumiremos que las matrices seran cuadradas
Definimos la función para el caso de eliminación de gaussiana
Asumimos que es una matríz cuadrada
Definimos la función para el caso de eliminación de gauss-jordan
Asumimos que es una matríz cuadrada
Por fines prácticos almacenamos lo resultados de diccionarios
Mostramos gráficas y resultados
Implementa los siguientes algoritmos sobre matrices.
Multiplicación de matrices
Eliminación gaussiana / Gauss-Jordan
Compara los desempeños de ambos algoritmos contando el número de operaciones y el tiempo real para matrices aleatorias de tamaño n×n para n=100,300,1000.
# Libreriasimport numpy as npimport timefrom tqdm import tqdm# definimos la función para la multiplicación de matricesdef matrix_multiplication(A, B):"""Multiplica dos matrices A y B y cuenta el número de operaciones. Args: A: La primera matriz. B: La segunda matriz. Return: Matriz resultante, número de multiplicaciones, número de sumas, tiempo de ejecución. """ n = A.shape[0] #.............................definimos el tamaño de las matrices (arreglos) C = np.zeros((n, n)) # ......................Se incializa la matriz resultante (C) en ceros multiplications =0# .......................contador para multiplicaciones (multiplications) additions =0# .............................contadors para las sumas (additions) start_time = time.time() # ..................guardamos el tiempo de ejecución#Hacemos las iteraciones de filas y columnasfor i in tqdm(range(n), desc="Filas"): #..............filas de Afor j inrange(n): # .............................columnas Bfor k inrange(n): # .........................iteramos sobre las columnas de A y las filas de B C[i, j] += A[i, k] * B[k, j] # ...........operaciones de suma y multiplicación multiplications +=1# ...................icremento de contador mmultiplicationsif k >0: additions +=1#......................incremento de contador additions end_time = time.time() # .............................guardamos el tiempo en que finaliza el proceso execution_time = end_time - start_time # .............tiempo total de ejecuciónreturn C, multiplications, additions, execution_time # retorno de matriz y contadores# diccionario para almacenar los resultados de las multiplicacionesmultiplicacion = {}# ejecutamos para diferentes tamaños de matriz con seguimiento del progreso (100,300 y 1000)for idx, n inenumerate([100, 300, 1000]): A = np.random.rand(n, n) B = np.random.rand(n, n) C, multiplications, additions, execution_time = matrix_multiplication(A, B)# guardamos los resultados en el diccionario que definimos antes multiplicacion[idx] = {"n": n,"multiplications": multiplications,"additions": additions,"execution_time": execution_time }# mostramos el progreso (por fines prácticos y análisis)print(f"Progreso: {((idx +1) /3) *100:.2f}% completado")
# definimos la función para eliminación de gaussdef gaussian_elimination(A):"""Realiza la eliminación Gaussiana y cuenta el número de operaciones. Args: A: La matriz a la que se le aplicará la eliminación Gaussiana. Return: Matriz resultante, número de multiplicaciones, número de sumas, tiempo de ejecución. """ n = A.shape[0] # ...........................definimos el tamaño de las matrices (arreglos) multiplications =0# ......................contador de multiplicaciones (multiplications) additions =0# ......................contador de sumas (additions) start_time = time.time() # ..................guardamos el tiempo de ejecuciónfor i in tqdm(range(n), desc="Filas"): #..............filas de A# Hacer el elemento A[i, i] igual a 1 pivot = A[i, i]for j inrange(i, n): #..............columnas de Aif A[i, j] !=0: A[i, j] /= pivot multiplications +=1# Hacer los elementos de la columna i, debajo de A[i, i], iguales a 0for k inrange(i +1, n): # ..............................iteramos filas de A desde i factor = A[k, i]for j inrange(i, n): # ...............................iteramos columnas de A desde i A[k, j] -= factor * A[i, j] additions +=1if A[i, j] !=0: multiplications +=1 end_time = time.time() # ...................guardamos el tiempo en que finaliza el proceso execution_time = end_time - start_time # .............tiempo total de ejecuciónreturn A, multiplications, additions, execution_time # retorno de matriz y contadores# diccionario para almacenar los resultados de la eliminación Gaussianaeliminacion_gauss = {}# ejecutamos para diferentes tamaños de matriz con seguimiento del progreso (100, 300 y 1000)for idx, n inenumerate([100, 300, 1000]): A = np.random.rand(n, n) A, multiplications, additions, execution_time = gaussian_elimination(A)# guardamos los resultados en el diccionario que definimos antes eliminacion_gauss[idx] = {"n": n,"multiplications": multiplications,"additions": additions,"execution_time": execution_time }# mostramos el progreso (por fines prácticos y análisis)print(f"Progreso: {((idx +1) /3) *100:.2f}% completado")
# definimos la función para eliminación gaussianadef gauss_jordan_elimination(A):"""Realiza la eliminación gaussiana con el método de Gauss-Jordan y cuenta el número de operaciones. Args: A: La matriz a la que se le aplicará la eliminación gaussiana. Return: Matriz resultante, número de multiplicaciones, número de sumas, tiempo de ejecución. """ n = A.shape[0] # ...........................definimos el tamaño de las matrices (arreglos) multiplications =0# ......................contador de multiplicaciones additions =0# .............................Contador de sumas start_time = time.time() # ..................guardamos el tiempo de ejecuciónfor i in tqdm(range(n), desc="Filas"): #..............filas de A# Hacer el elemento A[i, i] igual a 1 pivot = A[i, i] # ................................metemos un pivoteo para la fila ifor j inrange(n): # .,...........................columnas en Aif A[i, j] !=0: A[i, j] /= pivot # .......................dividimos cada elemento de la fila i por el pivote multiplications +=1# ...................incremento de contador mmultiplications# Hacer los elementos de la columna i, excepto A[i, i], iguales a 0for k inrange(n): # ..............................iteramos filas de Aif k != i: # ..................................restringuimos la fila pivote factor = A[k, i] # ........................obtenemos el factor de la fila kfor j inrange(n): # .............................iteramos columnas de A A[k, j] -= factor * A[i, j] # ......restamos el producto del factor y el elemento correspondiente de la fila del pivote additions +=1#......................incremento de contador additionsif A[i, j] !=0: multiplications +=1# Incrementa el contador de multiplicaciones si el elemento no es cero end_time = time.time() # .............................guardamos el tiempo en que finaliza el proceso execution_time = end_time - start_time # .............tiempo total de ejecuciónreturn A, multiplications, additions, execution_time # retorno de matriz y contadores# diccionario para almacenar los resultados de la eliminación gaussianaeliminacion = {}# ejecutamos para diferentes tamaños de matriz con seguimiento del progreso (100,300 y 1000)for idx, n inenumerate([100, 300, 1000]): A = np.random.rand(n, n) A, multiplications, additions, execution_time = gauss_jordan_elimination(A)# guardamos los resultados en el diccionario que definimos antes eliminacion[idx] = {"n": n,"multiplications": multiplications,"additions": additions,"execution_time": execution_time }# mostramos el progreso (por fines prácticos y análisis)print(f"Progreso: {((idx +1) /3) *100:.2f}% completado")
Maneja de manera separada los datos de conteo de operaciones (multiplicaciones y sumas escalares) y las de tiempo real.
import pandas as pd# Creamos un dataframe con la info de los diccionariosdata = []for idx in multiplicacion.keys(): data.append([ multiplicacion[idx]["n"], multiplicacion[idx]["multiplications"] + multiplicacion[idx]['additions'], multiplicacion[idx]["execution_time"], eliminacion_gauss[idx]["multiplications"] + eliminacion_gauss[idx]['additions'], eliminacion_gauss[idx]["execution_time"], eliminacion[idx]["multiplications"] + eliminacion[idx]['additions'], eliminacion[idx]["execution_time"] ])df = pd.DataFrame(data, columns=["Tamaño","Operaciones Multiplicación de Matrices","Tiempo Multiplicación de Matrices","Operaciones Gauss","Tiempo Gauss","Operaciones Gauss-Jordan","Tiempo Gauss-Jordan"])# mostramos el DataFramedf
Tamaño
Operaciones Multiplicación de Matrices
Tiempo Multiplicación de Matrices
Operaciones Gauss
Tiempo Gauss
Operaciones Gauss-Jordan
Tiempo Gauss-Jordan
0
100
1990000
0.741628
671650
0.235906
1495000
0.700069
1
300
53910000
17.891673
18044950
6.445393
40455000
18.622784
2
1000
1999000000
687.908043
667166500
238.529724
1499500000
701.887946
import matplotlib.pyplot as plt# graficamos los tiempos de ejecución de cada procesoplt.figure(figsize=(10, 5))plt.plot(df["Tamaño"], df["Tiempo Multiplicación de Matrices"], marker='o', label='Tiempo Multiplicación de Matrices')plt.plot(df["Tamaño"], df["Tiempo Gauss"], marker='o', label='Tiempo Gauss')plt.plot(df["Tamaño"], df["Tiempo Gauss-Jordan"], marker='o', label='Tiempo Gauss-Jordan')plt.xlabel('Tamaño de la Matriz')plt.ylabel('Tiempo de Ejecución (segundos)')plt.title('Comparación de Tiempos de Ejecución')plt.legend()plt.grid(True)plt.show()# graficamos el número de operaciones de los procesosplt.figure(figsize=(10, 5))plt.plot(df["Tamaño"], df["Operaciones Multiplicación de Matrices"], marker='o', label='Operaciones Multiplicación de Matrices')plt.plot(df["Tamaño"], df["Operaciones Gauss"], marker='o', label='Operaciones Gauss')plt.plot(df["Tamaño"], df["Operaciones Gauss-Jordan"], marker='o', label='Operaciones Gauss-Jordan')plt.xlabel('Tamaño de la Matriz')plt.ylabel('Número de Operaciones')plt.title('Comparación del Número de Operaciones')plt.legend()plt.grid(True)plt.show()
✅ Discute los resultados experimentales:
¿Qué puedes concluir? A partir de los resultados obtenidos, podemos extraer varias conclusiones clave:
La cantidad de operaciones de la multiplicación de matrices es significativamente mayor que la de los métodos de eliminación Gaussiana y Gauss-Jordan, y su tiempo de ejecución es considerablemente más alto para la multiplicación de matrices en comparación con los otros métodos, en particular cuando 𝑛 aumenta.
En operaciones el método Gauss-Jordan realiza más ciclos en el bucle que la eliminación Gaussiana, lo que explica su mayor tiempo de ejecución, esto se debe a que Gauss-Jordan no solo transforma la matriz en una forma triangular superior, sino que también la convierte en una matriz identidad, lo que requiere más pasos.
Para 𝑛 = 1000, la multiplicación de matrices toma más de 687 segundos, un incremento drástico en comparación con los 0.74 segundos para 𝑛 = 100, este comportamiento también ocurre con la eliminación Gaussiana y Gauss-Jordan, aunque en tiempo menor la eliminación de gauss.
Si tuvieramos una problematica que su objetivo sea resolver sistemas de ecuaciones lineales, la eliminación Gaussiana es más eficiente que la multiplicación de matrices en términos de tiempo de cómputo y operaciones requeridas, considerandola también más eficiente que Gauss-jordan.
¿Cuál es el impacto de acceder los elementos contiguos en memoria de una matriz?
Este acceso a elementos continuos en memoria, es una factor de eifciencia computacional, debido a cómo las computadoras almacenan y recuperan datos de la RAM, la multiplicación de matrices accede a los elementos de manera menos eficiente en comparación a los otros métodos que pueden verse más optimizados (hablando en particular de la eliminación de gauss), en particular estas últimas dos realizan muchas operaciones fila por fila, pero generalemnet son más eficientes en términos de terminos de acceso a la memoria.
¿Qué cambiarías si utilizas matrices dispersas? ¿Cuáles serían los costos?
Las matrices dispersas contienen muchos ceros, lo que nos permitiria usar estructuras de datos especiales para ahorrar memoria y mejorar la eficiencia computacional. Osea que si por ejemplo usaramos scipy.sparse.csr_matrix en lugar del numpy, pues solo almacenariamos datos no nulos y eso correspondería a reducir el uso de la memoria, omitir operaciones incesarias por los ceros, para que fuesé más rápido el cálculo.
Existen otras representaciones como la lista de coordenadas, coordinate lists (COO), o las representaciones dispersas compimidas, sparse row (CSR) y compressed sparse column (CSC) (Scott y Tůma 2023). Todas estas representaciones tratan de disminuir el uso de memoria y aprovechar la gran dispersión para realizar operaciones solo cuando sea estrictamente necesario.
📚 Referencias:
Scott, Jennifer, and Miroslav Tůma. 2023. “An Introduction to Sparse Matrices.” In Algorithms for Sparse Linear Systems, 1–18. Cham: Springer International Publishing.
Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2022). Introduction to Algorithms (4th ed.). MIT Press.
Cap. 10: Estructuras de datos y su impacto en la eficiencia computacional. -Cap. 22: Algoritmos elementales en grafos y su relación con el acceso a memoria.
Cap. 28: Algoritmos eficientes para operaciones matriciales, incluyendo la multiplicación de matrices.