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

NumPy-তে multilayer perceptron — forward pass আর activation function

A Multilayer Perceptron in NumPy — Batched Forward Pass, Tensor Shapes, Activation Functions, Stable Softmax and Linear Regions

Taught in Bangla, this lesson builds a multilayer perceptron forward pass in in-page Python with NumPy. It writes every layer as a batched matrix product with broadcasting, tracks tensor shapes, counts parameters (243 for a 4-16-8-3 network) and multiply-adds, compares sigmoid, tanh, ReLU, Leaky ReLU, GELU and SiLU by their slopes, makes softmax stable with max subtraction and log-sum-exp, adds temperature, and counts the linear pieces a ReLU network creates.

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

  • পুরো batch-এর জন্য দুই hidden layer-এর MLP NumPy-তে vectorised আকারে লেখা, আর প্রতিটি tensor-এর shape আগে থেকে বলতে পারা
  • যেকোনো MLP-এর parameter আর গুণ-যোগের সংখ্যা খাতায় গোনা
  • Sigmoid, tanh, ReLU, Leaky ReLU, GELU আর SiLU-এর সূত্র, derivative, saturation আর কোথায় কোনটি ব্যবহার হয়
  • Softmax-এর overflow কোথা থেকে আসে, max বিয়োগের কৌশল আর log-sum-exp কেন কাজ করে
  • ReLU network কীভাবে input-কে ভাঁজ করে linear region বানায়, আর গভীরতা ও প্রস্থে সেই সংখ্যা কীভাবে বদলায়

AI-এর ভিত্তি কোর্সের পর্ব ৬৮-এ আপনি একটি neuron, perceptron, হাতে বসানো XOR network, চারটি activation আর এক hidden layer-এর MLP-এর forward pass দেখেছেন। এই পর্বের প্রশ্ন আরও এক ধাপ গভীরে: বাস্তব network-এ পুরো batch একসঙ্গে কীভাবে চলে, প্রতিটি tensor-এর shape কী, আজকের activation-গুলো কেন আলাদা, softmax কোথায় ভেঙে পড়ে, আর বাড়তি layer আসলে কী যোগ করে? Training আর gradient এই পর্বে নেই; সেটি পরের পর্ব ০৪ (Backpropagation হাতে-কলমে — chain rule থেকে পুরো gradient)-এর কাজ। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python, NumPy, SciPy আর matplotlib); প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে।

১ · এক neuron থেকে পুরো batch — কেন সবকিছু matrix গুণ

বাজারঘরের গুদামে সকালে ছয়টি অর্ডার এসেছে। হিসাবরক্ষক প্রতিটি অর্ডারের জন্য আলাদা কাগজ না নিয়ে একটি খাতায় ছয় সারির একটি ছক বানান: প্রতি সারিতে একটি অর্ডার, প্রতি কলামে একটি তথ্য (দূরত্ব, দাম, আগের অর্ডার, রাতের অর্ডার কি না)। তারপর একটি নিয়ম পুরো ছকে একবারে খাটান। Neural network-এ এই ছকই input matrix X, আর “একবারে খাটানো” নিয়মটি matrix গুণ।

একটি neuron-এর হিসাব z = w1x1 + … + wdxd + b, তারপর activation. Hidden layer-এ এমন h টি neuron পাশাপাশি; তাদের weight-গুলো কলাম হিসেবে সাজালে পাওয়া যায় matrix W, যার shape (d, h). একটি উদাহরণ x (দৈর্ঘ্য d)-এর জন্য পুরো layer: z = x @ W + b, দৈর্ঘ্য h. এবার B টি উদাহরণ সারি হিসেবে একটি matrix X (B, d)-এ রাখলে:

Z = XW + b,   (B × d)(d × h) → (B × h),   H = f(Z)

এখানে B হলো batch size, d হলো feature-এর সংখ্যা, h হলো layer-এর unit-এর সংখ্যা, আর f হলো activation function, যা ঘরে ঘরে আলাদাভাবে খাটে। b-এর shape (h,); NumPy একে (1, h) ধরে B সারিতে ছড়িয়ে দেয়, যা AI-এর ভিত্তি কোর্সের পর্ব ১৫-এর broadcasting. এক লাইনের এই রূপকে বলে vectorised হিসাব। এর তিনটি সুবিধা:

  • গতি: Python-এর loop-এর বদলে একটি matrix গুণ, যা ভেতরে BLAS-এর মতো অপ্টিমাইজ করা library-তে বা GPU-তে চলে।
  • একই ফল: সারিগুলো একে অন্যকে ছোঁয় না। সারি i-এর output শুধু X-এর সারি i-এর ওপর নির্ভর করে, তাই একটি একটি করে হিসাব করলেও ফল হুবহু এক (অংশ ৫-এর কোডে মিলিয়ে দেখবেন)।
  • সব framework-এর ভাষা: PyTorch-এর মতো library-তে input-এর প্রথম মাত্রা প্রায় সবসময় batch; পর্ব ০৭ (PyTorch tensor — shape, dtype, device আর broadcasting)-এ একই ধারা দেখবেন।
দুই রকম লেখার রীতি

গণিতের বই প্রায়ই কলাম vector নেয়: z = Wx + b, যেখানে W-এর shape (h, d). কোডে উদাহরণ থাকে সারিতে, তাই লিখি X @ W, W (d, h). দুটি একই জিনিস, শুধু transpose করা। PyTorch-এর nn.Linear(d, h) weight রাখে (h, d) shape-এ, অর্থাৎ (out, in), আর হিসাব করে X @ W.T + b. অন্যের weight বা AI-এর লেখা কোড নেওয়ার সময় আগে দেখে নিন কোন রীতি, নইলে বর্গাকার layer-এ (d = h) ভুল transpose কোনো error ছাড়াই ভুল ফল দেবে।

একটি hidden layer থাকলে network-কে বলে MLP (multilayer perceptron); নামটি ঐতিহাসিক, কারণ এর neuron-গুলো perceptron-এর step function নয়, মসৃণ বা ভাঙা-রেখার activation ব্যবহার করে। Input থেকে output পর্যন্ত একবার হিসাবকে বলে forward pass. এই পর্বে network-এ দুটি hidden layer, কারণ দুটি থাকলেই স্তর থেকে স্তরে shape-এর ধারা আর গভীরতার প্রশ্ন দুটোই স্পষ্ট হয়।

২ · প্রতিটি tensor-এর shape — দুই hidden layer-এর network

বাজারঘরের কাজ: একটি অর্ডারের চারটি feature দেখে বলা, ডেলিভারি আগে হবে, সময়মতো হবে, না দেরিতে (তিনটি শ্রেণি)। Network: 4 → 16 → 8 → 3, অর্থাৎ d = ৪, প্রথম hidden layer-এ ১৬, দ্বিতীয়তে ৮, output-এ k = ৩ unit. নিচের ছবিতে প্রতিটি matrix আকার অনুযায়ী আঁকা: প্রতিটি ছোট ঘর একটি সংখ্যা।

4 → 16 → 8 → 3 MLP: তিনটি matrix গুণ, মাপ অনুযায়ী আঁকা

B = 6 orders · কমলা দাগ: যে দুটি মাপ মিলতেই হবে · নীল সারি: একটি অর্ডার

X @ W1 → Z1, H1 @ W2 → Z2, H2 @ W3 → Z3 → softmax → P, with shapes Layer 1 X (6,4) @ W1 (4,16) = + b1 (16,) Z1, H1 (6,16) Layer 2 H1 (6,16) @ W2 (16,8) = Z2, H2 (6,8) Layer 3 H2 (6,8) @ W3 (8,3) = Z3 (6,3) softmax → P (6,3) P-এর প্রতি সারির যোগফল ১

ছবির নিয়ম একটাই: (B, a) @ (a, c) → (B, c); বাঁয়ের কলাম-সংখ্যা আর ডানের সারি-সংখ্যা (কমলা দাগ) সমান হতেই হবে, আর ফলে থাকে বাঁয়ের সারি আর ডানের কলাম। লক্ষ করুন, B কোনো weight-এর shape-এ নেই: একই weight ৬টি বা ৬০০০টি অর্ডারে চলে। নিচের stepper প্রতিটি ধাপে shape আর তার কারণ দেখায়।

tensor-এর shapeB = 6

            
  1. Input. X-এর shape (6, 4): ৬টি অর্ডার সারিতে, ৪টি feature কলামে। কলামগুলোর একক আলাদা (কিলোমিটার, টাকা), তাই আগে প্রতিটি কলাম standardise করা হয়, যাতে বড় এককের feature একাই হিসাব দখল না করে।
  2. প্রথম layer. (6, 4) @ (4, 16): ভেতরের ৪ মেলে, ফল (6, 16). b1-এর shape (16,), broadcasting-এ ছয় সারিতেই একই bias যোগ হয়। প্রতিটি অর্ডার এখন ১৬টি নতুন সংখ্যায় বর্ণিত।
  3. Activation. ReLU ঘরে ঘরে খাটে, তাই H1-এর shape Z1-এর সমান, (6, 16). Activation কখনো shape বদলায় না; বদলায় শুধু মান।
  4. দ্বিতীয় layer. (6, 16) @ (16, 8) → (6, 8). আগের layer-এর unit-সংখ্যা ১৬ এখন W2-এর সারি-সংখ্যা। একটি layer-এর output-ই পরের layer-এর input.
  5. Output. (6, 8) @ (8, 3) → (6, 3), কাঁচা স্কোর বা logits. Softmax প্রতিটি সারিকে তিনটি সম্ভাবনায় বদলায়, তাই axis = 1 বরাবর হিসাব, আর প্রতিটি সারির যোগফল ১.
  6. গোনা। প্রতিটি layer-এ nin × nout টি weight আর nout টি bias, মোট (nin + 1) × nout: ৮০ + ১৩৬ + ২৭ = ২৪৩. B এখানে কোথাও নেই।

গুণ-যোগের সংখ্যা আর memory

