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

Neural network-এর ধারণা — perceptron থেকে ছোট network, NumPy দিয়ে

The Idea of Neural Networks — From the Perceptron and XOR to a Two-Layer MLP with Backpropagation, Written in NumPy

Taught in Bangla, this lesson goes from a single neuron and the perceptron rule to why XOR needs a hidden layer and activation functions. It works one backpropagation step by hand, writes a two-layer MLP in NumPy with gradient checking and mini-batch SGD, and checks it against MLPClassifier on load_digits and PyTorch autograd in real runs.

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

  • একটি artificial neuron কী হিসাব করে, আর জীববিজ্ঞানের নিউরনের সঙ্গে তার সম্পর্ক কতটুকু
  • Perceptron-এর শেখার নিয়ম হাতে, আর XOR কেন একটি রেখায় ভাগ হয় না
  • Hidden layer, activation function আর non-linearity কেন দরকার; step, sigmoid, tanh আর ReLU-এর তুলনা
  • ২-স্তরের MLP-এর forward pass (প্রতিটি ধাপের shape), softmax, cross-entropy আর backpropagation, হাতে আর vectorised রূপে
  • NumPy-তে নিজের MLP: weight initialization, gradient check, mini-batch, learning rate, loss curve আর decision boundary; তারপর MLPClassifier আর PyTorch দিয়ে মিলিয়ে দেখা

এই পর্ব মডিউল ১২-এর শুরু, যেখানে আধুনিক AI-কে হাতে-কলমে আর সৎভাবে দেখব। ভিত আগেই তৈরি: পর্ব ৩৪-এ chain rule, gradient descent আর gradient checking; পর্ব ৩৮-এ cross-entropy; পর্ব ৫৯-এ sigmoid, softmax আর log loss; পর্ব ৬১-এ overfitting আর regularization; আর পর্ব ২-এ perceptron আর XOR-এর ইতিহাস। এখানে সেগুলো জুড়ে একটি ছোট neural network বানাব। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python, NumPy 2.4, scikit-learn 1.8.0, matplotlib 3.10). শেষ তিনটি ব্লক একই namespace ভাগ করে, তাই পরের কোনো ব্লক আগে চালালে আগেরগুলো আপনা থেকে আগে চলে। শেষ ব্লকে scikit-learn প্রথমবার নামতে প্রায় ১০ সেকেন্ড লাগে।

১ · একটি কৃত্রিম neuron — weighted sum, bias আর activation

বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান) প্রতিটি অর্ডারের জন্য জানতে চায়, ডেলিভারি দেরি হওয়ার ঝুঁকি কতটা। তিনটি তথ্য আছে, সবগুলো ০ থেকে ১-এর মাপে আনা: গুদাম থেকে দূরত্ব x1, পার্সেলের ওজন x2, আর আজ বৃষ্টি হচ্ছে কি না x3 (০ বা ১)। একটি কৃত্রিম neuron এই তিনটি সংখ্যা থেকে একটি সংখ্যা বানায়, তিন ধাপে:

z = w1x1 + w2x2 + w3x3 + b = w·x + b · a = f(z)

এখানে w হলো প্রতিটি input-এর weight (কোন তথ্য কতটা গুরুত্বপূর্ণ, আর কোন দিকে টানে), b হলো bias (সব input শূন্য হলে স্কোর কত), z হলো weighted sum, আর f হলো activation function, যা z-কে চূড়ান্ত output a-তে বদলায়। f যদি sigmoid হয়, এই neuron আর পর্ব ৫৯-এর logistic regression একই জিনিস; নতুনত্ব আসে অনেক neuron স্তরে স্তরে জোড়ায়।

পাড়ার মুদি দোকানদার ভাবছেন, আজ একজন বাড়তি ডেলিভারি-কর্মী রাখবেন কি না। তিনি মনে মনে কয়েকটি বিষয়ের গুরুত্ব যোগ করেন: বৃষ্টি হলে বড় টান (বড় weight), দূরের অর্ডার বেশি হলে মাঝারি টান, পার্সেল হালকা হলে উল্টো দিকে সামান্য। তাঁর নিজের একটি ঝোঁকও আছে, কেউ বাড়তি খরচে রাজি, কেউ নন (bias). মোট টান একটি সীমা পেরোলে সিদ্ধান্ত “হ্যাঁ” (activation). একটি neuron ঠিক এই হিসাবটুকু, শুধু weight-গুলো ডেটা থেকে শেখা।

একটি কৃত্রিম neuron-এর ভেতরে

inputs × weights → sum + bias → activation → output

x → w → Σ + b → f → a x₁দূরত্ব x₂ওজন x₃বৃষ্টি w₁ w₂ w₃ Σ bias b z f(z)activation a z = w·x + b, তারপর a = f(z)

নিচের stepper একটি অর্ডারে একটি neuron-এর পুরো হিসাব দেখায়। ওজনগুলো এখানে হাতে বসানো (কাল্পনিক): w = (1.5, −0.5, 2.0), b = −1.0. চারটি ভিন্ন activation একই z থেকে কী দেয়, তাও পাশাপাশি।

x = (0.8, 0.5, 1), w = (1.5, −0.5, 2.0), b = −1.0
ধাপ
হিসাব
ফল
  1. Input আর weight. দূরত্ব ০.৮ (বেশ দূরে), ওজন ০.৫ (মাঝারি), বৃষ্টি ১ (হচ্ছে)। Weight-এর চিহ্ন দিক বলে: দূরত্ব আর বৃষ্টি ঝুঁকি বাড়ায়, ভারী পার্সেলের ঋণাত্মক weight এখানে কাল্পনিকভাবে ঝুঁকি একটু কমায়।
  2. প্রতিটি input তার weight-এ গুণ। ১.২০, −০.২৫ আর ২.০০. বৃষ্টির অবদান সবচেয়ে বড়, কারণ তার weight সবচেয়ে বড়।
  3. যোগ। ১.২০ − ০.২৫ + ২.০০ = ২.৯৫. এটি পর্ব ৩৩-এর dot product w·x.
  4. Bias যোগ। z = ২.৯৫ − ১.০ = ১.৯৫. Bias ঋণাত্মক, তাই “ঝুঁকি আছে” বলতে input-গুলোকে আগে ১-এর বেশি টান তৈরি করতে হয়।
  5. Step আর sigmoid. Step শুধু চিহ্ন দেখে: z > ০, তাই ১. Sigmoid একটি ধারাবাহিক মান দেয়: ০.৮৭৫৪, যাকে “দেরির সম্ভাবনা প্রায় ৮৮%” হিসেবে পড়া যায়, যদি model ঠিকভাবে train করা হয়।
  6. Tanh আর ReLU. Tanh −১ থেকে ১-এর মধ্যে চাপে: ০.৯৬০৩. ReLU ধনাত্মক z-কে অপরিবর্তিত রাখে: ১.৯৫; ঋণাত্মক হলে দিত ০. একই z, চার রকম output; কোনটি কোথায় কাজের, অংশ ৪-এ।

মস্তিষ্কের সঙ্গে তুলনা সাবধানে: ধারণাটির শিকড় Warren McCulloch আর Walter Pitts-এর ১৯৪৩ সালের একটি প্রবন্ধে, যেখানে নিউরনকে “সীমা পেরোলে জ্বলে ওঠা” একটি যুক্তির একক হিসেবে আঁকা হয়েছিল। আসল নিউরন অনেক জটিল, আর মস্তিষ্ক কীভাবে শেখে, তা এখনো অনেকটাই খোলা প্রশ্ন। তাই “neural network মস্তিষ্কের মতো ভাবে” কথাটি একটি রূপক, বৈজ্ঞানিক দাবি নয় (ভাবনার প্রশ্ন ২)।

২ · Perceptron — শেখার নিয়ম হাতে, AND শেখা আর XOR-এর ব্যর্থতা

McCulloch–Pitts নিউরনের weight মানুষকে বসাতে হতো। Frank Rosenblatt ১৯৫৮ সালে Psychological Review-এ perceptron প্রকাশ করেন, যার মূল নতুনত্ব একটি শেখার নিয়ম (পুরো গল্প পর্ব ২-এ)। Perceptron-এর activation হলো step function: s = w·x + b > ০ হলে output ১, নইলে ০. প্রতিটি উদাহরণে t (সঠিক উত্তর) আর out (perceptron-এর উত্তর) মেলানো হয়; ভুল হলে:

w ← w + η(t − out)·x · b ← b + η(t − out)

η (eta) হলো learning rate; এখানে η = ১. উত্তর ১ হওয়া উচিত ছিল কিন্তু ০ এসেছে (t − out = +১), তাহলে w-এ x যোগ হয়, ফলে পরের বার একই x-এ s বড় হয়। উল্টো ভুলে x বিয়োগ হয়। সব উদাহরণ একবার দেখা হলে এক epoch. পর্ব ২-এ এলোমেলো শুরু থেকে নিয়মটি চালানো হয়েছিল; এখানে সব ওজন শূন্য থেকে শুরু করব, যাতে প্রতিটি সংখ্যা কাগজে-কলমে মেলানো যায়। চারটি উদাহরণ এই ক্রমে: (0,0), (0,1), (1,0), (1,1); AND-এর t = (0, 0, 0, 1).

শুরু w = (0, 0), b = 0 · out = 1 যদি s > 0
কোথায়
s, out, t
বদল
w, b
ভুল/epoch
  1. প্রথম তিনটি উদাহরণ। সব ওজন শূন্য, তাই s = ০; শর্ত “s > ০” মিথ্যা, out = ০. তিনটির t-ও ০, তাই কিছু বদলায় না।
  2. (1, 1)-এ প্রথম ভুল। s = ০, out = ০, অথচ t = ১. t − out = +১, তাই w-এ x = (1, 1) যোগ: w = (1, 1), b = ১. Epoch ১-এ মোট ভুল ১টি।
  3. Epoch ২, (0, 0). এখন s = b = ১ > ০, out = ১, কিন্তু t = ০. x শূন্য, তাই w বদলায় না; শুধু b ১ কমে ০.
  4. (0, 1). s = ০ + ১ + ০ = ১, আবার ভুল ১. w থেকে (0, 1) বাদ: w = (1, 0), b = −১.
  5. (1, 0) ঠিক, (1, 1) ভুল। (1, 0)-এ s = ১ − ১ = ০, out = ০, ঠিক। (1, 1)-এ s = ১ + ০ − ১ = ০, out = ০ অথচ t = ১: w = (2, 1), b = ০. Epoch ২-এ ভুল ৩টি।
  6. Epoch ৩ থেকে ৬. আরও ছয়টি সংশোধনের পর (epoch ৩-এ ৩টি, ৪-এ ২টি, ৫-এ ১টি) epoch ৬-এ শূন্য ভুল। শেষ ওজন w = (2, 1), b = −২: চারটি s = −২, −১, ০, ১; শুধু (1, 1)-এ s > ০. রেখা 2x1 + x2 = 2 এখন (1, 1)-কে বাকিদের থেকে আলাদা করে।

Perceptron-এর সিদ্ধান্তের সীমানা w·x + b = ০, দুই মাত্রায় একটি সরল রেখা। যে ডেটার দুই দল একটি রেখা (বেশি মাত্রায় একটি সমতল) দিয়ে আলাদা করা যায়, তাকে বলে linearly separable. Perceptron convergence theorem বলে, ডেটা linearly separable হলে নিয়মটি সীমিত সংখ্যক ভুলের পর থামবেই (পরিষ্কার প্রমাণের জন্য সাধারণত Novikoff-এর ১৯৬২ সালের কাজ উদ্ধৃত হয়)। AND তেমন ডেটা। XOR (ঠিক একটি input ১ হলে ১) নয়, আর তা চার লাইনে প্রমাণ করা যায়। ধরুন কোনো w1, w2, b XOR ঠিকভাবে করে। তাহলে:

  • (0, 0) → ০: b ≤ ০
  • (0, 1) → ১: w2 + b > ০, আর (1, 0) → ১: w1 + b > ০
  • মাঝের দুটি যোগ করলে w1 + w2 + 2b > ০, অর্থাৎ w1 + w2 + b > −b ≥ ০
  • কিন্তু (1, 1) → ০ চায় w1 + w2 + b ≤ ০. পরস্পরবিরোধী, তাই এমন কোনো ওজন নেই।

তাই XOR-এ perceptron নিয়ম কখনো থামে না; ওজনগুলো একটি চক্রে ঘোরে, যা নিচের কোডে সরাসরি দেখবেন। Marvin Minsky আর Seymour Papert-এর ১৯৬৯ সালের বই Perceptrons এই ধরনের সীমা কঠোর গণিতে দেখায়, মূলত এক স্তরের perceptron-এর জন্য। বইটি neural network গবেষণায় আগ্রহ কমার একটি কারণ হিসেবে প্রায়ই উল্লেখ করা হয়, তবে প্রভাব কতটা বড় ছিল, তা নিয়ে মত ভিন্ন (ভাবনার প্রশ্ন ১)।

৩ · Hidden layer দিয়ে XOR — হাতে বসানো network, আর linear স্তর কেন একটিতে মিশে যায়

একটি রেখা পারে না, দুটি রেখা পারে। XOR-কে ভেঙে লেখা যায়: “x1 OR x2, কিন্তু x1 AND x2 নয়”. OR আর AND দুটিই linearly separable, তাই প্রতিটির জন্য একটি করে neuron রাখি। এই দুটি neuron input আর output-এর মাঝে বসে; কেউ তাদের সঠিক মান বলে দেয় না, তাই এদের স্তরকে বলে hidden layer. হাতে বসানো ওজন (step activation):

  • h1 = step(x1 + x2 − 0.5): অন্তত একটি ১ হলে ১, অর্থাৎ OR
  • h2 = step(x1 + x2 − 1.5): দুটিই ১ হলে ১, অর্থাৎ AND
  • output = step(h1 − 2h2 − 0.5): h1 = ১ আর h2 = ০ হলেই শুধু ১

যাচাই: (0,0) → h = (0,0) → step(−০.৫) = ০; (0,1) আর (1,0) → h = (1,0) → step(০.৫) = ১; (1,1) → h = (1,1) → step(১ − ২ − ০.৫) = ০. চারটিই ঠিক। মূল কথাটি জ্যামিতির: hidden layer ডেটাকে একটি নতুন জায়গায় নিয়ে যায়, যেখানে একটি রেখাই যথেষ্ট।

একই চারটি বিন্দু, দুই জায়গায় — input space বনাম hidden space

left: no single line works · right: after h = (OR, AND), one line separates

XOR: input space আর hidden space input: (x₁, x₂) hidden: (h₁ OR, h₂ AND) x₁x₂ h₁h₂ 01 01 দুটি বিন্দু কোনো একটি রেখা পারে না h₁ − 2h₂ = 0.5

Activation বাদ দিলে কী হয় — দুটি linear স্তর একটিই

একটি প্রশ্ন স্বাভাবিক: স্তর বাড়ালেই কি ক্ষমতা বাড়ে? না, যদি স্তরগুলোর মাঝে কোনো non-linear activation না থাকে। দুটি linear স্তর মানে প্রথমে matrix A দিয়ে গুণ, তারপর B দিয়ে। পর্ব ১৭-এর matrix গুণ সহযোগী (associative): (xA)B = x(AB). তাই দুই স্তরের কাজ একটি matrix C = AB-র একটি স্তরই করতে পারে। Bias থাকলেও তাই: (xA + a)B + c = x(AB) + (aB + c), আবার একটি linear স্তর। একটি ছোট সংখ্যার উদাহরণ:

A = [[1, 2], [0, 1]], B = [[2, 0], [1, −1]] ⇒ AB = [[4, −2], [1, −1]]

x = (1, 1) হলে xA = (1, 3), তারপর (1, 3)B = (1·2 + 3·1, 1·0 + 3·(−1)) = (5, −3). আর সরাসরি x(AB) = (4 + 1, −2 − 1) = (5, −3). একই। মাঝে ReLU বসালে ঋণাত্মক মান কেটে যায়, আর এই সমতা ভাঙে; সেই ভাঙনই network-কে বাঁকা সীমানা আঁকার ক্ষমতা দেয়। নিচের ব্লক অংশ ২ আর ৩-এর তিনটি দাবি একসঙ্গে যাচাই করে।

import numpy as np

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
step = lambda z: (z > 0).astype(int)                # s > 0 হলে 1, না হলে 0

def perceptron(y, epochs, show):
    w, b, errs = np.zeros(2), 0.0, []               # শুরু: সব ওজন শূন্য, η = 1
    for ep in range(1, epochs + 1):
        errs.append(0)
        for x, t in zip(X, y):
            out = int(w @ x + b > 0)
            if out != t:                            # ভুল হলেই শুধু বদল
                w, b = w + (t - out) * x, b + (t - out)
                errs[-1] += 1
                if show: print(f"  epoch {ep} x={x} t={t} out={out} -> w={w} b={b:+.0f}")
        if errs[-1] == 0: break
    return w, b, errs

for name, y, n_ep in [("AND", [0, 0, 0, 1], 10), ("XOR", [0, 1, 1, 0], 12)]:
    print(name)
    w, b, errs = perceptron(y, n_ep, show=(name == "AND"))
    print("  ভুল/epoch:", errs, "| w =", w, "b =", b, "| out =", step(X @ w + b), "t =", y)

W1, b1 = np.array([[1, 1], [1, 1]]), np.array([-0.5, -1.5])  # লুকানো স্তর: OR আর AND
W2, b2 = np.array([1, -2]), -0.5                              # OR হ্যাঁ কিন্তু AND না
H = step(X @ W1 + b1)
print("হাতে বসানো network: hidden =", H.tolist(), "-> XOR =", step(H @ W2 + b2))

A, B = np.array([[1, 2], [0, 1]]), np.array([[2, 0], [1, -1]])  # দুটি linear layer
x = np.array([[1, 1], [1, -3]])
print("A @ B =", (A @ B).tolist())
print("(x @ A) @ B =", (x @ A @ B).tolist(), "| x @ (A @ B) =", (x @ (A @ B)).tolist())
print("মাঝে ReLU দিলে:", (np.maximum(0, x @ A) @ B).tolist())

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

AND
  epoch 1 x=[1 1] t=1 out=0 -> w=[1. 1.] b=+1
  epoch 2 x=[0 0] t=0 out=1 -> w=[1. 1.] b=+0
  epoch 2 x=[0 1] t=0 out=1 -> w=[1. 0.] b=-1
  epoch 2 x=[1 1] t=1 out=0 -> w=[2. 1.] b=+0
  epoch 3 x=[0 1] t=0 out=1 -> w=[2. 0.] b=-1
  epoch 3 x=[1 0] t=0 out=1 -> w=[1. 0.] b=-2
  epoch 3 x=[1 1] t=1 out=0 -> w=[2. 1.] b=-1
  epoch 4 x=[1 0] t=0 out=1 -> w=[1. 1.] b=-2
  epoch 4 x=[1 1] t=1 out=0 -> w=[2. 2.] b=-1
  epoch 5 x=[0 1] t=0 out=1 -> w=[2. 1.] b=-2
  ভুল/epoch: [1, 3, 3, 2, 1, 0] | w = [2. 1.] b = -2.0 | out = [0 0 0 1] t = [0, 0, 0, 1]
XOR
  ভুল/epoch: [2, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] | w = [-1.  0.] b = 1.0 | out = [1 1 0 0] t = [0, 1, 1, 0]
হাতে বসানো network: hidden = [[0, 0], [1, 0], [1, 0], [1, 1]] -> XOR = [0 1 1 0]
A @ B = [[4, -2], [1, -1]]
(x @ A) @ B = [[5, -3], [1, 1]] | x @ (A @ B) = [[5, -3], [1, 1]]
মাঝে ReLU দিলে: [[5, -3], [2, 0]]

AND-এর দশটি সংশোধন stepper-এর সঙ্গে হুবহু মেলে: epoch ৬-এ শূন্য ভুল। XOR-এ epoch ৩ থেকে প্রতিটি epoch-এ চারটির চারটিই ভুল; ওজন প্রতি epoch-এর শেষে একই জায়গায় (w = (−1, 0), b = ১) ফিরে আসে, একটি চক্র। হাতে বসানো network-এর hidden মান [[0,0], [1,0], [1,0], [1,1]], ডানের ছবির তিনটি বিন্দু, আর output [0 1 1 0], ঠিক XOR. শেষ তিন লাইনে দ্বিতীয় সারি x = (1, −3) দেখুন: xA = (1, −1). Linear পথে দুই রকম হিসাবেই (1, 1), কিন্তু ReLU −১-কে ০ করে দেয়, তাই ফল (2, 0), এক matrix দিয়ে আর লেখা যায় না।

৪ · Activation function — step, sigmoid, tanh আর ReLU

XOR network হাতে বসানো গিয়েছিল। কিন্তু হাজার weight-এর network হাতে বসানো যায় না; weight শিখতে হবে gradient descent দিয়ে, আর তার জন্য activation-এর ঢাল (derivative) দরকার। Step function-এর ঢাল প্রায় সবখানে ০ আর z = ০-তে সংজ্ঞায়িত নয়, তাই gradient কোনো দিক দেখায় না। এ কারণেই বহুস্তরের network-এ ধারাবাহিক activation ব্যবহার হয়। নিচের ছবিতে চারটি activation (রঙিন রেখা) আর তাদের derivative (ড্যাশ দেওয়া রেখা), z = −৪ থেকে ৪:

চারটি activation function আর তাদের ঢাল

solid: f(z) · dashed: f′(z) · z from −4 to 4

step, sigmoid, tanh, ReLU আর তাদের derivative  step ০ বা ১ sigmoid ০ থেকে ১ tanh −১ থেকে ১ ReLU max(0, z)
Activationসূত্রঢালকোথায় কাজে লাগে, কী সমস্যা
stepz > 0 হলে 1, নইলে 0প্রায় সবখানে 0Perceptron; gradient descent-এ চলে না
sigmoid1/(1 + e−z)σ(1 − σ), সর্বোচ্চ 0.25দুই শ্রেণির output-এ সম্ভাবনা; hidden layer-এ ঢাল ছোট, আর output সবসময় ধনাত্মক
tanh(ez − e−z)/(ez + e−z)1 − tanh2, সর্বোচ্চ 1শূন্যকে কেন্দ্র করে, তাই পুরোনো network আর recurrent network-এ জনপ্রিয়; বড় |z|-এ ঢাল প্রায় 0
ReLUmax(0, z)z > 0 হলে 1, নইলে 0আজকের hidden layer-এর সাধারণ পছন্দ: হিসাব সস্তা, ধনাত্মক দিকে ঢাল কমে না; যে unit সবসময় ঋণাত্মক পায়, সে আর শেখে না (“dead ReLU”)

Sigmoid-এর সর্বোচ্চ ঢাল ০.২৫. Backpropagation-এ (অংশ ৬) প্রতিটি স্তর পার হতে gradient এই ঢাল দিয়ে গুণ হয়। দশটি sigmoid স্তর পেরোলে সবচেয়ে ভালো অবস্থাতেও গুণনীয়ক ০.২৫১০ ≈ ৯.৫ × ১০−৭: শুরুর দিকের স্তরে gradient প্রায় নেই, তারা প্রায় শেখে না। একে বলে vanishing gradient (weight-ও এই গুণে ঢোকে, তাই আসল হিসাব আরও জটিল)। ReLU-এর ধনাত্মক দিকে ঢাল ১, তাই সমস্যা অনেকটা কমে। ReLU জনপ্রিয় করার কৃতিত্ব সাধারণত ২০১০–২০১১ সালের কয়েকটি গবেষণাকে দেওয়া হয় (Nair ও Hinton; Glorot, Bordes ও Bengio), যদিও একই ধরনের function আরও আগে ব্যবহৃত হয়েছিল।

মূল কথা

Activation দুটি কাজ করে: স্তরগুলোকে একটিতে মিশে যেতে দেয় না (non-linearity), আর gradient-কে পেছনে যাওয়ার পথ দেয় (ঢাল)। প্রথম কাজটি না থাকলে গভীরতার কোনো মানে নেই; দ্বিতীয়টি না থাকলে network শিখতে পারে না।

