Phase 01 - Lesson 13

Estabilidade Numérica

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Ponto flutuante é uma abstracao que vaza. Ele vai te morder durante o treinamento, e você não vai ver a hora que chegou.

Tipo: Construir Linguagem: Python Pré-requisitos: Fase 1, Lições 01-04 Tempo: ~120 minutos

Objetivos de Aprendizagem

  • Implementar softmax e log-sum-exp numericamente estáveis usando o truque dá subtração do máximo
  • Identificar overflow, underflow e cancelamento catastrófico em computações de ponto flutuante
  • Verificar gradientes analiticos contra gradientes numéricos usando diferenças finitas centradas
  • Explicar por que bfloat16 é preferido em vez de float16 para treinamento e como o escalonamento dá loss evita o underflow de gradientes

O Problema

Seu modelo treina por três horas e então a loss vira NaN. Você adiciona uma instrucao de print. Os logits estão bem no passo 9.000. No passo 9.001 eles são inf. No passo 9.002 todos os gradientes são nan é o treinamento está morto.

Ou: seu modelo treina até o fim, mas a acurácia está 2% pior do que o paper afirma. Você verifica tudo. A arquitetura bate. Os hiperparâmetros batem. Os dados batem. O problema e que o paper usou float32 e você usou float16 sem o escalonamento certo. Trinta e dois bits de erro de arredondamento acumulado comeram silenciosamente a sua acurácia.

Ou: você implementa a loss de entropia cruzada do zero. Funciona com logits pequenos. Quando os logits passam de 100, ela retorna inf. O softmax estourou porque exp(100) é maior do que o float32 consegue representar. Todo framework de ML lida com isso usando um truque de duas linhas. Você não sabia que o truque existia.

Estabilidade numérica não é uma preocupacao teórica. É a diferença entre uma rodada de treinamento que dá certo é uma que falha silenciosamente. Todo bug serio de ML que você vier a depurar acaba se resumindo a ponto flutuante.

O Conceito

IEEE 754: Como os Computadores Armazenam Números Reais

Os computadores armazenam números reais como valores de ponto flutuante seguindo o padrão IEEE 754. Um float tem três partes: um bit de sinal, um expoente é uma mantissa (significando).

Float32 layout (32 bits total):
[1 sign] [8 exponent] [23 mantissa]

Value = (-1)^sign * 2^(exponent - 127) * 1.mantissa

A mantissa determina a precisão (quantos dígitos significativos). O expoente determina a faixa (quao grande ou pequeno um número pode ser).

Format     Bits   Exponent  Mantissa  Decimal digits  Range (approx)
float64    64     11        52        ~15-16          +/- 1.8e308
float32    32     8         23        ~7-8            +/- 3.4e38
float16    16     5         10        ~3-4            +/- 65,504
bfloat16   16     8         7         ~2-3            +/- 3.4e38

float32 te dá cerca de 7 dígitos decimais de precisão. Isso significa que ele consegue distinguir 1.0000001 de 1.0000002, mas não 1.00000001 de 1.00000002. Depois de 7 dígitos, tudo e ruido de arredondamento.

float16 te dá cerca de 3 dígitos. O maior número que ele consegue representar e 65.504. Isso é desconfortavelmente pequeno para ML, onde logits, gradientes e ativações ultrapassam esse valor rotineiramente.

bfloat16 é a resposta do Google para o problema de faixa do float16. Ele tem o mesmo expoente de 8 bits do float32 (mesma faixa, até 3.4e38) mas apenas 7 bits de mantissa (menos precisão que o float16). Para treinar redes neurais, a faixa importa mais que a precisão, então o bfloat16 normalmente vence.

Por que 0.1 + 0.2 != 0.3

O número 0.1 não pode ser representado exatamente em ponto flutuante binário. Na base 2, ele é uma fração periódica:

0.1 in binary = 0.0001100110011001100110011... (repeating forever)

O float32 trunca isso para 23 bits de mantissa. O valor armazenado e aproximadamente 0.100000001490116. De forma similar, 0.2 e armazenado como aproximadamente 0.200000002980232. A soma deles e 0.300000004470348, e não 0.3.

In Python:
>>> 0.1 + 0.2
0.30000000000000004

>>> 0.1 + 0.2 == 0.3
False

Isso importa para ML porque:

  1. Comparacoes de loss como if loss < threshold podem dar respostas erradas
  2. Acumular muitos valores pequenos (atualizações de gradiente ao longo de milhares de passos) desvia dá soma verdadeira
  3. Checksums e testes de reprodutibilidade falham se você compara floats com ==

A correção: nunca compare floats com ==. Use abs(a - b) < epsilon ou math.isclose().

Cancelamento Catastrófico

Quando você subtrai dois números de ponto flutuante quase iguais, os dígitos significativos se cancelam e você fica com ruido de arredondamento promovido a dígitos principais.

a = 1.0000001    (stored as 1.00000011920929 in float32)
b = 1.0000000    (stored as 1.00000000000000 in float32)

True difference:  0.0000001
Computed:         0.00000011920929

Relative error: 19.2%

Isso é um erro relativo de 19% a partir de uma única subtração. Em ML, isso acontece sempre que você:

  • Calcula a variância de dados com uma média grande: E[x^2] - E[x]^2 quando E[x] é grande
  • Subtrai log-probabilidades quase iguais
  • Calcula gradientes por diferenças finitas com um epsilon pequeno demais

A correção: reorganize as formulas para evitar subtrair números grandes e quase iguais. Para variância, use o algoritmo de Welford ou centre os dados primeiro. Para log-probabilidades, trabalhe no espaço logaritmico o tempo todo.

Overflow e Underflow

Overflow acontece quando um resultado é grande demais para ser representado. Underflow acontece quando ele é pequeno demais (mais próximo de zero do que o menor número positivo representável).

Float32 boundaries:
  Maximum:  3.4028235e+38
  Minimum positive (normal): 1.175e-38
  Minimum positive (denorm): 1.401e-45
  Overflow:  anything > 3.4e38 becomes inf
  Underflow: anything < 1.4e-45 becomes 0.0

A função exp() é a principal fonte de overflow em ML:

exp(88.7)  = 3.40e+38   (barely fits in float32)
exp(89.0)  = inf         (overflow)
exp(-87.3) = 1.18e-38   (barely above underflow)
exp(-104)  = 0.0         (underflow to zero)

A função log() acerta na direção oposta:

log(0.0)   = -inf
log(-1.0)  = nan
log(1e-45) = -103.3      (fine)
log(1e-46) = -inf        (input underflowed to 0, then log(0) = -inf)

Em ML, exp() aparece em softmax, sigmoid e computações de probabilidade. log() aparece em entropia cruzada, log-verossimilhancas e divergência KL. A combinacao log(exp(x)) é um campo minado sem os truques certos.

O Truque do Log-Sum-Exp

Calcular log(sum(exp(x_i))) diretamente é numericamente perigoso. Se qualquer x_i for grande, exp(x_i) estoura. Se todos os x_i forem muito negativos, todo exp(x_i) sofre underflow para zero e log(0) e -inf.

O truque: subtraia o valor máximo antes de exponenciar.

log(sum(exp(x_i))) = max(x) + log(sum(exp(x_i - max(x))))

Por que isso funciona: depois de subtrair max(x), o maior expoente e exp(0) = 1. Nenhum overflow é possível. Pelo menos um termo na soma e 1, então a soma e pelo menos 1, e log(1) = 0. Nenhum underflow para -inf é possível.

Prova:

log(sum(exp(x_i)))
= log(sum(exp(x_i - c + c)))                    (add and subtract c)
= log(sum(exp(x_i - c) * exp(c)))               (exp(a+b) = exp(a)*exp(b))
= log(exp(c) * sum(exp(x_i - c)))               (factor out exp(c))
= c + log(sum(exp(x_i - c)))                    (log(a*b) = log(a) + log(b))

Faça c = max(x) é o overflow e eliminado.

