{"cells":[{"metadata":{"trusted":true,"_kg_hide-output":true,"_kg_hide-input":true},"cell_type":"code","source":"!pip install '/kaggle/input/torch-15/torch-1.5.0cu101-cp37-cp37m-linux_x86_64.whl'\n!pip install '/kaggle/input/torch-15/torchvision-0.6.0cu101-cp37-cp37m-linux_x86_64.whl'\n!pip install '/kaggle/input/torch-15/yacs-0.1.7-py3-none-any.whl'\n!pip install '/kaggle/input/torch-15/fvcore-0.1.1.post200513-py3-none-any.whl'\n!pip install '/kaggle/input/pycocotools/pycocotools-2.0-cp37-cp37m-linux_x86_64.whl'\n!pip install '/kaggle/input/detectron2/detectron2-0.1.3cu101-cp37-cp37m-linux_x86_64.whl'","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm.notebook import tqdm\nimport gc\nimport os\nimport copy\nfrom glob import glob\nimport cv2\nfrom PIL import Image\nimport random\nfrom collections import deque, defaultdict\nfrom multiprocessing import Pool, Process\nfrom functools import partial\n\nimport torch\n\nimport pycocotools\nimport detectron2\nfrom detectron2.config import get_cfg\nfrom detectron2 import model_zoo\nfrom detectron2.engine import DefaultPredictor, DefaultTrainer\nfrom detectron2.utils.visualizer import Visualizer, ColorMode\nfrom detectron2.structures import BoxMode\nfrom detectron2.data import datasets, DatasetCatalog, MetadataCatalog, build_detection_train_loader, build_detection_test_loader\nfrom detectron2.data import transforms as T\nfrom detectron2.data import detection_utils as utils\nfrom detectron2.evaluation import COCOEvaluator, verify_results\nfrom detectron2.modeling import GeneralizedRCNNWithTTA\nfrom detectron2.data.transforms import TransformGen\nfrom detectron2.utils.logger import setup_logger\nsetup_logger()\n\nfrom fvcore.transforms.transform import TransformList, Transform, NoOpTransform\nfrom contextlib import contextmanager\n\nimport torch.nn as nn","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Config","execution_count":null},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"MAIN_PATH = '/kaggle/input/global-wheat-detection'\nTRAIN_IMAGE_PATH = os.path.join(MAIN_PATH, 'train/')\nTEST_IMAGE_PATH = os.path.join(MAIN_PATH, 'test/')\nTRAIN_PATH = os.path.join(MAIN_PATH, 'train.csv')\nSUB_PATH = os.path.join(MAIN_PATH, 'sample_submission.csv')\nPADDING = 5\n\nMODEL_USE = 'retinanet'\nNUMBER_TRAIN_SAMPLE = -1\nif MODEL_USE == 'faster_rcnn':\n    MODEL_PATH = 'COCO-Detection/faster_rcnn_R_101_FPN_3x.yaml'\n    WEIGHT_PATH = '/kaggle/input/detectron2-faster-rcnn-101/model_final_f6e8b1.pkl'\nelif MODEL_USE == 'retinanet':\n    MODEL_PATH = 'COCO-Detection/retinanet_R_101_FPN_3x.yaml'\n    WEIGHT_PATH = '/kaggle/input/detectron2-faster-rcnn-101/model_final_971ab9.pkl'","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Function","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def display_feature(df, feature):\n    \n    plt.figure(figsize=(15,8))\n    ax = sns.countplot(y=feature, data=df, order=df[feature].value_counts().index)\n\n    for p in ax.patches:\n        ax.annotate('{:.2f}%'.format(100*p.get_width()/df.shape[0]), (p.get_x() + p.get_width() + 0.02, p.get_y() + p.get_height()/2))\n\n    plt.title(f'Distribution of {feature}', size=25, color='b')    \n    plt.show()\n    \n    \n    \ndef rand_bbox(img, box_size=50):\n    \n    h, w = img.shape[:2]\n    num_rand = np.random.randint(10, 20)\n    for num_cut in range(num_rand):\n        x_rand, y_rand = random.randint(0, w-box_size), random.randint(0, h-box_size)\n        img[x_rand:x_rand+box_size, y_rand:y_rand+box_size, :] = 0\n    \n    return img","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Check image","execution_count":null},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"train_img = glob(f'{TRAIN_IMAGE_PATH}/*.jpg')\ntest_img = glob(f'{TEST_IMAGE_PATH}/*.jpg')\n\nprint(f'Number of train image:{len(train_img)}, test image:{len(test_img)}')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Sub","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"sub_df = pd.read_csv(SUB_PATH)\nsub_df.tail()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Train file","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train_df = pd.read_csv(TRAIN_PATH)\ntrain_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"list_source = train_df['source'].unique().tolist()\nprint(list_source)\ndisplay_feature(train_df, 'source')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"num_box = train_df.groupby('image_id')['bbox'].count().reset_index().add_prefix('Number_').sort_values('Number_bbox', ascending=False)\nnum_box.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\n\nimage_unique = train_df['image_id'].unique()\nimage_unique_in_train_path = [i for i in image_unique if i + '.jpg' in os.listdir(TRAIN_IMAGE_PATH)]\n\nprint(f'Number of image unique: {len(image_unique)}, in train path: {len(image_unique_in_train_path)}')\n\ndel image_unique, image_unique_in_train_path\ngc.collect()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Display","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def list_color():\n    class_unique = sorted(train_df['source'].unique().tolist())\n    dict_color = dict()\n    for classid in class_unique:\n        dict_color[classid] = random.sample(range(256), 3)\n    \n    return dict_color\n\n\ndef display_image(df, folder, num_img=1, cutmix_prob=0.5):\n    \n    if df is train_df:\n        dict_color = list_color()\n        \n    for i in range(num_img):\n        fig, ax = plt.subplots(figsize=(15, 15))\n        img_random = random.choice(df['image_id'].unique())\n        assert (img_random + '.jpg') in os.listdir(folder)\n        \n        img_df = df[df['image_id']==img_random]\n        img_df.reset_index(drop=True, inplace=True)\n        \n        img = cv2.imread(os.path.join(folder, img_random + '.jpg'))\n        if random.random() > cutmix_prob:\n            img = rand_bbox(img, box_size=50)\n                \n            \n        for row in range(len(img_df)):\n            source = img_df.loc[row, 'source']\n            box = img_df.loc[row, 'bbox'][1:-1]\n            box = list(map(float, box.split(', ')))\n            x, y, w, h = list(map(int, box))\n            if df is train_df:\n                cv2.rectangle(img, (x, y), (x+w, y+h), dict_color[source], 2)\n            else:\n                cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 255), 2)\n                \n        ax.set_title(f'{img_random} have {len(img_df)} bbox')\n        ax.imshow(img)   \n        \n    plt.show()        \n    plt.tight_layout()\n    \ndisplay_image(train_df, TRAIN_IMAGE_PATH)    ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Dataset","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\n\ndef wheat_dataset(df, folder, is_train, img_unique):\n    img_id, img_name = img_unique\n    if is_train:\n        img_group = df[df['image_id']==img_name].reset_index(drop=True)\n        record = defaultdict()\n        img_path = os.path.join(folder, img_name+'.jpg')\n        \n        record['file_name'] = img_path\n        record['image_id'] = img_id\n        record['height'] = int(img_group.loc[0, 'height'])\n        record['width'] = int(img_group.loc[0, 'width'])\n        \n        annots = deque()\n        for _, ant in img_group.iterrows():\n            source = ant.source\n            annot = defaultdict()\n            box = ant.bbox[1:-1]\n            box = list(map(float, box.split(', ')))\n            x, y, w, h = list(map(int, box))\n            \n            if random.random() >= 0.75:\n                random_x = random.randint(0, PADDING)       \n                if (x+random_x <= int(img_group.loc[0, 'width'])) and (w >= random_x):\n                    x += random_x\n                    w -= random_x                \n            elif random.random() >= 0.75:\n                random_y = random.randint(0, PADDING)\n                if (y+random_y <= int(img_group.loc[0, 'height'])) and (h >= random_y):\n                    y += random_y\n                    h -= random_y\n            else:\n                if random.random() >= 0.75:\n                    random_w = random.randint(0, PADDING)\n                    if w >= random_w:\n                        w -= random_w\n                elif random.random() >= 0.75:\n                    random_h = random.randint(0, PADDING)\n                    if h >= random_h:\n                        h -= random_h\n                            \n            annot['bbox'] = (x, y, x+w, y+h)\n            annot['bbox_mode'] = BoxMode.XYXY_ABS\n            annot['category_id'] = 0\n            \n            annots.append(dict(annot))\n            \n        record['annotations'] = list(annots)\n    \n    else:\n        img_group = df[df['image_id']==img_name].reset_index(drop=True)\n        record = defaultdict()\n        img_path = os.path.join(folder, img_name+'.jpg')\n        img = cv2.imread(img_path)\n        h, w = img.shape[:2]\n        \n        record['file_name'] = img_path\n        record['image_id'] = img_id\n        record['height'] = int(h)\n        record['width'] = int(w)\n    \n    return dict(record)\n\n\n\ndef wheat_parallel(df, folder, is_train):\n    \n    if is_train:\n        if NUMBER_TRAIN_SAMPLE != -1:\n            df = df[:NUMBER_TRAIN_SAMPLE]\n        \n    pool = Pool()\n    img_uniques = list(zip(range(df['image_id'].nunique()), df['image_id'].unique()))\n    func = partial(wheat_dataset, df, folder, is_train)\n    detaset_dict = pool.map(func, img_uniques)\n    pool.close()\n    pool.join()\n    \n    return detaset_dict","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class CutMix(Transform):\n    \n    def __init__(self, box_size=50, prob_cutmix=0.5):\n        super().__init__()\n        \n        self.box_size = box_size\n        self.prob_cutmix = prob_cutmix\n        \n    def apply_image(self, img):\n        \n        if random.random() > self.prob_cutmix:\n            \n            h, w = img.shape[:2]\n            num_rand = np.random.randint(10, 20)\n            for num_cut in range(num_rand):\n                x_rand, y_rand = random.randint(0, w-self.box_size), random.randint(0, h-self.box_size)\n                img[x_rand:x_rand+self.box_size, y_rand:y_rand+self.box_size, :] = 0\n        \n        return np.asarray(img)\n    \n    def apply_coords(self, coords):\n        return coords.astype(np.float32)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class DatasetMapper:\n    \"\"\"\n    A callable which takes a dataset dict in Detectron2 Dataset format,\n    and map it into a format used by the model.\n\n    This is the default callable to be used to map your dataset dict into training data.\n    You may need to follow it to implement your own one for customized logic,\n    such as a different way to read or transform images.\n    See :doc:`/tutorials/data_loading` for details.\n\n    The callable currently does the following:\n\n    1. Read the image from \"file_name\"\n    2. Applies cropping/geometric transforms to the image and annotations\n    3. Prepare data and annotations to Tensor and :class:`Instances`\n    \"\"\"\n\n    def __init__(self, cfg, is_train=True):\n        if cfg.INPUT.CROP.ENABLED and is_train:\n            self.crop_gen = T.RandomCrop(cfg.INPUT.CROP.TYPE, cfg.INPUT.CROP.SIZE)\n            logging.getLogger(__name__).info(\"CropGen used in training: \" + str(self.crop_gen))\n        else:\n            self.crop_gen = None\n\n#         self.tfm_gens = utils.build_transform_gen(cfg, is_train)\n        self.tfm_gens = [T.RandomBrightness(0.1, 1.6),\n                         T.RandomContrast(0.1, 3),\n                         T.RandomSaturation(0.1, 2),\n                         T.RandomRotation(angle=[90, 90]),\n                         T.RandomFlip(prob=0.4, horizontal=False, vertical=True),\n                         T.RandomCrop('relative_range', (0.4, 0.6)),\n                         CutMix()\n                        ]\n\n        # fmt: off\n        self.img_format     = cfg.INPUT.FORMAT\n        self.mask_on        = cfg.MODEL.MASK_ON\n        self.mask_format    = cfg.INPUT.MASK_FORMAT\n        self.keypoint_on    = cfg.MODEL.KEYPOINT_ON\n        self.load_proposals = cfg.MODEL.LOAD_PROPOSALS\n        # fmt: on\n        if self.keypoint_on and is_train:\n            # Flip only makes sense in training\n            self.keypoint_hflip_indices = utils.create_keypoint_hflip_indices(cfg.DATASETS.TRAIN)\n        else:\n            self.keypoint_hflip_indices = None\n\n        if self.load_proposals:\n            self.min_box_side_len = cfg.MODEL.PROPOSAL_GENERATOR.MIN_SIZE\n            self.proposal_topk = (\n                cfg.DATASETS.PRECOMPUTED_PROPOSAL_TOPK_TRAIN\n                if is_train\n                else cfg.DATASETS.PRECOMPUTED_PROPOSAL_TOPK_TEST\n            )\n        self.is_train = is_train\n\n    def __call__(self, dataset_dict):\n        \"\"\"\n        Args:\n            dataset_dict (dict): Metadata of one image, in Detectron2 Dataset format.\n\n        Returns:\n            dict: a format that builtin models in detectron2 accept\n        \"\"\"\n        dataset_dict = copy.deepcopy(dataset_dict)  # it will be modified by code below\n        # USER: Write your own image loading if it's not from a file\n        image = utils.read_image(dataset_dict[\"file_name\"], format=self.img_format)\n        utils.check_image_size(dataset_dict, image)\n\n        if \"annotations\" not in dataset_dict:\n            image, transforms = T.apply_transform_gens(\n                ([self.crop_gen] if self.crop_gen else []) + self.tfm_gens, image\n            )\n        else:\n            # Crop around an instance if there are instances in the image.\n            # USER: Remove if you don't use cropping\n            if self.crop_gen:\n                crop_tfm = utils.gen_crop_transform_with_instance(\n                    self.crop_gen.get_crop_size(image.shape[:2]),\n                    image.shape[:2],\n                    np.random.choice(dataset_dict[\"annotations\"]),\n                )\n                image = crop_tfm.apply_image(image)\n            image, transforms = T.apply_transform_gens(self.tfm_gens, image)\n            if self.crop_gen:\n                transforms = crop_tfm + transforms\n\n        image_shape = image.shape[:2]  # h, w\n\n        # Pytorch's dataloader is efficient on torch.Tensor due to shared-memory,\n        # but not efficient on large generic data structures due to the use of pickle & mp.Queue.\n        # Therefore it's important to use torch.Tensor.\n        dataset_dict[\"image\"] = torch.as_tensor(np.ascontiguousarray(image.transpose(2, 0, 1)))\n\n        # USER: Remove if you don't use pre-computed proposals.\n        if self.load_proposals:\n            utils.transform_proposals(\n                dataset_dict, image_shape, transforms, self.min_box_side_len, self.proposal_topk\n            )\n\n        if not self.is_train:\n            # USER: Modify this if you want to keep them for some reason.\n            dataset_dict.pop(\"annotations\", None)\n            dataset_dict.pop(\"sem_seg_file_name\", None)\n            return dataset_dict\n\n        if \"annotations\" in dataset_dict:\n            # USER: Modify this if you want to keep them for some reason.\n            for anno in dataset_dict[\"annotations\"]:\n                if not self.mask_on:\n                    anno.pop(\"segmentation\", None)\n                if not self.keypoint_on:\n                    anno.pop(\"keypoints\", None)\n\n            # USER: Implement additional transformations if you have other types of data\n            annos = [\n                utils.transform_instance_annotations(\n                    obj, transforms, image_shape, keypoint_hflip_indices=self.keypoint_hflip_indices\n                )\n                for obj in dataset_dict.pop(\"annotations\")\n                if obj.get(\"iscrowd\", 0) == 0\n            ]\n            instances = utils.annotations_to_instances(\n                annos, image_shape, mask_format=self.mask_format\n            )\n            # Create a tight bounding box from masks, useful when image is cropped\n            if self.crop_gen and instances.has(\"gt_masks\"):\n                instances.gt_boxes = instances.gt_masks.get_bounding_boxes()\n            dataset_dict[\"instances\"] = utils.filter_empty_instances(instances)\n\n        # USER: Remove if you don't do semantic/panoptic segmentation.\n        if \"sem_seg_file_name\" in dataset_dict:\n            with PathManager.open(dataset_dict.pop(\"sem_seg_file_name\"), \"rb\") as f:\n                sem_seg_gt = Image.open(f)\n                sem_seg_gt = np.asarray(sem_seg_gt, dtype=\"uint8\")\n            sem_seg_gt = transforms.apply_segmentation(sem_seg_gt)\n            sem_seg_gt = torch.as_tensor(sem_seg_gt.astype(\"long\"))\n            dataset_dict[\"sem_seg\"] = sem_seg_gt\n        return dataset_dict","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Preprocessing","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"for d in ['train', 'test']:\n    DatasetCatalog.register(f'wheat_{d}', lambda d=d: wheat_parallel(train_df if d=='train' else sub_df, \n                                                                     TRAIN_IMAGE_PATH if d=='train' else TEST_IMAGE_PATH,\n                                                                     True if d=='train' else False))\n    MetadataCatalog.get(f'wheat_{d}')\n    \nmicro_metadata = MetadataCatalog.get('wheat_train')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def visual_train(dataset, n_sampler=1):\n    for sample in random.sample(dataset, n_sampler):\n        img = cv2.imread(sample['file_name'])\n        v = Visualizer(img[:, :, ::-1], metadata=micro_metadata, scale=0.5)\n        v = v.draw_dataset_dict(sample)\n        plt.figure(figsize = (14, 10))\n        plt.imshow(cv2.cvtColor(v.get_image()[:, :, ::-1], cv2.COLOR_BGR2RGB))\n        plt.show()\n        \ntrain_dataset = wheat_parallel(train_df, TRAIN_IMAGE_PATH, True)        \nvisual_train(train_dataset)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Trainer","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def cfg_setup():\n    cfg = get_cfg()\n    cfg.merge_from_file(model_zoo.get_config_file(MODEL_PATH))\n    cfg.MODEL.WEIGHTS = WEIGHT_PATH\n    cfg.MODEL.RETINANET.NUM_CLASSES = 1\n    cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = 256\n\n    cfg.DATASETS.TRAIN = ('wheat_train',)\n    cfg.DATASETS.TEST = ()\n    cfg.DATALOADER.NUM_WORKERS = 4\n\n    cfg.SOLVER.IMS_PER_BATCH = 2\n    cfg.SOLVER.LR_SCHEDULER_NAME = 'WarmupCosineLR'\n    cfg.SOLVER.BASE_LS = 0.0002\n#     cfg.SOLVER.WARMUP_ITERS = 4500\n#     cfg.SOLVER.WARMUP_METHOD = \"linear\"\n    cfg.SOLVER.MAX_ITER = 10000\n    os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)\n    \n    return cfg\n\n\n\nclass WheatTrainer(DefaultTrainer):\n    \n    @classmethod\n    def build_train_loader(cls, cfg):\n        return build_detection_train_loader(cfg, mapper=DatasetMapper(cfg))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"%%time\n\ncfg = cfg_setup()\ntrainer = WheatTrainer(cfg)\ntrainer.resume_or_load(resume=False)\n\ngc.collect()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\ntrainer.train()\n\ngc.collect()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Load model","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def cfg_test():\n    cfg = get_cfg()\n    cfg.merge_from_file(model_zoo.get_config_file(MODEL_PATH))\n    cfg.MODEL.WEIGHTS = os.path.join(cfg.OUTPUT_DIR, 'model_final.pth')\n    cfg.DATASETS.TEST = ('wheat_test',)\n    cfg.MODEL.RETINANET.NUM_CLASSES = 1\n    cfg.MODEL.RETINANET.SCORE_THRESH_TEST = 0.45\n    \n    return cfg\n\ncfg = cfg_test()\npredict = DefaultPredictor(cfg)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\n\ndef visual_predict(dataset):\n    for sample in dataset:\n        img = cv2.imread(sample['file_name'])\n        output = predict(img)\n        \n        v = Visualizer(img[:, :, ::-1], metadata=micro_metadata, scale=0.5)\n        v = v.draw_instance_predictions(output['instances'].to('cpu'))\n        plt.figure(figsize = (14, 10))\n        plt.imshow(cv2.cvtColor(v.get_image()[:, :, ::-1], cv2.COLOR_BGR2RGB))\n        plt.show()\n\ntest_dataset = wheat_parallel(sub_df, TEST_IMAGE_PATH, False)\nvisual_predict(test_dataset)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def submit():\n    for idx, row in tqdm(sub_df.iterrows(), total=len(sub_df)):\n        img_path = os.path.join(TEST_IMAGE_PATH, row.image_id+'.jpg')\n        img = cv2.imread(img_path)\n        outputs = predict(img)['instances']\n        boxes = [i.cpu().detach().numpy() for i in outputs.pred_boxes]\n        scores = outputs.scores.cpu().detach().numpy()\n        list_str = []\n        for box, score in zip(boxes, scores):\n            box[3] -= box[1]\n            box[2] -= box[0]\n            box = list(map(int, box))\n            score = round(score, 4)\n            list_str.append(score) \n            list_str.extend(box)\n        sub_df.loc[idx, 'PredictionString'] = ' '.join(map(str, list_str))\n    \n    return sub_df\n\nsub_df = submit()    \nsub_df.to_csv('submission.csv', index=False)\nsub_df","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}