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

Convolution — filter, stride, padding আর receptive field

Convolution — Filters, Stride, Padding, Pooling and Receptive Field

Taught in Bangla, this lesson builds convolution from one output pixel up. In-page NumPy implements it with four loops and with im2col and matches nn.Conv2d in a real desktop PyTorch run. It derives the output-size formula with stride, padding and dilation, checks eight cases, explains multi-channel weight shapes and 1 × 1 convolution, tracks receptive field growth layer by layer, covers max, average and global average pooling, and measures the cost of depthwise separable convolution.

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

  • ছবিতে fully connected layer কেন ব্যর্থ, আর convolution-এর তিনটি অনুমান: স্থানীয়তা, weight sharing আর translation equivariance
  • হাতে একটি output pixel, NumPy-তে loop আর im2col দিয়ে convolution, আর kernel দিয়ে ঝাপসা, ধারালো ও কিনারা
  • Stride, padding আর dilation-এর output-size সূত্র, আসল array দিয়ে যাচাই
  • অনেক channel-এ weight-এর shape (out, in, kH, kW), 1 × 1 convolution আর parameter গোনা
  • Receptive field-এর হিসাব, max/average/global average pooling, depthwise separable convolution-এর খরচ, আর nn.Conv2d দিয়ে সব মিলিয়ে দেখা

মডিউল ৩ পর্যন্ত সব network ছিল MLP: প্রতিটি input প্রতিটি neuron-এ যায়। এই মডিউলে ছবি আসছে, আর ছবির জন্য একটি বিশেষ স্তর লাগে: convolution. AI-এর ভিত্তি কোর্সের পর্ব ১৮-এ হাতে একটি 3 × 3 convolution দিয়ে ছবি ঝাপসা করেছিলেন আর কিনারা খুঁজেছিলেন। এখানে সেটিকে neural network-এর স্তর বানাব: অনেক channel, stride, padding, শেখা weight. পাতার চারটি ব্লক ব্রাউজারের Python আর NumPy-তে চলে; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে। PyTorch-এর সব আউটপুট ডেস্কটপের সত্যিকারের run থেকে, CPU-তে।

১ · ছবিতে dense layer কেন হেরে যায়

বাজারঘরের গুদামে একজন কর্মী প্যাকেটের ছবি দেখে বলেন ভেতরে কোন পণ্য। তিনি পুরো ছবির প্রতিটি বিন্দু একসঙ্গে মুখস্থ করেন না। চোখ একটি ছোট অংশে রাখেন, “এখানে একটা কিনারা”, “এখানে লাল লেবেল” খোঁজেন, তারপর চোখ সরান। প্যাকেট তাকের বাঁয়ে থাকুক বা ডানে, একই চোখ একই নকশা চেনে। Convolution ঠিক এই অভ্যাসটিকে অঙ্কে লেখে।

একটি 224 × 224 রঙিন ছবিতে ২২৪ × ২২৪ × ৩ = ১,৫০,৫২৮টি সংখ্যা। এটিকে সমান করে (flatten) ১,০০০ neuron-এর একটি linear layer-এ দিলে weight ১,৫০,৫২৮ × ১,০০০ ≈ ১৫ কোটি, শুধু প্রথম স্তরে। পরিমাণ ছাড়াও তিনটি গভীর সমস্যা আছে:

অপচয়ী সংযোগ

বাঁ কোণের একটি pixel আর ডান কোণের একটি pixel-এর মধ্যে প্রায়ই সম্পর্ক কম, তবু dense layer প্রতিটি জোড়ার জন্য আলাদা weight রাখে। এত parameter শেখাতে বিপুল ডেটা লাগে, না হলে overfitting.

সরালেই নতুন

প্যাকেট দুই pixel ডানে সরালে dense layer-এর কাছে এটি একটি সম্পূর্ণ ভিন্ন input; বাঁয়ে শেখা নকশা ডানে আবার শিখতে হয়।

পাশাপাশির তথ্য হারায়

Flatten করলে কোন pixel কার পাশে ছিল, সেই কাঠামো model-কে আলাদা করে বলা হয় না; pixel-গুলোর ক্রম এলোমেলো করলেও dense layer-এর কাছে সমস্যাটি একই রকম কঠিন।

Convolution তিনটি অনুমান কাঠামোতেই বসায়, যাদের একসঙ্গে বলে CNN-এর inductive bias: (১) স্থানীয় সংযোগ: প্রতিটি output শুধু একটি ছোট জানালা দেখে। (২) Weight sharing: একই kernel ছবির সবখানে চলে, তাই parameter ছবির আকারের ওপর নির্ভর করে না। (৩) Translation equivariance: input সরালে output-ও ঠিক ততটাই সরে। এর ওপর pooling যোগ করলে ছোট সরণে output প্রায় অপরিবর্তিত থাকে, যাকে বলে translation invariance (পুরোপুরি নয়; ভাবনার প্রশ্ন ২ দেখুন)। এভাবে গড়া network-কে বলে CNN.

২ · হাতে একটি output pixel — জানালা, গুণ আর যোগ

একটি kernel (বা filter) হলো ছোট weight-এর টেবিল, যেমন 3 × 3. এটি input-এর ওপর জানালার মতো বসে; জানালার নিচের ৯টি সংখ্যাকে kernel-এর ৯টি সংখ্যা দিয়ে ঘরে ঘরে গুণ করে যোগ করা হয়, তারপর একটি bias b যোগ হয়। সূত্রে, input X, kernel K হলে output-এর (i, j) ঘর:

Y[i, j] = b + ∑u=0k−1 ∑v=0k−1 X[i + u, j + v] · K[u, v]

এখানে u, v kernel-এর সারি আর কলাম। লক্ষ করুন, kernel উল্টানো হয়নি। গণিতে convolution-এ kernel ১৮০° ঘুরিয়ে বসানো হয়; না ঘুরিয়ে বসানোর নাম cross-correlation. PyTorch-সহ deep learning লাইব্রেরিগুলো আসলে cross-correlation চালায়, কিন্তু নাম দেয় convolution. Kernel যেহেতু শেখা হয়, এতে ক্ষমতার কোনো পার্থক্য হয় না, শুধু হাতে বানানো kernel-এর চিহ্ন নিয়ে সতর্ক থাকতে হয়।

One output pixel of a 3x3 convolution input X (5 × 5)kernel K, b = 1output Y (3 × 3) 3 1 0 2 5 2 4 1 0 3 1 2 6 2 0 0 1 3 5 2 4 0 2 1 6 1 0 −1 2 0 −2 1 0 −1
  1. জানালা বসানো. Kernel-টি X-এর বাঁ-ওপরের 3 × 3 অংশে বসেছে (সবুজ)। এর ফলই হবে Y[0, 0]. Kernel-এর বাঁ কলাম ধনাত্মক, ডান কলাম ঋণাত্মক: বাঁয়ে উজ্জ্বল আর ডানে অন্ধকার হলে বড় ধনাত্মক মান দেবে।
  2. প্রথম সারি. X-এর [3, 1, 0] আর kernel-এর [1, 0, −1]: ৩ + ০ + ০ = ৩.
  3. দ্বিতীয় সারি. [2, 4, 1] আর [2, 0, −2]: ৪ + ০ − ২ = ২. মাঝের সারির ওজন দ্বিগুণ, Sobel kernel-এর মতো।
  4. তৃতীয় সারি আর bias. [1, 2, 6] আর [1, 0, −1]: ১ − ৬ = −৫. যোগফল ৩ + ২ − ৫ = ০, bias ১ যোগ করে Y[0, 0] = ১. জানালার বাঁ আর ডান প্রায় সমান উজ্জ্বল, তাই সাড়া ছোট।
  5. এক ঘর ডানে (stride ১). জানালা কলাম ১–৩-এ। তিন সারির ফল −১, ৮, ০; যোগ ৭, bias-সহ Y[0, 1] = ৮. এখানে মাঝের সারিতে বাঁয়ের ৪ ডানের ০-এর চেয়ে অনেক উজ্জ্বল, তাই সাড়া বড়।
  6. পুরো output. 5 × 5 input-এ 3 × 3 জানালা প্রতিটি দিকে ৩টি অবস্থানে বসে, তাই output 3 × 3. নয়টি output-এর সবগুলো একই ৯টি weight আর একটি bias দিয়ে হিসাব: এটাই weight sharing. সবচেয়ে বড় মান ১২ আর সবচেয়ে ছোট −১১, যেখানে উজ্জ্বলতা পাশাপাশি সবচেয়ে বেশি বদলায়।

Kernel মানে feature detector. Kernel-এর সংখ্যাগুলো ঠিক করে কোন নকশায় বড় সাড়া আসবে। একটি dot product বড় হয় যখন জানালার নকশা kernel-এর নকশার সঙ্গে মেলে (AI-এর ভিত্তি কোর্সের পর্ব ৩৩-এর similarity-র ধারণা)। তিনটি পরিচিত হাতে বানানো kernel:

  • Blur: সব ঘর ধনাত্মক, যোগফল ১ (যেমন Gaussian [1 2 1; 2 4 2; 1 2 1]/16): প্রতিটি pixel আশপাশের ওজনদার গড়, তাই noise আর কিনারা নরম হয়।
  • Sharpen: মাঝে ৫, চার পাশে −১, যোগফল ১: pixel-কে প্রতিবেশীর চেয়ে আলাদা করে তোলে, কিনারা ধারালো হয়।
  • Edge (Sobel): যোগফল ০: সমান অংশে ফল ০, উজ্জ্বলতা বদলালে বড় মান। এটাই edge detection.

CNN-এ কেউ এই সংখ্যা হাতে বসায় না: kernel-এর weight শুরুতে এলোমেলো, backpropagation (পর্ব ০৪) দিয়ে ডেটা থেকে শেখা হয়। একটি kernel পুরো ছবিতে চালালে যে 2-D ফল পাওয়া যায়, তাকে বলে feature map: “এই নকশা ছবির কোথায় কতটা আছে”-র মানচিত্র।

