فاز 4 · درس 2

پیاده‌سازی کانولوشن از صفر: از حلقه‌های تودرتو تا ترفند im2col

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

اگر برای پردازش یک تصویر 224x224 سراغ لایه‌های تمام‌متصل (Fully Connected) بروید، هر نورون فقط در ورودی به بیش از 150 هزار وزن نیاز دارد؛ ساختاری که میلیون‌ها پارامتر می‌بلعد و حتی درکی از موقعیت اشیا و جابه‌جایی آن‌ها در تصویر ندارد. کانولوشن با دو ایده‌ی بنیادینِ اشتراک وزن‌ها و موضعی دیدن پیکسل‌ها، حجم پارامترها را مهار می‌کند و پایه و اساس بینایی ماشین مدرن را شکل می‌دهد.

در این درس به‌جای تکیه‌ی کورکورانه بر کتابخانه‌ها، لایه‌ی کانولوشن دوبعدی را از صفر و صرفاً با NumPy پیاده‌سازی می‌کنید. ابتدا آن را با حلقه‌های تودرتو می‌سازید تا ریاضیات پدینگ (Padding) و استراید (Stride) را لمس کنید، سپس با ترفند im2col عملیات را به یک ضرب ماتریسی پرسرعت تبدیل می‌کنید؛ دقیقاً همان روشی که فریم‌ورک‌های صنعتی و موتورهای محاسباتی برای اجرای بهینه روی سخت‌افزار به کار می‌برند.

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

کانولوشن در واقع یک لایه‌ی متراکم کوچک است که آن را روی تصویر می‌لغزانید و در تمام نقاط از وزن‌های یکسانی استفاده می‌کنید.

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

  • پیاده‌سازی کانولوشن دوبعدی از صفر صرفاً با NumPy؛ شامل نسخه‌ی مبتنی بر حلقه‌های تودرتو و نسخه‌ی برداری‌شده با im2col
  • محاسبه‌ی ابعاد مکانی خروجی برای هر ترکیبی از ابعاد ورودی، ابعاد کرنل، پدینگ (padding) و استراید (stride)، به‌همراه درک و اثبات فرمول (H - K + 2P) / S + 1
  • طراحی دستی کرنل‌ها (لبه، تاری، وضوح‌بخشی و سوبل) و تحلیل چرایی شکل‌گیری الگوهای اکتیویشن ناشی از هرکدام
  • چیدن لایه‌های کانولوشن روی هم برای ساخت یک استخراج‌کننده‌ی ویژگی و پیوند دادن عمق این پشته با ابعاد میدان دید (receptive field)

صورت مسئله

یک لایه‌ی تمام‌متصل (fully connected) روی یک تصویر RGB با ابعاد 224x224، به‌ازای هر نورون به 224 * 224 * 3 = 150,528 وزن ورودی نیاز دارد. یک لایه‌ی پنهان با 1,000 واحد، پیش از آنکه اصلاً الگوی مفیدی یاد بگیرد، 150 میلیون پارامتر مصرف می‌کند. بدتر از آن، چنین لایه‌ای نمی‌فهمد که تصویر سگ در بالا سمت چپ و همان سگ در پایین سمت راست، یک الگوی یکسان هستند. این لایه با هر موقعیت پیکسلی کاملاً مستقل برخورد می‌کند؛ رویکردی که برای داده‌های تصویری کاملاً غلط است: جابه‌جا شدن یک گربه به‌اندازه‌ی سه پیکسل نباید شبکه را وادار کند که این مفهوم را دوباره از صفر یاد بگیرد.

دو ویژگی بنیادین برای هر مدل تصویری عبارت‌اند از: هم‌واریانس با انتقال (translation equivariance به‌معنای اینکه با جابه‌جایی ورودی، خروجی نیز جابه‌جا شود) و اشتراک پارامترها (parameter sharing به‌معنای اینکه یک آشکارساز ویژگیِ ثابت در تمام تصویر اجرا شود). لایه‌های متراکم هیچ‌یک از این دو را ندارند؛ اما کانولوشن هر دو را رایگان در اختیارتان می‌گذارد.

