Tutustutaan Sparkin ALS-suositusalgoritmiin PlatoBlockchain Data Intelligence. Pystysuuntainen haku. Ai.

Poraus Sparkin ALS-suositusalgoritmiin

ALS-algoritmi, jonka esitteli Hu et ai., on erittäin suosittu tekniikka, jota käytetään Recommender System -ongelmissa, varsinkin kun meillä on implisiittisiä tietojoukkoja (esimerkiksi napsautuksia, tykkäyksiä jne.). Se pystyy käsittelemään suuria tietomääriä kohtuullisen hyvin ja voimme löytää monia hyviä toteutuksia erilaisista koneoppimiskehyksistä. Spark sisältää algoritmin MLlib-komponentissa, joka on hiljattain muutettu parantamaan koodin luettavuutta ja arkkitehtuuria.

Sparkin toteutus edellyttää, että kohteen ja käyttäjätunnuksen on oltava kokonaislukualueella olevia numeroita (joko kokonaislukutyyppiä tai pitkää kokonaislukualueella), mikä on järkevää, koska tämä voi nopeuttaa toimintoja ja vähentää muistin kulutusta. Eräs asia, jonka huomasin kuitenkin koodia lukiessani, on, että nuo id-sarakkeet heitetään tuplauksiin ja sitten kokonaislukuihin sovitus/ennustusmenetelmien alussa. Tämä vaikuttaa hieman hakkivalta ja olen nähnyt sen rasittavan tarpeettomasti roskakoria. Tässä ovat rivit ALS-koodi joka heittää tunnukset tuplaan:
Tutustutaan Sparkin ALS-suositusalgoritmiin PlatoBlockchain Data Intelligence. Pystysuuntainen haku. Ai.
Tutustutaan Sparkin ALS-suositusalgoritmiin PlatoBlockchain Data Intelligence. Pystysuuntainen haku. Ai.

Ymmärtääksesi miksi tämä tehdään, sinun on luettava checkedCast():
Tutustutaan Sparkin ALS-suositusalgoritmiin PlatoBlockchain Data Intelligence. Pystysuuntainen haku. Ai.

Tämä UDF vastaanottaa tuplauksen ja tarkistaa sen alueen ja lähettää sen sitten kokonaisluvuksi. Tätä UDF:ää käytetään skeeman validointiin. Kysymys kuuluu, voimmeko saavuttaa tämän ilman rumia kaksoisvaluja? uskon kyllä:

  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.")
    }
  }

Yllä oleva koodi näyttää muokatun checkedCast():n, joka vastaanottaa syötteen, tarkistaa, että arvo on numeerinen ja aiheuttaa poikkeuksia muuten. Koska syöte on mikä tahansa, voimme turvallisesti poistaa kaikki cast to Double -lausekkeet muusta koodista. Lisäksi on kohtuullista olettaa, että koska ALS vaatii id:t kokonaislukualueen sisällä, suurin osa ihmisistä käyttää itse asiassa kokonaislukutyyppejä. Tämän seurauksena rivillä 3 tämä menetelmä käsittelee kokonaislukuja nimenomaisesti, jotta vältetään heitto. Kaikille muille numeerisille arvoille se tarkistaa, onko syöte kokonaislukualueella. Tämä tarkistus tapahtuu rivillä 7.

Tämän voisi kirjoittaa eri tavalla ja käsitellä kaikkia sallittuja tyyppejä. Valitettavasti tämä johtaisi kaksoiskoodiin. Sen sijaan teen tässä luvun muuntamisen kokonaisluvuksi ja vertaan sitä alkuperäiseen numeroon. Jos arvot ovat samat, yksi seuraavista on tosi:

  1. Arvo on tavu tai lyhyt.
  2. Arvo on pitkä, mutta kokonaislukualueen sisällä.
  3. Arvo on Double tai Float, mutta ilman murto-osaa.

Varmistaakseni koodin toimivuuden testasin sitä Sparkin vakioyksikkötesteillä ja manuaalisesti tarkistamalla menetelmän käyttäytyminen erilaisten laillisten ja laittomien arvojen varalta. Varmistaakseni, että ratkaisu on vähintään yhtä nopea kuin alkuperäinen, testasin useita kertoja alla olevan katkelman avulla. Tämä voidaan sijoittaa ALSSuite-luokka Sparkissa:

  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")

  }

Muutaman testin jälkeen voimme nähdä, että uusi korjaus on hieman nopeampi kuin alkuperäinen:

Koodi

Ajojen määrä

Toteutusaika yhteensä

Keskimääräinen suoritusaika ajoa kohti

Alkuperäinen 100 588.458s 5.88458s
kiinteä 100 566.722s 5.66722s

Toistin kokeet useita kertoja varmistaakseni ja tulokset ovat yhdenmukaisia. Täältä löydät yksityiskohtaisen tuloksen yhdestä kokeesta alkuperäinen koodi ja korjata. Ero on pieni pienelle tietojoukolle, mutta aiemmin olen onnistunut vähentämään GC:n yleiskustannuksia huomattavasti käyttämällä tätä korjausta. Voimme varmistaa tämän suorittamalla Sparkin paikallisesti ja liittämällä Java-profiilin Spark-instanssiin. Avasin a lippu ja Vedä-pyyntö virallisessa Spark repossa mutta koska on epävarmaa, yhdistetäänkö se, ajattelin jakaa sen täällä kanssasi ja se on nyt osa Spark 2.2:ta.

Kaikki ajatukset, kommentit tai kritiikki ovat tervetulleita! 🙂

Aikaleima:

Lisää aiheesta Datumbox