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

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

Вступление

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

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

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

Подготовьте данные для обучения

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

event_1: <customer_1, movie_1, fail>
event_2: <customer_1, movie_2, fail>
event_3: <customer_1, movie_3, success>
event_4: <customer_2, movie_2, fail>
event_5: <customer_2, movie_3, success>
…

Список можно интерпретировать следующим образом: customer_1 видел movie_1 и movie_2, но решил не покупать. Потом посмотрел movie_3 и решил купить фильм. Точно так же customer_2 посмотрел movie_2, но решил не покупать. Потом посмотрел movie_3 и решил купить.

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

Наши необработанные данные фильмов выглядят так:

movie_data.dtypes
title object
release_date datetime64[ns]
unknown int64
Action int64
Adventure int64
Animation int64
Children’s int64
Comedy int64
Crime int64
Documentary int64
Drama int64
Fantasy int64
Film-Noir int64
Horror int64
Musical int64
Mystery int64
Romance int64
Sci-Fi int64
Thriller int64
War int64
Western int64
ratings_average float64
ratings_count int64
price float64
dtype: object

а это пример фильма из набора данных:

‘title’, ‘release_date’, ‘unknown’, ‘Action’, ‘Adventure’, ‘Animation’, “Children’s”, ‘Comedy’, ‘Crime’, ‘Documentary’, ‘Drama’, ‘Fantasy’, ‘Film-Noir’, ‘Horror’, ‘Musical’, ‘Mystery’, ‘Romance’, ‘Sci-Fi’, ‘Thriller’, ‘War’, ‘Western’, ‘ratings_average’, ‘ratings_count’, ‘price’
‘Toy Story (1995)’, Timestamp(‘1995–01–01 00:00:00’), 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3.8783185840707963, 452, 7.0

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

movie_data[‘buy_probability’] = 1 — movie_data[‘price’] * 0.1

С этой функцией вероятности покупки наш идеальный рейтинг должен выглядеть так:

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

class User:
    def __init__(self, id):
        self.id = id
        self.positive = []
        self.negative = []
        
    def add_positive(self, movie_id):
        self.positive.append(movie_id)
    
    def add_negative(self, movie_id):
        self.negative.append(movie_id)
    
    def get_positive(self):
        return self.positive
    
    def get_negative(self):
        return self.negative

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

def build_learning_data_from(movie_data):
    feature_columns = np.setdiff1d(movie_data.columns, np.array(['title', 'buy_probability']))
    learning_data = movie_data.loc[:, feature_columns]
    
    scaler = StandardScaler()
    learning_data.loc[:, ('price')] = scaler.fit_transform(learning_data[['price']])
    learning_data['ratings_average'] = scaler.fit_transform(learning_data[['ratings_average']])
    learning_data['ratings_count'] = scaler.fit_transform(learning_data[['ratings_count']])
    learning_data['release_date'] = learning_data['release_date'].apply(lambda x: x.year)
    learning_data['release_date'] = scaler.fit_transform(learning_data[['release_date']])
    
    return learning_data

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

np.random.seed(1)
class EventsGenerator:
    NUM_OF_OPENED_MOVIES_PER_USER = 20
    NUM_OF_USERS = 1000
def __init__(self, learning_data, buy_probability):
        self.learning_data = learning_data
        self.buy_probability = buy_probability
        self.users = []
        for id in range(1, self.NUM_OF_USERS):
            self.users.append(User(id))
        
    def run(self):
        for user in self.users:
            opened_movies = np.random.choice(self.learning_data.index.values, self.NUM_OF_OPENED_MOVIES_PER_USER)
            self.__add_positives_and_negatives_to(user, opened_movies)
return self.__build_events_data()
def __add_positives_and_negatives_to(self, user, opened_movies):
        for movie_id in opened_movies:
            if np.random.binomial(1, self.buy_probability.loc[movie_id]): 
                user.add_positive(movie_id)
            else:
                user.add_negative(movie_id)
                
    def __build_events_data(self):
        events_data = []
        
        for user in self.users:
            for positive_id in user.get_positive():
                tmp = learning_data.loc[positive_id].to_dict()
                tmp['outcome'] = 1
                events_data += [tmp]
            
            for negative_id in user.get_negative():
                tmp = learning_data.loc[negative_id].to_dict()
                tmp['outcome'] = 0
                events_data += [tmp]
                
        return pd.DataFrame(events_data)

и так все склеивается. EventsGenerator принимает нормализованные данные фильма и использует вероятность покупки для генерации пользовательских событий.

learning_data = build_learning_data_from(movie_data)
events_data = EventsGenerator(learning_data, movie_data['buy_probability']).run()

А вот как выглядит одно из этих событий:

'Action', 'Adventure', 'Animation', "Children's", 'Comedy', 'Crime', 'Documentary', 'Drama', 'Fantasy', 'Film-Noir', 'Horror', 'Musical', 'Mystery', 'Romance', 'Sci-Fi', 'Thriller', 'War', 'Western', 'outcome', 'price',    'ratings_average', 'ratings_count', 'release_date', 'unknown'
1,        1,           0,           0,            0,        0,       0,             0,       0,         0,           0,        0,         0,         0,         0,        0,          0,     1,         0,         0.28363692, 0.16953213,        -0.14286941,     0.39397757,     0

В этом случае мы имеем отрицательный результат (значение 0), а функции были нормализованы и центрированы по нулю в результате того, что мы сделали в функции build_learning_data_from (movie_data).

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

