Autor: Taylor Foust

Este trabalho é fruto de um esforço colaborativo de muitos engenheiros do Nubank (em ordem alfabética): Austin McEver, Brayan Garzon, Daniel Braithwait, Fábio Souza, Gabriel Gandour, Gustavo Vieira, Helder Dias, Hiroto Udagawa, José Mora, Lucas Costa, Marcelo Buga, Matheus Ramos, Neriton Tolentino e Stelios Karvanis. Também agradecemos a Rohan Ramanath, Daniel Silva e Guilherme Tanure pelo apoio.


Introdução

Em publicações anteriores do nosso blog, apresentamos o conceito de como o Nubank utiliza modelos fundacionais para aprender representações ótimas de atributos em tarefas preditivas. Especificamente, mostramos como dados de transações bancárias podem ser representados como uma sequência de tokens construídos a partir dos atributos da transação (por exemplo: data, valor, descrição). As transações são combinadas com outros eventos e informações de contexto para formar uma representação do usuário (ou narrativa), que então é tokenizada e enviada aos nossos modelos fundacionais. Esses modelos transformam a sequência de tokens em representações otimizadas para suas tarefas de aprendizado. Essas narrativas de usuários não são fixas – é necessário escolher quais informações incluir e como representá-las da forma mais útil possível para os modelos.

No restante deste post, mostramos que a construção dessas narrativas pode ser encarada como uma busca por hiperparâmetros em duas dimensões principais: quais fontes de informação incluir e como representar os eventos que compõem cada fonte. Apresentamos nossa abordagem experimental para explorar esse espaço de hiperparâmetros e demonstramos como esse processo reduz significativamente o esforço necessário para incorporar novas fontes de dados, elimina suposições e melhora a performance dos modelos.

Conheça nossas oportunidades

Incluindo uma transação em uma narrativa de usuário: como representamos uma transação?

Vamos apresentar o processo com um exemplo simples: uma pessoa faz uma compra no cartão de crédito. Na forma mais simples, poderíamos representar esse evento com um único token, como <compra-cartao-credito>, mas essa história tem muitos outros detalhes.

Figura 1

Quando a compra aconteceu? Em qual estabelecimento? Foi uma compra presencial ou online? Qual foi o valor gasto? A compra foi aprovada? Se foi recusada, qual foi o motivo? Qual era o saldo da conta depois da transação?

De forma intuitiva, pode parecer que quanto mais informações incluirmos na representação da transação, melhor será o desempenho do modelo – afinal, o modelo poderia aprender a ignorar o que não é relevante. No entanto, nossos experimentos mostram que adicionar mais tokens (mesmo os que parecem informativos) nem sempre melhora a performance do modelo.

Nossos modelos, como toda arquitetura baseada em transformers, têm uma limitação de tamanho de contexto, por conta do escalonamento quadrático da atenção – mesmo utilizando algoritmos eficientes como o FlashAttention.. Isso significa que, quando a janela de contexto está cheia, qualquer nova informação inserida “empurra” outra para fora. Por isso, cada token precisa justificar sua presença: deve trazer informação útil para a tarefa de modelagem e não ser redundante com outros tokens. Em outras palavras, esse processo se assemelha à edição de um documento de texto: precisamos garantir que a linguagem seja concisa e que só mantenhamos o que é essencial.

Figura 2

Otimizando Representações de Transações

Nosso objetivo ao criar documentos de usuários é duplo: incluir o máximo de informações úteis possível e representar essas informações com o menor número de tokens possível. Mas como sabemos se um token específico vale a pena ser incluído? Determinamos a efetividade da seleção de tokens por meio de experimentação e testes empíricos com base em métricas de avaliação offline.

Para ilustrar esse processo, considere o seguinte exemplo de usuário: abc-123. Essa pessoa tem quatro transações: uma transferência recebida e três compras no cartão de crédito. Essas transações estão ilustradas na Figura 3 abaixo:

Figura 3: transações de exemplo para o usuário abc-123

Para cada uma dessas transações, há diversos atributos que podem ser úteis para os nossos modelos: o valor da transação, a data (ano, dia, dia da semana, mês etc.), o status da transação, a origem da transação, a descrição, o motivo de recusa (se aplicável) e o modo de entrada (como a transação foi realizada). Para cada um desses atributos, criamos módulos de pré-processamento que adicionam os tokens necessários à representação da transação. Muitos desses módulos adicionam tokens especiais (faixas de valor, se foi pagamento ou recebimento, atributos de data, status da transação), enquanto outros (como descrição) são passados como texto bruto para o tokenizador e tokenizados usando o algoritmo BPE.

