Schreiben von Milliarden Songs mit C # und Deep Learning

In diesem Artikel werde ich erklären, wie eine ASP.NET Core-Website erstellt wird , die mithilfe von KI eindeutige Songtexte mit einem Klick auf eine Schaltfläche generiert und Benutzer für die besten Songs stimmen lässt.

Das neuronale Netzwerk


Vor ungefähr 2,5 Monaten veröffentlichte OpenAI einen Blog-Beitrag , in dem sie nahezu unmöglich demonstrierten: ein Deep-Learning-Modell, das Artikel schreiben kann, die nicht von denen zu unterscheiden sind, die von Menschen geschrieben wurden. Der generierte Text war so beeindruckend, dass ich den Kalender überprüfen musste, um sicherzustellen, dass es sich nicht um einen Aprilscherz handelt (wohlgemerkt, das war Februar, und Seattle war schneebedeckt).

GPT-2-Textbeispiel


Sie haben nicht das größte neuronale Netzwerk mit über 1 Milliarde Parametern veröffentlicht, das sie bis heute erstellt haben (eine sehr kontroverse Entscheidung), aber sie haben eine kleinere 117M-Parameterversion auf GitHub unter MIT-Lizenz als Open-Source-Version bereitgestellt. Das Modell hat einen sehr unvergesslichen Namen: GPT-2 .


Vor ungefähr einem Monat, als ich überlegte, welches coole Projekt ich mit TensorFlow machen könnte, wurde dieses Netzwerk zum Ausgangspunkt. Wenn bereits englischer Text generiert werden konnte, sollte es nicht zu schwierig sein, ihn zu optimieren , um Songtexte zu generieren, wenn ein ausreichend großer Datensatz vorhanden ist.


Wie funktioniert GPT-2?


In der Deep-Learning-Forschung gibt es mehrere wichtige Erfolge, die GPT-2 ermöglicht haben:


Selbstüberwachtes Lernen


Diese Technik wurde erst einige Tage, nachdem ich die erste Version dieses Artikels geschrieben hatte, von Yan LeCunn endgültig benannt. Es ist eine sehr leistungsfähige Technik, die auf praktisch jede Art von realen Daten angewendet werden kann. Um GPT-2 zu trainieren, sammelte OpenAI Dutzende Gigabyte an Artikeln aus verschiedenen Quellen, die auf Reddit hochgestuft wurden.


Herkömmlicherweise müsste man einen Menschen haben, um all diese Artikel durchzugehen und sie beispielsweise als „positiv“ oder „negativ“ zu markieren. Dann unterrichteten sie ein neuronales Netzwerk auf überwachte Weise, um diese Artikel genauso zu klassifizieren, wie es ein Mensch tat.


RECAPTCHA: Finde Stree-Schilder


Die neue Idee hier ist, dass Sie zum Erstellen eines Deep-Learning-Modells, das ein umfassendes Verständnis Ihrer Daten bietet, einfach die Daten beschädigen und das Modell damit beauftragen, das Original wiederherzustellen. Dadurch versteht das Modell die Zusammenhänge zwischen Daten und ihren umgebenden Kontexten.


Nehmen wir als Beispiel Text. GPT-2 nimmt eine Stichprobe des Originaltextes, wählt 15% der zu beschädigenden Token aus, maskiert dann 80% davon (z. B. ersetzt durch ein spezielles Maskentoken, normalerweise ___), ersetzt 10% durch ein anderes zufälliges Token aus dem Wörterbuch. und hält die restlichen 10% intakt. Nehmen wir, ich warf einen Ball, und er fiel ins Gras . Nach der Korruption könnte es so aussehen: Ich warf einen Autoball und er ___ ins Gras . Um das Original des Netzwerks wiederherzustellen, muss Laien lernen, dass etwas, das geworfen wird, wahrscheinlich fallen wird und dass der Autoball im Kontext etwas sehr Ungewöhnliches ist.


Ein so trainiertes Modell eignet sich gut zum Generieren / Vervollständigen von Teildaten, aber die erlernten Funktionen auf hoher Ebene (als Ausgaben für innere Ebenen) können für andere Zwecke verwendet werden, indem ein oder zwei Ebenen hinzugefügt und eine Feinabstimmung vorgenommen werden nur diese neue letzte Ebene auf einem tatsächlichen, kleineren , vom Menschen markierten Datensatz auf herkömmliche Weise.


Spärliche Selbstaufmerksamkeit


GPT-2 verwendet etwas, das als spärliche Selbstaufmerksamkeit bezeichnet wird. Im Wesentlichen handelt es sich um eine Technik, die es einem neuronalen Netzwerk ermöglicht, große Eingaben zu verarbeiten, um sich mehr auf einige Teile davon als auf andere zu konzentrieren. Und das Netzwerk lernt, wo es während des Trainings „hinschauen“ soll. Der Aufmerksamkeitsmechanismus wird in diesem Blog-Beitrag besser erklärt.


Der spärliche Teil im Titel dieses Abschnitts bezieht sich auf eine Einschränkung, aus welchen Eingabesegmenten der Aufmerksamkeitsmechanismus auswählen kann. Die anfängliche Aufmerksamkeit könnte aus der gesamten Eingabe wählen. Dies führte dazu, dass die Gewichtsmatrix O (input_size ^ 2) war, was mit der Größe der Eingabe sehr schnell wächst. Eine geringe Aufmerksamkeit schränkt dies normalerweise in irgendeiner Weise ein. Weitere Informationen hierzu finden Sie in einem anderen OpenAI-Blogbeitrag .


Die Aufmerksamkeit in GPT-2 ist Multi-Head . Stellen Sie sich vor, Sie könnten ein oder zwei zusätzliche Augen haben, mit denen Sie überprüfen können, was im letzten Absatz enthalten ist, ohne das aktuelle zu lesen.


Viele mehr


Restverbindungen , Bytepaarkodierung , Vorhersage des nächsten Satzes und vieles mehr.


Portieren von GPT-2 (und Konvertieren von Python im Allgemeinen)


Der ursprüngliche Modellcode ist in Python, aber ich bin ein C # -Typ. Glücklicherweise ist der Quellcode gut lesbar und der Kern liegt in nur 5 Dateien, vielleicht insgesamt 500 Zeilen. Also habe ich ein neues .NET Standard-Projekt erstellt, Gradient (eine TensorFlow-Bindung für .NET) installiert und diese Dateien Zeile für Zeile in C # konvertiert. Das hat mich ungefähr 2 Stunden gekostet. Die einzige pythonische Sache im Code war die Verwendung des Python-Regex-Moduls von pip (dem am häufigsten verwendeten Paketmanager für Python), da ich keine Zeit damit verschwenden wollte, die Feinheiten der regulären Python-Ausdrücke zu lernen ( als ob es nicht genug wäre bereits mit .NET umgehen ).


Die Konvertierung bestand hauptsächlich darin, ähnliche Klassen zu definieren, Typen hinzuzufügen und Python- Listenverständnisse in entsprechende LINQ-Konstrukte umzuschreiben. Zusätzlich zu LINQ aus der Standardbibliothek habe ich MoreLinq verwendet , das die Möglichkeiten von LINQ geringfügig erweitert. Beispiel:


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

verwandelt in:


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

Eine andere Sache, mit der ich zu kämpfen hatte, war eine Diskrepanz zwischen der Art und Weise, wie Python mit Bereichen umgeht, und der neuen Funktion für Bereiche und Indizes im kommenden C # 8, die ich beim Debuggen meiner ersten Läufe entdeckt habe: In C # 8 ist das Ende des Bereichs inklusive , In Python ist es exklusiv (um das allerletzte Element in Python aufzunehmen, müssen Sie die rechte Seite des Ausdrucks .. weglassen).


In der Informatik gibt es zwei schwierige Dinge: Cache-Ungültigmachung, Benennung und Off-by-One-Fehler.

Leider enthielt der ursprüngliche Quell-Drop weder Training noch Feinabstimmungscode, aber Neil Shepperd stellte auf seinem GitHub einen einfachen Feinabstimmer zur Verfügung , den ich ebenfalls portieren musste. Das Ergebnis dieser Bemühungen ist jedoch, dass ein C # -Code , der zum Spielen mit GPT-2 verwendet werden kann , jetzt Teil des Gradient Samples- Repositorys ist.


Die Portierungsübung hat zwei Gründe: Nach dem Portieren kann man mit dem Modellcode in seiner bevorzugten C # -IDE spielen und zeigen, dass es jetzt möglich ist, hochmoderne Deep-Learning-Modelle benutzerdefiniert zum Laufen zu bringen .NET-Projekte kurz nach der Veröffentlichung (zwischen dem Code-Drop von GPT-2 und der ersten Veröffentlichung von Billion Songs - etwas mehr als einen Monat).


Feinabstimmung auf Songtexte


Es gibt verschiedene Möglichkeiten, wie man einen großen Korpus an Songtexten erhalten kann. Sie können eine der Internet-Websites, auf denen sie gehostet wird, mit einem HTML-Parser durchsuchen, aus Ihrer Karaoke-Sammlung oder aus MP3-Dateien entfernen. Zum Glück hat es jemand für uns getan. Ich habe einige vorbereitete Lyrics-Datensätze auf Kaggle gefunden . " Jedes Lied, das Sie gehört haben " schien das größte zu sein. Beim Versuch, GPT-2 darauf abzustimmen, hatte ich zwei Probleme.


CSV-Lesung


Ja, Sie haben es richtig gelesen, CSV-Analyse war ein Problem . Zunächst wollte ich ML.NET, die neue Microsoft-Bibliothek für maschinelles Lernen, zum Lesen der Datei verwenden. Nachdem ich die Dokumentation durchgesehen und eingerichtet hatte, stellte ich jedoch fest, dass Zeilenumbrüche in den Songs nicht richtig verarbeitet wurden. Egal was ich tat, es kämpfte nach ein paar hundert Beispielen und begann, Textstücke mit Titeln und Künstlern zu mischen.


Also musste ich auf eine Bibliothek auf niedrigerer Ebene zurückgreifen, mit der ich zuvor bessere Erfahrungen gemacht hatte: CsvHelper . Es bietet eine DataReader- ähnliche Schnittstelle. Sie können den Code hier sehen . Im Wesentlichen öffnen Sie eine Datei, konfigurieren einen CsvReader und verschachteln dann den Aufruf von .Read () mit dem Aufruf von .GetField (fieldName) .


Kurze Lieder


Die meisten Songs sind kurz im Vergleich zu einem durchschnittlichen Artikel im Originaldatensatz, der von OpenAI verwendet wird. Das GPT-2-Training ist bei großen Textstücken effizienter, daher musste ich mehrere Songs zu fortlaufenden Textblöcken bündeln, um sie dem Trainer zuzuführen. OpenAI schien diese Technik ebenfalls zu verwenden, daher hatten sie ein spezielles Token <| endoftext |> , das als Trennzeichen zwischen vollständigen Texten innerhalb eines Blocks fungiert und gleichzeitig als Start-Token fungiert. Ich habe Songs gebündelt, bis eine bestimmte Anzahl von Token erreicht war, und dann den gesamten Teil zurückgegeben, um ihn in die Trainingsdaten aufzunehmen. Der entsprechende Code ist hier .


Hardwareanforderungen für das Tuning


