{"cells":[{"metadata":{},"cell_type":"markdown","source":"**Import Required Libraries**"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load in \n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport matplotlib.pyplot as plt\nimport skimage.io\nfrom skimage.transform import resize\nfrom imgaug import augmenters as iaa\nfrom tqdm import tqdm\nimport PIL\nfrom PIL import Image, ImageOps\nimport cv2\n\n# Input data files are available in the \"../input/\" directory.\n# For example, running this (by clicking run or pressing Shift+Enter) will list the files in the input directory\n\nimport os\n#print(os.listdir(\"../input\"))\n\n#for dirname, _, filenames in os.walk('/kaggle/input'):\n    #for filename in filenames:\n        #print(os.path.join(dirname, filename))\n        \n# Any results you write to the current directory are saved as output.","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from keras.preprocessing.image import ImageDataGenerator\nfrom keras.models import Sequential, load_model\nfrom keras.layers import (Activation, Dropout, Flatten, Dense, GlobalMaxPooling2D,\n                          BatchNormalization, Input, Conv2D, GlobalAveragePooling2D,concatenate,Concatenate)\nfrom keras.callbacks import ModelCheckpoint\nfrom keras import metrics\nfrom keras.optimizers import Adam, RMSprop \nfrom keras import backend as K\nimport keras\nfrom keras.models import Model","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"from sklearn.utils import class_weight, shuffle\nfrom keras.losses import binary_crossentropy, categorical_crossentropy\nfrom keras.applications.densenet import DenseNet121,DenseNet169\nimport tensorflow as tf\nfrom sklearn.metrics import f1_score, fbeta_score, cohen_kappa_score\nfrom keras.utils import Sequence\nfrom keras.utils import to_categorical\nfrom sklearn.model_selection import train_test_split\nimport imgaug as ia\nimport math\nimport warnings\nimport seaborn as sns\nimport random\nwarnings.filterwarnings(\"ignore\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Configuration\nepochs_warmup = 2\nepochs = 20\nbatch_size = 32\nlearning_rate = 1e-4\nIMAGE_SIZE = 300\nworkers_warmup = 2\nworkers_final = 1\nCHANNEL = 3\nNUM_CLASSES = 5","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train = pd.read_csv('../input/aptos2019-blindness-detection/train.csv')\ndf_test = pd.read_csv('../input/aptos2019-blindness-detection/test.csv')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**EDA**"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Now check the distribution of train images\ndf_train['diagnosis'].hist(figsize = (8,4))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Training dataset is imbalanced"},{"metadata":{"trusted":true},"cell_type":"code","source":"def adjust_gamma(image, gamma=1.0):\n        # build a lookup table mapping the pixel values [0, 255] to\n        # their adjusted gamma values\n        invGamma = 1.0 / gamma\n        table = np.array([((i / 255.0) ** invGamma) * 255\n                for i in np.arange(0, 256)]).astype(\"uint8\")\n    \n        # apply gamma correction using the lookup table\n        return cv2.LUT(image, table)\n    \ndef info_image(im):\n    # Compute the center (cx, cy) and radius of the eye\n    cy = im.shape[0]//2\n    midline = im[cy,:]\n    midline = np.where(midline>midline.mean()/3)[0]\n    if len(midline)>im.shape[1]//2:\n        x_start, x_end = np.min(midline), np.max(midline)\n    else: # This actually rarely happens p~1/10000\n        x_start, x_end = im.shape[1]//10, 9*im.shape[1]//10\n    cx = (x_start + x_end)/2\n    r = (x_end - x_start)/2\n    return cx, cy, r\n\ndef resize_image(im, augmentation=False):\n    # Crops, resizes and potentially augments the image to IMAGE_SIZE\n    cx, cy, r = info_image(im)\n    scaling = IMAGE_SIZE/(2*r)\n    rotation = 0\n    if augmentation:\n        scaling *= 1 + 0.3 * (np.random.rand()-0.5)\n        rotation = 360 * np.random.rand()\n    M = cv2.getRotationMatrix2D((cx,cy), rotation, scaling)\n    M[0,2] -= cx - IMAGE_SIZE/2\n    M[1,2] -= cy - IMAGE_SIZE/2\n    return cv2.warpAffine(im,M,(IMAGE_SIZE,IMAGE_SIZE))\n\ndef subtract_median_bg_image(im):\n    k = np.max(im.shape)//20*2+1\n    bg = cv2.medianBlur(im, k)\n    return cv2.addWeighted (im, 4, bg, -4, 128)\n\n# To remove irregularities along the circular boundary of the image\nPARAM = 96\ndef Radius_Reduction(img,PARAM):\n    h,w,c=img.shape\n    Frame=np.zeros((h,w,c),dtype=np.uint8)\n    cv2.circle(Frame,(int(math.floor(w/2)),int(math.floor(h/2))),int(math.floor((h*PARAM)/float(2*100))), (255,255,255), -1)\n    Frame1=cv2.cvtColor(Frame, cv2.COLOR_BGR2GRAY)\n    img1 =cv2.bitwise_and(img,img,mask=Frame1)\n    return img1","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Display sample images of various classes**"},{"metadata":{"trusted":true},"cell_type":"code","source":"def display_samples(df, columns=4, rows=3):\n    fig=plt.figure(figsize=(5*columns, 4*rows))\n\n    for i in range(columns*rows):\n        image_path = df.loc[i,'id_code']\n        image_id = df.loc[i,'diagnosis']\n        img = cv2.imread(f'../input/aptos2019-blindness-detection/train_images/{image_path}.png')\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = resize_image(img)\n        img = subtract_median_bg_image(img)\n        img = Radius_Reduction(img, PARAM)\n\n        fig.add_subplot(rows, columns, i+1)\n        plt.title(image_id)\n        plt.imshow(img)\n    \n    plt.tight_layout()\n\ndisplay_samples(df_train)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Constants for various class titles\nCLASSES = {0: \"No DR\", 1: \"Mild\", 2: \"Moderate\", 3: \"Severe\", 4: \"Proliferative DR\"}\n\n#helper function to draw images for different target classes\ndef draw_img(imgs, class_label='0'):\n    fig, axis = plt.subplots(2, 6, figsize=(15, 6))\n    for idnx, (idx, row) in enumerate(imgs.iterrows()):\n        imgPath = (f'../input/aptos2019-blindness-detection/train_images/{row[\"id_code\"]}.png')\n        img = cv2.imread(imgPath)\n        row = idnx // 6\n        col = idnx % 6\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = resize_image(img)\n        img = subtract_median_bg_image(img)\n        img = Radius_Reduction(img, PARAM)\n        axis[row, col].imshow(img)\n    plt.suptitle(class_label)\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"CLASS_ID = 0\ndraw_img(df_train[df_train.diagnosis == CLASS_ID].head(12), CLASSES[CLASS_ID])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"CLASS_ID = 4\ndraw_img(df_train[df_train.diagnosis == CLASS_ID].head(12), CLASSES[CLASS_ID])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"x = df_train['id_code']\ny = df_train['diagnosis']\n\nx, y = shuffle(x, y, random_state=8)\ny.hist()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"y = to_categorical(y, num_classes=NUM_CLASSES)\ntrain_x, valid_x, train_y, valid_y = train_test_split(x, y, test_size=0.15,\n                                                      stratify=y, random_state=8)\nprint(train_x.shape)\nprint(train_y.shape)\nprint(valid_x.shape)\nprint(valid_y.shape)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_y","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Image data augmentation"},{"metadata":{"trusted":true},"cell_type":"code","source":"sometimes = lambda aug: iaa.Sometimes(0.5, aug)\nseq = iaa.Sequential(\n        [\n            # apply the following augmenters to most images\n            iaa.Fliplr(0.5), # horizontally flip 50% of all images\n            iaa.Flipud(0.2), # vertically flip 20% of all images\n            sometimes(iaa.Affine(\n                scale={\"x\": (0.9, 1.1), \"y\": (0.9, 1.1)}, # scale images to 80-120% of their size, individually per axis\n                translate_percent={\"x\": (-0.1, 0.1), \"y\": (-0.1, 0.1)}, # translate by -20 to +20 percent (per axis)\n                rotate=(-10, 10), # rotate by -45 to +45 degrees\n                shear=(-5, 5), # shear by -16 to +16 degrees\n                order=[0, 1], # use nearest neighbour or bilinear interpolation (fast)\n                cval=(0, 255), # if mode is constant, use a cval between 0 and 255\n                mode=ia.ALL # use any of scikit-image's warping modes \n            )),\n            # execute 0 to 5 of the following (less important) augmenters per image\n            # don't execute all of them, as that would often be way too strong\n            iaa.SomeOf((0, 5),\n                [\n                    sometimes(iaa.Superpixels(p_replace=(0, 1.0), n_segments=(20, 200))), # convert images into their superpixel representation\n                    iaa.OneOf([\n                        iaa.GaussianBlur((0, 1.0)), # blur images with a sigma between 0 and 3.0\n                        iaa.AverageBlur(k=(3, 5)), # blur image using local means with kernel sizes between 2 and 7\n                        iaa.MedianBlur(k=(3, 5)), # blur image using local medians with kernel sizes between 2 and 7\n                    ]),\n                    iaa.Sharpen(alpha=(0, 1.0), lightness=(0.9, 1.1)), # sharpen images\n                    iaa.Emboss(alpha=(0, 1.0), strength=(0, 2.0)), # emboss images\n                    # search either for all edges or for directed edges,\n                    # blend the result with the original image using a blobby mask\n                    iaa.SimplexNoiseAlpha(iaa.OneOf([\n                        iaa.EdgeDetect(alpha=(0.5, 1.0)),\n                        iaa.DirectedEdgeDetect(alpha=(0.5, 1.0), direction=(0.0, 1.0)),\n                    ])),\n                    iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, 0.01*255), per_channel=0.5), # add gaussian noise to images\n                    iaa.OneOf([\n                        iaa.Dropout((0.01, 0.05), per_channel=0.5), # randomly remove up to 10% of the pixels\n                        iaa.CoarseDropout((0.01, 0.03), size_percent=(0.01, 0.02), per_channel=0.2),\n                    ]),\n                    iaa.Invert(0.01, per_channel=True), # invert color channels\n                    iaa.Add((-2, 2), per_channel=0.5), # change brightness of images (by -10 to 10 of original value)\n                    iaa.AddToHueAndSaturation((-1, 1)), # change hue and saturation\n                    # either change the brightness of the whole image (sometimes\n                    # per channel) or change the brightness of subareas\n                    iaa.OneOf([\n                        iaa.Multiply((0.9, 1.1), per_channel=0.5),\n                        iaa.FrequencyNoiseAlpha(\n                            exponent=(-1, 0),\n                            first=iaa.Multiply((0.9, 1.1), per_channel=True),\n                            second=iaa.ContrastNormalization((0.9, 1.1))\n                        )\n                    ]),\n                    sometimes(iaa.ElasticTransformation(alpha=(0.5, 3.5), sigma=0.25)), # move pixels locally around (with random strengths)\n                    sometimes(iaa.PiecewiseAffine(scale=(0.01, 0.05))), # sometimes move parts of the image around\n                    sometimes(iaa.PerspectiveTransform(scale=(0.01, 0.1)))\n                ],\n                random_order=True\n            )\n        ],\n        random_order=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class My_Generator(Sequence):\n\n    def __init__(self, image_filenames, labels,\n                 batch_size, is_train=True,\n                 augment=False):\n        self.image_filenames, self.labels = image_filenames, labels\n        self.batch_size = batch_size\n        self.is_train = is_train\n        self.is_augment = augment\n        if(self.is_train):\n            self.on_epoch_end()\n\n    def __len__(self):\n        return int(np.ceil(len(self.image_filenames) / float(self.batch_size)))\n\n    def __getitem__(self, idx):\n        batch_x = self.image_filenames[idx * self.batch_size:(idx + 1) * self.batch_size]\n        batch_y = self.labels[idx * self.batch_size:(idx + 1) * self.batch_size]\n\n        if(self.is_train):\n            return self.train_generate(batch_x, batch_y)\n        return self.valid_generate(batch_x, batch_y)\n\n    def on_epoch_end(self):\n        if(self.is_train):\n            self.image_filenames, self.labels = shuffle(self.image_filenames, self.labels)\n        else:\n            pass\n   \n    def train_generate(self, batch_x, batch_y):\n        batch_images = []\n        for (sample, label) in zip(batch_x, batch_y):\n            img = cv2.imread('../input/aptos2019-blindness-detection/train_images/'+sample+'.png')\n\n            img = resize_image(img)\n            img = subtract_median_bg_image(img)\n            img = Radius_Reduction(img, PARAM)            \n            \n            if(self.is_augment):\n                img = seq.augment_image(img)\n            batch_images.append(img)\n        batch_images = np.array(batch_images, np.float32) / 255\n        batch_y = np.array(batch_y, np.float32)\n        return batch_images, batch_y\n\n    def valid_generate(self, batch_x, batch_y):\n        batch_images = []\n        for (sample, label) in zip(batch_x, batch_y):\n            img = cv2.imread('../input/aptos2019-blindness-detection/train_images/'+sample+'.png')\n           \n            img = resize_image(img)\n            img = subtract_median_bg_image(img)\n            img = Radius_Reduction(img, PARAM)            \n            \n            batch_images.append(img)\n        batch_images = np.array(batch_images, np.float32) / 255\n        batch_y = np.array(batch_y, np.float32)\n        return batch_images, batch_y","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Create base model by transferring learning from DenseNet121"},{"metadata":{"trusted":true},"cell_type":"code","source":"def create_model(input_shape, n_out):\n    input_tensor = Input(shape=input_shape)\n    base_model = DenseNet121(include_top=False,\n                   weights=None,\n                   input_tensor=input_tensor)\n    base_model.load_weights(\"../input/densenetkeras/DenseNet-BC-121-32-no-top.h5\")\n    x = GlobalAveragePooling2D()(base_model.output)\n    x = Dropout(0.5)(x)\n    x = Dense(1024, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    final_output = Dense(n_out, activation='softmax', name='final_output')(x)\n    model = Model(input_tensor, final_output) \n    return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from keras.callbacks import (ModelCheckpoint, LearningRateScheduler,\n                             EarlyStopping, ReduceLROnPlateau,CSVLogger)\n\n# create callbacks list\n# Store the best model for later use\ncheckpoint = ModelCheckpoint('../working/densenet_model.h5', monitor='val_loss', verbose=1, \n                             save_best_only=True, mode='min')\n\n# Monitor val_loss and adjust the learning rate\nreduceLROnPlat = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=4, \n                                   verbose=1, mode='auto', epsilon=0.0001)\n\n# If val_loss is not improving after 9 tries, stop the training process\nearly = EarlyStopping(monitor=\"val_loss\", \n                      mode=\"min\", \n                      patience=9)\n\n# Log the training process\ncsv_logger = CSVLogger(filename='../working/training_log.csv',\n                       separator=',',\n                       append=True)\n\ntrain_warmup_generator = My_Generator(train_x, train_y, 128, is_train=True)\ntrain_generator = My_Generator(train_x, train_y, batch_size, is_train=True, augment=True)\nvalid_generator = My_Generator(valid_x, valid_y, batch_size, is_train=False)\n\nmodel = create_model(\n    input_shape=(IMAGE_SIZE,IMAGE_SIZE,3), \n    n_out=NUM_CLASSES)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from keras.callbacks import Callback\nclass KappaEvaluation(Callback):\n    def __init__(self, validation_data=(), batch_size=64, interval=1):\n        super(Callback, self).__init__()\n\n        self.interval = interval\n        self.batch_size = batch_size\n        self.valid_generator, self.y_val = validation_data\n        self.history = []\n\n    def on_epoch_end(self, epoch, logs={}):\n        if epoch % self.interval == 0:\n            y_pred = self.model.predict_generator(generator=self.valid_generator,\n                                                  steps=np.ceil(float(len(self.y_val)) / float(self.batch_size)),\n                                                  workers=1, use_multiprocessing=False,\n                                                  verbose=1)\n            def flatten(y):\n                return np.argmax(y, axis=1).reshape(-1)\n            \n            score = cohen_kappa_score(flatten(self.y_val),\n                                      flatten(y_pred),\n                                      labels=[0,1,2,3,4],\n                                      weights='quadratic')\n            print(\"\\n epoch: %d - Kappa Score: %.6f \\n\" % (epoch+1, score))\n            self.history.append(score)\n            if score >= max(self.history):\n                print('saving checkpoint: ', score)\n                self.model.save('../working/densenet_best_kappa.h5')\n\ngetKappa = KappaEvaluation(validation_data=(valid_generator, valid_y),\n                    batch_size=batch_size, interval=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# warm up model\nfor layer in model.layers:\n    layer.trainable = False\n\nfor i in range(-3,0):\n    model.layers[i].trainable = True\n\nmodel.compile(\n    loss='categorical_crossentropy',\n    optimizer=Adam())\n\nmodel.fit_generator(\n    train_warmup_generator,\n    steps_per_epoch=np.ceil(float(len(train_y)) / float(128)),\n    epochs=epochs_warmup,\n    workers=workers_warmup, \n    use_multiprocessing=True,\n    verbose=2,\n    callbacks=[getKappa])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# train all layers\nfor layer in model.layers:\n    layer.trainable = True\ncallbacks_list = [checkpoint, csv_logger, reduceLROnPlat, early, getKappa]\nmodel.compile(loss='categorical_crossentropy',\n            optimizer=Adam(lr=learning_rate))\nhistory = model.fit_generator(\n                train_generator,\n                steps_per_epoch=np.ceil(float(len(train_x)) / float(batch_size)),\n                validation_data=valid_generator,\n                validation_steps=np.ceil(float(len(valid_x)) / float(batch_size)),\n                epochs=epochs,\n                verbose=2,\n                workers=workers_final, \n                use_multiprocessing=False,\n                callbacks=callbacks_list)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Kappa or Cohen’s Kappa is like classification accuracy, except that it is normalized at the baseline of random chance on your dataset. It is a more useful measure to use on problems like this one where there is an imbalance in the classes.(ref: cell 8)\n\nBest Kappa Score from the above cell for validation data is: 0.909"},{"metadata":{"trusted":true},"cell_type":"code","source":"model.summary()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Display plots for loss\n\n\"loss\" is the training loss of a batch of training data. \"val_loss\" is the validation loss over a batch of validation data."},{"metadata":{"trusted":true},"cell_type":"code","source":"print(history.history.keys())","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.figure(figsize=(8, 8))\nplt.title(\"Learning curve\")\nplt.plot(history.history[\"loss\"], label=\"loss\")\nplt.plot(history.history[\"val_loss\"], label=\"val_loss\")\nplt.plot(np.argmin(history.history[\"val_loss\"]), np.min(history.history[\"val_loss\"]), marker=\"x\", color=\"r\", label=\"best model\")\nplt.xlabel(\"Epochs\")\nplt.ylabel(\"log_loss\")\nplt.legend();","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Predictions for test data**"},{"metadata":{"trusted":true},"cell_type":"code","source":"submit = pd.read_csv('../input/aptos2019-blindness-detection/sample_submission.csv')\npredicted = []","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"for i, name in tqdm(enumerate(submit['id_code'])):\n    path = os.path.join('../input/aptos2019-blindness-detection/test_images/', name+'.png')\n    image = cv2.imread(path)\n    \n    # do the augmentation before predicting\n    image = resize_image(image)\n    image = subtract_median_bg_image(image)\n    image = Radius_Reduction(image, PARAM)\n           \n    X = np.array((image[np.newaxis])/255)\n    score_predict=((model.predict(X).ravel()*model.predict(X[:, ::-1, :, :]).ravel()*model.predict(X[:, ::-1, ::-1, :]).ravel()*model.predict(X[:, :, ::-1, :]).ravel())**0.25).tolist()\n    \n    label_predict = np.argmax(score_predict)\n    predicted.append(str(label_predict))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"submit['diagnosis'] = predicted\nsubmit.to_csv('submission.csv', index=False)\nsubmit.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"submit['diagnosis'].value_counts().plot(kind='bar')\nplt.title('Test Predictions Per Class')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Many thanks to the authors of following kernels / datasets **\n\n![](http://)https://www.kaggle.com/mathormad/aptos-resnet50-baseline\n![](http://)https://www.kaggle.com/ratan123/aptos-2019-keras-baseline\n![](http://)https://www.kaggle.com/xhlulu/aptos-2019-densenet-keras-starter\n![](http://)https://www.kaggle.com/ratthachat/aptos-updatedv14-preprocessing-ben-s-cropping\n![](http://)https://www.kaggle.com/carlolepelaars/efficientnetb5-with-keras-aptos-2019\n![](http://)https://www.kaggle.com/tanlikesmath/diabetic-retinopathy-resized\n"}],"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}