30 October, 2024

Coder un système RAG simple

Explorez la génération augmentée de récupération (RAG) et créez-en un à partir de zéro pour comprendre les concepts fondamentaux.

Coder un système RAG simple
Available in:
 English
 French
 Vietnamese
Reading time: 14 min.
Table of content

    Récemment, la génération augmentée de récupération (en anglais: Retrieval-Augmented Generation - RAG) est apparue comme un paradigme puissant dans le domaine de l'IA et des modèles de langage à grande échelle (en anglais: Large Language Model - LLM). Le RAG combine la recherche d'informations avec la génération de texte pour améliorer les performances des modèles de langage en intégrant des sources de connaissances externes. Cette approche a montré des résultats prometteurs dans diverses applications, telles que les systèmes de questions-réponses, les systèmes de dialogue et la génération de contenu.

    Dans cet article, nous explorerons le RAG et construirons un système RAG simple à partir de zéro en utilisant Python et ollama. Ce projet vous aidera à comprendre les composants clés des systèmes RAG et comment ils peuvent être mis en œuvre en utilisant des concepts de programmation fondamentaux.

    Qu'est-ce que le RAG ?

    Pour commencer, examinons un simple système de chatbot sans RAG :

    Bien que le chatbot puisse répondre à des questions courantes en fonction de son jeu de données d'entraînement, il peut manquer d'accès aux connaissances les plus à jour ou spécifiques à un domaine.

    Un exemple du monde réel serait de demander à ChatGPT "Quel est le nom de ma mère ?". ChatGPT ne peut pas répondre à cette question car il n'a pas accès à des connaissances externes, comme les informations sur les membres de votre famille.

    Réponse échouée

    Pour résoudre cette limitation, nous devons fournir des connaissances externes au modèle (dans cet exemple, une liste des noms des membres de la famille) :

    Un système RAG se compose de deux composants clés :

    • Un modèle de récupération qui récupère les informations pertinentes à partir d'une source de connaissances externe, qui peut être une base de données, un moteur de récupération ou tout autre dépôt d'informations.
    • Un modèle de langage qui génère des réponses en fonction des connaissances récupérées.

    Il existe plusieurs façons de mettre en œuvre le RAG, notamment le Graph RAG, le Hybrid RAG et le Hierarchical RAG, que nous aborderons à la fin de cet article.

    RAG simple

    Créons un système RAG simple qui récupère des informations à partir d'un jeu de données prédéfini et génère des réponses en fonction des connaissances récupérées. Le système comprendra les composants suivants :

    1. Modèle embeddings : Un modèle de langage pré-entraîné qui convertit le texte d'entrée en embeddings - des représentations vectorielles qui capturent le sens sémantique. Ces vecteurs seront utilisés pour rechercher les informations pertinentes dans le jeu de données.
    2. Base de données vectorielle (en anglais: vector database) : Un système de stockage pour les connaissances et leurs vecteurs embeddings correspondants. Bien qu'il existe de nombreuses technologies de bases de données vectorielles comme Qdrant, Pinecone et pgvector, nous mettrons en œuvre une simple base de données en mémoire à partir de zéro.
    3. Chatbot : Un modèle de langage qui génère des réponses en fonction des connaissances récupérées. Il peut s'agir de n'importe quel modèle de langage, comme Llama, Gemma ou GPT.

    Phase d'indexation

    La phase d'indexation est la première étape de la création d'un système RAG. Elle consiste à décomposer le jeu de données (ou les documents) en petits segments (ou "chunk" en anglais) et à calculer une représentation vectorielle pour chaque segment qui peut être efficacement recherchée lors de la génération.

    La taille de chaque segment peut varier en fonction du jeu de données et de l'application. Par exemple, dans un système de récupération de documents, chaque segment peut être un paragraphe ou une phrase. Dans un système de dialogue, chaque segment peut être un tour de conversation.

    Après la phase d'indexation, chaque segment avec son vecteur embeddings correspondant sera stocké dans la base de données vectorielle. Voici un exemple de ce à quoi pourrait ressembler la base de données vectorielle après l'indexation :

    SegmentVecteur embeddings
    L'Italie et la France produisent plus de 40% de tout le vin du monde.[0.1, 0.04, -0.34, 0.21, ...]
    Le Taj Mahal en Inde est entièrement fait de marbre.[-0.12, 0.03, 0.9, -0.1, ...]
    90% de l'eau douce du monde se trouve en Antarctique.[-0.02, 0.6, -0.54, 0.03, ...]
    ......

    Les vecteurs embeddings peuvent ensuite être utilisés pour récupérer les informations les plus pertinentes en fonction d'une requête donnée. Pensez-y comme à une clause SQL WHERE, mais au lieu de faire une requête par correspondance de texte exacte, nous pouvons maintenant interroger un ensemble de segments en fonction de leurs représentations vectorielles.

    Pour comparer la similarité entre deux vecteurs, nous pouvons utiliser la similarité cosinus, la distance euclidienne ou d'autres métriques de distance. Dans cet exemple, nous utiliserons la similarité cosinus. Voici la formule de la similarité cosinus entre deux vecteurs A et B :

    Ne vous inquiétez pas si vous n'êtes pas familier avec la formule ci-dessus, nous l'implémenterons dans la prochaine section.

    Phase de récupération

    Dans cette phase, nous prendrons l'exemple d'une Requête d'entrée donnée par un Utilisateur. Nous calculons ensuite le Vecteur de la requête pour représenter la requête, et le comparons aux vecteurs de la base de données pour trouver les segments les plus pertinents.

    Le résultat renvoyé par la Base de données vectorielle contiendra les N principaux segments les plus pertinents pour la requête. Ces segments seront utilisés par le Chatbot pour générer une réponse.

    Codons-le

    Dans cet exemple, nous écrirons une implémentation Python simple du RAG.

    Pour exécuter les modèles, nous utiliserons ollama, un outil en ligne de commande qui vous permet d'exécuter des modèles de Hugging Face. Avec ollama, vous n'avez pas besoin d'avoir accès à un serveur ou à un service cloud pour exécuter les modèles. Vous pouvez les exécuter directement sur votre ordinateur.

    Pour les modèles, utilisons les suivants :

    Et pour le jeu de données, nous utiliserons une simple liste de faits sur les chats. Chaque fait sera considéré comme un segment dans la phase d'indexation.

    Téléchargement d'ollama et des modèles

    Commençons par installer ollama à partir du site web du projet : ollama.com

    Une fois installé, ouvrez un terminal et exécutez les commandes suivantes pour télécharger les modèles requis :

    ollama pull hf.co/CompendiumLabs/bge-base-en-v1.5-gguf
    ollama pull hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF
    

    Si vous voyez la sortie suivante, cela signifie que les modèles ont été téléchargés avec succès :

    pulling manifest
    ...
    verifying sha256 digest
    writing manifest
    success
    

    Avant de continuer, pour utiliser ollama en Python, installons également le package ollama :

    pip install ollama
    

    Chargement du jeu de données

    Ensuite, créez un script Python et chargez le jeu de données en mémoire. Le jeu de données contient une liste de faits sur les chats qui seront utilisés comme segments dans la phase d'indexation.

    Vous pouvez télécharger le jeu de données d'exemple ici. Voici un exemple de code pour charger le jeu de données :

    dataset = []
    with open('cat-facts.txt', 'r') as file:
      dataset = file.readlines()
      print(f'Loaded {len(dataset)} entries')
    

    Implémentation de la base de données vectorielle

    Maintenant, implémentons la base de données vectorielle.

    Nous utiliserons le modèle embeddings d'ollama pour convertir chaque segment en un vecteur embeddings, puis nous stockerons le segment et son vecteur correspondant dans une liste.

    Voici un exemple de fonction pour calculer le vecteur embeddings pour un texte donné :

    import ollama
    
    EMBEDDING_MODEL = 'hf.co/CompendiumLabs/bge-base-en-v1.5-gguf'
    LANGUAGE_MODEL = 'hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF'
    
    # Chaque élément de la VECTOR_DB sera un tuple (segment, embeddings)
    # L'embeddings est une liste de flottants, par exemple : [0.1, 0.04, -0.34, 0.21, ...]
    VECTOR_DB = []
    
    def add_chunk_to_database(chunk):
      embedding = ollama.embed(model=EMBEDDING_MODEL, input=chunk)['embeddings'][0]
      VECTOR_DB.append((chunk, embedding))
    

    Dans cet exemple, nous considérerons chaque ligne du jeu de données comme un segment par souci de simplicité.

    for i, chunk in enumerate(dataset):
      add_chunk_to_database(chunk)
      print(f'Added chunk {i+1}/{len(dataset)} to the database')
    

    Implémentation de la fonction de récupération

    Ensuite, implémentons la fonction de récupération qui prend une requête et renvoie les N principaux segments les plus pertinents en fonction de la similarité cosinus. Nous pouvons imaginer que plus la similarité cosinus entre deux vecteurs est élevée, plus ils sont "proches" dans l'espace vectoriel. Cela signifie qu'ils sont plus similaires en termes de sens.

    Voici un exemple de fonction pour calculer la similarité cosinus entre deux vecteurs :

    Voici la suite de l'implémentation de la fonction de récupération :

    def cosine_similarity(a, b):
      dot_product = sum([x * y for x, y in zip(a, b)])
      norm_a = sum([x ** 2 for x in a]) ** 0.5
      norm_b = sum([x ** 2 for x in b]) ** 0.5
      return dot_product / (norm_a * norm_b)
    
    def recuperer(requete, top_n=3):
      query_embedding = ollama.embed(model=EMBEDDING_MODEL, input=chunk)['embeddings'][0]
      # liste temporaire pour stocker les paires (segment, similarité)
      similarities = []
      for chunk, embedding in VECTOR_DB:
        similarity = cosine_similarity(query_embedding, embedding)
        similarities.append((chunk, similarity))
      # trier par similarité dans l'ordre décroissant, car une similarité plus élevée signifie des segments plus pertinents
      similarities.sort(key=lambda x: x[1], reverse=True)
      # enfin, renvoyer les N principaux segments les plus pertinents
      return similarities[:top_n]
    

    Phase de génération

    Dans cette phase, le chatbot générera une réponse en fonction des connaissances récupérées à l'étape précédente. Cela se fait simplement en ajoutant les segments à l'invite qui sera utilisée comme entrée pour le chatbot.

    Par exemple, une invite peut être construite comme suit :

    input_query = input('Ask me a question: ')
    retrieved_knowledge = retrieve(input_query)
    
    print('Retrieved knowledge:')
    for chunk, similarity in retrieved_knowledge:
      print(f' - (similarity: {similarity:.2f}) {chunk}')
    
    instruction_prompt = f'''You are a helpful chatbot.
    Use only the following pieces of context to answer the question. Don't make up any new information:
    {'\n'.join([f' - {chunk}' for chunk, similarity in retrieved_knowledge])}
    '''
    

    Nous utilisons ensuite ollama pour générer la réponse. Dans cet exemple, nous utiliserons invite_instruction comme message système :

    stream = ollama.chat(
      model=LANGUAGE_MODEL,
      messages=[
        {'role': 'system', 'content': instruction_prompt},
        {'role': 'user', 'content': input_query},
      ],
      stream=True,
    )
    
    # afficher la réponse du chatbot en temps réel
    print('Chatbot response:')
    for chunk in stream:
      print(chunk['message']['content'], end='', flush=True)
    

    Tout rassembler

    Vous pouvez trouver le code final dans ce fichier. Pour exécuter le code, enregistrez-le dans un fichier nommé demo.py et exécutez la commande suivante :

    python demo.py
    

    Vous pouvez maintenant poser des questions au chatbot, et il générera des réponses en fonction des connaissances récupérées à partir du jeu de données.

    Ask me a question: tell me about cat speed
    Retrieved chunks: ...
    Chatbot response:
    According to the given context, cats can travel at approximately 31 mph (49 km) over a short distance. This is their top speed.
    

    Pistes d'amélioration

    Jusqu'à présent, nous avons implémenté un système RAG simple à l'aide d'un petit jeu de données. Cependant, il existe encore de nombreuses limites :

    • Si la question couvre plusieurs sujets à la fois, le système peut ne pas être en mesure de fournir une bonne réponse. C'est parce que le système ne récupère des segments que sur la base de la similarité de la requête avec les segments, sans tenir compte du contexte de la requête. La solution pourrait être de faire en sorte que le chatbot formule sa propre requête en fonction de l'entrée de l'utilisateur, puis de récupérer les connaissances en fonction de la requête générée. Nous pouvons également utiliser plusieurs requêtes pour récupérer des informations plus pertinentes.
    • Les N principaux résultats sont renvoyés en fonction de la similarité cosinus. Cela peut ne pas toujours donner les meilleurs résultats, surtout lorsque chaque segment contient beaucoup d'informations. Pour résoudre ce problème, nous pouvons utiliser un modèle de reclassement pour reclasser les segments récupérés en fonction de leur pertinence pour la requête.
    • La base de données est stockée en mémoire, ce qui peut ne pas être évolutif pour des jeux de données volumineux. Nous pouvons utiliser une base de données vectorielle plus efficace comme Qdrant, Pinecone, pgvector.
    • Actuellement, nous considérons que chaque phrase est un segment. Pour des tâches plus complexes, nous pourrons avoir besoin d'utiliser des techniques plus sophistiquées pour décomposer le jeu de données en segments plus petits. Nous pouvons également pré-traiter chaque segment avant de les ajouter à la base de données.
    • Le modèle de langage utilisé dans cet exemple est simple et ne compte que 1 milliard de paramètres. Pour des tâches plus complexes, nous aurons peut-être besoin d'utiliser un modèle de langage plus important.

    Autres types de RAG

    Dans la pratique, il existe de nombreuses façons de mettre en œuvre des systèmes RAG. Voici quelques types de systèmes RAG courants :

    • Graph RAG : Dans ce type de RAG, la source de connaissances est représentée sous forme de graphe, où les nœuds sont des entités et les arêtes sont des relations entre les entités. Le modèle de langage peut parcourir le graphe pour récupérer les informations pertinentes. Il y a de nombreuses recherches actives sur ce type de RAG. Voici une collection d'articles sur le Graph RAG.
    • Hybrid RAG : un type de RAG qui combine les graphes de connaissances (KG) et les techniques de base de données vectorielles pour améliorer les systèmes de questions-réponses. Pour en savoir plus, vous pouvez lire l'article ici.
    • Modular RAG : un type de RAG qui va au-delà du processus de base "récupérer puis générer", en utilisant des mécanismes de routage, de planification et de fusion pour créer un cadre flexible et reconfigurable. Cette conception modulaire permet différents modes de RAG (linéaire, conditionnel, ramifié et boucle), permettant des applications plus sophistiquées et adaptables impliquant des connaissances. Pour en savoir plus, vous pouvez lire l'article ici.

    Pour d'autres types de RAG, vous pouvez vous référer à cet article de Rajeev Sharma.

    Conclusion

    Le RAG représente une avancée significative dans la rend les modèles de langage plus connaisseurs et plus précis. En implémentant un système RAG simple à partir de zéro, nous avons exploré les concepts fondamentaux de l'embeddings, de la récupération et de la génération. Bien que notre implémentation soit basique, elle démontre les principes de base qui alimentent les systèmes RAG plus sophistiqués utilisés dans les environnements de production.

    Les possibilités d'extension et d'amélioration des systèmes RAG sont vastes, de la mise en œuvre de bases de données vectorielles plus efficaces à l'exploration d'architectures avancées comme le Graph RAG et le Hybrid RAG. À mesure que le domaine continue d'évoluer, le RAG reste une technique cruciale pour enrichir les systèmes d'IA avec des connaissances externes tout en maintenant leurs capacités de génération.

    Références

    Want to receive latest articles from my blog?
    Follow on