{"slug": "pytorch-autograd-explained-what-backward-actually-does", "title": "PyTorch Autograd Explained: What .backward() Actually Does", "summary": "A developer explains the inner workings of PyTorch's autograd engine, detailing how the .backward() method computes gradients by walking a computation graph in reverse. The post uses the example y = x² to illustrate that PyTorch applies the chain rule node-by-node without ever forming the derivative expression explicitly.", "body_md": "most ** autograd** tutorials show you\n\nSkip this section if you are comfortable with it, but most confusion about autograd is really confusion about what it is producing.\n\nImagine standing on a hillside in fog. You cannot see the bottom, but you can feel that the ground tilts. Step in the direction it tilts downward and you get lower. That is a gradient: the slope of the ground under your feet.\n\nNow put that on a graph. The horizontal axis is one adjustable number inside the model, a parameter. The vertical axis is the loss. The curve shows how the loss changes as the parameter\n\nchanges, and somewhere on it there is a lowest point.\n\nTake `y = x²`\n\n. Its slope at position `x`\n\nis `2x`\n\n. At `x = 3`\n\nthe slope is 6, steep and positive, so the loss climbs to the right and you should step left. At `x = -2`\n\nit is -4, so you step right. At `x = 0`\n\nit is 0, and you have arrived.\n\n``` python\nimport torch\n\nx = torch.tensor(3.0, requires_grad=True)\ny = x ** 2\ny.backward()\nprint(x.grad)      # tensor(6.)\n```\n\nPyTorch produced the slope without being told the formula. That is autograd, and the rest of this article is how.\n\nTensors do not track gradients by default. That would be wasted work on your input data, which is never adjusted. You opt in with `requires_grad=True`\n\n:\n\n```\nx = torch.tensor(3.0, requires_grad=True)\nprint(x.requires_grad)    # True\n```\n\nFrom this moment every operation involving `x`\n\ngets recorded. Think of it as a receipt. Each multiplication, addition and power you apply gets written down, along with enough information to reverse it later.\n\nThe receipt has a proper name, the computation graph. It is a chain of nodes where each node is an operation and each edge carries a tensor from one operation into the next.\n\n```\nx = torch.tensor(3.0, requires_grad=True)\ny = x ** 2        # node: power\nz = 2 * y + 1     # nodes: multiply, then add\n```\n\nThree lines of Python, four values, three operation nodes:\n\n```\nx (leaf)      **2         ×2          +1        z\n  3.0    →    9.0    →   18.0   →    19.0   →  19.0\n```\n\nThis is the forward pass. It runs left to right, computes the answer, and as a side effect builds the graph. Nothing has been differentiated yet. The graph exists purely so that something can walk it backwards.\n\nTwo things are worth noticing here. First, `x`\n\nis a **leaf** — you created it directly rather than computing it from something else. Second, every result node remembers the operation that produced it. You can see this:\n\n```\nprint(z.grad_fn)     # <AddBackward0 object at ...>\nprint(y.grad_fn)     # <PowBackward0 object at ...>\nprint(x.grad_fn)     # None  ← leaves have no history\n```\n\n`grad_fn`\n\nis the recording. `AddBackward0`\n\nis not the addition, it is the *instruction for reversing* the addition.\n\n```\nz.backward()\nprint(x.grad)     # tensor(12.)\n```\n\nCheck it by hand. `z = 2x² + 1`\n\n, so `dz/dx = 4x`\n\n, and at `x = 3`\n\nthat is 12. Correct.\n\nBut the interesting part is not that the answer is right, it is how it was produced, because PyTorch never formed the expression `4x`\n\nat all. It walked the graph right to left and at each node multiplied the incoming gradient by that node's own local slope:\n\n```\nz = 19          +1              ×2              **2            x.grad\nstart           slope 1         slope 2         slope 2x = 6\ngrad 1     →    grad 1     →    grad 2     →    grad 12\n```\n\nReading right to left: the `+1`\n\nnode has slope 1, so the gradient passes through unchanged. The `×2`\n\nnode has slope 2, so the gradient doubles. The `x²`\n\nnode has slope `2x`\n\n, which at `x = 3`\n\nis 6, so the gradient multiplies by 6. Altogether `1 × 2 × 6 = 12`\n\n.\n\nThat is the whole of backpropagation. It is the chain rule, applied one node at a time, right to left, automatically. Nobody ever writes down `4x`\n\n.\n\nThis also explains why the forward pass has to happen first, and why it has to store its intermediate values. The `x²`\n\nnode's local slope is `2x`\n\n, which needs the *value* of `x`\n\nthat went in. Every node keeps whatever it needs to compute its own derivative later. This is where the memory goes during training, and it is why a batch that fits in memory for inference can still run you out of memory when training.\n\nEverything above is one variable. Here is the smallest thing that is honestly a neural network:\n\none input, one weight, one bias, a ReLU, and a squared-error loss. Five operations. Enough to show every part of the chain rule, small enough to hold in your head.\n\nNumbers: `x = 2.0`\n\n, `w = 3.0`\n\n, `b = -1.0`\n\n, target `y = 2.0`\n\n.\n\n**Forward pass**, left to right, writing down everything:\n\n```\nh = w·x + b     = 3·2 + (-1)  = 5.0\na = ReLU(h)     = max(0, 5)   = 5.0\nL = (a - y)²    = (5 - 2)²    = 9.0\n```\n\nLoss is 9. Now the question that matters: how should `w`\n\nand `b`\n\nchange to make it smaller?\n\n**Backward pass**, one node at a time, right to left. At each node, local derivative times the\n\ngradient arriving from the right.\n\n*Loss to a.*\n\n`L = (a - y)²`\n\n, so `∂L/∂a = 2(a - y) = 2(5 - 2) = 6.0`\n\n* a to h, through the ReLU.* ReLU's derivative is 1 if its input was positive and 0 if it was\n\n`h`\n\nwas 5, positive, so `∂a/∂h = 1.0`\n\nand therefore`∂L/∂h = 6.0 × 1.0 = 6.0`\n\n* h to w and b.* Since\n\n`h = w·x + b`\n\n:\n\n```\n∂h/∂w = x = 2.0    →    ∂L/∂w = 6.0 × 2.0 = 12.0\n∂h/∂b = 1.0        →    ∂L/∂b = 6.0 × 1.0 = 6.0\n```\n\nSo by hand: `∂L/∂w = 12`\n\nand `∂L/∂b = 6`\n\n.\n\n**Now ask PyTorch the same question:**\n\n```\nx = torch.tensor(2.0)\nw = torch.tensor(3.0, requires_grad=True)\nb = torch.tensor(-1.0, requires_grad=True)\ny = torch.tensor(2.0)\n\nh = w * x + b\na = torch.relu(h)\nloss = (a - y) ** 2\n\nloss.backward()\n\nprint(f\"loss  = {loss.item()}\")     # 9.0\nprint(f\"dL/dw = {w.grad.item()}\")   # 12.0\nprint(f\"dL/db = {b.grad.item()}\")   # 6.0\n```\n\nExact match, all three. Autograd traced the same five operations and applied the same chain rule. It just did it without asking you.\n\nIf you read nothing else here, run that block and compare it to the hand calculation above. The whole point of the exercise is the moment the numbers agree.\n\nChange one number. Keep `w = 3`\n\nand `b = -1`\n\n, but feed `x = -1.0`\n\n. Now `h = 3·(-1) + (-1) = -4`\n\n, which is negative, so ReLU outputs 0 and its local derivative is also 0.\n\n```\nx = torch.tensor(-1.0)\nw = torch.tensor(3.0, requires_grad=True)\nb = torch.tensor(-1.0, requires_grad=True)\ny = torch.tensor(2.0)\n\nh = w * x + b          # -4.0\na = torch.relu(h)      #  0.0\nloss = (a - y) ** 2    # (0 - 2)² = 4.0\nloss.backward()\n\nprint(w.grad.item())   # 0.0\nprint(b.grad.item())   # 0.0\n```\n\nThe loss is 4, so the network is wrong, and yet both gradients are exactly zero. No learning\n\nsignal reaches `w`\n\nor `b`\n\nat all.\n\nThis follows straight from the hand calculation. `∂a/∂h = 0`\n\n, and every gradient behind that node gets multiplied by zero on its way through. The ReLU is a gate, and this one is shut.\n\nThat is the dying-ReLU problem, and it is worth meeting it here rather than three months later in a model with fifty layers. When a neuron's input is negative for every example in your data, its gradient is permanently zero and it never learns again. `LeakyReLU`\n\nexists because it lets a small gradient through instead.\n\nMore generally: when a model stops learning and you cannot see why, the question to ask is what is multiplying the gradient by zero on the way back.\n\n`.grad`\n\nIntermediate results do not store their gradient. It is computed, used to keep the chain going, and thrown away, because keeping every intermediate gradient in a real model would be enormous. If you want one, ask before calling backward:\n\n```\nx = torch.tensor(2.0, requires_grad=True)\ny = x ** 3\ny.retain_grad()        # keep y's gradient too\nz = y * 5\nz.backward()\n\nprint(x.grad)    # tensor(60.)   dz/dx = 15x² = 60\nprint(y.grad)    # tensor(5.)    dz/dy = 5\n```\n\nWithout `retain_grad()`\n\n, `y.grad`\n\nis `None`\n\nand you get a warning rather than an error, which is why people spend twenty minutes confused by it.\n\nThis one causes more silently broken training loops than anything else in PyTorch.\n\n```\nx = torch.tensor(3.0, requires_grad=True)\n\nfor i in range(3):\n    y = x ** 2\n    y.backward()\n    print(f\"step {i}  grad = {x.grad.item()}\")\nstep 0  grad = 6.0\nstep 1  grad = 12.0\nstep 2  grad = 18.0\n```\n\nThe gradient of `x²`\n\nat `x = 3`\n\nis 6, every time. It reads 12 and then 18 because `.grad`\n\nis added to, not replaced.\n\nThis is deliberate. It is what lets you accumulate gradients over several mini-batches and take one larger step, which is how people train with an effective batch size their GPU cannot hold.\n\nBut it means that unless you clear it, every step of your training loop is stepping on a sum of all previous gradients, and your loss curve will do something strange that is very hard to diagnose from the outside.\n\nThe fix is `x.grad.zero_()`\n\n, or in a real loop, `optimizer.zero_grad()`\n\nbefore every `loss.backward()`\n\n. That line is not boilerplate. It is load-bearing.\n\n`no_grad`\n\nand `detach`\n\n— switching the recorder off\nDuring evaluation you do not need gradients, and building the graph costs both time and memory.\n\n```\nx = torch.tensor(3.0, requires_grad=True)\n\nwith torch.no_grad():\n    y = x ** 2\n    print(y.requires_grad)   # False\n```\n\nInside the block no graph is built. This is why every evaluation loop you have ever copied is wrapped in `torch.no_grad()`\n\n.\n\n`.detach()`\n\nis the narrower tool. It gives you a tensor that shares the same data but has no connection to the graph:\n\n```\nx = torch.tensor(3.0, requires_grad=True)\ny = x ** 2\ny_val = y.detach()\n\nprint(y_val)                  # tensor(9.)\nprint(y_val.requires_grad)    # False\n```\n\nUse it when you want a value without dragging its history along — logging, or using a model's output as a target that should not be differentiated through. It is the mechanism behind stop-gradient tricks and target networks, and it is also, occasionally, the reason your gradient is unexpectedly zero.\n\nEverything so far had one or two parameters so the arithmetic stayed visible. Nothing changes at scale.\n\n```\nw = torch.tensor(2.0, requires_grad=True)\nb = torch.tensor(1.0, requires_grad=True)\nx = torch.tensor(3.0)\n\ny = w * x + b            # 2·3 + 1 = 7\nloss = (y - 5) ** 2      # (7 - 5)² = 4\n\nloss.backward()\n\nprint(w.grad)   # tensor(12.)\nprint(b.grad)   # tensor(4.)\n```\n\nOne `backward()`\n\nfilled in both. In a model with eleven million parameters, the same single call fills in all eleven million, because the graph reaches every one of them and the chain rule multiplies along every path. Nothing about the mechanism is different. There are just more nodes.\n\n`requires_grad=True`\n\nmarks a tensor as something you want the gradient of. Model parameters\nget it automatically; your data does not need it.`.backward()`\n\nwalks that graph in reverse, multiplying the incoming gradient by each node's\nlocal derivative. That is the chain rule, and it is all backpropagation is.`.grad`\n\non leaf tensors only. Use `retain_grad()`\n\nfor intermediates.`zero_grad()`\n\nbefore every backward, always.`torch.no_grad()`\n\nfor evaluation, `.detach()`\n\nfor a single value without its history.Take `x = 4.0`\n\nwith `requires_grad=True`\n\n, compute `y = x**3 - 2*x`\n\n, call `backward()`\n\n, and print\n\n`x.grad`\n\n. Then work out `3x² - 2`\n\nat `x = 4`\n\non paper and check whether they agree. It takes\n\nabout ninety seconds and it is worth more than rereading this article.\n\nThen the harder version: put a second weight after the ReLU in the tiny network above, `w2 =`\n\n, and write out the full chain rule for\n\n0.5`∂L/∂w`\n\nbefore you run it.\n\n*This is one chapter's worth of an idea from my book,* **PyTorch From Ground Up**, *which builds\neverything from tensors upward so nothing stays vague. If it helped: 8 chapters are free, no email required,\nthere's a free one-page tensor cheat-sheet here, every example runs\nin the companion notebooks on GitHub, and\nthe full book is on Leanpub or in\npaperback and Kindle on Amazon.*\n\nShape mechanics, the part that has to be solid before any of this makes sense:", "url": "https://wpnews.pro/news/pytorch-autograd-explained-what-backward-actually-does", "canonical_source": "https://dev.to/pytorchfromgroundup/pytorch-autograd-explained-what-backward-actually-does-4acg", "published_at": "2026-08-12 20:50:08+00:00", "updated_at": "2026-08-12 21:20:39.834340+00:00", "lang": "en", "topics": ["machine-learning", "developer-tools"], "entities": ["PyTorch"], "alternates": {"html": "https://wpnews.pro/news/pytorch-autograd-explained-what-backward-actually-does", "markdown": "https://wpnews.pro/news/pytorch-autograd-explained-what-backward-actually-does.md", "text": "https://wpnews.pro/news/pytorch-autograd-explained-what-backward-actually-does.txt", "jsonld": "https://wpnews.pro/news/pytorch-autograd-explained-what-backward-actually-does.jsonld"}}