v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
388
core/embedding/similarity.py
Normal file
388
core/embedding/similarity.py
Normal file
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
Similarity - Calculs de Similarité et Distance
|
||||
|
||||
Fonctions pour calculer différentes métriques de similarité et distance
|
||||
entre vecteurs d'embeddings.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from typing import Union, List
|
||||
|
||||
|
||||
def cosine_similarity(vec1: np.ndarray, vec2: np.ndarray) -> float:
|
||||
"""
|
||||
Calculer similarité cosinus entre deux vecteurs
|
||||
|
||||
similarity = (vec1 · vec2) / (||vec1|| * ||vec2||)
|
||||
|
||||
Args:
|
||||
vec1: Premier vecteur
|
||||
vec2: Deuxième vecteur
|
||||
|
||||
Returns:
|
||||
Similarité cosinus dans [-1, 1]
|
||||
1 = identiques, 0 = orthogonaux, -1 = opposés
|
||||
|
||||
Raises:
|
||||
ValueError: Si dimensions ne correspondent pas
|
||||
"""
|
||||
if vec1.shape != vec2.shape:
|
||||
raise ValueError(
|
||||
f"Vectors must have same shape: {vec1.shape} vs {vec2.shape}"
|
||||
)
|
||||
|
||||
# Produit scalaire
|
||||
dot_product = np.dot(vec1, vec2)
|
||||
|
||||
# Normes
|
||||
norm1 = np.linalg.norm(vec1)
|
||||
norm2 = np.linalg.norm(vec2)
|
||||
|
||||
# Éviter division par zéro
|
||||
if norm1 == 0 or norm2 == 0:
|
||||
return 0.0
|
||||
|
||||
# Similarité cosinus
|
||||
similarity = dot_product / (norm1 * norm2)
|
||||
|
||||
# Clamp dans [-1, 1] pour éviter erreurs numériques
|
||||
similarity = np.clip(similarity, -1.0, 1.0)
|
||||
|
||||
return float(similarity)
|
||||
|
||||
|
||||
def euclidean_distance(vec1: np.ndarray, vec2: np.ndarray) -> float:
|
||||
"""
|
||||
Calculer distance euclidienne (L2) entre deux vecteurs
|
||||
|
||||
distance = ||vec1 - vec2||_2 = sqrt(sum((vec1 - vec2)^2))
|
||||
|
||||
Args:
|
||||
vec1: Premier vecteur
|
||||
vec2: Deuxième vecteur
|
||||
|
||||
Returns:
|
||||
Distance euclidienne (>= 0)
|
||||
|
||||
Raises:
|
||||
ValueError: Si dimensions ne correspondent pas
|
||||
"""
|
||||
if vec1.shape != vec2.shape:
|
||||
raise ValueError(
|
||||
f"Vectors must have same shape: {vec1.shape} vs {vec2.shape}"
|
||||
)
|
||||
|
||||
return float(np.linalg.norm(vec1 - vec2))
|
||||
|
||||
|
||||
def manhattan_distance(vec1: np.ndarray, vec2: np.ndarray) -> float:
|
||||
"""
|
||||
Calculer distance de Manhattan (L1) entre deux vecteurs
|
||||
|
||||
distance = sum(|vec1 - vec2|)
|
||||
|
||||
Args:
|
||||
vec1: Premier vecteur
|
||||
vec2: Deuxième vecteur
|
||||
|
||||
Returns:
|
||||
Distance de Manhattan (>= 0)
|
||||
|
||||
Raises:
|
||||
ValueError: Si dimensions ne correspondent pas
|
||||
"""
|
||||
if vec1.shape != vec2.shape:
|
||||
raise ValueError(
|
||||
f"Vectors must have same shape: {vec1.shape} vs {vec2.shape}"
|
||||
)
|
||||
|
||||
return float(np.sum(np.abs(vec1 - vec2)))
|
||||
|
||||
|
||||
def dot_product(vec1: np.ndarray, vec2: np.ndarray) -> float:
|
||||
"""
|
||||
Calculer produit scalaire entre deux vecteurs
|
||||
|
||||
dot = vec1 · vec2 = sum(vec1 * vec2)
|
||||
|
||||
Args:
|
||||
vec1: Premier vecteur
|
||||
vec2: Deuxième vecteur
|
||||
|
||||
Returns:
|
||||
Produit scalaire
|
||||
|
||||
Raises:
|
||||
ValueError: Si dimensions ne correspondent pas
|
||||
"""
|
||||
if vec1.shape != vec2.shape:
|
||||
raise ValueError(
|
||||
f"Vectors must have same shape: {vec1.shape} vs {vec2.shape}"
|
||||
)
|
||||
|
||||
return float(np.dot(vec1, vec2))
|
||||
|
||||
|
||||
def normalize_l2(vector: np.ndarray, epsilon: float = 1e-10) -> np.ndarray:
|
||||
"""
|
||||
Normaliser un vecteur avec norme L2
|
||||
|
||||
normalized = vector / ||vector||_2
|
||||
|
||||
Args:
|
||||
vector: Vecteur à normaliser
|
||||
epsilon: Valeur minimale pour éviter division par zéro
|
||||
|
||||
Returns:
|
||||
Vecteur normalisé (norme L2 = 1.0)
|
||||
"""
|
||||
norm = np.linalg.norm(vector)
|
||||
if norm < epsilon:
|
||||
return vector
|
||||
return vector / norm
|
||||
|
||||
|
||||
def normalize_l1(vector: np.ndarray, epsilon: float = 1e-10) -> np.ndarray:
|
||||
"""
|
||||
Normaliser un vecteur avec norme L1
|
||||
|
||||
normalized = vector / sum(|vector|)
|
||||
|
||||
Args:
|
||||
vector: Vecteur à normaliser
|
||||
epsilon: Valeur minimale pour éviter division par zéro
|
||||
|
||||
Returns:
|
||||
Vecteur normalisé (norme L1 = 1.0)
|
||||
"""
|
||||
norm = np.sum(np.abs(vector))
|
||||
if norm < epsilon:
|
||||
return vector
|
||||
return vector / norm
|
||||
|
||||
|
||||
def batch_cosine_similarity(vectors: List[np.ndarray],
|
||||
query: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Calculer similarité cosinus entre une requête et un batch de vecteurs
|
||||
|
||||
Args:
|
||||
vectors: Liste de vecteurs
|
||||
query: Vecteur de requête
|
||||
|
||||
Returns:
|
||||
Array de similarités
|
||||
"""
|
||||
# Convertir en matrice
|
||||
matrix = np.array(vectors)
|
||||
|
||||
# Normaliser
|
||||
matrix_norm = matrix / (np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-10)
|
||||
query_norm = query / (np.linalg.norm(query) + 1e-10)
|
||||
|
||||
# Produit matriciel
|
||||
similarities = np.dot(matrix_norm, query_norm)
|
||||
|
||||
# Clamp
|
||||
similarities = np.clip(similarities, -1.0, 1.0)
|
||||
|
||||
return similarities
|
||||
|
||||
|
||||
def pairwise_cosine_similarity(vectors: List[np.ndarray]) -> np.ndarray:
|
||||
"""
|
||||
Calculer matrice de similarité cosinus entre tous les vecteurs
|
||||
|
||||
Args:
|
||||
vectors: Liste de vecteurs
|
||||
|
||||
Returns:
|
||||
Matrice de similarité (n x n)
|
||||
"""
|
||||
# Convertir en matrice
|
||||
matrix = np.array(vectors)
|
||||
|
||||
# Normaliser
|
||||
matrix_norm = matrix / (np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-10)
|
||||
|
||||
# Produit matriciel
|
||||
similarity_matrix = np.dot(matrix_norm, matrix_norm.T)
|
||||
|
||||
# Clamp
|
||||
similarity_matrix = np.clip(similarity_matrix, -1.0, 1.0)
|
||||
|
||||
return similarity_matrix
|
||||
|
||||
|
||||
def angular_distance(vec1: np.ndarray, vec2: np.ndarray) -> float:
|
||||
"""
|
||||
Calculer distance angulaire entre deux vecteurs
|
||||
|
||||
distance = arccos(cosine_similarity) / π
|
||||
|
||||
Args:
|
||||
vec1: Premier vecteur
|
||||
vec2: Deuxième vecteur
|
||||
|
||||
Returns:
|
||||
Distance angulaire dans [0, 1]
|
||||
"""
|
||||
similarity = cosine_similarity(vec1, vec2)
|
||||
angle = np.arccos(np.clip(similarity, -1.0, 1.0))
|
||||
return float(angle / np.pi)
|
||||
|
||||
|
||||
def jaccard_similarity(vec1: np.ndarray, vec2: np.ndarray) -> float:
|
||||
"""
|
||||
Calculer similarité de Jaccard pour vecteurs binaires
|
||||
|
||||
similarity = |intersection| / |union|
|
||||
|
||||
Args:
|
||||
vec1: Premier vecteur binaire
|
||||
vec2: Deuxième vecteur binaire
|
||||
|
||||
Returns:
|
||||
Similarité de Jaccard dans [0, 1]
|
||||
"""
|
||||
if vec1.shape != vec2.shape:
|
||||
raise ValueError(
|
||||
f"Vectors must have same shape: {vec1.shape} vs {vec2.shape}"
|
||||
)
|
||||
|
||||
intersection = np.sum(np.logical_and(vec1, vec2))
|
||||
union = np.sum(np.logical_or(vec1, vec2))
|
||||
|
||||
if union == 0:
|
||||
return 0.0
|
||||
|
||||
return float(intersection / union)
|
||||
|
||||
|
||||
def hamming_distance(vec1: np.ndarray, vec2: np.ndarray) -> float:
|
||||
"""
|
||||
Calculer distance de Hamming pour vecteurs binaires
|
||||
|
||||
distance = nombre de positions différentes
|
||||
|
||||
Args:
|
||||
vec1: Premier vecteur binaire
|
||||
vec2: Deuxième vecteur binaire
|
||||
|
||||
Returns:
|
||||
Distance de Hamming
|
||||
"""
|
||||
if vec1.shape != vec2.shape:
|
||||
raise ValueError(
|
||||
f"Vectors must have same shape: {vec1.shape} vs {vec2.shape}"
|
||||
)
|
||||
|
||||
return float(np.sum(vec1 != vec2))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fonctions de conversion
|
||||
# ============================================================================
|
||||
|
||||
def similarity_to_distance(similarity: float,
|
||||
method: str = "cosine") -> float:
|
||||
"""
|
||||
Convertir similarité en distance
|
||||
|
||||
Args:
|
||||
similarity: Valeur de similarité
|
||||
method: Méthode ("cosine", "angular")
|
||||
|
||||
Returns:
|
||||
Distance correspondante
|
||||
"""
|
||||
if method == "cosine":
|
||||
# distance = 1 - similarity (pour cosine dans [0, 1])
|
||||
return 1.0 - similarity
|
||||
elif method == "angular":
|
||||
# distance angulaire
|
||||
angle = np.arccos(np.clip(similarity, -1.0, 1.0))
|
||||
return float(angle / np.pi)
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
|
||||
def distance_to_similarity(distance: float,
|
||||
method: str = "euclidean") -> float:
|
||||
"""
|
||||
Convertir distance en similarité
|
||||
|
||||
Args:
|
||||
distance: Valeur de distance
|
||||
method: Méthode ("euclidean", "manhattan")
|
||||
|
||||
Returns:
|
||||
Similarité correspondante dans [0, 1]
|
||||
"""
|
||||
if method in ["euclidean", "manhattan"]:
|
||||
# similarity = 1 / (1 + distance)
|
||||
return 1.0 / (1.0 + distance)
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fonctions utilitaires
|
||||
# ============================================================================
|
||||
|
||||
def is_normalized(vector: np.ndarray,
|
||||
norm_type: str = "l2",
|
||||
tolerance: float = 1e-6) -> bool:
|
||||
"""
|
||||
Vérifier si un vecteur est normalisé
|
||||
|
||||
Args:
|
||||
vector: Vecteur à vérifier
|
||||
norm_type: Type de norme ("l2" ou "l1")
|
||||
tolerance: Tolérance pour la vérification
|
||||
|
||||
Returns:
|
||||
True si normalisé, False sinon
|
||||
"""
|
||||
if norm_type == "l2":
|
||||
norm = np.linalg.norm(vector)
|
||||
elif norm_type == "l1":
|
||||
norm = np.sum(np.abs(vector))
|
||||
else:
|
||||
raise ValueError(f"Unknown norm type: {norm_type}")
|
||||
|
||||
return abs(norm - 1.0) < tolerance
|
||||
|
||||
|
||||
def compute_centroid(vectors: List[np.ndarray]) -> np.ndarray:
|
||||
"""
|
||||
Calculer le centroïde (moyenne) d'un ensemble de vecteurs
|
||||
|
||||
Args:
|
||||
vectors: Liste de vecteurs
|
||||
|
||||
Returns:
|
||||
Vecteur centroïde
|
||||
"""
|
||||
if not vectors:
|
||||
raise ValueError("Cannot compute centroid of empty list")
|
||||
|
||||
matrix = np.array(vectors)
|
||||
return np.mean(matrix, axis=0)
|
||||
|
||||
|
||||
def compute_variance(vectors: List[np.ndarray]) -> float:
|
||||
"""
|
||||
Calculer la variance d'un ensemble de vecteurs
|
||||
|
||||
Args:
|
||||
vectors: Liste de vecteurs
|
||||
|
||||
Returns:
|
||||
Variance totale
|
||||
"""
|
||||
if not vectors:
|
||||
raise ValueError("Cannot compute variance of empty list")
|
||||
|
||||
matrix = np.array(vectors)
|
||||
return float(np.var(matrix))
|
||||
Reference in New Issue
Block a user