Parameter-এর পাশাপাশি খরচও গোনা যায়। (B, a) @ (a, c) গুণে প্রতিটি ফল-ঘরে a টি গুণ আর প্রায় a টি যোগ, তাই মোট প্রায় 2·B·a·c টি floating-point কাজ। পুরো network-এ forward pass ≈ 2·B·∑ ninnout; আমাদের network-এ B = ৬ হলে 2 × 6 × (64 + 128 + 24) = ২,৫৯২টি। Activation আর bias-এর খরচ এর তুলনায় ছোট। আরেকটি খরচ memory: training-এর সময় backward pass-এর জন্য Z1, H1, Z2, H2… সব রেখে দিতে হয়, যার মাপ B-এর সঙ্গে বাড়ে, আর weight-এর মাপ B-এর ওপর নির্ভর করে না। এই হিসাব পর্ব ০৪-এ আবার দরকার হবে, যখন backward-এর খরচ forward-এর সঙ্গে তুলনা করা হবে।

Parameter গোনার দুটি ব্যতিক্রম মনে রাখুন। প্রথম, normalization layer-এর ঠিক আগের linear layer-এ অনেক সময় bias রাখা হয় না (PyTorch-এ bias=False), কারণ normalization নিজেই একটি সরণ শেখে; তখন সেই layer-এ parameter শুধু nin × nout. দ্বিতীয়, কিছু model একই weight দুই জায়গায় ভাগ করে ব্যবহার করে (যেমন language model-এর input embedding আর output layer); তখন মোট parameter আলাদা আলাদা layer-এর যোগফলের চেয়ে কম। Model-এর “মাপ” পড়ার সময় তাই দেখে নিন কী গোনা হয়েছে।

৩ · Activation function গভীরে — sigmoid আর tanh-এর saturation

AI-এর ভিত্তি কোর্সের পর্ব ৬৮ দেখিয়েছে, activation না থাকলে দুটি linear layer মিলে একটি linear layer: X W1 W2 = X (W1W2). তাই non-linear f-ই গভীরতাকে অর্থ দেয়। কিন্তু কোন f? উত্তর আসে দুটি প্রশ্ন থেকে: f-এর মান কোন পরিসরে থাকে, আর f-এর derivative কোথায় কত বড়? দ্বিতীয়টি জরুরি, কারণ training-এ gradient প্রতিটি layer পেরোনোর সময় f′(z) দিয়ে গুণ হয় (পর্ব ০৪-এ এর পুরো হিসাব)।

পানির ট্যাংকের কল ভাবুন। ট্যাংক যখন অর্ধেক ভরা, কল একটু ঘোরালেই পানির উচ্চতা চোখে পড়ার মতো বদলায়। কিন্তু ট্যাংক উপচে পড়ছে বা একেবারে খালি, তখন কল যত ঘোরান, উচ্চতা প্রায় বদলায় না। Sigmoid-এর দুই প্রান্ত ঠিক এমন: input অনেক বদলালেও output প্রায় স্থির, তাই ঢাল প্রায় শূন্য। একে বলে saturation.

Sigmoid: σ(z) = 1 / (1 + e−z), মান ০ থেকে ১, derivative σ′(z) = σ(z)(1 − σ(z)). সর্বোচ্চ ঢাল z = ০-তে, মাত্র ০.২৫. z = ৩-এ ঢাল ০.০৪৫, z = ৬-এ ০.০০২৫. অর্থাৎ যে unit-এর weighted sum বড় হয়ে গেছে, সে প্রায় শেখা বন্ধ করে। আরেকটি সমস্যা: sigmoid-এর output সবসময় ধনাত্মক, তাই পরের layer-এর একটি unit-এর সব weight-এর gradient-এর চিহ্ন এক হয়, আর weight একসঙ্গে আঁকাবাঁকা পথে চলে।

tanh: tanh(z) = (ez − e−z) / (ez + e−z) = 2σ(2z) − 1, মান −১ থেকে ১, শূন্যকে কেন্দ্র করে। Derivative 1 − tanh²(z), সর্বোচ্চ ১. তাই hidden layer-এ tanh sigmoid-এর চেয়ে ভালো, কিন্তু saturation এখানে আরও দ্রুত: z = ৩-এ ঢাল ০.০০৯৯, sigmoid-এর চেয়েও কম। LSTM-এর মতো recurrent network-এ (পর্ব ২৯) tanh আর sigmoid এখনো gate-এর ভেতরে কাজ করে, কারণ সেখানে ০ থেকে ১ বা −১ থেকে ১-এর সীমাটাই দরকার।

দশটি sigmoid layer পেরোলে সবচেয়ে ভালো অবস্থাতেও গুণনীয়ক ০.২৫১০ ≈ ৯.৫ × ১০−৭, যাকে বলে vanishing gradient. এর প্রতিকার (ভালো initialization, normalization, residual connection) পর্ব ১৪ আর পর্ব ১৭-এর বিষয়; এখানে শুধু মনে রাখুন, activation-এর ঢালই সমস্যার মূল।

৪ · ReLU পরিবার আর মসৃণ বিকল্প — Leaky ReLU, GELU আর SiLU

ReLU = max(0, z). ধনাত্মক দিকে ঢাল ঠিক ১, তাই যত স্তরই হোক, সক্রিয় পথে gradient ছোট হয় না; হিসাবও সবচেয়ে সস্তা, একটি তুলনা মাত্র। z = ০-তে derivative সংজ্ঞায়িত নয়; library-গুলো সাধারণত সেখানে ০ ধরে নেয়, আর বাস্তবে z ঠিক ০ হওয়া বিরল। ২০১০ সালের আশেপাশে Nair ও Hinton, আর ২০১১ সালে Glorot, Bordes ও Bengio-র কাজের পর ReLU জনপ্রিয় হয়, আর ২০১২ সালের AlexNet ReLU ব্যবহার করে দ্রুত training-এর কথা জানায়।

ReLU-র দুর্বলতা হলো dying ReLU: যদি কোনো unit-এর z সব input-এ ঋণাত্মক হয় (ধরুন একটি বড় learning-rate ধাপ bias-কে অনেক নিচে নামিয়ে দিল), তাহলে তার output সবসময় ০, ঢালও ০, আর gradient কখনো তাকে ফিরিয়ে আনে না। সে network-এ থেকেও কোনো কাজ করে না।

  • Leaky ReLU (Maas, Hannun ও Ng, ২০১৩): z < ০ হলে αz, যেখানে α ছোট (PyTorch-এর nn.LeakyReLU-এ ডিফল্ট 0.01). ঋণাত্মক দিকেও সামান্য ঢাল থাকে, তাই কোনো unit পুরোপুরি মরে না। α-কে শেখার parameter বানালে তাকে বলে PReLU (He ও সহকর্মী, ২০১৫)।
  • GELU (Hendrycks ও Gimpel, ২০১৬): GELU(z) = z·Φ(z), যেখানে Φ হলো standard normal distribution-এর CDF (AI-এর ভিত্তি কোর্সের পর্ব ৩৬)। ভাবনাটি: input-কে “কতটা রাখব” তা ঠিক হয় z কত বড় তার সম্ভাবনা দিয়ে; বড় ধনাত্মক z প্রায় পুরোটা যায়, বড় ঋণাত্মক z প্রায় ০. BERT আর GPT-2-সহ অনেক Transformer-এ এটি ব্যবহার হয়েছে। দ্রুত হিসাবের জন্য একটি tanh-ভিত্তিক আসন্ন রূপও চলে: 0.5z(1 + tanh(√(2/π)(z + 0.044715z³))); PyTorch-এ nn.GELU(approximate="tanh").
  • SiLU / Swish: SiLU(z) = z·σ(z). Elfwing ও সহকর্মীরা ২০১৭ সালে একে SiLU নামে ব্যবহার করেন, আর একই বছর Ramachandran, Zoph ও Le স্বয়ংক্রিয় অনুসন্ধানে z·σ(βz) খুঁজে পেয়ে Swish নাম দেন; β = ১ হলে দুটি এক। আজকের অনেক বড় language model-এর feed-forward অংশে SiLU-র gate-যুক্ত রূপ (SwiGLU) দেখা যায়, যা পর্ব ৩৫ (Transformer block)-এ আসবে।
Activationf(z)f′(z)f′(0)বৈশিষ্ট্য
sigmoid1/(1 + e−z)σ(1 − σ)০.২৫০ থেকে ১; দুই দিকে saturate
tanh2σ(2z) − 11 − tanh²১−১ থেকে ১, শূন্য-কেন্দ্রিক; দুই দিকে saturate
ReLUmax(0, z)z > 0 হলে 1, নইলে 0সংজ্ঞায়িত নয়সস্তা, ধনাত্মক দিকে saturate করে না; মরে যেতে পারে
Leaky ReLUz > 0 হলে z, নইলে αz1 বা αসংজ্ঞায়িত নয়ঋণাত্মক দিকেও ঢাল α
GELUz·Φ(z)Φ(z) + z·φ(z)০.৫মসৃণ; সর্বনিম্ন প্রায় −০.১৭ (z ≈ −০.৭৫)
SiLUz·σ(z)σ(z)(1 + z(1 − σ(z)))০.৫মসৃণ; সর্বনিম্ন প্রায় −০.২৮ (z ≈ −১.২৮)

এখানে φ হলো standard normal-এর ঘনত্ব, e−z²/2/√(2π)। GELU আর SiLU monotonic নয়: ঋণাত্মক দিকে একটু নিচে নেমে আবার ০-এর দিকে ফেরে, আর তাদের ঢাল কিছু জায়গায় ১-এর চেয়েও বেশি (GELU-তে সর্বোচ্চ প্রায় ১.১৩, z = √2-এ)। নিচের ব্লক ছয়টির derivative সূত্র central difference দিয়ে যাচাই করে, আর ছবিতে saturation-এর এলাকা ধূসর রঙে দেখায়।

import numpy as np
from scipy.special import erf
import matplotlib.pyplot as plt
np.set_printoptions(precision=3, suppress=True)

