Erstellen Sie Ihren eigenen WhatsApp Text Generator (und erfahren Sie alles über Sprachmodelle)

Ein praktisches End-to-End-Beispiel für Deep Learning NLP

Ein normales Gespräch über WhatsApp, aber alles ist nicht so, wie es scheint. Die Leute sind real; Der Chat ist falsch. Es wurde von einem Sprachmodell generiert, das auf einer realen Gesprächsgeschichte trainiert wurde. In diesem Beitrag werde ich Sie durch die Schritte führen, um Ihre eigene Version mit der Kraft wiederkehrender neuronaler Netze zu erstellen und Lernen zu übertragen.

Bedarf

Ich habe die Fastai-Bibliothek in Googles kostenlosem Recherchetool für Datenwissenschaft, Colab, verwendet. Dies bedeutet sehr wenig Zeit (und kein Geld) für die Einrichtung. Alles, was Sie brauchen, um Ihr eigenes Modell zu erstellen, ist der in diesem Beitrag (auch hier) beschriebene Code und der folgende:

  • Ein Gerät für den Zugriff auf das Internet
  • Ein Google-Konto
  • WhatsApp-Chat-Historien

Ich diskutiere einige Theorien und vertiefe mich in einen Teil des Quellcodes, aber für weitere Einzelheiten gibt es verschiedene Links zu wissenschaftlichen Arbeiten und Dokumentationen. Wenn Sie mehr erfahren möchten, empfehle ich Ihnen dringend, sich den hervorragenden Fastai-Kurs anzusehen.

Drive und Colab beim erstmaligen Einrichten

Zunächst erstellen wir in Google Drive einen Speicherplatz für Ihr Notebook. Klicken Sie auf "Neu" und geben Sie dem Ordner einen Namen (ich habe "WhatsApp" verwendet).

Gehen Sie dann in Ihren neuen Ordner, klicken Sie erneut auf "Neu", öffnen Sie ein Colab-Notizbuch und geben Sie ihm einen geeigneten Namen.

Schließlich möchten wir eine GPU für das Notebook aktivieren. Dies wird den Trainings- und Texterzeugungsprozess erheblich beschleunigen (GPUs sind effizienter als CPUs für Matrixmultiplikationen, die Hauptberechnung unter der Haube in neuronalen Netzen).

Klicken Sie im oberen Menü auf "Laufzeit", dann auf "Laufzeittyp ändern" und wählen Sie "GPU" für den Hardwarebeschleuniger.

WhatsApp-Daten

Lassen Sie uns nun einige Daten erhalten. Je mehr desto besser, sollten Sie einen Chat mit einer relativ langen Geschichte auswählen. Erklären Sie außerdem allen anderen Beteiligten, was Sie tun, und holen Sie zuerst deren Erlaubnis ein.

Um den Chat herunterzuladen, klicken Sie auf Optionen (drei vertikale Punkte oben rechts), wählen Sie "Mehr", dann "Chat exportieren", "Ohne Medien" und wenn Sie Drive auf Ihrem Mobilgerät installiert haben, sollten Sie eine Option zum Speichern haben Ihren neu erstellten Ordner (andernfalls speichern Sie die Datei und fügen Sie sie manuell zum Laufwerk hinzu).

Datenaufbereitung

Zurück zum Notizbuch. Beginnen wir mit der Aktualisierung der Fastai-Bibliothek.

! curl -s https://course.fast.ai/setup/colab | Bash

Dann einige Standard-Magiebefehle und wir bringen drei Bibliotheken ein: fastai.text (für das Modell), pandas (für die Datenaufbereitung) und re (für reguläre Ausdrücke).

## magische Befehle% reload_ext autoreload% autoreload 2% matplotlib inline
## benötigte Pakete aus fastai.text importieren * Pandas als pd import re importieren

Wir möchten dieses Notizbuch mit Google Drive verknüpfen, um die soeben aus WhatsApp exportierten Daten zu verwenden und alle von uns erstellten Modelle zu speichern. Führen Sie dazu den folgenden Code aus, gehen Sie zum angegebenen Link, wählen Sie Ihr Google-Konto aus und kopieren Sie den Autorisierungscode zurück in Ihr Notizbuch.

## Colab Google Drive Zeug von google.colab importiert drive drive.mount ('/ content / gdrive', force_remount = True) root_dir = "/ content / gdrive / Mein Laufwerk /" base_dir = root_dir + 'whatsapp /'

Wir müssen noch etwas bereinigen, aber die Daten sind derzeit im TXT-Format. Nicht ideal. Hier ist eine Funktion, mit der Sie die Textdatei in einen Pandas-Datenrahmen mit einer Zeile für jeden Chat-Eintrag sowie einem Zeitstempel und dem Namen des Absenders konvertieren können.

## Funktion zum Analysieren der WhatsApp-Extraktdatei def parse_file (text_file): '' 'Konvertiert die WhatsApp-Chat-Protokolltextdatei in einen Pandas-Datenrahmen.' '' # einige Regex, um Nachrichten zu berücksichtigen, die mehrere Zeilen belegen pat = re.compile (r '^ (\ d \ d \ / \ d \ d \ / \ d \ d \ d \ d. *?) (? = ^^ \ d \ d \ / \ d \ d \ / \ d \ d \ d \ d | \ Z) ', re.S | re.M) mit open (text_file) als f: data = [m.group (1) .strip (). replace (' \ n ',' ') für m in pat.finditer (f.read ())]
Absender = []; message = []; datetime = [] für Zeile in Daten: # timestamp steht vor dem ersten Bindestrich datetime.append (row.split ('-') [0])
        # Absender ist zwischen am / pm, Bindestrich und Doppelpunkt try: s = re.search ('- (. *?):', Zeile) .group (1) sender.append (s) außer: sender.append ('' ) # Nachrichteninhalt ist nach dem ersten Doppelpunkt versuchen: message.append (row.split (':', 1) [1]) außer: message.append ('') df = pd.DataFrame (zip (Datum / Uhrzeit, Absender, message), column = ['timestamp', 'sender', 'text']) # Zeilen ausschließen, bei denen das Format nicht mit dem # richtigen Zeitstempelformat übereinstimmt df = df [df ['timestamp']. str.len () == 17] df ['timestamp'] = pd.to_datetime (df.timestamp, format = '% d /% m /% Y,% H:% M')
    # Ereignisse entfernen, die keinem Absender zugeordnet sind df = df [df.sender! = ''] .reset_index (drop = True) return df

Mal sehen, wie es funktioniert. Erstellen Sie den Pfad zu Ihren Daten, wenden Sie die Funktion auf den Chat-Export an und sehen Sie sich den resultierenden Datenrahmen an.

## Pfad zum Verzeichnis mit Ihrer Datei path = Pfad (base_dir)
## WhatsApp-Extrakt analysieren, chat.txt durch Ihren ## Extrakt-Dateinamen ersetzen df = parse_file (Pfad / 'chat.txt')
## schau dir das Ergebnis an df [205: 210]

Perfekt! Dies ist ein kleiner Ausschnitt aus Gesprächen zwischen mir und meiner schönen Frau. Einer der Vorteile dieses Formats ist, dass ich leicht eine Liste von Teilnehmernamen in Kleinbuchstaben erstellen kann, wobei Leerzeichen durch Unterstriche ersetzt werden. Dies wird später helfen.

## Liste der Teilnehmer an Konversationsteilnehmern = Liste (df ['Absender']. str.lower (). str.replace ('', '_'). unique ()) Teilnehmer

In diesem Fall gibt es nur zwei Namen, aber es funktioniert mit einer beliebigen Anzahl von Teilnehmern.

Schließlich müssen wir darüber nachdenken, wie dieser Text in unser Modellnetzwerk eingespeist werden soll. Normalerweise hätten wir mehrere eigenständige Textteile (z. B. Wikipedia-Artikel oder IMDB-Rezensionen), aber wir haben hier einen einzigen fortlaufenden Textstrom. Ein kontinuierliches Gespräch. Das schaffen wir. Eine lange Zeichenfolge, einschließlich der Absendernamen.

