Backpropagation شروع سے: پہلے انجن، پھر نیٹ ورک
pure Python میں 120 لائن کا autodiff انجن لکھیں، اسے PyTorch سے سولہ اعشاری مقامات تک ملائیں، اور zero_grad کا کام سمجھیں۔
اس صفحے پر
چار ابواب کے بعد، کورس کے بیچ میں ایک خلا موجود ہے۔
باب 3 نے ہمیں gradient descent دیا: کسی parameter کو بہتر بنانے کے لیے loss کے نسبت اس کی ڈھلوان نکالیں اور نیچے کی طرف قدم رکھیں۔ باب 4 نے ہمیں ایسا loss دیا جس پر اترنا معنی رکھتا تھا۔ لیکن دونوں میں derivative ہاتھ سے نکالا گیا — ایک model، ایک parameter، calculus کی ایک لائن، اور سب کچھ ایک صفحے پر سما گیا۔
اب دو layers ایک کے اوپر ایک رکھیں۔ پہلی layer کا output دوسری کو feed کرتا ہے، اس لیے پہلی layer کا ہر weight دوسری layer کے ہر neuron کے ذریعے loss کو متاثر کرتا ہے۔ سو units کی دو hidden layers والے network میں تقریباً بیس ہزار parameters ہوتے ہیں، اور ہر ایک کو اسی loss کا اپنا partial derivative چاہیے۔ اسے ہاتھ سے کرنا محض تھکا دینے والا نہیں؛ ناممکن ہے، اور اس کورس کے باقی ہر architecture کے لیے ناممکن ہی رہتا ہے۔
راستہ بہتر notation نہیں ہے۔ راستہ یہ سمجھنا ہے کہ composition کا derivative خود computation کی structure سے، ایک program کے ذریعے، میکانکی طور پر نکالا جا سکتا ہے — اور اگر آپ اسے درست سمت میں کریں تو loss کو ایک بار compute کرنے کی تقریباً لاگت میں تمام بیس ہزار derivatives مل جاتے ہیں۔
یہ mechanism reverse-mode automatic differentiation ہے۔ نیورل نیٹ ورک پر لاگو ہو تو اسے backpropagation کہتے ہیں، اور اس باب کے آخر تک آپ بغیر libraries کے تقریباً 120 لائن Python میں ایک لکھ چکے ہوں گے، اسے PyTorch کے خلاف check کر چکے ہوں گے، اور اسے وہ XOR مسئلہ حل کرنے کے لیے استعمال کر چکے ہوں گے جس نے باب 1 میں perceptron کو ناکام کیا تھا۔
پہلے: nonlinearity آخر ضروری کیوں ہے
اس حصے کا لنک: پہلے: nonlinearity آخر ضروری کیوں ہےmachine بنانے سے پہلے ایک سوال طے کرنا ضروری ہے، کیونکہ اگر جواب دوسری سمت جاتا تو بنانے کو کچھ نہ ہوتا۔
perceptron XOR پر اس لیے ناکام ہوا کہ ایک line چار points کو separate نہیں کر سکتی۔ واضح fix یہ ہے کہ stack کریں: input کو ایک linear layer سے گزاریں، پھر دوسری سے۔ کیا اس سے مدد ملتی ہے؟
نہیں، اور ثبوت دو لائنوں کا ہے۔ ایک linear layer ہے۔ اسے دوسری میں feed کریں، ، اور substitute کریں:
composition ہے، جہاں اور ۔ linear layers کا stack ایک ہی linear layer ہے۔ دس ہوں، ہزار ہوں: پھر بھی ایک line، پھر بھی XOR نہیں کر سکتیں۔
اسے ماننے کے بجائے ہوتے دیکھنا مفید ہے:
import numpy as np
rng = np.random.default_rng(0)
W1, b1 = rng.normal(size=(3, 2)), rng.normal(size=3)
W2, b2 = rng.normal(size=(1, 3)), rng.normal(size=1)
x = rng.normal(size=2)
two_layers = W2 @ (W1 @ x + b1) + b2
one_layer = (W2 @ W1) @ x + (W2 @ b1 + b2)
print(two_layers[0], one_layer[0], abs(two_layers[0] - one_layer[0]))-4.612963371048 -4.612963371048 0.00e+00تقریباً برابر نہیں۔ Bit-for-bit identical، کیونکہ وہی arithmetic صرف دوبارہ ترتیب دی گئی ہے۔
لہٰذا depth خود کچھ نہیں خریدتی۔ جو چیز فائدہ دیتی ہے وہ layers کے درمیان nonlinear function رکھنا ہے — اور activation functions کے وجود کی پوری وجہ یہی ہے۔ یہ کوئی biological flourish یا normalisation trick نہیں۔ ان کے بغیر دوسری layer صرف decoration ہے۔
chain rule، کاغذ پر، ایک shared node کے ساتھ
اس حصے کا لنک: chain rule، کاغذ پر، ایک shared node کے ساتھاب mathematics، اور یہ وہی ایک rule ہے جو آپ پہلے سے جانتے ہیں، بس تھوڑی ناواقف جگہ پر apply ہو رہا ہے۔
single-variable chain rule کہتا ہے کہ اگر ، پر depend کرتا ہے اور ، پر depend کرتا ہے، تو ۔ derivatives ایک chain کے ساتھ multiply ہوتے ہیں۔
یہاں اہم حصہ یہ ہے کہ جب کوئی variable ایک سے زیادہ downstream path کو feed کرتا ہے تو کیا ہوتا ہے۔ اگر ، کے ذریعے کو influence کرتا ہے اور کے ذریعے بھی، تو contributions جمع ہوتی ہیں:
path کے ساتھ multiply کریں، paths کے across sum کریں۔ یہی پوری backpropagation ہے، اور اس باب کے باقی ہر implementation detail — code میں += سمیت اور وہ zero_grad() call بھی جو اپنا پہلا training loop لکھنے والے ہر شخص کو پھنسا دیتی ہے — اسی دوسرے لفظ کا براہِ راست نتیجہ ہے۔
پانچ operations کا ایک concrete circuit لیں، جہاں اور :
نوٹ کریں کہ تین بار آتا ہے: میں، میں، اور براہِ راست میں۔ backward pass کاغذ پر، دائیں سے بائیں، سے شروع کرتے ہوئے کریں:
addition کے ذریعے
اس حصے کا لنک: addition کے ذریعے، اس لیے اور direct path کی contribution ہے۔ Addition incoming gradient کو دونوں inputs میں بغیر تبدیلی distribute کرتی ہے۔
tanh کے ذریعے
اس حصے کا لنک: tanh کے ذریعےwith ، اس لیے ۔
multiplication کے ذریعے
اس حصے کا لنک: multiplication کے ذریعے، اس لیے اور ۔ Multiplication swaps: ہر input کا gradient دوسرے input سے scale ہوتا ہے۔
تین paths کو x میں collect کریں
اس حصے کا لنک: تین paths کو x میں collect کریںکے ذریعے: ۔ کے ذریعے: ۔ براہِ راست: ۔
اس number کو یاد رکھیں۔ چند pages بعد ایک program اسے پیدا کرے گا، بغیر یہ سب بتائے۔
engine بنانا
اس حصے کا لنک: engine بناناوہ insight جو اسے programmable بناتا ہے: ان steps میں سے ہر ایک local تھا۔ multiplication node کے ذریعے gradient کو push کرنے کے لیے آپ کو incoming gradient اور دو stored input values درکار تھیں — circuit کے باقی حصے کے بارے میں کچھ نہیں۔ ہر operation جانتا ہے کہ خود کو differentiate کیسے کرنا ہے۔
تو ایک ایسا number بنائیں جو یاد رکھے کہ اسے کس نے produce کیا۔
class Value:
"""A number that remembers where it came from."""
def __init__(self, data, _children=(), _op=""):
self.data = data
self.grad = 0.0
self._backward = lambda: None
self._prev = set(_children)
self._op = _opچار fields۔ data value ہے۔ grad کو accumulate کرتا ہے۔ _prev ان Values کا set ہے جن سے یہ compute ہوا — graph کے edges۔ اور _backward ایک closure ہے جو ہر operation install کرتا ہے: اسے معلوم ہے کہ اس node کا gradient ایک step پیچھے اس کے inputs تک کیسے push کرنا ہے۔
ہر operator ایک ہی shape follow کرتا ہے: output compute کریں، parents record کریں، local rule install کریں۔
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 tanh(self):
t = math.tanh(self.data)
out = Value(t, (self,), "tanh")
def _backward():
self.grad += (1 - t * t) * out.grad
out._backward = _backward
return out
def relu(self):
out = Value(self.data if self.data > 0 else 0.0, (self,), "relu")
def _backward():
self.grad += (1.0 if out.data > 0 else 0.0) * out.grad
out._backward = _backward
return outچار _backward bodies کو table کی طرح پڑھیں اور paper derivation کے flow patterns وہیں نظر آتے ہیں:
| operation | یہ gradient کے ساتھ کیا کرتا ہے |
|---|---|
+ | distributes — ہر input کو وہی gradient |
* | swaps — ہر input دوسرے کی value سے scaled |
relu | routes — اسے pass کرتا ہے یا مکمل block کر دیتا ہے |
tanh | attenuates — سے scale کرتا ہے، جو زیادہ سے زیادہ 1 اور عموماً اس سے کم ہوتا ہے |
ہر ایک += استعمال کرتا ہے اور کبھی = نہیں۔ یہی «sum across paths» rule encoded ہے۔ ایک node جو دو consumers کو feed کرتا ہے دو بار call ہوتا ہے، اور دونوں contributions خود بخود add up ہو جاتی ہیں۔
پھر driver، جو واحد حصہ ہے جس کے پاس کوئی global knowledge ہے:
def backward(self):
order, seen = [], set()
def build(v):
if v in seen:
return
seen.add(v)
for child in v._prev:
build(child)
order.append(v)
build(self)
self.grad = 1.0
for v in reversed(order):
v._backward() build graph کی topological ordering بناتا ہے: ہر node اپنے تمام inputs کے بعد آتا ہے۔ اس list کو reverse میں walk کرنا guarantee کرتا ہے کہ جب آپ کسی node کا _backward call کرتے ہیں تو اس کا اپنا gradient پہلے ہی complete ہو چکا ہوتا ہے — downstream کے ہر consumer نے پہلے ہی contribute کر دیا ہوتا ہے۔ order غلط ہو جائے تو آپ آدھا تیار gradient backwards push کرتے ہیں، جس سے غلط answer آتا ہے اور کوئی error message نہیں ملتا۔
کیا یہ paper سے agree کرتا ہے؟
اس حصے کا لنک: کیا یہ paper سے agree کرتا ہے؟x = Value(0.5)
y = Value(1.4)
a = x * y
b = x + y
c = a * b
d = c.tanh()
L = d + x
L.backward()
print(x.grad, y.grad)forward: a=0.7000 b=1.9000 c=1.3300 d=0.8692 L=1.3692
backward: dL/dd=1.0000 dL/dc=0.2444 dL/da=0.4644 dL/db=0.1711
dL/dx=1.8212 dL/dy=0.40331.8212۔ وہی number، ایک ایسے program سے جسے + کا rule، * کا rule، tanh کا rule بتایا گیا، اور اس circuit کے بارے میں کچھ نہیں۔
دو independent checks، کیونکہ «یہ میری derive کی ہوئی چیز سے match کرتا ہے» ایک کمزور test ہے جب دونوں کام اسی شخص نے کیے ہوں۔
Numerical differentiation۔ input کو ذرا nudge کریں اور measure کریں۔ centred difference derivative کو بغیر کسی calculus کے estimate کرتا ہے:
dL/dx: analytic=1.821202805 numeric=1.821202805 |diff|=1.80e-10
dL/dy: analytic=0.403269235 numeric=0.403269235 |diff|=7.64e-12PyTorch کے خلاف، جس کا industrial autodiff engine ان لوگوں نے لکھا ہے جو یہی کام پیشہ ورانہ طور پر کرتے ہیں:
torch dL/dx=1.821202805316 ours=1.821202805316 |diff|=2.22e-16
torch dL/dy=0.403269234753 ours=0.403269234753 |diff|=1.11e-16پر agreement، جو 64-bit float کے لیے machine epsilon ہے: دونوں engines identical arithmetic perform کر رہے ہیں۔ numerical check اپنے پاس رکھیں — یہ نئی layer کے backward pass کو debug کرنے کا tool ہے، اور اسی وجہ سے wrong gradient findable ہوتا ہے۔
Saturation، measured
اس حصے کا لنک: Saturation، measuredوہی circuit، different inputs۔ اور set کریں، جس سے بنتا ہے:
x=0.5, y=1.4: dL/dc = 0.244400 three paths into x: 0.6501 + 0.1711 + 1.0000 = 1.8212
x=2.0, y=-3.0: dL/dc = 0.000025 three paths into x: 0.0001 + -0.0001 + 1.0000 = 0.9999node کو cross کرنے والا gradient 9,945 کے factor سے گر گیا۔ اس کے upstream میں موجود سب کچھ — real network میں، اس سے پہلے کی ہر layer — essentially کچھ نہیں پاتا۔ circuit کے دو paths silent ہو چکے ہیں؛ صرف direct connection جو کو skip کرتا ہے signal لے جا رہا ہے۔
یہ vanishing gradient problem ہے، ایک node میں۔ کی چالیس layers stack کریں اور ایسے چالیس factors multiply کریں، تو early layers مکمل طور پر learning روک دیتی ہیں۔ اتفاقاً، یہ skip connections کے حق میں ایک دلیل بھی ہے جسے آپ یہاں miniature میں دیکھ سکتے ہیں: nonlinearity کو bypass کرنے والا path ہی صرف survive ہوا۔
zero_grad اصل میں کیا کرتا ہے، اور bug کیوں چھپتا ہے
اس حصے کا لنک: zero_grad اصل میں کیا کرتا ہے، اور bug کیوں چھپتا ہےہر _backward، += استعمال کرتا ہے۔ یہ درست ہے — paths اسی طرح sum ہوتے ہیں۔ مگر اس کا ایک نتیجہ ہے جو ہر شخص کو پکڑتا ہے: gradients، backward() کی calls کے across بھی accumulate ہوتے ہیں۔ engine کو کوئی اندازہ نہیں کہ آپ کی دوسری call same graph میں ایک اور path نہیں بلکہ نیا training step ہے۔
اس لیے training loop کو انہیں clear کرنا پڑتا ہے:
for step in range(steps):
ys = [model(x) for x, _ in DATA]
loss = sum((yp - yt) ** 2 for yp, (_, yt) in zip(ys, DATA))
for p in model.parameters():
p.grad = 0.0
loss.backward()
for p in model.parameters():
p.data -= lr * p.gradPyTorch میں یہی optimizer.zero_grad() ہے، اور عام مشورہ یہ ہے کہ اسے بھولنا training توڑ دیتا ہے۔ تو آئیے وہ دو lines delete کرتے ہیں اور دیکھتے ہیں کہ یہ کتنا broken ہے۔ same seeds، same everything، XOR کے 200 steps:
| learning rate | seed | reset کے ساتھ | reset کے بغیر |
|---|---|---|---|
| 0.05 | 1337 | loss 3.255088, 3/4 | loss 0.000000, 4/4 |
| 0.05 | 7 | loss 2.144820, 2/4 | loss 0.000000, 4/4 |
| 0.05 | 42 | loss 2.126074, 2/4 | loss 0.000000, 4/4 |
| 0.1 | 1337 | loss 0.038597, 4/4 | loss 0.000000, 4/4 |
| 0.1 | 7 | loss 2.055048, 2/4 | loss 0.000000, 4/4 |
| 0.1 | 42 | loss 2.049876, 2/4 | loss 0.000073, 4/4 |
| 0.3 | 1337 | loss 4.512310, 2/4 | loss 8.000000, 2/4 |
| 0.3 | 7 | loss 0.015247, 4/4 | loss 4.000000, 3/4 |
| 0.3 | 42 | loss 0.005478, 4/4 | loss 4.000000, 3/4 |
چھوٹے learning rates پر، buggy version ہر row جیتتا ہے۔ correct version جہاں stall ہوتا ہے، یہ converge کر جاتا ہے۔
یہ fluke نہیں اور اسے سمجھنا ضروری ہے، کیونکہ یہی بتاتا ہے کہ یہ bug پکڑنا اتنا مشکل کیوں ہے۔ اگر آپ gradient کبھی clear نہیں کرتے، تو step پر parameter اب تک compute کیے گئے ہر gradient کے sum سے update ہوتا ہے۔ ایسے loss پر جو تقریباً اسی direction کی طرف point کرتا رہے، وہ sum مسلسل بڑھتا ہے، اور اثر ایک ایسے learning rate کا بنتا ہے جو خود بخود increase ہوتا ہے۔ پر، جہاں correct algorithm crawl کر رہا ہے، runaway step size بالکل fix جیسی لگتی ہے۔
پھر نیچے کی تین rows دیکھیں۔ پر یہی mechanism model کو اڑا دیتا ہے — loss 8.0 وہ score ہے جو constant پر collapsed model لیتا ہے — ان 16 کا half جو چار maximally wrong answers cost کرتے — — جبکہ correct version اب صاف converge کرتا ہے۔
لہٰذا honest statement یہ نہیں کہ «ہمیشہ zero_grad call کریں ورنہ آپ کا model train نہیں ہوگا»۔ یہ ہے: اس کے بغیر آپ gradient descent چلا ہی نہیں رہے۔ آپ ایسی چیز چلا رہے ہیں جس کا step size ایسی rate پر اوپر drift کرتا ہے جو کسی نے choose نہیں کی، اور یہ کام کرتا ہوا دکھائی دے گا، کبھی کبھی real thing سے بہتر بھی، عین اس وقت تک جب تک نہیں کرتا — پھر آپ learning rate، initialisation، یا data کو blame کریں گے۔ machine learning کے بدترین bugs کی shape یہی ہے: وہ crash نہیں کرتے، algorithm کو ایک different algorithm میں بدل دیتے ہیں جو کبھی کبھار بہتر score کرتا ہے۔
network، اور آخرکار XOR
اس حصے کا لنک: network، اور آخرکار XORengine مکمل ہو جائے تو neural network بمشکل ہی کوئی code ہے۔ ایک neuron dot product، bias اور activation ہے؛ ایک layer neurons کی list ہے؛ ایک network layers کی list ہے۔
class Neuron:
def __init__(self, nin):
self.w = [Value(random.uniform(-1, 1)) for _ in range(nin)]
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, nin, nout):
self.neurons = [Neuron(nin) for _ in range(nout)]
def __call__(self, x):
out = [n(x) for n in self.neurons]
return out[0] if len(out) == 1 else out
def parameters(self):
return [p for n in self.neurons for p in n.parameters()]
class MLP:
def __init__(self, nin, nouts):
sizes = [nin] + nouts
self.layers = [Layer(sizes[i], sizes[i + 1]) for i in range(len(nouts))]
def __call__(self, x):
for layer in self.layers:
x = layer(x)
return x
def parameters(self):
return [p for layer in self.layers for p in layer.parameters()]اس سب میں کوئی backward pass نہیں۔ ایک line بھی نہیں۔ Value class پہلے ہی جانتی ہے کہ یہ classes جو بھی build کریں اسے differentiate کیسے کرنا ہے، اور اسے پہلے لکھنے کا مقصد یہی تھا: autodiff engine کو معلوم نہیں ہوتا کہ اسے neural network کے لیے استعمال کیا جا رہا ہے۔
اب Chapter 1 والا problem۔ دو inputs، دو hidden units، ایک output، نو parameters:
step 1: loss 4.156690
step 10: loss 4.005572
step 50: loss 3.996708
step 100: loss 3.510700
step 200: loss 0.038597
[0, 0] -> -0.9081 (target -1) ok
[0, 1] -> +0.8934 (target +1) ok
[1, 0] -> +0.8906 (target +1) ok
[1, 1] -> -0.9207 (target -1) okچار میں سے چار۔ وہ function جسے کوئی perceptron compute نہیں کر سکتا — Chapter 1 میں چار inequalities سے proved، جنہوں نے کو positive بھی اور negative بھی ہونا لازم کیا — نو numbers سے compute ہوا جو automatically ملے۔
hidden layer نے کیا کیا
اس حصے کا لنک: hidden layer نے کیا کیااطمینان بخش بات یہ نہیں کہ یہ کام کرتا ہے۔ بات یہ ہے کہ آپ دیکھ سکتے ہیں کیسے، کیونکہ دو hidden units کے ساتھ intermediate representation ایک plane میں point ہے اور آپ اسے simply print کر سکتے ہیں۔
0.001241 کے loss تک trained، یہاں ہر input hidden layer کے بعد کہاں land کرتا ہے، اور output neuron اس کے ساتھ کیا کرتا ہے:
| input | hidden layer output | output score | label |
|---|---|---|---|
پہلی اور چوتھی rows دیکھیں۔ inputs اور square کے diagonally opposite corners ہیں — اس problem میں دو points جتنے دور ہو سکتے ہیں اتنے دور — اور hidden layer انہیں اور پر map کرتی ہے۔ تقریباً ایک ہی point۔ layer نے plane کو fold کر دیا تاکہ دونوں rejected corners ایک دوسرے کے اوپر land کریں، اور جب وہ ایک ہی جگہ آ جائیں تو ایک line انہیں باقی دو سے separate کر دیتی ہے۔
اور output neuron ٹھیک وہی line ہے۔ اس کے learned parameters ، ہیں، اس لیے اس کی decision boundary ہے
جو ایک straight line ہے — ایک perceptron، Chapter 1 والا وہی object، unchanged۔ تب یہ XOR حل نہیں کر سکتا تھا اور اب بھی نہیں کر سکتا۔ جو بدلا وہ یہ ہے کہ اب یہ input کو نہیں دیکھ رہا؛ یہ اس space کو دیکھ رہا ہے جو پہلی layer نے اس کے لیے بنایا، جہاں problem linearly separable ہے۔
یہی learned representation ہے، اور precise ہونا ضروری ہے کیونکہ یہ phrase اس کورس کے باقی حصے میں، اور field کے باقی حصے میں، loosely استعمال ہوگا۔ یہ compression، summary، یا کسی mystical sense میں embedding نہیں ہے۔ یہ coordinates کی تبدیلی ہے، designed کے بجائے learned، جس کا واحد کام اگلی layer کا کام آسان بنانا ہے۔
universal approximation theorem، اور یہ کیا نہیں کہتا
اس حصے کا لنک: universal approximation theorem، اور یہ کیا نہیں کہتایہاں ایک theorem ہے، اور اسے عموماً غلط quote کیا جاتا ہے۔
Cybenko نے 1989 میں اور Hornik نے 1991 میں prove کیا کہ single hidden layer اور suitable activation function والا feedforward network compact set پر کسی بھی continuous function کو، آپ کی پسند کی کسی بھی accuracy تک، enough hidden units کے ساتھ approximate کر سکتا ہے۔34 یہ genuine اور important result ہے: یہ کہتا ہے کہ architecture limitation نہیں ہے۔
اب پڑھیں کہ یہ کیا omit کرتا ہے۔ یہ نہیں کہتا کہ کتنے units — bound astronomically large ہو سکتی ہے۔ یہ نہیں کہتا کہ weights find کیے جا سکتے ہیں؛ یہ existence assert کرتا ہے، اور random start سے gradient descent oracle نہیں۔ اور یہ اس data پر behaviour کے بارے میں کچھ نہیں کہتا جو آپ نے نہیں دیکھا، جو باب 6 کا دوسرا نصف ہے۔
«exists» اور «findable» کے درمیان gap academic نہیں۔ یہی XOR problem ہے، ہر ایک کے 50 random initialisations، 1000 steps، صرف hidden layer size بدلتا ہے:
| hidden units | 4/4 تک پہنچنے والی initialisations |
|---|---|
| 2 | 38 / 50 (76 %) |
| 3 | 49 / 50 (98 %) |
| 4 | 50 / 50 (100 %) |
| 8 | 47 / 50 (94 %) |
minimum viable architecture کے ساتھ، چار میں سے ایک run کبھی وہاں نہیں پہنچتا — وہ ایسی configuration میں settle ہو جاتا ہے جس سے وہ descend نہیں کر سکتا، بالکل وہی local minimum جو Chapter 3 نے one-dimensional surface پر دکھایا تھا۔ ایک unit add کریں اور failures تقریباً غائب ہو جاتی ہیں، اس لیے نہیں کہ network زیادہ expressive ہو گیا (دو units پہلے ہی کافی ہیں — 38 runs اسے prove کرتے ہیں) بلکہ اس لیے کہ extra dimensions descent کو escape کے لیے زیادہ directions دیتی ہیں۔
اور پھر eight units، four سے ذرا worse کرتے ہیں۔ fixed learning rate اور step budget پر، زیادہ capacity monotonically better نہیں۔ جو بھی آپ سے کہتا ہے کہ stuck network کا fix ہمیشہ بڑا network ہے، وہ اس table کے middle سے extrapolate کر رہا ہے۔
یہی lesson Chapter 1 کے convergence theorem جیسا ہے، اور Chapter 10 میں scaling laws کے بارے میں یہی lesson ہوگا، اس form میں جو وہ chapter دیتا ہے: loss کی prediction اس capability کی prediction نہیں جس کے لیے آپ pay کر رہے ہیں، اور دونوں کے درمیان distance ہی engineering کی جگہ ہے۔
تفصیلات دکھائیں
Optional: matrix form، اور اوپر کا code اسے کیوں استعمال نہیں کرتا۔
یہاں سب کچھ ایک وقت میں ایک scalar کے طور پر لکھا گیا ہے، جو mechanism دیکھنے کا صاف ترین طریقہ اور اسے execute کرنے کا سب سے slow طریقہ ہے۔ practice میں layer ایک matrix multiply ہے، اور کا backward pass ہے
transposes یاد رکھنے کی trick نہیں؛ یہ sum-over-paths rule کی وہ شکل ہیں جب paths matrix entries سے indexed ہوں۔ general object Jacobian ہے، یعنی تمام outputs کے تمام inputs کے respect میں partial derivatives کی matrix، اور reverse mode دراصل vector-Jacobian product کی computation ہے بغیر کبھی Jacobian بنائے — جو اہم ہے، کیونکہ 4096 inputs اور 4096 outputs والی layer کے لیے اس matrix میں sixteen million entries ہوتی ہیں اور اسے بنانا کبھی worthwhile نہیں۔
اگلے chapters follow کرنے کے لیے آپ کو اس میں سے کچھ بھی ضروری نہیں؛ scalar version وہ سب کچھ کرتا ہے جو matrix version کرتا ہے، بس زیادہ slowly۔ یہ Chapter 9 میں necessary ہو جاتا ہے، جہاں shapes obvious نہیں رہتیں۔
یہ آگے کہاں جاتا ہے
اس حصے کا لنک: یہ آگے کہاں جاتا ہےاب آپ کے پاس ایک network ہے جو train ہوتا ہے۔ یہ اتنی بڑی achievement نہیں جتنی محسوس ہوتی ہے، کیونکہ آپ کا network چار examples پر train ہوتا ہے اور انہی چار پر measure ہوتا ہے۔
وہی code real dataset پر چلائیں تو problems کا نیا set آتا ہے، جن میں سے کوئی بھی gradients کے بارے میں نہیں۔ loss کچھ دیر نیچے جاتا ہے اور پھر رک جاتا ہے۔ یا training data پر نیچے جاتا ہے اور باقی ہر چیز پر اوپر۔ یا پہلے step سے ہی move نہیں کرتا، اور cause initial random weights کی range نکلتی ہے۔ یا epoch three میں ایک unit کا input ہر example پر negative drift کر گیا اور تب سے dead ہے، خاموشی سے، model کی capacity کا ایک chunk اپنے ساتھ لے کر۔
یہ exotic failures نہیں؛ یہ ابھی ابھی لکھے گئے network کی normal condition ہیں، اور ان میں سے کوئی خود اعلان نہیں کرتا۔ gradient درست ہے — آپ نے اسے PyTorch کے خلاف سولہ decimal places تک check کیا — اور model پھر بھی learn نہیں کرتا۔
Chapter 6 اسی کے بارے میں ہے: initialisation، normalisation، overfitting اور regularisation، اور کچھ بھی بدلنے سے پہلے یہ diagnostic habit کہ پوچھا جائے ان میں سے کون سی چیز ہو رہی ہے۔ یہ چلنے والے network اور کام کرنے والے network کا فرق ہے۔
Sources and method
اس حصے کا لنک: Sources and methodاس chapter کی Value class براہِ راست Andrej Karpathy کے micrograd سے descend کرتی ہے، اور ان کی video The spelled-out intro to neural networks and backpropagation: building micrograd اس material پر گزارے جانے والے بہترین تین گھنٹے ہیں اگر آپ اسے کسی اور سے دوسرے طریقے سے سمجھنا چاہتے ہیں۔ ان کی 2016 post Yes you should understand backprop خود ایک لکھنے کے حق میں case بناتی ہے اور Stanford کے CS224n میں assigned reading ہے۔ backpropagation پر CS231n notes (cs231n.github.io/optimization-2) اوپر tabulate کیے گئے flow patterns کا canonical treatment ہیں۔ mathematics کو neural-network folklore کے بجائے graph پر calculus کے طور پر سمجھنے کے لیے Deisenroth، Faisal اور Ong کی Mathematics for Machine Learning کا chapter 5.6 غیر معمولی طور پر clear ہے؛ اور Baydin، Pearlmutter، Radul اور Siskind کا survey Automatic Differentiation in Machine Learning: a Survey (arXiv:1502.05767) پوری field کا reference ہے، including اوپر discuss کیا گیا forward/reverse trade-off۔
حوالہ جات
اس حصے کا لنک: حوالہ جات-
Linnainmaa, S. The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. Master's thesis, University of Helsinki (1970). Reverse-mode accumulation، اس field تک پہنچنے سے سولہ سال پہلے اور بالکل different motivation کے تحت۔ ↩
-
Rumelhart, D. E., Hinton, G. E. and Williams, R. J. Learning representations by back-propagating errors. Nature 323, pp. 533–536 (1986). وہ paper جس نے method کو known بنایا، اور hidden units کو learned representations کے طور پر پڑھنے کا source جس پر اس chapter کا What the hidden layer did section اپنی measurements خرچ کرتا ہے۔ ↩
-
Cybenko, G. Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals and Systems 2, pp. 303–314 (1989). ↩
-
Hornik, K. Approximation capabilities of multilayer feedforward networks. Neural Networks 4(2), pp. 251–257 (1991). Cybenko کو generalise کرتا ہے: result activation کے non-polynomial ہونے پر depend کرتا ہے، sigmoidal ہونے پر نہیں۔ ↩