Реализация SVD на Python с рандомизированной линейной алгеброй

Матричная декомпозиция - мощный инструмент для решения многих задач машинного обучения, который широко используется для сжатия данных, уменьшения размерности и обучения разреженности, и это лишь некоторые из них. Во многих случаях для аппроксимации матрицы данных структурой низкого ранга оптимальным выбором часто считается разложение по сингулярным значениям (SVD). Однако точный и эффективный SVD для больших матриц данных (например, матрицы размером 8k на 10k) является сложной вычислительной задачей. Чтобы разрешить SVD в этой ситуации, было разработано множество алгоритмов с применением методов рандомизированной линейной алгебры. Одним из наиболее важных алгоритмов является рандомизированный SVD, который с точки зрения конкуренции эффективен для разложения любой большой матрицы с относительно низким рангом.

Этот пост представит предварительную и важную идею рандомизированной SVD. Чтобы помочь читателям лучше понять рандомизированный SVD, мы также предоставляем соответствующую реализацию Python в этом посте. Кроме того, здесь можно найти блокнот Jupyter этого поста.

Предварительный

Формула SVD

Начнем с напоминания о концепции СВД. Как вы, возможно, уже знаете, SVD - одна из самых важных формул разложения в линейной алгебре. Для любой данной матрицы A SVD имеет вид

A = UΣV^T

где матрицы U и V состоят из левых и правых сингулярных векторов соответственно. Диагональные элементы Σ являются сингулярными значениями.

Пример небольшой матрицы

Возьмем, к примеру, матрицу 3 на 3, мы можем вычислить SVD, используя numpy.linalg.svd() в Python. Давайте посмотрим:

import numpy as np
A = np.array([[1, 3, 2],
              [5, 3, 1],
              [3, 4, 5]])
u, s, v = np.linalg.svd(A, full_matrices = 0)
print('Left singular vectors:')
print(u)
print()
print('Singular values:')
print(s)
print()
print('Right singular vectors:')
print(v)
print()

В этом случае сингулярные значения равны 9,3427, 3,2450 и 1,0885.

Left singular vectors:
[[-0.37421754 0.28475648 -0.88253894]
 [-0.56470638 -0.82485997 -0.02669705]
 [-0.7355732 0.48838486 0.46948087]]
Singular values:
[9.34265841 3.24497827 1.08850813]
Right singular vectors:
[[-0.57847229 -0.61642675 -0.53421706]
 [-0.73171177 0.10269066 0.67383419]
 [ 0.36051032 -0.78068732 0.51045041]]

Рандомизированный СВД

Основная идея

Рандомизированный SVD можно разбить на три основных этапа. Для любой заданной матрицы m -by- n A, если мы наложим целевой ранг k с k ‹min (m, n), тогда первый шаг, как показано на рисунке 2, -

  • 1) сгенерировать гауссову матрицу случайных чисел Ω размером n- на -k,
  • 2) вычислить новую матрицу m на k Y,
  • и 3) применить QR-разложение к матрице Y.

Обратите внимание, что первый шаг должен вернуть матрицу m -by- k Q.

Затем второй шаг, показанный на рисунке 3, - это

  • 4) выведите матрицу k -by- n B путем умножения транспонированной матрицы Q и матрица A вместе,
  • и 5) вычислить SVD матрицы B. Здесь вместо вычисления SVD исходной матрицы A, B это меньшая матрица для работы.

Поскольку сингулярные значения (т. Е. Σ) и правые сингулярные векторы (т. Е. V) матрицы B также являются сингулярными значениями и правыми сингулярными векторами исходной матрицы A, мы должны сохранить вычисленные сингулярные значения и правые сингулярные векторы. матрицей B на этом этапе.

Как показано на рисунке 3, если мы объединим матрицу Q, полученную на первом этапе, с левыми сингулярными векторами B , мы можем получить левые сингулярные векторы (т. е. U) матрицы A в третьем шаг.

Пример небольшой матрицы

Несмотря на то, что мы узнали основную идею рандомизированного SVD выше, было бы не совсем понятно, если бы не было интуитивно понятного примера. С этой целью мы следуем вышеупомянутой небольшой матрице SVD.

Во-первых, давайте попробуем написать Python-функцию рандомизированного SVD. Здесь мы будем использовать две функции Numpy, то есть np.linalg.qr() и np.linalg.svd().

import numpy as np
def rsvd(A, Omega):
    Y = A @ Omega
    Q, _ = np.linalg.qr(Y)
    B = Q.T @ A
    u_tilde, s, v = np.linalg.svd(B, full_matrices = 0)
    u = Q @ u_tilde
    return u, s, v

Теперь давайте протестируем его с помощью матрицы 3 на 3 (rank = 2 для обозначения k с помощью k ‹min (m, n )):

np.random.seed(1000)
A = np.array([[1, 3, 2],
              [5, 3, 1],
              [3, 4, 5]])
rank = 2
Omega = np.random.randn(A.shape[1], rank)
u, s, v = rsvd(A, Omega)
print('Left singular vectors:')
print(u)
print()
print('Singular values:')
print(s)
print()
print('Right singular vectors:')
print(v)
print()

