Phase 02 - Lesson 09

Avaliação do modelo

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

Um modelo é tão bom quanto a forma como você o mede.

Tipo: Construir Idiomas: Python Pré-requisitos: Fase 1 (Probabilidade e Distribuições, Estatística para ML), Fase 2 Lições 1-8 Tempo: ~90 minutos

Objetivos de aprendizagem

  • Implementar validação cruzada K-fold e estratificada K-fold do zero e explicar por que a estratificação é importante para dados desequilibrados
  • Calcular precisão, recall, F1, AUC-ROC e métricas de regressão (MSE, RMSE, MAE, R-quadrado) do zero
  • Interpretar curvas de aprendizagem para diagnosticar se um modelo sofre de alto viés ou alta variância
  • Identificar erros comuns de avaliação, incluindo vazamento de dados, seleção errada de métricas e contaminação de conjuntos de testes

O problema

Você treinou um modelo. Obtém 95% de precisão em seus dados. Isso é bom?

Talvez. Talvez não. Se 95% dos seus dados pertencem a uma classe, um modelo que sempre prevê essa classe obtém 95% de precisão e é completamente inútil. Se você avaliou com base nos mesmos dados com os quais treinou, o número de 95% não tem sentido porque o modelo apenas memorizou as respostas. Se o seu conjunto de dados tiver um componente de tempo e você embaralhar aleatoriamente antes da divisão, seu modelo poderá estar usando dados futuros para prever o passado.

A avaliação do modelo é onde a maioria dos projetos de ML erram. A métrica errada faz com que um modelo ruim pareça bom. A divisão errada permite que um modelo trapaceie. A comparação errada faz você escolher o pior modelo. Acertar na avaliação não é opcional. É a diferença entre um modelo que funciona em produção e outro que falha no momento em que vê dados reais.

O Conceito

Treinar, Validar, Testar

flowchart LR
    A[Full Dataset] --> B[Train Set 60-70%]
    A --> C[Validation Set 15-20%]
    A --> D[Test Set 15-20%]
    B --> E[Fit Model]
    E --> C
    C --> F[Tune Hyperparameters]
    F --> E
    F --> G[Final Model]
    G --> D
    D --> H[Report Performance]

Três divisões, três propósitos:

  • Conjunto de treinamento: o modelo aprende com esses dados. Ele vê esses exemplos durante o treinamento.
  • Conjunto de validação: usado para ajustar hiperparâmetros e selecionar entre modelos. O modelo nunca treina com esses dados, mas suas decisões são influenciadas por eles.
  • Conjunto de testes: tocado exatamente uma vez, no final, para relatar o desempenho final. Se você observar o desempenho do teste e depois voltar para alterar seu modelo, ele não será mais um conjunto de testes. Tornou-se um segundo conjunto de validação.

O conjunto de testes é a sua garantia de que o desempenho relatado reflete o desempenho do modelo em dados verdadeiramente invisíveis.

Validação cruzada K-Fold

Com conjuntos de dados pequenos, uma única divisão de treinamento/validação desperdiça dados e fornece estimativas ruidosas. A validação cruzada K-fold usa todos os dados para treinamento e validação:

flowchart TB
    subgraph Fold1["Fold 1"]
        direction LR
        V1["Val"] --- T1a["Train"] --- T1b["Train"] --- T1c["Train"] --- T1d["Train"]
    end
    subgraph Fold2["Fold 2"]
        direction LR
        T2a["Train"] --- V2["Val"] --- T2b["Train"] --- T2c["Train"] --- T2d["Train"]
    end
    subgraph Fold3["Fold 3"]
        direction LR
        T3a["Train"] --- T3b["Train"] --- V3["Val"] --- T3c["Train"] --- T3d["Train"]
    end
    subgraph Fold4["Fold 4"]
        direction LR
        T4a["Train"] --- T4b["Train"] --- T4c["Train"] --- V4["Val"] --- T4d["Train"]
    end
    subgraph Fold5["Fold 5"]
        direction LR
        T5a["Train"] --- T5b["Train"] --- T5c["Train"] --- T5d["Train"] --- V5["Val"]
    end
    Fold1 --> R["Average scores"]
    Fold2 --> R
    Fold3 --> R
    Fold4 --> R
    Fold5 --> R
  1. Divida os dados em K dobras de tamanhos iguais
  2. Para cada dobra, treine nas dobras K-1 e valide nas dobras restantes
  3. Média das pontuações de validação K

