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

Weight initialization আর normalization — Xavier, He, BatchNorm আর LayerNorm

Weight Initialization and Normalization — Signal Propagation, Xavier, He, BatchNorm, LayerNorm and RMSNorm

Taught in Bangla, this lesson follows signal through depth in in-page NumPy, showing activations vanish or explode over 20 layers, and derives Xavier and He initialization from the variance condition. It compares PyTorch's default init with explicit init, shows why constant weights keep units identical, and explains BatchNorm in train versus eval with running statistics. LayerNorm, RMSNorm and GroupNorm are compared by axis, and real desktop runs train the same deep MLP with each choice.

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

  • গভীরতার সঙ্গে activation-এর variance কেন মিলিয়ে যায় বা ফেটে যায়, স্তর ধরে মেপে দেখা
  • Variance ধরে রাখার শর্ত থেকে Xavier আর He init নিজে বের করা, আর PyTorch-এর default init-এর সঙ্গে তুলনা
  • শূন্য বা constant init কেন hidden unit-গুলোকে যমজ বানায়
  • BatchNorm হাতে হিসাব, running statistics, momentum, train বনাম eval আর ছোট batch-এর ফাঁদ
  • LayerNorm, RMSNorm আর GroupNorm কোন axis ধরে কাজ করে, আর একই গভীর MLP-এ সবগুলোর মাপা তুলনা

পর্ব ০৩-এর NumPy MLP-এ আমরা weight-এর initialization করেছিলাম √(2/n_in) standard deviation দিয়ে, আর পর্ব ০৯-এ দেখেছি nn.Linear নিজে U(−1/√in, 1/√in) থেকে weight নেয়। কেন এই সংখ্যা, অন্য কোনোটি নয়? AI-এর ভিত্তি কোর্সের পর্ব ৬৮-এ এর সংক্ষিপ্ত উত্তর ছিল: স্তর পেরিয়ে signal যেন না হারায়। এই পর্বে সেই কথাটি মেপে, হিসাব করে, আর একটি সত্যিকারের ২০ স্তরের network শিখিয়ে দেখব। দ্বিতীয় অর্ধেকে আসবে normalization: শুরু ভালো হলেও training-এর মাঝে মাপ সরে যায়, তাই প্রতিটি স্তরে মাপ ঠিক রাখার একটি যন্ত্র। পাতার চারটি ব্লক NumPy-তে (প্রথম চালানোয় package আসতে কয়েক সেকেন্ড লাগে); PyTorch-এর অংশগুলো ডেস্কটপের সত্যিকারের run.

১ · Signal-এর পথ — গভীরতায় activation কেন হারায় বা ফাটে

গ্রামের হাটে মাইকে ঘোষণা হচ্ছে, আর পরপর ২০টি মাইক একটি আরেকটির শব্দ ধরে আবার ছড়ায়। প্রতিটি মাইকের volume যদি একটু কম রাখা থাকে, শেষ মাইকে আর কিছুই শোনা যায় না। একটু বেশি হলে শেষে শুধু কানফাটা চিৎকার, কথা বোঝা যায় না। কথা ঠিকঠাক পৌঁছাতে প্রতিটি মাইকের volume এমন হতে হবে যাতে যা ঢোকে, প্রায় ততটুকুই বের হয়। গভীর network-এর প্রতিটি স্তর একটি মাইক, আর weight-এর মাপ তার volume.

একটি স্তর হিসাব করে z = Wx, তারপর activation h = f(z). ধরুন x-এর প্রতিটি মান স্বাধীন, গড় ০, variance Var(x); W-এর প্রতিটি মানও স্বাধীন, গড় ০, variance Var(W). z-এর একটি উপাদান হলো n টি গুণফলের যোগ, যেখানে n = fan_in, অর্থাৎ একটি neuron-এ কতগুলো input ঢোকে। স্বাধীন রাশির যোগে variance যোগ হয় (AI-এর ভিত্তি কোর্সের পর্ব ৩৭), তাই

Var(z) = n · Var(W) · Var(x)

অর্থাৎ প্রতিটি স্তর variance-কে g = n · Var(W) দিয়ে গুণ করে (activation-এর প্রভাব একটু পরে)। L টি স্তর পরে গুণক gL. g = ০.৫ হলে ২০ স্তরে ০.৫২০ ≈ ১০−৬, আর g = ২ হলে প্রায় ১০৬. এই ঘটনাকে বলে signal propagation-এর সমস্যা। Forward-এ activation হারালে backward-এ gradient-ও একই শিকল ধরে ফেরে, তাই gradient-ও হারায় (vanishing gradient) বা ফাটে; সেই দিকটি পর্ব ১৭ (Vanishing আর exploding gradient — residual connection আর gradient clipping)-এ মাপা হবে। এখানে শুধু forward.

নিচের ব্লক ২৫৬ চওড়া ২০টি স্তরে ৫১২টি এলোমেলো input পাঠায়, bias ছাড়া, আর প্রতিটি স্তরের পরে activation-এর standard deviation মাপে। চার রকম weight-এর std: স্থির ০.০১, Xavier √(1/n), He √(2/n), আর বড় 3/√n. দুটি activation: tanh আর ReLU.

import numpy as np
import matplotlib.pyplot as plt

n, depth = 256, 20                        # প্রতিটি স্তরে ২৫৬টি neuron
x0 = np.random.default_rng(42).standard_normal((512, n))   # ৫১২টি input
inits = {"small 0.01": lambda n: 0.01,
         "Xavier sqrt(1/n)": lambda n: np.sqrt(1 / n),
         "He sqrt(2/n)": lambda n: np.sqrt(2 / n),
         "large 3/sqrt(n)": lambda n: 3 / np.sqrt(n)}
acts = {"tanh": np.tanh, "relu": lambda z: np.maximum(z, 0)}

fig, axes = plt.subplots(1, 2, figsize=(9, 3.6))
for ax, (aname, f) in zip(axes, acts.items()):
    print(aname, "— activation-এর std: স্তর 1 | 5 | 10 | 20")
    for iname, s in inits.items():
        rng = np.random.default_rng(0)        # সব init-এ একই এলোমেলো ক্রম
        h, stds = x0, []
        for _ in range(depth):
            W = rng.normal(0, s(n), (n, n))
            h = f(h @ W)                      # bias 0, শুধু weight-এর প্রভাব
            stds.append(h.std())
        ax.semilogy(range(1, depth + 1), stds, marker="o", ms=3, label=iname)
        s1, s5, s10, s20 = (stds[i] for i in (0, 4, 9, 19))
        print(f"  {iname:17s} {s1:9.3g} {s5:9.3g} {s10:9.3g} {s20:9.3g}")
    ax.set(title=f"{aname}: activation std by depth", xlabel="layer",
           ylabel="std (log scale)")
axes[1].legend(fontsize=8)

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

tanh — activation-এর std: স্তর 1 | 5 | 10 | 20
  small 0.01            0.157  0.000102  1.09e-08  1.19e-16
  Xavier sqrt(1/n)      0.629     0.321     0.231     0.159
  He sqrt(2/n)          0.722     0.567     0.558     0.553
  large 3/sqrt(n)       0.863     0.837     0.838     0.837
relu — activation-এর std: স্তর 1 | 5 | 10 | 20
  small 0.01           0.0943  1.56e-05   3.9e-10  1.13e-19
  Xavier sqrt(1/n)      0.589     0.148    0.0355  0.000936
  He sqrt(2/n)          0.834      0.84      1.14     0.959
  large 3/sqrt(n)        1.77      36.1   2.1e+03  3.26e+06

ReLU-এর সারি চারটি ঠিক তত্ত্বের মতো চলে। ০.০১-এ g = ২৫৬ × ০.০০০১ × ½ ≈ ০.০১৩, তাই প্রতি স্তরে std প্রায় ৯ ভাগের এক ভাগ, আর ২০ স্তরে 10−19: শেষ স্তরের কাছে input-এর কোনো খবরই পৌঁছায় না। Xavier √(1/n)-এ g = ½ (ReLU অর্ধেক শক্তি ফেলে দেয়), তাই ধীরে ধীরে কমে স্তর ২০-এ ০.০০০৯৩৬. He √(2/n)-এ g = ১, std ০.৮৩ থেকে ১.১৪-এর মধ্যে ঘোরে, ২০ স্তরেও ০.৯৬. বড় 3/√n-এ g = ৪.৫, ফলে 3.26 × 106. tanh-এর চিত্র আলাদা: tanh-এর মান ±১-এর বাইরে যায় না, তাই বড় init-এ বিস্ফোরণ নেই, কিন্তু প্রায় সব neuron ±১-এ আটকে থাকে (saturation), যেখানে tanh-এর ঢাল প্রায় শূন্য। tanh-এ Xavier ধীরে কমে (০.৬৩ → ০.১৬), কারণ tanh শূন্যের কাছে প্রায় সরলরেখা হলেও একটু চেপে দেয়। লেখচিত্রে y-axis log মাপে, তাই সোজা নিম্নমুখী রেখা মানে প্রতি স্তরে একই অনুপাতে ক্ষয়।

২ · Xavier আর He — variance ধরে রাখার শর্ত থেকে সূত্র

লক্ষ্য সহজ: g = ১, যাতে প্রতিটি স্তরে variance যেমন ঢোকে তেমন বের হয়। Activation না থাকলে (বা শূন্যের কাছে প্রায় সরলরেখা tanh হলে) শর্ত n · Var(W) = ১, অর্থাৎ Var(W) = 1/fan_in; এটি LeCun-এর ১৯৯৮ সালের “Efficient BackProp”-এর পরামর্শের সঙ্গে মেলে। Glorot আর Bengio (AISTATS ২০১০) দেখলেন backward-এ gradient-এর জন্য একই শর্ত দাঁড়ায় fan_out দিয়ে (gradient ফেরে স্তরের output-এর দিক থেকে)। দুটি একসঙ্গে মানা যায় না যদি fan_in ≠ fan_out, তাই তাঁরা গড় নিলেন:

Xavier/Glorot: Var(W) = 2 / (fan_in + fan_out)

ReLU-এর ক্ষেত্রে একটি জিনিস বদলায়, যা He, Zhang, Ren আর Sun-এর ২০১৫ সালের প্রবন্ধ (“Delving Deep into Rectifiers”) হিসাব করে দেখায়। নিচের stepper সেই হিসাব ধাপে ধাপে করে।

একটি neuron: z = Σ wᵢ xᵢfan_in n = 256

            
  1. শুরু. একটি neuron-এর z হলো n টি গুণফলের যোগ। Weight-এর গড় ০, কিন্তু ReLU-এর পরের x কখনো ঋণাত্মক নয়, তাই x-এর গড় ০ নয়।
  2. একটি গুণফলের variance. স্বাধীন w আর x-এ Var(wx) = E[w²]E[x²] − (E[w]E[x])². E[w] = ০, তাই শুধু Var(W) · E[x²] থাকে।
  3. যোগ. n টি স্বাধীন পদের variance যোগ হয়। এখানে আসে E[x²], অর্থাৎ x-এর বর্গের গড়, x-এর variance নয়; এটিই ReLU-এর বিশেষত্ব।
  4. ReLU অর্ধেক কেটে দেয়. আগের স্তরের z শূন্যের দুই পাশে প্রতিসম হলে অর্ধেক সময় x = ০, বাকি সময় x = z. তাই E[x²] = ½ Var(zআগের)।
  5. শর্ত. g = n · Var(W) / ২ = ১ চাইলে Var(W) = ২/n. Xavier-এর তুলনায় (fan_in = fan_out হলে ১/n) ঠিক দ্বিগুণ, যা ReLU-এর ফেলে দেওয়া অর্ধেক পূরণ করে।
  6. সংখ্যায়. n = ২৫৬-এ std ০.০৮৮৪; uniform রূপে U(−a, a)-এর variance a²/৩, তাই a = √(৬/n) = ০.১৫৩১. ডেস্কটপে kaiming_normal_-এর মাপা std ০.০৮৮৮, প্রায় একই।