sig = lambda z: 1 / (1 + np.exp(-z))
Phi = lambda z: 0.5 * (1 + erf(z / np.sqrt(2)))      # standard normal CDF
phi = lambda z: np.exp(-z**2 / 2) / np.sqrt(2 * np.pi)
al = 0.1                                             # Leaky slope (PyTorch default 0.01)
acts = {   # name: (f, f')
    "sigmoid": (sig, lambda z: sig(z) * (1 - sig(z))),
    "tanh":    (np.tanh, lambda z: 1 - np.tanh(z)**2),
    "ReLU":    (lambda z: np.maximum(0, z), lambda z: (z > 0) * 1.0),
    "Leaky":   (lambda z: np.where(z > 0, z, al * z), lambda z: np.where(z > 0, 1.0, al)),
    "GELU":    (lambda z: z * Phi(z), lambda z: Phi(z) + z * phi(z)),
    "SiLU":    (lambda z: z * sig(z), lambda z: sig(z) * (1 + z * (1 - sig(z)))),
}
zs = np.array([-6.0, -3.0, -1.0, -0.5, 0.5, 1.0, 3.0, 6.0])   # skip the kink at 0
print("z      ", zs)
for name, (f, df) in acts.items():
    num = (f(zs + 1e-5) - f(zs - 1e-5)) / 2e-5        # central difference
    print(f"{name:<7}", df(zs), f"check {np.max(np.abs(num - df(zs))):.0e}")

z = np.linspace(-6, 6, 601)
fig, ax = plt.subplots(1, 2, figsize=(10, 3.6))
for name, (f, df) in acts.items():
    ax[0].plot(z, f(z), label=name); ax[1].plot(z, df(z), label=name)
for a in ax:                                         # sigmoid/tanh saturate here
    a.axvspan(-6, -3, color="grey", alpha=0.15); a.axvspan(3, 6, color="grey", alpha=0.15)
ax[0].set(title="f(z)", xlabel="z", ylim=(-1.2, 3)); ax[1].set(title="f'(z), grey: |z| > 3", xlabel="z")
ax[1].legend(fontsize=8); fig.subplots_adjust(wspace=0.25); plt.show()

আসল আউটপুট (ব্রাউজারের Python), সঙ্গে দুই ভাগের একটি ছবি:

z       [-6.  -3.  -1.  -0.5  0.5  1.   3.   6. ]
sigmoid [0.002 0.045 0.197 0.235 0.235 0.197 0.045 0.002] check 6e-12
tanh    [0.    0.01  0.42  0.786 0.786 0.42  0.01  0.   ] check 1e-11
ReLU    [0. 0. 0. 0. 1. 1. 1. 1.] check 4e-11
Leaky   [0.1 0.1 0.1 0.1 1.  1.  1.  1. ] check 4e-11
GELU    [-0.    -0.012 -0.083  0.133  0.867  1.083  1.012  1.   ] check 2e-11
SiLU    [-0.012 -0.088  0.072  0.26   0.74   0.928  1.088  1.012] check 4e-11

প্রতিটি সারি একটি activation-এর derivative, আটটি z-এ। “check” কলামে সূত্র আর central difference-এর সর্বোচ্চ পার্থক্য ১০−১১-এর ঘরে, তাই ছয়টি সূত্রই সঠিক। z = ০ বাদ রাখা হয়েছে, কারণ ReLU আর Leaky-র ভাঁজে দুই দিকের পার্থক্য অর্থহীন। এবার সংখ্যাগুলো পড়ুন: sigmoid-এর ঢাল z = ±৩-এ ০.০৪৫, ±৬-এ ০.০০২; tanh-এর ±৩-এ ০.০১, ±৬-এ কার্যত ০. ধূসর এলাকায় এই দুটি প্রায় শেখে না। ReLU-র ঢাল ঋণাত্মক দিকে পুরোপুরি ০, Leaky-র ০.১ (ছবিতে দেখার জন্য α = ০.১ নেওয়া হয়েছে)। GELU-র ঢাল z = −১-এ −০.০৮৩ আর SiLU-র z = −৩-এ −০.০৮৮: ঋণাত্মক দিকের একটি অংশে এই দুটির ঢাল ঋণাত্মক, অর্থাৎ function সেখানে নামছে, আর z = ৩-এ তাদের ঢাল ১-এর চেয়ে সামান্য বেশি (১.০১২ আর ১.০৮৮)। ডান ছবিতে এই বাড়তি ঢাল ১-এর রেখার ওপরে ছোট কুঁজ হিসেবে দেখা যায়।

বাস্তবে কোনটি? সাধারণ MLP আর CNN-এ ReLU এখনো একটি নিরাপদ শুরু। Transformer-এ GELU বা SiLU-ভিত্তিক gate বেশি দেখা যায়। Output layer-এর activation আলাদা প্রশ্ন: শ্রেণিবিভাগে softmax বা sigmoid, regression-এ কিছুই নয় (identity). কোনটি “সবচেয়ে ভালো”, তা নিয়ে গবেষণায় ফলাফল কাজভেদে বদলায়; কোনো একটি সব জায়গায় সেরা, এমন প্রমাণ নেই। নিজের ডেটায় কয়েকটি seed-এ তুলনা করাই নিরাপদ।

৫ · বাজারঘর ডেটায় পুরো forward pass — shape, parameter আর প্রথম loss

এবার অংশ ২-এর network সত্যিই চালানো যাক। ডেটা কাল্পনিক, seed দেওয়া: ৬টি অর্ডার, চারটি feature (দূরত্ব ১ থেকে ২৫ কিলোমিটার, দাম ২০০ থেকে ৫,০০০ টাকা, আগের অর্ডার ০ থেকে ২৯, রাতের অর্ডার কি না)। Label-ও এলোমেলো, কারণ এখানে শেখা নয়, শুধু forward pass দেখা উদ্দেশ্য। Weight শুরু হয় He initialization দিয়ে, অর্থাৎ standard deviation √(2/nin)-এর normal distribution থেকে; ReLU-র জন্য এই মাপ কেন, তা পর্ব ১৪ (Weight initialization আর normalization)-এর বিষয়।

import numpy as np
np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(42)
B, d, h1, h2, k = 6, 4, 16, 8, 3     # batch, features, hidden 1, hidden 2, classes
# বাজারঘর orders: distance km, order value Tk, past orders, night order (0/1)
raw = np.column_stack([rng.uniform(1, 25, B), rng.uniform(200, 5000, B),
                       rng.integers(0, 30, B), rng.integers(0, 2, B)]).astype(float)
X = (raw - raw.mean(axis=0)) / raw.std(axis=0)       # standardise each column
y = rng.integers(0, k, B)        # 0 = early, 1 = on time, 2 = late (invented labels)

def layer(n_in, n_out):                              # He init, zero bias
    return rng.normal(0, np.sqrt(2 / n_in), (n_in, n_out)), np.zeros(n_out)
W1, b1 = layer(d, h1); W2, b2 = layer(h1, h2); W3, b3 = layer(h2, k)
relu = lambda Z: np.maximum(0, Z)
def softmax(Z):
    E = np.exp(Z - Z.max(axis=1, keepdims=True))
    return E / E.sum(axis=1, keepdims=True)

Z1 = X @ W1 + b1;  H1 = relu(Z1)                      # (B,d)@(d,h1) + (h1,)
Z2 = H1 @ W2 + b2; H2 = relu(Z2)
Z3 = H2 @ W3 + b3; P = softmax(Z3)
for n, T in [("X", X), ("W1", W1), ("b1", b1), ("H1", H1), ("W2", W2), ("b2", b2),
             ("H2", H2), ("W3", W3), ("b3", b3), ("Z3", Z3), ("P", P)]:
    print(f"{n:<3}{str(T.shape):<8}", end="\n" if n in ("b1", "b2", "b3", "P") else "")
params = sum(T.size for T in (W1, b1, W2, b2, W3, b3))
print("parameters:", params, "=", (d+1)*h1, "+", (h1+1)*h2, "+", (h2+1)*k)
print("P:\n", P, "\nrow sums:", P.sum(axis=1))
loss = -np.log(P[np.arange(B), y]).mean()
print(f"loss {loss:.3f}  vs ln 3 = {np.log(3):.3f}   zero share in H1: {np.mean(H1 == 0):.2f}")
x0 = X[0]                                            # one order alone, vector form
p0 = softmax((relu(relu(x0 @ W1 + b1) @ W2 + b2) @ W3 + b3)[None, :])
print("row 0 alone == batch row 0:", np.allclose(p0[0], P[0]))

আসল আউটপুট (ব্রাউজারের Python):

X  (6, 4)  W1 (4, 16) b1 (16,)
H1 (6, 16) W2 (16, 8) b2 (8,)
H2 (6, 8)  W3 (8, 3)  b3 (3,)
Z3 (6, 3)  P  (6, 3)
parameters: 243 = 80 + 136 + 27
P:
 [[0.215 0.554 0.231]
 [0.291 0.37  0.339]
 [0.361 0.383 0.257]
 [0.087 0.698 0.214]
 [0.294 0.359 0.347]
 [0.323 0.386 0.291]]
row sums: [1. 1. 1. 1. 1. 1.]
loss 1.064  vs ln 3 = 1.099   zero share in H1: 0.50
row 0 alone == batch row 0: True