K=5 ou K=10 são escolhas padrão. Cada ponto de dados é usado para validação exatamente uma vez. A pontuação média é uma estimativa mais estável do que qualquer divisão única.

Dobra K estratificada: preserva a distribuição de classes em cada dobra. Se o seu conjunto de dados for 70% classe A e 30% classe B, cada dobra terá aproximadamente a mesma proporção. Isto é importante para conjuntos de dados desequilibrados, onde uma divisão aleatória pode colocar todas as amostras minoritárias em uma única dobra.

Métricas de Classificação

Matriz de confusão: a base. Para classificação binária:

Previsto Positivo Negativo previsto
Na verdade positivo Verdadeiro Positivo (TP) Falso Negativo (FN)
Na verdade negativo Falso Positivo (FP) Verdadeiro Negativo (TN)

Desta matriz, seguem todas as outras métricas:

  • Precisão = (TP + TN) / (TP + TN + FP + FN). Fração de previsões corretas. Enganador quando as classes estão desequilibradas.
  • Precisão = TP / (TP + FP). De todas as coisas previstas como positivas, quantas realmente foram? Use quando falsos positivos custam caro (por exemplo, filtro de spam marcando e-mail real como spam).
  • Recall (sensibilidade) = TP / (TP + FN). De todos os aspectos positivos reais, quantos capturamos? Use quando falsos negativos são caros (por exemplo, rastreamento de câncer sem tumor).
  • F1 pontuação = 2 * precisão * recall / (precisão + recall). Média harmônica de precisão e recall. Equilibra ambos quando nenhum deles domina claramente.
  • AUC-ROC: Área sob a curva característica de operação do receptor. Representa graficamente a taxa de verdadeiros positivos versus a taxa de falsos positivos em vários limites de classificação. AUC = 0,5 significa adivinhação aleatória, AUC = 1,0 significa separação perfeita. Independente de limite: mede quão bem o modelo classifica os positivos acima dos negativos, independentemente do ponto de corte escolhido.

Métricas de regressão

  • MSE (erro quadrático médio) = média((y_true - y_pred)^2). Penaliza grandes erros quadraticamente. Sensível a valores discrepantes.
  • RMSE (erro quadrático médio) = sqrt(MSE). Mesmas unidades da variável de destino. Mais fácil de interpretar do que MSE.
  • MAE (Erro Médio Absoluto) = média(|y_true - y_pred|). Trata todos os erros linearmente. Mais robusto para valores discrepantes do que MSE.
  • R-quadrado = 1 - SS_res / SS_tot, onde SS_res = soma((y_true - y_pred)^2) e SS_tot = soma((y_true - y_mean)^2). Fração de variância explicada pelo modelo. R ^ 2 = 1,0 é perfeito. R ^ 2 = 0,0 significa que o modelo não é melhor do que sempre prever a média. R^2 pode ser negativo se o modelo for pior que a média.

Curvas de aprendizagem

Plote as pontuações de treinamento e validação em função do tamanho do conjunto de treinamento:

  • Viés alto (underfitting): ambas as curvas convergem para uma pontuação baixa. Adicionar mais dados não ajudará. Você precisa de um modelo mais complexo.
  • Alta variância (overfitting): a pontuação de treinamento é alta, mas a pontuação de validação é muito mais baixa. A diferença entre eles é grande. Adicionar mais dados deve ajudar.

Curvas de validação

Plote as pontuações de treinamento e validação em função de um hiperparâmetro:

  • Em baixa complexidade: ambas as pontuações são baixas (underfitting)
  • Na complexidade certa: ambas as pontuações são altas e próximas
  • Em alta complexidade: a pontuação de treinamento permanece alta, mas a pontuação de validação cai (overfitting)

O valor ideal do hiperparâmetro é onde a pontuação de validação atinge o pico.

Erros comuns de avaliação

