mozak.tech Engenharia de IA Corporativa 57%

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

2.5 — Avaliação de Modelos Ajustados: Testes A/B e Detecção de Sobreajuste (Model Evaluation)

Objetivo da Aula

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

Implementar testar_ab(pergunta, referencia) com LLM-as-judge cego

Implementar analisar_overfitting(scores_treino, scores_valid) detectando gap treino/validação

Calcular ROUGE-L com LCS dinâmica para avaliação automática

Completar os 2 TODOs do starter Python

Interpretar resultados do harness de avaliação para decidir se fine-tuning vale a pena

Por que isso importa

“O modelo ficou melhor” não é uma avaliação — é uma opinião. ROUGE-L quantifica a sobreposição de texto com referência. LLM-as-judge avalia qualidade semântica. A/B test cego remove o viés de quem desenvolveu o modelo. Análise de overfitting identifica quando o modelo decorou o dataset de treino mas não generaliza.

Conceitos Fundamentais

TODO 1: testar_ab(pergunta, referencia)

def testar_ab(pergunta: str, referencia: str) -> dict:

    # 1. Gerar respostas de ambos os modelos

    resp_base = gerar_resposta_base(pergunta)

    resp_ft = gerar_resposta_ft(pergunta)

    

    # 2. Pedir ao Claude para escolher a melhor (sem revelar qual é qual)

    # Randomizar ordem para evitar viés posicional

    import random

    opcoes = [('A', resp_base, 'base'), ('B', resp_ft, 'ft')]

    if random.random() > 0.5:

        opcoes = [('A', resp_ft, 'ft'), ('B', resp_base, 'base')]

    

    response = client.messages.create(

        model="claude-haiku-4-5-20251001",

        max_tokens=200,

        messages=[{

            "role": "user",

            "content": f"""Pergunta: {pergunta}

Referência esperada: {referencia}



Resposta A: {opcoes[0][1][:300]}

Resposta B: {opcoes[1][1][:300]}



Qual resposta é mais precisa tecnicamente e mais próxima da referência?

Responda APENAS com: A, B, ou EMPATE, seguido de uma justificativa de 1 linha.

Exemplo: "A: mais completa e técnica"

""",

        }],

    )

    

    texto = response.content[0].text if response.content[0].type == "text" else "EMPATE"

    primeira_letra = texto.strip()[0].upper() if texto.strip() else 'E'

    

    vencedor_letra = primeira_letra if primeira_letra in ('A', 'B') else 'EMPATE'

    

    if vencedor_letra == 'EMPATE':

        vencedor_modelo = 'empate'

    else:

        # Descobrir qual modelo corresponde à letra vencedora

        idx_vencedor = 0 if vencedor_letra == 'A' else 1

        vencedor_modelo = opcoes[idx_vencedor][2]

    

    return {

        "vencedor": vencedor_modelo,

        "justificativa": texto.strip(),

        "resp_base_len": len(resp_base.split()),

        "resp_ft_len": len(resp_ft.split()),

    }

TODO 2: analisar_overfitting(scores_treino, scores_valid)

def analisar_overfitting(scores_treino: list[float], scores_valid: list[float]) -> dict:

    if not scores_treino or not scores_valid:

        return {"overfitting": False, "gap": 0.0, "recomendacao": "Dados insuficientes"}

    

    # 1. Calcular médias

    media_treino = sum(scores_treino) / len(scores_treino)

    media_valid = sum(scores_valid) / len(scores_valid)

    gap = media_treino - media_valid

    

    # 2. Analisar tendência nos últimos N checkpoints

    def tendencia(scores: list[float]) -> str:

        if len(scores) < 3:

            return "insuficiente"

        recentes = scores[-3:]

        if recentes[-1] > recentes[0] * 1.01:

            return "melhorando"

        elif recentes[-1] < recentes[0] * 0.99:

            return "piorando"

        return "estagnando"

    

    tendencia_treino = tendencia(scores_treino)

    tendencia_valid = tendencia(scores_valid)

    

    # 3. Detectar overfitting: gap > 2.0 pontos

    overfitting = gap > 2.0

    

    # 4. Recomendação

    if overfitting:

        recomendacao = (

            "Overfitting detectado. Ações: (1) Reduzir n_epochs, "

            "(2) Adicionar exemplos de validação ao dataset, "

            "(3) Aumentar regularização (weight_decay), "

            "(4) Usar early stopping"

        )

    elif tendencia_valid == "piorando":

        recomendacao = "Validação piorando — possível overfitting iminente. Considere early stopping."

    elif tendencia_valid == "melhorando":

        recomendacao = "Modelo generalizando bem. Continue treinamento ou avalie com mais épocas."

    else:

        recomendacao = "Treinamento estável. Avalie qualidade com golden set."

    

    return {

        "overfitting": overfitting,

        "gap": round(gap, 2),

        "media_treino": round(media_treino, 2),

        "media_valid": round(media_valid, 2),

        "tendencia_treino": tendencia_treino,

        "tendencia_valid": tendencia_valid,

        "recomendacao": recomendacao,

    }