کانولوشن برای یادگیری عمیق اختراع نشد؛ این عملیات همان موتور محرک فشرده‌سازی JPEG، تاری گاوسی در Photoshop، لبه‌یابی در بینایی ماشین صنعتی و تک‌تک فیلترهای صوتی است. دلیل یکه‌تازیِ شبکه‌های عصبی کانولوشنی (CNN) در مسابقات ImageNet بین سال‌های 2012 تا 2020 این بود که کانولوشن دقیقاً همان پیشینِ (prior) درست برای داده‌هایی است که در آن‌ها مقادیر مجاور به هم وابسته‌اند و یک الگوی مشخص می‌تواند در هر نقطه‌ای ظاهر شود.

مفاهیم پایه

لغزش یک کرنل منفرد

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

خروجی (H-2 x W-2)کرنل (3 x 3)ورودی (H x W)لغزش کرنلمحاسبه ضرب داخلیدر هر موقعیتتصویر 5 x 5وزن‌هایآموزش‌دیدهنقشه 3 x 3

یک مثال ملموس از کرنل 3x3 روی ورودی 5x5 (بدون پدینگ، با استراید 1):

Input X (5 x 5):                Kernel W (3 x 3):

  1  2  0  1  2                   1  0 -1
  0  1  3  1  0                   2  0 -2
  2  1  0  2  1                   1  0 -1
  1  0  2  1  3
  2  1  1  0  1

The kernel slides across every valid 3 x 3 window. Output Y is 3 x 3:

 Y[0,0] = sum( W * X[0:3, 0:3] )
 Y[0,1] = sum( W * X[0:3, 1:4] )
 Y[0,2] = sum( W * X[0:3, 2:5] )
 Y[1,0] = sum( W * X[1:4, 0:3] )
 ... and so on

کل ایده در همین خلاصه می‌شود: وزن‌های مشترک، موضعی بودن، و پنجره‌ی لغزان. بقیه‌ی مسائل صرفاً حساب‌وکتاب است.

فرمول ابعاد خروجی

با فرض بعد مکانی ورودی H، بعد کرنل K، پدینگ P و استراید S:

Hout=⌊H−K+2PS⌋+1H_{\text{out}} = \left\lfloor \frac{H - K + 2P}{S} \right\rfloor + 1

این رابطه را به خاطر بسپارید؛ در طراحی هر معماری بارها آن را محاسبه خواهید کرد.

سناریو H K P S H_out
کانولوشن معتبر، بدون پدینگ 32 3 0 1 30
کانولوشن یکسان (حفظ ابعاد) 32 3 1 1 32
کاهش ابعاد با ضریب 2 32 3 1 2 16
پولینگ 2x2 32 2 0 2 16
میدان دید بزرگ 32 7 3 2 16

منظور از «Same padding» انتخاب PP به‌گونه‌ای است که در حالت Hout=HH_{\text{out}} = H، رابطه‌ی S=1S = 1 برقرار بماند. برای KKهای فرد، این مقدار برابر با P=(K−1)/2P = (K - 1) / 2 است. به همین دلیل کرنل‌های 3x3 همه‌گیر شده‌اند؛ آن‌ها کوچک‌ترین کرنل‌های فردی هستند که مرکز متقارن دارند.

پدینگ

بدون پدینگ، هر لایه‌ی کانولوشن نقشه‌ی ویژگی (feature map) را کوچک‌تر می‌کند. اگر 20 لایه را روی هم بچینید، تصویر 224x224 شما به 184x184 آب می‌رود؛ اتفاقی که توان پردازشی لبه‌ها را هدر می‌دهد و اتصال‌های باقیمانده (residual connections) را که به ابعاد یکسان وابسته‌اند، مختل می‌کند.

Zero padding (P = 1) on a 5 x 5 input:

  0  0  0  0  0  0  0
  0  1  2  0  1  2  0
  0  0  1  3  1  0  0
  0  2  1  0  2  1  0       Now the kernel can centre on pixel
  0  1  0  2  1  3  0       (0, 0) and still have three rows and
  0  2  1  1  0  1  0       three columns of values to multiply.
  0  0  0  0  0  0  0

