Предоставил: Йерун Франс

(Эта статья впервые появилась в бюллетене Data Science Briefings, информационном бюллетене DataMiningApps. Подпишитесь сейчас бесплатно, если хотите получать наши тематические статьи первыми, или подпишитесь на нас @DataMiningApps.)

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

Мошенничество — редкое явление. Из 284 807 транзакций только 492 были мошенническими. Это соответствует уровню заболеваемости 0,172%. Это означает, что существует сильный дисбаланс классов. Набор данных содержит 28 числовых признаков (V1 — V28), которые являются результатом преобразования PCA (по соображениям конфиденциальности). Помимо этих функций также регистрируется время (выраженное в секундах после первого наблюдения), а также транзакция.

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

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

df <- read_csv("Data/creditcard.csv", 
               col_types = list(Time = col_number()))
df_train <- df %>% 
  filter(row_number(Time) <= 230000) %>% select(-Time)
df_test <- df %>% 
  filter(row_number(Time) > 230000) %>% select(-Time)
## Picking joint bandwidth of 0.0309

Мы собираемся использовать автоэнкодеры для обнаружения случаев мошенничества. Для этого мы будем использовать только немошеннические обучающие данные, чтобы изучить функцию кодирования данных. Когда мы применяем это к проверочному набору, мы ожидаем увидеть, что мошеннические транзакции плохо реконструируются и, следовательно, могут быть обнаружены. Поскольку автоэнкодер — это тип нейронной сети, очень важно сначала нормализовать ваши данные. Вы можете сделать это, перемасштабировав данные в диапазон [0,1] или [-1,1] или стандартизировав их так, чтобы они были нормально распределены со средним значением 0 и стандартным отклонением 1. В R мы можем использовать пакет Caret который имеет функцию предварительной обработки. С помощью этой функции мы создаем преобразователь с нашими обучающими данными, который мы можем применить к «будущим» точкам данных или набору тестов.

minMaxScale <- df_train %>% select(-Class) %>%
  preProcess(method = "range") 
  # use c("center", "scale") for standardization
x_train <- predict(minMaxScale, df_train) %>%
  select(-Class) %>%
  as.matrix()
x_test <- predict(minMaxScale, df_test) %>%
  select(-Class) %>%
  as.matrix()
y_train <- df_train$Class
y_test <- df_test$Class

Далее нам нужно спроектировать архитектуру модели нейронной сети. В этом примере мы будем использовать Keras для определения архитектуры нашей модели и TensorFlow для выполнения реальных вычислений. Здесь мы будем использовать три скрытых слоя. Один с 12 нейронами, один узкий слой из 6 и один слой из 12 снова. Для функции активации мы будем использовать функцию гиперболического тангенса. Любая нелинейная функция должна работать здесь нормально, и вы также можете использовать линейные функции. Вы также можете поиграть с другими функциями активации, такими как ReLU и сигмоид. Если бы вы использовали функцию идентификации в качестве активации, то автоэнкодер работал бы аналогично анализу PCA.

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

После определения модели мы компилируем ее и обучаем. Используемая функция потерь представляет собой среднеквадратичную ошибку. Это имеет смысл, поскольку мы хотим свести к минимуму расстояние между нашими входными данными и реконструированным выходом. Для оптимизации использовался adam.

model <- keras_model_sequential()
model %>%
  layer_dense(units = 12, activation = "tanh", input_shape = ncol(x_train)) %>%
  layer_dense(units = 6, activation = "tanh") %>%
  layer_dense(units = 12, activation = "tanh") %>%
  layer_dense(units = ncol(x_train))
summary(model)
model %>% compile(
  loss = "mean_squared_error",
  optimizer = "adam"
)
history <- model %>% fit(
  x = x_train[y_train == 0,],
  y = x_train[y_train == 0,],
  epochs = 30,
  batch_size = 256,
  validation_data = list(x_test[y_test == 0,], x_test[y_test == 0,])
)

