Loss Functions for Binary Classification
Manual gradient computation uses grad(loss, tensor) from torch.autograd to ask how the loss changes with respect to a chosen parameter.
A Small Script to Start From
This article begins with a script rather than a definition. The script builds a tiny model with one input, one weight, and one bias. It passes the input through a sigmoid and then through a binary cross-entropy loss, and finally asks PyTorch for the gradient of that loss with respect to a parameter. As you trace it, watch how each variable receives its value and what the final gradient looks like when it is printed.
Keep one question in mind as you read: at which line does the state of the program stop matching what you would expect? Manual gradient code is short, so a mistake usually hides in one specific line. Finding that line is much faster when you already know what each line is supposed to produce.
Tracing the Forward Pass
Before any gradient is requested, the script has already built a chain of operations. The input x1 is multiplied by the weight w1, the bias b is added to produce z, the sigmoid of z gives a, and the binary cross-entropy of a against the target y gives the loss. PyTorch records each of these steps as it runs. That recorded chain of operations is what people mean by the computation graph.
The diagram shows the direction of data during the forward pass, from inputs toward the loss. Gradients run the other way, from the loss back toward the parameters. The parameters w1 and b are marked as requiring a gradient because the script creates them with requires_grad set to True. Tensors without that flag, such as the input x1 and the target y, are not the variables whose gradients this script asks for.
Reading the grad() Call
The grad function is imported from torch.autograd. It takes the loss and the variable you want to differentiate with respect to. In the script, the call grad(loss, b, retain_graph=True) asks how the loss changes as the bias b changes. The result is stored in a variable named grad_L_b. The same pattern works for any parameter created with requires_grad set to True, such as the weight w1.
The gradient answers a precise question: how does the loss change when one parameter changes? Written with calculus notation, the gradient of the loss L with respect to the bias b is $\frac{\partial L}{\partial b}$. The grad call is how you ask Autograd to compute this quantity for a tensor it has been tracking.
Why the Graph Is Freed by Default
By default, PyTorch destroys the computation graph after calculating the gradients, in order to free memory. This is a sensible default. A graph can hold many intermediate values, and once the gradient has been computed, a standard training step usually does not need them again. The consequence is that the graph is no longer available for another gradient request unless you ask PyTorch to keep it.
The script changes that default with retain_graph=True. This keyword argument tells PyTorch to keep the graph in memory after the call finishes. The reason is stated directly in the source: the script will reuse the same computation graph shortly, so the graph must stay available. Without that setting, a second request against the same loss can fail because the graph it needs has been released.
Tracing Two Gradients from One Loss
The source names two gradients that come from the same loss: one with respect to the weight w1 and one with respect to the bias b. The shared loss is the reason the graph must survive the first request. The code below is an original illustration of the same ordering pattern, using the same kind of model so the mechanism stays concrete.
- x1 and y are plain tensors with no gradient flag, so they are not differentiated
- w1 and b are created with
requires_grad=True, so Autograd tracks them - The forward pass builds the graph: multiply, add, sigmoid, then the loss
- The first grad call keeps the graph with
retain_graph=True - The second grad call uses the default, so the graph is freed when it finishes
- Each printed gradient is a tuple, so the output shows a trailing comma
What do you think happens?
If the first grad call omits retain_graph, and a second grad call then asks for a gradient from the same loss, what happens?
Reveal answer
The second request can fail, because the graph it needs has already been released.
By default PyTorch destroys the graph after gradients are calculated. Without retain_graph=True on the first call, the graph is gone before the second request runs.
Mistakes That Produce Unexpected Gradients
Common mistake Creating a parameter without requires_grad set to True
What it looks likew1 = torch.tensor(0.8) followed by grad(loss, w1)Why it failsAutograd only tracks tensors created with
requires_gradset to True, so the gradient request is not based on the tracked parameter.Do this insteadCreate the parameter with requires_grad=True, then build the loss from it.
Common mistake Omitting retain_graph=True on the first call when a second call needs the same loss
What it looks likegrad(loss, w1) followed later by grad(loss, b) with no setting on either call
Why it failsPyTorch frees the graph by default, so the second request may fail because the graph has been released.
Do this insteadAdd retain_graph=True to every call that needs the shared graph, except the final one when you no longer need it.
Common mistake Treating the tuple output as a bare tensor
What it looks likePrinting grad_L_b and reading it as if the value stood alone
Why it failsgrad returns its results inside a tuple, which is why the printed form has a parenthesis and a trailing comma.
Do this insteadRecognize the tuple wrapping before interpreting the printed value.
Common mistake Debugging a result by staring at the final print instead of checking the setup and each grad call in order
Finding Where a Gradient Goes Wrong
When a manual gradient script gives a result you did not expect, work through it in order rather than guessing. Start with the setup and confirm which tensors were created with requires_grad set to True and that the loss was built from them. Next, check each grad call: confirm which variable is being differentiated and whether retain_graph=True is present on every call that needs the shared graph. Finally, check the printed output and remember that the values come back wrapped in a tuple.
A script builds a loss from w1 and b, where both were created with requires_grad=True. It calls grad(loss, w1) first with no keyword argument, then calls grad(loss, b) on the same loss. Which line is the most likely origin of an error, and what single change would fix it?
Hints
- Start with the setup and ask whether the loss was built from tracked tensors.
- Ask whether the graph is still available when the second call runs.
- The first call is the one that decides whether the graph survives.
Summary of the Gradient Workflow
- 1Create parameters with
requires_gradset to True so Autograd tracks them. - 2Run the forward pass to build the computation graph from the inputs to the loss.
- 3Call
grad(loss, tensor)to ask how the loss changes with respect to that tensor. - 4Use
retain_graph=True on any call whose graph must survive for a later request. - 5When results look wrong, check the setup, then each grad call, then the tuple-wrapped output.
At a glance
- The gradient of a loss with respect to a parameter, written $\frac{\partial L}{\partial b}$, is requested with
grad(loss, tensor)from torch.autograd. - Parameters must be created with
requires_gradset to True for Autograd to track them. - PyTorch destroys the computation graph after gradients are calculated by default, and
retain_graph=True keeps it available for a later request. - grad returns its results inside a tuple, so printed values include a trailing comma.
- When a gradient looks wrong, check the setup first, then each grad call, then the printed output in that order.