🤖 Data Engineering for AI

Module : Data Engineering for AI

4. Enrichir les données


Durée estimée : 3-4 heures

Prérequis : Module 3 (Transformer) + Docker installé

📍 Où en es-tu dans le parcours ?

    DONNÉES BRUTES                                              DATASET PRÊT
    (chaos)                                                     (training-ready)
         │                                                            ▲
         │   Module 2       Module 3        Module 4       Module 5   │
         │  ──────────     ──────────      ──────────     ──────────  │
         │                                                            │
         ▼                                                            │
    ┌─────────┐      ┌─────────────┐      ┌──────────┐      ┌────────┴───────┐
    │         │      │             │      │          │      │                │
    │ STOCKER │─────▶│ TRANSFORMER │─────▶│ ENRICHIR │─────▶│ AUTOMATISER &  │
    │   ✅    │      │     ✅      │      │  ▲▲▲▲    │      │ VERSIONNER     │
    │         │      │             │      │          │      │                │
    └─────────┘      └─────────────┘      └──────────┘      └────────────────┘
                                           TU ES ICI

🎯 Question de ce module

Comment rendre les données "cherchables" et prêtes pour les équipes ML/Data Science ?

📦 Ce que tu vas livrer

À la fin de ce module, tu auras :

  • Des embeddings (vecteurs) pour chaque image
  • Une base vectorielle (Chroma) pour rechercher par similarité
  • Un système de déduplication sémantique
  • Un dataset prêt à être exploré par les Data Scientists

🛠️ Ce que tu vas apprendre

Compétence Pourquoi c'est important Outil
Embeddings Représenter les données en vecteurs CLIP
Vector DB Stocker et chercher par similarité Chroma
Déduplication sémantique Trouver des contenus similaires Cosine similarity

⚠️ Ce que ce module NE couvre PAS

Hors scope Pourquoi Qui s'en occupe
Clustering (K-Means) C'est du ML, pas du DE Data Scientist
Visualisation UMAP C'est de l'exploration Data Scientist
Fine-tuning CLIP C'est du ML ML Engineer

💡 Ton rôle de DE : Préparer les embeddings et les rendre disponibles. L'exploration et l'analyse, c'est le travail des Data Scientists.

4.0 C'est quoi un embedding ?

Le problème : les ordinateurs ne comprennent pas les images

┌─────────────────────────────────────────────────────────────────────────────┐
│                    LE PROBLÈME                                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Humain voit:              Ordinateur voit:                               │
│   ────────────              ────────────────                               │
│                                                                             │
│   🐱 "Un chat mignon"       [[255, 128, 64], [253, 127, 63], ...]          │
│                             (juste des pixels, pas de sens)                │
│                                                                             │
│   Question: "Trouve des images similaires à ce chat"                       │
│                                                                             │
│   ❌ Comparer pixel par pixel ?                                             │
│      → Même chat décalé de 1 pixel = "complètement différent"              │
│      → Chat noir vs chat blanc = "totalement différent"                    │
│      → Ça ne marche pas !                                                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

La solution : les embeddings

Un embedding est une représentation numérique qui capture le sens d'une donnée.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    LA SOLUTION : EMBEDDINGS                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   IMAGE                    MODÈLE                      EMBEDDING           │
│   (pixels)                 (CLIP)                      (vecteur)           │
│                                                                             │
│   🐱          ──────────▶  ┌─────────┐  ──────────▶   [0.12, -0.45, 0.78,  │
│   256×256×3                │  CLIP   │                 0.23, -0.11, ...]   │
│   = 196,608                │ encoder │                 = 512 nombres       │
│   nombres                  └─────────┘                                     │
│                                                                             │
│   Ce vecteur capture:                                                      │
│   • "C'est un animal"                                                      │
│   • "C'est un chat"                                                        │
│   • "Il est mignon"                                                        │
│   • "Il est assis"                                                         │
│   • ...                                                                    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

L'analogie : coordonnées GPS pour les concepts

Imagine que chaque image a des coordonnées dans un espace de concepts :

┌─────────────────────────────────────────────────────────────────────────────┐
│                    ESPACE DES EMBEDDINGS                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│                          "mignon"                                          │
│                              ▲                                             │
│                              │                                             │
│                    🐱 chat   │   🐕 chien                                  │
│                        ·     │     ·                                       │
│                         ·    │    ·                                        │
│    "animal" ◄────────────────┼────────────────► "objet"                   │
│                              │        ·                                    │
│                              │       🚗 voiture                            │
│                              │                                             │
│                              ▼                                             │
│                          "effrayant"                                       │
│                                                                             │
│   Les chats sont PROCHES des chiens (animaux)                              │
│   Les chats sont LOIN des voitures (pas animaux)                           │
│                                                                             │
│   Distance = Similarité sémantique !                                       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Propriété magique : la distance = la similarité

Images Distance Interprétation
Chat 1 vs Chat 2 0.1 (proche) Très similaires
Chat vs Chien 0.4 (moyen) Même catégorie (animaux)
Chat vs Voiture 0.9 (loin) Très différents

💡 C'est ça la magie : On peut maintenant chercher "images similaires" en cherchant les vecteurs proches !

Pourquoi le Data Engineer doit générer des embeddings ?

┌─────────────────────────────────────────────────────────────────────────────┐
│                    RÔLE DU DE : PRÉPARER LES EMBEDDINGS                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Le DE prépare                        Le DS/MLE utilise                   │
│   ─────────────                        ─────────────────                   │
│                                                                             │
│   • Génère les embeddings              • Explore le dataset                │
│   • Stocke dans une vector DB          • Fait du clustering                │
│   • Détecte les duplicatas             • Analyse les anomalies             │
│   • Crée des APIs de recherche         • Entraîne des modèles              │
│                                                                             │
│   ════════════════════════════════════════════════════════════════════     │
│                                                                             │
│   Cas d'usage concrets pour le DE:                                         │
│                                                                             │
│   1. DÉDUPLICATION SÉMANTIQUE                                              │
│      "Ces deux photos sont du même chat sous des angles différents"        │
│      → Éviter les fuites train/test                                        │
│                                                                             │
│   2. RECHERCHE DANS LE DATASET                                             │
│      "Montre-moi toutes les images de plages"                              │
│      → Sans avoir à labelliser manuellement                                │
│                                                                             │
│   3. QUALITY CHECK                                                         │
│      "Y a-t-il des images qui ne ressemblent à rien d'autre ?"             │
│      → Détecter les outliers/erreurs                                       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Pourquoi CLIP ?

CLIP (Contrastive Language-Image Pre-training) est un modèle d'OpenAI qui comprend AUSSI BIEN les images que le texte.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CLIP : IMAGES + TEXTE DANS LE MÊME ESPACE               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   IMAGE                         TEXTE                                      │
│   ─────                         ─────                                      │
│                                                                             │
│   🐱 ──▶ [0.12, -0.45, ...]     "a cute cat" ──▶ [0.11, -0.44, ...]        │
│                                                                             │
│   Les deux vecteurs sont PROCHES !                                         │
│                                                                             │
│   ════════════════════════════════════════════════════════════════════     │
│                                                                             │
│   Ce que ça permet:                                                        │
│                                                                             │
│   ✅ Chercher des images avec du texte                                      │
│      "sunset on beach" → trouve les images de couchers de soleil           │
│                                                                             │
│   ✅ Trouver des images similaires                                          │
│      Image de chat → autres images de chats                                │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
Modèle Dimensions Vitesse Usage
CLIP ViT-B/32 512 Rapide ✅ Recommandé pour commencer
CLIP ViT-L/14 768 Lent Plus précis, plus gourmand

Setup de l'environnement

# docker-compose.yml
version: '3.8'

services:
  minio:
    image: minio/minio:latest
    container_name: minio
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin123
    command: server /data --console-address ":9001"
    volumes:
      - minio_data:/data

  chroma:
    image: chromadb/chroma:latest
    container_name: chroma
    ports:
      - "8000:8000"
    volumes:
      - chroma_data:/chroma/chroma

volumes:
  minio_data:
  chroma_data:
# Lancer l'environnement
docker-compose up -d

# Installer les dépendances Python
pip install torch torchvision transformers chromadb pillow numpy boto3

4.1 Connexion au Module 3 : récupérer les images transformées

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CONTINUITÉ MODULE 3MODULE 4                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   MODULE 3                             MODULE 4                            │
│   ────────                             ────────                            │
│                                                                             │
│   ┌───────────────────┐                ┌───────────────────────┐           │
│   │ processed-images/ │                │  Lire les images      │           │
│   │ ├── transformed/  │ ──────────────▶│  Générer embeddings   │           │
│   │ └── catalog.parquet│               │  Stocker dans Chroma  │           │
│   └───────────────────┘                └───────────────────────┘           │
│                                                                             │
│   Images uniformes                     Images + Vecteurs                   │
│   256×256, RGB, JPEG                   prêts pour recherche                │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
pythonVoir le code
# ============================================
# Imports et configuration
# ============================================

import torch
import numpy as np
from PIL import Image
import io
import boto3
from botocore.client import Config
from typing import List, Optional
import warnings
warnings.filterwarnings('ignore')

# Vérifier GPU
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"🖥️ Device: {device}")
if device == "cpu":
    print("   💡 CPU sera plus lent, mais ça fonctionne !")

print("✅ Imports chargés")
pythonVoir le code
# ============================================
# Connexion à MinIO (depuis Module 3)
# ============================================

MINIO_ENDPOINT = "http://localhost:9000"
MINIO_ACCESS_KEY = "minioadmin"
MINIO_SECRET_KEY = "minioadmin123"

# Bucket source (images transformées du Module 3)
SOURCE_BUCKET = "processed-images"

# Créer le client S3
s3_client = boto3.client(
    's3',
    endpoint_url=MINIO_ENDPOINT,
    aws_access_key_id=MINIO_ACCESS_KEY,
    aws_secret_access_key=MINIO_SECRET_KEY,
    config=Config(signature_version='s3v4'),
    region_name='us-east-1'
)

def list_images(bucket: str, prefix: str = "") -> List[dict]:
    """Liste les images dans un bucket."""
    images = []
    paginator = s3_client.get_paginator('list_objects_v2')
    
    for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
        if 'Contents' in page:
            for obj in page['Contents']:
                key = obj['Key']
                if key.lower().endswith(('.jpg', '.jpeg', '.png', '.webp')):
                    images.append({'key': key, 'size': obj['Size']})
    return images

def read_image(bucket: str, key: str) -> Optional[Image.Image]:
    """Lit une image depuis MinIO."""
    try:
        response = s3_client.get_object(Bucket=bucket, Key=key)
        return Image.open(io.BytesIO(response['Body'].read())).convert('RGB')
    except Exception as e:
        print(f"❌ Erreur lecture {key}: {e}")
        return None

# Vérifier la connexion
try:
    images = list_images(SOURCE_BUCKET, "transformed/")
    print(f"✅ Connecté à MinIO")
    print(f"📷 {len(images)} images dans '{SOURCE_BUCKET}'")
except Exception as e:
    print(f"⚠️ Erreur connexion MinIO: {e}")
    print(f"   As-tu lancé docker-compose et complété le Module 3 ?")

4.2 Générer des embeddings avec CLIP

Pourquoi générer des embeddings ?

┌─────────────────────────────────────────────────────────────────────────────┐
│                    POURQUOI DES EMBEDDINGS ?                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   SANS EMBEDDINGS                      AVEC EMBEDDINGS                     │
│   ───────────────                      ────────────────                     │
│                                                                             │
│   "Trouve les duplicatas"              "Trouve les duplicatas"             │
│                                                                             │
│   → Hash MD5 : seulement               → Similarité cosinus :              │
│     les copies EXACTES                   même sujet, angle différent       │
│                                          même scène, autre moment          │
│                                                                             │
│   🐱 Photo 1 (face)                    🐱 Photo 1 ←──┐                     │
│   🐱 Photo 2 (profil)                  🐱 Photo 2 ←──┼── "Même chat !"     │
│   → "Différentes" ❌                   → "Similaires" ✅                   │
│                                                                             │
│   ════════════════════════════════════════════════════════════════════     │
│                                                                             │
│   "Cherche des images de plages"       "Cherche des images de plages"      │
│                                                                             │
│   → Impossible sans labels             → Embed "beach sunset"              │
│                                        → Cherche les vecteurs proches      │
│                                        → Trouve les plages ! ✅            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
pythonVoir le code
# ============================================
# Charger CLIP
# ============================================

from transformers import CLIPProcessor, CLIPModel

# Charger le modèle CLIP
print("📥 Chargement de CLIP (peut prendre quelques minutes la première fois)...")

model_name = "openai/clip-vit-base-patch32"  # Modèle léger et rapide
model = CLIPModel.from_pretrained(model_name).to(device)
processor = CLIPProcessor.from_pretrained(model_name)

# Passer en mode évaluation (pas de training)
model.eval()

print(f"✅ CLIP chargé: {model_name}")
print(f"   Dimensions embedding: {model.config.projection_dim}")
pythonVoir le code
# ============================================
# Fonctions pour générer des embeddings
# ============================================

def embed_image(image: Image.Image) -> np.ndarray:
    """
    Génère l'embedding d'une image.
    
    Args:
        image: Image PIL (RGB)
    
    Returns:
        Vecteur numpy de dimension 512, normalisé
    """
    with torch.no_grad():
        inputs = processor(images=image, return_tensors="pt").to(device)
        embedding = model.get_image_features(**inputs)
        # Normaliser (important pour la similarité cosinus)
        embedding = embedding / embedding.norm(dim=-1, keepdim=True)
    return embedding.cpu().numpy().flatten()

