Explorarea algoritmului de recomandare ALS al Spark PlatoBlockchain Data Intelligence. Căutare verticală. Ai.

Explorarea algoritmului de recomandare ALS al Spark

Algoritmul ALS introdus de Hu și colab., este o tehnică foarte populară folosită în problemele sistemului de recomandare, mai ales când avem seturi de date implicite (de exemplu clicuri, aprecieri etc.). Poate gestiona destul de bine volume mari de date și putem găsi multe implementări bune în diferite cadre de învățare automată. Spark include algoritmul în componenta MLlib, care a fost recent refactorizat pentru a îmbunătăți lizibilitatea și arhitectura codului.

Implementarea Spark necesită ca Elementul și ID-ul utilizatorului să fie numere în intervalul întreg (fie tip întreg, fie lung în intervalul întreg), ceea ce este rezonabil deoarece acest lucru poate ajuta la accelerarea operațiunilor și la reducerea consumului de memorie. Un lucru pe care l-am observat totuși în timp ce citesc codul este că acele coloane de id sunt turnate în duble și apoi în numere întregi la începutul metodelor de potrivire/predire. Acest lucru pare puțin hacker și am văzut că pune o presiune inutilă pe colectorul de gunoi. Iată rândurile de pe Cod ALS care aruncă id-urile în duble:
Explorarea algoritmului de recomandare ALS al Spark PlatoBlockchain Data Intelligence. Căutare verticală. Ai.
Explorarea algoritmului de recomandare ALS al Spark PlatoBlockchain Data Intelligence. Căutare verticală. Ai.

Pentru a înțelege de ce se face acest lucru, trebuie să citiți checkedCast():
Explorarea algoritmului de recomandare ALS al Spark PlatoBlockchain Data Intelligence. Căutare verticală. Ai.

Acest UDF primește un Double și își verifică intervalul și apoi îl transformă în număr întreg. Acest UDF este utilizat pentru validarea Schemei. Întrebarea este: putem realiza acest lucru fără a folosi piese turnate duble urâte? eu cred ca da:

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

Codul de mai sus arată un checkedCast() modificat care primește intrarea, verifică afirmă că valoarea este numerică și ridică excepții în caz contrar. Deoarece intrarea este Any, putem elimina în siguranță toate declarațiile cast to Double din restul codului. În plus, este rezonabil să ne așteptăm că, deoarece ALS necesită ID-uri în intervalul întreg, majoritatea oamenilor folosesc de fapt tipuri de numere întregi. Ca rezultat, pe linia 3, această metodă tratează în mod explicit numerele întregi pentru a evita orice turnare. Pentru toate celelalte valori numerice, verifică dacă intrarea este în intervalul întreg. Această verificare are loc pe linia 7.

S-ar putea scrie acest lucru diferit și s-ar putea gestiona în mod explicit toate tipurile permise. Din păcate, acest lucru ar duce la un cod duplicat. În schimb, ceea ce fac aici este să convertesc numărul în întreg și să-l compar cu numărul original. Dacă valorile sunt identice, una dintre următoarele este adevărată:

  1. Valoarea este Byte sau Short.
  2. Valoarea este lungă, dar în intervalul întreg.
  3. Valoarea este Double sau Float, dar fără nicio parte fracțională.

Pentru a mă asigura că codul rulează bine, l-am testat cu testele unitare standard ale Spark și manual, verificând comportamentul metodei pentru diferite valori legale și ilegale. Pentru a ne asigura că soluția este cel puțin la fel de rapidă ca cea originală, am testat de mai multe ori folosind fragmentul de mai jos. Acesta poate fi plasat în Clasa ALSSuite în Spark:

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

  }

După câteva teste, putem vedea că noua remediere este puțin mai rapidă decât cea originală:

Cod

Numărul de rulări

Timpul total de execuție

Timp mediu de execuție per rundă

Original 100 Anii 588.458 Anii 5.88458
Fixed 100 Anii 566.722 Anii 5.66722

Am repetat experimentele de mai multe ori pentru a confirma și rezultatele sunt consistente. Aici puteți găsi rezultatul detaliat al unui experiment pentru codul original si stabili. Diferența este mică pentru un set de date mic, dar în trecut am reușit să obțin o reducere vizibilă a supraîncărcării GC folosind această remediere. Putem confirma acest lucru rulând Spark local și atașând un profiler Java pe instanța Spark. am deschis un bilet și Pull-Request pe depozitul oficial Spark dar pentru că nu este sigur dacă va fi fuzionat, m-am gândit să vi-l împărtășesc aici și acum face parte din Spark 2.2.

Orice gânduri, comentarii sau critici sunt binevenite! 🙂

Timestamp-ul:

Mai mult de la Datumbox