Cosine, Euclidean, Dot Product — разбираем на примерах, какая метрика для какой задачи подходит. Практические тесты и сравнения.
Cosine, Euclidean, Dot Product — разбираем на примерах, какая метрика для какой задачи подходит. Практические тесты и сравнения.
За 45 минут вы:
После генерации эмбеддингов следующий шаг — найти ближайшие векторы. Но что значит "ближайшие"?
Разные метрики измеряют разные виды близости:
A = [0.5, 0.3, 0.2]
B = [0.48, 0.32, 0.21] # Похож на A
C = [1.0, 0.6, 0.4] # То же направление, но в 2 раза длиннее
# Cosine Similarity:
cosine(A, B) = 0.998 # Очень похожи
cosine(A, C) = 1.0 # Идентичное направление!
# Euclidean Distance:
L2(A, B) = 0.037 # Близко
L2(A, C) = 0.54 # ДальшеВывод: Cosine игнорирует длину вектора, Euclidean учитывает.
cos(θ) = (A · B) / (‖A‖ × ‖B‖)
Где:
A · B — скалярное произведение‖A‖ — длина (норма) вектора| Значение | Интерпретация |
|---|---|
| 1.0 | Идентичное направление (максимальное сходство) |
| 0.7–0.9 | Очень похожие |
| 0.5–0.7 | Умеренное сходство |
| 0.3–0.5 | Слабое сходство |
| 0.0 | Ортогональные (нет связи) |
| < 0 | Противоположное направление |
✅ Подходит для:
❌ Не подходит для:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# Два вектора
A = np.array([[0.12, -0.45, 0.78, 0.33]])
B = np.array([[0.15, -0.42, 0.75, 0.31]])
C = np.array([[0.89, 0.23, -0.12, 0.67]])
# Косинусное сходство
sim_ab = cosine_similarity(A, B)[0][0] # 0.998
sim_ac = cosine_similarity(A, C)[0][0] # 0.123
print(f"A ↔ B: {sim_ab:.3f}") # Очень похожи
print(f"A ↔ C: {sim_ac:.3f}") # Не похожиДля нормализованных векторов (длина = 1):
from sklearn.preprocessing import normalize
# Нормализация
A_norm = normalize(A)[0]
B_norm = normalize(B)[0]
# Скалярное произведение = косинусное сходство
similarity = np.dot(A_norm, B_norm)Почему это работает:
cos(θ) = (A · B) / (‖A‖ × ‖B‖)
Когда ‖A‖ = ‖B‖ = 1 (нормализованы):
cos(θ) = A · B
L2(A, B) = √Σ(Ai - Bi)²
| Значение | Интерпретация |
|---|---|
| 0.0 | Идентичные векторы |
| 0.0–0.5 | Очень близкие |
| 0.5–1.0 | Близкие |
| 1.0–2.0 | Умеренное расстояние |
| > 2.0 | Далёкие векторы |
Важно: Значения зависят от масштабирования данных!
✅ Подходит для:
❌ Не подходит для:
from scipy.spatial import distance
A = np.array([0.12, -0.45, 0.78, 0.33])
B = np.array([0.15, -0.42, 0.75, 0.31])
C = np.array([0.89, 0.23, -0.12, 0.67])
# Евклидово расстояние
dist_ab = distance.euclidean(A, B) # 0.057
dist_ac = distance.euclidean(A, C) # 1.42
print(f"Расстояние A ↔ B: {dist_ab:.3f}") # Близко
print(f"Расстояние A ↔ C: {dist_ac:.3f}") # ДалёкоДля нормализованных векторов:
# L2² = 2 × (1 - cosine_similarity)
# cosine_similarity = 1 - (L2² / 2)
cosine = 0.9
l2_squared = 2 * (1 - cosine) # 0.2
l2 = np.sqrt(l2_squared) # 0.447A · B = Σ(Ai × Bi)
От -∞ до +∞ (зависит от масштаба векторов)
Важно: Без нормализации значения трудно интерпретировать!
✅ Подходит для:
❌ Не подходит для:
A = np.array([0.5, 0.3, 0.2])
B = np.array([0.48, 0.32, 0.21])
# Скалярное произведение
dot = np.dot(A, B) # 0.38
# Для нормализованных векторов
from sklearn.preprocessing import normalize
A_norm = normalize(A.reshape(1, -1))[0]
B_norm = normalize(B.reshape(1, -1))[0]
dot_normalized = np.dot(A_norm, B_norm) # 0.998 (≈ cosine)Напишите тест для сравнения:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from scipy.spatial import distance
def compare_metrics(A, B, name=""):
"""Сравнение метрик для двух векторов."""
# Косинусное сходство
cosine = cosine_similarity([A], [B])[0][0]
# Евклидово расстояние
euclidean = distance.euclidean(A, B)
# Скалярное произведение
dot = np.dot(A, B)
print(f"\n{name}")
print(f"Вектор A: {A}")
print(f"Вектор B: {B}")
print(f" Cosine Similarity: {cosine:.4f}")
print(f" Euclidean Distance: {euclidean:.4f}")
print(f" Dot Product: {dot:.4f}")
# Пример 1: Похожие векторы
A = np.array([0.5, 0.3, 0.2])
B = np.array([0.48, 0.32, 0.21])
compare_metrics(A, B, "Пример 1: Похожие векторы")
# Пример 2: Векторы разной длины, но одного направления
C = np.array([0.5, 0.3, 0.2])
D = np.array([1.0, 0.6, 0.4]) # В 2 раза длиннее
compare_metrics(C, D, "Пример 2:Same направление, разная длина")
# Пример 3: Противоположные векторы
E = np.array([1.0, 0.0, 0.0])
F = np.array([-1.0, 0.0, 0.0])
compare_metrics(E, F, "Пример 3: Противоположные векторы")Вывод:
Пример 1: Похожие векторы
Вектор A: [0.5 0.3 0.2]
Вектор B: [0.48 0.32 0.21]
Cosine Similarity: 0.9978
Euclidean Distance: 0.0374
Dot Product: 0.3780
Пример 2: Same направление, разная длина
Вектор C: [0.5 0.3 0.2]
Вектор D: [1. 0.6 0.4]
Cosine Similarity: 1.0000 # Идентичное направление!
Euclidean Distance: 0.5385 # Дальше из-за разной длины
Dot Product: 0.7600
Пример 3: Противоположные векторы
Вектор E: [1. 0. 0.]
Вектор F: [-1. 0. 0.]
Cosine Similarity: -1.0000 # Противоположное направление
Euclidean Distance: 2.0000
Dot Product: -1.0000
# ✅ Cosine Similarity — стандарт
# Игнорирует длину документа, учитывает только направление
documents = [
"Краткая статья о Python", # Короткий
"Подробное руководство по Python с примерами" # Длинный
]
query = "программирование на Python"
# Cosine найдёт оба документа (направление схожее)
# Euclidean отдаст предпочтение короткому (меньше расстояние)# ✅ Dot Product — быстрее cosine
# После нормализации эквивалентно косинусному сходству
from sklearn.preprocessing import normalize
embeddings = model.encode(documents)
normalized = normalize(embeddings)
# FAISS с Inner Product (быстрее)
index = faiss.IndexFlatIP(dimension)
index.add(normalized)# ✅ Euclidean (L2) — K-means минимизирует L2
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=5, metric='euclidean')
kmeans.fit(embeddings)# ✅ Cosine Similarity — направление важнее величины
# Или Euclidean для точных дубликатов (расстояние = 0)
def find_duplicates(embeddings, threshold=0.95):
"""Поиск дубликатов по косинусному сходству."""
from sklearn.metrics.pairwise import cosine_similarity
sim_matrix = cosine_similarity(embeddings)
duplicates = []
n = len(embeddings)
for i in range(n):
for j in range(i + 1, n):
if sim_matrix[i, j] >= threshold:
duplicates.append((i, j, sim_matrix[i, j]))
return duplicatesВместо цикла по векторам используйте матричные операции:
import numpy as np
# 1000 векторов в базе
database = np.random.rand(1000, 384)
# 10 запросов
queries = np.random.rand(10, 384)
# ❌ Медленно: цикл
similarities = []
for query in queries:
for db_vec in database:
sim = cosine_similarity([query], [db_vec])
similarities.append(sim)
# ✅ Быстро: матричное умножение
from sklearn.preprocessing import normalize
database_norm = normalize(database)
queries_norm = normalize(queries)
# Скалярное произведение всех пар сразу
similarities_matrix = np.dot(queries_norm, database_norm.T)
# Форма: (10, 1000) — сходство для всех пар запрос-документ
# Топ-5 для каждого запроса
top_5_indices = np.argsort(similarities_matrix)[::-1][:, :5]def find_top_k(query, database, k=5):
"""
Найти k ближайших соседей для запроса.
Args:
query: Вектор запроса (384,)
database: Матрица базы данных (n, 384)
k: Количество результатов
"""
# Нормализация
query_norm = query / np.linalg.norm(query)
database_norm = database / np.linalg.norm(database, axis=1, keepdims=True)
# Косинусное сходство через скалярное произведение
similarities = np.dot(database_norm, query_norm)
# Индексы топ-k
top_k_indices = np.argsort(similarities)[::-1][:k]
return top_k_indices, similarities[top_k_indices]
# Использование
database = np.random.rand(10000, 384)
query = np.random.rand(384)
indices, scores = find_top_k(query, database, k=5)
print(f"Топ-5 индексов: {indices}")
print(f"Сходство: {scores}")def interpret_cosine(score):
"""Интерпретация косинусного сходства."""
if score >= 0.9:
return "Практически идентичные"
elif score >= 0.7:
return "Очень похожие"
elif score >= 0.5:
return "Умеренное сходство"
elif score >= 0.3:
return "Слабое сходство"
elif score >= 0:
return "Почти не связаны"
else:
return "Противоположные"
# Примеры
scores = [0.95, 0.82, 0.58, 0.35, 0.12, -0.25]
for score in scores:
print(f"{score:.2f} → {interpret_cosine(score)}")Вывод:
0.95 → Практически идентичные
0.82 → Очень похожие
0.58 → Умеренное сходство
0.35 → Слабое сходство
0.12 → Почти не связаны
-0.25 → Противоположные
import matplotlib.pyplot as plt
def visualize_similarity_distribution(embeddings, sample_size=1000):
"""Визуализация распределения косинусного сходства."""
from sklearn.metrics.pairwise import cosine_similarity
# Случайная выборка
indices = np.random.choice(len(embeddings), sample_size, replace=False)
sample = embeddings[indices]
# Матрица сходства
sim_matrix = cosine_similarity(sample)
# Берём только верхний треугольник (без диагонали)
upper_tri = sim_matrix[np.triu_indices(sample_size, k=1)]
# Гистограмма
plt.figure(figsize=(10, 6))
plt.hist(upper_tri, bins=50, alpha=0.7, edgecolor='black')
plt.axvline(x=0.7, color='r', linestyle='--', label='0.7 (очень похожие)')
plt.axvline(x=0.5, color='y', linestyle='--', label='0.5 (умеренное сходство)')
plt.xlabel('Cosine Similarity')
plt.ylabel('Количество пар')
plt.title('Распределение косинусного сходства')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Использование
embeddings = model.encode(documents)
visualize_similarity_distribution(embeddings)# ❌ Проблема: длинные документы будут дальше от коротких
# даже при одинаковом содержании
long_doc = model.encode(["текст"] * 100) # Длинный документ
short_doc = model.encode(["текст"]) # Короткий
distance_l2 = euclidean(long_doc, short_doc) # Большое расстояние!
# ✅ Решение: используйте cosine similarity
from sklearn.preprocessing import normalize
long_norm = normalize(long_doc)
short_norm = normalize(short_doc)
similarity = cosine_similarity(long_norm, short_norm) # Близко к 1# ❌ Нельзя сравнивать значения разных метрик напрямую
cosine_sim = 0.8 # Это хорошо?
l2_distance = 1.5 # Это далеко?
# ✅ Интерпретируйте в контексте метрики
# Cosine 0.8 = очень похожие (диапазон -1 до 1)
# L2 1.5 зависит от масштабирования данных# ❌ Dot product без нормализации зависит от масштаба
A = np.array([10, 10, 10])
B = np.array([1, 1, 1])
dot = np.dot(A, B) # 30 — большое, но не значит "похожие"
# ✅ Нормализуйте перед dot product
A_norm = A / np.linalg.norm(A)
B_norm = B / np.linalg.norm(B)
dot_norm = np.dot(A_norm, B_norm) # 1.0 — одинаковое направление# ❌ Использование Euclidean для текстов разной длины
documents = ["краткий текст", "очень длинный текст с тем же содержанием"]
embeddings = model.encode(documents)
# Euclidean покажет большое расстояние из-за разной длины
distance = euclidean(embeddings[0], embeddings[1])
# ✅ Cosine игнорирует длину
from sklearn.preprocessing import normalize
normalized = normalize(embeddings)
similarity = cosine_similarity(normalized) # Близко к 1Создайте скрипт для тестирования метрик на ваших данных:
# metric_tester.py
import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from scipy.spatial import distance
from sklearn.preprocessing import normalize
class MetricTester:
"""Тестирование метрик сходства."""
def __init__(self, model_name='all-MiniLM-L6-v2'):
self.model = SentenceTransformer(model_name)
def encode(self, texts):
"""Генерация нормализованных эмбеддингов."""
embeddings = self.model.encode(texts)
return normalize(embeddings)
def compare_pair(self, text1, text2):
"""Сравнение двух текстов всеми метриками."""
embeddings = self.encode([text1, text2])
cosine = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
euclidean = distance.euclidean(embeddings[0], embeddings[1])
dot = np.dot(embeddings[0], embeddings[1])
print(f"\nТекст 1: {text1}")
print(f"Текст 2: {text2}")
print(f" Cosine Similarity: {cosine:.4f}")
print(f" Euclidean Distance: {euclidean:.4f}")
print(f" Dot Product: {dot:.4f}")
return {
'cosine': cosine,
'euclidean': euclidean,
'dot': dot
}
def test_dataset(self, texts, labels):
"""Тестирование на наборе данных с метками."""
embeddings = self.encode(texts)
# Матрица сходства
sim_matrix = cosine_similarity(embeddings)
# Среднее сходство внутри классов и между классами
within_class = []
between_class = []
n = len(texts)
for i in range(n):
for j in range(i + 1, n):
if labels[i] == labels[j]:
within_class.append(sim_matrix[i, j])
else:
between_class.append(sim_matrix[i, j])
print(f"\nСреднее сходство внутри классов: {np.mean(within_class):.3f}")
print(f"Среднее сходство между классами: {np.mean(between_class):.3f}")
print(f"Разделение: {np.mean(within_class) - np.mean(between_class):.3f}")
# Использование
if __name__ == "__main__":
tester = MetricTester()
# Тест пар
tester.compare_pair("кот сидит на коврике", "кошка лежит на подстилке")
tester.compare_pair("Python программирование", "веб-разработка")
# Тест набора данных
texts = [
"кот кошка питомец",
"собака пёс питомец",
"автомобиль машина транспорт",
"мотоцикл байк транспорт"
]
labels = [0, 0, 1, 1] # 0 = животные, 1 = транспорт
tester.test_dataset(texts, labels)Теперь вы понимаете метрики сходства. Следующий шаг — FAISS: основы:
metric_comparison.py с тестером метрикГотовы? Открывайте faiss_basics!
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.