Borer inn i Sparks ALS-anbefalingsalgoritme PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.

Boring i Sparks ALS-anbefalingsalgoritme

ALS-algoritmen introdusert av Hu et al., er en veldig populær teknikk som brukes i Recommender System-problemer, spesielt når vi har implisitte datasett (for eksempel klikk, liker osv.). Den kan håndtere store datamengder rimelig bra, og vi kan finne mange gode implementeringer i forskjellige maskinlæringsrammer. Spark inkluderer algoritmen i MLlib-komponenten som nylig har blitt omformet for å forbedre lesbarheten og arkitekturen til koden.

Gnists implementering krever at vare- og bruker-ID-en er tall innenfor heltallsområdet (enten heltallstype eller langt innen heltallsområde), noe som er rimelig, da dette kan bidra til å øke hastigheten på operasjonene og redusere minneforbruket. En ting jeg la merke til mens jeg leste koden, er at disse id-kolonnene blir kastet i Double og deretter til Integers i begynnelsen av fit / predict metodene. Dette virker litt hacky, og jeg har sett det legge unødvendig belastning på søppeloppsamleren. Her er linjene på ALS-kode som kaster idene i dobbeltrom:
Borer inn i Sparks ALS-anbefalingsalgoritme PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.
Borer inn i Sparks ALS-anbefalingsalgoritme PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.

For å forstå hvorfor dette gjøres, må man lese avkrysset Cast ():
Borer inn i Sparks ALS-anbefalingsalgoritme PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.

Denne UDF mottar en dobbel og sjekker rekkevidden og kaster den til heltall. Denne UDF brukes til skjemavalidering. Spørsmålet er om vi kan oppnå dette uten å bruke stygge doble støpegods? Jeg tror ja:

  protected val checkedCast = udf { (n: Any) =>
    n match {
      case v: Int => v // Avoid unnecessary casting
      case v: Number =>
        val intV = v.intValue()
        // True for Byte/Short, Long within the Int range and Double/Float with no fractional part.
        if (v.doubleValue == intV) {
          intV
        }
        else {
          throw new IllegalArgumentException(s"ALS only supports values in Integer range " +
            s"for columns ${$(userCol)} and ${$(itemCol)}. Value $n was out of Integer range.")
        }
      case _ => throw new IllegalArgumentException(s"ALS only supports values in Integer range " +
        s"for columns ${$(userCol)} and ${$(itemCol)}. Value $n is not numeric.")
    }
  }

Koden ovenfor viser en modifisert checkedCast () som mottar inngangen, sjekker hevder at verdien er numerisk og hever unntak ellers. Siden inngangen er hvilken som helst, kan vi trygt fjerne alle rollebesetningene til dobbeltuttalelser fra resten av koden. Videre er det rimelig å forvente at siden ALS krever ID-er innen heltallsområde, bruker flertallet av mennesker faktisk heltallstyper. Som et resultat på linje 3 håndterer denne metoden Integers eksplisitt for å unngå støping. For alle andre numeriske verdier sjekker den om inngangen er innenfor heltallsområdet. Denne kontrollen skjer på linje 7.

Man kan skrive dette annerledes og eksplisitt håndtere alle tillatte typer. Dessverre vil dette føre til duplikatkode. I stedet er det jeg gjør her å konvertere nummeret til heltal og sammenligne det med det opprinnelige nummeret. Hvis verdiene er identiske, gjelder et av følgende:

  1. Verdien er Byte eller Kort.
  2. Verdien er lang, men innenfor heltallområdet.
  3. Verdien er dobbel eller flyt, men uten noen brøkdel.

For å sikre at koden kjører bra, testet jeg den med standard enhetstester av Spark og manuelt ved å sjekke oppførselen til metoden for forskjellige lovlige og ulovlige verdier. For å sikre at løsningen er minst like rask som originalen, testet jeg flere ganger med utdraget nedenfor. Dette kan plasseres i ALSSuite klasse i gnist:

  test("Speed difference") {
    val (training, test) =
      genExplicitTestData(numUsers = 200, numItems = 400, rank = 2, noiseStd = 0.01)

    val runs = 100
    var totalTime = 0.0
    println("Performing "+runs+" runs")
    for(i <- 0 until runs) {
      val t0 = System.currentTimeMillis
      testALS(training, test, maxIter = 1, rank = 2, regParam = 0.01, targetRMSE = 0.1)
      val secs = (System.currentTimeMillis - t0)/1000.0
      println("Run "+i+" executed in "+secs+"s")
      totalTime += secs
    }
    println("AVG Execution Time: "+(totalTime/runs)+"s")

  }

Etter noen tester kan vi se at den nye løsningen er litt raskere enn originalen:

Kode

Antall løp

Total utførelsestid

Gjennomsnittlig utførelsestid per løp

original 100 588.458s 5.88458s
Fikset 100 566.722s 5.66722s

Jeg gjentok eksperimentene flere ganger for å bekrefte, og resultatene er konsistente. Her kan du finne detaljert utdata fra ett eksperiment for original kode og fastsette. Forskjellen er liten for et lite datasett, men tidligere har jeg klart å oppnå en merkbar reduksjon i GC-overhead ved hjelp av denne løsningen. Vi kan bekrefte dette ved å kjøre Spark lokalt og legge ved en Java-profil på Spark-forekomsten. Jeg åpnet en billett og en Trekk-forespørsel på den offisielle Spark repo men fordi det er usikkert om det blir slått sammen, tenkte jeg å dele det her med deg og den er nå en del av Spark 2.2.

Eventuelle tanker, kommentarer eller kritikk er velkomne! 🙂

Tidstempel:

Mer fra Datoboks