Bygg din egen WhatsApp textgenerator (och lära dig allt om språkmodeller)

Ett praktiskt exemplar från Deep Learning NLP

En normal konversation på WhatsApp, men allt är inte som det verkar. Folket är verkligt; chatten är falsk. Det genererades av en språkmodell tränad i en riktig konversationshistoria. I det här inlägget tar jag dig igenom stegen för att bygga din egen version med kraften i återkommande neurala nätverk och överföra lärande.

Krav

Jag har använt fastai-biblioteket i Googles gratis forskningsverktyg för datavetenskap, Colab. Detta innebär väldigt lite tid (och inga pengar) på att komma igång. Allt du behöver för att bygga din egen modell är koden som anges i det här inlägget (även här) och följande:

  • En enhet för åtkomst till internet
  • Ett Google-konto
  • WhatsApp chatthistorik

Jag diskuterar lite teori och fördjupar en del av källkoden, men för mer detaljer finns det olika länkar till akademiska artiklar och dokumentation. Om du vill lära dig mer rekommenderar jag starkt att du tittar på den utmärkta fastaikursen.

Först startade Drive och Colab

Först ska vi skapa ett utrymme på Google Drive för din anteckningsbok. Klicka på "Ny" och ge mappen ett namn (jag använde "whatsapp").

Gå sedan in i din nya mapp, klicka på "ny" igen, öppna upp en Colab-anteckningsbok och ge den ett lämpligt namn.

Slutligen vill vi aktivera en GPU för den bärbara datorn. Detta kommer att påskynda processen för utbildning och textgenerering avsevärt (GPU: er är mer effektiva än CPU: er för matrismultiplikationer, den viktigaste beräkningen under huven i nervnätverk).

Klicka på "Runtime" från toppmenyn, sedan "Change runtime type" och välj "GPU" för maskinvaruacceleratorn.

WhatsApp-data

Låt oss nu få lite information. Ju mer desto bättre, så du vill välja en chatt med en ganska lång historia. Förklara också vad du gör till någon annan som är involverad i konversationen och få deras tillstånd först.

För att ladda ner chatten, klicka på alternativ (tre vertikala punkter längst upp till höger), välj "Mer", sedan "Exportera chatt", "Utan media" och om du har Drive installerat på din mobila enhet bör du ha ett alternativ att spara till din nyskapade mapp (annars spara filen och lägg till manuellt till Drive).

Förberedelse av data

Tillbaka till anteckningsboken. Låt oss börja med att uppdatera fastaibiblioteket.

! curl -s https://course.fast.ai/setup/colab | våldsamt slag

Sedan några vanliga magiska kommandon och vi tar in tre bibliotek: fastai.text (för modellen), pandaer (för dataförberedelse) och re (för vanliga uttryck).

## magiska kommandon% reload_ext autoreload% autoreload 2% matplotlib inline
## importera obligatoriska paket från fastai.text import * importera pandor som pd-import re

Vi vill länka den här anteckningsboken till Google Drive för att använda de data vi just exporterade från WhatsApp och spara alla modeller vi skapar. Kör följande kod genom att gå till den medföljande länken, välj ditt Google-konto och kopiera behörighetskoden tillbaka till din anteckningsbok.

## Colab google drive stuff från google.colab import drive drive.mount ('/ content / gdrive', force_remount = True) root_dir = "/ content / gdrive / My Drive /" base_dir = root_dir + 'whatsapp /'

Vi har lite rengöring att göra, men uppgifterna är för närvarande i .txt-format. Inte perfekt. Så här är en funktion för att ta textfilen och konvertera den till en pandas dataframe med en rad för varje chattpost, tillsammans med en tidsstämpel och avsändarens namn.

## funktion för att analysera whatsapp extrahera filen def parse_file (text_file): '' 'Konvertera WhatsApp chattlogg textfil till en Pandas dataframe.' '' # några regex för att ta hänsyn till meddelanden som tar upp flera rader 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) med open (text_fil) som f: data = [m.group (1) .strip (). ersätt (' \ n ',' ') för m i pat.finditer (f.read ())]
avsändare = []; meddelande = []; datetime = [] för rad i data: # tidsstämpel är före den första strecken datetime.append (row.split ('-') [0])
        # avsändare är mellan am / pm, streck och kolon försök: s = re.search ('- (. *?):', rad) .group (1) avsändare.append (s) förutom: avsändare.append ('' ) # meddelandeinnehållet är efter det första kolonförsöket: message.append (row.split (':', 1) [1]) förutom: message.append ('') df = pd.DataFrame (zip (datetime, avsändare, meddelande), kolumner = ['tidsstämpel', 'avsändare', 'text']) # utesluter alla rader där formatet inte stämmer med # rätt tidsstämpelformat df = df [df ['tidsstämpel']. str.len () == 17] df ['timestamp'] = pd.to_datetime (df.timestamp, format = '% d /% m /% Y,% H:% M')
    # ta bort händelser som inte är associerade med en avsändare df = df [df.sender! = ''] .reset_index (drop = True) returnera df

