Skip to content

Instantly share code, notes, and snippets.

@ilyarudyak
Created December 8, 2021 13:01
Show Gist options
  • Save ilyarudyak/e008bb569a95ee381d7c048f322df2cf to your computer and use it in GitHub Desktop.
Save ilyarudyak/e008bb569a95ee381d7c048f322df2cf to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 2,
"id": "small-creek",
"metadata": {},
"outputs": [],
"source": [
"import eecs598\n",
"import torch\n",
"import torch.nn as nn\n",
"import torch.nn.functional as F\n",
"import torchvision\n",
"import statistics\n",
"import random\n",
"import time\n",
"import math\n",
"import numpy as np\n",
"import cv2\n",
"import copy\n",
"import shutil\n",
"import os\n",
"import json\n",
"\n",
"import matplotlib.pyplot as plt\n",
"%matplotlib inline\n",
"\n",
"from eecs598 import reset_seed, Solver\n",
"from eecs598.grad import rel_error\n",
"\n",
"from a5_helper import *\n",
"\n",
"from single_stage_detector import IoU\n",
"\n",
"%load_ext autoreload\n",
"%autoreload 2"
]
},
{
"cell_type": "markdown",
"id": "nuclear-ghana",
"metadata": {},
"source": [
"## 01 - test data"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "peaceful-vision",
"metadata": {},
"outputs": [],
"source": [
"width = torch.tensor([35, 35], dtype=torch.float32, device='cpu')\n",
"heigh = torch.tensor([40, 40], dtype=torch.float32, device='cpu')\n",
"sample_bbox = torch.tensor([[[1,1,11,11,0], [20,20,30,30,0]]], dtype=torch.float32, device='cpu')\n",
"sample_proposals = torch.tensor([[[[[5,5,15,15], [27,27,37,37]]]]], dtype=torch.float32, device='cpu')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "catholic-washer",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"torch.Size([1, 1, 1, 2, 4])"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals.shape"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "written-start",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[[[ 5., 5., 15., 15.],\n",
" [27., 27., 37., 37.]]]]])"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "wooden-burden",
"metadata": {},
"outputs": [],
"source": [
"sample_proposals_resh = sample_proposals.reshape(1, 2, 4)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "martial-institution",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"torch.Size([1, 2, 4])"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals_resh.shape"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "subjective-optimization",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"torch.Size([1, 2, 5])"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_bbox.shape"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "applied-singles",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[ 1., 1., 11., 11., 0.],\n",
" [20., 20., 30., 30., 0.]]])"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_bbox"
]
},
{
"cell_type": "markdown",
"id": "inner-carnival",
"metadata": {},
"source": [
"## 02 - scalar iou"
]
},
{
"cell_type": "markdown",
"id": "coated-sight",
"metadata": {},
"source": [
"We need to figure out the formula for area of intersections - other formulas are straightforward.\n",
"\n",
"To compute `IoU` we need to find coordinates of intersection in `x` and `y` axis. For example in `x` axis we have to take:\n",
"- `max` of left coordinates;\n",
"- `min` of right coordinates;\n",
"\n",
"So the formula is $min(x_{br}^b, x_{br}^p) - max(x_{tl}^b, x_{tl}^p)$. And exactly the same in case of `y` axis. See for example [this post](https://medium.com/analytics-vidhya/iou-intersection-over-union-705a39e7acef).\n",
"\n",
"In the example below we may check that the area is in fact 36."
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "electoral-antique",
"metadata": {},
"outputs": [],
"source": [
"p = sample_proposals_resh[0, 0, :]\n",
"b = sample_bbox[0, 0, :4]"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "demographic-roommate",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(tensor([ 5., 5., 15., 15.]), tensor([ 1., 1., 11., 11.]))"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"p, b"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "chubby-seating",
"metadata": {},
"outputs": [],
"source": [
"xb_tl, yb_tl, xb_br, yb_br = b\n",
"xp_tl, yp_tl, xp_br, yp_br = p"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "generic-secondary",
"metadata": {},
"outputs": [],
"source": [
"dx = torch.min(xb_br, xp_br) - torch.max(xb_tl, xp_tl)\n",
"dy = torch.min(yb_br, yp_br) - torch.max(yb_tl, yp_tl)"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "thrown-remove",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(tensor(6.), tensor(6.))"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dx, dy"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "theoretical-castle",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor(36.)"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dx * dy"
]
},
{
"cell_type": "markdown",
"id": "happy-democracy",
"metadata": {},
"source": [
"## 03 - repetition"
]
},
{
"cell_type": "markdown",
"id": "waiting-rescue",
"metadata": {},
"source": [
"The next thing we have to figure out - how to compute `IoU` of *every* proposal to *every* bbox. \n",
"\n",
"We have to use [`torch.repeat`](https://pytorch.org/docs/stable/generated/torch.Tensor.repeat.html). We have to create a new dimension (using `unsqueese()`) and repeat it `N` times. "
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "knowing-pitch",
"metadata": {},
"outputs": [],
"source": [
"N = 2"
]
},
{
"cell_type": "code",
"execution_count": 45,
"id": "urban-sport",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"torch.Size([1, 2, 4])"
]
},
"execution_count": 45,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals_resh.shape"
]
},
{
"cell_type": "code",
"execution_count": 46,
"id": "aggregate-exhibition",
"metadata": {},
"outputs": [],
"source": [
"sample_proposals_resh_rep = sample_proposals_resh.unsqueeze(dim=2).repeat(1, 1, N, 1)"
]
},
{
"cell_type": "code",
"execution_count": 47,
"id": "empty-delight",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"torch.Size([1, 2, 2, 4])"
]
},
"execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals_resh_rep.shape"
]
},
{
"cell_type": "code",
"execution_count": 48,
"id": "accompanied-individual",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[[ 5., 5., 15., 15.],\n",
" [ 5., 5., 15., 15.]],\n",
"\n",
" [[27., 27., 37., 37.],\n",
" [27., 27., 37., 37.]]]])"
]
},
"execution_count": 48,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals_resh_rep"
]
},
{
"cell_type": "code",
"execution_count": 50,
"id": "fitted-pleasure",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[[ 5.],\n",
" [ 5.]],\n",
"\n",
" [[27.],\n",
" [27.]]]])"
]
},
"execution_count": 50,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals_resh_rep[:, :, :, 0:1]"
]
},
{
"cell_type": "code",
"execution_count": 51,
"id": "weird-necklace",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[[ 5.],\n",
" [ 5.]],\n",
"\n",
" [[27.],\n",
" [27.]]]])"
]
},
"execution_count": 51,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals_resh_rep[..., 0:1]"
]
},
{
"cell_type": "code",
"execution_count": 54,
"id": "quick-default",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[ 5., 5.],\n",
" [27., 27.]]])"
]
},
"execution_count": 54,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_proposals_resh_rep[..., 0]"
]
},
{
"cell_type": "markdown",
"id": "bottom-adelaide",
"metadata": {},
"source": [
"## 04 - area of proposals and boxes"
]
},
{
"cell_type": "code",
"execution_count": 56,
"id": "driving-ethiopia",
"metadata": {},
"outputs": [],
"source": [
"ps = sample_proposals_resh_rep"
]
},
{
"cell_type": "code",
"execution_count": 57,
"id": "equipped-battery",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[[ 5., 5., 15., 15.],\n",
" [ 5., 5., 15., 15.]],\n",
"\n",
" [[27., 27., 37., 37.],\n",
" [27., 27., 37., 37.]]]])"
]
},
"execution_count": 57,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ps"
]
},
{
"cell_type": "code",
"execution_count": 60,
"id": "minimal-apparel",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"torch.Size([1, 2, 2, 4])"
]
},
"execution_count": 60,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ps.shape"
]
},
{
"cell_type": "code",
"execution_count": 58,
"id": "retired-sender",
"metadata": {},
"outputs": [],
"source": [
"dx = ps[..., 2] - ps[..., 0]\n",
"dy = ps[..., 3] - ps[..., 1]\n",
"area_proposal = dx * dy"
]
},
{
"cell_type": "code",
"execution_count": 61,
"id": "waiting-cholesterol",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"torch.Size([1, 2, 2])"
]
},
"execution_count": 61,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"area_proposal.shape"
]
},
{
"cell_type": "code",
"execution_count": 62,
"id": "contrary-authority",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[100., 100.],\n",
" [100., 100.]]])"
]
},
"execution_count": 62,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"area_proposal"
]
},
{
"cell_type": "code",
"execution_count": 63,
"id": "rocky-seeker",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[ 1., 1., 11., 11., 0.],\n",
" [20., 20., 30., 30., 0.]]])"
]
},
"execution_count": 63,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sample_bbox"
]
},
{
"cell_type": "code",
"execution_count": 65,
"id": "wrapped-composite",
"metadata": {},
"outputs": [],
"source": [
"bb = sample_bbox[..., :4]\n",
"dx = bb[..., 2] - bb[..., 0]\n",
"dy = bb[..., 3] - bb[..., 1]\n",
"area_bboxes = dx * dy"
]
},
{
"cell_type": "code",
"execution_count": 66,
"id": "latin-story",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[100., 100.]])"
]
},
"execution_count": 66,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"area_bboxes"
]
},
{
"cell_type": "markdown",
"id": "accessory-soviet",
"metadata": {},
"source": [
"## 05 - intersection area"
]
},
{
"cell_type": "code",
"execution_count": 67,
"id": "modern-updating",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[[ 5., 5., 15., 15.],\n",
" [ 5., 5., 15., 15.]],\n",
"\n",
" [[27., 27., 37., 37.],\n",
" [27., 27., 37., 37.]]]])"
]
},
"execution_count": 67,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ps"
]
},
{
"cell_type": "code",
"execution_count": 68,
"id": "isolated-intensity",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[ 1., 1., 11., 11.],\n",
" [20., 20., 30., 30.]]])"
]
},
"execution_count": 68,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bb"
]
},
{
"cell_type": "code",
"execution_count": 69,
"id": "robust-healing",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(torch.Size([1, 2, 2, 4]), torch.Size([1, 2, 4]))"
]
},
"execution_count": 69,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ps.shape, bb.shape"
]
},
{
"cell_type": "code",
"execution_count": 71,
"id": "capable-stevens",
"metadata": {},
"outputs": [],
"source": [
"bb = bb.unsqueeze(dim=1).repeat(1, 2, 1, 1)"
]
},
{
"cell_type": "code",
"execution_count": 73,
"id": "urban-topic",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"torch.Size([1, 2, 2, 4])"
]
},
"execution_count": 73,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# shape is the same as ps\n",
"bb.shape"
]
},
{
"cell_type": "code",
"execution_count": 74,
"id": "noble-neighbor",
"metadata": {},
"outputs": [],
"source": [
"xp_tl, yp_tl, xp_br, yp_br = ps[..., 0], ps[..., 1], ps[..., 2], ps[..., 3]\n",
"xb_tl, yb_tl, xb_br, yb_br = bb[..., 0], bb[..., 1], bb[..., 2], bb[..., 3]\n",
"dx = torch.min(xb_br, xp_br) - torch.max(xb_tl, xp_tl)\n",
"dy = torch.min(yb_br, yp_br) - torch.max(yb_tl, yp_tl)\n",
"area_inters = dx * dy"
]
},
{
"cell_type": "code",
"execution_count": 76,
"id": "burning-hazard",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[ 6., -5.],\n",
" [-16., 3.]]])"
]
},
"execution_count": 76,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dx"
]
},
{
"cell_type": "code",
"execution_count": 75,
"id": "normal-advantage",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[ 36., 25.],\n",
" [256., 9.]]])"
]
},
"execution_count": 75,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"area_inters"
]
},
{
"cell_type": "markdown",
"id": "unexpected-mobile",
"metadata": {},
"source": [
"Is this correct? Let's look one more time at proposals and bboxes. Clearly `[ 5., 5., 15., 15.]` and `[20., 20., 30., 30.]` just don't have an intersection. So we have to use `torch.clamp()`."
]
},
{
"cell_type": "code",
"execution_count": 77,
"id": "forward-adaptation",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[[ 5., 5., 15., 15.],\n",
" [ 5., 5., 15., 15.]],\n",
"\n",
" [[27., 27., 37., 37.],\n",
" [27., 27., 37., 37.]]]])"
]
},
"execution_count": 77,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ps"
]
},
{
"cell_type": "code",
"execution_count": 78,
"id": "regulated-bullet",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([[[[ 1., 1., 11., 11.],\n",
" [20., 20., 30., 30.]],\n",
"\n",
" [[ 1., 1., 11., 11.],\n",
" [20., 20., 30., 30.]]]])"
]
},
"execution_count": 78,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bb"
]
},
{
"cell_type": "markdown",
"id": "contained-saturn",
"metadata": {},
"source": [
"## 07 - final check "
]
},
{
"cell_type": "markdown",
"id": "adjusted-expert",
"metadata": {},
"source": [
"Let's compute manually `IoU` for this example and compare with the function output. \n",
"\n",
"```python\n",
"def IoU(proposals, bboxes):\n",
" \"\"\"\n",
" Compute intersection over union between sets of bounding boxes.\n",
"\n",
" Inputs:\n",
" - proposals: Proposals of shape (B, A, H', W', 4)\n",
" - bboxes: Ground-truth boxes from the DataLoader of shape (B, N, 5).\n",
" Each ground-truth box is represented as tuple (x_lr, y_lr, x_rb, y_rb, class).\n",
" If image i has fewer than N boxes, then bboxes[i] will be padded with extra\n",
" rows of -1.\n",
" \n",
" Outputs:\n",
" - iou_mat: IoU matrix of shape (B, A*H'*W', N) where iou_mat[b, i, n] gives\n",
" the IoU between one element of proposals[b] and bboxes[b, n].\n",
"\n",
" For this implementation you DO NOT need to filter invalid proposals or boxes;\n",
" in particular you don't need any special handling for bboxxes that are padded\n",
" with -1.\n",
" \"\"\"\n",
" iou_mat = None\n",
" ##############################################################################\n",
" # TODO: Compute the Intersection over Union (IoU) on proposals and GT boxes. #\n",
" # No need to filter invalid proposals/bboxes (i.e., allow region area <= 0). #\n",
" # However, you need to make sure to compute the IoU correctly (it should be #\n",
" # 0 in those cases. # \n",
" # You need to ensure your implementation is efficient (no for loops). #\n",
" # HINT: #\n",
" # IoU = Area of Intersection / Area of Union, where #\n",
" # Area of Union = Area of Proposal + Area of BBox - Area of Intersection #\n",
" # and the Area of Intersection can be computed using the top-left corner and #\n",
" # bottom-right corner of proposal and bbox. Think about their relationships. #\n",
" ##############################################################################\n",
" # Replace \"pass\" statement with your code\n",
" \n",
" # unpack shapes\n",
" B, A, Hp, Wp, _ = proposals.shape\n",
" _, N, _ = bboxes.shape \n",
"\n",
" # repeat proposals and bboxes\n",
" proposals_resh = proposals.reshape(B, A*Hp*Wp, 4).unsqueeze(dim=2)\n",
" ps = proposals_resh.repeat(1, 1, N, 1)\n",
" # we can achieve the same effect with broadcasting but \n",
" # it's better to keep everything as clear as possible\n",
" bb = bboxes[:, :, :4].unsqueeze(dim=1).repeat(1, A*Hp*Wp, 1, 1)\n",
"\n",
" # compute simple areas; we use the fact that x_br >= x_tl and the same for y\n",
" # (B, A*Hp*Wp, N, 4)\n",
" dx = ps[..., 2] - ps[..., 0]\n",
" dy = ps[..., 3] - ps[..., 1]\n",
" area_proposal = dx * dy\n",
"\n",
" # (B, A*Hp*Wp, N, 4)\n",
" dx = bb[..., 2] - bb[..., 0]\n",
" dy = bb[..., 3] - bb[..., 1]\n",
" area_bboxes = dx * dy\n",
"\n",
" # compute intersection area using min / max\n",
" # https://medium.com/analytics-vidhya/iou-intersection-over-union-705a39e7acef\n",
" # if we have a negative number this means we don't have intersection, so we have \n",
" # to eliminate those cases with torch.clamp()\n",
" xp_tl, yp_tl, xp_br, yp_br = ps[..., 0], ps[..., 1], ps[..., 2], ps[..., 3]\n",
" xb_tl, yb_tl, xb_br, yb_br = bb[..., 0], bb[..., 1], bb[..., 2], bb[..., 3]\n",
"\n",
" dx = torch.min(xb_br, xp_br) - torch.max(xb_tl, xp_tl)\n",
" dx = torch.clamp(dx, min=0)\n",
"\n",
" dy = torch.min(yb_br, yp_br) - torch.max(yb_tl, yp_tl)\n",
" dy = torch.clamp(dy, min=0)\n",
"\n",
" area_inters = dx * dy\n",
"\n",
" # compute iou_mat\n",
" iou_mat = area_inters / (area_proposal + area_bboxes - area_inters)\n",
"\n",
" ##############################################################################\n",
" # END OF YOUR CODE #\n",
" ##############################################################################\n",
" return iou_mat\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 80,
"id": "metallic-slave",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[[0.21951219512195122, 0], [0, 0.04712041884816754]]"
]
},
"execution_count": 80,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"[[36 / (100 + 100 - 36), 0], [0, 9 / (100 + 100 - 9)]]"
]
},
{
"cell_type": "code",
"execution_count": 81,
"id": "divided-wyoming",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor([[[0.2195, 0.0000],\n",
" [0.0000, 0.0471]]])\n"
]
}
],
"source": [
"# simple sanity check\n",
"width = torch.tensor([35, 35], dtype=torch.float32, device='cpu')\n",
"heigh = torch.tensor([40, 40], dtype=torch.float32, device='cpu')\n",
"sample_bbox = torch.tensor([[[1,1,11,11,0], [20,20,30,30,0]]], dtype=torch.float32, device='cpu')\n",
"sample_proposals = torch.tensor([[[[[5,5,15,15], [27,27,37,37]]]]], dtype=torch.float32, device='cpu')\n",
"\n",
"\n",
"result = IoU(sample_proposals, sample_bbox)\n",
"print(result)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "mounted-comfort",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "freelance-asthma",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "heated-jamaica",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "automotive-torture",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "embedded-communist",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment