فاز 4 · درس 4

پیاده‌سازی پایپ‌لاین کامل طبقه‌بندی تصویر در PyTorch

نوع
ساختنی
زمان
حدود 75 دقیقه
زبان‌ها
Python
نمایی تمیز از یک خط لوله‌کشی شیشه‌ای شفاف که داده‌ها و رنگ‌های مختلف در آن جریان دارند و به یک منشور نوری برای تفکیک دسته‌ها متصل شده‌اند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. پایپ‌لاین طبقه‌بندی
  5. آنتروپی متقاطع، لاجیت‌ها و سافت‌مکس
  6. چرا افزایش داده کار می‌کند؟
  7. روش‌های Mixup و Cutmix
  8. هموارسازی برچسب‌ها (Label Smoothing)
  9. ارزیابی فراتر از دقت کلی
  10. پیاده‌سازی
  11. گام 1: ساخت یک دیتاست مصنوعی معین (deterministic)
  12. گام 2: نرمال‌سازی و افزایش داده
  13. گام 3: پیاده‌سازی Mixup
  14. گام 4: حلقه‌ی آموزش
  15. گام 5: سرهم‌کردن بخش‌ها
  16. گام 6: خواندن ماتریس درهم‌ریختگی
  17. استفاده‌ی عملی
  18. خروجی نهایی
  19. تمرین‌ها
  20. اصطلاحات کلیدی
  21. برای مطالعه‌ی بیشتر

بیشتر خطاهایی که در پروژه‌های بینایی ماشین مانع رسیدن مدل به دقت واقعی می‌شوند، ناشی از معماری شبکه نیستند؛ بلکه در پایپ‌لاین داده و حلقه‌ی آموزش رخ می‌دهند. اشتباهاتی مثل اعمال دوباره‌ی سافت‌مکس روی لاجیت‌ها، پدینگ نامناسب هنگام افزایش داده (data augmentation) یا صفر نکردن گرادیان‌ها، بدون اینکه هیچ خطایی پرتاب کنند، دقت مدل را تا 20 درصد کاهش می‌دهند.

در این درس یک پایپ‌لاین سرتاسری طبقه‌بندی تصویر (image classification) را از صفر در PyTorch می‌سازید. با ساخت یک دیتاست مصنوعی معین، تکنیک‌های پیشرفته‌ی پایدارسازی مانند Mixup و هموارسازی برچسب‌ها (label smoothing) را پیاده می‌کنید و یاد می‌گیرید که چطور فراتر از دقت کلی، با استفاده از ماتریس درهم‌ریختگی خطاهای مدل را ریشه‌یابی کنید.

کدهای این آموزش سبک طراحی شده‌اند و پیاده‌سازی آن‌ها به پردازنده‌های گرافیکی سنگین ابری نیاز ندارد؛ می‌توانید کل پایپ‌لاین را به‌راحتی روی CPU یا شتاب‌دهنده‌های محلی سیستم خود اجرا و آزمایش کنید.

یک طبقه‌بند (classifier) تابعی است که پیکسل‌ها را به یک توزیع احتمال روی کلاس‌ها نگاشت می‌کند. بقیه‌ی ماجرا فقط لوله‌کشی است.

اهداف یادگیری

  • ساخت یک پایپ‌لاین سرتاسری طبقه‌بندی تصویر روی CIFAR-10: دیتاست، افزایش داده (augmentation)، مدل، حلقه‌ی آموزش و ارزیابی
  • توضیح نقش هر بخش (dataloader، تابع زیان (loss)، بهینه‌ساز، زمان‌بند یا scheduler، افزایش داده) و پیش‌بینی نحوه‌ی بازتاب خرابی هر کدام روی منحنی یادگیری
  • پیاده‌سازی mixup، cutout و هموارسازی برچسب‌ها (label smoothing) از صفر و تحلیل این‌که چه‌زمانی ارزش استفاده دارند
  • خواندن ماتریس درهم‌ریختگی (confusion matrix) و جدول دقت/بازخوانی به ازای هر کلاس برای ریشه‌یابی خطاهای دیتاست و مدل، فراتر از دقت کلی

صورت مسئله

