Inferencia de ONNX en Spark

ONNX (Open Neural Network Exchange) proporciona un entorno de ejecución portátil optimizado para hardware para modelos de aprendizaje automático. Al convertir un modelo en formato ONNX, puede ejecutar la inferencia por lotes en Spark con menor latencia y sin depender del marco de entrenamiento original en el momento de la predicción.

En este artículo, entrena un modelo LightGBM con SynapseML, lo convierte al formato ONNX y, a continuación, usa el modelo ONNX para realizar inferencias en Spark en Microsoft Fabric.

Prerrequisitos

  • Adjunte su cuaderno a un almacén de lago de datos. En el lado izquierdo del cuaderno, seleccione Agregar para agregar un lago existente o crear uno.
  • Fabric Runtime 1.2 o posterior.

Instalación de paquetes necesarios

Ejecute la siguiente celda en el cuaderno para instalar los paquetes necesarios. El paquete onnxmltools no está preinstalado en el entorno de ejecución de Fabric.

%pip install onnxmltools --quiet

Una vez completada la instalación, compruebe que los paquetes están disponibles:

import onnxmltools
import lightgbm
print(f"onnxmltools version: {onnxmltools.__version__}")
print(f"lightgbm version: {lightgbm.__version__}")

Note

El paquete lightgbm está preinstalado en Fabric Runtime 1.2 y versiones posteriores. Solo tiene que instalar onnxmltools.

Carga de los datos de ejemplo

Cargue el conjunto de datos de predicción de quiebras del Azure Blob Storage público:

df = (
    spark.read.format("csv")
    .option("header", True)
    .option("inferSchema", True)
    .load(
        "wasbs://publicwasb@mmlspark.blob.core.windows.net/company_bankruptcy_prediction_data.csv"
    )
)

print(f"Rows: {df.count()}, Columns: {len(df.columns)}")
display(df.limit(5))

La tabla mostrada incluye columnas como:

¿En quiebra? Indicador de ingresos netos Equidad en responsabilidad
0 1,0 0.0165
0 1,0 0.0208

Entrena un modelo LightGBM

Use el VectorAssembler para combinar columnas de características y, a continuación, entrene un LightGBMClassifier:

from pyspark.ml.feature import VectorAssembler
from synapse.ml.lightgbm import LightGBMClassifier

feature_cols = df.columns[1:]
featurizer = VectorAssembler(inputCols=feature_cols, outputCol="features")

train_data = featurizer.transform(df)["Bankrupt?", "features"]

model = (
    LightGBMClassifier(featuresCol="features", labelCol="Bankrupt?")
    .setDataTransferMode("bulk")
    .setEarlyStoppingRound(300)
    .setLambdaL1(0.5)
    .setNumIterations(1000)
    .setNumThreads(-1)
    .setMaxDeltaStep(0.5)
    .setNumLeaves(31)
    .setMaxDepth(-1)
    .setBaggingFraction(0.7)
    .setFeatureFraction(0.7)
    .setBaggingFreq(2)
    .setObjective("binary")
    .setIsUnbalance(True)
    .setMinSumHessianInLeaf(20)
    .setMinGainToSplit(0.01)
)

model = model.fit(train_data)

Compruebe que el modelo se entrenó correctamente:

print(f"Model type: {type(model).__name__}")
print(f"Number of features: {len(feature_cols)}")

Convertir el modelo en formato ONNX

Exporte el modelo entrenado a un booster de LightGBM y, a continuación, conviértalo a ONNX:

import lightgbm as lgb
from typing import Union
from lightgbm import Booster, LGBMClassifier
from onnxmltools.convert import convert_lightgbm
from onnxmltools.convert.common.data_types import FloatTensorType


def convert_to_onnx(lgbm_model: Union[LGBMClassifier, Booster], input_size: int) -> bytes:
    initial_types = [("input", FloatTensorType([-1, input_size]))]
    onnx_model = convert_lightgbm(
        lgbm_model, initial_types=initial_types, target_opset=13
    )
    return onnx_model.SerializeToString()


booster_model_str = model.getLightGBMBooster().modelStr().get()
booster = lgb.Booster(model_str=booster_model_str)
model_payload_ml = convert_to_onnx(booster, len(feature_cols))

Compruebe que la conversión de ONNX se realizó correctamente:

print(f"ONNX model payload size: {len(model_payload_ml)} bytes")
assert len(model_payload_ml) > 0, "ONNX conversion failed: empty payload"

La salida muestra el tamaño de carga del modelo ONNX en bytes (normalmente alrededor de 800 000 bytes).

Importante

Use from onnxmltools.convert.common.data_types import FloatTensorType para la definición de tipo. La antigua ruta de importación from onnxconverter_common.data_types import FloatTensorType no es compatible con las versiones actuales de onnxmltools.

Carga y configuración del modelo ONNX

Cargue la carga de ONNX en synapseML ONNXModel e inspeccione las entradas y salidas del modelo:

from synapse.ml.onnx import ONNXModel

onnx_ml = ONNXModel().setModelPayload(model_payload_ml)

print("Model inputs:" + str(onnx_ml.getModelInputs()))
print("Model outputs:" + str(onnx_ml.getModelOutputs()))

La salida enumera los nodos de entrada y salida del modelo.

Configure el modelo mediante la asignación de columnas de entrada y salida. El FeedDict asigna los nombres de entrada del modelo ONNX a las columnas del DataFrame. El FetchDict asigna los nombres deseados de las columnas de salida a los nombres de salida del modelo ONNX:

onnx_ml = (
    onnx_ml.setDeviceType("CPU")
    .setFeedDict({"input": "features"})
    .setFetchDict({"probability": "probabilities", "prediction": "label"})
    .setMiniBatchSize(5000)
)

Ejecución de la inferencia

Crear datos de prueba y transformarlos a través del modelo ONNX:

from pyspark.ml.feature import VectorAssembler
import pandas as pd
import numpy as np

n = 10000
m = 95
test = np.random.rand(n, m)
testPdf = pd.DataFrame(test)
cols = list(map(str, testPdf.columns))
testDf = spark.createDataFrame(testPdf)
testDf = testDf.repartition(4)
testDf = (
    VectorAssembler()
    .setInputCols(cols)
    .setOutputCol("features")
    .transform(testDf)
    .drop(*cols)
    .cache()
)

display(onnx_ml.transform(testDf))

Note

Dado que los datos de prueba se generan aleatoriamente, los valores de predicción no representan resultados reales. En esta sección se muestra que el modelo ONNX se ejecuta correctamente en Spark.

La salida debe contener columnas para features, predictiony probability:

Características prediction probabilidad
{"type":1,"values":[0.105... 0 {"0":0.835...
{"type":1,"values":[0.814... 0 {"0":0.658...

Compruebe los resultados generados por la inferencia:

results = onnx_ml.transform(testDf)
print(f"Result count: {results.count()}")
print(f"Output columns: {results.columns}")
assert "prediction" in results.columns, "Missing prediction column"
assert "probability" in results.columns, "Missing probability column"

La salida confirma que todas las filas de prueba se puntuaron y el resultado DataFrame contiene las featurescolumnas , predictiony probability .

Solución de problemas

Cuestión Causa Solución
ModuleNotFoundError: No module named 'onnxmltools' El paquete no está preinstalado en Fabric runtime. Ejecute %pip install onnxmltools --quiet y reinicie el kernel de Python.
RuntimeError: Operator LgbmClassifier got an input with a wrong type Ruta de importación incorrecta para FloatTensorType. Use from onnxmltools.convert.common.data_types import FloatTensorType en lugar de importar desde onnxconverter_common.data_types.
ModuleNotFoundError: No module named 'onnx.mapping' La versión onnxmltools 1.7.0 o anterior es incompatible con el paquete actual onnx. Ejecute %pip install onnxmltools --upgrade --quiet para instalar una versión compatible.
ONNX conversion returns empty payload Falló la extracción de cadenas del modelo booster. Compruebe que model.getLightGBMBooster().modelStr().get() devuelve una cadena no vacía antes de la conversión.
Feature (Column_) appears more than one time durante model.fit() Las columnas del conjunto de datos con caracteres especiales generan nombres duplicados después del saneamiento de LightGBM. Agregue .setDataTransferMode("bulk") a la LightGBMClassifier configuración. El modo de procesamiento masivo usa Apache Arrow y evita el problema de normalización de los nombres de columna.
AssertionError en SparkContext en ONNXModel() La sesión de Spark no se inicializa. Ejecute este código en un cuaderno de Fabric con una casa de lago conectada. La variable spark se inicializa previamente en el entorno de ejecución.

Limpieza de recursos

Si ya no necesita el DataFrame de prueba almacenado en caché, despersistelo para liberar memoria del clúster:

testDf.unpersist()