সরাসরি পর্বে যান
পর্ব ২২ · ৯১-এর মধ্যে · মডিউল ৪

ResNet আর আধুনিক CNN — skip connection, EfficientNet আর ConvNeXt

ResNet and Modern CNNs — Residual Blocks, MobileNet, EfficientNet, ConvNeXt and Choosing an Architecture

Taught in Bangla, this lesson explains residual learning and counts basic and bottleneck block parameters, the projection shortcut, and full ResNet-18 and ResNet-50 totals that match torchvision exactly. A real desktop run trains 56-layer plain and residual CNNs on 10,000 CIFAR-10 images. It then covers MobileNet's depthwise separable convolutions, EfficientNet compound scaling and ConvNeXt, and measures parameters, FLOPs and CPU latency for four models to show that FLOPs are not time.

এই পর্বে যা শিখবেন

  • CNN-এর residual block: basic আর bottleneck, প্রতিটির shape, parameter আর গুণ-যোগের হিসাব
  • কখন আর কেন projection shortcut লাগে, আর ResNet-18/50-এ BatchNorm কোথায় বসে
  • একই গভীরতার plain CNN আর ResNet-এর সত্যিকারের training তুলনা
  • MobileNet-এর depthwise separable conv, EfficientNet-এর compound scaling আর ConvNeXt-এর নকশা
  • Parameter, FLOP আর latency মেপে ফোন বা সার্ভারের জন্য architecture বাছাই

পর্ব ২০ (Convolution — filter, stride, padding আর receptive field)-এ একটি convolution স্তর আর পর্ব ২১ (CNN-এর স্থাপত্য — LeNet থেকে AlexNet, VGG আর Inception)-এ স্তর সাজিয়ে network বানানোর প্রথম যুগ দেখেছেন। পর্ব ১৭-এ মেপেছেন, x + F(x)-এর derivative I + F′ বলে gradient-এর একটি সরাসরি পথ থাকে। এই পর্ব সেই যুক্তি আর পুনরাবৃত্তি করবে না; এখানে প্রশ্ন হলো ছবির network-এ সেই block দেখতে কেমন, shape কীভাবে মেলে, আর ২০১৫-এর পরে CNN কোন পথে হালকা আর দ্রুত হয়েছে। পাতার চারটি ব্লক ব্রাউজারের Python আর NumPy-তে চলে; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে। PyTorch আর torchvision-এর সব সংখ্যা ডেস্কটপের সত্যিকারের run থেকে, CPU-তে।

১ · ResNet-এর মূল চাল — block শুধু “বাকিটুকু” শেখে

পুরান ঢাকার একটি দর্জির দোকানে একটি পাঞ্জাবি দশ জন কারিগরের হাত ঘুরে তৈরি হয়। পুরোনো নিয়মে প্রত্যেকে আগের জনের কাজ খুলে নতুন করে সেলাই করেন; একজন ভুল করলে পরের সবাই সেই ভুলের ওপর কাজ করেন। নতুন নিয়মে প্রত্যেকে হাতে পাওয়া পাঞ্জাবিটি রেখে দেন, আর শুধু নিজের অংশ যোগ করেন: একজন বোতাম, একজন কলারের নকশা। কারও কিছু করার না থাকলে পাঞ্জাবি অবিকৃত পরের জনের কাছে যায়। Residual block ঠিক এই দ্বিতীয় নিয়ম।

২০১৫ সালে Kaiming He, Xiangyu Zhang, Shaoqing Ren আর Jian Sun-এর প্রবন্ধে (CVPR ২০১৬-তে প্রকাশিত) প্রস্তাবিত ResNet-এ প্রতিটি block-এর output y = ReLU(F(x) + x). এখানে x হলো block-এর input feature map, অর্থাৎ (channel, উচ্চতা, প্রস্থ) আকারের একটি tensor, আর F হলো দুটি বা তিনটি convolution স্তর, প্রতিটির পরে BatchNorm. যোগের পথটি skip বা residual connection. গভীর plain network-এ train loss-ই যে কমে না (degradation), আর shortcut কেন gradient বাঁচায়, সেই যুক্তি আর মাপ পর্ব ১৭-এ আছে। CNN-এ এর সঙ্গে একটি নতুন শর্ত যোগ হয়: যোগ করতে হলে F(x) আর x-এর shape হুবহু এক হতে হবে, channel-সংখ্যা, উচ্চতা আর প্রস্থ সবই। পরের দুটি অংশ এই শর্ত ঘিরে।

Basic block বনাম bottleneck block

Basic: two 3×3 convs. Bottleneck: 1×1 reduce → 3×3 → 1×1 expand. Shapes are (channels, H, W).

Basic and bottleneck residual blocks with tensor shapes Basic Bottleneck 3×3, 64 3×3, 64 + (64, 56, 56) ReLU → (64, 56, 56) x 1×1, 64 (কমানো) 3×3, 64 1×1, 256 (বাড়ানো) + (256, 56, 56) ReLU → (256, 56, 56) x weights 73,728 weights 69,632
ছবি ২২.১ — প্রতিটি conv-এর পরে BatchNorm আর (শেষটি ছাড়া) ReLU থাকে, ছবিতে বাদ রাখা হয়েছে। Bottleneck ৪ গুণ বেশি channel বহন করে, অথচ weight প্রায় সমান।

২ · Basic আর bottleneck block — shape আর parameter গোনা

একটি conv স্তরের weight-সংখ্যা k × k × Cin × Cout, যেখানে k হলো kernel-এর পাশ, Cin input channel আর Cout output channel (পর্ব ২০-এর সূত্র)। BatchNorm-এর পরে bias অর্থহীন, কারণ BN নিজেই গড় সরিয়ে β যোগ করে; তাই ResNet-এর conv-এ bias=False, আর প্রতিটি BN-এ প্রতি channel-এ দুটি শেখা সংখ্যা, γ আর β (পর্ব ১৪)। প্রতিটি output pixel-এ একটি conv k × k × Cin × Cout-টি গুণ-যোগ করে; এই এককের নাম MAC (multiply-accumulate), আর এক MAC = দুই FLOP.

Bottleneck-এর চালটি বোঝার জন্য ResNet-50-এর প্রথম stage-এর একটি block ধরুন, input (256, 56, 56). নিচের stepper-এ “পরে” চাপুন।

  1. Input x256×56×56shortcut-এর জন্য জমা
  2. 1×1 কমানো64×56×56channel ৪ ভাগের ১
  3. 3×364×56×56আসল স্থানিক কাজ
  4. 1×1 বাড়ানো256×56×56আবার ৪ গুণ
  5. যোগ + ReLU256×56×56F(x) + x
x(256, 56, 56)
256 × 56 × 56 = 802,816
56 × 56 = 3,136 pixel
x জমা থাকে, শেষে যোগ হবে
conv1: 1×1, 256 → 64+ BN + ReLU
weights = 1·1·256·64 = 16,384
MACs = 16,384 × 3,136
     = 51,380,224
প্রতি pixel-এ 256 → 64 মিশ্রণ
conv2: 3×3, 64 → 64, padding 1+ BN + ReLU
weights = 3·3·64·64 = 36,864
MACs = 36,864 × 3,136
     = 115,605,504
shape বদলায় না: (64, 56, 56)
conv3: 1×1, 64 → 256+ BN (ReLU নয়)
weights = 1·1·64·256 = 16,384
MACs = 16,384 × 3,136
     = 51,380,224
F(x) = (256, 56, 56) = x-এর shape
y = ReLU(F(x) + x)মোট হিসাব
conv weights 69,632
+ BN 768 = 70,400
MACs 218,365,952 ≈ 0.22 G
basic, 256 ch: 1,179,648 weights,
3.70 G MACs
  1. Input. ৫৬ × ৫৬ গ্রিডের প্রতিটি ঘরে ২৫৬টি সংখ্যা। Block-এর শুরুতেই x-এর একটি রেফারেন্স রাখা হয়, শেষে যোগের জন্য।
  2. 1×1 দিয়ে কমানো. 1×1 conv প্রতিটি pixel-এ আলাদাভাবে ২৫৬টি channel-কে ৬৪টি মিশ্রণে বদলায়, পাশের pixel দেখে না। Weight ১৬,৩৮৪ টি।
  3. 3×3, সরু পথে. আসল স্থানিক কাজ, অর্থাৎ আশপাশের pixel মিলিয়ে নকশা খোঁজা, মাত্র ৬৪ channel-এ হয়। ২৫৬ channel-এ হলে এই এক স্তরেই ৫,৮৯,৮২৪ weight লাগত, এখানে ৩৬,৮৬৪.
  4. 1×1 দিয়ে বাড়ানো. ৬৪ থেকে আবার ২৫৬ channel; এখন F(x)-এর shape x-এর সমান। এই স্তরের পরে ReLU নেই, যোগের পরে আসবে।
  5. যোগ আর ReLU. মোট ৬৯,৬৩২ conv weight আর প্রায় ০.২২ G MAC. একই ২৫৬ channel-এ basic block-এর খরচ প্রায় ১৭ গুণ। দাম: 1×1 স্তরে তথ্য চেপে ছোট করা হয়, তাই খুব সরু bottleneck কিছু তথ্য হারাতে পারে।

নিচের ব্লক এই হিসাব function-এ লেখে, তারপর পুরো ResNet-18 আর ResNet-50-এর parameter হাতে গোনে। ResNet-18-এর stage-গুলোতে [2, 2, 2, 2]টি basic block, ResNet-50-এ [3, 4, 6, 3]টি bottleneck; শুরুতে 7×7, stride 2-এর একটি conv (stem), শেষে global average pooling আর ১০০০ class-এর fully connected স্তর।

def conv(cin, cout, k):              # bias নেই, কারণ পরে BatchNorm
    return k * k * cin * cout

def bn(c):                           # BatchNorm-এর γ আর β, প্রতি channel-এ ২টি
    return 2 * c

def basic(cin, cout):                # 3×3 → 3×3, channel না বদলালে identity shortcut
    p = conv(cin, cout, 3) + bn(cout) + conv(cout, cout, 3) + bn(cout)
    if cin != cout:
        p += conv(cin, cout, 1) + bn(cout)      # projection shortcut
    return p

def bottleneck(cin, mid):            # 1×1 কমানো → 3×3 → 1×1 বাড়ানো (৪ গুণ)
    cout = 4 * mid
    p = conv(cin, mid, 1) + bn(mid) + conv(mid, mid, 3) + bn(mid)
    p += conv(mid, cout, 1) + bn(cout)
    if cin != cout:
        p += conv(cin, cout, 1) + bn(cout)
    return p