حالت‌های پرکاربرد در عمل: zero (رایج‌ترین حالت)، reflect (بازتاب لبه، برای جلوگیری از مرزهای تند در مدل‌های زایشی)، replicate (کپی کردن مقادیر لبه)، و circular (حلقه‌ای، در مسائلی با ساختار چرخشی یا توریدال).

استراید

استراید همان طول گام حرکت کرنل است. مقدار پیش‌فرض آن stride=1 است. مقدار stride=2 ابعاد مکانی را نصف می‌کند و روش استاندارد کاهش ابعاد در دل یک CNN بدون نیاز به لایه‌ی جداگانه‌ی پولینگ به شمار می‌رود؛ تمام معماری‌های مدرن (مثل ResNet ،ConvNeXt و MobileNet) در بخش‌هایی از مدل، کانولوشن‌های دارای استراید را جایگزین max-pool می‌کنند.

Stride 1 on a 5 x 5 input, 3 x 3 kernel:

  starts: (0,0) (0,1) (0,2)        -> output row 0
          (1,0) (1,1) (1,2)        -> output row 1
          (2,0) (2,1) (2,2)        -> output row 2

  Output: 3 x 3

Stride 2 on the same input:

  starts: (0,0) (0,2)              -> output row 0
          (2,0) (2,2)              -> output row 1

  Output: 2 x 2

کانال‌های ورودی چندگانه

تصاویر واقعی سه کانال دارند. بنابراین یک کانولوشن 3x3 روی ورودی RGB در واقع یک مکعب 3x3x3 است: یک برش 3x3 به‌ازای هر کانال ورودی. در هر موقعیت مکانی، ضرب و جمع را روی هر سه برش انجام می‌دهید و مقدار بایاس (bias) را اضافه می‌کنید.

Input:   (C_in,  H,  W)        3 x 5 x 5
Kernel:  (C_in,  K,  K)        3 x 3 x 3 (one kernel)
Output:  (1,     H', W')       2D map

For a layer that produces C_out output channels, you stack C_out kernels:

Weight:  (C_out, C_in, K, K)   e.g. 64 x 3 x 3 x 3
Output:  (C_out, H', W')       64 x 3 x 3

Parameter count: C_out * C_in * K * K + C_out   (the + C_out is biases)

خط آخر همان فرمولی است که هنگام برنامه‌ریزی یک مدل محاسبه خواهید کرد. یک کانولوشن 3x3 با 64 کانال روی ورودی 3 کاناله، دقیقاً 64 * 3 * 3 * 3 + 64 = 1,792 پارامتر دارد؛ بسیار سبک و کم‌هزینه.

ترفند im2col

خواندن حلقه‌های تودرتو آسان است، اما سرعت بسیار پایینی دارند. پردازنده‌های گرافیکی (GPU) به ضرب ماتریس‌های بزرگ نیاز دارند. ترفند کار این است: هر پنجره از میدان دید ورودی را باز کرده و در قالب یک ستون از یک ماتریس بزرگ بچینید (flatten)، کرنل را هم به‌صورت یک سطر درآورید؛ حالا کل عملیات کانولوشن به یک ضرب ماتریسی (matmul) تکی تبدیل می‌شود.

ورودی(C_in, H, W)im2col(استخراج وصله‌ها)ماتریس ستون‌ها(C_in * K * K, H_out * W_out)وزن(C_out, C_in, K, K)تخت‌سازی(C_out, C_in * K * K)matmulخروجی(C_out, H_out * W_out)تغییر شکل به (C_out, H_out, W_out)

تمام پیاده‌سازی‌های صنعتی کانولوشن، نوعی از همین روش را به‌همراه تکنیک‌های مدیریت حافظه‌ی کَش (مانند direct conv ،Winograd و FFT conv برای کرنل‌های بزرگ) به کار می‌برند. مفهوم im2col را که درک کنید، هسته‌ی اصلی را یاد گرفته‌اید.

میدان دید (Receptive Field)

