\subsection{Porcentaje de aciertos de las redes}

La red neuronal tiene un porcentaje de acierto promedio del 95\% en todas sus versiones. A pesar de que el tipo float tiene una menor capacidad de representación numérica que su contraparte de tipo double su precisión parece ser suficiente para realizar los cálculos necesarios y hacer que la red neuronal llegue a las mismas conclusiones que la red que utiliza double.

\subsection{Tiempos}

A continuación se listan, para los distintos algoritmos que fueron optimizados, tablas que permiten comparar los tiempos promedios para las distintas versiones implementadas, junto con el desvío estándar en cada caso.

Dado que las optimizaciones implementadas en ASM diferencian los casos en que ciertas dimensiones de los parámetros son divisibles por 2 (si usamos Double) o 4 (si usamos Float), decidimos usar siempre dimensiones múltiplo de 4. Esto es para poder ver el mejor caso de la optimización.

Los tiempos en esta sección están expresados en milisegundos salvo que se explicite lo contrario.

En total se corrieron 400k iteraciones de cada algoritmo para estimar estos valores, con un procesador i5-5300U y 8GB de memoria RAM. La versión en C fue compilada con la compilación más agresiva (O3).

\paragraph{cost\_derivative}

Como parámetros se le pasan dos matrices de tamaño $10\times32$. En este caso la dimensión que nos importa que sea múltiplo de 4 es la segunda. A parte de realizar los cálculos con SIMD aprovechamos que para el caso particular en que se usa esta matriz siempre la cantidad de filas es de 10 y realizamos una técnica de loop unrolling para mejorar los tiempos de cómputo de la versión de $ASSEMBLER$ intentando evitar el overhead del branch prediction.

\begin{center}
    \begin{tabular}{| l | c | c |}
                \hline
    Versión & Media & Desvío estándar \\
                \hline
    Double C & 0.251953 & 0.088764 \\
    Double ASM & 0.128182 & 0.060222 \\
    Float C & 0.252914 & 0.061287 \\
    Float ASM  & 0.083133 & 0.048002 \\
                \hline
			
        \end{tabular}
\end{center}

\paragraph{vector\_sum}

Esta función recibe como parámetros dos vectores de longitud 1000.

\begin{center}
    \begin{tabular}{| l | c | c |}
                \hline
    Versión & Media & Desvío estándar \\
                \hline
    Double C & 0.344978 & 0.085860 \\
    Double ASM & 0.415999 & 0.112268 \\
    Float C & 0.207263 & 0.062842 \\
    Float ASM  & 0.229253 & 0.070555 \\
                \hline
            
        \end{tabular}
\end{center}


\paragraph{update\_weight}

Toma como parámetros dos vectores de longitud 1000.

\begin{center}
    \begin{tabular}{| l | c | c |}
                \hline
    Versión & Media & Desvío estándar \\
                \hline
    Double C & 0.392532 & 0.108260 \\
    Double ASM & 0.393279 & 0.683633 \\
    Float C & 0.242674 & 0.061426 \\
    Float ASM  & 0.223944 & 0.680904 \\
                \hline
            
        \end{tabular}
\end{center}


\paragraph{hadamard\_product}

Recibe dos matrices de dimensiones $1000\times 10$. En este caso para que la optimización se aproveche al máximo el producto de las dimensiones debe ser divisible por 4.

\begin{center}
    \begin{tabular}{| l | c | c |}
                \hline
    Versión & Media & Desvío estándar \\
                \hline
    Double C & 0.307460 & 0.163436 \\
    Double ASM & 0.137181 & 0.062210 \\
    Float C & 0.256564 & 0.080366 \\
    Float ASM  & 0.093358 & 0.053049 \\
                \hline
            
        \end{tabular}
\end{center}

\paragraph{matrix\_prod}

Se le pasan dos matrices, una de dimensión $10\times 20$ y la otra de $20\times 30$. En este caso la dimensión que nos importa que sea múltiplo de 4 es la dimensión en común entre ambas matrices.

\begin{center}
    \begin{tabular}{| l | c | c |}
                \hline
    Versión & Media & Desvío estándar \\
                \hline
    Double C & 7.145342 & 1.293915 \\
    Double ASM & 3.770623 & 1.011386 \\
    Float C & 6.704427 & 0.491350 \\
    Float ASM  & 4.033694 & 0.324861 \\
                \hline
            
        \end{tabular}
\end{center}

Finalmente mostramos el tiempo promedio que insume realizar un epoch\footnote{Un epoch es una pasada de entrenamiento sobre un mini-batch}. Los hiperparámetros usados fueron:
\begin{itemize}
    \item cantidad de unidades de la capa oculta = 30
    \item mini\_batch = 32
    \item epochs = 50
    \item learning\_rate = 3.0 (esto no afecta el tiempo que tarda un epoch)
\end{itemize}

\begin{center}
    \begin{tabular}{| l | c |}
                \hline
    Versión & Media (en segundos) \\
                \hline
    Double C & 3.799580 \\
    Double ASM & 3.054587 \\
    Float C & 3.395475 \\
    Float ASM  & 2.576224 \\
                \hline
            
        \end{tabular}
\end{center}

\subsection{Análisis de los resultados}

Hay varios puntos destacables en los resultados obtenidos durante la experimentación. Por un lado hay funciones que no superaron a su versión de C, como son update\_weight y vector\_sum, que incluso dió peor. Ambas son funciones bastante simples formadas por un solo loop, creemos que el compilador de C puede tener ciertas optimizaciones para estos casos básicos que hacen que obtenga buenos tiempos. De todas maneras algo interesante para notar en estas dos funciones es que obtuvieron tiempos ideales comparando $float$ con $double$, en estas se cumple que $float$ tarda la mitad del tiempo que $double$ para la versión de $ASSEMBLER$, esto se debe probablemente a lo simple que es el código de estas funciones lo cual permite que las dos versiones sean muy similares y no se agregue gran complejidad al empezar a trabajar con 4 elementos a la vez como ocurre al implementar la versión de $float$. Esto no se dió tanto con funciones más complejas, teniendo como caso mas extremo a matrix\_prod la cual dió peor con $float$ que con $double$, esto sucedió debido a que tuvimos que utilizar funciones más sofisticadas y menos eficientes para reordenar y mover los valores obtenidos cuando trabajabamos de a 4, perdiendo todo lo que habiamos ganado al paralelizar mas.
\\
\\
Como último comentario es importante notar como todas estas optimizaciones juntas terminan logrando una mejora significativa en la red neuronal, logrando que una epoch reduzca su tiempo de ejecución en aproximadamente un 23\%.