LinkedIn

Como verificar gradientes com diferenças finitas em Python

Um gradiente errado pode passar despercebido até que o treinamento de uma rede neural pare de melhorar. Há um teste pequeno para investigar esse problema: alterar um parâmetro, medir a mudança na perda e comparar essa medida com a derivada calculada pelo código.

Neste tutorial, você vai verificar os três parâmetros de um neurônio com Python. O exemplo usa dados sintéticos, não baixa modelos e não acessa serviços externos. O código foi executado em ambiente isolado. Ele é uma demonstração de diagnóstico, sem treinamento ou implantação em produção.

O que você precisa

  • Python 3.12 e um terminal. A execução descrita aqui usou CPython 3.12.14, em Linux x86-64.
  • Apenas a biblioteca padrão. Não é preciso instalar NumPy, PyTorch ou outros pacotes.
  • Saber criar um arquivo, executar uma função e ler uma lista de números. As derivadas usadas abaixo estão explicadas.

Os cálculos usam float. No ambiente testado, sua mantissa tem 53 bits de precisão. Isso permite representar muitos números com boa aproximação, mas não elimina erros de arredondamento. A documentação do Python sobre ponto flutuante explica essa limitação.

1. Defina a perda e os parâmetros

Um parâmetro é um valor ajustável do modelo. Neste exemplo, há dois pesos, w1 e w2, e um deslocamento, b, também chamado de viés. As entradas são x1 = 0.6 e x2 = -1.2. O alvo, que representa a resposta desejada, é y = 0.4.

O neurônio calcula uma soma ponderada e aplica a função tangente hiperbólica, tanh. Ela transforma a soma em uma saída entre −1 e 1. Chamaremos essa saída de a.

z = w1 × x1 + w2 × x2 + b
a = tanh(z)
L = 0.5 × (a − y)²

A perda, indicada por L, mede o desacordo entre a saída e o alvo. O fator 0,5 simplifica a derivada. Começamos com (w1, w2, b) = (0.3, -0.2, 0.1). A soma z é 0,52.

O gradiente reúne as derivadas da perda em relação a cada parâmetro. Cada derivada indica como a perda muda quando aquele parâmetro sofre uma alteração muito pequena, mantendo os demais fixos.

2. Calcule o gradiente por dois caminhos

No caminho analítico, usamos a regra da cadeia: multiplicamos as derivadas das etapas que ligam o parâmetro à perda. A derivada de 0.5 × (a − y)² em relação a a é a − y. A derivada de tanh(z) é 1 − a². Portanto:

q = (a − y) × (1 − a²)
dL/dw1 = q × x1
dL/dw2 = q × x2
dL/db  = q

No caminho numérico, aumentamos um parâmetro em h e calculamos a perda. Depois o diminuímos em h e repetimos o cálculo. A diferença entre essas perdas, dividida por 2h, estima a derivada. Esse método se chama diferença central. A documentação técnica do PyTorch descreve o mesmo princípio para verificar gradientes.

Como funciona a diferença centralCalcule a perda com um parâmetro aumentado e diminuído por h. Subtraia as duas perdas e divida por dois h. Compare essa estimativa com o gradiente analítico.Mude um parâmetro por vezParâmetro + hCalcule a perda L₊Parâmetro − hCalcule a perda L₋(L₊ − L₋) / (2h)Estimativa da derivadaCompare com o cálculo analítico
Diagrama original do procedimento. As entradas e os outros parâmetros permanecem iguais nas duas avaliações.

Usaremos h = eps × max(1, |parâmetro|). Aqui, eps é o tamanho-base da perturbação, não a precisão da máquina. Para os três parâmetros iniciais, h = eps. Essa escolha é uma convenção do exemplo, não uma configuração universal.

3. Execute o exemplo completo

Salve o código abaixo em gradientes.py. Execute com python gradientes.py, usando o interpretador Python 3.12 do seu ambiente. Caso o comando local seja python3, use-o no lugar de python.

from math import isclose, isfinite, tanh

X = (0.6, -1.2)
Y = 0.4
THETA = (0.3, -0.2, 0.1)  # w1, w2, b


