Debezium Daily Sharing Series: uso de flujos de datos en bases de datos para el aprendizaje automático en línea

1. Introducción a los antecedentes

  • Utilice Debezium para crear múltiples flujos de datos a partir de la base de datos y utilice un flujo para aprender y mejorar continuamente nuestro modelo, y el segundo flujo para hacer predicciones sobre los datos.
  • Cuando un modelo mejora o se ajusta continuamente para adaptarse a las muestras de datos más recientes, este enfoque se denomina aprendizaje automático en línea. El aprendizaje en línea solo es adecuado para ciertos casos de uso, y la implementación de variaciones en línea de un algoritmo determinado puede resultar desafiante o incluso imposible. Sin embargo, cuando el aprendizaje en línea es posible, se convierte en una herramienta muy poderosa porque permite reaccionar a los cambios en los datos en tiempo real y evitar la necesidad de volver a entrenar y reimplementar nuevos modelos, ahorrando así tiempo.
  • Costos de hardware y operación. A medida que los flujos de datos se vuelven más frecuentes, por ejemplo con la llegada del Internet de las cosas, se puede esperar que el aprendizaje en línea se vuelva cada vez más popular. Generalmente es muy adecuado para analizar datos de transmisión en posibles casos de uso.
  • El objetivo no es construir el mejor modelo para un caso de uso determinado, sino ver cómo construir un proceso completo desde la inserción de datos en la base de datos hasta pasar los datos al modelo y usarlos para el entrenamiento y la predicción del modelo. Para simplificar, se utilizará otra muestra de datos conocida que se utiliza a menudo en tutoriales de aprendizaje automático. Explorará cómo clasificar varias flores de iris utilizando una variante en línea del algoritmo de agrupamiento k-means. Utilice Apache Flink y Apache Spark para procesar flujos de datos. Ambos marcos son marcos de procesamiento de datos muy populares y contienen una biblioteca de aprendizaje automático que, entre otras cosas, implementa el algoritmo k-means en línea. Por lo tanto, podemos centrarnos en construir una canalización completa que pase datos de la base de datos a un modelo determinado, procesándolos en tiempo real, en lugar de tener que lidiar con los detalles de implementación del algoritmo.

2. Preparación del conjunto de datos

  • Utilizará el conjunto de datos de flores de iris y el objetivo es determinar la especie de flor de iris en función de varias medidas de la flor de iris: longitud del sépalo, ancho del sépalo, longitud del pétalo y ancho del pétalo.

Insertar descripción de la imagen aquí

  • El conjunto de datos se puede descargar de varias fuentes. Se puede aprovechar el hecho de que ya está preprocesado, p. kit de herramientas scikit-learn y úselo desde allí. Cada fila de muestra contiene un punto de datos (longitud del sépalo, ancho del sépalo, longitud del pétalo y ancho del pétalo) y una etiqueta. La etiqueta es el número 0, 1 o 2, donde 0 representa Iris setosa, 1 representa Iris versicolor y 2 representa Iris virginica. El conjunto de datos es pequeño: sólo 150 puntos de datos.
  • Al cargar datos en la base de datos, primero se prepara el archivo SQL y luego se pasa a la base de datos. La muestra de datos original debe dividirse en tres submuestras: dos para entrenamiento y una para prueba. La capacitación inicial utilizará la primera muestra de datos de capacitación. Esta muestra de datos es intencionalmente pequeña para no producir buenas predicciones la primera vez que se prueba el modelo, de modo que pueda ver cómo las predicciones del modelo aumentarán en tiempo real a medida que se agreguen más datos al modelo.
  • Los tres archivos SQL se pueden generar utilizando el siguiente script de Python del repositorio de demostración adjunto.
$ ./iris2sql.py
  • train1.sql se carga automáticamente en la base de datos de Postgres al inicio. test.sql y train2.sql se cargarán manualmente en la base de datos más adelante.

3. Utilice Apache Flink para la clasificación.

Primero, echemos un vistazo a cómo realizar la clasificación y el aprendizaje del iris en línea en Apache Flink. La siguiente figura muestra la arquitectura de alto nivel de todo el oleoducto.

Insertar descripción de la imagen aquí
Usaremos Postgres como nuestra base de datos fuente. Debezium se implementa como un conector de origen de Kafka Connect, rastrea los cambios en la base de datos y crea un flujo de datos a partir de los datos recién insertados en Kafka. Kafka envía estas transmisiones a Apache Flink, que utiliza el algoritmo de transmisión k-means para el ajuste del modelo y la clasificación de datos. Las predicciones para el modelo de flujo de datos de prueba se generan como otro flujo y se envían de regreso a Kafka.

Nuestra base de datos contiene dos tablas. El primero almacena nuestros datos de entrenamiento y el segundo almacena los datos de prueba. Por lo tanto, hay dos flujos de datos, uno para cada tabla: uno para el aprendizaje y otro para los puntos de datos que se van a clasificar. En aplicaciones reales, sólo se puede utilizar una tabla o, por el contrario, se pueden utilizar más tablas. Se pueden implementar aún más conectores Debezium para fusionar datos de múltiples bases de datos.

4. Utilice Debezium y Kafka como flujos de datos de origen

Apache Flink tiene una excelente integración con Kafka. Se pueden pasar registros de Debezium, como registros JSON. Para crear tablas Flink, incluso admite el formato de registro de Debezium, pero para las transmisiones, es necesario extraer parte del mensaje de Debezium, que contiene las filas recién almacenadas en la tabla. Sin embargo, esto es muy fácil porque Debezium proporciona SMT, que extrae SMT del nuevo estado de registro, y hace exactamente eso. La configuración completa de Debezium se ve así:

{
    
    
    "name": "iris-connector-flink",
    "config": {
    
    
        "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
        "tasks.max": "1",
        "database.hostname": "postgres",
        "database.port": "5432",
        "database.user": "postgres",
        "database.password": "postgres",
        "database.dbname" : "postgres",
        "topic.prefix": "flink",
        "table.include.list": "public.iris_.*",
        "key.converter": "org.apache.kafka.connect.json.JsonConverter",
        "value.converter": "org.apache.kafka.connect.json.JsonConverter",
        "transforms": "unwrap",
        "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState"
    }
}

Esta configuración captura todas las tablas del esquema común que contienen tablas que comienzan con el prefijo iris_. Dado que los datos de entrenamiento y prueba se almacenan en dos tablas, se crean dos temas de Kafka denominados flink.public.iris_train y flink.public.iris_test respectivamente. DataStreamSource de Flink representa el flujo de datos entrante. Cuando un registro está codificado como JSON, será una secuencia de objetos JSON ObjectNode. Construir el flujo fuente es muy simple:

KafkaSource<ObjectNode> train = KafkaSource.<ObjectNode>builder()
    .setBootstrapServers("kafka:9092")
    .setTopics("flink.public.iris_train")
    .setClientIdPrefix("train")
    .setGroupId("dbz")
    .setStartingOffsets(OffsetsInitializer.earliest())
    .setDeserializer(KafkaRecordDeserializationSchema.of(new JSONKeyValueDeserializationSchema(false)))
    .build();
DataStreamSource<ObjectNode> trainStream = env.fromSource(train, WatermarkStrategy.noWatermarks(), "Debezium train");

Flink se ejecuta principalmente en el objeto abstracto Tabla. Además, los modelos de aprendizaje automático solo aceptan tablas como entrada y las predicciones también se generan en forma tabular. Por lo tanto, el flujo de entrada primero debe convertirse en un objeto Tabla. Primero convierta el flujo de datos de entrada en un flujo de filas de tabla. Debe definir una función de mapeo que devolverá un objeto Fila que contiene un vector de puntos de datos. Dado que el algoritmo k-means es un algoritmo de aprendizaje no supervisado, es decir, el modelo no requiere la "respuesta correcta" correspondiente al punto de datos, el campo de etiqueta se puede omitir del vector:

private static class RecordMapper implements MapFunction<ObjectNode, Row> {
    
    
    @Override
    public Row map(ObjectNode node) {
    
    
        JsonNode payload = node.get("value").get("payload");
        StringBuffer sb = new StringBuffer();
        return Row.of(Vectors.dense(
                        payload.get("sepal_length").asDouble(),
                        payload.get("sepal_width").asDouble(),
                        payload.get("petal_length").asDouble(),
                        payload.get("petal_width").asDouble()));
    }
}

