number-plate-study/why28.ipynb

2466 lines
77 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import torch\n",
"import torch.nn as nn\n",
"import torch.optim as optim\n",
"from torch.utils.data import DataLoader, Dataset, Subset\n",
"from torchvision import transforms\n",
"import random\n",
"from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageFilter\n",
"import cv2\n",
"import numpy as np\n",
"from torch.optim.lr_scheduler import ReduceLROnPlateau\n",
"from sklearn.model_selection import train_test_split\n",
"import torch.nn as nn\n",
"import torch.nn.functional as F\n",
"from tqdm import tqdm"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {},
"outputs": [],
"source": [
"DATASET_DIR = \"dataset\"\n",
"SAVE_PATH = \"best_model_10.pth\"\n",
"BATCH_SIZE = 128\n",
"EPOCHS = 30\n",
"LEARNING_RATE = 0.01\n",
"DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"\n",
"CLASSES = \"ABEKMHOPCTYX0123456789\"\n",
"NUM_CLASSES = len(CLASSES)\n",
"CLASS_TO_IDX = {char: idx for idx, char in enumerate(CLASSES)}\n",
"IDX_TO_CLASS = {idx: char for char, idx in CLASS_TO_IDX.items()}"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Датасет сохранен в папке 'dataset'.\n",
"Конфигурационный файл создан: dataset_config.txt\n"
]
}
],
"source": [
"FONT_PATH = \"RoadNumbers2.0.ttf\"\n",
"CONFIG_FILE = \"dataset_config.txt\"\n",
"DATASET_DIR = \"dataset\" # Папка для итогового датасета\n",
"GOSZNAK_DIR = \"gosznak\" # Папка с изображениями для аугментаций\n",
"REAL_GOSZNAK_DIR = \"real_gosznak\" # Папка с реальными изображениями без аугментаций\n",
"\n",
"SYMBOLS = \"ABEKMHOPCTYX0123456789\"\n",
"SMALL_FONT_SIZES = [26, 32]\n",
"IMG_SIZE = (28, 28)\n",
"FONT_SIZES = [26, 32, 38, 46, 54, 58]\n",
"MAX_ROTATION = 10\n",
"AUGMENTATIONS = 240\n",
"\n",
"os.makedirs(DATASET_DIR, exist_ok=True)\n",
"\n",
"try:\n",
" font = ImageFont.truetype(FONT_PATH, FONT_SIZES[0])\n",
"except IOError:\n",
" print(f\"Шрифт {FONT_PATH} не найден. Убедитесь, что он находится в текущей директории.\")\n",
"\n",
"def process_gosznak_images(input_dir, output_dir, apply_augmentations=True):\n",
" \"\"\"\n",
" Обрабатывает изображения из папки `input_dir` и сохраняет их в папку `output_dir`.\n",
"\n",
" :param input_dir: Путь к папке с исходными изображениями.\n",
" :param output_dir: Путь к папке для сохранения обработанных изображений.\n",
" :param apply_augmentations: Если True, применяются аугментации.\n",
" \"\"\"\n",
" config_lines = []\n",
" for filename in os.listdir(input_dir):\n",
" filepath = os.path.join(input_dir, filename)\n",
" if not os.path.isfile(filepath) or not filename.endswith((\".png\", \".jpg\", \".jpeg\")):\n",
" continue\n",
"\n",
" img = Image.open(filepath).convert(\"L\") # Конвертируем в оттенки серого\n",
" img = img.resize(IMG_SIZE, resample=Image.Resampling.LANCZOS) \n",
" symbol = os.path.splitext(filename)[0][0] # Предполагаем, что имя файла начинается с символа\n",
" symbol_dir = os.path.join(output_dir, symbol)\n",
" os.makedirs(symbol_dir, exist_ok=True)\n",
"\n",
" if apply_augmentations:\n",
" for i in range(AUGMENTATIONS):\n",
" aug_img = img.copy()\n",
"\n",
" # Случайное смещение\n",
" if random.random() < 0.7:\n",
" aug_img = shift_image(aug_img, max_shift=4)\n",
"\n",
" # Случайный поворот\n",
" rotation = random.uniform(-MAX_ROTATION, MAX_ROTATION)\n",
" aug_img = aug_img.rotate(rotation, expand=False, fillcolor=255)\n",
"\n",
" # Случайное расширение\n",
" if random.random() < 0.5:\n",
" kernel_size = (random.randint(1, 2), random.randint(1, 2))\n",
" aug_img = expand_characters(aug_img, kernel_size=kernel_size, iterations=1)\n",
"\n",
" # Случайные разрывы\n",
" if random.random() < 0.15:\n",
" aug_img = add_random_gaps(aug_img, num_gaps=random.randint(1, 5), gap_size=random.randint(2, 4))\n",
"\n",
" # Блюр\n",
" #blur_min_limits = {\"box\": 5, \"gaussian\": 5, \"motion\": 6}\n",
" #blur_max_limits = {\"box\": 10, \"gaussian\": 10, \"motion\": 12}\n",
" if random.random() < 0.85:\n",
" aug_img = apply_blur(aug_img)# blur_min_limits=blur_min_limits, blur_max_limits=blur_max_limits)\n",
"\n",
" # Добавление шума\n",
" if random.random() < 0.1:\n",
" aug_img = add_noise(aug_img, intensity=random.randint(10, 20))\n",
"\n",
" file_path = os.path.join(symbol_dir, f\"{symbol}_aug_{i}.png\")\n",
" aug_img.save(file_path)\n",
" config_lines.append(f\"{file_path},{symbol}\\n\")\n",
" else:\n",
" # Сохраняем изображение без аугментаций\n",
" file_path = os.path.join(symbol_dir, filename)\n",
" img.save(file_path)\n",
" config_lines.append(f\"{file_path},{symbol}\\n\")\n",
" return config_lines\n",
"\n",
"def expand_characters(img, kernel_size=(2, 2), iterations=1):\n",
" kernel = cv2.getStructuringElement(cv2.MORPH_RECT, kernel_size)\n",
" img_array = np.array(img)\n",
" expanded = cv2.dilate(img_array, kernel, iterations=iterations)\n",
" return Image.fromarray(expanded)\n",
"\n",
"def add_random_gaps(img, num_gaps=5, gap_size=5):\n",
" draw = ImageDraw.Draw(img)\n",
" for _ in range(num_gaps):\n",
" x1 = random.randint(0, IMG_SIZE[0] - gap_size)\n",
" y1 = random.randint(0, IMG_SIZE[1] - gap_size)\n",
" x2 = x1 + gap_size\n",
" y2 = y1 + gap_size\n",
" draw.rectangle([x1, y1, x2, y2], fill=255) \n",
" return img\n",
"\n",
"def apply_blur(img, blur_min_limits=None, blur_max_limits=None):\n",
" \"\"\"\n",
" Применяет случайное размытие (BoxBlur, GaussianBlur, MotionBlur) к изображению.\n",
"\n",
" :param img: PIL Image объект.\n",
" :param blur_min_limits: Словарь с минимальными значениями размытия.\n",
" Пример: {\"box\": 1, \"gaussian\": 1, \"motion\": 4}.\n",
" :param blur_max_limits: Словарь с максимальными значениями размытия.\n",
" Пример: {\"box\": 4, \"gaussian\": 3, \"motion\": 8}.\n",
" :return: Изображение с примененным размытием.\n",
" \"\"\"\n",
" if blur_min_limits is None:\n",
" blur_min_limits = {\"box\": 1, \"gaussian\": 1, \"motion\": 4}\n",
"\n",
" if blur_max_limits is None:\n",
" blur_max_limits = {\"box\": 4, \"gaussian\": 3, \"motion\": 8}\n",
"\n",
" blur_type = random.choice([\"box\", \"gaussian\", \"motion\"])\n",
"\n",
" if blur_type == \"box\":\n",
" min_radius = blur_min_limits[\"box\"]\n",
" max_radius = blur_max_limits[\"box\"]\n",
" radius = random.randint(min_radius, max_radius)\n",
" img = img.filter(ImageFilter.BoxBlur(radius))\n",
"\n",
" elif blur_type == \"gaussian\":\n",
" min_radius = blur_min_limits[\"gaussian\"]\n",
" max_radius = blur_max_limits[\"gaussian\"]\n",
" radius = random.uniform(min_radius, max_radius)\n",
" img = img.filter(ImageFilter.GaussianBlur(radius))\n",
"\n",
" elif blur_type == \"motion\":\n",
" min_kernel = blur_min_limits[\"motion\"]\n",
" max_kernel = blur_max_limits[\"motion\"]\n",
" kernel_size = random.randint(min_kernel, max_kernel)\n",
" kernel_motion_blur = np.zeros((kernel_size, kernel_size))\n",
" kernel_motion_blur[int((kernel_size - 1) / 2), :] = 1\n",
" kernel_motion_blur /= kernel_size\n",
" img_array = cv2.filter2D(np.array(img), -1, kernel_motion_blur)\n",
" img = Image.fromarray(img_array)\n",
"\n",
" return img\n",
"\n",
"\n",
"def add_noise(img, intensity=30):\n",
" img_array = np.array(img)\n",
" noise = np.random.normal(0, intensity, img_array.shape).astype(np.int32)\n",
" noisy_img = np.clip(img_array + noise, 0, 255).astype(np.uint8)\n",
" return Image.fromarray(noisy_img)\n",
"\n",
"def shift_image(img, max_shift=4):\n",
" \"\"\"\n",
" Сдвигает изображение на случайное количество пикселей по осям X и Y.\n",
"\n",
" :param img: PIL Image объект.\n",
" :param max_shift: Максимальное смещение в пикселях по каждой оси.\n",
" :return: Сдвинутое изображение.\n",
" \"\"\"\n",
" x_shift = random.randint(-max_shift, max_shift)\n",
" y_shift = random.randint(-max_shift, max_shift)\n",
" return img.transform(\n",
" img.size,\n",
" Image.AFFINE,\n",
" (1, 0, x_shift, 0, 1, y_shift),\n",
" fillcolor=255\n",
" )\n",
"\n",
"def create_augmentations(symbol, font_sizes, max_rotation, augmentations, config_lines):\n",
" for i in range(augmentations):\n",
" for font_size in font_sizes:\n",
" current_font = ImageFont.truetype(FONT_PATH, font_size)\n",
"\n",
" # Создаём пустое изображение с белым фоном\n",
" img = Image.new(\"L\", IMG_SIZE, 255)\n",
" draw = ImageDraw.Draw(img)\n",
"\n",
" # Получаем размеры текста с использованием Font.getbbox\n",
" text_bbox = draw.textbbox((0, 0), symbol, font=current_font)\n",
" text_width, text_height = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]\n",
"\n",
" # Рассчитываем координаты для центрирования текста\n",
" x = (IMG_SIZE[0] - text_width) // 2 - text_bbox[0]\n",
" y = (IMG_SIZE[1] - text_height) // 2 - text_bbox[1]\n",
"\n",
" # Рисуем текст\n",
" draw.text((x, y), symbol, font=current_font, fill=0)\n",
"\n",
" # Для первого прогона символа с данным размером шрифта сохраняем без аугментаций\n",
" if i == 0:\n",
" symbol_dir = os.path.join(DATASET_DIR, symbol)\n",
" os.makedirs(symbol_dir, exist_ok=True)\n",
" file_path = os.path.join(symbol_dir, f\"{symbol}_{font_size}_{i}.png\")\n",
" img.save(file_path)\n",
"\n",
" config_lines.append(f\"{file_path},{symbol}\\n\")\n",
" continue\n",
"\n",
" # Случайное смещение символа\n",
" if random.random() < 0.7: \n",
" img = shift_image(img, max_shift=4)\n",
"\n",
" # Случайный поворот изображения\n",
" rotation = random.uniform(-max_rotation, max_rotation)\n",
" img = img.rotate(rotation, expand=False, fillcolor=255)\n",
"\n",
" # Случайное расширение символов\n",
" if random.random() < 0.5:\n",
" kernel_size = (random.randint(1, 2), random.randint(1, 2))\n",
" iterations = 1\n",
" img = expand_characters(img, kernel_size=kernel_size, iterations=iterations)\n",
"\n",
" # Случайное добавление разрывов\n",
" if random.random() < 0.15:\n",
" num_gaps = random.randint(1, 5)\n",
" gap_size = random.randint(2, 4)\n",
" img = add_random_gaps(img, num_gaps=num_gaps, gap_size=gap_size)\n",
"\n",
" # Инверсия цветов с вероятностью 25%\n",
" if random.random() < 0.15:\n",
" img = ImageOps.invert(img)\n",
"\n",
" # Определяем лимиты блюра в зависимости от размера шрифта\n",
" if font_size in SMALL_FONT_SIZES:\n",
" blur_limits = {\"box\": 2, \"gaussian\": 1, \"motion\": 6} # Уменьшенные лимиты\n",
" else:\n",
" blur_limits = {\"box\": 4, \"gaussian\": 3, \"motion\": 8} # Оригинальные лимиты\n",
"\n",
" # Случайное добавление блюра\n",
" if random.random() < 0.85:\n",
" img = apply_blur(img, blur_max_limits=blur_limits)\n",
"\n",
" # Случайное добавление шума\n",
" if random.random() < 0.1:\n",
" noise_intensity = random.randint(10, 20)\n",
" img = add_noise(img, intensity=noise_intensity)\n",
"\n",
" # Сохраняем изображение\n",
" symbol_dir = os.path.join(DATASET_DIR, symbol)\n",
" os.makedirs(symbol_dir, exist_ok=True)\n",
" file_path = os.path.join(symbol_dir, f\"{symbol}_{font_size}_{i}.png\")\n",
" img.save(file_path)\n",
"\n",
" config_lines.append(f\"{file_path},{symbol}\\n\")\n",
"\n",
"def main():\n",
" config_lines = []\n",
"\n",
" for symbol in SYMBOLS:\n",
" create_augmentations(symbol, FONT_SIZES, MAX_ROTATION, AUGMENTATIONS, config_lines)\n",
"\n",
" config_path = os.path.join(DATASET_DIR, CONFIG_FILE)\n",
" with open(config_path, \"w\") as config:\n",
" config.writelines(config_lines)\n",
"\n",
" print(f\"Датасет сохранен в папке '{DATASET_DIR}'.\")\n",
" print(f\"Конфигурационный файл создан: {CONFIG_FILE}\")\n",
"\n",
"def main():\n",
" config_lines = []\n",
"\n",
" for symbol in SYMBOLS:\n",
" create_augmentations(symbol, FONT_SIZES, MAX_ROTATION, AUGMENTATIONS, config_lines)\n",
"\n",
" config_lines += process_gosznak_images(GOSZNAK_DIR, DATASET_DIR, apply_augmentations=True)\n",
"\n",
" config_lines += process_gosznak_images(REAL_GOSZNAK_DIR, DATASET_DIR, apply_augmentations=False)\n",
"\n",
" config_path = os.path.join(DATASET_DIR, CONFIG_FILE)\n",
" with open(config_path, \"w\") as config:\n",
" config.writelines(config_lines)\n",
"\n",
" print(f\"Датасет сохранен в папке '{DATASET_DIR}'.\")\n",
" print(f\"Конфигурационный файл создан: {CONFIG_FILE}\")\n",
"\n",
"if __name__ == \"__main__\":\n",
" main()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Размер тренировочного набора: 29568\n",
"Размер тестового набора: 7392\n"
]
}
],
"source": [
"class LicensePlateDataset(Dataset):\n",
" def __init__(self, dataset_dir, transform=None):\n",
" self.data = []\n",
" self.transform = transform\n",
" with open(os.path.join(dataset_dir, \"dataset_config.txt\"), \"r\") as file:\n",
" for line in file:\n",
" path, label = line.strip().split(\",\")\n",
" self.data.append((path, CLASS_TO_IDX[label]))\n",
"\n",
" def __len__(self):\n",
" return len(self.data)\n",
"\n",
" def __getitem__(self, idx):\n",
" img_path, label = self.data[idx]\n",
" image = Image.open(img_path).convert(\"L\")\n",
" if self.transform:\n",
" image = self.transform(image)\n",
" return image, label\n",
"\n",
"\n",
"transform = transforms.Compose([\n",
" transforms.Resize((28, 28), antialias=True), \n",
" transforms.ToTensor(),\n",
" transforms.RandomResizedCrop(size=(32, 32), scale=(0.8, 1.0), antialias=True),\n",
" transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.1),\n",
" transforms.RandomAffine(degrees=5, translate=(0.1, 0.1), scale=(0.9, 1.1), shear=5),\n",
" transforms.RandomApply([transforms.GaussianBlur(kernel_size=3, sigma=(0.1, 2.0))], p=0.5),\n",
" transforms.RandomErasing(p=0.3, scale=(0.02, 0.1), ratio=(0.3, 3.3)),\n",
" transforms.Normalize(mean=[0.5], std=[0.5])\n",
"])\n",
"\n",
"full_dataset = LicensePlateDataset(DATASET_DIR, transform=transform)\n",
"\n",
"indices = list(range(len(full_dataset)))\n",
"train_indices, test_indices = train_test_split(indices, test_size=0.2, random_state=42)\n",
"\n",
"train_dataset = Subset(full_dataset, train_indices)\n",
"test_dataset = Subset(full_dataset, test_indices)\n",
"\n",
"train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\n",
"test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)\n",
"\n",
"print(f\"Размер тренировочного набора: {len(train_dataset)}\")\n",
"print(f\"Размер тестового набора: {len(test_dataset)}\")\n"
]
},
{
"cell_type": "code",
"execution_count": 56,
"metadata": {},
"outputs": [],
"source": [
"class ImprovedCNN(nn.Module):\n",
" def __init__(self, num_classes):\n",
" super(ImprovedCNN, self).__init__()\n",
" self.conv_layers = nn.Sequential(\n",
" nn.Conv2d(1, 32, kernel_size=2, padding=1),\n",
" nn.LeakyReLU(negative_slope=0.1),\n",
" nn.BatchNorm2d(32),\n",
" nn.MaxPool2d(kernel_size=2, stride=2),\n",
"\n",
" nn.Conv2d(32, 64, kernel_size=3, padding=1),\n",
" nn.ELU(),\n",
" nn.BatchNorm2d(64),\n",
" nn.MaxPool2d(kernel_size=2, stride=2),\n",
"\n",
" nn.Conv2d(64, 128, kernel_size=3, padding=1),\n",
" nn.SiLU(),\n",
" nn.BatchNorm2d(128),\n",
" nn.MaxPool2d(kernel_size=2, stride=2),\n",
"\n",
" nn.Conv2d(128, 256, kernel_size=4, padding=1),\n",
" nn.LeakyReLU(negative_slope=0.1),\n",
" nn.BatchNorm2d(256),\n",
" nn.MaxPool2d(kernel_size=2, stride=2)\n",
" )\n",
" self.fc_layers = nn.Sequential(\n",
" nn.Linear(256, 256),\n",
" nn.GELU(),\n",
" nn.Dropout(0.5),\n",
" nn.Linear(256, num_classes)\n",
" )\n",
"\n",
" def forward(self, x):\n",
" x = self.conv_layers(x)\n",
" x = x.view(x.size(0), -1) \n",
" x = self.fc_layers(x)\n",
" return x"
]
},
{
"cell_type": "code",
"execution_count": 57,
"metadata": {},
"outputs": [],
"source": [
"model = ImprovedCNN(len(CLASSES)).to(DEVICE)\n",
"criterion = nn.CrossEntropyLoss(label_smoothing=0.1)\n",
"optimizer = optim.SGD(model.parameters(), lr=LEARNING_RATE, momentum=0.9, weight_decay=1e-4)\n",
"#optimizer = torch.optim.Adam(model.parameters(), lr=0.01)\n",
"scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=5)"
]
},
{
"cell_type": "code",
"execution_count": 58,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch [1/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 1.4878, Train Accuracy: 70.34%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.9624, Test Accuracy: 90.64%\n",
"Best model saved with test accuracy: 90.64%\n",
"Epoch [2/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.9608, Train Accuracy: 91.21%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.8277, Test Accuracy: 95.06%\n",
"Best model saved with test accuracy: 95.06%\n",
"Epoch [3/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.8817, Train Accuracy: 93.65%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7844, Test Accuracy: 96.32%\n",
"Best model saved with test accuracy: 96.32%\n",
"Epoch [4/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.8468, Train Accuracy: 94.64%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7595, Test Accuracy: 96.78%\n",
"Best model saved with test accuracy: 96.78%\n",
"Epoch [5/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.8135, Train Accuracy: 95.71%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7514, Test Accuracy: 96.74%\n",
"Epoch [6/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7922, Train Accuracy: 96.37%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7277, Test Accuracy: 97.38%\n",
"Best model saved with test accuracy: 97.38%\n",
"Epoch [7/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7822, Train Accuracy: 96.46%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7196, Test Accuracy: 97.66%\n",
"Best model saved with test accuracy: 97.66%\n",
"Epoch [8/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7708, Train Accuracy: 96.68%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7192, Test Accuracy: 97.47%\n",
"Epoch [9/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7652, Train Accuracy: 96.80%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7154, Test Accuracy: 97.46%\n",
"Epoch [10/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7518, Train Accuracy: 97.23%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7117, Test Accuracy: 97.71%\n",
"Best model saved with test accuracy: 97.71%\n",
"Epoch [11/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7467, Train Accuracy: 97.33%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7145, Test Accuracy: 97.39%\n",
"Epoch [12/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7387, Train Accuracy: 97.45%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.7060, Test Accuracy: 97.58%\n",
"Epoch [13/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7375, Train Accuracy: 97.44%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6970, Test Accuracy: 97.94%\n",
"Best model saved with test accuracy: 97.94%\n",
"Epoch [14/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7323, Train Accuracy: 97.72%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6920, Test Accuracy: 98.27%\n",
"Best model saved with test accuracy: 98.27%\n",
"Epoch [15/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7291, Train Accuracy: 97.62%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6895, Test Accuracy: 98.08%\n",
"Epoch [16/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7238, Train Accuracy: 97.80%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6878, Test Accuracy: 98.04%\n",
"Epoch [17/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7195, Train Accuracy: 97.96%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6834, Test Accuracy: 98.17%\n",
"Epoch [18/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7212, Train Accuracy: 97.80%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6863, Test Accuracy: 98.21%\n",
"Epoch [19/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7128, Train Accuracy: 98.19%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6845, Test Accuracy: 98.30%\n",
"Best model saved with test accuracy: 98.30%\n",
"Epoch [20/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7144, Train Accuracy: 98.06%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6818, Test Accuracy: 98.31%\n",
"Best model saved with test accuracy: 98.31%\n",
"Epoch [21/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7103, Train Accuracy: 98.17%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6837, Test Accuracy: 98.39%\n",
"Best model saved with test accuracy: 98.39%\n",
"Epoch [22/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7096, Train Accuracy: 98.12%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6801, Test Accuracy: 98.19%\n",
"Epoch [23/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7069, Train Accuracy: 98.22%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6810, Test Accuracy: 98.15%\n",
"Epoch [24/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7073, Train Accuracy: 98.20%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6748, Test Accuracy: 98.53%\n",
"Best model saved with test accuracy: 98.53%\n",
"Epoch [25/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7019, Train Accuracy: 98.35%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6773, Test Accuracy: 98.19%\n",
"Epoch [26/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7007, Train Accuracy: 98.31%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6742, Test Accuracy: 98.38%\n",
"Epoch [27/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.7010, Train Accuracy: 98.31%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6722, Test Accuracy: 98.58%\n",
"Best model saved with test accuracy: 98.58%\n",
"Epoch [28/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6999, Train Accuracy: 98.32%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6736, Test Accuracy: 98.53%\n",
"Epoch [29/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6981, Train Accuracy: 98.41%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6707, Test Accuracy: 98.62%\n",
"Best model saved with test accuracy: 98.62%\n",
"Epoch [30/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6976, Train Accuracy: 98.36%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" "
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6715, Test Accuracy: 98.48%\n",
"Обучение завершено.\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r"
]
}
],
"source": [
"def train_one_epoch(model, train_loader, optimizer, criterion, device):\n",
" model.train()\n",
" running_loss = 0.0\n",
" correct = 0\n",
" total = 0\n",
"\n",
" train_bar = tqdm(train_loader, desc=\"Training\", leave=False)\n",
" for images, labels in train_bar:\n",
" images, labels = images.to(device), labels.to(device)\n",
"\n",
" optimizer.zero_grad()\n",
" outputs = model(images)\n",
" loss = criterion(outputs, labels)\n",
" loss.backward()\n",
" optimizer.step()\n",
"\n",
" running_loss += loss.item()\n",
" _, predicted = torch.max(outputs, 1)\n",
" total += labels.size(0)\n",
" correct += (predicted == labels).sum().item()\n",
"\n",
" train_bar.set_postfix(loss=f\"{running_loss / len(train_loader):.4f}\", acc=f\"{100 * correct / total:.2f}%\")\n",
"\n",
" train_loss = running_loss / len(train_loader)\n",
" train_accuracy = 100 * correct / total\n",
" return train_loss, train_accuracy\n",
"\n",
"\n",
"def evaluate(model, test_loader, criterion, device):\n",
" model.eval()\n",
" running_loss = 0.0\n",
" correct = 0\n",
" total = 0\n",
"\n",
" with torch.no_grad():\n",
" test_bar = tqdm(test_loader, desc=\"Testing\", leave=False)\n",
" for images, labels in test_bar:\n",
" images, labels = images.to(device), labels.to(device)\n",
"\n",
" outputs = model(images)\n",
" loss = criterion(outputs, labels)\n",
" running_loss += loss.item()\n",
"\n",
" _, predicted = torch.max(outputs, 1)\n",
" total += labels.size(0)\n",
" correct += (predicted == labels).sum().item()\n",
"\n",
" test_loss = running_loss / len(test_loader)\n",
" test_accuracy = 100 * correct / total\n",
" return test_loss, test_accuracy\n",
"\n",
"\n",
"best_accuracy = 0.0\n",
"for epoch in range(EPOCHS):\n",
" print(f\"Epoch [{epoch + 1}/{EPOCHS}]\")\n",
" train_loss, train_accuracy = train_one_epoch(model, train_loader, optimizer, criterion, DEVICE)\n",
" print(f\"Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.2f}%\")\n",
"\n",
" test_loss, test_accuracy = evaluate(model, test_loader, criterion, DEVICE)\n",
" print(f\"Test Loss: {test_loss:.4f}, Test Accuracy: {test_accuracy:.2f}%\")\n",
"\n",
" if test_accuracy > best_accuracy:\n",
" best_accuracy = test_accuracy\n",
" torch.save(model.state_dict(), SAVE_PATH)\n",
" print(f\"Best model saved with test accuracy: {best_accuracy:.2f}%\")\n",
"\n",
" if scheduler is not None:\n",
" scheduler.step(test_loss)\n",
"\n",
"print(\"Обучение завершено.\")\n"
]
},
{
"cell_type": "code",
"execution_count": 59,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch [1/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6945, Train Accuracy: 98.48%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6733, Test Accuracy: 98.39%\n",
"Epoch [2/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6948, Train Accuracy: 98.46%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6739, Test Accuracy: 98.16%\n",
"Epoch [3/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6932, Train Accuracy: 98.45%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6719, Test Accuracy: 98.54%\n",
"Epoch [4/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6927, Train Accuracy: 98.49%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6723, Test Accuracy: 98.47%\n",
"Epoch [5/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6922, Train Accuracy: 98.56%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6768, Test Accuracy: 98.28%\n",
"Epoch [6/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6823, Train Accuracy: 98.76%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6567, Test Accuracy: 98.96%\n",
"Best model saved with test accuracy: 98.96%\n",
"Epoch [7/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6790, Train Accuracy: 98.81%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6565, Test Accuracy: 98.84%\n",
"Epoch [8/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6764, Train Accuracy: 98.97%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6542, Test Accuracy: 98.89%\n",
"Epoch [9/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6734, Train Accuracy: 99.06%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6565, Test Accuracy: 98.82%\n",
"Epoch [10/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6737, Train Accuracy: 99.02%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6530, Test Accuracy: 99.09%\n",
"Best model saved with test accuracy: 99.09%\n",
"Epoch [11/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6714, Train Accuracy: 99.03%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6546, Test Accuracy: 98.90%\n",
"Epoch [12/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6727, Train Accuracy: 99.00%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6534, Test Accuracy: 98.92%\n",
"Epoch [13/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6726, Train Accuracy: 98.94%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6549, Test Accuracy: 98.76%\n",
"Epoch [14/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6707, Train Accuracy: 99.05%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6516, Test Accuracy: 99.07%\n",
"Epoch [15/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6703, Train Accuracy: 99.10%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6541, Test Accuracy: 98.93%\n",
"Epoch [16/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6719, Train Accuracy: 99.02%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6496, Test Accuracy: 98.99%\n",
"Epoch [17/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6705, Train Accuracy: 99.03%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6534, Test Accuracy: 98.89%\n",
"Epoch [18/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6704, Train Accuracy: 99.09%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6491, Test Accuracy: 99.04%\n",
"Epoch [19/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6696, Train Accuracy: 99.02%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6553, Test Accuracy: 98.73%\n",
"Epoch [20/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6693, Train Accuracy: 99.12%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6515, Test Accuracy: 99.01%\n",
"Epoch [21/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6686, Train Accuracy: 99.13%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6479, Test Accuracy: 99.08%\n",
"Epoch [22/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6675, Train Accuracy: 99.20%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6485, Test Accuracy: 99.04%\n",
"Epoch [23/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6674, Train Accuracy: 99.23%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6466, Test Accuracy: 99.03%\n",
"Epoch [24/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6687, Train Accuracy: 99.07%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6466, Test Accuracy: 99.22%\n",
"Best model saved with test accuracy: 99.22%\n",
"Epoch [25/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6672, Train Accuracy: 99.20%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6505, Test Accuracy: 98.99%\n",
"Epoch [26/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6679, Train Accuracy: 99.22%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6492, Test Accuracy: 99.05%\n",
"Epoch [27/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6690, Train Accuracy: 99.04%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6485, Test Accuracy: 99.09%\n",
"Epoch [28/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6705, Train Accuracy: 99.00%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6498, Test Accuracy: 98.97%\n",
"Epoch [29/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6664, Train Accuracy: 99.20%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6498, Test Accuracy: 98.93%\n",
"Epoch [30/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train Loss: 0.6663, Train Accuracy: 99.20%\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" "
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test Loss: 0.6512, Test Accuracy: 98.85%\n",
"Обучение завершено.\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r"
]
}
],
"source": [
"for epoch in range(EPOCHS):\n",
" print(f\"Epoch [{epoch + 1}/{EPOCHS}]\")\n",
" train_loss, train_accuracy = train_one_epoch(model, train_loader, optimizer, criterion, DEVICE)\n",
" print(f\"Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.2f}%\")\n",
"\n",
" test_loss, test_accuracy = evaluate(model, test_loader, criterion, DEVICE)\n",
" print(f\"Test Loss: {test_loss:.4f}, Test Accuracy: {test_accuracy:.2f}%\")\n",
"\n",
" if test_accuracy > best_accuracy:\n",
" best_accuracy = test_accuracy\n",
" torch.save(model.state_dict(), SAVE_PATH)\n",
" print(f\"Best model saved with test accuracy: {best_accuracy:.2f}%\")\n",
"\n",
" if scheduler is not None:\n",
" scheduler.step(test_loss)\n",
"\n",
"print(\"Обучение завершено.\")"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch [1/30]\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" \r"
]
},
{
"ename": "KeyboardInterrupt",
"evalue": "",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
"Cell \u001b[1;32mIn[35], line 13\u001b[0m\n\u001b[0;32m 10\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEpoch [\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mepoch\u001b[38;5;241m+\u001b[39m\u001b[38;5;241m1\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m/\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mEPOCHS\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m]\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 11\u001b[0m train_bar \u001b[38;5;241m=\u001b[39m tqdm(train_loader, desc\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTraining\u001b[39m\u001b[38;5;124m\"\u001b[39m, leave\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[1;32m---> 13\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m images, labels \u001b[38;5;129;01min\u001b[39;00m train_bar:\n\u001b[0;32m 14\u001b[0m images, labels \u001b[38;5;241m=\u001b[39m images\u001b[38;5;241m.\u001b[39mto(DEVICE), labels\u001b[38;5;241m.\u001b[39mto(DEVICE)\n\u001b[0;32m 15\u001b[0m optimizer\u001b[38;5;241m.\u001b[39mzero_grad()\n",
"File \u001b[1;32mc:\\Users\\leonk\\Documents\\code\\number-plate-study\\.venv\\lib\\site-packages\\tqdm\\std.py:1181\u001b[0m, in \u001b[0;36mtqdm.__iter__\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 1178\u001b[0m time \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_time\n\u001b[0;32m 1180\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m-> 1181\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m obj \u001b[38;5;129;01min\u001b[39;00m iterable:\n\u001b[0;32m 1182\u001b[0m \u001b[38;5;28;01myield\u001b[39;00m obj\n\u001b[0;32m 1183\u001b[0m \u001b[38;5;66;03m# Update and possibly print the progressbar.\u001b[39;00m\n\u001b[0;32m 1184\u001b[0m \u001b[38;5;66;03m# Note: does not call self.update(1) for speed optimisation.\u001b[39;00m\n",
"File \u001b[1;32mc:\\Users\\leonk\\Documents\\code\\number-plate-study\\.venv\\lib\\site-packages\\torch\\utils\\data\\dataloader.py:633\u001b[0m, in \u001b[0;36m_BaseDataLoaderIter.__next__\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 630\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_sampler_iter \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m 631\u001b[0m \u001b[38;5;66;03m# TODO(https://github.com/pytorch/pytorch/issues/76750)\u001b[39;00m\n\u001b[0;32m 632\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_reset() \u001b[38;5;66;03m# type: ignore[call-arg]\u001b[39;00m\n\u001b[1;32m--> 633\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_next_data\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 634\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_num_yielded \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m 635\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_dataset_kind \u001b[38;5;241m==\u001b[39m _DatasetKind\u001b[38;5;241m.\u001b[39mIterable \u001b[38;5;129;01mand\u001b[39;00m \\\n\u001b[0;32m 636\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_IterableDataset_len_called \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m \\\n\u001b[0;32m 637\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_num_yielded \u001b[38;5;241m>\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_IterableDataset_len_called:\n",
"File \u001b[1;32mc:\\Users\\leonk\\Documents\\code\\number-plate-study\\.venv\\lib\\site-packages\\torch\\utils\\data\\dataloader.py:677\u001b[0m, in \u001b[0;36m_SingleProcessDataLoaderIter._next_data\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 675\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_next_data\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[0;32m 676\u001b[0m index \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_next_index() \u001b[38;5;66;03m# may raise StopIteration\u001b[39;00m\n\u001b[1;32m--> 677\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_dataset_fetcher\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfetch\u001b[49m\u001b[43m(\u001b[49m\u001b[43mindex\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# may raise StopIteration\u001b[39;00m\n\u001b[0;32m 678\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_pin_memory:\n\u001b[0;32m 679\u001b[0m data \u001b[38;5;241m=\u001b[39m _utils\u001b[38;5;241m.\u001b[39mpin_memory\u001b[38;5;241m.\u001b[39mpin_memory(data, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_pin_memory_device)\n",
"File \u001b[1;32mc:\\Users\\leonk\\Documents\\code\\number-plate-study\\.venv\\lib\\site-packages\\torch\\utils\\data\\_utils\\fetch.py:51\u001b[0m, in \u001b[0;36m_MapDatasetFetcher.fetch\u001b[1;34m(self, possibly_batched_index)\u001b[0m\n\u001b[0;32m 49\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdataset\u001b[38;5;241m.\u001b[39m__getitems__(possibly_batched_index)\n\u001b[0;32m 50\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m---> 51\u001b[0m data \u001b[38;5;241m=\u001b[39m [\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdataset[idx] \u001b[38;5;28;01mfor\u001b[39;00m idx \u001b[38;5;129;01min\u001b[39;00m possibly_batched_index]\n\u001b[0;32m 52\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 53\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdataset[possibly_batched_index]\n",
"File \u001b[1;32mc:\\Users\\leonk\\Documents\\code\\number-plate-study\\.venv\\lib\\site-packages\\torch\\utils\\data\\_utils\\fetch.py:51\u001b[0m, in \u001b[0;36m<listcomp>\u001b[1;34m(.0)\u001b[0m\n\u001b[0;32m 49\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdataset\u001b[38;5;241m.\u001b[39m__getitems__(possibly_batched_index)\n\u001b[0;32m 50\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m---> 51\u001b[0m data \u001b[38;5;241m=\u001b[39m [\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdataset\u001b[49m\u001b[43m[\u001b[49m\u001b[43midx\u001b[49m\u001b[43m]\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m idx \u001b[38;5;129;01min\u001b[39;00m possibly_batched_index]\n\u001b[0;32m 52\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 53\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdataset[possibly_batched_index]\n",
"File \u001b[1;32mc:\\Users\\leonk\\Documents\\code\\number-plate-study\\.venv\\lib\\site-packages\\torch\\utils\\data\\dataset.py:298\u001b[0m, in \u001b[0;36mSubset.__getitem__\u001b[1;34m(self, idx)\u001b[0m\n\u001b[0;32m 296\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(idx, \u001b[38;5;28mlist\u001b[39m):\n\u001b[0;32m 297\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdataset[[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mindices[i] \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m idx]]\n\u001b[1;32m--> 298\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdataset\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindices\u001b[49m\u001b[43m[\u001b[49m\u001b[43midx\u001b[49m\u001b[43m]\u001b[49m\u001b[43m]\u001b[49m\n",
"Cell \u001b[1;32mIn[32], line 15\u001b[0m, in \u001b[0;36mLicensePlateDataset.__getitem__\u001b[1;34m(self, idx)\u001b[0m\n\u001b[0;32m 13\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__getitem__\u001b[39m(\u001b[38;5;28mself\u001b[39m, idx):\n\u001b[0;32m 14\u001b[0m img_path, label \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdata[idx]\n\u001b[1;32m---> 15\u001b[0m image \u001b[38;5;241m=\u001b[39m \u001b[43mImage\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\u001b[43mimg_path\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mconvert(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mL\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 16\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtransform:\n\u001b[0;32m 17\u001b[0m image \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtransform(image)\n",
"File \u001b[1;32mc:\\Users\\leonk\\Documents\\code\\number-plate-study\\.venv\\lib\\site-packages\\PIL\\Image.py:3247\u001b[0m, in \u001b[0;36mopen\u001b[1;34m(fp, mode, formats)\u001b[0m\n\u001b[0;32m 3244\u001b[0m filename \u001b[38;5;241m=\u001b[39m fp\n\u001b[0;32m 3246\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m filename:\n\u001b[1;32m-> 3247\u001b[0m fp \u001b[38;5;241m=\u001b[39m \u001b[43mbuiltins\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfilename\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 3248\u001b[0m exclusive_fp \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m 3250\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n",
"\u001b[1;31mKeyboardInterrupt\u001b[0m: "
]
}
],
"source": [
"from tqdm import tqdm\n",
"\n",
"best_accuracy = 0.0\n",
"for epoch in range(EPOCHS):\n",
" model.train()\n",
" running_loss = 0.0\n",
" correct_train = 0\n",
" total_train = 0\n",
"\n",
" print(f\"Epoch [{epoch+1}/{EPOCHS}]\")\n",
" train_bar = tqdm(train_loader, desc=\"Training\", leave=False)\n",
"\n",
" for images, labels in train_bar:\n",
" images, labels = images.to(DEVICE), labels.to(DEVICE)\n",
" optimizer.zero_grad()\n",
" outputs = model(images)\n",
" loss = criterion(outputs, labels)\n",
" loss.backward()\n",
" optimizer.step()\n",
"\n",
" running_loss += loss.item()\n",
" _, predicted = torch.max(outputs, 1)\n",
" total_train += labels.size(0)\n",
" correct_train += (predicted == labels).sum().item()\n",
"\n",
" train_bar.set_postfix(loss=f\"{running_loss/len(train_loader):.4f}\", acc=f\"{100 * correct_train / total_train:.2f}%\")\n",
"\n",
" train_accuracy = 100 * correct_train / total_train\n",
" print(f\"Train Loss: {running_loss/len(train_loader):.4f}, Train Accuracy: {train_accuracy:.2f}%\")\n",
"\n",
" model.eval()\n",
" correct_test = 0\n",
" total_test = 0\n",
" test_loss = 0.0\n",
"\n",
" with torch.no_grad():\n",
" test_bar = tqdm(test_loader, desc=\"Testing\", leave=False)\n",
" for images, labels in test_bar:\n",
" images, labels = images.to(DEVICE), labels.to(DEVICE)\n",
"\n",
" outputs = model(images)\n",
" loss = criterion(outputs, labels)\n",
" test_loss += loss.item()\n",
"\n",
" _, predicted = torch.max(outputs, 1)\n",
" total_test += labels.size(0)\n",
" correct_test += (predicted == labels).sum().item()\n",
"\n",
" test_accuracy = 100 * correct_test / total_test\n",
" print(f\"Test Loss: {test_loss/len(test_loader):.4f}, Test Accuracy: {test_accuracy:.2f}%\")\n",
"\n",
" if test_accuracy > best_accuracy:\n",
" best_accuracy = test_accuracy\n",
" torch.save(model.state_dict(), SAVE_PATH)\n",
" print(f\"Best model saved with test accuracy: {best_accuracy:.2f}%\")\n",
"\n",
"print(\"Обучение завершено.\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"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.13"
}
},
"nbformat": 4,
"nbformat_minor": 2
}