सामग्री पर जाएँ
5/30अध्याय 5 / 30

Backpropagation शुरू से: पहले इंजन, फिर नेटवर्क

Pure Python में 120-line autodiff engine लिखें, उसे PyTorch से 16 decimals तक मिलाएँ, और zero_grad को हटाकर समझें।

इस पेज पर

चार chapters के बाद, course के बीचोंबीच एक खाली जगह बची है।

Chapter 3 ने हमें gradient descent दिया: किसी parameter को बेहतर करना हो, तो loss के संबंध में उसकी slope निकालो और downhill कदम बढ़ाओ। Chapter 4 ने हमें ऐसा loss दिया जिस पर उतरना सार्थक था। लेकिन दोनों में derivative हाथ से निकाला गया था — एक model, एक parameter, calculus की एक line, और सब कुछ एक page में फिट हो गया।

अब दो layers stack करें। पहली का output दूसरी को feed करता है, इसलिए पहली layer का हर weight, दूसरी layer के हर neuron के through loss को प्रभावित करता है। सौ units वाली दो hidden layers वाले network में करीब बीस हज़ार parameters होते हैं, और हर एक को उसी loss का अपना partial derivative चाहिए। इसे हाथ से करना tedious नहीं है; असंभव है, और इस course के बाकी हर architecture के लिए असंभव ही रहता है।

रास्ता बेहतर notation नहीं है। रास्ता यह समझना है कि composition का derivative computation की structure से ही, एक program द्वारा, mechanically निकाला जा सकता है — और अगर आप इसे सही दिशा में करते हैं, तो loss को एक बार compute करने की लगभग cost में सभी बीस हज़ार derivatives मिल जाते हैं।

यह mechanism reverse-mode automatic differentiation है। neural network पर लागू होने पर इसे backpropagation कहा जाता है, और इस chapter के अंत तक आप बिना libraries के लगभग 120 lines Python में इसे लिख चुके होंगे, PyTorch से check कर चुके होंगे, और इसका उपयोग उस XOR problem को solve करने में कर चुके होंगे जिसने Chapter 1 में perceptron को हरा दिया था।

Machine बनाने से पहले एक सवाल तय करना ज़रूरी है, क्योंकि अगर जवाब दूसरी तरफ जाता तो बनाने को कुछ होता ही नहीं।

Perceptron XOR पर इसलिए fail हुआ क्योंकि एक line चार points को separate नहीं कर सकती। साफ़ fix है stack करना: input को एक linear layer से चलाओ, फिर दूसरी से। क्या इससे मदद मिलती है?

नहीं, और proof दो lines का है। एक 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}_2Linear layers का stack एक single 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

लगभग equal नहीं। Bit-for-bit identical, क्योंकि वही arithmetic बस rearranged है।

तो depth अपने-आप कुछ नहीं खरीदती। जो चीज़ कुछ खरीदती है वह है layers के बीच एक nonlinear function रखना — और activation functions के होने की पूरी वजह यही है। वे कोई biological flourish या normalisation trick नहीं हैं। इनके बिना, second layer सिर्फ decoration है।

अब mathematics, और यह वही एक rule है जिसे आप पहले से जानते हैं, बस थोड़ी unfamiliar जगह लागू किया गया है।

Single-variable chain rule कहता है कि अगर LL, cc पर depend करता है और cc, xx पर depend करता है, तो dLdx=dLdcdcdx\frac{dL}{dx} = \frac{dL}{dc} \cdot \frac{dc}{dx}। Derivatives chain के along multiply होते हैं।

यहाँ जो हिस्सा मायने रखता है वह है कि जब कोई variable downstream में एक से अधिक paths को feed करता है तो क्या होता है। अगर xx, LL को aa के through भी influence करता है और bb के through भी, तो contributions add होते हैं:

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 के along multiply करें, paths के across sum करें। यही पूरा backpropagation है, और इस chapter के बाकी सभी implementation details — code में += और वह zero_grad() call भी जो अपना पहला training loop लिखने वाले हर व्यक्ति को उलझाती है — उस दूसरे शब्द का सीधा consequence हैं।

पाँच 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 को paper पर, right to left करें, dLdL=1\frac{dL}{dL} = 1 से शुरू करके:

L=d+xL = d + x, इसलिए Ld=1\frac{\partial L}{\partial d} = 1 और direct path Lx=1\frac{\partial L}{\partial x} = 1 contribute करता है। Addition incoming gradient को unchanged दोनों 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.1711Multiplication swaps: हर input का gradient दूसरे input से scale होता है।

aa के through: dLday=0.4644×1.4=0.6501\frac{dL}{da} \cdot y = 0.4644 \times 1.4 = 0.6501bb के through: 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 इसे produce करेगा, बिना इसके बारे में कुछ भी बताए।

इसे programmable बनाने वाली insight: उन 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 अपने inputs तक एक step पीछे कैसे 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 वहीं मौजूद हैं:

operationwhat it does to the gradient
+distributes — हर input को वही gradient
*swaps — हर input दूसरे की value से scaled
reluroutes — इसे pass करता है या पूरी तरह block करता है
tanhattenuates1t21 - t^2 से scale करता है, जो अधिकतम 1 और आमतौर पर उससे कम होता है

इनमें से हर एक += use करता है और कभी = नहीं। यही “sum across paths” rule encoded है। जो node दो consumers को feed करता है वह दो बार called होता है, और दोनों contributions अपने-आप add हो जाते हैं।

फिर 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 produce करता है: हर node अपने सभी inputs के बाद आता है। उस list को reverse में walk करना guarantee करता है कि जब आप किसी node का _backward call करते हैं, उसका अपना gradient पहले से complete होता है — उसके downstream हर consumer ने पहले ही contribute कर दिया है। Order गलत हो तो आप half-finished gradient को पीछे push करते हैं, जिससे बिना error message के गलत answer मिलता है।

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, क्योंकि “यह मेरे derived result से match करता है” weak test है जब दोनों काम एक ही व्यक्ति ने किए हों।

Numerical differentiation। Input को थोड़ा nudge करें और measure करें। Centred difference L(x+h)L(xh)2h\frac{L(x+h) - L(x-h)}{2h} बिना किसी calculus के derivative 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 के against, जिसके पास उन लोगों द्वारा लिखा गया 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 है, और इसी वजह से गलत gradient मिलना संभव होता है।

वही circuit, अलग 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 — लगभग कुछ नहीं receive करता। Circuit के through जाने वाले दो paths silent हो चुके हैं; सिर्फ वह direct connection जो tanh\tanh को skip करता है signal carry करता है।

यह vanishing gradient problem है, एक node में। tanh\tanh की चालीस layers stack करें और ऐसे चालीस factors को multiply करें, और early layers पूरी तरह सीखना बंद कर देती हैं। यह, incidentally, skip connections के पक्ष में एक argument भी है जिसे आप यहाँ miniature में देख सकते हैं: वह path जिसने nonlinearity को bypass किया, वही बचा।

हर _backward += use करता है। यह सही है — paths इसी तरह sum होते हैं। लेकिन इसका एक consequence है जो सबको पकड़ता है: gradients backward() calls के across भी accumulate होते हैं। Engine को यह पता नहीं कि आपका दूसरा call उसी graph का another 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() है, और usual advice है कि इसे भूलना training को break कर देता है। तो चलिए उन दो lines को delete करते हैं और देखते हैं यह कितना broken है। Same seeds, same everything, XOR के 200 steps:

learning rateseedwith resetwithout 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 जीतता है। यह converge करता है जब correct version stall हो जाता है।

यह fluke नहीं है और इसे समझना ज़रूरी है, क्योंकि इससे पता चलता है कि यह bug पकड़ना इतना मुश्किल क्यों है। अगर आप gradient को कभी clear नहीं करते, तो step kk पर parameter अब तक compute किए गए हर gradient के sum से update होता है। ऐसे loss पर जो लगभग उसी दिशा की ओर point करता रहता है, वह sum steady बढ़ता है, और effect ऐसा learning rate है जो अपने-आप बढ़ता जाता है। η=0.05\eta = 0.05 पर, जहाँ correct algorithm रेंग रहा है, runaway step size बिल्कुल fix जैसा दिखता है।

