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

Backpropagation হাতে-কলমে — chain rule থেকে পুরো gradient

Backpropagation by Hand — Matrix Form, Gradient Checking, Cost and Common Bugs

Taught in Bangla, this lesson derives backpropagation as upstream gradient times local gradient, first with numbers on a 2-2-1 network, then in batched matrix form for a linear layer (dW, db, dX) and for softmax with cross-entropy, where the gradient is exactly (P − Y)/B. In in-page Python with NumPy it writes a full backward pass, gradient-checks every parameter with central differences, trains a three-class model with mini-batch SGD, and catches three common bugs. It also counts compute and memory.

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

  • ২-২-১ network-এ সংখ্যা ধরে backprop, আর ReLU-এর “দরজা” কীভাবে gradient আটকায়
  • দুই hidden layer-এর batch MLP-এর সব gradient matrix আকারে, প্রতিটির shape সহ
  • Softmax আর cross-entropy মিলে কেন ∂L/∂Z = P − Y, Jacobian ধরে প্রমাণ
  • NumPy-তে backward pass, প্রতিটি parameter-এর relative error দিয়ে gradient check
  • Backward-এর খরচ আর memory মাপা, পুরো training, আর তিনটি চেনা বাগ ধরা

১ · কোথা থেকে শুরু — আগের কোর্সে যা দেখেছেন, এখানে যা নতুন

AI-এর ভিত্তি কোর্সের পর্ব ৬৮-এ একটি ছোট network-এ এক ধাপ backprop হাতে কষেছেন, আর এক hidden layer-এর NumPy MLP-এ gradient check চালিয়েছেন। পর্ব ৩৪-এ chain rule আর gradient descent আছে। মেশিন লার্নিং কোর্সের পর্ব ১১ matrix calculus-এর সূত্র (Jacobian, layout, ‖Ax − b‖²-এর gradient) প্রমাণসহ দিয়েছে। এই পর্ব সেগুলো আবার প্রমাণ করবে না; ধরে নিয়ে তিনটি প্রশ্নে গভীরে যাবে।

  • সাধারণ নিয়ম কী? যত স্তরই থাকুক, প্রতিটি স্তরের backward একই তিনটি সূত্র। দুই hidden layer-এর network-এ সব gradient লিখে ফেলব।
  • কীভাবে নিশ্চিত হব? প্রতিটি parameter-এর জন্য আলাদা relative error, আর ইচ্ছা করে ঢোকানো তিনটি বাগ সেটি কীভাবে ধরে।
  • দাম কত? Backward-এ কত গুণ হিসাব আর কত memory লাগে, মেপে দেখা।

বাজারঘরের একটি পণ্যের দাম ভাবুন: কৃষক থেকে আড়ত, আড়ত থেকে পাইকার, পাইকার থেকে খুচরা দোকান। প্রতিটি হাতে দাম একটি নির্দিষ্ট হারে বাড়ে। কৃষকের দাম ১ টাকা বাড়লে খুচরা দাম কত বাড়বে? প্রতিটি ধাপের হার গুণ করলেই উত্তর। এটিই chain rule. Backprop শুধু গুণের ক্রমটি উল্টে দেয়: খুচরা দোকান থেকে শুরু করে পেছনে হাঁটে, যাতে একবার হেঁটেই প্রতিটি ধাপের “দায়” জানা যায়।

এক সংখ্যার output থেকে পেছনে derivative জোড়ার পদ্ধতি (reverse-mode automatic differentiation) সাধারণত Seppo Linnainmaa-র ১৯৭০ সালের master's thesis-এর সঙ্গে যুক্ত করা হয়। Neural network-এ এর প্রয়োগের প্রস্তাব Paul Werbos-এর ১৯৭৪ সালের PhD thesis আর পরের কাজে পাওয়া যায়। ব্যাপক পরিচিতি আসে ১৯৮৬ সালে David Rumelhart, Geoffrey Hinton আর Ronald Williams-এর Nature-এর প্রবন্ধে। কৃতিত্ব কার কতটা, তা নিয়ে গবেষকদের মধ্যে বিতর্ক আছে।

২ · এক নিয়ম: upstream gradient × local gradient — ২-২-১ network-এ সংখ্যা ধরে

Network-এর প্রতিটি ধাপ একটি ছোট function, যেমন u = f(v). Backward pass-এ এই ধাপের কাছে ওপর থেকে আসে ∂L/∂u, অর্থাৎ u একটু বদলালে loss কত বদলায়। একে বলে upstream gradient. ধাপটি নিজে জানে শুধু নিজের ঢাল ∂u/∂v, যাকে বলে local gradient. দুটি গুণ করলেই ∂L/∂v, যা আরও পেছনের ধাপের upstream gradient হয়ে যায়। কোনো ধাপকে পুরো network-এর খবর রাখতে হয় না।

আগের কোর্সের উদাহরণে sigmoid আর classification ছিল। এবার ভিন্ন আকার: বাজারঘরের ডেলিভারি সময় (দিনে) অনুমান। দুটি input (scale করা দূরত্ব আর পার্সেলের ওজন), ReLU-এর দুটি hidden unit, একটি linear output, আর loss L = ½(ŷ − y)². ½ রাখা হয় যাতে derivative-এ ২ কেটে যায়।

x = (1.0, 2.0), y = 3.0 · W1 = [[0.5, 0.4], [0.25, −0.5]], b1 = (0, 0.1) · W2 = (2.0, −1.0), b2 = 0.5 · η = 0.05

W1-এর প্রতিটি সারি একটি input-এর, প্রতিটি কলাম একটি hidden unit-এর। η (eta) হলো learning rate.

x = (1.0, 2.0), y = 3.0 · ডেলিভারি সময়, দিনে
ধাপ
সূত্র
মান
  1. Forward, hidden. z1 = (১ × ০.৫ + ২ × ০.২৫ + ০, ১ × ০.৪ − ২ × ০.৫ + ০.১) = (১.০, −০.৫). ReLU ঋণাত্মক মান শূন্য করে, তাই h = (১.০, ০). দ্বিতীয় unit এই উদাহরণে বন্ধ। z1 আর h দুটিই মনে রাখা হয়, কারণ backward-এ লাগবে।
  2. Forward, output. ŷ = ২.০ × ১.০ + (−১.০) × ০ + ০.৫ = ২.৫ দিন। আসল সময় ৩ দিন, তাই L = ½ × (−০.৫)² = ০.১২৫.
  3. শুরু loss থেকে। ½(ŷ − y)²-এর local gradient ŷ − y = −০.৫. ঋণাত্মক মানে ŷ বাড়ালে loss কমবে।
  4. W2-এর gradient. ŷ = h·W2 + b2, তাই ∂ŷ/∂W2 = h. upstream × local: (−০.৫ × ১.০, −০.৫ × ০) = (−০.৫, ০). বন্ধ unit-এর weight-এর gradient শূন্য, কারণ তার output শূন্য।
  5. Hidden-এ নামা। ∂ŷ/∂h = W2, তাই upstream gradient δ2·W2 = (−১.০, ০.৫). ReLU-এর local gradient ১ (z > ০ হলে) বা ০, একটি দরজার মতো: (১, ০). উপাদান ধরে গুণ (⊙): δ1 = (−১.০, ০). দ্বিতীয় unit-এর ০.৫ দরজায় আটকে গেল।
  6. W1-এর gradient. ∂W1[i, j] = xi·δ1, j: প্রথম সারি (−১.০, ০), দ্বিতীয় সারি x2 = ২ দিয়ে (−২.০, ০). Central difference দিয়ে W1[1, 0]-এ সংখ্যায় মাপা gradient −১.৯৯৯৯৯৯৯৯৯৯৪, মিলে গেছে; W1[0, 1]-এ মাপা মান ঠিক ০.
  7. হালনাগাদ। প্রতিটি parameter থেকে ০.০৫ × gradient বিয়োগ। নতুন forward-এ z1 = (১.৩, −০.৫), ŷ = ৩.১৫৭৫, loss ০.১২৫ থেকে ০.০১২৪. η = ০.১ নিলে ŷ = ৩.৮৩ হয়ে লক্ষ্য পেরিয়ে যেত, loss ০.৩৪৪৫: বড় ধাপের বিপদ এক উদাহরণেই দেখা যায়।

দুটি জিনিস মনে রাখুন। প্রথমত, প্রতিটি ধাপের কাজ স্থানীয়: ReLU শুধু জানে নিজের দরজা খোলা কি না, linear স্তর শুধু জানে নিজের input আর weight. দ্বিতীয়ত, বন্ধ ReLU unit-এর ভেতরের সব weight এই উদাহরণে কোনো gradient পায় না। কোনো unit যদি সব উদাহরণে বন্ধ থাকে, সে আর কখনো শেখে না; একে বলে dying ReLU, যা পর্ব ০৩ (NumPy-তে multilayer perceptron — forward pass আর activation function)-এ activation-এর আলোচনায় আসে।

খাতায় হাতে কষার কৌশল: প্রথমে forward-এর সব মান (z, h, ŷ, L) একটি টেবিলে লিখুন। তারপর loss থেকে শুরু করে প্রতিটি ধাপে তিনটি জিনিস লিখুন: ওপর থেকে আসা gradient, নিজের local gradient, আর দুটির গুণফল। প্রতিটি gradient-এর পাশে তার shape বা আকার মিলিয়ে নিন (∂W1 একটি 2 × 2, W1-এর মতোই)। শেষে অন্তত একটি ঘর central difference দিয়ে মেলালে হিসাবের ভুল প্রায় সবসময় ধরা পড়ে।

৩ · Batch-এ matrix রূপ — একটি linear স্তরের তিনটি সূত্র

