Как стать автором
Обновить

Рекомендательная система SVD

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров3.4K

Взаимодействие между пользователем и товаром разделят на две категории:

  • Явное взаимодействие. Например, рейтинг, который пользователь ставит товару.

  • Неявное взаимодействие. Например, клик, просмотр или покупка товара не очевидны с точки зрения рейтинга.

User-item matrix — это таблица, в которой каждая строка представляет собой пользователя, каждый столбец - товар, а каждая ячейка содержит информацию о взаимодействии между пользователем и товаром. Например, в ячейке может быть указано количество раз, которое пользователь купил товар, или оценка, которую пользователь поставил товару. User-item matrix используется в рекомендательных системах для определения предпочтений пользователей и предложения им наиболее подходящих товаров. Очевидно, что user-item matrix - очень большая и разряженная матрица.

User-item matrix 
User-item matrix 

Для таких задач применяют матричную факторизацию (представление user-item matrix в виде произведения двух или более матриц более простого вида).

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

Так дело обстоит с основным компонентным анализом (PCA) и разложение по сингулярному значению (SVD).

R_{m \times n} = U_{m \times m} \Sigma_{m \times n} V^{T}_{n \times n}

гдеRuser-item matrix, \Sigma — диагональная матрица с неотрицательными сингулярными числами, U и V — ортогональные матрицы и U^T U = I.

PCA — один из основных способов уменьшить размерность данных, потеряв наименьшее количество информации. Математическое содержание метода главных компонент — это SVD разложение матрицыR, то есть представление пространства данных в виде суммы взаимно ортогональных собственных подпространств R, а самой матрицы R — в виде линейной комбинации ортогональных проекторов на эти подпространства с коэффициентами \lambda_i.

Оставив d-первых (главных) компонент (\lambda_i) в SVD разложении, получим наиболее точное приближение к R по норме Фробениуса.

За "Скрытым текстом" пример использования SVD и PCA.

Hidden text

Наглядно как работает SVD и PCA.

import numpy as np
from matplotlib import image
from matplotlib import pyplot as plt
import cv2
img = image.imread("photo.jpg")
plt.imshow(img)
plt.show()

Для работы с меньшей размерностью перейдем к черно-белому изображению.

gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
plt.imshow(gray_img, cmap='gray')
plt.show()
gray_img.shape

(558, 563)

Применим SVD разложение

U, lmbd, V = np.linalg.svd(gray_img)
U.shape, lmbd.shape, V.shape

((558, 558), (558,), (563, 563))
S = np.zeros((U.shape[1], V.shape[0])) 
S[ : U.shape[1], : U.shape[1]] = np.diag(lmbd)
S.shape

(558, 563)

Вернем как было, убедимся что ничего не изменилось

plt.imshow(U @ S @ V, cmap='gray')
plt.show()

Попробуем оставить первые 100 компонент и посмотрим насколько изменится качество изображения.

d = 100
U_crop = U[:, :d]
S_crop = S[:d, :d]
V_crop = V[:d, :]

plt.imshow(U_crop @ S_crop @ V_crop, cmap='gray')
plt.show()

Качество картинки почти не изменилось, зато сколько сэкономлено памяти!

Попробуем оставить первые 50 компонент.

d = 50
U_crop = U[:, :d]
S_crop = S[:d, :d]
V_crop = V[:d, :]

plt.imshow(U_crop @ S_crop @ V_crop, cmap='gray')
plt.show()

Оставим первые 10 компонент.

d = 10
U_crop = U[:, :d]
S_crop = S[:d, :d]
V_crop = V[:d, :]

plt.imshow(U_crop @ S_crop @ V_crop, cmap='gray')
plt.show()

Сингулярные числа в матрице S расположены по убыванию: чем ближе к началу, тем компоненты важнее и сильнее влияют на качество изображения.

Уберем первые 5 компонент.

k = 5U_crop = U[:, k:]
S_crop = S[k:, k:]
V_crop = V[k:, :]

plt.imshow(U_crop @ S_crop @ V_crop, cmap='gray')
plt.show()

Качество картинки значительно хуже.

Представим нашу user-item matrix как произведение следующих матриц:

R_{m \times n} = P_{m \times n} Q_{n \times n}P = U_{m \times m} \Sigma_{m \times d}Q = V_{d \times n}

Тогда i‑aя строка U — это представление i‑го пользователя, а j‑ый столбец V — представленте j‑го товара. Их отношение (рейтинг) выражается с помощью скалярного произведения:

R[i][j] = \langle P[i, :], Q[:, j] \rangle

Матрицы P и Q найдем с помощью градиентного спуска.

В качестве Loss возьмем RMSE без регуляризации для простоты:

