diff --git a/Challenge/README.md b/Challenge/README.md index a378107..b543e1d 100644 --- a/Challenge/README.md +++ b/Challenge/README.md @@ -5,11 +5,11 @@ The no-save notebook links are here: | Challenge Number | Notebook | | ------------- |-------------| -| 1 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials_2025/blob/main/Challenge/challenges/challenge_01.ipynb) | -| 2 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials_2025/blob/main/Challenge/challenges/challenge_02.ipynb) | -| 3 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials_2025/blob/main/Challenge/challenges/challenge_03.ipynb) | -| 4 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials_2025/blob/main/Challenge/challenges/challenge_04.ipynb) | -| 5 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials_2025/blob/main/Challenge/challenges/challenge_05.ipynb) | +| 1 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials/blob/main/Challenge/challenges/challenge01.ipynb) | +| 2 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials/blob/main/Challenge/challenges/challenge02.ipynb) | +| 3 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials/blob/main/Challenge/challenges/challenge03.ipynb) | +| 4 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials/blob/main/Challenge/challenges/challenge04.ipynb) | +| 5 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/BNL-Fermilab-RENEW/tutorials/blob/main/Challenge/challenges/challenge05.ipynb) | At the end of the challenge, you will have to present your work. diff --git a/Challenge/challenge_utilities.py b/Challenge/challenge_utilities.py index 0e9bbb4..a78d73f 100644 --- a/Challenge/challenge_utilities.py +++ b/Challenge/challenge_utilities.py @@ -2,155 +2,284 @@ import numpy as np import matplotlib.pyplot as plt from deepbench.astro_object import StarObject, GalaxyObject -import math -import tensorflow as tf +import torch +from torch.utils.data import Dataset, DataLoader from sklearn.metrics import roc_curve, confusion_matrix +from typing import TYPE_CHECKING +import torchvision +if TYPE_CHECKING: + from collections.abc import Callable -class SkyGeneratorTrue(tf.keras.utils.Sequence): - def __init__(self, n_samples, image_size=28, pre_processing=None, train=True, shuffle=False, batch_size=64): +class SkyDataset(Dataset): + """PyTorch Dataset for generating sky images.""" + def __init__(self, n_samples:int, image_size: int=28, shuffle:bool=False, seed:int=42): self.n_samples = n_samples - self.train = train - self.pre_processing = pre_processing - self.shuffle = shuffle - self.image_size = image_size - self.noise_level = 0.05 + self.seed = seed + if not hasattr(self, 'noise_level'): + self.noise_level = 0.05 # Default noise level, can be overridden in subclasses - self.rng = np.random.default_rng(seed=42) # Seed for the main notebook - self.batch_size=batch_size + # Generate all data upfront + self.rng = np.random.default_rng(seed=seed) self.labels = self.decide_labels() - - def decide_labels(self): + self.images = self._generate_all_images() + + def _generate_all_images(self): + """Generate all images once and store them.""" + images = [] + for label in self.labels: + images.append(self.generate_image(label)) + return np.stack(images) + + def decide_labels(self): + """Override in subclasses to change label distribution.""" n_stars = (self.rng.integers(low=int(.45*self.n_samples), high=int(.65*self.n_samples))) - n_galaxies = self.n_samples-n_stars + n_galaxies = self.n_samples - n_stars labels = [0 for _ in range(n_stars)] + [1 for _ in range(n_galaxies)] - - if self.shuffle: + + if self.shuffle: self.rng.shuffle(labels) - + return np.asarray(labels) - - def generate_image(self, label): - radius = self.rng.integers(low=1, high=self.image_size/2) + + def generate_image(self, label:int): + """Override in subclasses to change image generation.""" + radius = self.rng.integers(low=1, high=self.image_size // 2) center_x = self.rng.integers(low=1, high=self.image_size) center_y = self.rng.integers(low=1, high=self.image_size) - - if label == 0: + + if label == 0: image = StarObject( - image_dimensions=(self.image_size, self.image_size), + image_dimensions=(self.image_size, self.image_size), noise_level=self.noise_level, radius=radius - ).create_object( - center_x=center_x, center_y=center_y - ) - - else: + ).create_object( + center_x=center_x, center_y=center_y + ) + else: image = GalaxyObject( - image_dimensions=(self.image_size, self.image_size), + image_dimensions=(self.image_size, self.image_size), noise_level=self.noise_level, radius=radius - ).create_object( - center_x=center_x, center_y=center_y - ) + ).create_object( + center_x=center_x, center_y=center_y + ) + + return image + + def get(self, idx:int) -> tuple[torch.Tensor, torch.Tensor]: + return self.__getitem__(idx) - if self.pre_processing is not None: - image = self.pre_processing(image) + def __len__(self) -> int: + return len(self.labels) + + def __getitem__(self, idx: int) -> tuple[torch.Tensor, torch.Tensor]: + image = self.images[idx] + label = self.labels[idx] + + # Convert to torch tensors + image = torch.from_numpy(image).float() + label = torch.tensor(label, dtype=torch.long) + + # Add channel dimension if needed (for grayscale images) + if image.dim() == 2: + image = image.unsqueeze(0) + + return image, label - return image +class SkyGenerator: + """Factory class that returns PyTorch DataLoader for SkyDataset.""" + def __init__(self, n_samples: int, dataset, shuffle: bool=False, batch_size: int=64, seed: int=42, transform: "Callable | None"=None): + self.dataset = dataset(n_samples, seed=seed, transform=transform, shuffle=shuffle) + self.shuffle = shuffle + self.batch_size = batch_size + + def get_dataloader(self): + """Return a PyTorch DataLoader.""" + return DataLoader( + self.dataset, + batch_size=self.batch_size, + shuffle=self.shuffle, + ) + def __len__(self): - return math.ceil(self.n_samples / self.batch_size) - - def __getitem__(self, idx): - low = idx * self.batch_size - high = min(low + self.batch_size, len(self.labels)) - batch_y = self.labels[low:high] - batch_x = np.zeros((len(batch_y), self.image_size, self.image_size)) - for index, label in enumerate(batch_y): - batch_x[index] = self.generate_image(label) - return batch_x, batch_y + return len(self.dataset) +class TestDataset(SkyDataset): + def __init__(self, n_samples, seed=42, shuffle=False): + super().__init__(n_samples, image_size=64, seed=seed, shuffle=shuffle) -class SkyGenerator01(SkyGeneratorTrue): - def __init__(self, n_samples, pre_processing=None, train=True, shuffle=False, batch_size=64): - image_size = 28 - super().__init__(n_samples, image_size, pre_processing, train, shuffle, batch_size) +class SkyDataset01(SkyDataset): + def __init__(self, n_samples, seed=42, shuffle=False): + + self.size_options = [i for i in range(10, 30)] + self.random_augment = torchvision.transforms.v2.RandomApply([ + torchvision.transforms.v2.CenterCrop(size=16), + torchvision.transforms.v2.ColorJitter(brightness=0.5), + torchvision.transforms.v2.ColorJitter(contrast=0.5), + torchvision.transforms.v2.ColorJitter(saturation=0.5), + ]) + super().__init__(n_samples, image_size=64, seed=seed, shuffle=shuffle) -class SkyGenerator02(SkyGeneratorTrue): - def __init__(self, n_samples, pre_processing=None, train=True, shuffle=False, batch_size=64): - image_size = 28 - super().__init__(n_samples, image_size, pre_processing, train, shuffle, batch_size) + def generate_image(self, label): + image_size = int(self.image_size + self.rng.choice(self.size_options)) - def decide_labels(self): + radius = self.rng.integers(low=1, high=image_size // 2) + center_x = self.rng.integers(low=1, high=image_size) + center_y = self.rng.integers(low=1, high=image_size) + + if label == 0: + image = StarObject( + image_dimensions=(image_size, image_size), + noise_level=self.noise_level, + radius=radius + ).create_object( + center_x=center_x, center_y=center_y + ) + else: + image = GalaxyObject( + image_dimensions=(image_size, image_size), + noise_level=self.noise_level, + radius=radius + ).create_object( + center_x=center_x, center_y=center_y + ) + img = torch.from_numpy(image).float().unsqueeze(dim=0) + transformed_image = torchvision.transforms.v2.Resize(self.image_size)(self.random_augment(img)) + return transformed_image + +class SkyDataset02(SkyDataset): + def __init__(self, n_samples, seed=42, shuffle=False): + super().__init__(n_samples, image_size=64, seed=seed, shuffle=shuffle) + + def decide_labels(self): n_stars = (self.rng.integers(low=int(.85*self.n_samples), high=int(.95*self.n_samples))) - n_galaxies = self.n_samples-n_stars + n_galaxies = self.n_samples - n_stars labels = [0 for _ in range(n_stars)] + [1 for _ in range(n_galaxies)] - - if self.shuffle: + + if self.shuffle: self.rng.shuffle(labels) - + return np.asarray(labels) - -class SkyGenerator03(SkyGeneratorTrue): - def __init__(self, n_samples, pre_processing=None, train=True, shuffle=False, batch_size=64): - image_size = 64 - super().__init__(n_samples, image_size, pre_processing, train, shuffle, batch_size) + + +class SkyDataset03(SkyDataset): + def __init__(self, n_samples, seed=42, shuffle=False): + super().__init__(n_samples, image_size=28, seed=seed, shuffle=shuffle) def decide_labels(self): - n_stars = int((self.rng.integers(low=int(.15*self.n_samples), high=int(.25*self.n_samples)))) - n_galaxies = int(.5 * (self.n_samples-n_stars)) - wild_card = self.n_samples - (n_stars+n_galaxies) + n_stars = int((self.rng.integers(low=int(.2*self.n_samples), high=int(.3*self.n_samples)))) + n_galaxies = int(.5 * (self.n_samples - n_stars)) + wild_card = self.n_samples - (n_stars + n_galaxies) labels = [0 for _ in range(n_stars)] + [1 for _ in range(n_galaxies)] + [2 for _ in range(wild_card)] - - if self.shuffle: + + if self.shuffle: self.rng.shuffle(labels) - + return np.asarray(labels) def generate_image(self, label): - if label == 2: - return np.zeros((self.image_size, self.image_size)) - else: + if label == 2: + return torch.randn((self.image_size, self.image_size)) * self.noise_level # Generate random noise for wildcard class + else: return super().generate_image(label) def __getitem__(self, idx): - items, labels = super().__getitem__(idx) - labels[labels == 2] = 0 - return items, labels + image, label = super().__getitem__(idx) + # Convert label 2 (wildcard) to label 0 (Star) + if self.rng.random() < 0.5: + convert = torch.tensor(0) + else: + convert = torch.tensor(1) + label = torch.where(label == 2, convert, label) + return image, label -class SkyGenerator04(SkyGeneratorTrue): - def __init__(self, n_samples, pre_processing=None, train=True, shuffle=False, batch_size=64): - image_size = 28 - super().__init__(n_samples, image_size, pre_processing, train, shuffle, batch_size) - - def decide_labels(self): + +class SkyDataset04(SkyDataset): + def __init__(self, n_samples, seed=42, shuffle=False): + super().__init__(n_samples, image_size=28, seed=seed, shuffle=shuffle) + + def decide_labels(self): n_stars = (self.rng.integers(low=int(.45*self.n_samples), high=int(.65*self.n_samples))) - n_galaxies = (self.rng.integers(low=int(.6*(self.n_samples-n_stars)), high=int(.9*(self.n_samples-n_stars)))) - wild_card = self.n_samples - (n_stars+n_galaxies) + n_galaxies = (self.rng.integers(low=int(.6*(self.n_samples - n_stars)), high=int(.9*(self.n_samples - n_stars)))) + wild_card = self.n_samples - (n_stars + n_galaxies) labels = [0 for _ in range(n_stars)] + [1 for _ in range(n_galaxies)] + [2 for _ in range(wild_card)] - - if self.shuffle: + + if self.shuffle: self.rng.shuffle(labels) - + return np.asarray(labels) -class SkyGenerator05(SkyGeneratorTrue): - def __init__(self, n_samples, pre_processing=None, train=True, shuffle=False, batch_size=64): - image_size = 28 - super().__init__(n_samples, image_size, pre_processing, train, shuffle, batch_size) + def __getitem__(self, idx): + image, label = super().__getitem__(idx) + return image.numpy(), label.numpy() + + +class SkyDataset05(SkyDataset): + def __init__(self, n_samples:int, image_size: int=28, shuffle:bool=False, seed:int=42): + self.noise_level = 0.25 + self.regression_labels = torch.zeros(n_samples, 3, dtype=torch.float32) + super().__init__(n_samples, image_size=image_size, shuffle=shuffle, seed=seed) + + def _generate_all_images(self): + """Generate all images once and store them.""" + images = [] + for index, label in enumerate(self.labels): + image, reg_label = self.generate_image(label) + images.append(image) + self.regression_labels[index] = reg_label + return np.stack(images) + + def generate_image(self, label:int): + """Override in subclasses to change image generation.""" + radius = self.rng.integers(low=1, high=self.image_size // 2) + center_x = self.rng.integers(low=1, high=self.image_size) + center_y = self.rng.integers(low=1, high=self.image_size) + regression_label = torch.tensor([center_x, center_y, radius], dtype=torch.float32) + + if label == 0: + image = StarObject( + image_dimensions=(self.image_size, self.image_size), + noise_level=self.noise_level, + radius=radius + ).create_object( + center_x=center_x, center_y=center_y + ) + else: + image = GalaxyObject( + image_dimensions=(self.image_size, self.image_size), + noise_level=self.noise_level, + radius=radius + ).create_object( + center_x=center_x, center_y=center_y + ) + + return image, regression_label + + def __getitem__(self, idx: int) -> tuple[torch.Tensor, torch.Tensor]: + image = self.images[idx] + label = self.regression_labels[idx].to(torch.float32) + + # Convert to torch tensors + image = torch.from_numpy(image).float() + + # Add channel dimension if needed (for grayscale images) + if image.dim() == 2: + image = image.unsqueeze(0) + + return image, label class Eval: @staticmethod - def plot_loss_history(history): - loss = history['loss'] - epochs = range(len(loss)) + def plot_loss_history(train_loss, val_loss): + epochs = range(len(train_loss)) - val_loss = history['val_loss'] - plt.plot(epochs, loss, label="Train", marker='o') + plt.plot(epochs, train_loss, label="Train", marker='o') plt.plot(epochs, val_loss, label='Validation', marker='x') plt.title("Loss History") diff --git a/Challenge/challenges/challenge01.ipynb b/Challenge/challenges/challenge01.ipynb new file mode 100644 index 0000000..32e8370 --- /dev/null +++ b/Challenge/challenges/challenge01.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "jToNcYCsARAt" + }, + "source": [ + "## Model Training Challenge!\n", + "\n", + "In this notebook, there are a whole host of weird errors.\n", + "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking.\n", + "Do your best to sniff out the mistakes in both code and training procedure!\n", + "\n", + "Your mission, should you choose to accept it, is:\n", + "* Work together in groups of 4 to complete this notebook\n", + "* Figure out all the coding mistakes and make a notebook that runs\n", + "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", + "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RX8h5B-GARAt" + }, + "source": [ + "This line below installs a non-standard data generation package.\n", + "It is designed to make images for benchmarking computer vision problems, and is the back bone of your generator.\n", + "Unfortunately, it's not included with colab (which makes sense....), so we have to install it.\n", + "The `!` point here means \"Execute this line like it's a line in a bash prompt\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VPUeGpTrARAu" + }, + "outputs": [], + "source": [ + "! pip install deepbench" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xatOAXZ6ARAu" + }, + "source": [ + "`curl` is a package for downloading files off the internet.\n", + "This below line just downloads a file form the repo into this colab instance, and names it \"challenge_utilities.py\".\n", + "This file is also in the github repo, you can look at it there if you want.\n", + "It contains the data generator for each for your challenges, and the utilities for plotting and evaluating results.\n", + "(It won't contain any problems for you to solve, it's just there to keep this notebook from getting cluttered.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Kjo3hTS_ARAu" + }, + "outputs": [], + "source": [ + "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials/refs/heads/main/Challenge/challenge_utilities.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MKc8rBCmARAu" + }, + "outputs": [], + "source": [ + "# Now we'll import the different classes from this new file\n", + "# Import the dataloader and dataset classes\n", + "from challenge_utilities import SkaiDataset01 as SkyDataset # Dataset object itself\n", + "# A subclass of torch.utils.data.Dataset\n", + "from challenge_utilities import SkyGenerator, TestDataset # Generic dataloader object, same for all challenges\n", + "from challenge_utilities import Eval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2IXmuM_EARAv" + }, + "outputs": [], + "source": [ + "# Standard packages\n", + "import matplotlib.pyplot as plt\n", + "from tqdm import tqdm\n", + "\n", + "import torchvision\n", + "from torchsummary import summary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W9XZQhfDARAv" + }, + "source": [ + "#### Package documentation\n", + "\n", + "If you get stuck for syntax or anything - these are the packages used.\n", + "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names.\n", + "\n", + "[MatPlotLib](https://matplotlib.org/)\n", + "\n", + "[Pytorch](https://docs.pytorch.org/docs/)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m5EFoTAgARAv" + }, + "source": [ + "# Exploratory Data Analysis\n", + "\n", + "Understanding your data is a critical part of any AI/ML project.\n", + "Make sure you look at your data and understand the differences between each class.\n", + "\n", + "Something to note, which you would only know if you gathered the data yourself, is that the binary \"0\" and \"1\" labels correspond to \"stars\" and \"galaxies\".\n", + "You can use this to label your plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "66Gal0ukARAv" + }, + "outputs": [], + "source": [ + "\n", + "label_map = {\n", + " 0: \"Star\",\n", + " 1: \"Galaxy\"\n", + "}\n", + "\n", + "def plot_samples(generator, n_columns=3, n_rows=6, label_map:dict|None=None):\n", + " _, subplots = plt.subplots(n_columns, n_rows) # Make 9 plots in a 3 x 3 grid\n", + " plt.tight_layout()\n", + " plt.setp(subplots, xticks=[], yticks=[])\n", + "\n", + " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()):\n", + " image, label = generator.get(sample_index)\n", + " subplot.imshow(image)\n", + " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels).\n", + " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", + "\n", + " string_label = \"??????\"\n", + " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", + "\n", + "samples = SkyDataset(n_samples=9, seed=42) # Can just get a few samples\n", + "plot_samples(samples, label_map=label_map)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VTG-xMYWARAw" + }, + "source": [ + "# Look at the input data\n", + "\n", + "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6FJGVsZgARAw" + }, + "outputs": [], + "source": [ + "# These two generators produce the data we'll train with\n", + "\n", + "n_train_samples = 20\n", + "train_generator = SkyGenerator(n_samples=n_train_samples, dataset=SkyDataset, shuffle=True)\n", + "\n", + "n_val_samples = 1280\n", + "val_generator = SkyGenerator(n_samples=n_val_samples, dataset=SkyDataset, shuffle=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KM_sOu9sARAw" + }, + "outputs": [], + "source": [ + "# Each dataset object has a \"labels\" and \"images\" attribute, which we can use to check out the data range\n", + "\n", + "fig, ax = plt.subplots(1,2)\n", + "ax[0].hist(train_generator.dataset.labels)\n", + "ax[0].set_title(\"Train labels\")\n", + "ax[1].hist(val_generator.dataset.labels)\n", + "ax[1].set_title(\"Val labels\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TwAsJhDMARAw" + }, + "outputs": [], + "source": [ + "# We can also look at different attributes of the images\n", + "# Here, let's look at max values and mean values\n", + "\n", + "# These numbers will be slightly different every time, there's randomness controlling how the images are generated\n", + "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", + "\n", + "# Normally we would also have some metadata to work with, but here we only have images\n", + "\n", + "train_means = train_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "val_means = val_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "figure, subplots = plt.subplots(1, 2)\n", + "subplots[0].hist(train_means, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[0].hist(val_means, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[0].legend()\n", + "subplots[0].set_xlabel(\"Mean pixel value\")\n", + "\n", + "train_maxes = train_generator.dataset.images.max(axis=(1, 0))\n", + "val_maxes = val_generator.dataset.images.max(axis=(1, 0))\n", + "subplots[1].hist(train_maxes, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[1].hist(val_maxes, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[1].set_xlabel(\"Max pixel value\")\n", + "\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NhRpPEjdARAw" + }, + "source": [ + "## Data Processing\n", + "\n", + "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", + "There are near infinite ways to pre-process, especially in the computer vision space.\n", + "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways.\n", + "\n", + "For this, let's try bringing all the pixels in the image between 0 and 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Wp_k5XTiARAw" + }, + "outputs": [], + "source": [ + "transforms = torchvision.transforms.Compose([\n", + " torchvision.transforms.RandomResizedCrop(32, 164), # Resize the images to 64 x 64 pixels\n", + " torchvision.transforms.Normalize(0.5, 0.5) # Normalize the pixel values to be between 0 and 1\n", + "])\n", + "\n", + "# We can apply other transforms, in a process called \"data augmentation\"\n", + "# It helps make the model less likely to overfit by giving it different versions of the same data during the training process\n", + "# All transforms can be seen here - https://docs.pytorch.org/vision/0.26/transforms.html#v2-api-reference-recommended" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6gXn05XNARAx" + }, + "outputs": [], + "source": [ + "# Look at some processed samples!\n", + "\n", + "n_samples = 9\n", + "samples = SkyGenerator(n_samples=n_samples, batch_size=1, dataset=SkyDataset).get_dataloader()\n", + "for image, label in samples:\n", + " image = transforms(image)\n", + " plt.imshow(image[0][0]) # The image size is given as (batch, im channels, im_size, im_size)\n", + " plt.xlabel(label_map[label.item()])\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HJEKKWHpARAx" + }, + "source": [ + "## Make the binary classification model\n", + "\n", + "This function, when called, produces a keras Model instance that you can train to predict a class of an input.\n", + "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1.\n", + "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out.\n", + "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it.\n", + "\n", + "In this case, what we want:\n", + "* Take the input images from the data generator\n", + "* Apply two convolutional blocks to the input image\n", + "* Decode the second convolution block's output to a probability of the image being a given class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yb6L7SDoARAx" + }, + "outputs": [], + "source": [ + "# Create a simple convolusional model\n", + "\n", + "class SimpleCNN(torch.nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=3, padding=1)\n", + " self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=3, padding=1)\n", + " self.conv3 = torch.nn.Conv2d(32, 64, kernel_size=3, padding=1)\n", + "\n", + " # Pooling layer\n", + " self.pool = torch.nn.MaxPool2d(2, 2)\n", + "\n", + " # Fully connected layers\n", + " # After 3 pooling layers: 64 -> 32 -> 16 -> 8, so feature map is 8x8\n", + " # 64 channels * 8x8 = 4096 input features\n", + " self.fc1 = torch.nn.Linear(64 * 8 * 8, 128)\n", + " self.fc2 = torch.nn.Linear(128, 1)\n", + "\n", + " # Dropout\n", + " self.dropout = torch.nn.Dropout(0.3)\n", + "\n", + " # Activation\n", + " self.relu = torch.nn.ReLU()\n", + "\n", + " def forward(self, x):\n", + " # Input: (batch_size, 64, 64) - need to add channel dimension\n", + " if x.dim() == 3:\n", + " \"???\"\n", + " # Conv block 1: (batch_size, 1, 64, 64) -> (batch_size, 16, 32, 32)\n", + " x = self.pool(self.relu(self.conv1(x)))\n", + "\n", + " # Conv block 2: (batch_size, 16, 32, 32) -> (batch_size, 32, 16, 16)\n", + " x = self.pool(self.relu(self.conv2(x)))\n", + "\n", + " # Conv block 3: (batch_size, 32, 16, 16) -> (batch_size, 64, 8, 8)\n", + "\n", + " # Flatten: (batch_size, 64*8*8) = (batch_size, 4096)\n", + " x = x.view(x.size(0), -1)\n", + "\n", + " # Fully connected layers\n", + " x = self.relu(self.fc1(x))\n", + " x = self.dropout(x)\n", + " x = self.fc2(x) # Output: (batch_size, 1)\n", + "\n", + " return torch.softmax(x.squeeze()) # Output of (batch_size)\n", + "\n", + "print(summary(SimpleCNN(), input_size=(1, 64, 64)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NqOS_59-ARAy" + }, + "source": [ + "## Training process\n", + "\n", + "Based on the previous lessons, we know we need a few things to train a model\n", + "\n", + "### Loss and Optimizer\n", + "\n", + "\n", + "### Training and Validation data loops\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CPJDwFxSARAy" + }, + "outputs": [], + "source": [ + "# Initialize the model once\n", + "model = SimpleCNN()\n", + "\n", + "# Random choices of optimizer and loss functions\n", + "# These are pretty standard for classification\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)\n", + "criterion = torch.nn.BCELoss() # BCE = Binary Cross Entropy (for 2 classes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6XyF3QGjARAy" + }, + "source": [ + "# Train the model\n", + "\n", + "We have all the pieces in place:\n", + "- [x] Model\n", + "- [x] Train Data\n", + "- [x] Validation Data\n", + "- [x] Loss Function\n", + "- [x] Optimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "adEJ4vhuARAy" + }, + "outputs": [], + "source": [ + "\n", + "def train_epoch(model, dataloader, optimizer, criterion):\n", + " \"\"\"Trains the model for a single epoch (one loop through the entire training dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Training...\")\n", + " for images, labels in iterer:\n", + " optimizer.zero_grad() # Clear the gradients from the previous step\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " loss.backward() # Backward pass: compute the gradients of the loss with respect to the model parameters\n", + " # Update the model parameters using the computed gradients\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "\n", + "def val_epoch(model, dataloader, criterion):\n", + " \"\"\"Evaluates the model on the validation set for a single epoch (one loop through the entire validation dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Validating...\")\n", + " # Disable gradient computation for validation\n", + " for images, labels in iterer:\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "# One epoch should take around 15 seconds\n", + "# The number of epochs is depednent on the complexity and size of your data\n", + "n_epochs = 2\n", + "loss_history = {\"train\": [], \"val\": []}\n", + "for epoch in range(n_epochs):\n", + " model, train_loss = train_epoch(model, train_generator.get_dataloader(), optimizer, criterion)\n", + " model, val_loss = val_epoch(model, val_generator.get_dataloader(), criterion)\n", + "\n", + " loss_history[\"train\"].append(train_loss)\n", + " loss_history[\"val\"].append(val_loss)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3QCcxf0dARAy" + }, + "source": [ + "## Model Evaluation\n", + "\n", + "There are some steps we can take to see how well a model trained.\n", + "\n", + "### Loss Plots\n", + "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training.\n", + "Or, if the loss never moves or blows up entirely, that's a sign there's a problem.\n", + "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pfDIpqHGARAy" + }, + "outputs": [], + "source": [ + "Eval.plot_loss_history(loss_history['train'], loss_history['val'])\n", + "# Eval.plot_history is a simple function\n", + "# plots the loss as a function of epoch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V_9R4WYqARAy" + }, + "source": [ + "## Classification Accuracy Plots\n", + "\n", + "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training.\n", + "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it.\n", + "\n", + "### ROC\n", + "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate.\n", + "It shows how likely a model is to correctly predict something.\n", + "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves.\n", + "\n", + "\n", + "### Confusion Matrix\n", + "\n", + "Confusion matrices are a great tool for seeing how well each class does against each other.\n", + "It gets it name from its ability to tell if a model is \"confusing\" two different classes.\n", + "It plots the rate of predicted values for a given class versus the true values.\n", + "\n", + " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iP_xgNcMARAy" + }, + "outputs": [], + "source": [ + "test_generator = SkyGenerator(n_samples=1280, dataset=TestDataset, shuffle=True).get_dataloader()\n", + "\n", + "def make_prediction(model, test_generator):\n", + " predictions = []\n", + " labels = []\n", + " for image, label in test_generator:\n", + " predictions += model(transforms(image))\n", + " labels += label\n", + " prediction_classes = torch.where(torch.tensor(predictions)<0.5, 0, 1)\n", + " labels = torch.tensor(labels)\n", + " return prediction_classes, labels\n", + "\n", + "def test_quality(prediction, labels):\n", + " accuracy = torch.mean((prediction == labels).float()) # Compute the accuracy by comparing the predicted classes to the true labels\n", + " return accuracy.numpy()\n", + "\n", + "prediction, labels = make_prediction(model, test_generator)\n", + "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5f5Q78_IARAy" + }, + "outputs": [], + "source": [ + "# And now run them on your own data!\n", + "Eval.ROC_curve(prediction, labels)\n", + "Eval.confusion_matrix(prediction, labels)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G671xYOV7yfd" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "base", + "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.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/Challenge/challenges/challenge02.ipynb b/Challenge/challenges/challenge02.ipynb new file mode 100644 index 0000000..ba2959f --- /dev/null +++ b/Challenge/challenges/challenge02.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "jToNcYCsARAt" + }, + "source": [ + "## Model Training Challenge!\n", + "\n", + "In this notebook, there are a whole host of weird errors.\n", + "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking.\n", + "Do your best to sniff out the mistakes in both code and training procedure!\n", + "\n", + "Your mission, should you choose to accept it, is:\n", + "* Work together in groups of 4 to complete this notebook\n", + "* Figure out all the coding mistakes and make a notebook that runs\n", + "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", + "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RX8h5B-GARAt" + }, + "source": [ + "This line below installs a non-standard data generation package.\n", + "It is designed to make images for benchmarking computer vision problems, and is the back bone of your generator.\n", + "Unfortunately, it's not included with colab (which makes sense....), so we have to install it.\n", + "The `!` point here means \"Execute this line like it's a line in a bash prompt\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VPUeGpTrARAu" + }, + "outputs": [], + "source": [ + "! pip install deepbench" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xatOAXZ6ARAu" + }, + "source": [ + "`curl` is a package for downloading files off the internet.\n", + "This below line just downloads a file form the repo into this colab instance, and names it \"challenge_utilities.py\".\n", + "This file is also in the github repo, you can look at it there if you want.\n", + "It contains the data generator for each for your challenges, and the utilities for plotting and evaluating results.\n", + "(It won't contain any problems for you to solve, it's just there to keep this notebook from getting cluttered.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Kjo3hTS_ARAu" + }, + "outputs": [], + "source": [ + "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials/refs/heads/main/Challenge/challenge_utilities.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MKc8rBCmARAu" + }, + "outputs": [], + "source": [ + "# Now we'll import the different classes from this new file\n", + "# Import the dataloader and dataset classes\n", + "from challenge_utilities import SkaiDataset02 as SkyDataset # Dataset object itself\n", + "# A subclass of torch.utils.data.Dataset\n", + "from challenge_utilities import SkyGenerator, TestDataset # Generic dataloader object, same for all challenges\n", + "from challenge_utilities import Eval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2IXmuM_EARAv" + }, + "outputs": [], + "source": [ + "# Standard packages\n", + "import matplotlib.pyplot as plt\n", + "from tqdm import tqdm\n", + "\n", + "import torchvision\n", + "from torchsummary import summary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W9XZQhfDARAv" + }, + "source": [ + "#### Package documentation\n", + "\n", + "If you get stuck for syntax or anything - these are the packages used.\n", + "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names.\n", + "\n", + "[MatPlotLib](https://matplotlib.org/)\n", + "\n", + "[Pytorch](https://docs.pytorch.org/docs/)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m5EFoTAgARAv" + }, + "source": [ + "# Exploratory Data Analysis\n", + "\n", + "Understanding your data is a critical part of any AI/ML project.\n", + "Make sure you look at your data and understand the differences between each class.\n", + "\n", + "Something to note, which you would only know if you gathered the data yourself, is that the binary \"0\" and \"1\" labels correspond to \"stars\" and \"galaxies\".\n", + "You can use this to label your plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "66Gal0ukARAv" + }, + "outputs": [], + "source": [ + "\n", + "label_map = {\n", + " 0: \"Star\",\n", + " 1: \"Galaxy\"\n", + "}\n", + "\n", + "def plot_samples(generator, n_columns=3, n_rows=6, label_map:dict|None=None):\n", + " _, subplots = plt.subplots(n_columns, n_rows) # Make 9 plots in a 3 x 3 grid\n", + " plt.tight_layout()\n", + " plt.setp(subplots, xticks=[], yticks=[])\n", + "\n", + " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()):\n", + " image, label = generator.get(sample_index)\n", + " subplot.imshow(image)\n", + " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels).\n", + " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", + "\n", + " string_label = \"??????\"\n", + " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", + "\n", + "samples = SkyDataset(n_samples=9, seed=42) # Can just get a few samples\n", + "plot_samples(samples, label_map=label_map)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VTG-xMYWARAw" + }, + "source": [ + "# Look at the input data\n", + "\n", + "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6FJGVsZgARAw" + }, + "outputs": [], + "source": [ + "# These two generators produce the data we'll train with\n", + "\n", + "n_train_samples = 20\n", + "train_generator = SkyGenerator(n_samples=n_train_samples, dataset=SkyDataset, shuffle=True)\n", + "\n", + "n_val_samples = 1280\n", + "val_generator = SkyGenerator(n_samples=n_val_samples, dataset=SkyDataset, shuffle=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KM_sOu9sARAw" + }, + "outputs": [], + "source": [ + "# Each dataset object has a \"labels\" and \"images\" attribute, which we can use to check out the data range\n", + "\n", + "fig, ax = plt.subplots(1,2)\n", + "ax[0].hist(train_generator.dataset.labels)\n", + "ax[0].set_title(\"Train labels\")\n", + "ax[1].hist(val_generator.dataset.labels)\n", + "ax[1].set_title(\"Val labels\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TwAsJhDMARAw" + }, + "outputs": [], + "source": [ + "# We can also look at different attributes of the images\n", + "# Here, let's look at max values and mean values\n", + "\n", + "# These numbers will be slightly different every time, there's randomness controlling how the images are generated\n", + "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", + "\n", + "# Normally we would also have some metadata to work with, but here we only have images\n", + "\n", + "train_means = train_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "val_means = val_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "figure, subplots = plt.subplots(1, 2)\n", + "subplots[0].hist(train_means, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[0].hist(val_means, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[0].legend()\n", + "subplots[0].set_xlabel(\"Mean pixel value\")\n", + "\n", + "train_maxes = train_generator.dataset.images.max(axis=(1, 0))\n", + "val_maxes = val_generator.dataset.images.max(axis=(1, 0))\n", + "subplots[1].hist(train_maxes, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[1].hist(val_maxes, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[1].set_xlabel(\"Max pixel value\")\n", + "\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NhRpPEjdARAw" + }, + "source": [ + "## Data Processing\n", + "\n", + "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", + "There are near infinite ways to pre-process, especially in the computer vision space.\n", + "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways.\n", + "\n", + "For this, let's try bringing all the pixels in the image between 0 and 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Wp_k5XTiARAw" + }, + "outputs": [], + "source": [ + "transforms = torchvision.transforms.Compose([\n", + " torchvision.transforms.RandomResizedCrop(32, 164), # Resize the images to 64 x 64 pixels\n", + " torchvision.transforms.Normalize(0.5, 0.5) # Normalize the pixel values to be between 0 and 1\n", + "])\n", + "\n", + "# We can apply other transforms, in a process called \"data augmentation\"\n", + "# It helps make the model less likely to overfit by giving it different versions of the same data during the training process\n", + "# All transforms can be seen here - https://docs.pytorch.org/vision/0.26/transforms.html#v2-api-reference-recommended" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6gXn05XNARAx" + }, + "outputs": [], + "source": [ + "# Look at some processed samples!\n", + "\n", + "n_samples = 9\n", + "samples = SkyGenerator(n_samples=n_samples, batch_size=1, dataset=SkyDataset).get_dataloader()\n", + "for image, label in samples:\n", + " image = transforms(image)\n", + " plt.imshow(image[0][0]) # The image size is given as (batch, im channels, im_size, im_size)\n", + " plt.xlabel(label_map[label.item()])\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HJEKKWHpARAx" + }, + "source": [ + "## Make the binary classification model\n", + "\n", + "This function, when called, produces a keras Model instance that you can train to predict a class of an input.\n", + "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1.\n", + "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out.\n", + "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it.\n", + "\n", + "In this case, what we want:\n", + "* Take the input images from the data generator\n", + "* Apply two convolutional blocks to the input image\n", + "* Decode the second convolution block's output to a probability of the image being a given class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yb6L7SDoARAx" + }, + "outputs": [], + "source": [ + "# Create a simple convolusional model\n", + "\n", + "class SimpleCNN(torch.nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=3, padding=1)\n", + " self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=3, padding=1)\n", + " self.conv3 = torch.nn.Conv2d(32, 64, kernel_size=3, padding=1)\n", + "\n", + " # Pooling layer\n", + " self.pool = torch.nn.MaxPool2d(2, 2)\n", + "\n", + " # Fully connected layers\n", + " # After 3 pooling layers: 64 -> 32 -> 16 -> 8, so feature map is 8x8\n", + " # 64 channels * 8x8 = 4096 input features\n", + " self.fc1 = torch.nn.Linear(64 * 8 * 8, 128)\n", + " self.fc2 = torch.nn.Linear(128, 1)\n", + "\n", + " # Dropout\n", + " self.dropout = torch.nn.Dropout(0.3)\n", + "\n", + " # Activation\n", + " self.relu = torch.nn.ReLU()\n", + "\n", + " def forward(self, x):\n", + " # Input: (batch_size, 64, 64) - need to add channel dimension\n", + " if x.dim() == 3:\n", + " \"???\"\n", + " # Conv block 1: (batch_size, 1, 64, 64) -> (batch_size, 16, 32, 32)\n", + " x = self.pool(self.relu(self.conv1(x)))\n", + "\n", + " # Conv block 2: (batch_size, 16, 32, 32) -> (batch_size, 32, 16, 16)\n", + " x = self.pool(self.relu(self.conv2(x)))\n", + "\n", + " # Conv block 3: (batch_size, 32, 16, 16) -> (batch_size, 64, 8, 8)\n", + "\n", + " # Flatten: (batch_size, 64*8*8) = (batch_size, 4096)\n", + " x = x.view(x.size(0), -1)\n", + "\n", + " # Fully connected layers\n", + " x = self.relu(self.fc1(x))\n", + " x = self.dropout(x)\n", + " x = self.fc2(x) # Output: (batch_size, 1)\n", + "\n", + " return torch.softmax(x.squeeze()) # Output of (batch_size)\n", + "\n", + "print(summary(SimpleCNN(), input_size=(1, 64, 64)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NqOS_59-ARAy" + }, + "source": [ + "## Training process\n", + "\n", + "Based on the previous lessons, we know we need a few things to train a model\n", + "\n", + "### Loss and Optimizer\n", + "\n", + "\n", + "### Training and Validation data loops\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CPJDwFxSARAy" + }, + "outputs": [], + "source": [ + "# Initialize the model once\n", + "model = SimpleCNN()\n", + "\n", + "# Random choices of optimizer and loss functions\n", + "# These are pretty standard for classification\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)\n", + "criterion = torch.nn.BCELoss() # BCE = Binary Cross Entropy (for 2 classes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6XyF3QGjARAy" + }, + "source": [ + "# Train the model\n", + "\n", + "We have all the pieces in place:\n", + "- [x] Model\n", + "- [x] Train Data\n", + "- [x] Validation Data\n", + "- [x] Loss Function\n", + "- [x] Optimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "adEJ4vhuARAy" + }, + "outputs": [], + "source": [ + "\n", + "def train_epoch(model, dataloader, optimizer, criterion):\n", + " \"\"\"Trains the model for a single epoch (one loop through the entire training dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Training...\")\n", + " for images, labels in iterer:\n", + " optimizer.zero_grad() # Clear the gradients from the previous step\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " loss.backward() # Backward pass: compute the gradients of the loss with respect to the model parameters\n", + " # Update the model parameters using the computed gradients\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "\n", + "def val_epoch(model, dataloader, criterion):\n", + " \"\"\"Evaluates the model on the validation set for a single epoch (one loop through the entire validation dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Validating...\")\n", + " # Disable gradient computation for validation\n", + " for images, labels in iterer:\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "# One epoch should take around 15 seconds\n", + "# The number of epochs is depednent on the complexity and size of your data\n", + "n_epochs = 2\n", + "loss_history = {\"train\": [], \"val\": []}\n", + "for epoch in range(n_epochs):\n", + " model, train_loss = train_epoch(model, train_generator.get_dataloader(), optimizer, criterion)\n", + " model, val_loss = val_epoch(model, val_generator.get_dataloader(), criterion)\n", + "\n", + " loss_history[\"train\"].append(train_loss)\n", + " loss_history[\"val\"].append(val_loss)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3QCcxf0dARAy" + }, + "source": [ + "## Model Evaluation\n", + "\n", + "There are some steps we can take to see how well a model trained.\n", + "\n", + "### Loss Plots\n", + "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training.\n", + "Or, if the loss never moves or blows up entirely, that's a sign there's a problem.\n", + "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pfDIpqHGARAy" + }, + "outputs": [], + "source": [ + "Eval.plot_loss_history(loss_history['train'], loss_history['val'])\n", + "# Eval.plot_history is a simple function\n", + "# plots the loss as a function of epoch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V_9R4WYqARAy" + }, + "source": [ + "## Classification Accuracy Plots\n", + "\n", + "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training.\n", + "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it.\n", + "\n", + "### ROC\n", + "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate.\n", + "It shows how likely a model is to correctly predict something.\n", + "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves.\n", + "\n", + "\n", + "### Confusion Matrix\n", + "\n", + "Confusion matrices are a great tool for seeing how well each class does against each other.\n", + "It gets it name from its ability to tell if a model is \"confusing\" two different classes.\n", + "It plots the rate of predicted values for a given class versus the true values.\n", + "\n", + " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iP_xgNcMARAy" + }, + "outputs": [], + "source": [ + "test_generator = SkyGenerator(n_samples=1280, dataset=TestDataset, shuffle=True).get_dataloader()\n", + "\n", + "def make_prediction(model, test_generator):\n", + " predictions = []\n", + " labels = []\n", + " for image, label in test_generator:\n", + " predictions += model(transforms(image))\n", + " labels += label\n", + " prediction_classes = torch.where(torch.tensor(predictions)<0.5, 0, 1)\n", + " labels = torch.tensor(labels)\n", + " return prediction_classes, labels\n", + "\n", + "def test_quality(prediction, labels):\n", + " accuracy = torch.mean((prediction == labels).float()) # Compute the accuracy by comparing the predicted classes to the true labels\n", + " return accuracy.numpy()\n", + "\n", + "prediction, labels = make_prediction(model, test_generator)\n", + "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5f5Q78_IARAy" + }, + "outputs": [], + "source": [ + "# And now run them on your own data!\n", + "Eval.ROC_curve(prediction, labels)\n", + "Eval.confusion_matrix(prediction, labels)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G671xYOV7yfd" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "base", + "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.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/Challenge/challenges/challenge03.ipynb b/Challenge/challenges/challenge03.ipynb new file mode 100644 index 0000000..7250e29 --- /dev/null +++ b/Challenge/challenges/challenge03.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "jToNcYCsARAt" + }, + "source": [ + "## Model Training Challenge!\n", + "\n", + "In this notebook, there are a whole host of weird errors.\n", + "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking.\n", + "Do your best to sniff out the mistakes in both code and training procedure!\n", + "\n", + "Your mission, should you choose to accept it, is:\n", + "* Work together in groups of 4 to complete this notebook\n", + "* Figure out all the coding mistakes and make a notebook that runs\n", + "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", + "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RX8h5B-GARAt" + }, + "source": [ + "This line below installs a non-standard data generation package.\n", + "It is designed to make images for benchmarking computer vision problems, and is the back bone of your generator.\n", + "Unfortunately, it's not included with colab (which makes sense....), so we have to install it.\n", + "The `!` point here means \"Execute this line like it's a line in a bash prompt\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VPUeGpTrARAu" + }, + "outputs": [], + "source": [ + "! pip install deepbench" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xatOAXZ6ARAu" + }, + "source": [ + "`curl` is a package for downloading files off the internet.\n", + "This below line just downloads a file form the repo into this colab instance, and names it \"challenge_utilities.py\".\n", + "This file is also in the github repo, you can look at it there if you want.\n", + "It contains the data generator for each for your challenges, and the utilities for plotting and evaluating results.\n", + "(It won't contain any problems for you to solve, it's just there to keep this notebook from getting cluttered.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Kjo3hTS_ARAu" + }, + "outputs": [], + "source": [ + "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials/refs/heads/main/Challenge/challenge_utilities.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MKc8rBCmARAu" + }, + "outputs": [], + "source": [ + "# Now we'll import the different classes from this new file\n", + "# Import the dataloader and dataset classes\n", + "from challenge_utilities import SkaiDataset03 as SkyDataset # Dataset object itself\n", + "# A subclass of torch.utils.data.Dataset\n", + "from challenge_utilities import SkyGenerator, TestDataset # Generic dataloader object, same for all challenges\n", + "from challenge_utilities import Eval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2IXmuM_EARAv" + }, + "outputs": [], + "source": [ + "# Standard packages\n", + "import matplotlib.pyplot as plt\n", + "from tqdm import tqdm\n", + "\n", + "import torchvision\n", + "from torchsummary import summary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W9XZQhfDARAv" + }, + "source": [ + "#### Package documentation\n", + "\n", + "If you get stuck for syntax or anything - these are the packages used.\n", + "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names.\n", + "\n", + "[MatPlotLib](https://matplotlib.org/)\n", + "\n", + "[Pytorch](https://docs.pytorch.org/docs/)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m5EFoTAgARAv" + }, + "source": [ + "# Exploratory Data Analysis\n", + "\n", + "Understanding your data is a critical part of any AI/ML project.\n", + "Make sure you look at your data and understand the differences between each class.\n", + "\n", + "Something to note, which you would only know if you gathered the data yourself, is that the binary \"0\" and \"1\" labels correspond to \"stars\" and \"galaxies\".\n", + "You can use this to label your plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "66Gal0ukARAv" + }, + "outputs": [], + "source": [ + "\n", + "label_map = {\n", + " 0: \"Star\",\n", + " 1: \"Galaxy\"\n", + "}\n", + "\n", + "def plot_samples(generator, n_columns=3, n_rows=6, label_map:dict|None=None):\n", + " _, subplots = plt.subplots(n_columns, n_rows) # Make 9 plots in a 3 x 3 grid\n", + " plt.tight_layout()\n", + " plt.setp(subplots, xticks=[], yticks=[])\n", + "\n", + " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()):\n", + " image, label = generator.get(sample_index)\n", + " subplot.imshow(image)\n", + " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels).\n", + " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", + "\n", + " string_label = \"??????\"\n", + " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", + "\n", + "samples = SkyDataset(n_samples=9, seed=42) # Can just get a few samples\n", + "plot_samples(samples, label_map=label_map)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VTG-xMYWARAw" + }, + "source": [ + "# Look at the input data\n", + "\n", + "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6FJGVsZgARAw" + }, + "outputs": [], + "source": [ + "# These two generators produce the data we'll train with\n", + "\n", + "n_train_samples = 20\n", + "train_generator = SkyGenerator(n_samples=n_train_samples, dataset=SkyDataset, shuffle=True)\n", + "\n", + "n_val_samples = 1280\n", + "val_generator = SkyGenerator(n_samples=n_val_samples, dataset=SkyDataset, shuffle=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KM_sOu9sARAw" + }, + "outputs": [], + "source": [ + "# Each dataset object has a \"labels\" and \"images\" attribute, which we can use to check out the data range\n", + "\n", + "fig, ax = plt.subplots(1,2)\n", + "ax[0].hist(train_generator.dataset.labels)\n", + "ax[0].set_title(\"Train labels\")\n", + "ax[1].hist(val_generator.dataset.labels)\n", + "ax[1].set_title(\"Val labels\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TwAsJhDMARAw" + }, + "outputs": [], + "source": [ + "# We can also look at different attributes of the images\n", + "# Here, let's look at max values and mean values\n", + "\n", + "# These numbers will be slightly different every time, there's randomness controlling how the images are generated\n", + "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", + "\n", + "# Normally we would also have some metadata to work with, but here we only have images\n", + "\n", + "train_means = train_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "val_means = val_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "figure, subplots = plt.subplots(1, 2)\n", + "subplots[0].hist(train_means, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[0].hist(val_means, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[0].legend()\n", + "subplots[0].set_xlabel(\"Mean pixel value\")\n", + "\n", + "train_maxes = train_generator.dataset.images.max(axis=(1, 0))\n", + "val_maxes = val_generator.dataset.images.max(axis=(1, 0))\n", + "subplots[1].hist(train_maxes, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[1].hist(val_maxes, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[1].set_xlabel(\"Max pixel value\")\n", + "\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NhRpPEjdARAw" + }, + "source": [ + "## Data Processing\n", + "\n", + "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", + "There are near infinite ways to pre-process, especially in the computer vision space.\n", + "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways.\n", + "\n", + "For this, let's try bringing all the pixels in the image between 0 and 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Wp_k5XTiARAw" + }, + "outputs": [], + "source": [ + "transforms = torchvision.transforms.Compose([\n", + " torchvision.transforms.RandomResizedCrop(32, 164), # Resize the images to 64 x 64 pixels\n", + " torchvision.transforms.Normalize(0.5, 0.5) # Normalize the pixel values to be between 0 and 1\n", + "])\n", + "\n", + "# We can apply other transforms, in a process called \"data augmentation\"\n", + "# It helps make the model less likely to overfit by giving it different versions of the same data during the training process\n", + "# All transforms can be seen here - https://docs.pytorch.org/vision/0.26/transforms.html#v2-api-reference-recommended" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6gXn05XNARAx" + }, + "outputs": [], + "source": [ + "# Look at some processed samples!\n", + "\n", + "n_samples = 9\n", + "samples = SkyGenerator(n_samples=n_samples, batch_size=1, dataset=SkyDataset).get_dataloader()\n", + "for image, label in samples:\n", + " image = transforms(image)\n", + " plt.imshow(image[0][0]) # The image size is given as (batch, im channels, im_size, im_size)\n", + " plt.xlabel(label_map[label.item()])\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HJEKKWHpARAx" + }, + "source": [ + "## Make the binary classification model\n", + "\n", + "This function, when called, produces a keras Model instance that you can train to predict a class of an input.\n", + "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1.\n", + "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out.\n", + "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it.\n", + "\n", + "In this case, what we want:\n", + "* Take the input images from the data generator\n", + "* Apply two convolutional blocks to the input image\n", + "* Decode the second convolution block's output to a probability of the image being a given class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yb6L7SDoARAx" + }, + "outputs": [], + "source": [ + "# Create a simple convolusional model\n", + "\n", + "class SimpleCNN(torch.nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=3, padding=1)\n", + " self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=3, padding=1)\n", + " self.conv3 = torch.nn.Conv2d(32, 64, kernel_size=3, padding=1)\n", + "\n", + " # Pooling layer\n", + " self.pool = torch.nn.MaxPool2d(2, 2)\n", + "\n", + " # Fully connected layers\n", + " # After 3 pooling layers: 64 -> 32 -> 16 -> 8, so feature map is 8x8\n", + " # 64 channels * 8x8 = 4096 input features\n", + " self.fc1 = torch.nn.Linear(64 * 8 * 8, 128)\n", + " self.fc2 = torch.nn.Linear(128, 1)\n", + "\n", + " # Dropout\n", + " self.dropout = torch.nn.Dropout(0.3)\n", + "\n", + " # Activation\n", + " self.relu = torch.nn.ReLU()\n", + "\n", + " def forward(self, x):\n", + " # Input: (batch_size, 64, 64) - need to add channel dimension\n", + " if x.dim() == 3:\n", + " \"???\"\n", + " # Conv block 1: (batch_size, 1, 64, 64) -> (batch_size, 16, 32, 32)\n", + " x = self.pool(self.relu(self.conv1(x)))\n", + "\n", + " # Conv block 2: (batch_size, 16, 32, 32) -> (batch_size, 32, 16, 16)\n", + " x = self.pool(self.relu(self.conv2(x)))\n", + "\n", + " # Conv block 3: (batch_size, 32, 16, 16) -> (batch_size, 64, 8, 8)\n", + "\n", + " # Flatten: (batch_size, 64*8*8) = (batch_size, 4096)\n", + " x = x.view(x.size(0), -1)\n", + "\n", + " # Fully connected layers\n", + " x = self.relu(self.fc1(x))\n", + " x = self.dropout(x)\n", + " x = self.fc2(x) # Output: (batch_size, 1)\n", + "\n", + " return torch.softmax(x.squeeze()) # Output of (batch_size)\n", + "\n", + "print(summary(SimpleCNN(), input_size=(1, 64, 64)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NqOS_59-ARAy" + }, + "source": [ + "## Training process\n", + "\n", + "Based on the previous lessons, we know we need a few things to train a model\n", + "\n", + "### Loss and Optimizer\n", + "\n", + "\n", + "### Training and Validation data loops\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CPJDwFxSARAy" + }, + "outputs": [], + "source": [ + "# Initialize the model once\n", + "model = SimpleCNN()\n", + "\n", + "# Random choices of optimizer and loss functions\n", + "# These are pretty standard for classification\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)\n", + "criterion = torch.nn.BCELoss() # BCE = Binary Cross Entropy (for 2 classes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6XyF3QGjARAy" + }, + "source": [ + "# Train the model\n", + "\n", + "We have all the pieces in place:\n", + "- [x] Model\n", + "- [x] Train Data\n", + "- [x] Validation Data\n", + "- [x] Loss Function\n", + "- [x] Optimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "adEJ4vhuARAy" + }, + "outputs": [], + "source": [ + "\n", + "def train_epoch(model, dataloader, optimizer, criterion):\n", + " \"\"\"Trains the model for a single epoch (one loop through the entire training dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Training...\")\n", + " for images, labels in iterer:\n", + " optimizer.zero_grad() # Clear the gradients from the previous step\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " loss.backward() # Backward pass: compute the gradients of the loss with respect to the model parameters\n", + " # Update the model parameters using the computed gradients\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "\n", + "def val_epoch(model, dataloader, criterion):\n", + " \"\"\"Evaluates the model on the validation set for a single epoch (one loop through the entire validation dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Validating...\")\n", + " # Disable gradient computation for validation\n", + " for images, labels in iterer:\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "# One epoch should take around 15 seconds\n", + "# The number of epochs is depednent on the complexity and size of your data\n", + "n_epochs = 2\n", + "loss_history = {\"train\": [], \"val\": []}\n", + "for epoch in range(n_epochs):\n", + " model, train_loss = train_epoch(model, train_generator.get_dataloader(), optimizer, criterion)\n", + " model, val_loss = val_epoch(model, val_generator.get_dataloader(), criterion)\n", + "\n", + " loss_history[\"train\"].append(train_loss)\n", + " loss_history[\"val\"].append(val_loss)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3QCcxf0dARAy" + }, + "source": [ + "## Model Evaluation\n", + "\n", + "There are some steps we can take to see how well a model trained.\n", + "\n", + "### Loss Plots\n", + "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training.\n", + "Or, if the loss never moves or blows up entirely, that's a sign there's a problem.\n", + "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pfDIpqHGARAy" + }, + "outputs": [], + "source": [ + "Eval.plot_loss_history(loss_history['train'], loss_history['val'])\n", + "# Eval.plot_history is a simple function\n", + "# plots the loss as a function of epoch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V_9R4WYqARAy" + }, + "source": [ + "## Classification Accuracy Plots\n", + "\n", + "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training.\n", + "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it.\n", + "\n", + "### ROC\n", + "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate.\n", + "It shows how likely a model is to correctly predict something.\n", + "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves.\n", + "\n", + "\n", + "### Confusion Matrix\n", + "\n", + "Confusion matrices are a great tool for seeing how well each class does against each other.\n", + "It gets it name from its ability to tell if a model is \"confusing\" two different classes.\n", + "It plots the rate of predicted values for a given class versus the true values.\n", + "\n", + " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iP_xgNcMARAy" + }, + "outputs": [], + "source": [ + "test_generator = SkyGenerator(n_samples=1280, dataset=TestDataset, shuffle=True).get_dataloader()\n", + "\n", + "def make_prediction(model, test_generator):\n", + " predictions = []\n", + " labels = []\n", + " for image, label in test_generator:\n", + " predictions += model(transforms(image))\n", + " labels += label\n", + " prediction_classes = torch.where(torch.tensor(predictions)<0.5, 0, 1)\n", + " labels = torch.tensor(labels)\n", + " return prediction_classes, labels\n", + "\n", + "def test_quality(prediction, labels):\n", + " accuracy = torch.mean((prediction == labels).float()) # Compute the accuracy by comparing the predicted classes to the true labels\n", + " return accuracy.numpy()\n", + "\n", + "prediction, labels = make_prediction(model, test_generator)\n", + "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5f5Q78_IARAy" + }, + "outputs": [], + "source": [ + "# And now run them on your own data!\n", + "Eval.ROC_curve(prediction, labels)\n", + "Eval.confusion_matrix(prediction, labels)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G671xYOV7yfd" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "base", + "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.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/Challenge/challenges/challenge04.ipynb b/Challenge/challenges/challenge04.ipynb new file mode 100644 index 0000000..01305e6 --- /dev/null +++ b/Challenge/challenges/challenge04.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "jToNcYCsARAt" + }, + "source": [ + "## Model Training Challenge!\n", + "\n", + "In this notebook, there are a whole host of weird errors.\n", + "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking.\n", + "Do your best to sniff out the mistakes in both code and training procedure!\n", + "\n", + "Your mission, should you choose to accept it, is:\n", + "* Work together in groups of 4 to complete this notebook\n", + "* Figure out all the coding mistakes and make a notebook that runs\n", + "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", + "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RX8h5B-GARAt" + }, + "source": [ + "This line below installs a non-standard data generation package.\n", + "It is designed to make images for benchmarking computer vision problems, and is the back bone of your generator.\n", + "Unfortunately, it's not included with colab (which makes sense....), so we have to install it.\n", + "The `!` point here means \"Execute this line like it's a line in a bash prompt\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VPUeGpTrARAu" + }, + "outputs": [], + "source": [ + "! pip install deepbench" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xatOAXZ6ARAu" + }, + "source": [ + "`curl` is a package for downloading files off the internet.\n", + "This below line just downloads a file form the repo into this colab instance, and names it \"challenge_utilities.py\".\n", + "This file is also in the github repo, you can look at it there if you want.\n", + "It contains the data generator for each for your challenges, and the utilities for plotting and evaluating results.\n", + "(It won't contain any problems for you to solve, it's just there to keep this notebook from getting cluttered.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Kjo3hTS_ARAu" + }, + "outputs": [], + "source": [ + "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials/refs/heads/main/Challenge/challenge_utilities.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MKc8rBCmARAu" + }, + "outputs": [], + "source": [ + "# Now we'll import the different classes from this new file\n", + "# Import the dataloader and dataset classes\n", + "from challenge_utilities import SkaiDataset04 as SkyDataset # Dataset object itself\n", + "# A subclass of torch.utils.data.Dataset\n", + "from challenge_utilities import SkyGenerator, TestDataset # Generic dataloader object, same for all challenges\n", + "from challenge_utilities import Eval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2IXmuM_EARAv" + }, + "outputs": [], + "source": [ + "# Standard packages\n", + "import matplotlib.pyplot as plt\n", + "from tqdm import tqdm\n", + "\n", + "import torchvision\n", + "from torchsummary import summary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W9XZQhfDARAv" + }, + "source": [ + "#### Package documentation\n", + "\n", + "If you get stuck for syntax or anything - these are the packages used.\n", + "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names.\n", + "\n", + "[MatPlotLib](https://matplotlib.org/)\n", + "\n", + "[Pytorch](https://docs.pytorch.org/docs/)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m5EFoTAgARAv" + }, + "source": [ + "# Exploratory Data Analysis\n", + "\n", + "Understanding your data is a critical part of any AI/ML project.\n", + "Make sure you look at your data and understand the differences between each class.\n", + "\n", + "Something to note, which you would only know if you gathered the data yourself, is that the binary \"0\" and \"1\" labels correspond to \"stars\" and \"galaxies\".\n", + "You can use this to label your plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "66Gal0ukARAv" + }, + "outputs": [], + "source": [ + "\n", + "label_map = {\n", + " 0: \"Star\",\n", + " 1: \"Galaxy\"\n", + "}\n", + "\n", + "def plot_samples(generator, n_columns=3, n_rows=6, label_map:dict|None=None):\n", + " _, subplots = plt.subplots(n_columns, n_rows) # Make 9 plots in a 3 x 3 grid\n", + " plt.tight_layout()\n", + " plt.setp(subplots, xticks=[], yticks=[])\n", + "\n", + " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()):\n", + " image, label = generator.get(sample_index)\n", + " subplot.imshow(image)\n", + " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels).\n", + " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", + "\n", + " string_label = \"??????\"\n", + " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", + "\n", + "samples = SkyDataset(n_samples=9, seed=42) # Can just get a few samples\n", + "plot_samples(samples, label_map=label_map)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VTG-xMYWARAw" + }, + "source": [ + "# Look at the input data\n", + "\n", + "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6FJGVsZgARAw" + }, + "outputs": [], + "source": [ + "# These two generators produce the data we'll train with\n", + "\n", + "n_train_samples = 20\n", + "train_generator = SkyGenerator(n_samples=n_train_samples, dataset=SkyDataset, shuffle=True)\n", + "\n", + "n_val_samples = 1280\n", + "val_generator = SkyGenerator(n_samples=n_val_samples, dataset=SkyDataset, shuffle=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KM_sOu9sARAw" + }, + "outputs": [], + "source": [ + "# Each dataset object has a \"labels\" and \"images\" attribute, which we can use to check out the data range\n", + "\n", + "fig, ax = plt.subplots(1,2)\n", + "ax[0].hist(train_generator.dataset.labels)\n", + "ax[0].set_title(\"Train labels\")\n", + "ax[1].hist(val_generator.dataset.labels)\n", + "ax[1].set_title(\"Val labels\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TwAsJhDMARAw" + }, + "outputs": [], + "source": [ + "# We can also look at different attributes of the images\n", + "# Here, let's look at max values and mean values\n", + "\n", + "# These numbers will be slightly different every time, there's randomness controlling how the images are generated\n", + "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", + "\n", + "# Normally we would also have some metadata to work with, but here we only have images\n", + "\n", + "train_means = train_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "val_means = val_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "figure, subplots = plt.subplots(1, 2)\n", + "subplots[0].hist(train_means, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[0].hist(val_means, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[0].legend()\n", + "subplots[0].set_xlabel(\"Mean pixel value\")\n", + "\n", + "train_maxes = train_generator.dataset.images.max(axis=(1, 0))\n", + "val_maxes = val_generator.dataset.images.max(axis=(1, 0))\n", + "subplots[1].hist(train_maxes, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[1].hist(val_maxes, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[1].set_xlabel(\"Max pixel value\")\n", + "\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NhRpPEjdARAw" + }, + "source": [ + "## Data Processing\n", + "\n", + "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", + "There are near infinite ways to pre-process, especially in the computer vision space.\n", + "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways.\n", + "\n", + "For this, let's try bringing all the pixels in the image between 0 and 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Wp_k5XTiARAw" + }, + "outputs": [], + "source": [ + "transforms = torchvision.transforms.Compose([\n", + " torchvision.transforms.RandomResizedCrop(32, 164), # Resize the images to 64 x 64 pixels\n", + " torchvision.transforms.Normalize(0.5, 0.5) # Normalize the pixel values to be between 0 and 1\n", + "])\n", + "\n", + "# We can apply other transforms, in a process called \"data augmentation\"\n", + "# It helps make the model less likely to overfit by giving it different versions of the same data during the training process\n", + "# All transforms can be seen here - https://docs.pytorch.org/vision/0.26/transforms.html#v2-api-reference-recommended" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6gXn05XNARAx" + }, + "outputs": [], + "source": [ + "# Look at some processed samples!\n", + "\n", + "n_samples = 9\n", + "samples = SkyGenerator(n_samples=n_samples, batch_size=1, dataset=SkyDataset).get_dataloader()\n", + "for image, label in samples:\n", + " image = transforms(image)\n", + " plt.imshow(image[0][0]) # The image size is given as (batch, im channels, im_size, im_size)\n", + " plt.xlabel(label_map[label.item()])\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HJEKKWHpARAx" + }, + "source": [ + "## Make the binary classification model\n", + "\n", + "This function, when called, produces a keras Model instance that you can train to predict a class of an input.\n", + "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1.\n", + "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out.\n", + "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it.\n", + "\n", + "In this case, what we want:\n", + "* Take the input images from the data generator\n", + "* Apply two convolutional blocks to the input image\n", + "* Decode the second convolution block's output to a probability of the image being a given class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yb6L7SDoARAx" + }, + "outputs": [], + "source": [ + "# Create a simple convolusional model\n", + "\n", + "class SimpleCNN(torch.nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=3, padding=1)\n", + " self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=3, padding=1)\n", + " self.conv3 = torch.nn.Conv2d(32, 64, kernel_size=3, padding=1)\n", + "\n", + " # Pooling layer\n", + " self.pool = torch.nn.MaxPool2d(2, 2)\n", + "\n", + " # Fully connected layers\n", + " # After 3 pooling layers: 64 -> 32 -> 16 -> 8, so feature map is 8x8\n", + " # 64 channels * 8x8 = 4096 input features\n", + " self.fc1 = torch.nn.Linear(64 * 8 * 8, 128)\n", + " self.fc2 = torch.nn.Linear(128, 1)\n", + "\n", + " # Dropout\n", + " self.dropout = torch.nn.Dropout(0.3)\n", + "\n", + " # Activation\n", + " self.relu = torch.nn.ReLU()\n", + "\n", + " def forward(self, x):\n", + " # Input: (batch_size, 64, 64) - need to add channel dimension\n", + " if x.dim() == 3:\n", + " \"???\"\n", + " # Conv block 1: (batch_size, 1, 64, 64) -> (batch_size, 16, 32, 32)\n", + " x = self.pool(self.relu(self.conv1(x)))\n", + "\n", + " # Conv block 2: (batch_size, 16, 32, 32) -> (batch_size, 32, 16, 16)\n", + " x = self.pool(self.relu(self.conv2(x)))\n", + "\n", + " # Conv block 3: (batch_size, 32, 16, 16) -> (batch_size, 64, 8, 8)\n", + "\n", + " # Flatten: (batch_size, 64*8*8) = (batch_size, 4096)\n", + " x = x.view(x.size(0), -1)\n", + "\n", + " # Fully connected layers\n", + " x = self.relu(self.fc1(x))\n", + " x = self.dropout(x)\n", + " x = self.fc2(x) # Output: (batch_size, 1)\n", + "\n", + " return torch.softmax(x.squeeze()) # Output of (batch_size)\n", + "\n", + "print(summary(SimpleCNN(), input_size=(1, 64, 64)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NqOS_59-ARAy" + }, + "source": [ + "## Training process\n", + "\n", + "Based on the previous lessons, we know we need a few things to train a model\n", + "\n", + "### Loss and Optimizer\n", + "\n", + "\n", + "### Training and Validation data loops\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CPJDwFxSARAy" + }, + "outputs": [], + "source": [ + "# Initialize the model once\n", + "model = SimpleCNN()\n", + "\n", + "# Random choices of optimizer and loss functions\n", + "# These are pretty standard for classification\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)\n", + "criterion = torch.nn.BCELoss() # BCE = Binary Cross Entropy (for 2 classes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6XyF3QGjARAy" + }, + "source": [ + "# Train the model\n", + "\n", + "We have all the pieces in place:\n", + "- [x] Model\n", + "- [x] Train Data\n", + "- [x] Validation Data\n", + "- [x] Loss Function\n", + "- [x] Optimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "adEJ4vhuARAy" + }, + "outputs": [], + "source": [ + "\n", + "def train_epoch(model, dataloader, optimizer, criterion):\n", + " \"\"\"Trains the model for a single epoch (one loop through the entire training dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Training...\")\n", + " for images, labels in iterer:\n", + " optimizer.zero_grad() # Clear the gradients from the previous step\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " loss.backward() # Backward pass: compute the gradients of the loss with respect to the model parameters\n", + " # Update the model parameters using the computed gradients\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "\n", + "def val_epoch(model, dataloader, criterion):\n", + " \"\"\"Evaluates the model on the validation set for a single epoch (one loop through the entire validation dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Validating...\")\n", + " # Disable gradient computation for validation\n", + " for images, labels in iterer:\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "# One epoch should take around 15 seconds\n", + "# The number of epochs is depednent on the complexity and size of your data\n", + "n_epochs = 2\n", + "loss_history = {\"train\": [], \"val\": []}\n", + "for epoch in range(n_epochs):\n", + " model, train_loss = train_epoch(model, train_generator.get_dataloader(), optimizer, criterion)\n", + " model, val_loss = val_epoch(model, val_generator.get_dataloader(), criterion)\n", + "\n", + " loss_history[\"train\"].append(train_loss)\n", + " loss_history[\"val\"].append(val_loss)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3QCcxf0dARAy" + }, + "source": [ + "## Model Evaluation\n", + "\n", + "There are some steps we can take to see how well a model trained.\n", + "\n", + "### Loss Plots\n", + "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training.\n", + "Or, if the loss never moves or blows up entirely, that's a sign there's a problem.\n", + "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pfDIpqHGARAy" + }, + "outputs": [], + "source": [ + "Eval.plot_loss_history(loss_history['train'], loss_history['val'])\n", + "# Eval.plot_history is a simple function\n", + "# plots the loss as a function of epoch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V_9R4WYqARAy" + }, + "source": [ + "## Classification Accuracy Plots\n", + "\n", + "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training.\n", + "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it.\n", + "\n", + "### ROC\n", + "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate.\n", + "It shows how likely a model is to correctly predict something.\n", + "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves.\n", + "\n", + "\n", + "### Confusion Matrix\n", + "\n", + "Confusion matrices are a great tool for seeing how well each class does against each other.\n", + "It gets it name from its ability to tell if a model is \"confusing\" two different classes.\n", + "It plots the rate of predicted values for a given class versus the true values.\n", + "\n", + " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iP_xgNcMARAy" + }, + "outputs": [], + "source": [ + "test_generator = SkyGenerator(n_samples=1280, dataset=TestDataset, shuffle=True).get_dataloader()\n", + "\n", + "def make_prediction(model, test_generator):\n", + " predictions = []\n", + " labels = []\n", + " for image, label in test_generator:\n", + " predictions += model(transforms(image))\n", + " labels += label\n", + " prediction_classes = torch.where(torch.tensor(predictions)<0.5, 0, 1)\n", + " labels = torch.tensor(labels)\n", + " return prediction_classes, labels\n", + "\n", + "def test_quality(prediction, labels):\n", + " accuracy = torch.mean((prediction == labels).float()) # Compute the accuracy by comparing the predicted classes to the true labels\n", + " return accuracy.numpy()\n", + "\n", + "prediction, labels = make_prediction(model, test_generator)\n", + "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5f5Q78_IARAy" + }, + "outputs": [], + "source": [ + "# And now run them on your own data!\n", + "Eval.ROC_curve(prediction, labels)\n", + "Eval.confusion_matrix(prediction, labels)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G671xYOV7yfd" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "base", + "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.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/Challenge/challenges/challenge05.ipynb b/Challenge/challenges/challenge05.ipynb new file mode 100644 index 0000000..32e8370 --- /dev/null +++ b/Challenge/challenges/challenge05.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "jToNcYCsARAt" + }, + "source": [ + "## Model Training Challenge!\n", + "\n", + "In this notebook, there are a whole host of weird errors.\n", + "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking.\n", + "Do your best to sniff out the mistakes in both code and training procedure!\n", + "\n", + "Your mission, should you choose to accept it, is:\n", + "* Work together in groups of 4 to complete this notebook\n", + "* Figure out all the coding mistakes and make a notebook that runs\n", + "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", + "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RX8h5B-GARAt" + }, + "source": [ + "This line below installs a non-standard data generation package.\n", + "It is designed to make images for benchmarking computer vision problems, and is the back bone of your generator.\n", + "Unfortunately, it's not included with colab (which makes sense....), so we have to install it.\n", + "The `!` point here means \"Execute this line like it's a line in a bash prompt\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VPUeGpTrARAu" + }, + "outputs": [], + "source": [ + "! pip install deepbench" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xatOAXZ6ARAu" + }, + "source": [ + "`curl` is a package for downloading files off the internet.\n", + "This below line just downloads a file form the repo into this colab instance, and names it \"challenge_utilities.py\".\n", + "This file is also in the github repo, you can look at it there if you want.\n", + "It contains the data generator for each for your challenges, and the utilities for plotting and evaluating results.\n", + "(It won't contain any problems for you to solve, it's just there to keep this notebook from getting cluttered.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Kjo3hTS_ARAu" + }, + "outputs": [], + "source": [ + "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials/refs/heads/main/Challenge/challenge_utilities.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MKc8rBCmARAu" + }, + "outputs": [], + "source": [ + "# Now we'll import the different classes from this new file\n", + "# Import the dataloader and dataset classes\n", + "from challenge_utilities import SkaiDataset01 as SkyDataset # Dataset object itself\n", + "# A subclass of torch.utils.data.Dataset\n", + "from challenge_utilities import SkyGenerator, TestDataset # Generic dataloader object, same for all challenges\n", + "from challenge_utilities import Eval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2IXmuM_EARAv" + }, + "outputs": [], + "source": [ + "# Standard packages\n", + "import matplotlib.pyplot as plt\n", + "from tqdm import tqdm\n", + "\n", + "import torchvision\n", + "from torchsummary import summary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W9XZQhfDARAv" + }, + "source": [ + "#### Package documentation\n", + "\n", + "If you get stuck for syntax or anything - these are the packages used.\n", + "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names.\n", + "\n", + "[MatPlotLib](https://matplotlib.org/)\n", + "\n", + "[Pytorch](https://docs.pytorch.org/docs/)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m5EFoTAgARAv" + }, + "source": [ + "# Exploratory Data Analysis\n", + "\n", + "Understanding your data is a critical part of any AI/ML project.\n", + "Make sure you look at your data and understand the differences between each class.\n", + "\n", + "Something to note, which you would only know if you gathered the data yourself, is that the binary \"0\" and \"1\" labels correspond to \"stars\" and \"galaxies\".\n", + "You can use this to label your plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "66Gal0ukARAv" + }, + "outputs": [], + "source": [ + "\n", + "label_map = {\n", + " 0: \"Star\",\n", + " 1: \"Galaxy\"\n", + "}\n", + "\n", + "def plot_samples(generator, n_columns=3, n_rows=6, label_map:dict|None=None):\n", + " _, subplots = plt.subplots(n_columns, n_rows) # Make 9 plots in a 3 x 3 grid\n", + " plt.tight_layout()\n", + " plt.setp(subplots, xticks=[], yticks=[])\n", + "\n", + " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()):\n", + " image, label = generator.get(sample_index)\n", + " subplot.imshow(image)\n", + " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels).\n", + " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", + "\n", + " string_label = \"??????\"\n", + " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", + "\n", + "samples = SkyDataset(n_samples=9, seed=42) # Can just get a few samples\n", + "plot_samples(samples, label_map=label_map)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VTG-xMYWARAw" + }, + "source": [ + "# Look at the input data\n", + "\n", + "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6FJGVsZgARAw" + }, + "outputs": [], + "source": [ + "# These two generators produce the data we'll train with\n", + "\n", + "n_train_samples = 20\n", + "train_generator = SkyGenerator(n_samples=n_train_samples, dataset=SkyDataset, shuffle=True)\n", + "\n", + "n_val_samples = 1280\n", + "val_generator = SkyGenerator(n_samples=n_val_samples, dataset=SkyDataset, shuffle=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KM_sOu9sARAw" + }, + "outputs": [], + "source": [ + "# Each dataset object has a \"labels\" and \"images\" attribute, which we can use to check out the data range\n", + "\n", + "fig, ax = plt.subplots(1,2)\n", + "ax[0].hist(train_generator.dataset.labels)\n", + "ax[0].set_title(\"Train labels\")\n", + "ax[1].hist(val_generator.dataset.labels)\n", + "ax[1].set_title(\"Val labels\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TwAsJhDMARAw" + }, + "outputs": [], + "source": [ + "# We can also look at different attributes of the images\n", + "# Here, let's look at max values and mean values\n", + "\n", + "# These numbers will be slightly different every time, there's randomness controlling how the images are generated\n", + "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", + "\n", + "# Normally we would also have some metadata to work with, but here we only have images\n", + "\n", + "train_means = train_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "val_means = val_generator.dataset.images.mean(axis=(1, 0))\n", + "\n", + "figure, subplots = plt.subplots(1, 2)\n", + "subplots[0].hist(train_means, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[0].hist(val_means, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[0].legend()\n", + "subplots[0].set_xlabel(\"Mean pixel value\")\n", + "\n", + "train_maxes = train_generator.dataset.images.max(axis=(1, 0))\n", + "val_maxes = val_generator.dataset.images.max(axis=(1, 0))\n", + "subplots[1].hist(train_maxes, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[1].hist(val_maxes, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[1].set_xlabel(\"Max pixel value\")\n", + "\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NhRpPEjdARAw" + }, + "source": [ + "## Data Processing\n", + "\n", + "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", + "There are near infinite ways to pre-process, especially in the computer vision space.\n", + "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways.\n", + "\n", + "For this, let's try bringing all the pixels in the image between 0 and 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Wp_k5XTiARAw" + }, + "outputs": [], + "source": [ + "transforms = torchvision.transforms.Compose([\n", + " torchvision.transforms.RandomResizedCrop(32, 164), # Resize the images to 64 x 64 pixels\n", + " torchvision.transforms.Normalize(0.5, 0.5) # Normalize the pixel values to be between 0 and 1\n", + "])\n", + "\n", + "# We can apply other transforms, in a process called \"data augmentation\"\n", + "# It helps make the model less likely to overfit by giving it different versions of the same data during the training process\n", + "# All transforms can be seen here - https://docs.pytorch.org/vision/0.26/transforms.html#v2-api-reference-recommended" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6gXn05XNARAx" + }, + "outputs": [], + "source": [ + "# Look at some processed samples!\n", + "\n", + "n_samples = 9\n", + "samples = SkyGenerator(n_samples=n_samples, batch_size=1, dataset=SkyDataset).get_dataloader()\n", + "for image, label in samples:\n", + " image = transforms(image)\n", + " plt.imshow(image[0][0]) # The image size is given as (batch, im channels, im_size, im_size)\n", + " plt.xlabel(label_map[label.item()])\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HJEKKWHpARAx" + }, + "source": [ + "## Make the binary classification model\n", + "\n", + "This function, when called, produces a keras Model instance that you can train to predict a class of an input.\n", + "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1.\n", + "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out.\n", + "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it.\n", + "\n", + "In this case, what we want:\n", + "* Take the input images from the data generator\n", + "* Apply two convolutional blocks to the input image\n", + "* Decode the second convolution block's output to a probability of the image being a given class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yb6L7SDoARAx" + }, + "outputs": [], + "source": [ + "# Create a simple convolusional model\n", + "\n", + "class SimpleCNN(torch.nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=3, padding=1)\n", + " self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=3, padding=1)\n", + " self.conv3 = torch.nn.Conv2d(32, 64, kernel_size=3, padding=1)\n", + "\n", + " # Pooling layer\n", + " self.pool = torch.nn.MaxPool2d(2, 2)\n", + "\n", + " # Fully connected layers\n", + " # After 3 pooling layers: 64 -> 32 -> 16 -> 8, so feature map is 8x8\n", + " # 64 channels * 8x8 = 4096 input features\n", + " self.fc1 = torch.nn.Linear(64 * 8 * 8, 128)\n", + " self.fc2 = torch.nn.Linear(128, 1)\n", + "\n", + " # Dropout\n", + " self.dropout = torch.nn.Dropout(0.3)\n", + "\n", + " # Activation\n", + " self.relu = torch.nn.ReLU()\n", + "\n", + " def forward(self, x):\n", + " # Input: (batch_size, 64, 64) - need to add channel dimension\n", + " if x.dim() == 3:\n", + " \"???\"\n", + " # Conv block 1: (batch_size, 1, 64, 64) -> (batch_size, 16, 32, 32)\n", + " x = self.pool(self.relu(self.conv1(x)))\n", + "\n", + " # Conv block 2: (batch_size, 16, 32, 32) -> (batch_size, 32, 16, 16)\n", + " x = self.pool(self.relu(self.conv2(x)))\n", + "\n", + " # Conv block 3: (batch_size, 32, 16, 16) -> (batch_size, 64, 8, 8)\n", + "\n", + " # Flatten: (batch_size, 64*8*8) = (batch_size, 4096)\n", + " x = x.view(x.size(0), -1)\n", + "\n", + " # Fully connected layers\n", + " x = self.relu(self.fc1(x))\n", + " x = self.dropout(x)\n", + " x = self.fc2(x) # Output: (batch_size, 1)\n", + "\n", + " return torch.softmax(x.squeeze()) # Output of (batch_size)\n", + "\n", + "print(summary(SimpleCNN(), input_size=(1, 64, 64)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NqOS_59-ARAy" + }, + "source": [ + "## Training process\n", + "\n", + "Based on the previous lessons, we know we need a few things to train a model\n", + "\n", + "### Loss and Optimizer\n", + "\n", + "\n", + "### Training and Validation data loops\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CPJDwFxSARAy" + }, + "outputs": [], + "source": [ + "# Initialize the model once\n", + "model = SimpleCNN()\n", + "\n", + "# Random choices of optimizer and loss functions\n", + "# These are pretty standard for classification\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)\n", + "criterion = torch.nn.BCELoss() # BCE = Binary Cross Entropy (for 2 classes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6XyF3QGjARAy" + }, + "source": [ + "# Train the model\n", + "\n", + "We have all the pieces in place:\n", + "- [x] Model\n", + "- [x] Train Data\n", + "- [x] Validation Data\n", + "- [x] Loss Function\n", + "- [x] Optimizer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "adEJ4vhuARAy" + }, + "outputs": [], + "source": [ + "\n", + "def train_epoch(model, dataloader, optimizer, criterion):\n", + " \"\"\"Trains the model for a single epoch (one loop through the entire training dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Training...\")\n", + " for images, labels in iterer:\n", + " optimizer.zero_grad() # Clear the gradients from the previous step\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " loss.backward() # Backward pass: compute the gradients of the loss with respect to the model parameters\n", + " # Update the model parameters using the computed gradients\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "\n", + "def val_epoch(model, dataloader, criterion):\n", + " \"\"\"Evaluates the model on the validation set for a single epoch (one loop through the entire validation dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Validating...\")\n", + " # Disable gradient computation for validation\n", + " for images, labels in iterer:\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "# One epoch should take around 15 seconds\n", + "# The number of epochs is depednent on the complexity and size of your data\n", + "n_epochs = 2\n", + "loss_history = {\"train\": [], \"val\": []}\n", + "for epoch in range(n_epochs):\n", + " model, train_loss = train_epoch(model, train_generator.get_dataloader(), optimizer, criterion)\n", + " model, val_loss = val_epoch(model, val_generator.get_dataloader(), criterion)\n", + "\n", + " loss_history[\"train\"].append(train_loss)\n", + " loss_history[\"val\"].append(val_loss)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3QCcxf0dARAy" + }, + "source": [ + "## Model Evaluation\n", + "\n", + "There are some steps we can take to see how well a model trained.\n", + "\n", + "### Loss Plots\n", + "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training.\n", + "Or, if the loss never moves or blows up entirely, that's a sign there's a problem.\n", + "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pfDIpqHGARAy" + }, + "outputs": [], + "source": [ + "Eval.plot_loss_history(loss_history['train'], loss_history['val'])\n", + "# Eval.plot_history is a simple function\n", + "# plots the loss as a function of epoch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V_9R4WYqARAy" + }, + "source": [ + "## Classification Accuracy Plots\n", + "\n", + "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training.\n", + "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it.\n", + "\n", + "### ROC\n", + "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate.\n", + "It shows how likely a model is to correctly predict something.\n", + "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves.\n", + "\n", + "\n", + "### Confusion Matrix\n", + "\n", + "Confusion matrices are a great tool for seeing how well each class does against each other.\n", + "It gets it name from its ability to tell if a model is \"confusing\" two different classes.\n", + "It plots the rate of predicted values for a given class versus the true values.\n", + "\n", + " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iP_xgNcMARAy" + }, + "outputs": [], + "source": [ + "test_generator = SkyGenerator(n_samples=1280, dataset=TestDataset, shuffle=True).get_dataloader()\n", + "\n", + "def make_prediction(model, test_generator):\n", + " predictions = []\n", + " labels = []\n", + " for image, label in test_generator:\n", + " predictions += model(transforms(image))\n", + " labels += label\n", + " prediction_classes = torch.where(torch.tensor(predictions)<0.5, 0, 1)\n", + " labels = torch.tensor(labels)\n", + " return prediction_classes, labels\n", + "\n", + "def test_quality(prediction, labels):\n", + " accuracy = torch.mean((prediction == labels).float()) # Compute the accuracy by comparing the predicted classes to the true labels\n", + " return accuracy.numpy()\n", + "\n", + "prediction, labels = make_prediction(model, test_generator)\n", + "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5f5Q78_IARAy" + }, + "outputs": [], + "source": [ + "# And now run them on your own data!\n", + "Eval.ROC_curve(prediction, labels)\n", + "Eval.confusion_matrix(prediction, labels)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G671xYOV7yfd" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "base", + "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.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/Challenge/challenges/challenge_01.ipynb b/Challenge/challenges/challenge_01.ipynb deleted file mode 100644 index 33b9d73..0000000 --- a/Challenge/challenges/challenge_01.ipynb +++ /dev/null @@ -1,493 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Training Challenge! \n", - "\n", - "In this notebook, there are a whole host of weird errors. \n", - "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking. \n", - "Do your best to sniff out the mistakes in both code and training procedure! \n", - "\n", - "Your mission, should you choose to accept it, is: \n", - "* Work together in groups of 4 to complete this notebook\n", - "* Figure out all the coding mistakes and make a notebook that runs\n", - "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", - "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them.\n", - "\n", - "### Rules \n", - "* If you cannot correct the problem, identify it and write a quick paragraph about what you would do\n", - "* You are allowed to use any resource you can find; other groups, TA's, a random scientist walking by, and most importantly, the internet. The use of generative AI is highly discouraged - not because it's cheating, but because it can send you down rabbit holes that are hard to find your way out of. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Installing the data generation package \n", - "! pip install deepbench" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials_2024/main/Challenge/challenge_utilities.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Now we'll import the different classes from this new file \n", - "from challenge_utilities import SkyGenerator01 as SkyGenerator # `as` renames the imported package name\n", - "from challenge_utilities import Eval" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you have an issue with this, re-downloading the file may fix it.\n", - "If this still doesn't help, [directly pull the file from the github link](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/40758642b5570a3fe53dc49da237a19f5e748a78/07_Challenge/challenge_utilities.py)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Standard packages \n", - "\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from tensorflow.keras.layers import Input, Dropout, Conv1D, Dense, AvgPool1D, Flatten\n", - "from tensorflow.keras.models import Model\n", - "\n", - "import tensorflow as tf \n", - "from sklearn.preprocessing import MinMaxScaler" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Package documentation \n", - "\n", - "If you get stuck for syntax or anything - these are the packages used. \n", - "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names. \n", - "\n", - "[Numpy]()\n", - "\n", - "[MatPlotLib]() \n", - "\n", - "[Tensorflow/tf/Keras]()\n", - "\n", - "[Sci-kit Learn]()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "label_map = {\n", - " 0: \"Star\", \n", - " 1: \"Galaxy\"\n", - "}\n", - "\n", - "\n", - "def plot_samples(generator, n_columns=3, n_rows=3, label_map=None): \n", - " _, subplots = plt.subplots(n_columns, 1) # Make 9 plots in a 3 x 3 grid \n", - " plt.tight_layout()\n", - " plt.setp(subplots, xticks=[], yticks=[])\n", - "\n", - " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()): \n", - " image, label = generator[sample_index]\n", - " subplot.imshow(image.squeeze()) \n", - " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels). \n", - " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", - " \n", - " string_label = \"??\" \n", - " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", - "\n", - "samples = SkyGenerator(n_samples=9, batch_size=1) # Can just get a few samples\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Look at the input data \n", - "\n", - "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_train_samples = 1280\n", - "train_generator = SkyGenerator(n_samples=n_train_samples, shuffle=True)\n", - "\n", - "n_val_samples = 1280\n", - "val_generator = SkyGenerator(n_samples=n_val_samples, train=False, shuffle=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can look at the distribution of labels by grabbing them from the generator \n", - " # TF.Sequence generators supply data as a tuple \n", - " # of (x,y) (or (features, labels)) \n", - " # - so using index 1 we can get the labels\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][1] for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0] for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots.hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.supxlabel(\"Label Distributions\")\n", - "figure.supylabel(\"Label Frequency\")\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can also look at different attributes of the images \n", - "# Here, let's look at max values and mean values\n", - "\n", - "# These numbers will be slightly different every time, there's randomness controlling how the images are generated \n", - "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().mean() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].mean() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Mean value of pixels in a single image\")\n", - "plt.show()\n", - "\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().max() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].ravel().max() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Max value of pixels in a single image\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data Processing\n", - "\n", - "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", - "There are near infinite ways to pre-process, especially in the computer vision space. \n", - "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways. \n", - "\n", - "For this, let's try bringing all the pixels in the image between 0 and 1. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scaler = MinMaxScaler((0, 1))\n", - "\n", - "fit_data = np.concatenate([train_generator[i][0] for i in range(5)], axis=0) \n", - "# This is 5 batches of data, being used to find the approximate min and max of the data\n", - "# Because our data is synthetic, we don't need to worry about big outliers\n", - "\n", - "# Unfortunately, minmaxscaler only handles 1d of data, so we need to do a little pre-and-post processing on the input \n", - "# combining the last two dimensions together, making the 2d of the image 1d instead. \n", - "fit_data = fit_data.reshape((fit_data.shape[0], fit_data.shape[1]*fit_data.shape[2]) ) \n", - "scaler_fit = scaler.fit(fit_data)\n", - "\n", - "def processor(image): \n", - " # This function will take a single image and return the scaled version \n", - " image_flat = image.ravel() \n", - " image_scaled = scaler_fit.transform(image_flat.reshape(1, -1))\n", - " # Magically reshape it back into 2d \n", - " image_scaled_reshaped = image_scaled.reshape(image.shape)\n", - " return image_scaled_reshaped" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Look at the samples again! \n", - "\n", - "n_samples = 9\n", - "samples = SkyGenerator(n_samples=n_samples, batch_size=1, pre_processing=processor)\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Make the binary classification model \n", - "\n", - "This function, when called, produces a keras Model instance that you can train to predict a class of an input. \n", - "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1. \n", - "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out. \n", - "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it. \n", - "\n", - "In this case, what we want: \n", - "* Take the input images from the data generator \n", - "* Apply two convolutional blocks to the input image \n", - "* Decode the second convolution block's output to a probability of the image being a given class. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def make_model(): \n", - " \"\"\"\n", - " Make a network that can perform binary classification\n", - "\n", - " Returns:\n", - " model (keras.Model): classifier model that will \n", - " \"\"\"\n", - " in_layer = Input((2, 2))\n", - " x = Conv1D(filters=4, kernel_size=2)(input_layer)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - " \n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Dense(20, activation='relu')\n", - "\n", - " x = Dropout(0.3)(x)\n", - " output = Dense(10, activation='softmax')(x)\n", - " model = Model(in_layer, x)\n", - " \n", - " return model\n", - "\n", - "model = make_model()\n", - "loss = tf.losses.BinaryCrossentropy()\n", - "optimizer = tf.keras.optimizers.SGD(0.0001)\n", - "\n", - "# Compile tells the keras backend what loss and optimizer to use to perform gradients on the model\n", - "# You cannot train a keras model without compiling it first\n", - "model.compile(loss=loss, optimizer=optimizer)\n", - "\n", - "# Show what layers are in the model, and their input and output shapes \n", - "# This can help make sure all your stuff is a correct size\n", - "model.summary()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Train the model \n", - "\n", - "We have all the pieces in place:\n", - "- [x] Model \n", - "- [x] Train Data \n", - "- [x] Validation Data \n", - "- [x] Loss Function \n", - "- [x] Optimizer \n", - "\n", - "Now lets put this together into a fit model. \n", - "Keras trains in place, so you don't need a new variable to hold the `fit_model` vs `model`. \n", - "Once you call `fit`, the model is fit, and it re-train, you need to make a new model with the `make_model()` function. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "train_generator_scaled = SkyGenerator(n_samples=1280, pre_processing=processor, batch_size=64)\n", - "val_generator_scaled = SkyGenerator(n_samples=12, pre_processing=None, batch_size=64)\n", - "\n", - "history = model.fit(\n", - " train_generator_scaled, \n", - " validation_data=train_generator_scaled, \n", - " epochs=50, \n", - " verbose=1\n", - " ).history\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Evaluation \n", - "\n", - "There are some steps we can take to see how well a model trained. \n", - "\n", - "### Loss Plots \n", - "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training. \n", - "Or, if the loss never moves or blows up entirely, that's a sign there's a problem. \n", - "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems! " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Eval.plot_loss_history(history) \n", - "# Eval.plot_history is a simple function \n", - "# plots the loss as a function of epoch " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Classification Accuracy Plots \n", - "\n", - "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training. \n", - "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it. \n", - "\n", - "### ROC\n", - "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate. \n", - "It shows how likely a model is to correctly predict something. \n", - "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves. \n", - "\n", - "\n", - "\n", - "Roc-draft-xkcd-style.svg, CC BY-SA 4.0, Link\n", - "\n", - "\n", - "### Confusion Matrix\n", - "\n", - "Confusion matrices are a great tool for seeing how well each class does against each other. \n", - "It gets it name from its ability to tell if a model is \"confusing\" two different classes. \n", - "It plots the rate of predicted values for a given class versus the true values. \n", - "\n", - "\n", - "\n", - " Link\n", - "\n", - " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_generator = SkyGenerator(n_samples=1280, train=False, shuffle=True)\n", - "\n", - "def make_prediction(model, test_generator): \n", - " predictions = trained_model.predict(test_generator)\n", - " prediction_classes = np.where(predictions<0.5, 0, 1)\n", - " labels = test_generator.labels\n", - " return prediction_classes, labels\n", - "\n", - "def test_quality(prediction, labels): \n", - " accuracy = tf.keras.metrics.BinaryAccuracy()(labels, labels)\n", - " return accuracy.numpy()\n", - "\n", - "prediction, labels = make_prediction(model, test_generator)\n", - "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# And now run them on your own data! \n", - "Eval.ROC_curve(prediction, labels)\n", - "Eval.confusion_matrix(labels, labels)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.11" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/Challenge/challenges/challenge_02.ipynb b/Challenge/challenges/challenge_02.ipynb deleted file mode 100644 index 89f3da0..0000000 --- a/Challenge/challenges/challenge_02.ipynb +++ /dev/null @@ -1,502 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Training Challenge! \n", - "\n", - "In this notebook, there are a whole host of weird errors. \n", - "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking. \n", - "Do your best to sniff out the mistakes in both code and training procedure! \n", - "\n", - "Your mission, should you choose to accept it, is: \n", - "* Work together in groups of 4 to complete this notebook\n", - "* Figure out all the coding mistakes and make a notebook that runs\n", - "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", - "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them.\n", - "\n", - "### Rules \n", - "* If you cannot correct the problem, identify it and write a quick paragraph about what you would do\n", - "* You are allowed to use any resource you can find; other groups, TA's, a random scientist walking by, and most importantly, the internet. The use of generative AI is highly discouraged - not because it's cheating, but because it can send you down rabbit holes that are hard to find your way out of. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Installing the data generation package \n", - "! pip install deepbench" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials_2024/main/Challenge/challenge_utilities.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Now we'll import the different classes from this new file \n", - "from challenge_utilities import SkyGenerator02 as SkyGenerator # `as` renames the imported package name\n", - "from challenge_utilities import Eval" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you have an issue with this, re-downloading the file may fix it.\n", - "If this still doesn't help, [directly pull the file from the github link](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/40758642b5570a3fe53dc49da237a19f5e748a78/07_Challenge/challenge_utilities.py)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Standard packages \n", - "\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from tensorflow.keras.layers import Input, Dropout, Conv1D, Dense, AvgPool1D, Flatten\n", - "from tensorflow.keras.models import Model\n", - "\n", - "import tensorflow as tf \n", - "from sklearn.preprocessing import MinMaxScaler" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Package documentation \n", - "\n", - "If you get stuck for syntax or anything - these are the packages used. \n", - "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names. \n", - "\n", - "[Numpy]()\n", - "\n", - "[MatPlotLib]() \n", - "\n", - "[Tensorflow/tf/Keras]()\n", - "\n", - "[Sci-kit Learn]()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "label_map = {\n", - " 0: \"Star\", \n", - " 1: \"Galaxy\"\n", - "}\n", - "\n", - "\n", - "def plot_samples(generator, n_columns=3, n_rows=3, label_map=None): \n", - " _, subplots = plt.subplots(n_columns, 1) # Make 9 plots in a 3 x 3 grid \n", - " plt.tight_layout()\n", - " plt.setp(subplots, xticks=[], yticks=[])\n", - "\n", - " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()): \n", - " image, label = generator[sample_index]\n", - " subplot.imshow(image.squeeze()) \n", - " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels). \n", - " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", - " \n", - " string_label = \"??\" \n", - " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", - "\n", - "samples = SkyGenerator(n_samples=9, batch_size=1) # Can just get a few samples\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Look at the input data \n", - "\n", - "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_train_samples = 1280\n", - "train_generator = SkyGenerator(n_samples=n_train_samples, shuffle=True)\n", - "\n", - "n_val_samples = 1280\n", - "val_generator = SkyGenerator(n_samples=n_val_samples, train=False, shuffle=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can look at the distribution of labels by grabbing them from the generator \n", - " # TF.Sequence generators supply data as a tuple \n", - " # of (x,y) (or (features, labels)) \n", - " # - so using index 1 we can get the labels\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][1] for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0] for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots.hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.supxlabel(\"Label Distributions\")\n", - "figure.supylabel(\"Label Frequency\")\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can also look at different attributes of the images \n", - "# Here, let's look at max values and mean values\n", - "\n", - "# These numbers will be slightly different every time, there's randomness controlling how the images are generated \n", - "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().mean() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].mean() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Mean value of pixels in a single image\")\n", - "plt.show()\n", - "\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().max() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].ravel().max() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Max value of pixels in a single image\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data Processing\n", - "\n", - "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", - "There are near infinite ways to pre-process, especially in the computer vision space. \n", - "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways. \n", - "\n", - "For this, let's try bringing all the pixels in the image between 0 and 1. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scaler = MinMaxScaler((0, 1))\n", - "\n", - "fit_data = np.concatenate([train_generator[i][0] for i in range(5)], axis=0) \n", - "# This is 5 batches of data, being used to find the approximate min and max of the data\n", - "# Because our data is synthetic, we don't need to worry about big outliers\n", - "\n", - "# Unfortunately, minmaxscaler only handles 1d of data, so we need to do a little pre-and-post processing on the input \n", - "# combining the last two dimensions together, making the 2d of the image 1d instead. \n", - "fit_data = fit_data.reshape((fit_data.shape[0], fit_data.shape[1]*fit_data.shape[2]) ) \n", - "scaler_fit = scaler.fit(fit_data)\n", - "\n", - "def processor(image): \n", - " # This function will take a single image and return the scaled version \n", - " image_flat = image.ravel() \n", - " image_scaled = scaler_fit.transform(image_flat.reshape(1, -1))\n", - " # Magically reshape it back into 2d \n", - " image_scaled_reshaped = image_scaled.reshape(image.shape)\n", - " return image_scaled_reshaped" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Look at the samples again! \n", - "\n", - "n_samples = 9\n", - "samples = SkyGenerator(n_samples=n_samples, batch_size=1, pre_processing=processor)\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Make the binary classification model \n", - "\n", - "This function, when called, produces a keras Model instance that you can train to predict a class of an input. \n", - "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1. \n", - "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out. \n", - "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it. \n", - "\n", - "In this case, what we want: \n", - "* Take the input images from the data generator \n", - "* Apply two convolutional blocks to the input image \n", - "* Decode the second convolution block's output to a probability of the image being a given class. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def make_model(): \n", - " \"\"\"\n", - " Make a network that can perform binary classification\n", - "\n", - " Returns:\n", - " model (keras.Model): classifier model that will \n", - " \"\"\"\n", - " in_layer = Input((28, 28))\n", - " x = Conv1D(filters=4, kernel_size=2)(input_layer)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - " \n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Conv1D(filters=4, kernel_size=2)(x)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - " \n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Flatten()(x)\n", - " x = Dense(20, activation='relu')\n", - "\n", - " x = Dropout(0.9)(x)\n", - " output = Dense(1, activation='sigmoid')(x)\n", - " model = Model(in_layer, x)\n", - " \n", - " return model\n", - " \n", - " return Model(0, 0)\n", - "\n", - "model = make_model()\n", - "loss = tf.losses.BinaryCrossentropy()\n", - "optimizer = tf.keras.optimizers.SGD(0.01)\n", - "\n", - "# Compile tells the keras backend what loss and optimizer to use to perform gradients on the model\n", - "# You cannot train a keras model without compiling it first\n", - "model.compile(loss=loss, optimizer=optimizer)\n", - "\n", - "# Show what layers are in the model, and their input and output shapes \n", - "# This can help make sure all your stuff is a correct size\n", - "model.summary()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Train the model \n", - "\n", - "We have all the pieces in place:\n", - "- [x] Model \n", - "- [x] Train Data \n", - "- [x] Validation Data \n", - "- [x] Loss Function \n", - "- [x] Optimizer \n", - "\n", - "Now lets put this together into a fit model. \n", - "Keras trains in place, so you don't need a new variable to hold the `fit_model` vs `model`. \n", - "Once you call `fit`, the model is fit, and it re-train, you need to make a new model with the `make_model()` function. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "train_generator_scaled = SkyGenerator(n_samples=1280, pre_processing=processor, batch_size=64)\n", - "val_generator_scaled = SkyGenerator(n_samples=12, pre_processing=None, batch_size=64)\n", - "\n", - "history = model.fit(\n", - " train_generator_scaled, \n", - " validation_data=train_generator_scaled, \n", - " epochs=50, \n", - " verbose=1\n", - " ).history\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Evaluation \n", - "\n", - "There are some steps we can take to see how well a model trained. \n", - "\n", - "### Loss Plots \n", - "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training. \n", - "Or, if the loss never moves or blows up entirely, that's a sign there's a problem. \n", - "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems! " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Eval.plot_loss_history(history) \n", - "# Eval.plot_history is a simple function \n", - "# plots the loss as a function of epoch " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Classification Accuracy Plots \n", - "\n", - "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training. \n", - "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it. \n", - "\n", - "### ROC\n", - "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate. \n", - "It shows how likely a model is to correctly predict something. \n", - "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves. \n", - "\n", - "\n", - "\n", - "Roc-draft-xkcd-style.svg, CC BY-SA 4.0, Link\n", - "\n", - "\n", - "### Confusion Matrix\n", - "\n", - "Confusion matrices are a great tool for seeing how well each class does against each other. \n", - "It gets it name from its ability to tell if a model is \"confusing\" two different classes. \n", - "It plots the rate of predicted values for a given class versus the true values. \n", - "\n", - "\n", - "\n", - " Link\n", - "\n", - " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_generator = SkyGenerator(n_samples=1280, train=False, shuffle=True)\n", - "\n", - "def make_prediction(model, test_generator): \n", - " predictions = trained_model.predict(test_generator)\n", - " prediction_classes = np.where(predictions<0.01, 0, 1)\n", - " labels = test_generator.labels\n", - " return prediction_classes, labels\n", - "\n", - "def test_quality(prediction, labels): \n", - " accuracy = tf.keras.metrics.BinaryAccuracy()(labels, labels)\n", - " return accuracy.numpy()\n", - "\n", - "prediction, labels = make_prediction(model, test_generator)\n", - "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# And now run them on your own data! \n", - "Eval.ROC_curve(prediction, labels)\n", - "Eval.confusion_matrix(labels, labels)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.11" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/Challenge/challenges/challenge_03.ipynb b/Challenge/challenges/challenge_03.ipynb deleted file mode 100644 index 284aaec..0000000 --- a/Challenge/challenges/challenge_03.ipynb +++ /dev/null @@ -1,540 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Training Challenge! \n", - "\n", - "In this notebook, there are a whole host of weird errors. \n", - "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking. \n", - "Do your best to sniff out the mistakes in both code and training procedure! \n", - "\n", - "Your mission, should you choose to accept it, is: \n", - "* Work together in groups of 4 to complete this notebook\n", - "* Figure out all the coding mistakes and make a notebook that runs\n", - "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", - "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them.\n", - "\n", - "### Rules \n", - "* If you cannot correct the problem, identify it and write a quick paragraph about what you would do\n", - "* You are allowed to use any resource you can find; other groups, TA's, a random scientist walking by, and most importantly, the internet. The use of generative AI is highly discouraged - not because it's cheating, but because it can send you down rabbit holes that are hard to find your way out of. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Installing the data generation package \n", - "! pip install deepbench" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials_2024/main/Challenge/challenge_utilities.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Now we'll import the different classes from this new file \n", - "from challenge_utilities import SkyGenerator03 as SkyGenerator # `as` renames the imported package name\n", - "from challenge_utilities import Eval" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you have an issue with this, re-downloading the file may fix it.\n", - "If this still doesn't help, [directly pull the file from the github link](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/40758642b5570a3fe53dc49da237a19f5e748a78/07_Challenge/challenge_utilities.py)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Standard packages \n", - "\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from tensorflow.keras.layers import Input, Dropout, Conv1D, Dense, AvgPool1D, Flatten\n", - "from tensorflow.keras.models import Model\n", - "\n", - "import tensorflow as tf \n", - "from sklearn.preprocessing import MinMaxScaler" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Package documentation \n", - "\n", - "If you get stuck for syntax or anything - these are the packages used. \n", - "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names. \n", - "\n", - "[Numpy]()\n", - "\n", - "[MatPlotLib]() \n", - "\n", - "[Tensorflow/tf/Keras]()\n", - "\n", - "[Sci-kit Learn]()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "label_map = {\n", - " 0: \"Star\", \n", - " 1: \"Galaxy\"\n", - "}\n", - "\n", - "\n", - "def plot_samples(generator, n_columns=3, n_rows=3, label_map=None): \n", - " _, subplots = plt.subplots(n_columns, 1) # Make 9 plots in a 3 x 3 grid \n", - " plt.tight_layout()\n", - " plt.setp(subplots, xticks=[], yticks=[])\n", - "\n", - " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()): \n", - " image, label = generator[sample_index]\n", - " subplot.imshow(image.squeeze()) \n", - " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels). \n", - " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", - " \n", - " string_label = \"??\" \n", - " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", - "\n", - "samples = SkyGenerator(n_samples=9, batch_size=1) # Can just get a few samples\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Look at the input data \n", - "\n", - "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_train_samples = 1280\n", - "train_generator = SkyGenerator(n_samples=n_train_samples, shuffle=True)\n", - "\n", - "n_val_samples = 1280\n", - "val_generator = SkyGenerator(n_samples=n_val_samples, train=False, shuffle=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can look at the distribution of labels by grabbing them from the generator \n", - " # TF.Sequence generators supply data as a tuple \n", - " # of (x,y) (or (features, labels)) \n", - " # - so using index 1 we can get the labels\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][1] for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0] for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots.hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.supxlabel(\"Label Distributions\")\n", - "figure.supylabel(\"Label Frequency\")\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can also look at different attributes of the images \n", - "# Here, let's look at max values and mean values\n", - "\n", - "# These numbers will be slightly different every time, there's randomness controlling how the images are generated \n", - "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().mean() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].mean() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Mean value of pixels in a single image\")\n", - "plt.show()\n", - "\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().max() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].ravel().max() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Max value of pixels in a single image\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data Processing\n", - "\n", - "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", - "There are near infinite ways to pre-process, especially in the computer vision space. \n", - "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways. \n", - "\n", - "For this, let's try bringing all the pixels in the image between 0 and 1. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scaler = MinMaxScaler((0, 1))\n", - "\n", - "fit_data = np.concatenate([train_generator[i][0] for i in range(5)], axis=0) \n", - "# This is 5 batches of data, being used to find the approximate min and max of the data\n", - "# Because our data is synthetic, we don't need to worry about big outliers\n", - "\n", - "# Unfortunately, minmaxscaler only handles 1d of data, so we need to do a little pre-and-post processing on the input \n", - "# combining the last two dimensions together, making the 2d of the image 1d instead. \n", - "fit_data = fit_data.reshape((fit_data.shape[0], fit_data.shape[1]*fit_data.shape[2]) ) \n", - "scaler_fit = scaler.fit(fit_data)\n", - "\n", - "def processor(image): \n", - " # This function will take a single image and return the scaled version \n", - " image_flat = image.ravel() \n", - " image_scaled = scaler_fit.transform(image_flat.reshape(1, -1))\n", - " # Magically reshape it back into 2d \n", - " image_scaled_reshaped = image_scaled.reshape(image.shape)\n", - " return image_scaled_reshaped" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Look at the samples again! \n", - "\n", - "n_samples = 9\n", - "samples = SkyGenerator(n_samples=n_samples, batch_size=1, pre_processing=processor)\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Make the binary classification model \n", - "\n", - "This function, when called, produces a keras Model instance that you can train to predict a class of an input. \n", - "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1. \n", - "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out. \n", - "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it. \n", - "\n", - "In this case, what we want: \n", - "* Take the input images from the data generator \n", - "* Apply two convolutional blocks to the input image \n", - "* Decode the second convolution block's output to a probability of the image being a given class. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def make_model(): \n", - " \"\"\"\n", - " Make a network that can perform binary classification\n", - "\n", - " Returns:\n", - " model (keras.Model): classifier model that will \n", - " \"\"\"\n", - " in_layer = Input((28, 28))\n", - " x = Conv1D(filters=4, kernel_size=2)(input_layer)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - " \n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Dense(20, activation='relu')\n", - "\n", - " x = Dropout(0.3)(x)\n", - " output = Dense(10, activation='softmax')(x)\n", - " model = Model(in_layer, x)\n", - " \n", - " return model\n", - "\n", - "model = make_model()\n", - "loss = tf.losses.MeanSquaredError()\n", - "optimizer = tf.keras.optimizers.SGD(0.8)\n", - "\n", - "# Compile tells the keras backend what loss and optimizer to use to perform gradients on the model\n", - "# You cannot train a keras model without compiling it first\n", - "model.compile(loss=loss, optimizer=optimizer)\n", - "\n", - "# Show what layers are in the model, and their input and output shapes \n", - "# This can help make sure all your stuff is a correct size\n", - "model.summary()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Train the model \n", - "\n", - "We have all the pieces in place:\n", - "- [x] Model \n", - "- [x] Train Data \n", - "- [x] Validation Data \n", - "- [x] Loss Function \n", - "- [x] Optimizer \n", - "\n", - "Now lets put this together into a fit model. \n", - "Keras trains in place, so you don't need a new variable to hold the `fit_model` vs `model`. \n", - "Once you call `fit`, the model is fit, and it re-train, you need to make a new model with the `make_model()` function. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def train_model(model): \n", - " \"\"\"\n", - " Train a model on different training and validation data and show the progress with a plot\n", - "\n", - " Args:\n", - " model (keras.Model): Compiled model you want to train with the SkyGenerator data\n", - "\n", - " Returns:\n", - " tuple(keras.Model, dict): the trained model object and the history\n", - " \"\"\"\n", - " train_generator = SkyGenerator(n_sample=1280, shuffle=True)\n", - " val_generator = SkyGenerator(n_samples=12, train=False, shuffle=True)\n", - "\n", - " history = model.fit(\n", - " train_generator, \n", - " validation_data=train_generator, \n", - " epchs=1, \n", - " verbose=1\n", - " ).history\n", - " \n", - " ########### No Touchie Please ##########\n", - "\n", - " loss = history['loss']\n", - " epochs = range(len(loss))\n", - "\n", - " val_loss = history['val_loss']\n", - "\n", - " plt.plot(epochs, loss, label=\"Train\")\n", - " plt.plot(epochs, val_loss, label='Validation')\n", - "\n", - " plt.title(\"Loss History\")\n", - " plt.xlabel(\"Epoch\")\n", - " plt.ylabel(\"Loss\")\n", - " plt.legend()\n", - " plt.show()\n", - " \n", - " #####################\n", - "\n", - " return model, history" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "train_generator_scaled = SkyGenerator(n_samples=1280, pre_processing=processor, batch_size=64)\n", - "val_generator_scaled = SkyGenerator(n_samples=12, pre_processing=None, batch_size=64)\n", - "\n", - "history = model.fit(\n", - " train_generator_scaled, \n", - " validation_data=train_generator_scaled, \n", - " epchs=1, \n", - " verbose=1\n", - " ).history\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Evaluation \n", - "\n", - "There are some steps we can take to see how well a model trained. \n", - "\n", - "### Loss Plots \n", - "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training. \n", - "Or, if the loss never moves or blows up entirely, that's a sign there's a problem. \n", - "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems! " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Eval.plot_loss_history(history) \n", - "# Eval.plot_history is a simple function \n", - "# plots the loss as a function of epoch " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Classification Accuracy Plots \n", - "\n", - "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training. \n", - "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it. \n", - "\n", - "### ROC\n", - "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate. \n", - "It shows how likely a model is to correctly predict something. \n", - "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves. \n", - "\n", - "\n", - "\n", - "Roc-draft-xkcd-style.svg, CC BY-SA 4.0, Link\n", - "\n", - "\n", - "### Confusion Matrix\n", - "\n", - "Confusion matrices are a great tool for seeing how well each class does against each other. \n", - "It gets it name from its ability to tell if a model is \"confusing\" two different classes. \n", - "It plots the rate of predicted values for a given class versus the true values. \n", - "\n", - "\n", - "\n", - " Link\n", - "\n", - " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_generator = SkyGenerator(n_samples=1280, train=False, shuffle=True)\n", - "\n", - "def make_prediction(model, test_generator): \n", - " predictions = trained_model.predict(test_generator)\n", - " prediction_classes = np.where(predictions<0.5, 0, 1)\n", - " labels = test_generator.labels\n", - " return prediction_classes, labels\n", - "\n", - "def test_quality(prediction, labels): \n", - " accuracy = tf.keras.metrics.BinaryAccuracy()(labels, labels)\n", - " return accuracy.numpy()\n", - "\n", - "prediction, labels = make_prediction(model, test_generator)\n", - "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# And now run them on your own data! \n", - "Eval.ROC_curve(prediction, labels)\n", - "Eval.confusion_matrix(labels, labels)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.11" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/Challenge/challenges/challenge_04.ipynb b/Challenge/challenges/challenge_04.ipynb deleted file mode 100644 index 365a648..0000000 --- a/Challenge/challenges/challenge_04.ipynb +++ /dev/null @@ -1,499 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Training Challenge! \n", - "\n", - "In this notebook, there are a whole host of weird errors. \n", - "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking. \n", - "Do your best to sniff out the mistakes in both code and training procedure! \n", - "\n", - "Your mission, should you choose to accept it, is: \n", - "* Work together in groups of 4 to complete this notebook\n", - "* Figure out all the coding mistakes and make a notebook that runs\n", - "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", - "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them.\n", - "\n", - "### Rules \n", - "* If you cannot correct the problem, identify it and write a quick paragraph about what you would do\n", - "* You are allowed to use any resource you can find; other groups, TA's, a random scientist walking by, and most importantly, the internet. The use of generative AI is highly discouraged - not because it's cheating, but because it can send you down rabbit holes that are hard to find your way out of. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Installing the data generation package \n", - "! pip install deepbench" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials_2024/main/Challenge/challenge_utilities.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Now we'll import the different classes from this new file \n", - "from challenge_utilities import SkyGenerator04 as SkyGenerator # `as` renames the imported package name\n", - "from challenge_utilities import Eval" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you have an issue with this, re-downloading the file may fix it.\n", - "If this still doesn't help, [directly pull the file from the github link](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/40758642b5570a3fe53dc49da237a19f5e748a78/07_Challenge/challenge_utilities.py)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Standard packages \n", - "\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from tensorflow.keras.layers import Input, Dropout, Conv1D, Dense, AvgPool1D, Flatten\n", - "from tensorflow.keras.models import Model\n", - "\n", - "import tensorflow as tf \n", - "from sklearn.preprocessing import MinMaxScaler" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Package documentation \n", - "\n", - "If you get stuck for syntax or anything - these are the packages used. \n", - "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names. \n", - "\n", - "[Numpy]()\n", - "\n", - "[MatPlotLib]() \n", - "\n", - "[Tensorflow/tf/Keras]()\n", - "\n", - "[Sci-kit Learn]()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "label_map = {\n", - " 0: \"Star\", \n", - " 1: \"Galaxy\"\n", - "}\n", - "\n", - "\n", - "def plot_samples(generator, n_columns=3, n_rows=3, label_map=None): \n", - " _, subplots = plt.subplots(n_columns, 1) # Make 9 plots in a 3 x 3 grid \n", - " plt.tight_layout()\n", - " plt.setp(subplots, xticks=[], yticks=[])\n", - "\n", - " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()): \n", - " image, label = generator[sample_index]\n", - " subplot.imshow(image.squeeze()) \n", - " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels). \n", - " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", - " \n", - " string_label = \"??\" \n", - " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", - "\n", - "samples = SkyGenerator(n_samples=9, batch_size=1) # Can just get a few samples\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Look at the input data \n", - "\n", - "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_train_samples = 1280\n", - "train_generator = SkyGenerator(n_samples=n_train_samples, shuffle=True)\n", - "\n", - "n_val_samples = 1280\n", - "val_generator = SkyGenerator(n_samples=n_val_samples, train=False, shuffle=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can look at the distribution of labels by grabbing them from the generator \n", - " # TF.Sequence generators supply data as a tuple \n", - " # of (x,y) (or (features, labels)) \n", - " # - so using index 1 we can get the labels\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][1] for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0] for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots.hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.supxlabel(\"Label Distributions\")\n", - "figure.supylabel(\"Label Frequency\")\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can also look at different attributes of the images \n", - "# Here, let's look at max values and mean values\n", - "\n", - "# These numbers will be slightly different every time, there's randomness controlling how the images are generated \n", - "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().mean() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].mean() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Mean value of pixels in a single image\")\n", - "plt.show()\n", - "\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().max() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].ravel().max() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Max value of pixels in a single image\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data Processing\n", - "\n", - "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", - "There are near infinite ways to pre-process, especially in the computer vision space. \n", - "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways. \n", - "\n", - "For this, let's try bringing all the pixels in the image between 0 and 1. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scaler = MinMaxScaler((0, 1))\n", - "\n", - "fit_data = np.concatenate([train_generator[i][0] for i in range(5)], axis=0) \n", - "# This is 5 batches of data, being used to find the approximate min and max of the data\n", - "# Because our data is synthetic, we don't need to worry about big outliers\n", - "\n", - "# Unfortunately, minmaxscaler only handles 1d of data, so we need to do a little pre-and-post processing on the input \n", - "# combining the last two dimensions together, making the 2d of the image 1d instead. \n", - "fit_data = fit_data.reshape((fit_data.shape[0], fit_data.shape[1]*fit_data.shape[2]) ) \n", - "scaler_fit = scaler.fit(fit_data)\n", - "\n", - "def processor(image): \n", - " # This function will take a single image and return the scaled version \n", - " image_flat = image.ravel() \n", - " image_scaled = scaler_fit.transform(image_flat.reshape(1, -1))\n", - " # Magically reshape it back into 2d \n", - " image_scaled_reshaped = image_scaled.reshape(image.shape)\n", - " return image_scaled_reshaped" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Look at the samples again! \n", - "\n", - "n_samples = 9\n", - "samples = SkyGenerator(n_samples=n_samples, batch_size=1, pre_processing=processor)\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Make the binary classification model \n", - "\n", - "This function, when called, produces a keras Model instance that you can train to predict a class of an input. \n", - "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1. \n", - "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out. \n", - "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it. \n", - "\n", - "In this case, what we want: \n", - "* Take the input images from the data generator \n", - "* Apply two convolutional blocks to the input image \n", - "* Decode the second convolution block's output to a probability of the image being a given class. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def make_model(): \n", - " \"\"\"\n", - " Make a network that can perform binary classification\n", - "\n", - " Returns:\n", - " model (keras.Model): classifier model that will \n", - " \"\"\"\n", - " in_layer = Input((28, 28))\n", - " x = Conv1D(filters=4, kernel_size=2)(input_layer)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - " \n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Conv1D(filters=4, kernel_size=2)(x)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - "\n", - " x = Dense(20, activation='relu')\n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Flatten()(x)\n", - "\n", - " x = Dropout(0.3)(x)\n", - " output = Dense(10, activation='sigmoid')(x)\n", - " model = Model(in_layer, x)\n", - " \n", - " return model\n", - "\n", - "model = make_model()\n", - "loss = tf.losses.BinaryCrossropy()\n", - "optimizer = tf.keras.optimizers.aDAM(0.01)\n", - "\n", - "# Compile tells the keras backend what loss and optimizer to use to perform gradients on the model\n", - "# You cannot train a keras model without compiling it first\n", - "model.compile(loss=loss, optimizer=optimizer)\n", - "\n", - "# Show what layers are in the model, and their input and output shapes \n", - "# This can help make sure all your stuff is a correct size\n", - "model.summary()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Train the model \n", - "\n", - "We have all the pieces in place:\n", - "- [x] Model \n", - "- [x] Train Data \n", - "- [x] Validation Data \n", - "- [x] Loss Function \n", - "- [x] Optimizer \n", - "\n", - "Now lets put this together into a fit model. \n", - "Keras trains in place, so you don't need a new variable to hold the `fit_model` vs `model`. \n", - "Once you call `fit`, the model is fit, and it re-train, you need to make a new model with the `make_model()` function. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "train_generator_scaled = SkyGenerator(n_samples=1280, pre_processing=processor, batch_size=64)\n", - "val_generator_scaled = SkyGenerator(n_samples=12, pre_processing=None, batch_size=64)\n", - "\n", - "history = model.fit(\n", - " train_generator_scaled, \n", - " validation_data=train_generator_scaled, \n", - " epchs=1, \n", - " verbose=1\n", - " ).history" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Evaluation \n", - "\n", - "There are some steps we can take to see how well a model trained. \n", - "\n", - "### Loss Plots \n", - "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training. \n", - "Or, if the loss never moves or blows up entirely, that's a sign there's a problem. \n", - "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems! " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Eval.plot_loss_history(history) \n", - "# Eval.plot_history is a simple function \n", - "# plots the loss as a function of epoch " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Classification Accuracy Plots \n", - "\n", - "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training. \n", - "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it. \n", - "\n", - "### ROC\n", - "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate. \n", - "It shows how likely a model is to correctly predict something. \n", - "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves. \n", - "\n", - "\n", - "\n", - "Roc-draft-xkcd-style.svg, CC BY-SA 4.0, Link\n", - "\n", - "\n", - "### Confusion Matrix\n", - "\n", - "Confusion matrices are a great tool for seeing how well each class does against each other. \n", - "It gets it name from its ability to tell if a model is \"confusing\" two different classes. \n", - "It plots the rate of predicted values for a given class versus the true values. \n", - "\n", - "\n", - "\n", - " Link\n", - "\n", - " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_generator = SkyGenerator(n_samples=1280, train=False, shuffle=True)\n", - "\n", - "def make_prediction(model, test_generator): \n", - " predictions = trained_model.predict(test_generator)\n", - " prediction_classes = np.where(predictions<0.5, 0, 1)\n", - " labels = test_generator.labels\n", - " return prediction_classes, labels\n", - "\n", - "def test_quality(prediction, labels): \n", - " accuracy = tf.keras.metrics.BinaryAccuracy()(labels, labels)\n", - " return accuracy.numpy()\n", - "\n", - "prediction, labels = make_prediction(model, test_generator)\n", - "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# And now run them on your own data! \n", - "Eval.ROC_curve(prediction, labels)\n", - "Eval.confusion_matrix(labels, labels)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.11" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/Challenge/challenges/challenge_05.ipynb b/Challenge/challenges/challenge_05.ipynb deleted file mode 100644 index dc1d7ed..0000000 --- a/Challenge/challenges/challenge_05.ipynb +++ /dev/null @@ -1,501 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Training Challenge! \n", - "\n", - "In this notebook, there are a whole host of weird errors. \n", - "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking. \n", - "Do your best to sniff out the mistakes in both code and training procedure! \n", - "\n", - "Your mission, should you choose to accept it, is: \n", - "* Work together in groups of 4 to complete this notebook\n", - "* Figure out all the coding mistakes and make a notebook that runs\n", - "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", - "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them.\n", - "\n", - "### Rules \n", - "* If you cannot correct the problem, identify it and write a quick paragraph about what you would do\n", - "* You are allowed to use any resource you can find; other groups, TA's, a random scientist walking by, and most importantly, the internet. The use of generative AI is highly discouraged - not because it's cheating, but because it can send you down rabbit holes that are hard to find your way out of. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Installing the data generation package \n", - "! pip install deepbench" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials_2024/main/Challenge/challenge_utilities.py" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you have an issue with this, re-downloading the file may fix it.\n", - "If this still doesn't help, [directly pull the file from the github link](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/40758642b5570a3fe53dc49da237a19f5e748a78/07_Challenge/challenge_utilities.py)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Now we'll import the different classes from this new file \n", - "from challenge_utilities import SkyGenerator05 as SkyGenerator # `as` renames the imported package name\n", - "from challenge_utilities import Eval" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Standard packages \n", - "\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from tensorflow.keras.layers import Input, Dropout, Conv1D, Dense, AvgPool1D, Flatten\n", - "from tensorflow.keras.models import Model\n", - "\n", - "import tensorflow as tf \n", - "from sklearn.preprocessing import MinMaxScaler\n", - "from sklearn.decomposition import PCA" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Package documentation \n", - "\n", - "If you get stuck for syntax or anything - these are the packages used. \n", - "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names. \n", - "\n", - "[Numpy]()\n", - "\n", - "[MatPlotLib]() \n", - "\n", - "[Tensorflow/tf/Keras]()\n", - "\n", - "[Sci-kit Learn]()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "label_map = {\n", - " 0: \"Star\", \n", - " 1: \"Galaxy\"\n", - "}\n", - "\n", - "\n", - "def plot_samples(generator, n_columns=3, n_rows=3, label_map=None): \n", - " _, subplots = plt.subplots(n_columns, 1) # Make 9 plots in a 3 x 3 grid \n", - " plt.tight_layout()\n", - " plt.setp(subplots, xticks=[], yticks=[])\n", - "\n", - " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()): \n", - " image, label = generator[sample_index]\n", - " subplot.imshow(image.squeeze()) \n", - " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels). \n", - " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", - " \n", - " string_label = \"??\" \n", - " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", - "\n", - "samples = SkyGenerator(n_samples=9, batch_size=1) # Can just get a few samples\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Look at the input data \n", - "\n", - "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_train_samples = 1280\n", - "train_generator = SkyGenerator(n_samples=n_train_samples, shuffle=True)\n", - "\n", - "n_val_samples = 1280\n", - "val_generator = SkyGenerator(n_samples=n_val_samples, train=False, shuffle=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can look at the distribution of labels by grabbing them from the generator \n", - " # TF.Sequence generators supply data as a tuple \n", - " # of (x,y) (or (features, labels)) \n", - " # - so using index 1 we can get the labels\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][1] for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0] for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots.hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.supxlabel(\"Label Distributions\")\n", - "figure.supylabel(\"Label Frequency\")\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# We can also look at different attributes of the images \n", - "# Here, let's look at max values and mean values\n", - "\n", - "# These numbers will be slightly different every time, there's randomness controlling how the images are generated \n", - "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().mean() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].mean() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Mean value of pixels in a single image\")\n", - "plt.show()\n", - "\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().max() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].ravel().max() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Max value of pixels in a single image\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Data Processing\n", - "\n", - "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", - "There are near infinite ways to pre-process, especially in the computer vision space. \n", - "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways. \n", - "\n", - "For this, let's try bringing all the pixels in the image between 0 and 1. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scaler = MinMaxScaler((0, 1))\n", - "\n", - "fit_data = np.concatenate([train_generator[i][0] for i in range(5)], axis=0) \n", - "# This is 5 batches of data, being used to find the approximate min and max of the data\n", - "# Because our data is synthetic, we don't need to worry about big outliers\n", - "\n", - "# Unfortunately, minmaxscaler only handles 1d of data, so we need to do a little pre-and-post processing on the input \n", - "# combining the last two dimensions together, making the 2d of the image 1d instead. \n", - "fit_data = fit_data.reshape((fit_data.shape[0], fit_data.shape[1]*fit_data.shape[2]) ) \n", - "scaler_fit = scaler.fit(fit_data)\n", - "\n", - "def processor(image): \n", - " # This function will take a single image and return the scaled version \n", - " image_flat = image.ravel() \n", - " image_scaled = scaler_fit.transform(image_flat.reshape(1, -1))\n", - " # Magically reshape it back into 2d \n", - " image_scaled_reshaped = image_scaled.reshape(image.shape)\n", - " return image_scaled_reshaped\n", - "\n", - "# We can also use PCA decomposition to reduce the dimension of the data if we want\n", - "generator_sample = SkyGenerator(n_samples=20)\n", - "generator_sample = [generator_sample.__getitem__(index)[0] for index in range(20)]\n", - "decomposition = PCA(n_components=16).fit(generator_sample)\n", - "\n", - "def pca_processor(image): \n", - " return decomposition.transform(image)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Look at the samples again! \n", - "\n", - "n_samples = 9\n", - "samples = SkyGenerator(n_samples=n_samples, batch_size=1, pre_processing=processor)\n", - "plot_samples(samples)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Make the binary classification model \n", - "\n", - "This function, when called, produces a keras Model instance that you can train to predict a class of an input. \n", - "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1. \n", - "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out. \n", - "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it. \n", - "\n", - "In this case, what we want: \n", - "* Take the input images from the data generator \n", - "* Apply two convolutional blocks to the input image \n", - "* Decode the second convolution block's output to a probability of the image being a given class. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def make_model(): \n", - " \"\"\"\n", - " Make a network that can perform binary classification\n", - "\n", - " Returns:\n", - " model (keras.Model): classifier model that will \n", - " \"\"\"\n", - " in_layer = Input((4, 4))\n", - " x = Conv1D(filters=4, kernel_size=2)(input_layer)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - " \n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Dense(20, activation='relu')\n", - "\n", - " x = Dropout(0.3)(x)\n", - " output = Dense(10, activation='softmax')(x)\n", - " model = Model(in_layer, x)\n", - " \n", - " return model\n", - "\n", - "model = make_model()\n", - "loss = tf.losses.BinaryCrossentropy()\n", - "optimizer = tf.keras.optimizers.SGD(0.0001)\n", - "\n", - "# Compile tells the keras backend what loss and optimizer to use to perform gradients on the model\n", - "# You cannot train a keras model without compiling it first\n", - "model.compile(loss=loss, optimizer=optimizer)\n", - "\n", - "# Show what layers are in the model, and their input and output shapes \n", - "# This can help make sure all your stuff is a correct size\n", - "model.summary()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Train the model \n", - "\n", - "We have all the pieces in place:\n", - "- [x] Model \n", - "- [x] Train Data \n", - "- [x] Validation Data \n", - "- [x] Loss Function \n", - "- [x] Optimizer \n", - "\n", - "Now lets put this together into a fit model. \n", - "Keras trains in place, so you don't need a new variable to hold the `fit_model` vs `model`. \n", - "Once you call `fit`, the model is fit, and it re-train, you need to make a new model with the `make_model()` function. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "train_generator_scaled = SkyGenerator(n_samples=1280, pre_processing=pca_processor, batch_size=64)\n", - "val_generator_scaled = SkyGenerator(n_samples=12, pre_processing=processor, batch_size=64)\n", - "\n", - "history = model.fit(\n", - " train_generator_scaled, \n", - " validation_data=train_generator_scaled, \n", - " epchs=1, \n", - " verbose=1\n", - " ).history" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Evaluation \n", - "\n", - "There are some steps we can take to see how well a model trained. \n", - "\n", - "### Loss Plots \n", - "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training. \n", - "Or, if the loss never moves or blows up entirely, that's a sign there's a problem. \n", - "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems! " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Eval.plot_loss_history(history) \n", - "# Eval.plot_history is a simple function \n", - "# plots the loss as a function of epoch " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Classification Accuracy Plots \n", - "\n", - "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training. \n", - "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it. \n", - "\n", - "### ROC\n", - "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate. \n", - "It shows how likely a model is to correctly predict something. \n", - "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves. \n", - "\n", - "\n", - "\n", - "Roc-draft-xkcd-style.svg, CC BY-SA 4.0, Link\n", - "\n", - "\n", - "### Confusion Matrix\n", - "\n", - "Confusion matrices are a great tool for seeing how well each class does against each other. \n", - "It gets it name from its ability to tell if a model is \"confusing\" two different classes. \n", - "It plots the rate of predicted values for a given class versus the true values. \n", - "\n", - "\n", - "\n", - " Link\n", - "\n", - " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_generator = SkyGenerator(n_samples=1280, train=False, shuffle=True)\n", - "\n", - "def make_prediction(model, test_generator): \n", - " predictions = trained_model.predict(test_generator)\n", - " prediction_classes = np.where(predictions<0.5, 0, 1)\n", - " labels = test_generator.labels\n", - " return prediction_classes, labels\n", - "\n", - "def test_quality(prediction, labels): \n", - " accuracy = tf.keras.metrics.BinaryAccuracy()(labels, labels)\n", - " return accuracy.numpy()\n", - "\n", - "prediction, labels = make_prediction(model, test_generator)\n", - "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# And now run them on your own data! \n", - "Eval.ROC_curve(prediction, labels)\n", - "Eval.confusion_matrix(labels, labels)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.11" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/Challenge/guide/anwser_guide.md b/Challenge/guide/anwser_guide.md index 724b35e..7f47831 100644 --- a/Challenge/guide/anwser_guide.md +++ b/Challenge/guide/anwser_guide.md @@ -1,42 +1,36 @@ -## Common Mistakes +# Common Mistakes -### Imports -* line 8 - incorrect import name +## imports +* Missing the "torch" import +* Misspelled "SkyDataset" as "SkaiDataset" -### `make_model` -* line 9 - in_layer != input_layer -* line 19 - model = Model(in_layer, x) -> `model = Model(in_layer, output)` +## data processing +* Transformer uses "randomresized" instead of a resize to 64x64 +* Missing labels on the EDA, too many images being plotted +* Missing a `squeeze` on imshow -### `train_model` -* line 12 - too few validation samples -* line 16 - validation data is set to the train data -* line 17 - `epoch` spelled incorrectly +## model +* Using a "softmax" over the output function, for a binary problem this should be sigmoid +* Missing an "unsqueeze" to resize +* Missing the 3rd conv block -### `eval_model` -* line 8 - No parameter for the generator named "test" -* line 10 - wrong name for the model -* line 11 - wrong np.where order of 1/0 +## training +* Missing the optimizer step in training +* Validation step still includes the gradient +* Only trains for 2 epochs +# Notebook 1 +* Random input size, need to make sure the resize is doing it's job -# Notebook 1 - +# Notebook 2 +* Imbalanced training data - 85-95% stars. -Very low learning rate - The model will never learn. -Incorrect loss function - swap to a binary classification loss. +# Notebook 3 +* The data includes a ton of completely empty images with very large noise levels -# Notebook 2 - +# Notebook 4 +* Extra class in the training data. Write a function that removes label N=3, or weight it 0. +* Images are returned as numpy arrays and not torch tensors -Imbalanced training data - 85-95% stars. -Correct by weighting the labels in the training loop. - -# Notebook 3 - - -The data includes a ton of completely empty images. -A pre-processing function can be written to filter these out. - -# Notebook 4 - - -Extra class in the training data. Write a function that removes label N=3, or weight it 0. - -# Notebook 5 - - -A pre-processing function is applied to the training data but not the validation or testing data. \ No newline at end of file +# Notebook 5 +* This isn't a classification problem! It's a regression problem! - labels are 1x3 of radius, center x and center y. \ No newline at end of file diff --git a/Challenge/guide/challenge_main.ipynb b/Challenge/guide/challenge_main.ipynb index a7bb286..bee76f2 100644 --- a/Challenge/guide/challenge_main.ipynb +++ b/Challenge/guide/challenge_main.ipynb @@ -2,39 +2,46 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "jToNcYCsARAt" + }, "source": [ - "## Model Training Challenge! \n", + "## Model Training Challenge!\n", "\n", - "In this notebook, there are a whole host of weird errors. \n", - "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking. \n", - "Do your best to sniff out the mistakes in both code and training procedure! \n", + "In this notebook, there are a whole host of weird errors.\n", + "Some of them will be obvious and throw an error, but some are less clear and will break things without telling you they're breaking.\n", + "Do your best to sniff out the mistakes in both code and training procedure!\n", "\n", - "Your mission, should you choose to accept it, is: \n", + "Your mission, should you choose to accept it, is:\n", "* Work together in groups of 4 to complete this notebook\n", "* Figure out all the coding mistakes and make a notebook that runs\n", "* Find the mistakes in training and take appropriate corrective measures so the model trains well!\n", - "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them.\n", - "\n", - "### Rules \n", - "* If you cannot correct the problem, identify it and write a quick paragraph about what you would do\n", - "* You are allowed to use any resource you can find; other groups, TA's, a random scientist walking by, and most importantly, the internet. The use of generative AI is highly discouraged - not because it's cheating, but because it can send you down rabbit holes that are hard to find your way out of. " + "* Make a ~15 minute long presentation showing your results and the steps you took to find and solve them." ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "RX8h5B-GARAt" + }, "source": [ - "This line below installs a non-standard data generation package. \n", - "It is designed to make images for benchmarking computer vision problems, and is the back bone of your generator. \n", - "Unfortunately, it's not included with colab (which makes sense....), so we have to install it. \n", - "The `!` point here means \"Execute this line like it's a line in a bash prompt\". " + "This line below installs a non-standard data generation package.\n", + "It is designed to make images for benchmarking computer vision problems, and is the back bone of your generator.\n", + "Unfortunately, it's not included with colab (which makes sense....), so we have to install it.\n", + "The `!` point here means \"Execute this line like it's a line in a bash prompt\"." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "VPUeGpTrARAu", + "outputId": "742b2828-b17a-445a-bc57-327fa506f61a" + }, "outputs": [], "source": [ "! pip install deepbench" @@ -42,828 +49,579 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "xatOAXZ6ARAu" + }, "source": [ - "`curl` is a package for downloading files off the internet. \n", - "This below line just downloads a file form the repo into this colab instance, and names it \"challenge_utilities.py\". \n", - "This file is also in the github repo, you can look at it there if you want. \n", - "It contains the data generator for each for your challenges, and the utilities for plotting and evaluating results. \n", + "`curl` is a package for downloading files off the internet.\n", + "This below line just downloads a file form the repo into this colab instance, and names it \"challenge_utilities.py\".\n", + "This file is also in the github repo, you can look at it there if you want.\n", + "It contains the data generator for each for your challenges, and the utilities for plotting and evaluating results.\n", "(It won't contain any problems for you to solve, it's just there to keep this notebook from getting cluttered.)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "id": "Kjo3hTS_ARAu" + }, "outputs": [], "source": [ - "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials_2024/tree/main/07_Challenge/challenge_utilities.py" + "! curl -o challenge_utilities.py https://raw.githubusercontent.com/BNL-Fermilab-RENEW/tutorials/refs/heads/main/Challenge/challenge_utilities.py" ] }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, + "execution_count": null, + "metadata": { + "id": "MKc8rBCmARAu" + }, "outputs": [], "source": [ - "# Now we'll import the different classes from this new file \n", - "from challenge_utilities import SkyGeneratorTrue as SkyGenerator # `as` renames the imported package name\n", + "# Now we'll import the different classes from this new file\n", + "# Import the dataloader and dataset classes\n", + "from challenge_utilities import SkyDataset, TestDataset # Dataset object itself\n", + "# A subclass of torch.utils.data.Dataset\n", + "from challenge_utilities import SkyGenerator # Generic dataloader object, same for all challenges\n", "from challenge_utilities import Eval" ] }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, + "execution_count": null, + "metadata": { + "id": "2IXmuM_EARAv" + }, "outputs": [], "source": [ - "# Standard packages \n", - "\n", - "import numpy as np\n", + "# Standard packages\n", "import matplotlib.pyplot as plt\n", + "from tqdm import tqdm\n", "\n", - "from tensorflow.keras.layers import Input, Dropout, Conv1D, Dense, AvgPool1D, Flatten\n", - "from tensorflow.keras.models import Model\n", - "\n", - "import tensorflow as tf \n", - "from sklearn.preprocessing import MinMaxScaler" + "import torch\n", + "import torchvision\n", + "from torchsummary import summary" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "W9XZQhfDARAv" + }, "source": [ - "#### Package documentation \n", - "\n", - "If you get stuck for syntax or anything - these are the packages used. \n", - "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names. \n", - "\n", - "[Numpy](https://numpy.org/)\n", + "#### Package documentation\n", "\n", - "[MatPlotLib](https://matplotlib.org/) \n", + "If you get stuck for syntax or anything - these are the packages used.\n", + "Look up a function you're trying to use in their package search pages, and see if you maybe have types wrong, or wrong variable names.\n", "\n", - "[Tensorflow/tf/Keras](https://keras.io/)\n", + "[MatPlotLib](https://matplotlib.org/)\n", "\n", - "[Scikit Learn](https://scikit-learn.org/stable/index.html)" + "[Pytorch](https://docs.pytorch.org/docs/)\n" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "m5EFoTAgARAv" + }, "source": [ - "# Exploratory Data Analysis \n", + "# Exploratory Data Analysis\n", "\n", - "Understanding your data is a critical part of any AI/ML project. \n", - "Make sure you look at your data and understand the differences between each class. \n", + "Understanding your data is a critical part of any AI/ML project.\n", + "Make sure you look at your data and understand the differences between each class.\n", "\n", "Something to note, which you would only know if you gathered the data yourself, is that the binary \"0\" and \"1\" labels correspond to \"stars\" and \"galaxies\".\n", - "You can use this to label your plots. " + "You can use this to label your plots." ] }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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5Z/4xCfMs88+a7761a8m0DshphvFjxCnQzGEPFBvRm4TRZH58pe/bD7+8Y+bp59+Orb8O9/5jrl+/bp59tlnE+idENON4keI06EYinOi2jR/1fj85z9vXnzxRfPjH//YpNNpWPbd737XfOlLXzI//OEPzeXLlxPqoRDTi+JHiNOhGIrzwKVpXnrpJfP000+bt956y1y5cgWWBUFgqtWq+frXv26+8IUvJNRDIaYXxY8Qp0MxdGceuDTN2tqaMcbEBoExxjQaDVOv1++4TAih+BHitCiG7swD92akVquZt99+2zz55JOxZUEQmNdee81cuXLFlEqlBHonxHSj+BHidCiG7swD92bkJz/5iXnuuefuuGx9fd0899xz5s0337zPvRLibKD4EeJ0KIbuzAM3Gdnf3zdXr16947J+v2+uXr1qWq3Wfe6VEGcDxY8Qp0MxdGceuDSNEEIIIaaLB+7NiBBCCCGmC01GhBBCCJEomowIIYQQIlE0GRFCCCFEomgyIoQQQohE0WRECCGEEImiyYgQQgghEkWTESGEEEIkiiYjQgghhEgUTUaEEEIIkSiajAghhBAiUTQZEUIIIUSi/P9QlwLjMWf4eQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 477 + }, + "id": "66Gal0ukARAv", + "outputId": "8ccd7889-72d1-4fce-f6c7-a417d7515a7b" + }, + "outputs": [], "source": [ "\n", "label_map = {\n", - " 0: \"Star\", \n", + " 0: \"Star\",\n", " 1: \"Galaxy\"\n", "}\n", "\n", - "\n", - "def plot_samples(generator, n_columns=3, n_rows=3, label_map=None): \n", - " _, subplots = plt.subplots(n_columns, n_rows) # Make 9 plots in a 3 x 3 grid \n", + "def plot_samples(generator, n_columns=3, n_rows=3, label_map:dict|None=None):\n", + " _, subplots = plt.subplots(n_columns, n_rows) # Make 9 plots in a 3 x 3 grid\n", " plt.tight_layout()\n", " plt.setp(subplots, xticks=[], yticks=[])\n", "\n", - " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()): \n", - " image, label = generator[sample_index]\n", - " subplot.imshow(image.squeeze()) \n", - " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels). \n", + " for sample_index, subplot in zip(range(n_columns*n_rows), subplots.ravel()):\n", + " image, label = generator.get(sample_index)\n", + " subplot.imshow(image.squeeze())\n", + " # 'imshow' displays an image in 2d (black and white if it only has 1 color channel, or in color if it has 3 (r,g,b) color channels).\n", " # Here it's green and blue because the default colorway for matplotlib is \"viridis\", with is all cool colors\n", - " \n", - " string_label = \"??\" \n", + "\n", + " string_label = label_map[label.item()] if label_map else str(label.item())\n", " subplot.set_xlabel(string_label) # This gives you a label underneath the image (on the x axis)\n", "\n", - "samples = SkyGenerator(n_samples=9, batch_size=1) # Can just get a few samples\n", - "plot_samples(samples)" + "samples = SkyDataset(n_samples=9, seed=42) # Can just get a few samples\n", + "plot_samples(samples, label_map=label_map)" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "VTG-xMYWARAw" + }, "source": [ - "# Look at the input data \n", + "# Look at the input data\n", "\n", - "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with. " + "Understanding the data is a critical part of the training process, let's take a look at the distributions we're working with." ] }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, + "execution_count": null, + "metadata": { + "id": "6FJGVsZgARAw" + }, "outputs": [], "source": [ - "# These two generators produce the data we'll train with \n", + "# These two generators produce the data we'll train with\n", "\n", "n_train_samples = 1280\n", - "train_generator = SkyGenerator(n_samples=n_train_samples, shuffle=True)\n", + "train_generator = SkyGenerator(n_samples=n_train_samples, dataset=SkyDataset, shuffle=True)\n", "\n", "n_val_samples = 1280\n", - "val_generator = SkyGenerator(n_samples=n_val_samples, train=False, shuffle=True)" + "val_generator = SkyGenerator(n_samples=n_val_samples, dataset=SkyDataset, shuffle=True)" ] }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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X32lpk2b6oknnlBQUJDuuecevf/++/b1R48eVXJysqKjo+3LAgIC1Lx5cyUlJUmSkpKSFBgYaG94khQdHS0PDw9t3bo11/2mp6crLS3N4Qag5KB3AHCFEhOYfv75Z82ZM0d16tTRqlWrNGjQIP33f/+3FixYIElKTk6WJAUHBzs8Ljg42L4uOTlZQUFBDutLlSqlChUq2Mdcb/LkyQoICLDfqlat6uqpAShE9A4ArlCquAvIq6ysLDVt2lSTJk2SJN1zzz3au3ev5s6dq7i4uELb76hRozRixAj7/bS0tDw3vuI67Ajg/9A76BuAK5SYI0xVqlRReHi4w7L69evr2LFjkqSQkBBJUkpKisOYlJQU+7qQkBCdPn3aYf3Vq1d17tw5+5jr+fj4yN/f3+EGoOSgdwBwhRITmFq2bKmDBw86LPvXv/6lsLAwSVKNGjUUEhKitWvX2tenpaVp69atioqKkiRFRUXp/Pnz2rlzp33MunXrlJWVpebNmxfBLAAUNXoHAFcoMR/JDR8+XPfdd58mTZqk7t27a9u2bXrvvff03nvvSZJsNpuGDRum1157TXXq1FGNGjU0ZswYhYaGqkuXLpL+81tl+/btNWDAAM2dO1dXrlzR4MGD1bNnzzx9ywVAyUPvAOAKJSYw3XvvvVq8eLFGjRqliRMnqkaNGpo5c6Z69eplH/PCCy/o0qVLGjhwoM6fP69WrVpp5cqV8vX1tY/5xz/+ocGDB+uhhx6Sh4eHunXrplmzZhXHlAAUAXoHAFcoMddhchf5uY4DJ33jVlcSrsPkLoqzd9A34E64DhMAAMAtisAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABggcAEAABgocQEpvHjx8tmsznc6tWrZ19/+fJlxcfHq2LFiipbtqy6deumlJQUh20cO3ZM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tWrVIts/AOcVV9+Q6B3ArcapwBQWFubqOizt3LlTp0+fVpMmTezLMjMztXHjRv3P//yPVq1apYyMDJ0/f97ht8WUlBSFhIRIkkJCQrRt2zaH7WZ/GyZ7zPV8fHzk4+Pj4tkAKArF1Tckegdwqykxf/jtoYce0p49e/TDDz/Yb02bNlWvXr3s/+/l5aW1a9faH3Pw4EEdO3ZMUVFRkqSoqCjt2bPH4RpRiYmJ8vf3V3h4eJHPCUDhom8AcBWnjjB5eHjIZrPl6zE2m61A1y0pV66c7r77bodlZcqUUcWKFe3L+/fvrxEjRqhChQry9/fXkCFDFBUVpRYtWkiS2rVrp/DwcPXp00dTp05VcnKyRo8erfj4eH4TBG5B9A0AruJUYBo7dqyWLFmiffv2KSYmRnfddZck6aefftLq1at19913q0uXLq6sM09mzJghDw8PdevWTenp6YqJidE777xjX+/p6amlS5dq0KBBioqKUpkyZRQXF5env40H4NZE3wCQF04FptDQUJ0+fVp79+61h6VsBw4cUNu2bRUaGqoBAwa4pMgb2bBhg8N9X19fzZ49W7Nnz77hY8LCwgr1opoA3Bt9A4AznDqH6Y033tDgwYNzhCVJql+/vgYPHqypU6cWuDgAAAB34FRgOnHihLy8vG643svLSydOnHC6KAAAAHfiVGC6++679c477+i3337Lse7EiRN65513FBERUeDiAAAA3IFT5zDNmDFDMTExqlu3rh577DHVrl1bknTo0CEtWbJExhh9/PHHLi0UAACguDgVmFq1aqWtW7dqzJgxWrx4sf78809JUunSpRUTE6MJEyZwhAkAANwynApM0n8+llu8eLGysrL0+++/S5IqV64sD48Scy1MAACAPHE6MGXz8PCQr6+vypYtS1gCAAC3JKcTzo4dO9S+fXv5+fmpYsWK+uabbyRJZ86cUefOnXNc6wQAAKCkciowbd68Wa1atdKhQ4fUu3dvZWVl2ddVqlRJqampevfdd11WJAAAQHFyKjC9/PLLql+/vvbv369JkyblWP/ggw9q69atBS4OAADAHTgVmLZv366nnnpKPj4+uf4R3jvuuEPJyckFLg4AAMAdOBWYvLy8HD6Gu95vv/2msmXLOl0UAACAO3EqMLVo0UKff/55rusuXbqkhIQEtW7dukCFAQAAuAunAtOECRO0Y8cOxcbGasWKFZKk3bt364MPPlBkZKR+//13jRkzxqWFAgAAFBenrsPUvHlzLV++XIMGDVLfvn0lSc8995wkqVatWlq+fLkaNmzouioBAACKUb4DkzFGFy5c0H333aeDBw/qhx9+0KFDh5SVlaVatWopMjIy1xPBAQAASqp8B6aMjAxVqFBBkyZN0gsvvKDGjRurcePGhVAaAACAe8j3OUw+Pj4KCQmRj49PYdQDAADgdpw66btfv3766KOPlJGR4ep6AAAA3I5TJ31HRERoyZIlatCggfr166fq1aurdOnSOcZ17dq1wAUCAAAUN6cC05NPPmn//xtdPsBmsykzM9O5qgAAANyIU4Fp/fr1rq4DAADAbeU5ML388svq2bOnGjZsyFW8AQDAbSXPJ31PmTJFe/futd8/e/asPD09tW7dukIpDAAAwF049S25bMYYV9UBAADgtgoUmAAAAG4HBCYAAAAL+fqW3C+//KJdu3ZJklJTUyVJhw4dUmBgYK7jmzRpUrDqAAAA3EC+AtOYMWNyXHfp2WefzTHOGMN1mAAAwC0jz4EpISGhMOsAAABwW3kOTHFxcYVZBwAAgNvipG8AAAALBCYAAAALBCYAAAALBCYAAAALBCYAAAALBCYAAAALebqswMaNG53a+AMPPODU4wAAANxJngJTmzZtZLPZ8rxRrvQNAABuJXkKTOvXry/sOgAAANxWns5hat26tVM3V5ozZ44aNmwof39/+fv7KyoqSitWrLCvv3z5suLj41WxYkWVLVtW3bp1U0pKisM2jh07ptjYWPn5+SkoKEjPP/+8rl696tI6AbgXegcAVyjwSd+nTp3S7t27denSJVfUc0N33nmnpkyZop07d2rHjh1q27atOnfurH379kmShg8frq+//lqLFi3SN998o5MnT6pr1672x2dmZio2NlYZGRnavHmzFixYoPnz52vs2LGFWjeA4kXvAOAKTgemL7/8UvXq1dOdd96pJk2aaOvWrZKkM2fO6J577tGSJUtcVaMkqVOnTurYsaPq1KmjunXr6vXXX1fZsmW1ZcsWpaamat68eZo+fbratm2ryMhIJSQkaPPmzdqyZYskafXq1dq/f78+/vhjNW7cWB06dNCrr76q2bNnKyMjw6W1AnAf9A4AruBUYPr666/VtWtXVapUSePGjZMxxr6uUqVKuuOOO5SQkOCyIq+XmZmphQsX6tKlS4qKitLOnTt15coVRUdH28fUq1dP1apVU1JSkiQpKSlJERERCg4Oto+JiYlRWlqa/TfN3KSnpystLc3hBqBkoncAcJZTgWnixIl64IEH9N133yk+Pj7H+qioKH3//fcFLu56e/bsUdmyZeXj46O//vWvWrx4scLDw5WcnCxvb28FBgY6jA8ODlZycrIkKTk52aHhZa/PXncjkydPVkBAgP1WtWpV104KQKGjdwAoKKcC0969e9W9e/cbrg8ODtbp06edLupG7rrrLv3www/aunWrBg0apLi4OO3fv9/l+7nWqFGjlJqaar8dP368UPcHwPXoHQAKKk+XFbien5/fTU/y/vnnn1WxYkWni7oRb29v1a5dW5IUGRmp7du366233lKPHj2UkZGh8+fPO/ymmJKSopCQEElSSEiItm3b5rC97G/CZI/JjY+Pj3x8fFw8EwBFid4BoKCcOsL04IMPasGCBbl+rTY5OVnvv/++2rVrV+DirGRlZSk9PV2RkZHy8vLS2rVr7esOHjyoY8eOKSoqStJ/Pibcs2ePw5GvxMRE+fv7Kzw8vNBrBeA+6B0A8supI0yvv/66WrRooXvvvVdPPPGEbDabVq1apXXr1undd9+VMUbjxo1zaaGjRo1Shw4dVK1aNV24cEGffPKJNmzYoFWrVikgIED9+/fXiBEjVKFCBfn7+2vIkCGKiopSixYtJEnt2rVTeHi4+vTpo6lTpyo5OVmjR49WfHw8vwUCtzB6BwBXcCow3XXXXfruu+80dOhQjRkzRsYYvfHGG5L+82dUZs+ererVq7uyTp0+fVp9+/bVqVOnFBAQoIYNG2rVqlV6+OGHJUkzZsyQh4eHunXrpvT0dMXExOidd96xP97T01NLly7VoEGDFBUVpTJlyiguLk4TJ050aZ0A3Au9A4Ar2My11wRwwh9//KHDhw8rKytLNWvWVOXKlV1Vm1tKS0tTQECAUlNT5e/vf9Ox1V9a5vL9/zIl1uXbBJzl6td4Xl/f+Xkfuovi7B30DbiT4vq3saB9w6kjTNcqX7687r333oJuBgAAwG05faXv33//XSNHjlR4eLj8/Pzk5+en8PBwjRw5MsffYQIAACjJnApM+/btU0REhKZPn66AgAA98cQTeuKJJxQQEKDp06erYcOG2rt3r6trBQAAKBZOfSQXHx+vzMxMbd26NcfHcdu2bVPHjh01ZMgQrV+/3iVFAgAAFCenjjBt27ZNQ4cOzfXcpWbNmmno0KH2P8YLAABQ0jkVmIKCguTr63vD9b6+vgoKCnK6KAAAAHfiVGAaNmyY5syZk+sfnjx58qTmzJmjYcOGFbQ2AAAAt5Cnc5imT5+eY1nZsmVVu3ZtPfbYY/a/0XTo0CEtWbJEtWvXVgEv7wQAAOA28hSYRo4cecN1//jHP3Is+/HHHzVy5EgNHz7c+coAAADcRJ4C09GjRwu7DgAAALeVp8AUFhZW2HUAAAC4Laev9A0AAHC7cPpvyf344496++23tWvXLqWmpiorK8thvc1m05EjRwpcIAAAQHFz6gjThg0b1KxZMy1dulShoaH6+eefVbNmTYWGhurXX39V2bJl9cADD7i6VgAAgGLhVGAaO3asatasqYMHDyohIUGS9PLLL+u7777T5s2bdeLECXXv3t2lhQIAABQXpwLTrl271L9/f/n7+8vT01OSlJmZKUlq3ry5nnnmGY0ZM8Z1VQIAABQjpwJTqVKlVK5cOUlSYGCgvLy8dPr0afv6mjVrav/+/a6pEAAAoJg5FZhq166tQ4cOSfrPyd316tXT4sWL7euXLVumkJAQ11QIAABQzJwKTB07dtSnn36qq1evSpJGjBihL774QnXq1FGdOnX01Vdf6ZlnnnFpoQAAAMXFqcsKjBkzRkOHDrWfvxQXFydPT0/97//+rzw9PfXKK6+oX79+rqwTAACg2DgVmLy8vFSxYkWHZb1791bv3r0lSZcuXdLJkycVGhpa8AoBAACKWaFc6XvmzJmqWrVqYWwaAACgyPGnUQAAACwQmAAAACwQmAAAACwQmAAAACzk+Vtyu3btyvNGT5486VQxAAAA7ijPgalp06ay2Wx5GmuMyfNYAAAAd5fnwJSQkFCYdQAAALitPAemuLi4wqwDAADAbXHSNwAAgAUCEwAAgAUCEwAAgAUCEwAAgAUCEwAAgAUCEwAAgAUCEwAAgAUCEwAAgAUCEwAAgAUCEwAAgAUCEwAAgIUSE5gmT56se++9V+XKlVNQUJC6dOmigwcPOoy5fPmy4uPjVbFiRZUtW1bdunVTSkqKw5hjx44pNjZWfn5+CgoK0vPPP6+rV68W5VQAFCF6BwBXKDGB6ZtvvlF8fLy2bNmixMREXblyRe3atdOlS5fsY4YPH66vv/5aixYt0jfffKOTJ0+qa9eu9vWZmZmKjY1VRkaGNm/erAULFmj+/PkaO3ZscUwJQBGgdwBwBZsxxhR3Ec74/fffFRQUpG+++UYPPPCAUlNTVblyZX3yySd6/PHHJUk//fST6tevr6SkJLVo0UIrVqzQI488opMnTyo4OFiSNHfuXL344ov6/fff5e3tbbnftLQ0BQQEKDU1Vf7+/jcdW/2lZQWf6HV+mRLr8m0CznL1azyvr+/8vA+vdzv2DvoG3Elx/dtYkL4hlaAjTNdLTU2VJFWoUEGStHPnTl25ckXR0dH2MfXq1VO1atWUlJQkSUpKSlJERIS94UlSTEyM0tLStG/fvlz3k56errS0NIcbgJKL3gHAGSUyMGVlZWnYsGFq2bKl7r77bklScnKyvL29FRgY6DA2ODhYycnJ9jHXNrzs9dnrcjN58mQFBATYb1WrVnXxbAAUFXoHAGeVyMAUHx+vvXv3auHChYW+r1GjRik1NdV+O378eKHvE0DhoHcAcFap4i4gvwYPHqylS5dq48aNuvPOO+3LQ0JClJGRofPnzzv8ppiSkqKQkBD7mG3btjlsL/ubMNljrufj4yMfHx8XzwJAUaN3ACiIEnOEyRijwYMHa/HixVq3bp1q1KjhsD4yMlJeXl5au3atfdnBgwd17NgxRUVFSZKioqK0Z88enT592j4mMTFR/v7+Cg8PL5qJAChS9A4ArlBijjDFx8frk08+0Zdffqly5crZzxsICAhQ6dKlFRAQoP79+2vEiBGqUKGC/P39NWTIEEVFRalFixaSpHbt2ik8PFx9+vTR1KlTlZycrNGjRys+Pp7fBIFbFL0DgCuUmMA0Z84cSVKbNm0clickJKhfv36SpBkzZsjDw0PdunVTenq6YmJi9M4779jHenp6aunSpRo0aJCioqJUpkwZxcXFaeLEiUU1DQBFjN4BwBVKTGDKy+WifH19NXv2bM2ePfuGY8LCwrR8+XJXlgbAjdE7ALhCiTmHCQAAoLgQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACwQmAAAACyUqMC0ceNGderUSaGhobLZbFqyZInDemOMxo4dqypVqqh06dKKjo7WoUOHHMacO3dOvXr1kr+/vwIDA9W/f39dvHixCGcBoCjRNwC4QokKTJcuXVKjRo00e/bsXNdPnTpVs2bN0ty5c7V161aVKVNGMTExunz5sn1Mr169tG/fPiUmJmrp0qXauHGjBg4cWFRTAFDE6BsAXKFUcReQHx06dFCHDh1yXWeM0cyZMzV69Gh17txZkvTRRx8pODhYS5YsUc+ePXXgwAGtXLlS27dvV9OmTSVJb7/9tjp27Khp06YpNDS0yOYCoGjQNwC4Qok6wnQzR48eVXJysqKjo+3LAgIC1Lx5cyUlJUmSkpKSFBgYaG96khQdHS0PDw9t3bo11+2mp6crLS3N4Qbg1lBYfUOidwC3mlsmMCUnJ0uSgoODHZYHBwfb1yUnJysoKMhhfalSpVShQgX7mOtNnjxZAQEB9lvVqlULoXoAxaGw+oZE7wBuNbdMYCoso0aNUmpqqv12/Pjx4i4JQAlA7wBuLbdMYAoJCZEkpaSkOCxPSUmxrwsJCdHp06cd1l+9elXnzp2zj7mej4+P/P39HW4Abg2F1Tckegdwq7llAlONGjUUEhKitWvX2pelpaVp69atioqKkiRFRUXp/Pnz2rlzp33MunXrlJWVpebNmxd5zQCKF30DQF6VqG/JXbx4UYcPH7bfP3r0qH744QdVqFBB1apV07Bhw/Taa6+pTp06qlGjhsaMGaPQ0FB16dJFklS/fn21b99eAwYM0Ny5c3XlyhUNHjxYPXv25JsuwC2KvgHAFUpUYNqxY4cefPBB+/0RI0ZIkuLi4jR//ny98MILunTpkgYOHKjz58+rVatWWrlypXx9fe2P+cc//qHBgwfroYcekoeHh7p166ZZs2YV+VwAFA36BgBXsBljTHEXUZKkpaUpICBAqampluckVH9pmcv3/8uUWJdvE3CWq1/jeX195+d96C6Ks3fQN+BOiuvfxoL2jVvmHCYAAIDCQmACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwQGACAACwcNsGptmzZ6t69ery9fVV8+bNtW3btuIuCYCbo28At6/bMjD985//1IgRIzRu3Djt2rVLjRo1UkxMjE6fPl3cpQFwU/QN4PZ2Wwam6dOna8CAAXrqqacUHh6uuXPnys/PTx9++GFxlwbATdE3gNtbqeIuoKhlZGRo586dGjVqlH2Zh4eHoqOjlZSUlGN8enq60tPT7fdTU1MlSWlpaZb7ykr/twsqdpSX/QJFxdWv8by+vrPHGWNcuv8byW/fkNyrd9A34E6K69/GgvaN2y4wnTlzRpmZmQoODnZYHhwcrJ9++inH+MmTJ2vChAk5lletWrXQaryZgJnFslugSOT39X3hwgUFBAQUSi3Xym/fkNyrd9A3cKvLz2vc2b5x2wWm/Bo1apRGjBhhv5+VlaVz586pYsWKstlsN3xcWlqaqlatquPHj8vf378oSi1Ut9J8mIt7ys9cjDG6cOGCQkNDi6i6/KN3MBd3divNJ69zKWjfuO0CU6VKleTp6amUlBSH5SkpKQoJCckx3sfHRz4+Pg7LAgMD87w/f3//Ev9ivNatNB/m4p7yOpeiOLKULb99Q6J3XIu5uK9baT55mUtB+sZtd9K3t7e3IiMjtXbtWvuyrKwsrV27VlFRUcVYGQB3Rd8AcNsdYZKkESNGKC4uTk2bNlWzZs00c+ZMXbp0SU899VRxlwbATdE3gNvbbRmYevTood9//11jx45VcnKyGjdurJUrV+Y4obMgfHx8NG7cuByH5EuqW2k+zMU9uftciqJvSO7/c8gP5uK+bqX5FNVcbKaovpcLAABQQt125zABAADkF4EJAADAAoEJAADAAoEJAADAAoEpH2bPnq3q1avL19dXzZs317Zt2246ftGiRapXr558fX0VERGh5cuXO6w3xmjs2LGqUqWKSpcurejoaB06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XL344os5xpUrV07z58/X/v379dZbb+n999/XjBkzJEk9evTQc889pwYNGtiPdvfo0SPHNi5duqSYmBiVL19e27dv16JFi7RmzRoNHjzYYdz69et15MgRrV+/XgsWLND8+fNzBEYUMQOUYAkJCSYgIMB+f/369UaSWbJkieVjGzRoYN5++237/bCwMDNjxgz7fUlm9OjR9vsXL140ksyKFStcUrsrJCQkGElm+/btNxyTnp5uxowZY5o0aWL8/f2Nn5+fadWqlVm3bp3DuKNHjxpJ5o033jDTp0831apVM76+vuaBBx4we/bsybHdAwcOmG7dupny5csbHx8fExkZab788kuHMdnPx/r16ws0j0mTJhlJ5r333rMvGzdunLm+ha1evdq0bNnSBAQEmDJlypi6deuaUaNGOdRy/S0hIcEYY0zr1q1NgwYNzI4dO8z9999vSpcubYYOHWpf17p16xzzWrhwoRk1apQJDg42fn5+plOnTubYsWMONYWFhZm4uLgcc7p2m1a1xcXFmbCwMIfHX7x40YwYMcLceeedxtvb29StW9e88cYbJisry2GcJBMfH28WL15sGjRoYLy9vU14eHiO13FaWpoZOnSoCQsLM97e3qZy5comOjra7Ny5M9fnpKgdOHAgx2vp/vvvN7179851fGxsrHnuuefs91u3bm1/Po1xfL+vWrXKlCpVyvz222/29StWrDCSzOLFi29Y0xtvvGEiIyPt98eNG2caNWqUY9y123nvvfdM+fLlzcWLF+3rly1bZjw8PExycrIx5v+e76tXr9rHPPHEE6ZHjx43rAWFr1TRRTOg6DRt2tTh/sWLFzV+/HgtW7ZMp06d0tWrV/Xnn39aHmG69ryIMmXKyN/fX6dPny6UmgtLWlqaPvjgAz355JMaMGCALly4oHnz5ikmJkbbtm3LcZLqRx99pAsXLig+Pl6XL1/WW2+9pbZt22rPnj0KDg6WJO3bt08tW7bUHXfcoZdeekllypTRZ599pi5duuh///d/9dhjj7l0Dn369NHLL7+s1atXa8CAAbmO2bdvnx555BE1bNhQEydOlI+Pjw4fPqxNmzZJkurXr6+JEydq7NixGjhwoO6//35J0n333WffxtmzZ9WhQwf17NlTvXv3ts/3Rl5//XXZbDa9+OKLOn36tGbOnKno6Gj98MMPKl26dJ7nl5farmWM0aOPPqr169erf//+aty4sVatWqXnn39ev/32m/2oR7bvvvtOX3zxhZ599lmVK1dOs2bNUrdu3XTs2DFVrFhRkvTXv/5Vn3/+uQYPHqzw8HCdPXtW3333nQ4cOKAmTZrkeS6FpV69errvvvv04Ycfqk2bNjp8+LC+/fZbTZw4UZmZmZo0aZI+++wz/fbbb8rIyFB6enqez1E6cOCAqlatqtDQUPuyqKioHOP++c9/atasWTpy5IguXryoq1evyt/fP1/zOHDggBo1aqQyZcrYl7Vs2VJZWVk6ePCg/TXXoEEDeXp62sdUqVJFe/bsyde+4FoEJtySrm1GkjRy5EglJiZq2rRpql27tkqXLq3HH39cGRkZN92Ol5eXw32bzaasrCyX11uYypcvr19++cXhI6UBAwaoXr16evvttzVv3jyH8YcPH9ahQ4d0xx13SJLat2+v5s2b629/+5umT58uSRo6dKiqVaum7du32z8qevbZZ9WqVSu9+OKLLg9Md955pwICAnTkyJEbjklMTFRGRoZWrFihSpUq5VgfHBysDh06aOzYsYqKilLv3r1zjElOTtbcuXP1zDPP5Kmuc+fO6cCBAypXrpwkqUmTJurevbvef/99/fd//3ceZ5e32q711Vdfad26dXrttdf0yiuvSJLi4+P1xBNP6K233tLgwYNVq1Yt+/gDBw5o//799mUPPvigGjVqpE8//dT+UdCyZcs0YMAAvfnmm/bHvfDCC3meQ1Ho37+/hgwZotmzZyshIUG1atVS69at9be//U1vvfWWZs6cqYiICJUpU0bDhg2zfH/nR1JSknr16qUJEyYoJiZGAQEBWrhwocPPy5Vuhd5zq+EcJtwWNm3apH79+umxxx5TRESEQkJC9MsvvxR3WUXC09PTHpaysrJ07tw5Xb16VU2bNtWuXbtyjO/SpYs9LElSs2bN1Lx5cy1fvlzSf0LCunXr1L17d124cEFnzpzRmTNndPbsWcXExOjQoUP67bffXD6PsmXL3vTbcoGBgZKkL7/80ul/WHx8fPTUU0/leXzfvn3tYUmSHn/8cVWpUsX+syosy5cvl6enZ45Q9txzz8kYoxUrVjgsj46OdghQDRs2lL+/v37++Wf7ssDAQG3dulUnT54s1NoLonv37vLw8NAnn3yijz76SH/5y19ks9m0adMmde7cWb1791ajRo1Us2ZN/etf/8rzduvXr6/jx4/r1KlT9mVbtmxxGLN582aFhYXplVdeUdOmTVWnTh39+uuvDmO8vb2VmZlpua/du3fr0qVL9mWbNm2Sh4eH7rrrrjzXjKJHYMJtoU6dOvriiy/0ww8/aPfu3fqv//qv2+q3tQULFqhhw4by9fVVxYoVVblyZS1btkypqak5xtapUyfHsrp169oD5uHDh2WM0ZgxY1S5cmWH27hx4ySpUD62vHjxokM4uV6PHj3UsmVLPf300woODlbPnj312Wef5et5vuOOO/J1cvf1PyubzabatWsXehj/9ddfFRoamuPnUb9+ffv6a1WrVi3HNsqXL68//vjDfn/q1Knau3evqlatqmbNmmn8+PEOgcodlC1bVj169NCoUaN06tQp9evXT9J/nofExERt3rxZBw4c0DPPPKOUlJQ8bzc6Olp169ZVXFycdu/erW+//dZ+5C5bnTp1dOzYMS1cuFBHjhzRrFmztHjxYocx1atX19GjR/XDDz/ozJkzSk9Pz7GvXr16ydfXV3Fxcdq7d6/Wr1+vIUOGqE+fPpYfAaN4EZhwW5g+fbrKly+v++67T506dVJMTIxbnJdRFD7++GP169dPtWrV0rx587Ry5UolJiaqbdu2ToXG7Mdkf8yZ26127douncOJEyeUmpp60+2WLl1aGzdu1Jo1a9SnTx/9+OOP6tGjhx5++GHL3/qv3Yar3ejimnmtyRWuPRfmWuaab5x1795dP//8s95++22FhobqjTfeUIMGDXIcrSpu/fv31x9//KGYmBj7OUejR49WkyZNFBMTozZt2igkJERdunTJ8zY9PDy0ePFi/fnnn2rWrJmefvppvf766w5jHn30UQ0fPlyDBw9W48aNtXnzZo0ZM8ZhTLdu3dS+fXs9+OCDqly5cq6XNvDz89OqVat07tw53XvvvXr88cf10EMP6X/+53/y/8NAkeIcJpRo/fr1s/+WKf3niszX/iOQrXr16lq3bp3Dsvj4eIf71x8VyG0758+fd7rW4vL555+rZs2a+uKLLxz+8c4+GnS9Q4cO5Vj2r3/9y36l6Zo1a0r6zzkW0dHRri84F3//+98lSTExMTcd5+HhoYceekgPPfSQpk+frkmTJumVV17R+vXrFR0d7fIrg1//szLG6PDhww5fFihfvnyur5tff/3V/rOUbhyschMWFqY1a9bowoULDkeZfvrpJ/t6Z1SpUkXPPvusnn32WZ0+fVpNmjTR66+/7lZXqY6Kisrx3qxQoYLlnx65/mrz17/f69atq2+//dZh2fX7mTp1qqZOneqw7No/t+Lj45Pr1d+v305ERESOfnSt3C4fwJ9tKn4cYQJucdlHF65t2lu3blVSUlKu45csWeJwDtK2bdu0detW+z+aQUFBatOmjd59912Hcz6y/f77764sX+vWrdOrr76qGjVqqFevXjccl9v1sbK/AZj90Uj2lwFcFXyzv1GY7fPPP8/xZzBq1aqlLVu2OJyAvHTpUh0/ftxhW/mprWPHjsrMzMxxVGLGjBmy2Wz5DjiZmZk5Pp4NCgpSaGhorh8rAbcjjjABt4APP/xQK1euzLF86NCheuSRR/TFF1/oscceU2xsrI4ePaq5c+cqPDzc4erC2WrXrq1WrVpp0KBBSk9P18yZM1WxYkWHb0zNnj1brVq1UkREhAYMGKCaNWsqJSVFSUlJOnHihHbv3u3UPFasWKGffvpJV69eVUpKitatW6fExESFhYXpq6++uumFKidOnKiNGzcqNjZWYWFhOn36tN555x3deeedatWqlaT/hJfAwEDNnTtX5cqVU5kyZdS8eXPVqFHDqXorVKigVq1a6amnnlJKSopmzpyp2rVrO1z64Omnn9bnn3+u9u3bq3v37jpy5Ig+/vhjh5Ow81tbp06d9OCDD+qVV17RL7/8okaNGmn16tX68ssvNWzYsBzbtnLhwgXdeeedevzxx9WoUSOVLVtWa9as0fbt2wvtW2BAiVNM138C4ALZF3y80e348eMmKyvLTJo0yYSFhRkfHx9zzz33mKVLl+a4GOK1F6588803TdWqVY2Pj4+5//77ze7du3Ps+8iRI6Zv374mJCTEeHl5mTvuuMM88sgj5vPPP7ePye+FK7Nv3t7eJiQkxDz88MPmrbfeMmlpaTkec/2FK9euXWs6d+5sQkNDjbe3twkNDTVPPvmk+de//uXwuC+//NKEh4ebUqVK5Xrhytzc6MKVn376qRk1apQJCgoypUuXNrGxsebXX3/N8fg333zT3HHHHcbHx8e0bNnS7NixI8c2b1ZbbheuvHDhghk+fLgJDQ01Xl5epk6dOje9cOX1rr2gZnp6unn++edNo0aNTLly5UyZMmVMo0aNzDvvvJPrzwO4HdmMyeVEDQAAANhxDhMAAIAFAhMAAIAFAhMAAIAFAhMAAIAFAhMAAIAFAhMAAIAFAhMAAIAFAhMAAIAFAhMAAIAFAhMAAIAFAhMAAIAFAhMAAICF/we8k2nbBaARcwAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 470 + }, + "id": "KM_sOu9sARAw", + "outputId": "25602dc3-05cb-4784-c5a5-295eaa9ea0a1" + }, + "outputs": [], "source": [ - "# We can look at the distribution of labels by grabbing them from the generator \n", - " # TF.Sequence generators supply data as a tuple \n", - " # of (x,y) (or (features, labels)) \n", - " # - so using index 1 we can get the labels\n", - "\n", - "all_train_labels = np.array([\n", - " train_generator[i][1] for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][1] for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.supxlabel(\"Label Distributions\")\n", - "figure.supylabel(\"Label Frequency\")\n", - "\n", - "plt.show()" + "# Each dataset object has a \"labels\" and \"images\" attribute, which we can use to check out the data range\n", + "\n", + "fig, ax = plt.subplots(1,2)\n", + "ax[0].hist(train_generator.dataset.labels)\n", + "ax[0].set_title(\"Train labels\")\n", + "ax[1].hist(val_generator.dataset.labels)\n", + "ax[1].set_title(\"Val labels\")\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAHgCAYAAAB3vm02AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAABIUElEQVR4nO3de1xU1eL38e+AMmBc1JKLiHjXVLxhGpiiqaGZRzpldkVLrdMPficzsyjL1Ao7Hm9P3k8pp4tplpfzqGmkkplYalJqZVreA0xLUEssWM8fPc5h5KKDDFvk83699uvlXrPW2msxw/LLnj2zbcYYIwAAAIt4WD0AAABQtRFGAACApQgjAADAUoQRAABgKcIIAACwFGEEAABYijACAAAsRRgBAACWIowAAABLEUaA/69BgwYaMmSI1cMoVXZ2tu68805de+21stlsmjZt2mX3+cILL8hms13+4EqQlpYmm82mtLS0y+5ryJAhatCgwWX3U1EOHDggm82mlJQUtx6nvJ/DlJQU2Ww2HThwoNz6BEpDGKlizi8yNptNmzZtKvK4MUZhYWGy2Wy67bbbLBghSvP4449r7dq1SkpK0ptvvqk+ffpYPSQAuGzVrB4ArOHt7a2FCxfqpptucir/+OOPdeTIEdntdotGhtKsX79eAwYM0KhRo8qtzzFjxujpp58ut/7c6V//+pcKCgqsHsYlCw8P12+//abq1atbPRSXPPDAA7r77rtZB1BhODNSRd16661asmSJ/vjjD6fyhQsXKjIyUsHBwRaNDKU5duyYatasWa59VqtWTd7e3uXap7tUr169Uv0HabPZ5O3tLU9PT6uH4hJPT095e3u79e07oDDCSBV1zz336MSJE0pNTXWUnTt3Tu+9957uvffeYtsUFBRo2rRpatWqlby9vRUUFKRHHnlEv/zyi1O9FStWqF+/fqpbt67sdrsaN26sCRMmKD8/36le9+7d1bp1a3399dfq0aOHatSoodDQUP3jH/+46Phbt26tHj16FDvG0NBQ3XnnnY6yf/7zn4qOjta1114rHx8fRUZG6r333rvoMUp6H76k99M/+OADde3aVddcc438/PzUr18/7d69+6LHkaQffvhBAwcOVO3atVWjRg3deOONWrVqVZFjGmM0c+ZMx1ttJTl/rcI///lPTZ06VeHh4fLx8VFMTIx27dpV6jwXLFggm82m+fPnO9V7+eWXZbPZtHr1akfZt99+qzvvvFO1a9eWt7e3OnbsqP/85z8Xne/evXt1xx13KDg4WN7e3qpXr57uvvtu5eTklNruwmtGCs9z3rx5aty4sex2u2644QZt3br1ouP4+eefNWrUKEVERMjX11f+/v7q27evvvzyy4u2laTU1FTddNNNqlmzpnx9fdW8eXM988wzRcZX+JqRIUOGyNfXV0ePHlVcXJx8fX1Vp04djRo1qsjvyIkTJ/TAAw/I399fNWvW1ODBg/Xll19e8nUob731liIjI+Xj46PatWvr7rvv1uHDhy/arrjXeIMGDXTbbbcpLS1NHTt2lI+PjyIiIhzXAi1dulQRERHy9vZWZGSkduzY4dTnV199pSFDhqhRo0by9vZWcHCwHnroIZ04caLI8c8fw9vbW40bN9bcuXNL/H0s6xxxhTGoUhYsWGAkma1bt5ro6GjzwAMPOB5bvny58fDwMEePHjXh4eGmX79+Tm2HDRtmqlWrZoYPH27mzJljnnrqKXPNNdeYG264wZw7d85RLy4uztx1111m0qRJZvbs2WbgwIFGkhk1apRTfzExMaZu3bomLCzMPPbYY2bWrFnm5ptvNpLM6tWrS53H+PHjjYeHh8nMzHQq//jjj40ks2TJEkdZvXr1zP/8z/+YGTNmmClTpphOnToZSWblypVObcPDw83gwYMd+2PHjjXF/Yqc/xnu37/fUfbGG28Ym81m+vTpY1599VXzyiuvmAYNGpiaNWs61StOVlaWCQoKMn5+fubZZ581U6ZMMW3btjUeHh5m6dKlxhhjvv/+e/Pmm28aSaZ3797mzTffNG+++WaJfe7fv99IMhEREaZBgwbmlVdeMePGjTO1a9c2derUMVlZWaXO87bbbjMBAQHm0KFDxhhjvvrqK+Pl5WWGDh3qqLNr1y4TEBBgWrZsaV555RUzY8YM061bN2Oz2RzjNsaYDRs2GElmw4YNxhhj8vLyTMOGDU3dunXNiy++aF577TUzbtw4c8MNN5gDBw6U+rMaPHiwCQ8PLzLP9u3bmyZNmphXXnnF/OMf/zDXXXedqVevntPrsjhbt241jRs3Nk8//bSZO3euGT9+vAkNDTUBAQHm6NGjpbbdtWuX8fLyMh07djTTp083c+bMMaNGjTLdunUrMr4FCxY4zcHb29u0atXKPPTQQ2b27NnmjjvuMJLMrFmzHPXy8/NNVFSU8fT0NImJiWbGjBmmd+/epm3btkX6LO45fPHFF43NZjODBg0ys2bNMuPGjTPXXXedadCggfnll19KnVtxr/Hw8HDTvHlzExISYl544QUzdepUExoaanx9fc1bb71l6tevbyZOnGgmTpxoAgICTJMmTUx+fr6j/T//+U/TtWtXM378eDNv3jzz2GOPGR8fH9OpUydTUFDgqPfFF18Yu91uGjRoYCZOnGheeuklU7duXce8y2uOuLIQRqqYwmFkxowZxs/Pz/z666/GGGMGDhxoevToYYwxRcLIJ598YiSZt99+26m/NWvWFCk/319hjzzyiKlRo4Y5e/asoywmJsZIMm+88YajLC8vzwQHB5s77rij1Hns2bPHSDKvvvqqU/n//M//GF9fX6cxXDiec+fOmdatW5ubb77ZqbysYeTUqVOmZs2aZvjw4U71srKyTEBAQJHyC40YMcJIMp988omj7NSpU6Zhw4amQYMGTgu6JJOQkFBqf8b89z9BHx8fc+TIEUf5Z599ZiSZxx9/vNR5ZmZmmtq1a5vevXubvLw80759e1O/fn2Tk5PjqNOzZ08TERHh9JwWFBSY6Oho07RpU0fZhWFkx44dRQLjpSopjFx77bXm559/dpSvWLHCSDL/9//+31L7O3v2rNPP93yfdrvdjB8/vtS2U6dONZLMTz/9VGKdksKIpCL9t2/f3kRGRjr233//fSPJTJs2zVGWn5/vCOylhZEDBw4YT09P89JLLzkdY+fOnaZatWpFyi9UUhiRZDZv3uwoW7t2reN1dvDgQUf53LlznZ5zY4pfF9555x0jyWzcuNFR1r9/f1OjRg2nMLh3715TrVq1cp0jriy8TVOF3XXXXfrtt9+0cuVKnTp1SitXrizxLZolS5YoICBAvXv31vHjxx1bZGSkfH19tWHDBkddHx8fx79PnTql48ePq2vXrvr111/17bffOvXr6+ur+++/37Hv5eWlTp066Ycffih17M2aNVO7du20ePFiR1l+fr7ee+899e/f32kMhf/9yy+/KCcnR127dtUXX3xxkZ/QpUlNTdXJkyd1zz33OP1sPD091blzZ6efTXFWr16tTp06OV1M7Ovrq4cfflgHDhzQ119/XeaxxcXFKTQ01LHfqVMnde7c2emtluIEBwdr5syZSk1NVdeuXZWRkaH58+fL399f0p9vb6xfv1533XWX4zk+fvy4Tpw4odjYWO3du1dHjx4ttu+AgABJ0tq1a/Xrr7+WeW6FDRo0SLVq1XLsd+3aVZIu+jqy2+3y8PhzGczPz9eJEyccb7dc7PVx/tqdFStWlOmi2r/97W9O+127dnUa75o1a1S9enUNHz7cUebh4aGEhISL9r106VIVFBTorrvucnpNBgcHq2nTphd9TZakZcuWioqKcux37txZknTzzTerfv36RcoLz6fw7+HZs2d1/Phx3XjjjZLk+Fnn5+fro48+UlxcnOrWreuo36RJE/Xt27dC5ghr8GmaKqxOnTrq1auXFi5cqF9//VX5+flO11oUtnfvXuXk5CgwMLDYx48dO+b49+7duzVmzBitX79eubm5TvUuvCagXr16Rd4HrlWrlr766quLjn/QoEF65plndPToUYWGhiotLU3Hjh3ToEGDnOqtXLlSL774ojIyMpSXl+coL6+L8/bu3SvpzwW5OOf/Ay/JwYMHHYt3Yddff73j8datW5dpbE2bNi1S1qxZM7377rsXbXv33Xfrrbfe0qpVq/Twww+rZ8+ejsf27dsnY4yee+45Pffcc8W2P3bsmFMQOq9hw4YaOXKkpkyZorfffltdu3bVX/7yF91///2OoOKqwv8RSnIEkwuvZ7pQQUGBpk+frlmzZmn//v1O12xce+21pbYdNGiQXnvtNQ0bNkxPP/20evbsqb/+9a+68847HQGnJN7e3qpTp06RMRce78GDBxUSEqIaNWo41WvSpEmpfUt/viaNMcU+/5LK/OmeC3/O55+vsLCwYssLz+fnn3/WuHHjtGjRIqf1QvrvunDs2DH99ttvxc7xwjJ3zRHWIIxUcffee6+GDx+urKws9e3bt8RPahQUFCgwMFBvv/12sY+fX1hPnjypmJgY+fv7a/z48WrcuLG8vb31xRdf6KmnniryF2RJnzIwxlx07IMGDVJSUpKWLFmiESNG6N1331VAQIDTd2988skn+stf/qJu3bpp1qxZCgkJUfXq1bVgwQItXLiw1P5LCisXXmR4fk5vvvlmsZ9Cqlatcv6anThxQtu2bZMkff311yooKHD8J3t+zqNGjVJsbGyx7Uv7T3Py5MkaMmSIVqxYoQ8//FB///vflZycrC1btqhevXouj7Wsr6OXX35Zzz33nB566CFNmDBBtWvXloeHh0aMGHHRsx0+Pj7auHGjNmzYoFWrVmnNmjVavHixbr75Zn344YelfoLG3Z+uKSgokM1m0wcffFDssXx9fcvUb0njvpSf/1133aXNmzfrySefVLt27eTr66uCggL16dOnTGeW3DVHWKNyrpIoN7fffrseeeQRbdmyxektjws1btxYH330kbp06eJ0uvVCaWlpOnHihJYuXapu3bo5yvfv31+u45b+/Au7U6dOWrx4sRITE7V06VLFxcU5ffTz/fffl7e3t9auXetUvmDBgov2f/6v65MnTzqFtIMHDzrVa9y4sSQpMDBQvXr1cnke4eHh2rNnT5Hy829phYeHu9zneefP2hT23XffXdK3mCYkJOjUqVNKTk5WUlKSpk2bppEjR0qSGjVqJOnPvz7LMmdJioiIUEREhMaMGaPNmzerS5cumjNnjl588cUy9VcW7733nnr06KHXX3/dqfzkyZO67rrrLtrew8NDPXv2VM+ePTVlyhS9/PLLevbZZ7Vhw4Yy/1zOCw8P14YNG/Trr786nR3Zt2/fRds2btxYxhg1bNhQzZo1u6xxlIdffvlF69at07hx4/T88887yi98fQYGBsrb27vYOV5YdqXNEZeHa0aqOF9fX82ePVsvvPCC+vfvX2K9u+66S/n5+ZowYUKRx/744w+dPHlS0n//Qir8F9G5c+c0a9as8h34/zdo0CBt2bJF8+fP1/Hjx4u8RePp6SmbzeZ0NuPAgQNavnz5Rfs+HzI2btzoKDtz5oz+/e9/O9WLjY2Vv7+/Xn75Zf3+++9F+vnpp59KPc6tt96qzz//XOnp6U7HmTdvnho0aKCWLVtedKwlWb58udO1G59//rk+++yzIu+/X+i9997T4sWLNXHiRD399NO6++67NWbMGH333XeS/vxPo3v37po7d64yMzOLtC9tzrm5uUW+3yYiIkIeHh5Ob6NVBE9PzyJnT5YsWVLi9S6F/fzzz0XK2rVrJ0nlMo/Y2Fj9/vvv+te//uUoKygo0MyZMy/a9q9//as8PT01bty4IvMzxhT7cVp3Km5dkFTkdgaenp7q1auXli9frh9//NFRvm/fPn3wwQdOda+0OeLycGYEGjx48EXrxMTE6JFHHlFycrIyMjJ0yy23qHr16tq7d6+WLFmi6dOn684771R0dLRq1aqlwYMH6+9//7tsNpvefPPNS3rbpSzuuusujRo1SqNGjVLt2rWL/DXar18/TZkyRX369NG9996rY8eOaebMmWrSpMlFr0u55ZZbVL9+fQ0dOlRPPvmkPD09NX/+fNWpU0eHDh1y1PP399fs2bP1wAMPqEOHDrr77rsddVatWqUuXbpoxowZJR7n6aef1jvvvKO+ffvq73//u2rXrq1///vf2r9/v95///2LXn9QmiZNmuimm27So48+qry8PE2bNk3XXnutRo8eXWKbY8eO6dFHH1WPHj2UmJgoSZoxY4Y2bNigIUOGaNOmTfLw8NDMmTN10003KSIiQsOHD1ejRo2UnZ2t9PR0HTlypMTv6li/fr0SExM1cOBANWvWTH/88YfefPNNeXp66o477ijzXMvitttu0/jx4/Xggw8qOjpaO3fu1Ntvv+0481Oa8ePHa+PGjerXr5/Cw8N17NgxzZo1S/Xq1SvyzcZlERcXp06dOumJJ57Qvn371KJFC/3nP/9xhKDSrnlq3LixXnzxRSUlJenAgQOKi4uTn5+f9u/fr2XLlunhhx8u12/xvRh/f39169ZN//jHP/T7778rNDRUH374YbFnTF944QV9+OGH6tKlix599FHl5+drxowZat26tTIyMq7YOeIyVfwHeGClwh/tLU1x3zNijDHz5s0zkZGRxsfHx/j5+ZmIiAgzevRo8+OPPzrqfPrpp+bGG280Pj4+pm7dumb06NGOjwAW/qhfTEyMadWqVZFjXPjxzYvp0qWLkWSGDRtW7OOvv/66adq0qbHb7aZFixZmwYIFxX6c9cKP9hpjzPbt203nzp2Nl5eXqV+/vpkyZUqxH3s05s+PsMbGxpqAgADj7e1tGjdubIYMGWK2bdt20Tl8//335s477zQ1a9Y03t7eplOnTkW+B8UY1z/aO2nSJDN58mQTFhZm7Ha76dq1q/nyyy+d6l74s/jrX/9q/Pz8inznx/mPy77yyitO446PjzfBwcGmevXqJjQ01Nx2223mvffec/q5FH7uf/jhB/PQQw+Zxo0bG29vb1O7dm3To0cP89FHH110XiV9tHfSpElF6koyY8eOLbW/s2fPmieeeMKEhIQYHx8f06VLF5Oenm5iYmJMTExMqW3XrVtnBgwYYOrWrWu8vLxM3bp1zT333GO+++67IuO78KO911xzTZH+intN/vTTT+bee+81fn5+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a6+9pttvv101atRQ06ZN9Z///KeCZwvgtttuU506dZSSkuJUfvr0aS1ZskRxcXG65557FBoaqho1aigiIkLvvPNOqX1e+Pu+d+9edevWTd7e3mrZsqVSU1OLtHnqqafUrFkz1ahRQ40aNdJzzz2n33//XZKUkpKicePG6csvv3ScpT0/3gvfptm5c6duvvlm+fj46Nprr9XDDz+s06dPOx4fMmSI4uLi9M9//lMhISG69tprlZCQ4DgW3IswgnLz9NNPa+LEifrmm2/Upk0bnT59WrfeeqvWrVunHTt2qE+fPurfv78OHTpUaj/jxo3TXXfdpa+++kq33nqr7rvvPv38888VNAsAklStWjXFx8crJSVFhe+numTJEuXn5+v+++9XZGSkVq1apV27dunhhx/WAw88oM8///yS+i8oKNBf//pXeXl56bPPPtOcOXP01FNPFann5+enlJQUff3115o+fbr+9a9/aerUqZKkQYMG6YknnlCrVq2UmZmpzMxMDRo0qEgfZ86cUWxsrGrVqqWtW7dqyZIl+uijj5SYmOhUb8OGDfr++++1YcMG/fvf/1ZKSkqRMAY3cfk+v6jyFixYYAICAhz7GzZsMJLM8uXLL9q2VatW5tVXX3Xsh4eHm6lTpzr2JZkxY8Y49k+fPm0kmQ8++KBcxg7g0n3zzTdGktmwYYOjrGvXrub+++8vtn6/fv3ME0884diPiYkxjz32mGO/8O/72rVrTbVq1czRo0cdj3/wwQdGklm2bFmJY5o0aZKJjIx07I8dO9a0bdu2SL3C/cybN8/UqlXLnD592vH4qlWrjIeHh+N294MHDzbh4eHmjz/+cNQZOHCgGTRoUIljQfnhzAjKTceOHZ32T58+rVGjRun6669XzZo15evrq2+++eaiZ0YKvw99zTXXyN/fX8eOHXPLmAGUrEWLFoqOjtb8+fMlSfv27dMnn3yioUOHKj8/XxMmTFBERIRq164tX19frV279qK/3+d98803CgsLU926dR1lUVFRReotXrxYXbp0UXBwsHx9fTVmzJhLPkbhY7Vt21bXXHONo6xLly4qKChwuvatVatW8vT0dOyHhISw9lQQwgjKTeFfdEkaNWqUli1bppdfflmffPKJMjIyFBERoXPnzpXaT/Xq1Z32bTabCgoKyn28AC5u6NChev/993Xq1CktWLBAjRs3VkxMjCZNmqTp06frqaee0oYNG5SRkaHY2NiL/n67Ij09Xffdd59uvfVWrVy5Ujt27NCzzz5brscojLXHOoQRuM2nn36qIUOG6Pbbb1dERISCg4N14MABq4cFwAV33XWXPDw8tHDhQr3xxht66KGHZLPZ9Omnn2rAgAG6//771bZtWzVq1EjffffdJfd7/fXX6/Dhw06f1NuyZYtTnc2bNys8PFzPPvusOnbsqKZNm+rgwYNOdby8vJSfn3/RY3355Zc6c+aMo+zTTz+Vh4eHmjdvfsljhvsQRuA2TZs21dKlS5WRkaEvv/xS9957L39lAJWMr6+vBg0apKSkJGVmZmrIkCGS/vz9Tk1N1ebNm/XNN9/okUceUXZ29iX326tXLzVr1kyDBw/Wl19+qU8++UTPPvusU52mTZvq0KFDWrRokb7//nv9n//zf7Rs2TKnOg0aNND+/fuVkZGh48ePKy8vr8ix7rvvPnl7e2vw4MHatWuXNmzYoP/93//VAw88oKCgINd/KCh3hBG4zZQpU1SrVi1FR0erf//+io2NVYcOHaweFgAXDR06VL/88otiY2Md13iMGTNGHTp0UGxsrLp3767g4GDFxcVdcp8eHh5atmyZfvvtN3Xq1EnDhg3TSy+95FTnL3/5ix5//HElJiaqXbt22rx5s5577jmnOnfccYf69OmjHj16qE6dOsV+vLhGjRpau3atfv75Z91www2688471bNnT82YMcP1HwbcwmZMoc9sAQAAVDDOjAAAAEsRRgAAgKUIIwAAwFKEEQAAYCnCCAAAsBRhBAAAWIowAgAALEUYAQAAliKMAAAASxFGAACApQgjAADAUv8Pr92srYcem4MAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 449 }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhYAAAHgCAYAAAD5ZCRKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAABGWUlEQVR4nO3deVxVdf7H8fcF5OICuCSLirjmvmIpOoalhuto08/MzC21qbDJdCppU2sSmzJ10kwroTLSNLVyHTTRUiw3Sls0zdQcwNxAtLDg+/tjHt7xyiIXz2Xz9Xw8zuPh+d7vOefz5XK/vjn3nHttxhgjAAAAC3iUdAEAAKD8IFgAAADLECwAAIBlCBYAAMAyBAsAAGAZggUAALAMwQIAAFiGYAEAACxDsAAAAJYhWKBM69atm7p161bSZRQoMzNTY8aMUVBQkGw2m8aPH3/N+4yLi5PNZtNPP/10zfvKy08//SSbzaa4uLhr3teUKVNks9muvahiZLPZNGXKFLcew+rnMDExUTabTYmJiZbsDygqgkU5c2mystls+vzzz3M9boxRSEiIbDab+vXrVwIVXn+mTZumuLg4Pfjgg3r33Xc1bNiwki4JANzGq6QLgHv4+PgoPj5ef/rTn5zaN2/erJ9//ll2u72EKrv+fPrpp+rUqZMmT55s2T6HDRumu+++u0w8j08//bQmTZpU0mW45Ndff5WXV9maHm+55Rb9+uuv8vb2LulScJ3jjEU51adPHy1dulR//PGHU3t8fLzCwsIUFBRUQpVdf06cOKGqVatauk9PT0/5+PiUibcYvLy85OPjU9JluMTHx6fMBQsPDw/5+PjIw4NpHSWL38ByasiQITp16pQSEhIcbRcvXtSyZct0zz335LnNyy+/rM6dO6tGjRqqWLGiwsLCtGzZMqc+sbGxstlsWrhwoVP7tGnTZLPZtGbNmnxr6tevnxo0aJDnY+Hh4erQoYPTcW677TYFBATIbrerefPmmjdv3lXHnd/71vm9//zFF1+oV69e8vf3V6VKlRQREaGtW7de9TjSfwPD6NGjFRgYKB8fH7Vp00Zvv/12rmMePnxYq1evdrxFVdB76jabTePGjdN7772nJk2ayMfHR2FhYdqyZUuB4/z000/l4eGhZ5991qlffHy8bDab08/u+PHjuu+++xQYGCi73a4WLVrkej7zkpqaqlGjRqlOnTqy2+0KDg7WgAEDrnqNQF7XWFwa58qVK9WyZUtHHevWrbtqHRcvXtSzzz6rsLAw+fv7q3Llyuratas2bdp01W0laefOnYqMjNQNN9ygihUrqn79+rrvvvty1Xf5NRaXxnDw4EGNHDlSVatWlb+/v0aNGqULFy44bfvrr7/qb3/7m2644Qb5+vrqz3/+s44fP17o6zbWrl2rrl27qnLlyvL19VXfvn31zTffXHW7vH7Hu3XrppYtW+rrr79WRESEKlWqpEaNGjle15s3b1bHjh1VsWJFNWnSRBs2bHDa55EjR/TQQw+pSZMmqlixomrUqKFBgwbl+ZxfOkbFihVVp04d/eMf/3DMF1f2L+oYUUYYlCuxsbFGktmxY4fp3LmzGTZsmOOxlStXGg8PD3P8+HETGhpq+vbt67RtnTp1zEMPPWTmzJljXnnlFXPzzTcbSWbVqlVO/fr162f8/f3N0aNHjTHGfP3118bb29uMHj26wNreeecdI8l8+eWXTu0//fSTkWReeuklR9tNN91kRo4caWbOnGleffVVc/vttxtJZs6cOU7bRkREmIiIiFzjP3z4sFO/TZs2GUlm06ZNjraNGzcab29vEx4ebmbMmGFmzpxpWrdubby9vc0XX3xR4FguXLhgmjVrZipUqGAeffRR869//ct07drVSDKzZs0yxhiTmppq3n33XXPDDTeYtm3bmnfffde8++67JjMzM9/9SjItW7Y0N9xwg3nuuefMiy++aEJDQ03FihXN3r17CxxnVFSU8fLyMrt27TLGGPOf//zHVK9e3fTo0cPk5OQ4aqpTp44JCQkxzz33nJk3b57585//bCSZmTNnOvZ1+PBhI8nExsY62jp37mz8/f3N008/bd58800zbdo0c+utt5rNmzcX+LOaPHmyuXKqkWTatGljgoODzfPPP29mzZplGjRoYCpVqmROnjxZ4P5++eUXExwcbCZMmGDmzZtn/vnPf5omTZqYChUqmD179hS4bVpamqlWrZq58cYbzUsvvWTeeOMN89RTT5lmzZrlqm/y5Mm5xtCuXTvzl7/8xbz22mtmzJgxRpJ5/PHHnba96667jCQzbNgwM3fuXHPXXXeZNm3a5NpnXs/hO++8Y2w2m+nVq5d59dVXzYsvvmjq1atnqlatmut3+kp5/Y5HRESYWrVqmZCQEPPYY4+ZV199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4w799a9/1fbt27VkyZJ8+3344Yfy8fHR+vXrnf5yi42NzbN/VFSUzp07p5iYGEVHR2vWrFmaMGHCVeupXLmy+vXrp6VLl+qVV17RkiVL1LVrV9WqVcvR55NPPlFWVpY+/vhjp78QC3Oa9NJfkGfPnnVqv/IMwqVT8H5+furRo8dV93ul0NBQff3117kC1ffff+94vKgu/VV3uQMHDqhSpUqqWbNmgdtOnjxZ3333nV5++WU98cQTmjRpkv71r39J+u8ZLF9fX2VnZxdpzNJ/f24TJ07UxIkT9cMPP6ht27aaMWOGFi1aVKT9FcWyZcvUoEEDLV++3ClIuvI5IZ06dVKnTp30wgsvKD4+XkOHDtXixYs1ZsyYa6otNDRUOTk5Onz4sNOZp4MHD15120u/kwEBAUV+fqy2bNkyjRgxQjNmzHC0/fbbb7leX6GhoXmO8cq20jhGWI9rLMq5KlWqaN68eZoyZYr69++fbz9PT0/ZbDanv+x/+uknrVy5MlffZcuWacmSJZo+fbomTZqku+++W08//bQOHDhQqJoGDx6s//znP3rzzTf11VdfafDgwblqkZTrtGt+Iedylyauy2/PzM7OznXqPywsTA0bNtTLL7+szMzMXPv55ZdfCjxOnz59lJqa6hTW/vjjD7366quqUqWKIiIirlprfpKSkpzevz527Jg++ugj3X777fn+lSn999bZl19+WePHj9fEiRP12GOPac6cOY7rPTw9PXXnnXfqww8/1L59+3JtX9CYL1y4oN9++82prWHDhvL19XV626w45PX78cUXXygpKemq2545cybX2YVLZ6ysGEdkZKQk6bXXXnNqf/XVVwu1rZ+fn6ZNm5bn9R9X+510B09Pz1w/r1dffTXXGcDIyEglJSUpOTnZ0Xb69Gm99957ufqVtjHCepyxuA4UdB3BJX379tUrr7yiXr166Z577tGJEyc0d+5cNWrUSF9//bWj34kTJ/Tggw/q1ltv1bhx4yRJc+bM0aZNmzRy5Eh9/vnnV31LpE+fPvL19dXf//53x392l7v99tvl7e2t/v37669//asyMzP1xhtvKCAgQCkpKQXuu0WLFurUqZOio6N1+vRpVa9eXYsXL871QWEeHh5688031bt3b7Vo0UKjRo1S7dq1dfz4cW3atEl+fn765JNP8j3O/fffr/nz52vkyJHatWuX6tWrp2XLlmnr1q2aNWuWfH19C6yzIC1btlRkZKT+9re/yW63O/6Tmjp1ar7b/PbbbxoxYoQaN26sF154wdH/k08+0ahRo7R3715VrlxZ06dP16ZNm9SxY0eNHTtWzZs31+nTp7V7925t2LBBp0+fznP/Bw4cUPfu3XXXXXepefPm8vLy0ooVK5SWluZ0YWBx6Nevn5YvX6477rhDffv21eHDh/X666+refPmeYbEy7399tt67bXXdMcdd6hhw4Y6d+6c3njjDfn5+alPnz7XXFtYWJjuvPNOzZo1S6dOnVKnTp20efNmR+gu6APN/Pz8NG/ePA0bNkzt27fX3XffrZo1a+ro0aNavXq1unTpojlz5lxzja7o16+f3n33Xfn7+6t58+ZKSkrShg0bVKNGDad+jz/+uBYtWqSePXvq4YcfVuXKlfXmm2+qbt26On36tGPcpXGMcIMSuhsFbnL57aYFyesWyLfeess0btzY2O1207RpUxMbG5vrVsG//OUvxtfX1/z0009O23700UdGknnxxRcLVefQoUONJNOjR488H//4449N69atjY+Pj6lXr5558cUXzcKFC3PdYnnl7abGGHPo0CHTo0cPY7fbTWBgoHnyySdNQkJCrlvxjDFmz5495i9/+YupUaOGsdvtJjQ01Nx1111m48aNVx1DWlqaGTVqlLnhhhuMt7e3adWqldOtepe4ertpVFSUWbRokeO5aNeuXa66r7xV8dFHHzWenp65Pn9j586dxsvLyzz44INOdUdFRZmQkBBToUIFExQUZLp3724WLFjg6HPl7aYnT540UVFRpmnTpqZy5crG39/fdOzY0XzwwQdXHVN+t5tGRUXl6hsaGmpGjBhR4P5ycnLMtGnTTGhoqOPns2rVKjNixAgTGhpa4La7d+82Q4YMMXXr1jV2u90EBASYfv36Od3ee6m+vG43/eWXX5z65Xfbb1RUlKlevbqpUqWKGThwoNm/f7+RZKZPn17gtsb897bRyMhI4+/vb3x8fEzDhg3NyJEjc9V4pfxuN23RokWuvvn9Tl75vJw5c8bxO16lShUTGRlpvv/++zyfpz179piuXbsau91u6tSpY2JiYsy//vUvI8mkpqZaMkaUDTZjLLjqCIAlbDaboqKi+KutnElOTla7du20aNGiXLdglmfjx4/X/PnzlZmZWeDbeChfuMYC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RpiIiAintk2bNpm2bdsab29v06BBAxMbG1t8BRfA1bFEREQYSbmWK38XS0JRnpfLldVgUZ7miqIob/NLUZSnOclVZX0O42vTAQCAZcrWFT0AAKBUI1gAAADLECwAAIBlCBYAAMAyBAsAAGAZggUAALAMwQIAAFiGYAEAACxDsIBl6tWrp1mzZpV0GQDcoFu3bho/frxjvTCvd5vNppUrV17zsa3aD4oHweI6ZLPZClymTJlSpP3u2LFD999/v7XFArhm/fv3V69evfJ87LPPPpPNZtPXX3/t0j7d8XqfMmWK2rZtm6s9JSVFvXv3tvRYcJ8S/dp0lIyUlBTHv5csWaJnn33W6dv0qlSp4vi3MUbZ2dny8rr6r0rNmjWtLRSAJUaPHq0777xTP//8s+rUqeP0WGxsrDp06KDWrVu7tM/ifL0HBQUV27Fw7ThjcR0KCgpyLP7+/rLZbI7177//Xr6+vlq7dq3CwsJkt9v1+eef69ChQxowYIACAwNVpUoV3XTTTdqwYYPTfq88NWqz2fTmm2/qjjvuUKVKldS4cWN9/PHHxTxaAP369VPNmjUVFxfn1J6ZmamlS5dq4MCBGjJkiGrXrq1KlSqpVatWev/99wvc55Wv9x9++EG33HKLfHx81Lx5cyUkJOTa5oknntCNN96oSpUqqUGDBnrmmWf0+++/S5Li4uI0depUffXVV46zp5fqvfKtkL179+q2225TxYoVVaNGDd1///3KzMx0PD5y5EgNHDhQL7/8soKDg1WjRg1FRUU5jgX3IlggT5MmTdL06dP13XffqXXr1srMzFSfPn20ceNG7dmzR7169VL//v119OjRAvczdepU3XXXXfr666/Vp08fDR06VKdPny6mUQCQJC8vLw0fPlxxcXG6/Hsnly5dquzsbN17770KCwvT6tWrtW/fPt1///0aNmyYvvzyy0LtPycnR3/5y1/k7e2tL774Qq+//rqeeOKJXP18fX0VFxenb7/9VrNnz9Ybb7yhmTNnSpIGDx6siRMnqkWLFkpJSVFKSooGDx6cax/nz59XZGSkqlWrph07dmjp0qXasGGDxo0b59Rv06ZNOnTokDZt2qS3335bcXFxuYIV3KREvlMVpUZsbKzx9/d3rG/atMlIMitXrrzqti1atDCvvvqqYz00NNTMnDnTsS7JPP300471zMxMI8msXbvWktoBFN53331nJJlNmzY52rp27WruvffePPv37dvXTJw40bEeERFhHnnkEcf65a/39evXGy8vL3P8+HHH42vXrjWSzIoVK/Kt6aWXXjJhYWGO9cmTJ5s2bdrk6nf5fhYsWGCqVatmMjMzHY+vXr3aeHh4mNTUVGPMf79WPDQ01Pzxxx+OPoMGDTKDBw/OtxZYhzMWyFOHDh2c1jMzM/X3v/9dzZo1U9WqVVWlShV99913Vz1jcfn7tpUrV5afn59OnDjhlpoB5K9p06bq3LmzFi5cKEk6ePCgPvvsM40ePVrZ2dl6/vnn1apVK1WvXl1VqlTR+vXrr/r6vuS7775TSEiIatWq5WgLDw/P1W/JkiXq0qWLgoKCVKVKFT399NOFPsblx2rTpo0qV67saOvSpYtycnKcrhVr0aKFPD09HevBwcHMPcWEYIE8Xf6ilaS///3vWrFihaZNm6bPPvtMycnJatWqlS5evFjgfipUqOC0brPZlJOTY3m9AK5u9OjR+vDDD3Xu3DnFxsaqYcOGioiI0EsvvaTZs2friSee0KZNm5ScnKzIyMirvr5dkZSUpKFDh6pPnz5atWqV9uzZo6eeesrSY1yOuafkECxQKFu3btXIkSN1xx13qFWrVgoKCtJPP/1U0mUBcMFdd90lDw8PxcfH65133tF9990nm82mrVu3asCAAbr33nvVpk0bNWjQQAcOHCj0fps1a6Zjx4453XG2fft2pz7btm1TaGionnrqKXXo0EGNGzfWkSNHnPp4e3srOzv7qsf66quvdP78eUfb1q1b5eHhoSZNmhS6ZrgPwQKF0rhxYy1fvlzJycn66quvdM8995D+gTKmSpUqGjx4sKKjo5WSkqKRI0dK+u/rOyEhQdu2bdN3332nv/71r0pLSyv0fnv06KEbb7xRI0aM0FdffaXPPvtMTz31lFOfxo0b6+jRo1q8eLEOHTqkf/3rX1qxYoVTn3r16unw4cNKTk7WyZMnlZWVletYQ4cOlY+Pj0aMGKF9+/Zp06ZNevjhhzVs2DAFBga6/kOB5QgWKJRXXnlF1apVU+fOndW/f39FRkaqffv2JV0WABeNHj1aZ86cUWRkpOOaiKefflrt27dXZGSkunXrpqCgIA0cOLDQ+/Tw8NCKFSv066+/6uabb9aYMWP0wgsvOPX585//rEcffVTjxo1T27ZttW3bNj3zzDNOfe6880716tVLt956q2rWrJnnLa+VKlXS+vXrdfr0ad100036v//7P3Xv3l1z5sxx/YcBt7AZc9m9RwAAANeAMxYAAMAyBAsAAGAZggUAALAMwQIAAFiGYAEAACxDsAAAAJYhWAAAAMsQLAAAgGUIFgAAwDIECwAAYBmCBQAAsMz/A1QGVy/bRSEsAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "id": "TwAsJhDMARAw", + "outputId": "8438e12f-1aae-4b70-96b1-91510f8ee1b6" + }, + "outputs": [], "source": [ - "# We can also look at different attributes of the images \n", + "# We can also look at different attributes of the images\n", "# Here, let's look at max values and mean values\n", "\n", - "# These numbers will be slightly different every time, there's randomness controlling how the images are generated \n", + "# These numbers will be slightly different every time, there's randomness controlling how the images are generated\n", "# In scientific settings, it's good to remove the randomness if possible, but here it's fine\n", "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().mean() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].mean() for i in range(len(val_generator))\n", - "]).ravel()\n", - "\n", - "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", - "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", - "\n", - "figure.suptitle(\"Mean value of pixels in a single image\")\n", - "plt.show()\n", + "# Normally we would also have some metadata to work with, but here we only have images\n", "\n", + "train_means = train_generator.dataset.images.mean(axis=(1, 0))\n", "\n", - "all_train_labels = np.array([\n", - " train_generator[i][0].ravel().max() for i in range(len(train_generator))\n", - "]).ravel()\n", - "\n", - "all_val_labels = np.array([\n", - " val_generator[i][0].ravel().max() for i in range(len(val_generator))\n", - "]).ravel()\n", + "val_means = val_generator.dataset.images.mean(axis=(1, 0))\n", "\n", "figure, subplots = plt.subplots(1, 2)\n", - "subplots[0].hist(all_train_labels)\n", - "subplots[0].set_xlabel(\"Train\")\n", + "subplots[0].hist(train_means, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[0].hist(val_means, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[0].legend()\n", + "subplots[0].set_xlabel(\"Mean pixel value\")\n", + "\n", + "train_maxes = train_generator.dataset.images.max(axis=(1, 0))\n", + "val_maxes = val_generator.dataset.images.max(axis=(1, 0))\n", + "subplots[1].hist(train_maxes, color=\"orange\", edgecolor=\"black\", label=\"Train\", alpha=0.4)\n", + "subplots[1].hist(val_maxes, color=\"blue\", edgecolor=\"black\", label=\"Validation\", alpha=0.4)\n", + "subplots[1].set_xlabel(\"Max pixel value\")\n", "\n", - "subplots[1].hist(all_val_labels)\n", - "subplots[1].set_xlabel(\"Validation\")\n", "\n", - "figure.suptitle(\"Max value of pixels in a single image\")\n", "plt.show()" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "NhRpPEjdARAw" + }, "source": [ "## Data Processing\n", "\n", "There are many ways to help a model along when it comes to training - one of those is data pre-processing.\n", - "There are near infinite ways to pre-process, especially in the computer vision space. \n", - "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways. \n", + "There are near infinite ways to pre-process, especially in the computer vision space.\n", + "Think about it like applying different instagram filters, they have different impacts that emphasis the image in different ways.\n", "\n", - "For this, let's try bringing all the pixels in the image between 0 and 1. " + "For this, let's try bringing all the pixels in the image between 0 and 1." ] }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, + "execution_count": null, + "metadata": { + "id": "Wp_k5XTiARAw" + }, "outputs": [], "source": [ - "scaler = MinMaxScaler((0, 1))\n", - "\n", - "fit_data = np.concatenate([train_generator[i][0] for i in range(5)], axis=0) \n", - "# This is 5 batches of data, being used to find the approximate min and max of the data\n", - "# Because our data is synthetic, we don't need to worry about big outliers\n", - "\n", - "# Unfortunately, minmaxscaler only handles 1d of data, so we need to do a little pre-and-post processing on the input \n", - "# combining the last two dimensions together, making the 2d of the image 1d instead. \n", - "fit_data = fit_data.reshape((fit_data.shape[0], fit_data.shape[1]*fit_data.shape[2]) ) \n", - "scaler_fit = scaler.fit(fit_data)\n", - "\n", - "def processor(image): \n", - " # This function will take a single image and return the scaled version \n", - " image_flat = image.ravel() \n", - " image_scaled = scaler_fit.transform(image_flat.reshape(1, -1))\n", - " # Magically reshape it back into 2d \n", - " image_scaled_reshaped = image_scaled.reshape(image.shape)\n", - " return image_scaled_reshaped" + "transforms = torchvision.transforms.Compose([\n", + " torchvision.transforms.Resize((64, 64)), # Resize the images to 64 x 64 pixels\n", + " torchvision.transforms.Normalize(0.5, 0.5) # Normalize the pixel values to be between 0 and 1\n", + "])\n", + "\n", + "# We can apply other transforms, in a process called \"data augmentation\"\n", + "# It helps make the model less likely to overfit by giving it different versions of the same data during the training process\n", + "# All transforms can be seen here - https://docs.pytorch.org/vision/0.26/transforms.html#v2-api-reference-recommended" ] }, { "cell_type": "code", - "execution_count": 11, - "metadata": {}, + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "6gXn05XNARAx", + "outputId": "76322868-6110-4b6f-8dd4-7b8a9acca378" + }, "outputs": [], "source": [ - "train_generator_scaled = SkyGenerator(n_samples=n_train_samples, pre_processing=processor, batch_size=64)\n", - "val_generator_scaled = SkyGenerator(n_samples=n_val_samples, pre_processing=processor, batch_size=64)" + "# Look at some processed samples!\n", + "\n", + "n_samples = 9\n", + "samples = SkyGenerator(n_samples=n_samples, batch_size=1, dataset=SkyDataset).get_dataloader()\n", + "for image, label in samples:\n", + " image = transforms(image)\n", + " plt.imshow(image[0][0]) # The image size is given as (batch, im channels, im_size, im_size)\n", + " plt.xlabel(label_map[label.item()])\n", + " plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HJEKKWHpARAx" + }, + "source": [ + "## Make the binary classification model\n", + "\n", + "This function, when called, produces a keras Model instance that you can train to predict a class of an input.\n", + "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1.\n", + "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out.\n", + "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it.\n", + "\n", + "In this case, what we want:\n", + "* Take the input images from the data generator\n", + "* Apply two convolutional blocks to the input image\n", + "* Decode the second convolution block's output to a probability of the image being a given class." ] }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "yb6L7SDoARAx", + "outputId": "7765d938-5f7b-4e41-8ac3-a35aa79663f4" + }, + "outputs": [], "source": [ - "# Look at some processed samples! \n", + "# Create a simple convolusional model\n", "\n", - "n_samples = 9\n", - "samples = SkyGenerator(n_samples=n_samples, batch_size=1, pre_processing=processor)\n", - "plot_samples(samples)" + "class SimpleCNN(torch.nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=3, padding=1)\n", + " self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=3, padding=1)\n", + " self.conv3 = torch.nn.Conv2d(32, 64, kernel_size=3, padding=1)\n", + "\n", + " # Pooling layer\n", + " self.pool = torch.nn.MaxPool2d(2, 2)\n", + "\n", + " # Fully connected layers\n", + " # After 3 pooling layers: 64 -> 32 -> 16 -> 8, so feature map is 8x8\n", + " # 64 channels * 8x8 = 4096 input features\n", + " self.fc1 = torch.nn.Linear(64 * 8 * 8, 128)\n", + " self.fc2 = torch.nn.Linear(128, 1)\n", + "\n", + " # Dropout\n", + " self.dropout = torch.nn.Dropout(0.3)\n", + "\n", + " # Activation\n", + " self.relu = torch.nn.ReLU()\n", + "\n", + " def forward(self, x):\n", + " # Input: (batch_size, 64, 64) - need to add channel dimension\n", + " if x.dim() == 3:\n", + " x = x.unsqueeze(1) # Now (batch_size, 1, 64, 64)\n", + "\n", + " # Conv block 1: (batch_size, 1, 64, 64) -> (batch_size, 16, 32, 32)\n", + " x = self.pool(self.relu(self.conv1(x)))\n", + "\n", + " # Conv block 2: (batch_size, 16, 32, 32) -> (batch_size, 32, 16, 16)\n", + " x = self.pool(self.relu(self.conv2(x)))\n", + "\n", + " # Conv block 3: (batch_size, 32, 16, 16) -> (batch_size, 64, 8, 8)\n", + " x = self.pool(self.relu(self.conv3(x)))\n", + "\n", + " # Flatten: (batch_size, 64*8*8) = (batch_size, 4096)\n", + " x = x.view(x.size(0), -1)\n", + "\n", + " # Fully connected layers\n", + " x = self.relu(self.fc1(x))\n", + " x = self.dropout(x)\n", + " x = self.fc2(x) # Output: (batch_size, 1)\n", + "\n", + " return torch.sigmoid(x.squeeze()) # Output of (batch_size)\n", + "\n", + "print(summary(SimpleCNN(), input_size=(1, 64, 64)))" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "NqOS_59-ARAy" + }, "source": [ - "## Make the binary classification model \n", + "## Training process\n", + "\n", + "Based on the previous lessons, we know we need a few things to train a model\n", + "\n", + "### Loss and Optimizer\n", "\n", - "This function, when called, produces a keras Model instance that you can train to predict a class of an input. \n", - "Because this is a binary predictor, it can be used to pick if an image is closer to being class 0 or class 1. \n", - "It takes an input of a certain shape, (defined by the `in_layer`), fits it to a convolution operation, and gives you a number (or array!) back out. \n", - "The way this becomes a predictive engine is through the loss, of the output of the model will minimize a loss function, and give us a prediction that matches the data we fed it. \n", "\n", - "In this case, what we want: \n", - "* Take the input images from the data generator \n", - "* Apply two convolutional blocks to the input image \n", - "* Decode the second convolution block's output to a probability of the image being a given class. " + "### Training and Validation data loops\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Model: \"functional_1\"\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[1mModel: \"functional_1\"\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n",
-       "┃ Layer (type)                     Output Shape                  Param # ┃\n",
-       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n",
-       "│ input_layer (InputLayer)        │ (None, 28, 28)         │             0 │\n",
-       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
-       "│ conv1d_3 (Conv1D)               │ (None, 27, 4)          │           228 │\n",
-       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
-       "│ conv1d_4 (Conv1D)               │ (None, 24, 8)          │           136 │\n",
-       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
-       "│ conv1d_5 (Conv1D)               │ (None, 19, 12)         │           588 │\n",
-       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
-       "│ average_pooling1d_1             │ (None, 3, 12)          │             0 │\n",
-       "│ (AveragePooling1D)              │                        │               │\n",
-       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
-       "│ flatten (Flatten)               │ (None, 36)             │             0 │\n",
-       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
-       "│ dense (Dense)                   │ (None, 20)             │           740 │\n",
-       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
-       "│ dropout (Dropout)               │ (None, 20)             │             0 │\n",
-       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
-       "│ dense_1 (Dense)                 │ (None, 1)              │            21 │\n",
-       "└─────────────────────────────────┴────────────────────────┴───────────────┘\n",
-       "
\n" - ], - "text/plain": [ - "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n", - "┃\u001b[1m \u001b[0m\u001b[1mLayer (type) \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m Param #\u001b[0m\u001b[1m \u001b[0m┃\n", - "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n", - "│ input_layer (\u001b[38;5;33mInputLayer\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m28\u001b[0m, \u001b[38;5;34m28\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", - "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", - "│ conv1d_3 (\u001b[38;5;33mConv1D\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m27\u001b[0m, \u001b[38;5;34m4\u001b[0m) │ \u001b[38;5;34m228\u001b[0m │\n", - "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", - "│ conv1d_4 (\u001b[38;5;33mConv1D\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m24\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m136\u001b[0m │\n", - "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", - "│ conv1d_5 (\u001b[38;5;33mConv1D\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m19\u001b[0m, \u001b[38;5;34m12\u001b[0m) │ \u001b[38;5;34m588\u001b[0m │\n", - "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", - "│ average_pooling1d_1 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m3\u001b[0m, \u001b[38;5;34m12\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", - "│ (\u001b[38;5;33mAveragePooling1D\u001b[0m) │ │ │\n", - "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", - "│ flatten (\u001b[38;5;33mFlatten\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m36\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", - "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", - "│ dense (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m20\u001b[0m) │ \u001b[38;5;34m740\u001b[0m │\n", - "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", - "│ dropout (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m20\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", - "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", - "│ dense_1 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m21\u001b[0m │\n", - "└─────────────────────────────────┴────────────────────────┴───────────────┘\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
 Total params: 1,713 (6.69 KB)\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m1,713\u001b[0m (6.69 KB)\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
 Trainable params: 1,713 (6.69 KB)\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m1,713\u001b[0m (6.69 KB)\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
 Non-trainable params: 0 (0.00 B)\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m0\u001b[0m (0.00 B)\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "id": "CPJDwFxSARAy" + }, + "outputs": [], "source": [ - "def make_model(): \n", - " \n", - " # Define the input size\n", - " in_layer = Input((28, 28))\n", - "\n", - " # Perform some convolutions \n", - " x = Conv1D(filters=4, kernel_size=2)(in_layer)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - " \n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Conv1D(filters=4, kernel_size=2)(in_layer)\n", - " x = Conv1D(filters=8, kernel_size=4)(x)\n", - " x = Conv1D(filters=12, kernel_size=6)(x)\n", - " \n", - " x = AvgPool1D(6)(x)\n", - "\n", - " x = Flatten()(x)\n", - " # Decode the convolutional output into an array\n", - " x = Dense(20, activation='relu')(x)\n", - "\n", - " x = Dropout(0.3)(x)\n", - "\n", - " # Get the output class probabilities \n", - " output = Dense(1, activation='sigmoid')(x)\n", - " model = Model(in_layer, output)\n", - " \n", - " return model\n", - "\n", - "model = make_model()\n", - "loss = tf.losses.BinaryCrossentropy()\n", - "optimizer = tf.keras.optimizers.SGD(0.01)\n", - "\n", - "# Compile tells the keras backend what loss and optimizer to use to perform gradients on the model\n", - "# You cannot train a keras model without compiling it first\n", - "model.compile(loss=loss, optimizer=optimizer)\n", - "\n", - "# Show what layers are in the model, and their input and output shapes \n", - "# This can help make sure all your stuff is a correct size\n", - "model.summary()" + "# Initialize the model once\n", + "model = SimpleCNN()\n", + "\n", + "# Random choices of optimizer and loss functions\n", + "# These are pretty standard for classification\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)\n", + "criterion = torch.nn.BCELoss() # BCE = Binary Cross Entropy (for 2 classes)" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "6XyF3QGjARAy" + }, "source": [ - "# Train the model \n", + "# Train the model\n", "\n", "We have all the pieces in place:\n", - "- [x] Model \n", - "- [x] Train Data \n", - "- [x] Validation Data \n", - "- [x] Loss Function \n", - "- [x] Optimizer \n", - "\n", - "Now lets put this together into a fit model. \n", - "Keras trains in place, so you don't need a new variable to hold the `fit_model` vs `model`. \n", - "Once you call `fit`, the model is fit, and it re-train, you need to make a new model with the `make_model()` function. " + "- [x] Model\n", + "- [x] Train Data\n", + "- [x] Validation Data\n", + "- [x] Loss Function\n", + "- [x] Optimizer" ] }, { "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1/50\n", - "\u001b[1m 1/20\u001b[0m \u001b[32m━\u001b[0m\u001b[37m━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[1m6s\u001b[0m 344ms/step - loss: 0.7196" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/maggiev-local/miniforge3/envs/ss_tutorials/lib/python3.12/site-packages/keras/src/trainers/data_adapters/py_dataset_adapter.py:121: UserWarning: Your `PyDataset` class should call `super().__init__(**kwargs)` in its constructor. `**kwargs` can include `workers`, `use_multiprocessing`, `max_queue_size`. Do not pass these arguments to `fit()`, as they will be ignored.\n", - " self._warn_if_super_not_called()\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m13/20\u001b[0m \u001b[32m━━━━━━━━━━━━━\u001b[0m\u001b[37m━━━━━━━\u001b[0m \u001b[1m0s\u001b[0m 23ms/step - loss: 0.7027" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/maggiev-local/miniforge3/envs/ss_tutorials/lib/python3.12/site-packages/keras/src/trainers/data_adapters/py_dataset_adapter.py:121: UserWarning: Your `PyDataset` class should call `super().__init__(**kwargs)` in its constructor. `**kwargs` can include `workers`, `use_multiprocessing`, `max_queue_size`. Do not pass these arguments to `fit()`, as they will be ignored.\n", - " self._warn_if_super_not_called()\n" - ] + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 42ms/step - loss: 0.7016 - val_loss: 0.6924\n", - "Epoch 2/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 39ms/step - loss: 0.6935 - val_loss: 0.6898\n", - "Epoch 3/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.6845 - val_loss: 0.6854\n", - "Epoch 4/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 39ms/step - loss: 0.6825 - val_loss: 0.6820\n", - "Epoch 5/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 38ms/step - loss: 0.6904 - val_loss: 0.6772\n", - "Epoch 6/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.6820 - val_loss: 0.6696\n", - "Epoch 7/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.6686 - val_loss: 0.6584\n", - "Epoch 8/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.6683 - val_loss: 0.6519\n", - "Epoch 9/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.6617 - val_loss: 0.6364\n", - "Epoch 10/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 39ms/step - loss: 0.6537 - val_loss: 0.6176\n", - "Epoch 11/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.6292 - val_loss: 0.5954\n", - "Epoch 12/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.5707 - val_loss: 0.5651\n", - "Epoch 13/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.5833 - val_loss: 0.5337\n", - "Epoch 14/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.5388 - val_loss: 0.5104\n", - "Epoch 15/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.5583 - val_loss: 0.4788\n", - "Epoch 16/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.4495 - val_loss: 0.4471\n", - "Epoch 17/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.4636 - val_loss: 0.4222\n", - "Epoch 18/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.4695 - val_loss: 0.3941\n", - "Epoch 19/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.4557 - val_loss: 0.3754\n", - "Epoch 20/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.3648 - val_loss: 0.3461\n", - "Epoch 21/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.3671 - val_loss: 0.3200\n", - "Epoch 22/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.3570 - val_loss: 0.3101\n", - "Epoch 23/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.3379 - val_loss: 0.3009\n", - "Epoch 24/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.3183 - val_loss: 0.3250\n", - "Epoch 25/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 34ms/step - loss: 0.3330 - val_loss: 0.3016\n", - "Epoch 26/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.2995 - val_loss: 0.2629\n", - "Epoch 27/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.3274 - val_loss: 0.2719\n", - "Epoch 28/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 34ms/step - loss: 0.2903 - val_loss: 0.2680\n", - "Epoch 29/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 34ms/step - loss: 0.2717 - val_loss: 0.2781\n", - "Epoch 30/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.2965 - val_loss: 0.2485\n", - "Epoch 31/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.2596 - val_loss: 0.2503\n", - "Epoch 32/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.2431 - val_loss: 0.2375\n", - "Epoch 33/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.2730 - val_loss: 0.2270\n", - "Epoch 34/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.2812 - val_loss: 0.2255\n", - "Epoch 35/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.2572 - val_loss: 0.2446\n", - "Epoch 36/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.2892 - val_loss: 0.2085\n", - "Epoch 37/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.2782 - val_loss: 0.2195\n", - "Epoch 38/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 40ms/step - loss: 0.3044 - val_loss: 0.2136\n", - "Epoch 39/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.2722 - val_loss: 0.2084\n", - "Epoch 40/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 39ms/step - loss: 0.2731 - val_loss: 0.2218\n", - "Epoch 41/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.2242 - val_loss: 0.2198\n", - "Epoch 42/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.2543 - val_loss: 0.2182\n", - "Epoch 43/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.2482 - val_loss: 0.2029\n", - "Epoch 44/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 34ms/step - loss: 0.3031 - val_loss: 0.2136\n", - "Epoch 45/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.2334 - val_loss: 0.2099\n", - "Epoch 46/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.2446 - val_loss: 0.2319\n", - "Epoch 47/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 37ms/step - loss: 0.2239 - val_loss: 0.2520\n", - "Epoch 48/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.2825 - val_loss: 0.1996\n", - "Epoch 49/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 35ms/step - loss: 0.2069 - val_loss: 0.2404\n", - "Epoch 50/50\n", - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 36ms/step - loss: 0.2737 - val_loss: 0.2116\n" - ] - } - ], + "id": "adEJ4vhuARAy", + "outputId": "7856e742-8fb6-4956-bd34-61eab42495b0" + }, + "outputs": [], "source": [ - "history = model.fit(\n", - " train_generator_scaled, \n", - " validation_data=val_generator_scaled, \n", - " epochs=50, \n", - " verbose=1\n", - " ).history\n" + "\n", + "def train_epoch(model, dataloader, optimizer, criterion):\n", + " \"\"\"Trains the model for a single epoch (one loop through the entire training dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Training...\")\n", + " for images, labels in iterer:\n", + " optimizer.zero_grad() # Clear the gradients from the previous step\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " loss.backward() # Backward pass: compute the gradients of the loss with respect to the model parameters\n", + " optimizer.step() # Update the model parameters using the computed gradients\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "\n", + "def val_epoch(model, dataloader, criterion):\n", + " \"\"\"Evaluates the model on the validation set for a single epoch (one loop through the entire validation dataset)\"\"\"\n", + " batch_loss = torch.tensor(0.0) # Initialize the batch loss to 0\n", + " n_batches = len(dataloader)\n", + " iterer = tqdm(dataloader, desc=\"Validating...\")\n", + " with torch.no_grad(): # Disable gradient computation for validation\n", + " for images, labels in iterer:\n", + " outputs = model(transforms(images)) # Forward pass: compute the model output for the current batch of images\n", + " loss = criterion(outputs.to(float), labels.to(float)) # Compute the loss between the model output and the true labels\n", + " batch_loss += loss.item() # Accumulate the loss for this batch\n", + " iterer.set_postfix(loss=loss.item())\n", + " return model, batch_loss.item() / n_batches # Return the average loss for this epoch\n", + "\n", + "# One epoch should take around 15 seconds\n", + "# The number of epochs is depednent on the complexity and size of your data\n", + "n_epochs = 30\n", + "loss_history = {\"train\": [], \"val\": []}\n", + "for epoch in range(n_epochs):\n", + " model, train_loss = train_epoch(model, train_generator.get_dataloader(), optimizer, criterion)\n", + " model, val_loss = val_epoch(model, val_generator.get_dataloader(), criterion)\n", + "\n", + " loss_history[\"train\"].append(train_loss)\n", + " loss_history[\"val\"].append(val_loss)\n" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "3QCcxf0dARAy" + }, "source": [ - "## Model Evaluation \n", + "## Model Evaluation\n", "\n", - "There are some steps we can take to see how well a model trained. \n", + "There are some steps we can take to see how well a model trained.\n", "\n", - "### Loss Plots \n", - "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training. \n", - "Or, if the loss never moves or blows up entirely, that's a sign there's a problem. \n", - "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems! " + "### Loss Plots\n", + "Obvious one is to see how the loss progressed - if the loss was still trending down when the training stopped, it would make sense that the model would benefit from longer training.\n", + "Or, if the loss never moves or blows up entirely, that's a sign there's a problem.\n", + "Looking at the [common pitfalls notebook](https://github.com/BNL-Fermilab-RENEW/tutorials_2024/blob/main/07_Challenge/common_pitfalls.ipynb) may help diagnose your problems!" ] }, { "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 472 + }, + "id": "pfDIpqHGARAy", + "outputId": "a9aa0f30-8e10-47d6-f4c2-d5b5682bb392" + }, + "outputs": [], "source": [ - "Eval.plot_loss_history(history) \n", - "# Eval.plot_history is a simple function \n", - "# plots the loss as a function of epoch " + "Eval.plot_loss_history(loss_history['train'], loss_history['val'])\n", + "# Eval.plot_history is a simple function\n", + "# plots the loss as a function of epoch" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "id": "V_9R4WYqARAy" + }, "source": [ - "## Classification Accuracy Plots \n", + "## Classification Accuracy Plots\n", "\n", - "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training. \n", - "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it. \n", + "After we did all this work to train a model, we need to be able to report how good it is on data we didn't use in training.\n", + "For this, we'll make a new set of data (or use some data we held out from training), and run a few evaluation metrics on it.\n", "\n", "### ROC\n", - "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate. \n", - "It shows how likely a model is to correctly predict something. \n", - "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves. \n", - "\n", - "\n", - "\n", - "Roc-draft-xkcd-style.svg, CC BY-SA 4.0, Link\n", + "The `receiver operating characteristic curve` (or just \"ROC\" (pronounced \"Rock\") Curve) is a metric that plots the true positive rate against the false positive rate.\n", + "It shows how likely a model is to correctly predict something.\n", + "The idea is that a classifier a better classifier will have lower false positive rate, and a higher true positive rate, so the curve will get closer and closer to the upper left corner as the prediction improves.\n", "\n", "\n", "### Confusion Matrix\n", "\n", - "Confusion matrices are a great tool for seeing how well each class does against each other. \n", - "It gets it name from its ability to tell if a model is \"confusing\" two different classes. \n", - "It plots the rate of predicted values for a given class versus the true values. \n", - "\n", - "\n", - "\n", - " Link\n", + "Confusion matrices are a great tool for seeing how well each class does against each other.\n", + "It gets it name from its ability to tell if a model is \"confusing\" two different classes.\n", + "It plots the rate of predicted values for a given class versus the true values.\n", "\n", - " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes. " + " A good confusion matrix will have very high values in the green boxes, and lower values in the red boxes." ] }, { "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m 4/20\u001b[0m \u001b[32m━━━━\u001b[0m\u001b[37m━━━━━━━━━━━━━━━━\u001b[0m \u001b[1m0s\u001b[0m 17ms/step" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/maggiev-local/miniforge3/envs/ss_tutorials/lib/python3.12/site-packages/keras/src/trainers/data_adapters/py_dataset_adapter.py:121: UserWarning: Your `PyDataset` class should call `super().__init__(**kwargs)` in its constructor. `**kwargs` can include `workers`, `use_multiprocessing`, `max_queue_size`. Do not pass these arguments to `fit()`, as they will be ignored.\n", - " self._warn_if_super_not_called()\n" - ] + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1m20/20\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 22ms/step\n", - "The binary classification accuracy on the test set is: 0.891406238079071\n" - ] - } - ], + "id": "iP_xgNcMARAy", + "outputId": "62e30e98-f5f6-4b42-ab56-bd876d6d7e2d" + }, + "outputs": [], "source": [ - "test_generator = SkyGenerator(n_samples=1280, train=False, shuffle=True, pre_processing=processor)\n", - "\n", - "def make_prediction(test_generator): \n", - " predictions = model.predict(test_generator)\n", - " prediction_classes = np.where(predictions<0.5, 0, 1)\n", - " labels = test_generator.labels\n", + "test_generator = SkyGenerator(n_samples=1280, dataset=TestDataset, shuffle=True).get_dataloader()\n", + "\n", + "def make_prediction(model, test_generator):\n", + " predictions = []\n", + " labels = []\n", + " for image, label in test_generator:\n", + " predictions += model(transforms(image))\n", + " labels += label\n", + " prediction_classes = torch.where(torch.tensor(predictions)<0.5, 0, 1)\n", + " labels = torch.tensor(labels)\n", " return prediction_classes, labels\n", "\n", - "def test_quality(prediction, labels): \n", - " accuracy = tf.keras.metrics.BinaryAccuracy()(prediction, labels)\n", + "def test_quality(prediction, labels):\n", + " accuracy = torch.mean((prediction == labels).float()) # Compute the accuracy by comparing the predicted classes to the true labels\n", " return accuracy.numpy()\n", "\n", - "prediction, labels = make_prediction(test_generator)\n", + "prediction, labels = make_prediction(model, test_generator)\n", "print(f\"The binary classification accuracy on the test set is: {test_quality(prediction, labels)}\")" ] }, { "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHHCAYAAABDUnkqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAACAg0lEQVR4nO3dd1hT1xsH8G/CCBtEFBy4t3XvXS3u+XPhqOKodddK3Qu3trZWax1174Fara17r1oXYq0bxQ0oDjYkJOf3hyYaASWY5EL4fp6HR3Nz782bYzSv55z3HJkQQoCIiIjIQsilDoCIiIjImJjcEBERkUVhckNEREQWhckNERERWRQmN0RERGRRmNwQERGRRWFyQ0RERBaFyQ0RERFZFCY3REREZFGY3BAREZFFYXJDZGFWr14NmUym+7G2tka+fPnQq1cvPH78ONVrhBBYt24d6tevDzc3Nzg4OKBcuXKYOnUq4uLi0nytHTt2oHnz5vDw8ICtrS3y5s2Lzp0748iRI+mO99WrV7Czs4NMJsP169dTPefzzz/HZ599lupzkZGRkMlkmDx5corn7ty5g/79+6NIkSKws7ODi4sL6tSpg/nz5yMhISFd8R07dgzt27eHl5cXbG1tkTt3brRu3Rq///57ut8jEZmXtdQBEJFpTJ06FYULF0ZiYiL++ecfrF69GqdOncJ///0HOzs73XlqtRrdunVDYGAg6tWrh8mTJ8PBwQEnT57ElClTsHXrVhw6dAienp66a4QQ6NOnD1avXo1KlSrB398fXl5eCAsLw44dO/DFF1/g9OnTqF279kfj3Lp1K2QyGby8vLBhwwZMnz7dKO9/9+7d6NSpExQKBXr27InPPvsMSqUSp06dwsiRI3H16lUsXbr0g/cICAjA1KlTUbx4cfTv3x8FCxbE8+fPsWfPHnTo0AEbNmxAt27djBIvERmRICKLsmrVKgFAnD9/Xu/46NGjBQCxZcsWveMzZ84UAMSIESNS3GvXrl1CLpeLZs2a6R2fM2eOACC+/fZbodFoUly3du1acfbs2XTFW79+fdG+fXsxfPhwUbhw4VTPadCggShbtmyqzz179kwAEAEBAbpjd+/eFU5OTqJUqVLiyZMnKa65ffu2mDdv3gfj2rp1qwAgOnbsKJRKZYrn9+3bJ/78888P3iO94uLijHIfInqNyQ2RhUkrufnrr78EADFz5kzdsfj4eJEjRw5RokQJoVKpUr1f7969BQBx5swZ3TXu7u6iVKlSIjk5+ZNivX//vpDJZCIwMFCcPXtWABCnT59OcZ6hyc2AAQPSvFd6lSpVSri7u4vo6OiPnqtt89DQUL3jR48eFQDE0aNHdce07+XChQuiXr16wt7eXgwbNky0bNkyzeSuZs2aokqVKnrH1q1bJypXrizs7OxEjhw5hK+vr3jw4IHB75PIEnHODVE2ce/ePQBAjhw5dMdOnTqFly9folu3brC2Tn2UumfPngCAv/76S3fNixcv0K1bN1hZWX1STJs2bYKjoyNatWqF6tWro2jRotiwYcMn3RMA/vzzTxQpUiRdw2KpuX37Nm7cuIF27drB2dn5k+N53/Pnz9G8eXNUrFgR8+bNQ8OGDeHr64vQ0FCcP39e79z79+/jn3/+QZcuXXTHZsyYgZ49e6J48eKYO3cuvv32Wxw+fBj169fHq1evjB4vUVbDOTdEFioqKgqRkZFITEzE2bNnMWXKFCgUCrRq1Up3zrVr1wAAFSpUSPM+2ue0k321v5YrV+6TY9ywYQPatm0Le3t7AICvry+WLl2K+fPnp5lsfUx0dDQeP36Mtm3bZjguY77H1ISHh2PJkiXo37+/7lh0dDQUCgW2bNmCatWq6Y4HBgZCJpOhc+fOAF4nOwEBAZg+fTrGjRunO699+/aoVKkSFi1apHecKDtizw2RhfLx8UGuXLng7e2Njh07wtHREbt27UL+/Pl158TExADAB3sntM9FR0fr/fqpPRr//vsvrly5gq5du+qOde3aFZGRkdi/f3+G72uM+Iz1HtOiUCjQu3dvvWMuLi5o3rw5AgMDIYTQHd+yZQtq1qyJAgUKAAB+//13aDQadO7cGZGRkbofLy8vFC9eHEePHjVJzERZCXtuiCzUwoULUaJECURFRWHlypU4ceIEFAqF3jnaL29tkpOa9xMgFxeXj16THuvXr4ejoyOKFCmCkJAQAICdnR0KFSqEDRs2oGXLlgbdTyaTGS0+Y73HtOTLlw+2trYpjvv6+mLnzp04c+YMateujTt37uDixYuYN2+e7pzbt29DCIHixYunem8bGxuTxEyUlTC5IbJQ1atXR9WqVQEA7dq1Q926ddGtWzfcvHkTTk5OAIDSpUsDeN2L0q5du1Tv8++//wIAypQpAwAoVaoUAODKlStpXvMxQghs2rQJcXFxuvu+6+nTp4iNjdXFaWdnl+a6NPHx8bpzgNeJSd68efHff/9lKDZA/z2mhzaxep9arU71uHYY7n2tW7eGg4MDAgMDUbt2bQQGBkIul6NTp066czQaDWQyGfbu3ZvqnCdtmxFlZxyWIsoGrKysMGvWLDx58gS//vqr7njdunXh5uaGjRs3pvlFvHbtWgDQzdWpW7cucuTIgU2bNqV5zcccP34cjx49wtSpU7F161a9n6VLlyI+Ph47d+7UnV+wYEE8fPgw1QTn5s2bunO0WrVqhTt37uDMmTMZiq9EiRIoWbIk/vjjD8TGxn70fO0k7fcn896/f9+g19VOrt66dSs0Gg22bNmCevXqIW/evLpzihYtCiEEChcuDB8fnxQ/NWvWNOg1iSyStMVaRGRsaZWCCyFE9erVhaenp0hISNAdmz59ugAgRo8eneL8v/76S8jlctG0aVO947NnzxYAxHfffZfqOjfr1q374Do3ffv2FY6OjnpxvKt48eJ6a+vs3LlTABA///yz3nlqtVr873//E7a2tuLp06e64yEhIcLR0VGUKVNGhIeHp7h/SEjIR9e52bx5swAgfH19Uy2T379/v26dm//++08AEPPnz9c9n5ycLGrUqJFmKXhatm/fLgCI3377TQAQixYtShG7lZWV6NatW4q212g0IjIy8oPviyg74LAUUTYycuRIdOrUCatXr8aAAQMAAGPGjMGlS5fw/fff48yZM+jQoQPs7e1x6tQprF+/HqVLl8aaNWtS3Ofq1av46aefcPToUXTs2BFeXl4IDw/Hzp07ce7cOfz999+pxpCUlITt27ejcePGeislv6tNmzaYP38+nj59qtvuoEmTJhg+fDjOnTuH2rVrIz4+Hrt27cLp06cxffp05MqVS3d90aJFsXHjRvj6+qJ06dJ6KxT//fff2Lp1K3r16vXBtvL19cWVK1cwY8YMXLp0CV27dtWtULxv3z4cPnwYGzduBACULVsWNWvWxNixY/HixQu4u7tj8+bNSE5OTu8fjU6LFi3g7OyMESNGwMrKCh06dNB7vmjRopg+fTrGjh2Le/fu6crVQ0NDsWPHDnz99dcYMWKEwa9LZFGkzq6IyLg+1HOjVqtF0aJFRdGiRfUW4FOr1WLVqlWiTp06wsXFRdjZ2YmyZcuKKVOmiNjY2DRfa9u2baJJkybC3d1dWFtbizx58ghfX19x7NixNK/R9kysWLEizXOOHTuWoickMTFRTJ48WZQqVUooFArh6OgoatasKdavX5/mfW7duiX69esnChUqJGxtbYWzs7OoU6eOWLBggUhMTEzzuncdPnxYtG3bVuTOnVtYW1uLXLlyidatW4s//vhD77w7d+4IHx8foVAohKenpxg3bpw4ePCgwT03QgjRvXt3AUD4+Pikec727dtF3bp1haOjo3B0dBSlSpUSgwcPFjdv3kzX+yKyZDIh3qk5JCIiIsriOKGYiIiILAqTGyIiIrIoTG6IiIjIojC5ISIiIovC5IaIiIgsCpMbIiIisijZbhE/jUaDJ0+ewNnZOc39YIiIiChzEUIgJiYGefPmhVz+4b6ZbJfcPHnyBN7e3lKHQURERBnw8OFD5M+f/4PnZLvkxtnZGcDrxnFxcTHqvVUqFQ4cOIAmTZrAxsbGqPemt9jO5sF2Ng+2s/mwrc3DVO0cHR0Nb29v3ff4h2S75EY7FOXi4mKS5MbBwQEuLi78i2NCbGfzYDubB9vZfNjW5mHqdk7PlBJOKCYiIiKLwuSGiIiILAqTGyIiIrIoTG6IiIjIojC5ISIiIovC5IaIiIgsCpMbIiIisihMboiIiMiiMLkhIiIii8LkhoiIiCyKpMnNiRMn0Lp1a+TNmxcymQw7d+786DXHjh1D5cqVoVAoUKxYMaxevdrkcRIREVHWIWlyExcXhwoVKmDhwoXpOj80NBQtW7ZEw4YNERwcjG+//RZfffUV9u/fb+JIiYiIKKuQdOPM5s2bo3nz5uk+f8mSJShcuDB++uknAEDp0qVx6tQp/Pzzz2jatKmpwiQiIqJ0UKk1CI9OxPNEaePIUruCnzlzBj4+PnrHmjZtim+//TbNa5KSkpCUlKR7HB0dDeD1rqUqlcqo8WnvZ+z7kj62s3mwnc2D7Ww+bOuMSVKpERmnRGSsEs/jlHgem4TI2DePY5WIjEvS/f5Vwuu2Le4iRxcTfcemR5ZKbsLDw+Hp6al3zNPTE9HR0UhISIC9vX2Ka2bNmoUpU6akOH7gwAE4ODiYJM6DBw+a5L6kj+1sHmxn82A7mw/bGkhSAzEq7Y/s9a/Kd36v+xVIVMs+ej91fBQgBKwc3SCHgIDx2zk+Pj7d52ap5CYjxo4dC39/f93j6OhoeHt7o0mTJnBxcTHqa6lUKhw8eBCNGzeGjY2NUe9Nb7GdzYPtbB5sZ/Ox5LYWQiA2KVnXoxIZm4Tner0tr49pH8cr1Qbd38ZKhpyOtvBwUiCnky08nGzh4fj69xG3LuGXCd+hWImS2L5zF1zsrHD40CGjt7N25CU9slRy4+XlhYiICL1jERERcHFxSbXXBgAUCgUUCkWK4zY2Nib7cJvy3vQW29k82M7mwXY2n6zS1kIIRCWoEBmbhGcx2uTkzY/eYyWexSZBmawx6P52NnJ4OCl0P7mcbfUev05iFMjlpICLvTVkMv0eHI1Gg1mzZmHypEnQaDTwcHeDTBkHhYsHAOO3syH3ylLJTa1atbBnzx69YwcPHkStWrUkioiIiCj91BqBl/HKFAnKszePn8e9TV6exyVBpRYG3d/R1goeztoE5Z1kxVmBXO89drS1SpGwpFdERAR69OihG3rq2bMnFi5cCCcnp0wxp0nS5CY2NhYhISG6x6GhoQgODoa7uzsKFCiAsWPH4vHjx1i7di0AYMCAAfj1118xatQo9OnTB0eOHEFgYCB2794t1VsgIqJsLlmtwYu4170nkbFKRMYk6fWqvO55ef37F3FJ0BiWr8DFzlqXsOR6N2lJJYmxt7UyzZt8x5EjR9C9e3eEh4fDwcEBixYtgp+fn8lf1xCSJjcXLlxAw4YNdY+1c2P8/PywevVqhIWF4cGDB7rnCxcujN27d2P48OGYP38+8ufPj+XLl7MMnIiIjCopWf3OPJXXPSnP3k1Y3iQwz+OUeBmvhDAwYcnhYKPXi+LxzhCQxzvDQzmdbKGwNn3Ckl7JyckYMmQIwsPDUbZsWQQGBqJMmTJSh5WCpMnN559/DvGBT0Rqqw9//vnnuHTpkgmjIiIiS5SgVON5IhD88BVeJWreJC3681a0x6ITkw26t1wGuDu+TlJypTEs5OFki1xOCrg72sLaKmvufmRtbY1NmzZhyZIl+Omnn0xWdfypstScGyIiIi0hBOKUar1hoGdpDAtFxiQhTqkGYA1cOpeu+1vLZW8SE1vkdFTofp/rnUm32l6WHA62sJJnbP5KZnfgwAHcv38f/fr1AwBUqFABixcvljiqD2NyQ0REmYYQAtEJybpeFL2hoVSqhhJVhlUIWcsEcrvYv9O7oj8M9G7VkKu9TYYn3FqC5ORkBAQEYNasWbC2tkaVKlVQuXJlqcNKFyY3RERkUhqNwKs3Jc2RMUlvJ96+NyykTWaUasMSFgdbK71hoJxObyqD3k1gnGzhZifHicMH0bJl/SxRCi6lR48eoWvXrjh16hQAoG/fvplybk1amNwQEZHB1Brxumz5vZ6U57Epq4aexymhNrBEyFlhrTfRNrVeFu3kWwfb9H2VqVQqZOOOmHTbs2cPevbsiefPn8PZ2RnLly9H586dpQ7LIExuiIgIwOtND7XDQM90vSqpLx73IgMVQm66CqF3EhRnhW7l23eTGTubzFMhlJ2MHz8eM2fOBABUrlwZgYGBKFq0qMRRGY7JDRGRBUtUqVOUL+tVB8Vol+lPwqt4wxZfk8kAdwfbVOeteLwZFtJOvnV3tIWtddasEMpO3N3dAQBDhw7FnDlzUl3hPytgckNElMXEK5PfW3cl5fCQNpmJSTKspNlKLkvRk5LrvSGhnI6vf+/ukHVLmumtuLg4ODo6Ani93lyNGjVQt25diaP6NExuiIgkJgQQk6jCq1fvDQPFvCltfi+JSVAZtumhrZVct09Qaqvb5nrnsZu9DeQWWtJM+pRKJUaNGoX9+/fj/PnzcHJygkwmy/KJDcDkhojIJIQQeBWvwvO4D296+CwmCU+jrZD8z1GD7v+xTQ893qkWcrFLuekhZW93796Fr68vLly4AAD4888/0bVrV4mjMh4mN0RE6fSxTQ/frxpKTneF0OvEw0lhDQ8nW+Q08aaHlL1t374dffr0QXR0NHLkyIE1a9agdevWUodlVExuiChbU2k3PXxnvZXnZtz00M1Ojivn/0bHVk3h4mhnmjdJBCAxMREjRozAwoULAQC1a9fGpk2bUKBAAYkjMz4mN0RkcdK76WFkbBJeGlghBADujrZ6C8Z9yqaHKpUKT67ALLs5U/Y2cuRIXWIzevRoTJs2zWIXM2RyQ0RZQoJSner6K8+56SFRuowfPx7Hjh3DnDlz0KxZM6nDMSkmN0QkiYxteph+NlYyXclyiom22WjTQ8q+EhISsGPHDnTr1g0A4OXlhcuXL0Mut/zknMkNERnN+5seRr7Xy6JNYLRzWgzd9NDWWq4/b4WbHhKl6saNG+jcuTOuXLkCa2tr3fYJ2SGxAZjcENFHaDQCL+Le9qCYetPDt5NvtVVDb8uanRUsaSb6mLVr12LgwIGIj49H7ty5dasOZydMboiyoWS1Bi/ilWmuavvsTYXQk+dW8D97KFNsekhEHxYXF4ehQ4di1apVAIBGjRph/fr1yJMnj8SRmR//VSGyEMpkDZ7HvbMrs1E2PZQBeH1iWpseplY1xE0Piczr6tWr6Ny5M65duwa5XI6AgACMHz8eVlbZ8+8ikxuiTCw9mx5qH0clGHfTwxz2VrgRfB5tmzWCp6sjNz0kysTu3LmDa9euIU+ePNi4cSM+//xzqUOSFJMbIjOLS0p+Z+n9t3NVzLXpofYnh4PNB0uaVSoV4kIALxc72DCxIcp0hBC6OWht2rTB8uXL0bp1a+TOnVviyKTH5IboEwkhEJOUnHIYyIibHqZYkp+bHhJla5cvX8agQYOwefNmeHt7AwD69u0rcVSZB5MbolRoNz3ULRr3zrCQfi/L6+EhZbJhFULc9JCIMkIIgaVLl2LYsGFISkrCd999h8DAQKnDynSY3FC2YbpND1/TbnqoXXafmx4SkTFFR0fj66+/xpYtWwAALVu2xKJFiySOKnNickNZWmqbHqa2/oqpNj18t3KIewMRkakEBQXB19cXISEhsLa2xqxZs+Dv759tFuUzFJMbynQ+tunhs+gE3Au3wuTLRw3e9FAmA3I42OL99Vdy6uauGLbpIRGRqR09ehTNmjWDUqlEgQIFsGXLFtSsWVPqsDI1JjdkFmltevj++ivPYpMQk65ND2UAXic23PSQiCxZzZo1UbJkSRQpUgQrV67MlisOG4rJDWWIEAKxScl6OzOba9NDN3tr3L0ajBaN6sIrhyM3PSQii3P16lWUKlUKVlZWsLe3x9GjR+Hu7s65eunE5IZSePwqAQ9fxKe56aE2gUkysELIWJseqlQq7Hl8CSW9nGFjY2OMt0xElCkIITBv3jyMHj0akyZNwoQJEwAAOXPmlDiyrIXJDek5dC0CX629kO7zP7TpofZxTkduekhE9DEvXrxAr1698OeffwIA/vvvP72F+ij9mNyQnlMhkQAADydbFPZw5KaHRERm8Pfff6NLly54+PAhbG1t8fPPP2PgwIFMbDKI30yk51ZEDABgdLNS6FTVW+JoiIgsm0ajwY8//ohx48ZBrVajWLFiCAwMRKVKlaQOLUtj2Qjpuf00FgBQ3NNZ4kiIiCzfnTt3MGnSJKjVanTt2hVBQUFMbIyAPTek8yr+9WJ4AFAst5PE0RARWb7ixYvj119/hRACX331FYehjITJDeloe23yudnDScGPBhGRsWk0GsyePRs+Pj6oXr06AOCrr76SOCrLw2Ep0rkdoR2SYq8NEZGxRUREoFmzZhg/fjx8fX0RFxcndUgWi/89Jx3tZOLiHJIiIjKqI0eOoHv37ggPD4e9vT0CAgLg6OgodVgWiz03pBPCycREREalVqsxefJk+Pj4IDw8HGXLlsWFCxfQq1cvqUOzaOy5IR323BARGU90dDTatm2LY8eOAQD69OmDBQsWwMHBQdrAsgEmNwQAiIpX4SkrpYiIjMbJyQmOjo5wdHTEkiVL8OWXX0odUrbB5IYAALefvu61yetqB2c77tdERJQRycnJUKlUsLe3h1wux5o1axAZGYmSJUtKHVq2wjk3BOBtGXgxzrchIsqQR48eoVGjRhgwYIDuWM6cOZnYSIDJDQF4O9+mBIekiIgMtmfPHlSsWBEnT57Ejh07cO/ePalDytaY3BCAdyulmNwQEaWXSqXCqFGj0LJlSzx//hyVK1dGUFAQChUqJHVo2Rrn3BCAdyqlOCxFRJQuDx48QJcuXXDmzBkAwNChQzFnzhwoFAqJIyMmN4SoBBUiolkpRUSUXhqNBs2aNcP169fh6uqKlStXon379lKHRW9wWIp0Q1J5XO3gwkopIqKPksvlmD9/PmrWrIlLly4xsclkmNwQbr8ZkmKvDRFR2u7evYuDBw/qHjdu3BinT59G4cKFJYyKUsPkhnRl4CU434aIKFXbt29HpUqV0LFjR9y5c0d3XC7n12hmxD8V4rYLRERpSExMxJAhQ9CxY0dER0ejbNmysLHh8H1mx+SGuGEmEVEqbt++jdq1a2PhwoUAgFGjRuH48eMoUKCAxJHRx7BaKpuLTlQhLCoRAOfcEBFpbd68GV9//TViYmKQM2dOrF27Fi1atJA6LEonJjfZnLbXxsvFDq727GolIgKAs2fPIiYmBvXq1cPGjRuRP39+qUMiAzC5yeZu6xbvY68NEWVvQgjIZDIAwPfff49ixYqhf//+sLbmV2VWwzk32dztiDcbZnJIioiysfXr16Nly5ZITk4GANja2mLw4MFMbLIoJjfZ3C2WgRNRNhYXF4c+ffqgR48e2Lt3L1atWiV1SGQETEmzuRCWgRNRNnX16lV07twZ165dg0wmQ0BAAPr06SN1WGQEkvfcLFy4EIUKFYKdnR1q1KiBc+fOffD8efPmoWTJkrC3t4e3tzeGDx+OxMREM0VrWWISVXjyplKqeG723BBR9iCEwKpVq1CtWjVcu3YNXl5eOHz4MAICAmBlZSV1eGQEkiY3W7Zsgb+/PwICAhAUFIQKFSqgadOmePr0aarnb9y4EWPGjEFAQACuX7+OFStWYMuWLRg3bpyZI7cM2kqp3M4KuDqwUoqIsodp06ahT58+SEhIQOPGjXH58mU0bNhQ6rDIiCRNbubOnYt+/fqhd+/eKFOmDJYsWQIHBwesXLky1fP//vtv1KlTB926dUOhQoXQpEkTdO3a9aO9PZQ6brtARNlRp06d4OLighkzZmDfvn3InTu31CGRkUk250apVOLixYsYO3as7phcLoePjw/OnDmT6jW1a9fG+vXrce7cOVSvXh13797Fnj170KNHjzRfJykpCUlJSbrH0dHRAACVSgWVSmWkdwPdPd/9NbO7GRYFACji4ZBlYgayXjtnVWxn82A7m54QApcvX0bZsmUBAMWKFcOtW7fg7u4OtVoNtVotcYSWxVSfaUPuJ1lyExkZCbVaDU9PT73jnp6euHHjRqrXdOvWDZGRkahbty6EEEhOTsaAAQM+OCw1a9YsTJkyJcXxAwcOwMHB4dPeRBre3TU2M/v7uhyAHElPQ7Fnz12pwzFYVmnnrI7tbB5sZ9OIj4/H4sWLcfr0aUybNg1ly5ZlW5uJsds5Pj4+3edmqWqpY8eOYebMmVi0aBFq1KiBkJAQDBs2DNOmTcPEiRNTvWbs2LHw9/fXPY6Ojoa3tzeaNGkCFxcXo8anUqlw8OBBNG7cOEtsrPb9tRMAEtGuUU1ULZhD6nDSLau1c1bFdjYPtrPpXLp0Cd27d0dISAisrKx0/+azrU3LVJ9p7chLekiW3Hh4eMDKygoRERF6xyMiIuDl5ZXqNRMnTkSPHj3w1VdfAQDKlSuHuLg4fP311xg/fnyqW88rFAooFIoUx21sbEz24TblvY0lNilZVylVOq9bpo83NVmhnS0B29k82M7GI4TAokWL4O/vD6VSiQIFCmDz5s2oWrUq9uzZw7Y2E2O3syH3kmxCsa2tLapUqYLDhw/rjmk0Ghw+fBi1atVK9Zr4+PgUCYy2bE8IYbpgLZC2UiqXswJuDrYSR0NEZByvXr1Cp06dMGTIECiVSrRp0waXLl1K83uFLJOkw1L+/v7w8/ND1apVUb16dcybNw9xcXHo3bs3AKBnz57Ily8fZs2aBQBo3bo15s6di0qVKumGpSZOnIjWrVtzbQIDafeUKsE9pYjIguzcuRPbt2+HjY0NfvjhBwwbNky3XxRlH5ImN76+vnj27BkmTZqE8PBwVKxYEfv27dNNMn7w4IFeT82ECRMgk8kwYcIEPH78GLly5ULr1q0xY8YMqd5ClqUtA+fifURkSfz8/PDvv/+ia9euqFatmtThkEQkn1A8ZMgQDBkyJNXnjh07pvfY2toaAQEBCAgIMENklk3bc8MNM4koK3vx4gUmTJiAWbNmwdXVFTKZDHPnzpU6LJKY5MkNSeNWBBfwI6Ks7cyZM+jSpQsePHiAqKgobNiwQeqQKJOQfG8pMr+4pGQ8fpUAgBtmElHWo9FoMGfOHNSvXx8PHjxA0aJF8d1330kdFmUi7LnJhrSVUh5OCuRwZKUUEWUdkZGR8PPzw549ewC8nru5dOlSo69bRlkbk5ts6O1kYvbaEFHWERwcjFatWuHx48dQKBT45Zdf0K9fP1ZDUQpMbrKh209ZBk5EWU/+/PkBACVLlkRgYCDKly8vcUSUWTG5yYZuv5lMXIyTiYkok4uOjtYNOXl4eGD//v0oWLAgnJz4nzNKGycUZ0O6nhsOSxFRJnb06FGULFkSa9as0R0rW7YsExv6KCY32Uy8MhkPX7yplGLPDRFlQmq1GlOmTIGPjw/Cw8OxcOFCaDQaqcOiLITJTTZz52kcAMDDyRburJQiokwmLCwMTZo0weTJk6HRaNC7d28cPXo01Y2RidLCOTfZzC2uTExEmdTBgwfx5Zdf4unTp3B0dMTixYvRo0cPqcOiLIjJTTajLQPnysRElJncvXsXzZs3h1qtRrly5RAYGIhSpUpJHRZlUUxushntnlJc44aIMpMiRYpg9OjReP78OX7++WfY29tLHRJlYUxushltz00x7gZORBLbu3cvSpYsiSJFigAApk+fzgX5yCg4QysbSVCq8fBlPAAu4EdE0lGpVBg1ahRatGiBLl26QKlUAgATGzIa9txkI3eexUIIwN3RFjmdFFKHQ0TZ0IMHD9ClSxecOXMGAFC9enUIISSOiiwNk5ts5Bbn2xCRhHbt2oVevXrh5cuXcHV1xYoVK9ChQwepwyILxGGpbES3YSaHpIjIjJRKJfz9/dG2bVu8fPkS1apVQ1BQEBMbMhkmN9mItlKKZeBEZE5CCJw4cQIA8O233+LUqVO6ScREpsBhqWzkbaUUe26IyPSEEJDJZFAoFAgMDMSVK1fQtm1bqcOibIDJTTaRqFLjwQttpRR7bojIdJKSkjBixAi4ublh2rRpAF6vY8PeGjIXJjfZRMjT15VSORxskJN7ShGRiYSEhMDX1xdBQUGQy+Xw8/NDsWLFpA6LshnOuckmQnSTiZ25lgQRmURgYCAqV66MoKAg5MyZE7t27WJiQ5JgcpNNsAyciEwlISEBAwYMgK+vL2JiYlC3bl0EBwejZcuWUodG2RSHpbIJbphJRKYghICPjw/+/vtvyGQyjB07FlOmTIG1Nb9eSDr89GUT3DCTiExBJpOhX79+uH37NtavX48mTZpIHRIRh6Wyg3crpYpxAT8i+kTx8fG4fv267nGvXr1w8+ZNJjaUaTC5yQbuPIuFRgBuDjbIxT2liOgTXLt2DdWrV0eTJk3w/Plz3fEcOXJIGBWRPiY32YCuUiq3EyuliCjDVq9ejapVq+Lq1atITk7GvXv3pA6JKFVMbrIBXaUUJxMTUQbExsbCz88PvXv3RkJCAnx8fBAcHIwqVapIHRpRqpjcZAO3I9723BARGeLKlSuoVq0a1q5dC7lcjunTp2P//v3w9PSUOjSiNLFaKhtgGTgRZdT333+PGzduIG/evNi0aRPq168vdUhEH8XkxsIlqtS4/zwOAHtuiMhwCxcuhL29PWbOnIlcuXJJHQ5RunBYysKFRsZBIwBXexvkcmalFBF92KVLlzBy5EgIIQAArq6uWLZsGRMbylI+qecmMTERdnZ2xoqFTODdbRdYKUVEaRFCYPHixRg+fDiUSiXKlCmD3r17Sx0WUYYY3HOj0Wgwbdo05MuXD05OTrh79y4AYOLEiVixYoXRA6RP8+6GmUREqYmKikLnzp0xePBgKJVKtG7dGm3btpU6LKIMMzi5mT59OlavXo0ffvgBtra2uuOfffYZli9fbtTg6NNxw0wi+pDz58+jUqVK2LZtG2xsbDB37lz88ccfcHd3lzo0ogwzOLlZu3Ytli5diu7du8PKykp3vEKFCrhx44ZRg6NPx0opIkrLypUrUadOHYSGhqJQoUI4deoUhg8fziFsyvIMTm4eP36MYsWKpTiu0WigUqmMEhQZR1KyGvefv95Tqjj3lCKi9xQrVgxqtRrt27fHpUuXUL16dalDIjIKgycUlylTBidPnkTBggX1jm/btg2VKlUyWmD06UIj46DWCDjbWSM3K6WICMCrV6/g5uYGAKhfvz7Onj2LKlWqsLeGLIrByc2kSZPg5+eHx48fQ6PR4Pfff8fNmzexdu1a/PXXX6aIkTLoVsTbISn+w0WUvWk0GsydOxczZszAmTNnUKpUKQBA1apVJY6MyPgMHpZq27Yt/vzzTxw6dAiOjo6YNGkSrl+/jj///BONGzc2RYyUQSGcTExEACIjI9GmTRuMHDkSr169wrp166QOicikMrTOTb169XDw4EFjx0JGpu25YRk4UfZ16tQpdO3aFY8ePYJCocD8+fPx9ddfSx0WkUkZ3HNTpEgRPH/+PMXxV69eoUiRIkYJiozj9lP23BBlVxqNBrNmzcLnn3+OR48eoUSJEjh79iz69+/PYWqyeAYnN/fu3YNarU5xPCkpCY8fPzZKUPTpkpLVuPemUopl4ETZz+rVqzFu3Dio1Wp8+eWXuHjxIipUqCB1WERmke5hqV27dul+v3//fri6uuoeq9VqHD58GIUKFTJqcJRx9yLjX1dKKazh6cJKKaLspmfPnti8eTO6dOmC3r17s7eGspV0Jzft2rUDAMhkMvj5+ek9Z2Njg0KFCuGnn34yanCUcbohKU/uKUWUHajVaqxYsQK9evWCra0trK2tsX//fv79p2wp3cmNRqMBABQuXBjnz5+Hh4eHyYKiT6ebTJybQ1JEli48PBzdu3fHkSNHcOPGDcydOxcAmNhQtmVwtVRoaKgp4iAjC3mn54aILNehQ4fw5ZdfIiIiAg4ODlxMlQgZLAWPi4vD8ePH8eDBAyiVSr3nvvnmG6MERp+GZeBEli05ORlTpkzBjBkzIIRAuXLlEBgYqFucjyg7Mzi5uXTpElq0aIH4+HjExcXB3d0dkZGRcHBwQO7cuZncZALKZA3uRcYBAEqw54bI4jx+/BjdunXDiRMnAAD9+vXD/PnzYW9vL3FkRJmDwaXgw4cPR+vWrfHy5UvY29vjn3/+wf3791GlShX8+OOPpoiRDHTveRySNQJOCmt4udhJHQ4RGVlCQgIuXboEJycnbNy4EUuXLmViQ/QOg3tugoOD8dtvv0Eul8PKygpJSUkoUqQIfvjhB/j5+aF9+/amiJMMcPvNkFSx3KyUIrIUQgjd3+dixYohMDAQRYsWRfHixSWOjCjzMbjnxsbGBnL568ty586NBw8eAABcXV3x8OFD40ZHGXLrzZ5SHJIisgwPHz5EgwYNcOjQId2xZs2aMbEhSoPBPTeVKlXC+fPnUbx4cTRo0ACTJk1CZGQk1q1bh88++8wUMZKBQp6yDJzIUvz555/o1asXXrx4gcGDB+PatWuwsrKSOiyiTM3gnpuZM2ciT548AIAZM2YgR44cGDhwIJ49e4bffvvN6AGS4bQ9NywDJ8q6lEolvvvuO7Rp0wYvXrxA1apVsXfvXiY2ROlgcM9N1apVdb/PnTs39u3bZ9SA6NOo1BqEvqmUYhk4UdZ07949+Pr64ty5cwCAYcOG4fvvv4dCwa1UiNLD4J6btAQFBaFVq1YGX7dw4UIUKlQIdnZ2qFGjhu4vc1pevXqFwYMHI0+ePFAoFChRogT27NmT0bAtzr3I15VSjrZWyOvKSimirObhw4eoVKkSzp07Bzc3N+zYsQPz5s1jYkNkAIOSm/3792PEiBEYN24c7t69CwC4ceMG2rVrh2rVqum2aEivLVu2wN/fHwEBAQgKCkKFChXQtGlTPH36NNXzlUolGjdujHv37mHbtm24efMmli1bhnz58hn0upbs9pv5NsU8nVkpRZQF5c+fH61bt0bNmjURHBys29ePiNIv3cNSK1asQL9+/eDu7o6XL19i+fLlmDt3LoYOHQpfX1/8999/KF26tEEvPnfuXPTr1w+9e/cGACxZsgS7d+/GypUrMWbMmBTnr1y5Ei9evMDff/8NGxsbAOBO5O/RloGXyM35NkRZRVhYGJ4/fw4vLy/IZDIsWbIENjY2un/niMgw6U5u5s+fj++//x4jR47E9u3b0alTJyxatAhXrlxB/vz5DX5hpVKJixcvYuzYsbpjcrkcPj4+OHPmTKrX7Nq1C7Vq1cLgwYPxxx9/IFeuXOjWrRtGjx6d5iS7pKQkJCUl6R5HR0cDAFQqFVQqlcFxf4j2fsa+ryFuhkcBAIp4OEgahyllhnbODtjO5rF582b4+/vjzz//xI4dOyCTyXRJDdveuPiZNg9TtbMh90t3cnPnzh106tQJANC+fXtYW1tjzpw5GUpsACAyMhJqtRqenp56xz09PXHjxo1Ur7l79y6OHDmC7t27Y8+ePQgJCcGgQYOgUqkQEBCQ6jWzZs3ClClTUhw/cOAAHBwcMhT7xxw8eNAk902PS3esAMjw6v517Im+Jlkc5iBlO2cnbGfTUCqVWLlypa4oIzQ0FNu2bYOjo6PEkVk+fqbNw9jtHB8fn+5z053cJCQk6JIBmUwGhUKhKwk3F41Gg9y5c2Pp0qWwsrJClSpV8PjxY8yZMyfN5Gbs2LHw9/fXPY6Ojoa3tzeaNGkCFxcXo8anUqlw8OBBNG7cWJLuZJVagxHnDgMQ6Nryc+Rzs8zl2KVu5+yC7Ww6t27dQrdu3fDvv/8CADp06ICVK1dyCwUT42faPEzVztqRl/QwqBR8+fLlcHJ6PZcjOTkZq1evhoeHh9456d0408PDA1ZWVoiIiNA7HhERAS8vr1SvyZMnD2xsbPSGoEqXLo3w8HAolUrY2tqmuEahUKRaZWDK8Wypxsrvv4yFSv26Uqqgh+VPKOacBPNgOxvXhg0b0L9/f8TFxSFXrlxYtWoVkpOTYW9vz3Y2E36mzcPY7WzIvdKd3BQoUADLli3TPfby8sK6dev0zpHJZOlObmxtbVGlShUcPnxYVw2g0Whw+PBhDBkyJNVr6tSpg40bN0Kj0ei2gLh16xby5MmTamKT3dx+s3gf95Qiypzi4+MxYcIExMXF4fPPP8eGDRuQK1cuLmdBZGTpTm7u3btn9Bf39/eHn58fqlatiurVq2PevHmIi4vTVU/17NkT+fLlw6xZswAAAwcOxK+//ophw4Zh6NChuH37NmbOnJnuhMrS6crAue0CUabk4OCALVu2YM+ePZg4cSKsrKw4uZXIBAxeodiYfH198ezZM0yaNAnh4eGoWLEi9u3bp5tk/ODBA10PDQB4e3tj//79GD58OMqXL498+fJh2LBhGD16tFRvIVPhhplEmc+aNWugVqvRp08fAED16tVRvXp1iaMismySJjcAMGTIkDSHoY4dO5biWK1atfDPP/+YOKqsSbdhJpMbIsnFxsZi8ODBWLt2LRQKBerWrYsSJUpIHRZRtiB5ckPGkazW4O6zN3tKcViKSFJXrlxB586dcePGDcjlckyYMAFFixaVOiyibIPJjYW4/yIeSrUG9jZWFlsCTpTZCSGwYsUKDB06FImJicibNy82btyIBg0aSB0aUbbC5MZCaCulins6QS5npRSRuQkh4Ofnp6sibdasGdauXYtcuXJJHBlR9pOhXcHv3LmDCRMmoGvXrrpNLvfu3YurV68aNThKP+2eUsW4pxSRJGQyGYoXLw4rKyvMnj0bu3fvZmJDJBGDk5vjx4+jXLlyOHv2LH7//XfExr7+Ur18+XKaqwST6WnLwEt4cr4NkbkIIfDy5Uvd43HjxuHixYsYPXq0XqUnEZmXwX/7xowZg+nTp+PgwYN6C+c1atSIVUwS0paBF2fPDZFZREVFwdfXF59//jkSEhIAAFZWVqhQoYLEkRGRwcnNlStX8L///S/F8dy5cyMyMtIoQZFhktUa3I18XSnFnhsi07tw4QIqV66MrVu34tq1azh9+rTUIRHROwxObtzc3BAWFpbi+KVLl5AvXz6jBEWGefAiHspkVkoRmZoQAr/88gtq166Nu3fvomDBgjh16hR8fHykDo2I3mFwctOlSxeMHj0a4eHhkMlk0Gg0OH36NEaMGIGePXuaIkb6iLfbLrBSishUXr58ifbt22PYsGFQqVRo164dLl26hBo1akgdGhG9x+DkZubMmShVqhS8vb0RGxuLMmXKoH79+qhduzYmTJhgihjpI25zvg2RyQ0aNAg7d+6Era0tfvnlF/z+++/IkSOH1GERUSoMXufG1tYWy5Ytw8SJE/Hff/8hNjYWlSpVQvHixU0RH6WDrueG2y4Qmcz333+PO3fuYPHixahSpYrU4RDRBxic3Jw6dQp169ZFgQIFUKBAAVPERAa69WaNmxLcdoHIaJ4/f44///wTvXr1AgAUKFAAZ8+ehUzGoV+izM7gYalGjRqhcOHCGDduHK5du2aKmMgAao3AnWfcMJPImE6fPo2KFSuid+/e+PPPP3XHmdgQZQ0GJzdPnjzBd999h+PHj+Ozzz5DxYoVMWfOHDx69MgU8dFHaCul7GzkyJ/DQepwiLI0jUaD2bNno0GDBnj06BGKFy8Ob29vqcMiIgMZnNx4eHhgyJAhOH36NO7cuYNOnTphzZo1KFSoEBo1amSKGOkDtJOJi+ZyghUrpYgy7OnTp2jRogXGjh0LtVqNbt264eLFi6hYsaLUoRGRgT5pffDChQtjzJgxmD17NsqVK4fjx48bKy5KJ267QPTpjh8/jooVK2L//v2ws7PD8uXLsX79ejg78+8VUVaU4eTm9OnTGDRoEPLkyYNu3brhs88+w+7du40ZG6WDtueGG2YSZVxYWBjCwsJQunRpnD9/Hn379uX8GqIszOBqqbFjx2Lz5s148uQJGjdujPnz56Nt27ZwcOB8Dymw54YoY4QQugSmS5cuUCqV6NChAxwdHSWOjIg+lcE9NydOnMDIkSPx+PFj/PXXX+jatSsTG4moNQIhb5IbLuBHlH6HDx9G5cqVER4erjvWs2dPJjZEFsLgnhtuEJd5PHoZj6RkDRTWcni7M8Ek+hi1Wo0pU6Zg+vTpEEJgypQpWLx4sdRhEZGRpSu52bVrF5o3bw4bGxvs2rXrg+e2adPGKIHRx2kX72OlFNHHPXnyBN26ddMVPnz11Vf46aefJI6KiEwhXclNu3btEB4ejty5c6Ndu3ZpnieTyaBWq40VG33E7aevJxOX4OJ9RB+0f/9+fPnll4iMjISTkxN+++03dOvWTeqwiMhE0pXcaDSaVH9P0rodoV2ZmJOJidKydetWdO7cGQBQoUIFBAYGokSJEhJHRUSmZPCE4rVr1yIpKSnFcaVSibVr1xolKEofbc8Ny8CJ0tasWTOUKFECgwYNwj///MPEhigbMDi56d27N6KiolIcj4mJQe/evY0SFH2c5p1KKZaBE+n7559/IIQAADg7O+P8+fNYuHAh7OzsJI6MiMzB4OTm3bUh3vXo0SO4uroaJSj6uEcvE5Co0sDWWo4CrJQiAvC6B3nEiBGoVasW5s2bpzvu4uIiXVBEZHbpLgWvVKkSZDIZZDIZvvjiC1hbv71UrVYjNDQUzZo1M0mQlNIt7ilFpOfevXvo0qULzp49CwB4/PixxBERkVTSndxoq6SCg4PRtGlTODm9nedha2uLQoUKoUOHDkYPkFJ3m4v3Eens3LkTvXv3xqtXr+Dm5oZVq1Z9sLKTiCxbupObgIAAAEChQoXg6+vLsWuJafeUYhk4ZWdJSUkYNWoUfvnlFwBAjRo1sHnzZhQqVEjawIhIUgbPufHz82Nikwloe26K5eZkYsq+rl27hkWLFgEAvvvuO5w4cYKJDRGlr+fG3d0dt27dgoeHB3LkyPHB3XJfvHhhtOAodfqVUuy5oeyrUqVKWLBgAfLnz49WrVpJHQ4RZRLpSm5+/vlnODs7637/oeSGTO/xqwQkqNSwtWKlFGUviYmJGD16NPr27Yvy5csDAAYMGCBxVESU2aQrufHz89P9vlevXqaKhdJJu3hfkVyOsLYyeGSRKEu6desWOnfujMuXL+PAgQO4cuWKXtUmEZGWwd+MQUFBuHLliu7xH3/8gXbt2mHcuHFQKpVGDY5Sd4vbLlA2s3HjRlSpUgWXL19Grly5MG/ePCY2RJQmg5Ob/v3749atWwCAu3fvwtfXFw4ODti6dStGjRpl9AApJe2eUiVYBk4WLj4+Hv369UP37t0RGxuLBg0a6JajICJKi8HJza1bt1CxYkUArzeka9CgATZu3IjVq1dj+/btxo6PUqEdlirOycRkwcLDw1GjRg0sX74cMpkMkyZNwqFDh5A3b16pQyOiTM7gfl0hhG5n8EOHDukqFLy9vREZGWnc6CiFdyulWAZOlixXrlzInTs3PD09sWHDBnzxxRdSh0REWYTByU3VqlUxffp0+Pj44Pjx41i8eDEAIDQ0FJ6enkYPkPQ9fpWAeKUaNlYyFMrJSimyLHFxcbCysoKdnR2srKywYcMGAICXl5fEkRFRVmLwsNS8efMQFBSEIUOGYPz48ShWrBgAYNu2bahdu7bRAyR92l6bIh5OrJQii/Lff/+hWrVqGD58uO6Yl5cXExsiMpjBPTfly5fXq5bSmjNnDqysrIwSFKVNu2Em59uQpRBCYOXKlRgyZAgSExMRFRWF6dOnI2fOnFKHRkRZVIZrKS9evIjr168DAMqUKYPKlSsbLShK29sNMznfhrK+mJgYDBw4UDf81LRpU6xbt46JDRF9EoOTm6dPn8LX1xfHjx+Hm5sbAODVq1do2LAhNm/ejFy5chk7RnoHN8wkS3H58mV07twZt27dgpWVFaZPn45Ro0ZBLudwKxF9GoP/FRk6dChiY2Nx9epVvHjxAi9evMB///2H6OhofPPNN6aIkd4QQrztuWFyQ1lYUlISWrRogVu3biF//vw4fvw4xowZw8SGiIzC4J6bffv24dChQyhdurTuWJkyZbBw4UI0adLEqMGRvncrpQrmdJQ6HKIMUygUWLx4MZYtW4bVq1dzGIqIjMrg5Eaj0cDGxibFcRsbG936N2Qa2l6bwh6OsGGlFGUxFy9exMuXL+Hj4wMAaNOmDVq3bs2NeInI6Az+hmzUqBGGDRuGJ0+e6I49fvwYw4cP5yJbJhbCPaUoCxJCYMGCBahduzZ8fX3x8OFD3XNMbIjIFAxObn799VdER0ejUKFCKFq0KIoWLYrChQsjOjoaCxYsMEWM9IauDJx7SlEW8fLlS3To0AHffPMNlEol6tevDycnfn6JyLQMHpby9vZGUFAQDh8+rCsFL126tK6rmUxHOyxVgj03lAWcPXsWXbp0wb1792Bra4sff/wRQ4YMYW8NEZmcQcnNli1bsGvXLiiVSnzxxRcYOnSoqeKi9wjxdk8p9txQZiaEwM8//4zRo0cjOTkZRYoUQWBgIKpUqSJ1aESUTaR7WGrx4sXo2rUrLly4gNu3b2Pw4MEYOXKkKWOjd4RFJSI2KRnWclZKUeYmk8lw48YNJCcno1OnTggKCmJiQ0Rmle7k5tdff0VAQABu3ryJ4OBgrFmzBosWLTJlbPQO7Xybwh6OsLVmpRRlPu9WS86fPx/r16/Hli1b4OrqKmFURJQdpftb8u7du/Dz89M97tatG5KTkxEWFmaSwEhfCBfvo0xKo9Hg+++/R6tWrXQJjr29Pbp37875NUQkiXTPuUlKSoKj49vhELlcDltbWyQkJJgkMNL3tlKKk4kp83j27Bl69uyJffv2AQD++OMP/O9//5M4KiLK7gyaUDxx4kQ4ODjoHiuVSsyYMUOv23nu3LnGi450uO0CZTYnTpxA165d8eTJE9jZ2eHXX39Fu3btpA6LiCj9yU39+vVx8+ZNvWO1a9fG3bt3dY/ZBW0aQgjdAn4sAyepqdVqzJo1CwEBAdBoNChdujQCAwPx2WefSR0aEREAA5KbY8eOmTAM+pDw6ETEvKmUKsRKKZLYoEGDsHTpUgBAr1698Ouvv+oNWRMRSS1TlN0sXLgQhQoVgp2dHWrUqIFz586l67rNmzdDJpNZfFf4rTe9NoVYKUWZwMCBA+Hu7o41a9Zg1apVTGyIKNOR/Jtyy5Yt8Pf3R0BAAIKCglChQgU0bdoUT58+/eB19+7dw4gRI1CvXj0zRSqd29x2gSSkVqtx5swZ3eOKFSvi/v376Nmzp4RRERGlTfLkZu7cuejXrx969+6NMmXKYMmSJXBwcMDKlSvTvEatVqN79+6YMmUKihQpYsZopfG2DJzzbci8Xrx4gaZNm6JBgwY4f/687jj3hyKizEzS5EapVOLixYt6+1LJ5XL4+Pjo/U/xfVOnTkXu3LnRt29fc4QpOW6YSVI4cOAAhg8fjhMnTkChUODJkydSh0RElC4Gb5xpTJGRkVCr1fD09NQ77unpiRs3bqR6zalTp7BixQoEBwen6zWSkpKQlJSkexwdHQ0AUKlUUKlUGQs8Ddr7GfO+QghdGXiRnHZGjzkrMkU701vJyckICAjAnDlzAADlypXDpk2bUKJECba5CfDzbD5sa/MwVTsbcr8MJTcnT57Eb7/9hjt37mDbtm3Ily8f1q1bh8KFC6Nu3boZuWW6xMTEoEePHli2bBk8PDzSdc2sWbMwZcqUFMcPHDigt2aPMR08eNBo93qVBMQkWkMOgRvnTyJE8oHEzMOY7UyvPXv2DHPnzsX169cBAM2bN0fv3r0REhKCkJAQiaOzbPw8mw/b2jyM3c7x8fHpPtfg5Gb79u3o0aMHunfvjkuXLul6RaKiojBz5kzs2bMn3ffy8PCAlZUVIiIi9I5HRETAy8srxfl37tzBvXv30Lp1a90x7XLv1tbWuHnzJooWLap3zdixY+Hv7697HB0dDW9vbzRp0gQuLi7pjjU9VCoVDh48iMaNG8PGxsYo9zwV8hwIuohCHo5o08p0iWNWYop2ptcWLFiA69evw8XFBQsXLoSzszPb2cT4eTYftrV5mKqdtSMv6WFwcjN9+nQsWbIEPXv2xObNm3XH69Spg+nTpxt0L1tbW1SpUgWHDx/WlXNrNBocPnwYQ4YMSXF+qVKlcOXKFb1jEyZMQExMDObPnw9vb+8U1ygUCigUihTHbWxsTPbhNua97z5/vb1FCU8X/mV8jyn/DLOrb7/9FhEREfj6669RoEAB7Nmzh+1sJmxn82Fbm4ex29mQexmc3Ny8eRP169dPcdzV1RWvXr0y9Hbw9/eHn58fqlatiurVq2PevHmIi4tD7969AQA9e/ZEvnz5MGvWLNjZ2aVYBdXNzQ0ALHZ11JCnbyYTc9sFMoH79+9j4sSJWLRoEZycnCCXy/H9998D4LwEIsq6DE5uvLy8EBISgkKFCukdP3XqVIbKsn19ffHs2TNMmjQJ4eHhqFixIvbt26ebZPzgwQPI5dl3ool2AT+WgZOx/fHHH+jVqxdevXoFJycnLFq0SOqQiIiMwuDkpl+/fhg2bBhWrlwJmUyGJ0+e4MyZMxgxYgQmTpyYoSCGDBmS6jAU8PFtH1avXp2h18wKhBBcwI+MTqlUYtSoUZg/fz4AoHr16hg1apTEURERGY/Byc2YMWOg0WjwxRdfID4+HvXr14dCocCIESMwdOhQU8SYbT2NSUJ0YjLkMqBILi5xT5/u7t278PX1xYULFwAA3333HWbOnAlbW1uJIyMiMh6DkxuZTIbx48dj5MiRCAkJQWxsLMqUKcMVS03gtnZPqZyOUFhbSRwNZXXHjh1D27ZtER0drdsbqlWrVlKHRURkdBlexM/W1hZlypQxZiz0Ht3KxJxMTEZQsmRJ2NnZ6RblS626kIjIEhic3DRs2BAymSzN548cOfJJAdFb2pWJi+fmZGLKmMjISN2Cl3ny5MHx48dRtGhRlsESkUUzuAypYsWKqFChgu6nTJkyUCqVCAoKQrly5UwRY7bFMnD6FJs2bUKRIkWwbds23bFSpUoxsSEii2dwz83PP/+c6vHJkycjNjb2kwOi14QQb8vA2XNDBkhISMCwYcOwbNkyAMDatWvRsWNHiaMiIjIfoy0g8+WXX2LlypXGul229yw2CVEJKlZKkUFu3LiBGjVqYNmyZZDJZJg4cSJ+//13qcMiIjIro+0KfubMGdjZ2RnrdtmetlKqYE5H2NmwUoo+bu3atRg4cCDi4+Ph6emJ9evXw8fHR+qwiIjMzuDkpn379nqPhRAICwvDhQsXMryIH6WkXbyvGBfvo3QICgqCn58fAKBRo0bYsGFDqpvPEhFlBwYnN66urnqP5XI5SpYsialTp6JJkyZGCyy7u/WmUqoEJxNTOlSuXBnfffcdXF1dMW7cOFhZsbePiLIvg5IbtVqN3r17o1y5csiRI4epYiIAIZxMTB8ghMDatWvxxRdfIH/+/ACAH3/8UeKoiIgyB4MmFFtZWaFJkyYZ2v2b0k8IgVssA6c0xMTEoEePHujVqxe6du2K5ORkqUMiIspUDK6W+uyzz3D37l1TxEJvRMYq8Sr+daVU0VxMbuity5cvo2rVqtiwYQOsrKzQsmVLyOVGK3okIrIIBv+rOH36dIwYMQJ//fUXwsLCEB0drfdDn047mbiAuwMrpQjA69683377DTVq1MCtW7eQP39+HD9+HGPGjGFyQ0T0nnTPuZk6dSq+++47tGjRAgDQpk0bvW0YhBCQyWRQq9XGjzKb0W67UIzzbQivh6G++uorBAYGAgBatWqF1atXI2fOnBJHRkSUOaU7uZkyZQoGDBiAo0ePmjIewtsNM1kpRcDruW7Xrl2DtbU1Zs+eDX9//w/u70ZElN2lO7kRQgAAGjRoYLJg6DXdhplMbrItIQSEEJDL5XBwcEBgYCCioqJQs2ZNqUMjIsr0DBqs5/8WzSOEu4Fna69evULHjh3x/fff646VLl2aiQ0RUToZtM5NiRIlPprgvHjx4pMCyu4iY5PwIk4JGSulsqVz587B19cX9+7dw969e9GnTx94enpKHRYRUZZiUHIzZcqUFCsUk3Fp95Qq4O4Ae1tWSmUXQgjMmzcPo0ePhkqlQpEiRbBlyxYmNkREGWBQctOlSxfkzp3bVLEQgNvaxfu4p1S28eLFC/Tq1Qt//vknAKBjx45Yvnw5/yNBRJRB6U5uON/GPLQ9NywDzx6USiVq1qyJ27dvQ6FQ4Oeff8aAAQP4942I6BOke0KxtlqKTItl4NmLra0tvv32WxQvXhz//PMPBg4cyMSGiOgTpTu50Wg0HJIyA1ZKWb7IyEhcu3ZN93jgwIEIDg5GxYoVpQuKiMiCcN32TOR5bBKev6mUKsY5Nxbp5MmTqFChAlq3bo2oqCgAr4d8HRwcJI6MiMhyMLnJRLSL9+XPYc9KKQuj0WgwY8YMfP7553jy5AlsbW3x7NkzqcMiIrJIBlVLkWlpN8wswSEpixIREYEePXrg4MGDAAA/Pz8sXLgQjo6OEkdGRGSZmNxkIroNMzmZ2GIcOXIE3bt3R3h4OBwcHLBo0SL4+flJHRYRkUVjcpOJ3GLPjcX5+eefER4ejrJlyyIwMBBlypSROiQiIovHOTeZSAg3zLQ4q1atwogRI3Du3DkmNkREZsLkJpN4EadEZKwSACulsrIDBw5gxIgRusceHh6YM2cOq6GIiMyIw1KZhHYycf4c9nCw5R9LVpOcnIyAgADMmjULQgjUrl0b7du3lzosIqJsid+imYR2MnEJT863yWoePXqEbt264eTJkwCAAQMGoHnz5hJHRUSUfTG5ySS0PTfcMDNr2bNnD3r27Innz5/D2dkZy5cvR+fOnaUOi4goW+Ocm0xCVwbO5CbLmDlzJlq2bInnz5+jSpUquHTpEhMbIqJMgMlNJnErgsNSWU2VKlUgk8kwdOhQnD59GkWLFpU6JCIiAoelMoWXcUpExiYBYM9NZvf06VPdBrJNmzbF1atXUbp0aYmjIiKid7HnJhPQDknlc7OHo4L5ZmakVCoxfPhwlCxZEnfv3tUdZ2JDRJT5MLnJBG4/fTOZmIv3ZUqhoaGoW7cu5s2bh1evXmHv3r1Sh0RERB/A5CYTuM35NpnW9u3bUalSJZw/fx7u7u7YtWsXBg8eLHVYRET0AUxuMgFtzw3n22QeiYmJGDJkCDp27IioqCjUrl0bly5dQuvWraUOjYiIPoLJTSbASqnM55dffsHChQsBAKNHj8axY8dQoEABiaMiIqL04OxVib2KV+JZDCulMpthw4bh6NGj+Oabb7jaMBFRFsOeG4mFvFMp5cRKKckkJCTgxx9/RHJyMgBAoVBg7969TGyIiLIgfptKTDskxV4b6dy4cQOdO3fGlStX8OrVK0yfPl3qkIiI6BOw50Zi2snEJVgGLol169ahatWquHLlCjw9PfH5559LHRIREX0iJjcS05aBF8/NycTmFBcXhz59+qBnz56Ii4tDo0aNEBwcDB8fH6lDIyKiT8TkRmK6MnD23JjN9evXUb16daxatQpyuRxTpkzBgQMH4OXlJXVoRERkBJxzI6GoBBUiol9XShXnnBuz0Wg0CA0NRZ48ebBx40YORRERWRgmNxIKedNrk8fVDs52NhJHY9nUajWsrKwAAGXLlsWOHTtQqVIl3SaYRERkOTgsJSFtpVRxLt5nUpcvX0b58uVx6tQp3bGmTZsysSEislBMbiT0djIxh6RMQQiB3377DTVq1MC1a9cwcuRICCGkDouIiEyMyY2EWAZuOtHR0ejatSsGDBiApKQktGjRAn/++SdkMpnUoRERkYkxuZHQbd0CfhyWMqagoCBUqVIFW7ZsgbW1NebMmYM///wTHh4eUodGRERmwAnFEolKUCE8OhEAUJw9N0bz33//oVatWlAqlShQoAA2b96MWrVqSR0WERGZEZMbiWj3lPJysYMLK6WMpmzZsmjVqhWSk5OxatUquLu7Sx0SERGZWaYYllq4cCEKFSoEOzs71KhRA+fOnUvz3GXLlqFevXrIkSMHcuTIAR8fnw+en1lpy8DZa/PpLly4gKioKACATCbD+vXrsXPnTiY2RETZlOTJzZYtW+Dv74+AgAAEBQWhQoUKaNq0KZ4+fZrq+ceOHUPXrl1x9OhRnDlzBt7e3mjSpAkeP35s5sg/zS1uu/DJhBD4+eefUbt2bXz99de6Sih7e3tOHCYiysYkT27mzp2Lfv36oXfv3ihTpgyWLFkCBwcHrFy5MtXzN2zYgEGDBqFixYooVaoUli9fDo1Gg8OHD5s58k9z+82wFCulMiYmJgYdOnSAv78/VCoVNBoNlEql1GEREVEmIGlyo1QqcfHiRb3NCuVyOXx8fHDmzJl03SM+Ph4qlSrLDUHcjuCwVEb9888/GD58OP766y/Y2tpi4cKFCAwMhEKhkDo0IiLKBCSdUBwZGQm1Wg1PT0+9456enrhx40a67jF69GjkzZs3zd2ck5KSkJSUpHscHR0NAFCpVFCpVBmMPHXa+33svjGJyQiLel0pVSiHndHjsFQajQZz587FxIkToVarUbRoUWzcuBGVKlVCcnKy1OFZnPR+nunTsJ3Nh21tHqZqZ0Pul6WrpWbPno3Nmzfj2LFjsLOzS/WcWbNmYcqUKSmOHzhwAA4ODiaJ6+DBgx98/l4MAFjD1Ubg1NEPn0tvxcTE4Mcff4RarUa9evUwaNAghIWFISwsTOrQLNrHPs9kHGxn82Fbm4ex2zk+Pj7d50qa3Hh4eMDKygoRERF6xyMiIuDl5fXBa3/88UfMnj0bhw4dQvny5dM8b+zYsfD399c9jo6O1k1CdnFx+bQ38B6VSoWDBw+icePGsLFJu7x768XHwH9XUbZATrRoUdWoMVi6vHnz4vr168iXLx+aNGnywXamT5PezzN9Graz+bCtzcNU7awdeUkPSZMbW1tbVKlSBYcPH0a7du0AQDc5eMiQIWle98MPP2DGjBnYv38/qlb9cHKgUChSnYthY2Njsg/3x+59N/J19lnSy4V/wT5Ao9Fg1qxZKFiwIL788ksAQKNGjVCvXj3s2bPHpH+G9Bbb2TzYzubDtjYPY7ezIfeSfFjK398ffn5+qFq1KqpXr4558+YhLi4OvXv3BgD07NkT+fLlw6xZswAA33//PSZNmoSNGzeiUKFCCA8PBwA4OTnBySlrTM7VVkqxDDxtERER6NGjBw4ePAgHBwc0bNgQ+fLlkzosIiLKAiRPbnx9ffHs2TNMmjQJ4eHhqFixIvbt26ebZPzgwQPI5W+LuhYvXgylUomOHTvq3ScgIACTJ082Z+gZpq2UYhl46o4ePYpu3bohPDwc9vb2+PXXX5E3b16pwyIioixC8uQGAIYMGZLmMNSxY8f0Ht+7d8/0AZlQTKIKT95USrHnRp9arcb06dMxdepUaDQalC1bFoGBgShTpozUoRERURaSKZKb7ES7p1RuZwVcHTjmq5WcnIxmzZrpFmPs27cvfvnlF5NVtBERkeWSfIXi7EY334ZDUnqsra1RrVo1ODo6Yv369Vi+fDkTGyIiyhAmN2amW5mYQ1JITk7Gs2fPdI+nTp2Ky5cvo3v37hJGRUREWR2TGzNjz81rjx49QsOGDdGyZUvdnlA2NjYoWrSoxJEREVFWx+TGzG5HaDfMzL49N3v27EHFihVx6tQp3LhxA//995/UIRERkQVhcmNGcUnJePwqAQBQLFf267lRqVQYNWoUWrZsiefPn6Ny5coICgpC5cqVpQ6NiIgsCKulzEhbKeXhpEAOR1uJozGv+/fvo0uXLvjnn38AAEOHDsWcOXO4kzcRERkdkxszupWNF+/76quv8M8//8DV1RUrV65E+/btpQ6JiIgsFIelzChEt+1C9ktuFi9eDB8fH1y6dImJDRERmRSTGzPS9twUzwaTiUNDQ7F8+XLd42LFiuHgwYMoXLiwhFEREVF2wGEpM7qdTXputm/fjr59+yI6OhqFChWCj4+P1CEREVE2wp4bM4lLSsajl68rpSy1DDwxMRFDhgxBx44dERUVhZo1a6J48eJSh0VERNkMkxszufNMWylla5GVUiEhIahduzYWLlwIABg1ahSOHz+OggULShwZERFlNxyWMpNbEdohKcvrtdm6dSv69u2LmJgY5MyZE2vXrkWLFi2kDouIiLIpJjdmcvupdjKx5c23iY2NRUxMDOrVq4eNGzcif/78UodERETZGJMbM9Fuu2AplVLJycmwtn798enVqxecnJzwv//9T3eMiIhIKpxzYya6nhsLqJRat24dypcvj+fPnwMAZDIZOnXqxMSGiIgyBSY3ZhCvtIxKqbi4OPTp0wc9e/bE9evX8csvv0gdEhERUQr8r7YZ3HkaByGAnI62cM+ilVJXr15F586dce3aNchkMgQEBGDChAlSh0VERJQCkxsz0A5JFcuCQ1JCCKxevRqDBw9GQkICvLy8sHHjRjRs2FDq0IiIiFLFYSkz0JaBZ8UhqUWLFqFPnz5ISEhA48aNERwczMSGiIgyNSY3ZhCShcvAu3fvjmLFimHGjBnYt28fPD09pQ6JiIjogzgsZQZZaQE/IQQOHToEHx8fyGQyuLm54cqVK7Czs5M6NCIionRhz42JJSjVePgyHkDm77mJjo5Gt27d0KRJEyxbtkx3nIkNERFlJey5MbE7z2IhBODuaAsPJ4XU4aTp0qVL6Ny5M0JCQmBtbY2EhASpQyIiI1Cr1VCpVFKHkSmoVCpYW1sjMTERarVa6nAs1qe0s62tLeTyT+93YXJjYpm9UkoIgUWLFsHf3x9KpRIFChTA5s2bUatWLalDI6JPIIRAeHg4Xr16JXUomYYQAl5eXnj48CFkMpnU4VisT2lnuVyOwoULw9b205ZNYXJjYm8rpTJfcvPq1St89dVX2L59OwCgTZs2WLVqFdzd3SWOjIg+lTaxyZ07NxwcHPhlDkCj0SA2NhZOTk5G6R2g1GW0nTUaDZ48eYKwsDAUKFDgkz6zTG5M7HYmnkx85coV7NixAzY2Nvjhhx8wbNgw/gNIZAHUarUuscmZM6fU4WQaGo0GSqUSdnZ2TG5M6FPaOVeuXHjy5AmSk5NhY2OT4RiY3JhYZt4NvF69evj1119RtWpVVKtWTepwiMhItHNsHBwcJI6EyDDa4Si1Wv1JyQ1TVxNKVKnx4MWbSqlM0HPz4sULdOvWDTdv3tQdGzhwIBMbIgvFnljKaoz1mWVyY0LaSqkcDjbwcJJ2T6kzZ86gUqVK2LRpE3r06AEhhKTxEBFlRceOHYNMJjPLRO3JkyejYsWKKY55enpCJpNh586d6NWrF9q1a2fyWLIaJjcm9O58G6n+B6XRaDBnzhzUr18fDx48QNGiRbFkyRL+j46IMhUhBHx8fNC0adMUzy1atAhubm549OiRBJFJZ8SIETh8+LDu8fXr1zFlyhT89ttvCAsLQ/PmzTF//nysXr1auiAzKSY3JiT1fJvIyEi0bt0ao0aNQnJyMnx9fREUFITKlStLEg8RUVpkMhlWrVqFs2fP4rffftMdDw0NxahRo7BgwQLkz5/fqK+Z2df/cXJy0psQfufOHQBA27Zt4eXlBYVCAVdXV7i5uWX4NYQQSE5O/tRQMx0mNyb0dtsF8yc3ISEhqFixIvbs2QM7Ozv89ttv2LRpE1xcXMweCxFRenh7e2P+/PkYMWIEQkNDIYRA37590aRJExQoUADVq1eHQqFAnjx5MGbMGL0v5UKFCmHevHl696tYsSImT56se2xlZYUVK1agbdu2cHR0xIwZM1KNIykpCaNHj4a3tzcUCgWKFSuGFStWpHru8+fP0bVrV+TLlw8ODg4oV64cNm3apHfOtm3bUK5cOdjb2yNnzpzw8fFBXFwcgNfDXNWrV4ejoyPc3NxQp04d3L9/H4D+sNTkyZPRunVrAK/XgtH2vr8/LKXRaDBr1iwULlwY9vb2qFChArZt26Z7XjustnfvXlSpUgUKhQKnTp1K408k62K1lAmFPH2T3EiwG3jBggVRsGBBODk5ITAwEOXLlzd7DESUOQghkKCSZkVeexsrg4bB/fz8sGPHDvTp0wft27fHf//9h/Pnz6NMmTLo1asX1q5dixs3bqBfv36ws7PTS17S4/vvv8esWbMwf/58WFun/hXYs2dPnDlzBr/88gsqVKiA0NBQREZGpnpuYmIiqlSpgtGjR8PFxQW7d+9Gjx49ULRoUVSvXh1hYWHo2rUrfvjhB/zvf/9DTEwMTp48qesxadeuHfr164dNmzZBqVTi3LlzqbbXiBEjUKhQIfTu3RthYWFpvr9Zs2Zh/fr1WLJkCYoXL44TJ07gyy+/RK5cudCgQQPdeWPGjMGPP/6IIkWKIEeOHAa1YVbA5MZEElVq3H/+OjM317DUs2fP4OrqCltbW9jY2GDbtm1wdnaGk1PmK0MnIvNJUKlRZtJ+SV772tSmcLA17Ktm6dKlKFu2LE6cOIHt27dj6dKl8Pb2xq+//gqZTIZSpUrhyZMnGD16NCZNmmTQWiodO3ZE796907zm1q1bCAwMxMGDB+Hj4wMAKFKkSJr3y5cvH0aMGKF7PHToUOzfvx+BgYG65CY5ORnt27dHwYIFAQDlypUD8LqCNSoqCq1atULRokUBAKVLl071dZycnHTDT15eXqmek5SUhJkzZ+LQoUO6VeaLFCmCU6dO4bffftNLbqZOnYrGjRun+b6yOg5LmcjdZ3HQCMDV3ga5zLCn1NGjR1G+fHmMGzdOdyxPnjxMbIgoy8mdOzf69++P0qVLo127drh+/Tpq1aql16NRp04dxMbGGjzJ+P3qo/cFBwfDyspKLxH4ELVajWnTpqFcuXJwd3eHk5MT9u/fjwcPHgAAKlSogC+++ALlypVDp06dsGzZMrx8+RIA4O7ujl69eqFp06Zo3bo15s+f/8FemY8JCQlBfHw8GjduDCcnJ93P2rVrdfN1tKpWrZrh18kK2HNjItrJxCU8nUxamaRWqzF9+nRMnToVGo0G+/btw9SpU7l4FxHp2NtY4drUlFVI5nrtjLC2tk5z2Cg1crk8xRIXqU0YdnR0/OB97O3t0/2aADBnzhzMnz8f8+bNQ7ly5eDo6Ihvv/0WSqUSwOt5PgcPHsTff/+NAwcOYMGCBRg/fjzOnj2LwoULY9WqVfjmm2+wb98+bNmyBRMmTMDBgwdRs2ZNg+IAgNjY11Mhdu/ejXz58uk9p1Do/yf7Y+2Q1TG5MRFtGXgxEy7eFxYWhi+//BJHjhwBAPTp0wcLFixgYkNEemQymcFDQ5lJ6dKlsX37dgghdP9ZPH36NJydnXUVVLly5dLr9YiOjkZoaKjBr1WuXDloNBocP35cNyz1IadPn0bbtm3x5ZdfAng9offWrVsoU6aM7hyZTIY6deqgTp06mDRpEgoWLIgdO3bA398fAFCpUiVUqlQJY8eORa1atbBx48YMJTdlypSBQqHAgwcP0t3zZKk4LGUityLe9tyYwsGDB1GxYkUcOXIEjo6OWLt2LVasWMHEhogszqBBg/Dw4UMMHToUN27cwB9//IGAgAD4+/vr5s40atQI69atw8mTJ3HlyhX4+fnBysrwXqNChQrBz88Pffr0wc6dOxEaGopjx44hMDAw1fOLFy+u65m5fv06+vfvj4iICN3zZ8+excyZM3HhwgU8ePAAv//+O549e4bSpUsjNDQUY8eOxZkzZ3D//n0cOHAAt2/fTnPezcc4OztjxIgRGD58ONasWYM7d+4gKCgICxYswJo1azJ0z6wq66bymZyuUsoEPTevXr1Cp06dEBUVhXLlyiEwMBClSpUy+usQEWUG+fLlw549ezBy5EhUqFAB7u7u6Nu3LyZMmKA7Z+zYsQgNDUWrVq3g6uqKadOmZajnBgAWL16McePGYdCgQXj+/DkKFCigN5/xXRMmTMDdu3fRtGlTODg44Ouvv0a7du0QFRUFAHBxccGJEycwb948REdHo2DBgvjpp5/QvHlzRERE4MaNG1izZg2eP3+OPHnyYPDgwejfv3+G4gaAadOmIVeuXJg1axbu3r0LNzc3VK5cOc34LZVMZLN1+KOjo+Hq6oqoqCijr/miUqmwZ88efNG4KcpPOwyNAM6N+wK5XeyM+joAsHnzZhw9ehTz5s0zeIw4q9O2c4sWLT5pYzX6MLazeZiinRMTExEaGorChQvDzs74//5kVRqNBtHR0XBxceGu4Cb0Ke38oc+uId/f7LkxgdDn8dAIwMXOGrmcjVMptXfvXtjZ2aFhw4YAgC5duqBLly5GuTcREZElYepqAtohqRKen76nlEqlwujRo9GiRQt07dpVbyyXiIiIUmLPjQncfmqcxfsePHiALl264MyZMwBeLz7l6ur6yfERERFZMiY3JhDy7NMnE+/atQu9evXCy5cv4erqihUrVqBDhw7GCpGIiMhicVjKBN7uKWV4z41arYa/vz/atm2Lly9folq1aggKCmJiQ0RElE5MbowsWQPcf5EAIGM9N3K5HE+fPgUAfPvttzh16tQH9zUhIiIifRyWMrKnCYBaI+BsZw1Pl/RXSiUnJ8Pa2hoymQyLFy9G9+7d0bx5cxNGSkREZJnYc2Nk4Qmvq6OK507fnlJJSUkYOnQoOnTooNsXxdnZmYkNERFRBrHnxsjC418nNCU8Pz4kFRISAl9fXwQFBQEATp06hXr16pk0PiIiIkvHnhsjC3893QbFcn94MvGWLVtQuXJlBAUFIWfOnPjrr7+Y2BARSUgmk2Hnzp1Sh5Emc8V37NgxyGQyvHr1Snds586dKFasGKysrPDtt99i9erVcHNzM3ksGcXkxsi0w1Jp9dwkJCRgwIAB6NKlC2JiYlC3bl0EBwejZcuW5gyTiCjT6dWrF2QyGWQyGWxsbFC4cGGMGjUKiYmJUodmcuHh4Rg6dCiKFCkChUIBb29vtG7dGocPHzZ7LLVr10ZYWJjeumr9+/dHx44d8fDhQ0ybNg2+vr64deuW2WNLLw5LGZEyWYNnb3pu0ioD79KlC3bt2gWZTIaxY8diypQpsLbmHwMREQA0a9YMq1atgkqlwsWLF+Hn5weZTIbvv/9e6tBM5t69e6hTpw7c3NwwZ84clCtXDiqVCvv378fgwYNx48YNs8Zja2sLLy8v3ePY2Fg8ffoUTZs2Rd68eXXHP3VfQ5VKZbJ969hzY0T3nsdBAxmcFNbwSmOzzHHjxiFfvnzYt28fZsyYwcSGiOgdCoUCXl5e8Pb2Rrt27eDj44ODBw/qnn/+/Dm6du2KfPnywcHBAeXKlcOmTZv07vH555/jm2++wahRo+Du7g4vLy9MnjxZ75zbt2+jfv36sLOzQ5kyZfReQ+vKlSto1KgR7O3tkTNnTnz99deIjY3VPd+rVy+0a9cOM2fOhKenJ9zc3DB16lQkJydj5MiRcHd3R/78+bFq1aoPvudBgwZBJpPh3Llz6NChA0qUKIGyZcvC398f//zzT5rXjR49GiVKlICDgwOKFCmCiRMnQqVS6Z6/fPkyGjZsCGdnZ7i4uKBKlSq4cOECAOD+/fto3bo1cuTIAUdHR5QtWxZ79uwBoD8sdezYMTg7vx6JaNSoEWQyGY4dO5bqsNQff/yBypUrw8HBARUrVtS1hZa2GrhNmzZwdHTEjBkzPtgun4LfrEYU8mbbhWK5HXWVUvHx8Th//jwaNGgAAKhRowbu3LkDhcI4G2oSEaVXXFxcms9ZWVnp7cL8oXPlcrne/9rTOtfR0TEDUb7133//4e+//0bBggV1xxITE1GlShWMHj0aLi4u2L17N3r06IGiRYuievXquvPWrFkDf39/nD17FmfOnEGvXr1Qq1Yt1KhRAxqNBu3bt4enpyfOnj2LqKgofPvtt3qvHRcXh6ZNm6JWrVo4f/48nj59iq+++gpDhgzB6tWrdecdOXIE+fPnx4kTJ3D69Gn07dsXf//9N+rXr4+zZ89iy5Yt6N+/Pxo3boz8+fOneI8vXrzQ/Wc3tfb60LwWZ2dnrF69Gnnz5sWVK1fQr18/ODs7Y9SoUQCA7t27o1KlSli8eDGsrKwQHBys6ykZPHgwlEolTpw4AUdHR1y7dg1OTilHHGrXro2bN2+iZMmS2L59O2rXrg13d3fcu3dP77yTJ0+iZ8+e+OWXX1CnTh1cuXIF/v7+kMlkCAgI0J03efJkzJ49G/PmzTPtf+5FNhMVFSUAiKioKKPf+8d910TB0X+JEYGXhBBCXL16VZQtW1bY2dmJy5cvG/31siulUil27twplEql1KFYNLazeZiinRMSEsS1a9dEQkKC3nEAaf60aNFC71wHB4c0z23QoIHeuR4eHqmeZyg/Pz9hZWUlHB0dhUKhEACEXC4X27Zt++B1LVu2FN99953ucYMGDUTdunX1zqlWrZoYNWqUePnypdi7d6+wtrYWjx8/1j2/d+9eAUDs2LFDCCHE0qVLRY4cOURsbKzunN27dwu5XC7Cw8N18RYsWFCo1WrdOSVLlhT16tXTPU5OThaOjo5i06ZNqcZ+9uxZAUD8/vvvH2kdoRdfaubMmSOqVKmie+zs7CxWr16d6rnlypUTkydPTvW5o0ePCgDi5cuXQgghXr58KQCIo0eP6s5ZtWqVcHV11T3+4osvxMyZM4UQQqjVavHy5UuxZs0akSdPHr34v/322w++x7Q+u0IY9v2dKYalFi5ciEKFCsHOzg41atTAuXPnPnj+1q1bUapUKdjZ2aFcuXK6rjSpaTfMLOrhgFWrVqFq1aq4evUq3NzcEB0dLXF0RESZX8OGDREcHIyzZ8/Cz88PvXv31tt+Rq1WY9q0aShXrhzc3d3h5OSE/fv348GDB3r3KV++vN7jPHny6FZ/v3HjBry9vfXmj9SqVUvv/OvXr6NChQp6vSl16tSBRqPBzZs3dcfKli0LufztV6mnpyfKlSune2xlZYWcOXPqXvt94s36ZhmxZcsW1KlTB15eXnBycsKECRP02sHf3x9fffUVfHx8MHv2bNy5c0f33DfffIPp06ejTp06CAgIwL///pvhOIDXQ2BTp06Fk5MTXFxckD9/fvTv3x9hYWGIj4/XnVe1atVPep30kjy52bJlC/z9/REQEICgoCBUqFABTZs2TfOD8Pfff6Nr167o27cvLl26hHbt2qFdu3b477//zBx5SiFPY6FRJuD3eePRp08fJCQkoHHjxggODkbdunWlDo+IsrnY2Ng0f7Zv36537tOnT9M8d+/evXrn3rt3L9XzMsLR0RHFihVDhQoVsHLlSpw9exYrVqzQPT9nzhzMnz8fo0ePxtGjRxEcHIymTZtCqVTq3ef9iaoymQwajSZDMX1Iaq9jyGsXL14cMpnM4EnDZ86cQffu3dGiRQv89ddfuHTpEsaPH6/XDpMnT8bVq1fRsmVLHDlyBGXKlMGOHTsAAF999RXu3r2LHj164MqVK6hatSoWLFhgUAzvio2NxZQpUxAcHIygoCCcOHECly9fxu3bt/WGOz91qDK9JE9u5s6di379+qF3794oU6YMlixZAgcHB6xcuTLV8+fPn49mzZph5MiRKF26NKZNm4bKlSvj119/NXPk+pTJGty8fhVha4bj4K5tkMvlmD59Ovbt2wdPT09JYyMiAl5/saT18+4X0MfOfb9KJq3zPpVcLse4ceMwYcIEJCS8LkU9ffo02rZtiy+//BIVKlRAkSJFDC5JLlWqFB4+fIiwsDDdsfcn7pYuXRqXL1/Wm090+vRpyOVylCxZ8hPelT53d3c0bdoUCxcuTHXu0rtrzbxLOxdp/PjxqFq1KooXL4779++nOK9EiRIYPnw4Dhw4gPbt2+tNbvb29saAAQPw+++/47vvvsOyZcsy/D4qV66MmzdvolixYihWrBiKFCmi+/27PVvmIumEYqVSiYsXL2Ls2LG6Y3K5HD4+Pjhz5kyq15w5cwb+/v56x5o2bZrmwkZJSUlISkrSPdYOD6lUKr1Z5Z8q5GksYm6eQfKLR8iTJw/Wr1+PevXqQa1WQ61WG+11CLo/N2P++VFKbGfzMEU7q1QqCCGg0WhM0lthKkIIXdxaHTp0wMiRI/Hrr7/iu+++Q7FixbB9+3acOnUKOXLkwM8//4yIiAiULl1a77r37/Pu8M8XX3yBEiVKoGfPnvjhhx8QHR2N8ePHA4Cuzbp27YqAgAD07NkTAQEBePbsGYYOHYovv/wSuXLlgkajSTXe1F47rWNaCxYsQL169VC9enVMnjwZ5cuXR3JyMg4dOoQlS5bg6tWrunO18RUtWhQPHjzAxo0bUa1aNezZs0fXK6PRaJCQkIBRo0ahQ4cOKFy4MB49eoTz58+jffv20Gg0GD58OJo1a4YSJUrg5cuXOHr0KEqVKqX3mdH+/v3H2t+/++uECRPQpk0beHt7o3379khISMCdO3dw9epVTJs2LUX8adG2q0qlgpWVld5zhvwdkTS5iYyMhFqtTtGz4enpmWYXXXh4eKrnh4eHp3r+rFmzMGXKlBTHDxw4AAcHhwxGntLtKBny1OsEV6tkTOvTAjExMZlmLpClSq10k4yP7Wwexmxna2treHl5ITY2NsVwTWamUqmQnJycYo5i37598cMPP6Bbt2745ptvcOvWLTRv3hz29vbw8/NDixYtEB0drbsuOTkZSqVS7z7Jycm6L8e4uDisWbMGQ4cORc2aNVGgQAHMnj0bHTt2REJCgu66rVu3YuzYsahRowbs7e3Rpk0bTJ8+Xe8/ye/Hm9prazQaJCYmpjn30sPDA0ePHsVPP/2E7777DhEREfDw8ECFChUwZ84cveu08X3++ecYOHAghg4dCqVSicaNG2PEiBGYPXs2oqOjoVQqER4ejp49e+LZs2fImTMnWrVqBX9/f0RHRyMhIQGDBw/GkydP4OzsjC+++AIzZ85EdHS0bo5MTEwM5HI5YmJiALyu/tXGkpiYCCGE7nGtWrWwefNm/PDDD/jhhx9gbW2NEiVKoEePHqnGnxalUomEhAScOHFCr4xc+/rpJROfMpvpEz158gT58uXD33//rTeZa9SoUTh+/DjOnj2b4hpbW1usWbMGXbt21R1btGgRpkyZgoiIiBTnp9Zz4+3tjcjISLi4uBj1/SiVSuzefwitmjU22cJE9PoflIMHD6JxY7azKbGdzcMU7ZyYmIiHDx/qCjXoNSEEYmJi4OzsnK6NjSljPqWdExMTce/ePXh7e6f47EZHR8PDwwNRUVEf/f6WtOfGw8MDVlZWKZKSiIgIvdUR3+Xl5WXQ+QqFItU1ZWxsbEzyD7bCynT3Jn1sZ/NgO5uHMdtZrVZDJpNBLpdLMt8hs9IOh2jbhkzjU9pZLpfrJmW///fBkL8fkv7p2traokqVKnp7Z2g0Ghw+fDhFWZ5WrVq1Uuy1cfDgwTTPJyIiouxF8hWK/f394efnh6pVq6J69eqYN28e4uLi0Lt3bwBAz549kS9fPsyaNQsAMGzYMDRo0AA//fQTWrZsic2bN+PChQtYunSplG+DiIiIMgnJkxtfX188e/YMkyZNQnh4OCpWrKhXPv3gwQO9bq3atWtj48aNmDBhAsaNG4fixYtj586d+Oyzz6R6C0RERJSJSJ7cAMCQIUMwZMiQVJ87duxYimOdOnVCp06dTBwVERERZUWcUUVEZKEkLIYlyhBjfWaZ3BARWRhtVYkh64IQZQbadZneX8DPUJliWIqIiIzHysoKbm5uuj36HBwcuK4LXlfjKpVKJCYmshTchDLazhqNBs+ePYODgwOsrT8tPWFyQ0RkgbRrf6W1CXF2JIRAQkIC7O3tmeyZ0Ke0s1wuR4ECBT75z4fJDRGRBZLJZMiTJw9y587N/cHeUKlUOHHiBOrXr8+FKU3oU9rZ1tbWKL1qTG6IiCyYlZXVJ89fsBRWVlZITk6GnZ0dkxsTygztzEFHIiIisihMboiIiMiiMLkhIiIii5Lt5txoFwiKjo42+r1VKhXi4+MRHR3N8VwTYjubB9vZPNjO5sO2Ng9TtbP2ezs9C/1lu+QmJiYGAODt7S1xJERERGSomJgYuLq6fvAcmchm63NrNBo8efIEzs7ORl/nIDo6Gt7e3nj48CFcXFyMem96i+1sHmxn82A7mw/b2jxM1c5CCMTExCBv3rwfLRfPdj03crkc+fPnN+lruLi48C+OGbCdzYPtbB5sZ/NhW5uHKdr5Yz02WpxQTERERBaFyQ0RERFZFCY3RqRQKBAQEACFQiF1KBaN7WwebGfzYDubD9vaPDJDO2e7CcVERERk2dhzQ0RERBaFyQ0RERFZFCY3REREZFGY3BAREZFFYXJjoIULF6JQoUKws7NDjRo1cO7cuQ+ev3XrVpQqVQp2dnYoV64c9uzZY6ZIszZD2nnZsmWoV68ecuTIgRw5csDHx+ejfy70mqGfZ63NmzdDJpOhXbt2pg3QQhjazq9evcLgwYORJ08eKBQKlChRgv92pIOh7Txv3jyULFkS9vb28Pb2xvDhw5GYmGimaLOmEydOoHXr1sibNy9kMhl27tz50WuOHTuGypUrQ6FQoFixYli9erXJ44SgdNu8ebOwtbUVK1euFFevXhX9+vUTbm5uIiIiItXzT58+LaysrMQPP/wgrl27JiZMmCBsbGzElStXzBx51mJoO3fr1k0sXLhQXLp0SVy/fl306tVLuLq6ikePHpk58qzF0HbWCg0NFfny5RP16tUTbdu2NU+wWZih7ZyUlCSqVq0qWrRoIU6dOiVCQ0PFsWPHRHBwsJkjz1oMbecNGzYIhUIhNmzYIEJDQ8X+/ftFnjx5xPDhw80cedayZ88eMX78ePH7778LAGLHjh0fPP/u3bvCwcFB+Pv7i2vXrokFCxYIKysrsW/fPpPGyeTGANWrVxeDBw/WPVar1SJv3rxi1qxZqZ7fuXNn0bJlS71jNWrUEP379zdpnFmdoe38vuTkZOHs7CzWrFljqhAtQkbaOTk5WdSuXVssX75c+Pn5MblJB0PbefHixaJIkSJCqVSaK0SLYGg7Dx48WDRq1EjvmL+/v6hTp45J47Qk6UluRo0aJcqWLat3zNfXVzRt2tSEkQnBYal0UiqVuHjxInx8fHTH5HI5fHx8cObMmVSvOXPmjN75ANC0adM0z6eMtfP74uPjoVKp4O7ubqows7yMtvPUqVORO3du9O3b1xxhZnkZaeddu3ahVq1aGDx4MDw9PfHZZ59h5syZUKvV5go7y8lIO9euXRsXL17UDV3dvXsXe/bsQYsWLcwSc3Yh1fdgtts4M6MiIyOhVqvh6empd9zT0xM3btxI9Zrw8PBUzw8PDzdZnFldRtr5faNHj0bevHlT/IWitzLSzqdOncKKFSsQHBxshggtQ0ba+e7duzhy5Ai6d++OPXv2ICQkBIMGDYJKpUJAQIA5ws5yMtLO3bp1Q2RkJOrWrQshBJKTkzFgwACMGzfOHCFnG2l9D0ZHRyMhIQH29vYmeV323JBFmT17NjZv3owdO3bAzs5O6nAsRkxMDHr06IFly5bBw8ND6nAsmkajQe7cubF06VJUqVIFvr6+GD9+PJYsWSJ1aBbl2LFjmDlzJhYtWoSgoCD8/vvv2L17N6ZNmyZ1aGQE7LlJJw8PD1hZWSEiIkLveEREBLy8vFK9xsvLy6DzKWPtrPXjjz9i9uzZOHToEMqXL2/KMLM8Q9v5zp07uHfvHlq3bq07ptFoAADW1ta4efMmihYtatqgs6CMfJ7z5MkDGxsbWFlZ6Y6VLl0a4eHhUCqVsLW1NWnMWVFG2nnixIno0aMHvvrqKwBAuXLlEBcXh6+//hrjx4+HXM7/+xtDWt+DLi4uJuu1Adhzk262traoUqUKDh8+rDum0Whw+PBh1KpVK9VratWqpXc+ABw8eDDN8ylj7QwAP/zwA6ZNm4Z9+/ahatWq5gg1SzO0nUuVKoUrV64gODhY99OmTRs0bNgQwcHB8Pb2Nmf4WUZGPs916tRBSEiILnkEgFu3biFPnjxMbNKQkXaOj49PkcBoE0rBLReNRrLvQZNOV7YwmzdvFgqFQqxevVpcu3ZNfP3118LNzU2Eh4cLIYTo0aOHGDNmjO7806dPC2tra/Hjjz+K69evi4CAAJaCp4Oh7Tx79mxha2srtm3bJsLCwnQ/MTExUr2FLMHQdn4fq6XSx9B2fvDggXB2dhZDhgwRN2/eFH/99ZfInTu3mD59ulRvIUswtJ0DAgKEs7Oz2LRpk7h79644cOCAKFq0qOjcubNUbyFLiImJEZcuXRKXLl0SAMTcuXPFpUuXxP3794UQQowZM0b06NFDd762FHzkyJHi+vXrYuHChSwFz4wWLFggChQoIGxtbUX16tXFP//8o3uuQYMGws/PT+/8wMBAUaJECWFrayvKli0rdu/ebeaIsyZD2rlgwYICQIqfgIAA8weexRj6eX4Xk5v0M7Sd//77b1GjRg2hUChEkSJFxIwZM0RycrKZo856DGlnlUolJk+eLIoWLSrs7OyEt7e3GDRokHj58qX5A89Cjh49muq/t9q29fPzEw0aNEhxTcWKFYWtra0oUqSIWLVqlcnjlAnB/jciIiKyHJxzQ0RERBaFyQ0RERFZFCY3REREZFGY3BAREZFFYXJDREREFoXJDREREVkUJjdERERkUZjcEJGe1atXw83NTeowMkwmk2Hnzp0fPKdXr15o166dWeIhIvNjckNkgXr16gWZTJbiJyQkROrQsHr1al08crkc+fPnR+/evfH06VOj3D8sLAzNmzcHANy7dw8ymQzBwcF658yfPx+rV682yuulZfLkybr3aWVlBW9vb3z99dd48eKFQfdhIkZkOO4KTmShmjVrhlWrVukdy5Url0TR6HNxccHNmzeh0Whw+fJl9O7dG0+ePMH+/fs/+d4f2z0eAFxdXT/5ddKjbNmyOHToENRqNa5fv44+ffogKioKW7ZsMcvrE2VX7LkhslAKhQJeXl56P1ZWVpg7dy7KlSsHR0dHeHt7Y9CgQYiNjU3zPpcvX0bDhg3h7OwMFxcXVKlSBRcuXNA9f+rUKdSrVw/29vbw9vbGN998g7i4uA/GJpPJ4OXlhbx586J58+b45ptvcOjQISQkJECj0WDq1KnInz8/FAoFKlasiH379umuVSqVGDJkCPLkyQM7OzsULFgQs2bN0ru3dliqcOHCAIBKlSpBJpPh888/B6DfG7J06VLkzZtXbxduAGjbti369Omje/zHH3+gcuXKsLOzQ5EiRTBlyhQkJyd/8H1aW1vDy8sL+fLlg4+PDzp16oSDBw/qnler1ejbty8KFy4Me3t7lCxZEvPnz9c9P3nyZKxZswZ//PGHrhfo2LFjAICHDx+ic+fOcHNzg7u7O9q2bYt79+59MB6i7ILJDVE2I5fL8csvv+Dq1atYs2YNjhw5glGjRqV5fvfu3ZE/f36cP38eFy9exJgxY2BjYwMAuHPnDpo1a4YOHTrg33//xZYtW3Dq1CkMGTLEoJjs7e2h0WiQnJyM+fPn46effsKPP/6If//9F02bNkWbNm1w+/ZtAMAvv/yCXbt2ITAwEDdv3sSGDRtQqFChVO977tw5AMChQ4cQFhaG33//PcU5nTp1wvPnz3H06FHdsRcvXmDfvn3o3r07AODkyZPo2bMnhg0bhmvXruG3337D6tWrMWPGjHS/x3v37mH//v2wtbXVHdNoNMifPz+2bt2Ka9euYdKkSRg3bhwCAwMBACNGjEDnzp3RrFkzhIWFISwsDLVr14ZKpULTpk3h7OyMkydP4vTp03ByckKzZs2gVCrTHRORxTL51pxEZHZ+fn7CyspKODo66n46duyY6rlbt24VOXPm1D1etWqVcHV11T12dnYWq1evTvXavn37iq+//lrv2MmTJ4VcLhcJCQmpXvP+/W/duiVKlCghqlatKoQQIm/evGLGjBl611SrVk0MGjRICCHE0KFDRaNGjYRGo0n1/gDEjh07hBBChIaGCgDi0qVLeue8v6N527ZtRZ8+fXSPf/vtN5E3b16hVquFEEJ88cUXYubMmXr3WLdunciTJ0+qMQghREBAgJDL5cLR0VHY2dnpdk+eO3dumtcIIcTgwYNFhw4d0oxV+9olS5bUa4OkpCRhb28v9u/f/8H7E2UHnHNDZKEaNmyIxYsX6x47OjoCeN2LMWvWLNy4cQPR0dFITk5GYmIi4uPj4eDgkOI+/v7++Oqrr7Bu3Trd0ErRokUBvB6y+vfff7Fhwwbd+UIIaDQahIaGonTp0qnGFhUVBScnJ2g0GiQmJqJu3bpYvnw5oqOj8eTJE9SpU0fv/Dp16uDy5csAXg8pNW7cGCVLlkSzZs3QqlUrNGnS5JPaqnv37ujXrx8WLVoEhUKBDRs2oEuXLpDL5br3efr0ab2eGrVa/cF2A4CSJUti165dSExMxPr16xEcHIyhQ4fqnbNw4UKsXLkSDx48QEJCApRKJSpWrPjBeC9fvoyQkBA4OzvrHU9MTMSdO3cy0AJEloXJDZGFcnR0RLFixfSO3bt3D61atcLAgQMxY8YMuLu749SpU+jbty+USmWqX9KTJ09Gt27dsHv3buzduxcBAQHYvHkz/ve//yE2Nhb9+/fHN998k+K6AgUKpBmbs7MzgoKCIJfLkSdPHtjb2wMAoqOjP/q+KleujNDQUOzduxeHDh1C586d4ePjg23btn302rS0bt0aQgjs3r0b1apVw8mTJ/Hzzz/rno+NjcWUKVPQvn37FNfa2dmleV9bW1vdn8Hs2bPRsmVLTJkyBdOmTQMAbN68GSNGjMBPP/2EWrVqwdnZGXPmzMHZs2c/GG9sbCyqVKmil1RqZZZJ40RSYnJDlI1cvHgRGo0GP/30k65XQju/40NKlCiBEiVKYPjw4ejatStWrVqF//3vf6hcuTKuXbuWIon6GLlcnuo1Li4uyJs3L06fPo0GDRrojp8+fRrVq1fXO8/X1xe+vr7o2LEjmjVrhhcvXsDd3V3vftr5LWq1+oPx2NnZoX379tiwYQNCQkJQsmRJVK5cWfd85cqVcfPmTYPf5/smTJiARo0aYeDAgbr3Wbt2bQwaNEh3zvs9L7a2tinir1y5MrZs2YLcuXPDxcXlk2IiskScUEyUjRQrVgwqlQoLFizA3bt3sW7dOixZsiTN8xMSEjBkyBAcO3YM9+/fx+nTp3H+/HndcNPo0aPx999/Y8iQIQgODsbt27fxxx9/GDyh+F0jR47E999/jy1btuDmzZsYM2YMgoODMWzYMADA3LlzsWnTJty4cQO3bt3C1q1b4eXllerCg7lz54a9vT327duHiIgIREVFpfm63bt3x+7du7Fy5UrdRGKtSZMmYe3atZgyZQquXr2K69evY/PmzZgwYYJB761WrVooX748Zs6cCQAoXrw4Lly4gP379+PWrVuYOHEizp8/r3dNoUKF8O+//+LmzZuIjIyESqVC9+7d4eHhgbZt2+LkyZMIDQ3FsWPH8M033+DRo0cGxURkkaSe9ENExpfaJFStuXPnijx58gh7e3vRtGlTsXbtWgFAvHz5UgihP+E3KSlJdOnSRXh7ewtbW1uRN29eMWTIEL3JwufOnRONGzcWTk5OwtHRUZQvXz7FhOB3vT+h+H1qtVpMnjxZ5MuXT9jY2IgKFSqIvXv36p5funSpqFixonB0dBQuLi7iiy++EEFBQbrn8c6EYiGEWLZsmfD29hZyuVw0aNAgzfZRq9UiT548AoC4c+dOirj27dsnateuLezt7YWLi4uoXr26WLp0aZrvIyAgQFSoUCHF8U2bNgmFQiEePHggEhMTRa9evYSrq6twc3MTAwcOFGPGjNG77unTp7r2BSCOHj0qhBAiLCxM9OzZU3h4eAiFQiGKFCki+vXrJ6KiotKMiSi7kAkhhLTpFREREZHxcFiKiIiILAqTGyIiIrIoTG6IiIjIojC5ISIiIovC5IaIiIgsCpMbIiIisihMboiIiMiiMLkhIiIii8LkhoiIiCwKkxsiIiKyKExuiIiIyKIwuSEiIiKL8n/g/TgfLEOBJgAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 927 }, - { - "data": { - "image/png": "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zCmoQwByJSgoSIMHD9bhw4cVHh6uWbNm6f3331ffvn0lSWXLlpWPj49GjRqlf//732rbtq1WrlypFStWqHnz5i6efdFlsyzr3vwst7uwfPlyPf/887py5Yrq16+vFi1aqFevXnr44Ycl3fgX3cqVK9W1a9fbbqdOnTqKiorSiy++KOnGmemjjz6qlStX3vZ2aWlpTiFPTExUlSpV9OHOBvL0LlTPqOeZ4/tTNL77vhyP9/Pz04kTJ/Tqq6/qk08+kST5+vqqbNmyqlChgqKjo1WpUiWFhYVl+UdV7dq19e233+q9997TW2+9laP9vbGirgJre+X8gIqgRbUqu3oK95VE66J+1oZs16elpWnHjh0KCwtzLHvvvffUsGFDNWnSRBUqVFBcXJwWL16sZ555xjHm888/V0pKivr06ZPtth9Xa/naSuXNgRQB1610bdLnunz5snx9fW87ttCcmUo3XjONi4vTF198oXbt2mnTpk2qX7++5s2bl+1tkpOTFR0drZCQEPn7+8vb21sHDx7McmbaoEGDO+5/8uTJ8vPzc/xwZV3eu3z5sn777TcFBwc7liUmJio2NlabN2/W008/rVq1aqlbt25OtwsJCdGGDRs0Z86cHIcUcIX4+HgdOHDAadnBgwdVtWpVSdL58+eVnp5+2zEoeIUqppLk4eGhNm3aaOzYsdq2bZsiIiL0xhtvZDs+OjpaK1eu1KRJk7R582bFxMSobt26unbtmtM4L687n52MHj1aly9fdvzc7nU73B0vLy9Vr15d8fHxt1xvs9lks9lkt//fN72Ehobq22+/1fz58zVmzJiCmipwV7Zu3er09i5Jeuihh3TixAlJUnp6urZv337bMSh4hf65x9DQUMfbYdzd3ZWRkeG0fuvWrYqIiHCcySQnJ2e5ci6n7Ha70x9xmJs6daq+/PJLnThxQhUrVtT48eOVkZGhzz77TNWqVVPPnj21du1anTt3TpUrV9aoUaN05coVrVmzRtKNp3Y3btyob775RtOnT1e5cuUkSRkZGTp//rwrDw24pRkzZmjbtm0aPXq0li5dqscff1yDBg1yuvBx6tSpWrJkib7//nt9++23jms9WrZs6bqJF3GF5sw0ISFBf/rTn7Rw4ULt3btXx44d07JlyzRlyhTHVW6BgYHasGGDTp8+7XjPVo0aNbRixQrFxMRoz5496tOnjzIzC/7rkXBrlStX1meffaZDhw5p6dKlSkhI0P/8z//o/Pnzunr1qpo1a6Y1a9YoNjZWS5YsUVJSkpo0aaJz585Jkp5++mmVLVtWzz33nE6fPu34uXkBGnCv2bFjh7p166bevXvrl19+0dixY/XKK69o8eLFjjGrVq1SVFSURowYoX379mngwIF66qmnHO+pR8ErNGem3t7eatSokWbMmKEjR44oPT1dVapU0fPPP6+//vWvkqRp06bp1Vdf1ccff6xKlSrp+PHjmj59uvr3768mTZqoTJkyGjlypBITE118NLipd+/e2a6Lj49Xhw4dbnv78ePHa/z48Xk9LSBfrV69WqtXr77tmLlz52Z5HzVcp1BdzXuvSUxMlJ+fH1fz3kZur+YtaFzNe2dczZs7d7qaNz9xNW/uFNmreQEAcAViCgCAIWIKAIAhYgoAgCFiCgCAIWIKAIAhYgqX8ilVXO4lbK6exi25l7DJpxRvaULeKiG73Fzwp9dNbiohPqEtv/CXAi5VuqJdk7+pp6SL1109lSx8ShVX6Yr88UHe8rCVVBOrna4p61dF5qcSssvDVrJA91mUEFO4XOmKdqKFIsXDVlIeImyFCU/zAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBgiJgCAGCImAIAYIiYAgBg6K5iunnzZj377LNq3Lix/vOf/0iSFixYoC1btuTp5AAAuB/kOqbLly9XeHi4PD09tXv3bqWlpUmSLl++rEmTJuX5BAEAuNflOqZvvvmmZs+erY8//lju7u6O5WFhYdq1a1eeTg4AgPtBrmN66NAhNW/ePMtyPz8/Xbp0KS/mBADAfSXXMS1fvrxiY2OzLN+yZYuCgoLyZFIAANxPch3T559/XsOGDdNPP/0km82muLg4LVq0SNHR0Ro8eHB+zBEAgHta8dzeYNSoUcrMzFTr1q2Vmpqq5s2by263Kzo6Wi+99FJ+zBEAgHtarmNqs9n0+uuv67XXXlNsbKySk5MVGhoqb2/v/JgfAAD3vFzH9KYSJUooNDQ0L+cCAMB9KdcxbdWqlWw2W7brN27caDQhAADuN7mOab169Zx+T09PV0xMjH755Rf169cvr+YFAMB9I9cxnTFjxi2Xjxs3TsnJycYTAgDgfpNnH3T/7LPP6pNPPsmrzQEAcN+46wuQ/uiHH36Qh4dHXm2uUFnWKEDFbe53Hgjch76J2+HqKQD5IjEpU6UeytnYXMe0e/fuTr9blqX4+Hjt2LFDY8eOze3mAAC47+U6pn5+fk6/u7m5qWbNmpowYYLatm2bZxMDAOB+kauYZmRkKDIyUnXr1lWpUqXya04AANxXcnUBUrFixdS2bVu+HQYAgP+S66t569Spo6NHj+bHXAAAuC/d1ZeDR0dH66uvvlJ8fLwSExOdfgAAKGpy/JrphAkTNHz4cD355JOSpM6dOzt9rKBlWbLZbMrIyMj7WQIAcA/LcUzHjx+vqKgoffvtt/k5HwAA7js5jqllWZKkFi1a5NtkAAC4H+XqNdPbfVsMAABFVa7eZ/rQQw/dMagXLlwwmhAAAPebXMV0/PjxWT4BCQCAoi5XMe3Vq5fKli2bX3MBAOC+lOPXTHm9FACAW8txTG9ezQsAAJzl+GnezMzM/JwHAAD3rVx/nCAAAHBGTAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAwREwBADBETAEAMERMAQAw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3HbChAnWc889Z/n4+Fj9+vXL56NBURIeHm5VrlzZSk5OvuX6zMxMy7Isa9q0aVadOnWskiVLWpUrV7YGDx5sJSUlOcbNnTvX8vPzc/x+p8f2wYMHLU9PT2vRokWOZUuWLLE8PDys/fv3W999951VvHhxKz4+3mk+w4YNs5o2bZoXh15k8TQv7lne3t7y9vbWqlWrlJaWlmX99u3bJUlz585VfHy84/fk5GQ9+eST2rBhg3bv3q127dqpU6dOOnnypNPt33nnHT3yyCPavXu3xo4dm/8HhCIhISFBa9eu1dChQ+Xl5XXLMTe/+MLNzU3vv/++9u/fr/nz52vjxo0aMWJEttu+02O7Vq1aeueddzRkyBCdPHlSv//+u6KiovT2228rNDRUzZs3V1BQkBYsWODYZnp6uhYtWqT+/fvn4b1QBLm65sDt/Otf/7JKlSpleXh4WE2aNLFGjx5t7dmzx7FekrVy5co7bqd27drWzJkzHb8HBARYXbt2zY8po4j78ccfLUnWihUrnJaXLl3a8vLysry8vKwRI0bc8rbLli2zSpcu7fj9j2emt/LHx7ZlWVaHDh2sZs2aWa1bt7batm3rOBO2LMt6++23rZCQEMfvy5cvt7y9vbM9i0bOcGaKe9pTTz2luLg4ffHFF2rXrp02bdqk+vXra968edneJjk5WdHR0QoJCZG/v7+8vb118ODBLGemDRo0yOfZA//n559/VkxMjGrXru14pmX9+vVq3bq1KlWqJB8fHz333HNKSEjI9gKlnD62P/nkE+3du1e7du3SvHnznL4CMiIiQrGxsfrxxx8l3bhiuEePHtmeRSNniCnueR4eHmrTpo3Gjh2rbdu2KSIiQm+88Ua246Ojo7Vy5UpNmjRJmzdvVkxMjOrWratr1645jeOPB/JDcHCwbDabDh065LQ8KChIwcHB8vT0lCQdP35cHTt21MMPP6zly5dr586d+sc//iFJWR6rN+X0sb1nzx6lpKQoJSVF8fHxTuvKli2rTp06ae7cuTpz5oy+/vprnuLNA1zNi/tOaGio4+0w7u7uysjIcFq/detWRUREqFu3bpJu/Gv++PHjBTxLFFWlS5dWmzZt9MEHH+ill17K9h9tO3fuVGZmpqZNm+b44umlS5fedts5eWxfuHBBERERev311xUfH69nnnlGu3btckRckgYOHKjevXur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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "id": "5f5Q78_IARAy", + "outputId": "0dc23cc1-33b8-46d1-bf97-bee6523531bc" + }, + "outputs": [], "source": [ - "# And now run them on your own data! \n", + "# And now run them on your own data!\n", "Eval.ROC_curve(prediction, labels)\n", "Eval.confusion_matrix(prediction, labels)" ] } ], "metadata": { + "colab": { + "provenance": [] + }, "kernelspec": { "display_name": "base", "language": "python", @@ -879,10 +637,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" - }, - "orig_nbformat": 4 + "version": "3.11.13" + } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 0 }