Обучаем наших моделей

Теперь, когда у нас есть наши события, давайте посмотрим, насколько хорошо наши модели изучают (простую) функцию `buy_probability`. Мы разделим наши данные на набор для обучения и тестирования, чтобы измерить производительность модели (но убедитесь, что вы знаете, как работает перекрестная проверка), и воспользуемся этой универсальной функцией для печати производительности различных моделей.

def train_model(model, prediction_function, X_train, y_train, X_test, y_test):
    model.fit(X_train, y_train)
    
    y_train_pred = prediction_function(model, X_train)
print('train precision: ' + str(precision_score(y_train, y_train_pred)))
    print('train recall: ' + str(recall_score(y_train, y_train_pred)))
    print('train accuracy: ' + str(accuracy_score(y_train, y_train_pred)))
y_test_pred = prediction_function(model, X_test)
print('test precision: ' + str(precision_score(y_test, y_test_pred)))
    print('test recall: ' + str(recall_score(y_test, y_test_pred)))
    print('test accuracy: ' + str(accuracy_score(y_test, y_test_pred)))
    
    return model

обучение различных моделей с помощью scikit-learn теперь просто вопрос склейки. Начнем с логистической регрессии:

def get_predicted_outcome(model, data):
    return np.argmax(model.predict_proba(data), axis=1).astype(np.float32)
def get_predicted_rank(model, data):
    return model.predict_proba(data)[:, 1]
model = train_model(LogisticRegression(), get_predicted_outcome, X_train, y_train, X_test, y_test)

что дает нам следующую производительность

train precision: 0.717381689518
train recall: 0.716596235113
train accuracy: 0.717328291166
test precision: 0.720525676086
test recall: 0.726374636238
test accuracy: 0.721590909091

Мы можем сделать то же самое, используя нейронную сеть и дерево решений. Это нейронная сеть с 23 входами (столько же, сколько в фильмах) и 46 нейронами в скрытом слое (обычное практическое правило - удваивать нейроны скрытого слоя).

from nolearn.lasagne import NeuralNet
def nn():
    return NeuralNet(
        layers=[  # three layers: one hidden layer
            ('input', layers.InputLayer),
            ('hidden', layers.DenseLayer),
            ('output', layers.DenseLayer),
            ],
        # layer parameters:
        input_shape=(None, 23),  # this code won't compile without SIZE being set
        hidden_num_units=46,  # number of units in hidden layer
        output_nonlinearity=None,  # output layer uses identity function
        output_num_units=1,  # this code won't compile without OUTPUTS being set
# optimization method:
        update_learning_rate=0.01, 
        regression=True,  # If you're doing classification you want this off
        max_epochs=50,  # more epochs can be good, 
        verbose=1, # enabled so that you see meaningful output when the program runs
    )
def get_predicted_outcome(model, data):
    return np.rint(model.predict(data))
def get_predicted_rank(model, data):
    return model.predict(data)

и это спектакль, который мы получили

model = train_model(
 nn(), 
 get_predicted_outcome, 
 X_train.astype(np.float32), 
 y_train.astype(np.float32), 
 X_test.astype(np.float32), 
 y_test.astype(np.float32)
)
train precision: 0.698486217804
train recall: 0.687534749249
train accuracy: 0.65721971972
test precision: 0.667556742323
test recall: 0.679655641142
test accuracy: 0.636136136136

и, наконец, с деревьями решений

def get_predicted_outcome(model, data):
    return np.argmax(model.predict_proba(data), axis=1).astype(np.float32)
def get_predicted_rank(model, data):
    return model.predict_proba(data)[:, 1]

что дает нам следующую производительность

from sklearn import tree
model = train_model(tree.DecisionTreeClassifier(), get_predicted_outcome, X_train, y_train, X_test, y_test)
train precision: 0.680947848951
train recall: 0.711256135779
train accuracy: 0.653892069603
test precision: 0.668242778542
test recall: 0.704538759602
test accuracy: 0.644044702235

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

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

Что дальше

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

Если вы предпочитаете носить шляпу ученого, вы также можете запустить блокнот Jupyter на Github с другой формулой для buy_probability и посмотреть, насколько хорошо модели способны уловить основную истину. Я пробовал линейную комбинацию нелинейных функций цены и рейтинга, и она работала одинаково хорошо с аналогичными уровнями точности.

price_component = np.sqrt(movie_data['price'] * 0.1)
ratings_component = np.sqrt(movie_data['ratings_average'] * 0.1 * 2)
movie_data['buy_probability'] = 1 - price_component * 0.2 - ratings_component * 0.8

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

pair_event_1: <customer_1, movie_1, fail, movie_3, success>
pair_event_2: <customer_2, movie_2, fail, movie_3, success>
pair_event_3: <customer_3, movie_1, fail, movie_2, success>
...

В таком примере вы могли догадаться, что хороший рейтинг будет movie_3, movie_2, movie_1, поскольку выбор различных клиентов приводит к полному упорядочиванию нашего набора фильмов. Несмотря на то, что предсказание попарных результатов имеет точность, аналогичную приведенным выше примерам, придумать глобальное упорядочение для нашего набора фильмов оказалось непросто (NP - сложный, как показано в этой статье из лабораторий AT&T), и мы будем Приходится прибегать к жадному алгоритму ранжирования, который влияет на качество конечного результата. Более подробное описание этого подхода доступно в этом сообщении блога от Julien Letessier.

Заключение

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

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

Изначально опубликовано в Альфредо Мотта.