{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# \ud83d\udd22 Digit Recognizer \u2014 CNN dengan PyTorch\n**Kompetisi Kaggle:** [Digit Recognizer](https://kaggle.com/competitions/digit-recognizer)\n\n**Tujuan:** Klasifikasi gambar angka tulisan tangan (0-9) menggunakan Convolutional Neural Network\n\n**Dataset:** MNIST \u2014 42,000 training images, 28,000 test images (28x28 pixel)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport glob, warnings\nwarnings.filterwarnings('ignore')\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import DataLoader, TensorDataset\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'\u2705 Using: {device}')\nif device.type == 'cuda':\n    print(f'   GPU: {torch.cuda.get_device_name()}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# === LOAD DATA ===\ntrain_df = pd.read_csv(glob.glob('/kaggle/input/**/train.csv', recursive=True)[0])\ntest_df  = pd.read_csv(glob.glob('/kaggle/input/**/test.csv',  recursive=True)[0])\n\nprint(f'Training: {train_df.shape} | Test: {test_df.shape}')\nprint(f'Labels (0-9): {sorted(train_df[\"label\"].unique())}')\nprint(f'Distribusi label:\\n{train_df[\"label\"].value_counts().sort_index()}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# === VISUALISASI CONTOH ANGKA ===\nfig, axes = plt.subplots(2, 10, figsize=(15, 3))\nfor digit in range(10):\n    for row in range(2):\n        sample = train_df[train_df['label'] == digit].iloc[row]\n        img = sample.drop('label').values.reshape(28, 28)\n        axes[row, digit].imshow(img, cmap='gray')\n        axes[row, digit].axis('off')\n        if row == 0:\n            axes[row, digit].set_title(str(digit), fontsize=14, fontweight='bold')\nplt.suptitle('Contoh Angka 0-9 dari Dataset MNIST', fontsize=14)\nplt.tight_layout()\nplt.show()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# === PREPARE DATA for PyTorch ===\n# Pisahkan label dan pixel\ny_train = train_df['label'].values\nX_train = train_df.drop('label', axis=1).values.astype(np.float32) / 255.0  # Normalize 0-1\nX_test  = test_df.values.astype(np.float32) / 255.0\n\n# Reshape ke format gambar: (batch, channels, height, width)\nX_train = X_train.reshape(-1, 1, 28, 28)\nX_test  = X_test.reshape(-1, 1, 28, 28)\n\n# Split training -> train + validation (90/10)\nfrom sklearn.model_selection import train_test_split\nX_tr, X_val, y_tr, y_val = train_test_split(X_train, y_train, test_size=0.1, random_state=42, stratify=y_train)\n\n# Convert ke PyTorch tensors\ntrain_dataset = TensorDataset(torch.tensor(X_tr), torch.tensor(y_tr, dtype=torch.long))\nval_dataset   = TensorDataset(torch.tensor(X_val), torch.tensor(y_val, dtype=torch.long))\ntest_tensor   = torch.tensor(X_test)\n\ntrain_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)\nval_loader   = DataLoader(val_dataset,   batch_size=256, shuffle=False)\n\nprint(f'Train: {len(X_tr)} | Val: {len(X_val)} | Test: {len(X_test)}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================\n# CNN MODEL \u2014 Convolutional Neural Network\n# =============================================\nclass DigitCNN(nn.Module):\n    def __init__(self):\n        super(DigitCNN, self).__init__()\n        \n        # Conv Block 1: 1 channel -> 32 filters\n        self.conv1 = nn.Sequential(\n            nn.Conv2d(1, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(),\n            nn.Conv2d(32, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(),\n            nn.MaxPool2d(2),            # 28x28 -> 14x14\n            nn.Dropout(0.25)\n        )\n        \n        # Conv Block 2: 32 -> 64 filters\n        self.conv2 = nn.Sequential(\n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.Conv2d(64, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.MaxPool2d(2),            # 14x14 -> 7x7\n            nn.Dropout(0.25)\n        )\n        \n        # Fully Connected\n        self.fc = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(64 * 7 * 7, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(256, 10)          # 10 classes (digit 0-9)\n        )\n    \n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.conv2(x)\n        x = self.fc(x)\n        return x\n\nmodel = DigitCNN().to(device)\nprint(model)\ntotal_params = sum(p.numel() for p in model.parameters())\nprint(f'\\nTotal parameters: {total_params:,}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# === TRAINING ===\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=0.001)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5)\n\nEPOCHS = 25\nbest_val_acc = 0\nhistory = {'train_loss': [], 'val_loss': [], 'val_acc': []}\n\nfor epoch in range(EPOCHS):\n    # Training\n    model.train()\n    train_loss = 0\n    for X_batch, y_batch in train_loader:\n        X_batch, y_batch = X_batch.to(device), y_batch.to(device)\n        optimizer.zero_grad()\n        output = model(X_batch)\n        loss = criterion(output, y_batch)\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item()\n    \n    # Validation\n    model.eval()\n    val_loss = 0\n    correct = 0\n    total = 0\n    with torch.no_grad():\n        for X_batch, y_batch in val_loader:\n            X_batch, y_batch = X_batch.to(device), y_batch.to(device)\n            output = model(X_batch)\n            val_loss += criterion(output, y_batch).item()\n            pred = output.argmax(dim=1)\n            correct += (pred == y_batch).sum().item()\n            total += len(y_batch)\n    \n    val_acc = correct / total\n    avg_train_loss = train_loss / len(train_loader)\n    avg_val_loss = val_loss / len(val_loader)\n    \n    history['train_loss'].append(avg_train_loss)\n    history['val_loss'].append(avg_val_loss)\n    history['val_acc'].append(val_acc)\n    \n    scheduler.step(avg_val_loss)\n    \n    # Save best model\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        torch.save(model.state_dict(), 'best_model.pth')\n    \n    if (epoch + 1) % 5 == 0 or epoch == 0:\n        print(f'Epoch {epoch+1:02d}/{EPOCHS} | Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f} | Val Acc: {val_acc*100:.2f}%')\n\nprint(f'\\n\ud83c\udfc6 Best Validation Accuracy: {best_val_acc*100:.2f}%')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# === TRAINING CURVE ===\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))\n\nax1.plot(history['train_loss'], label='Train Loss', color='#FF6B6B')\nax1.plot(history['val_loss'], label='Val Loss', color='#4ECDC4')\nax1.set_title('Loss per Epoch')\nax1.set_xlabel('Epoch')\nax1.legend()\n\nax2.plot(history['val_acc'], label='Val Accuracy', color='#45B7D1')\nax2.set_title('Validation Accuracy per Epoch')\nax2.set_xlabel('Epoch')\nax2.set_ylabel('Accuracy')\nax2.legend()\n\nplt.tight_layout()\nplt.savefig('training_curve.png', dpi=100)\nplt.show()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# === PREDICT TEST SET ===\nmodel.load_state_dict(torch.load('best_model.pth'))\nmodel.eval()\n\ntest_loader = DataLoader(TensorDataset(test_tensor), batch_size=256)\npredictions = []\n\nwith torch.no_grad():\n    for (X_batch,) in test_loader:\n        X_batch = X_batch.to(device)\n        output = model(X_batch)\n        pred = output.argmax(dim=1)\n        predictions.extend(pred.cpu().numpy())\n\npredictions = np.array(predictions)\nprint(f'Total predictions: {len(predictions)}')\nprint(f'Distribusi prediksi:')\nfor d in range(10):\n    print(f'  Digit {d}: {(predictions==d).sum()}')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# === VISUALISASI PREDIKSI ===\nfig, axes = plt.subplots(2, 10, figsize=(15, 3))\nfor i in range(20):\n    row, col = i // 10, i % 10\n    img = X_test[i].reshape(28, 28)\n    axes[row, col].imshow(img, cmap='gray')\n    axes[row, col].set_title(str(predictions[i]), color='green', fontsize=12, fontweight='bold')\n    axes[row, col].axis('off')\nplt.suptitle('Prediksi Model pada 20 Gambar Test', fontsize=14)\nplt.tight_layout()\nplt.show()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# === GENERATE SUBMISSION ===\nsubmission = pd.DataFrame({\n    'ImageId': range(1, len(predictions) + 1),\n    'Label': predictions\n})\nsubmission.to_csv('submission.csv', index=False)\nprint('\u2705 submission.csv siap!')\nprint(f'Total: {len(submission)} gambar')\nsubmission.head(10)"}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.10.0"}}, "nbformat": 4, "nbformat_minor": 4}