Låt oss se hur det fungerar. Skapa sökvägen till dina data, tillämpa funktionen på chattexporten och titta på det resulterande dataframe.

## sökväg till katalog med din filväg = sökväg (base_dir)
## parse whatsapp extrakt, ersätt chat.txt med ditt ## extrakt filnamn df = parse_file (sökväg / 'chat.txt')
## titta på resultatet df [205: 210]

Perfekt! Det här är ett litet samtalstycke mellan mig och min härliga fru. En av fördelarna med detta format är att jag enkelt kan skapa en lista med deltagarnamn med små bokstäver och ersätta eventuella mellanslag med understreck. Detta hjälper senare.

## lista över deltagare för konversationsdeltagare = lista (df ['avsändare']. str.lower (). str.replace ('', '_'). unik ()) deltagare

I det här fallet finns det bara två namn, men det fungerar med valfritt antal deltagare.

Slutligen måste vi fundera över hur vi vill att den här texten ska matas in i vårt modellnätverk. Normalt skulle vi ha flera fristående textbitar (t.ex. wikipedia-artiklar eller IMDB-recensioner), men det vi har här är en enda pågående ström av text. En kontinuerlig konversation. Det är vad vi skapar. En lång sträng, inklusive avsändarens namn.

## sammanfoga namn och text i en strängtext = [(df ['avsändare']. str.replace ('', '_') + '' + df ['text']). str.cat (sep = ' ')]
## visa en del av strängtexten [0] [8070: 8150]

Ser bra ut. Vi är redo att få detta till en elev.

Lärande skapelse

För att använda fastai API måste vi nu skapa en DataBunch. Detta är ett objekt som sedan kan användas i en elev för att träna en modell. I det här fallet har den tre viktiga ingångar: data (delat i tränings- och valideringsuppsättningar), etiketter för data och batchstorlek.

Data

För att dela utbildning och validering låt oss bara välja en punkt någonstans mitt i vår långa konversationssträng. Jag gick för de första 90% för utbildning, sista 10% för validering. Då kan vi skapa ett par TextList-objekt och snabbt kontrollera att de ser lika ut som tidigare.

## hitta indexet för 90% genom den långa strängdelningen = int (len (text [0]) * 0,9)
## skapa TextLists for train / valid train = TextList ([text [0] [: split]]) valid = TextList ([text [0] [split:]])
## snabb titt på texttåget [0] [8070: 8150]

Det är värt att gå lite djupare på denna textlista.

Det är mer eller mindre en lista med text (med bara ett element i det här fallet), men låt oss titta snabbt på källkoden för att se vad som finns där.

Okej, en TextList är en klass med ett gäng metoder (funktioner, allt i ovanstående börjar med ”def”, allt minimerat). Det ärver från en ItemList, med andra ord är det en typ av ItemList. Gå på alla sätt och leta upp en artikellista, men jag är mest intresserad av variabeln "_processor". Processorn är en lista med en TokenizeProcessor och en NumericalizeProcessor. Dessa låter bekant i ett NLP-sammanhang:

Tokenize - bearbeta text och dela upp den i sina enskilda ord

Numericalize - ersätt dessa symboler med siffror som motsvarar ordets position i en vokab

Varför markerar jag detta? Det hjälper verkligen att förstå reglerna som används för att bearbeta din text, och att gräva in den här delen av källkoden och dokumentationen hjälper dig att göra det. Men specifikt vill jag lägga till min egen nya regel. Jag anser att vi borde visa att avsändarens namn i texten liknar på något sätt. Helst skulle jag vilja ha ett symbol före varje avsändarnamn som säger modellen "detta är ett avsändarnamn".

Hur kan vi göra detta? Det är där _processor är praktiskt. Dokumentationen säger att vi kan använda den för att skicka in en anpassad tokenizer.

Vi kan därför skapa vår egen regel och skicka den med en anpassad processor. Jag vill fortfarande behålla de tidigare standardvärdena, så allt jag behöver göra är att lägga till min nya funktion i den befintliga listan med standardregler och lägga till den nya listan i vår anpassade processor.

## ny regel def add_spk (x: Collection [str]) -> Collection [str]: res = [] för t i x: om t i deltagarna: res.append ('xxspk'); res.append (t) annars: res.append (t) return res
## lägg till en ny regel i standardinställningarna och skicka in kundprocessorn custom_post_rules = defaults.text_post_rules + [add_spk] tokenizer = Tokenizer (post_rules = custom_post_rules)
processor = [TokenizeProcessor (tokenizer = tokenizer), NumericalizeProcessor (max_vocab = 30000)]