Varias partes de la canalización interna de Flink se pueden ejecutar en diferentes nodos trabajadores, por lo que también es necesario proporcionar información de tipo sobre la tabla. Esto creará el objeto de tabla:

StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
TypeInformation<?>[] types = {
    
    DenseVectorTypeInfo.INSTANCE};
String names[] = {
    
    "features"};
RowTypeInfo typeInfo = new RowTypeInfo(types, names);

DataStream<Row> inputStream = trainStream.map(new RecordMapper()).returns(typeInfo);
Table trainTable = tEnv.fromDataStream(inputStream).as("features");

5. Construya la secuencia k-means de Flink

Una vez que tenga un objeto Tabla, puede pasarlo al modelo. Así que cree uno y pásele un flujo de capacitación para el entrenamiento continuo del modelo:

OnlineKMeans onlineKMeans = new OnlineKMeans()
    .setFeaturesCol("features")
    .setPredictionCol("prediction")
    .setInitialModelData(tEnv.fromDataStream(env.fromElements(1).map(new IrisInitCentroids())))
    .setK(3);
OnlineKMeansModel model = onlineKMeans.fit(trainTable);

Para simplificar las cosas, establezca directamente el número de clústeres requeridos en 3 en lugar de extraer los datos (por ejemplo, utilizando el método del codo) para encontrar el número óptimo de clústeres. También establezca algunos valores iniciales para los centros de los grupos en lugar de usar números aleatorios (Flink proporciona un método conveniente: KMeansModelData.generateRandomModelData() si desea intentar usar centros aleatorios).

Para obtener predicciones sobre los datos de prueba, el flujo de prueba nuevamente debe convertirse en una tabla. El modelo convierte una tabla que contiene datos de prueba en una tabla que contiene predicciones. Finalmente, convierta la predicción en una secuencia y guárdela, por ejemplo en un tema de Kafka:

DataStream<Row> testInputStream = testStream.map(new RecordMapper()).returns(typeInfo);
Table testTable = tEnv.fromDataStream(testInputStream).as("features");
Table outputTable = model.transform(testTable)[0];

DataStream<Row> resultStream = tEnv.toChangelogStream(outputTable);
resultStream.map(new ResultMapper()).sinkTo(kafkaSink);

Ahora, la aplicación está lista para ser construida y casi lista para ser enviada a Flink para su ejecución. Antes de hacer esto, debe crear el tema de Kafka requerido. Aunque los temas pueden estar vacíos, Flink requiere que al menos existan. Debido a que una pequeña porción de los datos se incluye en la tabla de entrenamiento de Postgres cuando se inicia la base de datos, Debezium crea el tema correspondiente cuando registra el conector Debezium Postgres en Kafka Connect. Dado que la tabla de datos de prueba aún no existe, el tema debe crearse manualmente en Kafka:

$ docker compose -f docker-compose-flink.yaml exec kafka /kafka/bin/kafka-topics.sh --create --bootstrap-server kafka:9092 --replication-factor 1 --partitions 1  --topic flink.public.iris_test

Ahora está listo para enviar su solicitud a Flink.

Si no está utilizando Docker Compose proporcionado en el código fuente de esta demostración, incluya la biblioteca Flink ML en la carpeta Flink lib, ya que la biblioteca ML no forma parte de la distribución predeterminada de Flink.

Flink proporciona una interfaz de usuario amigable, que se puede encontrar en http://localhost:8081/. Allí, entre otras cosas, podrás consultar el estado de tus trabajos, como el plan de ejecución del trabajo en una excelente representación gráfica:

Insertar descripción de la imagen aquí

6. Modelo de evaluación

Desde la perspectiva del usuario, todas las interacciones con el modelo se producen insertando nuevos registros en la base de datos o leyendo temas de Kafka con predicciones. Dado que ya se crea una muestra de datos de entrenamiento inicial muy pequeña cuando se inicia la base de datos, las predicciones del modelo se pueden verificar directamente insertando una muestra de datos de prueba en la base de datos:

