{"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":127282,"databundleVersionId":15520105,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install timm faiss-cpu -q\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:45.722698Z","iopub.execute_input":"2026-02-16T08:59:45.723103Z","iopub.status.idle":"2026-02-16T08:59:48.984758Z","shell.execute_reply.started":"2026-02-16T08:59:45.723070Z","shell.execute_reply":"2026-02-16T08:59:48.983870Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch, gc\n\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.cuda.ipc_collect()\n\nprint(\"GPU Cleared\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:48.986791Z","iopub.execute_input":"2026-02-16T08:59:48.987163Z","iopub.status.idle":"2026-02-16T08:59:49.275814Z","shell.execute_reply.started":"2026-02-16T08:59:48.987129Z","shell.execute_reply":"2026-02-16T08:59:49.274999Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:49.277028Z","iopub.execute_input":"2026-02-16T08:59:49.277713Z","iopub.status.idle":"2026-02-16T08:59:49.291703Z","shell.execute_reply.started":"2026-02-16T08:59:49.277687Z","shell.execute_reply":"2026-02-16T08:59:49.290997Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, gc\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nimport torchvision.transforms as T\nimport timm\n\nfrom sklearn.cluster import DBSCAN\nimport matplotlib.pyplot as plt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:49.293790Z","iopub.execute_input":"2026-02-16T08:59:49.294128Z","iopub.status.idle":"2026-02-16T08:59:49.306080Z","shell.execute_reply.started":"2026-02-16T08:59:49.294103Z","shell.execute_reply":"2026-02-16T08:59:49.305531Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_PATH = \"/kaggle/input/animal-clef-2026/images\"\n\nprint(\"Images path:\", BASE_PATH)\n\n# Verify structure\nfor sp in [\n    \"LynxID2025\",\n    \"SalamanderID2025\",\n    \"SeaTurtleID2022\",\n    \"TexasHornedLizards\"\n]:\n    print(\n        sp,\n        os.path.exists(os.path.join(BASE_PATH, sp))\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:49.306954Z","iopub.execute_input":"2026-02-16T08:59:49.307138Z","iopub.status.idle":"2026-02-16T08:59:49.324192Z","shell.execute_reply.started":"2026-02-16T08:59:49.307120Z","shell.execute_reply":"2026-02-16T08:59:49.323628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SPECIES = [\n    \"LynxID2025\",\n    \"SalamanderID2025\",\n    \"SeaTurtleID2022\"\n]\n\nTEST_ONLY_SPECIES = [\n    \"TexasHornedLizards\"\n]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:49.345949Z","iopub.execute_input":"2026-02-16T08:59:49.346219Z","iopub.status.idle":"2026-02-16T08:59:49.349612Z","shell.execute_reply.started":"2026-02-16T08:59:49.346190Z","shell.execute_reply":"2026-02-16T08:59:49.349012Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_rows = []\n\nfor sp in SPECIES:\n    \n    train_path = os.path.join(BASE_PATH, sp, \"train\")\n    \n    for identity in os.listdir(train_path):\n        \n        identity_path = os.path.join(train_path, identity)\n        \n        if not os.path.isdir(identity_path):\n            continue\n        \n        for img in os.listdir(identity_path):\n            \n            if img.lower().endswith((\".jpg\",\".png\",\".jpeg\")):\n                \n                train_rows.append({\n                    \"file_path\": os.path.join(sp,\"train\",identity,img),\n                    \"identity\": identity,\n                    \"species\": sp\n                })\n\ntrain_df = pd.DataFrame(train_rows)\n\nprint(\"Train images:\", len(train_df))\ntrain_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:49.350394Z","iopub.execute_input":"2026-02-16T08:59:49.350675Z","iopub.status.idle":"2026-02-16T08:59:51.921444Z","shell.execute_reply.started":"2026-02-16T08:59:49.350646Z","shell.execute_reply":"2026-02-16T08:59:51.920795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_rows = []\n\nfor sp in SPECIES:\n    \n    test_path = os.path.join(BASE_PATH, sp, \"test\")\n    \n    for img in os.listdir(test_path):\n        test_rows.append({\n            \"file_path\": os.path.join(sp,\"test\",img),\n            \"image_id\": img,\n            \"species\": sp\n        })\n\nfor sp in TEST_ONLY_SPECIES:\n    \n    test_path = os.path.join(BASE_PATH, sp, \"test\")\n    \n    for img in os.listdir(test_path):\n        test_rows.append({\n            \"file_path\": os.path.join(sp,\"test\",img),\n            \"image_id\": img,\n            \"species\": sp\n        })\n\ntest_df = pd.DataFrame(test_rows)\n\nprint(\"Test images:\", len(test_df))\ntest_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:51.922542Z","iopub.execute_input":"2026-02-16T08:59:51.922785Z","iopub.status.idle":"2026-02-16T08:59:51.945288Z","shell.execute_reply.started":"2026-02-16T08:59:51.922764Z","shell.execute_reply":"2026-02-16T08:59:51.944723Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class AnimalDataset(Dataset):\n    \n    def __init__(self, df, root, transforms=None, train=True):\n        self.df = df.reset_index(drop=True)\n        self.root = root\n        self.transforms = transforms\n        self.train = train\n        \n        if train:\n            self.labels = df[\"identity\"].astype(\"category\").cat.codes.values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        \n        row = self.df.iloc[idx]\n        img_path = os.path.join(self.root, row[\"file_path\"])\n        \n        image = Image.open(img_path).convert(\"RGB\")\n        \n        if self.transforms:\n            image = self.transforms(image)\n        \n        if self.train:\n            return image, self.labels[idx]\n        \n        return image, row[\"image_id\"]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:51.947276Z","iopub.execute_input":"2026-02-16T08:59:51.947542Z","iopub.status.idle":"2026-02-16T08:59:51.953249Z","shell.execute_reply.started":"2026-02-16T08:59:51.947519Z","shell.execute_reply":"2026-02-16T08:59:51.952657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_tfms = T.Compose([\n    T.Resize((224,224)),\n    T.RandomHorizontalFlip(),\n    T.ColorJitter(0.2,0.2,0.2,0.1),\n    T.ToTensor(),\n    T.Normalize([0.485,0.456,0.406],\n                [0.229,0.224,0.225])\n])\n\nvalid_tfms = T.Compose([\n    T.Resize((224,224)),\n    T.ToTensor(),\n    T.Normalize([0.485,0.456,0.406],\n                [0.229,0.224,0.225])\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:51.954157Z","iopub.execute_input":"2026-02-16T08:59:51.954404Z","iopub.status.idle":"2026-02-16T08:59:51.971329Z","shell.execute_reply.started":"2026-02-16T08:59:51.954383Z","shell.execute_reply":"2026-02-16T08:59:51.970652Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = AnimalDataset(train_df, BASE_PATH, train_tfms, True)\ntest_ds  = AnimalDataset(test_df,  BASE_PATH, valid_tfms, False)\n\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=8,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=8,\n    shuffle=False,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:51.972269Z","iopub.execute_input":"2026-02-16T08:59:51.972568Z","iopub.status.idle":"2026-02-16T08:59:51.990131Z","shell.execute_reply.started":"2026-02-16T08:59:51.972545Z","shell.execute_reply":"2026-02-16T08:59:51.989477Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ReIDModel(nn.Module):\n    \n    def __init__(self,\n                 backbone=\"resnet50\",\n                 emb_dim=512):\n        \n        super().__init__()\n        \n        self.backbone = timm.create_model(\n            backbone,\n            pretrained=True,\n            num_classes=0\n        )\n        \n        in_features = self.backbone.num_features\n        \n        self.embedding = nn.Sequential(\n            nn.Linear(in_features,1024),\n            nn.ReLU(),\n            nn.Linear(1024,emb_dim)\n        )\n\n    def forward(self,x):\n        feat = self.backbone(x)\n        emb  = self.embedding(feat)\n        emb  = F.normalize(emb, dim=1)\n        return emb","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:51.991207Z","iopub.execute_input":"2026-02-16T08:59:51.991519Z","iopub.status.idle":"2026-02-16T08:59:52.003019Z","shell.execute_reply.started":"2026-02-16T08:59:51.991484Z","shell.execute_reply":"2026-02-16T08:59:52.002536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class TripletLoss(nn.Module):\n    \n    def __init__(self, margin=0.3):\n        super().__init__()\n        self.margin = margin\n\n    def forward(self, embeddings, labels):\n        \n        sim_matrix = F.cosine_similarity(\n            embeddings.unsqueeze(1),\n            embeddings.unsqueeze(0),\n            dim=2\n        )\n        \n        dist_matrix = 1 - sim_matrix\n        \n        loss = 0.0\n        triplet_count = 0\n        \n        for i in range(len(labels)):\n            \n            pos_mask = labels == labels[i]\n            neg_mask = labels != labels[i]\n            \n            pos_dist = dist_matrix[i][pos_mask]\n            neg_dist = dist_matrix[i][neg_mask]\n            \n            if pos_dist.shape[0] < 2:\n                continue\n            \n            hardest_pos = pos_dist.max()\n            hardest_neg = neg_dist.min()\n            \n            loss += F.relu(\n                hardest_pos - hardest_neg + self.margin\n            )\n            \n            triplet_count += 1\n        \n        if triplet_count == 0:\n            return torch.tensor(\n                0.0,\n                requires_grad=True,\n                device=embeddings.device\n            )\n        \n        return loss / triplet_count","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:52.003825Z","iopub.execute_input":"2026-02-16T08:59:52.004201Z","iopub.status.idle":"2026-02-16T08:59:52.023715Z","shell.execute_reply.started":"2026-02-16T08:59:52.004170Z","shell.execute_reply":"2026-02-16T08:59:52.023003Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = ReIDModel().to(device)\n\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=1e-4\n)\n\ncriterion = TripletLoss()\nscaler = torch.amp.GradScaler(\"cuda\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T08:59:52.024650Z","iopub.execute_input":"2026-02-16T08:59:52.024870Z","iopub.status.idle":"2026-02-16T08:59:52.772688Z","shell.execute_reply.started":"2026-02-16T08:59:52.024849Z","shell.execute_reply":"2026-02-16T08:59:52.771883Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"scaler = torch.amp.GradScaler(\"cuda\")\n\nEPOCHS = 25\ntrain_losses = []\n\nfor epoch in range(EPOCHS):\n    \n    model.train()\n    total_loss = 0\n    valid_batches = 0\n    \n    for imgs, labels in tqdm(train_loader):\n        \n        imgs = imgs.to(device)\n        labels = labels.to(device)\n        \n        optimizer.zero_grad()\n        \n        with torch.amp.autocast(\"cuda\"):\n            emb = model(imgs)\n            loss = criterion(emb, labels)\n        \n        # Skip batches with no valid triplets\n        if loss is None or not torch.isfinite(loss):\n            continue\n        \n        if loss.item() == 0:\n            continue\n        \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        total_loss += loss.item()\n        valid_batches += 1\n    \n    if valid_batches == 0:\n        epoch_loss = 0\n    else:\n        epoch_loss = total_loss / valid_batches\n    \n    train_losses.append(epoch_loss)\n    \n    print(\n        f\"Epoch {epoch+1}/{EPOCHS} — \"\n        f\"Loss: {epoch_loss:.4f} \"\n        f\"(valid batches: {valid_batches})\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T09:00:47.745927Z","iopub.execute_input":"2026-02-16T09:00:47.746316Z","iopub.status.idle":"2026-02-16T09:50:59.159872Z","shell.execute_reply.started":"2026-02-16T09:00:47.746280Z","shell.execute_reply":"2026-02-16T09:50:59.158798Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.plot(train_losses)\nplt.title(\"Training Loss\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T09:56:37.204110Z","iopub.execute_input":"2026-02-16T09:56:37.204957Z","iopub.status.idle":"2026-02-16T09:56:37.383611Z","shell.execute_reply.started":"2026-02-16T09:56:37.204918Z","shell.execute_reply":"2026-02-16T09:56:37.382916Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\n\ngallery_embeddings = []\ngallery_labels = []\n\nwith torch.no_grad():\n    for imgs, labels in tqdm(train_loader):\n        \n        imgs = imgs.to(device)\n        emb = model(imgs)\n        \n        gallery_embeddings.append(emb.cpu())\n        gallery_labels.append(labels)\n\ngallery_embeddings = torch.cat(gallery_embeddings)\ngallery_labels = torch.cat(gallery_labels)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T09:56:39.890205Z","iopub.execute_input":"2026-02-16T09:56:39.890562Z","iopub.status.idle":"2026-02-16T09:58:46.148360Z","shell.execute_reply.started":"2026-02-16T09:56:39.890530Z","shell.execute_reply":"2026-02-16T09:58:46.147421Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_embeddings = []\ntest_ids = []\n\nwith torch.no_grad():\n    for imgs, ids in tqdm(test_loader):\n        \n        imgs = imgs.to(device)\n        emb = model(imgs)\n        \n        test_embeddings.append(emb.cpu())\n        test_ids.extend(ids)\n\ntest_embeddings = torch.cat(test_embeddings)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T09:58:46.150372Z","iopub.execute_input":"2026-02-16T09:58:46.150986Z","iopub.status.idle":"2026-02-16T10:00:25.525323Z","shell.execute_reply.started":"2026-02-16T09:58:46.150954Z","shell.execute_reply":"2026-02-16T10:00:25.524723Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn.functional as F\nfrom tqdm import tqdm\n\n# Move embeddings to CPU\ngallery_cpu = gallery_embeddings.cpu()\ntest_cpu    = test_embeddings.cpu()\n\nbatch_size = 64   # safe for CPU RAM\n\nbest_sims = []\nbest_idxs = []\n\nfor i in tqdm(range(0, len(test_cpu), batch_size)):\n    \n    test_batch = test_cpu[i:i+batch_size]\n    \n    # cosine similarity\n    sim = F.cosine_similarity(\n        test_batch.unsqueeze(1),\n        gallery_cpu.unsqueeze(0),\n        dim=2\n    )\n    \n    b_sim, b_idx = sim.max(dim=1)\n    \n    best_sims.append(b_sim)\n    best_idxs.append(b_idx)\n\nbest_sim = torch.cat(best_sims)\nbest_idx = torch.cat(best_idxs)\n\nprint(\"Similarity matching complete ✅\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T10:36:03.794085Z","iopub.execute_input":"2026-02-16T10:36:03.794405Z","iopub.status.idle":"2026-02-16T10:37:29.135791Z","shell.execute_reply.started":"2026-02-16T10:36:03.794380Z","shell.execute_reply":"2026-02-16T10:37:29.135052Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"THRESHOLD = 0.9   # increased\n\nknown_mask = best_sim > THRESHOLD\n\nprint(\"Known:\", known_mask.sum().item())\nprint(\"Unknown:\", (~known_mask).sum().item())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T10:39:46.365298Z","iopub.execute_input":"2026-02-16T10:39:46.365966Z","iopub.status.idle":"2026-02-16T10:39:46.371432Z","shell.execute_reply.started":"2026-02-16T10:39:46.365933Z","shell.execute_reply":"2026-02-16T10:39:46.370680Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"unknown_embeddings = test_embeddings[~known_mask].cpu()\n\nprint(\"Unknown samples:\", len(unknown_embeddings))\n\nif len(unknown_embeddings) == 0:\n    \n    print(\"No unknown individuals detected — skipping clustering.\")\n    unknown_clusters = np.array([])\n    \nelse:\n    \n    from sklearn.cluster import DBSCAN\n    \n    clustering = DBSCAN(\n        eps=0.3,\n        min_samples=2,\n        metric=\"cosine\",\n        n_jobs=-1\n    )\n    \n    unknown_clusters = clustering.fit_predict(\n        unknown_embeddings\n    )\n    \n    print(\"Clustering complete ✅\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T10:39:48.642893Z","iopub.execute_input":"2026-02-16T10:39:48.643657Z","iopub.status.idle":"2026-02-16T10:39:48.665500Z","shell.execute_reply.started":"2026-02-16T10:39:48.643622Z","shell.execute_reply":"2026-02-16T10:39:48.664869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.hist(best_sim.numpy(), bins=50)\nplt.title(\"Similarity Distribution\")\nplt.xlabel(\"Cosine Similarity\")\nplt.ylabel(\"Count\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T10:39:51.820415Z","iopub.execute_input":"2026-02-16T10:39:51.821227Z","iopub.status.idle":"2026-02-16T10:39:51.986313Z","shell.execute_reply.started":"2026-02-16T10:39:51.821195Z","shell.execute_reply":"2026-02-16T10:39:51.985534Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"gallery_labels = gallery_labels.numpy()\n\nsubmission = []\nunknown_counter = 0\n\nfor i, img_id in enumerate(test_ids):\n    \n    if known_mask[i]:\n        label = gallery_labels[best_idx[i]]\n        cluster_name = f\"cluster_LynxID2025_{label}\"\n    else:\n        cid = unknown_clusters[unknown_counter]\n        cluster_name = f\"cluster_New_{cid}\"\n        unknown_counter += 1\n    \n    submission.append([img_id, cluster_name])\n\nsub_df = pd.DataFrame(\n    submission,\n    columns=[\"image_id\",\"cluster\"]\n)\n\nsub_df.to_csv(\"submission.csv\", index=False)\nsub_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-16T10:39:54.245236Z","iopub.execute_input":"2026-02-16T10:39:54.246011Z","iopub.status.idle":"2026-02-16T10:39:54.309863Z","shell.execute_reply.started":"2026-02-16T10:39:54.245978Z","shell.execute_reply":"2026-02-16T10:39:54.309228Z"}},"outputs":[],"execution_count":null}]}