Рабочий пример для создания функций пользователей / элементов для решения проблемы холодного запуска и прогнозирования рейтингов для новых пользователей.
Примечание. У пятилетнего ребенка должно быть практическое знание Python!
Почему так много шума из-за проблемы с холодным запуском?
В рекомендательных системах проблема холодного старта относится к проблеме рекомендации элементов полностью новому пользователю, т. Е. пользователь, который не взаимодействовал ни с одним из существующих элементов в вашей базе данных. Другими словами, если вы создадите матрицу «пользователь-элемент» из исходных данных обучения, вы не найдете ни одной строки для этого нового пользователя. Похожая аналогия может использоваться для объяснения проблемы холодного запуска в случае совершенно нового элемента.
Это может быть довольно неудобно для вас как разработчика приложения, например, когда слушатели пытаются искать новые предложения подкастов для прослушивания, а вы получили - zip, zilch, nada, НИЧЕГО. Что ж, именно в этом заключалась проблема на начальных этапах разработки нашего подкаст-приложения Podurama. Посмотрим, как LightFM помог нам решить эту проблему…
Как LightFM помогает решить проблему холодного старта?
Одной из сильных сторон LightFM является то, что модель не страдает от проблем с холодным запуском, как для пользователя, так и для элемента. Причина в том, что LightFM позволяет создавать гибридную рекомендательную систему.
Гибридный рекомендатель - это особый вид рекомендателя, который использует как совместную фильтрацию, так и фильтрацию на основе содержимого для выработки рекомендаций.
Проще говоря, LightFM может использовать обычное взаимодействие пользователя с элементом для прогнозирования известных пользователей. В случае n новых пользователей он может делать прогнозы, если ему известна дополнительная информация об этих новых пользователях. Эта дополнительная информация может включать такие характеристики, как пол, возраст, этническая принадлежность и т. Д., И должна передаваться в алгоритм во время обучения.
Давайте погрузимся в кодирование
Я собираюсь использовать (очень) небольшие фиктивные данные рейтингов и данные функций, потому что я хочу иметь возможность показывать промежуточные результаты на каждом этапе. Если хотите, то вот Блокнот Jupyter.
Фрейм данных рейтинга
Он содержит данные от трех пользователей - u1, u2, u3 и четырех элементов - i1, i2, i3, i4.
# create dummy dataset data = {'user': ['u1','u1','u2','u2', 'u3', 'u3', 'u3'], 'item': ['i1', 'i3', 'i2', 'i3', 'i1', 'i4', 'i2'], 'r': [1,2,1,3,4,5,2] } df = pd.DataFrame(data, columns = ['user', 'item', 'r'])
Фреймворк пользовательских функций
У нас есть четыре дополнительных элемента информации о каждом пользователе - три логических функции - f1, f2, f3 и функция местоположения - loc, которая на данный момент может принимать два значения - Дели или Мумбаи.
#dummy user features data = {'user': ['u1','u2','u3', 'loc'], 'f1': [1, 0, 1, 'del'], 'f2': [1, 1, 1, 'mum'], 'f3': [0, 0, 1, 'del'] } features = pd.DataFrame(data, columns = ['user', 'f1', 'f2', 'f3', 'loc'])
Создание набора данных, совместимого с LightFM
Если вы бегло просмотрели документацию LightFM, вы бы знали, что ему нравятся входные данные в определенном формате. И поэтому мы должны подчиниться.
from lightfm.data import Dataset dataset1 = Dataset()
Вызов метода fit
Нам нужно вызвать метод fit, чтобы сообщить LightFM, кто пользователи, с какими элементами мы имеем дело, в дополнение к любым функциям пользователя / элемента.
Мы передадим методу fit три входа:
users
: список всех пользователейitems
: перечислить все элементыuser_features
: список дополнительных пользовательских функций
Передать список пользователей и элементов довольно просто - просто используйте столбцы «пользователь» и «элемент» из df
.
Когда дело доходит до передачи user_features
, я настоятельно рекомендую передать список, в котором каждый элемент имеет формат, подобный 'feature_name:feature_value'
(обещаю, я объясню, почему я предпочитаю делать это таким образом, и есть ли другие альтернативы, но пока просто потерпите меня).
Это означает, что наш user_features
должен выглядеть примерно так: ['f1:1', 'f1:0', 'f2:1', 'f3:0', 'f3:1', 'loc:mum', 'loc:del']
.
Как вы правильно догадались, этот список был создан путем рассмотрения всех возможных feature_name,feature_value
пар, которые могут встретиться в обучающем наборе. Например, для feature_name
, равного loc
, может быть два feature_values
, а именно mum
и del
.
Я написал небольшой фрагмент кода, который позволяет мне создать такой список (я называю его uf
):
uf = [] col = ['f1']*len(features.f1.unique()) + ['f2']*len(features.f2.unique()) + ['f3']*len(features.f3.unique()) + ['loc']*len(features['loc'].unique()) unique_f1 = list(features.f1.unique()) + list(features.f2.unique()) + list(features.f3.unique()) + list(features['loc'].unique()) #print('f1:', unique_f1) for x,y in zip(col, unique_f1): res = str(x)+ ":" +str(y) uf.append(res) print(res)
Наконец, со всеми доступными частями давайте вызовем метод fit в нашем наборе данных:
# we call fit to supply userid, item id and user/item features dataset1.fit( df['user'].unique(), # all the users df['item'].unique(), # all the items user_features = uf # additional user features )
Теперь, когда у нас есть готовый скелетный набор данных, мы все готовы добавить в него фактические взаимодействия и рейтинги.
Построение взаимодействий
Входными данными метода build_interactions
является итерация взаимодействий, где каждое взаимодействие представляет собой кортеж, содержащий три элемента:
- Пользователь
- элемент
- веса взаимодействия (необязательно)
Веса взаимодействия просто означают, что если пользователь «u» взаимодействовал с элементом «i», насколько важным является это взаимодействие. С точки зрения нашего примера, вес - это рейтинг, который у нас есть для каждой пары (пользователь, элемент).
Другой случай, когда установка весов может быть полезна, - это когда мы имеем дело с данными взаимодействия (пользователя, песни). В таком случае я мог бы назначить более высокий вес тем взаимодействиям, в которых пользователь слушал более 3/4 песни.
# plugging in the interactions and their weights (interactions, weights) = dataset1.build_interactions([(x[0], x[1], x[2]) for x in df.values ])
Таким образом, матрица
interactions
сообщает нам, взаимодействовал ли вообще пользователь с элементом, а матрицаweights
дает количественную оценку этого конкретного взаимодействия.
Мы можем проверить, как выглядят эти две выходные матрицы. Поскольку это разреженные матрицы, мы можем использовать метод .todense()
. В обеих матрицах строки - это пользователи, а столбцы - это элементы.
Если вы до сих пор со мной, то вам спасибо. Эта следующая часть чрезвычайно важна для понимания, поэтому вы можете реализовать ее и для своей собственной рекомендательной системы. (Интересный факт: значительная часть открытых проблем на странице LightFM Github каким-то образом связана с темой создания функций пользователя / элемента).
Создание пользовательских функций
Метод build_user_features
требует ввода в следующем формате:
[
(user1, [feature1, feature2, feature3,….]),
(user2, [feature1, feature2, feature3,…. ]),
(user3, [feature1, feature2, feature3,….]),
.
.
]
Здесь следует помнить одну очень важную вещь:
feature1
,feature2
,feature3
и т. д. должны быть одним из элементов, присутствующих в спискеuser_features
, который мы передали методуfit
в начале.
Повторюсь, наш список user_features
в настоящее время выглядит так: ['f1:1', 'f1:0', 'f2:1', 'f3:0', 'f3:1', 'loc:mum', 'loc:del']
.
Таким образом, для наших конкретных фиктивных данных ввод в build_user_features
должен быть таким:
[ ('u1', ['f1:1', 'f2:1', 'f3:0', 'loc:del']), ('u2', ['f1:0', 'f2:1', 'f3:0', 'loc:mum']), ('u3', ['f1:1', 'f2:1', 'f3:1', 'loc:del']) ]
Опять же, я написал (не такой уж маленький) фрагмент кода, который позволяет мне создать такой список:
# Helper function that takes the user features and converts them into the proper "feature:value" format def feature_colon_value(my_list): """ Takes as input a list and prepends the columns names to respective values in the list. For example: if my_list = [1,1,0,'del'], resultant output = ['f1:1', 'f2:1', 'f3:0', 'loc:del'] """ result = [] ll = ['f1:','f2:', 'f3:', 'loc:'] aa = my_list for x,y in zip(ll,aa): res = str(x) +""+ str(y) result.append(res) return result # Using the helper function to generate user features in proper format for ALL users ad_subset = features[["f1", 'f2','f3', 'loc']] ad_list = [list(x) for x in ad_subset.values] feature_list = [] for item in ad_list: feature_list.append(feature_colon_value(item)) print(f'Final output: {feature_list}')
Наконец, мы должны связать каждый элемент feature_list
с соответствующими идентификаторами пользователей.
user_tuple = list(zip(features.user, feature_list))
И вуаля, у нас есть желаемый ввод для метода build_user_features
. Давайте продолжим и назовем это:
user_features = dataset1.build_user_features(user_tuple, normalize= False)
В матрице user_features
, приведенной выше, строки - это пользователи, а столбцы - это пользовательские функции. 1 присутствует всякий раз, когда у этого пользователя есть эта конкретная пользовательская функция, присутствующая в данных обучения.
Всего мы видим 10 столбцов, что означает, что существует 10 пользовательских функций. Но почему 10, спросите вы, давайте посмотрим!
user_id_map, user_feature_map, item_id_map, item_feature_map = dataset1.mapping() user_feature_map
Если вы посмотрите на вывод выше, станет ясно, почему у нас 10 пользовательских функций. По умолчанию идентификатор пользователя также является функцией, поэтому у нас есть три из них. Остальные семь должны быть вам знакомы, поскольку мы создали их в самом начале.
Время построить модель
Этот шаг довольно простой и довольно общий. Вы, конечно, можете следовать документации и экспериментировать с разными loss
значениями или learning_schedule
параметрами.
model = LightFM(loss='warp') model.fit(interactions, user_features= user_features, sample_weight= weights, epochs=10)
Печать оценки AUC
from lightfm.evaluation import auc_score train_auc = auc_score(model, interactions, user_features=user_features ).mean() print('Hybrid training set AUC: %s' % train_auc) Output: Hybrid training set AUC: 0.9166667
Повторяю, не стоит слишком волноваться из-за высоких значений AUC. Помните, что это просто фиктивные данные.
Прогнозы для ИЗВЕСТНЫХ пользователей
Метод predict
принимает два входа:
- отображение идентификатора пользователя (например: для получения прогнозов для «u1» необходимо передать 0; для «u2» пройти 1 и т. д.). Эти сопоставления доступны из
user_id_map
dictionary. - список идентификаторов элементов (опять же, не i1, i2, а отображение; доступно из
item_id_map
), для которых вы хотите получить рекомендации.
# predict for existing user user_x = user_id_map['u3'] n_users, n_items = interactions.shape # no of users * no of items model.predict(user_x, np.arange(n_items)) # means predict for all Output: array([-0.18600112, -0.91691172, -0.295421 , -0.06632421])
Прогнозы для НЕИЗВЕСТНЫХ пользователей
Вот почему мы в первую очередь создавали гибридный рекомендатель.
Для нового пользователя это то, что мы знаем - у него есть значения для f1, f2, f3 как 1,1 и 0 соответственно. Кроме того, их местонахождение - Дели.
user_feature_list = ['f1:1', 'f2:1', 'f3:0', 'loc:del']
Теперь мы не можем передать это напрямую методу predict
. Мы должны преобразовать этот ввод в форму, понятную нашей модели lightFM.
В идеале входные данные должны выглядеть как одна из строк в матрице user_features
(см. Рисунок 1 выше).
Я нашел в Stackoverflow фрагмент кода, который делает именно это - преобразует user_feature_list
в требуемый формат (в нашем случае разреженная матрица с 10 столбцами). Я лишь немного изменил исходный код и заключил его в многоразовую функцию format_newuser_input
здесь:
from scipy import sparse def format_newuser_input(user_feature_map, user_feature_list): num_features = len(user_feature_list) normalised_val = 1.0 target_indices = [] for feature in user_feature_list: try: target_indices.append(user_feature_map[feature]) except KeyError: print("new user feature encountered '{}'".format(feature)) pass new_user_features = np.zeros(len(user_feature_map.keys())) for i in target_indices: new_user_features[i] = normalised_val new_user_features = sparse.csr_matrix(new_user_features) return(new_user_features)
Наконец, мы можем начать предсказывать нового пользователя:
new_user_features = format_newuser_input(user_feature_map, user_feature_list) model.predict(0, np.arange(n_items), user_features=new_user_features)
Здесь первый аргумент, то есть 0, больше не относится к сопоставленному идентификатору для пользователя u1. Вместо этого это означает - выберите первую строку разреженной new_user_features
матрицы. Передача любого значения, отличного от 0, вызовет ошибку, и это правильно, поскольку в new_user_features
нет строк за пределами row0.
Ура !! Мы сделали это. Наша система рекомендаций по фиктивному гибриду готова.
Другие способы создания функций пользователя / элемента
Возвращаясь к обещанию, которое я дал в начале, есть альтернативы построению user_features
матрицы, при условии , что ваши данные имеют определенный формат.
В качестве примера рассмотрим фрейм данных пользовательской функции, подобный приведенному ниже:
Если вы внимательно посмотрите, каждая из трех характеристик - этническая принадлежность, пол, моложе 30 лет - имеет неперекрывающиеся значения. Другими словами, возможные значения в столбце этнической принадлежности отличаются от возможных значений в столбце пола, которые, в свою очередь, отличаются от возможных значений в столбце до 30 лет.
Если это так, мы можем просто передать user_features
list формы [Asian, Hispanic, Female, Male, no, yes]
в fit
. Впоследствии мы должны также обновить способ создания наших пользовательских функций, и поэтому ввод в .build_user_features()
должен быть таким:
[ ('u1', ['Asian', 'Female', 'No']), ('u2', ['Hispanic', 'Male', 'Yes']), ('u3', ['Hispanic', 'Male', 'No']) ]
Выглядит отлично. Но теперь ответьте на это?
Что произойдет, если у меня будет одна дополнительная информация о каждом пользователе, то есть новый столбец «height_under_5ft», который может принимать значения «да» или «нет»?
В таких случаях может быть трудно отличить Да / Нет от возраста и Да / Нет от роста. Поэтому, чтобы избежать двусмысленности, было бы лучше использовать формат feature:value
, о котором мы говорили в начале, и передать user_features = ['eth:Asian', 'eth:Hispanic', 'gender:Male', 'gender:Female', 'age:yes', 'age:no', 'height:yes', 'height:no]
в fit
, а следующий ввод в .build_usear_features()
[ ('u1', ['eth:Asian', 'gender:Female', 'age:No','height:yes'
]), ('u2', ['eth:Hispanic', 'gender:Male', 'age:No','height:no'
]) ('u3', ['eth:Asian', 'gender:Female', 'age:No','height:yes'
]) ]
Конечная заметка
Есть много разных способов продвинуть это руководство вперед. Для начала попробуйте подключить свой реальный набор данных вместо этих фиктивных данных и посмотрите, насколько хорошо работает ваша рекомендательная система. Вы даже можете попробовать создать item_features
, используя аналогичную методологию, которую мы обсуждали в этом руководстве.
Я мало говорил о настройке модели, оценке модели, перекрестной проверке и т. Д., Но это в другой раз. Надеюсь, вам понравилось читать, и не стесняйтесь форкнуть мой код здесь, на Github, чтобы попробовать его в своих целях. Как всегда, мне любопытно узнать, есть ли лучшие способы сделать некоторые из вещей, о которых я упомянул.
До скорого :)
Чуть не забыл: «Если вам понравилась эта статья, возможно, вам понравится» (каламбур 😜)