Esse truque aparece em todo lugar em ML:

  • Normalização do softmax
  • Computação dá loss de entropia cruzada
  • Soma de log-probabilidades em modelos de sequência
  • Mistura de Gaussianas
  • Inferencia variacional

Por que o Softmax Precisa do Truque dá Subtração do Máximo

O softmax converte logits em probabilidades:

softmax(x_i) = exp(x_i) / sum(exp(x_j))

Sem o truque, logits de [100, 101, 102] causam overflow:

exp(100) = 2.69e43
exp(101) = 7.31e43
exp(102) = 1.99e44
sum      = 2.99e44

These overflow float32 (max ~3.4e38)? No, 2.69e43 < 3.4e38? Actually:
exp(88.7) is already at the float32 limit.
exp(100) = inf in float32.

Com o truque, subtraia max(x) = 102:

exp(100 - 102) = exp(-2) = 0.135
exp(101 - 102) = exp(-1) = 0.368
exp(102 - 102) = exp(0)  = 1.000
sum = 1.503

softmax = [0.090, 0.245, 0.665]

As probabilidades são idênticas. A computação e segura. Isso não é uma otimização. É um requisito para a correção.

NaN e Inf: Detecção e Prevenção

nan (Not a Number) e inf (infinito) se propagam viralmente pela computação. Um nan em uma atualização de gradiente torna o peso nan, o que torna toda saída subsequente nan. O treinamento está morto dentro de um passo.

Como o inf aparece:

  • exp() de um número positivo grande
  • Divisão por zero: 1.0 / 0.0
  • overflow de float32 em acumulações

Como o nan aparece:

  • 0.0 / 0.0
  • inf - inf
  • inf * 0
  • sqrt() de um número negativo
  • log() de um número negativo
  • Qualquer aritmética envolvendo um nan existente

Detecção:

import math

math.isnan(x)       # True if x is nan
math.isinf(x)       # True if x is +inf or -inf
math.isfinite(x)    # True if x is neither nan nor inf

Estrategias de prevenção:

  1. Limite (clamp) as entradas de exp(): exp(clamp(x, -80, 80))
  2. Adicione epsilon aos denominadores: x / (y + 1e-8)
  3. Adicione epsilon dentro de log(): log(x + 1e-8)
  4. Use implementações estáveis (log-sum-exp, softmax estável)
  5. Recorte de gradiente (gradient clipping) para evitar explosao de pesos
  6. Verifique nan/inf após cada forward pass durante a depuracao

Verificação Numérica de Gradiente

Gradientes analiticos (dá retropropagacao) podem ter bugs. A verificação numérica de gradiente os verifica calculando gradientes com diferenças finitas.

A formula dá diferença centrada:

df/dx ~= (f(x + h) - f(x - h)) / (2h)

Isso tem precisão O(h^2), muito melhor que a diferença progressiva (f(x+h) - f(x)) / h, que é apenas O(h).

Escolhendo h: grande demais é a aproximação fica errada. Pequeno demais é o cancelamento catastrófico destrói a resposta. h = 1e-5 a 1e-7 é o tipico.

A verificação: calcule a diferença relativa entre os gradientes analiticos e numéricos.

relative_error = |grad_analytical - grad_numerical| / max(|grad_analytical|, |grad_numerical|, 1e-8)

Regras de bolso:

  • relative_error < 1e-7: perfeito, o gradiente está correto
  • relative_error < 1e-5: aceitável, provavelmente correto
  • relative_error > 1e-3: alguma coisa está errada
  • relative_error > 1: o gradiente está completamente errado

Sempre verifique gradientes ao implementar uma nova camada ou função de loss. O PyTorch fornece torch.autograd.gradcheck() para isso.

Treinamento em Precisão Mista

As GPUs modernas tem hardware especializado (Tensor Cores) que calcula multiplicações de matrizes em float16 de 2 a 8 vezes mais rápido que em float32. O treinamento em precisão mista explora isso:

1. Maintain float32 master copy of weights
2. Forward pass in float16 (fast)
3. Compute loss in float32 (prevents overflow)
4. Backward pass in float16 (fast)
5. Scale gradients to float32
6. Update float32 master weights

O problema com o treinamento puro em float16: os gradientes costumam ser muito pequenos (1e-8 ou menores). O float16 sofre underflow de qualquer valor abaixo de ~6e-8 para zero. Seu modelo para de aprender porque todas as atualizações de gradiente são zero.

A correção é o escalonamento dá loss (loss scaling):

1. Multiply loss by a large scale factor (e.g., 1024)
2. Backward pass computes gradients of (loss * 1024)
3. All gradients are 1024x larger (pushed above float16 underflow)
4. Divide gradients by 1024 before updating weights
5. Net effect: same update, but no underflow

O escalonamento dinamico dá loss ajusta o fator de escala automaticamente. Comece com um valor grande (65536). Se os gradientes estourarem para inf, divida por dois. Se N passos passarem sem overflow, dobre.

bfloat16 vs float16: Por que o bfloat16 Vence para Treinamento

float16:   [1 sign] [5 exponent]  [10 mantissa]
bfloat16:  [1 sign] [8 exponent]  [7 mantissa]

float16 tem mais precisão (10 bits de mantissa vs 7) mas faixa limitada (max ~65.504). bfloat16 tem menos precisão mas a mesma faixa do float32 (max ~3.4e38).

Para treinar redes neurais:

  • Ativações e logits ultrapassam regularmente 65.504 durante picos de treinamento. O float16 estoura; o bfloat16 lida com isso.
  • O escalonamento dá loss é obrigatório com float16 mas geralmente desnecessario com bfloat16, porque sua faixa cobre o espectro de magnitude dos gradientes.
  • bfloat16 é um truncamento simples do float32: descarte os 16 bits inferiores dá mantissa. A conversao é trivial e sem perda no expoente.

float16 é preferido para inferencia, onde os valores são limitados é a precisão importa mais. bfloat16 é preferido para treinamento, onde a faixa importa mais.É por isso que TPUs e GPUs NVIDIA modernas (A100, H100) tem suporte nativo a bfloat16.

Recorte de Gradiente (Gradient Clipping)

Gradientes explosivos acontecem quando os gradientes crescem exponencialmente atraves de muitas camadas (comum em RNNs, redes profundas e transformers). Um único gradiente grande pode corromper todos os pesos em um passo.

Dois tipos de recorte:

Recorte por valor: limita cada elemento do gradiente de forma independente.

grad = clamp(grad, -max_val, max_val)

Simples, mas pode mudar a direção do vetor gradiente.

Recorte por norma: escala o vetor gradiente inteiro de modo que sua norma não ultrapasse um limiar.

if ||grad|| > max_norm:
    grad = grad * (max_norm / ||grad||)

Preserva a direção do gradiente. É o que torch.nn.utils.clip_grad_norm_() faz. É a escolha padrão.

Valores tipicos: max_norm=1.0 para transformers, max_norm=0.5 para RL, max_norm=5.0 para redes mais simples.

O recorte de gradiente não é uma gambiarra. É um mecanismo de segurança. Sem ele, um único batch atipico pode produzir um gradiente grande o suficiente para arruinar semanas de treinamento.

Camadas de Normalização como Estabilizadores Numericos

A normalização em batch (batch normalization), a normalização em camada (layer normalization) é a normalização RMS são geralmente apresentadas como regularizadores que ajudam o treinamento a convergir. Elas também são estabilizadores numéricos.

Sem normalização, as ativações podem crescer ou encolher exponencialmente atraves das camadas:

Layer 1: values in [0, 1]
Layer 5: values in [0, 100]
Layer 10: values in [0, 10,000]
Layer 50: values in [0, inf]

A normalização recentra e reescala as ativações em cada camada:

LayerNorm(x) = (x - mean(x)) / (std(x) + epsilon) * gamma + beta

O epsilon (tipicamente 1e-5) evita divisão por zero quando todas as ativações são idênticas. Os parâmetros aprendidos gamma e beta deixam a rede restaurar qualquer escala que ela precise.

Isso mantém os valores em uma faixa numericamente segura ao longo dá rede, evitando tanto overflow no forward pass quanto explosao de gradiente no backward pass.

Bugs Numericos Comuns de ML

Bug: A loss vira NaN após algumas épocas. Causa: os logits cresceram demais, o softmax estourou. Ou a taxa de aprendizado está alta demais e os pesos divergiram. Correção: use softmax estável (subtração do máximo), reduza a taxa de aprendizado, adicione recorte de gradiente.

Bug: A loss fica presa em log(num_classes). Causa: as saídas do modelo são probabilidades quase uniformes. Frequentemente significa que os gradientes estão sumindo ou que o modelo não está aprendendo nada. Correção: verifique se os rótulos dos dados estão corretos, verifique a função de loss, verifique ReLUs mortas.

Bug: A acurácia de validação está 1-3% abaixo do esperado. Causa: precisão mista sem o escalonamento adequado dá loss. O underflow de gradiente zera silenciosamente as atualizações pequenas. Correção: ative o escalonamento dinamico dá loss, ou mude para bfloat16.

Bug: As normas de gradiente são 0.0 para algumas camadas. Causa: neuronios ReLU mortos (todas as entradas negativas), ou underflow de float16. Correção: use LeakyReLU ou GELU, use escalonamento de gradiente, verifique a inicialização dos pesos.

Bug: O modelo funciona em uma GPU mas dá resultados diferentes em outra. Causa: ordem de acumulação de ponto flutuante não determinística. As reducoes paralelas dá GPU somam em ordens diferentes em hardwares diferentes, é a adição de ponto flutuante não é associativa. Correção: aceite pequenas diferenças (1e-6), ou defina torch.use_deterministic_algorithms(True) e aceite a penalidade de velocidade.

Bug: exp() retorna inf na computação dá loss. Causa: logits crus passados para exp() sem o truque dá subtração do máximo. Correção: use torch.nn.functional.log_softmax(), que implementa log-sum-exp internamente.

Bug: O treinamento diverge depois de trocar de float32 para float16. Causa: o float16 não consegue representar magnitudes de gradiente abaixo de 6e-8 ou ativações acima de 65.504. Correção: use precisão mista com escalonamento dá loss (AMP), ou use bfloat16.

Construa

Passo 1: Demonstre os limites de precisão do ponto flutuante

print("=== Floating Point Precision ===")
print(f"0.1 + 0.2 = {0.1 + 0.2}")
print(f"0.1 + 0.2 == 0.3? {0.1 + 0.2 == 0.3}")
print(f"Difference: {(0.1 + 0.2) - 0.3:.2e}")

Passo 2: Implemente softmax ingênuo vs estável

import math

def softmax_naive(logits):
    exps = [math.exp(z) for z in logits]
    total = sum(exps)
    return [e / total for e in exps]

def softmax_stable(logits):
    max_logit = max(logits)
    exps = [math.exp(z - max_logit) for z in logits]
    total = sum(exps)
    return [e / total for e in exps]

safe_logits = [2.0, 1.0, 0.1]
print(f"Naive:  {softmax_naive(safe_logits)}")
print(f"Stable: {softmax_stable(safe_logits)}")

dangerous_logits = [100.0, 101.0, 102.0]
print(f"Stable: {softmax_stable(dangerous_logits)}")
# softmax_naive(dangerous_logits) would return [nan, nan, nan]

Passo 3: Implemente log-sum-exp estável

def logsumexp_naive(values):
    return math.log(sum(math.exp(v) for v in values))

def logsumexp_stable(values):
    c = max(values)
    return c + math.log(sum(math.exp(v - c) for v in values))

safe = [1.0, 2.0, 3.0]
print(f"Naive:  {logsumexp_naive(safe):.6f}")
print(f"Stable: {logsumexp_stable(safe):.6f}")

large = [500.0, 501.0, 502.0]
print(f"Stable: {logsumexp_stable(large):.6f}")
# logsumexp_naive(large) returns inf

Passo 4: Implemente entropia cruzada estável

