За 2 с лишним года, что я проработал в Upside, я постоянно боролся с производственной реализацией моделей машинного обучения. Как и большинство специалистов по данным, я предпочитаю выполнять проектирование функций и разрабатывать свои модели в блокноте Jupyter, сильно полагаясь на pandas и scikit-learn. В зависимости от приложения создаваемые мною функции могут поступать из нескольких внешних API и иметь несколько этапов предварительной обработки. Я редко думаю: Как я собираюсь воссоздать эту функцию в производственной среде и проверить, не является ли созданное мной значение абсолютно бессмысленным?
При создании API машинного обучения для обслуживания прогнозов я обычно определял некоторый вариант OrderedDict
¹, чтобы отслеживать имена и значения функций во время разработки функций. Я называю этот объект контейнером функций и использую его для передачи значений в виде массива NumPy методу .predict
предварительно обученной модели машинного обучения. Это далеко не идеально по многим причинам и несколько раз сводило с ума инженеров Python, с которыми я работаю. Обычно я просто машу руками и говорю: Послушайте, у меня есть« модульный тест для вывода моего конвейера проектирования функций / прогнозирования моделей, и этого достаточно, чтобы сказать, что все работает, как ожидалось».
Мое утверждение выше не совсем неверно, но мы, специалисты по обработке данных, можем добиться большего!
Недавно я начал по-настоящему понимать концепцию типизированных переменных в Python - даже когда я разрабатываю модели в Jupyter Notebook! NamedTuples
и dataclasses
(доступны в Python 3.7+) - две мои любимые вещи, которые я использую при определении входов и выходов различных функций, которые я пишу. За последние несколько недель я полностью переписал серверную службу, которую построил около года назад, чтобы предсказывать задержки рейсов с использованием типизированных переменных. Но когда я добрался до той части кода, которая создавала и сохраняла функции, что-то меня все еще беспокоило. Я преобразовал свой контейнер функций из OrderedDict
в dataclass
², но процесс ввода каждого поля и типа данных казался слишком утомительным и многословным, особенно если мне когда-нибудь понадобится вернуться и что-то изменить в моей модели. Ему также не хватало возможности печатать и проверять значение, присвоенное полю, из коробки.
К счастью, мы можем использовать Pydantic ³ и некоторые простые вспомогательные функции, чтобы облегчить беспокойство, о котором я упоминал выше. Команда Upside Labs уже несколько месяцев использует Pydantic для проверки типов и валидации моделей данных в нескольких серверных сервисах, и это была еще одна чрезвычайно полезная библиотека. Давайте рассмотрим простой пример того, как мы можем использовать его для создания более надежного контейнера функций.
Пример
Допустим, я обучил модель, используя dataframe
ниже (вероятно, это не очень хорошая модель). Функции cat_1
и cat_2
являются категориальными функциями горячего кодирования, а dog
и bird
- непрерывными функциями.
Приведенный ниже фрагмент кода показывает, как мы можем представить это dataframe
как контейнер функций с помощью Pydantic.
Я не буду рассматривать каждую строку кода выше, но вот некоторые из основных моментов, которые я хотел бы выделить:
- Функции оболочки Pydantic
conint
иconfloat
используются для создания ограниченных целочисленных и ограниченных полей с плавающей запятой, соответственно, с использованием включающих границ, указанных параметрамиge
(больше или равно) иle
(меньше или равно). Если мы попытаемся присвоить значение полю, которое не соответствует определенному типу данных или выходит за указанные границы, возникает ошибка PydanticValidationError
. Значения, которые я установил по умолчанию, получены при проверке исходногоdataframe
. - Все поля определены как
T.Optional
со значением по умолчаниюNone
, так что объект-контейнер функций может быть создан, а затем передан из одной функции в другую, а значения функции могут быть присвоены последовательно⁴. - Вспомогательный метод
set_categorical_features
может использоваться для быстрого кодирования набора категориальных функций одновременно! Все, что вам нужно сделать, это указать функцииprefix
иpositive_category
(см. Фрагмент кода ниже, чтобы увидеть пример этого в действии). У меня часто есть категориальные функции с десятками или сотнями категорий, так что этот метод был чрезвычайно удобен. - Вспомогательный метод
set_bulk_features
также можно использовать для одновременной установки нескольких значений функций. Просто передайте емуmapping
словарь имен и значений полей, и метод позаботится обо всем остальном (включая проверку типов и валидацию). - Свойство
numpy_array
можно использовать для создания массива NumPy с правильной формой и порядком значений полей, указанными в исходном обученииdataframe
. Он имеет проверку, чтобы убедиться, что все значения были назначены, и вызовет пользовательскуюFeatureIsNoneError
ошибку, если какое-либо из полей будетNone
. Это свойство будет использоваться для передачи данных функции методу.predict
(или.predict_proba
) предварительно обученной модели машинного обучения.
Затем мы можем написать простой контейнер функций и конвейер прогнозирования модели, используя контейнер AnimalFeatures
, который мы определили выше.
Чтобы защитить себя от прогнозов, сделанных с ошибочными значениями функций, я добавил некоторую try/except
логику, чтобы выявлять любые ошибки проверки или отсутствующих функций, регистрировать предупреждение и возвращать прогноз по умолчанию. Если это произойдет, я не хочу, чтобы код взорвался, я просто не хочу, чтобы модель выдавала неверный прогноз. Подобная логика существует и в производственных сервисах, над которыми я недавно работал, и она очень помогает в поиске ошибок и функций, которые не работают должным образом.
Наконец, мы можем преобразовать исходный dataframe
в контейнер функций, используя простой скрипт для автоматической генерации имен полей, типов данных и границ, проверяя значения в каждом столбце данных. Я постоянно настраиваю свои модели и не хочу беспокоиться о том, как добавление или перемещение столбцов в моем обучении dataframe
повлияет на конвейер производственных функций. Выходные данные функции ниже можно использовать для создания кода, который станет объектом контейнера функций - класс AnimalFeatures
, определенный в приведенном выше фрагменте, был фактически сгенерирован с использованием этого сценария.
Закрытие
Мне бы хотелось получить отзывы от других специалистов по обработке данных, инженеров по машинному обучению или кого-либо еще о том, считаете ли вы, что приведенные выше концепции могут быть полезны для ваших собственных приложений. Обдумывание процесса переноса модели машинного обучения из Jupyter Notebook в производственный микросервис стало для меня отличным учебным опытом, и я надеюсь, что советы и рекомендации, изложенные в этой статье, будут полезны другим разработчикам.
Ох ... и мы набираем сотрудников - загляните на нашу страницу вакансий!
Сноски:
[1]: Использование упорядоченного словаря вместо стандартного имеет то преимущество, что значения словаря возвращаются в определенном порядке. Это очень важно для приложений машинного обучения, потому что модель ожидает функции в том порядке, в котором они использовались во время обучения.
[2]: Изначально я использовал dataclass
вместо NamedTuple
для контейнера функций, потому что dataclasses
изменяемы. Это позволило мне передать объект-контейнер через ряд функций, которые присваивают значения различным полям, которые я определил.
[3]: Огромный привет Rami Choudhury за то, что познакомил меня с Pydantic и за помощь в разработке кода в этом сообщении в блоге!
[4]: Это не является предписанием, это просто то, как я обычно пишу конвейеры производственных функций. Не стесняйтесь адаптировать любую из этой логики под свои нужды.