Att skapa AI genom maskininlärning

Experiment med maskininlärning

Många stora företag har gett sig in på AI; Google byter från Mobile-first till AI-first, media skriver mycket om AI och nya genombrott där exempelvis datorer lär sig spela enkla TV-spel på egen hand genom att testa sig fram. Men vad är egentligen AI och hur fungerar det?

AI och maskininlärning

AI, artificiell intelligens, är ett brett begrepp som innefattar flera områden, bland annat neuronnät, expertsystem och språkbearbetning. I media används det ofta för ett område som kallas maskininlärning där maskiner bygger statistiska modeller av verkligheten genom att gå igenom stora mängder data. Dessa modeller är specialiserade för att lösa ett specifikt problem och inte det som jag personligen skulle kalla ’intelligens’. Det är detta område som jag tänkte skriva om och göra ett litet experiment.

Typer av neurala nätverk

I maskininlärning tränar man ofta olika typer av neurala nätverk för att kunna göra förutsägelser och hitta mönster från sitt data. Vad är då ett neuralt nätverk?

Tidigt i AIs historia försökte man efterhärma den mänskliga hjärnan med celler som kommunicerade med varandra. Detta ledde fram till matematiska modeller där man med matrisalgebra kan beräkna resultatet från indatat genom enkel multiplikation och addition. Problemet består då i hur man beräknar vikterna man multiplicerar med och adderar. Det vanligaste sättet att beräkna det är att börjar med slumpmässiga värden och se hur fel det blir. Felet används sedan för att först ändra vikterna och sedan till slut närma sig en lösning där felet blir tillräckligt litet.

För enkla modeller var detta inget problem. Men när man hade många lager av ’celler’, vilket är ett måste för mer komplicerade modeller, så ökade felet man beräknade med snabbt för varje lager. Det är ett område man kommit inom långt de senaste 10-15 åren och man har möjliggjort det man kallar deep learning. Klassificering av innehåll i bilder är ett av de problem man lyckats bra med tack var utvecklingen av nya metoder.

RNN

RNN, Recurrent Neural Network, är en form av nätverk där man återanvänder tidigare signaler för att dra nytta av sekventiell information. Det kan exempelvis handla om ljudsignaler där man behöver ta hänsyn till flera tidpunkter i data för att se sammanhang eller för ord i meningar; ett ord för sig säger inte så mycket utan sitt sammanhang i meningen. En variant av RNN är LSTM som står för Long Short-Term Memory. Detta nätverk bygger på celler som kan komma ihåg och glömma tidigare tillstånd och har förmågan att hitta samband med ett godtyckligt tidsintervall vilket gjort att det populärt att använda för taligenkänning och språktolkning.

Träning

För att träna upp en modell så krävs mängder med data om man vill få ut något av värde. En del statistiska modeller kan automatiskt separera upp data i olika kluster och se sammanhang och mönster, men för många former av neuronnät krävs att ha data med exempel på input och förväntat resultat. För detta ändamål har det skapats öppet data med samlingar av exempelvis bilder på olika saker med tillhörande etikett för klassificering, och många andra varianter. Ett vanligt förkommande exempel är MNIST, vilket består av handskrivna siffror och som används flitigt i exempelprogram för implementationer.

För att träna ett nätverk och få det att fungera för data utanför sin exempelsamling använder man en del av data för själva träningen och en annan del för att utvärdera resultatet av träningen. Det är viktigt att en separat mängd används för utvärderingen. Om man tränar upp modellen på samma mängd som man mäter resultatet med, så kommer man få en modell som är väldigt bra på exakt det data som den har sett, men å andra sidan vara väldigt dålig på nytt okänt data. Detta kallas överträning eller ’overfitting’.

En annan svårighet kan vara att data inte representerar verkligheten tillräckligt bra och således drar felaktiga slutsatser. Forskning har visat att det ibland leder till att fördomar till och med förstärks i modellerna.

Ett experiment

Det finns många verktyg på marknaden för maskinlärning, för olika plattformar och språk. Denna gång tänkte jag titta närmare på Tensorflow från Google.

