{
  "id": 670261,
  "title": "Jaguar Re-ID Model Improvement Plan",
  "url": "/competitions/jaguar-re-id/discussion/670261",
  "author_name": "",
  "post_date": "2026-01-27T04:11:12.385617300Z",
  "votes": 1,
  "comment_count": 3,
  "views": 0,
  "content": "<p>Hi everyone,</p>\n<p>I wanted to share some findings from my recent experiments. Like many of you, I hit a wall early on with standard ResNet baselines and ran into some frustrating AssertionError crashes when trying to scale up data loading.</p>\n<p>Here is a breakdown of the pipeline that stabilized my training and significantly boosted my LB score.</p>\n<ol>\n<li>The \"Child Process\" Crash Fix</li>\n</ol>\n<p>If you are seeing AssertionError: can only test a child process or random hangs during training (especially after 1-2 epochs), this is likely due to the Kaggle/Docker environment handling multiprocessing poorly with PyTorch DataLoader.</p>\n<p>The Fix:\nSet num_workers=0 in your DataLoader.</p>\n<p>train_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=0)</p>\n<p>Trade-off: This moves data loading to the main process, which can be slower.</p>\n<p>Compensation: I offset the speed loss by using ConvNeXt-Small (instead of Base) and resizing images to 256x256. This keeps the GPU saturated even with a single worker.</p>\n<ol>\n<li>Model Choice: Why ConvNeXt?</li>\n</ol>\n<p>For Jaguar ReID, we rely on fine-grained texture details (rosettes).</p>\n<p>ConvNeXt models tend to perform better on texture-heavy tasks compared to ViTs (which crave massive datasets) or ResNets (which are older).</p>\n<p>Resolution: 256x256 seems to be the sweet spot for speed/accuracy. If you have the compute time, 384x384 definitely captures more unique rosette patterns.</p>\n<ol>\n<li>The \"Secret Sauce\": Post-Processing</li>\n</ol>\n<p>Training a good model is only half the battle in ReID. The similarity matrix calculation is where you can squeeze out extra performance.</p>\n<p>A. Test Time Augmentation (TTA):\nI extract features for the original image and the horizontal flip, then average them. This makes the embeddings robust to orientation.</p>\n<p>f1 = model(img)\nf2 = model(torch.flip(img, [3]))\nfeature = (f1 + f2) / 2</p>\n<p>B. Vectorized Jaccard Reranking:\nStandard Cosine Similarity is good, but k-Reciprocal Reranking is better. It asks: \"I think Image B is similar to Image A, but does Image B also think Image A is similar to it?\"</p>\n<p>Here is a fast, vectorized implementation of Jaccard similarity to mix with your cosine scores (I use a 70/30 split):</p>\n<p>def compute_jaccard_similarity(features, k1=20):\n    # Normalize features\n    features = F.normalize(torch.tensor(features), dim=1)</p>\n<pre><code># Cosine Sim\nsim_mat = features @ features.T\n\n# Get top k1 neighbors\nn = len(features)\n_, indices = torch.topk(sim_mat, k=k1, dim=1)\nindices = indices.numpy()\n\n# Create neighbor mask\nneighbor_mask = np.zeros((n, n), dtype=np.float32)\nfor i in range(n):\n    neighbor_mask[i, indices[i]] = 1.0\n\n# Jaccard = Intersection / Union\nintersection = neighbor_mask @ neighbor_mask.T\nunion = neighbor_mask.sum(1, keepdims=True) + neighbor_mask.sum(1, keepdims=True).T - intersection\n\nreturn intersection / (union + 1e-6)\n</code></pre>\n<p>Summary of My Pipeline</p>\n<p>Backbone: convnext_small.fb_in1k</p>\n<p>Loss: ArcFace (Margin=0.5, Scale=30)</p>\n<p>Optimizer: AdamW + CosineAnnealingLR</p>\n<p>Inference: TTA + (0.7 * Cosine + 0.3 * Jaccard)</p>\n<p>Hope this helps anyone stuck on the technical errors or looking for that extra accuracy boost! Good lu</p>",
  "messages": [
    {
      "id": "3397350",
      "postDate": "01/27/2026 04:11:12",
      "content": "<p>Hi everyone,</p>\n<p>I wanted to share some findings from my recent experiments. Like many of you, I hit a wall early on with standard ResNet baselines and ran into some frustrating AssertionError crashes when trying to scale up data loading.</p>\n<p>Here is a breakdown of the pipeline that stabilized my training and significantly boosted my LB score.</p>\n<ol>\n<li>The \"Child Process\" Crash Fix</li>\n</ol>\n<p>If you are seeing AssertionError: can only test a child process or random hangs during training (especially after 1-2 epochs), this is likely due to the Kaggle/Docker environment handling multiprocessing poorly with PyTorch DataLoader.</p>\n<p>The Fix:\nSet num_workers=0 in your DataLoader.</p>\n<p>train_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=0)</p>\n<p>Trade-off: This moves data loading to the main process, which can be slower.</p>\n<p>Compensation: I offset the speed loss by using ConvNeXt-Small (instead of Base) and resizing images to 256x256. This keeps the GPU saturated even with a single worker.</p>\n<ol>\n<li>Model Choice: Why ConvNeXt?</li>\n</ol>\n<p>For Jaguar ReID, we rely on fine-grained texture details (rosettes).</p>\n<p>ConvNeXt models tend to perform better on texture-heavy tasks compared to ViTs (which crave massive datasets) or ResNets (which are older).</p>\n<p>Resolution: 256x256 seems to be the sweet spot for speed/accuracy. If you have the compute time, 384x384 definitely captures more unique rosette patterns.</p>\n<ol>\n<li>The \"Secret Sauce\": Post-Processing</li>\n</ol>\n<p>Training a good model is only half the battle in ReID. The similarity matrix calculation is where you can squeeze out extra performance.</p>\n<p>A. Test Time Augmentation (TTA):\nI extract features for the original image and the horizontal flip, then average them. This makes the embeddings robust to orientation.</p>\n<p>f1 = model(img)\nf2 = model(torch.flip(img, [3]))\nfeature = (f1 + f2) / 2</p>\n<p>B. Vectorized Jaccard Reranking:\nStandard Cosine Similarity is good, but k-Reciprocal Reranking is better. It asks: \"I think Image B is similar to Image A, but does Image B also think Image A is similar to it?\"</p>\n<p>Here is a fast, vectorized implementation of Jaccard similarity to mix with your cosine scores (I use a 70/30 split):</p>\n<p>def compute_jaccard_similarity(features, k1=20):\n    # Normalize features\n    features = F.normalize(torch.tensor(features), dim=1)</p>\n<pre><code># Cosine Sim\nsim_mat = features @ features.T\n\n# Get top k1 neighbors\nn = len(features)\n_, indices = torch.topk(sim_mat, k=k1, dim=1)\nindices = indices.numpy()\n\n# Create neighbor mask\nneighbor_mask = np.zeros((n, n), dtype=np.float32)\nfor i in range(n):\n    neighbor_mask[i, indices[i]] = 1.0\n\n# Jaccard = Intersection / Union\nintersection = neighbor_mask @ neighbor_mask.T\nunion = neighbor_mask.sum(1, keepdims=True) + neighbor_mask.sum(1, keepdims=True).T - intersection\n\nreturn intersection / (union + 1e-6)\n</code></pre>\n<p>Summary of My Pipeline</p>\n<p>Backbone: convnext_small.fb_in1k</p>\n<p>Loss: ArcFace (Margin=0.5, Scale=30)</p>\n<p>Optimizer: AdamW + CosineAnnealingLR</p>\n<p>Inference: TTA + (0.7 * Cosine + 0.3 * Jaccard)</p>\n<p>Hope this helps anyone stuck on the technical errors or looking for that extra accuracy boost! Good lu</p>",
      "rawMarkdown": "Hi everyone,\n\nI wanted to share some findings from my recent experiments. Like many of you, I hit a wall early on with standard ResNet baselines and ran into some frustrating AssertionError crashes when trying to scale up data loading.\n\nHere is a breakdown of the pipeline that stabilized my training and significantly boosted my LB score.\n\n1. The \"Child Process\" Crash Fix\n\nIf you are seeing AssertionError: can only test a child process or random hangs during training (especially after 1-2 epochs), this is likely due to the Kaggle/Docker environment handling multiprocessing poorly with PyTorch DataLoader.\n\nThe Fix:\nSet num_workers=0 in your DataLoader.\n\ntrain_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=0)\n\n\nTrade-off: This moves data loading to the main process, which can be slower.\n\nCompensation: I offset the speed loss by using ConvNeXt-Small (instead of Base) and resizing images to 256x256. This keeps the GPU saturated even with a single worker.\n\n2. Model Choice: Why ConvNeXt?\n\nFor Jaguar ReID, we rely on fine-grained texture details (rosettes).\n\nConvNeXt models tend to perform better on texture-heavy tasks compared to ViTs (which crave massive datasets) or ResNets (which are older).\n\nResolution: 256x256 seems to be the sweet spot for speed/accuracy. If you have the compute time, 384x384 definitely captures more unique rosette patterns.\n\n3. The \"Secret Sauce\": Post-Processing\n\nTraining a good model is only half the battle in ReID. The similarity matrix calculation is where you can squeeze out extra performance.\n\nA. Test Time Augmentation (TTA):\nI extract features for the original image and the horizontal flip, then average them. This makes the embeddings robust to orientation.\n\nf1 = model(img)\nf2 = model(torch.flip(img, [3]))\nfeature = (f1 + f2) / 2\n\n\nB. Vectorized Jaccard Reranking:\nStandard Cosine Similarity is good, but k-Reciprocal Reranking is better. It asks: \"I think Image B is similar to Image A, but does Image B also think Image A is similar to it?\"\n\nHere is a fast, vectorized implementation of Jaccard similarity to mix with your cosine scores (I use a 70/30 split):\n\ndef compute_jaccard_similarity(features, k1=20):\n    # Normalize features\n    features = F.normalize(torch.tensor(features), dim=1)\n    \n    # Cosine Sim\n    sim_mat = features @ features.T\n    \n    # Get top k1 neighbors\n    n = len(features)\n    _, indices = torch.topk(sim_mat, k=k1, dim=1)\n    indices = indices.numpy()\n    \n    # Create neighbor mask\n    neighbor_mask = np.zeros((n, n), dtype=np.float32)\n    for i in range(n):\n        neighbor_mask[i, indices[i]] = 1.0\n        \n    # Jaccard = Intersection / Union\n    intersection = neighbor_mask @ neighbor_mask.T\n    union = neighbor_mask.sum(1, keepdims=True) + neighbor_mask.sum(1, keepdims=True).T - intersection\n    \n    return intersection / (union + 1e-6)\n\n\nSummary of My Pipeline\n\nBackbone: convnext_small.fb_in1k\n\nLoss: ArcFace (Margin=0.5, Scale=30)\n\nOptimizer: AdamW + CosineAnnealingLR\n\nInference: TTA + (0.7 * Cosine + 0.3 * Jaccard)\n\nHope this helps anyone stuck on the technical errors or looking for that extra accuracy boost! Good lu",
      "votes": null
    },
    {
      "id": "3399185",
      "postDate": "01/30/2026 11:02:21",
      "content": "<p>Hi! Nice summary and good overall tips. </p>\n<p>Note that jaguars have distinct patterns on their right and left sides. </p>\n<p>This means that performance gains that come from the left-right random augmentation come from learning the background features or the silhouette and pose. These are spurious correlations. <img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F371981%2Fba54b619a9b75265a008102d24f38b33%2Fidentifying_a_jaguar.png?generation=1769770488226967&amp;alt=media\" alt=\"\"></p>",
      "rawMarkdown": "Hi! Nice summary and good overall tips. \n\nNote that jaguars have distinct patterns on their right and left sides. \n\nThis means that performance gains that come from the left-right random augmentation come from learning the background features or the silhouette and pose. These are spurious correlations. ![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F371981%2Fba54b619a9b75265a008102d24f38b33%2Fidentifying_a_jaguar.png?generation=1769770488226967&alt=media)",
      "votes": null
    },
    {
      "id": "3401013",
      "postDate": "02/02/2026 18:45:07",
      "content": "<p>That’s a fair point. If the model sees a flipped right-side image and matches it to a left-side image based purely on silhouette, then it is effectively bypassing the real biological identifier, which is the rosette pattern. In that case, the network is learning a shortcut instead of learning what actually matters.</p>\n<p>Because the goal is to deploy this system for real-world conservation work, I am pivoting the pipeline to explicitly enforce biological consistency and remove those spurious correlations.</p>\n<p>First, I am introducing side-aware labeling. Rather than assigning a single identity per jaguar, I split each individual into two distinct identities based on body side, left and right. This forces the model to treat each side as its own visual barcode and prevents it from collapsing mirrored patterns into the same embedding.</p>\n<p>Second, I am restricting data augmentation. Horizontal flips are disabled entirely. Instead, I rely on random cropping, elastic transformations, and color jitter to simulate variation in camera distance, pose, and lighting. These augmentations preserve anatomical realism while still improving robustness.</p>\n<p>Third, I am adding a spatial focus mechanism through a global–local attention branch. By encouraging the model to attend to localized patches of rosettes, the network is pushed to prioritize fine-grained texture over coarse body shape or silhouette.</p>\n<p>To implement this, I construct side-aware identities directly in the training data by combining the individual ID with the recorded view.</p>\n<p>import pandas as pd</p>\n<p>def create_side_aware_labels(df):\n    df['side_aware_id'] = df['individual_id'] + \"_\" + df['view']\n    return df</p>\n<p>train_df = pd.read_csv('train.csv')\ntrain_df = create_side_aware_labels(train_df)</p>\n<p>With this setup, the ArcFace loss explicitly separates embeddings for something like Aju Left and Aju Right into different regions of the embedding space. At inference time, Jaccard re-ranking can be used to observe whether left and right identities consistently co-occur within the same camera trap sequences. This allows cross-side associations to be recovered after training, without contaminating the learning signal itself. </p>",
      "rawMarkdown": "That’s a fair point. If the model sees a flipped right-side image and matches it to a left-side image based purely on silhouette, then it is effectively bypassing the real biological identifier, which is the rosette pattern. In that case, the network is learning a shortcut instead of learning what actually matters.\n\nBecause the goal is to deploy this system for real-world conservation work, I am pivoting the pipeline to explicitly enforce biological consistency and remove those spurious correlations.\n\nFirst, I am introducing side-aware labeling. Rather than assigning a single identity per jaguar, I split each individual into two distinct identities based on body side, left and right. This forces the model to treat each side as its own visual barcode and prevents it from collapsing mirrored patterns into the same embedding.\n\nSecond, I am restricting data augmentation. Horizontal flips are disabled entirely. Instead, I rely on random cropping, elastic transformations, and color jitter to simulate variation in camera distance, pose, and lighting. These augmentations preserve anatomical realism while still improving robustness.\n\nThird, I am adding a spatial focus mechanism through a global–local attention branch. By encouraging the model to attend to localized patches of rosettes, the network is pushed to prioritize fine-grained texture over coarse body shape or silhouette.\n\nTo implement this, I construct side-aware identities directly in the training data by combining the individual ID with the recorded view.\n\nimport pandas as pd\n\ndef create_side_aware_labels(df):\n    df['side_aware_id'] = df['individual_id'] + \"_\" + df['view']\n    return df\n\ntrain_df = pd.read_csv('train.csv')\ntrain_df = create_side_aware_labels(train_df)\n\n\nWith this setup, the ArcFace loss explicitly separates embeddings for something like Aju Left and Aju Right into different regions of the embedding space. At inference time, Jaccard re-ranking can be used to observe whether left and right identities consistently co-occur within the same camera trap sequences. This allows cross-side associations to be recovered after training, without contaminating the learning signal itself.",
      "votes": null
    },
    {
      "id": "3420825",
      "postDate": "03/14/2026 01:16:20",
      "content": "<p>Hi,\nThank you, reading your post was helpful.</p>",
      "rawMarkdown": "Hi,\nThank you, reading your post was helpful.",
      "votes": null
    }
  ],
  "comments": [
    {
      "id": 3399185,
      "author_name": "andandand",
      "author_url": "",
      "post_date": "01/30/2026 11:02:21",
      "content": "<p>Hi! Nice summary and good overall tips. </p>\n<p>Note that jaguars have distinct patterns on their right and left sides. </p>\n<p>This means that performance gains that come from the left-right random augmentation come from learning the background features or the silhouette and pose. These are spurious correlations. <img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F371981%2Fba54b619a9b75265a008102d24f38b33%2Fidentifying_a_jaguar.png?generation=1769770488226967&amp;alt=media\" alt=\"\"></p>",
      "votes": null,
      "replies": [
        {
          "id": 3401013,
          "author_name": "sanjeevkv007",
          "author_url": "",
          "post_date": "02/02/2026 18:45:07",
          "content": "<p>That’s a fair point. If the model sees a flipped right-side image and matches it to a left-side image based purely on silhouette, then it is effectively bypassing the real biological identifier, which is the rosette pattern. In that case, the network is learning a shortcut instead of learning what actually matters.</p>\n<p>Because the goal is to deploy this system for real-world conservation work, I am pivoting the pipeline to explicitly enforce biological consistency and remove those spurious correlations.</p>\n<p>First, I am introducing side-aware labeling. Rather than assigning a single identity per jaguar, I split each individual into two distinct identities based on body side, left and right. This forces the model to treat each side as its own visual barcode and prevents it from collapsing mirrored patterns into the same embedding.</p>\n<p>Second, I am restricting data augmentation. Horizontal flips are disabled entirely. Instead, I rely on random cropping, elastic transformations, and color jitter to simulate variation in camera distance, pose, and lighting. These augmentations preserve anatomical realism while still improving robustness.</p>\n<p>Third, I am adding a spatial focus mechanism through a global–local attention branch. By encouraging the model to attend to localized patches of rosettes, the network is pushed to prioritize fine-grained texture over coarse body shape or silhouette.</p>\n<p>To implement this, I construct side-aware identities directly in the training data by combining the individual ID with the recorded view.</p>\n<p>import pandas as pd</p>\n<p>def create_side_aware_labels(df):\n    df['side_aware_id'] = df['individual_id'] + \"_\" + df['view']\n    return df</p>\n<p>train_df = pd.read_csv('train.csv')\ntrain_df = create_side_aware_labels(train_df)</p>\n<p>With this setup, the ArcFace loss explicitly separates embeddings for something like Aju Left and Aju Right into different regions of the embedding space. At inference time, Jaccard re-ranking can be used to observe whether left and right identities consistently co-occur within the same camera trap sequences. This allows cross-side associations to be recovered after training, without contaminating the learning signal itself. </p>",
          "votes": null,
          "replies": []
        }
      ]
    },
    {
      "id": 3420825,
      "author_name": "cisko6koman",
      "author_url": "",
      "post_date": "03/14/2026 01:16:20",
      "content": "<p>Hi,\nThank you, reading your post was helpful.</p>",
      "votes": null,
      "replies": []
    }
  ],
  "raw_markdown_by_id": {
    "3397350": "Hi everyone,\n\nI wanted to share some findings from my recent experiments. Like many of you, I hit a wall early on with standard ResNet baselines and ran into some frustrating AssertionError crashes when trying to scale up data loading.\n\nHere is a breakdown of the pipeline that stabilized my training and significantly boosted my LB score.\n\n1. The \"Child Process\" Crash Fix\n\nIf you are seeing AssertionError: can only test a child process or random hangs during training (especially after 1-2 epochs), this is likely due to the Kaggle/Docker environment handling multiprocessing poorly with PyTorch DataLoader.\n\nThe Fix:\nSet num_workers=0 in your DataLoader.\n\ntrain_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=0)\n\n\nTrade-off: This moves data loading to the main process, which can be slower.\n\nCompensation: I offset the speed loss by using ConvNeXt-Small (instead of Base) and resizing images to 256x256. This keeps the GPU saturated even with a single worker.\n\n2. Model Choice: Why ConvNeXt?\n\nFor Jaguar ReID, we rely on fine-grained texture details (rosettes).\n\nConvNeXt models tend to perform better on texture-heavy tasks compared to ViTs (which crave massive datasets) or ResNets (which are older).\n\nResolution: 256x256 seems to be the sweet spot for speed/accuracy. If you have the compute time, 384x384 definitely captures more unique rosette patterns.\n\n3. The \"Secret Sauce\": Post-Processing\n\nTraining a good model is only half the battle in ReID. The similarity matrix calculation is where you can squeeze out extra performance.\n\nA. Test Time Augmentation (TTA):\nI extract features for the original image and the horizontal flip, then average them. This makes the embeddings robust to orientation.\n\nf1 = model(img)\nf2 = model(torch.flip(img, [3]))\nfeature = (f1 + f2) / 2\n\n\nB. Vectorized Jaccard Reranking:\nStandard Cosine Similarity is good, but k-Reciprocal Reranking is better. It asks: \"I think Image B is similar to Image A, but does Image B also think Image A is similar to it?\"\n\nHere is a fast, vectorized implementation of Jaccard similarity to mix with your cosine scores (I use a 70/30 split):\n\ndef compute_jaccard_similarity(features, k1=20):\n    # Normalize features\n    features = F.normalize(torch.tensor(features), dim=1)\n    \n    # Cosine Sim\n    sim_mat = features @ features.T\n    \n    # Get top k1 neighbors\n    n = len(features)\n    _, indices = torch.topk(sim_mat, k=k1, dim=1)\n    indices = indices.numpy()\n    \n    # Create neighbor mask\n    neighbor_mask = np.zeros((n, n), dtype=np.float32)\n    for i in range(n):\n        neighbor_mask[i, indices[i]] = 1.0\n        \n    # Jaccard = Intersection / Union\n    intersection = neighbor_mask @ neighbor_mask.T\n    union = neighbor_mask.sum(1, keepdims=True) + neighbor_mask.sum(1, keepdims=True).T - intersection\n    \n    return intersection / (union + 1e-6)\n\n\nSummary of My Pipeline\n\nBackbone: convnext_small.fb_in1k\n\nLoss: ArcFace (Margin=0.5, Scale=30)\n\nOptimizer: AdamW + CosineAnnealingLR\n\nInference: TTA + (0.7 * Cosine + 0.3 * Jaccard)\n\nHope this helps anyone stuck on the technical errors or looking for that extra accuracy boost! Good lu",
    "3399185": "Hi! Nice summary and good overall tips. \n\nNote that jaguars have distinct patterns on their right and left sides. \n\nThis means that performance gains that come from the left-right random augmentation come from learning the background features or the silhouette and pose. These are spurious correlations. ![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F371981%2Fba54b619a9b75265a008102d24f38b33%2Fidentifying_a_jaguar.png?generation=1769770488226967&alt=media)",
    "3401013": "That’s a fair point. If the model sees a flipped right-side image and matches it to a left-side image based purely on silhouette, then it is effectively bypassing the real biological identifier, which is the rosette pattern. In that case, the network is learning a shortcut instead of learning what actually matters.\n\nBecause the goal is to deploy this system for real-world conservation work, I am pivoting the pipeline to explicitly enforce biological consistency and remove those spurious correlations.\n\nFirst, I am introducing side-aware labeling. Rather than assigning a single identity per jaguar, I split each individual into two distinct identities based on body side, left and right. This forces the model to treat each side as its own visual barcode and prevents it from collapsing mirrored patterns into the same embedding.\n\nSecond, I am restricting data augmentation. Horizontal flips are disabled entirely. Instead, I rely on random cropping, elastic transformations, and color jitter to simulate variation in camera distance, pose, and lighting. These augmentations preserve anatomical realism while still improving robustness.\n\nThird, I am adding a spatial focus mechanism through a global–local attention branch. By encouraging the model to attend to localized patches of rosettes, the network is pushed to prioritize fine-grained texture over coarse body shape or silhouette.\n\nTo implement this, I construct side-aware identities directly in the training data by combining the individual ID with the recorded view.\n\nimport pandas as pd\n\ndef create_side_aware_labels(df):\n    df['side_aware_id'] = df['individual_id'] + \"_\" + df['view']\n    return df\n\ntrain_df = pd.read_csv('train.csv')\ntrain_df = create_side_aware_labels(train_df)\n\n\nWith this setup, the ArcFace loss explicitly separates embeddings for something like Aju Left and Aju Right into different regions of the embedding space. At inference time, Jaccard re-ranking can be used to observe whether left and right identities consistently co-occur within the same camera trap sequences. This allows cross-side associations to be recovered after training, without contaminating the learning signal itself.",
    "3420825": "Hi,\nThank you, reading your post was helpful."
  },
  "source": "meta"
}