Vazamento de dados: informações do conjunto de testes vazam para o treinamento. Exemplos: ajustar um escalonador no conjunto de dados completo antes da divisão, incluindo dados futuros na previsão de série temporal, usando um recurso derivado do destino. Sempre divida primeiro e depois pré-processe.

Desequilíbrio de classes: 99% das transações são legítimas, 1% são fraudes. Um modelo que sempre prevê “legítimo” obtém 99% de precisão. Use precisão, recall, F1 ou AUC-ROC.

Métrica errada: otimizar a precisão quando você deveria otimizar o recall (diagnóstico médico) ou otimizar o RMSE quando seus dados tiverem valores discrepantes pesados ​​(use o MAE).

Não usar divisões estratificadas: com dados desequilibrados, uma divisão aleatória pode colocar muito poucas amostras minoritárias na dobra de validação, gerando estimativas instáveis.

Testando com muita frequência: toda vez que você observa o desempenho do teste e faz ajustes, você se ajusta demais ao conjunto de testes. O conjunto de teste é descartável.

Construa

Etapa 1: divisão de treinamento/validação/teste

import random
import math


def train_val_test_split(X, y, train_ratio=0.6, val_ratio=0.2, seed=42):
    random.seed(seed)
    n = len(X)
    indices = list(range(n))
    random.shuffle(indices)

    train_end = int(n * train_ratio)
    val_end = int(n * (train_ratio + val_ratio))

    train_idx = indices[:train_end]
    val_idx = indices[train_end:val_end]
    test_idx = indices[val_end:]

    X_train = [X[i] for i in train_idx]
    y_train = [y[i] for i in train_idx]
    X_val = [X[i] for i in val_idx]
    y_val = [y[i] for i in val_idx]
    X_test = [X[i] for i in test_idx]
    y_test = [y[i] for i in test_idx]

    return X_train, y_train, X_val, y_val, X_test, y_test

Etapa 2: validação cruzada de dobras K e dobras K estratificadas

def kfold_split(n, k=5, seed=42):
    random.seed(seed)
    indices = list(range(n))
    random.shuffle(indices)

    fold_size = n // k
    folds = []

    for i in range(k):
        start = i * fold_size
        end = start + fold_size if i < k - 1 else n
        val_idx = indices[start:end]
        train_idx = indices[:start] + indices[end:]
        folds.append((train_idx, val_idx))

    return folds


def stratified_kfold_split(y, k=5, seed=42):
    random.seed(seed)

    class_indices = {}
    for i, label in enumerate(y):
        class_indices.setdefault(label, []).append(i)

    for label in class_indices:
        random.shuffle(class_indices[label])

    folds = [{"train": [], "val": []} for _ in range(k)]

    for label, indices in class_indices.items():
        fold_size = len(indices) // k
        for i in range(k):
            start = i * fold_size
            end = start + fold_size if i < k - 1 else len(indices)
            val_part = indices[start:end]
            train_part = indices[:start] + indices[end:]
            folds[i]["val"].extend(val_part)
            folds[i]["train"].extend(train_part)

    return [(f["train"], f["val"]) for f in folds]


def cross_validate(X, y, model_fn, k=5, metric_fn=None, stratified=False):
    n = len(X)

    if stratified:
        folds = stratified_kfold_split(y, k)
    else:
        folds = kfold_split(n, k)

    scores = []
    for train_idx, val_idx in folds:
        X_train = [X[i] for i in train_idx]
        y_train = [y[i] for i in train_idx]
        X_val = [X[i] for i in val_idx]
        y_val = [y[i] for i in val_idx]

        model = model_fn()
        model.fit(X_train, y_train)
        predictions = [model.predict(x) for x in X_val]

        if metric_fn:
            score = metric_fn(y_val, predictions)
        else:
            score = sum(1 for yt, yp in zip(y_val, predictions) if yt == yp) / len(y_val)
        scores.append(score)

    return scores

Etapa 3: Matriz de confusão e métricas de classificação

def confusion_matrix(y_true, y_pred):
    tp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 1)
    tn = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 0)
    fp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 1)
    fn = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 0)
    return tp, tn, fp, fn


