Skip to content

Instantly share code, notes, and snippets.

@vjache
Created May 21, 2025 14:32
Show Gist options
  • Save vjache/515e44d107be006d302a30c7acb20822 to your computer and use it in GitHub Desktop.
Save vjache/515e44d107be006d302a30c7acb20822 to your computer and use it in GitHub Desktop.

Вопрос

Скажем у меня есть большой набор документов представленных векторами, например эмбеддингами. Я хочу применить к ним SVD преобразование для уменьшения размерности, а именно, выбора нового базиса состоящего из меньшего количества векторов. Этот базис я планирую использовать потом для новых приходящих векторов и их представлениея в этом новом базисе, и таким образом получая вектор меньшей размерности чем тот что "пришел". Как мне это сделать?

Ответ

Да, это классический подход, известный как Анализ Главных Компонент (Principal Component Analysis, PCA), где SVD является одним из основных методов его вычисления.

Вот пошаговая инструкция, как это сделать:

Шаг 1: Обучение (построение базиса на основе имеющихся документов)

  1. Подготовка данных:

    • У вас есть матрица X размером N x D, где N – количество документов, D – исходная размерность эмбеддингов.
    • (Опционально, но рекомендуется для PCA) Центрируйте данные: вычтите из каждого признака (каждого столбца матрицы X) его среднее значение. Сохраните эти средние значения, они понадобятся для новых данных. X_centered = X - mean(X, axis=0) data_mean = mean(X, axis=0) (вектор длины D)
  2. Применение SVD:

    • Выполните сингулярное разложение центрированной матрицы X_centered (или X, если не центрировали): U, S, V_T = svd(X_centered) Где:
      • U: левые сингулярные векторы (размер N x N или N x D в зависимости от реализации "economy SVD").
      • S: одномерный массив сингулярных значений (упорядочены по убыванию).
      • V_T (V транспонированное): правые сингулярные векторы (размер D x D). Строки V_T (или столбцы V) являются главными компонентами – это и есть ваш новый базис.
  3. Выбор количества компонент (новой размерности k):

    • Определите, сколько главных компонент вы хотите оставить (k < D). Это можно сделать несколькими способами:
      • Задать k заранее (например, "хочу уменьшить до 100 измерений").
      • Выбрать k так, чтобы сохранить определенный процент дисперсии (например, 95%). Дисперсия, объясняемая каждой компонентой, пропорциональна квадрату соответствующего сингулярного значения. explained_variance_ratio = (S^2) / sum(S^2) cumulative_explained_variance = cumsum(explained_variance_ratio) Найдите k такое, что cumulative_explained_variance[k-1] >= желаемого порога (например, 0.95).
  4. Формирование матрицы проекции (нового базиса):

    • Возьмите первые k строк из V_T (или первые k столбцов из V). Эта матрица W будет иметь размер k x D (если берем строки V_T) или D x k (если берем столбцы V). Чаще используют V и берут столбцы, тогда матрица проекции W = V[:, :k] будет иметь размер D x k.
    • Важно: W – это набор из k ортогональных векторов, каждый длиной D. Эти векторы определяют направления максимальной дисперсии в ваших исходных данных.

Шаг 2: Применение к новым векторам (получение представлений меньшей размерности)

  1. Поступление нового вектора:

    • У вас есть новый вектор x_new (размер 1 x D или просто вектор длины D).
  2. (Опционально, но обязательно, если центрировали на Шаге 1) Центрирование нового вектора:

    • Вычтите из x_new те же самые средние значения, которые были вычислены на тренировочном наборе: x_new_centered = x_new - data_mean
  3. Проекция на новый базис:

    • Умножьте центрированный (или исходный, если не центрировали) новый вектор на матрицу проекции W: x_reduced = x_new_centered @ W (матричное умножение) Или, если x_new_centered – это вектор-строка (1 x D), а W – это D x k: x_reduced (размер 1 x k) = x_new_centered @ W Если x_new_centered – это вектор-столбец (D x 1), а W – это D x k, то проекция будет W^T @ x_new_centered (размер k x 1). Обычно работают с векторами-строками для данных.

    • x_reduced – это и есть ваш новый вектор меньшей размерности k. Он представляет собой координаты исходного вектора в новом базисе, образованном главными компонентами.

Пример на Python с NumPy:

import numpy as np

# Шаг 1: Обучение (построение базиса)

# Допустим, у нас есть 1000 документов, каждый представлен вектором из 512 чисел
N_docs = 1000
D_original = 512
X = np.random.rand(N_docs, D_original) # Исходные эмбеддинги

# 1. Центрирование данных
data_mean = np.mean(X, axis=0)
X_centered = X - data_mean

# 2. Применение SVD
U, S_diag, V_T = np.linalg.svd(X_centered, full_matrices=False) # full_matrices=False для экономии
# S_diag - это одномерный массив сингулярных чисел.
# V_T - это уже V транспонированное, его строки - главные компоненты (собственные векторы)