def prediction(theta):
    w1, w2, b = theta
    if not all(isfinite(v) for v in theta):
        raise ValueError("Use parametros finitos.")
    return tanh(w1 * X[0] + w2 * X[1] + b)


def loss(theta):
    return 0.5 * (prediction(theta) - Y) ** 2


def analytic_gradient(theta):
    a = prediction(theta)
    dz = (a - Y) * (1.0 - a * a)
    return (dz * X[0], dz * X[1], dz)


def numeric_gradient(theta, eps=1e-5):
    if not isfinite(eps) or eps <= 0:
        raise ValueError("eps deve ser positivo e finito.")
    prediction(theta)  # Valida os parametros.
    result = []
    for i, value in enumerate(theta):
        h = eps * max(1.0, abs(value))
        plus, minus = list(theta), list(theta)
        plus[i], minus[i] = value + h, value - h
        if plus[i] == value or minus[i] == value:
            raise ValueError("Passo pequeno demais para este parametro.")
        result.append((loss(plus) - loss(minus)) / (2.0 * h))
    return tuple(result)


def agrees(analytic, numeric):
    return len(analytic) == len(numeric) and all(
        isfinite(a) and isfinite(n)
        and isclose(a, n, rel_tol=1e-5, abs_tol=1e-8)
        for a, n in zip(analytic, numeric)
    )


if __name__ == "__main__":
    analytic = analytic_gradient(THETA)
    numeric = numeric_gradient(THETA)
    print(f"perda = {loss(THETA):.10f}")
    for name, a, n in zip(("w1", "w2", "b"), analytic, numeric):
        print(f"{name}: analitico={a:.10f} numerico={n:.10f}")
    print("gradientes concordam:", agrees(analytic, numeric))
    wrong = tuple(0.5 * v for v in analytic)
    print("gradiente dividido por 2:", agrees(wrong, numeric))
    for eps in (1e-2, 1e-4, 1e-6, 1e-8, 1e-10, 1e-12):
        numeric = numeric_gradient(THETA, eps)
        error = max(abs(a - n) for a, n in zip(analytic, numeric))
        print(f"eps={eps:.0e}: erro maximo={error:.3e}")

A função numeric_gradient cria cópias dos parâmetros para cada tentativa. Assim, uma perturbação não fica acumulada no cálculo seguinte. Ela também rejeita passos inválidos e passos tão pequenos que não alteram o número armazenado.

A comparação usa math.isclose, com tolerância relativa de 1e-5 e absoluta de 1e-8. A primeira acompanha a escala dos valores. A segunda permite comparar valores próximos de zero. A regra completa está na documentação de math.isclose. As tolerâncias são específicas deste teste. Aumentá-las até uma falha desaparecer pode esconder um defeito.

4. Confira o resultado e provoque uma falha

Esta foi a saída obtida na execução de 15 de outubro de 2026. Pequenas diferenças nos últimos dígitos podem ocorrer em outro ambiente.

perda = 0.0030186459
w1: analitico=0.0359814474 numerico=0.0359814474
w2: analitico=-0.0719628949 numerico=-0.0719628948
b: analitico=0.0599690791 numerico=0.0599690790
gradientes concordam: True
gradiente dividido por 2: False
eps=1e-02: erro maximo=5.026e-05
eps=1e-04: erro maximo=5.026e-09
eps=1e-06: erro maximo=6.621e-12
eps=1e-08: erro maximo=3.215e-10
eps=1e-10: erro maximo=3.512e-08
eps=1e-12: erro maximo=5.825e-06

Os três componentes do gradiente concordam dentro das tolerâncias. O teste com o gradiente dividido por dois retorna False. Essa alteração intencional funciona como controle negativo: verifica se o comparador consegue detectar um erro conhecido.

O erro máximo exibido é o maior valor de |analítico − numérico| entre os três parâmetros. Ele serve para acompanhar a precisão. A aprovação usa a combinação de tolerâncias de agrees, não apenas esse máximo.

