A Framework Tour: PyTorch vs. TensorFlow

Neural Networks & Deep Learning

Chapter 10 · A Framework Tour: PyTorch vs. TensorFlow

nn1-1nn1-9 built the concepts. This chapter maps them onto real, working code — and covers the two dominant frameworks the rest of this course's own ecosystem (llm1 included) is built on.

Two Frameworks, Two Different Starting Philosophies

TensorFlow (Google) originally used define-then-run: build the entire computation graph first, as a fixed structure, then feed data through it. Efficient for deployment and optimization, but genuinely awkward for debugging — you couldn't simply inspect an intermediate value mid-computation the way you'd step through ordinary Python. PyTorch (Meta) used eager execution — define-by-run — from the start: the computation graph is built dynamically as code actually executes, line by line, exactly like ordinary Python. This genuinely mattered for research and experimentation, and is a real, documented reason PyTorch became — and largely remains — the dominant framework in academic and research settings specifically.

An Honest Update — This Gap Has Narrowed

The historical framing explains reputations, not current capabilities
TensorFlow 2.x adopted eager execution by default, following the field's own clear preference. PyTorch, in turn, added its own graph-based optimization and deployment tools (TorchScript, torch.compile). The define-then-run/define-by-run distinction explains why each framework built the ecosystem and reputation it has today — it no longer accurately describes either framework's own current capabilities in isolation.

The Current Practical Landscape

Strongest today in
PyTorchResearch and academic papers, and increasingly production as well
TensorFlowMobile/edge deployment (TensorFlow Lite), browser deployment (TensorFlow.js), enterprises already invested in the ecosystem

A Real Small Network — Every Concept, Real Code

import torch
import torch.nn as nn
import torch.optim as optim

class SmallNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(2, 4)   # nn1-1's own hidden layer
        self.output = nn.Linear(4, 1)   # nn1-1's own output layer
        self.relu = nn.ReLU()           # nn1-4's own activation function

    def forward(self, x):
        x = self.relu(self.hidden(x))   # nn1-1's own forward propagation
        return torch.sigmoid(self.output(x))   # ml1-5's own sigmoid, one more time

model = SmallNetwork()
loss_fn = nn.BCELoss()                          # cross-entropy for binary output (nn1-5)
optimizer = optim.SGD(model.parameters(), lr=0.1)   # nn1-6's own gradient descent, with a learning rate

for epoch in range(1000):                       # nn1-6's own epochs
    predictions = model(X_train)                # forward pass
    loss = loss_fn(predictions, y_train)
    optimizer.zero_grad()
    loss.backward()                              # nn1-5's own backpropagation, one line
    optimizer.step()                             # the weight update itself
Every line traces back to a chapter already covered
nn.Linear is nn1-1's own neuron layer, generalized. nn.ReLU is nn1-4's own activation choice. loss.backward() is nn1-5's own backpropagation — one line, doing exactly the chain-rule-driven gradient computation that chapter explained conceptually. optimizer.step() is nn1-6's own gradient descent update rule, applied automatically. Nothing here is new material — it's this course's own first nine chapters, in real syntax.

Hands-On Exercises

Exercise 1

Explain the real, historical reason PyTorch's own define-by-run approach mattered specifically for research and experimentation, using this chapter's own reasoning about debugging.

📄 View solution
Exercise 2

Explain why this chapter says the define-then-run/define-by-run distinction "explains reputations, not current capabilities," using the two specific updates (TensorFlow 2.x, TorchScript/torch.compile) this chapter names.

📄 View solution
Exercise 3

Using this chapter's own code example, identify which specific earlier chapter each of loss.backward() and optimizer.step() corresponds to, and explain what each line is actually doing under the hood.

📄 View solution

Chapter 10 Quick Reference

  • TensorFlow — originally define-then-run (a static graph); PyTorch — define-by-run (eager) from the start, favoring debugging/research
  • Honest update: TensorFlow 2.x defaults to eager execution; PyTorch added graph-based tools (TorchScript, torch.compile) — the gap has narrowed
  • PyTorch dominates research/academic work today; TensorFlow remains strong for mobile/edge and browser deployment
  • Real PyTorch code: nn.Linear/nn.ReLU (nn1-1/nn1-4), loss.backward() (nn1-5's backprop), optimizer.step() (nn1-6's gradient descent)
  • Next chapter: Capstone: Building and Training a Real Neural Network