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

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

Для решения проблемы обнаружения аномалий обычно применяются традиционные методы. Некоторыми из этих методов могут быть (S)ARIMA, отклонение параметров статического местоположения от базового распределения, квантильная регрессия, ансамблевые методы с использованием пакетов признаков, изолированные леса и обнаружение аномалий на основе кластерного анализа.

Вышеупомянутые методы хорошо работают при работе с точками данных и событиями, помеченными как «аномалии» с логикой бинарной классификации. К сожалению, они не предназначены для захвата шаблона временной/пространственной корреляции между точками данных. Хотя классические модели временных рядов могут моделировать эти шаблоны, мы можем прибегнуть к методам глубокого обучения в нейронных сетях (НС), чтобы получить значимые результаты.

В этой статье мы рассмотрим приложение для обнаружения аномалий с использованием метода, основанного на автокодировщиках RNN, где последовательность точек данных помечается как «нормальное» или «аномальное» поведение. NN реализована с помощью фреймворка PyTorch.

Решения для глубокого обучения

Автоэнкодер

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

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

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

Рекуррентные нейронные сети

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

RNN (рекуррентные нейронные сети) особенно способны фиксировать взаимозависимость данных благодаря своему рекуррентному механизму. Основная идея состоит в том, что если у нас есть последовательность размера T и t — это индекс наблюдения в диапазоне [1, 2, …, t, …, T], когда наблюдение в момент времени t передается в сеть, прошлые наблюдения в моменты времени [t-1 , t-2, …] влияют на результат вывода.

RNN были настолько успешными, что исследователи внедрили модификации ванильного алгоритма, такие как GRU (Gated Recurrent Unit) и LSTM (Long Short-Term Memory), которые, помимо решения проблемы исчезающего градиента ванильных RNN, способны интерполировать более длинные временные ряды. зависимости благодаря структурам, называемым воротами и ячейками памяти.

Источник данных

Исходный набор данных можно найти на Kaggle как данные датчика насоса, где отслеживается система водяного насоса, потому что она не работает в течение некоторых периодов. Следующая мотивация побуждает к расследованию и попыткам найти решение:

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

Столбцы набора данных:

  • метка времени:время наблюдения записывается каждую минуту.
  • sensor_##: значение показаний каждого датчика, где ## — номер датчика в диапазоне от 00 до 51 включительно.
  • machine_status: имеет значения «НОРМАЛЬНОЕ», когда водяной насос работает нормально, или «СЛОМАННОЕ» или «ВОССТАНОВЛЕНИЕ» в противном случае.

Прежде всего, столбец machine_status заменяется новым столбцом с именем target, где элементам присваивается значение 0, когда >machine_status имеет значение «НОРМАЛЬНОЕ» или 1 в противном случае.

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

В статье основное внимание уделяется применению методов машинного обучения, а не упаковке окончательного решения проблемы; следовательно, мы будем работать только с sensor_02.

Система водяного насоса не работает примерно в 6% случаев, и есть 7 отказов:

Ошибка 1: 945 минут, 15,75 часа

Ошибка 2: 3111 минут, 51,85 часа

Ошибка 3: 1313 минут, 21,88 часа

Ошибка 4: 606 минут 10,1 часа

Ошибка 5: 8391 минута, 139,85 часа

Ошибка 6: 42 минуты 0,7 часа

Ошибка 7: 76 минут 1,27 часа

Первые 4 сбоя длились не более 2 дней, а самый продолжительный сбой длился 6 дней, тогда как более поздние сбои были устранены менее чем за 2 часа.

Цель состоит в том, чтобы определить, когда система не работает, просматривая последнюю доступную последовательность моментов времени с произвольным размером t.Выбор размера t зависит от многих факторов. Если для параметра t установлено значение 60, для определения работоспособности системы наблюдаются измерения за последний час.

Обработка данных

Данные, предоставляемые датчиком, представляют собой длинный временной ряд, состоящий из 220320 наблюдений. Эти данные масштабируются с помощью класса Python sklearn.preprocessing.StandardScaler(), который сосредотачивает данные в нулевом среднем ряду и единичной дисперсии.

Масштаб аппроксимируется только на нормальных данных, а затем применяется ко всей серии. После масштабирования скользящее окно размером 60 точек данных с отставанием в 45 шагов создает набор данных 1-часовых последовательностей. Для каждой последовательности в столбце target подсчитывается количество аномальных точек.

Таким образом генерируется 4895 последовательностей, некоторые из которых показаны ниже:

Поскольку последовательности с аномалиями от 1 до 59 могут сигнализировать о частичной работе системы водяного насоса, сохраняются только строки, целевые столбцы которых имеют значения 0 или 60, представляющие нормальные и аномальные последовательности соответственно. В некоторых строках представлены значения NaN, заполненные медианами соответствующих последовательностей.

Из-за того, что PyTorch используется в качестве библиотеки Python для построения архитектуры автоэнкодера, последовательности преобразуются в тензоры факела, и, в конечном итоге, набор данных делится на обучение (85%), проверку (5%) и тест (10%). поднаборы данных, которые включают только «нормальные» последовательности. Напротив, аномальные последовательности сгруппированы как «аномальные» тестовые поднаборы из-за дизайна процедуры обучения.

