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

Optimizer আর learning-rate schedule — SGD, AdamW, warmup আর cosine

Optimizers and Learning-Rate Schedules — LR Range Test, Warmup, Cosine, One-Cycle, AdamW, Parameter Groups and Gradient Accumulation

Taught in Bangla, this lesson puts the learning rate first. It plots five schedule shapes, explains why Adam needs warmup, runs an LR range test, and fixes scheduler order and ReduceLROnPlateau usage. It shows why Adam plus L2 differs from AdamW, uses parameter groups to skip decay on biases and norms, and checks that scaled gradient accumulation matches a large batch. A real desktop run on MNIST compares SGD-momentum and AdamW with and without warmup plus cosine over three seeds.

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

  • পাঁচটি schedule (constant, step, cosine, warmup + cosine, one-cycle)-এর সূত্র আর যেকোনো ধাপে মান হাতে বের করা
  • Adam-এর জন্য warmup কেন দরকার, আর LR range test দিয়ে learning rate খোঁজা
  • PyTorch-এর scheduler, ডাকার ক্রম আর ReduceLROnPlateau-এর আচরণ
  • Adam + L2 আর AdamW-এর পার্থক্য, আর bias ও norm-কে weight decay থেকে বাদ রাখা
  • Gradient accumulation, effective batch আর batch size বাড়ালে learning rate কতটা বাড়াবেন

মেশিন লার্নিং কোর্সের পর্ব ১৩-এ SGD, mini-batch আর schedule-এর তত্ত্ব, আর পর্ব ১৪-এ momentum, Adam (bias correction-সহ) আর AdamW derive করা আছে। এখানে সেগুলো আবার derive করব না। এই পর্বের প্রশ্ন ব্যবহারিক: গভীর network-এ এগুলো কীভাবে আচরণ করে, আর PyTorch-এ ঠিকভাবে কীভাবে বসাবেন। এই কোর্সের পর্ব ১০-এ training loop আর scheduler ডাকার ক্রম প্রথম দেখেছেন। পাতার তিনটি ব্লক NumPy-তে চলে (প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে); PyTorch-এর সব আউটপুট ডেস্কটপের সত্যিকারের run থেকে, CPU-তে, torch 2.15.0 আর Python 3.14.4 দিয়ে (লেখার সময়ে)।

১ · Learning rate — সবচেয়ে জরুরি hyperparameter

অন্ধকারে পাহাড়ি পথে নামছেন, হাতে শুধু একটি টর্চ। পা ফেলার মাপটাই learning rate. খুব ছোট পা ফেললে রাত পেরিয়ে যাবে। খুব বড় পা ফেললে খাদের ওপারে লাফিয়ে পড়বেন। শুরুতে চোখ অন্ধকারে সয়ে আসেনি, তাই সাবধানে ছোট পা (warmup). তারপর আত্মবিশ্বাসী লম্বা পা, আর নিচে উপত্যকার কাছে এসে আবার ছোট ছোট পা, যাতে ঠিক সবচেয়ে নিচু জায়গায় থামেন (decay). পুরো পরিকল্পনাটিই learning-rate schedule.

এক লাইনের recap: SGD-তে w ← w − η·g, যেখানে η (ইটা) learning rate আর g একটি mini-batch-এর gradient. Momentum আগের ধাপগুলোর চলমান যোগফল রাখে; Adam প্রতিটি parameter-এর ধাপকে তার gradient-এর বর্গের চলমান গড়ের বর্গমূল দিয়ে ভাগ করে; AdamW সঙ্গে weight decay আলাদাভাবে প্রয়োগ করে। সব optimizer-এই η একটি গুণক, যা প্রতিটি ধাপের মাপ ঠিক করে।

গভীর network-এ η-এর প্রভাব আরও তীব্র, তিনটি কারণে। প্রথম, loss-এর ভূদৃশ্য বিভিন্ন দিকে খুব আলাদা বাঁকা; সবচেয়ে খাড়া দিকের জন্য η বড় হলে loss লাফিয়ে বিস্ফোরিত হয়, আর তখন অনেক সময় NaN আসে। দ্বিতীয়, শুরুর দিকে weight এলোমেলো আর gradient বড়, শেষে ছোট আর কোলাহলপূর্ণ; একটি স্থির η দুই অবস্থার জন্য ঠিক হয় না। তৃতীয়, ধ্রুব η-তে mini-batch-এর noise শেষ পর্যন্ত থাকে, তাই loss সর্বনিম্ন বিন্দুর চারপাশে কাঁপে (মেশিন লার্নিং কোর্সের পর্ব ১৩-এর “মেঝে”)। অভিজ্ঞ অনুশীলনকারীদের মধ্যে একটি প্রচলিত কথা আছে: hyperparameter-এর মধ্যে যদি শুধু একটি tune করার সময় থাকে, learning rate করুন। এটি কোনো প্রমাণিত নিয়ম নয়, অভিজ্ঞতার কথা; তবে এই পর্বের run-গুলোও একই দিকে ইঙ্গিত দেয়।

লক্ষণসম্ভাব্য কারণপ্রথম পদক্ষেপ
প্রথম কয়েক ধাপেই loss বাড়ে বা NaNη অনেক বড়η ৩ থেকে ১০ গুণ কমান, warmup যোগ করুন
Loss খুব ধীরে, প্রায় সরলরেখায় নামেη অনেক ছোটLR range test (অংশ ৪)
দ্রুত নেমে একটি স্তরে কাঁপতে থাকেধ্রুব η-এর noise মেঝেDecay schedule (cosine বা step)
Val loss হঠাৎ লাফায়, তারপর ফেরেশুরুতে বড় ধাপে অস্থিরতাWarmup, বা gradient clipping (পর্ব ১৭)

২ · Schedule-এর পাঁচ আকার — সূত্র আর মান

ধরুন মোট T ধাপ, সর্বোচ্চ learning rate ηmax, আর t হলো বর্তমান ধাপ (০ থেকে গোনা)। প্রচলিত পাঁচটি schedule:

  • Constant: ηt = ηmax. সরল, কিন্তু শেষে noise থেকে যায়।
  • Step decay: প্রতি s ধাপে γ গুণ, ηt = ηmax · γ⌊t/s⌋. ⌊ ⌋ মানে নিচের পূর্ণসংখ্যা। ResNet-যুগের ছবি-classification-এ প্রচলিত ছিল (যেমন ৩০ epoch পরপর ১০ ভাগের ১)।
  • Cosine: ηt = ηmin + ½(ηmax − ηmin)(1 + cos(πt/T)). শুরুতে প্রায় সমতল, মাঝে দ্রুত নামে, শেষে আবার নরমভাবে ηmin-এ থামে।
  • Warmup + cosine: প্রথম W ধাপে রৈখিক ওঠা, ηt = ηmax(t + 1)/W; তারপর বাকি T − W ধাপে cosine. Transformer আর বড় ভাষা model-এ এটি প্রায় মানক অভ্যাস।
  • One-cycle: Leslie Smith-এর প্রস্তাব (২০১৮ সালের দিকে)। ছোট মান (ηmax/২৫) থেকে প্রথম ৩০% ধাপে ηmax-এ ওঠা, তারপর প্রায় শূন্যে নামা; momentum উল্টো দিকে চলে (০.৯৫ → ০.৮৫ → ০.৯৫)।

নিচের ব্লক পাঁচটি schedule একই ছবিতে আঁকে আর কয়েকটি ধাপে মান ছাপে। One-cycle-এর সূত্র PyTorch-এর OneCycleLR-এর default-এর মতো লেখা, যাতে অংশ ৫-এ মিলিয়ে দেখা যায়।

import numpy as np, pandas as pd, matplotlib.pyplot as plt

T, peak, warm = 1000, 0.1, 100            # মোট ধাপ, সর্বোচ্চ lr, warmup ধাপ
t = np.arange(T)

def ramp(a, b, f):                        # f = 0-তে a, f = 1-এ b; মাঝে cosine বাঁক
    return b + (a - b) * 0.5 * (1 + np.cos(np.pi * np.clip(f, 0, 1)))

