Как стать автором
Обновить
76.78
Open Data Science
Крупнейшее русскоязычное Data Science сообщество

Чистый AutoML для “грязных” данных: как и зачем автоматизировать предобработку таблиц в машинном обучении

Время на прочтение14 мин
Количество просмотров7.7K

Или “Мне бы такую работу, чтобы поменьше работы.”:)

В данном посте хотелось бы затронуть такую очень известную и много где описанную тему как предобработка табличных данных в Data Science. Вы можете задать вопрос: “А зачем нам это нужно, ничего нового то тут не скажешь?”. Действительно, что может быть банальнее обработки табличных данных для моделей машинного обучения. Но мы постараемся собрать как можно больше информации в одном ультимативном, если так угодно, гайде, и подадим его через призму автоматического машинного обучения (AutoML). 

Дисклеймер: все описанные нами ниже подходы не являются единственно верными. Мы использовали их при развитии нашего open-source AutoML фреймворка FEDOT, у которого безусловно есть свои особенности как в архитектуре, так и в парадигме разработки. Несмотря на то, что мы реализовали довольно объемный блок предобработки изолированно от основной архитектуры проекта, он определенно пропитан духом ядра библиотеки. Приступаем!

Пролог (“Я не чувствую своих признаков. - У тебя их нет!”)

Сначала стоит прояснить (на всякий пожарный), что подразумевается под предобработкой табличных данных в Data Science. Это набор операций, который позволяет трансформировать данные таким образом, чтобы модели машинного обучения могли на таких данных корректно обучаться. В широком смысле в предобработку входит исходная очистка данных (удаление нечитаемых ячеек и столбцов и т.д.), устранение пропусков, кодировка категориальных значений, нормализация данных, удаление выбросов. Ниже в этом посте мы будем понимать под предобработкой только первые три блока. Если хотите подробнее почитать про предобработку табличных данных и какой она бывает, то можете начать с Предварительная обработка данных и продолжить c Data Preprocessing: Concepts.

Итак, потребность для внедрения блока предобработки таблиц (получившегося в итоге довольно большим), в AutoML-фреймворк возникла у нас с командой совсем не сразу. Поначалу нам было достаточно минимальной функциональности: заполнить пропуски, обработать категориальные признаки, нормализовать - передать на вход модели. Однако в течение последних нескольких месяцев у нас появилась задача запустить наш фреймворк на большом количестве датасетов из OpenML, kaggle и прочих источников (например, с ресурса “Departamento de ciencia de computadores”). И только тогда мы поняли насколько в  тепличных условиях мы растили фреймворк до этого - алгоритм не смог запуститься больше чем на половине датасетов. Для запуска мы брали 46 табличных наборов данных, большая часть из которых предназначалась для решения задачи классификации. С этого момента начинается наш путь.  

Рисунок 1. Обычно приходится играть с тем, что справа (мем)
Рисунок 1. Обычно приходится играть с тем, что справа (мем)

Немного про набор данных: размеры таблиц изменялись от совсем маленьких (748 строк на 5 столбцов для “blood-transfusion-service-center”) до довольно больших (4.9 млн строк на 42 столбца для “KDDCup99_full”). Общее количество элементов (умножение количества строк на количество столбцов) изменялось от нескольких тысяч до нескольких сотен миллионов. Всё это нам нужно было для возможности проверить адекватность работы алгоритмов на разных размерах данных (Рисунок 2).

Рисунок 2. Размеры табличных данных для экспериментов (не мем)
Рисунок 2. Размеры табличных данных для экспериментов (не мем)

Помимо проблем с наличием пропусков и большого количества категориальных признаков, данные имеют одну неприятную особенность - в одном столбце могут быть намешаны несколько типов данных, например float и string. Далее мы напишем к каким неприятностям такое соседство привело, а пока просто резюмируем - у датасетов есть некоторые особенности, которые не позволяют запустить на данных модели машинного обучения как есть, без серьёзной предобработки. 

Ссылки на наборы датасетов, которые использовались в экспериментах

Исследование (“Наши люди AutoML без предобработки не запускают”)

