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

فصل ۵ از ۸

پیشرفت ترم
۰٪

ترم ۲ · آموزشِ درست: تعمیم و آزمایشِ منظم

وقتی داده کم است

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

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

فصلِ ۱ نشان داد بیشتر داده بهترین درمانِ overfitting است. ولی داده گران است. data augmentation می‌گوید: از همان داده‌ای که داری، نسخه‌های تغییریافته بساز.

و در این فصل با عدد می‌بینی که این تکنیک وقتی کار می‌کند که تغییری را شبیه‌سازی کند که در دنیای واقعی واقعاً رخ می‌دهد — نه هر تغییری. روی دادهٔ تمیز ضرر می‌زند؛ روی دادهٔ واقع‌گرایانه، دقت را تقریباً دو برابر می‌کند.

یک قالب که از یک قطعه، چند نسخهٔ کمی متفاوت می‌سازد

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

  • augmentation را روی داده‌ات پیاده کنی
  • بگویی کدام تغییرها برچسب را حفظ می‌کنند و کدام‌ها نه
  • بسنجی که augmentation واقعاً کمک می‌کند یا نه
  • بگویی چرا فقط روی دادهٔ آموزش اعمال می‌شود

قبل از شروع#

از فصل ۱: اثرِ اندازهٔ داده روی فاصلهٔ تعمیم.

از ترمِ ۱ فصل ۷: DataLoader و اینکه تبدیل در کجای مسیر اعمال می‌شود.

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

۱. یک تغییرِ ساده#

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

loss_fn = nn.CrossEntropyLoss()

image = Xtr[0].reshape(8, 8)
shifted = torch.roll(image, shifts=1, dims=1)

print("اصلی (سطر ۳):  ", (image[3] * 16).round().int().tolist())
print("شیفت‌خورده:    ", (shifted[3] * 16).round().int().tolist())
اصلی (سطر ۳):   [0, 8, 10, 0, 0, 0, 0, 0]
شیفت‌خورده:     [0, 0, 8, 10, 0, 0, 0, 0]

همان رقم، یک پیکسل به راست. برای یک انسان هنوز همان عدد است — پس برچسبش هم همان می‌ماند. این هستهٔ augmentation است: تغییری که ظاهر را عوض می‌کند و معنا را نه.

۲. آزمایشِ اول: روی دادهٔ تمیز#

@torch.no_grad()
def accuracy(model, X, y):
    model.eval()
    return float((model(X).argmax(1) == y).float().mean())


def plain(seed=0):
    torch.manual_seed(seed)
    return nn.Sequential(nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 10))


def shift_batch(batch, generator):
    """کلِ batch را تصادفاً یک پیکسل در هر جهت جابه‌جا می‌کند."""
    imgs = batch.reshape(-1, 8, 8)
    dx = int(torch.randint(-1, 2, (1,), generator=generator))
    dy = int(torch.randint(-1, 2, (1,), generator=generator))
    return torch.roll(imgs, shifts=(dy, dx), dims=(1, 2)).reshape(-1, 64)


def fit(model, aug=None, epochs=40, subset=400, seed=0):
    loader = DataLoader(TensorDataset(Xtr[:subset], ytr[:subset]), batch_size=32, shuffle=True)
    opt = torch.optim.Adam(model.parameters(), lr=0.01)
    gen = torch.Generator().manual_seed(seed)
    for _ in range(epochs):
        model.train()
        for xb, yb in loader:
            if aug:
                xb = aug(xb, gen)
            opt.zero_grad()
            loss_fn(model(xb), yb).backward()
            opt.step()
    return model


for seed in (0, 1, 2, 3):
    without = accuracy(fit(plain(seed), aug=None, seed=seed), Xva, yva)
    with_aug = accuracy(fit(plain(seed), aug=shift_batch, seed=seed), Xva, yva)
    print(f"seed={seed}: بدون افزایش={without:.4f} | با افزایش={with_aug:.4f}")
seed=0: بدون افزایش=0.9666 | با افزایش=0.8635
seed=1: بدون افزایش=0.9666 | با افزایش=0.9109
seed=2: بدون افزایش=0.9666 | با افزایش=0.9025
seed=3: بدون افزایش=0.9526 | با افزایش=0.9220

augmentation نتیجه را بدتر کرد، در هر چهار seed.

و چهار seed را عمداً اجرا کردیم، نه یکی. ستونِ راست بینِ ۰٫۸۶ و ۰٫۹۲ بالا و پایین می‌رود — یعنی نوسانِ خودِ آموزش کم نیست. تنها دلیلی که می‌شود گفت «بدتر شد» این است که فاصله از آن نوسان بزرگ‌تر است: بدترین اجرای بدونِ افزایش هنوز از بهترین اجرای با افزایش بالاتر است.

