Construire un système RAG multimodal avec Elasticsearch : L'histoire de Gotham City
Apprenez à construire un système multimodal Retrieval-Augmented Generation (RAG) qui intègre des données textuelles, audio, vidéo et des images afin de fournir une recherche d'informations plus riche et contextualisée.
Dans ce blog, vous apprendrez à construire un pipeline RAG (Retrieval-Augmented Generation) multimodal à l'aide d'Elasticsearch. Nous verrons comment exploiter ImageBind pour générer des embeddings pour différents types de données, notamment le texte, les images, l'audio et les cartes de profondeur. Vous découvrirez également comment stocker et récupérer efficacement ces embeddings dans Elasticsearch à l'aide de dense_vector et de k-NN search. Enfin, nous intégrerons un modèle de langage étendu (LLM) pour analyser les preuves extraites et générer un rapport final complet.
Comment fonctionne la canalisation multimodale RAG ?
Collecte d'indices → Images, sons, textes et cartes de profondeur provenant de la scène de crime de Gotham.
Génération d'embeddings → Chaque fichier est converti en vecteur à l'aide du modèle multimodal ImageBind.
Indexation dans Elasticsearch → Les vecteurs sont stockés pour une récupération efficace.
Recherche par similarité → Étant donné un nouvel indice, les vecteurs les plus similaires sont récupérés.
Le LLM analyse les preuves → Un modèle GPT-4 synthétise la réponse et identifie le suspect !
Technologies utilisées
ImageBind → Génère des encastrements unifiés pour différentes modalités.
Elasticsearch → Permet une recherche vectorielle rapide et efficace.
LLM (GPT-4, OpenAI) → Analyse les preuves et produit un rapport final.
À qui s'adresse ce blog ?
Utilisateurs d'Elastic intéressés par la recherche vectorielle multimodale.
Les développeurs qui cherchent à comprendre le RAG multimodal dans la pratique.
Toute personne à la recherche de solutions évolutives pour l'analyse de données provenant de sources multiples.
Conditions préalables à la mise en place d'un RAG multimodal : mise en place de l'environnement
Pour résoudre les crimes de Gotham City, vous devez mettre en place votre environnement technologique. Suivez ce guide étape par étape :
1. Exigences techniques
Composant | Spécifications |
|---|---|
Système OS | Linux, macOS ou Windows |
Python | 3.10 ou plus récent |
RAM | 8 Go minimum (16 Go recommandés) |
GPU | Facultatif mais recommandé pour ImageBind |
2. Mise en place du projet
Tout le matériel d'enquête est disponible sur GitHub, et nous utiliserons Jupyter Notebook (Google Colab) pour cette expérience interactive de résolution de crimes. Suivez les étapes suivantes pour commencer :
Configuration avec Jupyter Notebook (Google Colab)
1. Accéder à l'ordinateur portable
Ouvrez notre carnet Google Colab prêt à l'emploi : RAG multimodal avec Elasticsearch.
Ce carnet contient tout le code et les explications dont vous avez besoin pour suivre le cours.
2. Cloner le référentiel
# Clone the repository with the multimodal RAG code
!git clone -b https://github.com/elastic/elasticsearch-labs.git
# Navigate to the project directory
cd elasticsearch-labs/supporting-blog-content/building-multimodal-rag-with-elasticsearch-gotham3. Installer les dépendances
# Install PyTorch and related libraries
!pip install torch>=2.1.0 torchvision>=0.16.0 torchaudio>=2.1.0
# Install vision processing libraries
!pip install opencv-python-headless pillow numpy
# Install the specific ImageBind fork
!pip install git+https://github.com/hkchengrex/ImageBind.git
# Install Elasticsearch and environment management
!pip install elasticsearch python-dotenv4. Configurer les informations d'identification
# Input your credentials securely
import getpass
ELASTICSEARCH_URL = input("Enter the Elasticsearch endpoint url: ")
ELASTICSEARCH_API_KEY = getpass.getpass("Enter the Elasticsearch API key: ")
OPENAI_API_KEY = getpass.getpass("Enter the OpenAI API key: ")
# Configure environment variables
import os
os.environ["ELASTICSEARCH_API_KEY"] = ELASTICSEARCH_API_KEY
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
os.environ["ELASTICSEARCH_URL"] = ELASTICSEARCH_URLNote : Le modèle ImageBind (~2GB) sera téléchargé automatiquement lors de la première exécution.
Maintenant que tout est prêt, plongeons dans les détails et résolvons le crime !
Introduction : Le crime à Gotham City
Par une nuit pluvieuse à Gotham City, un crime choquant secoue la ville. Le commissaire Gordon a besoin de votre aide pour élucider ce mystère. Les indices sont dispersés dans différents formats : images floues, sons mystérieux, textes cryptés et même cartes de profondeur. Êtes-vous prêt à utiliser la technologie d'IA la plus avancée pour résoudre l'affaire ?
Dans ce blog, vous serez guidé pas à pas dans la construction d'un système RAG (Retrieval-Augmented Generation) multimodal qui unifie différents types de données(images, audio, textes et cartes de profondeur) dans un espace de recherche unique. Nous utiliserons ImageBind pour générer des encastrements multimodaux, Elasticsearch pour stocker et récupérer ces encastrements, et un Grand Modèle de Langage (LLM) pour analyser les preuves et générer un rapport final.
Principes fondamentaux : Architecture multimodale RAG
Qu'est-ce qu'un RAG multimodal ?
L'essor du multimodal RAG (Retrieval-Augmented Generation) révolutionne la façon dont nous interagissons avec les modèles d'IA. Traditionnellement, les systèmes RAG travaillent exclusivement avec du texte, récupérant les informations pertinentes dans des bases de données avant de générer des réponses. Toutefois, le monde ne se limite pas au texte : les images, les vidéos et les sons sont également porteurs de connaissances précieuses. C'est pourquoi les architectures multimodales gagnent en importance, permettant aux systèmes d'IA de combiner des informations provenant de différents formats pour obtenir des réponses plus riches et plus précises.
Trois approches principales pour le RAG multimodal
Trois stratégies sont couramment utilisées pour mettre en œuvre un RAG multimodal. Chaque approche présente ses propres avantages et limites, en fonction du cas d'utilisation :
1. Espace vectoriel partagé
Les données provenant de différentes modalités sont mises en correspondance dans un espace vectoriel commun à l'aide de modèles multimodaux tels qu'ImageBind. Cela permet aux requêtes textuelles de récupérer des images, des vidéos et des fichiers audio sans conversion explicite de format.
Avantages :
Permet la recherche multimodale sans nécessiter de conversion de format explicite.
Il assure une intégration fluide entre les différentes modalités, permettant une recherche directe dans le texte, l'image, l'audio et la vidéo.
Évolutif pour divers types de données, ce qui le rend utile pour les applications de recherche à grande échelle.
Inconvénients :
La formation nécessite de grands ensembles de données multimodales, qui ne sont pas toujours disponibles.
L'espace d'intégration partagé peut introduire une dérive sémantique, lorsque les relations entre les modalités ne sont pas parfaitement préservées.
Les biais dans les modèles multimodaux peuvent avoir un impact sur la précision de la recherche, en fonction de la distribution de l'ensemble des données.
2. Une seule modalité ancrée
Toutes les modalités sont converties dans un format unique, généralement du texte, avant d'être extraites. Par exemple, les images sont décrites par des légendes générées automatiquement, et le son est transcrit en texte.
Avantages :
Simplifie la recherche, car tout est converti en une représentation textuelle uniforme.
Fonctionne bien avec les moteurs de recherche textuels existants, ce qui élimine la nécessité d'une infrastructure multimodale spécialisée.
Peut améliorer l'interprétabilité puisque les résultats obtenus sont dans un format lisible par l'homme.
Inconvénients :
Perte d'informations: Certains détails (par exemple, les relations spatiales dans les images, la tonalité dans le son) peuvent ne pas être entièrement pris en compte dans les descriptions textuelles.
Dépend de la qualité du sous-titrage/de la transcription: Les erreurs dans les annotations automatiques peuvent réduire l'efficacité de la recherche.
Elle n'est pas optimale pour les requêtes purement visuelles ou auditives, car le processus de conversion risque de supprimer le contexte essentiel.
3. Récupération séparée
Maintient des modèles distincts pour chaque modalité. Le système effectue des recherches distinctes pour chaque type de données et fusionne ensuite les résultats.
Avantages :
Permet une optimisation personnalisée par modalité, améliorant la précision de la recherche pour chaque type de données.
Moins de dépendance à l'égard de modèles multimodaux complexes, ce qui facilite l'intégration des systèmes de recherche existants.
Permet de contrôler finement le classement et le reclassement, les résultats provenant de différentes modalités pouvant être combinés de manière dynamique.
Inconvénients :
Nécessite la fusion des résultats, ce qui rend le processus de recherche et de classement plus complexe.
Peut générer des réponses incohérentes si différentes modalités renvoient des informations contradictoires.
Coût de calcul plus élevé car des recherches indépendantes sont effectuées pour chaque modalité, ce qui augmente le temps de traitement.
Notre choix : Espace vectoriel partagé avec ImageBind
Parmi ces approches, nous avons choisi l'espace vectoriel partagé, une stratégie qui s'aligne parfaitement avec le besoin de recherches multimodales efficaces. Notre implémentation est basée sur ImageBind, un modèle capable de représenter plusieurs modalités(texte, image, audio et vidéo) dans un espace vectoriel commun. Cela nous permet de
Effectuer des recherches multimodales entre différents formats de médias sans avoir à tout convertir en texte.
Utiliser des encastrements très expressifs pour capturer les relations entre les différentes modalités.
Garantir l'évolutivité et l'efficacité, en stockant des encastrements optimisés pour une recherche rapide dans Elasticsearch.
En adoptant cette approche, nous avons construit un pipeline de recherche multimodale robuste, dans lequel une requête textuelle peut directement récupérer des images ou du son sans prétraitement supplémentaire. Cette méthode permet d'élargir les applications pratiques, de la recherche intelligente dans les grands référentiels aux systèmes de recommandation multimodaux avancés.
La figure suivante illustre le flux de données dans le pipeline RAG multimodal, en mettant en évidence le processus d'indexation, d'extraction et de génération de réponses sur la base de données multimodales :

Comment fonctionne l'espace d'intégration ?
Traditionnellement, les enchâssements de texte proviennent de modèles de langage (par exemple, BERT, GPT). Aujourd'hui, avec des modèles multimodaux natifs comme ImageBind de Meta AI, nous disposons d'une colonne vertébrale qui génère des vecteurs pour de multiples modalités :
Le texte: Les phrases et les paragraphes sont transformés en vecteurs de même dimension.
Images (vision): Les pixels sont placés dans le même espace dimensionnel que celui utilisé pour le texte.
Audio: Les signaux sonores sont convertis en enregistrements comparables à des images et à du texte.
Cartes de profondeur: Les données relatives à la profondeur sont traitées et donnent lieu à des vecteurs.
Ainsi, tout indice(texte, image, son, profondeur) peut être comparé à un autre en utilisant des mesures de similarité vectorielle comme la similarité cosinusoïdale. Si un échantillon audio de rires et une image du visage d'un suspect sont "proches" dans cet espace, nous pouvons en déduire une certaine corrélation (par exemple, la même identité).
Étape 1 - Collecte d'indices sur la scène de crime
Avant d'analyser les preuves, il faut les collecter. Le crime commis à Gotham a laissé des traces qui peuvent être cachées dans des images, des sons, des textes et même des données de profondeur. Organisons ces indices pour qu'ils alimentent notre système.
Qu'avons-nous ?
Le commissaire Gordon nous a envoyé les fichiers suivants, qui contiennent des preuves recueillies sur la scène de crime selon quatre modalités différentes :
Description de la piste et modalité
a) Images (2 photos)
crime_scene1.jpg, crime_scene2.jpg→ Photos prises sur la scène de crime. Montre des traces suspectes sur le sol.suspect_spotted.jpg→ Image d'une caméra de sécurité montrant une silhouette s'enfuyant de la scène.



b) Audio (1 enregistrement)
joker_laugh.wav→ Un microphone situé à proximité de la scène de crime a capté un rire sinistre.

c) Texte (1 message)
Riddle.txt, note2.txt→ Des notes mystérieuses ont été trouvées sur place, peut-être laissées par le criminel.

d) Profondeur (1 carte de profondeur)
depth_suspect.png→ Une caméra de sécurité équipée d'un capteur de profondeur a filmé un suspect dans une ruelle voisine.jdancing-depth.png→ Une caméra de sécurité équipée d'un capteur de profondeur a filmé un suspect descendant la station de métro.


Ces éléments de preuve se présentent sous des formes différentes et ne peuvent être analysés directement de la même manière. Nous devons les transformer en encastrements, c'est-à-dire en vecteurs numériques qui permettront d'effectuer des comparaisons intermodales.
Organisation des dossiers
Avant de commencer le traitement, nous devons nous assurer que tous les indices sont correctement organisés dans le répertoire data/ afin que le pipeline fonctionne correctement.
Structure de répertoire attendue :
data/
├── images/
│ ├── crime_scene1.jpg
│ ├── suspect_spotted.jpg
│ ...
├── audios/
│ ├── joker_laugh.wav
│ ...
├── texts/
│ ├── riddle.txt
│ ...
├── depths/
│ ├── depth_suspect.pngCode pour vérifier l'organisation de l'indice
Avant de poursuivre, vérifions que tous les fichiers nécessaires se trouvent au bon endroit.
import os
# Base directory for clues
data_dir = "data"
# List of expected files
evidences = {
"images": ["crime_scene1.jpg","crime_scene1.jpg", "joker_alley.jpg"],
"audios": ["joker_laugh.wav"],
"texts": ["riddle.txt", "note2.txt”],
"depths": ["depth_suspect.png", "jdancing-depth.png"]
}
# Create directories if they don't exist
for category, files in evidences.items():
category_path = os.path.join(data_dir, category)
os.makedirs(category_path, exist_ok=True)
for file in files:
file_path = os.path.join(category_path, file)
if not os.path.exists(file_path):
print(f"Warning: {file} not found in {category_path}.")
print("All files are correctly organized!")Exécution du fichier
python stages/01-stage/files_check.pyRésultat attendu (si tous les fichiers sont corrects) :
All files are correctly organized!Résultat attendu (si un fichier est manquant) :
Warning: joker_laugh.wav not found in data/audios/
Warning: depth_suspect.png not found in data/depths/Ce script permet d'éviter les erreurs avant de commencer à générer des embeddings et à les indexer dans Elasticsearch.
Étape 2 - Organiser les preuves
Générer des embeddings avec ImageBind
Pour unifier les indices, nous devons les transformer en représentations vectorielles (embeddings) qui capturent la signification de chaque modalité. Nous utiliserons ImageBind, un modèle de Meta AI qui génère des embeddings pour différents types de données(images, audio, texte et cartes de profondeur) dans un espace vectoriel partagé.

Comment fonctionne ImageBind ?
Pour comparer différents types de preuves(images, audio, texte et cartes de profondeur), nous devons les transformer en vecteurs numériques à l'aide d'ImageBind. Ce modèle permet de convertir n'importe quel type d'entrée dans le même format d'intégration, ce qui permet d'effectuer des recherches intermodales entre les différentes modalités.
Vous trouverez ci-dessous un code optimisé (src/embedding_generator.py) permettant de générer des embeddings pour tout type d'entrée en utilisant les processeurs appropriés pour chaque modalité :
class EmbeddingGenerator:
"""Class for generating multimodal embeddings using ImageBind."""
def __init__(self):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model = self._load_model()
def _load_model(self):
"""Loads the ImageBind model and sets it to inference mode."""
model = imagebind_model.imagebind_huge(pretrained=True)
model.eval()
model.to(self.device)
return model
def generate_embedding(self, input_data, modality):
"""Generates embedding for different modalities"""
processors = {
"vision": lambda x: data.load_and_transform_vision_data(x, self.device),
"audio": lambda x: data.load_and_transform_audio_data(x, self.device),
"text": lambda x: data.load_and_transform_text(x, self.device),
"depth": self.process_depth
}
try:
# Input type verification
if not isinstance(input_data, list):
raise ValueError(f"Input data must be a list. Received: {type(input_data)}")
# Convert input data to a tensor format that the model can process
# For images: [batch_size, channels, height, width]
# For audio: [batch_size, channels, time]
# For text: [batch_size, sequence_length]
inputs = {modality: processors[modality](input_data)}
with torch.no_grad():
embedding = self.model(inputs)[modality]
return embedding.squeeze(0).cpu().numpy()
except Exception as e:
logger.error(f"Error generating {modality} embedding: {str(e)}", exc_info=True)
raiseUn tenseur est une structure de données fondamentale dans l'apprentissage automatique et l'apprentissage profond, en particulier lorsque l'on travaille avec des modèles comme ImageBind. Dans notre contexte :
input_tensor = processors[modality]([input_data], self.device)Le tenseur représente les données d'entrée (image, son ou texte) converties dans un format mathématique que le modèle peut traiter. En particulier :
Pour les images: Le tenseur représente l'image sous la forme d'une matrice multidimensionnelle de valeurs numériques (pixels organisés par hauteur, largeur et canaux de couleur).
Pour l'audio: Le tenseur représente les ondes sonores comme une séquence d'amplitudes dans le temps.
Pour le texte: Le tenseur représente les mots ou les tokens sous forme de vecteurs numériques.
Test de la génération d'encastrement :
Testons notre génération d'intégration avec le code suivant. Sauvegardez-le dans 02-stage/test_embedding_generation.py et exécutez-le avec cette commande :
python stages/02-stage/test_embedding_generation.pygenerator = EmbeddingGenerator()
image_embedding = generator.generate_embedding("data/images/crime_scene1.jpg","vision")
print(image_embedding.shape)Résultat attendu :
(1024,)L'image a été transformée en un vecteur à 1024 dimensions.
Étape 3 - Stockage et recherche dans Elasticsearch
À présent que nous avons généré les encastrements pour les éléments de preuve, nous devons les stocker dans une base de données vectorielle afin de permettre des recherches efficaces. Pour ce faire, nous utiliserons Elasticsearch, qui prend en charge les vecteurs denses (dense_vector) et permet d'effectuer des recherches par similarité.
Cette étape consiste en deux processus principaux :
Indexation des encastrements → Stockage des vecteurs générés dans Elasticsearch.
Recherche de similarité → Récupère les documents les plus similaires à un nouvel élément de preuve.
Indexer les preuves dans Elasticsearch
Chaque élément de preuve traité par ImageBind (image, son, texte ou profondeur) est converti en un vecteur à 1024 dimensions. Nous devons stocker ces vecteurs dans Elasticsearch pour permettre des recherches ultérieures.
Le code suivant (src/elastic_manager.py) crée un index dans Elasticsearch et configure le mapping pour stocker les embeddings.
from elasticsearch import Elasticsearch, helpers
...
class ElasticsearchManager:
"""Manages multimodal operations in Elasticsearch"""
def __init__(self):
load_dotenv() # Load variables from .env
self.es = self._connect_elastic()
self.index_name = "multimodal_content"
self._setup_index()
def _connect_elastic(self):
"""Connects to Elasticsearch"""
return Elasticsearch(
os.getenv("ELASTICSEARCH_URL"), # Elasticsearch endpoint
api_key=os.getenv("ELASTICSEARCH_API_KEY")
)
def _setup_index(self):
"""Sets up the index if it doesn't exist"""
if not self.es.indices.exists(index=self.index_name):
mapping = {
"mappings": {
"properties": {
"embedding": {
"type": "dense_vector",
"dims": 1024,
"index": True,
"similarity": "cosine"
},
"modality": {"type": "keyword"},
"content": {"type": "binary"},
"description": {"type": "text"},
"metadata": {"type": "object"},
"content_path": {"type": "text"}
}
}
}
self.es.indices.create(index=self.index_name, body=mapping)
def index_content(self, embedding, modality, content=None, description="", metadata=None, content_path=None):
"""Indexes multimodal content"""
doc = {
"embedding": embedding.tolist(),
"modality": modality,
"description": description,
"metadata": metadata or {},
"content_path": content_path
}
if content:
doc["content"] = base64.b64encode(content).decode() if isinstance(content, bytes) else content
return self.es.index(index=self.index_name, document=doc)
def search_similar(self, query_embedding, modality=None, k=5):
"""Searches for similar contents"""
query = {
"knn": {
"field": "embedding",
"query_vector": query_embedding.tolist(),
"k": k,
"num_candidates": 100,
"filter": [{"term": {"modality": modality}}] if modality else []
}
}
try:
response = self.es.search(
index=self.index_name,
query=query,
size=k
)
# Return both source data and score for each hit
return [{
**hit["_source"],
"score": hit["_score"]
} for hit in response["hits"]["hits"]]
except Exception as e:
print(f"Error: processing search_evidence: {str(e)}")
return "Error generating search evidence"Exécution de l'indexation
Maintenant, indexons un élément de preuve pour tester le processus.
# Example: Indexing an image from the crime scene
generator = EmbeddingGenerator()
es_manager = ElasticsearchManager(cloud_id="YOUR_CLOUD_ID", api_key="YOUR_API_KEY")
image_embedding = generator.generate_embedding("data/images/crime_scene1.jpg", "vision")
response = es_manager.index_content(
embedding=image_embedding,
modality="vision",
description="Photo of the crime scene with suspicious traces",
content_path="data/images/crime_scene1.jpg"
)
print(json.dumps(response, indent=2))Résultat attendu dans Elasticsearch (résumé du document indexé) :
{
"embedding": [0.12, -0.53, 0.89, ...],
"modality": "vision",
"description": "Photo of the crime scene with suspicious traces",
"content_path": "data/images/crime_scene1.jpg"
}Pour indexer toutes les preuves multimodales, veuillez exécuter la commande Python suivante :
python stages/03-stage/index_all_modalities.pyLes preuves sont désormais stockées dans Elasticsearch et prêtes à être récupérées en cas de besoin.
Vérification du processus d'indexation
Après avoir exécuté le script d'indexation, vérifions si toutes nos preuves ont été correctement stockées dans Elasticsearch. Vous pouvez utiliser les outils de développement de Kibana pour exécuter des requêtes de vérification :
1. Vérifiez d'abord si l'index a été créé :
GET _cat/indices/multimodal_content?v2. Ensuite, vérifiez le nombre de documents par modalité :
GET multimodal_content/_search
{
"size": 0,
"aggs": {
"modalities": {
"terms": {
"field": "modality.keyword"
}
}
}
}3. Enfin, examiner la structure du document indexé :
GET multimodal_content/_search
{
"size": 1,
"query": {
"match_all": {}
}
}Résultats attendus :
Un index nommé `multimodal_content` devrait exister.
Environ 7 documents répartis entre différentes modalités (vision, audio, texte, profondeur).
Chaque document doit contenir les champs suivants : embedding, modality, description, metadata et content_path.
Cette étape de vérification permet de s'assurer que notre base de données d'éléments de preuve est correctement constituée avant de procéder aux recherches de similitudes.
Recherche de preuves similaires dans Elasticsearch
Maintenant que les preuves ont été indexées, nous pouvons effectuer des recherches pour trouver les enregistrements les plus similaires à un nouvel indice. Cette recherche utilise la similarité vectorielle pour renvoyer les enregistrements les plus proches dans l'espace d'intégration.
Le code suivant effectue cette recherche.
def search_similar_evidence(self, query_embedding, k=5, modality=None):
"""Performs a kNN search to find the most similar clues."""
knn_query = {
"field": "embedding",
"query_vector": query_embedding.tolist(),
"k": k,
"num_candidates": 100
}
query_body = {"knn": knn_query}
if modality:
query_body = {
"bool": {
"must": [
query_body,
{"term": {"modality": modality}}
]
}
}
try:
results = self.es.search(
index=self.index_name,
query=query_body,
_source_includes=["description", "modality", "content_path"],
size=k
)
except Exception as e:
print(f"Error processing search_evidence: {str(e)}")
return "Error generating search evidence”
return results["hits"]["hits"]Tester la recherche - Utiliser l'audio comme requête pour des résultats multimodaux
Testons maintenant la recherche de preuves à l'aide d'un fichier audio suspect. Nous devons générer de la même manière un encapsulage pour le fichier et rechercher des encapsulages similaires :
python stages/03-stage/search_by_audio.py# Initialize classes
generator = EmbeddingGenerator()
es_manager = ElasticsearchManager(cloud_id="YOUR_CLOUD_ID", api_key="YOUR_API_KEY")
# Generate embedding for a suspicious audio
audio_embedding = generator.generate_embedding("data/audios/mysterious_laugh.wav", "audio")
# Search for similar evidence in Elasticsearch
similar_evidences = es_manager.search_similar_evidence(audio_embedding, k=3)
# Display the retrieved results
print("\n🔎 Similar evidence found:\n")
for i, evidence in enumerate(similar_evidences, start=1):
description = evidence['_source']['description']
modality = evidence['_source']['modality']
score = evidence['_score']
content_path = evidence['_source'].get('content_path', 'N/A')
print(f"{i}. {description} ({modality})")
print(f" Similarity: {score:.4f}")
print(f" File path: {content_path}\n")Résultats attendus dans le terminal :
🔎 Similar evidence found:
1. A sinister laugh captured near the crime scene (audio)
Similarity: 0.9985
File path: data/audios/joker_laugh.wav
2. The Joker with green hair, white face paint, and a sinister smile in an urban night setting. (vision)
Similarity: 0.6068
File path: data/images/joker_laughing.png
3. Suspect dancing (vision)
Similarity: 0.5591
File path: data/images/jdancing.png
Nous pouvons maintenant analyser les éléments de preuve retrouvés et déterminer leur pertinence dans le cadre de l'affaire.
Au-delà de l'audio - Explorer les recherches multimodales
Inverser les rôles : Toute modalité peut être une question ""
Dans notre système RAG multimodal, chaque modalité est une requête de recherche potentielle. Dépassons l'exemple de l'audio et explorons comment d'autres types de données peuvent déclencher des enquêtes.
1. Recherche par texte (déchiffrer la note du criminel)
Scénario : Vous avez trouvé un message texte crypté et vous souhaitez trouver des preuves connexes.
python stages/03-stage/search_by_text.py# Generate embedding from text
text = "Why so serious?"
embedding_text = generator.generate_embedding([text], "text")
# Search for related evidence
similar_evidences = es_manager.search_similar(
query_embedding=embedding_text,
k=3
)Résultats attendus :
🔎 Similar evidence found:
1. Mysterious note found at the location (text)
Similarity: 0.7639
File path: data/texts/riddle.txt
2. The Joker with green hair, white face paint, and a sinister smile in an urban night setting. (vision)
Similarity: 0.7161
File path: data/images/joker_laughing.png
3. Why so serious (text)
Similarity: 0.7132
File path: data/texts/note2.txt2. Recherche d'images (suivi de la scène de crime suspecte)
Scénario : Une nouvelle scène de crime (crime_scene2.jpg) doit être comparée à d'autres éléments de preuve.
python stages/03-stage/search_by_image.py# Generate embedding for a suspicious image
vision_embedding = generator.generate_embedding(["data/images/crime_scene2.jpg"], "vision")
# Search for similar evidence in Elasticsearch
similar_evidences = es_manager.search_similar(
query_embedding=vision_embedding,
k=3
)Sortie :
🔎 Similar evidence found:
1. Photo of the crime scene: A dark, rain-soaked alley is filled with playing cards, while a sinister graffiti of the Joker laughing stands out on the brick wall. (vision)
Similarity: 0.8258
File path: data/images/crime_scene1.jpg
2. The Joker with green hair, white face paint, and a sinister smile in an urban night setting. (vision)
Similarity: 0.6897
File path: data/images/joker_laughing.png
3. Suspect dancing (vision)
Similarity: 0.6588
File path: data/images/jdancing.png3. Recherche d'une carte de profondeur (poursuite 3D)
Scénario : Une carte de profondeur jdancing-depth.png() révèle les motifs d'évasion de l' image.
python stages/03-stage/search_by_depth.py# Generate embedding for a suspicious depth map
vision_embedding = generator.generate_embedding(["data/depths/jdancing-depth.png"], "depth")
# Search for similar evidence in Elasticsearch
similar_evidences = es_manager.search_similar(
query_embedding=vision_embedding,
modality="vision",
k=3
)Sortie
🔎 Similar evidence found:
1. The Joker with green hair, white face paint, and a sinister smile in an urban night setting. (vision)
Similarity: 0.5329
File path: data/images/joker_laughing.png
2. Photo of the crime scene: A dark, rain-soaked alley is filled with playing cards, while a sinister graffiti of the Joker laughing stands out on the brick wall. (vision)
Similarity: 0.5053
File path: data/images/crime_scene1.jpg
3. Suspect dancing (vision)
Similarity: 0.4859
File path: data/images/jdancing.png
Pourquoi cela est-il important ?
Chaque modalité révèle des connexions uniques:
Texte → Modèles linguistiques du suspect.
Images → Reconnaissance de lieux et d'objets.
Profondeur → reconstruction dela scène en 3D.
Nous disposons désormais d'une base de données de preuves structurée dans Elasticsearch, qui nous permet de stocker et d'extraire efficacement des preuves multimodales.
Résumé de ce que nous avons fait :
Stockage d'encastrements multimodaux dans Elasticsearch.
Effectuer des recherches de similitudes, trouver des preuves liées à de nouveaux indices.
Test de la recherche à l'aide d'un fichier audio suspect, afin de s'assurer que le système fonctionne correctement.
Prochaine étape : Nous utiliserons un LLM (Large Language Model) pour analyser les données extraites et produire un rapport final.
Étape 4 - Faire le lien avec le LLM
Maintenant que les preuves ont été indexées dans Elasticsearch et peuvent être retrouvées par similarité, nous avons besoin d'un LLM (Large Language Model) pour les analyser et générer un rapport final à envoyer au commissaire Gordon. Le LLM sera chargé d'identifier des modèles, de relier des indices et de suggérer un suspect possible sur la base des éléments de preuve récupérés.
Pour cette tâche, nous utiliserons le GPT-4 Turbo, en formulant une demande détaillée afin que le modèle puisse interpréter les résultats de manière efficace.
Intégration du LLM
Pour intégrer le LLM dans notre système, nous avons créé la classe LLMAnalyzer (src/llm_analyzer.py), qui reçoit les preuves extraites d'Elasticsearch et génère un rapport médico-légal en utilisant ces preuves comme contexte d'invite.
import os
from openai import OpenAI
import logging
from dotenv import load_dotenv
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class LLMAnalyzer:
"""Evidence analyzer using GPT-4"""
def __init__(self):
load_dotenv()
self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def analyze_evidence(self, evidence_results):
"""
Analyzes multimodal search results and generates a report
Args:
evidence_results: Dict with results by modality
{
'vision': [...],
'audio': [...],
'text': [...],
'depth': [...]
}
"""
# Format evidence for the prompt
evidence_summary = self._format_evidence(evidence_results)
# final prompt
prompt = f"""
You are a highly experienced forensic detective specializing in multimodal evidence analysis. Your task is to analyze the collected evidence (audio, images, text, depth maps) and conclusively determine the **prime suspect** responsible for the Gotham Central Bank case.
---
### **Collected Evidence:**
{evidence_summary}
### **Task:**
1. **Analyze all the evidence** and identify cross-modal connections.
2. **Determine the exact identity of the criminal** based on behavioral patterns, visual/auditory/textual clues, and symbolic markers.
3. **Justify your conclusion** by explaining why this suspect is definitively responsible.
4. **Assign a confidence score (0-100%)** to your conclusion.
---
### **Final Output Format (Strictly Follow This Format):**
- **Prime Suspect:** [Full Name or Alias]
- **Evidence Supporting Conclusion:** [Detailed breakdown of visual, auditory, textual, and behavioral evidence]
- **Behavioral Patterns:** [Key actions, motives, and criminal signature]
- **Confidence Level:** [0-100%]
- **Next Steps (if any):** [What additional evidence would further confirm the identity? If none, state "No further evidence required."]
If there is **insufficient evidence**, specify exactly what is missing and suggest what additional data would be needed for a conclusive identification.
This report must be **direct and definitive**--avoid speculation and provide a final, actionable determination of the suspect's identity.
"""
try:
response = self.client.chat.completions.create(
model="gpt-4-turbo-preview",
messages=[
{
"role": "system",
"content": "You are a forensic detective specialized in multimodal evidence analysis."
},
{"role": "user", "content": prompt_01}
],
temperature=0.5,
max_tokens=1000
)
report = response.choices[0].message.content
logger.info("\n📋 Forensic Report Generated:")
logger.info("=" * 50)
logger.info(report)
logger.info("=" * 50)
return report
except Exception as e:
logger.error(f"Error generating report: {str(e)}")
return NoneRéglage de la température dans l'analyse LLM :
Pour notre système d'analyse criminelle, nous utilisons une température modérée de 0,5. Ce cadre équilibré a été choisi pour les raisons suivantes :
Il s'agit d'une solution intermédiaire entre les résultats déterministes (trop rigides) et les résultats hautement aléatoires ;
À 0,5, le modèle conserve une structure suffisante pour fournir des conclusions médico-légales logiques et justifiables ;
Ce paramètre permet au modèle d'identifier des modèles et d'établir des connexions tout en restant dans des paramètres raisonnables d'analyse médico-légale ;
Il permet de concilier le besoin de résultats cohérents et fiables avec la capacité de générer des analyses perspicaces.
Cette température modérée permet de garantir que notre analyse médico-légale est à la fois fiable et perspicace, en évitant les conclusions trop rigides et trop spéculatives.
Effectuer l'analyse des données probantes
Maintenant que nous avons l'intégration LLM, nous avons besoin d'un script qui relie tous les composants du système. Ce script va :
Rechercher des preuves similaires dans Elasticsearch.
Analyser les preuves récupérées à l'aide du LLM pour produire un rapport final.
Code : Script d'analyse des preuves
python stages/04-stage/rag_crime_analyze.pyimport sys
import os
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))
from embedding_generator import EmbeddingGenerator
from elastic_manager import ElasticsearchManager
from llm_analyzer import LLMAnalyzer
import json
import logging
from dotenv import load_dotenv
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Load environment variables
load_dotenv()
# Initialize classes
generator = EmbeddingGenerator()
es_manager = ElasticsearchManager()
llm = LLMAnalyzer()
logger.info("✅ All components initialized successfully")
try:
evidence_data = {}
# Get data for each modality
test_files = {
'vision': 'data/images/crime_scene2.jpg',
'audio': 'data/audios/joker_laugh.wav',
'text': 'Why so serious?',
'depth': 'data/depths/jdancing-depth.png'
}
logger.info("🔍 Collecting evidence...")
for modality, test_input in test_files.items():
try:
if modality == 'text':
embedding = generator.generate_embedding([test_input], modality)
else:
embedding = generator.generate_embedding([str(test_input)], modality)
results = es_manager.search_similar(embedding, k=2)
if results:
evidence_data[modality] = results
logger.info(f"✅ Data retrieved for {modality}: {len(results)} results")
else:
logger.warning(f"⚠️ No results found for {modality}")
except Exception as e:
logger.error(f"❌ Error retrieving {modality} data: {str(e)}")
if not evidence_data:
raise ValueError("No evidence data found in Elasticsearch!")
# Test forensic report generation
logger.info("\n📝 Generating forensic report...")
report = llm.analyze_evidence(evidence_data)
if report:
logger.info("✅ Forensic report generated successfully")
logger.info("\n📊 Report Preview:")
logger.info("+" * 50)
logger.info(report)
logger.info("+" * 50)
else:
raise ValueError("Failed to generate forensic report")
except Exception as e:
logger.error(f"❌ Error in analysis : {str(e)}")Résultats attendus du LLM
**Prime Suspect:** The Joker
**Evidence Supporting Conclusion:**
- **Visual Evidence:**
- The photo of the crime scene with playing cards scattered around and the graffiti of the Joker laughing matches the Joker's known calling cards and thematic elements. The similarity score of 0.83 indicates a high likelihood that these elements are directly associated with the Joker.
- The image of the Joker with green hair, white face paint, and a sinister smile in an urban night setting, although with a lower similarity score of 0.69, still supports the presence or recent activity of the Joker in areas consistent with the crime scene's characteristics.
- **Auditory Evidence:**
- The captured sinister laugh with a similarity score of 1.00 perfectly matches known audio profiles of the Joker, making it a direct auditory signature of his presence at or near the crime scene.
- Despite the lower similarity score of 0.61, the second audio piece further corroborates the Joker's involvement through thematic consistency.
- **Textual Evidence:**
- The mysterious note found at the location, with a similarity score of 0.76, likely contains thematic or direct references to the Joker's modus operandi or signature phrases, further implicating him in the crime.
- The similarity score of 0.72 for the Joker's description in textual evidence reinforces the thematic connection to the crime scene.
- **Depth Evidence:**
- Depth sensor capture of the suspect with a similarity score of 0.77 suggests a physical presence matching the Joker's known dimensions or characteristic movements.
- The lower similarity score of 0.53 in the second depth evidence still contributes to the overall pattern of evidence pointing towards the Joker, albeit with less certainty.
**Behavioral Patterns:**
- The Joker is known for his theatrical crimes, often leaving behind a signature trail of chaos, including playing cards, sinister laughter, and thematic graffiti. These elements are not only consistent with his known criminal signature but also directly observed at the crime scene.
- His motives often include creating chaos, drawing attention to his acts, and challenging his arch-nemesis, Batman, making a high-profile bank heist fitting within his behavioral patterns.
**Confidence Level:** 95%
**Next Steps:** No further evidence required.
The combination of visual, auditory, textual, and depth evidence strongly points to the Joker as the prime suspect. The thematic consistency across multiple modes of evidence, combined with known behavioral patterns and criminal signature, leaves little doubt regarding his involvement. While there is always a small margin of uncertainty in forensic analysis, the evidence at hand provides a compelling case against the Joker with a high degree of confidence.
Conclusion : Affaire résolue
Après avoir recueilli et analysé tous les indices, le système RAG multimodal a identifié un suspect : Le Joker.
En combinant des images, du son, du texte et des cartes de profondeur dans un espace vectoriel partagé à l'aide d'ImageBind, le système a pu détecter des connexions qu'il aurait été impossible d'identifier manuellement. Elasticsearch a permis d'effectuer des recherches rapides et efficaces, tandis que le LLM a synthétisé les données dans un rapport clair et concluant.
Cependant, le véritable pouvoir de ce système va au-delà de Gotham City. L'architecture multimodale RAG ouvre la voie à de nombreuses applications dans le monde réel:
Surveillance urbaine : Identification des suspects à partir d'images, de sons et de données de capteurs.
L'analyse médico-légale : Corrélation d'éléments de preuve provenant de sources multiples pour résoudre des crimes complexes.
Recommandation multimédia : Création de systèmes de recommandation qui comprennent les contextes multimodaux (par exemple, suggestion de musique sur la base d'images ou de textes).
Tendances des médias sociaux : Détection des sujets en vogue dans différents formats de données.
Maintenant que vous avez appris à construire un système RAG multimodal, pourquoi ne pas le tester avec vos propres indices?
Partagez vos découvertes avec nous et aidez la communauté à progresser dans le domaine de l'IA multimodale!
Remerciements particuliers
Je tiens à remercier Adrian Cole pour sa précieuse contribution et son examen au cours du processus de définition de l'architecture de déploiement de ce code.
Références
Questions fréquentes
Qu'est-ce qu'un RAG multimodal ?
Le RAG multimodal est une méthode qui permet aux systèmes d'IA de combiner des informations provenant de différents formats (tels que des images, des vidéos et des sons) afin d'obtenir des réponses plus riches et plus précises.
Comment mettre en œuvre le RAG multimodal ?
Pour mettre en œuvre un RAG multimodal, trois stratégies sont couramment utilisées : espace vectoriel partagé, modalité unique et extraction séparée.




