در این فصل چه یاد میگیری#
پنج فصل، دهها آزمایش، و دهها عدد. سؤالِ ناراحتکننده: کدامشان فردا هم همان درمیآید؟
در سرنخ ترمِ ۵ فصلِ ۱ همین را برای scikit-learn دیدی. در یادگیری عمیق مسئله جدیتر است: منابعِ تصادف بیشترند، و روی GPU چیزهایی هست که حتی seed هم مهارشان نمیکند. در این فصل هر سه کتابخانه را قفل میکنی، مرزِ صداقت را میشناسی، و یک checkpoint میسازی که واقعاً کامل است.

آخر این فصل میتوانی:
- هر سه منبعِ تصادف را قفل کنی
- بگویی کجا
seedکافی نیست و چرا checkpointکامل ذخیره و بازیابی کنی- بگویی چه چیزهایی باید کنارِ مدل ثبت شوند
قبل از شروع#
از سرنخ ترمِ ۵ فصل ۱: seed، سنجاقِ نسخه، اثرِ انگشتِ داده.
از فصلهای ۱ تا ۵: همهٔ عددهایی که تا اینجا گرفتی.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
۱. با و بدونِ seed#
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
loss_fn = nn.CrossEntropyLoss()
@torch.no_grad()
def accuracy(model, X, y):
model.eval()
return float((model(X).argmax(1) == y).float().mean())
def build(dropout=0.0):
layers = [nn.Linear(64, 128), nn.ReLU()]
if dropout:
layers.append(nn.Dropout(dropout))
return nn.Sequential(*layers, nn.Linear(128, 10))
def run(seed=0, dropout=0.0, sched=False, epochs=30, subset=200):
torch.manual_seed(seed)
model = build(dropout)
loader = DataLoader(TensorDataset(Xtr[:subset], ytr[:subset]), batch_size=32, shuffle=True)
opt = torch.optim.Adam(model.parameters(), lr=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs) if sched else None
for _ in range(epochs):
model.train()
for xb, yb in loader:
opt.zero_grad()
loss_fn(model(xb), yb).backward()
opt.step()
if scheduler:
scheduler.step()
return accuracy(model, Xva, yva)
print("سه اجرا با seedهای متفاوت:", [round(run(seed=torch.seed() % 10000), 4) for _ in range(3)])
سه اجرا با seedهای متفاوت: [0.9415, 0.9443, 0.9304]
این سه عدد برای تو متفاوت درمیآیند — و همین نکتهٔ فصل است. torch.seed() هر بار از منبعِ تصادفِ سیستمعامل مقدار میگیرد، پس هیچکس نمیتواند این خروجی را تکرار کند. حالا همان کار با seed ثابت:
print("سه اجرا با seed=0:", [round(run(seed=0), 4) for _ in range(3)])
سه اجرا با seed=0: [0.9499, 0.9499, 0.9499]
با seed ثابت، هر سه اجرا دقیقاً یکیاند. بدونش نوسان دارد — در سه عددِ بالا حدودِ یکونیم واحد، و پیشِ تو عددهای دیگری. مقدارِ دقیقش مهم نیست؛ اینکه صفر نیست مهم است.
یکونیم واحد کوچک بهنظر میرسد، و همین خطرناک است. اگر دو ایده را مقایسه میکنی و تفاوتشان نیم واحد است، این نوسان کلِ نتیجهگیریات را میبلعد — و فصلِ بعد دقیقاً همین را با پنج seed اندازه میگیرد.
۲. سه منبعِ تصادف، سه قفل#
| منبع | چه چیزی را تصادفی میکند | قفلش |
|---|---|---|
torch |
وزنهای اولیه، dropout، بُر زدنِ DataLoader |
torch.manual_seed(s) |
numpy |
تقسیمِ داده، هر نمونهگیریِ خودت | np.random.seed(s) |
random |
کتابخانههای جانبی، shuffle پایتونی |
random.seed(s) |
سلولِ راهاندازیِ این ترم هر سه را با هم قفل میکند. یکی جامانده کافی است تا نتیجه غیرقابلِ تکرار شود — و چون تفاوت کوچک است، ممکن است ماهها متوجهش نشوی.
torch.manual_seed(0)
first = nn.Linear(4, 3).weight.sum().item()
torch.manual_seed(0)
second = nn.Linear(4, 3).weight.sum().item()
print("وزنِ اولیه با همان seed یکی است:", first == second)
gen = torch.Generator().manual_seed(7)
a = torch.randperm(6, generator=gen).tolist()
gen = torch.Generator().manual_seed(7)
b = torch.randperm(6, generator=gen).tolist()
print("ترتیبِ بُر زدن، دو بار:", a, b)
وزنِ اولیه با همان seed یکی است: True
ترتیبِ بُر زدن، دو بار: [3, 0, 1, 5, 2, 4] [3, 0, 1, 5, 2, 4]
آن torch.Generator() الگوی مهمی است: یک منبعِ تصادفِ مستقل که به حالتِ سراسری کاری ندارد. در فصلِ ۵ برای augmentation دقیقاً از همین استفاده کردیم — تا تصادفِ تبدیل، تصادفِ وزنها را جابهجا نکند و دو آزمایش قابلِ مقایسه بمانند.
۳. جایی که seed کافی نیست#
اینجا مرزِ صداقت است، و بیشترِ آموزشها از کنارش رد میشوند.
| منبع | آیا seed حلش میکند |
|---|---|
| وزنهای اولیه | بله |
ترتیبِ batchها |
بله |
dropout |
بله |
| ترتیبِ جمعِ اعشاری روی GPU | نه |
الگوریتمهای cuDNN که خودشان انتخاب میشوند |
نه، مگر صریح خاموششان کنی |
چند worker در DataLoader |
نه، مگر worker_init_fn بدهی |
دلیلِ ردیفِ چهارم ریاضی است، نه باگ: جمعِ اعشاری شرکتپذیر نیست — همیشه دقیقاً برابرِ نیست. GPU هزاران عدد را بهطورِ موازی جمع میزند و ترتیبِ رسیدنشان بینِ دو اجرا فرق میکند. نتیجه در رقمِ دهمِ اعشار فرق دارد، و بعد از هزاران قدم میتواند به تفاوتِ قابلِمشاهده برسد.
# قطعیتِ کامل روی GPU — به قیمتِ سرعت
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
⚠️ مواظب باش: این سه خط اجرا را کندتر میکنند، گاهی چشمگیر — و بعضی عملیاتها اصلاً نسخهٔ قطعی ندارند و خطا میدهند. قاعدهٔ عملی: در پژوهش و اشکالزدایی روشنشان کن، در آموزشِ نهایی نه. و مهمتر: اگر نتیجهات به این سه خط وابسته باشد، نتیجهٔ محکمی نیست. راهِ درستِ مقابله با نوسان، چند
seedاست، نه قطعیتِ کامل — و همان موضوعِ فصلِ بعد است.
۴. checkpoint کامل#
torch.manual_seed(0)
model = build()
opt = torch.optim.Adam(model.parameters(), lr=0.01)
checkpoint = {
"epoch": 12,
"model": model.state_dict(),
"optimizer": opt.state_dict(),
"seed": 0,
"val_acc": 0.9499,
}
torch.save(checkpoint, "checkpoint.pt")
loaded = torch.load("checkpoint.pt", weights_only=False)
print("کلیدهای checkpoint:", sorted(loaded.keys()))
torch.manual_seed(999)
fresh = build()
print("اختلافِ وزن قبل از بارگذاری:", round(float((fresh[0].weight - model[0].weight).abs().max()), 4))
fresh.load_state_dict(loaded["model"])
print("اختلافِ وزن بعد از بارگذاری:", round(float((fresh[0].weight - model[0].weight).abs().max()), 4))
کلیدهای checkpoint: ['epoch', 'model', 'optimizer', 'seed', 'val_acc']
اختلافِ وزن قبل از بارگذاری: 0.2456
اختلافِ وزن بعد از بارگذاری: 0.0
یک مدلِ کاملاً متفاوت، بعد از load_state_dict دقیقاً همان مدلِ ذخیرهشده شد.
و کلیدِ optimizer را جا نینداز. بیشترِ کدهایی که میبینی فقط model.state_dict() را ذخیره میکنند، و برای استفاده از مدل کافی است. ولی برای ادامهٔ آموزش نه:
print("optimizer فقط وزن نیست — کلیدهای حالتش:", sorted(loaded["optimizer"].keys()))
print("تعداد گروههای پارامتر:", len(loaded["optimizer"]["param_groups"]))
optimizer فقط وزن نیست — کلیدهای حالتش: ['param_groups', 'state']
تعداد گروههای پارامتر: 1
آن state همان حافظهٔ داخلیِ Adam است — میانگینهای متحرکی که در ترمِ ۱ فصلِ ۶ دیدی. اگر بدونِ آن ادامه بدهی، Adam از صفر شروع میکند و loss در چند قدمِ اول میپرد — پدیدهای که بارها دیده میشود و همیشه به «آموزشِ ناپایدار» نسبت داده میشود، در حالی که فقط یک checkpoint ناقص است.
✅ چک کن: بعد از بارگذاری، همان معیاری را که ذخیره کرده بودی دوباره حساب کن و مقایسهاش کن. اگر
val_accذخیرهشده با آنچه الان میگیری نمیخواند، یا داده عوض شده یاmodel.eval()جا مانده. این یک خطِ کد است و مچِ نیمی از باگهای بارگذاری را میگیرد.
۵. چه چیزی را کنارِ مدل ثبت کن#
| ثبت کن | چرا |
|---|---|
seed |
تا اجرای دوباره ممکن باشد |
نسخهٔ torch و python |
رفتارِ بعضی عملیاتها بینِ نسخهها فرق میکند |
| اثرِ انگشتِ داده | تا بدانی روی چه چیزی آموزش دیده |
کلِ hyperparameterها |
lr، batch، epoch، dropout، … |
| معیارِ نهایی و بازهاش | یک عدد از یک seed کافی نیست |
state_dict مدل و optimizer |
برای ادامهٔ آموزش |
🔧 اگر کار نکرد:
Missing key(s) in state_dict— معماریِ فعلی با معماریِ ذخیرهشده فرق دارد؛ همان تعریفِ دقیق را بساز.size mismatch— همان مشکل، ولی در ابعادِ لایه.torch.loadهشدارِ امنیتی میدهد — فایلِcheckpointکدِ اجرایی دارد؛weights_only=Trueامنتر است ولی فقط وزنها را میخواند. فایلِ ناشناس را باز نکن — همان هشدارِjoblibدر سرنخ.
🤖 از دستیارت بپرس: «چه چیزهای دیگری در آموزشِ یک شبکه ممکن است بینِ دو اجرا فرق کند، حتی با
seedثابت؟» فهرستش را با جدولِ بخشِ ۳ مقایسه کن. و اگرnum_workersدرDataLoaderرا نگفت، خودت اضافهاش کن — هرworkerمنبعِ تصادفِ خودش را دارد وworker_init_fnتنها راهِ قفل کردنشان است.
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| checkpoint | چکپوینت | عکسِ کاملِ وضعیتِ آموزش |
| state_dict | استیت-دیکت | دیکشنریِ پارامترهای یک مدل یا optimizer |
| determinism | دترمینیسم | اینکه اجرای دوباره دقیقاً همان نتیجه را بدهد |
| generator | جنریتور | منبعِ تصادفِ مستقل از حالتِ سراسری |
| worker | ورکر | فرآیندِ جانبیِ DataLoader برای خواندنِ داده |
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
نتیجههایت حالا تکرارپذیرند. ولی سؤالِ سختتر باقی است: از کجا میدانی تغییری که دادی واقعاً بهتر کرد و نه اینکه فقط شانس آورد؟ فصلِ بعد جوابش را میدهد — و نتیجهاش برای بیشترِ آزمایشهایی که تا اینجا کردیم، ناخوشایند است.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.