৩ · NumPy-তে convolution — চার loop বনাম im2col

বাজারঘরের একটি কাল্পনিক পণ্যের ছবি বানাই: 16 × 16, তিন channel (RGB), হালকা ধূসর পটভূমিতে একটি লাল প্যাকেট আর সাদা লেবেল, seed দেওয়া সামান্য noise. Array-এর shape (C, H, W), PyTorch-এর রীতিতে (পর্ব ০৭-এর NCHW). চারটি এলোমেলো filter, প্রতিটি 3 × 3 আর তিন channel জুড়ে, তাই weight-এর shape (4, 3, 3, 3).

দুটি পদ্ধতি। Loop: সংজ্ঞা হুবহু, প্রতিটি output ঘরের জন্য জানালা কেটে গুণ-যোগ। im2col (image to column): প্রতিটি জানালার C·k·k = ২৭টি সংখ্যা একটি কলামে সাজিয়ে একটি বড় matrix বানানো, তারপর পুরো convolution হয়ে যায় একটি matrix গুণ: (4, 27) @ (27, 196). দ্রুত matrix গুণের লাইব্রেরি (GEMM) বহু দশক ধরে অপটিমাইজ করা, তাই এই রূপান্তর বাস্তবে জনপ্রিয়। শেষ লাইনে ছবিটি দুই pixel ডানে সরিয়ে equivariance পরীক্ষা।

import numpy as np
rng = np.random.default_rng(42)
img = np.full((3, 16, 16), 0.9)                      # হালকা ধূসর পটভূমি, (C, H, W)
img[:, 3:13, 4:12] = np.array([0.8, 0.1, 0.1])[:, None, None]  # লাল প্যাকেট
img[:, 6:9, 4:12] = 1.0                              # সাদা লেবেল
img += rng.normal(0, 0.02, img.shape)                # সামান্য noise
W = rng.normal(0, 0.3, (4, 3, 3, 3))                 # 4টি filter: (out, in, kH, kW)
b = np.array([0.1, 0.0, -0.1, 0.2])
def conv_loops(x, W, b):                             # সংজ্ঞা ধরে, stride 1, padding 0
    O, C, k, _ = W.shape
    Ho, Wo = x.shape[1] - k + 1, x.shape[2] - k + 1
    y = np.zeros((O, Ho, Wo))
    for o in range(O):
        for i in range(Ho):
            for j in range(Wo):
                y[o, i, j] = np.sum(x[:, i:i+k, j:j+k] * W[o]) + b[o]
    return y
def im2col(x, k):                                    # প্রতিটি জানালা একটি কলাম
    C, H, Wd = x.shape
    Ho, Wo = H - k + 1, Wd - k + 1
    cols = np.empty((C * k * k, Ho * Wo))
    for c in range(C):
        for di in range(k):
            for dj in range(k):
                cols[(c * k + di) * k + dj] = x[c, di:di+Ho, dj:dj+Wo].ravel()
    return cols, Ho, Wo

y1 = conv_loops(img, W, b)
cols, Ho, Wo = im2col(img, 3)
y2 = (W.reshape(4, -1) @ cols + b[:, None]).reshape(4, Ho, Wo)   # একটি matrix গুণ
print("input", img.shape, "| W", W.shape, "| cols", cols.shape, "| W2d", W.reshape(4, -1).shape)
print("output", y2.shape, "| loops == im2col:", np.allclose(y1, y2), f"| max diff {np.abs(y1 - y2).max():.1e}")
y3 = conv_loops(np.roll(img, 2, axis=2), W, b)        # পণ্য 2 pixel ডানে সরানো
print("shift equivariance:", np.allclose(y3[:, :, 2:], y1[:, :, :-2]), "| y[0, 7, 5:9] =", y1[0, 7, 5:9].round(3))

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

input (3, 16, 16) | W (4, 3, 3, 3) | cols (27, 196) | W2d (4, 27)
output (4, 14, 14) | loops == im2col: True | max diff 1.1e-15
shift equivariance: True | y[0, 7, 5:9] = [0.642 0.628 0.713 0.673]

Shape-গুলো পড়ুন: 16 × 16-এ 3 × 3 জানালা প্রতিটি দিকে ১৪টি অবস্থানে বসে, তাই ১৯৬টি জানালা, আর im2col-এর matrix (27, 196): ২৭ সারি = ৩ channel × ৯ ঘর, ১৯৬ কলাম = জানালার সংখ্যা। Weight-কে (4, 27)-এ সমান করে গুণ করলে (4, 196), যা (4, 14, 14)-এ ফেরে। দুই পদ্ধতির পার্থক্য ১০−১৫-এর ঘরে, শুধু দশমিকের যোগের ক্রমের তফাত। Equivariance: ছবি দুই pixel ডানে সরালে output-ও হুবহু দুই কলাম ডানে সরেছে (কিনারার কলাম বাদে, যেখানে np.roll অন্য পাশ থেকে pixel ঘুরিয়ে এনেছে)। শেষ চারটি সংখ্যা পরে PyTorch-এর সঙ্গে মেলাব। দাম আছে: im2col-এর matrix-এ প্রতিটি pixel প্রায় k² = ৯ বার কপি হয়, তাই মেমরি বাড়ে (ভাবনার প্রশ্ন ৩)।

Backward-ও একটি matrix গুণ. im2col রূপে forward হলো Y = W2d · cols + b, যেখানে W2d-এর shape (Cout, Cink²). তাই পর্ব ০৪-এর linear স্তরের নিয়মই খাটে: ∂L/∂W2d = (∂L/∂Y) · colsT, ∂L/∂b = ∂L/∂Y-এর সব অবস্থানের যোগ, আর ∂L/∂cols = W2dT · (∂L/∂Y). শেষেরটিকে ছবির আকারে ফেরাতে প্রতিটি কলামের মান তার pixel-এর জায়গায় যোগ করতে হয় (col2im), কারণ একটি pixel অনেক জানালায় ছিল। অর্থাৎ weight sharing-এর ফলে একটি kernel-এর gradient হলো সব অবস্থানের gradient-এর যোগফল। Autograd (পর্ব ০৮) এসব নিজেই করে, কিন্তু হিসাবটি জানলে বোঝা যায় কেন বড় ছবিতে একটি kernel অনেক উদাহরণ থেকে একসঙ্গে শেখে।

এবার একটি বড় ধূসর ছবিতে তিনটি হাতে বানানো kernel. কিনারায় ছবির শেষ সারি-কলাম কপি করে (edge padding) output-এর আকার input-এর সমান রাখা হয়েছে; padding নিয়ে বিস্তারিত পরের অংশে।

import numpy as np
import matplotlib.pyplot as plt

g = np.full((32, 32), 0.85)                          # ধূসর ছবি: পটভূমি
g[6:27, 9:24] = 0.3                                  # গাঢ় প্যাকেট
g[12:17, 9:24] = 0.95                                # উজ্জ্বল লেবেল
yy, xx = np.mgrid[0:32, 0:32]
g[(yy - 21)**2 + (xx - 16)**2 <= 9] = 0.05           # গোল লোগো

def conv_same(x, k):                                 # 3x3, কিনারায় edge-padding
    xp = np.pad(x, 1, mode="edge")
    out = np.zeros_like(x)
    for di in range(3):
        for dj in range(3):
            out += k[di, dj] * xp[di:di + 32, dj:dj + 32]
    return out

kernels = {
    "blur": np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) / 16,
    "sharpen": np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]),
    "sobel_x": np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]),
}
res = {n: conv_same(g, k) for n, k in kernels.items()}
res["edges"] = np.hypot(res["sobel_x"], conv_same(g, kernels["sobel_x"].T))
for n, k in kernels.items():
    print(f"{n:8s} kernel sum = {k.sum():4.1f} | row 10, cols 6..11:", res[n][10, 6:12].round(2))
print("edges    max =", res["edges"].max().round(2), "| flat area value =", abs(res["edges"][2, 2]).round(3))
fig, ax = plt.subplots(1, 4, figsize=(8, 2.4))
for a, (t, im) in zip(ax, [("input", g), ("blur", res["blur"]),
                           ("sharpen", res["sharpen"]), ("edge magnitude", res["edges"])]):
    a.imshow(im, cmap="gray")
    a.set_title(t, fontsize=9)
    a.set_axis_off()
plt.show()

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

blur     kernel sum =  1.0 | row 10, cols 6..11: [0.85 0.85 0.71 0.44 0.3  0.3 ]
sharpen  kernel sum =  1.0 | row 10, cols 6..11: [ 0.85  0.85  1.4  -0.25  0.3   0.3 ]
sobel_x  kernel sum =  0.0 | row 10, cols 6..11: [ 0.   0.  -2.2 -2.2  0.   0. ]
edges    max = 3.1 | flat area value = 0.0

সারি ১০-এ কলাম ৮ আর ৯-এর মাঝে পটভূমি (০.৮৫) থেকে গাঢ় প্যাকেট (০.৩)-এ লাফ। Blur লাফটিকে ০.৭১, ০.৪৪-এ ভাগ করে নরম ঢাল বানিয়েছে। Sharpen উল্টো কাজ করেছে: কিনারার উজ্জ্বল পাশে ১.৪, অন্ধকার পাশে −০.২৫, মানে পার্থক্য বাড়িয়ে দিয়েছে (মান [0, 1]-এর বাইরে যেতে পারে)। Sobel-x সমান অংশে ঠিক ০, কিনারায় −২.২ (উজ্জ্বল থেকে অন্ধকারে, তাই ঋণাত্মক)। Blur আর sharpen-এর যোগফল ১ বলে সমান অংশে উজ্জ্বলতা অক্ষত; Sobel-এর যোগফল ০ বলে সমান অংশে উত্তর শূন্য। চতুর্থ ছবিতে খাড়া আর শোয়ানো Sobel মিলিয়ে কিনারার মাত্রা √(gx² + gy²): প্যাকেট, লেবেল আর গোল লোগোর রেখা উজ্জ্বল, বাকি সব কালো।

