├── .gitignore ├── AlexNet ├── README.md ├── alexnet.py ├── plots │ └── alexnet_metrics.png ├── train_alexnet.ipynb ├── trainer.py └── utils.py ├── LinearRegression ├── eval.ipynb └── linear_regression.py ├── LogisticRegression ├── eval.ipynb └── logistic_regression.py ├── README.md └── ResNet ├── README.md ├── plots ├── baseline_pytorch_resnet18_metrics.png └── resnet18_metrics.png ├── resnet18.py ├── train_resnet18.ipynb └── trainer.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /AlexNet/README.md: -------------------------------------------------------------------------------- 1 | # AlexNet Implementation 2 | A toy project to learn, implement, and train the famous AlexNet Architecture from scratch from the 2012 paper 3 | "[ImageNet Classification with Deep Convolutional Neural Networks](https://proceedings.neurips.cc/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf)" simply because we can. 4 | 5 | ## Some Personal Details on Model Training 6 | - Find a suitable batch size for training. Sticking to $128$ in accordance with AlexNet paper. 7 | - Ensure that data preprocessing transformations are appropriate and desired. 8 | 9 | # Results 10 | 11 | The final results after training for ~$30$ epochs are as follows: 12 | `Test Top-1 accuracy: 31.754350662231445 % | Top-5 accuracy: 58.7618670886076` 13 | 14 | ![alexnet results](plots/alexnet_metrics.png) 15 | 16 | Considering that the [original AlexNet paper](https://proceedings.neurips.cc/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf) reports top-1 and top-5 error rate scores of 67.4% and 40.9% 17 | (Top-1 and top-5 accuracies are inversely equivalent being 32.6% and 59.1% respectively) on ImageNet, we can say that the implementation is sufficiently accurate for the Tiny ImageNet dataset. 18 | 19 | 20 | # Acknowledgements 21 | - [ImageNet Classification with Deep Convolutional Neural Networks](https://proceedings.neurips.cc/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf) 22 | - [dansuh17/alexnet-pytorch](https://github.com/dansuh17/alexnet-pytorch) 23 | - [Writing AlexNet from Scratch in PyTorch](https://blog.paperspace.com/alexnet-pytorch/#data-loading) 24 | - https://pytorch.org/hub/pytorch_vision_alexnet/ 25 | - [Difference between AlexNet, VGGNet, ResNet, and Inception](https://towardsdatascience.com/the-w3h-of-alexnet-vggnet-resnet-and-inception-7baaaecccc96) -------------------------------------------------------------------------------- /AlexNet/alexnet.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementing the famous AlexNet Architecture from scratch from the 2012 paper 3 | "ImageNet Classification with Deep Convolutional Neural Networks" 4 | """ 5 | 6 | import torch 7 | import torch.nn as nn 8 | 9 | 10 | class AlexNet(nn.Module): 11 | 12 | def __init__(self, n_classes: int = 1000) -> None: 13 | super().__init__() 14 | 15 | self.features = nn.Sequential( 16 | nn.Conv2d(3, 96, kernel_size=11, stride=4), 17 | nn.ReLU(), 18 | nn.LocalResponseNorm(size=5, alpha=10e-4, beta=0.75, k=2), 19 | nn.MaxPool2d(kernel_size=3, stride=2), 20 | nn.Conv2d(96, 256, kernel_size=5, padding=2), 21 | nn.ReLU(), 22 | nn.LocalResponseNorm(size=5, alpha=10e-4, beta=0.75, k=2), 23 | nn.MaxPool2d(kernel_size=3, stride=2), 24 | nn.Conv2d(256, 384, kernel_size=3, padding=1), 25 | nn.ReLU(), 26 | nn.Conv2d(384, 384, kernel_size=3, padding=1), 27 | nn.ReLU(), 28 | nn.Conv2d(384, 256, kernel_size=3, padding=1), 29 | nn.ReLU(), 30 | nn.MaxPool2d(kernel_size=3, stride=2), 31 | ) 32 | 33 | self.classifier = nn.Sequential( 34 | nn.Dropout(0.5), 35 | nn.Linear(in_features=6 * 6 * 256, out_features=4096), 36 | nn.ReLU(), 37 | nn.Dropout(0.5), 38 | nn.Linear(in_features=4096, out_features=4096), 39 | nn.ReLU(), 40 | nn.Linear(in_features=4096, out_features=n_classes), 41 | ) 42 | 43 | def forward(self, x: torch.Tensor) -> torch.Tensor: 44 | 45 | x = self.features(x) # [bs, 256, 6, 6] 46 | x = x.reshape(x.size(0), -1) # reshape to [bs, 6*6*256] = [bs, 9216] 47 | o = self.classifier(x) 48 | 49 | return o 50 | 51 | 52 | # test basic forward pass of AlexNet 53 | if __name__ == "__main__": 54 | alexnet = AlexNet() 55 | n_params = sum(p.numel() for p in alexnet.parameters()) 56 | n_trainable_params = sum(p.numel() for p in alexnet.parameters() if p.requires_grad) 57 | 58 | print(f"Number of parameters: {n_params}") 59 | print(f"Number of trainable parameters: {n_trainable_params}") 60 | 61 | # paper mentions 224 x 224 but seems to be a mistake? 62 | dummy_image = torch.randn(1, 3, 227, 227) 63 | out = alexnet(dummy_image) 64 | 65 | assert out.shape == (1, 1000), f"Expected shape: (1, 1000) | Actual shape: {out.shape}" 66 | 67 | print(f"\nModel Summary:\n========\n{alexnet}") 68 | -------------------------------------------------------------------------------- /AlexNet/plots/alexnet_metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aandyw/StuffFromScratch/74578e8ccfd4bc40eaa06d9082e48ee6dfb85fb8/AlexNet/plots/alexnet_metrics.png -------------------------------------------------------------------------------- /AlexNet/trainer.py: -------------------------------------------------------------------------------- 1 | """Trainer class for AlexNet""" 2 | 3 | import sys, os 4 | import datetime 5 | import matplotlib.pyplot as plt 6 | from tqdm import tqdm 7 | import logging 8 | 9 | import torch 10 | import torch.nn as nn 11 | import torch.optim as optim 12 | from torch.utils.data import DataLoader 13 | 14 | 15 | class Trainer: 16 | def __init__( 17 | self, 18 | model: nn.Module, 19 | model_name: str = "alexnet", 20 | batch_size: int = 256, 21 | learning_rate: float = 0.01, 22 | weight_decay: float = 0.0005, 23 | momentum: float = 0.9, 24 | num_epochs: int = 30, 25 | check_val_every_n_epoch: int = 1, 26 | device: str = "cpu", 27 | checkpoints_dir: str = "checkpoints", 28 | ) -> None: 29 | """Trainer object to facilitate training and evaluation""" 30 | 31 | self.model = model 32 | self.model_name = model_name 33 | 34 | # training configurations 35 | self.batch_size = batch_size # does nothing; mainly for viz 36 | self.learning_rate = learning_rate 37 | self.momentum = momentum 38 | self.weight_decay = weight_decay 39 | self.num_epochs = num_epochs 40 | self.check_val_every_n_epoch = check_val_every_n_epoch 41 | self.device = device 42 | self.model.to(self.device) 43 | 44 | # set loss function and optimizer 45 | self.criterion = nn.CrossEntropyLoss() 46 | 47 | # SGD used by original alexnet paper 48 | self.optimizer = optim.SGD( 49 | self.model.parameters(), lr=self.learning_rate, momentum=self.momentum, weight_decay=self.weight_decay 50 | ) 51 | 52 | # Decays lr of each parameter group by 0.1 every step_size epochs 53 | self.scheduler = optim.lr_scheduler.StepLR(self.optimizer, step_size=7, gamma=0.1) 54 | 55 | # model metrics 56 | self.train_losses = [] 57 | self.train_accuracies = [] 58 | self.train_top_k_accuracies = [] 59 | self.val_losses = [] 60 | self.val_accuracies = [] 61 | self.val_top_k_accuracies = [] 62 | 63 | # logging info 64 | logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s | %(message)s") 65 | self.logger = logging.getLogger() 66 | 67 | # create checkpoints directory 68 | self.checkpoints_dir = checkpoints_dir 69 | os.makedirs(self.checkpoints_dir, exist_ok=True) # create plots dir 70 | 71 | def train(self, train_dataloader: DataLoader, val_dataloader: DataLoader) -> None: 72 | """Train the AlexNet Model""" 73 | 74 | for epoch in range(self.num_epochs): 75 | self.model.train() # set model to train 76 | 77 | # loss tracking metrics 78 | running_loss = 0.0 79 | running_vloss = 0.0 80 | batch_loss = 0.0 81 | running_acc = 0.0 82 | running_top_k_acc = 0.0 83 | 84 | pbar = tqdm(enumerate(train_dataloader), total=len(train_dataloader)) 85 | 86 | for i, (inputs, labels) in pbar: 87 | inputs, labels = inputs.to(self.device), labels.to(self.device) 88 | 89 | # zero gradients for every batch 90 | self.optimizer.zero_grad() 91 | 92 | # compute predictions + loss 93 | outputs = self.model(inputs) # predicted class 94 | loss = self.criterion(outputs, labels) 95 | 96 | # compute training accuracy 97 | running_acc += self.__accuracy(outputs, labels) 98 | running_top_k_acc += self._top_k_accuracy(outputs, labels, k=5) 99 | 100 | # perform backpropagation 101 | loss.backward() # compute gradients 102 | self.optimizer.step() # update model parameters 103 | 104 | # gather data and report 105 | running_loss += loss.item() 106 | batch_loss += loss.item() 107 | if i % 10 == 0: 108 | batch_loss = batch_loss / 10 # loss per batch 109 | pbar.set_postfix({"loss": round(batch_loss, 5)}) 110 | batch_loss = 0.0 111 | 112 | self.scheduler.step() 113 | 114 | train_accuracy = running_acc / len(train_dataloader) 115 | train_top_k_accuracy = running_top_k_acc / len(train_dataloader) 116 | avg_loss = running_loss / len(train_dataloader) 117 | 118 | prev_loss = self.train_losses[-1][0] if self.train_losses else float("inf") 119 | if avg_loss <= prev_loss: 120 | self.save(epoch + 1, avg_loss) 121 | 122 | self.train_accuracies.append((epoch, train_accuracy.cpu())) 123 | self.train_top_k_accuracies.append((epoch, train_top_k_accuracy)) 124 | self.train_losses.append((epoch, avg_loss)) 125 | 126 | if epoch % self.check_val_every_n_epoch == 0: 127 | self.model.eval() # set model to evaluation 128 | with torch.no_grad(): 129 | running_val_acc = 0 130 | running_val_top_k_acc = 0 131 | for inputs, labels in val_dataloader: 132 | inputs, labels = inputs.to(self.device), labels.to(self.device) 133 | 134 | outputs = self.model(inputs) 135 | loss = self.criterion(outputs, labels) 136 | 137 | running_vloss += loss.item() 138 | 139 | # compute validtion accuracy 140 | running_val_acc += self.__accuracy(outputs, labels) 141 | running_val_top_k_acc += self._top_k_accuracy(outputs, labels, k=5) 142 | 143 | val_top_k_accuracy = running_val_top_k_acc / len(val_dataloader) 144 | self.val_top_k_accuracies.append((epoch, val_top_k_accuracy)) 145 | 146 | val_accuracy = running_val_acc / len(val_dataloader) 147 | self.val_accuracies.append((epoch, val_accuracy.cpu())) 148 | 149 | avg_vloss = running_vloss / len(val_dataloader) 150 | self.val_losses.append((epoch, avg_vloss)) 151 | 152 | self.logger.info( 153 | f"[EPOCH {epoch + 1}] LOSS : train={avg_loss} val={avg_vloss} | ACCURACY (Top-1) : train={train_accuracy} val={val_accuracy} | TOP-5 : train={train_top_k_accuracy} val={val_top_k_accuracy}" 154 | ) 155 | 156 | def test(self, test_dataloader: DataLoader) -> None: 157 | """Test the AlexNet Model""" 158 | 159 | correct = 0 160 | top_5 = 0 161 | 162 | self.model.eval() 163 | with torch.no_grad(): 164 | for inputs, labels in test_dataloader: 165 | inputs, labels = inputs.to(self.device), labels.to(self.device) 166 | outputs = self.model(inputs) 167 | correct += self.__accuracy(outputs, labels) 168 | top_5 += self._top_k_accuracy(outputs, labels, k=5) 169 | 170 | self.logger.info( 171 | f"Test accuracy: {(correct / len(test_dataloader)) * 100} % | Top-5 accuracy: {(top_5 / len(test_dataloader)) * 100}" 172 | ) 173 | 174 | def plot_metrics(self) -> None: 175 | """Create plots for model metrics""" 176 | 177 | os.makedirs("plots", exist_ok=True) # create plots dir 178 | 179 | t_iters, t_loss = list(zip(*self.train_losses)) 180 | _, v_loss = list(zip(*self.val_losses)) 181 | _, acc = list(zip(*self.train_accuracies)) 182 | _, v_acc = list(zip(*self.val_accuracies)) 183 | 184 | fig, ax = plt.subplots(1, 2, figsize=(12, 5)) 185 | fig.suptitle(f"Model: [{self.model_name}]") 186 | 187 | ax[0].set_title(f"Loss Curve (batch_size={self.batch_size}, lr={self.learning_rate}), momentum={self.momentum}") 188 | ax[0].plot(t_iters, t_loss) 189 | ax[0].plot(t_iters, v_loss) 190 | ax[0].set_xlabel("Epochs") 191 | ax[0].set_ylabel("Loss") 192 | ax[0].legend(["Train", "Validation"]) 193 | ax[0].set_xticks(t_iters) 194 | 195 | ax[1].set_title( 196 | f"Accuracy Curve (batch_size={self.batch_size}, lr={self.learning_rate}), momentum={self.momentum}" 197 | ) 198 | ax[1].plot(t_iters, acc) 199 | ax[1].plot(t_iters, v_acc) 200 | ax[1].set_xlabel("Epochs") 201 | ax[1].set_ylabel("Accuracy") 202 | ax[1].legend(["Train", "Validation"]) 203 | ax[1].set_xticks(t_iters) 204 | 205 | fig.savefig(f"plots/{self.model_name}_metrics.png") 206 | plt.show() 207 | 208 | def _top_k_accuracy(self, outputs: torch.Tensor, labels: torch.Tensor, k: int) -> float: 209 | """Top-K accuracy. Top-1 is the equivalent to regular accuracy.""" 210 | 211 | values, indices = torch.topk(outputs, k) 212 | topk_correct = indices.eq(labels.view(-1, 1).expand_as(indices)) 213 | accuracy = topk_correct.sum().item() / labels.size(0) 214 | 215 | return accuracy 216 | 217 | def __accuracy(self, outputs: torch.Tensor, labels: torch.Tensor) -> float: 218 | """Compute accuracy given outputs as logits""" 219 | 220 | preds = torch.argmax(outputs, dim=1) 221 | accuracy = torch.sum(preds == labels) / len(preds) 222 | 223 | return accuracy 224 | 225 | def save(self, epoch: int, loss: float) -> None: 226 | """Save model""" 227 | 228 | time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 229 | checkpoint_path = os.path.join(self.checkpoints_dir, f"{self.model_name}_e{epoch}_{time}.pt") 230 | state = { 231 | "epoch": epoch, 232 | "model": self.model.state_dict(), 233 | "optimizer": self.optimizer.state_dict(), 234 | "loss": loss, 235 | } 236 | torch.save(state, checkpoint_path) 237 | 238 | def load(self, checkpoint_name: str) -> None: 239 | """Load model""" 240 | 241 | checkpoint_path = os.path.join(self.checkpoints_dir, checkpoint_name) 242 | checkpoint = torch.load(checkpoint_path) 243 | self.model.load_state_dict(checkpoint["model"]) 244 | self.optimizer.load_state_dict(checkpoint["optimizer"]) 245 | -------------------------------------------------------------------------------- /AlexNet/utils.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | 6 | def get_device(): 7 | """Get available device""" 8 | 9 | if torch.cuda.is_available(): 10 | print("Using CUDA...") 11 | return torch.device("cuda") 12 | elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): 13 | print("Using MPS...") 14 | return torch.device("mps") 15 | else: 16 | print("Using CPU...") 17 | return torch.device("cpu") 18 | 19 | 20 | def imshow(img): 21 | """Display image""" 22 | 23 | img = img / 2 + 0.5 # unnormalize 24 | npimg = img.numpy() 25 | plt.imshow(np.transpose(npimg, (1, 2, 0))) 26 | plt.show() 27 | -------------------------------------------------------------------------------- /LinearRegression/linear_regression.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy 3 | 4 | 5 | class LinearRegression: 6 | """Linear Regression with Least Squared Error""" 7 | 8 | def __init__(self, epochs=1000, learning_rate=1e-2): 9 | self.epochs = epochs 10 | self.learning_rate = learning_rate 11 | self.W = None # weights 12 | self.b = None # bias 13 | self.losses = [] 14 | 15 | def __compute_loss(self, y, y_pred): 16 | # Mean Squared Error (MSE) is our cost function 17 | 18 | least_squares = (y_pred - y) ** 2 19 | return np.mean(least_squares) 20 | 21 | def fit(self, X, y): 22 | N, features = X.shape 23 | self.W = np.random.randn(features) 24 | self.b = 0 25 | 26 | for epoch in range(self.epochs): 27 | y_pred = self.predict(X) 28 | loss = self.__compute_loss(y, y_pred) # MSE loss 29 | 30 | ### compute gradients ### 31 | residuals = y_pred - y 32 | grad_W = (2 / N) * np.matmul(X.T, residuals) 33 | grad_b = (2 / N) * np.sum(residuals) 34 | 35 | ### parameter updates ### 36 | self.W -= self.learning_rate * grad_W 37 | self.b -= self.learning_rate * grad_b 38 | self.losses.append(loss) 39 | 40 | if (epoch + 1) % 1000 == 0: 41 | print(f"[Epoch {epoch + 1}/{self.epochs}] Loss: {round(loss, 5)}") 42 | 43 | def predict(self, X): 44 | return np.matmul(X, self.W) + self.b 45 | -------------------------------------------------------------------------------- /LogisticRegression/eval.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Baseline Evaluation with Sklearn" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 15, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "name": "stdout", 17 | "output_type": "stream", 18 | "text": [ 19 | "The autoreload extension is already loaded. To reload it, use:\n", 20 | " %reload_ext autoreload\n" 21 | ] 22 | } 23 | ], 24 | "source": [ 25 | "%load_ext autoreload\n", 26 | "%autoreload 2\n", 27 | "import random\n", 28 | "import numpy as np\n", 29 | "import pandas as pd\n", 30 | "import matplotlib.pyplot as plt\n", 31 | "from sklearn.model_selection import train_test_split\n", 32 | "from sklearn.datasets import load_breast_cancer\n", 33 | "from sklearn.linear_model import LogisticRegression\n", 34 | "from sklearn.metrics import accuracy_score, confusion_matrix, ConfusionMatrixDisplay" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 16, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "def sklearn_to_df(data_loader):\n", 44 | " X_data = data_loader.data\n", 45 | " X_columns = data_loader.feature_names\n", 46 | " X = pd.DataFrame(X_data, columns=X_columns)\n", 47 | "\n", 48 | " y_data = data_loader.target\n", 49 | " label_names = data_loader.target_names\n", 50 | " y = pd.Series(y_data, name='target')\n", 51 | "\n", 52 | " return X, y, label_names\n", 53 | "\n", 54 | "X, y, label_names = sklearn_to_df(load_breast_cancer())\n", 55 | "\n", 56 | "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 17, 62 | "metadata": {}, 63 | "outputs": [ 64 | { 65 | "name": "stderr", 66 | "output_type": "stream", 67 | "text": [ 68 | "/Users/andy/miniconda3/envs/40.319/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:460: ConvergenceWarning: lbfgs failed to converge (status=1):\n", 69 | "STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.\n", 70 | "\n", 71 | "Increase the number of iterations (max_iter) or scale the data as shown in:\n", 72 | " https://scikit-learn.org/stable/modules/preprocessing.html\n", 73 | "Please also refer to the documentation for alternative solver options:\n", 74 | " https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression\n", 75 | " n_iter_i = _check_optimize_result(\n" 76 | ] 77 | }, 78 | { 79 | "data": { 80 | "text/html": [ 81 | "
LogisticRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" 82 | ], 83 | "text/plain": [ 84 | "LogisticRegression()" 85 | ] 86 | }, 87 | "execution_count": 17, 88 | "metadata": {}, 89 | "output_type": "execute_result" 90 | } 91 | ], 92 | "source": [ 93 | "baseline_model = LogisticRegression(max_iter=100)\n", 94 | "baseline_model.fit(X_train, y_train)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 18, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "def evaluate(model):\n", 104 | " y_pred = model.predict(X_test)\n", 105 | "\n", 106 | " accuracy = accuracy_score(y_test, y_pred)\n", 107 | " conf_matrix = confusion_matrix(y_test, y_pred)\n", 108 | "\n", 109 | " print(f'Accuracy: {accuracy*100:.2f}%')\n", 110 | "\n", 111 | " disp = ConfusionMatrixDisplay(confusion_matrix=conf_matrix, display_labels=label_names)\n", 112 | " disp.plot(cmap=plt.cm.Blues) # You can change the color map if you like\n", 113 | " plt.title('Confusion Matrix')\n", 114 | " plt.show()\n", 115 | "\n", 116 | "def plot_losses(losses):\n", 117 | " plt.plot(losses)\n", 118 | " plt.title('Training Loss')\n", 119 | " plt.xlabel('Epoch')\n", 120 | " plt.ylabel('Loss')\n", 121 | " plt.show()" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 19, 127 | "metadata": {}, 128 | "outputs": [ 129 | { 130 | "name": "stdout", 131 | "output_type": "stream", 132 | "text": [ 133 | "Accuracy: 95.61%\n" 134 | ] 135 | }, 136 | { 137 | "data": { 138 | "image/png": "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L79U7dq1NWbMGDVs2FANGzbUnDlzrnvd6dOnr/k5Co5f6erPUjBfyJ7PYrFYNGTIEL3//vtasGCBGjdurLvuuqvIc3fu3Knu3btLurxKbMuWLUpKStKkSZPsvm9Rn/N6MQ4ePFgXL15UYGAgc1+AayCBASqQq6urunTpot27dxeahFuUgl/iKSkphY79+uuvqlWrlsNi8/T0lCRlZ2fbtF89z0aS7rrrLn3yySc6d+6ctm/froiICMXExGj58uXX7N/f3/+an0OSQz/LlQYPHqxTp05pwYIFGjJkyDXPW758udzd3fXpp5/qkUceUWRkpNq3b1+iexY1GfpaUlJSNGbMGLVu3VqnT5/W888/X6J7AmZHAgNUsIkTJ8owDA0fPrzISa85OTn65JNPJEn33nuvJFkn4RZISkrSoUOH1KVLF4fFVbCSZt++fTbtBbEUxdXVVR07dtSbb74pSdqzZ881z+3SpYvWr19vTVgKvPfee/L29i6zJcZ16tTRCy+8oF69emnQoEHXPM9iscjNzU2urq7WtqysLC1durTQuY6qauXl5emxxx6TxWLR559/rvj4eM2dO1crV64sdd+A2fAcGKCCRUREaP78+YqOjla7du00evRotWjRQjk5Odq7d68WLlyoW2+9Vb169VKTJk00YsQIzZ07Vy4uLurRo4dOnDihl19+WSEhIXr22WcdFtcDDzygmjVratiwYZo6darc3NyUkJCg5ORkm/MWLFig9evXq2fPnqpXr54uXrxoXenTtWvXa/Y/efJkffrpp7rnnnv0yiuvqGbNmvrggw/0n//8R9OnT5efn5/DPsvVXnvttRue07NnT82cOVP9+/fXiBEjdPr0ab3++utFLnVv2bKlli9frn/84x9q0KCBPD09SzRvZfLkydq8ebPWrl2rwMBAPffcc9q0aZOGDRumNm3aKCwszO4+AbMigQEqgeHDh6tDhw6aNWuWpk2bptTUVLm7u6tx48bq37+/nnrqKeu58+fPV8OGDfXOO+/ozTfflJ+fn+6//37Fx8cXOeelpHx9fbVmzRrFxMTo8ccfV/Xq1fXkk0+qR48eevLJJ63ntW7dWmvXrtXkyZOVmpqqatWq6dZbb9Xq1autc0iK0qRJE23dulUvvviixowZo6ysLDVr1kxLliyx64m2ZeXee+/Vu+++q2nTpqlXr16qU6eO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", 139 | "text/plain": [ 140 | "
" 141 | ] 142 | }, 143 | "metadata": {}, 144 | "output_type": "display_data" 145 | } 146 | ], 147 | "source": [ 148 | "evaluate(baseline_model)" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "# Evaluating My Implementation" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 20, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "from logistic_regression import LogisticRegression as MyLogisticRegression" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 40, 170 | "metadata": {}, 171 | "outputs": [ 172 | { 173 | "name": "stdout", 174 | "output_type": "stream", 175 | "text": [ 176 | "[Epoch 100/1000] Loss: 12.93496\n", 177 | "[Epoch 200/1000] Loss: 3.32343\n", 178 | "[Epoch 300/1000] Loss: 3.37494\n", 179 | "[Epoch 400/1000] Loss: 10.7662\n", 180 | "[Epoch 500/1000] Loss: 2.17328\n", 181 | "[Epoch 600/1000] Loss: 2.27783\n", 182 | "[Epoch 700/1000] Loss: 2.49438\n", 183 | "[Epoch 800/1000] Loss: 2.68703\n", 184 | "[Epoch 900/1000] Loss: 2.40905\n", 185 | "[Epoch 1000/1000] Loss: 2.41401\n" 186 | ] 187 | } 188 | ], 189 | "source": [ 190 | "# set seed for reproducibility\n", 191 | "np.random.seed(0)\n", 192 | "\n", 193 | "my_model = MyLogisticRegression()\n", 194 | "my_model.fit(X_train.values, y_train)" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 41, 200 | "metadata": {}, 201 | "outputs": [ 202 | { 203 | "name": "stdout", 204 | "output_type": "stream", 205 | "text": [ 206 | "Accuracy: 94.74%\n" 207 | ] 208 | }, 209 | { 210 | "data": { 211 | "image/png": "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YmI0bNgwfffdd1ftNysrq8jyNdxYhmFo3MwVat8yTE0b1KzocACb1atVXU88eKeOpvyqB0e8qcUr/qvJox5S7/vaSpK+P56mn079pvHD/yZ/Xy+5VXJVbP/OCqrur8AAqs3OykUWuVjKuNlQgzEMo9htwIAB1jYWi0VxcXFKTU3V5cuXtWnTJusqJVuYeggpJCREM2fOlMViUaNGjbR//37NnDlTd999tz744AOdPHlSNWte+WH0/PPPa82aNVq8eLHi4+MlSTk5OZo/f77Cwq7838fTTz+tSZMmFXuvzMxMJSUlaenSperYsaOkKw/wKej/j+677z7FxMRIksaMGaOZM2dq48aNaty4cbF9JyQkaOLEiWX7ZqBMXpi6XAd+OKXVC5+r6FCAUnFxsWjvoZ/0ylufSJL2f39SjesH64kH79SHn+1Qbl6++o1ZpDkv99Xx9a8rNzdPG5MPa93Xtj/iHY7jRg8h3UimTmDat29faFlWZGSkpk+frp07d8owDDVs2LBQ+6ysLAUEBFj3vb29rcmLJAUHBys9Pb3Yex09elQ5OTlq27at9Zi/v78aNWpUpG2LFi2sX1ssFgUFBV21X0kaO3asRo4cad3PyMhQSEjIVdvDvka/vlyrv9qvzxbEqlZg1YoOByiVX05nWOe2FPj+eJq6393Suv/Ndym6q+9k+fl4ys2tkn47e17rFj+vvYd+usHRAtdn6gTmWlxdXbVr1y65uroWOl65cmXr125uboXOWSyWq646Kjhe3Nr2Pyuu3/z8/KvG6uHhcd319rA/wzA0+vV/6j8bv9En859V3VrVKzokoNS2f3NU4XVrFDoWVqeGTqb9XqRtxoUr873qh9yiiCZ1FD//0xsSI8qBiUswpk5gtm3bVmQ/PDxcERERysvLU3p6uu6880673CssLExubm7asWOHtTqSkZGhI0eOqEOHDna5B26s56cs178+36ml04aosrenfjl9Ze6RX2VPeXm6V3B0gG3e+mC9Pn9nlEYO6KKVX+xW62b11P/vd+i5+A+sbXp0jNDpM+d18pff1TSspiaPekj/2bRPG7ZffY4eHJs9nwPjaEydwKSkpGjkyJEaOnSodu/erTlz5mj69Olq2LCh+vbtq379+mn69OmKiIjQ6dOntX79ejVv3lz33Xefzffy9fVV//799cILL6hatWqqUaOGJkyY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oZdTDhgwQA888MANj+P48eOyWCzau3fvVdvUq1dPs2bNKnGfiYmJqlKlSpljs1gs+vjjj8vcD4CiSGAAExkwYIAsFov1hZf169fX888/rwsXLpT7vWfPnq3ExMQStS1J0gEA18JzYACTuffee7V48WLl5ORo8+bNevLJJ3XhwgXNmzevSNucnBy7Pd/G39/fLv0AQElQgQFMxsPDQ0FBQQoJCVGfPn3Ut29f6zBGwbDPu+++q/r168vDw0OGYejcuXMaMmSIatSoIT8/P91999365ptvCvU7efJkBQYGytfXV4MGDdLly5cLnf/zEFJ+fr6mTJmiBg0ayMPDQ3Xq1NFrr70mSQoNDZUkRUREyGKxKDo62nrd4sWLre/aady4sd56661C99mxY4ciIiLk6empNm3aaM+ePTZ/j2bMmKHmzZvLx8dHISEhiomJ0fnz54u0+/jjj9WwYUN5enqqc+fOSklJKXT+k08+UevWreXp6an69etr4sSJys3NtTkeALYjgQFMzsvLSzk5Odb9H374QcuXL9eKFSusQzjdunVTWlqaPvvsM+3atUutWrVSx44d9fvvv0uSli9frgkTJui1117Tzp07FRwcXCSx+LOxY8dqypQpevnll3Xw4EEtXbpUgYGBkq4kIZL0xRdfKDU1VR999JEkaeHChRo3bpxee+01HTp0SPHx8Xr55ZeVlJQk6cp7eO6//341atRIu3btUlxcnJ5//nmbvycuLi5644039O233yopKUnr16/X6NGjC7W5ePGiXnvtNSUlJenrr79WRkaGHnnkEev5zz//XI899pieeeYZHTx4UG+//bYSExOtSRqAcmYAMI3+/fsbPXr0sO5v377dCAgIMB5++GHDMAxjwoQJhpubm5Genm5t8+WXXxp+fn7G5cuXC/UVFhZmvP3224ZhGEZkZKTx1FNPFTrfrl0747bbbiv23hkZGYaHh4excOHCYuM8duyYIcnYs2dPoeMhISHG0qVLCx175ZVXjMjISMMwDOPtt982qlWrZly4cMF6ft68ecX29Ud169Y1Zs6cedXzy5cvNwICAqz7ixcvNiQZ27Ztsx47dOiQIcnYvn27YRiGceeddxrx8fGF+lmyZIkRHBxs3ZdkrFy58qr3BVB6zIEBTObTTz9V5cqVlZubq5ycHPXo0UNz5syxnq9bt65uueUW6/6uXbt0/vx5BQQEFOrn0qVL+vHHHyVJhw4d0lNPPVXofGRkpDZs2FBsDIcOHVJWVpZNb+/+9ddflZKSokGDBmnw4MHW47m5udb5NYcOHdJtt90mb2/vQnHYasOGDYqPj9fBgweVkZGh3NxcXb58WRcuXJCPj48kqVKlSmrTpo31msaNG6tKlSo6dOiQ2rZtq127dik5OblQxSUvL0+XL1/WxYsXC8UIwP5IYACT+etf/6p58+bJzc1NNWvWLDJJt+AHdIH8/HwFBwdr48aNRfoq7VJiLy8vm6/Jz8+XdGUYqV27doXOubq6SpIMO7y67cSJE7rvvvv01FNP6ZVXXlG1atX03//+V4MGDSo01CZdWQb9ZwXH8vPzNXHiRPXs2bNIG09PzzLHCeDaSGAAk/Hx8VGDBg1K3L5Vq1ZKS0tTpUqVVK9evWLbNGnSRNu2bVO/fv2sx7Zt23bVPsPDw+Xl5aUvv/xSTz75ZJHz7u7ukq5ULAoEBgaqVq1aOnr0qPr27Vtsv02bNtWSJUt06dIla5J0rTiKs3PnTuXm5mr69OlycbkyDXD58uVF2uXm5mrnzp1q27atJOnw4cM6e/asGjduLOnK9+3w4cM2fa8B2A8JDHCT69SpkyIjI/XAAw9oypQpatSokU6dOqXPPvtMDzzwgNq0aaNnn31W/fv3V5s2bfSXv/xF77//vg4cOKD69esX26enp6fGjBmj0aNHy93dXXfccYd+/fVXHThwQIMGDVKNGjXk5eWlNWvWqHbt2vL09JS/v7/i4uL0zDPPyM/PT127dlVWVpZ27typM2fOaOTIkerTp4/GjRunQYMG6aWXXtLx48c1bdo0mz5vWFiYcnNzNWfOHHXv3l1ff/215s+fX6Sdm5ubRowYoTfeeENubm56+umn1b59e2tCM378eN1///0KCQlRr1695OLion379mn//v169dVXbf+NAGATViEBNzmLxaLPPvtMd911l5544gk1bNhQjzzyiI4fP25dNdS7d2+NHz9eY8aMUevWrXXixAkNGzbsmv2+/PLLGjVqlMaPH68mTZqod+/eSk9Pl3Rlfskbb7yht99+WzVr1lSPHj0kSU8++aQWLVqkxMRENW/eXB06dFBiYqJ12XXlypX1ySef6ODBg4qIiNC4ceM0ZcoUmz5vy5YtNWPGDE2ZMkW33nqr3n//fSUkJBRp5+3trTFjxqhPnz6KjIyUl5eXli1bZj1/zz336NNPP9W6det0++23q3379poxY4bq1q1rUzwASsdi2GNQGQAA4AaiAgMAAJwOCQwAAHA6JDAAAMDpkMAAAACnQwIDAACcDgkMAABwOiQwAADA6ZDAAAAAp0MCAwAAnA4JDAAAcDokMAAAwOmQwAAAAKfz/wGyfvgruDWQKgAAAABJRU5ErkJggg==", 212 | "text/plain": [ 213 | "
" 214 | ] 215 | }, 216 | "metadata": {}, 217 | "output_type": "display_data" 218 | }, 219 | { 220 | "data": { 221 | "image/png": "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DDMydOrUSYwZM8ZuWpMmTcSkSZN0qsj4UlNTBQCxceNGIYQQVqtVREZGirfeess2T15enggKChIff/yxEEKI9PR04enpKRYvXmybJykpSbi5uYnVq1dr+wc4saysLNGwYUOxZs0a0aNHD1uY4TpWzssvvyy6d+9e4fNc18oYMGCAeOyxx+ymDR48WIwYMUIIwfWslBvDjFLr9dChQwKA2LZtm22erVu3CgDiyJEjDtXM00zVdO3aNezatQt9+vSxm96nTx9s2bJFp6qMLyMjAwAQGhoKADh9+jRSUlLs1rPFYkGPHj1s63nXrl0oKCiwmycqKgotWrTg/6KUsWPHYsCAAbjzzjvtpnMdK2flypXo0KED/vGPfyAiIgJt27bFZ599Znue61oZ3bt3xx9//IFjx44BAPbu3YvNmzejf//+ALie1aLUet26dSuCgoLQuXNn2zy33norgoKCHF73pr/RpNIuXbqEoqIi1KxZ0256zZo1kZKSolNVxiaEwMSJE9G9e3e0aNECAGzrsrz1nJCQYJvHy8sLISEhZebh/6LY4sWLER8fjx07dpR5jutYOadOncK8efMwceJEvPLKK4iLi8Nzzz0Hi8WCkSNHcl0r5OWXX0ZGRgaaNGkCd3d3FBUVYebMmRg6dCgAbtNqUWq9pqSkICIioszyIyIiHF73DDMySZJk91gIUWYaVc24ceOwb98+bN68ucxzctYz/xfFEhMTMX78ePz+++/w9vaucD6uY8dZrVZ06NABs2bNAgC0bdsWBw8exLx58zBy5EjbfFzXjlmyZAkWLlyIRYsWoXnz5tizZw8mTJiAqKgojBo1yjYf17M6lFiv5c2vxLrnaaZqCg8Ph7u7e5kUmZqaWia10s09++yzWLlyJdavX4/o6Gjb9MjISACodD1HRkbi2rVruHLlSoXzuLJdu3YhNTUV7du3h4eHBzw8PLBx40Z88MEH8PDwsK0jrmPH1apVC82aNbOb1rRpU5w9exYAt2elvPjii5g0aRIefvhhtGzZEo888gj+9a9/Yfbs2QC4ntWi1HqNjIzEhQsXyiz/4sWLDq97hplq8vLyQvv27bFmzRq76WvWrEHXrl11qsp4hBAYN24cli9fjnXr1qFevXp2z9erVw+RkZF26/natWvYuHGjbT23b98enp6edvOcP38eBw4c4P8CQO/evbF//37s2bPH9tOhQwcMHz4ce/bsQf369bmOFdKtW7cyQwscO3YMderUAcDtWSk5OTlwc7P/2HJ3d7ddms31rA6l1muXLl2QkZGBuLg42zzbt29HRkaG4+veoe7DLqrk0uwvvvhCHDp0SEyYMEH4+fmJM2fO6F2aYfzzn/8UQUFBYsOGDeL8+fO2n5ycHNs8b731lggKChLLly8X+/fvF0OHDi33UsDo6Gixdu1aER8fL+644w6Xv8SyMqWvZhKC61gpcXFxwsPDQ8ycOVMcP35cfPPNN8LX11csXLjQNg/XteNGjRolateubbs0e/ny5SI8PFy89NJLtnm4nuXJysoSu3fvFrt37xYAxHvvvSd2795tG3JEqfXat29f0apVK7F161axdetW0bJlS16aracPP/xQ1KlTR3h5eYl27drZLimmqgFQ7s/8+fNt81itVjFt2jQRGRkpLBaLuP3228X+/fvtlpObmyvGjRsnQkNDhY+Pjxg4cKA4e/asxn+NcdwYZriOlfPTTz+JFi1aCIvFIpo0aSI+/fRTu+e5rh2XmZkpxo8fL2JjY4W3t7eoX7++mDJlisjPz7fNw/Usz/r168s9Jo8aNUoIodx6vXz5shg+fLgICAgQAQEBYvjw4eLKlSsO1y8JIYRjbTtERERE+mGfGSIiIjI0hhkiIiIyNIYZIiIiMjSGGSIiIjI0hhkiIiIyNIYZIiIiMjSGGSIiIjI0hhkicjmSJGHFihV6l0FECmGYISJNjR49GpIklfnp27ev3qURkUF56F0AEbmevn37Yv78+XbTLBaLTtUQkdGxZYaINGexWBAZGWn3ExISAqD4FNC8efPQr18/+Pj4oF69eli6dKnd6/fv34877rgDPj4+CAsLw1NPPYXs7Gy7eb788ks0b94cFosFtWrVwrhx4+yev3TpEu6//374+vqiYcOGWLlypbp/NBGphmGGiJzOq6++igceeAB79+7FiBEjMHToUBw+fBgAkJOTg759+yIkJAQ7duzA0qVLsXbtWruwMm/ePIwdOxZPPfUU9u/fj5UrV6JBgwZ27/H666/jwQcfxL59+9C/f38MHz4caWlpmv6dRKQQh29VSURUDaNGjRLu7u7Cz8/P7ueNN94QQhTfUX3MmDF2r+ncubP45z//KYQQ4tNPPxUhISEiOzvb9vwvv/wi3NzcREpKihBCiKioKDFlypQKawAgpk6danucnZ0tJEkSq1atUuzvJCLtsM8MEWmuV69emDdvnt200NBQ2+9dunSxe65Lly7Ys2cPAODw4cNo3bo1/Pz8bM9369YNVqsVR48ehSRJSE5ORu/evSutoVWrVrbf/fz8EBAQgNTUVLl/EhHpiGGGiDTn5+dX5rTPzUiSBAAQQth+L28eHx+fKi3P09OzzGutVmu1aiIi58A+M0TkdLZt21bmcZMmTQAAzZo1w549e3D16lXb83/99Rfc3NzQqFEjBAQEoG7duvjjjz80rZmI9MOWGSLSXH5+PlJSUuymeXh4IDw8HACwdOlSdOjQAd27d8c333yDuLg4fPHFFwCA4cOHY9q0aRg1ahSmT5+Oixcv4tlnn8UjjzyCmjVrAgCmT5+OMWPGICIiAv369UNWVhb++usvPPvss9r+oUSkCYYZItLc6tWrUatWLbtpjRs3xpEjRwAUX2m0ePFiPPPMM4iMjMQ333yDZs2aAQB8fX3x22+/Yfz48ejYsSN8fX3xwAMP4L333rMta9SoUcjLy8P777+PF154AeHh4RgyZIh2fyARaUoSQgi9iyAiKiFJEn744Qfcd999epdCRAbBPjNERERkaAwzREREZGjsM0NEToVnvomoutgyQ0RERIbGMENERESGxjBDREREhsYwQ0RERIbGMENERESGxjBDREREhsYwQ0RERIbGMENERESGxjBDREREhvb/+mNKx9wdJFIAAAAASUVORK5CYII=", 222 | "text/plain": [ 223 | "
" 224 | ] 225 | }, 226 | "metadata": {}, 227 | "output_type": "display_data" 228 | } 229 | ], 230 | "source": [ 231 | "evaluate(my_model)\n", 232 | "plot_losses(my_model.losses)" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "Our implementation is a success! However, notice that the LogisticRegression implemented by Sklearn converges much faster than our implementation. This is because it is using a solver called Limited-memory Broyden–Fletcher–Goldfarb–Shanno algorithm (LBFGS) which converges much faster than Gradient Descent (GD) which is what our implementation is using." 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "# Ackowledgements" 247 | ] 248 | }, 249 | { 250 | "cell_type": "markdown", 251 | "metadata": {}, 252 | "source": [ 253 | "A few resources that helped out:\n", 254 | "- https://developer.ibm.com/articles/implementing-logistic-regression-from-scratch-in-python/\n", 255 | "- https://medium.com/@koushikkushal95/logistic-regression-from-scratch-dfb8527a4226" 256 | ] 257 | } 258 | ], 259 | "metadata": { 260 | "kernelspec": { 261 | "display_name": "40.319", 262 | "language": "python", 263 | "name": "python3" 264 | }, 265 | "language_info": { 266 | "codemirror_mode": { 267 | "name": "ipython", 268 | "version": 3 269 | }, 270 | "file_extension": ".py", 271 | "mimetype": "text/x-python", 272 | "name": "python", 273 | "nbconvert_exporter": "python", 274 | "pygments_lexer": "ipython3", 275 | "version": "3.10.8" 276 | } 277 | }, 278 | "nbformat": 4, 279 | "nbformat_minor": 2 280 | } 281 | -------------------------------------------------------------------------------- /LogisticRegression/logistic_regression.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy 3 | 4 | 5 | class LogisticRegression: 6 | 7 | def __init__(self, epochs=1000, learning_rate=1e-2, threshold=0.5): 8 | self.epochs = epochs 9 | self.learning_rate = learning_rate 10 | self.threshold = threshold 11 | self.W = None # weights 12 | self.b = None # bias 13 | self.losses = [] 14 | 15 | def __sigmoid(self, z): 16 | # sigmoid activation function 17 | 18 | # return 1 / (1 + np.exp(-z)) 19 | return scipy.special.expit(z) # handles np.exp(.) overflow 20 | 21 | def __compute_loss(self, y, y_pred, epsilon=1e-9): 22 | # binary cross-entropy loss (BCE) 23 | 24 | # epsilon added to prevent log(0) 25 | return -np.mean(y * np.log(y_pred + epsilon) + (1 - y) * np.log(1 - y_pred + epsilon)) 26 | 27 | def fit(self, X, y): 28 | N, features = X.shape 29 | self.W = np.random.randn(features) 30 | self.b = 0 31 | 32 | for epoch in range(self.epochs): 33 | z = np.matmul(X, self.W) + self.b 34 | y_pred = self.__sigmoid(z) 35 | loss = self.__compute_loss(y, y_pred) 36 | 37 | ### compute gradients ### 38 | residuals = y_pred - y 39 | grad_W = (1 / N) * np.matmul(X.T, residuals) 40 | grad_b = (1 / N) * np.sum(residuals) 41 | 42 | ### parameter updates ### 43 | self.W -= self.learning_rate * grad_W 44 | self.b -= self.learning_rate * grad_b 45 | self.losses.append(loss) 46 | 47 | if (epoch + 1) % 100 == 0: 48 | print(f"[Epoch {epoch + 1}/{self.epochs}] Loss: {round(loss, 5)}") 49 | 50 | def predict(self, X): 51 | z = np.matmul(X, self.W) + self.b 52 | y_pred = self.__sigmoid(z) 53 | y_pred = np.where(y_pred >= self.threshold, 1, 0) 54 | return y_pred 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ツ 2 | 3 | # Implementations 4 | 5 | ## Traditional 6 | |Implementation | Dataset | Notebooks | 7 | | --- | --- | --- | 8 | | Simple Linear Regression | Dummy + Diabetes Dataset (regression) | [![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54)](LinearRegression/eval.ipynb) | 9 | | Logistic Regression | Breast Cancer Wisconsin Dataset | [![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54)](LogisticRegression/eval.ipynb) | 10 | 11 | ## Deep Learning 12 | |Implementation | Dataset | Notebooks | 13 | | --- | --- | --- | 14 | | AlexNet | Tiny ImageNet | [![PyTorch](https://img.shields.io/badge/PyTorch-%23EE4C2C.svg?style=for-the-badge&logo=PyTorch&logoColor=white)](AlexNet/train_alexnet.ipynb) | 15 | | ResNet-18 | CIFAR-10 | [![PyTorch](https://img.shields.io/badge/PyTorch-%23EE4C2C.svg?style=for-the-badge&logo=PyTorch&logoColor=white)](ResNet/train_resnet18.ipynb) | 16 | 17 | ## Computer Vision 18 | |Implementation | Dataset | Notebooks | 19 | | --- | --- | --- | 20 | | U-Net Architecture | SOME RANDOM SEGMENTATION DATASET | [![PyTorch](https://img.shields.io/badge/PyTorch-%23EE4C2C.svg?style=for-the-badge&logo=PyTorch&logoColor=white)](...) | 21 | 22 | 23 | --- 24 | 25 | # Roadmap 26 | - [x] Linear Regression 27 | - [x] Logistic Regression 28 | - [ ] Autoencoder 29 | - [ ] Variational Autoencoder (VAE) 30 | - [ ] Generative Adversarial Network (GAN) 31 | - [ ] Graph Neural Network 32 | - [x] ResNet18 + Residual Layars 33 | - [ ] U-Net Architecture [WIP] 34 | - [x] AlexNet 35 | 36 | ### Sequence Models 37 | - [ ] Recurrent Neural Network (RNN) 38 | - [ ] Long Short-Term Memory (LSTM) 39 | - [ ] Gated Recurrent Unit (GRU) 40 | 41 | 42 | ### Misc 43 | - [ ] Ensembling + XGBoost 44 | 45 | 46 | # Acknowledgements 47 | This readme layout inspired by [rasbt/deeplearning-models](https://github.com/rasbt/deeplearning-models) 48 | -------------------------------------------------------------------------------- /ResNet/README.md: -------------------------------------------------------------------------------- 1 | # ResNet-18 Implementation 2 | A toy project to learn, implement, and train our own ResNet-18 on CIFAR-10. 3 | 4 | 5 | # Results 6 | 7 | ### Existing PyTorch ResNet-18 Model (Baseline) 8 | ![baseline pytorch resnet18](plots/baseline_pytorch_resnet18_metrics.png) 9 | 10 | 11 | ### Our ResNet-18 Implementation 12 | We load the pretrained weights from PyTorch's `ResNet18_Weights.IMAGENET1K_V1`. 13 | 14 | 15 | ![ours resnet18](plots/resnet18_metrics.png) 16 | 17 | 18 | # Acknowledgements 19 | - [PyTorch CIFAR10 Training Tutorial](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html) 20 | - [A Detailed Introduction to ResNet and Its Implementation in PyTorch](https://medium.com/@freshtechyy/a-detailed-introduction-to-resnet-and-its-implementation-in-pytorch-744b13c8074a) by Huili Yu 21 | - [Let's reproduce GPT-2 (124M)](https://www.youtube.com/watch?v=l8pRSuU81PU) by Andrej Karpathy 22 | - [Helpful conventions for PyTorch model building](https://github.com/FrancescoSaverioZuppichini/Pytorch-how-and-when-to-use-Module-Sequential-ModuleList-and-ModuleDict/blob/master/README.md) by FrancescoSaverioZuppichini 23 | 24 | -------------------------------------------------------------------------------- /ResNet/plots/baseline_pytorch_resnet18_metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aandyw/StuffFromScratch/74578e8ccfd4bc40eaa06d9082e48ee6dfb85fb8/ResNet/plots/baseline_pytorch_resnet18_metrics.png -------------------------------------------------------------------------------- /ResNet/plots/resnet18_metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aandyw/StuffFromScratch/74578e8ccfd4bc40eaa06d9082e48ee6dfb85fb8/ResNet/plots/resnet18_metrics.png -------------------------------------------------------------------------------- /ResNet/resnet18.py: -------------------------------------------------------------------------------- 1 | """ 2 | Building an 18-layer residual network (ResNet-18) from scratch. 3 | From the paper "Deep Residual Learning for Image Recognition" (https://arxiv.org/abs/1512.03385) 4 | """ 5 | 6 | import torchvision 7 | import torch 8 | import torch.nn as nn 9 | 10 | 11 | class BasicBlock(nn.Module): 12 | """The Residual Block""" 13 | 14 | def __init__(self, in_channels: int, out_channels: int, stride: int = 1, downsample: bool = False) -> None: 15 | """ 16 | Create the Residual Block 17 | 18 | Args: 19 | in_channels (int): number of input channels 20 | out_channels (int): number of output channels 21 | stride (int): stride of first 3x3 convolution layer 22 | downsample (bool): whether to adjust for spatial dimensions due to downsampling via stride=2 23 | """ 24 | super().__init__() 25 | self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) 26 | self.bn1 = nn.BatchNorm2d(out_channels) 27 | self.relu = nn.ReLU(inplace=True) 28 | self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) 29 | self.bn2 = nn.BatchNorm2d(out_channels) 30 | 31 | # For downsampling, the skip connection will pass through the 1x1 conv layer with stride of 2 to 32 | # match the spatial dimension of the downsampled feature maps and channels for the add operation. 33 | # 34 | # More specifically, the 'downsample block' is used for layer 2, 3, 4 of ResNet18 where the first conv2d 35 | # layer of the BasicBlock uses a stride of 2 instead of 1 to downsample feature maps for a larger 36 | # receptive field. 37 | # This is why we need to carefully craft our 'downsample block' to make sure spatial dimensions are 38 | # not disrupted when we add the skip connection in these residual blocks. 39 | self.downsample = None 40 | if downsample: 41 | self.downsample = nn.Sequential( 42 | nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), 43 | nn.BatchNorm2d(out_channels), 44 | ) 45 | 46 | def forward(self, x: torch.Tensor) -> torch.Tensor: 47 | identity = x.clone() 48 | x = self.relu(self.bn1(self.conv1(x))) 49 | x = self.bn2(self.conv2(x)) 50 | 51 | if self.downsample: # if layer not None 52 | identity = self.downsample(identity) 53 | 54 | x += identity 55 | o = self.relu(x) 56 | 57 | return o 58 | 59 | 60 | class ResNet18(nn.Module): 61 | """The ResNet-18 Model""" 62 | 63 | def __init__(self, n_classes: int = 10) -> None: 64 | """ 65 | Create the ResNet-18 Model 66 | 67 | Args: 68 | n_classes (int, optional): The number of output classes we predict for. Defaults to 10. 69 | """ 70 | super().__init__() 71 | self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=(3, 3), bias=False) 72 | self.bn1 = nn.BatchNorm2d(64) 73 | self.relu = nn.ReLU(inplace=True) 74 | self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) 75 | 76 | self.layer1 = nn.Sequential( 77 | BasicBlock(64, 64), 78 | BasicBlock(64, 64), 79 | ) 80 | self.layer2 = nn.Sequential( 81 | BasicBlock(64, 128, stride=2, downsample=True), 82 | BasicBlock(128, 128), 83 | ) 84 | self.layer3 = nn.Sequential( 85 | BasicBlock(128, 256, stride=2, downsample=True), 86 | BasicBlock(256, 256), 87 | ) 88 | self.layer4 = nn.Sequential( 89 | BasicBlock(256, 512, stride=2, downsample=True), 90 | BasicBlock(512, 512), 91 | ) 92 | self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1, 1)) 93 | 94 | # our fully connected layer will be different to accomodate for CIFAR-10 95 | self.fc = nn.Linear(in_features=512, out_features=n_classes) 96 | 97 | def forward(self, x: torch.Tensor) -> torch.Tensor: 98 | x = self.maxpool(self.relu(self.bn1(self.conv1(x)))) 99 | 100 | x = self.layer1(x) 101 | x = self.layer2(x) 102 | x = self.layer3(x) 103 | x = self.layer4(x) 104 | x = self.avgpool(x) # [bs, 512, 1, 1] 105 | 106 | x = torch.squeeze(x) # reshape to [bs, 512] 107 | o = self.fc(x) 108 | 109 | return o 110 | 111 | @classmethod 112 | def from_pretrained(cls, model_type: str) -> nn.Module: 113 | """ 114 | Load pretrained PyTorch ResNet-18 weights into our ResNet-18 implementation 115 | 116 | Inspired by Andrej Karpathy from 'Let's reproduce GPT-2 (124M)' 117 | (https://www.youtube.com/watch?v=l8pRSuU81PU) 118 | """ 119 | 120 | assert model_type in {"resnet18"}, "only supports resnet18" 121 | print("loading weights from pytorch pretrained resnet18") 122 | 123 | # our model 124 | model = ResNet18(n_classes=10) 125 | r18 = model.state_dict() 126 | r18_keys = r18.keys() 127 | 128 | # pretrained pytorch resnet18 model 129 | p_model = torchvision.models.resnet18(weights=torchvision.models.ResNet18_Weights.IMAGENET1K_V1) 130 | p_r18 = p_model.state_dict() 131 | p_r18_keys = p_r18.keys() 132 | 133 | assert len(p_r18_keys) == len(r18_keys), f"mistmatched keys: {len(p_r18_keys)} != {len(r18_keys)}" 134 | # load weights from pretrained 135 | for k in p_r18_keys: 136 | if k.startswith("fc"): # skip fc layer, we add our own for CIFAR-10 137 | continue 138 | 139 | assert p_r18[k].shape == r18[k].shape 140 | with torch.no_grad(): 141 | r18[k].copy_(p_r18[k]) 142 | 143 | return model 144 | -------------------------------------------------------------------------------- /ResNet/trainer.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import matplotlib.pyplot as plt 3 | from tqdm import tqdm 4 | import logging 5 | 6 | import torch 7 | import torch.nn as nn 8 | import torch.optim as optim 9 | from torch.utils.data import DataLoader 10 | 11 | 12 | class Trainer: 13 | def __init__( 14 | self, 15 | model: nn.Module, 16 | model_name: str = "resnet18", 17 | batch_size: int = 256, 18 | learning_rate: float = 0.01, 19 | num_epochs: int = 30, 20 | check_val_every_n_epoch: int = 1, 21 | device: str = "cpu", 22 | ) -> None: 23 | """Trainer object to facilitate training and evaluation""" 24 | 25 | self.model = model 26 | self.model_name = model_name 27 | 28 | # training configurations 29 | self.batch_size = batch_size # does nothing; mainly for viz 30 | self.learning_rate = learning_rate 31 | self.num_epochs = num_epochs 32 | self.check_val_every_n_epoch = check_val_every_n_epoch 33 | self.device = device 34 | self.model.to(self.device) 35 | 36 | # set loss function and optimizer 37 | self.criterion = nn.CrossEntropyLoss() 38 | 39 | # SGD used by original paper "Deep Residual Learning for Image Recognition" 40 | self.optimizer = optim.SGD(self.model.parameters(), lr=self.learning_rate, momentum=0.9) 41 | self.scheduler = optim.lr_scheduler.StepLR(self.optimizer, step_size=7, gamma=0.1) 42 | 43 | # model metrics 44 | self.train_losses = [] 45 | self.train_accuracies = [] 46 | self.val_losses = [] 47 | self.val_accuracies = [] 48 | 49 | # logging info 50 | logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s | %(message)s") 51 | self.logger = logging.getLogger() 52 | 53 | def train(self, train_dataloader: DataLoader, val_dataloader: DataLoader) -> None: 54 | """Train the ResNet-18 Model""" 55 | 56 | for epoch in range(self.num_epochs): 57 | self.model.train() # set model to train 58 | 59 | # loss tracking metrics 60 | running_loss = 0.0 61 | running_vloss = 0.0 62 | batch_loss = 0.0 63 | running_acc = 0.0 64 | 65 | pbar = tqdm(enumerate(train_dataloader), total=len(train_dataloader)) 66 | 67 | for i, (inputs, labels) in pbar: 68 | inputs, labels = inputs.to(self.device), labels.to(self.device) 69 | 70 | # zero gradients for every batch 71 | self.optimizer.zero_grad() 72 | 73 | # compute predictions + loss 74 | outputs = self.model(inputs) # predicted class 75 | loss = self.criterion(outputs, labels) 76 | 77 | # compute training accuracy 78 | running_acc += self.__accuracy(outputs, labels) 79 | 80 | # perform backpropagation 81 | loss.backward() # compute gradients 82 | self.optimizer.step() # update model parameters 83 | 84 | # gather data and report 85 | running_loss += loss.item() 86 | batch_loss += loss.item() 87 | if i % 10 == 0: 88 | batch_loss = batch_loss / 10 # loss per batch 89 | pbar.set_postfix({"loss": round(batch_loss, 5)}) 90 | batch_loss = 0.0 91 | 92 | self.scheduler.step() 93 | 94 | train_accuracy = running_acc / len(train_dataloader) 95 | self.train_accuracies.append((epoch, train_accuracy.cpu())) 96 | 97 | avg_loss = running_loss / len(train_dataloader) 98 | self.train_losses.append((epoch, avg_loss)) 99 | 100 | if epoch % self.check_val_every_n_epoch == 0: 101 | self.model.eval() # set model to evaluation 102 | with torch.no_grad(): 103 | running_val_acc = 0 104 | for inputs, labels in val_dataloader: 105 | inputs, labels = inputs.to(self.device), labels.to(self.device) 106 | 107 | outputs = self.model(inputs) 108 | loss = self.criterion(outputs, labels) 109 | 110 | running_vloss += loss.item() 111 | # compute validtion accuracy 112 | running_val_acc += self.__accuracy(outputs, labels) 113 | 114 | val_accuracy = running_val_acc / len(val_dataloader) 115 | self.val_accuracies.append((epoch, val_accuracy.cpu())) 116 | 117 | avg_vloss = running_vloss / len(val_dataloader) 118 | self.val_losses.append((epoch, avg_vloss)) 119 | 120 | self.logger.info( 121 | f"[EPOCH {epoch + 1}] LOSS : train={avg_loss} val={avg_vloss} | ACCURACY : train={train_accuracy} val={val_accuracy}" 122 | ) 123 | 124 | def test(self, test_dataloader: DataLoader) -> None: 125 | """Test the ResNet-18 Model""" 126 | 127 | correct = 0 128 | self.model.eval() 129 | with torch.no_grad(): 130 | for inputs, labels in test_dataloader: 131 | inputs, labels = inputs.to(self.device), labels.to(self.device) 132 | outputs = self.model(inputs) 133 | correct += self.__accuracy(outputs, labels) 134 | 135 | self.logger.info(f"Test accuracy: {(correct / len(test_dataloader)) * 100} %") 136 | 137 | def plot_metrics(self) -> None: 138 | """Create plots for model metrics""" 139 | 140 | os.makedirs("plots", exist_ok=True) # create plots dir 141 | 142 | t_iters, t_loss = list(zip(*self.train_losses)) 143 | _, v_loss = list(zip(*self.val_losses)) 144 | _, acc = list(zip(*self.train_accuracies)) 145 | _, v_acc = list(zip(*self.val_accuracies)) 146 | 147 | fig, ax = plt.subplots(1, 2, figsize=(12, 5)) 148 | fig.suptitle(f"Model: [{self.model_name}]") 149 | 150 | ax[0].set_title(f"Loss Curve (batch_size={self.batch_size}, lr={self.learning_rate})") 151 | ax[0].plot(t_iters, t_loss) 152 | ax[0].plot(t_iters, v_loss) 153 | ax[0].set_xlabel("Epochs") 154 | ax[0].set_ylabel("Loss") 155 | ax[0].legend(["Train", "Validation"]) 156 | ax[0].set_xticks(t_iters) 157 | 158 | ax[1].set_title(f"Accuracy Curve (batch_size={self.batch_size}, lr={self.learning_rate})") 159 | ax[1].plot(t_iters, acc) 160 | ax[1].plot(t_iters, v_acc) 161 | ax[1].set_xlabel("Epochs") 162 | ax[1].set_ylabel("Accuracy") 163 | ax[1].legend(["Train", "Validation"]) 164 | ax[1].set_xticks(t_iters) 165 | 166 | fig.savefig(f"plots/{self.model_name}_metrics.png") 167 | plt.show() 168 | 169 | def __accuracy(self, outputs: torch.Tensor, labels: torch.Tensor) -> float: 170 | """Compute accuracy given outputs as logits""" 171 | 172 | preds = torch.argmax(outputs, dim=1) 173 | return torch.sum(preds == labels) / len(preds) 174 | --------------------------------------------------------------------------------