<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://tavo91.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tavo91.github.io/" rel="alternate" type="text/html" /><updated>2024-03-07T01:30:54+00:00</updated><id>https://tavo91.github.io/feed.xml</id><title type="html">Gustavo’s website</title><subtitle>Natural Language Processing and Machine Learning</subtitle><author><name>Gustavo Aguilar</name></author><entry><title type="html">Random chess game</title><link href="https://tavo91.github.io/random-chess-game/" rel="alternate" type="text/html" title="Random chess game" /><published>2020-10-02T00:00:00+00:00</published><updated>2020-10-02T00:00:00+00:00</updated><id>https://tavo91.github.io/random-chess-game</id><content type="html" xml:base="https://tavo91.github.io/random-chess-game/"><![CDATA[<h1 id="chess">Chess!</h1>

<div id="myBoard" style="width: 300px"></div>
<p><button onclick="doChessMoves(board, game, ['e4', 'e5', 'Nf3', 'f5'], 1000)">Moves 1</button>
<button onclick="doChessMoves(board, game, ['d4', 'fxe4', 'Nxe5', 'Nf6'], 1000)">Moves 2</button>
<button onclick="showRandomGame(board, game)">Random</button></p>

<script type="text/javascript">
	game = new Chess();
	board = Chessboard('myBoard', 'start');
</script>]]></content><author><name>Gustavo Aguilar</name></author><category term="chess" /><summary type="html"><![CDATA[Random chess game]]></summary></entry><entry><title type="html">Introducción Práctica a Deep Learning (Taller 1)</title><link href="https://tavo91.github.io/data-science-el-salvador-taller-intro-deep-learning/" rel="alternate" type="text/html" title="Introducción Práctica a Deep Learning (Taller 1)" /><published>2020-01-13T00:00:00+00:00</published><updated>2020-01-13T00:00:00+00:00</updated><id>https://tavo91.github.io/taller-deep-learning-el-salvador</id><content type="html" xml:base="https://tavo91.github.io/data-science-el-salvador-taller-intro-deep-learning/"><![CDATA[<p><em><strong>Nota</strong>: aquí están los 
<a href="https://l.facebook.com/l.php?u=https%3A%2F%2Fdocs.google.com%2Fpresentation%2Fd%2F1vGsRTk50Ho9s_nqX-OTNaatA0DiK9yFAk9_sxLu3HYg%2Fedit%3Fusp%3Dsharing%26fbclid%3DIwAR3fTbtkByTw85MWVCUpWvSR46VGBp5gFjUL-q2PFVLoRIZAufGRPXa9mNg&amp;h=AT1nO2Do7efQeSTt3k_KQopeyqkyC5b4upehqZCuymOO08KXKINJm7MF4z5iYaEhMv_mSdoFgfrfm7srK6FeBTCqOYbjSEvXb9w3a9B0IXizQo9Ejf7t51Theo8FSWf3F-eMp1kQ4NYtZjUfLSxKx8BC7egllh6UNTch703UgI6FfYslJl-F4NCX6lKJx6U-yRJH7fH_ztFWd6Hn8ZR3hE5AkN3TMheDnvP0d-IQzTYctGYPqTlh3S_pHcGV1e0WAaNkSFa5HuJALemYkCiaHt3tcdELBEFiyrYEWPpoAt3GJFeTETF0V7SpG4KUV7jO1Xym6wmt7pqn2uuoZYHcUj-9dE9w7zhKmKSKdR6-CCi32NdTT1FOtXJsuzqoQwbDGKkhrn62RVC_7ATXVqO0IjknolmSuuT-EJBy6gYO0vzAZgRBP8XdydF7EiOjw2dV7BIzfG_9fotDW0PM">slides</a>,
el código <a href="https://colab.research.google.com/drive/1sBTn8PTcqUru9_pn8-toIBECN_qu9jZ-#scrollTo=bnZ5tKyi_Gab">parte 1</a> y <a href="https://colab.research.google.com/drive/1_CLmMcdLpXPZrAWmw2bPdufcBQ62tfTM">parte 2</a>, 
y el <a href="https://www.facebook.com/137784603720765/videos/2628659754087006/">video</a> de Facebook live del taller.</em></p>

<div>
    <img id="main-photo" src="/assets/post_assets/dses_2019_taller1/taller1_pic1.jpg" />
    <div style="display:inline-block; margin: 5px" align="center">
        
            <a>
                <img class="photo-gallery" src="/assets/post_assets/dses_2019_taller1/taller1_pic1.jpg" width="22%" />
            </a>
        
            <a>
                <img class="photo-gallery" src="/assets/post_assets/dses_2019_taller1/taller1_pic2.jpg" width="22%" />
            </a>
        
            <a>
                <img class="photo-gallery" src="/assets/post_assets/dses_2019_taller1/taller1_pic3.jpg" width="22%" />
            </a>
        
            <a>
                <img class="photo-gallery" src="/assets/post_assets/dses_2019_taller1/taller1_pic4.jpg" width="22%" />
            </a>
        
    </div>
</div>
<p><br /></p>

<script type="text/javascript">
    $('img.photo-gallery').each(function(index, elem){
        $(elem).on('click', function(){
            $("#main-photo").attr('src', $(elem).attr('src'));
        });
    });
</script>

<p>Durante las vacaciones de fin de año tuve la oportunidad de viajar a mi país y llevar a cabo
dos talleres de inteligencia artificial con la comunidad de 
<a href="https://www.facebook.com/Data-Science-El-Salvador-137784603720765/">Data Science El Salvador</a>. 
En el primer taller cubrimos redes neuronales desde cero teórica y prácticamente utilizando Python y la librería NumPy.
En el segundo, nos enfocamos en PyTorch, uno de los frameworks más populares para deep learning.
El taller despertó bastante interés en la comunidad :fire:, incluyendo la participación de
estudiantes de la UCA y la UES, compañías como SPOT, Applaudo Studios, y Avianca, e incluso miembros 
de otras comunidades de desarrollo de software como HorchataJS! Gracias a todos por asistir! :raised_hands: 
Y especiales gracias a <a href="https://www.linkedin.com/in/ricardo-rios-sv/">Ricardo Ríos</a> por organizar el evento! :ok_hand:</p>

<p>Este post cubre la implementación de redes neuronales desde cero incluyendo un poco de teoría. Específicamente:</p>
<ol>
  <li><strong>Introducción al perceptron</strong></li>
  <li><strong>Función de error</strong></li>
  <li><strong>Gradiente descendente</strong></li>
  <li><strong>Redes neuronales</strong></li>
  <li><strong>Propagación del error (backpropagation)</strong></li>
  <li><strong>Implementación</strong></li>
</ol>

<p>Para entrenar y evaluar el modelo, vamos a utilizar dos datasets:</p>
<ol>
  <li><strong>Moons dataset</strong> (sección 7): el dataset de lunas :moon:</li>
  <li><strong>MNIST dataset</strong> (sección 8): el dataset de reconocimiento de dígitos escritos a mano :writing_hand:</li>
</ol>

<h2 id="1-introducción-al-perceptron">1. Introducción al perceptron</h2>

<p>Las redes neuronales artificiales están inspiradas en las redes neuronales humanas. 
El principal concepto detrás es el <strong>perceptron</strong>, que escencialmente representa <strong>una neurona</strong>.
Los componentes de un perceptron son las señales de entrada, la transformación lineal, la activación, y la salida.</p>

<!--
<table style="border:0px !important;" align="center">
<tr>
    <td style="border:0px !important" width="60%">
        <div align="center">
            <figure>
                <img src='/assets/post_assets/dses_2019_taller1/perceptron.png'/>
                <figcaption>Fig. 1 - Partes de un perceptron</figcaption>
            </figure>
        </div>
    </td>
    <td style="border:0px !important">
        <div align="center">
            <figure>
                <img src='/assets/post_assets/dses_2019_taller1/neuron.png' />
                <figcaption>By <a href="//commons.wikimedia.org/wiki/User:BruceBlaus" title="User:BruceBlaus">BruceBlaus</a> - <span class="int-own-work" lang="en">Own work</span>, <a href="https://creativecommons.org/licenses/by/3.0" title="Creative Commons Attribution 3.0">CC BY 3.0</a>, <a href="https://commons.wikimedia.org/w/index.php?curid=28761830">Link</a></figcaption>
            </figure>
        </div>
    </td>
</tr>
</table>
-->

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/perceptron.png" width="60%" />
        <figcaption>Fig. 1 - Partes de un perceptron</figcaption>
    </figure>
</div>

<p>En la Figura 1 se pueden observar las entradas \(x_1, x_2, \dots, x_n\) y la salida \(\hat{y}\) en los bloques azules. 
Si consideramos el ejemplo de otorgamientos de préstamos, las entradas podrían ser datos del cliente como la edad, el salario, tiempo 
trabajando continuamente, etc. Mientras que la salida sería si se le otorga el préstamo al cliente o no. 
La decisión de entregar o no el préstamo la determina la función de activación, que en este caso es la función escalón
unitario (genera cero si el préstamo es rechazado o uno si es otorgado). Para poder decidir sobre el préstamo, el
modelo utiliza los parámetros \(\theta = \{w_1, w_2, w_3, \dots, w_n, b\}\). 
Estos parámetros son pesos que determinan lo relevante que son 
cada uno de los elementos de entrada (por ejemplo, el salario del cliente es más importante que su estado civil). 
Los componentes del perceptron pueden escribirse de la siguiente manera:</p>

