{"cells":[{"metadata":{},"cell_type":"markdown","source":"### 1 data\n- 2015 competetion data without test labels\n- 2015 competetion data with test labels\n- data preprocessing:\n    - Ben's method\n    - circle method\n- data augumentation:\n    - imgaug\n    - albumentation\n    \n### 2 model\n- resnet50\n- EfficientNetB7\n- Stacking\n\n### 3 loss\n- focal loss\n- kappa\n\n### 4 train\n- one cycle learning\n\n### 5 submission\n- TTA will be used in inference version"},{"metadata":{},"cell_type":"markdown","source":"Inference: coming soon."},{"metadata":{"trusted":true},"cell_type":"code","source":"!pip install torch==1.1.0","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"%%time\n! pip install -v --no-cache-dir --global-option=\"--cpp_ext\" --global-option=\"--cuda_ext\" ../input/nvidiaapex/repository/NVIDIA-apex-39e153a","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true,"scrolled":true},"cell_type":"code","source":"# system package\nimport os\nfrom os.path import isfile\nimport sys\nimport time\nimport urllib\nimport pickle\nimport random\nfrom IPython.display import display\n\n# for application\nimport cv2\nfrom PIL import Image, ImageFilter\nimport numpy as np\nimport pandas as pd \nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm import tqdm_notebook as tqdm\n\n# for ML and DL\nfrom sklearn import metrics\nfrom sklearn.model_selection import train_test_split, StratifiedKFold\nimport torch\nimport torch.nn as nn\nimport torch.functional as F\nimport torch.nn.init as init\nfrom torch.utils.data import Dataset\nfrom torch.autograd import Variable\nfrom torchvision import transforms\nfrom torch.optim import Adam, SGD, RMSprop\nimport torch.nn.functional as F\nfrom torchvision import models\n\n# For speeding\nfrom apex import amp\n\n# For showing the data in directory ../input\nprint(os.listdir(\"../input\"))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"package_path = '../input/efficientnet-pytorch/efficientnet-pytorch/EfficientNet-PyTorch-master/'\nsys.path.append(package_path)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from efficientnet_pytorch import EfficientNet","execution_count":null,"outputs":[]},{"metadata":{"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","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"num_classes = 5\nSEED = 620402\nseed_everything(SEED)\nIMG_SIZE    = 112","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"data_dir = '../input/diabetic-retinopathy-resized-train-15-19-dg/resized_train_15_19_dg/'\njpg_dir = os.path.join(data_dir, 'resized_train_15_19_DG/')\ncsv_dir = os.path.join(data_dir, 'trainlabels/')\ntrain19 = pd.read_csv(os.path.join(csv_dir, 'trainLabels19.csv'))\ntrain15 = pd.read_csv(os.path.join(csv_dir, 'trainLabels15.csv'))\ntrain15 = train15.rename(columns={'image':'id_code', 'level':'diagnosis'})\ndisplay(train19.head(), train19.shape)\ndisplay(train15.head(), train15.shape)\n\n# train = pd.concat([train19, train15])\ntrain = train19.copy()\ndisplay(train.head(), train.shape)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"766f44c87272f67d632e519dce11cf54a3382696"},"cell_type":"code","source":"train_df, val_df = train_test_split(train, test_size=0.1, random_state=SEED, stratify=train.diagnosis)\ntrain_df.reset_index(drop=True, inplace=True)\nval_df.reset_index(drop=True, inplace=True)\ndisplay(train_df.head())","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# data preprocessing\ndef crop_image1(img,tol=7):\n    # img is image data\n    # tol  is tolerance\n        \n    mask = img>tol\n    return img[np.ix_(mask.any(1),mask.any(0))]\n\ndef crop_image_from_gray(img,tol=7):\n    if img.ndim ==2:\n        mask = img>tol\n        return img[np.ix_(mask.any(1),mask.any(0))]\n    elif img.ndim==3:\n        gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n        mask = gray_img>tol\n        \n        check_shape = img[:,:,0][np.ix_(mask.any(1),mask.any(0))].shape[0]\n        if (check_shape == 0): # image is too dark so that we crop out everything,\n            return img # return original image\n        else:\n            img1=img[:,:,0][np.ix_(mask.any(1),mask.any(0))]\n            img2=img[:,:,1][np.ix_(mask.any(1),mask.any(0))]\n            img3=img[:,:,2][np.ix_(mask.any(1),mask.any(0))]\n    #         print(img1.shape,img2.shape,img3.shape)\n            img = np.stack([img1,img2,img3],axis=-1)\n    #         print(img.shape)\n        return img\n\ndef load_ben_color(path, sigmaX=30):\n    image = cv2.imread(path)\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    image = crop_image_from_gray(image)\n    image = cv2.resize(image, (IMG_SIZE, IMG_SIZE))\n    image=cv2.addWeighted ( image,4, cv2.GaussianBlur( image , (0,0) , sigmaX) ,-4 ,128)\n        \n    return image\n\ndef circle_crop(img, sigmaX=10):   \n    \"\"\"\n    Create circular crop around image centre    \n    \"\"\"    \n    \n    img = cv2.imread(img)\n    img = crop_image_from_gray(img)    \n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    \n    height, width, depth = img.shape    \n    \n    x = int(width/2)\n    y = int(height/2)\n    r = np.amin((x,y))\n    \n    circle_img = np.zeros((height, width), np.uint8)\n    cv2.circle(circle_img, (x,y), int(r), 1, thickness=-1)\n    img = cv2.bitwise_and(img, img, mask=circle_img)\n    img = crop_image_from_gray(img)\n    img=cv2.addWeighted ( img,4, cv2.GaussianBlur( img , (0,0) , sigmaX) ,-4 ,128)\n    return img ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"64d7e44b053ac654c681e77b04de74ba32020fbd"},"cell_type":"code","source":"def expand_path(p):\n    p = str(p)\n    p_jpg = p + '.jpg'\n    if isfile(os.path.join(jpg_dir, p_jpg)):\n        return os.path.join(jpg_dir, p_jpg)\n    return p\n\ndef p_show(imgs, label_name=None, per_row=3):\n    n = len(imgs)\n    rows = (n + per_row - 1)//per_row\n    cols = min(per_row, n)\n    fig, axes = plt.subplots(rows,cols, figsize=(15,15))\n    for ax in axes.flatten(): ax.axis('off')\n    for i,(p, ax) in enumerate(zip(imgs, axes.flatten())): \n        img = load_ben_color(expand_path(p))\n#         img = Image.open(expand_path(p))\n        ax.imshow(img)\n        ax.set_title(train_df[train_df.id_code == p].diagnosis.values)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"b4739904397dd22d058c36769034b91964dcb9fe"},"cell_type":"code","source":"%%time\nimgs = []\nfor p in train_df.id_code:\n    imgs.append(p)\n    if len(imgs) == 9: break\np_show(imgs)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"21908baa8df4e398b0d49a5146ce544504637c5a"},"cell_type":"code","source":"class MyDataset(Dataset):\n    \n    def __init__(self, dataframe, transform=None):\n        self.df = dataframe\n        self.transform = transform\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        \n        label = self.df.diagnosis.values[idx]\n        label = np.expand_dims(label, -1)\n        \n        p = self.df.id_code.values[idx]\n        image = load_ben_color(expand_path(p))\n        image = transforms.ToPILImage()(image)\n        \n        if self.transform:\n            image = self.transform(image)\n        \n        return image, label","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"batch_size = 16\nepochs = 1\nlr = 1e-3","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"f590638fd07b9aefe2210a39612ac77e0689c0c1"},"cell_type":"code","source":"train_transform = transforms.Compose([\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation((-120, 120)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])\n\ntrainset     = MyDataset(train_df, transform =train_transform)\ntrain_loader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True, num_workers=4)\nvalset       = MyDataset(val_df, transform = train_transform)\nval_loader   = torch.utils.data.DataLoader(valset, batch_size=batch_size, shuffle=False, num_workers=4)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"5600b405f51d623922c315eef30612e91205bfff","_kg_hide-output":true},"cell_type":"code","source":"%%time\nmodel = EfficientNet.from_name('efficientnet-b7')\nmodel.load_state_dict(torch.load('../input/efficientnet-pytorch/efficientnet-b7-dcc49843.pth'))\n# model.load_state_dict(torch.load('../input/efficientnet-pytorch/efficientnet-b0-08094119.pth'))\nin_features = model._fc.in_features\nmodel._fc = nn.Linear(in_features, num_classes)\nmodel.cuda()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from math import cos, pi, floor, sin\n\nfrom torch.optim import lr_scheduler\n\nclass CosineLR(lr_scheduler._LRScheduler):\n    def __init__(self, optimizer, lr_min, lr_max, step_size):\n        self.lr_min = lr_min\n        self.lr_max = lr_max\n        self.step_size = step_size\n        self.iteration = 0\n\n        super().__init__(optimizer, -1)\n\n    def get_lr(self):\n        lr = self.lr_min + 0.5 * (self.lr_max - self.lr_min) * (\n            1 + cos(self.iteration / self.step_size * pi)\n        )\n        self.iteration += 1\n\n        if self.iteration == self.step_size:\n            self.iteration = 0\n\n        return [lr for base_lr in self.base_lrs]\n\n\nclass PowerLR(lr_scheduler._LRScheduler):\n    def __init__(self, optimizer, lr_min, lr_max, warmup):\n        self.lr_min = lr_min\n        self.lr_max = lr_max\n        self.warmup = warmup\n        self.iteration = 0\n\n        super().__init__(optimizer, -1)\n\n    def get_lr(self):\n        if self.iteration < self.warmup:\n            lr = (\n                self.lr_min + (self.lr_max - self.lr_min) / self.warmup * self.iteration\n            )\n\n        else:\n            lr = self.lr_max * (self.iteration - self.warmup + 1) ** -0.5\n\n        self.iteration += 1\n\n        return [lr for base_lr in self.base_lrs]\n\n\nclass SineLR(lr_scheduler._LRScheduler):\n    def __init__(self, optimizer, lr_min, lr_max, step_size):\n        self.lr_min = lr_min\n        self.lr_max = lr_max\n        self.step_size = step_size\n        self.iteration = 0\n\n        super().__init__(optimizer, -1)\n\n    def get_lr(self):\n        lr = self.lr_min + (self.lr_max - self.lr_min) * sin(\n            self.iteration / self.step_size * pi\n        )\n        self.iteration += 1\n\n        if self.iteration == self.step_size:\n            self.iteration = 0\n\n        return [lr for base_lr in self.base_lrs]\n\n\nclass LinearLR(lr_scheduler._LRScheduler):\n    def __init__(self, optimizer, lr_min, lr_max, warmup, step_size):\n        self.lr_min = lr_min\n        self.lr_max = lr_max\n        self.step_size = step_size\n        self.warmup = warmup\n        self.iteration = 0\n\n        super().__init__(optimizer, -1)\n\n    def get_lr(self):\n        if self.iteration < self.warmup:\n            lr = self.lr_max\n\n        else:\n            lr = self.lr_max + (self.iteration - self.warmup) * (\n                self.lr_min - self.lr_max\n            ) / (self.step_size - self.warmup)\n        self.iteration += 1\n\n        if self.iteration == self.step_size:\n            self.iteration = 0\n\n        return [lr for base_lr in self.base_lrs]\n\n\nclass CLR(lr_scheduler._LRScheduler):\n    def __init__(self, optimizer, lr_min, lr_max, step_size):\n        self.epoch = 0\n        self.lr_min = lr_min\n        self.lr_max = lr_max\n        self.current_lr = lr_min\n        self.step_size = step_size\n\n        super().__init__(optimizer, -1)\n\n    def get_lr(self):\n        cycle = floor(1 + self.epoch / (2 * self.step_size))\n        x = abs(self.epoch / self.step_size - 2 * cycle + 1)\n        lr = self.lr_min + (self.lr_max - self.lr_min) * max(0, 1 - x)\n        self.current_lr = lr\n\n        self.epoch += 1\n\n        return [lr for base_lr in self.base_lrs]\n\n\nclass Warmup(lr_scheduler._LRScheduler):\n    def __init__(self, optimizer, model_dim, factor=1, warmup=16000):\n        self.optimizer = optimizer\n        self.model_dim = model_dim\n        self.factor = factor\n        self.warmup = warmup\n        self.iteration = 0\n\n        super().__init__(optimizer, -1)\n\n    def get_lr(self):\n        self.iteration += 1\n        lr = (\n            self.factor\n            * self.model_dim ** (-0.5)\n            * min(self.iteration ** (-0.5), self.iteration * self.warmup ** (-1.5))\n        )\n\n        return [lr for base_lr in self.base_lrs]\n\n\n# Copyright 2019 fastai\n\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n\n#     http://www.apache.org/licenses/LICENSE-2.0\n\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\n# Borrowed from https://github.com/fastai/fastai and changed to make it runs like PyTorch lr scheduler\n\n\nclass CycleAnnealScheduler:\n    def __init__(\n        self, optimizer, lr_max, lr_divider, cut_point, step_size, momentum=None\n    ):\n        self.lr_max = lr_max\n        self.lr_divider = lr_divider\n        self.cut_point = step_size // cut_point\n        self.step_size = step_size\n        self.iteration = 0\n        self.cycle_step = int(step_size * (1 - cut_point / 100) / 2)\n        self.momentum = momentum\n        self.optimizer = optimizer\n\n    def get_lr(self):\n        if self.iteration > 2 * self.cycle_step:\n            cut = (self.iteration - 2 * self.cycle_step) / (\n                self.step_size - 2 * self.cycle_step\n            )\n            lr = self.lr_max * (1 + (cut * (1 - 100) / 100)) / self.lr_divider\n\n        elif self.iteration > self.cycle_step:\n            cut = 1 - (self.iteration - self.cycle_step) / self.cycle_step\n            lr = self.lr_max * (1 + cut * (self.lr_divider - 1)) / self.lr_divider\n\n        else:\n            cut = self.iteration / self.cycle_step\n            lr = self.lr_max * (1 + cut * (self.lr_divider - 1)) / self.lr_divider\n\n        return lr\n\n    def get_momentum(self):\n        if self.iteration > 2 * self.cycle_step:\n            momentum = self.momentum[0]\n\n        elif self.iteration > self.cycle_step:\n            cut = 1 - (self.iteration - self.cycle_step) / self.cycle_step\n            momentum = self.momentum[0] + cut * (self.momentum[1] - self.momentum[0])\n\n        else:\n            cut = self.iteration / self.cycle_step\n            momentum = self.momentum[0] + cut * (self.momentum[1] - self.momentum[0])\n\n        return momentum\n\n    def step(self):\n        lr = self.get_lr()\n\n        if self.momentum is not None:\n            momentum = self.get_momentum()\n\n        self.iteration += 1\n\n        if self.iteration == self.step_size:\n            self.iteration = 0\n\n        for group in self.optimizer.param_groups:\n            group['lr'] = lr\n\n            if self.momentum is not None:\n                group['betas'] = (momentum, group['betas'][1])\n\n        return lr\n\n\ndef anneal_linear(start, end, proportion):\n    return start + proportion * (end - start)\n\n\ndef anneal_cos(start, end, proportion):\n    cos_val = cos(pi * proportion) + 1\n\n    return end + (start - end) / 2 * cos_val\n\n\nclass Phase:\n    def __init__(self, start, end, n_iter, anneal_fn):\n        self.start, self.end = start, end\n        self.n_iter = n_iter\n        self.anneal_fn = anneal_fn\n        self.n = 0\n\n    def step(self):\n        self.n += 1\n\n        return self.anneal_fn(self.start, self.end, self.n / self.n_iter)\n\n    def reset(self):\n        self.n = 0\n\n    @property\n    def is_done(self):\n        return self.n >= self.n_iter\n\n\nclass CycleScheduler:\n    def __init__(\n        self,\n        optimizer,\n        lr_max,\n        n_iter,\n        momentum=(0.95, 0.85),\n        divider=25,\n        warmup_proportion=0.3,\n        phase=('linear', 'cos'),\n    ):\n        self.optimizer = optimizer\n\n        phase1 = int(n_iter * warmup_proportion)\n        phase2 = n_iter - phase1\n        lr_min = lr_max / divider\n\n        phase_map = {'linear': anneal_linear, 'cos': anneal_cos}\n\n        self.lr_phase = [\n            Phase(lr_min, lr_max, phase1, phase_map[phase[0]]),\n            Phase(lr_max, lr_min / 1e4, phase2, phase_map[phase[1]]),\n        ]\n\n        self.momentum = momentum\n\n        if momentum is not None:\n            mom1, mom2 = momentum\n            self.momentum_phase = [\n                Phase(mom1, mom2, phase1, phase_map[phase[0]]),\n                Phase(mom2, mom1, phase2, phase_map[phase[1]]),\n            ]\n\n        else:\n            self.momentum_phase = []\n\n        self.phase = 0\n\n    def step(self):\n        lr = self.lr_phase[self.phase].step()\n\n        if self.momentum is not None:\n            momentum = self.momentum_phase[self.phase].step()\n\n        else:\n            momentum = None\n\n        for group in self.optimizer.param_groups:\n            group['lr'] = lr\n\n            if self.momentum is not None:\n                if 'betas' in group:\n                    group['betas'] = (momentum, group['betas'][1])\n\n                else:\n                    group['momentum'] = momentum\n\n        if self.lr_phase[self.phase].is_done:\n            self.phase += 1\n\n        if self.phase >= len(self.lr_phase):\n            for phase in self.lr_phase:\n                phase.reset()\n\n            for phase in self.momentum_phase:\n                phase.reset()\n\n            self.phase = 0\n\n        return lr, momentum\n\n\nclass LRFinder(lr_scheduler._LRScheduler):\n    def __init__(self, optimizer, lr_min, lr_max, step_size, linear=False):\n        ratio = lr_max / lr_min\n        self.linear = linear\n        self.lr_min = lr_min\n        self.lr_mult = (ratio / step_size) if linear else ratio ** (1 / step_size)\n        self.iteration = 0\n        self.lrs = []\n        self.losses = []\n\n        super().__init__(optimizer, -1)\n\n    def get_lr(self):\n        lr = (\n            self.lr_mult * self.iteration\n            if self.linear\n            else self.lr_mult ** self.iteration\n        )\n        lr = self.lr_min + lr if self.linear else self.lr_min * lr\n\n        self.iteration += 1\n        self.lrs.append(lr)\n\n        return [lr for base_lr in self.base_lrs]\n\n    def record(self, loss):\n        self.losses.append(loss)\n\n    def save(self, filename):\n        with open(filename, 'w') as f:\n            for lr, loss in zip(self.lrs, self.losses):\n                f.write('{},{}\\n'.format(lr, loss))\n                \n# optimizer = optim.Adam(model.parameters())\n# schedule = CycleScheduler(optimizer, 0.001, batch_size * epochs, momentum=(0.95, 0.85))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"criterion = nn.MSELoss()\n# scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)\noptimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-5)\nscheduler = CycleScheduler(optimizer, lr, batch_size * epochs, momentum=(0.95, 0.85))\nmodel, optimizer = amp.initialize(model, optimizer, opt_level=\"O1\", verbosity=0)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"c338feda0eee741964b4c3d736c30b1e0a7e3ace"},"cell_type":"code","source":"def train_model(epoch):\n    model.train() \n        \n    avg_loss = 0.\n    optimizer.zero_grad()\n    for idx, (imgs, labels) in tqdm(enumerate(train_loader)):\n        imgs_train, labels_train = imgs.cuda(), labels.float().cuda()\n        output_train = model(imgs_train)\n        loss = criterion(output_train, labels_train)\n        with amp.scale_loss(loss, optimizer) as scaled_loss:\n            scaled_loss.backward()\n        scheduler.step()\n        optimizer.step() \n        optimizer.zero_grad() \n        avg_loss += loss.item() / len(train_loader)\n        \n    return avg_loss\n\ndef test_model():\n    \n    avg_val_loss = 0.\n    model.eval()\n    with torch.no_grad():\n        for idx, (imgs, labels) in tqdm(enumerate(val_loader)):\n            imgs_vaild, labels_vaild = imgs.cuda(), labels.float().cuda()\n            output_test = model(imgs_vaild)\n            avg_val_loss += criterion(output_test, labels_vaild).item() / len(val_loader)\n        \n    return avg_val_loss","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"3562bd2ec1b0650519ca196bfc0e60eb139ca180"},"cell_type":"code","source":"best_avg_loss = 100.0\n\nfor epoch in tqdm(range(epochs)):\n    \n#     print('lr:', scheduler.get_lr()[0]) \n    start_time   = time.time()\n    avg_loss     = train_model(epoch)\n    avg_val_loss = test_model()\n    elapsed_time = time.time() - start_time \n    print('Epoch {}/{} \\t loss={:.4f} \\t val_loss={:.4f} \\t time={:.2f}s'.format(\n        epoch + 1, n_epochs, avg_loss, avg_val_loss, elapsed_time))\n    \n    if avg_val_loss < best_avg_loss:\n        best_avg_loss = avg_val_loss\n        torch.save(model.state_dict(), 'weight_best.pt')\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}