ЛР4
Факторизация: большую таблицу сжимаем двумя маленькими

Разобраться с нуля

Факторизация: большую таблицу сжимаем двумя маленькими

Две маленькие матрицы должны при умножении восстановить исходную с малой ошибкой.

Матрица A
Исходная таблица чисел, m строк и n столбцов.
Ранг k
Число скрытых координат, размер сжатия.
U и V_T
Два множителя размера m×k и k×n.
MSE
Среднее квадратов разностей между исходной и восстановленной таблицей.
Градиент
Производная ошибки: направление её роста.
Шаг learning_rate
Насколько сильно меняем параметры за раз.
Эпоха
Один шаг вычисления градиентов и обновления множителей.
На пальцах. Запоминаем не каждую оценку человека каждому фильму, а несколько вкусов человека и несколько свойств фильма. Их сочетание приближает исходную таблицу.

Размерности без магии

A имеет размер 2×3, k=2. U это 2×2, V_T это 2×3, произведение снова 2×3. Ошибка E = U @ V_T - A такой же формы.

L = mean(E²)
grad_U = 2 E @ V_T.T / A.size
grad_V = 2 U.T @ E / A.size
U = U - learning_rate × grad_U

@ это матричное умножение. .T меняет строки и столбцы местами. A.size это m×n. Оба градиента считаются до изменения U и V_T.

Мини-версия: подобрать x так, чтобы x≈3. Ошибка L=(x-3)², градиент 2(x-3).

Ловушка. Произвольные U и V_T из градиентного спуска не обязаны быть сингулярными ортогональными векторами. Название функции svd_gradient_descent взято из шаблона, точнее говорить о низкоранговой факторизации. Для второй тестовой матрицы 1000 эпох мало, поэтому дополнительно показаны 10000.

Код и построчный разбор

Код исходной ячейки 46: def svd_gradient_descent(A, k, learning_rate=0.001, epochs=100):
def svd_gradient_descent(A, k, learning_rate=0.001, epochs=100):
    A = np.asarray(A, dtype=float)
    m, n = A.shape
    rng = np.random.RandomState(42)
    U = rng.randn(m, k)
    V_T = rng.randn(k, n)
    for epoch in range(epochs):
        error = U @ V_T - A
        grad_U = 2 * error @ V_T.T / A.size
        grad_V = 2 * U.T @ error / A.size
        U -= learning_rate * grad_U
        V_T -= learning_rate * grad_V
    return U, V_T
  1. 1: def svd_gradient_descent(A, k, learning_rate=0.001, epochs=100):
    Объявляет функцию: код внутри выполняется при её вызове.
  2. 2: A = np.asarray(A, dtype=float)
    Превращает результат вычисления в массив NumPy; ravel делает его одномерным.
  3. 3: m, n = A.shape
    Вычисляет правую часть и сохраняет результат в m, n.
  4. 4: rng = np.random.RandomState(42)
    Вычисляет правую часть и сохраняет результат в rng.
  5. 5: U = rng.randn(m, k)
    Вычисляет правую часть и сохраняет результат в U.
  6. 6: V_T = rng.randn(k, n)
    Вычисляет правую часть и сохраняет результат в V_T.
  7. 7: for epoch in range(epochs):
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  8. 8: error = U @ V_T - A
    Разность восстановленной и исходной матрицы: UV_T - A.
  9. 9: grad_U = 2 * error @ V_T.T / A.size
    Градиент MSE по U, форма m×k; вычисляется матричной формулой.
  10. 10: grad_V = 2 * U.T @ error / A.size
    Градиент MSE по V_T, форма k×n; используется старая U.
  11. 11: U -= learning_rate * grad_U
    Сдвигает U против градиента на величину learning_rate.
  12. 12: V_T -= learning_rate * grad_V
    Сдвигает V_T против своего заранее вычисленного градиента.
  13. 13: return U, V_T
    Возвращает результат функции вызывающему коду.
Код исходной ячейки 47: # Примеры - значения матрицы Result должны приближаться к значениям исходной матрицам
# Примеры - значения матрицы Result должны приближаться к значениям исходной матрицам

print('-' * 50)

A = np.array([[3, 1, 1],
              [-1, 3, 1]])

U, V_T = svd_gradient_descent(A, k=2, learning_rate=0.1)
print("U:\n", U)
print("V_T:\n", V_T)
print("Result:\n", U @ V_T)

print('-' * 50)

A2 = np.array([[1, 2, 3],
               [4, 5, 6],
               [7, 8, 9]])

U2, V_T2 = svd_gradient_descent(A2, k=2, learning_rate=0.02, epochs=1000)
print("U2:\n", U2)
print("V_T2:\n", V_T2)
print("Result2:\n", U2 @ V_T2)

print('-' * 50)
  1. 1: # Примеры - значения матрицы Result должны приближаться к значениям исходной матрицам
    Комментарий объясняет причину следующего действия.
  2. 3: print('-' * 50)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  3. 5: A = np.array([[3, 1, 1],
    Вычисляет правую часть и сохраняет результат в A.
  4. 6: [-1, 3, 1]])
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  5. 8: U, V_T = svd_gradient_descent(A, k=2, learning_rate=0.1)
    Вычисляет правую часть и сохраняет результат в U, V_T.
  6. 9: print("U:\n", U)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  7. 10: print("V_T:\n", V_T)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  8. 11: print("Result:\n", U @ V_T)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  9. 13: print('-' * 50)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  10. 15: A2 = np.array([[1, 2, 3],
    Вычисляет правую часть и сохраняет результат в A2.
  11. 16: [4, 5, 6],
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  12. 17: [7, 8, 9]])
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  13. 19: U2, V_T2 = svd_gradient_descent(A2, k=2, learning_rate=0.02, epochs=1000)
    Вычисляет правую часть и сохраняет результат в U2, V_T2.
  14. 20: print("U2:\n", U2)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  15. 21: print("V_T2:\n", V_T2)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  16. 22: print("Result2:\n", U2 @ V_T2)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  17. 24: print('-' * 50)
    Печатает число или пояснение, чтобы проверить результат выполнения.

Вопросы защиты

Почему нет цикла по элементам?

Ошибка и оба градиента выражены матричными формулами NumPy. Цикл остаётся только по эпохам.

Почему минус перед градиентом?

Градиент показывает рост ошибки, а мы хотим идти в сторону уменьшения.

Можно сравнивать элементы U с чужим запуском?

Нет: разные множители могут давать одно произведение. Сравнивают реконструкцию и MSE.

Что если шаг слишком большой?

Ошибка может колебаться или расти; это видно в демо.

Одна фразаУменьшаю MSE восстановления A матричными градиентами, цикл только по эпохам.