\[\begin{aligned}
    \hat{y} =&amp; ~g(w_1 x_1 + w_2 x_2 + \dots + w_n x_n + b)  \\\\
    &amp; ~g(x) =
        \begin{cases}
            0 ~~~~\mathrm{si} ~~x &lt; 0, \\
            1 ~~~~\mathrm{si} ~~x \ge 0
        \end{cases}
\end{aligned}\]

<p>Con este simple modelo podríamos predecir si una persona es apta para un préstamo o no, y la calidad de nuestro modelo
depende de los parámetros que tenga.</p>

<h4 id="cómo-encuentro-los-parámetros-adecuados-thinking"><strong>¿Cómo encuentro los parámetros adecuados? :thinking:</strong></h4>

<p>Si pensamos en valores aleatorios para cada uno de los parámetros \(w\)’s, estaríamos otorgando préstamos 
a clientes sin importar sus condiciones y características. Sin embargo, con esos valores iniciales podemos 
determinar qué tan malo es el modelo, y a partir de ahí podemos mejorarlo.</p>

<p>Para mejorarlo tenemos que cuantificar el error asociado al modelo, y minimizarlo. Esto nos lleva a definir
una función de error.</p>

<h2 id="2-función-de-error">2. Función de error</h2>

<p>El siguiente diagrama muestra dos modelos que discriminan cuatro puntos. 
El modelo de la izquierda classifica erróneamente dos puntos (los puntos \(s_2\) y \(s_3\)), mientras que el de la derecha clasifica 
correctamente los cuatro puntos. Este simple conteo nos dice que el modelo de la derecha es mejor que el de la izquierda.
El problema ahora es que al intentar una línea diferente puede que sigamos teniendo los mismos dos errores, y no sabríamos
si nos estamos acercando al modelo de la derecha o no.</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/error_function_example.png" width="70%" />
        <figcaption>Fig. 2 - Modelo con parámetros iniciales (izquierda) y parámetros ideales (derecha). </figcaption>
    </figure>
</div>

<p>Nuestro principal problema es que el perceptron produce <strong>valores discretos</strong> (ceros o unos a partir de la función escalón unitario) y para monitorear que cada 
movimiento de la línea es una mejora necesitamos <strong>valores continuos</strong>. ¿Qué tal si solo utilizamos la transformación lineal 
sin pasar a la función de activación? El problema es que podemos tener tanto valores negativos como positivos por 
cada punto, y esto no permite la simple suma de los errores.</p>

<p>Además, nos interesa saber qué tan probable es un punto de recibir cierta clase (por ejemplo, un cliente de recibir un préstamo). 
Por lo tanto, necesitamos hacer los siguientes cambios:</p>
<ol>
  <li>Modificar la función de <strong>activación discreta a continua</strong></li>
  <li>Generar valores de salida en un <strong>espacio de probabilidad</strong></li>
</ol>

<p>Para ello vamos a utilizar la función sigmoid:</p>

<div align="center">
<figure>
    <table style="border:0px !important; max-width: 60% !important; text-align=center !important; margin: 0px" align="center">
    <tr>
        <td style="border:0px !important" width="60%">
            <img src="/assets/post_assets/dses_2019_taller1/sigmoid.png" />
        </td>
        <td style="border:0px !important">
            $$ \sigma(z) = \frac{1}{1+e^{-z}} $$
        </td>
    </tr>
    </table>
    <figcaption>Fig. 3 - Función sigmoid. </figcaption>
</figure>
</div>

<p>Esta función toma cualquier valor y lo proyecta en un espacio continuo entre 0 y 1 (un espacio de probabilidad). 
Además, la función intercepta en 0.5 cuando el valor de entrada es 0, lo que permite definir equilibradamente 
si el modelo escoge una clase u otra como la clase más probable:</p>

\[decision(z) =
        \begin{cases}
            0 ~~~~\mathrm{si} ~~\sigma(z) &lt; 0.5, \\
            1 ~~~~\mathrm{si} ~~\sigma(z) \ge 0.5
        \end{cases}\]

<h4 id="estimación-de-la-probabilidad-máxima-mle"><strong>Estimación de la Probabilidad Máxima (MLE)</strong></h4>

<p>Ahora que el modelo produce valores continuos podemos mejorar la línea de clasificación maximizando las probabilidades. 
Como nos interesa clasificar varios puntos correctamente y a la vez producir un solo valor para evaluar el modelo, 
vamos a calcular la probabilidad conjunta de todos los puntos considerando cada punto como un evento independiente 
condicionado a los parámetros \(\theta\) del modelo:</p>

\[\begin{aligned}
    \mathrm{P}(s_1, s_2, \dots, s_n) &amp;= ~\prod_{i=1}^N ~\mathrm{P}(s_i |~\theta)
\end{aligned}\]

<p>Sin embargo, multiplicar tantas probabilidades reduciría rápidamente la resolución del resultado, y muy probablemente 
generaría error de “underflow”. En lugar de multiplicar usaremos sumas con ayuda de logaritmos:</p>

\[\begin{aligned}
    \mathrm{P}(s_1, s_2, \dots, s_n) 
        &amp;= ~\prod_{i=1}^N ~\mathrm{P}(s_i |~ \theta) \\
        &amp;= ~log~\prod_{i=1}^N ~\mathrm{P}(s_i |~\theta) \\
        &amp;= ~log~\mathrm{P}(s_1|~\theta) + log~\mathrm{P}(s_2|~\theta) + \dots + log~\mathrm{P}(s_n|~\theta) \\ 
        &amp;= ~\sum_{i=1}^N log~\mathrm{P}(s_i|~\theta) \\
\end{aligned}\]

<p>Esta fórmula nos ayuda a maximizar el modelo. 
Sin embargo, en deep learning utilizamos el algoritmo “gradiente descendente” para optimizar 
nuestros modelos a partir de minimizar una función de error.
Por tanto, en lugar de maximizar nuestra fórmula, vamos a minizarla haciendo negativa la expresión anterior:</p>

<h4>
$$
\begin{aligned}
\operatorname*{argmin}_{\theta} ~-\sum_{i=1}^N log~\mathrm{P}(s_i |~\theta) \\
\end{aligned} 
$$
</h4>

<p>A esta fórmula se le conoce como <strong>“cross-entropy”</strong> o <strong>“negative log-likelihood”</strong>, 
y también se utiliza bastante en “information gain”.</p>

<h4 id="bulb-pero-por-qué-estamos-minimizando-algo-con-signo-negativo-thinking">:bulb: Pero… ¿por qué estamos minimizando algo con signo negativo? :thinking:</h4>

<p>Si graficamos la función negativa del logaritmo obtenemos la curva de la Figura 4. 
Recuerda que estamos sacando el logaritmo de probabilidades, así que nuestros \(x\)’s
están en el dominio de 0 a 1 (parte roja). Nota que cuando la función negativa del logaritmo recibe la 
máxima probabilidad (es decir, 1), el valor que genera es 0. Esto es equivalente a decir
no hay ningún error porque el modelo está 100% seguro de la predicción. Opuestamente, si 
el modelo está, por ejemplo, 20% seguro, el error va a ser mayor a cero, y significa que el modelo aún tiene que mejorar. 
En pocas palabras, al maximizar las probabilidades también estamos minimizando el error, 
que es lo que nos interesa para optimizar el modelo.</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/cross_entropy.png" width="35%" />
        <figcaption>Fig. 4 - Negative log-likelihood. </figcaption>
    </figure>
</div>

<h4 id="función-de-error-para-clasificación-binaria"><strong>Función de error para clasificación binaria</strong></h4>

<p>Hasta ahora la función de error nos dice el error de cada clase. 
Por ejemplo, el error tanto de otorgar un préstamo a un cliente como también el error de rechazarlo.
Sin embargo, cuando corregimos a nuestro modelo solo vamos a corregirlo de acuerdo a la decisión que debía haber tomado.
Si el modelo tenía que haber rechazado el préstamo, entonces solo utilizamos ese error e ignoramos el error de otorgar el préstamo.</p>

<p>Asumamos que aceptar el préstamo está representado por el número 1; y rechazarlo, por el 0. 
Nuestro valor real (lo que esperaríamos que el modelo aprenda) es \(y_i\) y la probabilidad de predecir la clase \(y_i\) 
está dada por \(p_i\):</p>

<h4>
$$ \mathcal{L}_{ce} = - \frac{1}{N} \sum_{i=1}^{N} y_i log(p_i) + (1-y_i) log(1 - p_i) $$
</h4>

<p>Nota que por cada ejemplo \(i\) la expresión anterior cancela uno de sus dos términos dependiendo del valor de \(y_i\). 
Si \(y_i = 0\) (e.g., rechazar el préstamo), se cancela el término de la izquierda y se usa el de la derecha, y viceversa.</p>

<h2 id="3-gradiente-descendente">3. Gradiente descendente</h2>

<p>Ya tenemos el modelo con sus parámetros y la función de error. Ahora necesitamos optimizar el modelo, y para ello vamos a
utilizar el <strong>gradiente descendente</strong>. Los pasos del algoritmo son los siguientes:</p>

<ol>
  <li>
    <p>Generar las predicciones \(\hat{y}\) a partir de los parámetros actuales del modelo:</p>

