mozak.tech Engenharia de IA Corporativa 50%

Parte II — Especialização de Modelos: Dados, Ajuste e Avaliação

2.3 — Ajuste Fino via API: Hiperparâmetros, Monitoramento e Iteração (Fine-tuning API)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Configurar hyperparameters no job de fine-tuning (n_epochs, batch_size, learning_rate_multiplier)

Implementar polling de status com client.fine_tuning.jobs.retrieve() em loop

Usar o modo --dry-run como salvaguarda antes de executar de verdade

Completar os 2 TODOs do starter Python

Interpretar status do job: validating_files → queued → running → succeeded/failed

Por que isso importa

Fine-tuning custa dinheiro e tempo. Um job mal configurado (n_epochs=20 quando 3 seriam suficientes) pode custar 6x mais e produzir modelo com overfitting. Polling sem timeout pode fazer um script rodar indefinidamente se o job travar.

Conceitos Fundamentais

Hiperparâmetros de Fine-Tuning

Fundamento: Hiperparâmetros e Ciclo do Job

n_epochs: quantas vezes o modelo vê o dataset completo. Mais épocas = mais custo de treino e risco de overfitting (decorar em vez de generalizar). learning_rate_multiplier: escala a taxa de aprendizado base — maior = ajuste mais agressivo por epoch, maior risco de perder qualidade do modelo base. batch_size: quantos exemplos processa antes de ajustar pesos — maior batch tende a generalizar melhor mas usa mais memória. O job é assíncrono: vai de validating_files → queued → running → succeeded (ou failed). Monitore o gap entre erro de treino e de validação: se treino cai mas validação sobe, o modelo está overfitting — pare mais cedo ou reduza n_epochs.

Parâmetro

Padrão

Quando ajustar

n_epochs

"auto" (1-4)

Aumente para dataset pequeno (<100 exemplos)

batch_size

"auto"

Deixe “auto” sempre — OpenAI otimiza

learning_rate_multiplier

1.0

Diminua (0.1) se modelo muito diferente do base

"auto" é quase sempre a melhor escolha para batch_size.

TODO 1: criar_job_finetuning(file_id, modelo_base)

def criar_job_finetuning(file_id: str, modelo_base: str = "gpt-4o-mini-2024-07-18") -> str:

    if DRY_RUN:

        print(f"  [DRY-RUN] Criaria job:")

        print(f"    modelo={modelo_base}")

        print(f"    file={file_id}")

        print(f"    n_epochs=3, batch_size=auto, lr=1.0")

        print(f"    suffix=suporte-ia-v1")

        return "ftjob-SIMULADO456"

    

    from openai import OpenAI

    client = OpenAI()

    response = client.fine_tuning.jobs.create(

        training_file=file_id,

        model=modelo_base,

        hyperparameters={

            'n_epochs': 3,

            'batch_size': 'auto',

            'learning_rate_multiplier': 1.0,

        },

        suffix='suporte-ia-v1',  # o modelo será nomeado ft:gpt-4o-mini:org:suporte-ia-v1:HASH

    )

    print(f"  [JOB] job_id: {response.id} | status: {response.status}")

    return response.id

TODO 2: monitorar_job(job_id) com Polling

def monitorar_job(job_id: str) -> dict:

    if DRY_RUN:

        print(f"  [DRY-RUN] Monitoraria job {job_id}")

        return {"status": "simulado", "modelo": "ft:gpt-4o-mini:org:suporte-ia:SIMULADO"}

    

    from openai import OpenAI

    client = OpenAI()

    

    TIMEOUT_MINUTOS = 60

    POLL_INTERVALO = 30  # segundos

    inicio = time.time()

    

    print(f"  [POLL] Monitorando job {job_id}...")

    

    while True:

        # 1. Verificar timeout

        elapsed = (time.time() - inicio) / 60

        if elapsed > TIMEOUT_MINUTOS:

            return {"status": "timeout", "erro": f"Job não concluiu em {TIMEOUT_MINUTOS} minutos"}

        

        # 2. Recuperar status atual

        job = client.fine_tuning.jobs.retrieve(job_id)

        

        print(f"  [POLL] Status: {job.status} ({elapsed:.1f}min)")

        

        # 3. Verificar estado terminal

        if job.status == "succeeded":

            return {"status": "succeeded", "modelo": job.fine_tuned_model}

        

        if job.status == "failed":

            erro = str(job.error) if job.error else "Erro desconhecido"

            return {"status": "failed", "erro": erro}

        

        if job.status == "cancelled":

            return {"status": "cancelled", "erro": "Job foi cancelado"}

        

        # 4. Aguardar para próximo poll (estados não-terminais: validating_files, queued, running)

        print(f"  [POLL] Aguardando {POLL_INTERVALO}s...")

        time.sleep(POLL_INTERVALO)

Aprofundamento Técnico

Ciclo de Vida do Job

upload → file-XXXX (processamento assíncrono)

criar_job → ftjob-XXXX

  └── validating_files (5-10 min)

      └── queued (espera em fila)

          └── running (treinamento ativo)

              ├── succeeded → fine_tuned_model = "ft:gpt-4o-mini:org:nome:HASH"

              └── failed → error.message com detalhe

Estimativa de Custo por n_epochs

Dataset: 1000 exemplos × 300 tokens médios = 300K tokens



n_epochs=1: 300K tokens × $0.008/1K = $2.40

n_epochs=3: 900K tokens × $0.008/1K = $7.20  ← recomendado

n_epochs=5: 1.5M tokens × $0.008/1K = $12.00

Mais épocas não é sempre melhor — verifique métricas de validação.

suffix e Identificação do Modelo

suffix="suporte-ia-v1" → modelo nomeado:

"ft:gpt-4o-mini-2024-07-18:sua-org:suporte-ia-v1:abcd1234"



Sem suffix:

"ft:gpt-4o-mini-2024-07-18:sua-org:abcd1234"  ← difícil identificar

Use sufixos descritivos: v1, prod, experimento-rag, etc.

Exemplos Anotados

Exemplo 1: Dry-run completo

$ python starter.py  # dry-run por padrão



=== FINE-TUNING OPENAI API (DRY-RUN) ===



  [UPLOAD] dataset_exemplo.jsonl → file-SIMULADO123

  [DRY-RUN] Criaria job:

    modelo=gpt-4o-mini-2024-07-18

    file=file-SIMULADO123

    n_epochs=3, batch_size=auto, lr=1.0

    suffix=suporte-ia-v1

  [DRY-RUN] Monitoraria job ftjob-SIMULADO456

  Resultado: {'status': 'simulado', 'modelo': 'ft:gpt-4o-mini:org:suporte-ia:SIMULADO'}



Concluído. Use --live para executar de verdade.

Padrões e Armadilhas

Padrões

Padrão 1: Sempre dry-run primeiro

python starter.py        # dry-run: sem custos, sem API calls reais

python starter.py --live # execução real: cobra dinheiro

Padrão 2: Cancelar job mal iniciado

client.fine_tuning.jobs.cancel(job_id)  # cancela se job ainda não está "running"

Armadilhas

⚠️ Armadilha 1: Poll sem sleep → rate limit

# ERRADO: loop sem sleep

while True:

    job = client.fine_tuning.jobs.retrieve(job_id)  # 429 em segundos



# CORRETO: aguardar entre polls

time.sleep(30)

⚠️ Armadilha 2: n_epochs alto com dataset grande 1000 exemplos × 5 épocas × 300 tokens = 1.5M tokens = $12. Fine-tuning de 3 épocas quase sempre é suficiente.

⚠️ Armadilha 3: Arquivo inválido não detectado até validating_files Valide o JSONL antes de fazer upload. Cada linha deve ter {"messages": [...]} com roles corretos.

⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

TODO 1 — criar_job_finetuning(file_id, modelo_base): adicione hyperparameters={'n_epochs': 3, 'batch_size': 'auto', 'learning_rate_multiplier': 1.0} e suffix='suporte-ia-v1'.

TODO 2 — monitorar_job(job_id): loop com client.fine_tuning.jobs.retrieve(job_id), time.sleep(30), verificar job.status in ('succeeded', 'failed', 'cancelled'), timeout após 60 minutos.

Agora você está pronto para o lab.