{"cells":[{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"import os\nimport math\nimport random\nimport torch\nimport torchvision\nimport torchvision.transforms as transforms\nimport torchvision.transforms.functional as F\nfrom sklearn.metrics import accuracy_score, balanced_accuracy_score, f1_score, precision_score, recall_score\nimport matplotlib.pyplot as plt\n\nfrom tqdm.autonotebook import tqdm\n\n\nMETRICS = {\n    'accuracy': {\n        'f': accuracy_score,\n        'args': {}\n    },\n    # 'balanced_accuracy': {\n    #     'f': balanced_accuracy_score,\n    #     'args': {}\n    # },\n    # 'f1': {\n    #     'f': f1_score,\n    #     'args': {'average': 'weighted'}\n    # },\n    # 'precision': {\n    #     'f': precision_score,\n    #     'args': {'average': 'weighted'}\n    # },\n    # 'recall': {\n    #     'f': recall_score,\n    #     'args': {'average': 'weighted'}\n    # }\n}\n\n\nNORM_MEAN = [0.485, 0.456, 0.406]\nNORM_STD = [0.229, 0.224, 0.225]\n\n\ndef make_image_label_grid(images, labels=None, class_names=None):\n    channels = images.shape[1]\n    if channels not in (3, 1):\n        raise ValueError(\"Images must have 1 or 3 channels\")\n    mean = NORM_MEAN if channels == 3 else [sum(NORM_MEAN) / 3]\n    std = NORM_STD if channels == 3 else [sum(NORM_STD) / 3]\n    mean = torch.tensor(mean)\n    std = torch.tensor(std)\n    mean = (-mean / std).tolist()\n    std = (1.0 / std).tolist()\n    img_grid = torchvision.utils.make_grid(images)\n    img_grid = F.normalize(img_grid, mean=mean, std=std)\n    return img_grid\n\n\ndef make_image_label_figure(images, labels=None, class_names=None):\n    channels = images.shape[1]\n    if channels not in (3, 1):\n        raise ValueError(\"Images must have 1 or 3 channels\")\n    mean = NORM_MEAN if channels == 3 else [sum(NORM_MEAN) / 3]\n    std = NORM_STD if channels == 3 else [sum(NORM_STD) / 3]\n    mean = torch.tensor(mean)\n    std = torch.tensor(std)\n    mean = (-mean / std).tolist()\n    std = (1.0 / std).tolist()\n    n = int(math.sqrt(len(images)))\n    figure = plt.figure(figsize=(n, n))\n    figure.subplots_adjust(hspace=0.4, wspace=0.4)\n    for i in range(n*n):\n        image, label = images[i], (0 if labels is None else labels[i])\n        image = F.normalize(image, mean=mean, std=std)\n        image = image.permute(1, 2, 0)\n        image = torch.squeeze(image)\n        image = (image * 255).int()\n        plt.subplot(n, n, i + 1, title='NA' if class_names is None else class_names[label])\n        plt.xticks([])\n        plt.yticks([])\n        plt.grid(False)\n        plt.imshow(image, cmap='gray' if channels == 1 else None)\n    return figure\n\n\nclass Transforms(transforms.Compose):\n\n    def __init__(self, in_channels=1, out_channels=1, size=(32, 32)):\n        if out_channels not in (3, 1) or in_channels not in (3, 1):\n            raise ValueError(\"Images must have 1 or 3 channels\")\n        mean = NORM_MEAN if out_channels == 3 else [sum(NORM_MEAN) / 3]\n        std = NORM_STD if out_channels == 3 else [sum(NORM_STD) / 3]\n        transforms_list = []\n        if in_channels != out_channels:\n            transforms_list.append(transforms.Grayscale(out_channels))\n        transforms_list.extend([\n            transforms.Resize(size),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=mean, std=std)])\n        super(Transforms, self).__init__(transforms_list)\n\n\nclass TrainTransforms(transforms.Compose):\n\n    def __init__(self, in_channels=1, out_channels=1, size=(32, 32), random_crop=False, random_affine=False,\n                 horizontal_flip=False, color_jitter=False, random_erasing=False):\n        if out_channels not in (3, 1) or in_channels not in (3, 1):\n            raise ValueError(\"Images must have 1 or 3 channels\")\n        mean = NORM_MEAN if out_channels == 3 else [sum(NORM_MEAN) / 3]\n        std = NORM_STD if out_channels == 3 else [sum(NORM_STD) / 3]\n        transforms_list = []\n        if in_channels != out_channels:\n            transforms_list.append(transforms.Grayscale(out_channels))\n        if random_affine:\n            transforms_list.append(transforms.RandomAffine(degrees=10.0, translate=(0.25, 0.25),\n                                                           shear=(-10, 10, -10, 10)))\n        if random_crop:\n            transforms_list.append(transforms.RandomResizedCrop(size, scale=(0.9, 1.1), ratio=(0.75, 1.33)))\n        else:\n            transforms_list.append(transforms.Resize(size))\n        if horizontal_flip:\n            transforms_list.append(transforms.RandomHorizontalFlip(p=0.5))\n        if color_jitter:\n            transforms_list.append(transforms.ColorJitter(brightness=(0.75, 1.5), contrast=(0.75, 1.5),\n                                                          saturation=(0.75, 1.5), hue=(-0.1, 0.1)))\n        transforms_list.append(transforms.ToTensor())\n        if random_erasing:\n            transforms_list.append(transforms.RandomErasing(p=0.2, scale=(0.02, 0.2), ratio=(0.3, 3.3), value=0))\n        transforms_list.append(transforms.Normalize(mean=mean, std=std))\n        super(TrainTransforms, self).__init__(transforms_list)\n\n\nclass TrainerProgressBar(tqdm):\n\n    def __init__(self, desc=None, total=10, unit='it', position=None):\n        super(TrainerProgressBar, self).__init__(\n            desc=desc, total=total, leave=True, unit=unit, position=position, dynamic_ncols=True\n        )\n\n    def reset(self, total=None, desc=None, ordered_dict=None):\n        # super(TrainerProgressBar, self).reset(total)\n        self.last_print_n = self.n = 0\n        self.last_print_t = self.start_t = self._time()\n        if total is not None:\n            self.total = total\n        super(TrainerProgressBar, self).refresh()\n        if desc is not None:\n            super(TrainerProgressBar, self).set_description(desc)\n        if ordered_dict is not None:\n            super(TrainerProgressBar, self).set_postfix(ordered_dict)\n\n    def update(self, desc=None, ordered_dict=None, n=1):\n        if desc is not None:\n            super(TrainerProgressBar, self).set_description(desc)\n        if ordered_dict is not None:\n            super(TrainerProgressBar, self).set_postfix(ordered_dict)\n        super(TrainerProgressBar, self).update(n)\n\n\nclass PyTorchTrainer(object):\n\n    def __init__(self, device=None, metrics=None, epoch_callback=None, batch_callback=None):\n        self.device = torch.device(device) if device else torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n        self.metrics = metrics or METRICS\n        self.epoch_callback = epoch_callback\n        self.batch_callback = batch_callback\n\n        self.train_pb = None\n        self.epoch_train_pb = None\n        self.epoch_val_pb = None\n\n    def train(self, model, optimizer, loss_criterion, train_data_loader, val_data_loader, scheduler=None,\n              epochs=10):\n        # Print log\n        print(f\"=============================== Training NN ===============================\")\n        print(f\"== Epochs:              {epochs:6d}\")\n        print(f\"== Train batch size:    {train_data_loader.batch_size:6d}\")\n        print(f\"== Train batches:       {len(train_data_loader):6d}\")\n        print(f\"== Validate batch size: {val_data_loader.batch_size:6d}\")\n        print(f\"== Validate batches:    {len(val_data_loader):6d}\")\n        print(f\"===========================================================================\")\n        # Initialize progress bars\n        self.train_pb = TrainerProgressBar(desc=f'== Epoch {1}', total=epochs, unit='epoch', position=0)\n        self.epoch_train_pb = TrainerProgressBar(desc=f'== Train {1}', total=len(train_data_loader),\n                                                 unit='batch', position=1)\n        self.epoch_val_pb = TrainerProgressBar(desc=f'== Val {1}', total=len(val_data_loader), unit='batch',\n                                               position=2)\n        # Reset progress bars\n        self.train_pb.reset(total=epochs)\n        self.epoch_train_pb.reset(total=len(train_data_loader))\n        self.epoch_val_pb.reset(total=len(val_data_loader))\n        for epoch in range(epochs):\n            # Train batches\n            train_loss, predictions, targets, inputs = self.forward_batches(model, optimizer, loss_criterion,\n                                                                            train_data_loader, epoch, train=True)\n            # Val batches\n            val_loss, predictions, targets, inputs = self.forward_batches(model, optimizer, loss_criterion,\n                                                                          val_data_loader, epoch, train=False)\n            # Make scheduler step\n            if scheduler:\n                if isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):\n                    scheduler.step(train_loss, epoch=epoch)\n                else:\n                    scheduler.step(epoch=epoch)\n            # Update progress bar\n            metrics_dict = {'lr': optimizer.param_groups[0]['lr'],\n                            'loss': {'train': round(train_loss, 4), 'val': round(val_loss, 4)}}\n            metrics_dict.update(self.metrics_dict(predictions, targets))\n            if self.epoch_callback:\n                self.epoch_callback(epoch, epochs, predictions, targets, inputs, metrics_dict)\n            self.train_pb.update(desc=f'== Epoch {epoch+1}', ordered_dict=metrics_dict)\n        # Close progress bars\n        self.train_pb.close()\n        self.epoch_train_pb.close()\n        self.epoch_val_pb.close()\n        # Print log\n        print(f\"===========================================================================\")\n\n    def forward_batches(self, model, optimizer, loss_criterion, data_loader, epoch, train=True):\n        # Set model train or eval due to current phase\n        if train:\n            model.train()\n        else:\n            model.eval()\n        # Preset variables\n        avg_loss_value = 0\n        all_predictions = None\n        all_targets = None\n        all_inputs = None\n        batches = len(data_loader)\n        # Reset progress bar\n        if train:\n            self.epoch_train_pb.reset(batches, f\"== Train {epoch+1}\")\n        else:\n            self.epoch_val_pb.reset(batches, f\"== Val {epoch+1}\")\n        for batch_i, data in enumerate(data_loader, 1):\n            # Forward batch\n            loss_value, predictions, targets, inputs = self.forward_batch(model, optimizer, loss_criterion, data,\n                                                                          train=train)\n            # Update variables\n            avg_loss_value += loss_value\n            all_predictions = predictions if all_predictions is None else torch.cat((all_predictions, predictions))\n            all_targets = targets if all_targets is None else torch.cat((all_targets, targets))\n            all_inputs = inputs if all_inputs is None else torch.cat((all_inputs, inputs))\n            # Update progress bar\n            metrics_dict = {'lr': optimizer.param_groups[0]['lr'], 'loss': avg_loss_value/batch_i}\n            metrics_dict.update(self.metrics_dict(all_predictions, all_targets))\n            if self.batch_callback:\n                self.batch_callback(train, epoch, batch_i, batches, predictions, targets, inputs, metrics_dict)\n            if train:\n                self.epoch_train_pb.update(ordered_dict=metrics_dict)\n            else:\n                self.epoch_val_pb.update(ordered_dict=metrics_dict)\n        # Update variables\n        avg_loss_value /= batches\n        # Return\n        return avg_loss_value, all_predictions, all_targets, all_inputs\n\n    def forward_batch(self, model, optimizer, loss_criterion, batch_data, train=True):\n        # Get Inputs and Targets and put them to device\n        inputs, targets = batch_data\n        inputs = inputs.to(self.device)\n        targets = targets.to(self.device)\n        with torch.set_grad_enabled(train):\n            # Forward model to get outputs\n            outputs = model.forward(inputs)\n            # Calculate Loss Criterion\n            loss = loss_criterion(outputs, targets)\n        if train:\n            # Zero optimizer gradients\n            optimizer.zero_grad()\n            # Calculate new gradients\n            loss.backward()\n            # Make optimizer step\n            optimizer.step()\n        # Variables\n        loss_value = loss.item()\n        predictions = outputs.argmax(dim=1).data.cpu()\n        targets = targets.data.cpu()\n        inputs = inputs.data.cpu()\n        return loss_value, predictions, targets, inputs\n\n    def metrics_dict(self, predictions, targets):\n        d = {}\n        for metric_name in self.metrics:\n            metric_value = self.metrics[metric_name]['f'](predictions, targets, **self.metrics[metric_name]['args'])\n            d[metric_name] = metric_value\n\n        return d","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import os\nimport datetime\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.optim.lr_scheduler as lr_scheduler\nimport torchvision.transforms as transforms\nfrom PIL import Image","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"CLASS_NAMES = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')\ndata_root = '../input/Kannada-MNIST'\ntrain_file_name = 'train.csv'\nval_file_name = 'Dig-MNIST.csv'\ntest_file_name = 'test.csv'\nNORM_MEAN = [0.485, 0.456, 0.406]\nNORM_STD = [0.229, 0.224, 0.225]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Transforms\nclass KannadaMNISTTransforms(transforms.Compose):\n\n    def __init__(self, in_channels=1, out_channels=1, size=(28, 28)):\n        if out_channels not in (3, 1) or in_channels not in (3, 1):\n            raise ValueError(\"Images must have 1 or 3 channels\")\n        mean = NORM_MEAN if out_channels == 3 else [sum(NORM_MEAN) / 3]\n        std = NORM_STD if out_channels == 3 else [sum(NORM_STD) / 3]\n        transforms_list = []\n        if in_channels != out_channels:\n            transforms_list.append(transforms.Grayscale(out_channels))\n        transforms_list.extend([\n            transforms.RandomAffine(10.0, translate=(0.25, 0.25), shear=(-10, 10, -10, 10)),\n            transforms.Resize(size),\n            transforms.ToTensor(),\n            transforms.RandomErasing(p=0.2, scale=(0.05, 0.1), ratio=(0.3, 3.3), value=0), \n            transforms.Normalize(mean=mean, std=std)\n        ])\n        super(KannadaMNISTTransforms, self).__init__(transforms_list)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Dataset\nclass KannadaMNISTDataset(torch.utils.data.Dataset):\n    \n    def __init__(self, images, targets=None, transform=None):\n        super(KannadaMNISTDataset, self).__init__()\n        self.images = [Image.fromarray(image) for image in images]\n        self.targets = np.zeros(len(images)) if targets is None else targets.astype(int)\n        self.transform = transform\n    \n    def __len__(self):\n        return len(self.images)\n        \n    def __getitem__(self, index):\n        return self.images[index] if self.transform is None else self.transform(self.images[index]), torch.tensor(self.targets[index], dtype=torch.long)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Read *.csv files to Pandas Dataframes\ntrain_df = pd.read_csv(os.path.join(data_root, train_file_name))\nval_df = pd.read_csv(os.path.join(data_root, val_file_name))\ntest_df = pd.read_csv(os.path.join(data_root, test_file_name))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Construct from DataFrames train/val/test datasets\ntrain_targets = train_df.label.values.astype(int)\ntrain_images = (train_df.drop('label', axis=1).values.astype(float) / 255).reshape(-1, 28, 28)\nval_targets = val_df.label.values.astype(int)\nval_images = (val_df.drop('label', axis=1).values.astype(float) / 255).reshape(-1, 28, 28)\ntest_ids = test_df.id.values.astype(int)\ntest_images = (test_df.drop('id', axis=1).values.astype(float) / 255).reshape(-1, 28, 28)\nprint(\"Origin trainset images/targets shapes\", train_images.shape, train_targets.shape)\nprint(\"Origin valset images/targets shapes\", val_images.shape, val_targets.shape)\nprint(\"Origin testset images/ids shape\", test_images.shape, test_ids.shape)\nprint()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Train/val/test dataloaders and datasets\n# Batch size\nbatch_size = 1000\nsize = (28, 28)\norigin_channels = 1\nin_channels = 1\n# Datasets\ntrain_dataset = KannadaMNISTDataset(train_images, train_targets, transform=KannadaMNISTTransforms(in_channels=origin_channels, out_channels=in_channels, size=size))\nval_dataset = KannadaMNISTDataset(val_images, val_targets, transform=Transforms(in_channels=origin_channels, out_channels=in_channels, size=size))\ntest_dataset = KannadaMNISTDataset(test_images, transform=Transforms(in_channels=origin_channels, out_channels=in_channels, size=size))\n# Dataloaders\ntrain_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)\nval_dataloader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4)\ntest_dataloader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=4)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Show train batch\nimages, targets = next(iter(train_dataloader))\nfig = make_image_label_figure(images[:9], targets[:9], CLASS_NAMES)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Show val batch\nimages, targets = next(iter(val_dataloader))\nfig = make_image_label_figure(images[:9], targets[:9], CLASS_NAMES)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Show test batch\nimages = next(iter(test_dataloader))\nfig = make_image_label_figure(images[0][:9])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class Conv2dBNReLU(nn.Sequential):\n    '''Convolution2d + BatchNormalization2d + ReLU Activation'''\n    def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, groups=1, bias=True):\n        super(Conv2dBNReLU, self).__init__(\n            nn.Conv2d(in_channels, out_channels,\n                      kernel_size=kernel_size, stride=stride, padding=padding, groups=groups, bias=bias),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True)\n        )\n\n\nclass DOLinearBNReLU(nn.Sequential):\n    '''Droupout + Linear'''\n    def __init__(self, in_features, out_features, bias=True):\n        super(DOLinearBNReLU, self).__init__(\n            nn.Dropout(0.2),\n            nn.Linear(in_features, out_features, bias=bias),\n            nn.BatchNorm1d(out_features),\n            nn.ReLU(inplace=True)\n        )\n\n\nclass DOLinear(nn.Sequential):\n    '''Droupout + Linear'''\n    def __init__(self, in_features, out_features, bias=True):\n        super(DOLinear, self).__init__(\n            nn.Dropout(0.2),\n            nn.Linear(in_features, out_features, bias=bias)\n        )\n\n\nclass KannadaMNISTNet(nn.Module):\n\n    def __init__(self, in_channels=1, classes=10):\n        super(KannadaMNISTNet, self).__init__()\n\n        self.feature_extractor = nn.Sequential(\n            Conv2dBNReLU(in_channels, 64, kernel_size=3, padding=1),\n            \n            Conv2dBNReLU(64, 64, kernel_size=3, padding=1),\n            Conv2dBNReLU(64, 64, kernel_size=3, padding=1),\n\n            nn.MaxPool2d(2),\n\n            Conv2dBNReLU(64, 128, kernel_size=3, padding=1),\n            Conv2dBNReLU(128, 128, kernel_size=3, padding=1),\n            Conv2dBNReLU(128, 128, kernel_size=3, padding=1),\n\n            nn.MaxPool2d(2),\n\n            Conv2dBNReLU(128, 256, kernel_size=3, padding=1),\n            Conv2dBNReLU(256, 256, kernel_size=3, padding=1),\n\n            Conv2dBNReLU(256, 512, kernel_size=3, padding=1),\n            Conv2dBNReLU(512, 512, kernel_size=3, padding=1),\n        )\n        self.pool = nn.AdaptiveAvgPool2d((7, 7))\n        self.classifier = nn.Sequential(\n            DOLinearBNReLU(7 * 7 * 512, 512),\n            DOLinear(512, classes)\n        )\n        self.sm = nn.Softmax()\n\n    def forward(self, x):\n        x = self.feature_extractor(x)\n        x = self.pool(x)\n        x = x.view(-1, 7 * 7 * 512)\n        x = self.classifier(x)\n        return x\n\n    def predict(self, x):\n        x = self.forward(x)\n        x = self.sm(x)\n        return x","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Train Net\ndevice = 'cuda:0' if torch.cuda.is_available() else 'cpu'\n\nnet = KannadaMNISTNet(in_channels=in_channels, classes=10)\nnet = net.to(torch.device(device))\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(net.parameters(), lr=0.002, weight_decay=0.00005)\nscheduler = lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.25, patience=2, verbose=False, mode='min', threshold=0.0001, cooldown=0, min_lr=0.000001)\n\ntrainer = PyTorchTrainer(device=device)\ntrainer.train(net, optimizer, criterion, train_dataloader, val_dataloader, scheduler=scheduler, epochs=110)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Trigger net to eval mode\nnet.eval()\nfor param in net.parameters():\n    param.requires_grad = False","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Check some validation dataset predictions\nimages, targets = next(iter(val_dataloader))\n_images = images.to(torch.device(device))\n_output = net.forward(_images)\npredictions = _output.argmax(dim=1).data.cpu()\n\nfig = make_image_label_figure(images[:9], predictions[:9], CLASS_NAMES)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Check some test dataset predictions\nimages = next(iter(test_dataloader))[0]\n_images = images.to(torch.device(device))\n_output = net.forward(_images)\npredictions = _output.argmax(dim=1).data.cpu()\n\nfig = make_image_label_figure(images[:9], predictions[:9], CLASS_NAMES)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Get all test predictions\ntest_predictions = None\nfor batch in test_dataloader:\n    inputs = batch[0]\n    inputs = inputs.to(torch.device(device))\n    output = net.forward(inputs)\n    predictions = output.argmax(dim=1).data\n    test_predictions = predictions if test_predictions is None else torch.cat((test_predictions, predictions))\ntest_predictions = test_predictions.cpu().numpy()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Write submission to pd.DataFrame\nsubmission_df = pd.DataFrame(np.c_[test_ids[:,None], test_predictions], columns=['id', 'label'])\nsubmission_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Write submission DataFrame to csv\nsubmission_df.to_csv('submission.csv', index=False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","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":1}