Faza 02 · lecția 09

Evaluarea modelelor

Scopul lecției: Tip: Construire Limbaje: Python Cerințe preliminare: Faza 1 (Probabilități și distribuții, Statistică pentru învățarea automată), Faza 2, lecțiile 1–8 Durată: ~90 de minute

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Bazele învățării automate
Lectură
27 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Antrenare, validare, testare
  5. Validarea încrucișată K-fold
  6. Metrici de clasificare
  7. Metrici de regresie
  8. Curbe de învățare
  9. Curbe de validare
  10. Greșeli frecvente de evaluare
  11. Construiți
  12. Pasul 1: împărțirea în antrenare, validare și testare
  13. Pasul 2: validare încrucișată K-fold și K-fold stratificată
  14. Pasul 3: matricea de confuzie și metricile de clasificare
  15. Pasul 4: metrici de regresie
  16. Pasul 5: curbe de învățare
  17. Pasul 6: un clasificator simplu pentru testare și demonstrația completă
  18. Folosiți
  19. Livrați
  20. Exerciții
  21. Termeni-cheie
  22. Lecturi suplimentare

Un model este doar atât de bun pe cât este modul în care îl măsurați.

Tip: Construire Limbaje: Python Cerințe preliminare: Faza 1 (Probabilități și distribuții, Statistică pentru învățarea automată), Faza 2, lecțiile 1–8 Durată: ~90 de minute

Obiective de învățare

  • Implementarea de la zero a validării încrucișate K-fold și K-fold stratificate și explicarea importanței stratificării pentru date dezechilibrate
  • Calcularea de la zero a preciziei, recall-ului, scorului F1, AUC-ROC și a metricilor de regresie MSE, RMSE, MAE și R pătrat
  • Interpretarea curbelor de învățare pentru a diagnostica dacă un model suferă de părtinire ridicată sau varianță ridicată
  • Identificarea greșelilor frecvente de evaluare, inclusiv scurgerea de informație, alegerea unei metrici nepotrivite și contaminarea setului de testare

Problema

Ați antrenat un model. Obține o acuratețe de 95% pe datele dumneavoastră. Este bun?

Poate că da. Poate că nu. Dacă 95% dintre date aparțin unei singure clase, un model care prezice întotdeauna acea clasă obține o acuratețe de 95%, deși poate fi complet inutil. Dacă ați evaluat modelul pe aceleași date pe care l-ați antrenat, valoarea de 95% nu spune nimic despre generalizare, deoarece modelul ar fi putut memora răspunsurile. Dacă setul de date are o componentă temporală și l-ați amestecat aleatoriu înainte de împărțire, modelul ar putea folosi date din viitor pentru a prezice trecutul.

Evaluarea modelelor este locul în care multe proiecte de învățare automată eșuează. O metrică nepotrivită face ca un model slab să pară bun. O împărțire nepotrivită îi permite modelului să trișeze. O comparație nepotrivită vă face să alegeți modelul mai slab. Evaluarea corectă nu este opțională. Ea poate face diferența dintre un model care funcționează în producție și unul care eșuează imediat ce întâlnește date reale.

Conceptul

Antrenare, validare, testare

Диаграмма к уроку «Evaluarea modelelor»

Trei împărțiri, trei scopuri:

  • Setul de antrenare: modelul învață din aceste date și vede exemplele în timpul antrenării.
  • Setul de validare: este folosit pentru reglarea hiperparametrilor și alegerea dintre modele. Modelul nu este ajustat direct pe aceste date, dar deciziile dumneavoastră sunt influențate de ele.
  • Setul de testare: este consultat pentru evaluarea finală, după încheierea alegerilor de modelare. Dacă examinați performanța pe test și reveniți pentru a schimba modelul, setul nu mai este un test independent; a devenit un al doilea set de validare.

Setul de testare oferă o estimare pe date reținute a performanței modelului pe exemple nevăzute, dacă distribuția de testare este reprezentativă pentru utilizarea viitoare.

Notă tehnică a traducerii: un set de testare nu este o „garanție” a performanței viitoare. Estimarea are incertitudine de eșantionare și poate deveni nevalabilă în cazul schimbării distribuției, al dependențelor dintre exemple sau al reutilizării repetate a setului. Proporțiile din diagramă sunt orientative, nu reguli universale.

Validarea încrucișată K-fold

Pentru seturi de date mici, o singură împărțire antrenare/validare rezervă o parte importantă din date și poate produce estimări zgomotoase. Validarea încrucișată K-fold rotește rolul de validare între mai multe submulțimi:

Диаграмма к уроку «Evaluarea modelelor»

  1. Împărțiți datele în K fold-uri cu dimensiuni cât mai apropiate.
  2. Pentru fiecare fold, antrenați pe celelalte K-1 fold-uri și validați pe fold-ul rămas.
  3. Calculați media celor K scoruri de validare.

K=5 și K=10 sunt alegeri frecvente. Fiecare punct este folosit pentru validare exact o dată în K-fold obișnuit. Media scorurilor este adesea mai puțin dependentă de o singură împărțire decât scorul unui unic set de validare.

K-fold stratificată: păstrează aproximativ distribuția claselor în fiecare fold. Dacă setul de date conține 70% clasa A și 30% clasa B, fiecare fold va avea pe cât posibil un raport asemănător. Acest lucru este important pentru date dezechilibrate, unde o împărțire necontrolată ar putea concentra prea multe exemple minoritare într-un singur fold.

Notă tehnică a traducerii: K-fold obișnuit și stratificarea presupun de regulă exemple independente și identic distribuite. Pentru serii temporale, grupuri de observații provenite de la aceeași persoană sau alte dependențe, folosiți scheme temporale ori pe grupuri. Scorurile fold-urilor nu sunt independente, deoarece seturile de antrenare se suprapun; abaterea lor standard nu este automat un interval de încredere.

Metrici de clasificare

Matricea de confuzie este punctul de plecare. Pentru clasificarea binară:

Pozitiv prezis Negativ prezis
Pozitiv în realitate Pozitiv adevărat (TP) Negativ fals (FN)
Negativ în realitate Pozitiv fals (FP) Negativ adevărat (TN)

Din această matrice rezultă celelalte metrici:

  • Acuratețe = (TP + TN) / (TP + TN + FP + FN). Proporția predicțiilor corecte. Poate induce în eroare când clasele sunt dezechilibrate.
  • Precizie = TP / (TP + FP). Dintre toate cazurile prezise pozitiv, câte sunt într-adevăr pozitive? Folosiți-o când rezultatele fals pozitive sunt costisitoare, de exemplu când un filtru de spam marchează drept spam un mesaj legitim.
  • Recall sau sensibilitate = TP / (TP + FN). Dintre toate cazurile pozitive reale, câte au fost găsite? Este important când rezultatele fals negative sunt costisitoare, de exemplu când un test de screening nu detectează o tumoare.
  • Scor F1 = 2 * precizie * recall / (precizie + recall). Media armonică a preciziei și recall-ului. Le combină atunci când niciuna nu domină clar obiectivul.
  • AUC-ROC: aria de sub curba caracteristicii de operare a receptorului. Curba reprezintă rata pozitivelor adevărate în funcție de rata pozitivelor false pentru diferite praguri de clasificare. Pentru scoruri aleatoare independente de etichetă, AUC are valoarea așteptată 0,5; AUC = 1,0 indică o ordonare perfectă. Metrica evaluează ordonarea pozitivelor față de negative pe toate pragurile, dar nu alege pragul operațional.

Notă tehnică a traducerii: AUC-ROC poate lua valori între 0 și 1; o valoare sub 0,5 indică o ordonare sistematic inversată, nu este imposibilă. În date foarte dezechilibrate, curba precizie–recall poate fi mai informativă despre clasa pozitivă. „Independentă de prag” nu înseamnă independentă de costurile deciziilor: pragul final trebuie ales separat, pe date de validare și în funcție de consecințele FP și FN.

Metrici de regresie

  • MSE (eroare pătratică medie) = mean((y_true - y_pred)^2). Penalizează pătratic erorile mari și este sensibilă la valori aberante.
  • RMSE (rădăcina erorii pătratice medii) = sqrt(MSE). Are aceleași unități ca variabila-țintă și este mai ușor de interpretat decât MSE.
  • MAE (eroare absolută medie) = mean(|y_true - y_pred|). Tratează erorile liniar și este mai robustă la valori aberante decât MSE.
  • R pătrat = 1 - SS_res / SS_tot, unde SS_res = sum((y_true - y_pred)^2), iar SS_tot = sum((y_true - y_mean)^2). Compară eroarea pătratică a modelului cu referința care prezice media țintei pe setul evaluat. R^2 = 1,0 este perfect, R^2 = 0,0 corespunde acelei referințe, iar R^2 poate fi negativ dacă modelul este mai slab decât referința.

