{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":3004,"databundleVersionId":861823,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:12.465575Z","iopub.execute_input":"2026-01-16T19:05:12.465934Z","iopub.status.idle":"2026-01-16T19:05:12.755809Z","shell.execute_reply.started":"2026-01-16T19:05:12.465906Z","shell.execute_reply":"2026-01-16T19:05:12.755127Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Import Libraries","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\n\nimport torch\nimport torch.nn as nn\nfrom torchvision import transforms, models\nfrom torch.autograd import Variable\nfrom torch import nn as nn_torch, optim\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, Dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:12.756898Z","iopub.execute_input":"2026-01-16T19:05:12.757372Z","iopub.status.idle":"2026-01-16T19:05:26.422866Z","shell.execute_reply.started":"2026-01-16T19:05:12.757332Z","shell.execute_reply":"2026-01-16T19:05:26.422133Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Load Data","metadata":{}},{"cell_type":"code","source":"train = pd.read_csv('/kaggle/input/digit-recognizer/train.csv', dtype=np.float32)\ntest = pd.read_csv('/kaggle/input/digit-recognizer/test.csv', dtype=np.float32)\nsample_sub = pd.read_csv('/kaggle/input/digit-recognizer/sample_submission.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:26.423825Z","iopub.execute_input":"2026-01-16T19:05:26.424258Z","iopub.status.idle":"2026-01-16T19:05:30.859658Z","shell.execute_reply.started":"2026-01-16T19:05:26.424225Z","shell.execute_reply":"2026-01-16T19:05:30.859083Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Pisahkan Target dan Fitur","metadata":{}},{"cell_type":"code","source":"targets_np = train.label.values\nfeatures_np = train.iloc[:, 1:].values/255\n\n# Reshape untuk CNN: (jumlah_samples, 1, 28, 28)\nfeatures_np = features_np.reshape(-1, 1, 28, 28)\n\n# Split data menjadi Train dan Test set\nX_train, X_test, y_train, y_test = train_test_split(\n    features_np, targets_np, test_size=0.2, random_state=42\n)\n\nprint(f\"Training set size: {len(X_train)}\")\nprint(f\"Test set size: {len(X_test)}\")\nprint(f\"Shape X_train: {X_train.shape}\")\nprint(f\"Shape X_test: {X_test.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:30.861139Z","iopub.execute_input":"2026-01-16T19:05:30.861389Z","iopub.status.idle":"2026-01-16T19:05:31.218714Z","shell.execute_reply.started":"2026-01-16T19:05:30.861366Z","shell.execute_reply":"2026-01-16T19:05:31.218088Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Konversi dari Arrays ke Tensors (untuk CNN)","metadata":{}},{"cell_type":"code","source":"class DigitDatasetCNN(Dataset):\n    def __init__(self, X, y):\n        # X sudah dalam bentuk (batch, channel, height, width)\n        self.X = torch.tensor(X, dtype=torch.float32)\n        self.y = torch.tensor(y, dtype=torch.long)\n        \n    def __len__(self):\n        return len(self.y)\n\n    def __getitem__(self, idx):\n        return self.X[idx], self.y[idx]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:31.219512Z","iopub.execute_input":"2026-01-16T19:05:31.219707Z","iopub.status.idle":"2026-01-16T19:05:31.224386Z","shell.execute_reply.started":"2026-01-16T19:05:31.219688Z","shell.execute_reply":"2026-01-16T19:05:31.22365Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"torch.manual_seed(1111)\ntrain_dataset = DigitDatasetCNN(X_train, y_train)\ntest_dataset = DigitDatasetCNN(X_test, y_test)\n\nprint(f\"Train dataset size: {len(train_dataset)}\")\nprint(f\"Test dataset size: {len(test_dataset)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:31.225355Z","iopub.execute_input":"2026-01-16T19:05:31.225692Z","iopub.status.idle":"2026-01-16T19:05:31.296962Z","shell.execute_reply.started":"2026-01-16T19:05:31.22566Z","shell.execute_reply":"2026-01-16T19:05:31.29622Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Data Loaders","metadata":{}},{"cell_type":"code","source":"batch_size = 64\ng = torch.Generator()\ng.manual_seed(1111)\ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, generator=g)\ntest_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True, generator=g)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:31.297858Z","iopub.execute_input":"2026-01-16T19:05:31.298117Z","iopub.status.idle":"2026-01-16T19:05:31.303055Z","shell.execute_reply.started":"2026-01-16T19:05:31.298094Z","shell.execute_reply":"2026-01-16T19:05:31.302177Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Fungsi untuk Plot Images","metadata":{}},{"cell_type":"code","source":"def plot_images(images):\n    n_images = len(images)\n    rows = int(np.sqrt(n_images))\n    cols = int(np.sqrt(n_images))\n\n    fig = plt.figure(figsize=(10, 10))\n    for i in range(rows*cols):\n        ax = fig.add_subplot(rows, cols, i+1)\n        # Untuk CNN, shape images[i] adalah (1, 28, 28), kita perlu squeeze channel dimension\n        ax.imshow(images[i].squeeze(), cmap='bone')\n        ax.axis('off')\n    plt.suptitle('Contoh Gambar Digit Tangan (CNN Input)', fontsize=16)\n    plt.show()\n\nN_IMAGES = 25\nimages = [image for image, label in [train_dataset[i] for i in range(N_IMAGES)]]\nplot_images(images)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:31.303907Z","iopub.execute_input":"2026-01-16T19:05:31.304127Z","iopub.status.idle":"2026-01-16T19:05:31.866793Z","shell.execute_reply.started":"2026-01-16T19:05:31.304108Z","shell.execute_reply":"2026-01-16T19:05:31.866199Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Definisikan Model CNN","metadata":{}},{"cell_type":"code","source":"class CNNdigit(nn.Module):\n    def __init__(self, num_classes=10):\n        super(CNNdigit, self).__init__()\n        \n        # Convolutional layers\n        self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)\n        self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)\n        self.conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1)\n        \n        # Pooling layer\n        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)\n        \n        # Batch normalization\n        self.bn1 = nn.BatchNorm2d(32)\n        self.bn2 = nn.BatchNorm2d(64)\n        self.bn3 = nn.BatchNorm2d(128)\n        \n        # Dropout\n        self.dropout = nn.Dropout(0.25)\n        self.dropout_fc = nn.Dropout(0.5)\n        \n        # Fully connected layers\n        # Setelah 3 pooling layers dengan kernel_size=2, image size menjadi: 28 -> 14 -> 7 -> 3 (pembulatan ke bawah)\n        # Jadi 128 channels * 3 * 3 = 1152\n        self.fc1 = nn.Linear(128 * 3 * 3, 256)\n        self.fc2 = nn.Linear(256, 128)\n        self.fc3 = nn.Linear(128, num_classes)\n        \n    def forward(self, x):\n        # Convolutional block 1\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = F.relu(x)\n        x = self.pool(x)\n        x = self.dropout(x)\n        \n        # Convolutional block 2\n        x = self.conv2(x)\n        x = self.bn2(x)\n        x = F.relu(x)\n        x = self.pool(x)\n        x = self.dropout(x)\n        \n        # Convolutional block 3\n        x = self.conv3(x)\n        x = self.bn3(x)\n        x = F.relu(x)\n        x = self.pool(x)\n        x = self.dropout(x)\n        \n        # Flatten untuk fully connected layers\n        x = x.view(x.size(0), -1)\n        \n        # Fully connected layers\n        x = F.relu(self.fc1(x))\n        x = self.dropout_fc(x)\n        x = F.relu(self.fc2(x))\n        x = self.dropout_fc(x)\n        x = self.fc3(x)\n        \n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:31.867729Z","iopub.execute_input":"2026-01-16T19:05:31.868058Z","iopub.status.idle":"2026-01-16T19:05:31.877097Z","shell.execute_reply.started":"2026-01-16T19:05:31.868031Z","shell.execute_reply":"2026-01-16T19:05:31.87636Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Alternatif Arsitektur CNN yang Lebih Sederhana","metadata":{}},{"cell_type":"code","source":"class SimpleCNNdigit(nn.Module):\n    def __init__(self, num_classes=10):\n        super(SimpleCNNdigit, self).__init__()\n        \n        # Feature extraction\n        self.features = nn.Sequential(\n            # Block 1\n            nn.Conv2d(1, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(32, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(kernel_size=2, stride=2),\n            nn.Dropout(0.25),\n            \n            # Block 2\n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(64, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(kernel_size=2, stride=2),\n            nn.Dropout(0.25),\n        )\n        \n        # Classifier\n        self.classifier = nn.Sequential(\n            nn.Linear(64 * 7 * 7, 512),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.5),\n            nn.Linear(512, 256),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.5),\n            nn.Linear(256, num_classes)\n        )\n        \n    def forward(self, x):\n        x = self.features(x)\n        x = x.view(x.size(0), -1)\n        x = self.classifier(x)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:31.879166Z","iopub.execute_input":"2026-01-16T19:05:31.879383Z","iopub.status.idle":"2026-01-16T19:05:31.905737Z","shell.execute_reply.started":"2026-01-16T19:05:31.879362Z","shell.execute_reply":"2026-01-16T19:05:31.905067Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Inisialisasi Model CNN dan Parameter","metadata":{}},{"cell_type":"code","source":"num_classes = 10\nlr = 0.001\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# Pilih model yang akan digunakan (ubah ke False untuk menggunakan SimpleCNNdigit)\nUSE_COMPLEX_CNN = True\n\nif USE_COMPLEX_CNN:\n    model = CNNdigit(num_classes).to(device)\n    model_name = \"Complex CNN\"\nelse:\n    model = SimpleCNNdigit(num_classes).to(device)\n    model_name = \"Simple CNN\"\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=lr)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', patience=3, factor=0.5)\n\nprint(f'Model: {model_name}')\nprint(f'Menggunakan device: {device}')\nprint(f'Jumlah parameter: {sum(p.numel() for p in model.parameters()):,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:31.906638Z","iopub.execute_input":"2026-01-16T19:05:31.906893Z","iopub.status.idle":"2026-01-16T19:05:32.229895Z","shell.execute_reply.started":"2026-01-16T19:05:31.906864Z","shell.execute_reply":"2026-01-16T19:05:32.229131Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Hitung Akurasi","metadata":{}},{"cell_type":"code","source":"def cal_accuracy(y_pred, y):\n    predicted = torch.argmax(y_pred, dim=-1)\n    correct = (predicted == y).sum()\n    acc = correct.float() / y.shape[0]\n    return acc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:32.230776Z","iopub.execute_input":"2026-01-16T19:05:32.231077Z","iopub.status.idle":"2026-01-16T19:05:32.235814Z","shell.execute_reply.started":"2026-01-16T19:05:32.231048Z","shell.execute_reply":"2026-01-16T19:05:32.235066Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Fungsi Training untuk CNN","metadata":{}},{"cell_type":"code","source":"def train_cnn(model, train_loader, optimizer, criterion, device):\n    epoch_loss = 0.0\n    epoch_acc = 0.0\n    model.train()\n\n    for X, y in train_loader:\n        X, y = X.to(device), y.to(device)\n        \n        # Forward pass\n        y_pred = model(X)\n        loss = criterion(y_pred, y)\n        acc = cal_accuracy(y_pred, y)\n\n        # Backward pass dan optimisasi\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n        epoch_loss += loss.item()\n        epoch_acc += acc.item()\n\n    return epoch_loss/len(train_loader), epoch_acc/len(train_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:32.236592Z","iopub.execute_input":"2026-01-16T19:05:32.236829Z","iopub.status.idle":"2026-01-16T19:05:32.253838Z","shell.execute_reply.started":"2026-01-16T19:05:32.236797Z","shell.execute_reply":"2026-01-16T19:05:32.253164Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Fungsi Evaluation untuk CNN dengan Return Predictions","metadata":{}},{"cell_type":"code","source":"def evaluate_cnn(model, test_loader, criterion, device, return_predictions=False):\n    epoch_loss = 0\n    epoch_acc = 0\n    all_predictions = []\n    all_labels = []\n    \n    model.eval()\n\n    with torch.no_grad():\n        for X, y in test_loader:\n            X, y = X.to(device), y.to(device)\n            y_pred = model(X)\n            loss = criterion(y_pred, y)\n            acc = cal_accuracy(y_pred, y)\n            \n            # Simpan prediksi dan label jika diminta\n            if return_predictions:\n                all_predictions.extend(torch.argmax(y_pred, dim=1).cpu().numpy())\n                all_labels.extend(y.cpu().numpy())\n\n            epoch_loss += loss.item()\n            epoch_acc += acc.item()\n\n    if return_predictions:\n        return epoch_loss/len(test_loader), epoch_acc/len(test_loader), all_predictions, all_labels\n    else:\n        return epoch_loss/len(test_loader), epoch_acc/len(test_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:32.254708Z","iopub.execute_input":"2026-01-16T19:05:32.254962Z","iopub.status.idle":"2026-01-16T19:05:32.270705Z","shell.execute_reply.started":"2026-01-16T19:05:32.254933Z","shell.execute_reply":"2026-01-16T19:05:32.270056Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Training Loop untuk CNN","metadata":{}},{"cell_type":"code","source":"best_valid_loss = float('inf')\nEPOCHS = 20  # CNN biasanya butuh lebih sedikit epoch\ntrain_losses, test_losses, train_accs, valid_accs = [], [], [], []\n\nprint(f\"\\nMemulai Training {model_name} untuk {EPOCHS} epochs...\")\nprint(\"=\"*60)\n\nfor epoch in range(EPOCHS):\n    train_loss, train_acc = train_cnn(model, train_loader, optimizer, criterion, device)\n    valid_loss, valid_acc = evaluate_cnn(model, test_loader, criterion, device, return_predictions=False)\n    \n    # Update learning rate scheduler\n    scheduler.step(valid_loss)\n    \n    train_losses.append(train_loss)\n    test_losses.append(valid_loss)\n    train_accs.append(train_acc)\n    valid_accs.append(valid_acc)\n\n    if valid_loss < best_valid_loss:\n        best_valid_loss = valid_loss\n        torch.save(model.state_dict(), f'digit-cnn-model-{model_name.replace(\" \", \"-\").lower()}.pt')\n        print(f\"✓ Model disimpan di epoch {epoch+1} dengan valid loss: {valid_loss:.4f}\")\n\n    if (epoch + 1) % 5 == 0 or epoch == 0:\n        print(f'\\nEpoch: {epoch+1:02}/{EPOCHS}')\n        print(f'\\t Train loss: {train_loss:.4f} | Train acc: {train_acc*100:.2f}%')\n        print(f'\\t  Val  loss: {valid_loss:.4f} |   Val acc: {valid_acc*100:.2f}%')\n        print(f'\\t  LR: {optimizer.param_groups[0][\"lr\"]:.6f}')\n\nprint(f\"\\nTraining selesai! Akurasi validation terbaik: {max(valid_accs)*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:05:32.27148Z","iopub.execute_input":"2026-01-16T19:05:32.271748Z","iopub.status.idle":"2026-01-16T19:06:21.470605Z","shell.execute_reply.started":"2026-01-16T19:05:32.271715Z","shell.execute_reply":"2026-01-16T19:06:21.469954Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Plot Training History","metadata":{}},{"cell_type":"code","source":"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))\n\n# Plot 1: Losses\nax1.plot(train_losses, label='Training Loss', linewidth=2, marker='o', markersize=4)\nax1.plot(test_losses, label='Validation Loss', linewidth=2, marker='s', markersize=4)\nax1.set_title(f'Loss vs. Epochs - {model_name}', fontsize=14)\nax1.set_xlabel('Epoch', fontsize=12)\nax1.set_ylabel('Loss', fontsize=12)\nax1.legend(fontsize=12)\nax1.grid(True, alpha=0.3)\n\n# Plot 2: Accuracies\nax2.plot(train_accs, label='Training Accuracy', color='green', linewidth=2, marker='o', markersize=4)\nax2.plot(valid_accs, label='Validation Accuracy', color='orange', linewidth=2, marker='s', markersize=4)\nax2.set_title(f'Accuracy vs. Epochs - {model_name}', fontsize=14)\nax2.set_xlabel('Epoch', fontsize=12)\nax2.set_ylabel('Accuracy', fontsize=12)\nax2.legend(fontsize=12)\nax2.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:06:21.471506Z","iopub.execute_input":"2026-01-16T19:06:21.471704Z","iopub.status.idle":"2026-01-16T19:06:21.788774Z","shell.execute_reply.started":"2026-01-16T19:06:21.471685Z","shell.execute_reply":"2026-01-16T19:06:21.788239Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Evaluasi Final dan Confusion Matrix","metadata":{}},{"cell_type":"code","source":"print(\"\\n\" + \"=\"*60)\nprint(f\"EVALUASI FINAL - {model_name}\")\nprint(\"=\"*60)\n\n# Get final evaluation with predictions\nfinal_loss, final_acc, all_predictions, all_labels = evaluate_cnn(\n    model, test_loader, criterion, device, return_predictions=True\n)\n\nprint(f\"\\nAkurasi Final pada Test Set: {final_acc*100:.2f}%\")\nprint(f\"Loss Final pada Test Set: {final_loss:.4f}\")\n\n# Convert to numpy arrays\ny_pred = np.array(all_predictions)\ny_true = np.array(all_labels)\n\n# Calculate confusion matrix\ncm = confusion_matrix(y_true, y_pred)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:06:21.789648Z","iopub.execute_input":"2026-01-16T19:06:21.789876Z","iopub.status.idle":"2026-01-16T19:06:22.007304Z","shell.execute_reply.started":"2026-01-16T19:06:21.789855Z","shell.execute_reply":"2026-01-16T19:06:22.006591Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Visualize Confusion Matrix","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(10, 8))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=True, \n            xticklabels=range(10), yticklabels=range(10),\n            annot_kws={\"size\": 10})\nplt.xlabel('Predicted Label', fontsize=12)\nplt.ylabel('True Label', fontsize=12)\nplt.title(f'Confusion Matrix - {model_name}', fontsize=14, pad=20)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:06:22.008251Z","iopub.execute_input":"2026-01-16T19:06:22.008509Z","iopub.status.idle":"2026-01-16T19:06:22.285874Z","shell.execute_reply.started":"2026-01-16T19:06:22.008488Z","shell.execute_reply":"2026-01-16T19:06:22.285335Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Confusion Matrix dengan Normalisasi","metadata":{}},{"cell_type":"code","source":"cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]\n\nplt.figure(figsize=(10, 8))\nsns.heatmap(cm_normalized, annot=True, fmt='.2f', cmap='OrRd', cbar=True,\n            xticklabels=range(10), yticklabels=range(10),\n            annot_kws={\"size\": 10})\nplt.xlabel('Predicted Label', fontsize=12)\nplt.ylabel('True Label', fontsize=12)\nplt.title(f'Normalized Confusion Matrix - {model_name}', fontsize=14, pad=20)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T19:06:22.286741Z","iopub.execute_input":"2026-01-16T19:06:22.286997Z","iopub.status.idle":"2026-01-16T19:06:22.573324Z","shell.execute_reply.started":"2026-01-16T19:06:22.286975Z","shell.execute_reply":"2026-01-16T19:06:22.572451Z"}},"outputs":[],"execution_count":null}]}