import numpy as np class SimpleNN: def __init__(self, architecture): self.architecture = architecture self.weights = [] self.biases = [] # Initialize weights and biases np.random.seed(99) for i in range(len(architecture) - 1): self.weights.append(np.random.uniform( low=-1, high=1, size=(architecture[i], architecture[i+1]) )) self.biases.append(np.zeros((1, architecture[i+1]))) @staticmethod def relu(x): return np.maximum(0, x) @staticmethod def relu_as_weights(x): return (x > 0).astype(float) def forward(self, X): perceptron_inputs = [X] perceptron_outputs = [] for W, b in zip(self.weights, self.biases): Z = np.dot(perceptron_inputs[-1], W) + b perceptron_outputs.append(Z) if W is self.weights[-1]: # Last layer (output) A = Z # Linear output for regression else: A = self.relu(Z) perceptron_inputs.append(A) return perceptron_inputs, perceptron_outputs def backward(self, perceptron_inputs, perceptron_outputs, y_true): weight_changes = [] bias_changes = [] m = len(y_true) dA = perceptron_inputs[-1] - y_true.reshape(-1, 1) # Output layer gradient for i in reversed(range(len(self.weights))): dZ = dA if i == len(self.weights) - 1 else dA * self.relu_as_weights(perceptron_outputs[i]) dW = np.dot(perceptron_inputs[i].T, dZ) / m db = np.sum(dZ, axis=0, keepdims=True) / m weight_changes.append(dW) bias_changes.append(db) if i > 0: dA = np.dot(dZ, self.weights[i].T) return list(reversed(weight_changes)), list(reversed(bias_changes)) def update_weights(self, weight_changes, bias_changes, lr): for i in range(len(self.weights)): self.weights[i] -= lr * weight_changes[i] self.biases[i] -= lr * bias_changes[i] def train(self, X, y, epochs, lr=0.01): for epoch in range(epochs): perceptron_inputs, perceptron_outputs = self.forward(X) weight_changes, bias_changes = self.backward(perceptron_inputs, perceptron_outputs, y) self.update_weights(weight_changes, bias_changes, lr) if epoch % 20 == 0 or epoch == epochs - 1: loss = np.mean((perceptron_inputs[-1].flatten() - y) ** 2) # MSE print(f"EPOCH {epoch}: Loss = {loss:.4f}") def predict(self, X): perceptron_inputs, _ = self.forward(X) return perceptron_inputs[-1].flatten()