{"cells":[{"metadata":{"trusted":true},"cell_type":"code","source":"## This notebook is copied from EFFB5 CV 0.9007 Single Model TF, Thanks to Ragnar for providing it\n## In this notebook cassava plant diseases are trained using EfficientNet B7 model in five expirements each with 5 folds\n## Best folds are choosen for the inference and achieved 0.901 accuracy","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Please Upvote If this is helpful"},{"metadata":{"trusted":true},"cell_type":"code","source":"!pip install -q efficientnet","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import gc\nimport os\nimport re\nimport random\nimport math\n\nimport pandas as pd\nimport numpy as np\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n%matplotlib inline\n\nimport cv2\n\nfrom tqdm import tqdm\n# tqdm.pandas()\n\nimport tensorflow as tf\nimport tensorflow_datasets as tfds\n\nimport efficientnet.tfkeras as efn\n\nfrom pandas_profiling import ProfileReport\n\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.applications import DenseNet121\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras import backend as K\n\nfrom sklearn import metrics\nfrom sklearn.model_selection import train_test_split, StratifiedShuffleSplit, KFold\nfrom sklearn.metrics import cohen_kappa_score, accuracy_score, confusion_matrix\n\nfrom kaggle_datasets import KaggleDatasets\n\n\nimport warnings\nwarnings.filterwarnings('ignore')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"try:\n    # TPU detection. No parameters necessary if TPU_NAME environment variable is\n    # set: this is always the case on Kaggle.\n    tpu = tf.distribute.cluster_resolver.TPUClusterResolver()\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    # Default distribution strategy in Tensorflow. Works on CPU and single GPU.\n    strategy = tf.distribute.get_strategy()\n\nprint(\"REPLICAS: \", strategy.num_replicas_in_sync)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"AUTO = tf.data.experimental.AUTOTUNE","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"GCS_DS_PATH = KaggleDatasets().get_gcs_path('cassava-leaf-disease-classification')\nGCS_PATH_2019 = KaggleDatasets().get_gcs_path('cassava-leaf-disease-50-tfrecords-external-512x512')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"TRAINING_FILENAMES = tf.io.gfile.glob(GCS_DS_PATH + '/train_tfrecords/*.tfrec')\nTRAINING_FILENAMES_2019 = tf.io.gfile.glob(GCS_PATH_2019  + \"/*.tfrec\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"EPOCHS = 25\nBATCH_SIZE = 16 * strategy.num_replicas_in_sync\nAUG_BATCH = BATCH_SIZE\nIMAGE_SIZE = [512, 512]\nSEED = 123\nLR = 0.0001\nTTA = 10\nVERBOSE = 2\nN_CLASSES = 5","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def seed_everything(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    tf.random.set_seed(seed)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def cutmix(image, label, PROBABILITY = 1.0):\n    # input image - is a batch of images of size [n,dim,dim,3] not a single image of [dim,dim,3]\n    # output - a batch of images with cutmix applied\n    DIM = IMAGE_SIZE[0]\n    CLASSES = 5\n    \n    imgs = []; labs = []\n    for j in range(AUG_BATCH):\n        # DO CUTMIX WITH PROBABILITY DEFINED ABOVE\n        P = tf.cast( tf.random.uniform([],0,1)<=PROBABILITY, tf.int32)\n        # CHOOSE RANDOM IMAGE TO CUTMIX WITH\n        k = tf.cast( tf.random.uniform([],0,AUG_BATCH),tf.int32)\n        # CHOOSE RANDOM LOCATION\n        x = tf.cast( tf.random.uniform([],0,DIM),tf.int32)\n        y = tf.cast( tf.random.uniform([],0,DIM),tf.int32)\n        b = tf.random.uniform([],0,1) # this is beta dist with alpha=1.0\n        WIDTH = tf.cast( DIM * tf.math.sqrt(1-b),tf.int32) * P\n        ya = tf.math.maximum(0,y-WIDTH//2)\n        yb = tf.math.minimum(DIM,y+WIDTH//2)\n        xa = tf.math.maximum(0,x-WIDTH//2)\n        xb = tf.math.minimum(DIM,x+WIDTH//2)\n        # MAKE CUTMIX IMAGE\n        one = image[j,ya:yb,0:xa,:]\n        two = image[k,ya:yb,xa:xb,:]\n        three = image[j,ya:yb,xb:DIM,:]\n        middle = tf.concat([one,two,three],axis=1)\n        img = tf.concat([image[j,0:ya,:,:],middle,image[j,yb:DIM,:,:]],axis=0)\n        imgs.append(img)\n        # MAKE CUTMIX LABEL\n        a = tf.cast(WIDTH*WIDTH/DIM/DIM,tf.float32)\n        if len(label.shape)==1:\n            lab1 = tf.one_hot(label[j],CLASSES)\n            lab2 = tf.one_hot(label[k],CLASSES)\n        else:\n            lab1 = label[j,]\n            lab2 = label[k,]\n        labs.append((1-a)*lab1 + a*lab2)\n            \n    # RESHAPE HACK SO TPU COMPILER KNOWS SHAPE OF OUTPUT TENSOR (maybe use Python typing instead?)\n    image2 = tf.reshape(tf.stack(imgs),(AUG_BATCH,DIM,DIM,3))\n    label2 = tf.reshape(tf.stack(labs),(AUG_BATCH,CLASSES))\n    return image2, label2","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def mixup(image, label, PROBABILITY = 1.0):\n    # input image - is a batch of images of size [n,dim,dim,3] not a single image of [dim,dim,3]\n    # output - a batch of images with mixup applied\n    DIM = IMAGE_SIZE[0]\n    CLASSES = 5\n    \n    imgs = []; labs = []\n    for j in range(AUG_BATCH):\n        # DO MIXUP WITH PROBABILITY DEFINED ABOVE\n        P = tf.cast( tf.random.uniform([],0,1)<=PROBABILITY, tf.float32)\n        # CHOOSE RANDOM\n        k = tf.cast( tf.random.uniform([],0,AUG_BATCH),tf.int32)\n        a = tf.random.uniform([],0,1)*P # this is beta dist with alpha=1.0\n        # MAKE MIXUP IMAGE\n        img1 = image[j,]\n        img2 = image[k,]\n        imgs.append((1-a)*img1 + a*img2)\n        # MAKE CUTMIX LABEL\n        if len(label.shape)==1:\n            lab1 = tf.one_hot(label[j],CLASSES)\n            lab2 = tf.one_hot(label[k],CLASSES)\n        else:\n            lab1 = label[j,]\n            lab2 = label[k,]\n        labs.append((1-a)*lab1 + a*lab2)\n            \n    # RESHAPE HACK SO TPU COMPILER KNOWS SHAPE OF OUTPUT TENSOR (maybe use Python typing instead?)\n    image2 = tf.reshape(tf.stack(imgs),(AUG_BATCH,DIM,DIM,3))\n    label2 = tf.reshape(tf.stack(labs),(AUG_BATCH,CLASSES))\n    return image2, label2","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def data_augment(image, image_name, target):\n    p_spatial = tf.random.uniform([], 0, 1.0, dtype = tf.float32)\n    p_rotate = tf.random.uniform([], 0, 1.0, dtype = tf.float32)\n    p_pixel_1 = tf.random.uniform([], 0, 1.0, dtype = tf.float32)\n    p_pixel_2 = tf.random.uniform([], 0, 1.0, dtype = tf.float32)\n    p_pixel_3 = tf.random.uniform([], 0, 1.0, dtype = tf.float32)\n    p_crop = tf.random.uniform([], 0, 1.0, dtype = tf.float32)\n            \n    # Flips\n    image = tf.image.random_flip_left_right(image)\n    image = tf.image.random_flip_up_down(image)\n    if p_spatial > 0.75:\n        image = tf.image.transpose(image)\n        \n    # Rotates\n    if p_rotate > 0.75:\n        image = tf.image.rot90(image, k = 3) # rotate 270º\n    elif p_rotate > 0.5:\n        image = tf.image.rot90(image, k = 2) # rotate 180º\n    elif p_rotate > 0.25:\n        image = tf.image.rot90(image, k = 1) # rotate 90º\n        \n    # Pixel-level transforms\n    if p_pixel_1 >= 0.4:\n        image = tf.image.random_saturation(image, lower = 0.7, upper = 1.3)\n    if p_pixel_2 >= 0.4:\n        image = tf.image.random_contrast(image, lower = 0.8, upper = 1.2)\n    if p_pixel_3 >= 0.4:\n        image = tf.image.random_brightness(image, max_delta = 0.1)\n        \n    # Crops\n    if p_crop > 0.7:\n        if p_crop > 0.9:\n            image = tf.image.central_crop(image, central_fraction = 0.7)\n        elif p_crop > 0.8:\n            image = tf.image.central_crop(image, central_fraction = 0.8)\n        else:\n            image = tf.image.central_crop(image, central_fraction = 0.9)\n    elif p_crop > 0.4:\n        crop_size = tf.random.uniform([], int(IMAGE_SIZE[0] * 0.8), IMAGE_SIZE[0], dtype = tf.int32)\n        image = tf.image.random_crop(image, size = [crop_size, crop_size, 3])\n\n    image = tf.image.resize(image, size = IMAGE_SIZE)\n    image = tf.reshape(image, [*IMAGE_SIZE, 3])\n    \n    return image, image_name, target","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def transform(image, image_name, target):\n    # This Function applies both cutmix and mixup\n    DIM = IMAGE_SIZE[0]\n    CLASSES = 5\n    SWITCH = 0.4\n    CUTMIX_PROB = 0.7\n    MIXUP_PROB = 0.7\n    # For SWITCH PERCENT OF TIME WE DO CUTMIX AND (1-SWITCH) WE DO MIXUP\n    image2, label2 = cutmix(image, target, CUTMIX_PROB)\n    image3, label3 = mixup(image, target, MIXUP_PROB)\n    imgs = []; labs = []\n    for j in range(AUG_BATCH):\n        P = tf.cast( tf.random.uniform([],0,1)<=SWITCH, tf.float32)\n        imgs.append(P*image2[j,]+(1-P)*image3[j,])\n        labs.append(P*label2[j,]+(1-P)*label3[j,])\n    # RESHAPE HACK SO TPU COMPILER KNOWS SHAPE OF OUTPUT TENSOR (maybe use Python typing instead?)\n    image4 = tf.reshape(tf.stack(imgs),(AUG_BATCH,DIM,DIM,3))\n    label4 = tf.reshape(tf.stack(labs),(AUG_BATCH,CLASSES))\n    return image4, image_name, label4","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def decode_image(image_data):\n    image = tf.image.decode_jpeg(image_data, channels = 3)\n    # Resize image to be aligned with the inference phase\n    image = tf.image.resize(image, IMAGE_SIZE)\n    # convert image to floats in [0, 1] range\n    image = tf.cast(image, tf.float32) / 255.0\n    # explicit size needed for TPU\n    image = tf.reshape(image, [*IMAGE_SIZE, 3])\n    return image","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def read_labeled_tfrecord(example):\n    LABELED_TFREC_FORMAT = {\n        # tf.string means bytestring\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"image_name\": tf.io.FixedLenFeature([], tf.string),\n        # shape [] means single element\n        \"target\": tf.io.FixedLenFeature([], tf.int64)\n    }\n    example = tf.io.parse_single_example(example, LABELED_TFREC_FORMAT)\n    image = decode_image(example['image'])\n    image_name = example['image_name']\n    target = tf.one_hot(tf.cast(example['target'], tf.int32), N_CLASSES)\n    return image, image_name, target","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def load_dataset(filenames, ordered = False):\n    # Read from TFRecords. For optimal performance, reading from multiple files at once and\n    # Diregarding data order. Order does not matter since we will be shuffling the data anyway\n    \n    ignore_order = tf.data.Options()\n    if not ordered:\n        # disable order, increase speed\n        ignore_order.experimental_deterministic = False \n        \n    # Automatically interleaves reads from multiple files\n    dataset = tf.data.TFRecordDataset(filenames, num_parallel_reads = AUTO)\n    # Use data as soon as it streams in, rather than in its original order\n    dataset = dataset.with_options(ignore_order)\n    # Returns a dataset of (image, image_name, label)\n    dataset = dataset.map(read_labeled_tfrecord, num_parallel_calls = AUTO) \n    return dataset","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_training_dataset(filenames, ordered = False):\n    dataset = load_dataset(filenames, ordered = ordered)\n    dataset = dataset.map(data_augment, num_parallel_calls = AUTO)\n    # The training dataset must repeat for several epochs\n    dataset = dataset.repeat()\n    dataset = dataset.batch(AUG_BATCH)\n    dataset = dataset.map(transform, num_parallel_calls = AUTO)\n    dataset = dataset.unbatch()\n    dataset = dataset.shuffle(2048)\n    dataset = dataset.batch(BATCH_SIZE)\n    # Prefetch next batch while training (autotune prefetch buffer size)\n    dataset = dataset.prefetch(AUTO)\n    return dataset","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_validation_dataset(filenames, ordered = True):\n    dataset = load_dataset(filenames, ordered = ordered)\n    dataset = dataset.batch(BATCH_SIZE)\n    # Using gpu, not enought memory to use cache\n    # dataset = dataset.cache()\n    # Prefetch next batch while training (autotune prefetch buffer size)\n    dataset = dataset.prefetch(AUTO) \n    return dataset","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_val_tta(filenames, ordered = True):\n    dataset = load_dataset(filenames, ordered = ordered)\n    dataset = dataset.map(data_augment, num_parallel_calls = AUTO)\n    dataset = dataset.repeat()\n    dataset = dataset.batch(BATCH_SIZE)\n    dataset = dataset.prefetch(AUTO)\n    return dataset","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def count_data_items(filenames):\n    # The number of data items is written in the name of the .tfrec files, i.e. flowers00-230.tfrec = 230 data items\n    n = [int(re.compile(r\"-([0-9]*)\\.\").search(filename).group(1)) for filename in filenames]\n    return np.sum(n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"NUM_TRAINING_IMAGES = count_data_items(TRAINING_FILENAMES)\nprint(f'Dataset: {NUM_TRAINING_IMAGES} training images')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_lr_callback():\n    lr_start   = 0.000001\n    lr_max     = 0.000003 * BATCH_SIZE\n    lr_min     = 0.000001\n    lr_ramp_ep = 1\n    lr_sus_ep  = 0\n    lr_decay   = 0.8\n   \n    def lrfn(epoch):\n        if epoch < lr_ramp_ep:\n            lr = (lr_max - lr_start) / lr_ramp_ep * epoch + lr_start   \n        elif epoch < lr_ramp_ep + lr_sus_ep:\n            lr = lr_max    \n        else:\n            lr = (lr_max - lr_min) * lr_decay**(epoch - lr_ramp_ep - lr_sus_ep) + lr_min    \n        return lr\n\n    lr_callback = tf.keras.callbacks.LearningRateScheduler(lrfn, verbose = False)\n    return lr_callback","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_model():\n    \n    with strategy.scope():\n        \n        inp = tf.keras.layers.Input(shape = (*IMAGE_SIZE, 3))\n\n        x = efn.EfficientNetB7(weights = 'imagenet', include_top = False)(inp)\n        x = tf.keras.layers.GlobalAveragePooling2D()(x)\n        x = tf.keras.layers.Dropout(0.2)(x)\n        output = tf.keras.layers.Dense(N_CLASSES, activation = 'softmax')(x)\n        \n        model = tf.keras.models.Model(inputs = [inp], outputs = [output])\n\n        opt = tf.keras.optimizers.Adam(learning_rate = LR)\n\n        model.compile(\n            optimizer = opt,\n            loss = [tf.keras.losses.CategoricalCrossentropy(label_smoothing = 0.4)],\n            metrics = [tf.keras.metrics.CategoricalAccuracy()]\n        )\n\n        return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_den_model():\n    \n    with strategy.scope():\n        \n        inp = tf.keras.layers.Input(shape = (*IMAGE_SIZE, 3))\n\n        x = tf.keras.applications.DenseNet121(weights = 'imagenet', include_top = False)(inp)\n        x = tf.keras.layers.GlobalAveragePooling2D()(x)\n        x = tf.keras.layers.Dropout(0.2)(x)\n        output = tf.keras.layers.Dense(N_CLASSES, activation = 'softmax')(x)\n        \n        model = tf.keras.models.Model(inputs = [inp], outputs = [output])\n\n        opt = tf.keras.optimizers.Adam(learning_rate = LR)\n\n        model.compile(\n            optimizer = opt,\n            loss = [tf.keras.losses.CategoricalCrossentropy(label_smoothing = 0.4)],\n            metrics = [tf.keras.metrics.CategoricalAccuracy()]\n        )\n\n        return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def train_and_evaluate(folds = 5, mod=\"EfficientNet\"):\n    oof_image_name = []\n    oof_target = []\n    oof_prediction = np.zeros((NUM_TRAINING_IMAGES, N_CLASSES))\n    previous_number_of_files = 0\n    total_number_of_files = 0\n    \n    # Seed everything\n    seed_everything(SEED)\n    kfold = KFold(folds, shuffle = True, random_state = SEED)\n    for fold, (trn_ind, val_ind) in enumerate(kfold.split(TRAINING_FILENAMES)):\n        if tpu:\n            tf.tpu.experimental.initialize_tpu_system(tpu)\n        print('\\n')\n        print('-'*50)\n        print(f'Training fold {fold + 1}/{folds}')\n        train_dataset = get_training_dataset([TRAINING_FILENAMES[x] for x in trn_ind] + TRAINING_FILENAMES_2019, ordered = False)\n        train_dataset = train_dataset.map(lambda image, image_name, target: (image, target))\n        val_dataset = get_validation_dataset([TRAINING_FILENAMES[x] for x in val_ind], ordered = True)\n        val_dataset_ = val_dataset.map(lambda image, image_name, target: (image, target))\n        STEPS_PER_EPOCH = count_data_items([TRAINING_FILENAMES[x] for x in trn_ind] + TRAINING_FILENAMES_2019) // BATCH_SIZE\n        K.clear_session()\n        \n        if mod == \"EfficientNet\":\n            model = get_model()\n        else:\n            model = get_den_model()\n        \n        # Model checkpoint\n        checkpoint = tf.keras.callbacks.ModelCheckpoint(f'{mod}_EXP5_{fold}_{IMAGE_SIZE[0]}_{SEED}.h5', \n                                                        monitor = 'val_categorical_accuracy', \n                                                        verbose = VERBOSE, \n                                                        save_best_only = True,\n                                                        save_weights_only = True, \n                                                        mode = 'max')\n        history = model.fit(train_dataset,\n                            steps_per_epoch = STEPS_PER_EPOCH,\n                            epochs = EPOCHS,\n                            callbacks = [checkpoint, get_lr_callback()], \n                            validation_data = val_dataset_,\n                            verbose = VERBOSE)\n        \n        # Load weights from the best epoch\n        model.load_weights(f'{mod}_EXP5_{fold}_{IMAGE_SIZE[0]}_{SEED}.h5')\n        \n        number_of_files = count_data_items([TRAINING_FILENAMES[x] for x in val_ind])\n        # Get validation real target and image name\n        image_name = val_dataset.map(lambda image, image_name, target: image_name).unbatch()\n        target = val_dataset.map(lambda image, image_name, target: target).unbatch()\n        image_name = next(iter(image_name.batch(number_of_files))).numpy().astype('U')\n        target = next(iter(target.batch(number_of_files))).numpy()\n        target = np.argmax(target, axis = -1)\n        oof_image_name.extend(list(image_name))\n        oof_target.extend(list(target))\n        \n        # Validation time augmentation\n        steps = TTA * number_of_files / BATCH_SIZE\n        dataset = get_val_tta([TRAINING_FILENAMES[x] for x in val_ind], ordered = True)\n        image = dataset.map(lambda image, image_name, target: image)\n        probabilities = model.predict(image, steps = steps)[: TTA * number_of_files]\n        probabilities = np.mean(probabilities.reshape((number_of_files, TTA, N_CLASSES), order = 'F'), axis = 1)\n        total_number_of_files += number_of_files\n        oof_prediction[previous_number_of_files:total_number_of_files] = probabilities\n        previous_number_of_files += number_of_files\n        \n        print('\\n')\n        print('-'*50)\n        fold_accuracy_score = accuracy_score(target, np.argmax(probabilities, axis = -1))\n        print(f'Our fold {fold + 1} accuracy score validation with {TTA} TTA is {fold_accuracy_score}')\n        \n    print('\\n')\n    print('-'*50)\n    oof_accuracy_score = accuracy_score(oof_target, np.argmax(oof_prediction, axis = -1))\n    print(f'Our out of folds accuracy score is {oof_accuracy_score}')\n    \n    # Save the out of folds predictions\n    print('Saving out of folds to disk...')\n    oof_dataset = pd.DataFrame({'oof_image_name': oof_image_name, 'oof_target': oof_target, 'oof_prediction': list(oof_prediction)})\n    oof_dataset.to_csv(f'{mod}_EXP5_{IMAGE_SIZE[0]}_{SEED}.csv', index = False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_and_evaluate(folds = 5, mod=\"EfficientNet\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# train_and_evaluate(folds = 5, mod=\"DenseNet\")","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}