Java-algoritme gætter Version2's emneord med en træfsikkerhed på 98 pct.

Kunstig intelligens og machine learning kan lyde kryptisk. Her piller vi mystikken af og gennemgår en klassisk algoritme - med kode og hele baduljen.

Kunstig intelligens lyder fancy, men for det meste handler det ‘bare’ om matematiske modeller og masser af data. At finde på modellerne er nok ikke så nemt, men at bruge teknologien er ikke så svært eller mystisk, som det kan lyde.

Her på Version2 har vi over frokosten diskuteret muligheden for at finde emneord med maskinlæring. Et emneord er et ord, der beskriver artiklens emne, såsom Databeskyttelsesloven, Sundheds-it, GDPR, Java, Ledelse, eller hvad det nu kunne være.

Det er ikke fordi journalisternes opgave med at finde de reglementerede tre emneord pr. artikel efter endt skrivearbejde er uoverkommelig - men måske kunne kunstig intelligens alligevel give et bud?

Det burde kunne lade sig gøre. Klassifikationsproblemer med naturlig tekst ender ofte med gode resultater, som er praktisk anvendelige. Det ved jeg fra et onlinekursus, et såkaldt MOOC, om processering af naturlig sprog, som jeg af nysgerrighed deltog i for en del år siden.

Den klassiske tilgang benytter metoden, der går under navnet Naive Bayes. Den bygger på en nogenlunde simpel sandsynlighedsmodel og er nem at kode.

Der findes bedre algoritmer til formålet, med navne som Support Vector Machines, K-Nearest Neighbor og neurale netværk, men de giver ikke nødvendigvis meget bedre resultater, siger sagkundskaben, og de er en del mere komplicerede. Naive Bayes kan vi klare med 250 linjers hjemmestrikket kode.

Fra formel til kode

Modellen, der ligger bag, forklarer vi i detaljer i en tilstødende artikel.

Kort fortalt bygger den på en forhåbning om, at hyppigheden af ord i artikler med ét emneord, er forskellig fra hyppigheden i artikler med et andet emneord. Vi udregner en score, eller et estimat, for emneordene, med en smart formel. Det emneord, der opnår højest score, har vundet - det er det emne, som algoritmen gætter på, givet en artikel. Derefter tester vi, om det er praktisk anvendeligt, på et testsæt af artikler.

For at gøre eksemplet nemmere, kigger vi bare på et enkelt emneord i det følgende, ‘sundheds-it’. Når vi skal fastslå, om en artikel handler om sundheds-it, betragter vi det som en dyst mellem to klasser: sundheds-it og ‘ikke-sundheds-it.’

For at bruge formlen skal vi gøre noget ved ordene i artiklerne. Vi fjerner, råt for usødet, alle tegn som ikke er A til Å, og skriver det hele med små bogstaver.

Nu skal vi bruge nogle tal.

Vi skal kende antallet af artikler om sundheds-it i træningssættet, som består af de seneste 2673 artikler på Version2. (Vi startede med 3000, tog 300 fra til et testsæt vi skal bruge senere, og slettede derefter artikler uden emneord.)

Ud fra det bestemmer vi sandsynligheden for, at en artikel har emneordet ‘sundheds-it’. Dem er der 107 af i træningssættet.

Det giver sandsynligheden 107 / 2673.

Sandsynligheden for ‘ikke-sundheds-it’ bliver så 2566 / 2673.

Vi skal også bruge antallet af unikke ord i alle artikler. Det er 54.423.

Og så skal vi bruge antallet af ord i alle sundheds-it-artikler. Det tal er 59.590. Og det samme tal for ikke-sundheds-klassen: 1.176.278 stk.

Så laver vi en tabel med ordhyppigheder for sundheds-it i træningssættet, og en anden for ikke-sundheds-it.

Det kræver jo ikke ligefrem det store programmør-kørekort.

Til slut laver vi to tilsvarende tabeller over såkaldte estimater, kaldet for log-P-hat - det er et ‘P’, med en hat over - som vi beregner på denne måde, i pseudokode, med hyppighedstabellen for sundheds-it:

for (ord in hyppighedstabel):
  logPHat(ord) = log( (hyppighedstabel(ord) + 1)
    / (antalOrdISundhedsItKlassen + antalUnikkeOrdIAlleArtikler) )
  sundhedsItEstimatTabel.put(ord, logPHat)

og samme måde for ikke-sundheds-it.

Vi tester

Tro det eller la’ vær, men nu er vi allerede parat til at teste vores algoritme.

Vi skal finde et estimat for sundheds-it, og et for ikke-sundheds-it.

