Engenharia

Busca por similaridade de texto com campos vetoriais

Desde seus primórdios como um mecanismo de busca de receitas, o Elasticsearch foi projetado para fornecer busca de texto completo rápida e poderosa. Devido a essas origens, melhorar a busca de texto tem sido uma motivação importante para o nosso trabalho contínuo com vetores. No Elasticsearch 7.0, introduzimos tipos de campos experimentais para vetores de alta dimensão; agora, a versão 7.3 oferece suporte ao uso desses vetores na pontuação de documentos.

Neste post, vamos nos concentrar em uma técnica específica chamada busca por similaridade de texto. Nesse tipo de busca, o usuário digita uma breve consulta de texto livre, e os documentos são classificados com base em sua similaridade com a consulta. A similaridade de texto pode ser útil em vários casos de uso:

  • Respostas para perguntas: dada uma coleção de perguntas frequentes, encontrar perguntas semelhantes à que o usuário digitou.
  • Busca de artigos: em uma coleção de artigos de pesquisa, retornar artigos com um título estreitamente relacionado à consulta do usuário.
  • Busca de imagens: em um conjunto de dados de imagens com legendas, encontrar imagens cuja legenda seja semelhante à descrição do usuário.

Uma abordagem simples para a busca por similaridade seria classificar documentos com base em quantas palavras eles têm em comum com a consulta. Mas um documento poderá ser semelhante à consulta mesmo se ambos tiverem muito poucas palavras em comum: uma noção mais robusta de similaridade levaria em conta também seu conteúdo sintático e semântico.

A comunidade de processamento de linguagem natural (PLN) desenvolveu uma técnica chamada text embedding que codifica palavras e frases como vetores numéricos. Essas representações vetoriais são projetadas para capturar o conteúdo linguístico do texto e podem ser usadas para avaliar a similaridade entre uma consulta e um documento.

Este post explora como é possível usar text embeddings e o novo tipo dense_vector do Elasticsearch para suporte à busca por similaridade. Primeiro, apresentaremos uma visão geral das técnicas de embedding e, em seguida, mostraremos os passos para criar um protótipo simples de busca por similaridade usando o Elasticsearch.

Observação: o uso de embeddings de texto na busca é uma área complexa e em evolução. Esperamos que este post do blog forneça um ponto de partida para a exploração, mas ele não é uma recomendação para uma arquitetura ou implementação de busca específica.

O que são text embeddings?

Vamos examinar mais de perto os diferentes tipos de text embeddings e como eles se comparam às abordagens tradicionais de busca.

Word embeddings

Um modelo de word embedding representa uma palavra como um vetor numérico denso. O objetivo desses vetores é capturar propriedades semânticas da palavra; palavras cujos vetores sejam próximos devem ser semelhantes em termos de significado semântico. Em um bom embedding, as direções no espaço vetorial estão atreladas a diferentes aspectos do significado da palavra. Como exemplo, o vetor para “Canadá” pode estar próximo a “França” em uma direção e próximo a “Toronto” em outra.

Já faz algum tempo que o PLN e as comunidades de busca se interessam pelas representações vetoriais de palavras. Houve um ressurgimento do interesse por word embeddings nos últimos anos, quando muitas tarefas tradicionais estavam sendo revisitadas usando redes neurais. Alguns algoritmos bem-sucedidos de word embedding foram desenvolvidos, incluindo o word2vec e o GloVe. Essas abordagens usam grandes coleções de texto e examinam o contexto no qual cada palavra aparece para determinar sua representação vetorial:

  • O modelo Skip-gram do word2vec treina uma rede neural para prever as palavras de contexto em torno de uma palavra em uma frase. Os word embeddings são determinados pelos pesos internos que a rede atribui a cada palavra.
  • No GloVe, a similaridade das palavras depende da frequência com que aparecem com outras palavras de contexto. O algoritmo treina um modelo linear simples nas contagens de coocorrência de palavras.

Muitos grupos de pesquisa distribuem modelos pré-treinados em grandes corpora de texto como Wikipédia ou Common Crawl, tornando-os convenientes para fazer download e conectar a tarefas de downstream. Embora as versões pré-treinadas sejam usadas diretamente com frequência, pode ser útil ajustar o modelo para se encaixar no conjunto de dados e na tarefa de destino específicos. Isso geralmente é realizado com a execução de uma etapa leve de ajuste fino no modelo pré-treinado.

