فاز 1 · درس 5

قاعده زنجیره‌ای و مشتق‌گیری خودکار: پیاده‌سازی Autograd از صفر

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
یک چرخ‌دنده‌ی مکانیکی شیشه‌ای متصل به چندین گره متوالی که جهت چرخش آن برعکس می‌شود، روی پس‌زمینه‌ای تیره و مهندسی
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. قاعده زنجیره‌ای
  5. گراف‌های محاسباتی
  6. حالت پیش‌رو در برابر حالت پس‌رو
  7. اعداد دوگانه برای حالت پیش‌رو
  8. ساخت یک موتور Autograd
  9. عملکرد درونی PyTorch Autograd
  10. پیاده‌سازی از صفر
  11. گام 1: کلاس Value
  12. گام 2: عملیات‌های حسابی همراه با ردیابی گرادیان
  13. گام 3: گذر برگشت
  14. گام 4: افزودن عملیات‌های بیشتر برای موتور کامل
  15. گام 5: پیاده‌سازی پرسپترون چندلایه (MLP) از صفر
  16. گام 6: بررسی گرادیان
  17. گام 7: اعتبارسنجی با محاسبات دستی
  18. نحوه استفاده
  19. مقایسه با PyTorch
  20. یک عبارت محاسباتی پیچیده‌تر
  21. خروجی نهایی
  22. تمرین‌ها
  23. راهنمایی‌ها
  24. اصطلاحات کلیدی
  25. برای مطالعه بیشتر

بسیاری از توسعه‌دهندگان، فریم‌ورک‌هایی مانند PyTorch یا TensorFlow را به‌عنوان جعبه‌های سیاهی می‌بینند که با یک فراخوانی ساده‌ی backward() همه‌چیز را حل می‌کنند. اما وقتی مدلی همگرا نمی‌شود، گرادیان صفر می‌شود یا خطاهای ناآشنا در گراف محاسباتی رخ می‌دهد، نداشتن درک عمیق از سازوکار درونی موتورهای مشتق‌گیری خودکار عیب‌یابی را بسیار دشوار می‌کند.

در این درس با کنار گذاشتن تمام کتابخانه‌های سنگین، یک موتور autograd کمینه را از صفر با پایتون خالص پیاده‌سازی می‌کنید. یاد می‌گیرید چگونه قاعده زنجیره‌ای در قالب یک گراف جهت‌دار بدون دور (DAG) به کمک مرتب‌سازی توپولوژیک اجرا می‌شود و گرادیان‌ها به شیوه پس‌رو (Reverse mode) منتشر می‌گردند.

پیاده‌سازی این درس کاملاً متکی بر منطق ریاضی و ساختار داده‌های استاندارد پایتون است و تمام مراحل آن روی CPU معمولی سیستم شخصی شما اجرا می‌شود، بنابراین برای دنبال‌کردن آن هیچ نیازی به سرور ابری یا سخت‌افزارهای پردازشی خاص ندارید.

قاعده زنجیره‌ای (Chain rule) موتور محرک هر شبکه عصبی است که چیزی یاد می‌گیرد.

اهداف یادگیری

  • ساخت یک موتور مشتق‌گیری خودکار (Autograd engine) کمینه (کلاس Value) که عملیات‌ها را ثبت کرده و گرادیان‌ها را با مشتق‌گیری خودکار پس‌رو (Reverse-mode autodiff) محاسبه کند
  • پیاده‌سازی گذر رفت (Forward pass) و گذر برگشت (Backward pass) روی گراف محاسباتی با استفاده از مرتب‌سازی توپولوژیک (Topological sort)
  • پیاده‌سازی و آموزش یک پرسپترون چندلایه (Multi-layer perceptron یا MLP) روی مسئله XOR، تنها با استفاده از موتور autograd دست‌ساز خودتان
  • بررسی صحت مشتق‌گیری خودکار از طریق بررسی گرادیان (Gradient checking) در مقایسه با تفاضل متناهی عددی (Numerical finite differences)

صورت مسئله

شما مشتق توابع ساده را به‌راحتی حساب می‌کنید. اما شبکه عصبی یک تابع ساده نیست؛ ترکیبی از صدها تابع تودرتو است: ضرب ماتریسی، جمع با بایاس، اعمال تابع فعال‌ساز، دوباره ضرب ماتریسی، سافت‌مکس (Softmax) و تابع زیان آنتروپی متقاطع (Cross-entropy loss). خروجی، تابعی از یک تابع دیگر از تابعی دیگر است.

برای آموزش شبکه، به گرادیانِ زیان نسبت به تک‌تک وزن‌ها نیاز دارید. محاسبه دستی این گرادیان‌ها برای میلیون‌ها پارامتر غیرممکن است. محاسبه عددی آن (تفاضل متناهی) هم بیش از حد کند است.

قاعده زنجیره‌ای پایه‌های ریاضی را در اختیارتان می‌گذارد و مشتق‌گیری خودکار، الگوریتمِ کار را. ترکیب این دو به شما امکان می‌دهد گرادیان دقیق ترکیب‌های دلخواه توابع را در زمانی متناسب با تنها یک گذر رفت محاسبه کنید.

کتابخانه‌هایی مثل PyTorch، TensorFlow و JAX دقیقاً همین‌طور کار می‌کنند. در این درس، یک نسخه مینیاتوری از آن را از صفر خواهید ساخت.

مفاهیم پایه

قاعده زنجیره‌ای

اگر y=f(g(x))y = f(g(x)) باشد، مشتق yy نسبت به xx برابر است با:

dydx=dydg⋅dgdx=f′(g(x))⋅g′(x)\frac{dy}{dx} = \frac{dy}{dg} \cdot \frac{dg}{dx} = f'(g(x)) \cdot g'(x)

مشتق‌ها را در امتداد زنجیره در هم ضرب کنید. هر حلقه، مشتق موضعی (Local derivative) خودش را وارد محاسبه می‌کند.

مثال: y=sin⁡(x2)y = \sin(x^2)

g(x)=x2g′(x)=2xf(g)=sin⁡(g)f′(g)=cos⁡(g)dydx=cos⁡(x2)⋅2x\begin{aligned} g(x) &= x^2 \qquad & g'(x) &= 2x \\ f(g) &= \sin(g) \qquad & f'(g) &= \cos(g) \\[4pt] \frac{dy}{dx} &= \cos(x^2) \cdot 2x \end{aligned}

برای توابع تودرتوی عمیق‌تر، این زنجیره گسترش پیدا می‌کند:

y=f(g(h(x)))dydx=f′(g(h(x)))⋅g′(h(x))⋅h′(x)\begin{aligned} y &= f(g(h(x))) \\[4pt] \frac{dy}{dx} &= f'(g(h(x))) \cdot g'(h(x)) \cdot h'(x) \end{aligned}

هر لایه در شبکه عصبی، یک حلقه از این زنجیره است.

گراف‌های محاسباتی

گراف محاسباتی (Computational graph) قاعده زنجیره‌ای را شهودی و بصری می‌کند. هر عملیات به یک گره (Node) تبدیل می‌شود. داده‌ها در مسیر رفت در گراف جلو می‌روند و گرادیان‌ها در مسیر برگشت به عقب می‌آیند.

گذر رفت (محاسبه مقادیر):

a = 6c = 7y = 7x1 = 2* (ضرب)x2 = 3+ (جمع)b = 1reluخروجی y

گذر برگشت (محاسبه گرادیان‌ها):

relu'(c)=1 چون c 0dc/da = 1dc/db = 1da/dx1 = x2 = 3da/dx2 = x1 = 2dy/dy = 1dy/dc = 1dy/da = 1dy/db = 1dy/dx1 = 3dy/dx2 = 2

گذر برگشت قاعده زنجیره‌ای را در تک‌تک گره‌ها اعمال می‌کند و گرادیان‌ها را از خروجی به سمت ورودی‌ها پیش می‌برد.

حالت پیش‌رو در برابر حالت پس‌رو

برای اعمال قاعده زنجیره‌ای روی گراف دو روش وجود دارد:

حالت پیش‌رو (Forward mode) از ورودی‌ها شروع می‌کند و مشتق‌ها را به جلو می‌فرستد (push می‌کند). این روش dx/dx=1dx/dx = 1 را محاسبه کرده و در تک‌تک عملیات‌ها به جلو می‌برد. این حالت برای مسائلی با ورودی‌های کم و خروجی‌های زیاد مناسب است.

Forward mode: seed dx/dx = 1, propagate forward

  x = 2       (dx/dx = 1)
  a = x^2     (da/dx = 2x = 4)
  y = sin(a)  (dy/dx = cos(a) * da/dx = cos(4) * 4 = -2.615)

