{"cells":[{"metadata":{"_uuid":"aa8401d73c7a19e1a43fdd6a992ea9dcb60039a2"},"cell_type":"markdown","source":"# Overview\nRather than trying to segment, we start off by making a model that simply tries to identify if any boat shows up in the image. \nFor this model we can see roughly how it performs in the compititon by guessing the whole image (as an RLE) if any boat shows up (not a very smart startegy, but might provide some interesting results). \n\n## Beyond\nThe model could also be useful as a quick way (low resolution images) to screen through lots of images to see if they are likely to have a boat and if they are then run a much more expensive full-resolution U-Net on that sample"},{"metadata":{"_uuid":"a6cd9d5ad61ffe3b8858769f20a5f9493f024a56"},"cell_type":"markdown","source":"## Model Parameters\nWe might want to adjust these later (or do some hyperparameter optimizations). It is slightly easier to keep track of parallel notebooks with different parameters if they are all at the beginning in a clear (machine readable format, see Kaggling with Kaggle (https://www.kaggle.com/kmader/kaggling-with-kaggle)."},{"metadata":{"trusted":true,"_uuid":"301a5d939c566d1487a049bb2554d09b592b18b1","collapsed":true},"cell_type":"code","source":"GAUSSIAN_NOISE = 0.1\nUPSAMPLE_MODE = 'SIMPLE'\n# number of validation images to use\nVALID_IMG_COUNT = 1000\n# maximum number of training images\nMAX_TRAIN_IMAGES = 15000 \nBASE_MODEL='DenseNet169' # ['VGG16', 'RESNET52', 'InceptionV3', 'Xception', 'DenseNet169', 'DenseNet121']\nIMG_SIZE = (299, 299) # [(224, 224), (384, 384), (512, 512), (640, 640)]\nBATCH_SIZE = 64 # [1, 8, 16, 24]\nDROPOUT = 0.5\nDENSE_COUNT = 128\nLEARN_RATE = 1e-4\nRGB_FLIP = 1 # should rgb be flipped when rendering images","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"collapsed":true},"cell_type":"code","source":"import os  # input data paths\nimport cv2\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nfrom skimage.io import imread\nimport matplotlib.pyplot as plt\nfrom skimage.segmentation import mark_boundaries  # can opencv be used ?\nfrom skimage.util.montage import montage2d as montage\nimport gc; gc.enable() # memory is tight\nmontage_rgb = lambda x: np.stack([montage(x[:, :, :, i]) for i in range(x.shape[3])], -1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"collapsed":true,"_uuid":"456e683871d69cd7b8329767a8106de4c685f821"},"cell_type":"code","source":"ship_dir = '../input'\ntrain_image_dir = os.path.join(ship_dir, 'train')\ntest_image_dir = os.path.join(ship_dir, 'test')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"3ca7119188fbb4c6540d9df55f5833b55435287e","collapsed":true},"cell_type":"code","source":"masks = pd.read_csv(os.path.join('../input/','train_ship_segmentations.csv'))\nmasks.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"6c916d1675509fa98435cf66c11131814aaa281e","collapsed":true},"cell_type":"code","source":"masks.iloc[1]['EncodedPixels']","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"ae58571c503e963fcf10712e47b4776b7c42b398","collapsed":true},"cell_type":"code","source":"print(masks.shape[0], 'masks found')\nprint(masks['ImageId'].value_counts().shape[0], 'unique images')\nmasks['path'] = masks['ImageId'].map(lambda x: os.path.join(train_image_dir, x))\nimages_with_ship = masks.ImageId[masks.EncodedPixels.isnull()==False]\nimages_with_ship = np.unique(images_with_ship.values)\nprint('There are ' +str(len(images_with_ship)) + ' image files with ships')\nmasks.head()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"f1f16094c630b914baab2fd862d1caae4b822a20"},"cell_type":"markdown","source":"There are 131030 findings of which 104070 are from different images."},{"metadata":{"_uuid":"40cb72e241c0c3d8bc245b4e3c663b4a835b0011"},"cell_type":"markdown","source":"# Split into training and validation groups\nWe stratify by the number of boats appearing so we have nice balances in each set"},{"metadata":{"trusted":true,"_uuid":"871720221ac25f7f9408bfe01aeb4ccb95edbd1f","collapsed":true},"cell_type":"code","source":"from sklearn.model_selection import train_test_split\nmasks['ships'] = masks['EncodedPixels'].map(lambda c_row: 1 if isinstance(c_row, str) else 0)\n","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"bb742d600c3fdaea05667870e7ca73a680cb39a7"},"cell_type":"markdown","source":"Added a new column  \"ships\".  If an image has ship or not is marked as 0 or 1 respectively."},{"metadata":{"trusted":true,"_uuid":"25555ad3062c705ecab0ea850df8a6f3912e9d12","collapsed":true},"cell_type":"code","source":"masks.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"8c143b39bd56dc5f89f4aabc1549f656ac7bf0d5","collapsed":true},"cell_type":"code","source":"    img_without_ship = cv2.imread('../input/train/00003e153.jpg')\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"6b79957ed3f0d9878db45a39f449ca990d29675a","collapsed":true},"cell_type":"code","source":"unique_img_ids = masks.groupby('ImageId').agg({'ships': 'sum'}).reset_index()\nunique_img_ids.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"6d530d4070a143561df56967b3ca69cbbde318a1","collapsed":true},"cell_type":"code","source":"unique_img_ids['has_ship'] = unique_img_ids['ships'].map(lambda x: 1.0 if x>0 else 0.0)\nunique_img_ids.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"c6896f03fb8124467e0b6d22655520a9ff0aaa12","collapsed":true},"cell_type":"code","source":"unique_img_ids['has_ship_vec'] = unique_img_ids['has_ship'].map(lambda x: [x])\nunique_img_ids.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"94db682433209c5f44d814715ccb7aac0251321b","collapsed":true},"cell_type":"code","source":"masks.drop(['ships'], axis=1, inplace=True)\nmasks.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"9db17121e2611914f6b593f7fce7c7226ae9b541","collapsed":true},"cell_type":"code","source":"train_ids, valid_ids = train_test_split(unique_img_ids, test_size = 0.3, stratify = unique_img_ids['ships'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"1128c1c9197f81738fdd9b2fca3d6ab9f874cc20","collapsed":true},"cell_type":"code","source":"train_ids.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"f177864cd07438e253d0d224df2f3285a1745d0d","collapsed":true},"cell_type":"code","source":"valid_ids.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"7a3a93d19a3a8cac1d9a67786d308c3d3e368165","collapsed":true},"cell_type":"code","source":"train_df = pd.merge(masks, train_ids)\ntrain_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"0f51b39c88e9686fd01a7d25e9826a879eb93555","collapsed":true},"cell_type":"code","source":"valid_df = pd.merge(masks, valid_ids)\nvalid_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"0b69764a2585c4c8222c535cefdb4bc6fc896716","collapsed":true},"cell_type":"code","source":"print(train_df.shape[0], 'training masks')\nprint(valid_df.shape[0], 'validation masks')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"c21d5bff04bf9180463969ac120379345745ed03"},"cell_type":"markdown","source":"### Examine Number of Ship Images\nHere we examine how often ships appear and replace the ones without any ships with 0"},{"metadata":{"trusted":true,"collapsed":true,"_uuid":"fa2154d3621497e7497731e5ef6c6c72f27ad483"},"cell_type":"code","source":"train_df = train_df.sample(min(MAX_TRAIN_IMAGES, train_df.shape[0])) # limit size of training set (otherwise it takes too long)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"2612fa47c7e9fdcaa7aa720c4e15fc86fd65d69a","collapsed":true},"cell_type":"code","source":"train_df[['ships', 'has_ship']].hist()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"a8f65e7942816fb75b687a549dc1d5cc48d00e21"},"cell_type":"markdown","source":"# Augment Data"},{"metadata":{"trusted":true,"_uuid":"ccbc1747ffb0f2942cc99bc27e4afc3262ba9f94","collapsed":true},"cell_type":"code","source":"from keras.preprocessing.image import ImageDataGenerator\nif BASE_MODEL=='VGG16':\n    from keras.applications.vgg16 import VGG16 as PTModel, preprocess_input\nelif BASE_MODEL=='RESNET52':\n    from keras.applications.resnet50 import ResNet50 as PTModel, preprocess_input\nelif BASE_MODEL=='InceptionV3':\n    from keras.applications.inception_v3 import InceptionV3 as PTModel, preprocess_input\nelif BASE_MODEL=='Xception':\n    from keras.applications.xception import Xception as PTModel, preprocess_input\nelif BASE_MODEL=='DenseNet169': \n    from keras.applications.densenet import DenseNet169 as PTModel, preprocess_input\nelif BASE_MODEL=='DenseNet121':\n    from keras.applications.densenet import DenseNet121 as PTModel, preprocess_input\nelse:\n    raise ValueError('Unknown model: {}'.format(BASE_MODEL))","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true,"collapsed":true},"cell_type":"code","source":"from keras.preprocessing.image import ImageDataGenerator\ndg_args = dict(featurewise_center = False, \n                  samplewise_center = False,\n                  rotation_range = 45, \n                  width_shift_range = 0.1, \n                  height_shift_range = 0.1, \n                  shear_range = 0.01,\n                  zoom_range = [0.9, 1.25],  \n                  brightness_range = [0.5, 1.5],\n                  horizontal_flip = True, \n                  vertical_flip = True,\n                  fill_mode = 'reflect',\n                   data_format = 'channels_last',\n              preprocessing_function = preprocess_input)\nvalid_args = dict(fill_mode = 'reflect',\n                   data_format = 'channels_last',\n                  preprocessing_function = preprocess_input)\n\ncore_idg = ImageDataGenerator(**dg_args)\nvalid_idg = ImageDataGenerator(**valid_args)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"collapsed":true,"_uuid":"c5cc31e3117fdfa923b2acb1e6542a7007f4f955"},"cell_type":"code","source":"def flow_from_dataframe(img_data_gen, in_df, path_col, y_col, **dflow_args):\n    base_dir = os.path.dirname(in_df[path_col].values[0])\n    print('## Ignore next message from keras, values are replaced anyways')\n    df_gen = img_data_gen.flow_from_directory(base_dir, \n                                     class_mode = 'sparse',\n                                    **dflow_args)\n    df_gen.filenames = in_df[path_col].values\n    df_gen.classes = np.stack(in_df[y_col].values)\n    df_gen.samples = in_df.shape[0]\n    df_gen.n = in_df.shape[0]\n    df_gen._set_index_array()\n    df_gen.directory = '' # since we have the full path\n    print('Reinserting dataframe: {} images'.format(in_df.shape[0]))\n    return df_gen","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"67136e1743dbbc4e07dba0d69f79231603f31d93","collapsed":true},"cell_type":"code","source":"train_gen = flow_from_dataframe(core_idg, train_df, \n                             path_col = 'path',\n                            y_col = 'has_ship_vec', \n                            target_size = IMG_SIZE,\n                             color_mode = 'rgb',\n                            batch_size = BATCH_SIZE)\n\n# used a fixed dataset for evaluating the algorithm\nvalid_x, valid_y = next(flow_from_dataframe(valid_idg, \n                               valid_df, \n                             path_col = 'path',\n                            y_col = 'has_ship_vec', \n                            target_size = IMG_SIZE,\n                             color_mode = 'rgb',\n                            batch_size = VALID_IMG_COUNT)) # one big batch\nprint(valid_x.shape, valid_y.shape)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"6122ccb9e58bfac6fa5e11c86121e78d9e5151b1","collapsed":true},"cell_type":"code","source":"t_x, t_y = next(train_gen)\nprint('x', t_x.shape, t_x.dtype, t_x.min(), t_x.max())\nprint('y', t_y.shape, t_y.dtype, t_y.min(), t_y.max())\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize = (20, 10))\nax1.imshow(montage_rgb((t_x-t_x.min())/(t_x.max()-t_x.min()))[:, :, ::RGB_FLIP], cmap='gray')\nax1.set_title('images')\nax2.plot(t_y)\nax2.set_title('ships')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"ba08494eb9736ec3556b7c879143cdcdea89febf"},"cell_type":"markdown","source":"# Build a Model\nWe build the pre-trained top model and then use a global-max-pooling (we are trying to detect any ship in the image and thus max is better suited than averaging (which would tend to favor larger ships to smaller ones). "},{"metadata":{"trusted":true,"_uuid":"9fb62d14ec059f7f92d82e07b35822169c77112d","collapsed":true},"cell_type":"code","source":"base_pretrained_model = PTModel(input_shape =  t_x.shape[1:], \n                              include_top = False, weights = 'imagenet')\nbase_pretrained_model.trainable = False","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"0bc7c0ee177697838d22aed4de046e7542027a10"},"cell_type":"markdown","source":"## Setup the Subsequent Layers\nHere we setup the rest of the model which we will actually be training"},{"metadata":{"trusted":true,"_uuid":"2687377309d3cbbab1197f4eccd2b50ab996f5a6","collapsed":true},"cell_type":"code","source":"from keras import models, layers\nfrom keras.optimizers import Adam\nimg_in = layers.Input(t_x.shape[1:], name='Image_RGB_In')\nimg_noise = layers.GaussianNoise(GAUSSIAN_NOISE)(img_in)\npt_features = base_pretrained_model(img_noise)\npt_depth = base_pretrained_model.get_output_shape_at(0)[-1]\nbn_features = layers.BatchNormalization()(pt_features)\nfeature_dropout = layers.SpatialDropout2D(DROPOUT)(bn_features)\ngmp_dr = layers.GlobalMaxPooling2D()(feature_dropout)\ndr_steps = layers.Dropout(DROPOUT)(layers.Dense(DENSE_COUNT, activation = 'relu')(gmp_dr))\nout_layer = layers.Dense(1, activation = 'sigmoid')(dr_steps)\n\nship_model = models.Model(inputs = [img_in], outputs = [out_layer], name = 'full_model')\n\nship_model.compile(optimizer = Adam(lr=LEARN_RATE), \n                   loss = 'binary_crossentropy',\n                   metrics = ['binary_accuracy'])\n\nship_model.summary()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"7282d18de3aff1cee12ff89b7d511a391702814f","collapsed":true},"cell_type":"code","source":"from keras.callbacks import ModelCheckpoint, LearningRateScheduler, EarlyStopping, ReduceLROnPlateau\nweight_path=\"{}_weights.best.hdf5\".format('boat_detector')\n\ncheckpoint = ModelCheckpoint(weight_path, monitor='val_loss', verbose=1, \n                             save_best_only=True, mode='min', save_weights_only = True)\n\nreduceLROnPlat = ReduceLROnPlateau(monitor='val_loss', factor=0.8, patience=10, verbose=1, mode='auto', epsilon=0.0001, cooldown=5, min_lr=0.0001)\nearly = EarlyStopping(monitor=\"val_loss\", \n                      mode=\"min\", \n                      patience=10) # probably needs to be more patient, but kaggle time is limited\ncallbacks_list = [checkpoint, early, reduceLROnPlat]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"5b67d808c0b8c7e28bff41e6d3858ff6f09dd626","scrolled":false,"collapsed":true},"cell_type":"code","source":"train_gen.batch_size = BATCH_SIZE\nship_model.fit_generator(train_gen, \n                      validation_data = (valid_x, valid_y), \n                      epochs = 30, \n                      callbacks = callbacks_list,\n                      workers = 3)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"collapsed":true,"_uuid":"a168c8b1af446b800f6129104906003ededd61c4"},"cell_type":"code","source":"ship_model.load_weights(weight_path)\nship_model.save('full_ship_model.h5')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"17edb177402ae51651692511827a7e9d60646533"},"cell_type":"markdown","source":"# Run the test data\nWe use the sample_submission file as the basis for loading and running the images."},{"metadata":{"trusted":true,"_uuid":"4911811f267f9f3397a58902da9e75c6f261ad40","collapsed":true},"cell_type":"code","source":"test_paths = os.listdir(test_image_dir)\nprint(len(test_paths), 'test images found')\nsubmission_df = pd.read_csv('../input/sample_submission.csv')\nsubmission_df['path'] = submission_df['ImageId'].map(lambda x: os.path.join(test_image_dir, x))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"5b3eea954bd883d598c6ac80167dfd4353b0f558"},"cell_type":"markdown","source":"# Setup Test Data Generator\nWe use the same generator as before to read and preprocess images"},{"metadata":{"trusted":true,"_uuid":"f75595679ba8606fd1ac15645b8612e117db0b30","collapsed":true},"cell_type":"code","source":"test_gen = flow_from_dataframe(valid_idg, \n                               submission_df, \n                             path_col = 'path',\n                            y_col = 'ImageId', \n                            target_size = IMG_SIZE,\n                             color_mode = 'rgb',\n                            batch_size = BATCH_SIZE, \n                              shuffle = False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"73ef7b3b2a74bf64968c79b4005075d4f0e23143","collapsed":true},"cell_type":"code","source":"fig, m_axs = plt.subplots(3, 2, figsize = (20, 30))\nfor (ax1, ax2), (t_x, c_img_names) in zip(m_axs, test_gen):\n    t_y = ship_model.predict(t_x)\n    t_stack = ((t_x-t_x.min())/(t_x.max()-t_x.min()))[:, :, :, ::RGB_FLIP]\n    ax1.imshow(montage_rgb(t_stack))\n    ax1.set_title('images')\n    alpha_stack = np.tile(np.expand_dims(np.expand_dims(t_y, -1), -1), [1, t_stack.shape[1], t_stack.shape[2], 1])\n    rgba_stack = np.concatenate([t_stack, alpha_stack], -1)\n    ax2.imshow(montage_rgb(rgba_stack))\n    ax2.set_title('ships')\nfig.savefig('test_predictions.png')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"22f5f7e53cb82ff54bfb0e4ee818315d05c1e1af"},"cell_type":"markdown","source":"# Prepare Submission\nProcess all images (batchwise) and keep the score at the end"},{"metadata":{"trusted":true,"_uuid":"b103e0ce6ccae69dbf82a55067dc693efaeadf8f","collapsed":true},"cell_type":"code","source":"BATCH_SIZE = BATCH_SIZE*2 # we can use larger batches for inference\ntest_gen = flow_from_dataframe(valid_idg, \n                               submission_df, \n                             path_col = 'path',\n                            y_col = 'ImageId', \n                            target_size = IMG_SIZE,\n                             color_mode = 'rgb',\n                            batch_size = BATCH_SIZE, \n                              shuffle = False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"0a38d343b2654f87934a88524ebc14a5759e07cb","collapsed":true},"cell_type":"code","source":"from tqdm import tqdm_notebook\nall_scores = dict()\nfor _, (t_x, t_names) in zip(tqdm_notebook(range(test_gen.n//BATCH_SIZE+1)),\n                            test_gen):\n    t_y = ship_model.predict(t_x)[:, 0]\n    for c_id, c_score in zip(t_names, t_y):\n        all_scores[c_id] = c_score","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"4a942dbb4a939d73526dbb35745402b35785fdf4"},"cell_type":"markdown","source":"# Show the Scores\nHere we see the scores and we have to decide about a cut-off for counting an image as ship or not. We can be lazy and pick 0.5 but some more rigorous cross-validation would definitely improve this process."},{"metadata":{"trusted":true,"_uuid":"41ab9326407f59c983f3991ff736da8dd822605c","collapsed":true},"cell_type":"code","source":"submission_df['score'] = submission_df['ImageId'].map(lambda x: all_scores.get(x, 0))\nsubmission_df['score'].hist()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"fae5322806f03112d8110755ca48a3bdb05eeded"},"cell_type":"markdown","source":"# Make the RLE data if there is a ship\nHere we make the RLE data for a positive image (assume every pixel is ship)"},{"metadata":{"trusted":true,"_uuid":"80acd5482d2a6117648a842b2f0da380bced79a5","collapsed":true},"cell_type":"code","source":"# ref: https://www.kaggle.com/paulorzp/run-length-encode-and-decode\ndef rle_encode(img):\n    '''\n    img: numpy array, 1 - mask, 0 - background\n    Returns run length as string formated\n    '''\n    pixels = img.T.flatten()\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    return ' '.join(str(x) for x in runs)\n# empty image\nzp_dim = 10\nout_img = np.ones((768-2*zp_dim, 768-2*zp_dim), dtype=bool)\nout_img = np.pad(out_img, ((zp_dim, zp_dim),), mode='constant', constant_values=0)\nplt.matshow(out_img)\nprint(out_img.shape)\npos_ship_str = rle_encode(out_img)\nprint(pos_ship_str[:50])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"580506b6e934b9a9a362deaf6b88e5d71c770405","collapsed":true},"cell_type":"code","source":"# add the whole image if it is above the threshold\nsubmission_df['EncodedPixels'] = submission_df['score'].map(lambda x: pos_ship_str if x>0.5 else None)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"b534a825f5a1997a935aa3e3f338da657024bc19","collapsed":true},"cell_type":"code","source":"out_df = submission_df[['ImageId', 'EncodedPixels']]\nout_df.to_csv('submission.csv', index=False)\nout_df.head(20)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"collapsed":true,"_uuid":"07dd065b26b4018ea76bd7d90ee9639a2b2c7480"},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"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}