Rag explicó: Comprender las incrustaciones, la similitud y la recuperación

Caminé Construyendo una tubería de trapo simple Uso de la API de OpenAI, Langchain y archivos locales, así como Coloque efectivamente archivos de texto grandes. Estas publicaciones cubren los conceptos básicos de configurar una tubería RAG capaz de generar respuestas basadas en el contenido de los archivos locales.

Imagen del autor

Entonces, hasta ahora, hemos hablado de leer los documentos de donde sea que estén almacenados, dividiéndolos en trozos de texto y luego crear una incrustación para cada fragmento. Después de eso, de alguna manera elegimos mágicamente los incrustaciones que son apropiadas para la consulta del usuario y generamos una respuesta relevante. Pero es importante comprender mejor cómo funciona realmente el paso de recuperación de RAG.

Por lo tanto, en esta publicación, llevaremos las cosas un paso más allá al observar más de cerca cómo funciona el mecanismo de recuperación y analizarlo con más detalle. Como en mi publicación anterior, usaré el Guerra y paz Texto como ejemplo, con licencia como dominio público y fácilmente accesible a través de Proyecto Gutenberg.

¿Qué pasa con los incrustaciones?

Para comprender cómo funciona el paso de recuperación del marco de RAG, es crucial comprender primero cómo se transforma y representa el texto en los incrustaciones. Para que los LLM manejen cualquier texto, debe ser en forma de vector, y para realizar esta transformación, necesitamos utilizar un modelo de incrustación.

Una incrustación es una representación vectorial de datos (en nuestro caso, texto) que captura su significado semántico. Cada palabra o oración del texto original se asigna a un vector de alta dimensión. Los modelos de incrustación utilizados para realizar esta transformación están diseñados de tal manera que significados similares dan como resultado vectores que están cerca uno del otro en el espacio vectorial. Por ejemplo, los vectores de las palabras feliz y alegre estarían cerca el uno del otro en el espacio vectorial, mientras que el vector para la palabra triste estaría lejos de ellos.

Para crear incrustaciones de alta calidad que funcionen de manera efectiva en una tubería de trapo, se necesita utilizar modelos de incrustación previos a la aparición, como Bert y GPT. Existen varios tipos de embebidos que uno puede crear y los modelos correspondientes disponibles. Por ejemplo:

  • Incrustaciones de palabras: En las incrustaciones de palabras, cada palabra tiene un vector fijo independientemente del contexto. Los modelos populares para crear este tipo de incrustación son Word2vec y Guante.
  • INCREMENTOS CONTEXTUALES: Las integridades contextuales tienen en cuenta que el significado de una palabra puede cambiar en función del contexto. Tomar, por ejemplo, la orilla de un río y Abrir una cuenta bancaria. Algunos modelos que pueden usarse para producir incrustaciones contextuales son Bert, Robertay GPT.
  • Incrustaciones de oraciones: Estas son incrustaciones que capturan el significado de oraciones completas. Los modelos respectivos que se pueden usar son Frase-bert o usar.

En cualquier caso, el texto debe transformarse en vectores para ser utilizables en los cálculos. Estos vectores son simplemente representaciones del texto. En otras palabras, los vectores y los números no tienen ningún significado inherente por su cuenta. En cambio, son útiles porque capturan similitudes y relaciones entre palabras o frases en forma matemática.

Por ejemplo, podríamos imaginar un pequeño vocabulario que consiste en las palabras rey, reina, mujery hombrey asignar a cada uno de ellos un vector arbitrario.

king  = [0.25, 0.75]  
queen = [0.23, 0.77]  
man   = [0.15, 0.80]  
woman = [0.13, 0.82]  

Luego, podríamos intentar hacer algunas operaciones vectoriales como:

king - man + woman  
= [0.25, 0.75] - [0.15, 0.80] + [0.13, 0.82]  
= [0.23, 0.77]  
≈ queen 👑  

Observe cómo se conservan la semántica de las palabras y las relaciones entre ellas después de mapearlas en vectores, lo que nos permite realizar operaciones.

Entonces, una incrustación es solo eso: un mapeo de palabras para vectores, con el objetivo de preservar el significado y las relaciones entre las palabras, y permitir realizar cálculos con ellos. Incluso podemos visualizar estos vectores ficticios en un espacio vectorial para ver cómo las palabras relacionadas se agrupan.

Imagen del autor

La diferencia entre estos simples ejemplos de vectores y los vectores reales producidos por los modelos de incrustación es que los modelos reales de incrustación generan vectores con cientos de dimensiones. Los vectores bidimensionales son útiles para desarrollar la intuición sobre cómo se puede asignar el significado en un espacio vectorial, pero son demasiado bajas dimensionales para capturar la complejidad del lenguaje real y el vocabulario. Es por eso que los modelos reales de incrustación funcionan con dimensiones mucho más altas, a menudo en cientos o incluso miles. Por ejemplo, Word2vec produce vectores de 300 dimensiones, mientras que Base de Bert produce 768 vectores dimensionales. Esta dimensionalidad más alta permite que los incrustaciones capturen las múltiples dimensiones del lenguaje real, como el significado, el uso, la sintaxis y el contexto de palabras y frases.

Evaluar la similitud de los incrustaciones

Después de que el texto se transforma en incrustaciones, la inferencia se convierte en matemáticas vectoriales. Esto es exactamente lo que nos permite identificar y recuperar documentos relevantes en el paso de recuperación del marco RAG. Una vez que convirtimos la consulta del usuario y los documentos de la base de conocimiento en vectores utilizando un modelo de incrustación, podemos calcular cuán similares están usando similitud de coseno.

La similitud de coseno es una medida de cuán similares son dos vectores (incrustaciones). Dados dos vectores A y B, la similitud del coseno se calcula de la siguiente manera:

Imagen del autor

En pocas palabras, la similitud de coseno se calcula como el coseno del ángulo entre dos vectores, y varía de 1 a -1. Más específicamente:

  • 1 indica que los vectores son semánticamente idénticos (por ejemplo, auto y automóvil).
  • 0 indica que los vectores no tienen relación semántica (por ejemplo, banana y justicia).
  • -1 indica que los vectores son semánticamente opuestos (por ejemplo, caliente y frío).

En la práctica, sin embargo, los valores cercanos a -1 son extremadamente raros en la incrustación de modelos. Esto se debe a que incluso palabras semánticamente opuestas (como caliente y frío) a menudo ocurren en contextos similares (por ejemplo, se está calentando y se está enfriando). Para la similitud cosena al alcance -1, las palabras en sí mismas y sus contextos tendrían que ser perfectamente opuestos, algo que realmente no sucede en el lenguaje natural. Como resultado, incluso las palabras opuestas generalmente tienen incrustaciones que aún tienen un significado un poco cercano.

Existen otras métricas de similitud aparte de la similitud coseno, como el producto DOT o la distancia euclidiana, pero estas no están normalizadas y dependen de la magnitud, lo que las hace menos adecuadas para comparar los incrustaciones de texto. De esta manera, la similitud de coseno es la métrica dominante utilizada para cuantificar la similitud entre los incrustaciones.

Volviendo a nuestra tubería de RAG, al calcular la similitud de coseno entre las incrustaciones de consultas del usuario y las incrustaciones de la base de conocimiento, podemos identificar los trozos de texto que son más similares y, por lo tanto, contextualmente relevantes, a la pregunta del usuario, recuperarlos y luego usarlos para generar la respuesta.

Encontrar los top k trozos similares

Entonces, después de obtener los incrustaciones de la base de conocimiento y la (s) incrustación (s) para el texto de la consulta del usuario, aquí es donde ocurre la magia. Lo que esencialmente hacemos es que calculemos la similitud de coseno entre la incrustación de consultas de usuario y los incrustaciones de la base de conocimiento. Por lo tanto, para cada trozo de texto de la base de conocimiento, obtenemos una puntuación entre 1 y -1 que indica la similitud de la fragmentación con la consulta del usuario.

Una vez que tenemos los puntajes de similitud, los clasificamos en orden descendente y seleccionamos los trozos de K superiores. Estos trozos de K superiores se pasan al paso de generación de la tubería RAG, lo que le permite recuperar eficazmente la información relevante para la consulta del usuario.

Para acelerar este proceso, el Vecino más cercano (Ann) La búsqueda se usa a menudo. Ann encuentra vectores que son casi los más similares, entregando resultados cercanos al verdadero Top-N pero a una velocidad mucho más rápida que los métodos de búsqueda exactos. Por supuesto, la búsqueda exacta es más precisa; No obstante, también es más costoso computacionalmente y puede no escalar bien en aplicaciones del mundo real, especialmente cuando se trata de conjuntos de datos masivos.

Además de esto, se puede aplicar un umbral a los puntajes de similitud para filtrar trozos que no cumplen con una puntuación de relevancia mínima. Por ejemplo, en algunos casos, una fragmentación solo podría considerarse si su puntaje de similitud excede un cierto umbral (por ejemplo, similitud de coseno> 0.3).

Entonces, ¿quién es Anna Pávlovna?

En el ‘Guerra y paz‘ ejemplo, Como se demuestra en mi publicación anteriordividimos todo el texto en trozos y luego creamos los incrustaciones respectivas para cada fragmento. Luego, cuando el usuario envía una consulta, como ‘¿Quién es Anna Pávlovna?? ‘, También creamos los respectivos incrustaciones para el texto de consulta del usuario.

import os
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import TextLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document

api_key = 'your_api_key'

# initialize LLM
llm = ChatOpenAI(openai_api_key=api_key, model="gpt-4o-mini", temperature=0.3)

# initialize embeddings model
embeddings = OpenAIEmbeddings(openai_api_key=api_key)

# loading documents to be used for RAG 
text_folder =  "RAG files"  

documents = []
for filename in os.listdir(text_folder):
    if filename.lower().endswith(".txt"):
        file_path = os.path.join(text_folder, filename)
        loader = TextLoader(file_path)
        documents.extend(loader.load())

splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
split_docs = []
for doc in documents:
    chunks = splitter.split_text(doc.page_content)
    for chunk in chunks:
        split_docs.append(Document(page_content=chunk))
        
documents = split_docs

# create vector database w FAISS 
vector_store = FAISS.from_documents(documents, embeddings)
retriever = vector_store.as_retriever()

def main():
    print("Welcome to the RAG Assistant. Type 'exit' to quit.\n")
    
    while True:
        user_input = input("You: ").strip()
        if user_input.lower() == "exit":
            print("Exiting…")
            break

        # get relevant documents
        relevant_docs = retriever.invoke(user_input)
        retrieved_context = "\n\n".join([doc.page_content for doc in relevant_docs])

        # system prompt
        system_prompt = (
            "You are a helpful assistant. "
            "Use ONLY the following knowledge base context to answer the user. "
            "If the answer is not in the context, say you don't know.\n\n"
            f"Context:\n{retrieved_context}"
        )

        # messages for LLM 
        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_input}
        ]

        # generate response
        response = llm.invoke(messages)
        assistant_message = response.content.strip()
        print(f"\nAssistant: {assistant_message}\n")

if __name__ == "__main__":
    main()

En este script, utilicé el objeto Retriever de Langchain retriever = vector_store.as_retriever()que por defecto utiliza la similitud de coseno para evaluar la relevancia de los incrustaciones del documento con la consulta del usuario. También recupera por defecto los documentos k = 4. Por lo tanto, en esencia, lo que estamos haciendo allí es que recuperamos el Top K más relevante para los fragmentos de consulta de usuario basados ​​en la similitud de coseno.

En cualquier caso, Langcahin’s .as_retriever() El método no nos permite mostrar los valores de similitud de coseno: solo obtenemos los k trozos relevantes. Entonces, para echar un vistazo a las similitudes de coseno, voy a ajustar nuestro guión un poco y usar .similarity_search_with_score() en lugar de .as_retriever(). Podemos hacer esto fácilmente agregando la siguiente parte a nuestro main() función:

# REMOVE THIS LINE
retriever = vector_store.as_retriever()

def main():
    print("Welcome to the RAG Assistant. Type 'exit' to quit.\n")

    while True:
        user_input = input("You: ").strip()
        if user_input.lower() == "exit":
            print("Exiting…")
            break
        
        # ADD THIS SECTION   
        # Similarity search with score 
        results = vector_store.similarity_search_with_score(user_input, k=2)

        # Extract documents and cosine similarity scores
        print(f"\nCosine Similarities for Top 5 Chunks:\n")
        for idx, (doc, sim_score) in enumerate(results):
            print(f"Chunk {idx + 1}:")
            print(f"Cosine Similarity: {sim_score:.4f}")
            print(f"Content:\n{doc.page_content}\n")
        
        # CONTINUE WITH REST OF THE CODE...
        # System prompt for LLM generation
        retrieved_context = "\n\n".join([doc.page_content for doc, _ in results])

Observe cómo podemos definir explícitamente el número de trozos recuperados k, ahora establecido como k = 2.

Finalmente, podemos volver a preguntar y recibir una respuesta:

Imagen del autor

… Pero ahora también podemos ver los fragmentos de texto en función de los cuales se crea esta respuesta y los respectivos puntajes de similitud de coseno …

Imagen del autor

Aparentemente, diferentes parámetros pueden dar lugar a diferentes respuestas. Por ejemplo, vamos a obtener respuestas ligeramente diferentes al recuperar los resultados Top K = 2, K = 4 y K = 10. Teniendo en cuenta los parámetros adicionales que se usan en el paso de fragmentación, como el tamaño del trozo y la superposición del fragmento, se hace obvio que los parámetros juegan un papel crucial para obtener buenos resultados de una tubería de trapo.

• • •

¿Me encantó esta publicación? ¡Seamos amigos! Únete a mí en:

📰Sustitución 💌 Medio 💼LinkedIn Cómprame un café!

• • •