Construindo nossa Feature Store para agilizar nossas predições de machine learning

Um olhar detalhado sobre a construção da nossa própria Feature Store

Gabriela Melo
Legiti
9 min readSep 28, 2021

--

Also available in English

No primeiro post dessa série, eu falei sobre como nós utilizamos um pacote Python para aumentar nossa velocidade de desenvolvimento de novas features, ao mesmo tempo em que reduzimos o risco de introduzirmos training-serving skew na Legiti. Neste post, vou falar sobre o problema de latência que começamos a ter no nosso serviço de fornecimento de modelos, e o que fizemos para endereçá-lo.

Como eu mencionei naquele post, nós tínhamos código no nosso pacote Python para calcular todas as features necessárias pelos nossos modelos para fazer a avaliação de uma transação. Esse pacote era instalado como um requisito do nosso serviço de avaliação, e nós rodávamos o processo de cálculo de features, utilizando o código desse pacote, para cada pedido que recebíamos. Para isso, nós fazíamos queries ao nosso banco de dados para buscar todos os pedidos “relacionados” com o pedido que estava sendo avaliado. Pedidos relacionados são aqueles que compartilham alguma informação com o pedido que está sendo avaliado: eles foram feitos pelo mesmo usuário, ou utilizando o mesmo documento, ou comprando o mesmo produto, etc.

Como você pode imaginar, isso não só é um tanto ineficiente (já que estávamos fazendo queries e cálculos bem similares repetidamente no nosso serviço), mas também essa solução não escala muito bem com um aumento na quantidade de dados; especialmente por que precisávamos fazer queries e cálculos em cima de todo nosso histórico de pedidos. Esse endpoint de avaliação é chamado pelos nossos clientes durante o processo de venda deles (que pode ser síncrono), então é um endpoint crítico e sob expectativas rígidas de latência, já que causa um atraso observado diretamente pelos usuários finais de nossos clientes. Com os processos de queries e cálculos sendo executados durante a requisição, havia uma possibilidade de que a nossa latência fosse mais alta do que a aceitável pelos nossos clientes e essa possibilidade aumentava cada vez mais conforme o tempo e acúmulo de dados coletados. Dessa forma, se tornou claro para nós que precisaríamos de algum jeito de começar a pré-computar, se não todas, pelo menos algumas de nossas features.

O que precisávamos na nossa Feature Store

Foi aí que surgiu a ideia de utilizar uma feature store para nos ajudar com esses problemas. Na época, features stores estavam começando a ser notadas e citadas por outras empresas, mas ainda não havia muita informação ou exemplos de features stores disponíveis. Já havia algumas soluções open-source ou enterprise, mas elas pareciam ter um pouco mais do que precisaríamos, um overhead que sentimos não fazer sentido para nós naquele momento.

Nós então decidimos construir nossa própria feature store. Como eu mencionei, no começo dessa jornada, não estava claro nem o que uma feature store era exatamente, como ela funcionava, como nós iríamos calcular nossas features, entre outras dúvidas. Conseguir responder com clareza essa pergunta inicial nos levou um tempo, mas conseguimos estabelecer algumas definições que serviriam como base para nossa feature store.

Tipos de features que temos na Legiti

Essa foi a primeira coisa que precisávamos ter completamente clara e alinhada quando começamos a discutir nossa feature store. Entender os tipos de features que temos e como elas são calculadas iria impactar diretamente o desenvolvimento da nossa feature store.

A maioria das features que temos são as que chamamos de features de velocidade, que são também as mais demoradas para calcular. Elas são baseadas em contagens de algo em algum intervalo de tempo. Para explicá-las, vamos olhar para os dois tipos de features de velocidades que temos.

O primeiro é baseado em contagens simples: “quantas vezes esse documento (CPF/CNPJ) foi associado a pedidos nos últimos 7 dias?”, “quantas vezes esse número de telefone foi associado com pedidos em todo o histórico de pedidos que temos?”, “quantas vezes esse CEP foi associado a chargebacks nas últimas 6 horas?”, e assim por diante.

O segundo tipo de feature de velocidade que temos é um pouco mais difícil de calcular: nós contamos com quantos identificadores diferentes para uma determinada entidade um identificador foi associado. Para deixar mais claro: “com quantos CEPs diferentes esse documento foi associado em pedidos nos últimos 7 dias?”, “com quantos usuários diferentes esse número de telefone foi associado em pedidos em todo o histórico de pedidos que temos?”, “com quantos números de telefone diferentes esse CEP foi associado em chargebacks nas últimas 6 horas?”.

