During the last over of a tight IPL run chase, the broadcast graphic flashes a number: Win Probability: 73%. It updates after almost every ball, climbing when a boundary is hit and dropping when a wicket falls. That number is not a commentator's guess. It is the output of a small neural network, fed numbers like runs required, balls remaining, and wickets in hand, recalculated in a fraction of a second for millions of viewers at once.
You already know, from earlier chapters, roughly how such a network works: multiply each input by a weight, add a bias, pass the result through an activation function, compare the output to reality with a loss function, and use the gradient of that loss to nudge every weight in a better direction. You may even have written this out in plain Python, one line of arithmetic at a time. That exercise was worth doing: it is the only real way to understand what a neural network is doing underneath. But no broadcaster's engineers are typing out derivatives by hand during a live match, and no research lab trains a network with millions of weights that way either. They reach for a framework: a software library that has already written, tested, and optimised the repetitive mathematics of neural networks, so a human only has to describe the network's shape and supply the data. The framework used by the majority of AI researchers today, and in production systems at organisations such as Meta, OpenAI, and Tesla, is called PyTorch.
The Trouble With Doing It All By Hand
Think back to what a single artificial neuron with two inputs actually requires. You compute a weighted sum, apply an activation function, calculate a loss, and then, to run gradient descent, you need the derivative of that loss with respect to every weight and the bias. For one neuron with two inputs, that is three derivatives, worked out by hand with the chain rule in a few minutes.
Now picture a network with three hidden layers of sixty-four neurons each, where every neuron in one layer connects to every neuron in the next. Training it needs derivatives for tens of thousands of individual weights, each one requiring the chain rule to be applied correctly through every layer the signal passed on its way to the output. Get one sign wrong, or drop one term, in one of those tens of thousands of hand-derived formulas, and the network trains incorrectly, usually with no error message at all, just a model that never learns properly. Past a certain size, doing this by hand does not merely take a long time; it becomes practically impossible to get right.
This is precisely the gap a deep learning framework fills. A framework needs to do two things well:
- Store and manipulate large grids of numbers efficiently, ideally on specialised hardware.
- Automatically work out every derivative needed for gradient descent, no matter how large the network grows, without the programmer writing a single line of calculus.
PyTorch does both, built on two ideas you are about to meet: the tensor and autograd.
What Exactly Is PyTorch?
PyTorch is an open-source library for building and training neural networks, released by Facebook's AI Research lab (FAIR) in 2016. Its governance later passed to the independent PyTorch Foundation, hosted by the Linux Foundation, which oversees the project today. PyTorch is free to use, and its source code is public for anyone to inspect or contribute to.
You write PyTorch code in Python, the same language used in earlier chapters, but the heavy numerical work underneath runs as highly optimised C++ code, and, when a compatible graphics card is available, as CUDA code executing directly on the GPU. This split matters: you write short, readable Python describing what the network should look like, while the actual multiplication of millions of numbers happens in code built purely for speed. Installing it, in most environments, takes one command:
pip install torch
On Google Colab, a free notebook environment many Indian students use precisely because it needs no powerful computer at home, PyTorch already comes pre-installed, along with time-limited but free access to a GPU.
Tensors: The Basic Unit of Data in PyTorch
Every number, list of numbers, or grid of numbers that flows through a PyTorch network is stored as a tensor. A tensor generalises the numeric objects you already know: a single number is a 0-dimensional tensor, a list of numbers is a 1-dimensional tensor, a table of numbers with rows and columns is a 2-dimensional tensor, and PyTorch is equally comfortable with three, four, or more dimensions, useful when a single input is an entire image, or a whole batch of images at once.
Creating one looks almost exactly like creating a Python list:
import torch
required_run_rate = torch.tensor([9.0, 6.0, 12.0])
print(required_run_rate)
print(required_run_rate.shape)
print(required_run_rate.dtype)
This prints tensor([ 9., 6., 12.]), then torch.Size([3]) (confirming a 1-dimensional tensor holding three values), and then torch.float32, the default numeric type PyTorch uses for decimals. A tensor looks and behaves much like a NumPy array, and converts to and from one easily, but it carries two abilities a plain array does not: it can be moved onto a GPU for faster computation, and, when asked, it can keep a record of every mathematical operation performed on it so that gradients can be worked out automatically later. That second ability is autograd, and it is the single most important feature in the whole library.
Autograd: Calculus PyTorch Does For You
Set the flag requires_grad=True on a tensor, and PyTorch quietly starts recording every operation performed on it, building what is called a computation graph, a map of exactly how the inputs turned into the output. Call .backward() on the final result, and PyTorch walks that map backward, applying the chain rule at every step automatically, storing the resulting derivative in each tensor's .grad attribute. This system is called autograd, short for automatic differentiation, and it is what removes hand-derived calculus from neural network code entirely.
A minimal example makes this concrete:
import torch
w = torch.tensor(2.0, requires_grad=True)
y = w ** 2
y.backward()
print(w.grad)
Here y equals w squared, so by ordinary calculus dy/dw = 2w, which at w = 2.0 equals 4.0. PyTorch never saw that formula written anywhere. It only saw the operation w ** 2 happen, recorded it, and reconstructed the derivative when asked. The output is tensor(4.), matching the hand calculation exactly. Scale this up to a network with fifty thousand weights spread across a dozen layers, and the same single call, .backward(), correctly computes every one of those fifty thousand derivatives, every time.
A Full Worked Example: Predicting a Run Chase
To see autograd do real work, trace one complete forward-and-backward pass of a tiny neuron, the same kind of neuron that could sit behind a win-probability graphic, first by hand, then in PyTorch, and confirm the two match.
Give the neuron two inputs: the required run rate, x1 = 9.0, and the wickets in hand, x2 = 4.0. To illustrate the mechanics, start it with weight w1 = -0.4 for required run rate (a higher required rate should pull win probability down, hence negative), weight w2 = 0.5 for wickets in hand (more wickets should push probability up, hence positive), and bias b = 0.2. These are illustrative starting values chosen to make the arithmetic easy to follow, not numbers from any real trained model. The point is to watch exactly what happens to them.
Step 1 — the forward pass. The neuron first computes a weighted sum, then squashes it through the sigmoid function so the result reads as a probability between 0 and 1:
import math
x1, x2 = 9.0, 4.0
w1, w2, b = -0.4, 0.5, 0.2
z = w1*x1 + w2*x2 + b
p = 1 / (1 + math.exp(-z))
print(round(z, 4), round(p, 4))
This prints -1.4 0.1978. Working the sum by hand confirms it: -0.4 × 9.0 = -3.6, 0.5 × 4.0 = 2.0, and -3.6 + 2.0 + 0.2 = -1.4. Passed through sigmoid, that is 1 / (1 + e^1.4); since e^1.4 ≈ 4.055, the result comes out to roughly 0.1978. With these starting weights, the neuron predicts about a 19.8% chance of winning.
Step 2 — the loss. Suppose the chasing team actually won, so y = 1. Using binary cross-entropy loss, L = -[y·ln(p) + (1-y)·ln(1-p)], which simplifies to L = -ln(p) whenever y = 1:
y = 1.0
loss = -(y*math.log(p) + (1 - y)*math.log(1 - p))
print(round(loss, 4))
This prints 1.6204. A loss this high makes sense: the network predicted only a 19.8% chance for the exact outcome that actually happened, so it should be pushed hard to correct itself.
Step 3 — the gradients. For a sigmoid neuron trained with binary cross-entropy loss, the calculus simplifies beautifully: the derivative of the loss with respect to z is just p - y. From there, the derivative with respect to each weight is that value multiplied by the matching input, and the derivative with respect to the bias is that value alone.
Step 4 — the update. With a learning rate of 0.01, gradient descent subtracts the learning rate times each gradient from the matching parameter:
dz = p - y
grad_w1, grad_w2, grad_b = dz*x1, dz*x2, dz
lr = 0.01
w1_new = w1 - lr*grad_w1
w2_new = w2 - lr*grad_w2
b_new = b - lr*grad_b
print(round(grad_w1, 4), round(grad_w2, 4), round(grad_b, 4))
print(round(w1_new, 4), round(w2_new, 4), round(b_new, 4))
This prints gradients of roughly -7.2197, -3.2087, and -0.8022 for w1, w2, and b, and updated values of roughly -0.3278, 0.5321, and 0.208. Notice the direction of every change: w1 moved up (softening the penalty attached to a high required run rate), w2 moved up (valuing wickets in hand a little more), and the bias rose too. All three changes push the next prediction closer to 1, correcting toward the outcome that actually happened. That is gradient descent, working after a single step.
Now the same forward-and-backward pass, in PyTorch:
import torch
x = torch.tensor([9.0, 4.0])
w = torch.tensor([-0.4, 0.5], requires_grad=True)
b = torch.tensor(0.2, requires_grad=True)
z = torch.dot(w, x) + b
p = torch.sigmoid(z)
y = torch.tensor(1.0)
loss = -(y * torch.log(p) + (1 - y) * torch.log(1 - p))
loss.backward()
print(round(p.item(), 4), round(loss.item(), 4))
print(w.grad)
print(b.grad)
This prints 0.1978 1.6204, then tensor([-7.2197, -3.2087]), then tensor(-0.8022), every figure matching the hand calculation exactly. The difference is that nothing in this code ever mentions the chain rule, or the shortcut p - y, or any derivative at all. One call, loss.backward(), reconstructed all of it from the operations PyTorch had already recorded during the forward pass.
Building Networks the PyTorch Way: nn.Module
Writing torch.dot(w, x) + b by hand works for one neuron, but a real network has many layers of many neurons, and typing every weighted sum manually would be as painful as typing every derivative. PyTorch's torch.nn module supplies ready-made building blocks for exactly this. The most common one, nn.Linear, represents an entire layer of neurons at once: it creates the weight matrix and bias vector for you, fills them with small random starting values, and computes the weighted sum for every neuron in the layer in a single operation.
A network is defined as a small Python class that inherits from nn.Module:
import torch
import torch.nn as nn
class ChaseWinPredictor(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Linear(2, 1)
def forward(self, x):
z = self.layer(x)
return torch.sigmoid(z)
model = ChaseWinPredictor()
x = torch.tensor([[9.0, 4.0]])
prediction = model(x)
print(prediction.shape)
Inside __init__, nn.Linear(2, 1) creates a layer accepting 2 input features (required run rate and wickets in hand) and producing 1 output: exactly the single neuron traced by hand above, except its starting weight and bias are now chosen randomly rather than fixed by the programmer, the way a real network is actually initialised before training begins. The forward method describes what happens to data as it passes through the network: a linear layer followed by a sigmoid, mirroring the forward pass from the worked example precisely. Notice the input is written as [[9.0, 4.0]], with an extra pair of brackets: PyTorch layers expect a batch dimension, so even a single example must be shaped as a batch of one. This prints torch.Size([1, 1]): one example in the batch, one output value for it. The same habit becomes useful later, since the identical line of code that predicts one run chase can predict a thousand of them at once by simply stacking more rows into x.
Optimizers and the Training Loop
The update step performed by hand above, subtract the learning rate times the gradient for every parameter, is itself automated by an optimizer. torch.optim.SGD implements exactly that rule (SGD stands for stochastic gradient descent); PyTorch also ships more advanced optimizers, such as torch.optim.Adam, which adjust the effective learning rate for each parameter individually and often train faster in practice, though the underlying job, moving every weight a little against its gradient, stays the same idea either way.
Put together, tensors, autograd, nn.Module, and an optimizer form the standard PyTorch training loop, repeated once per epoch, one full pass through the training data:
model = ChaseWinPredictor()
with torch.no_grad():
model.layer.weight[0] = torch.tensor([-0.4, 0.5])
model.layer.bias[0] = 0.2
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
loss_fn = nn.BCELoss()
x = torch.tensor([[9.0, 4.0]])
y = torch.tensor([[1.0]])
for epoch in range(200):
optimizer.zero_grad()
prediction = model(x)
loss = loss_fn(prediction, y)
loss.backward()
optimizer.step()
print(round(loss.item(), 4))
print(round(model(x).item(), 4))
The block before the loop deliberately overwrites nn.Linear's random starting weight and bias with the exact -0.4, 0.5, 0.2 traced by hand earlier, so this run continues that same story instead of starting from an unrelated random point. Five lines then repeat 200 times: clear old gradients, run the forward pass, measure the loss, call backward() for fresh gradients, and let the optimizer apply them with step(). Run this and the final numbers printed are a loss of roughly 0.0052, down from the 1.6204 computed by hand at the start, and a prediction of roughly 0.9948, up from 0.1978. Two hundred single gradient-descent steps, identical in kind to the one worked by hand above, carried the network from a confident wrong answer to a confident right one, and not a single derivative was written by hand.
The one line easiest to forget is optimizer.zero_grad(), and skipping it causes a genuinely confusing bug: PyTorch does not clear old gradients on its own. By design, calling .backward() again simply adds new gradients on top of whatever is already stored in .grad. Remove zero_grad() from a loop and call .backward() on the same weight three times in a row, and the recorded gradient does not stay constant: it grows 4, then 8, then 12, compounding every pass. Training quietly breaks in a way that raises no error message, only a model that never converges properly, so this line is worth remembering deliberately rather than by habit.
Running on a GPU — and Where to Get One for Free
The reason tensors matter as their own data type, rather than PyTorch simply reusing Python lists, is speed. A modern neural network performs billions of multiplications during training, and a graphics processing unit (GPU) can carry out huge numbers of these multiplications in parallel, where an ordinary processor works through them largely one at a time. PyTorch lets you move any tensor or an entire model onto a GPU with one method call:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
x = x.to(device)
torch.cuda.is_available() checks whether a compatible GPU is present; if not, this code simply falls back to the CPU, so the same script runs unchanged on a basic laptop and on a machine with a powerful graphics card. Since most students do not have a GPU sitting at home, Google Colab is worth knowing about for exactly this reason: it offers free, ready-to-use notebooks with PyTorch already installed and a GPU available to attach, which is how a great many Indian students and early researchers first train a network larger than a toy example.
Where PyTorch Fits in the Real World
PyTorch is not the only deep learning framework. Google released TensorFlow the year before PyTorch appeared, and it remains widely used, particularly inside production systems built around Google's own infrastructure. The two differ in an important design choice: TensorFlow's earliest versions required a computation graph to be fully defined before any data could run through it, while PyTorch builds its graph as the code actually executes, one operation at a time, an approach usually called define-by-run. In practice, this means a PyTorch program can be tested and debugged exactly like ordinary Python, using ordinary print() statements and a normal debugger, one line at a time. This is a large part of why PyTorch became the dominant choice among researchers publishing new architectures: surveys of papers presented at major AI conferences over the past several years have repeatedly found PyTorch used far more often than any competing framework. On the industry side, PyTorch trains production models at organisations including Meta and OpenAI, and Tesla's self-driving engineering team has spoken publicly about using it to train the neural networks behind Autopilot.
Back to the Run Chase
Return to that flashing "Win Probability: 73%" overlay. It is now possible to describe precisely everything standing between the raw match numbers and that percentage: the required run rate and wickets in hand arrive as a torch.tensor; a stack of nn.Linear layers (many more than the single neuron traced here, but built from the identical building block) computes weighted sums through several layers instead of one; a sigmoid turns the final number into a probability; and during training, months earlier, on thousands of historical matches, loss.backward() and an optimizer's .step() repeated millions of times, adjusting every weight exactly the way this chapter's single neuron was adjusted by hand, until the network's predictions matched real outcomes closely enough to trust on live television.
None of that required an engineer to write out one derivative by hand. That is the actual promise of a framework: not that it lets anyone skip understanding what a neural network does (this chapter still needed the forward pass, the loss, and the gradient traced by hand to know PyTorch's answer was correct), but that once that understanding is in place, PyTorch lets you build networks far larger than pen and paper could ever check, confident that the arithmetic underneath is exactly right, every single time.
Think About It
Think about this: How would you explain introduction to pytorch: your first deep learning framework to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind introduction to pytorch: your first deep learning framework, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.