{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Intro\n\n","metadata":{}},{"cell_type":"markdown","source":"This notebook is based on a starter notebook made by user [Michaln](https://www.kaggle.com/michaln), for the [Hotel-ID to Combat Human Trafficking 2022 - FGVC9](https://www.kaggle.com/competitions/hotel-id-to-combat-human-trafficking-2022-fgvc9) competition. \n\n- Starter Training notebook: [Hotel-ID starter - -similarity - inference](https://www.kaggle.com/code/michaln/hotel-id-starter-similarity-training)\n- Starter Inference notebook: [Hotel-ID starter - similarity- inference](https://www.kaggle.com/code/michaln/hotel-id-starter-similarity-inference)\n- Another starter notebook: [Hotel-ID starter - classification - traning](https://www.kaggle.com/code/michaln/hotel-id-starter-classification-traning)\n- With its inference part: [Hotel-ID starter - classification - inference](https://www.kaggle.com/code/michaln/hotel-id-starter-classification-inference)\n\n## Basic Overview\nUsing a pre-trained network, we generate an embedding for the input, this embedding is used to find the distance to other embeddings for all hotels. This notebook uses cosine distance for that. The 5 closest matches are chosen for the map5 score. We use ArcMargin to push the differences between embeddings from different classes. Additionally, we use various data augmentation techniques to divirsify the training data.\n","metadata":{}},{"cell_type":"markdown","source":"## Data\nThis notebook uses preprocessed images that were resized and padded to 512x512 pixels.\n\nUsed dataset: [Hotel-ID 2022 train images 512x512](https://www.kaggle.com/datasets/michaln/hotelid-2022-train-images-512x512) created by [Michaln](https://www.kaggle.com/michaln) notebook.","metadata":{}},{"cell_type":"markdown","source":"# Imports","metadata":{"id":"MyC4gTwZ3MKJ","papermill":{"duration":0.020375,"end_time":"2022-03-23T20:08:58.820405","exception":false,"start_time":"2022-03-23T20:08:58.80003","status":"completed"},"tags":[]}},{"cell_type":"code","source":"!pip install timm","metadata":{"papermill":{"duration":18.268618,"end_time":"2022-03-23T20:09:17.10966","exception":false,"start_time":"2022-03-23T20:08:58.841042","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:30.795438Z","iopub.execute_input":"2022-06-02T13:49:30.796078Z","iopub.status.idle":"2022-06-02T13:49:38.424044Z","shell.execute_reply.started":"2022-06-02T13:49:30.796002Z","shell.execute_reply":"2022-06-02T13:49:38.423111Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"!pip install git+https://github.com/ufoym/imbalanced-dataset-sampler.git\nfrom torchsampler import ImbalancedDatasetSampler","metadata":{"execution":{"iopub.status.busy":"2022-06-02T13:49:38.432247Z","iopub.execute_input":"2022-06-02T13:49:38.43246Z","iopub.status.idle":"2022-06-02T13:49:50.57464Z","shell.execute_reply.started":"2022-06-02T13:49:38.432435Z","shell.execute_reply":"2022-06-02T13:49:50.573892Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport random\nimport os","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","id":"u0Bz2ktn2_ap","papermill":{"duration":0.032275,"end_time":"2022-03-23T20:09:17.168351","exception":false,"start_time":"2022-03-23T20:09:17.136076","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:50.576235Z","iopub.execute_input":"2022-06-02T13:49:50.576496Z","iopub.status.idle":"2022-06-02T13:49:50.581688Z","shell.execute_reply.started":"2022-06-02T13:49:50.576459Z","shell.execute_reply":"2022-06-02T13:49:50.580944Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"from sklearn.metrics import accuracy_score\nfrom PIL import Image as pil_image\nfrom tqdm import tqdm\n\nimport matplotlib\nimport matplotlib.pyplot as plt\n\nimport plotly.graph_objects as go\nimport plotly.express as px\nfrom plotly.subplots import make_subplots","metadata":{"id":"tOszKuxt3PXn","papermill":{"duration":3.425867,"end_time":"2022-03-23T20:09:20.619496","exception":false,"start_time":"2022-03-23T20:09:17.193629","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:50.584107Z","iopub.execute_input":"2022-06-02T13:49:50.584294Z","iopub.status.idle":"2022-06-02T13:49:52.48356Z","shell.execute_reply.started":"2022-06-02T13:49:50.584271Z","shell.execute_reply":"2022-06-02T13:49:52.482797Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader\nfrom torch.utils.data import Dataset\n\nimport timm\nfrom timm.optim import Lookahead\n\nfrom sklearn.metrics.pairwise import cosine_similarity","metadata":{"id":"uQE7wYFR3QxV","papermill":{"duration":2.460075,"end_time":"2022-03-23T20:09:23.106294","exception":false,"start_time":"2022-03-23T20:09:20.646219","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:52.485181Z","iopub.execute_input":"2022-06-02T13:49:52.485471Z","iopub.status.idle":"2022-06-02T13:49:52.862295Z","shell.execute_reply.started":"2022-06-02T13:49:52.4854Z","shell.execute_reply":"2022-06-02T13:49:52.861391Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Global","metadata":{"id":"tirOg6jm3aIB","papermill":{"duration":0.025747,"end_time":"2022-03-23T20:09:23.159882","exception":false,"start_time":"2022-03-23T20:09:23.134135","status":"completed"},"tags":[]}},{"cell_type":"code","source":"IMG_SIZE = 512\nSEED = 42\nN_MATCHES = 5\n\nPROJECT_FOLDER = \"../input/hotel-id-to-combat-human-trafficking-2022-fgvc9/\"\nDATA_FOLDER = \"../input/hotelid-2022-train-images-512x512/\"\nIMAGE_FOLDER = DATA_FOLDER + \"images/\"\nOUTPUT_FOLDER = \"\"\n\ntrain_df = pd.read_csv(os.path.join(DATA_FOLDER, 'train.csv'))","metadata":{"id":"DV7qHDuYGoJH","papermill":{"duration":0.086034,"end_time":"2022-03-23T20:09:23.2714","exception":false,"start_time":"2022-03-23T20:09:23.185366","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:52.864076Z","iopub.execute_input":"2022-06-02T13:49:52.86433Z","iopub.status.idle":"2022-06-02T13:49:52.901694Z","shell.execute_reply.started":"2022-06-02T13:49:52.864295Z","shell.execute_reply":"2022-06-02T13:49:52.900871Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(os.listdir(PROJECT_FOLDER))","metadata":{"id":"TB9CXg8U3bbQ","papermill":{"duration":0.032796,"end_time":"2022-03-23T20:09:23.329909","exception":false,"start_time":"2022-03-23T20:09:23.297113","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:52.903166Z","iopub.execute_input":"2022-06-02T13:49:52.903443Z","iopub.status.idle":"2022-06-02T13:49:52.909097Z","shell.execute_reply.started":"2022-06-02T13:49:52.903406Z","shell.execute_reply":"2022-06-02T13:49:52.908359Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def seed_everything(seed):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True","metadata":{"id":"csp2OMgo2_ar","papermill":{"duration":0.03307,"end_time":"2022-03-23T20:09:23.38914","exception":false,"start_time":"2022-03-23T20:09:23.35607","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:52.910795Z","iopub.execute_input":"2022-06-02T13:49:52.91126Z","iopub.status.idle":"2022-06-02T13:49:52.917815Z","shell.execute_reply.started":"2022-06-02T13:49:52.91122Z","shell.execute_reply":"2022-06-02T13:49:52.916948Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Dataset and transformations","metadata":{"id":"8V_xuoN73lON","papermill":{"duration":0.025486,"end_time":"2022-03-23T20:09:23.440614","exception":false,"start_time":"2022-03-23T20:09:23.415128","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"Coarse dropout with fill_value=(255,0,0) (full red channel) is used to simulate the occlussions like the one in test dataset. \n```python\nA.CoarseDropout(p=1., max_holes=1, \n                min_height=IMG_SIZE//4, max_height=IMG_SIZE//2,\n                min_width=IMG_SIZE//4,  max_width=IMG_SIZE//2, \n                fill_value=(255,0,0))\n```\n\nPeculiarly, we found that applying the random brightness augmentation after the dropout did not make the results better or significantly worse.  ","metadata":{}},{"cell_type":"code","source":"import albumentations as A\nimport albumentations.pytorch as APT\nimport cv2 \n\n# used for training dataset - augmentations and occlusions\ntrain_transform = A.Compose([\n    A.RandomResizedCrop(height=IMG_SIZE, width=IMG_SIZE, scale=(0.25, 1.0), p=0.75),\n    A.HorizontalFlip(p=0.75),\n    A.VerticalFlip(p=0.25),\n    A.ShiftScaleRotate(p=0.5, border_mode=cv2.BORDER_CONSTANT),\n    A.OpticalDistortion(p=0.25),\n    A.Perspective(p=0.25),\n    A.CoarseDropout(p=0.5, min_holes=1, max_holes=6, \n                    min_height=IMG_SIZE//16, max_height=IMG_SIZE//4,\n                    min_width=IMG_SIZE//16,  max_width=IMG_SIZE//4), # normal coarse dropout\n    \n    A.CoarseDropout(p=0.75, max_holes=1, \n                    min_height=IMG_SIZE//4, max_height=IMG_SIZE//2,\n                    min_width=IMG_SIZE//4,  max_width=IMG_SIZE//2, \n                    fill_value=(255,0,0)),# simulating occlusions in test data\n\n    A.RandomBrightnessContrast(p=0.75),\n    A.ToFloat(),\n    APT.transforms.ToTensorV2(),\n])\n\n# used for validation dataset - only occlusions\nval_transform = A.Compose([\n    A.CoarseDropout(p=0.75, max_holes=1, \n                    min_height=IMG_SIZE//4, max_height=IMG_SIZE//2,\n                    min_width=IMG_SIZE//4,  max_width=IMG_SIZE//2, \n                    fill_value=(255,0,0)),# simulating occlusions\n    A.ToFloat(),\n    APT.transforms.ToTensorV2(),\n])\n\n# no augmentations\nbase_transform = A.Compose([\n    A.ToFloat(),\n    APT.transforms.ToTensorV2(),\n])","metadata":{"id":"8ucWZHeG2_as","papermill":{"duration":0.919086,"end_time":"2022-03-23T20:09:24.385553","exception":false,"start_time":"2022-03-23T20:09:23.466467","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:52.919395Z","iopub.execute_input":"2022-06-02T13:49:52.919722Z","iopub.status.idle":"2022-06-02T13:49:53.096708Z","shell.execute_reply.started":"2022-06-02T13:49:52.919678Z","shell.execute_reply":"2022-06-02T13:49:53.095949Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class HotelTrainDataset(Dataset):\n    def __init__(self, data, transform=None, data_path=\"train_images/\"):\n        self.data = data\n        self.data_path = data_path\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.data)\n    \n    def __getitem__(self, idx):\n        record = self.data.iloc[idx]\n        image_path = self.data_path + record[\"image_id\"]\n        image = np.array(pil_image.open(image_path)).astype(np.uint8)\n\n        if self.transform:\n            transformed = self.transform(image=image)\n            image = transformed[\"image\"]\n        \n        return {\n            \"image\" : image,\n            \"target\" : record['hotel_id_code'],\n        }\n    \n    def get_labels(self):\n        return list(self.data.loc[:,'hotel_id_code'])","metadata":{"id":"EiLYsfKq2_at","papermill":{"duration":0.035446,"end_time":"2022-03-23T20:09:24.447084","exception":false,"start_time":"2022-03-23T20:09:24.411638","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:53.097858Z","iopub.execute_input":"2022-06-02T13:49:53.09814Z","iopub.status.idle":"2022-06-02T13:49:53.105342Z","shell.execute_reply.started":"2022-06-02T13:49:53.098104Z","shell.execute_reply":"2022-06-02T13:49:53.104638Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model\n\nIncludes the ArcMargin class.","metadata":{}},{"cell_type":"code","source":"# from https://github.com/ronghuaiyang/arcface-pytorch\nimport math\nimport torch.nn.functional as F\n\nclass ArcMarginProduct(nn.Module):\n    r\"\"\"Implement of large margin arc distance: :\n        Args:\n            in_features: size of each input sample\n            out_features: size of each output sample\n            s: norm of input feature\n            m: margin\n            cos(theta + m)\n        \"\"\"\n    def __init__(self, in_features, out_features, s=30.0, m=0.50, easy_margin=False):\n        super(ArcMarginProduct, self).__init__()\n        self.in_features = in_features\n        self.out_features = out_features\n        self.s = s\n        self.m = m\n        self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features))\n        nn.init.xavier_uniform_(self.weight)\n\n        self.easy_margin = easy_margin\n        self.cos_m = math.cos(m)\n        self.sin_m = math.sin(m)\n        self.th = math.cos(math.pi - m)\n        self.mm = math.sin(math.pi - m) * m\n\n    def forward(self, input, label):\n        # --------------------------- cos(theta) & phi(theta) ---------------------------\n        cosine = F.linear(F.normalize(input), F.normalize(self.weight))\n        sine = torch.sqrt((1.0 - torch.pow(cosine, 2)).clamp(0, 1))\n        phi = cosine * self.cos_m - sine * self.sin_m\n        if self.easy_margin:\n            phi = torch.where(cosine > 0, phi, cosine)\n        else:\n            phi = torch.where(cosine > self.th, phi, cosine - self.mm)\n        # --------------------------- convert label to one-hot ---------------------------\n        # one_hot = torch.zeros(cosine.size(), requires_grad=True, device='cuda')\n        one_hot = torch.zeros(cosine.size(), device='cuda')\n        one_hot.scatter_(1, label.view(-1, 1).long(), 1)\n        # -------------torch.where(out_i = {x_i if condition_i else y_i) -------------\n        output = (one_hot * phi) + ((1.0 - one_hot) * cosine)  # you can use torch.where if your torch.__version__ is 0.4\n        output *= self.s\n        # print(output)\n\n        return output","metadata":{"execution":{"iopub.status.busy":"2022-06-02T13:49:53.107368Z","iopub.execute_input":"2022-06-02T13:49:53.108217Z","iopub.status.idle":"2022-06-02T13:49:53.123474Z","shell.execute_reply.started":"2022-06-02T13:49:53.108179Z","shell.execute_reply":"2022-06-02T13:49:53.1227Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class EmbeddingModel(nn.Module):\n    def __init__(self, n_classes=100, embedding_size=64, backbone_name=\"efficientnet_b0\"):\n        super(EmbeddingModel, self).__init__()\n        self.embed_size = embedding_size\n        self.backbone = timm.create_model(backbone_name, num_classes=0, pretrained=True)\n        o = self.backbone(torch.randn(1, 3, IMG_SIZE, IMG_SIZE))\n        in_features = o.shape[1]\n        \n        self.arcface = ArcMarginProduct(embedding_size, n_classes, m=0.2)\n       \n        self.post = nn.Sequential(\n            nn.utils.weight_norm(nn.Linear(in_features, self.embed_size*2), dim=None),\n            nn.BatchNorm1d(self.embed_size*2),\n            nn.Dropout(0.2),\n            nn.utils.weight_norm(nn.Linear(self.embed_size*2, self.embed_size)),\n            nn.BatchNorm1d(self.embed_size),\n        )\n        \n    def forward(self, x, targets = None):\n        x = self.backbone(x)\n        x = x.view(x.size(0), -1)\n        x = self.post(x)\n        if targets is not None:\n            x = self.arcface(x, targets)\n        return x","metadata":{"id":"_2mse3zX3pFQ","papermill":{"duration":0.033646,"end_time":"2022-03-23T20:09:24.558453","exception":false,"start_time":"2022-03-23T20:09:24.524807","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:53.126637Z","iopub.execute_input":"2022-06-02T13:49:53.127018Z","iopub.status.idle":"2022-06-02T13:49:53.138593Z","shell.execute_reply.started":"2022-06-02T13:49:53.126928Z","shell.execute_reply":"2022-06-02T13:49:53.137853Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model helper functions","metadata":{"id":"mTFCinps35ci","papermill":{"duration":0.025639,"end_time":"2022-03-23T20:09:24.609989","exception":false,"start_time":"2022-03-23T20:09:24.58435","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# method to iterate loader and generate embeddings of images\n# returns embeddings and image class\ndef generate_embeddings(loader, model, bar_desc=\"Generating embeds\"):\n    targets_all = []\n    outputs_all = []\n    \n    model.eval()\n    with torch.no_grad():\n        t = tqdm(loader, desc=bar_desc)\n        for i, sample in enumerate(t):\n            input = sample['image'].to(args.device)\n            target = sample['target'].to(args.device)\n            output = model(input)\n            \n            targets_all.extend(target.cpu().numpy())\n            outputs_all.extend(output.detach().cpu().numpy())\n\n    targets_all = np.array(targets_all).astype(np.float32)\n    outputs_all = np.array(outputs_all).astype(np.float32)\n            \n    return outputs_all, targets_all","metadata":{"execution":{"iopub.status.busy":"2022-06-02T13:49:53.143331Z","iopub.execute_input":"2022-06-02T13:49:53.143596Z","iopub.status.idle":"2022-06-02T13:49:53.152753Z","shell.execute_reply.started":"2022-06-02T13:49:53.143566Z","shell.execute_reply":"2022-06-02T13:49:53.152011Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def save_checkpoint(model, scheduler, optimizer, epoch, name, loss=None, score=None):\n    checkpoint = {\"epoch\": epoch,\n                  \"model\": model.state_dict(),\n                  \"scheduler\": scheduler.state_dict(),\n                  \"optimizer\": optimizer.state_dict(),\n                  \"loss\": loss,\n                  \"score\": score,\n                  }\n\n    torch.save(checkpoint, f\"{OUTPUT_FOLDER}checkpoint-{name}.pt\")\n\n\ndef load_checkpoint(model, scheduler, name):\n    checkpoint = torch.load(name)\n\n    model.load_state_dict(checkpoint[\"model\"])\n    scheduler.load_state_dict(checkpoint[\"scheduler\"])\n    return model, scheduler, checkpoint[\"epoch\"]","metadata":{"id":"ryZ6wE0zKPiz","papermill":{"duration":0.035168,"end_time":"2022-03-23T20:09:24.671044","exception":false,"start_time":"2022-03-23T20:09:24.635876","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:53.153987Z","iopub.execute_input":"2022-06-02T13:49:53.154302Z","iopub.status.idle":"2022-06-02T13:49:53.16255Z","shell.execute_reply.started":"2022-06-02T13:49:53.154266Z","shell.execute_reply":"2022-06-02T13:49:53.161779Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Train and validation functions","metadata":{}},{"cell_type":"code","source":"def train_epoch(args, model, loader, criterion, optimizer, scheduler, epoch):\n    losses = []\n    targets_all = []\n    outputs_all = []\n    \n    model.train()\n    t = tqdm(loader)\n    \n    for i, sample in enumerate(t):\n        optimizer.zero_grad()\n        \n        images = sample['image'].to(args.device)\n        targets = sample['target'].to(args.device)\n        \n        outputs = model(images, targets)\n        loss = criterion(outputs, targets)\n        \n        loss.backward()\n        optimizer.step()\n        \n        if scheduler:\n            scheduler.step()\n                \n        losses.append(loss.item())\n        targets_all.extend(targets.cpu().numpy())\n        outputs_all.extend(torch.sigmoid(outputs).detach().cpu().numpy())\n\n        score = accuracy_score(targets_all, np.argmax(outputs_all, axis=1))\n        t.set_description(f\"Epoch {epoch}/{args.epochs} - Train loss:{loss:0.4f}, score: {score:0.4f}\")\n        \n    return np.mean(losses), score","metadata":{"id":"SntLH82s2_au","papermill":{"duration":0.040666,"end_time":"2022-03-23T20:09:24.737525","exception":false,"start_time":"2022-03-23T20:09:24.696859","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:53.163839Z","iopub.execute_input":"2022-06-02T13:49:53.164618Z","iopub.status.idle":"2022-06-02T13:49:53.17586Z","shell.execute_reply.started":"2022-06-02T13:49:53.164579Z","shell.execute_reply":"2022-06-02T13:49:53.175096Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def test_classification(loader, model):\n    targets_all = []\n    outputs_all = []\n    \n    model.eval()\n    t = tqdm(loader, desc=\"Classification\")\n    \n    for i, sample in enumerate(t):\n        images = sample['image'].to(args.device)\n        targets = sample['target'].to(args.device)\n        \n        with torch.no_grad():\n            outputs = model(images, targets)\n        \n        targets_all.extend(targets.cpu().numpy())\n        outputs_all.extend(torch.sigmoid(outputs).detach().cpu().numpy())\n        \n    \n    # repeat targets to N_MATCHES for easy calculation of MAP@5\n    y = np.repeat([targets_all], repeats=N_MATCHES, axis=0).T\n    # sort predictions and get top 5\n    preds = np.argsort(-np.array(outputs_all), axis=1)[:, :N_MATCHES]\n    # check if any of top 5 predictions are correct and calculate mean accuracy\n    acc_top_5 = (preds == y).any(axis=1).mean()\n    # calculate prediction accuracy\n    acc_top_1 = np.mean(targets_all == np.argmax(outputs_all, axis=1))\n\n    print(f\"Classification accuracy: {acc_top_1:0.4f}, MAP@5: {acc_top_5:0.4f}\")\n    return acc_top_5","metadata":{"execution":{"iopub.status.busy":"2022-06-02T13:49:53.17868Z","iopub.execute_input":"2022-06-02T13:49:53.178865Z","iopub.status.idle":"2022-06-02T13:49:53.188663Z","shell.execute_reply.started":"2022-06-02T13:49:53.178842Z","shell.execute_reply":"2022-06-02T13:49:53.187835Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# find 5 most similar images from different hotels and return their hotel_id_code\ndef find_matches(query, base_embeds, base_targets, k=N_MATCHES):\n    distance_df = pd.DataFrame(index=np.arange(len(base_targets)), data={\"hotel_id_code\": base_targets})\n    # calculate cosine distance of query embeds to all base embeds\n    distance_df[\"distance\"] = cosine_similarity([query], base_embeds)[0]\n    # sort by distance and hotel_id\n    distance_df = distance_df.sort_values(by=[\"distance\", \"hotel_id_code\"], ascending=False).reset_index(drop=True)\n    # return first 5 different hotel_id_codes\n    return distance_df[\"hotel_id_code\"].unique()[:N_MATCHES]\n    \n\ndef test_similarity(args, base_loader, test_loader, model):\n    base_embeds, base_targets = generate_embeddings(base_loader, model, \"Generate base embeddings\")\n    test_embeds, test_targets = generate_embeddings(test_loader, model, \"Generate test embeddings\")\n    \n    preds = []\n    for query_embeds in tqdm(test_embeds, desc=\"Similarity - match finding\"):\n        tmp = find_matches(query_embeds, base_embeds, base_targets)\n        preds.extend([tmp])\n        \n    preds = np.array(preds)\n    test_targets_N = np.repeat([test_targets], repeats=N_MATCHES, axis=0).T\n    # check if any of top 5 predictions are correct and calculate mean accuracy\n    acc_top_5 = (preds == test_targets_N).any(axis=1).mean()\n    # calculate prediction accuracy\n    acc_top_1 = np.mean(test_targets == preds[:, 0])\n    print(f\"Similarity accuracy: {acc_top_1:0.4f}, MAP@5: {acc_top_5:0.4f}\")","metadata":{"execution":{"iopub.status.busy":"2022-06-02T13:49:53.189917Z","iopub.execute_input":"2022-06-02T13:49:53.190649Z","iopub.status.idle":"2022-06-02T13:49:53.201488Z","shell.execute_reply.started":"2022-06-02T13:49:53.190614Z","shell.execute_reply":"2022-06-02T13:49:53.200694Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Prepare data","metadata":{"id":"F2xgmwBW4LjC","papermill":{"duration":0.025544,"end_time":"2022-03-23T20:09:24.788734","exception":false,"start_time":"2022-03-23T20:09:24.76319","status":"completed"},"tags":[]}},{"cell_type":"code","source":"data_df = pd.read_csv(DATA_FOLDER + \"train.csv\")\n# encode hotel ids\ndata_df[\"hotel_id_code\"] = data_df[\"hotel_id\"].astype('category').cat.codes.values.astype(np.int64)","metadata":{"id":"Sn6HrWKQ2_aw","papermill":{"duration":0.154871,"end_time":"2022-03-23T20:09:27.317597","exception":false,"start_time":"2022-03-23T20:09:27.162726","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:53.202643Z","iopub.execute_input":"2022-06-02T13:49:53.203451Z","iopub.status.idle":"2022-06-02T13:49:53.245685Z","shell.execute_reply.started":"2022-06-02T13:49:53.203412Z","shell.execute_reply":"2022-06-02T13:49:53.244999Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# save hotel_id encoding for later decoding\nhotel_id_code_df = data_df.drop(columns=[\"image_id\"]).drop_duplicates().reset_index(drop=True)\nhotel_id_code_df.to_csv(OUTPUT_FOLDER + 'hotel_id_code_mapping.csv', index=False)\n# hotel_id_code_map = hotel_id_code_df.set_index('hotel_id_code').to_dict()[\"hotel_id\"]","metadata":{"execution":{"iopub.status.busy":"2022-06-02T13:49:53.247803Z","iopub.execute_input":"2022-06-02T13:49:53.248232Z","iopub.status.idle":"2022-06-02T13:49:53.265896Z","shell.execute_reply.started":"2022-06-02T13:49:53.248193Z","shell.execute_reply":"2022-06-02T13:49:53.26519Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Example of images","metadata":{}},{"cell_type":"code","source":"def show_images(ds, title_text, n_images=5):\n    fig, ax = plt.subplots(1,5, figsize=(22,8))\n    \n    ax[0].set_ylabel(title_text)\n    \n    for i in range(5):\n        d = ds.__getitem__(i)\n        ax[i].imshow(d[\"image\"].T)","metadata":{"execution":{"iopub.status.busy":"2022-06-02T13:49:53.267344Z","iopub.execute_input":"2022-06-02T13:49:53.267599Z","iopub.status.idle":"2022-06-02T13:49:53.272893Z","shell.execute_reply.started":"2022-06-02T13:49:53.267564Z","shell.execute_reply":"2022-06-02T13:49:53.272056Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Train and evaluate","metadata":{"id":"EMVYKwZ64zUN","papermill":{"duration":0.035036,"end_time":"2022-03-23T20:09:27.471338","exception":false,"start_time":"2022-03-23T20:09:27.436302","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def train_and_validate(args, data_df):\n    model_name = f\"embedding-model-{args.backbone_name}-{IMG_SIZE}x{IMG_SIZE}\"\n    print(model_name)\n\n    seed_everything(seed=SEED)\n    \n    if args.split_val:\n        # split data into train and validation set\n        hotel_image_count = data_df.groupby(\"hotel_id\")[\"image_id\"].count()\n        # hotels that have more images than samples for validation\n        valid_hotels = hotel_image_count[hotel_image_count > args.val_samples]\n        # data that can be split into train and val set\n        valid_data = data_df[data_df[\"hotel_id\"].isin(valid_hotels.index)]\n        # if hotel had less than required val_samples it will be only in the train set\n        valid_df = valid_data.groupby(\"hotel_id\").sample(args.val_samples, random_state=SEED)\n        train_df = data_df[~data_df[\"image_id\"].isin(valid_df[\"image_id\"])]\n    else: \n        train_df = data_df\n    \n    train_dataset = HotelTrainDataset(train_df, train_transform, data_path=IMAGE_FOLDER)\n    train_loader  = DataLoader(train_dataset, num_workers=args.num_workers, batch_size=args.batch_size, shuffle=True, drop_last=True)\n    train_loader_sampler = DataLoader(train_dataset, sampler=ImbalancedDatasetSampler(train_dataset), num_workers=args.num_workers, batch_size=args.batch_size, shuffle=False, drop_last=True)\n    if args.split_val:\n        valid_dataset = HotelTrainDataset(valid_df, val_transform, data_path=IMAGE_FOLDER)\n        valid_loader  = DataLoader(valid_dataset, num_workers=args.num_workers, batch_size=args.batch_size, shuffle=False)\n    \n    # base dataset for image similarity search\n    base_dataset  = HotelTrainDataset(train_df, base_transform, data_path=IMAGE_FOLDER)\n    base_loader   = DataLoader(base_dataset, num_workers=args.num_workers, batch_size=args.batch_size*4, shuffle=False)\n    break_at_epoch = args.break_at_epoch\n    \n    model = EmbeddingModel(args.n_classes, args.embedding_size ,args.backbone_name)\n    optimizer = Lookahead(torch.optim.AdamW(model.parameters(), lr=args.lr), k=3)\n    scheduler = torch.optim.lr_scheduler.OneCycleLR(\n            optimizer,\n            max_lr=args.lr,\n            epochs=args.epochs,\n            steps_per_epoch=len(train_loader),\n            div_factor=10,\n            final_div_factor=1,\n            pct_start=0.1,\n            anneal_strategy=\"cos\",\n)\n    \n    if args.checkpoint:\n        model, scheduler, start_epoch = load_checkpoint(model, scheduler, args.checkpoint)\n        start_epoch += 1\n    else:\n        start_epoch = 1\n    \n    model = model.to(args.device)\n    criterion = nn.CrossEntropyLoss()\n    \n    best_val_acc = 0\n    for epoch in range(start_epoch, args.epochs+1):\n        if epoch%2==1 or (not args.use_sampler):\n            train_loss, train_score = train_epoch(args, model, train_loader, criterion, optimizer, scheduler, epoch)\n        else:\n            train_loss, train_score = train_epoch(args, model, train_loader_sampler, criterion, optimizer, scheduler, epoch)\n        save_checkpoint(model, scheduler, optimizer, epoch, f\"{model_name}-last\", train_loss, train_score)\n        if args.split_val:\n            val_acc_top5 = test_classification(valid_loader, model)\n            if val_acc_top5 > best_val_acc:\n                best_val_acc = val_acc_top5\n                save_checkpoint(model, scheduler, optimizer, epoch, f\"{model_name}-best\", train_loss, train_score)\n        if epoch == break_at_epoch:\n            break\n    if args.split_val:\n        test_similarity(args, base_loader, valid_loader, model)\n    \n    # generate embeddings for all train images and save them for inference\n    base_dataset   = HotelTrainDataset(data_df, base_transform, data_path=IMAGE_FOLDER)\n    base_loader    = DataLoader(base_dataset, num_workers=args.num_workers, batch_size=args.batch_size*4, shuffle=False)\n    base_embeds, _ = generate_embeddings(base_loader, model, \"Generate embeddings for all images\")\n    data_df[\"embeddings\"] = list(base_embeds)\n    data_df.to_pickle(f\"{OUTPUT_FOLDER}{model_name}_image-embeddings.pkl\")","metadata":{"execution":{"iopub.status.busy":"2022-06-02T13:49:53.27452Z","iopub.execute_input":"2022-06-02T13:49:53.274827Z","iopub.status.idle":"2022-06-02T13:49:53.296871Z","shell.execute_reply.started":"2022-06-02T13:49:53.274791Z","shell.execute_reply":"2022-06-02T13:49:53.295819Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Training","metadata":{}},{"cell_type":"code","source":"%%time \n\nclass args:\n    epochs = 15\n    lr = 1e-3\n    batch_size = 32\n    num_workers = 2\n    split_val = True\n    val_samples = 1\n    embedding_size = 1800\n    backbone_name = \"eca_nfnet_l0\"\n    n_classes = data_df[\"hotel_id_code\"].nunique()\n    device = ('cuda' if torch.cuda.is_available() else 'cpu')\n    checkpoint = None #\"../input/hotelidcheckpoints/checkpoint-embedding-model-eca_nfnet_l0-512x512-embedding-1800.pt\"\n    break_at_epoch = 15\n    use_sampler = True\n\ntrain_and_validate(args, data_df)","metadata":{"id":"YONzJBtG2_a0","papermill":{"duration":1686.013804,"end_time":"2022-03-23T20:37:33.519992","exception":false,"start_time":"2022-03-23T20:09:27.506188","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2022-06-02T13:49:53.298615Z","iopub.execute_input":"2022-06-02T13:49:53.298949Z","iopub.status.idle":"2022-06-02T13:50:45.90063Z","shell.execute_reply.started":"2022-06-02T13:49:53.29891Z","shell.execute_reply":"2022-06-02T13:50:45.899848Z"},"trusted":true},"execution_count":null,"outputs":[]}]}