প্রথম চার লাইন stepper-এর shape-গুলোই, এবার সত্যিকারের array থেকে: প্রতিটি activation-এর প্রথম মাত্রা ৬ (B), আর weight-এর shape-এ B নেই। Parameter ২৪৩ = ৮০ + ১৩৬ + ২৭, খাতার হিসাবের সঙ্গে হুবহু মেলে। P-এর প্রতিটি সারি তিনটি সম্ভাবনা, যোগফল ১. এখনো কিছু শেখা হয়নি, তাই সম্ভাবনাগুলো মোটামুটি কাছাকাছি, আর গড় cross-entropy loss ১.০৬৪, তিন শ্রেণিতে সমান অনুমানের মান ln ৩ ≈ ১.০৯৯-এর কাছে। এটি একটি কাজের পরীক্ষা: training শুরুর আগে loss যদি ln k-এর চেয়ে অনেক বেশি হয়, তাহলে শুরুর weight খুব বড় বা softmax-এ ভুল আছে। H1-এর অর্ধেক ঘর ০: শুরুতে z মোটামুটি শূন্যের দুই পাশে সমানভাবে ছড়ানো, তাই ReLU প্রায় অর্ধেক ঘর বন্ধ করে। এটি স্বাভাবিক; dying ReLU মানে কোনো একটি unit-এর পুরো কলাম সব input-এ ০. শেষ লাইনে একটি অর্ডার আলাদাভাবে vector আকারে চালিয়ে batch-এর প্রথম সারির সঙ্গে মেলানো হয়েছে: সারিগুলো একে অন্যকে প্রভাবিত করে না।

Shape-এর তিনটি নীরব ফাঁদ: (১) b-কে (B,) shape-এ বানালে, যেমন (6,), আর layer-এর প্রস্থও ঘটনাক্রমে ৬ হলে broadcasting কোনো error ছাড়াই ভুল দিকে যোগ করবে। (২) softmax-এ axis=1 ভুলে গেলে পুরো matrix-এর সব ঘর মিলে যোগফল ১ হয়, প্রতিটি সারির নয়। (৩) keepdims=True বাদ দিলে Z.max(axis=1)-এর shape (B,), যা (B, k) থেকে বিয়োগ করতে গেলে broadcasting ভুল দিকে মেলায় (B ≠ k হলে error, B = k হলে নীরব ভুল)। প্রতিটি ধাপে .shape ছাপানো বা assert লেখাই সবচেয়ে সস্তা প্রতিরক্ষা।

৬ · Softmax-কে নিরাপদ রাখা — max বিয়োগ আর log-sum-exp

Softmax pj = ezj / ∑m ezm, যেখানে z হলো একটি সারির logits আর j শ্রেণির নম্বর। সূত্রটি সহজ, কিন্তু কম্পিউটারের দশমিক সংখ্যার (float) সীমা আছে। float64-এ সবচেয়ে বড় মান প্রায় 1.8 × 10308, তাই ez ভেঙে পড়ে z ≈ ৭০৯.৮-এর পরে: ফল হয় inf, যাকে বলে overflow. উল্টো দিকে z ≈ −৭৪৫-এর নিচে ez হয়ে যায় ঠিক ০, যাকে বলে underflow. তখন inf/inf বা 0/0 দুটোই nan.

সমাধান একটি সরল সমতা। যেকোনো ধ্রুবক c-এর জন্য

ezj / ∑ ezm = ezj − c / ∑ ezm − c

কারণ লব আর হর দুটিই e−c দিয়ে গুণ হয়েছে। c = maxm zm নিলে সবচেয়ে বড় ঘাত হয় ঠিক ০, তাই e০ = ১, overflow অসম্ভব; আর হরে অন্তত একটি ১ থাকে, তাই হর কখনো ০ নয়। Log নিলে একই কৌশল থেকে আসে log-sum-exp: LSE(z) = log ∑ ezm = c + log ∑ ezm − c, আর log-softmax = z − LSE(z). নিচের stepper একটি সারি হাতে হিসাব করে।

শ্রেণি: সন্তুষ্ট · নিরপেক্ষ · অসন্তুষ্টএকটি সারি

            
  1. সর্বোচ্চ খোঁজা। তিনটি logits-এর মধ্যে বড়টি ৩.২, সেটিই c.
  2. বিয়োগ। প্রতিটি ঘর থেকে ৩.২ বাদ: (০, −১.৯, −৩.০)। ক্রম আর পার্থক্য একই থাকে, শুধু পুরো সারি নিচে সরে।
  3. ঘাত। e০ = ১, e−১.৯ ≈ ০.১৪৯৬, e−৩ ≈ ০.০৪৯৮. সব মান ০ থেকে ১-এর মধ্যে, overflow-এর সুযোগ নেই।
  4. যোগফল। S ≈ ১.১৯৯৪. বড় ঘরটি ১ বলে S সবসময় অন্তত ১, তাই ভাগে ০ বা nan আসে না।
  5. ভাগ। p ≈ (০.৮৩৩৮, ০.১২৪৭, ০.০৪১৫), যোগফল ১. সরাসরি e৩.২ ইত্যাদি নিয়ে হিসাব করলেও একই ফল আসত, কারণ c বাতিল হয়ে যায়।
  6. Log-রূপ। LSE = ৩.২ + ln ১.১৯৯৪ ≈ ৩.৩৮১৮; log p = z − LSE. আসল শ্রেণি প্রথমটি হলে cross-entropy = −log p1 = ০.১৮১৮, কোনো log(0)-এর ঝুঁকি ছাড়াই।

এবার একই হিসাব তিনটি সারিতে: একটি সাধারণ, একটি খুব বড় (১০০০-এর ঘরে) আর একটি খুব ঋণাত্মক। সরল (naive) পথে কী ভাঙে, তা লুকানো নয়, সরাসরি মান দেখে ধরা হয়েছে; np.errstate(all="ignore") শুধু ডেস্কটপের RuntimeWarning চুপ রাখে। ব্রাউজারের Python-এ (WebAssembly) এমনিতেও floating-point সতর্কবার্তা আসে না, তাই সেখানে nan বা inf নীরবে জন্মায়; মান যাচাই করাই একমাত্র ভরসা।

import numpy as np
from scipy.special import logsumexp, softmax as sp_softmax
np.set_printoptions(precision=4, suppress=True)
Z = np.array([[3.2, 1.3, 0.2],                       # ordinary logits
              [1000.0, 999.0, 998.0],                # very large
              [-1000.0, -1001.0, -1002.0]])          # very negative
with np.errstate(all="ignore"):                      # no warnings, just look
    E = np.exp(Z)                                    # naive: exponentiate directly
    naive = E / E.sum(axis=1, keepdims=True)
    naive_lse = np.log(E.sum(axis=1))
print("exp(Z):\n", E)
print("naive softmax:\n", naive, "\nNaN count:", np.isnan(naive).sum())

m = Z.max(axis=1, keepdims=True)                     # stable: subtract row max
S = np.exp(Z - m)
stable = S / S.sum(axis=1, keepdims=True)
lse = (m + np.log(S.sum(axis=1, keepdims=True))).ravel()
print("stable softmax:\n", stable)
print("naive lse :", naive_lse)
print("stable lse:", lse)
print("scipy lse :", logsumexp(Z, axis=1), " softmax same:", np.allclose(stable, sp_softmax(Z, axis=1)))

z = np.array([0.0, -800.0])                          # log of a tiny probability
with np.errstate(under="ignore", divide="ignore"):
    p = np.exp(z) / np.exp(z).sum()
    print("p:", p, " log p naive:", np.log(p), " log-softmax z - lse:", z - logsumexp(z))
f32 = np.finfo(np.float32).max
with np.errstate(over="ignore"):
    print(f"float32 max {f32:.3e}, ln = {np.log(f32):.2f};  exp(88), exp(89):", np.exp(np.float32([88, 89])))

আসল আউটপুট (ব্রাউজারের Python):

exp(Z):
 [[24.5325  3.6693  1.2214]
 [    inf     inf     inf]
 [ 0.      0.      0.    ]]
naive softmax:
 [[0.8338 0.1247 0.0415]
 [   nan    nan    nan]
 [   nan    nan    nan]]
NaN count: 6
stable softmax:
 [[0.8338 0.1247 0.0415]
 [0.6652 0.2447 0.09  ]
 [0.6652 0.2447 0.09  ]]
naive lse : [3.3818    inf   -inf]
stable lse: [   3.3818 1000.4076 -999.5924]
scipy lse : [   3.3818 1000.4076 -999.5924]  softmax same: True
p: [1. 0.]  log p naive: [  0. -inf]  log-softmax z - lse: [   0. -800.]
float32 max 3.403e+38, ln = 88.72;  exp(88), exp(89): [1.6516e+38        inf]

প্রথম সারিতে সরল আর নিরাপদ পথ একই ফল দেয় (০.৮৩৩৮, ০.১২৪৭, ০.০৪১৫), stepper-এর সংখ্যাই। দ্বিতীয় সারিতে e১০০০ = inf, তাই inf/inf = nan; তৃতীয় সারিতে তিনটি ঘাতই underflow করে ০, তাই 0/0 = nan. মোট ৬টি nan, অথচ গণিতের দিক থেকে দুটি সারির উত্তর পরিষ্কার: (০.৬৬৫২, ০.২৪৪৭, ০.০৯০০), যা logits (2, 1, 0)-এর softmax-এর সমান, কারণ softmax শুধু পার্থক্য দেখে। LSE-তেও একই গল্প: সরল পথে inf আর −inf, নিরাপদ পথে ১০০০.৪০৭৬ আর −৯৯৯.৫৯২৪, যা SciPy-র logsumexp-এর সঙ্গে হুবহু মেলে। চতুর্থ লাইন cross-entropy-র ফাঁদ দেখায়: দ্বিতীয় শ্রেণির সম্ভাবনা underflow করে ঠিক ০, তাই log p = −inf, আর loss অসীম; কিন্তু log-softmax সরাসরি −৮০০ দেয়। শেষ লাইন: float32-এ সবচেয়ে বড় মান প্রায় 3.4 × 1038, যার ln ৮৮.৭২; তাই e৮৮ চলে, e৮৯ inf. GPU-তে প্রায়ই float32 বা তার চেয়েও ছোট float16 চলে (সবচেয়ে বড় মান ৬৫,৫০৪, অর্থাৎ ez ভাঙে z ≈ ১১.১-এর পরেই), তাই সেখানে এই কৌশল ঐচ্ছিক নয়।

Temperature — softmax-এর ধার বাড়ানো-কমানো

