পাঠ ২২ · ৩৫-এর মধ্যে · মডিউল ৫
Home / AI Courses / Math for AI & ML / অটোমেটিক ডিফারেনশিয়েশন

অটোমেটিক ডিফারেনশিয়েশন — PyTorch autograd কীভাবে কাজ করে

Automatic differentiation — how PyTorch autograd works
১৩ মিনিট পড়া উন্নত · Advanced NumPy কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ফরওয়ার্ড-মোড ও রিভার্স-মোড অটোমেটিক ডিফারেনশিয়েশনের পার্থক্য
  • কেন রিভার্স মোড ML-এর জন্য বিশেষভাবে উপযুক্ত — এবং এটি আসলে পাঠ ২০-এর ব্যাকপ্রপাগেশনেরই একটি সাধারণ নাম
  • PyTorch autograd ভেতরে কীভাবে কাজ করে — ডাইনামিক গ্রাফ, requires_grad, .backward()
  • পাঠ ২১-এর নেটওয়ার্কেই PyTorch autograd প্রয়োগ করে দেখা এবং একটি NumPy-ভিত্তিক স্বাধীন যাচাই

১ · ফরওয়ার্ড-মোড বনাম রিভার্স-মোড অটোমেটিক ডিফারেনশিয়েশন

অটোমেটিক ডিফারেনশিয়েশনAutomatic Differentiation (AD)একটি প্রোগ্রামের গণনাকে প্রাথমিক অপারেশনে ভেঙে, প্রতিটি ধাপে চেইন রুল যান্ত্রিকভাবে প্রয়োগ করে সঠিক (সিম্বলিক-নির্ভুল) ডেরিভেটিভ বের করার কৌশল — সিম্বলিক ডিফারেনশিয়েশন বা finite difference-এর বিকল্প। — কোনো কম্পিউটেশনাল গ্রাফে (পাঠ ১৯) চেইন রুল যান্ত্রিকভাবে প্রয়োগ করে সঠিক ডেরিভেটিভ বের করার একটি সাধারণ কৌশল। এটি দুই দিকে করা যায়:

ফরওয়ার্ড-মোড AD ইনপুট থেকে শুরু করে প্রতিটি নোডের সাথে সাথে তার ডেরিভেটিভও (একটি নির্দিষ্ট ইনপুট ভ্যারিয়েবলের সাপেক্ষে) ফরওয়ার্ড দিকেই বহন করে নিয়ে চলে। একবারের পাসে এটি একটি ইনপুটের সাপেক্ষে সব আউটপুটের ডেরিভেটিভ দেয়।

রিভার্স-মোড AD প্রথমে সম্পূর্ণ ফরওয়ার্ড পাস চালিয়ে সব মান ক্যাশ করে (পাঠ ১৯), তারপর আউটপুট থেকে ইনপুটের দিকে ফিরে গিয়ে চেইন রুল প্রয়োগ করে। একবারের ব্যাকওয়ার্ড পাসে এটি একটি আউটপুটের সাপেক্ষে সব ইনপুটের ডেরিভেটিভ দেয় — এটাই ঠিক পাঠ ২০-তে আমরা হাতে যা করেছি।

২ · রিভার্স মোড = ব্যাকপ্রপাগেশন — এবং কেন এটাই ML-এর জন্য উপযুক্ত

একটি নেটওয়ার্কে যদি $n$টি ইনপুট (প্যারামিটার) ও $m$টি আউটপুট থাকে, তাহলে:

  • ফরওয়ার্ড-মোড AD-তে সম্পূর্ণ গ্রেডিয়েন্ট তথ্য পেতে $n$ বার পাস লাগে (প্রতি ইনপুটের জন্য একবার)।
  • রিভার্স-মোড AD-তে $m$ বার পাস লাগে (প্রতি আউটপুটের জন্য একবার)।

একটি নিউরাল নেটওয়ার্কে $n$ (প্যারামিটার সংখ্যা, লক্ষ থেকে বিলিয়ন) $m$ (আউটপুট — সাধারণত একটি স্কেলার লস, $m=1$) থেকে বিশাল ব্যবধানে বেশি। তাই রিভার্স মোড ব্যবহার করলে একটিমাত্র ব্যাকওয়ার্ড পাসে সব প্যারামিটারের গ্রেডিয়েন্ট একসাথে পাওয়া যায় — এটাই পাঠ ২০-২১-এ আমরা ঠিক যা করেছি, এবং এই কারণেই রিভার্স-মোড AD (= ব্যাকপ্রপাগেশন) আধুনিক ডিপ লার্নিংয়ে ব্যবহৃত হয়, ফরওয়ার্ড-মোড নয়।

