6.17. Resolvedores e números aleatórios

Quando a função em estudo é definida por código Python em vez de um buffer de amostras, uma família diferente de ferramentas é a escolha certa: onde está a raiz da função, o seu mínimo, o seu integral num dado intervalo? Os submódulos scipy.integrate e scipy.optimize cobrem esse trabalho. Cada algoritmo chama de volta a função Python fornecida pelo utilizador, pelo que o custo por iteração é superior ao de uma redução de buffer; a conveniência está em não ter de escrever o resolvedor.

O submódulo scipy.special cobre as funções especiais estatísticas (função de erro, gama) que surgem ao calcular a função de distribuição cumulativa (CDF, a probabilidade de uma amostra ser no máximo um dado valor) ou a função de densidade de probabilidade (PDF, a probabilidade relativa num dado valor) de uma distribuição. numpy.random cobre o gerador pseudo-aleatório para dithering, simulação e dados de teste sintéticos.

6.17.1. Integração numérica de um chamável

Quando o integrando é uma função Python em vez de um buffer de amostras, scipy.integrate expõe quatro algoritmos de quadratura:

  • quad() – Gauss-Kronrod adaptativo. A predefinição certa para integrandos suaves. Devolve (value, error).

  • romberg() – Romberg / Newton-Cotes clássico. Devolve um único float. Descontinuado a montante; incluído por compatibilidade.

  • simpson() – regra de Simpson adaptativa. Devolve um único float.

  • tanhsinh() – quadratura dupla-exponencial. Use quando o integrando tem singularidades nos extremos ou um limite infinito. Devolve (value, error).

O integral gaussiano avaliado com a regra dupla-exponencial (tanhsinh):

from math import exp
from math import pi
from math import sqrt
from ulab import numpy as np
from ulab import scipy as sp

f = lambda x: exp(-x * x)
value, err = sp.integrate.tanhsinh(f, -np.inf, np.inf)
print("approx:", value, "   exact:", sqrt(pi))

Saída:

approx: 1.7724538...   exact: 1.7724538...

6.17.2. Pesquisa de raízes e minimização

scipy.optimize cobre três resolvedores clássicos de uma variável. Cada iteração chama de volta a função Python fornecida pelo utilizador, pelo que a aceleração relativamente a um resolvedor puramente Python é modesta (cerca de 2x); a conveniência está em não ter de escrever o resolvedor.

  • bisect() – encontrar uma raiz de f em [a, b] por bissecção do intervalo. f(a) e f(b) devem ter sinais opostos:

    def f(x):
        return x * x - 1
    
    sp.optimize.bisect(f, 0, 4)        # ~1.0
    
  • newton() – encontrar uma raiz usando iteração secante / Newton-Raphson:

    def f(x):
        return x * x * x - 2.0
    
    sp.optimize.newton(f, 3., tol=0.001, rtol=0.01)
    # ~1.260
    
  • fmin() – encontrar um mínimo local usando o método simplex descendente (Nelder-Mead):

    def f(x):
        return (x - 1) ** 2 - 1
    
    sp.optimize.fmin(f, 3.0)           # ~1.0
    

O âmbito de uma variável é suficiente para a maioria das otimizações do lado da câmara – a constante de calibração de um sensor, o ganho que maximiza uma medição de contraste, o limiar onde a bimodalidade de um histograma é mais nítida. Para problemas de múltiplas variáveis, a resposta certa é geralmente reformular o problema como uma resolução de álgebra linear de pequena dimensão, em vez de recorrer a um otimizador não linear genérico.

6.17.3. Funções especiais

scipy.special expõe um conjunto de funções estatísticas e de probabilidade que se comportam como funções universais – aceitam um escalar, um iterável ou um ndarray e devolvem um ndarray de floats:

x = np.linspace(0, 4, num=8)

sp.special.erf(x)         # error function
sp.special.erfc(x)        # complementary error function
sp.special.gamma(x + 1)   # gamma function
sp.special.gammaln(x + 1) # log-gamma function

A função de erro e o seu complemento aparecem na CDF de uma gaussiana – a aplicação de eleição para converter entre um z-score medido e uma probabilidade, ou para calcular o integral da cauda de uma distribuição normal. As funções gama e log-gama surgem em cálculos beta / qui-quadrado / student-t; gammaln é a forma numericamente estável para argumentos grandes onde o próprio gamma transbordaria.

6.17.4. Números aleatórios

numpy.random fornece uma classe Generator que extrai amostras de distribuições comuns. O gerador é estável: cada chamada avança o seu estado interno, pelo que chamadas consecutivas devolvem amostras independentes:

from ulab import numpy as np

rng = np.random.Generator(seed=42)

rng.random(size=5)             # 5 uniform [0.0, 1.0) samples
rng.uniform(low=-1.0, high=1.0, size=10)
rng.normal(loc=0.0, scale=1.0, size=(2, 4))

O dtype de saída é sempre float. size= aceita um inteiro (saída a 1-D) ou um tuple (saída a n-D); quando omitido, é devolvido um único float Python.

O gerador é adequado para simulação, dithering, dados de teste sintéticos e qualquer outra aplicação onde a robustez criptográfica não é necessária. Não é adequado para chaves ou tokens; use a fonte aleatória do sistema através de os para esses efeitos.

6.17.5. Disponibilidade em tempo de compilação

Se cada submódulo está realmente presente depende de como a câmara foi compilada. scipy.optimize e scipy.special não estão ativados em todas as câmaras; chamar uma função que a câmara não inclui levanta AttributeError. dir(sp), dir(sp.optimize), dir(np.random) e similares reportam o que está disponível na câmara em uso.