Softmax-এর আগে logits-কে একটি ধনাত্মক সংখ্যা T দিয়ে ভাগ করলে, অর্থাৎ softmax(z/T), সম্ভাবনার “ধার” বদলায়। T-কে বলে temperature. Stepper-এর z = (৩.২, ১.৩, ০.২)-এ T = ১ দেয় (০.৮৩৩৮, ০.১২৪৭, ০.০৪১৫); T = ২ দিলে বণ্টন চ্যাপ্টা হয়, (০.৬২১২, ০.২৪০২, ০.১৩৮৬); T = ০.৫ দিলে তীক্ষ্ণ, (০.৯৭৫৮, ০.০২১৮, ০.০০২৪)। T → ০ হলে সবচেয়ে বড় logit-টি প্রায় সব সম্ভাবনা পায় (argmax), T → ∞ হলে সব শ্রেণি সমান। Language model-এ পরের token বাছার সময় এই T-ই “সৃজনশীলতা” নিয়ন্ত্রণের হাতল, যা AI-এর ভিত্তি কোর্সের পর্ব ৭০-এ decoding-এর অংশে এসেছে। লক্ষ করুন, ছোট T logits-এর পার্থক্যকে বড় করে, তাই overflow-এর ঝুঁকিও বাড়ে; max বিয়োগ তখন আরও জরুরি। Knowledge distillation-এও (একটি বড় model-এর সম্ভাবনা দিয়ে ছোট model শেখানো) বড় T ব্যবহার করে “নরম” সম্ভাবনা নেওয়া হয়, যা পরে কোর্সে আসবে।

Framework-এ কী লেখা উচিত

PyTorch-এর F.cross_entropy (আর nn.CrossEntropyLoss) logits নেয়, সম্ভাবনা নয়: ভেতরে log-softmax আর negative log-likelihood একসঙ্গে, নিরাপদ পথে হিসাব করে। তাই model-এর শেষে নিজে softmax বসিয়ে তারপর এই loss দিলে softmax দুবার হয়, যা একটি পরিচিত নীরব বাগ। Loss-এর বিস্তারিত পর্ব ১৩ (Deep learning-এর loss)-এ। এই সূত্রগুলোর numerical stability-এর একটি উদাহরণ (np.logaddexp) আর gradient checking-এর কৌশল আছে মেশিন লার্নিং কোর্সের পর্ব ১১-এ।

৭ · গভীরতা কী যোগ করে — ReLU network ভাঁজ করে, টুকরো গোনা

ReLU network-এর output input-এর একটি piecewise linear function: input-এর জগৎ কয়েকটি এলাকায় ভাগ হয়, আর প্রতিটি এলাকার ভেতরে network একটি সাধারণ linear function. কারণ প্রতিটি এলাকায় কোন unit চালু আর কোনটি বন্ধ, তা স্থির; চালু unit identity, বন্ধ unit ০, তাই পুরো network সেখানে কয়েকটি matrix-এর গুণফল মাত্র। এই এলাকাগুলোকে বলে linear region. যত বেশি region, তত আঁকাবাঁকা আকার network আঁকতে পারে।

একটি কাগজের ফিতায় কাঁচি দিয়ে সরলরেখা কাটা ভাবুন। ফিতা খোলা অবস্থায় একটি কাট একটি দাগ দেয়। কিন্তু ফিতাটি একবার মাঝখানে ভাঁজ করে কাটলে একই কাট দুই জায়গায় পড়ে; দুবার ভাঁজ করলে চার জায়গায়। ReLU layer ঠিক এমন ভাঁজ করে, আর পরের layer-এর প্রতিটি “কাট” সেই ভাঁজ করা রেখার সব স্তরে একসঙ্গে পড়ে।

নয়টি input বিন্দু১-D input

            
  1. শুরু। ১-D input x, −২ থেকে ২-এর নয়টি বিন্দু। এখনো কোনো ভাঁজ নেই: একটিই সরলরেখা।
  2. দুটি unit. প্রথম layer-এর একটি unit-এর weight +১, অন্যটির −১, bias ০. তাদের z হলো x আর −x.
  3. ReLU. ধনাত্মক x-এ শুধু h1 চালু, ঋণাত্মক x-এ শুধু h2. কোন unit চালু, তা দিয়ে input-রেখা দুই এলাকায় ভাগ হলো।
  4. ভাঁজ। h1 + h2 = |x|: রেখাটি x = ০-তে ভাঁজ হয়ে নিজের ওপর পড়েছে। −১.৫ আর +১.৫ এখন একই মান ১.৫ পায়; পরের layer এদের আর আলাদা করতে পারে না।
  5. দ্বিতীয় layer. একই কৌশল u-এর ওপর, এবার bias দিয়ে ভাঁজ u = ১-এ: v = |u − ১|। u = ১ হয় x = −১ আর x = ১ দুই জায়গায়, তাই এক ভাঁজ দুটি নতুন ভাঁজ দেয়।
  6. গোনা। ভাঁজ x = −১, ০, ১, তাই ৪টি linear টুকরো, ঢাল −১, +১, −১, +১. প্রতিটি নতুন ভাঁজ-layer টুকরো দ্বিগুণ করে; এক layer-এ একই চারটি unit সর্বোচ্চ ৫টি টুকরো দিতে পারত।

এখান থেকে একটি সীমা খাতায় বের করা যায়। ১-D input-এ এক hidden layer-এর প্রতিটি unit ReLU(wx + b) একটি ভাঁজ দেয়, x = −b/w-তে; n টি unit সর্বোচ্চ n টি ভাঁজ, অর্থাৎ n + 1 টি টুকরো. গভীর network-এ ধরুন আগের layer পর্যন্ত R টি টুকরো আছে। পরের layer-এর প্রতিটি unit-এর z প্রতিটি টুকরোর ভেতরে linear, তাই প্রতিটি টুকরোয় সর্বোচ্চ একবার শূন্য পার হয়: একটি unit সর্বোচ্চ R টি নতুন ভাঁজ যোগ করে, n টি unit সর্বোচ্চ nR টি। নতুন মোট ≤ R + nR = R(n + 1). তাই n1, n2, … প্রস্থের layer-গুলোর জন্য সর্বোচ্চ টুকরো ≤ ∏(nl + 1): গভীরতায় গুণফল, প্রস্থে শুধু যোগফল। Montúfar, Pascanu, Cho ও Bengio (২০১৪) বহু-মাত্রার input-এর জন্য এই ধরনের সীমা নিয়ে কাজ করেন। কিন্তু সর্বোচ্চ সম্ভব আর এলোমেলো weight-এ বাস্তবে যা ঘটে, এক কথা নয়। নিচের ব্লক সেটি সংখ্যা দিয়ে মাপে: ৬০,০০১টি ঘন বিন্দুতে প্রতিটি unit-এর চালু/বন্ধ ধারা কতবার বদলায়, তা গুনে।

import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-3, 3, 60001)[:, None]           # dense 1-D grid, one column

def count(pats):                                  # regions = on/off pattern changes + 1
    P = np.hstack(pats)
    return 1 + np.any(P[1:] != P[:-1], axis=1).sum()
def net(widths, seed):
    rng = np.random.default_rng(seed)
    h, n_in, pats = x, 1, []
    for w in widths:
        W = rng.normal(0, np.sqrt(2 / n_in), (n_in, w))   # He init
        z = h @ W + rng.normal(0, 0.5, w)
        pats.append(z > 0)
        h, n_in = np.maximum(0, z), w
    return (h @ rng.normal(0, np.sqrt(1 / n_in), n_in)), count(pats)

print("net   units    bound  regions: mean (min-max), 10 seeds")
for ws in ([8], [24], [8] * 3, [96], [24] * 4):
    r = [net(ws, s)[1] for s in range(10)]
    bound = np.prod([w + 1 for w in ws])
    print(f"{len(ws)}x{ws[0]:<3} {sum(ws):>5} {bound:>8}  {np.mean(r):6.1f} ({min(r)}-{max(r)})")

u, pats = x.ravel(), []                           # hand-set folds: 3 layers x 2 units
for c in (0.0, 1.5, 0.75):
    pats.append((u > c)[:, None])                  # one on/off switch per fold
    u = np.maximum(0, u - c) + np.maximum(0, c - u)   # |u - c| from two ReLUs
print("hand-set fold net: 6 units, regions =", count(pats))

fig, ax = plt.subplots(1, 3, figsize=(11, 3))
for a, (t, y) in zip(ax, [("random 1x24", net([24], 0)[0]),
                          ("random 4x24", net([24] * 4, 0)[0]), ("hand-set 3x2 folds", u)]):
    a.plot(x.ravel(), y, lw=1.2); a.set(title=t, xlabel="x")
fig.subplots_adjust(wspace=0.3); plt.show()

আসল আউটপুট (ব্রাউজারের Python), সঙ্গে তিন ভাগের একটি ছবি:

net   units    bound  regions: mean (min-max), 10 seeds
1x8       8        9     8.4 (7-9)
1x24     24       25    23.3 (21-25)
3x8      24      729    20.0 (17-24)
1x96     96       97    88.8 (86-93)
4x24     96   390625    83.8 (64-113)
hand-set fold net: 6 units, regions = 8

“bound” কলাম ∏(nl + 1): এক layer-এ শুধু unit + ১, কিন্তু ৪ × ২৪-এ ৩,৯০,৬২৫. অথচ গোনা region-এর গড় দুই ক্ষেত্রেই মোট unit-সংখ্যার কাছাকাছি: ২৪টি unit-এ এক layer ২৩.৩, তিন layer ২০.০; ৯৬টি unit-এ এক layer ৮৮.৮, চার layer ৮৩.৮. অর্থাৎ এলোমেলো শুরুতে গভীরতা টুকরোর সংখ্যা গুণফলের মতো বাড়ায় না; Hanin ও Rolnick (২০১৯) তাত্ত্বিকভাবে ও পরীক্ষায় এই ধাঁচটির কথা জানান, যেখানে region-এর ঘনত্ব মোটামুটি মোট neuron-সংখ্যার সঙ্গে বাড়ে। কিন্তু হাতে বসানো ভাঁজ-network মাত্র ৬টি unit-এ ৮টি টুকরো দেয়, যেখানে এক layer-এর ৬টি unit সর্বোচ্চ ৭টি দিতে পারত; ভাঁজ বাড়ালে ব্যবধান দ্রুত বাড়ে। ছবির শেষ ভাগে এই ভাঁজ একটি নিয়মিত করাতের দাঁতের মতো আকার নেয়; প্রথম দুই ভাগে এলোমেলো network-এর আঁকাবাঁকা কিন্তু অনিয়মিত রেখা। (গণনাটি ঘন grid-এ, তাই ১০−৪-এর চেয়ে কাছাকাছি দুটি ভাঁজ এক হিসেবে ধরা পড়তে পারে; এটি নিম্নসীমা।)

