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

Что такое дрейф данных?

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

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

Это изменение данных называется «дрейфом данных». Другими словами, базовое распределение данных, на которых обучался алгоритм, изменилось, и в результате может пострадать производительность модели.

Как это можно исправить?

Первым шагом к решению любой проблемы является признание того, что она существует. Существует множество способов, которыми может произойти дрейф данных, и различные инструменты для решения таких проблем. Некоторые готовые решения для обнаружения дрейфа данных предлагаются такими инструментами, как Amazon Sagemaker и Azure ML.

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

  • adtk — набор инструментов Python для обнаружения аномалий на основе правил/неконтролируемого наблюдения во временных рядах.
  • Обнаружение алиби - Алгоритмы обнаружения выбросов и состязательных экземпляров, дрейф концепций и метрики.
  • Deequ — библиотека, созданная на основе Apache Spark для определения модульных тестов для данных, которые измеряют качество данных в больших наборах данных.

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

Настройка обнаружения алиби

Набор данных

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

Установка
ПРИМЕЧАНИЕ
. Alibi Detect работает как с бэкендами Tensorflow, так и с Pytorch, но требует предварительной установки. Это предполагает, что эта установка завершена.

!pip install nlp #Required to load the dataset
!pip install alibi-detect 
!pip install transformers # Required for bert model encodings

Основной импорт

Здесь мы используем AutoTokenizer из библиотеки трансформаторов, чтобы мы могли точно разбить обзоры на их соответствующие токенизированные выходные данные.

import nlp
import numpy as np
import os
from transformers import AutoTokenizer
from alibi_detect.cd import KSDrift

Что такое KSDrift?

Обнаружение дрейфа по Колмогорову-Смирнову (КС) — это статистический метод, используемый для обнаружения изменений в распределении данных во времени. Представьте, что у вас есть два набора данных: один представляет, как люди оценивали фильмы в прошлом году, а другой — как они оценивают фильмы в этом году. Тест KS может сравнить эти два набора данных и определить, произошли ли значительные изменения в том, как люди оценивают фильмы в этом году по сравнению с прошлым годом.

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

Загрузить набор данных

Сначала мы загружаем соответствующий токенизатор. Здесь мы используем модель bert-base-case, предварительно обученную для классических задач НЛП.

model_name = 'bert-base-cased'
tokenizer = AutoTokenizer.from_pretrained(model_name)

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

def load_dataset(dataset: str, split: str = 'test'):
    data = nlp.load_dataset(dataset)
    X, Y= [], []
    for x in data[split]:
        X.append(x['text'])
        Y.append(x['label'])
    X = np.array(X)
    Y= np.array(y)
    return X, Y

X, Y= load_dataset('imdb', split='train')

Внедрение дрейфующих данных

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

Эта техника взята отсюда.

def random_sample(X: np.ndarray, y: np.ndarray, proba_zero: float, n: int):
    if len(y.shape) == 1:
        idx_0 = np.where(y == 0)[0]
        idx_1 = np.where(y == 1)[0]
    else:
        idx_0 = np.where(y[:, 0] == 1)[0]
        idx_1 = np.where(y[:, 1] == 1)[0]
    n_0, n_1 = int(n * proba_zero), int(n * (1 - proba_zero))
    idx_0_out = np.random.choice(idx_0, n_0, replace=False)
    idx_1_out = np.random.choice(idx_1, n_1, replace=False)
    X_out = np.concatenate([X[idx_0_out], X[idx_1_out]])
    y_out = np.concatenate([y[idx_0_out], y[idx_1_out]])
    return X_out.tolist(), y_out.tolist()


def padding_last(x: np.ndarray, seq_len: int) -> np.ndarray:
    try:  # try not to replace padding token
        last_token = np.where(x == 0)[0][0]
    except:  # no padding
        last_token = seq_len - 1
    return 1, last_token


def padding_first(x: np.ndarray, seq_len: int) -> np.ndarray:
    try:  # try not to replace padding token
        first_token = np.where(x == 0)[0][-1] + 2
    except:  # no padding
        first_token = 0
    return first_token, seq_len - 1


def inject_word(token: int, X: np.ndarray, perc_chg: float, padding: str = 'last'):
    seq_len = X.shape[1]
    n_chg = int(perc_chg * .01 * seq_len)
    X_cp = X.copy()
    for _ in range(X.shape[0]):
        if padding == 'last':
            first_token, last_token = padding_last(X_cp[_, :], seq_len)
        else:
            first_token, last_token = padding_first(X_cp[_, :], seq_len)
        if last_token <= n_chg:
            choice_len = seq_len
        else:
            choice_len = last_token
        idx = np.random.choice(np.arange(first_token, choice_len), n_chg, replace=False)
        X_cp[_, idx] = token
    return X_cp.tolist()

Затем мы фактически разделяем набор данных —

Здесь будет введен reference_sample, который будет сравниваться с h0_sample.

sample_size = 1000
h0_sample = random_sample(X, Y, proba_zero=.5, n=sample_size)[0]
reference_sample = random_sample(X, Y, proba_zero=.5, n=sample_size)[0]
imbalance_fractions = [.1, .9]
imbalance_samples = {frac: random_sample(X, y, proba_zero=frac, n=sample_size)[0] for frac in imbalance_fractions}

Давайте теперь введем случайные данные, чтобы мы могли смоделировать «дрейф данных».

words = ['fantastic', 'good', 'bad', 'horrible']
perc_chg = [1., 5.]  # % of tokens to change in an instance

