Описание процесса построения системы рекомендаций по книгам с использованием распределенных вычислений с Spark, Databricks и Flask в полностью развернутом веб-приложении.

Системы рекомендаций, нравится нам это или нет, проникают во все аспекты нашей жизни. Из очереди шоу и фильмов под заголовком «Потому что вы смотрели» в вашем профиле Netflix до списков песен «Сделано для вас». на Spotify мы вступили в эпоху, когда контент, мультимедиа и информация соответствуют нашим уникальным интересам.

Хотя это может быть общеизвестным, менее очевидным может быть то, что происходит за кулисами для предоставления этих рекомендаций пользователям. Мы часто слышим об «алгоритмах» и «машинном обучении», составляющих основу этих систем, но помимо этого, мы обычно оставляем остальное на усмотрение.

После недавнего посещения курса рекомендательных систем в аспирантуре я погрузился в некоторые аспекты линейной алгебры и программного обеспечения, лежащего в основе этих движков. К последним двум неделям курса я почувствовал, что у меня достаточно знаний о внутренней работе этих систем, чтобы создать полноценное производственное веб-приложение, которое будет предлагать пользователям книжные рекомендации, используя возможности распределительных вычислений PySpark , PostgreSQL , Блоки данных и Flask .

Если вы заинтересованы в адаптации проекта, код доступен на моем GitHub, а описание моего окончательного проекта, включая исследовательский анализ и пример алгоритма ALS в производстве, можно найти в отдельном репо .

Я также добавил ссылку на веб-приложение в конец этой статьи, но если вы хотите создать учетную запись и начать оценивать книги / получать рекомендации по книгам, вы можете присоединиться здесь: https: // zach -book-recommender.herokuapp.com/ .

Как работают рекомендательные системы?

Теперь, прежде чем мы рассмотрим мой собственный движок, может быть полезно предоставить некоторый контекст для нескольких всеобъемлющих тем об этих системах. На самом базовом уровне рекомендательные системы работают с помощью алгоритмов машинного обучения. Как правило, эти алгоритмы можно разделить на две категории - на основе содержания и совместная фильтрация.

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

Хотя методы, основанные на содержании, в некоторых случаях довольно эффективны, у них есть свои недостатки. Методы совместной фильтрации работают по-другому и делают все возможное, чтобы устранить некоторые ограничения фильтрации на основе содержимого. При совместной фильтрации алгоритмы используют сходство между пользователями и элементами одновременно для предоставления рекомендаций. По сути, базовые модели предназначены для того, чтобы рекомендовать элемент пользователю A на основе интересов аналогичного пользователя B.

Снижение размерности и матричная факторизация

Совместная фильтрация может быть достигнута множеством способов, однако одним из распространенных методов является использование методов матричной факторизации и уменьшения размерности. Такие алгоритмы, как чередование наименьших квадратов (ALS), разложение по сингулярным значениям (SVD) и стохастический градиент-децент (SGD), делают это достаточно хорошо, и при работе с большими объемами данных (т. Е. 1M + точки данных) эти алгоритмы в сочетании с распределительными вычисления и параллельная обработка - хорошие рабочие решения для создания готовых к работе приложений с большими объемами данных.

Если вам интересно узнать больше об этих техниках, я бы порекомендовал прочитать больше в этих отличных статьях TDS:

Обладая базовыми знаниями о том, как работают эти алгоритмы, я перейду к своему собственному проекту и воспользуюсь ALS, чтобы предлагать пользователям рекомендации по книгам.

Как работает мое приложение

Прежде чем углубиться в каждый компонент создания приложения, я обрисую общую схему его работы и технологии, используемые для предоставления рекомендаций пользователям в веб-приложении Flask:

Как мы видим, это довольно сложная система, которая в значительной степени полагается на распределенные вычисления в Spark для обработки около 1 миллиона оценок книг и расчета обновленных рекомендаций для пользователей каждый час. Это также зависит от важного соединения между развернутой базой данных PostgreSQL и кластером Amazon Web Services, который содержит мой алгоритм ALS.

Имея общий алгоритм, описанный выше, мы можем теперь разбить каждый этап построения движка.

Определение качественного набора данных о пользователях для машинного обучения

В качестве первого шага было необходимо найти большой набор данных из книги пользователей, который бы хорошо работал с выбранными мной алгоритмами и давал пользователям надежные прогнозы и рекомендации.

Поэтому я провел предварительный анализ и решил использовать g oodbooks-10k dataset. Согласно Kaggle, этот набор данных содержит около 1 миллиона оценок по 10 000 различных книг, размещенных на популярной книжной платформе, Goodreads. Имея в руках этот набор данных, я заметил несколько вещей:

  • Набор данных содержал в общей сложности 53 425 пользователей, которые поставили оценки как минимум 10 книгам.
  • средняя оценка книги составляла около 3,85, при этом 4 - наиболее распространенная оценка по шкале Лайкерта от 1 до 5. Таким образом, большинство оценок были весьма положительными.
  • Как и ожидалось, набор данных был очень разреженным (~ 99,82% файла оценок было пустым).
  • Некоторые из книг с самым высоким рейтингом в наборе данных были любимыми в семье, в том числе части из коллекции Кальвина и Гоббса, Гарри Поттера и книги с религиозными принципами.