\[\hat{y} = \sigma(w_1 x_1 + \dots + w_n x_n + b)\]
  </li>
  <li>
    <p>Calcular el error de las predicciones:</p>

\[\mathcal{L} = - \frac{1}{N} \sum_{i=1}^{N} y_i log(\hat{y}_i) + (1-y_i) log(1 - \hat{y}_i)\]
  </li>
  <li>
    <p>Calcular el gradiente o error asociado a cada uno de los parámetros del modelo por medio de derivadas parciales:</p>

\[\nabla \mathcal{L} = (\frac{\partial \mathcal{L}}{\partial w_1}, \dots, \frac{\partial \mathcal{L}}{\partial w_n}, \frac{\partial \mathcal{L}}{\partial b})\]
  </li>
  <li>
    <p>Actualizar los parámetros utilizando el gradiente:</p>

\[w_i \leftarrow w_i - \alpha \frac{\partial \mathcal{L}}{\partial w_i}; ~~~ b \leftarrow b - \alpha \frac{\partial \mathcal{L}}{\partial b}\]
  </li>
  <li>
    <p>Volver al paso 1 con mejores predicciones que la iteración actual.</p>
  </li>
</ol>

<p>Algunos detalles importantes son que en el paso 1 asumimos <strong>parámetros aleatorios</strong> como punto de partida. 
En el paso 4 utilizamos \(\alpha\) como el <strong>radio de aprendizaje</strong> (“learning rate”). 
La idea de \(\alpha\) es que podamos optimizar el modelo más establemente, asegurándonos de converger en el mínimo local del error. 
Un \(\alpha\) muy grande haría modificaciones severas en los parámetros, y nos llevaría a diverger de la solución que buscamos.</p>

<p>Cabe resaltar que en el paso 4 <strong>restamos</strong> el delta de modificación (el error multiplicado por el radio de aprendizaje) al parámetro actual.
Esto se debe a que estamos minimizando el gradiente, no maximizándolo, y por tanto debemos usar la dirección opuesta:</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/gradient.png" width="35%" />
        <figcaption>Fig. 5 - Dirección opuesta del gradiente. </figcaption>
    </figure>
</div>

<h2 id="4-redes-neuronales">4. Redes neuronales</h2>

<p>Hasta aquí nuestro modelo es un simple perceptron con limitada capacidad de abstracción. 
Sin embargo, el perceptron es el componente básico de una red neuronal artificial, compuesta de muchos perceptrons.
La forma en la que varios perceptrons actuan conjuntamente es utilizando la salida de uno como la entrada de otro.
A la composición de perceptrons se les llama <strong>multi-layer perceptron (MLP)</strong>, que es equivalente a una red neuronal.</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/mlp_idea.png" width="60%" />
        <figcaption>Fig. 6 - Combinación de modelos para incrementar complejidad. </figcaption>
    </figure>
</div>

<p>En la Figura 6 vemos que al combinar dos modelos simples (lado izquierdo) podemos mejorar la capacidad de abstracción del modelo de la derecha. 
De hecho, podríamos agregar pesos a cada modelo simple y decir que queremos priorizar más un modelo que otro con el fin de mejorar el modelo final. 
En esencia, esto es equivalente a generar otro perceptron que recibe las salidas de los modelos previos.
Las redes neuronales son precisamente eso, combinación de varios perceptrons.</p>

<p>Veamos la siguiente red neuronal:</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/mlp.png" width="60%" />
        <figcaption>Fig. 7 - Multi-later perceptron de dos capas. </figcaption>
    </figure>
</div>

<p>Esta red neuronal tiene dos entradas \(x_1, x_2\) (<strong><font color="grey">círculos grises</font></strong>) y dos capas neuronales. 
La primera capa es de cinco neuronas (<strong><font color="red">círculos rojos</font></strong>) y la segunda es de dos neuronas (<strong><font color="blue">círculos azules</font></strong>). 
La primera capa puede variar en la cantidad de neuronas, pero la segunda se define a partir del número de clases posibles (por ejemplo, para predecir dígitos usaríamos 10 neuronas).
Nota que cada una de las neuronas es equivalente al perceptron que definimos anteriormente, y por tanto, cada conexión de la figura
representa un parámetro o peso de la red.</p>

<p>:bulb: <em><strong>NOTA:</strong> en el caso de clasificación binaria podríamos utilizar una sola neurona, pero por practicidad y generalización a múltiple clases vamos a usar tantas neuronas como clases sean.</em></p>

<p>Los parámetros de la Figura 7 están representados por las matrices \(\mathrm{W}_{1}\) y \(\mathrm{W}_{2}\), 
cuyos índices se refieren a la capa a la que pertenecen. Hay que tomar en cuenta que los interceptos \(b_{1}\) y \(b_{2}\) 
están omitidos por simplicidad, pero también son parte del modelo. Así es como se verían las matrices de parámetros:</p>

\[\mathrm{W}_{1} = 
    \begin{bmatrix}
        w_{1,1} &amp; w_{1,2} &amp; w_{1,3} &amp; w_{1,4} &amp; w_{1,5}\\
        w_{2,1} &amp; w_{2,2} &amp; w_{2,3} &amp; w_{2,4} &amp; w_{2,5}
    \end{bmatrix}_{2 \times 5} ~~~~~
\mathrm{W}_{2} = 
    \begin{bmatrix}
        w_{1,1} &amp; w_{1,2} \\
        w_{2,1} &amp; w_{2,2} \\
        w_{3,1} &amp; w_{3,2} \\
        w_{4,1} &amp; w_{4,2} \\
        w_{5,1} &amp; w_{5,2} \\
    \end{bmatrix}_{5 \times 2}\]

<p>En notación de matrices, nuestra red neuronal podría escribirse de la siguiente forma:</p>

\[\begin{aligned}
z_1 &amp;= x~\mathrm{W}_1 + b_1 \\
a_1 &amp;= \sigma(z_1) \\
\\
z_2 &amp;= a_{1} \mathrm{W}_{2} + b_{2} \\
\hat{y} &amp;= a_2 = \sigma(z_2)  
\end{aligned}\]

<p>Aquí tanto \(x\) como \(\hat{y}\) son matrices de la forma \(n \times 2\), siendo \(n\) el número de ejemplos.</p>

<h2 id="5-propagación-del-error-backpropagation">5. Propagación del error (“Backpropagation”)</h2>

<p>Optimizar la red neuronal es un poco más complicado que optimizar un solo perceptron. 
Sin embargo, ocupamos el mismo principio de asociar parte del error global \(\mathcal{L}\) a cada uno de los parámetros. 
La diferencia con el perceptron es que en la red neuronal tenemos funciones de funciones.
Por tanto, necesitamos aplicar la regla de la cadena para obtener el delta del error que generó cada parámetro,
incluyendo los parámetros de la capa incial.</p>

<p>Nuestro objetivo es encontrar las derivadas parciales del error con respecto a los parámetros \(\mathrm{W}_2, b_2, \mathrm{W}_1, b_1\):</p>

\[\begin{aligned}
\nabla \mathcal{L} = (
    \frac{\partial \mathcal{L}}{\partial \mathrm{W}_2}, 
    \frac{\partial \mathcal{L}}{\partial b_2},
    \frac{\partial \mathcal{L}}{\partial \mathrm{W}_1},
    \frac{\partial \mathcal{L}}{\partial b_1}
)
\end{aligned}\]

<p>Aplicando la regla de la cadena para los parámetros \(W_1, W_2\), tendríamos las siguientes expresiones:</p>

\[\begin{aligned}
    \frac{\partial \mathcal{L}}{\partial \mathrm{W}_2} &amp;=
        \frac{\partial \mathcal{L}}{\partial \hat{y}} 
            \frac{\partial \hat{y}}{\partial z_2}
                \frac{\partial z_2}{\partial \mathrm{W}_2}
    \\
    \frac{\partial \mathcal{L}}{\partial \mathrm{W}_1} &amp;=
        \frac{\partial \mathcal{L}}{\partial \hat{y}} 
            \frac{\partial \hat{y}}{\partial z_2}
                \frac{\partial z_2}{\partial a_1}
                    \frac{\partial a_1}{\partial z_1}
                            \frac{\partial z_1}{\partial \mathrm{W}_1}
    \\
\end{aligned}\]

<p>El cálculo de las derivadas para cada uno de los parámetros lo colocaré <strong><a href="">aquí (enlace pendiente)</a></strong>. 
Por ahora solo utilizaremos las soluciones directamente.</p>

<h4>
$$
\begin{aligned}
    \delta_3 = \frac{\partial \mathcal{L}}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial z_2} &amp;= \hat{y} - y \\
    \frac{\partial \mathcal{L}}{\partial \mathrm{W}_2} &amp;= a_1^{\intercal} \delta_3 \\
    \frac{\partial \mathcal{L}}{\partial b_2} &amp;= 1^{\intercal} \delta_3 \\
    \\
    \delta_2 = \delta_3 \mathrm{W}_2^{\intercal} * &amp; \sigma'(z_1) \\
    \frac{\partial \mathcal{L}}{\partial \mathrm{W}_1} &amp;= a_0^{\intercal}\delta_2 = x^{\intercal}\delta_2 \\
    \frac{\partial \mathcal{L}}{\partial b_1} &amp;= 1^{\intercal} \delta_2
\end{aligned}
$$
</h4>

<p>Ahora que tenemos las derivadas parciales podemos seguir el mismo procedimiento del gradiente descendente.</p>