Isso significa que o nosso cálculo de features envolve extrair do nosso banco de dados muitos dados de pedidos relacionados para conseguir realizar essas contagens. Nós utilizamos desde intervalos de tempo que olham somente para dados bem recentes (10 minutos antes do momento do pedido) até intervalos que olham para dados de um passado mais distante (nós temos features de velocidade olhando para o intervalo “ever”, isso é, o momento mais distante no tempo a partir do qual temos dados para o cliente). Isso nos leva ao próximo ponto.

Features do passado distante e recente

Após esclarecer os diferentes tipos de features que tínhamos, uma das primeiras coisas que fizemos foi começar a distinguir entre dois tipos diferentes de features de velocidade baseados nos diferentes períodos de dados relacionados que utilizamos para calculá-las.

  • O primeiro tipo são as features relacionadas ao passado distante — essas features podem utilizar dados somente até certo momento no tempo para seu cálculo. Esse momento no tempo é relativo ao momento do pedido. Por exemplo, nós podemos usar dados até a última meia noite para calcular uma feature de passado distante para um pedido acontecendo hoje à tarde.
  • O segundo tipo são as relacionadas ao passado recente — essas podem utilizar dados somente a partir de determinado momento no tempo (também relativo ao momento de cada pedido). Por exemplo, utilizando dados das últimas 24 horas antes do pedido para chegar ao valor da feature.

Dado que features de passado distante não dependem de dados muito recentes em relação ao momento do pedido, essas features podem ser calculadas em uma frequência fixa. Além disso, o cálculo dessas features é mais custoso (já que necessita de mais dados), então decidimos que esse seria o primeiro tipo de feature a ser colocado na nossa feature store. Isso significa que o que implementamos foi: quando um pedido chega para avaliação, features de velocidade de passado distante são consumidas da nossa feature store, e agora somente features de velocidade de passado recente e features que não são de velocidade são calculadas durante a avaliação do pedido no nosso serviço de avaliação (e nós ainda utilizamos nosso pacote Python para o cálculo dessas features).

Pré-calculando valores de features

Na seção acima, eu menciono que poderíamos começar a pré-calcular os valores de features de passado distante. Porém, como podemos calcular features para um pedido que ainda não aconteceu?

O motivo de podermos fazer isso é que nossas features de velocidade são relacionadas a o que chamamos de identificadores de entidades, que estão presentes nos pedidos. Por exemplo, um possível identificador que utilizamos é o CEP do usuário. Então o que podemos fazer é pré-calcular todos os valores de features para todos os CEPs que temos na nossa base. Assim, mesmo que um pedido com determinado CEP ainda não tenha acontecido hoje, nós já podemos ter os valores de features de CEP para ele, dado que estava presente no nosso histórico de pedidos.

Para CEPs que ainda não apareceram no nosso histórico, todos os valores de features relacionados a eles seriam 0 de qualquer forma, então quando tentamos pegar um valor de feature para um identificador na feature store e ele ainda não está presente, sabemos que podemos simplesmente utilizar 0 como o valor daquela feature.

Isso significa que para qualquer pedido acontecendo hoje, o que precisamos é o valor atual da feature para todos os identificadores de entidades presentes naquele pedido.

Acesso a valores de features

A próxima coisa que entendemos é que há dois tipos diferentes de acessos a valores de features, cada um deles possuindo requisitos diferentes:

  • Em momento de inferência em produção precisamos de acesso extremamente rápido ao valor atual de cada feature;
  • Em desenvolvimento e treinamento de modelos precisamos de acesso a valores de features para todos os pedidos, com consistência no tempo (point-in-time consistency — conseguir “viajar no tempo”) mas sem um forte requisito de latência.

Para conseguir ter acesso a esses diferentes tipos de valores de features, nós estabelecemos que teríamos duas stores diferentes. Seguindo a nomenclatura atual em feature stores, chamamos as nossas de online e offline stores. A online store seria baseada em soluções de armazenamento que permitem acesso muito rápido aos dados (Cassandra, Redis, etc). Essa store é a que provê valores atuais de features e é utilizada para prover features às requisições de inferência em produção. A offline store seria baseada em soluções de armazenamento para alta quantidade de dados (soluções comumente utilizadas são o Hive, S3, HDFS) e é a que provê valores de features para treinamento e desenvolvimento de modelos.

Um ponto de confusão comum é que, apesar de termos dois diferentes tipos de features (as de passado recente e as de passado distante), e duas stores diferentes (online e offline), ambas as stores podem conter valores para os dois tipos de features.

Processos de cálculo de features

Como mencionado ao fim da última seção, nós precisamos prover ambas nossas stores com features baseadas em dados atualizados continuamente, então temos alguns processos recorrentes que fazem isso.

Para prover valores de features de passado distante para nossa online store (que vão ser utilizados nas avaliações de pedidos do dia), nós temos um job diário, que roda durante a madrugada, que calcula todos esses valores e os coloca na nossa online store. Esse job também guarda todos os valores na offline store, para rastreabilidade.

Para gerar os datasets de treino, uma coisa que é comum em muitos lugares utilizando feature stores é que eles tem times diferentes reutilizando features. Com isso, para permitir que esses diferentes times produzam diferentes datasets de treino baseados nesses dados da feature store, o que normalmente é disponibilizado é um jeito de extrair features com point-in-time consistency da offline store e realizar um join de forma a construir o dataset que o modelo precisa, com todas as features necessárias. Porém, no nosso caso, nós temos um único formato de dataset que precisamos gerar a partir das features da offline store. Por isso, nós também executamos um job para guardar na nossa offline store os próprios datasets que são utilizados para treinamento de modelo, o que evita que tenhamos que fazer joins com point-in-time consistency no momento de extração de features da offline store. Isso significa que temos esse job separado, rodando periodicamente de acordo com a frequência de re-treino de modelos. Isso também elimina a necessidade de termos a tarefa de escrever um job separado para o backfill de features, já que esse nosso job já calcula valores de features para todos os pedidos do passado, o que também simplifica o versionamento de features.

Ferramentas e tecnologias na nossa Feature Store

Tendo essas definições esclarecidas, agora podemos olhar para a arquitetura que desenvolvemos.

Arquitetura da nossa Feature Store

Todos os dados que são utilizados para cálculo de features vem do nosso banco de dados, que é um PostgreSQL rodando no RDS. Nossos dois jobs de cálculo de features rodam em clusters EMR, com PySpark, e são disparados periodicamente pelo CircleCI. Nossa offline store é um bucket S3, e nossa online store é um cluster Redis no ElastiCache. Nosso serviço de avaliação de pedidos é uma API em Python, utilizando Flask, no Kubernetes, e nossos processos de treinamento de modelo rodam em EC2s, sendo executados tanto manualmente quanto de maneira agendada. Muitas das nossas decisões foram guiadas pelo tamanho do nosso time — nós ainda somos um time pequeno e precisamos de soluções que não vão demandar tanto tempo com configuração e manutenção: por isso o uso frequente de soluções gerenciadas pela AWS nessa arquitetura.

Conclusões

A implementação da nossa feature store trouxe grandes melhorias nas nossas métricas de latência do nosso endpoint de avaliação — a duração média das requisições caiu mais de um terço. Isso também resolveu um problema paralelo que estávamos tendo, de diminuir tempo de cálculo de features durante experimentos e treinamento de modelos, já que agora nossos cientistas de dados conseguem acessar valores pré-calculados de features para nossas features de passado distante.

Estamos bastante contentes com a nossa solução de feature store, e agora queremos levá-la adiante. Algumas das melhorias que temos em mente incluem deixá-la mais user-friendly para nossos cientistas de dados iterarem e criarem novas features com ela, e começar a incluir features de passado recente na feature store, por meio de processos de computação com streaming.

Recursos adicionais

Durante a implementação da nossa feature store, nós nos apoiamos bastante em alguns recursos que conseguimos encontrar online (todos estão em inglês). Se você quer uma recomendação de outras coisas para ler, sugerimos esse artigo bem completo da Uber sobre a plataforma de machine learning deles. Também utilizamos bastante esse website, que lista várias das atuais soluções abertas para features stores que empresas implementaram. Se você procura saber mais sobre o porquê das features stores serem úteis, esse artigo pode te ajudar. Também consultamos essa série de artigos, que começa explicando alguns conceitos de feature stores, e em seguida explica como haviam sido implementadas algumas das features stores que haviam sido documentadas abertamente no momento.

--

--