Reranking: Potencializando o Desempenho do RAG
Estratégias para otimizar a performance do RAG.
Em aplicações baseadas em LLMs, comumente usa-se a estratégia do RAG para realizar buscas semânticas em grandes conjuntos de documentos de texto, transformando-os em vetores para análise de similaridade.
No entanto, devido à compressão de informações em vetores, o RAG pode recuperar documentos pouco relevantes, comprometendo a eficácia da busca.
Como forma de amenizar esse problema, algumas arquiteturas propõem a implementação de uma etapa extra no desenho de sistemas baseados em LLM — um sistema de reranking, onde os documentos recuperados inicialmente são posteriormente reordenados de acordo com a sua relevância em relação à consulta.
Essa abordagem maximiza a eficácia da recuperação, garantindo que os documentos mais relevantes sejam apresentados ao usuário, melhorando assim a precisão e relevância das respostas fornecidas pelo RAG.
Ao minimizar a quantidade de documentos irrelevantes passados para o modelo de linguagem, a qualidade das respostas geradas é significativamente aprimorada, mitigando o problema inicial de retorno de informações pouco relevantes.
📚 Você é dev e quer aprender um pouco mais sobre a criação de aplicações baseadas em LLM?
Eu criei um curso que aborda aspectos teóricos e práticos do desenvolvimento de aplicações baseadas em LLMs. Alguns dos tópicos cobertos:
🟠 O que são e como criar embeddings
🟡 Como selecionar partes relevantes nos seus documentos
🔵 Como integrar essas partes documentos com uma LLM
Limitações da busca semântica
Com o RAG, estamos realizando uma busca semântica em muitos documentos de texto — estes podem ser de dezenas de milhares até dezenas de bilhões de documentos.
Para garantir tempos de busca rápidos em escala, geralmente usamos busca vetorial — ou seja, transformamos nosso texto em vetores, inserimos estes em um espaço vetorial e comparamos sua proximidade com um vetor de consulta usando uma métrica de similaridade, como a similaridade cosseno.
Para que a busca vetorial funcione, precisamos de vetores.
Esses vetores são essencialmente compressões do “significado” por trás de algum texto em vetores (tipicamente) de 768 ou 1536 dimensões. Há alguma perda de informação porque estamos comprimindo essa informação em um único vetor.
Devido a essa perda de informação, frequentemente observamos que os principais documentos da busca vetorial não são necessariamente os mais relevantes.
Agora imagine, por exemplo, que algum documento em uma posição inferior (lá pelo 20o ou 25o) poderia ajudar nossa LLM a formular melhores respostas. O que fazer?
Limite da Janela de Contexto
Uma solução mais simples seria aumentar o número de documentos que estamos retornando (ou seja, aumentar o top_k
) e passar todos esses documentos para a LLM.
No entanto, não podemos passar todos os documentos para nossa LLM. Primeiro pois as LLMs têm limites sobre quanto texto podemos passar para eles — chamamos esse limite de janela de contexto.
Mas, calma ai. Sabemos que algumas LLMs têm janelas de contexto enormes, como o Claude da Anthropic, com uma janela de contexto de 100K tokens, não é verdade?
Com isso, não poderíamos incluir muitas centenas de documentos até “preencher” a toda a capacidade da janela de contexto para melhorar a recuperação?
Novamente, não podemos usar preenchimento de contexto porque isso reduz o desempenho de recuperação do LLM, como evidenciado neste estudo científico, que percebeu que a qualidade da resposta da LLM degrada à medida que adicionamos mais tokens à janela de contexto.
Como podemos observar na imagem acima, a medida que aumentamos a quantidade de documentos que passamos para uma LLM, a capacidade de uma LLM de responder apropriadamente uma pergunta torna-se pior do que se o documento não tivesse sido fornecido em primeiro lugar.
Os LLMs também são menos propensos a seguir instruções à medida que preenchemos a janela de contexto — portanto, o preenchimento de contexto é uma má ideia.
Podemos aumentar o número de documentos retornados por nosso banco de dados vetorial para aumentar a recuperação da recuperação, mas não podemos passar esses documentos para nosso LLM sem prejudicar a recuperação do LLM.
ReRanking de documentos
Uma solução proposta para esse problema é maximizar a recuperação da recuperação, recuperando muitos documentos, e depois maximizar a recuperação do LLM minimizando o número de documentos que chegam ao LLM.
Para fazer isso, repriorizamos os documentos recuperados e mantemos apenas os mais relevantes para nosso LLM — para fazer isso, usamos um ReRanking.
Um modelo de ReRanking — também conhecido como um cross-encoder — é um tipo de modelo que, dado um par de consulta e documento, emitirá uma pontuação de similaridade. Usamos essa pontuação para reorganizar os documentos por relevância para nossa consulta.
Recuperação em dois estágios
Engenheiros de busca têm usado rerankers em sistemas de recuperação em dois estágios por muito tempo. Nestes sistemas são desenhados com dois estágios:
Primeiro, um modelo de incorporação/retriever recupera um conjunto de documentos relevantes de um conjunto de dados maior.
Segundo, um modelo de priorização (o reranker) é usado para reorganizar esses documentos recuperados pelo modelo de primeiro estágio.
Usamos dois estágios porque recuperar um pequeno conjunto de documentos de um grande conjunto de dados é muito mais rápido do que usar um ReRanker em um grande conjunto de documentos; RerRnkers são lentos e retreivers são rápidos.
Mas se um reranker é muito mais lento, por que se preocupar em usá-los? A resposta é que rerankers são muito mais precisos do que modelos de incorporação.
Busca semântica ou ReRanker?
A intuição por trás de uma busca semântica (ou bi-encoder) é que para realizar essas buscas, precisamos antes comprimir todos os documento em vetores, que é um processo lossy, o que significa que perdemos informação.
Por outro lado, um ReRanker pode receber as informações brutas diretamente, significando menos perda de informação. Como executamos o ReRanker no momento da consulta do usuário, temos o benefício adicional de analisar o significado de nosso documento específico para a consulta do usuário — em vez de tentar produzir um significado genérico médio.
Implementando um ReRank
Há diversas técnicas para criação de ReRankers.
Nesse texto vamos usar a biblioteca FlashRank, que é relativamente madura e possui integração com o LangChain.
Para instalar, basta um
pip install flashrank
Em seguida, basta criar o objeto Ranker
.
from flashrank import Ranker, RerankRequest
ranker = Ranker()
Por fim, basta passar a query junto com os documentos selecionados. Estes documentos devem vir de uma busca semântica, mas, por simplicidade, vou defini-los diretamente no código:
query = "Como acelerar LLMs?"
passages = [
{
"id": 1,
"text": "Introduza *lookahead decoding*: - um algoritmo de decodificação paralela para acelerar a inferência do LLM - sem a necessidade de um modelo preliminar ou armazenamento de dados - diminui linearmente o número de etapas de decodificação em relação ao log(FLOPs) utilizado por etapa de decodificação.",
"meta": {"additional": "info1"}
},
{
"id": 2,
"text": "A eficiência da inferência do LLM será um dos tópicos mais cruciais tanto para a indústria quanto para a academia, simplesmente porque quanto mais eficiente você for, mais dinheiro economizará. O projeto vLLM é uma leitura obrigatória para esta direção, e agora eles acabaram de lançar o artigo.",
"meta": {"additional": "info2"}
},
{
"id": 3,
"text": "Existem muitas maneiras de aumentar a taxa de transferência da inferência do LLM (tokens/segundo) e diminuir a pegada de memória, às vezes ao mesmo tempo. Aqui estão alguns métodos que descobri serem eficazes ao trabalhar com o Llama 2. Esses métodos são todos bem integrados com a Hugging Face. Esta lista está longe de ser exaustiva; alguns desses métodos podem ser usados em combinação entre si e há muitos outros para experimentar. - Bettertransformer (Biblioteca Ótima): Basta chamar `model.to_bettertransformer()` no seu modelo Hugging Face para uma melhoria modesta em tokens por segundo. - Fp4 Mixed-Precision (Bitsandbytes): Requer configuração mínima e reduz drasticamente a pegada de memória do modelo. - AutoGPTQ: Demorado, mas leva a um modelo muito menor e inferência mais rápida. A quantização é um custo único que se paga a longo prazo.",
"meta": {"additional": "info3"}
},
{
"id": 4,
"text": "Já quis fazer sua inferência do LLM ir brrrrr, mas ficou preso na implementação da decodificação especulativa e na busca pelo modelo preliminar adequado? Não mais dor! Empolgado em apresentar Medusa, um framework simples que remove o irritante modelo preliminar enquanto obtém um aumento de velocidade de 2x.",
"meta": {"additional": "info4"}
},
{
"id": 5,
"text": "vLLM é uma biblioteca rápida e fácil de usar para inferência e atendimento do LLM. vLLM é rápido com: Taxa de atendimento de última geração. Gerenciamento eficiente da memória de chave e valor de atenção com PagedAttention. Agrupamento contínuo de solicitações recebidas. Kernels CUDA otimizados.",
"meta": {"additional": "info5"}
}
]
Nesse exemplo, os metadados são opcionais. O ID pode ser o ID do seu banco de dados da etapa de recuperação.
Por fim, basta passar esses valores para os objetos RerankRequest
e Ranker
.
rerankrequest = RerankRequest(query=query, passages=passages)
results = ranker.rerank(rerankrequest)
print(results)
Conclusão
O reranking é um dos métodos mais simples que ajuda a melhorar o desempenho de recuperação no modelo do RAG.
Nesse texto, exploramos por que os rerankers podem oferecer um desempenho muito superior aos seus equivalentes de modelo de incorporação — e como um sistema de recuperação em dois estágios nos permite obter o melhor dos dois mundos, possibilitando a busca em grande escala enquanto mantemos um desempenho de qualidade.