{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "c4293cea", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "c4293cea", "outputId": "4df83220-5d9e-47fe-dedd-6828fa02a869" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: torch in /usr/local/lib/python3.12/dist-packages (2.10.0+cpu)\n", "Requirement already satisfied: torchvision in /usr/local/lib/python3.12/dist-packages (0.25.0+cpu)\n", "Requirement already satisfied: filelock in /usr/local/lib/python3.12/dist-packages (from torch) (3.25.2)\n", "Requirement already satisfied: typing-extensions>=4.10.0 in /usr/local/lib/python3.12/dist-packages (from torch) (4.15.0)\n", "Requirement already satisfied: setuptools in /usr/local/lib/python3.12/dist-packages (from torch) (75.2.0)\n", "Requirement already satisfied: sympy>=1.13.3 in /usr/local/lib/python3.12/dist-packages (from torch) (1.14.0)\n", "Requirement already satisfied: networkx>=2.5.1 in /usr/local/lib/python3.12/dist-packages (from torch) (3.6.1)\n", "Requirement already satisfied: jinja2 in /usr/local/lib/python3.12/dist-packages (from torch) (3.1.6)\n", "Requirement already satisfied: fsspec>=0.8.5 in /usr/local/lib/python3.12/dist-packages (from torch) (2025.3.0)\n", "Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (from torchvision) (2.0.2)\n", "Requirement already satisfied: pillow!=8.3.*,>=5.3.0 in /usr/local/lib/python3.12/dist-packages (from torchvision) (11.3.0)\n", "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.12/dist-packages (from sympy>=1.13.3->torch) (1.3.0)\n", "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.12/dist-packages (from jinja2->torch) (3.0.3)\n", "Using device: cpu\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 9.91M/9.91M [00:00<00:00, 37.3MB/s]\n", "100%|██████████| 28.9k/28.9k [00:00<00:00, 1.09MB/s]\n", "100%|██████████| 1.65M/1.65M [00:00<00:00, 9.65MB/s]\n", "100%|██████████| 4.54k/4.54k [00:00<00:00, 7.07MB/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Train size: 54000, Val size: 6000, Test size: 10000\n" ] } ], "source": [ "%pip install torch torchvision\n", "import copy\n", "import random\n", "import numpy as np\n", "import torch\n", "from torch import nn, optim\n", "import torch.nn.functional as F\n", "from torch.utils.data import DataLoader, random_split\n", "from torchvision import datasets, transforms\n", "\n", "# Reproducibility and device config\n", "seed = 42\n", "random.seed(seed)\n", "np.random.seed(seed)\n", "torch.manual_seed(seed)\n", "if torch.cuda.is_available():\n", " torch.cuda.manual_seed_all(seed)\n", "\n", "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", "print('Using device:', device)\n", "\n", "# Normalize MNIST inputs\n", "transform = transforms.Compose([\n", " transforms.ToTensor(),\n", " transforms.Normalize((0.5,), (0.5,)),\n", "])\n", "\n", "# Keep official MNIST test split untouched; derive validation split from train\n", "full_trainset = datasets.MNIST('~/.pytorch/MNIST_data/', download=True, train=True, transform=transform)\n", "testset = datasets.MNIST('~/.pytorch/MNIST_data/', download=True, train=False, transform=transform)\n", "\n", "val_ratio = 0.1\n", "val_size = int(len(full_trainset) * val_ratio)\n", "train_size = len(full_trainset) - val_size\n", "trainset, valset = random_split(\n", " full_trainset,\n", " [train_size, val_size],\n", " generator=torch.Generator().manual_seed(seed),\n", ")\n", "\n", "batch_size = 128\n", "trainloader = DataLoader(trainset, batch_size=batch_size, shuffle=True)\n", "valloader = DataLoader(valset, batch_size=batch_size, shuffle=False)\n", "testloader = DataLoader(testset, batch_size=batch_size, shuffle=False)\n", "\n", "print(f'Train size: {len(trainset)}, Val size: {len(valset)}, Test size: {len(testset)}')" ] }, { "cell_type": "code", "execution_count": 2, "id": "9235bb6d", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "9235bb6d", "outputId": "fb81434e-c87a-402f-dbd3-a68e72398f50" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Epoch 1/2 | train_loss=0.4595 | val_loss=0.1212 | val_acc=0.9642\n", "Epoch 2/2 | train_loss=0.1231 | val_loss=0.0976 | val_acc=0.9710\n" ] } ], "source": [ "import matplotlib.pyplot as plt\n", "from torch.utils.data import Subset\n", "\n", "class CNNClassifier(nn.Module):\n", " def __init__(self):\n", " super().__init__()\n", " self.features = nn.Sequential(\n", " nn.Conv2d(1, 32, kernel_size=3, padding=1),\n", " nn.BatchNorm2d(32),\n", " nn.ReLU(inplace=True),\n", " nn.MaxPool2d(2),\n", " nn.Conv2d(32, 64, kernel_size=3, padding=1),\n", " nn.BatchNorm2d(64),\n", " nn.ReLU(inplace=True),\n", " nn.MaxPool2d(2),\n", " )\n", " self.classifier = nn.Sequential(\n", " nn.Flatten(),\n", " nn.Linear(64 * 7 * 7, 128),\n", " nn.ReLU(inplace=True),\n", " nn.Dropout(0.3),\n", " nn.Linear(128, 10),\n", " )\n", "\n", " def forward(self, x):\n", " x = self.features(x)\n", " return self.classifier(x)\n", "\n", "\n", "def evaluate(model, loader, criterion, device):\n", " model.eval()\n", " total_loss = 0.0\n", " correct = 0\n", " total = 0\n", " with torch.no_grad():\n", " for images, labels in loader:\n", " images, labels = images.to(device), labels.to(device)\n", " logits = model(images)\n", " loss = criterion(logits, labels)\n", " total_loss += loss.item() * images.size(0)\n", " preds = logits.argmax(dim=1)\n", " correct += (preds == labels).sum().item()\n", " total += labels.size(0)\n", "\n", " return total_loss / total, correct / total\n", "\n", "\n", "model = CNNClassifier().to(device)\n", "criterion = nn.CrossEntropyLoss()\n", "optimizer = optim.Adam(model.parameters(), lr=1e-3)\n", "\n", "# Train on a subset for a quick notebook demo run.\n", "subset_size = 12000\n", "quick_indices = list(range(min(subset_size, len(trainset))))\n", "quick_trainset = Subset(trainset, quick_indices)\n", "quick_trainloader = DataLoader(quick_trainset, batch_size=batch_size, shuffle=True)\n", "\n", "epochs = 2\n", "for epoch in range(epochs):\n", " model.train()\n", " running_loss = 0.0\n", " for images, labels in quick_trainloader:\n", " images, labels = images.to(device), labels.to(device)\n", "\n", " optimizer.zero_grad()\n", " logits = model(images)\n", " loss = criterion(logits, labels)\n", " loss.backward()\n", " optimizer.step()\n", "\n", " running_loss += loss.item() * images.size(0)\n", "\n", " train_loss = running_loss / len(quick_trainset)\n", " val_loss, val_acc = evaluate(model, valloader, criterion, device)\n", " print(f\"Epoch {epoch + 1}/{epochs} | train_loss={train_loss:.4f} | val_loss={val_loss:.4f} | val_acc={val_acc:.4f}\")" ] }, { "cell_type": "code", "execution_count": 3, "id": "69ad349e", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 342 }, "id": "69ad349e", "outputId": "7477e7e0-ace9-4d98-9267-d6e13644a857" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Random index: 8071\n", "True label: 4, Predicted label: 4\n" ] }, { "data": { "image/png": "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9arTfnM7lemTTz6ppUuXOr1mzJgh6c/nFpYuXZppmHzz5s35OtEDKF5GjRolb29v9e/fX8ePH89U/8svv+iNN97Idv2sjv2LFy92Or9J//ccWAYPDw/VqVNHxhhduXJFqampmY6F5cuXV2BgYKYpNLMzffp0p+WpU6dK0g0TC3d3d919991atmyZ0y08x48f14IFC9SiRQvHLVgZZs6c6XRuiouL09WrVx37yrhyfm3fpKSk6K233soyhqtXrzqO6xltZ8yYoXLlyqlx48Y5xn8jGQlHbs573bt31++//65Zs2Zlqrt06ZLjt3/++OOPTPUZF6hy+3kBhcFSIwehoaFq0aKFBg8erOTkZL3++usqU6aMRo0a5Wjz6KOP6tVXX1VkZKQee+wxnThxQm+//bbCwsKcHo7y8vJSnTp19NFHH6lGjRoqXbq06tatq7p16xba++ncubPmzZsnf39/1alTR+vXr9fKlSsdt/dkGDVqlD744APdddddeuKJJxxTmVapUkV//PGH40qFn5+f4uLi1KdPHzVq1Eg9e/ZUuXLldOjQIX3xxReKiIjQtGnTso3H09NTd999t1auXKlx48Y51TVu3FgrV67Uq6++qsDAQFWrVi3TNHXXy+1n0bp1aw0aNEixsbHaunWr7r77bt12223au3evFi9erDfeeEMPPfSQpNxPZdqoUaNM0/hlnMDCwsIy/dLziRMn9PPPP2d6OA8ArhcSEqIFCxaoR48eql27ttMvJH/33XdavHhxjsenzp07a9y4cerXr5+aN2+ubdu2af78+U4P9krS3XffrYoVKyoiIkIVKlTQzp07NW3aNHXq1Em+vr46e/asKleurIceekgNGjSQj4+PVq5cqY0bNzqNSOdk//79uu+++3TPPfdo/fr1+uCDD/Twww+rQYMGN1z3xRdfdMzbP2TIEJUoUUIzZsxQcnKyXnrppUztU1JS1L59e3Xv3l27d+/WW2+9pRYtWui+++6TJDVv3lwBAQGKiorSk08+KZvNpnnz5mX7HFxgYKAmT56sAwcOqEaNGvroo4+0detWzZw5M9c/aJadhg0byt3dXZMnT1ZiYqLsdrvatWun8uXLZ2rbp08fLVq0SI8//rhWr16tiIgIpaamateuXVq0aJFWrFihJk2aaNy4cfr222/VqVMnBQcH68SJE3rrrbdUuXJlx+8D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DgAAAACkIzkAUGRNnz5dVatWlaenp5o1a6Yffvghx/aLFy9WrVq15OnpqXr16mn58uWFFKn15KVvZ82apZYtWyogIEABAQHq0KHDDT+L4iqvf7MZFi5cKJvNpq5duxZsgBaW1749e/asoqOjValSJdntdtWoUYNjQjby2revv/66atasKS8vLwUFBempp57S5cuXCyla6/j222/VpUsXBQYGymaz6ZNPPrnhOmvWrFGjRo1kt9sVGhqquXPn5n3HBgCKoIULFxoPDw/z7rvvmh07dpgBAwaYUqVKmePHj2fZft26dcbd3d289NJLJiEhwfz73/82t912m9m2bVshR37ry2vfPvzww2b69Olmy5YtZufOnaZv377G39/f/Pbbb4Uc+a0tr/2aYf/+/eb22283LVu2NPfff3/hBGsxee3b5ORk06RJE3PvvfeatWvXmv3795s1a9aYrVu3FnLkt7689u38+fON3W438+fPN/v37zcrVqwwlSpVMk899VQhR37rW758uXnuuefMkiVLjCSzdOnSHNv/+uuvpmTJkmb48OEmISHBTJ061bi7u5v4+Pg87ZfkAECR1LRpUxMdHe1YTk1NNYGBgSY2NjbL9t27dzedOnVyKmvWrJkZNGhQgcZpRXnt2+tdvXrV+Pr6mvfee6+gQrSk/PTr1atXTfPmzc0777xjoqKiSA6ykde+jYuLM9WrVzcpKSmFFaJl5bVvo6OjTbt27ZzKhg8fbiIiIgo0TqvLTXIwatQoExYW5lTWo0cPExkZmad9cVsRgCInJSVFmzdvVocOHRxlbm5u6tChg9avX5/lOuvXr3dqL0mRkZHZti+u8tO317t48aKuXLmi0qVLF1SYlpPffh03bpzKly+vxx57rDDCtKT89O2nn36q8PBwRUdHq0KFCqpbt64mTpyo1NTUwgrbEvLTt82bN9fmzZsdtx79+uuvWr58ue69995Cibkou1nnsRI3MygAuBWcOnVKqampqlChglN5hQoVtGvXrizXOXbsWJbtjx07VmBxWlF++vZ6Tz/9tAIDAzOdxIqz/PTr2rVrNXv2bG3durUQIrSu/PTtr7/+qq+//lq9e/fW8uXLtW/fPg0ZMkRXrlzR6NGjCyNsS8hP3z788MM6deqUWrRoIWOMrl69qscff1zPPvtsYYRcpGV3HktKStKlS5fk5eWVq+0w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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def view_classify(image_tensor, probs, true_label=None):\n", " image = image_tensor.squeeze().detach().cpu().numpy()\n", " probs = probs.squeeze().detach().cpu().numpy()\n", "\n", " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3))\n", " ax1.imshow(image * 0.5 + 0.5, cmap='gray')\n", " ax1.axis('off')\n", " title = 'Input image' if true_label is None else f'Input image (true: {true_label})'\n", " ax1.set_title(title)\n", "\n", " ax2.barh(np.arange(10), probs)\n", " ax2.set_yticks(np.arange(10))\n", " ax2.set_xlim(0, 1.0)\n", " ax2.set_xlabel('Probability')\n", " ax2.set_title('Class probabilities')\n", "\n", " plt.tight_layout()\n", " plt.show()\n", "\n", "\n", "model.eval()\n", "random_idx = torch.randint(low=0, high=len(testset), size=(1,)).item()\n", "sample_image, sample_label = testset[random_idx]\n", "sample_image = sample_image.unsqueeze(0).to(device)\n", "true_label = int(sample_label)\n", "\n", "with torch.no_grad():\n", " logits = model(sample_image)\n", " probs = torch.softmax(logits, dim=1)\n", " pred_label = int(torch.argmax(probs, dim=1).item())\n", "\n", "print(f'Random index: {random_idx}')\n", "print(f'True label: {true_label}, Predicted label: {pred_label}')\n", "view_classify(sample_image.cpu(), probs.cpu(), true_label=true_label)" ] }, { "cell_type": "code", "execution_count": 5, "id": "1bcad893", "metadata": {}, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "code", "execution_count": 6, "id": "c58440ac", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Model saved locally: /content/DL_CNN_NumberRecognition.pth\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "3eb76e7015334c26a8c8a3aca0f455c4", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b38e9a795c0441aa89f4d7bc0269029c", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "fda643108a3b4e0aa8c6bf5bcec40db2", "version_major": 2, "version_minor": 0 }, "text/plain": [ " DL_CNN_NumberRecognition.pth: 100%|#########9| 1.69MB / 1.70MB " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Uploaded to https://huggingface.co/looh2/model\n" ] } ], "source": [ "# Save model locally, then upload to Hugging Face Hub\n", "from pathlib import Path\n", "from huggingface_hub import HfApi\n", "\n", "repo_id = \"looh2/model\"\n", "model_path = Path(\"DL_CNN_NumberRecognition.pth\")\n", "\n", "torch.save(model.state_dict(), model_path)\n", "print(f\"Model saved locally: {model_path.resolve()}\")\n", "\n", "api = HfApi()\n", "api.create_repo(repo_id=repo_id, repo_type=\"model\", exist_ok=True)\n", "api.upload_file(\n", " path_or_fileobj=str(model_path),\n", " path_in_repo=model_path.name,\n", " repo_id=repo_id,\n", " repo_type=\"model\",\n", ")\n", "\n", "print(f\"Uploaded to https://huggingface.co/{repo_id}\")" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.13" } }, "nbformat": 4, "nbformat_minor": 5 }