Part 03 · Deep Learning

Convolutional Neural Networks

ConvolutionFiltersPoolingFeature maps

ছবির pixel দের পাশে প্রতিবেশী থাকে। ছোট একটা filter ছবির উপর দিয়ে সরে সরে প্রথমে edge, তারপর আকার, তারপর জিনিস খুঁজে পায়, সাধারণ network এর চেয়ে অনেক কম weight দিয়ে।

⏱ 1-2 ঘণ্টা · Station 19 / 92

জিনিসটা কী, কেন দরকার

একটা সাধারণ neural network কে 224 × 224 এর একটা রঙিন ছবি দাও, সে দেখবে 150,528 টা সম্পর্কহীন সংখ্যা। সে জানে না যে pixel 1,000 এর পাশেই pixel 1,001, আর বিড়ালটা দশ pixel বামে সরে গেলে প্রতিটা input বদলে যায়, network কে বিড়াল আবার নতুন করে শিখতে হয়। 64 neuron এর প্রথম layer এই 96 লাখ weight লাগত।

Convolutional neural network (CNN) ছবির দুইটা সত্য নিজের design এ ঢুকিয়ে নেয়। পাশের pixel গুরুত্বপূর্ণ: একটা edge মানে কয়েকটা pixel পাশাপাশি। একই জিনিস যেকোনো জায়গায় থাকতে পারে: উপরের কোণের edge নিচের কোণেও edge ই। তাই প্রতি pixel এ একটা weight না রেখে CNN ছবির উপর দিয়ে একটা ছোট filter টেনে নেয়, আর একই অল্প কয়েকটা weight সবখানে বারবার ব্যবহার করে। Photo search, medical scan, document scan, আর অনেক multimodal model এর ছবির অংশ চলে CNN দিয়ে।

ভেতরে কীভাবে চলে

Convolution। একটা filter (kernel) হলো weight এর ছোট একটা grid, ধরো 3 × 3। ছবির উপর-বামের 3 × 3 টুকরোর উপর বসাও, প্রতিটা pixel কে তার উপরের weight দিয়ে গুণ করো, সব যোগ করো: একটা output সংখ্যা। এক pixel ডানে সরো, আবার করো। Output এর grid টা হলো feature map: যেখানে টুকরোটা filter এর pattern এর মতো সেখানে বড়, অন্য জায়গায় ছোট।

patch          filter        sum of products
0 0 9          -1 0 1
0 0 9    ×     -1 0 1   =    9 + 9 + 9 = 27   (an edge here)
0 0 9          -1 0 1

শেখা filter। কেউ হাতে filter লেখে না। এগুলো weight, অন্য সব weight এর মতো backprop দিয়ে train হয়। প্রথম দিকের layer edge আর রঙের ছোপ শেখে, মাঝের layer সেগুলো জুড়ে texture আর অংশ বানায় (চোখ, চাকা), শেষের দিকের layer পুরো object এ সাড়া দেয়। প্রতিটা layer এ অনেক filter, তাই অনেক feature map (channel)।

দুইটা knob: stride (filter প্রতিবার কতটা সরে; 2 দিলে output এর সাইজ অর্ধেক) আর padding (চারপাশে শূন্যের একটা পাড় দেওয়া যাতে output input এর সাইজই থাকে)।

Pooling। Max pooling feature map এর প্রতিটা 2 × 2 অংশ থেকে সবচেয়ে বড় মানটা রাখে। Map প্রতিটা দিকে অর্ধেক হয়, আর ছবির ছোটখাটো সরে যাওয়া আর কোনো ব্যাপার থাকে না: "এখানে আশেপাশে কোথাও একটা edge আছে" এটুকুই যথেষ্ট।

সাধারণ গঠন: [conv → ReLU → pool] কয়েকবার, যাতে map ছোট আর গভীর হয়, তারপর flatten আর class গুলোর জন্য softmax সহ এক দুইটা dense layer।

এত কম weight কেন। 3-channel ছবির উপর একটা 3 × 3 filter এ 27 টা weight, সাথে 1 টা bias, ছবি যত বড়ই হোক। 64 টা filter মানে 1,728 টা weight, dense layer এর 96 লাখের জায়গায়।

আগে অনুমান করো

একটা 32×32 grayscale ছবি padding ছাড়া আর stride 1 এ একটা 3×3 convolution পার হলো, তারপর 2×2 max pooling। Result এর সাইজ কত?

ছবিতে দেখো

image 6×6: 0 left, 9 rightfeature map 4×4027270027270027270027270filter 3×3-1 0 1 on each rowflat area: (-1·0 + 1·0) × 3 rows = 0straddling the edge: 1·9 × 3 rows = 27past the edge, flat again: 0same 9 weights everywhere: the edge lights up wherever it is
একটা 3×3 edge filter 6×6 এর একটা ছবির উপরের সারি ধরে সরে, যেটা বামে অন্ধকার আর ডানে উজ্জ্বল। বামের সমান জায়গায় output 0; edge এর উপর দুই পা রাখলে দুইবার 27; edge পেরিয়ে ডানের সমান জায়গায় আবার 0। পুরো 4×4 feature map প্রতিটা সারিতে edge টা জ্বালিয়ে দেয়।

Code এ convolution

NumPy তে দুইটা loop দিয়ে লেখা একটা convolution, PyTorch এর conv2d এর সাথে মেলানো, তারপর max pooling, একটা সরানো ছবি, আর dense layer এর তুলনায় conv layer এর parameter গোনা।

import numpy as np
import torch

# A 6x6 grayscale image: dark on the left, bright on the right. There's a vertical edge in the middle.
img = np.array([[0, 0, 0, 9, 9, 9]] * 6, dtype=float)

# A 3x3 filter that responds to "dark on the left, bright on the right".
edge = np.array([[-1, 0, 1],
                 [-1, 0, 1],
                 [-1, 0, 1]], dtype=float)

def conv2d(x, k):
    h, w = x.shape[0] - k.shape[0] + 1, x.shape[1] - k.shape[1] + 1
    out = np.zeros((h, w))
    for i in range(h):
        for j in range(w):
            out[i, j] = np.sum(x[i:i + 3, j:j + 3] * k)   # the same 9 weights at every position
    return out

fmap = conv2d(img, edge)
print("feature map (4x4):")
print(fmap.astype(int))

def maxpool2(x):
    return x.reshape(x.shape[0] // 2, 2, x.shape[1] // 2, 2).max(axis=(1, 3))
print("after 2x2 max pooling:")
print(maxpool2(fmap).astype(int))

# PyTorch's Conv2d computes the same thing (it's cross-correlation, no kernel flip).
t = torch.nn.functional.conv2d(torch.tensor(img)[None, None], torch.tensor(edge)[None, None])
print("same as torch.nn.functional.conv2d:", np.array_equal(t[0, 0].numpy(), fmap))

# The same image shifted right by one pixel: the edge still lights up, one column later.
shifted = np.roll(img, 1, axis=1)
shifted[:, 0] = 0
print("shifted image, row 0 of feature map:", conv2d(shifted, edge)[0].astype(int))

# Weights: a dense layer vs a conv layer on a 224x224 RGB photo, 64 outputs.
dense = 224 * 224 * 3 * 64
conv = 3 * 3 * 3 * 64
print(f"\nparameters, dense layer: {dense:,}")
print(f"parameters, 3x3 conv layer: {conv:,}  ({dense // conv:,}x fewer)")

Output:

feature map (4x4):
[[ 0 27 27  0]
 [ 0 27 27  0]
 [ 0 27 27  0]
 [ 0 27 27  0]]
after 2x2 max pooling:
[[27 27]
 [27 27]]
same as torch.nn.functional.conv2d: True
shifted image, row 0 of feature map: [ 0  0 27 27]

parameters, dense layer: 9,633,792
parameters, 3x3 conv layer: 1,728  (5,575x fewer)

কী দেখলাম:

  • Feature map সমান জায়গায় 0, আর যে দুইটা column এ filter edge এর উপর দুই পা রেখেছে সেখানে 27। একটা filter "উজ্জ্বলের পাশে অন্ধকার" কে একটা পরিষ্কার signal বানিয়ে দিয়েছে।
  • Pooling 27 গুলো রাখে আর সাইজ অর্ধেক করে 2×2: "এই এলাকায় একটা খাড়া edge আছে" টিকে যায়, ঠিক কোন column সেটা না।
  • PyTorch হুবহু মেলে। এর conv2d এই loop ই, একসাথে অনেক ছবি আর filter এ দ্রুত optimised code এ করা।
  • ছবি এক pixel ডানে সরালে 27 গুলোও এক column ডানে সরে যায়। একই weight edge কে যেখানেই থাকুক খুঁজে পায়; নতুন করে কিছু শিখতে হয়নি। (একে বলে translation equivariance; তারপর pooling একটু shift invariance যোগ করে।)
  • 9,633,792 এর বদলে 1,728 টা weight, 5,575 গুণ কম, তাও এমন একটা layer এ যেটা ছবির জন্য বেশি মানানসই। কম weight মানে overfit না করে train করতে data ও কম লাগে।

Production এ যা মাথায় রাখবে

  • শূন্য থেকে train কোরো না। লাখ লাখ ছবিতে আগে থেকে train করা একটা CNN (ResNet, EfficientNet, ConvNeXt) দিয়ে শুরু করো আর তোমার data তে fine-tune করো। এভাবে প্রতি class এ কয়েকশো label দেওয়া ছবিই যথেষ্ট হতে পারে (transfer learning)।
  • Augment করো। Random crop, flip, ছোট rotation আর রঙের বদল model কে তুমি যা জোগাড় করেছ তার চেয়ে বেশি বৈচিত্র্য দেখায়। এই part এর শেষের project এ এগুলো আছে।
  • Serving এর সময় একই preprocessing। Training এর মতো হুবহু resize, crop আর normalise করো; অমিল হলে (RGB বনাম BGR, আলাদা mean/std) চুপচাপ accuracy নষ্ট হয়।
  • Compute এর বেশিরভাগ প্রথম দিকের layer এ। বড় feature map × অনেক filter খরচের বড় অংশ। আগেভাগে ছোট করো (stride 2, pooling), আর phone এর জন্য depthwise-separable convolution (MobileNet) নাও।
  • CNN বনাম vision transformer। বিশাল dataset এ এখন transformer (Part 4) এগিয়ে, কিন্তু অনেক production কাজে, বিশেষ করে edge device এ, CNN এখনো দ্রুত আর কম data তে ভালো।

এক কথায়

CNN ছবির উপর দিয়ে ছোট শেখা filter টেনে নেয়, সবখানে একই weight বারবার ব্যবহার করে, তাই লাখের বদলে হাজার weight দিয়েই pattern যেখানেই থাকুক খুঁজে পায়; pooling map ছোট করে আর ছোটখাটো সরে যাওয়া অপ্রাসঙ্গিক করে দেয়।

নিজে করে দেখো

এমন একটা 3×3 filter লেখো যেটা আড়াআড়ি edge ধরে (উপরে অন্ধকার, নিচে উজ্জ্বল)। তারপর 64×64 এর RGB ছবিতে 5×5 সাইজের 32 টা filter এর একটা layer, stride 1 আর padding 2 দিয়ে, তার output এর সাইজ আর weight এর সংখ্যা বের করো।

Answer

খাড়া filter টাকে কাত করে দাও: row গুলো [-1 -1 -1], [0 0 0], [1 1 1]। Padding 2 দিলে 5×5 filter সাইজ ধরে রাখে: 64 + 2·2 − 5 + 1 = 64, তাই output হলো 64×64 এর 32 টা feature map। Weight: 5 × 5 × 3 × 32 = 2,400, সাথে 32 টা bias = 2,432।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →