{"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":"gpu","dataSources":[{"sourceId":126777,"databundleVersionId":15314950,"sourceType":"competition"},{"sourceId":14719738,"sourceType":"datasetVersion","datasetId":9405102}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🐆 Jaguar Re-Identification - Top 10 Solution (0.938)\n\n## 📋 Overview\n\nThis notebook presents a **strong baseline** for the Jaguar Re-Identification Challenge, achieving a **0.938 score** (Top 10 ranking). The solution combines state-of-the-art computer vision techniques with carefully optimized post-processing.\n\n## 🎯 Key Results\n\n- **Score:** 0.938+ (Public LB)\n- **Approach:** Transfer learning + Metric learning + Advanced post-processing\n\n## 🧠 Methodology\n\n### Model Architecture\n1. **Backbone:** EVA-02 Large (448px input)\n   - Pre-trained on 38M images\n   - State-of-the-art vision transformer\n   - Excellent for fine-grained recognition\n\n2. **Pooling:** GeM (Generalized Mean Pooling)\n   - Focuses on discriminative features\n   - Learnable power parameter (p=3)\n   - Superior to average pooling for spot patterns\n\n3. **Loss:** ArcFace\n   - Adds angular margin to embeddings\n   - Enforces intra-class compactness\n   - Strong separability between different jaguars\n\n### Post-Processing Pipeline\n1. **Test-Time Augmentation (TTA)**\n   - Horizontal flip averaging\n   - Improves robustness to viewpoint\n\n2. **Query Expansion**\n   - Average with top-3 most similar embeddings\n   - Reduces noise and outliers\n\n3. **K-Reciprocal Re-ranking**\n   - Refines similarity using neighbor consensus\n   - Parameters: k1=20, k2=6, λ=0.2\n\n4. **Optimal Blending** ⭐ **KEY INNOVATION**\n   - 20% raw cosine similarity\n   - 80% re-ranked similarity\n   - Lower lambda (0.2) works better than higher values\n\n## 📊 Why This Works\n\n### Challenge-Specific Adaptations\n\nThe competition uses **Identity-Balanced mAP**, which:\n- Gives equal weight to each jaguar\n- Doesn't favor frequent identities\n- Requires accurate ranking, not just high scores\n\nMy approach addresses this by:\n- **Blending raw and re-ranked similarities** - Prevents over-correction\n- **Lower lambda value (0.2)** - Less aggressive re-ranking\n- **Conservative post-processing** - Maintains base model quality\n\n### Key Insight\n\nThrough extensive experimentation, we discovered:\n- **Higher mean similarity ≠ Better score**\n- Aggressive re-ranking can hurt performance\n- Sweet spot: 20% raw + 80% reranked with λ=0.2\n\n## 🚀 How to Use\n\n1. **Add the pre-trained model dataset:**\n   - Go to \"Add Data\" → Search \"jaguar-eva-model1\"\n   - Or use your own trained model\n\n2. **Run all cells**\n   - The notebook is fully self-contained\n   - Takes ~5-10 minutes on Kaggle GPU\n\n3. **Submit `submission.csv`**\n   - Expected score: 0.938+\n\n## 📈 Experimentation Journey\n\n| Technique | Score Impact |\n|-----------|--------------|\n| Base model (EVA-02 + ArcFace) | ~0.85 |\n| + TTA | +0.03 |\n| + Query Expansion | +0.04 |\n| + Re-ranking (λ=0.3) | +0.04 → 0.926 |\n| + Optimal blend (20/80, λ=0.2) | +0.012 → **0.938** |\n\n## 💡 Tips for Improvement\n\n1. **Ensemble:** Train multiple models with different seeds\n2. **Multi-scale:** Test at different image sizes (384, 448, 512)\n3. **Different backbones:** Try ConvNeXt, Swin, DINOv2\n4. **Fine-tune blending:** Experiment with ratios around 20% raw\n\n## 🔧 Requirements\n\n- Kaggle GPU \n- Pre-trained model: `/kaggle/input/jaguar-eva-model1/model.pth`\n- Runtime: ~5-10 minutes\n\n## 📚 References\n\n- EVA-02: https://arxiv.org/abs/2303.11331\n- ArcFace: https://arxiv.org/abs/1801.07698\n- K-Reciprocal Re-ranking: https://arxiv.org/abs/1701.08398\n- GeM Pooling: https://arxiv.org/abs/1711.02512\n\n## 🙏 Acknowledgments\n\nThanks to the competition organizers and the Kaggle community!\n\nIf this notebook helps you, please **upvote** 👍 and feel free to fork!\n\n---\n\n**Good luck!** 🍀","metadata":{}},{"cell_type":"code","source":"# ============================================================================\n# SETUP & IMPORTS\n# ============================================================================\n\nimport os\nimport math\nimport random\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nfrom PIL import Image\n\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 timm\n\nprint(\"✅ All imports successful!\")\n\n\n# ============================================================================\n# CONFIGURATION\n# ============================================================================\n\nclass Config:\n    seed = 42\n    model_name = \"eva02_large_patch14_448.mim_m38m_ft_in22k_in1k\"\n    img_size = 448  \n    embedding_dim = 1024\n    num_classes = 31 \n    \n    batch_size = 4\n    use_tta = True  \n    \n    # Post-processing parameters (OPTIMIZED FOR 0.938 SCORE)\n    use_qe = True           \n    qe_top_k = 3           \n    use_rerank = True       \n    rerank_k1 = 20         \n    rerank_k2 = 6           \n    rerank_lambda = 0.2     \n    blend_raw_ratio = 0.20 \n    \n    test_dir = \"/kaggle/input/jaguar-re-id/test/test\"\n    test_csv = \"/kaggle/input/jaguar-re-id/test.csv\"\n    model_path = \"/kaggle/input/jaguar-eva-model1/model.pth\"  # Pre-trained weights\n    \n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    device_type = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n\ndef set_seed(seed):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\n\nset_seed(Config.seed)\nprint(f\"✅ Configuration set | Device: {Config.device}\")\n\n\n# ============================================================================\n# MODEL ARCHITECTURE\n# ============================================================================\n\nclass GeM(nn.Module):\n    def __init__(self, p=3, eps=1e-6):\n        super().__init__()\n        self.p = nn.Parameter(torch.ones(1) * p)\n        self.eps = eps\n\n    def forward(self, x):\n        return F.avg_pool2d(\n            x.clamp(min=self.eps).pow(self.p),\n            (x.size(-2), x.size(-1))\n        ).pow(1.0 / self.p)\n\n\nclass ArcFaceLayer(nn.Module):\n    def __init__(self, in_features, out_features, s=30.0, m=0.5):\n        super().__init__()\n        self.s = s  \n        self.m = m  \n        self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features))\n        nn.init.xavier_uniform_(self.weight)\n\n    def forward(self, input, label=None):\n        \n        cosine = F.linear(F.normalize(input), F.normalize(self.weight))\n        \n        if label is None:  \n            return cosine\n        \n        phi = cosine - self.m\n        one_hot = torch.zeros_like(cosine)\n        one_hot.scatter_(1, label.view(-1, 1), 1)\n        output = (one_hot * phi) + ((1.0 - one_hot) * cosine)\n        return output * self.s\n\n\nclass EVAJaguarModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        \n        self.backbone = timm.create_model(\n            Config.model_name,\n            pretrained=False, \n            num_classes=0\n        )\n        self.feat_dim = self.backbone.num_features\n        \n        self.gem = GeM(p=3)\n        \n        self.bn = nn.BatchNorm1d(self.feat_dim)\n        \n        self.head = ArcFaceLayer(self.feat_dim, Config.num_classes)\n\n    def forward(self, x, label=None):\n        features = self.backbone.forward_features(x)\n        \n        if features.dim() == 3:\n            B, N, C = features.shape\n            H = W = int(math.sqrt(N))\n            if H * W != N:\n                features = features[:, -(H * W):, :]\n            features = features.permute(0, 2, 1).reshape(B, C, H, W)\n        \n        emb = self.gem(features).flatten(1)\n        \n        emb = self.bn(emb)\n        \n        if label is not None:\n            return self.head(emb, label)\n        return emb\n\n\nprint(\"✅ Model architecture defined\")\n\n\n# ============================================================================\n# DATASET & TRANSFORMS\n# ============================================================================\n\nclass JaguarDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None):\n        self.df = df\n        self.img_dir = Path(img_dir)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = self.img_dir / row[\"filename\"]\n        \n        try:\n            img = Image.open(img_path).convert(\"RGB\")\n        except Exception as e:\n            print(f\"Warning: Failed to load {img_path}, using blank image\")\n            img = Image.new(\"RGB\", (Config.img_size, Config.img_size))\n        \n        if self.transform:\n            img = self.transform(img)\n        \n        return img, row[\"filename\"]\n\n\ntest_transform = transforms.Compose([\n    transforms.Resize((Config.img_size, Config.img_size)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.481, 0.457, 0.408], \n        std=[0.268, 0.261, 0.275]\n    ),\n])\n\nprint(\"✅ Dataset and transforms ready\")\n\n\n# ============================================================================\n# FEATURE EXTRACTION WITH TTA\n# ============================================================================\n\n@torch.no_grad()\ndef extract_features(model, loader):\n    model.eval()\n    all_features = []\n    all_names = []\n    \n    for imgs, img_names in tqdm(loader, desc=\"Extracting Features\"):\n        imgs = imgs.to(Config.device)\n        \n        with torch.amp.autocast(Config.device_type):\n            features = model(imgs)\n            \n            if Config.use_tta:\n                features_flip = model(torch.flip(imgs, [3]))\n                features = (features + features_flip) / 2\n        \n        features = F.normalize(features, dim=1).cpu()\n        all_features.append(features)\n        all_names.extend(img_names)\n    \n    return torch.cat(all_features, dim=0).numpy(), all_names\n\n\n# ============================================================================\n# POST-PROCESSING: QUERY EXPANSION\n# ============================================================================\n\ndef query_expansion(embeddings, top_k=3):\n    print(f\"  Applying Query Expansion (top_k={top_k})...\")\n\n    similarity = embeddings @ embeddings.T\n    top_indices = np.argsort(-similarity, axis=1)[:, :top_k]\n    expanded = np.zeros_like(embeddings)\n    for i in range(len(embeddings)):\n        expanded[i] = np.mean(embeddings[top_indices[i]], axis=0)\n    \n    expanded = expanded / np.linalg.norm(expanded, axis=1, keepdims=True)\n    return expanded\n\n\n# ============================================================================\n# POST-PROCESSING: K-RECIPROCAL RE-RANKING\n# ============================================================================\n\ndef k_reciprocal_rerank(similarity_matrix, k1=20, k2=6, lambda_value=0.3):\n    print(f\"  Applying Re-ranking (k1={k1}, k2={k2}, λ={lambda_value})...\")\n    \n    distance_matrix = 1 - similarity_matrix\n    original_dist = distance_matrix.copy()\n    initial_rank = np.argsort(distance_matrix, axis=1)\n    \n    nn_k1 = []\n    for i in range(len(similarity_matrix)):\n        forward_k1 = initial_rank[i, :k1 + 1]\n        backward_k1 = initial_rank[forward_k1, :k1 + 1]\n        fi = np.where(backward_k1 == i)[0]\n        nn_k1.append(forward_k1[fi])\n    \n    jaccard_dist = np.zeros_like(distance_matrix)\n    for i in range(len(similarity_matrix)):\n        ind_non_zero = np.where(distance_matrix[i, :] < 0.6)[0]\n        ind_images = [\n            j for j in ind_non_zero\n            if len(np.intersect1d(nn_k1[i], nn_k1[j])) > 0\n        ]\n        \n        for j in ind_images:\n            intersection = len(np.intersect1d(nn_k1[i], nn_k1[j]))\n            union = len(np.union1d(nn_k1[i], nn_k1[j]))\n            if union > 0:\n                jaccard_dist[i, j] = 1 - intersection / union\n    \n    final_dist = jaccard_dist * lambda_value + original_dist * (1 - lambda_value)\n    \n    return 1 - final_dist\n\n\n# ============================================================================\n# SUBMISSION GENERATION\n# ============================================================================\n\ndef make_submission(similarity_matrix, img_to_idx, test_df, filename=\"submission.csv\"):\n    predictions = []\n    \n    for _, row in tqdm(test_df.iterrows(), total=len(test_df), desc=\"Generating submission\"):\n        q_idx = img_to_idx[row['query_image']]\n        g_idx = img_to_idx[row['gallery_image']]\n        \n        similarity = similarity_matrix[q_idx, g_idx]\n        \n        similarity = np.clip(similarity, 0.0, 1.0)\n        predictions.append(similarity)\n    \n    submission = pd.DataFrame({\n        'row_id': test_df['row_id'],\n        'similarity': predictions\n    })\n    submission.to_csv(filename, index=False)\n    \n    print(f\"\\n✅ Submission saved: {filename}\")\n    print(f\"   Mean similarity: {np.mean(predictions):.4f}\")\n    print(f\"   Std similarity:  {np.std(predictions):.4f}\")\n    print(f\"   Range: [{np.min(predictions):.4f}, {np.max(predictions):.4f}]\")\n    \n    return predictions\n\n\n# ============================================================================\n# MAIN INFERENCE PIPELINE\n# ============================================================================\n\ndef main():\n    print(\"\\n\" + \"=\"*80)\n    print(\"🐆 JAGUAR RE-IDENTIFICATION - INFERENCE PIPELINE\")\n    print(\"=\"*80)\n    \n    # ========================================================================\n    # STEP 1: Load Pre-trained Model\n    # ========================================================================\n    print(\"\\n📦 STEP 1: Loading Model\")\n    print(\"-\" * 80)\n    \n    model = EVAJaguarModel().to(Config.device)\n    \n    print(f\"Loading weights from: {Config.model_path}\")\n    model.load_state_dict(torch.load(Config.model_path, map_location=Config.device))\n    model.eval()\n    print(\"✅ Model loaded successfully!\")\n    \n    \n    # ========================================================================\n    # STEP 2: Prepare Data\n    # ========================================================================\n    print(\"\\n📋 STEP 2: Preparing Test Data\")\n    print(\"-\" * 80)\n    \n    test_df = pd.read_csv(Config.test_csv)\n    \n    unique_test_images = sorted(\n        set(test_df[\"query_image\"]) | set(test_df[\"gallery_image\"])\n    )\n    print(f\"Total unique test images: {len(unique_test_images)}\")\n    print(f\"Total query-gallery pairs: {len(test_df)}\")\n    \n    test_dataset = JaguarDataset(\n        pd.DataFrame({\"filename\": unique_test_images}),\n        Config.test_dir,\n        test_transform\n    )\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=Config.batch_size,\n        shuffle=False,\n        num_workers=2,\n        pin_memory=True\n    )\n    \n    \n    # ========================================================================\n    # STEP 3: Extract Features with TTA\n    # ========================================================================\n    print(\"\\n🔍 STEP 3: Feature Extraction with TTA\")\n    print(\"-\" * 80)\n    \n    embeddings, img_names = extract_features(model, test_loader)\n    img_to_idx = {name: i for i, name in enumerate(img_names)}\n    \n    print(f\"✅ Embeddings extracted: {embeddings.shape}\")\n    \n    \n    # ========================================================================\n    # STEP 4: Post-Processing Pipeline\n    # ========================================================================\n    print(\"\\n⚙️  STEP 4: Post-Processing\")\n    print(\"-\" * 80)\n    \n    print(\"Computing raw cosine similarity...\")\n    sim_raw = embeddings @ embeddings.T\n    \n    if Config.use_qe:\n        embeddings_qe = query_expansion(embeddings, top_k=Config.qe_top_k)\n        sim_qe = embeddings_qe @ embeddings_qe.T\n    else:\n        sim_qe = sim_raw\n    \n    if Config.use_rerank:\n        sim_rerank = k_reciprocal_rerank(\n            sim_qe,\n            k1=Config.rerank_k1,\n            k2=Config.rerank_k2,\n            lambda_value=Config.rerank_lambda\n        )\n    else:\n        sim_rerank = sim_qe\n    \n    print(f\"\\n🔬 Blending: {Config.blend_raw_ratio:.0%} raw + \"\n          f\"{1-Config.blend_raw_ratio:.0%} reranked (λ={Config.rerank_lambda})\")\n    \n    similarity_matrix = (\n        Config.blend_raw_ratio * sim_raw + \n        (1 - Config.blend_raw_ratio) * sim_rerank\n    )\n    \n    print(\"✅ Post-processing complete!\")\n    \n    \n    # ========================================================================\n    # STEP 5: Generate Submission\n    # ========================================================================\n    print(\"\\n💾 STEP 5: Generating Submission\")\n    print(\"-\" * 80)\n    \n    predictions = make_submission(similarity_matrix, img_to_idx, test_df, \"submission.csv\")\n    \n    \n    # ========================================================================\n    # DONE\n    # ========================================================================\n    print(\"\\n\" + \"=\"*80)\n    print(\"✅ INFERENCE COMPLETE!\")\n    print(\"=\"*80)\n\n# ============================================================================\n# RUN INFERENCE\n# ============================================================================\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:36:14.368036Z","iopub.execute_input":"2026-02-08T18:36:14.368358Z","iopub.status.idle":"2026-02-08T18:38:16.513307Z","shell.execute_reply.started":"2026-02-08T18:36:14.368329Z","shell.execute_reply":"2026-02-08T18:38:16.512461Z"}},"outputs":[],"execution_count":null}]}