Всем привет, меня зовут Дмитрий, я — 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-моделей используются специальные пайплайны, этап тестирования моделей включает проверку статуса работы пайплайна и дага.
Мы также ожидаем существенного сокращения времени тестирования, а еще добавим новые методы и способы генерации синтетических данных для проверки моделей.
Надеюсь, что описанная реализация и идеи найдут у вас отклик, и вы поделитесь своими историями автоматизации тестирования. С радостью отвечу в комментариях на ваши вопросы и подискутирую над тем, как и что можно было бы улучшить.
Рекомендуем: