Bienvenidos. En esta clase vamos hablar de un método para mejorar la velocidad con la que se entrenan las redes neuronales. Durante este curso y el anterior hemos hablado muchas formas de automatizar procesos del modelado, como cuando hablamos de regularización, que en esencia nos permite modificar la arquitectura de nuestra red neural de una manera relativamente automática o la arquitectura efectiva, realmente la arquitectura que nosotros le dimos. A pesar de eso, el trabajo de hacer modelos de aprendizaje automático sigue siendo bastante iterativo, o sea, hay que probar muchos modelos con distintos hiperparámetros y es un proceso que toma mucho tiempo de cálculo. Por lo tanto, en particular para redes neuronales que normalmente entrenan en una enorme cantidad de datos, como en Big Data necesita mucho datos, es importante tener este tipo de métodos a mano para acelerar estos tiempos, porque es muy importante la diferencia entrenar un modelo en un par de horas versus en un par de días. Ya hemos visto que la vectorización nos permite acelerar el entrenamiento evitándonos esos pollux que son mucho más lentos que usar mecanismos que están optimizados a nivel de CPU. Pero aún si uso vectorización, hay casos donde procesar todo mi set de entrenamiento sigue siendo lento en términos absolutos, tarda horas o minutos, muchos minutos u horas, y si eso lo tengo que hacer por varios epochs, el entrenamiento entero sería de días o semanas. Eso nos plantea el problema de qué hacer si mi dataset es demasiado grande. Porque, en efecto, tengo este primer problema, que es cada actualización de gradiente en mi red neuronal, tengo que hacer procesar todo mi set de entrenamiento y eso tarda horas. Cada actualización de gradiente tarda horas y eso hace que sea extremadamente lento la posibilidad de iterar y modificar el modelo. El otro problema que tengo que tomar en cuenta, en particular, si uso GPU para entrenar que tienden a tener menos memoria que un servidor, un servidor puede tener 1 tera de ram, un GPU está en el orden de las decenas o centenas de gigas, está mucho más abajo y si tengo que pasarle todo el dataset a la memoria del GPU, a veces no puedo y tengo que empezar a pensar formas más creativas de lograr que esto se pase todo al GPU. En vista a estos problemas, se fue pensando y se llegó a una conclusión que es, que en realidad yo no necesito procesar todo mi dataset para hacer una actualización de gradiente. Si yo tengo un subconjunto lo suficientemente grande, las propiedades estadísticas, en líneas generales, hacen que sea lo suficientemente significativo como para que yo pueda ir haciendo actualizaciones de gradiente paulatinamente. Si tengo 5.000, 10.000, 100.000 millones de casos, por ahí con un número muchísimo más pequeño, podría empezar a hacer algunas actualizaciones de gradiente mucho más rápido y eso hace que en lo que antes hacía una actualización de gradiente, puedo llegar hacer miles o decenas de miles. Es significativamente más rápido esa forma de entrenar y además, tiene la virtud de que puedo pasar una cantidad bastante menor de datos a la memoria del sistema para hacer lo mismo que estaba haciendo antes. Y resulta, en realidad, que el entrenamiento de esta manera con Mini-Batch, es el nombre que sería en inglés, o minibaches o subconjuntos, minisubconjuntos de mis datos de entrenamiento, no es tan diferente a como lo hacíamos antes. Digamos que nuestra función de actualización es una función A, que toma la red como estaba antes, un determinado conjunto de datos x con sus correspondientes y. El Gradient Descent Tradicional lo que hace es, por cada e de epoch entre todas las epochs, digamos que esto lo entreno por 10, 100, 1.000 epochs, la nueva red es la actualización de la red vieja con todo mi dataset x con sus correspondientes y. En Mini-Batch Gradient Descent, lo que hago es agrego un loop extra digo, "Por todos los epochs quiero hacer 100 epochs, por cada epoch que voy hacer voy a dividir mis n casos de entrenamiento en s subconjunto". Entonces, a cada elemento de x y a cada elemento de y, le voy a asignar uno de los s subconjuntos y voy a meter un loop intermedio que en vez de actualizar solamente una vez por epoch mi red, lo voy a hacer actualizar una vez por Mini-Batch. En términos programáticos es trivial implementar esto y hace, como dijimos antes, que podamos actualizar nuestros gradientes cientos o miles de veces más por epoch, acelerando muchísimo el tiempo de entrenamiento. Tenemos que pensar qué opciones de tamaños de s tenemos. Para eso tenemos que entender un poquito qué consecuencias tiene modificar la cantidad de subconjuntos en que vamos a dividir nuestro dataset dentro de la función de costo. En esencia, mientras más grande sea nuestro tamaño de cada subconjunto, más estable va a ser la trayectoria de nuestro algoritmo de optimización y mientras más pequeño sea, más inestable va a ser, más parecido a algo como esto. Pensemos los casos extremos, digamos que este azul es el más prolijo, la mejor estabilidad de convergencia que vamos a tener, que es cuando el tamaño del subconjunto es igual al tamaño de nuestro dataset, es decir, no hacemos Mini-Batch Gradient Descent, hacemos Bacht Gradient Descent, hacemos de a 1 todo nuestro dataset, ya como veníamos haciendo hasta ahora. Del otro extremo está tomar un valor de subconjunto igual a 1, donde cada caso es su propio subconjunto. En ese caso, tenemos el llamado "Stochastic Grandient Descent", "Descenso de Gradiente Estocástico" y esto va a ser el extremo de la inestabilidad respecto a los demás. Esto hasta parece prolijo para lo que sería un estocástico, los estocásticos iría por todos lados, porque cada caso tiraría a todo el modelo para optimizarse a sí mismo en vez de optimizar algo que sea más general. En líneas generales, nosotros vamos a tener un valor intermedio. ¿Qué valor? Es la pregunta más obvia en este momento. No hay una respuesta matemática que sirva para todos los casos, es más bien un tema de prueba y error. A medida que ustedes vayan conociendo sus datos y tengan muchos modelos sobre esos datos, van a ir entendiendo que algún valor está mejor que otros, que algún valor hace que sus modelos converjan más rápidos, eso lo han ido notando si hacen modelos de una manera prolija y metódica, donde puedan registrar cuánto tiempo tardó cada uno y qué hiperparámetros se probaron y con qué tamaño de baches, ese tipo de cosas. Si son prolijos en sus desarrollos lo van a poder registrar y van a poder ir mejorándolo. ¿Por dónde empezamos? La práctica, en líneas generales, nos indica que hay algunos valores que no son superóptimos, ni los mejores del mundo, pero seguramente son lo suficientemente buenos como para que el modelo empiece a avanzar hasta que se den cuenta si es necesario tocarlo o no. Tanto en GPU como en CPU los frameworks hasta dan por defecto un valor de 32. 32 tiende a funcionar bien. ¿Por qué? Porque hasta ahora viene funcionando bien. Posiblemente, ahora que tenemos máquinas con más RAM y GPU más potentes y otras cosas, ese valor en algún tiempo se va más cerca de 64 o de 128. Hoy en día para GPU estamos en ese rango, entre 16 y 128 es lo más usual si usan una computadora con 8-b100, le mandan 256 a cada GPU, pero no es lo común. Normalmente con hardware más limitado el número está en ese rango. En CPU para datos tabulares, que es lo que se suele usar el CPU, digamos datos tipo Excel, podemos usar minibaches mucho más grandes porque no ocupan la memoria del CPU y es que suele ser más grande, y los problemas tienden a ser problemas con menos variables, entonces se puede usar un número más grande. Acá no depende tanto de la memoria, el límite, sino que depende más bien de qué tan rápido converge el modelo. A veces converge más rápido con baches más grandes y con un "loading rate" un poquito más alto, o a veces converge más rápido con baches más en rango de los 32 a 64, que es lo más normalito para problemas tipo de imágenes. Hemos visto una manera de adaptar el algoritmo de descenso de gradiente a estos casos donde tenemos muchos datos y no queremos esperarlos a todos para ir actualizando nuestros parámetros. Y vemos, que este método de Mini-Batch Gradient Descent es algo que va a ser la norma. En realidad, no se usa entrenar en todo el dataset, todos los frameworks por defecto tienen este tipo de entrenamiento ya integrado, y a medida que vayamos avanzando y además veamos métodos adaptativos de la tasa de aprendizaje o de otras variantes sobre esa tasa, vamos a ver que es mucho más natural y es mucho más lógico usar minibaches y no usar toda nuestra tasa para hacer cada actualización de gradiente.