print(f"basic      64 -> 64       : {basic(64, 64):>9,}")
print(f"basic      256 -> 256     : {basic(256, 256):>9,}")
print(f"bottleneck 256 -> 64 -> 256: {bottleneck(256, 64):>8,}")

def resnet(kind, counts):
    total, cin = conv(3, 64, 7) + bn(64), 64          # stem: 7×7, stride 2
    for width, n in zip([64, 128, 256, 512], counts):
        for _ in range(n):
            if kind == "basic":
                total += basic(cin, width); cin = width
            else:
                total += bottleneck(cin, width); cin = 4 * width
    return total + cin * 1000 + 1000                  # শেষে 1000 class-এর fc
print(f"ResNet-18: {resnet('basic', [2, 2, 2, 2]):,}")
print(f"ResNet-50: {resnet('bottleneck', [3, 4, 6, 3]):,}")

আউটপুট (পাতার Python-এ চালানো):

basic      64 -> 64       :    73,984
basic      256 -> 256     : 1,180,672
bottleneck 256 -> 64 -> 256:   70,400
ResNet-18: 11,689,512
ResNet-50: 25,557,032

প্রথম তিন সারি SVG-এর হিসাব, BN-সহ: basic ৬৪ channel-এ ৭৩,৯৮৪, bottleneck ২৫৬ channel-এ ৭০,৪০০. ২৫৬ channel-এর basic block ১১ লাখের বেশি। শেষ দুই সারি ডেস্কটপে torchvision.models.resnet18(weights=None) আর resnet50-এর sum(p.numel() for p in m.parameters())-এর সঙ্গে হুবহু মেলে (অংশ ৯-এর run দেখুন)। অর্থাৎ ResNet-এ কোনো লুকোনো জিনিস নেই: conv, BN, projection আর শেষের fc. লক্ষ করুন, ResNet-50 প্রায় তিনগুণ গভীর (৫০ বনাম ১৮ ওজনওয়ালা স্তর) হলেও parameter মাত্র দ্বিগুণের একটু বেশি; bottleneck-এর কারণেই।

৩ · Channel বা আকার বদলালে — projection shortcut

প্রতিটি stage-এর শুরুতে ResNet ছবির আকার অর্ধেক করে (stride 2) আর channel দ্বিগুণ করে; এটিই downsampling-এর পরিচিত নিয়ম, যাতে প্রতিটি স্তরের হিসাবের খরচ মোটামুটি সমান থাকে। কিন্তু তখন F(x)-এর shape আর x-এর shape আলাদা। ResNet-18-এর প্রথম আর দ্বিতীয় stage-এর সীমানায় কী ঘটে, ধাপে ধাপে দেখুন।

x আসে stage ১ থেকে; F = 3×3 (stride 2) → 3×3
ধাপ
shape
weight
ফল
  1. x. Stage ১ শেষে ৬৪ channel, ৫৬ × ৫৬. পরের stage ১২৮ channel আর ২৮ × ২৮ চায়।
  2. প্রথম conv. 3×3, stride 2: প্রতি দুই pixel-এ একবার বসে, তাই ৫৬ থেকে ২৮; output channel ১২৮.
  3. দ্বিতীয় conv. 3×3, stride 1, padding 1: shape একই থাকে। F(x) এখন (১২৮, ২৮, ২৮)।
  4. সরাসরি যোগ. দুটি tensor-এর channel আর আকার দুটিই আলাদা; broadcasting-এর নিয়মেও মেলে না, তাই NumPy বা PyTorch error দেয়।
  5. Projection. Shortcut-এ একটি 1×1 conv, stride 2, ৬৪ থেকে ১২৮ channel, তারপর BN. Weight মাত্র ৮,১৯২, মূল পথের ২,২১,১৮৪-এর তুলনায় সামান্য। ResNet প্রবন্ধে এটিকে বলা হয় বিকল্প B: শুধু shape বদলালে projection, বাকি সব জায়গায় identity.
  6. বিকল্প A. প্রতি দুই pixel-এর একটি নিয়ে আকার অর্ধেক, আর নতুন ৬৪ channel শূন্য দিয়ে ভরাট: parameter শূন্য। প্রবন্ধে CIFAR-10-এর পরীক্ষায় এটি ব্যবহার হয়েছিল; ImageNet-এ B সামান্য ভালো ফল দেয় বলে জানানো হয়।

নিচের ব্লক NumPy-তে একটি ছোট convolution লেখে (sliding_window_view দিয়ে প্রতিটি k × k জানালা বের করে einsum-এ গুণ; পর্ব ২০-এর im2col ধারণার সংক্ষিপ্ত রূপ), তারপর ঠিক এই shape-গুলো দেখায়। মাপ ছোট রাখতে ৫৬ × ৫৬-এর বদলে ৮ × ৮.

import numpy as np
from numpy.lib.stride_tricks import sliding_window_view

rng = np.random.default_rng(42)
def conv(x, W, stride):                  # x: (C, H, W), W: (out, in, k, k), padding k//2
    k = W.shape[-1]; p = k // 2
    xp = np.pad(x, ((0, 0), (p, p), (p, p)))
    win = sliding_window_view(xp, (k, k), axis=(1, 2))[:, ::stride, ::stride]
    return np.einsum("chwij,ocij->ohw", win, W)
relu = lambda z: np.maximum(z, 0)

x = rng.normal(size=(64, 8, 8))          # stage-এর শেষ: 64 channel, 8×8
W1 = rng.normal(0, np.sqrt(2 / 576), size=(128, 64, 3, 3))
W2 = rng.normal(0, np.sqrt(2 / 1152), size=(128, 128, 3, 3))
F = conv(relu(conv(x, W1, stride=2)), W2, stride=1)   # মূল পথ
print("x:", x.shape, " F(x):", F.shape)
try:
    y = x + F
except ValueError as e:
    print("x + F(x) ->", e)

Ws = rng.normal(0, np.sqrt(1 / 64), size=(128, 64, 1, 1))
proj = conv(x, Ws, stride=2)                            # 1×1 conv, stride 2
same = np.einsum("oc,chw->ohw", Ws[:, :, 0, 0], x[:, ::2, ::2])
print("projection:", proj.shape, "| 1x1 conv == matmul per pixel:", np.allclose(proj, same))
y = relu(F + proj)
print("y = relu(F(x) + W_s x):", y.shape)
pad = np.concatenate([x[:, ::2, ::2], np.zeros((64, 4, 4))])   # বিকল্প A: শূন্য channel
print("option A (zero-pad):", pad.shape, "extra params: 0")
print("params  main 3x3+3x3:", W1.size + W2.size, "| projection 1x1:", Ws.size)

আউটপুট (পাতার Python-এ চালানো):

x: (64, 8, 8)  F(x): (128, 4, 4)
x + F(x) -> operands could not be broadcast together with shapes (64,8,8) (128,4,4) 
projection: (128, 4, 4) | 1x1 conv == matmul per pixel: True
y = relu(F(x) + W_s x): (128, 4, 4)
option A (zero-pad): (128, 4, 4) extra params: 0
params  main 3x3+3x3: 221184 | projection 1x1: 8192

দ্বিতীয় লাইনটি NumPy-র নিজের error বার্তা: দুটি shape broadcast করা যায় না। PyTorch-এ একই ভুলে বার্তা আসে “The size of tensor a must match the size of tensor b”. তৃতীয় লাইন একটি গুরুত্বপূর্ণ সত্য দেখায়: 1×1 conv আসলে প্রতিটি pixel-এ একই (১২৮ × ৬৪) matrix দিয়ে গুণ, stride 2 মানে শুধু প্রতি দুই pixel-এর একটি নেওয়া। এই NumPy conv ডেস্কটপে torch.nn.functional.conv2d-এর সঙ্গে মিলিয়ে দেখা হয়েছে (সর্বোচ্চ পার্থক্য প্রায় 10−13). শেষ লাইন: shortcut-এর দাম মূল পথের প্রায় ৩.৭%.

৪ · ResNet-18 আর ResNet-50-এর কাঠামো, আর BatchNorm-এর জায়গা

অংশoutput আকার (224 input)ResNet-18ResNet-50
stem112 × 112, তারপর max pool-এ 56 × 567×7 conv, 64, stride 2 → BN → ReLU → 3×3 max pool, stride 2
layer156 × 562 × basic, 643 × bottleneck, 64 → 256
layer228 × 282 × basic, 1284 × bottleneck, 128 → 512
layer314 × 142 × basic, 2566 × bottleneck, 256 → 1024
layer47 × 72 × basic, 5123 × bottleneck, 512 → 2048
head1 × 1global avg pool → fc 512 → 1000global avg pool → fc 2048 → 1000

নামের সংখ্যা ওজনওয়ালা স্তর গোনে: ResNet-18-এ ১ (stem) + ৮ × ২ + ১ (fc) = ১৮, ResNet-50-এ ১ + ১৬ × ৩ + ১ = ৫০; projection-এর 1×1 গোনা হয় না। Global average pooling প্রতিটি channel-এর ৭ × ৭ মানের গড় নিয়ে একটি সংখ্যা বানায়, তাই VGG-এর মতো বিশাল fully connected স্তর লাগে না (পর্ব ২১)। ডেস্কটপে torchvision-এর ResNet-50-এর layer2-এর প্রথম block ছাপলে দেখা যায় (সত্যিকারের run, torch 2.15.0, torchvision 0.29.2, BN-এর লম্বা লাইন ছেঁটে):

