شاید برای شما هم پیش آمده باشد که مدلی با صحت 95% آموزش دادهاید، اما به محض ورود به محیط عملیاتی و مواجهه با دادههای واقعی شکست خورده است. در یادگیری ماشین، آموزش مدل تنها نیمی از مسیر است؛ نیمهی حیاتیتر، سنجش درست و بیطرفانهی کارایی مدل است تا از توهم دقت بالا و فریب ناشی از دادههای نامتوازن در امان بمانید.
در این درس میآموزید که چطور استراتژیهای تفکیک داده (آموزش، اعتبارسنجی و آزمون) و روشهای اعتبارسنجی متقاطع مانند K-fold و Stratified K-fold را از پایه پیاده کنید. همچنین با درک عمیق ماتریس درهمریختگی، معیارهای دستهبندی و رگرسیون، و تحلیل منحنیهای یادگیری، نقاط ضعف مدل شامل کمبرازش، بیشبرازش و خطای مهلک نشت داده را شناسایی و برطرف خواهید کرد.
ارزش یک مدل فقط به اندازهی شیوهی ارزیابی آن است.
اهداف یادگیری
- پیادهسازی اعتبارسنجی متقاطع K-fold و K-fold لایهبندیشده (stratified) از صفر و توضیح دلیل اهمیت لایهبندی برای دادههای نامتوازن
- محاسبهی معیارهای دقت (precision)، بازخوانی (recall)، امتیاز F1، AUC-ROC و معیارهای رگرسیون (MSE، RMSE، MAE و ضریب تعیین یا R-squared) از صفر
- تحلیل منحنیهای یادگیری برای تشخیص بایاس بالا (high bias) یا واریانس بالا (high variance) در مدل
- شناسایی خطاهای رایج ارزیابی، از جمله نشت داده (data leakage)، انتخاب معیار نامناسب و آلودگی مجموعهی آزمون
صورت مسئله
مدلی آموزش دادهاید که روی دادههایتان به صحت (accuracy) 95% میرسد. آیا مدل خوبی است؟
شاید بله، شاید هم نه. اگر 95% دادههایتان متعلق به یک کلاس باشند، مدلی که همیشه همان کلاس را پیشبینی کند به صحت 95% میرسد، در حالی که کاملاً بیاستفاده است. اگر مدل را روی همان دادههایی ارزیابی کرده باشید که با آنها آموزش دیده، عدد 95% بیمعنی است؛ چون مدل فقط پاسخها را حفظ کرده است. اگر مجموعهدادهتان مؤلفهی زمانی داشته باشد و دادهها را پیش از تفکیک بهطور تصادفی بُر زده باشید، شاید مدل دارد از دادههای آینده برای پیشبینی گذشته استفاده میکند.
بیشتر پروژههای یادگیری ماشین دقیقاً در مرحلهی ارزیابی به بیراهه میروند. معیار اشتباه مدلی ضعیف را عالی نشان میدهد. تفکیک نادرست به مدل امکان تقلب میدهد. مقایسهی نادرست هم باعث میشود مدل بدتر را انتخاب کنید. درست ارزیابی کردن اختیاری نیست؛ دقیقاً تفاوت بین مدلی است که در محیط عملیاتی واقعاً کار میکند و مدلی که به محض دیدن دادههای واقعی زمین میخورد.
مفاهیم پایه
مجموعههای آموزش، اعتبارسنجی و آزمون
سه تفکیک، سه هدف:
- مجموعهی آموزش (Training set): مدل از این دادهها یاد میگیرد و این نمونهها را در طول فرایند آموزش میبیند.
- مجموعهی اعتبارسنجی (Validation set): برای تنظیم ابرپارامترها و انتخاب بین مدلهای مختلف به کار میرود. مدل هرگز روی این دادهها آموزش نمیبیند، اما تصمیمهای شما تحت تأثیر آن است.
- مجموعهی آزمون (Test set): دقیقاً یک بار و در پایان کار ارزیابی میشود تا عملکرد نهایی را گزارش کند. اگر به عملکرد مدل روی مجموعهی آزمون نگاه کنید و سپس برگردید و مدل را تغییر دهید، آن مجموعه دیگر مجموعهی آزمون نیست؛ بلکه به یک مجموعهی اعتبارسنجی دوم تبدیل شده است.
مجموعهی آزمون تضمین نگهداشتهی شماست تا عملکرد گزارششده، بازتابدهندهی رفتار واقعی مدل روی دادههای واقعاً دیدهنشده باشد.
اعتبارسنجی متقاطع K-Fold
در مجموعهدادههای کوچک، یک تفکیک ساده به آموزش و اعتبارسنجی، دادهها را هدر میدهد و برآوردهای پر از خطایی تولید میکند. اعتبارسنجی متقاطع K-fold از تمام دادهها هم برای آموزش و هم برای اعتبارسنجی بهره میبرد:
- دادهها را به بخش (fold) هماندازه تقسیم کنید.
- برای هر بخش، مدل را روی بخش آموزش دهید و روی بخش باقیمانده ارزیابی کنید.
- میانگین امتیازهای اعتبارسنجی بخش را بگیرید.
انتخابهای استاندارد معمولاً یا هستند. هر نقطهی داده دقیقاً یک بار برای اعتبارسنجی استفاده میشود. امتیاز میانگین، برآورد پایدارتری نسبت به هر تکتفکیک به دست میدهد.
K-fold لایهبندیشده (Stratified K-fold): توزیع کلاسها را در هر بخش حفظ میکند. اگر مجموعهدادهتان شامل 70% کلاس A و 30% کلاس B باشد، هر بخش تقریباً همین نسبت را حفظ خواهد کرد. این روش برای مجموعهدادههای نامتوازن حیاتی است؛ زیرا تفکیک تصادفی ممکن است تمام نمونههای کلاس اقلیت را در یک بخش بیندازد.
معیارهای دستهبندی
ماتریس درهمریختگی (Confusion matrix): پایهی همهچیز است. برای دستهبندی دودویی:
| پیشبینی مثبت | پیشبینی منفی | |
|---|---|---|
| در واقع مثبت | مثبت واقعی (TP) | منفی کاذب (FN) |
| در واقع منفی | مثبت کاذب (FP) | منفی واقعی (TN) |
تمام معیارهای دیگر از همین ماتریس به دست میآیند:
- صحت (Accuracy) = : نسبت پیشبینیهای درست. در شرایط نامتوازن بودن کلاسها گمراهکننده است.
- دقت (Precision) = : از بین تمام نمونههایی که مثبت پیشبینی شدهاند، چند نمونه واقعاً مثبت بودهاند؟ وقتی مثبتهای کاذب پرهزینهاند از این معیار استفاده کنید (مثلاً فیلتر هرزنامهای که ایمیل واقعی را اشتباهاً هرزنامه علامت میزند).
- بازخوانی (Recall یا sensitivity) = : از بین تمام مثبتهای واقعی، چند مورد را شناسایی کردیم؟ وقتی منفیهای کاذب پرهزینهاند به کار میآید (مثل غربالگری سرطان که ندیدن تومور در آن خطرساز است).
- امتیاز F1 (F1 score) = : میانگین همساز (harmonic mean) دقت و بازخوانی. وقتی هیچکدام از این دو برتری مطلقی ندارند، بین آنها تعادل برقرار میکند.
- AUC-ROC: سطح زیر منحنی مشخصهی عملکرد سیستم (Area Under the Receiver Operating Characteristic curve). این منحنی نرخ مثبت واقعی را در برابر نرخ مثبت کاذب در آستانههای مختلف دستهبندی رسم میکند. مقدار AUC = 0.5 به معنی حدس تصادفی و AUC = 1.0 نشاندهندهی تفکیک بینقص است. این معیار مستقل از آستانه است و نشان میدهد مدل چقدر در رتبه دادن به نمونههای مثبت بالاتر از نمونههای منفی موفق است؛ فارغ از اینکه چه حد آستانهای انتخاب کنید.
معیارهای رگرسیون
- MSE (میانگین مربعات خطا) = : خطاهای بزرگ را به شکل درجهدو جریمه میکند و به دادههای پرت (outliers) حساس است.
- RMSE (ریشهی میانگین مربعات خطا) = : واحدی همسان با متغیر هدف دارد و تفسیرش سادهتر از MSE است.
- MAE (میانگین قدر مطلق خطا) = : با تمام خطاها رفتار خطی دارد و در برابر دادههای پرت مقاومتر از MSE است.
- ضریب تعیین (R-squared) = ، که در آن و است: کسری از واریانس که توسط مدل توجیه میشود. مقدار نشانهی عملکرد بینقص است. یعنی مدل هیچ مزیتی نسبت به پیشبینی همیشگی میانگین ندارد. اگر عملکرد مدل از میانگین هم ضعیفتر باشد، مقدار میتواند منفی شود.
منحنیهای یادگیری
رسم امتیازهای آموزش و اعتبارسنجی بر حسب اندازهی مجموعهی آموزش:
- بایاس بالا یا کمبرازش (High bias / underfitting): هر دو منحنی روی امتیازی پایین همگرا میشوند. اضافه کردن دادهی بیشتر کمکی نمیکند؛ به مدل پیچیدهتری نیاز دارید.
- واریانس بالا یا بیشبرازش (High variance / overfitting): امتیاز آموزش بالاست اما امتیاز اعتبارسنجی فاصلهی زیادی با آن دارد و بسیار پایینتر است. افزودن دادهی بیشتر معمولاً کمک میکند.
منحنیهای اعتبارسنجی
رسم امتیازهای آموزش و اعتبارسنجی بر حسب تغییرات یک ابرپارامتر:
- در پیچیدگی کم: هر دو امتیاز پایین هستند (کمبرازش)
- در پیچیدگی مناسب: هر دو امتیاز بالا و نزدیک به هم هستند
- در پیچیدگی زیاد: امتیاز آموزش بالا میماند ولی امتیاز اعتبارسنجی افت میکند (بیشبرازش)
مقدار بهینهی ابرپارامتر جایی است که امتیاز اعتبارسنجی در آن اوج میگیرد.
اشتباهات رایج در ارزیابی
نشت داده (Data leakage): وقتی اطلاعاتی از مجموعهی آزمون به فرایند آموزش رخنه میکند. نمونهها: فیت کردن مقیاسبند (scaler) روی کل دادهها قبل از تفکیک، استفاده از دادههای آینده در پیشبینی سریهای زمانی، یا استفاده از ویژگیهایی که از خودِ متغیر هدف مشتق شدهاند. همیشه اول دادهها را تفکیک کنید، سپس پیشپردازش را انجام دهید.
نامتوازنی کلاسها (Class imbalance): اگر 99% تراکنشها عادی و 1% کلاهبرداری باشند، مدلی که همیشه گزینهی «عادی» را پیشبینی کند به صحت 99% میرسد. در چنین حالتی از دقت، بازخوانی، F1 یا AUC-ROC استفاده کنید.
معیار اشتباه (Wrong metric): بهینهسازی صحت به جای بازخوانی (در تشخیصهای پزشکی)، یا بهینهسازی RMSE وقتی دادهها نویز شدید و مقادیر پرت سنگین دارند (که MAE انتخاب بهتری است).
استفاده نکردن از تفکیک لایهبندیشده (Not using stratified splits): در دادههای نامتوازن، تفکیک تصادفی ممکن است تعداد بسیار کمی از نمونههای کلاس اقلیت را در بخش اعتبارسنجی بگذارد و برآوردهایی ناپایدار تولید کند.
ارزیابی مکرر روی مجموعهی آزمون (Testing too often): هر بار که عملکرد آزمون را چک میکنید و مدل را دستکاری میکنید، مدل را به مجموعهی آزمون بیشبرازش میکنید. مجموعهی آزمون مصرفی یکباره دارد.
پیادهسازی
گام 1: تفکیک به آموزش، اعتبارسنجی و آزمون
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
گام 2: اعتبارسنجی متقاطع K-fold و Stratified K-fold
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
گام 3: ماتریس درهمریختگی و معیارهای دستهبندی
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
گام 4: معیارهای رگرسیون
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
گام 5: منحنیهای یادگیری
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
گام 6: یک دستهبند ساده برای آزمایش به همراه دموی کامل
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)")
استفاده از ابزارهای واقعی
در scikit-learn، ارزیابی بخشی از روند کاری است:
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")
نسخههای پیادهسازیشده از صفر دقیقاً نشان میدهند که اعتبارسنجی متقاطع چه میکند (هیچ جادویی در کار نیست؛ صرفاً حلقههای for و دنبال کردن اندیسهاست)، هر معیار چگونه محاسبه میشود (فقط شمارش TP/FP/TN/FN) و چرا لایهبندی اهمیت دارد (حفظ نسبت کلاسها در هر بخش). نسخههای کتابخانهای قابلیت اجرای موازی، گزینههای امتیازدهی متنوعتر و یکپارچگی با پایپلاینها را به این ساختار اضافه میکنند.
خروجی این درس
این درس این خروجی را تولید میکند:
outputs/skill-evaluation.md- یک مهارت در زمینهی استراتژی ارزیابی برای مدلهای دستهبندی و رگرسیون
تمرینها
- منحنیهای دقت-بازخوانی (precision-recall) را پیادهسازی کنید: دقت را در برابر بازخوانی در آستانههای مختلف رسم کنید. میانگین دقت (سطح زیر منحنی PR) را محاسبه کنید. منحنی PR را با منحنی ROC روی یک مجموعهدادهی نامتوازن مقایسه کنید و توضیح دهید در چه شرایطی هر کدام اطلاعات مفیدتری ارائه میدهند.
- یک حلقهی اعتبارسنجی متقاطع تودرتو (nested cross-validation) بسازید: حلقهی بیرونی عملکرد مدل را ارزیابی کند و حلقهی درونی ابرپارامترها را تنظیم کند. از آن برای مقایسهی منصفانهی دو مدل استفاده کنید، طوری که دادههای اعتبارسنجی به فرایند ارزیابی نشت نکنند.
- یک آزمون جایگشت (permutation test) برای مقایسهی مدل پیادهسازی کنید: برچسبها را بُر بزنید، مدل را بازآموزی کنید و عملکرد را بسنجید. این کار را 100 بار تکرار کنید تا توزیع خنثی (null distribution) شکل بگیرد. در نهایت، مقدار p-value را برای عملکرد واقعی مدل در مقایسه با این توزیع به دست آورید.
اصطلاحات کلیدی
| اصطلاح | در گفتگوهای عامیانه | معنای دقیق فنی |
|---|---|---|
| بیشبرازش (Overfitting) | «حفظ کردن دادههای آموزش» | مدل نویز موجود در دادههای آموزش را یاد میگیرد؛ در نتیجه روی دادههای آموزش عالی عمل میکند اما روی دادههای ندیده ضعیف است |
| اعتبارسنجی متقاطع (Cross-validation) | «تست کردن روی بخشهای مختلف» | جابهجایی منظم بخشی از داده که برای اعتبارسنجی به کار میرود و میانگینگیری از نتایج در تمام این چرخشها |
| دقت (Precision) | «چند تا از پیشبینیهای مثبت درست بودهاند» | : کسری از پیشبینیهای مثبت که واقعاً مثبت هستند |
| بازخوانی (Recall) | «چند تا از مثبتهای واقعی را توانستیم پیدا کنیم» | : کسری از مثبتهای واقعی که به درستی شناسایی شدهاند |
| AUC-ROC | «مدل چقدر خوب کلاسها را از هم جدا میکند» | سطح زیر منحنیِ نرخ مثبت واقعی در برابر نرخ مثبت کاذب در تمام آستانهها، از 0.5 (تصادفی) تا 1.0 (بینقص) |
| ضریب تعیین (R-squared) | «چه مقدار از واریانس توجیه شده است» | : کسری از واریانس متغیر هدف که مدل توانسته آن را توضیح دهد |
| نشت داده (Data leakage) | «مدل تقلب کرده است» | استفاده از اطلاعاتی در زمان آموزش که هنگام پیشبینی در دسترس نخواهند بود، که منجر به ارزیابی بیشازحد خوشبینانه میشود |
| منحنی یادگیری (Learning curve) | «عملکرد مدل با افزایش داده چگونه تغییر میکند» | نمودار امتیازهای آموزش و اعتبارسنجی در برابر اندازهی مجموعهی آموزش، که کمبرازش یا بیشبرازش را مشخص میکند |
| تفکیک لایهبندیشده (Stratified split) | «متوازن نگهداشتن نسبت کلاسها» | تفکیک دادهها به شکلی که در هر زیرمجموعه، نسبت هر کلاس دقیقاً مانند کل مجموعهداده باشد |
برای مطالعهی بیشتر
- راهنمای انتخاب مدل در scikit-learn - مرجع جامعی دربارهی اعتبارسنجی متقاطع، معیارها و تنظیم ابرپارامترها
- فراتر از صحت: دقت و بازخوانی (دورهی فشردهی یادگیری ماشین گوگل) - توضیحی روان به همراه مثالهای تعاملی
- بررسی روشهای اعتبارسنجی متقاطع (Arlot & Celisse, 2010) - بررسی دقیق و آکادمیک دربارهی چرایی و زمان کارآمدی روشهای مختلف CV