حالت پس‌رو (Reverse mode) از خروجی شروع می‌کند و گرادیان‌ها را به عقب می‌کشد. این روش dy/dy=1dy/dy = 1 را محاسبه کرده و در تک‌تک عملیات‌ها در جهت معکوس منتشر می‌کند. این حالت برای مسائلی با ورودی‌های پرشمار و خروجی‌های کم عالی است.

Reverse mode: seed dy/dy = 1, propagate backward

  y = sin(a)  (dy/dy = 1)
  a = x^2     (dy/da = cos(a) = cos(4) = -0.654)
  x = 2       (dy/dx = dy/da * da/dx = -0.654 * 4 = -2.615)

شبکه‌های عصبی میلیون‌ها ورودی (وزن‌ها) و تنها یک خروجی (زیان) دارند. حالت پس‌رو همه گرادیان‌ها را تنها در یک گذر برگشت محاسبه می‌کند. به همین دلیل پس‌انتشار (Backpropagation) از حالت پس‌رو استفاده می‌کند.

حالت مقدار اولیه (Seed) جهت بهترین کاربرد
پیش‌رو dxi/dxi=1dx_i/dx_i = 1 ورودی به خروجی ورودی کم، خروجی زیاد
پس‌رو dy/dy=1dy/dy = 1 خروجی به ورودی ورودی زیاد، خروجی کم (شبکه‌های عصبی)

اعداد دوگانه برای حالت پیش‌رو

حالت پیش‌رو را می‌توان خیلی تمیز با اعداد دوگانه (Dual numbers) پیاده‌سازی کرد. یک عدد دوگانه شکلی شبیه به a+bϵa + b\epsilon دارد که در آن ϵ2=0\epsilon^2 = 0 است.

Dual number: (value, derivative)

(2, 1) means: value is 2, derivative w.r.t. x is 1

Arithmetic rules:
  (a, a') + (b, b') = (a+b, a'+b')
  (a, a') * (b, b') = (a*b, a'*b + a*b')
  sin(a, a')         = (sin(a), cos(a)*a')

متغیر ورودی را با مشتق 1 مقداردهی اولیه کنید. مشتق به‌طور خودکار در خلال تک‌تک عملیات‌ها منتشر می‌شود.

ساخت یک موتور Autograd

یک موتور autograd به سه بخش نیاز دارد:

  1. پوشش مقدار (Value wrapping): قرار دادن هر عدد درون شیئی که مقدار و گرادیان آن را ذخیره کند.
  2. ثبت گراف (Graph recording): هر عملیات باید ورودی‌های خود و تابع گرادیان موضعی را ثبت کند.
  3. گذر برگشت (Backward pass): مرتب‌سازی توپولوژیک گراف و سپس پیمایش معکوس آن با اعمال قاعده زنجیره‌ای روی هر گره.

این دقیقاً همان کاری است که autograd در PyTorch انجام می‌دهد. کلاس torch.Tensor مقادیر را نگه می‌دارد، در صورت فعال بودن requires_grad=True عملیات‌ها را ثبت می‌کند و هنگام فراخوانی .backward() گرادیان‌ها را به دست می‌آورد.

عملکرد درونی PyTorch Autograd

وقتی چنین کدی در PyTorch می‌نویسید:

x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 + 3 * x + 1
y.backward()
print(x.grad)  # 7.0 = 2*x + 3 = 2*2 + 3

PyTorch در پشت صحنه کارهای زیر را انجام می‌دهد:

  1. یک گره Tensor برای x با شرط requires_grad=True می‌سازد؛
  2. با هر عملیات (**، *، +) گره جدیدی ایجاد کرده و تابع برگشت (Backward function) آن را ثبت می‌کند؛
  3. فراخوانی y.backward() موتور autodiff حالت پس‌رو را روی گراف ثبت‌شده اجرا می‌کند؛
  4. مشخصه‌ی grad_fn در هر گره، گرادیان‌های موضعی را حساب کرده و به گره‌های والد می‌فرستد؛
  5. گرادیان‌ها از طریق جمع شدن (نه جایگزینی) در ویژگی .grad روی هم انباشته می‌شوند.

این گراف پویا (تعریف در حین اجرا یا Define-by-run) است؛ یعنی در هر گذر رفت، گراف جدیدی از نو ساخته می‌شود. به همین دلیل PyTorch به‌راحتی از ساختارهای کنترلی پایتون (حلقه‌ها، if/else) درون مدل‌ها پشتیبانی می‌کند.

قاعده زنجیره‌ایx و a را بکشید
مشتق تابع مرکب y = sin(a x²) از ضرب دو شیب موضعی به دست می‌آید: شیب بیرونی cos(a x²) و شیب درونی 2 a x. هر نوار نارنجی یا آبی یک عامل است؛ حاصل‌ضرب آن‌ها نوار پایینی است. این همان قاعده‌ای است که پس‌انتشار پیوند به پیوند اعمال می‌کند تا گرادیان‌ها را به کل یک شبکه push کند.

پیاده‌سازی از صفر

گام 1: کلاس Value

class Value:
    def __init__(self, data, children=(), op=''):
        self.data = data
        self.grad = 0.0
        self._backward = lambda: None
        self._prev = set(children)
        self._op = op

    def __repr__(self):
        return f"Value(data={self.data:.4f}, grad={self.grad:.4f})"

هر شیء Value داده عددی، گرادیان (در ابتدا صفر)، یک تابع برگشت و ارجاع‌هایی به گره‌های فرزندی که آن را ساخته‌اند نگه می‌دارد.

گام 2: عملیات‌های حسابی همراه با ردیابی گرادیان

    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data + other.data, (self, other), '+')
        def _backward():
            self.grad += out.grad
            other.grad += out.grad
        out._backward = _backward
        return out

    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data * other.data, (self, other), '*')
        def _backward():
            self.grad += other.data * out.grad
            other.grad += self.data * out.grad
        out._backward = _backward
        return out

    def relu(self):
        out = Value(max(0, self.data), (self,), 'relu')
        def _backward():
            self.grad += (1.0 if out.data > 0 else 0.0) * out.grad
        out._backward = _backward
        return out

هر عملیات یک کلوژر می‌سازد که روش محاسبه گرادیان‌های موضعی و ضرب آن‌ها در گرادیان بالادست (out.grad) را می‌داند. علامت += برای حالتی است که از یک مقدار در چندین عملیات مختلف استفاده شده باشد.

گام 3: گذر برگشت

    def backward(self):
        topo = []
        visited = set()
        def build_topo(v):
            if v not in visited:
                visited.add(v)
                for child in v._prev:
                    build_topo(child)
                topo.append(v)
        build_topo(self)

        self.grad = 1.0
        for v in reversed(topo):
            v._backward()

مرتب‌سازی توپولوژیک تضمین می‌کند که گرادیان هر گره پیش از انتشار به فرزندانش به‌طور کامل محاسبه شده باشد. مقدار اولیه گرادیان 1.0 است (dy/dy=1dy/dy = 1).

گام 4: افزودن عملیات‌های بیشتر برای موتور کامل

کلاس پایه‌ای Value جمع، ضرب و تابع relu را پوشش می‌دهد. اما یک موتور autograd واقعی به چیزهای بیشتری نیاز دارد. این‌ها عملیاتی هستند که برای ساخت شبکه‌های عصبی نیاز خواهید داشت:

    def __neg__(self):
        return self * -1

    def __sub__(self, other):
        return self + (-other)

    def __radd__(self, other):
        return self + other

    def __rmul__(self, other):
        return self * other

    def __rsub__(self, other):
        return other + (-self)

    def __pow__(self, n):
        out = Value(self.data ** n, (self,), f'**{n}')
        def _backward():
            self.grad += n * (self.data ** (n - 1)) * out.grad
        out._backward = _backward
        return out

    def __truediv__(self, other):
        return self * (other ** -1) if isinstance(other, Value) else self * (Value(other) ** -1)

    def exp(self):
        import math
        e = math.exp(self.data)
        out = Value(e, (self,), 'exp')
        def _backward():
            self.grad += e * out.grad
        out._backward = _backward
        return out

    def log(self):
        import math
        out = Value(math.log(self.data), (self,), 'log')
        def _backward():
            self.grad += (1.0 / self.data) * out.grad
        out._backward = _backward
        return out

    def tanh(self):
        import math
        t = math.tanh(self.data)
        out = Value(t, (self,), 'tanh')
        def _backward():
            self.grad += (1 - t ** 2) * out.grad
        out._backward = _backward
        return out

دلیل اهمیت هر عملیات:

عملیات قاعده مشتق‌گیری کاربرد
__sub__ استفاده مجدد از جمع و منفی‌سازی محاسبه تابع زیان (pred - target)
__pow__ n⋅xn−1n \cdot x^{n-1} توابع فعال‌ساز چندجمله‌ای، MSE (error2\text{error}^2)
__truediv__ استفاده مجدد از ضرب و توان منفی یک نرمال‌سازی، مقیاس‌دهی نرخ یادگیری
exp exp⁡(x)⋅upstream\exp(x) \cdot \text{upstream} تابع سافت‌مکس، درست‌نمایی لگاریتمی
log (1/x)⋅upstream(1/x) \cdot \text{upstream} زیان آنتروپی متقاطع، احتمال‌های لگاریتمی
tanh (1−tanh⁡2)⋅upstream(1 - \tanh^2) \cdot \text{upstream} تابع فعال‌ساز کلاسیک

نکته هوشمندانه اینجاست: __sub__ و __truediv__ بر پایه همان عملیات‌های موجود تعریف شده‌اند. این توابع گرادیان درست را رایگان به دست می‌آورند؛ چون قاعده زنجیره‌ای روی همان عملیات‌های پایه‌ای جمع، ضرب و توان اعمال می‌شود.

گام 5: پیاده‌سازی پرسپترون چندلایه (MLP) از صفر

با کامل شدن کلاس Value، می‌توانید یک شبکه عصبی بسازید. بدون نیاز به PyTorch یا NumPy؛ فقط با همین مقادیر Value و قاعده زنجیره‌ای.

import random

class Neuron:
    def __init__(self, n_inputs):
        self.w = [Value(random.uniform(-1, 1)) for _ in range(n_inputs)]
        self.b = Value(0.0)

    def __call__(self, x):
        act = sum((wi * xi for wi, xi in zip(self.w, x)), self.b)
        return act.tanh()

    def parameters(self):
        return self.w + [self.b]

class Layer:
    def __init__(self, n_inputs, n_outputs):
        self.neurons = [Neuron(n_inputs) for _ in range(n_outputs)]

    def __call__(self, x):
        return [n(x) for n in self.neurons]

    def parameters(self):
        return [p for n in self.neurons for p in n.parameters()]

class MLP:
    def __init__(self, sizes):
        self.layers = [Layer(sizes[i], sizes[i+1]) for i in range(len(sizes)-1)]

    def __call__(self, x):
        for layer in self.layers:
            x = layer(x)
        return x[0] if len(x) == 1 else x

    def parameters(self):
        return [p for layer in self.layers for p in layer.parameters()]

یک نورون (Neuron) مقدار tanh(w1*x1 + w2*x2 + ... + b) را حساب می‌کند. یک لایه (Layer) لیستی از این نورون‌هاست و MLP این لایه‌ها را روی هم می‌چیند. تمام وزن‌ها از جنس Value هستند؛ در نتیجه فراخوانی loss.backward() گرادیان‌ها را به تک‌تک پارامترها می‌رساند.

آموزش روی XOR:

random.seed(42)
model = MLP([2, 4, 1])  # 2 inputs, 4 hidden neurons, 1 output

xs = [[0, 0], [0, 1], [1, 0], [1, 1]]
ys = [-1, 1, 1, -1]  # XOR pattern (using -1/1 for tanh)

for step in range(100):
    preds = [model(x) for x in xs]
    loss = sum((p - y) ** 2 for p, y in zip(preds, ys))

    for p in model.parameters():
        p.grad = 0.0
    loss.backward()

    lr = 0.05
    for p in model.parameters():
        p.data -= lr * p.grad

    if step % 20 == 0:
        print(f"step {step:3d}  loss = {loss.data:.4f}")

print("\nPredictions after training:")
for x, y in zip(xs, ys):
    print(f"  input={x}  target={y:2d}  pred={model(x).data:6.3f}")

این همان micrograd است؛ یک چرخه‌ی کامل آموزش شبکه عصبی با پایتون خالص و مشتق‌گیری خودکار. تمام فریم‌ورک‌های تجاری یادگیری عمیق دقیقاً همین کار را در مقیاسی فوق‌العاده بزرگ انجام می‌دهند.

گام 6: بررسی گرادیان