## verketten Sie Namen und Text zu einer Zeichenfolge text = [(df ['sender']. str.replace ('', '_') + '' + df ['text']). str.cat (sep = ' ')]
## Teil des Zeichenfolgentextes anzeigen [0] [8070: 8150]

Sieht gut aus. Wir sind bereit, dies in einen Lernenden zu bringen.

Lernerkreation

Um die Fastai-API verwenden zu können, müssen wir jetzt einen DataBunch erstellen. Dies ist ein Objekt, das dann in einem Lernenden zum Trainieren eines Modells verwendet werden kann. In diesem Fall verfügt es über drei Schlüsseleingaben: die Daten (aufgeteilt in Trainings- und Validierungssätze), Beschriftungen für die Daten und die Stapelgröße.

Daten

Um Training und Validierung aufzuteilen, wählen wir einfach einen Punkt in der Mitte unserer langen Konversationszeichenfolge aus. Ich habe die ersten 90% für das Training und die letzten 10% für die Validierung verwendet. Dann können wir einige TextList-Objekte erstellen und schnell überprüfen, ob sie wie zuvor aussehen.

## finde den Index für 90% durch die lange Zeichenfolge split = int (len (text [0]) * 0.9)
## Textlisten für Zug erstellen / gültig Zug = Textliste ([Text [0] [: Split]]) Valid = Textliste ([Text [0] [Split:]])
## kurzer Blick auf den Textzug [0] [8070: 8150]

Es lohnt sich, etwas tiefer in diese Textliste einzusteigen.

Es ist mehr oder weniger eine Liste von Text (in diesem Fall nur mit einem Element), aber schauen wir uns den Quellcode kurz an, um zu sehen, was noch da ist.

Ok, eine TextList ist eine Klasse mit einer Reihe von Methoden (Funktionen, alles oben, beginnend mit "def", alle minimiert). Es erbt von einer ItemList, mit anderen Worten, es ist eine Art ItemList. Suchen Sie auf jeden Fall eine ItemList, aber die Variable "_processor" interessiert mich am meisten. Der Prozessor ist eine Liste mit einem TokenizeProcessor und einem NumericalizeProcessor. Diese klingen in einem NLP-Kontext vertraut:

Tokenize - Text verarbeiten und in einzelne Wörter aufteilen

Numerisieren - Ersetzen Sie diese Token durch Zahlen, die der Position des Wortes in einem Vokabular entsprechen

Warum hebe ich das hervor? Nun, es ist sicherlich hilfreich, die Regeln zu verstehen, die zur Verarbeitung Ihres Textes verwendet werden, und das Durchsuchen dieses Teils des Quellcodes und der Dokumentation hilft Ihnen dabei. Insbesondere möchte ich jedoch meine eigene neue Regel hinzufügen. Ich denke, wir sollten zeigen, dass die Absendernamen im Text in gewisser Weise ähnlich sind. Idealerweise möchte ich vor jedem Absendernamen ein Token, das dem Modell mitteilt, dass dies ein Absendername ist.

Wie können wir das machen? Hier bietet sich _processor an. Die Dokumentation sagt uns, dass wir damit einen benutzerdefinierten Tokenizer übergeben können.

Wir können daher unsere eigene Regel erstellen und diese an einen benutzerdefinierten Prozessor übergeben. Ich möchte weiterhin die vorherigen Standardeinstellungen beibehalten. Alles, was ich tun muss, ist, meine neue Funktion zur vorhandenen Liste der Standardregeln hinzuzufügen und diese neue Liste unserem benutzerdefinierten Prozessor hinzuzufügen.

## neue Regel def add_spk (x: Sammlung [str]) -> Sammlung [str]: res = [] für t in x: wenn t in Teilnehmern: res.append ('xxspk'); res.append (t) else: res.append (t) gibt res zurück
## Neue Regel zu Standardwerten hinzufügen und Kundenprozessor übergeben custom_post_rules = defaults.text_post_rules + [add_spk] tokenizer = Tokenizer (post_rules = custom_post_rules)
processor = [TokenizeProcessor (Tokenizer = Tokenizer), NumericalizeProcessor (max_vocab = 30000)]

