فاز 4 · درس 3

سیر تکامل شبکه‌های عصبی پیچشی: از LeNet تا ResNet

نوع
مفهومی و ساختنی
زمان
حدود 75 دقیقه
زبان‌ها
Python
چهار ماژول مکعبی شفاف در یک راستا که با یک خط فلزی منحنی به عنوان مسیر میان‌بر به هم متصل شده‌اند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. چهار ایده‌ای که بینایی ماشین را دگرگون کردند
  5. شبکه LeNet-5 (سال 1998)
  6. شبکه AlexNet (سال 2012)
  7. شبکه VGG (سال 2014)
  8. شبکه Inception (سال 2014، همان سال)
  9. مسئله افت عملکرد (Degradation Problem)
  10. شبکه ResNet (سال 2015)
  11. اهمیت اتصالات باقیمانده فراتر از بینایی ماشین
  12. پیاده‌سازی از صفر
  13. گام 1: پیاده‌سازی LeNet-5
  14. گام 2: پیاده‌سازی یک بلوک به سبک VGG
  15. گام 3: پیاده‌سازی BasicBlock در ResNet
  16. گام 4: ساخت یک مدل ResNet کوچک
  17. گام 5: مقایسه‌ی کارایی پارامترها نسبت به استخراج ویژگی
  18. استفاده از ابزارهای واقعی
  19. خروجی این درس
  20. تمرین‌ها
  21. اصطلاحات کلیدی
  22. برای مطالعه‌ی بیشتر

در طراحی سیستم‌های بینایی ماشین، انتخاب نادرست معماری مدل هزینه‌های پردازشی سنگینی به بار می‌آورد. بسیاری از توسعه‌دهندگان بدون ارزیابی نیاز مسئله، به سراغ بزرگ‌ترین مدل‌های موجود می‌روند؛ در حالی که شناخت سیر تکامل شبکه‌های عصبی پیچشی (CNN) نشان می‌دهد هر جهش تاریخی، پاسخی به یک محدودیت ساختاری مشخص بوده است.

این درس به شما کمک می‌کند خط تکامل LeNet-5 ،AlexNet ،VGG ،Inception و ResNet را بشناسید و درک کنید که چرا ایده‌ای ساده مثل اتصالات باقیمانده (residual connections)، بن‌بست افزایش عمق شبکه‌ها را حل کرد. این آموخته‌ها پایه‌ی شناخت بک‌بون‌های مدرن بینایی ماشین و حتی معماری‌های ترنسفورمر امروزی است.

تمام شبکه‌های عصبی پیچشی (CNN) مطرح سی سال گذشته از همان دستورالعمل همیشگی «کانولوشن–غیرخطی‌سازی–کاهش نمونه‌برداری» پیروی می‌کنند و فقط یک ایده‌ی جدید به آن اضافه کرده‌اند. این ایده‌ها را به ترتیب یاد بگیرید.

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

  • ردگیری مسیر تکامل معماری‌ها از LeNet-5 به AlexNet، VGG، Inception و در نهایت ResNet، و بیان تک‌ایده‌ی محوری هر خانواده
  • پیاده‌سازی LeNet-5، یک بلوک به سبک VGG و یک BasicBlock در ResNet با PyTorch (هر کدام در کمتر از 40 خط کد)
  • توضیح دلیل این‌که چرا اتصالات باقیمانده (residual connections) یک شبکه‌ی 1,000 لایه‌ایِ آموزش‌ناپذیر را به پیشرفته‌ترین مدل روز تبدیل می‌کنند
  • بررسی ساختار یک ستون‌فقرات یا بک‌بون (backbone) مدرن مثل ResNet-18 یا ResNet-50 و پیش‌بینی ابعاد خروجی، میدان دید (receptive field) و تعداد پارامترهای آن، پیش از نگاه کردن به سورس‌کد

صورت مسئله