از کجا بدانیم autodiff درست کار می‌کند؟ آن را با مشتق‌های عددی مقایسه می‌کنیم؛ به این کار بررسی گرادیان (Gradient checking) می‌گویند.

def gradient_check(build_expr, x_val, h=1e-7):
    x = Value(x_val)
    y = build_expr(x)
    y.backward()
    autodiff_grad = x.grad

    y_plus = build_expr(Value(x_val + h)).data
    y_minus = build_expr(Value(x_val - h)).data
    numerical_grad = (y_plus - y_minus) / (2 * h)

    diff = abs(autodiff_grad - numerical_grad)
    return autodiff_grad, numerical_grad, diff

آزمودن روی یک عبارت پیچیده:

def expr(x):
    return (x ** 3 + x * 2 + 1).tanh()

ad, num, diff = gradient_check(expr, 0.5)
print(f"Autodiff:  {ad:.8f}")
print(f"Numerical: {num:.8f}")
print(f"Difference: {diff:.2e}")
# اختلاف باید کمتر از 1e-5 باشد

بررسی گرادیان هنگام پیاده‌سازی عملیات‌های جدید حیاتی است. اگر گذر برگشت شما باگی داشته باشد، بررسی عددی مچ آن را می‌گیرد. هر پیاده‌سازی جدی در یادگیری عمیق در طول توسعه حتماً بررسی گرادیان را اجرا می‌کند.

چه زمانی از بررسی گرادیان استفاده کنیم؟

موقعیت نیاز به بررسی گرادیان هست؟
اضافه کردن یک عملیات جدید به موتور autograd بله، همیشه
رفع اشکال چرخه‌ای که همگرا نمی‌شود بله، اول گرادیان‌ها را بررسی کنید
آموزش در محیط پروداکشن خیر، خیلی کند است (2 گذر رفت به ازای هر پارامتر)
آزمون‌های واحد (Unit tests) برای کد autograd بله، حتماً خودکارسازی شود

گام 7: اعتبارسنجی با محاسبات دستی

x1 = Value(2.0)
x2 = Value(3.0)
a = x1 * x2          # a = 6.0
b = a + Value(1.0)    # b = 7.0
y = b.relu()          # y = 7.0

y.backward()

print(f"y = {y.data}")          # 7.0
print(f"dy/dx1 = {x1.grad}")   # 3.0 (= x2)
print(f"dy/dx2 = {x2.grad}")   # 2.0 (= x1)

بررسی دستی: y = relu(x1*x2 + 1). چون x1*x2 + 1 = 7 > 0 است، تابع relu همان تابع همانی خواهد بود. dy/dx1 = x2 = 3 و dy/dx2 = x1 = 2. خروجی موتور دقیقاً با محاسبات دستی همخوانی دارد.

نحوه استفاده

مقایسه با PyTorch

import torch

x1 = torch.tensor(2.0, requires_grad=True)
x2 = torch.tensor(3.0, requires_grad=True)
a = x1 * x2
b = a + 1.0
y = torch.relu(b)
y.backward()

print(f"PyTorch dy/dx1 = {x1.grad.item()}")  # 3.0
print(f"PyTorch dy/dx2 = {x2.grad.item()}")  # 2.0

گرادیان‌ها کاملاً یکسان هستند. موتور شما دقیقاً همان خروجی PyTorch را تولید می‌کند، چون منطق ریاضی هر دو یکی است: مشتق‌گیری خودکار پس‌رو بر پایه‌ی قاعده زنجیره‌ای.

یک عبارت محاسباتی پیچیده‌تر

a = Value(2.0)
b = Value(-3.0)
c = Value(10.0)
f = (a * b + c).relu()  # relu(2*(-3) + 10) = relu(4) = 4

f.backward()
print(f"df/da = {a.grad}")  # -3.0 (= b)
print(f"df/db = {b.grad}")  #  2.0 (= a)
print(f"df/dc = {c.grad}")  #  1.0

خروجی نهایی

از این درس موارد زیر حاصل می‌شود:

  • outputs/skill-autodiff.md — مهارتی برای ساخت و رفع اشکال سیستم‌های autograd
  • code/autodiff.py — یک موتور autograd کمینه که می‌توانید گسترشش دهید

کلاس Value که در اینجا ساختید، پایه و اساس چرخه‌ی آموزش شبکه عصبی در فاز 3 خواهد بود.

