Escribir mil millones de canciones con C # y Deep Learning

En este artículo, explicaré cómo crear un sitio web ASP.NET Core , que use IA para generar letras de canciones únicas con solo hacer clic en un botón, y permitir a los usuarios votar por las mejores canciones.

La red neuronal


Hace aproximadamente 2.5 meses, OpenAI publicó una publicación de blog , donde demostraron casi imposible: un modelo de aprendizaje profundo, que puede escribir artículos, indistinguibles de los escritos por humanos. El texto que generó fue tan impresionante que tuve que revisar el calendario para asegurarme de que no fuera una broma de los Inocentes (fíjate que era febrero y que Seattle estaba cubierta de nieve).

Muestra de texto GPT-2


No lanzaron la red neuronal más grande con más de mil millones de parámetros que construyeron hasta el día de hoy (una decisión muy controvertida), pero obtuvieron una versión más pequeña de 117M de parámetros en GitHub bajo licencia MIT. El modelo tiene un nombre muy inmemorable: GPT-2 .


Entonces, hace aproximadamente un mes, cuando estaba tratando de pensar qué proyecto genial podría hacer con TensorFlow, esa red se convirtió en el punto de partida. Si ya podía generar texto en inglés, no debería haber sido demasiado difícil ajustarlo para generar letras de canciones, si hay un conjunto de datos lo suficientemente grande.


¿Cómo funciona GPT-2?


Hay varios logros importantes en la investigación de aprendizaje profundo, que hicieron posible GPT-2:


Aprendizaje auto supervisado


Yan LeCunn finalizó su nombre con esta técnica solo unos días después de que escribí la primera versión de este artículo. Es una técnica muy poderosa, que se puede aplicar básicamente a cualquier tipo de datos del mundo real. Para entrenar a GPT-2, OpenAI recolectó decenas de gigabytes de artículos de varias fuentes, que fueron votados en Reddit.


Convencionalmente, uno tendría que tener un ser humano para revisar todos estos artículos y, por ejemplo, marcarlos como "positivos" o "negativos". Luego enseñarían una red neuronal de manera supervisada para clasificar estos artículos de la misma manera que lo hizo un humano.


RECAPTCHA: encuentra carteles callejeros


La nueva idea aquí es que para crear un modelo de aprendizaje profundo, que tenga una comprensión de alto nivel de sus datos, simplemente corrompe los datos y le encarga al modelo que restaure el original. Esto hace que el modelo entienda las conexiones entre datos y sus contextos circundantes.


Tomemos el texto como ejemplo. GPT-2 toma una muestra del texto original, recoge el 15% de las fichas que se corromperán, luego enmascara el 80% de ellas (por ejemplo, las reemplaza con una ficha de máscara especial, generalmente ___), reemplaza el 10% con alguna otra ficha aleatoria del diccionario, y mantiene el 10% restante intacto. Tome , tiré una pelota y cayó al pasto . Después de la corrupción, podría verse así: tiré la pelota del auto y ___ al césped . En términos simples, para que la red restaure el original, debe aprender que es probable que algo arrojado caiga, y que la pelota del auto es algo muy poco común en el contexto.


Un modelo entrenado así es bueno para generar / completar datos parciales, pero las características de alto nivel que aprendió (como salidas de capas internas) se pueden usar para otros fines agregando una capa o dos encima de ellas y ajustando solo esa nueva última capa en un conjunto de datos real, más pequeño y marcado por humanos de una manera convencional.


Escasa autoatención


GPT-2 usa algo llamado escasa auto-atención. En esencia, es una técnica que permite que el procesamiento de la red neuronal de gran entrada se centre en algunas partes de ella más que en otras. Y la red aprende dónde debe "mirar" durante el entrenamiento. El mecanismo de atención se explica mejor en esta publicación de blog .


La parte escasa en el título de esta sección se refiere a una restricción sobre qué segmentos de entrada puede elegir el mecanismo de atención. La atención inicial podría elegir entre toda la entrada. Eso causó que su matriz de peso fuera O (input_size ^ 2), que crece muy rápidamente con el tamaño de la entrada. La escasa atención generalmente restringe eso de alguna manera. Para obtener más información al respecto, eche un vistazo a otra publicación de blog de OpenAI .


La atención en GPT-2 es de múltiples cabezas . Imagine que podría tener un ojo adicional o dos que podría usar para verificar lo que estaba en el último párrafo sin dejar de leer el actual.


Muchos mas


Conexiones residuales , codificación de pares de bytes , predicción de la siguiente oración y mucho más.


Portar GPT-2 (y convertir Python en general)


El código del modelo original está en Python, pero soy un chico de C #. Afortunadamente, el código fuente es bastante legible, y su quid está en solo 5 archivos, tal vez 500 líneas en total. Así que creé un nuevo proyecto .NET Standard, instalé Gradient (un enlace TensorFlow para .NET) y convertí esos archivos línea por línea en C #. Eso me llevó unas 2 horas. La única cosa pitónica que quedaba en el código era el uso del módulo Python regex de pip (el administrador de paquetes más utilizado para Python), ya que no quería perder el tiempo aprendiendo las complejidades de las expresiones regulares de Python ( como si no fuera suficiente para tratar con .NET ya ).


En su mayoría, la conversión consistió en definir clases similares, agregar tipos y reescribir las comprensiones de la lista de Python en las construcciones LINQ correspondientes. Además de LINQ de la biblioteca estándar, utilicé MoreLinq , que amplía ligeramente lo que LINQ puede hacer. Por ejemplo:


bs = list(range(ord("!"), ord("~")+1)) + list(range(ord("¡"), ord("¬")+1)) + list(range(ord(""), ord("ÿ")+1)) 

convertido en:


 var bs = Range('!', '~' - '!' + 1) .Concat(Range('¡', '¬' -'¡' + 1)) .Concat(Range('', 'ÿ' - '' + 1)) .ToList(); 

Otra cosa con la que tuve que luchar fue la discrepancia entre la forma en que Python maneja los rangos, y los nuevos rangos e índices aparecen en el próximo C # 8, que descubrí al depurar mis ejecuciones iniciales: en C # 8 el final del rango es inclusivo , mientras que en Python es exclusivo (para incluir el último elemento en Python, debe omitir el lado derecho de ... expresión).


Hay dos cosas difíciles en informática: invalidación de caché, nombrar cosas y errores fuera de uno.

Desafortunadamente, la caída de la fuente original no contenía ningún entrenamiento o incluso código de ajuste, pero Neil Shepperd proporcionó un sintonizador simple en su GitHub , que también tuve que portar. De todos modos, el resultado de ese esfuerzo es un código C # , que se puede usar para jugar con GPT-2 , ahora forma parte del repositorio de muestras de degradado.


El objetivo del ejercicio de transferencia es doble: después de la transferencia, uno puede jugar con el código del modelo en su ID de C # favorito y mostrar que ahora es posible lograr que los modelos de aprendizaje profundo de última generación funcionen de manera personalizada .NET proyecta poco después del lanzamiento (entre la caída del código de GPT-2 y el primer lanzamiento de Billion Songs, solo un poco más de un mes).


Afinando la letra de la canción


Hay varias formas de obtener un gran corpus de letras de canciones. Puede raspar uno de los sitios web de Internet que lo alojan con un analizador HTML, extraerlo de su colección de karaoke o archivos mp3. Afortunadamente, alguien lo hizo por nosotros. Encontré bastantes conjuntos de datos de letras preparados en Kaggle . " Cada canción que has escuchado " parecía ser la más grande. Intentando ajustar GPT-2 a él, me enfrenté a dos problemas.


Lectura CSV