هر وظیفه‌ی بینایی ماشین که در عمل پیاده‌سازی می‌شود، در سطحی به طبقه‌بندی تصویر خلاصه می‌شود. تشخیص اشیا (detection) ناحیه‌ها را طبقه‌بندی می‌کند. بخش‌بندی تصویر (segmentation) پیکسل‌ها را طبقه‌بندی می‌کند. بازیابی اطلاعات (retrieval) بر اساس شباهت به مرکز خوشه‌ها (centroids) رتبه‌بندی انجام می‌دهد. پیاده‌سازی درست طبقه‌بندی — حلقه‌ی دیتاست، استراتژی افزایش داده، تابع زیان، ارزیابی — مهارتی است که به تمام وظایف دیگر این فاز منتقل می‌شود.

بیشتر باگ‌های طبقه‌بندی در مدل نیستند؛ در پایپ‌لاین خانه دارند: نرمال‌سازی نادرست، برنزدن داده‌های آموزش، افزایش داده‌ای که برچسب‌ها را تغییر می‌دهد، آلوده‌شدن داده‌های اعتبارسنجی با داده‌های آموزش، یا نرخ یادگیری که پس از دور (epoch) 30 بی‌صدا واگرا می‌شود. یک CNN که با تنظیمات درست به دقت 93% روی CIFAR-10 می‌رسد، با چنین خطاهایی معمولاً به 70 تا 75% افت می‌کند؛ در حالی که منحنی یادگیری در تمام مدت کاملاً معقول به نظر می‌رسد.

در این درس کل پایپ‌لاین را دستی سرهم می‌کنیم تا تک‌تک بخش‌های آن قابل بررسی باشند. از هیچ ابزاری در torchvision.datasets که باگی را پنهان کند استفاده نمی‌کنید.

مفاهیم پایه

پایپ‌لاین طبقه‌بندی

مجموعه‌داده(تصاویر + برچسب‌ها)افزایش داده(تبدیل‌های تصادفی)نرمال‌سازی(mean/std)دیتالودر(دسته‌بندی + برزدن)مدل(CNN)لاجیت‌ها(N, C)تابع زیان آنتروپی متقاطعArgmaxدر ارزیابیپس‌انتشارگام بهینه‌سازگام زمان‌بند

هر خط از این حلقه می‌تواند محل تولد یک باگ باشد. آنتروپی متقاطع لاجیت‌های خام را می‌گیرد نه خروجی‌های سافت‌مکس را؛ بنابراین هر بار فراخوانی model(x).softmax() قبل از تابع زیان، بی‌صدا گرادیان نادرستی محاسبه می‌کند. عملیات افزایش داده فقط روی ورودی‌ها اعمال می‌شود نه برچسب‌ها — مگر در mixup که هر دو را ترکیب می‌کند. optimizer.zero_grad() باید دقیقاً یک بار در هر گام اجرا شود؛ جا انداختن آن باعث انباشت گرادیان‌ها می‌شود و اثری مثل یک نرخ یادگیری بشدت ناپایدار می‌گذارد. هر یک از این باگ‌ها بدون اینکه خطایی پرتاب کنند، منحنی یادگیری را کاملاً تخت می‌کنند.

آنتروپی متقاطع، لاجیت‌ها و سافت‌مکس

یک طبقه‌بند به ازای هر تصویر C عدد تولید می‌کند که لاجیت‌ها نام دارند. اعمال سافت‌مکس آن‌ها را به یک توزیع احتمال تبدیل می‌کند:

softmax(z)i=exp⁡(zi)∑jexp⁡(zj)\mathop{\mathrm{softmax}}(z)_i = \frac{\exp(z_i)}{\sum_j \exp(z_j)}

آنتروپی متقاطع، منفیِ لگاریتم احتمال کلاس درست را اندازه می‌گیرد:

CE(z,y)=−log⁡(softmax(z)y)=−zy+log⁡(∑jexp⁡(zj))\begin{aligned} \mathrm{CE}(z, y) &= -\log\left( \mathop{\mathrm{softmax}}(z)_y \right) \\ &= -z_y + \log\left( \sum_j \exp(z_j) \right) \end{aligned}

فرم سمت راست، فرم محاسباتیِ پایدار است (log-sum-exp). تابع nn.CrossEntropyLoss در PyTorch محاسبات سافت‌مکس و NLL را در یک عمل ادغام می‌کند و لاجیت‌های خام را مستقیم می‌گیرد. اگر خودتان پیش از آن سافت‌مکس را اعمال کنید تقریباً همیشه باگ ساخته‌اید — مقداری نامفهوم یعنی log⁡(softmax(softmax(z)))\log(\mathop{\mathrm{softmax}}(\mathop{\mathrm{softmax}}(z))) را محاسبه خواهید کرد.

چرا افزایش داده کار می‌کند؟

یک CNN تورش استقرایی برای انتقال مکانی دارد (به‌خاطر اشتراک وزن‌ها)، اما هیچ ناورداییِ ذاتی نسبت به برش (crop)، چرخش افقی (flip)، نوسان رنگی (jitter) یا مسدود شدن بخشی از تصویر (occlusion) ندارد. تنها راه آموزش این ناوردایی‌ها به مدل، نشان‌دادن پیکسل‌هایی است که این حالت‌ها را تجربه کرده باشند. هر تبدیل تصادفی در طول آموزش به این معناست: «این دو تصویر برچسب یکسانی دارند؛ ویژگی‌هایی را یاد بگیر که این تفاوت‌ها را نادیده می‌گیرند.»

Original crop:  "dog facing left"
Flip:           "dog facing right"       <- same label, different pixels
Rotate(+15):    "dog, slight tilt"
Colour jitter:  "dog in warmer light"
RandomErasing:  "dog with patch missing"

قاعده‌ی کلی: افزایش داده باید برچسب را حفظ کند. برش تصادفی و چرخش روی یک رقم می‌تواند "6" را به "9" تبدیل کند؛ برای چنین مجموعه‌داده‌ای باید بازه‌ی چرخش کوچک‌تری انتخاب کنید و تبدیل‌هایی برگزینید که به ناوردایی‌های خاص ارقام دست نزنند.

روش‌های Mixup و Cutmix

افزایش داده‌ی عادی، پیکسل‌ها را تغییر می‌دهد اما برچسب‌ها را به‌صورت تک‌داغ (one-hot) حفظ می‌کند. روش‌های Mixup و Cutmix این ساختار را می‌شکنند و هر دو را درون‌یابی می‌کنند.

Mixup:
  lambda ~ Beta(a, a)
  x = lambda * x_i + (1 - lambda) * x_j
  y = lambda * y_i + (1 - lambda) * y_j

Cutmix:
  paste a random rectangle of x_j into x_i
  y = area-weighted mix of y_i and y_j

دلیل اثرگذاری: مدل به‌جای حفظ‌کردن مقادیر تک‌داغ و نقطه‌ای، یاد می‌گیرد میان کلاس‌ها درون‌یابی کند. تابع زیان آموزش بالا می‌رود، اما دقت اعتبارسنجی بهتر می‌شود. این کم‌هزینه‌ترین ارتقای پایداری برای هر طبقه‌بندی است.

هموارسازی برچسب‌ها (Label Smoothing)

هم‌خانواده‌ی mixup است. به‌جای آموزش روی [0, 0, 1, 0, 0]، روی [eps/C, eps/C, 1-eps+eps/C, eps/C, eps/C] با یک eps کوچک مثل 0.1 آموزش دهید (همان هدف تک‌داغ که با توزیعی یکنواخت مخلوط شده تا مجموع آن همچنان 1 بماند). این روش جلوی تولید لاجیت‌های بیش‌ازحد تیز را می‌گیرد و با هزینه‌ای نزدیک به صفر، کالیبراسیون را ارتقا می‌دهد. این قابلیت از نسخه 1.10 در PyTorch در nn.CrossEntropyLoss(label_smoothing=0.1) گنجانده شده است.

ارزیابی فراتر از دقت کلی

دقت تجمعی، عدم تعادل داده‌ها را پنهان می‌کند. یک طبقه‌بند دودویی روی داده‌های با نسبت 90-10 که مدام کلاس اکثریت را خروجی می‌دهد، دقت 90% ثبت می‌کند. ابزارهایی که وضعیت واقعی را نشان می‌دهند:

  • دقت به تفکیک کلاس (Per-class accuracy) — یک عدد برای هر کلاس؛ دسته‌های ضعیف را فوراً آشکار می‌کند.
  • ماتریس درهم‌ریختگی (Confusion matrix) — شبکه‌ای C×CC \times C که در آن سطر ii و ستون jj برابر است با تعداد نمونه‌های کلاس واقعی ii که کلاس jj پیش‌بینی شده‌اند؛ قطر اصلی پاسخ‌های درست را نشان می‌دهد و خانه‌های غیرقطری دقیقاً همان‌جایی است که اشتباهات مدل آشکار می‌شود.
  • دقت Top-1 / Top-5 — بررسی اینکه آیا کلاس درست در بین 1 یا 5 پیش‌بینی اول با بیشترین احتمال قرار دارد یا خیر؛ معیار Top-5 در ImageNet اهمیت زیادی دارد، چون کلاس‌هایی مثل "Norwich terrier" و "Norfolk terrier" واقعاً مبهم و مشابه‌اند.
  • کالیبراسیون (ECE) — آیا پیش‌بینی با ضریب اطمینان 0.8، در 80% مواقع درست است؟ شبکه‌های امروزی عموماً بیش‌اطمینانی (over-confident) سیستماتیک دارند؛ این مشکل با مقیاس‌بندی دما (temperature scaling) یا هموارسازی برچسب‌ها حل می‌شود.