Funktionen lägger till token 'xxspk' före varje namn.

Innan bearbetning: "... ägg, mjölk Paul_Solomon Ok ..."

Efter bearbetning: "... ägg, mjölk xxspk paul_solomon xxmaj ok ..."

Observera att jag har använt några av de andra standardreglerna, nämligen att identifiera stora bokstäver (lägger till 'xxmaj' före stora bokstäver) och separera skiljetecken.

Etiketter

Vi kommer att skapa något som kallas en språkmodell. Vad är detta? Enkelt, det är en modell som förutsäger nästa ord i en ordningsföljd. För att göra detta exakt måste modellen förstå språkregler och sammanhang. På vissa sätt måste det lära sig språket.

Så vad är etiketten? Enkelt, det är nästa ord. Mer specifikt, i den modellarkitektur som vi använder, för en ordrekkeföljd kan vi skapa en målsekvens genom att ta samma sekvens av tokens och flytta det ett ord till höger. När som helst i ingångssekvensen kan vi titta på samma punkt i målsekvensen och hitta rätt ord att förutsäga (dvs. etiketten).

Ingångssekvens: “... ägg, mjölk spkxx paul_solomon xxmaj…”

Etikett / nästa ord: “ok”

Målsekvens: “…, mjölk spkxx paul_solomon xxmaj ok…”

Vi gör detta genom att använda metoden label_for_lm (en av funktionerna i klassen TextList ovan).

## ta tåg och giltig och etikett för språkmodell src = ItemLists (sökväg, sökväg, tåg = tåg, giltig = giltig) .label_for_lm ()

Satsstorlek

Neurala nätverk tränas genom att mata in partier med data parallellt, så den sista inmatningen för databunch är vår batchstorlek. Vi använder 48, vilket innebär att 48 textsekvenser skickas genom nätverket åt gången. Var och en av dessa textsekvenser är 70 tokens långa som standard.

## skapa databunch med batchstorlek 48 bs = 48 data = src.databunch (bs = bs)

Vi har nu våra uppgifter! Låt oss skapa eleven.

## skapa elevlära = language_model_learner (data, AWD_LSTM, drop_mult = 0.3)

Fastai ger oss ett alternativ att snabbt skapa en språkmodellelever. Allt vi behöver är våra uppgifter (vi har det redan) och en befintlig modell. Detta objekt har ett argument som "förutbestämd" som standard. Det betyder att vi kommer att ta en förutbildad språkmodell och finjustera den till våra data.

Detta kallas transfer learning, och jag älskar det. Språkmodeller behöver mycket data för att fungera bra, men vi har inte någonstans nära nog i det här fallet. För att lösa detta problem kan vi ta en befintlig modell, tränad på enorma mängder data och finjustera den till vår text.

I det här fallet använder vi en AWD_LSTM-modell som har utbildats i WikiText-103-datasättet. AWD LSTM är en språkmodell som använder en typ av arkitektur som kallas ett återkommande neuralt nätverk. Det är tränat på text, och i det här fallet har det tränats på en hel mängd wikipedia-data. Vi kan slå upp hur mycket.

Denna modell har tränats på över 100 m tokens från 28k Wikipedia-artiklar med modernaste prestanda. Låter som en bra utgångspunkt för oss!

Låt oss få en snabb känsla av modellarkitekturen.

learn.model

Jag bryter ner det här.

  1. Kodare - Vokaben för vår text kommer att ha alla ord som har använts mer än två gånger. I det här fallet är det 2 864 ord (ditt kommer att vara annorlunda). Var och en av dessa ord representeras med en vektor med längden 2 864 med en 1 i lämplig position och alla nollor någon annanstans. Kodning tar denna vektor och multiplicerar den med en viktmatris för att klämma ner den till en längd på 400 ord inbäddning.
  2. LSTM-celler - Inbäddningslängden på 400 ord matas sedan in i en LSTM-cell. Jag kommer inte gå in i detalj i cellen, allt du behöver veta är att en längd på 400 vektorn går in till den första cellen, och en längd på 1.152 vektorn kommer ut. Två andra saker som är värda att notera: den här cellen har ett minne (det kommer ihåg tidigare ord) och utmatningen från cellen matas tillbaka in i sig själv och kombineras med nästa ord (det är den återkommande delen), samt skjuts in i nästa lager . Det finns tre av dessa celler i rad.
  3. Avkodare - Utgången från den tredje LSTM-cellen är en längd på 400 vektor, denna utvidgas igen till en vektor med samma längd som din vokab (2.864 i mitt fall). Detta ger oss förutsägelsen för nästa ord och kan jämföras med det faktiska nästa ordet för att beräkna vår förlust och noggrannhet.

Kom ihåg att detta är en förutbildad modell, så där möjligt är vikterna exakt som tränades med WikiText-data. Detta kommer att vara fallet för LSTM-cellerna, och för alla ord som finns i båda vokaberna. Alla nya ord initialiseras med medelvärdet av alla inbäddningar.

Låt oss nu finjustera det med våra egna data så att texten som den genererar låter som vår WhatsApp-chatt och inte en Wikipedia-artikel.

Träning

Först ska vi göra frusen träning. Det betyder att vi bara uppdaterar vissa delar av modellen. Specifikt kommer vi bara att träna den sista lagergruppen. Vi kan se ovan att den sista lagergruppen är “(1): LinearDecoder”, avkodaren. Alla ordinbäddningar och LSTM-celler förblir desamma under träning, det är bara det sista avkodningssteget som kommer att uppdateras.

En av de viktigaste hyperparametrarna är inlärningshastigheten. Fastai ger oss ett användbart litet verktyg för att snabbt hitta ett bra värde.

## kör lr finder learning.lr_find ()
## plot lr finder learning.recorder.plot (skip_end = 15)

Tumregeln är att hitta den brantaste delen av kurvan (dvs. punkten för snabbast inlärning). 1.3e-2 ser ut att handla just här.

Låt oss gå vidare och träna för en epok (en gång genom alla träningsdata).

## tåg för en epokfryst lär.fit_one_cykel (1, 1e-2, mammor = (0,8,0,7))

I slutet av epoken kan vi se förlusten på tränings- och valideringsuppsättningarna och noggrannheten på valideringsuppsättningen. Vi förutspår korrekt 41% av nästa ord i valideringsuppsättningen. Inte dåligt.

Fryst utbildning är ett utmärkt sätt att börja med överföringsinlärning, men nu kan vi öppna upp hela modellen genom att frigöra. Detta betyder att kodaren och LSTM-cellerna nu kommer att inkluderas i våra träningsuppdateringar. Det betyder också att modellen kommer att vara mer känslig, så vi minskar vår inlärningshastighet till 1e-3.

## tåg i ytterligare fyra cykler ofrossa learning.fit_one_cykel (4, 1e-3, mammor = (0,8,0,7))

Noggrannhet upp till 44,4%. Observera att träningsförlusten nu är lägre än valideringen, det är vad vi vill se och valideringsförlusten har bottnat.

Observera att du nästan säkert kommer att upptäcka att din förlust och noggrannhet skiljer sig från ovan (vissa samtal är mer förutsägbara än andra) så jag föreslår att du spelar med parametrarna (inlärningshastigheter, träningsprotokoll etc.) för att försöka bästa prestanda från din modell.

Textgenerering

Vi har nu en språkmodell, finjusterad till din WhatsApp-konversation. För att generera text allt vi behöver göra är att ställa den i gång och det börjar förutsäga nästa ord om och om igen så länge du ber det.

Fastai ger oss en användbar förutsägningsmetod för att göra exakt detta, allt vi behöver göra är att ge den lite text för att komma igång och berätta hur länge vi ska köra. Outputen kommer fortfarande att vara i tokeniserat format, så jag skrev en funktion för att rensa upp texten och skriva ut den snyggt i anteckningsboken.

## funktion för att generera text def generera_chat (start_text, n_words): text = learning.predict (start_text, n_words, temperatur = 0.75) text = text.replace ("xxspk", "\ n"). ersätt ("\ '" , "\ '"). ersätt ("n \' t", "n \ 't") text = re.sub (r' \ s ([?.! "] (?: \ s | $)) ' , r '\ 1', text) för deltagare i deltagare: text = text.replace (deltagare, deltagare + ":") tryck (text)

Låt oss gå vidare och börja med det.

## generera text med längd 200 ord generera_chat (deltagare [0] + "är du ok?", 200)

Trevlig! Det läser verkligen som en av mina samtal (i hög grad fokuserad på att resa hem efter arbete varje dag), sammanhanget upprätthålls (det är LSTM-minnet på jobbet), och texten ser till och med ut att vara skräddarsydd för varje deltagare.

Slutgiltiga tankar

Jag avslutar med ett ord av försiktighet. Som med många andra AI-applikationer kan falsk textgenerering användas i skala för oetiska ändamål (t.ex. sprida meddelanden som är utformade för att skada på internet). Jag har använt den här för att ge ett roligt och praktiskt sätt att lära sig om språkmodeller, men jag uppmuntrar dig att tänka på hur metoderna som beskrivs ovan kan användas för andra ändamål (t.ex. som en inmatning till ett textklassificeringssystem) eller till andra typer av sekvensliknande data (t.ex. musikkomposition).

Detta är ett spännande och snabbt rörande fält med potential att bygga kraftfulla verktyg som skapar värde och gynnar samhället. Jag hoppas att det här inlägget visar att vem som helst kan engagera sig.