~/bazarghar-dl$ uv run python peek.py
Bottleneck(
  (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
  (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, …)
  (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
  (bn2): BatchNorm2d(128, …)
  (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
  (bn3): BatchNorm2d(512, …)
  (relu): ReLU(inplace=True)
  (downsample): Sequential(
    (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
    (1): BatchNorm2d(512, …)
  )
)

তিনটি জিনিস লক্ষ করুন। এক, stride 2 বসেছে 3×3 conv-এ (conv2), প্রথম 1×1-এ নয়। মূল প্রবন্ধে stride প্রথম 1×1-এ ছিল; 1×1, stride 2 তিন-চতুর্থাংশ pixel একেবারে না দেখেই ফেলে দেয়। torchvision-এর docs এই রূপকে “ResNet V1.5” বলে আর জানায় এতে accuracy বাড়ে; গতি সামান্য কমে বলে কিছু বাস্তবায়নের বর্ণনায় (যেমন NVIDIA-র) পাওয়া যায়। দুই, downsample নামের Sequential-টিই projection shortcut: 1×1, stride 2, তারপর BN. তিন, BN-এর ক্রম: conv → BN → ReLU, আর block-এর শেষ BN-এর পরে প্রথমে যোগ, তারপর ReLU. একে বলে post-activation ResNet.

BatchNorm কোথায়, আর দুটি বাস্তব কৌশল

Pre-activation: He ও সহকর্মীদের ২০১৬ সালের দ্বিতীয় প্রবন্ধ ক্রম উল্টে দেয়: BN → ReLU → conv, আর যোগের পরে কোনো ReLU নেই, যাতে shortcut একেবারে পরিষ্কার identity থাকে; খুব গভীর (১০০০ স্তরের) network-এ এটি সাহায্য করে বলে জানানো হয়। শেষ BN-এর γ = 0: torchvision-এর resnet50(zero_init_residual=True) প্রতিটি block-এর শেষ BN-এর γ শূন্যে শুরু করে, ফলে শুরুতে F(x) = 0 আর প্রতিটি block identity; বড় batch-এ training স্থির করার একটি পরিচিত কৌশল। Eval mode: BN থাকায় inference-এর আগে model.eval() বাধ্যতামূলক (পর্ব ১০); না দিলে একটি ছবির batch-এর নিজের গড় দিয়ে normalize হয়ে ফল উল্টাপাল্টা হয়।

৫ · সত্যিকারের run — একই গভীরতার plain CNN বনাম ResNet, CIFAR-10-এ

বাজারঘরের নিজের পণ্যের ছবির ডেটা নেই, তাই এখানে একটি পরিচিত public ডেটাসেট: CIFAR-10 (৩২ × ৩২ রঙিন ছবি, ১০টি class, যেমন বিমান, গাড়ি, পাখি, বিড়াল)। সময় বাঁচাতে seed দিয়ে বাছাই করা ছোট অংশ: train-এ প্রতি class-এ ১,০০০টি (মোট ১০,০০০), test-এ প্রতি class-এ ২০০টি (মোট ২,০০০)। ডেটা নেওয়া হয়েছে Hugging Face Hub-এ University of Toronto-র কম্পিউটার বিজ্ঞান বিভাগের (uoft-cs) অ্যাকাউন্টের parquet ফাইল থেকে, কারণ লেখার সময়ে মূল সার্ভার থেকে নামানো খুব ধীর ছিল; ছবি আর label একই।

দুটি network হুবহু এক, শুধু একটিতে shortcut আছে: CIFAR-এর জন্য ResNet প্রবন্ধের ছোট নকশা, 3×3-এর stem (১৬ channel), তিনটি stage (১৬, ৩২, ৬৪ channel; দ্বিতীয় আর তৃতীয়টির শুরুতে stride 2), প্রতিটিতে ৯টি basic block, শেষে global average pooling আর fc: মোট ৬ × ৯ + ২ = ৫৬টি ওজনওয়ালা স্তর। দুটিতেই প্রতিটি conv-এর পরে BatchNorm আর একই init (একই seed). ResNet-এ stage বদলের দুই জায়গায় projection shortcut, তাই parameter সামান্য বেশি। Training: SGD, lr 0.1, momentum 0.9, weight decay 5e-4, cosine schedule (পর্ব ১৫), batch ১২৮, ২০ epoch, augmentation শুধু অর্ধেক ছবি এলোমেলোভাবে উল্টানো। অনুশীলন ৪-এর BasicBlock-ই এখানে ব্যবহার হয়েছে, residual=False দিলে যোগ বাদ।

~/bazarghar-dl$ uv run python train_cifar.py 9 20
Python 3.14.4 | torch 2.15.0+cpu | torchvision 0.29.2+cpu
CIFAR-10 subset: train 10000, test 2000 | depth 56 | 20 epochs, SGD 0.1 momentum 0.9, cosine, batch 128
plain   params 853,018 | final train loss 1.722 | test acc 0.353 | 570 s on CPU
resnet  params 855,770 | final train loss 0.589 | test acc 0.669 | 682 s on CPU
epoch  plain loss  resnet loss | plain acc  resnet acc
    1       2.306        2.401 |     0.113       0.183
    2       2.279        1.993 |     0.112       0.260
    5       2.091        1.654 |     0.210       0.369
   10       1.902        1.245 |     0.238       0.546
   15       1.793        0.865 |     0.331       0.644
   20       1.722        0.589 |     0.353       0.669

৫৬ স্তরের plain CNN বনাম ResNet, CIFAR-10-এর ১০,০০০ ছবি

Top: train loss per epoch. Bottom: test accuracy (2,000 images). One local run.

Train loss and test accuracy of a 56-layer plain CNN and ResNet on a CIFAR-10 subset 01.02.0 00.20.40.60.8 11020 11020 epoch train loss test accuracy plain 1.72 ResNet 0.59 ResNet 0.67 plain 0.35
ছবি ২২.২ — একই গভীরতা, একই parameter-সংখ্যা (প্রায়), একই training; শুধু shortcut-এর পার্থক্যে train loss আর test accuracy দুটিতেই বড় ফারাক।

Plain-56: ২০ epoch পরেও train loss ১.৭২, test accuracy ০.৩৫৩; দশটি class-এ অনুমানের ০.১০-এর চেয়ে ভালো, কিন্তু ধীর আর অস্থির (accuracy epoch ৯-এ ০.৩০০, epoch ১০-এ ০.২৩৮)। ResNet-56: train loss ০.৫৯, test accuracy ০.৬৬৯. লক্ষ করুন, পার্থক্য train loss-এ: plain network ডেটাটুকুও মুখস্থ করতে পারেনি, অর্থাৎ সমস্যা optimisation-এর, overfitting-এর নয়। BatchNorm থাকা সত্ত্বেও এটি ঘটেছে, যা ResNet প্রবন্ধের মূল পর্যবেক্ষণ (প্রবন্ধে CIFAR-10-এর পুরো ডেটায় plain-56-এর training error plain-20-এর চেয়ে বেশি)। সময়: ResNet প্রায় ২০% বেশি নিয়েছে, যোগ আর projection-এর খরচ; মেশিনে অন্য কাজ চলছিল, তাই সময়ের সংখ্যা মোটামুটি।

একই script ২০ স্তরে (প্রতি stage-এ ৩টি block) চালালে ছবিটি সম্পূর্ণ হয় (সত্যিকারের run, train_cifar.py 3 20, শেষ epoch):

modelparametertrain losstest accCPU-তে সময়
plain-20269,7220.8080.647151 s
ResNet-20272,4740.2850.719156 s
plain-56853,0181.7220.353570 s
ResNet-56855,7700.5890.669682 s

Plain network-এ গভীরতা বাড়িয়ে প্রায় তিনগুণ parameter দিলে ফল খারাপ হয়েছে: train loss ০.৮১ থেকে ১.৭২, accuracy ০.৬৪৭ থেকে ০.৩৫৩. এটিই degradation. ResNet-এ ২০ স্তরেও shortcut সাহায্য করেছে। তবে একটি সৎ পর্যবেক্ষণ: এই ছোট ডেটা আর ২০ epoch-এ ResNet-56 ResNet-20-কে ছাড়াতে পারেনি (০.৬৬৯ বনাম ০.৭১৯); গভীর model-এর পূর্ণ সুবিধা পেতে সাধারণত বেশি ডেটা আর লম্বা training লাগে। Shortcut গভীরতাকে শেখানো যায় এমন করে; গভীরতা নিজে থেকে সবসময় লাভ দেয় না।

এই ফল কতদূর খাটে: এক seed-এর এক run, ছোট ডেটা, মাত্র ২০ epoch, প্রায় কোনো augmentation নেই। ResNet প্রবন্ধে পুরো CIFAR-10-এ, অনেক বেশি training-এ ResNet-56-এর test error প্রায় ৭%; এখানকার ০.৬৬৯ তার সঙ্গে তুলনীয় নয়। দাবি করার আগে কয়েকটি seed আর আরও লম্বা training লাগত (পর্ব ১৯); augmentation-এর প্রভাব পর্ব ২৪ (Data augmentation)-এর বিষয়।

৬ · MobileNet — depthwise separable convolution

ResNet সার্ভারের GPU-র জন্য বানানো। ফোনে, যেখানে বিদ্যুৎ, মেমরি আর গতি সবই সীমিত, অন্য প্রশ্ন: একই কাজ কত কম গুণ-যোগে করা যায়? একটি standard conv একসঙ্গে দুটি কাজ করে: আশপাশের pixel মেশায় (স্থানিক) আর channel মেশায়। Depthwise separable convolution কাজ দুটি ভাগ করে:

  • Depthwise conv: প্রতিটি channel-এর নিজের একটি k × k filter, channel মেশে না। PyTorch-এ nn.Conv2d(C, C, k, groups=C), weight-এর shape (C, 1, k, k).
  • Pointwise conv: একটি 1×1 conv, যা প্রতিটি pixel-এ channel মেশায় (অংশ ৩-এ দেখেছেন, এটি প্রতি pixel-এ একটি matrix গুণ)।

একটি রেস্তোরাঁয় দশটি পদের প্রতিটির জন্য আলাদা বাবুর্চি সব মশলা নিজে বাটেন, এটি standard conv. বুদ্ধিমান রেস্তোরাঁ আগে প্রতিটি মশলা আলাদা করে বাটে (depthwise), তারপর প্রতিটি পদে শুধু অনুপাত মিশিয়ে নেয় (pointwise). কাজ অনেক কম, তবে প্রতিটি পদের মশলা আর আলাদা কায়দায় বাটা যায় না; সেই স্বাধীনতাটুকু হারায়।

খরচের হিসাব, প্রতিটি output pixel-এ: standard k²·Cin·Cout; separable k²·Cin + Cin·Cout. ভাগ করলে

separable ÷ standard = 1/Cout + 1/k2

3×3 kernel আর বেশি channel-এ অনুপাত ১/৯-এর দিকে যায়, অর্থাৎ প্রায় ৮ থেকে ৯ গুণ কম গুণ-যোগ। নিচের ব্লক কয়েকটি channel-সংখ্যায় হিসাবটি করে, তারপর একটি ছোট input-এ দেখায় যে separable conv আসলে একটি বিশেষ গঠনের standard conv: তার সমান standard kernel W[o, c] = P[o, c] · D[c] লেখা যায়, কিন্তু উল্টোটা সবসময় নয়।

import numpy as np
from numpy.lib.stride_tricks import sliding_window_view

k = 3
print("  C   standard   separable   ratio  1/C+1/k^2")
for C in [16, 32, 64, 128, 256, 512]:          # C_in = C_out = C
    std = k * k * C * C                         # প্রতি output pixel-এ MAC
    sep = k * k * C + C * C                     # depthwise k×k + pointwise 1×1
    print(f"{C:4d} {std:10,} {sep:11,} {sep / std:7.4f} {1 / C + 1 / k**2:10.4f}")

rng = np.random.default_rng(42)
Cin, Cout, H = 8, 16, 6
x = rng.normal(size=(Cin, H, H))
D = rng.normal(size=(Cin, k, k))                # depthwise: প্রতি channel-এ নিজের filter
P = rng.normal(size=(Cout, Cin))                # pointwise: 1×1, channel মেশানো
win = sliding_window_view(np.pad(x, ((0, 0), (1, 1), (1, 1))), (k, k), axis=(1, 2))
dw = np.einsum("chwij,cij->chw", win, D)        # (8, 6, 6): channel মেশে না
sep_out = np.einsum("oc,chw->ohw", P, dw)       # (16, 6, 6)
W_full = P[:, :, None, None] * D[None]          # সমান মানের একটি standard kernel
std_out = np.einsum("chwij,ocij->ohw", win, W_full)
print("depthwise out", dw.shape, "-> pointwise out", sep_out.shape)
print("same as standard conv with W[o,c] = P[o,c]*D[c]:", np.allclose(sep_out, std_out))
print("weights: separable", D.size + P.size, "| standard", Cout * Cin * k * k)

আউটপুট (পাতার Python-এ চালানো):

  C   standard   separable   ratio  1/C+1/k^2
  16      2,304         400  0.1736     0.1736
  32      9,216       1,312  0.1424     0.1424
  64     36,864       4,672  0.1267     0.1267
 128    147,456      17,536  0.1189     0.1189
 256    589,824      67,840  0.1150     0.1150
 512  2,359,296     266,752  0.1131     0.1131
depthwise out (8, 6, 6) -> pointwise out (16, 6, 6)
same as standard conv with W[o,c] = P[o,c]*D[c]: True
weights: separable 200 | standard 1152

“ratio” আর সূত্রের কলাম প্রতিটি সারিতে হুবহু এক। ১৬ channel-এ খরচ প্রায় ১৭%, ৫১২-তে ১১.৩%: channel যত বেশি, 1/C অংশ তত ছোট, আর খরচের বেশির ভাগ তখন pointwise 1×1-এ। শেষ তিন লাইন: separable output-কে একটি standard conv দিয়ে হুবহু পাওয়া গেল, যার kernel rank-১ গঠনের; separable-এ ২০০টি weight, সাধারণ standard-এ ১,১৫২. অর্থাৎ separable হলো standard conv-এর একটি সীমিত রূপ: কম খরচ, কম প্রকাশক্ষমতা।

MobileNet পরিবার এই ধারণার ওপর দাঁড়ানো। Howard ও সহকর্মীদের ২০১৭ সালের MobileNet (v1) প্রায় পুরো network depthwise separable conv দিয়ে বানায়, সঙ্গে width আর resolution কমানোর দুটি গুণক। ২০১৮ সালে Sandler ও সহকর্মীদের MobileNetV2 আনে inverted residual: ResNet-এর bottleneck-এর উল্টো, সরু input থেকে 1×1 দিয়ে channel বাড়ানো (সাধারণত ৬ গুণ), চওড়া জায়গায় depthwise 3×3, তারপর 1×1 দিয়ে আবার সরু, আর shortcut সরু অংশের মধ্যে; শেষ 1×1-এর পরে ReLU নেই (“linear bottleneck”), কারণ সরু জায়গায় ReLU তথ্য নষ্ট করে বলে লেখকেরা যুক্তি দেন। ২০১৯ সালের MobileNetV3 (Howard ও সহকর্মী) স্থাপত্যের অংশ খুঁজে পেতে neural architecture search ব্যবহার করে আর যোগ করে squeeze-and-excitation (প্রতিটি channel-এর গুরুত্ব একটি ছোট gate দিয়ে শেখা) আর hard-swish activation. torchvision-এর mobilenet_v3_small এই পরিবারের সবচেয়ে ছোট সদস্যদের একটি।

৭ · EfficientNet — depth, width আর resolution একসঙ্গে বাড়ানো

একটি CNN-কে বড় করার তিনটি পথ: আরও স্তর (depth, d), প্রতিটি স্তরে আরও channel (width, w), বা বড় input ছবি (resolution, r). ২০১৯ সালে Mingxing Tan আর Quoc V. Le-এর EfficientNet প্রবন্ধ (ICML ২০১৯) দেখায়, একটি পথে বাড়ালে লাভ দ্রুত কমে আসে, আর প্রস্তাব করে compound scaling: একটি সংখ্যা φ দিয়ে তিনটিকে একসঙ্গে বাড়ানো,

d = αφ, w = βφ, r = γφ, শর্ত α · β2 · γ2 ≈ 2

কেন β আর γ-এর বর্গ? একটি conv-এর FLOP depth-এর সঙ্গে সমানুপাতিক, কিন্তু width দ্বিগুণ হলে Cin আর Cout দুটিই দ্বিগুণ, তাই খরচ ৪ গুণ; resolution দ্বিগুণ হলে উচ্চতা আর প্রস্থ দুটিই দ্বিগুণ, তাই pixel ৪ গুণ। মোট FLOP তাই প্রায় d · w² · r², আর শর্তটির মানে প্রতিটি φ বাড়লে FLOP প্রায় দ্বিগুণ। প্রবন্ধে ছোট base model-এ grid search দিয়ে α = ১.২, β = ১.১, γ = ১.১৫ পাওয়া যায়। Base model B0 নিজেও neural architecture search দিয়ে খোঁজা, যার মূল ইট MobileNetV2-এর inverted residual (MBConv) আর squeeze-and-excitation.

import numpy as np
import matplotlib.pyplot as plt

a, b, g = 1.2, 1.1, 1.15            # depth, width, resolution-এর ভিত্তি (EfficientNet প্রবন্ধ)
print("a * b^2 * g^2 =", round(a * b**2 * g**2, 4))
print("phi  depth  width  res   FLOPs x")
phis = np.arange(0, 8)
for phi in phis:
    d, w, r = a**phi, b**phi, g**phi
    print(f"{phi:3d} {d:6.2f} {w:6.2f} {224 * r:5.0f} {d * w**2 * r**2:8.2f}")

print("one-dimension options for the same 2x FLOPs:")
print(f"  depth x2.00 | width x{np.sqrt(2):.3f} | resolution {224 * np.sqrt(2):.0f}px")

# torchvision 0.29-এর প্রকাশিত config: (width, depth, crop) আর ডেস্কটপে মাপা MAC অনুপাত
tv = {"B1": (1.0, 1.1, 240, 1.78), "B2": (1.1, 1.2, 288, 2.82), "B3": (1.2, 1.4, 300, 4.74)}
for name, (w, d, r, measured) in tv.items():
    pred = d * w**2 * (r / 224)**2
    print(f"{name}: d*w^2*r^2 = {pred:.2f} | measured MACs / B0 = {measured}")

fig, ax = plt.subplots(figsize=(6, 3.4))
ax.semilogy(phis, a**phis, "o-", label="depth x")
ax.semilogy(phis, b**phis, "s-", label="width x")
ax.semilogy(phis, g**phis, "^-", label="resolution x")
ax.semilogy(phis, (a * b**2 * g**2)**phis, "k--", label="FLOPs x")
ax.set_xlabel("phi (compound coefficient)"); ax.set_ylabel("multiplier (log)")
ax.set_title("EfficientNet compound scaling"); ax.legend(fontsize=8)
plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে log-scale-এর ছবি):

a * b^2 * g^2 = 1.9203
phi  depth  width  res   FLOPs x
  0   1.00   1.00   224     1.00
  1   1.20   1.10   258     1.92
  2   1.44   1.21   296     3.69
  3   1.73   1.33   341     7.08
  4   2.07   1.46   392    13.60
  5   2.49   1.61   451    26.11
  6   2.99   1.77   518    50.14
  7   3.58   1.95   596    96.28
one-dimension options for the same 2x FLOPs:
  depth x2.00 | width x1.414 | resolution 317px
B1: d*w^2*r^2 = 1.26 | measured MACs / B0 = 1.78
B2: d*w^2*r^2 = 2.40 | measured MACs / B0 = 2.82
B3: d*w^2*r^2 = 3.62 | measured MACs / B0 = 4.74

α·β²·γ² = ১.৯২, তাই প্রতি ধাপে FLOP প্রায় ১.৯২ গুণ; φ = ৭-এ প্রায় ৯৬ গুণ, depth প্রায় ৩.৬ গুণ, width প্রায় দ্বিগুণ, ছবি ৫৯৬ pixel. একই দ্বিগুণ খরচে একটি মাত্রায় বাড়ালে depth দ্বিগুণ, বা width ১.৪১৪ গুণ, বা ছবি ৩১৭ pixel; প্রবন্ধের দাবি, তিনটি একটু একটু করে বাড়ানো একটি অনেক বাড়ানোর চেয়ে ভালো, কারণ বড় ছবিতে সূক্ষ্ম নকশা ধরতে বড় receptive field (বেশি স্তর) আর বেশি channel দুটিই লাগে। শেষ তিন লাইন একটি সৎ সতর্কবার্তা: torchvision-এ প্রকাশিত B1–B3-এর সংখ্যা সূত্রের φ-এর হুবহু মান নয়, আর ডেস্কটপে মাপা MAC সূত্রের অনুমানের চেয়ে বেশি। কারণ স্তরের সংখ্যা গোল করা হয় ওপরের দিকে (১ স্তর × ১.১ মানে ২ স্তর), আর channel ৮-এর গুণিতকে গোল হয়। সূত্র দিক দেখায়; আসল খরচ মেপে নিতে হয়।

~/bazarghar-dl$ uv run python effscale.py
model             crop  params(M)  MACs(G)  x B0
efficientnet_b0    224       5.29    0.386  1.00
efficientnet_b1    240       7.79    0.687  1.78
efficientnet_b2    288       9.11    1.088  2.82
efficientnet_b3    300      12.23    1.827  4.74

ওপরের সংখ্যাগুলো এই সত্যিকারের run থেকে (weights=None, torch-এর FlopCounterMode-এ গোনা FLOP-কে ২ দিয়ে ভাগ করে MAC). পরে একই লেখকেরা ২০২১ সালে EfficientNetV2 প্রকাশ করেন, যেখানে শুরুর দিকের স্তরে depthwise-এর বদলে সাধারণ conv (Fused-MBConv) আর training-এর সময় ধীরে ধীরে ছবি বড় করার কৌশল আনা হয়, কারণ বড় ছবি আর depthwise conv-এ training ধীর বলে তাঁরা দেখান।

৮ · ConvNeXt — Transformer-যুগে CNN-কে আধুনিক করা

২০২০ সালে Vision Transformer (ViT; বিস্তারিত পর্ব ৩৮, Vision Transformer — ছবিকে patch-এর sequence হিসেবে দেখা) দেখায় যে convolution ছাড়াও ছবির শ্রেণিবিন্যাসে খুব ভালো ফল সম্ভব, বিশেষত বড় ডেটায়। প্রশ্ন উঠল: সাফল্যটা কি attention-এর, নাকি সঙ্গে আসা নতুন নকশা আর training-এর কৌশলের? ২০২২ সালে Zhuang Liu ও সহকর্মীদের “A ConvNet for the 2020s” প্রবন্ধ (CVPR ২০২২) একটি ResNet-50 থেকে শুরু করে ধাপে ধাপে Transformer-যুগের নকশা যোগ করে, আর প্রতিটি ধাপের প্রভাব মাপে। ফলাফলের নাম ConvNeXt. প্রবন্ধে বর্ণিত প্রধান বদলগুলো:

Training-এর কৌশল

AdamW, অনেক বেশি epoch, Mixup/CutMix-এর মতো augmentation, stochastic depth, label smoothing. প্রবন্ধ জানায়, শুধু এতেই ResNet-50-এর accuracy কয়েক শতাংশ বাড়ে।

Macro নকশা

Stage-এ block-এর অনুপাত (3, 3, 9, 3); stem হলো 4×4, stride 4-এর একটি “patchify” conv, যা ViT-এর patch-এর মতো ছবিকে টুকরো করে।

Block-এর ভেতর

Depthwise 7×7 conv (বড় kernel, MobileNet-এর ধারণা), inverted bottleneck (১ → ৪ → ১ গুণ), কম activation আর কম normalization, ReLU-র বদলে GELU, BatchNorm-এর বদলে LayerNorm.

আলাদা downsampling

Stage-এর মাঝে আলাদা 2×2, stride 2-এর conv স্তর, আগে LayerNorm; residual block-এর ভেতরে stride নেই।

torchvision-এর ConvNeXt-Tiny-এর একটি block ছাপলে নকশাটি স্পষ্ট দেখা যায় (সত্যিকারের run, একই peek.py):

CNBlock(
  (block): Sequential(
    (0): Conv2d(96, 96, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), groups=96)
    (1): Permute()
    (2): LayerNorm((96,), eps=1e-06, elementwise_affine=True, bias=True)
    (3): Linear(in_features=96, out_features=384, bias=True)
    (4): GELU(approximate='none')
    (5): Linear(in_features=384, out_features=96, bias=True)
    (6): Permute()
  )
  (stochastic_depth): StochasticDepth(p=0.0, mode=row)
)
0 Conv2dNormActivation (1, 96, 56, 56)
1 Sequential (1, 96, 56, 56)
2 Sequential (1, 192, 28, 28)
…
6 Sequential (1, 768, 7, 7)
7 Sequential (1, 768, 7, 7)

groups=96 মানে depthwise 7×7. তারপর Permute tensor-কে (N, C, H, W) থেকে (N, H, W, C)-তে নেয়, যাতে LayerNorm শেষ মাত্রা, অর্থাৎ channel ধরে normalize করে, আর দুটি Linear আসলে প্রতি pixel-এ 1×1 conv (৯৬ → ৩৮৪ → ৯৬)। একটি মাত্র GELU, একটি মাত্র normalization. নিচের সারিগুলো দেখায় stem ২২৪ থেকে সরাসরি ৫৬ × ৫৬ বানায় (stride 4), তারপর চারটি stage ৯৬ → ১৯২ → ৩৮৪ → ৭৬৮ channel. Residual যোগ হয় block-এর বাইরে, ResNet-এর মতোই; ConvNeXt তাই আসলে ResNet-এর বংশধর। প্রবন্ধের দাবি, এই CNN একই আকারের Swin Transformer-এর সমান বা ভালো ফল দেয়; তুলনাটি নির্দিষ্ট ডেটা আর training কৌশলে, আর কোনটি “ভালো” তা নিয়ে আলোচনা চলছে।

৯ · চারটি model মাপা — parameter, FLOP আর CPU latency, আর বাছাই

কাগজের হিসাব যথেষ্ট নয়, মেপে দেখতে হয়। নিচের script torchvision-এর পাঁচটি model weights=None দিয়ে বানায় (কোনো download নেই, এলোমেলো init; গতি আর আকার মাপতে trained weight লাগে না), তারপর প্রতিটির জন্য: parameter গোনে; Conv2d আর Linear-এ forward hook বসিয়ে MAC গোনে; PyTorch-এর নিজস্ব torch.utils.flop_counter.FlopCounterMode দিয়ে FLOP মিলিয়ে দেখে; আর একটি ২২৪ × ২২৪ ছবিতে model.eval() ও torch.inference_mode()-এ দশবার warm-up-এর পরে ৫০ বার চালিয়ে latency-র median নেয়।

def count_macs(model, x):
    macs = 0
    def conv_hook(m, inp, out):
        nonlocal macs
        k = m.kernel_size[0] * m.kernel_size[1] * (m.in_channels // m.groups)
        macs += out.numel() * k                    # প্রতিটি output ঘরে k গুণ-যোগ
    def lin_hook(m, inp, out):
        nonlocal macs
        macs += out.numel() * m.in_features
    hs = [m.register_forward_hook(conv_hook) for m in model.modules() if isinstance(m, nn.Conv2d)]
    hs += [m.register_forward_hook(lin_hook) for m in model.modules() if isinstance(m, nn.Linear)]
    with torch.inference_mode():
        model(x)
    for h in hs:
        h.remove()
    return macs

লক্ষ করুন in_channels // m.groups: depthwise conv-এ groups = C, তাই প্রতিটি output ঘর মাত্র k × k গুণ-যোগ করে। সত্যিকারের run (ডেস্কটপ, CPU-তে; Python 3.14.4, torch 2.15.0+cpu, torchvision 0.29.2):

~/bazarghar-dl$ uv run python zoo.py
weights=None (random init), eval mode, input 1x3x224x224, CPU, 4 threads
model                params(M)  MACs(G) FLOPs(G) median ms  min ms
resnet18                 11.69    1.814    3.628      15.8    14.9
resnet50                 25.56    4.089    8.178      34.4    31.9
mobilenet_v3_small        2.54    0.057    0.113       4.6     3.9
efficientnet_b0           5.29    0.386    0.772      12.1    10.4
convnext_tiny            28.59    4.456    8.911      30.0    27.8
~/bazarghar-dl$ uv run python zoo1.py        # একই, কিন্তু torch.set_num_threads(1)
model                params(M)  MACs(G) FLOPs(G) median ms  min ms
resnet18                 11.69    1.814    3.628      51.9    49.5
resnet50                 25.56    4.089    8.178     102.0    98.4
mobilenet_v3_small        2.54    0.057    0.113       5.0     4.6
efficientnet_b0           5.29    0.386    0.772      20.2    19.3
convnext_tiny            28.59    4.456    8.911      92.3    86.7

প্রথমে নিশ্চিত হওয়ার বিষয়: hook-এর MAC × ২ = FlopCounterMode-এর FLOP, প্রতিটি সারিতে। এখন আসল শিক্ষা। FLOP আর সময় সমানুপাতিক নয়। mobilenet_v3_small-এর MAC resnet18-এর প্রায় ৩২ ভাগের এক ভাগ, অথচ ৪ thread-এ সময় মাত্র প্রায় ৩.৪ গুণ কম; efficientnet_b0-এর MAC প্রায় ৪.৭ গুণ কম, কিন্তু সময় মাত্র ১৫.৮ বনাম ১২.১ ms. কারণ depthwise conv-এ প্রতিটি সংখ্যা মেমরি থেকে আনার পরে খুব কম গুণ-যোগ হয়, অর্থাৎ কাজটি memory-bound; আর ছোট model-এ অনেকগুলো ছোট স্তর চালানোর নিজস্ব খরচ থাকে। বড় 3×3 আর 1×1 conv উল্টো, CPU-র vector একক আর একাধিক thread পুরো কাজে লাগাতে পারে: resnet18 এক thread থেকে চার thread-এ ৩.৩ গুণ দ্রুত হয়েছে, mobilenet প্রায় একই থেকেছে। একই কারণে convnext_tiny-এর FLOP resnet50-এর চেয়ে বেশি হলেও এখানে সময় সামান্য কম। এসব এক মেশিনের এক run, অন্য কাজ পাশাপাশি চলছিল; ফোন, GPU বা অন্য CPU-তে অনুপাত বদলাবে।

torchvision প্রতিটি প্রকাশিত weight-এর সঙ্গে ImageNet-এর validation accuracy জানায়, যা weight download না করেই পড়া যায়। এগুলো torchvision-এর রিপোর্ট করা সংখ্যা, এখানে মাপা নয় (সত্যিকারের run):

~/bazarghar-dl$ uv run python meta.py
ResNet18_Weights.IMAGENET1K_V1             acc@1  69.76  file   44.7 MB
ResNet50_Weights.IMAGENET1K_V1             acc@1  76.13  file   97.8 MB
ResNet50_Weights.IMAGENET1K_V2             acc@1  80.86  file   97.8 MB
MobileNet_V3_Small_Weights.IMAGENET1K_V1   acc@1  67.67  file    9.8 MB
EfficientNet_B0_Weights.IMAGENET1K_V1      acc@1  77.69  file   20.5 MB
ConvNeXt_Tiny_Weights.IMAGENET1K_V1        acc@1  82.52  file  109.1 MB

ResNet-50-এর দুই সারি লক্ষ করুন: একই স্থাপত্য, একই ফাইলের আকার, কিন্তু V2 weight নতুন training কৌশলে শেখানো, accuracy প্রায় ৪.৭ শতাংশ-বিন্দু বেশি। স্থাপত্যের তুলনা তাই training কৌশল আলাদা থাকলে অর্থহীন হতে পারে, যা ConvNeXt প্রবন্ধের প্রথম শিক্ষাও। এবার বাজারঘরের দুটি বাস্তব প্রশ্নে এই সংখ্যাগুলো কাজে লাগানো যাক।

কাজ: পণ্যের ছবি থেকে category বলা (প্রায় ৪০টি category)
ধাপ
ফোন অ্যাপ
সার্ভার
  1. প্রশ্ন. একই কাজ, দুটি আলাদা জায়গা। ফোনে একবারে একটি ছবি আর উত্তর তাৎক্ষণিক চাই; সার্ভারে অনেক ছবি একসঙ্গে, অপেক্ষা চলে।
  2. সীমা. ফোনে latency, অ্যাপের আকার আর ব্যাটারি; সার্ভারে প্রতি ছবির খরচ আর মোট throughput. সীমা আগে লিখে নিলে পরের সিদ্ধান্ত সহজ।
  3. ছাঁটাই. ফোনের জন্য ১০০ MB-এর weight ফাইল অবাস্তব, তাই বড় দুটি বাদ। সার্ভারে আকার কোনো বাধা নয়।
  4. মাপ. এই মেশিনের CPU-র মাপ ফোনের প্রতিনিধি নয়, শুধু একটি ইঙ্গিত; ফোনে মাপতে হয় ফোনেই, export করা model দিয়ে। সার্ভারের জন্য CPU-র এক-ছবির latency নয়, GPU-তে batch-এর throughput মাপা দরকার।
  5. Accuracy. ImageNet-এর সংখ্যা বাজারঘরের ছবিতে সরাসরি খাটে না; pretrained weight থেকে নিজের ডেটায় শিখিয়ে নিজের validation set-এ মাপতে হয়, পর্ব ২৩ (Transfer learning)-এর বিষয়।
  6. সিদ্ধান্ত. ফোনে MobileNet বা EfficientNet-B0, সার্ভারে ConvNeXt বা ResNet-50; শেষ কথা বলে লক্ষ্য hardware-এ মাপা latency আর নিজের ডেটার accuracy, কাগজের FLOP নয়।

আরও গভীরে: ResNet-এর পরে একই পরিবারের কয়েকটি প্রভাবশালী নকশা: ResNeXt (Xie ও সহকর্মী, ২০১৭) bottleneck-এর 3×3-কে grouped conv-এ ভাগ করে “cardinality” নামে নতুন মাত্রা যোগ করে; DenseNet (Huang ও সহকর্মী, ২০১৭) যোগের বদলে আগের সব feature map জুড়ে দেয় (concatenate); SENet (Hu ও সহকর্মী, ২০১৮) squeeze-and-excitation আনে। Radosavovic ও সহকর্মীদের ২০২০ সালের RegNet প্রবন্ধ একক model না খুঁজে ভালো network-এর পুরো “design space” খোঁজে। Bello ও সহকর্মীদের ২০২১ সালের “Revisiting ResNets” যুক্তি দেয় যে training আর scaling-এর কৌশল প্রায়ই স্থাপত্যের বদলের সমান গুরুত্বপূর্ণ।

পরীক্ষা ও ইন্টারভিউতে:

  • 256 → 64 → 64 → 256 bottleneck-এর conv weight? ১৬,৩৮৪ + ৩৬,৮৬৪ + ১৬,৩৮৪ = ৬৯,৬৩২.
  • Projection shortcut কখন? F(x) আর x-এর channel বা উচ্চতা-প্রস্থ আলাদা হলে; সাধারণত 1×1 conv, stride 2, তারপর BN.
  • C = 256, k = 3-এ depthwise separable-এর খরচের অনুপাত? 1/256 + 1/9 ≈ ০.১১৫.
  • Width দ্বিগুণ করলে FLOP কত গুণ? প্রায় ৪ গুণ (Cin আর Cout দুটিই দ্বিগুণ); resolution দ্বিগুণেও ৪ গুণ, depth দ্বিগুণে ২ গুণ।
  • কম FLOP মানেই কি দ্রুত? না; depthwise conv memory-bound, স্তরের সংখ্যা আর hardware-ও সময় ঠিক করে, তাই লক্ষ্য hardware-এ মাপতে হয়।

১০ · AI-এর যুগে — CNN স্থাপত্য নিয়ে AI-এর পরামর্শ যাচাই

“আমার অ্যাপের জন্য সবচেয়ে ভালো CNN কোনটি?” জিজ্ঞেস করলে AI assistant প্রায়ই একটি নাম আর কয়েকটি accuracy-র সংখ্যা দেয়, যার কিছু হয়তো অন্য version-এর, কিছু বানানো। এই পর্বের পদ্ধতিই যাচাইয়ের হাতিয়ার: parameter গুনুন, shape মিলিয়ে দেখুন, লক্ষ্য hardware-এ মাপুন।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
models.resnet50(pretrained=True) লেখেtorchvision 0.13 থেকে deprecated; এখন weights=ResNet50_Weights.DEFAULTwarning পড়ুন; নিজের version-এর docs দেখুন
নিজে লেখা residual block-এ stride 2 দেয় কিন্তু shortcut identity রাখেF(x) আর x-এর shape মেলে না, runtime errorএকটি ছোট tensor চালিয়ে shape ছাপুন
FLOP আর MAC গুলিয়ে ফেলে, বা “GFLOPS” লিখে MAC দেয়দুটির মধ্যে ২ গুণ পার্থক্যকোন একক, তা উৎসে দেখুন; নিজে গুনে মেলান
Latency মাপে warm-up ছাড়া, training mode-এ, gradient চালু রেখেপ্রথম run ধীর; BN আর autograd বাড়তি খরচ যোগ করেeval(), inference_mode(), warm-up, অনেক run-এর median
“MobileNet সবসময় দ্রুত” বলে দাবি করেFLOP কম হলেও memory-bound স্তরে সময় ততটা কমে নালক্ষ্য hardware-এ নিজে মাপুন
ImageNet-এর accuracy-কে নিজের ডেটার প্রতিশ্রুতি ধরেডেটা, training কৌশল আর weight-এর version আলাদানিজের validation set-এ, কয়েকটি seed-এ মাপুন

AI-কে নাম নয়, যাচাই করা যায় এমন পরিকল্পনা চাইতে বলুন:

আমি একটি অনলাইন দোকানের ফোন অ্যাপে পণ্যের ছবি থেকে category বলতে চাই (প্রায় ৪০টি category, কয়েক হাজার লেবেল করা ছবি)। আমার torch আর torchvision-এর version নিচে দিলাম। ১. torchvision-এর কোন দুই-তিনটি model বিবেচনা করা উচিত, আর কেন, তা parameter আর FLOP-এর হিসাবসহ বলো; সংখ্যার উৎস (docs-এর কোন পাতা) দাও। ২. প্রতিটির parameter গোনা আর FlopCounterMode দিয়ে FLOP মাপার কোড দাও। ৩. CPU-তে এক ছবির latency ঠিকভাবে মাপার কোড দাও: eval mode, inference_mode, warm-up, median. ৪. weights= enum দিয়ে pretrained model লোড করার আধুনিক উপায় দেখাও, pretrained= নয়। ৫. কোন সংখ্যা তুমি নিশ্চিত জানো না, তা আলাদা করে চিহ্নিত করো।

AI-এর দেওয়া স্থাপত্য বা কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১1×1 convolution কীভাবে একটি “খেলনা” থেকে আধুনিক CNN-এর সবচেয়ে বেশি ব্যবহৃত স্তরে পরিণত হলো?

প্রথম দেখায় 1×1 convolution অর্থহীন মনে হয়: যে filter আশপাশের কোনো pixel দেখে না, সে ছবির নকশা খুঁজবে কীভাবে? উত্তর হলো, এটি স্থানিক কাজ করে না, channel-এর কাজ করে: প্রতিটি pixel-এর channel-গুলোকে একটি ছোট fully connected স্তরের মতো মিশিয়ে নতুন সংখ্যা বানায়। ২০১৩ সালে Min Lin, Qiang Chen আর Shuicheng Yan-এর “Network in Network” প্রবন্ধ এই ধারণাকে জনপ্রিয় করে বলে সাধারণত উল্লেখ করা হয়; একই প্রবন্ধ global average pooling-ও প্রস্তাব করে, যা পরে ResNet-এর head-এ বসেছে।

২০১৪ সালের GoogLeNet (Inception) 1×1 conv-কে খরচ কমানোর যন্ত্র হিসেবে ব্যবহার করে: বড় 3×3 বা 5×5 conv-এর আগে channel কমিয়ে নেয় (পর্ব ২১)। ২০১৫ সালে ResNet-এর bottleneck block একই চাল নেয়, আর এই পর্বের হিসাবে দেখেছেন, ২৫৬ channel-এ এটি block-এর খরচ প্রায় ১৭ ভাগের এক ভাগে নামায়। Projection shortcut-ও একটি 1×1 conv. MobileNet-এ depthwise separable conv-এর অর্ধেক কাজ pointwise 1×1, আর বেশি channel-এ খরচের বেশির ভাগই সেখানে। ConvNeXt-এর block-এর দুটি Linear স্তর আসলে 1×1 conv, আর Transformer-এর feed-forward স্তরও প্রতিটি token-এ আলাদাভাবে একই কাজ করে।

এই বিস্তারের একটি ব্যবহারিক কারণও আছে: 1×1 conv হিসাবের দিক থেকে একটি বড় matrix গুণ, যা GPU আর CPU-র গুণ-যোগের একক সবচেয়ে দক্ষভাবে চালায়। অংশ ৯-এর মাপে দেখেছেন, depthwise conv-এ গুণ-যোগ কম হলেও সময় ততটা কমে না; বিপরীতে 1×1 conv-এর FLOP বেশি হলেও hardware তা দ্রুত শেষ করে। ফলে আধুনিক CNN-এর মোট FLOP-এর বড় অংশ প্রায়ই 1×1 স্তরে থাকে।

মূল উপলব্ধি: একটি নকশা-উপাদানের মূল্য তার কাজ ভাগ করার ক্ষমতায়: 1×1 conv “channel মেশানো”-কে “স্থানিক নকশা খোঁজা” থেকে আলাদা করে, আর সেই ভাগ থেকেই bottleneck, MobileNet আর ConvNeXt এসেছে।

প্র ০২ফোনের জন্য CNN-এর ইতিহাস কেন শুধু FLOP কমানোর গল্প নয়, আর hardware-aware নকশা কী শেখায়?

২০১৬–২০১৭ সালে ফোনে deep learning চালানোর চাহিদা বাড়লে প্রথম লক্ষ্য ছিল গুণ-যোগ কমানো। Depthwise separable conv-এর ধারণা পুরোনো; Laurent Sifre-এর ২০১৪ সালের থিসিসে এর ব্যবহার উল্লেখ করা হয়, আর François Chollet-এর ২০১৭ সালের Xception বড় network-এ এটিকে মূল ইট বানায়। একই বছর Google-এর Andrew Howard ও সহকর্মীদের MobileNet দেখায়, প্রায় পুরো network depthwise separable দিয়ে বানালে ImageNet-এ যুক্তিসংগত accuracy অনেক কম খরচে পাওয়া যায়। একই সময়ে ShuffleNet (Zhang ও সহকর্মী, ২০১৮) grouped 1×1 conv আর channel shuffle দিয়ে খরচ আরও কমায়।

কিন্তু শিগগিরই দেখা গেল, FLOP কম মানেই ফোনে দ্রুত নয়। ShuffleNet V2 প্রবন্ধ (Ma ও সহকর্মী, ২০১৮) সরাসরি যুক্তি দেয় যে মেমরি থেকে ডেটা আনা-নেওয়ার খরচ, স্তরের সংখ্যা আর একসঙ্গে চালানোর সুযোগও সময় ঠিক করে, আর নকশার নির্দেশিকা দেয় সরাসরি মাপা গতির ভিত্তিতে। MnasNet (Tan ও সহকর্মী, ২০১৯) আর MobileNetV3 neural architecture search-এর লক্ষ্যে আসল ফোনে মাপা latency যোগ করে; একে বলে hardware-aware search. এই পর্বের মাপও একই কথা বলে: mobilenet_v3_small-এর MAC resnet18-এর প্রায় ৩২ ভাগের এক ভাগ, কিন্তু চার thread-এ সময় মাত্র প্রায় সাড়ে তিন ভাগের এক ভাগ।

আরেকটি স্তর হলো সংখ্যার ধরন। ফোনে প্রায়ই float32-এর বদলে ৮-bit integer-এ model চালানো হয় (quantization), আর কিছু স্তর (যেমন কিছু activation বা squeeze-and-excitation) quantization-এ ঝামেলা করে বলে MobileNetV3-এর লেখকেরা activation বাছাইয়ে তা মাথায় রাখেন বলে জানান। Model compression আর deployment-এর বিস্তারিত এই কোর্সের পরের মডিউলে।

মূল উপলব্ধি: দক্ষতার আসল মাপ লক্ষ্য hardware-এ ঘড়ির সময়, ব্যাটারি আর মেমরি; FLOP একটি সুবিধাজনক প্রতিনিধি মাত্র, আর প্রতিনিধিকে লক্ষ্য বানালে Goodhart-এর নিয়মের মতো ভুল পথে যাওয়ার ঝুঁকি থাকে।

প্র ০৩EfficientNet-এর compound scaling কতটা “নিয়ম” আর কতটা একটি নির্দিষ্ট পরীক্ষার ফল — এর পরে scaling নিয়ে কী শেখা গেল?

Tan আর Le-এর ২০১৯ সালের প্রবন্ধের পর্যবেক্ষণ সরল: শুধু depth, শুধু width বা শুধু resolution বাড়ালে accuracy প্রথমে বাড়ে, তারপর দ্রুত থিতিয়ে যায়; তিনটি একসঙ্গে বাড়ালে একই FLOP-এ ভালো ফল আসে। α, β, γ খোঁজা হয়েছিল ছোট base model-এ একটি grid search দিয়ে, আর তারপর সেই একই মান B1 থেকে B7 পর্যন্ত ব্যবহার করা হয়, বড় model-এ আবার না খুঁজে, খরচ বাঁচাতে। তাই α = ১.২, β = ১.১, γ = ১.১৫ প্রকৃতির নিয়ম নয়, একটি নির্দিষ্ট network পরিবার আর ImageNet-এর জন্য পাওয়া মান। এই পর্বের ব্লকেও দেখেছেন, প্রকাশিত config সূত্রের হুবহু অনুসরণ করে না, আর স্তরের সংখ্যা গোল করায় আসল খরচ সূত্রের চেয়ে বেশি।

পরের কাজগুলো ছবিটি জটিল করেছে। EfficientNetV2 (২০২১) দেখায়, খুব বড় ছবি training-এ মেমরি আর সময় খায়, আর শুরুর স্তরে depthwise conv hardware-এ ধীর; তাই তারা resolution কম বাড়ায় আর training-এর সময় ছবি ধীরে ধীরে বড় করে। Bello ও সহকর্মীদের “Revisiting ResNets” (২০২১) যুক্তি দেয়, আধুনিক training কৌশলে পুরোনো ResNet-ও অনেক উন্নতি করে, আর কোন মাত্রা বাড়ানো ভালো তা training-এর দৈর্ঘ্য আর overfitting-এর ঝুঁকির ওপর নির্ভর করে। Language model-এর জগতে Kaplan ও সহকর্মী (২০২০) আর Hoffmann ও সহকর্মীদের (২০২২) scaling law-এর কাজ parameter আর ডেটার ভারসাম্য নিয়ে একই ধরনের প্রশ্ন তোলে, যা পর্ব ৫১ (Scaling)-এর বিষয়।

ব্যবহারিক শিক্ষা হলো, compound scaling একটি ভালো শুরুর অনুমান: বড় করার সময় একটি মাত্রায় সব বাজেট ঢালবেন না। কিন্তু নিজের ডেটা আর hardware-এ কয়েকটি বিকল্প মেপে নেওয়াই নির্ভরযোগ্য পথ।

মূল উপলব্ধি: Scaling-এর সূত্র এক জায়গায় পাওয়া অভিজ্ঞতা থেকে আসে; সেগুলো দিক দেখায়, কিন্তু অন্য ডেটা, অন্য training কৌশল আর অন্য hardware-এ আবার যাচাই করতে হয়।

প্র ০৪ConvNeXt কি প্রমাণ করে যে CNN আর Vision Transformer সমান? “Architecture বনাম training কৌশল” বিতর্ক কী শেখায়?

২০২০ সালে Dosovitskiy ও সহকর্মীদের ViT দেখায়, ছবিকে ১৬ × ১৬ patch-এর sequence ধরে একটি সাধারণ Transformer চালালে, খুব বড় ডেটায় pretrain করলে, তখনকার সেরা CNN-এর সমান ফল আসে। ২০২১ সালে Swin Transformer স্থানীয় জানালায় attention আর ধাপে ধাপে ছোট হওয়া feature map এনে CNN-এর কিছু অভ্যাস Transformer-এ ফেরায়। একই সময়ে DeiT (Touvron ও সহকর্মী) দেখায়, শক্ত augmentation আর regularization দিয়ে ViT-কে শুধু ImageNet-এ শেখানো যায়। অর্থাৎ ফলের একটি বড় অংশ এসেছিল training কৌশল থেকে।

ConvNeXt প্রবন্ধ (Liu ও সহকর্মী, ২০২২) এই প্রশ্নটিকে পরীক্ষায় রূপ দেয়: একটি ResNet-50-কে শুধু আধুনিক training কৌশল দিলেই accuracy কয়েক শতাংশ বাড়ে বলে জানায়, তারপর একটি একটি নকশা-বদল যোগ করে Swin-এর সমান বা ভালো ফল দেখায়। এই পর্বের torchvision-এর metadata-তেও একই সুর: একই ResNet-50, V1 weight-এ ৭৬.১৩% আর নতুন কৌশলের V2 weight-এ ৮০.৮৬%.

তবু “সমান” বলা সরলীকরণ। CNN-এর inductive bias, অর্থাৎ স্থানীয়তা আর একই weight সবখানে ব্যবহার, কম ডেটায় সুবিধা দেয়; Transformer-এর নমনীয়তা খুব বড় ডেটা আর বহু ধরনের input (ছবি আর লেখা একসঙ্গে) মেলাতে সুবিধা দেয় বলে অনেকে যুক্তি দেন। ২০২৩ সালের ConvNeXt V2 self-supervised pretraining-এর সঙ্গে CNN-কে মানিয়ে নেয়। কোন পরিবার কোন কাজে ভালো, তা ডেটার আকার, ছবির মাপ, hardware আর deployment-এর ওপর নির্ভর করে, আর এ নিয়ে গবেষণা চলছে।

মূল উপলব্ধি: দুটি স্থাপত্যের ন্যায্য তুলনা করতে training কৌশল, ডেটা আর compute সমান রাখতে হয়; নইলে “নতুন স্থাপত্য জিতেছে” আসলে “নতুন recipe জিতেছে” হতে পারে।

প্র ০৫Model-এর গতি সৎভাবে মাপা এত কঠিন কেন, আর প্রকাশিত benchmark পড়ার সময় কী কী প্রশ্ন করা উচিত?

এই পর্বের মাপেই কয়েকটি ফাঁদ দেখা গেছে। প্রথম run সবসময় ধীর, কারণ মেমরি বরাদ্দ, cache আর library-র প্রস্তুতি তখন হয়; তাই warm-up লাগে। একই model এক thread থেকে চার thread-এ ৩ গুণের বেশি দ্রুত হয়েছে, অথচ mobilenet প্রায় একই; তাই thread-এর সংখ্যা না বললে সংখ্যাটির মানে নেই। মেশিনে অন্য কাজ চললে সময় ওঠানামা করে, তাই একটি মাপ নয়, অনেক run-এর median আর সর্বনিম্ন দুটিই দেখা ভালো। Training mode-এ বা gradient চালু রেখে মাপলে BN আর autograd বাড়তি কাজ করে।

GPU-তে আরও ফাঁদ আছে: GPU-র কাজ asynchronous, তাই synchronize না করে সময় মাপলে শুধু কাজ পাঠানোর সময় ধরা পড়ে (পর্ব ১২)। Batch size বদলালে ছবিপ্রতি সময় অনেক বদলায়: সার্ভারে বড় batch-এ throughput বাড়ে, কিন্তু ফোনে একটি ছবির latency-ই আসল। Precision (float32, float16, int8), compiler (torch.compile, ONNX Runtime বা ফোনের নিজস্ব runtime) আর input-এর মাপও ফল বদলায়। MLPerf-এর মতো উদ্যোগ নিয়ম বেঁধে দিয়ে তুলনা ন্যায্য করার চেষ্টা করে।

প্রকাশিত সংখ্যা পড়ার সময় প্রশ্নগুলো: কোন hardware আর কোন software version? Batch size আর precision কত? Latency না throughput? Warm-up আর কতবার মাপা? Accuracy কোন ডেটা আর কোন weight-এ? FLOP নাকি MAC? torchvision-এর docs-এ “GFLOPS” কলামের সংখ্যাগুলো (যেমন resnet18-এ ১.৮১) এই পর্বের মাপে MAC-এর সঙ্গে মেলে, FLOP-এর সঙ্গে নয়; এমন একক-গোলমাল প্রবন্ধেও সাধারণ।

মূল উপলব্ধি: একটি গতির সংখ্যা কেবল তার পরিবেশের বর্ণনাসহ অর্থবহ; নিজের সিদ্ধান্তের জন্য নিজের লক্ষ্য hardware-এ, নিজের batch আর precision-এ মাপাই একমাত্র নির্ভরযোগ্য পথ।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান। অংশ ২, ৬ আর ৭-এর ব্লক দিয়েও যাচাই করতে পারেন।

  1. পূর্ণ numerical: (ক) ResNet-50-এর layer3-এর একটি মাঝের bottleneck block: input ১০২৪ channel, মাঝে ২৫৬. Conv weight আর BN-সহ মোট parameter কত? (খ) Input (128, 28, 28)-এ 1×1, stride 2, ১২৮ → ২৫৬-এর projection-এর output shape আর weight-সংখ্যা? (গ) C = 128, k = 5-এ depthwise separable-এর খরচ standard-এর কত ভাগ? (ঘ) EfficientNet-এ φ = 3 হলে FLOP প্রায় কত গুণ?

    (ক) ১০২৪ · ২৫৬ = ২,৬২,১৪৪; ৯ · ২৫৬ · ২৫৬ = ৫,৮৯,৮২৪; ২৫৬ · ১০২৪ = ২,৬২,১৪৪; conv মোট ১১,১৪,১১২. BN: ২ × (২৫৬ + ২৫৬ + ১০২৪) = ৩,০৭২; মোট ১১,১৭,১৮৪ (মাঝের block, তাই projection নেই)। (খ) Stride 2-এ ২৮ থেকে ১৪: (256, 14, 14); weight ১২৮ × ২৫৬ = ৩২,৭৬৮. (গ) 1/128 + 1/25 = ০.০০৭৮১ + ০.০৪ = প্রায় ০.০৪৭৮, অর্থাৎ প্রায় ২১ গুণ কম; বড় kernel-এ সঞ্চয় আরও বেশি, এজন্যই ConvNeXt-এর 7×7 depthwise সস্তা। (ঘ) (১.২ × ১.১² × ১.১৫²)³ = ১.৯২০৩³ ≈ ৭.০৮ গুণ।

  2. সমালোচনা ও নকশা: একজন সহকর্মী বাজারঘরের ফোন অ্যাপে পণ্যের category চেনাতে ResNet-152 ব্যবহার করতে চান, যুক্তি: “এটি সবচেয়ে গভীর, তাই সবচেয়ে ভালো।” তিনি latency মেপেছেন নিজের ল্যাপটপের GPU-তে, ৬৪ ছবির batch-এ, প্রথম run-এ। এই পরিকল্পনার দুর্বলতা কী, আর আপনি কী করতেন?

    দুর্বলতা: (১) গভীরতা মানেই নিজের ডেটায় ভালো নয়; কয়েক হাজার ছবিতে বিশাল model সহজে overfit করে, আর ImageNet-এর accuracy-ও আকারের সঙ্গে ধীরে বাড়ে। (২) ResNet-152-এর weight ফাইল প্রায় ২৩০ MB-এর ঘরে (প্রায় ৬ কোটি parameter, float32-এ প্রতিটি ৪ বাইট), অ্যাপের জন্য অবাস্তব। (৩) Latency ভুল জায়গায় ভুলভাবে মাপা: ফোনে GPU নেই এমন ধরে নেওয়াই নিরাপদ, ফোনে batch ১, আর প্রথম run-এ warm-up-এর খরচ মেশানো। প্রস্তাব: লক্ষ্য লিখে নেওয়া (যেমন ফোনে ৫০ ms-এর কম, ২০ MB-এর কম); mobilenet_v3_small আর efficientnet_b0 প্রার্থী; দুটিকেই pretrained weight থেকে নিজের ডেটায় শেখানো (পর্ব ২৩); নিজের validation set-এ accuracy, কয়েকটি seed-এ; আসল ফোনে export করা model-এ warm-up আর median দিয়ে latency; তারপর সিদ্ধান্ত।

  3. আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে?
    import torch
    from torch import nn
    proj = nn.Conv2d(64, 128, kernel_size=1, stride=2, bias=False)
    dw = nn.Conv2d(64, 64, kernel_size=3, padding=1, groups=64, bias=False)
    x = torch.randn(1, 64, 56, 56)
    print(proj(x).shape, sum(p.numel() for p in proj.parameters()))
    print(dw(x).shape, dw.weight.shape)
    torch.Size([1, 128, 28, 28]) 8192
    torch.Size([1, 64, 56, 56]) torch.Size([64, 1, 3, 3])

    (ডেস্কটপে সত্যিকারের run-এ মিলিয়ে দেখা।) Projection: stride 2-এ ৫৬ থেকে ২৮, channel ১২৮, weight ৬৪ × ১২৮ = ৮,১৯২. Depthwise: padding 1 আর stride 1-এ আকার একই; groups=64 মানে প্রতিটি output channel মাত্র একটি input channel দেখে, তাই weight-এর দ্বিতীয় মাত্রা in_channels // groups = ১, মোট ৫৭৬টি weight.

  4. কোড লিখুন: PyTorch-এ একটি BasicBlock(cin, cout, stride) লিখুন: দুটি 3×3 conv (প্রথমটিতে stride), প্রতিটির পরে BN, প্রথমটির পরে ReLU; channel বা stride বদলালে shortcut-এ 1×1 projection আর BN, নইলে identity; শেষে যোগ, তারপর ReLU.
    import torch
    from torch import nn
    import torch.nn.functional as F
    
    class BasicBlock(nn.Module):
        def __init__(self, cin, cout, stride=1):
            super().__init__()
            self.c1 = nn.Conv2d(cin, cout, 3, stride, 1, bias=False)
            self.b1 = nn.BatchNorm2d(cout)
            self.c2 = nn.Conv2d(cout, cout, 3, 1, 1, bias=False)
            self.b2 = nn.BatchNorm2d(cout)
            self.short = nn.Identity()
            if stride != 1 or cin != cout:                 # shape বদলালে projection
                self.short = nn.Sequential(
                    nn.Conv2d(cin, cout, 1, stride, bias=False), nn.BatchNorm2d(cout))
    
        def forward(self, x):
            out = F.relu(self.b1(self.c1(x)))
            out = self.b2(self.c2(out))
            return F.relu(out + self.short(x))
    
    blk = BasicBlock(64, 128, stride=2)
    print(blk(torch.randn(2, 64, 56, 56)).shape)    # torch.Size([2, 128, 28, 28])

    শেষ লাইনের মতো একটি ছোট tensor চালিয়ে shape দেখাই প্রথম পরীক্ষা। এই পর্বের অংশ ৫-এর run-এ ঠিক এই block-ই ব্যবহার হয়েছে, residual=False দিলে যোগ বাদ যায় আর plain network হয়। Parameter গুনলে (BN-সহ) ২,৩০,১৪৪ আসা উচিত: ৭৩,৭২৮ + ২৫৬ + ১,৪৭,৪৫৬ + ২৫৬ + ৮,১৯২ + ২৫৬.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমResNet-50-এর একটি bottleneck block-এর forward pass সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 1×1 conv দিয়ে channel কমানো, তারপর BN আর ReLU
  2. 3×3 conv (দরকারে stride 2), তারপর BN আর ReLU
  3. 1×1 conv দিয়ে channel ৪ গুণ বাড়ানো, তারপর BN
  4. Shortcut-এর পথ (identity বা projection) যোগ করা
  5. যোগফলে ReLU

256 → 64 → 64 → 256 channel-এর একটি bottleneck block-এ (1×1, 3×3, 1×1; bias নেই) conv weight-এর মোট সংখ্যা কত?

  • ১১,৭৯,৬৪৮
  • ৬৯,৬৩২
  • ৩৬,৮৬৪
  • ৭৩,৭২৮

একটি residual block-এর input (64, 56, 56) আর মূল পথের output (128, 28, 28). Shortcut-এ কী লাগবে?

  • কিছু না, identity যথেষ্ট
  • 3×3 conv, stride 1
  • 1×1 conv, stride 2, ৬৪ → ১২৮ (সঙ্গে BN)
  • শুধু 2×2 max pooling

Cin = Cout = 256 আর 3×3 kernel-এ depthwise separable conv-এর গুণ-যোগ standard conv-এর মোটামুটি কত ভাগ?

  • প্রায় ০.১১৫ (প্রায় ৯ ভাগের এক ভাগ)
  • প্রায় ০.০০৪
  • প্রায় ০.৫
  • প্রায় ১, কারণ output একই

একটি CNN-এর সব স্তরের channel-সংখ্যা দ্বিগুণ করলে (depth আর ছবির মাপ একই রেখে) FLOP মোটামুটি কত গুণ হয়?

  • ২ গুণ
  • অপরিবর্তিত
  • ৮ গুণ
  • ৪ গুণ

একটি CPU-তে mobilenet_v3_small-এর গুণ-যোগ resnet18-এর প্রায় ৩২ ভাগের এক ভাগ, কিন্তু চার thread-এ সময় মাত্র প্রায় ৩.৪ গুণ কম। সবচেয়ে যুক্তিসংগত ব্যাখ্যা কোনটি?

  • MAC গোনায় ভুল হয়েছে
  • Depthwise স্তর memory-bound আর ছোট স্তরের বাড়তি খরচ আছে, তাই সময় FLOP-এর অনুপাতে কমে না
  • Weight random বলে MobileNet ধীর চলেছে
  • MobileNet-এর parameter বেশি

ConvNeXt প্রবন্ধে ResNet-কে আধুনিক করার বদলের তালিকায় কোনটি নেই?

  • বড় kernel-এর depthwise conv (7×7)
  • BatchNorm-এর বদলে LayerNorm আর ReLU-র বদলে GELU
  • প্রতিটি block-এ self-attention স্তর যোগ
  • 4×4, stride 4-এর “patchify” stem

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Block: y = ReLU(F(x) + x); basic = দুটি 3×3, bottleneck = 1×1 কমানো → 3×3 → 1×1 বাড়ানো, বেশি channel-এ অনেক সস্তা।
  2. Shape: যোগের আগে F(x) আর x হুবহু মিলতে হবে; stage-এর শুরুতে 1×1, stride 2-এর projection আর BN.
  3. গোনা: conv-এর weight k²·Cin·Cout, MAC তার সঙ্গে output pixel গুণ, FLOP = ২ × MAC; ResNet-18 আর ResNet-50-এর parameter হাতে গুনে মেলানো যায়।
  4. আধুনিক CNN: MobileNet-এ depthwise separable (খরচ 1/C + 1/k²), EfficientNet-এ d·w²·r² ধরে একসঙ্গে বাড়ানো, ConvNeXt-এ 7×7 depthwise, LayerNorm, GELU আর নতুন training কৌশল।
  5. বাছাই: FLOP সময় নয়; লক্ষ্য hardware-এ eval mode, warm-up আর median দিয়ে মাপুন, আর accuracy মাপুন নিজের ডেটায়।

পরের পর্বে: পর্ব ২৩ (Transfer learning — pretrained model, fine-tuning আর feature extraction): এই পর্বের model-গুলোর pretrained weight নিয়ে বাজারঘরের মতো ছোট ডেটায় শেখানো, আর শূন্য থেকে শেখানোর সঙ্গে তুলনা।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান