Aprendiendo Transformers Code First Parte 1 – La Configuración

'Aprendiendo Transformers Code First Parte 1' - Configuración.

Una exploración de 4 partes de los Transformers utilizando nanoGPT como punto de partida

Foto de Josh Riemer en Unsplash

No sé ustedes, pero a veces es más fácil mirar el código que leer artículos. Cuando estaba trabajando en AdventureGPT, comencé leyendo el código fuente de BabyAGI, una implementación del artículo ReAct en alrededor de 600 líneas de Python.

Recientemente, me enteré de un artículo reciente llamado TinyStories a través del episodio 33 del excelente podcast Cognitive Revolution. TinyStories intenta demostrar que los modelos entrenados con millones (no miles de millones) de parámetros pueden ser efectivos con datos de suficiente calidad. En el caso de los investigadores de Microsoft en el artículo, utilizaron datos sintéticos generados a partir de GPT-3.5 y GPT-4 que hubieran costado alrededor de $10,000 en el mercado para generar. El conjunto de datos y los modelos están disponibles en el repositorio HuggingFace del autor.

Me cautivó escuchar que se podía entrenar un modelo con 30 millones o menos de parámetros. Para referencia, estoy ejecutando todo mi entrenamiento de modelos e inferencia en una laptop Lenovo Legion 5 con una GTX 1660 Ti. Incluso solo para la inferencia, la mayoría de los modelos con más de 3 mil millones de parámetros son demasiado grandes para ejecutar en mi máquina. Sé que hay recursos de computación en la nube disponibles por un precio, pero estoy aprendiendo todo esto en mi tiempo libre y realmente solo puedo permitirme la modesta factura de OpenAI que acumulo a través de las llamadas a la API. Por lo tanto, la idea de que había modelos que podía entrenar en mi hardware modesto me emocionó instantáneamente.

Comencé a leer el artículo de TinyStories y pronto me di cuenta de que utilizaron el modelo GPT Neo, que ya no está en funcionamiento, en su entrenamiento de modelos. Comencé a analizar el código para ver si podía entenderlo y me di cuenta de que necesitaba algo aún más pequeño para empezar. Para contextualizar, soy principalmente un ingeniero de software de backend con suficiente experiencia en aprendizaje automático para no perderme completamente cuando escucho a la gente hablar sobre redes neuronales. Estoy lejos de ser un ingeniero de aprendizaje automático adecuado y esto me llevó a escribir “gpt desde cero” en mi motor de búsqueda preferido para encontrar una introducción más suave. Encontré el video a continuación y todo cambió.

Esto era lo que estaba buscando. Además del repositorio básico vinculado en el video, hay una versión pulida llamada nanoGPT que todavía está en desarrollo activo. Además, el código de entrenamiento y el código del modelo tienen alrededor de 300 líneas de Python cada uno. Para mí, eso fue aún más emocionante que el video. Cerré el video y comencé a examinar el código fuente. nanoGPT utiliza PyTorch, que nunca he utilizado antes. También presenta la cantidad justa de matemáticas y jerga de aprendizaje automático para ponerme ansioso como principiante. Esto iba a ser algo más grande de lo que anticipaba.

Una de las mejores formas de entender algo es escribir sobre ello. Por lo tanto, planeo analizar el código en el repositorio de nanoGPT, leer el famoso artículo “Attention is All You Need” y aprender sobre los transformers de manera práctica y desde cero. Todo lo que aprenda en el camino espero escribirlo en esta serie. Si quieres seguir, clona el repositorio de nanoGPT en tu máquina (el modelo incluso se puede entrenar en CPU, así que no hay excusas de hardware) y sigue adelante.

Lo primero que hice después de clonar el repositorio fue seguir las instrucciones del README para entrenar el modelo más simple, el modelo de generación a nivel de caracteres utilizando el conjunto de datos tiny_shakespeare. Hay un script para preparar el conjunto de datos para el entrenamiento, un script para hacer el entrenamiento real y un script de muestreo para generar texto. Con algunos comandos en la terminal y más de una hora de entrenamiento, tenía un modelo simple para generar texto que suena a Shakespeare.

Seguir instrucciones está bien, pero no entiendo realmente algo hasta que lo modifico para que funcione para mi propio caso de uso. Mi objetivo aquí era entrenar un modelo similar a nivel de caracteres utilizando el conjunto de datos TinyStories. Esto requería crear mi propio script de preparación de datos para preparar el conjunto de datos para el entrenamiento. Vamos a profundizar en eso.

El nanoGPT tiene dos tipos de scripts de preparación de datos: uno para modelos al estilo de GPT-2 y otro para modelos a nivel de caracteres. Tomé parte del código de los modelos GPT-2 para la descarga desde los repositorios de HuggingFace y tomé todo lo demás del script a nivel de caracteres de tiny_shakespeare. Un punto importante aquí, tiny_shakespeare tiene poco más de 1 MB y contiene solo 40,000 líneas de Shakespeare. TinyStories tiene más de 3 GB comprimidos y contiene 39.7 millones de historias. Los métodos para tokenizar y dividir tiny_shakespeare no eran directamente transferibles, al menos no con los 32 GB de RAM que tiene mi laptop. Hice que mi máquina se bloqueara varias veces al intentar preparar TinyStories con métodos pythonicos y fáciles de leer. El script final utiliza algunos trucos que detallaré a continuación.