Os word embeddings se mostraram bastante robustos e eficazes, e agora é prática comum usar embeddings no lugar de tokens individuais em tarefas de PLN, como tradução de máquina e classificação de sentimentos.

Sentence embeddings

Mais recentemente, os pesquisadores começaram a se concentrar em técnicas de embedding que representam não apenas palavras, mas trechos mais longos de texto. A maioria das abordagens atuais se baseia em arquiteturas de redes neurais complexas e, às vezes, incorpora dados rotulados durante o treinamento para ajudar na captura de informações semânticas.

Uma vez treinados, os modelos podem pegar uma frase e produzir um vetor para cada palavra no contexto, bem como um vetor para a frase inteira. De maneira semelhante ao word embedding, estão disponíveis versões pré-treinadas de muitos modelos, permitindo que os usuários pulem o caro processo de treinamento. Enquanto o processo de treinamento pode consumir muitos recursos, a invocação do modelo é muito mais leve: os modelos de sentence embedding geralmente são suficientemente rápidos para serem usados como parte de aplicações em tempo real.

Entre algumas técnicas comuns de sentence embedding, incluem-se InferSent, Universal Sentence Encoder, ELMo e BERT. A melhoria do word embedding e do sentence embedding é uma área ativa de pesquisa, e é provável que outros modelos fortes sejam introduzidos

Comparação com abordagens tradicionais de busca

Na recuperação de informações tradicional, uma maneira comum de representar o texto como um vetor numérico é atribuir uma dimensão para cada palavra no vocabulário. O vetor de um trecho de texto é, então, baseado no número de vezes que cada termo do vocabulário aparece. Essa maneira de representar o texto costuma ser chamada de “saco de palavras”, porque simplesmente contamos as ocorrências das palavras sem considerar a estrutura da frase.

Os text embeddings diferem das representações vetoriais tradicionais em alguns aspectos importantes:

  • Os vetores codificados são densos e de dimensões relativamente baixas, geralmente variando entre cem e mil dimensões. Por outro lado, os vetores de saco de palavras são esparsos e podem compreender mais de 50 mil dimensões. Os algoritmos de embedding codificam o texto em um espaço de menor dimensão como parte da modelagem de seu significado semântico. Idealmente, palavras e frases sinônimas acabam tendo uma representação semelhante no novo espaço vetorial.
  • Sentence embeddings podem levar em consideração a ordem das palavras ao determinar a representação vetorial. Por exemplo, a frase “tune in” pode ser mapeada como um vetor muito diferente de “in tune”.
  • Na prática, os sentence embeddings não costumam generalizar bem com grandes trechos de texto. Eles não são comumente usados para representar textos maiores do que um parágrafo curto.

Uso de embeddings para busca por similaridade

Vamos supor que tenhamos uma grande coleção de perguntas e respostas. Um usuário pode fazer uma pergunta e queremos recuperar a pergunta mais semelhante em nossa coleção para ajudá-lo a encontrar uma resposta.

Poderíamos usar text embeddings para possibilitar a recuperação de perguntas semelhantes:

  • Durante a indexação, cada pergunta é executada por meio de um modelo de sentence embedding para produzir um vetor numérico.
  • Quando um usuário digita uma consulta, ela é executada por meio do mesmo modelo de sentence embedding para produzir um vetor. Para classificar as respostas, calculamos a similaridade do vetor entre cada pergunta e o vetor da consulta. Ao comparar vetores de embedding, é comum usar similaridade de cossenos.

Este repositório fornece um exemplo simples de como isso pode ser realizado no Elasticsearch. O script principal indexa cerca de 20 mil perguntas do conjunto de dados do Stack Overflow e permite ao usuário digitar consultas de texto livre para o conjunto de dados.

Logo examinaremos cada parte do script em detalhes, mas primeiro vamos ver alguns exemplos de resultados. Em muitos casos, o método é capaz de capturar similaridade mesmo quando não há uma sobreposição forte de palavras entre a consulta e a pergunta indexada:

  • “zipping up files” retorna “Compressing / Decompressing Folders & Files”
  • “determine if something is an IP” retorna “How do you tell whether a string is an IP or a hostname”
  • “translate bytes to doubles” retorna “Convert Bytes to Floating Point Numbers in Python”

Detalhes da implementação

O script começa fazendo download do modelo de embedding e criando-o no TensorFlow. Escolhemos o Universal Sentence Encoder do Google, mas é possível usar muitos outros métodos de embedding. O script usa o modelo de embedding como está, sem nenhum ajuste fino ou treinamento adicional.

Em seguida, criamos o índice do Elasticsearch, que inclui mapeamentos para o título da pergunta, tags e também o título da pergunta codificado como um vetor:

"mappings": {
  "properties": {
    "title": {
      "type": "text"
    },
    "title_vector": {
      "type": "dense_vector",
      "dims": 512
    }
    "tags": {
      "type": "keyword"
    },
    ...
  }
}

No mapeamento para dense_vector, é necessário especificar o número de dimensões que os vetores conterão. Ao indexar um campo title_vector, o Elasticsearch verificará se ele tem o mesmo número de dimensões especificado no mapeamento.

Para indexar documentos, executamos o título da pergunta por meio do modelo de embedding para obter uma matriz numérica. Essa matriz é adicionada ao documento no campo title_vector.

Quando um usuário digita uma consulta, o texto é executado primeiro por meio do mesmo modelo de embedding e armazenado no parâmetro query_vector. A partir da versão 7.3, o Elasticsearch fornece uma função cosineSimilarity em sua linguagem de script nativa. Portanto, para classificar as perguntas com base em sua similaridade com a consulta do usuário, usamos uma consulta script_score:

{
  "script_score": {
    "query": {"match_all": {}},
    "script": {
      "source": "cosineSimilarity(params.query_vector, doc['title_vector']) + 1.0",
      "params": {"query_vector": query_vector}
    }
  }
}

Nós passamos o vetor da consulta como um parâmetro de script para evitar recompilar o script em cada nova consulta. Como o Elasticsearch não permite pontuações negativas, é necessário adicionar 1 à similaridade de cossenos.

Observação: este post do blog originalmente usava uma sintaxe diferente para funções vetoriais que estava disponível no Elasticsearch 7.3, mas foi descontinuada na versão 7.6.

Limitações importantes

A consulta script_score foi projetada para encapsular uma consulta restritiva e modificar as pontuações dos documentos que ela retorna. No entanto, nós fornecemos uma consulta match_all, o que significa que o script será executado em todos os documentos no índice. Essa é uma limitação atual da similaridade de vetores no Elasticsearch — os vetores podem ser usados na pontuação de documentos, mas não na etapa de recuperação inicial. O suporte para recuperação com base na similaridade de vetores é uma área importante do trabalho contínuo.

Para evitar a necessidade de ler todos os documentos e a fim de manter um alto desempenho, a consulta match_all pode ser substituída por uma consulta mais seletiva. A consulta correta a ser usada para recuperação provavelmente dependerá do caso de uso específico.

Embora tenhamos visto alguns exemplos encorajadores acima, é importante observar que os resultados também podem gerar muito ruído e ser pouco intuitivos. Por exemplo, “zipping up files” também atribui pontuações altas a “Partial .csproj Files” e “How to avoid .pyc files?”. E quando o método retorna resultados surpreendentes, nem sempre fica claro como depurar o problema: o significado de cada componente vetorial geralmente é opaco e não corresponde a um conceito interpretável. Com as técnicas de pontuação tradicionais baseadas na sobreposição de palavras, geralmente é mais fácil responder à pergunta “por que esse documento tem uma classificação alta?”.

Como mencionamos anteriormente, el objetivo de este prototipo es servir como ejemplo de cómo podrían usarse los modelos de incrustación con campos de vectores, y no como una solución lista para la producción. Al desarrollar una estrategia de búsqueda nueva, es fundamental probar el rendimiento del enfoque en tus propios datos y asegurarte de compararlo con una referencia sólida como una búsqueda match. Puede ser necesario realizar cambios importantes en la estrategia para lograr resultados sólidos, incluido el ajuste del modelo de incrustación para el set de datos objetivo o la prueba de diferentes formas de incorporar incrustaciones como la expansión de búsqueda a nivel de la palabra.

Conclusões

As técnicas de embedding fornecem uma maneira poderosa de capturar o conteúdo linguístico de um texto. Ao indexar embeddings e pontuação com base na distância do vetor, podemos classificar os documentos por uma noção de similaridade que vai além da sobreposição no nível da palavra.

Estamos ansiosos para introduzir mais funcionalidades com base no tipo de campo vetorial. O uso de vetores para busca é uma área importante e refinada. Como sempre, gostaríamos que você nos contasse seus casos de uso e experiências no Github e nos fóruns de discussão!