Neste experimento, geramos diversos módulos de pré-processamento: amount, date, description, source, status, denial_reason e entry_mode, que, ao serem selecionados, alteram a forma como as transações são representadas para o modelo. Podemos então selecionar qualquer combinação desses módulos ao inicializar o modelo, treinar o modelo e gerar métricas de avaliação em um conjunto de testes.

No teste 1, inicializamos o modelo com pre_processors = [amount, date, description. Uma versão tokenizada da transação 4 está na Figura 4, com o texto representado por cada token entre colchetes angulares.

Figura 4

No teste 2,  pre_processors = [amount, date, description, source].  A apresentação se tornaria:

Figura 5

No teste  3,  pre_processors = [amount, date, description, status]:

Figura 6

No teste 4,  pre_processors = [amount, date, description, entry_mode]:

Figura 7

No teste 5,   pre_processors = [amount, date, description, denial_reason]:

Figura 8

No testel 6,   pre_processors = [amount, date, description, status, denial_reason]:

Figura 9

Treinamos os modelos e avaliamos o desempenho em nosso conjunto de validação. Existem muitas outras combinações possíveis além das que listamos aqui. Resultados de exemplo estão ilustrados na Figura 10 abaixo:

Figura 10

Embora limitado em escopo para fins de ilustração, este experimento nos permitiu encontrar uma forma otimizada de representar transações — um componente fundamental da narrativa de usuário. Primeiro, criamos pré-processadores reutilizáveis que convertem atributos das transações em tokens, e em seguida realizamos uma busca de hiperparâmetros para determinar a combinação ideal desses pré-processadores. Neste caso, descobrimos que mais informação nem sempre leva a uma performance melhor: em algumas situações, a inclusão de certos pré-processadores (ou suas combinações) pode piorar os resultados, muitas vezes devido à limitação da janela de contexto do modelo.

Quais fontes de informação incluir?

Nos exemplos anteriores, discutimos apenas fontes de dados de transferências e compras no cartão de crédito, mas o Nubank possui muitas outras fontes de transações — além de fontes que não são transacionais. Ao avaliar novas fontes de dados, aplicamos a mesma abordagem experimental para garantir que os dados adicionais realmente enriquecem as representações dos usuários e melhoram nossos indicadores de performance. No entanto, para algumas fontes, certos tokens podem não ser úteis, e outras podem exigir a criação de novos tokens. Por isso, desenvolvemos nosso framework de forma que cada fonte possa utilizar uma lógica de pré-processamento própria.

As Caixinhas do Nubank são contas de investimento nas quais os usuários podem alocar valores em aplicações de sua escolha — geralmente com um objetivo de economia específico. A estrutura dessas transações é semelhante à que vimos com transferências e compras no cartão: observamos movimentações de valores para dentro e para fora, e normalmente, a descrição da transação reflete o objetivo da economia definido pelo próprio usuário. Um exemplo desses dados está representado na Figura 11 abaixo.

Figura 11

Usar os mesmos módulos de pré-processamento que aplicamos nas fontes de transações anteriores já permite capturar uma boa quantidade de informações (como valor, data, descrição, origem e status). No entanto, há alguns campos adicionais que são exclusivos das transações de Caixinhas e que também podem ser úteis: transaction_type, investment_type e account_balance. Podemos criar novos módulos que processem essas informações em tokens especiais e testar a inclusão da fonte de dados das Caixinhas tanto com os pré-processadores padrão quanto com os novos módulos.

Executar esses experimentos é quase idêntico aos testes que mostramos anteriormente — a única diferença está na especificação de um conjunto de dados que inclui transações de Caixinhas e na ativação dos novos pré-processadores voltados para essas transações. Com isso, podemos definir os seguintes testes adicionais:

Teste 7: pre_processors = [amount, date, description, status, denial_reason], dataset=”transactions_with_money_box”

Teste 8: pre_processors = [amount, date, description, status, denial_reason], dataset=”transactions_with_money_box”, pre_processor_overrides={“MONEY BOX”: [amount, date, description, status, transaction_type, investment_type, account_balance]}

No teste 8, adicionaríamos ao nosso documento de usuário uma entrada como a seguinte:

Figura 12

Da mesma forma, para transações relacionadas a empréstimos, existem diversos atributos adicionais que passamos para o modelo e que não são relevantes para os outros tipos de transações que mencionamos até agora. Empréstimos possuem taxas de juros associadas, saldos devedores, número de parcelas pagas, entre outros. Para aproveitar essas informações importantes sem ocupar a janela de contexto com tokens irrelevantes para outras fontes, permitimos que cada fonte extraia apenas os tokens de que realmente precisa.

Para representar o pagamento de um empréstimo com a inclusão dos pré-processadores interest_rate, number_of_payments_made e remaining_balance, a transação poderia ser representada da seguinte forma:

Figura 13

Também incluímos uma variedade de outras fontes de dados que são ainda mais diferentes das transações tradicionais. Por exemplo, em nossos modelos fundacionais brasileiros, incorporamos dados do Sistema de Informações de Crédito (SCR). Esses dados são totalmente distintos das transações e assumem a forma de atualizações mensais de métricas relacionadas a crédito dos usuários. Essas informações podem trazer visibilidade sobre o histórico financeiro dos usuários antes de se tornarem clientes do Nubank, além de oferecer um retrato da vida financeira deles fora do Nubank. Isso ajuda a garantir que nossos modelos sejam robustos tanto para novos clientes quanto para aqueles que utilizam os serviços do Nubank com menor frequência. Após o pré-processamento, esses dados podem assumir uma forma semelhante à da Figura 14.

Figura 14

Ter a flexibilidade de acomodar diferentes formatos de dados com facilidade é essencial para acelerar a experimentação e nos permite preencher lacunas no entendimento sobre nossos usuários. Assim como antes, adicionar essas novas fontes ao nosso modelo e testar seu impacto na performance envolve apenas criar o módulo de pré-processamento, selecionar o dataset com a nova fonte disponível e ativar o módulo de pré-processamento. Ao otimizar esses hiperparâmetros, otimizamos a representação dos usuários e conseguimos obter ganhos de desempenho no modelo. A Figura 15 abaixo mostra os resultados observados.

Figura 15

Escrevendo Documentos de Usuário

Com as fontes de dados que discutimos, o documento para nosso usuário de exemplo toma uma forma semelhante à seguinte:

Figura 16

Embora não seja uma leitura agradável para uma pessoa, validamos por meio de experimentação que nossos modelos preferem essa estrutura. Essa representação permite que nossos modelos compreendam a jornada do usuário desde antes de se tornar cliente do Nubank até a adoção de diversos produtos do banco. O modelo consegue entender a situação financeira do usuário, como ela mudou ao longo do tempo, seus hábitos de consumo e de poupança, seus objetivos financeiros e dificuldades, além dos produtos Nubank que usa para melhorar sua vida. Crucialmente, nosso papel não é criar a representação final dos dados para o modelo, mas sim apresentá-los de forma que o modelo consiga extrair as informações necessárias e construir as características que o ajudem a atingir seus objetivos.

A seleção e representação dos dados são de extrema importância em todas as aplicações de aprendizado de máquina, e são uma das partes mais demoradas do processo. Pode levar semanas para os praticantes de aprendizado de máquina avaliarem uma nova fonte de dados, descobrirem como derivar características tabulares, limparem e pré-processarem essas características e, finalmente, treinarem um novo modelo com esses dados e rodarem a avaliação.

Com os modelos fundacionais do Nubank, simplificamos esse processo, transformando-o em uma questão de aprimorar as maneiras de escrever documentos de usuários. Como mostramos, ainda há nuances nesse processo e é necessário pensar, mas o processo é muito mais simples, rápido, flexível e fornece feedback quantificável em termos de performance na avaliação do modelo. Conseguimos passar de uma fonte de dados bruta para resultados de testes em apenas alguns dias. E as representações dos dados não precisam se conformar às restrições típicas do aprendizado de máquina tabular. Como pesquisadores no Nubank, só precisamos considerar: que informações podem enriquecer a narrativa que temos para este usuário e como posso apresentar essas informações para nossos modelos de forma eficiente e útil? Então, apresentamos uma variedade de opções e deixamos que o modelo escolha a representação que considera melhor.

Conheça nossas oportunidades