در سال 2011، بهترین کلاسیفایر روی مجموعه‌داده‌ی ImageNet به دقتِ top-5 حدود 74% رسید. در سال 2012، AlexNet این عدد را به 85% رساند و در سال 2015، ResNet رکورد 96% را ثبت کرد. نه داده‌ی جدیدی در کار بود و نه نسل تازه‌ای از پردازنده‌های گرافیکی (GPU). تمام این پیشرفت‌ها حاصل ایده‌های معماری بود. هر مهندس بینایی ماشین باید بداند کدام ایده از کدام مقاله آمده است؛ چون هر بک‌بونی که در سال 2026 به محیط پروداکشن می‌فرستید، ترکیبی از همین تکه‌هاست. علاوه بر این، این مفاهیم مدام به حوزه‌های دیگر منتقل می‌شوند: کانولوشن‌های گروهی از CNNها وارد ترنسفورمرها شدند، اتصالات باقیمانده از ResNet به تک‌تک مدل‌های زبانی بزرگ (LLM) امروزی راه یافتند و نرمال‌سازی دسته‌ای (batch normalization) هنوز در مدل‌های دیفیوژن زنده است.

مطالعه‌ی این شبکه‌ها به ترتیب تاریخی، شما را در برابر یک اشتباه رایج واکسینه می‌کند: دست بردن به سمت بزرگ‌ترین مدل موجود، در حالی که شبکه‌ای در ابعاد LeNet مسئله را حل می‌کند. پایگاه داده‌ی MNIST نیازی به ResNet ندارد. شناخت منحنی مقیاس‌پذیری هر خانواده به شما می‌گوید کجای این طیف بایستید.

مفاهیم پایه

چهار ایده‌ای که بینایی ماشین را دگرگون کردند

1998LeNet-5لایه‌های Conv + pool +FC برای ارقام، آموزشروی CPU، 60 هزارپارامتر2012AlexNetعمیق‌تر + ReLU +dropout + دو GPU،پیروزی در ImageNet بااختلاف 10 امتیاز2014VGG / Inceptionپشته‌های 3x3 (VGG)،اندازه‌های موازی فیلتر(Inception)2015ResNetاتصالات میان‌بر همانیآموزش بیش از 100لایه را ممکن کردچهار ایده، چهار خانواده

در بینایی ماشین کلاسیک، هیچ چیز به اندازه‌ی این چهار جهش اهمیت نداشته است.

شبکه LeNet-5 (سال 1998)

سیستم بازشناسی ارقام دست‌نویس یان لکان (Yann LeCun). دارای 60,000 پارامتر، دو بلوک کانولوشن-ادغام (conv-pool)، دو لایه‌ی کاملاً متصل (fully connected) و توابع فعال‌ساز tanh. این شبکه الگویی را ساخت که به ارث تمام CNNهای بعدی تبدیل شد:

input (1, 32, 32)
  conv 5x5 -> (6, 28, 28)
  avg pool 2x2 -> (6, 14, 14)
  conv 5x5 -> (16, 10, 10)
  avg pool 2x2 -> (16, 5, 5)
  flatten -> 400
  dense -> 120
  dense -> 84
  dense -> 10

هر چیزی که امروز در دنیای مدرن CNN نامیده می‌شود — تکرار متناوب کانولوشن و کاهش نمونه‌برداری (downsampling) که به یک سر کلاسیفایرِ کوچک ختم می‌شوند — همان LeNet است؛ فقط با لایه‌های بیشتر، کانال‌های فراخ‌تر و توابع فعال‌ساز کارآمدتر.

شبکه AlexNet (سال 2012)

سه تغییر کلیدی که در کنار هم رکورد ImageNet را شکستند:

  1. تابع ReLU به جای tanh. دیگر خبری از محوشدگی گرادیان (vanishing gradient) نبود و سرعت آموزش 6 برابر شد.
  2. لایه Dropout در بخش کاملاً متصل انتهایی. تنظیم‌گری (regularization) به یک لایه‌ی مستقل تبدیل شد، نه صرفاً یک ترفند محاسباتی.
  3. عمق و پهنای بیشتر. پنج لایه‌ی کانولوشن، سه لایه‌ی متراکم (dense layer)، 60 میلیون پارامتر؛ آموزش‌دیده روی دو پردازنده‌ی گرافیکی که مدل بین آن‌ها تقسیم شده بود.

شکل 2 در مقاله‌ی اصلی هنوز این تقسیم روی GPU را به‌صورت دو جریان موازی نشان می‌دهد. این موازی‌سازی یک راهکار سخت‌افزاری بود، نه یک بینش معماری؛ اما آن سه ایده‌ی اصلی هنوز در تمام مدل‌های امروزی حضور دارند.

شبکه VGG (سال 2014)