৫ · একটি ২-স্তরের MLP — forward pass, প্রতিটি ধাপের shape, softmax আর cross-entropy

এবার শেখার উপযোগী একটি network. Input আর output-এর মাঝে একটি hidden layer থাকা সবচেয়ে সরল network-কে বলে MLP (multi-layer perceptron), যদিও এর neuron-গুলো আসলে perceptron নয়, ধারাবাহিক activation-এর neuron. আমাদের কাজ: বাজারঘরের দুটি scale করা feature (ধরুন অর্ডারের দূরত্ব আর অর্ডারের সময়) দেখে দুই দলের একটিতে ফেলা। ডেটা পুরো বানানো; দুই দলকে ইচ্ছা করে দুটি আধা-চাঁদের আকারে বানানো হয়েছে (machine learning-এ পরিচিত “two moons” খেলনা ডেটা), যাতে কোনো সরল রেখা তাদের আলাদা করতে না পারে। আসল দোকানের ডেটা এমন পরিষ্কার আকারে আসে না; এটি শুধু ধারণা দেখানোর খেলনা।

Forward pass মানে input থেকে output পর্যন্ত একবার হিসাব। n টি উদাহরণ একসঙ্গে একটি matrix-এ রাখলে (প্রতি সারিতে একটি), প্রতিটি ধাপ একটি matrix গুণ। h = ১৬টি hidden unit আর k = ২টি শ্রেণি ধরে:

২-স্তরের MLP-এর forward pass: প্রতিটি ধাপের shape

n examples, d = 2 features, h = 16 hidden units, k = 2 classes

X → Z1 → H → Z2 → softmax → loss, সঙ্গে shape X(n, 2) Z₁ = X·W₁ + b₁(2,16) → (n,16) H = ReLU(Z₁)(n, 16) Z₂ = H·W₂ + b₂(16,2) → (n,2) P = softmax(Z₂)(n, 2) L = −mean log P[i, yᵢ]সংখ্যা backward pass

Shape মেলানোর নিয়ম একটাই: (n, d) গুণ (d, h) = (n, h). b1-এর shape (16,), যা পর্ব ১৫-এর broadcasting-এ প্রতিটি সারিতে যোগ হয়। Parameter গোনা যায় সরাসরি: W1-এ ২ × ১৬ = ৩২, b1-এ ১৬, W2-এ ১৬ × ২ = ৩২, b2-এ ২; মোট ৮২টি। শেষ স্তরের কাঁচা স্কোর Z2-কে বলে logits. Softmax (পর্ব ৫৯-এর অংশ ৮) প্রতিটি সারিকে সম্ভাবনায় বদলায়:

pj = ezj / ∑m ezm · L = −(1/n) ∑i ln pi, yi

j হলো শ্রেণির নম্বর, আর loss হলো প্রতিটি উদাহরণের সঠিক শ্রেণিকে দেওয়া সম্ভাবনার −ln-এর গড়, অর্থাৎ পর্ব ৩৮-এর cross-entropy. উদাহরণ: logits (2, 1, 0) হলে softmax ≈ (০.৬৬৫, ০.২৪৫, ০.০৯০); সঠিক শ্রেণি প্রথমটি হলে loss = −ln ০.৬৬৫ ≈ ০.৪০৮. সব শ্রেণিকে সমান সম্ভাবনা দিলে loss = ln k (দুই শ্রেণিতে ০.৬৯৩১, দশে ২.৩০৩); শুরুর loss এর চেয়ে অনেক বেশি হলে শুরুর weight খুব বড়। আর e1000 overflow করে, তাই প্রতিটি সারি থেকে তার সর্বোচ্চ মান বিয়োগ করে নিই; লব আর হর দুটিই e−max দিয়ে গুণ হয় বলে ফল বদলায় না; এটি numerical stability-র একটি কৌশল।

৬ · Backpropagation — chain rule দিয়ে পেছনে হিসাব, হাতে এক ধাপ

Gradient descent-এর জন্য প্রতিটি weight-এর সাপেক্ষে loss-এর partial derivative লাগে। Network হলো ছোট ছোট function-এর একটি শিকল: x → z1 → h → z2 → p → L. পর্ব ৩৪-এর chain rule বলে, শিকলের derivative মানে প্রতিটি কড়ার ছোট derivative-এর গুণফল। Backpropagation এই গুণফল চতুরভাবে সাজায়: forward pass-এ প্রতিটি মধ্যবর্তী মান মনে রাখা হয়, তারপর loss থেকে শুরু করে পেছনে প্রতিটি স্তরে একবার করে গুণ। ফলে এক forward আর এক backward pass-এ সব weight-এর gradient পাওয়া যায়, প্রতিটি weight-এর জন্য আলাদা হিসাব ছাড়াই। এই হিসাবের নকশাকে বলে computational graph.

ক্রিকেট ম্যাচ হারার পর দলের বিশ্লেষক পেছন থেকে হিসাব করেন: শেষ ওভারে কত রান কম পড়ল, তার কতটা ডেথ বোলিংয়ের দায়, তার মধ্যে কতটা কোন বোলারের কোন বলের। প্রতিটি ধাপে আগের ধাপের দায় ভাগ হয়ে পেছনে যায়। Backpropagation-এ “দায়” হলো gradient: output-এর ভুল প্রতিটি স্তরে weight অনুযায়ী ভাগ হয়ে পেছনের neuron-এ পৌঁছায়।

একটি ২-২-১ network (দুটি input, দুটি sigmoid hidden unit, একটি sigmoid output, binary cross-entropy loss) দিয়ে এক ধাপ হাতে করি। পরীক্ষায় হাতে কষতে দেওয়া হয় ঠিক এমন আকারই। Sigmoid output আর cross-entropy একসঙ্গে থাকলে ∂L/∂z2 = p − y, যা পর্ব ৫৯-এর “আরও গভীরে” বাক্সে প্রমাণ করা আছে; আর sigmoid-এর ঢাল h(1 − h).

x = (1.0, 0.5), y = 1 · W1 = [[0.5, −0.3], [0.2, 0.4]], b1 = (0, 0) · W2 = (0.7, −0.6), b2 = 0 · η = 0.5

W1-এর প্রতিটি সারি একটি input-এর, প্রতিটি কলাম একটি hidden unit-এর; z1 = xW1 + b1.

x = (1.0, 0.5), y = 1 · মানগুলো চার দশমিকে
ধাপ
সূত্র
মান
  1. Forward, hidden. z1 = (১ × ০.৫ + ০.৫ × ০.২, ১ × (−০.৩) + ০.৫ × ০.৪) = (০.৬, −০.১); sigmoid দিয়ে h = (০.৬৪৫৭, ০.৪৭৫০). এই মানগুলো backward pass-এ লাগবে, তাই মনে রাখা হয়।
  2. Forward, output. z2 = ০.৭ × ০.৬৪৫৭ − ০.৬ × ০.৪৭৫০ ≈ ০.১৬৬৯ (পূর্ণ দশমিকে ০.১৬৬৯৫; গোল করা পদ থেকে ০.১৬৭০)। p = σ(z2) = ০.৫৪১৬, L = −ln ০.৫৪১৬ = ০.৬১৩২.
  3. Output-এর δ. ∂L/∂z2 = p − y = −০.৪৫৮৪. ঋণাত্মক মানে z2 বাড়ালে loss কমবে।
  4. W2-এর gradient. z2 = h·W2 + b2, তাই ∂z2/∂W2 = h. ∂W2 = −০.৪৫৮৪ × (০.৬৪৫৭, ০.৪৭৫০) = (−০.২৯৫৯, −০.২১৭৭); ∂b2 = −০.৪৫৮৪.
  5. পেছনে hidden-এ। Output-এর দায় W2 অনুযায়ী ভাগ হয়: δ2·W2 = (−০.৩২০৯, ০.২৭৫০). তারপর sigmoid-এর ঢাল h(1 − h) = (০.২২৮৮, ০.২৪৯৪) দিয়ে উপাদান ধরে গুণ (⊙): δ1 = (−০.০৭৩৪, ০.০৬৮৬). লক্ষ করুন, ঢাল ০.২৫-এর কম বলে gradient এখানে প্রায় চার ভাগের এক ভাগ হয়ে গেল।
  6. W1-এর gradient. ∂W1[i, j] = xi · δ1, j: প্রথম সারি x1 = ১ দিয়ে (−০.০৭৩৪, ০.০৬৮৬), দ্বিতীয় সারি x2 = ০.৫ দিয়ে (−০.০৩৬৭, ০.০৩৪৩). সংখ্যায় মাপা gradient (central difference) প্রথম ঘরে দেয় −০.০৭৩৪১, মিলে গেছে।
  7. হালনাগাদ। প্রতিটি parameter থেকে ০.৫ × gradient বিয়োগ: W2 = (০.৮৪৮০, −০.৪৯১১), b2 = ০.২২৯২, W1 = [[০.৫৩৬৭, −০.৩৩৪৩], [০.২১৮৪, ০.৩৮২৯]], b1 = (০.০৩৬৭, −০.০৩৪৩). নতুন forward pass-এ p = ০.৬৩৮৪, loss ০.৬১৩২ থেকে নেমে ০.৪৪৮৭.

একসঙ্গে n টি উদাহরণ — vectorised gradient

একটি একটি করে হিসাব শেখার জন্য ভালো, কাজের জন্য ধীর। অংশ ৫-এর softmax network-এ পুরো batch একবারে (Y হলো সঠিক শ্রেণির one-hot matrix, ⊙ উপাদান ধরে গুণ, 1[Z1 > 0] হলো ReLU-এর ঢাল):

D2 = (P − Y)/n · ∂W2 = HᵀD2 · ∂b2 = ∑ D2 (সারি ধরে)
D1 = (D2W2ᵀ) ⊙ 1[Z1 > 0] · ∂W1 = XᵀD1 · ∂b1 = ∑ D1

Softmax আর cross-entropy মিলেও output-এর gradient হুবহু “অনুমান বিয়োগ সত্য”, P − Y, sigmoid-এর মতোই। Shape দিয়ে যাচাই করুন: Hᵀ (16, n) গুণ D2 (n, 2) = (16, 2), ঠিক W2-এর shape; D2W2ᵀ = (n, 2)(2, 16) = (n, 16), ঠিক Z1-এর shape. প্রতিটি gradient-এর shape তার parameter-এর shape-এর সমান হতেই হবে; কোড লেখার সময় এটিই সবচেয়ে দ্রুত পরীক্ষা।

Backpropagation জনপ্রিয় হয় ১৯৮৬ সালে, David Rumelhart, Geoffrey Hinton আর Ronald Williams-এর Nature-এর প্রবন্ধে, যা দেখায় hidden layer কাজের feature শিখতে পারে। গণিতের শিকড় আরও পুরোনো (Seppo Linnainmaa ১৯৭০, Paul Werbos ১৯৭০-এর দশক); কে কতটুকু আগে, তা নিয়ে বিবরণ ভিন্ন, পর্ব ৩৪-এ এই ইতিহাস আছে।

৭ · নিজের হাতে NumPy MLP — weight initialization, gradient check, mini-batch আর learning rate

Training শুরুর আগে weight-এর একটি শুরুর মান লাগে। স্বাভাবিক প্রথম ভাবনা: সব শূন্য। কিন্তু তাতে একই স্তরের সব hidden unit হুবহু একই হিসাব করে, একই gradient পায়, আর চিরকাল একই থাকে; ১৬টি unit কার্যত একটি unit. এই সমতা ভাঙতে weight এলোমেলোভাবে শুরু করা হয় (symmetry breaking). ReLU আর আমাদের সূত্রে অবস্থা আরও খারাপ: W2 শূন্য হলে D1 শূন্য, আর W1 শূন্য হলে H শূন্য, তাই কোনো weight-ই নড়ে না, শুধু b2 শেখে। নিচের কোডে সেটি দেখবেন।

এলোমেলো কত বড়? খুব ছোট হলে স্তর পেরোতে পেরোতে সংকেত মিলিয়ে যায়, খুব বড় হলে বিস্ফোরিত হয় বা sigmoid/tanh দুই প্রান্তে আটকে যায়। একটি প্রচলিত নিয়ম: input-এর সংখ্যা d হলে weight-এর standard deviation প্রায় √(1/d) (Glorot ও Bengio, ২০১০-এর ধারণা থেকে) বা ReLU-এর জন্য √(2/d) (He ও সহকর্মীরা, ২০১৫), যাতে প্রতিটি স্তরের output-এর মাপ মোটামুটি একই থাকে। একে বলে weight initialization; নিয়মগুলো পুরো প্রমাণ নয়, কিছু অনুমানের ওপর দাঁড়ানো বাস্তব নির্দেশিকা।

দ্বিতীয় জরুরি কাজ gradient check: পর্ব ৩৪-এর central difference, (L(θ + ε) − L(θ − ε))/2ε, দিয়ে প্রতিটি parameter-এর gradient সংখ্যায় মেপে হাতে লেখা backprop-এর সঙ্গে মেলানো। এটি ধীর (প্রতিটি parameter-এর জন্য দুটি forward pass), তাই শুধু কোড যাচাইয়ে, ছোট ডেটায়, একবার। নিচের ব্লক খেলনা ডেটা বানায় (৩০০টি বিন্দু: ২৪০টি train, ৬০টি validation), MLP-এর function লেখে আর ৮২টি parameter-এর প্রতিটিতে gradient check চালায়।

import numpy as np

rng = np.random.default_rng(42)
n = 300                                               # বাজারঘরের খেলনা ডেটা, পুরো বানানো
t, y = rng.uniform(0, np.pi, n), rng.integers(0, 2, n)
X = np.column_stack([np.cos(t) + y - 0.5, np.sin(t) * (1 - 2 * y) + 0.5 * y - 0.25])
X = X + rng.normal(0, 0.15, (n, 2))
Xtr, ytr, Xva, yva = X[:240], y[:240], X[240:], y[240:]

def init(d, h, k):                                    # ছোট random ওজন, scale √(2/d)
    return {"W1": rng.normal(0, np.sqrt(2 / d), (d, h)), "b1": np.zeros(h),
            "W2": rng.normal(0, np.sqrt(1 / h), (h, k)), "b2": np.zeros(k)}

def forward(P, X):                                    # X (n,d) -> H (n,h) -> p (n,k)
    Z1 = X @ P["W1"] + P["b1"]; H = np.maximum(0, Z1)
    Z2 = H @ P["W2"] + P["b2"]
    E = np.exp(Z2 - Z2.max(axis=1, keepdims=True))   # stable softmax
    return Z1, H, E / E.sum(axis=1, keepdims=True)

def loss_grads(P, X, y):
    Z1, H, Pr = forward(P, X); n = len(y)
    D2 = Pr.copy(); D2[np.arange(n), y] -= 1; D2 /= n  # ∂L/∂Z2 = (p − onehot)/n
    D1 = (D2 @ P["W2"].T) * (Z1 > 0)                 # chain rule; ReLU-এর ঢাল 0 বা 1
    G = {"W1": X.T @ D1, "b1": D1.sum(0), "W2": H.T @ D2, "b2": D2.sum(0)}
    return -np.log(Pr[np.arange(n), y]).mean(), G

P = init(2, 16, 2)
L0, G = loss_grads(P, Xtr, ytr); worst, eps = 0.0, 1e-6
for k in P:                                           # gradient check: central difference
    for i in np.ndindex(P[k].shape):
        old = P[k][i]; P[k][i] = old + eps; lp = loss_grads(P, Xtr, ytr)[0]
        P[k][i] = old - eps; lm = loss_grads(P, Xtr, ytr)[0]; P[k][i] = old
        worst = max(worst, abs((lp - lm) / (2 * eps) - G[k][i]))
print("shape:", {k: v.shape for k, v in P.items()}, "| parameter:", sum(v.size for v in P.values()))
print(f"শুরুর loss {L0:.4f} (ln 2 = 0.6931) | gradient check: সবচেয়ে বড় পার্থক্য {worst:.1e}")

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

shape: {'W1': (2, 16), 'b1': (16,), 'W2': (16, 2), 'b2': (2,)} | parameter: 82
শুরুর loss 1.1328 (ln 2 = 0.6931) | gradient check: সবচেয়ে বড় পার্থক্য 2.8e-10

চারটি parameter-এর shape অংশ ৫-এর ছবির সঙ্গে মেলে, মোট ৮২টি। শুরুর loss ln ২-এর চেয়ে বেশি: এলোমেলো weight-এ network কিছু উদাহরণে আত্মবিশ্বাসী কিন্তু ভুল অনুমান করছে, যা cross-entropy কড়া শাস্তি দেয়। Gradient check-এ হাতে লেখা gradient আর সংখ্যায় মাপা gradient-এর সবচেয়ে বড় পার্থক্য প্রায় 10−10; ε = 10−6-এ central difference-এর নিজের ভুলই এই মাপের, তাই backprop সঠিক। একটি চিহ্ন বা transpose ভুল হলে পার্থক্য সাধারণত 10−2 বা তার বেশি হয়।

Training loop — epoch, mini-batch আর learning rate

প্রতিটি ধাপে পুরো ডেটার gradient না নিয়ে ছোট একটি দলের (mini-batch, এখানে ৩২টি) gradient নিলে অনেক বেশি ধাপ নেওয়া যায়, আর gradient-এর সামান্য এলোমেলোতা অনেক সময় সাহায্যও করে; একে stochastic gradient descent (SGD) বলে। প্রতিটি epoch-এ ডেটার ক্রম নতুন করে এলোমেলো করা হয়। Learning rate η = ০.২: খুব বড় হলে loss লাফাতে বা বিস্ফোরিত হতে পারে, খুব ছোট হলে training অনেক ধীর (পর্ব ৩৪)। নিচের ব্লক ২০০ epoch train করে, প্রতিটি epoch-এর শেষে train loss আর validation accuracy রাখে, শূন্য দিয়ে শুরুর পরীক্ষাটি চালায়, আর তিনটি ছবি আঁকে।

import numpy as np
import matplotlib.pyplot as plt

def train(P, X, y, Xv, yv, epochs, lr=0.2, bs=32):
    hist = []
    for ep in range(epochs):
        idx = rng.permutation(len(y))                 # প্রতি epoch-এ নতুন এলোমেলো ক্রম
        for s in range(0, len(y), bs):
            b = idx[s:s + bs]
            G = loss_grads(P, X[b], y[b])[1]          # একটি mini-batch-এর gradient
            for k in P: P[k] -= lr * G[k]             # SGD-এর এক ধাপ
        acc = (forward(P, Xv)[2].argmax(1) == yv).mean()
        hist.append((loss_grads(P, X, y)[0], acc))
    return np.array(hist)

rng = np.random.default_rng(0); P = init(2, 16, 2)
hist = train(P, Xtr, ytr, Xva, yva, epochs=200)
for e in (1, 10, 50, 200):
    print(f"epoch {e:3d}: train loss {hist[e-1, 0]:.3f} | validation accuracy {hist[e-1, 1]:.3f}")
Z = {k: np.zeros_like(v) for k, v in P.items()}       # সব ওজন শূন্য দিয়ে শুরু
hz = train(Z, Xtr, ytr, Xva, yva, epochs=50)
print("শূন্য থেকে শুরু: accuracy", hz[-1, 1].round(3), "| W1 এখনো সব শূন্য?", bool(np.all(Z["W1"] == 0)))

g1, g2 = np.meshgrid(np.linspace(-2, 2.5, 150), np.linspace(-1.6, 1.8, 150))
prob = forward(P, np.column_stack([g1.ravel(), g2.ravel()]))[2][:, 1].reshape(g1.shape)
fig, ax = plt.subplots(1, 3, figsize=(11, 3.3), layout="constrained")
ax[0].plot(hist[:, 0]); ax[0].set(title="Training loss", xlabel="epoch", ylabel="cross-entropy")
ax[1].plot(hist[:, 1], color="#16a34a")
ax[1].set(title="Validation accuracy", xlabel="epoch", ylim=(0.4, 1.02))
ax[2].contourf(g1, g2, prob, levels=[0, 0.5, 1], colors=["#bfdbfe", "#fde68a"])
ax[2].scatter(Xtr[:, 0], Xtr[:, 1], c=ytr, cmap="coolwarm", s=10)
ax[2].set(title="Decision boundary, 16 ReLU units", xlabel="x1", ylabel="x2")
plt.show()

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

epoch   1: train loss 0.391 | validation accuracy 0.700
epoch  10: train loss 0.209 | validation accuracy 0.867
epoch  50: train loss 0.070 | validation accuracy 0.983
epoch 200: train loss 0.021 | validation accuracy 1.000
শূন্য থেকে শুরু: accuracy 0.55 | W1 এখনো সব শূন্য? True

প্রথম epoch-এর পরেই train loss ০.৩৯১, কারণ এক epoch-এ ৮টি mini-batch মানে ৮টি ধাপ। ২০০ epoch-এ loss ০.০২১ আর validation-এর ৬০টি বিন্দুর সবগুলো ঠিক। বাঁয়ের ছবিতে loss curve প্রথমে খাড়া নামে, পরে সমতল হয়; মাঝের ছবিতে accuracy সিঁড়ির মতো লাফায়, কারণ ৬০টি বিন্দুতে প্রতিটি বিন্দু ১/৬০ ≈ ০.০১৭. ডানের ছবিতে decision boundary দুই চাঁদের মাঝ দিয়ে একটি বাঁকা রেখা (আসলে অনেক ছোট সরল টুকরো জোড়া), যা পর্ব ৫৯-এর logistic regression কখনো আঁকতে পারত না। শূন্য থেকে শুরু করা network ৫০ epoch পরেও W1-এ শূন্য, আর accuracy ০.৫৫: শুধু b2 শিখে সব বিন্দুকে একই শ্রেণি বলছে, যা validation-এ সংখ্যাগরিষ্ঠ শ্রেণির অনুপাত। সংখ্যাগুলো একটি seed-এর একটি run; অন্য seed-এ সামান্য আলাদা হবে।

৮ · হাতে লেখা অঙ্ক, MLPClassifier আর PyTorch — একই ধারণা, তিনটি হাতিয়ার

খেলনা ডেটা থেকে একটি সত্যিকারের, যদিও ছোট, ডেটাসেটে যাই। scikit-learn-এর সঙ্গে আসা load_digits-এ ১৭৯৭টি হাতে লেখা অঙ্কের (০–৯) ৮ × ৮ pixel-এর ছবি; প্রতিটি pixel ০ থেকে ১৬. scikit-learn-এর নথি অনুযায়ী এটি UCI-এর হাতে লেখা অঙ্কের একটি ডেটাসেট থেকে নেওয়া। প্রতিটি ছবিকে ৬৪টি সংখ্যার একটি সারি ধরলে (পর্ব ১৮: ছবি মানেই array) একই MLP কাজ করে, শুধু shape বদলায়: ৬৪ → ৩২ ReLU → ১০ softmax. বাজারঘরের হাতে লেখা অর্ডার-ফর্মের মোবাইল নম্বর পড়ার কাজ এই ধরনের, যদিও আসল ছবি অনেক বড় আর অগোছালো। একই ব্লকে scikit-learn-এর MLPClassifier, যার ভেতরে একই ধরনের network আর Adam নামের একটি উন্নত optimizer; সঙ্গে ConvergenceWarning আছে কি না গুনে দেখা।

import warnings
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.exceptions import ConvergenceWarning

d = load_digits()                                     # ৮×৮ pixel-এর হাতে লেখা অঙ্ক
Xa, Xb, ya, yb = train_test_split(d.data / 16, d.target, test_size=0.25,
                                  random_state=42, stratify=d.target)
print("train", Xa.shape, "| test", Xb.shape, "| pixel-এর মান 0 থেকে 1")

rng = np.random.default_rng(42)
P = init(64, 32, 10)                                  # 64 -> 32 ReLU -> 10 softmax
hist = train(P, Xa, ya, Xb, yb, epochs=30, lr=0.3)
print("নিজের MLP:", sum(v.size for v in P.values()), "parameter | train loss",
      hist[-1, 0].round(3), "| test accuracy", hist[-1, 1].round(3))
pred = forward(P, Xb)[2].argmax(1)
wrong = np.bincount(yb[pred != yb], minlength=10)
print("কোন অঙ্ক কতবার ভুল:", wrong.tolist())

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    clf = MLPClassifier(hidden_layer_sizes=(32,), max_iter=1000, random_state=42).fit(Xa, ya)
n_conv = sum(issubclass(w.category, ConvergenceWarning) for w in caught)
print("MLPClassifier: test accuracy", round(clf.score(Xb, yb), 3), "| iteration", clf.n_iter_,
      "| ConvergenceWarning", n_conv)

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

train (1347, 64) | test (450, 64) | pixel-এর মান 0 থেকে 1
নিজের MLP: 2410 parameter | train loss 0.026 | test accuracy 0.964
কোন অঙ্ক কতবার ভুল: [2, 3, 1, 1, 0, 2, 1, 2, 2, 2]
MLPClassifier: test accuracy 0.976 | iteration 398 | ConvergenceWarning 0

৪৫০টি test ছবিতে নিজের ২৪১০-parameter-এর MLP ৯৬.৪% ঠিক বলে, মাত্র ৩০ epoch-এ। Parameter: ৬৪ × ৩২ + ৩২ + ৩২ × ১০ + ১০ = ২৪১০. ভুলগুলো দশটি অঙ্কে মোটামুটি ছড়ানো, কোনো একটিতে জমা নয়। MLPClassifier (একই ৩২টি hidden unit, ReLU, Adam, আর default-এ একটি ছোট L2 penalty alpha=0.0001) ৯৭.৬% দেয়, প্রায় ৪০০ iteration-এ নিজের থামার নিয়মে থেমে; ConvergenceWarning শূন্য। দুটি সংখ্যা কাছাকাছি, এটিই যাচাই: হাতে লেখা network একটি পরীক্ষিত library-র মতোই আচরণ করছে (“iteration” এখানে epoch-এর সমান)। Test ডেটা শুধু শেষ মাপের জন্য; epoch বা hidden unit বাছতে আলাদা validation ভাগ লাগবে (পর্ব ৫৬, পর্ব ৬৬)।

ConvergenceWarning — থামার আগে থামানো

max_iter কম হলে MLPClassifier শেষ না করেই থামে আর একটি ConvergenceWarning দেয়। এই warning উপেক্ষা করা AI-এর লেখা কোডে খুব সাধারণ। ডেস্কটপে তিন রকম সেটিংয়ে একই ডেটা (সত্যিকারের run, Python 3.14.4, scikit-learn 1.9.1, লেখার সময়ে সর্বশেষ স্থিতিশীল; পাতার ব্রাউজারে 1.8.0):

~/bazarghar-ai$ python mlp_warning.py
~/bazarghar-ai/.venv/Lib/site-packages/sklearn/neural_network/_multilayer_perceptron.py:785: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (20) reached and the optimization hasn't converged yet.
  warnings.warn(
{'max_iter': 20} -> iteration 20 | test accuracy 0.88
{'max_iter': 1000} -> iteration 398 | test accuracy 0.976
{'early_stopping': True, 'max_iter': 1000} -> iteration 44 | test accuracy 0.896

২০ iteration-এ থামানো model-এর accuracy ০.৮৮, আর warning স্পষ্ট বলছে শেষ হয়নি। max_iter=1000 দিলে নিজে থেকে ৩৯৮-এ থামে। early_stopping=True train ডেটার ১০% আলাদা রেখে সেখানে score আর না বাড়লে থামে: এখানে ৪৪-এ থেমে ০.৮৯৬, কম; default ধৈর্য (n_iter_no_change=10) আর ছোট validation ভাগে এই ডেটায় আগেভাগে থেমে গেছে। কোন সেটিং ভালো, তা মেপে ঠিক করতে হয়; এটি একটি ডেটায় একটি run, সাধারণ নিয়ম নয়।

Overfitting আর তার ওষুধ — dropout আর weight decay

২৪১০ parameter আর ১৩৪৭টি train ছবি: parameter উদাহরণের চেয়ে বেশি। বড় network সহজেই train ডেটা মুখস্থ করতে পারে (পর্ব ৬১-এর overfitting); train loss কমছে অথচ validation loss বাড়ছে, তাহলে মুখস্থ শুরু। তিনটি প্রচলিত প্রতিকার:

  • Weight decay: loss-এ (λ/2)‖W‖2 যোগ, অর্থাৎ পর্ব ৬১-এর L2 regularization; gradient-এ শুধু λW যোগ হয়। scikit-learn-এ এটি alpha. (Adam-এর মতো optimizer-এ weight decay আর L2 penalty হুবহু এক নয়; পরের Deep Learning কোর্সের বিষয়।)
  • Dropout: training-এর প্রতি ধাপে hidden unit-এর একটি অংশ এলোমেলোভাবে শূন্য করা, যাতে network কোনো একটি unit-এর ওপর নির্ভর না করে; test-এর সময় সব unit চালু। পরিচিত উৎস Hinton ও সহকর্মীদের ২০১২ আর Srivastava ও সহকর্মীদের ২০১৪ সালের কাজ। MLPClassifier-এ dropout নেই; PyTorch-এ nn.Dropout.
  • Early stopping: validation-এর score আর না বাড়লে থামা, ওপরের run-এ যা দেখলেন।

রোগ না থাকলে ওষুধ ক্ষতিও করে: অনুশীলন ২-এ দেখবেন, overfitting-হীন খেলনা ডেটায় বড় λ accuracy কমায়।

PyTorch-এ একই network — autograd দিয়ে মিলিয়ে দেখা

বাস্তব কাজে কেউ হাতে backprop লেখেন না; PyTorch-এর মতো framework প্রতিটি হিসাব একটি computational graph-এ লিখে রাখে আর loss.backward() ডাকলে chain rule নিজে চালায় (automatic differentiation, PyTorch-এ যার নাম autograd). PyTorch ব্রাউজারের Python-এ চলে না, তাই ডেস্কটপে CPU-র জন্য তৈরি PyTorch install করে একই খেলনা ডেটায় দুটি জিনিস চালানো হলো: আমাদের NumPy network-এর একই শুরুর weight PyTorch-এ বসিয়ে gradient তুলনা, তারপর PyTorch-এর ভাষায় একই training loop. mlp_numpy.py হলো ওপরের প্রথম shared ব্লকের ডেটা আর function (gradient check বাদে)। Tensor হলো PyTorch-এর array, NumPy-র ndarray-এর মতো, সঙ্গে gradient রাখার ব্যবস্থা।

import numpy as np, torch
from torch import nn
from mlp_numpy import Xtr, ytr, Xva, yva, init, loss_grads   # একই ডেটা আর NumPy MLP

torch.manual_seed(0)
print("torch", torch.__version__, "| numpy", np.__version__)
model = nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 2)).double()
loss_fn = nn.CrossEntropyLoss()                    # softmax + cross-entropy একসঙ্গে

# ১) autograd বনাম হাতে লেখা backprop: একই শুরুর ওজনে gradient মেলে কি?
P = init(2, 16, 2)
with torch.no_grad():
    model[0].weight.copy_(torch.tensor(P["W1"].T)); model[0].bias.copy_(torch.tensor(P["b1"]))
    model[2].weight.copy_(torch.tensor(P["W2"].T)); model[2].bias.copy_(torch.tensor(P["b2"]))
X, y = torch.tensor(Xtr, dtype=torch.float64), torch.tensor(ytr)
loss = loss_fn(model(X), y)
loss.backward()                                    # autograd: পুরো backprop এক লাইনে
L_np, G = loss_grads(P, Xtr, ytr)
print(f"loss: torch {loss.item():.6f} | numpy {L_np:.6f}")
print("W1-এর gradient-এ সর্বোচ্চ পার্থক্য:", f"{np.abs(model[0].weight.grad.numpy().T - G['W1']).max():.1e}")

# ২) একই training loop, PyTorch-এর ভাষায়
opt = torch.optim.SGD(model.parameters(), lr=0.2)
g = torch.Generator().manual_seed(0)
for epoch in range(200):
    for idx in torch.randperm(len(y), generator=g).split(32):
        opt.zero_grad()
        loss_fn(model(X[idx]), y[idx]).backward()
        opt.step()
with torch.no_grad():
    acc = (model(torch.tensor(Xva)).argmax(1).numpy() == yva).mean()
    print(f"200 epoch পরে train loss {loss_fn(model(X), y).item():.3f} | validation accuracy {acc:.3f}")

সত্যিকারের run (ডেস্কটপ, CPU, Python 3.14.4, PyTorch 2.15.0 CPU build, NumPy 2.5.4; লেখার সময়ের সংস্করণ):

~/bazarghar-ai$ python mlp_torch.py
torch 2.15.0+cpu | numpy 2.5.4
loss: torch 1.132768 | numpy 1.132768
W1-এর gradient-এ সর্বোচ্চ পার্থক্য: 5.6e-17
200 epoch পরে train loss 0.021 | validation accuracy 1.000

একই শুরুর weight-এ দুই loss ছয় দশমিক পর্যন্ত এক (১.১৩২৭৬৮, ওপরের gradient check ব্লকের শুরুর loss-এর সঙ্গেও মেলে), আর autograd-এর gradient আমাদের হাতে লেখা gradient থেকে সর্বোচ্চ 5.6 × 10−17 দূরে, float64-এর গোল করার মাপ। অর্থাৎ loss.backward() ঠিক সেই হিসাবই করে, যা আমরা অংশ ৬-এ হাতে করেছি। দুটি খুঁটিনাটি: nn.Linear weight-কে (output, input) shape-এ রাখে, তাই .T; আর CrossEntropyLoss নিজেই softmax লাগায়, তাই সরাসরি logits নেয়। Model-এর শেষে আরেকটি softmax বসালে কোড চলবে কিন্তু শেখা খারাপ হবে, একটি পরিচিত ভুল।

৯ · বড় ছবি — universal approximation, deep learning-এর ফিরে আসা, আর এই কোর্স যা শেখায় না

একটি hidden layer যথেষ্ট চওড়া হলে কতটা পারে? ১৯৮৯ সালে George Cybenko (sigmoid-এর জন্য) আর একই সময়ে Kurt Hornik, Maxwell Stinchcombe ও Halbert White দেখান, একটি hidden layer-এর network যথেষ্ট unit পেলে একটি সীমিত এলাকায় যেকোনো ধারাবাহিক function-কে যত কাছে চাই তত কাছে আনতে পারে। একে বলে universal approximation theorem. কথাটি শুনতে বিশাল, কিন্তু সীমাগুলো জরুরি: উপপাদ্য বলে এমন weight আছে, তা খুঁজে পাওয়া যাবে কি না বলে না; কতগুলো unit লাগবে, তা অনেক সময় অবাস্তব রকম বেশি হতে পারে; আর train ডেটায় মিললেই নতুন ডেটায় ভালো হবে, এমন কোনো নিশ্চয়তা নেই। বাস্তবে গভীর (অনেক স্তরের) network অনেক কাজে কম parameter-এ ভালো করে বলে দেখা যায়; কেন, তা নিয়ে তাত্ত্বিক গবেষণা এখনো চলছে।

Neural network-এর ধারণা পুরোনো, তাহলে ২০১০-এর দশকে হঠাৎ এত সাফল্য কেন? সাধারণ ব্যাখ্যা (পর্ব ৪-এ বিস্তারিত): বড় লেবেলসহ ডেটা (যেমন ImageNet), GPU-তে দ্রুত matrix গুণ, আর ReLU, dropout, ভালো initialization-এর মতো কৌশল। প্রতীক হিসেবে প্রায়ই বলা হয় ২০১২ সালের ImageNet প্রতিযোগিতা, যেখানে Alex Krizhevsky, Ilya Sutskever আর Geoffrey Hinton-এর AlexNet নামের একটি convolutional network আগের পদ্ধতিগুলোর চেয়ে স্পষ্টভাবে কম ভুল করে। তবে কোন কারণের ভূমিকা কত বড়, আর আগের কাজের কৃতিত্ব কতটা, তা নিয়ে মতভেদ আছে; একটি ঘটনাকে “শুরু” বলা সবসময়ই সরলীকরণ।

এই কোর্সের লক্ষ্য ভিত, তাই অনেক কিছু ইচ্ছা করে বাদ। ছবির জন্য CNN (convolutional network, যা কাছাকাছি pixel-এর গঠন কাজে লাগায়), ক্রম বা সময়ের ডেটার জন্য RNN আর LSTM, আর আজকের language model-এর transformer স্থাপত্য পরের Deep Learning কোর্সের বিষয়। Attention-এর মূল ধারণাটুকু, তিনটি token-এর ছোট NumPy হিসাবসহ, আসবে পর্ব ৭০ (LLM কীভাবে কাজ করে — token, পরের শব্দের অনুমান আর সীমা)-এ। Momentum, Adam, batch normalization আর GPU-তে training-ও পরের কোর্সে; তবে forward, loss, backward আর হালনাগাদের এই চক্রই সবগুলোর নিচে।

আরও গভীরে: Backprop আসলে reverse-mode automatic differentiation-এর একটি বিশেষ রূপ: একটি সংখ্যা (loss) থেকে লক্ষ লক্ষ input (weight)-এর gradient এক backward pass-এ, forward-এর মোটামুটি কয়েক গুণ খরচে, পাওয়া যায়; forward-mode-এ প্রতিটি weight-এর জন্য আলাদা pass লাগত। এর দাম memory: সব মধ্যবর্তী মান (Z1, H…) রাখতে হয়, যা বড় model-এ বড় বাধা, আর “gradient checkpointing” কিছু মান ফেলে দিয়ে পরে আবার হিসাব করে memory বাঁচায়। Initialization-এর পেছনের যুক্তি পড়তে Glorot ও Bengio (২০১০) আর He ও সহকর্মীদের (২০১৫) প্রবন্ধ, আর তত্ত্বের দিকে Cybenko (১৯৮৯) ভালো শুরু। Ian Goodfellow, Yoshua Bengio ও Aaron Courville-এর বই Deep Learning (২০১৬)-এর ৬ নম্বর অধ্যায় এই পর্বের প্রায় পুরোটা গভীরভাবে দেখায়।

১০ · AI-এর যুগে — AI assistant-এর লেখা neural network কোড যাচাই

AI assistant কয়েক সেকেন্ডে চলনসই neural network কোড দেয়। সমস্যা হলো, এখানে অনেক ভুল কোড ভাঙে না, শুধু খারাপ শেখে: loss কমে, একটি accuracy ছাপা হয়, আর ভুলটি চোখের আড়ালে থাকে।

AI যা প্রায়ই করেকীভাবে ধরবেন
Backprop-এ একটি transpose বা ঢাল ভুল, তবু loss একটু কমেছোট ডেটায় gradient check; পার্থক্য 10−6-এর বেশি হলে খুঁজুন
PyTorch-এ model-এর শেষে softmax বসিয়ে তারপর CrossEntropyLossDocs পড়ুন: এটি logits নেয়; শেষ স্তরে কোনো softmax নয়
সব weight শূন্যে শুরু, বা বিশাল scale-এশুরুর loss ln k-এর কাছাকাছি কি না; hidden unit-গুলোর মান আলাদা কি না
ConvergenceWarning চুপ করাতে warnings.filterwarnings("ignore")Warning পড়ুন; max_iter বাড়ান বা early_stopping, আর loss curve দেখুন
Train accuracy বা test ডেটায় বারবার মেপে hyperparameter বাছাTrain/validation/test আলাদা; test একবার; সরল baseline (logistic regression)-এর সঙ্গে তুলনা
পুরোনো API: torch.autograd.Variable, NumPy-র np.float, অস্তিত্বহীন parameterনিজের সংস্করণের অফিসিয়াল docs; কোড চালিয়ে সব warning পড়া
“Network বুঝে ফেলেছে” বা “মস্তিষ্কের মতো শিখেছে” ধরনের ব্যাখ্যাযা মাপা হয়েছে শুধু তা লেখা: কোন ডেটায় কত accuracy, কোন সীমায়

নির্দিষ্ট করে চান, আর পরীক্ষাযোগ্য আউটপুট চান। আসল গ্রাহকের তথ্য, ছবি বা ফোন নম্বর কখনো পেস্ট করবেন না; কাল্পনিক বা public ডেটাই যথেষ্ট।

Python 3.14 আর NumPy 2 দিয়ে একটি ২-স্তরের MLP লেখো (input → ReLU hidden → softmax output), কোনো deep learning library ছাড়া। শর্ত: ১. প্রতিটি function-এর docstring-এ প্রতিটি array-এর shape লিখবে, আর forward-এর প্রতিটি ধাপে assert দিয়ে shape যাচাই করবে। ২. softmax-এ সারির সর্বোচ্চ মান বিয়োগ করে stable হিসাব; loss হবে mean cross-entropy. ৩. weight শুরু হবে np.random.default_rng(42) দিয়ে, scale √(2/d); কেন শূন্য নয়, comment-এ এক লাইনে লিখবে। ৪. central difference দিয়ে সব parameter-এ gradient check, আর সবচেয়ে বড় পার্থক্য ছাপাবে। ৫. mini-batch SGD, প্রতিটি epoch-এ train loss আর আলাদা validation ডেটায় accuracy রাখবে; test ডেটা শুধু শেষে একবার। ৬. scikit-learn-এর load_digits-এ চালিয়ে MLPClassifier আর LogisticRegression-এর সঙ্গে test accuracy তুলনা করবে, আর কোনো warning চুপ করাবে না।

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

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

  • Perceptron XOR কেন শিখতে পারে না? XOR linearly separable নয়: b ≤ ০, w1 + b > ০, w2 + b > ০ থেকে w1 + w2 + b > ০ আসে, যা (1,1) → ০-এর সঙ্গে বিরোধী।
  • Activation ছাড়া অনেক স্তর কেন অর্থহীন? (xA)B = x(AB): linear স্তরগুলো একটি linear স্তরে মিশে যায়।
  • Softmax + cross-entropy-র output gradient? ∂L/∂Z = (P − Y)/n; sigmoid + binary cross-entropy-তে p − y.
  • সব weight শূন্যে শুরু করলে? একই স্তরের সব unit একই gradient পায়, সমতা কখনো ভাঙে না; তাই এলোমেলো শুরু, scale প্রায় √(1/d) বা ReLU-তে √(2/d).
  • ৬৪ → ৩২ → ১০ MLP-এ parameter কত? ৬৪ × ৩২ + ৩২ + ৩২ × ১০ + ১০ = ২৪১০.

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

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

প্র ০১প্রায়ই শোনা যায়, Minsky আর Papert-এর ১৯৬৯ সালের বই neural network গবেষণাকে “মেরে” ফেলেছিল। এই গল্পের কতটা সত্য, আর এখান থেকে কী শেখার আছে?

গল্পের একটি শক্ত অংশ সত্য। Perceptrons বইটি এক স্তরের perceptron-এর সীমা গাণিতিকভাবে কঠোরভাবে দেখায়: XOR-এর মতো সরল কাজ, আর ছবির “সংযুক্ত কি না” ধরনের বৈশিষ্ট্য, স্থানীয় তথ্য দেখা এক স্তরের perceptron দিয়ে কার্যকরভাবে করা যায় না। এই পর্বের চার লাইনের প্রমাণ তারই সবচেয়ে ছোট রূপ। Rosenblatt আর তাঁর সময়ের কিছু প্রচার perceptron-এর ক্ষমতা নিয়ে বেশ উচ্চাশা ছড়িয়েছিল বলে বর্ণনা পাওয়া যায়, তাই কঠোর সীমা দেখানোর একটি বৈজ্ঞানিক মূল্য ছিল।

কিন্তু “মেরে ফেলেছিল” কথাটি অতিরঞ্জন। প্রথমত, বইটি বহুস্তরের network অসম্ভব প্রমাণ করেনি; বহুস্তরের network-কে কীভাবে train করতে হবে, সেটিই তখন অজানা ছিল। দ্বিতীয়ত, ১৯৭০-এর দশকে AI-এর তহবিল কমার পেছনে আরও কারণ ছিল: যুক্তরাজ্যে ১৯৭৩ সালের Lighthill রিপোর্ট, যুক্তরাষ্ট্রে সামরিক গবেষণা তহবিলের নীতিতে পরিবর্তন, আর পুরো AI ক্ষেত্রের অনেক প্রতিশ্রুতি না মেলা। ইতিহাসবিদরা বইটির ভূমিকা কতটা বড় ছিল তা নিয়ে একমত নন। তৃতীয়ত, কিছু গবেষক এই সময়েও neural network নিয়ে কাজ চালিয়ে গেছেন, আর ১৯৮০-এর দশকে Hopfield network, Boltzmann machine আর ১৯৮৬ সালের backpropagation প্রবন্ধ দিয়ে আগ্রহ ফিরে আসে। ১৯৮৮ সালে বইটির বর্ধিত সংস্করণে লেখকরা নিজেরাই এই নতুন কাজ নিয়ে মন্তব্য করেন।

শিক্ষাটি দুই দিকের। একটি সীমার প্রমাণ সবসময় নির্দিষ্ট অনুমানের অধীনে: “এক স্তরের, এই ধরনের input-এ” সীমা প্রমাণ মানে “neural network পারে না” নয়। আবার উচ্চাশার প্রচার একটি ক্ষেত্রকে বিপদে ফেলতে পারে, যখন প্রতিশ্রুতি না মেলে। আজকের AI আলোচনাতেও দুটিই চোখে পড়ে: কেউ একটি পরীক্ষার ব্যর্থতাকে পুরো পদ্ধতির মৃত্যু বলেন, কেউ একটি সাফল্যকে সাধারণ বুদ্ধিমত্তা বলেন।

মূল উপলব্ধি: একটি গাণিতিক সীমা ঠিক কোন অনুমানে প্রমাণিত, তা পড়ুন; perceptron-এর সীমা ছিল এক স্তরের, আর তার সমাধান ছিল hidden layer আর তাকে train করার পদ্ধতি।

প্র ০২“Neural network মানুষের মস্তিষ্কের মতো কাজ করে” — এই দাবি কতটা টেকে? মিল কোথায়, অমিল কোথায়, আর কেন এই প্রশ্ন জরুরি?

মিলটি ঐতিহাসিক আর খুব সাধারণ স্তরের। McCulloch আর Pitts ১৯৪৩ সালে নিউরনকে একটি যুক্তির একক হিসেবে আঁকেন; Rosenblatt তাঁর perceptron-কে মস্তিষ্কে তথ্য রাখার একটি model হিসেবে উপস্থাপন করেন; আর Donald Hebb-এর ১৯৪৯ সালের ধারণা, একসঙ্গে সক্রিয় নিউরনের সংযোগ শক্ত হয়, শেখার নিয়মের অনুপ্রেরণা দেয়। দুই জায়গাতেই অনেক সরল একক, সংযোগের শক্তি, আর অভিজ্ঞতায় সেই শক্তি বদলানো। কিছু গবেষণায় ছবি চেনার network-এর ভেতরের স্তর আর প্রাণীর দৃষ্টিপথের কিছু স্তরের মধ্যে পরিসংখ্যানগত মিলও দেখানো হয়েছে, যা neuroscience-এ একটি সক্রিয় গবেষণার ধারা।

অমিল অনেক গভীর। আসল নিউরন spike-এর সময় দিয়ে তথ্য পাঠায়, রাসায়নিক সংকেতে নিয়ন্ত্রিত হয়, তার dendrite-এ নিজস্ব জটিল হিসাব হয় বলে গবেষণায় দেখা যায়, আর মস্তিষ্কে বহু ধরনের নিউরন। আমাদের neuron একটি গুণ-যোগ আর একটি ReLU. সবচেয়ে বড় প্রশ্ন শেখা নিয়ে: backpropagation-এ ভুলের সংকেত একই weight দিয়ে উল্টো পথে যায় আর পুরো network-এর হিসাব মনে রাখতে হয়; মস্তিষ্ক ঠিক এভাবে শেখে, এমন প্রমাণ নেই, আর এটি জৈবিকভাবে সম্ভব কি না, তা নিয়ে বিতর্ক চলছে। এ ছাড়া একটি শিশু অল্প কয়েকটি উদাহরণে নতুন বস্তু চেনে, যেখানে আমাদের ছোট network-ও হাজার উদাহরণ চায়।

