- O autor explica, com vários exemplos, os problemas do NumPy
- Operações simples com arrays são fáceis com NumPy, mas, à medida que as dimensões aumentam, a complexidade e a confusão crescem rapidamente
- O design do NumPy, incluindo broadcasting e indexação avançada, carece de clareza e abstração
- Em vez de especificar eixos explicitamente, torna-se necessário escrever código baseado em adivinhação e tentativa e erro
- O autor apresenta ideias para uma linguagem de arrays melhorada e pretende mostrar alternativas concretas no próximo texto
Introdução: amor e ódio pelo NumPy
- O autor afirma que usa NumPy há muito tempo, mas que ficou muito decepcionado com suas limitações
- NumPy é uma biblioteca essencial e influente para operações com arrays em Python
- Bibliotecas modernas de machine learning, como PyTorch, também têm problemas semelhantes aos do NumPy
O que é fácil e o que é difícil no NumPy
- Operações simples, como resolver equações lineares básicas, podem ser feitas com uma sintaxe clara e elegante
- Porém, quando a dimensionalidade dos arrays aumenta ou as operações ficam mais complexas, torna-se necessário processar tudo em lote sem usar loops
for
- Em ambientes onde não se pode usar loops (como operações em GPU), são necessárias sintaxes de vetorização incomuns ou formas especiais de chamar funções
- No entanto, o modo exato de usar essas funções é ambíguo e difícil de entender claramente apenas pela documentação
- Na prática, é difícil para qualquer pessoa ter certeza de como usar corretamente a função
linalg.solve do NumPy no caso de arrays de alta dimensão
Problemas do NumPy
- O NumPy carece de uma teoria consistente para aplicar operações a partes de arrays multidimensionais ou a eixos específicos
- Quando a dimensionalidade do array é 2 ou menos, tudo é claro; mas, a partir de 3 dimensões ou mais, fica incerto quais eixos de cada array são o alvo da operação
- Para alinhar dimensões explicitamente, ele força o uso de métodos complexos como None, broadcasting e
np.tensordot
- Isso aumenta a chance de erro, reduz a legibilidade do código e eleva a possibilidade de bugs
Loops e clareza
- Na prática, se loops fossem permitidos, seria possível escrever código muito mais conciso e claro
- Código com loops pode parecer menos sofisticado, mas tem uma grande vantagem em termos de clareza
- Em contrapartida, quando a dimensionalidade do array muda, é preciso pensar manualmente em transpose ou na ordem dos eixos, o que aumenta a complexidade
np.einsum: uma função excepcionalmente boa
np.einsum é poderosa por oferecer uma linguagem específica de domínio flexível que permite nomear os eixos
- O einsum deixa clara a intenção da operação e generaliza muito bem, permitindo implementar explicitamente operações complexas sobre eixos
- No entanto, esse tipo de suporte a operações no estilo do einsum é limitado a algumas operações e, por exemplo, não pode ser usado com
linalg.solve
Problemas do broadcasting
- O broadcasting, principal truque do NumPy, é um recurso que ajusta automaticamente as dimensões quando elas não coincidem
- Em casos simples, ele é conveniente, mas na prática torna mais difícil entender claramente quais são as dimensões e gera muitos casos de erro
- Como o broadcasting é implícito, ao ler o código é preciso verificar toda vez como a operação realmente funciona
A falta de clareza na indexação
- A indexação avançada do NumPy torna muito difícil prever o shape de um array, além de ser pouco clara
- Como o shape do array resultante muda conforme a combinação de indexações, é difícil prever o resultado sem experiência prática
- A própria documentação que explica as regras de indexação é longa e complexa, exigindo muito tempo para ser assimilada
- Mesmo tentando usar apenas indexação simples, em certas operações acaba sendo inevitável recorrer à indexação avançada
Limites no design das funções do NumPy
- Muitas funções do NumPy são otimizadas apenas para shapes específicos de array
- Para arrays de alta dimensão, é necessário usar argumentos adicionais como axes, nomes de função separados ou convenções específicas, e não há consistência entre as funções
- Isso vai contra princípios básicos de programação, como abstração e reutilização
- Mesmo ao usar uma função para resolver um problema específico, para reaplicá-la a diferentes arrays e eixos é preciso reescrever o código praticamente do zero
Exemplo real: implementação de self-attention
- Ao escrever uma implementação de self-attention em NumPy, usar loops deixa o código claro, mas forçar a vetorização o torna complexo
- Quando são necessárias operações de alta dimensão, como em attention multi-head, é preciso combinar einsum e transformações de eixos, deixando o código difícil de entender
Conclusão e alternativa
- O autor afirma que o NumPy é "a única opção que se tornou tão importante no mercado, apesar de ser pior do que outras linguagens de arrays em muitos aspectos"
- Para superar vários problemas do NumPy (broadcasting, falta de clareza na indexação, inconsistência entre funções etc.), ele antecipa que criou um protótipo de uma linguagem de arrays melhorada
- O plano é apresentar as melhorias concretas (uma nova API de linguagem de arrays) em um texto separado no futuro
4 comentários
Parece a história de por que Julia surgiu. É preciso estudar as bibliotecas, mas parece uma opção realmente atraente por resolver muitos dos problemas do NumPy.
Se você não souber usar bem a vetorização do
numpy, o desempenho vai por água abaixo. Ter que escrever levando isso em consideração é estressante e difícil.Acho que muitas bibliotecas Python mais antigas têm um problema parecido.
Comentários no Hacker News
be a documentação, fica difícil de entender, mas como há uma explicação sobre oshaperetornado, é preciso verificar se o vetorbestá de fato em formato de matriz, especialmente no caso deK=1transpose, a maioria desses problemas desaparece, o Xarray é mais fraco que o NumPy no aspecto de álgebra linear, mas é fácil voltar para o NumPy e basta criar algumas funções auxiliares, com Xarray a produtividade aumenta bastante ao lidar com dados de 3 dimensões ou maisda.sel(x=some_x).isel(t=-1).mean(["y", "z"])são fáceis, o broadcasting também fica claro porque os nomes das dimensões são respeitados, e ele é forte no processamento de dados geoespaciais com vários CRSs, também funciona muito bem com Arviz, facilitando o tratamento de dimensões adicionais em análise bayesiana, além de permitir agrupar vários arrays em um único dataset compartilhando coordenadas em comum, então algo comods.isel(t=-1)funciona facilmente em todos os arrays com eixo temporalarray[:, :, None], então foi bom ver alguém com a mesma opiniãonp.linalg.solveé tratado como a forma mais rápida e, por isso, você teria que se adaptar a ela de qualquer jeito, também há vários motivos pelos quais escrever kernels especializados para o problema pode ser melhor'\'transpose/reshapeetc. não são consistentes, o que torna tudo ambíguovmapdo jaxsqueezeetc., a formulação do problema em si é tão ambígua que fica difícil até entenderreshapepoly1dPpela direita comz0, o resultado é umpoly1d, mas se fizer pela esquerda no formatoz0*P, ele retorna apenas um array e ocorre uma conversão de tipo silenciosa, o coeficiente líder de uma quadrática também pode ser acessado tanto porP.coef[0]quanto porP[2], o que facilita confusão, oficialmentepoly1dé uma API “antiga” e para código novo a recomendação é usar a classePolynomial, mas na prática não há nem aviso de deprecated, por causa dessas conversões de tipo e inconsistências de datatype, a biblioteca está cheia de minas terrestres e o debug vira um pesadeloto_numpy(), no fim gasta-se mais tempo convertendo formatos de dados do que resolvendo o problema, Julia também não tem só vantagens, mas a integração entre bibliotecas para unidades, incerteza etc. funciona bem, enquanto em Python sempre é preciso escrever muito boilerplatearray-apiestá tentando padronizar a API de manipulação de arrays em todo o ecossistema Pythonnumpyem vez desagenumpysaneegnuplotlib, desde que conheci essa combinação passei a usar NumPy ativamente para todo tipo de trabalho, sem isso seria praticamente impossível de usarnumpysaneno fim das contas ainda é loop em Python, então não é vetorização de verdadeeinsume usaroptimize="optimal"com o algoritmo de multiplicação em cadeia de matrizes para melhorar a performance, de fato ficou cerca de 2x mais rápido que uma implementação vetorizada comum, mas surpreendentemente uma implementação ingênua com loops ainda é mais rápida, quem quiser saber o motivo pode olhar o código, suspeito que ainda haja espaço para melhorar a coerência de cache dentro doeinsum