<h2 id="6-implementación">6. Implementación</h2>

<p>Finalmente hemos llegado a la parte divertida del post! Felicidades por leer hasta aquí! :tada::clap::clap:
Ahora vamos a implementar el mismo modelo de la Figura 7.</p>

<p>El plan de la implementación va así:</p>
<ol>
  <li><strong>Declaración de parámetros</strong>. Haremos una clase en Python que contenga los parámetros y los inicialice con valores aleatorios en el constructor.</li>
  <li><strong>Forward pass</strong>. Agregaremos un método a la clase para generar las predicciones.</li>
  <li><strong>Backward pass</strong>. Otro método para calcular el gradiente (error asociado a los parámetros).</li>
  <li><strong>Gradiente descendente</strong>. En el tercer método implementaremos el gradiente descendente.</li>
  <li><strong>Entrenamiento</strong>. Durante la optimización vamos a monitorear el error global (otro método!) para verificar que el modelo vaya mejorando.</li>
</ol>

<p>Antes de empezar con la implementación de la red, vamos a definir las funciones 
\(\sigma(\cdot)\), \(\sigma'(\cdot)\), y \(softmax(\cdot)\). 
La función \(softmax(\cdot)\) se encarga de normalizar la salida final del modelo de forma que cada neurona
esté asociada a cierta probabilidad y que a la vez todas las neuronas de la capa de salida sumen a 1.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sigmoid</span><span class="p">(</span><span class="n">z</span><span class="p">):</span>
    <span class="k">return</span> <span class="mi">1</span> <span class="o">/</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">z</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">d_sigmoid</span><span class="p">(</span><span class="n">z</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">sigmoid</span><span class="p">(</span><span class="n">z</span><span class="p">))</span> <span class="o">*</span> <span class="n">sigmoid</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">softmax</span><span class="p">(</span><span class="n">z</span><span class="p">):</span>
    <span class="n">exp_zi</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">exp_zi</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">exp_zi</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<h4 id="paso-1"><strong>Paso 1</strong></h4>

<p>Llamaremos a nuestra clase <code class="language-plaintext highlighter-rouge">NeuralNet</code>, y vamos a inicializar los parámetros aleatoriamente. 
Las dimensiones de nuestras matrices se podrán pasar por los argumentos del constructor.
Además, vamos a tener un <code class="language-plaintext highlighter-rouge">cache</code> para almacenar los cálculos del “forward pass” que necesitaremos en el “backward pass”.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">NeuralNet</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_dim</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">output_dim</span><span class="o">=</span><span class="mi">2</span><span class="p">):</span>
        <span class="c1"># Guardamos las dimensiones 
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">inp_dim</span> <span class="o">=</span> <span class="n">input_dim</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">hid_dim</span> <span class="o">=</span> <span class="n">hidden_dim</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">out_dim</span> <span class="o">=</span> <span class="n">output_dim</span>
        
        <span class="c1"># Creamos la primera capa con valores aleatorios
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">W1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">rand</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">inp_dim</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">hid_dim</span><span class="p">)</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">inp_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">b1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">hid_dim</span><span class="p">))</span>
        
        <span class="c1"># Creamos la segunda capa (la de salida)
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">W2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">rand</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">hid_dim</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">out_dim</span><span class="p">)</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">hid_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">b2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">out_dim</span><span class="p">))</span>
        
        <span class="c1"># Un cache para facilitar el calculo en el "backward pass"
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">cache</span> <span class="o">=</span> <span class="bp">None</span>
</code></pre></div></div>

<h4 id="paso-2"><strong>Paso 2</strong></h4>

<p>El “forward pass” es bastante simple. Tomamos la entrada <code class="language-plaintext highlighter-rouge">x</code>, la transformamos linealmente (<code class="language-plaintext highlighter-rouge">z1</code>) y la activamos (<code class="language-plaintext highlighter-rouge">a1</code>). 
Lo mismo hacemos con la segunda capa de la red utilizando la salida de la primera capa. 
El método retorna las predicciones del modelo.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
        <span class="n">z1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">W1</span><span class="p">)</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">b1</span>
        <span class="n">a1</span> <span class="o">=</span> <span class="n">sigmoid</span><span class="p">(</span><span class="n">z1</span><span class="p">)</span>

        <span class="n">z2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">W2</span><span class="p">)</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">b2</span>
        <span class="n">y_hat</span> <span class="o">=</span> <span class="n">softmax</span><span class="p">(</span><span class="n">z2</span><span class="p">)</span>  <span class="c1"># &lt;-- softmax en lugar de sigmoid
</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">cache</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">'a0'</span><span class="p">:</span> <span class="n">x</span><span class="p">,</span>
            <span class="s">'z1'</span><span class="p">:</span> <span class="n">z1</span><span class="p">,</span>
            <span class="s">'a1'</span><span class="p">:</span> <span class="n">a1</span><span class="p">,</span>
            <span class="s">'z2'</span><span class="p">:</span> <span class="n">z2</span><span class="p">,</span>
            <span class="s">'a2'</span><span class="p">:</span> <span class="n">y_hat</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">y_hat</span>
</code></pre></div></div>

<p>Nota que estamos utilizando <code class="language-plaintext highlighter-rouge">softmax</code> en lugar de <code class="language-plaintext highlighter-rouge">sigmoid</code>. De hecho, utilizar <code class="language-plaintext highlighter-rouge">sigmoid</code> sería más preciso para una tarea binaria;
solo necesitamos una neurona para manejar dos clases. 
Sin embargo, queremos que este mismo código sea generalizable para tareas con más de dos clases, 
y la función <code class="language-plaintext highlighter-rouge">softmax</code> se encarga de manejar \(n\) clases.</p>

<p>Otro detalle importante es que el <code class="language-plaintext highlighter-rouge">cache</code> contiene las transformaciones lineales (<code class="language-plaintext highlighter-rouge">z</code>’s) y las activaciones (<code class="language-plaintext highlighter-rouge">a</code>’s).
Tanto la entrada \(x\) como la salida \(\hat{y}\) han sido estandarizadas con la misma nomenclatura. 
La idea es que este código pueda expandirse a una cantidad arbitraria de capas (tal como lo hicimos durante el taller 2).</p>

<h4 id="paso-3"><strong>Paso 3</strong></h4>

<p>Para la implementación de la función <code class="language-plaintext highlighter-rouge">backward</code> utilizaremos las derivadas que calculamos en la sección de propagación del error.
Esta función solo recibe las clases reales por cada ejemplo en la entrada, y utiliza las transformaciones lineales y activaciones 
guardadas en el <code class="language-plaintext highlighter-rouge">cache</code>. 
El método retorna un diccionario con el resultado de las derivadas parciales del error con respecto a cada parámetro.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">backward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
      <span class="s">"""
      y: vector de forma (N,) con N samples y cada uno con un valor entre [0, C-1)
      """</span>
      <span class="n">delta3</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">copy</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">cache</span><span class="p">[</span><span class="s">'a2'</span><span class="p">])</span>  <span class="c1"># y_hat -&gt; la ultima activacion de la red
</span>      <span class="n">delta3</span><span class="p">[</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">y</span><span class="p">)),</span> <span class="n">y</span><span class="p">]</span> <span class="o">-=</span> <span class="mi">1</span>       <span class="c1"># delta3 -&gt; y_hat - y -&gt; dL/dy_hat * dy_hat/dz2
</span>
      <span class="n">dW2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">cache</span><span class="p">[</span><span class="s">'a1'</span><span class="p">].</span><span class="n">T</span><span class="p">,</span> <span class="n">delta3</span><span class="p">)</span>
      <span class="n">db2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">delta3</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>  <span class="c1"># alternativamente: np.dot(np.ones((1, len(y))), delta3)
</span>
      <span class="n">delta2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">delta3</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">W2</span><span class="p">.</span><span class="n">T</span><span class="p">)</span> <span class="o">*</span> <span class="n">d_sigmoid</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">cache</span><span class="p">[</span><span class="s">'z1'</span><span class="p">])</span>
      <span class="n">dW1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">matmul</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">cache</span><span class="p">[</span><span class="s">'a0'</span><span class="p">].</span><span class="n">T</span><span class="p">,</span> <span class="n">delta2</span><span class="p">)</span>
      <span class="n">db1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">delta2</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span> 

      <span class="n">grad_dict</span> <span class="o">=</span> <span class="p">{</span>
          <span class="s">'dW2'</span><span class="p">:</span> <span class="n">dW2</span><span class="p">,</span>
          <span class="s">'db2'</span><span class="p">:</span> <span class="n">db2</span><span class="p">,</span>
          <span class="s">'dW1'</span><span class="p">:</span> <span class="n">dW1</span><span class="p">,</span>
          <span class="s">'db1'</span><span class="p">:</span> <span class="n">db1</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="n">grad_dict</span>
</code></pre></div></div>

<p>Es importante destacar que si hubiesen más capas, el proceso para calcular las derivadas se vuelve repetitivo. 
A excepción de la capa final (cuya derivada es \(\hat{y} - y\)), podríamos repetir el proceso \(n\) veces. 
Como mencioné anteriormente, esto está en el código del segundo taller.</p>

<h4 id="paso-4"><strong>Paso 4</strong></h4>

<p>Ahora que ya tenemos listas las funciones de <code class="language-plaintext highlighter-rouge">forward</code> y <code class="language-plaintext highlighter-rouge">backward</code> podemos implementar el algoritmo del gradiente descendente.
Para ello vamos a definir el método <code class="language-plaintext highlighter-rouge">train</code> que recibe como argumentos tanto la entrada \(x\) como la salida esperada \(y\). 
Además, el método recibe el radio de aprendizaje (learning rate, <code class="language-plaintext highlighter-rouge">lr</code>) para generar pequeños pasos al reducir el error. 
Es importante experimentar con este valor ya que un valor muy pequeño haría que el entrenamiento se alargue mucho, 
mientras que un valor muy grande podría hacernos diverger del mínimo local de la función de error que queremos alcanzar.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">train</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">iters</span><span class="o">=</span><span class="mi">200000</span><span class="p">,</span> <span class="n">lr</span><span class="o">=</span><span class="mf">0.01</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span>
        <span class="c1"># Gradiente descendente.
</span>        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">iters</span><span class="o">+</span><span class="mi">1</span><span class="p">):</span>
            <span class="c1"># Correr el 'forward pass'
</span>            <span class="n">probs</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">forward</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

            <span class="c1"># Colectar los gradientes del 'backward pass'
</span>            <span class="n">grad_dict</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">backward</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>

            <span class="c1"># Actualizar los parametros con el gradiente descendiente 
</span>            <span class="c1"># NOTA: necesitamos un pequeño paso negativo 
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">W1</span> <span class="o">+=</span> <span class="o">-</span><span class="n">lr</span> <span class="o">*</span> <span class="n">grad_dict</span><span class="p">[</span><span class="s">'dW1'</span><span class="p">]</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">b1</span> <span class="o">+=</span> <span class="o">-</span><span class="n">lr</span> <span class="o">*</span> <span class="n">grad_dict</span><span class="p">[</span><span class="s">'db1'</span><span class="p">]</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">W2</span> <span class="o">+=</span> <span class="o">-</span><span class="n">lr</span> <span class="o">*</span> <span class="n">grad_dict</span><span class="p">[</span><span class="s">'dW2'</span><span class="p">]</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">b2</span> <span class="o">+=</span> <span class="o">-</span><span class="n">lr</span> <span class="o">*</span> <span class="n">grad_dict</span><span class="p">[</span><span class="s">'db2'</span><span class="p">]</span>

            <span class="k">if</span> <span class="n">verbose</span> <span class="ow">and</span> <span class="n">i</span> <span class="o">%</span> <span class="mi">1000</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
                <span class="k">print</span><span class="p">(</span><span class="s">"Error en la iteracion %i: %f"</span> <span class="o">%</span> <span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_loss</span><span class="p">(</span><span class="n">probs</span><span class="p">,</span> <span class="n">y</span><span class="p">)))</span>
</code></pre></div></div>

<p>Cabe mencionar que la actualización de los parámetros se hace con la suma de un valor negativo.
Como mencioné anteriormente, para minimizar el error tenemos que ir en la dirección opuesta al gradiente, 
de lo contrario estaríamos máximizando el error.</p>

<h4 id="paso-5"><strong>Paso 5</strong></h4>

<p>Las últimas dos líneas de la función de entrenamiento hacen que cada 1,000 iteraciones el modelo imprima el error global. 
De esta forma podemos monitorear si el modelo va mejorando o no.
La función <code class="language-plaintext highlighter-rouge">get_loss</code> está implementada en base a la fórmula del error de “cross-entropy”.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">get_loss</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">probs</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
        <span class="n">N</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>  <span class="c1"># N muestras
</span>        <span class="n">C</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">y</span><span class="p">))</span> <span class="c1"># C clases
</span>    
        <span class="c1"># Convertir el enumerado de clases en 'one-hot'
</span>        <span class="n">one_hot</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">((</span><span class="n">N</span><span class="p">,</span> <span class="n">C</span><span class="p">))</span> <span class="c1"># iniciamos el vector con ceros y dimensiones N x C
</span>        <span class="n">one_hot</span><span class="p">[</span><span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="n">N</span><span class="p">),</span> <span class="n">y</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>  <span class="c1"># colocamos un uno solo en la clase adecuada
</span>
        <span class="c1"># Cross entropy loss (negative log likelihood)
</span>        <span class="n">loss</span> <span class="o">=</span> <span class="o">-</span><span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">multiply</span><span class="p">(</span><span class="n">one_hot</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">probs</span><span class="p">)),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>

        <span class="k">return</span> <span class="p">(</span><span class="mf">1.</span> <span class="o">/</span> <span class="n">N</span><span class="p">)</span> <span class="o">*</span> <span class="n">loss</span>
</code></pre></div></div>

<p>Nota que la implementación de esta función es en base a \(C\) clases, y no al caso específico de tareas binarias (dos clases).
Por lo tanto, tenemos que convertir las clases \(y\) en un vector “one-hot” y calcular el error solo en la clase definida por \(y\).</p>

<blockquote>
  <p>:pencil: Un vector “one-hot” es un vector de \(C\) dimensiones, siendo \(C\) el número de clases. 
Cada dimensión corresponde a una clase, y el vector solo contiene un uno en la dimensión de la clase determinada por \(y\). 
El resto de sus valores son ceros.</p>
</blockquote>

<p>Con estos pasos ya tenemos el código necesario para entrenar modelos en diferentes datos.</p>

<h2 id="7-moons-dataset">7. Moons dataset</h2>

<p>:bulb: <em>El código completo para el modelo entrenado en el moons dataset está <a href="https://colab.research.google.com/drive/1sBTn8PTcqUru9_pn8-toIBECN_qu9jZ-#scrollTo=JclkpofK_Xme">aquí</a></em></p>

<p>El moons dataset es un dataset de juguete que intercala dos semi círculos utilizando dos dimensiones (\(x_1, x_2\)). 
Cada semi círculo pertenece a una clase, y es posible agregar ruido a los puntos para hacer un tanto más real el escenario.</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/moons_data.png" width="50%" />
        <figcaption>Fig. 8 - Moons dataset. </figcaption>
    </figure>
</div>

<p>En la Figura 8 tenemos la visualización de 200 puntos del dataset. 
Los colores <strong><font color="blue">azul</font></strong> y <strong><font color="red">rojo</font></strong> determinan las clases a la que pertenecen los puntos.</p>

<p>Con los detalles mencionados sobre el dataset, sabemos que</p>
<ol>
  <li>El número de elementos en la entrada es dos (\(x_1, x_2\))</li>
  <li>El número de elementos en la salida es dos (\(y \in {0, 1}\)).</li>
</ol>