یک کانولوشن 3x3 منفرد تنها به 9 پیکسل ورودی دید دارد. اگر دو کانولوشن 3x3 را روی هم بگذارید، یک نورون در لایه‌ی دوم پنجره‌ای 5x5 از پیکسل‌های ورودی را پوشش می‌دهد. سه لایه‌ی 3x3 به یک محدوده‌ی 7x7 می‌رسند. در حالت کلی:

RF after L stacked K x K convs (stride 1) = 1 + L * (K - 1)

With strides:   RF grows multiplicatively with stride along each layer.

دلیل اینکه معماری‌های برتر بر رویکرد «همه‌جا 3x3» تکیه می‌کنند (مثل VGG ،ResNet و ConvNeXt) این است که دو لایه‌ی کانولوشن 3x3 دقیقاً همان محدوده‌ی ورودیِ یک لایه‌ی 5x5 را می‌بینند، اما با پارامترهای کمتر و یک تابع غیرخطی اضافی در بین لایه‌ها.

کرنل کانولوشنانتخاب یک کرنل، لغزاندن پنجره
CONVOLUTION KERNELA convolution lays a small kernel over every 3x3 patch of the image and sums the elementwise products. The edge kernel cancels flat regions and lights up the vertical band; blur averages neighbors; sharpen amplifies the center against its surround. The output is two pixels smaller per side because the window cannot center on the border.ورودی 8x8خروجی 6x6
یک کانولوشن یک کرنل کوچک را روی تک‌تک بخش‌های 3x3 تصویر قرار داده و حاصل‌ضرب درایه‌ای آن‌ها را جمع می‌زند. کرنل لبه نواحی یکنواخت را خنثی کرده و نوار عمودی را برجسته می‌کند؛ تاری میانگین همسایه‌ها را می‌گیرد؛ و تیزکردن مرکز را در برابر محیط اطرافش تقویت می‌کند. اندازه خروجی در هر بعد دو پیکسل کوچک‌تر است زیرا پنجره نمی‌تواند روی لبه‌ها متمرکز شود.

پیاده‌سازی

گام 1: پد کردن آرایه

از پایه‌ای‌ترین بخش شروع کنید: تابعی که دور یک آرایه‌ی H x W را با صفر پد می‌کند.

import numpy as np

def pad2d(x, p):
    if p == 0:
        return x
    h, w = x.shape[-2:]
    out = np.zeros(x.shape[:-2] + (h + 2 * p, w + 2 * p), dtype=x.dtype)
    out[..., p:p + h, p:p + w] = x
    return out

x = np.arange(9).reshape(3, 3)
print(x)
print()
print(pad2d(x, 1))

ترفند استخراج محورهای انتهایی با x.shape[:-2] باعث می‌شود تا همین تابع برای (H, W) ،(C, H, W) یا (N, C, H, W) بدون هیچ تغییری کار کند.

گام 2: کانولوشن دوبعدی با حلقه‌های تودرتو

پیاده‌سازی مرجع؛ کند است، ولی جای هیچ ابهامی باقی نمی‌گذارد. این کد دقیقاً همان کاری است که torch.nn.functional.conv2d از نظر منطقی انجام می‌دهد.

def conv2d_naive(x, w, b=None, stride=1, padding=0):
    c_in, h, w_in = x.shape
    c_out, c_in_w, kh, kw = w.shape
    assert c_in == c_in_w

    x_pad = pad2d(x, padding)
    h_out = (h + 2 * padding - kh) // stride + 1
    w_out = (w_in + 2 * padding - kw) // stride + 1

    out = np.zeros((c_out, h_out, w_out), dtype=np.float32)
    for oc in range(c_out):
        for i in range(h_out):
            for j in range(w_out):
                hs = i * stride
                ws = j * stride
                patch = x_pad[:, hs:hs + kh, ws:ws + kw]
                out[oc, i, j] = np.sum(patch * w[oc])
        if b is not None:
            out[oc] += b[oc]
    return out

چهار حلقه‌ی تودرتو (کانال خروجی، سطر، ستون، به‌همراه جمع ضمنی روی C_in ،kh و kw). این پیاده‌سازی، معیار سنجش و اعتبارسنجی تمام نسخه‌های سریع‌تر بعدی خواهد بود.