def cross_entropy_naive(true_class, logits):
    probs = softmax_naive(logits)
    return -math.log(probs[true_class])

def cross_entropy_stable(true_class, logits):
    max_logit = max(logits)
    shifted = [z - max_logit for z in logits]
    log_sum_exp = math.log(sum(math.exp(s) for s in shifted))
    log_prob = shifted[true_class] - log_sum_exp
    return -log_prob

logits = [2.0, 5.0, 1.0]
true_class = 1
print(f"Naive:  {cross_entropy_naive(true_class, logits):.6f}")
print(f"Stable: {cross_entropy_stable(true_class, logits):.6f}")

Passo 5: Verificação de gradiente

def numerical_gradient(f, x, h=1e-5):
    grad = []
    for i in range(len(x)):
        x_plus = x[:]
        x_minus = x[:]
        x_plus[i] += h
        x_minus[i] -= h
        grad.append((f(x_plus) - f(x_minus)) / (2 * h))
    return grad

def check_gradient(analytical, numerical, tolerance=1e-5):
    for i, (a, n) in enumerate(zip(analytical, numerical)):
        denom = max(abs(a), abs(n), 1e-8)
        rel_error = abs(a - n) / denom
        status = "OK" if rel_error < tolerance else "FAIL"
        print(f"  param {i}: analytical={a:.8f} numerical={n:.8f} "
              f"rel_error={rel_error:.2e} [{status}]")

def f(params):
    x, y = params
    return x**2 + 3*x*y + y**3

def f_grad(params):
    x, y = params
    return [2*x + 3*y, 3*x + 3*y**2]

point = [2.0, 1.0]
analytical = f_grad(point)
numerical = numerical_gradient(f, point)
check_gradient(analytical, numerical)

Use

Simulacao de precisão mista

import struct

def float32_to_float16_round(x):
    packed = struct.pack('f', x)
    f32 = struct.unpack('f', packed)[0]
    packed16 = struct.pack('e', f32)
    return struct.unpack('e', packed16)[0]

def simulate_bfloat16(x):
    packed = struct.pack('f', x)
    as_int = int.from_bytes(packed, 'little')
    truncated = as_int & 0xFFFF0000
    repacked = truncated.to_bytes(4, 'little')
    return struct.unpack('f', repacked)[0]

Recorte de gradiente

def clip_by_norm(gradients, max_norm):
    total_norm = math.sqrt(sum(g**2 for g in gradients))
    if total_norm > max_norm:
        scale = max_norm / total_norm
        return [g * scale for g in gradients]
    return gradients

grads = [10.0, 20.0, 30.0]
clipped = clip_by_norm(grads, max_norm=5.0)
print(f"Original norm: {math.sqrt(sum(g**2 for g in grads)):.2f}")
print(f"Clipped norm:  {math.sqrt(sum(g**2 for g in clipped)):.2f}")
print(f"Direction preserved: {[c/clipped[0] for c in clipped]} == {[g/grads[0] for g in grads]}")

Detecção de NaN/Inf

def check_tensor(name, values):
    has_nan = any(math.isnan(v) for v in values)
    has_inf = any(math.isinf(v) for v in values)
    if has_nan or has_inf:
        print(f"WARNING {name}: nan={has_nan} inf={has_inf}")
        return False
    return True

check_tensor("good", [1.0, 2.0, 3.0])
check_tensor("bad",  [1.0, float('nan'), 3.0])
check_tensor("ugly", [1.0, float('inf'), 3.0])

Veja code/numerical.py para implementações completas com todos os casos de borda demonstrados.

Entregue

Está lição produz:

  • code/numerical.py com softmax estável, log-sum-exp, entropia cruzada, verificação de gradiente e simulacao de precisão mista
  • outputs/prompt-numerical-debugger.md para diagnosticar NaN/Inf e problemas numéricos no treinamento

Essas implementações estáveis reaparecem na Fase 3 ao construir o loop de treinamento e na Fase 4 ao implementar mecanismos de atencao.

