Давайте возьмем какой-то конкретный пример и посмотрим: если к нему применить SVD разложение, сможем ли мы вообще проинтерпретировать эти компоненты. Вот здесь у нас шесть пользователей, поправка — пять, и у нас несколько треков, и нам известно (каждый из этих пользователей — Артем, Вася, Маша), какие им треки музыкальные нравятся, а какие не очень. Здесь это все в пятибалльной шкале от одного до пяти. Берем эту матрицу и применяем прямо SVD разложение из scikit-learn или из NumPy — откуда угодно. Получится три вот таких матрицы: U, Сигма и V транспонированное. В U по строчкам записаны характеристики наших всех пользователей, в Сигма на диагонали — важность этих трех компонент, которые мы оставили в нашем SVD (а здесь мы взяли три компоненты, они обозначаются f1, f2, f3), и вот она есть матрица V транспонированное, в которой по столбцам, соответственно, записаны профили товаров. То есть каждый товар (здесь это музыкальный трек) имеет какие-то три характеристики в каком-то объеме — какие-то три числа. И теперь мы знаем, что рейтинг предсказывается просто как взвешенное скалярное произведение профиля юзера на профиль товара, и эти скалярные произведения мы считать не будем, а давайте лучше посмотрим, какой смысл у этих трех компонент: f1, f2 и f3 — три главные компоненты в наших данных. Какой за ними скрывается на самом деле смысл? Давайте попробуем это проинтерпретировать. Берем компоненту f1. У нее большие значения на "Highway to Hell", "Diamonds" и Guns N’ Roses — это все такие энергичные какие-то музыкальные треки. Маленькие значения у этой компоненты у "Yesterday" и "Summertime Sadness", то есть это такие заунывненькие треки, совсем не энергичные. Вроде бы так, на глазок, кажется, что компонента f1 оценивает энергичность композиции. И вот мы, глядя на эти данные, могли даже не догадаться, что это важный фактор, который помогает делать рекомендации, а модель его вычислила автоматом. Латентно мы даже ничего не задавали, что там есть какая-то энергичность у треков, ее просто можно так проинтерпретировать. И этот фактор самый важный: если вы посмотрите на матрицу Сигма, то там 22 с чем-то написано, а у остальных факторов сильно меньше, то есть он основополагающий. Грубо говоря, когда мы смотрим похожесть пользователя на товар, энергичность будет с самым большим весом давать вклад в скалярное произведение. Давайте посмотрим на компоненту f2. Здесь что мы можем заметить? Вот, например, большие значения у AC/DC и Guns N’ Roses. И то и другое, кажется, рок. The Beatles тоже вроде более или менее рок, а все остальные — какие-то маленькие значения, так что, возможно, компонента f2 описывает рок. Это все, конечно, махание руками, но мы же пытаемся интерпретировать какие-то не очень интерпретируемые вещи, поэтому кажется, что f2 — все-таки что-то, похожее на рок. Осталось объяснить компоненту f3. Вот я на нее смотрю и объяснить ее не могу: там какой-то рандом, кажется. И, скорее всего, это действительно просто рандом. У этой компоненты вес уже очень маленький, и, возможно, на нее не стоило смотреть, здесь в этих данных стоило в SVD разложение взять только две компоненты, и мы бы уже объяснили почти все те взаимодействия, которые мы наблюдаем. Давайте возьмем и оставим две компоненты и посмотрим, где у нас будут ошибочки. Вот здесь мы оставили две компоненты: матрица сверху — это исходная, матрица снизу — это предсказанная. Различия здесь только в выделенных трех ячейках, то есть вместо оценок 5, 5, 3 мы предсказали 4, 4, 4. Кажется, это уже достаточно близко, и можно на этом остановиться. Вот эта третья компонента — это уже какое-то переобучение, это какой-то шум; мы пытаемся вот эти три оценки разделить. Не стоит этим заниматься: скорее всего, две компоненты здесь уже очень классно работают. В итоге наша похожесть юзера на музыкальный трек в нашем случае это следующее: нужно понять, насколько пользователю нравятся энергичные треки и насколько трек энергичен (с каким-то большим весом), а затем посмотреть, насколько юзеру нравится рок, и насколько этот трек похож на рок — и это с каким-то меньшим весом тоже дает вклад в предсказания. Вот такой простой алгоритм разложил нам все эти данные, мы сможем для юзеров сделать рекомендации. Во всем этом было бы все понятно: просто берешь SVD разложение, применяешь — и все здорово; то самое, из линейной алгебры, и все работает. Но здесь есть подвох. И подвох здесь в том, что я вам дал матрицу, в которой нет пропусков, а это не настоящий сеттинг в этой задаче. У нас, как правило, матрица огромная, и пропусков в ней очень много, потому что нам нужно делать какие-то рекомендации. Заметьте, что здесь никаких рекомендаций нам делать не нужно: здесь все рейтинги уже известны, мы просто пытаемся их объяснить. Поэтому классический алгоритм просто матричной факторизации при помощи SVD здесь не заработает, нам нужно придумать какой-то новый. И здесь можно было бы поступить наивно. Можно было взять и вместо пропусков просто забить это все нулями и применить обычное SVD из линейной алгебры. Это не очень хороший подход на самом деле. Потому что, во-первых, мы вносим в данные (в исходные) шум, потому что вместо пропусков мы пишем нули и говорим, что эти нули очень важно предсказать. Это как-то глупо: мы же пытаемся рейтинг предсказать — от одного до пяти звездочку, а мы туда вбили нули, и алгоритм заставляем эти нули предсказывать. Как-то не очень хорошо, грязновато. Возможно, получится решение даже хуже. Есть еще один момент: когда вы забиваете эту всю матрицу нулями, она становится уже не разреженной, все эти нули нужно хранить, их нужно обрабатывать, и наше SVD разложение может работать просто очень долго. Стандартные рутины из NumPy, которые делают SVD, могут работать на такой матрице миллион на миллион, которая еще и не разрежена, очень долго. Это как-то не очень круто, поэтому предлагается придумать новый алгоритм, который будет учитывать разреженность нашей матрицы. Давайте мы будем оптимизировать точность наших предсказаний напрямую. Давайте возьмем следующую модель. Наше предсказание рейтинга юзера u айтему i (это предсказание, поэтому там крышка нарисована) будет складываться из следующих компонент: это Мю — наш какой-то глобально средний рейтинг; это bu — какой-то байес для юзера u (грубо говоря, это сдвиг в его оценках до среднего, то есть это какая-то его средняя оценка); есть еще bi — это средняя оценка айтема (некоторые айтемы в среднем очень популярны, некоторые, наоборот, не очень, и вот это здесь можно учесть); и последний компонент — это скалярное произведение двух векторов (qi — это профиль айтема, соответственно, а pu — это профиль юзера). И мы хотим, чтобы этот рейтинг юзера u айтему i объяснялся скалярным произведением их векторов в пространстве каких-то характеристик плюс сдвиги для айтема, для юзера и до среднего рейтинга. Вот такая у нас модель. Это рейтинги которые мы хотим. Мы хотим, чтобы рейтинги вот так предсказывались. Что нам нужно сделать, чтобы вот эти Мю, b, q и p все найти? Нам нужно задать какую-то функцию, которую мы будем оптимизировать, и здесь я предлагаю решать просто задачу регрессии. Если у нас явный рейтинг от одного до пяти, то давайте мы просто будем приближать этой комбинацией наши известные rui, которые без крышки здесь внизу нарисованы, просто квадратичными потерями. Взяли (rui минус rui с крышкой) в квадрате и все это усреднили по всем парам юзер-айтем, которые в нашей матрице есть. Здесь основной момент, что эта loss-функция считается только по тем парам, которые известны, которые не пропуски. Еще здесь добавляется такое слагаемое, которое называется регуляризация (про это вы уже знаете), и здесь она тоже помогает учить более генерализуемые модели, которые на новых парах юзер-айтем предсказывают лучше. Как же эту всю модель учить? На самом деле она несложно выглядит, ее можно учить так же, как и логистическую регрессию или линейную регрессию. Давайте просто использовать SGD или стохастический градиентный спуск. Давайте возьмем в качестве всех матриц q, p, параметров bu, bi какие-то случайные значения, и из этого случайного приближения будем считать градиент наших потерь на каких-то конкретных парах юзер-айтем (это ведь стохастический градиент), и мы будем двигаться в сторону антиградиента. Если так мы будем делать достаточно долго, то рано или поздно мы сойдемся к какому-то решению, где сумма всех потерь минимальна. Давайте распишем все эти шаги для градиентного спуска. Нужно просто взять вот ту функцию, которую мы минимизируем (через один слайд назад), и посчитать честно все производные нашей функции потерь по всем параметрам. Здесь я это сделал за вас. Можете поставить на паузу и попытаться это вывести сами — это будет очень полезным упражнением. Собственно, этот алгоритм оптимизации стохастическим градиентом такой функции потерь называется Funk SVD. Это был такой прорыв в Netflix Prize в свое время, когда его люди решали на протяжении многих лет, и в итоге решение победителя — это какая-то комбинация этого алгоритма и еще кучи других. Здесь, собственно, обычный стохастический градиент: берем случайные значения параметров в цикле, берем случайную пару юзер-айтем, для которой известен рейтинг, рассчитываем градиенты и с маленьким шажком двигаемся в сторону антиградиента. Здесь для Netflix Prize, например, оптимальные параметры такие: коэффициент регуляризации 0.005, шаг градиентного спуска 0.02, и еще есть вот такое правило, что шаг градиентного спуска со временем уменьшается. То есть чтобы лучше сойтись к оптимуму, они шаг начинают через сколько-то итераций уменьшать, чтобы у нас не было большого дрожания, и мы реально сошлись красиво в какую-нибудь точку. Вот, собственно, довольно просто запрограммировать, довольно быстро работает. Как мы теперь можем делать предсказания? Теперь во всех пропущенных ячейках по нашей формуле, в которой все параметры теперь уже оптимизированы, мы можем взять и сделать предсказания. Берем ячейку для какого-то юзера-айтема, для которого рейтинг еще неизвестен. Для этого юзера и для этого айтема все параметры у нас есть, и мы просто считаем вот такое скалярное произведение плюс байесы. И это наше предсказание r с крышкой. Это все тоже считается довольно тривиально.