Всем привет, меня зовут Дмитрий, я — MLE в Альфа-Банке, занимаюсь автоматизацией процессов и оптимизацией моделей, ищу в моделях проблемы и решаю их.
В прошлом году ко мне пришли ребята из отдела тестирования и задали два вопроса: «Как тестирование батч-моделей можно автоматизировать?» и «Что для этого нужно?». Коллеги поделились наболевшей историей, что в большинстве моделей выполняемые проверки повторяются. Выслушав весь запрос, я спроектировал и реализовал систему автоматического тестирования, о чём и расскажу в этой статье. Также здесь будут технические детали реализации, архитектурные решения и полученные результаты.
Статья будет полезна не только специалистам по автоматизации процессов тестирования, а и ML-инженерам, MLOps-специалистам и командам разработки, занимающимся поддержкой продакшн-систем машинного обучения.

№1. Подготовка к тестированию батч-моделей
Батч-модели — это способ получения прогнозов модели машинного обучения с помощью обрабатывания большого количества данных пакетами по расписанию (например, ежедневно или еженедельно), в отличие от онлайн и потоковых моделей, работающих в режиме реального времени. В банковской сфере такие модели широко используются, например, для периодического скоринга клиентов, анализа транзакций и других задач, не требующих мгновенного отклика.
После определения того, что понимается под батч-моделью, поговорим о том, какие у этого типа моделей есть особенности при тестировании и что мы выделили в качестве функционала и что у нас было перед началом разработки.
Итак, специалисты по тестированию выполняли следующие проверки:
-
Ручная проверка статуса выполнения DAG’a.
-
Контроль диапазонов выходных скоров.
Эти проверки осуществляются вследствие того, что для влияния на процесс работы модели необходимо подменять данные, либо лезть в код модели и вносить в него коррективы. О генерации и подмене данных я расскажу дальше, а от случая с изменением кода мы отказались из-за слишком большого количество моделей и сложностей, которые бы увеличили время тестирования в разы.
Из-за того, что процесс работы батч-модели подразумевает запуск всего пайплайна модели мы определили следующий функционал:
-
Автоматическая проверка скоров.
-
Добавление проверок на невалидных данных.
-
Сделать минимальное изменение инфраструктуры.
На момент начала разработки уже была внедрена стандартизированная структура проектов на базе cookiecutter. Это позволило разработать автотесты для большинства стандартных моделей, которые включали в себя шаблонную структуру. Начнём знакомство с шаблона репозитория.
Описание структуры репозитория.
Основными файлами в репозитории для батч моделей являются:
-
inference.py — файл запуска модели,
-
config.py — конфиг для класса модели
-
inference_wrapper.py — класс InferenceModel.
Базовый класс InferenceModel реализует стандартный пайплайн обработки данных:
-
read_data: чтение данных из файлов или таблиц, -
preproc: подготовка прочитанных данных в формате датафрейм, -
predict: инференс на подготовленных данных, -
save_results: сохранение результата в hdfs/целевую таблицу.
Пример того, как выглядит код класса InferenceModel:
class InferenceModel:
def __init__(self):
self.model = None
self.spark = get_spark()
def load_model(self):
self.model = load_model()
def read_data(self):
df = spark.table("schema.table").select("col1", "col2")
return df
def preproc(self, df):
df = df.withColumn("col1", col("col1") + 2)
return df
def predict(self, df):
scores = self.model.predict(df)
return scores
def save_results(self, df):
result_to_table(df, “schema.table_result”)
Учитывая шаблон cookiecutter, были выделены следующие принципы проектирования системы тестирования:
-
Использование класса
InferenceModel, когда это возможно. -
Простое добавление новых тест-кейсов без изменения базовой логики.
-
Настройка параметров тестирования осуществляется через существующий config.py.
№2. Генерация синтетических данных
Данные читаются через метод read_data один раз. Для генерации синтетики достаточно небольшого количества записей, ведь наша цель проверить, как методы класса справляются с некорректными данными, а не гонять полный inference на продакшн-объемах.
Последовательность действий для генерации синтетики для тест-кейса:
-
Прочитать данные с помощью метода
read_data. -
Выбрать небольшое количество примеров в данных.
-
Создать копию исходных данных, для генерации синтетических данных на основе копии.
-
Сгенерировать данные.
-
Подать сгенерированные данные в
preprocиpredictметоды. -
Сохранить результаты теста и перейти к новой генерации данных (перейти к шагу 4).
В псевдокоде это может быть записано так (первая строчка нужна для импорта всех функций для генерации данных):
TESTS = [getattr(syntetic_test, test) for test in filter(lambda x: "_test" in x, dir(synthetic_test))]
def synthetic_data_test():
model = InferenceModel()
model.load_model()
data = model.read_data()
if isinstance(data, pd.DataFrame):
data = data.iloc[:ROWS_LIMIT]
elif isinstance(data, SparkDF):
data = data.limit(ROWS_LIMIT)
for test in TESTS:
if isinstance(data, pd.DataFrame):
df = data.copy()
else:
df = data.select('*')
synthetic_df = test(df)
preprocessed_df = model.preproc(synthetic_df)
_ = model.predict(preprocessed_df)
Как было сказано выше, зачастую InferenceModel не меняется, но нам также необходимо обработать случаи, когда дата-сайентист по какой-то причине поменял сигнатуру методов класса.
Для этого мы предусмотрели пропуск этапа проверки модели на синтетических данных, если сигнатура класс не соответствует ожидаемой.
METHODS = [{'method': 'read_data'},
{'method': 'preproc',
'input': True},
{'method': 'predict',
'input': True}]
def signature_test(model_class) -> None:
assert all([method.get('method') in dir(model_class) for method in METHODS])
for method in METHODS:
if method.get('input'):
assert (len(list(filter(lambda x: x[0] != 'self',
inspect.signature(getattr(model_class, method['method']))
.parameters.items()))) == 1),
f"{method['method']} hasn't input argument"
Из сигнатуры проверяем следующее:
-
наличие методов,
-
read_dataиpreprocметоды должны возвращать датафреймы, -
preprocиpredictдолжны получать датафреймы.
Принципы проектирования автотестов.
-
Необходимо проводить детерминированное тестирование вместо простого exception handling по итогам работы тестов на синтетике. Каждый тест — чётко прописанная логика изменения датафрейма и вы должны ожидать определённую проблему.
-
Учитываем, что данные могут быть возвращены из метода
read_dataв разных форматах, например, Pandas или Spark dataframe. -
Спроектировать систему автотестирования с минимальным порогом входа, чтобы её поддержкой могли заниматься специалисты по тестированию, добавив, например, необходимый кейс с генерацией синтетических данных.
Альтернативный подход к генерации данных.
Генерация синтетических данных также может быть осуществлена через генерацию таблиц, но для внедрения этого метода может потребоваться переписывания большого количества моделей по следующим причинам:
-
Для модели необходимо получить список используемых таблиц, затем сгенерировать таблицы.
-
Используемые таблицы могут быть указаны в SQL-запросе, в случае формирования Spark-датафрейма. В таком случае замену таблицы потребуется делать вручную, что противоречит принципам автоматического тестирования.
Эти проблемы можно обойти, если у моделей есть конфиг, регулирующий используемые данные. Генерация синтетической таблицы может быть хорошим вариантом, через замену таблиц в конфиге на сгенерированные.
№3. Автоматическая проверка скоров
Система валидации скоров разработана для автоматической проверки корректности выходных данных модели после завершения инференса. Основные элементы:
-
Настройка параметров проверки через config.py.
-
Код для проверки диапазона скоров.
-
Обработчик отсутствующих скоров.
Мы предусмотрели, чтобы настройка теста включала в себя изменение всего нескольких параметров и легко добавлялась. Для этого мы добавили предустановленные параметры в шаблон файла config.py. Таким образом дата-сайентисту нужно изменить всего несколько значений для запуска проверки скора.
Для конфигурации проверки есть возможность настроить следующие параметры:
-
Название таблицы.
-
Название скора и его диапазон.
-
Как учитывать краевые точки.
-
Сколько скоров необходимо вывести в случае обнаружения скоров вне диапазона.
Для нашего случая мы определили следующий формат настройки параметров для автоматического тестирования:
@dataclass
class Config:
score_config: frozendict = frozendict({"model_score": {"range": (0, 1), "between_inclusive": "both"}})
score_table: str = "schema.table"
num_upper_max_border: int = 5
num_less_min_border: int = 5
Что мы учли при разработке проверок скора:
-
количество скоров за один инференс и как эти скоры хранятся,
-
как будет происходить настройка конфига проверки скора,
-
логирование,
-
обработка случаев, когда в названии скора опечатка, чтобы провести тестирование на валидных скорах, а ненайденные скоры вывести в результатах тестирования.
Наше логирование включает:
-
Названия скоров со значениями вне диапазона.
-
Количество скоров вне диапазона.
-
N скоров ниже и выше границы допустимого диапазона.
-
Ненайденные в таблице скоры.
Рекомендации по внедрению.
Прежде чем начать разрабатывать автоматические тесты, необходимо определить, как будет осуществляться их встраивание в имеющуюся инфраструктуру.
Для более простого использования и поддержки кода нами была сделана внутренняя библиотека, которая собирается из репозитория пайплайном. Запуск этой библиотеки почти полностью копирует шаг запуска инференса модели. Таким образом, инференс запускался с помощью Python inference.py, а автотесты запускаются с помощью python -m batch_autotests.
Подведение итогов
Подводя итоги, сформулирую список требований, который нам помог:
-
Простота конфигурации, изменение нескольких параметров сделает автоматическое тестирование более простым для интеграции в процессы вывода моделей.
-
Невозможно покрыть все кейсы сразу, постарайтесь выделить те модели, тестирование которых может быть автоматизировано в первую очередь.
-
Модульность для поддержки специалистами тестирования, чтобы для добавления кейсов на проверке синтетики не нужно было привлекать инженеров.
-
Автотесты должны быть готовы к любому коду модели, чтобы корректно обработать возможные исключения.
-
Оптимизировать ключевые компоненты: делать инференс на небольшом наборе синтетики, операции по извлечению скоров быть оптимальными.
-
Встраивание тестов должно учитывать существующие пайплайны для простой интеграции.
Добавление автотестов помогло сократить затрачиваемое время на тестирование моделей, как минимум на 1-2 часа, за счет анализа скоров, который до этого специалисты по тестированию делали вручную.
Что дальше?
На момент написания статьи мы занимаемся автоматизацией процесса тестирования наших AutoML-моделей, процесс их вывода и разработки отличается от батчевых моделей.
Основные отличия:
-
У моделей есть конфигурационный файл, в котором описаны используемые данные и параметры модели, благодаря этому генерация данных может осуществляться через создание синтетических таблиц, про которое мы говорили выше.
-
Для вывода AutoML-моделей используются специальные пайплайны, этап тестирования моделей включает проверку статуса работы пайплайна и дага.
Мы также ожидаем существенного сокращения времени тестирования, а еще добавим новые методы и способы генерации синтетических данных для проверки моделей.
Надеюсь, что описанная реализация и идеи найдут у вас отклик, и вы поделитесь своими историями автоматизации тестирования. С радостью отвечу в комментариях на ваши вопросы и подискутирую над тем, как и что можно было бы улучшить.
Рекомендуем:
Автор: admitrya