Чтобы изучить часть этого исследовательского анализа, вы можете перейти в этот блокнот jupyter.

Создание и развертывание веб-приложения

Затем, после некоторого исследовательского анализа, я решил создать простое приложение python Flask, чтобы позволить новым пользователям делать следующее:

  • Зарегистрируйтесь / войдите под определенным именем пользователя.
  • Загрузите страницу профиля, которая отображает историю рейтинговых книг пользователя и обеспечивает навигацию для оценки большего количества книг или просмотра рекомендованных книг (см. Пример ниже):

  • Включите функцию поиска, которая взаимодействует с Goodreads API, чтобы пользователи могли искать книгу по названию, а затем оценивать книгу по шкале от 1 до 5. (см. пример ниже):

  • После оценки нескольких книг пользователь может увидеть их индивидуальные рекомендации на отдельной странице (будет показано далее в статье).

В качестве важного примечания, эти оценки пользователей из приложения были затем включены обратно в надежный набор данных goodbooks и, в конечном итоге, в алгоритм ALS. Создав базу данных PostgreSQL, я смог сопоставить идентификаторы новых пользователей, их рейтинги и идентификаторы книг с этим исходным набором данных. Следовательно, всякий раз, когда от пользователя приходит новая оценка, она добавляется к набору данных оценок, который я позже использовал для своего механизма рекомендаций.

Проведя множество локальных тестов, чтобы убедиться, что идентификаторы книг и идентификаторы пользователей совпадают в базе данных, я развернул приложение с помощью Heroku.

Ресурсы для развертывания Flask и PostgreSQL

Для получения инструкций о том, как развернуть приложение Flask и соответствующую базу данных PostgreSQL с помощью Heroku, вы можете обратиться к этому чрезвычайно полезному руководству YouTube Брэда Трэверси. Особенности развертывания с помощью Heroku также можно найти в этой статье.

Работа с Databricks и PySpark - разработка моего алгоритма ALS

После настройки и развертывания функциональности приложения я затем использовал Databricks и Pyspark для создания алгоритма альтернативных наименьших квадратов (ALS), который поглощает мой набор данных рейтингов из моей базы данных PostgreSQL, вычисляет обновленные прогнозы на основе методов матричной факторизации, установленных в Databricks, и затем рекомендует книги каждому пользователю на основе наивысшего прогнозируемого балла для каждого пользователя.

Ниже приведен фрагмент кода, описывающий этот процесс:

Сначала я настроил схему для фрейма данных Spark, прежде чем читать таблицу рейтингов из PostgreSQL:

from pyspark.sql.types import StructType, StructField
from pyspark.sql.types import DoubleType, IntegerType, StringType

ratings_schema = StructType([
  StructField("col_id", IntegerType()),
  StructField("userid", IntegerType()),
  StructField("rating", DoubleType()),
  StructField("book_id", IntegerType()),
  StructField("username", StringType()),
  StructField("isbn10", StringType())
])

Затем, после настройки переменных среды и подключения JDBC к моей удаленной базе данных PostgreSQL, я прочитал таблицу и сохранил ее как фрейм данных Spark:

remote_table = spark.read.format("jdbc")\
  .option("driver", driver)\
  .option("inferSchema", ratings_schema) \
  .option("url", url)\
  .option("dbtable", table)\
  .option("user", user)\
  .option("password", password)\
  .load()

Загрузив фрейм данных Spark в Databricks, я начал настраивать данные для запуска моей модели ALS. Во-первых, я разделил фреймворк на набор данных для обучения, проверки и тестирования - 60% обучение, 20% проверка и 20% тестирование. Набор данных проверки использовался для проверки точной настройки параметров моей модели и позволил мне иметь набор тестов выдержки, который я использовал для вычисления среднеквадратичной ошибки (RMSE) в окончательной оптимизированной модели.

(training, validation, test) = remote_table.randomSplit([0.6, 0.2, 0.2])  
# caching data to cut down on cross-validation time later training.cache() 
validation.cache() 
test.cache()

После разделения данных я провел некоторую тонкую настройку модели, чтобы определить оптимальные параметры. Затем я запустил модель ALS с этими оптимальными параметрами и подогнал ее к набору обучающих данных.

als = ALS(maxIter=10, regParam=0.20, userCol="userid", itemCol="book_id", ratingCol="rating", coldStartStrategy="drop", nonnegative = True, implicitPrefs = False).setRank(50)
model = als.fit(training)

Вы заметите, что я установил ранг матрицы на 50, что сократило время обработки и по-прежнему давало ценные прогнозы. Затем я смог использовать эту модель для вычисления прогнозов для тестового набора данных и определения значения RMSE.

predictions = model.transform(test)
rmse = evaluator.evaluate(predictions)

