Écrire des milliards de chansons avec C # et Deep Learning

Dans cet article, je vais expliquer comment créer un site Web ASP.NET Core , qui utilise l'IA pour générer des paroles de chansons uniques en un seul clic, et permet aux utilisateurs de voter pour les meilleures chansons.

Le réseau neuronal


Il y a environ 2,5 mois, OpenAI a publié un article de blog , où ils ont démontré presque impossible: un modèle d'apprentissage en profondeur, qui peut écrire des articles, indiscernables de ceux écrits par des humains. Le texte qu'il a généré était si impressionnant, que j'ai dû vérifier le calendrier pour m'assurer que ce n'était pas une blague de poisson d'avril (rappelez-vous que c'était en février et que Seattle était couverte de neige).

Exemple de texte GPT-2


Ils n'ont pas publié le plus grand réseau de neurones avec plus d'un milliard de paramètres qu'ils ont construits à ce jour (une décision très controversée), mais ils ont ouvert une version plus petite de 117 millions de paramètres sur GitHub sous licence MIT. Le modèle porte un nom très inoubliable: GPT-2 .


Il y a environ un mois, alors que j'essayais de penser au projet cool que je pouvais faire avec TensorFlow, ce réseau est devenu le point de départ. S'il pouvait déjà générer du texte anglais, il n'aurait pas dû être trop difficile de le régler avec précision pour générer les paroles des chansons, s'il existe un ensemble de données suffisamment volumineux.


Comment fonctionne GPT-2?


Il existe plusieurs réalisations importantes dans la recherche sur l'apprentissage en profondeur, qui ont rendu possible le GPT-2:


Apprentissage auto-supervisé


Cette technique a obtenu son nom finalisé par Yan LeCunn quelques jours seulement après avoir écrit la première version de cet article. C'est une technique très puissante, qui peut être appliquée à pratiquement n'importe quel type de données du monde réel. Pour former GPT-2, OpenAI a collecté des dizaines de gigaoctets d'articles provenant de diverses sources, qui ont été votés sur Reddit.


Classiquement, il faudrait avoir un être humain pour parcourir tous ces articles et, par exemple, les marquer comme «positifs» ou «négatifs». Ensuite, ils enseigneraient un réseau de neurones de manière supervisée pour classer ces articles de la même manière qu'un humain.


RECAPTCHA: trouver des signes stree


La nouvelle idée ici est que pour créer un modèle d'apprentissage en profondeur, qui a une compréhension de haut niveau de vos données, vous corrompez simplement les données et chargez le modèle de restaurer l'original. Cela permet au modèle de comprendre les connexions entre les éléments de données et leurs contextes environnants.


Prenons le texte comme exemple. GPT-2 prend un échantillon du texte d'origine, sélectionne 15% des jetons à corrompre, puis en masque 80% (par exemple, remplace par un jeton de masque spécial, généralement ___), remplace 10% par un autre jeton aléatoire du dictionnaire, et conserve les 10% restants intacts. Prenez j'ai lancé une balle et elle est tombée dans l'herbe . Après la corruption, cela pourrait ressembler à ceci: j'ai jeté une balle de voiture et ___ dans l'herbe . En termes simples, pour que le réseau rétablisse l'original, il doit apprendre que quelque chose sera probablement tombé et que la balle de voiture est quelque chose de très rare dans le contexte.


Un modèle formé comme ça est bon pour générer / compléter des données partielles, mais les fonctionnalités de haut niveau qu'il a apprises (en tant que sorties de couches internes) peuvent être utilisées à d'autres fins en ajoutant une couche ou deux au-dessus et en affinant seulement cette nouvelle dernière couche sur un ensemble de données réel, plus petit et marqué par l'homme d'une manière conventionnelle.


Peu d'attention


GPT-2 utilise ce que l'on appelle l'auto-attention clairsemée. En substance, c'est une technique qui permet au réseau de neurones traitant de grandes entrées de se concentrer sur certaines parties de celui-ci plus que sur d'autres. Et le réseau apprend où il doit «regarder» pendant la formation. Le mécanisme d'attention est mieux expliqué dans ce billet de blog .


La partie clairsemée du titre de cette section fait référence à une restriction sur les segments d'entrée dans lesquels le mécanisme d'attention peut choisir. L'attention initiale pourrait choisir parmi l'ensemble de l'entrée. Cela a provoqué sa matrice de poids à O (input_size ^ 2), qui croît très rapidement avec la taille de l'entrée. Une attention limitée restreint généralement cela d'une certaine manière. Pour plus d'informations à ce sujet, consultez un autre article de blog OpenAI .


L'attention dans GPT-2 est multi-tête . Imaginez que vous puissiez avoir un ou deux yeux supplémentaires que vous pourriez utiliser pour vérifier ce qui était dans le dernier paragraphe sans arrêter la lecture de l'actuel.


Beaucoup plus


Connexions résiduelles , codage de paire d'octets , prédiction de la phrase suivante et bien d'autres.


Portage de GPT-2 (et conversion de Python en général)


Le code du modèle d'origine est en Python, mais je suis un gars C #. Heureusement, le code source est assez lisible, et le nœud de celui - ci est en seulement 5 fichiers, peut-être 500 lignes au total. J'ai donc créé un nouveau projet .NET Standard, installé Gradient (une liaison TensorFlow pour .NET) et converti ces fichiers ligne par ligne en C #. Cela m'a pris environ 2 heures. La seule chose pythonique restante dans le code était l'utilisation du module regex Python de pip (le gestionnaire de packages le plus couramment utilisé pour Python), car je ne voulais pas perdre de temps à apprendre les subtilités des expressions régulières Python ( comme si cela ne suffisait pas). pour traiter déjà ceux .NET ).


La conversion consistait principalement à définir des classes similaires, à ajouter des types et à réécrire les compréhensions de liste Python dans les constructions LINQ correspondantes. En plus de LINQ de la bibliothèque standard, j'ai utilisé MoreLinq , qui étend légèrement ce que LINQ peut faire. Par exemple:


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

transformé en:


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

Une autre chose avec laquelle je devais me battre était une divergence entre la façon dont Python gère les plages et les nouvelles fonctionnalités des plages et des index dans le prochain C # 8, que j'ai découvert lors du débogage de mes exécutions initiales: en C # 8, la fin de la plage est inclusive , tandis qu'en Python il est exclusif (pour inclure le tout dernier élément en Python, vous devez omettre le côté droit de .. expression).


Il y a deux choses difficiles en informatique: l'invalidation du cache, le nommage des choses et les erreurs ponctuelles.

Malheureusement, le drop source d'origine ne contenait aucune formation ni même de code de réglage fin, mais Neil Shepperd a fourni un simple réglage fin sur son GitHub , que je devais également porter. Quoi qu'il en soit, le résultat de cet effort est un code C # , qui peut être utilisé pour jouer avec GPT-2 , fait maintenant partie du référentiel des échantillons de dégradé.


Le but de l'exercice de portage est double: après le portage, on peut jouer avec le code du modèle dans son IDE C # préféré et montrer qu'il est désormais possible d'obtenir des modèles d'apprentissage en profondeur de pointe fonctionnant en mode personnalisé. Les projets .NET peu de temps après la sortie (entre la chute de code de GPT-2 et la première version de Billion Songs - un peu plus d'un mois).


Affiner les paroles des chansons


Il existe plusieurs façons d'obtenir un grand corpus de paroles de chansons. Vous pouvez gratter l'un des sites Internet qui l'hébergent avec un analyseur HTML, le retirer de votre collection de karaoké ou des fichiers mp3. Heureusement, quelqu'un l'a fait pour nous. J'ai trouvé pas mal de jeux de données de paroles préparés sur Kaggle . « Chaque chanson que vous avez entendue » semblait être la plus importante. En essayant d'affiner GPT-2, j'ai rencontré deux problèmes.


Lecture CSV


Oui, vous l'avez lu correctement, l' analyse CSV était un problème . Au départ, je voulais utiliser ML.NET, la nouvelle bibliothèque Microsoft pour l'apprentissage automatique, pour lire le fichier. Cependant, après avoir parcouru la documentation et l'avoir configurée, je me suis rendu compte qu'elle ne pouvait pas traiter correctement les sauts de ligne dans les chansons. Peu importe ce que j'ai fait, il a eu du mal après quelques centaines d'exemples et a commencé à mélanger des morceaux de paroles avec des titres et des artistes.


J'ai donc dû recourir à une bibliothèque de niveau inférieur, avec laquelle j'avais auparavant une meilleure expérience: CsvHelper . Il fournit une interface de type DataReader . Vous pouvez voir le code en l'utilisant ici . Essentiellement, vous ouvrez un fichier, configurez un CsvReader , puis entrelacez l'appel à .Read () avec les appels à .GetField (fieldName) .


Chansons courtes


La plupart des chansons sont courtes par rapport à un article moyen du jeu de données original utilisé par OpenAI. La formation GPT-2 est plus efficace sur de gros morceaux de texte, j'ai donc dû regrouper plusieurs chansons en morceaux de texte continus pour les transmettre au formateur. OpenAI semblait également utiliser cette technique, ils ont donc eu un jeton spécial <| endoftext |> , qui agit comme un séparateur entre les textes complets au sein d'un morceau, et sert également de jeton de début. J'ai regroupé des chansons jusqu'à ce qu'un certain nombre de jetons soit atteint, puis j'ai renvoyé le morceau entier à inclure dans les données d'entraînement. Le code correspondant est ici .


Configuration matérielle requise pour le réglage


Même la version plus petite de GPT-2 est grande. Avec 12 Go de RAM GPU, je ne pouvais que définir la taille du lot à 2 (par exemple, former sur deux morceaux à la fois, des tailles de lot plus grandes améliorent les performances du GPU et les résultats de la formation). 3 jetterait de mémoire dans CUDA. Et il a fallu une demi-journée pour régler les performances souhaitées sur mon V100. Le bonus est que vous pouvez voir la progression, car de temps en temps le code de formation génère des échantillons générés, qui commencent sous forme de texte simple et ressemblent de plus en plus aux paroles des chansons au fur et à mesure que la formation progresse.


Je ne l'ai pas essayé, mais la formation sur le CPU sera probablement très lente .


Modèle pré-réglé


Alors que je préparais ce billet de blog, j'ai réalisé qu'il valait mieux ne pas forcer tout le monde à passer des heures à affiner le modèle des paroles , j'ai donc publié un pré-réglé sur le référentiel Billion Songs . Si vous essayez simplement d'exécuter Billion Songs, vous n'avez même pas besoin de le télécharger manuellement. Le projet le fera par défaut pour vous.


mannequin semi-formé jouant HAL9000 sur moi
Je te jure, je suis censé t'écrire
Et je te jure, je jure
Tu l'as ruiné maintenant, j'espère que tu y arriveras
Et j'espère que tu rêves, j'espère que tu rêves, j'espère que tu rêves J'espère que tu rêves J'espère que tu rêves
À propos
ce que je vais. Je m'en vais. Je m'en vais. Je vais, je vais, je vais, je vais, je vais, je vais, je vais,
Je vais, je vais, je vais ...

Créer un site Web


OK! Cela ressemble à une chanson (en quelque sorte), faisons maintenant un site web!


Comme je ne prévois pas de fournir d'API, je choisis le modèle Razor Pages par opposition à MVC. J'ai également activé l'autorisation, car nous autoriserons les utilisateurs à voter pour les meilleures paroles et à avoir un top 10.


En précipitant le MVP, je suis allé de l'avant et j'ai créé une page Web Song.cshtml, dont l'objectif pour l'instant sera d'appeler simplement GPT-2 et d'obtenir une chanson au hasard. La mise en page de la page est triviale et se compose essentiellement de la chanson et de son titre:


 @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> 


Maintenant parce que j'aime mon code réutilisable, j'ai créé une interface, qui me permettra de brancher différents générateurs de paroles plus tard, qui seront injectés par ASP.NET dans SongModel.


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

En omettant le titre du morceau pour l'instant, tout ce que nous devons faire est d'enregistrer Gpt2LyricsGenerator dans le démarrage, de configurer les services et de l'appeler à partir du SongModel . Commençons donc par le générateur. Et la première chose que nous devons nous assurer, c'est que nous avons


Génération de paroles répétables


Parce que j'ai fait une déclaration audacieuse dans le titre, qu'il va y avoir plus d'un milliard de chansons, ne pensez même pas à les générer et à les stocker toutes. Tout d'abord, sans métadonnées, cela prendrait à lui seul plus de 1 To d'espace disque. Deuxièmement, il faut environ 3 minutes sur mon nettop pour générer une nouvelle chanson, il faudra donc une éternité pour les générer toutes. Et je veux pouvoir transformer ce milliard en quintillion en passant à Int64 si nécessaire! Imaginez que nous pourrions faire 1 cent par chanson, sur 1 quintillion de chansons? Ce serait plus que le PIB annuel mondial actuel!


Au lieu de cela, ce que nous devons faire est de nous assurer que GPT-2 génère encore et encore le même morceau, étant donné son identifiant , que je spécifie dans l'itinéraire. À cet effet, TensorFlow donne la possibilité de définir la graine de son générateur de nombres interne à tout moment via la fonction tf.set_random_seed comme ceci: tf.set_random_seed (songNumber) . Ensuite, je voulais simplement appeler Gpt2Sampler.SampleSequence , pour obtenir le texte du morceau encodé, le décoder et retourner le résultat, complétant ainsi Gpt2LyricsGenerator .


Malheureusement, au premier essai, cela n'a pas fonctionné comme prévu. Chaque fois que je cliquais sur le bouton d'actualisation, un nouveau texte unique était renvoyé sur la page. Après pas mal de débogage, j'ai finalement découvert que TensorFlow 1.X avait des problèmes de reproductibilité importants: de nombreuses opérations ont des états internes, qui ne sont pas affectés par set_random_seed et sont difficiles à atteindre pour réinitialiser.


La réinitialisation des variables du modèle a permis de compenser ce problème, mais a également signifié que la session devait être recréée et que les poids du modèle devaient être rechargés à chaque appel. Le rechargement d'une session de cette taille a provoqué une fuite de mémoire géante. Pour éviter de rechercher sa cause dans le code source TensorFlow C ++, au lieu de générer un processus de génération de texte, j'ai décidé de générer un nouveau processus avec Process.Start , de générer du texte à cet endroit et de le lire à partir de la sortie standard. Jusqu'à ce qu'un moyen de réinitialiser l'état du modèle dans TensorFlow soit stabilisé, ce serait la voie à suivre.


Je me suis donc retrouvé avec deux classes: Gpt2LyricsGenerator , qui implémente ILyricsGenerator d'en haut en générant une nouvelle instance de BillionSongs.exe avec des paramètres de ligne de commande, qui incluent l'identifiant du morceau, et finalement instancie Gpt2TextGenerator , qui appelle en fait GPT-2 pour générer des paroles, et l'imprime simplement.


Maintenant, rafraîchir la page m'a toujours donné le même texte.


Gérer 3 minutes pour générer une chanson


Quelle expérience utilisateur horrible ce serait! Vous allez sur un site Web, cliquez sur «Créer une nouvelle chanson», et absolument rien ne se passe pendant 3 (!) Minutes pendant que mon nettop prend son temps pour générer les paroles des chansons que vous avez demandées.


J'ai résolu ce problème à plusieurs niveaux:


Chansons prégénérantes


Comme mentionné ci-dessus, vous ne pouvez pas pré-générer toutes les chansons et les servir à partir d'une base de données. Et vous ne pouvez pas simplement générer à la demande, car cela ralentit. Alors, que pouvez-vous faire?


C'est simple! Étant donné que le principal moyen pour les utilisateurs de voir une nouvelle chanson est de cliquer sur le bouton «Créer au hasard», préparons à l'avance un grand nombre de chansons, les mettons dans une file d'attente simultanée et laissons «Make Random» faire apparaître des chansons. Bien que le nombre de visiteurs soit faible, le serveur mettra du temps entre eux pour générer des chansons, qui seront ensuite facilement accessibles.


Une autre astuce que j'ai utilisée consiste à boucler cette file d'attente plusieurs fois, afin que de nombreux utilisateurs puissent voir la même chanson prégénérée. Il suffit de garder un équilibre entre l'utilisation de la RAM et le nombre de fois qu'un utilisateur doit cliquer sur «Créer au hasard» pour voir quelque chose qu'il a déjà vu. J'ai simplement choisi 50 000 chansons comme un nombre raisonnable, ce qui prendrait seulement 50 Mo de RAM, tout en fournissant un assez grand nombre de clics.


J'ai implémenté cette fonctionnalité dans la classe PregeneratedSongProvider : IRandomSongProvider (l'interface est injectée dans le code, responsable de la gestion du bouton «Make Random»).


Mise en cache


Les chansons pré-générées sont mises en cache dans la mémoire, mais j'ai également défini l'en-tête du cache HTTP sur public pour laisser le navigateur, et CDN (j'utilise CloudFlare) le mettre en cache pour éviter d'être touché par un afflux d'utilisateurs.


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

Retourner des chansons populaires


La plupart des chansons générées par le GPT-2 affiné de cette manière sont assez ennuyeuses, sinon rudimentaires. Pour rendre les clics sur «Make Random» plus attrayants, j'ai ajouté une probabilité de 25%, qu'au lieu d'une chanson complètement aléatoire, vous obtiendrez une chanson, qui a été précédemment votée par d'autres utilisateurs. En plus d'augmenter l'engagement, cela augmente les chances que vous demandiez une chanson, mise en cache soit dans le CDN, soit en mémoire.


Toutes les astuces ci-dessus sont câblées ensemble à l'aide de l'injection de dépendance ASP.NET dans la classe de démarrage .


Vote


La mise en œuvre du vote n'a rien de spécial. Il y a SongVoteCache , qui maintient les comptes à jour. Et un iframe hébergeant le bouton de vote sur la page de la chanson, ce qui permet de mettre en cache la partie essentielle de la page - le titre et les paroles, tandis que le décompte des votes et le statut de connexion sont chargés plus tard.


Les résultats finaux


Échantillon de chanson généré


Une version de démonstration fonctionnant sur mon nettop, dirigée par CloudFlare (donnez-lui du mou, son Core i3) est maintenant figée et déplacée vers le niveau gratuit d'Azure App Service.


Le référentiel GitHub , contenant le code source et les instructions pour exécuter le site Web et régler le modèle.


Plans pour l'avenir / exercices


Générer des titres


GPT-2 est très facile à affiner. On pourrait lui faire générer des titres de chansons en ajoutant ou en suffixant chaque échantillon de paroles de l'ensemble de données avec un jeton artificiel comme <| startoftitle |> , suivi du titre du même ensemble de données.


Alternativement, les utilisateurs pourraient être autorisés à suggérer et / ou voter pour des titres.


Générer de la musique


À mi-chemin du développement de Billion Songs, j'ai pensé que ce serait cool de télécharger un tas de fichiers MIDI (c'est un format de musique old-school, qui est beaucoup plus proche du texte que des mp3), et de former GPT-2 sur eux pour en générer plus . Certains de ces fichiers avaient même du texte intégré, vous pouvez donc obtenir la génération de karaoké .


Je sais que la génération musicale de cette façon est très possible, car hier, OpenAI a en fait publié une implémentation de cette idée sur son blog . Mais hourra, ils n'ont pas fait le karaoké! J'ai trouvé qu'il était possible de gratter http://www.midi-karaoke.info à cet effet.


Gradient aka TensorFlow pour .NET


Veuillez consulter notre blog pour toute mise à jour.

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


All Articles