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

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

Предпосылки:

· Аккаунт AWS. Стоимость выполнения всего руководства составит менее 0,50 доллара США, так что не беспокойтесь.

· Понимание Python. Сегодня большая часть работы по машинному обучению выполняется на Python.

· Терпение. Неудача — важнейшая предпосылка успеха, поэтому продолжайте пробовать, пока не получится.

Что такое AWS SageMaker?

Amazon SageMaker — это облачная платформа машинного обучения (как и ваш ноутбук Anaconda Jupiter, но в облачной среде), которая помогает пользователям создавать, обучать, настраивать и развертывать модели машинного обучения в готовой к работе размещенной среде.

Преимущества sagemaker: высокая масштабируемость, быстрое обучение, поддержание работоспособности — процесс продолжает работать без остановки, высокий уровень безопасности данных и т. д.

Теперь давайте начнем создавать нашу модель в SageMaker.

1: Настройка

Этот блокнот был создан и протестирован на AWS Sagemaker с экземпляром блокнота ml.m4.xlarge.

Создание экземпляра блокнота. Точно так же, как вы создаете блокнот Jupyter в своей системе, мы создадим блокнот Jupyter на нашей платформе. Ниже приведены шаги, чтобы сделать то же самое:

  • Войдите в консоль AWS SageMaker.
  • Нажмите Экземпляры записной книжки и выберите создание экземпляра записной книжки.

На следующей странице назовите свой блокнот, сохраните тип экземпляра как ml.m4.xlarge и выберите Роль IAM для вашего экземпляра.

Роль IAM (управление идентификацией и доступом). Короче говоря, корзины SageMaker и S3 — это сервисы, предоставляемые AWS. Нашему экземпляру ноутбука нужны данные, которые мы храним в корзине S3, для построения модели. Сервис не может напрямую обращаться к другому сервису в AWS. Поэтому необходимо указать роль, чтобы экземпляр записной книжки мог получить доступ к данным из корзины S3. Вы можете предоставить определенные корзины S3 или все корзины S3 для работы вашего экземпляра.

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

Вот оно. Ваш блокнот создан.

Знакомство с используемыми библиотеками. На этом занятии мы рассмотрим все библиотеки, необходимые для выполнения задачи:

import sagemaker

sess = sagemaker.Session()
bucket = sess.default_bucket()
prefix = "sagemaker/DEMO-xgboost-churn"

# Define IAM role
import boto3
import re
from sagemaker import get_execution_role

role = get_execution_role()

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

# Importing all required libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import io
import os
import sys
import time
import json
from IPython.display import display
from time import strftime, gmtime
from sagemaker.inputs import TrainingInput
from sagemaker.serializers import CSVSerializer

2: Данные

У операторов мобильной связи есть информация о том, какие потребители в конечном итоге ушли, а какие остались верны услуге. В процессе обучения мы можем применить эти прошлые данные для построения модели машинного обучения, чтобы узнать об оттоке мобильного оператора. После обучения модели мы можем предоставить ей данные профиля любого клиента (та же информация профиля, которую мы использовали для обучения модели), и будет прогнозироваться, уйдет ли этот клиент или нет. очевидно, мы ожидаем, что модель все-таки будет делать ошибки, предсказывать будущее — сложная задача! Но здесь мы также покажем вам, как бороться с ошибками в предсказании.

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

В данном случае данных немного, и они были загружены в виде одного файла с заголовком на AWS S3. Чтобы скопировать данные из S3, вот синтаксис:

!aws s3 cp s3://<patht to s3 file> ./
# Displaying all columns by setting max value
churn = pd.read_csv(“./churn.txt”)
pd.set_option(“display.max_columns”, 500)
churn

Выход:

Это небольшой набор данных, содержащий всего 5000 записей и 21 атрибут, используемый для представления профиля клиента анонимного оператора сотовой связи в США. Категории следующие:

  • State: штат США, в котором проживает клиент, обозначается двухбуквенной аббревиатурой; например, PA или WY
  • Account Length: количество дней, в течение которых эта учетная запись была активна.
  • Area Code: трехзначный код города соответствующего номера телефона клиента.
  • Phone: оставшийся семизначный номер телефона
  • Int’l Plan: есть ли у клиента международный тарифный план: да/нет
  • VMail Plan: есть ли у клиента функция голосовой почты: да/нет
  • VMail Message: предположительно среднее количество сообщений голосовой почты в месяц
  • Day Mins: общее количество минут звонков, использованных в течение дня
  • Day Calls: общее количество звонков, сделанных в течение дня
  • Day Charge: выставленная стоимость звонков в дневное время
  • Eve Mins, Eve Calls, Eve Charge: стоимость вызовов в вечернее время.
  • Night Mins, Night Calls, Night Charge: стоимость звонков в ночное время.
  • Intl Mins, Intl Calls, Intl Charge: тарифицируемая стоимость международных звонков
  • CustServ Calls: количество звонков в службу поддержки клиентов.
  • Churn?: ушел ли клиент из сервиса: true/false