def accuracy(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    total = tp + tn + fp + fn
    return (tp + tn) / total if total > 0 else 0.0


def precision(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    return tp / (tp + fp) if (tp + fp) > 0 else 0.0


def recall(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    return tp / (tp + fn) if (tp + fn) > 0 else 0.0


def f1_score(y_true, y_pred):
    p = precision(y_true, y_pred)
    r = recall(y_true, y_pred)
    return 2 * p * r / (p + r) if (p + r) > 0 else 0.0


def roc_curve(y_true, y_scores):
    thresholds = sorted(set(y_scores), reverse=True)
    tpr_list = []
    fpr_list = []

    total_positives = sum(y_true)
    total_negatives = len(y_true) - total_positives

    for threshold in thresholds:
        y_pred = [1 if s >= threshold else 0 for s in y_scores]
        tp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 1)
        fp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 1)

        tpr = tp / total_positives if total_positives > 0 else 0.0
        fpr = fp / total_negatives if total_negatives > 0 else 0.0

        tpr_list.append(tpr)
        fpr_list.append(fpr)

    return fpr_list, tpr_list, thresholds


def auc_roc(y_true, y_scores):
    fpr_list, tpr_list, _ = roc_curve(y_true, y_scores)

    pairs = sorted(zip(fpr_list, tpr_list))
    fpr_sorted = [p[0] for p in pairs]
    tpr_sorted = [p[1] for p in pairs]

    area = 0.0
    for i in range(1, len(fpr_sorted)):
        width = fpr_sorted[i] - fpr_sorted[i - 1]
        height = (tpr_sorted[i] + tpr_sorted[i - 1]) / 2
        area += width * height

    return area

Etapa 4: Métricas de regressão

def mse(y_true, y_pred):
    n = len(y_true)
    return sum((yt - yp) ** 2 for yt, yp in zip(y_true, y_pred)) / n


def rmse(y_true, y_pred):
    return math.sqrt(mse(y_true, y_pred))


def mae(y_true, y_pred):
    n = len(y_true)
    return sum(abs(yt - yp) for yt, yp in zip(y_true, y_pred)) / n


def r_squared(y_true, y_pred):
    mean_y = sum(y_true) / len(y_true)
    ss_res = sum((yt - yp) ** 2 for yt, yp in zip(y_true, y_pred))
    ss_tot = sum((yt - mean_y) ** 2 for yt in y_true)
    if ss_tot == 0:
        return 0.0
    return 1.0 - ss_res / ss_tot

Etapa 5: Curvas de aprendizado

def learning_curve(X, y, model_fn, metric_fn, train_sizes=None, val_ratio=0.2, seed=42):
    random.seed(seed)
    n = len(X)
    indices = list(range(n))
    random.shuffle(indices)

    val_size = int(n * val_ratio)
    val_idx = indices[:val_size]
    pool_idx = indices[val_size:]

    X_val = [X[i] for i in val_idx]
    y_val = [y[i] for i in val_idx]

    if train_sizes is None:
        train_sizes = [int(len(pool_idx) * r) for r in [0.1, 0.2, 0.4, 0.6, 0.8, 1.0]]

    train_scores = []
    val_scores = []

    for size in train_sizes:
        subset = pool_idx[:size]
        X_train = [X[i] for i in subset]
        y_train = [y[i] for i in subset]

        model = model_fn()
        model.fit(X_train, y_train)

        train_pred = [model.predict(x) for x in X_train]
        val_pred = [model.predict(x) for x in X_val]

        train_scores.append(metric_fn(y_train, train_pred))
        val_scores.append(metric_fn(y_val, val_pred))

    return train_sizes, train_scores, val_scores

Etapa 6: um classificador simples para teste, além da demonstração completa

class SimpleLogistic:
    def __init__(self, lr=0.1, epochs=100):
        self.lr = lr
        self.epochs = epochs
        self.weights = None
        self.bias = 0.0

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

    def fit(self, X, y):
        n_features = len(X[0])
        self.weights = [0.0] * n_features
        self.bias = 0.0

        for _ in range(self.epochs):
            for xi, yi in zip(X, y):
                z = sum(w * x for w, x in zip(self.weights, xi)) + self.bias
                pred = self.sigmoid(z)
                error = yi - pred
                for j in range(n_features):
                    self.weights[j] += self.lr * error * xi[j]
                self.bias += self.lr * error

    def predict_proba(self, x):
        z = sum(w * xi for w, xi in zip(self.weights, x)) + self.bias
        return self.sigmoid(z)

    def predict(self, x):
        return 1 if self.predict_proba(x) >= 0.5 else 0


class SimpleLinearRegression:
    def __init__(self, lr=0.001, epochs=200):
        self.lr = lr
        self.epochs = epochs
        self.weights = None
        self.bias = 0.0

    def fit(self, X, y):
        n_features = len(X[0])
        self.weights = [0.0] * n_features
        self.bias = 0.0
        n = len(X)

        for _ in range(self.epochs):
            for xi, yi in zip(X, y):
                pred = sum(w * x for w, x in zip(self.weights, xi)) + self.bias
                error = yi - pred
                for j in range(n_features):
                    self.weights[j] += self.lr * error * xi[j] / n
                self.bias += self.lr * error / n

    def predict(self, x):
        return sum(w * xi for w, xi in zip(self.weights, x)) + self.bias


def standardize(values):
    n = len(values)
    mean = sum(values) / n
    var = sum((v - mean) ** 2 for v in values) / n
    std = math.sqrt(var) if var > 0 else 1.0
    return [(v - mean) / std for v in values], mean, std


def make_classification_data(n=300, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        x1 = random.gauss(0, 1)
        x2 = random.gauss(0, 1)
        label = 1 if (x1 + x2 + random.gauss(0, 0.5)) > 0 else 0
        X.append([x1, x2])
        y.append(label)
    return X, y


def make_regression_data(n=200, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        x1 = random.uniform(0, 10)
        x2 = random.uniform(0, 5)
        target = 3 * x1 + 2 * x2 + random.gauss(0, 2)
        X.append([x1, x2])
        y.append(target)
    return X, y


def make_imbalanced_data(n=300, minority_ratio=0.05, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        if random.random() < minority_ratio:
            x1 = random.gauss(3, 0.5)
            x2 = random.gauss(3, 0.5)
            label = 1
        else:
            x1 = random.gauss(0, 1)
            x2 = random.gauss(0, 1)
            label = 0
        X.append([x1, x2])
        y.append(label)
    return X, y


if __name__ == "__main__":
    X_clf, y_clf = make_classification_data(300)

    print("=== Train/Validation/Test Split ===")
    X_train, y_train, X_val, y_val, X_test, y_test = train_val_test_split(X_clf, y_clf)
    print(f"  Train: {len(X_train)}, Val: {len(X_val)}, Test: {len(X_test)}")
    print(f"  Train class distribution: {sum(y_train)}/{len(y_train)} positive")
    print(f"  Val class distribution: {sum(y_val)}/{len(y_val)} positive")

    model = SimpleLogistic(lr=0.1, epochs=200)
    model.fit(X_train, y_train)

    print("\n=== Classification Metrics ===")
    y_pred = [model.predict(x) for x in X_test]
    tp, tn, fp, fn = confusion_matrix(y_test, y_pred)
    print(f"  Confusion matrix: TP={tp}, TN={tn}, FP={fp}, FN={fn}")
    print(f"  Accuracy:  {accuracy(y_test, y_pred):.4f}")
    print(f"  Precision: {precision(y_test, y_pred):.4f}")
    print(f"  Recall:    {recall(y_test, y_pred):.4f}")
    print(f"  F1 Score:  {f1_score(y_test, y_pred):.4f}")

    y_scores = [model.predict_proba(x) for x in X_test]
    auc = auc_roc(y_test, y_scores)
    print(f"  AUC-ROC:   {auc:.4f}")

    print("\n=== K-Fold Cross-Validation (K=5) ===")
    cv_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        k=5,
        metric_fn=accuracy,
    )
    mean_cv = sum(cv_scores) / len(cv_scores)
    std_cv = math.sqrt(sum((s - mean_cv) ** 2 for s in cv_scores) / len(cv_scores))
    print(f"  Fold scores: {[round(s, 4) for s in cv_scores]}")
    print(f"  Mean: {mean_cv:.4f} (+/- {std_cv:.4f})")

    print("\n=== Stratified K-Fold Cross-Validation (K=5) ===")
    strat_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        k=5,
        metric_fn=accuracy,
        stratified=True,
    )
    strat_mean = sum(strat_scores) / len(strat_scores)
    strat_std = math.sqrt(sum((s - strat_mean) ** 2 for s in strat_scores) / len(strat_scores))
    print(f"  Fold scores: {[round(s, 4) for s in strat_scores]}")
    print(f"  Mean: {strat_mean:.4f} (+/- {strat_std:.4f})")

    print("\n=== Imbalanced Data: Why Accuracy Lies ===")
    X_imb, y_imb = make_imbalanced_data(300, minority_ratio=0.05)
    positives = sum(y_imb)
    print(f"  Class distribution: {positives} positive, {len(y_imb) - positives} negative ({positives/len(y_imb)*100:.1f}% positive)")

    always_negative = [0] * len(y_imb)
    print(f"  Always-negative baseline:")
    print(f"    Accuracy:  {accuracy(y_imb, always_negative):.4f}")
    print(f"    Precision: {precision(y_imb, always_negative):.4f}")
    print(f"    Recall:    {recall(y_imb, always_negative):.4f}")
    print(f"    F1 Score:  {f1_score(y_imb, always_negative):.4f}")

    X_tr_i, y_tr_i, X_v_i, y_v_i, X_te_i, y_te_i = train_val_test_split(X_imb, y_imb)
    model_imb = SimpleLogistic(lr=0.5, epochs=500)
    model_imb.fit(X_tr_i, y_tr_i)
    y_pred_imb = [model_imb.predict(x) for x in X_te_i]
    print(f"\n  Trained model on imbalanced data:")
    print(f"    Accuracy:  {accuracy(y_te_i, y_pred_imb):.4f}")
    print(f"    Precision: {precision(y_te_i, y_pred_imb):.4f}")
    print(f"    Recall:    {recall(y_te_i, y_pred_imb):.4f}")
    print(f"    F1 Score:  {f1_score(y_te_i, y_pred_imb):.4f}")

    print("\n=== Regression Metrics ===")
    X_reg, y_reg = make_regression_data(200)

    col0 = [x[0] for x in X_reg]
    col1 = [x[1] for x in X_reg]
    col0_s, m0, s0 = standardize(col0)
    col1_s, m1, s1 = standardize(col1)
    X_reg_scaled = [[col0_s[i], col1_s[i]] for i in range(len(X_reg))]

    X_tr_r, y_tr_r, X_v_r, y_v_r, X_te_r, y_te_r = train_val_test_split(X_reg_scaled, y_reg)
    reg_model = SimpleLinearRegression(lr=0.01, epochs=500)
    reg_model.fit(X_tr_r, y_tr_r)
    y_pred_r = [reg_model.predict(x) for x in X_te_r]

    print(f"  MSE:       {mse(y_te_r, y_pred_r):.4f}")
    print(f"  RMSE:      {rmse(y_te_r, y_pred_r):.4f}")
    print(f"  MAE:       {mae(y_te_r, y_pred_r):.4f}")
    print(f"  R-squared: {r_squared(y_te_r, y_pred_r):.4f}")

    mean_baseline = [sum(y_tr_r) / len(y_tr_r)] * len(y_te_r)
    print(f"\n  Mean baseline:")
    print(f"    MSE:       {mse(y_te_r, mean_baseline):.4f}")
    print(f"    R-squared: {r_squared(y_te_r, mean_baseline):.4f}")

    print("\n=== Learning Curve ===")
    sizes, train_sc, val_sc = learning_curve(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        metric_fn=accuracy,
    )
    print(f"  {'Size':>6} {'Train':>8} {'Val':>8}")
    for s, tr, va in zip(sizes, train_sc, val_sc):
        print(f"  {s:>6} {tr:>8.4f} {va:>8.4f}")

    print("\n=== Statistical Model Comparison ===")
    model_a_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=100),
        k=5, metric_fn=accuracy,
    )
    model_b_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=500),
        k=5, metric_fn=accuracy,
    )
    diffs = [a - b for a, b in zip(model_a_scores, model_b_scores)]
    mean_diff = sum(diffs) / len(diffs)
    std_diff = math.sqrt(sum((d - mean_diff) ** 2 for d in diffs) / len(diffs))
    t_stat = mean_diff / (std_diff / math.sqrt(len(diffs))) if std_diff > 0 else 0.0
    print(f"  Model A (100 epochs) mean: {sum(model_a_scores)/len(model_a_scores):.4f}")
    print(f"  Model B (500 epochs) mean: {sum(model_b_scores)/len(model_b_scores):.4f}")
    print(f"  Mean difference: {mean_diff:.4f}")
    print(f"  Paired t-statistic: {t_stat:.4f}")
    print(f"  (|t| > 2.78 for significance at p<0.05 with df=4)")