সংক্ষেপে: গভীরতা সম্ভাবনা বাড়ায়, অর্থাৎ কম parameter-এ অনেক বেশি টুকরো আঁকা যায়, আর সেই টুকরো পুনরাবৃত্ত, গঠনযুক্ত (একটি ভাঁজ আগের সব ভাঁজের ওপর পড়ে)। এটিই “স্তরে স্তরে feature জোড়া”-র জ্যামিতিক রূপ: প্রথম layer সরল সীমারেখা আঁকে, পরের layer সেই ভাঁজ-করা জগতে আবার সরল কাট দেয়, যা মূল input-এ জটিল আকার নেয়। কিন্তু training সেই সম্ভাবনা সত্যিই কাজে লাগাবে কি না, প্রস্থ বনাম গভীরতার তাত্ত্বিক ফল, আর করাতের দাঁতের মতো function-এ গভীর network-এর সুবিধা কতটা, তা পর্ব ০৬ (Network কী শিখতে পারে — universal approximation, গভীরতা আর প্রস্থ)-এর বিষয়।

আরও গভীরে: Linear region-এর ধারণা দিয়ে network-এর প্রকাশক্ষমতা মাপার কাজ শুরু হয় Montúfar ও সহকর্মীদের ২০১৪ সালের প্রবন্ধে (On the Number of Linear Regions of Deep Neural Networks); Serra, Tjandraatmadja ও Ramalingam (২০১৮) সীমাগুলো আরও আঁটসাঁট করেন, আর Hanin ও Rolnick (২০১৯) দেখান যে শুরুতে আর training-এর পরেও গড় region-সংখ্যা সর্বোচ্চ সীমার চেয়ে অনেক কম থাকে। Activation-এর তুলনায় Hendrycks ও Gimpel-এর GELU প্রবন্ধ আর Ramachandran, Zoph ও Le-এর “Searching for Activation Functions” (২০১৭) পড়ার মতো; softmax-এর নিরাপদ হিসাব নিয়ে Higham-এর Accuracy and Stability of Numerical Algorithms বইটি ক্লাসিক উৎস।

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

  • 784 → 256 → 128 → 10 MLP-এ parameter কত? (785 × 256) + (257 × 128) + (129 × 10) = ২,০০,৯৬০ + ৩২,৮৯৬ + ১,২৯০ = ২,৩৫,১৪৬.
  • σ′(0), tanh′(0), GELU′(0) কত? ০.২৫, ১, ০.৫.
  • Softmax-এ max বিয়োগ করলে ফল বদলায় না কেন? লব আর হর দুটিই e−c দিয়ে গুণ হয়, যা বাতিল হয়ে যায়; softmax শুধু logits-এর পার্থক্য দেখে।
  • Activation ছাড়া ১০ layer-এর network কী? একটি linear map, কারণ W1W2…W10 একটি matrix.
  • Dying ReLU কী, প্রতিকার কী? যে unit সব input-এ ঋণাত্মক z পায়, তার output আর gradient দুটিই ০; Leaky ReLU, ছোট learning rate বা ভালো initialization.

৮ · AI-এর যুগে — AI-এর লেখা forward pass আর activation কোড যাচাই

AI assistant-কে “NumPy-তে একটি MLP লিখে দাও” বললে সাধারণত চলার মতো কোড আসে। বিপদ হলো, ভুলগুলো প্রায়ই error দেয় না, শুধু ভুল সংখ্যা দেয়। এই বিষয়ে যে ভুলগুলো বারবার দেখা যায়: weight-এর (in, out) আর (out, in) রীতি মিশিয়ে ফেলা; softmax-এ max বিয়োগ না করা বা axis ভুল দেওয়া; সম্ভাবনার ওপর np.log নিয়ে loss বানানো, যেখানে log-softmax দরকার ছিল; PyTorch-এর loss-এ logits-এর বদলে softmax-এর ফল পাঠানো; output layer-এও ReLU বসানো; আর GELU-কে tanh-আসন্ন রূপে লিখে তাকেই “exact” বলা। এ ছাড়া AI মাঝে মাঝে activation-এর ইতিহাস বা প্রবন্ধের নাম আত্মবিশ্বাসের সঙ্গে ভুল বলে, তাই উদ্ধৃতি মূল উৎসে মিলিয়ে নিন।

প্রশ্নটি নির্দিষ্ট করুন: কোন রীতি, কোন shape, আর কী যাচাই চান, তা বলে দিন।

আমি NumPy-তে একটি MLP লিখছি: input X-এর shape (B, 4), hidden layer 16 আর 8 unit, ReLU, output 3 শ্রেণি, softmax. Weight রাখো (in, out) shape-এ, হিসাব X @ W + b. প্রতিটি লাইনের পাশে comment-এ ফলের shape লিখে দাও। Softmax নিরাপদ পথে লেখো (সারির max বিয়োগ, keepdims=True), আর loss-এর জন্য log-softmax ব্যবহার করো, np.log(softmax) নয়। শেষে assert দিয়ে সব shape যাচাই করো, আর মোট parameter-এর সংখ্যা ছাপাও। কোনো জায়গায় নিশ্চিত না হলে সেটা আলাদা করে বলো।

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

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

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

প্র ০১Hidden layer থেকে sigmoid আর tanh সরিয়ে ReLU কেন এল, আর পরে আবার মসৃণ GELU ও SiLU কেন ফিরল?

১৯৮০ আর ১৯৯০-এর দশকে backpropagation-এর বইপত্রে sigmoid আর tanh-ই ছিল স্বাভাবিক পছন্দ: মসৃণ, সব জায়গায় derivative আছে, আর জীববিজ্ঞানের neuron-এর “firing rate”-এর সঙ্গে একটা মিল টানা যেত। সমস্যা দেখা দেয় গভীর network-এ। প্রতিটি layer-এ gradient f′(z) দিয়ে গুণ হয়, আর sigmoid-এর f′ সর্বোচ্চ ০.২৫, saturation-এ প্রায় ০; কয়েক layer পরেই শুরুর দিকের weight প্রায় শেখে না। Sepp Hochreiter-এর ১৯৯১ সালের থিসিস আর পরে Bengio ও সহকর্মীদের ১৯৯৪ সালের প্রবন্ধ এই vanishing gradient-এর সমস্যা বিশ্লেষণ করে বলে সাধারণত উল্লেখ করা হয়।

ReLU-র ধারণা পুরোনো (Fukushima-র ১৯৬০-৭০-এর দশকের কাজেও এমন rectifier ছিল বলে উল্লেখ পাওয়া যায়), কিন্তু ২০১০ সালের Nair ও Hinton আর ২০১১ সালের Glorot, Bordes ও Bengio-র কাজের পর এটি গভীর network-এ ব্যাপক হয়। সুবিধা তিনটি: ধনাত্মক দিকে ঢাল ঠিক ১, তাই চালু পথে gradient ছোট হয় না; হিসাব একটি তুলনা মাত্র; আর অনেক unit ০ দেয় বলে ভেতরের representation বিরল (sparse). ২০১২ সালের AlexNet ReLU দিয়ে tanh-এর চেয়ে কয়েক গুণ দ্রুত training-এর কথা জানায়, যা জনপ্রিয়তায় বড় ভূমিকা রাখে।

তাহলে মসৃণ activation ফিরল কেন? ReLU-র ভাঁজ আর মরে যাওয়া unit বড় model-এ কিছু ক্ষেত্রে অসুবিধা করে বলে ধারণা করা হয়। GELU (২০১৬) BERT ও GPT-2-এ ব্যবহার হয়, আর পরে Shazeer-এর ২০২০ সালের GLU-variant প্রবন্ধে SwiGLU-এর মতো gate-যুক্ত রূপ ভালো ফল দেখায়, যা পরে অনেক বড় language model গ্রহণ করে। তবে এই পার্থক্যগুলো সাধারণত ছোট, আর কোনটি কেন ভালো, তার পরিষ্কার তত্ত্ব নেই; অনেকটাই পরীক্ষালব্ধ।

মূল উপলব্ধি: Activation বাছাই gradient-এর পথ খোলা রাখার প্রশ্ন; ReLU সেই পথ সহজ করেছে, আর মসৃণ বিকল্পগুলো বড় model-এ ছোট কিন্তু মাপা যায় এমন উন্নতি এনেছে বলে রিপোর্ট আছে, সর্বজনীন জয় নয়।

প্র ০২ReLU তো প্রায় linear — শুধু ০-তে একটি ভাঁজ। এত “সামান্য” non-linearity দিয়ে network কীভাবে জটিল function আঁকে?

প্রথম দেখায় ReLU সত্যিই হতাশাজনক: দুই দিকে দুটি সরলরেখা। কিন্তু non-linearity-র পরিমাণ একটি unit-এ নয়, অনেক unit আর অনেক layer-এর সংমিশ্রণে মাপতে হয়। এক hidden layer-এ n টি unit n টি জায়গায় ভাঁজ দেয়, আর output layer সেই ভাঁজ-করা টুকরোগুলো বিভিন্ন ঢালে জোড়া দেয়। যেকোনো ধারাবাহিক বাঁকা রেখাকে যথেষ্ট ছোট সরল টুকরোয় ভাগ করে কাছাকাছি আনা যায়, ঠিক যেমন ম্যাপে একটি নদীর বাঁক অনেক ছোট সরলরেখা দিয়ে আঁকা হয়। তাই এক hidden layer-এর ReLU network-ও যথেষ্ট চওড়া হলে সীমিত এলাকায় যেকোনো ধারাবাহিক function-এর কাছে যেতে পারে; এটি universal approximation-এর ReLU-রূপ, যার শর্ত আর সীমা পর্ব ০৬-এ।

