Analisi dell'algoritmo ALS Recommendation di Spark PlatoBlockchain Data Intelligence. Ricerca verticale. Ai.

Esplorazione dell'algoritmo di raccomandazione ALS di Spark

L'algoritmo ALS introdotto da Hu et al., è una tecnica molto popolare utilizzata nei problemi del sistema di raccomandazione, in particolare quando abbiamo set di dati impliciti (ad esempio clic, Mi piace, ecc.). Può gestire grandi volumi di dati ragionevolmente bene e possiamo trovare molte buone implementazioni in vari framework di Machine Learning. Spark include l'algoritmo nel componente MLlib che è stato recentemente sottoposto a refactoring per migliorare la leggibilità e l'architettura del codice.

L'implementazione di Spark richiede che l'Id dell'articolo e dell'utente siano numeri all'interno dell'intervallo intero (sia Tipo intero o Lungo nell'intervallo intero), il che è ragionevole in quanto ciò può aiutare ad accelerare le operazioni e ridurre il consumo di memoria. Una cosa che ho notato, tuttavia, durante la lettura del codice è che quelle colonne id vengono lanciate in Doubles e poi in Integer all'inizio dei metodi fit / predict. Sembra un po 'confuso e l'ho visto mettere a dura prova il bidone della spazzatura. Ecco le linee sul Codice ALS che getta gli id ​​in doppio:
Analisi dell'algoritmo ALS Recommendation di Spark PlatoBlockchain Data Intelligence. Ricerca verticale. Ai.
Analisi dell'algoritmo ALS Recommendation di Spark PlatoBlockchain Data Intelligence. Ricerca verticale. Ai.

Per capire perché questo è fatto, è necessario leggere il checkCast ():
Analisi dell'algoritmo ALS Recommendation di Spark PlatoBlockchain Data Intelligence. Ricerca verticale. Ai.

Questo UDF riceve un doppio e controlla il suo intervallo, quindi lo lancia in numero intero. Questo UDF viene utilizzato per la convalida dello schema. La domanda è: possiamo raggiungere questo obiettivo senza usare i doppi castings brutti? Io credo di si:

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

Il codice sopra mostra un checkCast () modificato che riceve l'input, controlla che il valore sia numerico e solleva eccezioni in caso contrario. Poiché l'input è Any, possiamo rimuovere in modo sicuro tutte le istruzioni cast su Double dal resto del codice. Inoltre, è ragionevole aspettarsi che, poiché la SLA richiede ID all'interno dell'intervallo intero, la maggior parte delle persone utilizza effettivamente tipi interi. Di conseguenza alla riga 3 questo metodo gestisce esplicitamente Integer per evitare qualsiasi casting. Per tutti gli altri valori numerici controlla se l'input rientra nell'intervallo intero. Questo controllo avviene sulla linea 7.

Si potrebbe scrivere in modo diverso ed esplicitamente gestire tutti i tipi consentiti. Purtroppo questo porterebbe a un codice duplicato. Invece quello che faccio qui è convertire il numero in intero e confrontarlo con il numero originale. Se i valori sono identici, vale quanto segue:

  1. Il valore è Byte o Short.
  2. Il valore è Lungo ma all'interno dell'intervallo Intero.
  3. Il valore è Double o Float ma senza alcuna parte frazionaria.

Per garantire che il codice funzioni correttamente, l'ho testato con i test unitari standard di Spark e manualmente verificando il comportamento del metodo per vari valori legali e illegali. Per garantire che la soluzione sia almeno veloce come l'originale, ho provato più volte utilizzando lo snippet di seguito. Questo può essere inserito in Classe ALSSuite in Scintilla:

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

  }

Dopo alcuni test possiamo vedere che la nuova correzione è leggermente più veloce dell'originale:

Code

Numero di corse

Tempo di esecuzione totale

Tempo medio di esecuzione per corsa

Originale 100 588.458 secondi 5.88458 secondi
Fissa 100 566.722 secondi 5.66722 secondi

Ho ripetuto gli esperimenti più volte per confermare e i risultati sono coerenti. Qui puoi trovare l'output dettagliato di un esperimento per il codice originale e la fisso. La differenza è piccola per un piccolo set di dati, ma in passato sono riuscito a ottenere una notevole riduzione delle spese generali del GC usando questa correzione. Possiamo confermarlo eseguendo Spark localmente e collegando un profiler Java sull'istanza Spark. Ho aperto a biglietto e Richiesta pull sul repository Spark ufficiale ma poiché non è sicuro se verrà unito, ho pensato di condividerlo qui con te ed è ora parte di Spark 2.2.

Qualsiasi pensiero, commento o critica sono i benvenuti! 🙂

Timestamp:

Di più da Databox