پژوهشگران VGG این پرسش را مطرح کردند: اگر فقط از کانولوشن‌های 3x3 استفاده کنیم و شبکه را بسیار عمیق بسازیم، چه می‌شود؟

stack:   conv 3x3 -> conv 3x3 -> pool 2x2
repeat:  16 or 19 conv layers

دو کانولوشن 3x3 پشت سر هم، همان ناحیه‌ی ورودی 5x5 را می‌بینند که یک کانولوشن 5x5 پوشش می‌دهد؛ اما با پارامترهای کمتر (2⋅9⋅C2=18C22 \cdot 9 \cdot C^2 = 18C^2 در برابر 25⋅C225 \cdot C^2) و یک لایه ReLU اضافی در میان آن‌ها. VGG این مشاهده را تبدیل به یک معماری کامل کرد. سادگی کار — تکرار مداومِ تنها یک نوع بلوک — آن را به نقطه‌ی مرجع تمام مدل‌های بعد از خود بدل کرد.

بهای این کار: 138 میلیون پارامتر، سرعت آموزش پایین و هزینه‌ی پردازشی سنگین در زمان استنتاج (inference).

شبکه Inception (سال 2014، همان سال)

پاسخ گوگل به این پرسش که «از چه اندازه‌ی کرنلی استفاده کنم؟» این بود: همه‌ی اندازه‌ها، به‌صورت موازی.

نقشه ویژگی ورودیکانولوشن 1x1کانولوشن 3x3کانولوشن 5x5ماکس‌پولینگ 3x3الحاقدر امتداد محور کانالبلاک بعدی

هر شاخه (branch) تخصصی عمل می‌کند: کرنل 1x1 برای ترکیب کانال‌ها، 3x3 برای بافت‌های محلی، 5x5 برای الگوهای بزرگ‌تر و ادغام (pooling) برای استخراج ویژگی‌های ناوردا نسبت به جابه‌جایی. در نهایت لایه‌ی الحاق (concat) به لایه‌ی بعد اجازه می‌دهد هر شاخه‌ای را که مفیدتر است انتخاب کند. Inception v1 از کانولوشن‌های 1x1 در داخل هر شاخه به عنوان گلوگاه (bottleneck) استفاده کرد تا تعداد پارامترها منطقی بماند.

مسئله افت عملکرد (Degradation Problem)

تا سال 2015، مدل VGG-19 به‌خوبی کار می‌کرد، اما VGG-32 شکست می‌خورد. قرار بود افزایش عمق به بهبود مدل کمک کند، اما فراتر از حدود 20 لایه، تابع زیان (loss) در هر دو مرحله‌ی آموزش و آزمون بدتر می‌شد. پای بیش‌برازش (overfitting) در میان نبود؛ بهینه‌ساز به این دلیل در پیدا کردن وزن‌های کارآمد شکست می‌خورد که گرادیان‌ها در گذر از هر لایه به‌صورت ضربی کوچک می‌شدند و افت می‌کردند.

Plain deep network:
  y = f_L( f_{L-1}( ... f_1(x) ... ) )

Gradient wrt early layer:
  dL/dW_1 = dL/dy * df_L/df_{L-1} * ... * df_2/df_1 * df_1/dW_1

Each multiplicative term has magnitude roughly (weight magnitude) * (activation gain).
Stack 100 of them with gains < 1 and the gradient is effectively zero.

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

شبکه ResNet (سال 2015)

تیم پژوهشی شامل He، Zhang، Ren و Sun یک تغییر را پیشنهاد دادند که همه چیز را حل کرد:

standard block:y=F(x)residual block:y=F(x)+x\begin{aligned} \text{standard block:} \quad & y = F(x) \\ \text{residual block:} \quad & y = F(x) + x \end{aligned}

عبارت + x یعنی لایه همیشه می‌تواند با صفر کردن F(x)، خروجی را بدون تغییر رد کند و دست به کاری نزند. حالا یک ResNet با 1,000 لایه در بدترین حالت عملکردی مشابه یک شبکه‌ی 1 لایه‌ای دارد، چون هر بلوکِ اضافه یک مسیر فرار ساده در اختیار دارد. با این تضمین، بهینه‌ساز می‌تواند هر بلوک را کمی مفیدتر کند؛ و همین فایده‌های اندک وقتی در 100 لایه روی هم قرار می‌گیرند، رکوردهای جهانی را جابه‌جا می‌کنند.

