Sådan skaber vi en journalist-robot

Illustration: Bigstock
Vores research-robot møder journalist-robotten, som skal skrive jobtrends-artikler for Version2.

Robot-journalistik er et varmt emne i mit fag. Det er efterhånden mange år siden, at branchen startede på at bruge robotter til at skrive vejrudsigter og sportsresultater, og siden da er der kun kommet mere fart på.

Herhjemme er det nok Lasso News, som er mest kendt for robot-journalistik. Mediet udgiver stakkevis af erhvervsrelaterede nyheder og portrætter hver dag, skrevet af to robotter. Ved at trække oplysninger ud af offentlige databaser, såsom CVR-registeret, kan robotterne skrive nyhedsartikler og portrætter af erhvervslivets personligheder på under to sekunder.

Læs også: Robotter skriver dagligt 1.500 erhvervsnyheder på dansk

Version2 søsatte i foråret en research-robot, som indsamler jobannoncer indenfor it, og derefter optæller forekomsten af omkring 500 termer. Ordene, som kan være Javascript, Cloud og Scrum, stammer fra konsulentfirmaer, sites som StackExchange og Version2’s egne emneord. Robotten høster annoncer et par gange om ugen. Det giver et billede af, hvad der efterspørges på det danske it-arbejdsmarked.

Indtil videre har journalisten selv skulle skrive de artikler, der kommer ud af robottens research, men nu sætter vi os for også at gøre noget ved den del.

Resultatet af research-robottens anstrengelser er et kommasepareret dokument (csv), der indeholder en række oplysninger. Her har vi for overskuelighedens skyld barberet indholdet ned til rangeringen af termerne.

Altså en hitliste. Første række indeholder datoerne for dataindsamling, og rækkerne indeholder rangeringen for termerne, som på illustrationen herunder.

Illustration: Version2

Og så skal der skrives en prototype.

Min ide er simpel. Jeg vil skrive en række skabeloner, som hver udlægger en vinkel, et bestemt perspektiv, på talmaterialet. Skabelonerne skal indeholde sproglige varianter, så robotten ikke skriver den samme historie igen og igen.

På den længere bane skal journalisterne selv kunne skrive skabelonerne, som jeg forestiller mig kan se sådan ud:

[Periodens] højdespringer/e er [højdespringere], som hopper [spring] pladser op, fra en [placering-før]-plads til en [placering-nu]-plads.

En sådan skabelon kunne så resultere i et afsnit i en artikel, i stil med dette:

Kvartalets højdespringere er Cloud og Python, som hopper seks pladser op, fra en 12. og 14.-plads til en sjette og ottende-plads.

Det er fremtidsmusik, men jeg prøver at have det i baghovedet, så min prototype senere kan udvikles i den retning.

Jeg beslutter mig for, at en vinkel skal skrives ud fra de data, der er i csv-dokumentet, samt en periode, der skal være sammenligningsgrundlag for hitlisten, og som kan være en uge, måned, kvartal osv.

Hver Vinkel hører på god, objektorienteret vis hjemme i sin egen klasse, og vinklen skal aflevere et Bud som resultat. Et bud består blot af den tekst, som er blevet genereret, samt en score mellem 0 og 1, der angiver hvor god historien er.

Jeg aftaler med mig selv, at en vinkel med en score på 0.5 er lidt kedelig, mens en vinkel på 1.0 betyder, at det er en rigtig god historie, i en eller anden forstand.

Senere kan en ArtikelSkriver så indlede artiklen med den vinkel, der har den højeste score, og derefter skrive dem med lavere score. Det minder om den såkaldte 'journalistiske nyhedstrekant,' hvor man starter med at skrive om sagens vigtigste oplysninger, og derefter uddyber de underliggende oplysninger.

Nok snak - til tasterne. Jeg bruger Java, som research-robotten også er skrevet i.

Top of the pops

Vores første Vinkel, som hedder Top1, ser sådan ud:

