ژرفا — خودآموز یادگیری عمیق و مهندسی هوش مصنوعی (متوسط)

فصل ۹ از ۹

پیشرفت ترم
۰٪

ترم ۱ · مکانیزمِ یادگیری: PyTorch از پایه

پروژه: یک شبکهٔ کامل روی دادهٔ واقعی

فصل ۹پیش‌نمایش رایگان

در این فصل چه یاد می‌گیری#

هیچ مفهومِ تازه‌ای نیست. کاری که می‌کنی این است: هشت فصلِ این ترم را روی دادهٔ واقعیِ سرنخ به‌کار می‌بری — همان جدولِ قیمتِ لپ‌تاپ — و شبکه‌ات را در برابرِ همان خطِ پایه‌ای که در سرنخ ترمِ ۳ گرفتی می‌سنجی.

و نتیجه‌ای می‌گیری که احتمالاً انتظارش را نداری. آن نتیجه، نه شکستِ این فصل، بلکه کلِ دلیلِ وجودِ ترمِ ۲ است.

یک ترازو که یک دستگاهِ پیچیده را در برابرِ یک ابزارِ ساده می‌سنجد

آخر این فصل می‌توانی:

  • دادهٔ جدولیِ واقعی را برای یک شبکه آماده کنی
  • یک آموزشِ کامل با ثبتِ منحنیِ آموزش و آزمون اجرا کنی
  • شبکه را در برابرِ خطِ پایه و مدلِ ساده منصفانه بسنجی
  • علامتِ overfitting را در عددها ببینی

قبل از شروع#

از کلِ ترمِ ۱. این فصل هیچ چیزِ تازه‌ای معرفی نمی‌کند.

از سرنخ ترمِ ۳: train_test_split، خطِ پایه، MAE — و عددهایی که آنجا گرفتی.

📓 نوت‌بوک: نوت‌بوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و به‌ترتیب داخلش هست.

۱. دادهٔ جدولی برای یک شبکه#

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.model_selection import train_test_split

df = pd.read_csv("laptops_clean.csv")
print("خانه‌های خالی:", df.isna().sum().to_dict())

X = pd.get_dummies(df[["year", "ram_gb", "brand", "city"]], columns=["brand", "city"], dtype=float)
y = df["price"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
fill = X_train.median()
X_train = X_train.fillna(fill)
X_test = X_test.fillna(fill)

print("شکل ورودی پس از one-hot:", X.shape)
print("بعد از پر کردن، خالی مانده:", int(X_train.isna().sum().sum()))
خانه‌های خالی: {'brand': 0, 'city': 0, 'year': 0, 'ram_gb': 25, 'price': 0}
شکل ورودی پس از one-hot: (238, 12)
بعد از پر کردن، خالی مانده: 0

آن ۲۵ خانهٔ خالی را جدی بگیر. در سرنخ SimpleImputer داخلِ Pipeline خودش پرشان می‌کرد. PyTorch چنین چیزی ندارد — و اگر nan وارد شبکه شود، loss هم nan می‌شود و هیچ خطایی نمی‌گیری، فقط یک ستونِ nan روی صفحه می‌بینی. این دقیقاً همان دستهٔ خطاهای فصلِ ۸ است.

و ترتیب مهم است: میانه را از دادهٔ آموزش گرفتیم و روی هر دو به‌کار بردیم. برعکسش نشتی است — همان درسِ سرنخ ترمِ ۳ فصلِ ۴.

get_dummies هم همان کارِ OneHotEncoder را می‌کند: چهار ستونِ اولیه به دوازده ستونِ عددی تبدیل شد، چون شبکه فقط عدد می‌فهمد.

۲. نرمال‌سازی و تبدیل به tensor#

mu, sigma = X_train.mean(), X_train.std().replace(0, 1)
y_mu, y_sigma = y_train.mean(), y_train.std()


def to_tensor(frame, series):
    xs = torch.tensor(((frame - mu) / sigma).values, dtype=torch.float32)
    ys = torch.tensor(((series - y_mu) / y_sigma).values, dtype=torch.float32).unsqueeze(1)
    return xs, ys


xtr, ytr = to_tensor(X_train, y_train)
xte, yte = to_tensor(X_test, y_test)

print("آموزش:", tuple(xtr.shape), "| آزمون:", tuple(xte.shape))
print("میانگین ورودی نرمال‌شده:", round(float(xtr.mean()), 4))
آموزش: (190, 12) | آزمون: (48, 12)
میانگین ورودی نرمال‌شده: 0.0

سه تصمیم در این هشت خط، و هر سه از فصل‌های قبل می‌آیند:

  • .replace(0, 1) روی انحرافِ معیار. بعضی ستون‌های one-hot ممکن است در دادهٔ آموزش ثابت باشند و انحرافشان صفر — و تقسیم بر صفر nan می‌دهد. این یک خطِ دفاعیِ کوچک با اثرِ بزرگ است.
  • هدف هم نرمال شد. قیمت‌ها ده‌ها میلیون‌اند و MSELoss رویشان عددهای نجومی می‌دهد. نرمال کردنِ هدف، گرادیان‌ها را در بازهٔ سالم نگه می‌دارد.
  • .unsqueeze(1) روی هدف. بدونش شکلِ (190,) می‌شد و همان تلهٔ broadcasting فصلِ ۸ بخشِ ۴ پیش می‌آمد.

۳. مدل#

torch.manual_seed(0)
model = nn.Sequential(
    nn.Linear(xtr.shape[1], 32), nn.ReLU(),
    nn.Linear(32, 16), nn.ReLU(),
    nn.Linear(16, 1),
)
print("پارامترها:", sum(p.numel() for p in model.parameters()))
پارامترها: 961

۹۶۱ پارامتر برای ۱۹۰ نمونهٔ آموزش. این عدد را نگه دار — در بخشِ ۵ دوباره سراغش می‌آییم.

۴. آموزش، با ثبتِ هر دو منحنی#

loader = DataLoader(TensorDataset(xtr, ytr), batch_size=32, shuffle=True)
opt = torch.optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.MSELoss()


def mae_toman(m, xs, ys):
    """MAE را به تومان برمی‌گرداند تا با عددهای سرنخ قابلِ مقایسه باشد."""
    m.eval()
    with torch.no_grad():
        return float(((m(xs) - ys).abs().mean()) * y_sigma)


for epoch in range(1, 61):
    model.train()
    for xb, yb in loader:
        opt.zero_grad()
        loss_fn(model(xb), yb).backward()
        opt.step()
    if epoch in (1, 10, 30, 60):
        print(f"epoch {epoch:>2}: MAE آموزش = {round(mae_toman(model, xtr, ytr)):>8} | "
              f"MAE آزمون = {round(mae_toman(model, xte, yte)):>8}")
epoch  1: MAE آموزش =  6387884 | MAE آزمون =  6595534
epoch 10: MAE آموزش =  1423511 | MAE آزمون =  1753220
epoch 30: MAE آموزش =  1225251 | MAE آزمون =  1842329
epoch 60: MAE آموزش =  1105180 | MAE آزمون =  1888727

به این چهار سطر خوب نگاه کن — مهم‌ترین چیزِ کلِ این فصل همین‌جاست.

  • تا epoch ۱۰، هر دو پایین می‌آیند. مدل واقعاً دارد یاد می‌گیرد.
  • از آن به بعد، MAE آموزش پایین می‌آید و MAE آزمون بالا می‌رود.

یعنی از حدودِ epoch ۱۰ به بعد، مدل دیگر «الگو» یاد نمی‌گیرد — دارد ۱۹۰ نمونهٔ آموزش را حفظ می‌کند. نامش overfitting است و در سرنخ ترمِ ۳ فصلِ ۸ مفهومش را دیدی؛ اینجا برای اولین بار با چشمِ خودت در عددهای یک شبکه می‌بینی‌اش.

و آن ۹۶۱ پارامتر در برابرِ ۱۹۰ نمونه، دقیقاً همین را پیش‌بینی می‌کرد: مدل ظرفیتِ حفظ کردن دارد، پس حفظ می‌کند.

📏 اندازه بگیر: از همین‌جا به بعد، هیچ‌وقت فقط loss آموزش را چاپ نکن. یک عددِ آموزشِ در حالِ کاهش هیچ‌چیز دربارهٔ کیفیتِ مدل نمی‌گوید — فقط می‌گوید مدل دارد داده‌ات را حفظ می‌کند. دو منحنی، همیشه، کنارِ هم.

۵. حالا مقایسهٔ صادقانه#

from sklearn.dummy import DummyRegressor
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error

base = DummyRegressor(strategy="median").fit(X_train, y_train)
ridge = Ridge(alpha=0.1).fit((X_train - mu) / sigma, y_train)

print("خط پایه (میانه):", round(mean_absolute_error(y_test, base.predict(X_test))))
print("خطی (سرنخ ت۳) :", round(mean_absolute_error(y_test, ridge.predict((X_test - mu) / sigma))))
print("شبکهٔ عصبی     :", round(mae_toman(model, xte, yte)))
خط پایه (میانه): 7793750
خطی (سرنخ ت۳) : 1377488
شبکهٔ عصبی     : 1888727

شبکهٔ عصبی از مدلِ خطیِ سرنخ بدتر شد. ۱٫۸۹ میلیون در برابرِ ۱٫۳۸ میلیون — حدودِ ۳۷ درصد بدتر.

نُه فصل کار کردی و نتیجه از یک مدلِ سه‌خطیِ ترمِ قبل بدتر درآمد. این را کسی از تو پنهان نمی‌کند، چون دقیقاً همان چیزی است که باید یاد بگیری.

سه دلیل، و هر سه واقعی‌اند:

  • داده کوچک است. ۱۹۰ نمونهٔ آموزش برای ۹۶۱ پارامتر خیلی کم است. شبکه‌ها وقتی می‌درخشند که داده زیاد باشد.
  • رابطه تقریباً خطی است. قیمت با سال و حافظه تقریباً یکنواخت بالا می‌رود. مدلِ خطی دقیقاً برای همین ساخته شده و انعطافِ اضافیِ شبکه فقط جا برای حفظ کردن می‌دهد.
  • هیچ انضباطی روی آموزش نگذاشتیم. نه توقفِ زودهنگام، نه regularization، نه تنظیمِ نرخ. همان سه چیزی که کلِ ترمِ ۲ دربارهٔ آن‌هاست.

چک کن: به عددِ epoch ۱۰ برگرد: MAE آزمون آنجا ۱٬۷۵۳٬۲۲۰ بود، بهتر از ۱٬۸۸۸٬۷۲۷ نهایی. یعنی اگر همان‌جا متوقف می‌شدیم، مدلِ بهتری داشتیم. این ساده‌ترین شکلِ early stopping است و ترمِ ۲ فصلِ ۲ کاملاً بازش می‌کند. پیدا کردنِ همین یک نکته در جدولِ خودت، مهارتی است که این فصل می‌خواهد به تو بدهد.

۶. گزارشِ صادقانه#

مسئله برآورد قیمت لپ‌تاپ دست‌دوم از چهار ویژگی
داده ۲۳۸ ردیف، ۱۹۰ آموزش و ۴۸ آزمون، random_state=0
مدل شبکهٔ سه‌لایه، ۹۶۱ پارامتر
آموزش ۶۰ epoch، Adam، lr=0.01، batch=32
نتیجه روی آزمون MAE برابرِ ۱٬۸۸۸٬۷۲۷ تومان
خط پایه ۷٬۷۹۳٬۷۵۰ (میانه)
بهترین مدلِ رقیب ۱٬۳۷۷٬۴۸۸ (خطی، سرنخ ترمِ ۳)
مشاهدهٔ اصلی از epoch ۱۰ به بعد overfitting شروع شد
نتیجه‌گیری برای این داده، شبکهٔ عصبی انتخابِ درستی نیست

آن سطرِ آخر را با همین صراحت بنویس. یک گزارشِ حرفه‌ای وقتی که مدلِ ساده‌تر برنده می‌شود همین را می‌گوید — و کسی که این را ننویسد، دیر یا زود سامانه‌ای می‌سازد که پیچیده است و کار نمی‌کند.

🔧 اگر کار نکرد: loss برابرِ nan شد — تقریباً حتماً fillna را جا انداخته‌ای؛ torch.isnan(xtr).any() را چک کن. MAE عددِ نجومی است — هدف را نرمال نکرده‌ای یا موقعِ برگرداندن در y_sigma ضرب نکرده‌ای. عددهایت با کتاب نمی‌خواندtorch.manual_seed(0) یا random_state=0 جا مانده.

🤖 از دستیارت بپرس: جدولِ منحنیِ بخشِ ۴ را به دستیارت بده و بپرس: «از این عددها چه چیزی می‌فهمی و چه کاری پیشنهاد می‌دهی؟» اگر overfitting و توقفِ زودهنگام را نگفت، جوابش ناقص است. و بعد بپرس: «برای این اندازهٔ داده، شبکهٔ عصبی انتخابِ درستی است؟» — جوابِ صادقانه «نه» است، و ببین دستیارت هم صادق است یا فقط تشویقت می‌کند.

تمرین‌ها

اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.

در فصل بعد#

ترمِ ۱ اینجا تمام می‌شود. سه چیز را با خودت ببر:

  • هیچ‌چیز در PyTorch جادو نیست. optimizer.step() را خودت با دست نوشتی، nn.Linear را با یک ضربِ ماتریسی ثابت کردی، و CrossEntropyLoss را با softmax و لگاریتم بازسازی کردی.
  • loss آموزش به‌تنهایی هیچ‌چیز نمی‌گوید.
  • مدلِ پیچیده‌تر خودبه‌خود بهتر نیست — باید در برابرِ خطِ پایه و مدلِ ساده ثابت کند.

و همان جدولِ بخشِ ۴، سؤالِ ترمِ بعد را می‌سازد: چطور جلوی حفظ کردن را بگیریم و شبکه را وادار کنیم به‌جای حافظه، الگو یاد بگیرد؟ ترمِ ۲ کاملاً دربارهٔ همین است.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.