Profundización en el algoritmo de recomendación ALS de Spark PlatoBlockchain Data Intelligence. Búsqueda vertical. Ai.

Profundizando en el algoritmo de recomendación ALS de Spark

El algoritmo ALS introducido por Hu y col., es una técnica muy popular utilizada en problemas del Sistema de recomendación, especialmente cuando tenemos conjuntos de datos implícitos (por ejemplo, clics, me gusta, etc.). Puede manejar grandes volúmenes de datos razonablemente bien y podemos encontrar muchas implementaciones buenas en varios marcos de Machine Learning. Spark incluye el algoritmo en el componente MLlib que recientemente ha sido refactorizado para mejorar la legibilidad y la arquitectura del código.

La implementación de Spark requiere que el ítem y la identificación del usuario sean números dentro del rango entero (ya sea tipo entero o largo dentro del rango entero), lo cual es razonable ya que esto puede ayudar a acelerar las operaciones y reducir el consumo de memoria. Sin embargo, una cosa que noté al leer el código es que esas columnas de identificación se están convirtiendo en Dobles y luego en Enteros al comienzo de los métodos de ajuste / predicción. Esto parece un poco extraño y lo he visto presionar innecesariamente al recolector de basura. Aquí están las líneas en el Código ALS que convierten los identificadores en dobles:
Profundización en el algoritmo de recomendación ALS de Spark PlatoBlockchain Data Intelligence. Búsqueda vertical. Ai.
Profundización en el algoritmo de recomendación ALS de Spark PlatoBlockchain Data Intelligence. Búsqueda vertical. Ai.

Para entender por qué se hace esto, uno necesita leer el CheckCast ():
Profundización en el algoritmo de recomendación ALS de Spark PlatoBlockchain Data Intelligence. Búsqueda vertical. Ai.

Este UDF recibe un Doble y comprueba su rango y luego lo convierte en entero. Este UDF se utiliza para la validación del esquema. La pregunta es: ¿podemos lograr esto sin usar doble fundición fea? Yo creo que 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.")
    }
  }

El código anterior muestra un checkCast modificado () que recibe la entrada, comprueba que el valor es numérico y, de lo contrario, genera excepciones. Como la entrada es Cualquiera, podemos eliminar de forma segura todas las declaraciones de conversión a Doble del resto del código. Además, es razonable esperar que dado que el ALS requiere identificadores dentro del rango de enteros, la mayoría de las personas realmente usan tipos enteros. Como resultado en la línea 3, este método maneja los números enteros explícitamente para evitar hacer cualquier conversión. Para todos los demás valores numéricos, verifica si la entrada está dentro del rango entero. Esta verificación ocurre en la línea 7.

Uno podría escribir esto de manera diferente y manejar explícitamente todos los tipos permitidos. Lamentablemente, esto conduciría a un código duplicado. En cambio, lo que hago aquí es convertir el número a Entero y compararlo con el Número original. Si los valores son idénticos, se cumple uno de los siguientes:

  1. El valor es Byte o Short.
  2. El valor es largo pero dentro del rango entero.
  3. El valor es Double o Float pero sin ninguna parte fraccional.

Para garantizar que el código funciona bien, lo probé con las pruebas unitarias estándar de Spark y manualmente comprobando el comportamiento del método para varios valores legales e ilegales. Para garantizar que la solución sea al menos tan rápida como la original, probé varias veces con el fragmento a continuación. Esto se puede colocar en el Clase ALSSuite en chispa:

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

  }

Después de algunas pruebas, podemos ver que la nueva solución es un poco más rápida que la original:

Código

Numero de carreras

Tiempo total de ejecución

Tiempo medio de ejecución por ejecución

Original 100 Los 588.458s Los 5.88458s
fijo 100 Los 566.722s Los 5.66722s

Repetí los experimentos varias veces para confirmar y los resultados son consistentes. Aquí puede encontrar el resultado detallado de un experimento para el código original y del reparar. La diferencia es pequeña para un conjunto de datos diminuto, pero en el pasado logré lograr una reducción notable en la sobrecarga de GC con esta solución. Podemos confirmar esto ejecutando Spark localmente y adjuntando un generador de perfiles de Java en la instancia de Spark. Abrí un boleto y Solicitud de extracción en el repositorio oficial de Spark pero como no está claro si se fusionará, pensé en compartirlo aquí con ustedes y ahora es parte de Spark 2.2.

¡Cualquier pensamiento, comentario o crítica son bienvenidos! 🙂

Sello de tiempo:

Mas de Caja de datos