Observe a variação do passo. Neste caso, diminuir eps de 1e-2 para 1e-6 reduz o erro. Diminuir ainda mais volta a aumentá-lo. Um passo grande mede uma região menos local da curva. Um passo muito pequeno exige subtrair perdas quase iguais, o que torna o arredondamento mais relevante. O menor passo não é necessariamente o melhor.

5. Repita em mais de um ponto

Um único conjunto de parâmetros pode deixar um erro passar, por coincidência. Para testar outros pontos, salve o trecho abaixo em teste_grade.py, na mesma pasta. Execute python teste_grade.py.

from gradientes import analytic_gradient, numeric_gradient, agrees

checked = 0
for w1 in (-0.5, 0.0, 0.5):
    for w2 in (-0.4, 0.0, 0.4):
        for b in (-0.2, 0.0, 0.2):
            theta = (w1, w2, b)
            if not agrees(analytic_gradient(theta),
                          numeric_gradient(theta)):
                raise AssertionError(theta)
            checked += 1
print(f"{checked} conjuntos aprovados")

A execução retorna 27 conjuntos aprovados. A validação deste tutorial também incluiu um ponto de perda praticamente nula, gradientes com sinal invertido, passos inválidos, parâmetros não finitos e preservação das entradas. Os oito testes automatizados dessa revisão passaram. Essa cobertura descreve somente o exemplo publicado.

Como levar o método para uma rede maior

  1. Escolha uma função de perda pequena e entradas fixas. Confirme primeiro que o cálculo da perda corresponde ao que você quer medir.
  2. Isole poucos parâmetros ou uma operação própria. Registre os valores, o passo e as tolerâncias.
  3. Garanta que avaliações repetidas usem o mesmo estado. Sorteios e atualizações internas podem alterar a perda sem relação com a perturbação.
  4. Compare o gradiente analítico ou obtido por diferenciação automática com as diferenças finitas. Diferenciação automática aplica a regra da cadeia às operações executadas.
  5. Teste vários pontos e passos. Inclua uma falha intencional para conferir o teste.

Em um projeto que já usa PyTorch, consulte torch.autograd.gradcheck. A referência consultada corresponde à versão 2.14. Ela informa que os valores-padrão foram pensados para dupla precisão. Também alerta para pontos não diferenciáveis e tensores com posições que compartilham memória. Este tutorial não instalou nem executou PyTorch.

Limites que importam

  • Custo. A versão apresentada faz duas avaliações da perda por parâmetro. Verificar todos os pesos de um modelo grande dessa forma pode ser caro. Prefira testes pequenos e direcionados.
  • Pontos sem derivada. Uma função como ReLU tem uma mudança de inclinação em zero. Perto desse ponto, os dois lados da perturbação podem seguir comportamentos diferentes.
  • Saturação. Quando tanh fica muito próxima de −1 ou 1, os gradientes podem ser pequenos demais para revelar alguns erros sob a tolerância escolhida.
  • Escala e precisão. O passo e as tolerâncias deste exemplo não devem ser copiados sem avaliação para cálculos em precisão reduzida ou com valores extremos.
  • Escopo da aprovação. A concordância verifica a derivada da função implementada, nos pontos testados. Ela não prova que a perda representa o problema correto, que o modelo vai aprender ou que uma implantação é segura.

O resultado útil é uma evidência local e reproduzível: duas maneiras de medir a mudança na perda concordam, e um erro conhecido é detectado. Guarde esse teste ao lado da operação que ele verifica e execute-o novamente quando o cálculo mudar.

Fontes e reprodução

Código e diagrama originais. Dados sintéticos definidos no próprio exemplo. Testes executados em CPython 3.12.14, sem dependências externas, em 15 de outubro de 2026. Documentação oficial consultada na mesma data:

Publicado originalmente no Vesperean Editorial. Todos os direitos reservados.

LinkedIn

[PUBLICAÇÕES recentes]

Aquisição anunciada da NeoSpace, privacidade na Receita e checagem de desinformação. Análise revisável de Zanshin. [...]
Ganhos de IA precisam de contexto, método e limites. À luz do código público da Vesperean, uma avaliação deve distinguir resultados observados, percepções e hipóteses. [...]

O futuro da sua operação não pode estar fora do seu controle.