৪ · Stride, padding আর dilation — output-এর আকারের সূত্র

তিনটি hyperparameter জানালার চলাফেরা ঠিক করে:

  • Stride s: জানালা প্রতিবার কত ঘর লাফায়। s = 2 হলে output প্রায় অর্ধেক চওড়া, তাই stride দিয়ে ছবি ছোট করা (downsampling) যায়। (পর্ব ০৭-এর tensor-এর memory stride একটি ভিন্ন ধারণা, নাম একই।)
  • Padding p: ছবির চারপাশে p ঘর বাড়তি মান, সাধারণত শূন্য। এতে কিনারার pixel-ও জানালার মাঝে আসে, আর output-এর আকার রাখা যায়।
  • Dilation d: kernel-এর ঘরগুলোর মাঝে d − 1 ঘর ফাঁক। 3 × 3 kernel d = 2-তে 5 × 5 এলাকা ছোঁয়, কিন্তু weight ৯টিই থাকে।

এক দিকে n দৈর্ঘ্যের input-এ, k আকারের kernel-এর জন্য (PyTorch-এর Conv2d docs-এর সূত্র):

nout = ⌊(n + 2p − d·(k − 1) − 1) / s⌋ + 1

যুক্তি সরল: padding-সহ দৈর্ঘ্য n + 2p; dilation-সহ kernel-এর আসল চওড়া d(k − 1) + 1; জানালা প্রথম অবস্থান থেকে s করে যতবার এগোতে পারে, ততগুলো output. d = 1 আর p = 0-তে এটি পরিচিত (n − k)/s + 1. নিচের stepper-এ একই X আর kernel দিয়ে padding-এর কাজ দেখুন।

Zero padding keeps the output size 000000000000000000000000 3 1 0 2 5 2 4 1 0 3 1 2 6 2 0 0 1 3 5 2 4 0 2 1 6 1 8 −2 −11 1 12 −8 −8 5 −5 −5 8 3 −11 5 −10 1 8 −2 5 −8 −11 1 12 10 −3 −8 −8 5 14 0 2 −5 −6 8 −5 3 5 −8 1 10 0 −5 8
  1. Padding ছাড়া. (৫ − ৩)/১ + ১ = ৩, output 3 × 3. প্রতি স্তরে চারপাশ থেকে এক ঘর হারায়; দশটি এমন স্তরে 32 × 32 ছবি 12 × 12 হয়ে যেত। আর কোণের X[0, 0] মাত্র একটি জানালায় পড়ে, মাঝের pixel নয়টিতে।
  2. শূন্যের বেষ্টনী, p = ১. চারপাশে এক ঘর শূন্য বসিয়ে input 7 × 7. এখন কোণের pixel-ও কোনো জানালার মাঝখানে বসতে পারে।
  3. কোণের জানালা. জানালার পাঁচটি ঘর শূন্য। বাকি ঘর থেকে: মাঝের সারিতে 1 × (−2) = −২, নিচের সারিতে 4 × (−1) = −৪; যোগ −৬, bias-সহ −৫. ডান দিকে প্যাকেটের মতো উজ্জ্বল অংশ, বাঁয়ে শূন্য, তাই ঋণাত্মক।
  4. আকার রক্ষা. (৫ + ২ − ৩)/১ + ১ = ৫: output 5 × 5, input-এর সমান। মাঝের 3 × 3 অংশ আগের ফলের সঙ্গে হুবহু মেলে (১, ৮, −২, …)। একে বলে “same” padding.
  5. Stride ২. একই padding-এ জানালা দুই ঘর করে লাফায় (কমলা দাগ): ⌊(৫ + ২ − ৩)/২⌋ + ১ = ৩. Output 3 × 3, আর এর প্রতিটি মান stride ১-এর output-এর একটি ঘর বাদ দিয়ে দিয়ে নেওয়া: −৫, ৩, ৫ …
  6. “Same”-এর নিয়ম. Stride ১ আর বিজোড় k-তে p = (k − 1)/2 দিলে আকার থাকে: k = 3-এ p = 1, k = 5-এ p = 2, k = 7-এ p = 3. জোড় k-তে দুই পাশে সমান padding সম্ভব নয়, তাই বেশিরভাগ CNN বিজোড় kernel নেয়।

Padding-এ কী বসবে. শূন্য সবচেয়ে প্রচলিত, কিন্তু একমাত্র নয়। PyTorch-এর Conv2d-এ padding_mode দিয়ে "zeros", "reflect" (আয়নার মতো ভেতরের মান), "replicate" (কিনারার মান কপি, অংশ ৩-এর blur ব্লকে যা করা হয়েছে) আর "circular" (অন্য পাশ থেকে ঘুরিয়ে আনা) বেছে নেওয়া যায়। শূন্য দিলে কিনারায় একটি কৃত্রিম অন্ধকার ফ্রেম তৈরি হয়, যা network কিনারা চেনার সংকেত হিসেবে শিখে ফেলতে পারে। নামগুলোও মনে রাখুন: padding ছাড়া “valid”, আকার রাখলে “same”, আর k − 1 padding দিয়ে প্রতিটি আংশিক মিলও নিলে “full”.

এবার সূত্রটিকে আসল array দিয়ে পরীক্ষা করি। নিচের conv2d সূত্র ব্যবহার করে না: সে শুধু জানালা যতক্ষণ padding-সহ ছবির ভেতরে থাকে ততক্ষণ s করে এগোয়। তারপর সূত্রের উত্তর আর array-এর shape পাশাপাশি।

import numpy as np

def conv2d(x, w, s=1, p=0, d=1):
    """সংজ্ঞা ধরে: জানালা পুরোপুরি (padding-সহ) ছবির ভেতরে থাকলে তবেই একটি output."""
    x = np.pad(x, p)                                 # চারপাশে p ঘর শূন্য
    k = w.shape[0]
    span = d * (k - 1) + 1                           # dilation-সহ জানালার আসল চওড়া
    rows = range(0, x.shape[0] - span + 1, s)
    cols = range(0, x.shape[1] - span + 1, s)
    return np.array([[np.sum(x[i:i + span:d, j:j + span:d] * w) for j in cols]
                     for i in rows])

def out_size(n, k, s=1, p=0, d=1):                   # পরীক্ষার সূত্র
    return (n + 2 * p - d * (k - 1) - 1) // s + 1

rng = np.random.default_rng(7)
cases = [(16, 3, 1, 0, 1), (16, 3, 1, 1, 1), (16, 3, 2, 1, 1), (16, 5, 2, 2, 1),
         (16, 3, 1, 2, 2), (15, 3, 2, 0, 1), (7, 3, 3, 0, 1), (32, 7, 2, 3, 1)]
print(" n  k  s  p  d | formula  array")
for n, k, s, p, d in cases:
    y = conv2d(rng.normal(size=(n, n)), rng.normal(size=(k, k)), s, p, d)
    f = out_size(n, k, s, p, d)
    print(f"{n:2d} {k:2d} {s:2d} {p:2d} {d:2d} | {f:5d}  {str(y.shape):9s}",
          "ok" if y.shape == (f, f) else "MISMATCH")
x = np.arange(25.0).reshape(5, 5)
print(conv2d(x, np.ones((3, 3)), s=2, p=1))          # stride 2, padding 1: 3 x 3

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

 n  k  s  p  d | formula  array
16  3  1  0  1 |    14  (14, 14)  ok
16  3  1  1  1 |    16  (16, 16)  ok
16  3  2  1  1 |     8  (8, 8)    ok
16  5  2  2  1 |     8  (8, 8)    ok
16  3  1  2  2 |    16  (16, 16)  ok
15  3  2  0  1 |     7  (7, 7)    ok
 7  3  3  0  1 |     2  (2, 2)    ok
32  7  2  3  1 |    16  (16, 16)  ok
[[ 12.  27.  24.]
 [ 63. 108.  81.]
 [ 72. 117.  84.]]

আটটিতেই মিল। কয়েকটি লক্ষ করার মতো: (16, 3, 1, 2, 2): dilation ২-এ kernel 5 × 5 এলাকা ছোঁয়, তাই আকার রাখতে p = 2 লাগে। (15, 3, 2, 0, 1): (১৫ − ৩)/২ + ১ = ৭, ভাগ মিলে গেছে। (7, 3, 3, 0, 1): (৭ − ৩)/৩ = ১.৩৩, floor ১, তাই ২; শেষ কলামটি কোনো জানালায় পড়েনি, নীরবে বাদ গেছে। (32, 7, 2, 3, 1): 7 × 7, stride ২, padding ৩ ঠিক অর্ধেক করে, ResNet-ধাঁচের network-এর প্রথম স্তরে এমন বিন্যাস দেখবেন (পর্ব ২২)। শেষ matrix-এ all-ones kernel জানালার যোগফল দেয়: কোণে padding-এর শূন্যসহ 0 + 1 + 5 + 6 = ১২, মাঝে 6 + 7 + 8 + 11 + 12 + 13 + 16 + 17 + 18 = ১০৮.

৫ · অনেক channel — weight-এর shape, 1 × 1 convolution আর PyTorch

রঙিন ছবিতে input-এর Cin = 3টি channel. একটি filter তাই 3 × 3 নয়, Cin × k × k: প্রতিটি channel-এর জন্য আলাদা 3 × 3, আর সব channel-এর গুণফল একসঙ্গে যোগ হয়ে একটি সংখ্যা। Coutটি এমন filter থাকলে Coutটি feature map. তাই PyTorch-এ weight-এর shape (Cout, Cin, kH, kW), bias-এর (Cout,), আর:

parameter = Cout · Cin · kH · kW + Cout

একটি convolution স্তরে channel-এর হিসাব

Each filter spans all input channels and produces one output channel.

