Phase 03 - Lesson 09

Agendamentos de Taxa de Aprendizado e Aquecimento

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

A taxa de aprendizado é o hiperparâmetro mais importante de todos. Não é a arquitetura. Não é o tamanho do dataset. Não é a função de ativação. É a taxa de aprendizado. Se você não ajustar mais nada, ajuste isto.

Tipo: Build Linguagens: Python Pré-requisitos: Lição 03.06 (Otimizadores), Lição 03.08 (Inicialização de Pesos) Tempo: ~90 minutos

Objetivos de Aprendizado

  • Implementar do zero os agendamentos de taxa de aprendizado constante, step decay, cosine annealing, aquecimento + cosseno e 1cycle
  • Demonstrar os três modos de falha na escolha da taxa de aprendizado: divergência (alta demais), estagnação (baixa demais) e oscilação (sem decaimento)
  • Explicar por que o aquecimento é necessário para otimizadores baseados em Adam e como ele estabiliza o início do treinamento
  • Comparar a velocidade de convergência entre os cinco agendamentos na mesma tarefa e escolher o mais adequado para um dado orçamento de treinamento

O Problema

Defina a taxa de aprendizado em 0.1. O treinamento diverge: a loss salta para o infinito em 3 passos. Defina em 0.0001. O treinamento se arrasta: depois de 100 épocas, o modelo mal saiu do estado aleatório. Defina em 0.01. O treinamento funciona por 50 épocas, e então a loss oscila em torno de um mínimo que ela nunca consegue alcançar, porque os passos são grandes demais.

A taxa de aprendizado ótima não é uma constante. Ela muda durante o treinamento. No início, você quer passos grandes para avançar rapidamente. No fim do treinamento, você quer passos minúsculos para assentar em um mínimo afiado. A diferença entre um modelo com 90% de acurácia e um com 95% de acurácia muitas vezes está justamente no agendamento.

Todo modelo importante publicado nos últimos três anos usa um agendamento de taxa de aprendizado. O Llama 3 usou lr de pico=3e-4 com 2000 passos de aquecimento e decaimento cosseno até 3e-5. O GPT-3 usou lr=6e-4 com aquecimento ao longo de 375 milhões de tokens. Essas não são escolhas arbitrárias. São o resultado de varreduras extensivas de hiperparâmetros que custaram milhões de dólares.

Você precisa entender agendamentos porque os padrões não vão funcionar para o seu problema. Quando você faz fine-tuning de um modelo pré-treinado, o agendamento certo é diferente do treinamento do zero. Quando você aumenta o tamanho do batch, o período de aquecimento precisa mudar. Quando o treinamento quebra no passo 10.000, você precisa saber se é um problema de agendamento ou outra coisa.

O Conceito

Taxa de Aprendizado Constante

A abordagem mais simples. Escolha um número e use-o em todos os passos.

lr(t) = lr_0

Raramente é ótima. Ou ela é alta demais para o fim do treinamento (oscilação em torno do mínimo) ou baixa demais para o começo (computação desperdiçada em passos minúsculos). Funciona bem para modelos pequenos e para depuração. Uma escolha terrível para qualquer coisa que treine por mais de uma hora.

Step Decay

A abordagem da velha guarda, da era da ResNet. Corte a taxa de aprendizado por um fator (geralmente 10x) em épocas fixas.

lr(t) = lr_0 * gamma^(floor(epoch / step_size))

Onde gamma = 0.1 e step_size = 30 significa: a lr cai por 10x a cada 30 épocas. A ResNet-50 usou isto: lr=0.1, com queda de 10x nas épocas 30, 60 e 90.

O problema: os pontos de decaimento ótimos dependem do dataset e da arquitetura. Mude para um problema diferente e você precisa reajustar quando cortar. As transições são abruptas: a loss pode disparar quando a taxa muda de repente.

Cosine Annealing

Decaimento suave da taxa de aprendizado máxima até a mínima, seguindo uma curva cosseno:

lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * t / T))

Onde t é o passo atual e T é o número total de passos.

