В заключительной четвертой части статьи подробно обсудим наиболее сложную с технической точки зрения часть antifraud-сервиса – аналитическую систему распознания мошеннических платежей по банковским картам.
Выявление различного рода мошенничеств является типичным кейсом для задач обучения с учителем (supervised learning), поэтому аналитическая часть антифрод-сервиса, в соответствии с лучшими отраслевыми практиками, будет построена с использованием алгоритмов машинного обучения.
Для стоящей перед нами задачи воспользуемся Azure Machine Learning – облачным сервисом выполнения задач прогнозной аналитики (predictive analytics). Для понимания статьи будут необходимы базовые знания в области машинного обучения и знакомство с сервисом Azure Machine Learning.
Во 2-ой части были описаны требования технического и нетехнического характера, которые предъявляются к таким системам, и то, как я собираюсь снизить стоимость разработки и владения antifraud-системы на порядок(и).
В 3-ей части была рассмотрена программная архитектура сервиса, его модульная структура и ключевые детали реализации.
В заключительной четвертой части у нас следующая цель…
Цель
В этой части я опишу проект, на первом шаге которого мы обучим четыре модели, используя логистическую регрессию, персептрон, метод опорных векторов и дерево решений. Из обученных моделей выберем ту, которая дает большую точность на тестовой выборке и опубликуем ее в виде REST/JSON-сервиса. Далее для полученного сервиса напишем программного клиента и проведем нагрузочное тестирование на REST-сервис.
Создание модели
Создадим новый эксперимент в Azure ML Studio. В конечном виде он будет выглядеть так, как показано на иллюстрации ниже. Соотнесем каждый элемент эксперимента с этапом последовательности, который среднестатистический data scientist проделывает в процессе обучения модели.
Рассмотрим каждый из этапов создания модели распознания мошеннических платежей, принимая во внимания технические детали, описанные в прошлой 3-ей части статьи.
Гипотеза
Основные концепции и предположения, полезные для создания модели, были обсуждены в первых 2-ух частях статьи. Повторяться не буду, лишь отмечу, что создание хорошей гипотезы – это итеративный процесс проб и ошибок, фундаментом для которого являются знания как в исследуемой предметной области, так и в области Data Science.
Получение данных
Набором данных для модели распознания мошеннических платежей будет являться лог транзакций, который состоит из 2-ух таблиц в NoSQL-хранилище (Azure Table): таблицы фактов о транзакциях TransactionsInfo и таблицы с предварительно рассчитанными статистическими метриками TransactionsStatistics.
На этапе получения данных загрузим эти 2 таблицы через элемент управления Reader.
Подготовка и исследование данных
Сделаем Inner Join загруженных таблиц по полю TransactionId. C помощь элемента управления Metadata Editor укажем типы данных (string, integer, timestamp), отметим столбец с ответами (label) и столбцы с предикторами (features), а также тип шкалы у этих данных: номинальные, абсолютные.
Не стоит недооценивать важность подготовки для создания адекватной модели: приведу простой пример с валютой платежа, которая храниться в виде ISO-кодов (целочисленное значение). ISO-коды – имеет номинальную (классификационную) шкалу. Но вряд ли стоит надеяться, что система автоматически определит, что в столбце «Currency» хранится не целочисленное значение с абсолютной шкалой (т.е. возможны такие операции как + или >). Потому что это слишком неочевидное правило, знаниями о котором система не обладает.
Набор данных может содержать пропущенные значения. В нашем случае, страну или IP-адрес плательщика не всегда есть возможность определить, такие поля могут содержать пустые значения. Проверив имеющийся набор данных, заменим пустые значения стран на «undefined» с помощью элемента управления Clean Missing Data. С помощью этого же элемента управления удалим строки, где в поле держатель карты, сумма платежа или валюта не содержатся значения, как строки, содержащие заведомо некорректные данные, то есть вносящие шум в модель.
На следующем этапе избавимся от неиспользуемых в модели полей: адрес (нас интересует только совпала ли страна плательщика со страной, откуда пришел запрос), хэш имени держателя карта (т.к. не имеет никакого влияния на результат платежа), RowId и PartitionId (служебные данные, попавшие к нам из Azure Table).
В заключении с помощью элемента управления Normalize Data проведем ZScore-нормализацию данных, содержащих большие числовые значения, такие как сумма платежа (столбец TransactionAmount).
Деление данных
Поделим получившийся набор данных на обучающую и тестовую выборку. Выберем оптимальное соотношение данных в обучающей выборки и в тестовой. Для наших целей с помощью элемента управления Split «отправим» 70% всех имеющихся данных в обучающую выборку, дополнительно включив произвольное смешивание данных (флаг Randomized split) при делении на поднаборы данных. Смешивание данных при делении позволит избежать «перекосов» в обучающей выборке, связанных с большими утечками номеров пластиковых карт (и, как следствие, аномальной активностью фрод-роботов в этой период).
Построение и оценка модели
Инициализируем несколько алгоритмов классификации и сравним какой из них дает лучший результат (точность) на тестовой выборке. Важно отметить, что совсем не факт, что на реальных данных будет достигнута та же производительность, что и тестовых данных. Поэтому очень важно понять, что в модели было не учтено, почему один из алгоритмов дает существенно худший или лучший результат, исправить ошибки и запустить алгоритм обучения заново. Этот процесс, заканчивается тогда, когда исследователь получает приемлемую по точности модель.
Azure ML позволяет нам подключать в одном эксперименте неограниченное количество алгоритмов машинного обучения. Это дает возможность на этапе исследования сравнить производительность нескольких алгоритмов с целью выявления того, который из них наилучшим образом подходит для нашей задачи. В нашем эксперименте мы используем несколько алгоритмов двуклассовой классификации: Two-Class Logistic Regression (логистическая регрессия), Two-Class Boosted Decision Tree (дерево решений, построенное методом градиентного роста), Two-Class Support Vector Machine (метод опорных векторов), Two-Class Neural Network (нейросеть).
Еще одна возможность получить лучшую производительность модели – настроить алгоритм машинного обучения, используя большое число параметров доступных для настройки алгоритма. Так для алгоритма Two-Class Boosted Decision Tree было указано количество деревьев, которое необходимо построить, а также минимальное/максимальное количество листьев на каждом дереве; для алгоритма Two-Class Neural Network количество скрытых узлов, итераций обучения и начальные веса.
На заключительном этапе просмотрим выходные данные элемента управления Evaluate Model (команда Visualize из контекстного меню элемента) для каждого из алгоритмов.
Элемент управления Evaluate Model содержит матрицу неточностей (confusion matrix), рассчитанные показатели точности работы алгоритма Accuracy, Precision, Recall, F1 Score, AUC, графики ROC и Precision/Recall. Если говорить упрощенно, то мы выберем алгоритм, чьи значения Accuracy, Precision, AUC ближе к 1, график ROC сильнее вогнут в сторону оси Y как для обучающей, так и для тестовой выборки.
Кроме того, непроходимо посмотреть на изменение AUC в зависимости от устанавливаемого значения Threshold. В случае с фродом – это важно, так как стоимость нераспознанных мошеннической платежей (False Positive) намного выше, чем стоимость платежей, ошибочно принятых за фрод (False Negative).
В таких случаях необходимо выбирать значение Threshold отличное от значения по умолчанию 0,5.
При выборе наиболее подходящего алгоритма для получения оптимальной модели распознания фрода, кроме уровня Threshold, учтем и тот факт, что логику принятия решения для некоторых алгоритмов (например, дерево решений) возможно воспроизвести, а для некоторых нет (персептрон). Наличие такой возможности может быть критичным, если важно знать, почему по определенному прецеденту система приняла конкретное решение.
Лучшую точность показал алгоритм двуклассовой нейронной сети – Two-Class Neural Network (показатели точности показаны на иллюстрации выше), за ним алгоритм на основе деревьев решений — Two-Class Boosted Decision Tree.
Публикация модели как веб-сервиса
После того, как была получена модель, работающая с требуемой точностью, опубликуем наш эксперимент как веб-сервис. Операция публикации проходит по нажатию кнопки «Publish Web Service» в Azure ML Studio. Процесс создания веб-сервиса из эксперимента тривиален и его описание я пропущу.
В результате Azure ML развернет масштабируемый отказоустойчивый (SLA 99,95%) web-сервис. После публикации сервиса станет доступна страница документации по API сервиса – API help, которая кроме общего описания сервиса, описания форматов ожидаемых входных и выходных сообщений, содержит еще и примеры вызова сервиса на C#, Python и R.
Принцип вызова сервиса программным клиентом можно изобразить так.
Подключение к Azure ML web-сервису
Возьмем пример на C# из API help и, немного изменив его, вызовем web-сервис Azure ML.
Листинг 1. Вызов web-сервиса Azure ML
private async Task<RequestStatistics> InvokePredictorService(TransactionInfo transactionInfo, TransactionStatistics transactionStatistics)
{
Contract.Requires<ArgumentNullException>(transactionInfo != null);
Contract.Requires<ArgumentNullException>(transactionStatistics != null);
var statistics = new RequestStatistics();
var watch = new Stopwatch();
using (var client = new HttpClient())
{
var scoreRequest = new
{
Inputs = new Dictionary<string, StringTable>() {
{
"transactionInfo",
new StringTable()
{
ColumnNames = new []
{
#region Column name list
},
Values = new [,]
{
{
#region Column value list
}
}
}
},
},
GlobalParameters = new Dictionary<string, string>()
};
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ConfigurationManager.AppSettings["FraudPredictorML:ServiceApiKey"]);
client.BaseAddress = new Uri("https://ussouthcentral.services.azureml.net/workspaces/<workspace_id>/services/<service_id>/execute?api-version=2.0&details=true");
watch.Start();
HttpResponseMessage response = await client.PostAsJsonAsync("", scoreRequest);
if (response.IsSuccessStatusCode)
await response.Content.ReadAsStringAsync();
statistics.TimeToResponse = watch.Elapsed;
statistics.ResponseStatusCode = response.StatusCode;
watch.Stop();
}
return statistics;
}
Получим следующие запрос/ответ:
Листинг 2.1. Запрос к web-сервису Azure ML
POST https://ussouthcentral.services.azureml.net/workspaces/<workspace_id>/services/<service_id>/execute?api-version=2.0&details=true HTTP/1.1
Authorization: Bearer <api key>
Content-Type: application/json; charset=utf-8
Host: ussouthcentral.services.azureml.net
/* другие заголовки */
{
"Inputs": {
"transactionInfo": {
"ColumnNames": [
"PartitionKey",
"RowKey",
"Timestamp",
"CardId",
"CrmAccountId",
"MCC",
"MerchantId",
"TransactionAmount",
"TransactionCreatedTime",
"TransactionCurrency",
"TransactionId",
"TransactionResult",
"CardExpirationDate",
"CardholderName",
"CrmAccountFullName",
"TransactionRequestHost",
"PartitionKey (2)",
"RowKey (2)",
"Timestamp (2)",
"CardsCountFromThisCrmAccount1D",
"CardsCountFromThisCrmAccount1H",
"CardsCountFromThisCrmAccount1M",
"CardsCountFromThisCrmAccount1S",
"CardsCountFromThisHost1D",
"CrmAccountsCountFromThisCard1D",
"FailedPaymentsCountByThisCard1D",
"SecondsPassedFromPreviousPaymentByThisCard1D",
"PaymentsCountByThisCard1D",
"HostsCountFromThisCard1D",
"HasHumanEmail",
"HasHumanPhone",
"IsCardholderNameIsTheSameAsCrmAccountName",
"IsRequestCountryIsTheSameAsCrmAccountCountry",
"TransactionDayOfWeek",
"TransactionLocalTimeOfDay"
/* значения прочие предикторы */
],
"Values": [
[
"990",
"f31f64f367644b1cb173a48a34817fbc",
"2015-03-15T20:54:28.6508575Z",
"349567471",
"10145",
"32",
"990",
"136.69",
"2015-03-15T20:54:28.6508575Z",
"840",
"f31f64f367644b1cb173a48a34817fbc",
null,
"2015-04-15T23:44:28.6508575+03:00",
"640ab2bae07bedc4c163f679a746f7ab7fb5d1fa",
"640ab2bae07bedc4c163f679a746f7ab7fb5d1fa",
"20.30.30.40",
"990",
"f31f64f367644b1cb173a48a34817fbc",
"2015-03-15T20:54:28.6508575Z",
"2",
"1",
"0",
"0",
"0",
"0",
"1",
"2",
"0",
"0",
"true",
null,
"true",
"true",
"Monday",
"Morning"
/* значения прочих предикторов */
]
]
}
},
"GlobalParameters": { }
}
Листинг 2.2. Ответ web-сервиса Azure ML
HTTP/1.1 200 OK
Content-Length: 1619
Content-Type: application/json; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
x-ms-request-id: f8cb48b8-6bb5-4813-a8e9-5baffaf49e15
Date: Sun, 15 Mar 2015 20:44:31 GMT
{
"Results": {
"transactionPrediction": {
"type": "table",
"value": {
"ColumnNames": [
"PartitionKey",
"RowKey",
"Timestamp",
"CardId",
"CrmAccountId",
"MCC",
"MerchantId",
"TransactionAmount",
"TransactionCreatedTime",
"TransactionCurrency",
"TransactionId",
/* значения прочие предикторы */
"Scored Labels",
"Scored Probabilities"
],
"Values": [
[
"990",
"f31f64f367644b1cb173a48a34817fbc",
"2015-03-15T20:54:28.6508575Z",
"349567471",
"10145",
"32",
"990",
"136.69",
"2015-03-15T20:54:28.6508575Z",
"840",
"f31f64f367644b1cb173a48a34817fbc",
/* значения прочих предикторов */
"Success",
"0.779961256980896"
]
]
}
}
}
}
Нагрузочное тестирование
Для целей нагрузочного тестирования воспользуемся IaaS-возможностями Azure – поднимем виртуальную машину (Instance A8: 8x CPU, 56Gb RAM, 40Gbit/s InfiniBand, Windows Server 2012 R2, $2.45/hr) в том же регионе (US Central South), в котором находиться наш Azure ML web-сервис. Запустим на VM задачу на ~20K запросов и посмотрим на результаты.
Листинг 3. Код клиента сервиса и задачи
/// <summary>
/// Entry point
/// </summary>
public void Main()
{
var client = new FraudPredictorMLClient();
RequestsStatistics invokeParallelStatistics = client.InvokeParallel(1024, 22);
LogResult(invokeParallelStatistics);
RequestsStatistics invokeAsyncStatistics = client.InvokeAsync(1024).Result;
LogResult(invokeAsyncStatistics);
}
private static void LogResult(RequestsStatistics statistics)
{
Contract.Requires<ArgumentNullException>(statistics != null);
Func<double, string> format = d => d.ToString("F3");
Log.Info("Results:");
Log.Info("Min: {0} ms", format(statistics.Min));
Log.Info("Average: {0} ms", format(statistics.Average));
Log.Info("Max: {0} ms", format(statistics.Max));
Log.Info("Count of failed requests: {0}", statistics.FailedRequestsCount);
}
/// <summary>
/// Client for FraudPredictorML web-service
/// </summary>
public class FraudPredictorMLClient
{
/// <summary>
/// Async invocation of method
/// </summary>
/// <param name="merchantId">Merchant id</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="merchantId"/></exception>
public async Task<RequestsStatistics> InvokeAsync(int merchantId)
{
Contract.Requires<ArgumentOutOfRangeException>(merchantId > 0);
IEnumerable<TransactionInfo> tis = null; IEnumerable<TransactionStatistics> tss = null;
// upload input data
Parallel.Invoke(
() => tis = new TransactionsInfoRepository().Get(merchantId),
() => tss = new TransactionsStatisticsRepository().Get(merchantId)
);
var inputs = tis
.Join(tss, ti => ti.TransactionId, ts => ts.TransactionId, (ti, ts) => new { TransactionInfo = ti, TransactionStatistics = ts })
.ToList();
// send requests
var statistics = new List<RequestStatistics>(inputs.Count);
foreach (var input in inputs)
{
RequestStatistics stats = await InvokePredictorService(input.TransactionInfo, input.TransactionStatistics).ConfigureAwait(false);
statistics.Add(stats);
}
// return result
return new RequestsStatistics(statistics);
}
/// <summary>
/// Parallel invocation of method (for load testing purposes)
/// </summary>
/// <param name="merchantId">Merchant id</param>
/// <param name="degreeOfParallelism">Count of parallel requests</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="merchantId"/></exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="merchantId"/></exception>
public RequestsStatistics InvokeParallel(int merchantId, int degreeOfParallelism)
{
Contract.Requires<ArgumentOutOfRangeException>(merchantId > 0);
Contract.Requires<ArgumentOutOfRangeException>(degreeOfParallelism > 0);
IEnumerable<TransactionInfo> tis = null; IEnumerable<TransactionStatistics> tss = null;
// upload input data
Parallel.Invoke(
() => tis = new TransactionsInfoRepository().Get(merchantId),
() => tss = new TransactionsStatisticsRepository().Get(merchantId)
);
var inputs = tis
.Join(tss, ti => ti.TransactionId, ts => ts.TransactionId, (ti, ts) => new { TransactionInfo = ti, TransactionStatistics = ts })
.ToList();
// send requests
var statistics = new List<RequestStatistics>(inputs.Count);
for (int i = 0; i < inputs.Count; i = i + degreeOfParallelism)
{
var tasks = new List<Task<RequestStatistics>>();
for (int j = i; j < i + degreeOfParallelism; j++)
{
if (inputs.Count <= j) break;
var input = inputs[j];
tasks.Add(InvokePredictorService(input.TransactionInfo, input.TransactionStatistics));
}
Task.WaitAll(tasks.ToArray());
statistics.AddRange(tasks.Select(t => t.Result));
}
// return result
return new RequestsStatistics(statistics);
}
/* other members */
}
Вызов InvokeParallel():
Лучшее время ответа: 421.683 ms
Худшее время: 1355.516 ms
Среднее время: 652.935 ms
Количество успешных запросов: 21017
Количество отказов: 956
Вызов InvokeAsync():
Лучшее время ответа: 478.102 ms
Худшее время: 1344.348 ms
Среднее время: 605.911 ms
Количество успешных запросов: 21017
Количество отказов: 0
Ограничения (потенциальные)
Бутылочным горлышком разрабатываемой системы, на первый взгляд, будет являться Azure ML. Поэтому крайне важно понимать ограничения Azure ML в общем и web-сервисов Azure ML, в частности. Но по данному вопросу очень мало как официальной документации, так и результатов, полученных от community.
Так остается открытым вопрос с throttled policy конечных точек web-сервиса Azure ML: не ясно максимальное значение параллельных запросов web-сервису Azure ML (эмпирически проверена цифра 20 параллельных запросов на одну конечную точку), а также максимальный размер принимаемого сообщения (актуального для пакетного режима работы сервиса).
Менее актуально, но стоит вопрос с максимальным размером входных данных (Criteo Labs разместили датасет на 1 Тб данных), максимальным количеством предикторов и прецедентов, которые можно отдать на вход алгоритму машинного обучения в Azure ML.
Критически важно сократить время ответа веб-сервиса FraudPredictorML, а также время переобучения модели до минимальных значений, но пока нет никаких официальных рекомендаций по тому, как это возможно сделать (и возможно ли вообще).
Рекомендации клиентам
Антифрод-сервис никак не ограничивает клиентов как в предварительной проверке платежей, так и в последующей интерпретации результатов предсказания. Предварительные специфичные для бизнес-процесса проверки, а также окончательное принятие решения о принятии/отклонение платежа – это задачи, которые явно выходят из зоны ответственности антифрод-сервиса.
В независимости о роли клиента – интернет-магазин, платежная система или банк – для клиентов существуют следующие рекомендации:
- выполняйте предварительную проверку платежей, как используя технологии, принятые в отрасли (fingerprint и т.п.), так и использую собственные знания о клиенте (историю заказов и т.п.);
- интерпретируйте результат, применяя следующую практику: вероятность фрода ниже 0,35 – принимать оплату без 3D-Secure, вероятность от 0,35 до 0,85 – принимайте оплату с включенным 3DS, вероятность фрода – более отказывайте;
- выбирайте уровни, предложенные в предыдущем пункте, на основе собственной аналитики и регулярно пересматривайте их (минимизируйте упущенные выгоды и штрафы за фрод).
Заключение
В этом цикле, состоящем из 4-ех статей, мы провели эксперимент по проектированию и разработке высокомасштабируемого отказоустойчивого надежного antifraud-сервиса, работающего в near real-time режиме, с открытым для внешних программным клиентов REST/JSON API.
Применение алгоритмов машинного обучения (дерево решений, нейросети) позволили создать аналитическую систему, способную к самообучению как на накопленной истории, так и на новых платежах. Благодаря использованию PaaS-/IaaS-сервисов удалось сократить первоначальные финансовые затраты на инфраструктуру и ПО практически до нуля. Наличие у разработчика компетенций в предметной области, data science, архитектуре распределенных систем помогло драматически снизить количество участников команды разработки.
В результате менее чем за 60 человеко-часов и с минимальными начальными затратами на инфраструктуру (<$150, которые были покрыты из подписки MSDN) удалось создать ядро антифрод-системы.
Получившийся сервис, конечно, требует еще тщательной проверки (и последующего исправления) основных модулей, более тонкой настройки работы классификатора(ов), разработки серии вспомогательных подсистем, интереса и (что тут греха таить) инвестиций. Но и несмотря на указанные выше недоработки, сервис на порядок (и больше) эффективнее аналогичных разработок в отрасли как с точки зрения стоимости разработки, так и с точки зрения стоимости владения.
Другие части статьи
Если Вам осталось неясно в чем проблема (часть 1).
Если Вы не пропустили, почему проблему фрода долго и дорого сложно решить (часть 2).
Если Вам интересно, как это выглядит с точки зрения программной архитектуры (часть 3).
Дмитрий Петухов,
Software Architect & Developer, Big Data Enthusiast, Microsoft Certified Professional
архитектор, разработчик, энтузиаст, неутомимый исследователь и кофеман
Автор: codezombie