Channels in a convolution layer: input (3, H, W), weight (4, 3, 3, 3), output (4, H′, W′) input (3, H, W) W (4, 3, 3, 3) output (4, H′, W′) প্রতি filter: 3·3·3 = 27 weight + 1 bias মোট 4 × 28 = 112 parameter
ছবি ২০.১ — চারটি filter-এর প্রতিটি তিনটি input channel একসঙ্গে দেখে আর একটি output channel বানায়। Output channel-এর সংখ্যা = filter-এর সংখ্যা।

1 × 1 convolution শুনতে অর্থহীন লাগে, কিন্তু অনেক channel-এ এটি খুব কাজের: প্রতিটি pixel-এ Cinটি সংখ্যার একটি ওজনদার যোগ, অর্থাৎ প্রতিটি pixel-এ আলাদা আলাদা চালানো একটি linear layer. পাশের কোনো pixel দেখে না, শুধু channel মেশায়। কাজ: channel কমানো বা বাড়ানো (256 → 64-এ খরচ কমানো, পর্ব ২১-এর Inception আর পর্ব ২২-এর bottleneck block), আর depthwise separable convolution-এর দ্বিতীয় ধাপ (অংশ ৮)।

এবার PyTorch. অংশ ৩-এর হুবহু ছবি আর weight (একই seed) nn.Conv2d-এ কপি করে NumPy-র loop-এর ফলের সঙ্গে মেলানো হলো, তারপর F.conv2d-এ কয়েকটি stride/padding/dilation-এর shape. সত্যিকারের run (ডেস্কটপ, CPU-তে; লেখার সময়ের সংস্করণ):

conv = nn.Conv2d(in_channels=3, out_channels=4, kernel_size=3)
with torch.no_grad():                                   # weight বদল autograd-এর বাইরে
    conv.weight.copy_(torch.from_numpy(W))
    conv.bias.copy_(torch.from_numpy(b))
    x = torch.from_numpy(img).float().unsqueeze(0)      # (N, C, H, W) = (1, 3, 16, 16)
    y_t = conv(x)
diff = np.abs(y_t[0].numpy() - conv_loops(img, W, b)).max()
~/bazarghar-dl$ uv run python conv_match.py
Python 3.14.4 | torch 2.15.0+cpu | numpy 2.5.4
weight (4, 3, 3, 3) | bias (4,) | parameters 112
input (1, 3, 16, 16) -> output (1, 4, 14, 14)
max |torch - numpy loops| = 6.0e-07  (float32 vs float64)
y[0, 7, 5:9] = [0.642 0.628 0.713 0.673]

F.conv2d shapes on x = (1, 3, 32, 32), weight (8, 3, k, k):
  k=3 s=1 p=0 d=1 -> (1, 8, 30, 30)
  k=3 s=1 p=1 d=1 -> (1, 8, 32, 32)
  k=3 s=2 p=1 d=1 -> (1, 8, 16, 16)
  k=5 s=2 p=2 d=1 -> (1, 8, 16, 16)
  k=3 s=1 p=2 d=2 -> (1, 8, 32, 32)
  k=7 s=2 p=3 d=1 -> (1, 8, 16, 16)
  k=1 s=1 p=0 d=1 -> (1, 8, 32, 32)
  k=5 padding='same' -> (1, 8, 32, 32)
  stride=2 with padding='same' -> RuntimeError: padding='same' is not supported for strided convolutions
  4-channel input, 3-channel weight -> RuntimeError: Given groups=1, weight of size [8, 3, 3, 3], expected input[1, 4, 32, 32] to have 3 channels, but got 4 channels instead

পড়ুন: weight (4, 3, 3, 3), bias (4,), মোট ৪ × ২৭ + ৪ = ১১২ parameter, ছবি ২০.১-এর হিসাব। Output-এর চারটি সংখ্যা NumPy-র সঙ্গে হুবহু (০.৬৪২, ০.৬২৮ …), সর্বোচ্চ পার্থক্য ৬ × ১০−৭, কারণ PyTorch float32-এ আর NumPy float64-এ হিসাব করেছে (পর্ব ০৭)। Shape-এর তালিকা অংশ ৪-এর সূত্রের সঙ্গে মেলে; 1 × 1 kernel আকার বদলায় না, শুধু channel 3 → 8. দুটি ত্রুটিও শিক্ষণীয়: padding="same" stride ১ ছাড়া চলে না, আর input-এর channel weight-এর Cin-এর সঙ্গে না মিললে PyTorch ঠিক shape দুটি বলে দেয়, যা ডিবাগে সবচেয়ে কাজের বার্তা।

Bias কখন বাদ. Convolution-এর পরে সরাসরি BatchNorm থাকলে সাধারণত bias=False দেওয়া হয়: BatchNorm প্রতিটি channel-এর গড় বিয়োগ করে, তাই আগের bias কেটে যায়, আর BatchNorm-এর নিজের β সেই কাজ করে (পর্ব ১৪)। এতে শুধু কয়েকটি parameter বাঁচে, ফল বদলায় না।

৬ · Receptive field — স্তরে স্তরে দৃষ্টি বড় হয়

একটি output pixel মূল ছবির যতটুকু অংশ দেখতে পায়, তাকে বলে তার receptive field. একটি 3 × 3 স্তরে তা 3 × 3. কিন্তু দ্বিতীয় স্তরের একটি pixel প্রথম স্তরের 3 × 3 দেখে, আর তাদের প্রত্যেকে input-এর 3 × 3, পাশাপাশি সরে সরে। নিচের stepper দেখায় মিলিয়ে কত হয়।

Receptive field of two stacked 3x3 layers inputlayer 1layer 2
  1. শেষ স্তরের একটি pixel. Layer 2-এর একটি output (নীল)। প্রশ্ন: input-এর কোন কোন pixel বদলালে এর মান বদলাতে পারে? নিজের স্তরে r = ১.
  2. এক স্তর পেছনে. এটি layer 1-এর একটি 3 × 3 জানালা থেকে হিসাব হয়েছে (কমলা)। Layer 1-এর হিসাবে r = ৩.
  3. কোণের neuron. Layer 1-এর বাঁ-ওপরের pixel নিজে input-এর সারি ০–২, কলাম ০–২ দেখে (সবুজ)।
  4. উল্টো কোণ. Layer 1-এর ডান-নিচের pixel input-এর সারি ২–৪, কলাম ২–৪ দেখে। দুটি জানালা দুই ঘর সরে আছে, কারণ layer 1-এর পাশাপাশি pixel input-এ এক ঘর করে সরে।
  5. মিলিয়ে 5 × 5. নয়টি জানালার মিলন পুরো 5 × 5 input. সূত্রে: শুরু r = ১, প্রতিটি 3 × 3 স্তর (k − 1) = ২ যোগ করে: ১ + ২ + ২ = ৫. তিনটি স্তরে ৭, n টি স্তরে 2n + 1: stride ১-এ বৃদ্ধি রৈখিক, ধীর।
  6. কেন দুটি 3 × 3, একটি 5 × 5 নয়. দৃষ্টি একই, কিন্তু C channel-এ weight দুটি 3 × 3-এ ২ × ৯C² = ১৮C², একটি 5 × 5-এ ২৫C²; C = ৬৪-এ bias-সহ ৭৩,৮৫৬ বনাম ১,০২,৪৬৪. মাঝে বাড়তি একটি ReLU-ও পাওয়া যায়। এই যুক্তি VGG-র (পর্ব ২১) কেন্দ্রে।

Stride বা pooling এলে বৃদ্ধি দ্রুত হয়। প্রতিটি স্তর পার হলে পাশাপাশি দুটি output input-এ কত ঘর দূরে, তাকে বলি jump j. সাধারণ নিয়ম (শুরুতে r = 1, j = 1):

r ← r + (k − 1) · j   ·   j ← j · s

অর্থাৎ receptive field বাড়ে যোগে, কিন্তু যোগের পরিমাণ আগের সব stride-এর গুণফলে গুণ হয়। নিচের ব্লক একটি ছোট stack-এ সূত্রটি চালায়, আর একই সঙ্গে আসল হিসাব দিয়ে মাপে: প্রতিটি স্তরকে একটি 1-D matrix হিসেবে লিখে (output কোন input দেখে, সেখানে ১) সব matrix গুণ করে, মাঝের output-এর সারিতে শূন্য-নয় ঘর গোনে। প্রতিটি অক্ষ আলাদা, তাই 2-D receptive field r × r. শেষে অংশ ১-এর parameter-এর তুলনা।

import numpy as np

def layer_matrix(n_in, k, s):                        # 1-D স্তর: কোন output কোন input দেখে
    n_out = (n_in - k) // s + 1
    M = np.zeros((n_out, n_in))
    for o in range(n_out):
        M[o, o * s:o * s + k] = 1.0
    return M

stack = [("conv3", 3, 1), ("conv3", 3, 1), ("pool2", 2, 2), ("conv3", 3, 1),
         ("conv3", 3, 1), ("pool2", 2, 2), ("conv3", 3, 1), ("conv3", 3, 1)]