میان‌بر همانیورودی xF(x)conv + BN + ReLUconv + BN+ReLUy

دو نسخه‌ی پرکاربرد از این بلوک همه‌جا دیده می‌شوند:

  • بلوک BasicBlock (در ResNet-18 و ResNet-34): دو کانولوشن 3x3 به همراه یک اتصال جهشی (skip connection) دور هر دو.
  • بلوک Bottleneck (در ResNet-50، ResNet-101 و ResNet-152): لایه‌ی 1x1 برای کاهش، 3x3 در میانه، 1x1 برای افزایش، همراه با اتصال جهشی دور این سه لایه. این روش هنگام بالا بودن تعداد کانال‌ها بسیار کم‌هزینه‌تر است.

وقتی اتصال جهشی باید از روی یک مرحله‌ی کاهش نمونه‌برداری (stride=2) عبور کند، مسیر همانی (identity) با یک کانولوشن 1x1 با گام 2 جایگزین می‌شود تا ابعاد تنسورها هم‌خوان شوند.

اهمیت اتصالات باقیمانده فراتر از بینایی ماشین

این ایده صرفاً مربوط به دسته‌بندی تصویر نبود؛ بلکه شبکه‌های عمیق را از یک بازی شانسی مبتنی بر «دعا کنیم گرادیان سالم برسد» به ابزاری مهندسی، قابل اعتماد و مقیاس‌پذیر تبدیل کرد. تک‌تک مدل‌های ترنسفورمری که در فاز بعدی بررسی می‌کنیم، دقیقاً همین اتصال جهشی را در تک‌تک بلوک‌های خود دارند. بدون ResNet، مدلی به نام GPT وجود نداشت.

پولینگ (POOLING)بیشینه یا میانگین
POOLINGPooling downsamples a feature map by summarizing each window into one value. A 2x2 window with stride 2 halves both dimensions, turning the 4x4 grid into 2x2. Max pooling forwards the strongest activation in each window (highlighted); average pooling forwards the mean. The result is smaller, translation-tolerant, and cheaper for the next layer.ورودی 4x4خروجی 2x238215194726314088978
پولینگ با خلاصه کردن هر پنجره در یک مقدار، ابعاد نقشه ویژگی را کاهش می‌دهد. یک پنجره 2x2 با گام (stride) برابر با 2، هر دو بعد را نصف کرده و شبکه 4x4 را به 2x2 تبدیل می‌کند. مکس پولینگ قوی‌ترین فعال‌سازی را در هر پنجره ارسال می‌کند (مشخص‌شده)؛ میانگین‌گیری (average pooling) مقدار میانگین را منتقل می‌کند. نتیجه کوچک‌تر، مقاوم در برابر جابه‌جایی و برای لایه بعدی کم‌هزینه‌تر است.

پیاده‌سازی از صفر

گام 1: پیاده‌سازی LeNet-5

یک پیاده‌سازی مینیمال و وفادار به ساختار اصلی LeNet؛ همراه با توابع فعال‌ساز tanh و ادغام میانگین‌گیر (average pooling). تنها سازش ما با متدهای مدرن، استفاده از nn.CrossEntropyLoss در خروجی به جای اتصالات گاوسی نسخه‌ی اصلی است.

import torch
import torch.nn as nn
import torch.nn.functional as F

class LeNet5(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5)
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
        self.pool = nn.AvgPool2d(2)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, num_classes)

    def forward(self, x):
        x = self.pool(torch.tanh(self.conv1(x)))
        x = self.pool(torch.tanh(self.conv2(x)))
        x = torch.flatten(x, 1)
        x = torch.tanh(self.fc1(x))
        x = torch.tanh(self.fc2(x))
        return self.fc3(x)

net = LeNet5()
x = torch.randn(1, 1, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

خروجی مورد انتظار: output: torch.Size([1, 10]) و params: 61,706. این تمام کلاسیفایر ارقامی است که بینایی ماشین مدرن را پایه‌گذاری کرد.

گام 2: پیاده‌سازی یک بلوک به سبک VGG

یک بلوک با قابلیت استفاده‌ی مجدد: دو کانولوشن 3x3، تابع ReLU، نرمال‌سازی دسته‌ای و ادغام بیشینه (max pool).

class VGGBlock(nn.Module):
    def __init__(self, in_c, out_c):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_c)
        self.pool = nn.MaxPool2d(2)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        return self.pool(x)

class MiniVGG(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stack = nn.Sequential(
            VGGBlock(3, 32),
            VGGBlock(32, 64),
            VGGBlock(64, 128),
        )
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        return self.head(self.stack(x))

net = MiniVGG()
x = torch.randn(1, 3, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

سه بلوک VGG روی ورودی در ابعاد CIFAR، یک ادغام تطبیقی (adaptive pool) و یک لایه‌ی خطی؛ با حدود 290 هزار پارامتر که برای CIFAR-10 کاملاً کافی است.

گام 3: پیاده‌سازی BasicBlock در ResNet

بلوک ساختمانی اصلی برای شبکه‌های ResNet-18 و ResNet-34.

class BasicBlock(nn.Module):
    def __init__(self, in_c, out_c, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_c)
        if stride != 1 or in_c != out_c:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_c, out_c, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_c),
            )
        else:
            self.shortcut = nn.Identity()

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out = out + self.shortcut(x)
        return F.relu(out)

تنظیم bias=False در لایه‌های کانولوشن یک قرارداد مرسوم در کنار نرمال‌سازی دسته‌ای است؛ پارامتر بتای نرمال‌سازی دسته‌ای کار بایاس را انجام می‌دهد، بنابراین نگه‌داشتن بایاس در کانولوشن صرفاً هدر دادن منابع است. متغیر shortcut تنها زمانی به یک لایه‌ی کانولوشن واقعی نیاز دارد که گام حرکتی (stride) یا تعداد کانال‌ها تغییر کند؛ در غیر این صورت یک نگاشت همانی بدون هزینه (no-op) خواهد بود.

گام 4: ساخت یک مدل ResNet کوچک

قرار دادن چهار گروه از بلوک‌های BasicBlock روی هم برای ساخت یک ResNet کاربردی متناسب با ابعاد ورودی CIFAR.

class TinyResNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stem = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
        )
        self.layer1 = self._make_group(32, 32, num_blocks=2, stride=1)
        self.layer2 = self._make_group(32, 64, num_blocks=2, stride=2)
        self.layer3 = self._make_group(64, 128, num_blocks=2, stride=2)
        self.layer4 = self._make_group(128, 256, num_blocks=2, stride=2)
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(256, num_classes),
        )

    def _make_group(self, in_c, out_c, num_blocks, stride):
        blocks = [BasicBlock(in_c, out_c, stride=stride)]
        for _ in range(num_blocks - 1):
            blocks.append(BasicBlock(out_c, out_c, stride=1))
        return nn.Sequential(*blocks)

    def forward(self, x):
        x = self.stem(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        return self.head(x)

net = TinyResNet()
x = torch.randn(1, 3, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

چهار گروه که هر کدام دو بلوک دارند. گام حرکتی 2 در ابتدای گروه‌های 2، 3 و 4 اعمال می‌شود. تعداد کانال‌ها با هر بار کاهش ابعاد، دو برابر می‌شود. این شبکه تقریباً 2.8 میلیون پارامتر دارد. این همان فرمول استانداردی است که به شکلی تمیز تا ResNet-152 مقیاس‌پذیر می‌شود.

گام 5: مقایسه‌ی کارایی پارامترها نسبت به استخراج ویژگی

ارسال یک ورودی یکسان به هر سه شبکه و مقایسه‌ی تعداد پارامترهای آن‌ها.

def summary(name, net, x):
    y = net(x)
    params = sum(p.numel() for p in net.parameters())
    print(f"{name:12s}  input {tuple(x.shape)} -> output {tuple(y.shape)}  params {params:>10,}")

x = torch.randn(1, 3, 32, 32)
summary("LeNet5",     LeNet5(),       torch.randn(1, 1, 32, 32))
summary("MiniVGG",    MiniVGG(),      x)
summary("TinyResNet", TinyResNet(),   x)

سه مدل، سه دوره و سه مرتبه‌ی بزرگی در تعداد پارامترها. برای رسیدن به دقت مناسب روی CIFAR-10 پس از چند دور آموزش، وضعیت تقریبی چنین خواهد بود: مدل LeNet حدود 60%، مدل MiniVGG حدود 89% و TinyResNet حدود 93%.

استفاده از ابزارهای واقعی

کتابخانه‌ی torchvision.models نسخه‌های از پیش آموزش‌دیده‌ی (pretrained) تمام این مدل‌ها را در اختیارتان می‌گذارد. ساختار فراخوانی در همه‌ی این خانواده‌ها یکسان است، و اتفاقاً هدف اصلی مفهوم انتزاعی بک‌بون هم همین است.

from torchvision.models import resnet18, ResNet18_Weights, vgg16, VGG16_Weights

r18 = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
r18.eval()

print(f"ResNet-18 params: {sum(p.numel() for p in r18.parameters()):,}")
print(r18.layer1[0])
print()

v16 = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)
v16.eval()
print(f"VGG-16   params: {sum(p.numel() for p in v16.parameters()):,}")

مدل ResNet-18 حدود 11.7 میلیون پارامتر دارد؛ در حالی که VGG-16 دارای 138 میلیون پارامتر است، اما دقت top-1 آن‌ها روی ImageNet تقریباً در یک سطح است (69.8% در برابر 71.6%). اتصالات باقیمانده بازدهی پارامترها را 12 برابر بهتر می‌کنند. به همین دلیل مشتقات ResNet از سال 2016 تا زمان ظهور ViT در سال 2021 بر این حوزه مسلط بودند — و هنوز هم در محیط‌های عملیاتی واقعی که محدودیت پردازشی دارند، انتخاب اول محسوب می‌شوند.

برای یادگیری انتقالی (transfer learning)، دستورالعمل همیشه ثابت است: بارگذاری وزن‌های از پیش آموزش‌دیده، فریز کردن بک‌بون و جایگزین کردن سر کلاسیفایر.

for p in r18.parameters():
    p.requires_grad = False
r18.fc = nn.Linear(r18.fc.in_features, 10)

تنها سه خط کد. حالا یک کلاسیفایر 10 کلاسه برای CIFAR دارید که از همان بازنمایی‌های آموزش‌دیده روی ImageNet بهره می‌برد.

خروجی این درس

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

  • outputs/prompt-backbone-selector.md — یک پرامپت برای انتخاب خانواده‌ی مناسب CNN (شامل LeNet، VGG، ResNet، MobileNet و ConvNeXt) بر اساس نوع تسک، حجم داده و محدودیت منابع پردازشی.
  • outputs/skill-residual-block-reviewer.md — مهارتی که کدهای ماژول PyTorch را بررسی می‌کند و خطاهای اتصالات جهشی را تشخیص می‌دهد (مانند جا افتادن shortcut هنگام تغییر stride، ترتیب اعمال فعال‌ساز در مسیر میان‌بر، و موقعیت قرارگیری نرمال‌سازی دسته‌ای نسبت به عمل جمع).

تمرین‌ها

  1. (ساده) پارامترهای مدل TinyResNet را لایه به لایه به‌صورت دستی محاسبه کنید. نتیجه را با خروجی sum(p.numel() for p in net.parameters()) بسنجید. بیشترین سهم پارامترها به کدام بخش اختصاص دارد: کانولوشن‌ها، نرمال‌سازی دسته‌ای یا سر کلاسیفایر؟
  2. (متوسط) بلوک Bottleneck را (مسیر 1x1 -> 3x3 -> 1x1 به همراه اتصال جهشی) پیاده‌سازی کنید و از آن برای ساخت شبکه‌ای به سبک ResNet-50 برای مجموعه‌داده‌ی CIFAR بهره ببرید. تعداد پارامترهای آن را با TinyResNet مقایسه کنید.
  3. (سخت) اتصال جهشی را از BasicBlock حذف کنید؛ سپس یک شبکه‌ی بدون اتصالِ 34 بلوکی و یک ResNet استاندارد 34 بلوکی را به مدت 10 دور (epoch) روی CIFAR-10 آموزش دهید. نمودار تابع زیان آموزش بر حسب epoch را برای هر دو رسم کنید. نتیجه‌ی شکل 1 در مقاله‌ی He و همکاران را بازتولید کنید؛ جایی که شبکه‌ی عمیقِ بدون اتصال جهشی، نسبت به همتای کم‌عمق‌تر خود در مقدار زیان بالاتری همگرا می‌شود.

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

اصطلاح آنچه افراد می‌گویند معنای واقعی آن
بک‌بون (Backbone) «خودِ مدل» مجموعه‌ای از بلوک‌های کانولوشنی که نقشه‌ی ویژگی (feature map) را برای ارسال به سرِ تسک می‌سازد
اتصال باقیمانده (Residual connection) «اتصال جهشی (skip connection)» فرمول y = F(x) + x؛ به بهینه‌ساز اجازه می‌دهد با صفر کردن تابع F، تابع همانی را یاد بگیرد؛ قابلیتی که آموزش عمق‌های دلخواه را ممکن می‌کند
بلوک BasicBlock «دو کانولوشن 3x3 با یک پرش» بلوک سازنده‌ی ResNet-18 و ResNet-34: شامل کانولوشن-BN-ReLU-کانولوشن-BN-جمع-ReLU
بلوک Bottleneck «1x1 کاهشی، 3x3، 1x1 افزایشی» بلوک شبکه‌های ResNet-50/101/152؛ در کانال‌های زیاد بسیار به‌صرفه است، چون کانولوشن 3x3 روی پهنای کاهش‌یافته اجرا می‌شود
مسئله افت عملکرد (Degradation problem) «شبکه عمیق‌تر، نتیجه بدتر» فراتر از حدود 20 لایه‌ی کانولوشن معمولی، خطای آموزش و آزمون هر دو بالا می‌روند؛ راه‌حل آن اتصالات باقیمانده است، نه داده‌ی بیشتر
ساقه (Stem) «لایه‌ی اول» کانولوشن ابتدایی که ورودی 3 کاناله را به پهنای ویژگی پایه تبدیل می‌کند؛ معمولاً 7x7 با گام 2 برای ImageNet و 3x3 با گام 1 برای CIFAR
سر (Head) «کلاسیفایر» لایه‌های پس از آخرین بلوک بک‌بون: شامل ادغام تطبیقی، مسطح‌سازی (flatten) و لایه‌های خطی
یادگیری انتقالی (Transfer learning) «وزن‌های از پیش آموزش‌دیده» بارگذاری بک‌بونی که روی ImageNet آموزش دیده و فاین‌تیونینگ اختصاصیِ بخش سر برای مسئله‌ی شما

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

خودآزمایی

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

  1. پیش از درسمدل الکس‌نت (AlexNet 2012) چه ایده معماری منحصربه‌فردی معرفی کرد که آموزش CNNهای عمیق روی GPU را عملی ساخت؟
  2. پیش از درسچرا VGG پشته‌ای از کانولوشن‌های 3x3 را به یک کرنل بزرگ‌تر ترجیح داد؟
  3. پس از درسمدل ResNet اتصالات باقی‌مانده را به‌صورت y = F(x) + x معرفی کرد. این کار چه مشکلی را حل می‌کند؟
  4. پس از درسدر BasicBlock مدل ResNet با in_channels=64، out_channels=128 و stride=2، نقش branch میانبر (shortcut) چیست؟
  5. پس از درسمدل ResNet-18 حدود 11.7 میلیون پارامتر دارد و در ImageNet عملکردی برابر یا بهتر از VGG-16 با 138 میلیون پارامتر نشان می‌دهد. این موضوع بیانگر چه چیزی درباره VGG است؟

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

مسئله افت عملکرد (degradation problem) در شبکه‌های عصبی چیست و چگونه حل شد؟

در شبکه‌های عمیق بدون اتصالات جهشی، با عبور از حدود 20 لایه، خطای آموزش و آزمون به دلیل محو شدن گرادیان‌ها افزایش می‌یافت. معماری ResNet با معرفی اتصالات باقیمانده (y = F(x) + x) به بهینه‌ساز اجازه داد لایه‌های اضافه را در صورت لزوم خنثی کند و امکان آموزش شبکه‌هایی با بیش از 1,000 لایه را فراهم سازد.

چرا VGG به جای کرنل‌های بزرگ از چند لایه با کرنل 3x3 استفاده کرد؟

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

تفاوت بلوک BasicBlock و Bottleneck در ResNet چیست؟

بلوک BasicBlock شامل دو کانولوشن 3x3 است و در ResNet-18 و ResNet-34 استفاده می‌شود. بلوک Bottleneck که در ResNet-50 و بالاتر به کار می‌رود، از ترکیب 1x1 کاهشی، 3x3 و 1x1 افزایشی استفاده می‌کند تا محاسبات در تعداد کانال‌های بالا بهینه بماند.

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

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