Метод кластеризации на основе предпочтений для рекомендации элементов с использованием AWS SageMaker и его встроенного алгоритма k-Means.

Цель

Цель этого проекта — создать механизм рекомендаций с помощью AWS SageMaker. Основная цель — порекомендовать 20 лучших фильмов пользователям, сгруппированным по сходству предпочтений.

Контекст

В рамках «Облачного гуру — вызов машинного обучения», предложенного Кешей Уильямс, энтузиастам машинного обучения предлагается реализовать свое решение следующей проблемы:



Среда и инструменты

Мы решили работать с набором данных MovieLens, загруженным с www.grouplens.org.

В частности, мы использовали файлы movies.csv и ratings.csv из файла ml-latest-small.zip, загруженного в нашу корзину AWS S3. . Этот проект был разработан на ноутбуке Jupyter с фреймворком conda_python3 в среде AWS SageMaker. Для обработки данных мы использовали популярные библиотеки машинного обучения, такие как Pandas и Scikit-Learn.

Наше решение

Импорт библиотек

У Scikit-Learn есть собственный алгоритм k-Means, но для этого проекта мы выбрали встроенный в SageMaker алгоритм k-Means. Мы также решили использовать Pandas для управления кадрами данных. В качестве инструментов визуализации использовались Matplotlib и Seaborn. Наконец, мы импортировали boto3, так как это библиотека Python для AWS.

import pandas as pd
import numpy as np
import io
import sagemaker.amazon.common as smac
from pandas import DataFrame
import boto3
from sagemaker import get_execution_role
import sagemaker
from sagemaker import KMeans
from datetime import datetime
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
import seaborn as sns

Загрузка данных

Давайте получим соответствующие учетные данные для SageMaker, чтобы получить доступ к нашей корзине S3.

role = get_execution_role()
bucket='guruchallenge-bucket'
sub_folder = 'movielens_dataset'

Давайте загрузим файлы csv в нашу записную книжку с фреймами данных Pandas, чтобы начать этапы предварительной обработки.

data_key = 'movies.csv'
data_location = 's3://{}/{}/{}'.format(bucket, sub_folder, data_key)
movies = pd.read_csv(data_location, low_memory=False, delimiter=',', encoding='utf-8')
data_key = 'ratings.csv'
data_location = 's3://{}/{}/{}'.format(bucket, sub_folder, data_key)
ratings = pd.read_csv(data_location, low_memory=False, delimiter=',', encoding='utf-8')

Мы видим, что набор данных содержит 100836 оценок 9742 фильмов от 610 пользователей.

Разработка функций

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

Matplotlib позволяет нам визуализировать данные и выявить 4 выброса. Эти 4 объединенных пользователя оценили в общей сложности 9148 фильмов. Это почти 10% всех оценок всего для 4 пользователей. Поскольку это существенно повлияет на наш процесс кластеризации, давайте избавимся от них. Кроме того, поскольку позже мы будем использовать MinMaxScaling, следует удалить выбросы.

Горячее кодирование жанров генерирует набор векторов для всех фильмов, просмотренных данным пользователем. Теперь для каждого пользователя давайте умножим каждый вектор на соответствующий ему рейтинг. Суммируя все столбцы для данного пользователя, мы можем создать «вектор оценок», который представляет их вкусы на основе их истории просмотров. Давайте разделим векторы оценки на их соответствующий UserRatingCount, чтобы сгенерировать среднюю оценку для каждого пользователя.

Давайте визуализируем тепловую карту (ее часть) нашего фрейма данных в ее текущем состоянии:

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

Чтобы решить эту проблему, нам нужен более актуальный и сбалансированный подход к кластеризации. Давайте выполним масштабирование функций для лучшей сегментации пользователей: MinMaxScaler масштабирует все функции от 0 до 1. Поэтому он будет выделять пользователей, которые имеют более сильные предпочтения, чем другие, в любом жанре.

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

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

Обучение и развертывание

Встроенный алгоритм k-Means ожидает в качестве входного формата float32. Наш фрейм данных преобразуется в float32 и используется в качестве обучающего набора данных. Артефакты модели хранятся на S3. Обучение будет проводиться на одном c4.xlarge.

А пока давайте определим k = 10 кластеров для этого обучения.

data_train = df_scaled
data_train = data_train.astype('float32')
num_clusters = 10
output_location = 's3://' + bucket + '/model-artifacts'
kmeans = KMeans(role=role,
               train_instance_count=1,
               train_instance_type='ml.c4.xlarge',
               output_path=output_location,
               k=num_clusters)

Теперь вызываем подгонку оценщика k-Means для обучения нашей модели. Этот процесс обычно занимает от 3 до 5 минут:

%%time
kmeans.fit(kmeans.record_set(data_train.values), job_name=job_name)

Теперь развертываем нашу модель на конечной точке. Этот шаг обычно занимает до 10 минут.

kmeans_predictor = kmeans.deploy(initial_instance_count=1,
                                 instance_type='ml.m4.xlarge')

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

%%time 
result = kmeans_predictor.predict(data_train.values[0:len(data_train)])
cluster_labels = [r.label['closest_cluster'].float32_tensor.values[0] for r in result]
CPU times: user 51.3 ms, sys: 82 µs, total: 51.4 ms
Wall time: 297 ms

Теперь удаляем конечную точку, чтобы избежать дополнительных затрат.

sagemaker.Session().delete_endpoint(kmeans_predictor.endpoint)

Давайте порекомендуем товары!

Мы собираемся сгенерировать 20 лучших рекомендаций фильмов для данной группы пользователей. Например, возьмем кластер №8. Прежде всего, давайте определим идентификаторы всех пользователей в нашем кластере.

Давайте определим «clust_movieRatings» как фрейм данных, который содержит все оценки для всех фильмов, просмотренных пользователями в этом кластере. Мы собираемся давать рекомендации по фильмам на основе данных, содержащихся в этом фрейме данных.

clust_movieRatings = ratings.loc[ratings['userId'].isin(users_cluster)]

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

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

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

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

Взвешенный рейтинг (𝑊𝑅) = (𝑣/(𝑣+𝑚))𝑅+(𝑚/(𝑣+𝑚))𝐶

Где:

R = среднее значение для фильма (внутри кластера)

v = количество голосов за фильм (в кластере)

m = минимальное количество голосов, необходимое для внесения в список («байесовский порог»)

C = среднее количество голосов по всему отчету («байесовская константа»)

Для элемента, который будет включен в этот конкретный проект, мы решили установить минимальное количество голосов равным 3.

Давайте добавим поле «weightedRating» в наш фрейм данных «clust_movieRatings» и посмотрим на наш окончательный рейтинг:

Давайте преобразуем эти movieId в настоящие названия фильмов:

Это 20 лучших фильмов, которые можно рекомендовать любому пользователю, отмеченному кластером № 8.

Давайте поиграем с нашим алгоритмом и визуализируем тепловую карту кластера № 6:

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

Вывод

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

Для дальнейших улучшений нам все еще нужно реализовать «метод локтя» для определения оптимального значения k. Нам также придется обучать нашу модель на больших наборах данных.

Вот ссылка на наш код, спасибо за прочтение!