প্রশ্নটি জরুরি, কারণ ভুল রূপক ভুল প্রত্যাশা তৈরি করে। “মস্তিষ্কের মতো” শুনলে মানুষ ধরে নেন model বোঝে, সাধারণ জ্ঞান রাখে, বা নিজে থেকে নৈতিক হবে। বাস্তবে এই পর্বের network একটি বড় গাণিতিক function, যার weight একটি loss কমাতে ঠিক হয়েছে; train ডেটার বাইরে সে কী করবে, তা মাপতে হয়। প্রকৌশলের ভাষা তাই বেশি সৎ: “parametrised function”, “loss কমানো”, “এই ডেটায় এই accuracy”.

মূল উপলব্ধি: মস্তিষ্ক neural network-এর অনুপ্রেরণা, নীলনকশা নয়; model কী পারে তা রূপক দিয়ে নয়, মাপ দিয়ে বলুন।

প্র ০৩Universal approximation theorem বলে এক hidden layer-ই যেকোনো ধারাবাহিক function কাছাকাছি আনতে পারে। তাহলে বাস্তবে গভীর, অনেক স্তরের network কেন?

উপপাদ্যটি অস্তিত্বের কথা বলে, খরচ বা খোঁজার কথা নয়। Cybenko আর Hornik ও সহকর্মীদের ১৯৮৯ সালের ফল বলে, যথেষ্ট unit থাকলে এমন weight আছে; কিন্তু “যথেষ্ট” কত, তা function-এর ওপর নির্ভর করে, আর কিছু function-এ এক স্তরে unit-এর সংখ্যা ভয়ানক দ্রুত বাড়তে হয়। তাত্ত্বিক গবেষণায় এমন function-এর উদাহরণ দেখানো হয়েছে যা কয়েকটি স্তরে অল্প unit-এ লেখা যায়, অথচ কম স্তরে লিখতে অনেক বেশি unit লাগে (যেমন Telgarsky-র ২০১৬ সালের কাজ)। সহজ একটি ভাবনা: XOR-এর মতো একটি “ভাঁজ” প্রতিটি স্তরে আবার ব্যবহার করা যায়, ফলে স্তর বাড়লে ভাঁজের সংখ্যা গুণে গুণে বাড়ে, যেখানে চওড়া করলে শুধু যোগ হয়।

দ্বিতীয় কারণ গঠনের পুনর্ব্যবহার। বাস্তব ডেটার গঠন প্রায়ই স্তরে স্তরে: ছবিতে রেখা থেকে কোণ, কোণ থেকে আকার, আকার থেকে বস্তু; ভাষায় অক্ষর থেকে শব্দ, শব্দ থেকে বাক্য। গভীর network এই ক্রমকে স্বাভাবিকভাবে ধরতে পারে বলে ধারণা করা হয়, যদিও “network ঠিক এভাবেই শেখে” বলাটা সবসময় প্রমাণিত নয়। তৃতীয় কারণ বাস্তব অভিজ্ঞতা: ২০১০-এর দশকে অনেক কাজে গভীর model ভালো ফল দিয়েছে, আর residual connection-এর মতো কৌশল গভীর network-এর training সহজ করেছে।

তবে গভীরতার দামও আছে: vanishing বা exploding gradient, বেশি hyperparameter, বেশি হিসাব, আর ছোট ডেটায় overfitting. তাই “যত গভীর তত ভালো” নিয়মও ভুল। ছোট tabular ডেটায় একটি ছোট network বা এমনকি logistic regression প্রায়ই যথেষ্ট। আর উপপাদ্য কখনো বলে না যে gradient descent সেই ভালো weight খুঁজে পাবে, বা train ডেটার বাইরে model ভালো করবে; generalization একটি আলাদা প্রশ্ন, যা মেপে দেখতে হয়।

মূল উপলব্ধি: “পারা সম্ভব” আর “অল্প খরচে শেখা সম্ভব” আলাদা; গভীরতা দ্বিতীয়টিতে সাহায্য করে বলে দেখা যায়, প্রথমটির জন্য লাগে না।

প্র ০৪বাজারঘরের অর্ডারের মতো একটি ছোট tabular ডেটায় (কয়েক হাজার সারি, দশ-বারোটি কলাম) neural network নাকি logistic regression বা gradient boosting? সিদ্ধান্ত কীভাবে নেবেন?

এখানে সম্প্রদায়ে সত্যিকারের মতভেদ আছে, আর উত্তর ডেটার ওপর নির্ভর করে। ছবি, শব্দ আর লেখায় neural network-এর সাফল্য স্পষ্ট, কারণ সেখানে কাঁচা ডেটার গঠন (পাশাপাশি pixel, শব্দের ক্রম) network কাজে লাগাতে পারে। Tabular ডেটায় কলামগুলোর এমন কোনো সাধারণ গঠন নেই; প্রতিটি কলাম আলাদা অর্থের। কয়েকটি তুলনামূলক গবেষণা (যেমন Grinsztajn, Oyallon ও Varoquaux-এর ২০২২ সালের একটি benchmark) দেখিয়েছে, মাঝারি আকারের tabular ডেটায় gradient boosting-এর মতো tree-ভিত্তিক model প্রায়ই neural network-এর সমান বা ভালো করে, আর tune করা সহজ। অন্যদিকে নতুন ধরনের tabular network আর বড় pretrained model নিয়ে গবেষণা চলছে, তাই এটি একটি চলমান প্রশ্ন।

বাস্তব সিদ্ধান্তের একটি সৎ পদ্ধতি আছে। প্রথমে একটি সরল baseline: logistic regression (পর্ব ৫৯), scaling আর encoding একটি pipeline-এ (পর্ব ৬৪)। তারপর একটি gradient boosting model, তারপর দরকার হলে একটি ছোট MLP, সবগুলো একই cross-validation-এ (পর্ব ৬৬)। তুলনা শুধু accuracy-তে নয়: ব্যাখ্যা দেওয়া কতটা সহজ (ফেরতের odds ratio বনাম ২৪১০টি weight), training আর চালানোর খরচ, ডেটা বদলালে model কতটা স্থির, আর কতটা tune করতে হয়। MLP scaling-এর প্রতি সংবেদনশীল, initialization আর learning rate-এ ফল বদলায়, আর একই সেটিংয়ে seed বদলালে ফল সামান্য বদলায়, তাই কয়েকটি seed-এ চালানো উচিত।

যদি সব model প্রায় একই ফল দেয়, সরলটিই সাধারণত ভালো পছন্দ: ব্যাখ্যা করা সহজ, ভুল ধরা সহজ, চালানো সস্তা। Neural network তখন যুক্তিসঙ্গত, যখন ডেটা বড়, feature-এর মধ্যে জটিল সম্পর্ক আছে, বা লেখা আর ছবির মতো কাঁচা ডেটা tabular কলামের সঙ্গে মেশাতে হবে।

মূল উপলব্ধি: Model-এর ফ্যাশন নয়, একই ভাগে মাপা তুলনা আর baseline সিদ্ধান্ত দেয়; tabular ডেটায় neural network স্বয়ংক্রিয়ভাবে সেরা নয়।

প্র ০৫PyTorch-এর autograd নির্ভুলভাবে gradient দেয়। তাহলে হাতে backpropagation আর gradient check শেখার দরকার কী?

প্রথম কারণ ভুল ধরা। Autograd যা লেখা হয়েছে তার নির্ভুল derivative দেয়; কিন্তু যা লেখা হয়েছে তা ভুল হলে নির্ভুলভাবে ভুল জিনিসের gradient দেয়। শেষে বাড়তি softmax, ভুল axis-এ গড়, label-এর ভুল dtype: এসব ক্ষেত্রে কোড চলে, loss কমে, কিন্তু শেখা খারাপ হয়। Backprop বুঝলে আপনি জানেন gradient-এর আকার আর চিহ্ন কী হওয়া উচিত, যেমন output-এ P − Y, আর তা থেকে সন্দেহ শুরু হয়। Andrej Karpathy ২০১৬ সালের একটি পরিচিত লেখায় এই যুক্তিই দেন: backprop একটি “leaky abstraction”, যার ভেতর না বুঝলে vanishing gradient, dead ReLU বা বিস্ফোরিত gradient-এর মতো সমস্যা চোখে পড়ে না।

দ্বিতীয় কারণ নতুন কিছু বানানো। নিজের loss, নিজের স্তর, বা কোনো custom operation লিখলে, বিশেষ করে যখন সংখ্যার স্থিতিশীলতার জন্য derivative হাতে লিখতে হয়, তখন gradient check-ই নিশ্চিত হওয়ার উপায়। PyTorch নিজেও torch.autograd.gradcheck নামে এমন একটি হাতিয়ার দেয়, যা ঠিক এই পর্বের central difference পদ্ধতি; library-র লেখকরাও এভাবে যাচাই করেন।

তৃতীয় কারণ খরচ বোঝা। Backward pass-এ forward-এর সব মধ্যবর্তী মান memory-তে রাখতে হয়; বড় model-এ GPU-র memory ফুরিয়ে যাওয়ার কারণ প্রায়ই এটিই। কোন স্তর কত হিসাব আর memory নেয়, shape থেকে তা হিসাব করতে পারলে batch size, model-এর আকার আর হার্ডওয়্যার নিয়ে যুক্তিসঙ্গত সিদ্ধান্ত নেওয়া যায়। আর ইন্টারভিউ বা লিখিত পরীক্ষায় এক ধাপ backprop হাতে করানো খুব সাধারণ, কারণ তা দেখায় প্রার্থী chain rule আর shape দুটিই বোঝেন।

মূল উপলব্ধি: Autograd হিসাব করে, বোঝে না; হাতে backprop জানা আপনাকে gradient-এর সঠিক রূপ জানায়, আর gradient check সেই জ্ঞানকে যাচাইয়ে বদলায়।

অনুশীলন

