{"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":[{"sourceType":"competition","sourceId":31254,"databundleVersionId":3103714}],"dockerImageVersionId":31328,"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-04-27T05:43:23.109732Z","iopub.execute_input":"2026-04-27T05:43:23.109981Z","iopub.status.idle":"2026-04-27T05:47:55.907742Z","shell.execute_reply.started":"2026-04-27T05:43:23.10996Z","shell.execute_reply":"2026-04-27T05:47:55.90689Z"},"_kg_hide-input":false},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install timm faiss-cpu -q\n\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\nfrom PIL import Image\nfrom tqdm import tqdm\nimport time\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as transforms\nimport torchvision.models as models\nimport timm\nimport faiss\n\n# Pastikan GPU menyala\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'✅ Device: {device}')\n\n# Konfigurasi\nIMG_SIZE = 224\nBATCH_SIZE = 32\nNUM_EPOCHS = 50\nLR = 1e-4\nMARGIN = 0.3\nEMBED_DIM = 128\nMAX_IMAGES = 10000 \nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\n\nprint('⏳ Mencari dan menyiapkan 10.000 gambar dari server Kaggle...')\nall_images = []\n\n# Taktik Bulldozer: Cari gambar .jpg di mana pun mereka bersembunyi di dalam /kaggle/input\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        if filename.endswith('.jpg'):\n            all_images.append(os.path.join(dirname, filename))\n            # Ambil sampel lebih banyak sedikit agar data variatif\n            if len(all_images) > MAX_IMAGES * 2: \n                break\n    if len(all_images) > MAX_IMAGES * 2: \n        break\n\nif len(all_images) == 0:\n    print(\"🚨 ERROR FATAL: Gambar tetap tidak ditemukan. Pastikan dataset H&M sudah di-Add ke notebook Kaggle ini!\")\nelse:\n    print(f\"Ketemu! Berhasil mengamankan {len(all_images)} gambar mentah.\")\n\n# Ambil subset 10.000 gambar acak\nall_images = random.sample(all_images, min(MAX_IMAGES, len(all_images)))\n\n# Buat label (berdasarkan nama sub-folder tempat gambar berada)\nlabels = [os.path.basename(os.path.dirname(p)) for p in all_images]\nunique_labels = list(set(labels))\nlabel2idx = {l: i for i, l in enumerate(unique_labels)}\nlabel_ids = [label2idx[l] for l in labels]\n\n# Split dataset\nfrom sklearn.model_selection import train_test_split\nindices = list(range(len(all_images)))\ntrain_idx, temp_idx = train_test_split(indices, test_size=0.3, random_state=SEED)\nval_idx, test_idx   = train_test_split(temp_idx, test_size=0.5, random_state=SEED)\n\nprint(f'✅ Dataset siap! Train: {len(train_idx)} | Val: {len(val_idx)} | Test: {len(test_idx)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-27T05:47:55.909309Z","iopub.execute_input":"2026-04-27T05:47:55.909811Z","iopub.status.idle":"2026-04-27T05:48:43.287838Z","shell.execute_reply.started":"2026-04-27T05:47:55.909784Z","shell.execute_reply":"2026-04-27T05:48:43.287095Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Augmentasi untuk training\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(degrees=15),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# Tanpa augmentasi untuk validasi & test\neval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nclass TripletFashionDataset(Dataset):\n    def __init__(self, image_paths, label_ids, transform=None):\n        self.image_paths = image_paths\n        self.label_ids = label_ids\n        self.transform = transform\n        self.label_to_indices = {}\n        for idx, label in enumerate(label_ids):\n            if label not in self.label_to_indices:\n                self.label_to_indices[label] = []\n            self.label_to_indices[label].append(idx)\n        self.valid_labels = [l for l, idxs in self.label_to_indices.items() if len(idxs) >= 2]\n\n    def __len__(self): return len(self.image_paths)\n\n    def load_image(self, path):\n        try:\n            img = Image.open(path).convert('RGB')\n            if self.transform: img = self.transform(img)\n            return img\n        except:\n            return torch.zeros(3, IMG_SIZE, IMG_SIZE)\n\n    def __getitem__(self, idx):\n        anchor_label = self.label_ids[idx]\n        if anchor_label not in self.label_to_indices or len(self.label_to_indices[anchor_label]) < 2:\n            anchor_label = random.choice(self.valid_labels)\n            idx = random.choice(self.label_to_indices[anchor_label])\n\n        pos_idx = idx\n        while pos_idx == idx: pos_idx = random.choice(self.label_to_indices[anchor_label])\n\n        neg_label = anchor_label\n        while neg_label == anchor_label: neg_label = random.choice(self.valid_labels)\n        neg_idx = random.choice(self.label_to_indices[neg_label])\n\n        return self.load_image(self.image_paths[idx]), self.load_image(self.image_paths[pos_idx]), self.load_image(self.image_paths[neg_idx])\n\n# Loaders\ntrain_paths, train_labels = [all_images[i] for i in train_idx], [label_ids[i] for i in train_idx]\nval_paths, val_labels = [all_images[i] for i in val_idx], [label_ids[i] for i in val_idx]\ntest_paths, test_labels = [all_images[i] for i in test_idx], [label_ids[i] for i in test_idx]\n\ntrain_dataset = TripletFashionDataset(train_paths, train_labels, train_transform)\nval_dataset = TripletFashionDataset(val_paths, val_labels, eval_transform)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(f'✅ Triplet Dataset siap. Train batches: {len(train_loader)} | Val batches: {len(val_loader)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-27T05:48:43.288986Z","iopub.execute_input":"2026-04-27T05:48:43.289612Z","iopub.status.idle":"2026-04-27T05:48:43.30679Z","shell.execute_reply.started":"2026-04-27T05:48:43.289535Z","shell.execute_reply":"2026-04-27T05:48:43.30607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ViTEmbedder(nn.Module):\n    def __init__(self, embed_dim=128, pretrained=True):\n        super().__init__()\n        self.vit = timm.create_model('vit_base_patch16_224', pretrained=pretrained, num_classes=0)\n        self.projector = nn.Sequential(nn.Linear(self.vit.num_features, 256), nn.ReLU(), nn.Dropout(0.1), nn.Linear(256, embed_dim))\n\n    def forward(self, x):\n        return F.normalize(self.projector(self.vit(x)), p=2, dim=1)\n\nclass CNNEmbedder(nn.Module):\n    def __init__(self, embed_dim=128, pretrained=True):\n        super().__init__()\n        backbone = models.resnet50(pretrained=pretrained)\n        self.backbone = nn.Sequential(*list(backbone.children())[:-1])\n        self.projector = nn.Sequential(nn.Linear(2048, 256), nn.ReLU(), nn.Dropout(0.1), nn.Linear(256, embed_dim))\n\n    def forward(self, x):\n        features = self.backbone(x)\n        return F.normalize(self.projector(features.view(features.size(0), -1)), p=2, dim=1)\n\nclass TripletLossWithHNM(nn.Module):\n    def __init__(self, margin=0.3, use_hnm=True):\n        super().__init__()\n        self.margin = margin\n        self.use_hnm = use_hnm\n\n    def forward(self, anchor, positive, negative):\n        d_ap = F.pairwise_distance(anchor, positive, p=2)\n        d_an = F.pairwise_distance(anchor, negative, p=2)\n        if self.use_hnm:\n            hard_mask = (d_an - d_ap) < self.margin\n            if hard_mask.sum() > 0:\n                d_ap, d_an = d_ap[hard_mask], d_an[hard_mask]\n        return torch.clamp(d_ap - d_an + self.margin, min=0.0).mean()\n\nprint('✅ Model ViT, ResNet-50, dan Triplet Loss siap.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-27T05:48:43.308672Z","iopub.execute_input":"2026-04-27T05:48:43.308976Z","iopub.status.idle":"2026-04-27T05:48:43.357802Z","shell.execute_reply.started":"2026-04-27T05:48:43.308952Z","shell.execute_reply":"2026-04-27T05:48:43.357045Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, loader, optimizer, criterion, device):\n    model.train(); total_loss = 0\n    for anchor, positive, negative in loader:\n        anchor, positive, negative = anchor.to(device), positive.to(device), negative.to(device)\n        optimizer.zero_grad()\n        loss = criterion(model(anchor), model(positive), model(negative))\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    return total_loss / len(loader)\n\ndef validate(model, loader, criterion, device):\n    model.eval(); total_loss = 0\n    with torch.no_grad():\n        for anchor, positive, negative in loader:\n            anchor, positive, negative = anchor.to(device), positive.to(device), negative.to(device)\n            loss = criterion(model(anchor), model(positive), model(negative))\n            total_loss += loss.item()\n    return total_loss / len(loader)\n\nvit_model = ViTEmbedder(embed_dim=EMBED_DIM).to(device)\ncriterion = TripletLossWithHNM(margin=MARGIN, use_hnm=True)\noptimizer = torch.optim.AdamW(vit_model.parameters(), lr=LR, weight_decay=1e-4)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=NUM_EPOCHS)\n\nhistory = {'epoch': [], 'train_loss': [], 'val_loss': []}\nbest_val_loss = float('inf')\n\nprint('🚀 MULAI TRAINING ViT...')\nfor epoch in range(1, NUM_EPOCHS + 1):\n    train_loss = train_one_epoch(vit_model, train_loader, optimizer, criterion, device)\n    val_loss = validate(vit_model, val_loader, criterion, device)\n    scheduler.step()\n\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        torch.save(vit_model.state_dict(), 'best_vit_model.pth')\n\n    if epoch % 5 == 0 or epoch == 1:\n        history['epoch'].append(epoch)\n        history['train_loss'].append(round(train_loss, 3))\n        history['val_loss'].append(round(val_loss, 3))\n        print(f'Epoch {epoch:3d}/{NUM_EPOCHS} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}')\n\nprint('\\n✅ Training ViT selesai!')\n\nplt.figure(figsize=(10, 5))\nplt.plot(history['epoch'], history['train_loss'], 'b-o', label='Training Loss')\nplt.plot(history['epoch'], history['val_loss'], 'r-s', label='Validation Loss')\nplt.xlabel('Epoch'); plt.ylabel('Triplet Loss'); plt.title('Kurva Loss ViT')\nplt.legend(); plt.grid(True)\nplt.savefig('loss_curve.png')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-27T05:48:43.358755Z","iopub.execute_input":"2026-04-27T05:48:43.359096Z","execution_failed":"2026-04-27T07:34:32.934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_embeddings(model, image_paths, labels, transform, device, batch_size=64):\n    model.eval(); all_embeddings, all_labels = [], []\n    for i in tqdm(range(0, len(image_paths), batch_size), desc='Extracting'):\n        batch_paths = image_paths[i:i+batch_size]\n        batch_imgs = [transform(Image.open(p).convert('RGB')) if os.path.exists(p) else torch.zeros(3, IMG_SIZE, IMG_SIZE) for p in batch_paths]\n        batch_tensor = torch.stack(batch_imgs).to(device)\n        with torch.no_grad():\n            emb = model(batch_tensor).cpu().numpy()\n        all_embeddings.append(emb); all_labels.extend(labels[i:i+batch_size])\n    return np.vstack(all_embeddings), np.array(all_labels)\n\ndef compute_recall_at_k(query_emb, query_lbl, db_emb, db_lbl, k_values=[1, 5, 10]):\n    index = faiss.IndexFlatL2(db_emb.shape[1])\n    index.add(db_emb.astype('float32'))\n    distances, indices = index.search(query_emb.astype('float32'), max(k_values) + 1)\n    results = {}\n    for k in k_values:\n        correct = sum(1 for q_idx, q_lbl in enumerate(query_lbl) if q_lbl in db_lbl[indices[q_idx][1:k+1]])\n        results[f'R@{k}'] = round(correct / len(query_lbl) * 100, 2)\n    return results\n\nprint('⏳ Evaluasi Model ViT Terbaik...')\nvit_model.load_state_dict(torch.load('best_vit_model.pth'))\ndb_paths, db_labels_arr = train_paths + val_paths, train_labels + val_labels\n\ndb_emb, db_lbl = extract_embeddings(vit_model, db_paths, db_labels_arr, eval_transform, device)\nquery_emb, query_lbl = extract_embeddings(vit_model, test_paths, test_labels, eval_transform, device)\n\nvit_recall = compute_recall_at_k(query_emb, query_lbl, db_emb, db_lbl)\nvit_avg = round(sum(vit_recall.values()) / len(vit_recall), 2)\nprint(f'\\n✅ HASIL ViT: {vit_recall} | Avg: {vit_avg}%')\n\n# Visualisasi Rekomendasi\nprint('🖼️ Membuat Visualisasi Rekomendasi...')\nq_idx = 0\nindex = faiss.IndexFlatL2(db_emb.shape[1])\nindex.add(db_emb.astype('float32'))\n_, indices = index.search(query_emb[q_idx:q_idx+1].astype('float32'), 6)\n\nfig, axes = plt.subplots(1, 6, figsize=(18, 4))\naxes[0].imshow(mpimg.imread(test_paths[q_idx])); axes[0].set_title('QUERY'); axes[0].axis('off')\nfor i, idx in enumerate(indices[0][1:6]):\n    axes[i+1].imshow(mpimg.imread(db_paths[idx])); axes[i+1].set_title(f'Top-{i+1}'); axes[i+1].axis('off')\nplt.savefig('recommendation_sample.png')\nplt.show()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-04-27T07:34:32.935Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print('🚀 Training CNN Baseline (ResNet-50) untuk pembanding...')\ncnn_model = CNNEmbedder(embed_dim=EMBED_DIM).to(device)\ncnn_crit = TripletLossWithHNM(margin=MARGIN, use_hnm=True)\ncnn_optim = torch.optim.AdamW(cnn_model.parameters(), lr=LR, weight_decay=1e-4)\n\nfor epoch in range(1, 11): # CNN dilatih 10 epoch saja agar cepat untuk pembanding\n    train_one_epoch(cnn_model, train_loader, cnn_optim, cnn_crit, device)\n\nprint('⏳ Evaluasi CNN Baseline...')\ndb_emb_cnn, db_lbl_cnn = extract_embeddings(cnn_model, db_paths, db_labels_arr, eval_transform, device)\nquery_emb_cnn, query_lbl_cnn = extract_embeddings(cnn_model, test_paths, test_labels, eval_transform, device)\n\ncnn_recall = compute_recall_at_k(query_emb_cnn, query_lbl_cnn, db_emb_cnn, db_lbl_cnn)\ncnn_avg = round(sum(cnn_recall.values()) / len(cnn_recall), 2)\n\ndf_compare = pd.DataFrame([\n    {'Model': 'CNN Baseline (ResNet-50)', 'R@1 (%)': cnn_recall['R@1'], 'R@5 (%)': cnn_recall['R@5'], 'R@10 (%)': cnn_recall['R@10'], 'Avg (%)': cnn_avg},\n    {'Model': 'ViT + Triplet Loss', 'R@1 (%)': vit_recall['R@1'], 'R@5 (%)': vit_recall['R@5'], 'R@10 (%)': vit_recall['R@10'], 'Avg (%)': vit_avg}\n])\n\nprint('\\n📋 TABEL 4.1 — SALIN KE PPT/SKRIPSI')\nprint(df_compare.to_string(index=False))\n\n# Ekspor\nwith pd.ExcelWriter('Hasil_Analisis_Data_BAB4.xlsx') as writer:\n    df_compare.to_excel(writer, sheet_name='Tabel4.1_RecallK', index=False)\n\nfrom IPython.display import FileLink, display\nprint('\\n📥 KLIK LINK DI BAWAH UNTUK MENGUNDUH HASIL:')\ndisplay(FileLink('Hasil_Analisis_Data_BAB4.xlsx'))\ndisplay(FileLink('loss_curve.png'))\ndisplay(FileLink('recommendation_sample.png'))\ndisplay(FileLink('best_vit_model.pth'))","metadata":{"trusted":true,"execution":{"execution_failed":"2026-04-27T07:34:32.935Z"}},"outputs":[],"execution_count":null}]}