বাস্তবে একবারে B টি উদাহরণ (একটি mini-batch) চলে, প্রতি সারিতে একটি। একটি linear স্তর Z = XW + b, যেখানে X-এর shape (B, nin), W-এর (nin, nout), b-এর (nout,), Z-এর (B, nout). ধরুন ওপর থেকে এসেছে D = ∂L/∂Z, shape (B, nout). তাহলে:

dW = Xᵀ·D · db = ∑সারি D · dX = D·Wᵀ

প্রথমটির যুক্তি এক লাইনে: Zbj = ∑i XbiWij + bj, তাই Wij বদলালে প্রতিটি উদাহরণ b-এর Zbj বদলায় Xbi হারে, আর সব উদাহরণের দায় যোগ হয়: ∂L/∂Wij = ∑b XbiDbj, যা হুবহু (XᵀD)ij. Batch-এর যোগফলটি matrix গুণের ভেতরেই লুকানো। b প্রতিটি সারিতে একই যোগ হয় (broadcasting), তাই তার gradient সব সারির যোগফল। পুরো প্রমাণ আর layout-এর নিয়ম মেশিন লার্নিং কোর্সের পর্ব ১১-এ।

একটি ছোট সংখ্যার উদাহরণ (B = ২, nin = ২, nout = ১): X = [[1, 2], [3, 4]], W = [[0.5], [−1]], আর ওপর থেকে আসা D = [[0.1], [−0.2]]. তাহলে dW = XᵀD = [[1 × 0.1 + 3 × (−0.2)], [2 × 0.1 + 4 × (−0.2)]] = [[−0.5], [−0.6]]; db = ০.১ − ০.২ = −০.১; আর dX = DWᵀ = [[0.05, −0.1], [−0.1, 0.2]]. dW-এর প্রতিটি ঘরে দুই উদাহরণের অবদান যোগ হয়েছে; dX-এর প্রতিটি সারি শুধু নিজের উদাহরণের, কারণ উদাহরণগুলো একে অপরের input-এ প্রভাব ফেলে না।

মূল ধারণা

Shape দিয়ে সূত্র মনে রাখা যায়। dW-এর shape হতেই হবে W-এর মতো (nin, nout). হাতে আছে X (B, nin) আর D (B, nout). B মাত্রাটি “খেয়ে ফেলার” একমাত্র উপায় Xᵀ·D. একইভাবে dX-এর shape (B, nin) পেতে D·Wᵀ. তবে সাবধান: W বর্গাকার হলে ভুল transpose-ও সঠিক shape দেয় (অংশ ৯)।

এবার এই পর্বের network: ২টি input → ১৬ ReLU → ১৬ ReLU → ৩টি শ্রেণি, softmax আর cross-entropy. Activation-এর জন্য নিয়ম আরও সহজ: H = ReLU(Z) হলে ∂L/∂Z = ∂L/∂H ⊙ 1[Z > 0], যেখানে ⊙ মানে উপাদান ধরে গুণ, যাকে বলে Hadamard product.

দুই hidden layer-এর MLP: বাঁয়ে forward, ডানে backward

B examples, 2 → 16 → 16 → 3; every gradient has its parameter's shape

Forward আর backward pass, প্রতিটি ধাপের shape সহ forward ↓ backward ↑ X (B, 2) dW1 = Xᵀ·D1 (2, 16) dX লাগে না Z1 = X·W1 + b1 H1 = ReLU(Z1) (B, 16) D1 = D2·W2ᵀ⊙[Z1>0] dW2 = H1ᵀ·D2 (16, 16) Z2 = H1·W2 + b2 H2 = ReLU(Z2) (B, 16) D2 = D3·W3ᵀ⊙[Z2>0] dW3 = H2ᵀ·D3 (16, 3) Z3 = H2·W3 + b3 P = softmax(Z3) (B, 3) D3 = (P − Y) / B (B, 3) loss L −mean log P[y] শুরু এখান থেকে ∂L/∂L = 1
ছবি ৪.১ — বাঁয়ের প্রতিটি মান (X, Z1, H1, Z2, H2, P) ডানের হিসাবে লাগে; ডটেড রেখা সেই সংযোগ। তাই forward-এর মান memory-তে রাখতে হয় (অংশ ৭)।
স্তরParameter আর তার gradientপেছনে পাঠানো gradientParameter সংখ্যা
১: 2 → 16W1, dW1: (2, 16); b1, db1: (16,)দরকার নেই (input)৩২ + ১৬ = ৪৮
২: 16 → 16W2, dW2: (16, 16); b2, db2: (16,)D1: (B, 16)২৫৬ + ১৬ = ২৭২
৩: 16 → 3W3, dW3: (16, 3); b3, db3: (3,)D2: (B, 16)৪৮ + ৩ = ৫১
মোটসব gradient-এর shape parameter-এর shape-এর সমান; B কোথাও নেই৩৭১

৪ · Softmax আর cross-entropy — কেন gradient হুবহু P − Y

শেষ স্তরের কাঁচা স্কোর Z3, যাকে বলে logits. Softmax প্রতিটি সারিকে সম্ভাবনায় বদলায়, pi = ezi / ∑j ezj, আর loss হলো সঠিক শ্রেণির সম্ভাবনার −ln: L = −∑i yi ln pi, যেখানে y হলো one-hot vector. দুটি ধাপ আলাদা করে differentiate করলে একটি 3 × 3 Jacobian আসে; একসঙ্গে করলে পুরো জিনিসটি একটি বিয়োগে নেমে আসে। নিচে বাজারঘরের এক গ্রাহকের তিনটি logits নিয়ে দুই পথেই হিসাব করা হলো। শ্রেণি তিনটি: নিয়মিত, মৌসুমি আর নতুন গ্রাহক; এই গ্রাহক আসলে নিয়মিত (শ্রেণি ০)।

z = (2.0, 1.0, 0.1), y = (1, 0, 0) · চার দশমিকে
ধাপ
সূত্র
মান
  1. Softmax. e2 = ৭.৩৮৯১, e1 = ২.৭১৮৩, e0.1 = ১.১০৫২; যোগফল ১১.২১২৫. ভাগ করে p = (০.৬৫৯০, ০.২৪২৪, ০.০৯৮৬), যোগফল ১.
  2. Loss আর তার ঢাল। সঠিক শ্রেণি ০, তাই L = −ln ০.৬৫৯০ = ০.৪১৭০. L শুধু p0-এর ওপর নির্ভর করে: ∂L/∂p = (−১/০.৬৫৯০, ০, ০) = (−১.৫১৭৪, ০, ০).
  3. Softmax-এর Jacobian. একটি z বাড়ালে সব p বদলায়, কারণ হর সবার জন্য এক। কর্ণে pi(১ − pi), বাকি ঘরে −pipj. যেমন J[0, 0] = ০.৬৫৯০ × ০.৩৪১০ = ০.২২৪৭. প্রতিটি সারির যোগফল ০, কারণ p-এর যোগফল সবসময় ১।
  4. Jacobian দিয়ে গুণ। Chain rule: ∂L/∂zj = ∑i (∂L/∂pi)·J[i, j]. শুধু i = ০-এর পদ শূন্য নয়, তাই −১.৫১৭৪ × J-এর প্রথম সারি = (−০.৩৪১০, ০.২৪২৪, ০.০৯৮৬). এটি একটি vector-Jacobian product; পুরো 3 × 3 matrix বানাতে হলো।
  5. শর্টকাট। বীজগণিতে: ∑i (−yi/pi)·pi(δij − pj) = −yj + pj∑i yi = pj − yj, কারণ ∑yi = ১. সংখ্যায়ও হুবহু এক। Central difference দিয়ে মাপা মানও (−০.৩৪১০, ০.২৪২৪, ০.০৯৮৬). Jacobian বানানোর দরকারই নেই।
  6. Batch আর ভুল শ্রেণি। Loss যেহেতু B টি উদাহরণের গড়, প্রতিটি সারির gradient-কে B দিয়ে ভাগ: D3 = (P − Y)/B. একই logits-এ সঠিক শ্রেণি ২ হলে loss ২.৩১৭০, আর gradient (০.৬৫৯০, ০.২৪২৪, −০.৯০১৪): ভুল উত্তরে বড় ধাক্কা, কিন্তু প্রতিটি ঘর −১ থেকে ১-এর মধ্যে সীমিত।

এই সীমিত, পরিষ্কার gradient-এর কারণেই classification-এ softmax আর cross-entropy প্রায় সবসময় একসঙ্গে লেখা হয়। Library-গুলোও তাই একসঙ্গে প্রয়োগ করে: PyTorch-এর CrossEntropyLoss softmax নয়, সরাসরি logits নেয় (পর্ব ১৩, Deep learning-এর loss-এ বিস্তারিত)। Sigmoid আর binary cross-entropy-তে একই কাঠামোয় p − y আসে; এটি exponential family আর তার canonical link-এর সাধারণ ধর্ম, যা মেশিন লার্নিং কোর্সের পর্ব ২০-এর বিষয়।

৫ · NumPy-তে পুরো backward pass, আর প্রতিটি parameter-এর gradient check

এবার সব একসঙ্গে। Gradient ঠিক কি না দেখতে আসল ডেটার দরকার নেই; ৬৪টি এলোমেলো input আর এলোমেলো শ্রেণির একটি ছোট batch-ই যথেষ্ট। বাজারঘরের ডেটা আসবে অংশ ৮-এর training-এ।

Gradient check-এ প্রতিটি parameter-এর প্রতিটি ঘর θ একবার +ε, একবার −ε সরিয়ে loss মাপা হয়, আর central difference (L(θ + ε) − L(θ − ε))/2ε দিয়ে সংখ্যায় gradient পাওয়া যায়। তারপর হাতে লেখা gradient ga আর সংখ্যায় মাপা gn মেলানো হয় relative error দিয়ে:

