{"cells":[{"metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true},"cell_type":"code","source":"!pip install -q efficientnet","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"# loading packages\n\nimport pandas as pd\nimport numpy as np\n\n#\n\nimport matplotlib\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\n\n#\n\nimport seaborn as sns\nimport plotly.express as px\n\n#\n\nimport os\nimport random\nimport re\nimport math\nimport time\n\nfrom tqdm import tqdm\nfrom tqdm.keras import TqdmCallback\n\n\nfrom pandas_summary import DataFrameSummary\n\nimport warnings\n\n\nwarnings.filterwarnings('ignore') # Disabling warnings for clearer outputs\n\n\n\nseed_val = 42\nrandom.seed(seed_val)\nnp.random.seed(seed_val)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Setting color palette.\norange_black = [\n    '#fdc029', '#df861d', '#FF6347', '#aa3d01', '#a30e15', '#800000', '#171820'\n]\n\n# Setting plot styling.\nplt.style.use('ggplot')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Importing packages\n\nimport tensorflow as tf\nimport tensorflow.keras.backend as K\nimport efficientnet.tfkeras as efn\nfrom kaggle_datasets import KaggleDatasets\n\ntf.random.set_seed(seed_val)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Loading image storage buckets\nIMG_READ_SIZE = 512\nGCS_PATH = KaggleDatasets().get_gcs_path('melanoma-%ix%i' % (IMG_READ_SIZE, IMG_READ_SIZE))\nGCS_PATH2 = KaggleDatasets().get_gcs_path('malignant-v2-%ix%i' % (IMG_READ_SIZE, IMG_READ_SIZE))\n\nmultiplier = 20\n\nfilenames_train = tf.io.gfile.glob([os.path.join(GCS_PATH2, \"train%.2i*.tfrec\" % i) for i in range(16, 60)])\nfilenames_train += tf.io.gfile.glob([os.path.join(GCS_PATH, \"train%.2i*.tfrec\" % i) for i in range(0, 12)])\nfor i in range(multiplier):\n    filenames_train += tf.io.gfile.glob([os.path.join(GCS_PATH2, \"train%.2i*.tfrec\" % i) for i in range(0, 12)])\nfilenames_valid = tf.io.gfile.glob([os.path.join(GCS_PATH, \"train%.2i*.tfrec\" % i) for i in range(12, 15)])\n\n# filenames_train = tf.io.gfile.glob(GCS_PATH2 + '/train*.tfrec')\n# np.random.shuffle(filenames_train)\nfilenames_test = np.array(tf.io.gfile.glob(GCS_PATH + '/test*.tfrec'))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Setting TPU as main device for training, if you get warnings while working with tpu's ignore them.\n\nDEVICE = 'TPU'\nif DEVICE == 'TPU':\n    print('connecting to TPU...')\n    try:        \n        tpu = tf.distribute.cluster_resolver.TPUClusterResolver()\n        print('Running on TPU ', tpu.master())\n    except ValueError:\n        print('Could not connect to TPU')\n        tpu = None\n\n    if tpu:\n        try:\n            print('Initializing  TPU...')\n            tf.config.experimental_connect_to_cluster(tpu)\n            tf.tpu.experimental.initialize_tpu_system(tpu)\n            strategy = tf.distribute.experimental.TPUStrategy(tpu)\n            print('TPU initialized')\n        except _:\n            print('Failed to initialize TPU!')\n    else:\n        DEVICE = 'GPU'\n\nif DEVICE != 'TPU':\n    print('Using default strategy for CPU and single GPU')\n    strategy = tf.distribute.get_strategy()\n\nif DEVICE == 'GPU':\n    print('Num GPUs Available: ',\n          len(tf.config.experimental.list_physical_devices('GPU')))\n\nprint('REPLICAS: ', strategy.num_replicas_in_sync)\nAUTO = tf.data.experimental.AUTOTUNE","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"cfg = dict(\n           batch_size=32,\n           img_size=IMG_READ_SIZE,\n    \n           lr_start=0.000005,\n           lr_max=0.00000125,\n           lr_min=0.000001,\n           lr_rampup=5,\n           lr_sustain=1,\n           lr_decay=0.8,\n           epochs=12,\n    \n           transform_prob=1.0,\n           rot=180.0,\n           shr=2.0,\n           hzoom=8.0,\n           wzoom=8.0,\n           hshift=8.0,\n           wshift=8.0,\n    \n           optimizer='adam',\n           label_smooth_fac=0.05,\n           tta_steps=20\n            \n        )","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_mat(rotation, shear, height_zoom, width_zoom, height_shift,\n            width_shift):\n    \n    ''' Settings for image preparations '''\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(\n        tf.concat([c1, s1, zero, -s1, c1, zero, zero, zero, one], axis=0),\n        [3, 3])\n\n    # SHEAR MATRIX\n    c2 = tf.math.cos(shear)\n    s2 = tf.math.sin(shear)\n    shear_matrix = tf.reshape(\n        tf.concat([one, s2, zero, zero, c2, zero, zero, zero, one], axis=0),\n        [3, 3])\n\n    # ZOOM MATRIX\n    zoom_matrix = tf.reshape(\n        tf.concat([\n            one / height_zoom, zero, zero, zero, one / width_zoom, zero, zero,\n            zero, one\n        ],\n                  axis=0), [3, 3])\n\n    # SHIFT MATRIX\n    shift_matrix = tf.reshape(\n        tf.concat(\n            [one, zero, height_shift, zero, one, width_shift, zero, zero, one],\n            axis=0), [3, 3])\n\n    return K.dot(K.dot(rotation_matrix, shear_matrix),\n                 K.dot(zoom_matrix, shift_matrix))\n\n\ndef transform(image, cfg):\n    \n    ''' This function takes input images of [: , :, 3] sizes and returns them as randomly rotated, sheared, shifted and zoomed. '''\n\n    DIM = cfg['img_size']\n    XDIM = DIM % 2  # fix for size 331\n\n    rot = cfg['rot'] * tf.random.normal([1], dtype='float32')\n    shr = cfg['shr'] * tf.random.normal([1], dtype='float32')\n    h_zoom = 1.0 + tf.random.normal([1], dtype='float32') / cfg['hzoom']\n    w_zoom = 1.0 + tf.random.normal([1], dtype='float32') / cfg['wzoom']\n    h_shift = cfg['hshift'] * tf.random.normal([1], dtype='float32')\n    w_shift = cfg['wshift'] * 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\n    return tf.reshape(d, [DIM, DIM, 3])\n\ndef prepare_image(img, cfg=None, augment=True):\n    \n    ''' This function loads the image, resizes it, casts a tensor to a new type float32 in our case, transforms it using the function just above, then applies the augmentations.'''\n    \n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [cfg['img_size'], cfg['img_size']],\n                          antialias=True)\n    img = tf.cast(img, tf.float32) / 255.0\n\n    if augment:\n        if cfg['transform_prob'] > tf.random.uniform([1], minval=0, maxval=1):\n            img = transform(img, cfg)\n\n        img = tf.image.random_flip_left_right(img)\n        img = tf.image.random_saturation(img, 0.7, 1.3)\n        img = tf.image.random_contrast(img, 0.8, 1.2)\n        img = tf.image.random_brightness(img, 0.1)\n\n    return img","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def read_labeled_tfrecord(example):\n    LABELED_TFREC_FORMAT = {\n        'image': tf.io.FixedLenFeature([], tf.string),\n        'image_name': tf.io.FixedLenFeature([], tf.string),\n#         'patient_id': tf.io.FixedLenFeature([], tf.int64),\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#         'diagnosis': tf.io.FixedLenFeature([], tf.int64),\n        'target': tf.io.FixedLenFeature([], tf.int64),\n#         'width': tf.io.FixedLenFeature([], tf.int64),\n#         'height': tf.io.FixedLenFeature([], tf.int64)\n    }\n\n    example = tf.io.parse_single_example(example, LABELED_TFREC_FORMAT)\n    return example['image'], example['target']\n\n\ndef read_unlabeled_tfrecord(example):\n    UNLABELED_TFREC_FORMAT = {\n        'image': tf.io.FixedLenFeature([], tf.string),\n        'image_name': tf.io.FixedLenFeature([], tf.string),\n        'patient_id': tf.io.FixedLenFeature([], tf.int64),\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    example = tf.io.parse_single_example(example, UNLABELED_TFREC_FORMAT)\n    return example['image'], example['image_name']\n\ndef count_data_items(filenames):\n    n = [\n        int(re.compile(r'-([0-9]*)\\.').search(filename).group(1))\n        for filename in filenames\n    ]\n    return np.sum(n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def getTrainDataset(files, cfg, augment=True, shuffle=True):\n    \n    ''' This function reads the tfrecord train images, shuffles them, apply augmentations to them and prepares the data for training. '''\n    \n    ds = tf.data.TFRecordDataset(files, num_parallel_reads=AUTO)\n    ds = ds.cache()\n\n    if shuffle:\n        opt = tf.data.Options()\n        opt.experimental_deterministic = False\n        ds = ds.with_options(opt)\n\n    ds = ds.map(read_labeled_tfrecord, num_parallel_calls=AUTO)\n    ds = ds.repeat()\n    if shuffle:\n        ds = ds.shuffle(2048)\n    ds = ds.map(lambda img, label:\n                (prepare_image(img, augment=augment, cfg=cfg), label),\n                num_parallel_calls=AUTO)\n    ds = ds.batch(cfg['batch_size'] * strategy.num_replicas_in_sync)\n    ds = ds.prefetch(AUTO)\n    return ds\n\ndef getTestDataset(files, cfg, augment=False, repeat=False):\n    \n    ''' This function reads the tfrecord test images and prepares the data for predicting. '''\n    \n    ds = tf.data.TFRecordDataset(files, num_parallel_reads=AUTO)\n    ds = ds.cache()\n    if repeat:\n        ds = ds.repeat()\n    ds = ds.map(read_unlabeled_tfrecord, num_parallel_calls=AUTO)\n    ds = ds.map(lambda img, idnum:\n                (prepare_image(img, augment=augment, cfg=cfg), idnum),\n                num_parallel_calls=AUTO)\n    ds = ds.batch(cfg['batch_size'] * strategy.num_replicas_in_sync)\n    ds = ds.prefetch(AUTO)\n    return ds\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# def get_model_b3():\n    \n#     ''' This function gets the layers inclunding efficientnet ones. '''\n    \n#     model_input = tf.keras.Input(shape=(cfg['img_size'], cfg['img_size'], 3),\n#                                  name='img_input')\n\n#     dummy = tf.keras.layers.Lambda(lambda x: x)(model_input)\n\n#     x = efn.EfficientNetB3(include_top=False,\n#                            weights='noisy-student',\n#                            input_shape=(cfg['img_size'], cfg['img_size'], 3),\n#                            pooling='avg')(dummy)\n#     x = tf.keras.layers.Dense(1, activation='sigmoid')(x)\n    \n#     model = tf.keras.Model(model_input, x)\n#     model.summary()\n#     return model\n\n# def get_model_b4():\n    \n#     ''' This function gets the layers inclunding efficientnet ones. '''\n    \n#     model_input = tf.keras.Input(shape=(cfg['img_size'], cfg['img_size'], 3),\n#                                  name='img_input')\n\n#     dummy = tf.keras.layers.Lambda(lambda x: x)(model_input)\n\n#     x = efn.EfficientNetB4(include_top=False,\n#                            weights='noisy-student',\n#                            input_shape=(cfg['img_size'], cfg['img_size'], 3),\n#                            pooling='avg')(dummy)\n#     x = tf.keras.layers.Dense(1, activation='sigmoid')(x)\n    \n#     model = tf.keras.Model(model_input, x)\n#     model.summary()\n#     return model\n\n# def get_model_b5():\n    \n#     ''' This function gets the layers inclunding efficientnet ones. '''\n    \n#     model_input = tf.keras.Input(shape=(cfg['img_size'], cfg['img_size'], 3),\n#                                  name='img_input')\n\n#     dummy = tf.keras.layers.Lambda(lambda x: x)(model_input)\n\n#     x = efn.EfficientNetB5(include_top=False,\n#                            weights='noisy-student',\n#                            input_shape=(cfg['img_size'], cfg['img_size'], 3),\n#                            pooling='avg')(dummy)\n#     x = tf.keras.layers.Dense(1, activation='sigmoid')(x)\n    \n#     model = tf.keras.Model(model_input, x)\n#     model.summary()\n#     return model\n\n# def get_model_b6():\n    \n#     ''' This function gets the layers inclunding efficientnet ones. '''\n    \n#     model_input = tf.keras.Input(shape=(cfg['img_size'], cfg['img_size'], 3),\n#                                  name='img_input')\n\n#     dummy = tf.keras.layers.Lambda(lambda x: x)(model_input)\n\n#     x = efn.EfficientNetB6(include_top=False,\n#                            weights='noisy-student',\n#                            input_shape=(cfg['img_size'], cfg['img_size'], 3),\n#                            pooling='avg')(dummy)\n#     x = tf.keras.layers.Dense(1, activation='sigmoid')(x)\n    \n#     model = tf.keras.Model(model_input, x)\n#     model.summary()\n#     return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def compileNewModel(cfg, model):\n    \n    ''' Configuring the model with losses and metrics. '''    \n\n    with strategy.scope():\n        model.compile(optimizer=cfg['optimizer'],\n                      loss=[\n                          tf.keras.losses.BinaryCrossentropy(\n                              label_smoothing=cfg['label_smooth_fac'])\n                      ],\n                      metrics=[tf.keras.metrics.AUC(name='auc')])\n    return model\n\ndef getLearnRateCallback(cfg):\n    \n    ''' Using callbacks for learning rate adjustments. '''\n    \n    lr_start = cfg['lr_start']\n    lr_max = cfg['lr_max'] * strategy.num_replicas_in_sync * cfg['batch_size']\n    lr_min = cfg['lr_min']\n    lr_rampup = cfg['lr_rampup']\n    lr_sustain = cfg['lr_sustain']\n    lr_decay = cfg['lr_decay']\n\n    def lrfn(epoch):\n        if epoch < lr_rampup:\n            lr = (lr_max - lr_start) / lr_rampup * epoch + lr_start\n        elif epoch < lr_rampup + lr_sustain:\n            lr = lr_max\n        else:\n            lr = (lr_max - lr_min) * lr_decay**(epoch - lr_rampup -\n                                                lr_sustain) + lr_min\n        return lr\n\n    lr_callback = tf.keras.callbacks.LearningRateScheduler(lrfn, verbose=False)\n    return lr_callback\n\ndef learnModel(model, ds_train, stepsTrain, cfg, ds_val=None, stepsVal=0, fold=0):\n    \n    ''' Fitting things together for training '''\n    cpoint_callback = tf.keras.callbacks.ModelCheckpoint(\n                    'B3ensemble.h5', monitor='val_auc', verbose=1, save_best_only=True,\n                     mode='max', save_freq='epoch')\n    \n    callbacks = [cpoint_callback, getLearnRateCallback(cfg)]\n\n    history = model.fit(ds_train,\n                        validation_data=ds_val,\n                        verbose=1,\n                        steps_per_epoch=stepsTrain,\n                        validation_steps=stepsVal,\n                        epochs=cfg['epochs'],\n                        callbacks=callbacks)\n\n    return history","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Here we train our model, takes a while but at the end we'll have strong model to make predictions!","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"ds_train = getTrainDataset(\n    filenames_train, cfg).map(lambda img, label: ((img, img, img, img, img), label))#(label, label, label, label)))\nstepsTrain = count_data_items(filenames_train) / \\\n    (cfg['batch_size'] * strategy.num_replicas_in_sync)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"ds_val = getTrainDataset(\n    filenames_valid, cfg).map(lambda img, label: ((img, img, img, img, img), label))#(label, label, label, label)))\nstepsVal = count_data_items(filenames_valid) / \\\n    (cfg['batch_size'] * strategy.num_replicas_in_sync)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Here we make predictions using the model we trained. Then we blend them for each EffNet by taking mean. We create csv file for each prediction including blended one.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"No of layers:\n\nB3 - 378\n\nB4 - 468\n\nB5 - 570\n\nB6 - 660","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"with strategy.scope():\n    model1 = tf.keras.models.load_model('../input/b3-models/B3-fold-0.h5')\n    model2 = tf.keras.models.load_model('../input/b3-models/B3-fold-1.h5')\n    model3 = tf.keras.models.load_model('../input/b3-models/B3-fold-2.h5')\n    model4 = tf.keras.models.load_model('../input/b3-models/B3-fold-3.h5')\n    model5 = tf.keras.models.load_model('../input/b3-models/B3-fold-4.h5')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def ensemble_model(models):\n    for i, model in enumerate(models):\n        for j, layer in enumerate(model.layers):\n            layer.trainable = False\n            layer._name = 'ensemble_' + str(i+1) + '_' + layer.name \n            if j>2:\n                layer.trainable = True\n\n    ensemble_visible = [model.input for model in models]\n    ensemble_outputs = [model.output for model in models]\n    merge = tf.keras.layers.concatenate(ensemble_outputs)\n    merge = tf.keras.layers.Dense(16, activation='relu')(merge)\n    output = tf.keras.layers.Dense(1, activation='sigmoid')(merge)\n    model = tf.keras.models.Model(inputs=ensemble_visible, outputs=output)\n    return model\n\nmodels = [model1, model2, model3, model4, model5]\nwith strategy.scope():\n    model = ensemble_model(models)\nmodel = compileNewModel(cfg, model)\nmodel.summary()\n\nds_train = getTrainDataset(\n    filenames_train, cfg).map(lambda img, label: ((img, img, img, img, img), label))#(label, label, label, label)))\nstepsTrain = count_data_items(filenames_train) / \\\n    (cfg['batch_size'] * strategy.num_replicas_in_sync)\n\nlearnModel(model, ds_train, stepsTrain, cfg, ds_val, stepsVal)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"cfg['batch_size'] = 95\nsteps = count_data_items(filenames_test) / \\\n    (cfg['batch_size'] * strategy.num_replicas_in_sync)\nz = np.zeros((cfg['batch_size'] * strategy.num_replicas_in_sync))\nds_testAug = getTestDataset(\n    filenames_test, cfg, augment=True,\n    repeat=True).map(lambda img, label: ((img, img, img, img, img), z))#(z, z, z, z)))\n\ndef find_probabilty(model, ds_testAug, steps, cfg, filenames_test, csv_name='sub.csv'):\n    probs = model.predict(ds_testAug, verbose=1, steps=steps * cfg['tta_steps'])\n    probs = np.stack(probs)\n    probs = probs[:, :count_data_items(filenames_test) * cfg['tta_steps']]\n    probs = np.stack(np.split(probs, cfg['tta_steps']), axis=1)\n    probs = np.mean(probs, axis=1)\n\n    test = pd.read_csv('../input/test-csv/test.csv')\n    y_test_sorted = np.zeros((1, probs.shape[1]))\n    test = test.reset_index()\n    test = test.set_index('image_name')\n\n\n    ds_test = getTestDataset(filenames_test, cfg)\n\n    image_names = np.array([img_name.numpy().decode(\"utf-8\") \n                            for img, img_name in iter(ds_test.unbatch())])\n    \n    submission = pd.DataFrame(dict(\n        image_name = image_names,\n        target     = probs[:,0]))\n    \n    submission = submission.sort_values('image_name') \n    submission.to_csv(csv_name, index=False)\n    return(submission)\n\n        \ncsv_name = 'B3-512x512-ensemble.csv' \nwith strategy.scope():\n    model = tf.keras.models.load_model('./B3ensemble.h5')\nsubmission = find_probabilty(model, ds_testAug, steps, cfg, filenames_test, csv_name)\n\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Ensembling With Meta\n\nHere's the last step. We'll use our blended predictions created by training images and simply metadata created by using tabular data. We ensemble them together with weights and make our final predictions. Feel free to experiment with ensembling. This basic blending increased my LB score a little, you can change lots of things in previous steps to do some experiments, it's fun!","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"sample.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"\n\n<div align='center'><font size='6' color='#000000'>Final Words</font></div>\n\n<hr>\n\n<div align='center'><font size='4' color='#000000'>This notebook still in progress and if you have any feedbacks please leave me a comment I'll be reading them for sure and if you liked my work please don't forget to leave an upvote. Thank you for reading!</font></div>\n\n<hr>","execution_count":null}],"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}