Примечание куратора: Этот пост был изначально написан в августе 2012 года.
В настоящее время я участвую в Neo4j-Heroku Challenge . Моя запись — пока еще неоконченная — служба оценки и рекомендации пива под названием FrostyMug . Все основные функции завершены, за исключением фактических рекомендаций, над которыми я сейчас работаю. Я хотел бы поделиться некоторыми своими мыслями и методами построения механизма рекомендаций.
Моей отправной точкой для механизма рекомендаций было сообщение в блоге Марко Родригеса,
основанного на графике механизма рекомендателя фильмов . Подход Марко довольно прост, и он отлично объясняет тонкости совместной и контентной фильтрации. Это был также отличный способ познакомиться с
API обхода графа Гремлин , которого я неразумно избегал, опасаясь, что он слишком сложный.
Базовая совместная фильтрация работает следующим образом: если я оцениваю пиво, скажем,
Lonerider Sweet Josie Brown Ale, как 9 из 10, а вы оцениваете то же пиво, что и 8 из 10, то я должен быть в состоянии получить рекомендации на основе других сортов пива. что вы (и другие, которые оценили это пиво так же, как я) оценили высоко.
Я думаю, что мы можем улучшить это. Мы с тобой оценили одно пиво одинаково. Что произойдет, если это единственное пиво, которое мы оценили подобным образом? Что если я оценил
Fullsteam Carver Sweet Potato Ale на 8 из 10, а вы оценили его на 2 из 10? Вы показали, что у вас плохой вкус, так почему я должен получать рекомендации, основанные на других ваших оценках, просто потому, что мы случайно оценили одинаковое пиво?
С небольшой модификацией мы можем преодолеть эту проблему. Вместо того, чтобы основывать рекомендации на одном подобном рейтинге, я могу рассчитать, насколько одинаково вы и я оценили все вещи, которые мы оценили, и получать рекомендации от вас, только если я определил, что мы достаточно похожи в наших вкусах. Назовите это «основанная на сходстве совместная фильтрация».
Поиск похожих пользователей работает так:
- Найти всех пользователей, которые оценили то же пиво, что и я.
- Для каждого оцененного пива рассчитайте разницу между моим рейтингом и рейтингом этого пользователя.
- Для каждого пользователя усредните различия.
- Верните всех пользователей, где средняя разница меньше или равна некоторому допуску (2 в приведенном ниже примере.)
Давайте посмотрим, как это выглядит в Gremlin, языке обхода графов для конкретного домена в Groovy. Gremlin построена на концепции «каналов», когда одна цепочка операций выполняется от начала до конца для каждого элемента, помещенного в канал. Элементами в нашем случае являются пользовательские узлы, пивные узлы и грани рейтинга, которые связывают их в нашем графике:
// We're going to use this again and again, so make a single "step" for it, // to which we can pass a "similarity threshold" Gremlin.defineStep('similarUser', [Vertex,Pipe], {Integer threshold -> // Capture the incoming pipe target = _(); // Set up a table to hold ratings for averages m=[:].withDefault{[0,0]}; // Find all beers rated by the target, and store each rating target.outE("RATED").sideEffect{w=it.rating} // For each rated beer, find all the users who have also rated that beer // and who are not the target user, then go back to their ratings .inV.inE("RATED").outV.except(target).back(2) // At this point in the pipeline, we are iterating over // only the ratings of beers that both users have rated. // For each rating, calculate the difference between // the target user's rating and the compared user's rating .sideEffect{diff=Math.abs(it.rating-w)} // For each compared user, store the number of beers rated in common // and the total difference for all common ratings. .outV.sideEffect{ me=m[it.id]; me[0]++; me[1]+=diff; }.iterate(); // Find the average difference for each user and filter // out any where the average is greater than two. m.findAll{it.value[1]/it.value[0] <= threshold}.collect{g.v(it.key)}._() }); // Return all the users who are similar to our target // with an average similarity of 2 or less g.v(123).similarUser(2)
Результатом этого сценария является список всех пользовательских узлов, которые считаются «похожими» на целевого пользователя. Теперь мы можем получить более точные рекомендации, основанные на пользователях, чьи вкусы похожи на наши. Пользователям, которые похожи на меня, рекомендуем пиво, которое они высоко оценили:
- Найти все сорта пива, оцененные подобными пользователями.
- Для каждого пива усредните оценки похожих пользователей.
- Сортируйте пиво по наивысшему и наименьшему среднему и верните 25 лучших
r=[:].withDefault{[0,0]}; // Find similar users g.v(123).similarUser(2) // Find all outgoing beer ratings by similar users // and the beer they are rating .outE("RATED").sideEffect{rating=it.rating}.inV // Store the ratings in a map for averaging .sideEffect{me=r[it.id]; me[0]++; me[1]+=rating; } // Transform the map into a new map of beer id : average r.collectEntries{key, value -> [key , value[1]/value[0]]} // sort by highest average first, then take the top 25 .sort{a,b -> b.value <=> a.value}[0..24] // Transform the map back into a mapping of beer node and rating .collect{key,value -> [g.v(key), value]}
Теперь я получаю рекомендации только от пользователей, чьи вкусы похожи на мои.
Я также могу рассчитать оценочный рейтинг для любого пива, которое я не оценил. Чтобы увидеть, как это может быть полезно, рассмотрим Netflix. Когда вы смотрите на любой фильм на Netflix, который вы еще не оценили, они показывают вам, как вам нравится этот фильм. Это может помочь определить, какой из фильмов вы хотите посмотреть, или, в нашем случае, какое пиво вы хотели бы попробовать, имея ограниченные карманные деньги. Опять же, мы можем использовать наших похожих пользователей, чтобы вычислить оценочный рейтинг данного пива:
- Найти все рейтинги целевого пива от похожих пользователей.
- Верните среднее значение их оценок этого пива.
// Grab our target beer beer=g.v(987) // Find similar users g.v(123).similarUser(2) // For each similar user, find all that have rated the target beer .outE("RATED").inV.filter{it==beer} // Go back to their ratings, and average them .back(2).rating.mean()
Так что теперь у нас есть простой механизм рекомендаций, основанный на рейтингах пользователей с похожими вкусами. Есть несколько улучшений, которые еще можно сделать:
- Не считайте пользователя «похожим», если он не оценил как минимум 10 сортов пива (или какой-либо другой порог). Это предотвращает ложное обозначение пользователей, которые оценили всего несколько сортов пива, как похожие.
- Просчитайте похожих пользователей заранее и сохраните их с отношением «ПОХОЖИЕ» к целевому пользователю. Периодически пересчитывайте похожих пользователей, чтобы список обновлялся.
- Используйте случайную выборку или ограничение размера на количество пользователей и рейтинги, проверенные при определении сходства, точности торговли для производительности.
Большое спасибо Марко за его помощь в списке рассылки, за то, что он дал мне несколько советов по написанию этого, и за то, что я был довольно удивительным парнем в целом.