rel = ‖ga − gn‖ / (‖ga‖ + ‖gn‖)

‖·‖ হলো vector-এর দৈর্ঘ্য (সব ঘরের বর্গের যোগফলের বর্গমূল)। শুধু পার্থক্য (absolute error) না দেখে ভাগ করা হয়, কারণ ০.০০১ পার্থক্য ১০০০ মাপের gradient-এ নগণ্য, কিন্তু ০.০০২ মাপের gradient-এ বিশাল। ফল সবসময় ০ থেকে ১-এর মধ্যে। নিচের ব্লক ডেটা বানায়, forward আর backward লেখে, তারপর ছয়টি parameter-এর প্রতিটির relative error ছাপে। bug নামের argument আপাতত খালি; অংশ ৯-এ সেটি দিয়ে ইচ্ছাকৃত ভুল চালু করা হবে। পাতায় প্রথমবার Python চালাতে কয়েক সেকেন্ড লাগে।

import numpy as np
from numpy.linalg import norm
def init(d, h, k, rng):                            # He-ধরনের scale: √(2/fan_in)
    s = lambda i, o: rng.normal(0, np.sqrt(2 / i), (i, o))
    return {"W1": s(d, h), "b1": np.zeros(h), "W2": s(h, h), "b2": np.zeros(h),
            "W3": s(h, k), "b3": np.zeros(k)}
def forward(P, X, bug=""):                         # bug: অংশ ৯-এর ইচ্ছাকৃত ভুলের সুইচ
    Z1 = X @ P["W1"] + P["b1"]; H1 = np.maximum(0, Z1)
    Z2 = H1 @ P["W2"] + P["b2"]; H2 = np.maximum(0, Z2); Z3 = H2 @ P["W3"] + P["b3"]
    if bug == "softmax": E = np.exp(Z3); logP = np.log(E / E.sum(1, keepdims=True))
    else: S = Z3 - Z3.max(1, keepdims=True); logP = S - np.log(np.exp(S).sum(1, keepdims=True))
    return (X, Z1, H1, Z2, H2), logP               # cache: backward-এর জন্য রাখা
def backward(P, cache, logP, Y, bug=""):
    X, Z1, H1, Z2, H2 = cache
    D3 = (np.exp(logP) - Y) / (1 if bug == "nodiv" else len(Y))   # (P − Y)/B
    D2 = (D3 @ P["W3"].T) * (Z2 > 0)               # ReLU-এর দরজা: 0 বা 1
    D1 = (D2 @ P["W2"].T) * (Z1 > 0)
    gW2 = (H1.T @ D2).T if bug == "transpose" else H1.T @ D2
    return {"W1": X.T @ D1, "b1": D1.sum(0), "W2": gW2, "b2": D2.sum(0),
            "W3": H2.T @ D3, "b3": D3.sum(0)}
def loss(P, X, Y, bug=""): return -(forward(P, X, bug)[1] * Y).sum() / len(Y)
def grad_check(P, X, Y, bug="", eps=1e-6):         # central difference, প্রতিটি ঘরে
    G, rel = backward(P, *forward(P, X, bug), Y, bug), {}
    for name, W in P.items():
        num = np.zeros_like(W)
        for i in np.ndindex(W.shape):
            o = W[i]; W[i] = o + eps; lp = loss(P, X, Y, bug)
            W[i] = o - eps; num[i] = (lp - loss(P, X, Y, bug)) / (2 * eps); W[i] = o
        rel[name] = norm(G[name] - num) / (norm(G[name]) + norm(num) + 1e-300)
    return G, rel
rng = np.random.default_rng(42); P = init(2, 16, 3, rng)
Xb, Yb = rng.normal(size=(64, 2)), np.eye(3)[rng.integers(0, 3, 64)]   # এলোমেলো ছোট batch
G, rel = grad_check(P, Xb, Yb)
for name in P: print(f"{name}: param {P[name].shape}, grad {G[name].shape}, rel error {rel[name]:.1e}")

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

W1: param (2, 16), grad (2, 16), rel error 1.2e-09
b1: param (16,), grad (16,), rel error 7.8e-10
W2: param (16, 16), grad (16, 16), rel error 7.9e-10
b2: param (16,), grad (16,), rel error 7.9e-10
W3: param (16, 3), grad (16, 3), rel error 3.8e-10
b3: param (3,), grad (3,), rel error 4.0e-10

ছয়টি parameter-এর প্রতিটির gradient-এর shape তার parameter-এর shape-এর সমান, অংশ ৩-এর টেবিলের মতোই; B (৬৪) কোথাও নেই, কারণ XᵀD গুণের ভেতরেই batch যোগ হয়ে গেছে। Relative error ৩.৮ × ১০−১০ থেকে ১.২ × ১০−৯. ε = ১০−৬-এ central difference-এর নিজের ভুলই প্রায় এই মাপের (সূত্রের ভুল প্রায় ε², float64-এর rounding প্রায় ১০−১৬/ε), তাই backward সঠিক। Parameter ধরে আলাদা মান ছাপার লাভ অংশ ৯-এ দেখবেন: ভুল থাকলে কোন parameter-এ, সেটিও জানা যায়। খরচের তুলনাটিও লক্ষ করুন: সংখ্যায় মাপতে ৩৭১টি ঘরের প্রতিটিতে দুটি করে, মোট ৭৪২টি forward লেগেছে; backprop একটি forward আর একটি backward-এ সব দিয়েছে।

Relative error (float64)সাধারণ অর্থ
10−7-এর কমGradient প্রায় নিশ্চিতভাবে ঠিক
10−7 থেকে 10−4ReLU-এর ভাঁজের কাছে হতে পারে; অন্য বিন্দু বা ছোট ε দিয়ে আবার দেখুন
10−4 থেকে 10−2সন্দেহজনক; ভাঁজ না থাকলে প্রায়ই বাগ
10−2-এর বেশিপ্রায় নিশ্চিত বাগ

সীমাগুলো একটি প্রচলিত অভিজ্ঞতার নিয়ম (যেমন Stanford-এর CS231n কোর্সের নোটে এমন সীমা দেওয়া আছে), কোনো উপপাদ্য নয়। Network গভীর হলে ভুল কিছুটা জমে, তাই গভীর network-এ সামান্য বড় মানও স্বাভাবিক হতে পারে। যাচাই সবসময় float64-এ; float32-এ ε = 10−6 দিলে rounding-এর ভুলই সব ঢেকে দেয় (মেশিন লার্নিং কোর্সের পর্ব ১১-এর তিনটি ফাঁদ দেখুন)।

ঠিক z = ০-তে ReLU-এর ঢাল কত? গাণিতিকভাবে সেখানে derivative নেই; বাঁয়ে ০, ডানে ১. কোডে (Z > 0) লিখলে ০ ধরা হয়, আর PyTorch-ও সাধারণত ০ ধরে। Float-এ ঠিক শূন্যে পড়া বিরল, কিন্তু gradient check-এ θ ± ε যদি কোনো z-কে শূন্যের এক পাশ থেকে আরেক পাশে নিয়ে যায়, সংখ্যায় মাপা gradient দুই ঢালের মাঝামাঝি কিছু দেয়, আর relative error হঠাৎ ১০−৪-এর মতো হয়। তখন বাগ খোঁজার আগে অন্য বিন্দু বা ছোট ε দিয়ে আবার দেখুন।

৬ · খরচের হিসাব — backward কেন forward-এর প্রায় দ্বিগুণ

প্রতিটি linear স্তরের forward-এ একটি matrix গুণ, XW: (B, nin) গুণ (nin, nout), খরচ প্রায় 2·B·nin·nout FLOP (প্রতিটি গুণ-যোগ জোড়া দুটি floating-point হিসাব)। Backward-এ সেই স্তরে দুটি গুণ: XᵀD (dW-এর জন্য) আর DWᵀ (dX-এর জন্য), দুটিরই খরচ সমান। তাই backward ≈ ২ × forward. ব্যতিক্রম প্রথম স্তর: input-এর gradient কারও লাগে না, তাই সেখানে একটি গুণ বাঁচে। ReLU, softmax আর যোগের মতো উপাদান-ধরে হিসাব matrix গুণের তুলনায় সস্তা, তবে ছোট network-এ তাদের ভাগ চোখে পড়ে।

এর মানে, একটি training ধাপ (forward + backward) একটি শুধু-forward ধাপের প্রায় তিন গুণ। কথাটি automatic differentiation-এর একটি পরিচিত ফলাফলের সঙ্গে মেলে: reverse mode-এ পুরো gradient-এর খরচ মূল হিসাবের একটি ছোট ধ্রুব গুণিতক, parameter যতই থাকুক (Andreas Griewank আর Andrea Walther-এর বইয়ে একে “cheap gradient principle” বলা হয়েছে; ধ্রুবকটির সীমা কীভাবে গোনা হয় তার ওপর নির্ভর করে)। নিচের ব্লক একটু বড় network (৩২ → ১২৮ → ১২৮ → ১০, B = ২৫৬)-এ FLOP গোনে, তারপর forward আর backward ২০ বার করে চালিয়ে median সময় মাপে।

খরচটি parameter-এর সংখ্যার ওপর নির্ভর করে না, এটিই backprop-এর আসল শক্তি। Gradient check-এ ৩৭১টি parameter-এর জন্য ৭৪২টি forward লেগেছিল; লক্ষ লক্ষ parameter-এর network-এ সেই পথ অচল। Backprop হলো reverse-mode differentiation: একটি সংখ্যা (loss) থেকে পেছনে হেঁটে সব input-এর gradient. উল্টো দিকের forward mode প্রতিটি input-দিকের জন্য একটি করে pass নেয়, যা অল্প input আর অনেক output-এ সুবিধাজনক। দুটির তুলনা আর নিজের engine পর্ব ০৫-এ; Jacobian-এর দিক থেকে ব্যাখ্যা মেশিন লার্নিং কোর্সের পর্ব ১১-এর অংশ ৮-এ।