Перед тем как начать писать собственные костыли, мы решили посмотреть как подобную проблему решали наши коллеги из других команд. Оговоримся сразу, что рассматривать будем наиболее критические изменения в данных, преобразования в духе “трансформация одномерного target массива в вектор-столбец” подразумевается, что производится при необходимости в любой сколько-нибудь крупной AutoML библиотеке. 

Начнём с фреймворка H2O, где эксплуатируется следующая структура. Предобработка предшествует запуску AutoML алгоритма. В препроцессинге классический джентльменский набор: автоматическое определение типов, кодирование категориальных признаков при помощи OneHotEncoding’a, заполнение пропусков и нормализация при необходимости. Также имеется возможность опциональной предобработки для некоторых моделей - например не для всяких требуется нормализация и т.д. Немного интересных фактов: H2O версии 3.28.1.2 успешно запустился на 24 датасетах из 46.

Популярный академический фреймворк TPOT не является в полной мере “end-to-end” решением, в отличие от H2O. Поэтому большую долю предобработки библиотека оставляет на стороне пользователя, концентрируясь на оптимизации пайплайна. Тем не менее в структуре TPOT есть методы, отвечающие за усвоение входных данных. В частности, в препроцессоре происходит проверка на наличие пропусков, есть различные стандартизаторы и нормализаторы. Разделение на категориальные и вещественные признаки происходит на основе количества уникальных значений в столбце. В отличие от H2O опциональной предобработки не предусмотрено - препроцессинг выступает единым блоком предшествующим запуску эволюционного алгоритма для поиска модели. Пусть предобработка не является сильной стороной данного фреймворка, он широко известен и довольно надежен. Хотя вычистить исходный датасет всё же рекомендуется перед подачей данных. Например, фреймворк не переваривает наличие строковых типов данных в массивах.

В фреймворке LightAutoML также присутствует стандартная предобработка: заполнение пропусков, обработка категориальных признаков и обработка типов. Типы в терминах фреймворка называются “ролями”, где для каждой “роли” (численный или категориальный тип) предусмотрен свой способ предобработки. Численные признаки проходят процедуру масштабирования. Запуску моделей предшествует несколько ступеней обработки: выбор подходящих признаков и генерация новых. В первом как раз сосредоточен весь арсенал препроцессинга. Препроцессинг обязателен для моделей, но может варьироваться.

В фреймворке AutoGluon реализована двухуровневая предобработка:  моделе-независимая и моделе-специфичная. Данное разделение во первых позволяет избежать лишних вычислений, если они не требуются, а во вторых позволяет сохранить гибкость. Помимо классических операций вроде нормализации, заполнения пропусков и кодирования категорий в фреймворке есть блок переработки datetime индексов в численные признаки. В наших экспериментах AutoGluon показал себя как наиболее надёжный из фреймворков.

Исходя из проведенного мини-обзора можно заключить, что наиболее распространенной схемой предобработки является “гибкая” версия. Набор преобразований для входных данных определяются исходя из структуры моделей, для которых производится предобработка. 

P.S. Краткий обзор здесь естественно не затрагивает особенностей  технических реализаций и более того, может потерять актуальность потому что фреймворки постоянно модифицируются. Также легко можно упустить некоторые детали, когда не занимаешься разработкой инструмента, а только делаешь его обзор. В связи с этим будем признательны, если найдёте несоответствия и предложите что-либо добавить в описание методов предобработки тех или иных инструментов.

Реализация (“Надежная, как швейцарские часы”)

Устранение проблем проходило итеративно, таблица за таблицей мы пробовали запустить библиотеку, и если что-то шло не так, - занимались улучшениями. Решено было реализовать предобработку отдельным блоком перед подачей данных в пайплайн. 

Рассмотрим предобработку на примере вот такой таблички. Пусть мы решаем задачу бинарной классификации и таблица хранится в csv файле: 

Таблица 1. Пример таблицы с признаками для решения задачи бинарной классификации
Таблица 1. Пример таблицы с признаками для решения задачи бинарной классификации

Какие особенности можно выделить у этой таблицы с признаками: 

  • В признаке 1 присутствуют значения плюс минус бесконечность;

  • В признаке 2 слишком много пропущенных значений;

  • В признаке 3 присутствует пропуск. Также отметим, что признак принимает только два возможных значения: 1 и 10;

  • В признаке 4 имеется три категории, однако из-за наличия отступов в строках количество категорий может быть определено как 5;

  • Признак 5 бинарный категориальный и имеет пропуск;

  • Признак 6 содержит в себе типы данных float и string;

  • Целевая переменная содержит пропуски. При этом представлена в виде сырых меток, представляющих собой строки.

Начинаем двигаться слева направо. Сначала заменим все значения плюс минус бесконечность в признаке 1 на nan. Далее удалим признак 2, потому что он содержит слишком много пропущенных значений (порог у нас в фреймворке задан как 90% объектов).

Таблица 2. Преобразование невалидных значений (плюс и минус бесконечность) в NaN, удаление признака с большим процентом пропусков
Таблица 2. Преобразование невалидных значений (плюс и минус бесконечность) в NaN, удаление признака с большим процентом пропусков

После удалим объект из обучающей выборки, значение целевой переменной для которого неизвестно - id 2.

Таблица 3. Удаление строк с неизвестными значениями для целевой переменной
Таблица 3. Удаление строк с неизвестными значениями для целевой переменной

После этого применим LabelEncoder, чтобы перевести метки ‘> 10’ и ‘<= 10’ в 0 и 1 соответственно. Устраним отступы в строках в признаке 4.

Таблица 4. Энкодинг целевой переменной и удаление отступов
Таблица 4. Энкодинг целевой переменной и удаление отступов

Небольшое лирическое отступление. Реализация OneHotEncoding’а в sklearn (а у нас в AutoML мы под капотом используем именно такую реализацию) поддерживает обработку бинарных категориальных признаков и обработку новых категорий, которых операция предобработки не видела во время обучения в тренировочной выборке. Первое значит, что если в таблице имеется категориальный бинарный признак, то его не будут увеличивать путём раздувания до нескольких столбцов. Второе значит, что если в тренировочной выборке были категории, например, “средний” и “маленький”, а в тестовой выборке “большой”, то алгоритм сможет продолжить работу без ошибки. Проблема только в том, что одновременно две этих опции не работают: нужно выбрать, либо одно, либо другое. Мы решили, что для нас важнее, чтобы фреймворк не проседал при обработке новых категорий. 

Поэтому бинарные категориальные признаки (количество уникальных категорий в которых не больше двух) следует обрабатывать отдельно. Так и поступаем с признаком 5, - конвертируем значения в “1” и “0”.

Таблица 5. Энкодинг бинарных категориальных признаков
Таблица 5. Энкодинг бинарных категориальных признаков

Наконец, признак 6. Для него критически важным оказывается результат работы системы определения типов, которая также была реализована как часть предобработки. Она работает абсолютно одинаково для всех столбцов. Но сначала обозначим проблему: в признаке с float значениями есть знак ‘?’, и когда вы такой датафрейм загрузите к себе, то обнаружите, что pandas заботливо перевёл все числа в строки. Звучит не так страшно, однако если количество уникальных значений в признаке велико, а для энкодинга используется One Hot Encoding, то вас удивит размер получившейся после преобразования таблицы. 

К решению. При помощи map мы просматриваем каждый элемент столбца и определяем из каких типов данных состоит столбец и в каких элементах какие типы расположены. В случае с этим столбцом выясняется, что количество уникальных значений намного больше, чем должно быть в категориальном признаке (может конкретно в этом фрагменте это и не так, но будь таблица чуть побольше, то это сразу стало бы заметно). Тогда алгоритм пытается привести столбец к типу float и попутно помечает все значения, которые в этот тип привести не может как nan. Таковым оказывается знак “?” в строке 4 (Таблица 6).

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

Таблица 6. Обработка столбцов со смешанными типами данных. Замена знака “?” на пропущенное значение.
Таблица 6. Обработка столбцов со смешанными типами данных. Замена знака “?” на пропущенное значение.

Итак, несмотря на то, что много всего уже было сделано, в данных всё ещё есть пропуски и категориальные признаки. И да, так и задумано. Теперь переходим ко второму, дополнительному этапу предобработки, который позволяет признаки закодировать и устранить пропуски.

Еще бóльшая реализация (“Я требую продолжения банкета!”)