Последний атрибут, Churn?, известен как целевой атрибут — атрибут, который мы хотим предсказать с помощью модели машинного обучения. Поскольку целевой атрибут является бинарным, наша модель будет выполнять бинарный прогноз.

Приступим к изучению данных:

# Displaying statistical info
churn.describe()

Столбец «Телефон» содержит уникальные номера, которые не связаны с другими значениями, поэтому мы можем удалить столбец «Телефон».

# Removing the columns not in use
churn = churn.drop(“Phone”, axis=1)
# converting 'area code' to object
churn[“Area Code”] = churn[“Area Code”].astype(object)

Сравнение каждой функции с целевой переменной, например, оттоком?

# let’s compare each feature with our target variable
for column in churn.select_dtypes(include=[“object”]).columns:
 if column != “Churn?”:
 display(pd.crosstab(index=churn[column], columns=churn[“Churn?”], normalize=”columns”))
# Histograms for each numeric features
for column in churn.select_dtypes(exclude=[“object”]).columns:
 print(column)
 hist = churn[[column, “Churn?”]].hist(by=”Churn?”, bins=30)
 plt.show()

Выход:

Гистограммы для каждого числового признака:

Отображение корреляции каждой функции и матрицы рассеяния

display(churn.corr())
pd.plotting.scatter_matrix(churn, figsize=(12, 12))
plt.show()

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

Давайте удалим по одному признаку из каждой из пар с высокой степенью корреляции: Дневная оплата из пары с дневными минутами,Eve Charge в паре с Eve Mins, Night Charge в паре с Night Mins, Международный платеж в паре с Intl Mins:

# Dropping few feature columns
churn = churn.drop([“Day Charge”, “Eve Charge”, “Night Charge”, “Intl Charge”], axis=1)

Давайте разберемся, какой алгоритм выбрать для очищенного набора данных. Как указывалось ранее, несколько переменных имеют как высокие, так и низкие значения, которые предсказывают отток. Чтобы учесть это в таком алгоритме, как линейная регрессия, нам нужно сгенерировать полином (или ) условия. Вместо этого давайте попробуем смоделировать эту проблему, используя деревья с усилением градиента. Amazon SageMaker предоставляет контейнер XGBoost, который можно использовать для обучения в управляемой распределенной среде, а затем разместить в качестве конечной точки прогнозирования в реальном времени.

Чтобы узнать больше об алгоритме XGBoost, посмотрите это видео.

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

# converting categorical features in numerical features
model_data = pd.get_dummies(churn)
model_data = pd.concat(
 [model_data[“Churn?_True.”], model_data.drop([“Churn?_False.”, “Churn?_True.”], axis=1)], axis=1
)

А теперь давайте разделим данные на наборы для обучения, проверки и тестирования. Это поможет предотвратить переоснащение модели и позволит нам проверить точность модели на данных, которые она еще не видела.

# spliting data into train, test and validation
train_data, validation_data, test_data = np.split(
 model_data.sample(frac=1, random_state=1729),
 [int(0.7 * len(model_data)), int(0.9 * len(model_data))],
)
train_data.to_csv(“train.csv”, header=False, index=False)
validation_data.to_csv(“validation.csv”, header=False, index=False)

Теперь мы загрузим эти файлы на S3

# uploading files to s3
boto3.Session().resource(“s3”).Bucket(bucket).Object(
 os.path.join(prefix, “train/train.csv”)
).upload_file(“train.csv”)
boto3.Session().resource(“s3”).Bucket(bucket).Object(
 os.path.join(prefix, “validation/validation.csv”)
).upload_file(“validation.csv”)

3: поезд

Переходя к обучению, сначала нам нужно указать расположение контейнеров алгоритма XGBoost.

# xgboost algorithms container
from sagemaker import image_uris
container = image_uris.retrieve(
 framework=”xgboost”, region=boto3.Session().region_name, version=”latest”
)

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

# Specifying the Training input path
s3_input_train = sagemaker.inputs.TrainingInput(
 s3_data=”s3://{}/{}/train”.format(bucket, prefix), content_type=”csv”
)
s3_input_validation = sagemaker.inputs.TrainingInput(
 s3_data=”s3://{}/{}/validation/”.format(bucket, prefix), content_type=”csv”
)

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

Вот несколько ключевых гиперпараметров: — max_depth определяет, насколько глубоко может быть построено каждое дерево в алгоритме. Более глубокие деревья могут привести к лучшему подбору, но требуют больших вычислительных затрат и могут привести к переобучению. Обычно существует некоторый компромисс в производительности модели, который необходимо исследовать между большим количеством мелких деревьев и меньшим количеством более глубоких деревьев. - subsample управляет выборкой обучающих данных. Этот метод может помочь уменьшить переоснащение, но слишком низкое значение также может лишить модель данных. - num_round контролирует количество раундов бустинга. По сути, это последующие модели, которые обучаются с использованием остатков предыдущих итераций. Опять же, большее количество раундов должно обеспечить лучшее соответствие обучающим данным, но может быть дорогостоящим в вычислительном отношении или привести к переобучению. -eta определяет, насколько агрессивным будет каждый раунд усиления. Большие значения приводят к более консервативному бустингу. -gamma определяет, насколько агрессивно растут деревья. Большие значения приводят к более консервативным моделям.

# adding XGBoost’s hyperparmeters
sess = sagemaker.Session()
xgb = sagemaker.estimator.Estimator(
 container,
 role,
 instance_count=1,
 instance_type=”ml.m5.4xlarge”, # change the instance type as per your need
 output_path=”s3://{}/{}/output”.format(bucket, prefix),
 sagemaker_session=sess,
)
xgb.set_hyperparameters(
 max_depth=5,
 eta=0.2,
 gamma=4,
 min_child_weight=6,
 subsample=0.8,
 silent=0,
 objective=”binary:logistic”,
 num_round=100,
)
xgb.fit({“train”: s3_input_train, “validation”: s3_input_validation})

4: Развернуть

Теперь, когда мы обучили алгоритм, давайте создадим модель и развернем ее на размещенной конечной точке. Нам также нужно настроить сериализаторы и десериализаторы для передачи наших массивов test_data NumPy в модель за конечной точкой.

# deploy the model to hosted endpoints
xgb_predictor = xgb.deploy(
    initial_instance_count=1, instance_type="ml.m5.4xlarge", serializer=CSVSerializer()
)

5: Оценить

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

# Evaluate
def predict(data, rows=500):
 split_array = np.array_split(data, int(data.shape[0] / float(rows) + 1))
 predictions = “”
 for array in split_array:
 predictions = “,”.join([predictions, xgb_predictor.predict(array).decode(“utf-8”)])
return np.fromstring(predictions[1:], sep=”,”)
predictions = predict(test_data.values[:, 1:])
print(predictions)

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

# creating a confusion matrix
pd.crosstab(
 index=test_data.iloc[:, 0],
 columns=np.round(predictions),
 rownames=[“actual”],
 colnames=[“predictions”],
)

Из 247 ушедших клиентов мы правильно предсказали 238 из них (истинные положительные результаты). Кроме того, мы неверно предсказали, что 18 клиентов уйдут, но в итоге этого не сделали (ложные срабатывания). Кроме того, есть 9клиентов, которые в конечном итоге ушли, хотя мы и предсказывали, что этого не произойдет (ложноотрицательные результаты).

Важным моментом здесь является то, что из-за приведенной выше функции np.round() мы используем простой порог (или отсечку) 0,5. Наши прогнозы из xgboost выводятся как непрерывные значения от 0 до 1, и мы помещаем их в бинарные классы, с которых начали. Однако, поскольку ожидается, что отток клиента будет стоить компании больше, чем упреждающие попытки удержать клиента, который, по нашему мнению, может уйти, нам следует рассмотреть возможность корректировки этого порога. Это почти наверняка увеличит количество ложных срабатываний, но также можно ожидать увеличения количества истинных срабатываний и уменьшения количества ложноотрицательных результатов.

Чтобы получить общее представление, давайте посмотрим на непрерывные значения наших прогнозов.

# continuous values of those predictions
plt.hist(predictions)
plt.show()

Прогнозы с непрерывным значением, полученные из нашей модели, имеют тенденцию к отклонению в сторону 0 или 1, но между 0,1 и 0,9 имеется достаточная масса, чтобы корректировка порога действительно должна сместить ряд прогнозов клиентов. Например…