Use-o

Com scikit-learn, a avaliação é incorporada ao fluxo de trabalho:

from sklearn.model_selection import cross_val_score, StratifiedKFold, learning_curve
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    roc_auc_score, confusion_matrix, mean_squared_error, r2_score,
)
from sklearn.linear_model import LogisticRegression

model = LogisticRegression()
scores = cross_val_score(model, X, y, cv=StratifiedKFold(5), scoring="f1")

As versões do zero mostram exatamente o que a validação cruzada faz (sem mágica, apenas for-loops e rastreamento de índice), como cada métrica é calculada (apenas contando TP/FP/TN/FN) e por que a estratificação é importante (preservando as proporções de classe em cada dobra). As versões da biblioteca adicionam paralelismo, mais opções de pontuação e integração com pipelines.

Envie

Esta lição produz:

  • outputs/skill-evaluation.md - uma habilidade que abrange estratégia de avaliação para modelos de classificação e regressão

Exercícios

  1. Implementar curvas de recuperação de precisão: plotar precisão versus recuperação em diferentes limites. Calcule a precisão média (área sob a curva PR). Compare a curva PR com a curva ROC em um conjunto de dados desequilibrado e explique quando cada uma é mais informativa.
  2. Construa um loop de validação cruzada aninhado: o loop externo avalia o desempenho do modelo, o loop interno ajusta os hiperparâmetros. Use-o para comparar dois modelos de forma justa, sem vazar dados de validação na avaliação.
  3. Implemente um teste de permutação para comparação de modelos: embaralhe os rótulos, treine novamente e meça o desempenho. Repita 100 vezes para construir uma distribuição nula. Calcule o valor p para o desempenho do modelo observado em relação a esta distribuição.