Auch die kleinere Version von GPT-2 ist groß. Mit 12 GB GPU-RAM konnte ich nur die Stapelgröße auf 2 einstellen (z. B. zwei Blöcke gleichzeitig trainieren, größere Stapelgrößen verbessern die GPU-Leistung und die Trainingsergebnisse). 3 würde in CUDA aus dem Speicher werfen. Und es dauerte einen halben Tag, um die gewünschte Leistung auf meinem V100 einzustellen. Der Bonus ist, dass Sie den Fortschritt sehen können, da der Trainingscode von Zeit zu Zeit einige generierte Samples ausgibt, die als einfacher Text beginnen und im Verlauf des Trainings immer mehr wie Songtexte aussehen.


Ich habe es nicht ausprobiert, aber das Training auf der CPU wird wahrscheinlich sehr langsam sein .


Vorab abgestimmtes Modell


Als ich diesen Blog-Beitrag vorbereitete, wurde mir klar, dass es besser ist, nicht alle zu zwingen, Stunden mit der Feinabstimmung des Textmodells zu verbringen, und so veröffentlichte ich einen vorab abgestimmten im Billion Songs-Repository . Wenn Sie nur versuchen, Billion Songs auszuführen, müssen Sie es nicht einmal manuell herunterladen. Das Projekt erledigt dies standardmäßig für Sie.


halb trainiertes Modell, das HAL9000 auf mir spielt
Ich schwöre dir, ich soll dir schreiben
Und ich schwöre dir, ich schwöre
Du hast es jetzt ruiniert, ich hoffe du schaffst es
Und ich hoffe dein Traum, ich hoffe du träumst, ich hoffe du träumst ich hoffe du träumst ich hoffe du träumst davon
Über
was ich gehe Ich gehe. Ich gehe. Ich gehe, ich gehe, ich gehe, ich gehe, ich gehe, ich gehe, ich gehe, ich gehe,
Ich gehe, ich gehe, ich gehe ...

Eine Website erstellen


OK! Das sieht aus wie ein Lied (irgendwie), jetzt machen wir eine Website!


Da ich keine APIs bereitstellen möchte, wähle ich im Gegensatz zu MVC die Vorlage "Razor Pages". Ich habe auch die Autorisierung aktiviert, da wir den Benutzern erlauben werden, für die besten Texte zu stimmen und eine Top-10-Tabelle zu haben.


Ich beeilte mich mit dem MVP und erstellte eine Song.cshtml-Webseite, deren Ziel es vorerst sein wird, einfach GPT-2 aufzurufen und einen zufälligen Song zu erhalten. Das Layout der Seite ist trivial und besteht im Wesentlichen aus dem Song und seinem Titel:


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


Da ich meinen Code jetzt wiederverwendbar mag, habe ich eine Schnittstelle erstellt, über die ich später verschiedene Textgeneratoren anschließen kann, die von ASP.NET in SongModel eingefügt werden.


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

Wenn Sie den Songtitel vorerst weglassen , müssen Sie lediglich Gpt2LyricsGenerator im Startup registrieren . Konfigurieren Sie die Dienste und rufen Sie ihn vom SongModel aus auf . Beginnen wir also mit dem Generator. Und das erste, was wir sicherstellen müssen, ist, dass wir haben


Wiederholbare Textgenerierung


Da ich im Titel eine kühne Aussage gemacht habe, dass es sich um über 1 Milliarde Songs handeln wird, denken Sie nicht einmal daran, alle zu generieren und zu speichern. Erstens würde dies ohne Metadaten allein mehr als 1 TB Speicherplatz beanspruchen. Zweitens dauert es auf meinem Nettop ~ 3 Minuten, um einen neuen Song zu generieren, so dass es ewig dauern wird, alle zu generieren. Und ich möchte in der Lage sein, diese Milliarde in eine Billion umzuwandeln, indem ich bei Bedarf auf Int64 umsteige ! Stellen Sie sich vor, wir könnten 1 Cent pro Song auf 1 Billion Songs machen? Das wäre mehr als das derzeitige jährliche BIP der Welt!


Stattdessen müssen wir sicherstellen, dass GPT-2 aufgrund seiner ID , die ich in der Route angegeben habe, immer wieder denselben Song generiert. Zu diesem Zweck bietet TensorFlow die Möglichkeit, den Startwert seines internen Zahlengenerators jederzeit über die Funktion tf.set_random_seed wie folgt festzulegen : tf.set_random_seed (songNumber) . Dann wollte ich einfach Gpt2Sampler.SampleSequence aufrufen , um den codierten Songtext abzurufen , zu dekodieren und das Ergebnis zurückzugeben, wodurch Gpt2LyricsGenerator vervollständigt wurde .


Leider hat das beim ersten Versuch nicht wie erwartet funktioniert. Jedes Mal, wenn ich auf die Schaltfläche "Aktualisieren" drückte, wurde ein neuer eindeutiger Text auf der Seite zurückgegeben. Nach einigem Debuggen stellte ich schließlich fest, dass TensorFlow 1.X erhebliche Probleme mit der Reproduzierbarkeit hat: Viele Vorgänge haben interne Zustände, die nicht von set_random_seed betroffen sind und schwer zurückzusetzen sind.


Die Neuinitialisierung der Modellvariablen trug zur Behebung dieses Problems bei, bedeutete jedoch auch, dass die Sitzung neu erstellt werden musste und die Modellgewichte bei jedem Aufruf neu geladen werden mussten. Das erneute Laden einer Sitzung dieser Größe verursachte einen riesigen Speicherverlust. Um zu vermeiden, dass im TensorFlow C ++ - Quellcode nach der Ursache gesucht wird, habe ich beschlossen, statt Process in Text einen neuen Prozess mit Process.Start zu erstellen , dort Text zu generieren und ihn aus der Standardausgabe zu lesen. Bis sich eine Möglichkeit zum Zurücksetzen des Modellstatus in TensorFlow stabilisiert hat, ist dies der richtige Weg.


Am Ende hatte ich zwei Klassen: Gpt2LyricsGenerator , der ILyricsGenerator von oben implementiert, indem eine neue Instanz von BillionSongs.exe mit Befehlszeilenparametern erzeugt wird, die die Song-ID enthalten, und schließlich Gpt2TextGenerator instanziiert, der tatsächlich GPT-2 aufruft, um Texte zu generieren, und druckt es einfach aus.


Das Aktualisieren der Seite gab mir immer den gleichen Text.


Umgang mit 3 Minuten Zeit, um einen Song zu generieren


Was für eine schreckliche Benutzererfahrung wäre das! Sie gehen auf eine Website, klicken auf "Neues Lied erstellen" und 3 (!) Minuten lang passiert absolut nichts, während sich mein Nettop die Zeit nimmt, um die von Ihnen angeforderten Songtexte zu generieren.


Ich habe dieses Problem auf mehreren Ebenen gelöst:


Songs vorgenerieren


Wie oben erwähnt, können Sie nicht alle Songs vorgenerieren und aus einer Datenbank bereitstellen. Und Sie können nicht einfach auf Abruf generieren, weil das zu langsam ist. Was können Sie also tun?


Einfach! Da Benutzer ein neues Lied in erster Linie sehen können, indem sie auf die Schaltfläche „Zufällig machen“ klicken, generieren wir viele Songs im Voraus, fügen sie in eine ConcurrentQueue ein und lassen „Songs zufällig“ Pop-Songs daraus erstellen. Während die Anzahl der Besucher gering ist, benötigt der Server einige Zeit, um einige Songs zu generieren, auf die dann leicht zugegriffen werden kann.


Ein weiterer Trick, den ich verwendet habe, besteht darin, diese Warteschlange mehrmals zu durchlaufen, sodass viele Benutzer denselben vorgenerierten Song sehen können. Man muss nur ein Gleichgewicht zwischen der RAM-Nutzung und der Häufigkeit, mit der ein Benutzer auf "Zufällig machen" klicken muss, um etwas zu sehen, das er zuvor gesehen hat. Ich habe einfach 50.000 Songs als vernünftige Anzahl ausgewählt, was nur 50 MB RAM beanspruchen würde, während ich eine ziemlich große Anzahl von Klicks zum Durchlaufen bereitstellen würde.


Ich habe diese Funktionalität in der Klasse PregeneratedSongProvider implementiert : IRandomSongProvider (die Schnittstelle wird in den Code eingefügt , der für die Behandlung der Schaltfläche "Zufällig machen" verantwortlich ist).


Caching


Vorgenerierte Songs werden im Speicher zwischengespeichert, aber ich habe auch den HTTP- Cache- Header auf public gesetzt , damit der Browser verwendet wird, und CDN (ich verwende CloudFlare) zwischenspeichere ihn, um nicht von einem Benutzereinstrom getroffen zu werden.


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

Rückgabe beliebter Songs


Die meisten Songs, die auf diese Weise von fein abgestimmtem GPT-2 erzeugt werden, sind ziemlich langweilig, wenn nicht rudimentär. Um die Klicks auf "Zufällig machen" ansprechender zu gestalten, habe ich eine Wahrscheinlichkeit von 25% hinzugefügt, dass anstelle eines völlig zufälligen Songs ein Song angezeigt wird, der zuvor von anderen Benutzern positiv bewertet wurde. Zusätzlich zur Erhöhung des Engagements erhöht sich die Wahrscheinlichkeit, dass Sie einen Titel anfordern, der entweder im CDN oder im Speicher zwischengespeichert ist.


Alle oben genannten Tricks sind mithilfe der ASP.NET-Abhängigkeitsinjektion in der Startup- Klasse miteinander verbunden.


Abstimmung


Die Umsetzung der Abstimmung ist nicht besonders. Es gibt SongVoteCache , der die Anzahl auf dem neuesten Stand hält. Und ein Iframe, der die Abstimmungsschaltfläche s auf der Song-Seite hostet, ermöglicht das Zwischenspeichern des wesentlichen Teils der Seite - Titel und Texte, während die Stimmenzahl und der Anmeldestatus später geladen werden.


Das Endergebnis


Generiertes Song-Sample


Eine Demoversion, die auf meinem Nettop ausgeführt wird und von CloudFlare (geben Sie etwas Ruhe, seinen Core i3) unterstützt wird, wurde jetzt eingefroren und in die kostenlose Azure App Service-Stufe verschoben.


Das GitHub-Repository mit Quellcode und Anweisungen zum Ausführen der Website und zum Optimieren des Modells.


Pläne für die Zukunft / Übungen


Titel generieren


GPT-2 ist sehr einfach zu optimieren. Man könnte es dazu bringen, Songtitel zu generieren, indem man jedem Textbeispiel aus dem Datensatz ein künstliches Token wie <| startoftitle |> voranstellt oder einfügt , gefolgt vom Titel aus demselben Datensatz.


Alternativ könnten Benutzer Titel vorschlagen und / oder für sie stimmen.


Musik generieren


Auf halbem Weg durch die Entwicklung von Billion Songs dachte ich, es wäre cool, eine Reihe von MIDI-Dateien herunterzuladen (das ist ein Musikformat der alten Schule, das dem Text viel näher kommt als MP3s) und GPT-2 darauf zu trainieren, um mehr zu generieren . In einige dieser Dateien war sogar Text eingebettet, sodass Sie Karaoke generieren konnten .


Ich weiß, dass die Musikgeneration auf diese Weise sehr gut möglich ist, da OpenAI gestern tatsächlich eine Implementierung dieser Idee in ihrem Blog veröffentlicht hat . Aber Hurra, sie haben kein Karaoke gemacht! Ich fand heraus, dass es möglich ist, http://www.midi-karaoke.info zu diesem Zweck abzukratzen.


Gradient aka TensorFlow für .NET


Updates finden Sie in unserem Blog .

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


All Articles