{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":127282,"databundleVersionId":15520105}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"cell-0","cell_type":"markdown","source":"# AnimalCLEF2026: Improved Baseline\n\nImproved version of the official starter notebook. Key changes:\n- **MiewID for all species** \n- **HDBSCAN** instead of DBSCAN (eliminates fragile `eps` tuning)\n- **Fixed distance metric** (`1 - cosine_similarity` instead of lossy conversion)\n- **Aspect-ratio-preserving resize** (prevents distortion of discriminative markings)\n- **Test-Time Augmentation** (horizontal flip + multi-view averaging)","metadata":{}},{"id":"cell-1","cell_type":"markdown","source":"## Dependencies","metadata":{}},{"id":"cell-2","cell_type":"code","source":"!uv pip install transformers==4.45.2 wildlife-datasets git+https://github.com/WildlifeDatasets/wildlife-tools hdbscan huggingface_hub         ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:16:59.018608Z","iopub.execute_input":"2026-03-22T18:16:59.018910Z","iopub.status.idle":"2026-03-22T18:16:59.704445Z","shell.execute_reply.started":"2026-03-22T18:16:59.018875Z","shell.execute_reply":"2026-03-22T18:16:59.703546Z"}},"outputs":[],"execution_count":null},{"id":"54183e69","cell_type":"code","source":"from huggingface_hub import hf_hub_download\nimport json\n\npath = hf_hub_download(\"conservationxlabs/miewid-msv3\", \"config.json\")\nwith open(path) as f:\n    cfg = json.load(f)\nprint(cfg.get(\"transformers_version\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:16:59.710625Z","iopub.execute_input":"2026-03-22T18:16:59.711280Z","iopub.status.idle":"2026-03-22T18:17:00.113050Z","shell.execute_reply.started":"2026-03-22T18:16:59.711235Z","shell.execute_reply":"2026-03-22T18:17:00.112238Z"}},"outputs":[],"execution_count":null},{"id":"cell-3","cell_type":"markdown","source":"## Imports","metadata":{}},{"id":"cell-4","cell_type":"code","source":"import warnings\nwarnings.filterwarnings(\"ignore\")\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport torchvision.transforms as T\nimport hdbscan\nfrom transformers import AutoModel\nfrom wildlife_datasets.datasets import AnimalCLEF2026\nfrom wildlife_tools.features import DeepFeatures\nfrom wildlife_tools.similarity import CosineSimilarity","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:17:00.114177Z","iopub.execute_input":"2026-03-22T18:17:00.114522Z","iopub.status.idle":"2026-03-22T18:17:06.929926Z","shell.execute_reply.started":"2026-03-22T18:17:00.114485Z","shell.execute_reply":"2026-03-22T18:17:06.929004Z"}},"outputs":[],"execution_count":null},{"id":"cell-5","cell_type":"markdown","source":"## Load and visualize data","metadata":{}},{"id":"cell-6","cell_type":"code","source":"root = '/kaggle/input/competitions/animal-clef-2026'\ndataset_full = AnimalCLEF2026(\n    root,\n    transform=None,\n    load_label=True,\n    factorize_label=True,\n    check_files=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:17:06.931065Z","iopub.execute_input":"2026-03-22T18:17:06.931827Z","iopub.status.idle":"2026-03-22T18:17:06.991864Z","shell.execute_reply.started":"2026-03-22T18:17:06.931784Z","shell.execute_reply":"2026-03-22T18:17:06.991186Z"}},"outputs":[],"execution_count":null},{"id":"cell-7","cell_type":"code","source":"dataset_full.metadata.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:17:06.992774Z","iopub.execute_input":"2026-03-22T18:17:06.993092Z","iopub.status.idle":"2026-03-22T18:17:07.025221Z","shell.execute_reply.started":"2026-03-22T18:17:06.993050Z","shell.execute_reply":"2026-03-22T18:17:07.024403Z"}},"outputs":[],"execution_count":null},{"id":"cell-8","cell_type":"code","source":"dataset_full.metadata[['dataset', 'split']].value_counts(sort=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:17:07.026387Z","iopub.execute_input":"2026-03-22T18:17:07.026898Z","iopub.status.idle":"2026-03-22T18:17:07.041726Z","shell.execute_reply.started":"2026-03-22T18:17:07.026873Z","shell.execute_reply":"2026-03-22T18:17:07.041009Z"}},"outputs":[],"execution_count":null},{"id":"cell-9","cell_type":"code","source":"dataset_full = dataset_full.get_subset(dataset_full.df['split'] == 'test')\n\ndatasets = {}\nfor name in dataset_full.metadata['dataset'].unique():\n    datasets[name] = dataset_full.get_subset(dataset_full.df['dataset'] == name)\ndatasets","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:17:07.042806Z","iopub.execute_input":"2026-03-22T18:17:07.043157Z","iopub.status.idle":"2026-03-22T18:17:07.062660Z","shell.execute_reply.started":"2026-03-22T18:17:07.043131Z","shell.execute_reply":"2026-03-22T18:17:07.062067Z"}},"outputs":[],"execution_count":null},{"id":"cell-10","cell_type":"code","source":"for dataset in datasets.values():\n    dataset.plot_grid(n_rows=3, n_cols=4, rotate=False);","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:17:07.063567Z","iopub.execute_input":"2026-03-22T18:17:07.063867Z","iopub.status.idle":"2026-03-22T18:17:10.189927Z","shell.execute_reply.started":"2026-03-22T18:17:07.063844Z","shell.execute_reply":"2026-03-22T18:17:10.189109Z"}},"outputs":[],"execution_count":null},{"id":"cell-11","cell_type":"markdown","source":"## Feature extraction with MiewID + TTA\n\nWe use [MiewID](https://huggingface.co/conservationxlabs/miewid-msv3) for **all species**. MiewID is a strong general-purpose wildlife Re-ID model trained on diverse species.\n\nWe apply Test-Time Augmentation (horizontal flip) and average the embeddings for a more robust representation.","metadata":{}},{"id":"cell-12","cell_type":"code","source":"device = 'cuda'\nbatch_size = 32\nsize = 512\n\n# Load MiewID once for all datasets\nmodel_tag = f\"conservationxlabs/miewid-msv3\"\nmodel = AutoModel.from_pretrained(model_tag, trust_remote_code=True)\n\nextractor = DeepFeatures(model=model, device=device, batch_size=batch_size)\nmatcher = CosineSimilarity()\n\n# Aspect-ratio-preserving resize + center crop\nbase_transform = T.Compose([\n    T.Resize(size, antialias=True),\n    T.CenterCrop(size),\n    T.ToTensor(),\n    T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n])\n\n# TTA: original + horizontal flip\nflip_transform = T.Compose([\n    T.RandomHorizontalFlip(p=1.0),\n    T.Resize(size, antialias=True),\n    T.CenterCrop(size),\n    T.ToTensor(),\n    T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n])\n\ntta_transforms = [base_transform, flip_transform]\n\nsimilarities = {}\nfor name, dataset in datasets.items():\n    print(f'Extracting features for {name} ({len(dataset)} images)...')\n    all_features = []\n    for t in tta_transforms:\n        dataset.set_transform(t)\n        feats = extractor(dataset)\n        all_features.append(feats.features)\n\n    # Average TTA embeddings and L2-normalize\n    avg_features = np.mean(all_features, axis=0)\n    avg_features = avg_features / np.linalg.norm(avg_features, axis=1, keepdims=True)\n    feats.features = avg_features\n\n    # Compute similarity matrix\n    similarity = matcher(feats, feats)\n    similarities[name] = similarity\n    print(f'  Features: {avg_features.shape}, Similarity: {similarity.shape}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:17:10.190954Z","iopub.execute_input":"2026-03-22T18:17:10.191353Z","iopub.status.idle":"2026-03-22T18:21:04.736741Z","shell.execute_reply.started":"2026-03-22T18:17:10.191328Z","shell.execute_reply":"2026-03-22T18:21:04.735710Z"}},"outputs":[],"execution_count":null},{"id":"cell-13","cell_type":"markdown","source":"## Clustering with HDBSCAN\n\nHDBSCAN automatically determines cluster density thresholds, no fragile `eps` parameter to tune. We use `min_cluster_size=2` to allow small clusters (animals with only 2 photos) and `min_samples=1` to recover singletons.","metadata":{}},{"id":"cell-14","cell_type":"code","source":"def relabel_negatives(labels):\n    \"\"\"Assign unclustered points (-1) to their own singleton clusters.\"\"\"\n    labels = np.array(labels)\n    neg_indices = np.where(labels == -1)[0]\n    new_labels = np.arange(labels.max() + 1, labels.max() + 1 + len(neg_indices))\n    labels[neg_indices] = new_labels\n    return labels\n\n\ndef run_clustering(similarity, min_cluster_size=2, min_samples=1):\n    \"\"\"Cluster using HDBSCAN on cosine distance matrix.\"\"\"\n    distance = (1.0 - similarity).astype(np.float64)\n    np.fill_diagonal(distance, 0.0)\n    # Clamp small negatives from floating point noise\n    distance = np.maximum(distance, 0.0)\n\n    clustering = hdbscan.HDBSCAN(\n        metric='precomputed',\n        min_cluster_size=min_cluster_size,\n        min_samples=min_samples,\n    )\n    labels = clustering.fit_predict(distance)\n    return relabel_negatives(labels)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:21:04.738348Z","iopub.execute_input":"2026-03-22T18:21:04.739731Z","iopub.status.idle":"2026-03-22T18:21:04.746299Z","shell.execute_reply.started":"2026-03-22T18:21:04.739699Z","shell.execute_reply":"2026-03-22T18:21:04.745592Z"}},"outputs":[],"execution_count":null},{"id":"cell-15","cell_type":"code","source":"# Per-dataset HDBSCAN parameters\n# min_cluster_size: minimum number of images to form a cluster\n# min_samples: controls how conservative clustering is (lower = more clusters)\ncluster_params = {\n    'LynxID2025':         {'min_cluster_size': 2, 'min_samples': 1},\n    'SalamanderID2025':   {'min_cluster_size': 2, 'min_samples': 1},\n    'SeaTurtleID2022':    {'min_cluster_size': 2, 'min_samples': 1},\n    'TexasHornedLizards': {'min_cluster_size': 2, 'min_samples': 1},\n}\n\nresults = None\nfor name, similarity in similarities.items():\n    params = cluster_params[name]\n    clusters = run_clustering(similarity, **params)\n    n_clusters = len(np.unique(clusters))\n    print(f'{name}: {n_clusters} clusters from {similarity.shape[0]} images')\n\n    result = pd.DataFrame({\n        'image_id': datasets[name].metadata['image_id'],\n        'cluster': [f'cluster_{name}_{c}' for c in clusters]\n    })\n    results = pd.concat((results, result))\n\nresults.to_csv('submission.csv', index=False)\nprint(f'\\nTotal: {len(results)} images, submission saved to submission.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:21:04.749168Z","iopub.execute_input":"2026-03-22T18:21:04.749506Z","iopub.status.idle":"2026-03-22T18:21:04.914065Z","shell.execute_reply.started":"2026-03-22T18:21:04.749483Z","shell.execute_reply":"2026-03-22T18:21:04.913131Z"}},"outputs":[],"execution_count":null},{"id":"cell-16","cell_type":"markdown","source":"## Visualize clustering results","metadata":{}},{"id":"cell-17","cell_type":"code","source":"# Visualize the largest clusters for the last dataset\nname = list(datasets.keys())[-1]\ndataset = datasets[name]\ndataset.set_transform(None)\nsimilarity = similarities[name]\nclusters = run_clustering(similarity, **cluster_params[name])\n\nn_plot = 5\ni_plot = pd.Series(clusters).value_counts().index\nfor i in i_plot[:n_plot]:\n    dataset.plot_grid(idx=clusters == i, n_cols=3, img_min=500, rotate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:21:04.915120Z","iopub.execute_input":"2026-03-22T18:21:04.915424Z","iopub.status.idle":"2026-03-22T18:21:07.019676Z","shell.execute_reply.started":"2026-03-22T18:21:04.915399Z","shell.execute_reply":"2026-03-22T18:21:07.019083Z"}},"outputs":[],"execution_count":null},{"id":"cell-18","cell_type":"markdown","source":"## Tuning: sweep `min_cluster_size`\n\nUnlike DBSCAN's `eps`, HDBSCAN's `min_cluster_size` is much more interpretable, it's the smallest group of images you'd consider a valid individual. We can still sweep it to see the effect.","metadata":{}},{"id":"cell-19","cell_type":"code","source":"mcs_values = range(2, 20)\nn_clusters_list = []\nfor mcs in mcs_values:\n    c = run_clustering(similarity, min_cluster_size=mcs, min_samples=1)\n    n_clusters_list.append(len(np.unique(c)))\n\nplt.plot(list(mcs_values), n_clusters_list, 'o-')\nplt.xlabel('min_cluster_size')\nplt.ylabel('number of clusters')\nplt.title(f'{name}: cluster count vs min_cluster_size')\nplt.grid(True, alpha=0.3);","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-22T18:21:07.020699Z","iopub.execute_input":"2026-03-22T18:21:07.021276Z","iopub.status.idle":"2026-03-22T18:21:07.732617Z","shell.execute_reply.started":"2026-03-22T18:21:07.021249Z","shell.execute_reply":"2026-03-22T18:21:07.731871Z"}},"outputs":[],"execution_count":null},{"id":"cell-20","cell_type":"markdown","source":"## Possible further improvements\n\n- **Fine-tune MiewID** with ArcFace loss on the SeaTurtleID2022 train split (identity labels available)\n- **k-Reciprocal Re-ranking** on the distance matrix before clustering (3-8% boost in Re-ID benchmarks)\n- **PCA whitening** on features before computing similarity (removes correlated noise dimensions)\n- **Model ensembling**: combine MiewID with DINOv2 features (concatenate or average after L2-norm)\n- **More TTA views**: add rotations (e.g. +/-15 degrees) for underwater turtles where orientation varies","metadata":{}}]}