<p>Estos aspectos del dataset encajan perfectamente con las especificaciones del modelo en la Figura 7.
Y ese es el modelo que vamos a instanciar y optimizar:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span> <span class="o">=</span> <span class="n">NeuralNet</span><span class="p">(</span><span class="n">input_dim</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">output_dim</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
<span class="n">model</span><span class="p">.</span><span class="n">train</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">iters</span><span class="o">=</span><span class="mi">20000</span><span class="p">,</span> <span class="n">lr</span><span class="o">=</span><span class="mf">0.01</span><span class="p">)</span>
</code></pre></div></div>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; Error en la iteracion 1000: 0.151160
&gt; Error en la iteracion 2000: 0.073622
&gt; Error en la iteracion 3000: 0.064136
&gt; Error en la iteracion 4000: 0.056121
&gt; Error en la iteracion 5000: 0.050703
&gt; Error en la iteracion 6000: 0.047125
&gt; Error en la iteracion 7000: 0.044488
&gt; Error en la iteracion 8000: 0.042473
&gt; Error en la iteracion 9000: 0.040899
&gt; Error en la iteracion 10000: 0.039640
&gt; Error en la iteracion 11000: 0.038610
&gt; Error en la iteracion 12000: 0.037748
&gt; Error en la iteracion 13000: 0.037018
&gt; Error en la iteracion 14000: 0.036396
&gt; Error en la iteracion 15000: 0.035867
&gt; Error en la iteracion 16000: 0.035421
&gt; Error en la iteracion 17000: 0.035052
&gt; Error en la iteracion 18000: 0.034754
&gt; Error en la iteracion 19000: 0.034521
&gt; Error en la iteracion 20000: 0.034342
</code></pre></div></div>

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/moons_optim.gif" width="50%" />
        <figcaption>Fig. 8 - Moons dataset. </figcaption>
    </figure>
</div>

<p>Después de realizar 20,000 iteraciones, el modelo logra reducir el error desde más de 0.15 hasta 0.03.
El gif de la Figura 8 muestra la evolución del modelo cada 1,000 iteraciones. 
Se puede observar cómo el modelo va de una línea de decisión sencilla a una mucho más compleja y detallada.</p>

<p>Sin embargo, tener una línea que abarque todos los detalles perfectamente no es lo ideal. 
Esto se debe a que alguno de esos puntos podrían simplemente ser “outliers”, y no generalizables del compartamiento promedio de todos los puntos.
Al capturar “outliers” se dice que estamos haciendo “overfitting”, es decir, el modelo está memorizando en lugar de generalizar. 
Entre las técnicas para prevenir overfitting están la <strong>regularización \(\ell_2\)</strong> y el uso de <strong>dropout</strong>.</p>

<h2 id="8-mnist-dataset">8. MNIST dataset</h2>

<p>:bulb: <em>El código completo para el modelo entrenado en el MNIST dataset está <a href="https://colab.research.google.com/drive/1_CLmMcdLpXPZrAWmw2bPdufcBQ62tfTM#scrollTo=uWtJ-vbVGLhr">aquí</a> e incluye la abstracción de múltiples capas neuronales.</em></p>

<p>El segundo caso en el que evaluaremos nuestra red es en el MNIST dataset. 
Este dataset continiene imágenes de dígitos escritos a mano, y la tarea es determinar el digito en una imagen.
Las imágenes son a blanco y negro y tienen una resolución de 28 x 28 pixeles.</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/dses_2019_taller1/mnist_sample.png" width="30%" />
        <figcaption>Fig. 9 - Ejemplo del número ocho del MNIST dataset. </figcaption>
    </figure>
</div>

<p>El dataset tiene un total de 70,000 imágenes. 
Este dataset contiene muchos más ejemplos que en el moons dataset, y es bastante más complejo.<br />
Más específicamente, los nuevos retos con respecto al dataset anterior son:</p>

<ol>
  <li>El dataset es mucho más grande y colocarlo todo en memoria tomaría muchos recursos.</li>
  <li>Las entradas son imágenes de 28x28 pixeles, es decir, 784 señales de entrada.</li>
  <li>Tenemos que predecir 10 clases (10 digitos) en lugar de dos clases.</li>
</ol>

<p>Considerando estos puntos, vamos a aprovechar que tenemos muchos más ejemplos para verificar que el modelo
este generalizando en lugar de memorizando (overfitting).</p>

<h4 id="data-de-entrenamiento-y-evaluación"><strong>Data de entrenamiento y evaluación</strong></h4>

<p>Nuestro primer paso va a ser dividir la data en dos partes, una de <strong>entrenamiento</strong> y otra de <strong>evaluación</strong>. 
La data de entrenamiento se ocupará para actualizar los parámetros como en el dataset anterior.
Por el otro lado, La data de evaluación solo se utilizará para verificar que los resultados en la data de entrenamiento
son consistentes y generalizables.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sklearn.datasets</span> <span class="kn">import</span> <span class="n">fetch_openml</span>

<span class="k">def</span> <span class="nf">get_mnist_dataset</span><span class="p">():</span>
    <span class="n">mnist</span> <span class="o">=</span> <span class="n">fetch_openml</span><span class="p">(</span><span class="s">'mnist_784'</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">cache</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">mnist</span><span class="p">.</span><span class="n">data</span> <span class="o">=</span> <span class="n">mnist</span><span class="p">.</span><span class="n">data</span> <span class="o">/</span> <span class="mf">255.</span>
    <span class="n">mnist</span><span class="p">.</span><span class="n">target</span> <span class="o">=</span> <span class="n">mnist</span><span class="p">.</span><span class="n">target</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">int8</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">mnist</span>
    
<span class="n">mnist</span> <span class="o">=</span> <span class="n">get_mnist_dataset</span><span class="p">()</span>

<span class="n">x_train</span><span class="p">,</span> <span class="n">x_test</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> \ 
    <span class="n">train_test_split</span><span class="p">(</span><span class="n">mnist</span><span class="p">.</span><span class="n">data</span><span class="p">,</span> <span class="n">mnist</span><span class="p">.</span><span class="n">target</span><span class="p">,</span> <span class="n">test_size</span><span class="o">=</span><span class="mf">0.2</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
</code></pre></div></div>

<h4 id="métrica-de-monitoreo"><strong>Métrica de monitoreo</strong></h4>

<p>Para tener una mejor noción de cómo esta funcionando el modelo, vamos a monitorear una <strong>métrica de exactitud (“accuracy”)</strong>.
Esta métrica nos va a decir el porcentaje de números han sido predecidos correctamente en base al total de números evaluados.
Si esta métrica es similar en entrenamiento y evaluación podemos asumir que el modelo está generalizando bien.</p>

<h4 id="batches-y-epochs"><strong>Batches y epochs</strong></h4>

<p>Adicionalmente, necesitamos incorporar el concepto de <strong>“batch”</strong> y <strong>“epoch”</strong>. 
Como no podemos procesar toda la data a la vez porque muy probablemente tendríamos problemas de recursos (“out-of-memmory exceptions”),
vamos a dividir toda la data en pequeños segmentos, a los que llamamos batches. 
Al procesar todos los batches (toda la data) habremos completado un epoch. 
Es decir, entrenar por \(n\) epochs es iterar por todo el dataset \(n\) veces.</p>

<p>Con esto en mente, organizamos nuestro método de entrenamiento de la siguiente forma:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">train</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">epochs</span><span class="o">=</span><span class="mi">200</span><span class="p">,</span> <span class="n">batch_size</span><span class="o">=</span><span class="mi">32</span><span class="p">,</span> <span class="n">lr</span><span class="o">=</span><span class="mf">0.01</span><span class="p">):</span>
        <span class="n">n_batches</span> <span class="o">=</span> <span class="n">epochs</span> <span class="o">//</span> <span class="n">batch_size</span> 
        <span class="k">if</span> <span class="n">epochs</span> <span class="o">%</span> <span class="n">batch_size</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span> 
            <span class="n">n_batches</span> <span class="o">+=</span> <span class="mi">1</span>

        <span class="k">for</span> <span class="n">epoch</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">epochs</span><span class="o">+</span><span class="mi">1</span><span class="p">):</span>
            <span class="n">epoch_losses</span> <span class="o">=</span> <span class="p">[]</span>
            <span class="n">epoch_acc</span> <span class="o">=</span> <span class="p">[]</span>

            <span class="c1"># Desordenar la data por cada epoch, para evitar que el   
</span>            <span class="c1"># modelo infiera a partir del orden en que le damos la data 
</span>            <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">shuffle</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>

            <span class="k">for</span> <span class="n">batch_i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n_batches</span><span class="p">):</span>
                <span class="n">x_batch</span> <span class="o">=</span> <span class="n">x</span><span class="p">[</span><span class="n">batch_size</span> <span class="o">*</span> <span class="n">batch_i</span><span class="p">:</span> <span class="n">batch_size</span> <span class="o">*</span> <span class="p">(</span><span class="n">batch_i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)]</span>
                <span class="n">y_batch</span> <span class="o">=</span> <span class="n">y</span><span class="p">[</span><span class="n">batch_size</span> <span class="o">*</span> <span class="n">batch_i</span><span class="p">:</span> <span class="n">batch_size</span> <span class="o">*</span> <span class="p">(</span><span class="n">batch_i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)]</span>

                <span class="c1"># Correr el 'forward pass'
</span>                <span class="n">probs_batch</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">forward</span><span class="p">(</span><span class="n">x_batch</span><span class="p">)</span>

                <span class="c1"># La actualización de parámetros es por cada batch de entrenamiento
</span>                <span class="bp">self</span><span class="p">.</span><span class="n">update_params</span><span class="p">(</span><span class="n">probs_batch</span><span class="p">,</span> <span class="n">y_batch</span><span class="p">,</span> <span class="n">lr</span><span class="p">)</span>

                <span class="c1"># Obtener el loss del batch actual
</span>                <span class="n">loss</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_loss</span><span class="p">(</span><span class="n">probs_batch</span><span class="p">,</span> <span class="n">y_batch</span><span class="p">)</span>

                <span class="c1"># Medir los resultados del modelo
</span>                <span class="n">acc</span> <span class="o">=</span> <span class="n">accuracy_score</span><span class="p">(</span><span class="n">y_batch</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">argmax</span><span class="p">(</span><span class="n">probs_batch</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">))</span>

                <span class="c1"># Guardar los losses de todos los batches para hacer promedio al final del epoch
</span>                <span class="n">epoch_losses</span> <span class="o">+=</span> <span class="p">[</span><span class="n">loss</span><span class="p">]</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">x_batch</span><span class="p">)</span>
                <span class="n">epoch_acc</span> <span class="o">+=</span> <span class="p">[</span><span class="n">acc</span><span class="p">]</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">x_batch</span><span class="p">)</span>

            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Epoch </span><span class="si">{</span><span class="n">epoch</span><span class="si">}</span><span class="s"> - Loss </span><span class="si">{</span><span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">epoch_losses</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="si">}</span><span class="s">, Accuracy: </span><span class="si">{</span><span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">epoch_acc</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">5</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">update_params</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">probs</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">lr</span><span class="p">):</span>
        <span class="c1"># Colectar los gradientes 
</span>        <span class="n">grad_dict</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">backward</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>

        <span class="c1"># Actualizar los parametros con el gradiente descendiente (NOTA: es un pequeño paso negativo)
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">W1</span> <span class="o">+=</span> <span class="o">-</span><span class="n">lr</span> <span class="o">*</span> <span class="n">grad_dict</span><span class="p">[</span><span class="s">'dW1'</span><span class="p">]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">b1</span> <span class="o">+=</span> <span class="o">-</span><span class="n">lr</span> <span class="o">*</span> <span class="n">grad_dict</span><span class="p">[</span><span class="s">'db1'</span><span class="p">]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">W2</span> <span class="o">+=</span> <span class="o">-</span><span class="n">lr</span> <span class="o">*</span> <span class="n">grad_dict</span><span class="p">[</span><span class="s">'dW2'</span><span class="p">]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">b2</span> <span class="o">+=</span> <span class="o">-</span><span class="n">lr</span> <span class="o">*</span> <span class="n">grad_dict</span><span class="p">[</span><span class="s">'db2'</span><span class="p">]</span>
</code></pre></div></div>