اگر انتظارِ دیگری داشتی، تنها نیستی — این تکنیک معمولاً به‌عنوان «همیشه مفید» معرفی می‌شود. ولی چرا اینجا ضرر زد؟

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

۳. آزمایشِ دوم: دنیای واقع‌گرایانه#

حالا فرض کن تصویرهای واقعی همیشه وسط‌چین نیستند — که در هر سامانهٔ واقعیِ خواندنِ رقم دقیقاً همین‌طور است.

gen_val = torch.Generator().manual_seed(123)
imgs = Xva.reshape(-1, 8, 8)
dx = torch.randint(-1, 2, (len(imgs),), generator=gen_val)
dy = torch.randint(-1, 2, (len(imgs),), generator=gen_val)
messy = torch.stack([
    torch.roll(im, shifts=(int(a), int(b)), dims=(0, 1))
    for im, a, b in zip(imgs, dy, dx)
]).reshape(-1, 64)

for seed in (0, 1, 2, 3):
    m_plain = fit(plain(seed), aug=None, seed=seed)
    m_aug = fit(plain(seed), aug=shift_batch, seed=seed)
    print(f"seed={seed}: بدون افزایش -> تمیز={accuracy(m_plain, Xva, yva):.4f} "
          f"جابه‌جاشده={accuracy(m_plain, messy, yva):.4f} | "
          f"با افزایش -> تمیز={accuracy(m_aug, Xva, yva):.4f} "
          f"جابه‌جاشده={accuracy(m_aug, messy, yva):.4f}")
seed=0: بدون افزایش -> تمیز=0.9666 جابه‌جاشده=0.4513 | با افزایش -> تمیز=0.8635 جابه‌جاشده=0.8078
seed=1: بدون افزایش -> تمیز=0.9666 جابه‌جاشده=0.4540 | با افزایش -> تمیز=0.9109 جابه‌جاشده=0.8106
seed=2: بدون افزایش -> تمیز=0.9666 جابه‌جاشده=0.4513 | با افزایش -> تمیز=0.9025 جابه‌جاشده=0.8440
seed=3: بدون افزایش -> تمیز=0.9526 جابه‌جاشده=0.4401 | با افزایش -> تمیز=0.9220 جابه‌جاشده=0.8440

چک کن: ستونِ اولِ سه سطرِ اول عددِ یکسانِ ۰٫۹۶۶۶ است و سطرِ چهارم ۰٫۹۵۲۶. آن یکسان بودن یک اتفاق است، نه یک قاعدهseed وزن‌های اولیه و ترتیبِ batchها را عوض می‌کند و نتیجه حق دارد نوسان کند؛ فصلِ ۶ همین نوسان را مستقیم اندازه می‌گیرد. پس اگر عددهای ستونِ اولِ تو با هم فرق داشتند، چیزی خراب نشده. چیزی که واقعاً باید چک کنی این است: در هر چهار سطر، «جابه‌جاشدهٔ با افزایش» از «جابه‌جاشدهٔ بدونِ افزایش» خیلی بالاتر است — و همان فاصله است که نتیجه‌گیریِ این فصل را می‌سازد.

و اینجا کلِ داستان روشن می‌شود:

  • مدلِ بدونِ افزایش روی دادهٔ جابه‌جاشده فرو می‌ریزد: ۰٫۹۷ به ۰٫۴۵. یک پیکسل جابه‌جایی، بیش از نصفِ دقتش را برد.
  • مدلِ با افزایش تقریباً ثابت می‌ماند: ۰٫۸۶ روی تمیز، ۰٫۸۱ روی جابه‌جاشده.
روی دادهٔ تمیز روی دادهٔ واقع‌گرایانه
بدونِ افزایش ۰٫۹۵۳ تا ۰٫۹۶۷ ۰٫۴۴۰ تا ۰٫۴۵۴
با افزایش ۰٫۸۶۴ تا ۰٫۹۲۲ ۰٫۸۰۸ تا ۰٫۸۴۴

عمداً بازه نوشتیم، نه یک عدد. چهار seed چهار جواب می‌دهند و نوشتنِ یکی‌شان به‌عنوانِ «نتیجه»، پنهان کردنِ سه‌تای دیگر است. ولی دو بازهٔ ستونِ راست حتی به هم نزدیک هم نمی‌شوند — و به همین دلیل است که این نتیجه‌گیری محکم است.

