{"cells":[{"metadata":{"trusted":true},"cell_type":"code","source":"!nvidia-smi","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import torch\ntorch.cuda.empty_cache()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!pip install timm albumentations  ","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"23d5710a-a7c1-4220-8435-287bb3dea69f","_cell_guid":"d9b400c2-1bf1-41d8-9e6c-338e187f2eca","trusted":true},"cell_type":"code","source":"import os\nimport random\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom sklearn.model_selection import StratifiedKFold\nfrom torch import nn\nfrom torch.utils.data import DataLoader\nfrom tqdm import tqdm\nimport cv2\nfrom PIL import Image\nfrom matplotlib import pyplot as plt","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"00097a86-df71-41d1-818d-39fb9b365eb2","_cell_guid":"359f040f-56b8-40fb-af86-cbe2d02f11d7","trusted":true},"cell_type":"code","source":"TRAIN_IMAGE_DIR = '/kaggle/input/lemon-classification/train_images/train_images'\nTRAIN_CSV = \"/kaggle/input/lemon-classification/train_images.csv\"\nTEST_CSV = \"/kaggle/input/lemon-classification/test_images.csv\"\nTEST_IMAGE_DIR = \"/kaggle/input/lemon-classification/test_images/test_images\"\nOUTPUT_DIR = '/kaggle/working/'\n\nclass Config:\n    DEBUG=False\n    epochs=40\n    fold_num=12\n    target_col='class_num'\n    seed=17\n    model_arch = 'ig_resnext101_32x16d'\n    img_size=420\n    train_bs=16\n    valid_bs=16\n    T_0=10\n    lr=1e-3\n    min_lr=1e-7\n    weight_decay=1e-6\n    num_workers=8\n    \n    # suppoprt to do batch accumulation for backprop with effectively larger batch size\n    # https://pytorch.org/docs/stable/notes/amp_examples.html#gradient-accumulation\n    accum_iter = 2 \n    \n    verbose_step=8\n    device='cuda:0'\n    num_class=4\n    use_folds=[0]\n    \nconfig=Config()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"dad398ea-7f71-4d3a-9502-f50cf76be3ed","_cell_guid":"a8f31e56-460c-48c7-82f3-5ba98836d4c6","trusted":true},"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\n    torch.backends.cudnn.benchmark = True","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"1ba3eb4b-8c3f-4749-a80d-af9ab8b5d81f","_cell_guid":"fa6019fe-5c8e-43f2-8568-18188503796e","trusted":true},"cell_type":"code","source":"from albumentations.core.transforms_interface import ImageOnlyTransform\n\ndef resize_square(img):\n    l=max(img.shape[:2])\n    \n    h,w = img.shape[:2]\n    hm = (l-h)//2\n    wm = (l-w)//2\n    return cv2.copyMakeBorder(img,\n                            hm,\n                            hm+(l-h)%2,\n                            wm,\n                            wm+(l-w)%2,\n                            cv2.BORDER_CONSTANT,\n                            value=0)\n\nclass CropLemon(ImageOnlyTransform):\n    def __init__(self, margin=10, always_apply=False, p=1.0):\n        super().__init__(always_apply, p)\n        self.margin = margin\n\n    def get_box(self, img):\n        h, s, v = cv2.split(cv2.cvtColor(img, cv2.COLOR_RGB2HSV))\n\n        # h,v のしきい値で crop\n        _, img_hcrop = cv2.threshold(h, 0, 40, cv2.THRESH_BINARY)\n        _, img_vcrop = cv2.threshold(v, v.mean(), 255, cv2.THRESH_BINARY)\n        th_img = (img_hcrop * (img_vcrop / 255)).astype(np.uint8)\n\n        contours, hierarchy = \\\n            cv2.findContours(th_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)\n        # サイズの大きいものだけ選択\n        contours = [c for c in contours if cv2.contourArea(c) > 10000]\n        if not contours: return None\n\n        # 中央に近いものを選択\n        center = np.array([img.shape[1] / 2, img.shape[0] / 2])  # w, h\n        min_contour = None\n        min_dist = 1e10\n\n        for c in contours:\n            tmp = np.array(c).reshape(-1, 2)\n            m = tmp.mean(axis=0)\n            dist = sum((center - m) ** 2)\n            if dist < min_dist:\n                min_contour = tmp\n                min_dist = dist\n\n        box = [\n            *(min_contour.min(axis=0) - self.margin).astype(np.int).tolist(),\n            *(min_contour.max(axis=0) + self.margin).astype(np.int).tolist()]\n        for i in range(4):\n            if box[i] < 0: box[i] = 0\n            if i % 2 == 0:\n                if box[i] > img.shape[1]: box[i] = img.shape[1]\n            else:\n                if box[i] > img.shape[0]: box[i] = img.shape[0]\n\n        return box  # left, top, right, bottom\n\n    def apply(self, image, **params):\n        image = image.copy()\n        box = self.get_box(image)\n        crop_img = None\n        if not box or (box[3] - box[1] < 50 or box[2] - box[0] < 50):\n            pass\n        else:\n            try:\n                crop_img = image[box[1]:box[3], box[0]:box[2]]\n            except:\n                pass\n        if crop_img is None:\n            crop_img = image[40:, 10:-20]\n        return resize_square(crop_img)\n\n    def get_transform_init_args_names(self):\n        return (\"margin\",)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"e99146ea-d34d-49b9-b365-20d7cebae0d2","_cell_guid":"a97d7326-bb59-4ae1-af1f-3067ed3c0053","trusted":true},"cell_type":"code","source":"img = cv2.imread('/kaggle/input/lemon-classification/train_images/train_images/train_0001.jpg')\nimg= cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\ncrop_img = CropLemon()(image=img)['image']\nplt.imshow(crop_img)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"7203009e-2490-4781-8caf-76e252ef3854","_cell_guid":"0435afa9-3546-4c5e-a082-0018e2b44c16","trusted":true},"cell_type":"code","source":"from torch.utils.data import Dataset\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\ndef get_img(path):\n    im_bgr = cv2.imread(path)\n    return cv2.cvtColor(im_bgr, cv2.COLOR_BGR2RGB)\n\n\nclass LemonDataset(Dataset):\n    def __init__(self, df, data_root, transforms=None, output_label=True):\n\n        super().__init__()\n        self.df = df.reset_index(drop=True).copy()\n        self.transforms = transforms\n        self.data_root = data_root\n\n        self.output_label = output_label\n\n        if output_label:\n            self.labels = self.df['class_num'].values\n\n    def __len__(self):\n        return self.df.shape[0]\n\n    def __getitem__(self, index: int):\n\n        # get labels\n        if self.output_label:\n            target = self.labels[index]\n\n        img_path = os.path.join(self.data_root, self.df.loc[index]['id'])\n        img = get_img(img_path)\n\n        if self.transforms:\n            img = self.transforms(image=img)['image']\n\n        if self.output_label:\n            return img, target\n        else:\n            return img\n\ndef get_train_transforms(config, force_light=False):\n    aug_list=[\n        CropLemon(p=1),\n        A.Transpose(p=0.5),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n#         A.OneOf([\n#             A.GaussNoise(p=1),\n#             A.GaussianBlur(p=1),\n#         ], p=0.3),\n#         A.RandomBrightnessContrast(brightness_limit=(-0.2, 0.2), contrast_limit=(-0.2, 0.2), p=0.3),\n#         A.HueSaturationValue(hue_shift_limit=5, val_shift_limit=5, p=0.3),\n        A.Resize(config.img_size, config.img_size),\n        A.Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225],\n            max_pixel_value=255.0,\n            p=1.0),\n        ToTensorV2(p=1.0),\n    ]\n    return A.Compose(aug_list, p=1.0)\n\n\ndef get_valid_transforms(config):\n    aug_list=[\n        CropLemon(p=1),\n        A.Resize(config.img_size, config.img_size),\n        A.Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225],\n            max_pixel_value=255.0,\n            p=1.0),\n        ToTensorV2(p=1.0),\n    ]\n    return A.Compose(aug_list, p=1.0)\n\n\n        \ndef prepare_dataloader(df, trn_idx, val_idx,\n                       data_root=TRAIN_IMAGE_DIR):\n    train_ = df.loc[trn_idx, :].reset_index(drop=True)\n    valid_ = df.loc[val_idx, :].reset_index(drop=True)\n\n\n    train_ds = LemonDataset(train_, data_root, transforms=get_train_transforms(config),\n                              output_label=True)\n    \n    print(f'train_ds len is:{len(train_ds)}')\n    valid_ds = LemonDataset(valid_, data_root, transforms=get_valid_transforms(config),\n                              output_label=True)\n\n    train_loader = torch.utils.data.DataLoader(\n        train_ds,\n        batch_size=config.train_bs,\n        drop_last=True,\n        shuffle=True,\n        num_workers=config.num_workers,\n    )\n    val_loader = torch.utils.data.DataLoader(\n        valid_ds,\n        batch_size=config.valid_bs,\n        num_workers=config.num_workers,\n        shuffle=False,\n    )\n    return train_loader, val_loader","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"2d7e1ce0-6ebe-4fc6-abac-f7fffe1ba727","_cell_guid":"4894057d-cb73-410b-b8fa-59a2584786a6","trusted":true},"cell_type":"code","source":"import timm\nimport torch\nfrom torch import nn\n\nclass LemonModel(nn.Module):\n    def __init__(self, model_arch, n_class, pretrained=False):\n        super().__init__()\n        self.model = timm.create_model(model_arch, pretrained=pretrained)\n#         n_features = self.model.classifier.in_features\n        self.model.fc = nn.Linear(4096, n_class)\n\n        self.softmax = nn.Softmax(dim=1)\n        self.label_vals = torch.arange(n_class)\n\n    # TODO: モデルの書き方これで良いのかわかんない\n    #       やりたいのは期待値返したいだけ\n    def forward(self, x):\n        x = self.model(x)\n        return (self.softmax(x) * self.label_vals).sum(axis=1)\n\n    def to(self, device, *args, **kwargs):\n        self.label_vals = self.label_vals.to(device)\n        return super().to(device, *args, **kwargs)\n\ndef fetch_loss_fn(device):\n    return nn.MSELoss().to(device)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from torch.utils.data import dataset\nfrom PIL import Image\nfrom torchvision import transforms, models\nimport random\nimport numpy as np\nimport os\nimport torch\nimport cv2\n\ndef rand_bbox(size, lam):\n    W = size[2]\n    H = size[3]\n    cut_rat = np.sqrt(1. - lam)\n    cut_w = np.int(W * cut_rat)\n    cut_h = np.int(H * cut_rat)\n    # uniform\n    cx = np.random.randint(W)\n    cy = np.random.randint(H)\n    bbx1 = np.clip(cx - cut_w // 2, 0, W)\n    bby1 = np.clip(cy - cut_h // 2, 0, H)\n    bbx2 = np.clip(cx + cut_w // 2, 0, W)\n    bby2 = np.clip(cy + cut_h // 2, 0, H)\n\n    return bbx1, bby1, bbx2, bby2\n\n\ndef cutmix_data(x, y, alpha=1., use_cuda=True):\n    if alpha > 0.:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1.\n    batch_size = x.size()[0]\n    if use_cuda:\n        index = torch.randperm(batch_size).cuda()\n    else:\n        index = torch.randperm(batch_size)\n    size = x.size()\n    bbx1, bby1, bbx2, bby2 = rand_bbox(size, lam)\n    x[:, :, bbx1:bbx2, bby1:bby2] = x[index, :, bbx1:bbx2, bby1:bby2]\n    # adjust lambda to exactly match pixel ratio\n    lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (x.size()[-1] * x.size()[-2]))\n    y_a, y_b = y, y[index]\n\n    return x, y_a, y_b, lam","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"45430517-0c48-484c-9db9-a43c0b4bf6f1","_cell_guid":"55dcc601-d929-4939-afbd-2c6a49439ffe","trusted":true},"cell_type":"code","source":"from torch.cuda.amp import autocast, GradScaler\n\ndef train_one_epoch(epoch, model, loss_fn, optimizer, scaler,\n                    train_loader, device, scheduler=None, schd_batch_update=False):\n    \"\"\"\n    Args:\n        schd_batch_update: \n            バッチごとに scheduler.step() するか？\n            False の場合は 1 epoch の終わりで step\n    \"\"\"\n    model.train()\n    pbar = tqdm(enumerate(train_loader), total=len(train_loader))\n    running_loss = 0.0\n    data_cnt = 0\n    for step, (imgs, image_labels) in pbar:\n\n        imgs = imgs.to(device).float()\n        image_labels = image_labels.to(device).float()\n        data_cnt += imgs.shape[0]\n\n        with autocast():\n            outputs = model(imgs)\n            \n            # TODO: Gradient accumulation ちゃんとわかってない\n            # https://pytorch.org/docs/stable/notes/amp_examples.html#gradient-accumulation\n            # 概要: https://qiita.com/cfiken/items/1de519e741cbbc09818c#gradient-accumulation-%E3%81%A8%E3%81%AF\n\n            loss = loss_fn(outputs, image_labels)\n            running_loss+=loss\n            \n            loss = loss / config.accum_iter\n            scaler.scale(loss).backward()\n            \n            \n            if ((step + 1) % config.accum_iter == 0) or ((step + 1) == len(train_loader)):\n                # may unscale_ here if desired (e.g., to allow clipping unscaled gradients)\n\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n\n                if scheduler is not None and schd_batch_update:\n                    scheduler.step()\n\n            if ((step + 1) % config.verbose_step == 0) or ((step + 1) == len(train_loader)):\n                description = f'train epoch {epoch} loss: {running_loss / data_cnt:.4f}'\n\n                pbar.set_description(description)\n\n    if scheduler is not None and not schd_batch_update:\n        scheduler.step()\n    running_loss = running_loss/data_cnt\n    print('train loss = {:.6f}'.format(running_loss))\n    return running_loss\n\n\ndef valid_one_epoch(epoch, model, loss_fn, val_loader, device):\n    model.eval()\n\n    tot_loss = 0.0\n    data_cnt = 0\n\n    preds = []\n    pbar = tqdm(enumerate(val_loader), total=len(val_loader))\n    for step, (imgs, image_labels) in pbar:\n        imgs = imgs.to(device).float()\n        image_labels = image_labels.to(device).float()\n        data_cnt += imgs.shape[0]\n\n        outputs = model(imgs)\n        loss = loss_fn(outputs, image_labels)\n        tot_loss += loss\n        \n        preds.append(outputs.detach().cpu().numpy())\n\n        if ((step + 1) % config.verbose_step == 0) or ((step + 1) == len(val_loader)):\n            description = f'val epoch {epoch} loss: {tot_loss / data_cnt:.4f}'\n            pbar.set_description(description)\n    \n    y_pred = np.concatenate(preds)\n    # y_pred=np.clip(y_pred.round(),0, config.num_class-1).astype(np.int)\n\n    test_loss = tot_loss / data_cnt\n    print('val loss = {:.4f}'.format(test_loss))\n    return test_loss, y_pred","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from sklearn.metrics import cohen_kappa_score\ndef qwk(y_true, y_pred):\n    return cohen_kappa_score(y_true, y_pred, weights=\"quadratic\")\n\ndef get_result(res_df):\n    y_pred=res_df['preds'].values\n    y_true=res_df[config.target_col].values\n    return qwk(y_true,y_pred)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"device = torch.device(config.device)\n\ndef get_dataset_with_folds():\n    train_df = pd.read_csv(TRAIN_CSV)\n    skf=StratifiedKFold(n_splits=config.fold_num,\n                            shuffle=True, random_state=config.seed\n                            )\n    folds = skf.split(np.arange(train_df.shape[0]), train_df[config.target_col].values)\n    return train_df, folds\n\n\ndef run_train():\n    os.makedirs(OUTPUT_DIR, exist_ok=True)\n\n    train_df, folds = get_dataset_with_folds()\n    print(f'train_df, folds: {train_df, folds}')\n    oof_df = pd.DataFrame()\n\n    for fold, (trn_idx, val_idx) in enumerate(folds):\n        \n        if config.DEBUG:\n            trn_idx = trn_idx[:len(trn_idx) // 20]\n            val_idx = val_idx[:len(val_idx) // 10]\n        \n        valid_folds=train_df.loc[val_idx].reset_index(drop=True)\n            \n        if fold not in config.use_folds:\n            continue\n\n        print('Training with {} started'.format(fold))\n\n\n        print(f'train_idx:{len(trn_idx)}')\n        train_loader, val_loader = prepare_dataloader(\n            train_df, trn_idx, val_idx, data_root=TRAIN_IMAGE_DIR)\n        \n        print(f'train_Loader len is:{len(train_loader)}')\n        print(f'val_loader len is:{len(val_loader)}')\n\n        model = LemonModel(config.model_arch, config.num_class, pretrained=True).to(device)\n        scaler = GradScaler()\n        optimizer=torch.optim.Adam(model.parameters(), \n                         lr=config.lr, \n                         weight_decay=config.weight_decay, \n                         amsgrad=False)\n#         optimizer=torch.optim.SGD(model.parameters(), \n#                           lr=config.lr, \n#                           weight_decay=config.weight_decay, \n#                           momentum=0.9)\n        \n        scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(\n            optimizer, \n            T_0=config.T_0, \n            T_mult=1,\n            eta_min=config.min_lr, \n            last_epoch=-1)\n        \n        loss_fn = fetch_loss_fn(device)\n        best_loss = 1e10\n        for epoch in range(config.epochs):\n            train_loss = train_one_epoch(epoch, model, loss_fn, optimizer,\n                            scaler, train_loader, device, scheduler=scheduler,\n                            schd_batch_update=False)\n\n            with torch.no_grad():\n                val_loss, preds = valid_one_epoch(epoch, model, loss_fn, val_loader, device)\n\n            # TODO:max(qwk) でとるほうが良い?\n            if best_loss > val_loss:\n                best_loss = val_loss\n            if train_loss < 4e-3:\n                best_path =  os.path.join(OUTPUT_DIR,f'epoch{epoch}_loss{train_loss}.pth')\n                torch.save({'model':model.state_dict(), 'preds': preds},\n                           best_path)\n        \n        best_path =  os.path.join(OUTPUT_DIR,f'fold_{fold}_best.bin')\n        preds = torch.load(best_path)['preds']\n        preds=np.clip(preds.round(),0, config.num_class-1).astype(np.int)\n        valid_folds['preds']=preds\n        \n        print(f'----- fold: {fold} result ------')\n        print(f'qwk score: {get_result(valid_folds)}')\n        \n        oof_df = pd.concat([oof_df, valid_folds])\n        \n        del model, optimizer, train_loader, val_loader, scaler, scheduler\n        torch.cuda.empty_cache()\n\n    \n    print('----- cv ------')\n    print(f'qwk score: {get_result(oof_df)}')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def inference(model, data_loader, device):\n    model.eval()\n    pbar = tqdm(enumerate(data_loader), total=len(data_loader))\n    pbar.set_description(\"inference\")\n    \n    preds = []\n    for step, imgs in pbar:\n        imgs = imgs.to(device).float()\n        outputs = model(imgs).detach().cpu().numpy()\n        preds.append(outputs)\n\n    y_pred = np.concatenate(preds)\n    return np.clip(y_pred.round(),0,3).astype(np.int)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def run_inference():\n        model = LemonModel(config.model_arch,config.num_class).to(device)\n        test_ds = LemonDataset(pd.read_csv(TEST_CSV), \n                               TEST_IMAGE_DIR, \n                               transforms=get_valid_transforms(config),\n                               output_label=False)\n\n        data_loader = torch.utils.data.DataLoader(test_ds,\n                                                  batch_size=config.valid_bs,\n                                                  num_workers=config.num_workers,\n                                                  shuffle=False,)\n        v_pred_df = pd.DataFrame()\n        test_df = pd.DataFrame()\n        model_list = ['/kaggle/working/epoch38_loss0.0013861209154129028.pth']\n        for model_path in model_list:\n#             model_path = os.path.join(OUTPUT_DIR,f'epoch{40}_best.pth')\n            model.load_state_dict(torch.load(model_path)['model'])\n            with torch.no_grad():\n                pred = inference(model, data_loader, device)\n\n            test_df = pd.concat([test_df, pd.DataFrame(pred)], axis=1)\n\n        # 最頻値とる\n        test_pred = test_df.mode(axis=1).loc[:, 0]\n        sub_df = pd.read_csv(TEST_CSV)\n        sub_df['num_class'] = test_pred.astype(np.int)\n        sub_df.to_csv('/kaggle/working/submission.csv', header=False, index=False)\n        print(sub_df.head())","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def main():\n    seed_everything(config.seed)\n    run_train()\n    run_inference()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"main()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!nvidia-smi","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"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":4,"nbformat_minor":4}