Aprofundamento Técnico

ROUGE-L: Métrica Baseada em LCS

# ROUGE-L usa Longest Common Subsequence (LCS) — não precisa ser contíguo

# Exemplo:

# Hipótese: "modelos de linguagem processam tokens sequencialmente"

# Referência: "modelos processam sequências de tokens"

# LCS: "modelos" "processam" "tokens" → 3 palavras comuns



# ROUGE-L = F1 de precision e recall do LCS

# Precision = |LCS| / len(hipótese)

# Recall = |LCS| / len(referência)

# F1 = 2 × Precision × Recall / (Precision + Recall)



# Valores típicos:

# ROUGE-L > 0.5: boa sobreposição

# ROUGE-L > 0.3: aceitável para geração livre

# ROUGE-L < 0.2: respostas muito diferentes da referência

LLM-as-Judge: Vantagens e Limitações

Vantagens:

✅ Avalia semântica, não apenas sobreposição de palavras

✅ Captura qualidade de raciocínio

✅ Escala para milhares de avaliações



Limitações:

❌ Juiz pode ter viés por comprimento (respostas longas parecem mais completas)

❌ Juiz pode preferir estilo similar ao próprio treinamento

❌ Não funciona bem para tarefas com única resposta correta (matemática)

Exemplos Anotados

Exemplo 1: A/B Test com randomização

resultado = testar_ab(

    "O que é temperatura em LLMs?",

    "Temperature controla aleatoriedade: 0=determinístico, 1=mais variado."

)

# → {

#   "vencedor": "ft",  ← FT geralmente mais conciso e técnico

#   "justificativa": "A: direta, técnica, inclui exemplo numérico",

#   "resp_base_len": 87,

#   "resp_ft_len": 23,  ← fine-tuned é 4x mais conciso

# }

Exemplo 2: Detecção de Overfitting

scores_treino = [6.2, 7.1, 7.8, 8.3, 8.9, 9.2, 9.5]  # subindo sempre

scores_valid =  [6.1, 6.8, 7.2, 7.3, 7.1, 6.8, 6.5]  # piorou depois de época 3



resultado = analisar_overfitting(scores_treino, scores_valid)

# → {"overfitting": True, "gap": 3.0, "recomendacao": "Overfitting detectado. Reduzir n_epochs..."}

# Melhor checkpoint estava na época 3 (score_valid=7.2)

Padrões e Armadilhas

Padrões

Padrão 1: Golden set fora do treino O golden set deve ser separado do dataset de treino ANTES de qualquer treinamento.

Padrão 2: Múltiplas métricas ROUGE-L para cobertura + LLM-judge para qualidade + A/B test para comparação.

Armadilhas

⚠️ Armadilha 1: A/B test sem randomização da ordem

# ERRADO: sempre apresentar base como "A"

# LLMs tendem a preferir a primeira opção apresentada

# CORRETO: randomizar qual modelo aparece como A/B

⚠️ Armadilha 2: Gap de 2.0 pontos como threshold fixo

# Ajuste o threshold conforme a escala:

# Score 1-10: gap > 1.5 = overfitting

# Score 0-1: gap > 0.15 = overfitting

⚠️ Armadilha 3: LLM-judge com modelo igual ao modelo avaliado

Avaliar gpt-4o-mini FT com gpt-4o-mini base → juiz pode ter viés

Use Claude como juiz para modelos OpenAI, e vice-versa
⚗ 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 — testar_ab(pergunta, referencia): gere resp_base e resp_ft, randomize ordem, peça ao LLM “A, B, ou EMPATE + justificativa”, mapeie letra vencedora de volta para modelo.

TODO 2 — analisar_overfitting(scores_treino, scores_valid): calcule gap = media_treino - media_valid, overfitting se gap > 2.0, analise tendência dos últimos 3 pontos.

Agora você está pronto para o lab.