{
  "id": 359161,
  "title": "5th place solution[NS embedding]",
  "content": "<p>※2022/10/12 Title changed due to change in ranking.<br>\n※2022/10/18 Add solution code: <a href=\"https://github.com/riron1206/kaggle-Google-Universal-Image-Embedding-Competition-5th-Place-Solution\" target=\"_blank\">https://github.com/riron1206/kaggle-Google-Universal-Image-Embedding-Competition-5th-Place-Solution</a><br>\n※2022/10/19 Add arXiv paper: <a href=\"https://arxiv.org/abs/2210.09495\" target=\"_blank\">https://arxiv.org/abs/2210.09495</a></p>\n<p>Thank you to the hosts and the organizers for having such a challenging competition! <br>\nThinking about good image embedding was a lot of fun. And thank you to my teammates and all the participants for hard working!</p>\n<h1>Key Points</h1>\n<ul>\n<li>Dataset tuning</li>\n<li>CLIP visual encoder trained on LAION-2B</li>\n<li>head only training with ArcFace </li>\n<li>strong regularization (weight_decay=0.1)</li>\n<li>use feature (normalized original height, original width, original aspect ratio)</li>\n<li>TTA</li>\n<li>resize antialias=True</li>\n</ul>\n<h1>Dataset</h1>\n<ul>\n<li><a href=\"https://www.kaggle.com/datasets/motono0223/guie-glr2021mini-tfrecords-label-10691-17690\" target=\"_blank\">motono0223's google landmark recognition 2021</a><ul>\n<li>Randomly deleted data for 2000 classes</li></ul></li>\n<li><a href=\"https://www.kaggle.com/datasets/motono0223/guie-products10k-tfrecords-label-1000-10690\" target=\"_blank\">motono0223's products10k</a><ul>\n<li>All data were used</li></ul></li>\n<li><a href=\"https://github.com/Visual-Computing/GPR1200\" target=\"_blank\">GPR1200</a><ul>\n<li>Deleted 200 classes of iNaturalist data</li></ul></li>\n<li><a href=\"https://www.kaggle.com/datasets/srujanesanakarra/food101\" target=\"_blank\">food101</a><ul>\n<li>Reduced the number of data per class to 30</li></ul></li>\n</ul>\n<h1>Validation</h1>\n<ul>\n<li>In order to see the performance of zeroshot, we prepared a test set with only the data of classes that do not exist in train and validation<ul>\n<li>train_valid test split: <code>StratifiedGroupKFold(n_splits=20).split(X, y=&lt; used Dataset &gt;, groups=&lt; class_label &gt;))</code></li>\n<li>train valid split: <code>StratifiedKFold(n_splits=20).split(X, y=&lt; class_label &gt;))</code></li></ul></li>\n<li>In the submit, we used the weights of the epochs with higher MAP5 in the test set</li>\n</ul>\n<h1>Model</h1>\n<p>It is very important to use ViT trained with LAION-2B from <a href=\"https://github.com/mlfoundations/open_clip\" target=\"_blank\">OpenCLIP</a> for the backbone of ArcFace. OpenCLIP is more powerful than the official <a href=\"https://github.com/openai/CLIP\" target=\"_blank\">openai/CLIP</a>.</p>\n<p>Also, adding image size information to embedding improved public lb by about 0.003.</p>\n<ul>\n<li><strong>backbone: OpenCLIP ViT-H/14 224x224</strong></li>\n<li>head: BatchNorm1d(num_features=1024+3, affine=False) -&gt; Linear(num_features=1024+3, out_features=64)<ul>\n<li>num_features is set to +3 because normalized original height, original width, original aspect ratio are concatenated in the embedding of the backbone</li></ul></li>\n<li>loss: ArcFace(scale=30.0, margin=0.5)</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1694617%2F0a169ad601296aabab75c6e1a80fd619%2Fmodel_architecture_loss_change_meta_info_del.png?generation=1665458129592589&amp;alt=media\" alt=\"model_architecture\"></p>\n<h1>Training</h1>\n<p>It is important to apply strong regularization(weight decay) to the model parameters to avoid overfitting the training data.</p>\n<ul>\n<li><p><strong>freeze backbone parameters and train only head</strong></p></li>\n<li><p>optimizer: SAM(AdamW(<strong>weight_decay=0.1</strong>))</p></li>\n<li><p>scheduler: warmup 0-3epoch(lr=0.0001-&gt;0.01) + Cosine Annealing 3-1000epoch(lr=0.01-&gt;0.0001)</p></li>\n<li><p>augmentation: we used Albumentations follow OpenCLIP augmentations</p></li>\n</ul>\n<pre><code>  A.Compose([\n      A.RandomResizedCrop(height=224, width=224, scale=(0.9, 1.0), ratio=(1.0, 1.0)),\n      A.Normalize(mean=[0.48145466, 0.4578275, 0.40821073], std=[0.26862954, 0.26130258, 0.27577711], max_pixel_value=255.0, p=1.0),\n      ToTensorV2(),\n  ])\n</code></pre>\n<h1>TTA</h1>\n<p>Since lb improved when images with different heights and widths were resized keeping the aspect ratio, TTA was switched according to the aspect ratio of the image as follows.</p>\n<p>In addition, we switched TTA with a classification model. The classification model was trained with <a href=\"https://www.kaggle.com/datasets/rhtsingh/130k-images-512x512-universal-image-embeddings\" target=\"_blank\">130k-images-512x512-universal-image-embeddings</a>.</p>\n<pre><code>import torch\nimport torch.nn as nn\nfrom torchvision import transforms\n\nclass CLIPTTA(nn.Module):\n    def __init__(self, encoder, neck, classifier):\n        super().__init__()\n        self.encoder = encoder  # torchscript OpenClip ViT\n        self.neck = neck  # Model neck(head)\n        self.classifier = classifier  # Classifier Model\n\n    ...\n\n    def concat_image_info(self, emb, H: int, W: int):\n        subfeature = torch.tensor([[\n            float(H/W),\n            float(H/224.0), \n            float(W/224.0),\n        ]], device=emb.device).float()\n        return torch.cat([emb, subfeature], dim=1)\n\n    def forward(self, x):\n        B,C,H,W = x.shape\n        ratio = W/H\n\n        if ratio==1.0:\n            x_resize = self.ccrop_resize(x,cratio=0.9)  # center_crop_resize\n            feat = self.encoder(x_resize)  # OpenClip ViT embedding\n            feat = self.concat_image_info(feat, H, W)\n            feat = self.head(feat)\n            return torch.nn.functional.normalize(feat)\n\n        else:\n            x_resize = self.ccrop_resize(x,cratio=1.0)  # resize\n            feat1 = self.encoder(x_resize)  # OpenClip ViT embedding\n            cls = torch.argmax(self.classifier(feat1))  # Classification\n            feat1 = self.concat_image_info(feat1, H, W)\n            feat1 = self.head(feat1)\n\n            # cls: apparel=0, packaged=8, toys=10\n            if cls == 0 or cls == 8 or cls == 10:\n                x_pad = self.pad_ccrop_resize(x,cratio=0.9)  # keep aspect center_crop_resize\n                feat2 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat2 = self.concat_image_info(feat2, H, W)\n                feat2 = self.head(feat2)\n\n                x_pad = self.pad_ccrop_resize(x,cratio=1.0)  # keep aspect resize\n                feat3 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat3 = self.concat_image_info(feat3, H, W)\n                feat3 = self.head(feat3)\n                return torch.nn.functional.normalize((feat1+feat2+feat3)/3.0)\n            else:\n                x_pad = self.pad_ccrop_resize(x,cratio=0.9)  # keep aspect center_crop_resize\n                feat2 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat2 = self.concat_image_info(feat2, H, W)\n                feat2 = self.head(feat2)\n\n                return torch.nn.functional.normalize((feat1+feat2)/2.0)\n</code></pre>\n<p>Also, setting <code>antialias=True</code> in torchvision's resize improved lb. This is because antialias is active when the openclip model is trained.</p>\n<pre><code>torchvision.transforms.functional.resize(x, [224, 224],interpolation=transforms.InterpolationMode.BICUBIC, antialias=True)\n</code></pre>\n<h1>LB update history</h1>\n<p>The yellow line is the border of the GOLD MEDAL.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1694617%2F576cf28ca33ef0699ce3f8a86512c4f9%2Flb_update_goldline_meta_info_del.png?generation=1665458200593557&amp;alt=media\" alt=\"lb_update\"></p>\n<h1>What did not work</h1>\n<ul>\n<li>Other Dataset (<a href=\"https://www.kaggle.com/datasets/motono0223/guie-imagenet1k-mini1-tfrecords-label-0-999\" target=\"_blank\">imagenet1k</a>, <a href=\"https://www.kaggle.com/datasets/dschettler8845/the-met-dataset\" target=\"_blank\">MET</a>, <a href=\"https://github.com/ZhangYuanhan-AI/OmniBenchmark\" target=\"_blank\">OmniBenchmark</a> etc…)</li>\n<li>fine tuning(<a href=\"https://arxiv.org/abs/2202.10054\" target=\"_blank\">LP-FT</a>)</li>\n<li>ArcFace + PCA (compress output of model to 64 dimensions)</li>\n<li>Mixup</li>\n<li>sub-center ArcFace, ElasticFace, AdaFace, DistanceMarginLayer</li>\n<li>multi-layered head</li>\n<li>Concat output of timm model to embedding</li>\n<li>Concat output of ViT projection layer to embedding</li>\n<li>Model ensemble (average embedding of 64 dimensions)</li>\n<li>and more…..</li>\n</ul>",
  "comments": [
    {
      "id": 1981887,
      "author_name": "KKY",
      "author_url": "",
      "post_date": "2022-10-11T06:00:14.503000",
      "content": "<p>Congrats!  lol , The not work list  are very similar to us. </p>\n<p>Our models seems not convergence … freeze train with 100ep + id uniform + 1e-3 cosine deday,  then unfreeze 20ep + 1e-5 cosine decay.</p>",
      "votes": 1,
      "replies": []
    },
    {
      "id": 1982673,
      "author_name": "Nanashi",
      "author_url": "",
      "post_date": "2022-10-11T14:50:03.543000",
      "content": "<p><a href=\"https://www.kaggle.com/anonamename\" target=\"_blank\">@anonamename</a> (and your team) congratulations! I like the idea of the <code>add_features</code> residual connection</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1982345,
      "author_name": "imori",
      "author_url": "",
      "post_date": "2022-10-11T11:46:17.017000",
      "content": "<p>Congrats!  Thank you for sharing solution.</p>\n<blockquote>\n  <p>num_features is set to +3 because normalized original height, original width, original aspect ratio are concatenated in the embedding of the backbone</p>\n</blockquote>\n<p>This is very interesting idea. Is this your original idea or used in other method?</p>",
      "votes": 0,
      "replies": [
        {
          "id": 1982390,
          "author_name": "hirune924",
          "author_url": "",
          "post_date": "2022-10-11T11:59:03.220000",
          "content": "<p>This is our original idea, invented by <a href=\"https://www.kaggle.com/sinpcw\" target=\"_blank\">@sinpcw</a> </p>",
          "votes": 3,
          "replies": []
        },
        {
          "id": 1982426,
          "author_name": "imori",
          "author_url": "",
          "post_date": "2022-10-11T12:22:10.727000",
          "content": "<p>Thank you </p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 1982143,
      "author_name": "alvitawa",
      "author_url": "",
      "post_date": "2022-10-11T09:28:20.217000",
      "content": "<p>Thanks for your solution! How did you sample instances for the arcface (and what was your batch size)?</p>",
      "votes": 0,
      "replies": [
        {
          "id": 1982188,
          "author_name": "anonamename",
          "author_url": "",
          "post_date": "2022-10-11T10:08:39.153000",
          "content": "<p>sample instances are sampled with <code>.sample(random_state=0)</code> as follows</p>\n<pre><code># glr2021 sampling\nlabels_glr = df_glr[\"label\"].unique()\nlabels_glr = pd.Series(labels_glr).sample(n=len(labels_glr) - 2000, random_state=0)\ndf_glr = df_glr[df_glr[\"label\"].isin(labels_glr)]\n\n# GPR1200 sampling\ndf_GPR1200 = df_GPR1200[df_GPR1200[\"category\"] != \"iNat\"]\n\n# food101 sampling\ndf_food101 = df_food101.groupby(\"label\").sample(n=30, random_state=0)\n</code></pre>\n<p>batch size is 256</p>",
          "votes": 2,
          "replies": []
        }
      ]
    },
    {
      "id": 1982070,
      "author_name": "Man of the year",
      "author_url": "",
      "post_date": "2022-10-11T08:33:52.947000",
      "content": "<p>Also, what do you mean by neck in your model? And how did you avoid notebooks timeout, when you used Vit huge? As I see, sometimes you run encoder like 3-4 times for an image. My notebook with just vit huge took about 5-6 hours, so I decided not to run encoder several times. </p>",
      "votes": 0,
      "replies": [
        {
          "id": 1982089,
          "author_name": "SiNpcw",
          "author_url": "",
          "post_date": "2022-10-11T08:53:10.310000",
          "content": "<p>Sorry for the lack of clarity. Neck means the same as the Head of the model.</p>\n<p>As you pointed out, inference of ViT-H took about 290 minutes for full data. We tried to speed up the process by using classification model and image aspect ratio and applying TTA only partially. Performing TTA on a non-square image and not performing TTA on a square image took 1.5 times longer (approximately 7 hours).</p>",
          "votes": 3,
          "replies": []
        }
      ]
    },
    {
      "id": 1981980,
      "author_name": "khyeh",
      "author_url": "",
      "post_date": "2022-10-11T07:02:30.743000",
      "content": "<p>Congrats and thanks for sharing, learned a lot! It is a good idea to have different preprocessing for different domains!</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1981831,
      "author_name": "Man of the year",
      "author_url": "",
      "post_date": "2022-10-11T04:45:49.993000",
      "content": "<p>Congragulations on winning, your strategy and solution is very cool. </p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1981979,
      "author_name": "",
      "author_url": "",
      "post_date": "2022-10-11T07:02:25.497000",
      "content": "",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1981978,
      "author_name": "",
      "author_url": "",
      "post_date": "2022-10-11T07:01:32.013000",
      "content": "",
      "votes": 0,
      "replies": []
    },
    {
      "id": 2039693,
      "author_name": "vvoo",
      "author_url": "",
      "post_date": "2022-11-22T12:27:47.247000",
      "content": "<p>Thanks for sharing solution.</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1984937,
      "author_name": "Mingyu Jeon",
      "author_url": "",
      "post_date": "2022-10-13T01:41:16.323000",
      "content": "<p>Thanks for sharing solution</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1984004,
      "author_name": "Vaidehi Savaliya",
      "author_url": "",
      "post_date": "2022-10-12T11:59:06.383000",
      "content": "<p>Thanks for sharing solution</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1981736,
      "author_name": "MiHu",
      "author_url": "",
      "post_date": "2022-10-11T03:07:31.720000",
      "content": "<p>Thank you. I have learned a lot.</p>",
      "votes": 0,
      "replies": []
    }
  ],
  "comment_count": 17,
  "messages": [
    {
      "id": 1981727,
      "postDate": "2022-10-11T02:59:29.393Z",
      "content": "<p>※2022/10/12 Title changed due to change in ranking.<br>\n※2022/10/18 Add solution code: <a href=\"https://github.com/riron1206/kaggle-Google-Universal-Image-Embedding-Competition-5th-Place-Solution\" target=\"_blank\">https://github.com/riron1206/kaggle-Google-Universal-Image-Embedding-Competition-5th-Place-Solution</a><br>\n※2022/10/19 Add arXiv paper: <a href=\"https://arxiv.org/abs/2210.09495\" target=\"_blank\">https://arxiv.org/abs/2210.09495</a></p>\n<p>Thank you to the hosts and the organizers for having such a challenging competition! <br>\nThinking about good image embedding was a lot of fun. And thank you to my teammates and all the participants for hard working!</p>\n<h1>Key Points</h1>\n<ul>\n<li>Dataset tuning</li>\n<li>CLIP visual encoder trained on LAION-2B</li>\n<li>head only training with ArcFace </li>\n<li>strong regularization (weight_decay=0.1)</li>\n<li>use feature (normalized original height, original width, original aspect ratio)</li>\n<li>TTA</li>\n<li>resize antialias=True</li>\n</ul>\n<h1>Dataset</h1>\n<ul>\n<li><a href=\"https://www.kaggle.com/datasets/motono0223/guie-glr2021mini-tfrecords-label-10691-17690\" target=\"_blank\">motono0223's google landmark recognition 2021</a><ul>\n<li>Randomly deleted data for 2000 classes</li></ul></li>\n<li><a href=\"https://www.kaggle.com/datasets/motono0223/guie-products10k-tfrecords-label-1000-10690\" target=\"_blank\">motono0223's products10k</a><ul>\n<li>All data were used</li></ul></li>\n<li><a href=\"https://github.com/Visual-Computing/GPR1200\" target=\"_blank\">GPR1200</a><ul>\n<li>Deleted 200 classes of iNaturalist data</li></ul></li>\n<li><a href=\"https://www.kaggle.com/datasets/srujanesanakarra/food101\" target=\"_blank\">food101</a><ul>\n<li>Reduced the number of data per class to 30</li></ul></li>\n</ul>\n<h1>Validation</h1>\n<ul>\n<li>In order to see the performance of zeroshot, we prepared a test set with only the data of classes that do not exist in train and validation<ul>\n<li>train_valid test split: <code>StratifiedGroupKFold(n_splits=20).split(X, y=&lt; used Dataset &gt;, groups=&lt; class_label &gt;))</code></li>\n<li>train valid split: <code>StratifiedKFold(n_splits=20).split(X, y=&lt; class_label &gt;))</code></li></ul></li>\n<li>In the submit, we used the weights of the epochs with higher MAP5 in the test set</li>\n</ul>\n<h1>Model</h1>\n<p>It is very important to use ViT trained with LAION-2B from <a href=\"https://github.com/mlfoundations/open_clip\" target=\"_blank\">OpenCLIP</a> for the backbone of ArcFace. OpenCLIP is more powerful than the official <a href=\"https://github.com/openai/CLIP\" target=\"_blank\">openai/CLIP</a>.</p>\n<p>Also, adding image size information to embedding improved public lb by about 0.003.</p>\n<ul>\n<li><strong>backbone: OpenCLIP ViT-H/14 224x224</strong></li>\n<li>head: BatchNorm1d(num_features=1024+3, affine=False) -&gt; Linear(num_features=1024+3, out_features=64)<ul>\n<li>num_features is set to +3 because normalized original height, original width, original aspect ratio are concatenated in the embedding of the backbone</li></ul></li>\n<li>loss: ArcFace(scale=30.0, margin=0.5)</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1694617%2F0a169ad601296aabab75c6e1a80fd619%2Fmodel_architecture_loss_change_meta_info_del.png?generation=1665458129592589&amp;alt=media\" alt=\"model_architecture\"></p>\n<h1>Training</h1>\n<p>It is important to apply strong regularization(weight decay) to the model parameters to avoid overfitting the training data.</p>\n<ul>\n<li><p><strong>freeze backbone parameters and train only head</strong></p></li>\n<li><p>optimizer: SAM(AdamW(<strong>weight_decay=0.1</strong>))</p></li>\n<li><p>scheduler: warmup 0-3epoch(lr=0.0001-&gt;0.01) + Cosine Annealing 3-1000epoch(lr=0.01-&gt;0.0001)</p></li>\n<li><p>augmentation: we used Albumentations follow OpenCLIP augmentations</p></li>\n</ul>\n<pre><code>  A.Compose([\n      A.RandomResizedCrop(height=224, width=224, scale=(0.9, 1.0), ratio=(1.0, 1.0)),\n      A.Normalize(mean=[0.48145466, 0.4578275, 0.40821073], std=[0.26862954, 0.26130258, 0.27577711], max_pixel_value=255.0, p=1.0),\n      ToTensorV2(),\n  ])\n</code></pre>\n<h1>TTA</h1>\n<p>Since lb improved when images with different heights and widths were resized keeping the aspect ratio, TTA was switched according to the aspect ratio of the image as follows.</p>\n<p>In addition, we switched TTA with a classification model. The classification model was trained with <a href=\"https://www.kaggle.com/datasets/rhtsingh/130k-images-512x512-universal-image-embeddings\" target=\"_blank\">130k-images-512x512-universal-image-embeddings</a>.</p>\n<pre><code>import torch\nimport torch.nn as nn\nfrom torchvision import transforms\n\nclass CLIPTTA(nn.Module):\n    def __init__(self, encoder, neck, classifier):\n        super().__init__()\n        self.encoder = encoder  # torchscript OpenClip ViT\n        self.neck = neck  # Model neck(head)\n        self.classifier = classifier  # Classifier Model\n\n    ...\n\n    def concat_image_info(self, emb, H: int, W: int):\n        subfeature = torch.tensor([[\n            float(H/W),\n            float(H/224.0), \n            float(W/224.0),\n        ]], device=emb.device).float()\n        return torch.cat([emb, subfeature], dim=1)\n\n    def forward(self, x):\n        B,C,H,W = x.shape\n        ratio = W/H\n\n        if ratio==1.0:\n            x_resize = self.ccrop_resize(x,cratio=0.9)  # center_crop_resize\n            feat = self.encoder(x_resize)  # OpenClip ViT embedding\n            feat = self.concat_image_info(feat, H, W)\n            feat = self.head(feat)\n            return torch.nn.functional.normalize(feat)\n\n        else:\n            x_resize = self.ccrop_resize(x,cratio=1.0)  # resize\n            feat1 = self.encoder(x_resize)  # OpenClip ViT embedding\n            cls = torch.argmax(self.classifier(feat1))  # Classification\n            feat1 = self.concat_image_info(feat1, H, W)\n            feat1 = self.head(feat1)\n\n            # cls: apparel=0, packaged=8, toys=10\n            if cls == 0 or cls == 8 or cls == 10:\n                x_pad = self.pad_ccrop_resize(x,cratio=0.9)  # keep aspect center_crop_resize\n                feat2 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat2 = self.concat_image_info(feat2, H, W)\n                feat2 = self.head(feat2)\n\n                x_pad = self.pad_ccrop_resize(x,cratio=1.0)  # keep aspect resize\n                feat3 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat3 = self.concat_image_info(feat3, H, W)\n                feat3 = self.head(feat3)\n                return torch.nn.functional.normalize((feat1+feat2+feat3)/3.0)\n            else:\n                x_pad = self.pad_ccrop_resize(x,cratio=0.9)  # keep aspect center_crop_resize\n                feat2 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat2 = self.concat_image_info(feat2, H, W)\n                feat2 = self.head(feat2)\n\n                return torch.nn.functional.normalize((feat1+feat2)/2.0)\n</code></pre>\n<p>Also, setting <code>antialias=True</code> in torchvision's resize improved lb. This is because antialias is active when the openclip model is trained.</p>\n<pre><code>torchvision.transforms.functional.resize(x, [224, 224],interpolation=transforms.InterpolationMode.BICUBIC, antialias=True)\n</code></pre>\n<h1>LB update history</h1>\n<p>The yellow line is the border of the GOLD MEDAL.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1694617%2F576cf28ca33ef0699ce3f8a86512c4f9%2Flb_update_goldline_meta_info_del.png?generation=1665458200593557&amp;alt=media\" alt=\"lb_update\"></p>\n<h1>What did not work</h1>\n<ul>\n<li>Other Dataset (<a href=\"https://www.kaggle.com/datasets/motono0223/guie-imagenet1k-mini1-tfrecords-label-0-999\" target=\"_blank\">imagenet1k</a>, <a href=\"https://www.kaggle.com/datasets/dschettler8845/the-met-dataset\" target=\"_blank\">MET</a>, <a href=\"https://github.com/ZhangYuanhan-AI/OmniBenchmark\" target=\"_blank\">OmniBenchmark</a> etc…)</li>\n<li>fine tuning(<a href=\"https://arxiv.org/abs/2202.10054\" target=\"_blank\">LP-FT</a>)</li>\n<li>ArcFace + PCA (compress output of model to 64 dimensions)</li>\n<li>Mixup</li>\n<li>sub-center ArcFace, ElasticFace, AdaFace, DistanceMarginLayer</li>\n<li>multi-layered head</li>\n<li>Concat output of timm model to embedding</li>\n<li>Concat output of ViT projection layer to embedding</li>\n<li>Model ensemble (average embedding of 64 dimensions)</li>\n<li>and more…..</li>\n</ul>",
      "rawMarkdown": "※2022/10/12 Title changed due to change in ranking.\n※2022/10/18 Add solution code: https://github.com/riron1206/kaggle-Google-Universal-Image-Embedding-Competition-5th-Place-Solution\n※2022/10/19 Add arXiv paper: https://arxiv.org/abs/2210.09495\n\nThank you to the hosts and the organizers for having such a challenging competition! \nThinking about good image embedding was a lot of fun. And thank you to my teammates and all the participants for hard working!\n\n\n# Key Points\n\n- Dataset tuning\n- CLIP visual encoder trained on LAION-2B\n- head only training with ArcFace \n- strong regularization (weight_decay=0.1)\n- use feature (normalized original height, original width, original aspect ratio)\n- TTA\n- resize antialias=True\n\n\n\n# Dataset\n\n- [motono0223's google landmark recognition 2021](https://www.kaggle.com/datasets/motono0223/guie-glr2021mini-tfrecords-label-10691-17690)\n  - Randomly deleted data for 2000 classes\n- [motono0223's products10k](https://www.kaggle.com/datasets/motono0223/guie-products10k-tfrecords-label-1000-10690)\n  - All data were used\n- [GPR1200](https://github.com/Visual-Computing/GPR1200)\n  - Deleted 200 classes of iNaturalist data\n- [food101](https://www.kaggle.com/datasets/srujanesanakarra/food101)\n  - Reduced the number of data per class to 30\n\n\n# Validation\n\n- In order to see the performance of zeroshot, we prepared a test set with only the data of classes that do not exist in train and validation\n  - train_valid test split: ```StratifiedGroupKFold(n_splits=20).split(X, y=< used Dataset >, groups=< class_label >))```\n  - train valid split: ```StratifiedKFold(n_splits=20).split(X, y=< class_label >))```\n- In the submit, we used the weights of the epochs with higher MAP5 in the test set\n\n\n# Model\n\nIt is very important to use ViT trained with LAION-2B from [OpenCLIP](https://github.com/mlfoundations/open_clip) for the backbone of ArcFace. OpenCLIP is more powerful than the official [openai/CLIP]( https://github.com/openai/CLIP).\n\nAlso, adding image size information to embedding improved public lb by about 0.003.\n\n- **backbone: OpenCLIP ViT-H/14 224x224**\n- head: BatchNorm1d(num_features=1024+3, affine=False) -> Linear(num_features=1024+3, out_features=64)\n  - num_features is set to +3 because normalized original height, original width, original aspect ratio are concatenated in the embedding of the backbone\n- loss: ArcFace(scale=30.0, margin=0.5)\n\n![model_architecture](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1694617%2F0a169ad601296aabab75c6e1a80fd619%2Fmodel_architecture_loss_change_meta_info_del.png?generation=1665458129592589&alt=media)\n\n# Training\n\nIt is important to apply strong regularization(weight decay) to the model parameters to avoid overfitting the training data.\n\n- **freeze backbone parameters and train only head**\n\n- optimizer: SAM(AdamW(**weight_decay=0.1**))\n\n- scheduler: warmup 0-3epoch(lr=0.0001->0.01) + Cosine Annealing 3-1000epoch(lr=0.01->0.0001)\n\n- augmentation: we used Albumentations follow OpenCLIP augmentations\n\n  ```\n  A.Compose([\n      A.RandomResizedCrop(height=224, width=224, scale=(0.9, 1.0), ratio=(1.0, 1.0)),\n      A.Normalize(mean=[0.48145466, 0.4578275, 0.40821073], std=[0.26862954, 0.26130258, 0.27577711], max_pixel_value=255.0, p=1.0),\n      ToTensorV2(),\n  ])\n  ```\n\n\n# TTA\n\nSince lb improved when images with different heights and widths were resized keeping the aspect ratio, TTA was switched according to the aspect ratio of the image as follows.\n\nIn addition, we switched TTA with a classification model. The classification model was trained with [130k-images-512x512-universal-image-embeddings](https://www.kaggle.com/datasets/rhtsingh/130k-images-512x512-universal-image-embeddings).\n\n```\nimport torch\nimport torch.nn as nn\nfrom torchvision import transforms\n\nclass CLIPTTA(nn.Module):\n    def __init__(self, encoder, neck, classifier):\n        super().__init__()\n        self.encoder = encoder  # torchscript OpenClip ViT\n        self.neck = neck  # Model neck(head)\n        self.classifier = classifier  # Classifier Model\n\n    ...\n    \n    def concat_image_info(self, emb, H: int, W: int):\n        subfeature = torch.tensor([[\n            float(H/W),\n            float(H/224.0), \n            float(W/224.0),\n        ]], device=emb.device).float()\n        return torch.cat([emb, subfeature], dim=1)\n\n    def forward(self, x):\n        B,C,H,W = x.shape\n        ratio = W/H\n\n        if ratio==1.0:\n            x_resize = self.ccrop_resize(x,cratio=0.9)  # center_crop_resize\n            feat = self.encoder(x_resize)  # OpenClip ViT embedding\n            feat = self.concat_image_info(feat, H, W)\n            feat = self.head(feat)\n            return torch.nn.functional.normalize(feat)\n\n        else:\n            x_resize = self.ccrop_resize(x,cratio=1.0)  # resize\n            feat1 = self.encoder(x_resize)  # OpenClip ViT embedding\n            cls = torch.argmax(self.classifier(feat1))  # Classification\n            feat1 = self.concat_image_info(feat1, H, W)\n            feat1 = self.head(feat1)\n            \n            # cls: apparel=0, packaged=8, toys=10\n            if cls == 0 or cls == 8 or cls == 10:\n                x_pad = self.pad_ccrop_resize(x,cratio=0.9)  # keep aspect center_crop_resize\n                feat2 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat2 = self.concat_image_info(feat2, H, W)\n                feat2 = self.head(feat2)\n\n                x_pad = self.pad_ccrop_resize(x,cratio=1.0)  # keep aspect resize\n                feat3 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat3 = self.concat_image_info(feat3, H, W)\n                feat3 = self.head(feat3)\n                return torch.nn.functional.normalize((feat1+feat2+feat3)/3.0)\n            else:\n                x_pad = self.pad_ccrop_resize(x,cratio=0.9)  # keep aspect center_crop_resize\n                feat2 = self.encoder(x_pad)  # OpenClip ViT embedding\n                feat2 = self.concat_image_info(feat2, H, W)\n                feat2 = self.head(feat2)\n\n                return torch.nn.functional.normalize((feat1+feat2)/2.0)\n```\n\nAlso, setting ```antialias=True``` in torchvision's resize improved lb. This is because antialias is active when the openclip model is trained.\n\n```\ntorchvision.transforms.functional.resize(x, [224, 224],interpolation=transforms.InterpolationMode.BICUBIC, antialias=True)\n```\n\n# LB update history\n\nThe yellow line is the border of the GOLD MEDAL.\n\n![lb_update](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F1694617%2F576cf28ca33ef0699ce3f8a86512c4f9%2Flb_update_goldline_meta_info_del.png?generation=1665458200593557&alt=media)\n\n# What did not work\n\n- Other Dataset ([imagenet1k](https://www.kaggle.com/datasets/motono0223/guie-imagenet1k-mini1-tfrecords-label-0-999), [MET](https://www.kaggle.com/datasets/dschettler8845/the-met-dataset), [OmniBenchmark](https://github.com/ZhangYuanhan-AI/OmniBenchmark) etc...)\n- fine tuning([LP-FT](https://arxiv.org/abs/2202.10054))\n- ArcFace + PCA (compress output of model to 64 dimensions)\n- Mixup\n- sub-center ArcFace, ElasticFace, AdaFace, DistanceMarginLayer\n- multi-layered head\n- Concat output of timm model to embedding\n- Concat output of ViT projection layer to embedding\n- Model ensemble (average embedding of 64 dimensions)\n- and more.....",
      "votes": 65
    },
    {
      "id": 1981887,
      "postDate": "2022-10-11T06:00:14.503Z",
      "content": "<p>Congrats!  lol , The not work list  are very similar to us. </p>\n<p>Our models seems not convergence … freeze train with 100ep + id uniform + 1e-3 cosine deday,  then unfreeze 20ep + 1e-5 cosine decay.</p>",
      "rawMarkdown": "Congrats!  lol , The not work list  are very similar to us. \n\nOur models seems not convergence ... freeze train with 100ep + id uniform + 1e-3 cosine deday,  then unfreeze 20ep + 1e-5 cosine decay.",
      "votes": 1
    },
    {
      "id": 1982673,
      "postDate": "2022-10-11T14:50:03.543Z",
      "content": "<p><a href=\"https://www.kaggle.com/anonamename\" target=\"_blank\">@anonamename</a> (and your team) congratulations! I like the idea of the <code>add_features</code> residual connection</p>",
      "rawMarkdown": "@anonamename (and your team) congratulations! I like the idea of the `add_features` residual connection"
    },
    {
      "id": 1982345,
      "postDate": "2022-10-11T11:46:17.017Z",
      "content": "<p>Congrats!  Thank you for sharing solution.</p>\n<blockquote>\n  <p>num_features is set to +3 because normalized original height, original width, original aspect ratio are concatenated in the embedding of the backbone</p>\n</blockquote>\n<p>This is very interesting idea. Is this your original idea or used in other method?</p>",
      "rawMarkdown": "Congrats!  Thank you for sharing solution.\n\n> num_features is set to +3 because normalized original height, original width, original aspect ratio are concatenated in the embedding of the backbone\n\nThis is very interesting idea. Is this your original idea or used in other method?",
      "replies": [
        {
          "id": 1982390,
          "postDate": "2022-10-11T11:59:03.220Z",
          "content": "<p>This is our original idea, invented by <a href=\"https://www.kaggle.com/sinpcw\" target=\"_blank\">@sinpcw</a> </p>",
          "rawMarkdown": "This is our original idea, invented by @sinpcw ",
          "votes": 3
        },
        {
          "id": 1982426,
          "postDate": "2022-10-11T12:22:10.727Z",
          "content": "<p>Thank you </p>",
          "rawMarkdown": "Thank you "
        }
      ]
    },
    {
      "id": 1982143,
      "postDate": "2022-10-11T09:28:20.217Z",
      "content": "<p>Thanks for your solution! How did you sample instances for the arcface (and what was your batch size)?</p>",
      "rawMarkdown": "Thanks for your solution! How did you sample instances for the arcface (and what was your batch size)?",
      "replies": [
        {
          "id": 1982188,
          "postDate": "2022-10-11T10:08:39.153Z",
          "content": "<p>sample instances are sampled with <code>.sample(random_state=0)</code> as follows</p>\n<pre><code># glr2021 sampling\nlabels_glr = df_glr[\"label\"].unique()\nlabels_glr = pd.Series(labels_glr).sample(n=len(labels_glr) - 2000, random_state=0)\ndf_glr = df_glr[df_glr[\"label\"].isin(labels_glr)]\n\n# GPR1200 sampling\ndf_GPR1200 = df_GPR1200[df_GPR1200[\"category\"] != \"iNat\"]\n\n# food101 sampling\ndf_food101 = df_food101.groupby(\"label\").sample(n=30, random_state=0)\n</code></pre>\n<p>batch size is 256</p>",
          "rawMarkdown": "sample instances are sampled with ```.sample(random_state=0)``` as follows\n```\n# glr2021 sampling\nlabels_glr = df_glr[\"label\"].unique()\nlabels_glr = pd.Series(labels_glr).sample(n=len(labels_glr) - 2000, random_state=0)\ndf_glr = df_glr[df_glr[\"label\"].isin(labels_glr)]\n\n# GPR1200 sampling\ndf_GPR1200 = df_GPR1200[df_GPR1200[\"category\"] != \"iNat\"]\n\n# food101 sampling\ndf_food101 = df_food101.groupby(\"label\").sample(n=30, random_state=0)\n```\n\nbatch size is 256",
          "votes": 2
        }
      ]
    },
    {
      "id": 1982070,
      "postDate": "2022-10-11T08:33:52.947Z",
      "content": "<p>Also, what do you mean by neck in your model? And how did you avoid notebooks timeout, when you used Vit huge? As I see, sometimes you run encoder like 3-4 times for an image. My notebook with just vit huge took about 5-6 hours, so I decided not to run encoder several times. </p>",
      "rawMarkdown": "Also, what do you mean by neck in your model? And how did you avoid notebooks timeout, when you used Vit huge? As I see, sometimes you run encoder like 3-4 times for an image. My notebook with just vit huge took about 5-6 hours, so I decided not to run encoder several times. ",
      "replies": [
        {
          "id": 1982089,
          "postDate": "2022-10-11T08:53:10.310Z",
          "content": "<p>Sorry for the lack of clarity. Neck means the same as the Head of the model.</p>\n<p>As you pointed out, inference of ViT-H took about 290 minutes for full data. We tried to speed up the process by using classification model and image aspect ratio and applying TTA only partially. Performing TTA on a non-square image and not performing TTA on a square image took 1.5 times longer (approximately 7 hours).</p>",
          "rawMarkdown": "Sorry for the lack of clarity. Neck means the same as the Head of the model.\n\nAs you pointed out, inference of ViT-H took about 290 minutes for full data. We tried to speed up the process by using classification model and image aspect ratio and applying TTA only partially. Performing TTA on a non-square image and not performing TTA on a square image took 1.5 times longer (approximately 7 hours).",
          "votes": 3
        }
      ]
    },
    {
      "id": 1981980,
      "postDate": "2022-10-11T07:02:30.743Z",
      "content": "<p>Congrats and thanks for sharing, learned a lot! It is a good idea to have different preprocessing for different domains!</p>",
      "rawMarkdown": "Congrats and thanks for sharing, learned a lot! It is a good idea to have different preprocessing for different domains!"
    },
    {
      "id": 1981831,
      "postDate": "2022-10-11T04:45:49.993Z",
      "content": "<p>Congragulations on winning, your strategy and solution is very cool. </p>",
      "rawMarkdown": "Congragulations on winning, your strategy and solution is very cool. "
    },
    {
      "id": 1981979,
      "postDate": "2022-10-11T07:02:25.497Z",
      "rawMarkdown": "",
      "isDeleted": true
    },
    {
      "id": 1981978,
      "postDate": "2022-10-11T07:01:32.013Z",
      "rawMarkdown": "",
      "isDeleted": true
    },
    {
      "id": 2039693,
      "postDate": "2022-11-22T12:27:47.247Z",
      "content": "<p>Thanks for sharing solution.</p>",
      "rawMarkdown": "Thanks for sharing solution."
    },
    {
      "id": 1984937,
      "postDate": "2022-10-13T01:41:16.323Z",
      "content": "<p>Thanks for sharing solution</p>",
      "rawMarkdown": "Thanks for sharing solution"
    },
    {
      "id": 1984004,
      "postDate": "2022-10-12T11:59:06.383Z",
      "content": "<p>Thanks for sharing solution</p>",
      "rawMarkdown": "Thanks for sharing solution\n"
    },
    {
      "id": 1981736,
      "postDate": "2022-10-11T03:07:31.720Z",
      "content": "<p>Thank you. I have learned a lot.</p>",
      "rawMarkdown": "Thank you. I have learned a lot."
    }
  ],
  "complete": true,
  "source": "live",
  "fetched_at": "2026-10-24T13:23:05Z",
  "competition_id": 36414,
  "competition_slug": "google-universal-image-embedding",
  "team_id": 8888698,
  "solution_url": "/competitions/google-universal-image-embedding/writeups/ns-embedding-5th-place-solution-ns-embedding"
}