হাতে হিসাব, কোড, সমালোচনা আর আউটপুট অনুমান; প্রতিটির উত্তর নিচে লুকানো আছে।

  1. এক ধাপ backprop (হিসাব): একটি ১-১-১ network: h = ReLU(w1x + b1), p = σ(w2h + b2), loss = −ln p (y = ১)। x = ২, w1 = ০.৫, b1 = ০, w2 = −১, b2 = ০.৫, η = ০.১. Forward, প্রতিটি gradient, হালনাগাদ আর নতুন loss বের করুন।
    • Forward: z1 = ০.৫ × ২ = ১, h = ReLU(১) = ১; z2 = −১ × ১ + ০.৫ = −০.৫; p = σ(−০.৫) = ০.৩৭৭৫; L = −ln ০.৩৭৭৫ = ০.৯৭৪১.
    • Output: δ2 = p − y = −০.৬২২৫; ∂w2 = δ2·h = −০.৬২২৫; ∂b2 = −০.৬২২৫.
    • Hidden: δ2·w2 = ০.৬২২৫; z1 = ১ > ০, তাই ReLU-এর ঢাল ১, δ1 = ০.৬২২৫; ∂w1 = δ1·x = ১.২৪৪৯; ∂b1 = ০.৬২২৫.
    • হালনাগাদ: w1 = ০.৫ − ০.১২৪৫ = ০.৩৭৫৫; b1 = −০.০৬২২; w2 = −১ + ০.০৬২২ = −০.৯৩৭৮; b2 = ০.৫৬২২.
    • নতুন forward: h = ০.৩৭৫৫ × ২ − ০.০৬২২ = ০.৬৮৮৮; z2 = −০.০৮৩৭; p = ০.৪৭৯১; loss ০.৯৭৪১ থেকে ০.৭৩৫৮ (ডেস্কটপে NumPy দিয়ে যাচাই করা)।

    লক্ষ করুন, w1 কমেছে, যদিও output বাড়ানো দরকার। কারণ w2 ঋণাত্মক: h বাড়লে output কমে, তাই network h কমাতে চায়। Chain rule-এর চিহ্নই এই “উল্টো” সিদ্ধান্ত নেয়। z1 ঋণাত্মক হলে ReLU-এর ঢাল ০ হতো, আর w1, b1 এই ধাপে একটুও নড়ত না।

  2. Weight decay যোগ (কোড): অংশ ৭-এর train function-এ একটি lam parameter যোগ করুন, যাতে W1 আর W2-এর gradient-এ λW যোগ হয় (bias-এ নয়)। খেলনা ডেটায় λ = ০, ০.০১ আর ০.১-এ ২০০ epoch চালিয়ে weight-এর মোট আকার ‖W‖, train loss আর validation accuracy তুলনা করুন। ফল থেকে কী বোঝেন?

    Gradient-এর লুপে মাত্র দুটি লাইন বদলায়:

    G = loss_grads(P, X[b], y[b])[1]
    for k in ("W1", "W2"):
        G[k] = G[k] + lam * P[k]        # ½λ‖W‖²-এর gradient; bias-এ penalty নেই
    for k in P:
        P[k] -= lr * G[k]

    সত্যিকারের run (ডেস্কটপ, Python 3.14.4, NumPy 2.5.4; প্রতিটি λ-এ একই seed 0):

    λ = 0.0 : ‖W‖ = 9.51 | train loss 0.021 | validation accuracy 1.000
    λ = 0.01: ‖W‖ = 4.74 | train loss 0.123 | validation accuracy 0.967
    λ = 0.1 : ‖W‖ = 1.72 | train loss 0.335 | validation accuracy 0.783

    λ বাড়লে weight-এর আকার স্পষ্টভাবে ছোট হয় (৯.৫১ → ১.৭২), ঠিক যা weight decay করার কথা। কিন্তু এই ডেটায় validation accuracy-ও কমে, ১.০০০ থেকে ০.৭৮৩. কারণ এখানে overfitting ছিলই না: λ = ০-তেই validation নিখুঁত। কড়া penalty network-কে প্রায় সরল সীমানায় চেপে দেয়, যা দুই চাঁদের বাঁক ধরতে পারে না (underfitting, পর্ব ৬১)। শিক্ষা: regularization একটি hyperparameter, যা validation-এ বাছতে হয়; overfitting দেখা গেলে তবেই কাজে আসে।

  3. সমালোচনা ও নতুন নকশা: একজন সহকর্মী লিখেছেন: “বাজারঘরের ২০০০টি অর্ডারে ফেরত আসবে কি না, তার জন্য ৫ স্তরের network বানালাম, প্রতি স্তরে ৫১২টি sigmoid unit, সব weight শূন্য দিয়ে শুরু। কোনো scaling করিনি। ConvergenceWarning আসছিল, তাই warning বন্ধ করে দিয়েছি। Training accuracy ৯৯%, তাই model প্রস্তুত; network গ্রাহকের আচরণ বুঝে ফেলেছে।” কী কী সমস্যা, আর আপনি কীভাবে নতুন করে সাজাবেন?
    • আকার: ৫ × ৫১২ unit মানে প্রায় দশ লক্ষ parameter, মাত্র ২০০০টি উদাহরণে; overfitting প্রায় নিশ্চিত।
    • শূন্য initialization: প্রতিটি স্তরের সব unit একই থাকবে; symmetry কখনো ভাঙবে না।
    • গভীর sigmoid: প্রতি স্তরে ঢাল সর্বোচ্চ ০.২৫, পাঁচ স্তরে vanishing gradient-এর ঝুঁকি; hidden layer-এ ReLU সাধারণ পছন্দ।
    • Scaling নেই: ছাড় ০–৫০ আর দিন ১–৭ একই network-এ গেলে training অস্থির আর ধীর হয়।
    • Warning বন্ধ: ConvergenceWarning বলছে training শেষ হয়নি; চুপ করালে সমস্যা লুকায়, যায় না।
    • Training accuracy আর “বুঝে ফেলেছে”: model যা দেখেছে তাতেই মাপা; আলাদা validation আর test, baseline, আর পর্ব ৬০-এর মতো FP/FN-এর খরচ দরকার। “বোঝে” একটি অপ্রমাণিত দাবি।

    নতুন নকশা: stratified train/validation/test ভাগ; শুধু train-এ fit হওয়া scaling আর one-hot-এর Pipeline; প্রথমে logistic regression baseline; তারপর একটি ছোট MLP (একটি hidden layer, ১৬–৩২টি ReLU unit, √(2/d) initialization), weight decay আর early stopping validation-এ বাছা; কয়েকটি seed; MLP baseline-কে স্পষ্টভাবে না হারালে সরল model.

  4. আউটপুট অনুমান: নিচের কোড কী ছাপবে? দ্বিতীয় সারির softmax কেন overflow করে না?
    import numpy as np
    z = np.array([[2.0, 1.0, 0.0], [1000.0, 1000.0, 1000.0]])
    e = np.exp(z - z.max(axis=1, keepdims=True))
    p = e / e.sum(axis=1, keepdims=True)
    print(p.round(3))
    print(round(-np.log(p[0, 0]), 3), round(-np.log(p[1, 2]), 3))
    X, W = np.ones((5, 3)), np.zeros((3, 4))
    print((X @ W).shape, (X @ W + np.arange(4)).shape, np.maximum(0, np.array([-2.0, 0.0, 3.0])))

    সত্যিকারের run (ডেস্কটপ, Python 3.14.4, NumPy 2.5.4):

    [[0.665 0.245 0.09 ]
     [0.333 0.333 0.333]]
    0.408 1.099
    (5, 4) (5, 4) [0. 0. 3.]

    প্রথম সারি: e0, e−1, e−2 = ১, ০.৩৬৮, ০.১৩৫, যোগফল ১.৫০৩, তাই (০.৬৬৫, ০.২৪৫, ০.০৯০)। দ্বিতীয় সারির তিনটি logit সমান, তাই সমান ভাগ, ১/৩ করে। সর্বোচ্চ বিয়োগ করায় সারিটি (0, 0, 0) হয়ে যায়, আর e0 = ১; সরাসরি e1000 নিলে overflow হয়ে inf/inf = nan আসত। Loss: −ln ০.৬৬৫ = ০.৪০৮, আর −ln(১/৩) = ln ৩ = ১.০৯৯, যা তিন শ্রেণিতে “কিছু না জানা”র loss. শেষ লাইনে (5, 3) গুণ (3, 4) = (5, 4); (4,)-এর bias broadcasting-এ প্রতিটি সারিতে যোগ হয়, shape একই থাকে; ReLU ঋণাত্মক −২ আর ০-কে ০ করে, ৩ রাখে।

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

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

ক্রমNumPy দিয়ে নিজের হাতে একটি MLP train আর যাচাইয়ের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ডেটা train, validation আর test-এ ভাগ করা
  2. Weight ছোট এলোমেলো মানে (যেমন scale √(2/d)) আর bias শূন্যে শুরু করা
  3. ছোট ডেটায় gradient check চালিয়ে backprop যাচাই করা
  4. প্রতিটি mini-batch-এ forward pass, loss আর backward pass চালানো
  5. Weight থেকে learning rate × gradient বিয়োগ করে হালনাগাদ, epoch-এর পর epoch
  6. Validation curve দেখে থামা, তারপর test-এ একবার মাপা

একটি perceptron-এর w = (1, 0), b = −1, learning rate η = 1, আর নিয়ম “s = w·x + b > 0 হলে output 1”. উদাহরণ x = (1, 1), সঠিক উত্তর t = 1. Perceptron rule প্রয়োগের পর নতুন w আর b কত?

  • কোনো বদল নেই: w = (1, 0), b = −1
  • w = (2, 1), b = 0
  • w = (0, −1), b = −2
  • w = (2, 1), b = −1

একটি MLP-এ 64টি input, 32টি unit-এর একটি hidden layer আর 10টি output শ্রেণি, প্রতিটি স্তরে bias সহ। মোট parameter কতটি?

  • ২৩৬৮
  • ২০৪৮০
  • ২৪১০
  • ১০৬

দুটি linear স্তর y = (xA)B, যেখানে মাঝে কোনো activation নেই। এই দুই-স্তরের network সম্পর্কে কোনটি সত্য?

  • এটি C = AB matrix-এর একটি মাত্র linear স্তরের সমান, তাই বাঁকা সীমানা আঁকতে পারে না
  • দুই স্তর থাকায় এটি XOR শিখতে পারে
  • শুধু bias যোগ করলেই এটি non-linear হয়ে যায়
  • Parameter বেশি বলে এটি একটি স্তরের চেয়ে বেশি ধরনের function প্রকাশ করতে পারে

তিন শ্রেণির একটি network একটি উদাহরণে logits z = (2, 1, 0) দিয়েছে, আর সঠিক শ্রেণি প্রথমটি। Softmax-এর পর cross-entropy loss (স্বাভাবিক log) প্রায় কত?

  • ০.৬৬৫
  • ০.৪০৮
  • ১.০৯৯
  • ২.০০০

একটি ReLU hidden layer-এর MLP-এর সব weight আর bias শূন্য দিয়ে শুরু করে gradient descent চালানো হলো। কী ঘটবে বলে আশা করা যায়?

  • Network দ্রুত শিখবে, কারণ শূন্য একটি নিরপেক্ষ শুরু
  • শুধু bias শূন্য বলে সমস্যা, weight শূন্য হলে কিছু হয় না
  • Hidden unit-গুলো আলাদা হতে পারে না; weight প্রায় নড়ে না, model প্রায় শুধু সংখ্যাগরিষ্ঠ শ্রেণি বলে
  • Softmax-এ overflow হয়ে loss nan হবে

দশটি sigmoid hidden layer-এর একটি network-এ শুরুর দিকের স্তরগুলো প্রায় শিখছে না। সবচেয়ে সম্ভাব্য ব্যাখ্যা আর একটি প্রচলিত প্রতিকার কোনটি?

  • Overfitting; প্রতিকার আরও স্তর যোগ করা
  • Learning rate খুব ছোট; প্রতিকার learning rate ১০০ গুণ করা
  • Backprop কোডে ভুল; প্রতিকার gradient check বন্ধ করা
  • Vanishing gradient; প্রতিকার hidden layer-এ ReLU আর উপযুক্ত initialization

শব্দকোষ

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

মনে রাখুন

  1. Neuron: a = f(w·x + b). Sigmoid বসালে logistic regression; মস্তিষ্কের সঙ্গে মিল শুধু অনুপ্রেরণার।
  2. Perceptron: ভুল হলে w ← w + η(t − out)x; linearly separable ডেটায় থামে (AND: শূন্য থেকে ৬ epoch), XOR-এ কখনো নয়, কারণ একটি রেখা যথেষ্ট নয়।
  3. Hidden layer আর activation: হাতে বসানো OR আর AND unit দিয়ে XOR; activation না থাকলে (xA)B = x(AB), স্তর মিশে যায়। Hidden layer-এ ReLU সাধারণ পছন্দ; sigmoid-এর ঢাল ≤ ০.২৫ গভীরে vanishing gradient আনে।
  4. Backprop: chain rule, পেছন থেকে: D2 = (P − Y)/n, ∂W2 = HᵀD2, D1 = D2W2ᵀ ⊙ f′(Z1), ∂W1 = XᵀD1; gradient-এর shape = parameter-এর shape; gradient check দিয়ে যাচাই।
  5. Training: এলোমেলো initialization (√(2/d)), mini-batch SGD, learning rate, loss আর validation curve; overfitting-এ weight decay, dropout, early stopping; MLPClassifier-এর ConvergenceWarning পড়ুন, আর PyTorch-এর autograd একই gradient দেয়।

পরের পর্বে: এই পর্বের hidden layer ডেটাকে একটি নতুন জায়গায় নিয়ে গিয়েছিল, যেখানে কাজ সহজ। পর্ব ৬৯ (Embedding আর vector search — অর্থ দিয়ে খোঁজা)-এ সেই ভাবনাই শব্দ আর বাক্যে: one-hot আর TF-IDF থেকে ঘন embedding, বাংলা রিভিউয়ে cosine দিয়ে খোঁজা, brute-force বনাম আনুমানিক search, আর recall@k দিয়ে মূল্যায়ন।

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

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