Mitt experiment går ut på att skapa en modell som föreslår nästa ord givet ett antal ord. En form av ’auto suggest’ som man till exempel kan hitta i tangentbordet i mobilen. Tensorflow har både låg- och högnivå API:er och finns i språken Python, C++ och Java. Här har jag valt att köra det i Python. Som datakälla har jag valt boken Nils Holgerssons resa som kan laddas ner från http://runeberg.org/nilsholg/. Nu vet jag att denna datamängd inte räcker för att representera det svenska språket på ett tillräckligt bra sätt, men det får duga för ett experiment. Texten består av ca 232 000 ord.

Min idé är att låta varje ord vara ett värde; exempelvis en mening som ”Det var en gång” och ordlistan ”Det” = 43, ”var” = 652, ”en” = 12 och ”gång” = 1002 skulle ge sekvensen 43, 652, 12, 1002 som data. Utdata blir en sannolikhet för varje ord i vokabulären som nästa ord. För att ge ett förslag på nästa ord väljer jag att ta det mest sannolika ordet varje gång. Det går här att ge topp 5-orden om man vill på ett enkelt sätt. Mitt nätverk kommer bestå av några lager med LSTM-nät och för att transformera de diskreta talen till vektorer av reella tal kommer jag använda något som kallas Embeddings. Detta gör att jag lätt kan välja dimension på nätverket för att anpassa inlärningen.

Varning! Nedanför kommer kod och tekniskt mumbo-jumbo.

Man kan scrolla ner till slutet och se resultatet om det är intressant…

Tensorflow

Tensorflow är baserat på att man bygger upp en graf för hur exekveringen ska utföras. I denna graf definierar man variabler, operationer samt behållare för riktiga värden. När grafen är byggd så kan denna köras i en session där de riktiga beräkningarna kommer att ske. Tensorflow är byggt för att köras distribuerat och på GPU för att accelerera körningar. Man måste inte ha en grafikprocessor, men det är en klar fördel när det är många beräkningar som behöver ske. Att gå från att köra beräkningar över natten till under en timme gör det till ett krav för att arbeta med.

Bygga upp nätverket

Min input kommer vara en tvådimensionell matris, eller ”tensor”, med dimensionerna [batch-storlek, sekvenslängd] där batch-storlek är antalet exempel som körs samtidigt och sekvenslängden är antalet ord i exemplet.

inputs = tf.reshape(inputs, [-1, config.num_steps])
embedding = tf.get_variable(

"embedding", [vocab_size, size], dtype=tf.float32)

inputs = tf.nn.embedding_lookup(embedding, inputs)

Embedding är här en variabel som definieras med dimensionerna alla möjliga ord i modellen samt storleken på LSTM-lager. Sedan transformeras indatat från heltal till vektorer med rätt antal element.

Definitionen av nätverket görs sedan genom att skapa en BasicLSTMCell för varje lager och slå i upp dessa till en enhet m.h.a MultiRNNCell

cell = tf.contrib.rnn.MultiRNNCell(

[tf.contrib.rnn.BasicLSTMCell(size) for _ in range(config.num_layers)])

För att mata in vår input i cellen så behöver det först formateras korrekt. Funktionen static_rnn vill ha en lista med tensorer av dimensionerna [batch-storlek, input-storlek], där varje tensor är ett steg i sekvensen. Funktionen unstack hjälper till att dela upp datat till listan.

inputs = tf.unstack(inputs, num=config.num_steps, axis=1)

outputs, next_state =

tf.nn.static_rnn(cell, inputs, self.state, dtype = tf.float32)

Outputs är nu en lista med resultat från varje steg. Detta kommer under träningen att användas till optimeringsmetoden för att anpassa vikterna i systemet.

Bygga upp träning

För att beräkna felet och hålla för att inte göra det allt för svårt så använder jag en metod som givet output och målvärden räknar ut korsentropin i sekvenser. Detta är väldigt matematiskt och går att läsa mer om, men jag väljer att ta den snabba vägen här och använda en färdig metod.

loss = tf.contrib.seq2seq.sequence_loss(

self.rnn.logits,
targets,
tf.ones([config.batch_size, config.num_steps],

dtype=tf.float32))