После того как данные прошли предварительную обязательную обработку, запускается сердце AutoML - эволюционный алгоритм. Именно этот “движок” генерирует множество пайплайнов, и в каждый из них подаётся первично предобработанные данные. Как помните, данные прошли только самую грубую очистку и всё ещё содержат пропуски и строковые типы данных. Поэтому для каждой сгенерированной модели применяется своя предобработка.

Главная задача дополнительной обработки табличек - не позволить модели во время выполнения упасть с ошибкой. Рассмотрим на примере заполнения пропусков. Если алгоритм “понимает”, что если пропуски не заполнить, то пайплайн не сможет обучиться, то применяется стратегия заполнения пропусков по умолчанию. Если же в структуре пайплайна есть модели, способные данные с пропусками усвоить, то таблица пройдёт далее нетронутой. Тогда возникает вопрос: “Так почему же не предобработать данные сразу, заполнить пропуски и применить энкодер? Надежней же.”. Дело в том, что способов заполнения пропусков существует довольно много, равно как и способов энкодинга много больше двух. Поэтому ограничивать пространство поиска только одним способом не хочется - AutoML алгоритм сам разберётся как ему лучше преобразовать данные, чтобы получить самую точную модель. 

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

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

  • могут переработать данные и устранить проблему (наличие пропусков например);

  • могут данные пропустить через себя, но при этом не устраняют источник ошибки;

  • при проведении операций над данными вызывают ошибку.

Рисунок 3. Анализ структуры композитной модели для применения дополнительной предобработки на примере наличия пропусков в табличных данных
Рисунок 3. Анализ структуры композитной модели для применения дополнительной предобработки на примере наличия пропусков в табличных данных

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

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

Представим, что в структуре пайплайна никаких операций предобработки не имеется и у алгоритма стоит необходимость заполнить пропуски и закодировать категориальные значения по умолчанию. Сначала разберёмся с пропусками: они присутствуют в признаках 1, 3, 5 и 6. Для столбцов 1 и 6 применяется самая обычная стратегия заполнения пропусков средним значением. Интереснее со столбцами 3 и 5, - столбцы содержат числовые значения, однако количество уникальных значений, которые могут принимать показатели в этих столбцах, не превышает двух. А много ли вы знаете числовых признаков, которые принимают всего два значения? - Не давая на размышление 15 сек, предположим, - признак скорее всего обозначает присутствие или отсутствие чего-либо у объекта. Объект либо имеет, например, хвост, либо не имеет. Полутонов вводить тут не стоит. Поэтому в таких столбцах заполним значения мажоритарным классом. В случае, если количество категорий равно (это в действительности случается исчезающе редко), то для бинарного признака пропуски заполняются всё таки средним значением. 

Таблица 7. Заполнение пропусков в таблице по умолчанию (восстановленные значения показаны зелёным)
Таблица 7. Заполнение пропусков в таблице по умолчанию (восстановленные значения показаны зелёным)

С категориями проще - стратегией кодировки по умолчанию является OneHotEncoding (Таблица 8).

Таблица 8. Преобразование категориальных признаков
Таблица 8. Преобразование категориальных признаков

Развязка (“Честно говоря, моя дорогая, мне наплевать”)

Попробуем повторить все те навороты, которые мы реализовали в ходе разработки блока предобработки в AutoML фреймворке. 

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

В обязательную входит: 

  1. Замена значений плюс минус бесконечность в признаках и целевом столбце на nan

  2. Удаление признаков, число пропущенных значений в которых превышает 90%

  3. Удаление объектов из таблицы, для которых неизвестно значение целевой переменной (nan в target)

  4. Перевод меток в целевом столбце из строкового формата в целочисленный

  5. Устранение отступов в строковых объектах в признаках

  6. Усвоение столбцов с разными типами данных, заключающееся в детекции столбцов со смешанными типами и их приведение к мажоритарному, какому-либо возможному или удаление столбца из таблицы.

Дополнительная:

  1. Анализ структуры пайплайна на наличие операций заполнения пропусков - заполнение пропусков при необходимости;

  2. Анализ структуры пайплайна на наличие операций кодирования категориальных значений - применение энкодинга при необходимости

