V2 amok i skygge-it: Bookmarklet-SSL-server på netværket med hjemmelavet certifikat

Illustration: Version2
Før sommeren fik vi skabt en helt ok emneords-gætter med machine learning. Nu blæser vi på sund fornuft og syr algoritmen ind i Version2's CMS med en bookmarklet og masser af skygge-it.

På Version2 er vi godt i gang med at automatisere valget af emneord til nye artikler, i hvert fald i den grad, som vores machine learning-algoritme Naive Bayes kan levere varen.

I sidste artikel fik vi optimeret algoritmen og pressede seks ekstra procentpoint ud af algoritmen, i hvert fald når emneordet var ‘sundheds-it.’

I mellemtiden har vi forbedret algoritmen lidt mere, ved at tælle overskrifter to gange, som vi også var inde på i sidste artikel. Vi prøvede også at tilføje artiklernes forfatternavne, men det trak en anelse ned i forudsigelserne - så det droppede vi igen. Version2’s journalister er åbenbart ikke fagidioter, kan man måske konkludere, eller så har der været mange forskellige igennem i årenes løb.

Nu er tiden kommet til at prøve det hele i virkeligheden, og se, om det egentlig kan bruges af dem, som det er tiltænkt - Version2’s journalister.

Vi vil implementere emneords-algoritmen i Version2’s CMS-system som en bookmarklet - en stump Javascript, der kan placeres i et link i et browser-bookmark. Så slipper vi for at overbevise webudviklerne om, at de skal smide vores tossede kode ind i deres system, og samtidigt er det nemmere end at lave en browser-extension.

Det er det rene skygge-it. Men den slags kan jo også have sin charme, indenfor rimelighedens grænser, altså.

Vores 'user story' lyder sådan her:

Journalisten har en ny, færdigskrevet artikel åben i redigerings-modus i CMS'et, og klikker på bookmarkletten i browserens bogmærkelinje. Derved sender bookmarkletten overskrifter og artikeltekst til serveren via et ajax-kald. Machine learning-algoritmen gør sit arbejde og returnerer bud på emneord til bookmarkletten, som derefter opmærker emneordene i CMS'ets dialogboks med gul baggrundsfarve. Så kan journalisten bruge forslagene eller ej, ved at sætte et flueben i en krydsboks - sådan som det ser ud i illustrationen i toppen af denne artikel.

Vi strikker et hjemmelavet certifikat

Vores CMS kører HTTPS, naturligvis, og reglen for bookmarklets er, som med eksterne scripts i almindelighed, at man ikke kan blande krypterede og ukrypterede kilder.

Vi skal med andre ord sætte en HTTPS-server op, som forsyner vores bookmarklet med bud på emneord.

Vi har ikke brug for stor server og det konfigureringsbesvær, der ofte følger med. HTTP-serveren vi benytter her, er en lille én, der følger med Javas udviklingsmiljø (JDK), men som ikke er officielt en del af klassebibliotekerne (JRE). Den befinder sig dog i et ‘jdk’-modul, og er dermed et velsignet medlem af JDK’et. Server-koden er open source, ligesom klassebibliotekerne i øvrigt.

For at skabe en SSL-forbindelse skal vores server have et certifikat. Her laver vi bare vores eget. Det betyder, at certifikatet ikke er underskrevet af et rodcertifikat, som ‘rigtige’ certifikater er, og at brugeren af bookmarkletten skal foretage en ‘sikkerhedsundtagelse’ i browseren.

Vi laver certifikatet med Java-værktøjet Keytool, som ligger i ‘bin’-mappen i Javas distribution. Jeg åbner et terminalvindue og starter programmet. På min pc ser det sådan ud:

C:\Program Files\Java\jre-9.0.4\bin>keytool -genkey -keyalg RSA -alias webservice -keystore C:\Users\tan\Desktop\selfsigned.jks -validity 365 -keysize 2048

– hvor det hele skal stå på samme linje.

Nu stiller programmet en række spørgsmål, der skal besvares.

Enter keystore password: 
Re-enter new password:
What is your first and last name?
  (Unknown):  Tania Andersen
What is the name of your organizational unit?
  (Unknown):  Version2
What is the name of your organization?
  (Unknown):  MI
What is the name of your City or Locality?
  (Unknown):  Copenhagen
What is the name of your State or Province?
  (Unknown):
What is the two-letter country code for this unit?
  (Unknown):  DK
Is CN=Tania Andersen, OU=Version2, O=MI, L=Copenhagen, ST=Unknown, C=DK correct?
  (no):  yes

