Skip to content

Commit 1eca8f1

Browse files
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
1 parent ad745ee commit 1eca8f1

File tree

1 file changed

+84
-66
lines changed

1 file changed

+84
-66
lines changed

machine_learning/multilayer_perceptron_classifier_from_scratch.py

Lines changed: 84 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from tqdm import tqdm
33
from numpy.random import default_rng
44

5+
56
class Dataloader:
67
"""
78
DataLoader class for handling dataset operations. Supports:
@@ -48,7 +49,9 @@ def __init__(self, features: list[list[float]], labels: list[int]) -> None:
4849
self.y = np.array(labels)
4950
self.class_weights = {0: 1.0, 1: 1.0} # Example class weights, adjust as needed
5051

51-
def get_train_test_data(self) -> tuple[list[np.ndarray], list[np.ndarray], list[np.ndarray], list[np.ndarray]]:
52+
def get_train_test_data(
53+
self,
54+
) -> tuple[list[np.ndarray], list[np.ndarray], list[np.ndarray], list[np.ndarray]]:
5255
"""
5356
Splits the data into training and testing sets.
5457
Here, we manually split the data.
@@ -60,13 +63,21 @@ def get_train_test_data(self) -> tuple[list[np.ndarray], list[np.ndarray], list[
6063
- Test data
6164
- Test labels
6265
"""
63-
train_data = np.array([self.X[0], self.X[1], self.X[2]]) # First 3 samples for training
64-
train_labels = [np.array([self.y[0]]), np.array([self.y[1]]), np.array([self.y[2]])] # Labels as np.ndarray
66+
train_data = np.array(
67+
[self.X[0], self.X[1], self.X[2]]
68+
) # First 3 samples for training
69+
train_labels = [
70+
np.array([self.y[0]]),
71+
np.array([self.y[1]]),
72+
np.array([self.y[2]]),
73+
] # Labels as np.ndarray
6574
test_data = np.array([self.X[3]]) # Last sample for testing
6675
test_labels = [np.array([self.y[3]])] # Labels as np.ndarray
6776
return train_data, train_labels, test_data, test_labels
6877

69-
def shuffle_data(self, paired_data: list[tuple[np.ndarray, int]]) -> list[tuple[np.ndarray, int]]:
78+
def shuffle_data(
79+
self, paired_data: list[tuple[np.ndarray, int]]
80+
) -> list[tuple[np.ndarray, int]]:
7081
"""
7182
Shuffles the data randomly.
7283
@@ -103,42 +114,45 @@ def one_hot_encode(labels: list[int], num_classes: int) -> np.ndarray:
103114
return one_hot
104115

105116

106-
class MLP():
117+
class MLP:
107118
"""
108-
A custom MLP class for implementing a simple multi-layer perceptron with
109-
forward propagation, backpropagation.
110-
111-
Attributes:
112-
learning_rate (float): Learning rate for gradient descent.
113-
gamma (float): Parameter to control learning rate adjustment.
114-
epoch (int): Number of epochs for training.
115-
hidden_dim (int): Dimension of the hidden layer.
116-
batch_size (int): Number of samples per mini-batch.
117-
train_loss (List[float]): List to store training loss for each fold.
118-
train_accuracy (List[float]): List to store training accuracy for each fold.
119-
test_loss (List[float]): List to store test loss for each fold.
120-
test_accuracy (List[float]): List to store test accuracy for each fold.
121-
dataloader (Dataloader): DataLoader object for handling training data.
122-
inter_variable (dict):
123-
Dictionary to store intermediate variables for backpropagation.
124-
weights1_list (List[Tuple[np.ndarray, np.ndarray]]):
125-
List of weights for each fold.
126-
127-
Methods:
128-
get_inout_dim:obtain input dimension and output dimension.
129-
relu: Apply the ReLU activation function.
130-
relu_derivative: Compute the derivative of the ReLU function.
131-
forward: Perform a forward pass through the network.
132-
back_prop: Perform backpropagation to compute gradients.
133-
update_weights: Update the weights using gradients.
134-
update_learning_rate: Adjust the learning rate based on test accuracy.
135-
accuracy: Compute accuracy of the model.
136-
loss: Compute weighted MSE loss.
137-
train: Train the MLP over multiple folds with early stopping.
138-
139-
140-
"""
141-
def __init__(self, dataloader, epoch: int, learning_rate: float, gamma=1, hidden_dim=2):
119+
A custom MLP class for implementing a simple multi-layer perceptron with
120+
forward propagation, backpropagation.
121+
122+
Attributes:
123+
learning_rate (float): Learning rate for gradient descent.
124+
gamma (float): Parameter to control learning rate adjustment.
125+
epoch (int): Number of epochs for training.
126+
hidden_dim (int): Dimension of the hidden layer.
127+
batch_size (int): Number of samples per mini-batch.
128+
train_loss (List[float]): List to store training loss for each fold.
129+
train_accuracy (List[float]): List to store training accuracy for each fold.
130+
test_loss (List[float]): List to store test loss for each fold.
131+
test_accuracy (List[float]): List to store test accuracy for each fold.
132+
dataloader (Dataloader): DataLoader object for handling training data.
133+
inter_variable (dict):
134+
Dictionary to store intermediate variables for backpropagation.
135+
weights1_list (List[Tuple[np.ndarray, np.ndarray]]):
136+
List of weights for each fold.
137+
138+
Methods:
139+
get_inout_dim:obtain input dimension and output dimension.
140+
relu: Apply the ReLU activation function.
141+
relu_derivative: Compute the derivative of the ReLU function.
142+
forward: Perform a forward pass through the network.
143+
back_prop: Perform backpropagation to compute gradients.
144+
update_weights: Update the weights using gradients.
145+
update_learning_rate: Adjust the learning rate based on test accuracy.
146+
accuracy: Compute accuracy of the model.
147+
loss: Compute weighted MSE loss.
148+
train: Train the MLP over multiple folds with early stopping.
149+
150+
151+
"""
152+
153+
def __init__(
154+
self, dataloader, epoch: int, learning_rate: float, gamma=1, hidden_dim=2
155+
):
142156
self.learning_rate = learning_rate
143157
self.gamma = gamma # learning_rate decay hyperparameter gamma
144158
self.epoch = epoch
@@ -221,13 +235,12 @@ def relu_derivative(self, input_array: np.ndarray) -> np.ndarray:
221235
"""
222236
return (input_array > 0).astype(float)
223237

224-
225238
def forward(
226-
self,
227-
input_data: np.ndarray,
228-
W1: np.ndarray,
229-
W2: np.ndarray,
230-
no_gradient: bool = False
239+
self,
240+
input_data: np.ndarray,
241+
W1: np.ndarray,
242+
W2: np.ndarray,
243+
no_gradient: bool = False,
231244
) -> np.ndarray:
232245
"""
233246
Performs a forward pass through the neural network with one hidden layer.
@@ -267,11 +280,11 @@ def forward(
267280
return a2
268281

269282
def back_prop(
270-
self,
271-
input_data: np.ndarray,
272-
true_labels: np.ndarray,
273-
W1: np.ndarray,
274-
W2: np.ndarray
283+
self,
284+
input_data: np.ndarray,
285+
true_labels: np.ndarray,
286+
W1: np.ndarray,
287+
W2: np.ndarray,
275288
) -> tuple[np.ndarray, np.ndarray]:
276289
"""
277290
Performs backpropagation to compute gradients for the weights.
@@ -313,20 +326,22 @@ def back_prop(
313326
grad_w2 = (
314327
np.dot(a1.T, delta_k) / batch_size
315328
) # (hidden, batch).dot(batch, output) = (hidden, output)
316-
input_data_flat = input_data.reshape(input_data.shape[0], -1) # (batch_size, input_dim)
329+
input_data_flat = input_data.reshape(
330+
input_data.shape[0], -1
331+
) # (batch_size, input_dim)
317332
grad_w1 = (
318333
np.dot(input_data_flat.T, delta_j) / batch_size
319334
) # (input_dim, batch_size).dot(batch, hidden) = (input, hidden)
320335

321336
return grad_w1, grad_w2
322337

323338
def update_weights(
324-
self,
325-
w1: np.ndarray,
326-
w2: np.ndarray,
327-
grad_w1: np.ndarray,
328-
grad_w2: np.ndarray,
329-
learning_rate: float
339+
self,
340+
w1: np.ndarray,
341+
w2: np.ndarray,
342+
grad_w1: np.ndarray,
343+
grad_w2: np.ndarray,
344+
learning_rate: float,
330345
) -> tuple[np.ndarray, np.ndarray]:
331346
"""
332347
Updates the weight matrices using the computed gradients and learning rate.
@@ -361,7 +376,6 @@ def update_weights(
361376
w2 -= learning_rate * grad_w2
362377
return w1, w2
363378

364-
365379
def update_learning_rate(self, learning_rate: float) -> float:
366380
"""
367381
Updates the learning rate by applying the decay factor gamma.
@@ -457,12 +471,13 @@ def train(self) -> None:
457471
"""
458472

459473
learning_rate = self.learning_rate
460-
train_data, train_labels, test_data, test_labels = self.dataloader.get_train_test_data()
474+
train_data, train_labels, test_data, test_labels = (
475+
self.dataloader.get_train_test_data()
476+
)
461477

462478
train_data = np.c_[train_data, np.ones(train_data.shape[0])]
463479
test_data = np.c_[test_data, np.ones(test_data.shape[0])]
464480

465-
466481
_, total_label_num = self.dataloader.get_inout_dim()
467482

468483
train_labels = self.dataloader.one_hot_encode(train_labels, total_label_num)
@@ -477,13 +492,16 @@ def train(self) -> None:
477492

478493
for j in tqdm(range(self.epoch)):
479494
for k in range(0, train_data.shape[0], batch_size): # retrieve every image
495+
batch_imgs = train_data[k : k + batch_size]
496+
batch_labels = train_labels[k : k + batch_size]
480497

481-
batch_imgs = train_data[k: k + batch_size]
482-
batch_labels = train_labels[k: k + batch_size]
483-
484-
output = self.forward(input_data=batch_imgs, W1=W1, W2=W2, no_gradient=False)
498+
output = self.forward(
499+
input_data=batch_imgs, W1=W1, W2=W2, no_gradient=False
500+
)
485501

486-
grad_W1, grad_W2 = self.back_prop(input_data=batch_imgs, true_labels=batch_labels, W1=W1, W2=W2)
502+
grad_W1, grad_W2 = self.back_prop(
503+
input_data=batch_imgs, true_labels=batch_labels, W1=W1, W2=W2
504+
)
487505

488506
W1, W2 = self.update_weights(W1, W2, grad_W1, grad_W2, learning_rate)
489507

@@ -498,7 +516,7 @@ def train(self) -> None:
498516

499517
self.test_accuracy = test_accuracy_list
500518
self.test_loss = test_loss_list
501-
print(f"Test accuracy:", sum(test_accuracy_list)/len(test_accuracy_list))
519+
print(f"Test accuracy:", sum(test_accuracy_list) / len(test_accuracy_list))
502520

503521

504522
if __name__ == "__main__":

0 commit comments

Comments
 (0)