import time
def median_time(f, reps=20):                       # একাধিকবার মেপে median
    ts = []
    for _ in range(reps):
        t0 = time.perf_counter(); f(); ts.append(time.perf_counter() - t0)
    return np.median(ts)
B, sizes = 256, [32, 128, 128, 10]                 # একটু বড় network, শুধু মাপার জন্য
Pw = init(32, 128, 10, rng)
Xw, Yw = rng.normal(size=(B, 32)), np.eye(10)[rng.integers(0, 10, B)]
cache, logP = forward(Pw, Xw)
tf = median_time(lambda: forward(Pw, Xw))
tb = median_time(lambda: backward(Pw, cache, logP, Yw))
mm = [2 * B * i * o for i, o in zip(sizes, sizes[1:])]   # প্রতিটি matmul-এর FLOP
fw, bw = sum(mm), 2 * sum(mm) - mm[0]              # প্রথম স্তরে dX বাদ
print(f"FLOP: forward {fw/1e6:.1f}M, backward {bw/1e6:.1f}M, ratio {bw/fw:.2f}")
print(f"time: forward {tf*1e3:.2f} ms, backward {tb*1e3:.2f} ms, ratio {tb/tf:.2f}")
act = sum(a.nbytes for a in cache[1:]) + logP.nbytes
par = sum(v.nbytes for v in Pw.values())
print(f"stored activations {act/1024:.0f} KB, parameters {par/1024:.0f} KB (float64)")

আসল আউটপুট (ব্রাউজারের Python; সময় আপনার যন্ত্র আর ব্রাউজারে আলাদা হবে):

FLOP: forward 11.1M, backward 20.2M, ratio 1.81
time: forward 3.40 ms, backward 6.70 ms, ratio 1.97
stored activations 1044 KB, parameters 172 KB (float64)

হিসাবে backward forward-এর ১.৮১ গুণ: ২-এর চেয়ে একটু কম, কারণ প্রথম স্তরের dX বাদ, আর সেই স্তরের ভাগ ৪০৯৬/২১৭৬০ ≈ ০.১৯. ব্রাউজারে মাপা অনুপাত ১.৯৭ (৩.৪০ বনাম ৬.৭০ মিলিসেকেন্ড)। একই কোড এক desktop-এ (NumPy 2.5.1, বহু thread-এর BLAS) এক run-এ দিয়েছে ০.৬৭ বনাম ০.৯৯ মিলিসেকেন্ড, অনুপাত ১.৪৮. অর্থাৎ FLOP-এর হিসাব একটি ভালো প্রথম অনুমান, কিন্তু আসল সময় নির্ভর করে যন্ত্র, matrix-এর আকার, memory থেকে ডেটা আনার খরচ আর উপাদান-ধরে হিসাবের ওপর; সংখ্যাগুলো একেকটি run, সাধারণ নিয়ম নয়। শেষ লাইনটি পরের অংশের ভূমিকা: B = ২৫৬-এই backward-এর জন্য জমা activation (Z1, H1, Z2, H2, প্রতিটি ২৫৬ × ১২৮ × ৮ byte = ২৫৬ KB, আর log P) ১০৪৪ KB, parameter-এর ১৭২ KB-এর প্রায় ছয় গুণ।

৭ · Memory — কেন forward-এর মান জমিয়ে রাখতে হয়

ছবি ৪.১-এর ডটেড রেখাগুলো আবার দেখুন। dW3 = H2ᵀD3-এর জন্য H2 লাগে, ReLU-এর দরজার জন্য Z2 বা H2 লাগে, আর D3-এর জন্য P. এগুলো forward-এ হিসাব হয়েছিল, কিন্তু backward চলে উল্টো ক্রমে: সবচেয়ে শেষে লাগে X আর Z1, যা সবার আগে তৈরি হয়েছিল। তাই হয় সব জমিয়ে রাখতে হবে, নয়তো আবার হিসাব করতে হবে। নিচের ধাপগুলো B = ৬৪, ২-১৬-১৬-৩ network আর float64 (প্রতি সংখ্যা ৮ byte) ধরে memory-র হিসাব দেখায়।

জমা activation · ১ KB = ১০২৪ byte
ধাপ
জমা / মুক্ত
মোট
  1. প্রথম স্তর। X (৬৪ × ২ × ৮ byte = ১ KB) রাখতে হয় dW1-এর জন্য, Z1 (৮ KB) দরজার জন্য, H1 (৮ KB) dW2-এর জন্য। মোট ১৭ KB.
  2. দ্বিতীয় স্তর। একই কারণে Z2 আর H2, আরও ১৬ KB. Memory বাড়ছে স্তরের সংখ্যা আর B, দুটির সঙ্গেই।
  3. শেষ স্তর আর loss. P (৬৪ × ৩ × ৮ = ১.৫ KB) D3-এর জন্য। এখন memory সর্বোচ্চ, ৩৪.৫ KB: forward শেষ, backward তখনো শুরু হয়নি।
  4. Backward শুরু। D3 = (P − Y)/B আর dW3 = H2ᵀD3 হয়ে গেলে P আর H2 আর লাগবে না; ১.৫ + ৮ = ৯.৫ KB মুক্ত, থাকে ২৫ KB.
  5. মাঝের স্তর। Z2 দিয়ে দরজা, H1 দিয়ে dW2; তারপর দুটিই মুক্ত, থাকে ৯ KB. Activation যে ক্রমে জমেছিল, তার উল্টো ক্রমে খালি হয়, ঠিক একটি stack-এর মতো।
  6. প্রথম স্তর। X আর Z1 দিয়ে dW1, তারপর সব খালি। Parameter আর gradient থেকে যায়, activation নয়।
  7. না রাখার বিকল্প। কিছুই না রেখে প্রতিটি স্তরের backward-এর সময় শুরু থেকে forward আবার চালালে memory প্রায় এক স্তরের সমান, কিন্তু L স্তরে মোট প্রায় L²/2 স্তর-forward. মাঝামাঝি পথ হলো মাঝে মাঝে কয়েকটি বিন্দু রাখা: gradient checkpointing.

এই ছোট network-এ সংখ্যাগুলো হাস্যকর রকম ছোট, কিন্তু অনুপাতটিই আসল। অংশ ৬-এর ব্লক দেখিয়েছে, B = ২৫৬-এই জমা activation parameter-এর চেয়ে কয়েক গুণ বড় হয়ে গেছে। Parameter-এর memory B-এর ওপর নির্ভর করে না, activation memory B-এর সঙ্গে সরাসরি বাড়ে। বড় image বা language model-এ training-এর সময় GPU memory ফুরানোর প্রধান কারণ প্রায়ই এটিই। এজন্য prediction (inference)-এর সময় PyTorch-এ torch.no_grad() দিয়ে এই জমানো বন্ধ রাখা হয়; সেটি পর্ব ০৮ (PyTorch autograd)-এর বিষয়।

আরও গভীরে: Tianqi Chen ও সহকর্মীরা ২০১৬ সালের “Training Deep Nets with Sublinear Memory Cost” প্রবন্ধে দেখান, n স্তরকে প্রায় √n দৈর্ঘ্যের খণ্ডে ভাগ করে শুধু খণ্ডের সীমায় activation রাখলে memory প্রায় O(√n)-এ নামে, বিনিময়ে প্রায় একটি বাড়তি forward. আরেক পথ উল্টানো যায় এমন স্তর (Aidan Gomez ও সহকর্মীদের RevNet, ২০১৭), যেখানে output থেকে input আবার হিসাব করা যায়, তাই রাখতেই হয় না। ছোট একটি কৌশল এই পাতাতেই চোখে পড়ে: ReLU-এর দরজার জন্য Z আর H দুটি রাখার দরকার নেই, কারণ Z > ০ আর H > ০ একই শর্ত। PyTorch-এ torch.utils.checkpoint আছে; পর্ব ১২ (GPU, mixed precision আর গতি)-এ memory মাপার প্রসঙ্গ আসবে।

৮ · পুরো training — বাজারঘরের তিন দলের গ্রাহক, mini-batch SGD

Gradient ঠিক জেনে এবার train করা যায়। ডেটা বাজারঘরের ৬০০ জন বানানো গ্রাহক, দুটি scale করা feature (ধরুন মাসে অর্ডারের হার আর গড় বাস্কেটের মাপ), তিন দল (নিয়মিত, মৌসুমি, নতুন) তিনটি পেঁচানো বাহুর মতো সাজানো, যাতে কোনো সরল রেখা আলাদা করতে না পারে। আসল গ্রাহকের ডেটা এমন পরিষ্কার আকারে আসে না; এটি ধারণা দেখানোর খেলনা। প্রতিটি epoch-এ ৪৮০টি train বিন্দু এলোমেলো করে ৩২টির ১৫টি mini-batch; প্রতিটি mini-batch-এ forward, backward আর একটি হালনাগাদ θ ← θ − η·dθ, η = ০.২. এটিই stochastic gradient descent (SGD); এর তত্ত্ব আর learning-rate schedule মেশিন লার্নিং কোর্সের পর্ব ১৩-এ। প্রতিটি epoch শেষে train আর validation দুই দিকের loss আর accuracy রাখা হয়।

