🤖 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 3 → MODULE 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