Вызов функции Java/Scala из задачи

Фон

Мой первоначальный вопрос: почему использование функции DecisionTreeModel.predict внутри карты вызывает исключение? и связано с Как генерировать кортежи (оригинальная маска, предсказанная метка) на Spark с MLlib?

Когда мы используем Scala API рекомендуемый способ получения прогнозов для RDD[LabeledPoint] с помощью DecisionTreeModel, это просто сопоставить более RDD:

val labelAndPreds = testData.map { point =>
  val prediction = model.predict(point.features)
  (point.label, prediction)
}

К сожалению, подобный подход в PySpark работает не так хорошо:

labelsAndPredictions = testData.map(
    lambda lp: (lp.label, model.predict(lp.features))
labelsAndPredictions.first()

Исключение: похоже, что вы пытаетесь ссылаться на SparkContext из широковещательной переменной, действия или трансформерации. SparkContext можно использовать только в драйвере, а не в коде, который он запускает на рабочих. Для получения дополнительной информации см. SPARK-5063.

Вместо официальная документация рекомендует что-то вроде этого:

predictions = model.predict(testData.map(lambda x: x.features))
labelsAndPredictions = testData.map(lambda lp: lp.label).zip(predictions)

Итак, что здесь происходит? Здесь нет переменной широковещания, а Scala API определяет predict следующим образом:

/**
 * Predict values for a single data point using the model trained.
 *
 * @param features array representing a single data point
 * @return Double prediction from the trained model
 */
def predict(features: Vector): Double = {
  topNode.predict(features)
}

/**
 * Predict values for the given data set using the model trained.
 *
 * @param features RDD representing data points to be predicted
 * @return RDD of predictions for each of the given data points
 */
def predict(features: RDD[Vector]): RDD[Double] = {
  features.map(x => predict(x))
}

поэтому, по крайней мере, на первый взгляд, вызов от действия или преобразования не является проблемой, поскольку предсказание кажется локальной операцией.

Объяснение

После некоторого рытья я понял, что источником проблемы является метод JavaModelWrapper.call, вызванный из DecisionTreeModel.predict. Это доступ SparkContext, который требуется для вызова функции Java:

callJavaFunc(self._sc, getattr(self._java_model, name), *a)

Вопрос

В случае DecisionTreeModel.predict существует рекомендуемое обходное решение, и весь требуемый код уже является частью API Scala, но есть ли элегантный способ справиться с такой проблемой вообще?

Единственные решения, о которых я сейчас думаю, - это тяжелый вес:

  • подталкивая все к JVM либо путем расширения классов Spark через Implicit Conversions, либо добавления каких-то оболочек
  • напрямую с помощью шлюза Py4j.

Ответ 1

Связь с использованием шлюза Py4J по умолчанию просто невозможна. Чтобы понять, почему мы должны взглянуть на следующую диаграмму из документа PySpark Internals [1]:

введите описание изображения здесь

Так как шлюз Py4J работает на драйвере, он недоступен для интерпретаторов Python, которые взаимодействуют с рабочими JVM через сокеты (см., например, PythonRDD/rdd.py).

Теоретически возможно создание отдельного шлюза Py4J для каждого рабочего, но на практике это вряд ли будет полезно. Игнорирование таких проблем, как надежность Py4J, просто не предназначено для выполнения задач, требующих интенсивной работы.

Есть ли способы обхода?

  • Используя Spark SQL Data Sources API, чтобы обернуть JVM-код.

    Преимущества. Поддерживаемый, высокий уровень, не требует доступа к внутреннему API PySpark

    Минусы. Относительно подробные и не очень хорошо документированные, ограниченные в основном входными данными.

  • Работа с DataFrames с использованием Scala UDF.

    Профи: легко реализовать (см. Искра: как сопоставить Python с Scala или функциями, определяемыми пользователем Java?), no преобразование данных между Python и Scala, если данные уже хранятся в DataFrame, минимальный доступ к Py4J

    Минусы: требуется доступ к шлюзу Py4J и внутренним методам, ограниченным Spark SQL, трудно отлаживать, не поддерживается

  • Создание интерфейса высокого уровня Scala аналогичным образом, как это делается в MLlib.

    Преимущества: гибкая, возможность выполнения произвольного сложного кода. Он может быть наложен непосредственно на RDD (см., Например, обертки модели MLlib) или с помощью DataFrames (см. Как использовать класс Scala внутри Pyspark). Последнее решение кажется гораздо более дружелюбным, поскольку все подробные данные уже обрабатываются существующим API.

    Минусы. Низкий уровень, требуемое преобразование данных, то же, что и UDF, требует доступа к Py4J и внутреннему API, не поддерживается

    Некоторые базовые примеры можно найти в Преобразование PySpark RDD с помощью Scala

  • Использование внешнего инструмента управления рабочим процессом для переключения между заданиями Python и Scala/Java и передачи данных в DFS.

    Преимущества: легко реализовать, минимальные изменения самого кода

    Минусы: стоимость чтения/записи данных (Alluxio?)

  • Использование общего SQLContext (см., например, Apache Zeppelin или Livy) для передачи данных между гостевыми языками с использованием зарегистрированных временных таблиц.

    Профи: хорошо подходит для интерактивного анализа

    Против: не столько для пакетных заданий (Zeppelin), либо может потребоваться дополнительная оркестровка (Livy)