import matplotlib.pyplot as plt
rng = np.random.default_rng(42)                    # বাজারঘরের ৬০০ গ্রাহক, ৩টি দল (বানানো)
y = np.repeat(np.arange(3), 200); t = rng.uniform(0.1, 1, 600)
ang = y * 2 * np.pi / 3 + 3 * t + rng.normal(0, 0.35, 600)
X, Y = np.column_stack([t * np.cos(ang), t * np.sin(ang)]), np.eye(3)[y]
tr, va = np.split(rng.permutation(600), [480])     # ৪৮০ train, ১২০ validation
rng = np.random.default_rng(0); P = init(2, 16, 3, rng); lr, bs = 0.2, 32
acc = lambda P, I: (forward(P, X[I])[1].argmax(1) == y[I]).mean()
hist = []
for ep in range(100):
    for b in np.array_split(rng.permutation(tr), len(tr) // bs):   # ১৫টি mini-batch
        G = backward(P, *forward(P, X[b]), Y[b])
        for name in P: P[name] -= lr * G[name]                      # SGD-এর এক ধাপ
    hist.append((loss(P, X[tr], Y[tr]), loss(P, X[va], Y[va]), acc(P, tr), acc(P, va)))
H = np.array(hist)
for e in (1, 10, 50, 100):
    print(f"epoch {e:3d}: loss train {H[e-1,0]:.3f} val {H[e-1,1]:.3f} | acc train {H[e-1,2]:.3f} val {H[e-1,3]:.3f}")
g1, g2 = np.meshgrid(np.linspace(-1.1, 1.1, 120), np.linspace(-1.1, 1.1, 120))
cls = forward(P, np.column_stack([g1.ravel(), g2.ravel()]))[1].argmax(1).reshape(g1.shape)
fig, ax = plt.subplots(1, 3, figsize=(11, 3.3), layout="constrained")
ax[0].plot(H[:, 0], label="train"); ax[0].plot(H[:, 1], label="validation")
ax[0].set(title="Cross-entropy loss", xlabel="epoch", yscale="log"); ax[0].legend()
ax[1].plot(H[:, 2], label="train"); ax[1].plot(H[:, 3], label="validation")
ax[1].set(title="Accuracy", xlabel="epoch", ylim=(0.5, 1.01)); ax[1].legend()
ax[2].contourf(g1, g2, cls, levels=[-0.5, 0.5, 1.5, 2.5], colors=["#bfdbfe", "#fde68a", "#bbf7d0"])
ax[2].scatter(X[va, 0], X[va, 1], c=y[va], cmap="viridis", s=12)
ax[2].set(title="Decision regions (validation points)", xlabel="x1", ylabel="x2")
plt.show()

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4; ছবি তিনটি ব্লকের নিচে আঁকা হয়):

epoch   1: loss train 0.750 val 0.715 | acc train 0.760 val 0.792
epoch  10: loss train 0.256 val 0.244 | acc train 0.921 val 0.933
epoch  50: loss train 0.025 val 0.099 | acc train 0.996 val 0.975
epoch 100: loss train 0.008 val 0.063 | acc train 1.000 val 0.958

শুরুর আগে loss ln ৩ ≈ ১.০৯৯-এর কাছে থাকার কথা; প্রথম epoch-এর ১৫টি ধাপেই তা ০.৭৫০-এ নেমেছে। ১০ epoch-এ train আর validation দুই দিকের accuracy প্রায় ৯২–৯৩%. ৫০ epoch-এর পর train loss প্রায় শূন্যের দিকে (১০০-তে ০.০০৮, train accuracy ১.০০০), কিন্তু validation loss ০.০৬৩-এ, অর্থাৎ ফাঁক তৈরি হচ্ছে। একটি সূক্ষ্ম বিষয়: ৫০ থেকে ১০০ epoch-এ validation loss কমেছে (০.০৯৯ → ০.০৬৩), অথচ validation accuracy কমেছে (০.৯৭৫ → ০.৯৫৮, মানে ১২০টির মধ্যে ১১৭টি থেকে ১১৫টি)। Loss মাপে সম্ভাবনা কতটা ঠিক, accuracy মাপে শুধু সবচেয়ে বড় সম্ভাবনার শ্রেণি; দুটি সবসময় একসঙ্গে চলে না, আর ১২০টি বিন্দুতে দুটি বিন্দুর পার্থক্য সহজেই এলোমেলোতার ফল হতে পারে। বাঁয়ের ছবিতে loss log scale-এ, মাঝে accuracy, ডানে তিন দলের সীমানা পেঁচানো বাহু ধরে বেঁকে গেছে। সংখ্যাগুলো একটি seed-এর একটি run.

৯ · তিনটি চেনা বাগ — gradient check দিয়ে ধরা, তারপর সারানো

Backprop-এর বাগ ভয়ংকর, কারণ কোড ভাঙে না। Loss কমে, accuracy ছাপা হয়, শুধু ফল একটু খারাপ বা training একটু অদ্ভুত। নিচে তিনটি সবচেয়ে চেনা ভুল, অংশ ৫-এর bug সুইচ দিয়ে:

  • nodiv, batch-এর আকারে ভাগ ভুলে যাওয়া: loss গড় (÷B), কিন্তু gradient যোগফল। প্রতিটি gradient B গুণ বড়; learning rate আসলে B গুণ বেড়ে গেল, আর batch বদলালেই আচরণ বদলায়।
  • transpose, ভুল transpose: dW2 = (H1ᵀD2)ᵀ. W2 বর্গাকার (১৬ × ১৬), তাই shape মিলে যায়, কোনো error আসে না।
  • softmax, অস্থির softmax: সরাসরি ez. Logits বড় হলে overflow হয়ে inf, তারপর inf/inf = nan. এখানে বড় logits বানাতে input ১০০০ দিয়ে গুণ করা হয়েছে, যেন কেউ গড় বাস্কেটের মাপ scale না করে টাকায় দিয়েছেন।

শেষ সারিতে একই বড় input-এ ঠিক করা, অর্থাৎ log-sum-exp কৌশলের softmax (প্রতিটি সারি থেকে সর্বোচ্চ মান বিয়োগ, পর্ব ০৩-এ বিস্তারিত)। অন্য দুটি বাগের ঠিক রূপ অংশ ৫-এর ফলাফলই।

Pc = init(2, 16, 3, np.random.default_rng(7)); Xb, Yb = X[tr[:64]], Y[tr[:64]]
for bug, Xc in [("nodiv", Xb), ("transpose", Xb), ("softmax", Xb * 1000), ("", Xb * 1000)]:
    G, rel = grad_check(Pc, Xc, Yb, bug)
    print(f"{bug or 'fixed softmax'}: loss {loss(Pc, Xc, Yb, bug):.4g}")
    print("  " + " ".join(f"{k} {v:.1e}" for k, v in rel.items()))
print("nodiv-এর অনুমান (B-1)/(B+1) =", round(63 / 65, 4))

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

nodiv: loss 1.052
  W1 9.7e-01 b1 9.7e-01 W2 9.7e-01 b2 9.7e-01 W3 9.7e-01 b3 9.7e-01
transpose: loss 1.052
  W1 2.6e-09 b1 1.4e-09 W2 7.4e-01 b2 1.9e-09 W3 1.2e-09 b3 6.9e-10
softmax: loss nan
  W1 nan b1 nan W2 nan b2 nan W3 nan b3 nan
fixed softmax: loss 78.86
  W1 1.5e-10 b1 6.4e-08 W2 1.4e-10 b2 4.7e-08 W3 4.7e-11 b3 1.6e-08
nodiv-এর অনুমান (B-1)/(B+1) = 0.9692

প্রথমে লক্ষ করুন, nodiv আর transpose-এ loss একই, ১.০৫২: forward-এ কোনো ভুল নেই, তাই loss ছাপিয়ে backward-এর বাগ কখনো ধরা যায় না। nodiv-এ ছয়টি parameter-এরই relative error ৯.৭ × ১০−১, ঠিক (B − ১)/(B + ১) = ৬৩/৬৫ = ০.৯৬৯২, কারণ D3-এর ভুল সব স্তরে ছড়িয়ে যায়। transpose-এ শুধু W2-এর ০.৭৪, বাকি পাঁচটি ১০−৯-এর ঘরে: বাগ কোথায়, check নিজেই দেখিয়ে দিল। D1 হিসাব হয় W2 দিয়ে, dW2 দিয়ে নয়, তাই ভুলটি আর ছড়ায়নি। অস্থির softmax-এ loss আর সব relative error nan: কিছু সারিতে ez = inf, inf/inf = nan, আর কিছু সম্ভাবনা ০ হয়ে log ০ = −inf. ঠিক করা রূপে একই বড় input-এ loss ৭৮.৮৬ (logits বিশাল, তাই অনেক ভুল উত্তরে চরম আত্মবিশ্বাস), কিন্তু সব relative error ৬.৪ × ১০−৮ বা তার কম; loss বড় হওয়ায় rounding-এর ভাগ একটু বেড়েছে, তবু সীমার ভেতরে।

nodiv বাগের একটি মজার দিক: এটি আসলে learning rate-কে B গুণ বাড়িয়ে দেয়। অংশ ৮-এর training-এ B = ৩২ আর η = ০.২ ছিল; ÷B ভুলে গেলে কার্যত η = ৬.৪. একই seed-এ desktop-এ এক run-এ চালিয়ে দেখা গেছে, train loss ১০০ epoch পরেও ১.১৯-এর মতো, ১.১ থেকে ১.৭-এর মধ্যে লাফাচ্ছে, যেখানে ঠিক কোডে ০.০০৮. কেউ তখন η কমিয়ে ০.০০৬-এ এনে “ঠিক” করে ফেলতে পারেন, আর বাগটি লুকিয়ে থাকে, যতক্ষণ না কেউ batch-এর আকার বদলায়। Gradient check এই লুকোচুরি চলতে দেয় না।

