{"cells":[{"metadata":{"trusted":true},"cell_type":"code","source":"!pip install -q efficientnet >> /dev/null","execution_count":null,"outputs":[]},{"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\n\nimport os\nimport gc\nimport random, re, math\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nimport tensorflow.keras.backend as K\n\nfrom tensorflow.keras.losses import Loss\nfrom tensorflow.keras.applications import ResNet101\nfrom tensorflow.keras.layers import Input, Dense, GlobalAveragePooling2D, Lambda\nfrom tensorflow.keras.models import Model, Sequential\nfrom tensorflow.keras.optimizers import Adam\n\nimport efficientnet.tfkeras as efn\n\nfrom sklearn.model_selection import KFold\nfrom sklearn.utils import shuffle\nfrom sklearn.preprocessing import OneHotEncoder\n\nfrom kaggle_datasets import KaggleDatasets\n\nimport cv2","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"try:\n    tpu = tf.distribute.cluster_resolver.TPUClusterResolver()  # TPU detection. No parameters necessary if TPU_NAME environment variable is set. On Kaggle this is always the case.\n    print('Running on TPU ', tpu.master())\nexcept ValueError:\n    tpu = None\n\nif tpu:\n    tf.config.experimental_connect_to_cluster(tpu)\n    tf.tpu.experimental.initialize_tpu_system(tpu)\n    strategy = tf.distribute.experimental.TPUStrategy(tpu)\nelse:\n    strategy = tf.distribute.get_strategy() # default distribution strategy in Tensorflow. Works on CPU and single GPU.\n\nprint(\"REPLICAS: \", strategy.num_replicas_in_sync)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"AUTO = tf.data.experimental.AUTOTUNE\n\n# Configuration\nIMAGE_SIZE = [256, 256]\nEPOCHS = 7\nBATCH_SIZE = max(32 * strategy.num_replicas_in_sync, 64)\n\nTF_RECORDS_FILES = KaggleDatasets().get_gcs_path('512x512-melanoma-tfrecords-70k-images')\n# TF_RECORDS_FILES = KaggleDatasets().get_gcs_path('melanoma-%ix%i'%(256,256))\n# TF_RECORDS_FILES = \"../input/siim-isic-melanoma-classification\"\nTRAIN_CSV = \"../input/siim-isic-melanoma-classification/train.csv\"\nTEST_CSV = \"../input/siim-isic-melanoma-classification/test.csv\"\nSUBMISSION_CSV = \"submission.csv\"","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Learning rate Scheduler","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# Learning rate schedule for TPU, GPU and CPU.\n# Using an LR ramp up because fine-tuning a pre-trained model.\n# Starting with a high LR would break the pre-trained weights.\n\nLR_START = 0.000005\nLR_MAX = 0.0004\nLR_MIN = 0.000001\nLR_RAMPUP_EPOCHS = 1\nLR_SUSTAIN_EPOCHS = 0\nLR_EXP_DECAY = 0.75\n\ndef lrfn(epoch):\n    if epoch < LR_RAMPUP_EPOCHS:\n        lr = (LR_MAX - LR_START) / LR_RAMPUP_EPOCHS * epoch + LR_START\n    elif epoch < LR_RAMPUP_EPOCHS + LR_SUSTAIN_EPOCHS:\n        lr = LR_MAX\n    else:\n        lr = (LR_MAX - LR_MIN) * LR_EXP_DECAY**(epoch - LR_RAMPUP_EPOCHS - LR_SUSTAIN_EPOCHS) + LR_MIN\n    return lr\n    \nlr_callback = tf.keras.callbacks.LearningRateScheduler(lrfn, verbose = True)\n\nrng = [i for i in range(16)]\ny = [lrfn(x) for x in rng]\nplt.plot(rng, y)\nprint(\"Learning rate schedule: {:.3g} to {:.3g} to {:.3g}\".format(y[0], max(y), y[-1]))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Data Augmentation","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_mat(rotation, shear, height_zoom, width_zoom, height_shift, width_shift):\n    # returns 3x3 transformmatrix which transforms indicies\n        \n    # CONVERT DEGREES TO RADIANS\n    rotation = math.pi * rotation / 180.\n    shear = math.pi * shear / 180.\n    \n    # ROTATION MATRIX\n    c1 = tf.math.cos(rotation)\n    s1 = tf.math.sin(rotation)\n    one = tf.constant([1],dtype='float32')\n    zero = tf.constant([0],dtype='float32')\n    rotation_matrix = tf.reshape( tf.concat([c1,s1,zero, -s1,c1,zero, zero,zero,one],axis=0),[3,3] )\n        \n    # SHEAR MATRIX\n    c2 = tf.math.cos(shear)\n    s2 = tf.math.sin(shear)\n    shear_matrix = tf.reshape( tf.concat([one,s2,zero, zero,c2,zero, zero,zero,one],axis=0),[3,3] )    \n    \n    # ZOOM MATRIX\n    zoom_matrix = tf.reshape( tf.concat([one/height_zoom,zero,zero, zero,one/width_zoom,zero, zero,zero,one],axis=0),[3,3] )\n    \n    # SHIFT MATRIX\n    shift_matrix = tf.reshape( tf.concat([one,zero,height_shift, zero,one,width_shift, zero,zero,one],axis=0),[3,3] )\n    \n    \n    return K.dot(K.dot(rotation_matrix, shear_matrix), K.dot(zoom_matrix, shift_matrix))\n\ndef transform(image):\n    # input image - is one image of size [dim,dim,3] not a batch of [b,dim,dim,3]\n    # output - image randomly rotated, sheared, zoomed, and shifted\n    DIM = IMAGE_SIZE[0]\n    XDIM = DIM%2 #fix for size 331\n    \n    rot = 180. * tf.random.normal([1],dtype='float32')\n    shr = 2. * tf.random.normal([1],dtype='float32') \n    h_zoom = 1.0 + tf.random.normal([1],dtype='float32')*0.12\n    w_zoom = 1.0 + tf.random.normal([1],dtype='float32')*0.12\n    h_shift = 8. * tf.random.normal([1],dtype='float32') \n    w_shift = 8. * tf.random.normal([1],dtype='float32')\n  \n    # GET TRANSFORMATION MATRIX\n    m = get_mat(rot,shr,h_zoom,w_zoom,h_shift,w_shift) \n\n    # LIST DESTINATION PIXEL INDICES\n    x = tf.repeat( tf.range(DIM//2,-DIM//2,-1), DIM )\n    y = tf.tile( tf.range(-DIM//2,DIM//2),[DIM] )\n    z = tf.ones([DIM*DIM],dtype='int32')\n    idx = tf.stack( [x,y,z] )\n    \n    # ROTATE DESTINATION PIXELS ONTO ORIGIN PIXELS\n    idx2 = K.dot(m,tf.cast(idx,dtype='float32'))\n    idx2 = K.cast(idx2,dtype='int32')\n    idx2 = K.clip(idx2,-DIM//2+XDIM+1,DIM//2)\n    \n    # FIND ORIGIN PIXEL VALUES           \n    idx3 = tf.stack( [DIM//2-idx2[0,], DIM//2-1+idx2[1,]] )\n    d = tf.gather_nd(image,tf.transpose(idx3))\n    im = tf.reshape(d,[DIM,DIM,3])\n    im = tf.image.random_flip_left_right(im)\n    im = tf.image.random_flip_up_down(im)\n    im = tf.image.random_brightness(im, 0.2)\n    im = tf.image.random_contrast(im, 0.9, 1.1)\n    im = tf.image.random_hue(im, 0.05)\n    return im\n\ndef transform_batch(images, feats, labels):\n    ret = tf.map_fn(transform, images)\n    return ret, feats, labels\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Data Loading","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train_feature_description = {\n    \"image_name\": tf.io.FixedLenFeature([], tf.string),\n    \"image\": tf.io.FixedLenFeature([], tf.string),\n#     'sex': tf.io.FixedLenFeature([], tf.int64),\n    'age_approx': tf.io.FixedLenFeature([], tf.int64),\n    'anatom_site_general_challenge': tf.io.FixedLenFeature([], tf.int64),\n    \"target\": tf.io.FixedLenFeature([], tf.int64)\n}\n\ntest_feature_description = {\n    \"image_name\": tf.io.FixedLenFeature([], tf.string),\n    \"image\": tf.io.FixedLenFeature([], tf.string),\n#     'sex': tf.io.FixedLenFeature([], tf.int64),\n    'age_approx': tf.io.FixedLenFeature([], tf.int64),\n    'anatom_site_general_challenge': tf.io.FixedLenFeature([], tf.int64),\n}\n\n\ndef parse_example(W_LABEL):\n    def function(ex):\n        if W_LABEL:\n            feature_description = train_feature_description\n        else:\n            feature_description = test_feature_description\n        return tf.io.parse_single_example(ex, feature_description)\n    return function\n\ndef decode_image(W_LABEL, IMAGE_SIZE):\n    def function(ex):\n        image = ex['image']\n        age = (ex['age_approx']-51)/(20)\n#         source = tf.cast(tf.one_hot(ex['source'], 3), tf.float32)\n        ana = tf.cast(tf.one_hot(ex['anatom_site_general_challenge'], 7), tf.float32)\n        feats = tf.concat([ana, tf.cast(tf.expand_dims(age, 0), tf.float32)], 0)\n        if W_LABEL:\n            label = ex['target']\n            feature_description = train_feature_description\n        else:\n            feature_description = test_feature_description\n            image_name = ex['image_name']\n        image = tf.image.decode_jpeg(image, channels=3)\n        image = tf.cast(image, tf.float32) / 255.0  # convert image to floats in [0, 1] range\n        image = tf.image.resize(image, IMAGE_SIZE)\n        image = tf.reshape(image, [*IMAGE_SIZE, 3]) # explicit size needed for TPU\n        ex['image'] = image\n        if W_LABEL:\n            return image, feats, label\n        return image, feats, image_name\n    return function\n\ndef increase_pos(images, feats, labels):\n    LEN = tf.size(labels)\n    pos = tf.squeeze(tf.where(tf.equal(labels, 1)), -1)\n    neg = tf.squeeze(tf.where(tf.equal(labels, 0)), -1)\n    if tf.size(pos) == 0:\n        return images, labels\n    if tf.size(neg) == 0:\n        return images, labels\n    neg = tf.tile(neg, [1+int(((LEN)//(2*tf.shape(neg)[0])))], name='neg_tile')\n    neg = neg[0:LEN//2]\n    pos = tf.tile(pos, multiples=[1 + (LEN)//(2*tf.size(pos))], name='pos_tile')\n    pos = pos[0:LEN//2]\n    indices = tf.concat([pos, neg], 0)\n    indices = tf.random.shuffle(indices)\n    imgs = tf.gather(images, indices)\n    lbls = tf.gather(labels, indices)\n    fts = tf.gather(feats, indices)\n    return imgs, fts, lbls\n\ndef input_to_dict(images, feats, labels):\n    \n    return {'in_img': images, 'in_feats': feats}, labels\n\n\nclass DataLoader:\n\n    def __init__(self, IMAGE_SIZE, BATCH_SIZE, TFR_FILES, CSV_FILE,\n                CACHE=True, W_LABELS=False, SPLIT=1.0, FROM_END=False, VAL=False):\n\n        self.IMAGE_SIZE = IMAGE_SIZE\n        self.BATCH_SIZE = BATCH_SIZE\n        self.TFR_FILES = TFR_FILES\n        self.CSV_FILE = CSV_FILE\n        self.CACHE = CACHE\n        self.W_LABELS = W_LABELS\n        self.SPLIT = SPLIT\n        self.FROM_END = FROM_END\n        self.VAL = VAL\n        \n\n        self.load_csv()\n        self.create_ds()\n\n    def load_csv(self):\n        pass\n#         self.csv_df = pd.read_csv(self.CSV_FILE)\n#         self.csv_size = self.csv_df.size\n#         self.datasize = self.csv_size\n    \n    def create_ds(self):\n        ignore_order = tf.data.Options()\n        if self.W_LABELS:\n            ignore_order.experimental_deterministic = False\n        \n        self.dataset = tf.data.TFRecordDataset(self.TFR_FILES, num_parallel_reads = AUTO)\n        if self.CACHE:\n#             self.dataset = self.dataset.cache()\n            self.dataset = self.dataset.with_options(ignore_order)\n        self.dataset = self.dataset.map(parse_example(self.W_LABELS), num_parallel_calls=AUTO)\n        \n        self.dataset = self.dataset.map(decode_image(self.W_LABELS, self.IMAGE_SIZE), num_parallel_calls=AUTO)\n        if self.W_LABELS:\n            self.dataset = self.dataset.repeat()\n#             self.dataset = self.dataset.batch(2048)\n#             self.dataset = self.dataset.map(increase_pos, num_parallel_calls=AUTO)\n#             self.dataset = self.dataset.unbatch()\n            self.dataset = self.dataset.shuffle(1024)\n        self.dataset = self.dataset.batch(self.BATCH_SIZE, self.W_LABELS)\n        if self.W_LABELS and (not self.VAL):\n            self.dataset = self.dataset.map(transform_batch, num_parallel_calls=AUTO)\n#         self.dataset = self.dataset.map(merge_feat, num_parallel_calls=AUTO)\n        self.dataset = self.dataset.map(input_to_dict, num_parallel_calls=AUTO)\n        self.dataset = self.dataset.prefetch(AUTO)\n        \n    def get_dataset(self):\n        return self.dataset\n    \n    def get_iterations(self):\n        return self.datasize // self.BATCH_SIZE\n        \nclass TrainDataLoader(DataLoader):\n    \"\"\"\n    Also used for validation (by setting VAL=True)\n    \"\"\"\n    def __init__(self, IMAGE_SIZE, BATCH_SIZE, VAL=False, CACHE=True, TFR_FILES=None):\n        if TFR_FILES is None:\n            TFR_FILES = tf.io.gfile.glob(TF_RECORDS_FILES + '/train*.tfrec')\n            if VAL:\n                TFR_FILES = TFR_FILES[-5:]\n            else:\n                TFR_FILES = TFR_FILES[0:-5]\n        super(TrainDataLoader, self).__init__( IMAGE_SIZE, BATCH_SIZE, TFR_FILES, CSV_FILE=TRAIN_CSV,\n                CACHE=CACHE, W_LABELS=True, VAL= VAL)\n\nclass TestDataLoader(DataLoader):\n    def __init__(self, IMAGE_SIZE, BATCH_SIZE):\n        TFR_FILES = tf.io.gfile.glob(TF_RECORDS_FILES + '/test*.tfrec')\n        super(TestDataLoader, self).__init__(IMAGE_SIZE, BATCH_SIZE, TFR_FILES, CSV_FILE=TEST_CSV,\n                CACHE=False, W_LABELS=False, SPLIT=1.0, FROM_END=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Examples from Data (after Augementation)","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train_dataset = TrainDataLoader(IMAGE_SIZE, BATCH_SIZE, False, True)\nval_dataset = TrainDataLoader(IMAGE_SIZE, BATCH_SIZE, True, True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"for im, l in train_dataset.get_dataset().take(1):\n    plt.figure(figsize=(15,int(15*4/4)))\n    for i, m in enumerate(im['in_img'][0:16]):\n        plt.subplot(4, 4, 1 + i)\n        plt.imshow(m)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Training","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def loss_function(y_true, y_pred):\n    return tf.math.reduce_mean(-(y_true*tf.math.log(y_pred)*0.99 + (1-y_true)*tf.math.log(1-y_pred)*0.01))\n\ndef get_f1(y_true, y_pred): #taken from old keras source code\n    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))\n    possible_positives = K.sum(K.round(K.clip(y_true, 0, 1)))\n    predicted_positives = K.sum(K.round(K.clip(y_pred, 0, 1)))\n    precision = true_positives / (predicted_positives + K.epsilon())\n    recall = true_positives / (possible_positives + K.epsilon())\n    f1_val = 2*(precision*recall)/(precision+recall+K.epsilon())\n    return f1_val\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"pres = [\n        efn.EfficientNetB5,\n        efn.EfficientNetB6,\n        efn.EfficientNetB3,\n        efn.EfficientNetB4,\n        efn.EfficientNetB4,\n        efn.EfficientNetB4\n]\ndropouts = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]\nmodels = []\nhists = []\nTFR_FILES = np.asarray(tf.io.gfile.glob(TF_RECORDS_FILES + '/train*.tfrec'))\nkf = KFold(n_splits=6)\ni = 0\nfor train, val in kf.split(TFR_FILES):\n    print(pres[i], dropouts[i])\n    train = TFR_FILES[train]\n    val = TFR_FILES[val]\n    train_dataset = TrainDataLoader(IMAGE_SIZE, BATCH_SIZE, False, True, train)\n    val_dataset = TrainDataLoader(IMAGE_SIZE, BATCH_SIZE, True, True, val)\n    with strategy.scope():\n        pre = pres[i](weights='imagenet', include_top=False ,input_shape=[*IMAGE_SIZE, 3])\n        pre.trainable=True\n        model = tf.keras.Sequential([\n            pre,\n            tf.keras.layers.GlobalAveragePooling2D(),\n            tf.keras.layers.Dropout(dropouts[i]),\n            tf.keras.layers.Dense(32, 'relu')\n        ])\n        in_img = tf.keras.layers.Input((*IMAGE_SIZE, 3), name='in_img')\n        in_feats = tf.keras.layers.Input((8), name='in_feats')\n        y_img = model(in_img)\n        y_feats = tf.keras.layers.Dense(32, 'relu')(in_feats)\n        y = tf.keras.layers.Concatenate(axis=1)([y_img, y_feats])\n        y = tf.keras.layers.Dropout(dropouts[i]/2)(y)\n        y = tf.keras.layers.Dense(1)(y)\n        y = tf.keras.layers.Activation('sigmoid')(y)\n        model = Model(inputs=[in_img, in_feats], outputs=y)\n        loss = tf.keras.losses.BinaryCrossentropy(label_smoothing=0.01)\n        model.compile('adam', loss=loss, metrics=[ \n          tf.keras.metrics.BinaryAccuracy(name='accuracy'),\n          tf.keras.metrics.Precision(name='precision'),\n          tf.keras.metrics.Recall(name='recall'),\n            get_f1, 'AUC'\n        ])\n    hist = model.fit(train_dataset.get_dataset(), \n           steps_per_epoch=202,#train_dataset.get_iterations(), \n           epochs=EPOCHS,\n          callbacks = [lr_callback],\n          validation_data=val_dataset.get_dataset(),\n          validation_steps=34,\n            verbose=0)\n    print('train_auc', hist.history['accuracy'])\n    print('val_auc', hist.history['val_accuracy'])\n    print('train_acc', hist.history['auc'])\n    print('val_acc', hist.history['val_auc'])\n    models.append(model)\n    hists.append(hist)\n    i += 1\n    if i == 5: \n        \"\"\"\n        using just 5 models because RAM gets full while running randForest with 6 models\n        better solution suggestion would be really appreciated\n        \"\"\"\n        break","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"aucs, models = zip(*sorted(zip([hist.history['val_auc'][-1] for hist in hists], models)))\nmodels = list(models)\naucs = list(aucs)\n# models = models[2:]\n\nprint(aucs)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Random Forest model parameter tuning","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"with strategy.scope():\n    in_img = tf.keras.layers.Input((*IMAGE_SIZE, 3), name='in_img')\n    in_feats = tf.keras.layers.Input((8), name='in_feats')\n    preds = []\n    for model in models:\n        model.trainable = False\n        temp_mod = Model(model.input, model.layers[-2].output)\n        preds.append(temp_mod({'in_img': in_img, 'in_feats': in_feats}))\n    y = tf.keras.layers.Concatenate(axis=1)(preds)\n    y = tf.keras.layers.Dense(1, 'sigmoid')(y)\n    forest = Model([in_img, in_feats], y)\n    \n    loss = tf.keras.losses.BinaryCrossentropy(label_smoothing=0.01)\n    forest.compile('adam', loss=loss, metrics=[ \n      tf.keras.metrics.BinaryAccuracy(name='accuracy'),\n      tf.keras.metrics.Precision(name='precision'),\n      tf.keras.metrics.Recall(name='recall'),\n        get_f1, 'AUC'\n    ])\ntrain_dataset = TrainDataLoader(IMAGE_SIZE, BATCH_SIZE//len(models), False, True, TFR_FILES) # with augmentation\nforest.fit(train_dataset.get_dataset(), \n           steps_per_epoch=236*len(models),#train_dataset.get_iterations(), \n           epochs=5,\n          callbacks = [lr_callback]\n         )","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Prediction","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def pred_to_label(pred, hthresh=0.766, lthresh=0.233):\n    if pred < lthresh:\n        return 0.0\n    elif pred > hthresh:\n        return 1.0\n    else:\n        return pred\n\ntest_dataset = TestDataLoader(IMAGE_SIZE, BATCH_SIZE)\nrow_list = []\nrow_list_threshed = []\n\nfor batch in test_dataset.get_dataset():\n    res = forest.predict_on_batch(batch[0])\n#     for model in models:\n#         if res is None:\n#             res = model.predict_on_batch(batch[0])\n#         else:\n#             res += model.predict_on_batch(batch[0])\n    preds = [[img_name.decode(\"utf-8\"), pred[0]] for img_name, pred in zip(batch[1].numpy(), res)]\n    row_list = row_list + preds\n\n    preds_threshed = [[img_name.decode(\"utf-8\"), pred_to_label(pred[0])] for img_name, pred in zip(batch[1].numpy(), res)]\n    row_list_threshed = row_list_threshed + preds_threshed\ndf = pd.DataFrame(row_list, \n               columns =['image_name', 'target']).sort_values(by='image_name')\ndf.to_csv(SUBMISSION_CSV, index=False)\n\ndft = pd.DataFrame(row_list_threshed, \n               columns =['image_name', 'target']).sort_values(by='image_name')\ndft.to_csv('t'+SUBMISSION_CSV, index=False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df.sort_values(by='image_name').head(10)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# save model","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# model.save('model_.h5')","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}