En primer lugar, mi solución preferida para procesar listas de datos es la comprensión de listas, una sintaxis para generar nuevas listas a partir de listas existentes con modificaciones. El problema con la comprensión de listas en este caso es que esos 3GB de texto comprimido se convierten en aproximadamente 10GB en RAM. Ahora, la comprensión de listas requiere múltiples copias de la lista en RAM. No es un problema para datos pequeños, pero no es viable para TinyStories.

Las salidas de los scripts de preparación de datos son un arreglo comprimido de NumPy de codificación a nivel de caracteres para los datos de entrenamiento y validación, además de un archivo pickle de metadatos que incluye la lista completa de caracteres únicos y los mapas de codificación/decodificación para convertir estos caracteres en números. Usando esto como referencia, no necesitamos nada más que el arreglo final codificado de números una vez que se encuentran los caracteres únicos y se mapean a números. La mejor manera de hacer esto de manera eficiente en memoria es iterar a través de los datos con un simple bucle while construyendo estas salidas pieza por pieza. Para hacer esto, se inicializa una variable inicial antes del bucle que luego se actualiza en cada iteración. Esto evita que se mantengan múltiples versiones del conjunto de datos en la RAM y solo se genera lo que necesitamos. El código final de generación de vocabulario es el siguiente:

chars_dataset = set([])len_dataset = 0# obtener todos los caracteres únicos que ocurren en este texto, así como la longitud total para los datos de entrenamientodesc = "Enumerar caracteres en el conjunto de entrenamiento"for story in tqdm(dataset['train']['text'], desc):    chars = list(set(story))    for char in chars:        chars_dataset.add(char)        len_dataset += len(story)

Dicho esto, un arreglo de 30.7M de historias (más de 4 mil millones de caracteres) codificadas como números aún ocupa una cantidad significativa de RAM porque Python almacena los enteros de manera dinámica. Entra en escena NumPy, que tiene un almacenamiento de arreglos mucho más eficiente donde se puede especificar el tamaño exacto de los enteros. Además del almacenamiento eficiente, NumPy también tiene una concatenación de arreglos eficiente en memoria que se puede utilizar para construir el arreglo codificado final de manera iterativa en lugar de todo de una vez.

El toque final en el script fue agregar una barra de progreso utilizando tqdm para cada paso y finalmente estaba listo para ejecutar el script. Así que lo ejecuté durante la noche y volví por la mañana. Cuando regresé, el script seguía ejecutándose, con más de 100 horas estimadas de tiempo de cálculo restantes.

En ese momento realmente me di cuenta: 30.7M de historias es pequeño para un modelo de lenguaje, pero de ninguna manera es un conjunto de datos de juguete que se pueda procesar en un solo hilo. Era hora de sacar las grandes armas: la paralelización. La paralelización agrega mucha complejidad y gastos generales, pero las ganancias de rendimiento valían la pena. Afortunadamente, existen varias formas de paralelizar el código de Python. Muchas de estas soluciones requieren reescribir por completo un script ejecutado en serie o abstracciones complicadas. Después de investigar un poco, encontré algo que me permitió mantener la mayor parte de mi script igual, pero aún así ejecutar varios procesos para aprovechar todos mis hilos.

Ray es una biblioteca para paralelizar fácilmente métodos en Python y se puede ejecutar fácilmente de forma local o como un clúster. Se encarga de ejecutar tareas en una cola y crear procesos de trabajo para procesar esa cola. A continuación, se muestra un excelente tutorial sobre ray si esto ha despertado su interés.

Python Paralelo y Distribuido Moderno: Un Tutorial Rápido sobre Ray

Ray es un proyecto de código abierto para Python paralelo y distribuido.

towardsdatascience.com

Cuando se trataba de elegir qué paralelizar, la función encode parecía ser un buen candidato. Tiene entradas y salidas claras, no tiene efectos secundarios en esas entradas y salidas, y fue fácilmente una de las partes más grandes del tiempo de cálculo. Adaptar el código existente para que funcione con ray no podría haber sido más fácil: la función se vuelve accesible para ray a través de un decorador, la llamada funcional cambia ligeramente para agregar un atributo remoto y hay una función para iniciar la ejecución de todos los datos. A continuación, se muestra un ejemplo de cómo se veía inicialmente en mi código base:

import rayray.init()...# dado todos los caracteres únicos dentro de un conjunto de datos, # crear una asignación única de caracteres a enterosstoi = { ch:i for i,ch in enumerate(chars_dataset) }@ray.remotedef encode(s):    return [stoi[c] for c in s]...encoded_stories = []for story in dataset['train']['text']:    encoded_stories.append(encode.remote(story))ray.get(encoded_stories)...