میدان دید (Receptive Field)روی هم چیدن لایه‌ها
یک نورون در اعماق یک CNN کل تصویر را نمی‌بیند، بلکه فقط بخشی (patch) را می‌بیند که از طریق تمام لایه‌های زیرین به آن رسیده است. هر کرنل آن بخش را به اندازهٔ (1 − kernel) عریض‌تر می‌کند و هر گام (stride) دسترسیِ تمام لایه‌های بعدی را چند برابر می‌سازد. روی هم چیدن کرنل‌های کوچک روشی است که شبکه از طریق آن بدون نیاز به یک فیلتر غول‌پیکر، میدان دید بزرگی را به شکلی کم‌هزینه می‌سازد.

پیاده‌سازی

گام 1: ساخت یک دیتاست مصنوعی معین (deterministic)

دیتاست CIFAR-10 روی حافظه ذخیره می‌شود. برای اینکه این درس تکرارپذیر و سریع پیش برود، یک دیتاست مصنوعی شبیه به CIFAR می‌سازیم — تصاویری 32x32 به‌صورت RGB با الگوهای ویژه‌ی هر کلاس که مدل باید یاد بگیرد. دقیقاً همین پایپ‌لاین بدون تغییر روی CIFAR-10 واقعی هم کار می‌کند.

import numpy as np
import torch
from torch.utils.data import Dataset


def synthetic_cifar(num_per_class=1000, num_classes=10, seed=0):
    rng = np.random.default_rng(seed)
    X = []
    Y = []
    for c in range(num_classes):
        centre = rng.uniform(0, 1, (3,))
        freq = 2 + c
        for _ in range(num_per_class):
            yy, xx = np.meshgrid(np.linspace(0, 1, 32), np.linspace(0, 1, 32), indexing="ij")
            r = np.sin(xx * freq) * 0.5 + centre[0]
            g = np.cos(yy * freq) * 0.5 + centre[1]
            b = (xx + yy) * 0.5 * centre[2]
            img = np.stack([r, g, b], axis=-1)
            img += rng.normal(0, 0.08, img.shape)
            img = np.clip(img, 0, 1)
            X.append(img.astype(np.float32))
            Y.append(c)
    X = np.stack(X)
    Y = np.array(Y)
    idx = rng.permutation(len(X))
    return X[idx], Y[idx]


class ArrayDataset(Dataset):
    def __init__(self, X, Y, transform=None):
        self.X = X
        self.Y = Y
        self.transform = transform

    def __len__(self):
        return len(self.X)

    def __getitem__(self, i):
        img = self.X[i]
        if self.transform is not None:
            img = self.transform(img)
        img = torch.from_numpy(img).permute(2, 0, 1)
        return img, int(self.Y[i])

برای هر کلاس یک پالت رنگی و الگوی فرکانسی مجزا به‌همراه نویز گوسی تعریف می‌شود تا مدل مجبور شود به‌جای حفظ‌کردن پیکسل‌ها، سیگنال اصلی را یاد بگیرد. 10 کلاس، هر کدام 1000 تصویر، با ترتیب تصادفی.

گام 2: نرمال‌سازی و افزایش داده

دو تبدیلی که در هر پایپ‌لاین بینایی ماشین وجود دارند.

def standardize(mean, std):
    mean = np.array(mean, dtype=np.float32)
    std = np.array(std, dtype=np.float32)
    def _fn(img):
        return (img - mean) / std
    return _fn


def random_hflip(p=0.5):
    def _fn(img):
        if np.random.random() < p:
            return img[:, ::-1, :].copy()
        return img
    return _fn


def random_crop(pad=4):
    def _fn(img):
        h, w = img.shape[:2]
        padded = np.pad(img, ((pad, pad), (pad, pad), (0, 0)), mode="reflect")
        y = np.random.randint(0, 2 * pad)
        x = np.random.randint(0, 2 * pad)
        return padded[y:y + h, x:x + w, :]
    return _fn


def compose(*fns):
    def _fn(img):
        for fn in fns:
            img = fn(img)
        return img
    return _fn

قبل از برش، از پدینگ بازتابی (reflect-pad) استفاده کنید نه پدینگ با صفر؛ چون حاشیه‌های سیاه، الگوهایی ایجاد می‌کنند که مدل یاد می‌گیرد آن‌ها را به شکلی نامناسب نادیده بگیرد.

گام 3: پیاده‌سازی Mixup

دو تصویر و دو برچسب را داخل گام آموزش ترکیب می‌کند. این روش به‌صورت یک تبدیل روی دسته (batch) پیاده می‌شود تا به‌جای دیتاست، در کنار گذر رو به جلو قرار بگیرد.

def mixup_batch(x, y, num_classes, alpha=0.2):
    if alpha <= 0:
        return x, torch.nn.functional.one_hot(y, num_classes).float()
    lam = float(np.random.beta(alpha, alpha))
    idx = torch.randperm(x.size(0), device=x.device)
    x_mixed = lam * x + (1 - lam) * x[idx]
    y_onehot = torch.nn.functional.one_hot(y, num_classes).float()
    y_mixed = lam * y_onehot + (1 - lam) * y_onehot[idx]
    return x_mixed, y_mixed


def soft_cross_entropy(logits, soft_targets):
    log_probs = torch.log_softmax(logits, dim=-1)
    return -(soft_targets * log_probs).sum(dim=-1).mean()

تابع soft_cross_entropy همان آنتروپی متقاطع روی توزیع برچسب‌های نرم است و وقتی برچسب دقیقاً تک‌داغ باشد، به همان رابطه‌ی معمول تبدیل می‌شود.

گام 4: حلقه‌ی آموزش

دستورالعمل کامل: یک دور پیمایش داده‌ها، گرادیان‌ها یک‌بار در هر دسته، و به‌روزرسانی زمان‌بند در هر دور.

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torch.optim import SGD
from torch.optim.lr_scheduler import CosineAnnealingLR

def train_one_epoch(model, loader, optimizer, device, num_classes, use_mixup=True):
    model.train()
    total, correct, loss_sum = 0, 0, 0.0
    for x, y in loader:
        x, y = x.to(device), y.to(device)
        if use_mixup:
            x_m, y_soft = mixup_batch(x, y, num_classes)
            logits = model(x_m)
            loss = soft_cross_entropy(logits, y_soft)
        else:
            logits = model(x)
            loss = nn.functional.cross_entropy(logits, y, label_smoothing=0.1)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        loss_sum += loss.item() * x.size(0)
        total += x.size(0)
        # دقت آموزش در مقایسه با برچسب‌های ترکیب‌نشدهٔ `y` صرفاً یک تقریب است
        # وقتی mixup فعال است (مدل تارگت‌های soft را دید، نه y). به آن به عنوان یک...
        # rough progress signal; rely on val accuracy for real performance.
        with torch.no_grad():
            pred = logits.argmax(dim=-1)
            correct += (pred == y).sum().item()
    return loss_sum / total, correct / total


@torch.no_grad()
def evaluate(model, loader, device, num_classes):
    model.eval()
    total, correct = 0, 0
    loss_sum = 0.0
    cm = torch.zeros(num_classes, num_classes, dtype=torch.long)
    for x, y in loader:
        x, y = x.to(device), y.to(device)
        logits = model(x)
        loss = nn.functional.cross_entropy(logits, y)
        pred = logits.argmax(dim=-1)
        for t, p in zip(y.cpu(), pred.cpu()):
            cm[t, p] += 1
        loss_sum += loss.item() * x.size(0)
        total += x.size(0)
        correct += (pred == y).sum().item()
    return loss_sum / total, correct / total, cm

پنج اصلی که هر بار هنگام نوشتن حلقه‌ی آموزش باید بررسی کنید:

  1. قرار دادن model.train() قبل از آموزش و model.eval() قبل از ارزیابی — این کار رفتار dropout و batchnorm را تغییر می‌دهد.
  2. اجرای .zero_grad() قبل از .backward().
  3. استفاده از .item() هنگام ذخیره‌سازی معیارها تا هیچ داده‌ای گراف محاسباتی را در حافظه نگه ندارد.
  4. استفاده از @torch.no_grad() در زمان ارزیابی — در مصرف حافظه و زمان صرفه‌جویی می‌کند و جلوی خطاهای پنهان را می‌گیرد.
  5. اعمال Argmax روی لاجیت‌های خام به‌جای خروجی سافت‌مکس — خروجی تفاوتی ندارد، اما یک عملیات کمتر انجام می‌شود.

