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

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

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

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

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

Подход окружения, использующий сюрприз

Первым шагом был сбор данных. Используя Python и BeautifulSoup, я скопировал все свои данные с популярного веб-сайта обзоров пива BeerAdvocate. Данные хранились в базе данных MongoDB, размещенной на AWS. Модели были обучены с использованием sci-kit learn и библиотек Surprise.

В итоге я соскоблил все сорта пива с наивысшим рейтингом (100 лучших в каждой подкатегории) по 7 категориям стилей пива. Полученные данные инкапсулировали 56 подкатегорий пива; 4964 сорта уникального пива; около 88 тысяч уникальных пользователей (рецензентов); примерно 1,4 миллиона пар пользовательских отзывов (например, Пользователь1: Рейтинг1).

Чтобы улучшить результаты рекомендателя, пиво из нижних 10 процентов с точки зрения средней оценки и количества отзывов было удалено. В результате было удалено около 20% сортов пива, в результате чего уникальное количество пива увеличилось с 4964 до 3959. Я хочу отметить, что при использовании библиотеки Surprise «коэффициент усадки» (для вменения недостающих оценок) учитывает количество отзывов для каждого элемента. /Пользователь. Он наказывает предметы / пользователей очень маленькими отзывами. Поэтому этот шаг не критичен.

Библиотека Surprise - это хорошо документированная (если вы знаете, где искать) и интуитивно понятная библиотека для создания рекомендательных систем. В этом случае использовался алгоритм прогнозирования KNN. Это называется алгоритмом прогнозирования, потому что он приписывает недостающие оценки базовым оценкам (см. Документацию Surprise или мой блог для получения дополнительной информации). Я расскажу о ключевых элементах реализации кода здесь.

  1. Форматирование имен входов в идентификаторы - понимание сырых и внутренних идентификаторов

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

Функция read_item_names () используется для преобразования введенных названий пива в сырые идентификаторы и наоборот.

Функция get_rec () возвращает k ближайших рекомендаций на основе сходства элементов после обучения модели. Здесь вы можете видеть, что каждый необработанный идентификатор сопоставляется с уникальным целым числом, называемым внутренним идентификатором - это сделано для того, чтобы сделать его более подходящим для манипулирования Surprise.

Приведенные выше коды были взяты из документации Surprise FAQ и изменены.

2. Обучите и оцените модель

reader = Reader(rating_scale=(1,5))
data = Dataset.load_from_df(merged_df2[['userID', 'beerID', 'rating']], reader)
trainset = data.build_full_trainset()
sim_options = {'name': 'pearson_baseline', 'user_based': False}
algo = KNNBaseline(sim_options=sim_options)
algo.fit(trainset)

Выполнение вышеуказанного блока кода позволит вам обучить модель с вашими данными. Функция Reader предназначена для нормализации данных (оценки по шкале от 1 до 5). Функции Dataset.load_from_df () и data.build_full_trainset () - это встроенные функции Surprise, позволяющие загружать данные во всем фреймворке данных, и они будут создавать для вас обучающий набор. . sim_options позволяет вам указать тип используемой меры сходства, например, зависимость Пирсона от косинусного расстояния от среднего квадрата расстояния. «User based = False» означает, что это схожесть элемент-элемент, а не пользователь-пользователь.

evaluate(algo, data, measures=['RMSE', 'MAE'])

Запуск метода оценки вернет 5-кратную перекрестную проверку с указанными показателями. Это измеряет, насколько хорошо алгоритм предсказывает недостающий рейтинг по сравнению с фактическим рейтингом. Для этого рекомендателя пива RMSE (среднеквадратичная ошибка) составляла 0,4, а MAE (средняя абсолютная ошибка) составляла 0,28. Неплохо, учитывая, что оценки выставлены по шкале от 1 до 5.

По сути, алгоритм возьмет разреженную матрицу (например, см. Ниже) и подставит все пропущенные значения с помощью базовой оценки (r_ui). Другими словами, теперь у нас есть прогнозы того, как каждый пользователь будет оценивать каждое пиво в этой матрице. Затем алгоритм kNN измеряет расстояние между рейтингами, чтобы определить «близость» и вернуть ближайших соседей.

Теперь давайте протестируем этот рекомендатель.

Топ-20 ближайших соседей по «Enjoy By IPA» от Stone brewing представили список довольно многих других IPA, но среди них были также стауты и янтарный эль. В этом списке я узнал 3 IPA из одной пивоварни, а «Blazing World» - уникальный хмелевой янтарный эль, по вкусу напоминающий IPA. Не слишком потрепанный!

Подход со скрытыми факторами и усеченным SVD

В качестве альтернативы мы можем использовать подход латентных факторов, используя усеченный SVD. В неожиданном пакете также есть алгоритм для выполнения SVD и других методов матричной факторизации, однако он потребует некоторого глубокого погружения в код и внесения некоторых изменений, чтобы иметь возможность запускать усеченный SVD. Модуль SVD по умолчанию в Surprise не позволяет использовать усеченный SVD. Причина, по которой необходим усеченный SVD, заключается в том, что для стандартного SVD потребуется слишком много памяти и вычислительной мощности. Учитывая, что у нас есть матрица размером примерно 5K x 88K, это было бы невозможно.

Сначала нам нужно отформатировать наши данные в сводную таблицу и заполнить любые NaN значениями 0. Усеченный SVD не обрабатывает разреженные матрицы с нулевыми значениями.

user_reviews_df2_pivot = user_reviews_df2.pivot_table(index='user', columns='beer_name', values='rating').fillna(0)

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

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

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

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

Основываясь на вышеизложенной информации, я решил использовать 200 компонентов для этого урезанного SVD. Я считаю, что это справедливо, учитывая, что существует только 3900 уникальных сортов пива (после удаления нижнего 10% -ного элемента в средних рейтингах и количестве отзывов) в 56 подкатегориях, а при 200 компонентах модель отражает 2/3 объясненной дисперсии. Более того, мне трудно представить, что обычный пьющий пиво сможет различить 200 различных «стилей» пива.

Дополнительное примечание о скрытых функциях

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

Еще один способ подумать об этом - подумать о том, сколькими различными способами вы можете классифицировать или описать типы доступных сортов пива. Даже в 56 подкатегориях многие из этих подкатегорий очень похожи друг на друга, и большинство людей, вероятно, не заметят разницы. Однако эти скрытые факторы заключают в себе особенности, которые трудно объяснить. Например, конкретный латентный фактор можно охарактеризовать как «слегка охмеленный + низкое содержание алкоголя + легкое тело + высокая карбонизация» или особую подкатегорию (например, IPA западного побережья). Вы можете подумать, что существует бесконечное количество комбинаций, но это не так.

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

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

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

# create the SVD
SVD200 = TruncatedSVD(n_components=200,random_state=200)
matrix200 = SVD200.fit_transform(T)
# get the similarity matrix
corr200 = np.corrcoef(matrix200)
# get list of all beer names
beer_rec_names200 = merged_df2_pivot.columns
beer_rec_list200 = list(beer_rec_names200)

Следующая функция используется для поиска первых n соседей с заданным названием пива.

Давайте проверим это на том же пиве раньше.

Этот список сильно отличается от того, который был составлен с помощью метода kNN, в обоих списках фигурируют только 3 сорта пива - «Lunch», «Sculpin IPA» и «Grapefruit Sculpin IPA».

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

Как видите, из 50 лучших рекомендаций степень перекрытия между двумя методами варьировалась от 0 до 20, что соответствует 0–40%. Таким образом, два разных метода возвращают очень разные результаты, и это ожидается, потому что SVD фиксирует отношения, которых не учитывает подход ближайшего соседа.

Вывод:

Создание рекомендательной системы для совместной фильтрации может быть довольно простым делом. Библиотека сюрпризов - отличный модуль для использования. Вы можете использовать подход соседей (KNN) или подход скрытых факторов (SVD). Оба метода могут давать очень разные результаты, однако на самом деле это может быть хорошо, поскольку вы можете объединить оба результата для создания своего рода гибридного рекомендателя. Совместная фильтрация отлично подходит, если вы хотите, чтобы ваш механизм рекомендаций давал разнообразные или разнообразные результаты. В противном случае вы можете просто использовать рекомендатель на основе контента, который в этом случае будет рекомендовать в основном пиво из той же подкатегории. Лучше всего было бы объединить результаты двух или всех трех (на основе контента, kNN, SVD).

Ссылки: