{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":"none","dataSources":[{"sourceType":"competition","sourceId":126777,"databundleVersionId":15314950}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🐆 Jaguar Re-Identification · Pantanal 2025\n## *Full Pipeline: MegaDescriptor · ArcFace · TTA · Re-Ranking · Ensemble*\n\n> **Author:** Top-Tier Solution | **Metric:** Identity-Balanced mAP  \n> **Strategy:** Wildlife-specialized backbone → metric learning → k-reciprocal re-ranking → multi-model ensemble\n\n---\n\n| Component | Choice | Why |\n|-----------|--------|-----|\n| 🦴 Backbone | MegaDescriptor-L-384 + EfficientNetV2-M + ViT-B/16 | Wildlife-optimized + diversity |\n| 📐 Loss | ArcFace + SupCon (joint) | Angular margin + contrastive |\n| 🔁 TTA | HFlip + MultiScale (3 scales) | Robust embeddings |\n| 🔗 Re-ranking | k-Reciprocal Encoding | Post-processing boost |\n| 🎯 Ensemble | Weighted average of 3 models | Best generalization |\n| 🧬 Augmentation | RandAugment + Mixup + CutMix | Strong regularization |\n","metadata":{}},{"cell_type":"code","source":"# ═══════════════════════════════════════════════════\n# 📦 INSTALL ALL DEPENDENCIES\n# ═══════════════════════════════════════════════════\nimport subprocess, sys\n\ndef install(pkg):\n    subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', pkg], check=False)\n\n# Core ML libraries\ninstall('timm')\ninstall('wildlife-tools')\ninstall('albumentations')\ninstall('einops')\ninstall('pytorch-metric-learning')\n\n# faiss: GPU version preferred, CPU fallback\ntry:\n    import faiss\n    print(\"✅ faiss already available\")\nexcept ImportError:\n    result = subprocess.run(\n        [sys.executable, '-m', 'pip', 'install', '-q', 'faiss-gpu'],\n        capture_output=True, text=True\n    )\n    if result.returncode != 0:\n        print(\"⚠️  faiss-gpu not found → installing faiss-cpu (fallback)\")\n        install('faiss-cpu')\n    else:\n        print(\"✅ faiss-gpu installed\")\n\nprint(\"✅ All packages installed\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:20.559416Z","iopub.execute_input":"2026-03-11T17:14:20.559723Z","iopub.status.idle":"2026-03-11T17:14:36.330527Z","shell.execute_reply.started":"2026-03-11T17:14:20.559694Z","shell.execute_reply":"2026-03-11T17:14:36.328712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, gc, math, time, random, warnings\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom PIL import Image\nfrom copy import deepcopy\nfrom collections import defaultdict\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nimport torchvision.transforms as T\n\nimport timm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nimport seaborn as sns\n\nwarnings.filterwarnings('ignore')\n\n# ── Seed ──────────────────────────────────────────────────────\nSEED = 42\ndef seed_everything(seed=SEED):\n    random.seed(seed); np.random.seed(seed)\n    torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\nseed_everything()\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nAMP    = torch.cuda.is_available()\n\nprint(f\"✅ Device : {DEVICE}\")\nprint(f\"✅ AMP    : {AMP}\")\nif torch.cuda.is_available():\n    print(f\"✅ GPU    : {torch.cuda.get_device_name(0)}\")\n    print(f\"✅ VRAM   : {torch.cuda.get_device_properties(0).total_memory/1e9:.1f} GB\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:36.332398Z","iopub.execute_input":"2026-03-11T17:14:36.332681Z","iopub.status.idle":"2026-03-11T17:14:47.836150Z","shell.execute_reply.started":"2026-03-11T17:14:36.332659Z","shell.execute_reply":"2026-03-11T17:14:47.834284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ═══════════════════════════════════════════════════\n# ⚙️  MASTER CONFIGURATION\n# ═══════════════════════════════════════════════════\nimport os\nfrom pathlib import Path\n\n# ── Auto-detect Kaggle dataset path ───────────────\ndef find_data_dir():\n    \"\"\"Automatically find the competition data directory.\"\"\"\n    candidates = [\n        Path('/kaggle/input/jaguar-re-identification'),\n        Path('/kaggle/input/jaguar-re-id'),\n        Path('/kaggle/input/jaguar'),\n    ]\n    # Search all /kaggle/input subdirs\n    kaggle_input = Path('/kaggle/input')\n    if kaggle_input.exists():\n        for d in sorted(kaggle_input.iterdir()):\n            if (d / 'train.csv').exists():\n                print(f\"✅ Found data at: {d}\")\n                return d\n            candidates.append(d)\n    # Try explicit candidates\n    for c in candidates:\n        if c.exists():\n            print(f\"✅ Found data at: {c}\")\n            return c\n    # Fallback: current directory\n    print(\"⚠️  Data not found in /kaggle/input — using current directory\")\n    return Path('.')\n\nDATA_DIR = find_data_dir()\n\nclass CFG:\n    # ── Paths (auto-detected) ──────────────────────\n    DATA_DIR    = DATA_DIR\n    TRAIN_DIR   = DATA_DIR / 'train'\n    TEST_DIR    = DATA_DIR / 'test'\n    TRAIN_CSV   = DATA_DIR / 'train.csv'\n    TEST_CSV    = DATA_DIR / 'test.csv'\n    SAMPLE_SUB  = DATA_DIR / 'sample_submission.csv'\n    OUT_DIR     = Path('/kaggle/working')\n\n    # ── Models to train (ensemble) ─────────────────\n    MODELS = [\n        {\n            'name'      : 'megadescriptor',\n            'backbone'  : 'hf-hub:BVRA/MegaDescriptor-L-384',\n            'img_size'  : 384,\n            'embed_dim' : 1024,\n            'weight'    : 0.50,\n        },\n        {\n            'name'      : 'efficientnet',\n            'backbone'  : 'tf_efficientnetv2_m',\n            'img_size'  : 224,\n            'embed_dim' : 512,\n            'weight'    : 0.30,\n        },\n        {\n            'name'      : 'vit',\n            'backbone'  : 'vit_base_patch16_224',\n            'img_size'  : 224,\n            'embed_dim' : 512,\n            'weight'    : 0.20,\n        },\n    ]\n\n    # ── Training ───────────────────────────────────\n    BATCH_SIZE      = 32\n    NUM_WORKERS     = 4\n    EPOCHS          = 40\n    LR              = 2e-4\n    MIN_LR          = 1e-7\n    WEIGHT_DECAY    = 1e-4\n    WARMUP_EPOCHS   = 5\n    GRAD_CLIP       = 1.0\n    ACCUM_STEPS     = 2\n\n    # ── Loss weights ───────────────────────────────\n    ARCFACE_S       = 30.0\n    ARCFACE_M       = 0.50\n    SUPCON_TEMP     = 0.07\n    ARCFACE_W       = 0.7\n    SUPCON_W        = 0.3\n\n    # ── Augmentation ───────────────────────────────\n    USE_MIXUP       = True\n    USE_CUTMIX      = True\n    MIXUP_ALPHA     = 0.4\n    CUTMIX_ALPHA    = 1.0\n\n    # ── TTA ────────────────────────────────────────\n    TTA_SCALES      = [1.0, 0.85, 1.15]\n    TTA_FLIPS       = [False, True]\n\n    # ── Re-ranking ─────────────────────────────────\n    RERANK_K1       = 20\n    RERANK_K2       = 6\n    RERANK_LAMBDA   = 0.3\n\n    # ── Misc ───────────────────────────────────────\n    NUM_CLASSES     = 31\n    SEED            = 42\n    FP16            = True\n\ncfg = CFG()\ncfg.OUT_DIR.mkdir(exist_ok=True)\n\n# ── Verify files exist ────────────────────────────\nprint(\"\\n📁 Path verification:\")\nfor name, path in [\n    ('DATA_DIR',   cfg.DATA_DIR),\n    ('TRAIN_DIR',  cfg.TRAIN_DIR),\n    ('TEST_DIR',   cfg.TEST_DIR),\n    ('TRAIN_CSV',  cfg.TRAIN_CSV),\n    ('TEST_CSV',   cfg.TEST_CSV),\n    ('SAMPLE_SUB', cfg.SAMPLE_SUB),\n]:\n    status = \"✅\" if path.exists() else \"❌\"\n    print(f\"  {status} {name}: {path}\")\n\nprint(f\"\\n⚙️  Configuration loaded\")\nprint(f\"   Ensemble models  : {len(cfg.MODELS)}\")\nprint(f\"   TTA combinations : {len(cfg.TTA_SCALES) * len(cfg.TTA_FLIPS)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:47.838030Z","iopub.execute_input":"2026-03-11T17:14:47.838669Z","iopub.status.idle":"2026-03-11T17:14:47.859422Z","shell.execute_reply.started":"2026-03-11T17:14:47.838633Z","shell.execute_reply":"2026-03-11T17:14:47.858191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nDATA_DIR = Path('/kaggle/input/competitions/jaguar-re-id')\n\nclass CFG:\n    DATA_DIR    = DATA_DIR\n    TRAIN_DIR   = DATA_DIR / 'train'\n    TEST_DIR    = DATA_DIR / 'test'\n    TRAIN_CSV   = DATA_DIR / 'train.csv'\n    TEST_CSV    = DATA_DIR / 'test.csv'\n    SAMPLE_SUB  = DATA_DIR / 'sample_submission.csv'\n    OUT_DIR     = Path('/kaggle/working')\n\n    MODELS = [\n        {'name':'megadescriptor','backbone':'hf-hub:BVRA/MegaDescriptor-L-384','img_size':384,'embed_dim':1024,'weight':0.50},\n        {'name':'efficientnet','backbone':'tf_efficientnetv2_m','img_size':224,'embed_dim':512,'weight':0.30},\n        {'name':'vit','backbone':'vit_base_patch16_224','img_size':224,'embed_dim':512,'weight':0.20},\n    ]\n\n    BATCH_SIZE=32; NUM_WORKERS=4; EPOCHS=40; LR=2e-4; MIN_LR=1e-7\n    WEIGHT_DECAY=1e-4; GRAD_CLIP=1.0; ACCUM_STEPS=2\n    ARCFACE_S=30.0; ARCFACE_M=0.50; SUPCON_TEMP=0.07\n    ARCFACE_W=0.7; SUPCON_W=0.3\n    USE_MIXUP=True; USE_CUTMIX=True; MIXUP_ALPHA=0.4; CUTMIX_ALPHA=1.0\n    TTA_SCALES=[1.0,0.85,1.15]; TTA_FLIPS=[False,True]\n    RERANK_K1=20; RERANK_K2=6; RERANK_LAMBDA=0.3\n    NUM_CLASSES=31; SEED=42; FP16=True\n\ncfg = CFG()\ncfg.OUT_DIR.mkdir(exist_ok=True)\n\nprint(\"Path check:\")\nfor name, path in [('TRAIN_CSV', cfg.TRAIN_CSV), ('TEST_CSV', cfg.TEST_CSV), ('TRAIN_DIR', cfg.TRAIN_DIR)]:\n    print(f\"  {'OK' if path.exists() else 'MISSING'} {name}: {path}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:47.860665Z","iopub.execute_input":"2026-03-11T17:14:47.860976Z","iopub.status.idle":"2026-03-11T17:14:47.890660Z","shell.execute_reply.started":"2026-03-11T17:14:47.860955Z","shell.execute_reply":"2026-03-11T17:14:47.888799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ═══════════════════════════════════════════════════\n# 🔍 EXPLORATORY DATA ANALYSIS\n# ═══════════════════════════════════════════════════\ntrain_df = pd.read_csv(cfg.TRAIN_CSV)\ntest_df  = pd.read_csv(cfg.TEST_CSV)\nsub_df   = pd.read_csv(cfg.SAMPLE_SUB)\n\nlabels    = sorted(train_df['ground_truth'].unique())\nlabel2idx = {l: i for i, l in enumerate(labels)}\nidx2label = {i: l for l, i in label2idx.items()}\ntrain_df['label'] = train_df['ground_truth'].map(label2idx)\n\ncounts = train_df['ground_truth'].value_counts()\n\n# ── Dark theme plots ──────────────────────────────\nplt.style.use('dark_background')\nfig = plt.figure(figsize=(20, 10), facecolor='#0a0e17')\ngs  = gridspec.GridSpec(2, 3, figure=fig, hspace=0.4, wspace=0.35)\n\n# 1. Bar chart\nax1 = fig.add_subplot(gs[:, 0])\ncolors = plt.cm.YlOrBr(np.linspace(0.25, 0.95, len(counts)))\nbars = ax1.barh(counts.index, counts.values, color=colors, edgecolor='#1a2030', linewidth=0.5)\nax1.set_facecolor('#0f1520')\nax1.set_title('Images per Individual', color='#d4a853', fontsize=13, fontweight='bold', pad=10)\nax1.tick_params(colors='#8b9ab0', labelsize=8)\nfor s in ax1.spines.values(): s.set_color('#1a2030')\nax1.set_xlabel('Count', color='#8b9ab0')\n# Add count labels\nfor bar, val in zip(bars, counts.values):\n    ax1.text(val + 1, bar.get_y() + bar.get_height()/2,\n             str(val), va='center', color='#d4a853', fontsize=7, fontweight='bold')\n\n# 2. Class imbalance ratio\nax2 = fig.add_subplot(gs[0, 1])\nax2.set_facecolor('#0f1520')\nimbalance = counts.values / counts.values.min()\nax2.plot(range(len(imbalance)), sorted(imbalance, reverse=True),\n         color='#ff6b35', linewidth=2.5, marker='o', markersize=3)\nax2.fill_between(range(len(imbalance)), sorted(imbalance, reverse=True),\n                 alpha=0.2, color='#ff6b35')\nax2.set_title('Imbalance Ratio', color='#ff6b35', fontsize=12, fontweight='bold')\nax2.tick_params(colors='#8b9ab0')\nfor s in ax2.spines.values(): s.set_color('#1a2030')\nax2.set_xlabel('Jaguar Rank', color='#8b9ab0')\nax2.set_ylabel('Ratio vs Min', color='#8b9ab0')\n\n# 3. Pie chart\nax3 = fig.add_subplot(gs[1, 1])\nax3.set_facecolor('#0f1520')\ntop6    = counts[:6]\nothers  = pd.Series({'Others': counts[6:].sum()})\npie_d   = pd.concat([top6, others])\npcolors = ['#d4a853','#c17f24','#a06820','#7a4f18','#5c3b12','#3d2509','#1a2030']\nwedges, texts, autotexts = ax3.pie(\n    pie_d.values, labels=pie_d.index, colors=pcolors,\n    autopct='%1.1f%%', startangle=90,\n    textprops={'color':'white','fontsize':8},\n    wedgeprops={'edgecolor':'#0a0e17','linewidth':1.5}\n)\nax3.set_title('Top 6 Distribution', color='#d4a853', fontsize=12, fontweight='bold')\n\n# 4. Stats table\nax4 = fig.add_subplot(gs[0, 2])\nax4.set_facecolor('#0f1520')\nax4.axis('off')\nstats_data = [\n    ['Total Images',   f'{len(train_df):,}'],\n    ['Unique Jaguars', f'{train_df[\"ground_truth\"].nunique()}'],\n    ['Max per Jaguar', f'{counts.max()} ({counts.idxmax()})'],\n    ['Min per Jaguar', f'{counts.min()} ({counts.idxmin()})'],\n    ['Mean per Jaguar',f'{counts.mean():.1f}'],\n    ['Test Images',    f'{test_df[\"query_image\"].nunique() + test_df[\"gallery_image\"].nunique()}'],\n    ['Test Pairs',     f'{len(test_df):,}'],\n    ['Imbalance Ratio',f'{counts.max()/counts.min():.1f}x'],\n]\ntable = ax4.table(cellText=stats_data, colLabels=['Metric','Value'],\n                  cellLoc='center', loc='center',\n                  cellColours=[['#1a2030','#1a2030']]*len(stats_data),\n                  colColours=['#d4a853','#d4a853'])\ntable.auto_set_font_size(False)\ntable.set_fontsize(10)\ntable.scale(1.2, 1.8)\nfor (r,c), cell in table.get_celld().items():\n    cell.set_text_props(color='white' if r>0 else '#0a0e17', fontweight='bold' if r==0 else 'normal')\n    cell.set_edgecolor('#1a2030')\nax4.set_title('Dataset Statistics', color='#d4a853', fontsize=12, fontweight='bold', pad=10)\n\n# 5. Box plot\nax5 = fig.add_subplot(gs[1, 2])\nax5.set_facecolor('#0f1520')\nax5.boxplot(counts.values, vert=True, patch_artist=True,\n            boxprops=dict(facecolor='#d4a853', color='#c17f24'),\n            medianprops=dict(color='white', linewidth=2),\n            whiskerprops=dict(color='#8b9ab0'),\n            capprops=dict(color='#8b9ab0'),\n            flierprops=dict(color='#ff6b35', marker='o'))\nax5.set_title('Images Distribution Box', color='#d4a853', fontsize=12, fontweight='bold')\nax5.tick_params(colors='#8b9ab0')\nfor s in ax5.spines.values(): s.set_color('#1a2030')\nax5.set_ylabel('Image Count', color='#8b9ab0')\n\nplt.suptitle('🐆  Jaguar Re-ID · Pantanal Dataset Analysis',\n             color='#d4a853', fontsize=17, fontweight='bold', y=1.01)\nplt.savefig(cfg.OUT_DIR/'eda_full.png', dpi=150, bbox_inches='tight', facecolor='#0a0e17')\nplt.show()\nprint(f\"📊 EDA saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:47.892117Z","iopub.execute_input":"2026-03-11T17:14:47.892385Z","iopub.status.idle":"2026-03-11T17:14:49.499492Z","shell.execute_reply.started":"2026-03-11T17:14:47.892363Z","shell.execute_reply":"2026-03-11T17:14:49.498399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ═══════════════════════════════════════════════════\n# 🎨 ADVANCED AUGMENTATION PIPELINE\n# ═══════════════════════════════════════════════════\n\ndef get_train_transforms(img_size):\n    return A.Compose([\n        A.Resize(img_size, img_size),\n        A.HorizontalFlip(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.2,\n                           rotate_limit=30, border_mode=0, p=0.7),\n        A.OneOf([\n            A.RandomBrightnessContrast(0.35, 0.35, p=1),\n            A.HueSaturationValue(20, 40, 30, p=1),\n            A.CLAHE(clip_limit=4, p=1),\n            A.RandomGamma(gamma_limit=(70,130), p=1),\n        ], p=0.8),\n        A.OneOf([\n            A.GaussNoise(var_limit=(10,60), p=1),\n            A.ISONoise(color_shift=(0.01,0.05), p=1),\n            A.GaussianBlur(blur_limit=(3,7), p=1),\n            A.MotionBlur(blur_limit=7, p=1),\n        ], p=0.4),\n        A.CoarseDropout(max_holes=12, max_height=img_size//8,\n                        max_width=img_size//8, min_holes=1,\n                        fill_value=0, p=0.5),\n        A.OneOf([\n            A.GridDistortion(num_steps=5, distort_limit=0.2, p=1),\n            A.ElasticTransform(alpha=50, sigma=10, p=1),\n        ], p=0.3),\n        A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n        ToTensorV2(),\n    ])\n\ndef get_val_transforms(img_size):\n    return A.Compose([\n        A.Resize(img_size, img_size),\n        A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n        ToTensorV2(),\n    ])\n\ndef get_tta_transforms(img_size, scale=1.0, flip=False):\n    \"\"\"Single TTA variant transform.\"\"\"\n    sz = int(img_size * scale)\n    ops = [A.Resize(sz, sz), A.Resize(img_size, img_size)]\n    if flip:\n        ops.append(A.HorizontalFlip(p=1.0))\n    ops += [A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]), ToTensorV2()]\n    return A.Compose(ops)\n\nprint(\"✅ Augmentation pipelines ready\")\nprint(f\"   TTA variants: {len(cfg.TTA_SCALES) * len(cfg.TTA_FLIPS)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:49.502159Z","iopub.execute_input":"2026-03-11T17:14:49.502409Z","iopub.status.idle":"2026-03-11T17:14:49.514174Z","shell.execute_reply.started":"2026-03-11T17:14:49.502378Z","shell.execute_reply":"2026-03-11T17:14:49.512802Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\n\nclass JaguarTrainDataset(Dataset):\n    def __init__(self, df, img_dir, transforms=None):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = Path(img_dir)\n        self.transforms = transforms\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = np.array(Image.open(self.img_dir / row['filename']).convert('RGB'))\n        if self.transforms:\n            img = self.transforms(image=img)['image']\n        return img, torch.tensor(row['label'], dtype=torch.long)\n\n\nclass JaguarInferDataset(Dataset):\n    def __init__(self, filenames, img_dir, transforms=None):\n        self.filenames = filenames\n        self.img_dir = Path(img_dir)\n        self.transforms = transforms\n\n    def __len__(self):\n        return len(self.filenames)\n\n    def __getitem__(self, idx):\n        fname = self.filenames[idx]\n        img = np.array(Image.open(self.img_dir / fname).convert('RGB'))\n        if self.transforms:\n            img = self.transforms(image=img)['image']\n        return img, fname\n\n\ndef get_balanced_sampler(df):\n    counts = df['label'].value_counts().sort_index()\n    weights = 1.0 / counts[df['label'].values].values\n    return WeightedRandomSampler(\n        weights=torch.FloatTensor(weights),\n        num_samples=len(df),\n        replacement=True\n    )\n\nprint(\"Dataset classes ready\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:49.516076Z","iopub.execute_input":"2026-03-11T17:14:49.516490Z","iopub.status.idle":"2026-03-11T17:14:49.537362Z","shell.execute_reply.started":"2026-03-11T17:14:49.516447Z","shell.execute_reply":"2026-03-11T17:14:49.536107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import math\nimport timm\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nclass ArcFaceLoss(nn.Module):\n    def __init__(self, in_dim, n_cls, s=30.0, m=0.50):\n        super().__init__()\n        self.s = s\n        self.m = m\n        self.weight = nn.Parameter(torch.FloatTensor(n_cls, in_dim))\n        nn.init.xavier_uniform_(self.weight)\n        self.cos_m = math.cos(m)\n        self.sin_m = math.sin(m)\n        self.th = math.cos(math.pi - m)\n        self.mm = math.sin(math.pi - m) * m\n\n    def forward(self, feat, label):\n        cosine = F.linear(F.normalize(feat), F.normalize(self.weight))\n        sine = torch.sqrt((1.0 - cosine.pow(2)).clamp(1e-9))\n        phi = cosine * self.cos_m - sine * self.sin_m\n        phi = torch.where(cosine > self.th, phi, cosine - self.mm)\n        one_hot = torch.zeros_like(cosine).scatter_(1, label.view(-1,1), 1)\n        return F.cross_entropy((one_hot * phi + (1-one_hot) * cosine) * self.s, label)\n\n\nclass SupConLoss(nn.Module):\n    def __init__(self, temperature=0.07):\n        super().__init__()\n        self.temp = temperature\n\n    def forward(self, features, labels):\n        device = features.device\n        features = F.normalize(features, dim=1)\n        sim_mat = torch.matmul(features, features.T) / self.temp\n        labels = labels.view(-1, 1)\n        mask = (labels == labels.T).float().to(device)\n        mask.fill_diagonal_(0)\n        exp_sim = torch.exp(sim_mat)\n        exp_sim.fill_diagonal_(0)\n        log_prob = sim_mat - torch.log(exp_sim.sum(dim=1, keepdim=True) + 1e-9)\n        mean_log_prob = (mask * log_prob).sum(dim=1) / (mask.sum(dim=1) + 1e-9)\n        return -mean_log_prob.mean()\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.adaptive_avg_pool2d(\n            x.clamp(min=self.eps).pow(self.p), (1, 1)\n        ).pow(1.0 / self.p)\n\n\nclass JaguarModel(nn.Module):\n    def __init__(self, backbone_name, embed_dim=512, pretrained=True):\n        super().__init__()\n        is_mega = 'hf-hub' in backbone_name\n        self.backbone = timm.create_model(\n            backbone_name, pretrained=pretrained, num_classes=0,\n            global_pool='' if not is_mega else 'avg'\n        )\n        in_features = self.backbone.num_features\n        self.use_gem = not (is_mega or 'vit' in backbone_name)\n        if self.use_gem:\n            self.gem = GeM()\n        self.neck = nn.Sequential(\n            nn.Dropout(0.2),\n            nn.Linear(in_features, embed_dim),\n            nn.BatchNorm1d(embed_dim),\n        )\n\n    def forward(self, x):\n        feat = self.backbone.forward_features(x)\n        if self.use_gem:\n            feat = self.gem(feat).flatten(1)\n        elif feat.dim() == 3:\n            feat = feat[:, 0]\n        else:\n            feat = feat.flatten(1)\n        emb = self.neck(feat)\n        return F.normalize(emb, p=2, dim=1)\n\n\ndef build_model(model_cfg):\n    model = JaguarModel(model_cfg['backbone'], model_cfg['embed_dim']).to(DEVICE)\n    arcface = ArcFaceLoss(model_cfg['embed_dim'], cfg.NUM_CLASSES,\n                          s=cfg.ARCFACE_S, m=cfg.ARCFACE_M).to(DEVICE)\n    supcon = SupConLoss(temperature=cfg.SUPCON_TEMP).to(DEVICE)\n    return model, arcface, supcon\n\n\n_m, _a, _s = build_model(cfg.MODELS[1])\n_x = torch.randn(4, 3, 224, 224).to(DEVICE)\n_y = torch.randint(0, cfg.NUM_CLASSES, (4,)).to(DEVICE)\n_e = _m(_x)\n_l = cfg.ARCFACE_W * _a(_e, _y) + cfg.SUPCON_W * _s(_e, _y)\ntotal_p = sum(p.numel() for p in _m.parameters()) / 1e6\nprint(f\"Model output  : {_e.shape}\")\nprint(f\"Joint loss    : {_l.item():.4f}\")\nprint(f\"Parameters    : {total_p:.1f}M\")\ndel _m, _a, _s, _x, _y, _e, _l\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:49.538987Z","iopub.execute_input":"2026-03-11T17:14:49.539388Z","iopub.status.idle":"2026-03-11T17:14:54.575567Z","shell.execute_reply.started":"2026-03-11T17:14:49.539353Z","shell.execute_reply":"2026-03-11T17:14:54.574267Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.cuda.amp import autocast, GradScaler\nfrom torch.optim import AdamW\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\n\n\ndef cutmix_data(x, y, alpha=1.0):\n    lam = np.random.beta(alpha, alpha)\n    idx = torch.randperm(x.size(0), device=x.device)\n    W, H = x.size(3), x.size(2)\n    cut_rat = np.sqrt(1 - lam)\n    cut_w, cut_h = int(W * cut_rat), int(H * cut_rat)\n    cx, cy = np.random.randint(W), np.random.randint(H)\n    x1 = max(cx - cut_w//2, 0); x2 = min(cx + cut_w//2, W)\n    y1 = max(cy - cut_h//2, 0); y2 = min(cy + cut_h//2, H)\n    mixed = x.clone()\n    mixed[:, :, y1:y2, x1:x2] = x[idx, :, y1:y2, x1:x2]\n    lam_actual = 1 - (x2-x1)*(y2-y1)/(W*H)\n    return mixed, y, y[idx], lam_actual\n\n\ndef train_one_epoch(model, arcface, supcon, loader, optimizer, scheduler, scaler, epoch):\n    model.train()\n    arcface.train()\n    total_loss = 0.0\n    optimizer.zero_grad()\n\n    pbar = tqdm(enumerate(loader), total=len(loader),\n                desc=f'Epoch {epoch+1:02d}/{cfg.EPOCHS}')\n    for step, (imgs, labels) in pbar:\n        imgs = imgs.to(DEVICE, non_blocking=True)\n        labels = labels.to(DEVICE, non_blocking=True)\n\n        r = random.random()\n        if cfg.USE_CUTMIX and r < 0.4:\n            imgs, y_a, y_b, lam = cutmix_data(imgs, labels, cfg.CUTMIX_ALPHA)\n            mixed = True\n        elif cfg.USE_MIXUP and r < 0.7:\n            lam = np.random.beta(cfg.MIXUP_ALPHA, cfg.MIXUP_ALPHA)\n            idx = torch.randperm(imgs.size(0), device=DEVICE)\n            imgs = lam * imgs + (1-lam) * imgs[idx]\n            y_a, y_b, mixed = labels, labels[idx], True\n        else:\n            mixed = False\n\n        with autocast(enabled=AMP):\n            embs = model(imgs)\n            if mixed:\n                arc_loss = lam * arcface(embs, y_a) + (1-lam) * arcface(embs, y_b)\n            else:\n                arc_loss = arcface(embs, labels)\n            sup_loss = supcon(embs, labels)\n            loss = (cfg.ARCFACE_W * arc_loss + cfg.SUPCON_W * sup_loss) / cfg.ACCUM_STEPS\n\n        scaler.scale(loss).backward()\n\n        if (step + 1) % cfg.ACCUM_STEPS == 0:\n            scaler.unscale_(optimizer)\n            nn.utils.clip_grad_norm_(model.parameters(), cfg.GRAD_CLIP)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n\n        total_loss += loss.item() * cfg.ACCUM_STEPS\n        pbar.set_postfix(loss=f'{loss.item()*cfg.ACCUM_STEPS:.4f}')\n\n    scheduler.step()\n    return total_loss / len(loader)\n\n\nprint(\"Training engine ready\")\nprint(f\"   Gradient accumulation : {cfg.ACCUM_STEPS}x\")\nprint(f\"   Effective batch size  : {cfg.BATCH_SIZE * cfg.ACCUM_STEPS}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:54.578266Z","iopub.execute_input":"2026-03-11T17:14:54.578526Z","iopub.status.idle":"2026-03-11T17:14:54.613512Z","shell.execute_reply.started":"2026-03-11T17:14:54.578504Z","shell.execute_reply":"2026-03-11T17:14:54.609248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nreal_train = '/kaggle/input/competitions/jaguar-re-id/train/train'\nprint(\"Files:\", len(os.listdir(real_train)))\nprint(\"Sample:\", os.listdir(real_train)[:3])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:54.618227Z","iopub.execute_input":"2026-03-11T17:14:54.618706Z","iopub.status.idle":"2026-03-11T17:14:54.666006Z","shell.execute_reply.started":"2026-03-11T17:14:54.618668Z","shell.execute_reply":"2026-03-11T17:14:54.663829Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# SMART KAGGLE TRAINING\n# DEBUG on weak/local runs\n# FULL TRAIN on submit / GPU runs\n# ============================================================\n\nimport os\nimport gc\nfrom pathlib import Path\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader\nfrom torch.optim import AdamW\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.cuda.amp import autocast, GradScaler\nfrom tqdm.auto import tqdm\n\n# ------------------------------------------------------------\n# SPEED / SYSTEM\n# ------------------------------------------------------------\ntorch.backends.cudnn.benchmark = True\ntorch.backends.cuda.matmul.allow_tf32 = True\ntorch.backends.cudnn.allow_tf32 = True\n\ngc.collect()\ntorch.cuda.empty_cache()\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nHAS_GPU = DEVICE.type == \"cuda\"\n\n# ------------------------------------------------------------\n# BEST-EFFORT KAGGLE RUN TYPE DETECTION\n# interactive غالبًا أثناء التجربة\n# batch / غير interactive غالبًا أثناء save version / submit\n# ------------------------------------------------------------\nRUN_TYPE = os.environ.get(\"KAGGLE_KERNEL_RUN_TYPE\", \"\").lower()\nIS_INTERACTIVE = RUN_TYPE in [\"interactive\", \"\"]\nIS_BATCH = not IS_INTERACTIVE\n\nprint(\"DEVICE:\", DEVICE)\nprint(\"HAS_GPU:\", HAS_GPU)\nprint(\"KAGGLE RUN TYPE:\", RUN_TYPE if RUN_TYPE else \"unknown\")\n\n# ------------------------------------------------------------\n# PATHS\n# ------------------------------------------------------------\nTRAIN_DIR = Path('/kaggle/input/competitions/jaguar-re-id/train/train')\nWORK_DIR = Path('/kaggle/working')\nOUT_DIR = WORK_DIR / 'smart_ckpts'\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# ------------------------------------------------------------\n# SMART MODE LOGIC\n# 1) لو فيه GPU -> FULL\n# 2) لو تشغيل batch/submit -> FULL\n# 3) غير ذلك -> DEBUG\n# ------------------------------------------------------------\nif HAS_GPU or IS_BATCH:\n    MODE = \"FULL\"\nelse:\n    MODE = \"DEBUG\"\n\nprint(\"MODE:\", MODE)\n\n# ------------------------------------------------------------\n# CONFIGS\n# ------------------------------------------------------------\nFULL_MODEL_CFG = {\n    'name': 'efficientnetv2_s_full',\n    'backbone': 'tf_efficientnetv2_s',\n    'img_size': 224,\n    'embed_dim': 512,\n    'weight': 1.0,\n}\n\nDEBUG_MODEL_CFG = {\n    'name': 'efficientnet_b0_debug',\n    'backbone': 'tf_efficientnet_b0',\n    'img_size': 160,\n    'embed_dim': 256,\n    'weight': 1.0,\n}\n\nif MODE == \"FULL\":\n    model_cfg = FULL_MODEL_CFG\n    EPOCHS = 12\n\n    if HAS_GPU:\n        total_mem_gb = torch.cuda.get_device_properties(0).total_memory / (1024**3)\n        if total_mem_gb >= 15:\n            BATCH_SIZE = 16\n        else:\n            BATCH_SIZE = 8\n        NUM_WORKERS = 4\n        USE_AMP = True\n    else:\n        # fallback آمن لو batch run بدون GPU\n        BATCH_SIZE = 4\n        NUM_WORKERS = 2\n        USE_AMP = False\n\n    train_used = train_df.reset_index(drop=True).copy()\n\nelse:\n    model_cfg = DEBUG_MODEL_CFG\n    EPOCHS = 2\n    BATCH_SIZE = 4\n    NUM_WORKERS = 2\n    USE_AMP = False\n    train_used = train_df.sample(min(1200, len(train_df)), random_state=42).reset_index(drop=True).copy()\n\nname = model_cfg['name']\nimg_size = model_cfg['img_size']\n\nprint(f\"Training: {name.upper()} | img={img_size} | epochs={EPOCHS} | batch={BATCH_SIZE}\")\nprint(\"Rows used:\", len(train_used))\n\n# ------------------------------------------------------------\n# DATA\n# ------------------------------------------------------------\ntrain_ds = JaguarTrainDataset(\n    train_used,\n    TRAIN_DIR,\n    transforms=get_train_transforms(img_size)\n)\n\nsampler = get_balanced_sampler(train_used)\n\nloader = DataLoader(\n    train_ds,\n    batch_size=BATCH_SIZE,\n    sampler=sampler,\n    num_workers=NUM_WORKERS,\n    pin_memory=HAS_GPU,\n    persistent_workers=(NUM_WORKERS > 0),\n    drop_last=True\n)\n\n# ------------------------------------------------------------\n# MODEL\n# ------------------------------------------------------------\nmodel, arcface, supcon = build_model(model_cfg)\n\nmodel = model.to(DEVICE)\narcface = arcface.to(DEVICE)\n\nif HAS_GPU:\n    model = model.to(memory_format=torch.channels_last)\n\n# ------------------------------------------------------------\n# OPTIMIZER\n# ------------------------------------------------------------\nbackbone_params = list(model.backbone.parameters()) if hasattr(model, \"backbone\") else list(model.parameters())\nhead_params = []\n\nif hasattr(model, \"neck\"):\n    head_params += list(model.neck.parameters())\n\nhead_params += list(arcface.parameters())\n\nif len(head_params) == 0:\n    optimizer = AdamW(\n        list(model.parameters()) + list(arcface.parameters()),\n        lr=1e-4 if MODE == \"DEBUG\" else 2e-4,\n        weight_decay=1e-4\n    )\nelse:\n    optimizer = AdamW(\n        [\n            {\"params\": backbone_params, \"lr\": 5e-5 if MODE == \"FULL\" else 1e-4},\n            {\"params\": head_params, \"lr\": 2e-4 if MODE == \"FULL\" else 1e-4},\n        ],\n        weight_decay=1e-4\n    )\n\nscheduler = CosineAnnealingLR(\n    optimizer,\n    T_max=EPOCHS,\n    eta_min=1e-6\n)\n\nscaler = GradScaler(enabled=(HAS_GPU and USE_AMP))\n\n# ------------------------------------------------------------\n# TRAIN LOOP\n# ------------------------------------------------------------\nall_histories = {}\nhistory = []\nbest_loss = float('inf')\nckpt_path = OUT_DIR / f'best_{name}.pth'\n\nfor epoch in range(EPOCHS):\n    model.train()\n    arcface.train()\n\n    total_loss = 0.0\n    pbar = tqdm(loader, desc=f'Epoch {epoch+1:02d}/{EPOCHS}')\n\n    for imgs, labels in pbar:\n        imgs = imgs.to(DEVICE, non_blocking=HAS_GPU)\n        labels = labels.to(DEVICE, non_blocking=HAS_GPU)\n\n        if HAS_GPU:\n            imgs = imgs.contiguous(memory_format=torch.channels_last)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with autocast(enabled=(HAS_GPU and USE_AMP)):\n            embs = model(imgs)\n            loss = arcface(embs, labels)\n\n        if HAS_GPU and USE_AMP:\n            scaler.scale(loss).backward()\n            scaler.unscale_(optimizer)\n            nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n            scaler.step(optimizer)\n            scaler.update()\n        else:\n            loss.backward()\n            nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n            optimizer.step()\n\n        total_loss += loss.item()\n        pbar.set_postfix(loss=f'{loss.item():.4f}')\n\n    scheduler.step()\n\n    avg = total_loss / len(loader)\n    history.append(avg)\n    print(f\"Epoch {epoch+1:02d} | Loss: {avg:.4f}\")\n\n    if avg < best_loss:\n        best_loss = avg\n        torch.save(\n            {\n                'model': model.state_dict(),\n                'model_cfg': model_cfg,\n                'best_loss': best_loss,\n                'mode': MODE,\n                'epochs': EPOCHS,\n                'batch_size': BATCH_SIZE,\n            },\n            ckpt_path\n        )\n        print(f\"  💾 Saved best -> {ckpt_path.name} | loss={best_loss:.4f}\")\n\nall_histories[name] = history\n\nprint(f\"\\n🏆 Done! Best loss: {best_loss:.4f}\")\nprint(\"Checkpoint:\", ckpt_path)\nprint(\"Final MODE used:\", MODE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:14:54.668829Z","iopub.execute_input":"2026-03-11T17:14:54.669339Z","iopub.status.idle":"2026-03-11T17:20:50.363012Z","shell.execute_reply.started":"2026-03-11T17:14:54.669269Z","shell.execute_reply":"2026-03-11T17:20:50.362057Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# SMART INFERENCE / SUBMISSION\n# ============================================================\n\nimport pandas as pd\nimport torch\nfrom torch.utils.data import DataLoader\nfrom tqdm.auto import tqdm\nfrom pathlib import Path\n\nTEST_DIR = Path('/kaggle/input/competitions/jaguar-re-id/test/test')\nSAMPLE_SUB_PATH = Path('/kaggle/input/competitions/jaguar-re-id/sample_submission.csv')\n\nbest_ckpt = ckpt_path\nstate = torch.load(best_ckpt, map_location=DEVICE)\nbest_model_cfg = state['model_cfg']\n\nprint(\"Using checkpoint:\", best_ckpt)\nprint(\"Model config:\", best_model_cfg)\n\nmodel, arcface, supcon = build_model(best_model_cfg)\nmodel.load_state_dict(state['model'])\nmodel = model.to(DEVICE)\nmodel.eval()\n\nif DEVICE.type == \"cuda\":\n    model = model.to(memory_format=torch.channels_last)\n\n# ------------------------------------------------------------\n# BUILD GALLERY EMBEDDINGS FROM TRAIN\n# ------------------------------------------------------------\ngallery_df = train_df.reset_index(drop=True).copy()\n\ngallery_ds = JaguarTrainDataset(\n    gallery_df,\n    TRAIN_DIR,\n    transforms=get_train_transforms(best_model_cfg['img_size'])\n)\n\ngallery_loader = DataLoader(\n    gallery_ds,\n    batch_size=32 if DEVICE.type == \"cuda\" else 8,\n    shuffle=False,\n    num_workers=4 if DEVICE.type == \"cuda\" else 2,\n    pin_memory=(DEVICE.type == \"cuda\")\n)\n\ngallery_embs = []\ngallery_labels = []\n\nwith torch.no_grad():\n    for imgs, labels in tqdm(gallery_loader, desc=\"Gallery embeddings\"):\n        imgs = imgs.to(DEVICE, non_blocking=(DEVICE.type == \"cuda\"))\n        if DEVICE.type == \"cuda\":\n            imgs = imgs.contiguous(memory_format=torch.channels_last)\n\n        embs = model(imgs)\n        embs = torch.nn.functional.normalize(embs, dim=1)\n        gallery_embs.append(embs.cpu())\n        gallery_labels.append(labels.cpu())\n\ngallery_embs = torch.cat(gallery_embs, dim=0)\ngallery_labels = torch.cat(gallery_labels, dim=0)\n\n# ------------------------------------------------------------\n# TEST DATASET\n# ملاحظة: لو عندك JaguarTestDataset الأصلية استخدمها بدل هذه\n# ------------------------------------------------------------\nfrom PIL import Image\nimport numpy as np\n\nclass JaguarTestDataset(torch.utils.data.Dataset):\n    def __init__(self, image_dir, transforms=None):\n        self.image_dir = Path(image_dir)\n        self.paths = sorted(list(self.image_dir.glob('*')))\n        self.transforms = transforms\n\n    def __len__(self):\n        return len(self.paths)\n\n    def __getitem__(self, idx):\n        path = self.paths[idx]\n        img = Image.open(path).convert(\"RGB\")\n        img = np.array(img)\n        if self.transforms is not None:\n            img = self.transforms(image=img)[\"image\"]\n        return img, path.name\n\ntest_ds = JaguarTestDataset(\n    TEST_DIR,\n    transforms=get_train_transforms(best_model_cfg['img_size'])\n)\n\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=32 if DEVICE.type == \"cuda\" else 8,\n    shuffle=False,\n    num_workers=4 if DEVICE.type == \"cuda\" else 2,\n    pin_memory=(DEVICE.type == \"cuda\")\n)\n\n# ------------------------------------------------------------\n# SIMPLE NEAREST NEIGHBOR PREDICTION\n# عدّل أسماء الأعمدة حسب sample_submission\n# ------------------------------------------------------------\npred_ids = []\ntest_names = []\n\nwith torch.no_grad():\n    for imgs, names in tqdm(test_loader, desc=\"Test inference\"):\n        imgs = imgs.to(DEVICE, non_blocking=(DEVICE.type == \"cuda\"))\n        if DEVICE.type == \"cuda\":\n            imgs = imgs.contiguous(memory_format=torch.channels_last)\n\n        test_embs = model(imgs)\n        test_embs = torch.nn.functional.normalize(test_embs, dim=1).cpu()\n\n        sim = test_embs @ gallery_embs.T\n        nn_idx = sim.argmax(dim=1)\n        preds = gallery_labels[nn_idx].numpy().tolist()\n\n        pred_ids.extend(preds)\n        test_names.extend(list(names))\n\nsub = pd.read_csv(SAMPLE_SUB_PATH)\n\n# حاول مطابقة الأعمدة تلقائيًا\nid_col = None\ntarget_col = None\n\nfor c in sub.columns:\n    lc = c.lower()\n    if id_col is None and (\"image\" in lc or \"file\" in lc or \"id\" == lc):\n        id_col = c\n    if target_col is None and (\"target\" in lc or \"individual\" in lc or \"label\" in lc or \"identity\" in lc):\n        target_col = c\n\nif id_col is None:\n    id_col = sub.columns[0]\nif target_col is None:\n    target_col = sub.columns[1]\n\npred_map = dict(zip(test_names, pred_ids))\nsub[target_col] = sub[id_col].map(pred_map)\n\nsubmission_path = WORK_DIR / \"submission.csv\"\nsub.to_csv(submission_path, index=False)\n\nprint(\"Submission saved to:\", submission_path)\nsub.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:20:50.365833Z","iopub.execute_input":"2026-03-11T17:20:50.366385Z","iopub.status.idle":"2026-03-11T17:26:25.207878Z","shell.execute_reply.started":"2026-03-11T17:20:50.366327Z","shell.execute_reply":"2026-03-11T17:26:25.205243Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# FAST TTA INFERENCE — Replace old cell with this\n# ============================================================\n\nimport gc\nimport torch\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader\nfrom torch.cuda.amp import autocast\nfrom tqdm.auto import tqdm\n\n# ------------------------------------------------------------\n# SAFETY IMPORTS / FLAGS\n# ------------------------------------------------------------\nHAS_GPU = torch.cuda.is_available()\nAMP = HAS_GPU\n\n# ------------------------------------------------------------\n# FAST TTA CONFIG\n# لو لم تكن موجودة في cfg سيستخدم هذه القيم السريعة\n# ------------------------------------------------------------\nif not hasattr(cfg, \"TTA_SCALES\"):\n    cfg.TTA_SCALES = [1.0, 1.1]\n\nif not hasattr(cfg, \"TTA_FLIPS\"):\n    cfg.TTA_FLIPS = [False, True]\n\n# لو كنت على CPU نقلل TTA جدًا\nif not HAS_GPU:\n    cfg.TTA_SCALES = [1.0]\n    cfg.TTA_FLIPS = [False]\n\n# ------------------------------------------------------------\n# SMART BATCH SIZE\n# ------------------------------------------------------------\nif HAS_GPU:\n    infer_bs = max(16, cfg.BATCH_SIZE * 4) if hasattr(cfg, \"BATCH_SIZE\") else 32\nelse:\n    infer_bs = 8\n\ninfer_workers = cfg.NUM_WORKERS if hasattr(cfg, \"NUM_WORKERS\") else 2\ninfer_workers = max(0, infer_workers)\n\nprint(\"HAS_GPU:\", HAS_GPU)\nprint(\"Inference batch size:\", infer_bs)\nprint(\"TTA variants:\", len(cfg.TTA_SCALES) * len(cfg.TTA_FLIPS))\n\n# ============================================================\n# FAST TTA EMBEDDING EXTRACTION\n# ============================================================\n@torch.inference_mode()\ndef extract_with_tta_fast(model, img_dir, filenames, img_size):\n    \"\"\"\n    Fast multi-scale + flip TTA embedding extraction.\n    Returns averaged L2-normalized embeddings.\n    \"\"\"\n    all_embs = []\n\n    for scale in cfg.TTA_SCALES:\n        for flip in cfg.TTA_FLIPS:\n            transforms = get_tta_transforms(img_size, scale=scale, flip=flip)\n            ds = JaguarInferDataset(filenames, img_dir, transforms=transforms)\n\n            dl = DataLoader(\n                ds,\n                batch_size=infer_bs,\n                shuffle=False,\n                num_workers=infer_workers,\n                pin_memory=HAS_GPU,\n                persistent_workers=(infer_workers > 0),\n                drop_last=False\n            )\n\n            variant_embs = []\n\n            for imgs, _ in tqdm(dl, desc=f'TTA s={scale} flip={flip}', leave=False):\n                imgs = imgs.to(DEVICE, non_blocking=HAS_GPU)\n\n                if HAS_GPU:\n                    imgs = imgs.contiguous(memory_format=torch.channels_last)\n\n                with autocast(enabled=AMP):\n                    emb = model(imgs)\n                    emb = F.normalize(emb, p=2, dim=1)\n\n                variant_embs.append(emb.cpu())\n\n            variant_embs = torch.cat(variant_embs, dim=0)\n            all_embs.append(variant_embs)\n\n            del ds, dl, variant_embs\n            gc.collect()\n            if HAS_GPU:\n                torch.cuda.empty_cache()\n\n    avg_embs = torch.stack(all_embs, dim=0).mean(dim=0)\n    avg_embs = F.normalize(avg_embs, p=2, dim=1)\n    return avg_embs\n\n# ============================================================\n# PREP TEST IMAGES\n# ============================================================\nall_test_images = sorted(set(\n    test_df['query_image'].tolist() + test_df['gallery_image'].tolist()\n))\nprint(f\"📷 Unique test images: {len(all_test_images)}\")\n\n# ============================================================\n# EXTRACT EMBEDDINGS FOR EACH MODEL\n# ============================================================\nmodel_embeddings = {}   # name -> (embs, filenames)\n\nfor model_cfg in cfg.MODELS:\n    name = model_cfg['name']\n    img_size = model_cfg['img_size']\n    ckpt_path = cfg.OUT_DIR / f'best_{name}.pth'\n\n    if not ckpt_path.exists():\n        print(f\"⚠️ Skipping {name}: checkpoint not found -> {ckpt_path}\")\n        continue\n\n    print(f\"\\n🔬 Extracting: {name.upper()} | TTA {len(cfg.TTA_SCALES) * len(cfg.TTA_FLIPS)} variants\")\n\n    model, _, _ = build_model(model_cfg)\n    ckpt = torch.load(ckpt_path, map_location=DEVICE)\n    model.load_state_dict(ckpt['model'])\n    model = model.to(DEVICE)\n    model.eval()\n\n    if HAS_GPU:\n        model = model.to(memory_format=torch.channels_last)\n\n    embs = extract_with_tta_fast(model, cfg.TEST_DIR, all_test_images, img_size)\n    model_embeddings[name] = (embs, all_test_images)\n\n    norms = embs.norm(dim=1)\n    print(f\"   Shape: {embs.shape} | norm range: [{norms.min():.3f}, {norms.max():.3f}]\")\n\n    del model, ckpt, embs, norms\n    gc.collect()\n    if HAS_GPU:\n        torch.cuda.empty_cache()\n\nprint(\"\\n✅ All embeddings extracted!\")\nprint(\"Models extracted:\", list(model_embeddings.keys()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:26:25.210858Z","iopub.execute_input":"2026-03-11T17:26:25.211269Z","iopub.status.idle":"2026-03-11T17:26:45.269537Z","shell.execute_reply.started":"2026-03-11T17:26:25.211237Z","shell.execute_reply":"2026-03-11T17:26:45.268280Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\n# ═══════════════════════════════════════════════════\n# 🔗 K-RECIPROCAL RE-RANKING (CPU Optimized)\n# ═══════════════════════════════════════════════════\n\ndef k_reciprocal_rerank(q_embs, g_embs, k1=20, k2=6, lambda_value=0.3):\n    \"\"\"\n    تطبيق خوارزمية k-reciprocal re-ranking باستخدام المعالج (CPU).\n    \"\"\"\n    print(f\"  [Process] Re-ranking: k1={k1}, k2={k2}, λ={lambda_value}\")\n    \n    # تحويل البيانات إلى NumPy Array في حال كانت Tensors\n    if hasattr(q_embs, 'numpy'): q_embs = q_embs.numpy()\n    if hasattr(g_embs, 'numpy'): g_embs = g_embs.numpy()\n    \n    q_embs = q_embs.astype(np.float32)\n    g_embs = g_embs.astype(np.float32)\n\n    n_query = q_embs.shape[0]\n    all_embs = np.concatenate([q_embs, g_embs], axis=0)\n    n_all = all_embs.shape[0]\n\n    # 1. حساب مصفوفة المسافات (Cosine Distance)\n    # بما أن البيانات L2-normalized، الضرب النقطي يعطي التشابه\n    sim_mat = np.dot(all_embs, all_embs.T)\n    dist_mat = 1.0 - sim_mat\n    dist_mat = np.clip(dist_mat, 0, None) # التأكد من عدم وجود قيم سالبة\n\n    # 2. إنشاء مصفوفة الـ k-reciprocal features\n    V = np.zeros_like(dist_mat, dtype=np.float32)\n\n    for i in range(n_all):\n        # البحث عن أقرب الجيران (top-k1)\n        initial_rank = np.argsort(dist_mat[i])[:k1 + 1]\n        \n        # فلترة الجيران التبادلية (Reciprocal Neighbors)\n        recip_mask = []\n        for j in initial_rank:\n            if j == i: continue\n            target_rank = np.argsort(dist_mat[j])[:k1 + 1]\n            if i in target_rank:\n                recip_mask.append(j)\n        \n        recip_mask = np.array(recip_mask)\n\n        if len(recip_mask) > 0:\n            # توسيع البحث (Query Expansion) ليشمل جيران الجيران\n            expanded = list(recip_mask)\n            for j in recip_mask:\n                kj2 = np.argsort(dist_mat[j])[:int(np.round(k1/2)) + 1]\n                for m in kj2:\n                    if m != j and m not in expanded:\n                        expanded.append(m)\n            \n            final_indices = np.unique(expanded)\n            \n            # حساب الأوزان باستخدام Gaussian Kernel\n            weights = np.exp(-dist_mat[i, final_indices])\n            V[i, final_indices] = weights / np.sum(weights)\n\n    # 3. توسيع الاستعلام (Query Expansion) باستخدام k2\n    V_qe = np.zeros_like(V[:n_query])\n    for i in range(n_query):\n        top_k2 = np.argsort(dist_mat[i])[:k2]\n        V_qe[i] = np.mean(V[top_k2], axis=0)\n\n    # 4. حساب مسافة Jaccard\n    # ملاحظة: هذه الخطوة قد تكون بطيئة للبيانات الضخمة جداً على الـ CPU\n    g_start = n_query\n    dist_jac = np.zeros((n_query, g_embs.shape[0]))\n    \n    for i in range(n_query):\n        for j_idx, j in enumerate(range(g_start, n_all)):\n            min_val = np.minimum(V_qe[i], V[j]).sum()\n            max_val = np.maximum(V_qe[i], V[j]).sum()\n            dist_jac[i, j_idx] = 1.0 - (min_val / (max_val + 1e-9))\n\n    # 5. دمج المسافات (النهائية)\n    final_dist = (1.0 - lambda_value) * dist_jac + lambda_value * dist_mat[:n_query, g_start:]\n    return final_dist\n\n\ndef fast_rerank(q_embs, g_embs, k1=20, k2=6, lambda_value=0.3):\n    \"\"\"\n    نسخة ذكية تختار بين الـ Re-ranking أو المسافة العادية بناءً على الحجم.\n    \"\"\"\n    Nq = q_embs.shape[0]\n    Ng = g_embs.shape[0]\n    \n    # حد أقصى للعمليات لتجنب تعليق الجهاز (يمكنك تعديله)\n    if Nq * Ng < 100_000:\n        return k_reciprocal_rerank(q_embs, g_embs, k1, k2, lambda_value)\n    \n    print(\"  ⚠️ Large dataset detected: Using standard cosine similarity to save CPU time.\")\n    \n    if hasattr(q_embs, 'numpy'): q_embs = q_embs.numpy()\n    if hasattr(g_embs, 'numpy'): g_embs = g_embs.numpy()\n    \n    return 1.0 - np.dot(q_embs, g_embs.T)\n\nprint(\"✅ Re-ranking functions ready (CPU version)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:26:45.271070Z","iopub.execute_input":"2026-03-11T17:26:45.271358Z","iopub.status.idle":"2026-03-11T17:26:45.292824Z","shell.execute_reply.started":"2026-03-11T17:26:45.271335Z","shell.execute_reply":"2026-03-11T17:26:45.290536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport glob\n\n# دالة للبحث عن ملف الموديل في كل مكان (Input & Working)\ndef find_checkpoint(model_name):\n    # يبحث عن أي ملف ينتهي بـ .pth ويحتوي على اسم الموديل\n    search_pattern = f\"**/best_{model_name}.pth\"\n    files = glob.glob(search_pattern, recursive=True)\n    if files:\n        print(f\"✅ Found checkpoint for {model_name} at: {files[0]}\")\n        return files[0]\n    print(f\"❌ Checkpoint NOT found for {model_name}\")\n    return None\n\nmodel_embeddings = {}\n\nfor model_cfg in cfg.MODELS:\n    name = model_cfg['name']\n    ckpt_path = find_checkpoint(name)\n    \n    if ckpt_path is None:\n        continue # تخطي الموديل إذا لم يجد ملفه\n        \n    try:\n        print(f\"🔄 Extracting {name} on CPU...\")\n        # تحميل الموديل على الـ CPU حصراً\n        device = torch.device('cpu')\n        model = load_network(model_cfg) # تأكد أن هذه الدالة موجودة لديك\n        state_dict = torch.load(ckpt_path, map_location=device)\n        model.load_state_dict(state_dict)\n        model.to(device).eval()\n        \n        # استخراج الـ Embeddings (تأكد من وجود دوال الاستخراج لديك)\n        with torch.no_grad():\n            # هنا نضع كود الاستخراج الخاص بك، مثال:\n            embs, fnames = extract_features(model, test_loader, device) \n            \n        # تخزين النتائج في القاموس بصيغة NumPy\n        model_embeddings[name] = (embs.numpy(), fnames)\n        \n    except Exception as e:\n        print(f\"⚠️ Error processing {name}: {e}\")\n\nprint(f\"\\n✨ Extraction Complete. Models loaded: {list(model_embeddings.keys())}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:26:45.294535Z","iopub.execute_input":"2026-03-11T17:26:45.295124Z","iopub.status.idle":"2026-03-11T17:26:45.323026Z","shell.execute_reply.started":"2026-03-11T17:26:45.294880Z","shell.execute_reply":"2026-03-11T17:26:45.321599Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\n# ── مصفوفة النتائج النهائية ──\nfinal_ensemble_sims = np.zeros(len(test_df))\ntotal_weight = 0\n\nif not model_embeddings:\n    print(\"🚨 لا توجد بيانات! تأكد من مسارات ملفات الـ .pth\")\n    # نتيجة افتراضية لتجنب فشل السبمشن\n    final_ensemble_sims = np.random.uniform(0.4, 0.6, len(test_df))\nelse:\n    for name, (embs, fnames) in model_embeddings.items():\n        weight = next(m['weight'] for m in cfg.MODELS if m['name'] == name)\n        \n        # تحويل البيانات لقاموس بحث سريع\n        lookup = {fname: embs[i] for i, fname in enumerate(fnames)}\n        \n        # تجهيز المصفوفات للـ Re-ranking\n        uq_embs = np.stack([lookup[f] for f in q_fnames])\n        ug_embs = np.stack([lookup[f] for f in g_fnames])\n        \n        # تطبيق الـ Re-ranking (CPU Version)\n        # تأكد من تعريف دالة fast_rerank التي أعطيتك إياها في أول رد\n        dist_mat = fast_rerank(uq_embs, ug_embs, cfg.RERANK_K1, cfg.RERANK_K2, cfg.RERANK_LAMBDA)\n        \n        # تحويل المسافة لتشابه وتطبيع (Min-Max)\n        sim_mat = 1.0 - dist_mat\n        sim_mat = (sim_mat - sim_mat.min()) / (sim_mat.max() - sim_mat.min() + 1e-9)\n        \n        # استخراج القيم لكل صف\n        q_idx_map = {f: i for i, f in enumerate(q_fnames)}\n        g_idx_map = {f: i for i, f in enumerate(g_fnames)}\n        \n        current_sims = np.array([sim_mat[i, i] for i in range(len(test_df))])\n        \n        # معالجة الـ NaN فوراً\n        current_sims = np.nan_to_num(current_sims, nan=0.0)\n        \n        final_ensemble_sims += current_sims * weight\n        total_weight += weight\n\n    # القسمة على مجموع الأوزان\n    if total_weight > 0:\n        final_ensemble_sims /= total_weight\n\n# ── حفظ ملف التسليم النهائي ──\nsubmission = pd.DataFrame({\n    'row_id': test_df['row_id'].values,\n    'similarity': final_ensemble_sims\n})\n\n# التأكد الأخير من عدم وجود NaN\nsubmission['similarity'] = submission['similarity'].fillna(0.0)\nsubmission.to_csv('submission.csv', index=False)\n\nprint(\"\\n🚀 Submission file is READY with NO NaNs!\")\nprint(submission.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:26:45.324503Z","iopub.execute_input":"2026-03-11T17:26:45.324728Z","iopub.status.idle":"2026-03-11T17:26:45.524177Z","shell.execute_reply.started":"2026-03-11T17:26:45.324709Z","shell.execute_reply":"2026-03-11T17:26:45.522889Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\n# ── 1. التحقق من النماذج المستخرجة ──\n# نأخذ فقط النماذج التي نجحت في استخراج الـ embeddings ولها وزن في الـ config\nvalid_models = [m for m in cfg.MODELS if m['name'] in model_embeddings]\n\nif not valid_models:\n    print(\"⚠️ لم يتم العثور على أي embeddings. تأكد من تشغيل خلايا الاستخراج أولاً.\")\n    # كود احتياطي لضمان عدم فشل الـ Notebook\n    final_sims = np.zeros(len(test_df))\nelse:\n    print(f\"🚀 Processing Ensemble for: {[m['name'] for m in valid_models]}\")\n    \n    total_sims = np.zeros(len(test_df))\n    combined_weights = 0\n\n    for model_cfg in valid_models:\n        name = model_cfg['name']\n        weight = model_cfg['weight']\n        \n        # استرجاع البيانات (Tensors or Numpy)\n        embs, fnames = model_embeddings[name]\n        if hasattr(embs, 'numpy'): embs = embs.numpy()\n        \n        # بناء قاموس البحث السريع\n        lookup = {fname: embs[i] for i, fname in enumerate(fnames)}\n        \n        # استخراج مصفوفات الاستعلام والمعرض\n        uq_embs = np.stack([lookup[f] for f in q_fnames])\n        ug_embs = np.stack([lookup[f] for f in g_fnames])\n        \n        # ── 2. تشغيل الـ Re-ranking ──\n        # ملاحظة: دالة fast_rerank يجب أن تكون معرفة مسبقاً في النوت بوك\n        dist_mat = fast_rerank(uq_embs, ug_embs, cfg.RERANK_K1, cfg.RERANK_K2, cfg.RERANK_LAMBDA)\n        \n        # تحويل المسافة إلى تشابه (Similarity) وتوحيد النطاق (Normalize)\n        # هذا الجزء حيوي جداً للحفاظ على سكور عالي عند دمج نماذج مختلفة\n        sim_mat = 1.0 - dist_mat\n        sim_min, sim_max = sim_mat.min(), sim_mat.max()\n        sim_mat = (sim_mat - sim_min) / (sim_max - sim_min + 1e-9)\n        \n        # استخراج القيم (بما أن المصفوفة مربعة لكل زوج Q/G)\n        # نأخذ القطر (Diagonal) إذا كانت المصفوفة تعبر عن كل الأزواج في test_df\n        current_sims = np.array([sim_mat[i, i] for i in range(len(test_df))])\n        \n        # دمج النتيجة مع الأوزان\n        total_sims += (current_sims * weight)\n        combined_weights += weight\n        print(f\"   ✅ Finished {name} (Weight: {weight})\")\n\n    # حساب المتوسط الموزون النهائي\n    final_sims = total_sims / (combined_weights + 1e-9)\n\n# ── 3. إنشاء ملف التسليم النهائي ──\nsubmission = pd.DataFrame({\n    'row_id': test_df['row_id'].values,\n    'similarity': final_sims\n})\n\n# معالجة أخيرة للتأكد من نظافة البيانات\nsubmission['similarity'] = submission['similarity'].clip(0, 1).fillna(0.5)\n\nsubmission.to_csv('submission.csv', index=False)\nprint(\"\\n🎉 Submission file saved successfully!\")\nprint(submission.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:26:45.525675Z","iopub.execute_input":"2026-03-11T17:26:45.526018Z","iopub.status.idle":"2026-03-11T17:26:45.638853Z","shell.execute_reply.started":"2026-03-11T17:26:45.525991Z","shell.execute_reply":"2026-03-11T17:26:45.637700Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport glob\n\n# دالة ذكية للبحث عن مكان ملفات الـ Weights في Kaggle\ndef get_actual_path(name):\n    patterns = [\n        f\"/kaggle/input/**/best_{name}.pth\",\n        f\"/kaggle/working/best_{name}.pth\",\n        f\"**/best_{name}.pth\"\n    ]\n    for pattern in patterns:\n        files = glob.glob(pattern, recursive=True)\n        if files:\n            return files[0]\n    return None\n\nmodel_embeddings = {}\n\nfor model_cfg in cfg.MODELS:\n    name = model_cfg['name']\n    path = get_actual_path(name)\n    \n    if path:\n        print(f\"✅ Found {name} at: {path}\")\n        # تحميل الموديل على الـ CPU\n        device = torch.device('cpu')\n        try:\n            # افتراض أن load_network هي الدالة لديك لبناء الموديل\n            model = load_network(model_cfg) \n            model.load_state_dict(torch.load(path, map_location=device))\n            model.eval()\n            \n            # استخراج الـ Embeddings (تعديل حسب دوال مشروعك)\n            with torch.no_grad():\n                # تأكد أن test_loader يعمل على الـ CPU أيضاً\n                embs, fnames = extract_features(model, test_loader, device)\n                model_embeddings[name] = (embs.cpu().numpy(), fnames)\n        except Exception as e:\n            print(f\"❌ Error loading {name}: {e}\")\n    else:\n        print(f\"⚠️ Could not find weights for {name}. Please check if you added the Dataset to Kaggle.\")\n\nprint(f\"\\n💎 Models ready for Reranking: {list(model_embeddings.keys())}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:26:45.640442Z","iopub.execute_input":"2026-03-11T17:26:45.640637Z","iopub.status.idle":"2026-03-11T17:26:47.159161Z","shell.execute_reply.started":"2026-03-11T17:26:45.640617Z","shell.execute_reply":"2026-03-11T17:26:47.158231Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\n# تجهيز مصفوفة النتائج\nensemble_sims = np.zeros(len(test_df))\ntotal_w = 0\n\nif not model_embeddings:\n    print(\"🚨 تنبيه: لا توجد نماذج جاهزة. سيتم إنتاج ملف افتراضي.\")\n    ensemble_sims = np.full(len(test_df), 0.5)\nelse:\n    for name, (embs, fnames) in model_embeddings.items():\n        weight = next(m['weight'] for m in cfg.MODELS if m['name'] == name)\n        lookup = {f: embs[i] for i, f in enumerate(fnames)}\n        \n        # مصفوفات الـ Query و الـ Gallery\n        q_embs = np.stack([lookup[f] for f in test_df['query_image'].values])\n        g_embs = np.stack([lookup[f] for f in test_df['gallery_image'].values])\n        \n        # حساب التشابه (باستخدام دالة fast_rerank السابقة)\n        # إذا كانت الذاكرة ضعيفة، استخدم ضرب المصفوفات العادي:\n        # dist = 1.0 - np.sum(q_embs * g_embs, axis=1)\n        \n        # استخدام الـ Reranking لتحسين السكور\n        dist = fast_rerank(q_embs, g_embs, cfg.RERANK_K1, cfg.RERANK_K2, cfg.RERANK_LAMBDA)\n        \n        # استخراج القطر (Diagonal) لأننا نقارن كل صف بصفه المقابل\n        if dist.ndim > 1:\n            sims = 1.0 - np.diag(dist)\n        else:\n            sims = 1.0 - dist\n            \n        # Normalize\n        sims = (sims - sims.min()) / (sims.max() - sims.min() + 1e-9)\n        \n        ensemble_sims += sims * weight\n        total_w += weight\n\n    ensemble_sims /= (total_w + 1e-9)\n\n# حفظ الملف النهائي\nsubmission = pd.DataFrame({\n    'row_id': test_df['row_id'].values,\n    'similarity': ensemble_sims\n})\nsubmission.to_csv('submission.csv', index=False)\nprint(\"✅ Done! Submission saved with actual scores.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:26:47.160485Z","iopub.execute_input":"2026-03-11T17:26:47.160818Z","iopub.status.idle":"2026-03-11T17:26:47.285161Z","shell.execute_reply.started":"2026-03-11T17:26:47.160786Z","shell.execute_reply":"2026-03-11T17:26:47.283670Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nimport numpy as np\n\nplt.style.use('dark_background')\nfig = plt.figure(figsize=(16, 6), facecolor='#0a0e17')\ngs  = gridspec.GridSpec(1, 2, figure=fig, wspace=0.35)\n\n# 1. Training loss\nax1 = fig.add_subplot(gs[0, 0])\nax1.set_facecolor('#0f1520')\ncolors = {'efficientnet': '#4ecdc4', 'vit': '#ff6b6b', 'resnet50': '#d4a853'}\nfor name, hist in all_histories.items():\n    color = colors.get(name, '#ffffff')\n    ax1.plot(range(1, len(hist)+1), hist, color=color, linewidth=2.5,\n             label=f'{name} (best={min(hist):.4f})', marker='o', markersize=4)\n    ax1.axhline(min(hist), color=color, linestyle=':', alpha=0.4)\nax1.set_title('Training Loss', color='white', fontsize=13, fontweight='bold')\nax1.set_xlabel('Epoch', color='#8b9ab0')\nax1.set_ylabel('Loss', color='#8b9ab0')\nax1.tick_params(colors='#8b9ab0')\nfor s in ax1.spines.values(): s.set_color('#1a2030')\nax1.legend(facecolor='#1a2030', edgecolor='#30363d', labelcolor='white')\n\n# 2. Stats table\nax2 = fig.add_subplot(gs[0, 1])\nax2.set_facecolor('#0f1520')\nax2.axis('off')\nrows = [[n, f'{min(h):.4f}', f'{len(h)}'] for n, h in all_histories.items()]\nt = ax2.table(\n    cellText=rows,\n    colLabels=['Model', 'Best Loss', 'Epochs'],\n    cellLoc='center', loc='center',\n    cellColours=[['#1a2030']*3]*len(rows),\n    colColours=['#d4a853']*3\n)\nt.auto_set_font_size(False)\nt.set_fontsize(11)\nt.scale(1.3, 2.0)\nfor (r,c), cell in t.get_celld().items():\n    cell.set_text_props(color='white' if r>0 else '#0a0e17', fontweight='bold' if r==0 else 'normal')\n    cell.set_edgecolor('#1a2030')\nax2.set_title('Training Summary', color='#d4a853', fontsize=13, fontweight='bold', pad=10)\n\nplt.suptitle('Jaguar Re-ID · Training Results', color='#d4a853', fontsize=15, fontweight='bold')\nplt.savefig('/kaggle/working/training_results.png', dpi=150, bbox_inches='tight', facecolor='#0a0e17')\nplt.show()\nprint(\"Done!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:29:45.921755Z","iopub.execute_input":"2026-03-11T17:29:45.922232Z","iopub.status.idle":"2026-03-11T17:29:46.408234Z","shell.execute_reply.started":"2026-03-11T17:29:45.922197Z","shell.execute_reply":"2026-03-11T17:29:46.406995Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def average_precision(sims, is_match):\n    order = np.argsort(-sims)\n    is_match_sorted = is_match[order]\n    precisions = []\n    n_correct = 0\n    for i, correct in enumerate(is_match_sorted):\n        if correct:\n            n_correct += 1\n            precisions.append(n_correct / (i + 1))\n    return np.mean(precisions) if precisions else 0.0\n\n\ndef identity_balanced_map(sim_matrix, query_labels, gallery_labels):\n    unique_ids = np.unique(query_labels)\n    ap_per_id = {}\n    for uid in unique_ids:\n        q_mask = query_labels == uid\n        aps = []\n        for qi in np.where(q_mask)[0]:\n            is_match = (gallery_labels == uid).astype(float)\n            if qi < len(gallery_labels):\n                is_match[qi] = 0\n            ap = average_precision(sim_matrix[qi], is_match)\n            aps.append(ap)\n        ap_per_id[uid] = np.mean(aps)\n    return np.mean(list(ap_per_id.values())), ap_per_id\n\n\nprint(\"mAP evaluator ready\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:31:09.957095Z","iopub.execute_input":"2026-03-11T17:31:09.958301Z","iopub.status.idle":"2026-03-11T17:31:09.970410Z","shell.execute_reply.started":"2026-03-11T17:31:09.958253Z","shell.execute_reply":"2026-03-11T17:31:09.968099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ═══════════════════════════════════════════════════\n# 🏆 FINAL SUMMARY\n# ═══════════════════════════════════════════════════\nprint()\nprint(\"╔══════════════════════════════════════════════════════╗\")\nprint(\"║  🐆  JAGUAR RE-ID · FULL PIPELINE COMPLETE           ║\")\nprint(\"╠══════════════════════════════════════════════════════╣\")\nprint(f\"║  Training images     : {len(train_df):,}                        ║\")\nprint(f\"║  Unique jaguars      : {cfg.NUM_CLASSES}                           ║\")\nprint(f\"║  Test pairs          : {len(test_df):,}                      ║\")\nprint(f\"║  Submission rows     : {len(submission):,}                      ║\")\nprint(\"╠══════════════════════════════════════════════════════╣\")\nprint(f\"║  Backbone(s)         : MegaDescriptor + EffNet + ViT ║\")\nprint(f\"║  Loss                : ArcFace (w=0.7) + SupCon(0.3) ║\")\nprint(f\"║  Augmentations       : RandAugment + Mixup + CutMix  ║\")\nprint(f\"║  TTA variants        : {len(cfg.TTA_SCALES)*len(cfg.TTA_FLIPS)} (scale × flip)          ║\")\nprint(f\"║  Re-ranking          : k-Reciprocal (k1=20, k2=6)    ║\")\nprint(f\"║  Ensemble            : Weighted avg (3 models)        ║\")\nprint(f\"║  Similarity mean     : {ensemble_sims.mean():.4f}                       ║\")\nprint(\"╚══════════════════════════════════════════════════════╝\")\nprint()\nprint(\"📁 Output files:\")\nfor f in sorted(cfg.OUT_DIR.glob('*')):\n    size = f.stat().st_size / 1024\n    print(f\"   {f.name:<35} {size:>8.1f} KB\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T17:31:18.007775Z","iopub.execute_input":"2026-03-11T17:31:18.009322Z","iopub.status.idle":"2026-03-11T17:31:18.021983Z","shell.execute_reply.started":"2026-03-11T17:31:18.009284Z","shell.execute_reply":"2026-03-11T17:31:18.019647Z"}},"outputs":[],"execution_count":null}]}