Exercícios

  1. Cancelamento catastrófico. Calcule a variância de [1000000.0, 1000001.0, 1000002.0] usando a formula ingênua E[x^2] - E[x]^2 em float32. Depois calcule usando o algoritmo online de Welford. Compare os erros contra a variância verdadeira (0.6667).

  2. Caca a precisão. Encontre o menor valor positivo de float32 x tal que 1.0 + x == 1.0 em Python. Esse é o epsilon de máquina. Verifique se ele bate com numpy.finfo(numpy.float32).eps.

  3. Casos de borda do log-sum-exp. Teste sua função logsumexp_stable com: (a) todos os valores iguais, (b) um valor muito maior que os demais, (c) todos os valores muito negativos (-1000). Verifique se ela dá resultados corretos onde a versao ingênua falha.

  4. Verificação de gradiente em uma camada de rede neural. Implemente uma única camada linear y = Wx + b e seu backward pass analitico. Use numerical_gradient para verificar a correção para uma matriz de pesos 3x2.

  5. Experimento de escalonamento dá loss. Simule treinamento com float16: crie gradientes aleatórios na faixa [1e-9, 1e-3], converta para float16 e meca que fração vira zero. Depois aplique o escalonamento dá loss (multiplique por 1024), converta para float16, reescale de volta e meca a fração de zeros novamente.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
IEEE 754 "O padrão do float" Padrão internacional que define os formatos binários de ponto flutuante, regras de arredondamento e valores especiais (inf, nan). Toda CPU e GPU moderna o implementa.
Epsilon de máquina "O limite de precisão" O menor valor e tal que 1.0 + e != 1.0 em um dado formato de float. Para float32, e cerca de 1.19e-7.
Cancelamento catastrófico "Perda de precisão por subtração" Quando se subtraem números de ponto flutuante quase iguais, os dígitos significativos se cancelam é o ruido de arredondamento domina o resultado.
Overflow "Número grande demais" Um resultado ultrapassa o maior valor representável e vira inf. exp(89) estoura o float32.
Underflow "Número pequeno demais" Um resultado fica mais próximo de zero do que o menor número positivo representável e vira 0.0. exp(-104) sofre underflow no float32.
Truque do log-sum-exp "Subtraia o máximo primeiro" Calcular log(sum(exp(x))) fatorando exp(max(x)) para evitar overflow e underflow. Usado em softmax, entropia cruzada e matemática de log-probabilidades.
Softmax estável "Softmax que não explode" Subtrair max(logits) antes de exponenciar. Resultado numericamente idêntico, sem overflow possível.
Verificação de gradiente "Verifique sua retropropagacao" Comparar gradientes analiticos dá retropropagacao contra gradientes numéricos de diferenças finitas para pegar bugs de implementação.
Precisão mista "Float16 no forward, float32 no backward" Usar floats de menor precisão para operações críticas em velocidade e floats de maior precisão para operações numericamente sensíveis. O ganho tipico e de 2-3x.
Escalonamento dá loss "Evitar underflow de gradiente" Multiplicar a loss por uma constante grande antes dá retropropagacao para que os gradientes fiquem na faixa representável do float16, e depois dividir pela mesma constante antes das atualizações de pesos.
bfloat16 "Brain floating point" O formato de 16 bits do Google com 8 bits de expoente (mesma faixa do float32) e 7 bits de mantissa (menos precisão que o float16). Preferido para treinamento.
Recorte de gradiente "Limitar a norma do gradiente" Escalar o vetor gradiente para que sua norma não ultrapasse um limiar. Evita que gradientes explosivos arruinem os pesos.
NaN "Not a Number" Valor especial de float vindo de operações indefinidas (0/0, inf-inf, sqrt(-1)). Propaga por toda a aritmética subsequente.
Inf "Infinito" Valor especial de float vindo de overflow ou divisão por zero. Pode combinar para produzir NaN (inf - inf, inf * 0).
Gradiente numérico "Derivada por força bruta" Aproximar uma derivada avaliando f(x+h) e f(x-h) e dividindo por 2h. Lento mas confiável para verificação.

Leitura Complementar

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).