Если мы теперь посмотрим на ошибку реконструкции (MSE) в тестовом наборе, мы увидим, что есть несколько довольно экстремальных выбросов. Большинство ошибок находятся в диапазоне от 0,01 до 0,025, но есть и более крупные случаи, достигающие 14. График плотности также показывает разницу в ошибках реконструкции между немошенническими транзакциями (красный цвет) и мошенничеством (синий цвет). Мы видим, что существует другое распределение с точки зрения ошибки реконструкции:

pred_train <- predict(model, x_train)
mse_train <- apply((x_train - pred_train)^2, 1, sum)
pred_test <- predict(model, x_test)
mse_test <- apply((x_test - pred_test)^2, 1, sum)

То, как мы справимся с этим результатом, может зависеть от доступных ресурсов. Один очень грубый подход — взять 200/500/. . . наблюдения с самой высокой ошибкой реконструкции в нашем тестовом наборе или, возможно, 0,1% с самой высокой ошибкой реконструкции. Второй подход заключается в том, чтобы найти хорошее пороговое значение на основе точности, отзыва или других показателей оценки. Третий подход состоит в том, чтобы найти оптимальное по стоимости значение отсечки. Это означает, что нам придется указать стоимость расследования транзакции, стоимость незамеченного случая мошенничества и, возможно, также стоимость ложной тревоги. Попробуем первый подход:

top_200 <- plotdata %>% arrange(desc(mse_test)) %>% top_n(200)
## Selecting by mse_test
incidence_rate <- sum(top_200$y_test)/200

Мы видим, что в 200 самых высоких есть уровень заболеваемости 19%. По сравнению с набором данных в целом, уровень мошенничества в котором составлял 0,17%, это уже значительное улучшение.

Второй подход заключается в создании компромисса на основе значений точности и полноты модели:

possible_k <- seq(0, 1, length.out = 100)
precision <- sapply(possible_k, function(k) {
  predicted_class <- as.numeric(mse_test > k)
  sum(predicted_class == 1 & y_test == 1)/sum(predicted_class)
})
ggplot(data=as.data.frame(cbind(possible_k,precision)), aes(x = possible_k, y = precision)) +
geom_line(color="steelblue") + xlab("k treshold")

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

recall <- sapply(possible_k, function(k) {
  predicted_class <- as.numeric(mse_test > k)
  sum(predicted_class == 1 & y_test == 1)/sum(y_test)
})
ggplot(data=as.data.frame(cbind(possible_k,recall)), aes(x = possible_k, y = recall)) +
geom_line(color="steelblue") + xlab("k treshold")

На практике неправильные прогнозы могут быть сопряжены с затратами. Это может превратить проблему сокращения
в проблему оптимизации затрат. Каждая проверка транзакции может иметь фиксированную стоимость, так как сотруднику
придется тратить на это время. Предположим, что эта стоимость составляет 2,5 евро. Стоимость необнаружения случая мошенничества также будет
иметь цену, а именно сумму денег мошеннической транзакции. Тогда проблема изменится на:

avg_cost_per_check <- 2.5
lost_money <- sapply(possible_k, function(k) {
  predicted_class <- as.numeric(mse_test > k)
  sum(avg_cost_per_check * predicted_class + (predicted_class == 0) * y_test * df_test$Amount)
})
ggplot(data = as.data.frame(cbind(possible_k,lost_money)), aes(x = possible_k, y = lost_money)) +
geom_line(color="steelblue") + xlab("k treshold") + ylab("Cost")

Теперь оптимальное отсечение будет 0,101. Вы также можете сделать функцию стоимости более сложной: также может быть стоимость ложных
сигналов тревоги, где y_test == 0 и предсказанный_класс == 1. Клиенты могут уйти, если их карта слишком часто блокируется без причины. Какова будет цена потери клиента?

Это начальное исследование и некоторые функции стоимости вдохновлены сообщением в блоге Д. Фалбеля, которое включает дополнительный раздел о настройке гиперпараметров. В Python рабочий процесс будет выглядеть очень похоже: модель обучается с той же настройкой TensorFlow/Keras, а нормализацию можно выполнить с помощью scikit-learn. Код для этого поста можно найти в этом репозитории Gitlab.