گام 5: سرهم‌کردن بخش‌ها

مدل TinyResNet را از درس قبلی بردارید، چند دور آموزش دهید و ارزیابی کنید.

from main import synthetic_cifar, ArrayDataset
from main import standardize, random_hflip, random_crop, compose
from main import mixup_batch, soft_cross_entropy
from main import train_one_epoch, evaluate
# TinyResNet از درس قبلی (03-cnns-lenet-to-resnet) می‌آید.
# مسیر import را متناسب با محل ذخیرهٔ کد درس قبلی تنظیم کنید.
from cnns_lenet_to_resnet import TinyResNet  # example placeholder

X, Y = synthetic_cifar(num_per_class=500)
split = int(0.9 * len(X))
X_train, Y_train = X[:split], Y[:split]
X_val, Y_val = X[split:], Y[split:]

mean = [0.5, 0.5, 0.5]
std = [0.25, 0.25, 0.25]
train_tf = compose(random_hflip(), random_crop(pad=4), standardize(mean, std))
eval_tf = standardize(mean, std)

train_ds = ArrayDataset(X_train, Y_train, transform=train_tf)
val_ds = ArrayDataset(X_val, Y_val, transform=eval_tf)

train_loader = DataLoader(train_ds, batch_size=128, shuffle=True, num_workers=0)
val_loader = DataLoader(val_ds, batch_size=256, shuffle=False, num_workers=0)

device = "cuda" if torch.cuda.is_available() else "cpu"
model = TinyResNet(num_classes=10).to(device)
optimizer = SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4, nesterov=True)
scheduler = CosineAnnealingLR(optimizer, T_max=10)

for epoch in range(10):
    tr_loss, tr_acc = train_one_epoch(model, train_loader, optimizer, device, 10, use_mixup=True)
    va_loss, va_acc, _ = evaluate(model, val_loader, device, 10)
    scheduler.step()
    print(f"epoch {epoch:2d}  lr {scheduler.get_last_lr()[0]:.4f}  "
          f"train {tr_loss:.3f}/{tr_acc:.3f}  val {va_loss:.3f}/{va_acc:.3f}")

روی دیتاست مصنوعی، این مدل طی 5 دور به دقت اعتبارسنجیِ نزدیک به 100% می‌رسد که هدف ما هم دقیقاً همین است: پایپ‌لاین بدون ایراد کار می‌کند و مدل توانایی یادگیری الگوها را دارد. دیتاست را با CIFAR-10 واقعی عوض کنید؛ همین حلقه بدون دستکاری تا دقت حدود 90% آموزش می‌بیند.

گام 6: خواندن ماتریس درهم‌ریختگی

دقت به‌تنهایی نشان نمی‌دهد که مدل در کدام بخش‌ها لغزش دارد؛ اما ماتریس درهم‌ریختگی این عیب‌ها را نمایان می‌کند.

def print_confusion(cm, labels=None):
    c = cm.shape[0]
    labels = labels or [str(i) for i in range(c)]
    print(f"{'':>6}" + "".join(f"{l:>5}" for l in labels))
    for i in range(c):
        row = cm[i].tolist()
        print(f"{labels[i]:>6}" + "".join(f"{v:>5}" for v in row))
    print()
    tp = cm.diag().float()
    fp = cm.sum(dim=0).float() - tp
    fn = cm.sum(dim=1).float() - tp
    prec = tp / (tp + fp).clamp_min(1)
    rec = tp / (tp + fn).clamp_min(1)
    f1 = 2 * prec * rec / (prec + rec).clamp_min(1e-9)
    for i in range(c):
        print(f"{labels[i]:>6}  prec {prec[i]:.3f}  rec {rec[i]:.3f}  f1 {f1[i]:.3f}")

_, _, cm = evaluate(model, val_loader, device, 10)
print_confusion(cm)

سطرها کلاس‌های واقعی و ستون‌ها پیش‌بینی‌ها هستند. اگر مقادیر غیرقطری بین کلاس‌های 3 و 5 تجمیع شده باشند، یعنی مدل این دو را با هم اشتباه می‌گیرد؛ این موضوع نقطه‌ی شروع مناسبی برای جمع‌آوری داده‌های هدفمند یا افزودن افزایش داده‌ی ویژه به آن کلاس است.