گام 3: بررسی صحت با یک کرنل دست‌ساز

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

def synthetic_step_image():
    img = np.zeros((1, 16, 16), dtype=np.float32)
    img[:, :, 8:] = 1.0
    return img

sobel_x = np.array([
    [[-1, 0, 1],
     [-2, 0, 2],
     [-1, 0, 1]]
], dtype=np.float32)[None]

x = synthetic_step_image()
y = conv2d_naive(x, sobel_x, padding=1)
print(y[0].round(1))

انتظار می‌رود روی ستون 7 مقادیر مثبت بزرگی ظاهر شود (ناشی از افزایش روشنایی از چپ به راست) و بقیه‌ی نقاط صفر باشند. همین یک خروجی ساده نشان می‌دهد محاسبات ریاضی را درست پیاده کرده‌اید.

گام 4: تکنیک im2col

هر پنجره به‌اندازه‌ی ابعاد کرنل در ورودی را به یک ستون از ماتریس تبدیل کنید. برای C_in=3, K=3، هر ستون شامل 27 عدد خواهد بود.

def im2col(x, kh, kw, stride=1, padding=0):
    c_in, h, w = x.shape
    x_pad = pad2d(x, padding)
    h_out = (h + 2 * padding - kh) // stride + 1
    w_out = (w + 2 * padding - kw) // stride + 1

    cols = np.zeros((c_in * kh * kw, h_out * w_out), dtype=x.dtype)
    col = 0
    for i in range(h_out):
        for j in range(w_out):
            hs = i * stride
            ws = j * stride
            patch = x_pad[:, hs:hs + kh, ws:ws + kw]
            cols[:, col] = patch.reshape(-1)
            col += 1
    return cols, h_out, w_out

این بخش همچنان از یک حلقه‌ی پایتونی استفاده می‌کند، اما بار سنگین محاسبات را به یک ضرب ماتریسی برداری واگذار خواهیم کرد.

گام 5: کانولوشن سریع با ترکیب im2col و matmul

چهار حلقه‌ی تودرتو را با یک ضرب ماتریسی جایگزین کنید.

def conv2d_im2col(x, w, b=None, stride=1, padding=0):
    c_out, c_in, kh, kw = w.shape
    cols, h_out, w_out = im2col(x, kh, kw, stride, padding)
    w_flat = w.reshape(c_out, -1)
    out = w_flat @ cols
    if b is not None:
        out += b[:, None]
    return out.reshape(c_out, h_out, w_out)

بررسی درستی عملکرد: هر دو پیاده‌سازی را اجرا کرده و مقایسه کنید.

rng = np.random.default_rng(0)
x = rng.normal(0, 1, (3, 16, 16)).astype(np.float32)
w = rng.normal(0, 1, (8, 3, 3, 3)).astype(np.float32)
b = rng.normal(0, 1, (8,)).astype(np.float32)

y_naive = conv2d_naive(x, w, b, padding=1)
y_im2col = conv2d_im2col(x, w, b, padding=1)

print(f"max abs diff: {np.max(np.abs(y_naive - y_im2col)):.2e}")

مقدار max abs diff باید در حدود 1e-5 باشد؛ این اختلاف جزئی به‌خاطر تغییر ترتیب جمع در محاسبات ممیز شناور رخ می‌دهد، نه یک باگ.

گام 6: مجموعه‌ای از کرنل‌های دست‌ساز

پنج فیلتر که نشان می‌دهند یک لایه‌ی کانولوشن منفرد، حتی پیش از هرگونه آموزش، توانایی بازنمایی چه الگوهایی را دارد.

KERNELS = {
    "identity": np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.float32),
    "blur_3x3": np.ones((3, 3), dtype=np.float32) / 9.0,
    "sharpen": np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtype=np.float32),
    "sobel_x": np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32),
    "sobel_y": np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32),
}

def apply_kernel(img2d, kernel):
    x = img2d[None].astype(np.float32)
    w = kernel[None, None]
    return conv2d_im2col(x, w, padding=1)[0]

