{"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_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install staintools\n!pip install spams\n!pip install colorsys\n!pip install segmentation_models_pytorch\n!pip install import_ipynb","metadata":{"id":"kcxD_J2N9Yn2","outputId":"d9045fac-56ed-4b77-c62c-01349ed0d715","collapsed":true,"jupyter":{"outputs_hidden":true}},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## import packages","metadata":{"id":"O_ypRgKh9zEw"}},{"cell_type":"code","source":"\"\"\"## import packages\"\"\"\n\nimport os\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport glob\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport cv2\nimport tifffile as tiff \nfrom tqdm.auto import tqdm\nimport copy\n\nimport torch\nimport torch.nn as nn\nimport torch.utils.data as td\nimport torchvision as tv\nfrom PIL import Image, ImageOps\nimport torch.optim as optim\nfrom torch.optim import lr_scheduler\nfrom torch.nn import functional as F\n\n\nimport time\n\nimport segmentation_models_pytorch as smp\n\nglobal device\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\nprint(device)","metadata":{"id":"Vn6yoDBa9Yqq","outputId":"7783427d-bde1-44bb-fd94-77f413e8ee54"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## rle code","metadata":{"id":"_35gmA7H-70_"}},{"cell_type":"code","source":"\"\"\"## rle code\"\"\"\n\ndef mask2rle(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 \ndef rle2mask(mask_rle, shape=(1600,256)):\n    '''\n    mask_rle: run-length as string formated (start length)\n    shape: (width,height) of array to return rg\n    Returns numpy array, 1 - mask, 0 - background\n\n    '''\n    s = mask_rle.split()\n    starts, lengths = [np.asarray(x, dtype=int) for x in (s[0:][::2], s[1:][::2])]\n    starts -= 1\n    ends = starts + lengths\n    img = np.zeros(shape[0]*shape[1], dtype=np.uint8)\n    for lo, hi in zip(starts, ends):\n        img[lo:hi] = 1\n    return img.reshape(shape).T","metadata":{"id":"XjGQzS3h9YvE"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## dataset class : auto thickness + staining","metadata":{"id":"yMxfuPRd-_En"}},{"cell_type":"code","source":"\"\"\"## dataset class : auto thickness + staining\"\"\"\n\n'''Dataset Class'''\nclass Dataset_saved(td.Dataset):\n    # 1. modes : 'train_images' or 'test_images'\n    # 2. aug : 0 (origin) or [ 1 (stain) and 2 (contrast) ] \n    # 3. csv_root : '../'\n    # 4. image_size : tuple (ex. (1024, 1024))\n    \n    def __init__(self, root_dir, mode = 'train', csv_root = '', organ = 'lung',image_size=(1024, 1024)):\n        super(Dataset_saved, self).__init__()\n        self.mode = mode\n        self.image_size = image_size\n        self.images_dir = os.path.join(root_dir,'img')\n        self.files = sorted(os.listdir(os.path.join(self.images_dir)))\n        if mode == 'train':\n            self.files = self.files[:789]\n        else:\n            self.files = self.files[789:]\n\n        self.df = pd.read_csv(csv_root).to_numpy()\n        self.idlist = self.df[:,0].tolist()\n\n        self.newfiles = []\n        for f in self.files:\n            try: cid = int(f[:f.index('_')])\n            except: cid = int(f[:-4])\n            index = self.idlist.index(cid)\n            if self.df[index, 1] == organ:\n                self.newfiles.append(f)\n        \n        self.files = self.newfiles\n\n\n    def __len__(self):\n        return len(self.files)\n\n    def __repr__(self):\n        return \"NoisyDataset(mode={}, image_size={})\". \\\n            format(self.mode, self.image_size)\n\n    def __getitem__(self, idx): \n        # img load\n        filename = self.files[idx]\n        img_path = os.path.join(self.images_dir, filename)\n        img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)\n        img = img.astype('float') / 255\n\n        try:\n          id = int(filename[:filename.index('_')])\n        except:\n          id = int(filename[:-4])\n\n        id_index = self.idlist.index(id)\n        \n        # mask generate\n        rle_code = self.df[id_index, 7]\n        mask = rle2mask(rle_code, (int(self.df[id_index, 3]), int(self.df[id_index, 3])))\n        mask = mask.astype('float')\n\n        mask_resize = cv2.resize(mask, (3000, 3000), cv2.INTER_NEAREST)\n        \n        # resize\n        if img.shape[0] != self.image_size[0]:\n\n            img = cv2.resize(img, self.image_size)\n            mask = cv2.resize(mask, self.image_size, cv2.INTER_NEAREST)\n            mask[mask >= 0.5] = 1\n            mask[mask < 0.5] = 0                        \n        \n        \n        img = img.transpose(2,0,1)\n        mask = np.reshape(mask, ((1,) + mask.shape))\n        mask_resize = np.reshape(mask_resize, ((1,) + mask_resize.shape))\n        \n\n        return img, mask, mask_resize","metadata":{"id":"ekWw8olN_I5Z"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Loss functions define","metadata":{"id":"Sy-OfpuzwaBu"}},{"cell_type":"code","source":"\"\"\"## Loss functions define\"\"\"\n\nclass Dice_loss(nn.Module):\n    def __init__(self, weight = None, size_average=True):\n        super(Dice_loss, self).__init__()\n\n    def forward(self, inputs, targets, mode = 1, smooth=1):\n        # inputs : (b_sz, 3, 266, 266) after sigmoid function\n        # targets : (b_sz, 3, 266, 266) \n        # mode - if 0 : compute dice coeffi/cient / if 1 : compute dice loss\n        # Dice loss : 1 - Dice coefficient\n        # Dice coefficient : 2 * intersection(INPUT, TARGE  T) / (sum(INPUT, TARGET) + *smooth)\n        \n        b_sz = inputs.shape[0]\n        dice_sum = torch.FloatTensor([0]).cuda()\n        for i in range(b_sz):\n\n            # flatten input and target tensors\n            input = inputs[i,:].view(-1)\n            target = targets[i,:].view(-1)\n\n            # compute the dice coefficient\n            intersection = (input * target).sum()\n            dice = (2 * intersection + smooth) / (input.sum() + targets.sum() + smooth)\n\n            dice_sum += dice\n        \n        mean_dice = dice / b_sz\n        \n        if mode == 1:\n            return 1 - mean_dice\n\n        else: return mean_dice.item()\n\n# beta : 0.7, alpha : 0.3, gamma = 3/4\nclass FocalTverskyLoss(nn.Module):\n    def __init__(self, alpha = 0.3, beta = 0.7, gamma = 3/4, weight=None, size_average=True):\n        super(FocalTverskyLoss, self).__init__()\n\n        self.alpha = alpha\n        self.beta = beta\n        self.gamma = gamma\n\n    def forward(self, inputs, targets, smooth=1):\n        # inputs : (b_sz, 1, 1024, 1024) after sigmoid !\n        # targets : (b_sz, 1, 1024, 1024)   \n        \n        #comment out if your model contains a sigmoid or equivalent activation layer\n        # inputs = F.sigmoid(inputs)       \n        \n        #flatten label and prediction tensors\n        inputs = inputs.view(-1)\n        targets = targets.view(-1)\n        \n        #True Positives, False Positives & False Negatives\n        TP = (inputs * targets).sum()    \n        FP = ((1-targets) * inputs).sum()\n        FN = (targets * (1-inputs)).sum()\n        \n        Tversky = (TP + smooth) / (TP + self.alpha*FP + self.beta*FN + smooth)  \n        FocalTversky = (1 - Tversky)**self.gamma\n                       \n        return FocalTversky","metadata":{"id":"JhOL5v3Pwcmq"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## validation matrices","metadata":{"id":"NzQUkR-kwhxj"}},{"cell_type":"code","source":"\"\"\"## validation matrices\"\"\"\n\nclass DiceScore(nn.Module):\n    def __init__(self, smooth=1.0, **kwargs):\n        super(DiceScore, self).__init__()\n        self.smooth = smooth\n\n    def forward(self, logits, targets):\n    # print('logits: {}, targets: {}'.format(logits.size(), targets.size()))\n        num = targets.size(0)\n        fn_class = lambda x: 1.0 * (x >= 0.3)\n        logits = fn_class(logits).type(torch.cuda.FloatTensor)\n        m1 = logits.view(num, -1)\n        m2 = targets.view(num, -1)\n        intersection = (m1 * m2)\n        # print(intersection.sum(1))\n        # label and outputs are all zero --> dice coeff. : zero\n        \n        # if sum(intersection.sum(1)) == 0:\n        #     score = (2. * intersection.sum(1)) / (m1.sum(1) + m2.sum(1) + self.smooth)\n        #     score = score.sum() / num\n        '''둘다 0일 때는 score = 1'''\n        \n        score = (2. * intersection.sum(1) + self.smooth) / (m1.sum(1) + m2.sum(1) + self.smooth)\n        score = score.sum() / num\n            \n        return score\n\n\nclass Iou(nn.Module):\n    def __init__(self, smooth = 1.0, **kwargs):\n        super(Iou, self).__init__()\n        self.smooth = smooth\n\n    def forward(self, logits, targets):\n        \n        # You can comment out this line if you are passing tensors of equal shape\n        # But if you are passing output from UNet or something it will most probably\n        # be with the BATCH x 1 x H x W shape\n        fn_class = lambda x: 1.0 * (x > 0.3)\n        logits = fn_class(logits).type(torch.cuda.FloatTensor)\n        logits = logits.view(-1)\n        targets = targets.view(-1)\n\n        #intersection is equivalent to True Positive count\n        #union is the mutually inclusive area of all labels & predictions\n        intersection = (logits * targets).sum()\n        total = (logits + targets).sum()\n        union = total - intersection\n\n        score = (intersection + self.smooth)/(union + self.smooth)\n\n        return score\n\n\nclass DiceScore_val(nn.Module):\n    def __init__(self, smooth=1.0, **kwargs):\n        super(DiceScore_val, self).__init__()\n        self.smooth = smooth\n\n    def forward(self, logits, targets):\n    # print('logits: {}, targets: {}'.format(logits.size(), targets.size()))\n        num = targets.size(0)\n        fn_class = lambda x: 1.0 * (x > 0.5)\n        logits = fn_class(logits).type(torch.cuda.FloatTensor)\n        m1 = logits.view(num, -1)\n        m2 = targets.view(num, -1)\n        intersection = (m1 * m2)\n        # print(intersection.sum(1))\n        # label and outputs are all zero --> dice coeff. : zero\n        intersect = 2. * intersection.sum(1)\n        plus = m1.sum(1) + m2.sum(1)\n        \n        return intersect, plus","metadata":{"id":"01WHgFN_wkaJ"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Traning definition","metadata":{"id":"f5x6Pjm7_OLY"}},{"cell_type":"code","source":"\"\"\"## Traning definition\"\"\"\n\ndef optimize_model(model, data_loader, scheduler, scheduler_yn, start):\n    global device\n    result_dict = {'train': {}, 'val': {}}\n    # result_dict = {'train' : {}}\n\n    for phase in ['train', 'val']:\n    # for phase in ['train']:\n        if phase == 'train' : model.train()\n        else: model.eval()\n        \n        running_loss = 0.0\n        running_iou = 0.0\n        running_dice = 0.0\n        #################################################\n        if phase == 'train':\n            running_dice = 0.0\n        else:\n            print('validation started ...')\n        #################################################\n      \n        for id, data in enumerate(data_loader[phase]):\n            # print(id)\n            inputs, masks, masks_origin = data[0], data[1], data[2]\n\n            if phase == 'train':\n                if (id % 300) == 0:\n                    print('time : {}m {:1f}s'.format( (time.time()-start)//60, (time.time()-start)%60 ))\n                    print('{}th batch started'.format(id+1))\n\n            # inputs and masks to device\n            inputs = inputs.to(device); masks = masks.to(device)\n\n            # initialize optimizer before optmizing current batch\n            optimizer.zero_grad()\n            with torch.set_grad_enabled(phase == 'train'):\n                \n                # forward propagation\n              \n                outputs = model(inputs.float())\n                outputs_sigm = torch.sigmoid(outputs)\n\n                # mean loss of current batch\n                loss = criterion(outputs_sigm.float(), masks_origin.cuda().float()) \n\n                # backward propagation (for SGD) in train phase\n                if phase == 'train':\n                    loss.backward()\n                    optimizer.step()\n\n            \n            # calculate accuracy    \n            # batch_dice = Diceloss(outputs_sigm, masks, mode=0, smooth=1)\n            #################################################################################\n            # if phase == 'train':\n            with torch.no_grad():\n                batch_dice = Dicescore(outputs_sigm.detach(), masks_origin.cuda())\n                batch_iou = iouscore(outputs_sigm.detach(), masks_origin.cuda())\n\n                running_loss += loss.item() * inputs.shape[0]\n                running_dice += batch_dice * inputs.shape[0]\n                running_iou += batch_iou * inputs.shape[0]\n                \n            if phase == 'train':\n                if (id % 300) == 0:\n                    print('{}th batch loss : {}'.format(id+1,loss.item()))\n                    print('{}th batch DICE : {}'.format(id+1,batch_dice))\n                    print('{}th batch IOU : {}'.format(id+1,batch_iou))\n                    print('-'*20)\n\n            #################################################################################\n\n        ## after an epoch finished\n        # scheduler step \n        if phase == 'train': \n            if scheduler_yn == 'yes':\n                scheduler.step()\n\n        # calculate epoch loss and accs (mean values)\n        ##########################################################################################\n        finalloss = running_loss/data_loader[phase].dataset.__len__()\n        finaldice = running_dice/data_loader[phase].dataset.__len__()\n        finaliou = running_iou/data_loader[phase].dataset.__len__()\n        \n        result_dict[phase]['loss'] = finalloss\n        result_dict[phase]['dice'] = finaldice\n        result_dict[phase]['iou'] = finaliou\n        ##########################################################################################\n\n    return result_dict","metadata":{"id":"FkfJbMXJ_M3Z"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def pixel_unshuffle(x, scale):\n    \"\"\" Pixel unshuffle.\n\n    Args:\n        x (Tensor): Input feature with shape (b, c, hh, hw).\n        scale (int): Downsample ratio.\n\n    Returns:\n        Tensor: the pixel unshuffled feature.\n    \"\"\"\n    b, c, hh, hw = x.size()\n    out_channel = c * (scale**2)\n    assert hh % scale == 0 and hw % scale == 0\n    h = hh // scale\n    w = hw // scale\n    x_view = x.view(b, c, h, scale, w, scale)\n    return x_view.permute(0, 1, 3, 5, 2, 4).reshape(b, out_channel, h, w)","metadata":{"id":"6D93lfhYFujU"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Main script","metadata":{"id":"rDh7cxrsD20p"}},{"cell_type":"markdown","source":"## get dataset","metadata":{"id":"4F8vZnajD5XJ"}},{"cell_type":"code","source":"'''get dataset'''\norgan = 'spleen'\nimage_size = (3072, 3072)\nroot_dir = './dataset/data_aug' ## custom dataset we augmented and saved : \ncsv_root = '/kaggle/input/hubmap-organ-segmentation/train.csv'\ntrain_dataset = Dataset_saved(root_dir, mode = 'train', csv_root = csv_root, organ = organ, image_size= image_size)\nval_dataset = Dataset_saved(root_dir, mode = 'val', csv_root = csv_root,organ = organ, image_size= image_size)\nprint(train_dataset.__len__())\nprint(val_dataset.__len__())\n\n'''augmentation'''\nbatch_size = 1\ntrain_loader = td.DataLoader(train_dataset, batch_size = batch_size, shuffle = True)\nval_loader = td.DataLoader(val_dataset, batch_size = batch_size, shuffle = False)\ndata_loader = {'train' : train_loader, 'val' : val_loader}","metadata":{"id":"QNi0dNdH_M78","outputId":"259e70b7-cdbf-4bad-ebbb-0e64799c3ff7"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"'''''for unet++ with efficientnet'''''\ndef build_unet():\n    model = smp.UnetPlusPlus(\n    # model = smp.DeepLabV3(\n        # encoder_name = \"timm-mobilenetv3_large_100\",\n        encoder_name = 'efficientnet-b1',\n        encoder_weights = None,\n        in_channels = 12,\n        classes = 1)\n\n    return model\n\nclass Mymodel(nn.Module):\n  def __init__(self):\n      super(Mymodel, self).__init__()\n      self.unet = build_unet()\n      self.convup = nn.Conv2d(1, 1, 3, 1, 1)\n      self.upsample = nn.Upsample(size = (3000, 3000), mode = 'nearest')\n      self.pixel_unshuffle = pixel_unshuffle\n      '''pixel unshuffle'''\n      # inputs = pixel_unshuffle(inputs, 2)\n  def forward(self, x):\n      x = self.pixel_unshuffle(x, 2)  # (1, 1, 3072, 3072) --> (1, 12, 1536, 1536)\n      x = self.unet(x)                \n      x = self.upsample(x)            # (1, 1, 1536, 1536) --> (1, 1, 3000, 3000)\n      x = self.convup(x)\n\n      return x","metadata":{"id":"CokaQLCRP0Un"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"'''checkout dataset'''\nimg_sizes = []\nfor id, (img, mask, mask_resize) in enumerate(train_loader):\n  \n    for b in range(img.shape[0]):\n      img_np = img[b].numpy().squeeze().transpose(1,2,0)\n      mask_np = mask_resize[b].squeeze().numpy()\n    \n      # print(imgsize)\n      fig, axe = plt.subplots(1,2, figsize = (20, 15))\n      axe[0].imshow(img_np)\n      axe[1].imshow(mask_np, cmap = 'gray')\n      plt.show()\n      \n    if id == 1:\n        break","metadata":{"id":"mach7rAJ2rvQ","outputId":"15b59c02-bd9d-4214-ca0d-f3a1b4157f2c"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"'''load checkpoint / Get model, optimizer'''\nDicescore = DiceScore().cuda()\n# Dicescore_val = DiceScore_val().cuda()\niouscore = Iou().cuda()\n\n# criterion = FocalTverskyLoss(alpha = 0.3, beta = 0.7, gamma = 2) # Focal Tversky Loss\ncriterion = nn.BCELoss()\nlr, ssize = 0.001, 10\n\nimagesize = str(image_size[0])\ndata_preprocess = 'thick_psize_stain_3000_unshuffle'\ncriterion_name = 'FTloss'\nmodel_name = 'Att-Unet'\nmodel_name = 'Unetpp_eff_pretrained'\nscheduler_yn = 'yes'\n# model_dir = './checkpoints/{}_{}_{}_{}_scheduler_{}'.format(imagesize,data_preprocess, model_name, criterion_name, scheduler_yn)\nmodel_dir = './checkpoints/{}'.format('spleen2')\n\nrelu = nn.ReLU(inplace = True)\n\nif os.path.isdir(os.path.join(model_dir, \"models\")):\n    if len(os.listdir(os.path.join(model_dir, \"models\"))) > 0:\n        times = []\n        model_list = os.listdir(os.path.join(model_dir, \"models\"))\n        for i in model_list:\n            t = os.path.getctime(os.path.join(model_dir, \"models\", i))\n            times.append(t)\n        a = sorted(range(len(times)), key=lambda k: times[k])\n        model_saved = os.path.join(model_dir, \"models\", model_list[a[-1]])\n        checkpoint = torch.load(model_saved)\n\n        model = Mymodel()\n        model.load_state_dict(checkpoint['model_state_dict'])\n        model = model.cuda()\n\n        optimizer = optim.Adam(model.parameters(), lr = lr)\n        optimizer.load_state_dict(checkpoint['optimizer_state_dict'])\n        scheduler = lr_scheduler.StepLR(optimizer, step_size=ssize, gamma=0.1)\n        start_epoch = checkpoint['epoch'] + 1\n    else:\n        model = Mymodel()\n        model = model.cuda()\n        optimizer = optim.Adam(model.parameters(), lr = lr)\n        scheduler = lr_scheduler.StepLR(optimizer, step_size=ssize, gamma=0.1)\n        start_epoch = 0\nelse:\n    os.mkdir(model_dir)\n    os.mkdir(os.path.join(model_dir, \"models\"))\n    model = Mymodel()\n    model = model.cuda()\n    optimizer = optim.Adam(model.parameters(), lr = lr)\n    scheduler = lr_scheduler.StepLR(optimizer, step_size=ssize, gamma=0.1)\n    start_epoch = 0\n\nmodel","metadata":{"id":"2rjeQZGz_M98"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_loss, val_loss = [], []\ntrain_dice, val_dice = [], []\ntrain_iou, val_iou = [], []\n\nEPOCH = 200\nfor epoch in range(start_epoch, EPOCH):\n# for epoch in range(1):\n    start = time.time()\n\n    print(f'Epoch {epoch + 1} of {EPOCH}')\n    result_dict = optimize_model(model, data_loader, scheduler,scheduler_yn, start)\n    train_loss.append(result_dict['train']['loss'])\n    train_dice.append(result_dict['train']['dice'])\n    train_iou.append(result_dict['train']['iou'])\n\n    val_loss.append(result_dict['val']['loss'])\n    val_dice.append(result_dict['val']['dice'])\n    val_iou.append(result_dict['val']['iou'])\n\n    end = time.time()\n\n    print('Epoch finished : {}m {:1f}sec'.format((end-start)//60, (end-start)%60))\n    print('Train Loss : {:3f}, Val Loss : {:3f}'.format(result_dict['train']['loss'], result_dict['val']['loss']))\n    print('Train Dice : {:3f}, Val Dice : {:3f}'.format(result_dict['train']['dice'], result_dict['val']['dice']))\n    print('Train IOU : {:3f}, Val IOU : {:3f}'.format(result_dict['train']['iou'], result_dict['val']['iou']))\n    print('-'*50)\n\n    checkpoint_path = os.path.join(model_dir, \"models\", \"{}th_epoch_{:.2f}dice_{:.4f}iou.pth\".format(epoch+1, result_dict['val']['dice'], result_dict['val']['iou']))\n    torch.save({\n                'epoch': epoch,\n                'model_state_dict':model.state_dict(),\n                'optimizer_state_dict':optimizer.state_dict(),\n                'train_loss' : result_dict['train']['loss'],\n                'val_loss' : result_dict['val']['loss'],\n                'train_dice' :result_dict['train']['dice'],\n                'val_dice' : result_dict['val']['dice'],\n                'train_iou' : result_dict['train']['iou'],\n                'val_iou' : result_dict['val']['iou']\n                },\n                checkpoint_path)\n","metadata":{"id":"F_b5KTw__FLi","outputId":"7bbcb106-3924-421e-992a-3909cf64e19c"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"id":"Fihyupt-e6hR"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"id":"XVWJY1AArqkt"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"id":"-f_nnQHrr5tk"},"execution_count":null,"outputs":[]}]}