Em t=0, o termo cosseno vale 1, então lr = lr_max. Em t=T, o termo cosseno vale -1, então lr = lr_min. O decaimento é gentil no início, acelera no meio e volta a ser gentil perto do fim.

Este é o padrão para a maioria dos treinamentos modernos. Nenhum hiperparâmetro para ajustar além de lr_max e lr_min. O formato do cosseno coincide com a observação empírica de que a maior parte do aprendizado acontece no meio do treinamento: você quer passos de tamanho razoável durante esse período crítico.

Aquecimento: Por Que Você Começa Pequeno

O Adam e outros otimizadores adaptativos mantêm estimativas correntes da média e da variância do gradiente. No passo 0, essas estimativas são inicializadas em zero. As primeiras atualizações de gradiente são baseadas em estatísticas lixo. Se a sua taxa de aprendizado for grande durante esse período, o modelo dá passos enormes e mal direcionados.

O aquecimento corrige isso. Comece com uma taxa de aprendizado minúscula (muitas vezes lr_max / warmup_steps ou até zero) e aumente linearmente até lr_max ao longo dos primeiros N passos. Quando você atinge a taxa de aprendizado completa, as estatísticas do Adam já se estabilizaram.

lr(t) = lr_max * (t / warmup_steps)     for t < warmup_steps

Aquecimento típico: 1-5% do total de passos de treinamento. O Llama 3 treinou por ~1,8 trilhão de tokens e aqueceu por 2000 passos. O GPT-3 aqueceu ao longo de 375 milhões de tokens.

Aquecimento Linear + Decaimento Cosseno

O padrão moderno. Aumente linearmente e depois decaia com cosseno:

if t < warmup_steps:
    lr(t) = lr_max * (t / warmup_steps)
else:
    progress = (t - warmup_steps) / (total_steps - warmup_steps)
    lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * progress))

É isto que o Llama, o GPT, o PaLM e a maioria dos transformers modernos usam. O aquecimento previne a instabilidade inicial. O decaimento cosseno assenta o modelo em um bom mínimo.

Política 1cycle

A descoberta de Leslie Smith (2018): aumente a taxa de aprendizado de um valor baixo para um valor alto na primeira metade do treinamento, e depois reduza-a de volta na segunda metade. Contraintuitivo: por que você aumentaria a taxa de aprendizado no meio do caminho?

A teoria: uma taxa de aprendizado alta age como regularização ao adicionar ruído à trajetória de otimização. O modelo explora mais do panorama da loss durante a fase de subida, encontrando melhores bacias. A fase de descida então refina dentro da melhor bacia encontrada.

Phase 1 (0 to T/2):    lr ramps from lr_max/25 to lr_max
Phase 2 (T/2 to T):    lr ramps from lr_max to lr_max/10000

A 1cycle frequentemente treina mais rápido que o cosine annealing para um orçamento de computação fixo. O trade-off: você precisa saber o número total de passos de antemão.

Formatos dos Agendamentos

graph LR
    subgraph "Constante"
        C1["lr"] --- C2["lr"] --- C3["lr"]
    end

    subgraph "Step Decay"
        S1["0.1"] --- S2["0.1"] --- S3["0.01"] --- S4["0.001"]
    end

    subgraph "Cosine Annealing"
        CS1["lr_max"] --> CS2["gradual"] --> CS3["acentuado"] --> CS4["lr_min"]
    end

    subgraph "Aquecimento + Cosseno"
        WC1["0"] --> WC2["lr_max"] --> WC3["cosseno"] --> WC4["lr_min"]
    end

Fluxograma de Decisão

flowchart TD
    Start["Escolhendo um agendamento de LR"] --> Know{"Conhece o total<br/>de passos de treino?"}

    Know -->|"Sim"| Budget{"Orçamento de computação?"}
    Know -->|"Não"| Constant["Use LR constante<br/>com decaimento manual"]

    Budget -->|"Grande (dias/semanas)"| WarmCos["Aquecimento + Decaimento Cosseno<br/>(padrão Llama/GPT)"]
    Budget -->|"Pequeno (horas)"| OneCycle["Política 1cycle<br/>(convergência mais rápida)"]
    Budget -->|"Moderado"| Cosine["Cosine Annealing<br/>(padrão seguro)"]

    WarmCos --> Warmup["Aquecimento = 1-5% dos passos"]
    OneCycle --> FindLR["Encontre lr_max com teste de faixa de LR"]
    Cosine --> MinLR["Defina lr_min = lr_max / 10"]

