""" 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))