danielsarmiento.dev

Vectorización en Python

March 01, 2020

Qué es la vectorización

La vectorización, no sólo en Python, se refiere a un tipo de operaciones que se benefician de la computación en paralelo SIMD (Single Instruction, Multiple Data). Esto quiere decir que para realizar operaciones en los elementos de los arrays (n-dimeniones) nos ahorraremos los costosos bucles for.

Listas vs Arrays

En la librería estándar de Python existen varias estructuras de datos que nos permiten almacenar colecciones de objetos (listas, tuplas, sets, diccionarios). En concreto, las listas son colecciones ordenadas y mutables.

# Ejemplos de listas
veggies = ["carrot", "tomato", "potato", "lettuce"]
even = [0, 2, 4, 6, 8, 10]

Estas se asemejan a otras estructuras tipo vector/array de lenguajes como Java o C, pero con ciertas diferencias bastante importantes.

// Ejemplos de arrays en Java
String[] veggies = new String[4] {"carrot","tomato","potato", "lettuce"};
int[] even = new int[6] {0, 2, 4, 6, 8, 10};
 // Ejemplos de arrays en C++
 string veggies[4] = { "carrot", "tomato", "potato", "lettuce" };
 int even[6] = {0, 2, 4, 6, 8, 10};

Como podemos observar, tanto en Java como en C++ hemos especificado el tamaño del array, pero no así en Python. Esto significa que Python no ha reservado la memoria para los contenidos de las listas, sino que la asignará dinámicamente. Esto tiene ventajas y desventajas:

Ventajas

  • Agilidad en el desarrollo
  • Mutabilidad
  • Almacenan distintos tipos sin protestar
  • Menos restricciones de uso

Desventajas

  • Más consumo de memoria
  • Peor tiempo de ejecución
  • Menos optimizada para algunas librerías como SciPy

Los arrays tienen más restricciones, pero son estas precisamente las que consiguen que pueda ser una muy buena opción para determinadas tareas. Por lo general, estas restricciones son:

  • En los arrays, todos sus elementos deben ser del mismo tipo.
  • La longitud de los arrays no puede modificarse (es inmutable) y debe ser definida en el momento de su definición (creación).

El tener una menor abstracción que las listas desemboca en una mejora del rendimiento. Al saber cuánto ocupa cada elemento y cuántos elementos hay en un array, el tamaño del array es conocido en el momento de su creación y, por lo tanto, los elementos ocuparán espacios de memoria contiguos (sin espacios entre ellos).

NumPy arrays

Para conseguir esto en Python se utiliza el paquete NumPy, del cual podemos aprovechar su array n-dimensional. NumPy no forma parte de la librería estándar de Python, por lo que tendremos que descargarlo e instalarlo. Desde la consola introducimos los siguientes comandos pip install numpy o pip3 install numpy dependiendo de nuestra versión.

Si quisiéramos reproducir los arrays del inicio del post con arrayas n-dimensionales de NumPy primero deberíamos pensar qué tipo de datos nos conviene para almacenar la misma información. Si omitimos esto, Python inferirá el tipo de datos según lo que le pasamos, pero esto no siempre es lo óptimo.

import numpy as np

even = np.arange(0, 12, 2) # equivalente a even = np.array([0, 2, 4, 6, 8, 10])
print(even, even.dtype, f'{even.size * even.itemsize} bytes')

----------------------------

[ 0  2  4  6  8 10] int64 - 48 bytes

Se ha asignado automáticamente el tipo de datos int64 (enteros de 64 bits) para nuestro array cuyo objetivo era almacenar números pares comprendidos entre el 0 y el 10. Teniendo en cuenta el rango de int64 (-9223372036854775808 a 9223372036854775807) mirando la documentación no estamos aprovechando muy bien la memoria dejando que Python asigne el espacio por nosotros. Para este caso nos vale uint8 (0 a 255) y lo indicamos así:

import numpy as np

even = np.arange(0, 12, 2, dtype='uint8')

print(even, even.dtype, f'- {even.size * even.itemsize} bytes')

----------------------------

[ 0  2  4  6  8 10] uint8 - 6 bytes

Tan solo indicando el tipo de datos adecuado hemos conseguido reducir el tamaño del array de 48 a 6 bytes (un 87.5%). Aunque trivial para estos ejemplos, podemos imaginar lo que ocurre conforme escalamos las necesidades. Por supuesto, ocupa también bastante menos que su equivalente en lista:

even_list = [0, 2, 4, 6, 8, 10]
even8 = np.array([0, 2, 4, 6, 8, 10], dtype='uint8')
even64 = np.array([0, 2, 4, 6, 8, 10])

print(even_list, type(even_list), f'- {sys.getsizeof(even_list)} bytes | 100%')
print(even64, even64.dtype, f'- {even64.size * even64.itemsize} bytes | {round(even64.size * even64.itemsize /sys.getsizeof(even_list) * 100, 0)}%')
print(even8, even8.dtype, f'- {even8.size * even8.itemsize} bytes | {round(even8.size * even8.itemsize / sysgetsizeof(even_list) * 100, 0)}%')

----------------------------

[0, 2, 4, 6, 8, 10] <class 'list'> - 112 bytes | 100%
[ 0  2  4  6  8 10] int64 - 48 bytes | 43.0%
[ 0  2  4  6  8 10] uint8 - 6 bytes | 5.0%

En conclusión, un NumPy array de uint8 es capaz de almacenar la misma información que una lista en un 5% de la memoria (20 veces menos).

Para apreciar la mejora en rendimiento usaremos otros ejemplos con arrays de mayor tamaño n = 100 000. El siguiente script mide el tiempo que tarda una suma de vectores (n = 100 000) elemento a elemento.

import numpy as np
import sys
import time

size_of_vec = 100000


def python_lists():
    t1 = time.time()
    X = range(size_of_vec)
    Y = range(size_of_vec)
    Z = [X[i] + Y[i] for i in range(len(X))]
    return time.time() - t1


def python_numpy_array():
    t1 = time.time()
    X = np.arange(size_of_vec)
    Y = np.arange(size_of_vec)
    Z = X + Y
    return time.time() - t1


t1 = python_lists()
t2 = python_numpy_array()

print(f'Python lists: {round(t1, 6)}  seconds\nNumPy arrays: {round(t2, 6)}  seconds')
print('===================================')
print("NumPy is in this example " + str(round(t1/t2, 2)) + " times faster")

----------------------------

Python lists: 0.045252  seconds
NumPy arrays: 0.003807  seconds
===================================
NumPy is in this example 11.89 times faster

En el siguiente post: broadcast, n-dimensiones, etc.


Software Developer based in Madrid. This is my personal blog. I am also on Twitter and LinkedIn.