В конце концов, мне удалось получить значение RMSE около 0,9, что для наших целей и шкалы Лайкерта от 1 до 5, это неплохо. Затем я смог использовать эту модель для создания прогнозов и рекомендаций для полного набора данных рейтингов. Для этого я использовал функцию recommendForAllUsers() в PySpark, чтобы найти 10 книг с наивысшим рейтингом для каждого пользователя.

ALS_recommendations = model.recommendForAllUsers(numItems = 10)
# Temporary table ALS_recommendations.registerTempTable("ALS_recs_temp") clean_recs = spark.sql("""SELECT userid,                             bookIds_and_ratings.book_id AS book_id,                             bookIds_and_ratings.rating AS prediction                         FROM ALS_recs_temp LATERAL VIEW explode(recommendations) exploded_table AS bookIds_and_ratings""")
clean_recs.join(remote_table, ["userid", "book_id"], "left").filter(remote_table.rating.isNull()).show()

clean_recs_filtered = clean_recs.select("userid", "book_id", "prediction")

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

new_books = (clean_recs_filtered.join(remote_table, ["userid", "book_id"], "left").filter(remote_table.rating.isNull()))

В качестве последнего шага я определил только идентификаторы пользователей, которые зарегистрировались через мое приложение, чтобы добавить рекомендации обратно в базу данных PostgreSQL. Поскольку из исходного набора данных goodbooks было такое большое количество прогнозов и оценок, которые вернутся к несуществующим пользователям, я отфильтровал, чтобы включить только рекомендации для пользователей, которые присоединились через веб-приложение:

new_books_fnl = new_books.select('userid', 'book_id', 'prediction')

new_books_users = new_books_fnl.filter(new_books_fnl['userid'] > 53424)

new_books_use = new_books_users.select('userid', 'book_id', 'prediction')

А вот пример рекомендаций, возвращаемых пользователям, присоединившимся к веб-приложению:

Теперь у нас есть рекомендации для пользователей в веб-приложении!

Как мы видим, некоторые прогнозы не очень хороши - например, первый пользователь в таблице получит книгу, которую, по прогнозам алгоритма, он оценит примерно в 3,5 / 4. В идеале мы хотим найти книги, которые действительно понравятся пользователям (не думая о таких компонентах, как интуитивная прозорливость, новизна и т. Д.). Поэтому, имея больше времени и более широкую базу пользователей, я надеюсь увидеть рекомендованные книги с более высокими прогнозируемыми оценками.

Когда модель работала локально в Databricks, я затем смог загрузить эту оптимизированную модель и развернуть ее в кластере веб-служб Amazon, выполняя задание cron каждый час для обработки любых новых оценок, которые занесены в мою базу данных PostgreSQL. Ежечасное задание cron, по сути, перезапустит модель, пересчитает прогнозы и рекомендации, а затем перезапишет таблицу рекомендаций в моем PostgreSQL с обновленными рекомендациями. Затем он снова подключается к моему приложению Flask для обслуживания пользователей. Инструкции по развертыванию, связанные с AWS и Databricks, вы можете найти здесь.

Когда таблица рекомендаций в моем PostgreSQL обновлена ​​моими рекомендациями и соответствующими идентификаторами книг, я мог затем использовать Goodreads API, чтобы найти информацию о книге (например, изображение, заголовок и т. Д.) Для построения внешнего приложения.

Итак, насколько хорошо это работает?

В конце концов, я провел небольшое тестирование и заметил, что рекомендации были неплохими! Например, я создал несколько пользователей, которые были фанатиками науки о данных, и оценил множество книг по науке о данных. Я намеренно имел некоторое совпадение оценок книг у разных пользователей, но также пытался оценить несколько новых книг для каждого пользователя.

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

Как видно из заголовков, выделенных красным, пользователь 2 очень высоко оценил эти две книги (рейтинг 5). Таким образом, при запуске нашего алгоритма машинного обучения рейтинги книг пользователя 1 и пользователя 2 частично совпадали - это означало, что книги, которые не были оценены пользователем 1 (и косвенно не были прочитаны), но получили высокую оценку пользователя 2, затем служили рекомендациями для Пользователя 1.

Попробуй сам!

Чтобы попробовать приложение, перейдите сюда: https://zach-book-recommender.herokuapp.com/

Хотя рекомендации для книг по науке о данных, кажется, работают довольно хорошо, существует большой пробел в других жанрах и темах. Вы, вероятно, заметите, что ваши рекомендации будут также привлекать другие книги с очень высоким рейтингом из набора данных goodbooks-10k (например, Calvin and Hobbes и т. Д.), Пока к приложению не присоединится больше пользователей, похожих на ваши вкусы. Поэтому я бы порекомендовал поделиться им с друзьями и посмотреть, станут ли со временем ваши рекомендации становиться все лучше и лучше!

Соединять

Если вам понравился этот материал или он был вам полезен, свяжитесь с нами! Мне нравится сотрудничать с другими в проектах и ​​учиться у коллег, увлеченных работой в области науки о данных.

Вы также можете проверить мое портфолио визуализации данных, где я в основном работаю с Javascript и D3.js, и рассказать больше о других проектах, которые я реализовал в аспирантуре.