words_tf = tokenizer(words)['input_ids']
words_tf = [token[1:-1][0] for token in words_tf]
max_len = 100
tokens = tokenizer(reference_sample, pad_to_max_length=True,
                   max_length=max_len, return_tensors='tf')
X_word = {}
for i, w in enumerate(words_tf):
    X_word[words[i]] = {}
    for p in perc_chg:
        x = inject_word(w, tokens['input_ids'].numpy(), p)
        dec = tokenizer.batch_decode(x, **dict(skip_special_tokens=True))
        X_word[words[i]][p] = dec

Предварительная обработка

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

from alibi_detect.models.tensorflow import TransformerEmbedding

emb_type = 'hidden_state'
n_layers = 8
layers = [-_ for _ in range(1, n_layers + 1)]

embedding = TransformerEmbedding(model_name, emb_type, layers)

Чтобы обнаружить дрейф в пространстве встраивания модели BERT, мы используем 768-мерный вектор для каждого экземпляра. Однако это многомерное пространство, что может затруднить статистический анализ. Чтобы упростить анализ, мы применяем шаг уменьшения размерности с помощью необученного автоэнкодера (UAE). Мы передаем модель вложения в качестве входных данных для UAE, который затем проецирует вложение в пространство более низкого измерения. Это упрощает обнаружение и анализ дрейфа данных.

from alibi_detect.cd.tensorflow import UAE

tokens = tokenizer(list(X[:5]), pad_to_max_length=True,
                   max_length=max_len, return_tensors='tf')
x_emb = embedding(tokens)
enc_dim = 32
shape = (x_emb.shape[1],)

uae = UAE(input_layer=embedding, shape=shape, enc_dim=enc_dim)

Обнаружение дрейфа

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

from functools import partial
from alibi_detect.cd.tensorflow import preprocess_drift

# define preprocessing function
preprocess_fn = partial(preprocess_drift, model=uae, tokenizer=tokenizer,
                        max_len=max_len, batch_size=32)

# initialize detector
drift_detector = KSDrift(X_ref, p_val=.05, preprocess_fn=preprocess_fn, input_shape=(max_len,))

Теперь, когда у нас есть дрейф_детектор, давайте посмотрим, сможем ли мы на самом деле обнаружить дрейф!

preds_h0 = drift_detector.predict(h0_sample)
labels = ['No Drift Detected', 'Drift Detected!']
print('Drift: {}'.format(labels[preds_h0['data']['is_drift']]))
print('Probability Value: {}'.format(preds_h0['data']['p_val']))

'''
This prints - 
Drift: No Drift Detected
Probability Value: [0.31356168 0.18111965 0.60991895 0.43243074 0.6852314  0.722555
 0.28769323 0.18111965 0.50035924 0.9134755  0.40047103 0.79439443
 0.79439443 0.722555   0.5726548  0.1640792  0.9540582  0.60991895
 0.5726548  0.5726548  0.31356168 0.40047103 0.6852314  0.34099194
 0.5726548  0.07762147 0.79439443 0.09710453 0.5726548  0.79439443
 0.7590978  0.26338065]
'''

И, наконец, давайте проверим на наших clang_samples —

for k, v in imbalance_samples.items():
    preds = drift_detector.predict(v)
    print('% Negative Sentiment: {}'.format(k * 100))
    print('Drift: {}'.format(labels[preds['data']['is_drift']]))
    print('Probability Value: {}'.format(preds['data']['p_val']))


'''
This prints - 
% Negative Sentiment: 10.0
Drift: Yes!
Probability Value: [4.32430744e-01 4.00471032e-01 5.46463318e-02 7.76214674e-02
 1.08282514e-01 1.12110768e-02 6.91903234e-02 2.82894098e-03
 8.59294355e-01 6.47557259e-01 1.33834302e-01 7.94394433e-01
 4.28151786e-02 2.87693232e-01 6.09918952e-01 1.33834302e-01
 2.40603596e-01 9.71045271e-02 7.76214674e-02 9.35580969e-01
 2.87693232e-01 2.92505771e-02 4.00471032e-01 6.09918952e-01
 2.87693232e-01 5.06567594e-04 1.64079204e-01 6.09918952e-01
 1.33834302e-01 2.19330013e-01 7.94394433e-01 2.56591532e-02]
'''

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

Заключение

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

  1. Независимость от модальности: Alibi Detect способен обнаруживать дрейф в различных модальностях, включая текст, изображения и табличные данные.
  2. Поддерживает несколько методов обнаружения дрейфа: Alibi Detect включает в себя несколько статистических и основанных на машинном обучении методов для обнаружения дрейфа, включая K-S, AD и расстояние L2.
  3. Не зависит от модели: Alibi Detect можно использовать с любой моделью машинного обучения, независимо от используемой архитектуры или платформы.

Некоторые ограничения Alibi Detect включают:

  1. Требуются помеченные данные: Alibi Detect требует помеченных данных для правильной работы, что может быть ограничением в случаях, когда помеченных данных мало или их получение дорого.
  2. Может требовать значительных вычислительных ресурсов. Процесс обнаружения дрейфа может потребовать значительных вычислительных ресурсов, особенно при работе с большими наборами данных или сложными моделями.
  3. Может работать не со всеми типами данных. Хотя Alibi Detect не зависит от модальности, он может работать не со всеми типами данных или во всех контекстах. Важно тщательно оценивать его производительность в каждом случае использования.

Этот блог иллюстрирует базовый вариант использования Alibi Detect для обнаружения дрейфа данных. Рабочий код можно найти здесь.