def embed_text(text: str) -> np.ndarray:
    """
    Génère l'embedding d'un texte.
    
    Args:
        text: Description textuelle
    
    Returns:
        Vecteur numpy de dimension 512, normalisé
    """
    with torch.no_grad():
        inputs = processor(text=text, return_tensors="pt", padding=True).to(device)
        embedding = model.get_text_features(**inputs)
        embedding = embedding / embedding.norm(dim=-1, keepdim=True)
    return embedding.cpu().numpy().flatten()

def embed_images_batch(images: List[Image.Image], batch_size: int = 32) -> np.ndarray:
    """
    Génère les embeddings pour un batch d'images.
    Plus efficace que de traiter une par une.
    
    Returns:
        Matrice numpy (n_images, 512)
    """
    all_embeddings = []
    
    for i in range(0, len(images), batch_size):
        batch = images[i:i + batch_size]
        
        with torch.no_grad():
            inputs = processor(images=batch, return_tensors="pt", padding=True).to(device)
            embeddings = model.get_image_features(**inputs)
            embeddings = embeddings / embeddings.norm(dim=-1, keepdim=True)
        
        all_embeddings.append(embeddings.cpu().numpy())
    
    return np.vstack(all_embeddings)

print("✅ Fonctions d'embedding définies")
pythonVoir le code
# ============================================
# Test : générer un embedding
# ============================================

# Créer une image de test
test_img = Image.new('RGB', (256, 256), color=(255, 128, 64))

# Générer l'embedding
embedding = embed_image(test_img)

print(f"📊 Embedding généré:")
print(f"   Shape: {embedding.shape}")
print(f"   Type: {embedding.dtype}")
print(f"   Norme: {np.linalg.norm(embedding):.4f} (devrait être ~1.0)")
print(f"   Premiers éléments: [{embedding[0]:.3f}, {embedding[1]:.3f}, {embedding[2]:.3f}, ...]")

4.3 Mesurer la similarité

Similarité cosinus : comment ça marche ?

┌─────────────────────────────────────────────────────────────────────────────┐
│                    SIMILARITÉ COSINUS                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│                     B                                                      │
│                    /                                                       │
│                   /  angle θ petit                                         │
│                  /   = très similaires                                     │
│                 /    cos(θ) ≈ 1                                            │
│                /                                                           │
│   ────────────A────────────────────                                        │
│                \                                                           │
│                 \                                                          │
│                  \  angle θ grand                                          │
│                   \ = très différents                                      │
│                    \ cos(θ) ≈ 0                                            │
│                     C                                                      │
│                                                                             │
│   Si les vecteurs sont normalisés (norme = 1):                             │
│   similarité = A · B  (simple produit scalaire !)                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
Similarité Interprétation Action DE
0.95+ Quasi-duplicata ⚠️ Supprimer un des deux
0.85-0.95 Très similaires Vérifier manuellement
0.70-0.85 Même catégorie OK
< 0.70 Différents OK
pythonVoir le code
# ============================================
# Calculer la similarité
# ============================================

def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    """
    Calcule la similarité cosinus entre deux vecteurs normalisés.
    C'est simplement le produit scalaire !
    """
    return float(np.dot(a, b))

# Test avec des images de couleurs différentes
print("🔍 Test de similarité:\n")

# Créer des images
red_img = Image.new('RGB', (256, 256), color=(255, 0, 0))
blue_img = Image.new('RGB', (256, 256), color=(0, 0, 255))
red_dark_img = Image.new('RGB', (256, 256), color=(128, 0, 0))  # Rouge foncé

# Embeddings images
red_emb = embed_image(red_img)
blue_emb = embed_image(blue_img)
red_dark_emb = embed_image(red_dark_img)

# Embeddings texte
red_text_emb = embed_text("a red image")
blue_text_emb = embed_text("a blue image")

# Comparer
print("Image vs Texte:")
print(f"   Rouge vs 'a red image':  {cosine_similarity(red_emb, red_text_emb):.3f}")
print(f"   Rouge vs 'a blue image': {cosine_similarity(red_emb, blue_text_emb):.3f}")
print(f"   Bleu vs 'a blue image':  {cosine_similarity(blue_emb, blue_text_emb):.3f}")

print("\nImage vs Image:")
print(f"   Rouge vs Rouge foncé:    {cosine_similarity(red_emb, red_dark_emb):.3f} (similaires)")
print(f"   Rouge vs Bleu:           {cosine_similarity(red_emb, blue_emb):.3f} (différents)")

4.4 Stocker les embeddings avec Chroma

Pourquoi une base vectorielle ?

┌─────────────────────────────────────────────────────────────────────────────┐
│                    POURQUOI UNE VECTOR DATABASE ?                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   SANS VECTOR DB                       AVEC VECTOR DB (Chroma)             │
│   ──────────────                       ────────────────────────             │
│                                                                             │
│   Stockage: fichier numpy              Stockage: base optimisée            │
│   Recherche: boucle sur tout           Recherche: index ANN                │
│   1M images: 10 minutes/requête        1M images: 10 ms/requête            │
│                                                                             │
│   ════════════════════════════════════════════════════════════════════     │
│                                                                             │
│   ANN = Approximate Nearest Neighbors                                      │
│   → Ne compare pas avec TOUT le dataset                                    │
│   → Trouve les ~mêmes résultats, 1000x plus vite                           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Alternatives à Chroma

Outil Type Quand l'utiliser
Chroma Embarqué/Serveur ✅ Prototypage, petits projets
Pinecone Cloud Production, managed
Milvus Self-hosted Production, gros volumes
pgvector Extension PostgreSQL Si tu as déjà PostgreSQL
pythonVoir le code
# ============================================
# Configurer Chroma
# ============================================

import chromadb

# Client local (persistant sur disque)
chroma_client = chromadb.PersistentClient(path="./chroma_db")

# Créer une collection pour nos images
COLLECTION_NAME = "image_embeddings"

# Supprimer si existe (pour repartir de zéro)
try:
    chroma_client.delete_collection(COLLECTION_NAME)
except:
    pass

# Créer la collection
collection = chroma_client.create_collection(
    name=COLLECTION_NAME,
    metadata={"description": "Embeddings CLIP des images"}
)

print(f"✅ Collection Chroma créée: {COLLECTION_NAME}")
pythonVoir le code
# ============================================
# Indexer les images dans Chroma
# ============================================

def index_images_to_chroma(
    bucket: str,
    collection,
    prefix: str = "",
    max_images: int = None,
    batch_size: int = 16
):
    """
    Indexe les images depuis MinIO vers Chroma.
    
    Args:
        bucket: Bucket MinIO source
        collection: Collection Chroma
        prefix: Préfixe des clés
        max_images: Limite (None = toutes)
        batch_size: Taille des batches
    """
    # Lister les images
    image_list = list_images(bucket, prefix)
    if max_images:
        image_list = image_list[:max_images]
    
    print(f"📤 Indexation de {len(image_list)} images...")
    
    # Traiter par batch
    for i in range(0, len(image_list), batch_size):
        batch_info = image_list[i:i + batch_size]
        
        # Charger les images
        images = []
        ids = []
        metadatas = []
        
        for info in batch_info:
            img = read_image(bucket, info['key'])
            if img:
                images.append(img)
                ids.append(info['key'])
                metadatas.append({
                    'bucket': bucket,
                    'key': info['key'],
                    'size_bytes': info['size']
                })
        
        if not images:
            continue
        
        # Générer les embeddings
        embeddings = embed_images_batch(images, batch_size=batch_size)
        
        # Ajouter à Chroma
        collection.add(
            ids=ids,
            embeddings=embeddings.tolist(),
            metadatas=metadatas
        )
        
        print(f"   [{min(i + batch_size, len(image_list))}/{len(image_list)}] indexées")
    
    print(f"\n✅ {collection.count()} images indexées dans Chroma")

# Indexer (limité à 50 pour la démo)
index_images_to_chroma(
    bucket=SOURCE_BUCKET,
    collection=collection,
    prefix="transformed/",
    max_images=50
)

4.5 Recherche par similarité

Deux types de recherche

┌─────────────────────────────────────────────────────────────────────────────┐
│                    TYPES DE RECHERCHE                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   1. RECHERCHE PAR IMAGE                                                   │
│      ─────────────────────                                                 │
│      "Trouve des images similaires à celle-ci"                             │
│      Image → Embed → Chercher les plus proches                             │
│                                                                             │
│   2. RECHERCHE PAR TEXTE                                                   │
│      ────────────────────                                                  │
│      "Trouve des images de couchers de soleil"                             │
│      Texte → Embed → Chercher les plus proches                             │
│                                                                             │
│   Dans les deux cas, c'est la MÊME opération :                             │
│   chercher les vecteurs les plus proches dans Chroma                       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
pythonVoir le code
# ============================================
# Recherche par similarité
# ============================================

def search_by_image(image: Image.Image, n_results: int = 5) -> dict:
    """Recherche les images similaires à une image."""
    query_embedding = embed_image(image)
    results = collection.query(
        query_embeddings=[query_embedding.tolist()],
        n_results=n_results
    )
    return results

def search_by_text(text: str, n_results: int = 5) -> dict:
    """Recherche les images correspondant à une description."""
    query_embedding = embed_text(text)
    results = collection.query(
        query_embeddings=[query_embedding.tolist()],
        n_results=n_results
    )
    return results

# Test de recherche par texte
print("🔍 Recherche par texte: 'colorful pattern'\n")
results = search_by_text("colorful pattern", n_results=3)

for i, (id_, distance) in enumerate(zip(results['ids'][0], results['distances'][0])):
    similarity = 1 - distance  # Chroma retourne la distance
    print(f"   {i+1}. {id_.split('/')[-1]}")
    print(f"      Similarité: {similarity:.3f}")

4.6 Déduplication sémantique

Les 3 niveaux de déduplication

┌─────────────────────────────────────────────────────────────────────────────┐
│                    NIVEAUX DE DÉDUPLICATION                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   NIVEAU 1: EXACT (Module 3)                                               │
│   ─────────────────────────────                                            │
│   Outil: Hash MD5                                                          │
│   Détecte: Copies bit-à-bit identiques                                     │
│   Exemple: Même fichier copié 2 fois                                       │
│                                                                             │
│   NIVEAU 2: PERCEPTUEL (Module 3)                                          │
│   ───────────────────────────────                                          │
│   Outil: dHash                                                             │
│   Détecte: Visuellement identiques malgré compression                      │
│   Exemple: JPEG qualité 80 vs qualité 90                                   │
│                                                                             │
│   NIVEAU 3: SÉMANTIQUE (Module 4) ◄── NOUVEAU                              │
│   ────────────────────────────────                                         │
│   Outil: Embeddings CLIP                                                   │
│   Détecte: Même sujet/concept, images différentes                          │
│   Exemple: Même chat photographié sous 2 angles                            │
│                                                                             │
│   🐱 face    🐱 profil                                                     │
│   MD5: ≠     dHash: ≠     CLIP: ≈ 0.92 → DUPLICATA SÉMANTIQUE             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Pourquoi c'est important ?

Problème Conséquence Solution
Même chat dans train ET test Fuite → Métriques faussées Dédup sémantique
100 photos du même objet Overfitting Dédup sémantique
Photos burst (rafale) Fausse diversité Dédup sémantique
pythonVoir le code
# ============================================
# Déduplication sémantique
# ============================================

from typing import List, Tuple

def find_semantic_duplicates(
    collection,
    similarity_threshold: float = 0.95
) -> List[Tuple[str, str, float]]:
    """
    Trouve les paires d'images sémantiquement similaires.
    
    Args:
        collection: Collection Chroma
        similarity_threshold: Seuil (0.95 = très similaire)
    
    Returns:
        Liste de tuples (id1, id2, similarité)
    """
    # Récupérer tous les embeddings
    all_data = collection.get(include=['embeddings'])
    ids = all_data['ids']
    embeddings = np.array(all_data['embeddings'])
    
    duplicates = []
    n = len(ids)
    
    print(f"🔍 Recherche de duplicatas parmi {n} images (seuil: {similarity_threshold})...")
    
    # Comparer chaque paire (O(n²) - OK pour petits datasets)
    for i in range(n):
        for j in range(i + 1, n):
            similarity = np.dot(embeddings[i], embeddings[j])
            
            if similarity >= similarity_threshold:
                duplicates.append((ids[i], ids[j], float(similarity)))
    
    # Trier par similarité décroissante
    duplicates.sort(key=lambda x: x[2], reverse=True)
    
    print(f"✅ {len(duplicates)} paires trouvées")
    
    return duplicates

# Chercher les duplicatas
duplicates = find_semantic_duplicates(collection, similarity_threshold=0.90)

if duplicates:
    print(f"\n📋 Paires les plus similaires:")
    for id1, id2, sim in duplicates[:5]:
        print(f"   {sim:.3f}: {id1.split('/')[-1]}{id2.split('/')[-1]}")
else:
    print("\n✅ Aucun duplicata sémantique trouvé")

4.7 Pipeline d'enrichissement complet

Ce que le DE livre

┌─────────────────────────────────────────────────────────────────────────────┐
│                    LIVRABLE DU DATA ENGINEER                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ENTRÉE (Module 3)                    SORTIE (Module 4)                   │
│   ─────────────────                    ─────────────────                   │
│                                                                             │
│   MinIO                                Chroma                              │
│   └── processed-images/                └── image_embeddings/               │
│       ├── transformed/*.jpg                ├── Vecteurs 512D               │
│       └── catalog.parquet                  ├── Métadonnées                 │
│                                            └── Index de recherche          │
│                                                                             │
│   Ce que peut faire le Data Scientist:                                     │
│   • Rechercher par image ou texte                                          │
│   • Explorer le dataset                                                    │
│   • Faire du clustering (son travail, pas le tien)                         │
│   • Détecter les anomalies                                                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
pythonVoir le code
# ============================================
# Pipeline d'enrichissement complet
# ============================================

class EnrichmentPipeline:
    """
    Pipeline DE pour l'enrichissement:
    1. Charge les images depuis MinIO
    2. Génère les embeddings CLIP
    3. Stocke dans Chroma
    4. Détecte les duplicatas sémantiques
    """
    
    def __init__(self, collection_name: str = "enrichment_pipeline"):
        self.collection_name = collection_name
        
        # Supprimer si existe
        try:
            chroma_client.delete_collection(collection_name)
        except:
            pass
        
        self.collection = chroma_client.create_collection(name=collection_name)
        self.stats = {'images': 0, 'duplicates': 0}
    
    def run(
        self,
        bucket: str,
        prefix: str = "",
        max_images: int = None,
        dedup_threshold: float = 0.95
    ) -> dict:
        """
        Exécute le pipeline complet.
        """
        print("="*60)
        print("🚀 PIPELINE D'ENRICHISSEMENT")
        print("="*60)
        
        # 1. Indexer
        print("\n📤 Étape 1: Indexation des images...")
        index_images_to_chroma(
            bucket=bucket,
            collection=self.collection,
            prefix=prefix,
            max_images=max_images
        )
        self.stats['images'] = self.collection.count()
        
        # 2. Déduplication
        print("\n🔍 Étape 2: Détection des duplicatas sémantiques...")
        duplicates = find_semantic_duplicates(self.collection, dedup_threshold)
        self.stats['duplicates'] = len(duplicates)
        
        # Résumé
        print("\n" + "="*60)
        print("📊 RÉSUMÉ")
        print("="*60)
        print(f"   Images indexées: {self.stats['images']}")
        print(f"   Duplicatas sémantiques: {self.stats['duplicates']}")
        print(f"   Collection Chroma: {self.collection_name}")
        
        return {
            'stats': self.stats,
            'duplicates': duplicates,
            'collection': self.collection
        }

print("✅ Pipeline défini")
pythonVoir le code
# ============================================
# Exécuter le pipeline
# ============================================

pipeline = EnrichmentPipeline(collection_name="final_enrichment")

results = pipeline.run(
    bucket=SOURCE_BUCKET,
    prefix="transformed/",
    max_images=50,
    dedup_threshold=0.90
)

4.8 Code source et ressources

Structure du projet

module4-enrichment/
├── docker-compose.yml       # MinIO + Chroma
├── requirements.txt
├── src/
│   ├── embeddings.py        # Fonctions CLIP
│   ├── vector_db.py         # Interface Chroma
│   ├── dedup.py             # Déduplication sémantique
│   └── pipeline.py          # Pipeline complet
└── notebooks/
    └── module4.ipynb
pythonVoir le code
# ============================================
# Générer le package téléchargeable
# ============================================

import os
import zipfile
import base64
from IPython.display import HTML, display

# Créer la structure
os.makedirs('module4-enrichment/src', exist_ok=True)

# requirements.txt
requirements = '''torch>=2.0.0
torchvision>=0.15.0
transformers>=4.30.0
chromadb>=0.4.0
pillow>=9.0.0
numpy>=1.24.0
boto3>=1.28.0
'''

with open('module4-enrichment/requirements.txt', 'w') as f:
    f.write(requirements)

# docker-compose.yml
docker_compose = '''version: '3.8'

services:
  minio:
    image: minio/minio:latest
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin123
    command: server /data --console-address ":9001"
    volumes:
      - minio_data:/data

  chroma:
    image: chromadb/chroma:latest
    ports:
      - "8000:8000"
    volumes:
      - chroma_data:/chroma/chroma

volumes:
  minio_data:
  chroma_data:
'''

with open('module4-enrichment/docker-compose.yml', 'w') as f:
    f.write(docker_compose)

# Créer le ZIP
zip_path = 'module4-enrichment.zip'
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
    for root, dirs, files in os.walk('module4-enrichment'):
        for file in files:
            file_path = os.path.join(root, file)
            zipf.write(file_path, file_path)

# Lien de téléchargement
with open(zip_path, 'rb') as f:
    b64 = base64.b64encode(f.read()).decode()

html = f'''
<a download="module4-enrichment.zip" 
   href="data:application/zip;base64,{b64}" 
   style="display: inline-block; padding: 10px 20px; 
          background-color: #4CAF50; color: white; 
          text-decoration: none; border-radius: 5px;
          font-weight: bold;">
   📦 Télécharger module4-enrichment.zip
</a>
'''

display(HTML(html))
print(f"\n✅ Archive créée: {zip_path}")

🎯 Checkpoint — Quiz

Teste tes connaissances ! Réponds aux questions, puis déroule pour voir les réponses.


Question 1 : Qu'est-ce qu'un embedding ?

A) Une copie compressée de l'image
B) Un vecteur qui capture le sens d'une donnée
C) Un hash de l'image
D) Un format de fichier

📖 Voir la réponse

Réponse : B

Un embedding est un vecteur de nombres (ex: 512 dimensions) qui représente le sens d'une donnée. Contrairement aux pixels qui sont "bruts", l'embedding capture des concepts : "c'est un chat", "il est mignon", etc.

  • A) Non, ce n'est pas une compression
  • C) Un hash est un identifiant unique, pas une représentation sémantique
  • D) Ce n'est pas un format de fichier

Question 2 : Pourquoi deux images de chats différents ont des embeddings proches ?

A) Parce qu'elles ont les mêmes pixels
B) Parce qu'elles ont le même hash MD5
C) Parce que CLIP comprend qu'elles représentent le même concept
D) Parce qu'elles ont la même taille

📖 Voir la réponse

Réponse : C

CLIP a été entraîné sur des millions de paires image-texte. Il a appris que les images de chats partagent des caractéristiques communes, même si les pixels sont complètement différents.

C'est la différence fondamentale avec les hashs :

  • Hash MD5 : compare les bits → chat1 ≠ chat2
  • Embedding : compare le sens → chat1 ≈ chat2

Question 3 : Quelle similarité indique un duplicata sémantique ?

A) 0.30
B) 0.50
C) 0.70
D) 0.95

📖 Voir la réponse

Réponse : D

Similarité Interprétation
0.30 Très différents
0.50 Peu de points communs
0.70 Même catégorie générale
0.95 Quasi-duplicata sémantique

Un seuil de 0.90-0.95 est typique pour détecter les duplicatas. En dessous, on risque de supprimer des images légitimement différentes.


Question 4 : Pourquoi utiliser Chroma plutôt qu'un fichier numpy ?

A) Chroma compresse mieux les données
B) Chroma permet une recherche rapide (ANN) même avec 1M de vecteurs
C) Chroma est gratuit, numpy est payant
D) Chroma génère automatiquement les embeddings

📖 Voir la réponse

Réponse : B

Avec un fichier numpy, chercher les voisins les plus proches nécessite de comparer avec tous les vecteurs → O(n).

Chroma utilise des index ANN (Approximate Nearest Neighbors) qui trouvent les voisins proches en O(log n) ou O(1), soit 1000x plus rapide sur de gros datasets.

  • A) La compression n'est pas le but principal
  • C) numpy est aussi gratuit
  • D) Non, CLIP génère les embeddings, Chroma les stocke

Question 5 : Le clustering, c'est le travail de qui ?

A) Du Data Engineer
B) Du Data Scientist
C) Du DevOps
D) Du Product Manager

📖 Voir la réponse

Réponse : B

Le Data Engineer prépare les données :

  • Génère les embeddings
  • Les stocke dans une vector DB
  • Détecte les duplicatas

Le Data Scientist explore et analyse :

  • Fait du clustering (K-Means, etc.)
  • Visualise avec UMAP/t-SNE
  • Détecte les anomalies
  • Entraîne des modèles

Question 6 : CLIP permet de chercher des images avec du texte. Pourquoi ?

A) Parce qu'il convertit le texte en image
B) Parce qu'il encode images ET texte dans le même espace vectoriel
C) Parce qu'il utilise OCR
D) Parce qu'il stocke le texte dans les métadonnées

📖 Voir la réponse

Réponse : B

CLIP a deux encodeurs :

  • Un pour les images → vecteur 512D
  • Un pour le texte → vecteur 512D

Ces deux vecteurs vivent dans le même espace. Donc "a cute cat" et 🐱 ont des embeddings proches !

C'est ce qui permet la recherche multimodale : texte → images similaires.


➡️ Prochaine étape

Module 5 : Automatiser et Versionner

Tu vas :

  • Orchestrer le pipeline avec Airflow
  • Versionner les données avec DVC
  • Tracker les expériences avec MLflow
  • Créer un pipeline reproductible de bout en bout

Module Data Engineering for AI — From Zero to Hero Bootcamp