Все приведенные выше модфикации позволяют фреймворку запускаться на рассмотренных датасетах (даже на самых страшных) именно с фразой из названия раздела.

Позволю себе и читателю здесь немного остановиться и поразмышлять. Всё что было описано выше это сложная техническая задача (без шуток). Но интересна ли она исследователю или заказчику? - скорее всего нет. Всю эту возню с данными обычно хочется проскочить как можно быстрее и перейти к разработке модели, ведь именно это самая захватывающая часть. Только представьте, сколько кода потребовалось бы написать, чтобы предобрабатывать данные таким образом. Так не лучше ли отдать всю предобработку на сторону AutoML алгоритмов?

Пример предобработки при помощи AutoML

Заметим, что иногда самим хочется построить модель, а не отдавать AutoML алгоритму самую интересную часть. Тогда поручить всю предобработку можно написав следующие строки кода для FEDOT:

import numpy as np 
from fedot.core.data.data import InputData 
from fedot.core.pipelines.node import PrimaryNode 
from fedot.core.pipelines.pipeline import Pipeline 
from fedot.core.repository.dataset_types import DataTypesEnum
from fedot.core.repository.tasks import Task, TaskTypesEnum 

train_input = InputData(idx=np.arange(0, len(features)), features=features, target=target, task=Task(TaskTypesEnum.classification), data_type=DataTypesEnum.table) 

pipeline = Pipeline(PrimaryNode('scaling')) 
pipeline.fit(train_input) 
preprocessed_output = pipeline.predict(train_input) 
transformed_data = preprocessed_output.predict

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

Эпилог. Эксперимент (“Тебя запустют, а ты досчитай!”)

Поскольку запускать фреймворк на всех таблицах дело небыстрое, мы подготовили небольшой игрушечный датасет в репозитории automl-crash-test. Таблица для решения задачи классификации совсем небольшая, но представляет собой собирательный образ всего того плохого, с чем мы столкнулись во время запусков на всех 46 датасетах. 

Если захотите запустить свой любимый фреймворк на этих данных - welcome. Пройти такой краш-тест на самом деле не так то просто как кажется. А выбить хороший скор (ROC AUC 1.0) - результат просто отличный. Набрать ROC AUC 1.0 на тестовой выборке для данной таблицы возможно только корректно предобработав хотя бы некоторые столбцы, отбросив все ненужные и оставив нужные. 

Ниже приведен пример запуска фреймворка FEDOT (версия 0.5.2) на этих данных. Результат выполнения программы для нас самый заветный - никаких ошибок не возникло и мы смогли получить финальное решение. Также весь код и зависимости для запуска есть в репозитории.

from fedot.api.main import Fedot
from sklearn.metrics import classification_report, roc_auc_score

from data.data import get_train_data, get_test_data

train_features, train_target = get_train_data() 
test_features, test_target = get_test_data() 

# Task selection, initialisation of the framework 
fedot_model = Fedot(problem='classification', timeout=timeout) 

# Fit model 
obtained_pipeline = fedot_model.fit(features=train_features, target=train_target) 
obtained_pipeline.show() 

# Make predictions
predict = fedot_model.predict(test_features) 
predict_probs = fedot_model.predict_proba(test_features)

Финальная метрика на тестовой части - 1.0 ROC AUC.

Заключение (“Вы привлекательны, я чертовски привлекателен… Чего время терять?”)

Как можно заметить, все вышеперечисленные внедренные в AutoML преобразования не являются новейшими технологиями. Однако их удачное сочетание позволяет добиться запуска моделей машинного обучения даже в тех случаях, когда табличные данные представляют собой самый настоящий “garbage in”. 

Естественно, что все описания стоит рассматривать через призму разработки AutoML инструмента, который в нашей парадигме должен работать надежно там, где junior Data Scientist быстро и надёжно работать не сможет (или сможет, но не очень надежно или не очень быстро). И, возможно, если такая предобработка подошла для наших моделей, она сможет подойти и вам для ваших.  

Полезные ссылки:

В выступлении со своеобразным гайдом по предобработке табличных данных участвовали: Сарафанов Михаил и команда NSS lab

Теги:
Хабы:
+13
Комментарии2

Публикации

Информация

Сайт
ods.ai
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия

Истории