nn-from-scratch/work-sc.ipynb

729 lines
239 KiB
Plaintext
Raw Normal View History

2025-11-04 18:05:00 +01:00
{
"cells": [
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:08.874062Z",
"start_time": "2025-11-04T22:33:08.871974Z"
2025-11-04 18:05:00 +01:00
}
},
"source": "import numpy as np",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 97
2025-11-04 18:05:00 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:08.918897Z",
"start_time": "2025-11-04T22:33:08.917441Z"
2025-11-04 18:05:00 +01:00
}
},
"cell_type": "code",
"source": [
"nn_architecture = [\n",
" {\"input_dim\": 2, \"output_dim\": 4, \"activation\": \"relu\"},\n",
" {\"input_dim\": 4, \"output_dim\": 6, \"activation\": \"relu\"},\n",
" {\"input_dim\": 6, \"output_dim\": 6, \"activation\": \"relu\"},\n",
" {\"input_dim\": 6, \"output_dim\": 4, \"activation\": \"relu\"},\n",
" {\"input_dim\": 4, \"output_dim\": 1, \"activation\": \"sigmoid\"},\n",
"]"
],
"id": "48cafaf4b64967bb",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 98
2025-11-04 18:05:00 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:08.972468Z",
"start_time": "2025-11-04T22:33:08.966895Z"
2025-11-04 18:05:00 +01:00
}
},
"cell_type": "code",
"source": [
"def init_layers(nn_architecture, seed = 99):\n",
" np.random.seed(seed)\n",
" number_of_layers = len(nn_architecture)\n",
" params_values = {}\n",
"\n",
" for idx, layer in enumerate(nn_architecture):\n",
" layer_idx = idx + 1\n",
" layer_input_size = layer[\"input_dim\"]\n",
" layer_output_size = layer[\"output_dim\"]\n",
"\n",
" params_values['W' + str(layer_idx)] = np.random.randn(\n",
" layer_output_size, layer_input_size) * 0.1\n",
" params_values['b' + str(layer_idx)] = np.random.randn(\n",
" layer_output_size, 1) * 0.1\n",
"\n",
" return params_values\n"
],
"id": "d13137630b41b756",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 99
2025-11-04 18:05:00 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.025064Z",
"start_time": "2025-11-04T22:33:09.020911Z"
2025-11-04 18:05:00 +01:00
}
},
"cell_type": "code",
2025-11-04 22:56:05 +01:00
"source": [
"params = init_layers(nn_architecture)\n",
"# params"
],
2025-11-04 18:05:00 +01:00
"id": "31f205147667dea6",
2025-11-04 22:56:05 +01:00
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 100
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.075270Z",
"start_time": "2025-11-04T22:33:09.073161Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"def sigmoid(Z):\n",
" return 1/(1+np.exp(-Z))\n",
"\n",
"def relu(Z):\n",
" return np.maximum(0,Z)\n",
"\n",
"def sigmoid_backward(dA, Z):\n",
" sig = sigmoid(Z)\n",
" return dA * sig * (1 - sig)\n",
"\n",
"def relu_backward(dA, Z):\n",
" dZ = np.array(dA, copy = True)\n",
" dZ[Z <= 0] = 0;\n",
" return dZ;"
],
"id": "c1b960e7dcf09d91",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 101
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.131020Z",
"start_time": "2025-11-04T22:33:09.123827Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"def single_layer_forward_propagation(A_prev, W_curr, b_curr, activation=\"relu\"):\n",
" Z_curr = np.dot(W_curr, A_prev) + b_curr\n",
"\n",
" if activation == \"relu\":\n",
" activation_func = relu\n",
" elif activation == \"sigmoid\":\n",
" activation_func = sigmoid\n",
" else:\n",
" raise Exception('Non-supported activation function')\n",
"\n",
" return activation_func(Z_curr), Z_curr"
],
"id": "efae2e184daf2fce",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 102
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.180896Z",
"start_time": "2025-11-04T22:33:09.175920Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"def full_forward_propagation(X, params_values, nn_architecture):\n",
" memory = {}\n",
" A_curr = X\n",
"\n",
" for idx, layer in enumerate(nn_architecture):\n",
" layer_idx = idx + 1\n",
" A_prev = A_curr\n",
"\n",
" activ_function_curr = layer[\"activation\"]\n",
" W_curr = params_values[\"W\" + str(layer_idx)]\n",
" b_curr = params_values[\"b\" + str(layer_idx)]\n",
" A_curr, Z_curr = single_layer_forward_propagation(A_prev, W_curr, b_curr, activ_function_curr)\n",
"\n",
" memory[\"A\" + str(idx)] = A_prev\n",
" memory[\"Z\" + str(layer_idx)] = Z_curr\n",
"\n",
" return A_curr, memory"
],
"id": "c3cd9e8f51dbe967",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 103
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.235246Z",
"start_time": "2025-11-04T22:33:09.228689Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"def get_cost_value(Y_hat, Y):\n",
" m = Y_hat.shape[1]\n",
" cost = -1 / m * (np.dot(Y, np.log(Y_hat).T) + np.dot(1 - Y, np.log(1 - Y_hat).T))\n",
" return np.squeeze(cost)\n",
"\n",
"# an auxiliary function that converts probability into class\n",
"def convert_prob_into_class(probs):\n",
" probs_ = np.copy(probs)\n",
" probs_[probs_ > 0.5] = 1\n",
" probs_[probs_ <= 0.5] = 0\n",
" return probs_\n",
"\n",
"def get_accuracy_value(Y_hat, Y):\n",
" Y_hat_ = convert_prob_into_class(Y_hat)\n",
" return (Y_hat_ == Y).all(axis=0).mean()"
],
"id": "121416e7bbab57bb",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 104
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.290642Z",
"start_time": "2025-11-04T22:33:09.283697Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"def single_layer_backward_propagation(dA_curr, W_curr, b_curr, Z_curr, A_prev, activation=\"relu\"):\n",
" m = A_prev.shape[1]\n",
"\n",
2025-11-04 23:06:08 +01:00
" if activation == \"relu\":\n",
2025-11-04 22:56:05 +01:00
" backward_activation_func = relu_backward\n",
2025-11-04 23:06:08 +01:00
" elif activation == \"sigmoid\":\n",
2025-11-04 22:56:05 +01:00
" backward_activation_func = sigmoid_backward\n",
" else:\n",
" raise Exception('Non-supported activation function')\n",
"\n",
" dZ_curr = backward_activation_func(dA_curr, Z_curr)\n",
" dW_curr = np.dot(dZ_curr, A_prev.T) / m\n",
" db_curr = np.sum(dZ_curr, axis=1, keepdims=True) / m\n",
" dA_prev = np.dot(W_curr.T, dZ_curr)\n",
"\n",
" return dA_prev, dW_curr, db_curr"
],
"id": "92e4b87664f18a63",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 105
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.345156Z",
"start_time": "2025-11-04T22:33:09.337966Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"def full_backward_propagation(Y_hat, Y, memory, params_values, nn_architecture):\n",
" grads_values = {}\n",
" m = Y.shape[1]\n",
" Y = Y.reshape(Y_hat.shape)\n",
"\n",
" dA_prev = - (np.divide(Y, Y_hat) - np.divide(1 - Y, 1 - Y_hat));\n",
"\n",
" for layer_idx_prev, layer in reversed(list(enumerate(nn_architecture))):\n",
" layer_idx_curr = layer_idx_prev + 1\n",
" activ_function_curr = layer[\"activation\"]\n",
"\n",
" dA_curr = dA_prev\n",
"\n",
" A_prev = memory[\"A\" + str(layer_idx_prev)]\n",
" Z_curr = memory[\"Z\" + str(layer_idx_curr)]\n",
" W_curr = params_values[\"W\" + str(layer_idx_curr)]\n",
" b_curr = params_values[\"b\" + str(layer_idx_curr)]\n",
"\n",
" dA_prev, dW_curr, db_curr = single_layer_backward_propagation(\n",
" dA_curr, W_curr, b_curr, Z_curr, A_prev, activ_function_curr)\n",
"\n",
" grads_values[\"dW\" + str(layer_idx_curr)] = dW_curr\n",
" grads_values[\"db\" + str(layer_idx_curr)] = db_curr\n",
"\n",
" return grads_values"
],
"id": "2c8e4eed1846f003",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 106
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.409004Z",
"start_time": "2025-11-04T22:33:09.405329Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"def update(params_values, grads_values, nn_architecture, learning_rate):\n",
" for layer_idx, layer in enumerate(nn_architecture):\n",
2025-11-05 12:25:25 +01:00
" # layer_idx=layer_idx+1\n",
2025-11-04 22:56:05 +01:00
" params_values[\"W\" + str(layer_idx)] -= learning_rate * grads_values[\"dW\" + str(layer_idx)]\n",
" params_values[\"b\" + str(layer_idx)] -= learning_rate * grads_values[\"db\" + str(layer_idx)]\n",
"\n",
" return params_values;"
],
"id": "16320b953a183511",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 107
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.455132Z",
"start_time": "2025-11-04T22:33:09.452975Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"def train(X, Y, nn_architecture, epochs, learning_rate, verbose=False, callback=None):\n",
" # initiation of neural net parameters\n",
" params_values = init_layers(nn_architecture, 2)\n",
" # initiation of lists storing the history\n",
" # of metrics calculated during the learning process\n",
" cost_history = []\n",
" accuracy_history = []\n",
"\n",
" # performing calculations for subsequent iterations\n",
" for i in range(epochs):\n",
" # step forward\n",
" Y_hat, cashe = full_forward_propagation(X, params_values, nn_architecture)\n",
"\n",
" # calculating metrics and saving them in history\n",
" cost = get_cost_value(Y_hat, Y)\n",
" cost_history.append(cost)\n",
" accuracy = get_accuracy_value(Y_hat, Y)\n",
" accuracy_history.append(accuracy)\n",
"\n",
" # step backward - calculating gradient\n",
" grads_values = full_backward_propagation(Y_hat, Y, cashe, params_values, nn_architecture)\n",
" # updating model state\n",
" params_values = update(params_values, grads_values, nn_architecture, learning_rate)\n",
"\n",
2025-11-04 23:26:22 +01:00
" if(i % 1000 == 0):\n",
2025-11-04 22:56:05 +01:00
" if(verbose):\n",
" print(\"Iteration: {:05} - cost: {:.5f} - accuracy: {:.5f}\".format(i, cost, accuracy))\n",
" if(callback is not None):\n",
" callback(i, params_values)\n",
"\n",
" return params_values"
],
"id": "fce33f70bba3898",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 108
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.500509Z",
"start_time": "2025-11-04T22:33:09.498676Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"import os\n",
"import tensorflow as tf\n",
"\n",
"from sklearn.datasets import make_moons\n",
"from sklearn.model_selection import train_test_split\n",
"\n",
"import seaborn as sns\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib import cm\n",
"from mpl_toolkits.mplot3d import Axes3D\n",
"sns.set_style(\"whitegrid\")\n",
"\n",
"import keras\n",
"from keras.models import Sequential\n",
"from keras.layers import Dense\n",
"# from keras.utils import np_utils\n",
"from keras import regularizers\n",
"\n",
"from sklearn.metrics import accuracy_score"
],
"id": "cccd73b5018799d4",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 109
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.547352Z",
"start_time": "2025-11-04T22:33:09.545714Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"# number of samples in the data set\n",
"N_SAMPLES = 1000\n",
"# ratio between training and test sets\n",
"TEST_SIZE = 0.1"
],
"id": "4f66ffa878f01c02",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 110
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.597021Z",
"start_time": "2025-11-04T22:33:09.592659Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"X, y = make_moons(n_samples = N_SAMPLES, noise=0.2, random_state=100)\n",
"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=TEST_SIZE, random_state=42)"
],
"id": "bebe0ed00a2d514",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 111
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.706999Z",
"start_time": "2025-11-04T22:33:09.649338Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
2025-11-04 23:26:22 +01:00
"params_values = train(np.transpose(X_train), np.transpose(y_train.reshape((y_train.shape[0], 1))), nn_architecture, 100000, 0.001, verbose=True)\n",
2025-11-04 22:56:05 +01:00
"# params_values\n"
],
"id": "ce04892d496c5147",
2025-11-04 23:26:22 +01:00
"outputs": [
{
2025-11-05 12:25:25 +01:00
"ename": "KeyError",
"evalue": "'W0'",
"output_type": "error",
"traceback": [
"\u001B[31m---------------------------------------------------------------------------\u001B[39m",
"\u001B[31mKeyError\u001B[39m Traceback (most recent call last)",
"\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[112]\u001B[39m\u001B[32m, line 1\u001B[39m\n\u001B[32m----> \u001B[39m\u001B[32m1\u001B[39m params_values = \u001B[43mtrain\u001B[49m\u001B[43m(\u001B[49m\u001B[43mnp\u001B[49m\u001B[43m.\u001B[49m\u001B[43mtranspose\u001B[49m\u001B[43m(\u001B[49m\u001B[43mX_train\u001B[49m\u001B[43m)\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mnp\u001B[49m\u001B[43m.\u001B[49m\u001B[43mtranspose\u001B[49m\u001B[43m(\u001B[49m\u001B[43my_train\u001B[49m\u001B[43m.\u001B[49m\u001B[43mreshape\u001B[49m\u001B[43m(\u001B[49m\u001B[43m(\u001B[49m\u001B[43my_train\u001B[49m\u001B[43m.\u001B[49m\u001B[43mshape\u001B[49m\u001B[43m[\u001B[49m\u001B[32;43m0\u001B[39;49m\u001B[43m]\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[32;43m1\u001B[39;49m\u001B[43m)\u001B[49m\u001B[43m)\u001B[49m\u001B[43m)\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mnn_architecture\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[32;43m100000\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[32;43m0.001\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mverbose\u001B[49m\u001B[43m=\u001B[49m\u001B[38;5;28;43;01mTrue\u001B[39;49;00m\u001B[43m)\u001B[49m\n\u001B[32m 2\u001B[39m \u001B[38;5;66;03m# params_values\u001B[39;00m\n",
"\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[108]\u001B[39m\u001B[32m, line 23\u001B[39m, in \u001B[36mtrain\u001B[39m\u001B[34m(X, Y, nn_architecture, epochs, learning_rate, verbose, callback)\u001B[39m\n\u001B[32m 21\u001B[39m grads_values = full_backward_propagation(Y_hat, Y, cashe, params_values, nn_architecture)\n\u001B[32m 22\u001B[39m \u001B[38;5;66;03m# updating model state\u001B[39;00m\n\u001B[32m---> \u001B[39m\u001B[32m23\u001B[39m params_values = \u001B[43mupdate\u001B[49m\u001B[43m(\u001B[49m\u001B[43mparams_values\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mgrads_values\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mnn_architecture\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mlearning_rate\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 25\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m(i % \u001B[32m1000\u001B[39m == \u001B[32m0\u001B[39m):\n\u001B[32m 26\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m(verbose):\n",
"\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[107]\u001B[39m\u001B[32m, line 4\u001B[39m, in \u001B[36mupdate\u001B[39m\u001B[34m(params_values, grads_values, nn_architecture, learning_rate)\u001B[39m\n\u001B[32m 1\u001B[39m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34mupdate\u001B[39m(params_values, grads_values, nn_architecture, learning_rate):\n\u001B[32m 2\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m layer_idx, layer \u001B[38;5;129;01min\u001B[39;00m \u001B[38;5;28menumerate\u001B[39m(nn_architecture):\n\u001B[32m 3\u001B[39m \u001B[38;5;66;03m# layer_idx=layer_idx+1\u001B[39;00m\n\u001B[32m----> \u001B[39m\u001B[32m4\u001B[39m \u001B[43mparams_values\u001B[49m\u001B[43m[\u001B[49m\u001B[33;43m\"\u001B[39;49m\u001B[33;43mW\u001B[39;49m\u001B[33;43m\"\u001B[39;49m\u001B[43m \u001B[49m\u001B[43m+\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;28;43mstr\u001B[39;49m\u001B[43m(\u001B[49m\u001B[43mlayer_idx\u001B[49m\u001B[43m)\u001B[49m\u001B[43m]\u001B[49m -= learning_rate * grads_values[\u001B[33m\"\u001B[39m\u001B[33mdW\u001B[39m\u001B[33m\"\u001B[39m + \u001B[38;5;28mstr\u001B[39m(layer_idx)]\n\u001B[32m 5\u001B[39m params_values[\u001B[33m\"\u001B[39m\u001B[33mb\u001B[39m\u001B[33m\"\u001B[39m + \u001B[38;5;28mstr\u001B[39m(layer_idx)] -= learning_rate * grads_values[\u001B[33m\"\u001B[39m\u001B[33mdb\u001B[39m\u001B[33m\"\u001B[39m + \u001B[38;5;28mstr\u001B[39m(layer_idx)]\n\u001B[32m 7\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m params_values\n",
"\u001B[31mKeyError\u001B[39m: 'W0'"
2025-11-04 23:26:22 +01:00
]
}
],
2025-11-05 12:25:25 +01:00
"execution_count": 112
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.744698885Z",
"start_time": "2025-11-04T22:30:15.730115Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"Y_test_hat, _ = full_forward_propagation(np.transpose(X_test), params_values, nn_architecture)\n",
"\n",
"acc_test = get_accuracy_value(Y_test_hat, np.transpose(y_test.reshape((y_test.shape[0], 1))))\n",
"print(\"Test set accuracy: {:.2f} - David\".format(acc_test))\n"
],
"id": "26e7a2a8848714d9",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test set accuracy: 0.46 - David\n"
]
}
],
2025-11-05 12:25:25 +01:00
"execution_count": 87
2025-11-04 23:26:22 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.745013072Z",
"start_time": "2025-11-04T22:30:15.777514Z"
}
},
"cell_type": "code",
"source": [
"# boundary of the graph\n",
"GRID_X_START = -1.5\n",
"GRID_X_END = 2.5\n",
"GRID_Y_START = -1.0\n",
"GRID_Y_END = 2\n",
"# output directory (the folder must be created on the drive)\n",
"OUTPUT_DIR = \"./binary_classification_vizualizations/\"\n",
"os.makedirs(OUTPUT_DIR, exist_ok=True)\n",
"### Definition of grid boundaries\n",
"grid = np.mgrid[GRID_X_START:GRID_X_END:100j, GRID_Y_START:GRID_Y_END:100j]\n",
"grid_2d = grid.reshape(2, -1).T\n",
"XX, YY = grid"
],
"id": "b070f03d55981894",
"outputs": [],
"execution_count": 88
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-04T22:33:09.756230428Z",
"start_time": "2025-11-04T22:30:15.825657Z"
2025-11-04 23:26:22 +01:00
}
},
"cell_type": "code",
"source": [
"def make_plot(X, y, plot_name, file_name=None, XX=None, YY=None, preds=None, dark=False):\n",
" if (dark):\n",
" plt.style.use('dark_background')\n",
" else:\n",
" sns.set_style(\"whitegrid\")\n",
" plt.figure(figsize=(16,12))\n",
" axes = plt.gca()\n",
" axes.set(xlabel=\"$X_1$\", ylabel=\"$X_2$\")\n",
" plt.title(plot_name, fontsize=30)\n",
" plt.subplots_adjust(left=0.20)\n",
" plt.subplots_adjust(right=0.80)\n",
" if(XX is not None and YY is not None and preds is not None):\n",
" plt.contourf(XX, YY, preds.reshape(XX.shape), 25, alpha = 1, cmap=cm.Spectral)\n",
" plt.contour(XX, YY, preds.reshape(XX.shape), levels=[.5], cmap=\"Greys\", vmin=0, vmax=.6)\n",
" plt.scatter(X[:, 0], X[:, 1], c=y.ravel(), s=40, cmap=plt.cm.Spectral, edgecolors='black')\n",
" if(file_name):\n",
" plt.savefig(file_name)\n",
" plt.close()"
],
"id": "553e08ddc23ab78c",
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 89
2025-11-04 23:26:22 +01:00
},
{
2025-11-05 12:25:25 +01:00
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-04T22:33:09.756818533Z",
"start_time": "2025-11-04T22:32:16.103852Z"
}
},
2025-11-04 23:26:22 +01:00
"cell_type": "code",
2025-11-05 12:25:25 +01:00
"source": "# make_plot(X, y, \"Dataset\")",
"id": "36c83562b7404392",
2025-11-04 23:26:22 +01:00
"outputs": [],
2025-11-05 12:25:25 +01:00
"execution_count": 96
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-04T22:33:09.757036155Z",
"start_time": "2025-11-04T22:31:17.522478Z"
}
},
"cell_type": "code",
2025-11-04 23:26:22 +01:00
"source": [
"def callback_numpy_plot(index, params):\n",
" plot_title = \"NumPy Model - It: {:05}\".format(index)\n",
" file_name = \"numpy_model_{:05}.png\".format(index // 50)\n",
" file_path = os.path.join(OUTPUT_DIR, file_name)\n",
" prediction_probs, _ = full_forward_propagation(np.transpose(grid_2d), params, nn_architecture)\n",
" prediction_probs = prediction_probs.reshape(prediction_probs.shape[1], 1)\n",
" make_plot(X_test, y_test, plot_title, file_name=file_path, XX=XX, YY=YY, preds=prediction_probs, dark=True)\n",
"\n",
"\n",
"# Training\n",
"params_values = train(np.transpose(X_train), np.transpose(y_train.reshape((y_train.shape[0], 1))), nn_architecture,\n",
2025-11-05 12:25:25 +01:00
" 100000, 0.001, False, callback_numpy_plot)\n",
2025-11-04 23:26:22 +01:00
"prediction_probs_numpy, _ = full_forward_propagation(np.transpose(grid_2d), params_values, nn_architecture)\n",
"prediction_probs_numpy = prediction_probs_numpy.reshape(prediction_probs_numpy.shape[1], 1)\n",
2025-11-05 12:25:25 +01:00
"\n",
2025-11-04 23:26:22 +01:00
"make_plot(X_test, y_test, \"NumPy Model\", file_name=None, XX=XX, YY=YY, preds=prediction_probs_numpy)"
],
2025-11-05 12:25:25 +01:00
"id": "b6a4d6a1a1fb289",
"outputs": [
{
"data": {
"text/plain": [
"<Figure size 1600x1200 with 1 Axes>"
],
"image/png": "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SFJGRoaefPJJHTx4UNKFb/4nTZqU+UEwJCREtWrVUp06dVSkSBG3x8bFxemRRx7JseDIKydPnnQ7Dg8Pz/x5RESEOnbsmHl85MgRrV279rKef+7cuVlKjX+uO+EJFy8guXr1asXFxeV47cmTJ7V69erM4/8y+yE9PV0PPPCA224afytRooTq16+vypUru/35kKSDBw/q9ttvd5uNcikffvihJk2alGWb0PDwcNWtW1fVq1dXQEBA5vnp06drypQpl/1+nE6nRo0apffeey/L78Xw8HDVqlVL9evXz3LbUEZGhp5//nlNmzbtsl8TAJC/UUAAAPKVChUqqH///pnH6enpev/99y1MlHeefPJJZWRkqGTJknrzzTe1bt06/fjjj5o9e7ZWr16tTz/91O3WiZSUFI0fP16bNm3Su+++K0lq0KCBpk2bpnXr1mnBggWaN2+e1q1bp5deesltGv6ff/6pb7/91mPvJSUlRdu3b3c7V758ebfjW2+91e145syZuX5+0zT1/fffZx7b7XbdfPPNV5D08vXq1Ut2u13ShQ/RP/zwQ47XLliwIPOWIbvdnmV3kMsxfvx4tzJDkjp27Kj58+dr5cqVmj17tn766SetXLlSTz75pFtJEB8fr0cfffRft6/dvHmzJkyY4HauXr16+vLLL7V27VrNnTtXixYt0tq1a/Xaa69llkrvv/++2y4fufH++++7zQ7x9fXVoEGDtGjRoszfv7Nnz9by5cv1yy+/6NZbb3VbiHbs2LHavHnzZb0mACB/o4AAAOQ7Q4cOdftwNXfu3MyZAAVZTEyMqlSpou+//159+vRRYGBg5phhGGrdurU+++wztyJh6dKlevbZZ+VyudS5c2fNmDFDrVq1yvyALF3YGvP222/X6NGj3V7v4g/weW369OmZMzn+1rJlS7fjVq1aqXLlypnHv/zyi86cOZOr51+3bp3bB942bdqodOnSVx74MpQsWVLXXntt5vGldsO4eOy6665TyZIlr+g1t23bluUb/4cffljvv/++ateu7XY+PDxc9957r77++msFBwdnnj98+HCWcuFiLpdLL774otvtFO3atdN3332nFi1auH34DwwM1M0336w5c+aoVKlScjgcio2NzfX72bx5s9usiaJFi2rGjBl67rnnVL169SzXV6xYUWPGjNH48eMzZ7k4HA69/PLLuX5NAED+RwEBAMh3SpYsqdtvvz3z2OFwaNKkSRYmyhu+vr6aMGHCJXeqqFKlituimE6nUwcPHlSlSpX01ltvZZl+f7E+ffq47YywZ8+eLLdJ5IXffvtNEydOdDv39y4G/3TxLIiMjIxsby/Izj/XALh4Vow3XHwrxd69e7V79+4s1+zYsUP79+/P9jGX64svvnArBtq3b6/hw4df8jH169fXK6+84nbu+++/d1sQ82KrVq1yy1uiRAmNHz/+kr+nypUrp/Hjx+fmLbh5//33M2/xsNls+uCDD9SgQYN/fVy3bt109913Zx7v3bs3y6wQAEDBRQEBAMiX7r//fgUFBWUe//jjj5m7SBRUnTt3zvJtdnY6dOiQ5dx9993nNmMiO4ZhZHlsdh+cL5fT6dSZM2e0YsUKPf7443rooYey3NM/YsSIbD/I9unTx21GR24WF4yPj9fPP/+ceVyiRAldf/31V/4GrkD
},
"metadata": {},
"output_type": "display_data",
"jetTransient": {
"display_id": null
}
}
],
"execution_count": 94
2025-11-04 23:26:22 +01:00
},
{
2025-11-05 12:25:25 +01:00
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-04T22:33:09.757406779Z",
"start_time": "2025-11-04T22:30:18.009348Z"
}
},
2025-11-04 23:26:22 +01:00
"cell_type": "code",
"source": "",
2025-11-05 12:25:25 +01:00
"id": "dc0b9266ae37298a",
"outputs": [],
"execution_count": null
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.757826120Z",
"start_time": "2025-11-04T22:30:18.058506Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
"model = Sequential()\n",
"model.add(Dense(25, input_dim=2,activation='relu'))\n",
"model.add(Dense(50, activation='relu'))\n",
"model.add(Dense(50, activation='relu'))\n",
"model.add(Dense(25, activation='relu'))\n",
"model.add(Dense(1, activation='sigmoid'))\n",
"\n",
"model.compile(loss='binary_crossentropy', optimizer=\"sgd\", metrics=['accuracy'])\n",
"\n",
"# Training\n",
"history = model.fit(X_train, y_train, epochs=200, verbose=0)"
],
"id": "f05ff40ed26e45c2",
2025-11-05 12:25:25 +01:00
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/oskar/projects/nn-from-scratch/.venv/lib/python3.13/site-packages/keras/src/layers/core/dense.py:95: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.\n",
" super().__init__(activity_regularizer=activity_regularizer, **kwargs)\n"
]
}
],
"execution_count": 91
2025-11-04 22:56:05 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.758008336Z",
"start_time": "2025-11-04T22:30:23.750365Z"
2025-11-04 22:56:05 +01:00
}
},
"cell_type": "code",
"source": [
2025-11-04 23:06:08 +01:00
"Y_test_prob = model.predict(X_test)\n",
"Y_test_hat = (Y_test_prob > 0.5).astype(int).ravel()\n",
2025-11-04 22:56:05 +01:00
"acc_test = accuracy_score(y_test, Y_test_hat)\n",
"print(\"Test set accuracy: {:.2f} - Goliath\".format(acc_test))"
],
"id": "ef52bee9c93081d3",
"outputs": [
2025-11-04 23:06:08 +01:00
{
"name": "stdout",
"output_type": "stream",
"text": [
2025-11-04 23:26:22 +01:00
"\u001B[1m4/4\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 7ms/step \n",
2025-11-05 12:25:25 +01:00
"Test set accuracy: 0.99 - Goliath\n"
2025-11-04 23:06:08 +01:00
]
}
],
2025-11-05 12:25:25 +01:00
"execution_count": 92
2025-11-04 23:06:08 +01:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-05 12:25:25 +01:00
"end_time": "2025-11-04T22:33:09.758218682Z",
"start_time": "2025-11-04T22:30:23.812884Z"
2025-11-04 23:06:08 +01:00
}
},
"cell_type": "code",
"source": [
"def callback_keras_plot(epoch, logs):\n",
" plot_title = \"Keras Model - It: {:05}\".format(epoch)\n",
" file_name = \"keras_model_{:05}.png\".format(epoch)\n",
" file_path = os.path.join(OUTPUT_DIR, file_name)\n",
2025-11-04 23:26:22 +01:00
" prediction_probs = model.predict(grid_2d, batch_size=32, verbose=0)\n",
" prediction_probs = prediction_probs.reshape(-1)\n",
2025-11-04 23:06:08 +01:00
" make_plot(X_test, y_test, plot_title, file_name=file_path, XX=XX, YY=YY, preds=prediction_probs)\n",
"\n",
"\n",
"# Adding callback functions that they will run in every epoch\n",
"testmodelcb = keras.callbacks.LambdaCallback(on_epoch_end=callback_keras_plot)\n",
"\n",
"# Building a model\n",
"model = Sequential()\n",
"model.add(Dense(25, input_dim=2, activation='relu'))\n",
"model.add(Dense(50, activation='relu'))\n",
"model.add(Dense(50, activation='relu'))\n",
"model.add(Dense(25, activation='relu'))\n",
"model.add(Dense(1, activation='sigmoid'))\n",
"\n",
"model.compile(loss='binary_crossentropy', optimizer=\"sgd\", metrics=['accuracy'])\n",
"\n",
"# Training\n",
"history = model.fit(X_train, y_train, epochs=200, verbose=0, callbacks=[testmodelcb])\n",
2025-11-04 23:26:22 +01:00
"prediction_probs = model.predict(grid_2d, batch_size=32, verbose=0)\n",
"prediction_probs = prediction_probs.reshape(-1)\n",
"make_plot(X_test, y_test, \"Keras Model\", file_name=None, XX=XX, YY=YY, preds=prediction_probs)\n"
2025-11-04 23:06:08 +01:00
],
"id": "6feab7da06e7a828",
"outputs": [
2025-11-05 12:25:25 +01:00
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/oskar/projects/nn-from-scratch/.venv/lib/python3.13/site-packages/keras/src/layers/core/dense.py:95: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.\n",
" super().__init__(activity_regularizer=activity_regularizer, **kwargs)\n"
]
},
2025-11-04 23:06:08 +01:00
{
2025-11-04 23:26:22 +01:00
"data": {
"text/plain": [
"<Figure size 1600x1200 with 1 Axes>"
],
2025-11-05 12:25:25 +01:00
"image/png": "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r1VZWVnHz/6bS4biP/1QXl6uOXPmWD73qlWr9KlPfapT+FBYWKhhw4Zp3LhxnT6Uezwe/frXv9a3vvWtmM5x4sSJTpM90juvx/Hjx6tPnz4dt588eTLkmmKR6tc+ACC3MQEBAMgpu3fv1sc//vGA+kFpaal+85vfaOrUqXE/X319vT772c/qzJkzHbeNHj1ad955p+bMmROwj0JbW5teeOEF/fznP++oJhw6dEjf/OY39cgjj8gwjJjO+cgjj6ihoUGSdP311+vOO+/UmDFjOu5vb2/X66+/HvLY/v37a/Hixbrmmms0YcKETnsAmKapnTt36sknn9RTTz0lj8cjyVcDmTt3rhYsWBB2XY8//njAh95evXrprrvu0qJFiwI+8F86z+HDh/X6669rxYoVeu2112L63ZOhoKBA119/vf785z9LkpYsWaK7775bBQUFnR7rcrm0ZMmSjp+vv/76kI+LxbFjx/Tv//7vamtr67itvLxcX/va13T99dcHvFbWr1+vhx56SGvXru247emnn9aECROiVlW+/e1v68iRIx0/5+fn6zOf+Yw+/OEPq6KiouP23bt36z//8z+1fPlynTx5Uj/5yU/i+n3S8doHAOQ2JiAAADlj+/bt+tjHPhYQPpSXl+sPf/iDpfBBku6+++6Ab44/+MEP6umnn9YNN9zQaRPHwsJCXX/99Xr66ac1ZcqUjttffvllrVy5MuZzXgofvvWtb+nnP/95QPgg+T5whprkuOeee7Ry5Up94xvf0MyZM0NuQGgYhsaMGaN77rlH//M//xPwYfs3v/lNxHXV1tZ2/HNBQYEef/xxfeADH+gUPlw6z6BBg/ShD31Iv/vd7/T888/L6XRG/sWTyL9GcfbsWb388sshH/fiiy/q3LlzIY+L17333qvz5893/FxVVaVnnnlG73//+zu9VqZMmaLHH39c733vewNu/9GPfqQTJ06EPceSJUv0yiuvdPxcUFCg//mf/9HnP//5gPBB8l2W9OGHH+7YA8M/tIhFOl77AIDcRgABAMgZDz74YMC3tZWVlXr00Uc1fvx4S8+3YcOGgA9711xzje69917l5+dHPK5Hjx56+OGHAz6k/f73v4/r3DfccINuu+22uI65/PLL4/qQf9VVV+mTn/xkx8+bNm2KuOeE//TD9OnTNXz48JjPNXz48JR+Cz5p0iQNHTq042f/moU//3rGsGHDNHHiREvn27dvn1atWtXxs8Ph0H/+53+quro67DEOh0P3339/wAaULS0tHZMbofzxj38M+Pmuu+7SzJkzI67trrvu0lVXXRXlNwiUztc+ACB3EUAAAHJG8M783/rWtzR69GjLzxf8Ye/uu++O+UN079699f73v7/j53Xr1nVMNsTiS1/6UsyPTcR73vOegJ/Xr18f9rGtra0d/5zKS2tadeONN3b887/+9a+AcEryXU7S/0O2/+Pj9de//jXg9XfDDTfEFGbk5eXp61//esBtf/nLX0JeZWLv3r3asGFDx899+/bVxz72sZjWF3yOaNL52gcA5C4CCABAzrrvvvti2rk/FK/XG/Dh9NLmivEI/tb57bffjum4CRMmaPDgwXGdy6oBAwYE/Lxt27awj/Xf2PDtt9/W0aNHbVtXMrz3ve/tuBRke3u7nn/++YD7n3vuObndbkm+aYT
2025-11-04 23:26:22 +01:00
},
"metadata": {},
"output_type": "display_data",
"jetTransient": {
"display_id": null
}
2025-11-04 22:56:05 +01:00
}
],
2025-11-05 12:25:25 +01:00
"execution_count": 93
2025-11-04 18:05:00 +01:00
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}