n, r, j, T = 200, 1, 1, np.eye(200)
print("layer   r(formula)  r(measured)  jump")
for name, k, s in stack:
    r, j = r + (k - 1) * j, j * s                    # r_new = r + (k-1)·j,  j_new = j·s
    M = layer_matrix(T.shape[0], k, s)
    T = M @ T                                        # input থেকে এই স্তর পর্যন্ত পুরো map
    measured = np.count_nonzero(T[T.shape[0] // 2])  # মাঝের output কতগুলো input দেখে
    print(f"{name:6s} {r:9d} {measured:11d} {j:7d}")
print("2-D receptive field of the last layer:", f"{r} x {r}")

def conv_params(c_in, c_out, k):
    return c_out * c_in * k * k + c_out              # weight + bias

def dense_params(n_in, n_out):
    return n_in * n_out + n_out
H = W = 32
print("conv 3->64, 3x3      :", f"{conv_params(3, 64, 3):,}")
print("dense 32x32x3->32x32x64:", f"{dense_params(H * W * 3, H * W * 64):,}")
print("conv 64->64, 3x3     :", f"{conv_params(64, 64, 3):,}")
print("two 3x3 vs one 5x5 (C=64):", f"{2 * conv_params(64, 64, 3):,}", "vs", f"{conv_params(64, 64, 5):,}")

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

layer   r(formula)  r(measured)  jump
conv3          3           3       1
conv3          5           5       1
pool2          6           6       2
conv3         10          10       2
conv3         14          14       2
pool2         16          16       4
conv3         24          24       4
conv3         32          32       4
2-D receptive field of the last layer: 32 x 32
conv 3->64, 3x3      : 1,792
dense 32x32x3->32x32x64: 201,392,128
conv 64->64, 3x3     : 36,928
two 3x3 vs one 5x5 (C=64): 73,856 vs 102,464

সূত্র আর মাপা মান প্রতিটি স্তরে মিলেছে। প্রথম দুই 3 × 3: ৩, ৫. Pool (k = 2, s = 2) নিজে মাত্র ১ যোগ করে (৬), কিন্তু jump ২ করে দেয়, তাই পরের প্রতিটি 3 × 3 এখন ২ × ২ = ৪ যোগ করে: ১০, ১৪. দ্বিতীয় pool-এর পরে jump ৪, প্রতিটি 3 × 3 যোগ করে ৮: ২৪, ৩২. আটটি ছোট স্তরে 32 × 32: পুরো একটি CIFAR আকারের ছবি। Parameter-এর সারিতে অংশ ১-এর দাবির সংখ্যা: একই 32 × 32 × 64 output-এ convolution ১,৭৯২ আর dense ২০,১৩,৯২,১২৮, প্রায় ১,১২,০০০ গুণ। Weight sharing ছাড়া এর ব্যাখ্যা নেই।

তাত্ত্বিক বনাম কার্যকর: এই r হলো তাত্ত্বিক receptive field: এর বাইরের pixel কোনোভাবেই প্রভাব ফেলতে পারে না। ভেতরের সব pixel সমান প্রভাব ফেলে না; মাঝের pixel অনেক বেশি পথে পৌঁছায়। Luo ও সহকর্মীদের ২০১৬ সালের একটি প্রবন্ধে দেখানো হয়, “কার্যকর” receptive field প্রায় Gaussian আকারের আর তাত্ত্বিকের চেয়ে অনেক ছোট। তাই “r = ছবির আকার” মানেই network পুরো ছবির প্রসঙ্গ ভালোভাবে ব্যবহার করছে, এমন নয়।

৭ · Pooling — max, average আর global average

Pooling একটি জানালার মানগুলোকে একটি সংখ্যায় নামায়, প্রতিটি channel-এ আলাদা। Max pooling নেয় সবচেয়ে বড়টি (“এই এলাকায় নকশাটা কোথাও আছে কি না”), average pooling নেয় গড়। সাধারণত 2 × 2 জানালা, stride ২: প্রতিটি দিক অর্ধেক। কোনো weight নেই, শেখার কিছু নেই। Global average pooling (GAP) পুরো H × W-এর গড় নেয়, প্রতিটি channel থেকে একটি সংখ্যা: (N, C, H, W) → (N, C, 1, 1). সত্যিকারের run:

~/bazarghar-dl$ uv run python pooling.py
max pool 2x2, stride 2:
 tensor([[6., 2.],
        [2., 8.]])
avg pool 2x2, stride 2:
 tensor([[3.5000, 1.0000],
        [1.0000, 5.5000]])
global average pool: 2.75
parameters in MaxPool2d: 0
max pool of a: [9.0, 0.0, 0.0, 0.0]
max pool of b: [9.0, 0.0, 0.0, 0.0]
max pool of c: [0.0, 9.0, 0.0, 0.0]
feature map (2, 256, 7, 7) -> GAP head output (2, 10)
feature map (2, 256, 10, 10) -> GAP head output (2, 10)
feature map (2, 256, 14, 14) -> GAP head output (2, 10)
head params: GAP + Linear = 2570 | Flatten + Linear (7x7) = 125450
Flatten head on 10x10 -> RuntimeError: addmm: mat1 and mat2 shapes cannot be multiplied (2x25600 and 12544x10)

৪ × ৪ input-এর বাঁ-ওপরের জানালা [1, 3; 4, 6]: max ৬, গড় ৩.৫. পুরো input-এর গড় ৪৪/১৬ = ২.৭৫. ছোট সরণ: a-তে ৯ আছে (০, ০)-তে, b-তে এক ঘর তির্যকভাবে সরে (১, ১)-এ; দুটিই একই 2 × 2 জানালায়, তাই max pool-এর ফল হুবহু এক। c-তে ৯ পাশের জানালায় চলে গেছে, ফল বদলেছে। অর্থাৎ max pooling শুধু জানালার ভেতরের সরণ সহ্য করে, সব সরণ নয়। GAP: 7 × 7, 10 × 10, 14 × 14, তিন আকারের feature map-ই (2, 10) দেয়, কারণ গড় নেওয়ার পরে আকার হারিয়ে যায়। Flatten-এর head শুধু 7 × 7-এ চলে আর ১,২৫,৪৫০ parameter নেয়; GAP-এর head ২,৫৭০. 10 × 10-এ flatten head ভেঙে পড়েছে: ২৫,৬০০ আর ১২,৫৪৪ মেলে না। Network in Network (Lin ও সহকর্মী, ২০১৩) GAP-কে classifier head হিসেবে জনপ্রিয় করে; আধুনিক বেশিরভাগ CNN-এর শেষে এটিই থাকে (পর্ব ২২)।

সব একসঙ্গে: একটি ছোট CNN-এর হিসাব. বাজারঘরের 32 × 32 রঙিন পণ্যের ছবি, ১০টি শ্রেণি। প্রতিটি সারি এই পর্বের একটি সূত্র থেকে, হাতে হিসাব করা:

স্তরoutput shapeparameterr, j
input(3, 32, 32)০১, ১
Conv 3 → 32, 3 × 3, p = 1(32, 32, 32)৩২ × ৩ × ৯ + ৩২ = ৮৯৬৩, ১
MaxPool 2 × 2(32, 16, 16)০৪, ২
Conv 32 → 64, 3 × 3, p = 1(64, 16, 16)৬৪ × ৩২ × ৯ + ৬৪ = ১৮,৪৯৬৮, ২
MaxPool 2 × 2(64, 8, 8)০১০, ৪
GAP(64,)০পুরো ছবি
Linear 64 → 10(10,)৬৫০—

মোট ২০,০৪২ parameter, যার ৯০ শতাংশের বেশি দ্বিতীয় convolution-এ। শেষ convolution-এর receptive field মাত্র 10 × 10, তাই ছবির বড় আকার ধরার ভার GAP-এর গড়ের ওপর পড়ছে; আরও স্তর যোগ করলে এই দুর্বলতা কমে। এমন network সত্যিকারের ডেটায় শেখানো পর্ব ২১-এর কাজ।

৮ · Depthwise separable convolution — খরচের হিসাব

একটি সাধারণ k × k convolution একই সঙ্গে দুটি কাজ করে: পাশের pixel মেশায় (স্থান) আর channel মেশায়। Depthwise separable convolution কাজ দুটি আলাদা করে: (১) depthwise: প্রতিটি input channel-এর নিজস্ব একটি k × k kernel, channel মেশে না (PyTorch-এ groups=C_in); (২) pointwise: একটি 1 × 1 convolution, যা channel মেশায়। H × W output-এ গুণ-যোগ (MAC) গুনলে:

সাধারণ: H·W·Cin·Cout·k²  ·  separable: H·W·Cin·(k² + Cout)

অনুপাত = 1/Cout + 1/k²

k = 3 আর Cout = 128-এ ১/১২৮ + ১/৯ ≈ ০.১১৯, অর্থাৎ প্রায় ৮ গুণ কম হিসাব। Parameter-এর অনুপাতও একই (bias বাদে)। সত্যিকারের run, 64 → 128 channel, 56 × 56 feature map, torch.utils.flop_counter দিয়ে FLOP গোনা (১ MAC = ২ FLOP, FLOP), আর CPU-তে ৪টি thread-এ সময় (৭ বার মেপে median; একটি local run, সাধারণ ফল নয়):

~/bazarghar-dl$ uv run python separable.py
Python 3.14.4 | torch 2.15.0+cpu | threads 4
depthwise weight (64, 1, 3, 3) | pointwise weight (128, 64, 1, 1)
standard 3x3         params  73,728 | FLOPs  462,422,016 | CPU 0.98 ms
depthwise separable  params   8,768 | FLOPs   54,992,896 | CPU 0.32 ms
ratio by formula 1/C_out + 1/k^2 = 0.1189
ratio of params = 0.1189

হাতে মিলিয়ে দেখুন: ২ × ৬৪ × ১২৮ × ৯ × ৫৬ × ৫৬ = ৪৬,২৪,২২,০১৬ FLOP, ঠিক যা counter বলেছে; separable-এ ২ × ৬৪ × (৯ + ১২৮) × ৩,১৩৬ = ৫,৪৯,৯২,৮৯৬. Depthwise-এর weight (64, 1, 3, 3): দ্বিতীয় মাত্রা ১, কারণ প্রতিটি filter একটি channel-ই দেখে। কিন্তু সময় ০.১১৯ গুণ হয়নি, এই run-এ প্রায় এক-তৃতীয়াংশ। Depthwise ধাপে প্রতিটি সংখ্যা মেমরি থেকে আনার তুলনায় হিসাব খুব কম, তাই হার্ডওয়্যার পুরো গতিতে চলে না; সময় প্রায়ই মেমরির চলাচলে আটকে থাকে (পর্ব ১২)। শিক্ষা: FLOP কম মানেই সমান অনুপাতে দ্রুত নয়; নিজের হার্ডওয়্যারে মাপুন। MobileNet আর EfficientNet এই স্তরের ওপর দাঁড়ানো, পর্ব ২২-এ বিস্তারিত।

মাঝামাঝি পথ: grouped convolution. groups=g দিলে input আর output channel g ভাগে ভাগ হয়, প্রতিটি ভাগ শুধু নিজের ভাগের input দেখে; weight-এর shape (Cout, Cin/g, k, k), খরচ g গুণ কম। g = 1 সাধারণ convolution, g = Cin depthwise. প্রচলিত বিবরণ অনুযায়ী AlexNet দুটি GPU-তে ভাগ করে চালাতে এমন ভাগ ব্যবহার করেছিল, আর ResNeXt (২০১৭) একে নকশার মূল অংশ বানায়।

আরও গভীরে: Convolution-এর আকার, padding আর transposed convolution-এর সব হিসাব একসঙ্গে পাবেন Dumoulin আর Visin-এর ২০১৬ সালের “A guide to convolution arithmetic for deep learning”-এ। বাস্তবের লাইব্রেরি (যেমন cuDNN, oneDNN) একই convolution-এর জন্য im2col + GEMM, সরাসরি পদ্ধতি, FFT আর Winograd-এর মতো কয়েকটি algorithm রাখে আর আকার দেখে বেছে নেয়; Winograd পদ্ধতি Lavin আর Gray-এর ২০১৬ সালের প্রবন্ধে CNN-এ জনপ্রিয় হয়। Dilated convolution-এর আধুনিক ব্যবহার Yu আর Koltun (২০১৬)-এর segmentation-এর কাজে, যদিও “à trous” কৌশলটি signal processing-এ অনেক পুরোনো।

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

  • 227 × 227 input, 11 × 11 kernel, stride ৪, padding ০: output? (২২৭ − ১১)/৪ + ১ = ৫৫.
  • Conv2d(16, 32, 3)-এর parameter? ৩২ × ১৬ × ৯ + ৩২ = ৪,৬৪০.
  • তিনটি 3 × 3 (stride ১) স্তরের receptive field? ১ + ২ + ২ + ২ = ৭.
  • Input (3, 32, 32), 3 × 3, stride ১, padding ১-এ im2col matrix? (27, 1024): ৩ × ৯ সারি, ৩২ × ৩২ কলাম।
  • Depthwise separable-এর খরচের অনুপাত? ১/Cout + ১/k²; k = 3-এ প্রায় ৮–৯ গুণ কম।

৯ · AI-এর যুগে — convolution-এর কোডে AI-এর ভুল ধরা

CNN-এর কোড AI assistant খুব দ্রুত লেখে, আর ভুলগুলো প্রায় সবসময় shape-এ: কোন অক্ষে channel, flatten-এর পরে কত সংখ্যা, padding কত। এই পর্বের সূত্র আর এক batch চালিয়ে shape ছাপাই যাচাইয়ের হাতিয়ার।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
(H, W, C) ছবি সরাসরি Conv2d-এ দেয়PyTorch (N, C, H, W) চায়; ভুল অক্ষের আকার কাকতালীয়ভাবে মিলে গেলে ভুলটা নীরবে চলে যায়permute(2, 0, 1) আর batch অক্ষ; shape ছাপুন
Flatten-এর পরের Linear-এর input আকার আন্দাজে লেখেPadding বা stride বদলালে সংখ্যা বদলায়, ক্রাশ বা ভুল modelসূত্রে হাতে হিসাব, বা একটি নমুনা batch চালিয়ে দেখা; অথবা GAP
padding="same"-এর সঙ্গে stride ২PyTorch এটি সমর্থন করে না (অংশ ৫-এর ত্রুটি)নিজে p হিসাব করুন
হাতে বানানো kernel-এ scipy.signal.convolve2d আর F.conv2d সমান ধরেপ্রথমটি kernel উল্টায়, দ্বিতীয়টি cross-correlationছোট input-এ দুটির ফল মিলিয়ে দেখুন
Depthwise-এ groups দেয় কিন্তু out_channels groups-এর গুণিতক রাখে নাPyTorch-এ in আর out দুটিই groups দিয়ে বিভাজ্য হতে হয়docs-এর শর্ত পড়ুন; weight-এর shape ছাপুন
“FLOP ৮ গুণ কম, তাই ৮ গুণ দ্রুত” দাবিমেমরির চলাচল আর kernel-এর দক্ষতা সময় ঠিক করেনিজের হার্ডওয়্যারে warm-up-সহ কয়েকবার মাপুন

AI-কে কোডের সঙ্গে প্রতিটি স্তরের shape-এর হিসাব চাইতে বলুন:

আমি PyTorch-এ (version লিখে দিচ্ছি) 64 × 64 রঙিন পণ্যের ছবির জন্য একটি ছোট CNN বানাতে চাই, ১০টি class. ১. প্রতিটি স্তরের পরে tensor-এর shape (N, C, H, W) একটি টেবিলে দাও, output-size সূত্র দেখিয়ে। ২. প্রতিটি স্তরের parameter সংখ্যা হাতে হিসাব করে দাও, আর মোট। ৩. প্রতিটি স্তরের পরে receptive field আর jump হিসাব করো। ৪. Classifier head-এ flatten না global average pooling, কেন, তা বলো। ৫. একটি নমুনা batch দিয়ে shape আর parameter মিলিয়ে দেখার ছোট কোড দাও। ৬. যে API বা যুক্তি নিয়ে নিশ্চিত নও, সেটা অনুমান বলে চিহ্নিত করো।

AI-এর লেখা CNN হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Convolutional network-এর ধারণা কোথা থেকে এল, আর “CNN মস্তিষ্কের দৃষ্টির নকল” দাবিটি কতটা ঠিক?

১৯৫০-এর দশকের শেষে আর ১৯৬০-এর দশকের শুরুতে David Hubel আর Torsten Wiesel বিড়ালের দৃষ্টি-কর্টেক্সে electrode বসিয়ে দেখেন, একেকটি neuron চোখের সামনের একটি ছোট এলাকায় (তার receptive field) নির্দিষ্ট দিকের রেখায় সাড়া দেয়; কিছু “simple” cell রেখার অবস্থানে সংবেদনশীল, কিছু “complex” cell একটু সরলেও সাড়া দেয়। এই কাজের জন্য তাঁরা ১৯৮১ সালে চিকিৎসাবিজ্ঞানে নোবেল পুরস্কারের অংশীদার হন। ১৯৮০ সালে Kunihiko Fukushima-র Neocognitron এই ধারণা থেকে স্তরে স্তরে স্থানীয় নকশা-খোঁজা আর সরণ সহ্য করার একটি network প্রস্তাব করে, তবে তার শেখার নিয়ম backpropagation ছিল না।

১৯৮৯ সালে Yann LeCun ও সহকর্মীরা হাতে লেখা ডাক-কোড চেনার কাজে backpropagation দিয়ে convolution-এর weight শেখান, আর ১৯৯৮ সালের প্রবন্ধে LeNet-5 বর্ণনা করেন (পর্ব ২১)। এই পর্বের তিনটি অনুমান, স্থানীয়তা, weight sharing আর pooling-এর মাধ্যমে ছোট সরণ সহ্য করা, সরাসরি simple আর complex cell-এর ধারণার সঙ্গে মেলে।

তবু “মস্তিষ্কের নকল” বললে বাড়াবাড়ি হয়। মস্তিষ্কে weight sharing-এর মতো হুবহু একই সংখ্যা সবখানে কপি হয় না, শেখা backpropagation দিয়ে হয় এমন প্রমাণ নেই, আর দৃষ্টিতে ওপর থেকে নিচে প্রচুর feedback সংযোগ আছে, যা সাধারণ CNN-এ নেই। কিছু গবেষণা প্রশিক্ষিত CNN-এর ভেতরের সাড়ার সঙ্গে বানরের দৃষ্টি-কর্টেক্সের সাড়ার আংশিক মিল পেয়েছে, কিন্তু মিল মানে একই প্রক্রিয়া নয়। নিরাপদ বক্তব্য হলো: জীববিজ্ঞান একটি ভালো inductive bias-এর অনুপ্রেরণা দিয়েছে, আর সেটি গণিত ও প্রকৌশলে টিকে গেছে কারণ ছবির পরিসংখ্যানের সঙ্গে খাপ খায়।

মূল উপলব্ধি: CNN-এর সাফল্য “মস্তিষ্কের মতো” হওয়ার জন্য নয়, ছবির কাঠামো (স্থানীয়তা আর সরণ) সম্পর্কে সঠিক অনুমান কাঠামোতে বসানোর জন্য।

প্র ০২Convolution translation-equivariant, pooling ছোট সরণে invariant, তাহলে CNN কি ছবি এক pixel সরালে একই উত্তর দেয়?

সবসময় নয়, আর এটি একটি পরিচিত দুর্বলতা। এই পর্বের NumPy ব্লকে stride ১-এর convolution-এ সরণ হুবহু output-এ সরেছে; সেখানে equivariance গাণিতিকভাবে সত্য, কিনারা বাদে। কিন্তু stride ২ বা 2 × 2 pooling এলে network প্রতি দুইটি অবস্থানের একটি রাখে। ছবি এক pixel সরালে নমুনা নেওয়ার বিন্দুগুলো বদলে যায়, আর output শুধু সরে না, বদলায়। Pooling-এর run-এ দেখেছেন: জানালার ভেতরের সরণে ফল এক, কিন্তু জানালার সীমা পেরোলেই আলাদা।

২০১৯ সালে Azulay আর Weiss একটি প্রবন্ধে দেখান, ImageNet-এ প্রশিক্ষিত কয়েকটি পরিচিত CNN-এর অনুমান ছবি মাত্র এক pixel সরালে বা সামান্য বড়-ছোট করলে উল্লেখযোগ্যভাবে বদলে যেতে পারে। তাঁদের ব্যাখ্যা signal processing-এর aliasing: উঁচু ঘনত্বের নকশা নমুনা কমানোর আগে ঝাপসা না করলে ভুলভাবে ধরা পড়ে। একই বছর Richard Zhang “Making Convolutional Networks Shift-Invariant Again” প্রবন্ধে downsampling-এর আগে একটি ছোট blur বসানোর প্রস্তাব দেন, যা এই পর্বের blur kernel-এর আত্মীয়। আরেকটি সূক্ষ্মতা: zero padding কিনারায় একটি সংকেত দেয়, তাই CNN ছবির ভেতরে বস্তুর পরম অবস্থান সম্পর্কে কিছুটা তথ্য শিখতে পারে, এমন ফলাফলও প্রকাশিত হয়েছে।

বাস্তবে সরণ সহ্য করার একটি বড় অংশ আসে data augmentation থেকে (পর্ব ২৪), যেখানে random crop আর shift দিয়ে model-কে সরানো ছবি দেখানো হয়। অর্থাৎ কাঠামো আংশিক নিশ্চয়তা দেয়, ডেটা বাকিটা শেখায়।

মূল উপলব্ধি: Stride ১-এর convolution equivariant; stride আর pooling সেই গুণ আংশিক ভাঙে, তাই “CNN সরণে অপরিবর্তিত” একটি সরলীকরণ, যাচাই না করে মেনে নেওয়ার মতো সত্য নয়।

প্র ০৩im2col মেমরি নষ্ট করে, তবু কেন এটি দীর্ঘদিন convolution চালানোর প্রধান পদ্ধতি ছিল, আর আজ লাইব্রেরি কী করে?

এই পর্বের ব্লকে 16 × 16 × 3 ছবির ৭৬৮টি সংখ্যা im2col-এর পরে (27, 196) = ৫,২৯২টি সংখ্যা হয়েছে, প্রায় সাত গুণ; বড় ছবিতে অনুপাত প্রায় k², 3 × 3-এ ৯ গুণ। তবু পদ্ধতিটি জনপ্রিয়, কারণ matrix গুণ (GEMM) কম্পিউটারের সবচেয়ে বেশি অপটিমাইজ করা কাজগুলোর একটি। BLAS লাইব্রেরি কয়েক দশক ধরে cache, vector instruction আর একাধিক core কাজে লাগাতে উন্নত হয়েছে; একটি সাধারণ চার loop-এর convolution এই দক্ষতার কাছাকাছিও যায় না। মেমরি কিছুটা বেশি লাগলেও সময়ে লাভ বড়।

২০০৬ সালে Chellapilla, Puri আর Simard ডকুমেন্ট প্রসেসিং-এর CNN-এ convolution-কে matrix গুণে রূপান্তর করে CPU আর GPU-তে গতি বাড়ানোর কথা লেখেন, আর পরে Caffe-এর মতো framework im2col + GEMM-কে মূল পথ বানায়। ২০১৪ সালে NVIDIA-র cuDNN লাইব্রেরি প্রকাশিত হয়; এর প্রবন্ধে বর্ণনা আছে কীভাবে পুরো im2col matrix মেমরিতে না বানিয়ে টুকরো টুকরো করে গুণের সময়ই তৈরি করা যায়, যাতে মেমরির সমস্যা কমে।

আজকের লাইব্রেরি একাধিক algorithm রাখে: সরাসরি পদ্ধতি, im2col-ভিত্তিক GEMM, FFT (বড় kernel-এ লাভ), আর Winograd (ছোট 3 × 3 kernel-এ গুণের সংখ্যা কমায়)। PyTorch-এ torch.backends.cudnn.benchmark = True দিলে প্রথম কয়েকটি ধাপে আকার অনুযায়ী দ্রুততম algorithm মেপে বেছে নেওয়া হয় (পর্ব ১২-এর গতির আলোচনার সঙ্গে মেলে)। Channels-last মেমরি বিন্যাসও কিছু হার্ডওয়্যারে convolution দ্রুত করে।

মূল উপলব্ধি: গণিতে convolution একটি; বাস্তবে এটি চালানোর বহু পথ, আর সেরা পথ নির্ভর করে আকার, হার্ডওয়্যার আর মেমরির ওপর। im2col দেখায়, convolution আসলে একটি কাঠামোবদ্ধ matrix গুণ।

প্র ০৪বড় receptive field পেতে তিনটি পথ: গভীরতা, stride/pooling আর dilation. প্রতিটির দাম কী?

গভীরতা: stride ১-এর প্রতিটি 3 × 3 স্তর মাত্র ২ যোগ করে; 224 × 224 ছবি পুরো দেখতে শতাধিক স্তর লাগত। প্রতিটি স্তর parameter, হিসাব আর মেমরি যোগ করে, আর গভীর network শেখানো কঠিন (পর্ব ১৭)। লাভ হলো প্রতিটি স্তরে একটি nonlinearity, তাই আরও জটিল function.

Stride/pooling: অংশ ৬-এর ব্লকে দুটি pool-এর পরে প্রতিটি 3 × 3 স্তর ৮ ঘর যোগ করেছে। সঙ্গে feature map ছোট হয় বলে হিসাবও কমে, তাই classification-এর CNN প্রায় সবাই এই পথ নেয়: ধাপে ধাপে আকার অর্ধেক, channel দ্বিগুণ। দাম হলো সূক্ষ্ম অবস্থানের তথ্য হারানো আর প্রশ্ন ২-এর aliasing. Segmentation-এ (পর্ব ২৫) প্রতিটি pixel-এর লেবেল লাগে, সেখানে এই হারানো তথ্য U-Net-এর মতো skip connection দিয়ে ফেরাতে হয়।

Dilation: আকার না কমিয়ে, parameter না বাড়িয়ে receptive field বাড়ায়; d = 1, 2, 4, 8 ক্রমে স্তর সাজালে receptive field সূচকীয়ভাবে বাড়ে। Yu আর Koltun ২০১৬ সালে segmentation-এ এই পথ জনপ্রিয় করেন, আর DeepLab পরিবারেও এর ব্যবহার আছে; WaveNet-এর মতো audio model-এও 1-D dilated convolution ব্যবহৃত হয়েছে (পর্ব ৩১)। দাম হলো “gridding”: একই dilation বারবার দিলে কিছু input pixel কখনো দেখা হয় না, ফলে ছকের মতো নকশা আসে; তাই dilation-এর হার মিশিয়ে নকশা করা হয়।

আরেকটি পথ হলো বড় kernel; পর্ব ২২-এ ConvNeXt-এর 7 × 7 depthwise kernel দেখবেন। আর Transformer-এ attention প্রথম স্তর থেকেই পুরো ছবি দেখতে পারে (পর্ব ৩৮), তবে তার দাম ভিন্ন।

মূল উপলব্ধি: Receptive field বাড়ানোর প্রতিটি পথ কিছু একটা কেনে কিছু একটার বিনিময়ে: গভীরতা কেনে প্রকাশক্ষমতা, stride কেনে গতি, dilation কেনে রেজোলিউশন; কাজের ধরন ঠিক করে কোনটি।

প্র ০৫Depthwise separable convolution-এ FLOP প্রায় আট গুণ কমল, কিন্তু সময় কমল তিন গুণের মতো। কেন, আর মোবাইল model নকশায় এর শিক্ষা কী?

FLOP শুধু গণনা গোনে, কিন্তু সময় নির্ভর করে দুটি জিনিসে: কত হিসাব আর কত ডেটা মেমরি থেকে আনা-নেওয়া। একটি byte আনার বিপরীতে কতগুলো হিসাব হয়, তাকে বলে arithmetic intensity. সাধারণ 3 × 3 convolution-এ প্রতিটি input সংখ্যা Cout·৯ বার কাজে লাগে, তাই intensity বেশি, হার্ডওয়্যার পুরো গতিতে চলে। Depthwise ধাপে প্রতিটি input সংখ্যা মাত্র ৯ বার কাজে লাগে; হার্ডওয়্যার বেশিরভাগ সময় ডেটার জন্য অপেক্ষা করে। এই পর্বের run-এ FLOP ০.১১৯ গুণ, সময় প্রায় ০.৩৩ গুণ; অন্য মেশিনে বা GPU-তে অনুপাত আলাদা হবে।

ধারণাটির ইতিহাস: Laurent Sifre-এর ২০১৪ সালের থিসিসে এর বিশ্লেষণ পাওয়া যায় বলে উল্লেখ করা হয়; ২০১৭ সালে François Chollet-এর Xception আর Google-এর Howard ও সহকর্মীদের MobileNet একে প্রধান building block বানায়। MobileNet প্রবন্ধে লেখকেরা নিজেরাই হিসাবের বেশিরভাগ 1 × 1 convolution-এ রেখেছিলেন, কারণ সেটি GEMM দিয়ে দক্ষভাবে চলে। পরে ShuffleNet V2 (২০১৮) প্রবন্ধ স্পষ্টভাবে বলে, শুধু FLOP দেখে নকশা করা বিভ্রান্তিকর; আসল গতি মেপে দেখতে হয়।

বাজারঘরের মতো একটি দোকান যদি কম দামি ফোনেও পণ্য চেনার model চালাতে চায়, এই শিক্ষা সরাসরি কাজের: লক্ষ্য ডিভাইসে latency মাপুন, শুধু parameter বা FLOP নয়। Quantization আর runtime-এর পছন্দও (পরের মডিউলগুলোতে) সময় বদলায়।

মূল উপলব্ধি: FLOP একটি সূচক, গতি নয়। মেমরির চলাচল, kernel-এর দক্ষতা আর হার্ডওয়্যার মিলে সময় ঠিক করে; দাবি নয়, measurement দিয়ে architecture বাছুন।

অনুশীলন

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

  1. পূর্ণ numerical: (ক) 64 × 64 input, 5 × 5 kernel, stride ২, padding ২: output-এর আকার? (খ) Conv2d(32, 64, 3)-এর parameter কত, আর 32 × 32 × 32 input থেকে 32 × 32 × 64 output বানাতে একটি dense layer-এর কত? (গ) conv3 → conv3 → pool2 (s = 2) → conv3 stack-এর প্রতিটি স্তরের পরে receptive field. (ঘ) Input (3, 32, 32), 3 × 3, stride ১, padding ১-এ im2col matrix-এর shape.

    (ক) ⌊(৬৪ + ৪ − ৫)/২⌋ + ১ = ⌊৩১.৫⌋ + ১ = ৩২, অর্থাৎ ঠিক অর্ধেক। (খ) ৬৪ × ৩২ × ৯ + ৬৪ = ১৮,৪৯৬; dense: input ৩২,৭৬৮, output ৬৫,৫৩৬, তাই ৩২,৭৬৮ × ৬৫,৫৩৬ + ৬৫,৫৩৬ = ২,১৪,৭৫,৪৯,১৮৪, প্রায় ২১৫ কোটি। (গ) r: ৩ → ৫ → ৬ (jump ২ হয়) → ৬ + ২ × ২ = ১০. (ঘ) সারি ৩ × ৯ = ২৭, কলাম ৩২ × ৩২ = ১,০২৪ (padding ১-এ আকার একই): (27, 1024).

  2. নকশা ও সমালোচনা: বাজারঘরের 224 × 224 পণ্যের ছবির ১০টি শ্রেণির জন্য একজন সহকর্মী লিখেছেন: দুটি Conv2d(…, 64, 3, padding=0) স্তর (stride ১, pooling নেই), তারপর Flatten আর Linear(64 * 220 * 220, 10). কী সমস্যা, আর আপনি কীভাবে বদলাতেন?

    (১) Receptive field মাত্র 5 × 5: শেষ স্তরের প্রতিটি pixel ২২৪-এর মধ্যে ৫ ঘর দেখে; পুরো প্যাকেটের আকার বা লেবেলের নকশা কোনো একক feature ধরতে পারে না, সবকিছু শেষ Linear-কে করতে হয়। (২) Linear-এ ৩,০৯,৭৬,০১০ parameter (৬৪ × ২২০ × ২২০ × ১০ + ১০), প্রায় সব parameter এক স্তরে; overfitting-এর ঝুঁকি বড়। (৩) Flatten head শুধু 224 × 224-এ চলে, অন্য আকারে ভাঙে (অংশ ৭)। (৪) কোনো downsampling নেই, তাই প্রতিটি স্তরে 220 × 220-এর বড় feature map: হিসাব আর মেমরি বেশি। বদল: padding ১-এর 3 × 3 স্তরগুলো ধাপে ধাপে, প্রতি ধাপের পরে stride ২ বা max pool (আকার অর্ধেক, channel দ্বিগুণ: 32 → 64 → 128 → 256), ৪–৫টি ধাপে receptive field ছবির আকারের কাছাকাছি; শেষে GAP + Linear(256, 10). তারপর shape আর parameter একটি নমুনা batch-এ মিলিয়ে দেখা। ডেটা কম হলে transfer learning (পর্ব ২৩) আরও ভালো পথ।

  3. আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে?
    import torch
    import torch.nn.functional as F
    x = torch.arange(16.).reshape(1, 1, 4, 4)
    w = torch.ones(1, 1, 2, 2)
    print(F.conv2d(x, w, stride=2))
    print(F.conv2d(x, w, padding=1).shape)
    print(F.max_pool2d(x, 2).flatten().tolist())

    সত্যিকারের run-এর আউটপুট:

    tensor([[[[10., 18.],
              [42., 50.]]]])
    torch.Size([1, 1, 5, 5])
    [5.0, 7.0, 13.0, 15.0]

    x-এর সারিগুলো [0..3], [4..7], [8..11], [12..15]. All-ones 2 × 2 kernel, stride ২: প্রতিটি 2 × 2 খণ্ডের যোগ। বাঁ-ওপরে ০ + ১ + ৪ + ৫ = ১০, ডান-ওপরে ২ + ৩ + ৬ + ৭ = ১৮, নিচে ৪২ আর ৫০. Padding ১, stride ১: (৪ + ২ − ২)/১ + ১ = ৫, তাই (1, 1, 5, 5); জোড় kernel-এ সমান padding আকার রাখে না, বাড়ায়। Max pool প্রতিটি খণ্ডের ডান-নিচের ঘর নেয়, কারণ মান বাঁ থেকে ডানে আর ওপর থেকে নিচে বাড়ে: ৫, ৭, ১৩, ১৫.

  4. কোড লিখুন: অংশ ৩-এর im2col-এ stride s আর padding p যোগ করুন, আর একটি conv2d_im2col(x, W, b, s, p) লিখুন। F.conv2d-এর সঙ্গে মিলিয়ে দেখুন।
    import numpy as np
    
    def im2col(x, k, s=1, p=0):
        x = np.pad(x, ((0, 0), (p, p), (p, p)))        # channel অক্ষে padding নয়
        C, H, W = x.shape
        Ho, Wo = (H - k) // s + 1, (W - k) // s + 1
        cols = np.empty((C * k * k, Ho * Wo))
        r = 0
        for c in range(C):
            for di in range(k):
                for dj in range(k):
                    cols[r] = x[c, di:di + s * (Ho - 1) + 1:s,
                                   dj:dj + s * (Wo - 1) + 1:s].ravel()
                    r += 1
        return cols, Ho, Wo
    
    def conv2d_im2col(x, Wt, b, s=1, p=0):
        O = Wt.shape[0]
        cols, Ho, Wo = im2col(x, Wt.shape[2], s, p)
        return (Wt.reshape(O, -1) @ cols + b[:, None]).reshape(O, Ho, Wo)

    মূল কৌশল: kernel-এর একটি ঘর (di, dj)-এর জন্য সব জানালার মান একটি strided slice: শুরু di, ধাপ s, মোট Ho টি। সারির ক্রম (c, di, dj) রাখলে Wt.reshape(O, -1)-এর সঙ্গে মেলে। লেখার সময়ে একটি সত্যিকারের run-এ (3, 11, 11) input আর (5, 3, 3, 3) weight-এ (s, p) = (1, 0), (2, 1), (3, 2) তিনটিতেই F.conv2d-এর সঙ্গে np.allclose True এসেছে, shape যথাক্রমে (5, 9, 9), (5, 6, 6), (5, 5, 5).

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

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

ক্রমএকটি ছোট image classifier CNN-এর forward pass সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Input ছবি (N, 3, H, W) আকারে
  2. 3 × 3 convolution (padding ১) আর ReLU
  3. 2 × 2 max pooling, আকার অর্ধেক
  4. আরেকটি 3 × 3 convolution, বেশি channel, আর ReLU
  5. Global average pooling: (N, C, H′, W′) থেকে (N, C)
  6. Linear স্তর থেকে প্রতিটি class-এর logit

32 × 32 input-এ 5 × 5 kernel, stride ১, padding ০ দিলে output-এর আকার কত?

  • 32 × 32
  • 29 × 29
  • 28 × 28
  • 27 × 27

PyTorch-এর nn.Conv2d(16, 32, kernel_size=3) স্তরে (bias-সহ) মোট কতটি parameter?

  • ৩২০
  • ৪,৬৪০
  • ৪,৬০৮
  • ছবির আকার না জানলে বলা যায় না

Stride ১-এর তিনটি 3 × 3 convolution স্তর পরপর থাকলে শেষ স্তরের একটি pixel-এর receptive field কত?

  • 3 × 3
  • 5 × 5
  • 9 × 9
  • 7 × 7

Input-এর shape (3, 8, 8), kernel 3 × 3, stride ১, padding ০. im2col-এ বানানো matrix-এর shape কী?

  • (27, 36)
  • (9, 36)
  • (27, 64)
  • (64, 27)

k = 3 আর Cout = 256-এ depthwise separable convolution সাধারণ convolution-এর তুলনায় প্রায় কত ভাগ গুণ-যোগ করে?

  • ঠিক ১/৯
  • প্রায় ১/২৫৬
  • প্রায় ০.১১৫ গুণ
  • প্রায় সমান

Stride ১, padding-সহ একটি convolution স্তরে input ছবিটি ৩ pixel ডানে সরালে output-এ কী হয় (কিনারা বাদে)?

  • Output হুবহু আগের মতো থাকে
  • Output-ও ৩ pixel ডানে সরে যায়
  • Output সম্পূর্ণ এলোমেলোভাবে বদলায়
  • Output-এর আকার ৩ কমে যায়

শব্দকোষ

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

মনে রাখুন

  1. কেন convolution: স্থানীয় সংযোগ, weight sharing আর translation equivariance; parameter ছবির আকারের ওপর নির্ভর করে না।
  2. হিসাব: প্রতিটি output = জানালা আর kernel-এর dot product + bias; লাইব্রেরি আসলে cross-correlation চালায়। im2col সব জানালা কলামে সাজিয়ে একটি matrix গুণ বানায়।
  3. আকার: ⌊(n + 2p − d(k − 1) − 1)/s⌋ + 1; stride ১-এ “same”-এর জন্য p = (k − 1)/2.
  4. Shape আর parameter: weight (Cout, Cin, kH, kW), parameter Cout·Cin·k² + Cout. Receptive field: r ← r + (k − 1)·j, j ← j·s.
  5. Pooling আর separable: pooling-এ weight নেই, GAP যেকোনো আকার নেয়; depthwise separable-এর খরচ ১/Cout + ১/k² গুণ, কিন্তু গতি নিজে মাপুন।

পরের পর্বে: পর্ব ২১ (CNN-এর স্থাপত্য — LeNet থেকে AlexNet, VGG আর Inception): এই পর্বের ইট দিয়ে ইতিহাসের বিখ্যাত CNN-গুলো কীভাবে গড়া, প্রতিটির shape আর parameter-এর হিসাব, আর ছোট LeNet ও VGG-ধাঁচের network সত্যিকারের ডেটায় শেখানো।

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

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