{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":3004,"databundleVersionId":861823,"sourceType":"competition"}],"dockerImageVersionId":30035,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Red Neuronal Simple para MNIST desde cero\n\nEn este notebook, implementaremos una red neuronal simple de dos capas y la entrené en el conjunto de datos MNIST para el reconocimiento de dígitos. Está pensado como un ejemplo instructivo, a través del cual puedes entender mejor las matemáticas subyacentes de las redes neuronales.\n\nEste es el esquema básico de la red neuronal simple de dos capas aplicada al conjunto de datos MNIST.\n\n(original source: https://www.kaggle.com/code/wwsalmon/simple-mnist-nn-from-scratch-numpy-no-tf-keras)","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom matplotlib import pyplot as plt\n\ndata = pd.read_csv('/kaggle/input/digit-recognizer/train.csv')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2024-09-28T10:49:58.409956Z","iopub.execute_input":"2024-09-28T10:49:58.410428Z","iopub.status.idle":"2024-09-28T10:50:01.207263Z","shell.execute_reply.started":"2024-09-28T10:49:58.410386Z","shell.execute_reply":"2024-09-28T10:50:01.205344Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"data = np.array(data)\nm, n = data.shape\nnp.random.shuffle(data) # shuffle before splitting into dev and training sets\n\ndata_dev = data[0:1000].T\nY_dev = data_dev[0]\nX_dev = data_dev[1:n]\nX_dev = X_dev / 255.\n\ndata_train = data[1000:m].T\nY_train = data_train[0]\nX_train = data_train[1:n]\nX_train = X_train / 255.\n_,m_train = X_train.shape","metadata":{"execution":{"iopub.status.busy":"2024-09-28T10:50:01.210323Z","iopub.execute_input":"2024-09-28T10:50:01.210674Z","iopub.status.idle":"2024-09-28T10:50:02.015921Z","shell.execute_reply.started":"2024-09-28T10:50:01.210643Z","shell.execute_reply":"2024-09-28T10:50:02.014727Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"Y_train","metadata":{"execution":{"iopub.status.busy":"2024-09-28T10:50:02.017400Z","iopub.execute_input":"2024-09-28T10:50:02.017712Z","iopub.status.idle":"2024-09-28T10:50:02.024102Z","shell.execute_reply.started":"2024-09-28T10:50:02.017683Z","shell.execute_reply":"2024-09-28T10:50:02.023016Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Nuestra red neuronal tendrá una arquitectura simple de dos capas. La capa de entrada $a^{[0]}$ tendrá 784 unidades correspondientes a los 784 píxeles en cada imagen de entrada de 28x28. Una capa oculta $a^{[1]}$ tendrá 10 unidades con activación ReLU, y finalmente nuestra capa de salida $a^{[2]}$ tendrá 10 unidades correspondientes a las diez clases de dígitos con activación softmax.\n\n## **Propagación hacia adelante (Forward propagation)**\n\nLa **propagación hacia adelante** es el proceso en el que los datos pasan por las diferentes capas de la red neuronal, desde la entrada hasta la salida, produciendo una predicción final.\n\n1. **Primera capa** (Capa oculta):\n\n$$Z^{[1]} = W^{[1]} X + b^{[1]}$$\n\nAquí \\(W^{[1]}\\) son los **pesos** y \\(b^{[1]}\\) son los **sesgos** (bias) de la primera capa. \\(X\\) es la **entrada** y \\(Z^{[1]}\\) es el valor intermedio antes de aplicar la activación.\n\nLuego, aplicamos la **función de activación ReLU** para generar la salida de la primera capa:\n\n$$A^{[1]} = g_{\\text{ReLU}}(Z^{[1]})$$\n\n2. **Segunda capa** (Capa de salida):\n\n$$Z^{[2]} = W^{[2]} A^{[1]} + b^{[2]}$$\n\nFinalmente, aplicamos la **función softmax** para obtener una probabilidad para cada clase:\n\n$$A^{[2]} = g_{\\text{softmax}}(Z^{[2]})$$\n\n---\n\n## **Propagación hacia atrás (Backward propagation)**\n\nLa **propagación hacia atrás** es el proceso de ajustar los pesos y sesgos de la red con base en el error.\n\n1. **Gradiente de la segunda capa**:\n\n$$dZ^{[2]} = A^{[2]} - Y$$\n\nSe calcula la derivada de los pesos:\n\n$$dW^{[2]} = \\frac{1}{m} dZ^{[2]} A^{[1]^T}$$\n\nY la derivada de los sesgos:\n\n$$dB^{[2]} = \\frac{1}{m} \\sum {dZ^{[2]}}$$\n\n2. **Gradiente de la primera capa**:\n\n$$dZ^{[1]} = W^{[2]^T} dZ^{[2]} \\cdot g^{[1]\\prime} (Z^{[1]})$$\n\nLuego, se calcula la derivada de los pesos de la primera capa:\n\n$$dW^{[1]} = \\frac{1}{m} dZ^{[1]} A^{[0]^T}$$\n\nY la derivada de los sesgos:\n\n$$dB^{[1]} = \\frac{1}{m} \\sum {dZ^{[1]}}$$\n\n---\n\n## **Actualización de parámetros**\n\n1. Actualización de los pesos y sesgos de la segunda capa:\n\n$$W^{[2]} := W^{[2]} - \\alpha dW^{[2]}$$\n\n$$b^{[2]} := b^{[2]} - \\alpha db^{[2]}$$\n\n2. Actualización de los pesos y sesgos de la primera capa:\n\n$$W^{[1]} := W^{[1]} - \\alpha dW^{[1]}$$\n\n$$b^{[1]} := b^{[1]} - \\alpha db^{[1]}$$\n\n---\n\n### **Función ReLU**\n\nLa **función ReLU** (Rectified Linear Unit) es una de las funciones de activación más utilizadas en redes neuronales debido a su simplicidad y eficacia. Su objetivo principal es introducir no linealidades en el modelo, lo que permite que la red neuronal aprenda relaciones más complejas en los datos.\n\nLa función se define matemáticamente como:\n\n$$\n\\text{ReLU}(x) = \\max(0, x)\n$$\n\nEsto significa que para cualquier valor de entrada \\(x\\), la función devolverá \\(0\\) si \\(x\\) es negativo, y devolverá \\(x\\) si \\(x\\) es positivo. \n\nEsta propiedad puede expresarse también como una función a trozos:\n\n$$\n\\text{ReLU}(x) =\n\\begin{cases} \n0 & \\text{si } x < 0 \\\\\nx & \\text{si } x \\geq 0\n\\end{cases}\n$$\n\n### **Características de la función ReLU:**\n\n1. **No linealidad**: A pesar de que la función es lineal para valores positivos, al \"apagar\" los valores negativos, introduce no linealidades en el modelo.\n  \n2. **Eficiencia computacional**: La función ReLU es computacionalmente eficiente porque no requiere operaciones complejas como exponenciales, lo que la hace rápida de calcular.\n\n3. **Evita el problema del desvanecimiento del gradiente**: A diferencia de funciones como la sigmoide o la tangente hiperbólica, ReLU no se satura para valores positivos, lo que ayuda a mantener gradientes más grandes durante la retropropagación.\n\n4. **\"Muerte\" de neuronas**: Un inconveniente de ReLU es que, si una neurona se activa con un valor negativo, puede quedar \"muerta\" y nunca volver a activarse, lo que significa que nunca contribuirá al aprendizaje. Este problema se puede mitigar usando variantes de ReLU, como Leaky ReLU.\n\n### **Gráficamente**\n\nSi representamos la función gráficamente, veremos que:\n\n- Para valores de \\(x < 0\\), la función se mantiene en \\(0\\).\n- Para valores de \\(x \\geq 0\\), la función crece linealmente, es decir, \\(y = x\\).\n\n---\n### Softmax \n\n$$\\text{Softmax}(z_i) = \\frac{e^{z_i}}{\\sum_{j=1}^{10} e^{z_j}}$$\n\n**¿Qué es la función Softmax?**\nLa función Softmax es una función de activación que se utiliza comúnmente en redes neuronales, especialmente para tareas de clasificación, como el reconocimiento de dígitos. Su trabajo es convertir los valores de salida \"en bruto\" (conocidos como logits) del modelo en probabilidades que sumen 1, es decir, una distribución de probabilidad sobre las clases posibles.\n\nEn un ejercicio de reconocimiento de dígitos, la red neuronal recibe una imagen de un dígito escrito a mano (por ejemplo, del conjunto de datos MNIST) y debe predecir qué número es (0, 1, 2... hasta 9).\n\nAl final de la red neuronal, hay 10 neuronas de salida (una para cada posible dígito). Estas neuronas devuelven números llamados logits que representan la \"confianza\" del modelo en cada clase, pero estos valores no son directamente interpretables como probabilidades.\n\n**¿Cómo funciona Softmax?**\nLa función Softmax toma estos logits y los convierte en probabilidades que suman 1, de modo que cada número representa la probabilidad de que la imagen pertenezca a cada clase (o dígito).\n\nEl **proceso matemático** es el siguiente:\n1. Se calcula la exponencial de cada logit (haciendo que todos los números sean positivos).\n2. Luego, se dividen estos valores exponenciales por la suma de todas las exponenciales, normalizando así los resultados para que sumen 1.\n\nDonde:\n- $(z_i)$ es el logit para la clase $𝑖$ (el número que representa el modelo antes de aplicar Softmax).\n- El denominador es la suma de todas las exponenciales de los logits de las 10 clases.\n\n---\n\n---\n\n### **Propagación hacia adelante (Forward Propagation)**\n\nEn la **propagación hacia adelante**, los datos pasan a través de la red neuronal desde la entrada hasta la salida, produciendo una predicción final. A continuación, se detallan las dimensiones de cada variable en esta etapa:\n\n- **Entrada**:\n  $A^{[0]} = X: \\quad 784 \\times m$\n  - Aquí, $X$ es la matriz de entrada que representa imágenes de dígitos en un formato vectorizado. Cada imagen tiene 784 píxeles (28x28) y $m$ es el número de imágenes en el lote.\n\n- **Primera capa**:\n  $Z^{[1]} \\sim A^{[1]}: \\quad 10 \\times m$\n  - $Z^{[1]}$ es el valor intermedio antes de aplicar la activación en la primera capa. Después de aplicar la función de activación (ReLU), obtendremos $A^{[1]}$, que tiene 10 unidades (una para cada dígito del 0 al 9).\n\n- **Pesos y sesgos de la primera capa**:\n  $W^{[1]}: \\quad 10 \\times 784 \\quad (\\text{ya que } W^{[1]} A^{[0]} \\sim Z^{[1]})$\n  - $W^{[1]}$ son los pesos que conectan la entrada con la primera capa oculta, y tienen dimensiones $10 \\times 784$.\n\n  $B^{[1]}: \\quad 10 \\times 1$\n  - $B^{[1]}$ son los sesgos de la primera capa.\n\n- **Segunda capa**:\n  $Z^{[2]} \\sim A^{[2]}: \\quad 10 \\times m$\n  - $Z^{[2]}$ es el valor intermedio antes de aplicar la activación en la segunda capa. Al aplicar la función softmax, obtendremos $A^{[2]}$, que también tiene 10 unidades, representando las probabilidades para cada dígito.\n\n- **Pesos y sesgos de la segunda capa**:\n  $W^{[2]}: \\quad 10 \\times 10 \\quad (\\text{ya que } W^{[2]} A^{[1]} \\sim Z^{[2]})$\n  - $W^{[2]}$ son los pesos que conectan la primera capa con la segunda capa (capa de salida).\n\n  $B^{[2]}: \\quad 10 \\times 1$\n  - $B^{[2]}$ son los sesgos de la segunda capa.\n\n---\n\n### **Propagación hacia atrás (Backward Propagation)**\n\nLa **propagación hacia atrás** es el proceso de ajustar los pesos y sesgos de la red en función del error. Aquí se presentan las dimensiones de las variables durante esta etapa:\n\n- **Gradientes de la segunda capa**:\n  $dZ^{[2]}: \\quad 10 \\times m \\quad (\\sim A^{[2]})$\n  - $dZ^{[2]}$ es el error en la capa de salida, que se calcula como la diferencia entre las predicciones $A^{[2]}$ y las etiquetas reales $Y$.\n\n  $dW^{[2]}: \\quad 10 \\times 10$\n  - $dW^{[2]}$ es el gradiente de los pesos de la segunda capa.\n\n  $dB^{[2]}: \\quad 10 \\times 1$\n  - $dB^{[2]}$ es el gradiente de los sesgos de la segunda capa.\n\n- **Gradientes de la primera capa**:\n  $dZ^{[1]}: \\quad 10 \\times m \\quad (\\sim A^{[1]})$\n  - $dZ^{[1]}$ es el error en la primera capa, que se propaga hacia atrás a partir del error de la segunda capa.\n\n  $dW^{[1]}: \\quad 10 \\times 10$\n  - $dW^{[1]}$ es el gradiente de los pesos de la primera capa.\n\n  $dB^{[1]}: \\quad 10 \\times 1$\n  - $dB^{[1]}$ es el gradiente de los sesgos de la primera capa.\n\n---\n\n### Resumen\n\n1. **Forward propagation**: Calculamos las salidas de cada capa utilizando los pesos, sesgos y las funciones de activación, generando finalmente probabilidades para cada clase.\n2. **Backward propagation**: Ajustamos los pesos y sesgos de la red basándonos en los errores calculados, lo que permite mejorar la precisión del modelo en futuras predicciones.\n\nEsta estructura y explicación ofrecen una comprensión clara de las dimensiones y el flujo de datos a través de la red neuronal en cada fase del proceso de aprendizaje.","metadata":{}},{"cell_type":"code","source":"def init_params():\n    W1 = np.random.rand(10, 784) - 0.5\n    b1 = np.random.rand(10, 1) - 0.5\n    W2 = np.random.rand(10, 10) - 0.5\n    b2 = np.random.rand(10, 1) - 0.5\n    return W1, b1, W2, b2\n\ndef ReLU(Z):\n    return np.maximum(Z, 0)\n\ndef softmax(Z):\n    exp_Z = np.exp(Z - np.max(Z, axis=0))  # Shift for numerical stability\n    return exp_Z / np.sum(exp_Z, axis=0)\n    \ndef forward_prop(W1, b1, W2, b2, X):\n    Z1 = W1.dot(X) + b1\n    A1 = ReLU(Z1)\n    Z2 = W2.dot(A1) + b2\n    A2 = softmax(Z2)\n    return Z1, A1, Z2, A2\n\ndef ReLU_deriv(Z):\n    return Z > 0\n\ndef one_hot(Y):\n    one_hot_Y = np.zeros((Y.size, Y.max() + 1))\n    one_hot_Y[np.arange(Y.size), Y] = 1\n    one_hot_Y = one_hot_Y.T\n    return one_hot_Y\n\ndef backward_prop(Z1, A1, Z2, A2, W1, W2, X, Y):\n    one_hot_Y = one_hot(Y)\n    dZ2 = A2 - one_hot_Y\n    dW2 = 1 / m * dZ2.dot(A1.T)\n    db2 = 1 / m * np.sum(dZ2)\n    dZ1 = W2.T.dot(dZ2) * ReLU_deriv(Z1)\n    dW1 = 1 / m * dZ1.dot(X.T)\n    db1 = 1 / m * np.sum(dZ1)\n    return dW1, db1, dW2, db2\n\ndef update_params(W1, b1, W2, b2, dW1, db1, dW2, db2, alpha):\n    W1 = W1 - alpha * dW1\n    b1 = b1 - alpha * db1    \n    W2 = W2 - alpha * dW2  \n    b2 = b2 - alpha * db2    \n    return W1, b1, W2, b2","metadata":{"execution":{"iopub.status.busy":"2024-09-28T10:50:02.025643Z","iopub.execute_input":"2024-09-28T10:50:02.025971Z","iopub.status.idle":"2024-09-28T10:50:02.046340Z","shell.execute_reply.started":"2024-09-28T10:50:02.025939Z","shell.execute_reply":"2024-09-28T10:50:02.044970Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def get_predictions(A2):\n    return np.argmax(A2, 0)\n\ndef get_accuracy(predictions, Y):\n    print(predictions, Y)\n    return np.sum(predictions == Y) / Y.size\n\ndef gradient_descent(X, Y, alpha, iterations):\n    W1, b1, W2, b2 = init_params()\n    for i in range(iterations):\n        Z1, A1, Z2, A2 = forward_prop(W1, b1, W2, b2, X)\n        dW1, db1, dW2, db2 = backward_prop(Z1, A1, Z2, A2, W1, W2, X, Y)\n        W1, b1, W2, b2 = update_params(W1, b1, W2, b2, dW1, db1, dW2, db2, alpha)\n        if i % 10 == 0:\n            print(\"Iteration: \", i)\n            predictions = get_predictions(A2)\n            print(get_accuracy(predictions, Y))\n    return W1, b1, W2, b2","metadata":{"execution":{"iopub.status.busy":"2024-09-28T10:50:02.049883Z","iopub.execute_input":"2024-09-28T10:50:02.050382Z","iopub.status.idle":"2024-09-28T10:50:02.064310Z","shell.execute_reply.started":"2024-09-28T10:50:02.050342Z","shell.execute_reply":"2024-09-28T10:50:02.063257Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"W1, b1, W2, b2 = gradient_descent(X_train, Y_train, 0.10, 500)","metadata":{"execution":{"iopub.status.busy":"2024-09-28T10:50:02.066337Z","iopub.execute_input":"2024-09-28T10:50:02.066802Z","iopub.status.idle":"2024-09-28T10:50:42.660959Z","shell.execute_reply.started":"2024-09-28T10:50:02.066748Z","shell.execute_reply":"2024-09-28T10:50:42.659874Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"~85% de precision con la data entrenada.","metadata":{}},{"cell_type":"code","source":"def make_predictions(X, W1, b1, W2, b2):\n    _, _, _, A2 = forward_prop(W1, b1, W2, b2, X)\n    predictions = get_predictions(A2)\n    return predictions\n\ndef test_prediction(index, W1, b1, W2, b2):\n    current_image = X_train[:, index, None]\n    prediction = make_predictions(X_train[:, index, None], W1, b1, W2, b2)\n    label = Y_train[index]\n    print(\"Prediction: \", prediction)\n    print(\"Label: \", label)\n    \n    current_image = current_image.reshape((28, 28)) * 255\n    plt.gray()\n    plt.imshow(current_image, interpolation='nearest')\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2024-09-28T10:50:42.662961Z","iopub.execute_input":"2024-09-28T10:50:42.663351Z","iopub.status.idle":"2024-09-28T10:50:42.673271Z","shell.execute_reply.started":"2024-09-28T10:50:42.663313Z","shell.execute_reply":"2024-09-28T10:50:42.672293Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Vamos a laznar el modelo, a ver qué nos muestra:","metadata":{}},{"cell_type":"code","source":"test_prediction(0, W1, b1, W2, b2)\ntest_prediction(1, W1, b1, W2, b2)\ntest_prediction(2, W1, b1, W2, b2)\ntest_prediction(3, W1, b1, W2, b2)\n\ntest_prediction(4, W1, b1, W2, b2)\ntest_prediction(5, W1, b1, W2, b2)\ntest_prediction(6, W1, b1, W2, b2)\ntest_prediction(7, W1, b1, W2, b2)\ntest_prediction(8, W1, b1, W2, b2)\ntest_prediction(9, W1, b1, W2, b2)","metadata":{"execution":{"iopub.status.busy":"2024-09-28T10:50:42.674999Z","iopub.execute_input":"2024-09-28T10:50:42.675371Z","iopub.status.idle":"2024-09-28T10:50:44.374646Z","shell.execute_reply.started":"2024-09-28T10:50:42.675333Z","shell.execute_reply":"2024-09-28T10:50:44.372188Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Finalmente, calculamos la precisión en el proceso predictivo así:","metadata":{}},{"cell_type":"code","source":"dev_predictions = make_predictions(X_dev, W1, b1, W2, b2)\n(dev_predictions, Y_dev)","metadata":{"execution":{"iopub.status.busy":"2024-09-28T10:50:44.378414Z","iopub.execute_input":"2024-09-28T10:50:44.379132Z","iopub.status.idle":"2024-09-28T10:50:44.394507Z","shell.execute_reply.started":"2024-09-28T10:50:44.379078Z","shell.execute_reply":"2024-09-28T10:50:44.393366Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Se logra aproximadamente un 84% de precisión, por lo que nuestro modelo generalizó bien a partir de los datos de entrenamiento.\n","metadata":{}}]}