استفاده‌ی عملی

کتابخانه‌ی torchvision همه‌ی این مراحل را در قالب ابزارهایی استاندارد پیاده کرده است. برای CIFAR-10 واقعی، کل پایپ‌لاین شامل 4 خط کد به‌علاوه‌ی یک حلقه‌ی آموزش است.

from torchvision.datasets import CIFAR10
from torchvision.transforms import Compose, RandomCrop, RandomHorizontalFlip, ToTensor, Normalize

mean = (0.4914, 0.4822, 0.4465)
std = (0.2470, 0.2435, 0.2616)
train_tf = Compose([
    RandomCrop(32, padding=4, padding_mode="reflect"),
    RandomHorizontalFlip(),
    ToTensor(),
    Normalize(mean, std),
])
eval_tf = Compose([ToTensor(), Normalize(mean, std)])

train_ds = CIFAR10(root="./data", train=True,  download=True, transform=train_tf)
val_ds   = CIFAR10(root="./data", train=False, download=True, transform=eval_tf)

به دو نکته توجه کنید: میانگین و انحراف معیار وابسته به دیتاست هستند — این مقادیر روی مجموعه‌ی آموزش CIFAR-10 محاسبه شده‌اند، نه ImageNet — و پدینگ بازتابی نیز استراتژی پیش‌فرض جامعه‌ی کاربری برای برش تصویر است. کپی‌کردن آمار ImageNet در اینجا باعث افت حدود 1% در دقت می‌شود؛ خطایی پنهان که معمولاً کسی متوجه آن نمی‌شود مگر اینکه مدل را دقیق ارزیابی و پروفایل کند.

خروجی نهایی

این درس دو فایل زیر را تحویل می‌دهد:

  • outputs/prompt-classifier-pipeline-auditor.md — پرامپتی که اسکریپت آموزش را بر اساس پنج اصل بالا بررسی می‌کند و اولین مغایرت را گزارش می‌دهد.
  • outputs/skill-classification-diagnostics.md — ابزاری که با دریافت ماتریس درهم‌ریختگی و اسامی کلاس‌ها، خطاهای هر کلاس را تحلیل می‌کند و کارآمدترین راهکار اصلاحی را پیشنهاد می‌دهد.

تمرین‌ها

  1. (ساده) همین مدل را به مدت 5 دور، یک بار با mixup و یک بار بدون آن روی دیتاست مصنوعی آموزش دهید. نمودار تابع زیان آموزش و اعتبارسنجی را برای هر دو رسم کنید. توضیح دهید چرا در حالت mixup، تابع زیان آموزش بالاتر است اما دقت اعتبارسنجی برابر یا بهتر می‌شود.
  2. (متوسط) متد Cutout را پیاده کنید — یک مربع 8x8 تصادفی را در هر تصویر آموزش صفر کنید — و اثر آن را در قالب یک مطالعه‌ی حذفی (ablation) در مقایسه با این حالات بررسی کنید: بدون افزایش داده، hflip+crop، hflip+crop+cutout، و hflip+crop+mixup. دقت اعتبارسنجی هر حالت را گزارش دهید.
  3. (سخت) یک پایپ‌لاین CIFAR-100 (با 100 کلاس و همان ابعاد ورودی) بسازید و آموزش ResNet-34 را طوری بازتولید کنید که خطای آن نسبت به دقت مقالات زیر 1% باشد. بخش امتیازی: آزمایش 3 نرخ یادگیری و 2 مقدار کاهش وزن (weight decay)، ثبت نتایج در یک فایل CSV محلی، و ارائه‌ی جدول خطاهای عمده در ماتریس درهم‌ریختگی.

اصطلاحات کلیدی

