Привет, Хабр!

Сегодня мы будем рассматривать один из самых мощных алгоритмов детектирования аномалий, который называется Isolation Forest.

Чтобы понять суть, представьте себе огромный лес. Вы идёте по нему, случайно выбирая направления. Чем быстрее вы натыкаетесь на редкое дерево, тем более аномальным оно является. Isolation Forest использует эту же идею, только вместо деревьев у нас данные, а вместо леса — решающие деревья (метафора века).

Основная идея

Обычные алгоритмы машинного обучения, например, SVM или нейросети, пытаются описать нормальное распределение данных, а затем искать выбросы. Isolation Forest идёт с другого конца: он не строит плотностную модель, а просто пытается изолировать выбросы.

Как это происходит:

  1. Строим дерево, где каждый узел случайно выбирает один признак и случайное значение разбиения.

  2. Рекурсивно делим данные, пока каждая точка не окажется в своём отдельном листе.

  3. Считаем аномальность точки по тому, насколько быстро она была изолирована (чем короче путь, тем аномальнее).

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

А теперь — код

Берём датасет транзакций и пробуем выловить подозрительные операции.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest

# Генерируем фейковый датасет транзакций
np.random.seed(42)
n_samples = 1000

# "Нормальные" транзакции
amounts = np.random.normal(loc=50, scale=10, size=n_samples)
durations = np.random.normal(loc=5, scale=2, size=n_samples)
num_items = np.random.normal(loc=3, scale=1, size=n_samples)

# Добавляем аномалии (очень большие суммы)
anomaly_size = 50
amounts[:anomaly_size] = np.random.normal(loc=300, scale=50, size=anomaly_size)
durations[:anomaly_size] = np.random.normal(loc=20, scale=5, size=anomaly_size)
num_items[:anomaly_size] = np.random.normal(loc=10, scale=3, size=anomaly_size)

df = pd.DataFrame({'amount': amounts, 'duration': durations, 'num_items': num_items})

Создали синтетический набор данных, где 50 транзакций явно аномальны (огромные суммы и длительность). Теперь научим модель находить их.

# Обучаем Isolation Forest
model = IsolationForest(contamination=0.05, random_state=42)  # 5% данных считаем аномальными
df['is_anomaly'] = model.fit_predict(df[['amount', 'duration', 'num_items']])

# Преобразуем -1 (аномалия) и 1 (норма) в 0/1
df['is_anomaly'] = df['is_anomaly'].map({1: 0, -1: 1})

# Теперь визуализация должна работать
plt.figure(figsize=(10,6))
plt.scatter(df['amount'], df['duration'], c=df['is_anomaly'], cmap='coolwarm', alpha=0.7)
plt.xlabel('Transaction Amount')
plt.ylabel('Transaction Duration')
plt.title('Isolation Forest Anomaly Detection')
plt.colorbar(label="Anomaly Score")
plt.show()
График
График

График показывает распределение транзакций по двум параметрам: сумма транзакции amount и длительность duration.

  1. Синие точки — нормальные транзакции (обычные суммы и короткая длительность).

  2. Красные точки — аномальные транзакции (очень большие суммы и продолжительное время выполнения).

  3. Цветовая шкала — теперь корректно подписана как "Anomaly Classification", отражая бинарную классификацию: 0 (норма) и 1 (аномалия).

Модель Isolation Forest успешно обнаружила 50 аномальных транзакций. Видно, что они сконцентрированы в области с высокими суммами (200–400) и длительностью свыше 15 секунд. Алгоритм правильно идентифицирует выбросы.

Как выжать максимум из Isolation Forest

Как и любой алгоритм, Isolation Forest зависит от гиперпараметров. Разберём, какие есть:

Гиперпараметр

Что делает

Как влияет

n_estimators

Количество деревьев в лесу

Больше деревьев → лучше результат, но дольше обучение

max_samples

Количество случайных выборок для каждого дерева

По умолчанию 256, можно уменьшить для скорости

contamination

Ожидаемая доля аномалий

Влияет на порог отсечения

max_features

Сколько признаков использовать в каждом дереве

Если данных мало, лучше уменьшить

Пример кастомной настройки:

model = IsolationForest(n_estimators=200, max_samples=500, contamination=0.02, max_features=2, random_state=42)
model.fit(df)
df['is_anomaly'] = model.predict(df)
df['is_anomaly'] = df['is_anomaly'].map({1: 0, -1: 1})

Лимиты Isolation Forest

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

Проблемы:

  1. Плохо работает при высокой размерности — если у вас тысячи фичей, деревья будут рубить наугад.

  2. Не учитывает временную зависимость — если у вас временные ряды, лучше взять LSTM или Prophet.

  3. Чувствителен к параметру contamination — если он задан неверно, можно либо не найти аномалии, либо переобучиться.

Если Isolation Forest не зашёл, можно попробовать:

  • LOF (Local Outlier Factor) — анализирует плотность соседей.

  • Autoencoders — если у вас нейросети в проде.

  • One‑Class SVM — иногда заходит лучше, но жрёт много ресурсов.

Как оценить качество Isolation Forest?

Супер, модель натренирована, но как понять, работает ли она хорошо? В отличие от задач классификации, тут нет размеченных данных, поэтому стандартные метрики, как accuracy или F1-score, не помогут.

Но есть способы валидировать аномалии:

Ручная проверка

Простой, но мощный метод:

  • Сортируем транзакции по аномалиям

  • Смотрим топ-100 подозрительных заказов

df.sort_values(by='is_anomaly', ascending=False).head(100)

Если в топе действительно подозрительные операции (очень большие суммы, странные страны) — модель работает.

Анализ "decision_function()"

Isolation Forest даёт оценку аномальности. Это не бинарный 0/1, а число от -0.5 до 0.5. Можно построить гистограмму и посмотреть, какие объекты попадают в «аномальные зоны»:

import pandas as pd
import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from joblib import dump

# Загружаем данные о заказах
df = pd.read_csv("orders.csv")

# Отбираем нужные признаки
num_features = ['amount', 'duration', 'num_items']
cat_features = ['payment_method', 'country']

# Заполняем пропуски
df[num_features] = df[num_features].fillna(df[num_features].median())
df[cat_features] = df[cat_features].fillna('Unknown')

# Кодируем категориальные признаки
df = pd.get_dummies(df, columns=cat_features, drop_first=True)

Если гистограмма показывает, что аномалии чётко отделены — модель работает хорошо.

Валидация на исторических данных

Допустим, у нас есть размеченные случаи мошенничества (например, chargebacks). Проверим, насколько модель их ловит:

fraud_orders = df[df['chargeback'] == 1]
fraud_detected = df[(df['chargeback'] == 1) & (df['is_anomaly'] == 1)]

print(f"Обнаружено {len(fraud_detected)} из {len(fraud_orders)} известных мошеннических заказов.")

Если процент высокий (>70%), значит, Isolation Forest делает свою работу.

Оптимизация скорости работы на больших данных

Isolation Forest хорош, но он не масштабируется линейно. Если у нас миллионы записей, стандартный fit() будет тормозить.

Решение 1: max_samples

Если данных много, лучше уменьшить max_samples. В sklearn по дефолту: max_samples = min(256, n_samples)

Можно задать вручную:

model = IsolationForest(n_estimators=100, max_samples=5000, contamination=0.01, n_jobs=-1, random_state=42)

Это позволит обрабатывать миллионы строк, не теряя качество.

Решение 2: n_jobs=-1

Используем все ядра процессора:

model = IsolationForest(n_estimators=200, max_samples=5000, contamination=0.01, n_jobs=-1)

Решение 3: GPU-ускорение

Isolation Forest нет в cuML, но можно переключиться на нейросети. Они работают на GPU и быстрее обрабатывают большие данные.

Привет, Хабр!

Isolation Forest — мощная штука, но одно дело натравить его на синтетические данные, и совсем другое — встроить в реальный бизнес. Давайте разберёмся, как он может спасти магазин элитных котиков от мошенников.

Кейс: магазин котиков и проблема подозрительных заказов

Представьте, что у вас есть онлайн‑магазин породистых котиков. Цены начинаются от 500$, но за некоторых экземпляров (особенно за британцев или мейн‑кунов) выкладывают и по 5000$.

Всё было бы хорошо, но...

  1. Появились странные заказы: клиент покупает 15 котиков сразу, платит с кредитки из Индонезии, доставка — на почту общего пользования в Новосибирске.

  2. Платёжные системы жалуются: повышенный риск chargeback'ов — клиенты заявляют, что не делали заказов.

  3. Подозрительные паттерны: одни и те же покупатели меняют имена, но используют один и тот же IP.

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

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

  1. Собираем данные о заказах.

  2. Тренируем Isolation Forest на нормальных покупателях.

  3. Выявляем аномальные транзакции и блокируем их.

Данные, с которыми работаем

Выглядит так:

order_id

customer_id

amount

num_cats

country

payment_method

ip_address

1

123

900

1

RU

Credit Card

192.168.1.2

2

456

5000

1

US

Bitcoin

172.34.56.78

3

789

10 000

15

ID

PayPal

36.72.11.55

4

234

300

1

RU

Credit Card

192.168.1.5

Что подозрительно:

  • num_cats > 10 (ну кто покупает сразу 10 котиков?)

  • amount > 5000 (редкие покупки на такие суммы)

  • country = ID (у нас редко покупают из Индонезии)

  • payment_method = Bitcoin (анонимность ≠ мошенничество, но повод проверить)

  • ip_address совпадает у разных пользователей

Подготовка данных:

import pandas as pd
import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from joblib import dump

# Загружаем данные заказов
df = pd.read_csv("cat_orders.csv")

# Выбираем нужные признаки
num_features = ['amount', 'num_cats']
cat_features = ['country', 'payment_method']

# Кодируем категориальные признаки (One-Hot Encoding)
df = pd.get_dummies(df, columns=cat_features, drop_first=True)

# Стандартизируем числовые признаки
scaler = StandardScaler()
df[num_features] = scaler.fit_transform(df[num_features])
model = IsolationForest(n_estimators=200, contamination=0.02, random_state=42)
model.fit(df[num_features])

df['anomaly_score'] = model.decision_function(df[num_features])
df['is_anomaly'] = model.predict(df[num_features])
df['is_anomaly'] = df['is_anomaly'].map({1: 0, -1: 1})

Какие заказы попали в аномалии?

suspicious_orders = df[df['is_anomaly'] == 1]
print(suspicious_orders)

Выводит что‑то типа:

order_id

amount

num_cats

is_anomaly

3

10 000

15

1

8

7500

12

1

И тут уже решаем: автоматически отклонять или отправлять на проверку менеджеру.

Улучшение модели

Isolation Forest хорош, но у него есть ограничения. Если просто натравить его на сырые данные — можно словить ложные срабатывания.

Признаки amount и num_cats — это хорошо, но что, если мошенник покупает по 1 котику, но делает это с 50 аккаунтов?

Решение: добавить customer_id в модель (например, кодируя частоту покупок).

df['customer_freq'] = df.groupby('customer_id')['order_id'].transform('count')

Теперь модель будет видеть, как часто человек делает заказы.

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

Решение: ввести обучение с частичным контролем.

Если у нас есть размеченные chargeback'и, можно сделать следующее:

# Обучаем модель только на нормальных заказах
normal_orders = df[df['chargeback'] == 0]
model.fit(normal_orders[num_features])

# Теперь смотрим аномалии
df['is_anomaly'] = model.predict(df[num_features])

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

Как это встраивается в магазин?

Окей, мы нашли аномалии. Теперь что? Надо встроить это в процессинг заказов:

  1. Клиент оформляет заказ.

  2. Данные проходят через Isolation Forest.

  3. Если is_anomaly == 1, заказ помечается как подозрительный.

  4. Скрипт отправляет админам уведомление.

Пример API на Flask:

from flask import Flask, request, jsonify
from joblib import load

app = Flask(__name__)

# Загружаем обученную модель
model = load("isolation_forest_model.joblib")

@app.route("/predict", methods=["POST"])
def predict():
    data = request.json
    df = pd.DataFrame([data])  # Преобразуем JSON в DataFrame
    df = scaler.transform(df)  # Нормализуем данные
    
    prediction = model.predict(df)
    is_anomaly = 1 if prediction[0] == -1 else 0
    
    return jsonify({"is_anomaly": is_anomaly})

if __name__ == "__main__":
    app.run(port=5000, debug=True)

Теперь магазин котиков получает предсказание аномалии за миллисекунды и может оперативно отменять подозрительные заказы.

А теперь слово вам! Как вы находите аномалии в своих данных? Используете Isolation Forest или предпочитаете что‑то другое? Давайте обсудим!


Всем, кому интересна тема машинного обучения, рекомендую посетить бесплатную онлайн-лекцию в Otus на тему «Практика работы с Docker — вывод модели в продакшн».

На вебинаре разберём контейнеризацию и работу с Docker: запустим контейнеры, освоим Dockerfile для упаковки ML-модели, создадим образ и развернём сервис. Также рассмотрим оркестраторы и построим микросервисную архитектуру с docker-compose. Записаться можно на странице курса "Machine Learning. Advanced".

Комментарии (1)


  1. CrazyElf
    13.02.2025 13:01

    • Зачем вы импортируете dump и Pipeline, вы же их не используете? Наверное, осталось от экспериментов.

    • И точно ли вам нужен StandardScaler, ведь любым деревянным моделям (то бишь решающим деревьям) вроде бы должно быть всё-равно какой масштаб фич?

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