most read
Building Stories
Modo Rua: Redefiniendo el desarrollo de aplicaciones mediante iteración centrada en el usuario Ago 23
Building Stories
NuStories: Adaptación de productos para clientes fanáticos en varios países Oct 30
Culture & Values
Cómo los valores y la cultura de Nu dan forma a los productos que creamos Ago 7
Careers
We are building diverse teams with the most creative and innovative professionals for each position we open.



Escrito por: Felipe Almeida, con contribuciones de Luiz Felix, Vívian Miranda, Lucas Nolasco y Caroline Custodio.
Esta es la parte 3 de una serie de 3 partes sobre Feature Stores para ML en tiempo real. Consulta la parte 1 y la parte 2.
Breve resumen e introducción
En la parte 1 de esta serie describimos qué son las feature stores y por qué tienen sentido cuando una organización orientada a datos alcanza cierto nivel de madurez, especialmente al usar sistemas en tiempo real habilitados por ML. En la parte 2, enumeramos las lecciones que aprendimos al aplicar conceptos de feature store a problemas reales en Nubank.
En las siguientes secciones repasaremos algunos patrones arquitectónicos concretos que hemos implementado. Algunos de ellos son lo que podríamos llamar una feature store completa (con definiciones de features comunes para los caminos de entrenamiento y de serving, por ejemplo), mientras que otros se enfocan en resolver solo un problema particular.
Check our job opportunies
Features precalculadas a partir de datos batch, materializadas en una capa de serving clave-valor
En esta variante producimos un dataset totalmente desnormalizado (con todas las features de cada entidad combinadas en una sola fila) como parte de nuestros jobs nocturnos de ETL/ELT y lo ponemos a disposición en una “capa de serving” (por ejemplo, DynamoDB), indexado por el ID, para usarlo en tiempo de inferencia. Esto se puede ver en la Figura 1:
Figura 1: En esta estrategia se agrega un paso extra a los jobs nocturnos de ETL/ELT: producir un dataset, indexado por el ID de la entidad (por ejemplo, ID de cliente, ID de cuenta), donde cada columna es el valor de una feature para esa entidad. Luego, ese snapshot se envía a un almacén clave-valor (KV) de baja latencia como DynamoDB o Redis. En tiempo de serving, el modelo de ML en tiempo real recuperará una sola fila de ese almacén, ya que está totalmente desnormalizado. Esto es rápido y fácil.
Este suele ser el primer “patrón” de feature store al que convergen muchas empresas, a menudo sin saber que se trata de una feature store. Se basa en la arquitectura lambda (sin la speed layer).
Es un caso de uso simple, pero bastante poderoso:
La principal desventaja es que los valores de las features servidos en tiempo de inferencia estarán desactualizados, ya que son un snapshot de la última vez que se ejecutaron los jobs de ETL/ELT (no una representación del mundo tal como es ahora). Por lo tanto, hay que tener en cuenta cuándo cada snapshot habría estado realmente disponible al construir el dataset de entrenamiento, para evitar fuga de datos (los snapshots no están disponibles instantáneamente; pueden tardar horas y a veces fallan).
Como ocurre con todas las estrategias de implementación de features “shift-left”, esto puede volverse costoso porque se precalculan features para todas las entidades (por ejemplo, clientes, cuentas), aunque solo una minoría de ellas necesite ser evaluada en un día determinado.
Features tabulares on-demand con Flink y Pinot
Las features on-demand se calculan en tiempo de serving, en lugar de precalcularse con antelación. Esto es útil cuando los modelos necesitan la información más actualizada y precalcular features para entidades que quizá nunca las usen sería un desperdicio.
Pueden aumentar la latencia si no tomamos precauciones, pero tienen ventajas claras:
Una de las estrategias que usamos para features on-demand es la de escritura dual (dual-write), en la que los microservicios transaccionales escriben eventos tanto en el camino offline (archivos Parquet en S3) como en el camino online (una app de Apache Flink y luego Pinot), como podemos ver en la Figura 2 a continuación.
Figura 2: Cada evento de comportamiento se enruta al camino offline y al camino online. Esto ayuda a reducir el training-serving skew en este caso, pero la lógica de agregación sigue siendo diferente en el camino offline (normalmente jobs de Spark) y en el camino online (queries SQL en Pinot).
En el camino de escritura, la estrategia de escritura dual anterior busca enviar los mismos eventos a los almacenes offline y online. Esto es importante para reducir el training-serving skew (pero no para eliminarlo, porque aún puede haber retrasos y eventos faltantes). Además, la lógica de agregación batch (usada en el entrenamiento) y las queries usadas en tiempo de serving son artefactos diferentes. Por esta razón, el monitoreo del skew sigue siendo necesario.
El camino de lectura consume los datos producidos por el camino de escritura (Figura 2). Tiene dos ramas: una para entrenamiento y otra para serving.
La lógica de tiempo de entrenamiento suele escribirse como jobs de procesamiento batch, ya que implica joins y agregaciones pesadas sobre horizontes de tiempo largos. La lógica de tiempo de serving es una query SQL ejecutada en tiempo real sobre una base de datos de procesamiento analítico en línea (OLAP) en tiempo real como Apache Pinot, como se puede ver en la Figura 3 a continuación. Esto forma parte de nuestro stack “Avalanche”, sobre el cual escribimos en este post del blog.
Figura 3: En tiempo de entrenamiento, construimos el dataset de entrenamiento a partir de un spine (un dataset con IDs de entidades y timestamps de referencia) y el event store, mediante un job de Spark. En tiempo de serving, ejecutamos la agregación en tiempo real, con una query SQL normal, sobre un backend de base de datos Pinot.
La query SQL enviada a Pinot es una query de agregación. Este es un ejemplo de la query que usaríamos para obtener la cantidad de veces que un cliente determinado abrió su app en las últimas 2 horas:
Algunos puntos para que esto funcione a nuestra escala:
Una ventaja secundaria de este enfoque es que cualquier otro equipo puede usar los eventos que guarda un equipo: toda la lógica de la feature está contenida en la query, así que varios equipos pueden compartir la base de datos Pinot y ejecutar queries diferentes sobre ella.
Esta no es una feature store completa; solo usamos esta configuración para soportar el camino de serving de features online. El código de entrenamiento está en un camino completamente distinto, y por eso debemos seguir monitoreando el training-serving skew.
Uso de una arquitectura de materialización y serving en dos etapas basada en Flink
En esta estrategia, los eventos de comportamiento fluyen por dos apps de Flink, una tras otra (Flink app #1 y Flink app #2, como se puede ver en la Figura 4 a continuación). Esto es útil cuando los modelos necesitan features actualizadas, pero calcularlas en tiempo de serving añadiría demasiada latencia:
Figura 4: En este ejemplo, cada nuevo evento “purchase-requested” se transmite a través de Flink app #1, y un conteo continuo de las últimas 24 horas para cada cliente se mantiene en Flink app #2 y luego se envía mediante upsert a DynamoDB. Cada vez que cualquier modelo en tiempo real necesita esta información, solo tiene que llamar a DynamoDB pasando el ID del cliente y el nombre del feature set para obtener el conteo materializado más reciente de ese cliente.
Flink app #1 escucha eventos de comportamiento (logs de CDC de bases de datos transaccionales y eventos crudos de Kafka), combina datos de múltiples shards en un solo stream, filtra los eventos no deseados y normaliza nombres y schemas. En esta primera app no se mantiene ningún estado.
Flink app #2 escucha los eventos de la primera app y mantiene cierto estado, guardando todos los eventos de cada cliente, con un TTL de 24 horas. Cuando un nuevo evento entra en la app #2, todas las features relacionadas se recalculan y se envía mediante upsert una nueva fila a una tabla de DynamoDB (indexada por un ID de entidad, como el ID de cliente).
Esta estrategia depende del precálculo de features, por lo que su latencia en tiempo de serving es muy baja (ya que es una simple búsqueda). Al igual que en el primer ejemplo de este artículo, el trade-off es que la mayoría de las features se calcularán para clientes que nunca las necesitarán.
Como en el ejemplo anterior, esta configuración solo cubre el camino de serving, así que no es una feature store completa. El código de entrenamiento corre en Spark, por lo que puede haber skew inducido por lógica, que debe monitorearse.
Próximos pasos
Además de mejorar constantemente nuestra configuración y aprender nuevas lecciones, hay al menos otras dos líneas que estamos explorando actualmente:
Además, Feldera podría permitirnos unificar la lógica de features para entrenamiento y serving, reduciendo los problemas de training-serving skew y acercando a Nubank a una feature store en tiempo real completa y para toda la empresa.
Las features secuenciales generan mucho valor, particularmente en redes neuronales, pero también introducen complejidad adicional y nuevos modos de falla; descubrir cómo integrarlas en nuestras plataformas de features en tiempo real es un desafío en curso.
Conclusión
En este post repasamos algunos patrones arquitectónicos que usamos en Nubank para poner features a disposición de modelos de ML en tiempo real.
Con esto cerramos nuestra serie de 3 partes sobre feature stores para ML en tiempo real en Nubank. No dejes de leer las partes 1 y 2 y mantente atento a otros posts sobre este tema, ¡ya que es algo en lo que estamos trabajando activamente!
Check our job opportunies