Скажем у меня есть большой набор документов представленных векторами, например эмбеддингами. Я хочу применить к ним SVD преобразование для уменьшения размерности, а именно, выбора нового базиса состоящего из меньшего количества векторов. Этот базис я планирую использовать потом для новых приходящих векторов и их представлениея в этом новом базисе, и таким образом получая вектор меньшей размерности чем тот что "пришел". Как мне это сделать?
Да, это классический подход, известный как Анализ Главных Компонент (Principal Component Analysis, PCA), где SVD является одним из основных методов его вычисления.
Вот пошаговая инструкция, как это сделать:
Шаг 1: Обучение (построение базиса на основе имеющихся документов)
-
Подготовка данных:
- У вас есть матрица
X
размеромN x D
, гдеN
– количество документов,D
– исходная размерность эмбеддингов. - (Опционально, но рекомендуется для PCA) Центрируйте данные: вычтите из каждого признака (каждого столбца матрицы
X
) его среднее значение. Сохраните эти средние значения, они понадобятся для новых данных.X_centered = X - mean(X, axis=0)
data_mean = mean(X, axis=0)
(вектор длиныD
)
- У вас есть матрица
-
Применение 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
) являются главными компонентами – это и есть ваш новый базис.
- Выполните сингулярное разложение центрированной матрицы
-
Выбор количества компонент (новой размерности
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).
- Задать
- Определите, сколько главных компонент вы хотите оставить (
-
Формирование матрицы проекции (нового базиса):
- Возьмите первые
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: Применение к новым векторам (получение представлений меньшей размерности)
-
Поступление нового вектора:
- У вас есть новый вектор
x_new
(размер1 x D
или просто вектор длиныD
).
- У вас есть новый вектор
-
(Опционально, но обязательно, если центрировали на Шаге 1) Центрирование нового вектора:
- Вычтите из
x_new
те же самые средние значения, которые были вычислены на тренировочном наборе:x_new_centered = x_new - data_mean
- Вычтите из
-
Проекция на новый базис:
-
Умножьте центрированный (или исходный, если не центрировали) новый вектор на матрицу проекции
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)}")
Что нужно сохранить после "обучения":
data_mean
: Вектор средних значений признаков из обучающего набора (длинойD
).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 используется для нахождения главных компонент.