{
  "id": 238361,
  "title": "8th place solution",
  "url": "/competitions/hpa-single-cell-image-classification/discussion/238361",
  "author_name": "Guanshuo Xu",
  "post_date": "2021-05-12T02:08:47.029000",
  "votes": 55,
  "comment_count": 20,
  "views": 0,
  "content": "<p>Since we are given image-levels labels and cell segmentations, we can train on image labels and predict cell labels directly.</p>\n<p>Let's say during training we sample B images, within each image we sample M cells. Let's further assume input channels=4, input_size=256x256, feature_dim=2048. </p>\n<ol>\n<li>Input shape to CNNs -&gt; BMx4x256x256</li>\n<li>CNN feature extraction -&gt; BMx2048x8x8</li>\n<li>GAP -&gt; BMx2048</li>\n<li>Reshape and permute -&gt; Bx2048xM</li>\n<li>Another GAP -&gt; Bx2048 </li>\n<li>Last linear -&gt; Bx18 (compute loss with image labels)</li>\n</ol>\n<p>During inference, to predict cell labels, we remove step 4 and 5:</p>\n<ol>\n<li>Input shape to CNNs -&gt; BMx4x256x256</li>\n<li>CNN feature extraction -&gt; BMx2048x8x8</li>\n<li>GAP -&gt; BMx2048</li>\n<li>Last linear -&gt; BMx18 (cell label prediction completed)</li>\n</ol>\n<p>This approach is partly inspired from the <a href=\"https://www.kaggle.com/c/prostate-cancer-grade-assessment/discussion/146855\" target=\"_blank\">lafoss concat tile pooling</a> and the class activation mapping. </p>\n<p>Postprocessings that bring some improvement, ranked roughly by their importance:<br>\n1) Downscale border cells predictions.<br>\n2) Cell-level finetuning with OOF predictions in CV.<br>\n3) 0.9xcell predictions+0.1xaverage cell predictions within image, to make use of label correlations within a image. It should be better to train some independent image level models though <a href=\"https://www.kaggle.com/c/hpa-single-cell-image-classification/discussion/230940\" target=\"_blank\">example</a>. Here I just reused cell predictions.</p>",
  "messages": [
    {
      "id": 1303291,
      "postDate": "2021-05-12T02:08:47.030Z",
      "content": "<p>Since we are given image-levels labels and cell segmentations, we can train on image labels and predict cell labels directly.</p>\n<p>Let's say during training we sample B images, within each image we sample M cells. Let's further assume input channels=4, input_size=256x256, feature_dim=2048. </p>\n<ol>\n<li>Input shape to CNNs -&gt; BMx4x256x256</li>\n<li>CNN feature extraction -&gt; BMx2048x8x8</li>\n<li>GAP -&gt; BMx2048</li>\n<li>Reshape and permute -&gt; Bx2048xM</li>\n<li>Another GAP -&gt; Bx2048 </li>\n<li>Last linear -&gt; Bx18 (compute loss with image labels)</li>\n</ol>\n<p>During inference, to predict cell labels, we remove step 4 and 5:</p>\n<ol>\n<li>Input shape to CNNs -&gt; BMx4x256x256</li>\n<li>CNN feature extraction -&gt; BMx2048x8x8</li>\n<li>GAP -&gt; BMx2048</li>\n<li>Last linear -&gt; BMx18 (cell label prediction completed)</li>\n</ol>\n<p>This approach is partly inspired from the <a href=\"https://www.kaggle.com/c/prostate-cancer-grade-assessment/discussion/146855\" target=\"_blank\">lafoss concat tile pooling</a> and the class activation mapping. </p>\n<p>Postprocessings that bring some improvement, ranked roughly by their importance:<br>\n1) Downscale border cells predictions.<br>\n2) Cell-level finetuning with OOF predictions in CV.<br>\n3) 0.9xcell predictions+0.1xaverage cell predictions within image, to make use of label correlations within a image. It should be better to train some independent image level models though <a href=\"https://www.kaggle.com/c/hpa-single-cell-image-classification/discussion/230940\" target=\"_blank\">example</a>. Here I just reused cell predictions.</p>",
      "rawMarkdown": "Since we are given image-levels labels and cell segmentations, we can train on image labels and predict cell labels directly.\n\nLet's say during training we sample B images, within each image we sample M cells. Let's further assume input channels=4, input_size=256x256, feature_dim=2048. \n\n1. Input shape to CNNs -> BMx4x256x256\n2. CNN feature extraction -> BMx2048x8x8\n3. GAP -> BMx2048\n4. Reshape and permute -> Bx2048xM\n5. Another GAP -> Bx2048 \n6. Last linear -> Bx18 (compute loss with image labels)\n\nDuring inference, to predict cell labels, we remove step 4 and 5:\n1. Input shape to CNNs -> BMx4x256x256\n2. CNN feature extraction -> BMx2048x8x8\n3. GAP -> BMx2048\n6. Last linear -> BMx18 (cell label prediction completed)\n\nThis approach is partly inspired from the [lafoss concat tile pooling](https://www.kaggle.com/c/prostate-cancer-grade-assessment/discussion/146855) and the class activation mapping. \n\nPostprocessings that bring some improvement, ranked roughly by their importance:\n1) Downscale border cells predictions.\n2) Cell-level finetuning with OOF predictions in CV.\n3) 0.9xcell predictions+0.1xaverage cell predictions within image, to make use of label correlations within a image. It should be better to train some independent image level models though [example](https://www.kaggle.com/c/hpa-single-cell-image-classification/discussion/230940). Here I just reused cell predictions.",
      "votes": 55
    },
    {
      "id": 1303556,
      "postDate": "2021-05-12T06:15:22.997Z",
      "content": "<p>Very interesting aproach. I have some questions. BM = B * M? Do you give on input B * M images duplicating them? Which M you use in solution? What if image has more or less cells than M? During inference you feed full image or independent cell? If full image how you find which of M images correspond to some cell?</p>\n<p>And for me it's not really obvious why step 4 on inference works…</p>",
      "rawMarkdown": "Very interesting aproach. I have some questions. BM = B * M? Do you give on input B * M images duplicating them? Which M you use in solution? What if image has more or less cells than M? During inference you feed full image or independent cell? If full image how you find which of M images correspond to some cell?\n\nAnd for me it's not really obvious why step 4 on inference works...",
      "votes": 5,
      "replies": [
        {
          "id": 1304093,
          "postDate": "2021-05-12T12:23:00.313Z",
          "content": "<p>I randomly sample M=8 cell patches from each full image, if number of cells&lt;8, I pad zero patches. So assume B=10, the input of network is 80x4x256x256.<br>\nThis approach doesn't use original full images, it only accepts input of cell patches.<br>\nI'll share some example code later</p>",
          "rawMarkdown": "I randomly sample M=8 cell patches from each full image, if number of cells<8, I pad zero patches. So assume B=10, the input of network is 80x4x256x256.\nThis approach doesn't use original full images, it only accepts input of cell patches.\nI'll share some example code later",
          "votes": 2
        },
        {
          "id": 1304309,
          "postDate": "2021-05-12T14:48:19.423Z",
          "content": "<p>For example, this is my dataset. I added some comments for better understanding. </p>\n<pre><code>class HPADataset(Dataset):\n    # image_list -&gt; list of names of full images\n    # label_dict -&gt; dict of image-level labels\n    # target_size -&gt; output size of cell images (patches)\n    # num_cell_per_image=8\n    # transform -&gt; augmentations\n    def __init__(self, image_list, label_dict, target_size, num_cell_per_image, transform):\n        self.image_list=image_list\n        self.label_dict=label_dict\n        self.target_size=target_size\n        self.num_cell_per_image=num_cell_per_image\n        self.transform=transform\n\n    def __len__(self):\n        return len(self.image_list)\n\n    def __getitem__(self, index):\n       # Initialize memory for output of a single full image composed of num_cell_per_image cells\n        x_image = np.zeros((self.num_cell_per_image, self.target_size, self.target_size, 4), dtype=np.uint8)\n\n        # Load all filenames of pre-saved cells within a single full image\n        image_files = sorted(glob.glob('../../../cell_seg/cell_segmentations/cells/'+self.image_list[index]+'/rgb/'+'/*.png'))\n\n       # permutation\n        perm = torch.randperm(len(image_files))\n\n        # read four channels, padding to square, and augmentations\n        for i in range(min(self.num_cell_per_image, len(image_files))):\n            x_rgb = cv2.imread(image_files[perm[i]])\n            x_y = np.expand_dims(cv2.imread(image_files[perm[i]].replace('rgb', 'y'), cv2.IMREAD_GRAYSCALE), axis=-1)\n            x = np.concatenate((x_rgb, x_y), axis=-1)\n            x = pad_to_square(x)\n            x = self.transform(image=x)['image'] \n            x_image[i] = x\n\n        # channel first\n        x_image = x_image.transpose(0, 3, 1, 2)\n\n        # image-level labels\n        y = self.label_dict[self.image_list[index]]\n        return x_image, y\n</code></pre>\n<p>And this is the model class for training.</p>\n<pre><code>class HPAImageResnet200d(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = timm.create_model('resnet200d', drop_path_rate=0.2, pretrained=True, in_chans=4)\n        self.avg_pool_cell = nn.AdaptiveAvgPool2d(1) # GAP within each cell image\n        self.avg_pool_image = nn.AdaptiveAvgPool1d(1) # image-level GAP\n        self.last_linear = nn.Linear(2048, 18, bias=False)\n\n    def forward(self, x, num_cell_per_image):\n        # input size 80x4x256x256, assume we sample 8 cells from each of the 10 full images\n        bs = x.size(0)\n        x = self.net.forward_features(x)  # output shape before GAP 80x2048x8x8\n        x = self.avg_pool_cell(x) # output shape 80x2048x1x1 \n        x = x.view(x.size(0), -1) # output shape 80x2048\n        x = x.view(bs//num_cell_per_image, num_cell_per_image, -1)  # output shape 10x8x2048\n        x = x.permute(0, 2, 1)  # 10x2048x8\n        x = self.avg_pool_image(x)  # 10x2048x1 \n        x = x.view(bs//num_cell_per_image, -1)  # output shape 10x2048\n        x = self.last_linear(x) # 10x18\n        return x\n</code></pre>\n<p>The output of dataloader has shape 10x8x4x256x256, so we make it 80x4x256x256 before inputting it to the model<br>\n<code>logits = model(images.view(-1, images.size(2), images.size(3), images.size(4)), num_cell_per_image)</code></p>\n<p>Model class for inference. Only need to add a wrapper for cell prediction. We can see this wrapper just removed a few lines in the training model, and we directly have cell-level predictions.</p>\n<pre><code># This part is same as the training code\nclass HPAImageResnet200d(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = timm.create_model('resnet200d', drop_path_rate=0.2, pretrained=False, in_chans=4)\n        self.avg_pool_cell = nn.AdaptiveAvgPool2d(1)\n        self.avg_pool_image = nn.AdaptiveAvgPool1d(1)\n        self.last_linear = nn.Linear(2048, 18, bias=False)\n    def forward(self, x, num_cell_per_image):\n        bs = x.size(0)\n        x = self.net.forward_features(x)\n        x = self.avg_pool_cell(x)\n        x = x.view(x.size(0), -1)\n        x = x.view(bs//num_cell_per_image, num_cell_per_image, -1)\n        x = x.permute(0, 2, 1)\n        x = self.avg_pool_image(x)\n        x = x.view(bs//num_cell_per_image, -1)\n        x = self.last_linear(x)\n        return x\n\n# Add a wrapper for cell-level prediction for convenience. \nclass HPAImageResnet200dCellPredict(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = HPAImageResnet200d()\n    def forward(self, x):\n        x = self.net.net.forward_features(x)  # output shape 80x2048x8x8\n        x = self.net.avg_pool_cell(x)  # output shape 80x2048x1x1 \n        x = x.view(x.size(0), -1) # output shape 80x2048\n        x = self.net.last_linear(x)    # cell-level predictions, output shape 80x2048\n        return x\n</code></pre>",
          "rawMarkdown": "For example, this is my dataset. I added some comments for better understanding. \n\n```\nclass HPADataset(Dataset):\n    # image_list -> list of names of full images\n    # label_dict -> dict of image-level labels\n    # target_size -> output size of cell images (patches)\n    # num_cell_per_image=8\n    # transform -> augmentations\n    def __init__(self, image_list, label_dict, target_size, num_cell_per_image, transform):\n        self.image_list=image_list\n        self.label_dict=label_dict\n        self.target_size=target_size\n        self.num_cell_per_image=num_cell_per_image\n        self.transform=transform\n\n    def __len__(self):\n        return len(self.image_list)\n\n    def __getitem__(self, index):\n       # Initialize memory for output of a single full image composed of num_cell_per_image cells\n        x_image = np.zeros((self.num_cell_per_image, self.target_size, self.target_size, 4), dtype=np.uint8)\n\n        # Load all filenames of pre-saved cells within a single full image\n        image_files = sorted(glob.glob('../../../cell_seg/cell_segmentations/cells/'+self.image_list[index]+'/rgb/'+'/*.png'))\n\n       # permutation\n        perm = torch.randperm(len(image_files))\n\n        # read four channels, padding to square, and augmentations\n        for i in range(min(self.num_cell_per_image, len(image_files))):\n            x_rgb = cv2.imread(image_files[perm[i]])\n            x_y = np.expand_dims(cv2.imread(image_files[perm[i]].replace('rgb', 'y'), cv2.IMREAD_GRAYSCALE), axis=-1)\n            x = np.concatenate((x_rgb, x_y), axis=-1)\n            x = pad_to_square(x)\n            x = self.transform(image=x)['image'] \n            x_image[i] = x\n\n        # channel first\n        x_image = x_image.transpose(0, 3, 1, 2)\n\n        # image-level labels\n        y = self.label_dict[self.image_list[index]]\n        return x_image, y\n```\n\nAnd this is the model class for training.\n\n```\nclass HPAImageResnet200d(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = timm.create_model('resnet200d', drop_path_rate=0.2, pretrained=True, in_chans=4)\n        self.avg_pool_cell = nn.AdaptiveAvgPool2d(1) # GAP within each cell image\n        self.avg_pool_image = nn.AdaptiveAvgPool1d(1) # image-level GAP\n        self.last_linear = nn.Linear(2048, 18, bias=False)\n\n    def forward(self, x, num_cell_per_image):\n        # input size 80x4x256x256, assume we sample 8 cells from each of the 10 full images\n        bs = x.size(0)\n        x = self.net.forward_features(x)  # output shape before GAP 80x2048x8x8\n        x = self.avg_pool_cell(x) # output shape 80x2048x1x1 \n        x = x.view(x.size(0), -1) # output shape 80x2048\n        x = x.view(bs//num_cell_per_image, num_cell_per_image, -1)  # output shape 10x8x2048\n        x = x.permute(0, 2, 1)  # 10x2048x8\n        x = self.avg_pool_image(x)  # 10x2048x1 \n        x = x.view(bs//num_cell_per_image, -1)  # output shape 10x2048\n        x = self.last_linear(x) # 10x18\n        return x\n```\n\nThe output of dataloader has shape 10x8x4x256x256, so we make it 80x4x256x256 before inputting it to the model\n`         logits = model(images.view(-1, images.size(2), images.size(3), images.size(4)), num_cell_per_image)`\n\n\nModel class for inference. Only need to add a wrapper for cell prediction. We can see this wrapper just removed a few lines in the training model, and we directly have cell-level predictions.\n\n```\n# This part is same as the training code\nclass HPAImageResnet200d(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = timm.create_model('resnet200d', drop_path_rate=0.2, pretrained=False, in_chans=4)\n        self.avg_pool_cell = nn.AdaptiveAvgPool2d(1)\n        self.avg_pool_image = nn.AdaptiveAvgPool1d(1)\n        self.last_linear = nn.Linear(2048, 18, bias=False)\n    def forward(self, x, num_cell_per_image):\n        bs = x.size(0)\n        x = self.net.forward_features(x)\n        x = self.avg_pool_cell(x)\n        x = x.view(x.size(0), -1)\n        x = x.view(bs//num_cell_per_image, num_cell_per_image, -1)\n        x = x.permute(0, 2, 1)\n        x = self.avg_pool_image(x)\n        x = x.view(bs//num_cell_per_image, -1)\n        x = self.last_linear(x)\n        return x\n\n# Add a wrapper for cell-level prediction for convenience. \nclass HPAImageResnet200dCellPredict(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = HPAImageResnet200d()\n    def forward(self, x):\n        x = self.net.net.forward_features(x)  # output shape 80x2048x8x8\n        x = self.net.avg_pool_cell(x)  # output shape 80x2048x1x1 \n        x = x.view(x.size(0), -1) # output shape 80x2048\n        x = self.net.last_linear(x)    # cell-level predictions, output shape 80x2048\n        return x\n```\n",
          "votes": 2
        },
        {
          "id": 1304517,
          "postDate": "2021-05-12T17:04:23.990Z",
          "content": "<p>Great, thanks!</p>",
          "rawMarkdown": "Great, thanks!"
        }
      ]
    },
    {
      "id": 1303571,
      "postDate": "2021-05-12T06:24:00.090Z",
      "content": "<p>Congrats <a href=\"https://www.kaggle.com/wowfattie\" target=\"_blank\">@wowfattie</a>. Interestingly, I had same model in my ensemble, but with attention pooling instead of GAP in step 5. For images that had less than M  cells, did you pad or sample some cells more often? What backbone did you use? Did you preprocess the single cell images or crop on the fly?</p>",
      "rawMarkdown": "Congrats @wowfattie. Interestingly, I had same model in my ensemble, but with attention pooling instead of GAP in step 5. For images that had less than M  cells, did you pad or sample some cells more often? What backbone did you use? Did you preprocess the single cell images or crop on the fly?",
      "votes": 4,
      "replies": [
        {
          "id": 1304098,
          "postDate": "2021-05-12T12:27:03.333Z",
          "content": "<p>I pad zero cell images if not enough cells. I used resnet200d and efficientnetb5, input size 288x288. All cells are pre-saved on disk for faster loading in the dataset. </p>",
          "rawMarkdown": "I pad zero cell images if not enough cells. I used resnet200d and efficientnetb5, input size 288x288. All cells are pre-saved on disk for faster loading in the dataset. ",
          "votes": 2
        }
      ]
    },
    {
      "id": 1304341,
      "postDate": "2021-05-12T15:04:44.090Z",
      "content": "<p>Congratulations! I also developed a very similar approach (called it 'bag of cells' - see #4 below) but couldn't get it to train to a good performance. I think that reflects my skill gap :) I'd love to learn the details of how this model was trained to see what I was doing wrong :) <br>\n<img src=\"https://pbs.twimg.com/media/E1Ml23qXMAYHNoe?format=jpg&amp;name=large\" alt=\"bag of cells\"></p>",
      "rawMarkdown": "Congratulations! I also developed a very similar approach (called it 'bag of cells' - see #4 below) but couldn't get it to train to a good performance. I think that reflects my skill gap :) I'd love to learn the details of how this model was trained to see what I was doing wrong :) \n![bag of cells](https://pbs.twimg.com/media/E1Ml23qXMAYHNoe?format=jpg&name=large)",
      "votes": 1,
      "replies": [
        {
          "id": 1304420,
          "postDate": "2021-05-12T15:55:45.713Z",
          "content": "<p>Hi Darek, I think this sheet summarizes pretty well the pros and cons of the major approaches to this problem. Yes, my approach looks closer to number 4, and it solves all the weakness of number 2 and 3. </p>\n<p>But I'm not sure if your #4 is same as mine. <br>\nFirst, did you do cell-level global pooling before image-level global pooling? It is important to do global pooling two times. <br>\nSecond, it might be wrong to use global max pooling for image-level pooling, global average pooling maybe the only choice here. But you are free to choice other types of global pooling for the cell-level pooling.</p>",
          "rawMarkdown": "Hi Darek, I think this sheet summarizes pretty well the pros and cons of the major approaches to this problem. Yes, my approach looks closer to number 4, and it solves all the weakness of number 2 and 3. \n\nBut I'm not sure if your #4 is same as mine. \nFirst, did you do cell-level global pooling before image-level global pooling? It is important to do global pooling two times. \nSecond, it might be wrong to use global max pooling for image-level pooling, global average pooling maybe the only choice here. But you are free to choice other types of global pooling for the cell-level pooling.",
          "votes": 1
        },
        {
          "id": 1305064,
          "postDate": "2021-05-13T04:48:44.337Z",
          "content": "<p>Thanks for response Guanshuo, I appreciate it! Yes, I did GAP layer twice - first for individual cells, then at image level. I started with max pooling at image level, but realized it might result in a very sparse signal, so later used avg+max concat pooling. I was very bullish on this architecture as it seemed like the most elegant way of solving the challenge, I did around 10 experiments with it, but all of them performed ~0.03-0.04 worse than other approaches, so I finally dropped it. I'm trying to think about what mistakes I may have made, I think I didn't do enough augmentation and I chose too many cells per image (I used 48 cells per image in most of my experiments). For reference, this is one of the architectures I tested (encoder is resnet-50 up until and including GAP layer):</p>\n<pre><code>class BagOfCellsModel(Module):\n    def __init__(self, encoder, out_dim):\n        self.encoder = encoder\n        self.bn1 = nn.BatchNorm1d(4096, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n        self.d1 = nn.Dropout(p=0.25, inplace=False)\n        self.l1 = nn.Linear(in_features=4096, out_features=2048, bias=False)\n        self.bn2 = nn.BatchNorm1d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n        self.d2 = nn.Dropout(p=0.5, inplace=False)\n        self.l2 = nn.Linear(in_features=2048, out_features=out_dim, bias=False)\n\n    def forward(self, cell_imgs):\n        b,cl,ch,h,w = cell_imgs.shape\n        unrolled = cell_imgs.reshape(-1,ch,h,w)\n        ftrs = encoder(unrolled).squeeze()\n        num_ftrs = ftrs.shape[-1]\n        ftrs = ftrs.reshape(b,cl,num_ftrs)        \n        ftrs_max = torch.max(ftrs, 1, keepdim=True)[0].squeeze()\n        ftrs_mean = torch.mean(ftrs, 1, keepdim=True).squeeze()\n        if b == 1: # error with batch size 1 being squeezed out above\n            ftrs_max = ftrs_max[None, ...]\n            ftrs_mean = ftrs_mean[None, ...]\n        ftrs_cat = torch.cat([ftrs_max, ftrs_mean], 1)\n        x = self.bn1(ftrs_cat)\n        x = self.d1(x)\n        x = self.l1(x)\n        x = F.relu(x)\n        x = self.bn2(x)\n        x = self.d2(x)\n        out = self.l2(x) \n        return out\n</code></pre>\n<p>PS. There's a bug above, I missed <code>self.</code> in front of encoder in forward… I think I fixed it in later experiments. Anyways, motivated by your example I'll try to repeat the experiments and figure out how to train this model properly :) </p>",
          "rawMarkdown": "Thanks for response Guanshuo, I appreciate it! Yes, I did GAP layer twice - first for individual cells, then at image level. I started with max pooling at image level, but realized it might result in a very sparse signal, so later used avg+max concat pooling. I was very bullish on this architecture as it seemed like the most elegant way of solving the challenge, I did around 10 experiments with it, but all of them performed ~0.03-0.04 worse than other approaches, so I finally dropped it. I'm trying to think about what mistakes I may have made, I think I didn't do enough augmentation and I chose too many cells per image (I used 48 cells per image in most of my experiments). For reference, this is one of the architectures I tested (encoder is resnet-50 up until and including GAP layer):\n```\nclass BagOfCellsModel(Module):\n    def __init__(self, encoder, out_dim):\n        self.encoder = encoder\n        self.bn1 = nn.BatchNorm1d(4096, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n        self.d1 = nn.Dropout(p=0.25, inplace=False)\n        self.l1 = nn.Linear(in_features=4096, out_features=2048, bias=False)\n        self.bn2 = nn.BatchNorm1d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n        self.d2 = nn.Dropout(p=0.5, inplace=False)\n        self.l2 = nn.Linear(in_features=2048, out_features=out_dim, bias=False)\n        \n    def forward(self, cell_imgs):\n        b,cl,ch,h,w = cell_imgs.shape\n        unrolled = cell_imgs.reshape(-1,ch,h,w)\n        ftrs = encoder(unrolled).squeeze()\n        num_ftrs = ftrs.shape[-1]\n        ftrs = ftrs.reshape(b,cl,num_ftrs)        \n        ftrs_max = torch.max(ftrs, 1, keepdim=True)[0].squeeze()\n        ftrs_mean = torch.mean(ftrs, 1, keepdim=True).squeeze()\n        if b == 1: # error with batch size 1 being squeezed out above\n            ftrs_max = ftrs_max[None, ...]\n            ftrs_mean = ftrs_mean[None, ...]\n        ftrs_cat = torch.cat([ftrs_max, ftrs_mean], 1)\n        x = self.bn1(ftrs_cat)\n        x = self.d1(x)\n        x = self.l1(x)\n        x = F.relu(x)\n        x = self.bn2(x)\n        x = self.d2(x)\n        out = self.l2(x) \n        return out\n```\nPS. There's a bug above, I missed `self.` in front of encoder in forward... I think I fixed it in later experiments. Anyways, motivated by your example I'll try to repeat the experiments and figure out how to train this model properly :) ",
          "votes": 1
        },
        {
          "id": 1305803,
          "postDate": "2021-05-13T13:42:05.843Z",
          "content": "<p>Yeah, I'm just not sure if max pooling is proper here because this method is still in essence CAM-based, and for CAM I think the default is mean pooling.<br>\nFor augmentation, I used randomresizedcrop, rotate90, flip, cutout, and mixup. When using mixup, I mixed cells from one whole image with cells from another whole image.   </p>",
          "rawMarkdown": "Yeah, I'm just not sure if max pooling is proper here because this method is still in essence CAM-based, and for CAM I think the default is mean pooling.\nFor augmentation, I used randomresizedcrop, rotate90, flip, cutout, and mixup. When using mixup, I mixed cells from one whole image with cells from another whole image.   ",
          "votes": 1
        },
        {
          "id": 1311755,
          "postDate": "2021-05-17T15:35:18.040Z",
          "content": "<p>So I repeated my experiment with bag-of-cells, the only change I made was number of sampled cells per image (48 -&gt; 8). It would have been my best submission at the time. Lesson learned, and thank you for your sharing!</p>",
          "rawMarkdown": "So I repeated my experiment with bag-of-cells, the only change I made was number of sampled cells per image (48 -> 8). It would have been my best submission at the time. Lesson learned, and thank you for your sharing!",
          "votes": 1
        }
      ]
    },
    {
      "id": 1303702,
      "postDate": "2021-05-12T07:45:43.053Z",
      "content": "<p>Congratulations!!! You make it look sooo easy</p>",
      "rawMarkdown": "Congratulations!!! You make it look sooo easy",
      "votes": 1
    },
    {
      "id": 1303681,
      "postDate": "2021-05-12T07:38:39.077Z",
      "content": "<p>Congratulations, thanks a lot for sharing your approach </p>",
      "rawMarkdown": "Congratulations, thanks a lot for sharing your approach ",
      "votes": 1
    },
    {
      "id": 1303372,
      "postDate": "2021-05-12T03:36:48.837Z",
      "content": "<p>Thanks for sharing your solution. did you try any other form of aggregating the features instead of GAP. for instance attention pooling, weighted average pooling, etc. </p>",
      "rawMarkdown": "Thanks for sharing your solution. did you try any other form of aggregating the features instead of GAP. for instance attention pooling, weighted average pooling, etc. ",
      "votes": 1,
      "replies": [
        {
          "id": 1304061,
          "postDate": "2021-05-12T11:57:57.100Z",
          "content": "<p>I tried GeM, but no big difference in score</p>",
          "rawMarkdown": "I tried GeM, but no big difference in score",
          "votes": 1
        }
      ]
    },
    {
      "id": 1303348,
      "postDate": "2021-05-12T03:10:11.477Z",
      "content": "<p><a href=\"https://www.kaggle.com/wowfattie\" target=\"_blank\">@wowfattie</a>  Congratulations and Thanks for sharing your approach </p>",
      "rawMarkdown": "@wowfattie  Congratulations and Thanks for sharing your approach "
    },
    {
      "id": 1304621,
      "postDate": "2021-05-12T18:33:16.280Z",
      "content": "<p>Thanks for sharing, <a href=\"https://www.kaggle.com/wowfattie\" target=\"_blank\">@wowfattie</a> ! This so elegant, very nice approach! <br>\nAnd congrats on your amazing solo performance! :)</p>",
      "rawMarkdown": "Thanks for sharing, @wowfattie ! This so elegant, very nice approach! \nAnd congrats on your amazing solo performance! :)"
    },
    {
      "id": 1304138,
      "postDate": "2021-05-12T12:53:37.710Z",
      "content": "<p>Congratulations and thanks for sharing! It looks like this is effectively same with cell wise training using image level label, and its approach exists in public kernel.<br>\nMaybe you have other important improve technics for this competition?</p>",
      "rawMarkdown": "Congratulations and thanks for sharing! It looks like this is effectively same with cell wise training using image level label, and its approach exists in public kernel.\nMaybe you have other important improve technics for this competition?",
      "replies": [
        {
          "id": 1304328,
          "postDate": "2021-05-12T14:58:30.767Z",
          "content": "<p>Thanks, but I'm not sure which public kernel has similar approach. I shared some code snippets below for better understanding</p>",
          "rawMarkdown": "Thanks, but I'm not sure which public kernel has similar approach. I shared some code snippets below for better understanding\n"
        }
      ]
    },
    {
      "id": 1303851,
      "postDate": "2021-05-12T09:27:33.417Z",
      "content": "<p>Congrats on the Gold Medal and your Nice Work.!!!</p>",
      "rawMarkdown": "Congrats on the Gold Medal and your Nice Work.!!!"
    }
  ],
  "comments": [
    {
      "id": 1303556,
      "author_name": "ZFTurbo",
      "author_url": "",
      "post_date": "2021-05-12T06:15:22.997000",
      "content": "<p>Very interesting aproach. I have some questions. BM = B * M? Do you give on input B * M images duplicating them? Which M you use in solution? What if image has more or less cells than M? During inference you feed full image or independent cell? If full image how you find which of M images correspond to some cell?</p>\n<p>And for me it's not really obvious why step 4 on inference works…</p>",
      "votes": 5,
      "replies": [
        {
          "id": 1304093,
          "author_name": "Guanshuo Xu",
          "author_url": "",
          "post_date": "2021-05-12T12:23:00.313000",
          "content": "<p>I randomly sample M=8 cell patches from each full image, if number of cells&lt;8, I pad zero patches. So assume B=10, the input of network is 80x4x256x256.<br>\nThis approach doesn't use original full images, it only accepts input of cell patches.<br>\nI'll share some example code later</p>",
          "votes": 2,
          "replies": []
        },
        {
          "id": 1304309,
          "author_name": "Guanshuo Xu",
          "author_url": "",
          "post_date": "2021-05-12T14:48:19.423000",
          "content": "<p>For example, this is my dataset. I added some comments for better understanding. </p>\n<pre><code>class HPADataset(Dataset):\n    # image_list -&gt; list of names of full images\n    # label_dict -&gt; dict of image-level labels\n    # target_size -&gt; output size of cell images (patches)\n    # num_cell_per_image=8\n    # transform -&gt; augmentations\n    def __init__(self, image_list, label_dict, target_size, num_cell_per_image, transform):\n        self.image_list=image_list\n        self.label_dict=label_dict\n        self.target_size=target_size\n        self.num_cell_per_image=num_cell_per_image\n        self.transform=transform\n\n    def __len__(self):\n        return len(self.image_list)\n\n    def __getitem__(self, index):\n       # Initialize memory for output of a single full image composed of num_cell_per_image cells\n        x_image = np.zeros((self.num_cell_per_image, self.target_size, self.target_size, 4), dtype=np.uint8)\n\n        # Load all filenames of pre-saved cells within a single full image\n        image_files = sorted(glob.glob('../../../cell_seg/cell_segmentations/cells/'+self.image_list[index]+'/rgb/'+'/*.png'))\n\n       # permutation\n        perm = torch.randperm(len(image_files))\n\n        # read four channels, padding to square, and augmentations\n        for i in range(min(self.num_cell_per_image, len(image_files))):\n            x_rgb = cv2.imread(image_files[perm[i]])\n            x_y = np.expand_dims(cv2.imread(image_files[perm[i]].replace('rgb', 'y'), cv2.IMREAD_GRAYSCALE), axis=-1)\n            x = np.concatenate((x_rgb, x_y), axis=-1)\n            x = pad_to_square(x)\n            x = self.transform(image=x)['image'] \n            x_image[i] = x\n\n        # channel first\n        x_image = x_image.transpose(0, 3, 1, 2)\n\n        # image-level labels\n        y = self.label_dict[self.image_list[index]]\n        return x_image, y\n</code></pre>\n<p>And this is the model class for training.</p>\n<pre><code>class HPAImageResnet200d(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = timm.create_model('resnet200d', drop_path_rate=0.2, pretrained=True, in_chans=4)\n        self.avg_pool_cell = nn.AdaptiveAvgPool2d(1) # GAP within each cell image\n        self.avg_pool_image = nn.AdaptiveAvgPool1d(1) # image-level GAP\n        self.last_linear = nn.Linear(2048, 18, bias=False)\n\n    def forward(self, x, num_cell_per_image):\n        # input size 80x4x256x256, assume we sample 8 cells from each of the 10 full images\n        bs = x.size(0)\n        x = self.net.forward_features(x)  # output shape before GAP 80x2048x8x8\n        x = self.avg_pool_cell(x) # output shape 80x2048x1x1 \n        x = x.view(x.size(0), -1) # output shape 80x2048\n        x = x.view(bs//num_cell_per_image, num_cell_per_image, -1)  # output shape 10x8x2048\n        x = x.permute(0, 2, 1)  # 10x2048x8\n        x = self.avg_pool_image(x)  # 10x2048x1 \n        x = x.view(bs//num_cell_per_image, -1)  # output shape 10x2048\n        x = self.last_linear(x) # 10x18\n        return x\n</code></pre>\n<p>The output of dataloader has shape 10x8x4x256x256, so we make it 80x4x256x256 before inputting it to the model<br>\n<code>logits = model(images.view(-1, images.size(2), images.size(3), images.size(4)), num_cell_per_image)</code></p>\n<p>Model class for inference. Only need to add a wrapper for cell prediction. We can see this wrapper just removed a few lines in the training model, and we directly have cell-level predictions.</p>\n<pre><code># This part is same as the training code\nclass HPAImageResnet200d(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = timm.create_model('resnet200d', drop_path_rate=0.2, pretrained=False, in_chans=4)\n        self.avg_pool_cell = nn.AdaptiveAvgPool2d(1)\n        self.avg_pool_image = nn.AdaptiveAvgPool1d(1)\n        self.last_linear = nn.Linear(2048, 18, bias=False)\n    def forward(self, x, num_cell_per_image):\n        bs = x.size(0)\n        x = self.net.forward_features(x)\n        x = self.avg_pool_cell(x)\n        x = x.view(x.size(0), -1)\n        x = x.view(bs//num_cell_per_image, num_cell_per_image, -1)\n        x = x.permute(0, 2, 1)\n        x = self.avg_pool_image(x)\n        x = x.view(bs//num_cell_per_image, -1)\n        x = self.last_linear(x)\n        return x\n\n# Add a wrapper for cell-level prediction for convenience. \nclass HPAImageResnet200dCellPredict(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.net = HPAImageResnet200d()\n    def forward(self, x):\n        x = self.net.net.forward_features(x)  # output shape 80x2048x8x8\n        x = self.net.avg_pool_cell(x)  # output shape 80x2048x1x1 \n        x = x.view(x.size(0), -1) # output shape 80x2048\n        x = self.net.last_linear(x)    # cell-level predictions, output shape 80x2048\n        return x\n</code></pre>",
          "votes": 2,
          "replies": []
        },
        {
          "id": 1304517,
          "author_name": "ZFTurbo",
          "author_url": "",
          "post_date": "2021-05-12T17:04:23.990000",
          "content": "<p>Great, thanks!</p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 1303571,
      "author_name": "Dieter",
      "author_url": "",
      "post_date": "2021-05-12T06:24:00.090000",
      "content": "<p>Congrats <a href=\"https://www.kaggle.com/wowfattie\" target=\"_blank\">@wowfattie</a>. Interestingly, I had same model in my ensemble, but with attention pooling instead of GAP in step 5. For images that had less than M  cells, did you pad or sample some cells more often? What backbone did you use? Did you preprocess the single cell images or crop on the fly?</p>",
      "votes": 4,
      "replies": [
        {
          "id": 1304098,
          "author_name": "Guanshuo Xu",
          "author_url": "",
          "post_date": "2021-05-12T12:27:03.333000",
          "content": "<p>I pad zero cell images if not enough cells. I used resnet200d and efficientnetb5, input size 288x288. All cells are pre-saved on disk for faster loading in the dataset. </p>",
          "votes": 2,
          "replies": []
        }
      ]
    },
    {
      "id": 1304341,
      "author_name": "Darek Kłeczek",
      "author_url": "",
      "post_date": "2021-05-12T15:04:44.090000",
      "content": "<p>Congratulations! I also developed a very similar approach (called it 'bag of cells' - see #4 below) but couldn't get it to train to a good performance. I think that reflects my skill gap :) I'd love to learn the details of how this model was trained to see what I was doing wrong :) <br>\n<img src=\"https://pbs.twimg.com/media/E1Ml23qXMAYHNoe?format=jpg&amp;name=large\" alt=\"bag of cells\"></p>",
      "votes": 1,
      "replies": [
        {
          "id": 1304420,
          "author_name": "Guanshuo Xu",
          "author_url": "",
          "post_date": "2021-05-12T15:55:45.713000",
          "content": "<p>Hi Darek, I think this sheet summarizes pretty well the pros and cons of the major approaches to this problem. Yes, my approach looks closer to number 4, and it solves all the weakness of number 2 and 3. </p>\n<p>But I'm not sure if your #4 is same as mine. <br>\nFirst, did you do cell-level global pooling before image-level global pooling? It is important to do global pooling two times. <br>\nSecond, it might be wrong to use global max pooling for image-level pooling, global average pooling maybe the only choice here. But you are free to choice other types of global pooling for the cell-level pooling.</p>",
          "votes": 1,
          "replies": []
        },
        {
          "id": 1305064,
          "author_name": "Darek Kłeczek",
          "author_url": "",
          "post_date": "2021-05-13T04:48:44.337000",
          "content": "<p>Thanks for response Guanshuo, I appreciate it! Yes, I did GAP layer twice - first for individual cells, then at image level. I started with max pooling at image level, but realized it might result in a very sparse signal, so later used avg+max concat pooling. I was very bullish on this architecture as it seemed like the most elegant way of solving the challenge, I did around 10 experiments with it, but all of them performed ~0.03-0.04 worse than other approaches, so I finally dropped it. I'm trying to think about what mistakes I may have made, I think I didn't do enough augmentation and I chose too many cells per image (I used 48 cells per image in most of my experiments). For reference, this is one of the architectures I tested (encoder is resnet-50 up until and including GAP layer):</p>\n<pre><code>class BagOfCellsModel(Module):\n    def __init__(self, encoder, out_dim):\n        self.encoder = encoder\n        self.bn1 = nn.BatchNorm1d(4096, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n        self.d1 = nn.Dropout(p=0.25, inplace=False)\n        self.l1 = nn.Linear(in_features=4096, out_features=2048, bias=False)\n        self.bn2 = nn.BatchNorm1d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n        self.d2 = nn.Dropout(p=0.5, inplace=False)\n        self.l2 = nn.Linear(in_features=2048, out_features=out_dim, bias=False)\n\n    def forward(self, cell_imgs):\n        b,cl,ch,h,w = cell_imgs.shape\n        unrolled = cell_imgs.reshape(-1,ch,h,w)\n        ftrs = encoder(unrolled).squeeze()\n        num_ftrs = ftrs.shape[-1]\n        ftrs = ftrs.reshape(b,cl,num_ftrs)        \n        ftrs_max = torch.max(ftrs, 1, keepdim=True)[0].squeeze()\n        ftrs_mean = torch.mean(ftrs, 1, keepdim=True).squeeze()\n        if b == 1: # error with batch size 1 being squeezed out above\n            ftrs_max = ftrs_max[None, ...]\n            ftrs_mean = ftrs_mean[None, ...]\n        ftrs_cat = torch.cat([ftrs_max, ftrs_mean], 1)\n        x = self.bn1(ftrs_cat)\n        x = self.d1(x)\n        x = self.l1(x)\n        x = F.relu(x)\n        x = self.bn2(x)\n        x = self.d2(x)\n        out = self.l2(x) \n        return out\n</code></pre>\n<p>PS. There's a bug above, I missed <code>self.</code> in front of encoder in forward… I think I fixed it in later experiments. Anyways, motivated by your example I'll try to repeat the experiments and figure out how to train this model properly :) </p>",
          "votes": 1,
          "replies": []
        },
        {
          "id": 1305803,
          "author_name": "Guanshuo Xu",
          "author_url": "",
          "post_date": "2021-05-13T13:42:05.843000",
          "content": "<p>Yeah, I'm just not sure if max pooling is proper here because this method is still in essence CAM-based, and for CAM I think the default is mean pooling.<br>\nFor augmentation, I used randomresizedcrop, rotate90, flip, cutout, and mixup. When using mixup, I mixed cells from one whole image with cells from another whole image.   </p>",
          "votes": 1,
          "replies": []
        },
        {
          "id": 1311755,
          "author_name": "Darek Kłeczek",
          "author_url": "",
          "post_date": "2021-05-17T15:35:18.040000",
          "content": "<p>So I repeated my experiment with bag-of-cells, the only change I made was number of sampled cells per image (48 -&gt; 8). It would have been my best submission at the time. Lesson learned, and thank you for your sharing!</p>",
          "votes": 1,
          "replies": []
        }
      ]
    },
    {
      "id": 1303702,
      "author_name": "Bibek",
      "author_url": "",
      "post_date": "2021-05-12T07:45:43.053000",
      "content": "<p>Congratulations!!! You make it look sooo easy</p>",
      "votes": 1,
      "replies": []
    },
    {
      "id": 1303681,
      "author_name": "Salim Khazem",
      "author_url": "",
      "post_date": "2021-05-12T07:38:39.077000",
      "content": "<p>Congratulations, thanks a lot for sharing your approach </p>",
      "votes": 1,
      "replies": []
    },
    {
      "id": 1303372,
      "author_name": "yuvaramsingh",
      "author_url": "",
      "post_date": "2021-05-12T03:36:48.837000",
      "content": "<p>Thanks for sharing your solution. did you try any other form of aggregating the features instead of GAP. for instance attention pooling, weighted average pooling, etc. </p>",
      "votes": 1,
      "replies": [
        {
          "id": 1304061,
          "author_name": "Guanshuo Xu",
          "author_url": "",
          "post_date": "2021-05-12T11:57:57.100000",
          "content": "<p>I tried GeM, but no big difference in score</p>",
          "votes": 1,
          "replies": []
        }
      ]
    },
    {
      "id": 1303348,
      "author_name": "Tensor Girl",
      "author_url": "",
      "post_date": "2021-05-12T03:10:11.477000",
      "content": "<p><a href=\"https://www.kaggle.com/wowfattie\" target=\"_blank\">@wowfattie</a>  Congratulations and Thanks for sharing your approach </p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1304621,
      "author_name": "Raman",
      "author_url": "",
      "post_date": "2021-05-12T18:33:16.280000",
      "content": "<p>Thanks for sharing, <a href=\"https://www.kaggle.com/wowfattie\" target=\"_blank\">@wowfattie</a> ! This so elegant, very nice approach! <br>\nAnd congrats on your amazing solo performance! :)</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 1304138,
      "author_name": "corochann",
      "author_url": "",
      "post_date": "2021-05-12T12:53:37.710000",
      "content": "<p>Congratulations and thanks for sharing! It looks like this is effectively same with cell wise training using image level label, and its approach exists in public kernel.<br>\nMaybe you have other important improve technics for this competition?</p>",
      "votes": 0,
      "replies": [
        {
          "id": 1304328,
          "author_name": "Guanshuo Xu",
          "author_url": "",
          "post_date": "2021-05-12T14:58:30.767000",
          "content": "<p>Thanks, but I'm not sure which public kernel has similar approach. I shared some code snippets below for better understanding</p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 1303851,
      "author_name": "Wang Xing",
      "author_url": "",
      "post_date": "2021-05-12T09:27:33.417000",
      "content": "<p>Congrats on the Gold Medal and your Nice Work.!!!</p>",
      "votes": 0,
      "replies": []
    }
  ],
  "raw_markdown_by_id": {
    "1303291": "Since we are given image-levels labels and cell segmentations, we can train on image labels and predict cell labels directly.\n\nLet's say during training we sample B images, within each image we sample M cells. Let's further assume input channels=4, input_size=256x256, feature_dim=2048. \n\n1. Input shape to CNNs -> BMx4x256x256\n2. CNN feature extraction -> BMx2048x8x8\n3. GAP -> BMx2048\n4. Reshape and permute -> Bx2048xM\n5. Another GAP -> Bx2048 \n6. Last linear -> Bx18 (compute loss with image labels)\n\nDuring inference, to predict cell labels, we remove step 4 and 5:\n1. Input shape to CNNs -> BMx4x256x256\n2. CNN feature extraction -> BMx2048x8x8\n3. GAP -> BMx2048\n6. Last linear -> BMx18 (cell label prediction completed)\n\nThis approach is partly inspired from the [lafoss concat tile pooling](https://www.kaggle.com/c/prostate-cancer-grade-assessment/discussion/146855) and the class activation mapping. \n\nPostprocessings that bring some improvement, ranked roughly by their importance:\n1) Downscale border cells predictions.\n2) Cell-level finetuning with OOF predictions in CV.\n3) 0.9xcell predictions+0.1xaverage cell predictions within image, to make use of label correlations within a image. It should be better to train some independent image level models though [example](https://www.kaggle.com/c/hpa-single-cell-image-classification/discussion/230940). Here I just reused cell predictions.",
    "1303556": "Very interesting aproach. I have some questions. BM = B * M? Do you give on input B * M images duplicating them? Which M you use in solution? What if image has more or less cells than M? During inference you feed full image or independent cell? If full image how you find which of M images correspond to some cell?\n\nAnd for me it's not really obvious why step 4 on inference works...",
    "1303571": "Congrats @wowfattie. Interestingly, I had same model in my ensemble, but with attention pooling instead of GAP in step 5. For images that had less than M  cells, did you pad or sample some cells more often? What backbone did you use? Did you preprocess the single cell images or crop on the fly?",
    "1304341": "Congratulations! I also developed a very similar approach (called it 'bag of cells' - see #4 below) but couldn't get it to train to a good performance. I think that reflects my skill gap :) I'd love to learn the details of how this model was trained to see what I was doing wrong :) \n![bag of cells](https://pbs.twimg.com/media/E1Ml23qXMAYHNoe?format=jpg&name=large)",
    "1303702": "Congratulations!!! You make it look sooo easy",
    "1303681": "Congratulations, thanks a lot for sharing your approach ",
    "1303372": "Thanks for sharing your solution. did you try any other form of aggregating the features instead of GAP. for instance attention pooling, weighted average pooling, etc. ",
    "1303348": "@wowfattie  Congratulations and Thanks for sharing your approach ",
    "1304621": "Thanks for sharing, @wowfattie ! This so elegant, very nice approach! \nAnd congrats on your amazing solo performance! :)",
    "1304138": "Congratulations and thanks for sharing! It looks like this is effectively same with cell wise training using image level label, and its approach exists in public kernel.\nMaybe you have other important improve technics for this competition?",
    "1303851": "Congrats on the Gold Medal and your Nice Work.!!!"
  }
}