Die Funktion fügt vor jedem Namen das Token 'xxspk' hinzu.

Vor der Verarbeitung: „… Eier, Milch Paul_Solomon Ok…“

Nach der Verarbeitung: "... Eier, Milch xxspk paul_solomon xxmaj ok ..."

Beachten Sie, dass ich einige der anderen Standardregeln angewendet habe, nämlich das Identifizieren von großgeschriebenen Wörtern (fügt 'xxmaj' vor großgeschriebenen Wörtern hinzu) und das Trennen von Interpunktion.

Etiketten

Wir werden ein sogenanntes Sprachmodell erstellen. Was ist das? Einfach, es ist ein Modell, das das nächste Wort in einer Folge von Wörtern vorhersagt. Um dies genau zu tun, muss das Modell die Sprachregeln und den Kontext verstehen. In gewisser Weise muss es die Sprache lernen.

Also, was ist das Label? Einfach, es ist das nächste Wort. Insbesondere können wir in der von uns verwendeten Modellarchitektur für eine Folge von Wörtern eine Zielsequenz erstellen, indem wir dieselbe Folge von Token nehmen und um ein Wort nach rechts verschieben. An jedem Punkt in der Eingabesequenz können wir denselben Punkt in der Zielsequenz betrachten und das richtige vorherzusagende Wort finden (dh die Bezeichnung).

Eingabesequenz: „… Eier, Milch spkxx paul_solomon xxmaj…“

Label / Nächstes Wort: "ok"

Zielsequenz: “…, Milch spkxx paul_solomon xxmaj ok…”

Dazu verwenden wir die Methode label_for_lm (eine der Funktionen in der obigen TextList-Klasse).

## Zug nehmen und gültig und Bezeichnung für Sprachmodell src = ItemLists (Pfad = Pfad, Zug = Zug, gültig = gültig) .label_for_lm ()

Batchsize

Neuronale Netze werden trainiert, indem Datenstapel parallel übergeben werden. Die endgültige Eingabe für den Datenbündel ist also unsere Stapelgröße. Wir verwenden 48, was bedeutet, dass 48 Textsequenzen gleichzeitig durch das Netzwerk geleitet werden. Jede dieser Textsequenzen ist standardmäßig 70 Token lang.

## Datenbunch mit einer Stapelgröße von 48 bs = 48 data = src.databunch (bs = bs) erstellen

Wir haben jetzt unsere Daten! Lassen Sie uns den Lernenden erstellen.

## create learner learn = language_model_learner (Daten, AWD_LSTM, drop_mult = 0.3)

Fastai bietet uns die Möglichkeit, schnell einen Lernenden für ein Sprachmodell zu erstellen. Wir brauchen nur unsere Daten (wir haben sie bereits) und ein vorhandenes Modell. Für dieses Objekt ist das Argument 'pretrained' standardmäßig auf 'True' gesetzt. Dies bedeutet, dass wir ein vorab trainiertes Sprachmodell nehmen und es an unsere Daten anpassen.

Dies nennt man Transferlernen, und ich liebe es. Sprachmodelle benötigen viele Daten, um gut zu funktionieren, aber in diesem Fall haben wir nicht annähernd genug. Um dieses Problem zu lösen, können wir ein vorhandenes Modell, das auf großen Datenmengen trainiert ist, an unseren Text anpassen.

In diesem Fall verwenden wir ein AWD_LSTM-Modell, das im WikiText-103-Dataset vorab trainiert wurde. AWD LSTM ist ein Sprachmodell, das eine Art Architektur verwendet, die als wiederkehrendes neuronales Netzwerk bezeichnet wird. Es wurde auf Text trainiert, und in diesem Fall wurde es auf einer ganzen Menge von Wikipedia-Daten trainiert. Wir können nachsehen, wie viel.

Dieses Modell wurde auf über 100 m Token aus 28.000 Wikipedia-Artikeln mit modernster Leistung trainiert. Klingt nach einem guten Ausgangspunkt für uns!

