|
| 1 | +""" |
| 2 | +References: |
| 3 | + - http://neuralnetworksanddeeplearning.com/chap2.html (Backpropagation) |
| 4 | + - https://en.wikipedia.org/wiki/Sigmoid_function (Sigmoid activation function) |
| 5 | + - https://en.wikipedia.org/wiki/Feedforward_neural_network (Feedforward) |
| 6 | +""" |
| 7 | + |
| 8 | +import numpy |
| 9 | + |
| 10 | + |
| 11 | +class TwoHiddenLayerNeuralNetwork: |
| 12 | + def __init__(self, input_array: numpy.ndarray, output_array: numpy.ndarray) -> None: |
| 13 | + """ |
| 14 | + This function initializes the TwoHiddenLayerNeuralNetwork class with random |
| 15 | + weights for every layer and initializes predicted output with zeroes. |
| 16 | +
|
| 17 | + input_array : input values for training the neural network (i.e training data) . |
| 18 | + output_array : expected output values of the given inputs. |
| 19 | + """ |
| 20 | + |
| 21 | + # Input values provided for training the model. |
| 22 | + self.input_array = input_array |
| 23 | + |
| 24 | + # Random initial weights are assigned where first argument is the |
| 25 | + # number of nodes in previous layer and second argument is the |
| 26 | + # number of nodes in the next layer. |
| 27 | + |
| 28 | + # Random initial weights are assigned. |
| 29 | + # self.input_array.shape[1] is used to represent number of nodes in input layer. |
| 30 | + # First hidden layer consists of 4 nodes. |
| 31 | + self.input_layer_and_first_hidden_layer_weights = numpy.random.rand( |
| 32 | + self.input_array.shape[1], 4 |
| 33 | + ) |
| 34 | + |
| 35 | + # Random initial values for the first hidden layer. |
| 36 | + # First hidden layer has 4 nodes. |
| 37 | + # Second hidden layer has 3 nodes. |
| 38 | + self.first_hidden_layer_and_second_hidden_layer_weights = numpy.random.rand( |
| 39 | + 4, 3 |
| 40 | + ) |
| 41 | + |
| 42 | + # Random initial values for the second hidden layer. |
| 43 | + # Second hidden layer has 3 nodes. |
| 44 | + # Output layer has 1 node. |
| 45 | + self.second_hidden_layer_and_output_layer_weights = numpy.random.rand(3, 1) |
| 46 | + |
| 47 | + # Real output values provided. |
| 48 | + self.output_array = output_array |
| 49 | + |
| 50 | + # Predicted output values by the neural network. |
| 51 | + # Predicted_output array initially consists of zeroes. |
| 52 | + self.predicted_output = numpy.zeros(output_array.shape) |
| 53 | + |
| 54 | + def feedforward(self) -> numpy.ndarray: |
| 55 | + """ |
| 56 | + The information moves in only one direction i.e. forward from the input nodes, |
| 57 | + through the two hidden nodes and to the output nodes. |
| 58 | + There are no cycles or loops in the network. |
| 59 | +
|
| 60 | + Return layer_between_second_hidden_layer_and_output |
| 61 | + (i.e the last layer of the neural network). |
| 62 | +
|
| 63 | + >>> input_val = numpy.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float) |
| 64 | + >>> output_val = numpy.array(([0], [0], [0]), dtype=float) |
| 65 | + >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val) |
| 66 | + >>> res = nn.feedforward() |
| 67 | + >>> array_sum = numpy.sum(res) |
| 68 | + >>> numpy.isnan(array_sum) |
| 69 | + False |
| 70 | + """ |
| 71 | + # Layer_between_input_and_first_hidden_layer is the layer connecting the |
| 72 | + # input nodes with the first hidden layer nodes. |
| 73 | + self.layer_between_input_and_first_hidden_layer = sigmoid( |
| 74 | + numpy.dot(self.input_array, self.input_layer_and_first_hidden_layer_weights) |
| 75 | + ) |
| 76 | + |
| 77 | + # layer_between_first_hidden_layer_and_second_hidden_layer is the layer |
| 78 | + # connecting the first hidden set of nodes with the second hidden set of nodes. |
| 79 | + self.layer_between_first_hidden_layer_and_second_hidden_layer = sigmoid( |
| 80 | + numpy.dot( |
| 81 | + self.layer_between_input_and_first_hidden_layer, |
| 82 | + self.first_hidden_layer_and_second_hidden_layer_weights, |
| 83 | + ) |
| 84 | + ) |
| 85 | + |
| 86 | + # layer_between_second_hidden_layer_and_output is the layer connecting |
| 87 | + # second hidden layer with the output node. |
| 88 | + self.layer_between_second_hidden_layer_and_output = sigmoid( |
| 89 | + numpy.dot( |
| 90 | + self.layer_between_first_hidden_layer_and_second_hidden_layer, |
| 91 | + self.second_hidden_layer_and_output_layer_weights, |
| 92 | + ) |
| 93 | + ) |
| 94 | + |
| 95 | + return self.layer_between_second_hidden_layer_and_output |
| 96 | + |
| 97 | + def back_propagation(self) -> None: |
| 98 | + """ |
| 99 | + Function for fine-tuning the weights of the neural net based on the |
| 100 | + error rate obtained in the previous epoch (i.e., iteration). |
| 101 | + Updation is done using derivative of sogmoid activation function. |
| 102 | +
|
| 103 | + >>> input_val = numpy.array(([0, 0, 0], [0, 0, 0], [0, 0, 0]), dtype=float) |
| 104 | + >>> output_val = numpy.array(([0], [0], [0]), dtype=float) |
| 105 | + >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val) |
| 106 | + >>> res = nn.feedforward() |
| 107 | + >>> nn.back_propagation() |
| 108 | + >>> updated_weights = nn.second_hidden_layer_and_output_layer_weights |
| 109 | + >>> (res == updated_weights).all() |
| 110 | + False |
| 111 | + """ |
| 112 | + |
| 113 | + updated_second_hidden_layer_and_output_layer_weights = numpy.dot( |
| 114 | + self.layer_between_first_hidden_layer_and_second_hidden_layer.T, |
| 115 | + 2 |
| 116 | + * (self.output_array - self.predicted_output) |
| 117 | + * sigmoid_derivative(self.predicted_output), |
| 118 | + ) |
| 119 | + updated_first_hidden_layer_and_second_hidden_layer_weights = numpy.dot( |
| 120 | + self.layer_between_input_and_first_hidden_layer.T, |
| 121 | + numpy.dot( |
| 122 | + 2 |
| 123 | + * (self.output_array - self.predicted_output) |
| 124 | + * sigmoid_derivative(self.predicted_output), |
| 125 | + self.second_hidden_layer_and_output_layer_weights.T, |
| 126 | + ) |
| 127 | + * sigmoid_derivative( |
| 128 | + self.layer_between_first_hidden_layer_and_second_hidden_layer |
| 129 | + ), |
| 130 | + ) |
| 131 | + updated_input_layer_and_first_hidden_layer_weights = numpy.dot( |
| 132 | + self.input_array.T, |
| 133 | + numpy.dot( |
| 134 | + numpy.dot( |
| 135 | + 2 |
| 136 | + * (self.output_array - self.predicted_output) |
| 137 | + * sigmoid_derivative(self.predicted_output), |
| 138 | + self.second_hidden_layer_and_output_layer_weights.T, |
| 139 | + ) |
| 140 | + * sigmoid_derivative( |
| 141 | + self.layer_between_first_hidden_layer_and_second_hidden_layer |
| 142 | + ), |
| 143 | + self.first_hidden_layer_and_second_hidden_layer_weights.T, |
| 144 | + ) |
| 145 | + * sigmoid_derivative(self.layer_between_input_and_first_hidden_layer), |
| 146 | + ) |
| 147 | + |
| 148 | + self.input_layer_and_first_hidden_layer_weights += ( |
| 149 | + updated_input_layer_and_first_hidden_layer_weights |
| 150 | + ) |
| 151 | + self.first_hidden_layer_and_second_hidden_layer_weights += ( |
| 152 | + updated_first_hidden_layer_and_second_hidden_layer_weights |
| 153 | + ) |
| 154 | + self.second_hidden_layer_and_output_layer_weights += ( |
| 155 | + updated_second_hidden_layer_and_output_layer_weights |
| 156 | + ) |
| 157 | + |
| 158 | + def train(self, output: numpy.ndarray, iterations: int, give_loss: bool) -> None: |
| 159 | + """ |
| 160 | + Performs the feedforwarding and back propagation process for the |
| 161 | + given number of iterations. |
| 162 | + Every iteration will update the weights of neural network. |
| 163 | +
|
| 164 | + output : real output values,required for calculating loss. |
| 165 | + iterations : number of times the weights are to be updated. |
| 166 | + give_loss : boolean value, If True then prints loss for each iteration, |
| 167 | + If False then nothing is printed |
| 168 | +
|
| 169 | + >>> input_val = numpy.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float) |
| 170 | + >>> output_val = numpy.array(([0], [1], [1]), dtype=float) |
| 171 | + >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val) |
| 172 | + >>> first_iteration_weights = nn.feedforward() |
| 173 | + >>> nn.back_propagation() |
| 174 | + >>> updated_weights = nn.second_hidden_layer_and_output_layer_weights |
| 175 | + >>> (first_iteration_weights == updated_weights).all() |
| 176 | + False |
| 177 | + """ |
| 178 | + for iteration in range(1, iterations + 1): |
| 179 | + self.output = self.feedforward() |
| 180 | + self.back_propagation() |
| 181 | + if give_loss: |
| 182 | + loss = numpy.mean(numpy.square(output - self.feedforward())) |
| 183 | + print(f"Iteration {iteration} Loss: {loss}") |
| 184 | + |
| 185 | + def predict(self, input: numpy.ndarray) -> int: |
| 186 | + """ |
| 187 | + Predict's the output for the given input values using |
| 188 | + the trained neural network. |
| 189 | +
|
| 190 | + The output value given by the model ranges in-between 0 and 1. |
| 191 | + The predict function returns 1 if the model value is greater |
| 192 | + than the threshold value else returns 0, |
| 193 | + as the real output values are in binary. |
| 194 | +
|
| 195 | + >>> input_val = numpy.array(([0, 0, 0], [0, 1, 0], [0, 0, 1]), dtype=float) |
| 196 | + >>> output_val = numpy.array(([0], [1], [1]), dtype=float) |
| 197 | + >>> nn = TwoHiddenLayerNeuralNetwork(input_val, output_val) |
| 198 | + >>> nn.train(output_val, 1000, False) |
| 199 | + >>> nn.predict([0,1,0]) |
| 200 | + 1 |
| 201 | + """ |
| 202 | + |
| 203 | + # Input values for which the predictions are to be made. |
| 204 | + self.array = input |
| 205 | + |
| 206 | + self.layer_between_input_and_first_hidden_layer = sigmoid( |
| 207 | + numpy.dot(self.array, self.input_layer_and_first_hidden_layer_weights) |
| 208 | + ) |
| 209 | + |
| 210 | + self.layer_between_first_hidden_layer_and_second_hidden_layer = sigmoid( |
| 211 | + numpy.dot( |
| 212 | + self.layer_between_input_and_first_hidden_layer, |
| 213 | + self.first_hidden_layer_and_second_hidden_layer_weights, |
| 214 | + ) |
| 215 | + ) |
| 216 | + |
| 217 | + self.layer_between_second_hidden_layer_and_output = sigmoid( |
| 218 | + numpy.dot( |
| 219 | + self.layer_between_first_hidden_layer_and_second_hidden_layer, |
| 220 | + self.second_hidden_layer_and_output_layer_weights, |
| 221 | + ) |
| 222 | + ) |
| 223 | + |
| 224 | + return int(self.layer_between_second_hidden_layer_and_output > 0.6) |
| 225 | + |
| 226 | + |
| 227 | +def sigmoid(value: numpy.ndarray) -> numpy.ndarray: |
| 228 | + """ |
| 229 | + Applies sigmoid activation function. |
| 230 | +
|
| 231 | + return normalized values |
| 232 | +
|
| 233 | + >>> sigmoid(numpy.array(([1, 0, 2], [1, 0, 0]), dtype=numpy.float64)) |
| 234 | + array([[0.73105858, 0.5 , 0.88079708], |
| 235 | + [0.73105858, 0.5 , 0.5 ]]) |
| 236 | + """ |
| 237 | + return 1 / (1 + numpy.exp(-value)) |
| 238 | + |
| 239 | + |
| 240 | +def sigmoid_derivative(value: numpy.ndarray) -> numpy.ndarray: |
| 241 | + """ |
| 242 | + Provides the derivative value of the sigmoid function. |
| 243 | +
|
| 244 | + returns derivative of the sigmoid value |
| 245 | +
|
| 246 | + >>> sigmoid_derivative(numpy.array(([1, 0, 2], [1, 0, 0]), dtype=numpy.float64)) |
| 247 | + array([[ 0., 0., -2.], |
| 248 | + [ 0., 0., 0.]]) |
| 249 | + """ |
| 250 | + return (value) * (1 - (value)) |
| 251 | + |
| 252 | + |
| 253 | +def example() -> int: |
| 254 | + """ |
| 255 | + Example for "how to use the neural network class and use the |
| 256 | + respected methods for the desired output". |
| 257 | + Calls the TwoHiddenLayerNeuralNetwork class and |
| 258 | + provides the fixed input output values to the model. |
| 259 | + Model is trained for a fixed amount of iterations then the predict method is called. |
| 260 | + In this example the output is divided into 2 classes i.e. binary classification, |
| 261 | + the two classes are represented by '0' and '1'. |
| 262 | +
|
| 263 | + >>> example() |
| 264 | + 1 |
| 265 | + """ |
| 266 | + # Input values. |
| 267 | + input = numpy.array( |
| 268 | + ( |
| 269 | + [0, 0, 0], |
| 270 | + [0, 0, 1], |
| 271 | + [0, 1, 0], |
| 272 | + [0, 1, 1], |
| 273 | + [1, 0, 0], |
| 274 | + [1, 0, 1], |
| 275 | + [1, 1, 0], |
| 276 | + [1, 1, 1], |
| 277 | + ), |
| 278 | + dtype=numpy.float64, |
| 279 | + ) |
| 280 | + |
| 281 | + # True output values for the given input values. |
| 282 | + output = numpy.array(([0], [1], [1], [0], [1], [0], [0], [1]), dtype=numpy.float64) |
| 283 | + |
| 284 | + # Calling neural network class. |
| 285 | + neural_network = TwoHiddenLayerNeuralNetwork(input_array=input, output_array=output) |
| 286 | + |
| 287 | + # Calling training function. |
| 288 | + # Set give_loss to True if you want to see loss in every iteration. |
| 289 | + neural_network.train(output=output, iterations=10, give_loss=False) |
| 290 | + |
| 291 | + return neural_network.predict(numpy.array(([1, 1, 1]), dtype=numpy.float64)) |
| 292 | + |
| 293 | + |
| 294 | +if __name__ == "__main__": |
| 295 | + example() |
0 commit comments