মূল কথা · Key takeaway

"ব্যাকপ্রপাগেশন" ও "রিভার্স-মোড অটোমেটিক ডিফারেনশিয়েশন" — দুটো নাম আসলে একই জিনিস বোঝায়। পাঠ ১৯-২১-এ আমরা হাতে যা করেছি, PyTorch/TensorFlow ঠিক সেটাই স্বয়ংক্রিয়ভাবে, যেকোনো আকারের গ্রাফের জন্য সাধারণীকৃতভাবে করে।

৩ · PyTorch autograd ভেতরে কীভাবে কাজ করে

একটি টেনসরে requires_grad=True সেট করলে PyTorch সেই টেনসরের উপর ঘটা প্রতিটি অপারেশন ট্র্যাক করতে শুরু করে — ঠিক পাঠ ১৯-এর মতো একটি কম্পিউটেশনাল গ্রাফ তৈরি হয়, কিন্তু এটি ডাইনামিক: গ্রাফটি আগে থেকে ঘোষণা করতে হয় না, বরং কোড চলার সাথে সাথে (লাইন বাই লাইন) নিজে থেকেই তৈরি হয়।

ফরওয়ার্ড পাস শেষে চূড়ান্ত স্কেলার আউটপুটে (আমাদের ক্ষেত্রে $L$) .backward() কল করলে PyTorch স্বয়ংক্রিয়ভাবে সেই গ্রাফের উপর দিয়ে ব্যাকওয়ার্ড দিকে হাঁটে, প্রতিটি নোডে তার লোকাল গ্রেডিয়েন্ট গণনা করে আপস্ট্রিম গ্রেডিয়েন্টের সাথে গুণ করে (ঠিক পাঠ ২০-এর প্যাটার্ন), এবং প্রতিটি লিফ টেনসরের (যেমন $w_1, b_1$) .grad অ্যাট্রিবিউটে চূড়ান্ত গ্রেডিয়েন্ট জমা রাখে।

একটি টেপ-রেকর্ডারের কথা ভাবুন — requires_grad=True চালু থাকলে PyTorch প্রতিটি অপারেশন "রেকর্ড" করতে থাকে (কোন ইনপুট থেকে কোন অপারেশন দিয়ে কোন আউটপুট এলো)। .backward() কল করা মানে সেই টেপ উল্টো দিকে "রিওয়াইন্ড" করে চালানো — প্রতিটি রেকর্ড করা অপারেশনের লোকাল গ্রেডিয়েন্ট প্রয়োগ করে পেছনের দিকে ফেরত যাওয়া।

৪ · PyTorch কোড — পাঠ ২১-এর নেটওয়ার্কে autograd প্রয়োগ (illustrative)

নিচের কোড পাঠ ২১-এর ঠিক একই নেটওয়ার্ক ও সংখ্যা ব্যবহার করে, কিন্তু গ্রেডিয়েন্ট হাতে না বসিয়ে PyTorch-কে autograd দিয়ে সেগুলো বের করতে দেয়। এই সাইটের কোড-রানার শুধু NumPy সমর্থন করে (Pyodide-ভিত্তিক), তাই এই সেলটি চালানো যাবে না — শুধু পড়ে বোঝার জন্য।

Python · PyTorch (illustrative)
import torch

x1, x2 = torch.tensor(1.0), torch.tensor(2.0)
y = torch.tensor(0.0)

# requires_grad=True মানে PyTorch এই টেনসরগুলোর সাপেক্ষে গ্রেডিয়েন্ট ট্র্যাক করবে
w1 = torch.tensor(0.5, requires_grad=True)
w2 = torch.tensor(-0.5, requires_grad=True)
b1 = torch.tensor(0.5, requires_grad=True)
w3 = torch.tensor(2.0, requires_grad=True)
b2 = torch.tensor(0.0, requires_grad=True)

# ---- ফরওয়ার্ড পাস: PyTorch নিজে থেকেই গ্রাফ তৈরি করছে ----
z1 = w1 * x1 + w2 * x2 + b1
h  = torch.sigmoid(z1)
yhat = w3 * h + b2
L = (yhat - y) ** 2

# ---- ব্যাকওয়ার্ড পাস: একটি কল, সব গ্রেডিয়েন্ট একসাথে ----
L.backward()

print("L =", L.item())              # আশা করি: 1.0, পাঠ ২১-এর মতো
print("dL/dw1 =", w1.grad.item())    # আশা করি: 1.0
print("dL/dw2 =", w2.grad.item())    # আশা করি: 2.0
print("dL/db1 =", b1.grad.item())    # আশা করি: 1.0
print("dL/dw3 =", w3.grad.item())    # আশা করি: 1.0
print("dL/db2 =", b2.grad.item())    # আশা করি: 2.0
লক্ষ করুন — L.backward() মাত্র একটি লাইন, অথচ পাঠ ২০-২১-এ আমরা যে পাঁচটি ধাপ হাতে লিখেছিলাম (আপস্ট্রিম × লোকাল গ্রেডিয়েন্ট, প্রতিটি নোডে) তার সবগুলোই এই একটি কলের ভেতরে স্বয়ংক্রিয়ভাবে ঘটে যায়। প্রিন্ট হওয়া প্রতিটি মান পাঠ ২১-এর হাতে-হিসাব করা টেবিলের সাথে হুবহু মেলে।

৫ · NumPy দিয়ে স্বাধীন যাচাই — এই সাইটেই চালানো যায়

যেহেতু উপরের PyTorch কোড এখানে সরাসরি চালানো যায় না, নিচে একটি সম্পূর্ণ NumPy-only কোড দেওয়া হলো যা autograd-এর দাবি করা গ্রেডিয়েন্টগুলো finite-difference (পাঠ ১৩-এর লিমিট সংজ্ঞা থেকে) দিয়ে স্বাধীনভাবে যাচাই করে — কোনো চেইন-রুল সূত্র ছাড়াই, শুধু সংজ্ঞা প্রয়োগ করে।

Python · NumPy
import numpy as np

x1, x2 = 1.0, 2.0
y = 0.0

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))

def loss(w1, w2, b1, w3, b2):
    z1 = w1 * x1 + w2 * x2 + b1
    h = sigmoid(z1)
    yhat = w3 * h + b2
    return (yhat - y) ** 2

# পাঠ ১৯-২১-এর একই প্যারামিটার
params = dict(w1=0.5, w2=-0.5, b1=0.5, w3=2.0, b2=0.0)

# ---- finite-difference দিয়ে প্রতিটি প্যারামিটারের গ্রেডিয়েন্ট আনুমানিক করা ----
eps = 1e-6
base = loss(**params)
fd_grad = {}
for name in params:
    p = dict(params)
    p[name] = p[name] + eps
    fd_grad[name] = (loss(**p) - base) / eps

# ---- পাঠ ২১-এর হাতে-করা বন্ধ-রূপ (analytical) গ্রেডিয়েন্ট, তুলনার জন্য ----
w1, w2, b1, w3, b2 = params["w1"], params["w2"], params["b1"], params["w3"], params["b2"]
z1 = w1 * x1 + w2 * x2 + b1
h = sigmoid(z1)
yhat = w3 * h + b2
dL_dyhat = 2 * (yhat - y)
analytical = {
    "w3": dL_dyhat * h,
    "b2": dL_dyhat * 1.0,
}
dL_dh = dL_dyhat * w3
dL_dz1 = dL_dh * h * (1 - h)
analytical["w1"] = dL_dz1 * x1
analytical["w2"] = dL_dz1 * x2
analytical["b1"] = dL_dz1 * 1.0

print("প্যারামিটার | finite-difference | analytical (পাঠ ২১)")
for name in params:
    print(f"  {name}: {fd_grad[name]:.6f}  vs  {analytical[name]:.6f}")

    
একটি সাধারণ ভুল ধারণা — মনে করা যে .backward() প্রতিবার কল করলে গ্রেডিয়েন্ট নতুন করে বসে যায়। বাস্তবে PyTorch ডিফল্টভাবে .grad-এ নতুন গ্রেডিয়েন্ট যোগ (accumulate) করে — তাই বাস্তব ট্রেনিং লুপে প্রতিটি ইটারেশনের আগে optimizer.zero_grad() কল করে পুরনো গ্রেডিয়েন্ট মুছে ফেলা আবশ্যক, নয়তো একাধিক ব্যাচের গ্রেডিয়েন্ট ভুলভাবে জমা হয়ে যাবে।

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

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

প্র ০১ কেন রিভার্স-মোড AD ফরওয়ার্ড-মোডের চেয়ে ML-এর জন্য এত বেশি efficient?

কারণ ML মডেলে প্যারামিটার সংখ্যা ($n$, লক্ষ থেকে বিলিয়ন) সবসময় আউটপুট সংখ্যার ($m$, সাধারণত $1$ — একটি স্কেলার লস) চেয়ে বিশাল ব্যবধানে বেশি। রিভার্স মোডের খরচ নির্ভর করে $m$-এর উপর ($m$ বার পাস), ফরওয়ার্ড মোডের খরচ নির্ভর করে $n$-এর উপর। $m=1$ হলে রিভার্স মোড মাত্র একটি ব্যাকওয়ার্ড পাসে সব গ্রেডিয়েন্ট দেয় — এখানেই বিশাল সাশ্রয়।