Números Reais de Modelos Publicados

graph TD
    subgraph "Configs de LR Publicadas"
        L3["Llama 3 (405B)<br/>Pico: 3e-4<br/>Aquecimento: 2000 passos<br/>Agendamento: Cosseno até 3e-5"]
        G3["GPT-3 (175B)<br/>Pico: 6e-4<br/>Aquecimento: 375M tokens<br/>Agendamento: Cosseno até 0"]
        R50["ResNet-50<br/>Pico: 0.1<br/>Aquecimento: nenhum<br/>Agendamento: Step decay x0.1 em 30,60,90"]
        B["BERT (340M)<br/>Pico: 1e-4<br/>Aquecimento: 10K passos<br/>Agendamento: Decaimento linear"]
    end

Construa

Passo 1: Funções de Agendamento

Cada função recebe o passo atual e retorna a taxa de aprendizado naquele passo.

import math


def constant_schedule(step, lr=0.01, **kwargs):
    return lr


def step_decay_schedule(step, lr=0.1, step_size=100, gamma=0.1, **kwargs):
    return lr * (gamma ** (step // step_size))


def cosine_schedule(step, lr=0.01, total_steps=1000, lr_min=1e-5, **kwargs):
    if step >= total_steps:
        return lr_min
    return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * step / total_steps))


def warmup_cosine_schedule(step, lr=0.01, total_steps=1000, warmup_steps=100, lr_min=1e-5, **kwargs):
    if total_steps <= warmup_steps:
        return lr * (step / max(warmup_steps, 1))
    if step < warmup_steps:
        return lr * step / warmup_steps
    progress = (step - warmup_steps) / (total_steps - warmup_steps)
    return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * progress))


def one_cycle_schedule(step, lr=0.01, total_steps=1000, **kwargs):
    mid = max(total_steps // 2, 1)
    if step < mid:
        return (lr / 25) + (lr - lr / 25) * step / mid
    else:
        progress = (step - mid) / max(total_steps - mid, 1)
        return lr * (1 - progress) + (lr / 10000) * progress

Passo 2: Visualize Todos os Agendamentos

Imprima um gráfico baseado em texto mostrando como cada agendamento evolui ao longo do treinamento.

def visualize_schedule(name, schedule_fn, total_steps=500, **kwargs):
    steps = list(range(0, total_steps, total_steps // 20))
    if total_steps - 1 not in steps:
        steps.append(total_steps - 1)

    lrs = [schedule_fn(s, total_steps=total_steps, **kwargs) for s in steps]
    max_lr = max(lrs) if max(lrs) > 0 else 1.0

    print(f"\n{name}:")
    for s, lr_val in zip(steps, lrs):
        bar_len = int(lr_val / max_lr * 40)
        bar = "#" * bar_len
        print(f"  Step {s:4d}: lr={lr_val:.6f} {bar}")

Passo 3: Rede de Treinamento

Uma rede simples de duas camadas no dataset do círculo, igual às lições anteriores, mas agora variamos o agendamento.

import random


def sigmoid(x):
    x = max(-500, min(500, x))
    return 1.0 / (1.0 + math.exp(-x))


def relu(x):
    return max(0.0, x)


def relu_deriv(x):
    return 1.0 if x > 0 else 0.0


def make_circle_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], label))
    return data