Notă tehnică a traducerii: expresia „proporția varianței explicate” este o interpretare uzuală, dar poate fi înșelătoare în afara modelelor și ipotezelor pentru care descompunerea variației este potrivită. Dacă toate valorile reale sunt constante, SS_tot = 0 și formula nu este definită; bibliotecile trebuie să adopte explicit o convenție pentru acest caz.

Curbe de învățare

Reprezentați scorurile de antrenare și validare în funcție de dimensiunea setului de antrenare:

  • Părtinire ridicată (subînvățare): ambele curbe tind către un scor slab. Mai multe date de același tip pot să nu ajute suficient; poate fi necesar un model sau o reprezentare mai adecvată.
  • Varianță ridicată (supraînvățare): scorul de antrenare este mare, dar cel de validare este mult mai mic. Diferența dintre curbe este mare, iar mai multe date pot ajuta.

Acestea sunt tipare de diagnostic, nu demonstrații definitive ale unei singure cauze.

Curbe de validare

Reprezentați scorurile de antrenare și validare în funcție de un hiperparametru:

  • La complexitate redusă, ambele scoruri pot fi slabe, indicând subînvățare.
  • Într-o zonă potrivită, ambele scoruri pot fi ridicate și apropiate.
  • La complexitate ridicată, scorul de antrenare poate rămâne ridicat, în timp ce scorul de validare scade, indicând supraînvățare.

Alegeți hiperparametrul pe baza scorului de validare sau a validării încrucișate și păstrați setul de testare pentru evaluarea finală. Dacă comparați multe configurații, validarea încrucișată imbricată oferă o estimare mai puțin optimistă a întregii proceduri de selecție.

Greșeli frecvente de evaluare

Scurgerea de informație: informația care nu ar fi disponibilă la momentul predicției ajunge în antrenare. Exemple: ajustarea unui scaler pe întregul set înainte de împărțire, includerea datelor viitoare într-o predicție temporală sau folosirea unei caracteristici derivate din țintă. Împărțiți mai întâi datele, apoi ajustați preprocesarea exclusiv pe porțiunea de antrenare. În validarea încrucișată, ajustați transformările separat în interiorul fiecărui fold, de preferință printr-o conductă.

Dezechilibrul claselor: 99% dintre tranzacții sunt legitime, iar 1% sunt fraude. Un model care prezice întotdeauna „legitimă” obține o acuratețe de 99%. Evaluați metrici potrivite obiectivului, precum precizia, recall-ul, F1 sau aria de sub curba precizie–recall; AUC-ROC poate completa analiza.

Metrica nepotrivită: optimizarea acurateții când costul falselor negative cere atenție asupra recall-ului sau optimizarea RMSE când valorile aberante domină și MAE descrie mai bine obiectivul. Alegerea trebuie să reflecte costurile și riscurile aplicației, nu doar un exemplu generic.

Lipsa împărțirilor stratificate: pentru date dezechilibrate, un fold de validare poate conține prea puține exemple minoritare, producând estimări instabile.

Testarea prea frecventă: de fiecare dată când examinați performanța de testare și ajustați modelul, vă adaptați la setul de testare. Păstrați evaluarea finală independentă.

precision-recall-threshold

Construiți

Pasul 1: împărțirea în antrenare, validare și testare

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

Pasul 2: validare încrucișată K-fold și K-fold stratificată

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

Pasul 3: matricea de confuzie și metricile de clasificare

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

Pasul 4: metrici de regresie

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

Pasul 5: curbe de învățare

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

Pasul 6: un clasificator simplu pentru testare și demonstrația completă

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)")

Notă tehnică a traducerii: codul este didactic și conține câteva discrepanțe importante. stratified_kfold_split trimite restul fiecărei clase în ultimul fold; dacă o clasă are mai puțin de K exemple, toate ajung în ultimul fold, deci implementarea nu păstrează proporțiile. roc_curve nu adaugă pragul inițial deasupra scorului maxim și omite punctul ROC (0,0), astfel încât auc_roc poate calcula o arie prea mică. r_squared întoarce 0 pentru orice țintă constantă, inclusiv pentru predicții perfecte, în timp ce cazul matematic este nedefinit și necesită o convenție explicită. Funcțiile MSE și MAE împart la zero pentru intrări goale.