اصطلاح در گفت‌وگوها معنای دقیق فنی
Logits «خروجی‌های خام» بردار قبل از سافت‌مکس شامل C عدد برای هر تصویر؛ تابع زیان آنتروپی متقاطع این مقادیر را می‌خواهد، نه خروجی‌های سافت‌مکس
Cross-entropy «تابع زیان» منفیِ لگاریتم احتمال کلاس درست؛ ادغامِ پایدار log-softmax و NLL در یک عملیات محاسباتی
DataLoader «دسته‌بندی‌کننده‌ی داده‌ها» ابزاری دور دیتاست برای برزدن، دسته‌بندی و بارگذاری داده‌ها (با امکان پردازش چندنخی)؛ عامل نیمی از باگ‌های آموزش
Augmentation «تبدیل‌های تصادفی» هر تبدیل در سطح پیکسل حین آموزش که برچسب را دست‌نخورده نگه دارد؛ ناوردایی‌هایی را که CNN ذاتاً ندارد به آن آموزش می‌دهد
Mixup / Cutmix «ترکیب دو تصویر» ادغام هم‌زمان ورودی‌ها و برچسب‌ها تا مدل به‌جای مرزهای سخت، درون‌یابیِ نرم را یاد بگیرد
Label smoothing «برچسب‌های ملایم‌تر» جایگزینی هدف تک‌داغ با توزیع (...,eps/C ,1-eps+eps/C)؛ کالیبراسیون را اصلاح می‌کند و کمی دقت را بالا می‌برد
Top-k accuracy «دقت Top-5» حضور کلاس درست در بین k پیش‌بینی با بیشترین احتمال؛ کاربردی برای دیتاست‌هایی با کلاس‌های مبهم و بسیار نزدیک
Confusion matrix «محل دیدن خطاها» جدولی C x C که درایه‌ی (i, j) آن نشان‌دهنده‌ی تعداد تصاویر کلاس واقعی i است که به‌اشتباه j پیش‌بینی شده‌اند؛ قطر اصلی پاسخ‌های درست را نشان می‌دهد و خانه‌های غیرقطری منشأ خطا را مشخص می‌کنند

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسمدل شما لاجیت‌های خام با ابعاد (N, C) تولید می‌کند. شما می‌نویسید `loss = cross_entropy(softmax(logits), y)`. چه مشکلی پیش می‌آید؟
  2. پیش از درسشما یک دسته‌بند 10 کلاسه را ارزیابی می‌کنید و به دقت 92% می‌رسید. کلاس 0 دارای 9,000 نمونه است؛ کلاس‌های 1 تا 9 در مجموع 1,000 نمونه دارند (تقریباً 111 نمونه برای هر کدام، در کل 10,000 نمونه). این عدد کلی چه چیزی را پنهان می‌کند؟
  3. پس از درستکنیک Mixup برچسب‌های one-hot را با اهداف پیوستهٔ درونیابی‌شده مانند lambda * y_i + (1-lambda) * y_j جایگزین می‌کند. چرا این کار به تعمیم‌پذیری کمک می‌کند؟
  4. پس از درسشما در دیتاست CIFAR-10 مقدار `RandomCrop(32, padding=4, padding_mode='zeros')` را با `padding_mode='reflect'` تعویض می‌کنید. چرا reflect در اینجا بهتر است؟
  5. پس از درسشما دسته‌بندی را آموزش می‌دهید و ماتریس درهم‌ریختگی نشان می‌دهد که بیشتر خطاها مربوط به پیش‌بینی کلاس 3 به جای کلاس 5 و کلاس 5 به جای کلاس 3 است. تأثیرگذارترین گام بعدی چیست؟

سؤالات متداول

چرا نباید قبل از nn.CrossEntropyLoss تابع softmax را اعمال کنیم؟

تابع CrossEntropyLoss در PyTorch بهینه‌سازی log-sum-exp را با منفیِ لگاریتم احتمال ترکیب می‌کند و لاجیت‌های خام را ورودی می‌گیرد؛ اعمال دستی سافت‌مکس قبل از آن، خروجی ریاضی بی‌معنی تولید کرده و گرادیان‌ها را مخدوش می‌کند.

تکنیک Mixup چه نقشی در آموزش طبقه‌بند تصویر دارد؟

میکس‌آپ هم‌زمان دو تصویر و برچسب‌های آن‌ها را با یک ضریب تصادفی درون‌یابی می‌کند. این کار مانع از حفظ کردن اهداف تک‌داغ (one-hot) توسط مدل شده و پایداری و دقت تست را افزایش می‌دهد.

تفاوت ماتریس درهم‌ریختگی با دقت کلی (Accuracy) چیست؟

دقت کلی خطاهای ناشی از عدم تعادل کلاس‌ها را پنهان می‌کند؛ در حالی که ماتریس درهم‌ریختگی نشان می‌دهد مدل دقیقاً کدام کلاس‌ها را به جای یکدیگر اشتباه گرفته است.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.