def train_with_schedule(schedule_fn, schedule_name, data, epochs=300, base_lr=0.05, **kwargs):
    random.seed(0)
    hidden_size = 8
    total_steps = epochs * len(data)

    std = math.sqrt(2.0 / 2)
    w1 = [[random.gauss(0, std) for _ in range(2)] for _ in range(hidden_size)]
    b1 = [0.0] * hidden_size
    w2 = [random.gauss(0, std) for _ in range(hidden_size)]
    b2 = 0.0

    step = 0
    epoch_losses = []

    for epoch in range(epochs):
        total_loss = 0
        correct = 0

        for x, target in data:
            lr = schedule_fn(step, lr=base_lr, total_steps=total_steps, **kwargs)

            z1 = []
            h = []
            for i in range(hidden_size):
                z = w1[i][0] * x[0] + w1[i][1] * x[1] + b1[i]
                z1.append(z)
                h.append(relu(z))

            z2 = sum(w2[i] * h[i] for i in range(hidden_size)) + b2
            out = sigmoid(z2)

            error = out - target
            d_out = error * out * (1 - out)

            for i in range(hidden_size):
                d_h = d_out * w2[i] * relu_deriv(z1[i])
                w2[i] -= lr * d_out * h[i]
                for j in range(2):
                    w1[i][j] -= lr * d_h * x[j]
                b1[i] -= lr * d_h
            b2 -= lr * d_out

            total_loss += (out - target) ** 2
            if (out >= 0.5) == (target >= 0.5):
                correct += 1
            step += 1

        avg_loss = total_loss / len(data)
        accuracy = correct / len(data) * 100
        epoch_losses.append(avg_loss)

    return epoch_losses

Passo 4: Compare Todos os Agendamentos

Treine a mesma rede com cada agendamento e compare a loss final e o comportamento de convergência.

def compare_schedules(data):
    configs = [
        ("Constant", constant_schedule, {}),
        ("Step Decay", step_decay_schedule, {"step_size": 15000, "gamma": 0.1}),
        ("Cosine", cosine_schedule, {"lr_min": 1e-5}),
        ("Warmup+Cosine", warmup_cosine_schedule, {"warmup_steps": 3000, "lr_min": 1e-5}),
        ("1cycle", one_cycle_schedule, {}),
    ]

    print(f"\n{'Schedule':<20} {'Start Loss':>12} {'Mid Loss':>12} {'End Loss':>12} {'Best Loss':>12}")
    print("-" * 70)

    for name, schedule_fn, extra_kwargs in configs:
        losses = train_with_schedule(schedule_fn, name, data, epochs=300, base_lr=0.05, **extra_kwargs)
        mid_idx = len(losses) // 2
        best = min(losses)
        print(f"{name:<20} {losses[0]:>12.6f} {losses[mid_idx]:>12.6f} {losses[-1]:>12.6f} {best:>12.6f}")

Passo 5: LR Alta Demais vs Baixa Demais

Demonstre os três modos de falha: alta demais (divergência), baixa demais (arrastando) e na medida certa.

def lr_sensitivity(data):
    learning_rates = [1.0, 0.1, 0.01, 0.001, 0.0001]

    print("\nLR Sensitivity (constant schedule, 100 epochs):")
    print(f"  {'LR':>10} {'Start Loss':>12} {'End Loss':>12} {'Status':>15}")
    print("  " + "-" * 52)

    for lr in learning_rates:
        losses = train_with_schedule(constant_schedule, f"lr={lr}", data, epochs=100, base_lr=lr)
        start = losses[0]
        end = losses[-1]

        if end > start or math.isnan(end) or end > 1.0:
            status = "DIVERGED"
        elif end > start * 0.9:
            status = "BARELY MOVED"
        elif end < 0.15:
            status = "CONVERGED"
        else:
            status = "LEARNING"

        end_str = f"{end:.6f}" if not math.isnan(end) else "NaN"
        print(f"  {lr:>10.4f} {start:>12.6f} {end_str:>12} {status:>15}")

Use

O PyTorch fornece agendadores em torch.optim.lr_scheduler:

import torch
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR, OneCycleLR, StepLR

model = nn.Sequential(nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 1))
optimizer = optim.Adam(model.parameters(), lr=3e-4)

scheduler = CosineAnnealingLR(optimizer, T_max=1000, eta_min=1e-5)