$ psql -h localhost -U postgres -f postgres/iris_test.sql

La inserción genera un flujo instantáneo de datos de prueba en Kafka, los pasa al modelo y envía las predicciones de regreso al tema iris_predictions de Kafka. Cuando se entrena el modelo en un conjunto de datos muy pequeño con solo dos grupos, las predicciones son inexactas. El siguiente cuadro muestra nuestro pronóstico preliminar:

[5.4, 3.7, 1.5, 0.2] is classified as 0
[4.8, 3.4, 1.6, 0.2] is classified as 0
[7.6, 3.0, 6.6, 2.1] is classified as 2
[6.4, 2.8, 5.6, 2.2] is classified as 2
[6.0, 2.7, 5.1, 1.6] is classified as 2
[5.4, 3.0, 4.5, 1.5] is classified as 2
[6.7, 3.1, 4.7, 1.5] is classified as 2
[5.5, 2.4, 3.8, 1.1] is classified as 2
[6.1, 2.8, 4.7, 1.2] is classified as 2
[4.3, 3.0, 1.1, 0.1] is classified as 0
[5.8, 2.7, 3.9, 1.2] is classified as 2

En nuestro ejemplo, la respuesta correcta sería:

[5.4, 3.7, 1.5, 0.2] is 0
[4.8, 3.4, 1.6, 0.2] is 0
[7.6, 3.0, 6.6, 2.1] is 2
[6.4, 2.8, 5.6, 2.2] is 2
[6.0, 2.7, 5.1, 1.6] is 1
[5.4, 3.0, 4.5, 1.5] is 1
[6.7, 3.1, 4.7, 1.5] is 1
[5.5, 2.4, 3.8, 1.1] is 1
[6.1, 2.8, 4.7, 1.2] is 1
[4.3, 3.0, 1.1, 0.1] is 0
[5.8, 2.7, 3.9, 1.2] is 1

Al comparar los resultados, solo 5 de 11 puntos de datos se clasificaron correctamente debido al tamaño de los datos de entrenamiento de la muestra inicial. Por otro lado, dado que no se comienza con grupos completamente aleatorios, las predicciones tampoco son completamente erróneas.

¿Qué sucede cuando el modelo recibe más datos de entrenamiento?

$ psql -h localhost -U postgres -f postgres/iris_train2.sql

Para ver las predicciones actualizadas, inserte nuevamente la misma muestra de datos de prueba en la base de datos:

 psql -h localhost -U postgres -f postgres/iris_test.sql

Las siguientes predicciones son mucho mejores ya que se han proporcionado las tres categorías. También clasificó correctamente 7 de 11 puntos de datos.

[5.4, 3.7, 1.5, 0.2] is classified as 0
[4.8, 3.4, 1.6, 0.2] is classified as 0
[7.6, 3.0, 6.6, 2.1] is classified as 2
[6.4, 2.8, 5.6, 2.2] is classified as 2
[6.0, 2.7, 5.1, 1.6] is classified as 2
[5.4, 3.0, 4.5, 1.5] is classified as 2
[6.7, 3.1, 4.7, 1.5] is classified as 2
[5.5, 2.4, 3.8, 1.1] is classified as 1
[6.1, 2.8, 4.7, 1.2] is classified as 2
[4.3, 3.0, 1.1, 0.1] is classified as 0
[5.8, 2.7, 3.9, 1.2] is classified as 1

Dado que toda la muestra de datos es muy pequeña, para un mayor entrenamiento del modelo, se puede reutilizar la segunda muestra de datos de entrenamiento:

$ psql -h localhost -U postgres -f postgres/iris_train2.sql
$ psql -h localhost -U postgres -f postgres/iris_test.sql

Esto lleva a la siguiente predicción.

[5.4, 3.7, 1.5, 0.2] is classified as 0
[4.8, 3.4, 1.6, 0.2] is classified as 0
[7.6, 3.0, 6.6, 2.1] is classified as 2
[6.4, 2.8, 5.6, 2.2] is classified as 2
[6.0, 2.7, 5.1, 1.6] is classified as 2
[5.4, 3.0, 4.5, 1.5] is classified as 1
[6.7, 3.1, 4.7, 1.5] is classified as 2
[5.5, 2.4, 3.8, 1.1] is classified as 1
[6.1, 2.8, 4.7, 1.2] is classified as 1
[4.3, 3.0, 1.1, 0.1] is classified as 0
[5.8, 2.7, 3.9, 1.2] is classified as 1

