Computação numérica com NumPy
A computação numérica faz parte da ciência da computação e se concentra no desenvolvimento e implementação de algoritmos. O Python conta com várias bibliotecas que podem ser usadas para essas finalidades.
Sem dúvidas, uma das principais é o NumPy. Neste post, faremos uma introdução prática à computação numérica com NumPy explorando algumas de suas funcionalidades.
O que é NumPy?
NumPy é a abreviação de Numeric Python. Ele é uma biblioteca extremamente popular de código aberto para computação numérica e científica em Python. O NumPy é altamente utilizado em aplicações de inteligência artificial, redes neurais e machine learning.
Com NumPy, é possível criar e manipular matrizes de dados com eficiência. Ele fornece suporte para matrizes e arrays grandes e multidimensionais, juntamente com uma coleção de funções matemáticas de alto nível para operá-los.
Para entender como isso é feito na prática, vamos começar com o mais básico: a instalação. O comando para a instalação com pip é:
pip install numpy
Já para fazer a instalação através do Anaconda:
conda install -c anaconda numpy
Agora, vamos importar o NumPy num arquivo ou terminal do Python para conhecer algumas de suas funcionalidades.
import numpy as np
Criando NumPy arrays
Um dos principais elementos do NumPy é o NumPy array. Esses arrays são semelhantes às listas do Python, mas vêm com uma série de recursos e capacidades adicionais. Pense num array como uma super estrutura de dados que permite a realização de cálculos em larga escala, mas sem o uso de for loops, o que torna sua computação altamente eficiente.
Veja como criar um NumPy array simples a partir de uma lista:
minha_lista = [1, 2, 3, 4, 5]
numpy_array = np.array(minha_lista)
print(numpy_array)
Esse é o resultado:
[1 2 3 4 5]
Os arrays também podem ser criados diretamente como mostrado abaixo:
numpy_array = np.array(([1, 2, 3, 4], [5, 6, 7, 8]))
print(numpy_array)
Esse é o resultado:
[[1 2 3 4]
[5 6 7 8]]
Tipos de NumPy Arrays
O NumPy possui vários tipos de arrays. Os mais importantes são:
Arrays unidimensionais
Arrays 1D são parecidos com listas. Eles são arrays lineares organizados em uma única linha.
numpy_array = np.array([1, 2, 3, 4])
Arrays bidimensionais
São parecidos com planilhas.
numpy_array = np.array(([1, 2, 3, 4], [5, 6, 7, 8]))
Arrays multidimensionais
Também é possível criar arrays com mais de duas dimensões. Arrays desse tipo são frequentes em implementações de algoritmos de inteligência artificial, especialmente implementaçẽs de redes neurais.
numpy_array = np.array(([[1, 2, 3, 4], [5, 6, 7, 8]], [[9, 10, 11, 12], [13, 14, 15, 16]]))
Tipos de dados NumPy
Os arrays do NumPy são homogêneos, o que significa que todos os elementos de um array têm o mesmo tipo de dados. A biblioteca NumPy fornece uma variedade de tipos de dados, como inteiros, números de ponto flutuante (float) e muito mais. É possível especificar o tipo de dados ao criar um array. Por exemplo:
int_array = np.array([1, 2, 3, 4, 5], dtype=np.int64)
float_array = np.array([1.0, 2.5, 3.7, 4.2, 5.9], dtype=np.float64)
Arrays estruturados
Arrays estruturados permitem definir tipos de dados personalizados para seus elementos.
# Define uma estrutura de dado personalizada
custom_type = np.dtype([('nome', 'S10'), ('idade', int), ('altura', float)])
# Cria array estruturado
pessoas = np.array([('Maria', 25, 1.63), ('Ana', 30, 1.72)], dtype=custom_type)
Diversas formas de inicialização
O NumPy possui várias formas de inicialização de arrays. Além do np.array() mostrado acima, arrays podem ser inicializados das seguintes maneiras:
- np.zeros(): inicia arrays em que todos os elementos são zeros.
- np.ones(): inicia arrays em que todos os elementos são 1.
- np.empty(): cria arrays sem inicializar nenhum valor.
- np.arange(): gera um array com espaçamentos regulares em um intervalo específico.
- np.linspace(): gera um array com espaçamentos uniformes em um intervalo específico.
- np.logspace(): similar a linspace, mas com valores em escala logarítmica.
- np.eye(): matriz identidade bidimensional com os elementos diagonais iguais a 1 e os demais iguais a zero.
- np.full(): inicializa um array com um valor constate específico.
- np.random.rand() e np.random.randn(): geram arrays com valores randômicos entre 0 e 1 com distribuições padrão ou normal.
- np.random.randint(): cria arrays com valores randômicos inteiros.
Alguns exemplos dessas inicializações são mostrados abaixo.
arr_zeros = np.zeros([3, 2])
arr_full = np.full([2, 2], 7)
arr_arange = np.arange(0, 5, 0.5)
arr_eye = np.eye(3)
arr_rand = np.random.randn(3, 5)
print('zeros', arr_zeros)
print('full', arr_full)
print('arange', arr_arange)
print('eye',arr_eye)
print('randn', arr_rand)
E esses são seus resultados.
zeros [[0. 0.]
[0. 0.]
[0. 0.]]
full [[7 7]
[7 7]]
arange [0. 0.5 1. 1.5 2. 2.5 3. 3.5 4. 4.5]
eye [[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
randn [[ 0.32742762 -0.65405963 -0.77542463 -2.29181277 -1.44596175]
[ 0.46479333 2.98837389 -0.23254603 0.60781843 -1.45877576]
[-0.64288494 -0.16920776 -0.44833929 -1.84856823 0.91530668]]
Arrays mascarados
Em muitas situações, os arrays possuem dados ausentes ou inválidos. Arrays mascarados permitem lidar facilmente com essas situações atuando como uma forma de borracha sobre esses dados.
import numpy.ma as ma
dado = np.array([1, 2, -1, 4, 5])
dado_mascarado = ma.masked_array(dado, mask=[0, 0, 1, 0, 0])
print(dado_mascarado)
O resultado é mostrado abaixo.
[1 2 -- 4 5]
Tratamento de dados ausentes
No mundo real, os dados raramente são perfeitos. Valores ausentes são comuns. Para lidar com isso, o NumPy possui o np.nan (not-a-number). Ele atua como um espaço reservado para dados ausentes ou inválidos.
dados = np.array([1, 2, np.nan, 4, 5])
Formas e reformas de arrays
Frequentemente é crucial entender o formato de um array e como remodelá-lo, se necessário.
Cada NumPy array tem um formato que indica quantos elementos existem em cada uma de suas dimensões. É possível verificar a forma de um array usando o atributo shape. Ele retorna uma tupla que indica o tamanho de cada dimensão do array.
arr = np.array([[1, 2, 3], [4, 5, 6]])
print(arr.shape)
Esse é o resultado:
(2, 3)
Para alterar a forma de um array, o atributo é o .reshape().
arr = np.array([1, 2, 3, 4, 5, 6])
reshaped = arr.reshape(2, 3)
print(reshaped)
Resultado:
[[1 2 3]
[4 5 6]]
Fatiamento e indexação de arrays
Arrays NumPy podem ser divididos e indexados como listas do Python, mas com mais flexibilidade. O NumPy aceita indexação inteira, indexação booleana e indexação sofisticada.
Indexação inteira:
arr = np.array([10, 20, 30, 40, 50])
print(arr[1])
O resultado é mostrado abaixo. Note que, assim como listas em Python, o primeiro índice é 0:
20
Indexação booleana:
arr = np.array([10, 20, 30, 40, 50])
mask = arr > 20
print(arr[mask])
Esse é o resultado:
[30 40 50]
Indexação sofisticada (fancy indexing):
arr = np.array([10, 20, 30, 40, 50])
indices = [1, 3]
Resultado:
[20 40]
Funções universais
A biblioteca NumPy oferece uma ampla gama de funções universais (ufuncs) para realizar operações elemento a elemento em arrays. Essas funções são o coração dos cálculos numéricos no NumPy e fornecem operações elemento a elemento rápidas e eficientes. No código abaixo, mostramos um exemplo de uso de ufunc para calcular a raiz quadrada de um array:
arr = np.array([1, 4, 9, 68, 225])
sqrt_arr = np.sqrt(arr)
print(sqrt_arr)
Resultado:
[ 1. 2. 3. 8.24621125 15. ]
Bradcasting
O NumPy permite a realização de operações entre arrays de diferentes formatos e tamanhos, um recurso conhecido como broadcasting. O broadcasting é uma ferramenta extremamente poderosa para simplificar a implementação de códigos e é frequentemente usado em códigos de redes neurais.
Por exemplo, para adicionar um escalar a um array, você pode fazer assim:
arr = np.array([1, 2, 3, 4, 5])
broad_arr = arr + 10
print(broad_arr)
O resultado é mostrado abaixo. Veja que o NumPy propaga automaticamente o valor escalar 10 para ser simétrico à forma do array sem precisar usar um for loop.
[11 12 13 14 15]
Já para somar dois arrays, você pode fazer assim:
arr = np.array([1, 2, 3, 4, 5])
broad_arr = arr + np.array(([10], [20]))
print(broad_arr)
E o resultado, novamente, indica que os valores são propagados para tornar os dois arrays simétricos para o cálculo.
[[11 12 13 14 15]
[21 22 23 24 25]]
Operações matemáticas
A biblioteca NumPy possui uma ampla gama de operações matemáticas. Aqui estão alguns exemplos principais:
Operações elementares (element-wise operations)
Saõ operações aritméticas básicas em NumPy arrays aplicadas elemento a elemento.
arr1 = np.array([1, 2, 3, 4, 5])
arr2 = np.array([6, 7, 8, 9, 10])
# soma
adicao = arr1 + arr2
# multiplicação
produto = arr1 * arr2
print(adicao, produto)
Resultado:
[ 7 9 11 13 15] [ 6 14 24 36 50]
Operações matriciais
O NumPy é muito poderoso na realização de operações matriciais. Por exemplo, é possível calcular o produto escalar de duas matrizes (dot product) usando o atributo np.dot() ou o operador @:
matriz1 = np.array([[1, 2], [3, 4]])
matriz2 = np.array([[5, 6], [7, 8]])
dot_product = np.dot(matriz1, matriz2)
dot_product = matriz1 @ matriz2
print(dot_product)
Resultado:
[[19 22]
[43 50]]
Operações estatísticas
O NumPy possibilita facilmente a realização de inúmeras operações estatísticas como cálculo de média (np.mean()), desvio padrão (np.std()), entre outras:
matriz1 = np.array([[1, 2], [3, 4]])
print(np.mean(matriz1))
Resultado:
2.5
Conclusões
Nesta introdução prática ao NumPy, tocamos em algumas das funcionalidades que essa biblioteca possui. O NumPy simplifica bastante o trabalho com arrays, oferece uma variedade de operações matemáticas e é capaz de se integrar perfeitamente com outras bibliotecas como Matplotlib e PyTorch.
Neste post, abordamos apenas o essencial, mas o NumPy tem muito mais a oferecer. Ele tem suporte para números complexos, transformadas de Fourier, álgebra linear e muito mais. Os recursos do NumPy são extensos e altamente utilizados em tarefas de computação científica e numérica.