Herefter kvitterer programmet med at generere en privat nøgle på mit skrivebord, med navnet selfsigned.jks.

Så kan vi sætte serveren op, således:

private static final String SUN_X509_ALGORITME = "SunX509";
 
public static void main(String[] args) throws IOException, 
    Exception, NoSuchAlgorithmException {
  // Opsæt og start webserveren.
  HttpsServer httpsServer = HttpsServer.create(
    new InetSocketAddress(8000), 0);
  KeyStore ks = KeyStore.getInstance("JKS");
  FileInputStream fis = new FileInputStream(
    "C:\Users\tan\Desktop\selfsigned.jks");
  char[] password = new char[] { 'p', '1', 'z', 'z', 'a', 
    'r', 'e', 's', 't' };
  // Char-array benyttes i stedet for String,
  // for at undgå at passwordet ender i String-poolen.
  ks.load(fis, password);
  KeyManagerFactory kmf = KeyManagerFactory
    .getInstance(SUN_X509_ALGORITME);
  kmf.init(ks, password);
  TrustManagerFactory tmf = TrustManagerFactory
    .getInstance(SUN_X509_ALGORITME);
  tmf.init(ks);
  SSLContext sslContext = SSLContext.getInstance("TLS");
  sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
 

Vi skaber en ny server med kaldet HttpsServer.create, som tager en socket-portadresse som parameter. Her sætter vi den til port 8000.

Så skaber vi en Keystore, som holder på vores private key, og indlæser vores nøgle selfsigned.jks fra før, med en FileInputStream, sammen med det password, vi brugte da vi skabte privat-nøglen.

Vi skal også bruge en KeyManagerFactory (kmf) og en TrustManagerFactory (tmf). Nu skal vi have et SSLContext-objekt, som vi får med metoden SSLContext.getInstance("TLS"), hvor vi beder om TLS-protokollen - vore dages udgave af SSL.

Nu initieres SSLContexten med metoden sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers()). Så kan vi konfigurere vores server. Det ser sådan ud:

httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
  public void configure(HttpsParameters params) {
    try {
      SSLContext c = SSLContext.getDefault();
      SSLParameters defaultSSLParameters = c
        .getDefaultSSLParameters();
      params.setSSLParameters(defaultSSLParameters);
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException(e);
    }
  }
});

Nu er serveren konfigureret til SSL med vores hjemmelavede certifikat.

(Man kunne måske have ønsket sig, at Javas api-designere havde gjort det lidt nemmere for os. Jeg har set et Python-eksempel på det samme, og det fyldte fire linjer. Men sådan er det nu en gang i Java-verdenen.)

Nu skal der skabes en ‘context’, som er en request-handler, der knyttes til en bestemt url. Det ser sådan ud i vores eksempel:

  httpsServer.createContext("/apps/emneord", new Servicehandler());
  httpsServer.setExecutor(null); // creates a default executor
  httpsServer.start();
  LocalDate now = LocalDate.now();
  now.format(DateTimeFormatter.ISO_DATE);
  System.out.println("Starter webtjenesten. " 
    + ZonedDateTime.now().format(FORMATTER));

Parameteren ‘new Servicehandler()‘ er en instans af interfacet HttpHandler, som har metoden ‘handle‘, der skaber HTTP-svaret. Det ser sådan ud i vores eksempel:

static class Servicehandler implements HttpHandler {
  private Tester tester = new Tester();
 
  public void handle(HttpExchange exchange) throws IOException {
     InputStream is = exchange.getRequestBody();
      final String requestBody = new String(is.readAllBytes(), "UTF-8");
      String response = findEmneord(requestBody);
      final Headers responseHeaders = exchange.getResponseHeaders();
      responseHeaders.put("Access-Control-Allow-Origin", List.of("*"));
      responseHeaders.put("Content-Type", List.of("text/plain"));
      exchange.sendResponseHeaders(200, response.length());
      OutputStream os = exchange.getResponseBody();
      os.write(response.getBytes());
      os.close();
  }
}

Her henter vi request-body'en ud af HttpExchange-parameteren med kaldet exchange.getRequestBody. Vi kalder derefter metoden findEmneord. Tilbage i artiklen i april havde vi en metode, der hed testArtikel, og vores findEmneord-metode gennemløber blot testArtikel med de 50 mest anvendte emneord. Hvis testArtikel vender tommelfingeren op på et givent emne, sender vi emneordet tilbage til vores bookmarklet.

For at bookmarkletten kan få lov at tilgå CMS-websiden, skal vi sætte response-headeren Access-Control-Allow-Origin til en asterisk. Det gøres i linjen responseHeaders.put("Access-Control-Allow-Origin", List.of("*")).

Kodeeksemplet kan downloades fra Google Drive, da det fylder for meget for Gitlab - det er modellens estimat-tabeller, der fylder en del. (Google Drive kan sige nogle fjollede ting undervejs, men bare tryk 'download.'). Når serveren er oppe og køre, via det medfølgende batch-script, kan en testside kan tilgås med url'en: https://localhost:8000/apps/emneord/test. (Den del af koden er udeladt i eksemplet her i artiklen.)

I en kommende artikel ser vi nærmere på udviklingen af bookmarkletten, der modtager svaret fra vores server, og syr det ind i vores CMS. Bliv på kanalen.

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

Tak for en sjov lille interessant artikel at starte dage på - selv om Java-eksemplerne får nakkehårene til at stritte, da de minder mig om alt for mange timer brugt på at få hul på diverse enterprise-API'er gennem tidens løb. Der mangler blot et par SOAP-kald, så er den hjemme!

Vi prøvede også at tilføje artiklernes forfatternavne, men det trak en anelse ned i forudsigelserne - så det droppede vi igen. Version2’s journalister er åbenbart ikke fagidioter, kan man måske konkludere, eller så har der været mange forskellige igennem i årenes løb.

Nu bevæger jeg mig ud på tynd is - men er det her ikke et klassisk eksempel på selection bias?

Man sidder med en algoritme, man gerne vil have til at producere et givent resultat, og derfor eliminerer man de faktorer, som får resultatet til at afvige - men til sidst ender man med en algoritme, der passer perfekt på ens testcase, men muligvis performer helt anderledes, når den udsættes for data, der ikke var en del af testen.

Der kan selvfølgelig være gode grunde til at udelade bestemte parametre fra sin algoritme, men alene det, at parameteren får algoritmens resultat til at afvige fra det forventede, bør vel ikke i sig selv være grund til at fjerne parameteren - som man jo har antaget havde en betydning, da man tilføjede den i første omgang.

Martin Storgaard Dieu

Man kan med fordel kigge lidt på Feature selection hvor der findes en masse smarte algoritmer. Jeg har dog kun brugt det på spatialdata, men mon ikke det skulle kunne lade sig gøre at bruge på ikke-spatial data?

Så længe man bruge sampling i sin træning, så bør du ikke få selection bias. Og med feature selection (brugt rigtigt) kan man også mindske overfitting :)

Tania Andersen Journalist

Hej Yoel, der er ikke tale om at 'få resultatet til at afvige'. Features evalueres i dette eksempel udelukkende ved at se, hvor meget f1-målet øges i træningssættet. Vi har gennemgået modellen i de forrige artikler, som der er linket til i artiklen og forneden.

Mvh Tania/v2

Tania Andersen Journalist

Jeg uddyber lige mit svar til Yoel her, nu hvor jeg er kommet hjem fra travlt arbejde og har fået skænket et glas fredags-vin, mens jeg venter på at pizza-dejen tør op.

Selection bias er, når du hiver fisk op i den ene ende af søen, måler gennemsnittet på størrelsen og får et forkert tal, fordi de store fisk holder til i den anden ende af søen.

I vores tilfælde er træningssættet omkr. 6000 af de seneste V2-artikler, så vi kender 'universet' ret godt i modellen - til forskel fra en visuel algoritme, der fx skal genkende vilkårlige cykler via et kamera i trafikken, optrænet med billeder af andre cykler.

Vores model-verden er altså 100% kendt, og det er kun de fremtidige artikler - der sikkert ligner de sidste par års produktion nogenlunde - der skal vurderes. Så risikoen for selection bias er lille, i modsætning til cykel-eksemplet, hvor en større variation i det fri må forventes. Præmissen er naturligvis, at vores model skal genoptrænes jævnligt med nyankomne artikler.

Det betyder også, at vi enøjet kan bruge f1-målet til at vurdere egnetheden af features - hvis en feature giver højere f1 i træningssættet, skal den med, og vice versa.

Når det er sagt, så er et af kendetegnene ved Naive Bayes, at algoritmen er meget biased - den tror, at træningssættet er lig med hele verden og ‘overfitter’ mere end andre algoritmer.

Planen er i en senere artikelrække at udskifte Naive Bayes med deep leaning (neurale netværk), som netop har mindre problemer med overfitting, og som burde give bedre f1-tal - hvis jeg får tid til det. :-)

Mvh Tania/V2

Log ind eller Opret konto for at kommentere