receipt_indexer/code/autocropper/notebooks/oldnotebooks/rotator.ipynb
Ethan Wellenreiter 423b511dd9 Cleanup commit
Moving around the testing notebooks. Autocropping is about done
with exception to any new versions or converting the stuff to C
code.

Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
2023-10-18 22:48:24 -04:00

778 lines
24 KiB
Plaintext

{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/usr/local/lib/python3.10/dist-packages/torchvision/datapoints/__init__.py:12: UserWarning: The torchvision.datapoints and torchvision.transforms.v2 namespaces are still Beta. While we do not expect major breaking changes, some APIs may still change according to user feedback. Please submit any feedback you may have in this issue: https://github.com/pytorch/vision/issues/6753, and you can also check out https://github.com/pytorch/vision/issues/7319 to learn more about the APIs that we suspect might involve future changes. You can silence this warning by calling torchvision.disable_beta_transforms_warning().\n",
" warnings.warn(_BETA_TRANSFORMS_WARNING)\n",
"/usr/local/lib/python3.10/dist-packages/torchvision/transforms/v2/__init__.py:54: UserWarning: The torchvision.datapoints and torchvision.transforms.v2 namespaces are still Beta. While we do not expect major breaking changes, some APIs may still change according to user feedback. Please submit any feedback you may have in this issue: https://github.com/pytorch/vision/issues/6753, and you can also check out https://github.com/pytorch/vision/issues/7319 to learn more about the APIs that we suspect might involve future changes. You can silence this warning by calling torchvision.disable_beta_transforms_warning().\n",
" warnings.warn(_BETA_TRANSFORMS_WARNING)\n"
]
}
],
"source": [
"import torch\n",
"import torch.nn as nn\n",
"import torch.nn.functional as fn\n",
"import torch.optim as optim\n",
"import torchvision.transforms.functional as tvf\n",
"from torchvision.transforms import v2\n",
"from torch.utils.data import DataLoader\n",
"\n",
"from PIL import Image\n",
"\n",
"import datasets as ds\n",
"from tqdm.autonotebook import tqdm\n",
"\n",
"import random\n",
"\n",
"import matplotlib.pyplot as plt\n",
"\n",
"import numpy as np\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"# original_dataset = ds.load_dataset(\"aharley/rvl_cdip\")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"working_dataset = ds.load_from_disk(\"../.cache/huggingfaces/datasets/customrotation/\")\n",
"prepimage = v2.Compose([v2.Grayscale(num_output_channels=3),v2.Resize(1100), v2.CenterCrop(1100),v2.ToImageTensor(), v2.ConvertImageDtype()])\n",
"working_dataset.set_transform(prepimage)\n",
"torch.cuda.empty_cache()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"# Parameter Declaration\n",
"minRotation=-180\n",
"maxRotation=180\n",
"minTranslation=0\n",
"maxTranslation=150\n",
"minScale = 0.4\n",
"maxScale = 1\n",
"minShear = 0\n",
"maxShear = 0\n",
"\n",
"minFill=0\n",
"maxFill=255\n",
"\n",
"params = {\"minRotation\":minRotation,\"maxRotation\":maxRotation,\"minTranslation\":minTranslation,\"maxTranslation\":maxTranslation,\"minScale\":minScale,\"maxScale\":maxScale,\"minShear\":minShear,\"maxShear\":maxShear,\"minFill\":minFill,\"maxFill\":maxFill}"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def transform_picture(image_label, parameters):\n",
" image = image_label['image']\n",
"\n",
" appliedRotation = random.uniform(parameters['minRotation'], parameters['maxRotation'])\n",
" appliedXTranslation = random.uniform(parameters['minTranslation'], parameters['maxTranslation'])\n",
" appliedYTranslation = random.uniform(parameters['minTranslation'], parameters['maxTranslation'])\n",
" appliedScale = random.uniform(parameters['minScale'], parameters['maxScale'])\n",
" appliedFill = random.uniform(parameters['minFill'], parameters['maxFill'])\n",
" appliedXShear = random.uniform(parameters['minShear'], parameters['maxShear'])\n",
" appliedYShear = random.uniform(parameters['minShear'], parameters['maxShear'])\n",
" \n",
" appliers = [v2.RandomApply(transforms=[v2.RandomPosterize(bits=1)], p=0.25),\n",
" v2.RandomApply(transforms=[v2.ElasticTransform(alpha=25.0, fill=appliedFill)], p=0.25), # maybe add fill=appliedFill\n",
" v2.RandomApply(transforms=[v2.GaussianBlur(kernel_size=(5,9), sigma=(0.1,2.))],p=0.25),\n",
" v2.RandomApply(transforms=[v2.RandomEqualize()],p=0.25)]\n",
" \n",
" adjustedimage = tvf.affine(image, appliedRotation, [appliedXTranslation,appliedYTranslation], appliedScale, [appliedXShear, appliedYShear], fill=appliedFill)\n",
"\n",
" for applier in appliers:\n",
" adjustedimage = applier(adjustedimage)\n",
"\n",
" \n",
" adjustedimage = tvf.resize(adjustedimage, size=[1100,1100])\n",
" \n",
" return {'image':adjustedimage,'label':appliedRotation}"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"# # Create own dataset from the images of the original dataset but make the labels the float value for the rotation. do the random rotation on all of the training ones but the labels for the validation and test should/can be 0\n",
"# og_training_dataset = original_dataset['train']\n",
"# og_testing_dataset = original_dataset['test']\n",
"# og_validation_dataset = original_dataset['validation']\n",
"\n",
"# type(og_testing_dataset[0]['label'])\n",
"\n",
"# # type(transform_picture(og_testing_dataset[0], params))\n",
"# new_testing_dataset = og_testing_dataset.map(transform_picture, fn_kwargs={'parameters':params})"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"# class WorkaroundDataset(torch.utils.data.Dataset):\n",
"# def __init__(self, dataset):\n",
"# self._dataset = dataset\n",
"\n",
"# def __len__(self):\n",
"# return len(self._dataset)\n",
"\n",
"# def __getitem__(self, idx):\n",
"# return v2.Compose([v2.ToImageTensor(), v2.ConvertImageDtype()])(self._dataset[idx]['image'])"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"# # type(image_dataset['train'][0]['image'])\n",
"# # print(image_dataset['train'][0]['image'])\n",
"# img = image_dataset['train'][2]['image']\n",
"# # img\n",
"# # print(img.size)\n",
"# crop = tvf.resize(img, size=[500])\n",
"# # crop\n",
"# # print(crop.size)\n",
"# newimg = tvf.affine(crop, 180, [0,0], 0.7, 0)\n",
"# newimg"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"# appliedRotation = random.uniform(minRotation, maxRotation)\n",
"# appliedXTranslation = random.uniform(minTranslation, maxTranslation)\n",
"# appliedYTranslation = random.uniform(minTranslation, maxTranslation)\n",
"# appliedScale = random.uniform(minScale, maxScale)\n",
"# appliedFill = random.uniform(minFill, maxFill)\n",
"\n",
"\n",
"\n",
"# newimg = tvf.affine(crop, appliedRotation, [appliedXTranslation,appliedYTranslation], appliedScale, shear, fill=appliedFill)\n",
"# newimg\n",
"\n",
"# appliers = [v2.RandomApply(transforms=[v2.RandomPosterize(bits=1)], p=0.25),\n",
"# v2.RandomApply(transforms=[v2.ElasticTransform(alpha=25.0, fill=appliedFill)], p=0.25),\n",
"# v2.RandomApply(transforms=[v2.GaussianBlur(kernel_size=(5,9), sigma=(0.1,2.))],p=0.25),\n",
"# v2.RandomApply(transforms=[v2.RandomEqualize()],p=0.25)]\n",
"\n",
"# for applier in appliers:\n",
"# newimg = applier(newimg)\n",
" \n",
"# # newimg\n",
"# newimg= tvf.resize(newimg, size=[1000,1000])\n",
"# newimg\n",
" "
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"# class SquarePad:\n",
"# \tdef __call__(self, image):\n",
"# \t\tw, h = image.size\n",
"# \t\tmax_wh = np.max([w, h])\n",
"# \t\thp = int((max_wh - w) / 2)\n",
"# \t\tvp = int((max_wh - h) / 2)\n",
"# \t\tpadding = (hp, vp, hp, vp)\n",
"# \t\treturn tvf.pad(image, padding, 0, 'constant')"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"\n",
"class RotationDeterminer(nn.Module):\n",
" def __init__(self):\n",
" super().__init__()\n",
" \n",
" torch.cuda.empty_cache()\n",
" \n",
" self.device = torch.device(\"cpu\")\n",
" if torch.cuda.is_available:\n",
" self.device = torch.device(\"cuda:0\")\n",
" \n",
" \n",
" self.appliers = [v2.RandomApply(transforms=[v2.RandomPosterize(bits=1)], p=0.25),\n",
" v2.RandomApply(transforms=[v2.ElasticTransform(alpha=25.0)], p=0.25), # maybe add fill=appliedFill\n",
" v2.RandomApply(transforms=[v2.GaussianBlur(kernel_size=(5,9), sigma=(0.1,2.))],p=0.25),\n",
" v2.RandomApply(transforms=[v2.RandomEqualize()],p=0.25)]\n",
" \n",
" \n",
" self.conv = nn.Sequential(nn.Conv2d(3, 9, kernel_size=11,stride=3), # 1100 x 1100 => 201 x 201\n",
" nn.ReLU(inplace=True),\n",
" nn.Conv2d(9, 18, kernel_size=5,stride=1),\n",
" nn.ReLU(inplace=True),\n",
" nn.MaxPool2d(kernel_size=4, stride=2),\n",
" nn.Conv2d(18, 36, kernel_size=3,stride=2),\n",
" nn.ReLU(inplace=True),\n",
" nn.Conv2d(36, 72, kernel_size=3,stride=2),\n",
" nn.ReLU(inplace=True),\n",
" nn.AvgPool2d(kernel_size=5, stride=3),\n",
" nn.Conv2d(72, 144, kernel_size=3,stride=1),\n",
" nn.ReLU(inplace=True),\n",
" nn.Conv2d(144, 288, kernel_size=5,stride=1),\n",
" nn.ReLU(inplace=True),\n",
" nn.MaxPool2d(kernel_size=4, stride=1),\n",
" nn.Conv2d(288, 192, kernel_size=3,stride=1),\n",
" nn.ReLU(inplace=True),\n",
" nn.Conv2d(192, 192, kernel_size=3,stride=1), # => 1\n",
" nn.ReLU(inplace=True))\n",
" \n",
" self.classifier = nn.Sequential(nn.Dropout(),\n",
" nn.Linear(192, 2048),\n",
" nn.ReLU(inplace=True),\n",
" nn.Dropout(),\n",
" nn.Linear(2048,2048),\n",
" nn.ReLU(inplace=True),\n",
" nn.Linear(2048,1))\n",
" \n",
" self.lossfunc = nn.MSELoss()\n",
" \n",
" self.imageprep = v2.Compose([self.SquarePad(),v2.Resize(1100),v2.Grayscale(num_output_channels=3),v2.CenterCrop(1100),v2.ToImageTensor(), v2.ConvertImageDtype()])\n",
" \n",
" \n",
" class SquarePad:\n",
" def __call__(self, image):\n",
" # print(\"hi type:\", type(image))\n",
" temp = image.size()\n",
" w = temp[-2]\n",
" h = temp[-1]\n",
" max_wh = max([w, h])\n",
" hp = int((max_wh - w) / 2)\n",
" vp = int((max_wh - h) / 2)\n",
" padding = (hp, vp, hp, vp)\n",
" return tvf.pad(image, padding, 0, 'edge')\n",
"\n",
"\n",
" \n",
"\n",
" \n",
" def forward(self, image):\n",
"\n",
" transformedimage = self.imageprep(image)\n",
" transformedimage = transformedimage.to(self.device)\n",
"\n",
" x = self.conv(transformedimage)\n",
" x = nn.Flatten(start_dim=-3)(x)\n",
" x = self.classifier(x)\n",
" guessRotation = nn.Flatten(start_dim=0)(x)\n",
" \n",
" return guessRotation\n",
" \n",
" def loss(self, guess, trueAnswer):\n",
" return self.lossfunc(guess, trueAnswer)\n",
" \n",
" "
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"# def batchmaker(entries, batchsize):\n",
"# random.shuffle(entries)\n",
"# listing = []\n",
"# for i in range(0,len(entries), batchsize):\n",
"# listing.append(entries[i:i+batchsize])\n",
"# return listing"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"# print(type(v2.Compose([v2.ToImageTensor(), v2.ConvertImageDtype()])(image_dataset['train'][0]['image'])))"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"# a, b, x = working_dataset['train'][0]['image'].size()\n",
"# print(x)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"def train(model, dataset, batchsize, num_epochs, stepsize, totalnumiters = -1):\n",
" device = torch.device(\"cpu\")\n",
" if torch.cuda.is_available:\n",
" device = torch.device(\"cuda:0\")\n",
" model = model.cuda()\n",
" optimizer = optim.Adam(model.parameters(), lr=stepsize)\n",
" \n",
" counter = totalnumiters\n",
" model = model.train()\n",
" \n",
" breakearly = True\n",
" if totalnumiters == -1:\n",
" print(\"hi\")\n",
" breakearly = False\n",
" totalnumiters = len(dataset) + 1\n",
" \n",
" for e in range(num_epochs):\n",
" \n",
" train_dataloader = DataLoader(dataset, batch_size=batchsize, shuffle=True)\n",
" \n",
" pbar = tqdm(train_dataloader)\n",
" \n",
" for i, batch in enumerate(pbar):\n",
" torch.cuda.empty_cache()\n",
" images, truerotations = batch['image'], batch['rotation']\n",
" images = images.to(device)\n",
" truerotations = truerotations.to(device)\n",
"\n",
" optimizer.zero_grad()\n",
" \n",
" guessRotation = model(images)\n",
" \n",
" truerotations = truerotations.float()\n",
" \n",
" loss = model.loss(guessRotation, truerotations)\n",
" \n",
" loss.backward()\n",
" \n",
" optimizer.step()\n",
" counter = counter - batchsize\n",
" if counter <= 0 and breakearly:\n",
" print(\"endearly\")\n",
" return\n",
"\n",
" "
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"testimage = working_dataset['train'][10]['image']\n",
"\n",
"# testimage = v2.Compose([v2.Grayscale(num_output_channels=3),v2.ToTensor(),])(testimage)\n",
"# testimage.size()"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [],
"source": [
"# plt.imshow(testimage)\n",
"# plt.show()"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [],
"source": [
"# temp = testimage.size()\n",
"# print(temp[-3])"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [],
"source": [
"model = RotationDeterminer()\n",
"device = torch.device(\"cpu\")\n",
"if torch.cuda.is_available:\n",
" device = torch.device(\"cuda:0\")\n",
" model = model.cuda()\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [],
"source": [
"# output = model(testimage)\n",
"# print(output)"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [],
"source": [
"# train_dataloader = DataLoader(working_dataset['test'], batch_size=100, shuffle=True)\n",
"# hold = next(iter(train_dataloader))\n",
"# images1, labels1 = hold['image'], hold['rotation']\n",
"# # print(images1)\n",
"# print(labels1.size())"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/usr/local/lib/python3.10/dist-packages/torchvision/transforms/functional.py:1603: UserWarning: The default value of the antialias parameter of all the resizing transforms (Resize(), RandomResizedCrop(), etc.) will change from None to True in v0.17, in order to be consistent across the PIL and Tensor backends. To suppress this warning, directly pass antialias=True (recommended, future default), antialias=None (current default, which means False for Tensors and True for PIL), or antialias=False (only works on Tensors - PIL will still use antialiasing). This also applies if you are using the inference transforms from the models weights: update the call to weights.transforms(antialias=True).\n",
" warnings.warn(\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"hi\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "faff1411ea0d485b9321271ebe6820db",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/12800 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "d26b6872bee74eaab8be6e7cfe53b190",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/12800 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"outputarray = np.array([working_dataset['train'][10]['rotation']])\n",
"model = model.eval()\n",
"output = model(testimage)\n",
"outputarray = np.append(outputarray, output.detach().cpu().numpy())\n",
"counter = 0\n",
"\n",
"\n",
"\n",
"train(model, working_dataset['train'], 25, 2, 5e-3)\n",
"\n",
"model = model.eval()\n",
"\n",
"counter = 2 + counter\n",
"output = model(testimage)\n",
"outputarray = np.append(outputarray, output.detach().cpu().numpy())\n",
"np.save(\"./testing_space/outputarray\", outputarray)\n",
"np.save(\"./testing_space/counter\", counter)"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"-164.93280103082208\n",
"8.194759720936418e-05\n",
"-0.1751984804868698\n"
]
}
],
"source": [
"print(outputarray[0])\n",
"print(outputarray[1])\n",
"print(outputarray[2])"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3"
]
},
"execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(outputarray)"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [],
"source": [
"torch.save(model.state_dict(), \"./testing_space/modelsave\" + str(counter) +\" epochs\")"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [],
"source": [
"#load model\n",
"# model.load_state_dict(torch.load(\"./testing_space/modelsave2epochs\"))"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"hi\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "e144d16317094603b328e2db88a4853a",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/12800 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "bb6cf6e77ed34628bdfa6ed2a64ef284",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/12800 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"train(model, working_dataset['train'], 25, 2, 1e-3)\n",
"counter = 2 + counter"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [],
"source": [
"# outputarray = []"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {},
"outputs": [],
"source": [
"model = model.eval()\n",
"output = model(testimage)\n",
"outputarray = np.append(outputarray, output.detach().cpu().numpy())\n",
"np.save(\"./testing_space/outputarray\", outputarray)\n",
"np.save(\"./testing_space/counter\", counter)"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {},
"outputs": [],
"source": [
"torch.save(model.state_dict(), \"./testing_space/modelsave\" + str(counter) +\" epochs\")"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"hi\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "c270b991cacd4abc996c602748e742f7",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/12800 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "1515223a29da4cfea86be156155fd06e",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/12800 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"train(model, working_dataset['train'], 25, 2, 1e-2)\n",
"counter = 2 + counter"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {},
"outputs": [],
"source": [
"model = model.eval()\n",
"output = model(testimage)\n",
"outputarray = np.append(outputarray, output.detach().cpu().numpy())\n",
"np.save(\"./testing_space/outputarray\", outputarray)\n",
"np.save(\"./testing_space/counter\", counter)"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {},
"outputs": [],
"source": [
"torch.save(model.state_dict(), \"./testing_space/modelsave\" + str(counter) +\" epochs\")"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[-1.64932801e+02 8.19475972e-05 -1.75198480e-01 -2.21363053e-01\n",
" -2.17262208e-01]\n"
]
}
],
"source": [
"print(outputarray)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.10.12"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}