وقتی این فیلترها را روی یک تصویر خاکستری اعمال کنید: فیلتر تاری تصویر را نرم می‌کند، فیلتر شارپ‌کننده لبه‌ها را شفاف و تیز می‌کند، سوبل افقی (Sobel-x) لبه‌های عمودی را مشخص می‌سازد و سوبل عمودی (Sobel-y) لبه‌های افقی را بیرون می‌کشد. این‌ها دقیقاً الگوهایی هستند که اولین لایه‌ی کانولوشنِ آموزش‌دیده در مدل‌هایی مثل AlexNet و VGG یاد گرفتند؛ زیرا یک مدل تصویری مناسب، فارغ از وظیفه‌ی نهایی‌اش، در ابتدا به آشکارسازهای لبه و لکه نیاز دارد.

استفاده در عمل

کلاس nn.Conv2d در PyTorch همین عملیات را همراه با مشتق‌گیری خودکار (autograd)، کرنل‌های CUDA و بهینه‌سازی‌های cuDNN کپسوله کرده است. ساختار و معناشناسی ابعاد در آن دقیقاً یکسان است.

import torch
import torch.nn as nn

conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1)
print(conv)
print(f"weight shape: {tuple(conv.weight.shape)}   # (C_out, C_in, K, K)")
print(f"bias shape:   {tuple(conv.bias.shape)}")
print(f"param count:  {sum(p.numel() for p in conv.parameters())}")

x = torch.randn(8, 3, 224, 224)
y = conv(x)
print(f"\ninput  shape: {tuple(x.shape)}")
print(f"output shape: {tuple(y.shape)}")

اگر padding=1 را به padding=0 تغییر دهید، خروجی به 222x222 افت می‌کند. تغییر stride=1 به stride=2 نیز ابعاد را به 112x112 می‌رساند؛ دقیقاً همان فرمولی که در بالا دیدید.

خروجی این درس

این درس موارد زیر را به همراه دارد:

  • outputs/prompt-cnn-architect.md — پرامپتی که بر اساس ابعاد ورودی، سقف پارامترها و میدان دید هدف، پشته‌ای از لایه‌های Conv2d با مقادیر درست K/S/P در هر مرحله طراحی می‌کند.
  • outputs/skill-conv-shape-calculator.md — مهارتی که ساختار لایه‌های شبکه را تحلیل کرده و ابعاد خروجی، میدان دید و تعداد پارامترهای هر بلوک را برمی‌گرداند.

تمرین‌ها

  1. (ساده) برای یک ورودی تک‌کاناله (خاکستری) با ابعاد 128x128 و پشته‌ای از لایه‌های [Conv3x3(s=1,p=1), Conv3x3(s=2,p=1), Conv3x3(s=1,p=1), Conv3x3(s=2,p=1)]، ابعاد مکانی خروجی و میدان دید را در هر لایه دستی حساب کنید. صحت نتیجه را با یک شیء nn.Sequential از کانولوشن‌های تصادفی در PyTorch بررسی کنید.
  2. (متوسط) توابع conv2d_naive و conv2d_im2col را طوری توسعه دهید که آرگومان groups را نیز بپذیرند. نشان دهید که شرط groups=C_in=C_out ساختار کانولوشن تفکیک‌پذیر عمقی (depthwise convolution) را بازتولید می‌کند و تعداد پارامترهای آن به‌جای C * C * K * K به C * K * K کاهش می‌یابد.
  3. (سخت) مسیر پس‌انتشار (backward pass) الگوریتم conv2d_im2col را دستی پیاده‌سازی کنید: با داشتن گرادیان خروجی، گرادیان‌های x و w را محاسبه کنید. خروجی را با torch.autograd.grad روی همان ورودی‌ها و وزن‌ها مقایسه کنید. نکته‌ی اصلی: گرادیان تابع im2col همان عملگر col2im است و باید مقادیر پنجره‌های هم‌پوشان را جمع کند.

واژگان کلیدی