Notă tehnică a traducerii: demonstrația de regresie standardizează X_reg înainte de împărțire, astfel încât media și abaterea standard includ seturile de validare și testare; aceasta este o scurgere de informație. Împărțiți mai întâi, ajustați scalarea numai pe antrenare și aplicați aceiași parametri celorlalte seturi. În plus, testul t final nu este valid ca test t pereche obișnuit: fold-urile au seturi de antrenare suprapuse și scoruri dependente, iar codul calculează abaterea standard cu numitorul K, nu abaterea de eșantion cu K-1. Pragul tipărit nu repară aceste probleme; pentru comparația algoritmilor folosiți o procedură concepută pentru reeșantionare sau validare încrucișată.

Folosiți

În scikit-learn, evaluarea este integrată în fluxul de lucru:

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")

Versiunile construite de la zero arată ce face validarea încrucișată — bucle și urmărirea indicilor — cum sunt calculate metricile de bază prin numărarea TP, FP, TN și FN și de ce contează stratificarea. Versiunile din bibliotecă adaugă paralelism, mai multe opțiuni de scor și integrare cu conductele de preprocesare.

Livrați

Această lecție produce:

  • outputs/skill-evaluation.md — o abilitate care acoperă strategia de evaluare pentru modele de clasificare și regresie.

Exerciții

  1. Implementați curbele precizie–recall: reprezentați precizia în funcție de recall la praguri diferite. Calculați precizia medie, conform definiției discrete standard, și comparați curba PR cu ROC pe un set dezechilibrat. Explicați când fiecare este mai informativă.
  2. Construiți o buclă de validare încrucișată imbricată: bucla exterioară evaluează performanța procedurii de modelare, iar cea interioară reglează hiperparametrii. Folosiți-o pentru a compara echitabil două modele fără a scurge datele de validare în evaluare.
  3. Implementați un test de permutare pentru performanța modelului: permutați etichetele, reantrenați și măsurați performanța. Repetați de cel puțin 100 de ori pentru a construi o distribuție nulă și calculați o valoare p Monte Carlo cu o corecție care evită valoarea zero. Creșteți numărul de permutări dacă aveți nevoie de o rezoluție mai fină.

Termeni-cheie

Termen Cum i se spune Ce înseamnă de fapt
Supraînvățare „Memorarea datelor de antrenare” Modelul surprinde zgomotul din datele de antrenare, obținând rezultate bune pe antrenare și slabe pe date nevăzute
Validare încrucișată „Testare pe submulțimi diferite” Rotirea sistematică a porțiunii folosite pentru validare și agregarea rezultatelor peste toate rotațiile
Precizie „Câte rezultate pozitive prezise sunt corecte” TP / (TP + FP): proporția predicțiilor pozitive care sunt într-adevăr pozitive
Recall „Câte rezultate pozitive reale am găsit” TP / (TP + FN): proporția cazurilor pozitive reale identificate corect
AUC-ROC „Cât de bine ordonează modelul clasele” Aria de sub curba ratei pozitivelor adevărate în funcție de rata pozitivelor false pentru toate pragurile; variază între 0 și 1, iar 0,5 este nivelul așteptat al unei ordonări aleatoare
R pătrat „Câtă variație este explicată” 1 - (suma pătratelor reziduurilor / suma totală a pătratelor): comparația erorii modelului cu referința bazată pe media țintei
Scurgere de informație „Modelul a trișat” Folosirea la antrenare a unei informații care nu ar fi disponibilă la momentul predicției, ceea ce produce o evaluare optimistă
Curbă de învățare „Cum se schimbă performanța cu mai multe date” Reprezentarea scorurilor de antrenare și validare în funcție de dimensiunea setului de antrenare, folosită pentru diagnosticarea subînvățării și supraînvățării
Împărțire stratificată „Păstrarea proporțiilor claselor” Împărțirea datelor astfel încât fiecare submulțime să păstreze pe cât posibil proporția fiecărei clase din setul complet

Lecturi suplimentare


Sursă: Model Evaluation — original

Navigare: înapoi: 02.08 — Ingineria și selecția caracteristicilor · Faza 2 — Bazele învățării automate · Catalog complet · în continuare: 02.10 — Compromisul deplasare–varianță.