مواد پر جائیں
5/30باب 5 از 30

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 h=W1x+b1\mathbf{h} = W_1\mathbf{x} + \mathbf{b}_1 ہے۔ اسے دوسری میں feed کریں، y=W2h+b2\mathbf{y} = W_2\mathbf{h} + \mathbf{b}_2، اور substitute کریں:

y=W2(W1x+b1)+b2=(W2W1)x+(W2b1+b2)\mathbf{y} = W_2(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = (W_2W_1)\mathbf{x} + (W_2\mathbf{b}_1 + \mathbf{b}_2)

composition Wx+bW\mathbf{x} + \mathbf{b} ہے، جہاں W=W2W1W = W_2W_1 اور b=W2b1+b2\mathbf{b} = W_2\mathbf{b}_1 + \mathbf{b}_2۔ linear layers کا stack ایک ہی linear layer ہے۔ دس ہوں، ہزار ہوں: پھر بھی ایک line، پھر بھی XOR نہیں کر سکتیں۔

اسے ماننے کے بجائے ہوتے دیکھنا مفید ہے:

linear_is_linear.pyPYTHON
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]))
TEXT
-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 کہتا ہے کہ اگر LL، cc پر depend کرتا ہے اور cc، xx پر depend کرتا ہے، تو dLdx=dLdcdcdx\frac{dL}{dx} = \frac{dL}{dc} \cdot \frac{dc}{dx}۔ derivatives ایک chain کے ساتھ multiply ہوتے ہیں۔

یہاں اہم حصہ یہ ہے کہ جب کوئی variable ایک سے زیادہ downstream path کو feed کرتا ہے تو کیا ہوتا ہے۔ اگر xx، aa کے ذریعے LL کو influence کرتا ہے اور bb کے ذریعے بھی، تو contributions جمع ہوتی ہیں:

dLdx=Laax+Lbbx\frac{dL}{dx} = \frac{\partial L}{\partial a}\frac{\partial a}{\partial x} + \frac{\partial L}{\partial b}\frac{\partial b}{\partial x}

path کے ساتھ multiply کریں، paths کے across sum کریں۔ یہی پوری backpropagation ہے، اور اس باب کے باقی ہر implementation detail — code میں += سمیت اور وہ zero_grad() call بھی جو اپنا پہلا training loop لکھنے والے ہر شخص کو پھنسا دیتی ہے — اسی دوسرے لفظ کا براہِ راست نتیجہ ہے۔

پانچ operations کا ایک concrete circuit لیں، جہاں x=0.5x = 0.5 اور y=1.4y = 1.4:

a=xy,b=x+y,c=ab,d=tanh(c),L=d+xa = xy, \quad b = x + y, \quad c = ab, \quad d = \tanh(c), \quad L = d + x

نوٹ کریں کہ xx تین بار آتا ہے: aa میں، bb میں، اور براہِ راست LL میں۔ backward pass کاغذ پر، دائیں سے بائیں، dLdL=1\frac{dL}{dL} = 1 سے شروع کرتے ہوئے کریں:

L=d+xL = d + x، اس لیے Ld=1\frac{\partial L}{\partial d} = 1 اور direct path کی contribution Lx=1\frac{\partial L}{\partial x} = 1 ہے۔ Addition incoming gradient کو دونوں inputs میں بغیر تبدیلی distribute کرتی ہے۔

d=tanh(c)d = \tanh(c) with c=ab=0.7×1.9=1.33c = ab = 0.7 \times 1.9 = 1.33، اس لیے dLdc=1tanh2(1.33)=0.2444\frac{dL}{dc} = 1 - \tanh^2(1.33) = 0.2444۔

c=abc = ab، اس لیے dLda=dLdcb=0.2444×1.9=0.4644\frac{dL}{da} = \frac{dL}{dc} \cdot b = 0.2444 \times 1.9 = 0.4644 اور dLdb=dLdca=0.2444×0.7=0.1711\frac{dL}{db} = \frac{dL}{dc} \cdot a = 0.2444 \times 0.7 = 0.1711۔ Multiplication swaps: ہر input کا gradient دوسرے input سے scale ہوتا ہے۔

aa کے ذریعے: dLday=0.4644×1.4=0.6501\frac{dL}{da} \cdot y = 0.4644 \times 1.4 = 0.6501۔ bb کے ذریعے: dLdb1=0.1711\frac{dL}{db} \cdot 1 = 0.1711۔ براہِ راست: 11۔

dLdx=0.6501+0.1711+1.0000=1.8212\frac{dL}{dx} = 0.6501 + 0.1711 + 1.0000 = 1.8212

اس number کو یاد رکھیں۔ چند pages بعد ایک program اسے پیدا کرے گا، بغیر یہ سب بتائے۔

وہ insight جو اسے programmable بناتا ہے: ان steps میں سے ہر ایک local تھا۔ multiplication node کے ذریعے gradient کو push کرنے کے لیے آپ کو incoming gradient اور دو stored input values درکار تھیں — circuit کے باقی حصے کے بارے میں کچھ نہیں۔ ہر operation جانتا ہے کہ خود کو differentiate کیسے کرنا ہے۔

تو ایک ایسا number بنائیں جو یاد رکھے کہ اسے کس نے produce کیا۔

value.pyPYTHON
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 Lself\frac{\partial L}{\partial \text{self}} کو 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 کریں۔

value.py (continued)PYTHON
    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
reluroutes — اسے pass کرتا ہے یا مکمل block کر دیتا ہے
tanhattenuates1t21 - t^2 سے scale کرتا ہے، جو زیادہ سے زیادہ 1 اور عموماً اس سے کم ہوتا ہے

ہر ایک += استعمال کرتا ہے اور کبھی = نہیں۔ یہی «sum across paths» rule encoded ہے۔ ایک node جو دو consumers کو feed کرتا ہے دو بار call ہوتا ہے، اور دونوں contributions خود بخود add up ہو جاتی ہیں۔

پھر driver، جو واحد حصہ ہے جس کے پاس کوئی global knowledge ہے:

value.py (continued)PYTHON
    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 نہیں ملتا۔

check.pyPYTHON
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)
TEXT
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.4033

1.8212۔ وہی number، ایک ایسے program سے جسے + کا rule، * کا rule، tanh کا rule بتایا گیا، اور اس circuit کے بارے میں کچھ نہیں۔

دو independent checks، کیونکہ «یہ میری derive کی ہوئی چیز سے match کرتا ہے» ایک کمزور test ہے جب دونوں کام اسی شخص نے کیے ہوں۔

Numerical differentiation۔ input کو ذرا nudge کریں اور measure کریں۔ centred difference L(x+h)L(xh)2h\frac{L(x+h) - L(x-h)}{2h} derivative کو بغیر کسی calculus کے estimate کرتا ہے:

TEXT
dL/dx:  analytic=1.821202805  numeric=1.821202805  |diff|=1.80e-10
dL/dy:  analytic=0.403269235  numeric=0.403269235  |diff|=7.64e-12

PyTorch کے خلاف، جس کا industrial autodiff engine ان لوگوں نے لکھا ہے جو یہی کام پیشہ ورانہ طور پر کرتے ہیں:

TEXT
torch dL/dx=1.821202805316   ours=1.821202805316   |diff|=2.22e-16
torch dL/dy=0.403269234753   ours=0.403269234753   |diff|=1.11e-16

2×10162 \times 10^{-16} پر agreement، جو 64-bit float کے لیے machine epsilon ہے: دونوں engines identical arithmetic perform کر رہے ہیں۔ numerical check اپنے پاس رکھیں — یہ نئی layer کے backward pass کو debug کرنے کا tool ہے، اور اسی وجہ سے wrong gradient findable ہوتا ہے۔

وہی circuit، different inputs۔ x=2x = 2 اور y=3y = -3 set کریں، جس سے c=6c = 6 بنتا ہے:

TEXT
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.9999

tanh\tanh node کو cross کرنے والا gradient 9,945 کے factor سے گر گیا۔ اس کے upstream میں موجود سب کچھ — real network میں، اس سے پہلے کی ہر layer — essentially کچھ نہیں پاتا۔ circuit کے دو paths silent ہو چکے ہیں؛ صرف direct connection جو tanh\tanh کو skip کرتا ہے signal لے جا رہا ہے۔

یہ vanishing gradient problem ہے، ایک node میں۔ tanh\tanh کی چالیس 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 کرنا پڑتا ہے:

train.pyPYTHON
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.grad

PyTorch میں یہی optimizer.zero_grad() ہے، اور عام مشورہ یہ ہے کہ اسے بھولنا training توڑ دیتا ہے۔ تو آئیے وہ دو lines delete کرتے ہیں اور دیکھتے ہیں کہ یہ کتنا broken ہے۔ same seeds، same everything، XOR کے 200 steps:

learning rateseedreset کے ساتھreset کے بغیر
0.051337loss 3.255088, 3/4loss 0.000000, 4/4
0.057loss 2.144820, 2/4loss 0.000000, 4/4
0.0542loss 2.126074, 2/4loss 0.000000, 4/4
0.11337loss 0.038597, 4/4loss 0.000000, 4/4
0.17loss 2.055048, 2/4loss 0.000000, 4/4
0.142loss 2.049876, 2/4loss 0.000073, 4/4
0.31337loss 4.512310, 2/4loss 8.000000, 2/4
0.37loss 0.015247, 4/4loss 4.000000, 3/4
0.342loss 0.005478, 4/4loss 4.000000, 3/4

چھوٹے learning rates پر، buggy version ہر row جیتتا ہے۔ correct version جہاں stall ہوتا ہے، یہ converge کر جاتا ہے۔

یہ fluke نہیں اور اسے سمجھنا ضروری ہے، کیونکہ یہی بتاتا ہے کہ یہ bug پکڑنا اتنا مشکل کیوں ہے۔ اگر آپ gradient کبھی clear نہیں کرتے، تو step kk پر parameter اب تک compute کیے گئے ہر gradient کے sum سے update ہوتا ہے۔ ایسے loss پر جو تقریباً اسی direction کی طرف point کرتا رہے، وہ sum مسلسل بڑھتا ہے، اور اثر ایک ایسے learning rate کا بنتا ہے جو خود بخود increase ہوتا ہے۔ η=0.05\eta = 0.05 پر، جہاں correct algorithm crawl کر رہا ہے، runaway step size بالکل fix جیسی لگتی ہے۔

پھر نیچے کی تین rows دیکھیں۔ η=0.3\eta = 0.3 پر یہی mechanism model کو اڑا دیتا ہے — loss 8.0 وہ score ہے جو constant ±1\pm 1 پر 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 کرتا ہے۔

engine مکمل ہو جائے تو neural network بمشکل ہی کوئی code ہے۔ ایک neuron dot product، bias اور activation ہے؛ ایک layer neurons کی list ہے؛ ایک network layers کی list ہے۔

nn.pyPYTHON
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:

TEXT
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، جنہوں نے bb کو positive بھی اور negative بھی ہونا لازم کیا — نو numbers سے compute ہوا جو automatically ملے۔

اطمینان بخش بات یہ نہیں کہ یہ کام کرتا ہے۔ بات یہ ہے کہ آپ دیکھ سکتے ہیں کیسے، کیونکہ دو hidden units کے ساتھ intermediate representation ایک plane میں point ہے اور آپ اسے simply print کر سکتے ہیں۔

0.001241 کے loss تک trained، یہاں ہر input hidden layer کے بعد کہاں land کرتا ہے، اور output neuron اس کے ساتھ کیا کرتا ہے:

inputhidden layer outputoutput scorelabel
(0,0)(0, 0)(+0.8206, 0.8474)(+0.8206,\ -0.8474)2.4045-2.40451-1
(0,1)(0, 1)(+0.9985, +0.8564)(+0.9985,\ +0.8564)+2.3049+2.3049+1+1
(1,0)(1, 0)(0.8401, 0.9990)(-0.8401,\ -0.9990)+2.3006+2.3006+1+1
(1,1)(1, 1)(+0.8368, 0.8550)(+0.8368,\ -0.8550)2.4786-2.47861-1

پہلی اور چوتھی rows دیکھیں۔ inputs (0,0)(0,0) اور (1,1)(1,1) square کے diagonally opposite corners ہیں — اس problem میں دو points جتنے دور ہو سکتے ہیں اتنے دور — اور hidden layer انہیں (0.82,0.85)(0.82, -0.85) اور (0.84,0.86)(0.84, -0.86) پر map کرتی ہے۔ تقریباً ایک ہی point۔ layer نے plane کو fold کر دیا تاکہ دونوں rejected corners ایک دوسرے کے اوپر land کریں، اور جب وہ ایک ہی جگہ آ جائیں تو ایک line انہیں باقی دو سے separate کر دیتی ہے۔

اور output neuron ٹھیک وہی line ہے۔ اس کے learned parameters w=(3.1153, +3.0893)\mathbf{w} = (-3.1153,\ +3.0893)، b=+2.7697b = +2.7697 ہیں، اس لیے اس کی decision boundary ہے

3.1153h1+3.0893h2+2.7697=0-3.1153\,h_1 + 3.0893\,h_2 + 2.7697 = 0

جو ایک 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 units4/4 تک پہنچنے والی initialisations
238 / 50 (76 %)
349 / 50 (98 %)
450 / 50 (100 %)
847 / 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 ہے، اور y=Wx\mathbf{y} = W\mathbf{x} کا backward pass ہے

LW=Lyx,Lx=WLy\frac{\partial L}{\partial W} = \frac{\partial L}{\partial \mathbf{y}}\mathbf{x}^\top, \qquad \frac{\partial L}{\partial \mathbf{x}} = W^\top\frac{\partial L}{\partial \mathbf{y}}

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 کا فرق ہے۔


اس 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۔

  1. 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 کے تحت۔

  2. 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 خرچ کرتا ہے۔

  3. Cybenko, G. Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals and Systems 2, pp. 303–314 (1989).

  4. Hornik, K. Approximation capabilities of multilayer feedforward networks. Neural Networks 4(2), pp. 251–257 (1991). Cybenko کو generalise کرتا ہے: result activation کے non-polynomial ہونے پر depend کرتا ہے، sigmoidal ہونے پر نہیں۔


تیار کردہ

David Vicente Campos

NeuraLIA Labs کے بانی اور MyRealFood کے شریک بانی

میں یونیورسٹی آف لیون سے کمپیوٹر انجینئر ہوں۔ میں نے MyRealFood کی مشترکہ بنیاد رکھی، جہاں بطور CTO میں نے وہ ایپ بنائی جسے لاکھوں لوگ بہتر غذا کے لیے استعمال کر چکے ہیں، اور میں نے NeuraLIA Labs قائم کیا، جہاں میں AI مصنوعات بناتا ہوں۔ یہاں میں ان باتوں کے بارے میں لکھتا ہوں جو اس سفر میں مجھے سمجھنی پڑیں، اس طرح جس طرح کاش کسی نے مجھے سمجھائی ہوتیں۔

مصنف کے بارے میں مزید

NeuraLIA Labs کی جانب سے شائع کردہ۔

نئی پوسٹس اپنے ان باکس میں پائیں

AI کی خبریں، گائیڈز اور پروڈکٹ اپ ڈیٹس — جب ہم آپ کے وقت کے قابل کچھ شائع کریں تو ایک مختصر ای میل۔

کورس انڈیکس

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev14 منٹ مطالعہ

Jev AI ماڈل فیصلوں کے لیے بنایا گیا ہے، نثر کے لیے نہیں

TypeSafe AI کا Jev اس لیے توجہ کھینچ رہا ہے کہ یہ software intelligence کو احتمال کے مسئلے کے طور پر دیکھتا ہے: درست branch چنیں، confidence منسلک کریں، اور جب code کو فیصلہ چاہیے ہو تو text لکھوانے کے لیے LLM کو ادائیگی سے بچیں۔

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering14 منٹ مطالعہ

طویل مدتی AI ایجنٹس کے لیے کانٹیکسٹ انجینئرنگ

طویل عرصے تک چلنے والے ایجنٹس صرف اس لیے ناکام نہیں ہوتے کہ ونڈو چھوٹی ہے۔ وہ اس وقت ناکام ہوتے ہیں جب فائلیں، ٹول آؤٹ پٹس اور پرانی ہسٹری اس کام کو باہر دھکیل دیتی ہیں جسے ایجنٹ نے مکمل کرنا تھا۔

ماڈل چننے کا کام LIA کے سپرد کرنے کے لیے تیار ہیں؟

ہر AI ماڈل ایک ہی جگہ — آج ہی مفت شروع کریں۔