بسیاری از توسعهدهندگان کار با یادگیری ماشین را مستقیماً با فراخوانی متدهایی مثل `loss.backward()` در PyTorch آغاز میکنند، بدون اینکه سازوکار درونی آن را لمس کرده باشند. در عمل، وقتی مدل شما در یادگیری درجا میزند، با انفجار مقادیر عددی یا خطای NaN روبهرو میشود یا لایههای ابتدایی آن متوقف میمانند، انتزاعهای کتابخانهها دیگر کمکی نمیکنند و باید دقیقاً بدانید پشت صحنه چه میگذرد.
در این درس موتور مشتقگیری خودکار (Autograd) را از پایهایترین سطح، یعنی ساخت گرههای محاسباتی تا مرتبسازی توپولوژیک و اِعمال قاعدهی زنجیرهای، با پایتون خالص میسازید. با اجرای این محاسبات روی مسائلی چون XOR و تفکیک دادههای دایرهای، نه تنها منطق ریاضی پسانتشار را پیاده میکنید، بلکه ریشهی خطاهایی مانند محو گرادیان را نیز بهصورت تجربی لمس خواهید کرد.
کدها و پیادهسازی این درس کاملاً متکی به ساختار دادههای ساده و محاسبات روی CPU شخصی هستند و هیچ وابستگی به سرویسهای ابری خارجی یا پردازندههای گرافیکی سنگین ندارند؛ بنابراین میتوانید تمام فرایند آموزش و آزمایش را مستقیماً روی محیط محلی خود اجرا کنید.
پسانتشار (Backpropagation) همان الگوریتمی است که یادگیری را ممکن میکند. بدون آن، شبکههای عصبی چیزی جز تولیدکنندههای گرانقیمت اعداد تصادفی نیستند.
اهداف یادگیری
- پیادهسازی یک موتور مشتقگیری خودکار (autograd) مبتنی بر گره Value که گراف محاسباتی (Computational Graph) میسازد و گرادیانها را با مرتبسازی توپولوژیک (Topological Sort) محاسبه میکند
- به دست آوردن گام رو به عقب (backward pass) برای جمع، ضرب و تابع سیگموئید (sigmoid) با استفاده از قاعدهی زنجیرهای (chain rule)
- آموزش یک شبکهی چندلایه روی مسائل XOR و تفکیک دادههای دایرهای، فقط با موتور پسانتشار دستنویس خودتان
- شناسایی مشکل محو گرادیان (vanishing gradient problem) در شبکههای عمیق با تابع سیگموئید و توضیح چرایی افت نمایی گرادیانها
صورت مسئله
شبکهی شما یک لایهی پنهان با 768 ورودی و 3072 خروجی دارد؛ یعنی 2,359,296 وزن. حالا شبکه یک پیشبینی اشتباه انجام داده است. کدام وزنها باعث این خطا شدهاند؟ آزمایش تکتک وزنها به معنای 2.3 میلیون بار اجرای گذر رو به جلو (forward pass) است. اما پسانتشار همهی این 2.3 میلیون گرادیان را تنها در یک گام رو به عقب محاسبه میکند. این فقط یک بهینهسازی (optimization) ساده نیست؛ مرز میان «آموزشپذیر بودن» و «غیرممکن بودن» است.
روش سادهانگارانه این است: یک وزن را بردارید، مقدارش را به اندازهی بسیار ناچیزی تغییر دهید، گذر رو به جلو را دوباره اجرا کنید و ببینید تابع زیان (loss) بیشتر شد یا کمتر. این کار گرادیان همان یک وزن را به شما میدهد. حالا این کار را برای تکتک وزنهای شبکه تکرار کنید؛ آن هم ضربدر هزاران گام آموزش و میلیونها نمونهی داده. برای آموزش هر مدل مفیدی به زمانی در مقیاس تاریخ زمینشناسی نیاز خواهید داشت!
پسانتشار این مشکل را حل میکند: یک گذر رو به جلو، یک گذر رو به عقب، و محاسبهی تمام گرادیانها. کل ماجرا به استفادهی نظاممند از قاعدهی زنجیرهای در حسابان روی یک گراف محاسباتی برمیگردد. همین الگوریتم بود که یادگیری عمیق (deep learning) را به ابزاری کاربردی تبدیل کرد. بدون آن، هنوز درگیر مسائل پیشپاافتاده و اسباببازی بودیم.
مفاهیم پایه
قاعدهی زنجیرهای در شبکههای عصبی
با قاعدهی زنجیرهای در درس 05 از فاز 01 آشنا شدید. مرور کوتاه: اگر ، آنگاه . شما مشتقها را در طول زنجیره در هم ضرب میکنید.
در یک شبکهی عصبی (neural network)، این «زنجیره» همان دنبالهی عملیات از ورودی تا تابع زیان است. هر لایه وزنها را اِعمال میکند، بایاسها را میافزاید و خروجی را از یک تابع فعالساز میگذراند. در پایان، تابع زیان خروجی نهایی را با هدف مقایسه میکند. پسانتشار این زنجیره را به عقب بازمیگردد و سهم هر عمل در خطای نهایی را محاسبه میکند.
گرافهای محاسباتی
هر گذر رو به جلو یک گراف میسازد. هر گره یک عمل ریاضی است (ضرب، جمع، سیگموئید) و هر یال در مسیر رفت حامل یک مقدار، و در مسیر برگشت حامل یک گرادیان است.
گذر رو به جلو: مقادیر از چپ به راست حرکت میکنند. و مقدار را میسازند. با افزودن به میرسیم. تابع سیگموئید مقدار فعالسازی را تولید میکند. در نهایت با استفاده از تابع زیان با هدفِ مقایسه میشود.
گذر رو به عقب: گرادیانها از راست به چپ جریان مییابند. کار با آغاز میشود (میزان تغییرات تابع زیان نسبت به فعالسازی). این مقدار در (مشتق سیگموئید) ضرب میشود تا به دست آید. سپس به دو شاخهی (که برابر با است، چون ) و تقسیم میشود. پس از آن نوبت به و میرسد.
هر گره در گراف طی گام رو به عقب تنها یک وظیفه دارد: گرادیانی را که از بالا میآید بگیرد، در مشتق محلی خودش ضرب کند و نتیجه را به گرههای پایینتر بفرستد.
مقایسهی مسیر رفت و برگشت
گذر رو به جلو تمام مقادیر میانی را ذخیره میکند: z، a و ورودیهای هر لایه. گام رو به عقب برای محاسبهی گرادیانها به این مقادیر ذخیرهشده نیاز دارد. این همان مصالحهی میان حافظه و محاسبات در قلب پسانتشار است: شما حافظه مصرف میکنید (با ذخیرهی مقادیر فعالسازی) تا در مقابل، سرعت بالایی به دست آورید (تنها یک گام محاسباتی به جای میلیونها بار تکرار).
جریان گرادیان در طول شبکه
در یک شبکهی 3 لایه، گرادیانها در تمام لایهها به صورت زنجیرهای حرکت میکنند:
در هر لایه، گرادیان در مشتق سیگموئید ضرب میشود. مشتق سیگموئید برابر با است که بیشینهی مقدار آن تنها 0.25 خواهد بود (زمانی که ). در لایهی سوم، گرادیان دستبالا در مقداری معادل ضرب شده است. در لایهی دهم: .
محو گرادیان
این همان مشکل محو گرادیان است. تابع سیگموئید خروجی خود را بین 0 و 1 فشرده میکند و مشتق آن همیشه کمتر از 0.25 است. با روی هم چیدن لایههای سیگموئید پرتعداد، گرادیانها عملاً به سمت صفر میل میکنند و ناپدید میشوند. لایههای ابتدایی چون گرادیانهای نزدیک به صفر دریافت میکنند، تقریباً هیچ چیزی یاد نمیگیرند.
sigmoid(z): Output range [0, 1]
sigmoid'(z): Max value 0.25 (at z = 0)
After 5 layers: gradient * 0.25^5 = 0.001x original
After 10 layers: gradient * 0.25^10 = 0.000001x original
به همین دلیل است که آموزش شبکههای عمیق با تابع سیگموئید تقریباً غیرممکن است. راهحل این مشکل — یعنی تابع ReLU و نسخههای دیگر آن — موضوع درس 04 است. فعلاً این نکته را به خاطر داشته باشید که الگوریتم پسانتشار بینقص کار میکند؛ مشکل از لایههایی است که گرادیان باید از آنها بگذرد.
محاسبهی گرادیانها برای یک شبکهی 2 لایه
محاسبات ریاضی ملموس برای شبکهای با ورودی ، یک لایهی پنهان با فعالساز سیگموئید، لایهی خروجی با فعالساز سیگموئید و تابع زیان میانگین مربعات خطا (MSE):
گذر رو به جلو:
گام رو به عقب (با اِعمال مرحلهبهمرحلهی قاعدهی زنجیرهای):
هر گرادیان حاصلضرب مشتقهای جزئی (partial derivative) محلی است که از تابع زیان به عقب دنبال شدهاند. کل ماهیت پسانتشار همین است.
پیادهسازی از صفر
گام 1: گره Value
هر عدد در محاسبات ما به یک شیء Value تبدیل میشود. این شیء مقدار خود، گرادیانش و نحوهی ساختهشدنش را نگهداری میکند (تا بداند در مسیر برگشت گرادیانها را چگونه به سمت عقب محاسبه کند).
class Value:
def __init__(self, data, children=(), op=''):
self.data = data
self.grad = 0.0
self._backward = lambda: None
self._children = set(children)
self._op = op
def __repr__(self):
return f"Value(data={self.data:.4f}, grad={self.grad:.4f})"
در ابتدا گرادیانی وجود ندارد (0.0). تابع بازگشتی هم هنوز تعریف نشده است (یک تابع بیاثر). صفت _children مشخص میکند کدام اشیای Value این گره را ساختهاند تا بعداً بتوانیم گراف را به صورت توپولوژیک مرتب کنیم.
گام 2: عملیات ریاضی به همراه توابع Backward
هر عملیات یک شیء Value جدید میسازد و نحوهی جریان گرادیان به سمت عقب را تعریف میکند.
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
برای عمل جمع: و . در نتیجه هر دو ورودی مستقیماً گرادیان خروجی را دریافت میکنند.
برای عمل ضرب: و . هر ورودی، مقدار ورودی دیگر ضربدر گرادیان خروجی را دریافت میکند.
استفاده از += حیاتی است؛ چون ممکن است یک Value در چندین عملیات مختلف به کار رفته باشد و گرادیان نهایی آن باید مجموع گرادیانهای همهی مسیرها باشد.
گام 3: تابع سیگموئید و تابع زیان
import math
def sigmoid(self):
x = self.data
x = max(-500, min(500, x))
s = 1.0 / (1.0 + math.exp(-x))
out = Value(s, (self,), 'sigmoid')
def _backward():
self.grad += (s * (1 - s)) * out.grad
out._backward = _backward
return out
مشتق سیگموئید برابر است با: . ما مقدار را در گذر رو به جلو محاسبه کردهایم؛ پس بدون هیچ کار اضافی از همان مقدار دوباره استفاده میکنیم.
def mse_loss(predicted, target):
diff = predicted + Value(-target)
return diff * diff
تابع زیان MSE برای یک خروجی منفرد: . عمل تفریق را با جمع کردن یک Value قرینهشده پیادهسازی میکنیم.
گام 4: گام رو به عقب
مرتبسازی توپولوژیک تضمین میکند که گرهها به ترتیب درست پردازش شوند؛ یعنی قبل از انتشار گرادیان از یک گره، گرادیانهای واردشده به آن بهطور کامل جمعآوری شده باشند.
def backward(self):
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._children:
build_topo(child)
topo.append(v)
build_topo(self)
self.grad = 1.0
for v in reversed(topo):
v._backward()
محاسبه از تابع زیان آغاز میشود (گرادیان = 1.0، چون ). سپس در طول گراف مرتبشده به سمت عقب حرکت میکنیم. متد _backward در هر گره، گرادیانها را به فرزندانش push میکند.
گام 5: لایه و شبکه
import random
class Neuron:
def __init__(self, n_inputs):
scale = (2.0 / n_inputs) ** 0.5
self.weights = [Value(random.uniform(-scale, scale)) for _ in range(n_inputs)]
self.bias = Value(0.0)
def __call__(self, x):
act = sum((wi * xi for wi, xi in zip(self.weights, x)), self.bias)
return act.sigmoid()
def parameters(self):
return self.weights + [self.bias]
class Layer:
def __init__(self, n_inputs, n_outputs):
self.neurons = [Neuron(n_inputs) for _ in range(n_outputs)]
def __call__(self, x):
out = [n(x) for n in self.neurons]
return out[0] if len(out) == 1 else out
def parameters(self):
params = []
for n in self.neurons:
params.extend(n.parameters())
return params
class Network:
def __init__(self, sizes):
self.layers = []
for i in range(len(sizes) - 1):
self.layers.append(Layer(sizes[i], sizes[i + 1]))
def __call__(self, x):
for layer in self.layers:
x = layer(x)
if not isinstance(x, list):
x = [x]
return x[0] if len(x) == 1 else x
def parameters(self):
params = []
for layer in self.layers:
params.extend(layer.parameters())
return params
def zero_grad(self):
for p in self.parameters():
p.grad = 0.0
یک نورون (Neuron) ورودیها را میگیرد، مجموع وزندار بهعلاوهی بایاس را محاسبه میکند و تابع سیگموئید را روی آن اِعمال میکند. وزنها با ضریب مقداردهی اولیه میشوند تا از اشباع سیگموئید در شبکههای عمیقتر جلوگیری شود. کلاس Layer فهرستی از نورونهاست و Network فهرستی از لایهها. متد parameters() تمام مقادیر Value آموزشپذیر را جمعآوری میکند تا بتوانیم آنها را بهروزرسانی کنیم.
گام 6: آموزش روی مسئلهی XOR
random.seed(42)
net = Network([2, 4, 1])
xor_data = [
([0.0, 0.0], 0.0),
([0.0, 1.0], 1.0),
([1.0, 0.0], 1.0),
([1.0, 1.0], 0.0),
]
learning_rate = 1.0
for epoch in range(1000):
total_loss = Value(0.0)
for inputs, target in xor_data:
x = [Value(i) for i in inputs]
pred = net(x)
loss = mse_loss(pred, target)
total_loss = total_loss + loss
net.zero_grad()
total_loss.backward()
for p in net.parameters():
p.data -= learning_rate * p.grad
if epoch % 100 == 0:
print(f"Epoch {epoch:4d} | Loss: {total_loss.data:.6f}")
print("\nXOR Results:")
for inputs, target in xor_data:
x = [Value(i) for i in inputs]
pred = net(x)
print(f" {inputs} -> {pred.data:.4f} (expected {target})")
کاهش تابع زیان را تماشا کنید. پیشبینیها از مقادیر کاملاً تصادفی به خروجیهای درست XOR میرسند؛ آن هم کاملاً با اتکا به الگوریتم پسانتشار که گرادیانها را محاسبه میکند و وزنها را در جهت درست تکان میدهد.
گام 7: تفکیک دادههای دایرهای
در درس 02، وزنها را برای تفکیک دادههای دایرهای دستی تنظیم کردید. حالا اجازه دهید خود شبکه آنها را یاد بگیرد.
random.seed(7)
def generate_circle_data(n=100):
data = []
for _ in range(n):
x1 = random.uniform(-1.5, 1.5)
x2 = random.uniform(-1.5, 1.5)
label = 1.0 if x1 * x1 + x2 * x2 < 1.0 else 0.0
data.append(([x1, x2], label))
return data
circle_data = generate_circle_data(80)
circle_net = Network([2, 8, 1])
learning_rate = 0.5
for epoch in range(2000):
random.shuffle(circle_data)
total_loss_val = 0.0
for inputs, target in circle_data:
x = [Value(i) for i in inputs]
pred = circle_net(x)
loss = mse_loss(pred, target)
circle_net.zero_grad()
loss.backward()
for p in circle_net.parameters():
p.data -= learning_rate * p.grad
total_loss_val += loss.data
if epoch % 200 == 0:
correct = 0
for inputs, target in circle_data:
x = [Value(i) for i in inputs]
pred = circle_net(x)
predicted_class = 1.0 if pred.data > 0.5 else 0.0
if predicted_class == target:
correct += 1
accuracy = correct / len(circle_data) * 100
print(f"Epoch {epoch:4d} | Loss: {total_loss_val:.4f} | Accuracy: {accuracy:.1f}%")
در اینجا از SGD برخط (online SGD) استفاده میکنیم؛ یعنی وزنها را بعد از هر نمونه داده بهروزرسانی میکنیم نه بعد از جمعآوری کل بسته (batch). این کار تقارن را سریعتر میشکند و مانع از اشباع سیگموئید روی کل چشمانداز زیان (loss landscape) میشود. بُر زدن دادهها در هر دور آموزش (epoch) نیز مانع از حفظ کردن ترتیب دادهها توسط شبکه میشود.
هیچ تنظیم دستی در کار نیست. شبکه خودش مرز تصمیمگیری (decision boundary) دایرهای را کشف میکند. قدرت واقعی پسانتشار همین است: شما معماری، تابع زیان و دادهها را مشخص میکنید؛ الگوریتم خودش وزنها را پیدا میکند.
استفاده از ابزارهای واقعی
کتابخانهی PyTorch تمام مراحل بالا را در چند خط خلاصه میکند. ایدهی اصلی دقیقاً یکی است: موتور autograd در گذر رو به جلو یک گراف محاسباتی میسازد و در مسیر برگشت با پیمایش معکوس آن، گرادیانها را محاسبه میکند.
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(2, 4),
nn.Sigmoid(),
nn.Linear(4, 1),
nn.Sigmoid(),
)
optimizer = torch.optim.SGD(model.parameters(), lr=1.0)
criterion = nn.MSELoss()
X = torch.tensor([[0,0],[0,1],[1,0],[1,1]], dtype=torch.float32)
y = torch.tensor([[0],[1],[1],[0]], dtype=torch.float32)
for epoch in range(1000):
pred = model(X)
loss = criterion(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("PyTorch XOR Results:")
with torch.no_grad():
for i in range(4):
pred = model(X[i])
print(f" {X[i].tolist()} -> {pred.item():.4f} (expected {y[i].item()})")
متد loss.backward() همان تابع total_loss.backward() دستنویس شماست. دستور optimizer.step() معادلِ p.data -= lr * p.grad دستی شماست. همچنین optimizer.zero_grad() کارِ net.zero_grad() را انجام میدهد. همان الگوریتم است، اما با پیادهسازی صنعتی. کتابخانهی PyTorch شتابدهی GPU، دقت ترکیبی (mixed precision)، چکپوینتگذاری گرادیان (gradient checkpointing) و صدها نوع لایهی گوناگون را مدیریت میکند. اما گام رو به عقب، همان قاعدهی زنجیرهای روی همان گراف محاسباتی است.
فرایند آموزش ابتدا گذر رو به جلو، سپس گام رو به عقب و بعد بهروزرسانی وزنها را اجرا میکند. اما فرایند استنتاج (inference) فقط گذر رو به جلو را اجرا میکند؛ بدون گرادیان و بدون بهروزرسانی. درک این تفاوت مهم است؛ زیرا در محیط عملیاتی همیشه با استنتاج سروکار داریم. وقتی یک API مثل Claude یا GPT را فراخوانی میکنید، در حال اجرای استنتاج هستید: پرامپت شما در جهت مستقیم از شبکه عبور میکند و توکنها از آن سو خارج میشوند. هیچ وزنی تغییر نمیکند. دلیل اهمیت فهم پسانتشار این است که تکتک وزنهای همان شبکه را همین الگوریتم شکل داده است.
خروجی این درس
خروجی این درس:
- فایل
outputs/prompt-gradient-debugger.md— یک پرامپت چندبارمصرف برای عیبیابی مشکلات گرادیان (محو شدن، انفجار یا تولید مقادیر NaN) در هر شبکهی عصبی
تمرینها
-
متد
__sub__را به کلاس Value اضافه کنید . سپس متد__neg__را پیادهسازی کنید. با مقایسهی نتایج با محاسبات دستی برای یک عبارت ساده مانند ، بررسی کنید که گرادیانها درست باشند. -
متد
reluرا به کلاس Value اضافه کنید (خروجی ، با مشتق 1 در صورتی که و در غیر این صورت 0). در لایههای پنهان، سیگموئید را با relu جایگزین کنید و دوباره روی XOR آموزش دهید. سرعت همگرایی را مقایسه کنید. باید افزایش سرعت آموزش را ببینید؛ این تمرین پیشنمایشی از درس 04 است. -
متد
__pow__را برای توانهای صحیح در کلاس Value پیادهسازی کنید. با استفاده از آن،mse_lossرا با یک عبارت استاندارد مثل(predicted - target) ** 2جایگزین کنید. بررسی کنید که گرادیانها با پیادهسازی قبلی همخوانی داشته باشند. -
برش گرادیان (gradient clipping) را به حلقهی آموزش اضافه کنید: بعد از فراخوانی
backward()، تمام گرادیانها را به بازهی [-1, 1] محدود کنید. یک شبکهی عمیقتر (4 لایه یا بیشتر با فعالساز سیگموئید) را آموزش دهید و منحنیهای تابع زیان را در دو حالتِ با و بدون برش گرادیان مقایسه کنید. این اولین خط دفاعی شما در برابر انفجار گرادیان است. -
یک بخش بصریسازی بسازید: بعد از آموزش روی XOR، گرادیان تمام پارامترهای شبکه را چاپ کنید. مشخص کنید کدام لایه کوچکترین گرادیانها را دارد. این کار پدیدهی محو گرادیان را که در بخش مفاهیم خواندید، بهطور عملی نشان میدهد.
اصطلاحات کلیدی
| اصطلاح | باور عامیانه | معنای دقیق فنی |
|---|---|---|
| پسانتشار (Backpropagation) | «شبکه یاد میگیرد» | الگوریتمی که را برای تمام وزنها با اِعمال رو به عقبِ قاعدهی زنجیرهای در گراف محاسباتی محاسبه میکند |
| گراف محاسباتی (Computational graph) | «ساختار شبکه» | یک گراف جهتدار بدون دور که در آن گرهها نمایانگر عملیات ریاضی هستند و یالها حامل مقادیر (در مسیر رفت) و گرادیانها (در مسیر برگشت) هستند |
| قاعدهی زنجیرهای (Chain rule) | «ضرب کردن مشتقها» | اگر ، آنگاه — پایهی ریاضی پسانتشار |
| گرادیان (Gradient) | «جهت تندترین شیب صعودی» | مشتق جزئیِ تابع زیان نسبت به یک پارامتر؛ به شما نشان میدهد برای کاهش زیان، آن پارامتر را چگونه تغییر دهید |
| محو گرادیان (Vanishing gradient) | «شبکههای عمیق یاد نمیگیرند» | افت نمایی گرادیانها هنگام انتشار در لایههایی با توابع فعالساز اشباعشونده مانند سیگموئید |
| گذر رو به جلو (Forward pass) | «اجرای شبکه» | محاسبهی خروجی از روی ورودیها با اِعمال پیاپیِ عملیات هر لایه و ذخیرهسازی مقادیر میانی |
| گام رو به عقب (Backward pass) | «محاسبهی گرادیانها» | پیمایش معکوس گراف محاسباتی و تجمیع گرادیانها در هر گره با بهرهگیری از قاعدهی زنجیرهای |
| نرخ یادگیری (Learning rate) | «سرعت یادگیری شبکه» | یک ضریب عددی برای کنترل اندازهی گام هنگام بهروزرسانی وزنها: |
| مرتبسازی توپولوژیک (Topological sort) | «ترتیب درست» | چینشی از گرههای گراف که در آن هر گره پس از همهی وابستگیهایش قرار میگیرد؛ این کار تضمین میکند گرادیانها پیش از انتشار بیشتر، کاملاً جمعآوری شده باشند |
| مشتقگیری خودکار (Autograd) | «مشتقگیری خودکار محاسباتی» | سیستمی که طی گذر رو به جلو گراف محاسباتی میسازد و گرادیانها را بهطور خودکار محاسبه میکند؛ دقیقاً همان کاری که موتور PyTorch انجام میدهد |
برای مطالعهی بیشتر
- مقالهی Rumelhart, Hinton & Williams با عنوان «Learning representations by back-propagating errors» (سال 1986) — مقالهای که پسانتشار را فراگیر کرد و مسیر آموزش شبکههای چندلایه را گشود
- مجموعهی ویدیویی «Neural Networks» از کانال 3Blue1Brown (به آدرس https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi) — بهترین توضیح تصویری دربارهی پسانتشار و جریان گرادیان در شبکههای عصبی