for step in range(1000):
    loss = train_step(model, optimizer)
    scheduler.step()

Para aquecimento + cosseno, use um agendador lambda ou o get_cosine_schedule_with_warmup da HuggingFace:

from transformers import get_cosine_schedule_with_warmup

scheduler = get_cosine_schedule_with_warmup(
    optimizer,
    num_warmup_steps=2000,
    num_training_steps=100000,
)

A função da HuggingFace é o que a maioria dos scripts de fine-tuning de Llama e GPT usa. Na dúvida, use aquecimento + cosseno com aquecimento = 3-5% do total de passos. Funciona para quase tudo.

Entregue

Esta lição produz:

  • outputs/prompt-lr-schedule-advisor.md -- um prompt que recomenda o agendamento de taxa de aprendizado certo e os hiperparâmetros para a sua configuração de treinamento

Exercícios

  1. Implemente o decaimento exponencial: lr(t) = lr_0 * gamma^t onde gamma = 0.999. Compare com o cosine annealing no dataset do círculo.

  2. Implemente o teste de faixa de taxa de aprendizado (Leslie Smith): treine por algumas centenas de passos enquanto aumenta exponencialmente a LR de 1e-7 para 1. Plote a loss vs LR. A LR máxima ótima fica logo antes de a loss começar a aumentar.

  3. Treine com aquecimento + cosseno mas varie a duração do aquecimento: 0%, 1%, 5%, 10%, 20% do total de passos. Encontre o ponto ideal onde o treinamento é mais estável.

  4. Implemente o cosine annealing com warm restarts (SGDR): reinicie a taxa de aprendizado para lr_max a cada T passos e decaia de novo. Compare com o cosseno padrão em uma execução de treinamento mais longa.

  5. Construa um "cirurgião de agendamento" que monitora a loss de treinamento e automaticamente troca de aquecimento para cosseno quando a loss se estabiliza, e reduz a lr se a loss estagnar por tempo demais.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
Taxa de aprendizado "Quão rápido o modelo aprende" O escalar que multiplica o gradiente para determinar o tamanho da atualização do parâmetro
Agendamento "Mudar a LR ao longo do tempo" Uma função que mapeia o passo de treinamento para a taxa de aprendizado, projetada para otimizar a convergência
Aquecimento "Comece com uma LR pequena" Aumentar linearmente a LR de quase-zero até o valor alvo ao longo dos primeiros N passos para estabilizar as estatísticas do otimizador
Cosine annealing "Decaimento suave da LR" Diminuir a LR seguindo uma curva cosseno de lr_max até lr_min ao longo do treinamento
Step decay "Cortar a LR em marcos" Multiplicar a LR por um fator (geralmente 0.1) em intervalos fixos de épocas
Política 1cycle "Subir e depois descer" O método de Leslie Smith de subir e depois descer a LR em um único ciclo para convergência mais rápida
Teste de faixa de LR "Encontrar a melhor taxa de aprendizado" Treinar brevemente enquanto aumenta a LR para encontrar o valor onde a loss começa a divergir
Cosseno com warm restarts "Reiniciar e repetir" Reiniciar periodicamente a LR para lr_max e decair de novo (SGDR)
Eta min "O piso da LR" A taxa de aprendizado mínima até a qual o agendamento decai
Taxa de aprendizado de pico "A LR máxima" A maior LR alcançada durante o treinamento, tipicamente após o aquecimento

Leitura Adicional

  • Loshchilov & Hutter, "SGDR: Stochastic Gradient Descent with Warm Restarts" (2017) -- introduziu o cosine annealing e os warm restarts
  • Smith, "Super-Convergence: Very Fast Training of Neural Networks Using Large Learning Rates" (2018) -- o artigo da política 1cycle
  • Touvron et al., "Llama 2: Open Foundation and Fine-Tuned Chat Models" (2023) -- documenta o agendamento de aquecimento + cosseno usado em escala
  • Goyal et al., "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour" (2017) -- a regra de escalonamento linear e o aquecimento para treinamento com batch grande
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).