Executando verificação de segurança...
7

Tentando não ficar pobre antes de ficar rico criando uma Startup de serviços de inteligência artificial

Neste post, vou mostrar como utilizar um banco de dados vetorial para diminuir os custos com tokens do GPT em uma aplicação de perguntas e respostas. O banco de dados vetorial que eu escolhi foi o Pinecone, que permite armazenar e consultar vetores de alta dimensão de forma eficiente e escalável. A ideia é transformar as perguntas e as respostas em vetores usando um modelo de linguagem natural pré-treinado, como o modelo text-embedding-ada-002, e depois usar o Pinecone para encontrar as respostas mais similares às perguntas dos usuários.

Bancos de dados vetoriais podem oferecer melhores resultados de consultas de texto do que os bancos de dados SQL porque eles usam uma representação matemática dos dados chamada vetor, que permite medir a similaridade entre documentos e consultas usando operações como distância ou ângulo. Os bancos de dados SQL, por outro lado, usam uma linguagem de consulta estruturada (SQL) que pode ser limitada para pesquisar correspondências exatas ou difusas de palavras ou frases usando o comando CONTAINS. Além disso, os bancos de dados SQL podem exigir mais recursos e tempo para indexar e pesquisar grandes quantidades de dados de texto do que os bancos de dados vetoriais.

Para gerenciar as perguntas, eu usei o Firebase Firestore, um banco de dados NoSQL na nuvem que oferece sincronização em tempo real e suporte offline. Cada pergunta é armazenada em um documento com um identificador único e um campo para a resposta. O Firestore também permite criar funções na nuvem que são acionadas por eventos no banco de dados, como a criação, atualização ou exclusão de documentos. Eu usei uma Cloud Function para disparar um evento sempre que uma nova pergunta for cadastrada no Firestore. Essa função é responsável por enviar a pergunta para a API do ChatGPT, um serviço que usa o modelo GPT-3.5-turbo para gerar respostas conversacionais. O ChatGPT retorna a resposta em formato de texto, que é então convertida em um vetor usando o mesmo modelo de linguagem natural que foi usado para as perguntas. Esse vetor é enviado para o Pinecone, que armazena o vetor para futuras novas perguntas.

Infra estrutura na versão 1.

Uma vantagem de usar o Pinecone é que ele permite fazer consultas por similaridade usando os vetores das perguntas e das respostas. Assim, quando um usuário faz uma pergunta, eu não preciso enviar a pergunta para o ChatGPT e gastar tokens do GPT. Eu posso simplesmente converter a pergunta em um vetor e enviar para o Pinecone, que me retorna os identificadores dos vetores mais similares. Eu posso adotar uma resposta existente como a resposta da pergunta baseado no score de similaridade entre a nova pergunta e as perguntas que já foram feitas anteriormente. Isso reduz significativamente os custos com tokens do GPT, já que eu só preciso usar o ChatGPT para gerar respostas para perguntas novas ou muito diferentes das existentes.

Pode ser um grande desafio encontrar a resposta exata para a pergunta do usuário com base nas perguntas realizadas anteriormente. Exemplo: ‘Quanto custa 1kg do seu produto?’ ou ‘Quanto custa 1g do seu produto?’. Possivelmente o score do texto vetorizado das duas perguntas terá um score quase de 1.0. Isso pode ser um problema dependendo do seu interesse. Não cheguei a pensar em algo concreto para resolver esse problema, mas acredito que haja formas de contornar isso definindo diferenciações entre algumas palavras tais como ‘kilograma’ e ‘grama’.

Para fazer um comparativo entre o custo entre as consultas do Pinecone e o modelo GPT-3.5-turbo, eu usei os seguintes valores:

  • O preço do Pinecone é de $0.096 / hora.
  • O preço do ChatGPT é de $0.002 / 1K tokens do gpt-3.5-turbo.

Tokens são sequências comuns de caracteres encontrados no texto. O GPT processa o texto usando tokens e entende as relações estatísticas entre eles. Os tokens podem incluir espaços e até sub-palavras. O número máximo de tokens que o GPT pode receber como entrada depende do modelo e do tokenizador usados. Por exemplo, o modelo text-embedding-ada-002 usa o tokenizador cl100k_base e pode receber até 8191 tokens.

Cheguei a considerar o uso do Cloud Function como plataforma de hospedagem do meu código de consulta ao ChatGPT, mas me traria altos custos financeiros. Isso porque o Cloud Function cobra de acordo com o tempo de execução da sua função, além do número de invocações e dos recursos provisionados. Como a API do ChatGPT pode demorar para responder, dependendo da complexidade da sua consulta, você pode acabar pagando muito pelo tempo que a sua função fica aguardando a resposta da API.

Uma forma que encontrei de reduzir os custos com o ChatGPT é usar um web service no Render.com, onde é possível criar uma aplicação em Flask onde eu consegui utilizar threads para não aguardar a API do GPT responder antes de já responder ao Cloud Function. O Render.com é uma plataforma que permite hospedar aplicações web de forma simples e barata, com planos a partir de $7 por mês. A ideia é criar uma camada intermediária entre o Cloud Function e o ChatGPT, que recebe a pergunta do Cloud Function e envia uma resposta imediata dizendo que a resposta está sendo gerada. Em seguida, a aplicação em Flask cria uma thread para enviar a pergunta para a API do ChatGPT e assim que obtiver a resposta, atualize a pergunta no Firestore.

Infra estrutura na versão 2.

Havia pensado em descrever o código fonte do projeto nesse post. Mas acho que por hora, vale a reflexão sobre a infra aplicada. Posteriormente estarei estarei criando novos post para detalhar mais sobre o projeto em NodeJS inserido no Google Cloud Function e o projeto em Python (Flask) hospedado no Render.com.

A aplicação web responsável por inserir e consultar as perguntas no Firestore ainda está em fase de desenvolvimento. Pretendo publicá-la em breve também.

Carregando publicação patrocinada...
2

Uaaauu! Que texto sensacional :D

Eu to criando uma aplicação usando a API do ChatGPT e percebi o mesmo problema que mencionasse de a API da OpenAI demorar muito para ser chamada enquanto o espaço em memória utilizado pela requisição é beeeem baixo.

Eu to usando o AWS Lambda, que permite ter uma bom limite de requisições gratuítas, mas nunca tinha parado pra pensar tanto sobre o custo de tempo de requisição. Eu não acho que tenha sido uma má escolha usar Serverless, porque permitiu eu de forma bem rápida construir o que precisava e integrar com a API da OpenAI.

E eu também havia reparado no problema de as atualizações e a documentação sobre a API da OpenAI parecem vir primeiro para ecossistema Python do que o Node.js. Tanto que a funcionalidade de Streaming, não está disponível na biblioteca oficial da OpenAI de Node.js, existindo apenas na de Python.

Me desse uma ideia interessante agora sobre a questão de dividir a API entre uma com Node e outra em Python, onde a que utilizaria Python, seria uma dedicada a integração com a OpenAI e a um servidor que os preços não cobrem por tempo de utilização ou que pelo menos cobrem mais barato. Enquanto uma outra para manter em Serverless, que realmente precise ser rápido.

Aliás, chegasse a dar uma olhada no Railway? Já havia ouvido falar do Render, mas não sei se em relação a custo-benefício acabasse percebendo que o Render era a melhor opção entre outras. Adoraria saber sobre isso =)

Além disso, to usando uma infraestrutura parecida com a sua, onde também uso o Firestore como DB, porém fiquei pensando se era uma decisão boa a longo prazo, se não seria melhor já colocar essas informações num MongoDB para que pudesse escalar de uma forma que não gastasse tanto dinheiro quanto deixando num Firestore da vida. Mas no meu caso eu pensei bastante em um estratégia de Offline-App, onde a maior parte das coisas ficaria salvo num banco de dados local ou cache do Aplicativo, e atualizaria com algum tipo de evento, como uma adição ao Banco de Dados remoto.

Post muito massa! É bom ver algo que fuja do padrão "IA vai roubar emprego" ou "IA é maligna".

1

Valeu @Thiago1Augusto1Zeferino.

Você questionou sobre o motivo de estar utilizando o render.com. Então, não é algo muito bem avaliado entre todas as opções. Mas depois que o Heroku deixou de ter opção gratuita, eu corri para encontrar uma alternativa para minhas aplicação em Ruby on Rails. Depois de alguns testes com mais uma outra alternativa, o Render.com me pareceu ser bem simples e confiável. Além dele ter opção de você utilizar Docker, ele também tem um arquivo YAML para definir um ou mais serviços. Esse arquivo YAML lhe economiza bastante tempo.

2

Putz isso vai ser extremamente dificil kk

Exemplo: ‘Quanto custa 1kg do seu produto?’ ou ‘Quanto custa 1g do seu produto?’.

Já trabalhei com NLP, sem IA, a sugestão que eu vou dar é baseado no que eu já fiz no passado.

Basicamente tu pode fazer o mesmo que a gente faz com parsing de linguagem de programação (coisa que já fiz também), quebra o texto em tokens, por exemplo:

[str("Quanto"), str("custa"), number("1"), str("kg"), str("do"), str("seu"), str("produto"), str("?")]

Aqui tem várias decisões que você pode fazer também, você pode já no processo de criação de tokens determinar algumas keywords, como as linguagens fazem, e ai tua palavra kg já poderia vir com um tipo diferente:

[str("Quanto"), str("custa"), number("1"), mass_unit(KG, str("kg")), str("do"), str("seu"), str("produto"), str("?")]

ai você pode tirar também as stopwords para simplificar:

[str("Quanto"), str("custa"), number("1"), mass_unit(KG, str("kg")), str("produto"), str("?")]

Detalhe, na hora de armazenar os tokens, a gente não armazena as substrings, só os ranges, isso é crucial para qualquer lexer e parser feito na mão. Exemplo, a palavra custa fica armazenada como Token(range: [7..12], type: Str), e mass_unit ficaria: Token(range: [14..16], type: MassUnit), sem nenhuma string sendo armazenada.

Isso traz muitos ganhos de desempenho, por n ter q copiar string, e pouco custo de memória, pois você está armazenado ali por volta de 3 valores de 64-bit — pelo menos no exemplo que eu forneci com range tendo start e end, e o campo type (no total 3).

Caso queira armazenar um "ponteiro" para essa String (ou string slice), ai seriam 4 campos de 64-bit, mas vai depender se a linguagem copia a String ou apenas referencia ela, ou se suporta ponteiros. Mas isso é só uma conveniência, alguns lexers preferem ter um método get(str) e ai eles retornam a substring a partir daquela string que foi passada ao metodo, ou seja, eles esquecem completamente o input e só trabalham com ranges.

Depois tu meio que vai ter duas opções, você pode fazer um matching na mão com lookahead e lookbehind (era o que a gente fazia), ou já pode progredir para um sistema de regras e criar uma estrutura composta (ou uma Syntax Tree simplificada), um exemplo de regra seria (em pseudo-EBNF e bem simplificada mesmo):

texts    = text+ ;
text     = mass | massUnit | str ;
mass     = NUMBER, massUnit ;
massUnit = KG | G ;
str      = ALPHA

KG     = "kg" | "kilograma" | "quilograma";
G      = "g"  | "grama";
NUMBER = [0-9]+;
ALPHA  = [A-Za-z]+;

E ai no final você teria uma estrutura de dados tipo assim:

texts([text(str("Quanto")), text(str("custa")), mass(number(1), mass_unit(KG)), text(str("produto")), text(str("?"))])

Com isso você já pode fazer muita coisa, ai deixo para sua imaginação kkkk, não conheço o Pinecone e nem se seria possível fazer ele não comparar trechos do texto, outra opção seria fazer obfuscação com um hash dos trechos com unidades de medida, por exemplo:

Quanto custa LztgJ2EO8YcNEcjipENcYA do seu produto?

E ai qualquer pequena mudança nesse trecho já afetaria a distância de forma perceptível.

Só que o que eu acho que é mais dificil é na verdade outras coisas, até aritmética daria para fazer esse tricky que eu comentei, mas teriam muitos corner cases, você teria que fazer a mesma ideia com estados, tipo SP e SC são muito próximos, depois água e águia, e assim por diante. Seria possível minar todos os casos e classificar? Não, mas com o tempo conseguiria diminuir bastante os corner cases e deixa-los mais raros. E teria q ter uma forma de fazer isso com agilidade e de capturar quando isso ocorre também (Levenshtein distance caberia aqui para você logar os casos), e levaria muito tempo — os mais comuns seriam faceis, o resto teria que ser na base do uso e descoberta.

2

valeu @jhrl. meu exemplo foi ficticio mas acredito que seu comentário pode ajudar muita gente.
Na minha cabeça eu tinha algo utilizando o https://spacy.io/ ou https://www.nltk.org/. Mas não tinha pensado nada tão avançado como você exemplificou.
Acho que para identificar os termos, algo voltado a nltk e depois dar os pesos como você mensionou, pode resultar em algo bem bacana.

1

Interessante, os dois parecem usar Phrase structure rules.

Tu pegou um baita desafio e achei super interessante a proposta, não acho que seja impossível, mas de fato vai ser isso ai, vai ter q empregar várias técnicas e ter umas sacadas de gênio. Admiro bastante essa determinação, fiquei curioso para o desfecho também.

1
1

Finalmente o projeto saiu do papel e ganhou sua interface final https://guruduno.site/.

Ferramentas

  • Ouça versões em áudio de perguntas e respostas fornecidas pelo público, disponíveis em um repositório compartilhado.
  • Use sua voz para enviar suas perguntas
  • Acesso às regras oficiais do jogo

Tecnologias usadas

  • Firebase do Google:
    Firestore: armazene as perguntas em tempo real
    Host: Hospede os ativos do projeto
    Functions: Após a inserção de uma nova pergunta no Firestore, há um gatilho para enviar os dados da pergunta para o app backend do Render.
  • Render: Hospede o aplicativo Flask para solicitar respostas da API OpenAI.
  • Ionic: versão Ionic React para criar a tela do app e consumir os dados do Firestore.
  • Pinecone: armazene os dados de incorporação gerados pelo modelo ‘text-embedding-ada-002-v2’ da API OpenAI.
  • API OpenAI (modelo 'gpt3.5-turbo'): responda às perguntas do usuário.
1