📏 اندازه بگیر: augmentation رایگان نیست: کمی از دقت روی توزیعِ اصلی می‌گیرد و مقاومت در برابرِ تغییر می‌دهد. و سؤالی که باید بپرسی این نیست که «آیا کمک می‌کند؟»، بلکه این است: «آیا این تغییر در دادهٔ واقعیِ من رخ می‌دهد؟» اگر بله، بگذارش. اگر نه، فقط داری کارِ مدل را سخت‌تر می‌کنی. و تنها راهِ فهمیدنش، سنجیدن روی داده‌ای است که واقعاً شبیهِ دنیای واقعی باشد.

۴. تغییری که برچسب را می‌شکند#

# چرخشِ ۱۸۰ درجه: برای یک تصویرِ گربه بی‌ضرر، برای رقم فاجعه
rotated = torch.flip(image, dims=(0, 1))
# رقمِ ۶ چرخانده‌شده، ۹ می‌شود — و برچسبش هنوز ۶ نوشته شده

این دیگر augmentation نیست، تولیدِ دادهٔ غلط است. مدل یاد می‌گیرد که چیزی که ۹ به‌نظر می‌رسد گاهی ۶ است — و دقتش روی هر دو رقم خراب می‌شود.

تغییر برای رقمِ دست‌نویس برای عکسِ حیوان
جابه‌جاییِ کوچک
نویزِ کم
چرخشِ کوچک (چند درجه)
آینه‌کردنِ افقی (۲ و ۵ خراب می‌شوند)
چرخشِ ۱۸۰ درجه (۶ و ۹) معمولاً ✗
تغییرِ رنگ ✓، مگر رنگ خودش برچسب باشد

قاعدهٔ واحد: تغییری مجاز است که برچسب را عوض نکند. و این قاعده به دامنه بستگی دارد، نه به کتابخانه — همان تبدیلی که برای یک مسئله درست است، برای مسئلهٔ دیگر تقلب است.

۵. سه قاعدهٔ سخت#

قاعده چرا
فقط روی دادهٔ آموزش افزایشِ اعتبارسنجی یعنی داری معیارت را عوض می‌کنی، نه مدلت
در هر epoch تازه، نه یک بار از قبل یک بار ساختن یعنی مدل همان نسخه‌ها را هم حفظ می‌کند
داخلِ __getitem__ یا حلقهٔ batch همان دلیلِ بالا، از نظرِ پیاده‌سازی

⚠️ مواظب باش: رایج‌ترین اشتباهِ این حوزه، اعمالِ augmentation روی مجموعهٔ آزمون است. آن‌وقت عددی که گزارش می‌کنی، دقتِ مدل روی دادهٔ ساختگی است، نه روی دنیای واقعی — و معمولاً هم بهتر به‌نظر می‌رسد، که بدترش می‌کند. در ترمِ ۳ که با torchvision.transforms کار می‌کنیم، دو تبدیلِ جدا برای آموزش و ارزیابی می‌سازیم، دقیقاً به همین دلیل.

🔧 اگر کار نکرد: دقت با افزایش خیلی افت کرد — شدتِ تبدیل زیاد است؛ روی تصویرِ ۸×۸ حتی یک پیکسل هم تغییرِ بزرگی است. افزایش هیچ اثری ندارد — احتمالاً بیرونِ حلقهٔ batch اعمالش کرده‌ای و همان یک نسخه در همهٔ epochها تکرار می‌شود. نتیجه بینِ اجراها خیلی فرق می‌کندgenerator جداگانه برای تبدیل بگذار، همان‌طور که اینجا گذاشتیم.

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

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

کلمه تلفظ به حروف فارسی یعنی چه
data augmentation دیتا آگمنتیشن ساختنِ نسخه‌های تغییریافتهٔ برچسب‌حفظ‌شده
label-preserving لیبل-پریزروینگ تبدیلی که معنا و برچسب را عوض نمی‌کند
invariance اینوریانس بی‌اعتناییِ مدل به یک نوع تغییر
distribution shift دیستریبیوشن شیفت تفاوتِ توزیعِ آموزش و دنیای واقعی

تمرین‌ها

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

در فصل بعد#

پنج فصل، ده‌ها آزمایش. ولی هر عددی که تا اینجا گرفتی، فردا هم همان درمی‌آید؟ فصلِ بعد این را جواب می‌دهد: seed در سه کتابخانه، عدمِ قطعیتی که حتی seed هم حلش نمی‌کند، و checkpointی که واقعاً کامل است.

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

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