{"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":126777,"databundleVersionId":15314950,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Jaguar Re-Identification","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\nfrom pathlib import Path\n\n# 1. Path Configuration\nINPUT_DIR = Path('/kaggle/input/jaguar-re-id')\nTRAIN_DIR = INPUT_DIR / 'train'\n\n# 2. Load Data\ntrain_df = pd.read_csv(INPUT_DIR / 'train.csv')\n\n# 3. Explore Data Structure\nprint(\"--- Train Dataframe Head ---\")\nprint(train_df.head())\n\nprint(\"\\n--- Dataframe Info ---\")\nprint(train_df.info())\n\nprint(\"\\n--- Checking for Null Values ---\")\nprint(train_df.isnull().sum())\n\n# 4. Count unique jaguars from 'ground_truth' column\nunique_jaguars = train_df['ground_truth'].nunique()\nprint(f\"\\nUnique Jaguars (Classes): {unique_jaguars}\")\n\n# 5. Verify if files actually exist in the TRAIN_DIR\nsample_file = train_df.iloc[0]['filename']\nsample_path = os.path.join(TRAIN_DIR, sample_file)\nprint(f\"\\nChecking sample file: {sample_file}\")\nprint(f\"File exists: {os.path.exists(sample_path)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T07:17:21.678099Z","iopub.execute_input":"2026-02-11T07:17:21.678766Z","iopub.status.idle":"2026-02-11T07:17:21.995546Z","shell.execute_reply.started":"2026-02-11T07:17:21.678733Z","shell.execute_reply":"2026-02-11T07:17:21.994851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom pathlib import Path\n\n# 1. Automatic Path Discovery\n# Checking all directories in input to find the actual image folder\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    if 'train_0001.png' in filenames:\n        TRAIN_DIR = Path(dirname)\n        print(f\"Actual Train Directory Found: {TRAIN_DIR}\")\n        break\nelse:\n    # Defaulting to common structure if not found\n    TRAIN_DIR = Path('/kaggle/input/jaguar-re-id/train')\n    print(\"Defaulting to standard path. Please verify manually if it fails.\")\n\nINPUT_DIR = Path('/kaggle/input/jaguar-re-id')\ntrain_df = pd.read_csv(INPUT_DIR / 'train.csv')\n\n# 2. Augmentations (High-Res for patterns)\n# Rosette patterns remain consistent across life.\n# 384x384 resolution captures these fingerprints effectively.\ntransforms = A.Compose([\n    A.Resize(384, 384),\n    A.HorizontalFlip(p=0.5),\n    A.RandomBrightnessContrast(p=0.2),\n    A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\n# 3. Dataset Class with corrected columns\nclass JaguarDataset(Dataset):\n    def __init__(self, df, root_dir, transform=None):\n        self.df = df\n        self.root_dir = root_dir\n        self.transform = transform\n        # Mapping 'Abril', 'Akaloi' etc. to 0, 1, 2...\n        self.label_map = {name: i for i, name in enumerate(df['ground_truth'].unique())}\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 = os.path.join(self.root_dir, row['filename'])\n        \n        image = cv2.imread(img_path)\n        if image is None:\n            # Troubleshooting: Print the path if file is not found\n            # print(f\"Error: Could not read image at {img_path}\")\n            image = np.zeros((384, 384, 3), dtype=np.uint8)\n        else:\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        if self.transform:\n            image = self.transform(image=image)['image']\n            \n        label = self.label_map[row['ground_truth']]\n        return image, torch.tensor(label, dtype=torch.long)\n\n# 4. Initialization\ndataset = JaguarDataset(train_df, TRAIN_DIR, transform=transforms)\ntrain_loader = DataLoader(dataset, batch_size=16, shuffle=True, num_workers=4, pin_memory=True)\n\n# Final Check\nprint(f\"\\nStep 1 Complete!\")\nprint(f\"Total Unique Jaguar Classes: {len(dataset.label_map)}\")\nsample_img, sample_label = dataset[0]\nprint(f\"Sample Image Tensor Shape: {sample_img.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T07:17:21.997040Z","iopub.execute_input":"2026-02-11T07:17:21.997308Z","iopub.status.idle":"2026-02-11T07:17:29.644886Z","shell.execute_reply.started":"2026-02-11T07:17:21.997285Z","shell.execute_reply":"2026-02-11T07:17:29.644273Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import timm\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\n\n# 1. ArcFace Layer for Metric Learning\nclass ArcMarginProduct(nn.Module):\n    def __init__(self, in_features, out_features, s=30.0, m=0.50):\n        super(ArcMarginProduct, self).__init__()\n        self.in_features = in_features\n        self.out_features = out_features\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        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, input, label):\n        # Cosine similarity between input features and weights\n        cosine = F.linear(F.normalize(input), F.normalize(self.weight))\n        sine = torch.sqrt(1.0 - torch.pow(cosine, 2))\n        \n        # phi = cos(theta + m)\n        phi = cosine * self.cos_m - sine * self.sin_m\n        phi = torch.where(cosine > self.th, phi, cosine - self.mm)\n        \n        # One-hot encoding for targets\n        one_hot = torch.zeros(cosine.size(), device='cuda')\n        one_hot.scatter_(1, label.view(-1, 1).long(), 1)\n        \n        # Adding margin to the target class\n        output = (one_hot * phi) + ((1.0 - one_hot) * cosine)\n        output *= self.s\n        return output\n\n# 2. Main Model using ConvNeXt Backbone\nclass JaguarModel(nn.Module):\n    def __init__(self, model_name='convnext_tiny.fb_in22k', out_features=31, embedding_size=512):\n        super(JaguarModel, self).__init__()\n        # Using a pretrained model from ImageNet-22k for better feature extraction\n        self.backbone = timm.create_model(model_name, pretrained=True)\n        \n        # Adjusting the head\n        in_features = self.backbone.head.fc.in_features\n        self.backbone.head.fc = nn.Identity() \n\n        # Bottleneck layer to create a robust embedding\n        self.embedding = nn.Linear(in_features, embedding_size)\n        self.bn = nn.BatchNorm1d(embedding_size)\n        \n        # Final ArcFace head\n        self.arcface = ArcMarginProduct(embedding_size, out_features)\n\n    def forward(self, x, label=None):\n        features = self.backbone(x)\n        embedding = self.embedding(features)\n        embedding = self.bn(embedding)\n        \n        if label is not None:\n            return self.arcface(embedding, label)\n        \n        return F.normalize(embedding) # Normalized for inference similarity\n\n# 3. Model Initialization on GPU T4 x2\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel = JaguarModel(out_features=31).to(device)\n\n# Using DataParallel for utilizing both T4 GPUs\nif torch.cuda.device_count() > 1:\n    print(f\"Using {torch.cuda.device_count()} GPUs!\")\n    model = nn.DataParallel(model)\n\nprint(\"Step 2 Complete: Model Architecture is ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T07:17:29.645623Z","iopub.execute_input":"2026-02-11T07:17:29.645856Z","iopub.status.idle":"2026-02-11T07:17:39.222856Z","shell.execute_reply.started":"2026-02-11T07:17:29.645834Z","shell.execute_reply":"2026-02-11T07:17:39.222126Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.optim as optim\nfrom tqdm.auto import tqdm\n\n# 1. Hyperparameters\nEPOCHS = 15 # Start with 15 epochs\nLR = 1e-4\nWEIGHT_DECAY = 1e-6\n\n# 2. Loss Function & Optimizer\n# Using CrossEntropy on top of ArcFace outputs\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)\n\n# Learning Rate Scheduler - Important for fine-tuning\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\n\n# 3. Training Loop\nprint(f\"Starting Training for {EPOCHS} Epochs...\")\n\nfor epoch in range(EPOCHS):\n    model.train()\n    running_loss = 0.0\n    \n    # Progress bar for the current epoch\n    pbar = tqdm(enumerate(train_loader), total=len(train_loader), desc=f\"Epoch {epoch+1}/{EPOCHS}\")\n    \n    for i, (images, labels) in pbar:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        \n        # Forward pass through ArcFace\n        outputs = model(images, labels)\n        loss = criterion(outputs, labels)\n        \n        # Backward pass and optimization\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item()\n        pbar.set_postfix({'loss': running_loss / (i + 1)})\n    \n    scheduler.step()\n    \n    epoch_loss = running_loss / len(train_loader)\n    print(f\"Epoch [{epoch+1}/{EPOCHS}] Average Loss: {epoch_loss:.4f}\")\n\n# 4. Save the model weights\ntorch.save(model.state_dict(), 'jaguar_convnext_arcface.pth')\nprint(\"Model saved as jaguar_convnext_arcface.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T07:17:39.224529Z","iopub.execute_input":"2026-02-11T07:17:39.224850Z","iopub.status.idle":"2026-02-11T09:09:11.972511Z","shell.execute_reply.started":"2026-02-11T07:17:39.224827Z","shell.execute_reply":"2026-02-11T09:09:11.971761Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F\nimport pandas as pd\nimport numpy as np\nimport os\nimport cv2\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.auto import tqdm\nfrom pathlib import Path\n\nINPUT_DIR = Path('/kaggle/input/jaguar-re-id')\ntest_df = pd.read_csv(INPUT_DIR / 'test.csv')\n\n# ২. Test Transforms (No augmentation, only normalization)\ntest_transforms = A.Compose([\n    A.Resize(384, 384),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\n# ৩. Extract Embeddings for all 371 Test Images\nmodel.eval()\n# Query and Gallery make a image galary\ntest_images = sorted(list(set(test_df['query_image']) | set(test_df['gallery_image'])))\nembeddings_dict = {}\n\nprint(f\"Extracting embeddings for {len(test_images)} unique test images...\")\n\nwith torch.no_grad():\n    for img_name in tqdm(test_images):\n    \n        img_path = os.path.join(INPUT_DIR / 'test/test', img_name)\n        \n        image = cv2.imread(img_path)\n        if image is None:\n            image = np.zeros((384, 384, 3), dtype=np.uint8)\n        else:\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            \n        image = test_transforms(image=image)['image'].unsqueeze(0).to(device)\n        \n        # find embreading ( normalized for similarity )\n        embedding = model(image)\n        embeddings_dict[img_name] = embedding.squeeze().cpu().numpy()\n\n# ৪. Compute Similarities for Submission\nprint(\"Computing pairwise similarities for submission...\")\nsimilarities = []\n\nfor _, row in tqdm(test_df.iterrows(), total=len(test_df)):\n    q_emb = embeddings_dict[row['query_image']]\n    g_emb = embeddings_dict[row['gallery_image']]\n    \n    # Cosine Similarity: Dot product of normalized vectors\n    dot_product = np.dot(q_emb, g_emb)\n    \n    # Mapping [-1, 1] range to [0, 1] as per competition rules\n    similarity = (dot_product + 1) / 2\n    similarities.append(similarity)\n\n# ৫. Create Final Submission File\nsubmission = pd.DataFrame({\n    'row_id': test_df['row_id'],\n    'similarity': similarities\n})\n\nsubmission.to_csv('submission.csv', index=False)\nprint(\"Step 4 Complete: submission.csv saved successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T09:09:11.974329Z","iopub.execute_input":"2026-02-11T09:09:11.974573Z","iopub.status.idle":"2026-02-11T09:11:26.904011Z","shell.execute_reply.started":"2026-02-11T09:09:11.974546Z","shell.execute_reply":"2026-02-11T09:11:26.902041Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nsub = pd.read_csv('submission.csv')\nprint(sub.head())\nprint(f\"Total rows: {len(sub)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T09:11:26.913682Z","iopub.execute_input":"2026-02-11T09:11:26.913965Z","iopub.status.idle":"2026-02-11T09:11:26.961777Z","shell.execute_reply.started":"2026-02-11T09:11:26.913942Z","shell.execute_reply":"2026-02-11T09:11:26.960754Z"}},"outputs":[],"execution_count":null}]}