Автоэнкодер: как нейросеть учится понимать норму. data analysis.. data analysis. Data Mining.. data analysis. Data Mining. data science.. data analysis. Data Mining. data science. python.. data analysis. Data Mining. data science. python. автоэнкодер.. data analysis. Data Mining. data science. python. автоэнкодер. ИИ.

Введение

Непосвящённому человеку кажется, что нейронная сеть может всё.
Средства массовой информации этот миф только подпитывают, а где-то в недрах Голливуда Джеймс Камерон шепчет:
«Я не режиссёр — я пророк».

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

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

Автоэнкодер: как нейросеть учится понимать норму - 1

1. Базовая идея автоэнкодера

Идея автоэнкодера на удивление проста.
Он учится сжимать данные, а затем восстанавливать их обратно, стараясь потерять как можно меньше информации.

Всё, что восстановилось плохо, — потенциальные аномалии.

Формально автоэнкодер состоит из двух частей:

1.      Encoder — превращает входные данные x в компактное представление z

2.      Decoder — пытается восстановить исходные данные x^ из  z Упрощённая схема выглядит так: x → Encoder → z → Decoder → x̂ Обучение предельно честное:
мы просто минимизируем разницу между входом и выходом. Важно подчеркнуть:
таргет — это сам вход.
Никаких классов, никаких ярлыков.

2. Зачем вообще нужен автоэнкодер

«Ну как зачем?» — спросит читатель. — «Ты же сам сказал: искать аномалии».

И он будет прав. Но это слишком широкий ответ.

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

·         изображения → латентные векторы

·         транзакции → поведенческие профили

·         тексты → семантические представления

Но для нас здесь важно другое.

Как автоэнкодер ищет аномалии

·         мы обучаем автоэнкодер только на нормальных данных

·         он учится хорошо восстанавливать «привычный мир»

·         аномальные объекты он восстанавливает плохо

·         ошибка реконструкции становится метрикой аномальности

И принципиальный момент:
это не классификация и не кластеризация.

Автоэнкодер не отвечает на вопрос:

«К какому классу ты относишься?»

Он отвечает на более фундаментальный вопрос:

«Насколько ты вписываешься в привычную структуру мира?»

3. Почему автоэнкодер не является копиром

Автоэнкодер не копирует данные из входа в выход — это было бы глупо и бесполезно.
Если бы он просто научился делать x→x, мы могли бы вообще не использовать нейросети и обойтись проводом. Поэтому у автоэнкодера есть ключевое ограничение — бутылочное горлышко: dim(z)≪dim Мы сознательно лишаем модель возможности сохранить всё.

Представьте себе:

·        200 признаков поведения пользователя

·         и всего 10 чисел, чтобы всё это описать

Физически невозможно сохранить каждую деталь. Приходится обобщать. Именно здесь автоэнкодер перестаёт быть копиром и становится моделью.

Энкодер, по сути, отвечает на вопрос:

«Какие несколько чисел лучше всего описывают то, что здесь происходит?»

Он не задаётся вопросом, что означают конкретные признаки. Его интересует структура, стоящая за ними.

Шум, разовые всплески и случайные отклонения просто не помещаются в такое представление.
А вот то, что повторяется и выглядит устойчивым, — остаётся.

4. Почему одного бутылочного горлышка недостаточно

На бумаге всё выглядит красиво:
маленький латентный вектор, сильное сжатие, здравый смысл.

На практике нейросеть — существо изворотливое.

Она может:

·         выучить странные нелинейные кодировки

·         запомнить частные случаи

·         аккуратно протащить шум через латентное пространство

·         и в итоге восстанавливать почти всё, включая то, что мы не хотели

Формально ошибка реконструкции будет маленькой.
А по смыслу — модель нас обманула.

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

Чтобы автоэнкодер действительно учил структуру данных, а не хитрил,
ему начинают мешать жить.

Причём сразу несколькими способами.

5. Регуляризация: когда сети не дают «распускаться»

Регуляризация — это напоминание модели:

«Не умничай слишком сильно. Будь устойчивой.»

Обычно это штрафы на веса:

·         L2 (weight decay)

·         реже L1

Мы наказываем сеть за:

·         слишком большие веса

·         резкие, неустойчивые решения

·         попытки идеально подогнаться под каждый объект

В результате модель:

·         перестаёт запоминать частные случаи

·         учится описывать общую форму данных

·         становится устойчивой к мелким флуктуациям

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

6. Шум во входе: отделяем структуру от мусора

Следующий приём выглядит почти издевательски.

Мы берём нормальные данные и специально портим их:

·         добавляем шум

·         зануляем признаки

·         искажаем значения

А потом просим сеть восстановить чистый оригинал.

То есть модель учится делать не просто:

x → x̂

а:

x + шум → x

Философия здесь очень человеческая:

мир шумный, но суть за шумом остаётся той же.

Это позволяет автоэнкодеру:

·         игнорировать случайные всплески

·         не считать шум аномалией

·         но чувствовать структурные отличия

Аномалия — это не шум. Аномалия — это другая форма данных.

7. Разреженность: когда важно, что именно включено

Разреженность — один из самых недооценённых приёмов.

Мы заставляем латентный вектор z быть почти пустым:

·         большинство компонент ≈ 0

·         активны только несколько

Тем самым мы говорим модели:

«Не всё сразу, приятель. Только главное.»

В реальном поведении так и есть. Человек не использует все свои возможности одновременно. В конкретной ситуации активны лишь некоторые паттерны,
остальное — фон.

Разреженные представления:

·         более интерпретируемы

·         менее шумные

·         ближе к факторным моделям, чем к чёрному ящику

8. Автоэнкодер и философский камень

Если собрать всё вместе, автоэнкодер — это не про магию.

Это модель, которой намеренно делают плохо:

·         тесно (бутылочное горлышко)

·         не дают хитрить (регуляризация)

·         путают вход (шум)

·         заставляют выбирать главное (разреженность)

В таких условиях у модели остаётся только одно —выучить реальную структуру данных.

Немного кода

import numpy as np
import pandas as pd

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

from sklearn.preprocessing import StandardScaler

# 1) Генерируем небольшие нормальные данные

rng = np.random.default_rng(42)

n_train = 200
n_test_normal = 60
n_test_anom = 20

X_train = rng.normal(0.0, 1.0, size=(n_train, 4))
X_train[:, 1] = 0.7 * X_train[:, 0] + rng.normal(0, 0.4, size=n_train)
X_train[:, 3] = 0.5 * X_train[:, 2] + rng.normal(0, 0.5, size=n_train)

X_test_norm = rng.normal(0.0, 1.0, size=(n_test_normal, 4))
X_test_norm[:, 1] = 0.7 * X_test_norm[:, 0] + rng.normal(0, 0.4, size=n_test_normal)
X_test_norm[:, 3] = 0.5 * X_test_norm[:, 2] + rng.normal(0, 0.5, size=n_test_normal)

# 2) Добавляем аномалии — сдвиги, масштабы, экстремумы

X_test_anom = rng.normal(0.0, 1.0, size=(n_test_anom, 4))
X_test_anom[:, 0] += 5.0
X_test_anom[:, 2] *= 4.0
X_test_anom[:5, 1] += 8.0

df_train = pd.DataFrame(X_train, columns=["f1", "f2", "f3", "f4"])
df_train["label"] = 0

df_test_norm = pd.DataFrame(X_test_norm, columns=["f1", "f2", "f3", "f4"])
df_test_norm["label"] = 0

df_test_anom = pd.DataFrame(X_test_anom, columns=["f1", "f2", "f3", "f4"])
df_test_anom["label"] = 1

df_test = pd.concat([df_test_norm, df_test_anom], ignore_index=True)

# 3) Масштабируем признаки — почти всегда нужно для нейросетей

scaler = StandardScaler()
X_train_sc = scaler.fit_transform(df_train[["f1", "f2", "f3", "f4"]].values)
X_test_sc = scaler.transform(df_test[["f1", "f2", "f3", "f4"]].values)

X_train_t = torch.tensor(X_train_sc, dtype=torch.float32)
X_test_t = torch.tensor(X_test_sc, dtype=torch.float32)

# 4) Описываем простой автоэнкодер

class AutoEncoder(nn.Module):
    def __init__(self, input_dim=4, latent_dim=2):
        super().__init__()

        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 16),
            nn.ReLU(),
            nn.Linear(16, latent_dim)
        )

        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 16),
            nn.ReLU(),
            nn.Linear(16, input_dim)
        )

    def forward(self, x):
        z = self.encoder(x)
        x_hat = self.decoder(z)
        return x_hat

torch.manual_seed(42)
model = AutoEncoder()

criterion = nn.MSELoss(reduction="none")
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)

# 5) Обучаем модель только на нормальных данных

loader = DataLoader(TensorDataset(X_train_t), batch_size=32, shuffle=True)

model.train()
for epoch in range(60):
    epoch_loss = 0.0

    for (xb,) in loader:
        optimizer.zero_grad()
        x_hat = model(xb)

        loss_vec = criterion(x_hat, xb).mean(dim=1)
        loss = loss_vec.mean()

        loss.backward()
        optimizer.step()

        epoch_loss += loss.item()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1:02d}, loss={epoch_loss/len(loader):.6f}")

# 6) Считаем reconstruction error и флаг аномалии

model.eval()
with torch.no_grad():
    x_hat_train = model(X_train_t)
    train_err = criterion(x_hat_train, X_train_t).mean(dim=1).cpu().numpy()

    x_hat_test = model(X_test_t)
    test_err = criterion(x_hat_test, X_test_t).mean(dim=1).cpu().numpy()

threshold = np.quantile(train_err, 0.95)

df_test_out = df_test.copy()
df_test_out["recon_error"] = test_err
df_test_out["is_anomaly_pred"] = (df_test_out["recon_error"] > threshold).astype(int)

print("Threshold:", threshold)
print("nСамые подозрительные строки:")
print(df_test_out.sort_values("recon_error", ascending=False).head(10))

print("nСводка (здесь label есть только для проверки примера):")
print(pd.crosstab(df_test_out["label"], df_test_out["is_anomaly_pred"]))

Что здесь важно понять

Хотя архитектура выглядит привычно, это не бинарная классификация. Энкодер и декодер — это регрессионные модели, которые решают задачу восстановления, а не распознавания классов. А дальше всё просто и честно:

·         считаем ошибку реконструкции

·         смотрим распределение

·         выбираем порог

·         получаем аномалии

Без классов.
Без ярлыков.
Без иллюзии полного знания.

 Вместо заключения

Автоэнкодер — это не модель «плохих» данных. Это модель реального мира, в котором странности просто не вписываются в привычную картину. И в этом его сила.

Автор: mmshaa9

Источник

Rambler's Top100