Termos-chave

Prazo O que as pessoas dizem O que isso realmente significa
Sobreajuste “Memorizando os dados de treinamento” O modelo captura ruído nos dados de treinamento, apresentando bom desempenho no treinamento, mas fraco em dados não vistos
Validação cruzada "Testando em diferentes subconjuntos" Girar sistematicamente qual parte dos dados é usada para validação, calculando a média dos resultados em todas as rotações
Precisão “Quantos positivos previstos estão corretos” TP / (TP + FP): a fração de previsões positivas que são realmente positivas
Lembrar "Quantos pontos positivos reais encontramos" TP / (TP + FN): fração de positivos reais que foram corretamente identificados
AUC-ROC “Quão bem o modelo separa as classes” A área sob a curva da taxa de verdadeiros positivos versus taxa de falsos positivos em todos os limites, de 0,5 (aleatório) a 1,0 (perfeito)
R-quadrado “Quanta variação é explicada” 1 - (soma dos quadrados dos resíduos / soma total dos quadrados): a fração da variância alvo capturada pelo modelo
Vazamento de dados “A modelo trapaceou” Utilizar informações durante o treinamento que não estariam disponíveis no momento da previsão, levando a uma avaliação otimista
Curva de aprendizagem “Como o desempenho muda com mais dados” Um gráfico de pontuações de treinamento e validação versus tamanho do conjunto de treinamento, revelando underfitting ou overfitting
Divisão estratificada “Manter os rácios de classes equilibrados” Divisão de dados para que cada subconjunto tenha a mesma proporção de cada classe que o conjunto de dados completo

Leitura Adicional

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