<h4 id="entrenamiento"><strong>Entrenamiento</strong></h4>

<p>Con las nuevas modificaciones podemos echar a andar el modelo. 
Lo que haremos es entrenar por 200 epochs continuamente y luego evaluar el modelo en la data de test.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span> <span class="o">=</span> <span class="n">NeuralNet</span><span class="p">(</span><span class="n">input_dim</span><span class="o">=</span><span class="mi">784</span><span class="p">,</span> <span class="n">hidden_dim</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span> <span class="n">output_dim</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
<span class="n">model</span><span class="p">.</span><span class="n">train</span><span class="p">(</span><span class="n">x_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">epochs</span><span class="o">=</span><span class="mi">200</span><span class="p">,</span> <span class="n">batch_size</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">lr</span><span class="o">=</span><span class="mf">0.01</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; Epoch 1 - Loss 18.05591, Accuracy: 0.10156
&gt; Epoch 2 - Loss 17.80754, Accuracy: 0.08203
&gt; Epoch 3 - Loss 4.31588, Accuracy: 0.11719
&gt; Epoch 4 - Loss 2.97415, Accuracy: 0.14062
&gt; Epoch 5 - Loss 2.56107, Accuracy: 0.14844
&gt; Epoch 6 - Loss 2.48315, Accuracy: 0.15234
&gt; Epoch 7 - Loss 2.36749, Accuracy: 0.16016
&gt; Epoch 8 - Loss 2.38260, Accuracy: 0.15234
&gt; Epoch 9 - Loss 2.27143, Accuracy: 0.19922
&gt; Epoch 10 - Loss 2.31277, Accuracy: 0.18750
&gt; ...
&gt; Epoch 190 - Loss 0.25798, Accuracy: 0.92578
&gt; Epoch 191 - Loss 0.38271, Accuracy: 0.86328
&gt; Epoch 192 - Loss 0.37735, Accuracy: 0.89062
&gt; Epoch 193 - Loss 0.32548, Accuracy: 0.90625
&gt; Epoch 194 - Loss 0.32819, Accuracy: 0.90234
&gt; Epoch 195 - Loss 0.28316, Accuracy: 0.92188
&gt; Epoch 196 - Loss 0.27505, Accuracy: 0.92188
&gt; Epoch 197 - Loss 0.28925, Accuracy: 0.92578
&gt; Epoch 198 - Loss 0.32936, Accuracy: 0.90234
&gt; Epoch 199 - Loss 0.25858, Accuracy: 0.94141
&gt; Epoch 200 - Loss 0.28152, Accuracy: 0.91797
</code></pre></div></div>

<p>En el último epoch (aunque no el mejor) el modelo alcanzó <strong>91.79% de accuracy en la data de entrenamiento</strong>.
Ahora al correr el modelo en la data de evaluación esperamos tener un resultado similar:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">y_hat_test</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">x_test</span><span class="p">)</span>
<span class="n">accuracy_score</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">y_hat_test</span><span class="p">)</span>

<span class="o">&gt;</span> <span class="mf">0.9110714285714285</span>
</code></pre></div></div>

<p>En efecto, obtenemos <strong>91.10% de accuracy en la data de evaluación</strong>! 
Esto sugiere que el modelo es capaz de reconocer números que no ha visto antes y de mantener más o menos la misma exactitud.</p>

<h2 id="9-conclusión">9. Conclusión</h2>

<p>En este post cubrimos los componentes básicos de redes neuronales artificiales. 
A partir de discutir la teoría y dar motivación del porqué de cada componente, implementamos nuestra versión de una red neuronal.
Luego probamos la red en dos datasets: <strong>Moons dataset</strong> y <strong>MNIST dataset</strong>.</p>

<p>Este post es la versión escrita del taller 1 que conduje en la comunidad de Data Science El Salvador.
Si te interesan estos temas, puedes unirte a la comunidad en su página en Facebook o LinkedIn.</p>

<p>Cualquier duda, pregunta, corrección o comentario es bienvenido!</p>

<h2 id="agradecimientos">Agradecimientos</h2>

<p>El contenido mostrado en este post ha sido creado a partir de mis notas de estudio en diferentes lugares y cursos a lo largo de mi doctorado.
Las prinicipales fuentes son:</p>
<ul>
  <li>Fundamentals of Machine Learning (Rice University)</li>
  <li>Advanced Natural Language Processing (University of Houston)</li>
  <li>Machine Learning (Coursera)</li>
  <li>Deep Learning Nanodegree (Udacity)</li>
</ul>]]></content><author><name>Gustavo Aguilar</name></author><category term="deep_learning" /><category term="backpropagation" /><category term="redes_neuronales" /><summary type="html"><![CDATA[Deep learning desde cero, taller teórico y práctico para la comunidad de Data Science El Salvador]]></summary></entry><entry><title type="html">Multimodal and Multi-view Models for Emotion Recognition - Part I</title><link href="https://tavo91.github.io/multimodal-multiview-emotion-recognition-part1/" rel="alternate" type="text/html" title="Multimodal and Multi-view Models for Emotion Recognition - Part I" /><published>2019-12-27T00:00:00+00:00</published><updated>2019-12-27T00:00:00+00:00</updated><id>https://tavo91.github.io/multiview-emotion-recognition</id><content type="html" xml:base="https://tavo91.github.io/multimodal-multiview-emotion-recognition-part1/"><![CDATA[<p><em><strong>NOTE</strong>: here are the links of the <a href="https://www.aclweb.org/anthology/P19-1095">paper</a> and ACL presentation 
<a href="http://www.livecongress.it/aol/indexSA.php?id=581B1C5F&amp;ticket=">video</a></em></p>

<p><img src="/assets/post_assets/multimodal_paper/boston.JPG" alt="alt text" title="Boston photo" /></p>

<p>I had the great opportunity to do my first internship in the US at 
Amazon; I worked at the <a href="https://amazon.jobs/en/teams/alexa-ai">Alexa Speech</a> team in Boston over the summer in 2018. 
This post describes in a more friendly way the work my co-authors and I did for 
emotion recognition using acoustics and language coming from speech.</p>

<p>The paper covers two main sections:</p>
<ol>
  <li><strong>Multimodal fusion</strong>: we propose to fuse acoustic and text modalities at the word level</li>
  <li><strong>Multi-view learning</strong>: we induce semantics into acoustic-only models</li>
</ol>

<p>This post covers the multimodal fusion part. I will add the multi-view learning part in another post.</p>

<h3 id="introduction">Introduction</h3>

<p>You can get two important conversational aspects from speech: <em><strong>how</strong> things are said</em>, and 
<em><strong>what</strong> is being said</em>. The first one corresponds to acoustic information, where things like
<em>pitch</em> and <em>arousal</em> are captured. The second one refers to linguistic information, which focuses on 
the semantics and thoughts that the speaker wants to convey.</p>

<p>These two aspects are complementary, and we use both of them in our daily conversation to 
communicate effectively. For example, consider the phrase “<em>oh my gosh,</em>” by only looking at the 
text/language, would you be able to tell the emotion behind it? 
Probably you would be ambivalent in between choosing <em>sadness</em> or <em>happiness</em>, but things become 
clearer when you add acoustic information to it:</p>

<div align="center">
    <audio src="/assets/post_assets/multimodal_paper/Ses05F_impro03_M008.wav" controls="" preload=""></audio>
</div>
<p><br /></p>

<p>From the audio/acoustics, you can clear up things and guess that the emotion is most likely <em>happiness</em>. 
Similarly, you can imagine cases where acoustics by itself (think of a uniform and constant tone 
from someone’s voice) is not sufficient to tell the emotion and thus you have to look into the 
semantics to make a decision.
<br /></p>

<h3 id="okay-okay-got-it-now-tell-me-how-to-combine-modalities-thinking">Okay, okay… got it! Now tell me how to combine modalities :thinking:</h3>

<p>At the point of the paper publication, people had been combining modalities at the utterance level. 
That is, they process each modality <strong>independently</strong> to get separate representation for the entire utterance, 
and then such representations are fused (e.g., concatenated) into a single one. Although this works 
pretty well in practice, it is not really how both modalities occur. Acoustics and semantics are 
naturally emmitted and perceived together, and we, as humans, understand both of them mainly at 
the word level. So it makes sense to fuse modalities at every word instead of at the utterance 
level. And that’s what we did in this paper.</p>

<p>Here’s an illustration of the alignment between the audio frames and the corresponding transcripts:</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/multimodal_paper/audio_and_text.png" alt="Audio frames and transcripts" width="60%" />
        <figcaption>Fig. 1 - Audio frames and their corresponding transcripts</figcaption>
    </figure>
</div>

<h3 id="aligning-modalities">Aligning modalities</h3>

<p>We use the USC-IEMOCAP dataset in the paper, which provides audios and the corresponding transcripts 
of multiple conversations between two people (check their website for more details). Each word in the 
transcripts has the starting and ending frame numbers. This allows us to focus on the chunks of frames 
where the words occur along the audio.</p>

<p>Now one can think of concatenating acoustic features and text at the word level because we know what 
are the corresponding frames for every word, but we have a dimnesionality incompatibility here. 
For every word in the utterance, we have a sequence of frame features. This incompatibility is what 
led us to introduce <strong>acoustic word</strong> representations. We process the sequence of acoustic features using 
a bidirectional LSTM and choose the hidden states of the timesteps determined by the word boundaries. 
This provides two vectors, one per direction, which we concatenate into a single one:</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/multimodal_paper/acoustic_words.png" alt="Acoustic words" width="60%" />
        <figcaption>Fig. 2 - The module for acoustic word representations</figcaption>
    </figure>
