PyTorch, TensorFlow & Keras
scikit-learn is the right tool for classic machine learning, but it has no convolutional, recurrent or transformer layers and cannot use a GPU. For deep learning, people use dedicated frameworks. The three names you will meet everywhere are PyTorch, TensorFlow and Keras.
In this lesson you will see what these frameworks do for you, write a tiny network by hand in NumPy to appreciate it, and read the same network in PyTorch and in Keras. The framework code is not runnable in the browser (the libraries are far too large), so it is shown as plain examples to run on your own computer or on a free cloud notebook.
What a deep learning framework gives you
All three frameworks provide the same core pieces:
- Tensors: multi-dimensional arrays like NumPy arrays, which can live on a GPU.
- Automatic differentiation (autograd): you write the forward pass, and the framework computes every gradient for backpropagation.
- Layers and models: dense, convolution, recurrent, attention, normalisation and many more, ready to combine.
- Optimisers and losses: SGD, Adam, cross-entropy and friends.
- Data loading, saving and deployment tools, plus access to thousands of pretrained models.
| Framework | Made by | Style | Latest release (30 Oct 2026) |
|---|---|---|---|
| PyTorch | Started at Meta; now run by the PyTorch Foundation | Pythonic, you write the training loop | 2.15.0 (28 October 2026) |
| TensorFlow | Large ecosystem for production and mobile | 2.21.0 (March 2026) | |
| Keras | Google and community | High-level API on top of JAX, TensorFlow or PyTorch | 3.15.1 (July 2026) |
| JAX | NumPy-like functions with fast compilation; popular in research | - |
PyTorch is the most common choice in research and in many companies today, and most open pretrained models are published for it. TensorFlow remains widely used in production systems and on mobile devices. Keras offers the simplest code and, since Keras 3, can run on any of three backends.
What autograd saves you from
To appreciate autograd, here is a two-layer network trained on the two-moons data with every gradient written by hand. The backward pass applies the chain rule layer by layer, exactly as described in Neural Networks with MLPClassifier.
import numpy as np
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=400, noise=0.2, random_state=42)
y = y.reshape(-1, 1)
rng = np.random.default_rng(42)
W1, b1 = rng.normal(0, 1, (2, 16)), np.zeros(16) # input -> 16 hidden neurons
W2, b2 = rng.normal(0, 1, (16, 1)), np.zeros(1) # hidden -> 1 output
learning_rate = 0.5
def sigmoid(z):
return 1 / (1 + np.exp(-z))
for epoch in range(1, 2001):
h = np.tanh(X @ W1 + b1) # forward pass
p = sigmoid(h @ W2 + b2)
loss = -np.mean(y * np.log(p + 1e-9) + (1 - y) * np.log(1 - p + 1e-9))
dz2 = (p - y) / len(X) # backward pass, by hand
dW2, db2 = h.T @ dz2, dz2.sum(axis=0)
dh = dz2 @ W2.T * (1 - h ** 2) # derivative of tanh
dW1, db1 = X.T @ dh, dh.sum(axis=0)
W1 -= learning_rate * dW1 # gradient descent step
b1 -= learning_rate * db1
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
if epoch in (1, 10, 100, 500, 2000):
print(f"epoch {epoch:>4}: loss {loss:.3f}, accuracy {np.mean((p > 0.5) == y):.3f}")epoch 1: loss 0.702, accuracy 0.642 epoch 10: loss 0.358, accuracy 0.833 epoch 100: loss 0.256, accuracy 0.880 epoch 500: loss 0.105, accuracy 0.965 epoch 2000: loss 0.071, accuracy 0.968
It works, but the backward pass took as many lines as the forward pass, and every new layer type would need its own hand-derived gradients. A network with 50 layers of different kinds would be impossible to maintain this way. Frameworks remove the whole "backward pass" section: you call one method and every gradient is computed for you.
The same network in PyTorch
In PyTorch you define the model, a loss and an optimiser, then write the training loop yourself. loss.backward() is autograd: it replaces all the hand-written gradient lines above. Moving work to a GPU is one .to(device) call.
import torch
from torch import nn
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=400, noise=0.2, random_state=42)
X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = nn.Sequential(nn.Linear(2, 16), nn.Tanh(), nn.Linear(16, 1)).to(device)
loss_fn = nn.BCEWithLogitsLoss() # sigmoid and log loss in one stable step
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
X, y = X.to(device), y.to(device)
for epoch in range(500):
optimizer.zero_grad() # clear the previous gradients
loss = loss_fn(model(X), y) # forward pass
loss.backward() # autograd: compute every gradient
optimizer.step() # update every weight
with torch.no_grad(): # no gradients needed for evaluation
accuracy = ((model(X) > 0).float() == y).float().mean().item()
print(f"loss {loss.item():.3f}, accuracy {accuracy:.3f}")For speed, PyTorch 2 added torch.compile(model), which compiles the model into faster code, often with no other changes. Real projects also use a DataLoader to feed the data in small batches.
The same network in Keras
Keras hides the loop: you describe the layers, compile the model with an optimiser and loss, and call fit. Keras 3 runs on JAX, TensorFlow or PyTorch; you choose with the KERAS_BACKEND environment variable before importing Keras.
import os
os.environ["KERAS_BACKEND"] = "torch" # or "jax" or "tensorflow"
import keras
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=400, noise=0.2, random_state=42)
model = keras.Sequential([
keras.Input(shape=(2,)),
keras.layers.Dense(16, activation="tanh"),
keras.layers.Dense(1, activation="sigmoid"),
])
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.01),
loss="binary_crossentropy", metrics=["accuracy"])
model.fit(X, y, epochs=100, batch_size=32, validation_split=0.2, verbose=0)
loss, accuracy = model.evaluate(X, y, verbose=0)
print(f"loss {loss:.3f}, accuracy {accuracy:.3f}")Notice how much the three styles share: layers of 2 → 16 → 1 neurons, a tanh activation, Adam, and binary cross-entropy (log loss). Once you understand the ideas, switching frameworks is mostly a matter of syntax. For a problem this small, scikit-learn does the same job in two lines:
from sklearn.datasets import make_moons
from sklearn.neural_network import MLPClassifier
X, y = make_moons(n_samples=400, noise=0.2, random_state=42)
mlp = MLPClassifier(hidden_layer_sizes=(16,), activation="tanh", solver="lbfgs",
max_iter=2000, random_state=42).fit(X, y)
print(f"Training accuracy: {mlp.score(X, y):.3f}")Training accuracy: 0.990
Installing and GPUs
Install inside a virtual environment. The exact PyTorch command depends on your system and GPU, and the "Get Started" page on pytorch.org builds it for you; TensorFlow and Keras are installed from PyPI.
python -m pip install torch # CPU build; see pytorch.org for GPU builds
python -m pip install keras # Keras 3, plus a backend such as torch or jax
python -m pip install "tensorflow[and-cuda]" # TensorFlow with NVIDIA GPU support on Linux or WSL2A few practical facts:
- Deep learning on a GPU is usually many times faster than on a CPU. NVIDIA GPUs use CUDA; PyTorch also supports AMD GPUs (ROCm) and Apple silicon (
"mps"). - TensorFlow 2.10 was the last version with GPU support on native Windows; on Windows, use TensorFlow inside WSL2 for GPU training.
- No GPU at home? Free cloud notebooks such as Google Colab and Kaggle Notebooks offer limited free GPU time, which is plenty for learning.
Which one should you learn?
| If you want to... | Start with |
|---|---|
| Learn deep learning ideas with the least code | Keras |
| Follow research papers, use most open pretrained models, write custom training | PyTorch |
| Maintain or deploy existing TensorFlow systems, or target mobile with LiteRT (formerly TensorFlow Lite) | TensorFlow |
| Classify tables of data | scikit-learn or gradient boosting, not deep learning |
The ideas carry over between all of them, so pick one, build a few projects, and learn the others when a job requires it.
Common mistakes
- Reaching for deep learning first. For tabular data, start with scikit-learn and gradient boosting.
- Forgetting
optimizer.zero_grad()in PyTorch. Gradients add up across steps unless you clear them, which silently breaks training. - Setting
KERAS_BACKENDafter importing Keras. It must be set before the firstimport keras. - Installing a GPU build that does not match your driver. Use the official install selector and check with
torch.cuda.is_available().
Key points
- Deep learning frameworks provide GPU tensors, automatic differentiation, ready-made layers and optimisers.
- Autograd computes all gradients from the forward pass, replacing hand-written backpropagation.
- PyTorch (2.15) gives you an explicit training loop; Keras (3.15) gives
compileandfitand runs on JAX, TensorFlow or PyTorch; TensorFlow (2.21) has a large production ecosystem. - Since TensorFlow 2.16,
tf.kerasis Keras 3; GPU TensorFlow on Windows needs WSL2. - Use free cloud GPUs to learn, and keep scikit-learn for classic machine learning.
Exercise
In the hand-written NumPy network, change the learning rate to 0.05 and then to 100, keeping 2,000 epochs. Print the loss and accuracy at epochs 10, 100, 1,000 and 2,000. What happens with a learning rate that is too small, and with one that is too large?
Show solution
import numpy as np
from sklearn.datasets import make_moons
def train(learning_rate):
X, y = make_moons(n_samples=400, noise=0.2, random_state=42)
y = y.reshape(-1, 1)
rng = np.random.default_rng(42)
W1, b1 = rng.normal(0, 1, (2, 16)), np.zeros(16)
W2, b2 = rng.normal(0, 1, (16, 1)), np.zeros(1)
for epoch in range(1, 2001):
h = np.tanh(X @ W1 + b1)
p = 1 / (1 + np.exp(-(h @ W2 + b2)))
dz2 = (p - y) / len(X)
dh = dz2 @ W2.T * (1 - h ** 2)
W2 -= learning_rate * (h.T @ dz2)
b2 -= learning_rate * dz2.sum(axis=0)
W1 -= learning_rate * (X.T @ dh)
b1 -= learning_rate * dh.sum(axis=0)
if epoch in (10, 100, 1000, 2000):
loss = -np.mean(y * np.log(p + 1e-9) + (1 - y) * np.log(1 - p + 1e-9))
print(f" lr {learning_rate}, epoch {epoch:>4}: loss {loss:.3f}, accuracy {np.mean((p > 0.5) == y):.3f}")
for lr in [0.05, 100.0]:
train(lr)lr 0.05, epoch 10: loss 0.561, accuracy 0.698 lr 0.05, epoch 100: loss 0.355, accuracy 0.838 lr 0.05, epoch 1000: loss 0.255, accuracy 0.880 lr 0.05, epoch 2000: loss 0.189, accuracy 0.927 lr 100.0, epoch 10: loss 8.809, accuracy 0.570 lr 100.0, epoch 100: loss 7.564, accuracy 0.635 lr 100.0, epoch 1000: loss 2.066, accuracy 0.900 lr 100.0, epoch 2000: loss 1.905, accuracy 0.875
With 0.05 the loss falls slowly but steadily; after 2,000 epochs it is still well behind the run with 0.5. With 100 the steps are so large that the weights overshoot: the loss jumps up and down, often far above its starting value, and the accuracy never settles. A good learning rate lies between these extremes, and adaptive optimisers such as Adam make the choice much less delicate.
Quick Quiz
1. What does automatic differentiation (autograd) do?
Autograd records the forward computation and applies the chain rule for you during backward().
2. In PyTorch, which call computes the gradients?
backward() fills in the gradients; optimizer.step() then uses them to update the weights.
3. Which backends can Keras 3 run on?
Keras 3 is multi-backend; set KERAS_BACKEND before importing it.
4. What does tf.keras refer to in TensorFlow 2.16 and later?
Since TensorFlow 2.16, installing TensorFlow gives Keras 3; legacy Keras 2 needs the tf_keras package.
Learn Machine Learning with a mentor
Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
