{"cells":[{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfile_list = []\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        file_list.append(os.path.join(dirname, filename))\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import re\ndef photo_jpg(x): return bool(re.search('photo_jpg',x))\ndef monet_jpg(x): return bool(re.search('monet_jpg',x))\n\nphoto_path_list = list(filter(photo_jpg, file_list))\nmonet_path_list = list(filter(monet_jpg, file_list))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torchtext import data, datasets\nimport random\nimport matplotlib.pyplot as plt\nimport itertools\nfrom torchvision import transforms","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"#Make Dataset"},{"metadata":{"trusted":true},"cell_type":"code","source":"from PIL import Image\n\nclass GANDataset(torch.utils.data.Dataset):\n    def __init__(self, photo_path_list, monet_path_list, transform):\n        self.photo_path_list = photo_path_list\n        self.monet_path_list = monet_path_list\n        self.transform = transform\n        self.path = {'Photo': photo_path_list, 'Monet': monet_path_list}\n\n    def __len__(self):\n        if (len(self.path['Photo']) < len(self.path['Monet'])):\n            return len(self.path['Photo'])\n        else:\n            return len(self.path['Monet'])\n\n    def __getitem__(self, idx):\n        imgP_path = os.path.join(self.path['Photo'][idx])\n        imgM_path = os.path.join(self.path['Monet'][idx])\n        imgP = Image.open(imgP_path)\n        imgM = Image.open(imgM_path)\n        imgP_transformed = self.transform(imgP)\n        imgM_transformed = self.transform(imgM)\n        data = {'Photo': imgP_transformed, 'Monet': imgM_transformed}\n        return data\n\n\nclass ImgTransform(object):\n    def __init__(self, mean, std):\n        self.data_transform = transforms.Compose([\n            transforms.ToTensor(),\n            transforms.Normalize((mean, mean, mean), (std, std, std))\n        ])\n    \n    def __call__(self, img):\n        return self.data_transform(img)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.imshow(GANDataset(photo_path_list, monet_path_list, ImgTransform(0.5, 0.5))[15]['Monet'].permute(1, 2, 0)) ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Define models ###\nreference : https://github.com/davidADSP/GDL_code/blob/master/models/cycleGAN.py"},{"metadata":{},"cell_type":"markdown","source":"# Define Resnet Generator model"},{"metadata":{"trusted":true},"cell_type":"code","source":"class ResidualBlock(nn.Module):\n    def __init__(self, num_filter, kernel_size=3, stride=1, padding=1):\n        super().__init__()\n        self.residual = nn.Sequential(\n            nn.Conv2d(num_filter, num_filter, kernel_size=kernel_size,\n                             stride=stride, padding=padding, padding_mode='reflect', bias=True),\n            nn.InstanceNorm2d(num_filter),\n            nn.ReLU(),\n            nn.Conv2d(num_filter, num_filter, kernel_size=kernel_size,\n                             stride=stride, padding=padding, padding_mode='reflect', bias=True),\n            nn.InstanceNorm2d(num_filter)\n        )\n\n    def forward(self, x):\n        return x + self.residual(x)\n    \nclass ConvBlock(torch.nn.Module):\n    def __init__(self,in_dim, out_dim, kernel_size=3, stride=2, padding=1, activation='relu', batch_norm=True):\n        super(ConvBlock,self).__init__()\n        self.conv = torch.nn.Conv2d(in_dim, out_dim, kernel_size, stride, padding)\n        self.batch_norm = batch_norm\n        self.bn = torch.nn.InstanceNorm2d(out_dim)\n        self.activation = activation\n        self.relu = torch.nn.ReLU(True)\n        self.tanh = torch.nn.Tanh()\n    def forward(self,x):\n        x = self.conv(x)\n        if self.batch_norm:\n            x = self.bn(x)\n        if self.activation == 'relu':\n            return self.relu(x)\n        elif self.activation == 'tanh':\n            return self.tanh(x)\n            \nclass DeconvBlock(torch.nn.Module):\n    def __init__(self,in_dim,out_dim,kernel_size=3,stride=2,padding=1,output_padding=1,activation='relu',batch_norm=True):\n        super(DeconvBlock,self).__init__()\n        self.deconv = torch.nn.ConvTranspose2d(in_dim,out_dim,kernel_size,stride,padding,output_padding)\n        self.batch_norm = batch_norm\n        self.bn = torch.nn.InstanceNorm2d(out_dim)\n        self.activation = activation\n        self.relu = torch.nn.ReLU(True)\n    def forward(self,x):\n        x = self.deconv(x)\n        if self.batch_norm:\n            out = self.bn(x)\n        if self.activation == 'relu':\n            return self.relu(out)\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class Resnet_Generator(nn.Module):\n    def __init__(self, input_dim=3, output_dim=3, num_filter=64, num_resnet=9):\n        super(Resnet_Generator, self).__init__()\n        self.pad = torch.nn.ReflectionPad2d(3)\n        self.conv1 = ConvBlock(input_dim, num_filter, kernel_size=7, stride=1, padding=0)\n        self.conv2 = ConvBlock(num_filter, num_filter * 2)\n        self.conv3 = ConvBlock(num_filter * 2, num_filter * 4)\n        self.resnet_blocks = []\n        for i in range(num_resnet):\n            self.resnet_blocks.append(ResidualBlock(4 * num_filter))\n        self.resnet_blocks = torch.nn.Sequential(*self.resnet_blocks)\n        \n        self.deconv1 = DeconvBlock(num_filter * 4, num_filter * 2)\n        self.deconv2 = DeconvBlock(num_filter * 2, num_filter)\n        self.deconv3 = ConvBlock(num_filter, output_dim,\n                                 kernel_size=7, stride=1, padding=0, activation='tanh', batch_norm=False)\n\n    def forward(self, x):\n        x = self.conv1(self.pad(x))\n        x = self.conv2(x)\n        x = self.conv3(x)\n        \n        x = self.resnet_blocks(x)\n        \n        x = self.deconv1(x)\n        x = self.deconv2(x)\n        x = self.deconv3(self.pad(x))\n        return x","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Define U-net Generator model"},{"metadata":{"trusted":true},"cell_type":"code","source":"#downsample\ndef downsample(in_dim, out_dim, act_fn, f_size=4):\n    model = nn.Sequential(\n        nn.Conv2d(in_dim, out_dim, kernel_size=f_size, stride=2, padding=1),\n        nn.InstanceNorm2d(out_dim),\n        act_fn,\n    )\n    return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#upsample without concatenate\ndef upsample(in_dim, out_dim, act_fn, f_size=4):\n  model = nn.Sequential(\n      nn.ConvTranspose2d(in_dim, out_dim, kernel_size=f_size, stride=2, padding=1),\n      nn.InstanceNorm2d(out_dim),\n      act_fn\n  )\n  return model\n    ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class Unet_Generator(nn.Module):\n    def __init__(self, in_dim=3, out_dim=3, num_filter=64):\n        super(Unet_Generator, self).__init__()\n        self.in_dim = in_dim\n        self.out_dim = out_dim\n        self.num_filter = num_filter\n        act_fn = nn.ReLU()\n        \n        self.down_1 = downsample(in_dim, num_filter*1, act_fn)\n        self.down_2 = downsample(num_filter*1, num_filter*2, act_fn)\n        self.down_3 = downsample(num_filter*2, num_filter*4, act_fn)\n        self.down_4 = downsample(num_filter*4, num_filter*8, act_fn)\n        \n        self.up_1 = upsample(num_filter*8, num_filter*4, act_fn)\n        self.up_2 = upsample(num_filter*8, num_filter*2, act_fn)\n        self.up_3 = upsample(num_filter*4, num_filter*1, act_fn)\n        self.up_4 = nn.Upsample(scale_factor=2, mode='nearest')\n        \n        self.out = nn.Sequential(\n            nn.Conv2d(num_filter*2, out_dim, kernel_size=3,stride=1,padding=1),\n            nn.Tanh(),\n        )\n\n    def forward(self, input):\n        down_1 = self.down_1(input)\n        down_2 = self.down_2(down_1)\n        down_3 = self.down_3(down_2)\n        down_4 = self.down_4(down_3)        \n        \n        up_1 = self.up_1(down_4)\n        concat_1 = torch.cat([up_1, down_3], dim=1)\n        up_2 = self.up_2(concat_1)\n        concat_2 = torch.cat([up_2, down_2], dim=1)\n        up_3 = self.up_3(concat_2)\n        concat_3 = torch.cat([up_3, down_1], dim=1)\n        up_4 = self.up_4(concat_3)\n        out = self.out(up_4)\n\n        return out","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Define Discriminator"},{"metadata":{"trusted":true},"cell_type":"code","source":"def conv4(in_dim, num_filter, stride, act_fn, norm=True):\n    kernel_size = 3 + stride - 1\n    if norm:\n      model = nn.Sequential(\n          nn.Conv2d(in_dim, num_filter, kernel_size=4, stride=stride, padding=1),\n          nn.InstanceNorm2d(num_filter),\n          act_fn\n      )\n    else:\n      model = nn.Sequential(\n        nn.Conv2d(in_dim, num_filter, kernel_size=4, stride=stride, padding=1),\n        act_fn\n      )\n    return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# define discriminator\n\nclass Discriminator(nn.Module):\n    def __init__(self, in_dim=3, num_filter=64):\n        super(Discriminator, self).__init__()\n        self.in_dim = in_dim\n        self.num_filter = num_filter\n        act_fn = nn.LeakyReLU(0.2)\n        self.layer_1 = conv4(in_dim, num_filter, 2, act_fn, norm=False)\n        self.layer_2 = conv4(num_filter, num_filter*2, 2, act_fn)\n        self.layer_3 = conv4(num_filter*2, num_filter*4, 2, act_fn)\n        self.layer_4 = conv4(num_filter*4, num_filter*8, 1, act_fn)\n\n        self.out = nn.Conv2d(num_filter*8, 1, kernel_size=4, stride=1, padding=0)\n    \n    def forward(self, input):\n        y = self.layer_1(input)\n        y = self.layer_2(y)\n        y = self.layer_3(y)\n        y = self.layer_4(y)\n        out = self.out(y)\n        return out","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_dataset = GANDataset(\n    photo_path_list=photo_path_list, monet_path_list=monet_path_list, \n    transform = ImgTransform(mean=0.5, std=0.5)\n)\n\nbatch_size = 1\nepochs = 130\ntrain_dataloader = torch.utils.data.DataLoader(train_dataset, \n                                               batch_size=batch_size, shuffle=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#reference: https://medium.com/humanscape-tech/ml-practice-cyclegan-f9153ef72297\n\n# Define accelerator\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# Define Network\n#netG = Unet_Generator().to(device)\n#netF = Unet_Generator().to(device)\nnetG = Resnet_Generator().to(device)\nnetF = Resnet_Generator().to(device)\nnetDP = Discriminator().to(device)\nnetDM = Discriminator().to(device)\n\n#initializing network\ndef init_weights(m):\n    classname = m.__class__.__name__\n    if classname.find('Conv') != -1:\n        #initializing Con2d, ConvTranspose2d\n        nn.init.normal_(m.weight.data, 0.0, 0.02)\n        nn.init.constant_(m.bias.data, 0)\n    \n    elif classname.find('Instance') != -1:\n        #initializing InstanceNorm\n        nn.init.normal_(m.weight.data, 1.0, 0.02)\n        nn.init.constant_(m.bias.data, 0)\n\n\n# initialize network\ninit_weights(netG)\ninit_weights(netF)\ninit_weights(netDP)\ninit_weights(netDM)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class LambdaLR():\n    def __init__(self, n_epochs, offset, decay_start_epoch):\n        assert ((n_epochs - decay_start_epoch) > 0), \"Decay must start before the training session ends!\"\n        self.n_epochs = n_epochs\n        self.offset = offset\n        self.decay_start_epoch = decay_start_epoch\n\n    def step(self, epoch):\n        return 1.0 - max(0, epoch + self.offset - self.decay_start_epoch)/(self.n_epochs - self.decay_start_epoch)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# define loss function\nganLoss = nn.MSELoss().to(device)\ncycleConsistencyLoss = nn.L1Loss().to(device)\nidentityLoss = nn.L1Loss().to(device)\n\n# set up optimizer\ninitial_learning_rate = 2e-4\n\noptimG = torch.optim.Adam(itertools.chain(netG.parameters(), netF.parameters()), lr=initial_learning_rate, betas=(0.5, 0.999))\noptimDP = torch.optim.Adam(netDP.parameters(), lr=initial_learning_rate, betas=(0.5, 0.999))\noptimDM = torch.optim.Adam(netDM.parameters(), lr=initial_learning_rate, betas=(0.5, 0.999))\n\n# set up scheduler\nschedulerG = torch.optim.lr_scheduler.LambdaLR(optimizer=optimG,\n                                lr_lambda=LambdaLR(epochs, 0, 100).step)\nschedulerDP = torch.optim.lr_scheduler.LambdaLR(optimizer=optimDP,\n                                lr_lambda=LambdaLR(epochs, 0, 100).step)\nschedulerDM = torch.optim.lr_scheduler.LambdaLR(optimizer=optimDM,\n                                lr_lambda=LambdaLR(epochs, 0, 100).step)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"denormalize = transforms.Normalize(\n    mean=(-1 * 0.5/0.5, -1 * 0.5/0.5, -1 * 0.5/0.5), \n    std=(1/0.5, 1/0.5, 1/0.5)\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import time\nimport datetime\nimport matplotlib.pyplot as plt\nfrom IPython.display import clear_output\n\nepoch_lossDM = []\nepoch_lossDP = []\nepoch_lossGen = []\nepoch_ = []\nsave_path_list = []\nstart=time.time()\nfor epoch in range(1, epochs + 1):\n    # train start\n    netG.train()\n    netF.train()\n    netDP.train()\n    netDM.train()\n\n    # loss array declaration\n    lossFakeDP = []\n    lossRealDP = []\n    lossFakeDM = []\n    lossRealDM = []\n\n    lossGanG = []\n    lossGanF = []\n    lossPositiveCycle = []\n    lossNegativeCycle = []\n    lossIdentityG = []\n    lossIdentityF = []\n\n    for batch, data in enumerate(train_dataloader, 1):\n        # forward path\n        realP = data['Photo'].to(device)\n        realM = data['Monet'].to(device)\n\n        # Validity : Whether the image created by each Generator tricks the discriminator\n        fakeM = netG(realP)\n        fakeP = netF(realM)\n        \n        # loss by discrimiator\n        discFakeP = netDP(fakeP)\n        discFakeM = netDM(fakeM)\n        \n        lossG = ganLoss(discFakeM, torch.ones_like(discFakeM))\n        lossF = ganLoss(discFakeP, torch.ones_like(discFakeP))\n        \n        \n        # Reconstruction : Cycle reconstruction for original image\n        reconP = netF(fakeM)\n        reconM = netG(fakeP)\n        \n        lossPositiveCycle = cycleConsistencyLoss(reconP, realP)\n        lossNegativeCycle = cycleConsistencyLoss(reconM, realM)\n\n        # Identity : Whether each Generator maintain their target domain image's identity\n        identityM = netG(realM)\n        identityP = netF(realP)\n        \n        lossIdentityG = identityLoss(identityM, realM)\n        lossIdentityF = identityLoss(identityP, realP)\n        \n        # Generator's total loss\n        lossGen = (lossG + lossF) + (lossPositiveCycle + lossNegativeCycle) + (lossIdentityG + lossIdentityF)\n\n        optimG.zero_grad()\n        lossGen.backward()\n        optimG.step()\n        \n        ######## Discriminator for Photo ########\n        discFakeP = netDP(fakeP.detach())\n        discRealP = netDP(realP)\n\n        # torch.zeros_like() -> fake label, torch.ones_like() -> real label\n        lossDFakeP = ganLoss(discFakeP, torch.zeros_like(discFakeP))\n        lossDRealP = ganLoss(discRealP, torch.ones_like(discRealP))\n\n        lossDP = (lossDFakeP + lossDRealP) * 0.5\n        optimDP.zero_grad()\n        lossDP.backward()\n        optimDP.step()\n\n        \n        ######## Discriminator for Monet ########\n        discFakeM = netDM(fakeM.detach())\n        discRealM = netDM(realM)\n        \n        lossDFakeM = ganLoss(discFakeM, torch.zeros_like(discFakeM))\n        lossDRealM = ganLoss(discRealM, torch.ones_like(discRealM))\n\n        lossDM = (lossDFakeM + lossDRealM) * 0.5\n        optimDM.zero_grad()\n        lossDM.backward()\n        optimDM.step()\n    schedulerG.step()\n    schedulerDP.step() \n    schedulerDM.step()\n\n    #Saving models\n    if epoch % 10 == 0:\n        if not os.path.exists('cycleGANmodel'):\n            os.makedirs('cycleGANmodel')\n        save_path = 'cycleGANmodel/Photo2Monet_generator_{0}.pt'.format(epoch)\n        save_path_list.append(save_path)\n        torch.save(netG.state_dict(), save_path)\n    epoch_lossGen.append(lossGen)\n    epoch_lossDP.append(lossDP)\n    epoch_lossDM.append(lossDM)\n    epoch_.append(epoch)\n    clear_output(wait = True)\n    print(\"epoch : {0}  lossGen : {1} lossDP : {2} lossDM : {3}\".format(epoch, lossGen, lossDP, lossDM))\n\n    \nfig = plt.figure(figsize=(8,8))\nfig.set_facecolor('white')\nax = fig.add_subplot()\n \nax.plot(epoch_,epoch_lossGen, label='lossGen')\nax.plot(epoch_,epoch_lossDP, label='lossDP') \nax.plot(epoch_,epoch_lossDM, label='lossDM') \nax.legend()\nax.set_xlabel('epoch')\nax.set_ylabel('loss')\n\nplt.show()\n\nend = time.time()-start\ntimes = str(datetime.timedelta(seconds=end)).split(\".\")\nprint('Finished in {0}'.format(times[0]))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Plot 5 samples of each model's fake_photo result"},{"metadata":{"trusted":true},"cell_type":"code","source":"from torchvision.utils import save_image\nimport os\nimport zipfile\n#model_path = 'cycleGANmodel'\n\nrandom_sample = random.sample(photo_path_list, 5)\nnum_sample = 10\n\nfor file in save_path_list:\n    print(file)\n    model_idx = file\n    netG.load_state_dict(torch.load('./{0}'.format(file)))\n    #os.makedirs('{0}'.format(model_idx[:-3]), exist_ok=True)\n    \n    fakeM_ReverseNormed_list = []\n    \n    for photo_path in random_sample:\n      img_path = os.path.join(photo_path)\n      imgP = Image.open(img_path)\n        \n      transform = ImgTransform(mean=0.5, std=0.5)\n      imgP_transformed = transform(imgP).unsqueeze(dim=0)\n      imgP_transformed = imgP_transformed.to(device)\n    \n      fakeM = netG(imgP_transformed)\n      fakeM = fakeM.to('cpu')\n      fakeM_ReverseNormed = denormalize(fakeM)\n      fakeM_ReverseNormed = np.transpose(fakeM_ReverseNormed.detach().numpy().squeeze(), (1,2,0))\n      fakeM_ReverseNormed_list.append(fakeM_ReverseNormed)\n    \n    plt.figure(figsize=(50,50))\n    for num, img in enumerate(fakeM_ReverseNormed_list):\n      plt.subplot(1, 5, num+1)\n      plt.axis('off'), plt.xticks([]), plt.yticks([])\n      plt.imshow(img)\n\n    plt.tight_layout()\n    plt.subplots_adjust(left = 0, bottom = 0, right = 1, top = 1, hspace = 0, wspace = 0)\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Save fake images as [model_name]/images.zip"},{"metadata":{"trusted":true},"cell_type":"code","source":"from torchvision.utils import save_image\nimport os\nimport zipfile\nimport shutil\nimport time\nimport datetime\nimport glob\nmodel_path = 'cycleGANmodel'\nstart = time.time()\n\nlast_path = save_path_list[-1]\nnetG.load_state_dict(torch.load('./{0}'.format(last_path)))\nnetG = netG.to(device)\nos.makedirs('{0}'.format(last_path[:-3]), exist_ok=True)\ncnt = 0\nfor photo_path in photo_path_list:\n    cnt += 1\n    img_path = os.path.join(photo_path)\n    imgP = Image.open(img_path)\n    transform = ImgTransform(mean=0.5, std=0.5)\n    imgP_transformed = transform(imgP).unsqueeze(dim=0)\n    imgP_transformed = imgP_transformed.to(device)\n    fakeM = netG(imgP_transformed)\n    fakeM = fakeM.to('cpu')\n    fakeM_ReverseNormed = denormalize(fakeM)\n    fakeM_ReverseNormed = fakeM_ReverseNormed.detach().squeeze()\n    filename = re.findall(r\"[0-9a-zA-Z]*[.]jpg\", photo_path)[0]\n    save_image(fakeM_ReverseNormed, '{0}/{1}'.format(last_path[:-3], filename))\n    print('Saved transformed images by last model to {0}'.format(last_path[:-3]))\n    print('{0}/{1} completed'.format(cnt, len(photo_path_list)))\n    clear_output(wait = True)\n    \n\n\n\nzip_file = zipfile.ZipFile('{0}.zip'.format('images'), \"w\")\n\nfor file in os.listdir(last_path[:-3]):\n    if file.endswith('.jpg'):\n        jpg_file_path = os.path.join(last_path[:-3], file)\n        zip_file.write(jpg_file_path, os.path.basename(jpg_file_path), compress_type=zipfile.ZIP_DEFLATED)\nzip_file.close()\n\n[os.remove(f) for f in glob.glob(\"./{0}/*.jpg\".format(last_path[:-3]))]\n\nend = time.time() - start\ntimes = str(datetime.timedelta(seconds=end)).split(\".\")\nprint('Finished in {0}'.format(times[0]))\n","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":4}