# 3. Выбор новой размерности k (например, 50)
k = 50

# 4. Формирование матрицы проекции W
# V_T имеет размерность (D_original, D_original) или (min(N_docs, D_original), D_original)
# если full_matrices=False и N_docs < D_original.
# Нам нужны первые k строк V_T, но для проекции вектора-строки удобнее иметь V[:, :k]
# V_T.shape будет (D_original, D_original), если N_docs >= D_original,
# или (N_docs, D_original) если N_docs < D_original.
# V = V_T.T
# W = V[:, :k] # Матрица проекции D_original x k
# Альтернативно, если мы хотим умножать x_new_centered (1 x D) @ W (D x k)
W = V_T.T[:, :k] # V_T.T это V. Берем первые k столбцов V. Размер W: (D_original, k)

# Можно также спроецировать исходные данные для проверки:
X_reduced_train = X_centered @ W # (N_docs, k)

print(f"Исходная размерность: {D_original}")
print(f"Новая размерность: {k}")
print(f"Форма матрицы проекции W: {W.shape}")
print(f"Форма уменьшенных тренировочных данных: {X_reduced_train.shape}")


# Шаг 2: Применение к новому вектору

# Допустим, пришел новый вектор
x_new = np.random.rand(D_original) # вектор длины D_original

# 1. Центрирование нового вектора (используя СРЕДНЕЕ ОТ ТРЕНИРОВОЧНЫХ ДАННЫХ)
x_new_centered = x_new - data_mean

# 2. Проекция на новый базис
# x_new_centered (1 x D_original) @ W (D_original x k) -> (1 x k)
x_new_reduced = x_new_centered @ W

print(f"\nНовый вектор (оригинальный, первые 5 элементов): {x_new[:5]}")
print(f"Новый вектор (центрированный, первые 5 элементов): {x_new_centered[:5]}")
print(f"Новый вектор (уменьшенная размерность, первые 5 элементов): {x_new_reduced[:5]}")
print(f"Форма уменьшенного нового вектора: {x_new_reduced.shape}")

# Восстановление (для понимания потерь)
# x_reconstructed_centered = x_new_reduced @ W.T
# x_reconstructed = x_reconstructed_centered + data_mean
# print(f"Восстановленный вектор (первые 5 элементов): {x_reconstructed[:5]}")
# print(f"Ошибка восстановления (норма): {np.linalg.norm(x_new - x_reconstructed)}")

Что нужно сохранить после "обучения":

  1. data_mean: Вектор средних значений признаков из обучающего набора (длиной D).
  2. W: Матрица проекции (размером D x k).

Когда приходит новый вектор, вы сначала вычитаете из него data_mean, а затем умножаете на W.

Использование Scikit-learn:

Библиотека scikit-learn имеет готовую реализацию PCA, которая делает все это "под капотом":

from sklearn.decomposition import PCA
import numpy as np

# Исходные данные
N_docs = 1000
D_original = 512
X = np.random.rand(N_docs, D_original)

# Новая размерность
k = 50

# Обучение PCA
pca_model = PCA(n_components=k)
pca_model.fit(X) # PCA в scikit-learn сама выполняет центрирование

# Матрица проекции W доступна как pca_model.components_.T
# pca_model.components_ это аналог V_T[:k, :] (k x D_original)
# W = pca_model.components_.T (D_original x k)

# Средние значения доступны как pca_model.mean_

# Проекция тренировочных данных
X_reduced_train_sklearn = pca_model.transform(X)

print(f"Форма уменьшенных тренировочных данных (sklearn): {X_reduced_train_sklearn.shape}")

# Применение к новому вектору
x_new_sklearn = np.random.rand(D_original)

# Scikit-learn требует, чтобы вход был 2D, даже для одного вектора
x_new_sklearn_2d = x_new_sklearn.reshape(1, -1)
x_new_reduced_sklearn = pca_model.transform(x_new_sklearn_2d) # transform сам центрирует
x_new_reduced_sklearn = x_new_reduced_sklearn.flatten() # обратно в 1D, если нужно

print(f"\nУменьшенный новый вектор (sklearn, первые 5 элементов): {x_new_reduced_sklearn[:5]}")
print(f"Форма уменьшенного нового вектора (sklearn): {x_new_reduced_sklearn.shape}")

# Если нужно посмотреть, что внутри pca_model:
# print("pca_model.mean_:", pca_model.mean_[:5]) # сохраненные средние
# print("pca_model.components_ (V_T[:k,:]) shape:", pca_model.components_.shape) # сохраненные главные компоненты (строки)

Использование PCA из scikit-learn предпочтительнее для большинства практических задач, так как оно оптимизировано и менее подвержено ошибкам в реализации. Суть остается той же: SVD используется для нахождения главных компонент.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment