{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":3362,"databundleVersionId":31148,"sourceType":"competition"}],"dockerImageVersionId":31012,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import zipfile\nimport os\n\n# Path ke file ZIP\nzip_path = '/kaggle/input/dogs-vs-cats/train.zip'\nextract_to = '/kaggle/working'\n\n# Mengekstrak file ZIP\nwith zipfile.ZipFile(zip_path, 'r') as zip_ref:\n    zip_ref.extractall(extract_to)\n\nprint(\"Selesai ekstraksi.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:36:50.291063Z","iopub.execute_input":"2025-05-12T13:36:50.291611Z","iopub.status.idle":"2025-05-12T13:36:58.597731Z","shell.execute_reply.started":"2025-05-12T13:36:50.291583Z","shell.execute_reply":"2025-05-12T13:36:58.597133Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nfrom tqdm import tqdm\n\nsrc_dir = os.path.join(extract_to, 'train')\ndst_dir = os.path.join(extract_to, 'train_cleaned')\n\nos.makedirs(dst_dir, exist_ok=True)\n\n# Pindahkan semua file .jpg ke dst_dir\nfor filename in tqdm(os.listdir(src_dir)):\n    if filename.endswith('.jpg'):\n        shutil.move(os.path.join(src_dir, filename), os.path.join(dst_dir, filename))\n\nprint(\"Selesai merapikan file.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:36:58.599105Z","iopub.execute_input":"2025-05-12T13:36:58.599295Z","iopub.status.idle":"2025-05-12T13:36:59.273594Z","shell.execute_reply.started":"2025-05-12T13:36:58.599279Z","shell.execute_reply":"2025-05-12T13:36:59.272779Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 2. Pindahkan semua .jpg ke folder 'train_cleaned'\nsrc_dir = os.path.join(extract_to, 'train')  # hasil ekstrak\ndst_dir = os.path.join(extract_to, 'train_cleaned')  # tempat rapi\nos.makedirs(dst_dir, exist_ok=True)\n\nfor filename in tqdm(os.listdir(src_dir)):\n    if filename.endswith('.jpg'):\n        shutil.move(os.path.join(src_dir, filename), os.path.join(dst_dir, filename))\n\nprint(\"✅ Selesai memindahkan file gambar\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:36:59.274416Z","iopub.execute_input":"2025-05-12T13:36:59.274693Z","iopub.status.idle":"2025-05-12T13:36:59.282001Z","shell.execute_reply.started":"2025-05-12T13:36:59.274667Z","shell.execute_reply":"2025-05-12T13:36:59.281357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 3. Cek isi folder\nall_files = os.listdir(dst_dir)\ncat_files = [f for f in all_files if f.startswith('cat')]\ndog_files = [f for f in all_files if f.startswith('dog')]\n\nprint(f\"\\n📦 Total gambar: {len(all_files)}\")\nprint(f\"🐱 Kucing: {len(cat_files)}\")\nprint(f\"🐶 Anjing: {len(dog_files)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:36:59.282817Z","iopub.execute_input":"2025-05-12T13:36:59.283287Z","iopub.status.idle":"2025-05-12T13:36:59.307311Z","shell.execute_reply.started":"2025-05-12T13:36:59.283262Z","shell.execute_reply":"2025-05-12T13:36:59.306818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom PIL import Image\nimport random\n\n# Set path direktori yang benar\ntrain_dir = '/kaggle/working/train_cleaned'\n\n# Mendapatkan daftar file\ncat_files = [f for f in os.listdir(train_dir) if f.startswith('cat')]\ndog_files = [f for f in os.listdir(train_dir) if f.startswith('dog')]\n\n# Memilih 5 gambar secara acak dari masing-masing kelas\nsample_cats = random.sample(cat_files, 5)\nsample_dogs = random.sample(dog_files, 5)\n\n# Menampilkan gambar kucing\nplt.figure(figsize=(15, 3))\nfor i, file in enumerate(sample_cats):\n    img = Image.open(os.path.join(train_dir, file))\n    plt.subplot(1, 5, i + 1)\n    plt.imshow(img)\n    plt.title(\"Kucing\")\n    plt.axis('off')\nplt.suptitle(\"Gambar Sampel Kucing\", fontsize=16)\nplt.show()\n\n# Menampilkan gambar anjing\nplt.figure(figsize=(15, 3))\nfor i, file in enumerate(sample_dogs):\n    img = Image.open(os.path.join(train_dir, file))\n    plt.subplot(1, 5, i + 1)\n    plt.imshow(img)\n    plt.title(\"Anjing\")\n    plt.axis('off')\nplt.suptitle(\"Gambar Sampel Anjing\", fontsize=16)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:36:59.308834Z","iopub.execute_input":"2025-05-12T13:36:59.309052Z","iopub.status.idle":"2025-05-12T13:37:00.212228Z","shell.execute_reply.started":"2025-05-12T13:36:59.309038Z","shell.execute_reply":"2025-05-12T13:37:00.211649Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision import transforms\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:37:00.213099Z","iopub.execute_input":"2025-05-12T13:37:00.213306Z","iopub.status.idle":"2025-05-12T13:37:06.954834Z","shell.execute_reply.started":"2025-05-12T13:37:00.213289Z","shell.execute_reply":"2025-05-12T13:37:06.954242Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = 224","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:37:06.955495Z","iopub.execute_input":"2025-05-12T13:37:06.955750Z","iopub.status.idle":"2025-05-12T13:37:06.959264Z","shell.execute_reply.started":"2025-05-12T13:37:06.955733Z","shell.execute_reply":"2025-05-12T13:37:06.958575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Buat Transformasi Preprocessing\nresnet_transforms = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],  # ImageNet mean\n        std=[0.229, 0.224, 0.225]    # ImageNet std\n    )\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:37:06.960117Z","iopub.execute_input":"2025-05-12T13:37:06.960380Z","iopub.status.idle":"2025-05-12T13:37:06.991367Z","shell.execute_reply.started":"2025-05-12T13:37:06.960341Z","shell.execute_reply":"2025-05-12T13:37:06.990791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Dataset Kustom\nfrom torch.utils.data import random_split\n\nclass CatDogDataset(Dataset):\n    def __init__(self, image_dir, transform=None):\n        self.image_dir = image_dir\n        self.transform = transform\n        self.image_filenames = [f for f in os.listdir(image_dir) if f.endswith('.jpg')]\n\n    def __len__(self):\n        return len(self.image_filenames)\n\n    def __getitem__(self, idx):\n        img_name = self.image_filenames[idx]\n        img_path = os.path.join(self.image_dir, img_name)\n        image = Image.open(img_path).convert('RGB')\n\n        label = 0 if img_name.startswith('cat') else 1\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, label\n\n#Split Train/Validation\nfull_dataset = CatDogDataset(train_dir, transform=resnet_transforms)\n\n# Split: 80% train, 20% validation\ntrain_size = int(0.8 * len(full_dataset))\nval_size = len(full_dataset) - train_size\n\ntrain_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:37:06.992034Z","iopub.execute_input":"2025-05-12T13:37:06.992257Z","iopub.status.idle":"2025-05-12T13:37:07.033506Z","shell.execute_reply.started":"2025-05-12T13:37:06.992240Z","shell.execute_reply":"2025-05-12T13:37:07.033001Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#DataLoader\ndataset = CatDogDataset(train_dir, transform=resnet_transforms)\ndataloader = DataLoader(dataset, batch_size=32, shuffle=True)\n\ntrain_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:37:07.034327Z","iopub.execute_input":"2025-05-12T13:37:07.034711Z","iopub.status.idle":"2025-05-12T13:37:07.052813Z","shell.execute_reply.started":"2025-05-12T13:37:07.034694Z","shell.execute_reply":"2025-05-12T13:37:07.052330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Sampel Training: {len(train_dataset)}\")\nprint(f\"Sampel Validation: {len(val_dataset)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:37:07.053408Z","iopub.execute_input":"2025-05-12T13:37:07.053617Z","iopub.status.idle":"2025-05-12T13:37:07.057449Z","shell.execute_reply.started":"2025-05-12T13:37:07.053600Z","shell.execute_reply":"2025-05-12T13:37:07.056852Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from collections import Counter\n\n# Mengambil semua label dari dataset pelatihan\ntrain_labels = [label for _, label in train_dataset]\nlabel_counts = Counter(train_labels)\n\n# Menampilkan distribusi label\nprint(\"Distribusi label dalam dataset pelatihan:\")\nprint(f\"Kucing (label 0): {label_counts[0]}\")\nprint(f\"Anjing (label 1): {label_counts[1]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:37:07.058043Z","iopub.execute_input":"2025-05-12T13:37:07.058264Z","iopub.status.idle":"2025-05-12T13:38:01.999131Z","shell.execute_reply.started":"2025-05-12T13:37:07.058239Z","shell.execute_reply":"2025-05-12T13:38:01.998408Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Fungsi untuk menampilkan gambar hasil transformasi dari dataset\ndef show_transformed_images(dataset, num_images=5):\n    plt.figure(figsize=(15, 3))\n    for i in range(num_images):\n        idx = random.randint(0, len(dataset) - 1)\n        img_tensor, label = dataset[idx]\n        \n        # Mengubah tensor menjadi gambar numpy untuk ditampilkan\n        img = img_tensor.permute(1, 2, 0).numpy()\n        img = img * [0.229, 0.224, 0.225] + [0.485, 0.456, 0.406]  # Melakukan unnormalisasi\n        img = img.clip(0, 1)\n        \n        plt.subplot(1, num_images, i + 1)\n        plt.imshow(img)\n        plt.title(\"Anjing\" if label == 1 else \"Kucing\")\n        plt.axis('off')\n    plt.suptitle(\"Gambar Sampel Hasil Transformasi dari Dataset Pelatihan\", fontsize=16)\n    plt.show()\n\n# Menampilkan gambar\nshow_transformed_images(train_dataset)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:38:02.000440Z","iopub.execute_input":"2025-05-12T13:38:02.000651Z","iopub.status.idle":"2025-05-12T13:38:02.430214Z","shell.execute_reply.started":"2025-05-12T13:38:02.000635Z","shell.execute_reply":"2025-05-12T13:38:02.429576Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# VGG16 (metode utama)","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torchvision.models as models\nfrom torch.optim import Adam\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nfrom torch.nn import BCEWithLogitsLoss\nfrom tqdm import tqdm\n\n# Memuat model VGG16 dengan bobot pretrained\nmodel = models.vgg16(pretrained=True)\n\n# Membekukan semua layer fitur VGG16\nfor param in model.features.parameters():\n    param.requires_grad = False\n\n# Memodifikasi classifier untuk output biner\nmodel.classifier[6] = nn.Linear(model.classifier[6].in_features, 1)\n\n# Mengatur device (GPU atau CPU)\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\nmodel = model.to(device)\n\n# Loss, optimizer, scheduler\ncriterion = BCEWithLogitsLoss()\noptimizer = Adam(model.parameters(), lr=1e-4)\nscheduler = ReduceLROnPlateau(optimizer, mode='min', patience=2, factor=0.5, verbose=True)\n\n# Early stopping\nbest_val_loss = float('inf')\npatience_counter = 0\nearly_stop_patience = 2\n\n# Menyimpan history\ntrain_losses, val_losses = [], []\ntrain_accuracies, val_accuracies = [], []\n\nEPOCHS = 7\n\n# Loop pelatihan\nfor epoch in range(EPOCHS):\n    model.train()\n    running_loss, correct, total = 0, 0, 0\n    \n    for images, labels in tqdm(train_loader):\n        images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item()\n        preds = torch.sigmoid(outputs) > 0.5\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n\n    train_loss = running_loss / len(train_loader)\n    train_acc = correct / total\n    train_losses.append(train_loss)\n    train_accuracies.append(train_acc)\n\n    # Validasi\n    model.eval()\n    val_loss_total, correct, total = 0, 0, 0\n\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            val_loss_total += loss.item()\n            preds = torch.sigmoid(outputs) > 0.5\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n\n    val_loss = val_loss_total / len(val_loader)\n    val_acc = correct / total\n    val_losses.append(val_loss)\n    val_accuracies.append(val_acc)\n\n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f} | Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}\")\n    \n    scheduler.step(val_loss)\n\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        patience_counter = 0\n        torch.save(model.state_dict(), 'best_vgg16_model.pt')\n    else:\n        patience_counter += 1\n        if patience_counter >= early_stop_patience:\n            print(f\"Early stopping triggered at epoch {epoch+1}\")\n            break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:38:02.433190Z","iopub.execute_input":"2025-05-12T13:38:02.433514Z","iopub.status.idle":"2025-05-12T13:50:35.940871Z","shell.execute_reply.started":"2025-05-12T13:38:02.433494Z","shell.execute_reply":"2025-05-12T13:50:35.940090Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\n# Menyimpan model terbaik yang sudah dilatih\nmodel.load_state_dict(torch.load('best_vgg16_model.pt'))  # Memuat model terbaik\nmodel.eval()  # Mode evaluasi\n\nall_labels = []\nall_preds = []\n\n# Evaluasi pada data validasi\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n        outputs = model(images)\n        preds = torch.sigmoid(outputs) > 0.5  # Menentukan prediksi biner (0 atau 1)\n\n        all_labels.extend(labels.cpu().numpy())  # Menyimpan semua label asli\n        all_preds.extend(preds.cpu().numpy())  # Menyimpan semua prediksi\n\n# Mengubah list menjadi array untuk evaluasi\nall_labels = [int(x[0]) for x in all_labels]\nall_preds = [int(x[0]) for x in all_preds]\n\n# Menghitung metrik evaluasi\nacc = accuracy_score(all_labels, all_preds)\nprec = precision_score(all_labels, all_preds)\nrec = recall_score(all_labels, all_preds)\nf1 = f1_score(all_labels, all_preds)\ncm = confusion_matrix(all_labels, all_preds)\n\n# Menampilkan hasil evaluasi\nprint(f\"\\nEvaluasi Model VGG16 pada Validation Set:\")\nprint(f\"Akurasi     : {acc:.4f}\")\nprint(f\"Presisi     : {prec:.4f}\")\nprint(f\"Recall      : {rec:.4f}\")\nprint(f\"F1 Score    : {f1:.4f}\")\nprint(\"\\nClassification Report:\\n\")\nprint(classification_report(all_labels, all_preds))\n\n# Menghitung confusion matrix\ncm = confusion_matrix(all_labels, all_preds)\n\n# Menampilkan heatmap confusion matrix\nplt.figure(figsize=(5, 4))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=['Kucing', 'Anjing'],\n            yticklabels=['Kucing', 'Anjing'])\nplt.xlabel('Predicted Label')\nplt.ylabel('True Label')\nplt.title('Confusion Matrix - VGG16')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:50:35.941853Z","iopub.execute_input":"2025-05-12T13:50:35.942159Z","iopub.status.idle":"2025-05-12T13:51:22.556258Z","shell.execute_reply.started":"2025-05-12T13:50:35.942135Z","shell.execute_reply":"2025-05-12T13:51:22.555420Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# ResNet50 (metode pembanding 1)","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torchvision.models as models\nfrom torch.optim import Adam\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nfrom torch.nn import BCEWithLogitsLoss\nfrom tqdm import tqdm  \n\n# Memuat model ResNet-50 dengan bobot yang telah dilatih sebelumnya (pretrained)\nmodel = models.resnet50(pretrained=True)\n\n# Membekukan semua layer dari model ResNet-50, kecuali layer klasifikasi terakhir\nfor param in model.parameters():\n    param.requires_grad = False  \n\n# Memodifikasi layer klasifikasi akhir untuk klasifikasi biner (Kucing vs Anjing)\nmodel.fc = nn.Linear(model.fc.in_features, 1)\n\n# Memindahkan model ke GPU jika tersedia, jika tidak maka gunakan CPU\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\nmodel = model.to(device)\n\n# Fungsi loss, optimizer, dan scheduler untuk learning rate\ncriterion = BCEWithLogitsLoss()  # Digunakan untuk klasifikasi biner (karena output belum melalui sigmoid)\noptimizer = Adam(model.parameters(), lr=1e-4)  # Optimizer Adam dengan learning rate 0.0001\nscheduler = ReduceLROnPlateau(optimizer, mode='min', patience=2, factor=0.5, verbose=True)\n# Scheduler akan menurunkan learning rate jika loss validasi tidak membaik selama 2 epoch (patience)\n\n# Pengaturan untuk Early Stopping\nbest_val_loss = float('inf')           # Menyimpan nilai loss validasi terbaik\npatience_counter = 0                   # Menghitung jumlah epoch tanpa perbaikan\nearly_stop_patience = 2               # Hentikan pelatihan jika tidak ada perbaikan selama 2 epoch berturut-turut\n\n# Menyimpan riwayat loss untuk pelatihan dan validasi\ntrain_losses, val_losses = [], []\n\n# Inisialisasi variabel untuk menyimpan metrik pelatihan dan validasi\ntrain_losses = []\ntrain_accuracies = []\nval_losses = []\nval_accuracies = []\n\n# Jumlah epoch untuk pelatihan\nEPOCHS = 7\nbest_val_loss = float('inf')  # Menyimpan loss terbaik selama validasi\npatience_counter = 0  # Untuk early stopping\nearly_stop_patience = 2  \n\n# Loop pelatihan\nfor epoch in range(EPOCHS):\n    model.train()  # Mengatur model ke mode pelatihan\n    running_loss, correct, total = 0, 0, 0\n    \n    for images, labels in tqdm(train_loader):   # Iterasi batch dalam data pelatihan\n        images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n         # .float(): karena kita pakai BCEWithLogitsLoss yang butuh tipe float\n        # .unsqueeze(1): memastikan label berbentuk [batch_size, 1] untuk dicocokkan dengan output model\n        \n        optimizer.zero_grad()  # Mengatur ulang gradien ke nol\n        outputs = model(images)  # Forward pass\n        loss = criterion(outputs, labels)  # Hitung loss\n        loss.backward()  # Backpropagation\n        optimizer.step()  # Perbarui bobot\n\n        running_loss += loss.item()  # Menjumlahkan total loss untuk 1 epoch\n        preds = torch.sigmoid(outputs) > 0.5  # Konversi output ke prediksi (threshold 0.5)\n        correct += (preds == labels).sum().item()  # Hitung prediksi yang benar\n        total += labels.size(0)  # Jumlah total sampel\n\n    # Menghitung rata-rata loss dan akurasi pelatihan\n    train_loss = running_loss / len(train_loader)\n    train_acc = correct / total\n    train_losses.append(train_loss)\n    train_accuracies.append(train_acc)\n\n    # Loop validasi\n    model.eval()  # Mengatur model ke mode evaluasi\n    val_loss_total, correct, total = 0, 0, 0\n\n    with torch.no_grad():  # Menonaktifkan perhitungan gradien\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n            outputs = model(images)  # Forward pass\n            loss = criterion(outputs, labels)  # Hitung loss\n\n            val_loss_total += loss.item()  # Menambahkan total loss validasi\n            preds = torch.sigmoid(outputs) > 0.5  # Konversi ke prediksi biner\n            correct += (preds == labels).sum().item()  # Hitung prediksi yang benar\n            total += labels.size(0)  # Jumlah total sampel\n\n    # Menghitung rata-rata loss dan akurasi validasi\n    val_loss = val_loss_total / len(val_loader)\n    val_acc = correct / total\n    val_losses.append(val_loss)\n    val_accuracies.append(val_acc)\n\n    # Menampilkan progres untuk setiap epoch\n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f} | Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}\")\n    \n    # Meng-update scheduler\n    scheduler.step(val_loss)\n\n    # Cek untuk early stopping\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        patience_counter = 0\n        torch.save(model.state_dict(), 'best_resnet50_model.pt')   # Menyimpan model terbaik sejauh ini\n    else:\n        patience_counter += 1\n        if patience_counter >= early_stop_patience:\n            print(f\"Early stopping triggered at epoch {epoch+1}\")\n            break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T13:51:22.557157Z","iopub.execute_input":"2025-05-12T13:51:22.557568Z","iopub.status.idle":"2025-05-12T14:09:24.066197Z","shell.execute_reply.started":"2025-05-12T13:51:22.557545Z","shell.execute_reply":"2025-05-12T14:09:24.065456Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\n# Menyiapkan model dan data untuk evaluasi\nmodel.load_state_dict(torch.load('best_resnet50_model.pt'))  # Memuat model terbaik\nmodel.eval()  # Mode evaluasi\n\nall_labels = []\nall_preds = []\n\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images = images.to(device)\n        labels = labels.to(device).float().unsqueeze(1)\n        outputs = model(images)\n        preds = torch.sigmoid(outputs) > 0.5\n\n        all_labels.extend(labels.cpu().numpy())\n        all_preds.extend(preds.cpu().numpy())\n\n# Konversi list ke array untuk evaluasi\nall_labels = [int(x[0]) for x in all_labels]\nall_preds = [int(x[0]) for x in all_preds]\n\n# Menghitung metrik evaluasi\nacc = accuracy_score(all_labels, all_preds)\nprec = precision_score(all_labels, all_preds)\nrec = recall_score(all_labels, all_preds)\nf1 = f1_score(all_labels, all_preds)\ncm = confusion_matrix(all_labels, all_preds)\n\n# Tampilkan metrik\nprint(f\"\\nEvaluasi Model ResNet50 pada Validation Set:\")\nprint(f\"Akurasi     : {acc:.4f}\")\nprint(f\"Presisi     : {prec:.4f}\")\nprint(f\"Recall      : {rec:.4f}\")\nprint(f\"F1 Score    : {f1:.4f}\")\nprint(\"\\nClassification Report:\\n\")\nprint(classification_report(all_labels, all_preds))\n\n# Menghitung confusion matrix\ncm = confusion_matrix(all_labels, all_preds)\n\n# Menampilkan heatmap confusion matrix\nplt.figure(figsize=(5, 4))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Reds',\n            xticklabels=['Kucing', 'Anjing'],\n            yticklabels=['Kucing', 'Anjing'])\nplt.xlabel('Predicted Label')\nplt.ylabel('True Label')\nplt.title('Confusion Matrix - ResNet50')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T14:09:24.067088Z","iopub.execute_input":"2025-05-12T14:09:24.067361Z","iopub.status.idle":"2025-05-12T14:09:53.786091Z","shell.execute_reply.started":"2025-05-12T14:09:24.067317Z","shell.execute_reply":"2025-05-12T14:09:53.785394Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# AlexNet (metode pembanding 2)","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torchvision.models as models\nfrom torch.optim import Adam\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nfrom torch.nn import BCEWithLogitsLoss\nfrom tqdm import tqdm\n\n# Memuat model AlexNet dengan bobot pretrained\nmodel = models.alexnet(pretrained=True)\n\n# Membekukan semua parameter fitur AlexNet\nfor param in model.features.parameters():\n    param.requires_grad = False\n\n# Memodifikasi classifier terakhir untuk output 1 neuron (klasifikasi biner)\nmodel.classifier[6] = nn.Linear(model.classifier[6].in_features, 1)\n\n# Memindahkan model ke GPU jika tersedia\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\nmodel = model.to(device)\n\n# Loss function, optimizer, dan scheduler\ncriterion = BCEWithLogitsLoss()\noptimizer = Adam(model.parameters(), lr=1e-4)\nscheduler = ReduceLROnPlateau(optimizer, mode='min', patience=2, factor=0.5, verbose=True)\n\n# Inisialisasi variabel untuk early stopping\nbest_val_loss = float('inf')\npatience_counter = 0\nearly_stop_patience = 2\n\n# History loss dan akurasi\ntrain_losses, val_losses = [], []\ntrain_accuracies, val_accuracies = [], []\n\n# Jumlah epoch\nEPOCHS = 7\n\n# Training loop\nfor epoch in range(EPOCHS):\n    model.train()\n    running_loss, correct, total = 0, 0, 0\n    \n    for images, labels in tqdm(train_loader):\n        images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item()\n        preds = torch.sigmoid(outputs) > 0.5\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n\n    train_loss = running_loss / len(train_loader)\n    train_acc = correct / total\n    train_losses.append(train_loss)\n    train_accuracies.append(train_acc)\n\n    # Validasi\n    model.eval()\n    val_loss_total, correct, total = 0, 0, 0\n\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            val_loss_total += loss.item()\n            preds = torch.sigmoid(outputs) > 0.5\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n\n    val_loss = val_loss_total / len(val_loader)\n    val_acc = correct / total\n    val_losses.append(val_loss)\n    val_accuracies.append(val_acc)\n\n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f} | Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}\")\n    \n    scheduler.step(val_loss)\n\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        patience_counter = 0\n        torch.save(model.state_dict(), 'best_alexnet_model.pt')\n    else:\n        patience_counter += 1\n        if patience_counter >= early_stop_patience:\n            print(f\"Early stopping triggered at epoch {epoch+1}\")\n            break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T14:09:53.786869Z","iopub.execute_input":"2025-05-12T14:09:53.787325Z","iopub.status.idle":"2025-05-12T14:16:33.771866Z","shell.execute_reply.started":"2025-05-12T14:09:53.787307Z","shell.execute_reply":"2025-05-12T14:16:33.771142Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\n# Menyimpan model terbaik yang sudah dilatih\nmodel.load_state_dict(torch.load('best_alexnet_model.pt'))  # Memuat model terbaik\nmodel.eval()  # Mode evaluasi\n\nall_labels = []\nall_preds = []\n\n# Evaluasi pada data validasi\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n        outputs = model(images)\n        preds = torch.sigmoid(outputs) > 0.5  # Menentukan prediksi biner (0 atau 1)\n\n        all_labels.extend(labels.cpu().numpy())  # Menyimpan semua label asli\n        all_preds.extend(preds.cpu().numpy())  # Menyimpan semua prediksi\n\n# Mengubah list menjadi array untuk evaluasi\nall_labels = [int(x[0]) for x in all_labels]\nall_preds = [int(x[0]) for x in all_preds]\n\n# Menghitung metrik evaluasi\nacc = accuracy_score(all_labels, all_preds)\nprec = precision_score(all_labels, all_preds)\nrec = recall_score(all_labels, all_preds)\nf1 = f1_score(all_labels, all_preds)\ncm = confusion_matrix(all_labels, all_preds)\n\n# Menampilkan hasil evaluasi\nprint(f\"\\nEvaluasi Model AlexNet pada Validation Set:\")\nprint(f\"Akurasi     : {acc:.4f}\")\nprint(f\"Presisi     : {prec:.4f}\")\nprint(f\"Recall      : {rec:.4f}\")\nprint(f\"F1 Score    : {f1:.4f}\")\nprint(\"\\nClassification Report:\\n\")\nprint(classification_report(all_labels, all_preds))\n\n# Menghitung confusion matrix\ncm = confusion_matrix(all_labels, all_preds)\n\n# Menampilkan heatmap confusion matrix\nplt.figure(figsize=(5, 4))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Greens',\n            xticklabels=['Kucing', 'Anjing'],\n            yticklabels=['Kucing', 'Anjing'])\nplt.xlabel('Predicted Label')\nplt.ylabel('True Label')\nplt.title('Confusion Matrix - AlexNet')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T14:16:33.772678Z","iopub.execute_input":"2025-05-12T14:16:33.772916Z","iopub.status.idle":"2025-05-12T14:16:50.801188Z","shell.execute_reply.started":"2025-05-12T14:16:33.772889Z","shell.execute_reply":"2025-05-12T14:16:50.800578Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LeNet (metode pembanding 3)","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nfrom torch.nn import BCEWithLogitsLoss\nfrom tqdm import tqdm\n\n# Mendefinisikan arsitektur LeNet untuk klasifikasi biner\nclass LeNet(nn.Module):\n    def __init__(self):\n        super(LeNet, self).__init__()\n        \n        self.conv1 = nn.Conv2d(3, 6, kernel_size=5)  # Conv layer 1\n        self.pool = nn.MaxPool2d(2, 2)  # Maxpooling\n        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)  # Conv layer 2\n        self.fc1 = nn.Linear(16 * 53 * 53, 120)  # Fully connected layer 1 (adjusted for image size)\n        self.fc2 = nn.Linear(120, 84)  # Fully connected layer 2\n        self.fc3 = nn.Linear(84, 1)  # Output layer for binary classification\n\n    def forward(self, x):\n        x = self.pool(torch.relu(self.conv1(x)))  # Apply conv1 + ReLU + maxpooling\n        x = self.pool(torch.relu(self.conv2(x)))  # Apply conv2 + ReLU + maxpooling\n        x = x.view(-1, 16 * 53 * 53)  # Flattening output for fully connected layers\n        x = torch.relu(self.fc1(x))  # Apply fully connected layer 1 + ReLU\n        x = torch.relu(self.fc2(x))  # Apply fully connected layer 2 + ReLU\n        x = self.fc3(x)  # Output layer\n        return x\n\n# Instansiasi model LeNet\nmodel = LeNet()\n\n# Memindahkan model ke GPU jika tersedia, jika tidak maka gunakan CPU\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\nmodel = model.to(device)\n\n# Loss function dan optimizer\ncriterion = BCEWithLogitsLoss()  # Digunakan untuk klasifikasi biner\noptimizer = optim.Adam(model.parameters(), lr=1e-4)  # Optimizer Adam dengan learning rate 0.0001\nscheduler = ReduceLROnPlateau(optimizer, mode='min', patience=2, factor=0.5, verbose=True)\n\n# Early stopping\nbest_val_loss = float('inf')\npatience_counter = 0\nearly_stop_patience = 2\n\n# Menyimpan riwayat loss\ntrain_losses, val_losses = [], []\ntrain_accuracies, val_accuracies = [], []\n\nEPOCHS = 7\n\n# Loop pelatihan\nfor epoch in range(EPOCHS):\n    model.train()  # Set model ke mode pelatihan\n    running_loss, correct, total = 0, 0, 0\n    \n    for images, labels in tqdm(train_loader):\n        images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n        \n        optimizer.zero_grad()  # Mengatur ulang gradien\n        outputs = model(images)  # Forward pass\n        loss = criterion(outputs, labels)  # Hitung loss\n        loss.backward()  # Backpropagation\n        optimizer.step()  # Update bobot\n\n        running_loss += loss.item()  # Menjumlahkan total loss\n        preds = torch.sigmoid(outputs) > 0.5  # Mengubah output menjadi prediksi biner\n        correct += (preds == labels).sum().item()  # Hitung jumlah prediksi yang benar\n        total += labels.size(0)  # Hitung total jumlah sampel\n\n    # Menghitung rata-rata loss dan akurasi untuk pelatihan\n    train_loss = running_loss / len(train_loader)\n    train_acc = correct / total\n    train_losses.append(train_loss)\n    train_accuracies.append(train_acc)\n\n    # Validasi\n    model.eval()  # Set model ke mode evaluasi\n    val_loss_total, correct, total = 0, 0, 0\n\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n            outputs = model(images)  # Forward pass\n            loss = criterion(outputs, labels)  # Hitung loss\n\n            val_loss_total += loss.item()  # Tambahkan loss validasi\n            preds = torch.sigmoid(outputs) > 0.5  # Mengubah output menjadi prediksi biner\n            correct += (preds == labels).sum().item()  # Hitung jumlah prediksi yang benar\n            total += labels.size(0)  # Hitung total jumlah sampel\n\n    # Menghitung rata-rata loss dan akurasi untuk validasi\n    val_loss = val_loss_total / len(val_loader)\n    val_acc = correct / total\n    val_losses.append(val_loss)\n    val_accuracies.append(val_acc)\n\n    # Menampilkan progres untuk setiap epoch\n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f} | Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}\")\n    \n    # Meng-update scheduler\n    scheduler.step(val_loss)\n\n    # Cek untuk early stopping\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        patience_counter = 0\n        torch.save(model.state_dict(), 'best_lenet_model.pt')  # Menyimpan model terbaik\n    else:\n        patience_counter += 1\n        if patience_counter >= early_stop_patience:\n            print(f\"Early stopping triggered at epoch {epoch+1}\")\n            break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T14:16:50.801915Z","iopub.execute_input":"2025-05-12T14:16:50.802145Z","iopub.status.idle":"2025-05-12T14:26:12.887950Z","shell.execute_reply.started":"2025-05-12T14:16:50.802126Z","shell.execute_reply":"2025-05-12T14:26:12.887297Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\n# Menyimpan model terbaik yang sudah dilatih\nmodel.load_state_dict(torch.load('best_lenet_model.pt'))  # Memuat model terbaik\nmodel.eval()  # Mode evaluasi\n\nall_labels = []\nall_preds = []\n\n# Evaluasi pada data validasi\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images, labels = images.to(device), labels.to(device).float().unsqueeze(1)\n        outputs = model(images)\n        preds = torch.sigmoid(outputs) > 0.5  # Menentukan prediksi biner (0 atau 1)\n\n        all_labels.extend(labels.cpu().numpy())  # Menyimpan semua label asli\n        all_preds.extend(preds.cpu().numpy())  # Menyimpan semua prediksi\n\n# Mengubah list menjadi array untuk evaluasi\nall_labels = [int(x[0]) for x in all_labels]\nall_preds = [int(x[0]) for x in all_preds]\n\n# Menghitung metrik evaluasi\nacc = accuracy_score(all_labels, all_preds)\nprec = precision_score(all_labels, all_preds)\nrec = recall_score(all_labels, all_preds)\nf1 = f1_score(all_labels, all_preds)\ncm = confusion_matrix(all_labels, all_preds)\n\n# Menampilkan hasil evaluasi\nprint(f\"\\nEvaluasi Model LeNet pada Validation Set:\")\nprint(f\"Akurasi     : {acc:.4f}\")\nprint(f\"Presisi     : {prec:.4f}\")\nprint(f\"Recall      : {rec:.4f}\")\nprint(f\"F1 Score    : {f1:.4f}\")\nprint(\"\\nClassification Report:\\n\")\nprint(classification_report(all_labels, all_preds))\n\n# Menghitung confusion matrix\ncm = confusion_matrix(all_labels, all_preds)\n\n# Menampilkan heatmap confusion matrix\nplt.figure(figsize=(5, 4))\nsns.heatmap(cm, annot=True, fmt='d', cmap='seismic',\n            xticklabels=['Kucing', 'Anjing'],\n            yticklabels=['Kucing', 'Anjing'])\nplt.xlabel('Predicted Label')\nplt.ylabel('True Label')\nplt.title('Confusion Matrix - LeNet')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T14:26:12.888720Z","iopub.execute_input":"2025-05-12T14:26:12.888925Z","iopub.status.idle":"2025-05-12T14:26:29.825782Z","shell.execute_reply.started":"2025-05-12T14:26:12.888909Z","shell.execute_reply":"2025-05-12T14:26:29.825097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\n\n# Data\nmodels = ['VGG16', 'ResNet50', 'AlexNet', 'LeNet']\nakurasi = [0.9874, 0.9854, 0.9642, 0.7604]\npresisi = [0.9907, 0.9844, 0.9711, 0.7258]\nrecall = [0.9839, 0.9864, 0.9566, 0.8342]\nf1_score = [0.9873, 0.9854, 0.9638, 0.7762]\n\n# Konversi ke persen\nakurasi = [x * 100 for x in akurasi]\npresisi = [x * 100 for x in presisi]\nrecall = [x * 100 for x in recall]\nf1_score = [x * 100 for x in f1_score]\n\n# Posisi bar\nx = np.arange(len(models))\nwidth = 0.2\n\n# Plot\nfig, ax = plt.subplots(figsize=(10, 6))\nbars1 = ax.bar(x - 1.5*width, akurasi, width, label='Akurasi')\nbars2 = ax.bar(x - 0.5*width, presisi, width, label='Presisi')\nbars3 = ax.bar(x + 0.5*width, recall, width, label='Recall')\nbars4 = ax.bar(x + 1.5*width, f1_score, width, label='F1-Score')\n\n# Menambahkan angka pada tiap bar\ndef add_labels(bars):\n    for bar in bars:\n        yval = bar.get_height()\n        ax.text(bar.get_x() + bar.get_width()/2.0, yval + 1, f'{yval:.1f}%', \n                ha='center', va='bottom', fontsize=9)\n\nadd_labels(bars1)\nadd_labels(bars2)\nadd_labels(bars3)\nadd_labels(bars4)\n\n# Label dan tampilan\nax.set_ylabel('Nilai (%)')\nax.set_title('Perbandingan Internal Performa Model CNN')\nax.set_xticks(x)\nax.set_xticklabels(models)\nax.set_ylim(0, 110)\nax.legend()\nax.grid(axis='y', linestyle='--', alpha=0.7)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T14:39:18.003391Z","iopub.execute_input":"2025-05-12T14:39:18.003935Z","iopub.status.idle":"2025-05-12T14:39:18.226126Z","shell.execute_reply.started":"2025-05-12T14:39:18.003914Z","shell.execute_reply":"2025-05-12T14:39:18.225276Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Data model dan akurasi\nmodels = [\n    \"32-layer CNN (Ikan)\",\n    \"DenseNet201 (Pneumonia)\",\n    \"AlexNet (Brain Tumor)\",\n    \"ResNet50 (Daun Kentang)\",\n    \"VGG-16 (Kerbau)\",\n    \"ResNet50 (Kerbau)\"\n]\n\naccuracy = [96.63, 98.00, 96.10, 97.00, 95.00, 80.00]\n\n# Plot\nplt.figure(figsize=(12, 6))\nbars = plt.bar(models, accuracy, color='cornflowerblue')\nplt.ylabel(\"Akurasi (%)\")\nplt.title(\"Perbandingan Eksternal Akurasi Model CNN\")\nplt.xticks(rotation=45, ha=\"right\")\nplt.ylim(0, 100)\n\n# Tambahkan label di dalam bar\nfor bar in bars:\n    yval = bar.get_height()\n    plt.text(bar.get_x() + bar.get_width()/2.0, yval - 5, f'{yval:.2f}%', \n             ha='center', va='center', color='white', fontsize=9, fontstyle='italic')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-12T14:56:26.937047Z","iopub.execute_input":"2025-05-12T14:56:26.937512Z","iopub.status.idle":"2025-05-12T14:56:27.118199Z","shell.execute_reply.started":"2025-05-12T14:56:26.937491Z","shell.execute_reply":"2025-05-12T14:56:27.117520Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Interpretasi Hasil:\nVGG16 menunjukkan performa terbaik secara keseluruhan, dengan akurasi, presisi, recall, dan F1 score paling tinggi. Model ini sangat baik dalam menangani klasifikasi biner dengan dataset gambar yang cukup kompleks, berkat arsitekturnya yang dalam dan terstruktur.\n\nResNet50 berada di posisi kedua, dengan performa yang hampir setara dengan VGG16. Arsitektur residual-nya memungkinkan pelatihan lebih dalam tanpa mengalami degradasi performa, namun pada kasus ini sedikit tertinggal dibandingkan VGG16.\n\nAlexNet memiliki performa yang cukup baik, meskipun masih di bawah VGG16 dan ResNet50. Ini dapat dimengerti karena AlexNet adalah arsitektur yang lebih tua dan lebih sederhana, yang pertama kali populer pada tahun 2012.\n\nLeNet menunjukkan performa terendah. Ini wajar karena LeNet didesain untuk tugas yang jauh lebih sederhana. Akurasi dan F1 score-nya tertinggal jauh, sehingga kurang cocok untuk dataset gambar modern dan kompleks.","metadata":{}}]}