Lassen Sie uns einen kurzen Eindruck von der Modellarchitektur bekommen.

learn.model

Ich werde das aufschlüsseln.

  1. Encoder - Das Vokabular für unseren Text enthält jedes Wort, das mehr als zweimal verwendet wurde. In diesem Fall sind das 2.864 Wörter (Ihre werden anders sein). Jedes dieser Wörter wird unter Verwendung eines Vektors der Länge 2.864 mit einer 1 an der entsprechenden Position und allen Nullen an anderer Stelle dargestellt. Die Codierung nimmt diesen Vektor und multipliziert ihn mit einer Gewichtsmatrix, um ihn in eine Einbettung mit einer Länge von 400 Wörtern zu zerquetschen.
  2. LSTM-Zellen - Die Einbettung mit einer Länge von 400 Wörtern wird dann in eine LSTM-Zelle eingespeist. Ich werde nicht auf die Details der Zelle eingehen. Alles, was Sie wissen müssen, ist, dass ein Vektor mit einer Länge von 400 in die erste Zelle eingeht und ein Vektor mit einer Länge von 1.152 herauskommt. Zwei weitere erwähnenswerte Dinge: Diese Zelle hat einen Speicher (sie erinnert sich an frühere Wörter) und die Ausgabe der Zelle wird in sich selbst zurückgeführt und mit dem nächsten Wort (das ist der wiederkehrende Teil) kombiniert sowie in die nächste Ebene verschoben . Es gibt drei dieser Zellen in einer Reihe.
  3. Decoder - Die Ausgabe der dritten LSTM-Zelle ist ein Vektor mit einer Länge von 400, der erneut zu einem Vektor mit der gleichen Länge wie Ihr Vokabular erweitert wird (in meinem Fall 2.864). Dies gibt uns die Vorhersage für das nächste Wort und kann mit dem tatsächlichen nächsten Wort verglichen werden, um unseren Verlust und unsere Genauigkeit zu berechnen.

Denken Sie daran, dass dies ein vorab trainiertes Modell ist. Daher sind die Gewichte nach Möglichkeit genau so, wie sie mit den WikiText-Daten trainiert wurden. Dies gilt für die LSTM-Zellen und für alle Wörter, die in beiden Vokabeln enthalten sind. Alle neuen Wörter werden durch den Mittelwert aller Einbettungen initialisiert.

Lassen Sie es uns nun mit unseren eigenen Daten optimieren, sodass der generierte Text wie unser WhatsApp-Chat und nicht wie ein Wikipedia-Artikel klingt.

Ausbildung

Zuerst werden wir ein gefrorenes Training machen. Dies bedeutet, dass wir nur bestimmte Teile des Modells aktualisieren. Insbesondere werden wir nur die letzte Ebenengruppe trainieren. Wir können oben sehen, dass die letzte Schichtgruppe "(1): LinearDecoder" ist, der Decoder. Alle Worteinbettungen und LSTM-Zellen bleiben während des Trainings gleich. Es ist nur die letzte Dekodierungsstufe, die aktualisiert wird.

Einer der wichtigsten Hyperparameter ist die Lernrate. Fastai gibt uns ein nützliches kleines Werkzeug, um schnell einen guten Wert zu finden.

## run lr finder learn.lr_find ()
## plot lr finder learn.recorder.plot (skip_end = 15)

Als Faustregel gilt, den steilsten Teil der Kurve zu finden (dh den Punkt des schnellsten Lernens). 1.3e-2 scheint ungefähr hier zu sein.

Lassen Sie uns fortfahren und für eine Epoche trainieren (einmal alle Trainingsdaten durchgehen).

## trainiere für eine Epoche eingefroren learn.fit_one_cycle (1, 1e-2, moms = (0.8,0.7))

Am Ende der Epoche können wir den Verlust der Trainings- und Validierungssätze und die Genauigkeit der Validierungssätze sehen. Wir sagen 41% der nächsten Wörter im Validierungssatz korrekt voraus. Nicht schlecht.

Gefrorenes Training ist eine gute Möglichkeit, um mit dem Transferlernen zu beginnen, aber jetzt können wir das gesamte Modell durch Auftauen öffnen. Dies bedeutet, dass die Encoder- und LSTM-Zellen jetzt in unseren Trainingsupdates enthalten sind. Dies bedeutet auch, dass das Modell empfindlicher ist, sodass wir unsere Lernrate auf 1e-3 reduzieren.

## trainiere für vier weitere Zyklen ungefroren learn.fit_one_cycle (4, 1e-3, moms = (0.8,0.7))

Genauigkeit bis zu 44,4%. Beachten Sie, dass der Trainingsverlust jetzt geringer ist als die Validierung. Das möchten wir sehen, und der Validierungsverlust hat seinen Tiefpunkt erreicht.

Beachten Sie, dass Sie mit ziemlicher Sicherheit feststellen werden, dass sich Ihr Verlust und Ihre Genauigkeit von den oben genannten unterscheiden (einige Gespräche sind vorhersehbarer als andere). Ich empfehle Ihnen daher, mit den Parametern (Lernraten, Trainingsprotokoll usw.) herumzuspielen, um zu versuchen, die zu erhalten beste Leistung von Ihrem Modell.

Texterzeugung

Wir haben jetzt ein Sprachmodell, das genau auf Ihre WhatsApp-Konversation abgestimmt ist. Um Text zu generieren, müssen wir ihn nur zum Laufen bringen und das nächste Wort so lange vorhersagen, wie Sie es verlangen.

Fastai bietet uns eine nützliche Vorhersagemethode, um genau dies zu tun. Alles, was wir tun müssen, ist, ihm einen Text zu geben, um ihn zu starten, und ihm mitzuteilen, wie lange er laufen soll. Die Ausgabe wird weiterhin im Token-Format vorliegen, daher habe ich eine Funktion geschrieben, um den Text zu bereinigen und ihn schön im Notizbuch auszudrucken.

## Funktion zum Generieren von Text def generate_chat (start_text, n_words): text = learn.predict (start_text, n_words, Temperatur = 0,75) text = text.replace ("xxspk", "\ n"). replace ("\ '" , "\ '"). replace ("n \' t", "n \ 't") text = re (r' \ s ([?.! "] (?: \ s | $)) ' , r '\ 1', Text) für Teilnehmer an Teilnehmern: text = text.replace (Teilnehmer, Teilnehmer + ":") print (Text)

Lass uns weitermachen und loslegen.

## generiere einen Text mit einer Länge von 200 Wörtern generate_chat (Teilnehmer [0] + "Bist du in Ordnung?", 200)

Nett! Es liest sich sicherlich wie eine meiner Konversationen (die sich hauptsächlich darauf konzentrieren, jeden Tag nach der Arbeit nach Hause zu reisen), der Kontext bleibt erhalten (das ist der LSTM-Speicher bei der Arbeit), und der Text scheint sogar auf jeden Teilnehmer zugeschnitten zu sein.

Abschließende Gedanken

Ich werde mit einem Wort der Vorsicht beenden. Wie bei vielen anderen KI-Anwendungen kann die Erzeugung von gefälschtem Text für unethische Zwecke in großem Maßstab verwendet werden (z. B. zum Verbreiten von Nachrichten, die im Internet Schaden anrichten sollen). Ich habe es hier verwendet, um eine unterhaltsame und praktische Möglichkeit zum Erlernen von Sprachmodellen zu bieten. Ich möchte Sie jedoch dazu ermutigen, darüber nachzudenken, wie die oben beschriebenen Methoden für andere Zwecke verwendet werden können (z. B. als Eingabe für ein Textklassifizierungssystem). oder zu anderen Arten von sequenzähnlichen Daten (z. B. Musikkomposition).

Dies ist ein aufregendes und schnelllebiges Feld mit dem Potenzial, leistungsstarke Werkzeuge zu entwickeln, die Werte schaffen und der Gesellschaft zugute kommen. Ich hoffe, dieser Beitrag zeigt Ihnen, dass sich jeder beteiligen kann.