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

Введение

Неотрицательная матричная факторизация (NMF) — очень мощный алгоритм, который исторически использовался во многих различных областях. Он применялся в астрономии, анализе текста, биоинформатике и ядерной визуализации (см. Здесь для других приложений). В этой статье я объясню и продемонстрирую, как можно эффективно применять NMF в рекомендательной системе. Используя реализацию sklearn на Python, я применю алгоритм к набору данных книг, чтобы предоставить читателям список книг, которые они еще не читали.

Прежде чем перейти к примеру с Python, я кратко рассмотрю шаги алгоритма. Если вы хотите получить более подробную информацию и объяснение математики, лежащей в основе этого метода, посетите мою предыдущую статью: Рекомендуемые продукты с NMF.

Алгоритм

Предположим, у нас есть матрица V, в которой строки обозначают идентификаторы книг, столбцы — идентификаторы пользователей, а значения матрицы — числа от 1 до 10, указывающие, как пользователи оценили книги, где 10 — наилучшая возможная оценка. Алгоритм NMF разлагает матрицу V на две неотрицательные матрицы W и H в соответствии с приведенными ниже функциями:

Эти новые матрицы W и H могут предоставить нам новую информацию о скрытых факторах, которые не обязательно видны в исходных данных. Затем мы можем восстановить V, взяв произведение W и H, чтобы получить WH. Эта новая матрица WH даст нам представление о вероятных предпочтениях пользователей, о которых нам не могли рассказать исходные данные. В нашем примере мы получим представление о том, насколько высоко пользователь может оценить книгу, которую он раньше не читал. В этом сила NMF!

Пример Python

В этом примере мы будем использовать набор данных Book-Crossing, найденный на Kaggle.

Начнем с импорта необходимых пакетов. В этом примере мы будем использовать sklearn реализацию NMF.

import numpy as np
import pandas as pd
from sklearn.metrics import mean_squared_error
from sklearn.decomposition import NMF

Затем мы загрузим данные, которые мы скачали с сайта Kaggle.

users_master = pd.read_csv('Users.csv', sep=';')
books_master = pd.read_csv('Books.csv', sep=';')
ratings_master = pd.read_csv('Ratings.csv', sep=';')

Чтобы сократить размер набора данных и ускорить наш пример, мы будем хранить только книги, которые оценили более 20 пользователей, и только пользователей, которые оценили более 3 книг. Есть много других способов, которыми мы могли бы очистить этот набор данных, но для этого примера мы продолжим и начнем подготовку к вводу данных в алгоритм NMF.

ratings = ratings_master.copy()

# Keep books with more than 20 ratings
book_rating_group = ratings.groupby(['ISBN']).count()
book_rating_group = book_rating_group[book_rating_group['Rating']>20]
ratings = ratings[ratings['ISBN'].isin(book_rating_group.index)]

# Keep users that have rated more than 3 books
user_rating_group = ratings.groupby(['User-ID']).count()
user_rating_group = user_rating_group[user_rating_group['Rating']>3]
ratings = ratings[ratings['User-ID'].isin(user_rating_group.index)]

# Apply to the books and users datasets
books = books_master.copy()
books = books[books['ISBN'].isin(ratings['ISBN'])]
users = users_master.copy()
users = users[users['User-ID'].isin(ratings['User-ID'])]

Затем мы объединим наборы данных в одну большую матрицу V с идентификаторами книг в строках и идентификаторами пользователей в столбцах. Значения матрицы будут соответствующими рейтингами. В нашем примере мы будем использовать 0, чтобы указать, что книга не была оценена пользователем. Фактические рейтинги книг будут ненулевыми. Обратите внимание, что для запуска этого блока кода на моем компьютере потребовалось несколько минут.

cols = np.concatenate((['ISBN'],user_ids))
df = pd.DataFrame(columns=cols)
book_ids = books['ISBN']
df['ISBN'] = book_ids
df['ISBN'] = df['ISBN'].astype(str)

# Fill the df with the ratings from the Ratings.csv file
for i in range(ratings.shape[0]):
    user_id = ratings['User-ID'].iloc[i]
    book_id = ratings['ISBN'].iloc[i]
    rating = ratings['Rating'].iloc[i]
    row = df[df['ISBN']==book_id].index
    if len(row)>0:
        row = row[0]
        df.loc[row, user_id] = rating

df.columns = df.columns.astype(str)

# We will use this original_df later to verify whether or not a user has read a certain book
original_df = df.copy()
original_df = original_df.set_index('ISBN')
original_df.fillna('No Ranking', inplace=True)

# Replace NaN's with 0 to indicate books with no ratings
df.fillna(0,inplace=True)
df = df.set_index('ISBN')
df

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

def rank_calculation(data=df):
    """
    Calculate the optimal rank of the specified dataframe.
    """
    # Read the data
    df = data
    
    # Calculate benchmark value
    benchmark = np.linalg.norm(df, ord='fro') * 0.0001
    
    # Iterate through various values of rank to find optimal
    rank = 3
    while True:
        
        # initialize the model
        model = NMF(n_components=rank, init='random', random_state=0, max_iter=500)
        W = model.fit_transform(df)
        H = model.components_
        V = W @ H
        
        # Calculate RMSE of original df and new V
        RMSE = np.sqrt(mean_squared_error(df, V))
        
        if RMSE < benchmark:
            return rank, V
        
        # Increment rank if RMSE isn't smaller than the benchmark
        rank += 1

    return rank

# Hardcode the value so that we don't have to run the above function
#optimal_rank = rank_calculation()
optimal_rank = 15 

Теперь, когда мы рассчитали оптимальный рейтинг, мы готовы запустить модель NMF и построить нашу матрицу рекомендаций WH.

# Decompose the dataset using sklearn NMF
model = NMF(n_components=optimal_rank)
model.fit(df)

H = pd.DataFrame(model.components_)    
W = pd.DataFrame(model.transform(df))    
V = pd.DataFrame(np.dot(W,H), columns=df.columns)
V.index = df.index
V

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

user_id = '276521'
# Grab top 10 books ID's that the user hasn't reviewed
user_col = V[user_id]
user_col = user_col.sort_values(ascending=False)

top_10_ISBN = []
for book in user_col.index:
    if original_df[user_id].loc[book] == 'No Ranking': # haven't read the book
        top_10_ISBN.append(book)

    if len(top_10_ISBN) == 10:
        break

top_10_ISBN

# Return the titles and authors of the recommended books
books_df = books.set_index('ISBN')
books_df.index = books_df.index.astype(str)

top_10_books = []
for book in top_10_ISBN:
    top_10_books.append([books_df['Title'].loc[book], books_df['Author'].loc[book]])

top_10_books = pd.DataFrame(top_10_books, columns=['Title','Author'])

print(f'Top 10 Book Recommendations for user {user_id}:')
top_10_books

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

Заключение

Благодарим вас за то, что следили за этим пошаговым примером рекомендателя NMF в Python. NMF — очень мощный алгоритм, и я надеюсь, что вы рассмотрите возможность его использования в своих будущих проектах! Пожалуйста, подпишитесь, чтобы узнать больше о математике и науке о данных!