এই পর্বে যা শিখবেন
- Loss কমানোকে “শেখা” হিসেবে দেখা, আর রেখার ঢাল থেকে curve-এর derivative পর্যন্ত যাওয়া
- Power, constant multiple আর sum rule, eˣ আর ln x-এর derivative; finite difference দিয়ে সংখ্যায় derivative আর h-এর টানাপোড়েন নিজে মাপা
- Partial derivative, gradient vector আর contour plot-এ gradient-এর তির; chain rule আর backpropagation-এর মূল ধারণা
- Gradient descent-এর নিয়ম, তিনটি learning rate-এর আসল run, আর বাজারঘরের ডেটায় এক ও দুই parameter-এর রেখা fit করে lstsq-এর সঙ্গে মেলানো
- Local minimum, saddle point, SGD আর mini-batch, থামার নিয়ম, gradient checking আর automatic differentiation-এর পরিচয়
এই পর্ব ক্যালকুলাসের পুরো কোর্স নয়। লক্ষ্য একটাই: একটি মডেল কীভাবে নিজের সংখ্যাগুলো একটু একটু বদলে ভালো হয়, সেই যন্ত্রটি বোঝা। তার জন্য লাগে মাত্র তিনটি ধারণা: ঢাল (derivative), অনেক দিকের ঢাল একসঙ্গে (gradient), আর ঢালের উল্টো দিকে ছোট পা (gradient descent). প্রতিটি ধারণা আগে ছবি আর হাতের হিসাবে আসবে, তারপর সূত্র, তারপর NumPy-তে যাচাই। কোড পাতাতেই চলবে (ব্রাউজারের ভেতরে Python, Pyodide); প্রথমবার চালাতে কয়েক সেকেন্ড লাগতে পারে। Vector আর matrix-এর হিসাব এসেছে পর্ব ১৭-এ; এখানে gradient-কে একটি vector হিসেবেই দেখব।
১ · শেখা মানে loss কমানো — কুয়াশায় পাহাড় থেকে নামা
বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান) ক্রেতাকে অর্ডারের সময়েই বলতে চায়, “আপনার পার্সেল পৌঁছাতে মোটামুটি কত দিন লাগবে।” হাতে আছে পুরোনো অর্ডারের দূরত্ব আর আসলে কত দিন লেগেছিল। ধরুন নিয়মটি হবে সরল: দিন ≈ w × দূরত্ব + b. এখানে w আর b দুটি অজানা সংখ্যা, যাদের বলে parameter. প্রশ্ন হলো, কোন w আর b সবচেয়ে ভালো?
“ভালো” মাপতে একটি সংখ্যা লাগে। প্রতিটি পুরোনো অর্ডারে নিয়মের অনুমান থেকে আসল দিন বাদ দিলে পাওয়া যায় ভুল; ভুলকে বর্গ করে সবগুলোর গড় নিলে পাওয়া যায় mean squared error (MSE). এই সংখ্যাটিই loss: যত ছোট, নিয়ম তত ভালো মেলে। সূত্রে, n-টি অর্ডারের জন্য:
L(w, b) = (1/n) · ∑i (w·xi + b − yi)2
এখানে xi হলো i-তম অর্ডারের দূরত্ব, yi আসল দিন, আর ∑ মানে সব অর্ডার ধরে যোগ। মেশিন লার্নিং-এর বড় অংশ এই এক বাক্যে বলা যায়: একটি loss ঠিক করুন, তারপর এমন parameter খুঁজুন যাতে loss সবচেয়ে কম হয়। শেখা কত রকম হয়, তা আসবে পর্ব ৫৫-এ (যন্ত্র কীভাবে শেখে); আর এই রেখার সমস্যাটিই পূর্ণ মডেল হিসেবে আসবে পর্ব ৫৮-এ (Linear regression).
কুয়াশায় পাহাড় থেকে নামা। ধরুন আপনি সিলেটের কোনো চা-বাগানের টিলায়, ঘন কুয়াশায় দুই হাত দূরেও দেখা যায় না, আর নামতে হবে উপত্যকার সবচেয়ে নিচু জায়গায়। পুরো মানচিত্র নেই, আছে শুধু পায়ের নিচের মাটির ঢাল। বুদ্ধিমানের কাজ হলো: পা দিয়ে অনুভব করা কোন দিকে মাটি নিচু, সেদিকে এক পা ফেলা, আবার অনুভব করা। ঢাল খাড়া হলে একটু বড় পা, সমান হয়ে এলে ছোট পা, আর মাটি সমান হয়ে গেলে থামা। এখানে উচ্চতা হলো loss, আপনার অবস্থান হলো parameter, পায়ের নিচের ঢাল হলো derivative, আর পায়ের মাপ হলো learning rate. পুরো পর্বটি এই ছবির গণিত।
একটি কথা এখনই জেনে রাখুন। রেখা fit করার মতো সরল সমস্যায় সরাসরি সূত্রেও উত্তর বের করা যায় (পর্ব ১৭-এর least squares, np.linalg.lstsq). তাহলে ঢাল ধরে হাঁটার কষ্ট কেন? কারণ neural network-এর মতো মডেলে এমন কোনো সরাসরি সূত্র নেই, parameter থাকে লাখ বা কোটি, আর loss-এর আকার জটিল। সেখানে একমাত্র ব্যবহারিক উপায় হলো ঢাল মেপে মেপে নামা। তাই আমরা সরল সমস্যায় পদ্ধতিটি শিখব, যেখানে সঠিক উত্তর জানা আছে, আর মিলিয়ে দেখব।
২ · রেখার ঢাল থেকে curve-এর ঢাল — derivative কী
সরলরেখার ঢাল আপনি স্কুলে পড়েছেন। রিকশাভাড়া যদি প্রতি কিলোমিটারে ৩০ টাকা বাড়ে, তাহলে “ভাড়া বনাম দূরত্ব” রেখার slope বা ঢাল 30: x এক একক বাড়লে y কত বাড়ে। সূত্রে ঢাল = Δy ÷ Δx, যেখানে Δ (ডেল্টা) মানে “পার্থক্য”। সরলরেখার ঢাল সব জায়গায় এক।
Curve-এর ঢাল কিন্তু জায়গায় জায়গায় আলাদা। f(x) = x² ভাবুন: x = 0-এর কাছে curve প্রায় সমান, x = 2-এর কাছে বেশ খাড়া। তাহলে “x = 1-এ ঢাল কত” প্রশ্নের অর্থ কী? কৌশলটি হলো: x = 1-এর বিন্দু P আর তার একটু ডানের আরেকটি বিন্দু Q নিন, দুটিকে একটি রেখায় যুক্ত করুন। দুই বিন্দু দিয়ে যাওয়া এই রেখার নাম secant, আর তার ঢাল আমরা হিসাব করতে পারি। তারপর Q-কে P-এর দিকে সরাতে থাকুন। নিচের ধাপে দেখুন রেখাটি কীভাবে ঘুরতে ঘুরতে একটি নির্দিষ্ট রেখায় স্থির হয়।
- h = 1. Q = (2, 4). Secant-এর ঢাল = (4 − 1) ÷ (2 − 1) = 3. রেখাটি curve-কে দুই জায়গায় কাটে, তাই x = 1-এর আসল ঢালের চেয়ে বেশি খাড়া।
- h = 0.5. Q = (1.5, 2.25). ঢাল = (2.25 − 1) ÷ 0.5 = 2.5. Q কাছে আসায় রেখা একটু শুয়ে পড়েছে।
- h = 0.1. Q = (1.1, 1.21). ঢাল = 0.21 ÷ 0.1 = 2.1. প্রতিবার ঢাল হয়েছে ঠিক 2 + h: হাতে দেখুন, ((1 + h)² − 1) ÷ h = (2h + h²) ÷ h = 2 + h.
- h = 0.01. Q = (1.01, 1.0201). ঢাল = 0.0201 ÷ 0.01 = 2.01. ছবিতে Q আর P প্রায় এক বিন্দু, রেখাটিও প্রায় স্থির।
- h → 0. 2 + h-এর h শূন্যের দিকে গেলে ঢাল যায় 2-এর দিকে। সবুজ রেখাটি curve-কে P-তে শুধু ছুঁয়ে যায়; এর নাম tangent, আর এর ঢালই x = 1-এ x²-এর derivative: f′(1) = 2.
এই “h-কে শূন্যের দিকে নেওয়া”-ই derivative-এর সংজ্ঞা। একটি function f-এর x বিন্দুতে derivative, লেখা হয় f′(x) বা df/dx:
f′(x) = limh→0 [ f(x + h) − f(x) ] ÷ h
“limh→0” (limit) মানে “h শূন্যের যত কাছে খুশি নিলে ভাগফলটি যে সংখ্যার কাছে স্থির হয়”। h ঠিক শূন্য বসানো যায় না, কারণ তখন 0 ÷ 0. ছোঁয়া-রেখাটির নাম tangent. x²-এর ক্ষেত্রে যেকোনো x-এ একই হিসাব করলে পাওয়া যায় ((x + h)² − x²) ÷ h = 2x + h, তাই f′(x) = 2x: x = 0-এ ঢাল 0 (curve-এর তলায় সমান মাটি), x = 1-এ 2, x = 3-এ 6.
Derivative-এর তিনটি তথ্য gradient descent-এর জন্য যথেষ্ট। চিহ্ন বলে দিক: f′ ধনাত্মক মানে ডানে গেলে f বাড়ে, তাই নামতে হলে বাঁয়ে যেতে হবে; ঋণাত্মক মানে উল্টো। মাপ বলে ঢাল কত খাড়া। আর f′ = 0 মানে সমান জায়গা: তলা হতে পারে, চূড়া হতে পারে, অথবা অংশ ৯-এর saddle point.
৩ · অল্প কয়েকটি নিয়ম — power, constant, sum, eˣ আর ln x
প্রতিবার limit দিয়ে হিসাব করতে হয় না; কয়েকটি নিয়ম মুখস্থ রাখলেই AI-এর বেশির ভাগ loss-এর ঢাল বের করা যায়। এই পর্বে শুধু এগুলোই লাগবে:
| নিয়ম | f(x) | f′(x) | উদাহরণ |
|---|---|---|---|
| Power rule | xⁿ | n·xⁿ⁻¹ | x³ → 3x²; x → 1; ধ্রুবক 7 → 0 |
| Constant multiple | c·g(x) | c·g′(x) | 5x² → 10x |
| Sum rule | g(x) + h(x) | g′(x) + h′(x) | x² + x → 2x + 1 |
| Exponential | eˣ | eˣ | 3eˣ → 3eˣ |
| Natural log | ln x (x > 0) | 1/x | ln x at x = 2 → 0.5 |
e ≈ 2.71828 একটি বিশেষ সংখ্যা: eˣ-এর ঢাল প্রতিটি বিন্দুতে তার নিজের মানের সমান, এই কারণেই সম্ভাবনা আর neural network-এর সূত্রে এটি বারবার আসে। ln x হলো e-ভিত্তিক logarithm, eˣ-এর উল্টো; পর্ব ৩৮-এর (Entropy আর information) cross-entropy loss-এ এটিই মূল উপাদান। এবার দুটি হাতে-করা উদাহরণ।
উদাহরণ ১: f(x) = 3x⁴ − 2x + 5. Sum rule-এ তিনটি অংশ আলাদা: 3x⁴-এর ঢাল 3 × 4x³ = 12x³ (constant multiple আর power), −2x-এর ঢাল −2, আর ধ্রুবক 5-এর ঢাল 0 (সমান রেখার কোনো ঢাল নেই)। তাই f′(x) = 12x³ − 2, আর x = 1-এ f′(1) = 12 − 2 = 10.
উদাহরণ ২: একটিমাত্র অর্ডারের loss, যেখানে দূরত্ব x = 2 আর আসল দিন y = 5: L(w) = (2w − 5)². বর্গটি খুলে লিখলে L(w) = 4w² − 20w + 25, তাই নিয়মে L′(w) = 8w − 20. w = 1-এ L′(1) = −12: ঋণাত্মক, মানে w বাড়ালে loss কমবে। এই ফলটি অংশ ৬-এ chain rule দিয়ে আরেক পথে আবার পাব, বর্গ না খুলেই।
পরীক্ষার খাতার জন্য একটি দ্রুত যাচাই: derivative বের করার পর একটি বিন্দুতে ছোট h (যেমন 0.001) নিয়ে [f(x + h) − f(x − h)] ÷ 2h ক্যালকুলেটরে হিসাব করুন। উদাহরণ ১-এ x = 1, h = 0.001 নিলে প্রায় 10.000012 আসে, 10-এর খুব কাছে। মিললে নিয়ম ঠিক খাটিয়েছেন। এই যাচাইটিই পরের অংশের বিষয়, আর অংশ ৯-এ এটি পেশাদারদের একটি অভ্যাস হিসেবে ফিরবে।
৪ · সংখ্যা দিয়ে derivative — finite difference আর h-এর টানাপোড়েন
সূত্র না জানলেও কম্পিউটার derivative আন্দাজ করতে পারে: সংজ্ঞার h-কে শূন্য না করে একটি ছোট সংখ্যা বসিয়ে দিলেই হলো। একে বলে finite difference. দুটি প্রচলিত রূপ:
forward: [f(x + h) − f(x)] ÷ h · central: [f(x + h) − f(x − h)] ÷ 2h
Forward শুধু ডানের একটি বিন্দু দেখে; central ডানে আর বাঁয়ে সমান দূরের দুটি বিন্দু দেখে, তাই দুই দিকের বাঁক একে অপরকে কাটাকাটি করে আর সাধারণত অনেক বেশি নির্ভুল হয়। অংশ ২-এর x²-এ forward দিয়েছিল 2 + h, আর central দেয় ঠিক 2, যেকোনো h-এ।
তাহলে কি h যত ছোট, তত ভালো? গণিতে হ্যাঁ, কম্পিউটারে না। দুটি ভুল একে অপরের সঙ্গে টানাটানি করে:
- Truncation error: h শূন্য নয় বলে সূত্রটিই আনুমানিক। Forward-এ এই ভুল মোটামুটি h-এর সমানুপাতিক, central-এ h²-এর। h বড় হলে এটি বড়।
- Rounding error: float64 মোটামুটি ১৬টি দশমিক অঙ্ক রাখে (পর্ব ৯-এ দেখেছেন 0.1 + 0.2 কেন 0.3 নয়)। h খুব ছোট হলে f(x + h) আর f(x) প্রায় একই সংখ্যা, বিয়োগ করলে অর্থপূর্ণ অঙ্কগুলো মুছে যায়, পড়ে থাকে শুধু শেষ অঙ্কের গোলমাল; তারপর ছোট h দিয়ে ভাগ করায় সেই গোলমাল বিশাল হয়ে ওঠে। h ছোট হলে এটি বড়।
নিচের কোড f(x) = eˣ-এর x = 1-এ derivative (আসল মান e, কারণ eˣ-এর ঢাল eˣ) ১৫টি ভিন্ন h দিয়ে মাপে, আর দুই পদ্ধতির ভুল log-log ছবিতে আঁকে।
import numpy as np
import matplotlib.pyplot as plt
f, x0 = np.exp, 1.0 # f(x) = e^x, তাই আসল f'(1) = e
exact = np.e
hs = 10.0 ** -np.arange(1, 16) # h = 1e-1, 1e-2, ..., 1e-15
fwd = (f(x0 + hs) - f(x0)) / hs # forward difference
cen = (f(x0 + hs) - f(x0 - hs)) / (2 * hs) # central difference
err_f = np.abs(fwd - exact)
err_c = np.abs(cen - exact)
print(" h forward ভুল central ভুল")
for h, a, b in zip(hs, err_f, err_c):
print(f"{h:.0e} {a:.1e} {b:.1e}")
print("সেরা h (forward):", f"{hs[err_f.argmin()]:.0e}",
" সেরা h (central):", f"{hs[err_c.argmin()]:.0e}")
fig, ax = plt.subplots(figsize=(6, 3.6))
ax.loglog(hs, err_f, "o-", label="forward: (f(x+h) - f(x)) / h")
ax.loglog(hs, err_c, "s-", label="central: (f(x+h) - f(x-h)) / 2h")
ax.invert_xaxis()
ax.set_xlabel("step size h (smaller to the right)")
ax.set_ylabel("absolute error")
ax.set_title("Finite differences for d/dx e^x at x = 1")
ax.legend(fontsize=8)
plt.show()আসল আউটপুট (ব্রাউজারের Pyodide-এ):
h forward ভুল central ভুল
1e-01 1.4e-01 4.5e-03
1e-02 1.4e-02 4.5e-05
1e-03 1.4e-03 4.5e-07
1e-04 1.4e-04 4.5e-09
1e-05 1.4e-05 5.9e-11
1e-06 1.4e-06 1.6e-10
1e-07 1.4e-07 5.9e-11
1e-08 6.6e-09 6.6e-09
1e-09 2.2e-07 6.6e-09
1e-10 1.5e-06 6.7e-07
1e-11 3.3e-05 1.0e-05
1e-12 4.3e-04 2.1e-04
1e-13 4.6e-04 4.6e-04
1e-14 9.3e-03 9.3e-03
1e-15 3.9e-01 1.7e-01
সেরা h (forward): 1e-08 সেরা h (central): 1e-05
ওপরের দিকে (h বড়) truncation error রাজত্ব করে: h দশ গুণ ছোট করলে forward-এর ভুল দশ গুণ কমে (1.4e-01 → 1.4e-02 → 1.4e-03), আর central-এর ভুল একশো গুণ কমে (4.5e-03 → 4.5e-05 → 4.5e-07), ঠিক h আর h²-এর মতো। হাতে মিলিয়ে নিন: forward-এর ভুল প্রায় (h/2)·f″ = 0.05 × 2.718 ≈ 0.136, আর central-এর প্রায় (h²/6)·f‴ = 0.01 × 2.718 ÷ 6 ≈ 0.0045. তারপর একটি তলা, আর তার পরে ভুল আবার বাড়ে: h = 10−15-এ forward-এর উত্তর ৩৯% ভুল! এটি rounding error, কারণ e1 + 10−15 আর e1 float64-এ প্রায় অভিন্ন। ছবিতে দুটি রেখাই তাই “V” বা “U” আকারের। Forward-এর সেরা h এসেছে 10−8-এ, যা machine epsilon-এর (প্রায় 2.2 × 10−16) বর্গমূলের কাছাকাছি; central-এর সেরা h 10−5, ε1/3-এর কাছাকাছি। পাঠ্যবইয়ের মোটা নিয়মও এমনই বলে। (10−5 আর 10−7-এ central-এর ভুল সমান দেখাচ্ছে; argmin প্রথমটি বেছেছে। এই শেষ অঙ্কগুলো float-এর গোলমাল, অন্য মেশিনে সামান্য বদলাতে পারে।)
সতর্কতা: “h = 1e-12 দিলে নিশ্চয়ই খুব নির্ভুল” — এই অনুমানটি ভুল, আর অনেক কোডে চুপচাপ বসে থাকে। float64-এ central difference-এর জন্য h ≈ 10−5 থেকে 10−6 (x-এর মাপ অনুযায়ী বদলে) সাধারণত নিরাপদ। float32-এ (GPU-তে প্রচলিত) ε প্রায় 1.2 × 10−7, সেখানে সেরা h অনেক বড়, আর finite difference-এর ভুলও অনেক বেশি।
৫ · দুই চলকের function — partial derivative আর gradient
ডেলিভারির নিয়মে parameter দুটি, w আর b, তাই loss একটি দুই চলকের function: L(w, b). এর ছবি আর একটি curve নয়, একটি পৃষ্ঠ, যেন একটি বাটি বা উপত্যকা। কোনো বিন্দুতে “ঢাল কত” জিজ্ঞেস করলে পাল্টা প্রশ্ন আসে: কোন দিকে? পূর্ব দিকে এক রকম, উত্তর দিকে আরেক রকম।
সমাধানটি সহজ: একবারে একটি দিক। b-কে স্থির ধরে শুধু w বদলালে যে ঢাল পাওয়া যায়, তা হলো w-এর সাপেক্ষে partial derivative, লেখা হয় ∂L/∂w (∂ পড়া হয় “partial” বা “ডেল”)। হিসাবের নিয়ম আগের মতোই, শুধু বাকি চলককে ধ্রুবক মনে করতে হয়। একটি বাটির মতো সরল উদাহরণ নিই:
L(w1, w2) = w12 + 3·w22 ⇒ ∂L/∂w1 = 2w1, ∂L/∂w2 = 6w2
∂L/∂w1 বের করতে 3w22-কে ধ্রুবক ধরেছি, তাই তার ঢাল 0; ∂L/∂w2-এর বেলায় উল্টো। এবার দুটি partial derivative পাশাপাশি একটি vector-এ রাখলে পাওয়া যায় gradient, লেখা হয় ∇L (∇ পড়া হয় “nabla” বা “ডেল”):
∇L = (∂L/∂w1, ∂L/∂w2) = (2w1, 6w2)
বিন্দু (2, 1)-এ L = 4 + 3 = 7 আর ∇L = (4, 6). Gradient-এর সবচেয়ে গুরুত্বপূর্ণ গুণটি হলো: এটি সেই দিক দেখায় যেদিকে L সবচেয়ে দ্রুত বাড়ে, আর এর দৈর্ঘ্য (‖∇L‖ = √(16 + 36) ≈ 7.21) বলে সেই চড়াই কত খাড়া। তাই সবচেয়ে দ্রুত নামতে হলে যেতে হবে ঠিক উল্টো দিকে, −∇L. ছবিতে দেখতে ব্যবহার করা হয় contour plot: মানচিত্রের সমোন্নতি রেখার মতো, প্রতিটি রেখায় L-এর মান এক। নিচের কোড gradient-কে সংখ্যায় যাচাই করে, তারপর contour-এর ওপর gradient-এর তির আঁকে।
import numpy as np
import matplotlib.pyplot as plt
def L(w1, w2): # একটি বাটির মতো loss
return w1**2 + 3 * w2**2
def grad(w1, w2): # (∂L/∂w1, ∂L/∂w2)
return np.array([2 * w1, 6 * w2])
p = np.array([2.0, 1.0])
g = grad(*p)
h = 1e-5
num = [(L(p[0] + h, p[1]) - L(p[0] - h, p[1])) / (2 * h),
(L(p[0], p[1] + h) - L(p[0], p[1] - h)) / (2 * h)]
print("বিন্দু", p, " L =", L(*p))
print("gradient (সূত্রে):", g, " সংখ্যায়:", np.round(num, 6))
step = 0.05 * g / np.linalg.norm(g) # ছোট একটি পা
print("gradient-এর দিকে গেলে L =", round(L(*(p + step)), 4))
print("উল্টো দিকে গেলে L =", round(L(*(p - step)), 4))
a = np.linspace(-3, 3, 200)
W1, W2 = np.meshgrid(a, a)
fig, ax = plt.subplots(figsize=(5, 4.4))
cs = ax.contour(W1, W2, L(W1, W2), levels=[0.5, 2, 5, 9, 14, 20], cmap="viridis")
ax.clabel(cs, fontsize=8)
q = np.linspace(-2.5, 2.5, 6)
Q1, Q2 = np.meshgrid(q, q)
ax.quiver(Q1, Q2, 2 * Q1, 6 * Q2, color="tab:red", angles="xy")
ax.plot(*p, "ko")
ax.set_xlabel("w1"); ax.set_ylabel("w2"); ax.set_aspect("equal")
ax.set_title("L = w1^2 + 3 w2^2: gradient arrows point uphill")
plt.show()আসল আউটপুট (ব্রাউজারের Pyodide-এ):
বিন্দু [2. 1.] L = 7.0
gradient (সূত্রে): [4. 6.] সংখ্যায়: [4. 6.]
gradient-এর দিকে গেলে L = 7.3665
উল্টো দিকে গেলে L = 6.6454
সূত্রের gradient (4, 6) আর central difference-এর সংখ্যা ছয় দশমিক পর্যন্ত মিলেছে। একই দৈর্ঘ্যের (0.05) ছোট একটি পা gradient-এর দিকে ফেললে L 7 থেকে বেড়ে 7.3665, উল্টো দিকে ফেললে কমে 6.6454: gradient চড়াই দেখায়, −gradient উতরাই। ছবিতে তিনটি জিনিস লক্ষ করুন। এক, প্রতিটি লাল তির contour রেখার সঙ্গে লম্ব, কারণ রেখা ধরে হাঁটলে L বদলায় না, আর সবচেয়ে খাড়া দিক তার আড়াআড়ি। দুই, তিরগুলো কেন্দ্র থেকে বাইরের দিকে, চড়াইয়ের দিকে; কেন্দ্রের কাছে ছোট, দূরে বড়। তিন, বাটিটি গোল নয়, ডিমের মতো লম্বা, কারণ w2-এর দিকে ঢাল তিন গুণ খাড়া; তাই তিরগুলো কেন্দ্রের দিকে সোজা তাক করে না। অংশ ৮-এ দেখবেন, এই লম্বাটে বাটিই gradient descent-কে ধীর করে।
কেন উল্টো দিকই “সবচেয়ে খাড়া” উতরাই? খুব ছোট একটি পা d ফেললে L-এর বদল মোটামুটি ∇L · d, মানে gradient আর পায়ের vector-এর dot product (পর্ব ১৭). নির্দিষ্ট দৈর্ঘ্যের পায়ের জন্য dot product সবচেয়ে ঋণাত্মক হয় যখন d ঠিক −∇L-এর দিকে থাকে। Vector-এর দিক আর কোণের জ্যামিতিক ছবিটি পর্ব ৩৩-এ (Vector আর similarity) বিস্তারিত।
৬ · Chain rule — ধাপে ধাপে জোড়া function-এর ঢাল
বাস্তবের loss প্রায় সবসময় কয়েকটি ধাপে জোড়া: প্রথমে parameter দিয়ে অনুমান, তারপর অনুমান থেকে ভুল, তারপর ভুলের বর্গ। একটি function-এর ভেতরে আরেকটি function বসানো থাকলে মোট ঢাল বের করার নিয়মই chain rule. যদি y নির্ভর করে u-এর ওপর, আর u নির্ভর করে x-এর ওপর:
dy/dx = (dy/du) · (du/dx)
গাড়ির গিয়ারের মতো ভাবুন। প্রথম চাকা এক পাক ঘুরলে দ্বিতীয়টি ৩ পাক ঘোরে, আর দ্বিতীয়টি এক পাক ঘুরলে তৃতীয়টি ২ পাক; তাহলে প্রথমটির এক পাকে তৃতীয়টি 3 × 2 = 6 পাক ঘোরে। বদলের হার শিকল ধরে গুণ হয়। অংশ ৩-এর উদাহরণ ২ এবার বর্গ না খুলে ধাপে ধাপে করি: দূরত্ব x = 2, আসল দিন y = 5, এখনকার w = 1.
1p = w * x # অনুমান2e = p - y # ভুল3L = e ** 2 # loss4dL_de = 2 * e5dL_dp = dL_de * 16dL_dw = dL_dp * x
| p | |
| e | |
| L | |
| dL/de | |
| dL/dp | |
| dL/dw |
- সামনে, ধাপ ১. অনুমান p = w·x = 1 × 2 = 2 দিন। এটি সামনের দিকের হিসাব (forward pass), যেমন মডেল স্বাভাবিকভাবে চলে।
- সামনে, ধাপ ২. ভুল e = p − y = 2 − 5 = −3. অনুমান আসলের চেয়ে ৩ দিন কম।
- সামনে, ধাপ ৩. Loss L = e² = 9. প্রতিটি ধাপের মধ্যবর্তী মান (p, e) জমা রাখা হলো, কারণ পেছনের হিসাবে এগুলো লাগবে।
- পেছনে, শেষ ধাপ থেকে। L = e², তাই power rule-এ dL/de = 2e = 2 × (−3) = −6. এখানে জমা রাখা e = −3 কাজে লাগল।
- পেছনে, এক ধাপ। e = p − y, তাই de/dp = 1 (y ধ্রুবক)। Chain rule: dL/dp = dL/de × de/dp = −6 × 1 = −6.
- পেছনে, parameter পর্যন্ত। p = w·x, তাই dp/dw = x = 2. dL/dw = −6 × 2 = −12. অংশ ৩-এ বর্গ খুলে পেয়েছিলাম 8w − 20 = −12; একই উত্তর। Gradient ঋণাত্মক, তাই w বাড়াতে হবে।
ডেস্কটপে একই হিসাব central difference দিয়ে মিলিয়ে দেখা হয়েছিল (সত্যিকারের run, Python 3.14.4, NumPy 2.5.1): h = 10−6-এ পাওয়া গেছে −12.000000000789, chain rule-এর −12-এর সঙ্গে প্রায় দশ অঙ্ক পর্যন্ত মিল; বাকিটুকু অংশ ৪-এর rounding গোলমাল। আর learning rate η = 0.05 নিয়ে এক পা ফেললে w = 1 − 0.05 × (−12) = 1.6, নতুন অনুমান 3.2 দিন, loss 9 থেকে নেমে 3.24.
এই “সামনে মান, পেছনে ঢাল” কৌশলটিই backpropagation-এর হৃদয়। একটি neural network হলো শত শত ধাপের এমন শিকল; সামনের হিসাবে প্রতিটি ধাপের মান জমা থাকে, তারপর শেষ থেকে শুরু করে প্রতিটি ধাপের ছোট ঢাল গুণ করতে করতে পেছনে গেলে একবারের পেছন-যাত্রাতেই সব weight-এর gradient পাওয়া যায়। বিস্তারিত পর্ব ৬৮-এ (Neural network-এর ধারণা — perceptron থেকে ছোট network, NumPy দিয়ে); এখানে শুধু ধারণাটুকু।
৭ · Gradient descent — নিয়ম, এক ধাপ হাতে, আর learning rate
এখন সব টুকরো হাতে আছে। Gradient descent-এর নিয়ম এক লাইনের:
w ← w − η · ∇L(w)
“←” মানে নতুন মান বসানো (Python-এর w = w - eta * grad), w হলো সব parameter-এর vector, ∇L হলো সেখানে loss-এর gradient, আর η (গ্রিক অক্ষর eta) learning rate, মানে পায়ের মাপ। বিয়োগ চিহ্নটিই “উল্টো দিকে” যাওয়া। এই এক লাইন বারবার চালানোই পুরো পদ্ধতি। একটি সরল loss L(w) = (w − 3)² নিই, যার সবচেয়ে নিচু জায়গা স্পষ্টতই w = 3; derivative L′(w) = 2(w − 3). শুরু w = 0, η = 0.1.
1L = (w - 3) ** 22g = 2 * (w - 3)3step = -eta * g4w = w + step5# 3 − w প্রতিবার × 0.8
| w | |
| L(w) | |
| g = L′(w) | |
| step | |
| 3 − w |
- শুরু। w = 0, loss L = (0 − 3)² = 9. আমরা জানি তলা w = 3-এ, কিন্তু পদ্ধতিটি তা জানে না; সে শুধু এখানকার ঢাল মাপতে পারে।
- ঢাল মাপা। g = 2(0 − 3) = −6. ঋণাত্মক ঢাল মানে ডানে গেলে loss কমে।
- পায়ের মাপ। step = −η·g = −0.1 × (−6) = +0.6. বিয়োগ চিহ্ন ঢালের ঋণাত্মক চিহ্নকে উল্টে ডানের দিকে পা বানিয়েছে।
- পা ফেলা। w = 0 + 0.6 = 0.6, নতুন loss (0.6 − 3)² = 2.4² = 5.76. এক ধাপেই loss 9 থেকে 5.76.
- আবার। নতুন জায়গায় ঢাল g = 2(0.6 − 3) = −4.8, একটু কম খাড়া; step = +0.48, w = 1.08, L = 1.92² = 3.6864. ঢাল কমায় পা নিজে থেকেই ছোট হয়েছে, η একই থাকলেও।
- প্যাটার্ন। তলা থেকে দূরত্ব 3 → 2.4 → 1.92: প্রতি ধাপে × 0.8. কারণ w − 3 নতুন হয় (w − 3) − η·2(w − 3) = (1 − 2η)(w − 3), আর η = 0.1-এ 1 − 2η = 0.8. এই একটি সংখ্যাই বলে দেয় learning rate কেমন।
শেষ ধাপের সূত্রটি থেকে learning rate-এর পুরো গল্প বের হয়। প্রতি ধাপে তলা থেকে দূরত্ব গুণ হয় (1 − 2η) দিয়ে। এই গুণক 0 আর 1-এর মাঝে হলে দূরত্ব ধীরে ধীরে কমে; −1 আর 0-এর মাঝে হলে w তলার এপাশ-ওপাশ লাফিয়ে কমে; আর −1-এর চেয়ে ছোট হলে প্রতি লাফে দূরত্ব বাড়ে, মানে পদ্ধতি ছিটকে যায় (diverge). এই loss-এ তাই নিরাপদ সীমা 0 < η < 1; আরও সাধারণভাবে সীমাটি নির্ভর করে loss কত বাঁকা তার ওপর (মোটামুটি η < 2 ÷ বাঁকের মাপ, এখানে L″ = 2). তিনটি learning rate-এর আসল run দেখুন।
import numpy as np
import matplotlib.pyplot as plt
L = lambda w: (w - 3) ** 2 # সবচেয়ে কম loss w = 3-এ
dL = lambda w: 2 * (w - 3) # derivative
def descend(eta, w=0.0, steps=20):
path = [w]
for _ in range(steps):
w = w - eta * dL(w) # w ← w − η · dL/dw
path.append(w)
return np.array(path)
np.set_printoptions(precision=3, suppress=True)
runs = {0.02: "too small", 0.3: "good", 1.05: "too large"}
fig, axes = plt.subplots(1, 3, figsize=(10, 3.2), sharey=True)
grid = np.linspace(-4, 10, 200)
for ax, (eta, name) in zip(axes, runs.items()):
path = descend(eta)
print(f"η = {eta}: প্রথম ৬ ধাপ {path[:6]}")
print(f" ২০ ধাপ পরে w = {path[-1]:.4f}, L = {L(path[-1]):.4f}")
ax.plot(grid, L(grid), color="0.7")
ax.plot(path, L(path), "o-", ms=4)
ax.set_title(f"eta = {eta} ({name})")
ax.set_xlabel("w"); ax.set_ylim(-2, 60)
axes[0].set_ylabel("loss L(w)")
plt.tight_layout()
plt.show()আসল আউটপুট (ব্রাউজারের Pyodide-এ):
η = 0.02: প্রথম ৬ ধাপ [0. 0.12 0.235 0.346 0.452 0.554]
২০ ধাপ পরে w = 1.6740, L = 1.7583
η = 0.3: প্রথম ৬ ধাপ [0. 1.8 2.52 2.808 2.923 2.969]
২০ ধাপ পরে w = 3.0000, L = 0.0000
η = 1.05: প্রথম ৬ ধাপ [ 0. 6.3 -0.63 6.993 -1.392 7.832]
২০ ধাপ পরে w = -17.1825, L = 407.3333
η = 0.02 (খুব ছোট): গুণক 1 − 0.04 = 0.96, তাই প্রতি ধাপে দূরত্ব মাত্র ৪% কমে; ২০ ধাপ পরেও w = 1.674, তলা থেকে অনেক দূরে, loss 1.76. হাতে মিলিয়ে নিন: 3 − 3 × 0.9620 ≈ 3 − 3 × 0.442 = 1.674. ভুল দিকে যায়নি, শুধু ধীর। η = 0.3 (ঠিকঠাক): গুণক 0.4; দূরত্ব 3 → 1.2 → 0.48 → …, প্রথম পাঁচ ধাপেই w = 2.969, ২০ ধাপে চার দশমিক পর্যন্ত 3. η = 1.05 (খুব বড়): গুণক −1.1; w তলার দুই পাশে লাফায় (6.3, −0.63, 6.993 …) আর প্রতি লাফে দূরে সরে। ২০ ধাপ পরে w = −17.18, loss 407, শুরুর 9-এর চেয়ে ৪৫ গুণ বেশি। ছবির তৃতীয় প্যানেলে বিন্দুগুলো বাটির দেয়াল বেয়ে ওপরে উঠছে। ডেস্কটপের একটি run-এ η = 1.0 দিলে w চিরকাল 0 আর 6-এর মধ্যে দোলে (গুণক ঠিক −1), কখনো নামে না।
একই বাটি, তিনটি learning rate
Gradient descent paths on L(w) = (w − 3)², starting at w = 0
৮ · বাজারঘরের ডেলিভারি — gradient descent দিয়ে রেখা fit, lstsq-এর সঙ্গে তুলনা
এবার আসল (তবে কাল্পনিক) ডেটা। বাজারঘরের ৮টি পুরোনো অর্ডার, দূরত্ব আর লাগা দিন। সংখ্যাগুলো এই পর্বের জন্য বানানো, কোনো আসল কুরিয়ারের নয়।
| অর্ডার | ১ | ২ | ৩ | ৪ | ৫ | ৬ | ৭ | ৮ |
|---|---|---|---|---|---|---|---|---|
| দূরত্ব (কিমি) | 15 | 40 | 80 | 120 | 180 | 230 | 290 | 350 |
| x (১০০ কিমি এককে) | 0.15 | 0.4 | 0.8 | 1.2 | 1.8 | 2.3 | 2.9 | 3.5 |
| y (দিন) | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 5 |
দূরত্ব কিলোমিটারে না রেখে ১০০ কিমি এককে নেওয়া হয়েছে; কেন, তা একটু পরেই দেখবেন। দুটি ধাপে এগোব। ধাপ ১: একটিমাত্র parameter, দিন ≈ w·x (রেখা শূন্য বিন্দু দিয়ে যায়)। Loss L(w) = (1/n)∑(w·xi − yi)², আর chain rule-এ (অংশ ৬-এর মতোই, শুধু সব অর্ডারের গড়):
dL/dw = (2/n) · ∑ (w·xi − yi) · xi
এর একটি সরাসরি উত্তরও আছে: dL/dw = 0 বসিয়ে পাওয়া যায় w = ∑xiyi ÷ ∑xi2 = 45.95 ÷ 31.4525 ≈ 1.4609 (হাতে যোগগুলো করে দেখুন)। ধাপ ২: দুটি parameter, দিন ≈ w·x + b. এখন gradient দুই ঘরের:
∂L/∂w = (2/n)·∑ ri·xi, ∂L/∂b = (2/n)·∑ ri, যেখানে ri = w·xi + b − yi
ri হলো i-তম অর্ডারের অমিল বা residual. নিচের কোড দুটি ধাপই gradient descent দিয়ে চালায়, কয়েক ধাপ পরপর loss ছাপে, তারপর সরাসরি সমাধান (closed form) np.linalg.lstsq আর np.polyfit-এর সঙ্গে মেলায়।
import numpy as np
# বাজারঘরের ৮টি কাল্পনিক অর্ডার: দূরত্ব (১০০ কিমি এককে) আর ডেলিভারির দিন
x = np.array([0.15, 0.4, 0.8, 1.2, 1.8, 2.3, 2.9, 3.5])
y = np.array([1, 1, 2, 2, 3, 3, 4, 5], dtype=float)
mse = lambda pred: np.mean((pred - y) ** 2)
# ধাপ ১: এক parameter, দিন ≈ w · x
w, eta = 0.0, 0.05
for t in range(21):
if t % 5 == 0:
print(f"১-param ধাপ {t:2d}: w = {w:.4f}, MSE = {mse(w * x):.4f}")
w -= eta * 2 * np.mean((w * x - y) * x) # dMSE/dw
print("closed form Σxy/Σx² :", round(x @ y / (x @ x), 4))
# ধাপ ২: দুই parameter, দিন ≈ w · x + b
w, b = 0.0, 0.0
for t in range(401):
r = w * x + b - y # residual
if t % 100 == 0:
print(f"২-param ধাপ {t:3d}: w = {w:.4f}, b = {b:.4f}, MSE = {mse(w*x+b):.5f}")
w, b = w - eta * 2 * np.mean(r * x), b - eta * 2 * np.mean(r)
A = np.column_stack([x, np.ones_like(x)])
w_ls, b_ls = np.linalg.lstsq(A, y)[0]
print(f"lstsq : w = {w_ls:.4f}, b = {b_ls:.4f}, MSE = {mse(w_ls*x+b_ls):.5f}")
print("polyfit :", np.polyfit(x, y, 1).round(4))
print("৫০০ কিমি দূরে অনুমান:", round(w * 5 + b, 2), "দিন")আসল আউটপুট (ব্রাউজারের Pyodide-এ):
১-param ধাপ 0: w = 0.0000, MSE = 8.6250
১-param ধাপ 5: w = 1.3407, MSE = 0.2906
১-param ধাপ 10: w = 1.4510, MSE = 0.2342
১-param ধাপ 15: w = 1.4601, MSE = 0.2338
১-param ধাপ 20: w = 1.4609, MSE = 0.2338
closed form Σxy/Σx² : 1.4609
২-param ধাপ 0: w = 0.0000, b = 0.0000, MSE = 8.62500
২-param ধাপ 100: w = 1.1559, b = 0.7360, MSE = 0.05282
২-param ধাপ 200: w = 1.1508, b = 0.7476, MSE = 0.05277
২-param ধাপ 300: w = 1.1505, b = 0.7483, MSE = 0.05277
২-param ধাপ 400: w = 1.1504, b = 0.7484, MSE = 0.05277
lstsq : w = 1.1504, b = 0.7484, MSE = 0.05277
polyfit : [1.1504 0.7484]
৫০০ কিমি দূরে অনুমান: 6.5 দিন
শুরুর MSE 8.625 = (1 + 1 + 4 + 4 + 9 + 9 + 16 + 25) ÷ 8, কারণ w = 0-তে অনুমান সবসময় 0. এক parameter-এ ২০ ধাপেই w = 1.4609, হাতের সূত্রের সঙ্গে চার দশমিক পর্যন্ত মিল; কিন্তু MSE আটকে আছে 0.2338-এ, কারণ রেখাকে শূন্য দিয়ে যেতে বাধ্য করা হয়েছে, অথচ ১৫ কিমির অর্ডারেও ১ দিন লাগে। Intercept b যোগ করতেই MSE নেমে 0.0528, চার গুণের বেশি কম। দুই parameter-এ gradient descent ৪০০ ধাপে পৌঁছেছে w = 1.1504, b = 0.7484-এ, আর lstsq ও polyfit ঠিক একই রেখা দিয়েছে। অর্থ: প্রতি ১০০ কিমিতে প্রায় ১.১৫ দিন বেশি, আর দূরত্ব যা-ই হোক প্রায় ০.৭৫ দিনের একটি “স্থির” সময় (প্যাকিং, হাতবদল)। শেষ লাইনটি সতর্কবার্তা: ৫০০ কিমির অনুমান 6.5 দিন, কিন্তু ডেটায় সবচেয়ে দূরের অর্ডার ৩৫০ কিমি; ডেটার সীমার বাইরে রেখা টেনে নেওয়া (extrapolation) ঝুঁকির।
দুটি পর্যবেক্ষণ। প্রথমত, দুই parameter-এ এক parameter-এর চেয়ে অনেক বেশি ধাপ লেগেছে (১০০ ধাপেও শেষ অঙ্কগুলো বদলাচ্ছিল)। কারণ অংশ ৫-এর লম্বাটে বাটি: এই loss-এর বাঁক এক দিকে অন্য দিকের চেয়ে প্রায় ১৭ গুণ বেশি (হিসাবটি loss-এর দ্বিতীয় derivative-এর matrix থেকে, ডেস্কটপে মাপা), তাই খাড়া দিকের জন্য η ছোট রাখতে হয়, আর তাতে সমতল দিকে চলা ধীর হয়। দ্বিতীয়ত, scaling-এর প্রভাব। একই কোড দূরত্ব কিলোমিটারে রেখে η = 0.05 দিয়ে ডেস্কটপে চালানো হয়েছিল (সত্যিকারের run, Python 3.14.4, NumPy 2.5.1):
ধাপ 0: w = 0, b = 0, MSE = 8.625
ধাপ 1: w = 57.44, b = 0.2625, MSE = 1.296e+08
ধাপ 2: w = -2.257e+05, b = -936.5, MSE = 2.003e+15
ধাপ 3: w = 8.872e+08, b = 3.681e+06, MSE = 3.095e+22
ধাপ 4: w = -3.487e+12, b = -1.447e+10, MSE = 4.781e+29
ধাপ 5: w = 1.371e+16, b = 5.687e+13, MSE = 7.387e+36
পাঁচ ধাপেই MSE 1036, আর কিছুক্ষণ চালালে overflow হয়ে inf বা nan. কিলোমিটারে x-এর মান শত শত, তাই w-এর দিকে loss প্রায় দশ হাজার গুণ বেশি বাঁকা; সেই কোডেই নিরাপদ η-এর সীমা নেমে যায় প্রায় 2.5 × 10−5-এ, আর দুই দিকের বাঁকের অনুপাত দাঁড়ায় এক লাখের বেশি, ফলে b প্রায় নড়েই না। একক বদলানোয় ডেটার অর্থ একটুও বদলায়নি, বদলেছে শুধু loss-এর আকার। এই কারণেই gradient descent-এর আগে feature-কে তুলনীয় মাপে আনা প্রায় বাধ্যতামূলক অভ্যাস; বিস্তারিত পর্ব ৬৩-এ (Feature engineering).
৯ · বাস্তবের ঝামেলা — local minimum, saddle, SGD, থামার নিয়ম আর gradient checking
Local minimum আর অ-বাটি loss
এ পর্যন্ত সব loss ছিল বাটির মতো, গণিতের ভাষায় convex: তলা একটিই, যেখান থেকেই শুরু করুন সেখানে পৌঁছাবেন। Neural network-এর loss তেমন নয়; তাতে থাকতে পারে অনেক গর্ত। একটি ১-মাত্রার উদাহরণ: L(w) = w⁴ − 3w² + w.
দুটি গর্তের loss — কোথায় থামবেন, তা নির্ভর করে কোথা থেকে শুরু
Non-convex loss L(w) = w⁴ − 3w² + w: a global and a local minimum
ডেস্কটপে η = 0.01, ২০০ ধাপ (সত্যিকারের run, Python 3.14.4, NumPy 2.5.1):
start +2.00 -> w = +1.1309, L = -1.0702, slope = +9.3e-09
start +0.50 -> w = +1.1309, L = -1.0702, slope = -9.8e-08
start -2.00 -> w = -1.3008, L = -3.5139, slope = -1.9e-13
তিনটি run-ই “সফল”: ঢাল প্রায় শূন্য, পদ্ধতি থেমেছে। কিন্তু ডান দিক থেকে শুরু করা দুটি run আটকে গেছে local minimum-এ, যেখানে loss −1.07, অথচ বাঁয়ে −3.51-এর গভীর গর্ত আছে। Gradient descent শুধু পায়ের নিচের ঢাল দেখে; ছোট চূড়া পেরিয়ে অন্য উপত্যকা খোঁজা তার সাধ্যে নেই। তাই প্রায়ই কয়েকটি ভিন্ন শুরুর বিন্দু থেকে চালিয়ে সেরাটি নেওয়া হয়। এই খোঁজার সমস্যাটি পর্ব ৪৪-এর (Local search — hill climbing, simulated annealing) বিষয়ের খুব কাছের।
Saddle point: দুই চলকে আরেক রকম ফাঁদ আছে। f(x, y) = x² − y²-এর (0, 0)-এ gradient শূন্য, কিন্তু x-এর দিকে এটি তলা আর y-এর দিকে চূড়া, ঘোড়ার জিনের মতো। ডেস্কটপের run-এ (1, 0) থেকে শুরু করলে ৫০ ধাপে পৌঁছায় প্রায় (1.4 × 10−5, 0)-এ, gradient প্রায় শূন্য, যেন তলা পেয়েছে; অথচ শুরুতে y-এ মাত্র 10−6-এর ধাক্কা দিলে ৫০ ধাপে y বেড়ে 0.0091, মানে পদ্ধতি জিন থেকে পিছলে নামতে শুরু করেছে। অনেক মাত্রার loss-এ local minimum-এর চেয়ে saddle point-ই বেশি দেখা যায় বলে গবেষকেরা যুক্তি দিয়েছেন (ভাবনার প্রশ্ন ৫).
SGD আর mini-batch — পুরো ডেটা নয়, এক মুঠো
ওপরের প্রতিটি ধাপে gradient হিসাব হয়েছে সব অর্ডার দিয়ে (full batch). ৮টি অর্ডারে তা সস্তা, কিন্তু কোটি সারিতে প্রতিটি পায়ের আগে পুরো ডেটা পড়া অসম্ভব রকম ধীর। Stochastic gradient descent (SGD) এলোমেলোভাবে বাছা এক বা কয়েকটি সারি দিয়ে gradient আন্দাজ করে এখনই পা ফেলে। আন্দাজটি গোলমেলে (noisy), তবে গড়ে ঠিক দিকেই। মাঝামাঝি পথ mini-batch: যেমন একবারে ৩২টি সারি। বাজারঘরের ৪০০টি seed-করা কাল্পনিক অর্ডারে তিনটি পদ্ধতিকে সমান ডেটা দেওয়া হয়েছিল, প্রতিটিকে পুরো ডেটার ২০ বার (২০টি epoch), η = 0.05 (সত্যিকারের run, ডেস্কটপে, একবারের মাপ):
lstsq w, b = [1.1728 0.6938] MSE = 0.0922
full batch updates= 20 w, b = [1.2144 0.602 ] MSE = 0.09412
mini-batch 32 updates= 260 w, b = [1.1601 0.6893] MSE = 0.09309
SGD (1) updates= 8000 w, b = [1.1276 0.7965] MSE = 0.09459
তিনটিই একই পরিমাণ সারি পড়েছে (৪০০ × ২০ = ৮০০০), কিন্তু পা ফেলেছে ২০, ২৬০ আর ৮০০০ বার। Full batch মাত্র ২০টি নিখুঁত পায়ে এখনো পৌঁছায়নি (MSE 0.0941, সেরা 0.0922). Mini-batch সবচেয়ে কাছে গেছে। SGD অনেক পা ফেললেও প্রতিটি পা গোলমেলে, তাই তলার চারপাশে কাঁপছে (b = 0.7965, আসল সেরা 0.6938); বাস্তবে তাই সময়ের সঙ্গে η কমানো হয় (learning rate schedule). এই ছোট ফলগুলো একটি seed-এর একটি run, সাধারণ নিয়ম নয়; তবে “mini-batch প্রায়ই সেরা আপস” কথাটি ব্যবহারিক অভিজ্ঞতার সঙ্গে মেলে, আর GPU-তে ৩২ বা ২৫৬টি সারি একসঙ্গে গুণ করা সস্তা (পর্ব ১৫-এর vectorization).
থামার নিয়ম
Gradient descent নিজে থেকে থামে না; কখন থামবেন, তা আপনাকে ঠিক করতে হয়। প্রচলিত নিয়মগুলো, প্রায়ই একসঙ্গে:
- ধাপের সর্বোচ্চ সংখ্যা: যেমন ১০,০০০. সবসময় রাখুন, যাতে ভুল η-এ loop চিরকাল না চলে।
- Gradient ছোট হলে: ‖∇L‖ < 10−6-এর মতো সীমা; সমান মাটি মানে আর নামার জায়গা নেই (অথবা saddle!).
- Loss আর না কমলে: পরপর কয়েক ধাপে loss-এর বদল খুব ছোট হলে। একে বলে convergence.
- Loss বাড়লে বা
nanহলে: সঙ্গে সঙ্গে থামুন আর η কমান; অংশ ৮-এর কিলোমিটারের run-এর মতো। - Early stopping: মডেলের জন্য সবচেয়ে গুরুত্বপূর্ণ নিয়ম। Train ডেটার loss কমতেই থাকতে পারে, কিন্তু আলাদা validation ডেটার loss বাড়তে শুরু করলে মডেল মুখস্থ করছে; তখন থামুন। Validation ডেটা কী, পর্ব ৫৬-এ; overfitting পর্ব ৬১-এ।
Gradient checking — নিজের লেখা gradient যাচাই
হাতে gradient-এর সূত্র লিখলে একটি ২ বা একটি চিহ্ন বাদ পড়া খুব সহজ, আর মজার বিপদ হলো, ভুল gradient দিয়েও loss প্রায়ই কমে, শুধু ধীরে বা ভুল জায়গায়। তাই অভিজ্ঞরা gradient checking করেন: সূত্রের gradient-কে central difference-এর সংখ্যার সঙ্গে মেলানো, আপেক্ষিক ভুল ‖g − gnum‖ ÷ ‖g + gnum‖ দিয়ে। ডেলিভারির MSE-তে একটি ঠিক আর একটি ইচ্ছে করে ভুল (২ গুণ বাদ দেওয়া) gradient (সত্যিকারের run, ডেস্কটপে):
ঠিক: analytic [-6.903437 -3.21875 ] numeric [-6.903437 -3.21875 ] rel. error 1.8e-12
ভুল: analytic [-3.451719 -1.609375] numeric [-6.903437 -3.21875 ] rel. error 3.3e-01
ঠিক সূত্রে আপেক্ষিক ভুল 10−12-এর ঘরে, শুধু float-এর গোলমাল; ভুল সূত্রে 0.33, স্পষ্ট বাগ। ভুল gradient-টি দিক ঠিক দেখাচ্ছে, শুধু অর্ধেক মাপে; এমন gradient দিয়ে training চলবে, কেবল ধীরে, তাই loss দেখে বাগটি ধরা কঠিন। মোটা নিয়মে আপেক্ষিক ভুল 10−7-এর নিচে হলে নিশ্চিন্ত, 10−3-এর ওপরে হলে প্রায় নিশ্চিত বাগ। Gradient checking ধীর (প্রতিটি parameter-এর জন্য দুবার loss), তাই শুধু ছোট ডেটায় আর কয়েকটি বিন্দুতে, debugging-এর সময়।
আরও গভীরে: automatic differentiation. বাস্তবে কেউ বড় মডেলের gradient হাতে লেখেন না। PyTorch, JAX, TensorFlow-এর মতো library automatic differentiation করে: আপনার কোডের প্রতিটি ছোট হিসাব (যোগ, গুণ, exp) রেকর্ড করে, প্রতিটির ছোট derivative জানে, আর অংশ ৬-এর মতো পেছন থেকে chain rule জুড়ে সঠিক gradient দেয় (reverse mode). এটি finite difference-এর মতো আনুমানিক নয়, আবার symbolic algebra-র মতো বিশাল সূত্রও বানায় না। PyTorch-এ loss.backward(), JAX-এ jax.grad এই কাজের প্রচলিত নাম। এই পর্বে কিছু install করার দরকার নেই; NumPy-র হাতে-লেখা gradient বোঝাই ভিত্তি। Reverse-mode-এর ধারণা অনেক পুরোনো; Seppo Linnainmaa-র ১৯৭০ সালের কাজকে প্রায়ই এর আধুনিক রূপের শুরু বলা হয়, আর Baydin ও সহকর্মীদের ২০১৮ সালের সমীক্ষা-প্রবন্ধ “Automatic differentiation in machine learning: a survey” ভালো সূচনা।
পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):
- Gradient descent-এর update rule কী, বিয়োগ কেন? w ← w − η·∇L; gradient চড়াই দেখায়, তাই উল্টো দিকে গেলে loss কমে।
- Learning rate খুব বড় হলে কী হয়? Loss দোলে বা বাড়তে থাকে (diverge), শেষে
inf/nan; খুব ছোট হলে ধীর। - w = 2, dL/dw = 5, η = 0.1 হলে নতুন w? 2 − 0.1 × 5 = 1.5.
- Batch, mini-batch আর SGD-র পার্থক্য? প্রতি পায়ে পুরো ডেটা, একটি ছোট দল, বা একটি সারি দিয়ে gradient; কম সারি মানে সস্তা কিন্তু গোলমেলে পা।
- Chain rule কেন backpropagation-এর ভিত্তি? Network একটি function-এর শিকল; শেষ থেকে প্রতিটি ধাপের derivative গুণ করে সব weight-এর gradient এক পেছন-যাত্রায় পাওয়া যায়।
১০ · AI-এর যুগে — AI সহকারীর gradient আর training loop যাচাই
“Gradient descent দিয়ে linear regression লিখে দাও” বললে AI সহকারী কয়েক সেকেন্ডে কোড দেয়, আর কোডটি প্রায়ই চলে, loss-ও কিছুটা কমে। কিন্তু এই পর্বে দেখেছেন, ভুল gradient বা ভুল η-ও “চলে”। AI সহকারীরা প্রায়ই যা করে:
- Gradient-এর সূত্রে ২ বা গড়ের ভাগ (1/n) বাদ দেয়, চিহ্ন উল্টে
w += eta * gradলেখে, বা(n,)আর(n, 1)গুলিয়ে broadcasting-এ ভুল shape-এর gradient বানায়। - Feature scale না করে বড় η দেয়; loss
nanহলেnp.nan_to_numবাtry/exceptদিয়ে চাপা দেয়, অথবা warning বন্ধ করে দেয়। - ধাপের নির্দিষ্ট সংখ্যা ধরে থামে, convergence দেখে না; আর loss ও “accuracy” মাপে train ডেটাতেই, validation ডেটা আলাদা না করে, বা scaling-এর গড় পুরো ডেটা থেকে নিয়ে test-এর তথ্য train-এ মিশিয়ে ফেলে।
- পুরোনো বা বানানো API লেখে: NumPy 2-এ সরানো
np.float, অস্তিত্বহীন parameter, বা পুরোনো library-র optimizer-এর নাম। - বিশ্বাসযোগ্য কিন্তু না-মাপা দাবি করে: “learning rate 0.01 সবসময় ভালো”, “gradient descent সবসময় global minimum পায়”, বা না-চালানো আউটপুট বানিয়ে লেখে।
কোড চাওয়ার সময় গণিত, যাচাই আর থামার নিয়ম নিজেই বলে দিন। নিচের প্রম্পটে খেয়াল করুন: আসল ডেটা নেই, বানানো ৮টি সারি; gradient checking আর baseline আগে থেকেই চাওয়া হয়েছে।
AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Calculus কে আবিষ্কার করেছেন, Newton না Leibniz? আর একটি চিহ্নের পছন্দ কীভাবে আজকের AI-কোডে টিকে আছে?
সপ্তদশ শতকের শেষ দিকে দুজন মানুষ প্রায় একই সময়ে, আলাদাভাবে, বদলের হার আর ক্ষেত্রফলের গণিত গড়ে তোলেন। Isaac Newton ১৬৬০-এর দশকে নিজের “fluxions” পদ্ধতি তৈরি করেছিলেন বলে তাঁর খাতা থেকে জানা যায়, কিন্তু দীর্ঘদিন ছাপেননি। Gottfried Wilhelm Leibniz তাঁর পদ্ধতি প্রথম ছাপেন ১৬৮৪ সালে। পরে তীব্র অগ্রাধিকার-বিতর্ক শুরু হয়; ১৭১২ সালে লন্ডনের Royal Society একটি প্রতিবেদনে Newton-এর পক্ষ নেয়, যদিও সেই প্রতিবেদনের পেছনে Newton নিজেই প্রভাব খাটিয়েছিলেন বলে পরে জানা যায়। আজকের ইতিহাসবিদেরা সাধারণভাবে মনে করেন, দুজনেই স্বাধীনভাবে আবিষ্কার করেছিলেন।
দুজনের চিহ্ন ছিল আলাদা, আর এই পার্থক্যের প্রভাব পড়েছিল অনেক দূর। Newton লিখতেন অক্ষরের মাথায় বিন্দু দিয়ে (ẋ), Leibniz লিখতেন dy/dx, যেন দুটি ছোট পরিবর্তনের ভাগফল। Leibniz-এর চিহ্নে chain rule প্রায় নিজে থেকেই চোখে পড়ে: dy/dx = (dy/du)(du/dx), যেন du কেটে যায়। ইংল্যান্ডের গণিতবিদেরা জাতীয় গর্বে দীর্ঘদিন Newton-এর চিহ্ন আঁকড়ে ছিলেন, আর অনেক ইতিহাসবিদের মতে এতে মহাদেশীয় ইউরোপের তুলনায় তাঁরা কিছুটা পিছিয়ে পড়েন; উনিশ শতকের শুরুতে কেমব্রিজের কয়েকজন তরুণ গণিতবিদ (Charles Babbage তাঁদের একজন) Leibniz-এর চিহ্ন চালুর আন্দোলন করেন। পরে Lagrange-এর f′(x) চিহ্নও প্রচলিত হয়, যা এই পর্বেও ব্যবহার করেছি।
AI-কোডে এই উত্তরাধিকার সরাসরি দেখা যায়। PyTorch বা JAX-এর মতো automatic differentiation library-র নকশা মূলত Leibniz-এর ভাবনা: প্রতিটি ছোট হিসাবকে আলাদা “ধাপ” ধরে তাদের স্থানীয় derivative গুণ করা। কোডে ভেরিয়েবলের নাম dL_dw রাখার অভ্যাসও সেই চিহ্ন থেকে। তবে মনে রাখবেন, Newton বা Leibniz কেউই limit-এর আধুনিক কঠোর সংজ্ঞা দেননি; সেটি এসেছে উনিশ শতকে Cauchy আর Weierstrass-এর হাতে।
মূল উপলব্ধি: ভালো চিহ্ন শুধু লেখার সুবিধা নয়, ভাবনার হাতিয়ার; dy/dx-এর মতো একটি চিহ্ন chain rule-কে স্বাভাবিক করে তুলেছিল, আর আজ সেই chain rule-ই কোটি parameter-এর মডেল শেখায়।
প্র ০২১৮৪৭ সালের একটি জ্যোতির্বিদ্যার হিসাবের পদ্ধতি কীভাবে আধুনিক deep learning-এর প্রধান হাতিয়ার হলো?
Gradient descent-এর প্রথম স্পষ্ট বর্ণনার কৃতিত্ব সাধারণত দেওয়া হয় ফরাসি গণিতবিদ Augustin-Louis Cauchy-কে, ১৮৪৭ সালে প্যারিসের বিজ্ঞান একাডেমিতে উপস্থাপিত একটি ছোট লেখার জন্য। তাঁর সমস্যা ছিল জ্যোতির্বিদ্যার: গ্রহ বা ধূমকেতুর কক্ষপথের সমীকরণ মেলানো, যেখানে অনেক অজানা একসঙ্গে। তাঁর প্রস্তাব ছিল আজকের নিয়মের মতোই: একটি ফলকে ছোট করতে প্রতিবার সবচেয়ে খাড়া উতরাইয়ের দিকে একটু সরানো। গণিতের ইতিহাস নিয়ে লেখা কিছু প্রবন্ধে উল্লেখ আছে যে Jacques Hadamard ১৯০৭ সালে স্বাধীনভাবে প্রায় একই পদ্ধতি বর্ণনা করেন; তাই “আবিষ্কারক” একজন নয় বলাই নিরাপদ।
মজার ব্যাপার, বিশ শতকের সংখ্যাগত গণিতে সাধারণ gradient descent-কে প্রায়ই একটু দুর্বল পদ্ধতি হিসেবে দেখা হতো। Newton-এর পদ্ধতি loss-এর বাঁকও (দ্বিতীয় derivative) ব্যবহার করে, তাই তলার কাছে অনেক কম ধাপে পৌঁছায়; conjugate gradient-এর মতো পদ্ধতি অংশ ৮-এর লম্বাটে বাটিতে অনেক ভালো করে। কিন্তু এদের প্রতিটি ধাপ ব্যয়বহুল: n-টি parameter থাকলে বাঁকের matrix-এ n² ঘর। কোটি parameter-এ n² ঘর রাখাই অসম্ভব।
এখানেই সাধারণ gradient descent জেতে। প্রতি ধাপে লাগে শুধু gradient, n-টি সংখ্যা, যা backpropagation প্রায় একটি forward pass-এর খরচে দেয়। আর mini-batch দিয়ে প্রতিটি ধাপ আরও সস্তা হয়। ফলে বিশাল মডেলে “অনেক সস্তা ধাপ” “অল্প দামি ধাপ”-এর চেয়ে বাস্তবে দ্রুত। আজকের জনপ্রিয় optimizer-গুলো (momentum, RMSProp, Kingma আর Ba-র ২০১৪ সালে প্রস্তাবিত Adam) মূলত gradient descent-এরই উন্নত রূপ: আগের ধাপের দিক মনে রাখা, বা প্রতিটি parameter-এর জন্য আলাদা পায়ের মাপ। কোনটি “সেরা”, তা নিয়ে গবেষকদের মধ্যে এখনো মতভেদ আছে, আর ফল প্রায়ই সমস্যার ওপর নির্ভর করে।
মূল উপলব্ধি: কোনো পদ্ধতির “ভালো” হওয়া নির্ভর করে সমস্যার মাপের ওপর; ছোট সমস্যায় যা ধীর ও সাধারণ, কোটি parameter-এর জগতে সেটিই একমাত্র সাশ্রয়ী পথ হয়ে উঠতে পারে।
প্র ০৩Gradient-এর আন্দাজ গোলমেলে হলেও SGD কেন কাজ করে? Batch-এর মাপ নিয়ে বিতর্কটি কী?
SGD-র তাত্ত্বিক শিকড় সাধারণত ধরা হয় Herbert Robbins আর Sutton Monro-র ১৯৫১ সালের প্রবন্ধ “A Stochastic Approximation Method”-কে। তাঁদের প্রশ্ন ছিল: যখন কোনো function-এর মান শুধু গোলমালসহ মাপা যায়, তখনো কি তার শূন্য খুঁজে পাওয়া সম্ভব? উত্তর ছিল হ্যাঁ, যদি পায়ের মাপ ধীরে ধীরে কমানো হয় একটি নির্দিষ্ট শর্ত মেনে: সব পায়ের যোগফল অসীম হবে (যাতে যতদূর দরকার যাওয়া যায়), কিন্তু পায়ের বর্গের যোগফল সসীম (যাতে গোলমাল শেষে মিলিয়ে যায়)। অংশ ৯-এর run-এ SGD-র তলার চারপাশে কাঁপাকাঁপি ঠিক এই কারণেই; η স্থির রাখা হয়েছিল।
মূল যুক্তিটি সহজ। একটি এলোমেলো সারির gradient গড়ে পুরো ডেটার gradient-এর সমান (গণিতের ভাষায় unbiased), শুধু ছড়ানো বেশি। আর অনেক সারি প্রায়ই একই রকম তথ্য দেয়: বাজারঘরের ৪০০টি অর্ডারের মধ্যে ৩২টি দেখলেই রেখার দিক মোটামুটি বোঝা যায়। তাই পুরো ডেটা পড়ার আগেই পা ফেলা লাভজনক। Léon Bottou-র মতো গবেষকেরা ২০০০-এর দশকে বড় ডেটায় SGD-র এই সুবিধা জোর দিয়ে দেখিয়েছিলেন।
Batch-এর মাপ নিয়ে বিতর্ক চলছে বহু বছর। বড় batch GPU-কে ভালোভাবে ব্যস্ত রাখে, তাই প্রতি সেকেন্ডে বেশি সারি; কিন্তু ২০১৭ সালের আশপাশে Keskar ও সহকর্মীদের একটি প্রবন্ধসহ কয়েকটি কাজ দেখায় যে খুব বড় batch-এ প্রশিক্ষিত মডেল কখনো নতুন ডেটায় খারাপ করে, আর একটি ব্যাখ্যা হিসেবে loss-এর “তীক্ষ্ণ” বনাম “চ্যাপ্টা” তলার ধারণা প্রস্তাব করা হয়। আবার অন্য গবেষকেরা দেখান, learning rate আর batch একসঙ্গে ঠিকভাবে বাড়ালে এই পার্থক্যের অনেকটা কমানো যায়। তাই “ছোট batch-এর গোলমাল উপকারী” কথাটি আংশিক সত্য, সর্বজনীন নিয়ম নয়।
মূল উপলব্ধি: নিখুঁত দিকের জন্য অপেক্ষা না করে গোলমেলে কিন্তু সস্তা দিকে অনেকবার হাঁটা বড় ডেটায় প্রায়ই বেশি লাভের; কতটা গোলমাল সহনীয়, তা learning rate আর batch-এর মাপ মিলিয়ে ঠিক করতে হয়।
প্র ০৪Backpropagation কে আবিষ্কার করেছেন? ১৯৮৬ সালের প্রবন্ধটি কেন এত বিখ্যাত, যদি ধারণাটি আগেই ছিল?
Backpropagation-এর ইতিহাস “একজন আবিষ্কারক” গল্পের সঙ্গে মেলে না। গণিতের দিক থেকে এটি reverse-mode automatic differentiation, আর ফিনিশ গবেষক Seppo Linnainmaa ১৯৭০ সালে তাঁর master's thesis-এ এর আধুনিক রূপ বর্ণনা করেন বলে উল্লেখ করা হয়। নিয়ন্ত্রণ-প্রকৌশলে (control theory) ১৯৬০-এর দশকেই Henry Kelley, Arthur Bryson প্রমুখের কাজে অনুরূপ পেছনমুখী হিসাব ছিল। Paul Werbos ১৯৭৪ সালের PhD থিসিসে neural network-এ এটি খাটানোর প্রস্তাব দেন, কিন্তু তখন তা বেশি মনোযোগ পায়নি।
১৯৮৬ সালে David Rumelhart, Geoffrey Hinton আর Ronald Williams-এর Nature-এ প্রকাশিত প্রবন্ধ “Learning representations by back-propagating errors” পরিস্থিতি বদলে দেয়। এর গুরুত্ব নতুন গণিতে নয়, বরং প্রদর্শনে: তাঁরা দেখান যে hidden layer-সহ network এই পদ্ধতিতে নিজে থেকে কাজের অভ্যন্তরীণ উপস্থাপন (representation) শিখতে পারে, যা perceptron-এর সীমা নিয়ে আগের হতাশা কাটাতে সাহায্য করে। একই সময়ে প্রকাশিত “Parallel Distributed Processing” বইগুলো ধারণাটি ব্যাপক পাঠকের কাছে পৌঁছে দেয়। Jürgen Schmidhuber-এর মতো কয়েকজন গবেষক বহু বছর ধরে যুক্তি দিয়েছেন যে আগের অবদানগুলো যথেষ্ট স্বীকৃতি পায়নি; এ নিয়ে মতভেদ এখনো আছে।
প্রকৌশলের দিক থেকে reverse mode কেন এত কার্যকর, তা অংশ ৬-এর stepper থেকেই বোঝা যায়। একটি loss (একটি সংখ্যা) আর অনেক parameter থাকলে পেছন থেকে একবার হাঁটলেই সব parameter-এর gradient মেলে, মোট খরচ মোটামুটি forward pass-এর একটি ছোট গুণিতক। Finite difference দিয়ে করলে প্রতিটি parameter-এর জন্য আলাদা করে loss হিসাব লাগত: কোটি parameter-এ কোটি গুণ কাজ। বিনিময়ে reverse mode-কে forward pass-এর সব মধ্যবর্তী মান মেমরিতে রাখতে হয়, আর বড় মডেলে এই মেমরিই প্রায়ই সবচেয়ে বড় বাধা।
মূল উপলব্ধি: বিজ্ঞানে একটি ধারণার আবিষ্কার, তার সঠিক প্রয়োগ আর তার জনপ্রিয় হওয়া তিনটি আলাদা ঘটনা হতে পারে; backpropagation-এর গণিত সহজ chain rule, কিন্তু তাকে কাজের হাতিয়ার বানাতে কয়েক দশক আর অনেক মানুষ লেগেছে।
প্র ০৫Neural network-এর loss তো non-convex; তবু gradient descent কেন এত ভালো কাজ করে? Local minimum কি সত্যিই বড় সমস্যা?
১৯৯০-এর দশকে অনেক গবেষক neural network নিয়ে সন্দিহান ছিলেন, আর একটি বড় যুক্তি ছিল অংশ ৯-এর ছবিটি: loss non-convex, তাই gradient descent কোনো খারাপ local minimum-এ আটকে যাবে, আর কোনো নিশ্চয়তা নেই। Support vector machine-এর মতো পদ্ধতির convex loss তখন এক বড় আকর্ষণ ছিল, কারণ সেখানে সেরা উত্তর নিশ্চিত।
২০১৪ সালে Yann Dauphin ও সহকর্মীদের একটি প্রবন্ধ (“Identifying and attacking the saddle point problem in high-dimensional non-convex optimization”) একটি ভিন্ন ছবি তুলে ধরে। অনেক মাত্রায় একটি বিন্দু local minimum হতে হলে সব দিকে তাকে তলা হতে হয়; এলোমেলো ধরনের function-এ মাত্রা যত বাড়ে, এমন হওয়ার সম্ভাবনা তত কমে, আর কিছু দিকে চড়াই কিছু দিকে উতরাই, মানে saddle point, অনেক বেশি সাধারণ হয়। তাঁদের যুক্তি অনুযায়ী উঁচু loss-এর জায়গায় যে বিন্দুগুলোতে ঢাল শূন্য, সেগুলো মূলত saddle, আর যে local minimum-গুলো আছে সেগুলোর loss প্রায়ই global minimum-এর কাছাকাছি। একই সময়ে Choromanska ও সহকর্মীরা অন্য পথে অনুরূপ সিদ্ধান্তে পৌঁছান। এগুলো সরলীকৃত মডেলের ওপর ভিত্তি করে, তাই বাস্তব network-এ কতটা খাটে, তা নিয়ে আলোচনা চলছে।
Saddle point-এ সাধারণ gradient descent ধীর হয়ে যেতে পারে (অংশ ৯-এর run-এ (1, 0) থেকে শুরু করা পথ জিনের মাঝে প্রায় থেমে গিয়েছিল), কিন্তু SGD-র গোলমাল আর momentum প্রায়ই তাকে ঠেলে বের করে দেয়, যেমন 10−6-এর ছোট্ট ধাক্কা দিয়েছিল। Hao Li ও সহকর্মীদের ২০১৮ সালের কাজের মতো কিছু গবেষণা loss-এর পৃষ্ঠকে নিচু মাত্রায় এঁকে দেখিয়েছে যে network-এর নকশা (যেমন skip connection) পৃষ্ঠকে মসৃণ বা এবড়োখেবড়ো করতে পারে। তবু বলা নিরাপদ: কেন deep network-এর training এত ভালো কাজ করে, তার পূর্ণ তাত্ত্বিক ব্যাখ্যা এখনো নেই।
মূল উপলব্ধি: নিচু মাত্রার ছবি থেকে পাওয়া অন্তর্দৃষ্টি (গর্তে আটকে যাওয়া) বহু মাত্রায় পুরোপুরি খাটে না; ব্যবহারিক সাফল্য আর তাত্ত্বিক নিশ্চয়তা এক নয়, আর এই ফাঁকটি স্বীকার করাই সৎ বিজ্ঞান।
অনুশীলন
প্রথমটি হাতে হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লেখার।
-
হাতে হিসাব: (ক) f(x) = 2x³ − 5x + 1-এর derivative বের করুন, আর x = 2-এ মান কত? (খ) h = 0.1 নিয়ে x = 2-এ forward আর central difference হিসাব করুন; কোনটির ভুল কম? (গ) L(w1, w2) = (w1 − 1)² + 2(w2 + 2)². বিন্দু (3, 0)-এ gradient কত, আর η = 0.1 দিয়ে এক ধাপ gradient descent-এর পরে বিন্দু ও loss কত?
(ক) f′(x) = 6x² − 5 (power, constant আর sum rule). x = 2-এ 6 × 4 − 5 = 19.
(খ) f(2.1) = 2 × 9.261 − 10.5 + 1 = 9.022; f(2) = 16 − 10 + 1 = 7; f(1.9) = 2 × 6.859 − 9.5 + 1 = 5.218. Forward = (9.022 − 7) ÷ 0.1 = 20.22, ভুল 1.22. Central = (9.022 − 5.218) ÷ 0.2 = 19.02, ভুল 0.02. Central প্রায় ৬০ গুণ নির্ভুল, কারণ তার truncation error h²-এর সমানুপাতিক।
(গ) ∂L/∂w1 = 2(w1 − 1) = 4, ∂L/∂w2 = 4(w2 + 2) = 8; ∇L = (4, 8). নতুন বিন্দু = (3, 0) − 0.1 × (4, 8) = (2.6, −0.8). Loss আগে 2² + 2 × 2² = 12, পরে 1.6² + 2 × 1.2² = 2.56 + 2.88 = 5.44.
-
আউটপুট অনুমান করুন: চালানোর আগে ছয়টি লাইনের আউটপুট লিখুন, তারপর যেকোনো Python-এ চালিয়ে মিলিয়ে দেখুন। কেন এমন হলো, ব্যাখ্যা করুন।
for eta in (0.5, 1.0): w = 0.0 for _ in range(3): w = w - eta * 2 * (w - 3) # L(w) = (w - 3)**2 print(eta, w)0.5 3.0 0.5 3.0 0.5 3.0 1.0 6.0 1.0 0.0 1.0 6.0তলা থেকে দূরত্ব প্রতি ধাপে (1 − 2η) দিয়ে গুণ হয়। η = 0.5-এ গুণক 0: প্রথম পায়েই ঠিক তলায় (0 − 0.5 × 2 × (−3) = 3), তারপর ঢাল 0, তাই আর নড়ে না। এই loss-এর জন্য এটি “নিখুঁত” η, কারণ এখানে বাঁক সব জায়গায় সমান (L″ = 2), আর η = 1 ÷ L″. η = 1.0-এ গুণক −1: w চিরকাল 0 আর 6-এর মধ্যে দোলে, loss 9-এই থাকে। (ডেস্কটপে Python 3.14.4-এ চালিয়ে মেলানো।)
-
নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) ডেলিভারির দিন অনুমানের জন্য নিচের training loop লিখেছেন। অন্তত চারটি সমস্যা চিহ্নিত করুন আর ভালো একটি পথ বলুন।
x = distance_km # 15 থেকে 350 w, b = 0.0, 0.0 for t in range(10): r = w * x + b - y w += 0.1 * np.mean(r * x) b += 0.1 * np.mean(r) print("train MSE:", np.mean((w * x + b - y) ** 2))- চিহ্ন উল্টো:
+=মানে gradient-এর দিকে, চড়াইয়ে হাঁটা; হতে হবেw -= …. সঙ্গে gradient-এ 2 গুণও বাদ পড়েছে (এটি শুধু η-এর মাপ বদলায়, তবে gradient checking-এ ধরা পড়বে)। - Scaling নেই: x কিলোমিটারে, শত শত; η = 0.1 এই বাঁকে বিশাল, তাই চিহ্ন ঠিক করলেও অংশ ৮-এর run-এর মতো কয়েক ধাপে loss বিস্ফোরিত হবে। x-কে ১০০ কিমি এককে বা standardize করে নিন।
- থামার নিয়ম দুর্বল: ঠিক ১০ ধাপ, loss কমছে কি না,
nanহয়েছে কি না কিছুই দেখা হয়নি। সর্বোচ্চ ধাপ, gradient-এর norm আর loss বাড়লে থামার শর্ত যোগ করুন, আর মাঝে মাঝে loss ছাপান। - ভুল জায়গায় মাপ: শুধু train MSE; নতুন অর্ডারে কেমন করবে তা জানতে আলাদা validation/test ডেটা লাগবে, আর একটি baseline (সব অর্ডারে গড় দিন) ও
lstsq-এর সঙ্গে তুলনা দরকার। - ভালো পথ: এত সরল মডেলে সরাসরি
np.linalg.lstsq-ই যথেষ্ট; gradient descent শিখতে চাইলে সেই উত্তরকে মাপকাঠি রাখুন।
- চিহ্ন উল্টো:
-
কোড লিখুন: বাজারঘরের ৮টি অর্ডারের দিন
y = [1, 1, 2, 2, 3, 3, 4, 5]. সবচেয়ে সরল মডেল নিন: দূরত্ব না দেখে সব অর্ডারে একই অনুমান, দিন ≈ b. MSE-কে b-এর সাপেক্ষে কমাতে gradient descent লিখুন (η = 0.1, ৫০ ধাপ), শেষ b ছাপান আরy.mean()-এর সঙ্গে তুলনা করুন। ফলটি থেকে কী শিখলেন?import numpy as np y = np.array([1, 1, 2, 2, 3, 3, 4, 5], dtype=float) b = 0.0 for t in range(50): b -= 0.1 * 2 * np.mean(b - y) # dMSE/db = 2·mean(b − y) print(round(b, 4), y.mean())ডেস্কটপে চালিয়ে পাওয়া ফল:
2.625 2.625. Gradient 2·mean(b − y) শূন্য হয় ঠিক তখন, যখন b = y-এর গড় = 21 ÷ 8 = 2.625. অর্থাৎ বর্গ-ভুল (MSE) সবচেয়ে কম করে গড়; এটিই সেই baseline, যার চেয়ে ভালো না করলে কোনো মডেলের দাম নেই। এর MSE = y-এর variance = 13.875 ÷ 8 ≈ 1.73 (পর্ব ৩৭-এ variance আসবে), যেখানে অংশ ৮-এর রেখার MSE ছিল 0.053.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমGradient descent দিয়ে একটি মডেল fit করার এক পাকের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🎲 Parameter-এর শুরুর মান ঠিক করা (যেমন w = 0, b = 0)
- 📈 এখনকার parameter দিয়ে সব সারির অনুমান হিসাব
- 📏 অনুমান আর আসল মান থেকে loss (যেমন MSE) হিসাব
- 🧭 Chain rule দিয়ে loss-এর gradient বের করা
- 👣 w ← w − η·∇L দিয়ে parameter বদলানো
- 🛑 থামার নিয়ম দেখা: convergence, সর্বোচ্চ ধাপ বা validation loss
f(x) = 4x³ হলে x = 1-এ derivative f′(1) কত?
- 4
- 3
- 12
- 16
Gradient descent-এ এখন w = 2, এই বিন্দুতে dL/dw = 5, আর learning rate η = 0.1. এক ধাপ পরে w কত?
- 2.5
- 1.5
- −3
- 2 (বদলাবে না)
L(w) = (w − 3)², শুরু w = 0. Learning rate η = 1.05 দিয়ে gradient descent চালালে কী ঘটে?
- ধীরে ধীরে w = 3-এর দিকে যায়
- প্রথম ধাপেই ঠিক w = 3-এ পৌঁছায়
- 0 আর 6-এর মধ্যে চিরকাল দোলে
- তলার দুই পাশে লাফাতে লাফাতে দূরে সরে যায়, loss বাড়ে
দুই চলকের একটি loss L(w1, w2)-এর কোনো বিন্দুতে gradient ∇L কোন দিক দেখায়?
- যেদিকে L সবচেয়ে দ্রুত বাড়ে
- যেদিকে L সবচেয়ে দ্রুত কমে
- Contour রেখা বরাবর, যেদিকে L বদলায় না
- সরাসরি global minimum-এর দিকে
float64-এ finite difference [f(x + h) − f(x)] ÷ h দিয়ে derivative আন্দাজের সময় h = 10−15 নিলে ভুল সাধারণত খুব বড় হয়। প্রধান কারণ কী?
- Truncation error বেড়ে যায়
- প্রায় সমান দুটি সংখ্যার বিয়োগে rounding error বড় হয়ে যায়
- Python ZeroDivisionError দেয়
- Forward difference ছোট h-এ সবসময় নিখুঁত হয়
y = (2x + 1)³ হলে chain rule দিয়ে x = 1-এ dy/dx কত?
- 9
- 27 (শুধু বাইরের ঢাল)
- 54
- 6
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- শেখা মানে loss কমানো; derivative হলো একটি বিন্দুতে curve-এর ঢাল (secant-কে কাছে আনলে tangent), তার চিহ্ন দিক বলে, মাপ বলে খাড়া কতটা।
- নিয়ম: xⁿ → n·xⁿ⁻¹, ধ্রুবক গুণ আর যোগ আলাদা আলাদা, eˣ → eˣ, ln x → 1/x; আর chain rule: dy/dx = (dy/du)(du/dx), backpropagation-এর ভিত্তি।
- Finite difference-এ h খুব ছোট করবেন না: বড় h-এ truncation, ছোট h-এ rounding error; float64-এ central difference সাধারণত সেরা, h প্রায় 10−5.
- Gradient = সব partial derivative-এর vector, সবচেয়ে খাড়া চড়াই; gradient descent চলে উল্টো দিকে:
w ← w − η·∇L. η খুব ছোট হলে ধীর, খুব বড় হলে diverge; feature scaling η-কে নিরাপদ রাখে। - বাস্তবে: non-convex loss-এ local minimum আর saddle, বড় ডেটায় mini-batch, থামার নিয়ম (validation loss), আর নিজের লেখা gradient-এ gradient checking; বড় মডেলে library automatic differentiation করে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৫ · AI-এর জন্য সম্ভাবনা — event, conditional আর independenceপরবর্তী পর্বঅনিশ্চয়তা মাপার ভাষা।
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ১৭ · NumPy-তে linear algebraঝালাইdot product, norm আর
lstsq, যার সঙ্গে এই পর্বের রেখা মিলেছে। - Computer Architecture · IEEE 754 floating pointগভীরে যানfloat-এ কেন শেষ অঙ্কে গোলমাল, আর finite difference-এর rounding error কোথা থেকে।
- DSA · Math for DSA — logarithm, series আর countingপাশের ট্র্যাকঅ্যালগরিদমের হিসাবে দরকারি log আর ধারার গণিত।