Ahora se ha descubierto que 9 de 11 puntos de datos están clasificados correctamente. Si bien este todavía no es un resultado estelar, se espera que el resultado sea solo parcialmente exacto ya que es solo una predicción. La motivación principal aquí es demostrar todo el proceso y demostrar que el modelo puede mejorar las predicciones cuando se agregan nuevos datos sin la necesidad de volver a entrenar y implementar el modelo.

7. Utilice Apache Spark para la clasificación.

Desde la perspectiva del usuario, Apache Spark es muy similar a Flink y la implementación también es muy similar.

Spark tiene dos modelos de transmisión: el DStreams más antiguo (ahora heredado) y el modelo de transmisión estructurada más nuevo y recomendado. Sin embargo, dado que el algoritmo de transmisión k-means incluido en la biblioteca Spark ML solo funciona con DStreams, en este ejemplo se utiliza un DStream para simplificar. Un mejor enfoque es utilizar transmisión estructurada e implementar la transmisión k-means usted mismo.

Spark admite la transmisión desde Kafka mediante DStreams. Sin embargo, no se admite la escritura de un DStream en Kafka y, aunque es posible, no es trivial.

Nuevamente, para hacerlo simple, omita la última parte y simplemente escriba las predicciones en la consola en lugar de volver a escribirlas en Kafka. La imagen general del oleoducto se ve así:

Insertar descripción de la imagen aquí

8. Definir el flujo de datos

De manera similar a Flink, crear una secuencia Spark a partir de una secuencia Kafka es muy simple y la mayoría de los parámetros se explican por sí mismos:

Set<String> trainTopic = new HashSet<>(Arrays.asList("spark.public.iris_train"));
Set<String> testTopic = new HashSet<>(Arrays.asList("spark.public.iris_test"));
Map<String, Object> kafkaParams = new HashMap<>();
kafkaParams.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
kafkaParams.put(ConsumerConfig.GROUP_ID_CONFIG, "dbz");
kafkaParams.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
kafkaParams.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
kafkaParams.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

JavaInputDStream<ConsumerRecord<String, String>> trainStream = KafkaUtils.createDirectStream(
        jssc,
        LocationStrategies.PreferConsistent(),
        ConsumerStrategies.Subscribe(trainTopic, kafkaParams));
JavaDStream<LabeledPoint> train = trainStream.map(ConsumerRecord::value)
        .map(SparkKafkaStreamingKmeans::toLabeledPointString)
        .map(LabeledPoint::parse);

En la última línea, la secuencia de Kafka se convierte en una secuencia de marcador, que la biblioteca Spark ML utiliza para procesar su modelo ML. Markpoint debe ser una cadena formateada como una etiqueta de punto de datos separada por una coma de un valor de punto de datos separado por espacios. Entonces la función de mapa se ve así:

private static String toLabeledPointString(String json) throws ParseException {
    
    
    JSONParser jsonParser = new JSONParser();
    JSONObject o = (JSONObject)jsonParser.parse(json);
    return String.format("%s, %s %s %s %s",
            o.get("iris_class"),
            o.get("sepal_length"),
            o.get("sepal_width"),
            o.get("petal_length"),
            o.get("petal_width"));
}

Sigue siendo cierto que k-means es un algoritmo no supervisado y no utiliza etiquetas de puntos de datos. Sin embargo, es conveniente pasarlos a la clase LabeledPoint para que podamos mostrarlos más tarde junto con las predicciones del modelo.

Luego encadenamos una función de mapeo para analizar la cadena y crear un punto de datos etiquetado a partir de ella. En este caso, es una función integrada de Spark LabeledPoint.

A diferencia de Flink, Spark no requiere que los temas de Kafka existan previamente, por lo que al implementar un modelo, no es necesario crear el tema. Una vez que se crean y completan las tablas que contienen los datos de su prueba, puede dejar que Debezium las cree.

9. Definir y evaluar modelos.

Definir un modelo de transmisión de k-medias es muy similar a Flink:

StreamingKMeans model = new StreamingKMeans()
        .setK(3)
        .setInitialCenters(initCenters, weights);
model.trainOn(train.map(lp -> lp.getFeatures()));

Además, en este caso, establezca directamente el número de grupos en 3 y proporcione a los grupos el mismo punto central inicial. Además, solo pase puntos de datos para entrenamiento, no etiquetas.

Como se mencionó anteriormente, podemos usar etiquetas para mostrarlas junto con las predicciones:

JavaPairDStream<Double, Vector> predict = test.mapToPair(lp -> new Tuple2<>(lp.label(), lp.features()));
model.predictOnValues(predict).print(11);

Imprime 11 elementos de secuencia en la consola en la secuencia resultante con predicciones, ya que este es el tamaño de la muestra de prueba. Al igual que con Flink, los resultados pueden ser mejores después del entrenamiento inicial con muestras de datos muy pequeñas. El primer número de la tupla es la etiqueta del punto de datos, mientras que el segundo número es la predicción correspondiente realizada por el modelo:

spark_1      | (0.0,0)
spark_1      | (0.0,0)
spark_1      | (2.0,2)
spark_1      | (2.0,2)
spark_1      | (1.0,0)
spark_1      | (1.0,0)
spark_1      | (1.0,2)
spark_1      | (1.0,0)
spark_1      | (1.0,0)
spark_1      | (0.0,0)
spark_1      | (1.0,0)

Sin embargo, cuando se proporcionan más datos de entrenamiento, las predicciones mejoran:

spark_1      | (0.0,0)
spark_1      | (0.0,0)
spark_1      | (2.0,2)
spark_1      | (2.0,2)
spark_1      | (1.0,1)
spark_1      | (1.0,1)
spark_1      | (1.0,2)
spark_1      | (1.0,0)
spark_1      | (1.0,1)
spark_1      | (0.0,0)
spark_1      | (1.0,0)

Si vuelve a pasar una segunda muestra de datos de entrenamiento para el entrenamiento, el modelo realiza predicciones correctas para toda la muestra de prueba:

---
spark_1      | (0.0,0)
spark_1      | (0.0,0)
spark_1      | (2.0,2)
spark_1      | (2.0,2)
spark_1      | (1.0,1)
spark_1      | (1.0,1)
spark_1      | (1.0,1)
spark_1      | (1.0,1)
spark_1      | (1.0,1)
spark_1      | (0.0,0)
spark_1      | (1.0,1)
----

La predicción es la cantidad de grupos creados por el algoritmo k-means, independientemente de las etiquetas en la muestra de datos. Esto significa que, por ejemplo, (0,0,1) no es necesariamente una predicción incorrecta. Es posible que se asigne un punto de datos con la etiqueta 0 al clúster correcto, pero Spark lo etiqueta internamente como el clúster número 1. Esto debe tenerse en cuenta al evaluar modelos.

Por lo tanto, al igual que Flink, cuando se pasan más datos de entrenamiento, obtendrá mejores resultados sin la necesidad de volver a entrenar y implementar el modelo. En este caso se obtuvieron mejores resultados que el modelo de Flink.

10. Conclusión

  • Muestra cómo pasar datos de la base de datos a Apache Flink y Apache Spark en tiempo real como un flujo de datos. En ambos casos, la integración es fácil de configurar y funciona bien.
  • Esto se demuestra en el ejemplo que nos permite utilizar un algoritmo de aprendizaje en línea, concretamente el algoritmo k-means en línea, para resaltar el poder de los flujos de datos. El aprendizaje automático en línea nos permite hacer predicciones en tiempo real sobre flujos de datos y mejorar o ajustar los modelos tan pronto como llegan nuevos datos de entrenamiento. El ajuste de modelos no requiere volver a entrenar ningún modelo en un clúster informático separado ni volver a implementar nuevos modelos, lo que hace que las operaciones de aprendizaje automático sean más simples y rentables.

Supongo que te gusta

Origin blog.csdn.net/zhengzaifeidelushang/article/details/133513688
Recomendado
Clasificación