Результатом этого рандомизированного примера SVD является:

Left singular vectors:
[[ 0.38070859  0.60505354]
 [ 0.56830191 -0.74963644]
 [ 0.72944767  0.26824507]]
Singular values:
[9.34224023 3.02039888]
Right singular vectors:
[[ 0.57915029  0.61707064  0.53273704]
 [-0.77420021  0.21163814  0.59650929]]

Напомним, что сингулярные значения этой матрицы - 9,3427, 3,2450 и 1,0885. В этом случае рандомизированный SVD имеет первые два сингулярных значения: 9.3422 и 3.0204.

Мы видим, что первые сингулярные значения, вычисленные этими двумя алгоритмами SVD, очень близки. Однако второе сингулярное значение рандомизированного SVD имеет небольшую погрешность. Есть ли другой способ улучшить этот результат? И как?

Ответ положительный!

Рандомизированный SVD с степенной итерацией

Для повышения качества рандомизированного SVD можно напрямую использовать метод степенных итераций. Для получения более подробной информации об итерации мощности см. Стр. 39 в [1], а также реализацию Matlab на стр. 40.

В следующих кодах Python power_iteration() - это функция для итеративного вычисления матрицы m на k Y ( значение по умолчанию power_iter равно 3), а затем получить матрицу m на k Q с помощью QR-разложения.

import numpy as np
def power_iteration(A, Omega, power_iter = 3):
    Y = A @ Omega
    for q in range(power_iter):
        Y = A @ (A.T @ Y)
    Q, _ = np.linalg.qr(Y)
    return Q
def rsvd(A, Omega):
    Q = power_iteration(A, Omega)
    B = Q.T @ A
    u_tilde, s, v = np.linalg.svd(B, full_matrices = 0)
    u = Q @ u_tilde
    return u, s, v

Давайте протестируем нашу новую функцию rsvd():

np.random.seed(1000)
A = np.array([[1, 3, 2],
              [5, 3, 1],
              [3, 4, 5]])
rank = 2
Omega = np.random.randn(A.shape[1], rank)
u, s, v = rsvd(A, Omega)
print('Left singular vectors:')
print(u)
print()
print('Singular values:')
print(s)
print()
print('Right singular vectors:')
print(v)
print()

Результат:

Left singular vectors:
[[ 0.37421757  0.28528579]
 [ 0.56470638 -0.82484381]
 [ 0.73557319  0.48810317]]
Singular values:
[9.34265841 3.24497775]
Right singular vectors:
[[ 0.57847229  0.61642675  0.53421706]
 [-0.73178429  0.10284774  0.67373147]]

Напомним, что:

  • Особые значения SVD: 9,3427, 3,2450 и 1,0885.
  • Особые значения рандомизированного SVD без степенной итерации: 9.3422 и 3.0204.
  • Особые значения рандомизированного SVD с степенной итерацией: 9.3427 и 3,2450.

Как видите, рандомизированный SVD с степенной итерацией обеспечивает чрезвычайно точные сингулярные значения.

Сжатие изображения

Как упомянуто выше, можно сжать (низкоранговую) матрицу сигналов с помощью SVD или рандомизированного SVD. Фактически, способ сжатия изображения с помощью SVD довольно прост: взять SVD изображения напрямую и сохранить только доминирующие сингулярные значения и левый / правый сингулярные векторы. В терминах рандомизированного SVD мы можем сначала предопределить количество доминирующих сингулярных значений, а затем получить сингулярные значения и левые / правые сингулярные векторы с помощью рандомизированного SVD.

Для нашей оценки мы выбрали цветное изображение Лены в качестве наших данных. Размер этого изображения 256 × 256 × 3. Здесь мы строим матрицу A размером 256 × 256, выбирая только зеленый канал.

  • Использование SVD напрямую

  • Вместо этого используется рандомизированный SVD

Из этого эксперимента по сжатию изображений мы видим, что:

  • 1) По сравнению с SVD, рандомизированный SVD также может производить точное сжатие с заданным низким рангом (здесь мы устанавливаем rank = 50).
  • 2) Рандомизированный SVD удобен для вычислений. При указании rank = 50 общее время ЦП рандомизированного SVD составляет около 11,6 мс, а общее время ЦП SVD составляет 31,5 мс.

Резюме

В этом посте вы открыли для себя метод рандомизированной линейной алгебры для SVD. В частности, вы узнали:

  • Основная идея рандомизированной СВД.
  • Как реализовать рандомизированный SVD в Python.

У вас есть вопросы?

использованная литература

[1] Стивен Л. Брантон, Дж. Натан Куц (2019). Наука и инженерия, управляемые данными: машинное обучение, динамические системы и управление. Стр. 37–41.

[2] Н. Бенджамин Эрихсон, Сергей Воронин, Стивен Л. Брантон, Дж. Натан Куц (2016). Рандомизированное разложение матриц с использованием R. arXiv: 1608.02148. [PDF]