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

فصل ۷ از ۹

پیشرفت ترم
۰٪

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

حلقهٔ آموزشِ کامل

فصل ۷پیش‌نمایش رایگان
۹ دقیقه مطالعه فصل ۷

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

تا اینجا کلِ داده را یکجا به مدل می‌دادی. برای بیست نقطه خوب است؛ برای دادهٔ واقعی نه در حافظه جا می‌شود و نه کارآمد است. در این فصل Dataset و DataLoader را می‌بینی، batch و epoch را تعریف می‌کنی، و ساختارِ استانداردی می‌سازی که تا آخرِ این دوره و تا آخرِ کارِ حرفه‌ای‌ات همان می‌ماند.

و یک تفاوتِ ظریف را یاد می‌گیری که فراموش کردنش بی‌صدا نتیجه‌ات را خراب می‌کند: حالتِ train در برابرِ eval.

یک نوارِ نقالهٔ دسته‌بندی که هر بار چند قطعه را به یک ایستگاه می‌رساند

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

  • Dataset و DataLoader بسازی و batch بگیری
  • epoch و batch را تعریف کنی و اثرشان را بگویی
  • حلقهٔ آموزشِ استاندارد را از حفظ بنویسی
  • بگویی model.train() و model.eval() چه فرقی دارند

قبل از شروع#

از فصل ۶: loss، optimizer، و سه‌گانهٔ zero_grad/backward/step.

از فصل ۴: nn.Module و شمردنِ پارامترها.

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

۱. Dataset: قراردادِ دسترسی به داده#

import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader

X = torch.arange(20.0).unsqueeze(1)
Y = 3 * X + 1
dataset = TensorDataset(X, Y)

print("تعداد نمونه:", len(dataset))
print("نمونهٔ پنجم:", dataset[4])
تعداد نمونه: 20
نمونهٔ پنجم: (tensor([4.]), tensor([13.]))

یک Dataset فقط دو چیز را قول می‌دهد: len() بگو چند نمونه داری، و [i] نمونهٔ i-ام را بده.

همین قراردادِ دوتایی کافی است تا بقیهٔ PyTorch با هر منبعِ داده‌ای کار کند — چه tensor در حافظه باشد، چه صد هزار فایلِ تصویر روی دیسک. در ترمِ ۳ Dataset خودت را می‌نویسی و می‌بینی که واقعاً همین دو متد است.

۲. DataLoader: تکه‌تکه کردن#

loader = DataLoader(dataset, batch_size=6, shuffle=False)

print("تعداد batch:", len(loader))
for i, (xb, yb) in enumerate(loader):
    print(f"batch {i + 1}: shape={tuple(xb.shape)} | اولین x={xb[0].item():.0f}")
تعداد batch: 4
batch 1: shape=(6, 1) | اولین x=0
batch 2: shape=(6, 1) | اولین x=6
batch 3: shape=(6, 1) | اولین x=12
batch 4: shape=(2, 1) | اولین x=18

بیست نمونه با batch_size=6 می‌شود چهار دسته: سه‌تای شش‌تایی و یکی دوتایی.

آن دستهٔ آخرِ ناقص را یادت باشد. اگر مثلاً میانگینِ loss را با تقسیم بر تعدادِ دسته‌ها حساب کنی، جوابت کمی غلط می‌شود — چون دستهٔ آخر وزنِ کمتری دارد. راهِ درست: loss هر دسته را در تعدادِ نمونه‌هایش ضرب کن، جمع بزن، و بر کلِ نمونه‌ها تقسیم کن. در بخشِ ۴ همین کار را می‌کنیم.

torch.manual_seed(0)
shuffled = DataLoader(dataset, batch_size=6, shuffle=True)
first = next(iter(shuffled))[0].flatten()
print("با shuffle، اولین batch:", first)
با shuffle، اولین batch: tensor([ 6., 15., 11., 13.,  9.,  2.])

⚠️ مواظب باش: shuffle=True روی دادهٔ آموزش اجباری است و روی آزمون بی‌فایده. اگر داده‌ات مرتب باشد — مثلاً همهٔ نمونه‌های یک کلاس پشتِ سرِ هم — بدونِ shuffle هر batch فقط یک کلاس دارد و گرادیان‌ها یک‌طرفه می‌شوند. این خطا هیچ پیامی نمی‌دهد و فقط مدلت بد آموزش می‌بیند.

batch کوچک batch بزرگ
نویزِ گرادیان زیاد کم
سرعتِ هر epoch کند سریع (روی GPU)
حافظه کم زیاد
اثرِ جانبی نویز خودش کمی مثلِ regularization عمل می‌کند نیاز به lr بزرگ‌تر

۳. epoch: یک دور روی کلِ داده#

در فصلِ ۴ مدل را با class و forward ساختی. برای مدلی که فقط یک زنجیرهٔ سادهٔ «این، بعد این، بعد این» است، راهِ کوتاه‌تری هم هست:

torch.manual_seed(0)
model = nn.Sequential(nn.Linear(1, 8), nn.ReLU(), nn.Linear(8, 1))
loss_fn = nn.MSELoss()
opt = torch.optim.Adam(model.parameters(), lr=0.02)
train_loader = DataLoader(dataset, batch_size=4, shuffle=True)

for epoch in range(1, 11):
    model.train()
    total = 0.0
    for xb, yb in train_loader:
        opt.zero_grad()
        loss = loss_fn(model(xb), yb)
        loss.backward()
        opt.step()
        total += loss.item() * len(xb)          # وزن‌دار، به‌خاطرِ دستهٔ ناقص
    if epoch in (1, 2, 5, 10):
        print(f"epoch {epoch:>2}: میانگین loss = {total / len(dataset):.4f}")
epoch  1: میانگین loss = 818.4216
epoch  2: میانگین loss = 666.2618
epoch  5: میانگین loss = 162.9219
epoch 10: میانگین loss = 13.7372

اول دربارهٔ آن خطِ اولِ کد: nn.Sequential خودش یک nn.Module است که فهرستی از لایه‌ها می‌گیرد و forwardش را خودش می‌سازد — هر لایه را به‌ترتیب صدا می‌زند و خروجیِ هر کدام را به بعدی می‌دهد. دقیقاً همان کاری که در TinyNet فصلِ ۴ با دست نوشتی، فقط بدونِ نوشتنِ class.

پس دو راه داری و هر دو درست‌اند: nn.Sequential برای زنجیرهٔ ساده، و class وقتی forwardت شرط، حلقه، یا چند مسیر دارد. از اینجا به بعد در این دوره هر دو را می‌بینی و همه‌چیزِ دیگر — parameters()، train()/eval()، state_dict() — برای هر دو یکسان است.

epoch یعنی یک بار عبور از کلِ داده. با ۲۰ نمونه و batch_size=4، هر epoch پنج قدمِ optimizer دارد — یعنی ده epoch می‌شود پنجاه به‌روزرسانی.

و همین‌جا یک تمایزِ مهم: epoch واحدِ داده است، step واحدِ به‌روزرسانی. وقتی می‌گویند «مدل ده epoch آموزش دید»، تعدادِ قدم‌ها به batch_size هم بستگی دارد.

۴. train و eval: دو حالتِ متفاوت#

model.eval()
with torch.no_grad():
    pred = model(torch.tensor([[7.0]]))

print("پیش‌بینی برای x=7:", round(pred.item(), 2), "| درست:", 3 * 7 + 1)
پیش‌بینی برای x=7: 24.52 | درست: 22

نزدیک، ولی نه دقیق — و صادقانه‌اش این است که ده epoch روی بیست نقطه کم است. با آموزشِ بیشتر بهتر می‌شود؛ در تمرین‌ها امتحانش کن.

ولی نکتهٔ اصلیِ این بخش آن دو خطِ قبلش است:

d = nn.Dropout(p=0.5)
t = torch.ones(8)

d.train()
torch.manual_seed(0)
print("در حالت train:", d(t))

d.eval()
print("در حالت eval :", d(t))
در حالت train: tensor([0., 0., 2., 0., 0., 0., 2., 2.])
در حالت eval : tensor([1., 1., 1., 1., 1., 1., 1., 1.])

همان لایه، همان ورودی، دو خروجیِ کاملاً متفاوت.

Dropout در حالتِ train نیمی از مقادیر را صفر می‌کند (و بقیه را دو برابر، تا میانگین حفظ شود) و در حالتِ eval هیچ کاری نمی‌کند. BatchNorm هم رفتارِ دوگانه دارد. پس:

  • model.train() قبل از حلقهٔ آموزش.
  • model.eval() قبل از هر ارزیابی یا پیش‌بینی.
  • torch.no_grad() هم موقعِ ارزیابی — نه برای درستی، بلکه برای سرعت و حافظه.

⚠️ مواظب باش: فراموش کردنِ model.eval() یکی از موذی‌ترین باگ‌های این حوزه است. هیچ خطایی نمی‌دهد — فقط ارزیابی‌ات نویزی و بدبینانه می‌شود و تو فکر می‌کنی مدلت بدتر از چیزی است که هست. و اگر بعد از ارزیابی model.train() را برنگردانی، ادامهٔ آموزش هم خراب می‌شود. در ترمِ ۲ که Dropout را واقعاً به‌کار می‌بریم، این را با عدد می‌بینی.

۵. الگوی استاندارد#

for epoch in range(epochs):
    model.train()                       # ۱. حالتِ آموزش
    for xb, yb in train_loader:
        opt.zero_grad()                 # ۲. پاک کن
        loss = loss_fn(model(xb), yb)   # ۳. جلو
        loss.backward()                 # ۴. عقب
        opt.step()                      # ۵. قدم

    model.eval()                        # ۶. حالتِ ارزیابی
    with torch.no_grad():               # ۷. بدونِ گراف
        val_loss = evaluate(model, val_loader)
    print(epoch, val_loss)              # ۸. ثبت کن

این هشت خط را حفظ کن. هر حلقهٔ آموزشی که تا آخرِ دوره می‌بینی، همین است با جزئیاتِ بیشتر: scheduler بینِ خطوط، checkpoint بعد از ارزیابی، clipping قبل از step. اسکلت عوض نمی‌شود.

چک کن: سه چیزِ ساده که قبل از هر آموزشِ طولانی باید ببینی: (۱) loss در چند epoch اول پایین می‌آید. (۲) شکلِ خروجیِ مدل با شکلِ هدف یکی است. (۳) روی ده نمونه، مدل می‌تواند loss را به نزدیکِ صفر برساند. اگر سومی برقرار نیست، حلقه‌ات ایراد دارد، نه داده‌ات — و ادامه دادنِ آموزش هدر دادنِ وقت است.

🔧 اگر کار نکرد: DataLoader worker exited unexpectedly روی Colab معمولاً از num_workers بزرگ می‌آید؛ صفر یا دو بگذارش. loss بینِ epochها نوسان می‌کند طبیعی است اگر batch کوچک باشد — روندِ چند epoch را ببین نه یک عدد را. آموزش خیلی کند است و روی CPU هستی: batch_size را بزرگ‌تر کن یا دادهٔ کمتری بردار.

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

واژه‌های تازهٔ این فصل#

کلمه تلفظ به حروف فارسی یعنی چه
nn.Sequential ان‌ان سیکوئنشال مدلی که forwardش «هر لایه به‌ترتیب» است
Dataset دیتاست قراردادِ «طول بده، نمونهٔ i را بده»
DataLoader دیتالودر تکه‌کننده و بُر زنندهٔ داده
batch size بچ سایز تعدادِ نمونه در هر به‌روزرسانی
epoch اپاک یک دورِ کامل روی همهٔ داده
train / eval mode ترین / ایوال مود دو حالتِ رفتاریِ مدل

تمرین‌ها

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

در فصل بعد#

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

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

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