Аналитика будущего: предсказываем вероятность покупки в реальном времени

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

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

Итак, что если мы знаем вероятность покупки, пока пользователь еще не ушел с сайта?
Артём Козлов
Аналитик данных
Варианты использования
Автоматизированный поиск сессий пользователей, которым необходима помощь online-консультанта
Автоматическая генерация real-time рекомендаций при добавлении в систему информации о товарах
Автоматическая генерация специальных ценовых и бонусных предложений для удержания клиента
Данные
Посмотрим, какие данные у нас есть.
Что-то из них будет полезно, а что-то отбросим.
Временной слепок (tstamp)
Используем отдельные характеристики времени: календарный день, время, день недели.
Из этого атрибута выведем кумулятивную сумму длительности сессии, время с предыдущего действия.
ID сессии (session_id)
Будет использоваться, как группирующий объект для анализа совокупности действий пользователя.
Дата конкретного действия клиента (calday)
Дата в формате DD.MM.YYYY. Эта информация у нас уже есть из временного слепка, поэтому опустим.
Количество (cnt)
Значение-константа, равно «1». Не несет полезной информации, поэтому использовать не будем.
Платформа (platform) и Операционная система (os)
Знать устройство пользователя полезно. Используем в one-hot представлении.
Cookie пользователя (cookie)
Позволяет идентифицировать пользователя, с которым мы уже взаимодействовали.
Используем cookie для подсчета сессий покупателей.
Действие на сайте (action), Тип страницы (page type)
Комбинируем действия и тип страницы, получаем характеристику «действие со страницей».
Для модели воспользуемся one-hot представлением, благо таких пар всего 42.
Цель действия (target)
Действие над объектом (товаром). Полезно, т.к. дает дополнительный смысл действию в последовательности — используем. Благодаря низкой кардинальности признака, мы легко можем позволить себе старый добрый one-hot encoding.
ID товара (material), Описание товара (txtlg), Категория (category1), Подкатегория (category1), Брэнд (brand)
Говорит о товаре, над которым совершается действие. Атрибут обладает высокой кардинальностью: 27 279 уникальных товаров! Эта информация пригодится, однако нужно подумать о представлении данных для модели. Есть множество классических вариантов кодирования категориальных признаков с высокой кардинальностью. Но все они сводятся к группировке на более высокоуровневые категориальные признаки, статистическому представлению или снижению размерности. Нам же интересно низкоуровневое представление, позволяющее не потерять семантику каждого товара и учитывать близость (похожесть), основываясь не только на латентной статистике. На помощь может прийти подобный подход, но для корректной реализации понадобится дополнительная информация о характеристиках товаров. На данном этапе разумно оставить признаки, относящиеся к товару, неиспользованными.
Флаг промо (promo)
Отличный признак, очевидно влияющий на вероятность покупки, еще и бинарный!
Бережно донесем его до модели в первозданном виде.
Состав последовательностей
Представим нашу задачу как простейший пример бинарной классификации: значениями будет последовательность действий пользователя, целевой переменной — факт покупки.
В рамках одной сессии клиент мог совершить несколько последовательных покупок, поэтому сессию разобьем на несколько сегментов. Конечный элемент каждого сегмента — покупка или неявное завершение сессии. Считаем что на покупку № 2 не влияют действия, произведенные до покупки № 1 (сегменты не пересекаются).
В реальной жизни это, конечно, не совсем верное ограничение. Очевидно, что вероятность покупки чехла для смартфона очень зависит от того, купил ли человек ранее сам смартфон. Но в рамках текущей задачи мы отказались от использования информации о продуктах, поэтому обходимся тем, что имеем.

Далее промаркируем последовательности, которые привели к покупке. Делаем это на основании присутствия действия на странице purchase_ThankYouPage. Обязательно удалим из последовательностей записи с действиями, явно отражающими факт покупки: purchase_ThankYouPage, view_ThankYouPage.

Забегая вперед, скажу что, чтобы подать последовательности в нейронную сеть, нам нужно сформировать матрицы одинаковой размерности. Длины последовательностей варьируются от 1 до 813. Так какую же нам использовать? Сильно урезая реальную длину последовательностей мы можем потерять часть данных. Мы можем легко пожертвовать 1,5% данных — возьмем квантиль 0.985. Более длинные последовательности выглядят как аномалии. Скорее всего это просто мониторинг цен, который не может привести к покупке. Таким образом у нас получается максимальную длину последовательности в 54 действия. Для коротких последовательностей мы заполняем недостающие строки нулями и получаем каждую в виде n_rows * m_features (54, 70).
Минутка теории.

За основу мы взяли общий опыт решения задач NLP (Natural Language Processing) через сверточные нейросети. На высоком уровне это хорошо описано в одной из самых цитируемых работ — «Convolutional Neural Networks for Sentence Classification», Yoon Kim.
Артём Козлов
Аналитик данных
Архитектура модели с двумя каналами на примере одного предложения:
Автор использовал сверточные слои с фильтрами разной размерности, чтобы выделить наборы последовательностей слов, в совокупности отражающие смысл целевой переменной. В данном случае — сентиментальный окрас. Успех обеспечило изящное математическое решение, позволившее учитывать близость и последовательность слов в предложении. Структура любого языка определяется правилами грамматики. Любое предложение можно разделить на граф определенной сложности по членам предложения. Почему бы не использовать этот подход в других задачах, где последовательность действий пользователя можно разложить на граф, а достижение цели явно определяется этой последовательностью?

Попробуем!
Эксперимент
В целях эксперимента построим базовую модель с размерностью фильтра 3. Воспользуемся популярным фреймворком Keras.

Наша модель будет состоять из сверточного слоя (1D), pooling-слоя, полносвязного слоя и слоя-бинарного классификатора на конце. Также используем Dropout-слои, чтобы избежать переобучения.

Архитектура сети отображена на рисунке справа.

model_1h = Sequential()
model_1h.add(InputLayer(input_shape=(max_len, max_width)))
model_1h.add(Conv1D(72, 3, activation='relu' , padding='valid'))
model_1h.add(Dropout(0.33))
model_1h.add(GlobalMaxPooling1D()))
model_1h.add(Dense(32, activation='relu'))
model_1h.add(Dropout(0.33))
model_1h.add(Dense(1, activation='sigmoid'))
model_1h.compile(loss='binary_crossentropy', optimizer='adam' , metrics=['acc'])
Что может базовая модель?
Чтобы ускорить процесс мы использовали стандартный hold-out сплит с отношением 80/20. Модели быстро обучаются даже на CPU. Высокая точность достигается всего за две эпохи:
Trian on 347198 samples, validate on 86778 samples
Epoch 1/2
347108/347108 [===============] - 149s - loss: 0.0829 - acc: 0.9683 - val_loss: 0.0520 - val_acc: 0.9783
Epoch 2/2
347108/347108 [===============] - 149s - loss: 0.0495 - acc: 0.9772 - val_loss: 0.0478 - val_acc: 0.9790
Имея несбалансированные классы, воспользуемся метриками precision, recall и f-score:
print('CNN')
print(classification_report(y_test, \
                prediction_cnn_1h[:] > 0.5))
CNN
                   precision         recall      f1-score         support

             0          1.00          0.98            0.99          83527
             1          0.65          0.88            0.74           3251

avg / total             0.98          0.98            0.98          86778
Построим матрицу ошибок для интерпретации результатов:
Можно улучшить?
В классификации последовательностей модель показала хорошие результаты. Но для нас это не самое главное. Нужно убедиться, что модель научилась определять наборы действий, ведущих к покупке. За счет визуализации «возбуждающихся» нейронов можем выработать определенную интуицию.
На графике видно, какие фильтры вносят наибольший вклад в классификацию последовательности.

Ура, базовая модель оказалась рабочей. Улучшаем результаты? Конечно! Снова обратимся к работе Yoon Kim и применим к нашей модели несколько фильтров различной размерности.

# CNN
plt.imshow(model_1h.get_weights()[2])
<matplotlib.image.AxesImage at 0x7f54a8347320>
Используем аналогичную модель, но с тремя входами. Каждый из входов будет обрабатывать идентичные данные фильтрами размерностью 2, 3 и 4 соответственно. Затем используем в полносвязном слое информацию из каждого из них.

Архитектура будет выглядеть так:
Что получилось?
К сожалению, усложнение нашего эксперимента лишь немного улучшило результаты и не стоит увеличения времени на обучение. Можно говорить об изначально верном интуитивном выборе фильтра, который вносит наибольший вклад.
print('CNN_multi_input')
print(classification_report(y_test, prediction_cnn_1h[:] > 0.5))
CNN_multi_input
                   precision         recall      f1-score         support

             0          0.99          0.98            0.99          83589
             1          0.66          0.87            0.75           3189

avg / total             0.98          0.98            0.98          86778
Использование модели
Используем обученную таким образом модель для моделирования ситуации на новой сессии реального клиента. Как это выглядит?
С каждым действием пользователя можем итеративно снимать показатели модели. Результаты могут стать триггерами для предиктивных действий.
Попробуем
это смоделировать
Возьмем из тестовой выборки несколько случайных примеров, каждый из которых закончился покупкой. Чтобы смоделить использование в реальном времени, начнем с момента, когда пользователь совершил только одно действие.

Далее будем итеративно добавлять по одному действию и фиксировать предсказания модели. Полученные результаты для наглядности добавим к необработанным данным. Атрибут «proba» содержит вероятностное значение покупки в рамках текущей сессии. Оно основано на анализе данных с первой по текущую строку включительно.
Вариант поведения №1: «Пришел, увидел, победил»
Простой случай, в котором модель реагирует на действия и временные расстояния между ними. С такими пользователями не нужно ничего делать, просто дайте им купить то, что они хотят.
Вариант поведения №2: «Сомнения → покупка»
Вариант поведения №3: «Сомнения → Отказ»
После чемпионата мы продолжаем развивать наше решение. В первую очередь с точностью помогут три направления:
Дополнение информацией из пользовательских Cookie
Дополнение информацией о товрах
Учёт зависимости от предыдущих покупок в рамках сессии