import numpy as np class SimpleNN: def __init__(self, architecture): self.architecture = architecture self.weights = [] self.biases = [] #keeping track of these values in this code block #so we can observe them self.perceptron_inputs = None self.perceptron_outputs = None # 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) def forward(self, X): self.perceptron_inputs = [X] self.perceptron_outputs = [] for W, b in zip(self.weights, self.biases): Z = np.dot(self.perceptron_inputs[-1], W) + b self.perceptron_outputs.append(Z) if W is self.weights[-1]: # Last layer (output) A = Z # Linear output for regression else: A = self.relu(Z) self.perceptron_inputs.append(A) return self.perceptron_inputs, self.perceptron_outputs def predict(self, X): perceptron_inputs, _ = self.forward(X) return perceptron_inputs[-1].flatten() #defining a model architecture = [2, 64, 64, 64, 1] # Two inputs, two hidden layers, one output model = SimpleNN(architecture) # Generate predictions prediction = model.predict(np.array([0.1,0.2])) #looking through critical optimization values for i, (inpt, outpt) in enumerate(zip(model.perceptron_inputs, model.perceptron_outputs[:-1])): print(f'layer {i}') print(f'input: {inpt.shape}') print(f'output: {outpt.shape}') print('') print('Final Output:') print(model.perceptron_outputs[-1].shape)