public static class Top1Vinkel implements Vinkel {
    public Bud bud(Data data, Periode periode) {
      var tekst = "";
      var score = 0.0;
 
      var vinder = data.nyeste().plads(1);
      var tidligerePlacering = data.tidligere(periode)
          .findPlacering(vinder);
      var flertalsEr = tidligerePlacering > 1 ? "er" : "";
      if (tidligerePlacering > 1) {
        tekst = String.format(
            "%1$s étter er %2$s, som rykker fra en %3$s til "
                + "førstepladsen. "
                + "| Denne %4$ss nummer ét er %2$s, der har rykket"
                + " %3$s plads%5$s i løbet af perioden.", 
            stortBegyndelsesbogstav(periode.ejefaldBestemtForm()),
            vinder, kardinaltalTilTekst(tidligerePlacering),
            periode.ental(), flertalsEr);
        score = Math.min(1.0, 0.5 + tidligerePlacering / 10.0);
      } else {
        tekst = String.format(
            "%1$s étter er %2$s, som bevarer sin førsteplads. "
                + "| Periodens nummer ét er %2$s, og det var det "
                + "også for en %3$s siden.",
            stortBegyndelsesbogstav(periode.ejefaldBestemtForm()),
            vinder, periode.ental());
        score = 0.6;
      }
 
      return new Bud(tekst, score);
    }
  }

Jeg har skrevet en hjælpeklasse, Data, der gør alt det indviklede med at finde de rigtige tal i csv-dokumentet.

Jeg finder vinderen, top-1, i den nyeste dataindsamling, med linjen

var vinder = data.nyeste().plads(1);

Jeg finder dens tidligere placering:
var tidligerePlacering = data.tidligere(periode)
          .findPlacering(vinder);

– hvor periode er en enum med værdien uge, måned, kvartal osv.

Det første spørgsmål, der falder mig ind i forhold til top-1, er: Har den rykket sig fra sidst?

if (tidligerePlacering > 1) {

I så fald vil jeg bruge skabelonen
tekst = String.format(
            "%1$s étter er %2$s, som rykker fra en %3$s til "
                + "førstepladsen. "
                + "| Denne %4$ss nummer ét er %2$s, der har rykket"
                + " %3$s plads%5$s i løbet af perioden.", 
            stortBegyndelsesbogstav(periode.ejefaldBestemtForm()),
            vinder, kardinaltalTilTekst(tidligerePlacering),
            periode.ental(), flertalsEr);

Javas String.format-metode erstatter forekomster af brikkerne %1$s, %2$s, %3$s med metodens streng-parametre, i den rækkefølge, de er angivet i.

Altså:

  • %1$s = stortBegyndelsesbogstav(periode.ejefaldBestemtForm())
  • %2$s = vinder
  • %3$s = kardinaltalTilTekst(tidligerePlacering)

– og så fremdeles.

Det kan resultere i dette stykke herlige prosa:

Denne måneds nummer ét er sikkerhed, der har rykket to pladser i løbet af perioden.

I skabelon-strengen har jeg angivet to alternative formuleringer, som er adskilt med |-symbolet. Når artiklen genereres, kan der så vælges et tilfældigt alternativ, så den færdige artikel ikke ser ens ud hver gang, der skrives en ny artikel.

Endelig udregner jeg en score, hvor 0.5 altså er en lidt kedelig artikel, og 1.0 ret god. Jeg laver denne formel:

score = Math.min(1.0, 0.5 + tidligerePlacering / 10.0);

TidligerePlacering angiver hvor langt, termen er hoppet. Jo højere hop, jo bedre score. Jeg vægter hoppet ved at dele med 10, og så må anvendelsen vise, om det er hensigtsmæssigt eller ej.

Jeg skriver et par vinkler til - de hedder HøjdespringerVinkel og SynkerVinkel, som i ‘ugens højdespringer’ og ‘månedens synker.’

Og jeg et par stykker på tegnebrættet - ‘månedens stræber,’ der stiger støt gennem hele perioden, samt ‘kvartalets nedstiger,’ der gør det modsatte.

‘Kongens fald’ kunne være vinklen om en top-1-placering, som har holdt skansen i lang tid og så bliver skubbet fra tronen, og ‘rivalernes kamp’ kunne være forholdet mellem Java og C#, eller Aws og Azure.

Det er ikke svært at skrive nye vinkler, der som regel kun kræver en håndfuld linjer.

Til tasterne

Nu skal artiklen skrives. Det er ikke særligt kompliceret. Første del ser sådan ud:

public class ArtikelSkriver {
  String indledning = "Version2's job-robot har igen været på høst og "
      + "kigget på populære termer i it-jobannoncer. "
      + "| Robot-redaktionen har igen været på flittig jagt efter "
      + "populære ord i jobannoncer.";
 
  String uddybning = "Vi har sat en robot til at indsamle danske "
      + "it-jobannoncer og derefter kigget efter omkring 500 termer, "
      + "som har noget at gøre med it. Ordene stammer blandt andet "
      + "fra konsulentfirmer, sites som Stack Exchange og Version2’s "
      + "egne emneord. | Version2 har sat en robot til at indsamle "
      + "jobannoncer indenfor it, og derefter optalt forekomsten af "
      + "omkring 500 termer. Ordene stammer fra konsulentfirmer, "
      + "sites som Stack Exchange, Version2’s egne emneord med mere.";
 
  String hitlisteTekst = "Her er hele hitlisten for denne gang: "
      + "| Sådan ser hele listen ud denne gang:";
 
  String jobtrendGrafik = "[grafik:43679]";
 
  Vinkel[] vinkler = new Vinkel[] { new Vinkler.Top1Vinkel(),
      new Vinkler.HøjdespringerVinkel(), new Vinkler.SynkerVinkel() };
 
  public List<Bud> rangeredeVinkler(Data data, Periode periode) {
    var rangeredeVinkler = new TreeSet<Bud>(
        (b1, b2) -> Double.compare(b2.score, b1.score));
    for (var vinkel : vinkler) {
      rangeredeVinkler.add(vinkel.bud(data, periode));
    }
    return new ArrayList<Bud>(rangeredeVinkler);
  }

Først skriver vi en indledning, der ligesom tidligere har en række alternative formuleringer. Så har vi en uddybning, efterfulgt af et afsnit der præsenterer hitlisten sidst i artiklen, og et markdown-element, der indsætter Jobtrends-grafikken i cms’et.

Nu gennemløber vi de tre vinkler, vi har skrevet indtil videre. Vi finder buddet for hver vinkel, og putter det i et TreeSet, som i Java er en ordnet mængde. Ordningen er

(b1, b2) -> Double.compare(b2.score, b1.score)

– som placerer bud med højeste score først. Det returnerer vi som en liste.

Så er det bare at smække afsnittene sammen:

  private static final String NYT_AFSNIT = String.format("%n%n");
  private static final String SIDEFOD = "*Denne artikel er "
           + "skrevet af en robot.*";
 
  public String skrivArtikel(Data data, Periode periode) {
    var vinkler = rangeredeVinkler(data, periode);
    var artikel = vælgTilfældig(indledning);
    artikel += NYT_AFSNIT;
    artikel += jobtrendGrafik;
    artikel += NYT_AFSNIT;
    artikel += vælgTilfældig(vinkler.get(0).tekst);
    artikel += NYT_AFSNIT;
    artikel += vælgTilfældig(uddybning);
    artikel += NYT_AFSNIT;
 
    for (var v = 1; v < vinkler.size(); v++) {
      artikel += vælgTilfældig(vinkler.get(v).tekst);
      artikel += NYT_AFSNIT;
    }
 
    artikel += vælgTilfældig(hitlisteTekst);
    artikel += NYT_AFSNIT;
    artikel += data.skrivHitliste(periode);
    artikel += NYT_AFSNIT;
    artikel += SIDEFOD;
 
    return artikel;
  }

Efter indledningen starter vi artiklen med det bud, der gav højest score:

 artikel += vælgTilfældig(vinkler.get(0).tekst);

VælgTilfældig-metoden tager en tilfældig af alternativerne i strengen, der altså er adskilt af |-symbolet.

I løkken:

   for (var v = 1; v < vinkler.size(); v++) {
      artikel += vælgTilfældig(vinkler.get(v).tekst);
      artikel += NYT_AFSNIT;
    }

– indsætter vi så de øvrige bud, rangeret efter score.

Til sidst indsætter vi hitlisten, der blot sætter markdown-symboler om data, så der kommer en pæn tabel nederst i artiklen.

Det tager 0,06 sekunder at skrive artiklen. Tag den, Lasso News.

Resultatet er her:

Læs også: Jobtrends: Html hopper højt

Historien har kun været i luften ganske få minutter, før nogen påpeger, at der er stavefejl i indledningen, og at ‘synker’ ikke er et rigtigt ord i denne sammenhæng.

Det er heldigvis nemt og hurtigt at ændre skabelonen, så nu hedder det for fremtiden en ‘dykker’ i stedet for.

Der er også en kollega, der synes artiklen er lidt kedelig, og - hånden på hjertet - det er ikke helt forkert.

Så motivationen til det videre arbejde er på plads. Der er i hvert fald masser af muligheder for at forbedre robotten, så det er bare at klø på. Målet er fuldt ud automatiseret luksus-journalistik, og måske ender vi der en dag.

Artiklens kildekode, som er skrevet med Java 11, eksekverbart program og eksempel-csv-fil kan downloades her.

Tips og korrekturforslag til denne historie sendes til tip@version2.dk
Følg forløbet
Kommentarer (3)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Aslak Stage

Super fin artikel og god inspiration for en kodenybegynder som jeg.

Det er et meget fascinerende område og jeg synes det er spændende at følge med i.

Jeg savner dog en vigtig pointe: er det journalistik det som algoritmen laver ? Eller er det blot indsamling af data, som derefter kategoriseres og der konstateres at eksempelvis HTML er højere oppe på listen end tidligere?

I min bog er det ikke journalistik.

Journalistik er efter min overbevisning i det her tilfælde, der hvor man begynder at grave spadestikket dybere, og på baggrund af den her data indsamling spørger hvad er årsagen til det ? Man er kildekritisk og stiller spørgsmål til de data som er indsamlet.

Eksempel:
At en robot indsamler data om et firma og siger at det nu har et regnskab, som siger omsætning A og overskud B kan godt være en nyhed, men alene fordi det er ny information.
Men det er ikke en journalistisk bearbejdet nyhed. Blot en information om at der er et nyt regnskab.
Den journalistiske bearbejdning er der hvor tallene er blevet analyseret og man som journalist spørger ledelsen/Bestyrelsen om hvorfor tallene ikke lever op til de forudsigelser der er udmeldte eller hvad fremtiden bringer m.fl.

Bare fordi vi kalder en spade en spade bliver det ikke til en spade, hvis det ikke er det, fordi det er en skovl, men spade lyder fancier.

Ligesom AI/Kunstig intelligens ikke bliver ligesom menneskelig intelligens bare fordi nogen siger det.

Afslutningsvis super mange tak for dine kodeartikler, Tania, de giver mig mod på at lære mere om det der kodning:-)

  • 1
  • 0
Hans Nielsen

Her levere Bot allerede nu mange artikler

Synes stadig at det værste er når der er brugt en computere genneret stemme, i video lavet af bot.

Man bliver simplehen træt af at høre på en sådan i længten.

Men denne udvikling er jo bare en bekræftigelse, af den udvilkling som der forudsiges i "Humans Need Not Apply" Selv om den er 5 år gammel, så får den ret i mange af sine forudsigelser. Lige så stille.

Min nye bil, klare selv at holde vejbannen og fart. og passer på omgivelser, og den kaldes hverken selv selvkørende, men er i dag bare en del af pakken.

Det er bagrunden for at Donald Trumper blevet valgt og Bernie Sanders måske får en chance næste gang. Der er rigtigt mange hvide mennesker i USA, som har gjort alt hvad der blvet bedt om. Men som nu står med en uddanellese, gæld men ingen ordenlige Job.

https://www.youtube.com/watch?v=7Pq-S557XQU

  • 0
  • 0
Log ind eller Opret konto for at kommentere