RMSE = \frac{1}{\lvert D \rvert} \sum_{(i,j) \in D} (\hat{R}_{i, j} - R_{i, j})^2 =     \frac{1}{\lvert D \rvert} \sum_{(i, j) \in D} (P_{i} Q_{j} - R_{i, j})^2

Посчитав градиенты по P и по Q, получим формулы обновления весов:

\begin{cases}     P[i][k] = P[i][k] - \frac{2}{\lvert D \rvert} \cdot \sum_{(i,j) \in D} (\hat{R}_{i,j} - R_{i,j}) \cdot Q[k][i]     \\     Q[k][i] = Q[k][i] - \frac{2}{\lvert D \rvert} \cdot \sum_{(i,j) \in D} (\hat{R}_{i,j} - R_{i,j}) \cdot P[i][k] \end{cases}

k пробегает от 1 до d, где d - число главных компонент.

Продемонстрируем работу алгоритма. Пусть нам заданы пользователи и их оценки к просмотренным фильмам. Задача - спрогнозировать незаполненные ячейки user-item matrix.

import numpy as np
import pandas as pd

Зададим пользователей и фильмы:

k = 10 # максимальная оценка

movies = ['Фантазия', 'ВАЛЛ-И', 'Пиноккио', 'Бемби' , 'Шрэк', 'Дамбо', 'Спасатели', 'Геркулес', 'Кунг-фу Панда']
m_movies = len(movies)

users = ['Андрей', 'Аня', 'Алиса', 'Ваня', 'Леша', 'Оксана', 'Саша', 'Паша', 'Сеня', 'Гриша']        
n_users = len(users)

Оценки раскидаем случайным образом. С учетом чтобы пара user-movie не повторялась

RANDOMSEED = 42
np.random.seed(42)

N = np.random.randint(50, 60) # сколько оценок будет поставлено

ind_users, ind_movies, rating = [], [], []
user_movie = [] # чтобы пара user-movie не повторялись
for _ in range(N):
    user = np.random.randint(0, n_users)
    movie = np.random.randint(0, m_movies)
    if not [user, movie] in user_movie:
        ind_users.append(user)
        ind_movies.append(movie)
        rating.append(np.random.randint(1, k)) # случайная оценка пользователя фильму
        user_movie.append([user, movie])        

N = len(user_movie)

def get_user_item_matrix(n_users, m_movies, ind_users, ind_movies, rating):
    R = [[0] * m_movies for _ in range(n_users)]
    N = len(ind_users)
    for i in range(N):
        R[ind_users[i]][ind_movies[i]] = rating[i]
    R = np.array(R)
    return R

R = get_user_item_matrix(n_users, m_movies, ind_users, ind_movies, rating)

pd.DataFrame(R, users, movies) 

Ошибку будем считать как:

def MSE(R, U, V):
    mse = 0
    for ind in range(N):
        i = ind_users[ind]
        j = ind_movies[ind]
        mse += ( R[i][j] - np.dot(U[i,:], V[:,j]) )**2 / N
    return mse

Использовать будем стохастический градиентный спуск c фиксированным шагом step. Сделаем n_iters итераций.

def SVD(ind_users, ind_movies, R, d, step, n_iters):
    
    # инициализация матриц разложения
    P = np.random.rand(R.shape[0], d)
    Q = np.random.rand(d, R.shape[1])

    start_mse = MSE(R, P, Q)
    
    # Стохастический градиентный спуск
    for n in range(n_iters):
        ind = np.random.randint(0, N)
        i = ind_users[ind]
        j = ind_movies[ind]
        
        for k in range(0, d):
            P[i, k] = P[i, k] + step * (R[i][j] - P[i, :] @ Q[:, j]) * Q[k, j] 
            Q[k, j] = Q[k, j] + step * (R[i][j] - P[i, :] @ Q[:, j]) * P[i, k] 

        mse = MSE(R, P, Q)
    return P, Q, start_mse, mse

P, Q, start_mse, mse = SVD(ind_users, ind_movies, R, 3, 0.1, 3000) 
start_mse, mse

(22.327189007469947, 0.9592817343783187)

Спрогнозируем user-item matrix

R_pred = np.zeros((R.shape[0], R.shape[1]))

for i in range(n_users):
    for j in range(m_movies):
        R_pred[i][j] = round(P[i,:] @ Q[:,j], 2)

pd.DataFrame(R_pred, users, movies)
# Вывод еще раз для сравнения
pd.DataFrame(R, users, movies)

В продолжении темы небольшая модификация алгоритма "SVD++: Funk MF (Matrix Factorization)" (как учитывать предубеждения пользователей относительно товаров).

Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии4

Публикации

Истории

Работа

Data Scientist
58 вакансий

Ближайшие события

One day offer от ВСК
Дата16 – 17 мая
Время09:00 – 18:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область