اصطلاح در گفتگوی عامیانه معنای دقیق فنی
کانولوشن (Convolution) «لغزاندن یک فیلتر» ضرب داخلیِ قابل‌یادگیری که با وزن‌های مشترک در تمام موقعیت‌های مکانی اعمال می‌شود؛ از نظر ریاضی همبستگی متقابل (cross-correlation) است، اما در یادگیری عمیق همه آن را کانولوشن می‌نامند
کرنل / فیلتر (Kernel / filter) «آشکارساز ویژگی» تنسور وزن کوچکی با ابعاد (C_in, K, K) که ضرب داخلی آن با پنجره‌ای از ورودی، یک پیکسل خروجی را می‌سازد
استراید (Stride) «طول پرش» طول گام جابه‌جایی بین موقعیت‌های متوالیِ کرنل؛ استراید 2 هر بعد مکانی را نصف می‌کند
پدینگ (Padding) «صفرهای حاشیه» مقادیر اضافه‌ای که دور ورودی قرار می‌گیرند تا کرنل بتواند روی پیکسل‌های مرزی متمرکز شود؛ حالت same ابعاد خروجی را برابر با ورودی نگه می‌دارد
میدان دید (Receptive field) «وسعت دید نورون» محدوده‌ای از ورودی اصلی که یک خروجی مشخص به آن وابسته است؛ این مقدار با افزایش عمق و استراید گسترش می‌یابد
متد im2col «ترفند GEMM» بازآرایی پنجره‌های ورودی در قالب ستون‌های یک ماتریس تا کانولوشن به یک ضرب ماتریسی بزرگ تبدیل شود؛ اساس کار کرنل‌های محاسباتی سریع
کانولوشن عمقی (Depthwise conv) «یک کرنل به‌ازای هر کانال» نوعی کانولوشن با شرط groups == C_in که هر کانال خروجی را صرفاً از روی کانال متناظر خودش در ورودی محاسبه می‌کند؛ ستون فقرات معماری‌های MobileNet و ConvNeXt
هم‌واریانس با انتقال (Translation equivariance) «جابه‌جایی ورودی، جابه‌جایی خروجی» این ویژگی که جابه‌جا شدن ورودی به‌اندازه‌ی k پیکسل، خروجی را هم دقیقاً به‌اندازه‌ی k پیکسل جابه‌جا می‌کند؛ این ویژگی به‌طور مستقیم ناشی از اشتراک پارامترهاست

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

خودآزمایی

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

  1. پیش از درسیک تصویر RGB با ابعاد 224x224 را به یک لایه کانولوشن با kernel_size=3، stride=1 و padding=0 می‌دهید. ابعاد فضایی خروجی چقدر است؟
  2. پیش از درسیک لایه کانولوشن دارای in_channels=3، out_channels=64، kernel_size=3 و بایاس است. چند پارامتر قابل یادگیری دارد؟
  3. پس از درسچرا شبکه‌های CNN مدرن، پشته‌ای از کانولوشن‌های 3x3 را به یک کانولوشن تکی 5x5 یا 7x7 ترجیح می‌دهند؟
  4. پس از درستبدیل im2col در عمل چه کاری انجام می‌دهد؟
  5. پس از درسچهار لایه کانولوشن 3x3 متوالی را روی هم قرار می‌دهید (همگی با گام 1 و بدون pooling). میدان دیدِ یک نورون در لایه‌ی نهایی چقدر است؟

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

چرا لایه‌های متراکم (Dense) برای پردازش تصاویر نامناسب هستند؟

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

فرمول محاسبه ابعاد خروجی کانولوشن چیست؟

بعد خروجی از رابطه‌ی (H - K + 2P) / S + 1 محاسبه می‌شود که در آن H بعد ورودی، K بعد کرنل، P میزان پدینگ و S استراید است.

ترفند im2col در کانولوشن چه کمکی می‌کند؟

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

چرا استفاده از دو لایه کانولوشن 3x3 بهتر از یک لایه 5x5 است؟

دو لایه‌ی 3x3 همان میدان دید 5x5 را پوشش می‌دهند، اما پارامترهای کمتری مصرف می‌کنند (18 وزن در برابر 25 وزن) و امکان قرار دادن یک تابع غیرخطیِ اضافی بین دو لایه را فراهم می‌سازند.

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

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