Sí, lo leyó correctamente, el análisis CSV fue un problema . Inicialmente, quería usar ML.NET, la nueva biblioteca de Microsoft para aprendizaje automático, para leer el archivo. Sin embargo, después de hojear la documentación y configurarla, me di cuenta de que no pudo procesar los saltos de línea en las canciones correctamente. No importa lo que hice, luchó después de unos cientos de ejemplos, y comenzó a mezclar piezas de letras con títulos y artistas.


Así que tuve que recurrir a una biblioteca de nivel inferior, que anteriormente tenía una mejor experiencia con: CsvHelper . Proporciona una interfaz tipo DataReader . Puedes ver el código usándolo aquí . Esencialmente, abre un archivo, configura un CsvReader y luego intercala la llamada a .Read () con la (s) llamada (s) a .GetField (fieldName) .


Canciones cortas


La mayoría de las canciones son cortas en comparación con un artículo promedio en el conjunto de datos original utilizado por OpenAI. El entrenamiento GPT-2 es más eficiente en grandes fragmentos de texto, por lo que tuve que agrupar varias canciones en fragmentos de texto continuo para alimentarlas al entrenador. OpenAI también parecía usar esta técnica, por lo que tenían un token especial <| endoftext |> , que actúa como un separador entre los textos completos dentro de un fragmento, y se dobla como el token de inicio. Agrupe las canciones hasta que se alcanzara un cierto número de tokens, luego devolví todo el fragmento para incluirlo en los datos de entrenamiento. El código relevante está aquí .


Requisitos de hardware para el ajuste


Incluso la versión más pequeña de GPT-2 es grande. Con 12 GB de RAM de GPU solo pude establecer el tamaño del lote en 2 (por ejemplo, entrenar en dos trozos a la vez, los tamaños de lote más grandes mejoran el rendimiento de la GPU y los resultados del entrenamiento). 3 se perderían de memoria en CUDA. Y me llevó medio día ajustarlo al rendimiento deseado en mi V100. La ventaja es que puede ver el progreso, ya que de vez en cuando el código de entrenamiento genera algunas muestras generadas, que comienzan como texto simple y simple, y se parecen cada vez más a las letras de las canciones a medida que avanza el entrenamiento.


No lo he probado, pero el entrenamiento en CPU probablemente será muy lento .


Modelo preajustado


Mientras preparaba esta publicación de blog, me di cuenta de que sería mejor no obligar a todos a pasar horas ajustando el modelo de letra , así que publiqué una sintonizada previamente en el repositorio de Billion Songs . Si solo está intentando ejecutar Billion Songs, ni siquiera tiene que descargarlo manualmente. El proyecto lo hará por usted por defecto.


modelo medio entrenado jugando HAL9000 en mí
Te lo juro, se supone que debo escribirte
Y te lo juro, lo juro
Lo arruinaste ahora, espero que lo hagas
Y espero tu sueño, espero que sueñes, espero que sueñes, espero que sueñes, espero que sueñes
Acerca de
a lo que voy Me voy Me voy Me voy, me voy, me voy, me voy, me voy, me voy, me voy,
Me voy, me voy, me voy ...

Hacer un sitio web


OK! Parece una canción (más o menos), ¡ahora hagamos un sitio web!


Como no planeo proporcionar ninguna API, elijo la plantilla de Razor Pages en lugar de MVC. También activé la autorización, ya que permitiremos a los usuarios votar por las mejores letras y tener una lista de los 10 mejores.


Apresurando el MVP, seguí adelante y creé una página web Song.cshtml, cuyo objetivo por ahora será simplemente llamar a GPT-2 y obtener una canción aleatoria. El diseño de la página es trivial, y básicamente consiste en la canción y su título:


 @page "/song/{id}" @model BillionSongs.Pages.SongModel</p> @{ ViewData["Title"] = @Model.Song.Title ?? "Untitled"; } <article style="text-align: center"> <h3>@(Model.Song.Title ?? "Untitled")</h3> <pre>@Model.Song.Lyrics</pre> </article> 


Ahora, porque me gusta que mi código sea reutilizable, creé una interfaz que me permitirá conectar diferentes generadores de letras más adelante, que ASP.NET inyectará en SongModel.


 interface ILyricsGenerator { Task<string> GenerateLyrics(uint song, CancellationToken cancellation); } 

Omitiendo el título de la canción por ahora, todo lo que tenemos que hacer es registrar Gpt2LyricsGenerator en Inicio. Configure los Servicios y llame desde SongModel . Entonces, comencemos con el generador. Y lo primero que debemos asegurar es que tenemos


Generación de letras repetibles


Debido a que hice una declaración audaz en el título, que va a ser más de mil millones de canciones, ni siquiera piense en generarlas y almacenarlas. Primero, sin metadatos, eso tomaría solo más de 1TB de espacio en disco. En segundo lugar, se tarda ~ 3 minutos en mi nettop para generar una nueva canción, por lo que tardará una eternidad en generarlas todas. ¡Y quiero poder convertir esos mil millones en un quintillón cambiando a Int64 si es necesario! ¿Imagina que podríamos hacer 1 centavo por canción, en un quintillón de canciones? ¡Eso sería más que el PIB anual actual mundial!


En cambio, lo que debemos hacer es asegurarnos de que GPT-2 genere la misma canción una y otra vez, dada su identificación , que especifico en la ruta. Para ese propósito, TensorFlow brinda la capacidad de establecer la semilla de su generador de números interno en cualquier momento a través de la función tf.set_random_seed de esta manera: tf.set_random_seed (songNumber) . Entonces solo quería llamar a Gpt2Sampler.SampleSequence , para obtener el texto de la canción codificada, decodificarlo y devolver el resultado, completando así Gpt2LyricsGenerator .


Desafortunadamente, en el primer intento eso no funcionó como se esperaba. Cada vez que presiono el botón Actualizar, se devuelve un nuevo texto único en la página. Después de un poco de depuración, finalmente descubrí que TensorFlow 1.X tiene problemas importantes con la reproducibilidad: muchas operaciones tienen estados internos, que no se ven afectados por set_random_seed y son difíciles de alcanzar para restablecer.


La reinicialización de las variables del modelo ayudó a compensar ese problema, pero también significó que la sesión debe recrearse, y los pesos del modelo tuvieron que recargarse en cada llamada. Volver a cargar una sesión de ese tamaño provocó una pérdida de memoria gigante. Para evitar buscar su causa en el código fuente de TensorFlow C ++, en lugar de generar texto en el proceso, decidí generar un nuevo proceso con Process.Start , generar texto allí y leerlo desde la salida estándar. Hasta que se estabilice una forma de restablecer el estado del modelo en TensorFlow, este sería el camino a seguir.


Así que terminé con dos clases: Gpt2LyricsGenerator , que implementa ILyricsGenerator desde arriba, generando una nueva instancia de BillionSongs.exe con parámetros de línea de comando, que incluyen la identificación de la canción, y eventualmente instancia Gpt2TextGenerator , que en realidad llama a GPT-2 para generar letras, y simplemente lo imprime.


Ahora actualizar la página siempre me dio el mismo texto.


Tratar con 3 minutos de tiempo para generar una canción


¡Qué experiencia de usuario horrible sería! Vaya a un sitio web, haga clic en "Crear nueva canción", y no pasa absolutamente nada durante 3 (!) Minutos mientras mi nettop se toma su tiempo para generar la letra de la canción que solicitó.


Resolví este problema en múltiples niveles:


Canciones pregenerativas


Como se mencionó anteriormente, no puede pregenerar todas las canciones y servirlas desde una base de datos. Y no puede simplemente generar a pedido, porque eso es disminuir. Entonces que puedes hacer?


Simple! Dado que la forma principal para que los usuarios vean una nueva canción es hacer clic en el botón "Hacer aleatorio", pregeneremos muchas canciones de antemano, colóquelas en una ConcurrentQueue y dejemos que "Hacer canciones aleatorias" aparezcan. Si bien el número de visitantes es bajo, el servidor tomará tiempo entre ellos para generar algunas canciones, que luego serán fácilmente accesibles.


Otro truco que utilicé es hacer un bucle en esa cola varias veces, para que muchos usuarios pudieran ver la misma canción pregenerada. Solo hay que mantener un equilibrio entre el uso de RAM y cuántas veces un usuario tiene que hacer clic en "Hacer aleatorio" para ver algo que ha visto antes. Simplemente elegí 50,000 canciones como un número razonable, lo que tomaría solo 50MB de RAM, mientras proporcionaba una gran cantidad de clics.


Implementé esa funcionalidad en la clase PregeneratedSongProvider : IRandomSongProvider (la interfaz se inyecta en el código, responsable de manejar el botón "Hacer aleatorio").


Almacenamiento en caché


Las canciones pregeneradas se almacenan en caché en la memoria, pero también configuro el encabezado de caché HTTP en público para permitir que el navegador, y CDN (uso CloudFlare) lo almacene en caché para evitar ser golpeado por la afluencia de usuarios.


 [ResponseCache(VaryByHeader = "User-Agent", Duration = 3*60*60)] public class SongModel: PageModel { … } 

Volviendo canciones populares


La mayoría de las canciones generadas por GPT-2 afinado de esa manera son bastante aburridas, si no rudimentarias. Para hacer que los clics en "Hacer aleatorio" sean más atractivos, agregué un 25% de probabilidad de que, en lugar de una canción completamente aleatoria, obtengas alguna canción que otros usuarios hayan votado previamente. Además de aumentar el compromiso, aumenta la posibilidad de que solicite una canción, almacenada en caché en el CDN o en la memoria.


Todos los trucos anteriores están conectados entre sí mediante la inyección de dependencia ASP.NET en la clase de inicio .


Votar


No hay mucho especial sobre la implementación de la votación. Hay SongVoteCache , que mantiene los recuentos actualizados. Y un iframe que aloja los botones de voto en la página de la canción, que permite almacenar en caché la parte esencial de la página: título y letra, mientras que el conteo de votos y el estado de inicio de sesión se cargan más tarde.


Los resultados finales


Muestra de canción generada


Una versión de demostración que se ejecuta en mi nettop, liderada por CloudFlare (darle un poco de holgura, su Core i3) ahora congelada y movida al nivel gratuito de Azure App Service.


El repositorio de GitHub , que contiene el código fuente, y las instrucciones para ejecutar el sitio web y ajustar el modelo.


Planes para el futuro / ejercicios


Generar títulos


GPT-2 es muy fácil de ajustar. Se podría hacer que genere títulos de canciones al anteponer o sufijar cada muestra de letra del conjunto de datos con un token artificial como <| startoftitle |> , seguido del título del mismo conjunto de datos.


Alternativamente, los usuarios podrían sugerir y / o votar por títulos.


Generar música


A la mitad del desarrollo de Billion Songs, pensé que sería genial descargar un montón de archivos MIDI (que es un formato de música de la vieja escuela, que está mucho más cerca del texto que los mp3), y entrenar GPT-2 en ellos para generar más . Algunos de esos archivos incluso tenían texto incrustado, por lo que podría obtener la generación de karaoke .


Sé que la generación de música de esta manera es muy posible, porque ayer OpenAI en realidad publicó una implementación de esa idea en su blog . Pero, ¡hurra, no hicieron el karaoke! Descubrí que es posible raspar http://www.midi-karaoke.info para ese propósito.


Gradiente, también conocido como TensorFlow para .NET


Por favor, vea nuestro blog para cualquier actualización.

Source: https://habr.com/ru/post/453232/


All Articles