up = 0.3 * T - 1                          # one-cycle: 30% ধাপে ওঠা (PyTorch-এর default)
S = {
    "constant": np.full(T, peak),
    "step": peak * 0.1 ** (t // 400),     # প্রতি 400 ধাপে 10 ভাগের 1
    "cosine": ramp(peak, 0.0, t / T),
    "warmup+cosine": np.where(t < warm, peak * (t + 1) / warm,
                              ramp(peak, 0.0, (t - warm) / (T - warm))),
    "one-cycle": np.where(t <= up, ramp(peak / 25, peak, t / up),
                          ramp(peak, peak / 25 / 1e4, (t - up) / (T - 1 - up))),
}
pd.set_option("display.width", 70)
rows = [0, 50, 99, 100, 299, 550, 999]
print(pd.DataFrame({k: v[rows] for k, v in S.items()}, index=rows).round(4))

fig, ax = plt.subplots(figsize=(7, 3.4))
for k, v in S.items():
    ax.plot(t, v, label=k)
ax.set_xlabel("step"); ax.set_ylabel("learning rate")
ax.set_title("Five learning-rate schedules, peak 0.1, 1000 steps")
ax.legend(fontsize=8)
plt.show()

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

     constant   step  cosine  warmup+cosine  one-cycle
0         0.1  0.100  0.1000         0.0010     0.0040
50        0.1  0.100  0.0994         0.0510     0.0105
99        0.1  0.100  0.0976         0.1000     0.0277
100       0.1  0.100  0.0976         0.1000     0.0281
299       0.1  0.100  0.0795         0.0884     0.1000
550       0.1  0.010  0.0422         0.0500     0.0715
999       0.1  0.001  0.0000         0.0000     0.0000

সারিগুলো ধাপ, কলামগুলো schedule. Warmup + cosine ধাপ ০-তে মাত্র ০.০০১, ধাপ ৯৯-এ পূর্ণ ০.১, আর ধাপ ৫৫০-এ (cosine অংশের ঠিক মাঝে) অর্ধেক ০.০৫. Cosine-এর ধাপ ৫০-এ মান প্রায় বদলায়নি (০.০৯৯৪): বাঁকের শুরু সমতল। Step-এর ধাপ ৫৫০-এ ০.০১ আর ৯৯৯-এ ০.০০১, হঠাৎ লাফে নামে। One-cycle ধাপ ২৯৯-এ শিখরে। ছবিতে দেখুন, শেষের দিকে constant ছাড়া সবাই প্রায় শূন্যে পৌঁছায়: এখানেই noise কমে, আর loss নিচে থিতু হয়।

পরীক্ষায় প্রায়ই আসে: নির্দিষ্ট ধাপে schedule-এর মান। একই warmup + cosine (T = ১০০০, W = ১০০, ηmax = ০.১) পাঁচটি ধাপে হাতে বের করুন:

η_t = 0.1·(t + 1)/100 যদি t < 100; নইলে 0.1 · ½(1 + cos(π(t − 100)/900))
ধাপ t
অংশ
হিসাব
η_t
  1. ধাপ ০. Warmup অংশ, তাই η = ০.১ × (০ + ১)/১০০ = ০.০০১. সর্বোচ্চ মানের মাত্র ১০০ ভাগের ১ দিয়ে শুরু।
  2. ধাপ ৪৯. এখনো warmup: ০.১ × ৫০/১০০ = ০.০৫. Warmup-এর অর্ধেক পথে learning rate-ও অর্ধেক।
  3. ধাপ ৩২৫. Cosine অংশ। ভগ্নাংশ (৩২৫ − ১০০)/৯০০ = ০.২৫, কোণ π × ০.২৫ = ৪৫°. ½(১ + ০.৭০৭১) = ০.৮৫৩৬, তাই η ≈ ০.০৮৫৪. প্রথম চতুর্থাংশে মাত্র ১৫% কমেছে।
  4. ধাপ ৫৫০. ভগ্নাংশ ০.৫, কোণ ৯০°, cos ৯০° = ০; η = ০.১ × ০.৫ = ০.০৫. Cosine অংশের ঠিক মাঝে সবসময় অর্ধেক।
  5. ধাপ ৯৯৯. ভগ্নাংশ ৮৯৯/৯০০, কোণ প্রায় ১৮০°, cos প্রায় −১; η প্রায় ৩ × ১০−৭, কার্যত শূন্য। শেষ ধাপগুলোতে weight প্রায় নড়ে না।

৩ · Warmup — Adam কেন শুরুতে হোঁচট খায়

Warmup-এর দুটি সাধারণ যুক্তি আছে। একটি সব optimizer-এর জন্য: শুরুতে weight এলোমেলো, gradient বড় আর দিক অনিশ্চিত, তাই পূর্ণ η-তে বড় লাফ network-কে খারাপ অঞ্চলে ঠেলে দিতে পারে। Goyal ও সহকর্মীরা (২০১৭) বিশাল batch-এ ImageNet train করতে কয়েক epoch-এর ধীর warmup ব্যবহার করেন, আর জানান তা ছাড়া শুরুর অস্থিরতা সামলানো কঠিন ছিল।

দ্বিতীয় যুক্তি Adam-এর নিজস্ব। Adam-এর ধাপ η · m̂/√v̂, যেখানে v̂ gradient-এর বর্গের চলমান গড় (bias correction-এর পরে)। প্রথম কয়েক ধাপে v̂ মাত্র এক-দুটি নমুনা থেকে হিসাব, তাই খুব অনিশ্চিত। প্রথম ধাপে m̂ = g আর √v̂ = |g|, তাই প্রতিটি parameter ঠিক ±η সরে, gradient যত ছোটই হোক। Liu ও সহকর্মীরা (RAdam, ২০১৯ সালের দিকে) যুক্তি দেন যে শুরুতে এই adaptive learning rate-এর variance বেশি, আর warmup সেটি নিয়ন্ত্রণ করে; এই ব্যাখ্যা নিয়ে আলোচনা চলছে, একমাত্র কারণ বলে ধরে নেওয়া ঠিক নয়। নিচে তিনটি parameter-এ প্রথম তিন ধাপ হাতে দেখুন (η = ০.০০১, β₁ = ০.৯, β₂ = ০.৯৯৯)।

তিনটি parameter: ছোট, মাঝারি আর বড় gradient · η = 0.001
ধাপ
gradient g
m̂/√v̂
Adam-এর ধাপ
SGD-এর ধাপ η·g
  1. ধাপ ১. Bias correction-এর পরে m̂ = g আর √v̂ = |g|, তাই অনুপাত তিনটিতেই ১. Gradient ০.০০১ হোক বা ২০, প্রতিটি parameter ঠিক ০.০০১ সরে। SGD-তে ছোট gradient-এর parameter সরত মাত্র ১০−৬: Adam তাকে ১০০০ গুণ বড় ধাপ দিল, মাত্র একটি নমুনা দেখে।
  2. ধাপ ২. প্রথম parameter-এর gradient চিহ্ন বদলেছে, m̂ প্রায় শূন্যে (−০.০৫৩), তাই সে প্রায় থামল। বাকি দুটি এখনো প্রায় পূর্ণ η নিচ্ছে। √v̂ এখনো মাত্র দুটি বর্গের গড়।
  3. ধাপ ৩. অনুপাত দ্রুত বদলাচ্ছে (০.৩৮২, ০.৪২০, ০.৯৮৭). অল্প নমুনার v̂ অনিশ্চিত, তাই প্রতিটি ধাপের মাপও অস্থির। Gradient বিশাল বা সামান্য, যেকোনো parameter পূর্ণ η পর্যন্ত লাফাতে পারে।
  4. Warmup. W = ১০০ হলে প্রথম তিন ধাপে η = ১০−৫, ২ × ১০−৫, ৩ × ১০−৫. অনুপাত একই থাকে, কিন্তু ধাপ ১০০ থেকে ৩৩ গুণ ছোট। v̂ যতক্ষণে অনেক নমুনার গড় হয়ে স্থির হয়, ততক্ষণে η পূর্ণ মানে পৌঁছায়।
  5. সারকথা. Adam শুরুতে কম তথ্যে প্রায় পূর্ণ মাপের ধাপ দেয়। Warmup সেই অনিশ্চিত সময়ে ধাপ ছোট রাখে। এ কারণেই Transformer-এ AdamW প্রায় সবসময় warmup-এর সঙ্গে আসে।

Transformer-এর ক্ষেত্রে আরেকটি কারণ আছে। ২০১৭ সালের মূল Transformer প্রবন্ধে (Vaswani ও সহকর্মী) কয়েক হাজার ধাপের warmup, তারপর ধাপের বর্গমূলের বিপরীত অনুপাতে decay ব্যবহার হয়েছিল। পরে Xiong ও সহকর্মীরা (২০২০) দেখান, LayerNorm residual-এর পরে বসানো (post-LN) Transformer-এ শুরুতে শেষ layer-গুলোর gradient বড় থাকে, তাই warmup ছাড়া training অস্থির হয়; LayerNorm আগে বসালে (pre-LN) warmup-এর ওপর নির্ভরতা কমে। Normalization-এর বিস্তারিত পর্ব ১৪ (Weight initialization আর normalization)-এ, আর Transformer ব্লক পরের মডিউলগুলোতে।

৪ · LR range test — কয়েকশো ধাপে learning rate খোঁজা

সেরা η খুঁজতে পুরো training বারবার চালানো ব্যয়বহুল। Leslie Smith (“Cyclical Learning Rates for Training Neural Networks”, ২০১৫ সালে arXiv-এ, WACV ২০১৭) একটি সস্তা পথ জনপ্রিয় করেন, যা এখন LR range test নামে পরিচিত: খুব ছোট η থেকে শুরু করে প্রতি mini-batch-এ η একটু গুণ করে বাড়ান, আর loss লিখে রাখুন। ছোট η-তে loss প্রায় নড়ে না; একটি পরিসরে দ্রুত নামে; তারপর η খুব বড় হলে loss বিস্ফোরিত হয়। fastai library-র “lr_find” এই ধারণার একটি পরিচিত রূপ।

নিচের ব্লকে বাজারঘরের তিনটি customer segment (৪টি feature, ৬০০ জন গ্রাহক) আর ৪ → ৩২ → ৩-এর ছোট MLP. η ১০−৪ থেকে ১০ পর্যন্ত ১০০ ধাপে জ্যামিতিক হারে বাড়ে; loss-এর চলমান গড় নেওয়া হয়, কারণ একক batch-এর loss খুব কোলাহলপূর্ণ।

import numpy as np, matplotlib.pyplot as plt

rng = np.random.default_rng(42)            # বাজারঘর: ৩টি customer segment, ৪টি feature
y = rng.integers(0, 3, 600)
X = rng.normal(0, 1, (600, 4)) + 1.5 * np.eye(3, 4)[y]
W1 = rng.normal(0, 0.5, (4, 32)); W2 = rng.normal(0, 0.25, (32, 3))

lrs = np.geomspace(1e-4, 10, 100)           # প্রতি batch-এ lr একটু বাড়ানো
losses, avg = [], 0.0
for i, lr in enumerate(lrs):
    b = rng.integers(0, 600, 32)
    H = np.maximum(X[b] @ W1, 0); Z = H @ W2
    Z -= Z.max(1, keepdims=True); P = np.exp(Z) / np.exp(Z).sum(1, keepdims=True)
    loss = -np.log(P[np.arange(32), y[b]] + 1e-12).mean()
    avg = 0.9 * avg + 0.1 * loss            # চলমান গড়, bias সংশোধনসহ
    losses.append(avg / (1 - 0.9 ** (i + 1)))
    if losses[-1] > 4 * min(losses):        # loss বিস্ফোরিত: থামুন
        break
    G = P.copy(); G[np.arange(32), y[b]] -= 1; G /= 32
    dH = (G @ W2.T) * (H > 0)
    W2 -= lr * H.T @ G; W1 -= lr * X[b].T @ dH

L = np.array(losses); n = len(L)
best = lrs[L.argmin()]
k = L.argmin(); g = np.gradient(L[:k + 1], np.log(lrs[:k + 1]))
steep = lrs[10 + g[10:].argmin()]            # প্রথম ১০ ধাপ বাদ (গড় তখনো অস্থির)
print(f"থামল lr {lrs[n - 1]:.3g}-এ ({n} ধাপ) | সবচেয়ে কম loss {L.min():.3f}, lr {best:.3g}-এ")
print(f"সবচেয়ে খাড়া নামা lr {steep:.3g} | best/10 = {best / 10:.3g}")
fig, ax = plt.subplots(figsize=(6.5, 3.2))
ax.plot(lrs[:n], L); ax.set_xscale("log")
ax.axvline(best, ls="--", c="gray"); ax.axvline(steep, ls=":", c="red")
ax.set_xlabel("learning rate (log scale)"); ax.set_ylabel("smoothed loss")
ax.set_title("LR range test: loss vs learning rate")
plt.show()

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

থামল lr 6.28-এ (96 ধাপ) | সবচেয়ে কম loss 0.568, lr 1.56-এ
সবচেয়ে খাড়া নামা lr 0.305 | best/10 = 0.156

η ১০−৪ থেকে প্রায় ০.০১ পর্যন্ত loss প্রায় সমতল: শেখা হচ্ছে না বললেই চলে। প্রায় ০.১ থেকে ১-এর মধ্যে loss দ্রুত নামে, ১.৫৬-এ সবচেয়ে কম, তারপর ৬.২৮-এ বিস্ফোরিত, তাই থামা। কোন η বাছবেন? দুটি প্রচলিত নিয়ম: সবচেয়ে কম loss-এর η-এর ১০ ভাগের ১ (এখানে ০.১৫৬), অথবা যেখানে loss সবচেয়ে খাড়াভাবে নামছে (লাল দাগ, ০.৩০৫)। দুটিই মোটামুটি একই পরিসর দেয়। সবচেয়ে কম loss-এর η নিজে বাছবেন না: ওখানে আপনি বিস্ফোরণের কিনারায়, আর পুরো training-এ weight বদলালে কিনারা সরে।

একই পরীক্ষা PyTorch-এ MNIST-এর ২০,০০০টি ছবিতে (৭৮৪ → ২৫৬ → ১২৮ → ১০-এর MLP, প্রথম layer-এর পরে LayerNorm, batch ১২৮), দুই optimizer-এর জন্য। সত্যিকারের run:

~/bazarghar-dl$ uv run python rangetest.py
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
SGD-momentum  থামল lr 1.2 | সবচেয়ে কম loss 0.431 lr 0.24-এ | তার 1/10: 0.024
   lr 0.0001: smoothed loss 2.325
   lr 0.001: smoothed loss 2.260
   lr 0.01: smoothed loss 1.684
   lr 0.1: smoothed loss 0.479
   lr 1: smoothed loss 0.941
AdamW         থামল lr 0.31 | সবচেয়ে কম loss 0.470 lr 0.048-এ | তার 1/10: 0.0048
   lr 0.0001: smoothed loss 2.204
   lr 0.001: smoothed loss 1.300
   lr 0.01: smoothed loss 0.569
   lr 0.1: smoothed loss 0.520

দুই optimizer-এর উপযুক্ত পরিসর প্রায় ৪ থেকে ৫ গুণ আলাদা: SGD-momentum-এর বিস্ফোরণ ১.২-এ, AdamW-এর ০.৩১-এ। তাই “lr = 0.001” কোনো সর্বজনীন সংখ্যা নয়; প্রতিটি optimizer আর model-এর নিজের পরিসর আছে। ১/১০ নিয়মে অংশ ৮-এর তুলনায় SGD-momentum-এর জন্য ০.০২৫ আর AdamW-এর জন্য ০.০০৫ নেওয়া হয়েছে। সীমাবদ্ধতা মনে রাখুন: range test মাত্র কয়েকশো ধাপ দেখে, তাই এটি শুরুর পরিসর বলে, শেষের সেরা মান নয়; আর batch size বা model বদলালে আবার চালাতে হয়।

৫ · PyTorch-এ scheduler — ক্রম, প্রতি ধাপ না প্রতি epoch, আর plateau

torch.optim.lr_scheduler-এ প্রতিটি scheduler একটি optimizer নেয় আর তার param_groups-এর lr বদলায়। Warmup + cosine লেখার সবচেয়ে সরল পথ LambdaLR: একটি function দেয় ηmax-এর কত ভগ্নাংশ। আরেকটি পথ SequentialLR দিয়ে LinearLR (warmup) আর CosineAnnealingLR জোড়া লাগানো।

def warmup_cosine(total, warm):
    def f(step):                      # LambdaLR-এর গুণক, peak lr-এর ভগ্নাংশ
        if step < warm:
            return (step + 1) / warm
        return 0.5 * (1 + math.cos(math.pi * (step - warm) / (total - warm)))
    return f

opt = torch.optim.AdamW(model.parameters(), lr=5e-3, weight_decay=0.01)
sch = torch.optim.lr_scheduler.LambdaLR(opt, warmup_cosine(TOTAL, WARM))
for xb, yb in batches:
    loss = F.cross_entropy(model(xb), yb)
    opt.zero_grad(); loss.backward(); opt.step()
    sch.step()                        # প্রতি ধাপে, optimizer.step()-এর পরে

পাতার NumPy সূত্র আর PyTorch-এর scheduler কি একই মান দেয়? একই সাতটি ধাপে (০, ৫০, ৯৯, ১০০, ২৯৯, ৫৫০, ৯৯৯) সত্যিকারের run:

~/bazarghar-dl$ uv run python extras.py sched
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
step           0.1000 0.1000 0.1000 0.1000 0.1000 0.0100 0.0010
cosine         0.1000 0.0994 0.0976 0.0976 0.0795 0.0422 0.0000
warmup+cosine  0.0010 0.0510 0.1000 0.1000 0.0884 0.0500 0.0000
one-cycle      0.0040 0.0105 0.0277 0.0281 0.1000 0.0715 0.0000
one-cycle momentum শুরুতে / শিখরে: 0.95 / 0.85

চারটি সারিই অংশ ২-এর টেবিলের সঙ্গে চার দশমিক ঘর পর্যন্ত মেলে। StepLR(step_size=400, gamma=0.1), CosineAnnealingLR(T_max=1000), আর OneCycleLR(max_lr=0.1, total_steps=1000)-এর default (শুরু max_lr/২৫, ৩০% ধাপে শিখর) ব্যবহার হয়েছে। শেষ লাইন দেখায় OneCycleLR momentum-ও বদলায়: learning rate শিখরে উঠলে momentum ০.৯৫ থেকে ০.৮৫-এ নামে।

প্রতি ধাপ না প্রতি epoch

Scheduler কোনো ঘড়ি জানে না; সে শুধু গোনে কতবার step() ডাকা হলো। T_max বা total_steps যে এককে দিয়েছেন (ধাপ বা epoch), ঠিক সেই হারে ডাকতে হবে। OneCycleLR আর ধাপ-ভিত্তিক warmup প্রতি batch-এ; epoch-ভিত্তিক StepLR প্রতি epoch-এ। ভুল হারে ডাকলে cosine কয়েক epoch-এই শেষ হয়ে যায়, অথবা কখনো নামেই না।

পর্ব ১০-এ বলা নিয়মটি আবার: scheduler.step() optimizer.step()-এর পরে। উল্টো ক্রমে কী হয়, StepLR (প্রতি epoch-এ অর্ধেক, শুরু ০.১) দিয়ে সত্যিকারের run:

~/bazarghar-dl$ uv run python extras.py order
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
epoch 0: optimizer যে lr-এ চলল = 0.05
epoch 1: optimizer যে lr-এ চলল = 0.025
epoch 2: optimizer যে lr-এ চলল = 0.0125
warning: Detected call of `lr_scheduler.step()` before `optimizer.step()`. In PyTorch 1.1.0 and later, you should call them in  …

প্রথম epoch-এর optimizer ০.১-এ চলার কথা ছিল, কিন্তু scheduler আগে ডাকায় চলল ০.০৫-এ: schedule-এর প্রথম মান বাদ পড়ল, পুরো schedule এক ধাপ এগিয়ে গেল। এখানে ক্ষতি ছোট, কিন্তু warmup-এ প্রথম মানটিই সবচেয়ে ছোট আর সবচেয়ে জরুরি। PyTorch সতর্ক করে; warning উপেক্ষা করবেন না।

ReduceLROnPlateau অন্যরকম: সে ধাপ গোনে না, val loss দেখে। নির্দিষ্ট সংখ্যক epoch (patience) উন্নতি না হলে η-কে একটি গুণক দিয়ে কমায়। তাই step()-এ metric দিতে হয়: sch.step(val_loss). একটি বানানো val loss-এর ধারায় (factor ০.৫, patience ২, threshold ১০−৩) সত্যিকারের run:

~/bazarghar-dl$ uv run python extras.py plateau
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
epoch 0: val loss 0.900 | bad epochs 0 | lr পরে 0.1000
epoch 1: val loss 0.700 | bad epochs 0 | lr পরে 0.1000
epoch 2: val loss 0.650 | bad epochs 0 | lr পরে 0.1000
epoch 3: val loss 0.660 | bad epochs 1 | lr পরে 0.1000
epoch 4: val loss 0.650 | bad epochs 2 | lr পরে 0.1000
epoch 5: val loss 0.651 | bad epochs 0 | lr পরে 0.0500
epoch 6: val loss 0.600 | bad epochs 0 | lr পরে 0.0500
epoch 7: val loss 0.610 | bad epochs 1 | lr পরে 0.0500
epoch 8: val loss 0.620 | bad epochs 2 | lr পরে 0.0500
epoch 9: val loss 0.630 | bad epochs 0 | lr পরে 0.0250

তিনটি সূক্ষ্ম বিষয় দেখুন। Epoch ৪-এ val loss আবার ০.৬৫০, সেরার সমান, কিন্তু উন্নতি গোনা হয়নি: threshold-এর আপেক্ষিক নিয়মে সেরার চেয়ে অন্তত ০.১% কম হতে হয়। Epoch ৫-এ খারাপ epoch তিনটি হলো, patience ২ পেরোল, তাই η অর্ধেক আর গোনা শূন্যে ফিরল (যে কারণে লাইনে ০ দেখাচ্ছে)। আর epoch ৬-এর ০.৬০০ নতুন সেরা হওয়ায় পরের কমানো আসে আবার তিন খারাপ epoch পরে। এই scheduler সুবিধাজনক যখন মোট training-এর দৈর্ঘ্য আগে জানা নেই; অসুবিধা, val noise-এ সিদ্ধান্ত ঝাঁকুনি খায়, আর cosine-এর মতো আগে থেকে জানা পথ থাকে না।

৬ · Adam + L2 বনাম AdamW, আর parameter group

মেশিন লার্নিং কোর্সের পর্ব ১৪-এ derive করা হয়েছে: SGD-তে L2 শাস্তি (loss-এ ½λ‖w‖²) আর weight decay (প্রতি ধাপে w ← (1 − ηλ)w) একই জিনিস, কিন্তু Adam-এ নয়। Adam + L2-তে λw gradient-এর ভেতরে ঢোকে, তারপর √v̂ দিয়ে ভাগ হয়। AdamW (Loshchilov আর Hutter, ২০১৭ সালে arXiv-এ, ICLR ২০১৯) decay-কে আলাদা করে: আগে w ← (1 − ηλ)w, তারপর সাধারণ Adam ধাপ। নিচের ব্লক পার্থক্যটি দুইভাবে দেখায়: এক ধাপের হিসাব, আর দুই ধরনের weight-এর ৩০০ ধাপ।

import numpy as np

def run(w, grads, lr=0.01, l2=0.0, wd=0.0, b1=0.9, b2=0.999, eps=1e-8):
    m = np.zeros_like(w); v = np.zeros_like(w)
    for t, g in enumerate(grads, start=1):
        g = g + l2 * w                       # Adam + L2: শাস্তি gradient-এর ভেতরে
        w = w * (1 - lr * wd)                # AdamW: ওজন আলাদাভাবে ছোট করা
        m = b1 * m + (1 - b1) * g; v = b2 * v + (1 - b2) * g * g
        mh = m / (1 - b1 ** t); vh = v / (1 - b2 ** t)
        w = w - lr * mh / (np.sqrt(vh) + eps)
    return w

# ১) এক ধাপ, হাতে মেলানোর মতো: w = 1, data gradient 0.5, lr 0.01, λ 0.1
one = [np.array([0.5])]
for name, kw in [("Adam", {}), ("Adam+L2", {"l2": 0.1}), ("AdamW", {"wd": 0.1})]:
    print(f"{name:8s} এক ধাপ পরে w = {run(np.array([1.0]), one, **kw)[0]:.4f}")

# ২) দুই ধরনের weight, প্রতিটির ২০০০টি কপি; gradient শুধু noise (গড় 0)
rng = np.random.default_rng(42)
scale = np.array([[10.0], [0.01]])            # সারি ০: বড় gradient, সারি ১: ছোট
G = [scale * rng.normal(0, 1, (2, 2000)) for _ in range(300)]
w0 = np.ones((2, 2000))
print("\n৩০০ ধাপ পরে গড় w   বড়-gradient   ছোট-gradient")
for name, kw in [("Adam", {}), ("Adam+L2", {"l2": 0.1}), ("AdamW", {"wd": 0.1})]:
    wf = run(w0, G, **kw).mean(axis=1)
    print(f"{name:8s}             {wf[0]:.3f}         {wf[1]:.3f}")
print(f"শুধু decay হলে (1 - 0.01*0.1)^300 = {(1 - 0.001) ** 300:.3f}")

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

Adam     এক ধাপ পরে w = 0.9900
Adam+L2  এক ধাপ পরে w = 0.9900
AdamW    এক ধাপ পরে w = 0.9890

৩০০ ধাপ পরে গড় w   বড়-gradient   ছোট-gradient
Adam                 0.996         1.003
Adam+L2              0.967         0.000
AdamW                0.737         0.743
শুধু decay হলে (1 - 0.01*0.1)^300 = 0.741

এক ধাপ: Adam + L2-তে gradient হলো ০.৫ + ০.১ × ১ = ০.৬, কিন্তু প্রথম ধাপে m̂/√v̂ = ০.৬/০.৬ = ১, তাই ধাপ ঠিক ০.০১, শাস্তি ছাড়া Adam-এর মতোই: w = ০.৯৯০০. শাস্তির কোনো প্রভাবই নেই! AdamW আগে w-কে (১ − ০.০১ × ০.১) = ০.৯৯৯ গুণ করে, তারপর ০.০১ সরায়: ০.৯৯৯ − ০.০১ = ০.৯৮৯০. ৩০০ ধাপ: gradient শুধু noise, গড় শূন্য, তাই শাস্তি না থাকলে w প্রায় ১-এ থাকে। Adam + L2-তে বড়-gradient-এর weight (noise ১০) প্রায় ছোট হয়নি (০.৯৬৭), কারণ λw-কে বড় √v̂ দিয়ে ভাগ করা হয়েছে; ছোট-gradient-এর weight (noise ০.০১) একেবারে শূন্যে, কারণ সেখানে শাস্তিই পুরো ধাপ ঠিক করছে। AdamW-এ দুই ধরনই প্রায় ০.৭৪, যা শুধু decay-এর হিসাব (১ − ০.০০১)৩০০ = ০.৭৪১-এর সঙ্গে মেলে। অর্থাৎ AdamW-এ λ-এর অর্থ সব weight-এর জন্য এক।

গভীর network-এ এর প্রভাব কেমন? একই MNIST MLP, warmup + cosine, lr ০.০০২, ৬ epoch, তিন seed; Adam-কে weight_decay=0.05 দিলে PyTorch তা gradient-এ যোগ করে (L2), আর AdamW আলাদা decay. সত্যিকারের run (CPU-তে প্রায় ২০ সেকেন্ড):

~/bazarghar-dl$ uv run python adam_l2.py
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
Adam (wd=0)                    test acc 97.30 ± 0.10% | weight norm 18.5
Adam + L2 (weight_decay=0.05)  test acc 92.42 ± 0.11% | weight norm 4.0
AdamW (weight_decay=0.05)      test acc 97.25 ± 0.06% | weight norm 17.9

একই সংখ্যা ০.০৫, কিন্তু অর্থ সম্পূর্ণ আলাদা। Adam + L2-তে weight-এর norm ১৮.৫ থেকে ৪.০-এ নেমে গেছে আর accuracy প্রায় ৫ শতাংশ-বিন্দু কমেছে: অনেক weight-এর gradient ছোট, তাই শাস্তি সেখানে প্রায় পুরো ধাপ দখল করেছে। AdamW-এ একই λ মৃদু: norm সামান্য কম, accuracy প্রায় একই। এই ছোট, ৬ epoch-এর কাজে decay-এর উপকার চোখে পড়েনি; regularization-এর লাভ মাপা হবে পর্ব ১৬ (Deep learning-এ regularization)-এ, ইচ্ছা করে overfit করা setup-এ। শিক্ষা: Adam আর AdamW-এর মধ্যে λ বদলানো যায় না, আর অন্য কারও AdamW-এর λ Adam-এ বসালে ফল ভেঙে যেতে পারে।

আরেকটি প্রচলিত অভ্যাস: bias আর normalization layer-এর weight (LayerNorm-এর γ) decay থেকে বাদ রাখা। যুক্তি: এগুলো সংখ্যায় খুব কম, overfitting-এ ভূমিকা সামান্য, আর norm-এর γ-কে শূন্যের দিকে টানলে layer-এর output-এর মাপই বদলে যায়। এর জন্য optimizer-কে parameter group-এর তালিকা দিতে হয়, প্রতিটির নিজের weight_decay:

decay, no_decay = [], []
for name, p in model.named_parameters():
    (decay if p.ndim >= 2 else no_decay).append(p)   # matrix-এ decay; bias আর norm-এর weight-এ নয়
opt = torch.optim.AdamW([{"params": decay, "weight_decay": 0.1},
                         {"params": no_decay, "weight_decay": 0.0}], lr=0.01)
for p in model.parameters(): p.grad = torch.zeros_like(p)        # gradient শূন্য: শুধু decay দেখা
opt.step()

সত্যিকারের run:

~/bazarghar-dl$ uv run python groups.py
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
0.weight   (256, 784)  decay
0.bias     (256,)      no decay
1.weight   (256,)      no decay
1.bias     (256,)      no decay
3.weight   (128, 256)  decay
3.bias     (128,)      no decay
5.weight   (10, 128)   decay
5.bias     (10,)       no decay
groups: [(3, 0.1), (5, 0.0)] | parameter: 234752 + 906
weight[0,0]: -0.00026739 -> -0.00026712 (অনুপাত 0.999000)
bias[0]:     -0.011718 -> -0.011718
LayerNorm.weight[0]: 1.000000 -> 1.000000

“ndim ≥ ২” নিয়মটি সরল কিন্তু কার্যকর: তিনটি Linear-এর weight matrix decay group-এ, আর তিনটি bias আর LayerNorm-এর দুটি vector অন্য group-এ। ২,৩৪,৭৫২টি parameter decay পায়, ৯০৬টি পায় না। Gradient শূন্য রেখে একটি ধাপ নিলে শুধু decay কাজ করে: weight ঠিক (১ − ০.০১ × ০.১) = ০.৯৯৯ গুণ হলো, bias আর LayerNorm-এর γ বদলায়নি। Embedding-এর মতো ২-মাত্রার কিন্তু “বিশেষ” parameter থাকলে নাম দিয়ে আলাদা করুন; এ নিয়ে সম্প্রদায়ে একমত নেই। Parameter group দিয়ে আলাদা layer-এ আলাদা learning rate-ও দেওয়া যায়, যা fine-tuning-এ কাজে লাগে; সেটি পর্ব ২৩ (Transfer learning)-এর বিষয়।

৭ · Gradient accumulation, effective batch আর learning rate

বড় batch memory-তে না ধরলে gradient accumulation: কয়েকটি ছোট micro-batch-এর backward চালিয়ে gradient জমতে দিন, তারপর একবার step(). তখন effective batch = micro-batch × জমানোর সংখ্যা (× GPU-র সংখ্যা)। একটি জরুরি খুঁটিনাটি: CrossEntropyLoss default-ভাবে batch-এর গড় দেয়, তাই k টি micro-batch-এর loss যোগ করলে k গুণ বড় gradient আসে। প্রতিটির loss k দিয়ে ভাগ করুন।

def grads(model, k, divide=True):            # k টি micro-batch-এ ভাগ করে backward
    model.zero_grad()
    for xm, ym in zip(xb.chunk(k), yb.chunk(k)):
        loss = nn.functional.cross_entropy(model(xm), ym)
        (loss / k if divide else loss).backward()
    return torch.cat([p.grad.flatten() for p in model.parameters()])

৬৪টি MNIST ছবি, একবারে বনাম ১৬-এর চারটি micro-batch-এ। সত্যিকারের run:

~/bazarghar-dl$ uv run python accum.py
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
১ x 64 বনাম ৪ x 16 (loss/4): সর্বোচ্চ পার্থক্য 1.49e-08, norm 1.4495 বনাম 1.4495
৪ x 16, ভাগ ছাড়া: norm 5.7980 (অনুপাত 4.000)
এক SGD ধাপের পরে parameter-এর সর্বোচ্চ পার্থক্য: 7.45e-09
BatchNorm থাকলে: সর্বোচ্চ পার্থক্য 3.94e-02, আপেক্ষিক 0.360

৪ দিয়ে ভাগ করলে দুই পথের gradient-এর পার্থক্য ১.৪৯ × ১০−৮: float32-এ যোগের ক্রম আলাদা হওয়ার ছাপ মাত্র, গাণিতিকভাবে একই। ভাগ না করলে gradient ঠিক ৪ গুণ, যা SGD-তে learning rate ৪ গুণ করার সমান (Adam-এ অনুপাতের কারণে প্রভাব কম চোখে পড়ে, কিন্তু epsilon আর weight decay-এর ভারসাম্য বদলায়)। শেষ লাইনটি সতর্কবাণী: BatchNorm প্রতিটি micro-batch-এর নিজের গড় দিয়ে normalize করে, তাই ১৬-এর batch-এর পরিসংখ্যান ৬৪-এর মতো নয়, আর gradient-এ ৩৬% আপেক্ষিক পার্থক্য। LayerNorm প্রতিটি উদাহরণে আলাদা কাজ করে বলে এই সমস্যা নেই। শেষ micro-batch ছোট হলে ভাগ উদাহরণের সংখ্যা দিয়ে করাই নিখুঁত।

Batch বড় হলে learning rate কতটা বাড়াবেন? Goyal ও সহকর্মীরা (২০১৭) SGD-তে linear scaling rule জানান: batch k গুণ হলে η-ও k গুণ, সঙ্গে warmup; তাঁদের ImageNet-এর পরীক্ষায় একটি বড় সীমা পর্যন্ত (প্রায় ৮ হাজারের batch) নির্ভুলতা ধরে রাখা গিয়েছিল। এটি একটি অভিজ্ঞতালব্ধ নিয়ম, প্রমাণ নয়; খুব বড় batch-এ ভেঙে পড়ে। Adam-জাতীয় optimizer-এ অনেকে বর্গমূলের নিয়ম (batch k গুণ হলে η √k গুণ) ব্যবহার করেন; এ নিয়ে একমত নেই, তাই নতুন batch-এ range test আবার চালানোই নিরাপদ।

শুরু: batch 128-এ η = 0.1 ভালো কাজ করে
সেটআপ
effective batch
optimizer.step() / epoch
η
  1. Effective batch. GPU-তে ৩২টির বেশি ছবি ধরে না, তাই ৩২-এর চারটি micro-batch জমিয়ে একটি ধাপ: effective batch ৩২ × ৪ = ১২৮, যার জন্য η = ০.১ আগে থেকে জানা।
  2. Loss ভাগ. প্রতিটি micro-batch-এর গড় loss ৪ দিয়ে ভাগ করে backward, যাতে জমানো gradient ১২৮-এর গড়ের সমান হয়। ভাগ না করলে gradient ৪ গুণ।
  3. ধাপ গোনা. Optimizer প্রতি ৪টি micro-batch-এ একবার চলে: ৫০,০০০/১২৮ = ৩৯০.৬, অর্থাৎ ৩৯১টি ধাপ। Scheduler-এর T এই ধাপ দিয়ে গুনুন, micro-batch দিয়ে নয়।
  4. Batch ৪ গুণ. Accum ১৬ করলে effective batch ৫১২, ধাপ মাত্র ৯৮. প্রতি epoch-এ ধাপ ৪ গুণ কম, তাই linear scaling rule অনুযায়ী η = ০.১ × ৫১২/১২৮ = ০.৪.
  5. Warmup আর সীমা. বড় η-তে শুরুর অস্থিরতা বাড়ে, তাই warmup সঙ্গে দিন। নিয়মটি এক সীমার পরে কাজ করে না; Adam-এ অনেকে √৪ = ২ গুণ (η = ০.২) থেকে শুরু করেন। যাচাই করুন val curve দিয়ে।

৮ · একই কাজে চার রকম training — SGD-momentum বনাম AdamW, schedule-সহ ও ছাড়া

এখন সব জোড়া লাগাই। একই MNIST MLP, ২০,০০০টি train ছবি, ১০,০০০ validation, ১০,০০০ test; ৮ epoch × ১৫৭ ধাপ = ১২৫৬ ধাপ; batch ১২৮. SGD-momentum (০.৯, weight decay ৫ × ১০−৪) lr ০.০২৫, AdamW (weight decay ০.০১) lr ০.০০৫, দুটিই অংশ ৪-এর range test-এর ১/১০ নিয়মে। Schedule থাকলে ৬২ ধাপের warmup (৫%), তারপর cosine. প্রতিটি সেটআপ তিনটি seed-এ। সত্যিকারের run (CPU-তে পুরোটা প্রায় ৩০ সেকেন্ড):

~/bazarghar-dl$ uv run python compare.py
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
train 20000 | val 10000 | test 10000 | 8 epoch x 157 ধাপ = 1256 | warmup 62
SGD-momentum                  test acc 97.07 ± 0.19% | seed: 96.92, 97.00, 97.28
                              val loss/epoch: 0.197 0.134 0.131 0.114 0.108 0.119 0.106 0.103
SGD-momentum + warmup-cosine  test acc 97.15 ± 0.03% | seed: 97.17, 97.12, 97.15
                              val loss/epoch: 0.209 0.137 0.122 0.107 0.104 0.100 0.098 0.098
AdamW                         test acc 96.44 ± 0.46% | seed: 96.00, 96.92, 96.41
                              val loss/epoch: 0.192 0.139 0.138 0.135 0.128 0.131 0.149 0.140
AdamW + warmup-cosine         test acc 97.47 ± 0.08% | seed: 97.46, 97.40, 97.55
                              val loss/epoch: 0.185 0.129 0.125 0.108 0.105 0.097 0.095 0.095

চার সেটআপের val loss, তিন seed-এর গড়

Validation loss per epoch, MNIST MLP, mean of 3 seeds (real run)

Validation loss per epoch for SGD-momentum and AdamW with and without warmup plus cosine 0.19 0.15 0.11 13578 epoch SGD (ধ্রুব) SGD + warmup-cosine AdamW (ধ্রুব) AdamW + warmup-cosine
ছবি ১৫.১ — নিচে মানে কম loss. ধ্রুব lr-এর AdamW (কমলা ড্যাশ) epoch ৩-এর পরে প্রায় ০.১৩-এ আটকে যায় আর শেষে একটু ওঠে; একই AdamW warmup + cosine-এ (সবুজ) শেষ epoch-গুলোতে সবচেয়ে নিচে। SGD-momentum-এর দুই রেখার ব্যবধান ছোট।

সবচেয়ে বড় পার্থক্য optimizer-এ নয়, schedule-এ। ধ্রুব lr ০.০০৫-এ AdamW-এর val loss শেষ পাঁচ epoch-এ ০.১২৮ থেকে ০.১৪৯-এর মধ্যে কাঁপছে, test accuracy-র seed-ভেদে ছড়ানো ০.৪৬, তিন seed-এর মধ্যে প্রায় ১ শতাংশ-বিন্দুর ফারাক। একই lr-এ শিখর রেখে cosine দিয়ে নামালে val loss ০.০৯৫, accuracy ৯৭.৪৭ ± ০.০৮: শেষে ছোট ধাপ noise-এর মেঝে ভেঙেছে। SGD-momentum-এ lr তুলনামূলক ছোট (ধ্রুব রেখাও শেষে ০.১০৩), তাই schedule-এর লাভ কম, মূলত seed-ভেদ কমেছে (০.১৯ থেকে ০.০৩)। অর্থাৎ “AdamW SGD-এর চেয়ে ভালো” বা উল্টোটি এই run থেকে বলা যায় না: ধ্রুব lr-এ SGD এগিয়ে, schedule-সহ AdamW সামান্য এগিয়ে, আর পার্থক্য এক শতাংশ-বিন্দুরও কম।

এই তুলনার সীমা: এক local run, একটি ছোট MLP, MNIST-এর মতো সহজ কাজ, তিনটি seed, ৮ epoch, আর প্রতিটি optimizer-এর একটি মাত্র lr. ভিন্ন model (CNN, Transformer), দীর্ঘ training বা ভালোভাবে tune করা lr-এ ক্রম বদলাতে পারে। ন্যায্য তুলনার জন্য প্রতিটি সেটআপের নিজের সেরা lr খুঁজে, আরও seed-এ চালিয়ে, পরিসংখ্যানগত পরীক্ষা করতে হয় (মেশিন লার্নিং কোর্সের পর্ব ২২; deep learning-এ পর্ব ১৯, Deep learning experiment-এর পর্বে)। Test set শুধু শেষ মাপের জন্য; lr বাছাই হয়েছে training ডেটার range test থেকে।

৯ · নতুন optimizer — সতর্ক নোট, আর কোনটি কখন

প্রতি বছর নতুন optimizer প্রস্তাব হয়, প্রায়ই নির্দিষ্ট কিছু কাজে ভালো ফলের দাবিসহ। কয়েকটি নাম, যা আলোচনায় শুনবেন (সবগুলোর দাবি নির্দিষ্ট সেটআপের, সর্বজনীন নয়):

  • Lion (Chen ও সহকর্মী, ২০২৩): program search দিয়ে খুঁজে পাওয়া; momentum-এর একটি মিশ্রণের শুধু চিহ্ন (sign) নিয়ে ধাপ দেয়, তাই v রাখতে হয় না, optimizer state অর্ধেক। সাধারণত AdamW-এর চেয়ে ছোট lr আর বড় weight decay চায়।
  • Sophia (Liu ও সহকর্মী, ২০২৩): Hessian-এর diagonal-এর একটি সস্তা অনুমান দিয়ে ধাপ ভাগ করে, আর ধাপ clip করে; ভাষা model-এর pre-training-এর জন্য প্রস্তাবিত।
  • Shampoo (Gupta ও সহকর্মী, ২০১৮): প্রতিটি weight matrix-এর জন্য ছোট preconditioner matrix রাখে, second-order পদ্ধতির কাছাকাছি (মেশিন লার্নিং কোর্সের পর্ব ১৫); হিসাব আর memory বেশি।
  • Muon (২০২৪ সালের দিকে Keller Jordan ও সহকর্মীদের লেখা থেকে জনপ্রিয়): ২-মাত্রার weight matrix-এর momentum-কে Newton–Schulz পুনরাবৃত্তি দিয়ে প্রায় orthogonal করে; bias, embedding আর শেষ layer-এ সাধারণত AdamW-ই রাখা হয়।

এদের কয়েকটির পক্ষে ভালো ফলের রিপোর্ট আছে, কিন্তু স্বাধীন benchmark-এ (যেমন MLCommons-এর AlgoPerf উদ্যোগ) ফল কাজভেদে মিশ্র এসেছে বলে জানা যায়, আর ভালোভাবে tune করা AdamW এখনো শক্ত baseline. বাস্তব নিয়ম: নতুন optimizer চেষ্টার আগে AdamW-এর lr, warmup, schedule আর weight decay ঠিক করুন; তুলনা করুন একাধিক seed আর প্রতিটির নিজের tune করা lr-এ।

পরিস্থিতিপ্রচলিত শুরুর পছন্দ
Transformer, ভাষা model, fine-tuningAdamW + warmup (কয়েক শতাংশ ধাপ) + cosine বা linear decay; bias/norm-এ decay নয়
ছবির CNN, দীর্ঘ trainingSGD-momentum (০.৯, Nesterov ঐচ্ছিক) + cosine বা step; অথবা AdamW
দ্রুত একটি ভালো ফল, কম epochOne-cycle (SGD বা AdamW)
Training-এর দৈর্ঘ্য অজানাReduceLROnPlateau, সঙ্গে early stopping

আরও গভীরে: Cosine schedule-এর জনপ্রিয়তা Loshchilov আর Hutter-এর SGDR (arXiv ২০১৬, ICLR ২০১৭) থেকে, যেখানে cosine-কে বারবার “restart” করা হয়েছিল। One-cycle আর “super-convergence” Smith আর Topin-এর ২০১৭ সালের প্রবন্ধে। বড় batch-এর জন্য layer অনুযায়ী learning rate মানিয়ে নেওয়া LARS আর LAMB (You ও সহকর্মী, ২০১৭ ও ২০১৯) আরেকটি ধারা। ভাষা model-এ “warmup-stable-decay” (WSD) ধাঁচের schedule, যেখানে দীর্ঘ সময় η স্থির রেখে শেষে দ্রুত নামানো হয়, সাম্প্রতিক আলোচনায় এসেছে; এর তুলনামূলক সুবিধা নিয়ে গবেষণা চলছে। Scheduler-এর ঠিক আচরণ আপনার version-এর torch.optim docs-এ দেখে নিন।

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

  • T = ১০০০, W = ১০০, ηmax = ০.১-এর warmup + cosine-এ ধাপ ৫৫০-এ η? Cosine-এর ঠিক মাঝে, ০.০৫.
  • Adam + L2 আর AdamW-এর পার্থক্য? L2 gradient-এ যোগ হয়ে √v̂ দিয়ে ভাগ হয়; AdamW সরাসরি w ← (1 − ηλ)w করে, সব weight-এ সমান অনুপাতে।
  • Micro-batch ১৬, accumulation ৮, ২টি GPU; effective batch? ১৬ × ৮ × ২ = ২৫৬; প্রতিটি loss ৮ দিয়ে ভাগ।
  • Batch ২৫৬ থেকে ১০২৪, SGD-তে η ০.১ থেকে? Linear scaling rule-এ ০.৪, warmup-সহ; নিয়মটি অভিজ্ঞতালব্ধ।
  • Warmup কেন Adam-এ জরুরি? শুরুতে v̂ অল্প নমুনার, অনুপাত m̂/√v̂ প্রায় ±১, তাই সব parameter পূর্ণ η-এর ধাপ নেয়।

১০ · AI-এর যুগে — AI-এর দেওয়া optimizer সেটিং যাচাই

“AdamW আর cosine schedule দিয়ে training কোড দাও” বললে AI assistant দ্রুত কোড দেয়, কিন্তু এই পর্বের ভুলগুলো চুপচাপ: কোড চলে, শুধু ফল একটু খারাপ, বা seed-ভেদে অস্থির।

AI প্রায়ই যা করেফলকীভাবে ধরবেন
OneCycleLR বা ধাপ-ভিত্তিক cosine প্রতি epoch-এ ডাকেSchedule শেষই হয় না, বা অনেক আগে শেষTraining-এর সময় lr লগ করে schedule-এর ছবি আঁকুন
torch.optim.Adam(..., weight_decay=0.01) লিখে বলে “AdamW-এর মতো”L2, decoupled decay নয়; λ-এর অর্থ আলাদাAdamW ক্লাস ব্যবহার; docs-এ update সূত্র পড়ুন
Accumulation-এ loss ভাগ করে না, বা scheduler-এর T micro-batch দিয়ে গোনেk গুণ gradient, বা schedule k গুণ লম্বাএক batch বনাম micro-batch-এর gradient মিলিয়ে দেখুন
সব parameter-এ weight decay, বা অস্তিত্বহীন argument (পুরোনো বা বানানো API)Norm-এর γ ছোট হয়; বা TypeErrorparam_groups ছাপুন; নিজের version-এর docs
অন্য model-এর “সেরা lr” কপি করে, এক seed-এ “নতুন optimizer ভালো” বলেভুল পরিসর; ভাগ্যকে উন্নতি ভাবাRange test, কয়েকটি seed, AdamW baseline

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

আমি PyTorch-এ একটি MLP train করছি, AdamW দিয়ে, micro-batch ৩২ আর gradient accumulation ৪. ১. Bias আর LayerNorm-এর weight-কে weight decay থেকে বাদ দিয়ে দুটি parameter group বানাও, আর প্রতিটি group-এ কতটি parameter তা ছাপাও। ২. LambdaLR দিয়ে ৫% warmup + cosine schedule লেখো; মোট ধাপ গোনো optimizer.step() দিয়ে, micro-batch দিয়ে নয়। ৩. scheduler.step() কোথায় বসবে, আর কেন optimizer.step()-এর পরে, তা বলো। ৪. একটি পরীক্ষা লেখো যা দেখায় accumulation-এর gradient একবারে বড় batch-এর gradient-এর সঙ্গে মেলে। ৫. Training-এর সময় প্রতিটি ধাপের lr লগ করে schedule আঁকার কোড দাও। ৬. আমার torch version-এ কোনো argument নিয়ে নিশ্চিত না হলে স্পষ্ট বলো; অনুমান করে argument বানিও না।

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

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

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

প্র ০১Adam কি SGD-কে “পরাজিত” করেছে? গভীর network-এ adaptive optimizer নিয়ে বিতর্কটি কী?

Kingma আর Ba-র Adam (২০১৪ সালে arXiv-এ, ICLR ২০১৫) দ্রুত জনপ্রিয় হয়, কারণ default সেটিংয়েই অনেক কাজে চলে আর learning rate-এর বড় পরিসরে স্থির থাকে। কিন্তু ২০১৭ সালে Wilson ও সহকর্মীদের “The Marginal Value of Adaptive Gradient Methods in Machine Learning” প্রবন্ধ একটি অস্বস্তিকর পর্যবেক্ষণ দেয়: তাঁদের কিছু ছবি আর ভাষার পরীক্ষায় ভালোভাবে tune করা SGD-momentum adaptive পদ্ধতির চেয়ে ভালো test ফল দিয়েছিল, যদিও training loss-এ adaptive পদ্ধতি দ্রুত নামে। এক ব্যাখ্যা ছিল যে adaptive পদ্ধতি ভিন্ন ধরনের সমাধানে পৌঁছায়, যার generalization আলাদা।

এরপর বিতর্ক অনেকটা বদলেছে। Loshchilov আর Hutter দেখান, Adam-এর একটি দুর্বলতা আসলে L2 আর weight decay-এর মিশ্রণ; AdamW-এ decay আলাদা করলে ফারাক অনেক কমে। পরে Choi ও সহকর্মীরা (২০১৯) যুক্তি দেন যে optimizer তুলনা hyperparameter খোঁজার পরিসরের ওপর খুব নির্ভরশীল; যথেষ্ট tune করলে সাধারণ পদ্ধতি (Adam) বিশেষ পদ্ধতির (SGD) চেয়ে খারাপ হওয়ার কথা নয়, কারণ Adam-এর কিছু সেটিংয়ে সে momentum-এর কাছাকাছি আচরণ করে।

আজকের অনুশীলনে ভাগাভাগি মোটামুটি এমন: Transformer আর ভাষা model-এ AdamW প্রায় সর্বজনীন, কারণ SGD সেখানে প্রায়ই ভালো চলে না (বিভিন্ন layer-এর gradient-এর মাপ খুব আলাদা)। ছবির CNN-এ SGD-momentum এখনো শক্ত পছন্দ। এই পর্বের run-ই দেখায় কেন সাধারণ ঘোষণা বিপজ্জনক: ধ্রুব lr-এ SGD এগিয়ে, schedule-সহ AdamW এগিয়ে, আর পার্থক্য এক শতাংশ-বিন্দুর কম। সিদ্ধান্ত বদলে দিয়েছে schedule, optimizer নয়।

গবেষণা পদ্ধতির দিক থেকেও শিক্ষা আছে। নতুন optimizer-এর প্রবন্ধে প্রায়ই নিজের পদ্ধতি বেশি tune করা হয়, baseline কম; এ কারণেই AlgoPerf-এর মতো উদ্যোগ নির্দিষ্ট বাজেটে স্বাধীনভাবে tune করে তুলনার নিয়ম বানাতে চেয়েছে।

মূল উপলব্ধি: “কোন optimizer সেরা” প্রশ্নটি অসম্পূর্ণ; কোন কাজে, কত tuning বাজেটে, কোন schedule-এ, সেটি না বললে উত্তর অর্থহীন।

প্র ০২Warmup কেন কাজ করে — এটি কি শুধু Adam-এর একটি তাপ্পি, নাকি আরও গভীর কিছু?

Warmup-এর ইতিহাস কয়েকটি স্বাধীন ধারা থেকে এসেছে। ছবির জগতে He ও সহকর্মীরা (২০১৫) খুব গভীর ResNet-এ শুরুতে ছোট learning rate দিয়ে কিছু সময় চালানোর কথা লেখেন। Goyal ও সহকর্মীরা (২০১৭) বড় batch-এ linear scaling rule-এর সঙ্গে ধীরে ধীরে বাড়ানো warmup ব্যবহার করেন, আর জানান যে তা ছাড়া শুরুর দিকে training ভেঙে পড়ত। ভাষার জগতে ২০১৭ সালের Transformer প্রবন্ধ warmup-কে schedule-এর অংশ করে, আর BERT থেকে শুরু করে বহু model-এ linear warmup প্রায় মানক হয়ে যায়।

ব্যাখ্যা নিয়ে কয়েকটি তত্ত্ব আছে, যা পরস্পরবিরোধী নয়। প্রথম, Adam-এর পরিসংখ্যান: শুরুতে v̂ অল্প নমুনার, তাই ধাপের মাপ অনিশ্চিত (RAdam প্রবন্ধের যুক্তি; এই পর্বের stepper-এ দেখেছেন প্রথম ধাপে সব parameter ঠিক ±η সরে)। দ্বিতীয়, স্থাপত্য: post-LN Transformer-এ শুরুতে শেষ layer-এর gradient বড় (Xiong ও সহকর্মী, ২০২০)। তৃতীয়, ভূদৃশ্যের বক্রতা: কিছু গবেষণা দেখায়, শুরুর বিন্দুতে loss খুব তীক্ষ্ণভাবে বাঁকা থাকে, আর ছোট ধাপে চলতে চলতে network এমন অঞ্চলে যায় যেখানে বক্রতা কম, তখন বড় η সহ্য করা যায়। “Edge of stability” নিয়ে Cohen ও সহকর্মীদের (২০২১) কাজ দেখায় যে gradient descent-এ বক্রতা নিজেই learning rate-এর সঙ্গে মানিয়ে নেয়।

এই তৃতীয় ব্যাখ্যা বলে warmup শুধু Adam-এর তাপ্পি নয়: SGD-তেও বড় η-এর জন্য এটি দরকার হতে পারে, যা Goyal-দের অভিজ্ঞতার সঙ্গে মেলে। আবার RAdam বা pre-LN-এর মতো পরিবর্তন warmup-এর প্রয়োজন কমায়, কিন্তু পুরোপুরি দূর করে না বলে অনেকের অভিজ্ঞতা। বাস্তবে warmup সস্তা: মোট ধাপের ১ থেকে ৫ শতাংশ, আর ক্ষতি প্রায় নেই।

মূল উপলব্ধি: Warmup একাধিক সমস্যার একটি সস্তা সমাধান: অনিশ্চিত optimizer পরিসংখ্যান, স্থাপত্যের শুরুর বড় gradient, আর তীক্ষ্ণ শুরুর ভূদৃশ্য; কোনটি আপনার ক্ষেত্রে মূল কারণ, তা বলা কঠিন, কিন্তু অল্প warmup রাখা প্রায় সবসময় নিরাপদ।

প্র ০৩Batch size বাড়ালে কেন একসময় আর লাভ হয় না — linear scaling rule-এর সীমা কোথায়?

বড় batch-এর আকর্ষণ সরল: বেশি GPU একসঙ্গে কাজ করলে প্রতি epoch-এ সময় কমে। Goyal ও সহকর্মীদের ২০১৭ সালের কাজ দেখায়, ImageNet-এ ResNet-50 প্রায় ৮ হাজারের batch-এ, linear scaling আর warmup দিয়ে, ছোট batch-এর কাছাকাছি নির্ভুলতায় পৌঁছায়। যুক্তিটি এমন: k টি ছোট SGD ধাপ প্রায় একটি k গুণ বড় ধাপের সমান, যদি এই ধাপগুলোর মধ্যে gradient খুব না বদলায়। কিন্তু এই শর্ত বড় η-তে ভাঙে, আর তাঁদের রিপোর্টেই আরও বড় batch-এ নির্ভুলতা কমতে দেখা গেছে।

McCandlish ও সহকর্মীরা (২০১৮) “critical batch size” ধারণা দেন: gradient-এর noise-এর মাপ থেকে একটি সীমা অনুমান করা যায়, যার নিচে batch দ্বিগুণ করলে ধাপের সংখ্যা প্রায় অর্ধেক হয়, আর ওপরে প্রায় কোনো লাভ নেই, শুধু হিসাব বাড়ে। এই সীমা কাজভেদে আলাদা, আর training চলার সঙ্গে বাড়ে। Shallue ও সহকর্মীদের (২০১৯) বিস্তৃত পরীক্ষাও দেখায় batch বাড়ানোর লাভ এক জায়গায় থেমে যায়, আর তার অবস্থান model আর ডেটায় নির্ভরশীল।

Generalization নিয়েও আলোচনা আছে। Keskar ও সহকর্মীরা (২০১৭) যুক্তি দেন বড় batch “তীক্ষ্ণ” minimum-এ যায়, যা কম generalize করে; পরে অন্যরা দেখান সঠিক lr আর schedule দিলে এই ফারাকের অনেকটা দূর হয়, তাই বিষয়টি পুরো মীমাংসিত নয়। Adam-এ batch আর lr-এর সম্পর্ক আরও জটিল; বর্গমূলের নিয়ম আর linear নিয়ম দুটোই ব্যবহার হয়। Gradient accumulation এই হিসাব বদলায় না: effective batch বড় হলে একই সীমা, শুধু সময় বাঁচে না, কারণ micro-batch-গুলো ক্রমে চলে।

মূল উপলব্ধি: Linear scaling rule একটি ভালো শুরুর অনুমান, প্রাকৃতিক নিয়ম নয়; প্রতিটি কাজের একটি সীমা আছে যার পরে বড় batch শুধু খরচ বাড়ায়, তাই নতুন batch-এ lr আর schedule আবার যাচাই করুন।

প্র ০৪Weight decay আসলে কী করে — বিশেষ করে normalization থাকলে, যেখানে weight-এর মাপ output বদলায় না?

সরল ছবিতে weight decay একটি regularizer: weight ছোট রাখে, তাই model সরল থাকে (মেশিন লার্নিং কোর্সের পর্ব ১৭-এ L2-এর MAP ব্যাখ্যা)। কিন্তু আধুনিক network-এ একটি অবাক করা ঘটনা আছে। ধরুন একটি Linear layer-এর পরে BatchNorm বা LayerNorm আছে। তখন weight-কে যেকোনো ধনাত্মক সংখ্যা দিয়ে গুণ করলে normalization-এর পরে output একই থাকে: network weight-এর মাপের প্রতি উদাসীন, ইংরেজিতে যাকে scale-invariant বলা হয়। তাহলে weight ছোট করে কোনো “সরলতা” আসে না।

van Laarhoven (২০১৭) আর পরে Zhang ও সহকর্মীরা (২০১৯) যুক্তি দেন, এই ক্ষেত্রে weight decay-এর কাজ আসলে কার্যকর learning rate বাড়ানো। Scale-invariant weight-এ gradient weight-এর মাপের বিপরীত অনুপাতে ছোট হয়, আর দিকের বদল হয় η/‖w‖²-এর অনুপাতে। Decay না থাকলে training-এর সঙ্গে ‖w‖ বাড়ে, তাই কার্যকর ধাপ নিজে থেকেই ছোট হয়ে যায়; decay ‖w‖ ছোট রেখে কার্যকর ধাপ বড় রাখে। অর্থাৎ decay আর learning rate schedule জড়িয়ে আছে।

এ থেকেই কয়েকটি ব্যবহারিক অভ্যাসের যুক্তি আসে। Norm layer-এর নিজের γ আর bias-এ decay দিলে output-এর মাপ সত্যিই বদলায়, তাই সেগুলো বাদ রাখা হয় (অংশ ৬)। AdamW-এ η আর λ গুণ হয়ে আসে (প্রতি ধাপে ηλ), তাই cosine-এ η নামলে decay-ও নামে; কেউ কেউ এ কারণে η আর λ একসঙ্গে tune করেন। আর Adam + L2-তে এই সব সম্পর্ক √v̂ দিয়ে আরও জট পাকায়, যা অংশ ৬-এর run-এ দেখেছেন: একই ০.০৫ দিয়ে Adam + L2-এ accuracy প্রায় ৫ শতাংশ-বিন্দু কমেছে।

মূল উপলব্ধি: Normalization-সহ network-এ weight decay শুধু regularizer নয়, কার্যকর learning rate-এর একটি নিয়ন্ত্রকও; তাই λ, η আর schedule আলাদা নয়, একসঙ্গে ভাবুন।

প্র ০৫Schedule-এর জন্য মোট ধাপ আগে জানতে হয় — দীর্ঘ বা অনির্দিষ্ট training-এ কী করবেন?

Cosine আর one-cycle-এর একটি লুকানো শর্ত আছে: T আগে জানতে হয়। পুরো পথটি T-এর ওপর আঁকা; ধাপ ৫৫০-এ η কত হবে তা নির্ভর করে মোট ১০০০ না ২০০০ ধাপ, তার ওপর। এতে দুটি বাস্তব সমস্যা হয়। প্রথম, training থামিয়ে আরও চালাতে চাইলে schedule আবার শুরু করতে হয়, নয়তো η প্রায় শূন্যে আটকে থাকে। দ্বিতীয়, ভিন্ন দৈর্ঘ্যের training তুলনা করতে প্রতিটির জন্য আলাদা run লাগে, কারণ ছোট run-এর মাঝের checkpoint দীর্ঘ run-এর সমতুল্য নয়।

এর কয়েকটি উত্তর এসেছে। Loshchilov আর Hutter-এর SGDR (২০১৬) cosine-কে বারবার restart করে, প্রতি চক্রের শেষে একটি ভালো checkpoint পাওয়া যায়। ReduceLROnPlateau T জানতে চায় না, val loss দেখে নামে, কিন্তু noise-এ ঝাঁকুনি খায়। ভাষা model-এর জগতে warmup-stable-decay (WSD) ধাঁচের schedule আলোচিত: দীর্ঘ সময় η স্থির, আর যখন থামতে চান তখন অল্প কিছু ধাপে দ্রুত নামানো। এতে স্থির অংশের যেকোনো checkpoint থেকে ছোট একটি decay চালিয়ে ব্যবহারযোগ্য model পাওয়া যায়। কয়েকটি প্রকাশিত কাজে এর ফল cosine-এর কাছাকাছি এসেছে বলে রিপোর্ট আছে, তবে সব ক্ষেত্রে নয়, আর এটি এখনো সক্রিয় গবেষণার বিষয়।

আরেকটি পথ হলো weight-এর গড় নেওয়া: শেষ দিকের checkpoint-গুলোর গড় (SWA) বা চলমান গড় (EMA) noise কমায়, যা অনেকটা decay-এর কাজ করে। এর বিস্তারিত পর্ব ১৬ (Deep learning-এ regularization)-এ।

ব্যবহারিক নিয়ম: T জানা থাকলে warmup + cosine সরল আর নির্ভরযোগ্য; না জানা থাকলে ReduceLROnPlateau বা WSD-ধাঁচ; আর যেকোনো ক্ষেত্রে schedule-এর অবস্থা (scheduler-এর state_dict) checkpoint-এ রাখুন, যাতে resume-এ η ঠিক জায়গা থেকে চলে (পর্ব ১০)।

মূল উপলব্ধি: Schedule একটি পরিকল্পনা, আর পরিকল্পনার জন্য দৈর্ঘ্য জানতে হয়; দৈর্ঘ্য অনিশ্চিত হলে এমন schedule বাছুন যা মাঝপথে থামা বা বাড়ানো সামলাতে পারে।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান।

  1. পূর্ণ numerical: (ক) T = ২০০০, W = ২০০, ηmax = ০.০০৩-এর warmup + cosine (ηmin = ০)-এ ধাপ ৯৯ আর ধাপ ১১০০-এ η কত? (খ) Adam (β₁ = ০.৯, β₂ = ০.৯৯৯, η = ০.০১)-এ w = ২, data gradient ১.০, λ = ০.১. প্রথম ধাপের পরে Adam + L2 আর AdamW-এ w কত?

    (ক) ধাপ ৯৯ warmup-এ: ০.০০৩ × ১০০/২০০ = ০.০০১৫. ধাপ ১১০০: ভগ্নাংশ (১১০০ − ২০০)/১৮০০ = ০.৫, তাই ০.০০৩ × ½(১ + cos ৯০°) = ০.০০১৫. দুটি একই, কাকতালীয়ভাবে নয়: warmup-এর মাঝ আর cosine-এর মাঝ দুটিতেই অর্ধেক। (খ) Adam + L2: gradient ১ + ০.১ × ২ = ১.২; প্রথম ধাপে m̂/√v̂ = ১, তাই w = ২ − ০.০১ = ১.৯৯ (L2-এর প্রভাব নেই)। AdamW: আগে ২ × (১ − ০.০১ × ০.১) = ১.৯৯৮, তারপর −০.০১: ১.৯৮৮.

  2. সমালোচনা ও নকশা: একটি দল BERT-ধাঁচের ছোট model fine-tune করছে: torch.optim.Adam(model.parameters(), lr=5e-3, weight_decay=0.01), কোনো warmup নেই, CosineAnnealingLR(opt, T_max=3) প্রতি batch-এ ডাকা (৩ epoch, প্রতিটিতে ৫০০ ধাপ), micro-batch ৮ আর accumulation ৪, কিন্তু loss ভাগ করা হয় না, scheduler প্রতি micro-batch-এ। প্রথম কয়েকশো ধাপে loss NaN হয়। কী কী সমস্যা, আর আপনার নকশা কী?

    ১, Adam + weight_decay: এটি L2, decoupled নয়; AdamW ব্যবহার করুন, bias আর LayerNorm-কে decay-হীন group-এ। ২, lr ৫ × ১০−৩: Transformer fine-tuning-এ সাধারণত অনেক ছোট মান ব্যবহার হয়; range test বা ছোট পরিসরে খুঁজুন। ৩, warmup নেই: Adam-এর প্রথম ধাপগুলো পূর্ণ η নেয়; কয়েক শতাংশ ধাপের warmup দিন। ৪, T_max = ৩ কিন্তু প্রতি batch-এ ডাকা: cosine ৩ ধাপেই শূন্যে নামে, তারপর আবার ওঠে (cosine পর্যায়ক্রমিক), তাই η লাফাতে থাকে। T গুনুন optimizer ধাপে। ৫, accumulation: loss ৪ দিয়ে ভাগ নেই, তাই gradient ৪ গুণ; scheduler micro-batch-এ ডাকায় schedule ৪ গুণ দ্রুত। ধাপ = ৩ × ৫০০/৪ = ৩৭৫ (৫০০ যদি micro-batch হয়)। নকশা: AdamW, দুটি group, LambdaLR-এ ~৫% warmup + cosine, T = ৩৭৫, প্রতি optimizer ধাপে scheduler, loss/৪, gradient clipping (পর্ব ১৭), আর lr লগ করে schedule-এর ছবি।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে (StepLR প্রতি ২ ডাকে ১০ ভাগের ১ করে)।
    import torch
    p = torch.nn.Parameter(torch.zeros(1))
    opt = torch.optim.SGD([p], lr=1.0)
    sch = torch.optim.lr_scheduler.StepLR(opt, step_size=2, gamma=0.1)
    for epoch in range(5):
        print(epoch, opt.param_groups[0]["lr"])
        opt.step(); sch.step()
    0 1.0
    1 1.0
    2 0.1
    3 0.1
    4 0.010000000000000002

    (সত্যিকারের run, PyTorch 2.15.0.) epoch ০ আর ১-এ ১.০; দুবার sch.step()-এর পরে ০.১; আরও দুবার পরে ০.০১. শেষ লাইনের অদ্ভুত লেজ float-এর গুণের ফল (০.১ × ০.১ ঠিক ০.০১ হয় না)। ক্রম উল্টালে (sch.step() আগে) epoch ১-এই ০.১ দেখাত আর warning আসত।

  4. নকশা ও কোড: বাজারঘরের একটি ছবি-classifier-এ GPU-তে ৬৪-এর বেশি ছবি ধরে না, কিন্তু আপনি ২৫৬-এর effective batch চান। ছোট পরীক্ষায় batch ৬৪-এ AdamW lr ১ × ১০−৩ ভালো চলেছে। Model-এ BatchNorm আছে। Accumulation, lr আর scheduler-সহ training ধাপটি লিখুন, আর BatchNorm নিয়ে কী সতর্কতা নেবেন বলুন।
    K = 4                                    # 64 x 4 = 256
    opt = torch.optim.AdamW(groups, lr=2e-3)  # √4 গুণ থেকে শুরু; range test দিয়ে যাচাই
    steps = epochs * (len(loader) // K)
    sch = torch.optim.lr_scheduler.LambdaLR(opt, warmup_cosine(steps, steps // 20))
    opt.zero_grad()
    for i, (xb, yb) in enumerate(loader):
        loss = F.cross_entropy(model(xb), yb) / K
        loss.backward()
        if (i + 1) % K == 0:
            opt.step(); sch.step(); opt.zero_grad()

    Adam-এ batch ৪ গুণ হলে √৪ = ২ গুণ lr একটি যুক্তিসংগত শুরু; linear নিয়মে ৪ × ১০−৩, যা Adam-এ ঝুঁকিপূর্ণ হতে পারে; দুটিই range test দিয়ে যাচাই করুন। Scheduler-এর মোট ধাপ optimizer ধাপে গোনা। শেষে K-এর কম micro-batch বাকি থাকলে সেগুলো বাদ পড়ে; প্রয়োজনে আলাদা করে সামলান। BatchNorm: পরিসংখ্যান ৬৪-এর batch থেকেই আসে, ২৫৬ নয়, তাই accumulation বড় batch-এর হুবহু সমান নয় (অংশ ৭-এ ৩৬% আপেক্ষিক পার্থক্য)। ৬৪ সাধারণত BatchNorm-এর জন্য যথেষ্ট বড়, কিন্তু ছোট micro-batch হলে GroupNorm বা LayerNorm বিবেচনা করুন (পর্ব ১৪)।

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

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

ক্রমনতুন একটি model-এর optimizer সেটআপের কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Model আর effective batch ঠিক করা
  2. Bias আর norm-কে decay-হীন রেখে parameter group বানানো
  3. LR range test চালিয়ে learning rate-এর পরিসর বের করা
  4. Optimizer ধাপে মোট T আর warmup W হিসাব করে scheduler বানানো
  5. প্রতি ধাপে optimizer.step()-এর পরে scheduler.step() ডেকে training
  6. কয়েকটি seed-এ val curve আর test মেপে তুলনা

Warmup + cosine schedule-এ T = ১০০০ ধাপ, W = ১০০, ηmax = ০.১, ηmin = ০. ধাপ ৫৫০-এ learning rate কত?

  • ০.০৪৫
  • ০.০৫
  • ০.০৮৫৪
  • ০.১

Adam-এ (β₁ = ০.৯, β₂ = ০.৯৯৯, bias correction-সহ) একটি parameter-এর প্রথম ধাপের gradient ০.০০১, আরেকটির ২০. η = ০.০০১ হলে প্রথম ধাপে দুটি কতটা সরে?

  • ১০−৬ আর ০.০২
  • দুটিই প্রায় শূন্য, কারণ m শূন্য থেকে শুরু
  • দুটিই প্রায় ০.০০১
  • ০.০০১ আর ০.০২

PyTorch-এ torch.optim.Adam(params, weight_decay=0.05) আর torch.optim.AdamW(params, weight_decay=0.05)-এর পার্থক্য কী?

  • Adam-এ শাস্তি gradient-এ যোগ হয়ে adaptive মাপে ভাগ হয়; AdamW weight-কে আলাদাভাবে একই অনুপাতে ছোট করে
  • কোনো পার্থক্য নেই, AdamW শুধু নতুন নাম
  • AdamW নিজে থেকে bias-কে decay থেকে বাদ দেয়
  • Adam decay আলাদা করে, AdamW gradient-এ যোগ করে

Micro-batch ১৬, gradient accumulation ৮, আর ২টি GPU-তে data parallel training. Effective batch কত, আর প্রতিটি micro-batch-এর গড় loss কী দিয়ে ভাগ করবেন?

  • ১২৮, ভাগ ৮
  • ২৫৬, ভাগ ২৫৬
  • ৩২, ভাগ ২
  • ২৫৬, ভাগ ৮

ReduceLROnPlateau (mode min, factor ০.৫, patience ২, আপেক্ষিক threshold ০.০০১, শুরু lr ০.১)-এ val loss ক্রমে ০.৯০, ০.৭০, ০.৬৫, ০.৬৬, ০.৬৫, ০.৬৫১. শেষ ডাকের পরে lr কত?

  • ০.১, কারণ ০.৬৫ আবার এসেছে, তাই উন্নতি হয়েছে
  • ০.০৫
  • ০.০২৫
  • প্রতি epoch-এ অর্ধেক, তাই ০.১ × ০.৫৬

SGD-তে batch ২৫৬ থেকে ১০২৪ করা হলো, আগে η = ০.১ ভালো ছিল। Linear scaling rule অনুযায়ী কী করবেন?

  • η ০.১-ই রাখবেন
  • η ০.২
  • η ০.৪, সঙ্গে warmup
  • η ০.০২৫

PyTorch-এ প্রতি epoch-এ scheduler.step() আগে আর optimizer.step() পরে ডাকা হলো, StepLR(step_size=1, gamma=0.5), শুরু lr ০.১. প্রথম epoch-এ optimizer কোন lr-এ চলে?

  • ০.০৫, আর PyTorch সতর্ক করে
  • ০.১
  • RuntimeError আসে
  • ০.০২৫

শব্দকোষ

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

মনে রাখুন

  1. Learning rate আগে: LR range test-এ loss যেখানে সবচেয়ে কম, তার প্রায় ১/১০ বা সবচেয়ে খাড়া নামার জায়গা একটি ভালো শুরু; প্রতিটি optimizer-এর পরিসর আলাদা।
  2. Warmup + cosine: η = ηmax(t + 1)/W, তারপর ½ηmax(1 + cos(π(t − W)/(T − W))); cosine অংশের মাঝে অর্ধেক। Adam-এর শুরুর অনিশ্চিত v̂-এর জন্য warmup জরুরি।
  3. Scheduler: optimizer.step()-এর পরে, T যে এককে সেই হারে; ReduceLROnPlateau-কে val loss দিন; scheduler-এর state checkpoint-এ রাখুন।
  4. AdamW: decay সব weight-এ একই অনুপাতে; Adam + L2 নয়। Bias আর norm-কে decay-হীন parameter group-এ রাখুন।
  5. Accumulation: effective batch = micro × k × GPU; loss k দিয়ে ভাগ; BatchNorm থাকলে হুবহু মেলে না। Batch বাড়ালে SGD-তে linear scaling আর warmup, তবে যাচাই করে।

পরের পর্বে: Weight decay এই পর্বে optimizer-এর অংশ হিসেবে এসেছে। পর্ব ১৬ (Deep learning-এ regularization — dropout, weight decay আর early stopping) ইচ্ছা করে overfit করা একটি setup-এ dropout, weight decay, augmentation আর weight-এর গড় (SWA, EMA)-এর প্রভাব train/val curve দিয়ে মাপবে।

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

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