├── .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 | 
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) | [](LinearRegression/eval.ipynb) |
9 | | Logistic Regression | Breast Cancer Wisconsin Dataset | [](LogisticRegression/eval.ipynb) |
10 |
11 | ## Deep Learning
12 | |Implementation | Dataset | Notebooks |
13 | | --- | --- | --- |
14 | | AlexNet | Tiny ImageNet | [](AlexNet/train_alexnet.ipynb) |
15 | | ResNet-18 | CIFAR-10 | [](ResNet/train_resnet18.ipynb) |
16 |
17 | ## Computer Vision
18 | |Implementation | Dataset | Notebooks |
19 | | --- | --- | --- |
20 | | U-Net Architecture | SOME RANDOM SEGMENTATION DATASET | [](...) |
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 | 
9 |
10 |
11 | ### Our ResNet-18 Implementation
12 | We load the pretrained weights from PyTorch's `ResNet18_Weights.IMAGENET1K_V1`.
13 |
14 |
15 | 
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 |
--------------------------------------------------------------------------------