För att nu minimera denna “förlust” använder jag summan av hela batchens förluster och använder ”Gradient descent” för att förändra variabler. Ni som är bekanta med matte och algoritmer för att hitta minimum och maximum i ekvationer känner nog igen metoden där man steg för steg går ner för en lutning med kortare och kortare steg. Denna finns såklart inbyggd i Tensorflow. Jag har lagt in ett steg för att klippa gradienterna som sägs vara det korrekta sättet, enligt Tensorflow. Detta gör man för att undvika problem med gradienter som växer obehindrat eller försvinner.

cost = tf.reduce_sum(loss)
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars),

config.max_grad_norm)

training = optimizer.apply_gradients(zip(grads, tvars))

För att sedan köra igenom träningen så måste en session sättas upp och här finns det stöd för att automatiskt spara tillståndet under körning och läsa in vid uppstart.

with tf.Graph().as_default():

init = tf.global_variables_initializer()

# Bygg upp grafen med träning och LSTM
trainer = Trainer(train_data, config)

sv = tf.train.Supervisor(logdir=FLAGS.save_path)

with sv.managed_session() as session:

session.run(init)
feed_dict = {}
for epoch in range(config.max_max_epoch):
for i in range(trainer.epoch_size):
_, the_cost, current_state =

session.run([trainer.training,

trainer.cost,
trainer.rnn.next_state],
feed_dict = feed_dict)

feed_dict[trainer.rnn.state[0]] = current_state[0]
feed_dict[trainer.rnn.state[1]] = current_state[1]

Efter varje batch skickar vi tillbaka cellernas tillstånd i feed_dict som håller värden som ska användas i körningen. Metoden run tar en lista över de operationer som ska utföras och ge värden ifrån. Som nyckel i feed_dict används den tensor eller operation som ska ta emot värden så jag pekar här ut ”state” som ligger i ”rnn” i min ”trainer”. Statet för en LSTM är som standard en tuple med två värden som här måste pekas ut var för sig.

Den fullständiga koden för mitt experiment finns att hitta på här: https://github.com/tobhult/tf-lstm-experiment

Det finns gott om förbättringspunkter i experimentet. Bland annat saknas en utvärdering av förlusten med hjälp av testdata. Just nu körs bara träningen ett förbestämt antal omgångar vilket kan ge en övertränad modell. En annan förbättring skulle kunna vara att läsa in fler böcker samtidigt och därmed utöka mängden data.

Slutsatser

Som nybörjare med Tensorflow kan man lätt missförstå hur det fungerar. Det är lätt att tro att koden man skriver exekverar beräkningar när man egentligen bara bygger upp en graf för hur det ska utföras. Det gäller att för varje steg hålla reda på dimensioner i tensorer som bollas runt, ibland behöver man forma om dem för att passa nästa beräkning men man får tydliga fel som beskriver felet.

Efter att ha tränat med 2 lager med 400 noder i varje och kört igenom hela texten 30 gånger i träning under ca 5 timmar utan GPU får jag följande, där fetstilt är det jag skickat in.

(OBS. Jag har även separerat in punkter, komman, frågetecken, utropstecken, citationstecken och de html-taggar som finns i datat som egna ”ord”)
Jag vill ha en av dem den där tiden, som jag hade fått fråga var det med dem.”  <p> Karr sträckte mot örnen
Stockholm borde nog kanske gå något, som är så vis, ”sade hon.” Men jag tror inte, att jag kan glömma
Han visade sig vara en lång stund. Det var, som om han hade haft lust att göra, men han blev på en lång
Det var dags att se. Det var, som om den ena djuret hade blivit sämre än den där hade givit sig ut över
Nu ska jag ha drömt för, om det bara en gång har varit ett stycke deg. <p> Det var märkvärdigt med detta

Som vi kan se så har det blivit lite osammanhängande delar av meningar. Den försöker återskapa stycken från boken kan man se men lyckas inte helt. För att göra en bättre modell kan det krävas mer data, fler lager eller större lager. En sak som är säker är att man behöver mycket tid och dyr hårdvara.