Реализация PyTorch

Реализация LSTM Autoencoder будет показана и объяснена ниже, поскольку GRU и Vanilla RNN с точки зрения кода представляют собой простую вариацию LSTM.

Кодер

Полный код энкодера представлен ниже:

from torch import nn, distributions, exp, log
import torch

class LSTM_Encoder(nn.Module):

  def __init__(self, n_features, seq_len, embedding_dim=20):
    super(LSTM_Encoder, self).__init__()

    self.seq_len, self.n_features = seq_len, n_features
    self.embedding_dim = embedding_dim

    self.rnn = nn.LSTM(
      input_size=n_features,
      hidden_size=embedding_dim,
      num_layers=1,
      batch_first=True
    )

  def forward(self, x):
    x = x.reshape((1, self.seq_len, self.n_features))
    _, (hidden_n, _) = self.rnn(x)
    return hidden_n.reshape((self.n_features, self.embedding_dim))

Ниже приводится построчное объяснение реализации кодировщика.

from torch import nn, distributions, exp, log
import torch

Это все элементы факела: torch.nn позволяет построить нейронную сеть, а torch.distributions, torch.exp и torch.log нужны для вариационного автоэнкодера.

class RNN_Encoder(nn.Module):

  def __init__(self, n_features, seq_len, embedding_dim=20):
    super(RNN_Encoder, self).__init__() # The same as super().__init__()
    
    self.seq_len, self.n_features = seq_len, n_features
    self.embedding_dim = embedding_dim

    self.rnn = nn.LSTM(
      input_size=n_features,
      hidden_size=self.embedding_dim,
      num_layers=1,
      batch_first=True
    )

Класс RNN_Encoder наследует функциональные возможности класса nn.Module, self.seq_len — длина входной последовательности временных рядов (в данном случае 60), self.n_features — входное количество последовательностей, соответствующее 1 здесь, так как учитываются только данные датчика_02. Атрибут self.embedding_dim – это размер вектора уменьшенного размера, в который закодирована информация входной последовательности.

self.rnn — это реализуемая рекуррентная нейронная сеть, и ее слой LSTM имеет следующие параметры:

  • input_size — количество последовательностей функций (только одна для датчика_02).
  • hidden_size — размер скрытого слоя.
  • num_layers – количество скрытых слоев архитектуры RNN.
  • Для batch_first задано значение true, когда размер входного вектора факела должен быть в порядке (размер пакета, длина последовательности и количество функций)
  def forward(self, x):
    x = x.reshape((1, self.seq_len, self.n_features))
    _, (hidden_n, _) = self.rnn(x)
    return hidden_n.reshape((self.n_features, self.embedding_dim))

Прямой метод определяет, как ввод передается через сеть. Он состоит из изменения формы входного вектора факела для этого приложения, поскольку для параметра batch_first задано значение true; для последующего шага LSTM размерность тензора факела равна (1, 60, 1).

Проход LSTM создаст первый тензор факела, соответствующий выходным данным NN, и второй двумерный тензор факела, соответствующий скрытому слою и слою ячеек. Кодер возвращает скрытый вектор из скрытого слоя.

Декодер

class LSTM_Decoder(nn.Module):

  def __init__(self, n_features=1, seq_len=60, input_dim=20):
    super(LSTM_Decoder, self).__init__()

    self.seq_len, self.input_dim = seq_len, input_dim
    self.hidden_dim, self.n_features = 2 * input_dim, n_features

    self.rnn = nn.LSTM(
      input_size=input_dim,
      hidden_size=self.hidden_dim,
      num_layers=1,
      batch_first=True
    )
    self.output_layer = nn.Linear(self.hidden_dim, n_features)

  def forward(self, x):
    x = x.repeat(self.seq_len, self.n_features)
    
    x = x.reshape((self.n_features, self.seq_len, self.input_dim))
    x, (hidden_n, cell_n) = self.rnn(x)
    x = x.reshape((self.seq_len, self.hidden_dim))

    return self.output_layer(x)

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

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

Обучение

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

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.L1Loss(reduction='sum').to(device)
history = dict(train=[], val=[])
best_loss = 10000.0
best_model_wts = copy.deepcopy(model.state_dict())

# train_dataset
# val_dataset

Первая строка кода регулирует использование графического процессора, когда он доступен. Оптимизатор представляет собой известный оптимизатор Адама с параметром обучения 0,0003. Эксперименты доказывают, что это значение является оптимальным для обучения: большие значения не приводят к сходимости, а меньшие значения приводят к более медленной сходимости. Убыток представляет собой сумму абсолютной разницы.

Словарь под названием history создается для сохранения набора данных для обучения и проверки потери набора данных в разные эпохи. Переменная best_loss представляет собой произвольное большое значение в качестве заполнителя для самых высоких потерь: при превышении этого значения в любой из последующих эпох параметры модели будут сохранены в объекте best_model_wts.

Есть набор последовательностей train_dataset и val_dataset — обучающий и проверочный набор, состоящий только из обычных последовательностей.