Testalgoritmen ser sådan ud, i pseudokode, givet en artikel:

sundhedsItlogEstimat = log(artiklerOmSundhedsIt / antalArtikler)
for (ord in artikel):
   logPHat = sundhedsItEstimatTabel.get(ord)
   if (logPHat == null):
      logPHat = log(1 / (antalOrdISundhedsItKlassen
       + antalUnikkeOrdIAlleArtikler))
   sundhedsItlogEstimat += logPHat

(Forklaringen på algoritmen findes i næste artikel.)

Vi laver samme beregning for ikke-sundheds-it-klassen. Det estimat, der er højest, har vundet.

Vi tester ikke på en enkelt artikel, for det bliver vi ikke meget klogere af. I stedet har vi et testsæt på 297 artikler, som fortæller os, om vi kan bruge det til noget i praksis.

Forvirringsmatrix på et højere plan

Vi kværner de 297 uberørte artikler igennem testalgoritmen ovenfor. Resultatet er fire tal, kaldet en 'confusion matrix', eller forvirringsmatrix, i en skidt oversættelse.

De fire tal er:

  • Sande positive: Hvor mange gange udpegede testalgoritmen 'sundheds-it' rigtigt
  • Sande negative: Hvor mange gange pegede algoritmen rigtigt på 'ikke-sundheds-it'
  • Falske positive: Algoritmen udpegede fejlagtigt en artikel som sundheds-it
  • Falske negative: Algoritmen kunne ikke genkende en sundheds-it-artikel.

Hvad var resultatet? Kære læser, du skal ikke efterlades i spænding. Det er:

Sande positive: 7
Sande negative: 285
Falske positive: 0
Falske negative: 5

Der var 12 artikler om sundheds-it blandt de 297 test-artikler. Algoritmen fandt 7 af dem. Den udpegede korrekt alle 285 artikler, der ikke handlede om sundheds-it. Den pegede ikke på nogle forkerte. Der var 5 artikler om sundheds-it, den ikke kunne finde.

Er det godt eller skidt? Det spørgsmål afhænger af, hvad brugsscenariet er.

For at komme tættere på den problemstilling, udregner vi fire 'mål' - tal mellem 0 og 100% - som kan give os et bedre billede af resultatet.

De fire mål er:

Precision - hvor tit gættede algoritmen 'sundheds-it' rigtigt?

Recall - hvor stor procentdel af sundheds-it-artiklerne fandt den?

Accuracy - træfsikkerhed

F1 - et slags gennemsnit af precision og recall, som kan bruges til at optimere algoritmen med.

De udregnes således, hvor tp er sande positive, tn sande negative, fp falske positive og fn falske negative:

precision = tp / (tp + fp)
recall    = tp / (tp + fn)
accuracy  = (tp + tn) / (tp + tn + fp + fn)
f1        = 2 * (precision * recall) / (precision + recall)

Med vores testsæt giver det:

Precision: 100%
Recall: 58%
Accuracy: 98%
F1: 74%

Algoritmen var knald-god til at ramme plet på sundheds-it-artiklerne. Den fandt 58% af sundheds-it-artiklerne i testsættet. Træfsikkerheden var sublim (men det er ikke så praktisk anvendeligt), og f1-målet var ikke dårligt, bedømt på tidligere erfaringer.

I en kommende artikel kigger vi på, hvad det egentlig betyder, i forhold til vores formål, og hvordan vi kan 'tune' algoritmen til at give bedre resultater, specielt i forhold til f1-målet.

Download kodeeksempel, træningssæt og testsæt

Det ville fylde for meget at gengive den Java-kode, vi har brugt i artiklen. Så den kan downloades her (Google Drive kan sige nogle fjollede ting undervejs, men bare tryk 'download.').

Eksemplet indeholder det samme trænings- og testsæt, der gennemgås her i artiklen, med den lille krølle, at ordene i artiklerne er randomiseret. På den måde er der ikke nogen i Mediehuset Ingeniøren, der behøver ligge vågen om natten og tænke på ophavsret osv. Algoritmen er ligeglad med rækkefølgen af ordene, så det gør ingen forskel for eksemplet.

Koden er skrevet så eksemplet er nemt at forstå, og er ikke optimeret. Der er tre hardcodede filstier, der kan tilrettes, hvis der er bøvl. Det er kommenteret i kildeteksten.

Modellen bag algoritmen gennemgår vi i den næste artikel.

Læs også: Her er modellen bag Version2's emneords-gætteri

Tips og korrekturforslag til denne historie sendes til tip@version2.dk
Følg forløbet
Kommentarer (1)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Log ind eller Opret konto for at kommentere