फिर नीचे की तीन rows देखें। η=0.3\eta = 0.3 पर वही mechanism model को तोड़ देता है — loss 8.0 वह score है जो constant ±1\pm 1 पर collapsed model पाता है — चार maximally wrong answers से आने वाले 16 का आधा — — जबकि correct version अब साफ़ converge करता है।

तो honest statement यह नहीं है कि “हमेशा zero_grad call करें वरना आपका model train नहीं होगा”। यह है: इसके बिना आप अब gradient descent चला ही नहीं रहे। आप कुछ ऐसा चला रहे हैं जिसकी step size ऐसी rate से ऊपर drift करती है जिसे किसी ने choose नहीं किया, और यह काम करता हुआ दिखेगा, कभी-कभी असली चीज़ से बेहतर भी, ठीक उस समय तक जब तक यह नहीं करता — और तब आप learning rate, initialisation, या data को दोष देंगे। Machine learning के सबसे खराब bugs का shape यही है: वे crash नहीं करते, वे algorithm को एक अलग algorithm में बदल देते हैं जो कभी-कभी बेहतर score करता है।

Engine पूरा होने के बाद, neural network barely any 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 कैसे करना है, और इसे पहले लिखने का point यही था: 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 से prove किया गया था जिन्होंने bb को positive और negative दोनों होना माँगा — नौ automatically मिले numbers से compute होता है।

संतोषजनक हिस्सा यह नहीं है कि यह काम करता है। वह यह देख पाना है कि कैसे, क्योंकि दो hidden units के साथ intermediate representation plane में एक point है और आप उसे बस print कर सकते हैं।

0.001241 के loss तक trained होने पर, hidden layer के बाद हर input कहाँ 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 करें, और जब वे same जगह पर हैं, तो एक 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 solve नहीं कर सका और अब भी नहीं कर सकता। जो बदला है वह यह है कि अब यह input को नहीं देख रहा; यह उस space को देख रहा है जिसे पहली layer ने इसके लिए बनाया, जिसमें problem linearly separable है।

यही learned representation है, और precise रहना ज़रूरी है क्योंकि यह phrase इस course के बाकी हिस्से और field के बाकी हिस्से में loosely use होगी। यह कोई compression, summary, या किसी mystical sense में embedding नहीं है। यह coordinates का change है, designed के बजाय learned, जिसका एकमात्र काम अगली layer का काम आसान बनाना है।

यहाँ एक theorem है, और आमतौर पर उसे badly quote किया जाता है।

Cybenko ने 1989 में और Hornik ने 1991 में prove किया कि single hidden layer और suitable activation function वाला feedforward network compact set पर किसी भी continuous function को, जितनी accuracy आप चाहें, approximate कर सकता है, अगर hidden units पर्याप्त हों।34 यह genuine और important result है: यह कहता है कि architecture limitation नहीं है।

अब पढ़ें कि यह क्या omit करता है। यह नहीं कहता कि कितने units — bound astronomically large हो सकता है। यह नहीं कहता कि weights found किए जा सकते हैं; यह existence assert करता है, और random start से gradient descent कोई oracle नहीं है। और यह unseen data पर behaviour के बारे में कुछ नहीं कहता, जो Chapter 6 का दूसरा half है।

“exists” और “findable” के बीच gap academic नहीं है। यहाँ वही XOR problem है, हर एक में 50 random initialisations, 1000 steps, सिर्फ hidden layer size बदला गया:

hidden unitsinitialisations reaching 4/4
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 पहले से sufficient हैं — 38 runs इसे prove करते हैं) बल्कि इसलिए कि extra dimensions descent को escape करने के लिए अधिक directions देते हैं।

और फिर आठ units, चार से थोड़ा worse करते हैं। Fixed learning rate और step budget पर, अधिक capacity monotonically better नहीं है। जो भी आपको बताता है कि stuck network का fix हमेशा बड़ा network है, वह उस table के middle से extrapolate कर रहा है।

यह Chapter 1 के convergence theorem जैसा ही lesson है, और Chapter 10 में scaling laws के बारे में भी यही lesson होगा, उस chapter के दिए form में: loss की prediction उस capability की prediction नहीं है जिसके लिए आप pay कर रहे हैं, और दोनों के बीच की दूरी ही engineering की जगह है।

विवरण दिखाएँ

Optional: matrix form, और ऊपर का code इसे use क्यों नहीं करता।

यहाँ सब कुछ एक समय में एक scalar लिखा गया है, जो mechanism देखने का सबसे clear तरीका है और 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 में सोलह million entries होती हैं और उसे बनाना कभी worth नहीं होता।

अगले chapters follow करने के लिए आपको इनमें से कुछ भी ज़रूरी नहीं; scalar version वही सब करता है जो matrix version करता है, बस slowly। यह Chapter 9 में ज़रूरी हो जाता है, जहाँ shapes obvious होना बंद कर देती हैं।

अब आपके पास ऐसा network है जो train करता है। यह जितना लगता है उससे छोटी उपलब्धि है, क्योंकि आपके पास जो network है वह चार examples पर train करता है और उन्हीं चार पर measure होता है।

उसी code को real dataset पर चलाएँ और problems का नया set सामने आता है, जिनमें से कोई भी gradients के बारे में नहीं है। Loss कुछ समय तक नीचे जाता है और फिर रुक जाता है। या यह training data पर नीचे जाता है और बाकी सब पर ऊपर। या यह पहले step से बिल्कुल नहीं हिलता, और वजह initial random weights की range निकलती है। या epoch three में किसी unit का input हर example पर negative drift कर गया और तब से वह silently dead है, model की capacity का एक chunk अपने साथ ले जाते हुए।

ये exotic failures नहीं हैं; ये अभी-अभी लिखे गए network की normal condition हैं, और इनमें से कोई खुद announcement नहीं करता। Gradient correct है — आपने इसे PyTorch से sixteen decimal places तक check किया — और model फिर भी नहीं सीखता।

Chapter 6 इसी के बारे में है: initialisation, normalisation, overfitting और regularisation, और कुछ भी बदलने से पहले यह पूछने की diagnostic habit कि इनमें से कौन सा हो रहा है। यही फर्क है ऐसे network में जो run करता है और ऐसे network में जो काम करता है।


इस chapter की Value class सीधे Andrej Karpathy के micrograd से निकली है, और उनका video The spelled-out intro to neural networks and backpropagation: building micrograd इस material पर खर्च किए जा सकने वाले सबसे अच्छे तीन घंटे हैं, अगर आप इसे किसी और से दूसरे तरीके से समझना चाहते हैं। उनका 2016 post Yes you should understand backprop खुद एक लिखने के पक्ष में argument देता है और 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 है, जिसमें ऊपर 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 तक पहुँचने से सोलह साल पहले और बिल्कुल अलग 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.
jev13 मिनट पढ़ें

Jev AI मॉडल गद्य के लिए नहीं, निर्णयों के लिए बना है

TypeSafe AI का Jev ध्यान खींच रहा है क्योंकि यह सॉफ्टवेयर इंटेलिजेंस को संभावना की समस्या मानता है: सही शाखा चुनें, भरोसे का स्तर जोड़ें, और जब कोड को निर्णय चाहिए तो LLM से टेक्स्ट लिखवाने पर खर्च न करें।

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering14 मिनट पढ़ें

लॉन्ग-होराइजन AI एजेंट्स के लिए कॉन्टेक्स्ट इंजीनियरिंग

लंबे समय तक चलने वाले एजेंट सिर्फ इसलिए असफल नहीं होते कि विंडो छोटी है। वे तब असफल होते हैं जब फ़ाइलें, टूल आउटपुट और पुराना इतिहास उस काम को ही पीछे धकेल देते हैं जिसे एजेंट को पूरा करना था।

मॉडल चुनने का काम LIA पर छोड़ने के लिए तैयार हैं?

हर AI मॉडल एक ही जगह — आज ही मुफ़्त शुरू करें।