# creating a confusion matrix
pd.crosstab(index=test_data.iloc[:, 0], columns=np.where(predictions > 0.3, 1, 0))

Мы видим, что изменение отсечки с 0,5 до 0,3 приводит к увеличению числа истинных положительных результатов на 6, ложных положительных результатов на 20 и уменьшению числа ложных отрицательных результатов на 6. В целом цифры здесь невелики, но в целом это 6–10% клиентов, которые уходят из-за изменения ограничения. Было ли это правильным решением? Определение оптимальных пороговых значений — ключевой шаг в правильном применении машинного обучения в реальных условиях. Давайте обсудим это более широко, а затем применим конкретное гипотетическое решение для нашей текущей проблемы.

6: Относительная стоимость ошибок

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

Что еще более важно, если мы запустим модель ML в производство, возникнут затраты, связанные с тем, что модель ошибочно присваивает ложные положительные и ложные отрицательные результаты. Нам также необходимо рассмотреть аналогичные затраты, связанные с правильными предсказаниями истинных положительных и истинных отрицательных результатов. Поскольку выбор точки отсечки влияет на все четыре из этих статистических данных, нам необходимо рассмотреть относительные затраты для бизнеса для каждого из этих четырех результатов для каждого прогноза.

7: Назначение затрат

Каковы затраты на нашу проблему оттока операторов мобильной связи? Затраты, конечно, зависят от конкретных действий, которые предпринимает бизнес. Сделаем здесь некоторые предположения.

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

Ложноотрицательные результаты являются наиболее проблематичными, потому что они неверно предсказывают, что уходящие клиенты останутся. Мы теряем клиента и должны будем оплатить все расходы по приобретению нового клиента, включая упущенный доход, расходы на рекламу, административные расходы, расходы на точки продаж и, вероятно, субсидию на аппаратное обеспечение телефона. Быстрый поиск в Интернете показывает, что такие расходы обычно исчисляются сотнями долларов, поэтому для целей этого примера давайте предположим 500 долларов США. Это цена ложноотрицательных результатов.

Наконец, для клиентов, которых наша модель идентифицирует как уходящих, давайте предположим поощрение за удержание в размере 100 долларов США. Если бы мой провайдер предложил мне такую ​​уступку, я бы, конечно, дважды подумал, прежде чем уйти. Это цена как истинно положительных, так и ложноположительных результатов. В случае ложных срабатываний (клиент доволен, но модель ошибочно предсказала отток) мы «потратим» уступку в размере 100 долларов США. Вероятно, мы могли бы потратить эти 100 долларов более эффективно, но, возможно, мы повысили лояльность уже лояльного клиента, так что это не так уж и плохо.

Нахождение оптимальной отсечки

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

$500 * FN(C) + $0 * TN(C) + $100 * FP(C) + $100 * TP(C)

FN(C) означает, что процент ложноотрицательных результатов является функцией отсечки, C, и аналогично для TN, FP и TP. Нам нужно найти отсечение C, при котором результат выражения наименьший.

Простой способ сделать это — просто запустить симуляцию для большого количества возможных отсечек. Мы тестируем 100 возможных значений в цикле for ниже.

# $500 * FN(C) + $0 * TN(C) + $100 * FP(C) + $100 * TP(C)
cutoffs = np.arange(0.01, 1, 0.01)
costs = []
for c in cutoffs:
    costs.append(
        np.sum(
            np.sum(
                np.array([[0, 100], [500, 100]])
                * pd.crosstab(index=test_data.iloc[:, 0], columns=np.where(predictions > c, 1, 0))
            )
        )
    )
costs = np.array(costs)
plt.plot(cutoffs, costs)
plt.show()
print(
    "Cost is minimized near a cutoff of:",
    cutoffs[np.argmin(costs)],
    "for a cost of:",
    np.min(costs),
)

мы указываем целевое семейство экземпляров, а также корзину S3, в которой будет храниться скомпилированная модель.

# Path to store complied model into S3
"/".join(xgb.output_path.split("/")[:-1])

8: Очистить версию Sagemaker

# rollback the SageMaker Python SDK to the kernel's original version
with open("orig_sm_version.txt", "r") as f:
    orig_sm_version = f.read()
print("Original version: {}".format(orig_sm_version))
print("Current version: {}".format(sagemaker.__version__))
s = "sagemaker=={}".format(orig_sm_version)
print("Rolling back to... {}".format(s))
%pip install --no-cache-dir -qU {s}
%rm orig_sm_version.txt

9: (необязательно) очистка

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

xgb_predictor.delete_endpoint()

Ссылки-