প্র ০২ PyTorch-এ কোনো টেনসরে requires_grad=True সেট না করলে কী হবে?

সেই টেনসরের উপর ঘটা অপারেশনগুলো কম্পিউটেশনাল গ্রাফে রেকর্ড হবে না, ফলে .backward() কল করলে সেই টেনসরের .grad সবসময় None থাকবে — PyTorch ধরে নেয় আপনি এই ভ্যারিয়েবলের সাপেক্ষে গ্রেডিয়েন্ট চান না (যেমন ইনপুট ডেটা $x_1, x_2$, যেগুলো আমরা শেখাতে চাই না, শুধু ব্যবহার করতে চাই)।

প্র ০৩ "ডাইনামিক গ্রাফ" (PyTorch-এর style) বনাম "স্ট্যাটিক গ্রাফ" — এই পার্থক্যের ব্যবহারিক সুবিধা কী?

ডাইনামিক গ্রাফে গ্রাফটি কোড চলার সাথে সাথেই তৈরি হয় — তাই সাধারণ Python-এর if/for-এর মতো কন্ট্রোল ফ্লো সরাসরি ব্যবহার করা যায় (যেমন ইনপুটের উপর ভিত্তি করে ভিন্ন সংখ্যক লেয়ার চালানো), এবং ডিবাগ করা সহজ (সাধারণ Python এরর/print দিয়েই)। পুরনো স্ট্যাটিক-গ্রাফ পদ্ধতিতে (যেমন প্রথমদিকের TensorFlow) পুরো গ্রাফ আগে থেকে ঘোষণা করতে হতো, যা নমনীয়তা কমাত কিন্তু কিছু ক্ষেত্রে পারফরম্যান্স অপটিমাইজেশনে সুবিধা দিত।

অনুশীলন

  1. কোড চালান: উপরের NumPy code cell Run করুন এবং নিশ্চিত করুন প্রতিটি প্যারামিটারের finite-difference মান analytical মানের সাথে (প্রায়) হুবহু মেলে।

    পাঁচটি প্যারামিটারের জন্যই finite-difference ও analytical মান প্রায় অভিন্ন হওয়া উচিত (সামান্য floating-point পার্থক্যসহ) — যেমন w1: ≈1.000000 vs 1.000000, w3: ≈1.000000 vs 1.000000, ইত্যাদি।

  2. ধারণাগতভাবে উত্তর দিন: যদি একটি নেটওয়ার্কে ১টি ইনপুট কিন্তু ১০০০টি আউটপুট থাকত (যেমন multi-label classification-এর মতো), কোন মোড (ফরওয়ার্ড না রিভার্স) দ্রুততর হতো?

    এই বিশেষ ক্ষেত্রে ($n=1$ ইনপুট, $m=1000$ আউটপুট) ফরওয়ার্ড-মোড AD দ্রুততর হতো, কারণ ফরওয়ার্ড মোডের খরচ নির্ভর করে $n$-এর উপর (মাত্র ১ বার পাস), রিভার্স মোডের খরচ নির্ভর করে $m$-এর উপর (১০০০ বার পাস)। বাস্তব ML-এ এই পরিস্থিতি বিরল — প্রায় সবসময় প্যারামিটার সংখ্যাই আউটপুটের চেয়ে বহুগুণ বেশি, তাই রিভার্স মোডই ডিফল্ট থাকে।

  3. তুলনা করুন: PyTorch কোডের L.backward() লাইনটি পাঠ ২০-এর ব্যাকওয়ার্ড পাসের কোন কোন ধাপের সমতুল্য?

    এই একটি লাইন পাঠ ২০-এর সবগুলো ধাপের সমতুল্য: $\partial L/\partial \hat{y}$ থেকে শুরু করে $\partial L/\partial z_2$, $\partial L/\partial w_3$, $\partial L/\partial b_2$, $\partial L/\partial h$, $\partial L/\partial z_1$, এবং সবশেষে $\partial L/\partial w_1$, $\partial L/\partial w_2$, $\partial L/\partial b_1$ পর্যন্ত — সবকিছু PyTorch-এর গ্রাফ ইঞ্জিন অভ্যন্তরীণভাবে স্বয়ংক্রিয়ভাবে করে ফেলে।

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

পূর্ববর্তী পাঠ
সম্পূর্ণ ব্যাকপ্রপ — হাতে-কলমে ও NumPy-তে