Skip to content

Commit 9371e46

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

File tree

1 file changed

+85
-65
lines changed

1 file changed

+85
-65
lines changed

machine_learning/multilayer_perceptron_classifier_from_scratch.py

Lines changed: 85 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
from tqdm import tqdm
33
from numpy.random import default_rng
44
from numpy.random import seed
5+
56
seed(42)
7+
8+
69
class Dataloader:
710
"""
811
DataLoader class for handling dataset, including data shuffling, one-hot encoding, and train-test splitting.
@@ -45,7 +48,9 @@ def __init__(self, features: list[list[float]], labels: list[int]) -> None:
4548
self.y = np.array(labels)
4649
self.class_weights = {0: 1.0, 1: 1.0} # Example class weights, adjust as needed
4750

48-
def get_Train_test_data(self) -> tuple[list[np.ndarray], list[np.ndarray], list[np.ndarray], list[np.ndarray]]:
51+
def get_Train_test_data(
52+
self,
53+
) -> tuple[list[np.ndarray], list[np.ndarray], list[np.ndarray], list[np.ndarray]]:
4954
"""
5055
Splits the data into training and testing sets. Here, we manually split the data.
5156
@@ -56,13 +61,21 @@ def get_Train_test_data(self) -> tuple[list[np.ndarray], list[np.ndarray], list[
5661
- Test data
5762
- Test labels
5863
"""
59-
train_data = np.array([self.X[0], self.X[1], self.X[2]]) # First 3 samples for training
60-
train_labels = [np.array([self.y[0]]), np.array([self.y[1]]), np.array([self.y[2]])] # Labels as np.ndarray
64+
train_data = np.array(
65+
[self.X[0], self.X[1], self.X[2]]
66+
) # First 3 samples for training
67+
train_labels = [
68+
np.array([self.y[0]]),
69+
np.array([self.y[1]]),
70+
np.array([self.y[2]]),
71+
] # Labels as np.ndarray
6172
test_data = np.array([self.X[3]]) # Last sample for testing
6273
test_labels = [np.array([self.y[3]])] # Labels as np.ndarray
6374
return train_data, train_labels, test_data, test_labels
6475

65-
def shuffle_data(self, paired_data: list[tuple[np.ndarray, int]]) -> list[tuple[np.ndarray, int]]:
76+
def shuffle_data(
77+
self, paired_data: list[tuple[np.ndarray, int]]
78+
) -> list[tuple[np.ndarray, int]]:
6679
"""
6780
Shuffles the data randomly.
6881
@@ -99,40 +112,43 @@ def one_hot_encode(labels: list[int], num_classes: int) -> np.ndarray:
99112
return one_hot
100113

101114

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

218-
219234
def forward(
220-
self,
221-
input_data: np.ndarray,
222-
W1: np.ndarray,
223-
W2: np.ndarray,
224-
no_gradient: bool = False
235+
self,
236+
input_data: np.ndarray,
237+
W1: np.ndarray,
238+
W2: np.ndarray,
239+
no_gradient: bool = False,
225240
) -> np.ndarray:
226241
"""
227242
Performs a forward pass through the neural network with one hidden layer.
@@ -261,11 +276,11 @@ def forward(
261276
return a2
262277

263278
def back_prop(
264-
self,
265-
input_data: np.ndarray,
266-
true_labels: np.ndarray,
267-
W1: np.ndarray,
268-
W2: np.ndarray
279+
self,
280+
input_data: np.ndarray,
281+
true_labels: np.ndarray,
282+
W1: np.ndarray,
283+
W2: np.ndarray,
269284
) -> tuple[np.ndarray, np.ndarray]:
270285
"""
271286
Performs backpropagation to compute gradients for the weights.
@@ -307,20 +322,22 @@ def back_prop(
307322
grad_w2 = (
308323
np.dot(a1.T, delta_k) / batch_size
309324
) # (hidden, batch).dot(batch, output) = (hidden, output)
310-
input_data_flat = input_data.reshape(input_data.shape[0], -1) # (batch_size, input_dim)
325+
input_data_flat = input_data.reshape(
326+
input_data.shape[0], -1
327+
) # (batch_size, input_dim)
311328
grad_w1 = (
312329
np.dot(input_data_flat.T, delta_j) / batch_size
313330
) # (input_dim, batch_size).dot(batch, hidden) = (input, hidden)
314331

315332
return grad_w1, grad_w2
316333

317334
def update_weights(
318-
self,
319-
w1: np.ndarray,
320-
w2: np.ndarray,
321-
grad_w1: np.ndarray,
322-
grad_w2: np.ndarray,
323-
learning_rate: float
335+
self,
336+
w1: np.ndarray,
337+
w2: np.ndarray,
338+
grad_w1: np.ndarray,
339+
grad_w2: np.ndarray,
340+
learning_rate: float,
324341
) -> tuple[np.ndarray, np.ndarray]:
325342
"""
326343
Updates the weight matrices using the computed gradients and learning rate.
@@ -355,7 +372,6 @@ def update_weights(
355372
w2 -= learning_rate * grad_w2
356373
return w1, w2
357374

358-
359375
def update_learning_rate(self, learning_rate: float) -> float:
360376
"""
361377
Updates the learning rate by applying the decay factor gamma.
@@ -451,12 +467,13 @@ def train(self) -> None:
451467
"""
452468

453469
learning_rate = self.learning_rate
454-
train_data, train_labels, test_data, test_labels = self.dataloader.get_Train_test_data()
470+
train_data, train_labels, test_data, test_labels = (
471+
self.dataloader.get_Train_test_data()
472+
)
455473

456474
train_data = np.c_[train_data, np.ones(train_data.shape[0])]
457475
test_data = np.c_[test_data, np.ones(test_data.shape[0])]
458476

459-
460477
_, total_label_num = self.dataloader.get_inout_dim()
461478

462479
train_labels = self.dataloader.one_hot_encode(train_labels, total_label_num)
@@ -471,13 +488,16 @@ def train(self) -> None:
471488

472489
for j in tqdm(range(self.epoch)):
473490
for k in range(0, train_data.shape[0], batch_size): # retrieve every image
491+
batch_imgs = train_data[k : k + batch_size]
492+
batch_labels = train_labels[k : k + batch_size]
474493

475-
batch_imgs = train_data[k: k + batch_size]
476-
batch_labels = train_labels[k: k + batch_size]
477-
478-
output = self.forward(input_data=batch_imgs, W1=W1, W2=W2, no_gradient=False)
494+
output = self.forward(
495+
input_data=batch_imgs, W1=W1, W2=W2, no_gradient=False
496+
)
479497

480-
grad_W1, grad_W2 = self.back_prop(input_data=batch_imgs, true_labels=batch_labels, W1=W1, W2=W2)
498+
grad_W1, grad_W2 = self.back_prop(
499+
input_data=batch_imgs, true_labels=batch_labels, W1=W1, W2=W2
500+
)
481501

482502
W1, W2 = self.update_weights(W1, W2, grad_W1, grad_W2, learning_rate)
483503

@@ -492,10 +512,10 @@ def train(self) -> None:
492512

493513
self.test_accuracy = test_accuracy_list
494514
self.test_loss = test_loss_list
495-
print(f"Test accuracy:", sum(test_accuracy_list)/len(test_accuracy_list))
515+
print(f"Test accuracy:", sum(test_accuracy_list) / len(test_accuracy_list))
496516

497517

498518
if __name__ == "__main__":
499519
import doctest
500520

501-
doctest.testmod()
521+
doctest.testmod()

0 commit comments

Comments
 (0)