গভীরতা এখানে আরেকটি মাত্রা যোগ করে। এই পর্বের ভাঁজ-stepper-এ দেখেছেন, প্রথম layer রেখাটিকে নিজের ওপর ভাঁজ করলে পরের layer-এর একটি ভাঁজ দুই জায়গায় পড়ে। ফলে টুকরোর সংখ্যা layer-প্রতি গুণ হতে পারে, যোগ নয়। তাই ছবি বা ভাষার মতো পুনরাবৃত্ত গঠনের ডেটায় গভীর network কম parameter-এ বেশি প্রকাশ করতে পারে বলে মনে করা হয়।

এখানে একটি সতর্কতা জরুরি: “সম্ভব” মানে “ঘটে” নয়। এই পর্বের region-গোনায় এলোমেলো weight-এ গভীর network-এর টুকরো মোট unit-সংখ্যার কাছাকাছি থেকেছে, সর্বোচ্চ সীমার ধারেকাছেও নয়। Training-এর পর কী হয়, আর gradient descent আদৌ ওই জটিল ভাঁজগুলো খুঁজে পায় কি না, তা আলাদা প্রশ্ন, যার উত্তর এখনো গবেষণার বিষয়। বাস্তবে গভীর network-এর সাফল্যের কৃতিত্ব শুধু region-সংখ্যাকে দেওয়া যায় না; architecture-এর inductive bias আর optimization-এর আচরণও বড় ভূমিকা রাখে।

মূল উপলব্ধি: ReLU-র শক্তি একটি ভাঁজে নয়, অনেক ভাঁজের সংমিশ্রণে; গভীরতা সেই ভাঁজগুলোকে গুণ করার সুযোগ দেয়, কিন্তু সুযোগ কাজে লাগানো training-এর ওপর নির্ভর করে।

প্র ০৩NumPy-তে X @ W, গণিতের বইয়ে Wx, আর PyTorch-এ weight-এর shape (out, in) — এত রীতির কারণ কী, আর ভুল রীতি বাস্তবে কী ক্ষতি করে?

গণিতে vector-কে কলাম ধরা একটি পুরোনো প্রথা, তাই linear map লেখা হয় y = Wx, W-এর shape (out, in). একটি উদাহরণ নিয়ে চিন্তা করলে এটি স্বাভাবিক। কিন্তু ডেটা আসে টেবিলে, প্রতিটি সারি একটি উদাহরণ, যেমন pandas DataFrame বা CSV ফাইল। তখন পুরো batch-এ লেখা সহজ Y = XWᵀ বা, W-কে আগেই (in, out) রাখলে, Y = XW. NumPy আর C-এর row-major memory-তে একটি উদাহরণের সব feature পাশাপাশি থাকে, তাই সারিতে উদাহরণ রাখা memory-র দিক থেকেও স্বাভাবিক।

PyTorch-এর nn.Linear weight রাখে (out, in) shape-এ, অর্থাৎ গণিতের W-এর মতো, আর হিসাব করে x Wᵀ + b. এর একটি সুবিধা: weight-এর প্রতিটি সারি একটি output unit-এর পুরো weight vector, যা দেখা ও বিশ্লেষণ করা সহজ। TensorFlow/Keras-এর Dense layer আবার kernel রাখে (in, out) shape-এ। দুই framework-এর মধ্যে weight কপি করার সময় তাই transpose লাগে, আর এখান থেকেই অনেক বাগ জন্মায়।

সবচেয়ে বিপজ্জনক ক্ষেত্র বর্গাকার layer, যেমন Transformer-এর attention-এর projection, যেখানে in = out. সেখানে ভুল transpose-এ shape মিলে যায়, কোনো error আসে না, model শুধু খারাপ ফল দেয়, আর মানুষ ভাবে hyperparameter-এর দোষ। প্রতিরক্ষা সহজ: পরীক্ষার সময় সব মাত্রা আলাদা রাখুন (d = ৪, h = ১৬, k = ৩), যাতে ভুল রীতি সঙ্গে সঙ্গে shape error দেয়; আর weight কপির পর একটি ইনপুটে দুই framework-এর output মিলিয়ে দেখুন।

মূল উপলব্ধি: রীতিগুলো সবই সঠিক, শুধু ভিন্ন; বিপদ রীতি মেশানোয়, আর সমান মাপের মাত্রা সেই বিপদকে নীরব করে দেয়।

প্র ০৪Float64-এ e৭১০ লাগে overflow-এ, logits কি আসলেই এত বড় হয়? তাহলে নিরাপদ softmax নিয়ে এত কথা কেন?

ছোট পরীক্ষায় float64-এ logits কদাচিৎ ৭০০ পেরোয়, তাই সরল softmax প্রায়ই “কাজ করে”। কিন্তু deep learning-এ পরিস্থিতি আলাদা। প্রথমত, GPU-তে training সাধারণত float32-এ, আর গতি ও memory বাঁচাতে প্রায়ই float16 বা bfloat16-এ (mixed precision, পর্ব ১২)। float32-এ সীমা e৮৮.৭, আর float16-এ সবচেয়ে বড় মান মাত্র ৬৫,৫০৪, অর্থাৎ z ≈ ১১-এর পরেই ez inf. ১১ মোটেও বড় logit নয়। দ্বিতীয়ত, training-এর মাঝপথে একটি খারাপ ধাপ (বড় learning rate, ভুল ডেটা) logits-কে হঠাৎ বড় করে দিতে পারে; একবার nan জন্মালে তা gradient হয়ে সব weight-এ ছড়ায়, আর পুরো run নষ্ট।

তৃতীয়ত, softmax শুধু output-এ নয়। Transformer-এর attention-এ প্রতিটি query-র জন্য সব key-এর স্কোরের ওপর softmax হয় (পর্ব ৩৩)। সেখানে dot product √d দিয়ে ভাগ করার একটি কারণই হলো স্কোরকে পরিমিত রাখা, যাতে softmax saturate না করে। আর FlashAttention-এর মতো দক্ষ পদ্ধতি (পর্ব ৩৭) “online softmax” নামে চলমান max আর চলমান যোগফল রেখে ঠিক এই max-বিয়োগ কৌশলই টুকরো টুকরো করে খাটায়।

চতুর্থত, log-রূপে underflow-ও সমান বিপদ: কোনো শ্রেণির সম্ভাবনা ০ হয়ে গেলে log p = −inf, loss অসীম। এজন্য framework-গুলো loss-এ logits নেয় আর log-softmax ও negative log-likelihood একসঙ্গে হিসাব করে। একই log-sum-exp কৌশল HMM-এর forward algorithm, mixture model আর beam search-এর মতো সম্ভাবনা-যোগের জায়গায় বারবার আসে।

মূল উপলব্ধি: নিরাপদ softmax কোনো বিরল কোণার জন্য বিলাসিতা নয়; কম-নির্ভুল float, অস্থির training আর attention-এর কারণে এটি deep learning-এর দৈনন্দিন প্রয়োজন।

প্র ০৫একই সংখ্যক parameter-এ একটি চওড়া hidden layer ভালো, না কয়েকটি সরু layer? Linear region-এর হিসাব এ প্রশ্নে কতটা সাহায্য করে?

Region-এর তাত্ত্বিক সীমা স্পষ্টভাবে গভীরতার পক্ষে: ১-D input-এ এক layer-এর n unit সর্বোচ্চ n + 1 টুকরো দেয়, আর L টি layer-এ প্রতিটিতে n unit থাকলে সীমা (n + 1)L. Montúfar ও সহকর্মীরা (২০১৪) বহু-মাত্রার input-এ এমন সূচকীয় সীমা দেখান, আর Telgarsky-র ২০১৬ সালের কাজ এমন function-এর উদাহরণ দেয়, যা গভীর network ছোট আকারে প্রকাশ করতে পারে কিন্তু অগভীর network-এর জন্য সূচকীয় বেশি unit লাগে (পর্ব ০৬-এ বিস্তারিত)।

কিন্তু এই পর্বের গণনা একটি বিপরীত সত্যও দেখায়: এলোমেলো শুরুতে গভীর আর অগভীর network-এর region-সংখ্যা মোট unit-এর কাছাকাছি, গভীরতার “সুবিধা” চোখে পড়ে না। Hanin ও Rolnick (২০১৯) এই ধাঁচকে তত্ত্বে ব্যাখ্যা করেন, আর জানান যে training-এর পরেও region-সংখ্যা সাধারণত সীমার চেয়ে অনেক নিচে থাকে। তাহলে region-সংখ্যা দিয়ে architecture বাছাই করা যায় না।

বাস্তবের উত্তর তাই পরীক্ষালব্ধ আর কাজভেদে আলাদা। গভীর network-এর পক্ষে আছে স্তরে স্তরে feature গড়ার ক্ষমতা (ছবিতে কিনারা থেকে আকার থেকে বস্তু), আর parameter-প্রতি বেশি প্রকাশক্ষমতা। বিপক্ষে আছে training-এর কষ্ট: vanishing gradient, initialization-এর সংবেদনশীলতা, যার জন্য residual connection আর normalization লাগে (পর্ব ১৪, ১৭)। ছোট tabular ডেটায় একটি বা দুটি চওড়া layer প্রায়ই যথেষ্ট, আর অনেক সময় gradient boosting-ই ভালো। Generalization-এর প্রশ্ন (মেশিন লার্নিং কোর্সের পর্ব ৩) region-গোনা দিয়ে উত্তর হয় না।

মূল উপলব্ধি: Linear region প্রকাশক্ষমতার একটি জ্যামিতিক মাপ, যা গভীরতার সম্ভাবনা বোঝায়; কিন্তু কোনটি ভালো শিখবে, তা ঠিক করে optimization, ডেটা আর inductive bias, তাই উত্তর আসে পরীক্ষা থেকে।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান। চাইলে অংশ ৫-এর কোড বদলে যাচাই করুন।

  1. Shape আর parameter গুনুন: হাতে লেখা অঙ্ক চেনার একটি MLP: 784 → 256 → 128 → 10, ReLU, শেষে softmax. B = ৩২ হলে X, প্রতিটি W, b, প্রতিটি hidden activation আর P-এর shape লিখুন; মোট parameter আর forward pass-এর আনুমানিক floating-point কাজের সংখ্যা বের করুন।

    X (32, 784); W1 (784, 256), b1 (256,), H1 (32, 256); W2 (256, 128), b2 (128,), H2 (32, 128); W3 (128, 10), b3 (10,), Z3 ও P (32, 10).

    Parameter: (784 + 1) × 256 = ২,০০,৯৬০; (256 + 1) × 128 = ৩২,৮৯৬; (128 + 1) × 10 = ১,২৯০; মোট ২,৩৫,১৪৬. প্রায় ৮৫ শতাংশ প্রথম layer-এ, কারণ input চওড়া।

    Floating-point কাজ ≈ 2 × B × ∑ ninnout = 2 × 32 × (২,০০,৭০৪ + ৩২,৭৬৮ + ১,২৮০) = 2 × 32 × ২,৩৪,৭৫২ ≈ ১.৫ × ১০৭, অর্থাৎ প্রায় দেড় কোটি; একটি আধুনিক CPU-তে এক মিলিসেকেন্ডের ঘরের কাজ।

  2. আউটপুট অনুমান করুন: কোড না চালিয়ে বলুন প্রতিটি লাইন কী ছাপাবে, বা কোথায় error আসবে:
    X = np.ones((5, 3)); W = np.ones((3, 4))
    print((X @ W + np.zeros(4)).shape)        # (ক)
    print((X @ W + np.zeros(5)).shape)        # (খ)
    z = np.array([1000.0, 1000.0])
    e = np.exp(z - z.max()); print(e / e.sum())   # (গ)
    print(np.maximum(0, np.array([-2., 0., 3.])) @ np.array([1., 2., 3.]))  # (ঘ)

    (ক) (5, 4): bias (4,) প্রতিটি সারিতে যোগ হয়। (খ) ValueError: “operands could not be broadcast together with shapes (5,4) (5,)”, কারণ broadcasting শেষ মাত্রা মেলায়, ৪ আর ৫ মেলে না। (গ) [0.5 0.5]: max বিয়োগের পর z হয় (0, 0), e = (1, 1); সরল পথে এখানে nan আসত। (ঘ) ReLU দেয় (0, 0, 3), dot product 0·1 + 0·2 + 3·3 = 9.0.

  3. সমালোচনা ও নকশা: একজন সহকর্মী ১০ layer-এর একটি network জমা দিয়েছেন: সব hidden layer-এ sigmoid, weight শুরু N(0, 1) থেকে, output-এ softmax নিজে লেখা np.exp(z) / np.exp(z).sum() (axis ছাড়া), আর loss -np.log(P[range(B), y]).mean(). তিনি বলছেন “loss প্রথম ধাপ থেকেই nan, আর ঠিক হলেও শুরুর layer শেখে না”। সমস্যাগুলো চিহ্নিত করে একটি ভালো নকশা প্রস্তাব করুন।

    সমস্যা: (১) softmax-এ axis নেই, তাই পুরো batch মিলে যোগফল ১, প্রতিটি সারির নয়; সম্ভাবনাগুলো ভুল আর খুব ছোট। (২) max বিয়োগ নেই, বড় logits-এ inf/inf = nan. (৩) সম্ভাবনার ওপর log, underflow-এ log 0 = −inf. (৪) দশটি sigmoid layer: ঢাল সর্বোচ্চ ০.২৫, আর N(0, 1) weight-এ চওড়া layer-এর z বড় হয়ে saturate করে, তাই vanishing gradient.

    নকশা: hidden layer-এ ReLU (বা GELU), He initialization (standard deviation √(2/nin)); output-এ logits রেখে loss = mean(LSE(z) − zy), সারিপ্রতি max বিয়োগ আর keepdims=True; শুরুর loss ln k-এর কাছে কি না দেখা। ১০ layer সত্যিই দরকার কি না ডেটা দেখে ঠিক করা, আর লাগলে normalization বা residual connection (পর্ব ১৪, ১৭)।

  4. কোড লিখুন: GELU-র tanh-আসন্ন রূপ 0.5z(1 + tanh(√(2/π)(z + 0.044715z³))) NumPy-তে লিখুন, আর [−4, 4]-এ exact GELU z·Φ(z)-এর সঙ্গে সর্বোচ্চ পরম পার্থক্য মাপুন। সঙ্গে খাতায় σ′(0), tanh′(0), GELU′(0) আর SiLU′(0) বের করুন।
    import numpy as np
    from scipy.special import erf
    z = np.linspace(-4, 4, 800001)
    exact = z * 0.5 * (1 + erf(z / np.sqrt(2)))
    approx = 0.5 * z * (1 + np.tanh(np.sqrt(2 / np.pi) * (z + 0.044715 * z**3)))
    print(np.abs(exact - approx).max())

    ফল প্রায় 4.7 × 10−4 (একটি স্থানীয় run-এ 0.000473): রেখা দুটি চোখে আলাদা করা যায় না, কিন্তু “exact” নয়; তাই দুটি framework-এর ফল মেলানোর সময় কোন রূপ, তা জানতে হবে। খাতায়: σ′(0) = σ(0)(1 − σ(0)) = ০.৫ × ০.৫ = ০.২৫; tanh′(0) = 1 − 0 = ১; GELU′(0) = Φ(0) + 0·φ(0) = ০.৫; SiLU′(0) = σ(0)(1 + 0) = ০.৫.

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

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

ক্রমদুই hidden layer-এর MLP-এর forward pass-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Input batch X (B, d) তৈরি, প্রতিটি কলাম standardise করা
  2. Z1 = X @ W1 + b1, তারপর ReLU দিয়ে H1
  3. Z2 = H1 @ W2 + b2, তারপর ReLU দিয়ে H2
  4. Z3 = H2 @ W3 + b3, অর্থাৎ logits
  5. প্রতিটি সারির max বিয়োগ করে softmax বা log-softmax
  6. সঠিক শ্রেণির −log p-এর গড়, অর্থাৎ cross-entropy loss

একটি MLP-এর layer-এর মাপ 10 → 32 → 5 (bias-সহ)। মোট parameter কত?

  • 480
  • 517
  • 527
  • 1600

X-এর shape (64, 20), W-এর shape (20, 50), b-এর shape (50,)। H = ReLU(X @ W + b)-এর shape কী?

  • (20, 50)
  • (64, 20)
  • (64, 50)
  • (64,)

z = 4-এ কোন activation-এর derivative সবচেয়ে ছোট?

  • tanh
  • sigmoid
  • ReLU
  • GELU

Logits z = (1000, 999). সরাসরি exp(z) / sum(exp(z)) float64-এ হিসাব করলে কী হয়, আর সঠিক উত্তর কত?

  • সরল হিসাবেই সঠিক (0.731, 0.269) আসে
  • ফল (0, 0), কারণ underflow
  • ফল nan, আর সঠিক উত্তর (0.5, 0.5)
  • ফল nan, আর সঠিক উত্তর প্রায় (0.731, 0.269)

“Dying ReLU” বলতে কী বোঝায়?

  • কোনো batch-এ hidden activation-এর প্রায় অর্ধেক ঘর ০ হওয়া
  • কোনো unit-এর z সব input-এ ঋণাত্মক, তাই তার output ও gradient স্থায়ীভাবে ০
  • বড় ধনাত্মক z-এ ReLU-র ঢাল ছোট হয়ে যাওয়া
  • ReLU-র output ঋণাত্মক হয়ে যাওয়া

১-D input-এ এক hidden layer-এর ReLU network-এ ১০টি unit আছে। Output-এ সর্বোচ্চ কয়টি linear টুকরো থাকতে পারে, আর ১০টি unit দুই layer-এ ৫ + ৫ ভাগ করলে সীমা কত?

  • ১০ আর ১০
  • ১১ আর ১১
  • ১১ আর ৩৬
  • ১০২৪ আর ৩৬

PyTorch-এ F.cross_entropy-এ input হিসেবে কী দেওয়া উচিত?

  • শেষ layer-এর কাঁচা logits
  • Softmax-এর পরের সম্ভাবনা
  • np.log(softmax)-এর ফল
  • ReLU-এর পরের logits

শব্দকোষ

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

মনে রাখুন

  1. উদাহরণ সারিতে: Z = X @ W + b, (B, a) @ (a, c) → (B, c); activation shape বদলায় না, আর weight-এর shape-এ B থাকে না।
  2. Parameter = ∑ (nin + 1) × nout; forward-এর কাজ ≈ 2·B·∑ ninnout.
  3. sigmoid/tanh saturate করে (সর্বোচ্চ ঢাল ০.২৫ আর ১); ReLU ধনাত্মক দিকে ঢাল ১ রাখে কিন্তু মরতে পারে; Leaky ReLU, GELU, SiLU তার বিকল্প।
  4. Softmax-এ সারির max বিয়োগ, log-এ log-sum-exp; framework-এর loss-এ logits দিন।
  5. ReLU network input-কে ভাঁজ করে linear region বানায়; সীমা গভীরতায় গুণফল, কিন্তু এলোমেলো শুরুতে region মোটামুটি মোট unit-এর কাছাকাছি।

পরের পর্বে: forward pass এখন পরিষ্কার। কিন্তু weight শেখাতে লাগবে প্রতিটি W আর b-এর gradient. পর্ব ০৪-এ softmax + cross-entropy থেকে P − Y-এর সুন্দর gradient, প্রতিটি layer-এর gradient-এর shape, আর gradient checking দিয়ে পুরো backward pass যাচাই।

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

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