Armado con todo el poder de mi CPU, seguí adelante solo para estrellar mi computadora portátil inmediatamente. Con la pila de llamadas distribuidas localmente utilizada por ray, todo el conjunto de datos estaba en memoria varias veces. Simplemente encolar todo el conjunto de datos causó un error de falta de memoria. Molesto, aproveché esto como una excusa para comprar más RAM (¡64GB aquí vamos!), pero seguí ajustando el código mientras el RAM llegaba.

El siguiente lugar lógico fue agrupar las solicitudes manejadas por Ray en algo que pudiera caber en una cantidad razonable de memoria. Agregar lógica de agrupamiento fue bastante sencillo y está presente en el código final que enlazaré al final del artículo. Lo que realmente se volvió interesante fue experimentar con el tamaño del lote. Inicialmente, elegí un tamaño de lote aleatorio (5000) y comenzó bien, pero me di cuenta de que se estaba gastando bastante tiempo en el código de un solo hilo durante cada lote.

Esencialmente, viendo mi monitor de sistema preferido, vi que un solo núcleo estaba ocupado durante minutos antes de que finalmente todos los núcleos de mi laptop se activaran durante unos segundos antes de volver a utilizar solo un núcleo. Esto me llevó a jugar un poco con el tamaño del lote, esperando alimentar a los núcleos de la CPU hambrientos más rápido y mantenerlos ocupados durante más tiempo. Reducir el tamaño del lote no ayudó porque había mucho código síncrono en cada lote utilizado para dividir y preparar un lote a partir del conjunto de datos completo. Ese código no se podía paralelizar, así que significaba que cada lote tenía un gran costo de inicio en tiempo para generar el fragmento. Esto me llevó a probar lo contrario, aumentando el tamaño del fragmento para mantener los núcleos más ocupados durante más tiempo. Esto funcionó, ya que la generación del fragmento tomaba la misma cantidad de tiempo independientemente del tamaño del fragmento, pero cada fragmento procesaba más datos. Combinando esto con mover mi posprocesamiento de codificación a funciones de rayo, pude procesar el 30% del conjunto de datos de entrenamiento en solo unas pocas horas, todo en una sola laptop.

Finalmente, después de unas pocas horas más, tenía un conjunto de datos personalizado y completamente preparado para alimentar al modelo a nivel de caracteres. Me alegró no tener que recurrir al uso de un costoso procesamiento en la nube para procesar el conjunto de entrenamiento, que era mi siguiente paso si el aumento de RAM no funcionaba. Además, aprendí íntimamente lo que significaba crear/procesar un conjunto de datos para un modelo a nivel de caracteres.

En el siguiente artículo de esta serie, examinaré el código real del modelo, explicando lo mejor que pueda y enlazando a numerosos recursos externos para proporcionar información adicional donde mi conocimiento sea insuficiente. Una vez que se escriba el artículo, volveré y proporcionaré un enlace aquí. Mientras tanto, he enlazado la versión final de mi script de preparación de conjunto de datos a continuación para que puedas seguirlo y ver lo que se necesita para procesar un conjunto de datos bastante grande en una plataforma de cómputo limitada.

nanoGPT/data/tinystories_char/prepare.py en master · oaguy1/nanoGPT

El repositorio más sencillo y más rápido para el entrenamiento/ajuste fino de GPT de tamaño VoAGI. – nanoGPT/data/tinystories_char/prepare.py…

github.com

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Noticias de Inteligencia Artificial

Un Asistente Robótico Vestible Que Está Por Todas Partes

El robot asistente Calico desarrollado por investigadores de la Universidad de Maryland puede ser usado en la ropa de...

Aprendizaje Automático

Rendimiento sobrehumano en la prueba Atari 100K El poder de BBF - Un nuevo agente de RL basado en valores de Google DeepMind, Mila y la Universidad de Montreal.

El aprendizaje por refuerzo profundo (RL) ha surgido como un algoritmo de aprendizaje automático poderoso para aborda...

Inteligencia Artificial

Uso de Computadoras Analógicas en Inteligencia Artificial (IA)

Las Computadoras Analógicas son una clase de dispositivos en los cuales las cantidades físicas como el voltaje eléctr...

Inteligencia Artificial

Los Juegos Asiáticos, un hito para los eSports que alimenta los sueños olímpicos

En los Juegos Asiáticos de Hangzhou, China, los jugadores de eSports tendrán la oportunidad de ganar medallas por pri...

Investigación

Investigadores de MIT CSAIL discuten las fronteras del AI generativo.

Expertos se reúnen para examinar el código, lenguaje e imágenes generados por la inteligencia artificial, así como su...

Inteligencia Artificial

Explorando NLP - Comenzando con NLP (Paso #1)

Este semestre, tengo NLP como parte de mi plan de estudios. ¡YAY! Como parte de una próxima evaluación para la materi...