{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "dcc90b4c",
   "metadata": {},
   "source": [
    "# PyTorch for Biology: A Gentle, Code-First Intro\n",
    "\n",
    "This notebook is for students with a biological background who want a fast, practical start with PyTorch. We'll use small, synthetic examples resembling gene expression and promoter detection so everything runs quickly on CPU."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "46a473a9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PyTorch version: 2.10.0+cpu\n",
      "CUDA available: False\n",
      "Using device: cpu\n"
     ]
    }
   ],
   "source": [
    "# Section 1: Setup and installation checks\n",
    "import torch, numpy as np, random, pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# Reproducibility\n",
    "SEED = 42\n",
    "random.seed(SEED)\n",
    "np.random.seed(SEED)\n",
    "torch.manual_seed(SEED)\n",
    "\n",
    "print(f\"PyTorch version: {torch.__version__}\")\n",
    "print(f\"CUDA available: {torch.cuda.is_available()}\")\n",
    "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "print(f\"Using device: {device}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "60f13c91",
   "metadata": {},
   "source": [
    "## 2. Tensors: creation, shapes, and dtypes\n",
    "We'll start with DNA-encoded toy data (A,T,G,C mapped to 0–3) to show tensor creation, shapes, and dtypes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "e4df1c99",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor_from_np: torch.Size([4, 8]) torch.int64\n",
      "float_tensor : torch.Size([4, 8]) torch.float32\n",
      "zeros shape   : torch.Size([2, 3]) ones shape: torch.Size([2, 3]) rand shape: torch.Size([2, 3])\n"
     ]
    }
   ],
   "source": [
    "# Small DNA example: 4 samples, 8 bases each encoded as ints 0-3\n",
    "bases = np.array([\n",
    "    [0,1,2,3,0,1,2,3],\n",
    "    [3,3,2,2,1,1,0,0],\n",
    "    [1,0,1,0,2,2,3,3],\n",
    "    [2,2,1,1,0,0,3,3],\n",
    "], dtype=np.int64)\n",
    "\n",
    "tensor_from_np = torch.tensor(bases)                 # infers dtype/int64\n",
    "float_tensor = torch.tensor(bases, dtype=torch.float32)\n",
    "zeros = torch.zeros((2,3), dtype=torch.float32)\n",
    "ones  = torch.ones((2,3), dtype=torch.float32)\n",
    "rand  = torch.rand((2,3), dtype=torch.float32)\n",
    "\n",
    "print(\"tensor_from_np:\", tensor_from_np.shape, tensor_from_np.dtype)\n",
    "print(\"float_tensor :\", float_tensor.shape, float_tensor.dtype)\n",
    "print(\"zeros shape   :\", zeros.shape, \"ones shape:\", ones.shape, \"rand shape:\", rand.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b0e497c",
   "metadata": {},
   "source": [
    "## 3. Basic tensor operations (indexing, slicing, reshaping)\n",
    "Think of rows as samples and columns as genes/features. We'll slice, reshape, and concatenate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "ef3f3960",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "First two samples:\n",
      " tensor([[0., 1., 2., 3., 0., 1., 2., 3.],\n",
      "        [3., 3., 2., 2., 1., 1., 0., 0.]])\n",
      "First four features of all samples:\n",
      " tensor([[0., 1., 2., 3.],\n",
      "        [3., 3., 2., 2.],\n",
      "        [1., 0., 1., 0.],\n",
      "        [2., 2., 1., 1.]])\n",
      "Reshaped (2,2,8): torch.Size([2, 2, 8])\n",
      "Concatenated shape: torch.Size([8, 8])\n",
      "Feature means: tensor([1.5000, 1.5000, 1.5000, 1.5000, 0.7500, 1.0000, 2.0000, 2.2500])\n",
      "Feature stds : tensor([1.2910, 1.2910, 0.5774, 1.2910, 0.9574, 0.8165, 1.4142, 1.5000])\n"
     ]
    }
   ],
   "source": [
    "data = float_tensor  # from earlier cell\n",
    "\n",
    "first_two_samples = data[:2, :]          # slicing\n",
    "first_four_features = data[:, :4]\n",
    "reshaped = data.view(2, 2, 8)             # reshape into (batch, chunk, features)\n",
    "concat = torch.cat([data, data], dim=0)   # stack samples\n",
    "\n",
    "print(\"First two samples:\\n\", first_two_samples)\n",
    "print(\"First four features of all samples:\\n\", first_four_features)\n",
    "print(\"Reshaped (2,2,8):\", reshaped.shape)\n",
    "print(\"Concatenated shape:\", concat.shape)\n",
    "\n",
    "# Elementwise operations\n",
    "mean_per_feature = data.mean(dim=0)\n",
    "std_per_feature  = data.std(dim=0)\n",
    "print(\"Feature means:\", mean_per_feature)\n",
    "print(\"Feature stds :\", std_per_feature)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cbf15ff8",
   "metadata": {},
   "source": [
    "## 4. Autograd: gradients on simple functions\n",
    "PyTorch tracks operations when `requires_grad=True`. Let's compute a simple mean-square and get gradients."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "fb9dfde8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "y: 2.0\n",
      "dy/dx: tensor([-0.8000, -0.4000,  0.0000,  0.4000,  0.8000])\n"
     ]
    }
   ],
   "source": [
    "x = torch.linspace(-2, 2, steps=5, requires_grad=True)\n",
    "y = (x**2).mean()  # simple function\n",
    "print(\"y:\", y.item())\n",
    "\n",
    "y.backward()       # compute dy/dx\n",
    "print(\"dy/dx:\", x.grad)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b03eadac",
   "metadata": {},
   "source": [
    "## 5. Data handling: loading tabular biological data\n",
    "We'll simulate a small gene expression table (samples × genes) and load it with pandas, then feed it to a DataLoader."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "3b7428ac",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "      Gene1     Gene2     Gene3     Gene4     Gene5     Gene6     Gene7  \\\n",
      "0  0.087047 -0.299007  0.091761 -1.987569 -0.219672  0.357113  1.477894   \n",
      "1  1.296120  1.261055  1.005113  0.765413 -0.415371 -0.420645 -0.342715   \n",
      "2 -0.034712 -1.168678  1.142823  0.751933  0.791032 -0.909387  1.402794   \n",
      "3 -0.783253 -0.322062  0.813517 -1.230864  0.227460  1.307143 -1.607483   \n",
      "4  1.865775  0.473833 -1.191303  0.656554 -0.974682  0.787085  1.158596   \n",
      "\n",
      "      Gene8     Gene9    Gene10  ...    Gene12    Gene13    Gene14    Gene15  \\\n",
      "0 -0.518270 -0.808494 -0.501757  ...  0.328751 -0.529760  0.513267  0.097078   \n",
      "1 -0.802277 -0.161286  0.404051  ...  0.174578  0.257550 -0.074446 -1.918771   \n",
      "2 -1.401851  0.586857  2.190456  ... -0.566298  0.099651 -0.503476 -1.550663   \n",
      "3  0.184634  0.259883  0.781823  ... -1.320457  0.521942  0.296985  0.250493   \n",
      "4 -0.820682  0.963376  0.412781  ...  1.896793 -0.245388 -0.753736 -0.889514   \n",
      "\n",
      "     Gene16    Gene17    Gene18    Gene19    Gene20  label  \n",
      "0  0.968645 -0.702053 -0.327662 -0.392108 -1.463515      0  \n",
      "1 -0.026514  0.060230  2.463242 -0.192361  0.301547      1  \n",
      "2  0.068563 -1.062304  0.473592 -0.919424  1.549934      0  \n",
      "3  0.346448 -0.680025  0.232254  0.293072 -0.714351      0  \n",
      "4 -0.815810 -0.077102  0.341152  0.276691  0.827183      0  \n",
      "\n",
      "[5 rows x 21 columns]\n"
     ]
    }
   ],
   "source": [
    "# Simulate a small gene expression dataset\n",
    "n_samples = 200\n",
    "n_genes = 20\n",
    "# Healthy vs disease: disease has slightly higher expression on first 5 genes\n",
    "labels = np.random.randint(0, 2, size=n_samples)\n",
    "expression = np.random.normal(loc=0.0, scale=1.0, size=(n_samples, n_genes))\n",
    "expression[labels == 1, :5] += 1.0  # shift disease class\n",
    "\n",
    "# Build pandas DataFrame (for illustration of CSV-like loading)\n",
    "gene_names = [f\"Gene{i+1}\" for i in range(n_genes)]\n",
    "df = pd.DataFrame(expression, columns=gene_names)\n",
    "df[\"label\"] = labels\n",
    "print(df.head())\n",
    "\n",
    "# Convert to tensors\n",
    "X = torch.tensor(df[gene_names].values, dtype=torch.float32)\n",
    "y = torch.tensor(df[\"label\"].values, dtype=torch.long)\n",
    "\n",
    "from torch.utils.data import TensorDataset, DataLoader\n",
    "\n",
    "dataset = TensorDataset(X, y)\n",
    "train_loader = DataLoader(dataset, batch_size=32, shuffle=True)\n",
    "val_loader   = DataLoader(dataset, batch_size=64, shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7142c549",
   "metadata": {},
   "source": [
    "## 6. Building a simple feedforward network\n",
    "We use a tiny multilayer perceptron: input → ReLU → hidden → ReLU → output (2 classes)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "c684e450",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ExpressionNet(\n",
      "  (net): Sequential(\n",
      "    (0): Linear(in_features=20, out_features=32, bias=True)\n",
      "    (1): ReLU()\n",
      "    (2): Linear(in_features=32, out_features=32, bias=True)\n",
      "    (3): ReLU()\n",
      "    (4): Linear(in_features=32, out_features=2, bias=True)\n",
      "  )\n",
      ")\n",
      "Trainable parameters: 1794\n"
     ]
    }
   ],
   "source": [
    "import torch.nn as nn\n",
    "\n",
    "class ExpressionNet(nn.Module):\n",
    "    def __init__(self, n_features, hidden=32):\n",
    "        super().__init__()\n",
    "        self.net = nn.Sequential(\n",
    "            nn.Linear(n_features, hidden),\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(hidden, hidden),\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(hidden, 2)\n",
    "        )\n",
    "    def forward(self, x):\n",
    "        return self.net(x)\n",
    "\n",
    "model = ExpressionNet(n_genes).to(device)\n",
    "print(model)\n",
    "print(\"Trainable parameters:\", sum(p.numel() for p in model.parameters()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad872901",
   "metadata": {},
   "source": [
    "## 7. Training loop: loss, optimizer, metrics\n",
    "We'll train with cross-entropy loss and Adam, tracking loss and accuracy per epoch."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "6891d57a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 05 | loss 0.1153 | acc 0.960\n",
      "Epoch 10 | loss 0.0108 | acc 1.000\n",
      "Epoch 15 | loss 0.0021 | acc 1.000\n",
      "Epoch 20 | loss 0.0009 | acc 1.000\n",
      "Epoch 25 | loss 0.0006 | acc 1.000\n"
     ]
    }
   ],
   "source": [
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)\n",
    "\n",
    "def accuracy(logits, y_true):\n",
    "    preds = logits.argmax(dim=1)\n",
    "    return (preds == y_true).float().mean().item()\n",
    "\n",
    "n_epochs = 25\n",
    "history = {\"train_loss\": [], \"train_acc\": []}\n",
    "\n",
    "for epoch in range(1, n_epochs+1):\n",
    "    model.train()\n",
    "    total_loss, total_acc, n_batches = 0.0, 0.0, 0\n",
    "    for xb, yb in train_loader:\n",
    "        xb, yb = xb.to(device), yb.to(device)\n",
    "        optimizer.zero_grad()\n",
    "        logits = model(xb)\n",
    "        loss = criterion(logits, yb)\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        total_loss += loss.item()\n",
    "        total_acc += accuracy(logits, yb)\n",
    "        n_batches += 1\n",
    "    history[\"train_loss\"].append(total_loss / n_batches)\n",
    "    history[\"train_acc\"].append(total_acc / n_batches)\n",
    "    if epoch % 5 == 0:\n",
    "        print(f\"Epoch {epoch:02d} | loss {history['train_loss'][-1]:.4f} | acc {history['train_acc'][-1]:.3f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bdcd1d60",
   "metadata": {},
   "source": [
    "## 8. Toy biological task: gene-expression classification\n",
    "We simulate healthy vs disease expression. Let's visualize training curves."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "f8b904c7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAYzdJREFUeJzt3Qd4VFXawPE3PSEhIKQQIIROAggqWAARRQGxITbUVSzgitgAy4Ksq7B+oqAsugIWRJYVEQtWUESlirqCoAihKB0SUoA0SJ/veU+YmEkChGQydybz/z3PZWbu3Llz5uQOd957znmPj81mswkAAAAAAHA6X+fvEgAAAAAAEHQDAAAAAFCLaOkGAAAAAKCWEHQDAAAAAFBLCLoBAAAAAKglBN0AAAAAANQSgm4AAAAAAGoJQTcAAAAAALWEoBsAAAAAgFpC0A24kI+PT5WW5cuX1+h9nn76abOf6tD3dkYZPO29AQCeiXMrAHfnb3UBAG/y/fffOzz+5z//KcuWLZNvv/3WYX3Hjh1r9D7Dhw+Xyy+/vFqvPeecc0w5a1oGAABcgXMrAHdH0A240AUXXODwODIyUnx9fSusL+/o0aNSr169Kr9P8+bNzVId4eHhpywPAADugnNr3XLs2DEJDg6udo89wB3RvRxwMxdffLF07txZVq5cKT179jTB9t13322eW7BggfTv319iYmIkJCREEhISZOzYsZKTk3PK7uUtW7aUq666Sr788kvTmq2vj4+Pl9mzZ5+yi/edd94pYWFh8vvvv8sVV1xh7sfGxsojjzwieXl5Dq/ft2+f3HDDDVK/fn1p2LCh/OUvf5GffvrJ7HPOnDnVqpNPP/1UevToYepC99uvX78KLRupqany17/+1ZQrKCjIXNDo1auXfP3116XbrF+/3tRBVFSU2aZp06Zy5ZVXmjIDAOoubz636vlx5MiRpgebvoeeA/v27SurVq2qsK2+78SJE00daODbuHFjueSSS2TNmjWl2xQXF8u///1vOeuss8zn1fLohQ89V9tpubS+ytP60s9tp2XXbb/66ivz99Bzt/5ttBxaL3fddZe0a9fOrGvWrJlcffXVsnHjxgr7PXLkiKm31q1bm/O7fkat0y1btojNZjP7GDBgQIXXZWdnS4MGDeT+++8/aR0CNUVLN+CGkpKS5LbbbpPHH39cnn32WdMarrZv325OIqNGjZLQ0FBzMnn++eflf//7X4Uu6pX55ZdfzElJf0xER0fLrFmzZNiwYdK2bVu56KKLTvragoICueaaa8z2ug/94aLd4/Vk9Y9//MNsoz9Q9OR86NAhUy7dr/4QGTJkSLXr4p133jE/LvQH0fz5882JePLkyeYH1DfffCMXXnih2e7222+Xn3/+Wf7v//5P2rdvb07A+jg9Pb20bBqst2rVSqZPn24+f3Jysunen5WVVe3yAQA8g7eeW/V16qmnnpImTZqYQPOjjz4qPY/qrSosLJSBAweaYFzrQgNzXffDDz/Inj17zMUKpUHz22+/bcqsAXpgYKA53+7atUuqSwNuvQj+3//+13zegIAAOXDggAn6n3vuOROM6+f4z3/+I+eff765iN6hQwfzWj2H628Bff+//e1v5nn9jFqX+jfXiyAPPvig+Uz6t9YA3G7u3LmSmZlJ0I3aZwNgmTvuuMMWGhrqsK5Pnz42/Wp+8803J31tcXGxraCgwLZixQqz/S+//FL63FNPPWXWlRUXF2cLDg627d69u3TdsWPHbI0aNbLde++9peuWLVtmXqu3Zcup69577z2HfV5xxRW2Dh06lD6ePn262e6LL75w2E73r+vfeuutk36m8u9dVFRka9q0qe3MM8809+2ysrJsUVFRtp49e5auCwsLs40aNeqE+167dq3Z98cff3zSMgAAPBvn1pMrLCw0vx8uvfRS2+DBg0vXz50715wn33jjjRO+duXKlWab8ePHn/Q9dBv9LVKe/hbRv4+d/i7QbYcOHVqlcufn59vatWtnGz16dOn6iRMnmn0sXbr0hK/NzMy01a9f3/bwww87rO/YsaPtkksuOeV7AzVF93LADZ1xxhnmCnN5O3bskFtvvdVcqfbz8zNXgvv06WOeS0xMPOV+tStYixYtSh9r1zFtFd69e/cpX6vdv7RbV1ldunRxeO2KFStM17fySdxuueUWqY6tW7eaK93aim1vkVDaPe766683V991vLs677zzTDe1Z555xqzX1oOytGVA61Wvgr/66quyefPmapUJAOCZvPncquc97f6uZfP39zefUVu5y36+L774wjxv73ZfGd1GObs7tp7Ty9NWdu2RoN3itTVdy6232lpdvtxa35dddtkJ96/1p13V9XeCfdiA9mLQ3wIPPPCAUz8LUBmCbsAN6biy8rSr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",
      "text/plain": [
       "<Figure size 1000x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Plot loss and accuracy\n",
    "plt.figure(figsize=(10,4))\n",
    "plt.subplot(1,2,1)\n",
    "plt.plot(history[\"train_loss\"], label=\"train loss\")\n",
    "plt.xlabel(\"epoch\")\n",
    "plt.ylabel(\"loss\")\n",
    "plt.title(\"Training loss\")\n",
    "plt.grid(True, alpha=0.3)\n",
    "\n",
    "plt.subplot(1,2,2)\n",
    "plt.plot(history[\"train_acc\"], label=\"train acc\", color=\"green\")\n",
    "plt.xlabel(\"epoch\")\n",
    "plt.ylabel(\"accuracy\")\n",
    "plt.title(\"Training accuracy\")\n",
    "plt.ylim(0,1)\n",
    "plt.grid(True, alpha=0.3)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c89c1da5",
   "metadata": {},
   "source": [
    "### Quick inference on a few samples\n",
    "Let's inspect predictions on 5 random samples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "552b8057",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sample  28 | true label: 1 | pred: 1 | p(disease)=0.997\n",
      "Sample  93 | true label: 1 | pred: 1 | p(disease)=1.000\n",
      "Sample   2 | true label: 0 | pred: 0 | p(disease)=0.001\n",
      "Sample 171 | true label: 0 | pred: 0 | p(disease)=0.000\n",
      "Sample  69 | true label: 1 | pred: 1 | p(disease)=1.000\n"
     ]
    }
   ],
   "source": [
    "model.eval()\n",
    "with torch.no_grad():\n",
    "    sample_idx = np.random.choice(n_samples, size=5, replace=False)\n",
    "    sample_x = X[sample_idx].to(device)\n",
    "    sample_y = y[sample_idx]\n",
    "    logits = model(sample_x)\n",
    "    probs = torch.softmax(logits, dim=1)[:,1].cpu().numpy()\n",
    "    preds = logits.argmax(dim=1).cpu().numpy()\n",
    "\n",
    "for i, idx in enumerate(sample_idx):\n",
    "    print(f\"Sample {idx:3d} | true label: {sample_y[i].item()} | pred: {preds[i]} | p(disease)={probs[i]:.3f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9038e5e2",
   "metadata": {},
   "source": [
    "## 9. Saving and loading models\n",
    "Persist model weights with `state_dict`; reload to continue training or run inference later."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "94a25164",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved to expression_net.pt\n",
      "Loaded model logits shape: torch.Size([2, 2])\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/tmp/ipykernel_822106/237955992.py:7: FutureWarning: You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.\n",
      "  loaded_model.load_state_dict(torch.load(ckpt_path, map_location=device))\n"
     ]
    }
   ],
   "source": [
    "ckpt_path = \"expression_net.pt\"\n",
    "torch.save(model.state_dict(), ckpt_path)\n",
    "print(f\"Saved to {ckpt_path}\")\n",
    "\n",
    "# Load into a fresh model\n",
    "loaded_model = ExpressionNet(n_genes).to(device)\n",
    "loaded_model.load_state_dict(torch.load(ckpt_path, map_location=device))\n",
    "loaded_model.eval()\n",
    "\n",
    "# Verify a quick prediction matches shape\n",
    "with torch.no_grad():\n",
    "    test_logits = loaded_model(X[:2].to(device))\n",
    "    print(\"Loaded model logits shape:\", test_logits.shape)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "ekman-teaching",
   "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.11.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