এভাবে পাওয়া He/Kaiming init ReLU-পরিবারের network-এর প্রচলিত শুরু। দুটি নিয়মকে এক ছাঁচে লেখা যায়: Var(W) = gain² / fan, যেখানে gain activation-এর ওপর নির্ভর করে (linear-এ ১, ReLU-তে √২, tanh-এ PyTorch-এর মান ৫/৩) আর fan হয় fan_in (forward রক্ষা) বা fan_in ও fan_out-এর গড় (Xavier). Normal আর uniform দুই রূপই আছে; variance মিললে আচরণ প্রায় একই।

নিয়মVar(W)কোথায় মানায়PyTorch
LeCun (১৯৯৮)1/fan_inlinear, SELU, শূন্যের কাছে tanhkaiming_normal_(w, nonlinearity="linear")
Xavier/Glorot (২০১০)2/(fan_in + fan_out)tanh, sigmoid, linearxavier_uniform_, xavier_normal_
He/Kaiming (২০১৫)2/fan_inReLU, Leaky ReLU (a দিয়ে সংশোধিত)kaiming_normal_(w, nonlinearity="relu")
nn.Linear default1/(3·fan_in)সাধারণ শুরু, গভীর ReLU-তে দুর্বলনিজে থেকেই

৩ · PyTorch-এর default init বনাম স্পষ্ট init

পর্ব ০৯-এ দেখেছিলাম nn.Linear weight আর bias দুটিই U(−1/√in, 1/√in) থেকে নেয়। এই সমবণ্টনের variance (1/√in)²/৩ = 1/(3·in). ReLU-এর হিসাবে g = n · 1/(3n) · ½ = 1/6, অর্থাৎ প্রতি স্তরে std প্রায় √(1/6) ≈ ০.৪১ গুণ হয়। তত্ত্ব বলে দশ স্তরেই প্রায় ১০−৪. ডেস্কটপে মেপে দেখা যাক, সঙ্গে torch.nn.init-এর ফাংশনগুলোর std আর calculate_gain.

import math, torch
from torch import nn
torch.manual_seed(0)
n = 256
lin = nn.Linear(n, n)                              # PyTorch-এর default init
print("default weight std", round(lin.weight.std().item(), 4),
      "| তত্ত্ব 1/sqrt(3n) =", round(1 / math.sqrt(3 * n), 4))
w = torch.empty(n, n)
nn.init.kaiming_normal_(w, nonlinearity="relu")
print("kaiming_normal_ std", round(w.std().item(), 4),
      "| sqrt(2/n) =", round(math.sqrt(2 / n), 4))
nn.init.xavier_uniform_(w)
print("xavier_uniform_ std", round(w.std().item(), 4),
      "| sqrt(2/(n+n)) =", round(math.sqrt(2 / (2 * n)), 4))
for nl in ["linear", "tanh", "relu"]:
    print(f"calculate_gain('{nl}') =", round(nn.init.calculate_gain(nl), 4))

x0 = torch.randn(512, n)
for name in ["default", "he"]:
    torch.manual_seed(1); h, out = x0, []
    for i in range(20):
        lin = nn.Linear(n, n)
        if name == "he":
            nn.init.kaiming_normal_(lin.weight, nonlinearity="relu")
            nn.init.zeros_(lin.bias)
        with torch.no_grad():
            h = torch.relu(lin(h))
        if i + 1 in (1, 5, 10, 20):
            out.append(f"{i + 1}: {h.std().item():.3g}")
    print(f"{name:7s} ReLU activation std ->", " | ".join(out))

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

~/bazarghar-dl$ uv run python init_check.py
default weight std 0.036 | তত্ত্ব 1/sqrt(3n) = 0.0361
kaiming_normal_ std 0.0888 | sqrt(2/n) = 0.0884
xavier_uniform_ std 0.0626 | sqrt(2/(n+n)) = 0.0625
calculate_gain('linear') = 1
calculate_gain('tanh') = 1.6667
calculate_gain('relu') = 1.4142
default ReLU activation std -> 1: 0.339 | 5: 0.0226 | 10: 0.0228 | 20: 0.0233
he      ReLU activation std -> 1: 0.83 | 5: 0.789 | 10: 0.755 | 20: 0.671

প্রথম তিন লাইনে মাপা std সূত্রের সঙ্গে তৃতীয় দশমিক পর্যন্ত মেলে। শেষ দুই লাইন একটি সূক্ষ্ম কথা বলে। Default-এ std স্তর ১-এ ০.৩৪, স্তর ৫-এ ০.০২৩, তারপর আর কমে না, ০.০২৩-এর কাছে থেমে যায়। কারণ bias: প্রতিটি স্তর নিজের bias যোগ করে, যার std-ও প্রায় 1/√(3 · 256) ≈ ০.০৩৬; weight-এর পথে আসা signal মরে গেলেও bias-এর একটি স্থির “গুঞ্জন” থাকে। অর্থাৎ শেষ স্তরের activation-এ যা দেখা যায়, তার প্রায় সবটাই bias, input-এর তথ্য নয়। He init-এ (bias শূন্য) ২০ স্তরেও ০.৬৭. অংশ ৮-এ দেখব এই পার্থক্য training-এ কী করে।

সতর্কতা: PyTorch-এর default খারাপ নয়; অগভীর network, normalization-ওয়ালা network বা Adam-এর মতো optimizer-এ সাধারণত কাজ চলে যায়। কিন্তু ১০–২০ স্তরের সাধারণ ReLU MLP-এ normalization ছাড়া চালালে স্পষ্ট init দিন। Init বসাতে হয় model বানানোর পরে, training শুরুর আগে, torch.no_grad()-এর মতো আচরণে, আর ফাংশনের নামের শেষে _ মানে tensor জায়গায় বদলায় (in-place). একসঙ্গে সব Linear-এ বসাতে model.apply(fn), যেখানে fn প্রতিটি submodule পায়।

৪ · সব weight সমান হলে — symmetry-র সমস্যা

“ছোট রাখাই যদি লক্ষ্য হয়, সব weight শূন্য দিলেই তো হয়?” না। একই স্তরের দুটি hidden unit-এর weight হুবহু এক হলে তারা একই input থেকে একই output দেয়, backward-এ একই gradient পায়, আর একই পরিমাণ বদলায়। পরের ধাপেও তারা এক। এভাবে চিরকাল: ১০০টি unit কার্যত একটি unit. এলোমেলো শুরু এই প্রতিসাম্য ভাঙে, যার নাম symmetry breaking. সব শূন্য হলে আরও খারাপ: tanh(0) = ০, তাই hidden output শূন্য, ফলে output-এর weight-এর gradient শূন্য, আর hidden-এর gradient-ও শূন্য (কারণ তা output-এর শূন্য weight দিয়ে গুণ হয়)। শুধু শেষ bias নড়ে।

নিচের ব্লক বাজারঘরের ২০০টি কাল্পনিক অর্ডারে (তিনটি standardized feature) একটি ৩ → ৪ (tanh) → ১ (sigmoid) network হাতে লেখা backprop-এ ২০০০ ধাপ শেখায় (পর্ব ০৪-এর ছাঁচ)। Label একটি XOR-ধাঁচের অ-রৈখিক নিয়ম, যা একটি unit দিয়ে শেখা যায় না। তিন রকম শুরু: সব শূন্য, সব ০.৫, আর এলোমেলো।

import numpy as np
np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(42)
X = rng.normal(size=(200, 3))     # বাজারঘর: দূরত্ব, পণ্য, বৃষ্টি (standardized)
y = ((X[:, 0] * X[:, 1] > 0) ^ (X[:, 2] > 1))[:, None] * 1.0

def train(W1, W2, steps=2000, lr=0.5):            # 3 → 4 (tanh) → 1 (sigmoid)
    b1, b2 = np.zeros(4), np.zeros(1)
    for _ in range(steps):
        H = np.tanh(X @ W1 + b1)
        p = 1 / (1 + np.exp(-(H @ W2 + b2)))
        d2 = (p - y) / len(X)                    # BCE-এর gradient, logit-এ
        dH = (d2 @ W2.T) * (1 - H ** 2)
        W2 -= lr * H.T @ d2; b2 -= lr * d2.sum(0)
        W1 -= lr * X.T @ dH; b1 -= lr * dH.sum(0)
    return W1, H, ((p > 0.5) == y).mean()

runs = {"zero 0": (np.zeros((3, 4)), np.zeros((4, 1))),
        "constant 0.5": (np.full((3, 4), 0.5), np.full((4, 1), 0.5)),
        "random N(0, 0.5²)": (rng.normal(0, 0.5, (3, 4)),
                              rng.normal(0, 0.5, (4, 1)))}
for name, (W1, W2) in runs.items():
    W1, H, acc = train(W1, W2)
    print(f"{name} | accuracy {acc:.3f}")
    print("  W1-এর ৪টি কলাম (প্রতিটি একটি hidden unit):\n", W1)
    print("  ৪টি unit-এর output-এ সবচেয়ে বড় পার্থক্য:",
          round(float(np.ptp(H, axis=1).max()), 6))

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

zero 0 | accuracy 0.530
  W1-এর ৪টি কলাম (প্রতিটি একটি hidden unit):
 [[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
  ৪টি unit-এর output-এ সবচেয়ে বড় পার্থক্য: 0.0
constant 0.5 | accuracy 0.590
  W1-এর ৪টি কলাম (প্রতিটি একটি hidden unit):
 [[1.377 1.377 1.377 1.377]
 [1.816 1.816 1.816 1.816]
 [0.231 0.231 0.231 0.231]]
  ৪টি unit-এর output-এ সবচেয়ে বড় পার্থক্য: 0.0
random N(0, 0.5²) | accuracy 0.965
  W1-এর ৪টি কলাম (প্রতিটি একটি hidden unit):
 [[-1.483 -1.439  1.819 -1.583]
 [-1.6    1.448  1.631  1.36 ]
 [-1.144 -0.971 -1.074  0.942]]
  ৪টি unit-এর output-এ সবচেয়ে বড় পার্থক্য: 1.999998

শূন্য শুরুতে W1 ২০০০ ধাপ পরেও পুরোপুরি শূন্য, accuracy ০.৫৩: প্রায় সব সময় একই উত্তর। ০.৫ শুরুতে W1 বদলেছে (১.৩৭৭, ১.৮১৬, ০.২৩১), কিন্তু চারটি কলাম হুবহু এক, আর চারটি unit-এর output-এর পার্থক্য ০.০: চারটি unit আসলে একটি, তাই অ-রৈখিক নিয়মে accuracy মাত্র ০.৫৯০. এলোমেলো শুরুতে চারটি কলাম আলাদা দিকে গেছে, accuracy ০.৯৬৫. শিক্ষা: init-এর মাপ (অংশ ১–২) আর এলোমেলোতা (এই অংশ) দুটিই দরকার। Bias শূন্য দিয়ে শুরু করা কিন্তু নিরাপদ, কারণ weight-এর এলোমেলোতাই প্রতিসাম্য ভাঙে।

৫ · BatchNorm — প্রতিটি feature-কে batch ধরে normalize করা

পরীক্ষার খাতা দেখার সময় একজন পরীক্ষক কড়া, আরেকজন উদার। বোর্ড যদি প্রতিটি পরীক্ষকের দেওয়া নম্বরকে তাঁর নিজের খাতাগুলোর গড় আর ছড়ানো দিয়ে মানিয়ে নেয়, তবে সব পরীক্ষকের নম্বর একই মাপে আসে। তারপর বোর্ড চাইলে নিজের পছন্দের গড় আর ছড়ানো বসাতে পারে। BatchNorm প্রতিটি feature-এর জন্য ঠিক এটি করে: বর্তমান batch-কে “একজন পরীক্ষকের খাতার স্তূপ” ধরে।

Init শুধু শুরুটা ঠিক করে; training-এ weight বদলালে প্রতিটি স্তরের input-এর মাপও সরে যায়। Ioffe আর Szegedy (ICML ২০১৫) প্রস্তাব করলেন Batch Normalization: একটি স্তরের প্রতিটি feature (কলাম) j-এর জন্য, B আকারের batch-এ

μj = গড়(x·j), σ²j = গড়((x·j − μj)²), x̂ = (x − μj) / √(σ²j + ε), y = γj x̂ + βj

এখানে ε (PyTorch-এ 1e-5) শূন্য দিয়ে ভাগ ঠেকায়; γ (scale, শুরুতে ১) আর β (shift, শুরুতে ০) শেখা parameter, যাতে network দরকার হলে normalize-এর আগের মাপে ফিরে যেতে পারে। Training-এর variance-এ ভাগ হয় B দিয়ে (biased). এর সঙ্গে চলে দুটি running statistics: running_mean আর running_var, যা প্রতি batch-এ হালনাগাদ হয়, momentum m (PyTorch-এ ০.১) দিয়ে:

running = (1 − m) · running + m · batch-এর মান

PyTorch running_var-এ batch-এর unbiased variance (B − ১ দিয়ে ভাগ) জমায়। Eval-এ batch-এর পরিসংখ্যান নয়, এই জমানো মান দিয়ে normalize হয়, যাতে একটি নমুনার উত্তর batch-এর অন্যদের ওপর নির্ভর না করে। নিচের stepper একটি feature-এর চারটি সংখ্যায় পুরো হিসাব করে।

একটি feature, batch-এ ৪টি অর্ডার: x = [2, 4, 6, 8]
ধাপ
হিসাব
ফল
  1. গড়. চারটি মানের গড় μ = ২০/৪ = ৫. এটি শুধু এই batch-এর গড়; পরের batch-এ অন্য হবে।
  2. বিচ্যুতি. প্রতিটি থেকে ৫ বিয়োগ: [−৩, −১, ১, ৩], যোগফল ০.
  3. Variance. বর্গের গড়, N = ৪ দিয়ে ভাগ: (৯ + ১ + ১ + ৯)/৪ = ৫. Training-এর normalize-এ এই biased মান।
  4. Normalize. √(৫ + ε) ≈ ২.২৩৬০৭ দিয়ে ভাগ: x̂-এর গড় ০, std ১. লক্ষ করুন x̂ শুধু ক্রম আর আপেক্ষিক দূরত্ব রাখে; মূল মাপ মুছে গেছে।
  5. γ আর β. শেখা scale ২ আর shift ০.৫ বসিয়ে y: এখন গড় ০.৫, std ২. γ = ১, β = ০ (শুরুর মান) হলে y = x̂.
  6. Running stats. শুরু running_mean ০, running_var ১. গড়ে ১০% নতুন মান: ০.৯ × ০ + ০.১ × ৫ = ০.৫. Variance-এ unbiased ২০/৩ ≈ ৬.৬৬৭ লাগে: ০.৯ + ০.৬৬৭ = ১.৫৬৬৭. ডেস্কটপের nn.BatchNorm1d হুবহু এই দুটি সংখ্যা দেয় (অংশ ৬)।

এবার একই হিসাব NumPy-তে, দুটি feature (পণ্যের সংখ্যা আর শত টাকায় দাম) নিয়ে, তারপর একই উৎসের ৬০টি batch দিয়ে running statistics কীভাবে আসল মানের দিকে এগোয় তা দেখা। আসল উৎসের গড় [৫, ২৫] আর variance [৪, ১০০]।

import numpy as np
np.set_printoptions(precision=4, suppress=True)
# batch-এ ৪টি অর্ডার, feature ২টি: পণ্যের সংখ্যা আর দাম (শত টাকায়)
X = np.array([[2., 10.], [4., 30.], [6., 20.], [8., 40.]])
gamma, beta, eps = np.array([2., 1.]), np.array([0.5, 0.]), 1e-5

mu, var = X.mean(axis=0), X.var(axis=0)    # প্রতিটি feature-এর কলাম ধরে, ÷N
x_hat = (X - mu) / np.sqrt(var + eps)
Y = gamma * x_hat + beta
print("mean", mu, "| var (÷N)", var)
print("x̂ =\n", x_hat)
print("y = γ·x̂ + β =\n", Y)
print("y-এর কলাম-গড়", Y.mean(0), "| কলাম-std", Y.std(0))

# running statistics: momentum 0.1, শুরু mean 0, var 1 (PyTorch-এর মতো)
rm, rv, m = np.zeros(2), np.ones(2), 0.1
rm = (1 - m) * rm + m * mu
rv = (1 - m) * rv + m * X.var(axis=0, ddof=1)     # running-এ ÷(N−1)
print("১টি step পরে running_mean", rm, "| running_var", rv)

rng = np.random.default_rng(0)
for step in range(2, 61):                        # একই উৎসের নতুন batch
    B = rng.normal([5, 25], [2, 10], size=(32, 2))
    rm = (1 - m) * rm + m * B.mean(0)
    rv = (1 - m) * rv + m * B.var(0, ddof=1)
    if step in (10, 30, 60):
        print(f"step {step}: running_mean {rm} | running_var {rv}")
one = np.array([[5., 25.]])                      # eval: মাত্র একটি অর্ডার
print("eval-এ একটি অর্ডার:", gamma * (one - rm) / np.sqrt(rv + eps) + beta)

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

mean [ 5. 25.] | var (÷N) [  5. 125.]
x̂ =
 [[-1.3416 -1.3416]
 [-0.4472  0.4472]
 [ 0.4472 -0.4472]
 [ 1.3416  1.3416]]
y = γ·x̂ + β =
 [[-2.1833 -1.3416]
 [-0.3944  0.4472]
 [ 1.3944 -0.4472]
 [ 3.1833  1.3416]]
y-এর কলাম-গড় [0.5 0. ] | কলাম-std [2. 1.]
১টি step পরে running_mean [0.5 2.5] | running_var [ 1.5667 17.5667]
step 10: running_mean [ 3.1817 16.6135] | running_var [ 3.1273 67.505 ]
step 30: running_mean [ 4.9093 23.2224] | running_var [ 4.2297 90.1461]
step 60: running_mean [ 4.9155 25.0446] | running_var [ 3.6444 98.0532]
eval-এ একটি অর্ডার: [[ 0.5886 -0.0045]]

প্রথম কলাম stepper-এর সঙ্গে হুবহু মেলে। দ্বিতীয় কলামের গড় ২৫, variance ১২৫; normalize-এর পর দুই কলামই একই মাপে, যদিও দামের কলাম প্রায় পাঁচ গুণ বেশি ছড়ানো ছিল। y-এর কলাম-গড় ঠিক β আর std ঠিক γ. Running statistics ধীরে এগোয়: momentum ০.১-এ প্রতিটি নতুন batch মাত্র ১০% ওজন পায়, তাই শুরুর (০, ১) থেকে আসল (৫, ২৫) আর (৪, ১০০)-এর কাছে আসতে কয়েক ডজন batch লাগে; step ৬০-এ গড় ৪.৯২ আর ২৫.০৪, variance ৩.৬৪ আর ৯৮.০৫, এখনো একটু দোলে, কারণ প্রতিটি batch-এর মান নিজেই এলোমেলো। শেষ লাইনে eval: একটি মাত্র অর্ডার, কোনো batch নেই, তবু জমানো মান দিয়ে normalize করা যায়।

BatchNorm কেন কাজ করে? মূল প্রবন্ধ বলেছিল এটি internal covariate shift কমায়: training-এ আগের স্তর বদলালে পরের স্তরের input-এর বণ্টন সরে যায়, BatchNorm তা স্থির রাখে। পরে Santurkar ও সহকর্মীদের (NeurIPS ২০১৮) পরীক্ষায় দেখা যায় ইচ্ছে করে বণ্টন সরিয়ে দিলেও BatchNorm-এর সুবিধা থাকে; তাঁদের ব্যাখ্যা, এটি loss-এর তলকে মসৃণ করে, ফলে বড় learning rate চলে। কোন ব্যাখ্যা পুরো সত্য, তা নিয়ে আলোচনা এখনো চলছে; ব্যবহারিক ফল নিয়ে বিতর্ক কম: দ্রুত, বড় learning rate-এ, init-এর প্রতি কম সংবেদনশীল training. Linear-এর ঠিক পরে BatchNorm বসালে Linear-এর bias অর্থহীন (গড় বিয়োগে মুছে যায়), তাই অনেকে bias=False দেন।

৬ · Train বনাম eval — running statistics আর ছোট batch-এর ফাঁদ

BatchNorm-ই PyTorch-এর সেই layer, যার আচরণ train() আর eval() (eval mode)-এ সবচেয়ে বেশি বদলায় (পর্ব ০৯-এ Dropout দেখেছিলাম, পর্ব ১০-এ লুপের নিয়ম)। ডেস্কটপে stepper-এর চারটি অর্ডারই nn.BatchNorm1d-কে দেওয়া হলো, γ = [২, ১], β = [০.৫, ০] বসিয়ে।

import torch
from torch import nn
torch.set_printoptions(precision=4, sci_mode=False)

X = torch.tensor([[2., 10.], [4., 30.], [6., 20.], [8., 40.]])
bn = nn.BatchNorm1d(2)                       # momentum 0.1, eps 1e-5
with torch.no_grad():
    bn.weight.copy_(torch.tensor([2., 1.]))  # γ
    bn.bias.copy_(torch.tensor([0.5, 0.]))   # β
print(bn)
print("buffers:", [n for n, _ in bn.named_buffers()])
print("শুরুতে running_mean", bn.running_mean, "running_var", bn.running_var)

bn.train()
y_train = bn(X)
print("train-mode output:\n", y_train.detach())
print("১ step পরে running_mean", bn.running_mean,
      "running_var", bn.running_var, "| num_batches_tracked",
      bn.num_batches_tracked.item())

bn.eval()
with torch.no_grad():
    print("eval-mode output (একই X):\n", bn(X))

bn.train()
try:
    bn(X[:1])                                # batch-এ মাত্র একটি নমুনা
except ValueError as e:
    print("ValueError:", e)

a = torch.tensor([[5., 25.]])
with torch.no_grad():
    o1 = bn(torch.cat([a, X[:1]]))[0]
    o2 = bn(torch.cat([a, X[3:]]))[0]
print("একই অর্ডার, দুটি আলাদা batch-এ (train):", o1, o2)
ln = nn.LayerNorm(2)
print("LayerNorm একটি নমুনায়:", ln(a).detach())

সত্যিকারের run (ডেস্কটপ, CPU, PyTorch 2.15.0):

~/bazarghar-dl$ uv run python bn_modes.py
BatchNorm1d(2, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
buffers: ['running_mean', 'running_var', 'num_batches_tracked']
শুরুতে running_mean tensor([0., 0.]) running_var tensor([1., 1.])
train-mode output:
 tensor([[-2.1833, -1.3416],
        [-0.3944,  0.4472],
        [ 1.3944, -0.4472],
        [ 3.1833,  1.3416]])
১ step পরে running_mean tensor([0.5000, 2.5000]) running_var tensor([ 1.5667, 17.5667]) | num_batches_tracked 1
eval-mode output (একই X):
 tensor([[ 2.8968,  1.7894],
        [ 6.0925,  6.5613],
        [ 9.2883,  4.1754],
        [12.4840,  8.9472]])
ValueError: Expected more than 1 value per channel when training, got input size torch.Size([1, 2])
একই অর্ডার, দুটি আলাদা batch-এ (train): tensor([2.5000, 1.0000]) tensor([-1.5000, -1.0000])
LayerNorm একটি নমুনায়: tensor([[-1.,  1.]])

পাঁচটি জিনিস মিলিয়ে দেখুন। এক: BatchNorm-এর তিনটি buffer: running_mean, running_var আর num_batches_tracked; এগুলো state_dict-এ যায়, শেখানো হয় না। দুই: train-mode output হুবহু পাতার NumPy-র y, আর এক step পরে running_mean [০.৫, ২.৫], running_var [১.৫৬৬৭, ১৭.৫৬৬৭], ঠিক stepper-এর হিসাব; দ্বিতীয় feature-এ ০.৯ + ০.১ × (৫০০/৩) = ১৭.৫৬৬৭, অর্থাৎ unbiased variance. তিন: একই X-এ eval-mode output পুরো আলাদা (২.৯ থেকে ১২.৫), কারণ মাত্র এক step-এর running stats এখনো শুরুর (০, ১)-এর কাছে। চার: training-এ একটি নমুনার batch-এ variance হিসাব অর্থহীন, তাই PyTorch থামিয়ে দেয়। পাঁচ: train mode-এ একই অর্ডার [৫, ২৫] এক batch-এ পায় [২.৫, ১.০], আরেক batch-এ [−১.৫, −১.০]: output নির্ভর করে পাশে কে আছে তার ওপর। LayerNorm একটি নমুনাতেই চলে।

এখন বড় ফাঁদটি একটি আসল model-এ। ২০ স্তরের MLP (প্রতিটি Linear-এর পরে BatchNorm) Fashion-MNIST-এ দুই epoch শেখানো হলো, তারপর ১০,০০০টি test ছবিতে accuracy মাপা হলো পাঁচভাবে: ঠিক নিয়মে eval mode-এ, আর model.eval() ভুলে train mode-এ বিভিন্ন batch-এ। প্রতিবার একই state_dict থেকে শুরু, কারণ train mode-এ forward running stats বদলে দেয়।

bn1 = model[1]
print("প্রথম BatchNorm: running_mean[:4]", bn1.running_mean[:4].numpy().round(3))
print("                 running_var[:4] ", bn1.running_var[:4].numpy().round(3))
print("num_batches_tracked", bn1.num_batches_tracked.item(), "(= 2 epoch × 469 batch)")

state = {k: v.clone() for k, v in model.state_dict().items()}
def acc(mode, bs, order=None):
    model.load_state_dict(state)               # প্রতিবার একই শুরু
    model.train(mode == "train")               # False মানে eval()
    idx = order if order is not None else torch.arange(len(Xte))
    hit = 0
    with torch.no_grad():
        for b in idx.split(bs):
            hit += (model(Xte[b]).argmax(1) == yte[b]).sum().item()
    return hit / len(Xte)

by_class = torch.argsort(yte, stable=True)     # একই শ্রেণির ছবি পরপর
print(f"eval mode, batch 256          : {acc('eval', 256):.4f}")
print(f"eval mode, batch 1            : {acc('eval', 1):.4f}")
print(f"train mode ভুলে, batch 256     : {acc('train', 256):.4f}")
print(f"train mode ভুলে, batch 8       : {acc('train', 8):.4f}")
print(f"train mode ভুলে, শ্রেণি ধরে সাজানো: {acc('train', 256, by_class):.4f}")

সত্যিকারের run (ডেস্কটপ, CPU, PyTorch 2.15.0, torchvision 0.29.2; Fashion-MNIST, Zalando Research, MIT লাইসেন্স):

~/bazarghar-dl$ uv run python bn_trap.py
প্রথম BatchNorm: running_mean[:4] [-1.192 -4.481  0.081 -1.784]
                 running_var[:4]  [52.41  31.801  9.8   22.389]
num_batches_tracked 938 (= 2 epoch × 469 batch)
eval mode, batch 256          : 0.8330
eval mode, batch 1            : 0.8330
train mode ভুলে, batch 256     : 0.8363
train mode ভুলে, batch 8       : 0.5656
train mode ভুলে, শ্রেণি ধরে সাজানো: 0.1465

দুই epoch training-এর পরে প্রথম BatchNorm-এর running_var ৯.৮ থেকে ৫২: প্রথম Linear-এর output-এর মাপ শুরুর তুলনায় (default init-এ হিসাব করলে প্রায় ১/৩) অনেক বড় হয়েছে, কিন্তু BatchNorm তা সামলে নেওয়ায় পরের স্তর মাপ-ঠিক input পায়। ৯৩৮ = ২ × ৪৬৯ batch, প্রতিটি training step-এ একবার করে হালনাগাদ। Eval mode-এ batch ২৫৬ আর batch ১ হুবহু একই accuracy (০.৮৩৩০): output batch-নিরপেক্ষ। model.eval() ভুলে গেলে বড় এলোমেলো batch-এ ক্ষতি চোখে পড়ে না (০.৮৩৬৩), আর ঠিক এ কারণেই বাগটি বিপজ্জনক: পরীক্ষায় লুকিয়ে থাকে। Batch ৮-এ accuracy নামে ০.৫৬৫৬-এ। আর test ছবি শ্রেণি ধরে সাজানো থাকলে (প্রতিটি batch-এ প্রায় একই শ্রেণি) ০.১৪৬৫, প্রায় আন্দাজের সমান: batch-এর গড় নিজেই শ্রেণির তথ্য মুছে দেয়। বাস্তবে ডেটাবেস থেকে তারিখ বা বিভাগ ধরে সাজিয়ে আসা batch-এ ঠিক এটি ঘটতে পারে।

BatchNorm-এর নিয়ম

Training: batch-এর গড় আর biased variance দিয়ে normalize, running stats-এ unbiased variance জমে (momentum ০.১)। Eval: শুধু running stats; output batch-এর ওপর নির্ভর করে না। Batch ছোট (প্রায় ৮-এর নিচে) বা অসম হলে batch-এর পরিসংখ্যান অনির্ভরযোগ্য; তখন GroupNorm বা LayerNorm ভাবুন। Validation আর prediction-এ সবসময় model.eval().

৭ · LayerNorm, RMSNorm আর GroupNorm — কোন axis ধরে normalize

BatchNorm-এর দুর্বলতা তার batch-নির্ভরতা: ছোট batch, sequence-এর আলাদা দৈর্ঘ্য, আর একটি একটি token ধরে text তৈরিতে batch-এর পরিসংখ্যান অর্থহীন বা অনুপলব্ধ। Ba, Kiros আর Hinton (২০১৬) প্রস্তাব করলেন Layer Normalization: একই হিসাব, কিন্তু প্রতিটি নমুনার ভেতরে, তার সব feature জুড়ে। কোনো running stats নেই, train আর eval-এ একই আচরণ। Zhang আর Sennrich (NeurIPS ২০১৯) দেখালেন গড় বিয়োগটুকু বাদ দিলেও প্রায়ই চলে: RMSNorm শুধু root mean square দিয়ে ভাগ করে, y = γ · x / √(গড়(x²) + ε), যা একটু সস্তা। Wu আর He (ECCV ২০১৮)-এর GroupNorm ছবির channel-গুলোকে কয়েকটি দলে ভাগ করে প্রতিটি নমুনার প্রতিটি দলের ভেতরে normalize করে; ছোট batch-এর ছবির কাজে এটি BatchNorm-এর বিকল্প। ছবিটি তিনটির পার্থক্য দেখায়।

কোন ঘরগুলো একসঙ্গে normalize হয়

Rows = samples in the batch (N), columns = features or channels (C)

BatchNorm normalises one column across the batch, LayerNorm one row within a sample, GroupNorm a group of columns within a sample BatchNorm একটি feature, সব নমুনা জুড়ে batch-নির্ভর, running stats LayerNorm একটি নমুনা, সব feature জুড়ে batch-নিরপেক্ষ, Transformer-এ GroupNorm একটি নমুনা, একটি দল জুড়ে batch-নিরপেক্ষ, ছবি, ছোট batch সারি = নমুনা (N) · কলাম = feature (C)
ছবি ১৪.১ — চারটি পদ্ধতির (RMSNorm সহ) পার্থক্য শুধু কোন ঘরগুলোর গড় আর ছড়ানো একসঙ্গে মাপা হয়। RMSNorm-এর ঘর LayerNorm-এর মতোই একটি সারি; তফাত হিসাবে, গড় বিয়োগ নেই।
সারি = নমুনা, কলাম = featureγ = 1, β = 0

            
  1. ছক. তিনটি নমুনা (সারি), প্রতিটির তিনটি feature (কলাম)। একই সংখ্যায় তিনটি পদ্ধতি চালাব।
  2. BatchNorm-এর পরিসংখ্যান. প্রতিটি কলাম নিচের দিকে: প্রথম কলাম ১, ৩, ৫, গড় ৩, variance ৮/৩ ≈ ২.৬৬৭. একটি feature-এর সব নমুনা একসঙ্গে।
  3. BatchNorm-এর ফল. (১ − ৩)/১.৬৩৩ = −১.২২৫, ইত্যাদি। প্রতিটি কলাম এখন গড় ০, std ১; কিন্তু নমুনা ক-এর ফল খ আর গ-এর মানের ওপর নির্ভর করে।
  4. LayerNorm-এর পরিসংখ্যান. এবার প্রতিটি সারি আড়াআড়ি: নমুনা ক ১, ২, ৬, গড় ৩, variance ১৪/৩ ≈ ৪.৬৬৭. অন্য নমুনার কোনো ভূমিকা নেই।
  5. LayerNorm-এর ফল. (১ − ৩)/২.১৬০ = −০.৯২৬, ইত্যাদি। প্রতিটি সারি গড় ০, std ১. নমুনা গ বদলালেও নমুনা ক-এর ফল একই থাকে।
  6. RMSNorm. প্রতিটি সারিকে শুধু তার RMS দিয়ে ভাগ: নমুনা ক-এ √((১ + ৪ + ৩৬)/৩) = √১৩.৬৭ ≈ ৩.৬৯৭. গড় বিয়োগ নেই, তাই কোনো মানের চিহ্ন বদলায় না (এখানে সব ০ বা ধনাত্মক); শুধু মাপ ঠিক হয়।

এবার NumPy-তে, এমন তিনটি vector দিয়ে যা পার্থক্যগুলো স্পষ্ট করে: দ্বিতীয়টি প্রথমটির ১০ গুণ, তৃতীয়টি প্রথমটির সঙ্গে ১০০ যোগ। শেষে তৃতীয় সারি বদলে দেখা হবে প্রথম সারির ফল কোন পদ্ধতিতে বদলায়।

import numpy as np
np.set_printoptions(precision=3, suppress=True)
X = np.array([[1., 2., 3., 4.],          # একটি token বা অর্ডারের ৪টি feature
              [10., 20., 30., 40.],      # প্রথম সারি × 10
              [101., 102., 103., 104.]]) # প্রথম সারি + 100

def layer_norm(x, eps=1e-5):             # প্রতিটি সারির ভেতরে, শেষ axis
    mu = x.mean(-1, keepdims=True)
    return (x - mu) / np.sqrt(x.var(-1, keepdims=True) + eps)

def rms_norm(x, eps=1e-6):               # গড় বিয়োগ নেই, শুধু RMS দিয়ে ভাগ
    return x / np.sqrt((x ** 2).mean(-1, keepdims=True) + eps)

def batch_norm(x, eps=1e-5):             # প্রতিটি কলাম, batch-এর axis 0
    return (x - x.mean(0)) / np.sqrt(x.var(0) + eps)

print("LayerNorm:\n", layer_norm(X))
print("RMSNorm:\n", rms_norm(X))
print("BatchNorm (এই তিন সারি একটি batch):\n", batch_norm(X))

X2 = X.copy(); X2[2] = [0., 0., 0., 50.]          # শুধু তৃতীয় সারি বদলাল
print("প্রথম সারি, তৃতীয়টি বদলের পরে:")
print("  LayerNorm", layer_norm(X2)[0], "| BatchNorm", batch_norm(X2)[0])

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

LayerNorm:
 [[-1.342 -0.447  0.447  1.342]
 [-1.342 -0.447  0.447  1.342]
 [-1.342 -0.447  0.447  1.342]]
RMSNorm:
 [[0.365 0.73  1.095 1.461]
 [0.365 0.73  1.095 1.461]
 [0.985 0.995 1.005 1.015]]
BatchNorm (এই তিন সারি একটি batch):
 [[-0.804 -0.904 -1.002 -1.096]
 [-0.605 -0.49  -0.363 -0.226]
 [ 1.41   1.394  1.365  1.322]]
প্রথম সারি, তৃতীয়টি বদলের পরে:
  LayerNorm [-1.342 -0.447  0.447  1.342] | BatchNorm [-0.593 -0.593 -0.593 -1.384]

LayerNorm তিনটি সারিকেই একই [−১.৩৪২, −০.৪৪৭, ০.৪৪৭, ১.৩৪২] বানিয়েছে: সে মাপ (×১০) আর স্থানান্তর (+১০০) দুটিই মুছে দেয়। RMSNorm মাপ মোছে (প্রথম দুই সারি এক), কিন্তু স্থানান্তর নয়: তৃতীয় সারি [০.৯৮৫ … ১.০১৫], প্রায় সমান সংখ্যা, কারণ গড় বিয়োগ না করায় ১০০-এর বড় অংশটাই থেকে গেছে। BatchNorm-এ ফল নির্ভর করে কোন তিনটি সারি একসঙ্গে এসেছে তার ওপর: তৃতীয় সারি বদলাতেই প্রথম সারির ফল [−০.৮০৪ …] থেকে [−০.৫৯৩ …] হয়েছে, LayerNorm-এরটি একচুলও নড়েনি। Transformer-এর token vector-গুলো সাধারণত শূন্যের কাছাকাছি গড়ের হয়, তাই RMSNorm-এর না-বিয়োগ করা গড় বাস্তবে বড় ক্ষতি করে না বলে রিপোর্ট হয়েছে।

কোথায় বসাবেন. MLP আর CNN-এ প্রচলিত ক্রম Linear/Conv → BatchNorm → ReLU (মূল প্রবন্ধের মতো); কেউ কেউ ReLU-এর পরে বসান, দুই পক্ষেই পরীক্ষার ফল আছে, সর্বজনীন উত্তর নেই। Transformer-এ প্রশ্নটি pre-norm বনাম post-norm: ২০১৭ সালের মূল Transformer residual যোগের পরে LayerNorm বসাত (post-norm), আর পরে Xiong ও সহকর্মীদের (২০২০) বিশ্লেষণসহ অনেক কাজে দেখা যায় residual শাখার ভেতরে, sublayer-এর আগে বসালে (pre-norm) গভীর model warmup ছাড়াও স্থিরভাবে শেখে। রিপোর্ট অনুযায়ী Llama-র মতো অনেক নতুন ভাষা model pre-norm RMSNorm ব্যবহার করে। বিস্তারিত পর্ব ৩৫ (Transformer block — residual, LayerNorm আর feed-forward)-এ।

৮ · একই গভীর MLP-এ মাপা — default, He, BatchNorm আর LayerNorm

এবার সব একসঙ্গে। Fashion-MNIST (২৮ × ২৮ ধূসর পোশাকের ছবি, ১০টি শ্রেণি; ৬০,০০০ train, ১০,০০০ test). Model: ২০টি Linear স্তর, প্রতিটি ২৫৬ চওড়া, মাঝে ReLU; SGD (learning rate ০.০২, momentum ০.৯), batch ১২৮, ৬ epoch. শুধু একটি জিনিস বদলায়: (ক) default init, (খ) He init আর শূন্য bias, (গ) default init + প্রতিটি Linear-এর পরে BatchNorm, (ঘ) default init + প্রতিটি Linear-এর পরে LayerNorm. প্রতিটি epoch-এর পরে train loss আর test accuracy, আর কোন epoch-এ প্রথম ৮৫% test accuracy.

import os, sys, time, torch
from torch import nn
from torchvision import datasets

torch.set_num_threads(4)
DEPTH, WIDTH, EPOCHS, TARGET = 20, 256, 6, 0.85

def load(train):
    ds = datasets.FashionMNIST("data", train=train, download=True)
    x = ds.data.float().div(255).view(len(ds), -1)
    return (x - 0.2860) / 0.3530, ds.targets      # train-এর গড়/std

Xtr, ytr = load(True); Xte, yte = load(False)

def make(kind):
    layers, d = [], 784
    for _ in range(DEPTH - 1):
        lin = nn.Linear(d, WIDTH)
        if kind in ("he", "lnhe"):
            nn.init.kaiming_normal_(lin.weight, nonlinearity="relu")
            nn.init.zeros_(lin.bias)
        norm = {"bn": nn.BatchNorm1d(WIDTH), "ln": nn.LayerNorm(WIDTH),
                "lnhe": nn.LayerNorm(WIDTH)}.get(kind)
        layers += [lin] + ([norm] if norm else []) + [nn.ReLU()]
        d = WIDTH
    return nn.Sequential(*layers, nn.Linear(d, 10))

def run(kind, seed=0):
    torch.manual_seed(seed)
    model = make(kind)
    opt = torch.optim.SGD(model.parameters(), lr=float(os.environ.get("LR", "0.02")), momentum=0.9)
    lossf, hit, t0, out = nn.CrossEntropyLoss(), None, time.time(), []
    for ep in range(1, EPOCHS + 1):
        model.train(); tot = 0.0
        for idx in torch.randperm(len(Xtr)).split(128):
            loss = lossf(model(Xtr[idx]), ytr[idx])
            opt.zero_grad(); loss.backward(); opt.step()
            tot += loss.item() * len(idx)
        model.eval()
        with torch.no_grad():
            acc = (model(Xte).argmax(1) == yte).float().mean().item()
        if hit is None and acc >= TARGET: hit = ep
        out.append(f"{tot / len(Xtr):.3f}/{acc:.3f}")
    print(f"{kind:8s} seed {seed}", " ".join(out), f"| {TARGET:.0%}: epoch {hit}",
          f"| {time.time() - t0:.0f} s")

if __name__ == "__main__":
    print("torch", torch.__version__, "| depth", DEPTH, "| width", WIDTH)
    print("প্রতি epoch: train loss/test accuracy")
    for kind in sys.argv[1:] or ["default", "he", "bn", "ln"]:
        run(kind, int(os.environ.get("SEED", "0")))

সত্যিকারের run (ডেস্কটপ, CPU-তে, ৪টি thread; PyTorch 2.15.0, torchvision 0.29.2). প্রতিটি ঘরে “train loss/test accuracy”:

~/bazarghar-dl$ uv run python deep_mlp.py
torch 2.15.0+cpu | depth 20 | width 256
প্রতি epoch: train loss/test accuracy
default  seed 0 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 | 85%: epoch None | 22 s
he       seed 0 0.783/0.779 0.467/0.831 0.401/0.844 0.359/0.866 0.336/0.857 0.317/0.871 | 85%: epoch 4 | 25 s
bn       seed 0 0.711/0.798 0.480/0.833 0.408/0.842 0.374/0.858 0.351/0.863 0.330/0.863 | 85%: epoch 4 | 30 s
ln       seed 0 2.326/0.100 2.312/0.100 2.308/0.100 2.306/0.100 2.305/0.100 2.304/0.100 | 85%: epoch None | 33 s
~/bazarghar-dl$ SEED=1 uv run python deep_mlp.py
…
default  seed 1 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 | 85%: epoch None | 18 s
he       seed 1 0.727/0.804 0.464/0.834 0.394/0.854 0.352/0.860 0.328/0.861 0.308/0.863 | 85%: epoch 3 | 18 s
bn       seed 1 0.703/0.795 0.477/0.839 0.414/0.858 0.373/0.845 0.353/0.868 0.328/0.859 | 85%: epoch 3 | 25 s
ln       seed 1 1.900/0.197 1.853/0.197 2.251/0.108 2.302/0.100 2.306/0.100 2.305/0.100 | 85%: epoch None | 33 s
~/bazarghar-dl$ SEED=2 uv run python deep_mlp.py
…
default  seed 2 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 2.303/0.100 | 85%: epoch None | 19 s
he       seed 2 0.797/0.807 0.458/0.844 0.395/0.860 0.357/0.860 0.327/0.872 0.310/0.869 | 85%: epoch 3 | 20 s
bn       seed 2 0.698/0.813 0.476/0.852 0.413/0.850 0.381/0.862 0.352/0.859 0.334/0.864 | 85%: epoch 2 | 20 s
ln       seed 2 2.328/0.100 2.310/0.100 2.307/0.100 2.305/0.100 2.304/0.100 2.304/0.100 | 85%: epoch None | 30 s
~/bazarghar-dl$ uv run python deep_mlp.py lnhe
…
lnhe     seed 0 0.751/0.782 0.493/0.832 0.421/0.844 0.379/0.864 0.352/0.861 0.334/0.872 | 85%: epoch 4 | 25 s
~/bazarghar-dl$ LR=0.005 uv run python deep_mlp.py ln
…
ln       seed 0 0.714/0.806 0.473/0.852 0.405/0.848 0.367/0.863 0.348/0.851 0.348/0.873 | 85%: epoch 2 | 24 s
~/bazarghar-dl$ uv run python ln_probe.py   # training-এর আগে
default  শেষ hidden std 0.022 | নমুনা-ভেদে std 0.0000 | দুই ছবির cosine 1.0000
he       শেষ hidden std 1.089 | নমুনা-ভেদে std 0.4215 | দুই ছবির cosine 0.9340
ln       শেষ hidden std 0.570 | নমুনা-ভেদে std 0.1866 | দুই ছবির cosine 0.9293
ln+he    শেষ hidden std 0.592 | নমুনা-ভেদে std 0.1466 | দুই ছবির cosine 0.9536

পড়ার নিয়ম: ১০টি শ্রেণিতে আন্দাজে উত্তর দিলে loss −ln(১/১০) = ২.৩০৩ আর accuracy ০.১০০. তিনটি seed-এর সারাংশ:

সংস্করণ৮৫%-এ পৌঁছানোর epoch (seed ০, ১, ২)৬ epoch পরে test accuracyseed ০-এর সময়
default initকখনো না (৩টিতেই)০.১০০, ০.১০০, ০.১০০২২ s
He init৪, ৩, ৩০.৮৭১, ০.৮৬৩, ০.৮৬৯২৫ s
default + BatchNorm৪, ৩, ২০.৮৬৩, ০.৮৫৯, ০.৮৬৪৩০ s
default + LayerNorm (lr ০.০২)কখনো না (৩টিতেই)০.১০০, ০.১০০, ০.১০০৩৩ s
He + LayerNorm (lr ০.০২)৪ (seed ০)০.৮৭২২৫ s
default + LayerNorm (lr ০.০০৫)২ (seed ০)০.৮৭৩২৪ s

Default init: loss ছয় epoch ধরে ২.৩০৩-এ স্থির, accuracy ঠিক ০.১০০, তিনটি seed-এই। অংশ ৩-এর মাপের সঙ্গে মেলে: শেষ স্তরে প্রায় শুধু bias-এর গুঞ্জন পৌঁছায়, তাই model সব ছবিকে একই রকম দেখে, আর gradient-ও একই শিকল ধরে মিলিয়ে যায়। He init আর BatchNorm দুটিই প্রথম epoch-এই ৭৮–৮১% আর ২ থেকে ৪ epoch-এ ৮৫%. BatchNorm init ঠিক না করেও কাজ করেছে, কারণ প্রতিটি স্তরে মাপ নতুন করে বসায়; বিনিময়ে প্রতি epoch-এ সময় একটু বেশি। এই ছোট পরীক্ষায় দুটির মধ্যে স্পষ্ট বিজয়ী নেই; seed বদলালে ক্রম বদলায়।

LayerNorm-এর ফলটি সবচেয়ে শিক্ষণীয়। Default init + LayerNorm, lr ০.০২-এ তিনটি seed-এই আটকে গেছে (seed ১-এ প্রথমে ০.১৯৭ পর্যন্ত উঠে আবার ০.১০০-এ নেমেছে)। অথচ শুরুর অবস্থা খারাপ ছিল না: আলাদা একটি পরীক্ষায় (ln_probe.py, একই model, training-এর আগে) শেষ hidden স্তরে বিভিন্ন ছবির মধ্যে পার্থক্যের std ০.১৮৭, যেখানে default-এ ০.০০০০. একই model lr ০.০০৫-এ দুই epoch-এ ৮৫% ছোঁয়, আর He init দিলে lr ০.০২-তেও চলে। অর্থাৎ এখানে LayerNorm signal বাঁচিয়েছে, কিন্তু এই learning rate-এ শেখা অস্থির হয়েছে। কেন ঠিক এই সংমিশ্রণে, তা এই কয়েকটি run থেকে নিশ্চিত বলা যায় না; বাস্তব শিক্ষা হলো normalization যাদু নয়: init, learning rate আর স্থাপত্য একসঙ্গে কাজ করে, আর সাধারণ MLP-এ LayerNorm-এর চেয়ে Transformer-এর residual পথের সঙ্গে LayerNorm-এর জুটিই বেশি প্রচলিত। Learning rate কীভাবে বাছবেন তা পর্ব ১৫ (Optimizer আর learning-rate schedule — SGD, AdamW, warmup আর cosine)-এ, আর “loss ২.৩০৩-এ আটকে” ধরনের লক্ষণ পড়া পর্ব ১৮ (Training ডিবাগ করা — এক batch overfit, learning curve আর সাধারণ ভুল)-এ। সব সংখ্যা একটি মেশিনে একটি করে run; সাধারণ benchmark নয়।

আরও গভীরে: Saxe ও সহকর্মীরা (২০১৪) দেখান orthogonal weight (nn.init.orthogonal_) গভীর linear network-এ শুধু variance নয়, পুরো singular value-র বণ্টন ধরে রাখে; পরে “dynamical isometry” ধারণায় (Pennington ও সহকর্মী, ২০১৭) এর বিস্তার হয়। Residual network-এ normalization ছাড়াই শেখার চেষ্টা হয়েছে, যেমন Fixup (Zhang ও সহকর্মী, ২০১৯) শেষ স্তরকে শূন্যে শুরু করে, আর NFNet (Brock ও সহকর্মী, ২০২১) weight standardization আর gradient clipping দিয়ে BatchNorm বাদ দেয়। Transformer-এ বড় model-এর জন্য init-এর মাপ গভীরতা অনুযায়ী ছোট করার কৌশল (যেমন residual শাখার output-কে 1/√(2L) দিয়ে মাপা) বহুল প্রচলিত বলে রিপোর্ট হয়। PyTorch-এর torch.nn.init docs-এ প্রতিটি ফাংশনের সঠিক সূত্র আছে।

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

  • fan_in = ৫১২, ReLU: He init-এর std কত? √(২/৫১২) = ০.০৬২৫.
  • Xavier আর He-এর পার্থক্য কেন? ReLU অর্ধেক মান শূন্য করে, E[x²] = Var(z)/২; সেটি পূরণে He-এ Var(W) দ্বিগুণ।
  • সব weight শূন্য দিলে কী হয়? সব hidden unit একই gradient পায়, যমজ থেকে যায়; tanh/ReLU-তে gradient-ই শূন্য।
  • BatchNorm-এ train আর eval-এর পার্থক্য? Train-এ batch-এর গড়/variance আর running stats হালনাগাদ; eval-এ জমানো running_mean/running_var.
  • Transformer-এ BatchNorm নয় কেন LayerNorm? LayerNorm প্রতিটি token-এর ভেতরে কাজ করে, batch আকার, padding আর একটি একটি token তৈরির ওপর নির্ভর করে না।

৯ · AI-এর যুগে — AI-এর লেখা init আর normalization যাচাই

AI assistant-কে “deep MLP লিখে দাও” বললে সাধারণত চলে এমন কোড আসে, কিন্তু এই পর্বের ভুলগুলো ঠিক সেই ধরনের, যা error দেয় না। পুরোনো টিউটোরিয়াল থেকে শেখা AI কখনো nn.init.xavier_uniform (শেষে _ ছাড়া, অনেক আগে deprecated) লেখে, ReLU network-এ Xavier দেয়, BatchNorm-এর আগের Linear-এ bias রাখে, বা validation-এ model.eval() ভুলে যায়। “BatchNorm internal covariate shift দূর করে” কথাটি প্রায়ই প্রমাণিত সত্য হিসেবে লেখে, যদিও অংশ ৫-এ দেখেছেন বিষয়টি বিতর্কিত।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
ReLU-এ Xavier, বা activation না দেখে initপ্রতি স্তরে variance অর্ধেকForward hook বা লুপে স্তর ধরে std ছাপুন
Validation/prediction-এ eval() নেইBatchNorm batch-এর পরিসংখ্যান নেয়, ফল batch-নির্ভরএকই ছবি দুটি আলাদা batch-এ দিয়ে মিলিয়ে দেখুন
batch ১ বা ২-এ BatchNorm দিয়ে fine-tuneঅর্থহীন পরিসংখ্যান, ValueError বা নষ্ট running statsbatch আকার দেখুন; GroupNorm/LayerNorm বা BN freeze
একটি seed-এর ফলে “X সবচেয়ে দ্রুত” দাবিঅংশ ৮-এ দেখেছেন learning rate আর seed-এ ফল বদলায়কয়েকটি seed আর learning rate-এ তুলনা
পুরোনো বা বানানো APIdeprecated নাম, ভুল আর্গুমেন্টনিজের PyTorch version-এর docs-এ মিলিয়ে নিন

AI-কে কোডের সঙ্গে যাচাইয়ের পথও চাইতে বলুন:

আমি PyTorch-এ (আমার version নিজে লিখে দেব) ১৫ স্তরের একটি ReLU MLP লিখছি, input 784, প্রতিটি hidden 256, output 10. ১. প্রতিটি nn.Linear-এ He (kaiming_normal_, nonlinearity="relu") init আর শূন্য bias বসানোর কোড model.apply দিয়ে দাও। ২. একটি function দাও যা একটি random batch-এ প্রতিটি স্তরের activation-এর std ছাপে, যাতে আমি দেখতে পারি std গভীরতায় স্থির কি না। ৩. BatchNorm1d যোগ করলে কোথায় বসবে আর আগের Linear-এ bias=False কেন, এক লাইনে বলো। ৪. Validation-এ eval() আর no_grad() কোথায় বসবে দেখাও, আর একই ছবি দুটি আলাদা batch-এ দিয়ে একই output আসে কি না যাচাইয়ের assert দাও। ৫. যে ফাংশনের নাম বা আর্গুমেন্টে তুমি নিশ্চিত নও, সেটি স্পষ্ট করে বলো, আর docs-এর কোন পাতায় মেলাব তা দাও।

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

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

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

প্র ০১২০০০ সালের আগে গভীর network শেখানো “প্রায় অসম্ভব” মনে করা হতো। Init-এর ধারণা সেই গল্পে কোথায় বসে?

১৯৯০-এর দশকে backpropagation জানা ছিল, কিন্তু দুই-তিন স্তরের বেশি গভীর network সাধারণত ভালো শিখত না। Hochreiter-এর ১৯৯১ সালের থিসিস আর Bengio ও সহকর্মীদের ১৯৯৪ সালের প্রবন্ধ দেখায়, অনেক ধাপ পেরিয়ে gradient কেন হারায় বা ফাটে। ২০০৬ সালে Hinton ও সহকর্মীরা স্তর ধরে ধরে unsupervised pretraining (deep belief network) দিয়ে গভীর network শেখালেন, আর কয়েক বছর এটিই প্রধান পথ বলে মনে হয়েছিল।

Glorot আর Bengio-র ২০১০ সালের প্রবন্ধ এই গল্প বদলাতে সাহায্য করে। তাঁরা স্তর ধরে activation আর gradient-এর histogram এঁকে দেখালেন, তখনকার প্রচলিত init আর sigmoid activation মিলে প্রথম দিকেই শেষ স্তরগুলো saturate করে; ঠিক মাপের init আর ভালো activation দিলে pretraining ছাড়াই অনেক দূর যাওয়া যায়। একই সময়ে ReLU-এর ব্যবহার (Nair আর Hinton ২০১০, Glorot ও সহকর্মী ২০১১) আর GPU-তে বড় ডেটা মিলে ২০১২ সালের AlexNet-এর পথ তৈরি হয়। ২০১৫ সালে He ও সহকর্মীরা ReLU-এর জন্য সংশোধিত সূত্র দেন আর খুব গভীর CNN শুরু থেকে শেখান; একই বছর BatchNorm আসে, আর তার কিছু পরে residual connection.

গল্পটির শিক্ষা হলো, “গভীর network শেখে না” ধারণাটি আংশিকভাবে ভুল প্রকৌশলের ফল ছিল, মৌলিক সীমা নয়। এই পর্বের অংশ ৮-এর default-init MLP ঠিক সেই পুরোনো অবস্থার একটি ছোট প্রতিচ্ছবি: একই স্থাপত্য, একই ডেটা, শুধু init বদলে ১০% থেকে ৮৭%. তবে কোনো একটি কৌশল একাই সব বদলায়নি; ডেটা, hardware, activation, init, normalization আর স্থাপত্য একসঙ্গে এগিয়েছে, আর কোন অবদান কতটা, তা নিয়ে ইতিহাসবিদ আর গবেষকদের মতভেদ আছে।

মূল উপলব্ধি: গভীরতার সমস্যার একটি বড় অংশ ছিল সংখ্যার মাপের সমস্যা; variance-এর সরল হিসাব সেটি অনেকটা সমাধান করেছে।

প্র ০২BatchNorm কেন কাজ করে — “internal covariate shift” ব্যাখ্যাটি নিয়ে বিতর্ক কী, আর তা থেকে বিজ্ঞানের কোন শিক্ষা পাওয়া যায়?

Ioffe আর Szegedy ২০১৫ সালে BatchNorm প্রস্তাবের সময় যুক্তি দেন: training-এ আগের স্তর বদলালে পরের স্তরের input-এর বণ্টন সরে যায়, তাই পরের স্তরকে বারবার নতুন বণ্টনে মানিয়ে নিতে হয়; এর নাম তাঁরা দেন internal covariate shift, আর BatchNorm তা কমায়। ফলাফল ছিল চমকপ্রদ: ImageNet-এ অনেক কম ধাপে একই accuracy, আর বড় learning rate.

২০১৮ সালে Santurkar, Tsipras, Ilyas আর Madry একটি সরাসরি পরীক্ষা করেন। BatchNorm-এর পরে ইচ্ছে করে এলোমেলো noise যোগ করে বণ্টন আরও অস্থির করলেন, তবু training-এর সুবিধা প্রায় থেকে গেল। আবার কিছু মাপে BatchNorm-ওয়ালা network-এ covariate shift কমেনি। তাঁদের ব্যাখ্যা: BatchNorm loss-এর তল আর gradient-কে মসৃণ করে (Lipschitz ধ্রুবক কমায়), ফলে বড় ধাপ নিরাপদ হয়। অন্য কাজ আরও ব্যাখ্যা দেয়: weight-এর মাপের প্রতি অসংবেদনশীলতা, যার ফলে কার্যকর learning rate নিজে থেকেই বদলায়; আর batch-এর noise থেকে কিছুটা regularization.

আজও একটি সর্বসম্মত ব্যাখ্যা নেই। এর শিক্ষা দুটি। প্রথম, কোনো পদ্ধতি কাজ করছে মানে লেখকের ব্যাখ্যাও সঠিক, তা নয়; deep learning-এ অনেক কৌশল আগে পরীক্ষায় কাজ করেছে, তত্ত্ব এসেছে পরে। দ্বিতীয়, ব্যাখ্যা যাচাইয়ের পথ হলো এমন পরীক্ষা নকশা করা যেখানে দুটি ব্যাখ্যা আলাদা ভবিষ্যদ্বাণী করে, ঠিক যেমন Santurkar ও সহকর্মীরা noise যোগ করে করেছেন।

AI assistant বা জনপ্রিয় ব্লগ প্রায়ই “BatchNorm covariate shift দূর করে” কথাটি সত্য হিসেবে লেখে। ইন্টারভিউতে প্রশ্ন এলে মূল দাবি, পরের সমালোচনা আর ব্যবহারিক ফল তিনটিই বলা সবচেয়ে সৎ উত্তর।

মূল উপলব্ধি: BatchNorm-এর উপকার পরীক্ষায় দৃঢ়, কিন্তু কেন, তার ব্যাখ্যা এখনো খোলা প্রশ্ন; কাজ করা আর বোঝা আলাদা জিনিস।

প্র ০৩Transformer কেন BatchNorm নয়, LayerNorm (আর এখন প্রায়ই RMSNorm) ব্যবহার করে? Batch-নির্ভরতা text-এ কী কী সমস্যা তৈরি করে?

Text-এর একটি batch-এ বাক্যগুলোর দৈর্ঘ্য আলাদা, তাই ছোট বাক্যের শেষে padding বসে (পর্ব ২৭-এ বিস্তার)। BatchNorm একটি feature-এর গড় batch আর সব অবস্থান জুড়ে নিলে padding-এর ভুয়া token-ও গড়ে ঢোকে, আর কোন বাক্য কোন batch-এ পড়ল তার ওপর ফল বদলায়। Text তৈরির সময় model একবারে একটি token বানায়, প্রায়ই batch ১ নিয়ে; তখন training-এর batch-পরিসংখ্যান আর inference-এর running stats-এর মধ্যে অমিল বাড়ে। বড় model-কে অনেক GPU-তে ভাগ করে শেখালে প্রতিটি GPU-র ছোট batch-এর পরিসংখ্যান অস্থির, আর সব GPU মিলিয়ে নিতে (SyncBatchNorm) যোগাযোগের খরচ।

LayerNorm এই সব এড়ায়: প্রতিটি token-এর vector তার নিজের feature জুড়ে normalize হয়, batch-এ আর কে আছে তাতে কিছু যায় আসে না, train আর eval-এ একই হিসাব, running stats নেই। ২০১৭ সালের মূল Transformer তাই LayerNorm নেয়। পরে RMSNorm (২০১৯) গড় বিয়োগ বাদ দিয়ে একটু সস্তা হিসাব দেয়, আর রিপোর্ট অনুযায়ী Llama-র মতো অনেক নতুন ভাষা model RMSNorm ব্যবহার করে। গতির লাভ কতটা, তা hardware আর বাস্তবায়নের ওপর নির্ভর করে; লেখকেরা নিজেদের পরীক্ষায় কয়েক শতাংশ থেকে বেশি সময় বাঁচার কথা জানান।

ছবিতে কিন্তু BatchNorm এখনো প্রচলিত, কারণ সেখানে batch সাধারণত বড়, সব ছবি একই আকারের, আর একটি channel-এর পরিসংখ্যান batch আর পুরো ছবি জুড়ে অর্থবহ। আবার কিছু নতুন CNN নকশা (যেমন ConvNeXt, পর্ব ২২-এর বিষয়) LayerNorm নিয়েছে বলে রিপোর্ট হয়েছে। অর্থাৎ পছন্দটি ডেটার গঠন আর batch-এর বাস্তবতা থেকে আসে, কোনো একটি “সেরা” নিয়ম থেকে নয়।

মূল উপলব্ধি: কোন axis ধরে normalize করবেন, তা ঠিক হয় কোন axis-এর পরিসংখ্যান training আর inference দুই সময়েই নির্ভরযোগ্য, তা দিয়ে।

প্র ০৪Xavier আর He-এর হিসাব স্বাধীনতা, শূন্য গড় আর একই বণ্টন ধরে নেয়। বাস্তবে এই অনুমানগুলো কোথায় ভাঙে, আর তবু সূত্রগুলো কেন কাজে লাগে?

অনুমান তিনটি: input-এর উপাদানগুলো পরস্পর স্বাধীন, weight-এর গড় শূন্য আর input থেকে স্বাধীন, আর পূর্ব-activation শূন্যের দুই পাশে প্রতিসম। প্রথমটি ছবিতে স্পষ্টভাবে ভাঙে: পাশাপাশি pixel প্রায় একসঙ্গে ওঠানামা করে। দ্বিতীয়টি শুধু শুরুর মুহূর্তে সত্য; training শুরু হলেই weight ডেটার সঙ্গে সম্পর্কিত হয়ে যায়। তৃতীয়টি bias বা ReLU-এর পরে ধনাত্মক মানে ভাঙে, আর input নিজে standardize না হলে প্রথম স্তরেই গোলমাল।

তবু সূত্রগুলো কাজে লাগে, কারণ তাদের লক্ষ্য সঠিক মান নয়, সঠিক মাত্রা (order of magnitude). ২০ স্তরে প্রতি স্তরের গুণক ০.৫ আর ১.০-এর পার্থক্য ১০৬ গুণ; কিন্তু ০.৯ আর ১.১-এর পার্থক্য অনেক ছোট, আর normalization বা optimizer সেটুকু সামলে নেয়। এই পর্বের মাপেও He-এ std ০.৮৩ থেকে ১.১৪-এর মধ্যে ঘুরেছে, ঠিক ১ নয়, কিন্তু হারায়ওনি, ফাটেওনি। এজন্য init-কে “প্রথম অনুমান” বলা যায়, যা network-কে শেখার যোগ্য অঞ্চলে বসায়; বাকিটা training করে।

যেখানে অনুমান খুব বেশি ভাঙে, সেখানে ডেটা দেখে init ঠিক করার পদ্ধতি আছে: Mishkin আর Matas-এর LSUV (২০১৫) প্রতিটি স্তরে একটি আসল batch চালিয়ে weight-কে এমনভাবে মাপে যাতে output-এর variance ঠিক ১ হয়। Transformer-এর মতো বড় model-এ গভীরতা অনুযায়ী residual শাখা ছোট করে শুরু করা হয়। আর ভাষা model-এর embedding-এর init নিয়ে আলাদা রীতি আছে।

পরীক্ষায় সূত্র মুখস্থের সঙ্গে অনুমানগুলোও লিখতে পারলে উত্তর অনেক পূর্ণ হয়।

মূল উপলব্ধি: init-এর সূত্র সঠিক মাত্রা দেয়, সঠিক মান নয়; অনুমান জানলে বোঝা যায় কখন সূত্রে ভরসা করবেন আর কখন মেপে নেবেন।

প্র ০৫Fine-tuning বা ছোট batch-এ BatchNorm কেন গোলমাল করে, আর বাস্তবে দলগুলো কী কী সমাধান নেয়?

BatchNorm-এর দুই রূপ: training-এ batch-এর পরিসংখ্যান, eval-এ running stats. দুটি মিলবে শুধু যদি training-এর batch যথেষ্ট বড় আর প্রতিনিধিত্বমূলক হয়। ছোট batch-এ (ধরুন ২ থেকে ৪) প্রতিটি batch-এর গড় আর variance খুব এলোমেলো, ফলে training-এর normalize অস্থির, আর running stats-ও সেই দোলায় ভরে। Detection বা segmentation-এর মতো কাজে বড় ছবির জন্য GPU-তে কয়েকটি ছবিই আঁটে, তাই সমস্যাটি খুব বাস্তব; Wu আর He-এর GroupNorm প্রবন্ধ (২০১৮) ঠিক এই পরিস্থিতিতে batch ২-এ BatchNorm-এর ভুলের হার অনেক বেড়ে যাওয়া দেখায়।

Fine-tuning-এ আরেকটি ঝামেলা: pretrained model-এর running stats বড় সাধারণ ডেটা থেকে জমানো, আর নতুন ছোট ডেটায় train mode চালালেই সেগুলো নতুন ছোট batch-এর দিকে সরে যায়, যদিও weight freeze করা থাকে, কারণ buffer optimizer-এর নিয়ন্ত্রণে নয় (পর্ব ০৯)। Transfer learning-এ (পর্ব ২৩) তাই অনেকে BatchNorm layer-গুলোকে আলাদাভাবে eval mode-এ রাখেন।

প্রচলিত সমাধান কয়েকটি। Batch বাড়ানো, বা কয়েকটি GPU-র পরিসংখ্যান মিলিয়ে নেওয়া (SyncBatchNorm). BatchNorm-কে freeze করা: শুধু running stats দিয়ে চালানো, শেখা বন্ধ। GroupNorm বা LayerNorm-এ বদলানো, যারা batch দেখে না। মনে রাখুন, gradient accumulation (পর্ব ১৫-এর বিষয়) gradient-এর জন্য বড় batch-এর অনুকরণ করে, কিন্তু BatchNorm-এর পরিসংখ্যান তখনো ছোট micro-batch থেকেই আসে। আর অংশ ৬-এর মতো সাজানো ডেটার batch এড়াতে shuffle জরুরি (পর্ব ১১)।

মূল উপলব্ধি: BatchNorm-এর পরিসংখ্যান batch-এর মান যতটা ভালো, ততটাই ভালো; ছোট, সাজানো বা fine-tuning-এর batch-এ আগে থেকে পরিকল্পনা দরকার।

অনুশীলন

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

  1. পূর্ণ numerical: (ক) একটি Linear(512, 128), পরে ReLU. He (normal) আর Xavier (uniform)-এ weight-এর std আর uniform সীমা কত? (খ) একটি BatchNorm feature-এ batch x = [1, 3, 5, 7], γ = 0.5, β = 1, ε উপেক্ষা করুন। y কত? (গ) শুরুতে running_mean = 0, running_var = 1, momentum 0.1 হলে এক step পরে দুটি কত?

    (ক) He: Var = ২/৫১২ = ০.০০৩৯০৬, std = ০.০৬২৫. Xavier: Var = ২/(৫১২ + ১২৮) = ০.০০৩১২৫, std ≈ ০.০৫৫৯; uniform সীমা √(৬/৬৪০) ≈ ০.০৯৬৮. (খ) গড় ৪, বিচ্যুতি [−৩, −১, ১, ৩], variance (÷N) = ২০/৪ = ৫, std ≈ ২.২৩৬; x̂ = [−১.৩৪২, −০.৪৪৭, ০.৪৪৭, ১.৩৪২]; y = ০.৫x̂ + ১ = [০.৩২৯, ০.৭৭৬, ১.২২৪, ১.৬৭১]. (গ) running_mean = ০.১ × ৪ = ০.৪; unbiased variance ২০/৩ ≈ ৬.৬৬৭, তাই running_var = ০.৯ + ০.৬৬৭ = ১.৫৬৬৭.

  2. সমালোচনা ও নকশা: একজন সহকর্মী বাজারঘরের পণ্যের ছবির জন্য ৩০ স্তরের MLP বানিয়েছেন: সব weight torch.randn × ০.০১, প্রতিটি স্তরে sigmoid, BatchNorm শেষ স্তরের পরে একবার, batch ৪, validation-এ model.eval() নেই। loss প্রথম থেকেই প্রায় স্থির। কী কী সমস্যা, আর আপনার নকশা কী?

    Init: std ০.০১ fan_in-এর তুলনায় খুব ছোট, ৩০ স্তরে signal হারায় (অংশ ১-এর মাপে ২০ স্তরেই ১০−১৯)। Activation: sigmoid-এর ঢাল সর্বোচ্চ ০.২৫ আর গড় ০.৫, ফলে গভীরে gradient আরও কমে; ReLU আর He init নিন। Normalization-এর জায়গা: শুধু শেষে একবার বসালে মাঝের স্তরের মাপ ঠিক হয় না; প্রতিটি Linear-এর পরে বসান, আগের Linear-এ bias=False. Batch ৪: BatchNorm-এর পরিসংখ্যান অস্থির; batch বাড়ান বা LayerNorm/GroupNorm. Eval: validation-এ eval() না দিলে ফল batch-নির্ভর আর running stats নষ্ট। ছবির জন্য দীর্ঘমেয়াদে CNN (পর্ব ২০) আর residual connection (পর্ব ১৭) ভাবুন। পরিবর্তনের পর স্তর ধরে activation-এর std ছেপে আর শুরুর loss −ln(1/C)-এর কাছে কি না দেখে যাচাই করুন।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন দুটি print কী দেখাবে।
    import torch
    from torch import nn
    bn = nn.BatchNorm1d(1)
    x = torch.tensor([[1.], [2.], [3.], [6.]])
    bn(x)
    print(bn.running_mean.item(), round(bn.running_var.item(), 4))
    bn.eval()
    print(bn(torch.tensor([[3.]])).item() == bn(torch.tensor([[3.], [100.]]))[0].item())
    0.30000001192092896 1.3667
    True

    নতুন module default-এ train mode-এ, তাই প্রথম forward running stats হালনাগাদ করে। গড় ৩, তাই running_mean = ০.১ × ৩ = ০.৩ (float32-এ ০.৩০০০০০০১১৯…)। Running-এ unbiased variance: বিচ্যুতি [−২, −১, ০, ৩], বর্গের যোগ ১৪, ÷৩ ≈ ৪.৬৬৭; running_var = ০.৯ × ১ + ০.১ × ৪.৬৬৭ = ১.৩৬৬৭. দ্বিতীয় লাইন: eval-এ শুধু running stats ব্যবহার হয়, তাই নমুনা ৩ একা থাকুক বা ১০০-এর পাশে, output একই, True. ডেস্কটপে (PyTorch 2.15.0) চালিয়ে হুবহু এই আউটপুট পাওয়া গেছে।

  4. কোড লিখুন: NumPy-তে একটি function group_norm(x, groups) লিখুন, যেখানে x-এর আকার (N, C) আর C, groups দিয়ে বিভাজ্য। প্রতিটি নমুনার প্রতিটি দলের ভেতরে গড় বিয়োগ করে std দিয়ে ভাগ করবে (ε = 1e-5). groups = 1 আর groups = C হলে কোন পরিচিত পদ্ধতি পাওয়া যায়?
    import numpy as np
    def group_norm(x, groups, eps=1e-5):
        N, C = x.shape
        g = x.reshape(N, groups, C // groups)       # (N, দল, দলের ভেতরের feature)
        mu = g.mean(-1, keepdims=True)
        var = g.var(-1, keepdims=True)
        return ((g - mu) / np.sqrt(var + eps)).reshape(N, C)
    
    x = np.array([[1., 2., 3., 4., 10., 20.]])
    print(group_norm(x, 2).round(3))
    # ডেস্কটপের আউটপুট: [[-1.225  0.     1.225 -1.111 -0.202  1.313]]

    groups = ১ হলে একটি নমুনার সব feature একসঙ্গে: এটি LayerNorm (γ, β ছাড়া)। groups = C হলে প্রতিটি feature একা; (N, C) আকারে তখন প্রতিটি দলে একটি মাত্র সংখ্যা, variance ০, ফল সব ০. ছবির (N, C, H, W) আকারে এটিই InstanceNorm: প্রতিটি ছবির প্রতিটি channel তার H × W জুড়ে। PyTorch-এ nn.GroupNorm(num_groups, num_channels)-এ শেখা γ আর β প্রতিটি channel-এ থাকে।

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

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

ক্রমTraining mode-এ BatchNorm-এর একটি forward-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রতিটি feature-এর জন্য batch-এর গড় μ হিসাব করা
  2. গড় থেকে বিচ্যুতির বর্গের গড়, অর্থাৎ variance σ² (÷N) হিসাব করা
  3. (x − μ)/√(σ² + ε) দিয়ে x̂ বের করা
  4. শেখা γ দিয়ে গুণ আর β যোগ করে y বের করা
  5. momentum দিয়ে running_mean আর running_var (unbiased) হালনাগাদ করা

একটি স্তরের fan_in = 128, fan_out = 128, আর activation ReLU. He (Kaiming) normal init-এ weight-এর variance কত?

  • 1/128
  • 2/256
  • 2/128
  • √(2/128)

একটি MLP-এর একটি hidden স্তরের সব weight একই মান 0.5 দিয়ে শুরু করা হলো (bias শূন্য), তারপর gradient descent চালানো হলো। কী ঘটবে?

  • কয়েক ধাপ পরে unit-গুলো নিজে থেকে আলাদা feature শিখবে
  • সব hidden unit সারাজীবন একই থাকবে; স্তরটি কার্যত একটি unit-এর মতো কাজ করবে
  • gradient সবসময় শূন্য, তাই কিছুই বদলাবে না
  • Learning rate বড় করলেই প্রতিসাম্য ভাঙবে

PyTorch-এর nn.BatchNorm1d (momentum 0.1) শুরুতে running_mean = 0. একটি feature-এ প্রথম batch-এর গড় 8. এক training step পরে running_mean কত?

  • 8
  • 7.2
  • 4
  • 0.8

BatchNorm-ওয়ালা একটি PyTorch model-এ prediction-এর আগে model.eval() ডাকা হয়নি, আর test-এর ডেটা শ্রেণি ধরে সাজানো batch-এ আসছে। সবচেয়ে সম্ভাব্য ফল কী?

  • Accuracy অনেক নেমে যাবে, কারণ batch-এর গড় শ্রেণির বৈশিষ্ট্য মুছে দেয়; running stats-ও বদলাবে
  • কিছুই বদলাবে না, কারণ BatchNorm সবসময় running stats ব্যবহার করে
  • PyTorch সঙ্গে সঙ্গে error দেবে
  • শুধু গতি একটু কমবে, ফল একই থাকবে

x = [1, 2, 3, 4] আর x + 100 = [101, 102, 103, 104] দুটি vector-এ LayerNorm আর RMSNorm (γ = 1, β = 0) চালালে কোনটি সত্য?

  • দুটি পদ্ধতিই দুটি vector-কে একই ফল দেবে
  • RMSNorm দুটিকে একই করবে, LayerNorm নয়
  • LayerNorm দুটিকে একই করবে, RMSNorm নয়
  • কোনোটিই একই করবে না, কারণ দুটিই batch-এর ওপর নির্ভর করে

২০ স্তরের ReLU network-এ প্রতিটি স্তরের weight-এর variance 1/fan_in (bias শূন্য, input-এর variance 1). তত্ত্ব অনুযায়ী শেষ স্তরের পূর্ব-activation-এর variance মোটামুটি কত?

  • প্রায় 1, কারণ 1/fan_in ঠিক variance রক্ষা করে
  • প্রায় (½)^20 ≈ 10^−6
  • প্রায় 2^20 ≈ 10^6
  • ঠিক 0, কারণ ReLU সব মান শূন্য করে

শব্দকোষ

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

মনে রাখুন

  1. গুণের শিকল: প্রতি স্তরে variance n · Var(W) · (activation-এর ভাগ) দিয়ে গুণ হয়; গভীরতায় ১-এর একটু কম বা বেশিও হারায় বা ফাটে।
  2. সূত্র: Xavier Var(W) = 2/(fan_in + fan_out), tanh/linear-এ; He Var(W) = 2/fan_in, ReLU-তে। nn.Linear-এর default 1/(3·fan_in), গভীর ReLU MLP-এ দুর্বল।
  3. এলোমেলোতা: শূন্য বা constant init-এ hidden unit-গুলো যমজ থাকে; weight এলোমেলো, bias শূন্য চলে।
  4. BatchNorm: train-এ batch-এর গড়/variance আর running stats হালনাগাদ, eval-এ running stats; ছোট বা সাজানো batch আর eval() ভুলে যাওয়া বড় ফাঁদ।
  5. LayerNorm/RMSNorm/GroupNorm: নমুনার ভেতরে normalize, batch-নিরপেক্ষ; Transformer-এ LayerNorm বা RMSNorm. Normalization যাদু নয়: learning rate আর init-এর সঙ্গে মেপে দেখুন।

পরের পর্বে: Network এখন শেখার যোগ্য শুরু পেয়েছে; এবার কত বড় ধাপে আর কোন পথে শেখাবেন। পর্ব ১৫ (Optimizer আর learning-rate schedule — SGD, AdamW, warmup আর cosine) learning rate range test, warmup, cosine decay আর AdamW-এর weight decay একই task-এ মেপে দেখাবে, আর এই পর্বের LayerNorm-এর অস্থিরতার মতো ঘটনা পড়তে সাহায্য করবে।

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

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