Curso Gratuito en Ciencia de Datos y Aprendizaje Automático con Python

Numpy
Como mencionamos anteriormente, para usar una biblioteca científica compilada, la memoria asignada en el intérprete de Python debe llegar de alguna manera a esta biblioteca como entrada. Además, la salida de estas bibliotecas también debe regresar al intérprete de Python. Este intercambio bidireccional de memoria es esencialmente la función principal del módulo Numpy (matrices numéricas en Python). Numpy es el estándar de facto para matrices numéricas en Python.Surgió como un esfuerzo de Travis Oliphant y otros para unificar las matrices numéricas preexistentes en Python. En esta sección, proporcionamos una descripción general y algunos consejos para usar Numpy de manera efectiva, pero para muchos más detalles, el libro de Travis disponible gratuitamente [1] es un excelente lugar para comenzar.
Numpy proporciona especi fi cación de matrices de tamaño de bytes en Python. Por ejemplo, a continuación creamos una matriz de tres números, cada uno de 4 bytes de longitud (32 bits a 8 bits por byte) como se muestra en la propiedad itemsize. La primera línea importa Numpy como np, que es la convención recomendada. La siguiente línea crea una matriz de números de coma flotante de 32 bits. La propiedad itemize muestra el número de bytes por elemento.
code
Además de proporcionar contenedores uniformes para números, Numpy proporciona un conjunto completo de funciones universales (es decir, unfuncs) que procesan matrices por elementos sin semántica de bucle adicional. A continuación, mostramos cómo calcular el seno por elementos usando Numpy:
code
Esto calcula el seno de la matriz de entrada [1 , 2 , 3 ] , usando la función unaria de Numpy , np.sin . Hay otra función sinusoidal en el módulo matemático incorporado , pero la versión de Numpy es más rápida porque no requiere un bucle explícito (es decir, usar un bucle for ) sobre cada uno de los elementos de la matriz. Ese bucle ocurre en la propia función np.sin compilada . De lo contrario, tendríamos que hacer bucles explícitamente como en el siguiente:
Numpy usa reglas de conversión de sentido común para resolver los tipos de salida. Por ejemplo, si las entradas hubieran sido de tipo entero, la salida aún habría sido de tipo punto flotante. En este ejemplo, proporcionamos una matriz Numpy como entrada a la función seno. También podríamos haber usado una lista simple de Python en su lugar y Numpy habría construido la matriz intermedia de Numpy (por ejemplo, np.sin ([1,1,1]) ). La documentación de Numpy proporciona una lista completa (y muy larga) de ufuncs disponibles .
Los arreglos numerosos vienen en muchas dimensiones. Por ejemplo, a continuación se muestra una matriz de 2x3 bidimensional construida a partir de dos listas de Python conformes.
code
Tenga en cuenta que Numpy está limitado a 32 dimensiones a menos que lo construya para más. 2 arrays NumPy siguen las reglas habituales de rebanado Python en múltiples dimensiones como se muestra a continuación, donde la : de colon de caracteres selecciona todos los elementos a lo largo de un eje particular.
code
También puede seleccionar subsecciones de matrices utilizando el corte como se muestra a continuación
code
Numpy matrices y memoria
Algunos lenguajes interpretados asignan memoria implícitamente. Por ejemplo, en MATLAB, puede extender una matriz simplemente agregando otra dimensión como en la siguiente sesión de MATLAB:
code
Esto funciona porque las matrices de MATLAB utilizan semántica de paso por valor para que las operaciones de corte copien partes de la matriz según sea necesario. Por el contrario, Numpy usa semántica de paso por referencia para que las operaciones de corte sean vistas en la matriz sin copia implícita. Esto es particularmente útil con arreglos grandes que ya agotan la memoria disponible. En la terminología Numpy, rebanar crea puntos de vista (sin copiar) y la indexación avanzada crea copias. Comencemos con la indexación avanzada.
Si el objeto de indexación (es decir, el elemento entre paréntesis) es un objeto de secuencia que no es tupla , otra matriz Numpy (de tipo entero o booleano) o una tupla con al menos un objeto de secuencia o matriz Numpy, entonces la indexación crea copias. Para el ejemplo anterior, para lograr la misma extensión de matriz en Numpy, debe hacer algo como lo siguiente:
code
Debido a la indexación avanzada, la variable y tiene su propia memoria porque se copiaron las partes relevantes de x . Para demostrarlo, asignamos un nuevo elemento ax y vemos que y no está actualizado.
code
Sin embargo, si comenzamos una y construcción y por rebanado (que lo convierte en un punto de vista) como se muestra a continuación, entonces el cambio que hicimos hace afectar Y debido a que una vista es una ventana a la misma memoria.
code
Tenga en cuenta que si desea forzar explícitamente una copia sin ningún truco de indexación, puede hacer y = x.copy () . El siguiente código funciona a través de otro ejemplo de indexación avanzada versus segmentación.
code
En este ejemplo, y es una copia, no una vista, porque se creó mediante indexación avanzada, mientras que z se creó mediante división. Por lo tanto, a pesar de que y y z tienen las mismas entradas, solamente z se ve afectada por cambios en x . Tenga en cuenta que la propiedad flags de las matrices Numpy puede ayudar a resolver esto hasta que se acostumbre.
La manipulación de la memoria mediante vistas es particularmente poderosa para los algoritmos de procesamiento de señales e imágenes que requieren la superposición de fragmentos de memoria. El siguiente es un ejemplo de cómo usar Numpy avanzado para crear bloques superpuestos que en realidad no consumen memoria adicional.
code
El código anterior crea un rango de números enteros y luego se superpone a las entradas para crear una matriz Numpy de 7x4 . El argumento final en la función as_strided son los pasos, que son los pasos en bytes para moverse en las dimensiones de fila y columna, respectivamente. Por lo tanto, la matriz resultante escalona ocho bytes en la dimensión de la columna y dieciséis bytes en la dimensión de la fila. Debido a que los elementos enteros en la matriz Numpy tienen ocho bytes, esto equivale a moverse por un elemento en la dimensión de la columna y por dos elementos en la dimensión de la fila. La segunda fila en la matriz Numpy comienza en dieciséis bytes (dos elementos) desde la primera entrada (es decir, 2 ) y luego avanza en ocho bytes (por un elemento) en la dimensión de la columna (es decir, 2,3,4,5). La parte importante es que la memoria se reutiliza en la matriz Numpy de 7x4 resultante . El siguiente código demuestra esto reasignando elementos en la matriz x original . Los cambios aparecen en la matriz y porque apuntan a la misma memoria asignada.
code
Tenga en cuenta que as_strided no comprueba que se mantenga dentro de los límites del bloque de memoria. Entonces, si el tamaño de la matriz de destino no se llena con los datos disponibles, los elementos restantes provendrán de los bytes que estén en esa ubicación de memoria. En otras palabras, no existe un relleno predeterminado con ceros u otra estrategia que defienda los límites de los bloques de memoria. Una defensa es controlar explícitamente las dimensiones como en el siguiente código:
code
Matrices Numpy
Las matrices en Numpy son similares a las matrices Numpy pero solo pueden tener dos dimensiones. Implementan la multiplicación de matrices de filas y columnas en lugar de la multiplicación de elementos. Si tiene dos matrices que desea multiplicar, puede crearlas directamente o convertirlas a partir de matrices Numpy. Por ejemplo, a continuación se muestra cómo crear dos matrices y multiplicarlas.
code
Esto también se puede hacer usando matrices como se muestra a continuación
code
Los arreglos numerosos admiten la multiplicación por elementos , no la multiplicación por filas y columnas . Debe usar matrices Numpy para este tipo de multiplicación a menos que use el producto interno np.dot , que también funciona en múltiples dimensiones (consulte np.tensordot para obtener productos punto más generales). Tenga en cuenta que Python 3.x tiene una nueva notación @ para la multiplicación de matrices, por lo que podemos volver a hacer el último cálculo de la siguiente manera:
code
No es necesario convertir todos los multiplicandos en matrices para la multiplicación. En el siguiente ejemplo, todo hasta la última línea es una matriz Numpy y, a partir de entonces, convertimos la matriz como una matriz con np.matrix que luego usa la multiplicación de filas y columnas . Tenga en cuenta que no es necesario convertir la variable x como una matriz porque el orden de izquierda a derecha de la evaluación se encarga de eso automáticamente. Si necesitamos usar A como matriz en otra parte del código, entonces deberíamos vincularla a otra variable en lugar de volver a convertirla cada vez. Si te encuentras pasando de un lado a otro para matrices grandes, pasando la copia = Bandera falsa a la matrizevita el gasto de hacer una copia.
code
Difusión Numpy
La difusión numpy es una forma poderosa de crear cuadrículas multidimensionales implícitas para expresiones. Probablemente sea la característica más poderosa de Numpy y la más difícil de comprender. Siguiendo con el ejemplo, considere los vértices de un cuadrado unitario bidimensional como se muestra a continuación
code
Meshgrid de Numpy crea cuadrículas bidimensionales . Las matrices X e Y tienen entradas correspondientes que coinciden con las coordenadas de los vértices del cuadrado unitario (por ejemplo, ( 0 , 0 ), ( 0 , 1 ), ( 1 , 0 ), ( 1 , 1 ) ). Para sumar las coordenadas xey, podríamos usar X e Y como en X + Y que se muestra a continuación. La salida es la suma de las coordenadas del vértice del cuadrado unitario.
code
Debido a que las dos matrices tienen formas compatibles, se pueden sumar por elementos. Resulta que podemos omitir un paso aquí y no molestarnos con meshgrid para obtener implícitamente las coordenadas del vértice utilizando la transmisión como se muestra a continuación.
code
En la línea 7, el singleton None Python le dice a Numpy que haga copias de y a lo largo de esta dimensión para crear un cálculo conforme. Tenga en cuenta que np.newaxis se puede usar en lugar de None para ser más explícito. Las siguientes líneas muestran que obtenemos el mismo resultado que cuando usamos las matrices X + Y Numpy. Tenga en cuenta que sin transmitir x + y = matriz ([0, 2]) que no es lo que estamos tratando de calcular. Continuemos con un ejemplo más complicado en el que tenemos diferentes formas de matriz.
code
En este ejemplo, las formas de matriz son diferentes, por lo que la adición de x y y no es posible sin Numpy radiodifusión. La última línea muestra que la transmisión genera la misma salida que usar la matriz compatible generada por meshgrid . Esto muestra que la transmisión funciona con diferentes formas de matriz. En aras de la comparación, en la línea 3, meshgrid crea dos matrices conformable, X y Y . En la última línea, x + y [:, None] produce la misma salida que X + Y sin la cuadrícula . También podemos poner el Ninguno dimensión en la x matriz como x [:, Ninguno] + yque daría la transposición del resultado.
La radiodifusión también funciona en múltiples dimensiones. La salida mostrada tiene forma (4,3,2) . En la última línea, x + y [:, None] produce una matriz bidimensional que luego se transmite contra z [:, None, None] , que se duplica a lo largo de las dos dimensiones agregadas para acomodar el resultado bidimensional en su izquierda (es decir, x + y [:, Ninguno] ). La advertencia sobre la transmisión es que potencialmente puede crear arreglos intermedios grandes que consumen memoria . Existen métodos para controlar esto reutilizando la memoria asignada previamente, pero eso está más allá de nuestro alcance aquí. Las fórmulas en física que evalúan funciones en los vértices de cuadrículas de alta dimensión son excelentes casos de uso para la transmisión.
code
Matrices enmascaradas numerosas
Numpy proporciona un método poderoso para ocultar temporalmente elementos de la matriz sin cambiar la forma de la matriz en sí,
code
Tenga en cuenta que los elementos de la matriz para los que la condición lógica ( x < 5 ) es verdadera están enmascarados, pero el tamaño de la matriz sigue siendo el mismo. Esto es particularmente útil para trazar datos categóricos, donde es posible que solo desee aquellos valores que corresponden a una categoría determinada para una parte del gráfico. Otro uso común es el procesamiento de imágenes, en el que es posible que sea necesario excluir partes de la imagen del procesamiento posterior. Tenga en cuenta que la creación de una matriz enmascarada no fuerza una operación de copia implícita a menos que se utilice el argumento copy = True . Por ejemplo, el cambio de un elemento en x no cambiar el elemento correspondiente en y , a pesar de que Y es una matriz de enmascarado,
code
Números de coma flotante
Existen limitaciones de precisión al representar números de coma flotante en una computadora con memoria finita. Por ejemplo, a continuación se muestran estas limitaciones al sumar dos números simples,
code
Entonces, ¿por qué la salida no es 0.3 ? El problema es la representación de punto flotante de los dos números y el algoritmo que los suma. Para representar un número entero en binario, simplemente lo escribimos en potencias de 2 . Por ejemplo, 230 = ( 11100110 ) 2 . Python puede hacer esta conversión usando formato de cadena,
code
Para sumar enteros, simplemente sumamos los bits correspondientes y los ajustamos al número permitido de bits. A menos que haya un desbordamiento (los resultados no se pueden representar con ese número de bits), entonces no hay problema. Representar un punto flotante es más complicado porque tenemos que representar estos números como fracciones binarias. El estándar IEEE 754 requiere que los números de coma flotante se representen como ± C × 2 E, donde C es el significado ( mantisa ) y E es el exponente.
Para representar una fracción decimal regular como fracción binaria, tenemos que calcular la expansión de la fracción de la siguiente forma un 1 / 2 + un 2 / 2 2 + un 3 / 2 3 ... En otras palabras, tenemos que fi nd la una i coe fi cientes. Podemos hacer esto utilizando el mismo proceso que se uso para una fracción decimal: sólo seguir dividiendo por las potencias fraccionarias de 1 / 2 y mantener un registro de las partes enteras y fraccionarias. La función divmod de Python puede hacer la mayor parte del trabajo para esto. Por ejemplo, para representar 0.125 como una fracción binaria,
code
El primer elemento de la tupla es el cociente y el otro es el resto. Si el cociente fue mayor que 1 , entonces el término correspondiente a i es uno y es cero en caso contrario. Para este ejemplo, tenemos un 1 = 0. Para obtener el siguiente término en la expansión, sólo seguimos multiplicando por 2 la que nos mueve hacia la derecha a lo largo de la expansión a un i + 1 y así sucesivamente. Luego,
code
El algoritmo se detiene cuando el término restante es cero. Entonces, tenemos ese 0 . 125 = ( 0 . 001 ) 2 . La especificación requiere que el término principal en la expansión sea uno. Por tanto, tenemos 0 . 125 = ( 1 . 000 ) × 2 - 3 . Esto significa que el significado es 1 y el exponente es -3 . Ahora, volvamos a nuestro problema principal 0.1 + 0.2 desarrollando la representación 0.1 codificando los pasos individuales anteriores.
code
Tenga en cuenta que la representación tiene un patrón que se repite infinitamente. Esto significa que tenemos ( 1 . 1001 ) 2 × 2 - 4 . El estándar IEEE no tiene una forma de representar secuencias que se repiten infinitamente. No obstante, podemos calcular esto, Por tanto, 0 . 1 ≈ 1 . 6 × 2 - 4 . Según el estándar IEEE 754, para el tipo flotante , tenemos 24 bits para el significado y 23 bits para la parte fraccionaria. Como no podemos representar la secuencia que se repite infinitamente, tenemos que redondear en 23 bits, 10011001100110011001101 . Así, mientras que la representación del significado solía ser 1.6 , con este redondeo, ahora es
code
Por tanto, ahora tenemos 0 . 1 ≈ 1 . 600000023841858 × 2 - 4 = 0 . 10000000149011612. Para la expansión 0.2 , tenemos la misma secuencia repetida con un exponente diferente, de modo que tenemos 0 . 2 ≈ 1 . 600000023841858 × 2 - 3 = 0 . 20000000298023224. Para sumar 0.1 + 0.2 en binario, debemos ajustar los exponentes hasta que coincidan con el mayor de los dos. Así, 0.11001100110011001100110 +1.10011001100110011001101 -------------------------- 10.01100110011001100110011 Ahora, la suma debe reducirse para que encaje en los bits disponibles del significante, por lo que el resultado es 1.00110011001100110011010 con exponente -2 . Calcular esto de la forma habitual como se muestra a continuación da el resultado.
code
que coincide con lo que obtenemos con numpy
code
Todo el proceso procede del mismo modo para fl oats de 64 bits . Python tiene fracciones y módulos decimales que permiten representaciones numéricas más exactas. El módulo decimal es particularmente importante para ciertos cálculos financieros. Error de redondeo . Consideremos el ejemplo de sumar 100.000.000 y 10 encoma flotante de 32 bits .
code
Esto significa que 100 , 000 , 000 = ( 1 . 01111101011110000100000000 ) 2 × 2 26 . Como- sabia, 10 = ( 1 . 010 ) 2 × 2 3 . Para sumar estos tenemos que hacer coincidir los exponentes como se muestra a continuación, 1.01111101011110000100000000 +0,00000000000000000000001010 ------------------------------- 1.01111101011110000100001010 Ahora, tenemos que redondear porque solo tenemos 23 bits a la derecha del punto decimal y obtenemos 1.0111110101111000010000 , perdiendo así los 10 bits finales . Esto efectivamente hace que el decimal 10 = ( 1010 ) 2 con el que comenzamos se convierta en 8 = ( 1000 ) 2 . Por lo tanto, usando Numpy nuevamente,
code
El problema aquí es que el orden de magnitud entre los dos números era tan grande que resultó en una pérdida en los bits de significación cuando el número más pequeño se desplazó hacia la derecha. Al sumar números como estos, el algoritmo de suma de Kahan (consulte math.fsum () ) puede gestionar eficazmente estos errores de redondeo.
code
Error de cancelación . El error de cancelación (pérdida de significación) se produce cuando se restan dos números de coma flotante casi iguales . Consideremos restar 0.1111112 y 0.1111111 . Como fracciones binarias, tenemos lo siguiente, 1.11000111000111001000101 E -4 -1.11000111000111000110111 E -4 --------------------------- 0.00000000000000000011100 Como una fracción binaria, esto es 1,11 con exponente -23 o ( 1 . 75 ) 10 × 2 - 23 ≈ 0 . 00000010430812836. En Numpy, esta pérdida de precisión se muestra a continuación:
code
En resumen, al usar el punto flotante, debe verificar la igualdad aproximada usando algo como Numpy allclose en lugar del signo de igualdad de Python habitual (es decir, == ). Esto impone límites de error en lugar de una igualdad estricta. Siempre que sea posible, utilice una escala fija para emplear valores enteros en lugar de fracciones decimales. Los números de coma flotante de 64 bits de doble precisión son mucho mejores que los de precisión simple y, aunque no eliminan estos problemas, patean eficazmente el camino para todos los requisitos de precisión, excepto los más estrictos. El algoritmo de Kahan es eficaz para sumar números de punto flotante en datos muy grandes sin acumular errores de redondeo. Para minimizar los errores de cancelación, vuelva a factorizar el cálculo para evitar restar dos números casi iguales.
Prospecto y optimizaciones de Numpy
La comunidad científica de Python continúa ampliando las fronteras de la informática científica. Varias extensiones importantes de Numpy están en desarrollo activo. Primero, Numba es un compilador que genera código de máquina optimizado a partir de código Python puro utilizando la infraestructura del compilador LLVM. LLVM comenzó como un proyecto de investigación en la Universidad de Illinois para proporcionar una estrategia de compilación independiente del objetivo para lenguajes de programación arbitrarios y ahora es una tecnología bien establecida . La combinación de LLVM y Python a través de Numba significa que acelerar un bloque de código Python puede ser tan fácil como poner un @ numba.jitdecorador sobre la de fi nición de la función, pero esto no funciona para todas las situaciones. Numba también puede apuntar a unidades de procesamiento de gráficos generales (GPGPU).
El proyecto Dask contiene extensiones dask.array para manipular conjuntos de datos muy grandes que son demasiado grandes para caber en la RAM de una sola computadora (es decir, fuera del núcleo) usando semántica Numpy. Además, dask incluye extensiones para los marcos de datos de Pandas (consulte la Sección 1.7 ). En términos generales, esto significa que dask comprende cómo descomprimir las expresiones de Python y traducirlas para una variedad de servicios de datos de backend distribuidos sobre los que se realiza la computación. Esto significa que dask separa la expresión del cálculo de la implementación particular en un backend determinado.
[1] Es un libro interactivo que usa el estilo de un informe de investigación reproducible que permite a los lectores no solo reproducir los resultados de los estudios realizados en \textit{R}, sino que también fortalece la capacidad para utilizar las habilidades recién adquiridas en otras aplicaciones empíricas.