{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 01 \u2014 Data preprocessing\n", "\n", "The model is downstream. Most real-world bugs are in this notebook's worth of code: bad dtype, wrong channel order, forgotten normalization, wrong image size.\n", "\n", "## Roadmap\n", "1. Image as a NumPy array\n", "2. dtype and value range \u2014 the most common bug\n", "3. Channel order: RGB vs BGR vs CHW vs HWC\n", "4. **Image size \u2014 decision table for classifier vs detector vs regressor**\n", "5. Normalization: why, and how to compute the constants\n", "6. Augmentation: random flip, crop, color jitter in NumPy\n", "7. One-hot encoding (classifier) vs scalar target (regressor)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# === Bootstrap (safe to re-run) ===\n", "# Installs minimal deps and downloads tiny CIFAR-10 subset for any notebook\n", "# that needs it. The from-scratch notebooks deliberately avoid importing the\n", "# project package so they run anywhere (Colab, plain Python, Jupyter).\n", "import os, sys, subprocess\n", "IN_COLAB = \"google.colab\" in sys.modules\n", "if IN_COLAB:\n", " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\",\n", " \"numpy\", \"matplotlib\", \"pillow\", \"torchvision\"], check=True)\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "np.random.seed(0)\n", "print(\"numpy\", np.__version__)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Image as a NumPy array\n", "\n", "A color image is a 3-D array: `(height, width, channels)`. Each channel value is typically 0..255 (uint8) or 0..1 (float).\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from PIL import Image\n", "import numpy as np\n", "\n", "# Make a synthetic 4\u00d76 RGB image: red square on white background\n", "arr = np.full((4, 6, 3), 255, dtype=np.uint8)\n", "arr[1:3, 1:3] = [255, 0, 0]\n", "img = Image.fromarray(arr, mode=\"RGB\")\n", "print(\"shape:\", arr.shape)\n", "print(\"first pixel (R,G,B):\", arr[0, 0])\n", "img.resize((180, 120)) # display upscale\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. dtype + value range (the most common silent bug)\n", "\n", "| Where | Typical dtype | Typical range |\n", "|---|---|---|\n", "| Disk (jpg/png) | uint8 | 0..255 |\n", "| Just-loaded NumPy | uint8 | 0..255 |\n", "| **Model input** | **float32** | **0..1 or normalised** |\n", "| Model output | float32 | depends on layer |\n", "\n", "**Always convert to float and divide by 255** before feeding a model. uint8 arithmetic wraps around silently \u2014 `np.uint8(200) + np.uint8(100) == 44`.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u = np.array([200, 100], dtype=np.uint8)\n", "print(\"uint8 add:\", u.sum()) # 44 \u2014 wraparound!\n", "f = u.astype(np.float32) / 255.0\n", "print(\"float add:\", f.sum()) # 1.176 \u2014 correct\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Channel order: RGB vs BGR, HWC vs CHW\n", "\n", "- **PIL / matplotlib / torchvision** use **HWC + RGB**.\n", "- **OpenCV** uses **HWC + BGR**.\n", "- **PyTorch tensors** use **CHW + RGB**.\n", "\n", "Mixing these is the #2 most common bug \u2014 model trained with one order tested with another quietly drops 20+ accuracy points.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Convert HWC \u2192 CHW (NumPy axis transpose)\n", "hwc = np.zeros((32, 32, 3), dtype=np.float32)\n", "chw = hwc.transpose(2, 0, 1) # axes: 2\u21920, 0\u21921, 1\u21922\n", "print(\"HWC:\", hwc.shape, \" \u2192 CHW:\", chw.shape)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Image-size decision rules\n", "\n", "You **never** feed the raw image. You pick a target size based on the *task*:\n", "\n", "| Task | Standard input size | Why |\n", "|---|---|---|\n", "| Image classifier (ResNet-style) | **224 \u00d7 224** | Pretrained ImageNet weights use this. Smaller loses fine detail; larger costs 4x memory per doubling. |\n", "| Object detector (YOLO-style) | **640 \u00d7 640** | Bigger so small objects survive. Must be divisible by 32 (the network's downsampling stride). |\n", "| Pixel-precise (segmentation) | 512\u20131024 | Need spatial detail in the output. |\n", "| Image regressor (cost, age, \u2026) | 224 \u00d7 224 | Same as classifier; the head is a single scalar instead of N classes. |\n", "\n", "**Output-size formula for a conv layer:**\n", "\n", "$$H_\\text{out} = \\left\\lfloor \\frac{H_\\text{in} + 2P - K}{S} \\right\\rfloor + 1$$\n", "\n", "where $K$ is kernel size, $S$ is stride, $P$ is padding. Worked example: a 224\u00d7224 image through a 7\u00d77 conv with stride 2 and padding 3:\n", "\n", "$$\\left\\lfloor \\frac{224 + 6 - 7}{2} \\right\\rfloor + 1 = 112$$\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def conv_output_size(H_in, K, S, P):\n", " return (H_in + 2*P - K) // S + 1\n", "\n", "print(\"first conv of ResNet50:\", conv_output_size(224, K=7, S=2, P=3)) # 112\n", "print(\"after maxpool 3\u00d73 s2 p1:\", conv_output_size(112, K=3, S=2, P=1)) # 56\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Normalization \u2014 why and how\n", "\n", "Networks train better when inputs are roughly mean 0, std 1. Two reasons:\n", "\n", "1. **Activations stay in the useful range** of sigmoid / tanh / softmax.\n", "2. **Gradient magnitudes are stable** across layers \u2014 avoids vanishing/exploding gradients.\n", "\n", "For ImageNet-pretrained models, the standard constants are computed once over millions of training images:\n", "\n", "```\n", "mean = [0.485, 0.456, 0.406]\n", "std = [0.229, 0.224, 0.225]\n", "```\n", "\n", "For your own dataset, compute them from the train split.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Compute CIFAR-10-style normalization constants from a small batch.\n", "imgs = np.random.rand(100, 32, 32, 3).astype(np.float32) # fake\n", "mean = imgs.mean(axis=(0, 1, 2))\n", "std = imgs.std(axis=(0, 1, 2))\n", "print(\"mean per channel:\", mean.round(3), \" std:\", std.round(3))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. Augmentation \u2014 three useful ones in 10 lines of NumPy\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def random_horizontal_flip(img, p=0.5):\n", " return img[:, ::-1] if np.random.rand() < p else img\n", "\n", "def random_crop_and_resize(img, crop_size, out_size):\n", " \"\"\"Crop a random crop_size\u00d7crop_size region then upscale (nearest) to out_size.\"\"\"\n", " H, W = img.shape[:2]\n", " y = np.random.randint(0, H - crop_size + 1)\n", " x = np.random.randint(0, W - crop_size + 1)\n", " crop = img[y:y+crop_size, x:x+crop_size]\n", " # Nearest-neighbour upscale; for production prefer LANCZOS via PIL.\n", " factor = out_size / crop_size\n", " rows = (np.arange(out_size) / factor).astype(int)\n", " cols = (np.arange(out_size) / factor).astype(int)\n", " return crop[np.ix_(rows, cols)]\n", "\n", "def color_jitter(img, strength=0.1):\n", " return np.clip(img + np.random.uniform(-strength, strength, size=3), 0, 1)\n", "\n", "# Quick demo on a synthetic image\n", "fig, axes = plt.subplots(1, 4, figsize=(12, 3))\n", "src = np.random.rand(32, 32, 3).astype(np.float32)\n", "for ax, fn, t in zip(axes,\n", " [lambda x: x, random_horizontal_flip,\n", " lambda x: random_crop_and_resize(x, 24, 32),\n", " color_jitter],\n", " [\"original\", \"h-flip\", \"random crop+resize\", \"color jitter\"]):\n", " ax.imshow(fn(src)); ax.set_title(t); ax.axis(\"off\")\n", "plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Targets: classifier vs regressor\n", "\n", "The model's *output head* and the *loss function* depend on what you want it to predict.\n", "\n", "| Task | Target shape | Final layer | Loss |\n", "|---|---|---|---|\n", "| Multi-class (one of N) | scalar int $\\in [0, N)$ | N logits | Softmax + cross-entropy |\n", "| Multi-label (any subset of N) | length-N vector of 0/1 | N logits | N independent BCEs |\n", "| Regression (scalar) | one float | 1 linear output | MSE or MAE |\n", "| Detection (boxes + classes) | list of (cls, bbox) | per-grid-cell predictions | sum of regression + classification |\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# One-hot encode an integer label\n", "def one_hot(y, num_classes):\n", " out = np.zeros(num_classes, dtype=np.float32)\n", " out[y] = 1.0\n", " return out\n", "\n", "print(one_hot(3, num_classes=10)) # plane=0 ... dog=5 ... so index 3 = cat \u2192 [0,0,0,1,0,0,0,0,0,0]\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Next:** build a single neuron, then a 2-layer MLP, and train it on a toy problem.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11" } }, "nbformat": 4, "nbformat_minor": 5 }