ব্রাউজারের Python-এ np.exp-এর overflow নিঃশব্দে inf দেয়, কারণ WebAssembly-তে floating-point-এর সতর্ক-সংকেত নেই। নিজের কম্পিউটারে একই কোড চালালে NumPy সতর্ক করে। সত্যিকারের run (desktop, Python 3.14, NumPy 2.5.1; path ছোট করা হয়েছে):

$ python planted_bugs.py
planted_bugs.py:10: RuntimeWarning: overflow encountered in exp
  if bug == "softmax": E = np.exp(Z3); logP = np.log(E / E.sum(1, keepdims=True))
planted_bugs.py:10: RuntimeWarning: invalid value encountered in divide
  if bug == "softmax": E = np.exp(Z3); logP = np.log(E / E.sum(1, keepdims=True))
planted_bugs.py:10: RuntimeWarning: divide by zero encountered in log
  if bug == "softmax": E = np.exp(Z3); logP = np.log(E / E.sum(1, keepdims=True))
planted_bugs.py:21: RuntimeWarning: invalid value encountered in multiply
  def loss(P, X, Y, bug=""): return -(forward(P, X, bug)[1] * Y).sum() / len(Y)
…
softmax: loss nan
…

এজন্য warning কখনো চুপ করাবেন না: desktop-এ এটিই প্রথম সংকেত। আর loss-এ nan দেখলে সবার আগে softmax, log আর ভাগের জায়গাগুলো খুঁজুন।

১০ · AI-এর যুগে — AI-এর লেখা backprop আর derivation যাচাই

AI assistant-কে “backprop লিখে দাও” বললে কয়েক সেকেন্ডে চলনসই কোড আসে। কিন্তু অংশ ৯ দেখিয়েছে, ভুল backprop-ও চলে। আর derivation চাইলে AI প্রায়ই সুন্দর, আত্মবিশ্বাসী, কিন্তু মাঝপথে একটি transpose বা ১/B হারানো ব্যাখ্যা দেয়, কখনো অস্তিত্বহীন প্রবন্ধের নামও।

AI যা প্রায়ই করেকীভাবে ধরবেন
Loss গড়, gradient যোগফল (বা উল্টো)Gradient check; relative error (B − 1)/(B + 1)-এর কাছে হলে এটিই
বর্গাকার weight-এ ভুল transposeParameter ধরে আলাদা relative error; শুধু একটি parameter ব্যর্থ হলে সেখানেই খুঁজুন
Softmax-এ max বিয়োগ নেই, বা log(softmax(z)) আলাদা করেবড় logits (input × 1000) দিয়ে পরীক্ষা; loss-এ nan বা inf আছে কি না
PyTorch-এ model-এর শেষে softmax, তারপর CrossEntropyLossনিজের version-এর অফিসিয়াল docs: এটি logits নেয়
Gradient check float32-এ, বা ε = 10−12float64, ε প্রায় 10−6; ছোট ডেটায়, এলোমেলো বিন্দুতে
একটি seed-এর একটি run দেখে “model কাজ করে”কয়েকটি seed, validation curve, আর সরল baseline-এর সঙ্গে তুলনা

নির্দিষ্ট করে চান, আর এমন আউটপুট চান যা নিজে যাচাই করা যায়। আসল গ্রাহকের ডেটা পেস্ট করবেন না; বানানো ডেটাই যথেষ্ট।

NumPy 2 দিয়ে (কোনো deep learning library ছাড়া) একটি MLP-এর backward pass লেখো: input → ReLU → ReLU → softmax, loss হবে mean cross-entropy. শর্ত: ১. প্রতিটি gradient-এর সূত্রের পাশে comment-এ shape লিখবে, যেমন dW2 = H1.T @ D2 # (h, B) @ (B, h) -> (h, h). ২. softmax আর log-softmax হবে log-sum-exp কৌশলে; logits × 1000 দিলেও nan আসবে না, এমন একটি test রাখবে। ৩. central difference (ε = 1e-6, float64) দিয়ে প্রতিটি parameter-এর আলাদা relative error ছাপাবে। ৪. ∂L/∂Z = (P − Y)/B কোথা থেকে এলো, ৩ শ্রেণির একটি সংখ্যার উদাহরণে ধাপে ধাপে দেখাবে। ৫. কোনো প্রবন্ধ বা বইয়ের নাম দিলে শুধু সেগুলোই দেবে যা তুমি নিশ্চিত জানো; অনিশ্চিত হলে বলবে।

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

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

  • Z = XW + b হলে dW, db, dX? dW = XᵀD, db = D-এর সারি-যোগ, dX = DWᵀ; D = ∂L/∂Z.
  • Softmax + cross-entropy-র logits gradient? (P − Y)/B; প্রমাণ: Jacobian pi(δij − pj) দিয়ে গুণ, ∑y = ১ ব্যবহার করে।
  • Gradient check-এ relative error কেন, absolute নয়? Gradient-এর মাপ থেকে স্বাধীন হতে; float64-এ ঠিক কোডে সাধারণত 10−7-এর কম।
  • Backward-এর খরচ? প্রতিটি স্তরে দুটি matrix গুণ (dW আর dX), তাই প্রায় ২ × forward; এক training ধাপ প্রায় ৩ × forward.
  • B দিয়ে ভাগ ভুলে গেলে relative error? (B − 1)/(B + 1); B = ৬৪-এ ০.৯৬৯২.

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

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

প্র ০১Backpropagation কে আবিষ্কার করেছেন? ১৯৮৬-র আগেই গণিত থাকলে neural network-এ তা জনপ্রিয় হতে এত সময় লাগল কেন?

ছোট উত্তর: একজন নন। Chain rule তো সপ্তদশ শতকের ক্যালকুলাসের অংশ। একটি প্রোগ্রামের output থেকে পেছনে হেঁটে সব input-এর derivative একবারে পাওয়ার পদ্ধতি, অর্থাৎ reverse-mode automatic differentiation, সাধারণত Seppo Linnainmaa-র ১৯৭০ সালের master's thesis-এর সঙ্গে যুক্ত করা হয়। Control theory-তে ১৯৬০-এর দশকে Henry Kelley, Arthur Bryson প্রমুখের কাজে একই ধাঁচের পেছনমুখী হিসাব ছিল বলে অনেক ইতিহাস-লেখক উল্লেখ করেন। Neural network-এ প্রয়োগের প্রস্তাব Paul Werbos-এর ১৯৭৪ সালের PhD thesis-এ, আর আরও স্পষ্টভাবে ১৯৮০-এর দশকের শুরুতে তাঁর কাজে পাওয়া যায়।

তাহলে ১৯৮৬ কেন? Rumelhart, Hinton আর Williams-এর Nature-এর প্রবন্ধ শুধু পদ্ধতি দেয়নি, দেখিয়েছিল hidden unit নিজে থেকে অর্থবহ feature শেখে, যেমন পরিবারের সম্পর্কের ছোট একটি সমস্যায়। একই সময়ে প্রকাশিত Parallel Distributed Processing বই এই চিন্তাকে মনোবিজ্ঞান আর cognitive science-এর বড় পাঠকগোষ্ঠীর কাছে পৌঁছে দেয়। অর্থাৎ পার্থক্যটা গণিতে নয়, প্রদর্শনে আর প্রচারে।

দেরির আরও কারণ: ১৯৭০-এর দশকে কম্পিউটার ধীর, ডেটা কম, আর ১৯৬৯-এর Minsky-Papert-এর বইয়ের পর neural network নিয়ে উৎসাহ কমে গিয়েছিল (কতটা দায় বইটির, তা নিয়ে মতভেদ আছে)। কৃতিত্ব নিয়ে বিতর্ক আজও চলে; Jürgen Schmidhuber-এর মতো কয়েকজন গবেষক বারবার লিখেছেন যে আগের কাজ যথেষ্ট উদ্ধৃত হয়নি, অন্যরা বলেন ধারণা আর কার্যকর প্রদর্শন আলাদা অবদান।

মূল উপলব্ধি: বিজ্ঞানে একটি ধারণা “আবিষ্কার” আর “কাজে লাগানো” প্রায়ই আলাদা মানুষের হাতে, আলাদা দশকে ঘটে। ইতিহাস লিখলে একজনের নাম না দিয়ে শিকলটি বলুন।

প্র ০২আমাদের মস্তিষ্ক কি backpropagation করে? না করলে নাম “neural network” কেন?

সৎ উত্তর: জানা নেই, আর আক্ষরিক অর্থে backprop মস্তিষ্কে চলে বলে কোনো দৃঢ় প্রমাণ নেই। আপত্তিগুলো পুরোনো। ১৯৮৯ সালে Francis Crick Nature-এ লিখেছিলেন, backprop জীববিজ্ঞানের দিক থেকে অবাস্তব মনে হয়। কয়েকটি সুনির্দিষ্ট সমস্যা:

  • Weight transport: backward-এ D·Wᵀ লাগে, অর্থাৎ পেছনের পথকে সামনের পথের হুবহু weight জানতে হয়। মস্তিষ্কের synapse সাধারণত এক দিকে সংকেত পাঠায়; উল্টো দিকে একই শক্তির আলাদা সংযোগ কীভাবে থাকবে, তা স্পষ্ট নয়।
  • আলাদা দুই পর্যায়: backprop-এ পুরো forward শেষ করে, সব activation জমিয়ে, তারপর backward. মস্তিষ্ক অবিরাম চলে।
  • সংকেতের ধরন: backprop-এ ধনাত্মক-ঋণাত্মক সূক্ষ্ম সংখ্যা যায়; neuron মূলত spike দিয়ে কথা বলে।

তবে গবেষণা থেমে নেই। Timothy Lillicrap ও সহকর্মীরা (২০১৬, Nature Communications) দেখান, backward-এ Wᵀ-এর বদলে একটি এলোমেলো স্থির matrix ব্যবহার করলেও কিছু কাজে network শেখে (feedback alignment). Predictive coding-এর মতো আরও কিছু প্রস্তাব আছে, যেগুলো কিছু শর্তে backprop-এর কাছাকাছি ফল দেয় বলে দাবি করা হয়। এগুলো সক্রিয় বিতর্কের বিষয়।

নামটি ঐতিহাসিক। ১৯৪৩-এর McCulloch-Pitts neuron আর ১৯৫৮-এর perceptron জীববিজ্ঞান থেকে অনুপ্রাণিত ছিল (পর্ব ০২-এ এই উপমার সীমা আলোচিত)। আধুনিক network-এর বেশিরভাগ নকশা, যেমন ReLU, softmax, attention, প্রকৌশলের সুবিধা থেকে এসেছে, মস্তিষ্ক থেকে নয়।

মূল উপলব্ধি: “মস্তিষ্কের মতো শেখে” কথাটি বিজ্ঞাপনের ভাষা। যা বলা যায়: এটি chain rule দিয়ে একটি loss কমানোর কার্যকর গাণিতিক পদ্ধতি; মস্তিষ্ক কীভাবে শেখে, তা আলাদা, খোলা প্রশ্ন।

প্র ০৩Gradient check পাস করেছে। তাহলে কি training কোড নির্ভুল?

না। Gradient check একটি নির্দিষ্ট প্রশ্নের উত্তর দেয়: “আপনি যে loss function লিখেছেন, তার gradient কি আপনার backward ঠিকঠাক দিচ্ছে?” এর বাইরে অনেক কিছুই সে দেখে না।

  • ভুল loss: loss-এ নিজেই ভুল থাকলে (যেমন ভুল শ্রেণির log নেওয়া, বা label এক ঘর সরে যাওয়া), forward আর backward দুটোই একই ভুল জিনিসের হিসাব করে; check পাস করে।
  • ডেটার ভুল: leakage, ভুল ভাগ, ভুল normalization; gradient এগুলোর খবর রাখে না।
  • একটি বিন্দু: check একটি parameter-বিন্দুতে, একটি ছোট batch-এ। Training-এর পরে কোনো unit মরে গেলে বা logits বড় হলে আলাদা সমস্যা দেখা দিতে পারে, যেমন অংশ ৯-এর softmax বাগ ছোট input-এ ধরা পড়ে না।
  • এলোমেলো স্তর: dropout-এর মতো এলোমেলো কিছু থাকলে +ε আর −ε-এর দুই forward-এ আলাদা mask পড়ে; seed বা mask স্থির না রাখলে check অর্থহীন।
  • Optimizer আর হালনাগাদ: P -= lr * G-এর বদলে P += lr * G লিখলে gradient ঠিক, training উল্টো।

তাই পেশাদার অভ্যাসে gradient check একটি স্তর মাত্র। সঙ্গে থাকে: শুরুর loss প্রায় ln k কি না (তিন শ্রেণিতে ১.০৯৯), এক ছোট batch-এ loss প্রায় শূন্যে নামানো যায় কি না, train আর validation curve, আর সরল baseline-এর সঙ্গে তুলনা। পর্ব ১৮ (Training ডিবাগ করা)-এ এই তালিকা বিস্তারিত আসবে। আর PyTorch-এর মতো autograd library ব্যবহার করলে backward সাধারণত আপনাকে লিখতে হয় না, কিন্তু নিজে custom operation লিখলে torch.autograd.gradcheck ঠিক এই কাজটিই করে।

মূল উপলব্ধি: প্রতিটি পরীক্ষা নির্দিষ্ট এক ধরনের ভুল ধরে। কোনটি কী ধরে, আর কী ধরে না, জানাটাই যাচাইয়ের দক্ষতা।

প্র ০৪বড় model train করতে গিয়ে GPU memory ফুরিয়ে যায়, অথচ parameter অত বেশি নয়। Memory কোথায় যায়, আর কী করা যায়?

Training-এর memory-র চারটি বড় ভাগ: parameter, তাদের gradient, optimizer-এর অবস্থা (যেমন Adam-এ প্রতিটি parameter-এর জন্য দুটি গড়, অর্থাৎ parameter-এর দ্বিগুণ; Adam-এর গণিত মেশিন লার্নিং কোর্সের পর্ব ১৪-এ), আর activation. প্রথম তিনটি batch-এর আকারের ওপর নির্ভর করে না। Activation করে, এবং স্তর, প্রস্থ আর (language model-এ) sequence-এর দৈর্ঘ্যের সঙ্গেও বাড়ে। অংশ ৬-এর ছোট network-এই B = ২৫৬-এ activation parameter-এর কয়েক গুণ হয়ে গেছে।

উপায়গুলো সবই কোনো না কোনো দাম দিয়ে memory কেনে:

  • ছোট batch আর gradient accumulation: কয়েকটি ছোট batch-এর gradient যোগ করে তারপর একবার হালনাগাদ। Memory কমে, সময় বাড়ে; BatchNorm-এর মতো batch-নির্ভর স্তরে আচরণও বদলাতে পারে।
  • Gradient checkpointing: Chen ও সহকর্মীদের (২০১৬) পদ্ধতিতে কিছু activation ফেলে দিয়ে backward-এর সময় আবার হিসাব। বাড়তি খরচ মোটামুটি একটি forward.
  • কম precision: float32-এর বদলে float16 বা bfloat16-এ activation রাখলে memory প্রায় অর্ধেক; numerical stability-র দিকে সতর্ক থাকতে হয় (পর্ব ১২)।
  • উল্টানো যায় এমন স্তর: RevNet-এর মতো নকশায় activation রাখতেই হয় না, কিন্তু স্থাপত্য সীমিত হয়।

এখানে একটি সাধারণ প্রকৌশল-নীতি কাজ করছে: সময় আর জায়গার বিনিময়। Dynamic programming-এ সব উপ-উত্তর মনে রাখা (memoization) আর আবার হিসাব করার মধ্যে যে বাছাই, এখানেও তাই।

মূল উপলব্ধি: Backprop-এর দাম শুধু FLOP নয়, memory-ও। “কত parameter” প্রশ্নের পাশাপাশি “কত activation জমছে” জিজ্ঞেস করুন।

প্র ০৫Softmax-এর output-এ squared error (MSE) না দিয়ে cross-entropy কেন? Gradient-এর ভাষায় পার্থক্যটা কোথায়?

দুটি loss-ই ভুল অনুমানে বড় হয়, কিন্তু gradient-এর আচরণ খুব আলাদা। Cross-entropy-তে logits-এর gradient p − y, সরল আর সীমিত: অনুমান যত ভুল, ধাক্কা তত বড়, সর্বোচ্চ ১-এর কাছে।

MSE-তে L = ½∑(pi − yi)² হলে ∂L/∂p = p − y, কিন্তু তারপর softmax-এর Jacobian দিয়ে গুণ করতে হয়, যার প্রতিটি ঘরে pi(δij − pj)-এর মতো পদ। সমস্যা দেখা দেয় ঠিক যখন network আত্মবিশ্বাসী কিন্তু ভুল: ধরুন সঠিক শ্রেণির p প্রায় ০, আর ভুল শ্রেণির প্রায় ১. তখন Jacobian-এর প্রায় সব ঘর শূন্যের কাছে, তাই gradient-ও প্রায় শূন্য। Network সবচেয়ে বড় ভুলে সবচেয়ে কম শেখে। Cross-entropy-র −ln p পদটি ঠিক এই ছোট হয়ে যাওয়াকে কেটে দেয়; দুই শ্রেণির sigmoid-এ একই ঘটনা p(১ − p) গুণকের সঙ্গে।

দ্বিতীয় যুক্তি সম্ভাবনার: cross-entropy কমানো মানে ডেটার likelihood বাড়ানো (maximum likelihood), তাই output-কে সম্ভাবনা হিসেবে পড়ার একটি নীতিগত ভিত্তি আছে। Logistic regression আর GLM-এ এটিই canonical পছন্দ।

ইতিহাসে, ১৯৮০-এর দশকের অনেক network-এ squared error ব্যবহার হতো। পরে classification-এ cross-entropy প্রায় সর্বজনীন হয়ে যায়; কেউ কেউ এর পক্ষে পরীক্ষামূলক তুলনাও প্রকাশ করেছেন। তবে উল্টো দিকের গবেষণাও আছে: কিছু বড় পরীক্ষায় ঠিকঠাক tuning-সহ squared loss-ও classification-এ কাছাকাছি ফল দিয়েছে বলে রিপোর্ট আছে। তাই “MSE সবসময় খারাপ” না বলে বলুন, softmax-এর পরে MSE দিলে gradient কোথায় দুর্বল হয়, সেটি জেনে বাছাই করুন। পর্ব ১৩ (Deep learning-এর loss)-এ label smoothing-সহ আরও loss আসবে।

মূল উপলব্ধি: Loss বাছাই মানে gradient বাছাই। একটি loss কেমন শেখাবে, তা বুঝতে তার gradient-এর সূত্র লিখে দেখুন, বিশেষ করে “আত্মবিশ্বাসী কিন্তু ভুল” অবস্থায়।

অনুশীলন

