В этом посте содержится часть материалов из лекций Качество инфраструктуры нашего курса Машинное обучение в производстве. Остальные главы смотрите в содержании.
Конвейеры машинного обучения содержат код для обучения, оценки и развертывания моделей, которые затем используются в продуктах. Как и весь код, код в конвейере можно и нужно тестировать. Когда возникают проблемы, код в конвейерах часто дает сбой, просто не выполняя правильных действий, но без сбоев. Если никто не замечает тихих сбоев, проблемы могут долгое время оставаться незамеченными. Проблемы в конвейерах часто приводят к модели более низкого качества. Обеспечение качества кода конвейера становится особенно важным, когда конвейеры регулярно выполняются для обновления моделей новыми данными.
Поскольку конвейеры машинного обучения — это код, их можно гарантировать, как и другой код в программной системе. Хотя модели с машинным обучением трудно тестировать (как обсуждалось в предыдущих главах Качество модели), конвейерный код на самом деле не отличается от традиционного кода: он преобразует данные, вызывает различные библиотеки и взаимодействует с файлами, базами данных или сетями. Это делает конвейерный код гораздо более поддающимся традиционным подходам к обеспечению качества программного обеспечения, рассмотренным в главе Основы обеспечения качества. Далее мы обсудим тестирование, проверку кода и статический анализ конвейерного кода.
Незаметные ошибки в конвейерах машинного обучения
Большинство специалистов по данным могут поделиться историями об ошибках в конвейерах машинного обучения, которые долгое время оставались незамеченными. В некоторых случаях были не обнаруженные проблемы изначально, а в других случаях какие-то внешние изменения что-то сломали, но остались незамеченными. Рассмотрим пример компании по доставке продуктов с грузовыми велосипедами, которая, столкнувшись с дрейфом данных, продолжает собирать данные и регулярно переобучает модель для прогнозирования спроса и оптимальных маршрутов доставки каждый день.
- В какой-то момент набор данных становится настолько большим, что больше не подходит для виртуальной машины, используемой для обучения. Обучение новых моделей не удается, но система продолжает работать со старой моделью. Проблема обнаруживается только через несколько недель, когда производительность модели в производстве ухудшилась до такой степени, что клиенты начинают жаловаться на ненадежные графики поставок.
- Процесс, извлекающий дополнительные обучающие данные из последних заказов, дает сбой после обновления библиотеки соединителя базы данных. Таким образом, обучающие данные больше не обновляются, но конвейер продолжает ежедневно обучать модель на одних и тех же данных. Проблему не замечают операторы, которые каждый день видят успешное выполнение тренировок и стабильные отчеты об оценке точности в офлайне.
- Внешний коммерческий API погоды предоставляет часть данных, используемых при обучении модели. Когда API погоды недоступен, данные записываются со значениями n/a, которые позже заменяются значениями по умолчанию на более позднем этапе очистки данных конвейера. По истечении срока действия кредитной карты для оплаты API погоды отклоняет запросы, но этого долго не замечают, потому что пайплайн все равно выдает модель, пусть и на основе менее качественных данных со значениями по умолчанию вместо реальных данных о погоде.
- Во время разработки признаков время заказов циклически кодируется, чтобы лучше соответствовать свойствам алгоритма машинного обучения и лучше обрабатывать прогнозы около полуночи. К сожалению, из-за ошибки в коде алгоритм обучения получает исходные непреобразованные данные. Он по-прежнему изучает модель, но более слабую, чем если бы данные были преобразованы должным образом.
- Система собирает огромное количество телеметрии. По мере того, как система становится популярной, сервер телеметрии перегружается, и почти вся телеметрия, отправленная с мобильных устройств водителей доставки, сбрасывается. Никто не замечает, что объем собираемой телеметрии не продолжает расти вместе с количеством драйверов, а проблемы, с которыми сталкиваются пользователи водителей, остаются незамеченными, пока пользователи не начнут массово жаловаться в отзывах в магазине приложений.
Общей темой здесь является то, что ни одна из этих проблем не проявляется как сбой. Мы наблюдаем эти проблемы только в том случае, если мы активно отслеживаем правильные проблемы в правильных местах. Тихие сбои обычно вызваны стремлением к надежному выполнению и отсутствием гарантии качества того, что воспринимается как «просто» инфраструктурный код: во-первых, алгоритмы машинного обучения намеренно устойчивы к зашумленным данным, так что они продолжают обучать модель, даже если данные не были подготовлены, нормализованы или обновлены должным образом. Во-вторых, конвейеры часто взаимодействуют со многими другими частями системы, такими как компоненты, собирающие данные, компоненты хранения данных, компоненты для маркировки данных, инфраструктура, выполняющая обучающие задания, и компоненты для развертывания. Эти взаимодействия с другими частями системы редко хорошо специфицируются или тестируются. В-третьих, конвейеры часто состоят из сценариев, выполняющих несколько разных шагов, и может показаться, что они работают, даже если отдельные шаги завершаются сбоем, тогда как последующие шаги могут работать с неполными результатами или промежуточными результатами предыдущих запусков. Код обнаружения ошибок и исправления ошибок часто не является приоритетом в коде обработки данных, даже когда он перемещается в рабочую среду.
Обзор кода для конвейеров машинного обучения
Весь код науки о данных можно просмотреть, как и любой другой код в системе. Это может включать в себя дополнительные проверки изменений, а также более глубокую проверку конкретных фрагментов кода перед развертыванием. Как обсуждалось в главе Основы обеспечения качества, проверка кода может обеспечить множество преимуществ при умеренных затратах, включая обнаружение проблем, обмен знаниями и повышение осведомленности в командах. Например, специалисты по данным могут обнаруживать проблемные преобразования данных или учиться у других специалистов по данным при просмотре их кода или могут давать предложения по улучшению моделирования.
На ранних стадиях исследования обычно не стоит просматривать все изменения в коде обработки данных в блокноте, поскольку код постоянно изменяется и заменяется (см. также главу Модели процессов обработки данных и разработки программного обеспечения ). Однако после того, как конвейер будет подготовлен для использования в рабочей среде, может быть хорошей идеей просмотреть весь код конвейера и оттуда просмотреть любые дальнейшие изменения, используя традиционную проверку кода. Когда большой объем кода переносится в рабочую среду, может быть полезно провести отдельную более систематическую проверку конвейерного кода (например, всей записной книжки, а не только изменения) для выявления проблем или сбора предложений по улучшению. Например, рецензент может предложить нормализовать функцию перед обучением или заметить, что строка df["count"].astype(str).astype(int)
на самом деле не меняет никаких данных, потому что она не выполняет операции на месте.
Проверка кода особенно эффективна для проблем с наукой о данных, которые трудно обнаружить с помощью тестирования, таких как неэффективное кодирование функций, плохое решение проблем с качеством данных или плохая защита личных данных. Помимо проблем, связанных с наукой о данных, проверка кода также может выявить многие другие, более традиционные проблемы, включая проблемы со стилем и плохой документацией, неправильное использование библиотек, неэффективные шаблоны кодирования и традиционные ошибки. Контрольные списки эффективны для того, чтобы сфокусировать действия по проверке кода и направлять рецензентов в систематическом поиске проблем, которые иначе было бы трудно найти.
Тестирование компонентов конвейера
Тестирование преднамеренно выполняет код с выбранными входными данными, чтобы проверить, ведет ли код ожидаемое. Код в конвейерах машинного обучения, который преобразует данные или взаимодействует с другими компонентами в системе, можно тестировать так же, как и любой другой код.
Тестируемость и модульность («от ноутбуков до конвейеров»)
Гораздо проще тестировать небольшие и четко определенные блоки кода, чем большие и сложные программы. Следовательно, инженеры-программисты обычно разбивают сложные программы на небольшие блоки (например, модули, объекты, функции), каждый из которых может быть определен и протестирован независимо. Каждый отдельный оператор if в программе может удвоить количество путей в программе, которые, возможно, потребуется протестировать, экспоненциально увеличивая сложность с увеличением количества решений. Когда фрагмент кода имеет мало внутренних решений и ограничивает взаимодействие с другими частями системы, гораздо проще идентифицировать входные данные, которые представляют различные ожидаемые (и недопустимые) пути выполнения.
Большая часть кода для обработки данных изначально пишется в блокнотах и сценариях, обычно с минимальной структурой и абстракциями, но с множеством глобальных переменных. Код Data Science также обычно автономен, поскольку он загружает данные из определенного источника и просто выводит результаты, часто без какой-либо параметризации. Все это затрудняет тестирование кода обработки данных в записных книжках и сценариях, потому что (1) мы не можем легко выполнить код с разными входными данными, (2) мы не можем легко изолировать и отдельно тестировать разные части записной книжки независимо друг от друга и (3) нам может быть трудно автоматически проверять выходные данные, если они выводятся только на консоль.
В главе Автоматизация конвейера машинного обучения мы приводили доводы в пользу переноса кода конвейера из блокнотов в модульные реализации (например, отдельные функции или компоненты для каждого этапа преобразования или конвейера). Эта модульность также очень полезна для того, чтобы сделать конвейер более тестируемым. То есть код преобразования данных в середине записной книжки, который изменяет значения в определенном фрейме данных, может быть преобразован в функцию, которая может работать с разными фреймами данных и возвращать измененный фрейм данных или результат вычисления. Теперь эту функцию можно намеренно протестировать, предоставив разные значения для фрейма данных и наблюдая, правильно ли были выполнены преобразования, даже в крайних случаях.
# typical data science code from a notebook df = pd.read_csv('data.csv', parse_dates=True) # data cleaning # ... # feature engineering df['month'] = pd.to_datetime(df['datetime']).dt.month df['dayofweek']= pd.to_datetime(df['datetime']).dt.dayofweek df['delivery_count'] = boxcox(df['delivery_count'], 0.4) df.drop(['datetime'], axis=1, inplace=True) dummies = pd.get_dummies(df, columns = ['month', 'weather', 'dayofweek']) dummies = dummies.drop(['month_1', 'hour_0', 'weather_1'], axis=1) X = dummies.drop(['delivery_count'], axis=1) y = pd.Series(df['delivery_count']) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1) # training and evaluation lr = LinearRegression() lr.fit(X_train, y_train) print(lr.score(X_train, y_train)) print(lr.score(X_test, y_test)) # after restructuring into separate function def encode_day_of_week(df): if 'datetime' not in df.columns: raise ValueError("Column datetime missing") if df.datetime.dtype != 'object': raise ValueError("Invalid type for column datetime") df['dayofweek']= pd.to_datetime(df['datetime']).dt.day_name() df = pd.get_dummies(df, columns = ['dayofweek']) return df # ... def prepare_data(df): df = clean_data(df) df = encode_day_of_week(df) df = encode_month(df) df = encode_weather(df) df.drop(['datetime'], axis=1, inplace=True) return (df.drop(['delivery_count'], axis=1), encode_count(pd.Series(df['delivery_count']))) def learn(X, y): lr = LinearRegression() lr.fit(X, y) return lr def pipeline(): train = pd.read_csv('train.csv', parse_dates=True) test = pd.read_csv('test.csv', parse_dates=True) X_train, y_train = prepare_data(train) X_test, y_test = prepare_data(test) model = learn(X_train, y_train) accuracy = eval(model, X_test, y_test) return model, accuracy
Пример линейного и свободного от абстракций кода обработки данных из записной книжки и того, как его можно разделить на несколько отдельных функций.
Весь конвейерный код, включая сбор данных, очистку данных, извлечение функций, обучение модели и этапы оценки модели, должен быть написан в модульной, воспроизводимой и тестируемой реализации, как правило, в виде отдельных функций с четкими входными и выходными данными и четкими зависимостями от библиотек и другие компоненты системы (при необходимости).
Многие предложения инфраструктуры для конвейеров обработки данных теперь поддерживают запись шагов конвейера в виде отдельных функций с инфраструктурой, а затем управление планированием выполнения и перемещением данных между функциями. Например, для этой оркестровки модульных блоков можно использовать фреймворки потоков данных, такие как Luigi, DVC, Airflow, d6tflow и Ploomber, особенно если шаги выполняются долго и должны планироваться и распределяться гибко. . Несколько облачных провайдеров предоставляют услуги по размещению и выполнению целых конвейеров и экспериментальной инфраструктуры с их инфраструктурой, например DataBricks и AWS SageMaker Pipelines.
Автоматизация тестов и непрерывная интеграция
Чтобы на самом деле протестировать код в конвейерах, мы можем вернуться к стандартным инструментам тестирования программного обеспечения. Обычно тесты пишутся в среде тестирования, чтобы все тесты могли выполняться автоматически с помощью тестового драйвера среды тестирования. Для кода Python pyunit — это популярный тестовый драйвер, который выполняет все тестовые функции в файле и сообщает о результатах тестирования.
def test_day_of_week_encoding(): df = pd.DataFrame({'datetime': ['2020-01-01','2020-01-02','2020-01-08'], 'delivery_count': [1, 2, 3]}) encoded = encode_day_of_week(df) assert "dayofweek_Wednesday" in encoded.columns assert (encoded["dayofweek_Wednesday"] == [1, 0, 1]).all() # more tests...
Пример теста для функции `encode_day_of_week`, проверяющего, правильно ли функция добавляет столбец во фрейм данных с ожидаемыми закодированными значениями для дня недели.
Точно так же можно написать тест всего конвейера (по сути, интеграционные тесты), выполняя весь конвейер с преднамеренными входными данными и проверяя, соответствует ли результат оценки модели ожиданиям.
def test_pipeline(): train = pd.read_csv('pipelinetest_training.csv', parse_dates=True) test = pd.read_csv('pipelinetest_test.csv', parse_dates=True) X_train, y_train = prepare_data(train) X_test, y_test = prepare_data(test) model = learn(X_train, y_train) accuracy = eval(model, X_test, y_test) assert accuracy > 0.9
Пример интеграционного теста, который одновременно выполняет несколько шагов конвейера, но на фиксированных тестовых данных с заданными ожиданиями точности.
Все эти тесты теперь также могут выполняться как часть непрерывной интеграции при каждом изменении кода конвейера (см. главу Основы обеспечения качества).
Минимизация и заглушка зависимостей
Небольшие модульные блоки кода с небольшим количеством внешних зависимостей гораздо легче тестировать, чем более крупные модули со сложными зависимостями. Например, код очистки данных и кодирования признаков гораздо проще тестировать, если он получает данные для обработки в качестве аргумента функции очистки, а не извлекает такие данные из внешнего файла. Например, в нашем примере мы можем намеренно передавать разные входные данные функции encode_day_of_week, что было невозможно, когда источник данных был жестко закодирован в исходном немодулярном коде.
Важно отметить, что тестирование с внешними зависимостями обычно нежелательно, если эти зависимости могут меняться между выполнениями теста или если они даже зависят от рабочего состояния производственной системы. Обычно лучше изолировать тесты от таких зависимостей, если они не имеют отношения к тесту. Например, если тестируемый код отправляет запрос в веб-API для получения данных, результат вычислений может измениться, поскольку API возвращает разные результаты, что затрудняет написание конкретных утверждений для теста. Если база данных иногда временно недоступна или работает медленно, результаты тестирования могут показаться ненадежными, даже если код конвейера работает должным образом. Хотя есть смысл также тестировать взаимодействие нескольких компонентов, предпочтительно тестировать поведение изолированно, насколько это возможно, чтобы уменьшить сложность и избежать шума из-за нерелевантных помех.
Не все зависимости легко устранить. Если перемещение вызовов к внешним зависимостям невозможно или нежелательно, можно заменить эти зависимости заглушкой (или макетом или тестовым двойным ) во время тестирования. Заглушка реализует тот же интерфейс, что и внешняя зависимость, но предоставляет простую фиксированную реализацию для тестирования, которая всегда возвращает одни и те же фиксированные результаты без фактического использования внешней зависимости. Более сложные библиотеки фиктивных объектов, такие как unittest.mock, могут помочь в написании объектов с конкретными ответами на различные вызовы. Теперь тесты могут выполняться детерминировано без каких-либо внешних вызовов.
# original implementation hardcodes external API def clean_gender_original(df): def clean(row): if pd.isnull(row['gender']): row['gender'] = gender_api_client.predict(row['firstname'], row['lastname'], row['location']) return row return df.apply(clean, axis=1) # decouple implementation from API def clean_gender(df, model): def clean(row): if pd.isnull(row['gender']): row['gender'] = model(row['firstname'], row['lastname'], row['location']) return row return df.apply(clean, axis=1) # test implementation with stub def test_do_not_overwrite_gender(): def model_stub(first, last, location): return 'M' df = pd.DataFrame({'firstname': ['John', 'Jane', 'Jim'], 'lastname': ['Doe', 'Doe', 'Doe'], 'location': ['Pittsburgh, PA', 'Rome, Italy', 'Paris, PA '], 'gender': [np.nan, 'F', np.nan]}) out = clean_gender(df, model_stub) assert(out['gender'] ==['M', 'F', 'M']).all()
Пример кода очистки данных, который заполняет отсутствующую информацию о поле в наших данных о клиентах. Внешняя модель машинного обучения (называемая службой удаленного вывода) используется для определения пола на основе имени и местоположения клиента. Чтобы сделать этот код более пригодным для тестирования, функция отделена от конкретного API, который теперь передается в качестве аргумента. Теперь мы можем протестировать код очистки без внешнего API, вызвав функцию очистки с альтернативной жестко запрограммированной реализацией зависимости (`model_stub`), которая обеспечивает предсказуемое поведение во время тестирования. Таким образом, несколько тестов могут преднамеренно внедрять разное поведение для разных тестов, даже не имея дело с серверной частью вывода реальной модели.
Концептуально тестовые драйверы и заглушки заменяют рабочий код с обеих сторон тестируемого кода. С одной стороны, вместо того, чтобы вызывать код из производственного кода (например, из конвейера или из пользовательского интерфейса), автоматизированные модульные тесты действуют как тестовые драйверы, которые решают, как вызывать тестируемый код. С другой стороны, заглушки заменяют внешние зависимости (при необходимости), чтобы тестируемый код мог выполняться изолированно. Обратите внимание, что тестовый драйвер должен настроить заглушку при вызове тестируемого кода, обычно передавая заглушку в качестве аргумента.
Проверка обработки ошибок
Хороший способ избежать скрытых ошибок в конвейерах машинного обучения — четко указать обработку ошибок для всего кода конвейера: что должно произойти, если в обучающих данных пропущены некоторые значения? Что должно произойти во время разработки признаков, если отсутствует целый столбец? Что должно произойти, если время ожидания внешней службы вывода модели, используемой во время разработки признаков, истекло? Что должно произойти, если загрузка обученной модели не удалась?
На любой из этих вопросов нет единственно правильного ответа, но инженеры, разрабатывающие надежные конвейеры, должны учитывать различные сценарии ошибок, особенно в отношении качества данных и дисковых и сетевых операций. Разработчики могут выбрать (1) реализовать механизмы восстановления, такие как заполнение отсутствующих значений или повторная попытка неудачных сетевых подключений, или (2) сообщить об ошибке, создав исключение, которое будет обработано клиентом, вызывающим код. Мониторинг того, как часто запускаются механизмы восстановления или исключения, может помочь определить, когда проблемы со временем усугубляются. В идеале предполагаемое поведение обработки ошибок документируется и тестируется.
Как механизмы восстановления, так и преднамеренное создание исключений при недопустимых входных данных или ошибках среды можно явно протестировать в модульных тестах. Модульный тест, предоставляющий недопустимые входные данные, будет либо подтверждать фиксированное поведение, либо утверждать, что код завершается с ожидаемым исключением.
def test_invalid_day_of_week_data(): df = pd.DataFrame({'datetime_us': ['01/01/2020'], 'delivery_count': [1]}) with pytest.raises(ValueError): encode_day_of_week(df)
Пример модульного теста, который гарантирует, что функция `encode_day_of_week` правильно отклоняет недопустимые входные данные (здесь неправильное имя столбца) с ошибкой ValueError.
Если в коде есть внешние зависимости, которые могут вызвать проблемы на практике (например, потому что он использует сетевые подключения, которые могут быть не всегда доступны), обычно рекомендуется убедиться, что код также обрабатывает ошибки из этих зависимостей. С этой целью заглушки представляют собой мощный механизм имитации сбоев в тестовом примере, чтобы гарантировать, что система либо правильно восстановится после смоделированного сбоя, либо выдаст правильное исключение, если восстановление невозможно. Заглушки можно использовать для имитации множества различных дефектов внешних компонентов, таких как обрыв сетевых соединений, медленные ответы и неправильные ответы. Например, мы могли бы внедрить проблемы с подключением, ведущие себя так, как будто удаленный сервер недоступен с первой попытки, чтобы проверить, правильно ли механизм повторных попыток восстанавливается после кратковременного простоя, но также и то, что он выдает исключение после третьей неудачной попытки. .
## testing retry mechanism from retry.api import retry_call import pytest # stub of a network connection, sometimes failing class FailedConnection(Connection): remaining_failures = 0 def __init__(self, failures): self.remaining_failures = failures def get(self, url): print(self.remaining_failures) self.remaining_failures -= 1 if self.remaining_failures >= 0: raise TimeoutError('fail') return "success" # function to be tested, with recovery mechanism def get_data(connection, value): def get(): return connection.get('https://replicate.npmjs.com/registry/'+value) return retry_call(get, exceptions = TimeoutError, tries=3, delay=0.1, backoff=2) # 3 tests for no problem, recoverable problem, and not recoverable def test_no_problem_case(): connection = FailedConnection(0) assert get_data(connection, '') == 'success' def test_successful_recovery(): connection = FailedConnection(2) assert get_data(connection, '') == 'success' def test_exception_if_unable_to_recover(): connection = FailedConnection(10) with pytest.raises(TimeoutError): get_data(connection, '')
Пример тестирования механизма восстановления при сбое сетевого подключения с использованием заглушки для этого подключения, которая преднамеренно создает сетевые проблемы. Код должен работать, когда нет сетевых проблем и когда есть восстанавливаемые сетевые проблемы, и он должен выдавать исключение, если проблема не может быть устранена с трех попыток.
Тот же вид тестирования следует применять и к этапам развертывания в конвейере, чтобы гарантировать, что неудачные развертывания будут замечены и сообщены правильно. Опять же, заглушки можно использовать для проверки правильной обработки ситуаций, когда загрузка моделей не удалась или контрольные суммы не совпадают после развертывания.
Для обработки ошибок и кода восстановления часто рекомендуется регистрировать возникновение проблемы, даже если система восстановилась после нее. Затем системы мониторинга могут подавать сигналы тревоги, когда проблемы возникают необычно часто. Конечно, мы также можем написать тесты, чтобы проверить, правильно ли увеличился счетчик, как часть тестовых случаев, проверяющих обработку ошибок с внедренными ошибками.
from prometheus_client import Counter connection_timeout_counter = Counter( 'connection_retry_total', 'Retry attempts on failed connections') class RetryLogger(): def warning(self, fmt, error, delay): connection_timeout_counter.inc() retry_logger = RetryLogger() def get_data(connection, value): def get(): return connection.get('https://replicate.npmjs.com/registry/'+value) return retry_call(get, exceptions = TimeoutError, tries=3, delay=0.1, backoff=2, logger = retry_logger)
Пример использования счетчика Prometheus для записи каждого сбоя подключения и повторной попытки, который затем можно отслеживать на панели инструментов и в инфраструктуре оповещения, такой как Grafana.
На чем сосредоточить тестирование
Конвейеры обработки данных часто содержат множество рутинных шагов, основанных на общих библиотеках, таких как pandas или scikit-learn. Обычно мы предполагаем, что эти библиотеки уже протестированы и не нужно писать собственные тесты, чтобы убедиться, что библиотека правильно реализует свои функции. Например, мы не будем проверять, правильно ли scikit-learn вычисляет среднеквадратичную ошибку, правильно ли реализован метод groupby в panda или правильно ли Hadoop распределяет вычисления по большим наборам данных. Напротив, полезно проверить, работает ли наш собственный код преобразования данных, использующий эти библиотеки, как задумано.
Проверки качества данных. Любой код, который получает данные, должен проверять качество данных, и эти проверки качества данных должны быть проверены, чтобы убедиться, что код правильно обнаруживает и, возможно, исправляет недействительные и некачественные данные. Код качества данных обычно состоит из двух частей:
- Обнаружение. Код анализирует, соответствуют ли предоставленные данные ожиданиям. Проверки качества данных могут выполняться во многих формах, как обсуждалось в главе «Качество данных», включая (1) проверку того, что какие-либо данные вообще были предоставлены, (2) простую проверку схемы, которая выявляет, когда API предоставляет данные в другом формате, и (3) ) более сложные подходы, которые проверяют стабильные распределения во входных данных.
- Восстановить. Код исправляет данные, если обнаружена проблема. Восстановление может просто удалить неверные данные или заменить неверные или отсутствующие данные значениями по умолчанию, но восстановление может также выполнять более сложные действия, такие как вычисление правдоподобных значений из контекста.
И код обнаружения, и код восстановления можно протестировать с помощью модульных тестов. Код обнаружения обычно представляет собой функцию, которая получает данные и возвращает логический результат, указывающий, являются ли данные действительными. Это можно проверить с помощью примеров допустимых и недопустимых данных, как это также показано выше в тестах для encode_day_of_week с допустимыми и недопустимыми фреймами данных. Код восстановления обычно представляет собой функцию, которая получает данные и возвращает восстановленные данные. Для этого тесты могут проверить, что предоставленные неверные данные исправлены должным образом. В приведенном выше примере clean_gender мы протестировали код, который обнаруживает и заменяет отсутствующие значения пола (где восстановление управляется непроверенной функцией); мы могли бы также написать тесты для (внешней) модели, которая предсказывает значение, используемое для ремонта.
Как правило, если устранение проблем с качеством данных невозможно или наблюдается слишком много проблем с качеством данных, конвейер также может принять решение о завершении с ошибкой. Даже если восстановление возможно, конвейер может сообщить о проблеме в систему мониторинга, чтобы определить, является ли проблема распространенной или даже более частой, как показано для механизма повторных попыток в get_data. Как преднамеренное создание сообщений об ошибках, так и мониторинг частоты ремонта могут помочь избежать некоторых распространенных скрытых ошибок.
Код обработки данных. Любой код, связанный с преобразованием данных — например, для разработки функций — заслуживает тщательного изучения. При преобразовании данных часто приходится иметь дело со сложными краеугольными случаями, и легко допустить ошибки, которые бывает трудно обнаружить. Исследователи данных часто проверяют некоторые выборочные данные, чтобы убедиться, что преобразованные данные выглядят правильно, но редко систематически проверяют крайние случаи и редко преднамеренно решают, как обрабатывать недействительные данные (например, выдать исключение или заменить значением по умолчанию).
Например, приведенный ниже код (вариант A) из конкурса Kaggle для приложений Android пытается преобразовать текстовые представления количества загрузок (например, «142», «10k», «100,5M») в числа, но дает неверные результаты для некоторых значений. потому что одно из регулярных выражений соответствует букве «K» в верхнем регистре вместо используемой буквы «k» в нижнем регистре. Простой тест с тремя указанными выше числами мог бы найти ошибку. Другая реализация (вариант B), пытающаяся достичь той же цели, описанной ниже, не предполагала, что значения могут содержать десятичные точки, не соответствующие входным данным, таким как «100,5M». Обратите внимание, что оба варианта молча терпят неудачу, давая неверные результаты для последующего шага обучения. Даже если бы десятичные точки не предполагались, тесты могли бы гарантировать, что будут приняты только ожидаемые значения, то есть только комбинации чисел, за которыми следует «k» или «M», гарантируя, что исключения будут возникать для других входных данных.
# Variant A, returns 10 for “10k” num = data.Size.replace(r'[kM]+$', '', regex=True).astype(float) factor = data.Size.str.extract(r'[\d\.]+([KM]+)', expand =False) factor = factor.replace(['k','M'], [10**3, 10**6]). fillna(1) data['Size'] = num*factor.astype(int) # Variant B, returns 100.5000000 for “100.5M” data["Size"]= data["Size"].replace(regex =['k'], value='000') data["Size"]= data["Size"].replace(regex =['M'], value='000000') data["Size"]= data["Size"].astype(str). astype(float)
Обучающий код. Даже если полный процесс обучения может потребовать больших наборов данных и занять много времени, тесты с небольшими входными наборами данных могут гарантировать, что обучающий код настроен правильно. Например, большинство проблем неправильного использования API и большинство проблем несоответствия размеров и размеров тензоров в глубоком обучении можно обнаружить, выполнив обучающий код с небольшими наборами данных. Может быть полезно передать фиксированный или случайный небольшой набор данных в обучающий код, чтобы гарантировать, что модель в правильном формате и форме обучена без исключений. Проверка правильности библиотеки машинного обучения выходит за рамки того, что разумно в большинстве проектов, если только для проекта не была разработана пользовательская библиотека.
Взаимодействие с другими компонентами. Конвейер взаимодействует со многими другими компонентами системы, и здесь может возникнуть много проблем, например, при загрузке обучающих данных, при взаимодействии с сервером функций, при загрузке сериализованной модели, или при проведении A/B-тестов. Такого рода проблемы связаны с взаимодействием нескольких компонентов. Мы можем проверить, что механизмы локальной обработки ошибок и сообщения об ошибках работают должным образом, как обсуждалось выше. Кроме того, мы можем проверить правильность взаимодействия нескольких компонентов с помощью интеграционного и системного тестирования, к которым мы вскоре вернемся в главе «Интеграция и системное тестирование».
Помимо функциональной корректности. Помимо проверки правильности реализации конвейера, может быть целесообразно учитывать другие качества, такие как задержка, пропускная способность и потребность в памяти, когда конвейеры машинного обучения работают в масштабе с большими наборами данных. Это может помочь гарантировать, что изменения в коде в конвейере не нарушат потребности в ресурсах для выполнения конвейера. Стандартные библиотеки и профилировщики можно использовать так же, как и в программном обеспечении, отличном от ML.
Статический анализ конвейеров машинного обучения
Несмотря на то, что существует несколько недавних исследовательских проектов по статическому анализу кода науки о данных, на момент написания этой статьи готовых к использованию инструментов было немного. Как обсуждалось в главе Основы обеспечения качества, статический анализ можно рассматривать как форму автоматической проверки кода, которая фокусируется на узких конкретных проблемах, часто с использованием эвристики. Если команды осознают, что они часто допускают определенные виды ошибок, они могут написать статический анализатор, который выявляет эту проблему на ранней стадии, когда она возникает снова.
Инструменты статического анализа для языков со статической типизацией, таких как Java и C, обычно более совершенны, чем инструменты для Python и других языков сценариев, обычно используемых в проектах по науке о данных. Есть несколько инструментов статического анализа для Python, которые также можно использовать для кода науки о данных и даже в блокнотах, например Flake8, но большинство из них сосредоточено на проблемах стиля и простых шаблонах ошибок, включая соглашения об именах, форматирование документации, неиспользуемые переменные. и проверки сложности кода.
Академики разработали различные инструменты статического анализа для определенных видов проблем, и мы ожидаем, что в будущем их станет больше. Недавние примеры включают:
- DataLinter ищет подозрительные или неэффективные шаблоны кодирования данных перед задачами машинного обучения, такие как числа, закодированные как строки, перечисления, закодированные как действительные числа, отсутствие нормализации данных с сильными выбросами и повторяющиеся строки данных. . Другие исследователи предложили больше шаблонов и инструментов для других потенциальных проблем с качеством данных.
- Pythia использует статический анализ для обнаружения несоответствия формы в программах TensorFlow, например, когда тензоры, предоставленные библиотеке TensorFlow, не совпадают по своим размерам и размерам.
- Анализ утечки анализирует код науки о данных с помощью анализа потока данных, чтобы найти случаи утечки данных, когда обучающие и тестовые данные не строго разделены, что может привести к переоснащению тестовых данных и слишком оптимистичным результатам точности.
- PySmell и аналогичные детекторы запаха кода для кода науки о данных могут обнаруживать распространенные анти-шаблоны и подозрительные фрагменты кода, которые указывают на неправильные методы кодирования, такие как большое количество плохо структурированного кода глубокого обучения и нежелательная отладка. код.
- mlint анализирует инфраструктуру конвейерного кода, статически обнаруживая использование лучших инженерных практик, таких как использование контроля версий, использование управления зависимостями и написание тестов в проекте.
- Gather — это подключаемый модуль для блокнотов Jupyter, который статически анализирует структуру кода, чтобы определить, какая часть кода необходима для создания определенного рисунка или другого вывода в блокноте. Затем он может создать срез, подмножество блокнота, содержащее только код для создания этого вывода.
Интеграция процессов и зрелость тестирования
Как обсуждалось в главе Основы обеспечения качества, важно интегрировать действия по обеспечению качества в процесс. Если разработчики не пишут тесты, никогда не запускают свои тесты, никогда не запускают свои инструменты статического анализа или просто одобряют каждую проверку кода, не просматривая код, то они вряд ли обнаружат проблемы на раннем этапе.
В целом код обработки данных и конвейерный код, готовый к производству, следует рассматривать как любой другой код в системе, проходящий те же (и, возможно, дополнительные) этапы обеспечения качества. В нем используются те же этапы интеграции процесса, что и в традиционном коде, например, автоматическое выполнение тестов с непрерывной интеграцией при каждой фиксации, автоматический отчет о покрытии и отображение предупреждений статического анализа во время проверки кода.
Поскольку код обработки данных часто разрабатывается в исследовательском стиле в блокноте, а затем преобразуется в более надежный конвейер для производства, не принято писать тесты при написании исходного кода обработки данных, потому что большая его часть все равно выбрасывается при проведении экспериментов. неудача. Это увеличивает нагрузку на написание и тестирование надежного кода при переносе конвейера в рабочую среду. В спешке, чтобы выйти на рынок, может быть мало стимула отступить и инвестировать в тестирование, когда код обработки данных в блокноте уже показывает многообещающие результаты, но обеспечение качества, вероятно, должно быть частью вехи для выпуска модели и должно безусловно, является обязательным условием для автоматизации регулярных запусков конвейера для развертывания обновлений модели без участия разработчиков. Пренебрежение гарантиями качества ведет к скрытым ошибкам, которые обсуждались в этой главе, и может потребовать значительных усилий по исправлению системы позже; мы вернемся к этому вопросу в главе Технический долг.
Руководители проектов должны планировать действия по обеспечению качества для конвейеров, выделять время и четко определять результаты и обязанности. Наличие четкого контрольного списка может помочь гарантировать, что охвачены многие общие проблемы, а не только функциональная правильность определенных преобразований данных. Например, группа в Google представила идею оценки теста ML, состоящей из списка возможных тестов, которые команда может выполнить в рамках конвейера, начисляя баллы за каждую из 28 проблем, протестированных разработчиками. команда и вторая точка для каждой проблемы, где тесты автоматизированы. 28 проблем включают в себя ряд различных вопросов, которые можно проверить, например, приносит ли функция пользу модели, автономная и онлайн-оценка моделей, проверка кода модели, модульное тестирование конвейерного кода, внедрение канареечных выпусков и мониторинг для обучения. обслуживая перекосы, сгруппированные по темам, тесты функций, тесты моделей, тесты инфраструктуры машинного обучения и производственный мониторинг.
Идея отслеживания зрелости методов обеспечения качества в проекте и сравнения оценок по проектам или командам может сигнализировать о важности обеспечения качества для команд и способствовать принятию и документированию методов обеспечения качества как части процесс. Хотя конкретные проблемы из документа Оценка теста ML могут не распространяться на все проекты и могут быть неполными для других, они являются отличной отправной точкой для обсуждения того, какие методы обеспечения качества следует отслеживать или даже требовать.
Краткое содержание
Код для преобразования данных, обучения моделей и автоматизации всего процесса от загрузки данных до развертывания модели в конвейере должен пройти проверку качества, как и любой другой код в системе. В отличие от самой модели с машинным обучением, которая требует различных стратегий обеспечения качества, конвейерный код можно гарантировать так же, как и любой другой код, посредством автоматического тестирования, проверки кода и статического анализа. Тестирование упрощается за счет модульности кода и минимизации зависимостей. Учитывая исследовательский характер науки о данных, обеспечением качества для конвейерного кода часто пренебрегают даже при переходе от ноутбука к производственной инфраструктуре, поэтому полезно приложить явные усилия для интеграции обеспечения качества в процесс.
Дальнейшее чтение
- Список из 28 проблем, которые могут быть протестированы автоматически в конвейерах машинного обучения, и обсуждение тестовых результатов для оценки зрелости методов обеспечения качества команды: 🗎 Брек, Эрик, Шанцин Кай, Эрик Нильсен, Майкл Салиб и Д. Скалли. . «Оценка теста ML: критерий готовности к производству ML и сокращения технического долга.» Международная конференция IEEE по большим данным (Big Data, 2017), стр. 1123–1132. ИИЭР, 2017.
- Обеспечение качества широко освещается в большинстве учебников по разработке программного обеспечения, и существуют специальные книги по тестированию и другим стратегиям обеспечения качества, такие как 🕮 Дастин, Эльфрида. Эффективное тестирование программного обеспечения: 50 конкретных способов улучшить тестирование. Пирсон, 2003 г. и 🕮 Роман, Адам. Тестирование, основанное на мышлении. Издательство Springer International, 2018.
- Примеры академических работ с использованием различных методов статического анализа кода науки о данных: 🗎 Лагувардос, Сифис, Джулиан Долби, Невилл Греч, Анастасиос Антониадис и Яннис Смарагдакис. «Статический анализ формы в программах TensorFlow.» В проц. Европейская конференция по объектно-ориентированному программированию (ECOOP), 2020 г. 🗎 Ян, Ченьянг, Рэйчел А. Брауэр-Синнинг, Грейс А. Льюис и Кристиан Кестнер. «Утечка данных в ноутбуках: обнаружение статики и улучшенные процессы.» проц. Международная конф. Автоматизированная разработка программного обеспечения (2022 г.). 🗎 Хэд, Эндрю, Фред Хохман, Титус Барик, Стивен М. Друкер и Роберт Делайн. «Устранение беспорядка в вычислительных блокнотах.» В материалах конференции CHI 2019 г. по человеческому фактору в вычислительных системах, стр. 1–12. 2019. 🗎 Геси, Иржи, Сики Лю, Джиавэй Ли, Ифтехар Ахмед, Начиаппан Нагаппан, Дэвид Ло, Эдуардо Сантана де Алмейда, Павнит Сингх Кочхар и Линфенг Бао. «Запахи кода в системах машинного обучения.» Препринт arXiv arXiv: 2203.00803 (2022 г.). 🗎 ван Оорт, Барт, Луис Крус, Бабак Лони и Арье ван Дерсен. «Проектные запахи — опыт анализа качества программного обеспечения проектов машинного обучения с помощью mllint».» В проц. МКСЭ СЭИП (2022 г.).
Как и все главы, этот текст выпущен под лицензией Creative Commons 4.0 BY-SA.