در این فصل چه یاد میگیری#
تا اینجا کلِ داده را یکجا به مدل میدادی. برای بیست نقطه خوب است؛ برای دادهٔ واقعی نه در حافظه جا میشود و نه کارآمد است. در این فصل 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 پایین نمیآید. شش علتِ رایج، هر کدام با نشانهٔ خودش و راهِ تشخیصش — همه با کدی که خودش خراب میشود تا با چشمِ خودت ببینیاش.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.