Перейти к содержанию

Vector Math — Полный код

Уровень: L3 (deep-dive) | ← Назад к Embeddings

Файл: frontend/src/embeddings/vectorMath.ts

Математические операции над векторами для similarity search.

/**
 * Cosine Similarity между двумя векторами.
 * 
 * Формула:
 *   cos(θ) = (A · B) / (||A|| × ||B||)
 * 
 * Где:
 *   A · B = Σ(aᵢ × bᵢ)        — скалярное произведение
 *   ||A|| = √Σ(aᵢ²)            — евклидова норма
 * 
 * Возвращает значение в диапазоне [-1, 1]:
 *   1  = идентичные направления (максимальное сходство)
 *   0  = ортогональные (нет сходства)
 *  -1  = противоположные направления
 * 
 * Для ResNet50 embeddings значения обычно в диапазоне [0.3, 0.99]
 */
export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
  if (a.length !== b.length || a.length === 0) return 0;

  let dot = 0;     // Скалярное произведение
  let normA = 0;   // Квадрат нормы A
  let normB = 0;   // Квадрат нормы B

  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }

  const denom = Math.sqrt(normA) * Math.sqrt(normB);
  if (!isFinite(dot) || denom === 0) return 0;  // Защита от деления на ноль
  return dot / denom;
}

/**
 * Средний вектор (centroid) из набора векторов.
 * 
 * Используется для вычисления embedding образа:
 *   outfit_embedding = mean([item1_emb, item2_emb, ...])
 * 
 * Формула:
 *   mean[i] = (1/N) × Σ(vectors[j][i])
 * 
 * Фильтрует вектора неправильной размерности.
 */
export function meanVector(vectors: Float32Array[]): Float32Array {
  if (vectors.length === 0) return new Float32Array(0);
  const dim = vectors[0].length;
  const valid = vectors.filter((v) => v.length === dim);
  if (valid.length === 0) return new Float32Array(0);

  const result = new Float32Array(dim);

  // Суммирование по всем векторам
  for (const vec of valid) {
    for (let i = 0; i < dim; i++) {
      result[i] += vec[i];
    }
  }

  // Деление на количество
  const count = valid.length;
  for (let i = 0; i < dim; i++) {
    result[i] /= count;
  }

  return result;
}

/**
 * Поиск TopK наиболее похожих кандидатов.
 * 
 * Алгоритм:
 * 1. Вычислить cosine similarity для каждого кандидата
 * 2. Отсортировать по убыванию score
 * 3. Вернуть первые topK результатов
 * 
 * Сложность: O(N × D + N log N)
 *   N = количество кандидатов
 *   D = размерность вектора (1024)
 */
export function topKSimilar(
  query: Float32Array,
  candidates: Array<{ id: string; vec: Float32Array }>,
  topK: number
): Array<{ id: string; score: number }> {
  // Вычислить similarity для каждого кандидата
  const scored = candidates.map((c) => ({
    id: c.id,
    score: cosineSimilarity(query, c.vec),
  }));

  // Сортировка по убыванию (наиболее похожие первыми)
  scored.sort((a, b) => b.score - a.score);

  // Вернуть только topK
  return scored.slice(0, topK);
}

Математическое обоснование

Cosine Similarity vs Euclidean Distance

Метрика Формула Свойства
Cosine Similarity cos(θ) = (A·B)/(‖A‖×‖B‖) Инвариантна к масштабу вектора
Euclidean Distance ‖A-B‖ = √Σ(aᵢ-bᵢ)² Чувствительна к масштабу

Почему cosine: ResNet50 embeddings не нормализованы. Cosine similarity сравнивает направления векторов, игнорируя их длину. Это важно, т.к. вещи разных категорий могут иметь векторы разной магнитуды.

Mean Vector для образов

Интуиция: embedding образа — это "центр масс" embeddings его вещей в пространстве признаков.

Образ = {футболка, джинсы, кроссовки}
outfit_emb = (emb_футболка + emb_джинсы + emb_кроссовки) / 3

Похожие образы — те, чьи центры масс расположены близко в embedding space.

Float32Array

Использование Float32Array вместо обычного number[]: - 2× экономия памяти (32-bit vs 64-bit per element) - Быстрее в циклах (типизированный массив, no boxing) - Для 1024-dim vector: 4KB vs 8KB per embedding

Вычислительная сложность

Для гардероба из N=500 вещей, D=1024:

cosine similarity: O(D) = O(1024) ≈ 3072 операций
topK search: O(N×D + N×log(N)) = 500×1024 + 500×log(500) ≈ 516000 операций

На современном CPU в Web Worker: ~2-3ms для 500 вещей.