20+ Ejemplos de multilplicación de matrices en NumPy
En este tutorial, revisaremos varias formas de realizar multiplicaciones de matrices utilizando arreglos NumPy. Aprenderemos como multiplicar matrices con diferentes tamaños.
Además, aprenderemos a acelerar el proceso de multiplicación usando la GPU y otros temas de actualidad, ¡así que empecemos!
Antes de seguir adelante, es mejor revisar algunas terminologías básicas del Álgebra de Matriz.
Tabla de contenidos
Terminología Básica
Vector: Algebraicamente, un vector es una colección de coordenadas de un punto en el espacio. Así, un vector con dos valores representa un punto en un espacio bidimensional. En informática, un vector es una disposición de números a lo largo de una sola dimensión. También se conoce comúnmente como una matriz o una lista o una tupla.
ej. [1,2,3,4]
Matriz: Una matriz (matrices plurales) es una disposición bidimensional de números o un conjunto de vectores ej:
[[1,2,3], [4,5,6], [7,8,9]]
Producto Punto: Un producto punto es una operación matemática entre 2 vectores de igual longitud. Es igual a la suma de los productos de los elementos correspondientes de los vectores.
Con un claro entendimiento de estas terminologías, estamos listos para empezar.
Multiplicación de una matriz con un vector
Comencemos con una forma simple de multiplicación de matrices, entre una matriz y un vector.
Antes de proceder, entendamos primero cómo crear una matriz usando NumPy.
El método array() de NumPy se utiliza para representar vectores, matrices y tensores de mayor dimensión. Definamos un vector de 5 dimensiones y una matriz de 3×3 usando NumPy.
import numpy as np a = np.array([1, 3, 5, 7, 9]) b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) print("Vector a:\n", a) print() print("Matrix b:\n", b)
Salida:
Veamos ahora cómo se produce la multiplicación entre una matriz y un vector.
Para una multiplicación matriz-vectorial, hay que tener en cuenta los siguientes puntos:
- El resultado de una multiplicación matriz-vectorial es un vector.
- Cada elemento de este vector se obtiene realizando un producto puntual entre cada fila de la matriz y el vector que se multiplica.
- El número de columnas de la matriz debe ser igual al número de elementos del vector
usaremos el método matmul() de NumPy para la mayoría de nuestras operaciones de multiplicación de matrices.
Definamos una matriz de 3×3 y la multiplicamos con un vector de longitud 3.
import numpy as np a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) b= np.array([10, 20, 30]) print("A =", a) print("b =", b) print("Ab =",np.matmul(a,b))
Salida:
Obsérvese cómo el resultado es un vector de longitud igual a las filas de la matriz multiplicadora.
Multiplicación con otra matriz
hora, entendimos la multiplicación de una matriz con un vector; sería fácil calcular la multiplicación de dos matrices.
Pero, antes de eso, repasemos las reglas más importantes de la multiplicación de matrices:
- El número de columnas de la primera matriz debe ser igual al número de filas de la segunda matriz.
- Si multiplicamos una matriz de dimensiones m x n por otra matriz de dimensiones n x p, el producto resultante será una matriz de dimensiones m x p.
Consideremos la multiplicación de una matriz m x n A con una matriz n x p B:
El producto de las dos matrices C = AB tendrá m hileras y p columnas.
Cada elemento de la matriz de productos C resulta de un producto puntual entre un vector fila en A y un vector columna en B.
Hagamos ahora una multiplicación matricial de 2 matrices en Python, usando NumPy.
Generaremos aleatoriamente dos matrices de dimensiones 3 x 2 y 2 x 4.
Usaremos el método np.random.randint() para generar los números.
import numpy as np np.random.seed(42) A = np.random.randint(0, 15, size=(3,2)) B = np.random.randint(0, 15, size =(2,4)) print("Matrix A:\n", A) print("shape of A =", A.shape) print() print("Matrix B:\n", B) print("shape of B =", B.shape)
Salida:
Nota: estamos estableciendo una semilla aleatoria usando ‘np.random.seed()’ para hacer determinístico el generador de números aleatorios.
Esto generará los mismos números aleatorios cada vez que se ejecute este fragmento de código. Este paso es esencial si quieres reproducir el resultado en un momento posterior.
Puedes establecer cualquier otro entero como semilla, pero sugiero que lo establezca en 42 para este tutorial para que su salida coincida con las que se muestran en las capturas de pantalla de salida.
Multipliquemos ahora las dos matrices usando el método np.matmul(). La matriz resultante debería tener la forma 3 x 4.
C = np.matmul(A, B) print("product of A and B:\n", C) print("shape of product =", C.shape)
Salida:
Multiplicación entre 3 matrices
La multiplicación de las tres matrices estará compuesta por dos operaciones de multiplicación de dos matrices, y cada una de las dos operaciones seguirá las mismas reglas que se han discutido en la sección anterior.
Digamos que estamos multiplicando tres matrices A, B y C, y el producto es D = ABC.
Aquí, el número de columnas en A debe ser igual al número de filas en B, y el número de filas en C debe ser igual al número de columnas en B.
La matriz resultante tendrá filas iguales al número de filas en A y columnas iguales al número de columnas en C.
Una propiedad importante de la operación de multiplicación de la matriz es que es asociativa.
Con la multiplicación de múltiples matrices, el orden de las operaciones de multiplicación individuales no importa y, por lo tanto, no produce resultados diferentes.
Por ejemplo, en nuestro ejemplo de multiplicación de 3 matrices D = ABC, no importa si realizamos primero AB o primero BC.
Ambos pedidos darían el mismo resultado. Hagamos un ejemplo en Python.
import numpy as np np.random.seed(42) A = np.random.randint(0, 10, size=(2,2)) B = np.random.randint(0, 10, size=(2,3)) C = np.random.randint(0, 10, size=(3,3)) print("Matrix A:\n{}, shape={}\n".format(A, A.shape)) print("Matrix B:\n{}, shape={}\n".format(B, B.shape)) print("Matrix C:\n{}, shape={}\n".format(C, C.shape))
Salida:
Basándonos en las reglas que hemos discutido anteriormente, la multiplicación de estas tres matrices debería dar lugar a una matriz resultante de forma (2, 3).
Obsérvese que el método np.matmul() sólo acepta dos matrices como entrada para la multiplicación, por lo que llamaremos al método dos veces en el orden en que deseamos multiplicar, y pasaremos el resultado de la primera llamada como parámetro a la segunda.
(Encontraremos una mejor manera de tratar este problema en una sección posterior cuando introduzcamos el operador “@”)
Hagamos la multiplicación en ambos órdenes y validemos la propiedad de la asociatividad.
D = np.matmul(np.matmul(A,B), C) print("Result of multiplication in the order (AB)C:\n\n{},shape={}\n".format(D, D.shape)) D = np.matmul(A, np.matmul(B,C)) print("Result of multiplication in the order A(BC):\n\n{},shape={}".format(D, D.shape))
Salida:
Como podemos ver, el resultado de la multiplicación de las tres matrices sigue siendo el mismo tanto si multiplicamos primero A y B, como B y C.
Por lo tanto, la propiedad de la asociatividad se mantiene validada.
Además, la forma de la matriz resultante es (2, 3), que está en las líneas esperadas.
Multiplicación de Matriz 3D con NumPy
Una matriz 3D no es más que una colección (o una pila) de muchas matrices 2D, igual que una matriz 2D es una colección/pila de muchos vectores 1D.
Por lo tanto, la multiplicación de matrices 3D implica múltiples multiplicaciones de matrices 2D, que eventualmente se reducen a un producto de puntos entre sus vectores de fila/columna.
Consideremos un ejemplo de matriz A de forma (3,3,2) multiplicada por otra matriz 3D B de forma (3,2,4).
import numpy as np np.random.seed(42) A = np.random.randint(0, 10, size=(3,3,2)) B = np.random.randint(0, 10, size=(3,2,4)) print("A:\n{}, shape={}\nB:\n{}, shape={}".format(A, A.shape,B, B.shape))
Salida:
La primera matriz es una pila de tres matrices 2D, cada una de ellas de forma (3,2), y la segunda es una pila de 3 matrices 2D, cada una de forma (2,4).
La multiplicación de la matriz entre estas dos implicará tres multiplicaciones entre las correspondientes matrices 2D de A y B que tienen formas (3,2) y (2,4) respectivamente.
Específicamente, la primera multiplicación será entre A[0] y B[0], la segunda multiplicación será entre A[1] y B[1], y finalmente, la tercera multiplicación será entre A[2] y B[2].
El resultado de cada multiplicación individual de las matrices 2D será de forma (3,4). Por lo tanto, el producto final de las dos matrices 3D será una matriz de forma (3,3,4).
Vamos a realizar esto usando el código.
C = np.matmul(A,B) print("Product C:\n{}, shape={}".format(C, C.shape))
Salida:
Alternativas a np.matmul()
Apart from ‘np.matmul()’, there are two other ways of doing matrix multiplication – the np.dot() method and the ‘@’ operator, each offering some differences/flexibility in matrix multiplication operations.
The ‘np.dot()’ method
Aparte de ‘np.matmul()’, hay otras dos formas de hacer multiplicación de matrices – el método np.dot() y el operador ‘@’, cada uno ofreciendo algunas diferencias/flexibilidad en las operaciones de multiplicación de matrices.
Veamos un ejemplo:
import numpy as np # a 3x2 matrix A = np.array([[8, 2, 2], [1, 0, 3]]) # a 2x3 matrix B = np.array([[1, 3], [5, 0], [9, 6]]) # dot product should return a 2x2 product C = np.dot(A, B) print("product of A and B:\n{} shape={}".format(C, C.shape))
Salida:
Aquí, definimos una matriz de 3×2, y una matriz de 2×3 y su producto puntual da un resultado de 2×2 que es la multiplicación de la matriz de las dos matrices,
lo mismo que lo que ‘np.matmul()’ habría devuelto.
La diferencia entre np.dot() y np.matmul() está en su funcionamiento en matrices 3D.
Mientras que ‘np.matmul()’ opera en dos matrices 3D calculando la multiplicación de la matriz de los pares correspondientes de matrices 2D (como se discutió en la última sección), np.dot() por otro lado calcula los productos de puntos de varios pares de vectores de fila y vectores de columna de la primera y segunda matriz respectivamente.
np.dot() en dos matrices 3D A y B devuelve un producto de suma sobre el último eje de A y el penúltimo eje de B.
Esto no es intuitivo y no es fácilmente comprensible.
Así, si A tiene forma (a, b, c) y B tiene forma (d, c, e), entonces el resultado de np.dot(A, B) tendrá forma (a,d,b,e) cuyo elemento individual en una posición (i,j,k,m) viene dado por:
dot(A, B)[i,j,k,m] = sum(A[i,j,:] * B[k,:,m])
Veamos un ejemplo:
import numpy as np np.random.seed(42) A = np.random.randint(0, 10, size=(2,3,2)) B = np.random.randint(0, 10, size=(3,2,4)) print("A:\n{}, shape={}\nB:\n{}, shape={}".format(A, A.shape,B, B.shape))
Salida:
Si ahora pasamos estas matrices al método ‘np.dot()’, nos devolverá una matriz de forma (2,3,3,4) cuyos elementos individuales se calculan con la fórmula dada anteriormente.
C = np.dot(A,B) print("np.dot(A,B) =\n{}, shape={}".format(C, C.shape))
Salida:
Otra diferencia importante entre ‘np.matmul()’ y ‘np.dot()’ es que ‘np.matmul()’ no permite la multiplicación con un escalar (lo discutiremos en la siguiente sección), mientras que ‘np.dot()’ lo permite.
The ‘@’ operator
El operador @ introducido en Python 3.5, realiza la misma operación que ‘np.matmul()’.
Veamos un ejemplo anterior de “np.matmul()” usando el operador @, y veremos el mismo resultado que se devolvió anteriormente:
import numpy as np np.random.seed(42) A = np.random.randint(0, 15, size=(3,2)) B = np.random.randint(0, 15, size =(2,4)) print("Matrix A:\n{}, shape={}".format(A, A.shape)) print("Matrix B:\n{}, shape={}".format(B, B.shape)) C = A @ B print("product of A and B:\n{}, shape={}".format(C, C.shape))
Salida:
El operador “@” se vuelve útil cuando realizamos multiplicación de matrices de más de dos matrices.
Anteriormente, tuvimos que llamar ‘np.matmul()’ varias veces y pasar sus resultados como un parámetro a la siguiente llamada.
Ahora, podemos realizar la misma operación de una manera más simple (y más intuitiva):
import numpy as np np.random.seed(42) A = np.random.randint(0, 10, size=(2,2)) B = np.random.randint(0, 10, size=(2,3)) C = np.random.randint(0, 10, size=(3,3)) print("Matrix A:\n{}, shape={}\n".format(A, A.shape)) print("Matrix B:\n{}, shape={}\n".format(B, B.shape)) print("Matrix C:\n{}, shape={}\n".format(C, C.shape)) D = A @ B @ C # earlier np.matmul(np.matmul(A,B),C) print("Product ABC:\n\n{}, shape={}\n".format(D, D.shape))
Salida:
Multiplicación con un escalar (valor individual)
Hasta ahora, hemos realizado la multiplicación de una matriz con un vector u otra matriz. Pero, ¿qué sucede cuando realizamos la multiplicación de una matriz con un escalar o un valor numérico único?
El resultado de tal operación se obtiene multiplicando cada elemento de la matriz con el valor escalar. Así, la matriz de salida tiene la misma dimensión que la matriz de entrada.
Nótese que ‘np.matmul()’ no permite la multiplicación de una matriz con un escalar. Esto se puede lograr utilizando el método np.dot() o utilizando el operador ‘*’.
Veamos esto en un ejemplo de código.
import numpy as np A = np.array([[1,2,3], [4,5, 6], [7, 8, 9]]) B = A * 10 print("Matrix A:\n{}, shape={}\n".format(A, A.shape)) print("Multiplication of A with 10:\n{}, shape={}".format(B, B.shape))
Salida:
Multiplicación de una matriz de elementos
A veces queremos hacer la multiplicación de los elementos correspondientes de dos matrices que tienen la misma forma.
Esta operación también se llama el Producto Hadamard. Acepta dos matrices de las mismas dimensiones y produce una tercera matriz de la misma dimensión.
Se puede lograr esto llamando a la función multiply() de NumPy o usando el operador ‘*’.
import numpy as np np.random.seed(42) A = np.random.randint(0, 10, size=(3,3)) B = np.random.randint(0, 10, size=(3,3)) print("Matrix A:\n{}\n".format(A)) print("Matrix B:\n{}\n".format(B)) C = np.multiply(A,B) # or A * B print("Element-wise multiplication of A and B:\n{}".format(C))
Salida:
La única regla que hay que tener en cuenta para la multiplicación de los elementos es que las dos matrices deben tener la misma forma.
Sin embargo, si falta una dimensión de una matriz, NumPy la emitiria para que coincida con la forma de la otra matriz.
De hecho, la multiplicación de matrices con un escalar también implica la emisión del valor escalar a una matriz de la forma igual a la matriz operando en la multiplicación.
Esto significa que cuando multiplicamos una matriz de forma (3,3) con un valor escalar 10, NumPy crearía otra matriz de forma (3,3) con valores constantes diez en todas las posiciones de la matriz y realizaría la multiplicación por elementos entre las dos matrices.
Entendamos esto a través de un ejemplo:
import numpy as np np.random.seed(42) A = np.random.randint(0, 10, size=(3,4)) B = np.array([[1,2,3,4]]) print("Matrix A:\n{}, shape={}\n".format(A, A.shape)) print("Matrix B:\n{}, shape={}\n".format(B, B.shape)) C = A * B print("Element-wise multiplication of A and B:\n{}".format(C))
Salida:
Obsérvese cómo la segunda matriz, que tenía forma (1,4) se transformó en una matriz (3,4) a través de la emisión, y la multiplicación por elementos entre las dos matrices tuvo lugar.
Matriz elevada a una potencia (potenciación de matriz)
Al igual que podemos elevar un valor escalar a un exponente, podemos hacer la misma operación con las matrices.
Al igual que elevar un valor escalar (base) a un exponente n es igual a multiplicar repetidamente las n bases, se observa el mismo patrón al elevar una matriz a la potencia, lo que implica multiplicaciones repetidas de la matriz.
Por ejemplo, si elevamos una matriz A a una potencia n, es igual a las multiplicaciones de matriz de n matrices, todas las cuales serán la matriz A.
Obsérvese que para que esta operación sea posible, la matriz de la base tiene que ser cuadrada.
Esto es para asegurar el número de columnas en la matriz anterior = número de filas en la matriz siguiente.
Esta operación se realiza en Python mediante el método linalg.matrix_power() de NumPy, que acepta como parámetros la matriz base y una potencia entera.
Veamos un ejemplo en Python:
import numpy as np np.random.seed(10) A = np.random.randint(0, 10, size=(3,3)) A_to_power_3 = np.linalg.matrix_power(A, 3) print("Matrix A:\n{}, shape={}\n".format(A, A.shape)) print("A to the power 3:\n{}, shape={}".format(A_to_power_3,A_to_power_3.shape))
Salida:
Podemos validar este resultado haciendo una multiplicación matricial normal con tres operandos (todos ellos A), usando el operador “@”:
B = A @ A @ A print("B = A @ A @ A :\n{}, shape={}".format(B, B.shape))
Salida:
Como pueden ver, los resultados de ambas operaciones coinciden.
Una pregunta importante que surge de esta operación es – ¿Qué pasa cuando la potencia es 0?
Para responder a esta pregunta, repasemos lo que sucede cuando elevamos una base escalar a la potencia 0.
Obtenemos el valor 1, ¿verdad? Ahora, ¿cuál es el equivalente a 1 en el Álgebra de Matriz? ¡Adivinaste bien!
Es la matriz de identidad.
Así que, elevando una matriz de n x n a la potencia 0 resulta en una matriz de identidad I de forma n x n.
Comprobemos esto rápidamente en Python, usando nuestra anterior matriz A.
C = np.linalg.matrix_power(A, 0) print("A to power 0:\n{}, shape={}".format(C, C.shape))
Salida:
Potenciación de los elementos
Al igual que podemos hacer la multiplicación de matrices por elementos, también podemos hacer la potenciación por elementos, es decir, elevar cada elemento individual de una matriz a alguna potencia.
Esto se puede lograr en Python usando el operador de exponente estándar ‘**’ – un ejemplo de sobrecarga del operador.
De nuevo, podemos proporcionar una única potencia constante para todos los elementos de la matriz, o una matriz de potencias para cada elemento de la matriz base.
Veamos ejemplos de ambas en Python:
import numpy as np np.random.seed(42) A = np.random.randint(0, 10, size=(3,3)) print("Matrix A:\n{}, shape={}\n".format(A, A.shape)) #constant power B = A**2 print("A^2:\n{}, shape={}\n".format(B, B.shape)) powers = np.random.randint(0, 4, size=(3,3)) print("Power matrix:\n{}, shape={}\n".format(powers, powers.shape)) C = A ** powers print("A^powers:\n{}, shape={}\n".format(C, C.shape))
Salida:
Multiplicación a partir de un índice determinado
Supongamos que tenemos una matriz A de 5 x 6 y otra B de 3 x 3. Obviamente, no podemos multiplicar estas dos juntas, debido a las inconsistencias dimensionales.
Pero, ¿y si quisiéramos multiplicar una submatriz de 3 x 3 en la matriz A con la matriz B manteniendo los otros elementos en A sin cambios?
Para una mejor comprensión, consulte la siguiente imagen:
Se puede lograr esta operación en Python utilizando el rebanado de la matriz para extraer la submatriz de A, realizando la multiplicación con B, y luego escribiendo el resultado en el índice correspondiente en A.
Veamos esto en acción.
import numpy as np np.random.seed(42) A = np.random.randint(0, 10, size=(5,6)) B = np.random.randint(0, 10, size=(3,3)) print("Matrix A:\n{}, shape={}\n".format(A, A.shape)) print("Matrix B:\n{}, shape={}\n".format(B, B.shape)) C = A[1:4,2:5] @ B A[1:4,2:5] = C print("Matrix A after submatrix multiplication:\n{}, shape={}\n".format(A, A.shape))
Salida:
Como se puede ver, sólo los elementos de los índices de fila 1 a 3 y de columna 2 a 4 se han multiplicado con B y se han vuelto a escribir en A, mientras que los elementos restantes de A han permanecido inalterados.
Además, no es necesario sobrescribir la matriz original. También podemos escribir el resultado en una nueva matriz copiando primero la matriz original a una nueva matriz y luego escribiendo el producto en la posición de la submatriz.
Multiplicación de Matrices utilizando GPU
Sabemos que NumPy acelera las operaciones de la matriz al paralelizar muchos cálculos y hacer uso de las capacidades de cálculo paralelo de nuestra CPU.
Sin embargo, las aplicaciones de hoy en día necesitan más que eso. Las CPUs ofrecen capacidades de cálculo limitadas, y no son suficientes para el gran número de cálculos que necesitamos, típicamente en aplicaciones como el aprendizaje profundo.
Ahí es donde las GPU entran en escena. Ofrecen grandes capacidades de cálculo y una excelente infraestructura de cálculo en paralelo, lo que nos ayuda a ahorrar una cantidad significativa de tiempo al realizar cientos de miles de operaciones en fracciones de segundos.
En esta sección, veremos cómo podemos realizar la multiplicación de matrices en una GPU en lugar de una CPU y ahorrar mucho tiempo al hacerlo.
NumPy no ofrece la funcionalidad de hacer multiplicaciones de matriz en la GPU. Así que debemos instalar algunas librerías adicionales que nos ayuden a lograr nuestro objetivo.
Primero instalaremos las bibliotecas ‘scikit-cuda‘ y ‘PyCUDA‘ utilizando pip instalador. Estas bibliotecas nos ayudan a realizar cálculos en las GPU basadas en CUDA. Para instalar estas librerías desde su terminal, si tiene una GPU instalada en su máquina.
pip install pycuda pip install scikit-cuda
Si no tiene una GPU en su máquina, puede probar Google Colab portátiles, y permitir el acceso a la GPU; es de uso gratuito. Ahora escribiremos el código para generar dos matrices de 1000×1000 y realizaremos la multiplicación de las matrices entre ellas usando dos métodos
- Using NumPy’s ‘matmul()‘ method on a CPU
- Using scikit-cuda’s ‘linalg.mdot()‘ method on a GPU
En el segundo método, generaremos las matrices en una CPU; luego las almacenaremos en la GPU (usando el método ‘gpuarray.to_gpu()’ de PyCUDA) antes de realizar la multiplicación entre ellas. Usaremos el módulo ‘tiempo‘ para calcular el tiempo de cálculo en ambos casos.
Usar CPU
import numpy as np import time # generating 1000 x 1000 matrices np.random.seed(42) x = np.random.randint(0,256, size=(1000,1000)).astype("float64") y = np.random.randint(0,256, size=(1000,1000)).astype("float64") #computing multiplication time on CPU tic = time.time() z = np.matmul(x,y) toc = time.time() time_taken = toc - tic #time in s print("Time taken on CPU (in ms) = {}".format(time_taken*1000))
Salida:
En algunos sistemas de hardware antiguos, puede que se produzca un error de memoria, pero si tiene suerte, funcionará en mucho tiempo (depende de su sistema).
Ahora, hagamos la misma multiplicación en una GPU y veamos cómo el tiempo de cálculo difiere entre los dos.
Usar GPU
#computing multiplication time on GPU linalg.init() # storing the arrays on GPU x_gpu = gpuarray.to_gpu(x) y_gpu = gpuarray.to_gpu(y) tic = time.time() #performing the multiplication z_gpu = linalg.mdot(x_gpu, y_gpu) toc = time.time() time_taken = toc - tic #time in s print("Time taken on a GPU (in ms) = {}".format(time_taken*1000))
Salida:
más que en la CPU.
Este era todavía un cálculo pequeño. Para los cálculos a gran escala, las GPU nos dan aceleraciones de unos pocos órdenes de magnitud.
Conclusión
En este tutorial, vimos cómo se produce la multiplicación de dos matrices, las reglas que las rigen y cómo implementarlas en Python.
También vimos diferentes variantes de la multiplicación de la matriz estándar (y su implementación en NumPy) como la multiplicación de más de dos matrices, la multiplicación sólo en un índice particular, o la potencia de una matriz.
También vimos los cálculos de los elementos en matrices como la multiplicación de matrices de los elementos, o la potenciación de los elementos.
Por último, vimos cómo podemos acelerar el proceso de multiplicación de matrices al realizarlas en una GPU.
Fundadora de LikeGeeks. Estoy trabajando como administrador de sistemas Linux desde 2010. Soy responsable de mantener, proteger y solucionar problemas de servidores Linux para múltiples clientes de todo el mundo. Me encanta escribir guiones de shell y Python para automatizar mi trabajo.