{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8add56da",
   "metadata": {},
   "source": [
    "# 11 · AI Frameworks: PyTorch and TensorFlow Side by Side\n",
    "\n",
    "Almost all modern AI research and production runs on two Python frameworks: **PyTorch** (Meta, now the\n",
    "PyTorch Foundation) and **TensorFlow** with its high-level API **Keras** (Google). Both do the same three\n",
    "things: tensors on CPU or GPU, automatic differentiation, and building and training neural networks.\n",
    "This module solves one problem in both, line by line, so the differences are visible.\n",
    "\n",
    "> Our free course *Neural Networks 1001* builds these ideas from first principles; here the focus is the\n",
    "> frameworks themselves.\n",
    "\n",
    "**You will learn**\n",
    "1. Tensors and automatic differentiation in each framework\n",
    "2. The same model trained in both: PyTorch's explicit loop and Keras' `fit`\n",
    "3. Comparing accuracy and speed fairly\n",
    "4. Saving, loading and exporting models\n",
    "5. How to choose, and the rest of the ecosystem (JAX, Hugging Face, ONNX)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7b94bfc",
   "metadata": {},
   "source": [
    "## 1. Tensors and gradients\n",
    "Both frameworks record operations on tensors and can differentiate through them. Check: the derivative\n",
    "of $f(x) = x^3 + 2x$ at $x = 2$ is $3 \\cdot 2^2 + 2 = 14$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "24cf83b3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:26:39.175333Z",
     "iopub.status.busy": "2026-10-17T23:26:39.174811Z",
     "iopub.status.idle": "2026-10-17T23:26:50.181299Z",
     "shell.execute_reply": "2026-10-17T23:26:50.179787Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n",
      "I0000 00:00:1792279602.488395  822786 cudart_stub.cc:31] Could not find cuda drivers on your machine, GPU will not be used.\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n",
      "I0000 00:00:1792279606.537984  822786 cudart_stub.cc:31] Could not find cuda drivers on your machine, GPU will not be used.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PyTorch 2.14.1+cpu | TensorFlow 2.21.0\n",
      "PyTorch    df/dx = 14.0\n",
      "TensorFlow df/dx = 14.0\n",
      "tensor([ 3., 12.]) [ 3. 12.]\n"
     ]
    }
   ],
   "source": [
    "import os, time\n",
    "os.environ.setdefault(\"TF_CPP_MIN_LOG_LEVEL\", \"3\")       # quieter TensorFlow start-up\n",
    "import numpy as np\n",
    "import torch\n",
    "import tensorflow as tf\n",
    "\n",
    "print(\"PyTorch\", torch.__version__, \"| TensorFlow\", tf.__version__)\n",
    "\n",
    "x_pt = torch.tensor(2.0, requires_grad=True)\n",
    "f_pt = x_pt ** 3 + 2 * x_pt\n",
    "f_pt.backward()                                            # PyTorch: gradients accumulate in .grad\n",
    "print(\"PyTorch    df/dx =\", x_pt.grad.item())\n",
    "\n",
    "x_tf = tf.Variable(2.0)\n",
    "with tf.GradientTape() as tape:                             # TensorFlow: record inside a tape\n",
    "    f_tf = x_tf ** 3 + 2 * x_tf\n",
    "print(\"TensorFlow df/dx =\", tape.gradient(f_tf, x_tf).numpy())\n",
    "\n",
    "a = np.arange(6, dtype=np.float32).reshape(2, 3)\n",
    "print(torch.from_numpy(a) @ torch.ones(3), tf.linalg.matvec(tf.constant(a), tf.ones(3)).numpy())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6fe688e4",
   "metadata": {},
   "source": [
    "## 2. The task\n",
    "Handwritten digits (8×8 pixels, 1,797 images, bundled with scikit-learn), split once so both frameworks\n",
    "see exactly the same training and test images."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0e391974",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:26:50.186213Z",
     "iopub.status.busy": "2026-10-17T23:26:50.185286Z",
     "iopub.status.idle": "2026-10-17T23:26:50.794446Z",
     "shell.execute_reply": "2026-10-17T23:26:50.792899Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(1347, 8, 8) (450, 8, 8)\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 900x140 with 8 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from sklearn.datasets import load_digits\n",
    "from sklearn.model_selection import train_test_split\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "digits = load_digits()\n",
    "X = (digits.images / 16.0).astype(np.float32)               # scale pixels to [0, 1]\n",
    "y = digits.target.astype(np.int64)\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, stratify=y, random_state=0)\n",
    "print(X_train.shape, X_test.shape)\n",
    "\n",
    "fig, axes = plt.subplots(1, 8, figsize=(9, 1.4))\n",
    "for ax, img, label in zip(axes, X_train, y_train):\n",
    "    ax.imshow(img, cmap=\"gray_r\"); ax.set_title(label); ax.axis(\"off\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "675c05b7",
   "metadata": {},
   "source": [
    "## 3. PyTorch: define a module, write the loop\n",
    "PyTorch makes the training loop explicit. More code, but every step is visible and easy to customise,\n",
    "which is one reason it dominates research."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "20faef45",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:26:50.798829Z",
     "iopub.status.busy": "2026-10-17T23:26:50.798443Z",
     "iopub.status.idle": "2026-10-17T23:26:58.442081Z",
     "shell.execute_reply": "2026-10-17T23:26:58.440897Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PyTorch: test accuracy 0.971 after 30 epochs, 4.7 s, 38,282 parameters\n"
     ]
    }
   ],
   "source": [
    "torch.manual_seed(0)\n",
    "EPOCHS, BATCH, LR = 30, 64, 1e-3\n",
    "\n",
    "class SmallCNN(torch.nn.Module):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        self.features = torch.nn.Sequential(\n",
    "            torch.nn.Conv2d(1, 16, 3, padding=1), torch.nn.ReLU(), torch.nn.MaxPool2d(2),   # 8x8 -> 4x4\n",
    "            torch.nn.Conv2d(16, 32, 3, padding=1), torch.nn.ReLU(),\n",
    "        )\n",
    "        self.head = torch.nn.Sequential(torch.nn.Flatten(), torch.nn.Linear(32 * 4 * 4, 64), torch.nn.ReLU(),\n",
    "                                        torch.nn.Dropout(0.2), torch.nn.Linear(64, 10))\n",
    "    def forward(self, x):\n",
    "        return self.head(self.features(x))\n",
    "\n",
    "pt_model = SmallCNN()\n",
    "optimizer = torch.optim.Adam(pt_model.parameters(), lr=LR)\n",
    "loss_fn = torch.nn.CrossEntropyLoss()\n",
    "Xtr, ytr = torch.from_numpy(X_train[:, None]), torch.from_numpy(y_train)      # (N, 1, 8, 8): channels first\n",
    "Xte, yte = torch.from_numpy(X_test[:, None]), torch.from_numpy(y_test)\n",
    "loader = torch.utils.data.DataLoader(torch.utils.data.TensorDataset(Xtr, ytr), batch_size=BATCH, shuffle=True)\n",
    "\n",
    "pt_history, t0 = [], time.perf_counter()\n",
    "for epoch in range(EPOCHS):\n",
    "    pt_model.train()\n",
    "    for xb, yb in loader:\n",
    "        optimizer.zero_grad()                 # 1. reset gradients\n",
    "        loss = loss_fn(pt_model(xb), yb)      # 2. forward pass\n",
    "        loss.backward()                       # 3. backpropagate\n",
    "        optimizer.step()                      # 4. update weights\n",
    "    pt_model.eval()\n",
    "    with torch.no_grad():\n",
    "        pt_history.append((pt_model(Xte).argmax(1) == yte).float().mean().item())\n",
    "pt_time = time.perf_counter() - t0\n",
    "print(f\"PyTorch: test accuracy {pt_history[-1]:.3f} after {EPOCHS} epochs, {pt_time:.1f} s, \"\n",
    "      f\"{sum(p.numel() for p in pt_model.parameters()):,} parameters\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ab0dd35c",
   "metadata": {},
   "source": [
    "## 4. TensorFlow / Keras: declare the model, call `fit`\n",
    "The same architecture. Keras hides the loop behind `compile` and `fit`, which is concise and hard to get\n",
    "wrong. (Writing a custom loop with `GradientTape` is also possible when you need it.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "fa34b2f4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:26:58.446557Z",
     "iopub.status.busy": "2026-10-17T23:26:58.445892Z",
     "iopub.status.idle": "2026-10-17T23:27:09.898568Z",
     "shell.execute_reply": "2026-10-17T23:27:09.896964Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TensorFlow: test accuracy 0.978 after 30 epochs, 11.3 s, 38,282 parameters\n"
     ]
    }
   ],
   "source": [
    "tf.keras.utils.set_random_seed(0)\n",
    "tf_model = tf.keras.Sequential([\n",
    "    tf.keras.layers.Input(shape=(8, 8, 1)),                                   # channels last\n",
    "    tf.keras.layers.Conv2D(16, 3, padding=\"same\", activation=\"relu\"), tf.keras.layers.MaxPooling2D(2),\n",
    "    tf.keras.layers.Conv2D(32, 3, padding=\"same\", activation=\"relu\"),\n",
    "    tf.keras.layers.Flatten(), tf.keras.layers.Dense(64, activation=\"relu\"),\n",
    "    tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(10),\n",
    "])\n",
    "tf_model.compile(optimizer=tf.keras.optimizers.Adam(LR),\n",
    "                 loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=[\"accuracy\"])\n",
    "t0 = time.perf_counter()\n",
    "hist = tf_model.fit(X_train[..., None], y_train, validation_data=(X_test[..., None], y_test),\n",
    "                    epochs=EPOCHS, batch_size=BATCH, verbose=0)\n",
    "tf_time = time.perf_counter() - t0\n",
    "tf_history = hist.history[\"val_accuracy\"]\n",
    "print(f\"TensorFlow: test accuracy {tf_history[-1]:.3f} after {EPOCHS} epochs, {tf_time:.1f} s, \"\n",
    "      f\"{tf_model.count_params():,} parameters\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c798d739",
   "metadata": {},
   "source": [
    "## 5. Comparing them\n",
    "Same data, same architecture, same optimiser and number of parameters. Differences in the final accuracy\n",
    "come from random initialisation and shuffling, not from the framework."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "6fef0ace",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:27:09.902259Z",
     "iopub.status.busy": "2026-10-17T23:27:09.901890Z",
     "iopub.status.idle": "2026-10-17T23:27:10.565721Z",
     "shell.execute_reply": "2026-10-17T23:27:10.564608Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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Q0JCAp988kmBnpMXnTp1IikpiUOHDgGwa9cuOnXqRKdOndi1axcA//77L5GRkXTq1El33p49e2jRogVubm567fXt21e3P6969uyZ5fai9l4prNcuu9cD4KeffuLFF19k5MiRrFu3Dmtra7399erVY+XKlcyfP5+bN29m244QpZEEskKIbHl6ejJ69Gi+/fZbLl68yC+//MLBgwd55513dMd88cUXjB8/niFDhnD16lXS09N1qZjS0tIytfnfP/y2trZP3f7kXMIHDx7g7++PqakppqammJiYYGxsjLGxMYmJiURERGT7XNzd3QkJCclz6rAaNWowcuRIfvzxR27dulVg5+RFq1atsLS0ZOfOnYSGhnLhwgVdMBYSEkJAQAC7du3CxMSE9u3b686LiIigXLlymdrL2Pbff1hyo0KFCpm2FcX3SmG9dlm9Hhn++usvbG1tGT16NMbGmf8s//bbb/Tv359PP/2UqlWrUqlSJcaNGydzZIVAAlkhRC6MHDmSihUr6kavAH7//Xc6dOjAhAkTKF++PCYmJgB6i2KelNXcwKdtf5KzszNdunQhPT2d9PR01Go1Go1GN+fyt99+y/bcrl27EhMTw9GjR595nezMnDkTc3Nzpk2bVqDn5JZKpaJ169bs3LmT3bt3Y2VlRcuWLalfvz6urq7s3LmTXbt20bhxYxwcHHTnlSlThtDQ0EztZWx7nrRaZmZmmbYVxfdKYb12Wb0eGdatW4enpydt2rTh33//zbTf1dWVX375hfDwcM6fP8+ECRP466+/9EaIhSitJJAVQmTy8ccfExkZmWl7UlISkZGRen/QgUwr/YODgzl48GC+96tHjx4cPnyYe/fu5frcCRMm4ODgwPvvv09qamqWx3zxxRekpKRk20bZsmWZPHkyq1at4sSJEzm6bl7OyUrGx83Z9a9Tp06cP3+eP/74g7Zt22Jubo6RkREdOnRgw4YNHDlyJFPg06FDB44ePZopIFu7di2A3ghkfimK7xVDv3YuLi7s27ePWrVq0b59+2ynyBgbG1OnTh3effddxo4dy6VLl7L8ORWiNJFAVgiRyYEDB6hduzY//fQToaGhJCUlcfr0afr06UNSUpLeCvGePXuyfft21q5dS0JCAidPnmTgwIF07Ngx3/s1Y8YMypYtS7du3di1axcxMTGEh4dz4MABXnnllafO6Sxf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",
      "text/plain": [
       "<Figure size 700x340 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "the two models agree on 97.8% of test images; both wrong on the same image: 7\n"
     ]
    }
   ],
   "source": [
    "plt.figure(figsize=(7, 3.4))\n",
    "plt.plot(range(1, EPOCHS + 1), pt_history, label=f\"PyTorch ({pt_time:.1f} s)\")\n",
    "plt.plot(range(1, EPOCHS + 1), tf_history, label=f\"TensorFlow ({tf_time:.1f} s)\")\n",
    "plt.xlabel(\"epoch\"); plt.ylabel(\"test accuracy\"); plt.ylim(0.8, 1.0); plt.legend(); plt.title(\"Same CNN, two frameworks\")\n",
    "plt.tight_layout(); plt.show()\n",
    "\n",
    "with torch.no_grad():\n",
    "    p_pt = pt_model(Xte).argmax(1).numpy()\n",
    "p_tf = tf_model.predict(X_test[..., None], verbose=0).argmax(1)\n",
    "print(f\"the two models agree on {np.mean(p_pt == p_tf):.1%} of test images; \"\n",
    "      f\"both wrong on the same image: {np.sum((p_pt != y_test) & (p_tf != y_test))}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bceb278b",
   "metadata": {},
   "source": [
    "## 6. Saving, loading, exporting"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "a8954d88",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:27:10.570933Z",
     "iopub.status.busy": "2026-10-17T23:27:10.570508Z",
     "iopub.status.idle": "2026-10-17T23:27:11.255725Z",
     "shell.execute_reply": "2026-10-17T23:27:11.254634Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PyTorch reload identical: True\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Keras reload identical: True\n",
      "{'cnn.pt': '153 KB', 'cnn.keras': '485 KB'}\n"
     ]
    }
   ],
   "source": [
    "import tempfile\n",
    "d = tempfile.mkdtemp()\n",
    "\n",
    "torch.save(pt_model.state_dict(), os.path.join(d, \"cnn.pt\"))      # PyTorch: save the weights\n",
    "restored = SmallCNN(); restored.load_state_dict(torch.load(os.path.join(d, \"cnn.pt\"))); restored.eval()\n",
    "with torch.no_grad():\n",
    "    print(\"PyTorch reload identical:\", torch.equal(restored(Xte).argmax(1), pt_model(Xte).argmax(1)))\n",
    "\n",
    "tf_model.save(os.path.join(d, \"cnn.keras\"))                         # Keras: architecture + weights in one file\n",
    "reloaded = tf.keras.models.load_model(os.path.join(d, \"cnn.keras\"))\n",
    "print(\"Keras reload identical:\", np.array_equal(reloaded.predict(X_test[..., None], verbose=0).argmax(1), p_tf))\n",
    "\n",
    "print({f: f\"{os.path.getsize(os.path.join(d, f)) / 1024:.0f} KB\" for f in os.listdir(d)})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b23f062",
   "metadata": {},
   "source": [
    "For deployment, models are often exported to a framework-neutral or optimised format: **ONNX** (both\n",
    "frameworks, run anywhere with ONNX Runtime), **TorchScript / torch.export** (PyTorch), **SavedModel** and\n",
    "**LiteRT** (formerly TensorFlow Lite, for phones and microcontrollers).\n",
    "\n",
    "## 7. Choosing\n",
    "| | PyTorch | TensorFlow / Keras |\n",
    "|---|---|---|\n",
    "| style | explicit, Pythonic, eager by default | high-level `fit`, graph compilation with `tf.function` |\n",
    "| strongest in | research, new architectures, most new papers and LLM work | production pipelines (TFX), mobile and embedded (LiteRT), TPUs |\n",
    "| ecosystem | Hugging Face Transformers, PyTorch Lightning, torchvision | Keras, TensorFlow Serving, TF.js in the browser |\n",
    "| note | | Keras 3 can also run on PyTorch or JAX as its backend |\n",
    "\n",
    "The wider landscape: **JAX** (Google; NumPy API with composable transforms `grad`, `jit`, `vmap`, popular\n",
    "for research at scale), **Hugging Face** (pretrained models and datasets for all frameworks),\n",
    "**scikit-learn** (classical machine learning, often the right first model), **ONNX Runtime** (fast\n",
    "inference anywhere).\n",
    "\n",
    "## Exercises\n",
    "1. Write the Keras model's training with a custom loop using `tf.GradientTape`, mirroring the PyTorch loop.\n",
    "2. Remove the convolutional layers and use only dense layers. How much accuracy is lost?\n",
    "3. Export the PyTorch model to ONNX (`torch.onnx.export`) and run it with `onnxruntime`."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