for epoch in range(50):
    model = model.train()

    train_losses = []
    for seq_true in train_dataset:
        seq_true = seq_true.to(device)
        seq_pred = model(seq_true)
        loss = criterion(seq_pred, seq_true)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_losses.append(loss.item())

    val_losses = []
    model = model.eval()
    with torch.no_grad():
        for seq_true in val_dataset:

            seq_true = seq_true.to(device)
            seq_pred = model(seq_true)

            loss = criterion(seq_pred, seq_true)
            val_losses.append(loss.item())

    train_loss = np.mean(train_losses)
    val_loss = np.mean(val_losses)

    history['train'].append(train_loss)
    history['val'].append(val_loss)

    if val_loss < best_loss:
        best_loss = val_loss
        best_model_wts = copy.deepcopy(model.state_dict())

Обучение начинается, как обычно, в NN. В течение 50 эпох каждая последовательность перемещается в GPU, если она доступна, проходит через модель, вычисляются потери и настраиваются параметры нейронной сети благодаря обратному распространению. Затем потери при обучении и проверке сохраняются, и через эпохи выбирается наиболее эффективная модель.

Порог

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

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

Одна стратегия состоит в том, чтобы установить квантиль распределения в качестве порога. если квантиль установлен как высокий, более аномальные последовательности будут помечены как нормальные; наоборот, более низкий квантиль означает, что нормальные последовательности будут помечены как аномальные. Выбор порога сильно зависит от бизнес-задачи, и для определения оптимального порога необходимо провести анализ затрат. Эта информация недоступна; поэтому произвольный квантиль 90% кажется разумным.

Показатели эффективности

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

  • TP — количество аномальных последовательностей, помеченных моделью как аномальные, т. е. ошибка реконструкции превышает пороговое значение.
  • TN — это количество нормальных последовательностей, помеченных моделью как нормальные, т. е. ошибка реконструкции меньше порогового значения.
  • FP — это количество нормальных последовательностей, плохо реконструированных моделью и, следовательно, помеченных как аномальные.
  • FN — это количество аномальных последовательностей, помеченных как нормальные, поскольку модель могла восстановиться близко к входным данным для выбранного порога.

Из этих величин можно получить следующие показатели:

Точность = (TP + TN) / (TP + TN + FP + FN)

Точность = TP / (TP + FP)

Отзыв = ТП / (ТП + FN)

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

Для этой задачи точность представляет собой процент правильно помеченных последовательностей, независимо от того, являются ли они аномальными или нормальными.

Полученные результаты

Ниже приведены некоторые результаты наших экспериментов. Ванильные RNN, GRU и LSTM были реализованы аналогичным образом, поскольку продемонстрированный код выделен суффиксом _light, где существует только один скрытый слой, более глубокая версия с двумя скрытыми слоями. , и результаты также включают вариационный автоэнкодер с одним скрытым слоем RNN.

Все методы работают хорошо в соответствии с рассматриваемыми показателями. Реализации GRU и LSTM имеют явное преимущество в отзыве по сравнению с RNN, который работает хуже всего, особенно при использовании более легкой архитектуры; он, вероятно, не может уловить часть сложности последовательностей. Реализация VAE повышает производительность архитектуры LSTM. Два других типа RNN не столь полезны; наоборот, они ухудшают ТП.

Краткое содержание

Можно сделать вывод, что среди рассмотренных нет предпочтительной архитектуры модели, которая бы значительно превосходила другие по этой задаче. Архитектура GRU кажется хорошим выбором в производственной среде с точки зрения MLOps, поскольку она показала более высокую производительность, чем RNN, и она легче, чем LSTM, а последняя также является самой медленной для обучения.

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

Нулевая обработка может быть выполнена с использованием лучших методов, таких как сглаживание ядер, и других показателей, таких как F1, кривая ROC AUC и т. д., которые можно учитывать для выбора оптимальной модели для нужд проблемы. Кроме того, для вариационного автоэнкодера функция обучения должна быть снабжена более сложной функцией потерь, учитывающей сходимость KL.

Наши команды GEMS в составе Luxoft создали несколько приложений не только для обнаружения аномалий, но и для различных областей машинного обучения, среди которых компьютерное зрение, классическое и глубокое машинное обучение, НЛП и т. д.
Вы можете увидеть их на нашем сайте.

Об авторах

Мартин Д'Ипполито — статистик, специализирующийся на науке о данных и приложениях машинного обучения. Опыт работы Мартина связан с финансовой и автомобильной промышленностью.

Артур Шайхатаровявляется техническим руководителем AI/ML, который управляет командой разработчиков и координирует ее. Основным заказчиком Артура является AMD, хотя он также работает с другими крупными ИТ-компаниями.

Ссылки

Бумажная теория вариационных автоэнкодеров

https://tiao.io/post/tutorial-on-variational-autoencoders-with-a-concise-keras-implementation/

https://debuggercafe.com/getting-started-with-variational-autoencoders-using-pytorch/

https://avandekleut.github.io/vae/

https://machinelearningmastery.com/lstm-autoencoders/