{"cells":[{"metadata":{},"cell_type":"markdown","source":"# PANDA: baseline","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"This notebook got inspirations from\n- [PANDA 16X128X128 tiles from Iafoss](https://www.kaggle.com/iafoss/panda-16x128x128-tiles) \n- [PANDA keras baseline from Noble](https://www.kaggle.com/nobletp/panda-keras-baseline)\n\nThank you for sharing!\n\nThis model uses quadratic weighted kappa (QWK) as loss and metric, as implemented in TensorFlow Addons.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"- Version 1  Only the top fully connected layer is trained. Lower layers from EfficientNetB3 are fixed at ImageNet weights. QWKs for train and validation set are both 0.54.\n- Version 2  Top layers from 'block7b_add' and above from EfficientNetB3 base model are also trained.\n    - ValueError: axes don't match array\n- Version 4  Load saved weights from Version 1 using the same model as Version 1. Then set more layers trainable.","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Set some parameters.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"DATA_PATH     = '/kaggle/input/prostate-cancer-grade-assessment/'\nTRAIN_PATH    = DATA_PATH + 'train_images/'\nMASKS_PATH    = DATA_PATH + 'train_label_masks/'\nTEST_PATH     = DATA_PATH + 'test_images/'\nMODEL_PATH    = './'                                       # path to save model\nENET_MODEL_PATH = '/kaggle/input/efficientnettf/'          # pretrained model from efficientnet package\nMODEL_VERSION = 'v1.1'                                       # version for the model to be saved\nrestart       = True\nrestart_path  = '/kaggle/input/panda-modelweights/model_v1.0.h5'\nEPOCHS        = 40\n\nTILE_SIZE     = 128      # 16 128x128 tiles are selected from each image and mask\nNUM_TILE      = 16\nBATCH_SIZE    = 4\nINI_LR        = 1.e-4\n\nSEED          = 2020","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Update to TensorFlow 2.2.0 and TensorFlow-Addons 0.10.0.\n- CohenKappa metric and WeightedKappaLoose are used as implemented in TensorFlow Addons 0.10.0.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"import sys\nprint('Python {}'.format(sys.version))\nprint(sys.version_info)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\n!pip uninstall tensorflow -y\n!pip install --quiet /kaggle/input/tensorflow-addons/tensorboard_plugin_wit-1.6.0.post3-py3-none-any.whl\n!pip install --quiet /kaggle/input/tensorflow-addons/tensorboard-2.2.2-py3-none-any.whl\n!pip install --quiet /kaggle/input/tensorflow-addons/astunparse-1.6.3-py2.py3-none-any.whl\n!pip install --quiet /kaggle/input/tensorflow-addons/gast-0.3.3-py2.py3-none-any.whl\n!pip install --quiet /kaggle/input/tensorflow-addons/tensorflow_estimator-2.2.0-py2.py3-none-any.whl\n!pip install --quiet /kaggle/input/tensorflow-addons/tensorflow-2.2.0-cp37-cp37m-manylinux2010_x86_64.whl\n\n!pip install --quiet /kaggle/input/tensorflow-addons/typeguard-2.9.1-py3-none-any.whl\n!pip install --quiet /kaggle/input/tensorflow-addons/tensorflow_addons-0.10.0-cp37-cp37m-manylinux2010_x86_64.whl\n\n!pip install --quiet /kaggle/input/efficientnetrepo110/efficientnet-1.1.0-py3-none-any.whl","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Check TensorFlow and TensorFlow-Addons version.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"import tensorflow as tf\nimport tensorflow_addons as tfa\nprint('TensorFlow version:        {}'.format(tf.__version__))\nprint('TensorFlow-Addons version: {}'.format(tfa.__version__))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Load essential modules. ","execution_count":null},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport tensorflow.keras as K\nimport matplotlib.pyplot as plt\nimport math\n\nfrom sklearn.model_selection import train_test_split\nfrom tensorflow.keras.utils import Sequence\nimport albumentations as albu      # a fast image augmentation library\n\nimport skimage.io\nimport json\n\nfrom tensorflow.keras import Model, Sequential\nimport efficientnet.tfkeras as efn\n\nimport os\nimport memory_profiler\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Check GPUs availability. ","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"gpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    logical_gpus = tf.config.list_logical_devices('GPU')\n    print('{} Physical GPUs, {} Logical GPUs'.format(len(gpus), len(logical_gpus)))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"os.listdir(DATA_PATH)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Tiles of image","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Load whole slide image(WSI), and select 16 128x128 tiles from each image according to the number of tissure pixels.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def tile(img_id, mode = 'train', mask = False, tile_size = 128, num_tile = 16, aug = None):\n    # This function selects <num_tile> tiles of size <tile_size>x<tile_size> \n    # for image of <img_id> (and mask) \n    # based on the maximum number of tissue pixels.\n    if(mode == 'train'):\n        img = skimage.io.MultiImage(os.path.join(TRAIN_PATH, img_id + '.tiff'))[-1] \n    elif(mode == 'test'):\n        img = skimage.io.MultiImage(os.path.join(TEST_PATH, img_id + '.tiff'))[-1]\n    else:\n        raise AttributeError('tile mode Error')\n    if aug:\n        img = aug(image=img)['image']\n    shape = img.shape\n    pad0, pad1 = (tile_size - shape[0]%tile_size)%tile_size, (tile_size - shape[1]%tile_size)%tile_size\n    img = np.pad(img, [[pad0//2, pad0-pad0//2], [pad1//2, pad1-pad1//2], [0,0]],\n                 constant_values = 255)\n    img = img.reshape(img.shape[0]//tile_size, tile_size, img.shape[1]//tile_size, tile_size,3)\n    img = img.transpose(0,2,1,3,4).reshape(-1, tile_size, tile_size, 3)\n    if len(img) < num_tile:\n        img = np.pad(img, [[0, num_tile - len(img)], [0, 0], [0, 0], [0, 0]], \n                     constant_values = 255)    \n    idxs = np.argsort(img.reshape(img.shape[0], -1).sum(-1))[:num_tile]\n    img = np.array(img[idxs])/255.\n    \n    if(mask):\n        mask = skimage.io.MultiImage(os.path.join(MASKS_PATH, img_id + '_mask.tiff'))[-1]\n        if aug: \n            mask = aug(image=mask)['image']\n        mask = np.pad(mask, [[pad0//2, pad0-pad0//2], [pad1//2, pad1-pad1//2], [0,0]],\n                      constant_values = 0)\n        mask = mask.reshape(mask.shape[0]//tile_size, tile_size, mask.shape[1]//tile_size, tile_size,3)\n        mask = mask.transpose(0,2,1,3,4).reshape(-1, tile_size, tile_size, 3)\n        if len(mask) < num_tile:\n            mask = np.pad(mask, [[0, num_tile - len(img)], [0, 0], [0, 0], [0, 0]], \n                          constant_values = 0)\n        mask = np.array(mask[idxs])\n        return img, mask\n    else:\n        return img\n    \ndef glue_to_one(tile_seq):\n    l_tile = int(math.sqrt(NUM_TILE))\n    img_glue = np.zeros((l_tile*TILE_SIZE, l_tile*TILE_SIZE, 3),\n                         dtype = np.float32)\n    for i, t in enumerate(tile_seq):\n        x = i//l_tile\n        y = i%l_tile\n        img_glue[x*TILE_SIZE:(x+1)*TILE_SIZE,\n                 y*TILE_SIZE:(y+1)*TILE_SIZE, :] = t\n    return img_glue","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class PANDA_Sequence(Sequence):\n    def __init__(self, df, batch_size=16, mode='fit', shuffle = False, aug = None, \n                 num_tile = 16, tile_size = 128, n_classes=6):\n        self.df = df            # data frame with the image_id\n        self.batch_size = batch_size\n        self.mode = mode\n        self.shuffle = shuffle\n        self.aug = aug\n        self.tile_size = tile_size\n        self.num_tile = num_tile\n        self.n_classes = n_classes\n        self.l_tile = int(math.sqrt(self.num_tile))\n        if(self.mode == 'fit'):\n            self.tile_mode = 'train'\n            self.tile_mask = False\n        elif(self.mode == 'validate'):\n            self.tile_mode = 'train'\n            self.tile_mask = False\n        elif(self.mode == 'predict'):\n            self.tile_mode = 'test'\n            self.tile_mask = False\n        else:\n            raise AttributeError('Sequence mode Error')\n        self.on_epoch_end()\n    def __len__(self):\n        return math.ceil(len(self.df) / self.batch_size)\n    def on_epoch_end(self):\n        self.indexes = np.arange(len(self.df))\n        if self.shuffle:\n            np.random.shuffle(self.indexes)\n    def __getitem__(self, index):\n        X = np.zeros((self.batch_size, self.l_tile*self.tile_size, self.l_tile*self.tile_size, 3), dtype = np.float32)\n        img_batch = self.df[index*self.batch_size : (index+1)*self.batch_size]['image_id'].values\n        for i, img_id in enumerate(img_batch):\n            img_tiles = tile(img_id, mode = self.tile_mode, mask = self.tile_mask, tile_size = self.tile_size,\n                             num_tile = self.num_tile, aug = self.aug)\n            X[i,] = glue_to_one(img_tiles)\n        if self.mode in ['fit', 'validate']:\n            y = np.zeros((self.batch_size, self.n_classes), dtype = np.float32)\n            # encode label list\n            lbls_batch = self.df[index * self.batch_size: (index+1) * self.batch_size]['isup_grade'].values\n            for i in range(self.batch_size):\n                y[i, lbls_batch[i]] = 1\n            return X, y\n        elif self.mode == 'predict':\n            return X\n        else:\n            raise AttributeError('mode parameter error')        ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Load data","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"print('Memory usage : {} MB'.format(*memory_profiler.memory_usage(-1)))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_train = pd.read_csv('{}/train.csv'.format(DATA_PATH))\nprint('train: {}'.format(df_train.shape))\nprint('unique isup grade: {}'.format(df_train['isup_grade'].nunique()))\nX_train, X_val = train_test_split(df_train, test_size = .2, stratify=df_train['isup_grade'],\n                                  random_state = SEED)\nlbl_value_counts = X_train['isup_grade'].value_counts()\nclass_weights = {i: max(lbl_value_counts)/v for i, v in lbl_value_counts.items()}\nprint('classes weights: {}'.format(class_weights))\nprint('Memory usage : {} MB'.format(*memory_profiler.memory_usage(-1)))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Augmentation","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"aug = albu.Compose([\n    albu.OneOf([albu.RandomBrightness(limit=.15),\n                albu.RandomContrast(limit=.3),\n                albu.RandomGamma()], p=.25),\n    albu.HorizontalFlip(p=.25),\n    albu.VerticalFlip(p=.25),\n    albu.ShiftScaleRotate(shift_limit = .1,\n                          scale_limit=.1,\n                          rotate_limit = 20,\n                          p=.25)\n])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def plot_tiles(img_id):\n    img = skimage.io.MultiImage(os.path.join(TRAIN_PATH, img_id + '.tiff'))[-1]\n    img_tiles, mask_tiles = tile(img_id, mode = 'train', mask = True, tile_size = TILE_SIZE,\n                                 num_tile = NUM_TILE)\n    fig, ax = plt.subplots(1, 3, figsize=(18,12))\n    fig.suptitle('Image ID: {}  data_provider: {}  ISUP grade: {}'.format(img_id, \n                                 df_train[df_train['image_id']==img_id]['data_provider'].values[0],\n                                 df_train[df_train['image_id']==img_id]['isup_grade'].values[0]))\n    ax[0].imshow(img)\n    ax[1].imshow(glue_to_one(img_tiles))\n    ax[2].imshow(glue_to_one(mask_tiles)[:,:,0], cmap = 'hot', vmin = 0, vmax = 5)\n    for i in range(3):\n        ax[i].axis('off')\n    plt.show()\n    return\n    \n#img_id = df_train[df_train['isup_grade']==3]['image_id'].sample(n=1, random_state = SEED).to_numpy()[0]\n#plot_tiles(img_id)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Get statistics of images composed of tiles.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_statistics(df, mode = 'train', tile_size = 128, num_tile = 16, aug = None):\n    tile_avgs = []\n    tile_sqavgs = []\n    for img_id in df['image_id'].to_numpy():\n        img_tiles = tile(img_id, mode = mode, mask = False, tile_size = tile_size, num_tile = num_tile, aug = aug)\n        tile_avgs.append(img_tiles.reshape(-1,3).mean(axis=0))\n        tile_sqavgs.append((img_tiles**2).reshape(-1,3).mean(axis=0))\n    return np.array(tile_avgs), np.array(tile_sqavgs)\n\n#tile_avgs, tile_sqavgs = get_statistics(df_train, mode = 'train', tile_size = TILE_SIZE, num_tile = NUM_TILE, aug = aug)\n#tile_avg = tile_avgs.mean(axis = 0)\n#tile_std = np.sqrt(tile_sqavgs.mean(axis = 0) - tile_avg**2)\ntile_avg = [0.90413509, 0.81429153, 0.87288452]\ntile_std = [0.13412014, 0.24779248, 0.16435623]\nprint('Tiles average {}'.format(tile_avg))\nprint('Tiles std.    {}'.format(tile_std))\n     ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_gen = PANDA_Sequence(\n    df = X_train,\n    batch_size = BATCH_SIZE,\n    mode = 'fit',\n    shuffle = False,\n    aug = None,\n    num_tile = NUM_TILE,\n    tile_size = TILE_SIZE,\n    n_classes = 6)\n\nval_gen = PANDA_Sequence(\n    df = X_val,\n    batch_size = BATCH_SIZE,\n    mode = 'validate',\n    shuffle = False,\n    aug = None,\n    num_tile = NUM_TILE,\n    tile_size = TILE_SIZE,\n    n_classes = 6)\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The generated data in a batch:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"Xt, yt = train_gen.__getitem__(0)\nprint('test X: {}'.format(Xt.shape))\nprint('test y: {}'.format(yt.shape))\nfig, axes = plt.subplots(ncols = BATCH_SIZE, figsize=(18, 18))\nfor j in range(BATCH_SIZE):\n    axes[j].imshow(Xt[j])\n    axes[j].axis('off')\n    axes[j].set_title('ISUP grade {}'.format(np.argmax(yt[j,])))\nplt.show()\nprint('Memory usage : {} MB'.format(*memory_profiler.memory_usage(-1)))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Model","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Model based on EfficientNetB3:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_uncompiled_model():\n    conv_base = efn.EfficientNetB3(\n        input_shape = (int(math.sqrt(NUM_TILE))*TILE_SIZE, int(math.sqrt(NUM_TILE))*TILE_SIZE, 3),\n        weights = os.path.join(ENET_MODEL_PATH, \n                               'efficientnet-b3_weights_tf_dim_ordering_tf_kernels_autoaugment_notop.h5'),  # pretrained weights with ImageNet\n        include_top = False,\n        pooling = 'avg')\n    conv_base = K.Model(inputs=conv_base.inputs, outputs=conv_base.outputs)\n    model = Sequential()\n    model.add(conv_base)\n    #model.add(K.layers.Dropout(.2))\n    model.add(K.layers.Dense(6, activation='softmax'))\n    conv_base.trainable = False\n    \n    if restart:\n        model.load_weights(restart_path, by_name = True, skip_mismatch = True)\n        print('model weights loaded')\n        # set more layers trainable\n        conv_base.trainable = True\n        set_trainable = False\n        for layer in conv_base.layers:\n            if layer.name == 'block7b_add':\n                set_trainable = True\n            if set_trainable:\n                layer.trainable = True\n            else:\n                layer.trainable = False\n    else:\n        print('train from scratch')\n\n    return model\n\ndef get_compiled_model():\n    model = get_uncompiled_model()\n    model.compile(\n        optimizer = K.optimizers.Adam(lr=INI_LR),\n        loss = tfa.losses.WeightedKappaLoss(num_classes=6, weightage='quadratic'),\n        metrics = ['categorical_accuracy', tfa.metrics.CohenKappa(num_classes=6, weightage='quadratic')]\n        )\n    return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model = get_compiled_model()\nprint('Memory usage : {} MB'.format(*memory_profiler.memory_usage(-1)))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model.summary()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"K.utils.plot_model(model, show_shapes=True)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Plot of the top layers from EfficientNetB3 base model. Layer 'block7b_add' and above layers are trained. Lower layers are fixed at 'ImageNet' weights.  ","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"conv_base = efn.EfficientNetB3(\n        input_shape = (int(math.sqrt(NUM_TILE))*TILE_SIZE, int(math.sqrt(NUM_TILE))*TILE_SIZE, 3),\n        weights = os.path.join(ENET_MODEL_PATH, \n                               'efficientnet-b3_weights_tf_dim_ordering_tf_kernels_autoaugment_notop.h5'),  # pretrained weights with ImageNet\n        include_top = False,\n        pooling = 'avg')\n\n_ = K.utils.plot_model(conv_base, to_file = 'model_ENetB3_notop.png', show_shapes=True)\nim = plt.imread('model_ENetB3_notop.png')\nfig = plt.figure(figsize=(12,12))\nax = fig.add_subplot()\nax.imshow(im)\nax.set_xlim(200,850)\nax.set_ylim(32800,32180)\nax.axis('off')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Train the model","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nmodel_file = '{}/model_{}.h5'.format(MODEL_PATH, MODEL_VERSION)\nearlystopper = K.callbacks.EarlyStopping(\n    monitor = 'val_loss',\n    patience = 10,\n    verbose = 1,\n    mode = 'min'\n)\nmodelsaver = K.callbacks.ModelCheckpoint(\n    model_file,\n    monitor = 'val_loss',\n    verbose = 1,\n    save_weights_only = True,\n    save_best_only = True,\n    mode = 'min'\n)\nlrreducer = K.callbacks.ReduceLROnPlateau(\n    monitor = 'val_loss',\n    factor = .1,\n    patience = 5,\n    verbose = 1,\n    min_lr = 1.e-7\n)\n\nclass mem_use(K.callbacks.Callback):    \n    def on_epoch_end(self, epoch, logs=None):\n        mem_usage = memory_profiler.memory_usage(-1)\n        format_str = 'Memory usage at epoch {:8d}: ' + '{:8.0f}'*len(mem_usage) + ' MB'\n        print(format_str.format(epoch, *mem_usage))\n        \n\n\nhistory = model.fit(\n    train_gen,\n    validation_data = val_gen,\n    class_weight = class_weights,\n    callbacks = [earlystopper, modelsaver, lrreducer, mem_use()],\n    epochs = EPOCHS,\n    verbose = 1\n    )\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"history_file = 'history_{}.txt'.format(MODEL_VERSION)\ndict_to_save = {}\nfor k, v in history.history.items():\n    dict_to_save.update({\n        k: [np.format_float_positional(x) for x in history.history[k]]\n    })\nwith open(history_file, 'w') as file:\n    json.dump(dict_to_save, file)\nep_max = EPOCHS\nplt.plot(history.history['loss'][:ep_max], label='loss')\nplt.plot(history.history['val_loss'][:ep_max], label='val_loss')\nplt.legend()\nplt.show()\nplt.plot(history.history['categorical_accuracy'][:ep_max], label='accuracy')\nplt.plot(history.history['val_categorical_accuracy'][:ep_max], label='val_accuracy')\nplt.plot(history.history['cohen_kappa'][:ep_max], label = 'cohen_kappa')\nplt.plot(history.history['val_cohen_kappa'][:ep_max], label = 'val_cohen_kappa ')\nplt.legend()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"mem_usage = memory_profiler.memory_usage(-1)\nformat_str = 'Memory usage: ' + '{:8.0f}'*len(mem_usage) + ' MB'\nprint(format_str.format(*mem_usage))\n        ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Inference","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"df_test = pd.read_csv('{}/test.csv'.format(DATA_PATH))\nprint(df_test.shape)\npred = np.zeros((len(df_test), 6))\nif os.path.exists(TEST_PATH):\n    sub_gen = PANDA_Sequence(\n        df = df_test,\n        batch_size = 1,\n        mode = 'predict',\n        shuffle = False,\n        aug = None,\n        num_tile = NUM_TILE,\n        tile_size = TILE_SIZE,\n        n_classes = 6\n    )\n    pred = model.predict(sub_gen)\n    print('Predict for {} images'.format(len(pred)))\nelse:\n    print('Predict zeros')\n\ndf_test['isup_grade'] = np.argmax(pred, axis = 1)\ndf_test.drop('data_provider', axis = 1, inplace = True)\ndf_test.to_csv('submission.csv', index = False)\nprint('submission saved')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!head /kaggle/working/submission.csv","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}