{"cells":[{"metadata":{},"cell_type":"markdown","source":"# iMet Collection 2019 - FGVC6\n**Simple baseline for iMet Collection 2019 competition using fastai v1**\n* Model: densenet201\n* Loss: Focal loss\n* Metric: $F_{2}$ score\n\n**What to try next?**\n* Different models\n* Optimize hyperparameter choice\n* Few-shot learning to improve score on classes with very few samples"},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"import fastai\nfrom fastai.vision import *\nfastai.__version__","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Initial setup"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = Path('../input/imet-2019-fgvc6/') # iMet data path","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Making pretrained weights work without needing to find the default filename\nfrom torch.utils import model_zoo\nPath('models').mkdir(exist_ok=True)\n!cp '../input/densenet201/densenet201.pth' 'models/'\ndef load_url(*args, **kwargs):\n    model_dir = Path('models')\n    filename  = 'densenet201.pth'\n    if not (model_dir/filename).is_file(): raise FileNotFoundError\n    return torch.load(model_dir/filename)\nmodel_zoo.load_url = load_url","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Load train dataframe\ntrain_df = pd.read_csv(path/'train.csv')\ntrain_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Load labels dataframe\nlabels_df = pd.read_csv(path/'labels.csv')\nlabels_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Load sample submission\ntest_df = pd.read_csv(path/'sample_submission.csv')\ntest_df.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Create data object using datablock API"},{"metadata":{"trusted":true},"cell_type":"code","source":"train, test = [ImageList.from_df(df, path=path, cols='id', folder=folder, suffix='.png') \n               for df, folder in zip([train_df, test_df], ['train', 'test'])]\ndata = (train.split_by_rand_pct(0.2, seed=42)\n        .label_from_df(cols='attribute_ids', label_delim=' ')\n        .add_test(test)\n        .transform(get_transforms(), size=128)\n        .databunch(path=Path('.'), bs=64).normalize())","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"data.show_batch()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Create learner with densenet121 and FocalLoss\nFor problems with high class imbalance Focal Loss is usually a better choice than the usual Cross Entropy Loss."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Source: https://www.kaggle.com/c/human-protein-atlas-image-classification/discussion/78109\nclass FocalLoss(nn.Module):\n    def __init__(self, gamma=2):\n        super().__init__()\n        self.gamma = gamma\n\n    def forward(self, input, target):\n        if not (target.size() == input.size()):\n            raise ValueError(\"Target size ({}) must be the same as input size ({})\"\n                             .format(target.size(), input.size()))\n\n        max_val = (-input).clamp(min=0)\n        loss = input - input * target + max_val + \\\n               ((-max_val).exp() + (-input - max_val).exp()).log()\n\n        invprobs = F.logsigmoid(-input * (target * 2.0 - 1.0))\n        loss = (invprobs * self.gamma).exp() * loss\n\n        return loss.sum(dim=1).mean()\n    \nclass FbetaLoss(nn.Module):\n    def __init__(self, beta=1):\n        super(FbetaLoss, self).__init__()\n        self.small_value = 1e-6\n        self.beta = beta\n\n    def forward(self, logits, labels):\n        beta = self.beta\n        batch_size = logits.size()[0]\n        p = F.sigmoid(logits)\n        l = labels\n        num_pos = torch.sum(p, 1) + self.small_value\n        num_pos_hat = torch.sum(l, 1) + self.small_value\n        tp = torch.sum(l * p, 1)\n        precise = tp / num_pos\n        recall = tp / num_pos_hat\n        fs = (1 + beta * beta) * precise * recall / (beta * beta * precise + recall + self.small_value)\n        loss = fs.sum() / batch_size\n        return 1 - loss\n\nclass CombineLoss(nn.Module):\n    def __init__(self):\n        super(CombineLoss, self).__init__()\n        self.fbeta_loss = FbetaLoss(beta=2)\n        self.focal_loss = FocalLoss()\n        \n    def forward(self, logits, labels):\n        loss_beta = self.fbeta_loss(logits, labels)\n        loss_focal = self.focal_loss(logits, labels)\n        return 0.5 * loss_beta + 0.5 * loss_focal","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"learn = cnn_learner(data, base_arch=models.densenet201, loss_func=CombineLoss(), metrics=fbeta)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Train the model"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Find a good learning rate\nlearn.lr_find()\nlearn.recorder.plot()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"lr = 3e-2\nlearn.fit_one_cycle(3, slice(lr))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"learn.unfreeze()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"learn.lr_find()\nlearn.recorder.plot()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"lr = 1e-3\nlearn.fit_one_cycle(21, slice(lr/10, lr))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Get predictions"},{"metadata":{"trusted":true},"cell_type":"code","source":"def find_best_fixed_threshold(preds, targs, do_plot=True):\n    score = []\n    thrs = np.arange(0, 0.5, 0.01)\n    for thr in progress_bar(thrs):\n        score.append(fbeta(valid_preds[0],valid_preds[1], thresh=thr))\n    score = np.array(score)\n    pm = score.argmax()\n    best_thr, best_score = thrs[pm], score[pm].item()\n    print(f'thr={best_thr:.3f}', f'F2={best_score:.3f}')\n    if do_plot:\n        plt.plot(thrs, score)\n        plt.vlines(x=best_thr, ymin=score.min(), ymax=score.max())\n        plt.text(best_thr+0.03, best_score-0.01, f'$F_{2}=${best_score:.3f}', fontsize=14);\n        plt.show()\n    return best_thr\n\ni2c = np.array([[i, c] for c, i in learn.data.train_ds.y.c2i.items()]).astype(int) # indices to class number correspondence\n\ndef join_preds(preds, thr):\n    return [' '.join(i2c[np.where(t==1)[0],1].astype(str)) for t in (preds[0].sigmoid()>thr).long()]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Validation predictions\nvalid_preds = learn.get_preds(DatasetType.Valid)\nbest_thr = find_best_fixed_threshold(*valid_preds)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Test predictions\ntest_preds = learn.get_preds(DatasetType.Test)\ntest_df.attribute_ids = join_preds(test_preds, best_thr)\ntest_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_df.to_csv('submission.csv', index=False)","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}