</div>

<h3 id="fusing-modalities">Fusing modalities</h3>

<p>With this <strong>acoustic word</strong> representation, now we can fuse modalities at the word level. The fusion can simply
be a concatenation of the vectors from the modalities, but sometimes one modality is more informative
than the other, just like in the examples at the beggining of this post. That said, we let the model learn
when to pay more attention to one or the other using the GMU cell [1]. In this case, we use the bimodal version 
since we are only dealing with text and audio:</p>

<table style="border:0px !important; max-width: 70% !important; text-align=center !important; " align="center">
<tr>
    <td style="border:0px !important">
        $$
        \begin{aligned}
        h_a &amp;= \mathrm{tanh}(\mathrm{W}_a x_a + b_a) \\
        h_t &amp;= \mathrm{tanh}(\mathrm{W}_t x_t + b_t) \\
        z   &amp;= \sigma(\mathrm{W}_z [x_a, x_t] + b_z) \\
        h   &amp;= z * h_a + (1 - z) * h_t
        \end{aligned} 
        $$
    </td>
    <td style="border:0px !important">
        <div align="center">
            <figure>
                <img src="/assets/post_assets/multimodal_paper/gmu_arevalo.png" alt="GMU Cell (Arevalo et al., 2017)" style="width:50%" />
                <figcaption>Fig. 3 - GMU cell from Arevalo et al. (2017)</figcaption>
            </figure>
        </div>
    </td>
</tr>
</table>

<p>where the superscripts \(a\) and \(t\) denote audio and text, \(x\) is the input vector from each modality, and \(\mathrm{W}_*\) and \(b_*\) are parameters. 
The idea is to let the model decide with the \(z\) vector which modality to allow to pass. 
Maybe just acoustics, maybe just text, or maybe a bit of both. Then, the resulting representation 
\(h\) is the sum of the weighted vectors \(h_a\) and \(h_t\), which filter out non-informative aspects from each modality. 
We will see later how important and insightful is this fusion technique in the visualization discussion.</p>

<h3 id="global-attention">Global attention</h3>

<p>While fusing modalities at the word level is important to enhance word representations, some words are still
not relevant for the emotion recognition task. For exmaple, stopwords won’t add much to the overall emotion. 
This motivates us to add an attention layer at the utterance level. We use multiplicative attention [2] as follows:</p>

\[\begin{equation}
	\begin{split}
		e_i =&amp;~ v^\intercal ~\mathrm{tanh}(\mathrm{W}_h h_i + b_h) \notag\\
		a_i =&amp;~ \frac{\mathrm{exp}(e_i)}{\sum^{N}_{j=1}\mathrm{exp}(e_j)}, ~~~\text{where}~\sum_{i=1}^N a_i = 1 \notag\\
		z   =&amp;~ \sum_{i=1}^N a_i h_i \notag
	\end{split}
\end{equation}\]

<p>where \(h_i\) is the bimodal representation of the \(i\)-th word (i.e., the output of the GMU cell), and \(\mathrm{W}_h\), \(b_h\), 
and \(v^\intercal\) are parameters. The final output is \(z\), which is the weighted sum of all the words in the utterance.</p>

<h3 id="overall-model">Overall model</h3>

<p>We have all the components of the model now, which we can visulalize in the following diagram:</p>

<div align="center">
    <figure>
        <img src="/assets/post_assets/multimodal_paper/multimodal_model.png" alt="Multimodal model" width="45%" />
        <figcaption>Fig. 4 - Overall model architecture</figcaption>
    </figure>
</div>

<p>The shadow box at the top of the the figure denotes the acoustic word module, where the inputs are the frame features from the audio. 
\(w_{1,2}\) are the words from the transcripts of the audios, and they are represented by contextualized vectors from ELMo [3]. Both 
the modalities are fused in the GMU cell and passed to a bidirectional LSTM followed by the attention layer. 
We optimized the model by minimizing the negative log-likelihood loss function (other training details can be found in the paper).</p>

<h3 id="results">Results</h3>

<div align="center">
    <figure>
        <img src="/assets/post_assets/multimodal_paper/results.png" alt="Results" width="100%" />
        <figcaption>Table 1 - Results of the model</figcaption>
    </figure>
</div>

<p>Let’s focus on the highlighted box from the table, which shows to the hierarchical multimodal models 
(see the paper for other details). 
These experiments use both acoustics and lexical information, and you can think of row 7 as the baseline 
because it simply fuses modalities by concatenation and does not have global attention. 
As you can see in rows 8 and 9, by adding the attention mechanisms, either the word or utterance level 
attentions, the model improves over row 7. The best results are given by combining both attention mechanisms in row 10. 
These results align with the intuition that</p>
<ul>
  <li>although modalities are complementary, sometimes one modality can carry more information than the other, and</li>
  <li>words along the utterance are not equally important.</li>
</ul>

<p>We can actually see what the model captures in its attention mechanisms in the following predictions:</p>

<div align="center">
<figure>
    <table style="border:0px !important; text-align=center !important; margin:0px !important" align="center">
        <tr>
            <td style="border:0px !important; width:8%">
                <audio id="audio1" preload="none">
                    <source src="/assets/post_assets/multimodal_paper/Ses05F_impro03_M008.wav" type="audio/wav" />
                </audio>
                <a href="#" id="control1" class="audio-play">
                    <img id="control1-bttn" src="/assets/images/play.png" />
                </a>
            </td>
            <td style="border:0px !important">
                <img src="/assets/post_assets/multimodal_paper/attention_viz3.png" width="30%" />
            </td>
        </tr>
        <tr>
            <td style="border:0px !important; width:8%">
                <audio id="audio2" preload="none">
                    <source src="/assets/post_assets/multimodal_paper/Ses05M_script01_3_M016.wav" type="audio/wav" />
                </audio>
                <a href="#" id="control2" class="audio-play">
                    <img id="control2-bttn" src="/assets/images/play.png" />
                </a>
            </td>
            <td style="border:0px !important">
                <img src="/assets/post_assets/multimodal_paper/attention_viz2.png" />
            </td>
        </tr>
    </table>
    <figcaption>Fig. 5 - Visualization of the attention mechanisms</figcaption>
</figure>
</div>

<script type="text/javascript">
    var audio1 = document.getElementById('audio1');
    var control1 = document.getElementById('control1');
    var button1 = document.getElementById('control1-bttn');
    
    control1.onclick = function () {
        var pause = control1.className === 'audio-pause';
        console.log(pause);
        control1.className = pause ? 'audio-play' : 'audio-pause';
        button1.src = pause ? '/assets/images/play.png' : '/assets/images/pause.png';
        audio1[pause ? 'pause' : 'play']();
        return false;
    };
    
    var audio2 = document.getElementById('audio2');
    var control2 = document.getElementById('control2');
    var button2 = document.getElementById('control2-bttn');
    
    control2.onclick = function () {
        var pause = control2.className === 'audio-pause';
        control2.className = pause ? 'audio-play' : 'audio-pause';
        button2.src = pause ? '/assets/images/play.png' : '/assets/images/pause.png';
        audio2[pause ? 'pause' : 'play']();
        return false;
    };
</script>

<p>The <strong><font color="blue">blue</font></strong> and <strong><font color="red">red</font></strong> bars above every word denote 
the acoustic and lexical information that the model is letting flow through the layers. The bars 
add up to 1 for every word, and the higher the bar is the more information of that modality is being considered for that word. 
If you read and then play the examples, you will sense that the acoustic information is essential 
in the first one, whereas in the second one is more supportive to what is already described by the language. 
This is precisely what the model is capturing: <strong><font color="blue">blue</font></strong> bars are higher in the first
example, whereas in the second example <strong><font color="red">red</font></strong> bars can be more informative in some parts 
of the utterance while still other parts rely better on acoustics (e.g., the beginning of the utterance).</p>

<p>That’s at the word level, but the model also considers filtering out at the utterance level. 
We can see the probability mass from the global attention distributed along the words of the utterance based 
on the background color. Note that some words are pretty much ignored leaving the emotion signal on few words.
It’s worth mentioning that when only text is used, the behavior of the model changes, and different words are
highlighted. This supports the intuition that acoustics and language are complementary and one help the other 
to be more accurate.</p>

<h3 id="conclusion">Conclusion</h3>

<p>This post briefly covers the first part of our paper “Multimodal and Multi-view Models for Emotion Recognition,”
giving more intuition behind the technical decisions and presenting the content in a more friendly manner. 
Let me know in the comments if you have any questions or suggestions, I will be happy to address them. 
Thanks for reading!</p>

<h3 id="references">References</h3>

<p>[1] Arevalo et al., 2017. Gated Multimodal Units for Information Fusion.<br />
[2] Bahdanau et al., 2015. Neural Machine Translation by Jointly Learning to Align and Translate.    <br />
[3] Peters et al., 2018. Deep Contextualized Word Representations.</p>]]></content><author><name>Gustavo Aguilar</name></author><category term="text" /><category term="audio" /><category term="multimodal" /><category term="multiview" /><category term="emotion" /><summary type="html"><![CDATA[Describing the methodology to fuse modalities more naturally]]></summary></entry></feed>