تمرین‌ها

  1. متد __pow__ را به کلاس Value اضافه کنید تا بتوانید x ** n را محاسبه کنید. بررسی کنید که آیا ddx(x3)\frac{d}{dx}(x^3) به ازای x=2x=2 برابر با 12.0 می‌شود یا خیر.

  2. تابع tanh را به‌عنوان یک تابع فعال‌ساز اضافه کنید. درستی روابط tanh⁡′(0)=1\tanh'(0) = 1 و tanh⁡′(2)=0.0707\tanh'(2) = 0.0707 (به‌صورت تقریبی) را بررسی کنید.

  3. گراف محاسباتی یک تک‌نورون را بسازید: y = relu(w1*x1 + w2*x2 + b). هر پنج گرادیان را حساب کرده و درستی‌شان را با PyTorch بسنجید.

  4. حالت پیش‌روی مشتق‌گیری خودکار را با اعداد دوگانه پیاده‌سازی کنید. یک کلاس Dual بسازید و بررسی کنید که آیا مشتق‌های حاصل از آن با خروجی موتور پس‌رو همخوانی دارد یا نه.

راهنمایی‌ها

تمرین 1. کلاس Value از قبل متد __pow__ را تعریف کرده است. به پیاده‌سازی _backward آن نگاه کنید و مطمئن شوید که nxn−1n x^{n-1} را روی گرادیان ورودی اعمال می‌کند. کدهای x = Value(2.0)، y = x ** 3 و y.backward() را اجرا کنید؛ مقدار x.grad باید دقیقاً 12.0 باشد.

تمرین 2. در _backward از فرمول tanh⁡′(x)=1−tanh⁡(x)2\tanh'(x) = 1 - \tanh(x)^2 استفاده کنید. نتیجه را با تفاضل متناهیِ خودتان بسنجید: مقدار tanh⁡(x+h)−tanh⁡(x−h)2h\frac{\tanh(x + h) - \tanh(x - h)}{2h} باید در نقاط x=0x = 0 و x=2x = 2 همخوانی داشته باشد.

تمرین 3. عبارت w1*x1 + w2*x2 + b را با اشیاء Value بسازید، تابع relu را روی آن اعمال کرده و backward را صدا بزنید. قبل از مقایسه با PyTorch، دستی بررسی کنید: اگر ورودی relu مثبت باشد، گرادیان w1 برابر با x1 خواهد بود؛ اگر منفی باشد، هر پنج گرادیان 0 می‌شوند.

تمرین 4. در هر شیء Dual یک مقدار و یک مشتق ذخیره کنید و برای ضرب از قاعده ضرب (a,a′)(b,b′)=(ab,a′b+ab′)(a, a')(b, b') = (ab, a'b + ab') استفاده کنید. متغیر ورودی را با مشتق 1 مقداردهی اولیه کرده و نتیجه را برای همان تابع با x.grad به‌دست‌آمده از موتور پس‌رو مقایسه کنید.

اصطلاحات کلیدی

اصطلاح در گفتار عامیانه معنای دقیق فنی
قاعده زنجیره‌ای (Chain rule) «ضرب کردن مشتق‌ها» مشتق توابع تودرتو برابر است با حاصل‌ضرب مشتق موضعی هر تابع که در نقطه مناسب محاسبه شده باشد
گراف محاسباتی (Computational graph) «نمودار شبکه» یک گراف جهت‌دار بدون دور (DAG) که در آن گره‌ها نمایانگر عملیات‌ها و یال‌ها حامل مقادیر (در مسیر رفت) یا گرادیان‌ها (در مسیر برگشت) هستند
حالت پیش‌رو (Forward mode) «هدایت مشتق به جلو» مشتق‌گیری خودکاری که مشتق‌ها را از ورودی‌ها به خروجی‌ها می‌برد؛ یک گذر به ازای هر متغیر ورودی
حالت پس‌رو (Reverse mode) «پس‌انتشار» مشتق‌گیری خودکاری که گرادیان‌ها را از خروجی‌ها به ورودی‌ها منتقل می‌کند؛ یک گذر به ازای هر متغیر خروجی
مشتق‌گیری خودکار (Autograd) «گرادیان‌های خودکار» سیستمی که عملیات‌های اعمال‌شده روی مقادیر را ثبت می‌کند، گرافی می‌سازد و گرادیان‌های دقیق را بر اساس قاعده زنجیره‌ای به دست می‌آورد
اعداد دوگانه (Dual numbers) «مقدار به همراه مشتق» اعدادی به فرم a+bϵa + b\epsilon (با شرط ϵ2=0\epsilon^2 = 0) که اطلاعات مشتق را در حین محاسبات حسابی همراه خود منتقل می‌کنند
مرتب‌سازی توپولوژیک (Topological sort) «ترتیب وابستگی‌ها» مرتب کردن گره‌های گراف به‌طوری که هر گره پس از تمام پیش‌نیازهایش قرار بگیرد؛ این کار برای انتشار صحیح گرادیان ضروری است
انباشت گرادیان (Gradient accumulation) «جمع کن، جایگزین نکن» وقتی یک مقدار به چند عملیات مختلف وارد می‌شود، گرادیان نهایی آن حاصل جمع تمام گرادیان‌های ورودی به آن خواهد بود
گراف پویا (Dynamic graph) «تعریف در حین اجرا» گراف محاسباتی که در هر گذر رفت از نو ساخته می‌شود و امکان استفاده از جریان‌های کنترلی پایتون را درون مدل‌ها می‌دهد (رویکرد PyTorch)
بررسی گرادیان (Gradient checking) «بررسی عددی صحت» مقایسه گرادیان‌های مشتق‌گیری خودکار با گرادیان‌های عددی روش تفاضل متناهی جهت اطمینان از صحت کارکرد؛ ابزاری حیاتی برای اشکال‌زدایی
پرسپترون چندلایه (MLP) «شبکه عصبی چندلایه» یک شبکه عصبی با یک یا چند لایه پنهان از نورون‌ها. هر نورون مجموع وزن‌دار ورودی‌ها را با بایاس جمع کرده و سپس تابع فعال‌ساز را اعمال می‌کند
نورون (Neuron) «مجموع وزن‌دار + فعال‌ساز» واحد پایه محاسباتی: output=activation(w1x1+w2x2+⋯+b)\text{output} = \text{activation}(w_1 x_1 + w_2 x_2 + \cdots + b). وزن‌ها و بایاس پارامترهای قابل یادگیری این واحد هستند

برای مطالعه بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسقاعده زنجیره‌ای در حسابان چه چیزی را بیان می‌کند؟
  2. پیش از درسگراف محاسباتی چیست؟
  3. پس از درسچرا PyTorch به جای حالت رو به جلو از مشتق‌گیری خودکار حالت معکوس (پس‌انتشار) استفاده می‌کند؟
  4. پس از درسدر موتور autograd کلاس Value، چرا تابع backward برای انباشت گرادیان‌ها به جای '=' از '+=' استفاده می‌کند؟
  5. پس از درسبررسی گرادیان (gradient checking) چیست و چه زمانی باید از آن استفاده کرد؟

سؤالات متداول

چرا شبکه‌های عصبی از مشتق‌گیری خودکار پس‌رو (Reverse-mode) استفاده می‌کنند؟

چون شبکه‌های عصبی میلیون‌ها ورودی (وزن‌ها و پارامترها) و تنها یک خروجی اسکالر (زیان یا Loss) دارند. حالت پس‌رو تمام گرادیان‌ها را تنها در یک گذر برگشت محاسبه می‌کند، در حالی که حالت پیش‌رو به ازای هر متغیر ورودی به یک گذر جداگانه نیاز دارد.

چرا انباشت گرادیان با جمع (+=) انجام می‌شود و جایگزین نمی‌شود؟

اگر یک متغیر در چند عملیات مختلف به‌طور همزمان استفاده شود، طبق قاعده زنجیره‌ای چندمتغیره، گرادیان کل آن متغیر برابر با مجموع اثرات واردشده از تمام آن مسیرهاست؛ بنابراین مقدار جدید باید با مقادیر قبلی جمع شود.

بررسی گرادیان (Gradient Checking) چیست و چه کاربردی دارد؟

روشی است برای اعتبارسنجی کد مشتق‌گیری تحلیلی با مقایسه‌ی آن در برابر مشتق عددی بر پایه‌ی تفاضل متناهی. این روش در زمان توسعه یا رفع اشکال موتور autograd به کار می‌رود تا از نبود باگ در توابع backward اطمینان حاصل شود.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.