প্রথম দুটি খাতা-কলমে, তৃতীয়টি সমালোচনা, শেষটি কোড।

  1. হাতে backprop: অংশ ২-এর ২-২-১ network-এ সব weight একই রেখে আসল ডেলিভারি সময় y = ২.০ দিন ধরুন। L, δ2, ∂W2, ∂b2, δ1, ∂W1 বের করুন। W1-এর দ্বিতীয় কলাম কেন আবার শূন্য gradient পায়?

    Forward বদলায় না: h = (১.০, ০), ŷ = ২.৫. এবার L = ½ × (০.৫)² = ০.১২৫, δ2 = ২.৫ − ২.০ = ০.৫. ∂W2 = ০.৫ × (১.০, ০) = (০.৫, ০), ∂b2 = ০.৫. upstream δ2·W2 = (১.০, −০.৫), দরজা (১, ০), তাই δ1 = (১.০, ০). ∂W1 = [[১.০, ০], [২.০, ০]], ∂b1 = (১.০, ০). সব চিহ্ন উল্টে গেছে, কারণ এবার অনুমান বেশি; gradient descent ŷ কমানোর দিকে যাবে। দ্বিতীয় hidden unit-এর z = −০.৫ < ০, তাই ReLU-এর local gradient ০; y যা-ই হোক, এই উদাহরণে তার weight নড়বে না।

  2. আউটপুট অনুমান করুন: নিচের কোড কী ছাপাবে? প্রথম লাইনের shape আর দ্বিতীয় লাইনের সংখ্যা (চার দশমিকে) চালানোর আগে লিখুন।
    import numpy as np
    rng = np.random.default_rng(0)
    B, h, k = 16, 8, 3
    H, D, W = rng.normal(size=(B, h)), rng.normal(size=(B, k)), rng.normal(size=(h, k))
    print((H.T @ D).shape, (D @ W.T).shape, D.sum(0).shape)
    g = rng.normal(size=5)                       # সঠিক gradient
    bad = B * g                                  # ÷B ভুলে গেলে
    print(round(np.linalg.norm(bad - g) / (np.linalg.norm(bad) + np.linalg.norm(g)), 4))
    (8, 3) (16, 8) (3,)
    0.8824

    HT·D হলো (8, 16)·(16, 3) = (8, 3), একটি W-এর gradient-এর shape; D·WT = (16, 3)·(3, 8) = (16, 8), আগের স্তরে পাঠানো gradient; D-এর কলাম-যোগ (3,), bias-এর gradient. দ্বিতীয় লাইন: bad = Bg হলে ‖Bg − g‖ = (B − ১)‖g‖ আর হর (B + ১)‖g‖, তাই ফল (B − ১)/(B + ১) = ১৫/১৭ = ০.৮৮২৪, g যা-ই হোক। (Desktop-এ Python 3.14 আর NumPy 2.5.1-এ চালিয়ে মেলানো হয়েছে।)

  3. সমালোচনা করুন: একজন সহকর্মী লিখেছেন, “আমার backprop-এর gradient check করেছি: float32-এ, ε = 10−10, পুরো ১০,০০০ সারির ডেটায়, প্রথম ১০টি weight-এ, input সব পূর্ণসংখ্যা আর bias শূন্য। Absolute পার্থক্য 10−3, তাই ঠিক আছে।” এই পরিকল্পনায় কী কী সমস্যা, আর আপনি কীভাবে করতেন?
    • float32 আর ছোট ε: float32-এ প্রায় ৭ অঙ্কের নির্ভুলতা; ε = 10−10-এ θ + ε আর θ প্রায়ই একই সংখ্যা, পার্থক্য হয় শূন্য নয়তো rounding-এর আবর্জনা। float64 আর ε প্রায় 10−6.
    • Absolute পার্থক্য: gradient-এর মাপ না জেনে 10−3 ভালো না খারাপ বলা যায় না; relative error দেখুন।
    • শুধু প্রথম ১০টি weight: সব বাগ প্রথম স্তরে থাকে না। অংশ ৯-এর transpose বাগ শুধু W2-তে ছিল। প্রতিটি parameter থেকে অন্তত কয়েকটি এলোমেলো ঘর, ছোট network-এ সব ঘর।
    • পূর্ণসংখ্যা input আর শূন্য bias: ReLU-এর ভাঁজে (z = ০) পড়ার সম্ভাবনা অনেক বেশি, যেখানে central difference অর্থহীন। এলোমেলো দশমিক input আর ছোট এলোমেলো bias.
    • পুরো ডেটা: প্রতিটি ঘরের জন্য দুটি পূর্ণ forward, খুব ধীর; ২০–৬৪ সারির ছোট batch-ই যথেষ্ট।
  4. কোড: দ্বিতীয় hidden layer-এর ReLU-এর বদলে tanh দিন (H2 = tanh(Z2)). forward আর backward-এ কোন লাইন বদলাবে? তারপর অংশ ৫-এর grad_check চালিয়ে দেখান যে relative error ছোটই থাকে।

    tanh-এর derivative ১ − tanh², আর সেটি H2 থেকেই পাওয়া যায়, Z2 রাখার দরকার নেই। দুটি লাইন বদলায়:

    # forward-এ
    Z2 = H1 @ P["W2"] + P["b2"]; H2 = np.tanh(Z2)
    # backward-এ (ReLU-এর দরজার বদলে)
    D2 = (D3 @ P["W3"].T) * (1 - H2**2)

    তারপর grad_check(init(2, 16, 3, np.random.default_rng(1)), X[tr[:64]], Y[tr[:64]]) চালান। Desktop-এ (NumPy 2.5.1) এক run-এ সব relative error ৬ × ১০−১০ থেকে ২ × ১০−৯-এর মধ্যে এসেছে; tanh মসৃণ, তাই ভাঁজের ঝুঁকিও নেই। 1 - H2**2-এর বদলে ভুলে 1 - Z2**2 লিখলে একই run-এ W1, b1, W2, b2-এর relative error ০.২৯ থেকে ০.৩৭ হয়েছে, কিন্তু W3 আর b3 ঠিক থেকেছে, কারণ তাদের gradient এই লাইনের আগেই হিসাব হয়।

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

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

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

  1. Forward pass, X থেকে P পর্যন্ত, মধ্যবর্তী মান জমিয়ে
  2. D3 = (P − Y)/B, তারপর dW3 আর db3
  3. D2 = D3W3ᵀ ⊙ 1[Z2 > 0], তারপর dW2 আর db2
  4. D1 = D2W2ᵀ ⊙ 1[Z1 > 0], তারপর dW1 আর db1
  5. সব parameter হালনাগাদ: θ ← θ − η·dθ

একটি linear স্তর Z = XW + b, যেখানে X-এর shape (64, 16), W-এর (16, 3), আর ∂L/∂Z = D-এর shape (64, 3). dW কোনটি?

  • D·Xᵀ
  • Xᵀ·D
  • D·Wᵀ
  • X·D

তিন শ্রেণির softmax-এ logits z = (2.0, 1.0, 0.1) থেকে p ≈ (0.659, 0.242, 0.099), আর সঠিক শ্রেণি দ্বিতীয়টি (y = (0, 1, 0)). Mean cross-entropy, B = 1 হলে ∂L/∂z কত?

  • (0.659, 0.242, 0.099)
  • (−0.341, 0.242, 0.099)
  • (0.659, −0.758, 0.099)
  • (−0.659, 0.758, −0.099)

Loss গড় (÷B), কিন্তু backward-এ (P − Y)-কে B দিয়ে ভাগ করা হয়নি, B = 32. Gradient check-এ প্রতিটি parameter-এর relative error প্রায় কত দেখাবে?

  • প্রায় 0.94
  • প্রায় 10⁻⁹
  • প্রায় 31
  • শুধু W3-এ বড়, বাকিগুলো ছোট

একটি 2 → 16 → 16 → 3 MLP-এ dW2-এর জন্য ভুল করে (H1ᵀ·D2)ᵀ লেখা হয়েছে। কী ঘটবে?

  • NumPy সঙ্গে সঙ্গে shape error দেবে
  • কিছুই ঘটবে না, কারণ transpose-এ মান বদলায় না
  • সব parameter-এর gradient check ব্যর্থ হবে
  • কোড চলবে; gradient check-এ শুধু W2 ব্যর্থ হবে

Matrix গুণের FLOP গুনলে একটি MLP-এর backward pass forward pass-এর প্রায় কত গুণ, আর কেন?

  • প্রায় ১ গুণ, একই গুণ উল্টো দিকে
  • প্রায় ২ গুণ: প্রতিটি স্তরে dW আর dX-এর দুটি গুণ
  • Parameter-এর সংখ্যার সমান গুণ
  • স্তরের সংখ্যার বর্গ গুণ

Training-এর সময় forward pass-এর মধ্যবর্তী activation memory-তে রাখতে হয় কেন?

  • পরের prediction দ্রুত করতে
  • Parameter-এর একটি কপি রাখতে
  • Backward-এ dW আর activation-এর ঢাল হিসাবে সেগুলো লাগে
  • Gradient check-এর জন্য

শব্দকোষ

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

মনে রাখুন

  1. প্রতিটি ধাপে upstream gradient × local gradient; loss থেকে শুরু করে পেছনে।
  2. Linear স্তর: dW = XᵀD, db = ∑D, dX = DWᵀ; ReLU: ⊙ 1[Z > 0]; প্রতিটি gradient-এর shape = parameter-এর shape.
  3. Softmax + cross-entropy: D = (P − Y)/B; ÷B ভুললে relative error (B − 1)/(B + 1).
  4. Gradient check: float64, ε ≈ 10−6, প্রতিটি parameter-এর আলাদা relative error; বর্গাকার W-এর ভুল transpose shape দিয়ে ধরা পড়ে না।
  5. Backward ≈ ২ × forward FLOP; activation memory B-এর সঙ্গে বাড়ে, checkpointing সময় দিয়ে memory কেনে।

পরের পর্বে: আজ প্রতিটি স্তরের backward হাতে লিখলেন। পর্ব ০৫ (নিজে বানানো autograd — computational graph আর reverse mode)-এ এই কাজটি যন্ত্রের হাতে দেবেন: প্রতিটি ছোট operation নিজের local gradient জানবে, আর একটি ছোট engine পুরো graph ধরে পেছনে হাঁটবে।

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

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