/
Author: Райан Марк Массарон Лука
Tags: программотехника разработка вычислительных систем искусственный интеллект программирование машинное обучение искусственные нейронные сети
ISBN: 978-5-4461-4391-7
Year: 2026
Text
Advanced Analytics
for Business
GENERATIVE AI AND MACHINE LEARNING
FOR TABULAR DATA
M ARK RYAN, LUCA M ASSARON
FOREWORD BY A NTONIO GULLI
Машинное обучение
на табличных данных
XGBOOST, ГЛУБОКОЕ ОБУЧЕНИЕ И ИИ
М АРК РАЙАН, Л УКА М АССАРОН
ПРЕДИСЛОВИЕ А НТОНИО Г УЛ ЛИ
2026
ББК 32.973.2-018 +32.813
УДК 004.41+004.85
Р18
Райан Марк, Массарон Лука
Р18 Машинное обучение на табличных данных: XGBoost, глубокое обучение и ИИ. —
СПб.: Питер, 2026. — 560 с.: ил. — (Серия «Библиотека программиста»).
ISBN 978-5-4461-4391-7
Машинное обучение может ускорить выполнение повседневных бизнес-задач, таких как сверка счетов,
прогнозирование спроса или автоматизация обслуживания клиентов, не говоря уже о более сложных задачах,
например выявление мошенничества, прогнозное техническое обслуживание и персонализированный маркетинг. Извлекайте важную информацию из электронных таблиц, реестров, баз данных и других источников,
используя градиентный бустинг, глубокое обучение и генеративный ИИ.
«Машинное обучение на табличных данных» описывает практические методы машинного обучения для
модернизации каждого этапа анализа бизнес-данных. Вас ждут примеры использования XGBoost и Keras для
прогнозирования цен на краткосрочную аренду недвижимости, развертывание локальной модели машинного
обучения с помощью Python и Flask и оптимизация рабочих процессов с использованием больших языковых
моделей (LLM). Научитесь делать свои модели более мощными и интерпретируемыми.
16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ББК 32.973.2-018 +32.813
УДК 004.41+004.85
Права на издание получены по соглашению с Manning Publications. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может
гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные
ошибки, связанные с использованием книги. В книге возможны упоминания организаций, деятельность которых
запрещена на территории Российской Федерации, таких как Meta Platforms Inc., Facebook, Instagram и др. Издательство не несет ответственности за доступность материалов, ссылки на которые вы можете найти в этой
книге. На момент подготовки книги к изданию все ссылки на интернет-ресурсы были действующими.
ISBN 978-1633438545 англ.
ISBN 978-5-4461-4391-7
Authorized translation of the English edition © 2024 Manning Publications.
This translation is published and sold by permission of Manning Publications,
the owner of all rights to publish and sell the same.
© Перевод на русский язык ООО «Прогресс книга», 2026
© Издание на русском языке, оформление ООО «Прогресс книга», 2026
© Серия «Библиотека программиста», 2026
Краткое содержание
Часть 1
Понятие о машинном обучении
для табличных данных
Глава 1. Табличные данные................................................................................................ 32
Глава 2. Табличные датасеты............................................................................................. 45
Глава 3. Машинное и глубокое обучение...................................................................... 99
Часть 2
Машинное обучение и градиентный бустинг
для табличных данных
Глава 4. Классические алгоритмы для табличных данных................................... 130
Глава 5. Деревья решений и градиентный бустинг.................................................. 178
Глава 6. Продвинутые методы обработки признаков............................................. 245
Глава 7. Полный пример применения XGBoost....................................................... 302
Часть 3
Глубокое обучение
для анализа табличных данных
Глава 8. С чего начать?...................................................................................................... 365
Глава 9. Лучшие практики глубокого обучения....................................................... 395
6 Краткое содержание
Глава 10. Развертывание модели.................................................................................... 431
Глава 11. Пайплайн машинного обучения.................................................................. 473
Глава 12. Комбинация градиентного бустинга и глубокого обучения.............. 524
Приложение А. Г
иперпараметры для классических моделей
машинного обучения.......................................................................... 543
Приложение Б. Методы k ближайших соседей и опорных векторов............... 547
Оглавление
От издательства..................................................................................................................... 18
О научном редакторе русского издания.................................................................. 18
Предисловие............................................................................................................................ 19
Введение................................................................................................................................... 21
Благодарности........................................................................................................................ 22
Об этой книге.......................................................................................................................... 24
Для кого эта книга........................................................................................................... 24
Структура книги.............................................................................................................. 25
О коде в книге................................................................................................................... 26
Форум liveBook..................................................................................................................... 28
Об авторах............................................................................................................................... 29
Иллюстрация на обложке.................................................................................................. 30
Часть 1
Понятие о машинном обучении
для табличных данных
Глава 1. Табличные данные................................................................................................ 32
1.1. Что такое табличные данные?............................................................................. 33
1.2. Весь мир работает на табличных данных........................................................ 35
8 Оглавление
1.3. Сравнение машинного и глубокого обучения................................................ 36
1.4. В чем особенность табличных данных?........................................................... 38
1.5. Генеративный ИИ и табличные данные........................................................... 41
Итоги.................................................................................................................................... 44
Глава 2. Табличные датасеты............................................................................................. 45
2.1. Свойства строк и столбцов................................................................................... 46
2.1.1. Как должна выглядеть идеальная строка таблицы.......................... 47
2.1.2. Как должен выглядеть идеальный столбец таблицы...................... 52
2.1.3. Представление строк и столбцов............................................................ 55
2.2. Возможные проблемы и способы их устранения.......................................... 57
2.2.1. Столбцы с нулевой или почти нулевой дисперсией . .................... 59
2.2.2. Дублирующиеся и высококоллинеарные признаки........................ 59
2.2.3. Нерелевантные признаки.......................................................................... 62
2.2.4. Пропуски......................................................................................................... 62
2.2.5. Редкие категории......................................................................................... 63
2.2.6. Ошибки в данных......................................................................................... 64
2.2.7. Утечка данных............................................................................................... 64
2.3. Поиск данных вовне и внутри организации................................................... 65
2.3.1. Использование pandas для доступа к хранилищам данных.......... 66
2.3.2. Данные из интернета................................................................................... 71
2.3.3. Синтетические данные............................................................................... 74
2.4. Разведочный анализ данных................................................................................ 78
2.4.1. Загрузка датасета Auto MPG................................................................... 80
2.4.2. Изучение меток, значений, распределений......................................... 82
2.4.3. Изучение двумерных и многомерных взаимосвязей....................... 92
Итоги.................................................................................................................................... 97
Глава 3. Машинное и глубокое обучение...................................................................... 99
3.1. Прогнозирование цен на Airbnb в Нью-Йорке............................................ 100
Оглавление 9
3.1.1. Датасет Airbnb в Нью-Йорке................................................................. 100
3.1.2. Знакомство с кодом................................................................................... 103
3.1.3. Решение для глубокого обучения на Keras....................................... 105
3.1.4. Обучение моделей...................................................................................... 105
3.1.5. Сравнение градиентного бустинга и глубокого обучения........... 107
3.1.6. Выводы.......................................................................................................... 115
3.2. Прозрачность........................................................................................................... 115
3.2.1. Объяснимость............................................................................................. 116
3.2.2. Важность признаков................................................................................. 119
3.2.3. Выводы.......................................................................................................... 121
3.3. Эффективность....................................................................................................... 121
3.3.1. Оценка производительности.................................................................. 122
3.4. Если копнуть глубже............................................................................................ 123
Итоги.................................................................................................................................. 128
Часть 2
Машинное обучение и градиентный бустинг
для табличных данных
Глава 4. Классические алгоритмы для табличных данных................................... 130
4.1. Знакомство с scikit-learn...................................................................................... 131
4.1.1. Общие особенности пакетов scikit-learn............................................ 132
4.1.2. Общий интерфейс scikit-learn............................................................... 134
4.1.3. Введение в пайплайны scikit-learn....................................................... 137
4.2. Исследование и обработка признаков датасета Airbnb NYC.................. 139
4.2.1. Исследование датасета............................................................................. 139
4.2.2. Подготовка пайплайнов........................................................................... 147
4.3. Классическое машинное обучение................................................................... 149
4.3.1. Линейная и логистическая регрессии................................................. 152
4.3.2. Методы регуляризации............................................................................ 157
10 Оглавление
4.3.3. Логистическая регрессия........................................................................ 162
4.3.4. Обобщенные линейные методы............................................................ 166
4.3.5. Обработка больших датасетов с помощью стохастического
градиентного спуска............................................................................................. 169
4.3.6. Выбор алгоритма........................................................................................ 174
Итоги.................................................................................................................................. 175
Глава 5. Деревья решений и градиентный бустинг.................................................. 178
5.1. Знакомство с алгоритмами на основе деревьев........................................... 179
5.1.1. Бэггинг и семплирование........................................................................ 186
5.1.2. Прогнозирование с помощью случайных лесов.............................. 191
5.1.3. Использование особо случайных деревьев....................................... 194
5.2. Градиентный бустинг............................................................................................ 196
5.2.1. Как работает градиентный бустинг..................................................... 199
5.2.2. Экстраполяция с градиентным бустингом........................................ 202
5.2.3. Почему градиентный бустинг эффективен?.................................... 207
5.3. Бустинг в scikit-learn............................................................................................. 210
5.3.1. Применение ранней остановки для предотвращения
переобучения.......................................................................................................... 212
5.4. Использование XGBoost...................................................................................... 216
5.4.1. Основные параметры XGBoost............................................................. 218
5.4.2. Как работает XGBoost.............................................................................. 223
5.4.3. Разбиение на основе гистограмм.......................................................... 226
5.4.4. Применение ранней остановки в XGBoost........................................ 229
5.5. Введение в LightGBM.......................................................................................... 231
5.5.1. Как LightGBM строит деревья.............................................................. 234
5.5.2. Увеличение скорости с помощью эксклюзивного
объединения признаков и одностороннего семплирования
на основе градиента.............................................................................................. 236
5.5.3. Применение ранней остановки в LightGBM.................................... 238
Оглавление 11
5.5.4. Заставляем XGBoost работать, как LightGBM................................ 240
5.5.5. Как LightGBM повлиял на scikit-learn............................................... 240
Итоги.................................................................................................................................. 242
Глава 6. Продвинутые методы обработки признаков............................................. 245
6.1. Особенности обработки признаков................................................................. 246
6.1.1. Многомерная импутация пропущенных значений........................ 248
6.1.2. Обработка пропущенных данных с помощью GBDT................... 252
6.1.3. Кодирование по целевой переменной................................................. 253
6.1.4. Преобразование числовых данных...................................................... 259
6.2. Отбор признаков.................................................................................................... 267
6.2.1. Стабильный отбор предикторов для линейных моделей............ 268
6.2.2. Фоновые признаки и Boruta.................................................................. 270
6.2.3. Прямое включение и обратное исключение..................................... 273
6.3. Оптимизация гиперпараметров........................................................................ 276
6.3.1. Системный поиск....................................................................................... 277
6.3.2. Случайный поиск....................................................................................... 279
6.3.3. Снижение вычислительной нагрузки................................................. 281
6.3.4. Расширение поиска с помощью байесовских методов.................. 283
6.3.5. Ручная настройка гиперпараметров.................................................... 288
6.4. Освоение градиентного бустинга..................................................................... 290
6.4.1. Выбор между XGBoost и LightGBM................................................... 290
6.4.2. Исследование структуры деревьев...................................................... 291
6.4.3. Ускорение с помощью GBDT и компиляции................................... 296
Итоги.................................................................................................................................. 299
Глава 7. Полный пример применения XGBoost....................................................... 302
7.1. Подготовка и исследование данных................................................................ 303
7.1.1. Использование генеративного ИИ для подготовки
данных....................................................................................................................... 303
12 Оглавление
7.1.2. Получение и подготовка данных.......................................................... 304
7.1.3. Генерация более сложных признаков.................................................. 313
7.1.4. Завершение обработки данных............................................................. 321
7.1.5. Исследование и исправление данных................................................. 322
7.1.6. Изучение целевой переменной.............................................................. 327
7.2. Создание и оптимизация модели..................................................................... 329
7.2.1. Подготовка стратегии кросс-валидации............................................ 330
7.2.2. Подготовка пайплайна............................................................................. 332
7.2.3. Построение базовой модели................................................................... 336
7.2.4. Создание первой предварительной модели...................................... 342
7.2.5. Оптимизация модели................................................................................ 344
7.2.6. Обучение финальной модели................................................................ 348
7.3. Объяснение модели с помощью SHAP.......................................................... 351
Итоги.................................................................................................................................. 361
Часть 3
Глубокое обучение для анализа табличных данных
Глава 8. С чего начать?...................................................................................................... 365
8.1. Стек технологий для глубокого обучения на табличных данных......... 366
8.2. PyTorch с fastai........................................................................................................ 371
8.2.1. Ключевые особенности кода решения на fastai............................... 371
8.2.2. Сравнение решений на fastai и Keras.................................................. 379
8.3. PyTorch с TabNet.................................................................................................... 380
8.3.1. Ключевые особенности кода решения на TabNet........................... 380
8.3.2. Сравнение решений на TabNet и Keras............................................... 383
8.4. PyTorch с Lightning Flash.................................................................................... 384
8.4.1. Ключевые особенности решения на Lightning Flash..................... 385
8.4.2. Сравнение решений на Lightning Flash и Keras.............................. 388
8.5. Сравнение всех стеков.......................................................................................... 389
Оглавление 13
8.6. Стеки, которые остались за кадром................................................................. 390
Итоги.................................................................................................................................. 393
Глава 9. Лучшие практики глубокого обучения....................................................... 395
9.1. Знакомство с данными о недвижимости Куала-Лумпура....................... 396
9.2. Обработка датасета................................................................................................ 401
9.2.1. Обработка столбцов Bathrooms, Car Parks, Furnishing,
Property Type и Location..................................................................................... 401
9.2.2. Обработка столбца Price.......................................................................... 403
9.2.3. Обработка столбца Rooms....................................................................... 404
9.2.4. Обработка столбца Size............................................................................ 407
9.3. Определение модели глубокого обучения.................................................... 411
9.3.1. Сравнение подходов с пользовательскими слоями
и слоями предварительной обработки Keras............................................... 412
9.3.2. Изучение кода для определения модели
с использованием слоев предварительной обработки Keras................. 416
9.4. Обучение модели глубокого обучения........................................................... 422
9.4.1. Кросс-валидация в процессе обучения............................................... 423
9.4.2. Регуляризация в процессе обучения................................................... 423
9.4.3. Нормализация в процессе обучения................................................... 424
9.5. Применение модели глубокого обучения...................................................... 425
9.5.1. Тестирование обученной модели на новых данных....................... 425
9.5.2. Тестирование обученной модели на новых точках данных......... 427
Итоги.................................................................................................................................. 429
Глава 10. Развертывание модели.................................................................................... 431
10.1. Простое развертывание в веб-приложении................................................ 432
10.1.1. Общая схема веб-развертывания....................................................... 432
10.1.2. Модуль сервера Flask............................................................................. 434
10.1.3. Страница home.html................................................................................ 437
14 Оглавление
10.1.4. Страница show-prediction.html........................................................... 442
10.1.5. Веб-развертывание.................................................................................. 443
10.2. Публичные облачные сервисы и операции машинного обучения........ 444
10.3. Начало работы с Google Cloud........................................................................ 445
10.3.1. Первый вход в Google Cloud................................................................ 445
10.3.2. Создание проекта Google Cloud.......................................................... 447
10.3.3. Создание бакета Google Cloud Storage............................................. 448
10.4. Развертывание модели в Vertex AI................................................................ 450
10.4.1. Загрузка модели в бакет Cloud Storage............................................ 450
10.4.2. Импорт модели в Vertex AI.................................................................. 452
10.4.3. Развертывание модели на эндпоинте............................................... 456
10.4.4. Начальное тестирование развертывания модели......................... 458
10.5. Использование развертывания Vertex AI с Flask..................................... 461
10.5.1. Настройка Vertex AI SDK..................................................................... 462
10.5.2. Обновление модуля сервера Flask для вызова эндпоинта........ 462
10.5.3. Преимущества развертывания модели на эндпоинте................. 465
10.6. Gemini для Google Cloud: помощь генеративного ИИ
в Google Cloud................................................................................................................. 467
10.6.1. Настройка Gemini для Google Cloud................................................ 468
10.6.2. Использование Gemini для ответов на вопросы
о Google Cloud........................................................................................................ 468
Итоги.................................................................................................................................. 471
Глава 11. Пайплайн машинного обучения.................................................................. 473
11.1. Знакомство с пайплайнами машинного обучения................................... 474
11.1.1. Три вида пайплайнов.............................................................................. 474
11.1.2. Обзор ML-пайплайнов Vertex AI ...................................................... 476
11.2. Этапы подготовки ML-пайплайна . .............................................................. 477
11.2.1. Создание сервисного аккаунта для ML-пайплайна ................... 477
11.2.2. Создание ключа сервисного аккаунта.............................................. 480
Оглавление 15
11.2.3. Предоставление сервисному аккаунту права доступа
к базовому сервисному аккаунту Compute Engine.................................... 482
11.2.4. Знакомство с Cloud Shell...................................................................... 484
11.2.5. Загрузка ключа сервисного аккаунта............................................... 486
11.2.6. Загрузка очищенного датасета в бакет Google
Cloud Storage.......................................................................................................... 488
11.2.7. Создание управляемого набора данных Vertex AI....................... 490
11.3. Определение ML-пайплайна........................................................................... 494
11.3.1. Что выбрать: локальную реализацию или ML-пайплайн......... 495
11.3.2. Знакомство с контейнерами................................................................. 497
11.3.3. Преимущества контейнеризации в ML-пайплайне..................... 498
11.3.4. Адаптация кода для запуска в контейнере...................................... 498
11.3.5. Изменение кода обучения для работы в контейнере.................. 500
11.3.6. Скрипт пайплайна................................................................................... 502
11.3.7. Тестирование модели, обученной в пайплайне............................. 505
11.4. Использование генеративного ИИ для создания
ML-пайплайна................................................................................................................ 507
11.4.1. Использование Gemini для ответов на вопросы
об ML‑пайплайне ................................................................................................. 508
11.4.2. Использование Gemini для генерации кода
ML-пайплайна . ..................................................................................................... 510
11.4.3. Использование Gemini для объяснения кода
ML‑пайплайна........................................................................................................ 513
11.4.4. Использование Gemini для анализа записей логов..................... 514
11.4.5. Дообучение базовой модели в Vertex AI.......................................... 519
Итоги.................................................................................................................................. 522
Глава 12. Комбинация градиентного бустинга и глубокого обучения.............. 524
12.1. Обзор решения с градиентным бустингом из главы 7............................ 525
12.2. Выбор решения на основе глубокого обучения........................................ 531
12.3. Решение задачи Airbnb Токио с помощью глубокого обучения.......... 532
16 Оглавление
12.4. Сравнение решений на XGBoost и fastai для задачи
Airbnb Токио.................................................................................................................... 535
12.5. Ансамблирование двух решений задачи Airbnb Токио.......................... 538
12.6. Общее сравнение градиентного бустинга и глубокого обучения....... 541
Итоги.................................................................................................................................. 542
Приложение А. Гиперпараметры для классических моделей
машинного обучения.......................................................................................................... 543
Приложение Б. Методы k ближайших соседей и опорных векторов............... 547
Б.1. Алгоритм k ближайших соседей (k-NN)....................................................... 548
Б.2. Метод опорных векторов (SVM)..................................................................... 552
Б.3. Использование графических процессоров
для машинного обучения............................................................................................ 555
Памяти моей племянницы Анны Джой, жизнь которой послужила примером
того, как пытливый интерес к окружающему миру может сосуществовать
в согласии с благоговением перед его Создателем.
Марк Райан
Памяти Джона Пола Мюллера, моего давнего соратника и друга, вместе
с которым мы написали множество книг о чудесах компьютерных технологий.
Лука Массарон
От издательства
Мы выражаем огромную благодарность сотрудникам Центрального университета за помощь в работе над русскоязычным изданием книги и вклад в повышение
качества переводной литературы.
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com
(издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию
о наших книгах.
О научном редакторе русского издания
Кирилл Быков — старший аналитик-разработчик во «ВКонтакте». Занимается
поддержкой внутренней MapReduce-системы, разработкой платформы ABтестирования и дата-инженерией. Преподаватель курсов по основам машинного
обучения, глубокого обучения, программирования на Python и продуктовой
аналитике в МФТИ, НИУ ВШЭ и Центральном университете. Развивает аналитические инструменты и ведет курсы по машинному обучению, вдохновляя
студентов на освоение современных методов анализа данных и искусственного
интеллекта.
Предисловие
Книга «Машинное обучение на табличных данных: XGBoost, глубокое обучение
и ИИ» посвящена важной области машинного обучения — работе с табличными
данными. Они повсюду. Это и электронные таблицы, которыми вы пользуетесь
каждый день, и базы данных, без которых нельзя представить работу ни одной
организации. Табличные данные — как алмазы, но, чтобы они засияли, им требуется качественная огранка.
В этой книге вы найдете не только теоретические сведения. Она вооружит вас
методами машинного обучения, специально разработанными для табличных
данных, чтобы раскрыть весь их потенциал. Вы узнаете, как осмысливать свои
данные, выявлять в них закономерности, создавать реальные приложения —
и при этом получать понятные и интерпретируемые результаты.
С помощью книги вы научитесь писать код и затем применять его на практике.
В отличие от большинства публикаций, посвященных преимущественно вопросам обучения моделей, с этой книгой вы сможете проследить целиком весь цикл
работы с табличными данными. Множество примеров кода проиллюстрируют
важные этапы этого процесса: обработку сырых данных, создание моделей,
оценку их производительности и бесшовное развертывание в профессиональной общедоступной облачной среде с использованием лучших практик MLOps.
Все важные мысли и практический опыт работы с табличными данными сосредоточены здесь, на страницах одной книги. Это превращает ее в идеального
спутника для тех, кто хочет глубоко разобраться в вопросе и научиться получать
реальную пользу от анализа своих табличных данных.
Часть 1 знакомит с машинным обучением на табличных данных, включая
электронные таблицы, CSV-файлы и базы данных. Эта часть знакомит сначала
с основами машинного и глубокого обучения, а затем подробно объясняет оба
подхода.
20 Предисловие
Часть 2 исследует машинное обучение с градиентным бустингом — мощной
техникой, которая хорошо зарекомендовала себя в области анализа табличных
данных. Вы познакомитесь с различными моделями, лучшими практиками
и даже протестируете пример на реальных данных.
Часть 3 посвящена глубокому обучению в задаче анализа табличных данных
и охватывает все этапы — от начальных шагов до развертывания модели, включая построение готового к использованию в продакшене пайплайна машинного
обучения. В заключение приводится сравнение возможностей градиентного
бустинга и глубокого обучения, чтобы помочь вам в будущем обоснованно
выбирать правильный подход, отвечающий вашим целям. Эта часть мне понравилась больше всего.
Надеюсь, эта замечательная книга не оставит вас равнодушными, равно как
и изложенные в ней передовые методы и практические советы по максимально
эффективному использованию табличных данных.
Антонио Гулли (Antonio Gulli),
старший директор Google Cloud, Search and AI в офисе СTO
Введение
Тема этой книги находится на пересечении двух наших любимых областей:
машинного обучения (ML) и табличных данных (tabular data). Мы хорошо понимаем, как важны таблицы: информация в виде строк и столбцов, скрытая во
всевозможных хранилищах, имеющих разный размер и масштаб — от скромной
электронной таблицы до сложнейших реляционных баз данных.
Несмотря на волну популярности нетабличных (nontabular) данных в последние
два десятилетия, почти каждая коммерческая или государственная организация
на планете по-прежнему зависит от данных, помещенных в таблицы. Когда издательство Manning обратилось к нам с предложением написать книгу, описывающую новейшие способы анализа табличных данных с помощью машинного
обучения, включая не только классические методы, например градиентный
бустинг, но и глубокое обучение, мы не могли упустить такой шанс. А когда
во время работы над книгой стала набирать обороты революция, вызванная
генеративным ИИ, мы воспользовались возможностью осветить этот вопрос
в нашем повествовании.
Мы надеемся, что книга даст читателям практические советы и покажет конкретные подходы, которые позволят углубить понимание этих двух важнейших
тем — машинного обучения и табличных данных.
Благодарности
Эта книга стала результатом многолетнего самоотверженного сотрудничества
десятков талантливых людей.
Марк Райан: я хотел бы выразить признательность Серкану Арику (Sercan
Arik) из Google за то, что он читал мои главы по мере их написания. Я также
хотел бы выразить признательность моему менеджеру Бретту Джонсону (Brett
Johnson) и директору Хелен Слэттери (Helen Slattery) за предоставленные мне
возможности и за поддержку во время моей работы в Google. Я в долгу перед
своей семьей и перед Лоуренсом Муссио (Dr. Laurence Mussio) за его советы
и веру в меня. Наконец, я хотел бы поблагодарить Луку за его мастерство и все
знания, которыми он поделился со мной, а также за его терпение в процессе
создания этой книги. Я многому научился у Луки и считаю честью разделить
авторство книги с ним.
Лука Массарон: я хотел бы сердечно поблагодарить мою семью, Юкико и Амелию, за их любовь, поддержку и терпение на протяжении долгого времени работы
над книгой. Хочу выразить особую благодарность Антонио Гулли за то, что он
прочитал нашу книгу и написал к ней замечательное предисловие, а также за
все его бесценные советы. Наконец, большое спасибо моему соавтору Марку за
то, что он привлек меня к этому проекту, который я так полюбил и благодаря
которому я многому научился.
И последнее, но не менее важное: спасибо всем рецензентам, которые нашли
время поделиться своими ценными отзывами. Вот их имена: Ади Шавит (Adi
Shavit), Акса Фулара (Aqsa Fulara), Двипам Катария (Dwipam Katariya),
Джованни Альцетта (Giovanni Alzetta), Гири Сваминатан (Giri Swaminathan),
Грегори Граймс (Gregory Grimes), Харчаран Каббай (Harcharan Kabbay), Хосе
Антонио Мартинес (Jose Antonio Martinez), Кейт Ким (Keith Kim), Кхайнг
Вин (Khaing Win), Мартин Цыган (Martin Czygan), Максим Волгин (Maxim
Volgin), Ник Васк ес (Nick Vazquez), Нинослав Черкес (Ninoslav Cerkez),
Благодарности 23
Оливер Кортен (Oliver Korten), Ричард Майнсен (Richard Meinsen), Саид
Эх-Чади (Said Ech-Chadi), Серхио Говони (Sergio Govoni), Саймон Чеке
(Simon Tschöke), Тиаго Бритто Борхес (Thiago Britto Borges), Томас Сибер
(Thomas Seeber), Тиртханкар Чакраварти (Tirthankar Chakravarty), Томаш
Шипка (Tomáš Šipka) и Вайджанатх Рао (Vaijanath Rao). Вы помогли сделать
эту книгу лучше.
Об этой книге
В этой книге описывается, как можно использовать подходы машинного обу
чения (в том числе классический метод градиентного бустинга), глубокое
обучение и генеративные модели ИИ для извлечения важной информации из
табличных данных (структурированных, организованных в строки и столбцы)
и применения их на практике. Вы узнаете об определяющих характеристиках
табличных данных, лучших практиках применения ML для их анализа и о том,
как другие смогут применять в своих задачах вашу модель. Вы также узнаете
о плюсах и минусах классического машинного и глубокого обучения в задачах
анализа таблиц. На протяжении всей книги вы научитесь ускорять процесс
с помощью моделей генеративного ИИ.
Для кого эта книга
Эта книга предназначена для широкой аудитории, которая охватывает специа
листов по данным, ML-инженеров, а также представителей бизнеса. Если у вас
имеются базовые знания о классических методах машинного обучения и понятие
о глубоком обучении, эта книга будет вам особенно полезна. В примерах кода,
которые представлены в книге, используется Python, и большинство примеров
предназначены для запуска в облачных средах Jupyter Notebook, таких как
Google Colab. Очень хорошо, если вам уже знакомы стандартные методы Python
для работы с табличными данными, в частности библиотеки pandas и NumPy.
Помимо этого главы 10 и 11 включают код, который запускается в среде Google
Cloud. При этом вам не обязательно уметь работать с этой платформой — мы
расскажем все, что нужно знать; однако, если вам уже приходилось иметь дело
с одной из основных облачных сред (AWS, Azure или Google Cloud), этот опыт
вам пригодится.
Структура книги 25
Структура книги
Книга «Машинное обучение на табличных данных: XGBoost, глубокое обучение
и ИИ» состоит из 12 глав, разбитых на три части.
В части 1 представлены основные концепции табличных данных и описаны
подходы к работе с ними.
Глава 1 дает определение табличных данных и знакомит с двумя подходами
к составлению прогнозов на их основе.
В главе 2 описываются определяющие характеристики табличных данных
и основные понятия для исследования их датасетов.
В главе 3 сопоставляются сильные и слабые стороны классического машинного и глубокого обучения при анализе табличных данных.
В части 2 описываются имеющиеся классические подходы к машинному обучению и объясняется, как использовать один из методов градиентного бустинга,
XGBoost, для получения наилучшего результата.
В главе 4 показано, как использовать классические ML-алгоритмы для составления прогнозов на основе табличных датасетов.
В главе 5 описывается подход к машинному обучению с использованием
табличных данных, в частности алгоритмы на основе деревьев и градиентный бустинг.
В главе 6 описывается, как извлечь максимальную пользу из классических
подходов к машинному обучению, в частности градиентного бустинга, путем
оптимизации подготовки и отбора признаков, а также посредством оптимизации гиперпараметров, управляющих процессом обучения модели.
В главе 7 показан целиком весь процесс применения одного из подходов
градиентного бустинга, XGBoost, к сложному табличному датасету.
В части 3 рассматривается, как создать модель глубокого обучения на табличных
данных, как развернуть ее и как глубокое обучение соотносится с классическим
подходом.
В главе 8 описываются различные доступные технологии, позволяющие
применять глубокое обучение к табличным данным, а также анализируются
сильные и слабые стороны разных инструментов.
В главе 9 на примере кода иллюстрируются основные советы и рекомендации
по максимально эффективному использованию моделей глубокого обучения
на табличных датасетах.
26 Об этой книге
В главе 10 показано, как интегрировать модель, обученную в главе 9, в простое веб-приложение, запущенное на локальной машине, и как развернуть
ту же модель в популярной управляемой облачной среде — Google Cloud.
В главе 11 показано, как автоматизировать этапы обучения и развертывания модели с помощью возможностей пайплайна Vertex AI в Google Cloud,
а также как можно воспользоваться преимуществами генеративного ИИ для
упрощения задачи создания ML-пайплайнов.
Глава 12 объединяет все содержание книги: в ней сравниваются градиентный
бустинг из главы 7 и глубокое обучение и показывается, как достичь наилучших результатов, комбинируя эти два подхода.
Наконец, в двух приложениях к книге подробно рассматриваются вопросы,
которые не получилось полностью раскрыть в основном тексте. В приложении A содержится обзор ключевых гиперпараметров всех классических
ML-алгоритмов, обсуждаемых в книге, и приводятся типичные значения
для оптимизации. Приложение Б посвящено классическим методам k-NN
(k ближайших соседей) и SVM (метод опорных векторов), которые в последнее время снова стали популярны благодаря новым инструментам, таким
как библиотека NVIDIA RAPIDS, использующая графические процессоры
для ускорения вычислений.
Если вы интересуетесь обоими подходами к анализу табличных данных (классическое машинное и глубокое обучение), мы рекомендуем читать главы последовательно. Если вас в первую очередь интересуют классические модели,
мы предлагаем вам прочитать часть 1, часть 2 и главу 12. Если вам больше
интересны подходы на основе глубокого обучения, прочтите часть 1, а затем
переходите к части 3.
О коде в книге
Эта книга содержит много примеров исходного кода как в пронумерованных
листингах, так и в тексте. В обоих случаях исходный код отформатирован моноширинным шрифтом. Иногда код также выделен жирным шрифтом; так выделяются
фрагменты, которые изменились по сравнению с предыдущими шагами, например когда к существующей строке кода добавляется новая функция.
Во многих случаях исходный код был переформатирован; мы добавили разрывы
строк и переработали отступы, чтобы код умещался на странице. Кроме того,
комментарии в исходном коде часто удалялись из листингов, когда код описывался в тексте. Все важные моменты по коду приводятся в пояснениях к нему.
Примеры кода в этой книге иллюстрируют ключевые моменты и призваны
послужить отправной точкой ваших собственных исследований. Вы можете
О коде в книге 27
получить исполняемые фрагменты кода из liveBook — онлайн-версии этой
книги по адресу https://livebook.manning.com/book/machine-learning-for-tabular-data.
Полный код для примеров в книге доступен для загрузки с веб-сайта Manning
по адресу www.manning.com и с GitHub по адресу https://github.com/lmassaron/
Machine-Learning-on-Tabular-Data.
Примеры кода во всех главах, за исключением глав 10 и 11, предназначены для
запуска в облачной среде Jupyter Notebook, такой как Kaggle или Google Colab.
Глава 10 включает пример, который необходимо запустить на локальной системе,
а также примеры для запуска в Google Cloud. Примеры в главе 11 предназначены
для запуска в Google Cloud.
Форум liveBook
Приобретая книгу «Машинное обучение на табличных данных: XGBoost, глубокое обучение и ИИ», вы также получаете бесплатный доступ к платформе
для онлайн-чтения liveBook издательства Manning (на английском языке).
Эксклюзивные возможности liveBook позволяют оставлять комментарии как
к книге в целом, так и к отдельным ее разделам или абзацам. Можно легко делать заметки для себя, задавать технические вопросы и отвечать на них, а также
получать помощь от авторов и других пользователей. Чтобы получить доступ
к форуму, посетите страницу https://livebook.manning.com/book/machine-learning-fortabular-data/discussion. Информацию о форумах Manning и правилах поведения
на них см. на https://livebook.manning.com/discussion.
В рамках своих обязательств перед читателями издательство Manning предоставляет ресурс для содержательного общения читателей и авторов. Эти обязательства не подразумевают конкретную степень участия автора, которое остается добровольным (и неоплачиваемым). Задавайте авторам хорошие вопросы,
чтобы им было интересно участвовать в диалоге! Форум и архивы обсуждений
доступны на сайте Manning, пока книга продолжает издаваться.
Об авторах
Марк Райан — руководитель отдела ИИ в Developer
Knowledge Platform в Google. Имеет степень бакалавра математики в области computer science Университета Ватерлоо и степень магистра computer
science Университета Торонто. Является автором
двух книг: «Deep Learning with Structured Data»
(Manning, 2020) и «Deep Learning with fastai
Cookbook» (Manning, 2021). Марку нравится исследовать малоизученные приложения машинного
обучения, включая использование генеративного ИИ
для модернизации систем COBOL, взаимосвязь реляционных баз данных и машинного обучения, а также тематику этой книги: машинное обучение для анализа
табличных данных. Помимо машинного обучения, Марк интересуется историей
технологий и любит читать о том, как различные достижения — от железных
дорог до мобильных телефонов — изменили наш мир.
Лука Массарон — специалист в области data science
с более чем десятилетним опытом преобразования
данных в интеллектуальные продукты и решения
реальных задач, представляющих ценность для бизнеса. Он является автором бестселлеров по ИИ,
машинному обучению и алгоритмам. Лука также
трехкратный гроссмейстер (Grandmaster) Kaggle,
достигший 7-го места в мировом рейтинге соревнований, и эксперт-разработчик (Developer Expert)
Google в области машинного обучения.
Иллюстрация на обложке
Рисунок «La Marchande de Gateaux» («Продавщица пирожных») на обложке
книги «Машинное обучение на табличных данных: XGBoost, глубокое обучение
и ИИ» взят из книги Луи Кермера (Louis Curmer), опубликованной в 1841 году.
Каждая иллюстрация в ней была тщательно прорисована и раскрашена вручную.
В прежние времена по одежде человека можно было легко определить, где он
живет и какова его профессия или положение в обществе. Издательство Manning
приветствует изобретательность и инициативность — качества, присущие
индустрии IT, — и в знак этого размещает на обложках изображения, которые
демонстрируют богатое разнообразие региональных культур, запечатленное на
старинных рисунках.
Часть 1
Понятие о машинном
обучении для табличных
данных
Вступительный раздел книги призван создать прочную основу для понимания
работы с табличными данными и посвящен их специфическим характеристикам,
различным подходам к их моделированию (сравниваются машинное и глубокое
обучение), а также лучшим практикам для разведочного анализа и подготовки
данных. После прочтения этих глав у вас появится четкое представление об особенностях работы с табличными данными. Вы также подготовитесь к освоению
более сложных методов, о которых пойдет речь в следующих разделах.
Глава 1 знакомит с понятием табличных данных и объясняет, что это такое,
почему они важны и чем отличаются от других типов данных, таких как изображения, аудио и текст. Мы познакомимся с концепциями машинного и глубокого обучения и попытаемся разрешить споры об использовании методологий
глубокого обучения на табличных данных. Глава завершается обзором характеристик табличных данных, которые требуют особого подхода к их анализу
и моделированию.
Глава 2 исследует структуру и характеристики табличных данных, выделяя потенциальные проблемы и средства их решения. Мы покажем, как искать такие
данные в онлайн- и офлайн-источниках, особенно внутри бизнес-организаций.
В этой главе также дается исчерпывающее описание, как извлечь максимальную
пользу из разведочного анализа данных.
В главе 3 сравниваются и противопоставляются подходы на основе машинного
и глубокого обучения с помощью трех ключевых критериев: простота, прозрачность и эффективность, а также сильные и слабые стороны каждого из них. Мы рассмотрим датасеты Airbnb, которые будем использовать на протяжении всей книги.
После завершения части 1 вы будете готовы к изучению как классических, так
и более сложных методов машинного обучения для анализа табличных данных.
1
Табличные данные
В этой главе
3 Что такое табличные данные
3 Почему они так важны
3 Различие между глубоким обучением и другими методами анализа
табличных данных
3 Что думают специалисты об использовании глубокого обучения
для табличных данных
3 Что отличает табличные данные от других типов данных, таких как
изображения, звук или текст
Табличные данные — основа современной жизни, а для многих из нас — часть
нашей повседневной работы. Они присутствуют в CSV-файлах и реляционных
базах данных, служат основой для аналитических отчетов и используются для
обучения ML-моделей. А ML-модели, обученные на бизнес-данных, в свою
очередь, могут успешно решать множество полезных задач, например прогнозирование потребностей в запасах для розничных точек или предсказание
рыночных цен на товары.
1.1. Что такое табличные данные? 33
В этой главе мы расскажем, как выбрать подходящий метод моделирования для
задач с табличными данными. Мы представим два главных подхода: глубокое
и машинное обучение. Затем рассмотрим уникальные особенности, характерные
именно для применения табличных данных в ML-моделях.
1.1. Что такое табличные данные?
Для целей этой книги табличные данные — это просто данные, организованные в строки и столбцы. Коллекция табличных данных может быть названа
табличным набором данных, табличным датасетом или просто таблицей.
Все элементы в строке связаны с одной точкой данных, также называемой
наблюдением. Каждая строка является автономной, не зависит от других
строк и полностью описывает определенное состояние или условие. Столбцы
представляют собой атрибуты для этой точки данных, и их часто называют
переменными (это термин из статистики) или признаками (что более типично
для машинного обучения). Все записи в столбце имеют один и тот же тип
данных, например целое число (integer), строка (string) или число с плавающей
точкой (float).
Рассмотрим для примера таблицу, в которой содержится информация о валютах,
используемых в ряде стран (рис. 1.1).
Каждый столбец таблицы содержит информацию об одном и том же
признаке валюты для всех стран (атрибуты наблюдений)
Каждая
строка таблицы
содержит
информацию
о валюте
определенной
страны (наши
наблюдения)
Страна
Название
валюты
Австралия
Австралийский
доллар
Канада
Канадский
доллар
Короткое
наименование,
неформальное
название
Символ
Код
по ISO 4217
Курс по
отношению
к доллару США
$
AUD
1.45
$
CAD
1.29
Новая Зеландия Новозеландский Золотой
доллар
$
NZD
1.61
ЮАР
Баксы
R
ZAR
16.78
Великобритания Фунт
стерлингов
Квид
£
GBP
0.83
США
Баксы, «зеленые»
$
USD
1.0
Рэнд
Доллар США
Рис. 1.1. Пример табличных данных
Баксы, луни ,
пиастры
34 Глава 1. Табличные данные
Столбцы в этой таблице содержат значения разных типов.
Страна, название и символ валюты, код ISO 4217 являются категориальными
столбцами, поскольку допустимые значения для них берутся из конечного,
относительно небольшого набора значений.
Неформальные названия валют представляют собой столбцы с текстовыми
данными в свободной форме, поскольку они могут содержать ряд значений
или не содержать никаких в зависимости от страны.
Курс по отношению к доллару США — это столбец с непрерывными данными,
поскольку он содержит действительные числовые значения.
Более подробно характеристики табличных данных мы рассмотрим в главе 2.
Табличные данные могут храниться в различных физических форматах.
Отдельные файлы, включая файлы CSV и электронные таблицы, такие как
Excel и Google Таблицы.
Таблицы в реляционных базах данных:
базы данных с открытым исходным кодом, например Postgres (https://
www.postgresql.org/) и MySQL (https://www.mysql.com/);
локальные проприетарные (on-premise vendor) базы данных, например
SQL Server (https://mng.bz/MD2W) и Oracle (https://www.oracle.com/ca-en/
database/);
облачные нативные (cloud-native) базы данных, например Google Cloud
Spanner (https://cloud.google.com/spanner), AWS Aurora (https://aws.amazon.
com/rds/aurora/) и Snowflake (https://www.snowflake.com/).
ПРИМЕЧАНИЕ Вы могли слышать термин структурированные данные, используе
мый наряду с табличными данными. Однако эти два понятия — не абсолютные синонимы. Например, иногда речь может идти о данных, которые имеют определенную степень структурированности, но при этом не являются таблицами, допустим вложенные
записи в формате JSON. Структурированные данные могут включать реляционные,
графические, пространственные данные и временнˆые ряды, которые также могут
быть представлены в табличной форме. Чтобы избежать путаницы, в этой книге мы
будем использовать исключительно термин табличные данные.
Теперь, когда мы определились, что представляют собой табличные данные,
попробуем разобраться, что включают в себя нетабличные. Это важная тема, поскольку различия между ними помогают прояснить один из ключевых вопросов,
рассматриваемых в этой книге: существуют ли ситуации, в которых при анализе
таблиц следует использовать глубокое обучение? Ниже приведены некоторые
примеры данных, которые не являются табличными:
1.2. Весь мир работает на табличных данных 35
изображения;
видео;
аудио;
текст;
данные датчиков в формате JSON, например генерируемые устройствами
интернета вещей (IoT);
стриминговые данные в социальных сетях.
Можете ли вы назвать одно общее свойство, которое объединяет их? Если вы
ответите: «Все они очень успешно использовались для обучения моделей глубокого обучения», то будете абсолютно правы. Действительно, за последние 10 лет
одна за другой были созданы принципиально новые модели с использованием
различных нетабличных датасетов. В этой книге мы выясним, почему глубокое
обучение не произвело такой же революции для табличных данных и когда имеет
смысл применять его к ним.
1.2. Весь мир работает на табличных данных
Согласно статье «Structured vs Unstructured Data» (https://mng.bz/5g7Z), до 90 %
всех цифровых данных в мире являются нетабличными, и их доля увеличивается с каждым годом. Если это так, то зачем читать книгу о применении методов
машинного обучения к таблицам? Хотя только небольшая часть данных в мире
является табличной, но без них невозможно представить себе современную
жизнь. Каждый банк, страховая компания, государственное учреждение, ретейлер и производитель — все они ведут свою основную деятельность с помощью
таблиц. Это продиктовано, во-первых, тем, что такая структура данных, организованных в строки и столбцы, делает их удобными для ввода, извлечения,
управления и анализа. Во-вторых, такой формат поддерживается многими
бизнес-программами и приложениями, такими как электронные таблицы, базы
данных и инструменты бизнес-аналитики.
Помимо своей основной деятельности, организации зависят от табличных данных в части мониторинга своего прогресса и обнаружения проблем. Поскольку
в современном мире каждый из нас является потребителем, сотрудником и гражданином, наши ежедневные действия обновляют сотни и даже тысячи таблиц.
В течение трех лет один из авторов этой книги имел честь руководить техподдержкой одной из крупнейших реляционных баз данных в мире. Работа 24/7
продемонстрировала масштабы организаций, которые работают с табличными данными, а также последствия сбоев в таких системах: покупатели на
целом континенте не могли использовать свои кредитные карты, фуры стояли
36 Глава 1. Табличные данные
в многокилометровых пробках на границе, грузовые поезда останавливались,
сайты интернет-магазинов обваливались в «черную пятницу», а производство
кардиостимуляторов прекращалось. Это не преувеличение: весь мир работает
на табличных и в целом структурированных данных.
Табличные данные повсюду, и они критически важны. Работа многих из нас зависит от них, так что умение эффективно применять к ним машинное (и, если
это уместно, глубокое) обучение является очень полезным навыком. В этой
книге вы узнаете о методах, позволяющих раскрыть весь огромный потенциал
таких данных.
1.3. Сравнение машинного и глубокого обучения
И глубокое, и классическое машинное обучение решают задачу сопоставления
входных данных и предсказания (прогноза). Однако они используют разные
подходы: методы глубокого обучения разработаны для имитации поведения
биологического мозга, тогда как машинное обучение обычно основано на статистических оптимизациях или установлении сходства. Однако, кроме того, эти
два подхода различаются способом работы с данными.
В классическом машинном обучении преобразование и генерация признаков
(feature engineering) играют важнейшую роль, поскольку, независимо от выбранной модели, необходимо выполнить соответствующие преобразования
входных данных на основе их характеристик и предметной области, к которой
они относятся. (Это бизнес-данные? Отражают ли они какие-либо социальные,
экономические или физические явления?) Вот некоторые причины, почему
генерация признаков так важна в классическом машинном обучении.
Извлечение релевантной информации. Далеко не всегда первичные данные
хорошо отражают специфику решаемой задачи. Генерация признаков помогает определить и извлечь наиболее информативные параметры данных,
одновременно исключая незначащие или зашумленные. Сориентировав
модель на наиболее значимые признаки, можно обучить ее находить наиболее важные закономерности, что улучшает ее обобщающую способность
и производительность.
Представление данных. Различные модели имеют разные требования к представлению данных. В процессе генерации признаков необходимо преобразовать данные в нужный формат, учитывающий требования и ограничения
конкретной модели. Этот шаг гарантирует, что модель сможет эффективно
обучаться на данных и делать точные прогнозы.
Решение проблемы нелинейности. Во многих реальных задачах взаимо
связи между признаками и целевой переменной могут быть нелинейными.
1.3. Сравнение машинного и глубокого обучения 37
Генерация признаков помогает преобразовать данные для работы с нелинейностями, упрощая линейным моделям аппроксимацию сложных
зависимостей.
Учет предметной области. В некоторых случаях эксперты в предметной
области обладают ценной информацией, которую можно использовать при
генерации признаков. Применение специфических знаний может значительно улучшить производительность модели при решении конкретных задач.
С другой стороны, подходы глубокого обучения полагаются на обучение представлений (representation learning), которое заключается в их способности автоматически обрабатывать данные внутри модели, приводя их в осмысленную
форму для решения поставленной задачи. Возможности обучения представлений позволяют моделям глубокого обучения преобразовывать данные в более
компактный и осмысленный формат, который фиксирует соответствующие
признаки и выявляет закономерности для конкретной задачи. Фактически в процессе обучения, благодаря тому что все входные признаки нелинейно взаимодействуют с другими, модели глубокого обучения сами обнаруживают сложные
закономерности и зависимости в данных, которые могут быть неочевидны при
ручной генерации признаков, и выстраивают иерархические представления
входных данных, начиная с базовых признаков и постепенно переходя к более
сложным и абстрактным.
Таким образом, при анализе табличных данных с помощью классического машинного обучения внимание в первую очередь сосредоточено на тщательной
и эффективной генерации признаков, тогда как в моделях глубокого обучения
отводят гораздо большее значение архитектуре слоев нейронов и характеристикам отдельных нейронов. Это и есть основная мысль нашей книги. В следующих
главах будет не только проиллюстрировано различие между классическим
машинным и глубоким обучением, но и представлены варианты постановки
задачи в контексте обработки данных и поиска решений на основе этого фундаментального различия между двумя подходами.
Мы понимаем, что упрощение неизбежно привносит неточность. Однако на
протяжении всей книги мы будем использовать общий термин «машинное
обучение» или «классическое машинное обучение» для всех подходов, за исключением нейронных сетей, а термин «глубокое обучение» будет применяться для
подходов, основанных на нейронных сетях.
Мы рассмотрим как базовые, так и более продвинутые модели машинного обу
чения, имеющиеся в популярных библиотеках.
Базовые модели машинного обучения, доступные в библиотеке scikit-learn
(https://scikit-learn.org/) и специализированных библиотеках для графических
38 Глава 1. Табличные данные
процессоров, таких как NVIDIA Rapids (https://developer.nvidia.com/rapids), для
следующих алгоритмов:
линейная регрессия;
логистическая регрессия;
обобщенные линейные модели.
Алгоритмы на основе деревьев из scikit-learn:
бэггинг на основе слабых предикторов;
случайный лес;
особо случайные деревья (extremely randomized trees).
Градиентный бустинг на основе гистограмм:
XGBoost eXtreme Gradient Boosting (https://github.com/dmlc/xgboost);
LightGBM от Microsoft (https://github.com/Microsoft/LightGBM);
HistGradientBoosting из scikit-learn.
В качестве моделей глубокого обучения мы рассмотрим ряд архитектур, реализованных во фреймворках глубокого обучения TensorFlow и PyTorch, которые
показали свою эффективность при работе с табличными данными:
неглубокие сети с категориальными эмбеддингами (непосредственно реализованные с использованием одного из доступных фреймворков глубокого
обучения);
fastai tabular (https://docs.fast.ai/tabular.model.html);
PyTorch Tabular (https://github.com/manujosephv/pytorch_tabular);
TabNet (https://arxiv.org/abs/1908.07442);
SAINT (https://arxiv.org/abs/2106.01342);
DeepTables (https://github.com/DataCanvasIO/deeptables).
Вы сами сможете наблюдать различие между машинным и глубоким обучением на протяжении всей книги, поскольку мы рассмотрим оба подхода и дадим
рекомендации по использованию каждого из них для решения задач анализа
табличных данных.
1.4. В чем особенность табличных данных?
Мы знаем, что подходы глубокого обучения преобладают в решении проблем,
связанных со многими типами данных, которые мы могли бы назвать нетаб
личными или неструктурированными вследствие большого разнообразия их
1.4. В чем особенность табличных данных? 39
характеристик, размеров и модальностей и которые нельзя заключить в узкие
рамки формата «строки/столбцы». Типичные примеры неструктурированных
данных, с которыми успешно справляются модели глубокого обучения, это:
аудио;
изображения;
видео;
текст.
Здесь, в отличие от задач анализа структурированных табличных данных, нет
ничего похожего на типичный табличный формат, а есть разные файлы или
экземпляры, содержащие множество информации в неупорядоченном виде. До
того как глубокое обучение произвело революцию в способе моделирования
неструктурированных данных, для их применения в предиктивной модели их
нужно было преобразовывать в структурированный формат путем тщательного
создания четко и однозначно определенных признаков. Эта процедура называется
генерацией признаков (feature engineering). Для каждого типа задач с неструктурированными данными исследователям и практикам требовались годы, чтобы
найти оптимальные признаки для извлечения данных, чтобы затем передать их
в модель машинного обучения и получить предсказание приемлемого качества.
Благодаря умению работать с представлениями, модели глубокого обучения
могут сами производить все необходимые преобразования, чтобы превратить
неструктурированные данные в адекватный прогноз в сквозном (end-to-end)
режиме, то есть напрямую от входа к решению. Учитывая это, можно было бы
ожидать, что модели глубокого обучения будут еще эффективнее на табличных
данных, но пока этого не произошло.
По правде говоря, существуют различные объяснения трудностей, с которыми
сталкивается глубокое обучение при работе с табличными данными. Первое
связано с фактическими направлениями академических исследований и частными инвестициями в новые технологии и методологии. Как мы уже упоминали,
в прошлом исследователи посвящали все свое время и тратили усилия на поиск
наилучшего способа преобразования неструктурированных данных в структурированные, чтобы соответствовать парадигме машинного обучения своего
времени. В настоящее время те же усилия прилагаются к продвижению глубокого
обучения, по большей части на неструктурированных данных, поскольку они
более доступны в публичных репозиториях и более однородны (uniform), чем
таблицы, что позволяет надеяться на больший успех проводимых исследований.
Хранилища изображений, такие как ImageNet (https://image-net.org/index.php),
и открытые текстовые корпуса, такие как Wikipedia или веб-архив Common Crawl
(https://commoncrawl.org/), легкодоступны как академическим исследователям,
так и практикам для обучения или доработки моделей глубокого обучения. Что
касается таблиц, то для них нет аналогичного общего репозитория с открытым
40 Глава 1. Табличные данные
исходным кодом. Напротив, они рассредоточены по множеству частных баз
данных, каждая из которых демонстрирует еще более высокую степень изменчивости, чем неструктурированные данные, поскольку имеет собственные
правила сбора данных и структуру признаков.
Мало того что табличные датасеты с открытым исходным кодом, представляю
щие реальные бизнес-задачи, как правило, сложнее найти; они также обычно
меньше по размеру и часто сильно отличаются от данных, которые находятся
в собственности предприятий и государственных учреждений. Как следствие,
отсутствие достаточно большого объема открытых данных приводит к тому, что
нейронные сети становятся неэффективны. Кроме того, не существует общепринятых метрик для оценки достигнутого успеха, поскольку использование
определенного типа данных ограничено одной конкретной задачей из обширной
области проблем табличных данных. Для любого исследователя гораздо сложнее
применить лучшие практики к табличному датасету или некоторому ограниченному их подмножеству, чем сделать то же самое, используя изображения,
аудио или тексты, которые общедоступны и для которых есть общепризнанные
метрики качества и производительности.
Поскольку табличные датасеты труднодоступны и чрезвычайно разнообразны
по типу содержащейся в них информации, они представляют собой еще одно
ограничение для решений на основе глубокого обучения: нельзя придумать
никакие универсальные, предварительно обученные модели, поскольку нельзя
получить доступ ко всем видам табличных задач. Как только вы разработаете
модель глубокого обучения для задач с изображениями и текстом, вы можете
сделать ее общедоступной и подождать, когда другие ученые или инженеры
смогут применить ее для своих задач после небольшой дополнительной настройки или дообучения. Технический термин для этого процесса звучит как
трансферное обучение (transfer learning): нейросеть, обученная на одной задаче, «переводится» на другую похожую задачу с минимальными усилиями.
Распространение методов глубокого обучения во многом произошло именно
благодаря возможности использовать предобученные модели для дообучения
на похожих задачах.
В заключение следует отметить, что отсутствие обобщаемых табличных примеров, большое разнообразие существующих таблиц и повышенное внимание
ученых к неструктурированным данным привели к тому, что генерация признаков в машинном и глубоком обучении стала играть различную роль.
В машинном обучении удачная генерация признаков может обеспечить
гораздо более мощную предсказательную способность табличным данным,
чем сами алгоритмы, и обычно рассматривается скорее как искусство, нежели как наука.
1.5. Генеративный ИИ и табличные данные 41
В глубоком обучении, напротив, ученые и практики склонны слишком полагаться на обучение представлений и позволять сети справляться со всем
самостоятельно, вместо того чтобы самим использовать генерацию признаков
и продемонстрировать, как глубокое обучение — при тех же входных данных, что и у алгоритмов машинного обучения, — может находить решение
альтернативным и эффективным способом.
Действительно, как показали недавние исследования, типичные свойства табличных данных, такие как избыточные признаки, смещенные распределения и нерегулярные закономерности целевой переменной, представляют собой проблему
для нейронных сетей. Мы обсудим это более подробно в главе 5, когда будем
рассматривать модели градиентного бустинга. Тем не менее мы утверждаем, что
как машинное, так и глубокое обучение являются жизнеспособными методами
решения задач табличных данных, и нейросети приобретут большее влияние
в будущем, поскольку в настоящее время учеными прилагается много усилий для
тестирования архитектур и решений на более реалистичных табличных данных.
1.5. Генеративный ИИ и табличные данные
Генеративный ИИ, в частности большие языковые модели (large language models,
LLM), стал незаменимым помощником в различных задачах, связанных с созданием и обработкой текста. В целом LLM оказались прорывным решением для
определенного круга задач, среди которых следует назвать следующие.
Генерация — генерация текста, например очередного токена, для получения
законченной фразы или целого текста на основе инструкции, или промпта.
Извлечение — распознавание именованных сущностей (named entities), сегментация предложений, извлечение ключевых слов, тематическое моделирование, определение семантических связей.
Классификация — задачи классификации текстов по языкам, на которых
они написаны, а также классификации намерений, тональности, семантики
и даже такие нетривиальные задачи, как распознавание сарказма, иронии
или отрицания.
Преобразование текста — переводы, исправления ошибок, изменения стиля,
перефразирование, резюмирование.
Понимание — диалоги в форме «вопрос — ответ», рассуждения, дополнение
знаний.
Многие из этих задач могут распространяться на сферу занятий специалистов
data science или инженеров по данным (data engineer). LLM могут поддерживать
пользователя при решении таких задач, как генерация признаков, написание
42 Глава 1. Табличные данные
кода и визуализация (например, с использованием команд из пакета matplotlib),
снабжать его аналитическими рекомендациями, помогая интерпретировать результаты и представляя итоговые выводы для диаграмм и отчетов. Одним из заметных практических применений LLM в задачах обработки табличных данных
является автоматизация операций работы с текстом. При работе с текстовыми
столбцами LLM могут генерировать новые признаки, реферировать тексты,
классифицировать их и определять их тематику. Они также могут писать код
обработки на Python, например создавая функции или формируя корректное
регулярное выражение для обработки текста.
Помимо поддержки пользователя и предоставления помощи, LLM также могут играть более явную и активную роль в аналитике. Последние приложения
для ChatGPT (например, API Advanced Data Analytics) обеспечивают непосредственный анализ данных в формате CSV, а также решают смежные задачи,
связанные с данными, включая реферирование, предварительную обработку,
анализ, визуализацию и создание отчетов. На каждом этапе инструмент может
предоставить код Python для выполнения и получения результатов, самостоятельно запускать его и визуализировать результаты в виде диаграмм и таблиц.
Это соответствует возможностям TableGPT или других инструментов, таких как
MediTab. TableGPT (https://arxiv.org/pdf/2307.08674.pdf) — это новый фреймворк,
который использует LLM для улучшения взаимодействия человека с табличными данными. Он позволяет пользователям с помощью команд, выраженных
на естественном языке, выполнять различные задачи: отвечать на вопросы, работать с данными, создавать визуализации, генерировать отчеты и даже строить
прогнозы. MediTab (https://arxiv.org/pdf/2305.12081.pdf) работает с медицинскими
табличными данными: объединяет семплы, адаптирует информацию из внешних
источников к целевой задаче и расширяет объем обучающих данных. На материа
ле задач прогнозирования на текстовых данных MediTab продемонстрировал
производительность, превосходящую классические алгоритмы машинного
обучения, такие как XGBoost.
В целом LLM не обеспечивают сопоставимой эффективности в задачах прогнозирования на табличных данных, как показано в тесте TABLET (https://arxiv.
org/pdf/2304.13188.pdf). Для оценки производительности LLM относительно моделей машинного обучения с учителем (fully supervised) в статье сравнивались
модели Flan-T5 11b и ChatGPT (с использованием четырех примеров с подсказками) с моделью XGBoost, обученной на всем датасете. Модель XGBoost,
примененная ко всем данным, достигла среднего показателя F1 0.94 в задачах
прогнозирования. Для сравнения, ChatGPT набрала в среднем 0.68 балла,
а Flan-T5 11b — 0.66. Этот анализ показал, что производительность LLM все
еще имеет потенциал для роста, в том числе в задачах с разнородными данными (текст и числа), в то время как эти инструменты по-прежнему превосходно
выполняют инструкции, особенно при обработке текстовых входных данных
1.5. Генеративный ИИ и табличные данные 43
и генерации текстовых выходных. Такой инструмент, как llm-classifier (https://
github.com/lamini-ai/llm-classifier), может использовать информацию, уже заложенную в LLM, но не умеет извлекать дополнительную, типичную для табличных
задач, и это вызывает удивление.
Подводя итог, можно сказать, что генеративный ИИ пока не является безусловно
хорошим решением для работы с табличными данными не только из-за производительности, но и по другим важным причинам, таким как:
затратность — генеративные модели требуют значительных ресурсов графического процессора, что приводит к более высоким эксплуатационным
расходам;
масштабируемость — ресурсоемкость моделей генеративного ИИ, особенно их зависимость от графических процессоров, может препятствовать их
масштабируемости;
задержка и пропускная способность — с ростом модели, как правило, увеличивается время обработки каждого запроса, что негативно влияет на задержку и пропускную способность;
смещение, или предвзятость (bias), — генеративные модели могут наследовать смещение обучающих данных, потенциально закрепляя или усиливая
его;
гибкость — адаптация моделей генеративного ИИ к конкретным задачам
часто требует масштабного дообучения, что ограничивает их гибкость;
детерминированность — сложность архитектуры, присущая генеративным
моделям, может затруднить контроль и прогнозирование результатов, что
влияет на их устойчивость и воспроизводимость;
интерпретируемость — сложность генеративных моделей может терять
объяснимость, затрудняя понимание того, как они работают и как приходят
к результатам.
Учитывая эти ограничения, мы сосредоточимся на основных классических
методах машинного и глубокого обучения, а также на том, как правильно и тщательно подготовить данные для анализа. Однако мы также уделим внимание
инструментам генеративного ИИ, таким как ChatGPT, Google Gemini и Gemini
для Google Cloud, поскольку признаем перспективность этих технологий для
анализа табличных данных. Основываясь на нашем опыте в этой области, мы
не видим LLM в качестве полноценной замены классическим алгоритмам машинного обучения или архитектурам глубокого обучения в силу преимуществ,
которые предлагают традиционные инструменты — и с точки зрения эффективности, и в плане контролируемости. Однако в качестве вспомогательного
средства LLM и другие модели генеративного ИИ могут дополнить обработку
44 Глава 1. Табличные данные
таблиц, их анализ и моделирование, повышая квалификацию специалистов
и производительность проектов.
Итоги
Табличные данные — это данные, организованные в строки и столбцы, как,
например, в CSV-файлах или таблицах реляционных баз.
Структурированные данные иногда используются как альтернативный термин для табличных, но это более широкое понятие, включающее, например,
данные в формате JSON.
Табличные данные составляют небольшую часть всех цифровых данных
в мире, но оказывают огромное влияние на нашу жизнь.
В отличие от иных типов данных (например, изображения, видео, текст,
аудио), таблицы являются наиболее распространенными в бизнесе, поэтому
изучение того, как эффективно применять к ним машинное и глубокое обу
чение, — полезный для многих людей навык.
В этой книге мы называем классическим машинным обучением или просто
машинным обучением все, от линейной регрессии до градиентного бустинга,
исключая нейронные сети, чтобы различать эти две группы методов.
По сравнению с глубоким обучением для других типов данных (например,
изображений, видео, текста, аудио) глубокое обучение для табличных данных
привлекает меньше внимания со стороны исследователей.
Традиционно с табличными данными применяется метод градиентного бус
тинга, такой как XGBoost.
В социальных сетях развернулась оживленная дискуссия о том, есть ли место
глубокому обучению в решении задач, связанных с табличными данными.
Мы не встаем ни на чью сторону в этом споре. Вместо этого мы попытаемся
объективно обосновать, почему можно использовать машинное или глубокое
обучение для конкретной задачи, а также познакомим читателя с лучшими
практиками использования каждого подхода.
Табличные данные имеют некоторые особенности, которые не свойственны
другим типам данных, таким как изображения, видео или текст, а именно:
отсутствие больших датасетов с открытым исходным кодом, аналогичных
тем, которые можно увидеть в реальных бизнес-задачах.
Генеративный ИИ, особенно LLM, существенно влияет на то, как в целом
воспринимается искусственный интеллект, как он распространяется среди
людей и организаций и как используется. LLM могут помочь автоматизировать различные задачи, связанные с анализом и моделированием табличных
данных, особенно когда это касается моделей с текстовым входом и выходом.
2
Табличные датасеты
В этой главе
3 Свойства строк и столбцов в табличных датасетах
3 Возможные проблемы и способы их устранения
3 Поиск табличных данных в интернете и внутренних системах
организаций
3 Исследование данных для решения типичных проблем табличных
данных
Табличные данные могут включать в себя практически всё — от узкоспециализированных научных исследований до анализа поведения потребителей на
веб-сайте и статистики в лиге фэнтези-спорта. При всем многообразии, сходства
в табличных данных преобладают над различиями, и можно выполнить бˆольшую
часть работы по анализу данных, просто применяя стандартные подходы и инструменты, даже не имея большого опыта в данной области.
В этой главе мы рассмотрим, как собирать табличные датасеты и подготавливать их к работе. Мы также проведем небольшое исследование и посмотрим на
данные с разных точек зрения: по строкам, по столбцам, в свете взаимосвязей
между признаками и с учетом их общего распределения по датасету. Для этого
46 Глава 2. Табличные датасеты
примера мы будем использовать синтетический датасет, Auto MPG, доступный
на сайте UCI Machine Learning (https://archive.ics.uci.edu/dataset/9/auto+mpg).
2.1. Свойства строк и столбцов
В зависимости от предметной области разнообразие табличных данных может
быть поразительным. Это происходит потому, что они являются скорее правилом, нежели исключением в мире данных, и так было всегда. Они собирались
в течение тысяч лет: вспомним учет зерна в Древнем Египте или записи о рождениях, браках и смертях в приходских книгах средневековой Европы и так
далее, вплоть до наших дней в современных государственных учреждениях.
В 1960‑х годах начали собирать данные в компьютерных базах, что придало термину табличный более компьютерный смысл. Широкое внедрение реляционных
баз данных с 1970-х годов популяризировало табличные данные, и они стали
использоваться повсеместно. В реляционных базах таблицы можно объединять
с помощью значений определенных столбцов, которые называются ключами.
Такое нововведение позволило компьютерам хранить больше информации на
меньшем дисковом пространстве и обеспечило успех технологии и ее широкое
распространение.
Открытые репозитории или соревнования по анализу данных, такие как Kaggle,
позволяют получить представление о нынешнем разнообразии табличных данных. Например, в последние два года Kaggle организовал Tabular Playground
Series (https://mng.bz/pK2z), серию соревнований, вдохновленных наиболее распространенными проблемами машинного обучения с использованием табличных
датасетов и синтетических данных, разработанных генеративным ИИ, о чем мы
подробнее поговорим далее в этой главе. Хотя соревнования Tabular Playground
используют сгенерированные данные, примеры и исходные данные, которые
послужили для них образцом, выбираются из реальных кейсов, например:
вероятность и размер страховых требований;
невозвраты кредитов в банковском секторе;
тестирование продукции;
электронная коммерция;
данные с датчиков окружающей среды;
биологические и геномные данные;
экологические измерения.
Несмотря на все разнообразие применений, каждый табличный датасет имеет одну и ту же структуру в виде матрицы строк и столбцов со значениями
в числовых и текстовых форматах; могут также встречаться даты и время. Это
2.1. Свойства строк и столбцов 47
касается всех табличных датасетов, независимо от их особенностей. Хотя знание предметной области необходимо для генерации оптимальных признаков
для предиктивных алгоритмов, базовая структура табличных данных остается
единообразной во всех сферах. Такая универсальность объясняет, зачем нужна
отдельная книга о табличных данных, ведь примеры и методы можно с легкостью
переносить из одной области в другую.
Погрузимся в детали: в таблице данных, также называемой набором данных
или датасетом, мы видим строки значений, и каждая строка представляет собой
единицу (unit) анализа, которая в статистических терминах может быть названа
статистической единицей или наблюдением. Если мы, например, анализируем образцы ДНК, каждая строка в таблице представляет собой такой пример
(sample). Если мы анализируем промышленные продукты, каждая строка — это
продукт. Природа единиц анализа может значительно различаться, но принцип
сохраняется неизменным.
2.1.1. Как должна выглядеть идеальная строка таблицы
Единственное ограничение, которое следует помнить для строк в табличном
датасете, заключается в том, что примеры должны быть независимыми, если
только вы не работаете с временнˆыми прогнозами (при анализе временнˆых рядов) или другими задачами, связанными со временем. Этот принцип называется
IID, и о нем вам, возможно, уже доводилось слышать. IID — это аббревиатура
от independent and identically distributed («независимые и одинаково распределенные»). «Независимые» означает, что анализируемые значения должны быть
получены независимо друг от друга, где каждое из них не влияет на последующие
и не несет в себе информации о них. «Одинаково распределенные» означает, что
выборка производится одним и тем же способом из одной и той же совокупности
данных с одинаковым распределением.
Рассмотрим простой пример IID: подбрасывание монеты. Каждый раз, когда
мы подбрасываем монету, результат не зависит от всех предыдущих бросков,
и вероятность выпадения орла или решки одинакова для всех этих случаев.
Другими словами, подбрасывания монеты распределены одинаково и подчиняются распределению Бернулли. Если бы мы сгенерировали датасет, подбрасывая
одну и ту же монету несколько раз, полученные в результате данные были бы
независимые и одинаково распределенные, то есть IID. Свойство одинаковой
распределенности позволяет упростить моделирование и анализ данных. Например, это в будущем позволит нам случайным образом семплировать данные
при создании фолдов для кросс-валидации. Это позволит нам предположить,
что предиктивные алгоритмы не будут запоминать и воспроизводить порядок
представления данных. Типичными примерами «не-IID» являются данные о продажах из нескольких магазинов, где продажи из одного и того же магазина, как
48 Глава 2. Табличные датасеты
правило, сильно коррелируют между собой и не обязательно отражают динамику
других магазинов. Также не-IID являются данные школьных опросов, когда
каждый класс демонстрирует схожие характеристики из-за общих интересов
или жизненного опыта, привнося в данные не-IID характеристики. В этих случаях данные проявляют свойства, отличные от предположения о независимом
и одинаковом распределении, поскольку выборки получены из определенных
групп (магазинов или школьных классов). Но похожие ситуации возникают
и тогда, когда данные иерархически организованы, когда мы имеем дело с повторными измерениями (один и тот же объект измеряется несколько раз с разными, но коррелированными результатами) или когда присутствует какая-либо
временнˆая зависимость, типичная для временнˆых рядов, где отсутствие IID не
является проблемой.
На рис. 2.1 мы видим, как при графическом отображении различаются данные
IID и не-IID. Поведение IID-величины можем наблюдать на левом графике.
Это результат случайной последовательности сочетаний бросков игральных
кубиков с подбрасываниями монеты. На правом графике можно наблюдать поведение не-IID величины: обратите внимание на скачок, появляющийся после
определенного количества попыток, указывающий на изменение в процессе
генерации распределения или метода выборки его значений.
не-IID данные
Значение
Значение
Независимо и одинаково распределенные
(IID) данные
Номер семпла
Номер семпла
Рис. 2.1. Сравнение последовательностей данных IID и не-IID. На левом графике
представлены данные IID, а на правом — не-IID, что иллюстрирует типичный
характер корреляции внутри данных
Даже если подробности распределения, из которого мы черпаем данные, неизвестны и не позволяют получить сведения о IID, мы обязаны брать семплы,
независимые друг от друга и при одних и тех же условиях. Данные не-IID могут
повлиять на результаты анализа разными способами. Вот некоторые ситуации
и проблемы, которые могут возникать.
2.1. Свойства строк и столбцов 49
Бутстрапирование (bootstrapping) представляет собой выборку с повторением из исходной последовательности до тех пор, пока мы не получим новую последовательность того же размера, что и исходная. Сабсемплинг, или
подвыборка (subsampling), — это выборка без повторения до тех пор, пока не
получим новую последовательность желаемого размера. Оба эти метода семплирования имеют тот недостаток, что могут чрезмерно усилить или исказить
определенные сигналы данных, и структура зависимостей будет нарушена.
Мы вернемся к этим процедурам, поскольку некоторые алгоритмы обучения,
обсуждаемые в главе 4, используют бутстрапирование и сабсемплинг.
Обучение модели может происходить некорректно, поскольку она может запомнить нежелательные зависимости внутри обучающей последовательности, которые окажутся бесполезными при прогнозировании, когда тестовые
примеры будут отличаться от обучающих и не связаны с ними. Например,
все алгоритмы обучения, основанные на стохастическом градиентном спуске (stochasitc gradient descent, SDG) и градиентном спуске по мини-батчам (включая глубокое обучение), зависят от порядка, в котором примеры
предъявляются алгоритму. Подумайте, как не-IID данные, которые имеют
внутренние, скрытые зависимости, могут повлиять на результаты обучения
таких алгоритмов.
Кросс-валидация (cross-validation), которая является наиболее эффективным методом тестирования для валидации моделей машинного обучения,
может давать завышенные оценки, если данные не являются IID, поскольку
алгоритм может научиться группировать случаи на основе их взаимосвязей,
характерных лишь для обучающего набора.
Данные, которые не являются IID, могут быть обнаружены на этапах анализа
процесса генерации и исследования данных, поскольку:
в данных, не относящихся к IID, каждый пример, вероятно, будет демонстрировать некоторую форму связи с по крайней мере одним другим примером в том же датасете. Это часто происходит, потому что единицы данных,
которые мы считаем различными, оказываются одинаковыми. Например,
при анализе медицинских данных исследователь может изучать несколько
медицинских записей и думать, что единицей анализа являются сами записи,
которые действительно отличаются. На самом деле мы анализируем данные
о состоянии здоровья одних и тех же пациентов в разное время: истинными
единицами анализа должны быть пациенты, а не их диагнозы. Похожая ситуация однажды произошла с командой профессора Эндрю Ына (см. https://
mng.bz/OBGE), когда они подготовили данные для статьи, используя датасет
из 100 000 рентгеновских снимков от 30 000 пациентов, и, разделив его на
обучающие и тестовые данные, не учли, что рентгеновские снимки одного
и того же пациента могут как попасть в обучающий набор, так и оказаться
50 Глава 2. Табличные датасеты
частью тестового набора, тем самым завышая результаты и искажая выводы,
полученные в результате анализа;
распределение в данных неоднородно. Обычно это можно наблюдать с течением времени, и происходящие изменения могут отражаться на многих
аспектах данных. Например, при анализе балансовых счетов важно учитывать, что даже схожие по типу компании могут относиться к разным
распределениям, если отчеты составлены в разные годы. Это связано с тем,
что макроэкономическая ситуация изменчива и характеристики компаний
и секторов экономики могут со временем сильно меняться (в свою очередь,
меняя характеристики распределения, которое мы изучаем).
Поскольку любой анализ основан на некоторых ожидаемых предположениях
о законе распределения, IID данные имеют первостепенное значение для получения правильных оценок в статистическом и регрессионном анализе. В машинном обучении, даже если методы подкрепляются данными (data-driven) и не
являются сугубо параметрическими, настоятельно рекомендуется все равно
всегда брать IID-данные, хотя на практике они едва ли встречаются в реальных
датасетах. Одним из основных ограничений алгоритмов машинного обучения
является то, что они «знают» о соотношении между признаками и целевой переменной, но не могут определить отношения между строками данных. Алгоритмы
машинного обучения умеют делать выводы на основе столбцов, но не строк. Следовательно, необходимо адекватно предоставлять дополнительные признаки для
обеспечения нормальной работы алгоритмов. Если два признака коррелируют
между собой, алгоритм будет интерпретировать эту корреляцию как следствие
их внутренней взаимосвязи, независимо от влияния других признаков, а не как
результат временнˆых зависимостей или скрытого фактора, исказившего выборку.
Другими словами, когда данные не являются IID, алгоритм обучения будет выявлять закономерности на основе времени или корреляции между примерами,
как если бы они были отношением между признаками.
Временнˆые ряды и все виды панельных, или лонгитюдных, данных (longitudinal
data), отражающих динамику процессов, обычно не являются IID. Во временнˆых
рядах каждое наблюдение автокоррелировано, что означает, что каждое целевое
значение коррелирует с предыдущими, а также со связанными признаками,
называемыми в статистике ковариатами, которые могут объяснить поведение
целевой переменной. В случае с бизнес-данными в процессе их сбора и компоновки необходимо понимать, как время влияет на наблюдения, и пытаться
контролировать его влияние с помощью признаков, основанных на времени.
Благодаря им модель может определить, как время влияет на значения других
признаков и целевой переменной. Обычно введение временнˆого признака и признаков, отражающих сдвиг по времени (лаг), помогает решить эту проблему. По
сути, здесь может быть два сценария:
2.1. Свойства строк и столбцов 51
Каждая строка обозначена датой или имеет заданный временной интервал.
Можно использовать ее как признак после соответствующего преобразования, например преобразования дат в формат времени Unix, то есть в непрерывный числовой признак. В таком случае, проводя анализ временнˆых рядов,
важно использовать правильные стратегии кросс-валидации.
Можно разбить на несколько отдельных признаков столбцы данных, относящихся к разным периодам времени, получая, таким образом, признаки, не
зависящие от времени. Например, имея значение признака в разные моменты
времени, можно создать отдельный признак для каждого из таких моментов
(например, sales_month_1, sales_month_2 и т. д.). В этом случае мы получаем
IID данные и можем приступить к анализу без дополнительных условий.
Перекрестные данные (cross-sectional), обычно представленные в табличной
форме, могут не быть естественным образом IID, даже если они взяты из одного
и того же временнˆого периода. Это связано с тем, что взаимодействия между
единицами анализа и их принадлежность к определенным группам могут создавать скрытые зависимости между наблюдениями, часто называемые утечкой
информации. Утечка может вызвать неожиданную передачу предсказательной
информации от признаков к целевой переменной во время обучения, но этого
не произойдет во время прогнозирования. Чтобы решить такую проблему, необходимо создавать признаки, которые явно фиксируют принадлежность строк
к разным группам и их взаимосвязи. Например, если вы работаете с данными из
разных компаний, невозможность получить информацию о секторе экономики
или стране, где они находятся, либо об их операционной деятельности может
породить утечку: алгоритм может изучить и использовать скрытые порядки
в данных, которые не будут воспроизводиться на этапе прогнозирования. Важно
четко разделять группы в процессе кросс-валидации, чтобы каждая из них появлялась только в обучающих или валидационных фолдах. Если вы пометите
принадлежность примеров в обучающих данных к правильной группе и не
сделаете то же самое в тестовых, это может привести к завышенным оценкам
кросс-валидации, поскольку алгоритм машинного обучения может использовать
автокорреляцию внутри групп.
Когда перекрестные данные используются для сравнения различных временнˆых
периодов, данные могут не быть независимыми, даже если взаимодействия между
единицами и группами отсутствуют. В этом случае порядок наблюдений имеет
значение, и необходимо учитывать временнˆые зависимости между ними. Это
означает, что данные не являются IID, и обычные предположения о независимости могут не выполняться. Чтобы справиться с этой ситуацией, мы можем
использовать модели временнˆых рядов для обработки временнˆых зависимостей
между наблюдениями. Такие модели считают наблюдения, сделанные в разные моменты времени, вероятно коррелированными и могут использовать эту
52 Глава 2. Табличные датасеты
корреляцию для улучшения прогнозов. Используя соответствующие модели
и методы анализа временнˆых рядов, можно получать точные прогнозы даже при
работе с данными, не являющимися IID.
В качестве общего заключительного совета по подготовке данных с точки зрения
строк рекомендуется следующее.
Проверьте, как время влияет на ваши данные. Рассмотрите возможность использования временнˆых признаков и моделирования влияния времени, используя лаги и скользящие средние, как это принято в анализе временнˆых рядов, чтобы контролировать изменения, вызванные исключительно временем.
Будьте внимательны к тому, что вы представляете в строках, поскольку группы и связи между ними могут повлиять на результаты обучения, валидации
и тестирования вашей модели.
Если в ваших данных присутствуют скрытые группы, явно представьте группирующие переменные в виде признаков.
При кросс-валидации, если данные делятся на группы, используйте групповую кросс-валидацию, чтобы группы не разделялись между обучающими
и валидационными фолдами (https://mng.bz/YDgA).
Если время является определяющим фактором в вашей модели, используйте
валидацию на основе времени (https://mng.bz/GeGO).
В следующем разделе мы обсудим идеальные критерии, которым должны соответствовать табличные столбцы, и то, как тип данных определяет способ его
обработки.
2.1.2. Как должен выглядеть идеальный столбец таблицы
Анализ данных, организованных в строки, может представлять некоторые проблемы, но важно помнить, что бˆольшая часть информации находится в столбцах
таблицы, которые также называются признаками, и именно им следует уделить
основное внимание. Столбец имеет то свойство, что информация в нем однородна
и связана с описываемыми ею строками. Например, если вы создаете датасет по
оценке недвижимости, где каждая строка представляет отдельный дом, столбцы могут отображать его площадь (в квадратных метрах) и рыночную оценку
(цена последней продажи). Поскольку столбцы однородны, можно ожидать,
что каждый из них будет содержать только ту информацию, для которой он
предназначен. Например, в столбце о площади не должно быть цены продажи
или других сведений. Кроме того, значения столбцов должны быть однозначно
связаны с единицей анализа, представленной в строке.
Прежде чем мы перейдем к разбору структур данных, подходящих для составления датасетов, состоящих из строк и столбцов, давайте познакомимся с пятью
2.1. Свойства строк и столбцов 53
типами данных, которые могут заполнять столбцы (табл. 2.1), и обсудим наилучший способ обработки каждого типа.
Таблица 2.1. Типы данных в табличном датасете
Тип
Описание
Числовые признаки
Целые числа для подсчета данных, например при подсчете продаж
по дням для продукта: [105, 122, 91, … 124].
Числа с плавающей точкой для значений, например доходов от продаж по дням для продукта: [1000.50, 1230.00, 950.80, …, 1200.00]
Порядковые признаки
Целые числа (или числа с плавающей точкой без десятичных знаков) для ранжирования или упорядоченных уровней: [0, 1, 2, 3, …,
999].
Порядковые признаки могут быть выражены словами с помощью шкалы Лайкерта или шкалы предпочтений: [«полностью не
согласен», «не согласен», «нейтрально», «согласен», «полностью
согласен»]
Категориальные признаки с малым числом
значений (низкокатегориальные)
Поскольку они выражают качество, они могут быть строкой (и называться «метками») или целыми числами, связанными с метками;
в этом случае обычно имеется правило расшифровки: {0: «красный»,
1: «зеленый», 2: «синий»}.
Категориальные
признаки с большим
числом значений (высококатегориальные)
То же, что и низкокатегориальные признаки, но с большим количеством меток: коды городов или почтовые индексы (например,
в США https://postal-codes.net/united-states)
Даты
Могут быть строкой или в специальных форматах данных, таких
как «2022-03-04», «15 февраля 1957 г. 23:45» или «12 июня 2022 г.»
(существует много стандартов даты)
Знание типов данных является базовым шагом в построении датасета и правильной обработке информации для подачи в модель машинного обучения. Каждый
алгоритм потребует специальной подготовки каждого типа данных для обес
печения наилучшего понимания моделью и, как следствие, качества прогноза.
Мы работаем с числовыми признаками, если данные выражены целым числом или
числом с плавающей точкой, отражающим результат подсчета некоторого значения. Согласно статистике, признаки, представленные в виде чисел с плавающей
точкой, часто задаются в виде шкал отношений или интервалов. Различие между
ними заключается в том, что шкалы отношений, представляющие измерение
чего-то реального, например единиц продаж или денежных сумм, имеют абсолютный действительный ноль и являются только положительными. Интервальные
шкалы вместо этого являются абстрактными или произвольными измерениями;
54 Глава 2. Табличные датасеты
их единицы могут представлять что угодно. В интервальных шкалах нулевое
значение является произвольным, а значения могут быть отрицательными. Например, такие шкалы являются мерой температуры в градусах Фаренгейта или
Цельсия. Мы можем превратить шкалу отношений в интервальную с помощью
простых преобразований, таких как вычитание среднего значения (это называется центрированием) или деление на стандартное отклонение (это называется
нормализацией или стандартизацией). Таким образом, можно сделать нулевое
значение и всю шкалу произвольными.
Целые числа в числовых признаках изображаются только шкалами отношений.
Целые числа также могут использоваться для порядковых и даже категориальных признаков. Если требуется глубже понять числовой признак, выраженный
целыми числами, следует проверить, действительно ли числа не являются непрерывными и принимают ли они достаточно много различных значений. Когда
эти условия выполняются, можно быть почти уверенными, что мы имеем дело
с числовыми признаками. Они не требуют большой обработки, и для целей машинного обучения не важно, являются они шкалой отношений или интервалов.
Все, что нужно учесть, это то, что они представляют собой один тип измерения.
Например, если мы заносим в свой датасет денежные значения, нельзя создать
столбец, представляющий доллары, евро и фунты вместе: чтобы числовой признак был пригоден для использования, нужен один тип измерения для каждого
столбца. Кроме того, следует убедиться, что в числовом признаке не слишком
много пропусков и у него достаточно различных принимаемых значений. Следует избегать констант и признаков с малым разбросом значений.
Порядковые признаки всегда состоят из целых чисел, обычно представляющих
рейтинги и оценки или баллы. В качестве примера порядкового признака на ум
приходит рейтинг в виде звездочек для товаров, продаваемых на электронных
маркетплейсах. Поскольку они также представляют собой числа, порядковые
признаки схожи с числовыми, однако работать с ними следует иначе. Во-первых,
если числовые значения представляют собой шкалу, где каждое значение равноудалено от следующего и предыдущего, нельзя гарантировать, что расстояние
между точками значений в порядковой шкале всегда постоянно. Например, как
в соревнованиях по бегу на длинные дистанции, первое и второе места могут
отличаться на несколько секунд. Однако между вторым и третьим местами разница может составлять уже минуты.
Аналогично в порядковой шкале, представляющей некоторую величину, одни
соседние точки могут быть численно очень близки друг к другу, а другие — очень
далеки. Порядковая шкала просто отражает порядок, то есть каждое значение
предшествует следующему и следует за предыдущим. Поэтому без довольно
грубых допущений нельзя обрабатывать порядковый признак так же, как если
бы он был чисто числовым, например вычислять для него среднее значение
и стандартное отклонение. С определенной точки зрения можно рассматривать
порядковые признаки как категориальные с неким порядком категорий.
2.1. Свойства строк и столбцов 55
Категориальные признаки — это наборы меток, и каждое уникальное значение
такого признака представляет собой некоторое качество, которым обладает
наш образец. Эти метки могут быть выражены в виде строк — в этом случае мы
однозначно работаем с категориальным признаком, или в виде целых чисел —
и тогда мы не должны путать его с порядковым признаком. Пропущенные
значения тоже можно рассматривать как метку. Следовательно, с пропусками
легче иметь дело в категориальных признаках. Мы различаем категориальные
признаки с низкой и высокой кардинальностью в зависимости от количества уникальных принимаемых значений в столбце. Это разделение уместно, поскольку
категориальные признаки с высокой кардинальностью трудны для алгоритмов
глубокого и машинного обучения и требуют более сложной обработки, чем
признаки с низкой кардинальностью. Не существует четкого понятия, что такое
низкая и высокая кардинальность. Однако наличие более десятка различных
значений одного признака обычно представляет собой нетривиальную задачу
для аналитика. На протяжении всей книги мы будем говорить о специальных
стратегиях для обработки категориальных признаков под общим названием
«кодирование» (encoding). На данный момент просто запомните один особый
тип категориального признака — одна метка и значения, показывающие наличие или отсутствие признака, обычно 0 для отсутствия и 1 для наличия. Такие
признаки называются бинарными или, используя термин из статистики, дихотомическими переменными.
Даты очень часто встречаются в бизнес-данных и их базах, поскольку отслеживать время для правильной работы бизнес-процессов и ресурсов очень важно.
Формат дат различается в зависимости от страны, типа бизнес-приложения или
принятых стандартов. Ввиду такого разнообразия были предприняты некоторые
усилия по стандартизации формата, например существует стандарт ISO 8601
(https://mng.bz/zZQQ), но до общепринятого подхода все равно пока далеко. Важно
знать, что даты могут быть уже представлены в виде столбцов, то есть они уже
прошли обработку и, для большей ясности, помещены в отдельные столбцы,
содержащие как циклическую компоненту (время, число, день недели), так
и нециклическую, например год. Их также можно преобразовать в числовое непрерывное значение, отражающее течение времени; лучшим примером является
время Unix — количество секунд, прошедших с 00:00:00 UTC 1 января 1970 года,
исключая високосные секунды. Наконец, даты требуют некоторого понимания
используемых соглашений для обработки пропусков, поскольку в некоторых
приложениях есть тенденция избегать пропущенных или нулевых значений
и вместо этого подставлять значение даты далеко в прошлом или будущем, чтобы
показать, что значение времени отсутствует или неизвестно.
2.1.3. Представление строк и столбцов
При организации табличного датасета важно убедиться, что каждая строка идентифицируема и использует соответствующие типы данных для дат, числовых,
56 Глава 2. Табличные датасеты
порядковых и категориальных значений, которые могут иметь тип данных
datetime, float, integer, string, и boolean для бинарных признаков. Для эффективной
обработки всех типов данных лучшей структурой, доступной в Python, является
датафрейм (DataFrame) из библиотеки pandas. Это упорядоченный набор столбцов, который обеспечивает гибкую и эффективную работу с данными. Получить
больше информации о pandas DataFrame можно по адресу https://mng.bz/0Qw6.
В листинге 2.1 мы создаем с нуля небольшой табличный датасет с четырьмя
строками, представляющими сведения о четырех людях, и четырьмя столбцами,
представляющими числовые и категориальные признаки, которые их характеризуют. При этом используется такая структура данных, как словарь. Мы также
определяем список, содержащий имена людей, который будет использоваться
в качестве ссылки для доступа к информации о них. Затем мы применяем библио
теку pandas для преобразования словаря в датафрейм — двумерную структуру
данных, похожую на таблицу, — и назначаем метки.
Листинг 2.1. Создание простого табличного датасета
import pandas as pd
data = {'пол': ['мужской', 'женский', 'мужской', 'женский'],
'возраст': [25, 30, 27, 29],
'образование': ['бакалавр', 'магистр', 'бакалавр', 'PhD'],
'доход': [50000, 60000, 55000, 70000]}
Создает словарь данных
index = ['Боб', 'Алиса', 'Чарли', 'Эмили']
Создает индексный столбец для строк
df = pd.DataFrame(data, index=index)
Создает структуру pandas DataFrame
print(df)
из данных словаря
Печатает строку с индексом 1
print(df.iloc[1])
print(df.loc['Алиса'])
Печатает строку с индексом ‘Алиса’
Вывод команды print(df) должен выглядеть следующим образом:
Боб
Алиса
Чарли
Эмили
пол
мужской
женский
мужской
женский
возраст
25
30
27
29
образование
бакалавр
магистр
бакалавр
PhD
доход
50000
60000
55000
70000
Обратите внимание, что названия строк — имена людей, а столбцы названы
именами признаков. Мы можем получить доступ к строкам и столбцам по их
названиям или числовому индексу, начиная с нуля. Таким образом, мы можем
получить доступ к информации об Алисе как с помощью индекса строки, который равен 1, так и с помощью ее имени:
пол
женский
возраст
30
образование
магистр
доход
60000
Name: Алиса, type: object
2.2. Возможные проблемы и способы их устранения 57
Это возможно благодаря функционалу pandas (https://pandas.pydata.org/). Эта
библиотека представляет собой пакет Python, предназначенный для обработки
данных. Он позволяет быстро и удобно загружать их из нескольких источников;
обращаться к данным и работать с ними, адресуясь по столбцам и/или строкам;
обрабатывать пропущенные значения; добавлять, переименовывать, вычислять,
группировать и агрегировать признаки; а также строить сводные таблицы, изменять форму и, наконец, визуализировать обрабатываемые данные. Помимо
суперполезных функций обработки данных, pandas также славится своими
структурами данных, Series и DataFrame — наиболее широко используемые
типы данных Python для табличных данных.
В датафрейме pandas, помимо таблицы данных, имеется индекс для столбцов,
чтобы можно было к ним обращаться, а также для строк, что обеспечивает значительное удобство при выполнении операций идентификации и фильтрации.
Кроме того, есть возможность эффективно выполнять выборку данных, а также
другие операции, такие как объединение столбцов или подстановка пропущенных значений. В последнее время популярная библиотека машинного обучения
scikit-learn, которая исторически принимает датафреймы pandas в качестве
входных данных для своих алгоритмов, предприняла шаги для того, чтобы обеспечить возможность работы с этой структурой данных на протяжении всего
процесса их обработки (или, как принято говорить в машинном обучении, всего
пайплайна). Теперь все выходные данные вместо преобразования в массивы
Numpy, которые представляют собой однородные матрицы, могут оставаться
в формате датафреймов pandas. Более подробную информацию о том, как это
работает и какие изменения могут быть при использовании пакета, см. по адресу https://mng.bz/nR54. Датафрейм pandas является де-факто стандартом для
табличных данных и будет широко использоваться на протяжении всей книги.
С его помощью мы покажем, как выполнять наиболее полезные преобразования
характеристик и решать возможные проблемы.
Итак, мы обсудили, какими идеальными свойствами должны обладать строки
и столбцы табличного датасета. В следующем разделе мы рассмотрим, что может
пойти не так, а также причины и способы устранения проблем.
2.2. Возможные проблемы и способы их устранения
Необходимо стараться избегать определенных состояний признаков, независимо
от их типа. Мы уже кратко упоминали об этом при обсуждении каждого типа
столбцов датасета.
Итоги соревнований по анализу данных могут помочь понять, что бывает не
так с табличными датасетами. Возьмем, например, датасет Madelon (https://
archive.ics.uci.edu/ml/datasets/Madelon), который остался в истории как очень
58 Глава 2. Табличные датасеты
сложная задача обработки данных из-за его специфических характеристик,
затрудняющих прогнозирование. Madelon — это набор данных, искусственно
созданный с помощью специального алгоритма, разработанного Изабель Гийон
(Isabel Guyon) (https://guyon.chalearn.org/), директором Google Brain с 2022 года.
Датасет был представлен на конкурсе в рамках NIPS 2003, седьмой ежегодной
конференции по нейронным системам обработки информации. Изабель Гийон
внесла ошибки непосредственно в данные в виде случайного шума и в целевой
признак, перевернув часть меток. Она добавила избыточные и сильно коллинеарные признаки, сгруппировала наблюдения вдоль вершин пятимерного
гиперкуба, не предоставляя никакой информации об этом, и, наконец, добавила
нерелевантные данные. Это заставило тогда многих специалистов поломать головы. Если интересно, см. https://mng.bz/ga4V, где вы найдете много интересного
о генерации синтетических данных.
Совсем недавно на Kaggle появились такие соревнования, как Don’t Overfit
(https://www.kaggle.com/competitions/overfitting), Don’t Overfit II (https:/www.
kaggle.com/competitions/dont-overfit-ii), Categorical Feature Encoding Challenge I
и II (https://www.kaggle.com/competitions/cat-in-the-dat и https://mng.bz/jpyP), в ходе
которых стало ясно, что даже самые мощные алгоритмы машинного обучения
мало что могут сделать, если в данных слишком много проблем, например,
с категориальными признаками и пропусками.
Исходя из практики, проблемы в данных, на которые следует обращать больше
всего внимания при работе с таблицами, следующие:
столбцы с нулевой или почти нулевой дисперсией (quasi-constant column);
дублирующиеся или высококоллинеарные признаки;
нерелевантные признаки. Приоритет следует отдавать тем, которые демонстрируют высокую предсказательную силу;
столбцы, имеющие слишком много пропусков;
редкие категории с большим количеством меток у категориального признака;
несоответствия, а также неуместные, перевернутые или искаженные зна
чения;
вероятные места утечки данных.
Давайте рассмотрим каждую проблему с теоретической и практической точек
зрения, используя искусственные данные. В заключительном разделе будет показано больше реальных ситуаций и команд для обнаружения и обработки таких
случаев. Довольно сложно найти единый реальный общедоступный датасет,
содержащий все вышеупомянутые примеры плохих данных (на самом деле их
много в частных репозиториях). Публичные датасеты обычно уже обработаны,
и большинство таких ловушек данных в них уже устранены.
2.2. Возможные проблемы и способы их устранения 59
2.2.1. Столбцы с нулевой или почти нулевой дисперсией
Избегайте столбцов с нулевой или околонулевой дисперсией. Как правило, дисперсия не должна приближаться к нулю для числовых признаков, а наиболее
вероятный класс для категориальных признаков не должен превышать 99.9 %. Все
это имеет первостепенное значение, поскольку алгоритмы машинного обучения
могут учиться только на том, как эмпирические условные математические ожидания целевой переменной изменяются в зависимости от признаков. Отсутствие
изменений в признаках подразумевает отсутствие обусловленных ими изменений
в целевой переменной и, соответственно, информации, на которой можно учиться. Константные признаки приводят к большему объему обработки вычислений
внутри алгоритмов, а квазиконстантные могут даже приводить к некоторому
переобучению, поскольку минимальная непостоянная часть признака может
быть интерпретирована как важная информация, влияющая на целевое выходное
значение. Решение состоит в том, чтобы удалить такие столбцы (листинг 2.2).
Листинг 2.2. Исключение признаков с нулевой дисперсией
import pandas as pd
from sklearn.preprocessing import OrdinalEncoder
from sklearn.feature_selection import VarianceThreshold
data = pd.DataFrame({"признак_1":['A' for i in range(15)],
"признак_2":['B' if i%2==0 else 'C'
for i in range(15)],
"признак_3":[i**2 for i in range(15)]})
ord_enc = OrdinalEncoder()
data[data.columns] = ord_enc.fit_transform(data)
var_threshold = VarianceThreshold(threshold=0)
Порядковый
кодировщик
преобразует данные
из строковых меток
в упорядоченные
числовые значения
Класс VarianceThreshold отфильтрует
все значения, равные заданному
clean_data = var_threshold.fit_transform(data)
порогу или меньше него
print(var_threshold.variances_)
Все
значения
дисперсий можно получить
print(clean_data.shape)
при помощи атрибута .variances_
2.2.2. Дублирующиеся и высококоллинеарные признаки
Избавляться от дублирующихся или высококоллинеарных столбцов необходимо, потому что хотя обилие информации и повышает надежность обучения, но
все же с некоторыми оговорками. Самое главное — избыточность информации
делает обучение более сложным и вычислительно затратным. Прежде всего,
дублирующиеся признаки бесполезны, как и константы. Они должны быть сразу
отброшены, потому что просто тратят память и процессорное время. Для сильно
коллинеарных признаков ситуация обстоит несколько иначе. Независимо от
того, говорим мы о числовых признаках, где коллинеарность измеряется при
помощи корреляции, или о категориальных, где коллинеарность определяется
60 Глава 2. Табличные датасеты
мерами ассоциации на основе критерия хи-квадрат, если набор признаков имеет
сильные внутренние связи, это обусловлено тем, что:
между признаками существуют причинно-следственные связи. Например,
в датасете с результатами успеваемости время, потраченное на учебу, и результаты экзаменов будут сильно коррелировать, поскольку время подготовки
является одним из факторов успешной сдачи экзаменов, что доказано во
многих исследованиях;
все коллинеарные признаки связаны с некоторым скрытым признаком, который является для них общей причиной или влияющим фактором. Например,
в датасете с данными об автомобилях мощность двигателя и выброс выхлопных газов частично определяются типом используемого топлива. Даже если
этот признак не зафиксирован в данных и связь неочевидна, характеристики
мощности и выбросов будут тесно связаны друг с другом.
В первом случае видится простое решение: нужно сохранить признак, отвечаю
щий за причину явлений, и отбросить все остальные. Правда, ничто в самих
данных не подскажет нам, какой именно из признаков отражает причину: в основном, это можно выяснить только с помощью знания предметной области,
логики или причинно-следственного анализа, что потребует дополнительного
моделирования и экспериментов. Второй случай несколько сложнее, потому что,
даже если вы сможете определить признак, являющийся причиной, он может
не присутствовать в явном виде в датасете, и вам нужно будет подумать, какие
признаки его заменят, и сохранить их. Опять же, знание предметной области
поможет вам принять правильное решение. Анализ данных, в свою очередь,
поможет понять связи между целевой переменной и имеющимися признаками,
поскольку позволит добиться меньшего числа ошибок, пропусков в данных
и выбросов. В идеале нужно сохранить только самый качественный признак
либо тот, который наиболее тесно связан с целевой переменной (листинг 2.3).
Листинг 2.3. Обнаружение высококоллинеарных числовых признаков
Добавляет больше коррелированных признаков в датасет: мы выбираем
первые пять признаков и дублируем их в конце датасета, предварительно
добавив немного шума, чтобы они не совпадали с исходными
Создает синтетический датасет: команда make_classification
создаст датасет из двадцати слабо коррелированных
признаков (см. https://mng.bz/eynQ)
import numpy as np
from sklearn.datasets import make_classification
from statsmodels.stats.outliers_influence import variance_inflation_factor
Задает параметр псевдослучайному генератору, чтобы
np.random.seed(0)
обеспечить повторение случайной последовательности
X, _ = make_classification(n_redundant=0,
n_repeated=0,
random_state=0)
X = np.hstack([X, X[:,:5] + np.random.random((X.shape[0],5))])
vif = [variance_inflation_factor(X, i) for i in range(X.shape[1])]
print(np.round(vif,2))
for a in range(X.shape[1]):
for b in range(X.shape[1]):
if a < b:
from sklearn.datasets import make_classification
from statsmodels.stats.outliers_influence import variance_inflation_factor
Задает параметр псевдослучайному генератору, чтобы
np.random.seed(0)
обеспечить повторение случайной последовательности
X, _ = make_classification(n_redundant=0,
n_repeated=0,
2.2.random_state=0)
Возможные проблемы и способы их устранения 61
X = np.hstack([X, X[:,:5] + np.random.random((X.shape[0],5))])
vif = [variance_inflation_factor(X, i) for i in range(X.shape[1])]
print(np.round(vif,2))
for a in range(X.shape[1]):
for b in range(X.shape[1]):
if a < b:
r = np.corrcoef(X[:, a], X[:, b])[0][1]
if np.abs(r) > 0.90:
print(f"признаки {a} и {b} имеют r={r:0.3f}")
Пороговое значение для принятия решения о коллинеарности признаков
оценивается по абсолютной величине (корреляция будет отрицательной, если
значения признаков обратно пропорциональны). В зависимости от этапа анализа
это значение можно установить на 0.90, 0.95 или даже 0.99
Коэффициент корреляции вычисляется с использованием
функции NumPy np.corrcoef (https://mng.bz/8Olw)
При поиске коррелированных признаков путем итерации по матрице корреляции
мы вычисляем только коэффициенты нижнего треугольника матрицы
Вычисляет фактор инфляции дисперсии для определения признаков,
которые вносят наименьший уникальный вклад
В предыдущем примере мы работали с высококоллинеарными числовыми признаками в синтетическом датасете, используя два подхода: фактор инфляции
дисперсии и коэффициент корреляции Пирсона. Фактор инфляции дисперсии
(variance inflation factor, VIF) является типичным инструментом анализа при
подготовке к использованию линейных моделей — семейству моделей, которые
будут обсуждаться в главе 4. VIF служит для количественной оценки того, насколько информация в одном признаке может быть найдена в других признаках.
Таким образом, VIF анализирует данные шире, чем коэффициент корреляции,
который является двумерной функцией связи только между двумя переменными. VIF, в свою очередь, пытается определить роль признака по отношению ко
всем остальным. Коэффициенты VIF являются мощным средством определения
потенциально менее полезных признаков. Тем не менее они не покажут, какой
признак связан с другими или какой должен быть удален. Чем выше значение
VIF, которое начинается с 1 и растет до бесконечности, тем выше коллинеарность
признака. В нашем примере, поскольку более высокие значения VIF указывают на менее уникальный вклад признака, можно заметить, что есть проблемы
с пятью первыми и пятью последними признаками:
[14.98 13.64 11.85 13.12 15.75
1.24 1.63 1.28 1.45 1.23 1.15
1.2 1.41 1.19 1.46 1.31 1.38 1.3 1.19
1.16 18.86 16.79 15.39 16.45 17.82]
Далее необходимо провести попарное сравнение с помощью корреляционного
анализа, чтобы определить, какие признаки можно исключить. Используя пороговое значение 0.99, мы выявим только почти идентичные признаки. Установив
планку пониже, на 0.90 или 0.95, мы выявим группу признаков, чей уникальный
вклад минимален:
62 Глава 2. Табличные датасеты
признаки
признаки
признаки
признаки
признаки
0
1
2
3
4
и
и
и
и
и
20
21
22
23
24
имеют
имеют
имеют
имеют
имеют
r=0.966
r=0.963
r=0.947
r=0.958
r=0.964
После этого нужно решить, какой из двух высококоллинеарных признаков мы
оставим. Отбор признаков на основе их качества — это необходимая операция
для того, чтобы впоследствии работать только с полезной информацией. В общем,
имея неустранимые проблемы с данными, такими как ошибки при измерениях
или при записи значений либо большой объем пропусков и выбросов, признак
следует сохранять только в том случае, если он показывает интересную и значимую связь с целевой переменной.
2.2.3. Нерелевантные признаки
Сказанное выше в равной мере касается и нерелевантных признаков, которые не
имеют большого смысла в контексте решаемой задачи и практически никакой
связи с целевой переменной. На этом этапе нужно просто исключить маловероятные признаки, которые не несут никакой полезной информации, что можно
выяснить, основываясь на знаниях предметной области или простых статистических одномерных тестах, таких как коэффициент корреляции или хи-квадрат,
о котором мы поговорим в последней части этой главы.
Выбор признаков важен для снижения вычислительных требований во время
обучения, для получения более интерпретируемой модели и улучшения качества
прогнозов на тестовых данных, тогда как нерелевантные признаки генерируют
ложные сигналы и вносят искажения в работу ML-алгоритмов, как классических,
так и на базе нейронных сетей. В главе 6 мы подробно обсудим отбор признаков
и покажем инструменты, подходящие для выбора рабочего подмножества из
начального набора признаков.
Мы уделим особое внимание тем инструментам, которые помогают изучить поведение признаков после их перемешивания (feature reshuffle) или случайной замены
в их значениях (randomization). Идея состоит в том, чтобы построить модель для
решения задачи, а затем перемешать признаки, чтобы проверить, не слишком ли
сильно меняются результаты и не снижаются ли прогностические характеристики.
Признаки, рандомизация значений которых не влияет на производительность
модели, скорее всего, будут нерелевантными или содержат избыточную информацию, доступную в других признаках. Следовательно, их можно спокойно отбросить, что снизит использование памяти и время обучения/прогнозирования.
2.2.4. Пропуски
Пропуски в данных — это критический фактор. Они встречаются в любом источнике и нарушают работу многих алгоритмов. Модели глубокого и машинного
2.2. Возможные проблемы и способы их устранения 63
обучения не могут напрямую обрабатывать пропущенные значения. Однако
некоторые специализированные алгоритмы, такие как XGBoost и LightGBM,
могут справляться с этой проблемой, не требуя какого-либо вмешательства.
Они принимают пропуски последним ненулевым значением, использовавшимся в аналогичной ситуации. Для получения дополнительной информации см.
https://mng.bz/EaxO.
Помимо специализированных алгоритмов, пропущенные значения обычно
устраняются с помощью процесса, известного как импутация (imputation).
Это подразумевает использование для вставки пропуска значения или класса
информации, присутствующей в том же столбце (простая одномерная импутация) или во всех других доступных столбцах (многомерная импутация).
Пополнение данных с помощью многомерного алгоритма более эффективно,
даже для XGBoost и LightGBM. Мы обсудим многомерную импутацию более
подробно в главе 6.
Обратите внимание, что, если вы имеете дело с пропущенными данными на
раннем этапе подготовки, их отсутствие может само по себе содержать некую
информацию. Например, если вы работаете с запросом реляционной базы данных
и используете операции LEFT или RIGHT JOIN (либо FULL OUTER JOIN),
у вас будут появляться пропуски, если записи не совпадают. В такой ситуации это
будет означать отсутствие соответствия условиям, записанным в определенной
таблице базы данных, что является ценной информацией.
В других случаях пропуск будет означать что-то конкретное относительно
того, как были сгенерированы данные. Например, в переписи населения могут
быть пропущены социально неприемлемые ответы относительно дохода, если
отвечающий слишком богат или беден. Создание бинарного признака, указывающего, отсутствует ли значение, может помочь отслеживать такие закономерности. См. MissingIndicator в пакете scikit-learn для получения более подробной
информации об этом виде обработки: https://mng.bz/EaxO.
2.2.5. Редкие категории
Что касается категориальных признаков, то слишком большое количество меток
или присутствие редкой категории среди значений признака — это две проблемы, которые необходимо устранить как можно скорее, например еще на этапе
извлечения данных. Мы уже упоминали в этой главе категориальные признаки
с высокой кардинальностью. И наоборот, среди категориальных переменных
бывают так называемые редкие, когда определенные значения категориального
признака встречаются нечасто или слабо представлены в датасете.
Редкие категории увеличивают вероятность переобучения. Обычно проблему
можно исправить путем их агрегации в более крупный класс, для этого необходимо знание предметной области.
64 Глава 2. Табличные датасеты
В случае, когда категорий слишком много, наиболее подходящим решением является кодирование целевой переменной, которое эффективно для градиентного
бустинга, или использование эмбеддингов, что отлично работает для подходов
глубокого обучения. Мы обсудим эти методы и способы их эффективной реализации в главах 6 и 7.
2.2.6. Ошибки в данных
Обнаружение несоответствий, неуместных, перевернутых или искаженных
значений — это отдельная тема, поскольку решение такой проблемы в основном
зависит от понимания области применения и процедур записи данных. Многие
ошибки могут возникнуть во время наблюдения за явлением в реальном мире
и записи наблюдений. Ошибки могут быть разными, от неизбежных погрешностей измерения у приборов и датчиков до всевозможных несоответствий или
отклонений оценок в большую или меньшую сторону, которые приводят к тому,
что снятые данные становятся ненадежными. Если в данных то здесь, то там
обнаруживаются ошибки, следует проверить весь датасет на предмет того, не
является ли проблема системной. Ошибки почти всегда случаются в определенных ситуациях и искажают часть информации. Знание структуры и возможных
значений данных — единственное разумное средство для преодоления таких проблем. Этот совет может показаться слишком общим и расплывчатым, но такова
природа этой проблемы. Старайтесь понять свои данные и не принимать ничего
как должное. Не надейтесь, что алгоритмы машинного обучения надежны и сами
исправят все огрехи и неточности: систематические ошибки могут ограничивать
способность моделей к обобщению и формированию надежных прогнозов.
2.2.7. Утечка данных
Наконец, последний момент, с которым нам следует разобраться, — это наличие
признаков, приводящих к утечке данных. Утечка данных возникает, когда некая
предиктивная информация, которая не должна быть задействована в обучении
модели, временно завышает результаты, что приводит к снижению производительности при последующем прогнозировании. Утечка может происходить на
уровне строк и признаков. Мы уже обсуждали утечку при работе со строками, не
являющимися IID, которая возникает, когда некоторые образцы связаны с другими через время или через какой-либо другой признак. Теперь пришло время
обсудить утечку на уровне признаков. Она является более распространенной,
чем хотелось бы, проблемой, с которой можно столкнуться при извлечении
бизнес-данных из баз.
Ключевой принцип — обеспечение временнˆой согласованности признаков, используемых для моделирования. В идеале они должны соответствовать целевой переменной или предшествовать ей во времени. В лучшем случае ни один
2.3. Поиск данных вовне и внутри организации 65
признак не должен создаваться или генерироваться после временнˆой точки
целевой переменной. Такое согласование во времени помогает избежать потенциальной утечки данных и гарантирует, что модель делает прогнозы на основе
информации, которая была доступна на момент прогнозирования. Нам нужна
модель, способная предсказывать настоящее или будущее. Но мы не сможем ее
построить, если необходимые признаки, на которых обучается модель, будут
получены в момент времени, следующий за самим прогнозом.
На этапе обучения модели это ограничение легко нарушить, поскольку вся информация берется из прошлого и многие источники информации, используемые
для построения обучающих данных, могут не иметь меток времени создания
или изменения. Возьмем, к примеру, компанию, предлагающую кредиты и использующую в работе алгоритм машинного обучения. Знание сроков платежей
может оказаться ценным предсказательным признаком на момент обучения,
но рассуждения о том, будет ли такой признак доступен при выдаче кредита
или нет, — поскольку это будущее поведение, близкое к моменту определения
целевой переменной, — делают его бесполезным и вводящим в заблуждение
из-за высокого риска будущих утечек. В качестве возможного решения этой
проблемы мы предлагаем тщательно сверять время генерации признаков и целевых переменных, если это возможно, и проверять, следует ли целевое значение
за признаками или наоборот. Часто такая информация вполне доступна, в зависимости от того, как организовано хранение данных в компании. Например,
данные могут содержать специальные метаданные, показывающие дату и время
вставки или обновления, или существует специальное поле временнˆой метки
для указания времени внесения каких-либо изменений, настроенное администратором базы данных.
Обсудив требования к идеальным табличным данным и риски, возникающие
при их нарушении, в следующем разделе мы перейдем к реальным кейсам и расскажем, как найти табличные данные в интернете и внутри организаций.
2.3. Поиск данных вовне и внутри организации
Предположим, нам нужен датасет для проекта с машинным обучением. Следуя
инструкциям в этом разделе, вы гарантированно найдете то, что вам нужно,
если это доступно в интернете. Фактически со временем не только количество
и качество репозиториев данных увеличилось, но и появились новые инструменты поиска и агрегации, такие как Google Dataset Search и Kaggle Datasets,
которые позволяют определить, что мы ищем, и получить результаты списком.
Но давайте по порядку.
Поиск необходимых данных всегда происходит после постановки задачи проекта
и определения его целей. Независимо от того, связаны наши цели с бизнесом
66 Глава 2. Табличные датасеты
или это научная задача, только после того, как мы четко сформулируем цели,
мы сможем решить, какие данные нужно собрать. Это процесс нелинейный,
поскольку потребуется многократно проводить сопоставление целей, данных
и доступных ресурсов. Сбор и подготовка данных всегда выполняются после
постановки задачи и определения цели, но до любых инженерных задач, таких
как создание пайплайна (определение этапов обработки данных, от репозитория
до места выполнения вычислений и любых дальнейших действий по созданию
модели). Между предоставлением данных и их обработкой обязательно присутствует этап осмысления данных, который описан в заключительной части
данной главы.
Поскольку ML-моделям нужна целевая переменная, то есть то, что следует
спрогнозировать, например число в задаче регрессии или класс (метка) в задаче классификации, сначала необходимо будет задать один или несколько
выходов модели. Затем всем ML-моделям потребуется другая информация —
набор предикторов, или признаков, которые будут использоваться для прогнозирования целевых выходных данных, то есть в процессе, при котором одни
данные (предикторы) сопоставляются с другими (выходами). Не задав целевую
переменную, нельзя начать работу, но, с другой стороны, создать модель с неполным набором предикторов и увеличивать его по ходу дела можно, однако
всегда сначала следует начинать с обозначения целевой переменной, а затем
уже думать о предикторах.
Поиск подходящих данных для ML-моделей и их правильная обработка на
подготовительном этапе и при создании пайплайна — это один из этапов, на
который специалисты тратят больше всего времени и усилий, и, на самом деле,
тут они чаще всего испытывают трудности. Требуется приложить усилия для
извлечения данных из репозиториев, в которых они хранятся; после необходимо
разобраться, как они организованы (то есть какова схема данных) и как правильно объединить все это в единый датасет таким образом, чтобы это можно
было использовать в проекте.
2.3.1. Использование pandas для доступа к хранилищам данных
Обычно и целевая переменная, и предикторы находятся в одном и том же
хранилище данных и даже в одной и той же таблице или файле. Но иногда
данные необходимо предварительно собрать. Их можно найти как внутри, так
и за пределами организации. В первом случае придется искать в файлах Excel,
в нормализованной форме в хранилище данных (data warehouse, DWH) или
среди сырых потоковых данных. Для того чтобы минимизировать избыточность
и повысить целостность данных, их масштабируемость, производительность
и простоту обслуживания, информация в DWH обычно разбивается на более
мелкие, узкоспециальные таблицы, содержащие одну предметную область или
2.3. Поиск данных вовне и внутри организации 67
сущность. Это состояние данных называется нормализованной схемой. Как правило, в такой ситуации вы можете столкнуться с тремя типами таблиц.
Таблицы событий (таблицы транзакций или таблицы фактов) предназначены для хранения записей определенных бизнес-событий, измеряемых
в определенный момент. В качестве примеров можно привести заказы, размещенные на платформе электронной коммерции, транзакции по кредитной
карте в банковском деле, визит к врачу в здравоохранении или поток кликов
пользователя в интернете. Эти таблицы могут принимать различные формы
в зависимости от типа деятельности и конкретных отслеживаемых событий.
Таблицы товаров (также известные как таблицы продуктов) — это тип таблиц
в DWH, которые содержат детальную информацию о конкретных бизнеспродуктах или событиях. Например, они могут содержать описание, цены
и уровни складских запасов товаров, приобретенных клиентом, или сведения
о рецептах на лекарства, выписанных пациенту. Цель такой таблицы — предоставить более подробные данные для анализа и принятия решений.
Таблицы измерений хранят дополнительную описательную информацию для
добавления контекста к данным в других таблицах, например дату или место
рождения человека или категорию продукта. Нужно быть внимательным
при использовании таких таблиц, поскольку информация в них подвержена
изменениям или обновлениям с течением времени. Это может происходить
двумя способами: с сохранением истории изменений, когда создается новая
запись для каждой модификации с обозначенной датой, или просто путем
перезаписи старых данных новыми. В последнем случае строки могут оказаться не согласованными по времени, и такие данные могут внести шум
или способствовать утечке информации в будущем, негативно воздействуя
на корректность прогнозов модели.
При работе с файлами Excel также приходится часто сталкиваться с разрозненными и ненормализованными данными. Здесь, как и в случае с DWH, возникает проблема организации работы с похожими таблицами и необходимость
консолидации и сборки их в целостную структуру. Для координации между
таблицами событий, товаров и измерений на рынке существуют различные
инструменты — некоторые из них даже не предполагают программирования,
и работа происходит без скриптов, с использованием графического интерфейса
по принципу «навести курсор и щелкнуть». На протяжении многих лет SQL являлся основным инструментом для сбора и объединения данных из нескольких
источников. И сегодня это лучший вариант для работы с реляционной базой
данных. Он помогает структурировать последовательность запросов и временнˆых
таблиц, пока не сформируется окончательная таблица данных для загрузки
в алгоритм машинного обучения. Однако если имеется несколько источников
данных и нужно проверить и визуализировать промежуточные результаты, то,
68 Глава 2. Табличные датасеты
по нашему мнению, удобнее воспользоваться pandas. Этот пакет предлагает множество функций, которые повторяют и расширяют возможности SQL-запросов.
Выборка, фильтрация, агрегация, упорядочивание и обработка так же просты
в pandas, как и в реляционной базе данных, и следуют тем же принципам, хотя
иногда называются по-другому. Например, индексы в датафреймах — это первичные ключи в реляционных базах данных. Кроме того, датафреймы в pandas
обладают некоторым дополнительным функционалом, который делает их более
универсальными и мощными для задач анализа данных, чем SQL.
Универсальный API, совместимый с другими инструментами, обеспечивает
простой переход с pandas (ориентированных на одноядерные вычисления)
на многопроцессорную обработку или распределенные вычисления.
Можно одновременно выполнять обработку и исследование данных. В частности, легко построить график подготовленных данных, и мы увидим, как
это делается, в конце этой главы.
Больше контроля над всеми необходимыми этапами обработки данных и изменениями в них, что означает повышенный риск ошибок, но одновременно
позволяет сохранять промежуточные результаты и некоторые характеристики данных, такие как их типы и порядок сортировки, чего иногда трудно
добиться в SQL.
Давайте представим, что в хранилище данных есть три таблицы, каждая из которых содержит различную информацию о продуктах компании. Эти таблицы
необходимо объединить для анализа или обучения модели (рис. 2.2).
Таблица df2
Таблица df1
product_id product_name price
Таблица df3
product_id product_description category
product_id
manufacturer
weight
1
Товар А
10.99
1
Отличный товар
Категория А
1
Производитель А
1.5
2
Товар Б
20.99
2
Высококачественный товар
Категория Б
2
Производитель Б
2.0
3
Товар В
15.99
3
Надежный товар
Категория В
3
Производитель В
1.8
4
Товар Г
8.99
4
Доступный товар
Категория Г
4
Производитель Г
1.2
Рис. 2.2. Три простые таблицы, описывающие характеристики продуктов
Задача представляет собой довольно простой пример, который потребует
создать SQL-запрос, возвращающий датасет, объединяющий три исходные
таблицы:
SELECT df1.product_id, df1.product_name, df1.price,
df2.product_description, df2.category, df3.manufacturer, df3.weight
FROM df1
JOIN df2 ON df1.product_id = df2.product_id
JOIN df3 ON df1.product_id = df3.product_id;
2.3. Поиск данных вовне и внутри организации 69
Того же самого можно легко добиться в pandas, используя функцию merge, сохраняя контроль над различными этапами объединения данных и подготавливая их к дальнейшим операциям по преобразованию их в вид, пригодный для
машинного обучения (листинг 2.4).
Листинг 2.4. Объединение датасетов в pandas
import pandas as pd
df1 = pd.DataFrame({'product_id': [1, 2, 3, 4],
'product_name': ['Товар А',
Первая
'Товар Б',
таблица,
'Товар В',
содержащая
'Товар Г'],
цены
'price': [10.99, 20.99, 15.99, 8.99]})
df2 = pd.DataFrame({'product_id': [1, 2, 3, 4],
'product_description': ['Отличный товар',
'Высококачественный товар',
'Надежный товар',
Вторая таблица,
'Доступный товар'],
содержащая
'category': ['Категория A', 'Категория Б',
описания
'Категория В', 'Категория Г']})
df3 = pd.DataFrame({'product_id': [1, 2, 3, 4],
'manufacturer': ['Производитель A', 'Производитель Б',
'Производитель В', 'Производитель Г'],
'weight': [1.5, 2.0, 1.8, 1.2]})
Третья таблица, содержащая
merged_df = pd.merge(df1, df2, on='product_id')
информацию об изготовителе
merged_df = pd.merge(merged_df, df3, on='product_id')
и характеристики товаров
print(merged_df)
Слияние с третьей
таблицей
Слияние первых двух
таблиц
Одним из ограничений для выполнения этого в pandas является вычислительная
эффективность, поскольку пакет медленный. См., например, следующий ответ
Stack Overflow: https://mng.bz/N1x1. Также существуют проблемы с масштабируемостью, поскольку pandas не может обрабатывать данные, превышающие
доступную память вашего компьютера. Все дело в том, что при разработке биб
лиотеки приоритет отдавался функциональности, а не производительности.
Следовательно, большинство функций pandas написаны на чистом Python, и для
них недоступна оптимизация на уровне компиляции, в отличие от NumPy — другого популярного пакета для работы с матрицами и массивами, где применяются
Fortran и C++. Однако у pandas удобный и популярный API, который можно
быстро освоить и начать с ним свои проекты, а затем их можно масштабировать
при помощи более мощных инструментов. Как уже было сказано, разные продукты имеют бˆольшую или меньшую совместимость с pandas.
Dask (https://www.dask.org/) — это библиотека Python с открытым исходным
кодом, которая предоставляет пользователю как низко-, так и высокоуровневые интерфейсы. Она предназначена для работы на кластерах компьютеров.
70 Глава 2. Табличные датасеты
Dask также может использоваться в многопроцессорном режиме на нескольких процессорах CPU и легко справляется с задачами, превышающими объем
оперативной памяти (out-of-core). Вы сможете обрабатывать больше данных,
объем которых превышает размер RAM, путем анализа их фрагментов (чанков), находящихся на диске. Эта структура данных копирует API pandas
DataFrame, но способна обрабатывать значительно большее число строк.
Ray (https://www.ray.io/) — это низкоуровневый фреймворк, который распараллеливает код Python на процессорах или кластерах. Он идеально подходит в качестве бэкенда для других высокопроизводительных решений,
таких как Modin.
Modin (https://github.com/modin-project/modin) — это, пожалуй, самый совместимый с pandas API инструмент. Он работает с помощью простой замены,
такой как import modin.pandas as pd, и показывает наилучшую производительность с Ray в качестве бэкенда.
Vaex (https://vaex.io/) — это библиотека Python для работы с данными, превышающими объем оперативной памяти, использующая ленивую парадигму вычислений (аналог pandas). Допускает визуализацию и исследование
больших табличных датасетов на автономной машине или сервере. Ленивые
вычисления позволяют оптимизировать операции и достигать производительности до миллиарда объектов/строк в секунду.
RAPIDS (https://rapids.ai/) — это набор библиотек для использования вычислительных возможностей GPU на больших матрицах. Он предлагает
cuDF — частичную замену pandas. Обработка данных с помощью RAPIDS
повышает производительность (сокращает время вычислений), но не масштабируемость, поскольку GPU должны обращаться к памяти, чтобы определить, что именно вычислять.
Spark (https://spark.apache.org/) — это решение для map-reduce (метода обработки больших данных), графовых алгоритмов, потоковых данных и SQLзапросов, работающих на одиночных машинах или кластерах. Он предлагает
различные пакеты и структуру данных DataFrame, аналогичную pandas. Это
решение лучше всего подходит для обработки больших объемов табличных
данных.
Polars (https://www.pola.rs/) разработана как высокопроизводительная библио
тека для работы с DataFrame. Она написана на Rust, что обеспечивает более
быстрое выполнение, сравнимое с C/C++, и поддержку распределенных
вычислений. Polars также имеет лучшее управление памятью и обработку
больших датасетов и использует столбцовый формат хранения, который
более эффективен для плотных данных, которые являются наиболее распространенным типом табличных данных. Строковые же форматы хранения
(как в pandas) больше подходят для разреженных данных. Polars и pandas име-
2.3. Поиск данных вовне и внутри организации 71
ют похожие API, но отличаются режимами работы: Polars поддерживает как
eager-режим, когда команды выполняются немедленно, так и ленивый (lazy)
режим, когда выполнение откладывается до явного указания. Несмотря на
то что библиотека все еще развивается, она быстро набирает популярность
в сообществе специалистов по data science.
Каждый из этих инструментов может оказаться полезным в зависимости от
вашего опыта использования pandas и размеров ваших табличных датасетов.
В качестве общего совета мы рекомендуем проверить, доступны ли функции
pandas в каждом из продуктов, упомянутых выше, чтобы вам не пришлось вносить изменения в код и думать, какое решение выбрать.
2.3.2. Данные из интернета
Теперь, когда мы создали план получения и сбора данных из различных источников хранения, таких как реляционные базы, DWH и озера данных, пришло
время обсудить, где искать дополнительные источники информации в интернете. Они помогут вам применить алгоритмы глубокого и машинного обучения,
описанные в этой книге, и позволят протестировать их на различных датасетах
или даже провести сравнительный анализ ваших моделей. Мы посоветуем, где
найти онлайн-ресурсы, которые можно использовать для увеличения и улучшения уже полученных вами данных.
Что касается конкретных источников, существуют специализированные платформы, где табличные данные собираются и активно используются исследователями и практиками. Лучшим примером является репозиторий машинного
обучения UCI Machine Learning Repository (https://archive.ics.uci.edu/). Сообщество ML-специалистов давно использует этот веб-сайт в образовательных целях
и для исследования алгоритмов машинного обучения. Также можно упомянуть
OpenML (https://www.openml.org/) — репозиторий, который scikit-learn задействует для своих примеров. Он поддерживается Open Machine Learning Foundation,
некоммерческой организацией, чья миссия заключается в том, чтобы сделать
машинное обучение простым, доступным, совместным и открытым. Он также
спонсируется частными компаниями, такими как Amazon, и университетами.
В обоих случаях — UCI и OpenML — вы можете загрузить каждый датасет,
следуя предоставленным инструкциям или напрямую перейдя по URL-адресу
самих данных. Когда мы будем использовать датасеты из этих источников, то
будем приводить фрагмент кода Python для прямой загрузки данных, чтобы вы
могли сразу приступить к работе.
Помимо этих открытых репозиториев, многие другие веб-сайты предлагают
более узкие подборки открытых данных, предоставляемых бесплатно для
академического, научного или даже коммерческого использования (но нужно
72 Глава 2. Табличные датасеты
проверять условия лицензии). Правительства, научное сообщество, а иногда
даже частные компании предоставляют такие данные общественности. Если
повезет, вы найдете подходящие датасеты, обратившись к бесплатному репозиторию данных Harvard University Dataverse по адресу https://dataverse.harvard.
edu/ или просмотрев сабреддит Dataset https://www.reddit.com/r/datasets/. Оба
ресурса — это отличный шанс случайно обнаружить что-то полезное, но они
могут разочаровать, если вы ищете конкретные данные. Предположим, ваш поиск связан с данными, относящимися к общественной или макроэкономической
сфере, такой как транспорт, энергетика, политическая жизнь, торговля, промышленное производство, потребление и т. д. В этом случае порталы открытых
данных укажут, где искать такие датасеты. Два лучших подобных портала — это
Data Portals (http://dataportals.org/), который охватывает данные со всего мира,
и Open Data Monitor (https://opendatamonitor.eu/), который специализируется на
европейском регионе. Еще одним хорошим источником является Национальная статистическая служба. Вы можете найти данные по конкретным странам
в этом исчерпывающем списке, предоставленном Бюро переписи населения
США (https://mng.bz/eynQ).
Помимо этих примеров, отличным способом взаимодействия с открытыми
данными, несомненно, является поисковая система по датасетам Dataset Search,
предоставляемая Google, которую можно найти по адресу https://datasetsearch.
research.google.com/. Поиск датасетов Google, включающий множество возможных источников и разрозненных репозиториев, может помочь вам найти то, что
вы ищете.
На рис. 2.3 показаны результаты поиска по Dataset Search в Google, где мы запросили датасет, связанный с данными кредитного скоринга.
Нередко приходится искать внешние данные для начала проекта, когда в ваших системах нет похожей информации либо вы хотите поэкспериментировать с какой-то идеей или моделью машинного обучения. Например, давайте
представим, что вам нужно найти внешние данные по кредитному скорингу,
чтобы построить на них модель с нуля. Кредитный скоринг — это вычисляемая
количественная оценка кредитоспособности человека или компании. Модель
кредитного скоринга помогает успешно вести бизнес, связанный, например,
с выдачей и продлением ипотечных и автокредитов, кредитных карт и частных
займов. Кредитный скоринг можно использовать для ценообразования на основе риска, то есть для установления справедливой кредитной ставки с учетом
рисков невозврата.
Если вы ищете данные по кредитному скорингу в поисковой системе Google,
в левой части браузера откроется панель со списком датасетов, которые могут
удовлетворять вашему запросу. Вы можете отфильтровать результаты по новизне данных, формату, лицензии (этот момент следует учесть, если вы хотите
2.3. Поиск данных вовне и внутри организации 73
использовать данные в коммерческих целях), стоимости и категории. На правой
панели появится описание ключевых характеристик выбранного датасета со
ссылкой, позволяющей получить доступ к исходному репозиторию, где хранятся и документируются данные. Чтобы загрузить их, следуйте инструкциям на
каждой целевой странице, на которую вы будете перенаправлены.
Насколько нам известно, Google Dataset Search — лучший инструмент для поиска
нужных данных. Кроме того, мы хотели бы указать на другой ресурс, предоставленный Google через Kaggle — компанию, занимающуюся соревнованиями по
data science, которая была приобретена Google в 2017 году. Как вы наверняка
заметите, поисковая система Google Dataset Search часто возвращает датасеты,
размещенные на платформе Kaggle. Помимо организации состязаний, Kaggle
предлагает услугу хостинга датасетов под названием Kaggle Datasets, которую
можно найти по адресу https://www.kaggle.com/datasets.
Рис. 2.3. Что возвращает Google Dataset Search для данных кредитного скоринга.
Поисковая строка (вверху страницы): здесь вы вводите поисковый запрос, так же
как и в обычной поисковой системе. Панель результатов (слева): здесь вы можете
просматривать результаты, найденные поисковой системой. Информация о датасете
(центр и правая страница): здесь отображается основная информация о наборе
данных, включая ссылки для доступа к данным
74 Глава 2. Табличные датасеты
Kaggle Datasets — это часть сервиса, предлагаемого пользователям платформы,
позволяющего им свободно загружать данные (иногда потому, что они открыли
их исходный код) в обмен на баллы и повышение рейтинга в игровой системе
Kaggle. Игровая механика также стимулирует пользователей оперативно загружать данные, поэтому здесь вы найдете самые свежие и регулярно обновляемые
наборы. Результатом является впечатляющая коллекция датасетов, которую
не так-то просто найти где-либо еще и которая охватывает широкий спектр
предметных областей, начиная от экономической статистики и заканчивая
транзакциями, которые полезны для создания рекомендательных систем с нуля.
В зависимости от активности пользователя, разместившего датасет, и его вовлеченности в сообщество Kaggle вы можете найти документацию, анализ данных,
модели машинного обучения и связанные с этим обсуждения.
Если вы нашли то, что искали, можете загрузить данные из Kaggle Dataset напрямую, используя кнопку загрузки, показанную на рис. 2.4, после регистрации
на сайте. Регистрация также позволяет установить и использовать kaggle-api
(https://github.com/Kaggle/kaggle-api) для загрузки датасетов из командной строки.
Например, после установки вы можете загрузить данные о кредитном риске
в Германии German Credit Risk командой
kaggle datasets download -d uciml/german-credit
Рис. 2.4. Параметры на странице кредитного риска Германии в Kaggle Datasets
(меню справа). Можно легко загрузить весь датасет с помощью команды Kaggle API
из выпадающего меню. Чтобы найти команду, запросите ее прямо на странице через
меню, связанное с набором данных, как показано на рисунке
2.3.3. Синтетические данные
В ситуации, когда у вас уже есть некоторые датасеты, но нужно увеличить их
объем, можно воспользоваться генерацией синтетических данных. Этот подход
не создает данные с нуля, но эффективно расширяет и улучшает существующие
наборы, делая возможным использование моделей машинного и глубокого
2.3. Поиск данных вовне и внутри организации 75
обучения, которые требуют более значительного количества примеров для
корректной работы. Генерация синтетических данных — это применение генеративного ИИ и стремительно развивающаяся область, которая позволяет:
устранить дефицит данных, увеличив количество доступных табличных
примеров;
обеспечить разнообразие данных путем улучшения части набора, как в случае
с классом меньшинства в задаче несбалансированной классификации;
генерировать пограничные случаи, чтобы предоставить примеры, которые
сложно встретить в реальных данных, но которые необходимы для тестирования систем;
сохранить конфиденциальность, поскольку умеет генерировать табличные
данные с теми же характеристиками, что и исходные, но без рисков утечки
приватной информации, поскольку все сгенерированные данные являются
вымышленными.
В последнее время генерация данных совершила огромный скачок вперед
благодаря первым экспериментам в области генеративного ИИ с использованием генеративно-состязательных сетей (GAN) и вариационных автоэнкодеров
(VAE) — двух архитектур глубокого обучения, которые несколько лет назад
стали лидерами генеративного ИИ.
GAN были представлены Яном Гудфеллоу (Ian Goodfellow) и его коллегами
в 2014 году и состоят из пары глубоких нейронных сетей, а именно генератора
и дискриминатора, которые обучаются одновременно и взаимодействуют друг
с другом. Генератор генерирует образцы, а дискриминатор должен отличать сгенерированные образцы от реальных. Это обучение без учителя (unsupervised).
При наличии достаточного времени и вычислительных ресурсов постоянное
взаимодействие между генератором и дискриминатором приводит к тому, что
первый научится имитировать распределение реальных данных, на которых
обучается дискриминатор. Это удивительно, если учесть, что генератор нико
гда не видит реальных данных, которые он должен воспроизвести, потому что
создает их из чисто случайного шума. Такой процесс отлично работает как для
изображений, так и для табличных данных.
VAE состоят из энкодера и декодера. Их функция заключается в сжатии входных
данных с помощью энкодера до состояния, называемого скрытым (латентным)
пространством, где информация о данных сильно сжата. Затем декодер распаковывает данные с высокой точностью и восстанавливает исходные данные. Основная идея заключается в том, что если процесс сжатия-распаковки эффективен,
то скрытое пространство инкапсулирует основную информацию о распределении данных, позволяя генерировать новые. Архитектура VAE спроектирована
таким образом, что входные данные принимаются энкодером и проходят через
76 Глава 2. Табличные датасеты
последовательность слоев, количество нейронов в которых может оставаться неизменным или уменьшаться, пока не достигнут слоя, представляющего скрытое
пространство. Этот слой служит отправной точкой для декодера.
Эффективность этих методов глубокого обучения доказывается тем фактом, что
платформа для соревнований по анализу данных Kaggle использует синтетические данные для своих мероприятий. Из-за нехватки состязаний по анализу
табличных данных Kaggle недавно запустила серию конкурсов, применяющих
синтетические данные, которые не уступают традиционным соревнованиям, применяющим тщательно отобранные исходные данные. Серия Tabular Playground
стала визитной карточкой Kaggle за последние два года и продолжает ею быть
по сей день (https://www.kaggle.com/competitions).
В листинге 2.5 мы используем пакет sdv для генерации 10 000 дополнительных
примеров из исходного датасета German Credit Risk, представленного в предыдущем разделе и состоящего из 1000 образцов. Чтобы установить пакет sdv на
вашей системе, введите следующую команду в терминале:
pip install sdv
Пакет sdv, созданный по инициативе MIT, представляет собой набор инструментов с открытым исходным кодом, разработанных для того, чтобы помочь людям
и предприятиям генерировать синтетические данные, начиная с некоторых исходных. Такие инструменты используют как классические статистические методы, например гауссовы копулы, которые могут воспроизводить распределение
множества переменных одновременно, так и глубокое обучение. Инструменты
глубокого обучения основаны на GAN и VAE. В нашем примере мы обнаружили,
что архитектура последнего может лучше и проще имитировать имеющиеся
данные. Вы можете найти больше информации о пакете sdv на странице проекта GitHub (https://github.com/sdv-dev/SDV) или прочитав справочную статью,
описывающую данную методологию: Неха Патки (Neha Patki), Рой Уэдж (Roy
Wedge), Калян Веерамачанени (Kalyan Veeramachaneni), «The Synthetic Data
Vault», IEEE DSAA 2016 (https://mng.bz/ga4V).
Листинг 2.5. Создание синтетического датасета
import pandas as pd
from sdv.metadata import SingleTableMetadata
from sdv.single_table import TVAESynthesizer
Импортирует из sdv функцию для генерации метаданных из исходного датасета
Импортирует VAE из sdv для
генерации синтетических данных
data = pd.read_csv("./german_credit_data.csv", index_col=0)
data = data.reset_index()
print(data.shape)
Создает экземпляр
print(data.head)
Загружает датасет
German Credit Risk
детектора метаданных
Находит метаданные
metadata = SingleTableMetadata()
в первоначальных данных
metadata.detect_from_dataframe(data)
print(metadata.to_dict())
metadata.update_column(column_name="Saving accounts",
sdtype="categorical")
synthesizer = TVAESynthesizer(metadata, epochs=10_000)
synthesizer.fit(data)
synthetic_data = synthesizer.sample(num_rows=10_000)
synthetic_data.head()
Инстанциирует VAE-модель и дает
Проверяет
корректность
считывания
метаданных
в каждом
столбце
from sdv.single_table import TVAESynthesizer
Импортирует VAE из sdv для
генерации синтетических данных
data = pd.read_csv("./german_credit_data.csv", index_col=0)
data = data.reset_index()
print(data.shape)
Создает экземпляр
print(data.head)
Загружает датасет
German Credit Risk
детекторавовне
метаданных
2.3. Поиск данных
и внутри организации 77
Находит метаданные
metadata = SingleTableMetadata()
в первоначальных данных
metadata.detect_from_dataframe(data)
print(metadata.to_dict())
metadata.update_column(column_name="Saving accounts",
sdtype="categorical")
synthesizer = TVAESynthesizer(metadata, epochs=10_000)
synthesizer.fit(data)
synthetic_data = synthesizer.sample(num_rows=10_000)
synthetic_data.head()
Инстанциирует VAE-модель и дает
команду обучаться 10 000 эпох
Выполняет обучение VAE-модели
на первоначальных данных
Генерирует новый датасет на 10 000 образцов
Проверяет
корректность
считывания
метаданных
в каждом
столбце
Исправляет
метаданные
столбца Saving
accounts
Выполнение скрипта занимает некоторое время, поскольку VAE для разработки
хорошего скрытого представления исходных характеристик и распределений данных необходимо определенное количество итераций по исходным данным. После
завершения выборочные примеры из новых синтетических данных напоминают
выборку из исходных, но, если вам нужно более формальное доказательство, вы
можете прибегнуть к методу состязательной проверки, или адверсариальной
валидации (adversarial validation), когда алгоритм машинного обучения должен
отличить исходные данные от сгенерированных. Алгоритм ML более склонен
обнаруживать даже тонкие закономерности в данных; следовательно, если он
может запутаться при различении исходных и сгенерированных данных, сгенерированные считаются хорошего качества. В листинге 2.6 мы покажем, как
приступить к настройке состязательной проверки для синтетических данных,
полученных из данных о кредитном риске в Германии.
Листинг 2.6. Метод состязательной проверки синтетического датасета
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import roc_auc_score
X = pd.concat([synthetic_data, data])
Объединяет путем конкатенации
categorical_columns = [
оригинальные и сгенерированные данные
"Sex", "Housing", "Saving accounts",
"Checking account", "Purpose"]
X_encoded = pd.get_dummies(X, columns=categorical_columns)
Кодирует
y = [0] * len(synthetic_data) + [1] * len(data)
категориальные
model = RandomForestClassifier()
cv_preds = cross_val_predict(
model,
X_encoded,
Классифицирует,
y,
является ли пример
cv=5,
оригинальным или
n_jobs=-1,
синтетическим
method="predict_proba"
)
roc_adv_score = roc_auc_score(y_true=y, y_score=cv_preds[:, 1])
print(f"roc auc adv score: {roc_adv_score:0.3f}")
признаки
в фиктивные
переменные
Вычисляет
показатель
ROC AUC для
классификации
78 Глава 2. Табличные датасеты
Площадь под кривой ROC AUC — это метрика, используемая для оценки качества алгоритма машинного обучения в задаче бинарной классификации. Она
измеряет способность алгоритма различать положительные и отрицательные
примеры на основе предсказанных вероятностей, присвоенных каждому наблюдению. Оценка 1 указывает на идеальный классификатор, а оценка 0.5 — на
модель, которая работает не лучше случайного угадывания. Подробнее об этой
метрике можно узнать на сайте https://mlu-explain.github.io/roc-auc/ Университета
машинного обучения — образовательной инициативы Amazon, направленной на
преподавание теории машинного обучения.
В данном случае оценка ROC AUC составляет приблизительно 0.567. Это означает, что синтетические данные практически неотличимы от исходных.
В следующем разделе мы завершим наш обзор, изучив набор табличных данных, полученный из репозитория машинного обучения UCI и датасетов Kaggle.
В ходе анализа данных будут обнаружены многие из проблем, упомянутых ранее,
а также реализованы некоторые меры по их устранению.
2.4. Разведочный анализ данных
Сборка датасета в матрицу данных, организованную по строкам и столбцам, — это
лишь начальный этап длительного процесса. Затем данные изучаются, исследуются, преобразуются и, наконец, используются в модели. Изучение и анализ
данных — критически важные этапы процесса, поскольку они позволяют выяснить, не упущено ли что-то важное на этапе сбора информации и какие шаги
можно предпринять для решения конкретной прогностической проблемы.
Разведочный анализ данных (EDA, exploratory data analysis) часто ассоциируется с генерацией признаков, особенно в случае использования классических
подходов машинного обучения для табличных данных. Однако, как правило,
EDA практически полностью игнорируется в глубоком обучении. Мы хотим
подчеркнуть, что EDA не должен ограничиваться созданием признаков. Это
комплексный процесс исследования, направленный на то, чтобы понять, как
можно наилучшим образом использовать данные.
EDA, безусловно, тесно связан со статистикой, поскольку был предложен одним
из самых выдающихся статистиков XX века Джоном У. Тьюки (John W. Tukey)
в его шедевре 1977 года «Exploratory Data Analysis»1. В своей работе Тьюки
утверждал, что статистическая работа не сводится к простому моделированию
и проверке гипотез, основанных на теоретических предположениях. По его
словам, данные, полученные с помощью EDA, также могут рассказать о возможностях решения научных и инженерных задач и подсказать наилучшие
способы их обработки. Для EDA не существует готовых шаблонов. Тем не менее
1
Тьюки Дж. «Анализ результатов наблюдения. Разведочный анализ».
2.4. Разведочный анализ данных 79
грамотное использование статистического описания и тестов, а также графическое представление признаков как по отдельности, так и в их взаимосвязи
с другими может подсказать, как работать с данными. Как правило, сначала
с помощью описательной статистики и графиков изучается каждый признак
по отдельности — это называется одномерным подходом в статистике. Затем
смотрят, как признаки соотносятся друг с другом, — это двумерный подход. Наконец, пытаются получить общее представление обо всех признаках, используя
многомерные методы и методы снижения размерности, такие как t-SNE и UMAP.
На этом этапе, сразу после сборки данных в табличную матрицу, с помощью
EDA можно сделать следующее:
узнать о характеристиках данных, например являются ли они числовыми,
порядковыми, категориальными или датами;
получить представление о значениях каждого признака и о том, как они
распределены. Это особенно полезно при работе с нейронными сетями, где
важны шкалы значений;
получить сведения о наличии пропущенных значений. Это важно, поскольку
нейронные сети и некоторые алгоритмы машинного обучения считаются
неработоспособными, если есть пропуски в данных;
найти выбросы и кластеры значений, вызванные ошибками при извлечении
данных и сборке датасета;
выявить редкие категории, которые можно исключить или объединить
с другими.
Мы покажем, как провести такие исследования с помощью простых команд
pandas: мы считаем процесс EDA более эффективным, чем автоматизированный,
потому что ключевую роль в нем играют знания и мышление эксперта. Однако
вы также можете использовать автоматические инструменты разведки данных,
если количество признаков велико и нужно сэкономить время. Позже можно
интегрировать автоматический EDA со специализированными и целенаправленными пользовательскими исследованиями. Среди библиотек с открытым
исходным кодом для автоматического EDA есть несколько очень удобных,
которые мы хотели бы предложить вам опробовать, и все они являются допустимыми решениями, которые легко изучить и использовать:
AutoViz (https://github.com/AutoViML/AutoViz);
Sweetviz (https://github.com/fbdesignpro/sweetviz);
Pandas Profiling (https:/mng.bz/DMxw).
В следующих главах мы снова рассмотрим EDA, чтобы дополнить картину дальнейшими исследованиями. Они могут понадобиться в зависимости от методов
80 Глава 2. Табличные датасеты
(машинное или глубокое обучение), которые вы будете применять к своим
данным в целях прогнозирования.
2.4.1. Загрузка датасета Auto MPG
Теперь в качестве примера того, как простой EDA может подсказать идеи и решения для дальнейшей обработки данных и принятия необходимых мер по их
улучшению при адаптации к модели, мы можем обратиться к Auto MPG Data
Set — набору, который свободно доступен в репозитории UCI Machine Learning
(https://archive.ics.uci.edu/ml/datasets/auto+mpg). Датасет, собранный Эрнесто
Рамосом (Ernesto Ramos) и Дэвидом Донохо (David Donoho), происходит из
библиотеки StatLib, поддерживаемой Университетом Карнеги — Меллона.
Ранее датасет был представлен в 1983 году на выставке Американской статистической ассоциации (American Statistical Association Exposition) (https://mng.bz/
lYa8) и в работах Росса Куинлана (Ross Quinlan), который внес большой вклад
в разработку алгоритма дерева решений и создал алгоритмы C4.5 и ID3. Он
ссылается на этот датасет в своей статье 1993 года «Combining Instance-Based
and Model-Based Learning» (https://mng.bz/BXx8) — важной вехе в решении задач регрессии в машинном обучении. Для наших целей это простой, удобный
пример из-за смешанного набора признаков и некоторых пропущенных данных,
которые нужно обработать в признаках mpg (расход топлива, мили на галлон)
и horsepower (мощность, л. с.).
Ниже приведен список доступных признаков в датасете:
mpg (расход топлива, мили на галлон): непрерывный;
cylinders (количество цилиндров): многозначный дискретный;
displacement (объем двигателя): непрерывный;
horsepower (мощность, л. с.): непрерывный;
weight (вес): непрерывный;
acceleration (разгон): непрерывный;
model year (год модели): многозначный дискретный;
origin (страна): многозначный дискретный;
car name (марка автомобиля): строка (уникально для каждого примера, может
использоваться как индекс в датафрейме pandas).
У нас короткий список, но в большинстве сложных табличных датасетов признаки могут занимать десятки страниц.
Чтобы загрузить наш датасет, можно обратиться к коду из листинга 2.7, который
подключится к репозиторию UCI и выгрузит данные в pandas DataFrame.
2.4. Разведочный анализ данных 81
Листинг 2.7. Загрузка датасета Auto MPG из репозитория UCI
StringIO читает и пишет
буфер строк в памяти
requests — это HTTPбиблиотека, которая
помогает извлечь
данные из интернета
from io import StringIO
import requests
import pandas as pd
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/"
data = "auto-mpg.data-original"
columns = ["mpg", "cylinders", "displacement", "horsepower", "weight",
"acceleration", "model_year", "origin", "car_name"]
Взято из документации
репозитория машинного
обучения UCI
Для данных фиксированной ширины требуется
задать каждый признак на входе с указанием
начального и конечного значения
colspecs = [(0, 4), (6, 9), (12, 17), (23, 28), (34, 39),
(45, 49), (52, 55), (57, 59), (61, -2)]
data_ingestion = StringIO
(requests.get(url + data).text)
data = pd.read_fwf(data_ingestion,
colspecs=colspecs,
names=columns)
Датасет считывается из интернета
с помощью requests.get()
pd.read_fwf считывает таблицу отформатированных строк
фиксированной ширины в DataFrame
Датасет Auto MPG хранится в текстовом файле фиксированной ширины, где
значения находятся на одной строке и разделены определенным количеством
пробелов или табуляцией. Возврат каретки в конце строки сигнализирует об
окончании примера. Ниже приведен фрагмент текстового файла, который биб
лиотека requests получает из репозитория UCI Machine Learning:
18.0
15.0
18.0
…
31.0
8. 307.0 130.0 3504. 12.0 70. 1. "chevrolet chevelle malibu"
8. 350.0 165.0 3693. 11.5 70. 1. "buick skylark 320"
8. 318.0 150.0 3436. 11.0 70. 1. "plymouth satellite"
4. 119.0 82.00 2720. 19.4 82. 1. "chevy s-10"
Это объясняет, почему мы должны использовать ридер с фиксированной шириной, такой как pandas read_fwf, и указывать позицию символа, где будет начинаться и заканчиваться каждое значение. Другие датасеты чаще организованы
в виде файлов CSV. Если бы тот же датасет был сохранен в CSV-формате, он
выглядел бы следующим образом:
"mpg","cylinders","displacement","horsepower","weight","acceleration",
"model_year","origin","car_name"
18.0,8.0,307.0,130.0,3504.0,12.0,70.0,1.,"chevrolet chevelle malibu"
15.0,8.0,350.0,165.0,3693.0,11.5,70.0,1.,"buick skylark 320"
18.0,8.0,318.0,150.0,3436.0,11.0,70.0,1.,"plymouth satellite"
…
31.0,4.0,119.0,82.0,2720.0,19.4,82.0,1.,"chevy s-10"
82 Глава 2. Табличные датасеты
В файлах CSV значения признаков не размещаются в фиксированных позициях
в строках; они различаются по позиции, но не по порядку в последовательности,
а разделены специальным символом-разделителем. В нашем примере это запятая.
Также обратите внимание, что текстовые данные, такие как строки или даты,
часто заключаются в кавычки, чтобы избежать путаницы, если текст содержит
разделитель. В таком случае надо использовать ридер pandas read_csv, который
может обрабатывать CSV-файлы. В зависимости от ситуации ваши данные могут храниться в виде файла JSON или XML, однако в pandas есть ридеры и для
таких форматов (табл. 2.2).
Таблица 2.2. Распространенные методы чтения данных в pandas
Функция
Описание
read_csv
Загружает разделенные данные из файла, URL или объекта, похожего
на файл. Использует запятую в качестве разделителя по умолчанию
read_json
Считывает данные из строкового представления JSON
read_xml
Импортирует поверхностные XML-документы как DataFrame
read_html
Читает все таблицы, найденные в указанном HTML-документе
read_excel
Считывает табличные данные из файла Excel XLS или XLSX
2.4.2. Изучение меток, значений, распределений
Все, что вам нужно сделать, — это понять, какие данные вы считываете из интернета или с локального диска, и использовать подходящие ридеры из тех,
что предлагает pandas: каждый формат будет автоматически обработан соответствующей функцией.
Поскольку у нас уже есть справочник по датасету (просто прочитайте attribute
information (информацию об атрибутах) по адресу https://archive.ics.uci.edu/ml/
datasets/Auto%2BMPG), можем сразу же отметить в конкретных переменных, какие
признаки являются числовыми, порядковыми и категориальными:
numeric_feats = ["mpg", "displacement", "horsepower", "weight", "acceleration"]
ordinal_feats = ["cylinders", "model_year"]
categorical_feats = ["origin", "car_name"]
В других датасетах вам придется искать такую информацию самостоятельно.
Переменные должны будут заполняться теми значениями, которые были вами
найдены. Следовательно, если мы не знаем характеристики признаков, то должны выяснить это посредством первичного анализа данных, что можно быстро
выполнить с помощью нескольких ручных команд pandas и сделать некоторые
2.4. Разведочный анализ данных 83
выводы. Например, мы могли бы запросить выборку первых строк и сразу получить представление о данных, с которыми имеем дело. Мы получаем только
первые пять строк, используя метод .head(n):
data.head(5)
Результаты показаны на рис. 2.5.
Рис. 2.5. Результат выполнения команды data.head(5)
В этом примере многие признаки — это числа с плавающей точкой без десятичных частей, это может свидетельствовать о том, что они являются целыми
числами. Целые числа также могут указывать на числовой, порядковый или
категориальный признак. Запрос количества различных значений для каждого
признака будет вторым шагом в нашем исследовании:
data.nunique()
Возвращенные результаты:
mpg
cylinders
displacement
horsepower
weight
acceleration
model_year
origin
car_name
dtype: int64
129
5
84
92
356
96
14
4
312
Из-за небольшого количества уникальных значений признаки cylinders и origin
можно рассматривать как категориальные переменные. Следовательно, их
можно преобразовать либо с помощью эмбеддинга (процедуры, которая преобразует их в непрерывные числовые признаки), либо в бинарные признаки для
каждого значения с помощью one-hot-кодирования. Знание предметной области
поможет классифицировать количество цилиндров как порядковый признак,
поскольку они могут иметь разный объем в различных моделях автомобилей,
84 Глава 2. Табличные датасеты
и, как правило, наличие большего количества цилиндров соответствует большей
мощности двигателя. Аналогично признак, связанный с годами, также может
рассматриваться как порядковый, отражающий ход времени.
Еще одна быстрая исследовательская проверка, основанная на изучении стандартного отклонения каждого из числовых признаков, может помочь более точно
идентифицировать данные и даже отобрать их:
data[numeric_feats].std()
Возвращенные результаты:
mpg
displacement
horsepower
weight
acceleration
dtype: float64
7.815984
105.207362
38.522063
849.827166
2.820984
С помощью этой команды вы можете получить сведения о признаках с нулевой
или почти нулевой дисперсией. Если исключить пропущенные значения, то
признак с нулевой или слишком низкой дисперсией можно безопасно исключить из данных, поскольку он не принесет никакой ощутимой пользы. Однако
предположим, что пропущенных значений много. В этом случае стоит создать
индикаторную переменную для отслеживания паттернов пропусков в этом
признаке, поскольку они могут быть предиктивными. Проверьте количество
пропусков с помощью команды:
(data.isna()
.sum(axis=0)
)
.isna() вернет логическое значение, указывающее на наличие пропуска. Далее
мы можем подсчитать их общее количество. Истинное значение равно 1, а ложное — 0. Полученные результаты:
mpg
cylinders
displacement
horsepower
weight
acceleration
model_year
origin
car_name
dtype: int64
8
0
0
6
0
0
0
0
0
Метод .isna помогает отслеживать значения NaN в признаках. Как уже обсуждалось, признаки с большим количеством пропущенных значений могут скрывать
2.4. Разведочный анализ данных 85
некоторые интересные предиктивные закономерности и быть преобразованы
в индикаторные переменные, отмечающие пропуски. Для этой цели можно
использовать MissingIndicator в scikit-learn: https://mng.bz/dXoO. Кроме того, неважно, какой алгоритм прогнозирования вы выбрали, пропущенные значения
будут всегда — много или мало. Если вы используете глубокое обучение, их
нужно заменить на числа. Обычно берут среднее значение, медиану или моду.
Но существуют и более сложные методы, основанные на итеративных оценках
оптимального значения для замены. Примером является IterativeImputer в scikitlearn: https://mng.bz/rKaD.
Полезно помнить, что замена пропущенных значений нулевыми является хорошей стратегией для нейронных сетей и обобщенных линейных моделей, когда
числовые признаки стандартизированы (из них вычитается среднее значение
и далее делится на стандартное отклонение): в таком случае ноль соответствует
среднему значению для всех числовых признаков. То же самое касается большинства алгоритмов машинного обучения, но подход отличается для самых продвинутых реализаций градиентного бустинга, таких как XGBoost и LightGBM.
Такие алгоритмы могут соответствующим образом обрабатывать пропущенные
данные без какого-либо дополнительного вмешательства с вашей стороны.
Иногда в табличных датасетах используются специальные значения, например
–999. Если вы знаете, что определенное значение указывает на пропуск, нужно
изменить команду, чтобы учесть эту информацию:
(data[numeric_feats]==-999).sum(axis=0)
В этом фрагменте кода вы проверяете все числовые признаки относительно
значения –999, которое является маркером для пропусков. По сути, в качестве
маркера может использоваться любое число, и вы должны заранее знать, какое
именно. Поскольку это число, оно не вызывает никаких ошибок при обучении
модели, но серьезно сбивает ее с толку. Следовательно, маркер пропущенного
значения всегда должен быть представлен как NaN, чтобы предотвратить потенциально неправильную его интерпретацию моделями глубокого обучения
и определенными алгоритмами машинного обучения. Модели на основе деревьев, такие как градиентный бустинг или случайный лес, лучше подходят для
обработки таких случаев, особенно когда маркер расположен на крайнем конце
распределения признака.
Проверив наличие пропущенных данных и стандартные отклонения, теперь
можно проверить распределение признаков, чтобы получить другую полезную
информацию, которая может помочь в обработке датасета:
data.describe()
На рис. 2.6 показан вывод метода describe.
86 Глава 2. Табличные датасеты
Рис. 2.6. Результаты работы команды data.describe()
Метод pandas describe позволяет получать базовые описательные статистики
для всех числовых признаков. Обратите внимание, что пропущенные данные
игнорируются. После проверки пропущенных значений и дисперсии или стандартного отклонения обратите внимание на минимальные и максимальные
значения, а также их соотношение со средним. Слишком большие или слишком
малые значения сигнализируют о наличии выбросов (экстремальных значений
в распределении данных) или сильно смещенном распределении. Такое встречается достаточно часто, но в некоторых случаях это необходимо устранить.
Иногда выбросы и асимметричные распределения следует принимать как есть
без каких-либо корректирующих действий. Все зависит от цели вашего анализа.
На этом этапе они могут указывать на проблемы при сборе и компоновке датасета, и корректировка подразумевает их удаление или исправление. Например,
выбросы возникают из-за ошибок в данных. Кроме того, объединенные данные,
записанные с помощью разных методологий, могли привести к смещенному
распределению. Например, в одной таблице ваши измерения указаны в метрах,
а в другой — в сантиметрах.
Причины таких ошибок в данных очень разные, поэтому рекомендуется тщательно все проверить с помощью описательных статистик и диаграмм, а затем
продумать, как все исправить. Такие ошибки характерны не только для числовых
признаков, они могут быть обнаружены и в категориальных. Давайте рассмотрим
единственный доступный строковый признак, car_name, разделив его элементы:
words = (data.car_name
.apply(lambda x: x.split())
.explode()
.value_counts()
)
words.head(15)
2.4. Разведочный анализ данных 87
Функция лямбда в функции apply разделит экземпляры car_name на отдельные
слова. Поскольку результатом будет несколько списков, с помощью метода
.explode() мы разворачиваем их в один признак. Подсчитывая отдельные
значения в этом новом признаке, мы получаем результаты частоты для каждого
слова:
ford
53
chevrolet
44
plymouth
32
(sw)
32
amc
29
dodge
28
toyota
25
datsun
23
custom
18
buick
17
pontiac
16
volkswagen
16
honda
13
mercury
11
brougham
10
Name: car_name, dtype: int64
На первый взгляд все выглядит хорошо, большинство экземпляров содержат
название бренда и некоторую информацию о типе автомобиля. Однако среди
наиболее частых меток есть ненужная информация, например sw, что означает
«спортивный универсал», и custom, которое относится к автомобильным аксессуарам. При более внимательном рассмотрении вы, наконец, замечаете, что
даже в этом модерируемом датасете есть проблемы, поскольку многие бренды
написаны неправильно:
(words.index
.sort_values()
)[-50:]
Возвращенные результаты:
Index(['seville', 'sj', 'skyhawk', 'skylark', 'special', 'spirit', 'sport',
'sportabout', 'squire', 'sst', 'st.', 'stanza', 'starfire', 'starlet',
'strada', 'subaru', 'suburb', 'sunbird', 'super', 'supreme', 'sx', 'tc',
'tc3', 'tercel', 'thunderbird', 'torino', 'town', 'toyota', 'toyouta',
'tr7', 'triumph', 'turbo', 'type', 'v6', 'v8', 'valiant', 'vega',
'ventura', 'vista', 'vokswagen', 'volare', 'volkswagen', 'volvo', 'vw',
'wagon', 'woody', 'x1.9', 'xe', 'yorker', 'zephyr'], dtype='object')
В этом срезе данных, содержащем слова, составляющие категориальный признак,
мы замечаем, что некоторые бренды написаны неправильно (например, Toyota
и Toyouta, Volkswagen и Vokswagen, а также VW как сокращение). Ошибки в написании разделят примеры, относящиеся к одной категории (Volkswagen и Toyota
88 Глава 2. Табличные датасеты
в данном случае), на несколько, подвергая модель риску сбора зашумленных
данных или отбрасывания более слабых сигналов. То же самое происходит
с другими категориальными и порядковыми признаками, которые представляют
ошибочные дополнительные категории:
(data.origin
.value_counts()
)
Возвращенные результаты для источника данных:
1.
253
3.
79
2.
73
.
1
Name: origin, dtype: int64
Давайте проделаем то же самое для model_year, упорядочив результаты по году:
(data.model_year
.value_counts()
.reset_index()
.rename(columns={'index':'model_year',
'model_year':'counts'})
.sort_values(by="model_year")
)
На рис. 2.7 показано, как будут выглядеть упорядоченные
значения для model_year. Не все отдельные проблемы
с данными сильно влияют на результаты. Однако их
совокупное присутствие может снизить производительность модели, когда датасет состоит из десятков признаков с незначительными проблемами. Решение простое: просто удалите случаи с ошибочными метками
категорий, если вы не можете разумно исправить их или
рассматривать как пропущенные значения, а затем импутировать. Поскольку нужны проверки и обоснования
по каждому признаку, может потребоваться много времени и усилий. Но в этом и есть смысл EDA, и позже
ваши старания окупятся созданием модели, которая
более надежна и дает правильные прогнозы. К таблицам
предъявляются более строгие требования к чистоте
(особенно при применении классических моделей машинного обучения), в отличие от неструктурированных
данных, обрабатываемых с помощью глубокого обучения, где ошибки иногда считаются полезным шумом,
помогающим избежать переобучения.
Рис. 2.7.
Упорядоченные
значения для
model_year
2.4. Разведочный анализ данных 89
Продолжаем искать ошибки в данных, и теперь, после проверки категориальных признаков, пришло время внимательно изучить числовые. Здесь мы
будем использовать диаграммы «ящик с усами» (boxplot, также называемые
диаграммами размаха1) и гистограммы для визуализации, а фокус сместится
с отдельных признаков (так называемый статистический одномерный подход)
на совместный анализ нескольких из них. Каждое значение в признаках примера может не быть ошибочным само по себе. Тем не менее, если все значения
взять вместе, вы можете понять, что их комбинация крайне маловероятна, если
только это не результат какой-то ошибки. Начните изучать распределения с помощью ящиков с усами, границы которых отображают ключевые характеристики
распределения: вы можете увидеть первый (Q1) и третий квартиль (Q3) как
верхнюю и нижнюю стороны ящика (медиана, Q2, находится внутри ящика).
Его усы представляют собой самые дальние точки в обоих направлениях, не
превышающие Q3 + 1,5 × IQR и Q1 – 15 × IQR (напомним, IQR — это разница
между Q3 и Q1). Выходящие за эти границы наблюдения отображаются в виде
отдельных точек, и вы можете сразу же определить, есть ли они и сколько их.
Диаграмма масштабируется в единицах измерения представляемого вами признака и, если вы хотите сравнить несколько таких диаграмм и проанализировать
распределение числовых признаков, сначала нужно стандартизировать их, вычтя
среднее значение и разделив на стандартное отклонение:
standardized = ((data[numeric_feats] - data[numeric_feats].mean())
/ data[numeric_feats].std())
standardized.boxplot(column=numeric_feats, figsize= (12, 4))
На рис. 2.8 показан результат.
Рис. 2.8. Ящик с усами для числовых признаков из датасета Auto MPG
Цель состоит в том, чтобы определить, являются ли данные неверными из-за
ошибок процедуры сбора или агрегации. Признаки с выбросами, такие как mpg,
1
Подробнее про ящики с усами см. на с. 142. — Примеч. ред.
90 Глава 2. Табличные датасеты
horsepower и acceleration, требуют более тщательного изучения с помощью
гистограммы, чтобы исключить наличие посторонних или ошибочных значений:
data.horsepower.round().hist(bins=64)
На рис. 2.9 показана полученная 64-биновая гистограмма.
Рис. 2.9. Гистограмма
признака horsepower
Что касается horsepower, то не наблюдается ничего экстраординарного, кроме
своеобразного пика около значения 150: концентрация примеров по фискальным
причинам. Если у вас автомобиль, мощность которого превышает определенный
порог, то придется платить больше налогов, и конструкторы двигателей просто
не стали превышать его, чтобы сделать свою продукцию более продаваемой.
Что касается acceleration, то тут все в порядке. Просто есть длинные хвосты
с обеих сторон:
data.acceleration.hist(bins=24)
На рис. 2.10 показана полученная 24-биновая гистограмма.
Гистограммы, представляющие собой столбчатые диаграммы частот значений
по интервалам (бинам), подходят для числовых переменных. Для порядковых
и категориальных переменных ту же информацию предоставляет простая столбчатая диаграмма частот значений:
(data.cylinders
.value_counts()
.reset_index()
.rename(columns={'index':'counts'})
.sort_values(by="counts")
.plot.bar(x="counts")
)
2.4. Разведочный анализ данных 91
Рис. 2.10. Гистограмма
acceleration
На рис. 2.11 показана полученная столбчатая диаграмма.
Рис. 2.11. Столбчатая
диаграмма для
признака cylinders
Здесь следует обратить внимание на редкие классы, поскольку они могут содержать ошибки или экстремальные наблюдения, и вам предстоит решить, нужно
ли их сохранять. Знание предметной области должно помочь вам принять эти
решения. В нашем примере относительно cylinders мы на самом деле должны
сохранить как трехцилиндровые, так и пятицилиндровые классы, даже если они
редкие. Быстрая проверка показывает, что у нас мало автомобилей с трехцилиндровыми двигателями — транспортных средств, на которых установлен этот тип
двигателя, меньше, и, как правило, они имеют гораздо меньшую долю рынка, чем
более распространенные авто с четырехцилиндровыми движками. Кроме того,
инженеры-конструкторы реже используют пятицилиндровую компоновку, как
92 Глава 2. Табличные датасеты
и другие нечетные конфигурации, поскольку она имеет затраты, аналогичные
шестицилиндровым схемам, но подразумевает бˆольшую сложность во многих
инженерных аспектах и имеет гораздо больше недостатков с точки зрения производительности.
2.4.3. Изучение двумерных и многомерных взаимосвязей
Потратив время на изучение того, как распределены значения, с помощью гистограмм и столбчатых диаграмм, вы получите информацию, которая поможет
выявить ошибки, способные в дальнейшем повлиять на вашу работу. Даже
если ошибки в каждом отдельном признаке оказывают минимальный эффект,
их сумма по всем используемым признакам может существенно повлиять на
прогнозный алгоритм. После того как вы закончите выявлять проблемы с отдельными признаками, настанет время проверить, как они соотносятся друг
с другом. То, как они связаны с вашей целевой переменной, и то, как можно
исключить часть из них без изменения или даже с улучшением результатов, —
это тема, которую мы обсудим позже при рассмотрении отбора признаков. На
данном этапе вашим приоритетом является устранение избыточных признаков
в датасете. Под ними мы подразумеваем дублирующиеся или очень похожие
признаки:
дублирующиеся признаки с разными названиями;
высококоллинеарные числовые признаки;
похожие категориальные признаки, которые кажутся различными из-за
агрегации уровней или использования разных меток;
похожие числовые и категориальные признаки, которые получены из одного
источника. Пример из финансовой сферы: наличие вероятности дефолта
и соответствующего дефолтного рейтинга, который обычно выражается
буквенными обозначениями, такими как AAA или BB.
Мы начинаем понимать, как выявлять дубликаты и высокую коллинеарность
среди числовых признаков. Предпочтительным инструментом для этого исследования является парная корреляция: корреляция между признаками, вычисленная попарно. Когда парные корреляции числовых признаков организованы
в симметричную матрицу, мы получаем корреляционную матрицу, которую
можно сразу визуализировать в виде тепловой карты для обнаружения коллинеарности, если признаков не слишком много:
import seaborn as sns
corr = data[numeric_feats].corr()
sns.heatmap(corr, cmap="Blues",annot=True)
Получившийся график показан на рис. 2.12.
2.4. Разведочный анализ данных 93
Рис. 2.12. Тепловая
карта значений
корреляции
В нашем примере задействовано всего несколько признаков, и читаемость матрицы высокая. Когда число признаков велико, лучше перечислить только парные
корреляции, которые превышают определенный порог. В обоих случаях нужно
обращать внимание на те из них, которые превышают абсолютное значение
от 0.98 до 0.99. Кроме того, они могут быть отрицательными, а отрицательная
корреляция, приближающаяся к минус единице, является еще одним случаем
коллинеарности. Когда у вас такие высокие корреляции между признаками,
нужно выяснить причину, а затем решить, какой из них следует удалить. Исключение некоторых коллинеарных признаков сократит датасет и позволит
избежать различных проблем в дальнейшем, в зависимости от используемого
метода отбора признаков или алгоритма обучения. Фактически при коллинеарности или мультиколлинеарности, если она включает более двух признаков
одновременно, вы можете столкнуться с проблемами в датасетах как в плане
сходимости алгоритма обучения, что приводит к неоптимальным результатам,
так и в плане интерпретируемости решения.
Коллинеарность, хотя она и связана с корреляцией, также затрагивает категориальные признаки, а иногда ее удается обнаруживать даже между категориальными и числовыми признаками. В этом случае использовать корреляцию нельзя.
Даже если она для этой цели вполне надежна и даже если вы сравниваете порядковые и бинарные признаки, при работе с категориальными их необходимо преобразовывать в числа с помощью процедуры, называемой кодированием меток
(label encoding), которая подразумевает присвоение категориям произвольных
чисел. Поскольку кодирование меток основано на произвольном присвоении
значений, нельзя установить идентичность одного и того же закодированного
категориального признака с помощью корреляции, если кодирование применяется к одному и тому же признаку по-разному.
94 Глава 2. Табличные датасеты
В подобных ситуациях, когда корреляция неприменима, можно использовать
ассоциативные показатели на основе статистики хи-квадрат или коэффициенте V
Крамера. Коэффициент V Крамера — это статистический показатель, основанный
на значении хи-квадрат, деленном на количество меток в сравниваемых признаках. Эта операция нормализует значение, зафиксированное для признака, делая
его сопоставимым для всех признаков датасета. Квадратный корень результата
находится в диапазоне от 0 до 1, что позволяет оценить интенсивность, но не
направленность связи. Что касается интенсивности, значения коэффициента V
Крамера, близкие к 0, указывают на несвязанные признаки, тогда как значения,
близкие к 1, — на высокую степень ассоциации и коллинеарности между признаками.
Коэффициент V Крамера также можно применять, сравнивая категориальный
признак и числовой, если предварительно дискретизировать числовой признак
в категориальный, используя, например, преобразование на основе децилей.
Листинг 2.8 представляет сравнение двух признаков нашего датасета. В этом
примере мы сначала создаем функцию для вычисления cramerV из оценки хиквадрат из таблицы. Затем применяем ее к сравнению категориального и числового признака после дискретизации последнего с использованием децилей.
Листинг 2.8. Использование коэффициента V Крамера для обнаружения
ассоциации
Импортирует из scipy функцию для
вычисления значения критерия
независимости хи-квадрат
from scipy.stats import chi2_contingency
Подготавливает функцию для расчета V Крамера, имея
def cramerV(chi2, table)
в качестве входных данных оценку хи-квадрат теста на
независимость и таблицу, из которой он был получен
Вычисляет минимальную
Подсчитывает все
размерность таблицы
элементы в таблице
Результатом является корень кваn = table.values.sum()
дратный из хи-квадрат, деленный
minimum_dimension = min(table.shape)-1
на число элементов в таблице и ее
минимальную размерность
result = ((chi2 / n) / minimum_dimension)**0.5
return result
deciles = pd.qcut(data.mpg, q=10, labels=False)
Распределяет числовые значения
mpg по децилям, чтобы они
соответствовали таблице
table = pd.crosstab(data.model_year, deciles)
chi2, p, dof, expected = chi2_contingency(observed=table)
print(cramerV(chi2, table))
Выводит на экран полученный
результат V Крамера
Возвращает оценку, p-значение,
число степеней свободы
и ожидаемую таблицу, основанную
на маргинальных вероятностях
Создает таблицу
соответствия децилей
mpg и model_year для
оценки их ассоциации
2.4. Разведочный анализ данных 95
В коде, передав значение хи-квадрат и саму таблицу, мы вызываем функцию
Cramer-V, чтобы получить значение 0.855 для коэффициента V Крамера, сравнивая mpg и model_year. Коэффициент V Крамера — это взаимная мера, используемая для идентификации признаков с аналогичной ролью в прогнозировании.
В отличие от направленных мер, коэффициент нечувствителен к перестановке
признаков. Это свойство позволяет идентифицировать высококоррелированные
признаки, которые можно удалить из анализа. Очень высокая положительная
или отрицательная корреляция между признаками может указывать на избыточность, и один из двух признаков можно исключить.
В качестве последнего шага после того, как с помощью EDA вы завершили одномерные и двумерные исследования, необходимо изучить датасет с многомерной
точки зрения: это поможет обнаружить наличие какого-то фрагмента данных, который не вписывается в общую картину. Опять же, вы проводите такую проверку
не для того, чтобы найти более точные способы построения модели, а в качестве
предварительной операции по проверке табличных данных на предмет соответствия задаче, которую вы хотите решить. Многомерные подходы требуют, чтобы
все признаки были числовыми, как это было ранее показано для V Крамера;
вы можете дискретизировать числовые признаки для этой цели и работать,
проецируя все данные в пространство меньшей размерности независимо от их
сложности, что позволит визуализировать их и легко обнаруживать аномалии.
Процесс состоит из сведˆения ваших данных к нескольким обобщающим сводным
признакам и нанесения их на график для визуального определения закономерностей и изолированных кластеров точек, которые могут оказаться аномалиями. Обычные многомерные подходы для получения проекции с более низкой
размерностью:
метод главных компонент (PCA) и сингулярное разложение (SVD): https://
mng.bz/VVl0;
стохастическое вложение соседей с t-распределением (T-distributed stochastic
neighbor embedding, t-SNE): https://lvdmaaten.github.io/tsne/;
равномерное аппроксимационное проецирование многообразий (uniform
manifold approximation and projection, UMAP): https://github.com/lmcinnes/umap.
PCA и SVD берут свое начало в статистическом анализе, и PCA имеет долгую
историю применения для целей EDA. Однако оба подхода основаны на линейных комбинациях признаков (сокращенные суммарные измерения являются
взвешенной суммой данных) и поэтому не всегда подходят для выявления
нелинейных закономерностей, которые часто встречаются в реальной жизни.
Более современные t-SNE и UMAP являются методами, которые прекрасно
сокращают размерность данных, позволяя строить надежное их представление
с сохранением исходных характеристик. Однако есть некоторые оговорки: настройка гиперпараметров обоих методов может привести к совершенно разным
96 Глава 2. Табличные датасеты
проекциям одних и тех же данных. Это связано с потерей информации в данных
при переходе от множества признаков к нескольким, что может привести к разным визуализациям — более или менее репрезентативным для самих данных.
Кроме того, полученные графики не всегда легко интерпретировать, особенно
если данных много или они сильно кластеризованы. Тем не менее t-SNE и UMAP
являются ценными инструментами для исследования и визуализации, поскольку
помогают выявить в сложных датасетах закономерности и кластеры, которые
могут быть не видны при использовании других методов. Конечно, основной
принцип EDA заключается не в том, чтобы полагаться исключительно на них,
а чтобы сравнивать их результаты с другими двумерными и одномерными методами, как было показано ранее.
Прежде чем начать работать с этими методами, прочтите, например, такие
статьи: «How to t-SNE Effectively» (https://distill.pub/2016/misread-tsne/) или
«Understanding UMAP» (https://mng.bz/AQxK). Они дадут вам дополнительную
уверенность в использовании методов с соответствующими мерами предосторожности. Также нужно помнить, что оба подхода являются вычислительно интенсивными и может потребоваться довольно много времени, чтобы уменьшить
большой и сложный датасет. Однако недавно NVIDIA разработала пакет RAPIDS
на основе технологий CUDA и GPU (https://developer.nvidia.com/rapids), который
может значительно сократить время, необходимое для получения результатов
как от UMAP, так и от t-SNE. Это делает их еще более эффективными для интенсивных исследований EDA. В листинге 2.9 показан код для анализа нашего
примера датасета с использованием реализации t-SNE в пакете scikit-learn. Мы
представим реализацию RAPIDS от NVIDIA, а также другие инструменты для
обработки табличных данных далее в книге.
Листинг 2.9. Построение низкоразмерной проекции t-SNE для датасета
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
tsne = TSNE(n_components=2,
perplexity=30.,
init="random",
learning_rate="auto",
random_state=42)
Импортирует класс t-SNE из scikit-learn
Импортирует pyplot из matplotlib
t-SNE задается проецировать результаты в двух проекциях,
остальные параметры оставлены по умолчанию
X = data[numeric_feats + ordinal_feats].fillna
(data[numeric_feats + ordinal_feats].mean())
projection_2D = tsne.fit_transform(X)
Используются только числовые
и порядковые признаки; пропуски
заменяются средним значением,
поскольку t-SNE требуется полная
матрица входных данных
plt.figure(figsize=(15, 15))
plt.scatter(projection_2D[:, 0], projection_2D[:, 1],
С помощью tsne.fit_transform данedgecolor='none',
alpha=0.80, Данные, преобразованные с помощью ные изображаются как двумерные
t-SNE, отображаются на графике, обра- относительно самих себя
s=10)
plt.show()
зуя двумерную диаграмму рассеяния
Итоги 97
На рис. 2.13 показан график t-SNE преобразования данных.
20
15
10
5
0
−5
−10
−15
−20
−15
−10
−5
0
5
10
15
20
Рис. 2.13.
Распределение
точек, полученное
в результате
двумерного
преобразования t-SNE
Фрагмент кода работает только с числовыми признаками, которые после замены
всех пропущенных значений средним (ни t-SNE, ни UMAP не могут работать
с пропусками) сжимают данные в двумерное представление, которое может
построить диаграмма рассеяния. В этом датасете результаты выглядят закономерными и не являются неожиданными, если предположить, что это выборка
тщательно отобранных примеров, собранных с определенной целью: все точки
данных в итоге были выровнены в виде криволинейного облака, создавая общее
впечатление примеров, выбранных последовательно и равномерно на основе
определенных критериев.
На этом мы завершаем наше исследование датасета Auto MPG. В следующей
главе обсудим алгоритмы машинного обучения и изучим фундаментальные
классические модели, обычно применяемые к табличным датасетам. Мы
приведем примеры, на которых покажем сильные и слабые стороны каждого
алгоритма в контексте данных и признаков, с которыми вы можете столкнуться
в проекте.
Итоги
Несмотря на огромные различия в предметных областях и организациях,
общие характеристики табличных датасетов позволяют наметить наилучшие
методы обработки данных и моделирования.
98 Глава 2. Табличные датасеты
Строки в табличном наборе данных представляют единицы наблюдения,
и при их не-IID-природе возникают как ограничения, так и возможности, на
которые следует обратить внимание. Это может влиять на выбор принятых
в data science процедур, таких как бутстрапирование, сабсемплинг и кроссвалидация.
В столбцах могут быть различные типы данных: числовые (как с плавающей
точкой, так и целые), порядковые (целые числа), категориальные (как с низкой, так и с высокой кардинальностью, то есть с маленьким или большим
количеством отдельных меток) и даты. Каждый из них требует своего подхода
к обработке и анализу. Мы предлагаем освоить пакет pandas (и его структуру
данных DataFrame), чтобы обрабатывать все эти различные типы признаков,
встречающиеся в табличных данных.
Различные проблемы данных в столбцах связаны с их типом: значения
с нулевой или почти нулевой дисперсией, дублирующиеся, высококоллинеарные, нерелевантные признаки, редкие категории и другие несоответствия,
пропущенные данные и утечка информации. Кроме того, для каждой из этих
проблем существует свое решение.
Найти и получить табличные данные относительно просто, если воспользоваться открытыми репозиториями (например, UCI Machine Learning
Repository или Kaggle Datasets) или поисковой системой Google Dataset.
EDA играет важную роль в очистке данных, которые собраны внутри организации или взяты из интернета. Можно использовать подсчеты значений
и описания, гистограммы, диаграммы «ящики с усами», корреляционные
матрицы и низкоразмерные проекции, такие как t-SNE, чтобы выявить
структуру и проблемы в данных.
3
Машинное и глубокое обучение
В этой главе
3 Сравнение машинного и глубокого обучения как методов решения
задач анализа табличных данных
3 Отличие машинного обучения от глубокого с точки зрения
простоты
3 Отличие машинного обучения от глубокого с точки зрения
прозрачности
3 Отличие машинного обучения от глубокого с точки зрения
эффективности
В сообществе специалистов по data science ведутся открытые дебаты о том, какой подход машинного обучения является наилучшим для табличных данных.
Одни утверждают, что классические методы, такие как градиентный бустинг,
например XGBoost или LightGBM, отлично подходят для решения большинства
задач с табличными данными. Другие же выступают за включение глубокого
обучения в аналитический инструментарий. В этой главе мы рассмотрим оба
подхода на двух конкретных примерах.
Прогнозирование цены аренды жилья на Airbnb в Нью-Йорке. Здесь мы
будем использовать реальный датасет Airbnb для обучения моделей, которые
100 Глава 3. Машинное и глубокое обучение
предсказывают, будет новая цена выше или ниже средней цены аренды. Мы
воспользуемся этим примером для оценки простоты, прозрачности и эффективности сравниваемых подходов.
Прогнозирование сроков продажи объекта недвижимости. В этом примере
мы возьмем вымышленный датасет, чтобы проиллюстрировать прозрачность.
Мы сосредоточимся на трех критериях, которые имеют особую ценность при
интерпретации научных и бизнес-данных:
Простота — чем проще решение с точки зрения кода и основного API
фреймворка, тем лучше.
Прозрачность — лучшим является решение, которое можно интерпретировать и легко объяснить стейкхолдерам.
Эффективность — решение, которое обеспечивает наилучшие результаты
и требует меньше времени на обучение и внедрение, является более предпочтительным. Кроме того, исследовательский интерес может привести к более
эффективным результатам по мере обнаружения новых подходов.
3.1. Прогнозирование цен на Airbnb в Нью-Йорке
Чтобы сравнить машинное и глубокое обучение с точки зрения простоты, сопоставим два решения для конкретной задачи классификации табличных данных:
прогнозирование того, будет ли объект недвижимости, предлагаемый в аренду на
платформе Airbnb в Нью-Йорке (NYC), иметь цену больше или меньше средней
цены в объявлениях Airbnb на этом рынке. Мы сравним:
машинное обучение — представлено решением, использующим XGBoost, популярный подход на основе градиента;
глубокое обучение — представлено решением, использующим функциональный API Keras.
Сравним сложность кода этих решений и посмотрим, что это скажет нам о простоте или сложности обоих подходов.
3.1.1. Датасет Airbnb в Нью-Йорке
Чтобы решить задачу прогнозирования того, будет ли объект на Airbnb NYC
иметь цену, которая больше или меньше средней цены, мы воспользуемся таб
личным датасетом с объявлениями Airbnb в Нью-Йорке. Рисунок 3.1 включает
описания столбцов датасета Airbnb NYC вместе с указанием типа данных для
каждого из них, а сам датасет, образец которого вы можете увидеть на рис. 3.2,
опубликован в Kaggle: https://mng.bz/avJ7.
3.1. Прогнозирование цен на Airbnb в Нью-Йорке 101
Рис. 3.1. Подробная информация о столбцах в датасете Airbnb NYC
Каждая строка в этом датасете содержит информацию об одном объявлении,
а каждый столбец содержит значения для всех объявлений по заданной характеристике.
Датасет Airbnb NYC обладает свойствами, которые делают его хорошим кандидатом для сравнения подходов к анализу табличных данных.
У него удобный размер, около 49 000 записей: он достаточно большой, чтобы
быть интересным, но не настолько огромный, чтобы для его обработки требовались специальные инструменты Big Data, такие как Spark.
Он имеет достаточное количество столбцов для сравнения машинного и глубокого обучения. Как мы узнаем из следующих глав, для датасетов, содержащих всего три-четыре столбца, предпочтение однозначно отдается
классическому ML. Наборы данных с сотнями столбцов исследовать труднее.
Airbnb NYC имеет как раз «правильное» количество столбцов — достаточное,
чтобы дать возможность глубокому обучению проявить себя, но не настолько
огромное, чтобы человек не смог разобраться в данных.
Поскольку датасет имеет приемлемое количество строк и столбцов, его
легко и быстро просмотреть в электронной таблице. Это означает, что
не нужно писать код Python каждый раз, когда у нас возникает вопрос
о данных. С помощью электронной таблицы можно быстро сортировать,
фильтровать, вычислять характеристики и использовать скрипты для Excel
или Google Sheets, чтобы провести более подробное исследование. Датасет
Airbnb NYC поддается анализу в электронной таблице, а это экономит
много сил.
102 Глава 3. Машинное и глубокое обучение
Рис. 3.2. Выборка строк из датасета Airbnb NYC
Датасет включает в себя ряд интересных типов данных, в том числе несколько
столбцов с непрерывными данными: minimum_nights (минимум ночей для
бронирования) — целые значения, price (цена) и reviews_per_month (отзы-
3.1. Прогнозирование цен на Airbnb в Нью-Йорке 103
вов за месяц) — значения с плавающей точкой, а latitude (географическая
широта) и longitude (географическая долгота) — геопространственные
значения; столбцы с категориальными данными: neighboring_group (особенности района), neighborhood (инфраструктура поблизости) и room_type
(тип помещения); и текстовые столбцы свободной формы: name (описание
объекта) и host_name (имя арендодателя).
В датасете есть недостатки, например в некоторых столбцах пропущены
значения, но он не настолько запутанный, чтобы требовалась масштабная
очистка, перед тем как его можно будет использовать для обучения модели.
Можно быстро создать приложение на основе такого датасета, не слишком
отвлекаясь на очистку.
Он свободно распространяется и основан на данных из реального бизнеса.
Как мы увидим в последующих главах книги, одной из проблем исследования
машинного и глубокого обучения с помощью табличных данных является
нехватка хороших датасетов в свободном доступе, которые представляют
реальные бизнес-проблемы. Датасет Airbnb является редким исключением.
При работе с этим датасетом целевая переменная очевидна: price. Модель
должна сделать предсказание, будет ли конкретный объект иметь цену выше
или ниже медианной для объектов в представленном наборе.
В этом подразделе мы познакомились с датасетом Airbnb NYC. В следующем
рассмотрим код для обучения модели.
3.1.2. Знакомство с кодом
Теперь, когда мы представили датасет Airbnb NYC, взглянем на код решения.
Мы не будем вдаваться во все его детали, но важно иметь представление о том,
как его отдельные части взаимодействуют друг с другом.
На рис. 3.3 показаны файлы в составе обоих решений.
Ниже приведена более подробная информация о файлах решения.
CSV-файл с входными данными (https://mng.bz/avJ7).
Код очистки данных (https://mng.bz/gawV). Обратите внимание, что, хотя
XGBoost и Keras используют один и тот же код очистки, XGBoost имеет
встроенные возможности, такие как обработка пропущенных значений, что
означает, что его код может быть проще.
Файл конфигурации очистки (https://mng.bz/ey7Q).
Файл конфигурации обучения для XGBoost (https://mng.bz/pKOz).
Файл конфигурации обучения для Keras (https://mng.bz/vKpr).
Код для обучения модели XGBoost (https://mng.bz/YDGA). Среди решений на
градиентном бустинге мы выбрали XGBoost, потому что он очень популярен,
104 Глава 3. Машинное и глубокое обучение
и в сети есть множество руководств по этому алгоритму на случай возможных проблем.
Код для обучения модели Keras (https://mng.bz/JYwP). Мы выбрали Keras для
иллюстрации подхода на основе глубокого обучения, потому что, в отличие
от альтернатив, таких как PyTorch или fastai поверх PyTorch, Keras, наряду
с сырым TensorFlow, наиболее часто используется в бизнес-приложениях. Мы
выбрали Keras вместо одной из табличных библиотек глубокого обучения,
представленных в разделе «Сравнение градиентного бустинга и глубокого
обучения», потому что Keras используется чаще, чем любая из них, и сравнение его API с API XGBoost больше похоже на сравнение яблок с яблоками,
чем сравнение библиотеки глубокого обучения, специально разработанной
для табличных данных, и общих возможностей XGBoost.
CSV-файл
с входными
данными
Обрабатывает пропущенные
и недопустимые значения
во входном наборе данных
и генерирует pickle-файл,
содержащий версию набора данных
с замененными пропущенными
и недопустимыми значениями
Содержит код
для определения
и обучения
модели XBoost
Блокнот
для обучения
XGBoost
Сырые данные из набора
Файл
конфигурации
очистки
Код очистки
данных
pickle-файл
очищенных
данных
Содержит настройки
параметров для кода
очистки данных
Выходной файл,
созданный кодом очистки данных
Файлы
конфигурации
обучения
Блокнот
для обучения
Keras
Содержит код
для определения
и обучения
модели Keras
Эти файлы конфигурации содержат
настройки параметров для определения
и обучения модели для каждого подхода
Рис. 3.3. Файлы решения для датасета Airbnb NYC
Для большего удобства решения XGBoost и Keras размещены в двух отдельных
папках, но бˆольшая часть кода в обоих решениях одинакова.
Код решения на основе XGBoost находится по адресу https://mng.bz/GeEO.
Код решения с использованием Keras находится по адресу https://mng.bz/zZ4Q.
Различия между этими двумя репозиториями ограничиваются кодами для обу
чения и файлами конфигурации обучения.
3.1. Прогнозирование цен на Airbnb в Нью-Йорке 105
3.1.3. Решение для глубокого обучения на Keras
Прежде чем углубляться в детали датасета Airbnb и используемого кода, давайте
рассмотрим решение на основе Keras в контексте его программного стека, который представлен на рис. 3.4.
Решение Airbnb NYC
Низкоуровневый
фреймворк
TensorFlow
Высокоуровневый
API
Keras
Пользовательский
код
Функция get_model()
определяет слои
модели Keras для
всех типов столбцов
Рис. 3.4. Стек Keras для
задачи прогнозирования
цен Airbnb NYC
В главе 8 мы более подробно рассмотрим слои стека, показанные на рис. 3.4.
На данный момент отметим, что Keras — это высокоуровневый API глубокого
обучения, который используется для решения задачи прогнозирования цен
на Airbnb на основе глубокого обучения, которую мы разберем в этой главе.
Существуют два низкоуровневых фреймворка глубокого обучения, и решение,
рассматриваемое в этой главе, основано на фреймворке TensorFlow, поскольку
именно на нем построен Keras.
3.1.4. Обучение моделей
Целью обоих решений является прогнозирование того, будет ли объявление на
Airbnb иметь цену выше или ниже средней цены во входном датасете. Для этого
обе модели обучаются на одном и том же наборе признаков. Их подмножество
определено в файле конфигурации для обучения модели: https:/mng.bz/OBoE.
Ниже приведена часть этого файла, в которой указаны признаки, используемые
для обучения моделей:
categorical: # столбцы с категориальными значениями
- 'neighbourhood_group'
- 'neighbourhood'
- 'room_type'
continuous: # столбцы с непрерывными значениями
- 'minimum_nights'
- 'number_of_reviews'
- 'reviews_per_month'
- 'calculated_host_listings_count'
106 Глава 3. Машинное и глубокое обучение
Файл конфигурации также содержит список признаков, которые явно исключены из процесса обучения:
excluded:
# столбцы, не используемые в качестве входящих признаков для обучения модели
- 'price'
- 'id'
- 'latitude'
- 'longitude'
- 'host_id'
- 'last_review'
- 'name'
- 'host_name'
- 'availability_365'
Причины, по которым эти столбцы не используются в качестве признаков для
обучения модели, следующие.
price (цена) не включен, поскольку это целевой признак для модели.
Два столбца с id не включены в качестве признаков, поскольку не несут
никакой информации о цене в объявлении и представляют собой просто
числовые идентификаторы, назначенные объявлениям и арендодателям.
Мы не используем longitude или latitude в качестве признаков, поскольку географическое положение объектов недвижимости уже закодировано
в neighborhood_group и neighborhood, которые используются для обучения
модели. Если бы у нас их не было, мы могли бы использовать значения
latitude и longitude (или полярные координаты, полученные из них: https://
mng.bz/0Q66) для кластеризации объявлений в соответствии с их местоположением или преобразования их в полярные координаты. Использование
сырых широты и долготы для каждого объявления в качестве признаков
может привести к переобучению, поскольку каждый объект недвижимости
будет иметь уникальное значение для пары (latitude, longitude).
name и host_name не используются, поскольку являются в некоторой степени
произвольными наборами токенов, которые позволяют читателям-людям
идентифицировать объявления. Интересным упражнением было бы включение host_name в качестве признака, чтобы посмотреть, предоставляет ли
он какой-либо сигнал, связанный с ценой на аренду объектов одного и того
же арендодателя.
Мы решили не включать availabiltiy_365 (доступность 365 дней в году)
в набор признаков, поскольку этот столбец сложно интерпретировать.
Мы рассмотрели признаки, которые предстоит использовать для обучения
модели, чтобы предсказать, будет ли объект недвижимости в объявлении на
Airbnb иметь цену выше или ниже средней. В следующем разделе сравним
3.1. Прогнозирование цен на Airbnb в Нью-Йорке 107
простоту кода для моделей градиентного бустинга и глубокого обучения для
этого датасета.
3.1.5. Сравнение градиентного бустинга
и глубокого обучения
Как мы уже упоминали ранее, два решения задачи Airbnb NYC отличаются
только в нескольких местах. На рис. 3.5 показана файловая структура решения
и выделены файлы, в которых есть отличия. Если решения различаются только
в указанных четырех файлах, как мы можем использовать этот пример для сравнения простоты глубокого обучения XGBoost и Keras? Таблица 3.1 содержит
критерии оценки сложности кода.
CSV-файл
с входными
данными
Код
очистки данных
Файл
конфигурации
очистки
pickle-файл
очищенных
данных
Блокнот
для обучения
XGBoost
Файлы
конфигурации
обучения
Блокнот
для обучения
Keras
Рис. 3.5. Файлы решений задачи Airbnb, различающиеся для XGBoost и Keras
Оценим код решения задачи Airbnb NYC по каждому из представленных критериев сложности.
Решение XGBoost содержит дополнительный код для подготовки данных. Исходное решение было написано с использованием глубокого обучения на Keras,
а затем на его основе было создано решение на XGBoost. Первоначальная модель
Keras требовала обучающих входных данных в виде списка массивов numpy.
XGBoost требует входных данных в виде массива списков1 numpy. Листинг 3.1
содержит код для XGBoost, который преобразует исходный формат данных
в формат, подходящий для этой модели.
1
Это просто двумерные массивы. — Примеч. науч. ред.
108 Глава 3. Машинное и глубокое обучение
Таблица 3.1. Сравнение сложности кода XGBoost и Keras
Критерий сложности кода
XGBoost
Модель глубокого обучения на Keras
Данные
(подготовка)
Блок кода, необходимый для преобразования списка массивов
numpy в массив списков numpy
Процесс подготовки данных был
специально разработан с учетом
глубокого обучения
Определение
модели
Одна команда по стандартам
scikit-learn
Блок кода, необходимый для задания
слоев модели, с отдельными слоями
для столбцов со значениями каждого
типа: непрерывными, категориальными, текстовыми
Обучение
модели
Одна команда по стандартам
scikit-learn
Блок кода, необходимый для управления обратными вызовами (колбэками): позволяет избегать лишних итераций обучения, которые не приводят
к улучшению модели, и сохранить
вариант, имеющий лучшую производительность.
Блок кода для определения обратного
вызова, необходимого для эффективного обучения модели Keras
Сохранение
модели
Одна команда по стандартам
scikit-learn
Является частью функции обратного
вызова сохранения модели
Загрузка
модели
Блок кода — требует установки последней версии XGBoost,
иначе загруженная модель выводит ошибку: AttributeError:
Одна команда
'XGBClassifier' object has no
attribute '_le'
Листинг 3.1. Код подготовки данных для XGBoost
list_of_lists_train = []
Определяет списки списков для обучающего и тестового
list_of_lists_test = []
датасетов (по одному на каждый признак)
for i in range(0,7):
list_of_lists_train.append(X_train_list[i].tolist())
list_of_lists_test.append(X_test_list[i].tolist())
# преобразует списки списков в массивы списков numpy
xgb_X_train = np.array(list_of_lists_train).T
Преобразует обучающий список
xgb_X_test = np.array(list_of_lists_test).T
Преобразует тестовый список
списков в массив списков numpy
списков в массив списков numpy
3.1. Прогнозирование цен на Airbnb в Нью-Йорке 109
Обратите внимание, что, хотя этот код для XGBoost немного сложнее кода для
Keras, он не является неотъемлемой частью XGBoost, а скорее необходим из-за
способа, которым решение на XGBoost было создано из решения на Keras.
Чтобы получить дополнительное объяснение того, что делает код подготовки
данных XGBoost, перейдите на сайт Gemini (https://gemini.google.com), вставьте
код в поле ввода вместе с промптом «what does this code do?» («что делает этот
код?») и отправьте его (рис. 3.6).
what does this code do?
list_of_lists_train = []
list_of_lists_test = []
for i in range(0,7):
#A
list_of_lists_train.append(X_train_list[i].tolist())
list_of_lists_test.append(X_test_list[i].tolist())
# convert lists of lists to numpy arrays of lists
Рис. 3.6. Ввод запроса на интерпретацию кода в Gemini
Вы получите ответ, в котором объясняется, что делает код1:
По сути, код делает следующее:
Он извлекает определенные элементы из
двух входных списков (X_train_list и X_test_list).
Он упорядочивает эти элементы в определенном формате (списки списков).
Он преобразует эти списки в массивы NumPy,
подготавливая данные для дальнейшей обработки или обучения модели.
Теперь, когда мы рассмотрели разницу между XGBoost и Keras с точки зрения
подготовки данных, давайте сравним код определения модели для двух решений.
Ниже приведено определение модели XGBoost для задачи Airbnb NYC — одна
строка кода в соответствии со стандартами scikit-learn:
model = XGBClassifier()
Давайте посмотрим, как выглядит код определения модели для решения на Keras.
На рис. 3.7 показано начало функции, которая определяет модель глубокого
обучения для рассматриваемой задачи.
На рис. 3.8 показана остальная часть функции, определяющей модель глубокого
обучения для задачи Airbnb NYC.
1
Это перевод ответа на английском языке, который приведен у автора. Вы можете задать
промпт на русском и получить объяснение с несколько другими формулировками. —
Примеч. ред.
110 Глава 3. Машинное и глубокое обучение
def get_model():
''' определяет модель Keras по типу значений в колонках
Returns:
model: модель Keras со слоями, специфичными для датасета
'''
catinputs = {} # словарь входных категориальных признаков
textinputs = {} # словарь входных текстовых признаков
continputs = {} # словарь входных непрерывных признаков
embeddings = {}
textembeddings = {}
catemb = 10 # размерность категориальных эмбеддингов
textemb = 50 # размерность текстовых эмбеддингов
Списки для
входных признаков
и вложений
collistfix = []
textlayerlist = []
inputlayerlist = []
i = 0
print("textmax это ",
textmax)
# определяем слои для столбцов с категориальными признаками
for col in collist:
catinputs[col] = Input(shape=[1],name=col)
Создание
inputlayerlist.append(catinputs[col])
слоев для
#print("inputname",inputname)
категориembeddings[col] = (Embedding(max_dict[col],catemb) (catinputs[col]))
альных
# batchnorm для всех слоев
признаков
embeddings[col] = (BatchNormalization() (embeddings[col]))
collistfix.append(embeddings[col])
# определение слоев для столбцов с текстовыми значениями
if includetext:
for col in textcols:
print("столбец ",col)
textinputs[col] = Input(shape=[X_train[col].shape[1]], name=col)
print("размерность текстовых входных данных ",X_train[col].shape[1])
Создание
inputlayerlist.append(textinputs[col])
слоев для
textembeddings[col] = (Embedding(textmax,textemb) (textinputs[col]))
текстовых
textembeddings[col] = (BatchNormalization() (textembeddings[col]))
признаtextembeddings[col] = Dropout(dropout_rate) ( GRU(16,kernel_
ков
regularizer=l2(l2_lambda))
(textembeddings[col]))
collistfix.append(textembeddings[col])
print("максимум в середине ",np.max([np.max(train[col].max()),
np.max(test[col].max())])+10)
print("через циклы для столбцов ")
Рис. 3.7. Определение модели глубокого обучения Keras для задачи Airbnb NYC (часть 1)
# определяем слои для столбцов с непрерывным типом данных
for col in continuouscols:
continputs[col] = Input(shape=[1], name=col)
inputlayerlist.append(continputs[col])
# создаем слои
main_l = concatenate([Dropout(dropout_rate) (Flatten()
(embeddings[collist[0]])
),Dropout(dropout_rate) (Flatten() (embeddings[collist[1]]) )])
for cols in collist:
Создание слоев
для непрерывных
значений
# определяем слои для столбцов с непрерывным типом данных
for col in continuouscols:
continputs[col] = Input(shape=[1], name=col)
inputlayerlist.append(continputs[col])
Создание слоев
для непрерывных
значений
3.1. Прогнозирование цен на Airbnb в Нью-Йорке 111
# создаем слои
main_l = concatenate([Dropout(dropout_rate) (Flatten()
(embeddings[collist[0]])
),Dropout(dropout_rate) (Flatten() (embeddings[collist[1]]) )])
for cols in collist:
if (cols != collist[0]) & (cols != collist[1]):
main_l = concatenate([main_l,Dropout(dropout_rate) (Flatten()
(embeddings[cols]) )])
if includetext:
for col in textcols:
main_l = concatenate([main_l,textembeddings[col]])
for col in continuouscols:
main_l = concatenate([main_l,continputs[col]])
# определяем выходной слой
output = Dense(1, activation=output_activation) (main_l)
# определяем модель
model = Model(inputlayerlist, output)
Определение
выходного
слоя
# определяем оптимизатор
optimizer = SGD(lr=learning_rate)
# компилируем модель
model.compile(loss=loss_func, optimizer=optimizer, metrics=["accuracy"], Компиляция
модели
weighted_metrics=["accuracy"])
return model
Рис. 3.8. Определение модели для решения на основе глубокого обучения Keras
для задачи Airbnb NYC (часть 2)
На рис. 3.9 показана визуализация модели для задачи Airbnb.
Контраст довольно резкий: модель глубокого обучения можно было бы определить проще. Данное определение модели задает отдельный набор слоев Keras
для каждого типа столбцов (непрерывные, категориальные и текстовые). Это не
самое лаконичное определение слоев модели для табличных данных, но очень
гибкое. Оно сможет работать с табличными датасетами, содержащими различные комбинации непрерывных, категориальных и текстовых столбцов. Кроме
того, это определение модели включает код, который задает слои для текстовых
столбцов, а мы их не брали для обучения модели, поэтому этот блок кода можно
было бы опустить. Тем не менее разница между простотой определения модели
XGBoost и сложностью определения модели на Keras подчеркивает преимущество первой: код определения модели проще на XGBoost, чем на Keras.
Мы сравнили код подготовки данных и код определения модели. Теперь давайте
сравним код обучения моделей на XGBoost и на Keras. В решении XGBoost обу
чение выполняется с помощью одной строки кода со значениями по умолчанию,
принятыми для всех параметров:
model.fit(xgb_X_train, dtrain.target)
112 Глава 3. Машинное и глубокое обучение
Входные
слои
Внутренние
слои
Выходной
слой
Рис. 3.9. Визуализация модели Keras для задачи Airbnb
В листинге 3.2 показано, что в Keras есть две различные версии оператора fit,
а также необходимо задать несколько дополнительных параметров, включая
размер батча и количество эпох, которое будет запущено в процессе обучения
по умолчанию.
Листинг 3.2. Обучение модели на Keras
if early_stop:
Метод fit с параметром, котоmodelfit = model.fit(X_train_list, dtrain.target,
рый задает список обратных
epochs=epochs, batch_size=batch_size
, validation_data=(X_valid_list, dvalid.target), вызовов для ранней остановки
и сохранения модели
verbose=1,callbacks=callback_list)
else:
modelfit = model.fit(X_train_list,
dtrain.target, epochs=epochs, batch_size=batch_size
Метод fit без списка
, validation_data=(X_valid_list,
обратных вызовов
dvalid.target), verbose=1)
Между XGBoost и Keras нет большой разницы в сложности обучающего кода.
Однако, чтобы сделать процесс обучения на Keras эффективным, нам нужно
использовать обратные вызовы: это позволит не проходить бесполезные циклы
обучения, получив в итоге неоптимальную модель. Подробнее об использовании
обратных вызовов на Keras для оптимизации процесса обучения см. в главе 6
книги «Deep Learning with Structured Data» (https://mng.bz/KGx0).
3.1. Прогнозирование цен на Airbnb в Нью-Йорке 113
Прежде чем рассмотреть функции обратного вызова для решения на Keras,
давайте посмотрим, что Gemini может нам рассказать о методах fit. Снова отправим этот код в Gemini, предварив его промптом «what does this code do?» («что
делает этот код?»). Если ответ вас не удовлетворил, нажмите Regenerate draft
(«Перегенерировать ответ»). Со второй попытки мы получили от Gemini подробное
описание кода, которое включало следующее описание параметров для метода fit1:
В обоих случаях модель обучается с помощью метода model.fit(), который является
распространенным способом обучения моделей в библиотеках машинного обучения,
таких как TensorFlow или Keras.
Ключевые аргументы, передаваемые в model.fit():
X_train_list: признаки обучающих данных.
dtrain.target: целевые переменные (метки) обучающих данных.
epochs: количество эпох (итераций обучения).
batch_size: размер батча (количество образцов данных на шаг обучения).
validation_data: данные для валидации для мониторинга производительности
(необязательно).
verbose: уровень детализации вывода в процессе обучения (1 для отображения
прогресс-бара).
callbacks: список функций обратных вызовов, выполняемых во время обучения
(только в случае ранней остановки)
Gemini также приводит следующее заключение:
Код обучает модель требуемым образом с ранней остановкой и без нее,
предотвращающей переобучение и улучшающей способность модели к обобщению.
Конкретная реализация ранней остановки зависит от содержимого callback_list.
Чтобы полностью понять его назначение, необходимо больше информации о модели,
процессе обучения и критериях ранней остановки.
Обратите внимание, как в последней строке Gemini уточняет ограничения своего
анализа, справедливо отмечая, что, имея только обучающий фрагмент, он не
может сделать вывод обо всех деталях полного решения.
В листинге 3.3 показан код, определяющий обратные вызовы, которые используются на этапе обучения модели на Keras. Этот код добавляет дополнительную
сложность в версию на Keras для Airbnb NYC.
Листинг 3.3. Функции обратного вызова для Keras
Определяет объект обратного вызова
callback_list = []
ранней остановки, указывая, что процесс
es = EarlyStopping(monitor=es_monitor,
обучения должен прерваться, как только
mode=es_mode, verbose=1,
производительность перестанет улучшаться
patience = patience_threshold)
callback_list.append(es)
Добавляет первый обратный вызов в список
model_path = get_model_path()
save_model_path =
os.path.join(model_path,'scmodel'+modifier+"_"+str(experiment_number)+'.h5')
mc = ModelCheckpoint(save_model_path,
1
См.
сноску на с.109. — Примеч. ред.
monitor=es_monitor, mode=es_mode,
verbose=1, save_best_only=True)
Определяет объект обратного вызова, сохраняющий
callback_list.append(mc)
модель, чтобы гарантировать, что обученная модель,
Добавляет второй обратный вызов в список
обратных вызовов, которые будут использоваться в процессе обучения
которая будет иметь оптимальную производительность
в течение всего обучающего цикла, будет сохранена при
выходе из режима обучения
Определяет объект обратного вызова
callback_list = []
ранней остановки, указывая, что процесс
es = EarlyStopping(monitor=es_monitor,
обучения должен прерваться, как только
mode=es_mode, verbose=1,
производительность перестанет улучшаться
114
Глава
3.
Машинное
и
глубокое
обучение
patience = patience_threshold)
callback_list.append(es)
Добавляет первый обратный вызов в список
model_path = get_model_path()
save_model_path =
os.path.join(model_path,'scmodel'+modifier+"_"+str(experiment_number)+'.h5')
mc = ModelCheckpoint(save_model_path,
monitor=es_monitor, mode=es_mode,
verbose=1, save_best_only=True)
Определяет объект обратного вызова, сохраняющий
callback_list.append(mc)
модель, чтобы гарантировать, что обученная модель,
Добавляет второй обратный вызов в список
обратных вызовов, которые будут использоваться в процессе обучения
которая будет иметь оптимальную производительность
в течение всего обучающего цикла, будет сохранена при
выходе из режима обучения
После того как мы обучили модель, следует сохранить ее в файл, чтобы иметь
возможность загрузить ее и использовать в другом сеансе или в процессе развертывания модели. В нашем простом примере мы сохраняем и перезагружаем
модель в том же блокноте, который используем для обучения модели. Метод
сохранения модели для XGBoost:
model.save_model(xgb_save_model_path)
Для Keras нам не нужен явный метод сохранения модели, поскольку она сохраняется автоматически с помощью обратного вызова для сохранения.
В листинге 3.4 показан код для загрузки модели в XGBoost.
Листинг 3.4. Загрузка модели в XGBoost
loaded_saved_model = xgb.XGBClassifier()
loaded_saved_model.load_model(xgb_save_model_path)
Загружает новый объект классификатора
XGBoost с моделью, сохраненной
с помощью метода save_model
Определяет новый объект
классификатора XGBoost
Метод загрузки модели для Keras:
saved_model = load_model(save_model_path)
Еще одно отличие между XGBoost и Keras заключается в том, что, когда вы попытаетесь загрузить сохраненную модель классификатора XGBoost (например,
ту, которую мы обучили для задачи Airbnb NYC) и запустить процесс прогнозирования, возникнет ошибка, если используется не самая последняя версия
XGBoost. Чтобы избежать этого, код обучения модели XGBoost включает следующую инструкцию, гарантирующую использование последней версии XGBoost:
!pip install --upgrade XGBoost
В этом разделе мы сравнили простоту кода XGBoost и Keras для подготовки
данных, а также определения, обучения и сохранения модели. Далее обсудим
выводы, которые можно сделать из этого сравнения.
3.2. Прозрачность 115
3.1.6. Выводы
Один из лучших советов для проектов data science — использовать максимально
простой подход. Примените бритву Оккама1. Если существует более одного способа решить задачу, выберите самый простой. Если линейная регрессия решит
проблему, то зачем использовать метод опорных векторов (SVM)? Если традиционный подход к программированию решит проблему, то зачем вообще использовать
машинное обучение? Если вы выберете максимально простой подход, то, скорее
всего, быстрее получите начальные результаты, быстрее завершите разработку
всего решения и вам будет легче сопровождать систему после ее развертывания.
Чтобы ответить на вопрос о том, что проще: классическое машинное или глубокое обучение, мы сопоставили решения, использующие каждый подход,
к конкретной задаче — прогнозирование цен для Airbnb NYC. Отвечая на этот
вопрос, в задачах анализа табличных данных мы советуем руководствоваться
принципом «не усложняй».
Прежде чем завершить сравнение простоты кода для градиентного бустинга
и глубокого обучения, необходимо отметить, что Keras — не единственный подход к глубокому обучению. Существуют и другие, работающие с табличными
данными, и некоторые из них предлагают более простой код для определения
и обучения моделей, чем Keras. Например, с помощью фреймворка fastai (https://
docs.fast.ai/), который более подробно будет рассмотрен в главе 9, вы можете определить модель для работы с табличными данными, обучить ее и использовать для
получения прогнозов, написав менее 10 строк кода. Готовые оценщики Tensorflow
(tensorflow canned estimators) (https://mng.bz/9Y51) — еще один простой способ
глубокого обучения на табличных данных. С их помощью вы можете обучить
модель на табличном датасете и получать прогнозы с помощью API, столь же
простого, как API XGBoost. Это всего лишь два примера. Преимущество Keras
заключается в его высокой гибкости, и именно это одна из причин, по которой
компании в продакшене часто используют его, в то время как более простые
инструменты, такие как fastai, встречаются редко.
Теперь, когда мы сравнили машинное и глубокое обучение с точки зрения простоты кода, в следующем разделе сравним два подхода с точки зрения прозрачности.
3.2. Прозрачность
При сравнении методов градиентного бустинга с глубоким обучением важны два
критерия прозрачности: объяснимость (то есть насколько легко объяснить, как
работает модель) и важность признаков (то есть насколько легко определить,
1
Принцип, который в кратком виде гласит: «Не следует множить сущее без необходимости». — Примеч. ред.
116 Глава 3. Машинное и глубокое обучение
какой признак оказывает наибольшее влияние на результат). В этом разделе мы
сравним градиентный бустинг с глубоким обучением по этим двум критериям.
Чтобы сопоставить степень объяснимости моделей машинного и глубокого обу
чения, мы рассмотрим простой, вымышленный датасет и способы объяснения
обученных на нем моделей.
Датасет, который будем использовать, содержит информацию о сроках продажи
домов на конкретном рынке недвижимости (табл. 3.2).
Таблица 3.2. Датасет о сроках продажи домов на рынке
Время на рынке (недели)
Город
Запрашиваемая
цена (тыс. $)
Расстояние до
транспортного узла (км)
6
Китченер
600
10
5
Ватерлоо
700
5
12
Китченер
900
20
6
Ватерлоо
700
15
1
Ватерлоо
500
5
4
Ватерлоо
600
5
8
Ватерлоо
750
5
2
Китченер
500
5
9
Китченер
1000
5
4
Ватерлоо
750
10
Мы вернемся к датасету Airbnb в следующем разделе. На текущий момент нам
нужен этот, чтобы легко и просто проиллюстрировать объяснимость.
Таблица включает информацию о городе, в котором расположен дом, запрашивае
мую цену и расстояние до ближайшего транспортного узла, а также количество
недель, в течение которых объект был выставлен на продажу до осуществления
сделки. С помощью этого набора данных мы хотим обучить модель, которая
сможет предсказать, будет ли дом продан в течение месяца или нет.
3.2.1. Объяснимость
Предположим, что мы хотим дать неспециалисту, бизнес-аудитории, представление о том, как модель дерева решений может быть использована для решения
задачи (определить срок продажи для этого датасета). Мы могли бы создать
3.2. Прозрачность 117
схему, подобную той, что показана на рис. 3.10, чтобы дать приблизительное
представление о том, как работает такое дерево решений.
Запрашиваемая цена
<700 тыс. $
>=700 тыс. $
Город
Город
Китченер
Ватерлоо
1 объект в течение месяца
Продажа
Sell
within a
и 1 более месяца
в течение
месяца
month
Ватерлоо
Китченер
Срок продажи
более месяца
Расстояние
до транспортного узла
<15 км
2 объекта в течение месяца
и 1 более месяца
>=15 км
Срок >продажи
a month
более месяца
to sell
Рис. 3.10. Схема дерева решений
Обратите внимание, что иллюстрация не содержит никакого айтишного сленга
и не предполагает, что читатель имеет какой-либо опыт в машинном обучении.
Суть дерева решений очевидна. Заметим, что это является грубым упрощением
и существуют важные технические различия между простым деревом решений,
таким как это, и моделью, которая использует градиентный бустинг. В XGBoost, например, применяется несколько деревьев, поэтому наша иллюстрация сама по себе
не будет достаточной. Тем не менее она показывает, что некоторые классические
алгоритмы машинного обучения интуитивно понятны и их работу можно объяснить даже неспециалистам, не заставляя их вникать в технические подробности.
Что, если бы мы хотели дать неспециализированной аудитории общее представление о том, как ту же задачу будет решать модель глубокого обучения? Мы могли
бы начать с общей схемы нейронной сети, подобной той, что показана на рис. 3.11.
Входной слой
Выходной слой
Связи
Скрытые слои
Рис. 3.11.
Архитектура
нейросети
118 Глава 3. Машинное и глубокое обучение
Такая иллюстрация может помочь объяснить «глубину» в «глубоком обучении»,
но она не дает никакого представления о том, как на самом деле обучается модель.
Поможет ли нам увеличение масштаба, чтобы показать, как работает отдельный
узел в сети (рис. 3.12)?
Скрытый/выходной узел
Веса
Сигмоида
Входные
значения
Выход
Смещение
Рис. 3.12. Простая иллюстрация узла нейронной сети
Большинству неспециалистов будет трудно интерпретировать этот рисунок.
Если увеличение деталей нейронной сети не дает лучшей объяснимости, что,
если мы возьмем другой подход и положимся на аналогию между нейронными
сетями и биологическими нейронами (рис. 3.13)? Эта иллюстрация пытается
связать общую нейронную сеть с работой отдельного узла в сети (нейрона),
а затем провести аналогию между ним и биологическим нейроном.
Нейронная сеть
Входной
слой
Связи
Скрытые
слои
Выходной
слой
Дендриты
Скрытый/выходной узел
Аксон
Нейрон нейронной сети
Ядро
Биологический нейрон
Рис. 3.13. Сравнение искусственной нейронной сети с биологическим нейроном
3.2. Прозрачность 119
Обращение к биологической аналогии поднимает две проблемы. Во-первых,
сама аналогия спорна. Некоторые эксперты в отрасли считают, что нейронные
сети работают совсем не так, как биологические нейроны (например, https://
mng.bz/jp2P). Даже если вы признаете, что аналогия между нейронными сетями
и биологическими нейронами верна, ее использование для объяснения системы
глубокого обучения может привести к серьезному недопониманию, если неспециалисты сделают вывод о том, что простые системы глубокого обучения
обладают возможностями как у человеческого мозга. Во-вторых, аналогия на
самом деле не проясняет, как обучается модель глубокого обучения. Большинство
людей знают, что такое биологический нейрон, но они не знают, как на самом
деле он работает. Аналогия бесполезна, если то, с чем она проводится, само по
себе является загадкой. В целом аналогия между узлами нейронной сети и биологическими нейронами в лучшем случае вызовет любопытство, но не поможет
неспециалисту понять, как на самом деле работает нейросеть.
Какой вывод можно сделать из этого примера? Хотя миллионы людей уже достаточно много узнали о глубоком обучении, чтобы оценить его возможности, оно
все еще представляет серьезные трудности для объяснения бизнес-аудитории.
В отличие от деревьев решений, основы которых можно легко проиллюстрировать, глубокое обучение не поддается объяснению с помощью одной простой
картинки. Даже сегодня, с доступными фреймворками глубокого обучения,
такими как fastai, и сотнями бесплатных онлайн-ресурсов, людям с хорошей
базой в линейной алгебре, математическом анализе и программировании требуется длительное время, чтобы получить прочное интуитивное представление
о том, как работает глубокое обучение. Мы отдаем себе отчет, что невозможно
передать это знание неспециалистам с помощью одной простой картинки, не
говоря уже о создании доступного объяснения того, как нейросеть будет работать
с конкретным датасетом.
3.2.2. Важность признаков
В предыдущем разделе мы сравнили подходы машинного и глубокого обучения
по одному критерию прозрачности: легко ли объяснить выбранный подход неспециалистам? В этом разделе рассмотрим следующий критерий: легко ли определить важность конкретного признака для производительности модели в целом?
Возвращаясь к примеру с Airbnb NYC, видим, что решение XGBoost использует
встроенный API для определения важности признаков:
model.feature_importances_
XGBoost предлагает несколько различных вариантов расчета важности признака. Метод по умолчанию — gain (прирост), который рассчитывает средний
прирост по всем разделениям, в которых используется признак. Прирост означает степень, с которой признак разделяет входные примеры (в нашем случае
120 Глава 3. Машинное и глубокое обучение
сдаваемые в аренду объекты на Airbnb) в соответствии с целевой переменной
(в нашем случае, является ли цена объекта выше или ниже медианной).
В примере Airbnb NYC вывод для этого API показывает значение прироста для
каждого из признаков. Третий признак, используемый для обучения модели
(room_type), оказывает наибольшее влияние на результат, потом следует первый
признак (neighborhood_group):
array([0.10064548, 0.0438753, 0.7586573, 0.01957352, 0.02225152,
0.01597736, 0.03901952], dtype=float32)
Мы можем отобразить значения важности признаков в виде диаграммы, используя следующее выражение:
plt.barh(np.array(final_features), model.feature_importances_)
Результатом работы этого метода является диаграмма, показывающая относительную важность каждого из признаков (рис. 3.14).
calculated_host_listings_count
reviews_per_month
number_of_reviews
minimum_nights
room_type
neighbourhood
neighbourhood_group
0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
Рис. 3.14. Важность признаков для задачи Airbnb NYC, рассчитанная XGBoost
Эта диаграмма наглядно демонстрирует, что, согласно встроенному методу
XGBoost для определения важности признаков, room_type является наиболее
важным из них, neighbor_group находится на втором месте с большим отрывом,
а все остальные относительно не важны для поведения модели.
Значения для функции room_type следующие:
entire home/apartment (отдельный дом/квартира);
private room (отдельная комната);
shared room (комната на несколько человек).
3.3. Эффективность 121
Интуитивно понятно, что room_type будет оказывать значительное влияние на
цену объекта аренды. Цены аренды целого дома и комнаты на несколько человек
будут сильно различаться.
Теперь, когда мы рассмотрели, как можно получить важность признаков для
модели XGBoost, давайте узнаем, как сделать это же для Keras. В отличие от
XGBoost, Keras (и фреймворки глубокого обучения в целом) не имеет встроенного метода определения важности признаков. Однако можно применить внешние. Например, использовать утилиту lime (https://github.com/marcotcr/lime) или
shap (https://github.com/slundberg/shap). Рассмотрение этих подходов выходит за
рамки данной главы. Сейчас мы лишь отметим, что с помощью XGBoost можно
получить базовое представление о важности признаков, используя лишь пару
строк кода, в то время как у фреймворков глубокого обучения вроде Keras нет
такого простого способа.
3.2.3. Выводы
В этом разделе мы сравнили машинное и глубокое обучение по следующим двум
критериям прозрачности.
Объяснимость — насколько легко объяснить, как работает модель, особенно
бизнес-стейкхолдерам или другим людям, которые не являются специалистами в data science? Все они будут больше доверять модели, которую
можно понять интуитивно, а не той, которая кажется черным ящиком. Что
еще более важно, для регулируемых отраслей, таких как автострахование,
прозрачность не сводится к простому успокоению стейкхолдеров доступными абстракциями о работе модели. Регуляторы в таких отраслях требуют
предоставить подробные и понятные объяснения того, как работают модели
и как меняется их поведение по мере развертывания новых версий.
Важность признаков — насколько легко определить, какие признаки оказывают наибольшее влияние на поведение модели?
Мы увидели, что модели машинного обучения проще объяснить, чем нейросети,
и что XGBoost предоставляет встроенный API определения важности признаков,
в то время как Keras не имеет такого преимущества. Теперь, когда мы сравнили
машинное и глубокое обучение с точки зрения прозрачности, в следующем разделе сопоставим два подхода с точки зрения их эффективности.
3.3. Эффективность
Мы сравнили машинное и глубокое обучение с точки зрения простоты и прозрачности. Теперь давайте сопоставим эти два подхода с точки зрения эффективности. Рассмотрим два аспекта.
122 Глава 3. Машинное и глубокое обучение
Производительность — вернемся к примеру Airbnb NYC, чтобы сравнить относительную производительность версии приложения на XGBoost с версией
приложения на Keras. В задаче Airbnb NYC мы обучаем модель предсказывать, будет ли новое предложение об аренде иметь цену выше или ниже средней цены. Мы сравним точность прогнозов, полученных каждым подходом,
и время, необходимое для запуска кода для каждого из них.
Исследования — сравним объем исследований, которые поддерживают и оспаривают целесообразность применения глубокого обучения к табличным
данным.
3.3.1. Оценка производительности
Сначала рассмотрим производительность версий на XGBoost и Keras для Airbnb
NYC. Сравним результаты, которые мы получаем «из коробки» для каждого
подхода.
После обучения модели XGBoost мы можем получить долю верных результатов (accuracy) обученной модели на тестовом датасете с помощью следующих
методов:
y_pred = model.predict(xgb_X_test)
xgb_predictions = [round(value) for value in y_pred]
xgb_accuracy = accuracy_score(test.target, xgb_predictions)
print("Accuracy: %.2f%%" % (xgb_accuracy * 100.0))
Для конкретного прогона кода получаем следующий результат:
Accuracy: 79.24%
Повторно запуская код, получаем долю верных результатов от 79 до 81 % с исходными настройками параметров в файле конфигурации для обучения (https://
mng.bz/pKOz). Время, затраченное на запуск кода, составляет от 3 до 4 секунд
в базовой среде Colab.
Для модели Keras с исходными настройками параметров из файла конфигурации
обучения (https:/mng.bz/vKpr), запущенной в стандартной среде Colab, модель
имеет следующие ключевые характеристики производительности.
Доля верных результатов — от 80 до 81 %.
Требуемое время для выполнения кода — от 10 до 15 секунд. Это без использования GPU на Colab. В качестве упражнения вы можете попробовать
запустить учебный код Keras с GPU и без него на Colab и сравнить время,
необходимое для выполнения.
Если сравнить производительность на XGBoost и Keras для задачи Airbnb NYC,
то первый вариант лучше с точки зрения скорости обучения. Этот пример не
3.4. Если копнуть глубже 123
раскрывает всех нюансов, и в последующих главах мы увидим, что при некотором
терпении и настройке решение для глубокого обучения может конкурировать
и даже в некоторых случаях превосходить производительность XGBoost. Цель
этого простого сравнения — продемонстрировать, что XGBoost обеспечивает
хорошую производительность «из коробки», без необходимости делать много
настроек и доработок.
3.4. Если копнуть глубже
В цели этой книги не входит подробный обзор всех последних исследований,
выступающих за и против глубокого обучения на табличных данных, но в этом
разделе мы более подробно рассмотрим существующие публикации и попытаемся получить представление о том, какая сторона «выигрывает» в научном споре.
Статья «A Short Chronology of Deep Learning for Tabular Data» (https://mng.bz/
W2x1) представляет собой прекрасный обзор последних научных работ и является хорошей отправной точкой для более детального рассмотрения глубокого
обучения на табличных данных.
Ниже приведен список некоторых недавних исследований, посвященных глубокому обучению на табличных данных. По сравнению с тысячами опубликованных
научных работ по глубокому обучению на нетабличных данных, включая текст
и изображения, глубокому обучению на таблицах посвящена лишь малая часть.
Помимо обоснования применения глубокого обучения на табличных данных,
следующие научные работы также представляют библиотеки, предназначенные
для этой цели. Они предлагают удобные способы применения глубокого обу
чения к табличным наборам данных. Мы воспользуемся некоторыми из них
в главе 8, когда рассмотрим дополнительные примеры и альтернативы подходу
на основе Keras, который мы использовали в этой главе для задачи Airbnb NYC.
SAINT: «Improved Neural Networks for Tabular Data via Row Attention and
Contrastive Pre-Training» (https://arxiv.org/abs/2106.01342). Мы рассмотрим
этот фреймворк более подробно в следующей главе.
TabNet: «Attentive Interpretable Tabular Learning» (https://arxiv.org/abs/
1908.07442). В этой статье представлен еще один фреймворк для глубокого
обучения на табличных данных, который мы рассмотрим более подробно
в следующей главе.
PyTorch Tabular: «A Framework for Deep Learning with Tabular Data» (https://
arxiv.org/abs/2104.13638). Статья знакомит с библиотекой на основе PyTorch,
и к ней мы тоже вернемся в следующей главе.
fastai: «A Layered API for Deep Learning» (https://arxiv.org/abs/2002.04688).
Здесь речь идет о fastai, высокоуровневом фреймворке, построенном на основе PyTorch. Он включает явную поддержку табличных данных.
124 Глава 3. Машинное и глубокое обучение
«Deep Tables» (https://deeptables.readthedocs.io/en/latest/).
DANets: «Deep Abstract Networks for Tabular Data Classification and Regression» (https://arxiv.org/abs/2112.02962).
В каждой из этих статей есть код, который можно использовать для проверки
результатов исследования и, что еще важнее, определения надежности библио
тек. Если мы хотим задействовать какую-то из них для решения реальных проблем, нужно оценить, просты ли библиотеки в применении и работают ли они
с используемыми фреймворками глубокого обучения. На рис. 3.15 показана
относительная популярность некоторых из этих библиотек, рассчитанная на
основе количества цитирований соответствующих статей, а также количества
звезд, полученных репозиториями.
Важно учитывать популярность библиотеки. Если она широко распространена,
то, скорее всего, будет работать в различных средах. Хотя нельзя дать абсолютную гарантию: как мы увидим в главе 8, некоторые библиотеки не работают,
например, в Colab, что затрудняет их оценку. Кроме того, если вы применяете
библиотеку, которую используют сотни или тысячи других ML-специалистов,
вы, скорее всего, найдете готовые ответы на возникающие вопросы. Если вы
один из немногих пользователей библиотеки, то можете оказаться первым, кто
столкнулся с определенной проблемой, и вам придется потратить время на ее
отладку, вместо того чтобы просто поискать на Stack Overflow.
Критики использования глубокого обучения в задачах анализа табличных
данных представили исследования, подтверждающие их правоту, в том числе
следующие статьи:
«Why Do Tree-Based Models Still Outperform Deep Learning on Tabular Data?»
(https://arxiv.org/abs/2207.08815);
«Tabular Data: Deep Learning Is Not All You Need» (https://arxiv.org/abs/
2106.03253);
«Deep Neural Networks and Tabular Data: A Survey» (https://arxiv.org/abs/
2110.01889).
Этот список, как и предыдущий со статьями, поддерживающими применение
глубокого обучения, ни в коем случае не является исчерпывающим. Однако
справедливо будет отметить, что опубликовано больше исследований, которые
выступают за использование глубокого обучения для табличных данных, чем
тех, которые выступают против.
Какая доля всех опубликованных исследовательских работ по глубокому обу
чению посвящена табличным данным? Трудно получить точный ответ, но
3.4. Если копнуть глубже 125
рассмотрим рис. 3.16, где показано количество публикаций по глубокому обу
чению за полтора десятилетия, предшествовавших 2018 году.
Эти цифры показывают, что в последнее время ежегодно публикуются десятки
тысяч статей по глубокому обучению, при этом менее ста статей по теме применения глубокого обучения для табличных данных.
Число цитирований соответствующих статей
Количество звезд у соответствующего репозитория
Названия библиотек приводятся как в оригинале
Количество звезд
у соответствующего
репозитория
Число цитирований
соответствующих статей
Рис. 3.15. Популярность библиотек для глубокого обучения на табличных данных
126 Глава 3. Машинное и глубокое обучение
Число публикаций
Тысячи
Google
Scopus
Рис. 3.16. Количество опубликованных статей по глубокому обучению по годам.
Количество статей было получено из результатов поиска в Scopus и Google Scholar
по запросу «deep learning»
Один из способов получить представление о том, какая доля исследований
глубокого обучения связана с табличными данными, — это выполнить поиск
в Google Scholar (https://scholar.google.com/). Рассмотрим количество результатов
поиска в Google Scholar, которые соответствуют следующим критериям1:
deep learning (глубокое обучение): ~1.6 миллиона;
deep learning вместе с tabular data (табличные данные) или structured data
(структурированные данные) и без учета graph-structured (графы): ~34 500;
deep neural networks (глубокие нейронные сети): ~530 000;
deep neural networks вместе с tabular data или structured data и без учета
graph-structured и deep learning: ~1500.
Другой способ определить, какая доля исследований глубокого обучения связана с табличными данными, — это выполнить поиск на arXiv (https://arxiv.org/).
Рассмотрим количество статей на arXiv, которые соответствуют следующим
критериям поиска:
deep learning в названии: ~32 000;
deep learning вместе с tabular data или structured data: ~200;
deep neural networks в названии: ~17 500;
deep neural networks вместе с tabular data или structured data: ~11.
1
Данные приведены для поисковых запросов на английском языке. — Примеч. ред.
3.4. Если копнуть глубже 127
Из результатов поиска в Google Scholar и arXiv становится ясно, что лишь незначительная часть опубликованных исследований в области глубокого обучения
касается табличных данных.
Подводя итог, можно сделать следующие выводы.
Публикаций, поддерживающих использование глубокого обучения для
табличных данных, больше, чем выступающих против.
В некоторых статьях приводятся библиотеки, с помощью которых реализуется описываемый подход. До сих пор ни одна из них не стала явным
фаворитом у специалистов, которые интересуются проблемой применения
глубокого обучения для анализа табличных данных.
Из всех исследований, проведенных в области глубокого обучения, работы,
посвященные табличным данным, составляют лишь малую часть.
Становится ясно, что ни машинное, ни глубокое обучение не являются однозначным победителем. В дискуссии о применимости глубокого обучения к табличным данным исследовательское сообщество еще не пришло к единому мнению.
Мы сравнили машинное и глубокое обучение по трем критериям: простота,
прозрачность и эффективность. Результаты по каждому из них представлены
на рис. 3.17.
Критерий
Машинное обучение
Глубокое обучение
Простота
Более простой код для:
• определения модели;
• обучения модели на табличном
датасете;
• использования обученной модели
для составления прогнозов
Более простой код для сохранения
обученной модели и ее повторной
загрузки
Прозрачность
• Легче объяснить неспециалистам;
• встроенные методы определения
важности признаков
Эффективность • Лучшая производительность модели
«из коробки»
• Большая гибкость;
• больше внимания со стороны
исследователей
В исследованиях, связанных с табличными данными, ни машинное,
ни глубокое обучение не имеют решающих преимуществ
Рис. 3.17. Сравнение применения машинного и глубокого обучения для табличных
данных
128 Глава 3. Машинное и глубокое обучение
На этом мы завершаем сравнение машинного и глубокого обучения. В следую
щей главе мы выйдем за рамки простого примера с Airbnb NYC на XGBoost
и углубимся в детали машинного обучения на табличных данных.
Итоги
Есть три критерия, которые можно использовать для сравнения машинного
и глубокого обучения: простота, прозрачность и эффективность.
Сравнение простоты кода дает нам представление о том, какое решение изначально проще создать и в долгосрочной перспективе легче сопровождать.
Прозрачность решения включает в себя то, насколько легко объяснить поведение модели неспециалистам и оценить относительную важность признаков,
используемых для обучения модели.
Эффективность включает в себя успешность каждого подхода в коммерческих приложениях и исследованиях.
Если сравнивать машинное и глубокое обучение с точки зрения простоты
кода, то преимущество за первым.
Если сравнивать машинное и глубокое обучение с точки зрения прозрачности, то первенство снова за машинным обучением.
Если сравнивать машинное и глубокое обучение с точки зрения эффективности, то их обоих можно назвать решениями «из коробки», хотя дополнительная настройка может выявить более существенные различия между
подходами.
Пока еще не ясно, надо ли учитывать другие критерии, например успешность
применения в соревнованиях Kaggle, в бизнесе и исследованиях.
Часть 2
Машинное обучение
и градиентный бустинг
для табличных данных
Главы 4, 5 и 6 дают полный обзор классических алгоритмов машинного обучения
и помогают освоить передовые методы градиентного бустинга, такие как XGBoost
и LightGBM. Вы узнаете, как использовать каждый алгоритм и применять его
к подходящим табличным данным. Глава 7 поможет вам закрепить полученные
знания на практическом примере, демонстрирующем полный процесс анализа
с использованием табличных данных.
В частности, глава 4 знакомит со scikit-learn и различными классическими методами машинного обучения, такими как линейная и логистическая регрессия
и обобщенные линейные модели. Вы поймете, как работает пайплайн данных
с практической точки зрения, и научитесь проверять результаты различных моделей и сравнивать их между собой. Затем мы перейдем к главе 5 и рассмотрим
деревья решений и их ансамбли, включая бэггинг, случайные леса и деревья
решений с градиентным бустингом. Затем подробно разберем алгоритм градиентного бустинга и его работу с табличными данными. Глава 5 завершается
обзором различных реализаций, от scikit-learn до XGBoost и LightGBM.
Глава 6 научит вас применять лучшие практики использования табличных данных в машинном обучении, в частности градиентный бустинг. Мы рассмотрим
множество передовых методов для создания наилучшего набора признаков,
оптимизации гиперпараметров модели и повышения ее производительности. Мы
также обсудим, как работать с пропущенными данными и как правильно преобразовывать категориальные. Часть 2 завершается главой 7 — иллюстративной,
которая представляет собой полный пример применения градиентного бустинга
к реальным данным. Пройдя все эти шаги, вы освоите лучшие практики и методики, обсуждавшиеся ранее. После этого вы будете готовы сравнить все эти
классические и продвинутые методы с подходами глубокого обучения, которые
находятся в центре внимания следующей, заключительной части.
4
Классические алгоритмы
для табличных данных
В этой главе
3 Что представляет собой библиотека scikit-learn
3 Изучение и обработка признаков датасета Airbnb NYC
3 Как работают классические методы машинного обучения
В зависимости от поставленной задачи классические алгоритмы машинного
обучения часто оказываются наиболее практичным подходом для работы с таб
личными данными. Благодаря десятилетиям исследований и практики эти
инструменты и алгоритмы предлагают богатый выбор проверенных решений.
В этой главе мы рассмотрим основные алгоритмы классического машинного обу
чения для прогнозирования на основе табличных данных. Мы сосредоточимся
на линейных моделях, поскольку они по-прежнему широко используются как
в качестве базовых моделей (baseline), так и в продакшене. Кроме того, обсуждение этих моделей помогает сформировать концептуальную основу, которая
будет полезна для понимания архитектур глубокого обучения и более продвинутых ML-алгоритмов, таких как градиентный бустинг на деревьях решений
(что станет одной из тем следующей главы).
Мы также дадим краткое введение в scikit-learn — мощную и универсальную
библиотеку машинного обучения, которую мы будем использовать для дальнейшего изучения датасета Airbnb в Нью-Йорке. Мы воздержимся от длинных
4.1. Знакомство с scikit-learn 131
математических определений и подробностей из учебников в пользу примеров
и практических рекомендаций по применению этих моделей к задачам табличных данных.
4.1. Знакомство с scikit-learn
Scikit-learn — это библиотека с открытым исходным кодом для классического
машинного обучения. Она появилась в 2007 году как проект Google Summer
of Code Давида Курнапо (David Cournapeau), а затем стала частью SciKits
(сокращение от Scipy Toolkits: https://projects.scipy.org/scikits.html), пока INRIA
(Institut National de Recherche en Informatique et en Automatique, Национальный
институт исследований в информатике и автоматике) и его фонд не взяли на
себя руководство проектом и его разработкой. Приведем краткий обзор того,
как scikit-learn может быстро решать большинство задач машинного обучения.
Итак, в нашем новом примере:
1) мы создадим синтетический датасет для задачи классификации с бинарным
сбалансированным целевым признаком, где половина меток положительные,
а половина отрицательные;
2) создадим пайплайн, стандартизирующий признаки и передающий их в модель логистической регрессии — один из самых простых и эффективных
статистических алгоритмов для задач классификации;
3) проведем оценку его эффективности с помощью кросс-валидации;
4) наконец, получив хорошие результаты на кросс-валидации, обучим модель
на всех доступных данных.
В листинге 4.1 показан полный код и большинство функций, предлагаемых
scikit-learn для простой задачи классификации на основе синтетически сгенерированных данных. После их создания мы определяем пайплайн, объединяющий
статистическую стандартизацию признаков с базовой моделью (логистической
регрессией) для классификации. Сначала все отправляется в функцию, которая
автоматически оценивает качество прогнозов (accuracy, долю верных результатов). Убедившись в удовлетворительности результатов, мы обучаем модель на
всех доступных данных для получения финального алгоритма.
Листинг 4.1. Пример использования библиотеки scikit-learn для задачи
классификации
import numpy as np
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_validate
from sklearn.pipeline import Pipeline
X, y = make_classification(n_features=32,
n_redundant=0,
n_informative=24,
random_state=1,
n_clusters_per_class=1
Создает синтетический датасет
с заданными характеристиками
import numpy as np
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
132
Глава 4. Классическиеimport
алгоритмы
для табличных данных
from
sklearn.model_selection
cross_validate
from sklearn.pipeline import Pipeline
X, y = make_classification(n_features=32,
n_redundant=0,
n_informative=24,
random_state=1,
n_clusters_per_class=1
)
model = LogisticRegression()
Создает синтетический датасет
с заданными характеристиками
Создает экземпляр класса LogisticRegression
pipeline = Pipeline(
[('processing', StandardScaler()),
('modeling', model)])
Создает пайплайн, который последовательно
применяет стандартизацию и модель
логистической регрессии
cv_scores = cross_validate(estimator=pipeline,
X=X,
y=y,
scoring="accuracy",
cv=5)
Выполняет 5-фолдовую кроссвалидацию с использованием
заданного пайплайна и вычисляет
долю верных результатов
Выводит среднее значение
и стандартное отклонение оценок
mean_cv = np.mean(cv_scores['test_score'])
accuracy, рассчитанных на валидаstd_cv = np.std(cv_scores['test_score'])
ционных фолдах кросс-валидации
print(f"accuracy: {mean_cv:0.3f} ({std_cv:0.3f})")
pipeline.fit(X, y)
Обучает модель логистической регрессии на всем
датасете X и соответствующих метках y
Полученный результат показывает долю верных результатов, полученную в процессе кросс-валидации в задаче классификации:
accuracy 0.900 (0.032)
Ключевым моментом здесь является не модель, а процедура выполнения действий,
которая стандартна для всех табличных задач, независимо от того, работаете вы
с классическими моделями машинного обучения или передовыми алгоритмами
глубокого обучения. Scikit-learn прекрасно встраивает такую процедуру в свой
API, тем самым представляя собой универсальный и незаменимый инструмент
для задач табличных данных. В следующих разделах мы рассмотрим его характеристики scikit-learn и его работу, поскольку будем многократно использовать
его процедуры в наших примерах в книге.
4.1.1. Общие особенности пакетов scikit-learn
Вот ключевые характеристики библиотеки scikit-learn.
Она предлагает широкий спектр моделей для задач классификации и регрессии, а также для кластеризации, снижения размерности, предварительной
обработки данных и выбора моделей. Большинство моделей работают в оперативной памяти, когда данные помещаются в RAM, и вне ее, когда данные
4.1. Знакомство с scikit-learn 133
считываются с диска, что позволяет проводить обучение на датасетах, объем
которых превышает доступную память компьютера.
Во всех моделях реализован единый API (такие методы, как fit, partial_fit,
predict, predict_proba, transform), который можно быстро изучить и использовать и который фокусируется исключительно на преобразованиях
и процессах, необходимых для того, чтобы модель училась на данных и делала
прогнозы на их основе. API scikit-learn также предлагает автоматическое
разделение данных на обучающие и тестовые, возможность связывать и повторно использовать элементы в пайплайне данных и обеспечивать доступность параметров путем простой проверки открытых атрибутов применяемого
класса.
Первоначально scikit-learn работал с массивами NumPy и разреженными
матрицами, но позже возможности расширились до датафреймов pandas,
что позволило использовать их в качестве входных данных. В более поздних
версиях (начиная с версии 1.1.3) можно сохранить ключевые характеристики датафрейма, такие как имена столбцов и преобразования, выполняемые
функциями и классами scikit-learn. Поддержка датафреймов давно была
востребована и действительно необходима для табличных данных — темы
этой книги.
Чтобы определить рабочие параметры каждого класса scikit-learn, нужно
просто использовать стандартные типы и классы Python (строки, числа
с плавающей точкой, списки). Кроме того, значения по умолчанию всех таких параметров уже установлены на надлежащее значение, чтобы вы могли
создать базовую модель для начала проекта и последующего улучшения.
Благодаря основной группе ведущих участников (таких как Андреас Мюллер
(Andreas Mueller), Оливер Гризель (Oliver Grisel), Фабиан Педрегоса (Fabian
Pedregosa), Гаэль Варокво (Gael Varoquaux) и Жиль Луп (Gilles Loupe))
scikit-learn постоянно улучшается. Отладка ведется непрерывно, и каждый
раз добавляются новые функции и новые модели или исключаются старые
из-за их плохой надежности и масштабируемости.
Пакет также содержит обширную и легкодоступную документацию с примерами, с которыми можно ознакомиться в интернете (https://scikit-learn.org/
stable/user_guide.html) или в автономном режиме с помощью команды help().
В зависимости от типа операционной системы и системных настроек для установки scikit-learn вам просто нужно следовать инструкциям на https://scikit-learn.org/
stable/install.html. Вместе с pandas (https://pandas.pydata.org/) scikit-learn является
основной библиотекой для анализа и моделирования табличных данных. Она
предлагает широкий спектр алгоритмов машинного обучения и статистики исключительно для структурированных данных; фактически входными данными
должны быть датафрейм pandas, массив NumPy или разреженная матрица на
134 Глава 4. Классические алгоритмы для табличных данных
выбор. Все эти алгоритмы хорошо зарекомендовали себя, поскольку команда
scikit-learn решила включать их в пакет на основе следующих критериев: «не
менее трех лет с момента публикации, более 200 цитирований, широкое использование и полезность». Более подробную информацию о требованиях
к включению алгоритмов в scikit-learn см. по адресу https://mng.bz/8OMw.
4.1.2. Общий интерфейс scikit-learn
Другим ключевым аспектом scikit-learn, который делает библиотеку подходящей
для задач табличных данных, является ее API оценщика (estimator) — методы
fit, predict/transform. Такой API не ограничивается только scikit-learn и широко
признан как наиболее эффективный подход к обработке обучающих и тестовых данных. Многие другие проекты приняли его (см. https://mng.bz/EaWO).
Фактически, следуя API scikit-learn, вы автоматически включаете все лучшие
практики анализа данных в свой проект: в частности, разделение обучающих,
валидационных и тестовых данных, что является обязательным условием для
успеха любого моделирования, как мы продемонстрируем в следующем разделе
на примере датасета Airbnb NYC.
Прежде чем углубляться в более практически значимые примеры, давайте разберем базовые сведения об оценщиках scikit-learn. Во-первых, мы выделяем
четыре типа объектов в scikit-learn, каждый из которых имеет свой интерфейс.
Один класс может реализовывать несколько объектов одновременно. Оценщик — лишь один из многих объектов, хотя и самый важный, поскольку большинство классов scikit-learn являются оценщиками. В следующем примере мы
определяем оценщик машинного обучения — логистическую регрессию (которая
будет рассмотрена позже в этой же главе) для классификации с использованием
класса LogisticRegression, предлагаемого scikit-learn:
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(C=1.0)
Оценщик — это объект, обучаемый на основе данных с использованием метода
.fit. Этот метод может применяться для обучения с учителем (когда сопоставляются данные с целевым признаком) или без учителя (когда задействованы
только данные):
для обучения с учителем: estimator = estimator.fit(data, targets)
для обучения без учителя: estimator = estimator.fit(data)
Под капотом оценщик использует данные для оценки некоторых параметров,
которые служат для последующего сопоставления данных с прогнозами или
их преобразования. Параметры и другая информация, собранная в процессе,
становятся доступными в качестве атрибутов объекта.
4.1. Знакомство с scikit-learn 135
Другие типичные объекты scikit-learn.
Преобразователь (transformer) — объект, предназначенный для отображения
преобразования данных:
transformed_data = transformer.transform(data)
Предиктор — объект, необходимый для отображения прогнозируемого ответа
с учетом некоторых данных с помощью методов .predict (прогнозирование
общего результата) и .predict_proba (прогнозирование вероятности):
prediction = predictor.predict(data)
probability = predictor.predict_proba(data)
Модель — объект для оценки того, насколько хорошо модель описывает
данные, с помощью метода .score, что является стандартным подходом
в статистике:
score = model.score(data)
Независимо от того, нужен вам оценщик или преобразователь, каждый класс
всегда создается путем присвоения его переменной и указания ее параметров.
Под капотом все эти классы хранят параметры для своей задачи. Некоторые из
них извлекаются непосредственно из данных и обычно называются весами, или
параметрами моделей. Это как коэффициенты в математической формуле: неизвестные значения, которые должны быть определены данными и вычислениями.
Другие задаются пользователем при создании экземпляра класса и могут быть
настройками конфигурации или инициализации, которые влияют на то, как
алгоритм обучается на данных. Обычно мы называем их гиперпараметрами.
Они, как правило, различаются в зависимости от модели машинного обучения;
поэтому мы обсудим самые важные из них при объяснении каждого алгоритма.
Параметры конфигурации и настройки одинаковы для всех алгоритмов. Например, настройка random_state помогает определить случайное начальное число,
или сид (seed) для воспроизведения точного поведения модели при использовании тех же данных, благодаря чему результаты не будут меняться в разных
запусках. Параметр конфигурации n_jobs позволит установить, сколько процессоров CPU вы хотите использовать в вычислениях, тем самым ускоряя время,
необходимое для работы модели, но это помешает выполнять другие операции
на компьютере. В зависимости от алгоритма другие доступные настройки того
же типа могут определять допуск или кэш памяти, используемый моделью.
Как мы уже упоминали, некоторые из этих гиперпараметров влияют на работу
модели, а другие — на то, как она обучается на данных. Давайте повторим наш
предыдущий пример:
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(C=1.0)
136 Глава 4. Классические алгоритмы для табличных данных
Среди гиперпараметров, которые влияют на обучение модели, в нашем примере
мы используем параметр C, который, принимая различные значения, инструктирует ML-алгоритм применять некоторые ограничения при выявлении закономерностей в данных. Мы рассмотрим все параметры, которые должны быть
заданы для каждого алгоритма, по мере их представления. Важно отметить, что
гиперпараметры обычно устанавливаются во время создания экземпляра класса.
После создания экземпляра класса обычно предоставляются данные для обу
чения и некоторые ограниченные инструкции по работе с ними, например
каждому примеру присваиваются разные веса. На этом этапе мы говорим, что
вы обучаете (train, fit) класс на данных. Этот этап обычно называют «обучение
оценщика» (fitting an estimator), и выполняется он путем передачи данных в виде
массива NumPy, разреженной матрицы или датафрейма pandas1 в метод .fit:
X = [[-1, -1], [-2, -1], [1, 1], [2, 1]]
y = [1, 1, 0, 0]
model.fit(X, y)
Поскольку обучение модели требует сопоставления ответов с некоторыми
данными, метод .fit принимает на вход матрицу признаков и вектор целевых
значений. Такое поведение стандартно для моделей, поскольку некоторые другие
классы scikit-learn также принимают данные на вход. Метод .fit также является
общим для всех преобразующих классов в scikit-learn. Например, он типичен для
всех классов, выполняющих предварительную обработку данных, как вы можете
убедиться, перейдя по адресу https://mng.bz/N161, потому что преобразования
требуют изучения некоторой информации, извлеченной из признаков. Так, если
вам нужно стандартизировать данные, вы должны сначала узнать стандартное
отклонение и среднее значение каждого числового признака. StandardScaler
scikit-learn (https://mng.bz/DMgw) делает именно это:
from sklearn.preprocessing import StandardScaler
processing = StandardScaler().fit(X)
В нашем примере мы создаем экземпляр класса, необходимый для стандартизации данных (StandardScaler), и сразу же после этого применяем метод .fit.
Поскольку он возвращает экземпляр того же класса, который мы в него подали,
можно безопасно получить обратно класс со всеми установленными параметрами, объединив эти два шага. Такой подход будет полезен при построении
пайплайнов данных и моделей обучения, поскольку он помогает отделить действия, которые чему-то обучаются на данных, от действий, которые применяют
то, чему они научились, к новым данным. Таким образом, вы не запутаетесь и не
смешаете информацию из обучающих, валидационных или тестовых данных.
1
В примере передаются списки списков. — Примеч. науч. ред.
4.1. Знакомство с scikit-learn 137
В зависимости от сложности базовых операций и количества предоставленных
данных обучение модели или расчет параметров функции обработки данных
могут занять некоторое время. После завершения процесса вам станут доступны
многие другие атрибуты для последующего использования в зависимости от
задействованного алгоритма.
Для обученной модели можно получить вектор прогнозов на основе любых
новых данных, если применить метод .predict, который используется как для
классификации, так и для регрессии:
X_test = [[-1, 1], [2, -1]]
model.predict(X_test)
А если вы работаете над задачей классификации и вам нужно получить вероятность того, что определенный класс является правильным предсказанием для
нового образца? В этом случае надо использовать метод .predict_proba, который
доступен не для всех моделей:
model.predict_proba(X_test)
Классы, обрабатывающие данные, не имеют метод .predict. Вместо него они
используют метод .transform, который возвращает преобразованные данные,
если класс был ранее создан и снабжен некоторыми обучающими данными для
выявления ключевых параметров, необходимых для преобразования:
processing.transform(X)
Поскольку преобразование часто применяется к тем же данным, на основе которых были рассчитаны ключевые параметры, метод .fit_transform, который
объединяет эту фазу с этапом преобразования, является удобным способом
сокращения объема кода:
processing.fit_transform(X)
4.1.3. Введение в пайплайны scikit-learn
В scikit-learn существует возможность обернуть последовательность преобразований, а затем предсказаний, попутно выбирая, что именно преобразовывать, и объединяя различные последовательности преобразований с помощью
служебных функций:
Pipeline (https://mng.bz/lYx8);
ColumnTransformer (https://mng.bz/BXM8);
FeatureUnion (https://mng.bz/dX2O).
138 Глава 4. Классические алгоритмы для табличных данных
Команда Pipeline позволяет создать последовательность классов scikit-learn,
которая проводит серию преобразований данных, так что в итоге мы получаем
модель и ее прогнозы. Таким образом вы можете интегрировать любую модель
с требуемыми ей преобразованиями для данных и работать со всеми задействованными параметрами одновременно — параметрами преобразований и параметрами самой модели. Команда Pipeline является основной для перемещения
табличных данных из источника в прогнозы в пакете scikit-learn. Чтобы задать
ее во время создания экземпляра, нужно просто предоставить список кортежей, каждый из которых содержит имя шага в пайплайне и класс или модель
scikit-learn. После создания экземпляра пайплайна можно использовать общие
спецификации API scikit-learn (fit, transform/predict). Пайплайн выполнит все
предопределенные шаги последовательно и вернет конечный результат. Разу
меется, можно получать доступ, проверять и настраивать отдельные шаги для
достижения наилучших результатов и производительности, но можно управлять
пайплайном и как одной макрокомандой.
Однако табличные столбцы могут иметь разные типы и требовать совершенно разных преобразований; либо может быть, что вы придумали два разных
способа обработки данных, которые хотели бы объединить. ColumnTrasformer
и FeatureUnion — вот команды, которые помогут вам в таких случаях.
ColumnTrasformer позволяет применять определенное преобразование или
пайплайн преобразований только к определенным столбцам (которые вы можете
определить по их имени или индексу). Команда принимает список кортежей, как
и команда Pipeline, но требуется задать имя для преобразования, класс scikitlearn для выполнения и список имен столбцов или индексов, к которым должно
быть применено преобразование. Поскольку это просто преобразующая команда,
ее идеальное использование — внутри пайплайна, где преобразования могут быть
частью подачи данных в модель. FeatureUnion, напротив, — это простой способ
объединения результатов двух отдельных пайплайнов. Того же результата можно
достичь с помощью простой команды NumPy, такой как np.hstack (https://mng.bz/
rKJD). Однако при использовании FeatureUnion у вас есть преимущество в том,
что команда может быть помещена в пайплайн scikit-learn и, следовательно,
автоматически использоваться как часть подачи данных в модель.
Модульность операций и последовательность методов API, предлагаемые scikitlearn, а также команды Pipeline, ColumnTrasformer и FeatureUnion позволяют
с легкостью проводить сложные преобразования данных, которые будут обрабатываться как одна команда. Это сделает код компактным, легко читаемым
и сопровождаемым.
В следующем разделе мы вернемся к датасету Airbnb NYC, который использовали ранее. Мы создадим последовательность преобразований в scikit-learn, которые позволят продемонстрировать, что эта библиотека и ее функции пайплайна
4.2. Исследование и обработка признаков датасета Airbnb NYC 139
очень удобны для задач анализа табличных данных. Мы также покажем, что
переключение между различными опциями машинного обучения не составит
сложностей, если есть четко определенный пайплайн.
4.2. Исследование и обработка признаков датасета
Airbnb NYC
Ранее представленный датасет Airbnb NYC идеально подходит для демонстрационных целей, поскольку представляет реальную практическую задачу и содержит
разнородные типы признаков. Нам потребуется создавать и комбинировать
разные пайплайны для обработки различных признаков, и следующие главы
дадут возможность представить еще более продвинутые методы обработки, чем
те, которые вы найдете в данной главе.
А пока что поместим признаки, с которыми будем работать, в список под названием exclusion_list. Это координаты latitude и longitude или last_review
(дата последнего отзыва), которые требуют особой обработки. Кроме того,
датасет содержит несколько столбцов, которые потенциально могут выступать
в качестве целевых признаков: цена price, доступность представленных объектов (availability_365) и количество отзывов (number_of_reviews). Для наших
целей предпочтительнее использовать цену. Поскольку это непрерывный набор
значений выше нуля, для задачи регрессии лучше взять ее в качестве целевой
переменной. Кроме того, разделив значения по среднему или медиане либо
сгруппировав их по децилям, мы можем преобразовать признак price в целевую переменную для бинарной или многоклассовой классификации. Мы также
будем использовать все остальные признаки как предиктивные или для более
продвинутой генерации признаков.
В следующем подразделе продемонстрируем пошаговый подход к исследованию
датасета, его фильтрации на основе полезных столбцов и настройке целевых
переменных. В принципе мы будем следовать подсказкам и примерам, представленным в главе 2 при обсуждении разведочного анализа данных (EDA).
В следующем разделе воспользуемся этими знаниями и создадим для данных
подходящие пайплайны, которые повторно используем в следующих параграфах, когда будем делать обзор различных алгоритмов машинного обучения для
таблиц.
4.2.1. Исследование датасета
В качестве первого шага в исследовании датасета импортируем необходимые
библиотеки (NumPy и pandas), определим список исключенных признаков,
а также отдельные списки для категориальных и непрерывных признаков на
140 Глава 4. Классические алгоритмы для табличных данных
основе наших знаний, полученных в предыдущей главе, и загрузим данные из
текущего рабочего каталога. Код, который нужно выполнить:
import numpy as np
import pandas as pd
excluding_list = ['price', 'id', 'latitude',
'longitude', 'host_id',
Список названий столбцов, которые
'last_review', 'name',
надо исключить из анализа
'host_name']
categorical = ['neighbourhood_group',
Список столбцов категориальных
'neighbourhood',
переменных
'room_type']
continuous = ['minimum_nights',
'number_of_reviews',
Список столбцов непрерывных
'reviews_per_month',
числовых переменных
'Calculated_host_listings_count']
data = pd.read_csv("./AB_NYC_2019.csv")
После того как фрагмент кода завершил загрузку данных, сначала проверяем,
сколько строк и столбцов было возвращено в датафрейме:
data.shape
Мы получили 48 895 строк — достаточное количество для табличной задачи,
позволяющее использовать любой доступный алгоритм обучения, — и 16 столбцов. Поскольку нас интересуют только некоторые из них — те, которые мы
определили в переменных как категориальные и непрерывные, — разделим
категориальные признаки на низко- и высококардинальные на основе числа их
уникальных значений:
data[categorical].nunique()
Результатом выполнения команды является следующий вывод:
neighbourhood_group
neighbourhood
room_type
5
221
3
Наш стандартный подход при работе с категориальными признаками заключается в применении one-hot-кодирования, которое создает одну бинарную переменную для каждого уникального значения в исходном признаке. Однако при
использовании такого кодирования признаки, имеющие более 20 уникальных
значений, приведут к созданию чрезмерного количества столбцов и разреженности данных, которая возникает, когда они в основном состоят из нулевых
значений, что является серьезной проблемой, особенно для нейронных сетей
и онлайн-алгоритмов, поскольку усложняет процесс обучения. В главе 6 мы представим методы, такие как кодирование по целевой переменной (target encoding),
для работы с признаками со слишком большим количеством уникальных значений, называемыми категориальными признаками с высокой кардинальностью.
4.2. Исследование и обработка признаков датасета Airbnb NYC 141
В примерах этой главы мы разделим категориальные признаки с низкой и высокой кардинальностью и обработаем только первую группу:
low_card_categorical = ['neighbourhood_group', 'room_type']
high_card_categorical = ['neighbourhood']
Далее, определив признаки (на данный момент будем работать только с числовыми и категориальными признаками с низкой кардинальностью), нам нужно
выяснить, есть ли в наших данных пропущенные значения. Следующая команда
выявляет истинно пропущенные значения и подсчитывает их количество для
каждого признака:
data[low_card_categorical + continuous].isna().sum()
Получаем следующий результат, который указывает на проблему только с признаком reviews_per_month (отзывов за месяц):
neighbourhood_group
room_type
minimum_nights
number_of_reviews
reviews_per_month
calculated_host_listings_count
availability_365
0
0
0
0
10052
0
0
Как мы уже говорили в главе 2, работа с пропущенными значениями не должна
быть автоматизированной процедурой; она требует некоторого осмысления со
стороны специалиста, который должен определить, есть ли причина пропусков.
В этом случае становится очевидной проблема с обработкой данных в источнике,
поскольку, если проверить минимальное значение, оно окажется выше нуля:
data.reviews_per_month.min()
Минимальное зарегистрированное значение составляет 0.01. Пропуск значения
возникает, когда отзывов для формирования статистики недостаточно. Следовательно, можно заменить пропущенное значение этого признака на ноль.
Отфильтровав признаки, которые будут использоваться для прогнозирования,
и проверив наличие пропущенных значений (поскольку большинство алгоритмов машинного обучения не работают с ними, за исключением нескольких,
таких как XGBoost или LightGBM — они рассматриваются в следующей главе),
можем перейти к проверке нашей целевой переменной. Эта часть EDA, анализ
целевой переменной, часто упускается из виду, однако она очень важна, поскольку
в табличных задачах возможности ML-алгоритмов по работе с целевыми переменными различаются. Например, целевые переменные с большим количеством
нулей, «толстыми хвостами» и многомодальными значениями сложны для
некоторых моделей и приводят к недообучению. Давайте начнем с проверки
распределения признака price. Для этого можно использовать гистограмму,
142 Глава 4. Классические алгоритмы для табличных данных
отображающую частотность значений, попадающих в те или иные диапазоны
(бины), которая покажет, какой тип распределения у ваших данных, например
нормальное, или выделит, вокруг каких значений есть пики и где данные плотнее
(рис. 4.1). Если вы работаете с датафреймом pandas, такую гистограмму можно
построить, просто вызвав метод hist:
data[["price"]].hist(bins=10)
Рис. 4.1.
Гистограмма
распределения
признака price
Распределение, показанное на рис. 4.1, сильно смещено вправо, с большим количеством выбросов, поскольку отображенные значения находятся в диапазоне до
10 000. Однако непосредственно перед 2000 трудно различить какие-либо бары,
отображающие частоты. Это хорошо видно на диаграмме под названием «ящик
с усами» (boxplot), которая очень полезна, когда нужно визуализировать, где
находится основная часть распределения переменной. Ящик с усами для переменной — это график, на котором ключевые значения распределения изображены в виде прямоугольника («ящика») с двумя линиями вне ящика, которые
простираются до ожидаемых пределов распределения переменных («усами»).
Ящик ограничен межквартильным размахом (IQR), определяемым 25-м и 75-м
процентилями, и разделен на две части линией медианы. Усы простираются вверх
и вниз до значений, в 1,5 раза превышающих IQR. Все, что выше или ниже краев
усов, считается выбросом: необычным или неожиданным значением. Давайте
построим диаграмму этого типа для переменной price, используя встроенный
в pandas метод boxplot (рис. 4.2):
data[["price"]].boxplot()
4.2. Исследование и обработка признаков датасета Airbnb NYC 143
Рис. 4.2. Ящик с усами,
иллюстрирующий
распределение
признака price;
видно тяжелый хвост
справа при больших
значениях цены
Неудивительно, что ящик и усы зажаты в нижней части графика и почти неотличимы друг от друга. Длинная очередь выбросов тянется от верхнего предела
верхней крайней точки диаграммы. Это правостороннее асимметричное распределение (right skewed distribution). В таких случаях стандартным решением
для улучшения переменной является логарифмическое преобразование целевого
признака. Обычная практика — добавление константы для смещения значений в область положительных чисел для обработки нулевых и отрицательных
значений. В нашем случае в этом нет необходимости, поскольку все значения
положительные и больше нуля. В следующем фрагменте кода мы представляем
логарифмически преобразованный признак price (рис. 4.3 и 4.4):
np.log1p(data["price"]).hist(bins=20)
data[["price"]].apply(lambda x: np.log1p(x)).boxplot()
Рис. 4.3. Гистограмма
признака price
становится более
симметричной после
логарифмического
преобразования
144 Глава 4. Классические алгоритмы для табличных данных
Рис. 4.4. Ящик
с усами для
признака price после
логарифмического
преобразования,
показывающий
сохранение
экстремальных
значений на обоих
хвостах распределения
Теперь распределение более симметрично, хотя очевидно, что имеются выбросы
с обеих сторон. Поскольку мы преследуем иллюстративную цель, можем игнорировать исходное распределение и сосредоточиться на содержательном представлении целевой переменной. Например, можно сохранить только значения
цен ниже 1000 (рис. 4.5). В следующем фрагменте кода мы создаем гистограмму
только на значениях цен ниже 1000:
data[["price"]][data.price <= 1000].hist(bins=20)
Рис. 4.5. Гистограмма
признака price для
значений ниже 1000
по-прежнему имеет
длинный хвост,
смещенный вправо
Представленное здесь распределение все еще смещено вправо, но оно напоминает
распределения с длинным хвостом, встречающиеся в электронной коммерции
или других продажах. Кроме того, если мы сосредоточимся на диапазоне от 50 до
200, распределение будет выглядеть более равномерным (рис. 4.6). В следующем
4.2. Исследование и обработка признаков датасета Airbnb NYC 145
фрагменте кода мы дополнительно ограничиваем наш анализ ценами от 50 до
200 и строим соответствующую гистограмму:
data[["price"]][(data.price >= 50) & (data.price <= 200)].hist(bins=20)
Рис. 4.6. Гистограмма
признака price для
значений от 50 до
200, показывающая
распределенные
значения по всему
диапазону
Следовательно, мы можем создать две маскирующие переменные, состоящие
из булевых значений, которые помогут отфильтровать целевую переменную
в соответствии с типом алгоритма, который хотим протестировать. Переменная
price_capped продемонстрирует, как легко некоторые алгоритмы машинного
обучения могут обрабатывать длинные хвосты:
price_capped = data.price <= 1000
price_window = (data.price >= 50) & (data.price <= 200)
На рис. 4.7 показан ящик с усами для price_capped, где присутствуют правосторонние выбросы, но, по крайней мере, сам ящик и его усы видны.
Рис. 4.7. Ящик
с усами признака
price для значений
ниже 1000,
показывающий
длинный хвост
экстремальных
значений в правой
части
146 Глава 4. Классические алгоритмы для табличных данных
На рис. 4.8 показан ящик с усами для price_window, где не наблюдается выбросов:
data[["price"]][price_window].boxplot()
Рис. 4.8. Ящик
с усами для
признака price для
значений от 50 до
200, показывающий
немного
смещенное вправо
распределение
без экстремальных
значений
Завершив исследование предикторов и целевой переменной, мы можем подготовить четыре различные целевые переменные, которые будут использоваться
в наших примерах:
target_mean = (data["price"] > data["price"].mean()).astype(int)
target_median = (data["price"] > data["price"].median()).astype(int)
target_multiclass = pd.qcut(data["price"], q=5, labels=False)
target_regression = data["price"]
Мы подготовили две бинарные целевые переменные, target_mean и target_
median, а также многоклассовую целевую переменную с пятью различными
классами на основе процентилей для классификации.
Важно отметить, что наша target_median — это бинарная сбалансированная целевая переменная. Следовательно, мы можем смело использовать долю верных
результатов (accuracy) в качестве метрики качества. При проверке вы получите
почти равное количество случаев для положительного и отрицательного классов:
target_median.value_counts()
Вы получаете результат:
0
1
24472
24423
Если же нужно сделать то же самое с целевой переменной target_mean, то используем следующую команду:
target_mean.value_counts()
4.2. Исследование и обработка признаков датасета Airbnb NYC 147
Вы получите распределение, смещенное в сторону отрицательных случаев; то
есть случаев ниже среднего значения будет больше из-за асимметричного распределения:
0
1
34016
14879
В такой ситуации при оценке результатов работы классификатора машинного
обучения предпочтительнее использовать такие метрики, как площадь под ROCкривой (Receiver Operating Characteristic Area Under the Curve, ROC-AUC) или
средняя точность (average precision) — обе можно применять при упорядочивании.
Наконец, что касается многоклассовой целевой переменной, подсчет случаев
для каждого из пяти классов показывает, что их распределение также является
сбалансированным:
target_multiclass.value_counts()
Эта команда возвращает результат:
0
1
2
3
4
10063
9835
9804
10809
8384
Что касается регрессионной целевой переменной target_regression, то это
исходная переменная без преобразований. Однако мы будем использовать ее
подмножества и соответствующим образом преобразовывать их в зависимости
от демонстрируемого алгоритма машинного обучения.
Завершив исследование данных, целевой переменной и выполнив базовый отбор
признаков, в следующем разделе мы будем применять модульный подход для
подготовки нескольких пайплайнов, которые помогут нам в изучении различных
вариантов машинного обучения для задач с табличными данными.
4.2.2. Подготовка пайплайнов
Для подготовки пайплайнов мы будем использовать ранее рассмотренные
классы Pipeline и ColumnTransformer из scikit-learn. При модульном подходе
начинать нужно с создания операций, которые будут применяться к разным
типам данных, характеризующим признаки в табличном датасете.
Следующий код определяет три основные процедуры, которые будут многократно использоваться в этой главе.
One-hot-кодирование категориальных признаков — категориальные признаки
преобразуются в бинарные. Если значение никогда ранее не встречалось, оно
будет проигнорировано.
148 Глава 4. Классические алгоритмы для табличных данных
Сквозная обработка числовых признаков — пропущенные значения в числовых признаках заполняются нулями.
Стандартизация числовых признаков — после заполнения пропущенных
значений числовые признаки масштабируются путем вычитания их среднего
значения и деления на стандартное отклонение.
Код, описывающий эти процедуры, показан в листинге 4.2.
Листинг 4.2. Создание модулей для пайплайнов обучения на табличных
данных
from
from
from
from
from
from
sklearn.pipeline import Pipeline
sklearn.compose import ColumnTransformer
sklearn.preprocessing import OneHotEncoder
sklearn.preprocessing import OrdinalEncoder
sklearn.impute import SimpleImputer
sklearn.preprocessing import StandardScaler
Преобразует категориальный признак
categorical_onehot_encoding = OneHotEncoder(
при помощи прямого кодирования
handle_unknown='ignore')
Заменяет пропущенные
numeric_passthrough = SimpleImputer(
числовые значения нулями
strategy="constant", fill_value=0)
numeric_standardization = Pipeline([
("imputation", SimpleImputer(strategy="constant", fill_value=0)),
("standardizing", StandardScaler())
])
Пайплайн заменяет пропущенные числовые значения
нулями и приводит признаки к стандартному виду
На этом этапе мы можем составить пайплайны преобразования, которые обрабатывают данные в соответствии с нашими потребностями для каждого алгоритма
машинного обучения. Так, в этом примере мы задаем пайплайн, который выполняет прямое кодирование для категориальных признаков с низкой кардинальностью и заполняет пропущенные значения нулями для числовых признаков.
Такой пайплайн создается с помощью функции ColumnTransformer, которая
объединяет операции, применяемые к разным наборам признаков. Это превосходная стратегия, подходящая для большинства моделей машинного обучения:
column_transform = ColumnTransformer(
[('categories',
Первый этап: прямое кодирование
categorical_onehot_encoding,
low_card_categorical),
категориальных признаков
('numeric',
Второй этап: обработка
numeric_passthrough,
числовых признаков
continuous),
],
remainder='drop',
Необработанные признаки отбрасываются
verbose_feature_names_out=False,
и не учитываются в результатах
sparse_threshold=0.0
Названия признаков сохранены
)
Результатом всегда является плотная
матрица (то есть массив NumPy)
в первоначальном виде
4.3. Классическое машинное обучение 149
Мы можем сразу же запустить этот фрагмент кода и проверить, как этот пайплайн
преобразует наш датасет Airbnb в Нью-Йорке:
X = column_transform.fit_transform(data)
print(type(X), X.dtype, X.shape)
В результате получается массив NumPy, состоящий из чисел с плавающей точкой, и его размерность увеличилась до 13 столбцов. Фактически из-за прямого
кодирования каждое значение в категориальных признаках превратилось в отдельный признак:
<class 'numpy.ndarray'> float64 (48895, 13)
В следующем разделе будут рассмотрены основные методы машинного обучения
для табличных данных. Каждый из них будет сопровождаться своим классом
преобразования столбцов, который мы интегрируем в пайплайн с моделью.
4.3. Классическое машинное обучение
Для объяснения различных моделей из классических методов машинного обу
чения для табличных данных мы будем сначала перечислять основные характеристики алгоритма, а затем разбирать код, работающий на нашем эталонном
табличном наборе данных — Airbnb NYC. Ниже приведены некоторые лучшие
практики, которые мы будем использовать в наших примерах, чтобы обеспечить
воспроизводимость и сопоставимость различных подходов.
Определение пайплайна, включающего как преобразование данных, так
и моделирование.
Задание метрики ошибки, например среднеквадратичной ошибки (RMSE)
для регрессии или доли верных результатов (accuracy) для классификации,
и ее измерение с использованием единой стратегии кросс-валидации.
Указание среднего значения и стандартного отклонения — это имеет решающее значение для выяснения того, является ли производительность модели
стабильной на различных выборках данных, — для оценки ошибки, полученной в процессе кросс-валидации.
В предыдущем разделе мы представили различные инструменты, которые
scikit-learn предлагает для создания пайплайнов данных, объединяющих преобразование признаков и модели машинного обучения. В этом разделе мы представим рекомендуемые метрики оценки и объясним, как работает оценка кроссвалидации, выполняемая с помощью команды cross_validate в scikit-learn.
Давайте сначала рассмотрим метрики качества. Мы решили использовать
среднеквадратичную ошибку — распространенную метрику для задач регрессии,
150 Глава 4. Классические алгоритмы для табличных данных
и долю верных результатов — еще одну стандартную метрику для сбалансированных бинарных и многоклассовых задач классификации, когда классы имеют
примерно одинаковые размеры выборки. В последующих главах мы также будем
использовать метрики, подходящие для задач несбалансированной классификации, такие как ROC-AUC и средняя точность.
Кросс-валидация является стандартом в data science и используется, когда необходимо оценить ожидаемую производительность ML-модели на данных, отличных от обучающих, но взятых из того же распределения. Важно отметить,
что кросс-валидация оценивает будущую производительность модели, исходя
из предположения, что данные могут измениться в будущем, но не станут
радикально иными. Для корректной работы модель ожидает, что в будущем
будут использоваться те же признаки, что они будут иметь те же уникальные
значения (если это категориальный признак) с похожими распределениями (как
для категориальных, так и для числовых признаков) и, это наиболее важно, что
признаки будут находиться в той же связи с целевой переменной.
Предположение о том, что распределение данных не изменится в будущем, часто не оправдывается, поскольку экономика, потребительские рынки, а также
социальная и политическая ситуация быстро меняются в реальном мире. На
практике модель может столкнуться с дрейфом концепций (concept drifting),
когда смоделированные отношения между признаками и целевыми переменными больше не отражают реальное положение дел. В результате модель будет
демонстрировать ухудшение производительности при работе с новыми данными.
Кросс-валидация — лучший инструмент для оценки моделей на этапе их создания, поскольку она основана на доступной информации в текущий момент
и поскольку, если все правильно сделать, на нее не влияет способность модели
к переобучению. Ее полезность сохраняется даже в случаях, когда результаты
кросс-валидации не подтверждаются в будущем — обычно из-за изменения
базового распределения данных. Альтернативные методы, такие как пошаговое исключение (leave-one-out) или бутстрэппинг (bootstrapping), предлагают
более точные оценки, но требуют значительно более высоких вычислительных
затрат, тогда как более простые методы, такие как train/test split (разделение на
обучающую и тестовую выборки), дают менее надежные оценки.
В своей наиболее простой форме k-фолдовая кросс-валидация (реализованная
в scikit-learn с помощью функции KFold: https://mng.bz/VVM0) основана на разделении имеющихся у вас обучающих данных на k частей и построении k версий
модели, которой каждый раз подаются различные наборы из k –1 частей, а затем она тестируется на оставшейся исключенной части (производительность
на новых данных). Среднее значение и стандартное отклонение полученных k
оценок позволяют получить оценку модели и количественное выражение меры
неопределенности, которые будут использоваться как ориентир ожидаемой производительности на новых данных в будущем. На рис. 4.9 показана k-фолдовая
4.3. Классическое машинное обучение 151
валидация при k = 5: каждая строка представляет собой разделение данных на
каждом фолде. Валидационная часть каждого фолда всегда отличается от других,
а обучающая всегда имеет разный состав.
Фолд 1
Валидация
Фолд 2
Обучение
Фолд 3
Фолд 4
Фолд 5
Обучение
Валидация
Обучение
Обучение
Валидация
Обучение
Валидация
Обучение
Обучение
Обучение
Валидация
Рис. 4.9. Как данные распределяются между обучением и валидацией по фолдам
при пятифолдовой кросс-валидации
Выбор правильного значения k зависит от того, сколько имеется обучающих
данных, насколько вычислительно затратно обучение модели на них, насколько
полученная выборка охватывает все возможные вариации целевого распределения данных, которые требуется смоделировать, и с какой целью вы хотите получить оценку производительности. Как правило, оптимальное значение k — от 5
до 10, причем k = 10 больше подходит для точной оценки производительности,
а k = 5 — компромисс между точностью и вычислительными затратами для таких видов деятельности, как оценка модели, признаков и гиперпараметров (вот
почему в наших примерах будет использоваться k = 5).
Чтобы получить общую оценку производительности модели, можно построить
необходимые итерации кросс-валидации, используя функцию KFold (или ее
вариации, предлагающие стратификацию выборки или контроль по временнˆому
измерению: https://mng.bz/xKne), или воспользоваться процедурой cross_validate
(https://mng.bz/AQyK), которая все сделает за вас и вернет результаты. Для наших целей тестирования различных алгоритмов cross_validate очень удобна,
поскольку при задании соответствующих параметров она предоставляет ряд
метрик:
Оценки кросс-валидации на тестовой выборке (качество на новых данных,
out-of-sample performance).
Оценки кросс-валидации на обучающей выборке (качество на обучающих
данных, in-sample performance).
152 Глава 4. Классические алгоритмы для табличных данных
Время обучения и время предсказания (для оценки вычислительной стоимости).
Обучение оценщиков на различных фолдах кросс-валидации.
Все, что нужно сделать, — это предоставить оценщик, которым может быть любой
объект scikit-learn с методом fit, предикторы и целевую переменную, стратегию
кросс-валидации и одну или несколько функций оценки в виде списка. Этот
оценщик должен быть вызываемым объектом, который можно создать с помощью
команды make_scorer (https://mng.bz/ZlAO). В следующем разделе мы рассмотрим,
как получить оценки производительности с помощью кросс-валидации, используя перечисленные входные данные, и начнем с классических алгоритмов
машинного обучения, таких как линейная и логистическая регрессии.
4.3.1. Линейная и логистическая регрессии
В линейной регрессии — статистическом методе, который моделирует связь между
зависимой и одной или несколькими независимыми переменными путем подбора
коэффициентов линейного уравнения к наблюдаемым данным, — сначала все
признаки преобразуются в числовые и помещаются в матрицу, включая категориальные признаки с кодировкой one-hot. Цель алгоритма — найти оптимальные
значения весов в виде вектор-столбца (коэффициенты), чтобы при умножении на
матрицу признаков получить вектор результатов, наилучшим образом приближающих целевые значения (предсказания). Другими словами, алгоритм стремится
минимизировать остаточную сумму квадратов между целевыми значениями и прогнозами, полученными путем умножения признаков на вектор весов. В процессе
можно рассмотреть использование базовой модели для прогнозов (состоящей
только лишь из свободного коэффициента) либо наложение ограничений на
значения весов, например чтобы они были только положительными.
Поскольку алгоритм линейной регрессии представляет собой взвешенное суммирование, необходимо учитывать три ключевых момента.
Убедитесь, что нет пропущенных значений, так как их нельзя использовать
для умножения или сложения, если вы не приписали им какое-либо значение.
Обязательно обработайте выбросы, так как они могут повлиять на работу
алгоритма как при обучении, так и при прогнозировании.
Убедитесь, что признаки и целевая переменная связаны линейно, насколько
это возможно (то есть имеют высокую корреляцию Пирсона). Признаки,
слабо связанные с целевой переменной, как правило, просто добавляют
шум в модель, что может привести к недообучению или, при их большом
количестве, к переобучению.
Поскольку предсказание формируется как взвешенная сумма признаков, легко
определить, какой фактор оказывает наибольшее влияние на результат и какой
4.3. Классическое машинное обучение 153
вклад вносит в него каждый признак. Наблюдение за коэффициентами при
каждом признаке позволяет понять логику работы алгоритма. Такая интерпретируемость особенно ценна, когда нужно объяснить регулирующим органам
или стейкхолдерам, как работает модель, или когда вы хотите проверить, соответствуют ли прогнозы гипотезам или экспертным знаниям в конкретной
предметной области.
Однако простота интерпретации моделей регрессии таит в себе и скрытые риски.
Когда два или более признака в данных сильно коррелируют (явление, известное в статистике как мультиколлинеарность), интерпретация коэффициентов
становится намного сложнее, даже если оба признака объективно влияют на
прогноз. Обычно только один из группы коррелированных признаков получает
значимый коэффициент, тогда как другие — небольшие значения, как если бы
они не были связаны с целевой переменной. В действительности же часто верно
обратное, и относительная легкость понимания вклада признака в регрессионном прогнозе может привести к серьезному концептуальному недопониманию.
Еще одним большим преимуществом алгоритма линейной регрессии является
то, что, поскольку это всего лишь умножения и сложения, его легко реализовать на любой программной платформе, даже вручную написав код. Другие
ML-алгоритмы сложнее воспроизвести, и, следовательно, их реализация с нуля
может сопровождаться ошибками и багами. Однако, хотя это и нецелесообразно,
нужно отметить, что ручная реализация любой ML-модели может быть ценным
опытом, позволяющим глубже понять внутреннюю работу алгоритма и лучше
подготовиться к устранению неполадок и оптимизации производительности
в будущем. Для учебных целей в главе 5 мы представляем реализации с нуля
нескольких алгоритмов.
Начнем с примера сквозного применения линейной регрессионной модели к нашим данным Airbnb NYC. Пример следует схеме, предложенной на рис. 4.10,
которую мы воспроизведем для каждого представленного нами классического
алгоритма. Она основана на пайплайнах scikit-learn и функциях оценки кроссвалидации.
Схема довольно линейна. Входные данные из файла значений, разделенных запятыми, сначала проходят через ColumnTransformer, который составляет часть
подготовки данных: применяет к ним преобразование, отбрасывает их или пропускает без изменений в зависимости от названий столбцов, а затем передает
их в модель машинного обучения. Оба компонента объединяются в пайплайн,
протестированный функцией cross_validate, выполняющей кросс-валидацию
и записывающей время вычислений, обученные модели и производительность
на определенном количестве фолдов. Наконец, результаты анализируются для
демонстрации работы модели. Кроме того, мы можем получить доступ к коэффициентам и весам модели через пайплайн, чтобы узнать больше о функциональности протестированного алгоритма.
154 Глава 4. Классические алгоритмы для табличных данных
CSV-файл
с входными
данными
функция_1
функция_2
функция_...
Column Transformer
ML-модель
Пайплайн
Кросс-валидация
Вывод
результатов
Рис. 4.10. Схема того, как мы организуем примеры для классических алгоритмов
машинного обучения
Применяя такую схему, мы используем стандартную модель линейной регрессии
в листинге 4.3, поскольку этот алгоритм обычно не требует указания каких-либо
параметров. Для специальных задач, связанных с интерпретируемостью модели,
можно указать fit_intercept как false, чтобы удалить свободный член из модели
и получать все прогнозы только на основе признаков, или установить параметр
positive в значение true, чтобы получить только положительные коэффициенты.
Листинг 4.3. Линейная регрессия
from
from
from
from
sklearn.linear_model import LinearRegression
sklearn.metrics import make_scorer, mean_squared_error
sklearn.model_selection import KFold
sklearn.model_selection import cross_validate
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_passthrough, continuous)],
remainder='drop',
ColumnTransformer, преобразующий данные
verbose_feature_names_out=False,
в числовые признаки и выполняющий
sparse_threshold=0.0)
импутацию пропусков
model = LinearRegression()
Стандартная модель линейной регрессии
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)]
)
cv = KFold(5, shuffle=True, random_state=0)
rmse = make_scorer(mean_squared_error,
squared=False)1
Создание пайплайна из
ColumnTransformer и модели
Пятифолдовая кросс-валидация
со случайным разбиением
Функция вычисления
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_passthrough, continuous)],
remainder='drop',
ColumnTransformer, преобразующий данные
verbose_feature_names_out=False,
в числовые признаки и выполняющий
sparse_threshold=0.0)
пропусков
4.3.импутацию
Классическое
машинное обучение 155
model = LinearRegression()
Стандартная модель линейной регрессии
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)]
)
cv = KFold(5, shuffle=True, random_state=0)
rmse = make_scorer(mean_squared_error,
squared=False)1
Создание пайплайна из
ColumnTransformer и модели
Пятифолдовая кросс-валидация
со случайным разбиением
Функция вычисления
метрики качества на основе
среднеквадратичной ошибки
cv_scores = cross_validate(estimator=model_pipeline,
X=data[price_window],
y=target_regression[price_window],
scoring=rmse,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с., прогнозирование: {score_time:0.2f} с.")
Автоматизация кроссвалидации
Результат возвращается
в виде метрики качества,
стандартного отклонения,
времени обучения
и времени прогнозирования
Выполнение указанного кода даст следующие результаты RMSE:
33.949 (0.274) обучение: 0.06 с., прогнозирование: 0.01 с.
Это хороший результат, полученный за минимальное время (с использованием стандартного экземпляра Google Colab или Kaggle notebook), и он может
служить основой для более сложных попыток. Например, если запустить код
в листинге 4.4, то поймете, что можно получить похожие результаты с меньшим
количеством признаков, но зато тщательно подготовленных. Это называется
генерацией признаков (feature engineering), и ее преимущество в том, что можно
получить лучшие результаты или те же, но с меньшим количеством признаков, значимых для экспертов в предметной области или в бизнесе. Например,
мы создаем различные новые признаки в листинге кода, генерируя бинарные
признаки для определенных значений, комбинируя признаки и преобразуя их
с помощью логарифмической функции.
1
Выдаст ошибку в новых версиях sklearn. Свойство squared было удалено. Нужно использовать root_mean_squared_error (здесь и в следующих листингах). — Примеч. науч.
ред.
156 Глава 4. Классические алгоритмы для табличных данных
Листинг 4.4. Кастомизированная подготовка данных для линейной
регрессии
Бинарный признак, указывающий, имеет ли
neighbourhood_group значение Manhattan
Создает пустой датафрейм
data_2 = data[[]].copy()
data_2['neighbourhood_group_Manhattan'] = (
(data['neighbourhood_group']=='Manhattan')
.astype(int))
data_2['neighbourhood_group_Queens'] = (
Вычисляет натуральный
логарифм number_of_views+1
Бинарный признак, указывающий,
имеет ли room_type значение
Entire home/apt
Бинарный признак,
указывающий, имеет
ли neighbourhood_
group значение Queens
(data['neighbourhood_group']=='Queens').astype(int))
data_2['room_type_Entire home/apt'] = (
(data['room_type']=='Entire
home/apt').astype(int))
data_2['minimum_nights_log'] = np.log1p(
Вычисляет натуральный
data["minimum_nights"])
логарифм параметра
data_2['number_of_reviews_log'] = np.log1p(
minimum_nights+1
data["number_of_reviews"])
label1 = 'neighbourhood_group_Manhattan*room_type_Entire home/apt'
data_2[label1] = (
Произведение бинарных
data_2['neighbourhood_group_Manhattan'] *
признаков neighbourhood_
data_2['room_type_Entire home/apt'])
group_Manhattan и room_
label2 = 'availability_365*neighbourhood_group_Manhattan'
type_Entire home/apt
data_2[label2] = (data['availability_365'] *
data_2['neighbourhood_group_Manhattan'])
Произведение
label3 = 'availability_365*room_type_Entire home/apt'
признака
data_2[label3] = (data['availability_365'] *
availability_365
data_2['room_type_Entire home/apt'])
rmse = make_scorer(mean_squared_error, squared=False)
cv = KFold(5, shuffle=True, random_state=0)
cv_scores = cross_validate(estimator=LinearRegression(),
X=data_2[price_window],
y=target_regression[price_window],
scoring=rmse,
cv=cv,
return_train_score=True,
return_estimator=True)
и бинарного
neighbourhood_
group_Manhattan
Произведение
признака
availability_365
и бинарного room_
type_Entire home/apt
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
print(f"{mean_cv:0.5f}, {std_cv:0.5f}")
Полученное значение RMSE равно:
33.937 (0.240)
Хотя новый результат сопоставим с предыдущим, на этот раз используется
датасет с меньшим количеством признаков, которые были созданы с помощью
4.3. Классическое машинное обучение 157
определенных преобразований, таких как прямое кодирование категориальных признаков, преобразования числовых признаков с помощью специальных
функций (возведение в куб, квадрат, логарифмирование или извлечение квадратного корня) и путем умножения признаков друг на друга. Как показывает
практика, модель с меньшим количеством, но более осмысленных признаков,
созданных путем их генерации и на основе экспертных знаний, обычно более
приемлема для бизнес-пользователей, даже если она имеет сопоставимую или
немного меньшую предсказательную способность, чем модель, основанная исключительно на данных.
Перемножение признаков — это операция, характерная только для моделей
линейной регрессии. Полученный результат называется взаимодействиями
(interaction) признаков. Они создаются путем умножения двух или более признаков для получения нового. Все эти преобразования направлены на то, чтобы
сделать связь между каждым признаком и целевой переменной максимально
линейной. Хорошие результаты могут быть получены автоматически или на
основе вашего понимания данных и задачи. Применение таких преобразований к признакам типично для семейства моделей линейной регрессии. Они
оказывают незначительное или нулевое влияние на более сложные алгоритмы,
которые мы рассмотрим далее в этой и последующих главах. Инвестирование
времени в определение того, как должны быть выражены признаки, является
как преимуществом, так и недостатком моделей линейной регрессии. Однако
существуют способы автоматизации этого процесса с помощью регуляризации,
о чем мы поговорим далее.
В следующем разделе мы обсудим регуляризацию в линейных моделях (линейная и логистическая регрессии). Регуляризация — лучшее решение, когда у вас
много признаков и их взаимная мультиколлинеарность (когда два предиктора
сильно коррелируют друг с другом) не позволяет линейной модели найти лучшие коэффициенты для прогноза, поскольку они становятся нестабильными
и ненадежными — например, появляется коэффициент с неожиданным знаком
или величиной.
4.3.2. Методы регуляризации
Модели линейной регрессии обычно достаточно просты, для того чтобы люди
могли напрямую интерпретировать их как формулы с коэффициентами, применяемыми к признакам. Это означает, что в реальной задаче они могут оказаться
грубым приближением и, таким образом, систематически давать неправильные
прогнозы. Технически это модели с высоким смещением (bias). Для устранения
этого недостатка их формулы усложняют: добавляют все больше признаков и их
преобразований (логарифмирование, возведение в квадрат, извлечение корня
и т. д.), а также создают взаимодействия между признаками (посредством умножения). Таким образом можно уменьшить смещение, и модель станет более
158 Глава 4. Классические алгоритмы для табличных данных
точной. Однако в то же время дисперсия модели также возрастает, что может
привести к переобучению.
Принцип бритвы Оккама гласит, что среди конкурирующих гипотез следует
выбирать ту, которая содержит наименьшее количество предположений (https://
mng.bz/RV40). Принцип отлично работает для линейных моделей, тогда как для
нейронных сетей, применяемых к табличным данным, часто верно обратное: чем
сложнее, тем лучше. Следовательно, линейные модели должны быть максимально
простыми, но соответствовать задаче. Вот где в игру вступает регуляризация,
которая помогает снижать сложность линейной модели, пока она не станет
соответствовать задаче. Регуляризация — это метод, используемый для уменьшения переобучения в ML путем ограничения сложности модели, что улучшает
ее обобщающую способность. Регуляризация работает за счет того, что модель
линейной регрессии штрафуется при поиске оптимальных коэффициентов для
предсказаний. Штрафы вычисляются на основе суммирования коэффициентов.
Таким образом, регрессионная модель поощряется поддерживать как можно
меньшие значения коэффициентов, а в идеале обнулять их. Ограничение величины коэффициентов регрессии имеет два важных последствия.
Позволяет избежать запоминания данных и переобучения (то есть брать
определенные значения коэффициентов при наличии большого количества
признаков по сравнению с имеющимися примерами).
По мере уменьшения величин коэффициентов оценки стабилизируются,
поскольку значения коэффициентов мультиколлинеарных признаков будут
изменены или сконцентрированы только на одном из признаков.
В процессе оптимизации коэффициенты обновляются многократно, и эти шаги
называются итерациями. На каждом шаге каждый коэффициент регрессии
корректируется в направлении своего оптимального значения, которое определяется градиентом — числом, указывающим направление, которое значительно
улучшает коэффициент на этом шаге. Более подробное объяснение приведено
в конце главы. Штраф — это форма ограничения, которая заставляет веса, полученные в результате оптимизации модели, иметь определенные характеристики.
Существуют два варианта регуляризации.
Первый вариант заключается в том, что штраф рассчитывается путем суммирования абсолютных значений коэффициентов. Это называется регуляризацией L1. Она делает коэффициенты разреженными, поскольку может
свести некоторые из них к нулю, тем самым связанные с ними признаки
становятся несущественными.
Во втором варианте штраф вычисляется путем суммирования квадратов
коэффициентов. Это называется регуляризацией L2, и ее эффект обычно
заключается в уменьшении размера коэффициентов (она также вычисляется
относительно быстро).
4.3. Классическое машинное обучение 159
Регуляризация L1 (или регрессия Лассо) сводит многие коэффициенты к нулевым значениям, тем самым осуществляя неявный отбор полезных признаков
(нулевой коэффициент означает, что признак не играет никакой роли в прогнозировании). Кроме того, коэффициенты всегда смещаются к нулю с одинаковой
скоростью (технически градиенты решения всегда +1 или –1). Следовательно,
в процессе оптимизации признаки, слабо связанные с целевой переменной, быстро обнуляются и становятся нерелевантными для прогнозирования. Короче
говоря, если два или более признака мультиколлинеарны и достаточно предсказательны, при применении L1-регуляризации только один из них получит
ненулевой коэффициент.
Регуляризация L2 (или регрессия Ridge, «гребневая») работает иначе: возведение коэффициентов в квадрат предотвращает взаимное сокращение отрицательных и положительных значений в штрафе и придает больший вес большим
коэффициентам. Результатом является набор в целом меньших коэффициентов, а мультиколлинеарные признаки, как правило, имеют схожие значения
коэффициентов. Все признаки участвуют в суммировании. Важные из них
лучше заметны, потому что, в отличие от стандартной регрессии, их роль не
маскируется корреляцией с другими признаками. L2-регуляризация ослабляет
коэффициенты пропорционально на каждом шаге оптимизации: технически
градиенты, ведущие к решению, становятся все меньше и меньше. Поэтому
коэффициенты могут достигать нулевого значения или быть около него. Но
даже для полностью нерелевантных признаков это требует многих итераций
оптимизации и довольно затратно по времени. Следовательно, в случае двух
или более мультиколлинеарных признаков регуляризация L2, в отличие от L1,
которая сохраняет только один ненулевой коэффициент, присвоит всем им ненулевые коэффициенты схожего размера.
В нашем примере мы сначала создаем новые признаки посредством систематических взаимодействий между доступными признаками, а затем выполняем
регрессию со штрафом L2 и L1, чтобы сравнить их результаты и полученные
коэффициенты. PolynomialFeatures — это функция scikit-learn (https://mng.
bz/2ynd), которая автоматически многократно умножает признаки на другие
признаки и на самих себя. Процесс напоминает математическое полиномиальное
разложение, где степень суммы выражается через отдельные члены:
Scikit-learn упрощает этот процесс, поскольку при указании степени функция
автоматически создает полиномиальное разложение до этой степени. Вы можете
решить, нужно ли сохранять только взаимодействия. Такой процесс интересен
для регрессионной модели, поскольку:
взаимодействия помогают регрессионной модели лучше учитывать совместные значения большего количества признаков, так как они обычно связаны
с целевой переменной не по отдельности, а в синергии с другими;
160 Глава 4. Классические алгоритмы для табличных данных
набор показателей степени признака помогает моделировать его как кривую.
Например, a + a2 — это кривая в форме параболы.
Хотя использование полиномиального разложения позволяет избежать сложностей создания признаков, у него есть и недостаток — оно значительно увеличивает их количество. Больше признаков обычно обеспечивает бˆольшую предсказательную силу, но они также означают больше шума, мультиколлинеарности
и шансов, что модель просто запомнит примеры и переобучится. Регуляризация
может помочь устранить эту проблему с помощью штрафа L2 и выбрать только
те признаки, которые будут сохранены с помощью штрафа L1.
В коде листинга 4.5 мы тестируем применение L2, а в листинге 4.6 — регуляризацию L1 к тому же самому полиномиальному расширению. Важно отметить
результат применения каждого вида регуляризации. В первом примере мы
используем регуляризацию L2 (Ridge). Поскольку она имеет смысл, если у вас
большое число признаков, по которым строится прогноз, мы создаем новые признаки из старых с помощью полиномиального разложения. Затем для обработки
возросшего числа коллинеарных признаков в гребневой модели устанавливается
высокое значение параметра alpha.
Листинг 4.5. L2-регуляризация линейной регрессии
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import Ridge, Lasso
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_passthrough, continuous)],
remainder='drop',
Экземпляр PolynomialFeatures,
verbose_feature_names_out=False,
который производит
sparse_threshold=0.0)
полиномиальное расширение
признаков второй степени
polynomial_expansion = PolynomialFeatures(degree=2)
model = Ridge(alpha=2500.0)
Экземпляр модели гребневой
регресии с параметром
регуляризации (alpha) = 2500.0
model_pipeline = Pipeline(
[('processing', column_transform),
('polynomial_expansion', polynomial_expansion),
('standardizing', numeric_standardization),
Пайплайн с применением преобра('modeling', model)]
зования столбцов, полиномиального
)
расширения, стандартизации
и моделирования гребневой регрессии
cv = KFold(5, shuffle=True, random_state=0)
rmse = make_scorer(mean_squared_error, squared=False)
cv_scores = cross_validate(estimator=model_pipeline,
X=data[price_window],
y=target_regression[price_window],
scoring=rmse,
cv=cv,
Пятифолдовая кросс-валидация
return_train_score=True,
с использованием пайплайна,
return_estimator=True)
заданного выше, и расчет
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
среднеквадратичной ошибки
Выводит среднее значение и стандартное
)
расширения, стандартизации
и моделирования гребневой регрессии
cv = KFold(5, shuffle=True, random_state=0)
rmse = make_scorer(mean_squared_error, squared=False)
cv_scores = cross_validate(estimator=model_pipeline,
4.3. Классическое машинное обучение 161
X=data[price_window],
y=target_regression[price_window],
scoring=rmse,
cv=cv,
Пятифолдовая кросс-валидация
return_train_score=True,
с использованием пайплайна,
return_estimator=True)
заданного выше, и расчет
среднеквадратичной ошибки
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
Выводит среднее значение и стандартное
fit_time = np.mean(cv_scores['fit_time'])
отклонение RMSE, рассчитанных на
score_time = np.mean(cv_scores['score_time'])
валидационных фолдах кросс-валидации
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f} с, прогнозирование: {score_time:0.2f} с.")
Результатом работы скрипта является следующий вывод:
33.738 (0.275) обучение: 0.13 с, прогнозирование: 0.03 с.
Если подсчитать количество ненулевых коэффициентов (округлив до пяти
знаков после запятой, чтобы исключить крайне малые значения), то получим:
(cv_scores['estimator'][0]['modeling'].coef_.round(5)!=0).sum()
Девяносто один коэффициент из 105 имеет ненулевые значения.
Далее мы применим регуляризацию L1 и сравним результаты с предыдущим
примером. Процедура остается та же, что и в последнем листинге, хотя на этот
раз мы прибегаем к модели Лассо.
Листинг 4.6. L1-регуляризация линейной регрессии
model = Lasso(alpha=0.1)
Экземпляр модели Лассо с параметром
регуляризации (alpha) = 0.1
model_pipeline = Pipeline(
[('processing', column_transform),
('polynomial_expansion', polynomial_expansion),
('standardizing', numeric_standardization),
Пайплайн с применением преобра('modeling', model)]
зования столбцов, полиномиального
)
расширения, стандартизации и моде-
лирования с помощью Лассо-регрессии
cv = KFold(5, shuffle=True, random_state=0)
rmse = make_scorer(mean_squared_error, squared=False)
cv_scores = cross_validate(estimator=model_pipeline,
X=data[price_window],
y=target_regression[price_window],
scoring=rmse,
Пятифолдовая кросс-валидация
cv=cv,
с использованием заданного
return_train_score=True,
пайплайна и расчет оценок RMSE
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
Выводит среднее значение и стандартное
fit_time = np.mean(cv_scores['fit_time'])
отклонение RMSE, рассчитанных на
score_time = np.mean(cv_scores['score_time'])
валидационных фолдах кросс-валидации
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f} с, прогнозирование: {score_time:0.2f} с.")
162 Глава 4. Классические алгоритмы для табличных данных
Полученный результат:
33.718 (0.269) обучение: 0.64 с, прогнозирование: 0.03 с.
Если мы проверим, сколько коэффициентов имеют ненулевые значения, взяв
первую модель, созданную в цикле кросс-валидации, то на этот раз их окажется
меньше:
(cv_scores['estimator'][0]['modeling'].coef_.round(5) !=0).sum()
При 53 ненулевых коэффициентах количество рабочих коэффициентов сократилось вдвое. Увеличивая параметр alpha вызова Лассо, мы можем получить
еще более резкое сокращение используемых коэффициентов, хотя и ценой более
высокого времени вычислений. Существует золотая середина, после которой
применение более высокого штрафа L1 не улучшает результаты предсказаний.
Для целей прогнозирования придется найти правильное значение alpha методом
проб и ошибок или с помощью удобных автоматических функций, таких как
LassoCV (https://mng.bz/1XoV) или RidgeCV (https://mng.bz/Pdn9).
Интересно, что регуляризация также используется в нейронных сетях, которые
применяют последовательные матричные умножения на основе матриц коэффициентов для перехода от признаков к прогнозам, что является расширением
принципа работы линейной регрессии. Однако структура нейронных сетей более
сложная, хотя с точки зрения матричных вычислений они напоминают регрессионную модель. Учитывая схожесть механизмов, для вашей задачи по анализу
табличных данных может быть полезно использовать архитектуру глубокого
обучения с применением штрафа L2, чтобы ослабить и распределить коэффициенты сети, и/или штрафа L1, чтобы коэффициенты стали разреженными
и многие из них обнулились. В следующем разделе мы продолжим обсуждение
линейных моделей, разбираясь, как решить задачу классификации.
4.3.3. Логистическая регрессия
Модель линейной регрессии может быть эффективно расширена для решения
задач классификации. В случае бинарной классификации (с двумя классами — положительным и отрицательным) используется тот же подход, что
и в регрессии (матрица признаков, вектор коэффициентов, смещение). Однако
целевая переменная преобразуется с помощью функции логита (подробнее
об этом статистическом распределении см. https://mng.bz/JY20). Эта преобразующая функция называется функцией связи (link function). С точки зрения
оптимизации алгоритм использует условное распределение Бернулли (для
повторения этой темы см. https://mng.bz/wJoq) вместо нормального распределения. В результате получаются выходные значения в диапазоне от 0 до 1,
представляющие вероятность принадлежности образца к положительному
4.3. Классическое машинное обучение 163
классу. Такой метод называется логистической регрессией. Это интуитивно
понятный и практичный способ решения задач бинарной классификации,
а также многоклассовых задач.
В листинге 4.7 мы воспроизводим тот же подход, что и в линейной регрессии, —
на этот раз пытаясь построить модель, которая предсказывает, превышает ли
значение целевой переменной медиану. Обратите внимание, что преобразования
признаков остаются теми же, хотя на этот раз мы используем модель логистической регрессии. Наша целевая переменная — это класс, который сообщает,
находится ли целевое значение выше медианы. Такая целевая переменная — это
бинарный сбалансированный результат, где половина меток положительные,
а половина — отрицательные.
Листинг 4.7. Логистическая регрессия
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import make_scorer, accuracy_score
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
model = LogisticRegression(solver="saga",
penalty=None,
max_iter=1_000)
Экземпляр модели логистической
регрессии: максимум 1000 итераций
с решателем «saga», без штрафа
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_standardization, continuous)],
remainder='drop',
verbose_feature_names_out=False,
sparse_threshold=0.0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
ColumnTransformer, применяющий one-hotкодирование к категориальным признакам
и стандартизацию к числовым
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
Пайплайн с последовательным
применением преобразования
столбцов и логистической регрессии
Пятифолдовая кросс-валидация
с использованием заданного пайплайна и расчетом оценок accuracy
mean_cv = np.mean(cv_scores['test_score'])
Выводит среднее значение и стандартное
std_cv = np.std(cv_scores['test_score'])
отклонение доли верных результатов
fit_time = np.mean(cv_scores['fit_time'])
(accuracy), рассчитанных на валидационных
score_time = np.mean(cv_scores['score_time'])
фолдах кросс-валидации
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f} с, прогнозирование: {score_time:0.2f} с.")
164 Глава 4. Классические алгоритмы для табличных данных
Результатом работы скрипта являются следующие оценки:
0.821 (0.004) обучение: 3.00 с, прогнозирование: 0.02 с.
Поскольку обработка признаков остается неизменной, мы сосредоточимся на
том, как логистическая регрессия учитывает некоторые специфические параметры по сравнению с линейной регрессией. В частности, вы можете установить
штраф напрямую, не меняя алгоритм, и выбрать используемый оптимизатор
(с помощью параметра solver). Каждый оптимизатор поддерживает определенные типы штрафов и может быть более или менее эффективным в зависимости
от характеристик данных:
lbfgs для L2 или без штрафа.
liblinear для штрафов L1 и L2 — лучше подходит для небольших датасетов,
ограничивается схемой «один против остальных» для многоклассовых задач.
newton-cg для L2 или без штрафа.
newton-cholesky для L2 или без штрафа.
sag для L2 или без штрафа — идеально для больших датасетов. Должны
быть стандартизированные признаки (или признаки со схожим масштабом /
стандартным отклонением).
saga без штрафов, штрафы L1, L2, elasticnet (смесь штрафов L1 и L2) —
идеально подходит для больших датасетов, требует стандартизированных
признаков (или признаков со схожим масштабом / стандартным отклонением).
В листинге 4.8 мы используем L2-регуляризацию для многоклассовой целевой
переменной, чтобы проверить, как легко справляться с несколькими из них при
помощи параметра multi_class, установленного на «ovr» (one-versus-rest, один
против всех). Это решение преобразует многоклассовую задачу в несколько
бинарных для каждого из классов, которые нужно предсказать. На этапе прогнозирования вероятности нормализуются так, чтобы их сумма равнялась 1.0,
и выбирается класс с наибольшей вероятностью. Такой подход аналогичен
функции softmax в нейронных сетях, которая преобразует вектор произвольных
значений в вероятностное распределение, где сумма всех элементов равна 1
(более подробное объяснение softmax см. на https://mng.bz/qxYw). Альтернативой
подходу one-versus-rest является полиномиальный вариант, где одна регрессионная модель напрямую моделирует распределение вероятностей по всем
классам одновременно.
Полиномиальный подход предпочтительнее, когда важны взаимосвязи между
классами (например, для ранжирования или принятия решений на основе уверенности) или когда требуется компактное решение на основе одной модели.
4.3. Классическое машинное обучение 165
Листинг 4.8. L2-регуляризованная многоклассовая линейная регрессия
Экземпляр модели логистической регрессии
со штрафом L2, регуляризацией C=0.1,
solver = «sag», многоклассовой стратегией «ovr»
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import make_scorer, accuracy_score
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
model = LogisticRegression(penalty="l2", C=0.1, solver="sag",
multi_class="ovr", max_iter=1_000)
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_standardization, continuous)],
remainder='drop',
verbose_feature_names_out=False,
ColumnTransformer, применяющий one-hotsparse_threshold=0.0)
кодирование к категориальным признакам
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
и стандартизацию к числовым
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_multiclass,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Пайплайн с применением
преобразования столбцов
и логистической регрессии
Кросс-валидация с использованием
заданного пайплайна и расчет
оценок accuracy
Выводит среднее значение и стандартное отклонение доли верных
результатов (accuracy), рассчитанных на валидационных фолдах
кросс-валидации
Предсказать целевую переменную как класс, безусловно, сложнее, чем угадать,
превышает ли целевая цена пороговое значение или нет:
0.435 (0.002) обучение: 31.08 с, прогнозирование: 0.02 с.
Важно отметить, что время обучения для фолда кросс-валидации увеличилось
в 10 раз. Причина в том, что применение штрафа к коэффициентам требует
больших усилий для оптимизации алгоритма для достижения стабильного
результата и потому что теперь создается модель для каждого класса. Просто
запомните, что для L2-регуляризации нужны длительные вычисления, и еще
166 Глава 4. Классические алгоритмы для табличных данных
более длительные для L1. Настроив параметр max_iter, вы можете ограничить
количество итераций, но результат, который вы получите, сократив время, необходимое для сходимости алгоритма, не будет гарантированно лучшим.
4.3.4. Обобщенные линейные методы
Идея расширения линейной регрессии до бинарной классификации с помощью
логит-преобразования может быть применена к распределениям, отличным
от распределения Бернулли. Необходимость в этом возникает, когда целевая
переменная представляет собой категориальные, счетные или другие данные,
распределение которых заведомо не является нормальным. Как мы видели
в предыдущем разделе, многоклассовые задачи можно моделировать с использованием распределения Бернулли (стратегия «один против всех» с применением
множественных логистических регрессий) и мультиномиального распределения.
Другие задачи, более типичные для таких областей, как финансы или страхование, требуют иных подходов. Например, на сайте scikit-learn упоминаются несколько реальных применений и наиболее подходящие для них распределения
(для справки см. https://mng.bz/7py9).
Моделирование климата — количество осадков в год (распределение Пуассона для счетных данных и дискретных событий). Распределение Пуассона
используется для моделирования таких событий, как количество звонков
в кол-центр или количество посетителей ресторана. Для моделирования
количества осадков за одно событие используется гамма-распределение,
удобное для моделирования из-за своей асимметрии и длинного хвоста, а для
общего количества осадков в год — распределение Твиди, которое является
композицией гамма- и пуассоновского распределений.
Моделирование рисков или ценообразование страховых полисов — количество
страховых случаев или страхователей в год (Пуассон), стоимость события
(гамма), общая стоимость на страхователя в год (Твиди).
Предиктивное техническое обслуживание — количество событий простоя
в производстве за год (Пуассон), продолжительность простоя (гамма) и общее
время простоев в год (Твиди).
На рис. 4.11 показаны три распределения — Пуассона, Твиди и гамма — для
разных средних значений. Распределение Твиди рассчитано для степени 1.5 —
это смесь пуассоновского и гамма-распределений.
Конечно, в подобных ситуациях вы можете попробовать любое распределение,
даже простую регрессионную модель. Однако подход с использованием соответствующей обобщенной линейной модели, которая оптимизирует конкретное
распределение, гарантирует наилучший результат в большинстве случаев.
4.3. Классическое машинное обучение 167
Распределение Пуассона
Распределение Твиди (степень = 1.5)
Распределение Пуассона
Y
PDF(Y)
PDF(Y)
PDF(Y)
μ = 0.5
μ = 1.0
μ = 3.0
μ = 5.0
Y
Y
PDF(Y) — плотность распределения случайной величины Y
Рис. 4.11. Сравнение распределений Пуассона, Твиди и гамма при различных
значениях среднего (μ)
Мы не будем вдаваться в подробности каждого распределения; просто нужно
помнить, что палочкой-выручалочкой обобщенных линейных моделей является
TweedieRegressor (https://mng.bz/mGOr). Эта реализация scikit-learn в зависимости
от показателя степени поможет быстро проверить распределения: нормальное
(обычная регрессия), Пуассона (https://mng.bz/4a4w), гамма (https://mng.bz/QDvG),
обратное гауссово (для неотрицательных данных с положительным смещением)
и смесь гаммы и Пуассона (распределение Твиди) (табл. 4.1).
Таблица 4.1. Показатель степени и соответствующие статистические распределения
Значение
Закон распределения
0
Нормальное
1
Пуассон
1.2
Пуассон и гамма
2
Гамма
3
Обратное гауссово
В листинге 4.9 мы тестируем различные распределения, предлагаемые
TweedieRegressor, на полном распределении цен датасета Airbnb NYC — ранее
мы избегали такого моделирования из-за тяжелых хвостов, выявленных на
этапе EDA. Мы проверяем каждое из этих распределений одно за другим на
всем диапазоне значений цен, поскольку уверены, что использование специализированных распределений решит проблему целевой переменной с тяжелыми
168 Глава 4. Классические алгоритмы для табличных данных
хвостами. Важно помнить, что такие распределения имеют ограничения из-за
их математической природы:
нормальное — любые значения;
Пуассон — нулевые или положительные значения;
Твиди, гамма, обратное гауссово — только ненулевые положительные значения.
Это подразумевает, что требуется адаптировать данные, если есть отрицательные
или нулевые значения, добавив значение смещения (offset). Следовательно,
в зависимости от смоделированного распределения мы ограничиваем целевые
значения нижней границей на основе вышеупомянутых ограничений.
Листинг 4.9. Регрессия Твиди
from
from
from
from
sklearn.linear_model import TweedieRegressor
sklearn.metrics import make_scorer, mean_squared_error
sklearn.model_selection import KFold
sklearn.model_selection import cross_validate
experiments = [
['normal', 0, float('-inf')],
['poisson', 1, 0.0],
['tweedie', 1.5, 0.1],
['gamma', 2, 0.1],
['inverse gaussian', 3, 0.1]]
Список экспериментов, составленный из
названия распределения, показателя степени
и минимального целевого значения
Просматривает список экспериментов
с названиями распределений
и показателем степени
for experiment, power, min_val in experiments:
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_standardization, continuous)],
remainder='drop',
verbose_feature_names_out=False,
sparse_threshold=0.0)
model = TweedieRegressor(power=power,
max_iter=1_000)
Экземпляр модели TweedieRegressor
с показателем степени для текущего
эксперимента
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
cv = KFold(5, shuffle=True, random_state=0)
rmse = make_scorer(mean_squared_error, squared=False)
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_regression.clip(
lower=min_val),
scoring=rmse,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
Сужает целевые
данные регрессии до
минимального значения
согласно используемому
распределению
('modeling', model)])
cv = KFold(5, shuffle=True, random_state=0)
rmse = make_scorer(mean_squared_error, squared=False)
Сужает целевые
данные регрессии до
cv_scores = cross_validate(estimator=model_pipeline,
4.3. Классическое машинное
обучение
169
минимального
значения
X=data,
согласно используемому
y=target_regression.clip(
распределению
lower=min_val),
scoring=rmse,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{experiment:18}: {mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Выводит на экран название
эксперимента вместе
с результатами кроссвалидации
В результате наилучшим и наиболее подходящими распределениями являются
распределения Пуассона и Твиди со степенью 1.5:
normal : 233.858 (15.826) обучение: 0.13 с, прогнозирование: 0.03 с.
poisson: 229.189 (16.075) обучение: 0.66 с, прогнозирование: 0.03 с.
tweedie: 229.607 (16.047) обучение: 0.46 с, прогнозирование: 0.03 с.
gamma : 233.991 (15.828) обучение: 0.22 с, прогнозирование: 0.03 с.
inverse gaussian: 239.577 (15.453) обучение: 0.18 с, прогнозирование: 0.03 с.
Важно помнить, что секрет производительности обобщенных линейных моделей
заключается в конкретном распределении, которое они стремятся смоделировать на этапе оптимизации. Столкнувшись с аналогичными задачами, можно
прибегнуть к похожим распределениям в некоторых более продвинутых алгоритмах, чем обобщенные линейные модели, в частности в реализациях градиентного бустинга, таких как XGBoost или LightGBM, которые будут обсуждаться
в следующей главе. Далее мы познакомимся с другим подходом, связанным
с большими датасетами.
4.3.5. Обработка больших датасетов с помощью
стохастического градиентного спуска
Когда табличный датасет не помещается в память системы — будь то облачный
экземпляр или настольный компьютер, — возможности для моделирования
сужаются. Помимо решений в области глубокого обучения, которые будут
обсуждаться в третьей части этой книги, существует еще один вариант с использованием классического машинного обучения — прибегнуть к внеядерному
обучению (out-of-core learning), при котором данные остаются в хранилище
(например, в вашем хранилище данных), а модель обучается на них по частям,
используя небольшие выборки, называемые батчами (batch), или пакетами
данных. Это реализуемо на практике, поскольку современные хранилища обеспечивают доступ к конкретным выборкам данных, хотя и с некоторой задержкой —
временнˆым интервалом между началом операции с данными и ее завершением
или получением ответа. Кроме того, существуют инструменты для обработки
170 Глава 4. Классические алгоритмы для табличных данных
данных на лету (например, Apache Kafka или Amazon Kinetics), которые могут
перенаправлять данные в алгоритмы внеядерного обучения.
С алгоритмической точки зрения это также выполнимо благодаря свойствам
моделей линейной/логистической регрессии. Обе они состоят из сумм коэффициентов, соответствующих признакам, которые используются для обучения.
Внеядерное обучение включает в себя сначала оценку этих коэффициентов
на основе небольших выборок, а затем их обновление с использованием все
большего количества батчей, извлеченных из данных. В конце концов, хотя
этот процесс и долгий, окончательный рассчитанный коэффициент не будет
сильно отличаться от того, который вы получили бы, если бы могли загрузить
все данные в память.
Сколько таких батчей потребуется использовать для внеядерного моделирования
и нужно ли использовать их многократно — вопрос эмпирического исследования:
это зависит от задачи и от имеющихся данных. Предоставление новых батчей
с ранее не встречавшимися данными может просто продлить фазу обучения,
а повторное использование тех же батчей — привести к переобучению. К сожалению, в большинстве ситуаций нужно многократно повторять итерации
на одних и тех же батчах, потому что обучение вне оперативной памяти не так
просто провести, как оптимизацию; оно занимает много времени, и вам может
потребоваться больше проходов по одним и тем же данным, даже если речь
идет об огромных их объемах. Но к счастью, вы можете положиться на методы
регуляризации, такие как L1 и L2, чтобы избежать переобучения.
В листинге 4.10 мы возвращаемся к примеру с логистической регрессией и адаптируем его для внеядерного обучения. Сначала разделяем наши данные на
обучающий и тестовый наборы, поскольку создать процедуру кросс-валидации
для внеядерного обучения довольно сложно. В реальных условиях обучения
вне оперативной памяти кросс-валидация не просто сложна, но и часто невыполнима, поскольку примеры зачастую обрабатываются только один раз. Они
передаются потоком из источников и могут отбрасываться. Обычная стратегия
валидации заключается в сборе примеров для тестирования или использования
каждого n-го батча в качестве тестового для проверки на незнакомых (out-ofsample) данных. Мы же предпочтем сразу выделить отдельный тестовый набор.
Листинг 4.10. Внеядерное обучение логистической регрессии
с регуляризацией L2
from
from
from
from
sklearn.linear_model import SGDClassifier
sklearn.model_selection import train_test_split
sklearn.utils import gen_batches
sklearn.metrics import accuracy_score
def generate_batches(X,
y,
Определяет функцию для генерации
batch_size,
батчей обучающих данных
random_state):
"""разбиваем датасет на батчи"""
examples = len(X)
Генерирует батчи индексов
batches = gen_batches(n=examples,
данных для обработки
batch_size=batch_size)
sequence = np.arange(examples)
from
from
from
from
sklearn.linear_model import SGDClassifier
sklearn.model_selection import train_test_split
sklearn.utils import gen_batches
sklearn.metrics import accuracy_score
4.3. Классическое машинное обучение 171
def generate_batches(X,
y,
Определяет функцию для генерации
batch_size,
батчей обучающих данных
random_state):
"""разбиваем датасет на батчи"""
examples = len(X)
Генерирует батчи индексов
batches = gen_batches(n=examples,
данных для обработки
batch_size=batch_size)
sequence = np.arange(examples)
Перемешивает примеры, если
if random_state:
указано случайное начальное число
np.random.seed(random_state)
np.random.shuffle(sequence)
for batch in batches:
items = sequence[batch]
yield(X.iloc[items], y.iloc[items])
model = SGDClassifier(loss="log_loss",
average=True,
penalty='l2',
alpha=0.001)
Возвращает батчи входных
признаков и соответствующих меток
Создает экземпляр модели SGDClassifier
с логистической функцией потерь, усреднением,
штрафом L2 и альфа-регуляризацией
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_standardization, continuous)],
remainder='drop',
verbose_feature_names_out=False,
Разделяет данные и целевую переменную на
sparse_threshold=0.0)
обучающий и тестовый наборы в соотношении 80:20
X_train, X_test, y_train, y_test = train_test_split(data, target_median,
test_size=0.20,
random_state=0)
iterations = 10
Проходит по батчам обучающих данных,
обучая column_transform на первом батче
for j in range(iterations):
generator = generate_batches(X_train, y_train, batch_size=256,
random_state=j)
for k, (Xt, yt) in enumerate(generator):
if k == 0:
Использует метод
column_transform.fit(Xt)
partial_fit для обучения
Xt = column_transform.transform(Xt)
модели на первом батче
if k == 0:
с указанием классов
model.partial_fit(Xt, yt, classes=(0, 1))
else:
model.partial_fit(Xt, yt)
Использует метод
predictions = model.predict(column_transform.transform(X_test))
score = accuracy_score(y_true=y_test, y_pred=predictions)
print(f"Accuracy на тестовых данных: {score:0.3f}")
Выводит на экран долю верных
результатов на тестовых данных
partial_fit для
дальнейшего
обучения модели
на последующих
батчах
172 Глава 4. Классические алгоритмы для табличных данных
Затем обучающие данные разделяются на несколько батчей, и каждый из них
предлагается алгоритму стохастического градиентного спуска (stochastic gradient
descent, SGD). Это не самостоятельный алгоритм, а процедура оптимизации
для линейных моделей, оптимизирующая их веса путем итеративного обучения
на нескольких батчах данных или даже на отдельных примерах. Он основан на
оптимизации градиентного спуска и также используется в глубоком обучении.
Градиентный спуск начинается с начального предположения для весов модели
и вычисления ошибки. На следующем шаге вычисляется градиент ошибки, который получается путем взятия отрицательного значения вектора, содержащего
частные производные ошибки по весам модели. Поскольку градиент можно
интерпретировать как направление наискорейшего спуска по поверхности
ошибки, распространенной аналогией для градиентного спуска является спуск
с горных вершин в самую низкую долину по самому крутому пути вниз. «Горы»
в этой аналогии представляют поверхность ошибки, а «самая низкая долина» —
минимум функции ошибки. Рисунок 4.12 наглядно представляет этот процесс
в виде постепенного спуска от случайного высокого места до самой низкой точки
в чашеобразной кривой ошибок.
Классическое машинное обучение
Ошибка
Случайная
начальная
точка
Глобальный
минимум
Веса
Рис. 4.12.
Оптимизация
градиентного спуска
в действии на
простом ландшафте
оптимизации
Помимо аналогии, важно помнить, что градиент определяет, как следует корректировать веса, чтобы уменьшить ошибку на этом шаге. С помощью повторных
итераций ошибку можно минимизировать путем корректировки весов модели.
Однако то, как обновляются веса, может существенно повлиять на результат.
Если обновления слишком большие, алгоритм будет делать слишком большие
шаги, это может привести к тому, что модель промахнется мимо цели и поднимется по кривой ошибок. В худшем случае это приведет к непрерывному
ухудшению ошибки без возможности восстановления. И наоборот, меньшие
шаги, как правило, безопаснее, но могут быть вычислительно затратными. Размер таких шагов определяется скоростью обучения — параметром, который
регулирует, как выполняются обновления.
4.3. Классическое машинное обучение 173
Линейные модели можно легко оптимизировать с помощью градиентного спуска,
поскольку их поверхность ошибок проста и имеет форму чаши. Однако более
продвинутые модели, такие как градиентный бустинг (который будет рассмотрен в следующей главе) и архитектуры глубокого обучения, могут столкнуться
с трудностями оптимизации из-за их более высокой сложности, взаимосвязанных
параметров и замысловатого ландшафта ошибок. В зависимости от начальной
точки эти модели могут застрять в локальном минимуме или плато во время
оптимизации, что приведет к неоптимальным результатам (рис. 4.13).
Ошибка
Локальный
минимум
Глобальный
минимум
Плато
Веса
Рис. 4.13.
Градиентный
спуск в сложном
ландшафте ошибок,
показывающий,
как локальные
минимумы и плато
могут приводить
к неоптимальным
решениям
Обучение линейной модели с помощью SGD стало возможным благодаря методу
partial_fit библиотеки scikit-learn, который после информированного старта
(алгоритму необходимо знать целевые метки) может обучаться, используя
один батч за другим. Одна и та же процедура повторяется многократно — это
называется итерациями или эпохами — для повышения качества обучения, хотя
слишком частое повторение одних и тех же примеров может вызвать переобу
чение. Алгоритм будет видеть одни и те же примеры, хотя и в разном порядке,
и каждый раз обновлять свои коэффициенты. Чтобы избежать резких изменений,
которые возможны при наличии выброса в батче, обновленные коэффициенты
не заменяют существующие, а усредняются с ними.
После завершения всего процесса обучения вы получите следующий результат:
Accuracy на тестовых данных: 0.818
Результат вполне сопоставим с логистической регрессией, обученной в памяти.
Внеядерное обучение, хотя и ограничено только простейшими алгоритмами,
такими как линейная или логистическая регрессии, является эффективным способом обучения на табличных данных, когда образцов слишком много и они не
помещаются в памяти. Все решения глубокого обучения также используют идею
пакетного потока. Этот вопрос будет снова обсуждаться в главах, посвященных
глубоким нейронным сетям, вместе со стратегиями типа ранней остановки —
метода прерывания итераций по данным, чтобы избежать переобучения из-за
174 Глава 4. Классические алгоритмы для табличных данных
чрезмерного воздействия на алгоритм примеров, показанных в предыдущих
итерациях.
Теперь мы можем предположить, что фундаментальный принцип таких стратегий обучения — это рандомизация порядка примеров. Поскольку оптимизация
является итеративным процессом, наличие специфического порядка в данных
может привести к смещению в процессе оптимизации и, как следствие, к неоптимальным результатам обучения. Повторение одних и тех же батчей в одинаковом порядке может негативно повлиять на результаты, следовательно,
рандомизация порядка очень важна. Однако еще одним значимым моментом
в SGD является этап подготовки данных, на котором необходимо масштабировать признаки, поскольку процесс оптимизации чувствителен к их масштабу,
а также провести все вычисления по генерации признаков и их взаимодействиям и настроить его максимально детерминированным образом. Использовать
глобальные параметры, такие как максимум/минимум или среднее значение
и стандартное отклонение признака, может быть сложно, если данные разделены на несколько батчей.
4.3.6. Выбор алгоритма
В качестве общего практического правила следует учитывать, что алгоритмы
машинного обучения масштабируются по-разному в зависимости от количества строк и столбцов. Исходя из количества доступных строк, при работе
с примерно 102 или меньшим их количеством необходимо использовать только
простые алгоритмы на основе правил или статистики. Для объемов до 103 строк
лучше всего подходят модели, основанные на линейных комбинациях, такие как
линейная и логистическая регрессии, поскольку они, как правило, не переобу
чаются на небольшом количестве данных. Обычно невозможно предугадать,
какой алгоритм будет работать лучше с количеством строк примерно от 103 до
104–105. Следовательно, все это вопрос тестирования и экспериментирования.
Здесь решения глубокого обучения могут превзойти другие только в том случае, если существует некая структура, которую можно использовать, например
упорядоченная последовательность или иерархия данных. Если строк до 109,
то решения из семейства градиентного бустинга, вероятно, являются наиболее
эффективными. Опять же, такие методы, как обучение вне оперативной памяти,
гораздо лучше подходят для определенных задач, например в рекламной отрасли. В рекламе существует множество фиксированных взаимодействий, которые
нужно оценить, например между устройствами отображения, веб-сайтами и рекламными объявлениями.
Внеядерное обучение — часто используемая стратегия: вместо того чтобы обу
чаться на основе всех данных сразу, алгоритмы учатся постепенно на меньших
выборках — батчах — или даже на отдельных примерах одним за другим, что
Итоги 175
также известно как онлайн-обучение, или обучение в реальном времени. Наконец, по нашему опыту, в ситуациях с датасетами, превышающими 109 строк,
глубокие нейросети и некоторые алгоритмы внеядерного обучения, как правило,
работают лучше, поскольку могут эффективно справляться с таким объемом
данных, тогда как другие модели будут обучаться на подвыборках или потребуют
других не самых оптимальных решений.
Что касается столбцов, некоторые алгоритмы плохо масштабируются на датасетах с большим количеством столбцов, особенно если данные являются разреженными, то есть содержат много бинарных признаков. Чем разреженнее датасеты
(это можно измерить процентом нулевых значений по отношению к общему
количеству числовых значений в наборе данных), тем больше вероятность, что
вам придется применять алгоритмы онлайн- или глубокого обучения.
Однако, помимо вопросов масштабируемости, связанных с памятью и вычислительной сложностью, каждое решение машинного обучения также отвечает
различным требованиям в части управления моделью, открытости и интерпретируемости решения. Таким образом, разнообразие потребностей и моделей
машинного обучения опровергает идею о существовании одного наилучшего
алгоритма, который подходит для всех задач. Другими словами, недостаточно
просто поэкспериментировать с большим числом моделей, потому что «не бывает бесплатных обедов» (no free lunch), как утверждается в известной теореме
Дэвида Волперта (David Wolpert) и Уильяма Макреди (William Macready) (см.
http://www.no-free-lunch.org для получения более подробной информации). Чаще,
чем можно ожидать, бывает так, что менее популярный алгоритм неожиданно
превосходит признанного лидера. Необходимость в большем количестве алгоритмов в основном продиктована тем, что по мере изменения взгляда на проблему
вам могут понадобиться разные инструменты для ее решения, как художнику
на разных этапах создания шедевра требуются разные кисти.
В следующей главе мы представим более мощный класс алгоритмов машинного
обучения — ансамбли и, наконец, семейство градиентного бустинга и его успешные и популярные реализации, такие как XGBoost и LightGBM.
Итоги
Выбор алгоритма машинного обучения зависит от нескольких факторов:
количества примеров и признаков, требуемой производительности, скорости
прогнозирования и интерпретируемости. В качестве общего практического
правила:
Статистическое машинное обучение подходит для датасетов с небольшим
количеством наблюдений.
176 Глава 4. Классические алгоритмы для табличных данных
Классическое машинное обучение подходит для датасетов с умеренным
количеством наблюдений.
Алгоритмы градиентного бустинга особенно эффективны для датасетов
с умеренным и большим количеством наблюдений.
Решения на основе глубокого обучения целесообразны и наиболее эффективны для датасетов с огромными объемами данных.
Scikit-learn — это библиотека с открытым исходным кодом для машинного
обучения, которая предлагает широкий спектр моделей для классификации
и регрессии, а также функции для кластеризации, снижения размерности,
предварительной обработки и выбора моделей. Перечислим ее основные
преимущества:
единый API для всех моделей;
поддержка обучения в оперативной памяти и вне ее;
поддержка работы с pandas DataFrames;
идеально подходит для табличных задач;
простота установки;
обширная документация.
Линейная регрессия представляет собой сумму взвешенных признаков,
преобразованных в числовые значения (one-hot-кодирование для категориальных признаков):
Алгоритм находит оптимальные значения весов (коэффициенты) для
минимизации остаточной суммы квадратов между целевыми значениями
и прогнозами.
Линейную регрессию легко объяснить и понять, как каждый признак
влияет на конечный результат.
Высокая корреляция между признаками (мультиколлинеарность) может
привести к неправильной интерпретации модели.
Линейная регрессия проста в вычислительном отношении и легко реализуема.
Линейная регрессия ограничена в возможности решения сложных задач
с нелинейными данными, если признаки не были подготовлены заранее
с помощью методов их генерации (например, не были созданы полиномиальные признаки).
Регуляризация используется для предотвращения переобучения путем
снижения сложности регрессионной модели и улучшения ее способности
к обобщению. Существуют два типа регуляризации:
Итоги 177
L1 (или регрессия Лассо) сводит многие коэффициенты к нулевым значениям, тем самым делая некоторые признаки нерелевантными для модели;
L2 уменьшает величину коэффициентов.
Регуляризация L1 полезна для отбора признаков, в то время как L2 снижает
переобучение при работе с большим их количеством и ускоряет вычисления.
Линейную регрессию можно расширить до задач классификации, используя
логит-функцию для преобразования целевой переменной и распределение
Бернулли для оптимизации алгоритма. Это приводит нас к модели логистической регрессии, которую можно использовать для задач бинарной,
многоклассовой (multiclass) и многометочной (multilabel) классификации.
Логистическая регрессия проста в реализации и понимании, но имеет те же
ограничения, что и линейная.
Такой же подход преобразования целевой переменной может быть применен
и к другим распределениям (например, Пуассона или гамма), в зависимости от природы данных. Полученные обобщенные линейные модели могут
использоваться в моделировании климата, оценке рисков и предиктивном
обслуживании. Однако важно отметить, что результаты могут быть неоптимальными без надлежащего понимания специфики распределения, применяемого в каждой ситуации.
5
Деревья решений
и градиентный бустинг
В этой главе
3 Деревья решений и их ансамбли
3 Деревья решений с градиентным бустингом
3 Варианты деревьев решений с градиентным бустингом в scikitlearn
3 Алгоритм XGBoost и его новаторство
3 Как работает алгоритм LightGBM
До сих пор мы рассматривали алгоритмы машинного обучения, основанные на
линейных моделях, поскольку они способны как решать табличные задачи на
небольших наборах данных, так и масштабироваться для работы с миллионами
строк и столбцов. Линейные модели быстро обучаются и делают прогнозы. Они
относительно просты для понимания, интерпретации и настройки. Они также
удобны тем, что к ним можно применять регуляризации L1 и L2 и градиентный
спуск, которые мы будем использовать далее в книге.
В этой главе обсудим другой классический алгоритм машинного обучения — деревья решений. Они являются основой ансамблевых моделей, таких как случайные
леса и бустинг. Мы уделим особое внимание ансамблям, градиентному бустингу
5.1. Знакомство с алгоритмами на основе деревьев 179
и его реализациям eXtreme Gradient Boosting (XGBoost) и Light Gradient Boosted
Machines (LightGBM), которые считаются передовыми решениями для задач
анализа табличных данных.
5.1. Знакомство с алгоритмами на основе деревьев
Модели на основе деревьев представляют собой семейство ансамблевых алгоритмов различных видов и являются предпочтительными методами обработки
таблиц из-за высокой производительности и низких требований к предварительной обработке данных. Ансамблевые алгоритмы — это наборы ML-моделей,
которые совместно участвуют в формировании единого прогноза. Все древовидные ансамблевые модели основаны на популярном алгоритме, появившемся
еще в 1960-х годах, — деревьях решений. Основная идея, независимо от того,
используются они для классификации или регрессии, заключается в том, что
можно разделить обучающий набор на подмножества, благодаря чему прогноз
станет более точным из-за преобладающего выходного класса (в задаче классификации) или сниженной вариативности целевых значений (так как все они
очень близки; это относится к задаче регрессии).
На рис. 5.1 показана схема ключевых элементов, составляющих дерево решений. Задача, которую решает дерево, — классифицировать животное на основе
количества его ног и глаз. Вы начинаете с корня дерева, который соответствует
всему имеющемуся датасету, и задаете условие для разбиения. Оно может
быть либо истинным, либо ложным — это так называемое бинарное разбиение.
Тем не менее некоторые варианты деревьев решений допускают применение
нескольких условий в одном и том же узле, что приводит к нескольким разбиениям, каждое из которых основано на другом значении или метке признака.
Каждая ветвь ведет к другому узлу, где может быть применено новое условие,
или к конечному узлу (листу), который используется для прогнозов на основе
экземпляров, попадающих в него.
Разбиение происходит на основе целенаправленного поиска среди признаков
и их наблюдаемых значений. В качестве критерия разбиения в задаче классификации алгоритм деревьев решений ищет наилучшую комбинацию признака
и его значения, которая разделяет данные на подмножества с однородной целевой
переменной. Однородность целевой переменной в подмножестве обычно оценивается при помощи энтропии, прироста информации или коэффициента Джини.
Энтропия измеряет степень беспорядка или случайности в распределении
меток в подмножестве.
Прирост информации, полученный из энтропии, измеряет снижение неопределенности в метках данных, что достигается путем разбиения данных
на основе определенного признака.
180 Глава 5. Деревья решений и градиентный бустинг
Коэффициент Джини измеряет вероятность неправильной классификации
произвольно выбранного элемента в подмножестве, если его метка назначается
случайным образом в соответствии с распределением меток в подмножестве.
Корень дерева
Число
ног >=3
Да
Промежуточный
узел с условием
Нет
Число
глаз >=3
Да
Паук
Пингвин
Листовой узел
с предсказанием
Нет
Пингвин
Рис. 5.1. Ключевые элементы дерева решений (корни, ветвления и листья),
классифицирующего животных по количеству ног и глаз
Если дерево решений используется для регрессии, оно применяет иные критерии
разбиения. В регрессии цель состоит в том, чтобы разделить данные на подмножества, минимизируя среднеквадратичную ошибку, среднюю абсолютную
ошибку или дисперсию целевой переменной в каждом подмножестве. В процессе
обучения происходит автоматический выбор лучших признаков, и бˆольшая
часть вычислений для дерева направлена на определение оптимальных точек
разбиений. Однако после построения дерева прогнозирование метки класса или
целевого значения для новых данных происходит относительно быстро и просто: нужно пройти от корня до листа, основываясь на значениях ограниченного
набора признаков.
Деревья решений просты в вычислении и визуализации. Они не требуют масштабирования, моделирования нелинейностей либо иного преобразования
признаков или целевой переменной, поскольку могут аппроксимировать любую
нелинейную зависимость между целевой переменной и предикторами, рассматривая отдельные части их распределения. По сути, они разрезают кривую на части
так, что каждая из них похожа на прямую. С другой стороны, деревья решений
склонны к переобучению и могут создавать избыточное количество разбиений,
которые идеально соответствуют обучающим данным. Чтобы предотвратить это,
были разработаны различные стратегии:
5.1. Знакомство с алгоритмами на основе деревьев 181
ограничение количества разбиений в дереве;
обратная обрезка узлов после их создания для снижения переобучения.
Рисунок 5.2 представляет альтернативный взгляд на дерево решений. Рисунок 5.1
визуализирует дерево как граф на основе двух признаков, в то время как рис. 5.2
показывает дерево как разбиение пространства данных. Каждое разбиение дерева
является линией на диаграмме. Имеются семь вертикальных линий (результат
бинарных условий для признака на оси x) и три горизонтальные (для признака на оси y), что в общей сложности составляет 10 разбиений. Дерево можно
считать успешным, поскольку каждый класс четко отделен в соответствующих
разделах (каждый раздел является конечным узлом). Однако при внимательном
рассмотрении становится очевидным, что некоторые разделы были созданы
только для того, чтобы соответствовать отдельным примерам в определенных
областях пространства. Например, несколько разделов содержат только по
одному примеру. Это создает риск некорректной классификации новых образцов, если они не идеально соответствуют распределению обучающих данных
(ситуация переобучения).
Рис. 5.2. Полностью
сформированное ветвление
дерева решений можно
интерпретировать как
последовательность
разбиений датасета
На рис. 5.3 показана та же задача, визуализированная с меньшим количеством
разбиений — два для каждого признака. Этого можно достичь, обрезая предыдущие разбиения дерева в обратном порядке, удаляя те, которые включают
слишком мало обучающих примеров; либо можно просто ограничить рост дерева, например задав максимальное количество создаваемых разбиений. Если
использовать меньше разбиений, дерево может не так точно подстроиться под
обучающие данные, однако повышается вероятность корректной классификации
новых случаев, поскольку решение явным образом зависит от отдельных точек
обучающего набора.
182 Глава 5. Деревья решений и градиентный бустинг
Рис. 5.3. Задача, решаемая
с помощью более
простого дерева решений,
полученного путем обрезки
или ограничения его роста
С точки зрения недообучения и переобучения этот алгоритм обладает высокой дисперсией, поскольку его сложность часто превышает необходимую для
конкретных задачи и данных. Найти оптимальный баланс непросто. По правде
говоря, лучший способ повысить точность деревьев — это не использовать их
по отдельности, а объединить в ансамбли. Далее мы рассмотрим ансамблевые
методы, такие как бэггинг, случайные леса и градиентный бустинг — продвинутый метод, основанный на деревьях.
В этой главе мы вернемся к датасету Airbnb NYC, чтобы продемонстрировать
основные реализации градиентного бустинга на основе деревьев решений и то,
как работает эта техника. Код в листинге 5.1 повторно использует данные и некоторые важные функции и классы, которые мы ранее применяли для иллюстрации других классических алгоритмов машинного обучения.
Листинг 5.1. Повторное использование датасета Airbnb NYC
Список признаков, исключаемых
из обработки данных
Список категориальных признаков
с низкой кардинальностью для
прямого кодирования
import numpy as np
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
from sklearn.compose import ColumnTransformer
Список категориальных признаков
from sklearn.impute import SimpleImputer
с высокой кардинальностью
для порядкового кодирования
data = pd.read_csv("./AB_NYC_2019.csv")
excluding_list = ['price', 'id', 'latitude', 'longitude', 'host_id',
'last_review', 'name', 'host_name']
low_card_categorical = ['neighbourhood_group',
_
_
'room_type']
high_card_categorical = ['neighbourhood']
continuous = ['minimum_nights', 'number_of_reviews', 'reviews_per_month',
'calculated_host_listings_count', 'availability_365']
target_mean = (
Создает бинарную целевую переменную,
(data["price"] > data["price"].mean())
указывающую, находится ли цена выше
.astype(int))
среднего значения (несбалансированная
target_median = (
бинарная целевая переменная)
(data["price"] > data["price"].median())
.astype(int))
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
Список категориальных признаков
с высокой кардинальностью
для порядкового кодирования
data = pd.read_csv("./AB_NYC_2019.csv")
excluding_list = ['price', 'id', 'latitude', 'longitude', 'host_id',
'last_review', 'name', 'host_name']
5.1. Знакомство с алгоритмами на основе деревьев 183
low_card_categorical = ['neighbourhood_group',
_
_
'room_type']
high_card_categorical = ['neighbourhood']
continuous = ['minimum_nights', 'number_of_reviews', 'reviews_per_month',
'calculated_host_listings_count', 'availability_365']
target_mean = (
Создает бинарную целевую переменную,
(data["price"] > data["price"].mean())
указывающую, находится ли цена выше
.astype(int))
среднего значения (несбалансированная
target_median = (
бинарная целевая переменная)
(data["price"] > data["price"].median())
.astype(int))
target_multiclass = pd.qcut(
data["price"], q=5, labels=False)
target_regression = data["price"]
categorical_onehot_encoding = OneHotEncoder(handle_unknown='ignore')
categorical_ord_encoding =
OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=np.nan)
numeric_passthrough = SimpleImputer(strategy="constant", fill_value=0)
column_transform = ColumnTransformer(
[('low_card_categories',
categorical_onehot_encoding,
low_card_categorical),
('high_card_categories',
categorical_ord_encoding,
high_card_categorical),
('numeric',
numeric_passthrough,
continuous),
],
remainder='drop',
verbose_feature_names_out=False,
sparse_threshold=0.0)
Задает целевую переменную для
регрессии в виде столбца цены price
Создает многоклассовую целевую переменную,
по квантилям разделяя цену на пять классов
Создает бинарную целевую переменную,
указывающую, находится ли цена выше медианы
(сбалансированная бинарная целевая переменная)
Создает преобразователь столбцов
ColumnTransformer, который применяет различные
преобразования к разным группам признаков
Код считывает CSV-файл с данными об объявлениях на Airbnb NYC за 2019 год
с помощью библиотеки pandas. Затем определяются списки, которые категоризируют признаки данных по типам:
exclusion_list — список признаков, исключаемых из анализа, например
уникальные идентификаторы и текстовые поля;
low_card_categorical — подмножество категориальных признаков с низкой
кардинальностью (мало уникальных значений), которые будут преобразованы с использованием one-hot-кодирования;
high_card_categorical — подмножество категориальных признаков с высо-
кой кардинальностью (много уникальных значений), которые будут преобразованы с использованием порядкового кодирования;
continuous — список непрерывных числовых признаков, которые будут
стандартизированы для анализа.
184 Глава 5. Деревья решений и градиентный бустинг
Затем код создает несколько целевых переменных на основе признака цены price:
target_mean — бинарная переменная, указывающая, превышает ли цена
среднее значение цен из всех объявлений;
target_median — бинарная переменная, указывающая, превышает ли цена
медианную цену из всех объявлений;
target_multiclass — переменная с пятью классами на основе квантилей
распределения цен;
target_regression — фактические значения цен, которые будут использо-
ваться для регрессионного анализа.
Все эти целевые переменные позволяют решать различные задачи регрессии
и классификации и тестировать алгоритмы машинного обучения. В этой главе
мы всегда будем использовать target_median, но вы, внеся небольшие изменения в код, можете проводить эксперименты с любыми другими целевыми
переменными.
Далее код настраивает несколько преобразований для предварительной обработки данных:
categorical_onehot_encoding — используется для one-hot-кодирования для
категориальных признаков с низкой кардинальностью;
categorical_ord_encoding — используется для порядкового кодирования
категориальных признаков с высокой кардинальностью;
numeric_passthrough — просто передает непрерывные числовые признаки
без изменений.
В завершение код создает объект ColumnTransformer, который применяет соответствующие преобразования к каждому подмножеству признаков в зависимости
от их типа. Он применяет one-hot-кодирование к категориальным признакам
с низкой кардинальностью, а непрерывные числовые признаки передаются без
изменений. Преобразование настроено на исключение любых явно не указанных
признаков и вывод их кратких имен. Параметр sparse_threshold установлен
на ноль, чтобы гарантировать, что преобразование всегда возвращает плотные
массивы.
Листинг 5.2 показывает, как стандартная модель деревьев применяется к нашей
задаче. Как и в примерах, рассмотренных в предыдущей главе, мы импортируем
необходимые модули из библиотеки scikit-learn, определяем пользовательскую
метрику оценки на основе доли верных результатов и настраиваем стратегию
пятифолдовой кросс-валидации. Затем мы определяем ColumnTransformer
с именем column_transform, который организует предварительную обработку
данных. Она включает:
5.1. Знакомство с алгоритмами на основе деревьев 185
преобразование категориальных переменных с использованием функции
categorical_onehot_encoding для определенных категориальных столбцов
с низкой кардинальностью;
передачу числовых признаков с помощью функции numeric_passthrough для
непрерывных переменных;
удаление всех оставшихся необработанных столбцов (remainder='drop');
настройку некоторых параметров, таких как запрет на использование по
дробных имен признаков (verbose_feature_names_out=False) и разреженного матричного представления (sparse_threshold=0.0).
На этом этапе пайплайн, объединяющий ColumnTransformer с классификатором
на основе дерева решений, тестируется с использованием кросс-валидации,
которая возвращает долю верных результатов вместе со средним временем обу
чения и временем оценки.
Под капотом процедуры кросс-валидации и пайплайна данных лежит многократное разбиение датасета классификатором дерева решений во время обучения
на основе значения разбиения определенного признака. Алгоритмически процедуру можно объяснить как «жадную», поскольку дерево выбирает признак
с наилучшим разбиением на каждом шаге, не учитывая, могут ли альтернативы
привести к лучшему результату. Это просто, но эффективно. Процесс продолжается до тех пор, пока не останется разбиений, улучшающих обучение, как
показано в листинге 5.2.
Листинг 5.2. Классификатор на основе дерева решений
from
from
from
from
sklearn.tree import DecisionTreeClassifier
sklearn.pipeline import Pipeline
sklearn.metrics import make_scorer, accuracy_score
sklearn.model_selection import KFold, cross_validate
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
Создает преобразователь столбцов
ColumnTransformer, который применяет
различные преобразования
к категориальным и числовым признакам
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_passthrough, continuous)],
remainder='drop',
verbose_feature_names_out=False,
sparse_threshold=0.0)
Экземпляр классификатора
дерева решений
model = DecisionTreeClassifier(random_state=0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
Пайплайн, последовательно
применяющий преобразование
столбцов и модель дерева решений
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
Пятифолдовая кросс-валидация
с использованием заданного
пайплайна, расчетом доли
верных результатов и возвратом
дополнительной информации
verbose_feature_names_out=False,
sparse_threshold=0.0)
model = DecisionTreeClassifier(random_state=0)
Экземпляр классификатора
дерева решений
model_pipeline = Pipeline(
Пайплайн, последовательно
186[('processing',
Глава 5. Деревья
решений и градиентный
бустинг
column_transform),
применяющий преобразование
('modeling', model)])
столбцов и модель дерева решений
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Пятифолдовая кросс-валидация
с использованием заданного
пайплайна, расчетом доли
верных результатов и возвратом
дополнительной информации
Выводит среднее значение
и стандартное отклонение
доли верных результатов,
полученных на кроссвалидации
Результат, который мы получаем, следующий:
0.761 (0.005) обучение: 0.22 с, прогнозирование: 0.01 с.
Результат оказался неудовлетворительным по сравнению с предыдущими экспериментами, где использовались другие алгоритмы машинного обучения. Это
связано с переобучением дерева решений, которое построило избыточное количество ветвлений. Производительность можно улучшить, ограничив рост дерева,
например путем подбора параметра max_depth. Однако есть более эффективные
способы повышения качества работы этого алгоритма. В следующем подразделе
мы рассмотрим первый из них, который основан на ансамбле множества деревьев,
обученных на различных вариантах примеров и признаков.
5.1.1. Бэггинг и семплирование
Мы рассмотрели все одиночные алгоритмы обучения на основе деревьев решений. Следующим шагом является ансамблирование алгоритмов одного типа,
которое позволяет достичь большей предсказательной силы. Идея интуитивно
понятна: если одна модель показывает определенный уровень точности, использование нескольких или их объединение в цепочку (чтобы каждая последующая
училась на результатах и ошибках предыдущей) должно дать более точные
результаты. Существуют две основные стратегии ансамблирования.
Усреднение (averaging) — прогнозы получаются путем усреднения предсказаний нескольких моделей. Различия заключаются в том, как объединяются
модели, например с помощью пейстинга (pasting), бэггинга (bagging), метода случайных подпространств (random subspaces) или случайных патчей
(random patches), что, как мы увидим в этом разделе, приводит к разным
результатам. Лучшим примером ансамблевых моделей такого рода является
алгоритм случайных лесов (random forests), который построен на подходе,
похожем на случайные патчи.
5.1. Знакомство с алгоритмами на основе деревьев 187
Бустинг (boosting) — прогнозы строятся как средневзвешенное значение
последовательно соединенных моделей, где каждая последующая модель обу
чается на ошибках предыдущих. Лучшим примером такого типа алгоритма
является градиентный бустинг, например XGBoost и LightGBM.
В следующем подразделе мы рассмотрим случайные леса. Прежде чем углубляться в эту тему, давайте уделим немного времени другим подходам усреднения —
не только потому, что метод случайных патчей основан на них, но и потому, что
они предлагают решения, которые стоит применять к табличным данным для
снижения дисперсии оценок. Это позволяет получать более надежные прогнозы
для любой модели машинного обучения.
Пейстинг — первый подход, который мы рассмотрим. Лео Брейман (Leo Breiman),
создатель алгоритма случайных лесов, предлагает создавать набор различных
моделей, обученных на подвыборках, полученных путем случайного семплирования без замены из обучающих данных. Предсказания моделей объединяются
путем усреднения в случае задачи регрессии или путем голосования большинством в случае задачи классификации.
Преимущества пейстинга:
улучшение результатов за счет снижения дисперсии прогнозов при незначительном увеличении смещения, которое является мерой того, насколько
отклонились прогнозы модели от истинных значений;
повышение устойчивости прогнозов и снижение влияния выбросов;
уменьшение объема данных, используемых для обучения, что снижает требования к памяти.
Недостатки:
сокращение объема доступных данных увеличивает смещение, поскольку
при семплировании существует вероятность исключения важных частей
распределения данных;
высокие вычислительные затраты при использовании сложных алгоритмов.
Последний недостаток зависит от временнˆых ограничений или доступных ресурсов. Исторически методы усреднения предлагалось применять к слабым моделям
(то есть таким, которые очень быстро обучаются из-за своей простоты: линейная
регрессия или модель k ближайших соседей). На практике было замечено, что
ансамбли слабых моделей могут превзойти результаты одного более сложного
алгоритма. Однако слабые модели обычно имеют сильное смещение, и с помощью
сабсемплинга (взятия подвыборок) дополнительно создается только некоторая
дисперсия в их оценках, тогда как проблема смещения остается нерешенной.
Использование усреднения имеет главное преимущество в том, что уменьшает
188 Глава 5. Деревья решений и градиентный бустинг
дисперсию оценок ценой небольшого увеличения смещения. Поскольку слабые модели изначально имеют значительное смещение, они могут не достичь
результатов, сопоставимых с тем же подходом, применяемым к более сложным
моделям. В ситуациях, когда необходимы более значимые улучшения за счет
снижения дисперсии оценок, применение стратегии усреднения к сложным
моделям может оказаться эффективнее.
Бэггинг, также предложенный Лео Бреймаром как улучшенное решение, отличается от пейстинга использованием бутстрэппинга, который подразумевает
многократное случайное извлечение подвыборок с заменой из исходной выборки
данных для аппроксимации распределения генеральной совокупности. Бутстрэппинг — это часто используемый статистический метод, который позволяет
оценивать изменчивость и неопределенность статистики на основе доступных
данных без знания истинного распределения популяции. Используя информацию из доступной выборки посредством нескольких повторных семплирований,
имитирующих истинное поведение популяции, бутстрэппинг эмулирует поведение популяции, не требуя явных знаний о ее статистическом распределении.
Причина использования бутстрэппинга в машинном обучении заключается
в оценке неопределенности производительности модели или в оценке распределения статистики. Кроме того, бутстрэппинг помогает создавать более
разнообразные вариации исходного датасета для обучения и ансамблирования.
Это основано на наблюдении, что усреднение нескольких моделей снижает
дисперсию больше, если прогнозы этих моделей мало коррелируют (то есть
более разнообразны). Сабсемплинг создает такие разнообразные датасеты для
обучения. Однако он имеет ограничения, поскольку если проводить его агрессивно — например, выбирая менее 50 % исходных данных, — то, как правило,
будет внесено смещение.
В отличие от ограниченного сабсемплинга (например, использования 90 %
данных), при котором создаются коррелированные подвыборки, бутстрэппинг
более эффективен: в среднем каждая бутстрэп-выборка включает около 63.2 %
уникальных данных из исходного набора. Подробное объяснение с точки зрения
статистики см. на StackExchange https://mng.bz/zZ0w. Более того, семплирование
с заменой, как правило, дает результаты, которые имитируют исходное распределение данных. Бутстрэппинг создает набор из большего количества различных
датасетов для обучения и, следовательно, набор из большего количества различных прогнозов, которые можно объединить, уменьшая дисперсию.
Действительно, при усреднении мы формируем распределение прогнозов и принимаем его центр в качестве итогового предсказания. Чем больше усредненные
прогнозы напоминают случайное распределение, тем меньше центр распределения будет смещен из-за проблем в данных, полученных моделью (таких как
переобучение).
5.1. Знакомство с алгоритмами на основе деревьев 189
В отличие от этого, метод случайных подпространств, предложенный Т. Хо (T. Ho)
(«The Random Subspace Method for Constructing Decision Forests», Pattern Analysis
and Machine Intelligence, 20(8), 832–844, 1998), применяет семплирование только
к признакам. Это работает, поскольку моделью ансамбля является дерево решений — алгоритм с высокой дисперсией оценок, которая значительно снижается
в ансамбле за счет использования только части признаков для каждой отдельной
модели. Улучшение результата достигается за счет того, что модели, обученные
на подвыборке признаков, как правило, дают некоррелированные прогнозы — все
деревья решений переобучаются, но по-разному относительно друг к другу.
Наконец, при использовании случайных патчей (Дж. Лупп (G. Louppe) и П. Гертс
(P. Geurts), «Ensembles on Random Patches» в Machine Learning and Knowledge
Discovery in Databases (2012): 346–361) семплирование как наблюдений, так
и признаков используется вместе для получения еще более некоррелированных
прогнозов, которые можно усреднить с еще большей эффективностью.
Пейстинг, бэггинг, случайные подпространства и случайные патчи могут быть
реализованы с использованием функций scikit-learn для бэггинга. Поведение
BaggingClassifier для классификации и BaggingRegressor для регрессии можно
контролировать следующими параметрами:
bootstrap;
max_sample;
max_features.
Комбинируя их в соответствии со спецификациями каждого метода усреднения,
можно реализовать все четыре описанные нами стратегии (табл. 5.1).
Таблица 5.1. Стратегии бэггинга и семплирования
Стратегия
усреднения
Что происходит с данными
Параметры для BaggingClassifier/
BaggingRegressor
Пейстинг
Обучающие примеры семплируются без замены
bootstrap = False
max_samples < 1.0
max_features = 1.0
Бэггинг
Обучающие примеры семплируются с заменой (бутстрэппинг)
bootstrap = True
max_samples = 1.0
max_features = 1.0
Случайные
подпространства
Признаки семплируются
(без замены)
bootstrap = False
max_samples = 1.0
max_features < 1.0
Случайные патчи
Обучающие примеры и признаки семплируются без замены
bootstrap = False
max_samples < 1.0
max_features < 1.0
190 Глава 5. Деревья решений и градиентный бустинг
Путем указания желаемого класса модели scikit-learn в параметрах оценщика,
вы можете выбрать, какой алгоритм использовать для построения ансамбля.
Дерево решений является значением по умолчанию, но вы можете сами решить,
какие слабые или сильные модели предпочтительнее. В следующем примере мы
применяем бэггинг-классификатор, устанавливая количество деревьев решений
на 300. В листинге 5.3 показано, как все модели совместно улучшают низкие
результаты, которые дерево решений продемонстрировало в этой задаче (как
мы видели на примере листинга 5.2).
Листинг 5.3. Классификатор на основе упакованного дерева
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
Создает ансамблевую модель
BaggingClassifier на основе
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0) деревьев решений
model = BaggingClassifier(
Задает бутстрэп-семплирование
estimator=DecisionTreeClassifier(),
для BaggingClassifier
n_estimators=300,
Задает отсутствие семплирования
bootstrap=True,
признаков для BaggingClassifier
max_samples=1.0,
max_features=1.0,
Задает отсутствие семплирования
random_state=0)
данных для BaggingClassifier
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_passthrough, continuous)],
remainder='drop',
verbose_feature_names_out=False,
sparse_threshold=0.0)
Преобразователь столбцов ColumnTransformer,
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
который применяет различные преобразования
к категориальным и числовым признакам
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Пайплайн, который
последовательно применяет
преобразование столбцов и модель
бэггинг-классификатора
Пятифолдовая кросс-валидация
с использованием заданного
пайплайна и расчетом доли
верных результатов
Выводит на экран среднее значение
и стандартное отклонение доли
верных результатов, полученных
на кросс-валидации
5.1. Знакомство с алгоритмами на основе деревьев 191
Получение результатов займет немного больше времени, и они выглядят многообещающе, но улучшения все еще недостаточны, чтобы конкурировать с нашими предыдущими решениями, основанными на методе опорных векторов
и логистической регрессии:
0.809 (0.004) обучение: 37.93 с, прогнозирование: 0.83 с.
В следующем подразделе мы сделаем очередной шаг в изучении ансамблирования, вернувшись к случайным лесам, и узнаем, зачем в них используется метод
случайных патчей в бэггинге.
5.1.2. Прогнозирование с помощью случайных лесов
Случайные леса работают аналогично бэггингу, но с дополнительным применением метода случайных патчей (семплирование обучающих примеров и признаков
без замены): перед обучением каждой модели осуществляется бутстрэппинг
примеров и субдискретизация признаков. Поскольку базовый алгоритм, используемый в ансамбле случайных лесов, представляет собой дерево решений,
построенное путем бинарных разбиений, семплирование признаков происходит
при каждом разбиении: из всего множества признаков случайно выбирается
подмножество кандидатов для поиска наилучшего разбиения.
Если позволить каждому дереву решений в ансамбле разрастаться до предельной глубины, это может привести к переобучению и высокой дисперсии оценок.
Однако использование бутстрэппинга и семплирования признаков смягчает
эти проблемы. Бутстрэппинг гарантирует, что модели обучаются на немного
отличающихся выборках данных из одного и того же распределения, в то время
как семплирование признаков на каждом разбиении гарантирует разнообразие
структур деревьев. Эта комбинация генерирует набор малокоррелированных
моделей, которые производят сильно отличающиеся прогнозы (следовательно,
мы можем сказать, что их прогнозы весьма некоррелированы), и это большое
преимущество для метода усреднения, потому что при ансамблировании в единый вектор прогнозирования получаются более надежные и точные результаты.
На рис. 5.4 показан принцип работы случайных лесов. Рисунок иллюстрирует
задачу бинарной классификации с датасетом из двух классов. Набор данных
моделируется с применением нескольких деревьев решений, использующих
бутстрэппинг и семплирование признаков. Эти методы приводят к разбиениям
датасета, представленным в верхней части рисунка тремя примерами. Деревья
разбивают пространство данных различными способами, демонстрируя изменчивость их стратегий разбиения. Для упрощения визуализации показаны
только два признака.
В итоге, когда все результаты объединяются путем мажоритарного голосования
(когда в качестве прогноза выбирается класс, за который голосует большинство
192 Глава 5. Деревья решений и градиентный бустинг
моделей), оказывается, что случайный лес обеспечивает более точные прогнозы,
полученные на основе результатов всех деревьев. Это показано в нижней части
рисунка, где различные оттенки указывают на распространенность того или
иного класса в конкретном разделе. Окончательная граница между классами
показана в виде черной ломаной линии, полученной в результате голосования
большинством голосов. Линия может быть более плавной и стать похожей на
кривую при использовании большего количества деревьев. Ансамблевые методы
способны аппроксимировать любую кривую при условии задействования достаточного количества моделей.
Изначально разработанный Лео Брейманом и Адель Катлер (Adele Cutler)
(https://mng.bz/0Qlp) алгоритм, хотя и защищен коммерчески, стал открытым —
отсюда и множество различных названий его реализаций. Случайные леса открывают еще более интересные возможности, помимо улучшения прогнозирования, поскольку их можно использовать для определения важности признаков
и измерения степени сходства примеров в датасете.
В листинге 5.4 мы проверяем, как случайные леса работают на нашей задаче
классификации с датасетом Airbnb NYC. За исключением алгоритма, при применении деревьев решений нет никаких отличий от нашей стандартной обработки
данных. One-hot-кодирование преобразует категориальные признаки с малым
количеством значений в бинарные, а числовые остаются без изменений.
Датасет
Дерево 1
Дерево 2
Дерево 3
Голосование большинством
Рис. 5.4. Как случайный лес получает результаты путем объединения различных
разбиений данных через голосование большинством
5.1. Знакомство с алгоритмами на основе деревьев 193
Листинг 5.4. Классификатор на основе случайного леса
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
model = RandomForestClassifier(n_estimators=300,
min_samples_leaf=3,
random_state=0)
Классификатор RandomForestClassifier
с 300 оценщиками и минимальным
количеством образцов в листовых
узлах, равным 3
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_passthrough, continuous)],
remainder='drop',
verbose_feature_names_out=False,
sparse_threshold=0.0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
Преобразователь столбцов ColumnTransformer,
который применяет различные преобразования
к категориальным и числовым признакам
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Пайплайн, который
последовательно применяет
преобразование столбцов
и модель классификатора на
основе случайных лесов
Пятифолдовая кросс-валидация
с использованием заданного
пайплайна и вычислением доли
верных результатов
Выводит на экран среднее
значение и стандартное
отклонение доли верных
результатов, полученных
на кросс-валидации
После запуска скрипта вы получите следующие результаты, которые действительно являются лучшими для этой задачи:
0.826 (0.004) обучение: 12.29 с, прогнозирование: 0.68 с.
Секрет получения хорошего результата с помощью случайных лесов заключается в грамотном выборе нескольких его гиперпараметров. Хотя этот алгоритм
прост в применении и отлично работает с параметрами по умолчанию, его тонкая
настройка (fine tuning) улучшит прогнозы. Во-первых, цель алгоритма — уменьшить дисперсию оценок, и это делается путем установки достаточно большого
числа деревьев в параметре n_estimators (количество оценщиков). Смысл
заключается в том, что, если у вас много деревьев, вы получаете распределение
результатов, и если они выбираются случайным образом, возникает эффект,
194 Глава 5. Деревья решений и градиентный бустинг
аналогичный регрессии к среднему (наилучшему прогнозу) вследствие закона
больших чисел. Бутстрэппинг примеров и семплирование признаков, которые
учитываются для разбиения, обычно приводят к тому, что полученные деревья
в ансамбле становятся достаточно непохожими друг на друга, и это позволяет
рассматривать их как действительно «случайные». Однако для обеспечения
должной регрессии к среднему необходимо довольно большое количество таких
выборок.
Для тонкой настройки количества деревьев необходимо найти золотую середину: после достижения определенного количества оценщиков вы не получите
дальнейших улучшений, а иногда это может даже привести к снижению производительности. Кроме того, установка слишком большого количества деревьев
увеличит вычислительные затраты алгоритма, и потребуется больше времени
на обучение и прогнозирование. Однако независимо от того, сколько деревьев
вы обучаете, если значение дисперсии изначально высоко при использовании
настроек по умолчанию, вы мало что можете сделать для ее уменьшения. Здесь
в силу вступает компромисс между дисперсией и смещением; можно пожертвовать частью дисперсии (то есть вы переобучите модель) ради большего смещения.
Правильное смещение для случайных лесов можно задать следующими настройками:
установка меньшего количества признаков, которые следует учитывать при
поиске наилучшего разбиения, путем установки параметра max_features;
установка максимального количества разбиений на дерево, что ограничит
его рост до определенного размера, путем установки параметра max_depth;
установка минимального количества примеров в конечных листьях дерева,
что ограничит его рост, путем установки параметра min_samples_leaf со
значением больше 1.
В следующем разделе мы рассмотрим особо случайные деревья (extremely
randomized trees, ERT) — вариант случайных лесов, который удобен при работе
с большими и зашумленными данными.
5.1.3. Использование особо случайных деревьев
ERT (также известный как extra-trees в scikit-learn) — это более рандомизированный вид алгоритма случайных лесов. Различие заключается в выборе
кандидатов для разбиений в отдельных деревьях ансамбля. В случайных лесах
выбираются кандидаты для каждого разбиения, а затем определяется лучший
признак среди них. В ERT делается иначе: признак для разбиения выбирается
случайным образом. После этого алгоритм оценивает оптимальную точку разбиения в случайно выбранном признаке. Это влечет за собой ряд последствий.
5.1. Знакомство с алгоритмами на основе деревьев 195
Во-первых, поскольку полученные деревья еще менее коррелированы, дисперсия в прогнозах ERT еще ниже, но ценой более высокого смещения. Случайное
разбиение признаков влияет на точность прогнозов. Во-вторых, ERT более
эффективен в вычислительном отношении, поскольку проверяет не наборы
признаков, а только один из них за раз для поиска наилучшего разбиения. Все
эти характеристики делают ERT наиболее подходящим для обработки:
высокоразмерных данных, поскольку признаки разделяются быстрее, чем
в любом другом ансамблевом алгоритме на основе деревьев;
зашумленных данных, поскольку случайный процесс отбора признаков и примеров может помочь снизить влияние шумных точек данных, делая модель
более устойчивой к экстремальным значениям;
несбалансированных данных, поскольку из-за случайного отбора признаков
сигналы из миноритарного подмножества данных не будут систематически
исключаться в пользу мажоритарного.
Листинг 5.5 тестирует ERT, заменяя случайные леса из листинга 5.4, где мы
построили модель с датасетом Airbnb NYC, чтобы выяснить, выше или ниже
медианного значения окажется цена объекта недвижимости для сдачи в аренду.
Листинг 5.5. Классификатор ERT
from sklearn.ensemble import ExtraTreesClassifier
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
model = ExtraTreesClassifier(n_estimators=300,
min_samples_leaf=3,
random_state=0)
ExtraTreesClassifier с 300 деревьями
и минимальным количеством
примеров в листовом узле, равным 3
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_passthrough, continuous)],
remainder='drop',
verbose_feature_names_out=False,
sparse_threshold=0.0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
Преобразователь столбцов ColumnTransformer,
который применяет различные преобразования
к категориальным и числовым признакам
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
Пайплайн, который
последовательно применяет
преобразование столбцов и модель
классификатора случайных лесов
Пятифолдовая кросс-валидация
с использованием заданного
пайплайна и расчетом доли верных
результатов
[('processing', column_transform),
('modeling', model)])
Пайплайн, который
cv_scores = cross_validate(estimator=model_pipeline, последовательно применяет
преобразование столбцов и модель
X=data,
классификатора случайных лесов
y=target_median,
196 Глава 5. Деревья решений
и градиентный бустинг
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Пятифолдовая кросс-валидация
с использованием заданного
пайплайна и расчетом доли верных
результатов
Выводит на экран среднее
значение и стандартное
отклонение доли верных
результатов на кросс-валидации
Результат немного лучше, чем при использовании случайных лесов:
0.823 (0.004) обучение: 4.99 с, прогнозирование: 0.42 с.
Если вы запустите представленный выше код, то увидите, что обучение с помощью ETR намного быстрее, чем с помощью случайных лесов, при использовании
того же датасета и одинакового количества деревьев. ETR становится интересной
альтернативой, когда ваш датасет больше (то есть содержит больше примеров),
и тем более когда он шире (то есть содержит больше признаков), так как можно
сэкономить много времени при выборе признака для разбиения за счет того, что
он выбирается случайным образом. В отличие от этого, алгоритм случайных
лесов должен искать лучший признак среди отобранных.
Тот факт, что разбиения определяются случайным образом, является большим
преимуществом, когда многие коллинеарные и зашумленные признаки связаны
с целевой переменной. Алгоритм ETR избегает выбора одних и тех же сигналов,
в отличие от алгоритма, который ищет признак, лучше всего соответствующий
цели. Кроме того, можно рассматривать динамику работы ETR в разбиении
признаков как еще один способ обмена дисперсии на смещение. Случайное разбиение является ограничением для алгоритма и снижает дисперсию, поскольку
результирующий набор деревьев будет очень некоррелированным.
В следующем разделе мы завершаем обзор ансамблей на основе деревьев изу
чением градиентного бустинга. Этот немного отличающийся подход к ансамб
лированию часто более эффективен для задач анализа табличных данных, чем
бэггинг или случайные патчи.
5.2. Градиентный бустинг
В последние годы градиентный бустинг на основе деревьев решений (gradient
boosting decision trees, GBDT) прочно утвердился как передовой метод для задач с табличными данными, хотя он используется для широкого спектра задач
5.2. Градиентный бустинг 197
в различных областях, включая многоклассовую классификацию, прогнозирование кликов по рекламе и ранжирование в поисковых системах. При применении к стандартным табличным задачам можно ожидать, что GBDT покажет
лучшие результаты, чем нейронные сети, метод опорных векторов, случайные
леса и ансамбли бэггинга.
Способность GBDT обрабатывать неоднородные признаки и гибкие возможности в выборе функции потерь и метрик оценки делают алгоритм наиболее
подходящим для задач предиктивного моделирования на табличных данных.
Вот его преимущества:
при правильной настройке гиперпараметров может достичь наилучшей производительности среди всех других методов;
нет необходимости в масштабировании или других монотонных преобразованиях признаков;
автоматически фиксирует нелинейные взаимосвязи в данных;
устойчив к выбросам и зашумленным данным;
автоматически обрабатывает пропущенные значения;
автоматически выбирает лучшие признаки и может определить их важность.
Все эти характеристики зависят от принципа работы алгоритма, который комбинирует последовательности решающих деревьев в рамках оптимизации методом
градиентного спуска. Фактически в градиентном бустинге вы начинаете с постоянного базового значения (константы) и последовательно добавляете деревья
в ансамбль, причем каждая следующая модель исправляет ошибки предыдущих,
подобно тому как это происходит при оптимизации методом градиентного спуска. Градиентный бустинг представляет собой эволюцию исходного подхода
к бустингу. В таких более ранних версиях, как Adaboost, модели, построенные
на остатках (residuals) предыдущих, просто усреднялись.
В Adaboost алгоритм подбирает последовательность слабых учеников (weak
learner) — любой алгоритм машинного обучения, который последовательно превосходит случайное угадывание, — к данным (объяснение, как выбирать слабых
учеников, см. https://mng.bz/KG9P). Затем он присваивает больший вес неверным
предсказаниям и меньший — правильным. Взвешивание помогает алгоритму
больше сосредоточиться на наблюдениях, которые сложнее предсказать. Процесс
завершается после множества исправлений путем голосования большинством
при классификации или усреднением прогнозов при регрессии.
В отличие от этого, в градиентном бустинге вы полагаетесь на двойную оптимизацию: сначала на оптимизацию отдельных деревьев, которые стремятся
уменьшить ошибку на основе своей функции оптимизации, а затем на общую
198 Глава 5. Деревья решений и градиентный бустинг
оптимизацию, включающую вычисление ошибки от суммирования бустинговой
модели, в форме, которая имитирует градиентный спуск, где постепенно корректируется предсказание модели. Поскольку имеется оптимизация второго
уровня — основанная на ошибке всей процедуры ансамбля, — градиентный
бустинг оказывается более универсальным, чем рассмотренные ранее ансамбли
деревьев, и позволяет использовать произвольные функции потерь при вычислении того, насколько суммирование предсказаний моделей расходится
с ожидаемыми результатами.
Рисунок 5.5 наглядно демонстрирует, как уменьшается ошибка на обучении после
добавления нового дерева. Каждое дерево принимает участие в оптимизации по
принципу градиентного спуска, внося свой вклад в прогнозирование поправки
к ошибкам, допущенным предыдущими деревьями.
Ошибка
на этапе
обучения
Одиночное
дерево
Комбинация
из двух
деревьев
Комбинация
из нескольких
деревьев
Номер
итерации
Рис. 5.5. Как градиентный спуск работает с деревьями при бустинге
Если градиентный спуск обеспечивает оптимальные результаты и гибкость
в оптимизации, то использование деревьев решений в качестве базового обу
чения (ансамбли, как уже было сказано, не ограничиваются только ими) дает
разнообразные преимущества. Это связано с тем, что необходимые признаки
выбираются автоматически и не нужно указывать функциональную форму
(формулу, как в регрессии), масштабирование или линейные отношения между
признаками и целевой переменной.
В следующем разделе, прежде чем перейти к рассмотрению конкретных реализаций (scikit-learn, XGBoost, LightGBM), мы попробуем создать собственную
простую реализацию градиентного бустинга, чтобы разобраться, как использовать этот мощный алгоритм.
5.2. Градиентный бустинг 199
5.2.1. Как работает градиентный бустинг
Все реализации GBDT предлагают множество гиперпараметров, которые необходимо задать для получения наилучших результатов в решаемой задаче.
Разобраться, что делает каждый параметр, сложно, но если не сделать этого
и понадеяться на автоматическую процедуру, то легче не станет, поскольку все
равно придется указать алгоритму, что и как настраивать.
По нашему опыту, написание простой реализации — лучший способ понять,
как работает алгоритм, и выяснить, как гиперпараметры связаны с предиктивной производительностью и результатами. В листинге 5.6 показан класс
GradientBoosting, способный решать любые задачи бинарной классификации,
например на основе датасета Airbnb NYC, который мы рассматриваем в качестве
примера, используя два параметра для процедуры градиентного спуска и параметры модели дерева решений, предлагаемой scikit-learn.
Код создает класс GradientBoosting, который включает методы для обучения,
предсказания вероятностей и класса. Внутри он хранит последовательность
обученных деревьев решений в списке, откуда к ним можно последовательно
возвращаться для восстановления следующей формулы суммирования:
.
В формуле:
H(X) — модель градиентного бустинга, применяемая к предикторам X;
M соответствует количеству используемых деревьев-оценщиков;
ν — скорость обучения;
wm — корректировки, которые нужно предсказать, из предыдущих деревьев;
hm относится к m-му используемому дереву решений.
Интересен тот факт, что деревья градиентного бустинга всегда являются деревьями регрессии (даже для задач классификации), — отсюда наш выбор использования DecisionTreeRegressor из scikit-learn. Это также объясняет, почему
GBDT лучше предсказывает вероятности, чем другие ансамблевые модели на
основе деревьев: он проводит регрессию непосредственно на логит вероятности класса, что обеспечивает оптимизацию, не слишком отличающуюся от
логистической регрессии. С другой стороны, такие алгоритмы, как случайные
леса, оптимизированы по метрикам чистоты (purity metric) и оценивают вероятности путем подсчета доли класса в конечном узле, что не является истинной
оценкой вероятности. Как правило, вероятности на выходе GBDT верны и редко
требуют последующей калибровки — этапа постобработки, используемого для
200 Глава 5. Деревья решений и градиентный бустинг
корректировки предсказанных вероятностей с целью повышения их точности
и надежности в приложениях, где оценки имеют первостепенное значение, таких
как медицинская диагностика (например, выявление заболеваний), обнаружение
мошенничества или оценка кредитного риска.
В нашей реализации кода мы позволяем передавать любые параметры для регрессора DecisionTreeRegressor (см. https://mng.bz/9YQx), хотя наиболее полезными
являются те, которые связаны со сложностью построения дерева, такие как
max_depth, фиксирующий максимальную глубину дерева, или min_samples_split
и min_samples_leaf, определяющие минимальное количество примеров, необходимое для разделения внутреннего узла или для нахождения в конечном
узле соответственно.
Роль каждого дерева-регрессора заключается в предоставлении вектора w, содержащего вычисленные корректировки, которые суммируются с предыдущими
оценками после взвешивания по скорости обучения. Каждый вектор w зависит
от предыдущего, поскольку создается деревом-регрессором, обученным на градиентах, необходимых для корректировки оценок в соответствии с истинными
метками классификации. Связанные в цепочку векторы w напоминают последовательность градиентных поправок — сначала больших, затем все более мелких,
сходящихся к оптимальному выходному прогнозу. Такой градиентный спуск
полностью аналогичен процедуре оптимизации градиентного спуска, которую
мы представили в главе 4. Кроме того, изменяя функцию потерь, на которой
основывается вычисление градиентов, можно настроить GBDT на оптимизацию
разных видов потерь.
Листинг 5.6. Создание классификатора с градиентным бустингом
from sklearn.tree import DecisionTreeRegressor
import numpy as np
class GradientBoosting():
def __init__(self, learning_rate=0.1, n_estimators=10, **params):
self.learning_rate = learning_rate
self.n_estimators = n_estimators
self.params = params
self.trees = list()
def sigmoid(self, x):
x = np.clip(x, -100, 100)
return 1 / (1 + np.exp(-x))
Реализация сигмоидной функции, используемой
для преобразования вероятностей, которая
преобразует логиты обратно в вероятности
def logit(self, x, eps=1e-6):
xp = np.clip(x, eps, 1-eps)
return np.log(xp / (1 - xp))
Реализация логит-функции,
используемой для преобразования
вероятностей в логиты
def gradient(self, y_true, y_pred):
gradient = y_pred - y_true
return gradient
Вычисляет градиент функции потерь
(отрицательное логарифмическое
правдоподобие) по предсказаниям
def fit(self, X, y):
self.init = self.logit(np.mean(y))
Инициализирует модель с логитy_pred = self.init * np.ones((X.shape[0],))
преобразованным среднего
for k in range(self.n_estimators):
значения целевых переменных
gradient = self.gradient(self.logit(y), y_pred)
def sigmoid(self, x):
x = np.clip(x, -100, 100)
return 1 / (1 + np.exp(-x))
для преобразования вероятностей, которая
преобразует логиты обратно в вероятности
def logit(self, x, eps=1e-6):
xp = np.clip(x, eps, 1-eps)
return np.log(xp / (1 - xp))
Реализация логит-функции,
используемой для преобразования
5.2. Градиентный
бустинг 201
вероятностей
в логиты
def gradient(self, y_true, y_pred):
gradient = y_pred - y_true
return gradient
Вычисляет градиент функции потерь
(отрицательное логарифмическое
правдоподобие) по предсказаниям
def fit(self, X, y):
self.init = self.logit(np.mean(y))
Инициализирует модель с логитy_pred = self.init * np.ones((X.shape[0],))
преобразованным среднего
for k in range(self.n_estimators):
значения целевых переменных
gradient = self.gradient(self.logit(y), y_pred)
tree = DecisionTreeRegressor(**self.params)
tree.fit(X, -gradient)
Обучает дерево регрессии на отрицательном градиself.trees.append(tree)
енте преобразованных в логит целевых значений
y_pred += (
self.learning_rate * tree.predict(X)
)
Обновляет прогнозируемые значения, используя выходные
данные обученного дерева и коэффициента скорости обучения
def predict_proba(self, X):
y_pred = self.init * np.ones((X.shape[0],))
for tree in self.trees:
y_pred += (
self.learning_rate * tree.predict(X)
)
Предсказание требует суммирования
return self.sigmoid(y_pred)
прогнозов от всех деревьев
def predict(self, X, threshold=0.5):
proba = self.predict_proba(X)
return np.where(proba >= threshold, 1, 0)
Как и в градиентном спуске, который мы рассмотрели в применении к линейным моделям, здесь используется стохастичность процесса, чтобы избежать
застревания оптимизации на неоптимальном решении. Это достигается путем
выборки строк или столбцов перед обучением каждого дерева решений. Кроме того, применяется ранняя остановка, чтобы предотвратить использование
слишком большого количества деревьев в последовательности и излишнюю
адаптацию к обучающим данным. Мы продемонстрируем раннюю остановку
в следующей главе.
Теперь, когда мы объяснили внутреннюю работу нашего класса GradientBoosting,
можем поэкспериментировать с ним. Мы будем использовать датасет Airbnb
NYC и начнем с его разделения на обучающий и тестовый наборы. Это потребует
создания двух списков индексов строк — одного для обучающего и одного для
тестового набора — с использованием функции train_test_split (https://mng.
bz/jp1z) из scikit-learn. Мы создаем экземпляр класса GradientBoosting со скоростью обучения 0.1 и 300 деревьев решений, с максимальной глубиной в четыре
ветвления и конечными листьями с не менее чем тремя примерами. После преобразования обучающих данных путем обработки числовых и категориальных
признаков мы обучим модель, получим прогнозы на тестовом наборе и оценим
результаты (листинг 5.7).
202 Глава 5. Деревья решений и градиентный бустинг
Листинг 5.7. Тестирование класса, реализующего градиентный бустинг
from sklearn.model_selection import train_test_split
train, test = train_test_split(range(len(data)), test_size=0.2,
random_state=0)
Разделяет индексы датасета на
обучающие и тестовые наборы,
используя фиксированное случайное
начальное число
cls = GradientBoosting(n_estimators=300,
learning_rate=0.1,
max_depth=4,
min_samples_leaf=3,
Инициализирует модель GradientBoosting
с заданными гиперпараметрами
random_state=0)
X = column_transform.fit_transform(data.iloc[train])
y = target_median[train]
Применяет преобразования
столбцов к обучающим
данным
Извлекает целевые значения,
соответствующие обучающим данным
cls.fit(X, y)
Xt = column_transform.transform(data.iloc[test])
yt = target_median[test]
preds = cls.predict(Xt)
score = accuracy_score(y_true=yt, y_pred=preds)
print(f"Accuracy: {score:0.5f}")
Выводит рассчитанную долю
верных результатов
Применяет те же преобразования
столбцов к тестовым данным
Извлекает целевые значения,
соответствующие тестовым данным
Вычисляет долю верных результатов,
сравнивая предсказанные метки
с фактическими из тестового набора
Доля верных результатов нашего тестового набора составляет:
Accuracy: 0.82503
Это очень хорошая оценка, указывающая на то, что даже базовая реализация
способна отлично работать с данными. В следующем разделе мы исследуем полученные результаты и рассмотрим ключевую характеристику моделей GBDT,
которая отличает их от других ансамблей деревьев решений.
5.2.2. Экстраполяция с градиентным бустингом
В нашей реализации GBDT с нуля мы можем визуализировать, как модель
обучается на данных, делая предсказания на том же обучающем наборе. Визуализация, представленная на рис. 5.6 и созданная небольшим фрагментом
кода из листинга 5.8, представляет собой нормированную гистограмму плотности распределения. В ней высота каждого столбца отражает относительную
частоту точек данных, попадающих в определенный бин, а общая площадь под
5.2. Градиентный бустинг 203
гистограммой становится равной 1. Результат показывает распределение значений, преимущественно поляризованных к краям границ 0–1, то есть модель
уверенно справляется с классификацией примеров.
Листинг 5.8. Построение графика вероятностей, предсказанных
градиентным бустингом
import matplotlib.pyplot as plt
proba = cls.predict_proba(Xt)
Генерирует прогнозируемые вероятности для тестовых
plt.figure(figsize=(8, 6))
данных с использованием обученной модели
plt.hist(proba,
bins=30,
density=True,
color='blue',
alpha=0.7)
Создает гистограмму прогнозируемых вероятностей
plt.xlabel('Предсказанные вероятности')
с заданными бинами и нормированной плотностью
plt.ylabel('Плотность')
plt.title('Гистограмма предсказанных вероятностей')
plt.grid(True)
plt.show()
Плотность
Гистограмма предсказанных вероятностей
Предсказанные вероятности
Рис. 5.6. Гистограмма, описывающая вероятности, предсказанные в ходе
классификации градиентным бустингом, показывающая, как уверенно модель
принимает решение для большинства случаев, определяя, являются они
положительными или отрицательными
204 Глава 5. Деревья решений и градиентный бустинг
Наша реализация под капотом использует регрессионную функцию потерь —
квадратичную, градиент которой равен остатку вероятностей, преобразованных
в логиты. Определение логитов см. по адресу https://mng.bz/W214.
Логиты для вероятности p рассчитываются так:
.
Преимущество этого определения заключается в том, что логит-функция отображает вероятности в шкалу логарифмических шансов, которая представляет
собой неограниченную шкалу, от минус до плюс бесконечности, что позволяет
рассматривать нашу задачу как регрессию.
Это означает, что на каждой итерации алгоритм градиентного бустинга обучает
регрессионную модель предсказывать градиент функции потерь относительно
значений логитов, что соответствует разнице между логитом истинных целевых
значений и текущими прогнозами, выраженными в логитах. Этот подход позволяет алгоритму итеративно улучшать прогнозы, корректируя их в направлении
самого крутого спуска функции потерь, и в итоге получить логит-прогноз, ограниченный диапазоном от 0 до 1, благодаря обратной функции логита — сигмоиде. Сигмоида — это математическая функция, которая отображает свой вход
в значение от нуля до единицы, обеспечивая плавную и непрерывную кривую.
Формула для сигмоиды:
,
где:
σ(x) — сигмоидная функция, примененная к входному значению x;
exp(–x) — экспоненциальная функция, где exp обозначает число Эйлера
(приблизительно 2.71828), возведенное в степень –x;
1 + exp(–x) — знаменатель, который гарантирует, что выход сигмоидной
функции всегда положительный;
1 / (1 + exp(– x)) представляет собой деление 1 на знаменатель, в результате
чего получается выходное значение сигмоидной функции.
Эта формула обычно используется в машинном обучении и статистических
моделях для преобразования логит-прогнозов в вероятности.
А что, если вместо этого мы будем рассматривать задачу как регрессионную?
В листинге 5.9 мы определяем класс GradientBoostingRegression, основанный
5.2. Градиентный бустинг 205
на нашем классе GradientBoosting, переопределим методы fit и predict, удаляя
логит- и сигмоидные преобразования.
Листинг 5.9. Тестирование класса GradientBoostingRegression
class GradientBoostingRegression(GradientBoosting):
def fit(self, X, y):
Инициализирует прогнозы средним значением y
self.init = np.mean(y)
y_pred = self.init * np.ones((X.shape[0],))
for k in range(self.n_estimators):
gradient = self.gradient(y, y_pred)
tree = DecisionTreeRegressor(**self.params)
tree.fit(X, -gradient)
self.trees.append(tree)
y_pred += (
self.learning_rate * tree.predict(X)
)
Обучает дерево на
отрицательном градиенте
Обновляет предсказания,
используя прогнозы дерева,
def predict(self, X):
масштабированные по скорости
y_pred = self.init * np.ones((X.shape[0],))
обучения
for tree in self.trees:
y_pred += (
self.learning_rate * tree.predict(X)
)
Предсказание требует суммирования
return y_pred
прогнозов от всех деревьев
reg = GradientBoostingRegression(n_estimators=300,
learning_rate=0.1,
max_depth=4,
min_samples_leaf=3,
random_state=0)
reg.fit(X, y)
proba = reg.predict(Xt)
plt.figure(figsize=(8, 6))
plt.hist(proba,
bins=10,
density=True,
Строит гистограмму вероятностей,
color='blue',
предсказанных регрессией
alpha=0.7)
plt.xlabel('Предсказанные вероятности')
plt.ylabel('Плотность')
plt.title('Гистограмма предсказанных вероятностей')
plt.grid(True)
plt.show()
При запуске кода из листинга 5.9 будет создана гистограмма прогнозов, как
показано на рис. 5.7: она демонстрирует, что вероятности выходят за границы
диапазона 0–1. Как и в случае с линейной регрессией, которая представляет
206 Глава 5. Деревья решений и градиентный бустинг
собой взвешенную комбинацию признаков, градиентный бустинг — взвешенная комбинация результатов цепочки моделей — может экстраполироваться
за пределы изученных целевых значений. Такие экстраполяции невозможны
для других ансамблей на основе деревьев решений, например случайного леса.
Деревья решений в регрессии не могут предсказать значения за пределами тех,
которые наблюдались ими при обучении, поскольку предсказания основаны на
средних значениях подвыборок обучающих данных. Способность GBDT к экстраполяции, основанная на том факте, что они являются аддитивным ансамблем,
лежит в основе их успеха в работе с временнˆыми рядами, где требуется экстраполировать будущие результаты, которые могут сильно отличаться от прошлых.
Плотность
Гистограмма предсказанных вероятностей
Предсказанные вероятности
Рис. 5.7. Гистограмма, описывающая вероятности, полученные с помощью модели
регрессии с градиентным бустингом, где некоторые значения выходят за границу
диапазона 0–1
Однако следует учитывать, что экстраполяционные возможности GBDT не
столь обширны, как у линейных моделей. В задачах прогнозирования временнˆых
рядов, когда предсказываемое значение значительно отклоняется от целевых
показателей, предоставленных для обучения (например, в случае выброса),
экстраполяция будет ограничена и может привести к неправильной оценке.
Линейная модель, которая напрямую устанавливает линейную зависимость
5.2. Градиентный бустинг 207
между входными данными и прогнозами, может оказаться более подходящей
в таких ситуациях. Линейные модели способны обрабатывать экстремальные
прогнозы для полностью неизвестных выбросов. Чтобы предложить альтернативу
деревьям решений в качестве базовых обучаемых моделей в таких ситуациях,
многие реализации GBDT предлагают линейный бустинг, представляющий собой
простое ансамблирование линейных моделей (например, в реализации XGBoost),
или кусочно-линейный градиентный бустинг, где линейные модели строятся на
конечных узлах деревьев решений (например, в реализации LightGBM).
Кроме того, сила GBDT в задачах временнˆых рядов основана на автоматическом
выборе информации для прогнозов с очень небольшим количеством настраиваемых гиперпараметров. Все, что вам нужно сделать, — это иметь достаточное
количество примеров (как минимум тысячи точек данных) и тщательно сгенерировать признаки временнˆых рядов, такие как лаговые значения и скользящие
средние на разных временнˆых горизонтах. Для более коротких рядов классические методы временнˆых рядов, такие как ARIMA или экспоненциальное сглаживание, по-прежнему являются рекомендуемым выбором. Для сложных задач,
таких как иерархически структурированные ряды, GBDT могут превзойти даже
самые сложные архитектуры глубокого обучения, специально разработанные
для работы с временнˆыми рядами. Например, GBDT исключительно хорошо
справляются с задачами в сетях супермаркетов, где продаются как медленно,
так и быстро реализуемые товары.
Наглядный пример преимущества GBDT в анализе временнˆых рядов был продемонстрирован во время конкурса прогнозирования M5, недавно проведенного
на Kaggle (https://github.com/Mcompetitions/M5-methods), где решения, созданные
с помощью алгоритма LightGBM, оказались лучше архитектур глубокого обучения, разработанных для задач прогнозирования иерархически структурированных рядов, таких как DeepAR (https://arxiv.org/abs/1704.04110) или NBEATS
(https://arxiv.org/abs/1905.10437). Подробный и глубокий анализ конкурса, а также
успеха и повсеместности методов, основанных на деревьях, в практике анализа
временнˆых рядов можно найти в статье «Forecasting with Trees» Тима Янушковски (Tim Januschowski) и др. (International Journal of Forecasting 38.4 (2022):
1473–1481: https://mng.bz/8O4Z).
5.2.3. Почему градиентный бустинг эффективен?
Сегодня, несмотря на замечательные результаты в распознавании и генерации
изображений и текста, нейронные сети на табличных данных не достигают
производительности решений на основе градиентного бустинга, таких как
XGBoost и LightGBM, которым отдают предпочтение и практикующие специалисты, и участники соревнований по data science. Например, см. «State of
Competitive Machine Learning» о соревнованиях по анализу табличных данных
208 Глава 5. Деревья решений и градиентный бустинг
(https://mlcontests.com/tabular-data/). Но откуда берется преимущество GBDT
над глубокими нейронными сетями (DNN)? Из нашего опыта построения
классификатора на основе градиентного бустинга мы могли оценить, как
алгоритм сочетает градиентный спуск с гибкостью деревьев решений при
работе с неоднородными данными. Достаточно ли этого, чтобы объяснить
эффективность GBDT на табличных данных?
«Why Do Tree-Based Models Still Outperform Deep Learning on Typical Tabular
Data?» Лео Гринштайна (Leo Grinsztajn), Эдуарда Ояллона (Edouard Oyallon)
и Гаэля Варокво (Gael Varoquaux) (Thirty-sixth Conference on Neural Information
Processing Systems Datasets and Benchmarks Track, 2022 г.: https://hal.science/
hal-03723551v2/document) — это совсем свежее исследование, которое пытается
пролить свет на разницу в производительности между архитектурами глубокого
обучения и деревьями решений с градиентным бустингом. Оно показывает, что
методы на основе деревьев превосходят DNN (даже современные архитектуры)
на табличных данных. Авторы явно фокусируются на неоднородности столбцов, которая отличает таблицы от датасетов исключительно с непрерывными
признаками (мы могли бы называть их однородными табличными датасетами)
и определяют стандартный эталонный показатель с использованием 45 открытых наборов данных. Они рассматривали только данные из приблизительно
10 000 примеров, заключенных в столбцы разных типов, в том числе с числовыми
признаками с разными единицами измерения и категориальными признаками,
поскольку это считается типичной ситуацией для табличных датасетов.
Были опробованы различные модели глубокого обучения, включая многослойные персептроны (MLP), ResNets, SAINT и FTtransformer, но методы на
основе деревьев показали лучшую производительность при меньшей настройке
гиперпараметров. Даже при рассмотрении только числовых признаков методы
на основе деревьев превзошли DNN. Это преимущество стало более выраженным при учете времени обучения, хотя используемое оборудование (включая
графические процессоры) также повлияло на результаты. Разрыв между двумя
методами был меньше на больших датасетах, которые нетипичны для таблиц.
Авторы также исследовали особенности табличных данных, которые объясняют
разницу в производительности между методами на основе деревьев и глубокого обучения. Они обнаружили, что сглаживание результата в пространстве
признаков сокращает разрыв, поскольку глубокие архитектуры борются с нерегулярными паттернами, тогда как сглаживание не влияет на древовидные
модели. Удаление неинформативных признаков еще больше сократило разрыв
для нейронных архитектур, подобных MLP. Однако только после применения
метода случайных вращений (random rotations) к данным глубокие архитектуры
превзошли древовидные модели.
Метод случайных вращений означает применение случайной матрицы вращения
к входным признакам датасета перед их подачей в модель машинного обучения.
5.2. Градиентный бустинг 209
Эта матрица представляет собой квадратную матрицу, которая сохраняет длину
векторов и углы между ними, гарантируя, что повернутые данные остаются
эквивалентными исходным. Случайные вращения используются в машинном
обучении для различных целей, включая повышение разнообразия ансамблей,
надежности моделей и решение задачи инвариантности к вращению в таких
задачах, как компьютерное зрение и машинное обучение для квантовой химии.
Такой процесс, являющийся полностью обратимым, тем не менее скрывает
взаимосвязь между предикторами и целевой переменной для алгоритмов на
основе деревьев, в то время как на моделях глубокого обучения это никак не
сказывается, поскольку они способны к репрезентативному обучению, позволяющему также изучать примененное вращение.
Этот результат не обязательно указывает на преимущество DNN, а скорее на
ограничение GBDT. Глубокие архитектуры инвариантны к вращению, что
означает, что они могут обнаруживать повернутые сигналы, как при распознавании изображений, где какие-то из них могут быть распознаны независимо от
их ориентации. Напротив, GBDT неинвариантны к вращению и могут обнаруживать только сигналы, которые всегда ориентированы одинаково, поскольку
они работают на основе правил разбиения. Поэтому использование любого
вида вращения данных, такого как метод главных компонент или сингулярное
разложение, может быть вредным для GBDT. Работа DNN не ухудшается при
вращении данных, поэтому в такой ситуации они могут показывать лучшие
результаты.
В настоящее время это исследование подтверждает наш опыт работы с GBDT
и их предполагаемые сильные стороны:
могут хорошо работать даже с датасетами среднего размера (1000–5000 случаев), но превосходят другие алгоритмы при объеме от 10 000 до 100 000 примеров (по нашему опыту);
как правило, лучше других в работе на неоднородных данных;
устойчивы к шумам и неоднородностям в целевых данных;
отфильтровывают шумовые или нерелевантные признаки благодаря процессу автоматического отбора признаков.
В дополнение к упомянутым здесь преимуществам следует отметить, что GBDT
часто предпочитают DNN в определенных сценариях. Одна из причин заключается в том, что GBDT требуют меньше предварительной обработки данных, что
делает их более эффективными и простыми в реализации. Более того, GBDT
так же гибки, как и DNN с точки зрения целевых функций. В обоих случаях
существует множество вариантов выбора, что может быть особенно полезно
в областях со сложными целями оптимизации. Еще одним преимуществом
GBDT является то, что они предлагают больший контроль над тем, как строятся
210 Глава 5. Деревья решений и градиентный бустинг
правила деревьев решений, обеспечивая пользователям некоторую прозрачность
и интерпретируемость. Наконец, GBDT в большинстве случаев могут обучаться
быстрее, чем DNN, и предсказывать с разумным временем вывода, в зависимости
от их сложности, что критически важно для приложений реального времени или
задач с жесткими временнˆыми ограничениями.
Теперь, когда вы получили представление об основных концепциях градиентного
бустинга и его эффективности в решении задач анализа табличных данных по
сравнению с глубоким обучением, в следующем разделе мы рассмотрим некоторые из его реализаций, начиная с той, которую предоставляет scikit-learn.
5.3. Бустинг в scikit-learn
Scikit-learn предоставляет модели градиентного бустинга для задач как регрессии,
так и классификации. Доступ к этим алгоритмам можно получить через классы
GradientBoostingClassifier (https://mng.bz/Ea9o) и GradientBoostingRegressor
(https://mng.bz/N1VN) соответственно.
Реализация градиентного бустинга scikit-learn была одним из самых ранних
вариантов, доступных пользователям Python, и следует алгоритму, предложенному Джеромом Фридманом (Jerome Friedman) в 1999 году («Greedy Function
Approximation: A Gradient Boosting Machine», Annals of Statistics (2001): 1189–
1232). В листинге 5.10 кода мы проведем кросс-валидацию производительности классификатора на наборе данных Airbnb NYC для прогнозирования того,
является цена аренды выше или ниже медианного значения.
Листинг 5.10. GradientBoostingClassifier из scikit-learn
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
model = GradientBoostingClassifier(
n_estimators=300,
learning_rate=0.1,
max_depth=4,
min_samples_leaf=3,
Модель GradientBoostingClassifier
random_state=0
с заданными гиперпараметрами
)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
Пайплайн, который сначала применяет
cv_scores = cross_validate(
estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True
)
обработку данных с помощью column_
transform, а затем обучает модель
Пятифолдовая кросс-валидация с использованием заданного
пайплайна, вычисляющая долю верных результатов
и возвращающая дополнительную информацию
)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
Пайплайн, который сначала применяет
cv_scores = cross_validate(
estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True
)
обработку данных
5.3.с помощью
Бустингcolumn_
в scikit-learn 211
transform, а затем обучает модель
Пятифолдовая кросс-валидация с использованием заданного
пайплайна, вычисляющая долю верных результатов
и возвращающая дополнительную информацию
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Выводит на экран среднее
значение и стандартное
отклонение доли верных
результатов, полученных
на кросс-валидации
С теми же параметрами результаты получаются немного лучше, чем в нашей
реализации:
0.826 (0.004) обучение: 16.48 с, прогнозирование: 0.07 с.
Одним из важных аспектов решения scikit-learn является то, что процесс обучения может занять значительное количество времени, а узкое место может быть
связано с деревьями решений — это единственная поддерживаемая модель для
построения последовательного ансамбля, используемого scikit-learn. Взамен
вы получаете некоторую гибкость в настройке параметров и контроль над тем,
как GBDT использует отдельные деревья. Например, градиентный бустинг
scikit-learn позволяет:
определить функцию init. Мы использовали среднее значение в качестве
начальной точки (first estimator). Вы можете применить любой алгоритм для
ее определения. Поскольку градиентный бустинг основан на градиентном
спуске, а процесс его оптимизации чувствителен к начальной точке, это может
оказаться преимуществом при решении более сложных задач;
вернуться к Adaboost — оригинальному алгоритму, который лежит в основе
создания последовательных ансамблей, когда GradientBoostingClassifier
обучается с экспоненциальной функцией потерь (loss="exponential");
детально контролировать сложность используемых деревьев решений, это
означает, что можно моделировать более сложные данные ценой риска переобучения с помощью таких параметров, как:
min_samples_split — минимальное количество образцов, необходимых
для разбиения промежуточного узла;
min_sample_leaf — минимальное количество образцов, необходимых для
конечных узлов;
min_weight_fraction_leaf — минимальная взвешенная доля от общего
веса всех входных образцов, требуемая для образования конечного узла;
212 Глава 5. Деревья решений и градиентный бустинг
max_depth — максимальная глубина дерева;
min_impurity_decrease — пороговое значение уменьшения неоднорости
(примеси), используемое для принятия решения о разбиении узла или
остановке роста дерева;
max_leaf_nodes максимально допустимое количество конечных узлов
перед остановкой роста дерева;
если контроля роста деревьев решений недостаточно, после их построения
можно уменьшить их сложность с помощью параметра ccp_alpha. Он выполняет «обратную» обрезку дерева, начиная с конечных узлов, удаляя те
из них, которые не проходят тест на сложность (подробности см. на https://
mng.bz/DM9n);
сабсемплинг как строк (subsample), так и столбцов (max_features) — это
исключительно эффективный способ уменьшить переобучение и повысить
обобщаемость модели.
Кроме того, реализация также поддерживает работу с разреженными данными
и раннюю остановку — процедуру предотвращения переобучения в GBDT
и нейронных сетях, которая будет подробно рассмотрена далее.
Также в части предоставляемых результатов эта версия демонстрирует впечатляющие возможности, что делает ее идеальным инструментом для понимания
и объяснения причин принятия моделью GBDT тех или иных решений. Например, можно получить доступ ко всем используемым деревьям решений
и запросить значения, предсказанные каждым из них, в виде как общего результата (метод decision_function), так и последовательности шагов (метод
staged_decision_function).
В последнее время эта реализация стала менее популярной среди практикую
щих специалистов из-за появившихся более быстрых и производительных
решений, таких как XGBoost, LightGBM и HistGradientBoosting от scikit-learn.
Тем не менее она остается интересной для небольших датасетов, если требуется
детальный контроль над процессом градиентного бустинга. В следующем разделе мы рассмотрим XGBoost и узнаем, почему он может стать более мощным
инструментом для решения задач анализа табличных данных.
5.3.1. Применение ранней остановки для предотвращения
переобучения
Представление оригинальных классов scikit-learn для градиентного бустинга дает
возможность познакомиться с процедурой, которая помогает контролировать
переобучение. Это ранняя остановка — метод, изначально использовавшийся
в градиентном спуске для ограничения количества итераций, когда дальнейшие
5.3. Бустинг в scikit-learn 213
корректировки оптимизируемых коэффициентов не приводят к улучшениям или
даже ухудшают обобщающую способность решения. Метод также использовался
для обучения нейронных сетей. В градиентном бустинге, который применяет
градиентный спуск как часть процесса оптимизации, метод помогает решить
ту же проблему: ограничить количество добавляемых деревьев для снижения
вычислительной нагрузки и предотвращения возможного переобучения.
Ранняя остановка выполняется в несколько простых шагов.
1. Часть обучающего датасета откладывается для формирования валидационного набора.
2. На каждой итерации обучения полученная промежуточная версия модели
оценивается на валидационном наборе.
3. Производительность промежуточной версии модели на валидационном наборе записывается и сравнивается с предыдущими результатами.
4. Если производительность не улучшается, алгоритм увеличивает счетчик
количества итераций (обычно называемый patience) без улучшений. В ином
случае счетчик сбрасывается.
5. Обучение останавливается, если за определенное количество итераций не
произошло улучшений.
6. В противном случае обучение продолжается на следующей итерации, пока
не достигнуто указанное количество деревьев, — в этот момент обучение
завершается.
На рис. 5.8 этот процесс представлен в виде блок-схемы.
Рисунок 5.9 показывает этот же процесс с точки зрения валидационной метрики,
которой может быть функцией потерь или любая другая оценка, для которой вы
хотите получить наилучший результат. По мере совершения итераций обычно
наблюдается уменьшение ошибки на обучении. С другой стороны, ошибка на
валидации достигает оптимума до того, как алгоритм начнет переобучаться.
Ранняя остановка помогает зафиксировать увеличение ошибки на валидации,
а мониторинг ее динамики позволяет вернуться к итерации, предшествующей
переобучению.
Для каждой модели, обученной на различных фолдах в процессе кросс-валидации
с ранней остановкой, могут быть найдены различные точки остановки. При
обучении на всем датасете можно полагаться на раннюю остановку на основе
валидационной выборки. Таким образом, нужно просто задать большое количество итераций и ждать, когда обучение остановится. Также можно использовать
фиксированное количество итераций на основе точек остановки, которые наблюдались при кросс-валидации. В этом случае можно вычислить среднее или
214 Глава 5. Деревья решений и градиентный бустинг
медиану всех этих точек для определения количества деревьев бустинга, которые
следует использовать. Однако не существует какого-то строгого правила, и вы
можете взять второе максимальное значение для агрессивной стратегии остановки или второе минимальное значение для консервативной.
Инициализация
алгоритма
Итерация
обучения
patience = 0
Нет
Да
Улучшение
на тесте?
Нет
patience += 1
Последняя
итерация?
Да
Остановка
обучения
Да
> max
patience?
Нет
Рис. 5.8. Схема процесса ранней остановки в GBDT
Ошибка
Ошибка
на валидации
Ошибка
на обучении
Наименьшая ошибка на валидации
Точка ранней остановки
Число
итераций
Рис. 5.9. Как обычно изменяется ошибка на валидации и обучении на протяжении
множественных итераций при использовании GBDT
5.3. Бустинг в scikit-learn 215
Кроме того, рассмотрите возможность увеличения количества деревьев бустинга,
поскольку обучение проводится на большем объеме данных, чем при кроссвалидации. В качестве общего правила: увеличивайте количество деревьев на
процент, эквивалентный делению единицы на количество фолдов, использованных
в кросс-валидации. Однако, поскольку не существует универсального решения,
придется поэкспериментировать, чтобы найти наилучший подход для вашей задачи.
В качестве примера мы снова запускаем предыдущий код, на этот раз устанавливая большее количество базовых оценщиков и два параметра, validation_
fraction и n_iter_no_change, которые активируют процедуру ранней остановки.
Параметр validation_fraction определяет долю обучающих данных, используемых для валидации, и он эффективен только в том случае, если n_iter_no_change
установлен в целое число, указывающее, сколько итераций должно пройти без
улучшений при тестировании модели на валидационном наборе перед остановкой (листинг 5.11).
Листинг 5.11. Применение ранней остановки в GradientBoostingClassifier
model = GradientBoostingClassifier(
n_estimators=1000,
learning_rate=0.1,
validation_fraction=0.2,
n_iter_no_change=10,
max_depth=4,
min_samples_leaf=3,
random_state=0
)
Модель GradientBoostingClassifier, количество
итераций которой увеличено с 300 до 1000
В качестве доли на валидацию
GradientBoostingClassifier использует 20 %
обучающих данных
Обучение GradientBoostingClassifier
прекратится после 10 итераций без
улучшения результатов на валидации
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
Извлекает количество оценщиков,
std_cv = np.std(cv_scores['test_score'])
использованных во время обучения
fit_time = np.mean(cv_scores['fit_time'])
для каждой оценки фолда
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f} с, прогнозирование: {score_time:0.2f} с.")
iters = [cv_scores["estimator"][i].named_steps["modeling"].n_estimators_
for i in range(5)]
print(iters)
Выводит на экран список оценок для каждого прохода
216 Глава 5. Деревья решений и градиентный бустинг
Вывод будет выглядеть как
0.826 (0.005) обучение: 6.24 с, прогнозирование: 0.04 с.
[145, 109, 115, 163, 159]
Поскольку среднее число итераций для фолдов кросс-валидации составляет
138, то, исходя из эмпирического правила, при использовании всех доступных
данных на этапе обучения следует увеличить число итераций на 20 %, установив
его на уровне 166.
В следующих разделах мы познакомим вас с новыми реализациями градиентного
бустинга, такими как XGBoost и LightGBM, а также покажем, как использовать
раннюю остановку с ними.
5.4. Использование XGBoost
XGBoost получил признание после успеха в конкурсе Kaggle, Higgs Boson
Machine Learning Challenge (https://www.kaggle.com/c/higgs-boson), где был представлен как быстрое и точное решение по сравнению с градиентным бустингом
scikit-learn. С тех пор XGBoost успешно использовался во многих других конкурсах по data science, доказав эффективность и полезность платформы Kaggle как
площадки для внедрения инноваций, меняющих стандарты производительности.
Keras — еще один пример инновации, которая получила широкое распространение после успеха в конкурсах Kaggle. На момент написания статьи пакет XGBoost
был обновлен и достиг версии 2.0.3, которую мы и использовали в этой книге.
Изначально задуманный Тяньци Ченом (Tianqi Chen) как исследовательский
проект и позже доработанный с участием Карлоса Гестрина (Carlos Guestrin),
XGBoost представляет собой фреймворк градиентного бустинга, доступный
как программное обеспечение с открытым исходным кодом. Примечательно,
что в отличие от других инициатив, таких как LightGBM, спонсируемого
Microsoft, и Yggdrasil Decision Forests (см. https://mng.bz/lYV6), спонсируемого
Google, XGBoost остался полностью независимым и поддерживался сообществом
Distributed (Deep) Machine Learning Common (dmlc.github.io).
Со временем фреймворк был усовершенствован и теперь предоставляет расширенные возможности для распределенной обработки и параллелизации, что
позволяет ему работать с крупномасштабными датасетами. Он завоевал популярность и в настоящее время доступен на нескольких языках программирования,
включая C/C++, Python и R. Более того, он поддерживается на нескольких
платформах data science, таких как H2O.ai и Apache Spark.
Как пользователь, вы сразу же оцените несколько ключевых характеристик
этого фреймворка. Среди них:
5.4. Использование XGBoost 217
возможность обработки различных типов входных данных;
поддержка пользовательских функций потерь (целевых функций) и метрик
оценки;
автоматическая обработка пропущенных значений;
простота обучения на GPU;
учет ограничений монотонности и взаимодействия признаков;
оптимизация использования многоядерных процессоров и кеша на автономных компьютерах.
С точки зрения производительности системы примечательными особенностями
являются:
сетевое параллельное обучение, которое позволяет распределять вычисления
по кластеру машин;
использование всех доступных ядер CPU при построении дерева для параллелизации;
внеядерные вычисления при работе с большими датасетами, которые не помещаются в память.
Однако то, что отличает «экстремальный» алгоритм XGBoost, — это его инновационные алгоритмические детали оптимизации, включая вариант градиентного
спуска, известный как метод Ньютона (Newton Descent), и условия регуляризации, а также его уникальный подход к разбиению признаков и обработке
разреженных данных. В следующем разделе кратко излагаются эти новаторские
методы, которые обеспечивают высокую производительность XGBoost.
Давайте опробуем алгоритм для нашей задачи классификации на датасете
Airbnb NYC. Воспользуемся XGBClassifier из XGBoost. Для регрессии можно
применить класс XGBegressor. Однако сначала нужно установить XGBoost, и для
этого можно просто выполнить pip install:
pip install XGBoost
Или используйте conda:
conda install -c conda-forge py-XGBoost
Команды установки выполнят все необходимые шаги автоматически; также
по возможности установите на вашу систему как CPU-, так и GPU-варианты.
Инструкции и подробности о процессе установки см. на https://mng.bz/BXd0.
В листинге 5.12 мы повторяем подход, который ранее использовали
с GradientBoostingClassifier от scikit-learn: мы берем 300 деревьев, ограничиваем
218 Глава 5. Деревья решений и градиентный бустинг
их глубину четырьмя уровнями и устанавливаем не менее трех примеров
в узлах.
Листинг 5.12. Классификатор XGBoost
from XGBoost import XGBClassifier
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=300,
max_depth=4,
min_child_weight=3)
model_pipeline = Pipeline(
[('processing', column_transform),
('XGBoost', xgb)])
Создает модель XGBClassifier
с заданными гиперпараметрами,
включая тип бустера
Параметр objective —
эквивалент loss в scikit-learn
min_child_weight — эквивалент
min_samples_leaf в scikit-learn
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Выводит на экран среднее
значение и стандартное
отклонение результатов
кросс-валидации
Полученный результат является лучшим из имеющихся на данный момент.
Особенно впечатляет, что время обучения составляет всего лишь долю того, что
требовалось ранее для реализации scikit-learn:
0.826 (0.004) обучение: 0.84 с, прогнозирование: 0.05 с.
В следующем подразделе мы рассмотрим ключевые параметры, которые использовались в этом примере, и обсудим, какие из них, предлагаемые алгоритмом
(полный список см. по ссылке https://mng.bz/dX6N), понадобятся для успешного
запуска любого проекта по анализу табличных данных.
5.4.1. Основные параметры XGBoost
Давайте рассмотрим конкретные параметры, которые мы выбрали в нашем
предыдущем примере (листинг 5.12), начиная с n_estimators. Он определяет
5.4. Использование XGBoost 219
количество деревьев решений, используемых при построении ансамбля, и является частью процесса градиентного спуска, который мы обсуждали ранее
в этой главе.
Параметр n_estimators в XGBoost задает количество деревьев решений, участвующих в формировании итогового результата. В стандартных табличных
данных типичные значения этого параметра находятся в диапазоне от 10 до
10 000. Хотя увеличение этого значения может улучшить производительность
прогнозирования за счет привлечения большего количества слабых учеников,
оно также может увеличить время обучения. Стоит отметить, что существует
оптимальное количество деревьев, которое максимизирует производительность
на новых данных, и его поиск зависит от других параметров XGBoost, таких
как скорость обучения. Чтобы добиться высокой производительности модели
XGBoost, необходимо правильно подобрать количество деревьев в зависимости
от решаемой задачи, одновременно правильно настраивая другие параметры,
включая скорость обучения.
В то время как scikit-learn ограничивается только деревьями решений, XGBoost
предлагает больше вариантов через параметр booster:
gbtree — деревья решений, как и ожидается при градиентном бустинге;
gblinear — модели линейной регрессии;
dart — деревья решений, но процесс оптимизации более упорядочен.
Бустер gblinear создает сумму линейных моделей, связанных по цепочке. Поскольку сумма линейных комбинаций линейна, в итоге вы получаете коэффициент для каждого использованного вами признака — аналогично линейной
модели. Доступ к коэффициентам можно получить с помощью метода .coef.
Это альтернативный способ обучения модели GBDT с фокусом на интерпретируемость. Модель сводится к линейной комбинации, отличной от той, которую
можно было бы получить прямым обучением линейной модели из-за использования другого подхода к штрафованию сложности и оптимизации. Наиболее
заметным отличием является то, что коэффициенты нельзя интерпретировать
так же, как если бы их создавала линейная регрессия или обобщенная линейная
модель. Кроме того, интерпретация свободного члена (intercept), сгенерированного бустером gblinear, отличается от классических линейных моделей,
поскольку на нее влияют как скорость обучения, так и исходное приближение,
используемое бустером.
Бустер dart отличается тем, что сочетает оптимизацию на основе градиентного
спуска с подходом, похожим на dropout и используемым в глубоком обучении.
Представленный ученой из Калифорнийского университета в Беркли Рашми
Корлакай Винаяк (Rashmi Korlakai Vinayak) и исследователем Microsoft Раном
Гилад-Бакраком (Ran Gilad-Bachrach) в статье «DART: Dropouts Meet Multiple
220 Глава 5. Деревья решений и градиентный бустинг
Additive Regression Trees» (Artificial Intelligence and Statistics. PMLR, 2015),
DART фокусируется на переобучении из-за зависимости оценок каждого дерева
решений от предыдущих. Затем исследователи берут идею dropout из глубокого
обучения, где маска случайным образом частично закрывает слой нейронной
сети. Это не позволяет сети всегда полагаться на определенные сигналы в конкретном слое для определения весов следующего слоя. В DART градиенты вычисляются не относительно суммы остатков всех ранее построенных деревьев;
вместо этого алгоритм на каждой итерации случайным образом выбирает подмножество предыдущих деревьев и масштабирует их листья с коэффициентом
1/k, где k — количество исключенных деревьев.
gblinear и dart — единственные доступные альтернативные бустеры. Например,
не существует бустера для имитации случайных лесов (как в другой реализации GBDT, LightGBM). Однако хотя такой бустер пока не поддерживается
XGBClassifier и XGBRegression, можно получить похожий результат, «поиграв»
с параметрами и функциями XGBoost.
Используйте параметр num_parallel_tree и установите его на число больше 1.
На каждом шаге оптимизации градиенты оцениваются не на основе одного
дерева решений, а с помощью ансамбля деревьев, обученных с использованием бэггинга, тем самым создавая модель бустингового случайного леса.
В некоторых случаях этот подход может обеспечить лучшие результаты по
сравнению с классическим градиентным бустингом, поскольку уменьшает
дисперсию оценок за счет увеличения вычислительных затрат.
Используйте XGBRFClassifier или XGBRFRegressor — два класса из XGBoost,
которые реализуют подход случайных лесов. Эти классы все еще считаются
экспериментальными. Для получения более подробной информации см.
https://mng.bz/rKVB с оговоркой, что есть некоторые отличия от алгоритма
случайных лесов, предлагаемого scikit-learn. Дело в том, что XGBoost для
взвешивания градиентов вычисляет матрицу, состоящую из вторых производных (она называется гессианом, см. математическое определение на
https://brilliant.org/wiki/hessian-matrix/), а также не поддерживает бутстрэппинг.
Следовательно, результаты будут отличаться.
Что касается функции потерь, контролируемой параметром objective , мы
выбрали reg:logistic, но могли бы также выбрать binary:logistic — оба эквивалентны log-loss для бинарной классификации. В XGBoost функции потерь
организованы в шесть классов:
reg для задач регрессии (включая логистическую регрессию);
binary для задач бинарной классификации;
multi для многоклассовой классификации;
5.4. Использование XGBoost 221
count для счетных данных — то есть дискретных событий;
survival для анализа выживаемости — статистического метода, предна-
значенного для анализа данных о времени наступления целевого события, например отказа детали в механизме. Он учитывает цензурирование
(censoring), когда для некоторых объектов в исследовании целевое событие
еще не произошло;
rank для задач ранжирования, таких как оценка рейтинга веб-сайтов.
Помимо распределения Пуассона, удобного для моделирования частоты событий, XGBoost также предлагает reg:gamma и reg:tweedie, оптимизирующие
два распределения, используемые в страховании для моделирования размера
страховых выплат, как упоминалось в главе 4 при обсуждении обобщенных
линейных моделей.
Наличие различных функций потерь, или целевых функций (objective functions),
демонстрирует широкий спектр возможных применений XGBoost в различных
областях. Полный обзор функций потерь см. по ссылке https://mng.bz/dX6N. Они
играют ключевую роль в градиентном бустинге, поскольку определяют цель
оптимизации. При этом метрики оценки не используются для оптимизации
градиентного спуска в бустинге. Однако они играют важную роль в мониторинге
процесса обучения, оптимизации отбора признаков, подбора гиперпараметров
и даже для реализации ранней остановки, когда дальнейшее обучение перестает быть полезным. Аналогом min_samples_leaf из scikit-learn в XGBoost
является min_child_weight. Оба параметра задают минимальное количество
образцов, которое должно находиться в листовом узле дерева решений. Таким
образом, они регуляризируют дерево, ограничивая его глубину. Однако между
ними есть различия, поскольку min_child_weight относится к минимальной
сумме весов гессиана в дочернем узле, а min_samples_leaf — к минимальному
количеству примеров в листе. Следовательно, эти два параметра не полностью
эквивалентны.
Эмпирическое правило таково: min_child_weight влияет на то, как строятся
отдельные деревья решений, а именно — чем больше значение этого параметра,
тем более стабильным будет полученное дерево. Типичные значения для экспериментов находятся в диапазоне от 0 (что подразумевает отсутствие ограничений
на размер конечных узлов) до 10. В своем докладе 2015 года в Академии науки
о данных в Нью-Йорке (NYC Data Science Academy) под названием «Winning
Data Science Competitions» Оуэн Чжан (Owen Zhang), в прошлом топовый
участник Kaggle, предложил вычислять оптимальное значение этого параметра,
разделив 3 на процент наиболее редких событий в прогнозируемых данных.
Например, следуя этому правилу и учитывая, что наши классы распределены
поровну (50/50 %), идеальным значением должно быть 3/0.5, что дает 6.
222 Глава 5. Деревья решений и градиентный бустинг
Другие важные параметры XGBoost, которые мы не использовали в нашем
примере, следующие:
learning_rate, также называемый eta, — это параметр в XGBoost, который
определяет скорость обучения модели. Более низкая скорость позволяет
модели сходиться медленнее, но точнее, потенциально улучшая прогнозную
эффективность, однако увеличивает количество итераций и время обучения.
С другой стороны, установка слишком высокого значения ускоряет процесс,
но может ухудшить производительность из-за перескока оптимума в градиентном спуске;
alpha и lambda являются регуляризаторами L1 и L2 соответственно. Они
оба помогают избежать переобучения в части оптимизации градиентного
спуска XGBoost;
max_depth управляет сложностью алгоритма. Если это значение слишком
мало, модель не сможет распознать много образцов (недообучение). Однако
если значение слишком велико, она может стать слишком сложной и создать
закономерности, которые плохо переносятся на новые данные (переобучение). В идеале это значение должно находиться в диапазоне от 1 до 16;
gamma, или min_split_loss в XGBoost — это параметр регуляризации от 0
до бесконечности. Увеличение значения усиливает регуляризацию, снижая
риск переобучения, но может привести к недообучению, если значение
будет слишком велико. Кроме того, этот параметр контролирует итоговую
сложность деревьев решений. Мы предлагаем начать с нуля или какого-то
небольшого числа, а затем поэкспериментировать с его увеличением после
установки всех остальных параметров;
colsample_bytree в XGBoost контролирует долю признаков или предикто-
ров, которые используются для построения каждого дерева. Установка этого
значения менее 1 означает, что каждое дерево будет задействовать разное
подмножество признаков для прогнозирования, что потенциально снижает
риск переобучения или слишком сильного влияния отдельных признаков
в данных. Это также повышает скорость обучения за счет обработки меньшего числа признаков в каждом дереве. Допустимый диапазон значений этого
параметра составляет от 0 до 1;
subsample в XGBoost управляет долей примеров, используемых для данного
дерева во время обучения. Как и colsample_bytree, этот параметр может по-
мочь уменьшить переобучение и сократить время обучения. Изменение доли
примеров для каждого дерева позволяет модели выявить более обобщаемые
закономерности в данных. Значение по умолчанию для subsample равно 1.0,
это означает, что все примеры используются в каждом дереве.
Во многих случаях вам потребуются только некоторые из параметров, предоставляемых XGBoost и обсуждаемых здесь. Чаще всего достаточно просто настроить
5.4. Использование XGBoost 223
learning_rate, задать количество шагов оптимизации и min_child_weight для
предотвращения переобучения отдельных деревьев решений в процессе градиентного бустинга. Кроме того, может быть полезно установить параметры objective,
max_depth, colsample_bytree и subsample, но маловероятно, что настройка многочисленных других доступных параметров даст значительные улучшения. Это
касается не только XGBoost, но и других реализаций градиентного бустинга.
Далее мы объясним, почему XGBoost обеспечивает лучшую производительность
вычислений и прогнозов.
5.4.2. Как работает XGBoost
Как поясняется в статье «XGBoost: A Scalable Tree Boosting System» Тяньци
Чена и Карлоса Гестрина (Proceedings of the 22nd ACM SIGKDD International
Conference on Knowledge Discovery and Data Mining, 2016), высокая производительность XGBoost сводится к нескольким инновациям, которых не было
в других реализациях:
блок столбцов для параллельного обучения;
аппроксимация второго порядка для более быстрой оптимизации;
улучшенные алгоритмы поиска разбиений;
поиск разбиений с учетом разреженности данных.
Блок столбцов — это метод, используемый в параллельном обучении, который
включает разбиение датасета на блоки столбцов (подмножества признаков).
Это позволяет проводить обучение параллельно на нескольких процессорах,
значительно сокращая общее время обучения. Это хорошо видно при обучении
модели XGBoost: загрузка CPU указывает на использование нескольких ядер.
XGBoost не может использовать несколько ядер для одновременного обучения
нескольких моделей, как это происходит в других ансамблях, например в случайных лесах. Это связано с тем, что градиентный бустинг — последовательная
модель, в которой каждое дерево обучается на основе результатов предыдущего.
Вместо этого процесс обучения каждой отдельной модели распределяется между
несколькими ядрами для повышения эффективности и скорости.
В настоящее время XGBoost можно использовать в Python через два различных API: Native API и scikit-learn API. В этой книге мы будем применять
исключительно второй из-за его преимуществ с точки зрения лучших практик
моделирования и дополнительной возможности легко применять различные
инструменты из библиотеки scikit-learn, такие как подбор модели и пайплайны,
как объяснялось в главе 4.
При использовании Native API пользователю необходимо преобразовать свои
данные в DMatrix, которая является внутренней структурой данных XGBoost,
224 Глава 5. Деревья решений и градиентный бустинг
оптимизированной как для эффективности потребления памяти, так и для
скорости обучения (https://mng.bz/VVxP). Применение формата DMatrix делает
возможным метод блока столбцов. Однако при использовании scikit-learn API
пользователи могут вводить свои данные в виде pandas DataFrames или массивов Numpy без необходимости явного преобразования в формат DMatrix.
Это связано с тем, что XGBoost выполняет преобразование «под капотом», что
делает процесс более оптимизированным. Поэтому можно смело выбирать API,
который лучше всего соответствует вашим предпочтениям: оба они предлагают
одинаковую производительность и отличаются только некоторыми параметрами,
значениями по умолчанию и предоставляемыми опциями.
Аппроксимация второго порядка для ускорения оптимизации, включающая вторую производную (градиент, полученный из первой производной), основана на
более комплексной методике поиска корней — методе Ньютона. В контексте минимизации мы часто называем этот метод ньютоновским, а не градиентным спус
ком. В листинге 5.13 он реализован как новый класс, NewtonianGradientBoosting,
который наследует исходный класс GradientBoosting с некоторыми дополнения
ми и модификациями его существующих методов и атрибутов. В частности, мы
добавляем вычисления матрицы Гессе для балансировки шагов градиента с целью
ускорения сходимости и член регуляризации для предотвращения переобучения.
Листинг 5.13. Как работает XGBoost
Определяет новый класс
NewtonianGradientBoosting как
подкласс GradientBoosting
class NewtonianGradientBoosting(GradientBoosting):
"""для обновления прогнозов используется метод Ньютона-Рафсона"""
reg_lambda = 0.25
def hessian(self, y_true, y_pred):
hessian = np.ones_like(y_true)
return hessian
Устанавливает параметр
регуляризации reg_lambda
Инициализирует константную
матрицу Гессе единицами
def fit(self, X, y):
self.init = self.logit(np.mean(y))
y_pred = self.init * np.ones((X.shape[0],))
for k in range(self.n_estimators):
gradient = self.gradient(self.logit(y), y_pred)
hessian = self.hessian(self.logit(y), y_pred)
tree = DecisionTreeRegressor(**self.params)
tree.fit(
X,
-gradient / (
Обучает дерево решений, деля
hessian + self.reg_lambda
отрицательный градиент на сумму матрицы
)
Гессе и параметра регуляризации
)
self.trees.append(tree)
y_pred += self.learning_rate * tree.predict(X)
cls = NewtonianGradientBoosting(n_estimators=300,
learning_rate=0.1,
max_depth=4,
min_samples_leaf=3,
Создает экземпляр класса
NewtonianGradientBoosting
gradient = self.gradient(self.logit(y), y_pred)
hessian = self.hessian(self.logit(y), y_pred)
tree = DecisionTreeRegressor(**self.params)
tree.fit(
X,
-gradient / (
Обучает
дерево решений,
деля 225
Использование
XGBoost
hessian + self.reg_lambda 5.4.отрицательный
градиент на сумму матрицы
)
Гессе и параметра регуляризации
)
self.trees.append(tree)
y_pred += self.learning_rate * tree.predict(X)
cls = NewtonianGradientBoosting(n_estimators=300,
learning_rate=0.1,
max_depth=4,
min_samples_leaf=3,
random_state=0)
Создает экземпляр класса
NewtonianGradientBoosting
с заданными гиперпараметрами
cls.fit(X, y)
Обучает модель NewtonianGradientBoosting
preds = cls.predict(Xt)
score = accuracy_score(y_true=yt, y_pred=preds) на обучающих данных
print(f"Accuracy: {score:0.5f}")
Прогнозирует целевые значения, используя обученную
модель, и вычисляет долю верных результатов
Новое значение доли верных результатов немного лучше, чем то, которое мы
получили из первоначального класса GradientBoosting:
Accuracy: 0.82514
В нашем случае гессиан, вероятно, не особенно полезен, поскольку одинаков
для всех наблюдений из-за используемой целевой функции (квадратичной
ошибки). Однако в контексте оптимизации с другими целевыми функциями
матрица Гессе содержит информацию об их кривизне, и это можно использовать
для определения направления и скорости их изменения. Интуитивно понятно,
что при большей кривизне значения Гессе увеличиваются, что снижает влияние
градиента, действуя как тормоз для скорости обучения. Напротив, меньшая
кривизна приводит к ускорению обучения. Используя гессиан, вы получаете
адаптивную скорость обучения для каждого из обучающих примеров. Однако,
как побочный эффект, вычисление второй производной часто может быть сложным или даже невозможным. Аналитические выражения и численные методы
для определения второй производной требуют значительных вычислительных
усилий. В следующей главе, посвященной более сложным темам, мы предоставим дополнительную информацию о том, как создавать пользовательские
целевые функции, вычисляя градиенты и гессианы аналитически и численными
методами.
Важную роль в оптимизации по Ньютону, используемой XGBoost, также играет
член регуляризации, который суммируется с гессианом и еще больше уменьшает
целевую переменную, то есть корректировку, которую нужно оценить с помощью
базовых алгоритмов. Другая идея, заимствованная из градиентного спуска, которую использует XGBoost, — это регуляризации L2, как это реализовано в нашем примере, и L1. Дополнительные члены регуляризации помогают сгладить
окончательные веса и избежать переобучения, напрямую модифицируя шаг
ньютоновского спуска. Следовательно, важно учитывать, как подбирать значения L1 и L2, называемые лямбда и альфа в XGBoost и LightGBM, поскольку
226 Глава 5. Деревья решений и градиентный бустинг
они являются значимыми гиперпараметрами для улучшения результатов оптимизации и снижения переобучения. Эти значения регуляризации гарантируют,
что ньютоновский спуск будет делать меньшие шаги в процессе оптимизации.
В следующем разделе мы продолжим изучать новые возможности, представленные XGBoost, изучив вклад алгоритмов поиска разбиений в повышение скорости
работы, обеспечиваемое алгоритмом.
5.4.3. Разбиение на основе гистограмм
Градиентный бустинг основан на бинарных деревьях, которые работают путем
разбиения данных, чтобы добиться более оптимизированной по сравнению
с исходным набором целевой метрики в полученных разбиениях. Поскольку
градиентный бустинг рассматривает все признаки как числовые, он использует
уникальный способ принятия решения о том, как провести разбиение. Чтобы
найти нужный признак и правило, бинарное дерево должно перебрать все признаки, отсортировать каждый из них и оценить каждую точку разбиения. В итоге
выбирается тот признак и та точка разбиения, которые привели к максимальному
улучшению целевой функции.
С появлением более крупных датасетов процедура разбиения в деревьях решений
создает серьезные проблемы масштабируемости и вычислительной сложности
для исходной архитектуры GBDT, основанной на цепочках моделей, которые
обрабатывают данные друг за другом. С вычислительной точки зрения основные
затраты в GBDT приходятся на обучение деревьев решений, а наиболее трудоемкая часть этого процесса — поиск лучших точек разбиения.
Постоянный поиск лучшей точки разбиения занимает довольно много времени,
что делает алгоритм весьма требовательным при обучении на большом количестве признаков и наблюдений. Разбиение на основе гистограмм помогает
сократить время, заменяя значение каждого признака точками разбиения гистограммы для суммирования ее значений. В листинге 5.14 имитируется поиск
разбиения для нашей задачи. При этом мы определяем целевую и функцию
разбиения, которые могут работать как исходный алгоритм разбиения дерева
или как более быстрое разбиение на основе гистограмм.
Листинг 5.14. Разбиение на основе гистограмм
import numpy as np
def gini_impurity(y):
_, counts = np.unique(y, return_counts=True)
probs = counts / len(y)
return 1 - np.sum(probs**2)
Функция, вычисляющая
и возвращающая коэффициент
Джини для набора меток y
def histogram_split(x, y, use_histogram, n_bins=256):
if use_histogram:
Если use_histogram равно true, вычисляет
hist, thresholds = np.histogram(
гистограмму для выбранного признака
x, bins=n_bins, density=False
)
Если use_histogram равно false, перебирает
else:
все уникальные значения признака
thresholds = np.unique(x)
best_score = -1
import numpy as np
def gini_impurity(y):
Функция, вычисляющая
_, counts = np.unique(y, return_counts=True)
и возвращающая коэффициент
probs = counts / len(y)
Джини для набора меток
y 227
5.4. Использование
XGBoost
return 1 - np.sum(probs**2)
def histogram_split(x, y, use_histogram, n_bins=256):
if use_histogram:
Если use_histogram равно true, вычисляет
hist, thresholds = np.histogram(
гистограмму для выбранного признака
x, bins=n_bins, density=False
)
Если use_histogram равно false, перебирает
else:
все уникальные значения признака
thresholds = np.unique(x)
best_score = -1
best_threshold = None
Инициализирует наилучший показатель
for threshold in thresholds:
и пороговое значение
left_mask = x <= threshold
Перебирает все возможные
right_mask = x > threshold
пороговые значения
left_y = y[left_mask]
right_y = y[right_mask]
Разделяет y на левое и правое
score = (
подмножества в соответствии
gini_impurity(left_y) * len(left_y)
с выбранным пороговым значением
+ gini_impurity(right_y) * len(right_y)
)
Вычисляет коэффициент
if score > best_score:
неоднородности Джини для левого
best_threshold = threshold
и правого подмножеств
best_score = score
return best_threshold, best_score
Обновляет наилучший показатель
Возвращает наилучшее пороговое значение
и соответствующий ему коэффициент
неоднородности Джини
и пороговое значение, если текущее
разбиение имеет более высокий
коэффициент неоднородности Джини,
чем предыдущее наилучшее разбиение
В коде в листинге 5.14 после определения функции оценки (коэффициент неоднородности Джини) мы определяем функцию, которая выбирает признак
и перебирает значения его потенциальных разбиений для оценки. При базовом
подходе учитываются все уникальные значения признака. При использовании
подхода с гистограммами вычисляется гистограмма с 256 бинами, и в качестве
кандидатов на разбиение используются границы этих бинов. Если признак
имеет более 256 уникальных значений, использование гистограммы сэкономит
много времени, так как мы просто перебираем всех кандидатов на разбиение
и оцениваем их с помощью функции оценки.
Теперь, когда мы объяснили работу функций из кода, мы готовы опробовать его
на нашем датасете. Мы решили оптимально разделить признак latitude в задаче
классификации, чтобы спрогнозировать, находится ли объект недвижимости
в верхнем или нижнем ценовом диапазоне. Поскольку признак latitude имеет
много уникальных значений (Манхэттен представляет собой длинный узкий
остров, вытянутый с севера на юг, где стоимость недвижимости варьируется в зависимости от географической широты), эта задача должна оказаться сложной,
так как нам потребуется оценить множество различных широт относительно
целевой переменной.
В первом тесте мы пытаемся найти наилучшее разбиение, оценивая все уникальные значения, которые представляет признак:
228 Глава 5. Деревья решений и градиентный бустинг
%%time
histogram_split(x=data.latitude, y=target_median, use_histogram=False)
CPU times: user 46.9 s, sys: 10.1 ms, total: 46.9 s
Wall time: 46.9 s
(40.91306, 24447.475447387256)
Во втором тесте полагаемся на оценку точек разбиения, которые были найдены
с помощью гистограммы с 256 бинами, построенной на основе признака:
%%time
histogram_split(
x=data.latitude,
y=target_median,
use_histogram=True,
n_bins=256
)
CPU times: user 563 ms, sys: 0 ns, total: 563 ms
Wall time: 562 ms
(40.91306, 24447.475447387256)
Заглянув под капот алгоритма разбиения по гистограммам, мы обнаружим биннинг, при котором значения переменной группируются в дискретные интервалы
(бины), и каждому из них присваивается уникальное целое число для сохранения порядка между ними. Биннинг также называют разбиением по k-бинам,
где k в названии относится к числу групп, на которые перегруппировывается
числовая переменная. Этот метод используется при построении гистограмм, для
которых вы можете самостоятельно или автоматически задать значение k для
суммирования и представления распределения ваших данных.
Ускорение достигается не только за счет меньшего количества точек разбиения
для оценки (их можно тестировать параллельно, используя многоядерные архитектуры), но и благодаря тому, что гистограммы представляют собой целочисленные структуры данных, которые гораздо быстрее обрабатывать, чем векторы
непрерывных значений.
XGBoost применяет алгоритм для вычисления наилучшего разбиения на основе
предварительной сортировки значений и использования гистограмм. Алгоритм
с предварительной сортировкой работает следующим образом.
Для каждого узла перебираются все признаки.
Для каждого признака примеры сортируются по их значениям.
С помощью линейного поиска и гистограмм определяется наилучшее разбиение для признака и вычисляется прирост информации.
Выбирается лучшее решение среди всех признаков и их наилучших точек
разбиения.
5.4. Использование XGBoost 229
XGBoost имеет и другие концептуальные улучшения. Традиционный алгоритм
поиска разбиения обозначается как exact (значение параметра tree_method).
А вот взвешенный квантильный скетч (Weighted Quantile Sketch), обозначаемый
как approx в API XGBoost, является эксклюзивной особенностью, уникальной
для XGBoost. Этот метод поиска разбиения использует аппроксимации и информацию, полученную из статистики градиента. Метод определяет потенциальные точки разбиения среди кандидатов с помощью квантилей. В частности,
квантили взвешиваются для приоритизации выбора кандидатов, способных
уменьшить высокие градиенты и снизить вероятность значительных ошибок
прогнозирования.
Взвешенный квантильный скетч с использованием гистограмм теперь доступен
как tree_method="hist", который, начиная с версии 2.0.0, устанавливается по
умолчанию. В отличие от него, метод approx генерирует новый набор бинов для
каждой итерации, тогда как метод hist повторно использует бины в течение
множества итераций.
Другая особенность алгоритма связана с хранением данных в DMatrices. Наиболее затратной частью обучения дерева является сортировка данных. Чтобы
снизить эту стоимость, было предложено хранить данные в единицах хранения
памяти — блоках. Это позволяет линейно сканировать предварительно отсор
тированные записи и распараллеливать процесс, обеспечивая эффективный
параллельный алгоритм для поиска разбиений.
После успешной реализации агрегации через гистограммы в LightGBM, XGBoost
также принял этот подход. Гистограммная агрегация является ключевой особенностью и HistGradientBoosting — градиентного бустинга на основе гистограмм
в scikit-learn, который мы представим после LightGBM.
5.4.4. Применение ранней остановки в XGBoost
Раздел 5.3.1 иллюстрирует, как работает ранняя остановка с градиентным бустингом scikit-learn. XGBoost также поддерживает раннюю остановку. Вы можете
активировать ее, добавив несколько аргументов при создании экземпляра модели
XGBClassifier или XGBRegressor:
early_stopping_rounds — это количество раундов, в течение которых обу
чение будет продолжаться без улучшения показателя на валидационной выборке перед остановкой. Если задать положительное целое число, обучение
остановится, когда производительность на наборе проверки не улучшится
за указанное количество раундов;
eval_metric — это метрика оценки, используемая для ранней остановки. По
умолчанию XGBoost использует rmse (среднеквадратичная ошибка) для
регрессии и error (доля верных результатов) для классификации. Тем не
230 Глава 5. Деревья решений и градиентный бустинг
менее, вы можете указать любую другую метрику из обширного списка (доступного по адресу https://mng.bz/xK2W), а также задать свою метрику (это
будет обсуждаться в следующей главе, посвященной продвинутым темам
машинного обучения).
Помимо установки этих параметров, для обучения необходимо также указать
выборку с целевой переменной для мониторинга метрики оценки. Это делается
с помощью параметра eval_set, который содержит список кортежей со всеми
валидационными выборками и соответствующими им значениями целевой
переменной. В нашем примере мы используем только валидационный набор,
но если указано несколько наборов, XGboost будет использовать для остановки
только последний кортеж.
В листинге 5.15 мы повторяем тот же подход, с которым экспериментировали
ранее, разделяя данные на обучающую и тестовую выборки. Однако для надлежащего мониторинга метрики оценки мы дополнительно разделяем обучающую
выборку, чтобы выделить из нее валидационную.
Листинг 5.15. Применение ранней остановки к XGBoost
train, test = train_test_split(
range(len(data)),
Разделяет индексы данных на обучающий
test_size=0.2,
и тестовый наборы, используя фиксированное
random_state=0
случайное начальное число
)
train, validation = train_test_split(
train,
Далее разделяет обучающий набор на
test_size=0.2,
обучающий и валидационный, используя
random_state=0
)
то же случайное начальное число
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=1000,
max_depth=4,
min_child_weight=3,
early_stopping_rounds=100,
eval_metric='error')
Инициализирует классификатор
XGBoost с ранним остановом
на 100 раундов
Использует параметр error,
эквивалентный доли верных
результатов accuracy, в качестве
метрики оценки
X = column_transform.fit_transform(data.iloc[train])
y = target_median[train]
Xv = column_transform.transform(data.iloc[validation])
yv = target_median[validation]
xgb.fit(X, y, eval_set=[(Xv, yv)], verbose=False)
Xt = column_transform.transform(data.iloc[test])
yt = target_median[test]
preds = xgb.predict(Xt)
score = accuracy_score(y_true=yt, y_pred=preds)
print(f"Accuracy: {score:0.5f}")
Обучает классификатор XGBoost
на обучающих данных X,
присваивает метки y и оценивает
производительность на
валидационных данных Xv и yv
Выводит на экран долю верных
результатов после сравнения
предсказанных меток с истинными
метрики оценки
Xv = column_transform.transform(data.iloc[validation])
yv = target_median[validation]
Обучает классификатор XGBoost
на обучающих данных X,
присваивает метки y и оценивает
5.5. Введение
в LightGBM 231
производительность на
Xt = column_transform.transform(data.iloc[test])
валидационных данных Xv и yv
yt = target_median[test]
xgb.fit(X, y, eval_set=[(Xv, yv)], verbose=False)
preds = xgb.predict(Xt)
score = accuracy_score(y_true=yt, y_pred=preds)
print(f"Accuracy: {score:0.5f}")
Выводит на экран долю верных
результатов после сравнения
предсказанных меток с истинными
После завершения обучения мы видим, что значение доли верных результатов
(accuracy) немного уступает нашим предыдущим результатам кросс-валидации,
поскольку модель получена путем обучения на меньшем количестве примеров,
то есть на 64 % доступных данных, ведь мы зарезервировали 20 % для тестирования и 16 % для валидации:
Accuracy: 0.82657
Во время обучения постоянно отслеживалась метрика оценки, и процедура обу
чения прерывалась, если улучшение не наблюдалось в течение большего числа
раундов, чем указано в early_stopping_rounds. Лучшая итерация автоматически сохраняется и используется для прогнозирования. Таким образом, модель
не требует дополнительных действий. Чтобы проверить, на какой итерации
произошла остановка, можно обратиться к атрибуту best_iteration. В нашем
примере xgb.best_iteration возвращает значение 200.
5.5. Введение в LightGBM
LightGBM был впервые представлен в статье 2017 года «LightGBM: A Highly
Efficient Gradient Boosting Decision Tree» Гуолином Ке (Guolin Ke) и его командой из Microsoft (https://mng.bz/AQdz). Недавно пакет достиг версии 4.3.0, которую мы тестировали в этой книге. По словам авторов, слово «light» («легкий»)
в названии подчеркивает более быстрое обучение алгоритма и меньший объем
памяти по сравнению с традиционными деревьями решений с градиентным
бустингом. Эксперименты на множестве публичных наборов данных продемонстрировали эффективность алгоритма и его способность ускорять процесс
обучения деревьев решений с градиентным бустингом более чем в 20 раз при
сохранении практически той же точности. LightGBM стал доступен как программное обеспечение с открытым исходным кодом на GitHub (https://github.
com/microsoft/LightGBM/), быстро завоевав популярность среди специалистов по
данным и машинному обучению.
LightGBM имеет много характеристик, схожих с XGBoost, таких как обработка
пропущенных значений, встроенная возможность преобразования категориальных переменных, обучение на GPU, распределенное параллельное обучение
и ограничения монотонности. Мы расскажем об этом подробнее в следующей
главе. Кроме того, LightGBM также поддерживает работу с разреженными данными. Однако его главное преимущество заключается в скорости, поскольку он
значительно быстрее XGBoost на многих задачах, и это сделало его популярным алгоритмом как в соревнованиях Kaggle, так и в реальных приложениях.
232 Глава 5. Деревья решений и градиентный бустинг
Сообщество Kaggle сразу же обратило на него внимание и стало включать в свои
конкурсные заявки наряду с уже популярным XGBoost. Веб-сайт mlcontests.com,
отслеживающий события в мире соревнований по data science, сообщил в 2022
году, что LightGBM стал предпочтительным инструментом среди победителей,
превзойдя XGBoost в популярности. Впечатляющие 25 % решений для задач
с табличными данными были основаны на LightGBM. Хотя алгоритм добился
сопоставимого успеха среди специалистов, XGBoost в целом остается более распространенным. Например, репозиторий XGBoost имеет гораздо больше звезд
на GitHub, чем репозиторий LightGBM.
LightGBM — это кроссплатформенная библиотека машинного обучения, доступная для Windows, Linux и MacOS. Ее можно установить с помощью различных
инструментов, таких как pip или conda, или собрать из исходного кода (см.
полное руководство по установке по адресу https://mng.bz/ZlEP). Синтаксис ее
использования аналогичен scikit-learn, что упрощает переход на LightGBM для
пользователей, знакомых с scikit-learn. При оптимизации градиентного спуска
LightGBM следует по стопам XGBoost, используя обновление Ньютона — Рафсона, которое заключается в делении градиента на матрицу Гессе. Ответ Гуолина Ке
на GitHub подтверждает это (см. https://github.com/microsoft/LightGBM/issues/5233).
Давайте протестируем алгоритм на той же задаче, которую мы ранее рассматривали с помощью GradientBoosting от scikit-learn и XGBoost (листинг 5.16).
Листинг 5.16. Классификатор LightGBM
from lightgbm import LGBMClassifier
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
lgbm = LGBMClassifier(boosting_type='gbdt',
n_estimators=300,
max_depth=-1,
min_child_samples=3,
force_col_wise=True,
verbosity=0)
model_pipeline = Pipeline(
[('processing', column_transform),
('lightgbm', lgbm)])
Инициализирует LGBMClassifier с заданным количеством оценщиков, максимальной глубиной дерева и минимальным количеством дочерних примеров
Принудительно строит гистограмму
по столбцам
Создает пайплайн модели, включающий
этап преобразования столбцов
и применение модели LGBMClassifier
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
Выполняет пятифолдовую кроссвалидацию с использованием
пайплайна модели и оценкой
доли верных результатов
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"CV Accuracy {mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Выводит среднее значение
и стандартное отклонение
тестовых баллов,
полученных на кроссвалидации
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
Выполняет пятифолдовую кроссscoring=accuracy,
валидацию с использованием
cv=cv,
5.5.
Введение
в LightGBM
233
пайплайна
модели и оценкой
return_train_score=True,
доли верных результатов
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"CV Accuracy {mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Выводит среднее значение
и стандартное отклонение
тестовых баллов,
полученных на кроссвалидации
Ниже приведены впечатляющие результаты в плане доли верных результатов,
времени обучения и прогнозирования:
0.826 (0.004) обучение: 1.16 с, прогнозирование: 0.16 с.
Как и XGBoost, LightGBM управляет градиентным спуском с помощью таких
параметров, как n_estimators, learning_rate, lambda_l1 и lambda_l2 (регуляризация L1 и L2 соответственно). Наиболее важными из них, помогающими
контролировать сложность модели, являются следующие:
max_depth — контролирует максимальную глубину каждого дерева в ансамб
ле. Более высокое значение увеличивает сложность модели, делая ее более
склонной к переобучению. Если установлено значение –1, это означает, что
рост деревьев не ограничен;
num_leaves — определяет максимальное количество листьев в дереве и, сле-
довательно, сложность модели. Чтобы избежать переобучения, его следует
установить на значение меньше 2**(max_depth);
min_data_in_leaf — этот параметр контролирует минимальное количество
образцов, которые должны присутствовать в каждом листовом узле. Более высокое значение может предотвратить излишнее углубление дерева
и переобучение, но также может привести к недообучению, если установлено слишком большое число. Значение по умолчанию — 20. Мы предлагаем
попробовать более низкие, например 10, а затем провести эксперименты,
доведя до 300.
Параметры feature_fraction и bagging_fraction управляют тем, как LightGBM
делает выборки из признаков и примеров:
feature_fraction — управляет долей признаков, используемых при каждом
разбиении. Аналогичен параметру colsample_bytree в XGBoost и может по-
мочь уменьшить переобучение, не давая модели слишком сильно полагаться
на какой-либо отдельный признак;
bagging_fraction — определяет долю данных, которая будет использоваться
для каждого дерева. Как и параметр subsample в XGBoost, он способствует
снижению переобучения и ускорению обучения за счет случайной выборки
из данных;
234 Глава 5. Деревья решений и градиентный бустинг
bagging_freq — этот параметр, которого нет в XGBoost. Он определяет часто-
ту применения бэггинга. Значение 0 отключает бэггинг, даже если указано
bagging_fraction. Значение n означает, что бэггинг применяется на каждой
n-й итерации. Например, значение 2 означает, что бэггинг активируется через
каждые две итерации (в половине случаев).
Что касается процесса обучения LightGBM, параметр verbosity контролирует
уровень детализации выходной информации, в то время как force_col_wise
указывает на построение гистограмм для разбиения признаков по столбцам
во время построения дерева. LightGBM может строить гистограммы как по
столбцам, так и по строкам. Первый вариант, как правило, быстрее, но может
потребовать больше памяти, особенно для датасетов с большим количеством
столбцов. Построение гистограмм по строкам медленнее, но более эффективно
расходует память при работе с данными, содержащими множество столбцов.
LightGBM автоматически выбирает оптимальный метод построения гистограмм
для конкретного датасета. Однако вы также можете принудительно задать метод,
установив параметры force_col_wise или force_row_wise.
Как и XGBoost, LightGBM поддерживает различные базовые обучающиеся
алгоритмы через параметр boosting:
gbdt — вариант по умолчанию, использующий деревья решений в качестве
базовых учеников;
rf — реализует алгоритм случайных лесов;
dart — реализует алгоритм Dropouts meet Multiple Additive Regression Trees
(выпадения соответствуют множественным аддитивным деревьям регрессии).
Кроме того, установка параметра linear_tree в значение true при использовании
варианта по умолчанию boosting=gbdt позволяет создать кусочно-линейное
дерево с градиентным бустингом, то есть деревья решений, в конечных узлах
которых используются линейные модели. Это компромиссное решение, которое
сочетает возможности обучения деревьев решений с экстраполяционными способностями линейных моделей для работы с новыми данными или выбросами.
В следующем разделе мы подробно рассмотрим все инновации, отличающие
LightGBM от XGBoost.
5.5.1. Как LightGBM строит деревья
Давайте рассмотрим ключевые особенности, которые отличают LightGBM от
XGBoost, начав с того, как LightGBM строит деревья. В отличие от XGBoost,
который использует поуровневый рост (подход depth-first, «сначала в глубину»),
5.5. Введение в LightGBM 235
LightGBM применяет полистовой (best first, «сначала лучшие»). Это означает,
что алгоритм выбирает лист с максимальным приростом полезности и разделяет
его, пока это целесообразно. Напротив, поуровневый подход одновременно разделяет все узлы на одном уровне глубины.
Таким образом, в XGBoost все ветвления дерева растут до одинаковой глубины,
и разбиение происходит синхронно. Это может приводить к созданию множества
незначимых разбиений, слабо влияющих на итоговый прогноз. А в LightGBM
полистовой рост фокусируется на разбиении узлов с максимальным снижением ошибки, что приводит к меньшему количеству узлов, но большей точности.
Такой подход позволяет модели сосредоточиться только на важных признаках,
значительно влияющих на целевую переменную. Это означает, что алгоритм
будет быстро сходиться к оптимальному решению с меньшим количеством
разбиений и меньшим количеством деревьев.
На рис. 5.10 представлены два подхода: слева — поуровневый рост, справа — полистовой, при этом оба ограничены максимальным количеством конечных
узлов (четырьмя). Эти два подхода кардинально различаются в выборе правил
и стратегии сегментации данных.
Рис. 5.10. Отличия поуровневого (слева) и полистового (справа) роста деревьев
Важно отметить, что, если позволить двум деревьям полностью вырасти на
одних и тех же данных — одно с использованием полистового, а другое с применением поуровневого подхода, — они определят одни и те же конечные листья
и дадут идентичные прогнозы. Различие заключается в процессе построения:
полистовой подход более агрессивен и сначала разделяет узлы с наибольшим
приростом информации.
Это означает, что различия между подходами проявляются, когда применяются
правила остановки (например, ограничение на количество конечных узлов или
глубину дерева). В этом случае полистовой подход может привести к созданию
меньших по размеру деревьев, более быстрому обучению и большей точности,
но он также несет риск переобучения. Для контроля глубины и снижения переобучения в LightGBM используется параметр max_depth, который ограничивает
максимальную глубину дерева.
236 Глава 5. Деревья решений и градиентный бустинг
5.5.2. Увеличение скорости с помощью эксклюзивного
объединения признаков и одностороннего семплирования
на основе градиента
Чтобы сделать обучение еще быстрее, в градиентном бустинге и многих других
алгоритмах машинного обучения обрабатывают не все данные сразу, а только
часть. Самый простой способ — использовать стохастическую выборку (то есть
сокращение строк) и/или методы сокращения размерности, такие как семплирование столбцов или метод главных компонент (то есть сокращение столбцов).
Хотя семплирование может повысить точность при наличии шума в данных, его
чрезмерное применение может навредить процессу обучения и снизить прогностическую производительность. В LightGBM способ и объем семплирования
определяются алгоритмом одностороннего семплирования на основе градиента
(Gradient-Based One-Side Sampling, GOSS). Методы сокращения размерности
основаны на выявлении избыточности в данных и объединении их с помощью
линейных комбинаций, например взвешенных сумм. Однако линейные комбинации могут разрушать нелинейные зависимости в данных. Сокращение размерности путем отбрасывания редких сигналов может привести к снижению
точности модели, если успешное решение задачи зависит от них. В LightGBM
уменьшение размерности выполняется с помощью эксклюзивного объединения
признаков (Exclusive Feature Bundling, EFB), что позволяет уменьшить размерность столбца без потери информации.
Давайте разберемся с двумя основными способами увеличения скорости
в LightGBM, начав с принципа работы EFB. Это метод, который эффективно
уменьшает количество признаков, не нарушая целостность данных. При активном использовании one-hot-кодирования и бинарных признаков многие признаки становятся разреженными: содержат мало ненулевых значений и обилие
нулей. EFB позволяет сохранить все ненулевые значения без потерь, суммируя
эти признаки и перекодируя некоторые значения. LightGBM оптимизирует
вычисления и размерность данных, группируя эти признаки в эксклюзивные
бандлы (группы) признаков, гарантируя сохранение точности прогнозирования.
На рис. 5.11 показано, как можно эффективно объединить два признака. Решение включает добавление признака Б к признаку A только для ненулевых
значений с использованием максимального значения признака A. Такое объединение сохраняет порядок исходных признаков, поскольку значения признака A и признака Б остаются разделенными и располагаются в разных участках
распределения значений.
Поиск оптимального способа объединения эксклюзивных признаков — сложная
задача, классифицированная как NP-трудная. Однако, как показано в статье
Гуолина Ке и его команды о LightGBM, жадный алгоритм может обеспечить хорошее приближение, автоматически группируя множество признаков. Алгоритм
5.5. Введение в LightGBM 237
объединения признаков работает последовательно, выбирая те из них, у которых
наименьшее перекрытие значений, и связывая их вместе. В противном случае
алгоритм создает новый бандл, до тех пор пока не останется возможных кандидатов для объединения. Правило остановки определяется степенью конфликтов
между признаками: если уровень конфликтов превышает заданный порог gamma,
объединение прекращается и бандл не создается, а процесс останавливается,
если не находятся более подходящие кандидаты. Хотя полученные в результате
работы этого жадного механизма бандлы не гарантируют оптимальности, алгоритм обеспечивает приемлемое решение за разумное время.
Признак А
Признак Б
Бандл
1
0
1
2
0
2
1
0
1
2
0
2
0
0
0
0
1
3
0
2
4
0
3
5
0
4
6
Рис. 5.11. Демонстрация работы
EFB при объединении двух
признаков
Другое улучшение производительности, представленное в статье, — это GOSS.
Как мы уже упоминали, если EFB нацелен на снижение размерности столбцов,
GOSS работает со строками, эффективно выбирая их без смещения.
GOSS основан на идее, что некоторые экземпляры данных вряд ли предоставят
полезную информацию для поиска точки разбиения. Поиск по тщательно отобранному подмножеству обучающих данных может сэкономить время вычислений без ущерба для точности прогнозирования. Кроме того, в деревьях решений
с градиентным бустингом алгоритм неявно указывает вес для экземпляров
данных при оптимизации градиента для каждого из них. Определение весов
имеет решающее значение для вычисления корректировки предыдущих оценок,
но также может быть использовано для выборки образцов, которые могут быть
более интересными для обучения.
GOSS оценивает примеры с бˆольшими градиентами, чтобы внести бˆольший вклад
в прирост информации. Фокусировка на примерах с большими градиентами
и игнорирование части примеров с малыми позволяют сократить количество
обрабатываемых данных в ходе оптимизации. Процедура для GOSS сводится
к следующему.
Сначала GOSS сортирует примеры по абсолютному значению их градиентов.
Потом выбирает лучшие примеры из a × 100 %.
238 Глава 5. Деревья решений и градиентный бустинг
Случайным образом выбирает b × 100 % примеров данных из оставшихся.
Обучает дерево решений на объединенных выборках, используя вес 1 для
лучших примеров и вес (1 – a) / b для случайно выбранных.
Окончательное взвешивание необходимо для сохранения исходного распределения данных в датасете и предотвращения нежелательного сдвига в его
представлении.
GOSS может ускорить обучение деревьев решений с градиентным бустингом,
особенно при работе с большими датасетами и сложными деревьями. Авторы
оригинальной статьи продемонстрировали, что ошибка, вызванная аппроксимацией выборки в GOSS, становится незначительной для больших датасетов по
сравнению с традиционным методом. По нашему опыту использования GOSS,
в лучшем случае вы получаете схожие результаты по сравнению со стандартным
обучением LightGBM. Тем не менее увеличение скорости является значительным, что делает GOSS хорошим выбором для более быстрого экспериментирования при поиске правильных гиперпараметров или выборе наиболее релевантных
признаков для вашей задачи.
В отличие от других представленных нами методов ускорения обучения, GOSS не
используется по умолчанию: нужно явно указать, что вы хотите его использовать.
5.5.3. Применение ранней остановки в LightGBM
LightGBM поддерживает раннюю остановку, а параметры для ее контроля аналогичны тем, что используются в XGBoost. В примере в листинге 5.17 мы используем LightGBM для обучения и оцениваем производительность алгоритма
во время обучения на тестовом наборе данных. Если за 100 итераций улучшений
нет, алгоритм останавливает процесс и выбирает раунд итераций, который достиг наивысшей производительности на тестовом наборе.
Листинг 5.17. Применение ранней остановки в LightGBM
from lightgbm import LGBMClassifier, log_evaluation
train, test = train_test_split(range(len(data)), test_size=0.2,
random_state=0)
Разделяет индексы данных на обучающий
train, validation = train_test_split(
и тестовый наборы, используя
train,
фиксированное случайное начальное число
test_size=0.2,
random_state=0
)
Далее разделяет обучающий набор
lgbm = LGBMClassifier(boosting_type='gbdt',
early_stopping_round=150,
n_estimators=1000,
max_depth=-1,
min_child_samples=3,
force_col_wise=True,
verbosity=0)
на обучающую и валидационную части,
используя то же случайное начальное число
Инициализирует классификатор LightGBM
с несколькими оценщиками, максимальной
глубиной и минимальным количеством
дочерних образцов
X = column_transform.fit_transform(data.iloc[train])
y = target_median[train]
Обучает классификатор LightGBM
на обучающих данных X,
присваивает метки y и оценивает
random_state=0)
train, validation = train_test_split(
train,
test_size=0.2,
random_state=0
)
Разделяет индексы данных на обучающий
и тестовый наборы, используя
фиксированное случайное начальное число
lgbm = LGBMClassifier(boosting_type='gbdt',
early_stopping_round=150,
n_estimators=1000,
max_depth=-1,
min_child_samples=3,
force_col_wise=True,
verbosity=0)
Далее
разделяет обучающий
набор
5.5.
Введение
в LightGBM
239
на обучающую и валидационную части,
используя то же случайное начальное число
Инициализирует классификатор LightGBM
с несколькими оценщиками, максимальной
глубиной и минимальным количеством
дочерних образцов
X = column_transform.fit_transform(data.iloc[train])
y = target_median[train]
Xv = column_transform.transform(data.iloc[validation])
yv = target_median[validation]
lgbm.fit(X, y, eval_set=[(Xv, yv)],
eval_metric='accuracy',
callbacks=[log_evaluation(period=0)])
Xt = column_transform.transform(data.iloc[test])
yt = target_median[test]
preds = lgbm.predict(Xt)
score = accuracy_score(y_true=yt, y_pred=preds)
print(f"Accuracy на тестовых данных: {score:0.5f}")
Обучает классификатор LightGBM
на обучающих данных X,
присваивает метки y и оценивает
производительность на
валидационных данных Xv и yv
Устанавливает долю верных
результатов в качестве
метрики оценки
Устанавливает обратный
вызов для подавления
вывода оценки (period= 0)
Выводит на экран долю верных
результатов после сравнения
предсказанных меток с истинными
Даже в этом случае результат ухудшается из-за того, что мы обучаем только на
64 % доступных данных:
Accuracy на тестовых данных: 0.82585
Однако есть некоторые незначительные отличия от реализации XGBoost, которые можно заметить в коде. Параметр eval_metric использует другие названия
(которые вы можете посмотреть на https://mng.bz/RVZK), и для подавления вывода оценок во время обучения применяется не параметр verbose, как в XGBoost,
а особая функция обратного вызова (log_evaluation), которая должна быть
объявлена во время обучения в списке обратных вызовов.
В последнее время ранняя остановка также реализуется в виде функции обратного вызова (см. https://mng.bz/2yK0). Объявление раундов ранней остановки
во время создания экземпляра модели было сделано только для поддержания
совместимости API с XGBoost. В случае использования ранней остановки как
обратного вызова у вас будет больше контроля над тем, как LightGBM останавливает обучение:
first_metric_only позволяет указать, следует ли использовать только первую
метрику для ранней остановки или любую указанную вами метрику;
min_delta сигнализирует о минимальном улучшении метрики для продолже-
ния обучения, которое обычно задается равным нулю (любое улучшение), но
его можно увеличить, чтобы установить более строгий контроль над ростом
ансамбля.
240 Глава 5. Деревья решений и градиентный бустинг
В предыдущем примере вам просто нужно удалить early_stopping_rounds из
экземпляра LGBMClassifier и добавить соответствующий обратный вызов в список обратных вызовов в методе fit, чтобы получить тот же результат:
early_stopping(
stopping_rounds=150,
first_metric_only=True,
verbose=False,
min_delta=0.0
)
Какой бы подход вы ни использовали, индекс итерации, дающий наилучшую
оценку на валидации, будет сохранен в атрибуте best_iteration модели, и эта
итерация будет автоматически использоваться при прогнозировании.
5.5.4. Заставляем XGBoost работать, как LightGBM
С момента появления LightGBM и эффективного использования им несбалансированных деревьев решений XGBoost также начал поддерживать полистовую
стратегию в дополнение к своей оригинальной поуровневой, которая получила
название depthwise, тогда как первая — lossguide. Задавая то или другое значение с помощью параметра grow_policy, вы можете заставить модель XGBoost
вести себя как LightGBM. Кроме того, авторы XGBoost рекомендуют при использовании lossguide устанавливать следующие параметры, чтобы избежать
переобучения:
max_leaves — задает максимальное количество добавляемых узлов и актуален
только для lossguide;
max_depth — задает максимальную глубину дерева. Если grow_policy установлен на depthwise, то max_depth работает как обычно. Однако если grow_policy
установлен на lossguide, max_depth может быть установлен на ноль, что
означает отсутствие ограничения по глубине.
Кстати, в LightGBM есть те же самые параметры, которые можно использовать
для той же цели (max_leaves — это альтернативное рабочее название параметра
num_leaves).
5.5.5. Как LightGBM повлиял на scikit-learn
В версии 0.21 scikit-learn были добавлены две новые реализации деревьев с градиентным бустингом: HistGradientBoostingClassifier и HistGradientBoosting
Regressor, появившиеся под влиянием успеха LightGBM. Вы можете задаться вопросом, зачем нужна эта новая реализация, если современные версии LightGBM
и XGBoost предлагают все необходимое для разработки эффективных решений
для табличных данных на основе градиентного бустинга, а также обеспечивают
5.5. Введение в LightGBM 241
полную совместимость с API scikit-learn. Однако стоит уделить время изучению этой новой реализации. Подход на основе гистограмм, хотя и находится
на стадии развития, в будущем, как ожидается, заменит исходный алгоритм,
поскольку, предоставляя тот же контроль над процессом обучения и построением деревьев решений, демонстрирует при этом более высокую предиктивную
производительность по сравнению с XGBoost и LightGBM. Следовательно, его
стоит протестировать для конкретных задач.
По сравнению с исходной реализацией градиентного бустинга в scikit-learn,
новые подходы на основе гистограмм обладают дополнительными характеристиками:
биннинг;
многопоточность (первоначальная реализация была однопоточной);
нет поддержки разреженных данных;
встроенная поддержка пропущенных значений;
ограничения монотонности и взаимодействий;
собственные категориальные переменные.
На данный момент в новой гистограммной реализации нет поддержки разреженных данных. Следовательно, если ваши данные представлены в формате разреженной матрицы, ее следует сначала уплотнить. Кроме того, некоторые другие функции, типичные для GradientBoostingClassifier и GradientBoostingRegressor,
пока не поддерживаются — например, часть функций потерь.
Со стороны API большинство параметров не изменились по сравнению
с GradientBoostingClassifier и GradientBoostingRegressor. Исключением
является max_iter, параметр, который заменяет n_estimators. В листинге 5.18
показан пример использования HistGradientBoostingClassifier для нашей
задачи классификации объявлений по цене аренды объектов недвижимости из
датасета Airbnb NYC.
Листинг 5.18. Новый гистограммный градиентный бустинг в scikit-learn
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import accuracy_score
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
Инициализирует
HistGradientBoostingClassifier
с определенными
model = HistGradientBoostingClassifier(learning_rate=0.1,
гиперпараметрами
для
max_iter=300,
алгоритма бустинга
max_depth=4,
min_samples_leaf=3,
random_state=0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
Создает пайплайн, объединяющий
предварительную обработку столбцов
(column_transform) и модель
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
Инициализирует
HistGradientBoostingClassifier
с определенными
model = HistGradientBoostingClassifier(learning_rate=0.1,
гиперпараметрами
для
max_iter=300,
алгоритма бустинга
max_depth=4,
242 Глава 5. Деревья решений и градиентный бустинг
min_samples_leaf=3,
random_state=0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
Создает пайплайн, объединяющий
предварительную обработку столбцов
(column_transform) и модель
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Выполняет пятифолдовую
кросс-валидацию на
пайплане модели,
возвращая оценки
и обученные оценщики
Выводит на экран среднее
значение и стандартное
отклонение доли верных
результатов, полученных
на кросс-валидации
Результаты таковы:
0.827 (0.005) обучение: 1.71 с, прогнозирование: 0.13 с.
По сравнению с нашими предыдущими примерами с XGBoost и LightGBM, этот
отличается используемой командой и параметром max_iter, который заменяет
привычный n_estimators. Кроме того, новый алгоритм бустинга в scikit-learn
является гистограммным. Чтобы изменить начальное значение по умолчанию
255 (это 256 бинов, поскольку 1 зарезервирован для пропущенных значений),
достаточно просто установить параметр max_bins.
Алгоритм все еще находится на стадии разработки и не поддерживает разреженные данные. Это означает, что он не может работать так же быстро, как XGBoost
или LightGBM при наличии множества one-hot-закодированных признаков,
независимо от подготовки данных.
Итоги
Ансамблевые алгоритмы применяются для повышения предсказательной
силы одной модели путем использования нескольких или их объединения
в цепочку.
Ансамблевые алгоритмы часто основаны на деревьях решений.
Существуют две основные стратегии ансамбля: усреднение и бустинг.
Стратегии усреднения, такие как случайные леса, имеют тенденцию
уменьшать дисперсию прогнозов, при этом лишь незначительно увеличивая смещение.
Итоги 243
Пейстинг — это разновидность усреднения, которая включает в себя
создание набора различных моделей, обученных на подвыборках данных,
и объединение их прогнозов.
Бэггинг похож на усреднение, но вместо сабсемплинга используется
бутстрэппинг.
Методы усреднения могут быть ресурсоемкими и увеличивать смещение,
исключая важные части распределения данных посредством семплирования.
Случайные леса — это ансамблевый алгоритм обучения, который объединяет
деревья решений через бутстрэп-выборки и сабсемплинг признаков во время
моделирования (случайные патчи).
Он создает набор моделей, которые отличаются друг от друга, и дает более
надежные и точные прогнозы.
Его можно использовать для определения важности признаков и измерения схожести примеров в датасете.
Алгоритм требует тонкой настройки нескольких гиперпараметров, таких
как количество используемых деревьев, и регулировки компромисса между
смещением и дисперсией путем установки максимального количества признаков для разбиений, максимальной глубины деревьев и минимального
размера конечных ветвей.
Если количество деревьев слишком велико, это может привести к большим вычислительным затратам.
Особо случайные деревья (ERT) — это разновидность алгоритма случайных
лесов.
Он случайным образом выбирает признак для разбиения в каждом узле
дерева решений, что приводит к меньшей дисперсии (потому что деревья
более разнообразны), но большему смещению (рандомизация жертвует
предиктивной точностью).
Он более эффективен с точки зрения вычислений и полезен для больших датасетов с большим количеством коллинеарных и зашумленных
признаков.
Уменьшает дисперсию, делая результирующий набор деревьев менее
коррелированным.
GBDT (градиентный бустинг на основе деревьев решений) — высокоэффективный метод машинного обучения для задач анализа табличных данных.
Он стал ведущим подходом в различных областях, включая многоклассовую
классификацию, прогнозирование кликов по рекламе и ранжирование в поисковых системах. По сравнению с другими методами, такими как нейронные
244 Глава 5. Деревья решений и градиентный бустинг
сети, опорные векторы, случайные леса и ансамбли бэггинга, GBDT обычно
лучше справляется со стандартными табличными задачами.
Градиентный бустинг эффективен, поскольку сочетает в себе градиентный
спуск — процедуру оптимизации, типичную для линейных моделей и нейронных сетей, и деревья решений, обученные на градиентах, полученных из
суммы результатов предыдущих деревьев решений.
Библиотека scikit-learn предлагает один из самых ранних вариантов алгоритмов градиентного бустинга для задач регрессии и классификации. Недавно исходный алгоритм был заменен более быстрым вариантом на основе
гистограмм.
XGBoost, алгоритм для градиентного бустинга на деревьях решений, приобрел популярность после его успешного использования в соревновании
Higgs Boson Machine Learning Challenge на Kaggle. Он основан на более
сложной оптимизации с помощью метода Ньютона и предлагает следующие
преимущества:
возможность обработки различных типов входных данных;
поддержка настраиваемых функций (целевой и оценки);
автоматическая обработка пропущенных значений;
простота обучения на GPU;
учет ограничений монотонности и взаимодействий признаков;
оптимизация использования многоядерных процессоров и кеша на автономных компьютерах.
LightGBM — это высокоэффективный алгоритм градиентного бустинга на
деревьях решений, представленный в статье 2017 года Гуолином Ке и его
командой из Microsoft. Алгоритм был разработан для более быстрой работы
и меньшего потребления памяти по сравнению с традиционными методами
градиентного бустинга, что подтверждено экспериментами на множестве
общедоступных датасетов. Алгоритм LightGBM достигает этого благодаря
своей стратегии полистового разбиения и эксклюзивному объединению признаков (Exclusive Feature Bundling, EFB).
6
Продвинутые методы
обработки признаков
В этой главе
3 Обработка признаков с использованием более совершенных
методов
3 Отбор полезных признаков для создания более легких и понятных
моделей
3 Оптимизация гиперпараметров для повышения
производительности моделей
3 Особенности характеристик и опций градиентного бустинга на
деревьях решений
Мы обсудили деревья решений, их характеристики, ограничения и все их ансамб
левые модели — как основанные на случайных повторных выборках, например
случайные леса, так и на бустинге, например градиентный бустинг. Поскольку
бустинговые решения считаются современным стандартом для анализа табличных данных, мы подробно объяснили принципы их работы и оптимизацию их
прогнозов. В частности, представили две надежные реализации градиентного
бустинга, XGBoost и LightGBM, которые доказали свою эффективность и являются лучшими инструментами для специалистов по data science, работающих
с табличными данными.
246 Глава 6. Продвинутые методы обработки признаков
В этой главе будут рассмотрены более общие темы, касающиеся классического
машинного обучения. Однако основное внимание уделяется градиентному
бустингу на деревьях решений (GBDT), особенно XGBoost. Мы обсудим более
продвинутые методы обработки признаков, такие как многомерная импутация
пропущенных значений, кодирование по целевой переменной для преобразования высококардинальных категориальных признаков к числовым и общие подходы к анализу и преобразованию признаков на основе их взаимосвязи с целевой
переменной. Мы предложим несколько способов сократить количество признаков до наиболее значимых и оптимизировать гиперпараметры в зависимости от
доступных вычислительных ресурсов и выбранной модели. Завершается глава
разделом, посвященным продвинутым методам и опциям, связанным с GBDT.
6.1. Особенности обработки признаков
При работе с реальными табличными данными может возникнуть множество
проблем, и все обсуждавшиеся методы покажут неудовлетворительные результаты, если не адаптировать подходы к особенностям данных. Здесь мы рассмотрим несколько таких проблем: интеллектуальная обработка пропущенных
значений, преобразование категориальных признаков с большим количеством
уникальных значений и постобработка признаков после обучения модели для
дополнительного улучшения производительности. Конечно, это не исчерпывающий список, но он даст вам возможность попрактиковаться в выявлении
проблем и планировании соответствующего подхода.
Как и в предыдущей главе, для наших объяснений и примеров мы обратимся
к датасету Airbnb NYC, чтобы на практике разобрать сложные аспекты работы
с табличными данными. Листинг 6.1 повторяет структуру данных и некоторые
ключевые функции и классы, которые будут использоваться в этой главе.
Листинг 6.1. И снова датасет Airbnb NYC
import numpy as np
import pandas as pd
from sklearn.preprocessing import (
StandardScaler,
OneHotEncoder,
OrdinalEncoder
)
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
Список
исключенных
столбцов
data = pd.read_csv("./AB_NYC_2019.csv")
excluding_list = ['price', 'id', 'latitude', 'longitude', 'host_id',
'last_review', 'name', 'host_name']
low_card_categorical = [
Список категориальных столбцов
'neighbourhood_group',
с низкой кардинальностью для
'room_type'
one-hot-кодирования
]
high_card_categorical = ['neighbourhood']
Список категориальных столбцов
continuous = [
с высокой кардинальностью
'minimum_nights',
для порядкового кодирования
'number_of_reviews',
OrdinalEncoder
)
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
Список
исключенных
6.1. Особенности обработки признаков
247
столбцов
data = pd.read_csv("./AB_NYC_2019.csv")
excluding_list = ['price', 'id', 'latitude', 'longitude', 'host_id',
'last_review', 'name', 'host_name']
low_card_categorical = [
Список категориальных столбцов
'neighbourhood_group',
с низкой кардинальностью для
'room_type'
one-hot-кодирования
]
high_card_categorical = ['neighbourhood']
Список категориальных столбцов
continuous = [
с высокой кардинальностью
'minimum_nights',
для порядкового кодирования
'number_of_reviews',
'reviews_per_month',
'calculated_host_listings_count',
Список столбцов с непрерывными
'availability_365'
признаками
]
target_mean = (
Создает бинарную целевую переменную
(data["price"] > data["price"].mean())
(несбалансированную), указывающую,
.astype(int)
превышает ли цена price среднее значение
)
target_median = (
Создает бинарную целевую переменную
(data["price"] > data["price"].median())
(сбалансированную), указывающую,
.astype(int)
превышает ли цена price медиану
)
Создает многоклассовую целевую
target_multiclass = pd.qcut(
переменную, разделяя цену на пять
data["price"], q=5, labels=False
)
классов по квантилю
target_regression = data["price"]
categorical_onehot_encoding = OneHotEncoder(handle_unknown='ignore')
categorical_ord_encoding =
OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=np.nan)
numeric_standardization = Pipeline([('StandardScaler', StandardScaler()),
('Imputer',
SimpleImputer(strategy="constant", fill_value=0))])
Задает целевую
column_transform = ColumnTransformer(
[
('low_card_categories',
categorical_onehot_encoding,
low_card_categorical),
('high_card_categories',
categorical_ord_encoding,
high_card_categorical),
('numeric',
numeric_standardization,
continuous)
],
remainder='drop',
verbose_feature_names_out=True,
sparse_threshold=0.0)
lm_column_transform = ColumnTransformer(
[
('low_card_categories',
categorical_onehot_encoding,
low_card_categorical),
('numeric',
numeric_standardization,
continuous)
],
remainder='drop',
verbose_feature_names_out=True,
sparse_threshold=0.0)
переменную для регрессии
как столбец цены price
Создает преобразователь столбцов
ColumnTransformer, который применяет различные
преобразования к разным группам признаков
Создает преобразователь столбцов
ColumnTransformer, подходящий
для линейных моделей
remainder='drop',
verbose_feature_names_out=True,
sparse_threshold=0.0)
Создает преобразователь столбцов
ColumnTransformer, который применяет различные
преобразования к разным группам признаков
lm_column_transform = ColumnTransformer(
[
248 ('low_card_categories',
Глава 6. Продвинутые методы обработки признаков
categorical_onehot_encoding,
low_card_categorical),
('numeric',
numeric_standardization,
continuous)
],
Создает преобразователь столбцов
remainder='drop',
ColumnTransformer, подходящий
verbose_feature_names_out=True,
для линейных моделей
sparse_threshold=0.0)
Обратитесь к предыдущей главе, чтобы уточнить все детали того, что делает
код. Единственное дополнение — это преобразователь столбцов, разработанный
специально для линейных моделей. Он обрабатывает только категориальные
признаки с низкой кардинальностью, выполняя one-hot-кодирование категориальных признаков, кроме высококардинальных.
6.1.1. Многомерная импутация пропущенных значений
Наличие пропущенных данных в табличном датасете является серьезной проблемой, поскольку классические алгоритмы машинного обучения не имеют для
них встроенной обработки, за исключением решений на основе GBDT (таких
как XGBoost, LightGBM и HistGradientBoosting от scikit-learn). Кроме того,
даже если выбранный вами алгоритм GBDT может обрабатывать пропуски,
как будет показано в следующем разделе, прямая импутация может оказаться
более эффективной, поскольку позволяет заранее проверить, как обрабатывается
каждый признак или конкретный случай.
В главе 2 мы обсудили простые методы импутации, такие как использование
среднего или медианы, а также полезность создания индикаторов пропущенных
значений (missing indicator), что позволяет алгоритмам легче выявлять паттерны
пропусков. В этом разделе будут предоставлены более подробные сведения об
этих методах и многомерной импутации (multivariate imputation).
Прежде всего, если пропущенные случаи не зависят от ненаблюдаемых переменных (таких признаков, к которым нет доступа), пропущенные данные можно
классифицировать так:
Пропущенные полностью случайно (missing completely at random, MCAR) —
в этом сценарии пропуски данных не связаны с наблюдаемыми или ненаблюдаемыми переменными. Пропуски происходят случайным образом по
всему датасету.
Пропущенные случайно (missing at random, MAR) — предполагается, что
наблюдаемые переменные могут объяснить пропуски. Другими словами,
вероятность пропусков зависит исключительно от наблюдаемых данных.
Когда пропущенные примеры зависят от ненаблюдаемых значений самих пропущенных данных, вы имеете дело со случаем «данных, пропущенных неслучайно»
6.1. Особенности обработки признаков 249
(missing not at random, MNAR), который требует довольно специализированного подхода, выходящего за рамки этой книги. Однако предположим, что вам
известен механизм, приводящий к таким пропускам (например, отсутствие
данных о людях с очень высоким доходом из-за стремления к приватности или
о людях с очень низким доходом из-за ограниченного доступа к ним); тогда вы
можете попытаться собрать новые признаки, косвенно указывающие на эти
характеристики (например, уровень благосостояния), и тем самым перевести
задачу в категорию MAR.
Как правило, часто встречаются случаи, когда пропущенные данные относятся
к категориям MCAR или MAR. В обоих ситуациях, помимо простой импутации с использованием ожидаемого значения (которая отлично работает для
MCAR), можно более точно восстановить пропущенные данные с помощью
многомерной импутации. Многомерная импутация — это метод, который
использует корреляции между предикторами в датасете для подстановки
пропущенных значений. Это включает построение серии моделей для оценки
пропусков на основе связей между переменными. В этом подходе каждая модель
рассматривает признак с пропущенными значениями как целевую переменную
(моделируя только ее известные значения) и использует остальные признаки
как предикторы. Полученная модель затем используется для определения
того, какими значениями следует заменить пропуски в целевом признаке. Вы
можете задать, как алгоритм будет циклически проходить по признакам для
импутации. Обычно используется настройка по умолчанию: начинать с признаков с наименьшим количеством пропусков и постепенно переходить к тем,
у которых пропусков больше, что является предпочтительным и наиболее
эффективным подходом.
Для обработки пропущенных значений в предикторах выполняется начальный
шаг импутации с использованием простого среднего или другого базового
метода. Затем в ходе нескольких итераций процесс уточняет начальные оценки, включая результаты из моделей импутации. Этот итеративный процесс
продолжается до достижения стабильности импутированных значений, когда
дальнейшие итерации не приводят к значительным изменениям. Многомерная
импутация реализована в scikit-learn при помощи класса IterativeImputer
(https://mng.bz/MDZQ). Следуя идеям пакета R MICE (Multivariate Imputation by
Chained Equations: https://mng.bz/avEj), он позволяет выполнять как многомерную
(multivariate), так и множественную (multiple) импутацию — распространенный
подход в статистике и социальных науках, где вместо одного импутированного значения получают распределение правдоподобных замен. С помощью
IterativeImputer можно осуществлять множественные подстановки, запуская
его несколько раз с параметром sample_posterior, установленным в значение
True, и каждый раз используя другое случайное начальное число.
250 Глава 6. Продвинутые методы обработки признаков
Однако многомерная импутация является предпочтительным выбором в приложениях для работы с табличными данными, поскольку позволяет строить
модели на основе отдельных, но точных оценок. В нашем примере мы берем
непрерывные признаки датасета Airbnb NYC и случайным образом удаляем
5 % данных, таким образом имитируя ситуацию MCAR. После этого запускаем
SimpleImputer, заменяя пропущенные значения средним и IterativeImputer.
Наконец, с помощью средней абсолютной ошибки (MAE) сравниваем восстановленные каждым методом значения с исходными (листинг 6.2).
Листинг 6.2. Многомерная импутация
from sklearn.experimental import (
Импортирует IterativeImputer, который все еще
enable_iterative_imputer
находится в стадии доработки в scikit-learn
)
from sklearn.impute import SimpleImputer, IterativeImputer
from sklearn.ensemble import RandomForestRegressor
Xm = data[continuous].copy()
Создает копию данных
missing_percentage = 0.05
непрерывных признаков
np.random.seed(0)
mask = np.random.rand(*Xm.shape) < missing_percentage
Xm[mask] = np.nan
simple_imputer = SimpleImputer()
Xm_si = simple_imputer.fit_transform(Xm)
rf = RandomForestRegressor(random_state=0, n_jobs=-1)
multivariate_imputer = IterativeImputer(
estimator=rf,
max_iter=1,
tol=0.01
)
Xm_mi = multivariate_imputer.fit_transform(Xm)
Создает маску для
случайной маркировки
пропущенных значений
Использует экземпляр
SimpleImputer со стратегией
заполнения средним
значением
Создает экземпляр
RandomForestRegressor для
итеративной импутации
Создает экземпляр IterativeImputer
с max_iter и tol в качестве
критериев остановки
mae = pd.DataFrame(
{
Выполняет подстановку
"Простая": np.mean(
np.abs(data[continuous] - Xm_si), axis=0 пропущенных данных с помощью
),
итеративной импутации
"Многомерная": np.mean(
np.abs(data[continuous] - Xm_mi), axis=0
)
},
Подсчитывает MAE для
index = continuous
восполненных и исходных данных
)
print(mae)
В результате выполнения команды print(mae) получаем таблицу, сравнивающую
методы простой и многомерной импутации:
6.1. Особенности обработки признаков 251
minimum_nights
number_of_reviews
reviews_per_month
calculated_host_listings_count
availability_365
Простая
0.347355
1.327776
0.057980
0.579423
6.025748
Многомерная
0.260156
0.858506
0.036876
0.368567
4.62264
Результаты сравнения показывают, что многомерный метод, в частности
IterativeImputer, последовательно обеспечивает более низкие значения MAE
по сравнению с простым, даже после одной итерации. Это указывает на то, что
IterativeImputer эффективнее заменяет пропущенные значения с меньшим
количеством ошибок. Для получения еще более точных оценок можно увеличить
max_iter, позволив алгоритму самостоятельно остановиться на основе значений
tol — порога допуска, используемого для проверки стабильности результатов.
Увеличение max_iter приведет к увеличению времени восполнения пропусков,
поскольку в качестве модели подстановки мы используем алгоритм случайных
лесов, которые обычно являются наиболее эффективным способом обработки
многомерных оценок (метод, известный в сообществе R как MissForest: https://
rpubs.com/lmorgan95/MissForest). Однако можно использовать и более быстрые
методы, основанные на линейных моделях или k ближайших соседях, просто
заменив estimator в IterativeImputer:
BayesianRidge — байесовская линейная регрессия с регуляризацией, реализуемая простым вызовом BayesianRidge().
RandomForestRegressor — для регрессии на основе ансамбля случайных
деревьев можно задать n_estimators, max_depth и max_features, чтобы создать менее глубокие деревья и таким образом ускорить процесс импутации, например: RandomForestRegressor(n_estimators=30, max_depth=6,
max_features=0.5).
Nystroem + Ridge — пайплайн с полиномиальным ядром второй степени
и линейной регрессией с регуляризацией, объединяющий различные команды scikit-learn: make_pipeline(Nystroem(kernel="polynomial", degree=2,
random_state=0), Ridge(alpha=1e3)).
KNeighborsRegressor — подход на основе метода k ближайших соседей,
при котором задается, сколько соседей следует учитывать, например
KNeighborsRegressor(n_neighbors=5).
Выбор используемого оценщика повлияет на качество получаемых вами результатов и время вычисления. BayesianRidge является выбором по умолчанию и наиболее быстрым вариантом. Если у вас больше времени, RandomForestRegressor
обеспечит более точные оценки. Одновременно импутируя несколько переменных, IterativeImputer лучше улавливает зависимости между ними ценой большего количества вычислений и объема написанного кода. В качестве простого
252 Глава 6. Продвинутые методы обработки признаков
готового решения некоторые реализации GBDT предоставляют встроенную
поддержку для обработки пропущенных значений, о чем мы поговорим в следующем разделе.
6.1.2. Обработка пропущенных данных с помощью GBDT
Алгоритмы XGBoost и LightGBM (а также HistGradientBoosting от scikit-learn)
обрабатывают пропущенные значения аналогичным образом, направляя их каждый раз в ту ветвь разбиения, которая минимизирует функцию потерь. XGBoost
представил эту технику в рамках своего алгоритма поиска разбиений с учетом
разреженности данных. Этот алгоритм задает направление по умолчанию для
случая, когда данные отсутствуют — либо из-за пропущенных значений, либо
потому, что они находятся в разреженной матрице, где сохраняются только ненулевые элементы.
Таким образом, не забывайте, что XGBoost будет считать нули в разреженной
матрице пропусками и применять свой алгоритм для обработки пропущенных
данных. Следовательно, с одной стороны, может быть удобно при анализе категориальных переменных с высокой кардинальностью кодировать их с помощью
метода one-hot и создавать разреженные матрицы, поскольку это сэкономит
много памяти и вычислений. С другой стороны, вы можете заметить, что XGBoost
строит совершенно разные модели для одних и тех же данных, но представленных в виде либо плотной, либо разреженной матрицы.
Разница заключается в том, как XGBoost обрабатывает примеры с пропусками.
Во время обучения алгоритм на каждом шаге разбиения определяет, в какую
ветвь (левую или правую) следует направить объекты с пропущенными значениями, основываясь на максимизации полезности разбиения. При составлении
прогнозов примеры с пропусками направляются в соответствующую ветвь по
правилам, полученным во время обучения. Это позволяет алгоритму учитывать
сам факт пропуска значения как значимый признак, если это улучшает прогноз.
Если для данного признака во время обучения не было пропусков, то во время
прогнозирования примеры с отсутствующими значениями направляются в ту
ветвь, где их больше.
С помощью параметра missing можно указать, какое значение следует считать
пропуском. По умолчанию используется NaN, но вы можете задать любое другое.
Важно помнить, что в XGBoost бустер gblinear, использующий линейные модели в качестве базовых алгоритмов, трактует пропущенные значения как нули.
Предположим, вы стандартизировали числовые признаки, как это часто делается
при работе с линейными моделями. В этом случае бустер gblinear будет обрабатывать пропуски как среднее значение для этого признака, поскольку после
стандартизации среднее значение становится нулевым.
6.1. Особенности обработки признаков 253
LightGBM применяет аналогичный подход (см. https://github.com/microsoft/
LightGBM/issues/2921), используя специальные параметры.
LightGBM позволяет обрабатывать пропущенные значения по умолчанию.
Эту функцию можно отключить, установив use_missing=false.
LightGBM использует NA (NaN) для представления пропущенных значений по умолчанию. Чтобы считать пропуском ноль, установите параметр
zero_as_missing=true.
Если zero_as_missing=false (по умолчанию), незарегистрированные значения в разреженных матрицах (и LightSVM) рассматриваются как нули.
Если zero_as_missing=true, NA и нули (включая незарегистрированные
значения в разреженных матрицах (и LightSVM)) рассматриваются как
пропущенные значения.
Эта стратегия обработки пропущенных данных в целом работает хорошо, особенно если данные относятся к категории MCAR. Это означает, что пропуски
возникают случайно и не связаны с другими признаками или неявными процессами. Ситуация меняется в случаях MAR, когда пропущенные значения
связаны со значениями других признаков, но не со значениями самого признака с пропусками, и MNAR, где пропуски систематически связаны с самим
признаком и другими переменными. В этих ситуациях наилучшим решением
будет использовать другие методы импутации, поскольку стратегия XGBoost
и LightGBM может оказаться неэффективной.
Однако вместо импутации можно применять и другие подходы: например,
создать индикаторы пропущенных данных — бинарные признаки, которые отмечают наличие пропущенного значения в переменной. Это особенно полезно,
если пропуски в данных не относятся к категории MCAR, и метод совместим
с любыми классическими алгоритмами машинного обучения. Другой популярный подход на основе деревьев решений — замена на экстремальное значение
(например, большое отрицательное), которое не встречается в датасете. Если вы
используете точные разбиения, а не гистограммные, где значения группируются
по бинам, такая замена может оказаться эффективным и простым решением.
6.1.3. Кодирование по целевой переменной
Категориальные признаки, обычно представленные в датасете в виде строк, могут
эффективно обрабатываться с помощью различных стратегий. Мы уже упоминали one-hot кодирование в главах 2 и 4. Все стратегии работы с категориальными
признаками, независимо от их кардинальности, требуют кодирования — процедуры преобразования данных в числовой формат, подходящий для алгоритмов машинного обучения. Несмотря на некоторые сходства, не следует путать
254 Глава 6. Продвинутые методы обработки признаков
кодирование с эмбеддингами, которые представляют собой процедуру преобразования многомерных данных, таких как текст или изображения, в пространство
с меньшей размерностью с сохранением определенных характеристик или взаимосвязей исходных данных. Эмбеддинги обычно обучаются с помощью моделей
на основе нейронных сетей и не будут подробно рассматриваться в нашей книге.
Пакет scikit-learn предлагает несколько решений для кодирования.
OneHotEncoder — для one-hot-кодирования (то есть преобразования каждого
уникального строкового значения в двоичный признак), которое мы использовали до сих пор.
OrdinalEncoder — для порядкового кодирования (то есть преобразова-
ния строковых значений признака в упорядоченные числовые; есть также
LabelEncoder, но он работает так же и в основном предназначен для преобразования категориальных целевых переменных).
Как правило, one-hot-кодирование хорошо работает как для линейных, так и для
древовидных моделей, а порядковое эффективно для более сложных моделей
на основе деревьев, таких как случайные леса и GBDT, поскольку деревья могут
рекурсивно разбивать категориальный признак и находить полезные для прогнозирования разбиения. Однако при использовании one-hot или порядкового
кодирования возникают проблемы с категориальными признаками с высокой
кардинальностью, которая является слабым местом как линейных, так и древовидных моделей. При one-hot-кодировании таких признаков создаются разреженные матрицы, которые непросто превратить в плотные из-за ограничений
памяти. Кроме того, деревья решений с большим количеством разветвлений
и уровней не могут эффективно разбивать такие признаки на значимые для
прогнозирования разделы.
Не существует общепринятого стандарта, какую кардинальность считать высокой, поскольку это зависит от размера датасета и вычислительных ресурсов.
Однако к высококардинальным признакам обычно относят все те, которые
имеют много уникальных значений: идентификаторы, почтовые индексы, названия товаров или географических объектов. Например, условным порогом
может служить значение 512 уникальных категорий, но его можно снизить для
небольших датасетов. Распространенное эмпирическое правило гласит, что
количество классов в признаке не должно превышать 5–10 % от общего числа
строк в датасете, так что 512 может оказаться слишком большим числом для небольших датасетов. В таких случаях стандартная практика (особенно в рамках
соревнований по data science, таких как Kaggle) — использовать кодирование по
целевой переменной (target encoding), также известное как mean encoding.
Впервые представленное в статье Мичи-Барреки (Micci-Barreca) «A Preprocessing Scheme for High-Cardinality Attributes in Classification and Prediction
6.1. Особенности обработки признаков 255
Problems» (ACM SIGKDD Explorations Newsletter 3.1, 2001), кодирование по
целевой переменной — это способ преобразования значений категориального
признака в соответствующие им ожидаемые значения целевого признака. Если
решаемая задача — регрессия, такое кодирование будет использовать среднее
значение целевой переменной, а в задачах классификации используется условная
вероятность (conditional probability) или отношение шансов (odds ratio). Такой
подход несет риск переобучения, если категория представлена малым количеством примеров в данных. Для снижения этого риска используется взвешенное
среднее между ожидаемым значением для категории (апостериорная вероятность целевой переменной) и средним значением по всему датасету (априорная
вероятность целевой переменной на всех обучающих данных).
Кодирование по целевой переменной доступно в пакете category-encoders (https://
mng.bz/gave), совместимом с scikit-learn через класс TargetEncoder (https://mng.
bz/5glq). Установить пакет можно командой pip install category_encoders
в командной строке. В классе TargetEncoder необходимо указать параметр
сглаживания smoothing (значение выше нуля) для балансировки апостериорной
и априорной вероятностей целевой переменной по всем обучающим данным.
Оптимальное значение параметра подбирается экспериментальным путем.
Или можно взять его альтернативу — кодировщик Джеймса Стейнера (James
Steiner encoder), который автоматически определяет степень сглаживания на
основе дисперсии, обусловленной кодируемой категорией (https://mng.bz/5glq),
но предъявляет более строгие требования к данным. Необходимо будет выбрать
способ оценки условной дисперсии через параметр модели (для задач регрессии рекомендуется использовать «independent», а для задач классификации —
«binary»). Это избавляет от необходимости экспериментировать с различными
порогами сглаживания, которые в других методах приходится настраивать как
гиперпараметры.
В нашем примере кода мы используем признак neighborhood, который имеет
более 200 уникальных значений, а также координаты широты latitude и долготы
longitude, преобразованные в пространство сетки 100 × 100. Такое преобразование создает признак с более чем 2000 уникальных значений, что однозначно
определяет его как высококардинальный. В листинге 6.3 мы сначала группируем
широту и долготу в бины, а затем объединяем их, суммируя таким образом, чтобы
каждая комбинация бинов получала отдельный код. Биннинг выполняется путем
деления диапазона между минимальным и максимальным значениями признака
на равные интервалы. Кроме того, фрагмент кода выполняет группировку двух
отдельных признаков, в результате чего получаются наборы целочисленных значений для каждого из них. Значения одного признака умножаются на степень 10,
которая больше максимального значения другого признака. Это гарантирует,
что при суммировании двух наборов значений всегда получается уникальное
число, независимо от конкретных суммируемых значений.
256 Глава 6. Продвинутые методы обработки признаков
Листинг 6.3. Создание категориального признака с высокой
кардинальностью
def bin_2_cat(feature, bins=100):
min_value = feature.min()
bin_size = (feature.max() - min_value) / bins
bin_values = (feature - min_value) / bin_size
return bin_values.astype(int)
data['coordinates'] = (
bin_2_cat(data['latitude']) * 1000
+ bin_2_cat(data['longitude']
)
high_card_categorical += ['coordinates']
print(data[high_card_categorical].nunique())
Функция преобразования
числовых данных
в категориальные группы
Преобразует широту
и долготу в категориальные
признаки
Выводит количество уникальных
значений в категориальных признаках с высокой кардинальностью
Фрагмент кода завершается проверкой количества уникальных значений для
каждого из признака с высокой кардинальностью:
neighbourhood
coordinates
221
2259
Поскольку у нас два категориальных признака с высокой кардинальностью,
разумно добавить TargetEncoder в наш пайплайн предварительной обработки
из пакета category-encoders (листинг 6.4).
Листинг 6.4. Использование кодирования по целевой переменной
в пайплайне
from
from
from
from
category_encoders.target_encoder import TargetEncoder
XGBoost import XGBClassifier
sklearn.model_selection import KFold, cross_validate
sklearn.metrics import accuracy_score, make_scorer
Инициализирует
TargetEncoder для
категориальных
признаков с высокой
кардинальностью
target_encoder = TargetEncoder(cols=high_card_categorical,
smoothing=0.5)
Сглаживает значение,
accuracy = make_scorer(accuracy_score)
смешивая априорные
cv = KFold(5, shuffle=True, random_state=0)
и апостериорные вероятности
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=300,
max_depth=4,
min_child_weight=3)
Инициализирует классификатор
column_transform = ColumnTransformer(
[
('low_card_categories',
categorical_onehot_encoding,
low_card_categorical),
('high_card_categories',
target_encoder,
high_card_categorical),
('numeric',
numeric_standardization,
continuous)
],
remainder='drop',
XGBoost с заданными
гиперпараметрами
Определяет преобразователь
столбцов ColumnTransformer для
n_estimators=300,
max_depth=4,
min_child_weight=3)
Инициализирует классификатор
XGBoost с заданными
гиперпараметрами
column_transform = ColumnTransformer(
[
6.1. Особенности обработки признаков 257
('low_card_categories',
categorical_onehot_encoding,
low_card_categorical),
('high_card_categories',
target_encoder,
high_card_categorical),
('numeric',
numeric_standardization,
continuous)
Определяет преобразователь
],
столбцов ColumnTransformer для
remainder='drop',
предварительной обработки признаков
verbose_feature_names_out=True,
с помощью TargetEncoder
sparse_threshold=0.0)
model_pipeline = Pipeline(
[('processing', column_transform),
('model', xgb)])
Создает пайплайн, включающий
предварительную обработку
и моделирование
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Выполняет
пятифолдовую
кросс-валидацию
и получает метрики
оценки
Выводит на экран
среднюю долю верных
результатов, время обучения
и прогнозирования на кроссвалидации
При выполнении код обрабатывает результаты для запуска XGBoost с дополнительным использованием высококардинальных категориальных признаков.
Результаты указывают на небольшое улучшение точности. Далее в этой главе
мы рассмотрим вес вклада кодирования по целевой переменной при анализе
интерпретируемости модели:
0.840 (0.004) обучение: 4.52 с, прогнозирование: 0.06 с.
Хотя кодирование по целевой переменной — удобный метод, который может
быстро преобразовать любой категориальный признак в числовой, при его
использовании важно сохранить всю существенную информацию из данных.
Это кодирование делает дальнейшее моделирование любого взаимодействия
между признаками невозможным. Например, вы работаете с данными о кликах
по рекламе, содержащими информацию о множестве веб-сайтов и форматов
объявлений. Если вы закодируете оба признака (каждый с высокой кардинальностью и тысячами уникальных значений), то сможете легко создать любую
классическую модель. Однако после кодирования модель, будь то линейная
или древовидная, не сможет уловить возможное взаимодействие между этими
258 Глава 6. Продвинутые методы обработки признаков
признаками. Решение состоит в том, чтобы заранее создать новый признак,
объединив две категориальные переменные с высокой кардинальностью, с последующим применением кодирования по целевой переменной к их комбинации.
Таким образом, как и в случае с другими инструментами, следует учитывать
плюсы и минусы этой передовой техники кодирования. По нашему опыту,
в зависимости от ситуации, прежде чем прибегать к этому методу, нужно рассмотреть несколько других возможных вариантов для классических алгоритмов
и градиентного бустинга при работе с высококардинальными признаками:
простое удаление проблемных категориальных признаков;
использование OneHotEncoder;
использование OrdinalEncoder и обработка категорий как упорядоченных
равноудаленных величин;
использование OrdinalEncoder и встроенной поддержки обработки категориальных признаков в гистограммных алгоритмах градиентного бустинга;
использование кодирования по целевой переменной в качестве крайней
меры.
Исключение признаков не всегда является оптимальным решением. Однако, как
уже упоминалось в главе 2, полезность номинального признака для прогнозирования целевой переменной можно оценить с помощью метрики ассоциации
Крамера (коэффициент V Крамера).
При работе с высококардинальным признаком очевиден выбор в пользу onehot для линейных моделей. Однако для других алгоритмов, таких как деревья
решений и их ансамбли, может существовать более эффективный подход. Это
связано с тем, что one-hot-кодирование создает отдельный признак для каждой
категории признака. Это приводит к увеличению количества точек разбиения,
которые модель на основе дерева должна учитывать во время обучения. Следовательно, использование one-hot-кодированных данных требует большей
глубины дерева для достижения эквивалентного разбиения, которое может
быть достигнуто с помощью одной его точки при использовании иного способа
обработки категориального признака.
При порядковом кодировании категории представляются как 0, 1, 2 и т. д., обрабатываясь как непрерывные признаки. Хотя этот подход может вводить в заблуждение линейные модели, он эффективен для деревьев, которые на основе
порядкового кодирования могут точно разделять данные в соответствии с их
взаимосвязью с целевой переменной. Это справедливо для XGBoost, который
обрабатывает все признаки как числовые и непрерывные.
Если мы решим использовать встроенную обработку категориальных признаков, то эта опция доступна в LightGBM и в версии XGBoost, предоставляемой
6.1. Особенности обработки признаков 259
библиотекой H2O.ai (https://mng.bz/6e75). Такая функциональность позволяет
этим моделям обрабатывать категориальные признаки более эффективно без
преобразования их в числовые значения. В этом случае, поскольку требуется
сортировка категорий, алгоритм будет работать немного медленнее по сравнению
с обработкой категорий как порядковых чисел. При встроенной обработке сор
тировка категорий признака основывается на дисперсии целевой переменной
для каждой категории. После завершения сортировки признак может использоваться как непрерывный числовой атрибут.
6.1.4. Преобразование числовых данных
Деревья решений способны автоматически обрабатывать нелинейности и взаи
модействия в данных. Это происходит потому, что они могут разделить переменную в любой точке на две части с последующим многократным повторным
разбиением. Это свойство особенно полезно для обработки сложных и глубоких
взаимодействий, однако с важной оговоркой: деревья решений являются довольно грубыми аппроксиматорами. С точки зрения точности моделирования
сложных взаимосвязей нейронные сети при наличии достаточного количества
примеров более эффективны.
На рис. 6.1 показано, как ансамбль деревьев решений с бэггингом аппроксимирует нелинейную функцию. Результат строится при помощи набора правил
принятия решений «если — то», которые рекурсивно разделяют пространство.
Однако шум в данных может привести к неточностям в отдельных областях пространства. Напротив, нейронная сеть с тем же количеством узлов, что и деревья
в ансамбле с бэггингом, обеспечивает более точную оценку кривой.
Поскольку GBDT также основан на деревьях решений, он может испытывать
аналогичные трудности в точном воспроизведении нелинейных функций с помощью бинарных разбиений. Следовательно, при использовании GBDT, если
известны специфические нелинейности или взаимодействия, полезно явно
определить их с помощью преобразований к линейным формам, биннинга или
дискретизации и предварительно вычисленных взаимодействий между признаками. Для нелинейностей преобразования помогают уменьшить количество
разбиений. Кроме того, предварительное вычисление определенных взаимодействий также уменьшает количество разбиений, которые происходят в более
оптимальных точках.
Однако перед применением таких преобразований необходимо понять свои
данные. Линейные и нелинейные зависимости, даже если нет никакой связи
с целевой переменной, можно легко обнаружить после обучения модели с помощью графика частичной зависимости (partial dependence plot, PDP). Этот
метод визуализации, не зависящий от модели, объясняет, как связаны признаки
и целевая переменная через обученную модель.
260 Глава 6. Продвинутые методы обработки признаков
точки данных
нейронная сеть
Целевая переменная
деревья с бэггингом
Данные
Рис. 6.1. Сравнение прогнозов нейронной сети и ансамбля деревьев с бэггингом для
случайного датасета с зашумленной синусоидальной функцией
PDP отображает, как изменяется целевая переменная в зависимости от конкретных входных признаков, игнорируя при этом влияние остальных. Другими
словами, он показывает среднее прогнозируемое значение, если задать определенное значение для всех точек данных рассматриваемого признака. В основе
такого анализа лежит предположение о независимости исследуемого признака
от других. При таких условиях PDP отражает прямое влияние признака на целевую переменную. Однако это предположение часто нарушается на практике,
то есть признак не является полностью независимым от остальных. В результате
график показывает изменение целевой переменной при варьировании значения
признака, одновременно отражая усредненное влияние других признаков модели.
В листинге 6.5 мы исследуем возможное использование и ограничения PDP. На
примере модели XGBoost, обученной на датасете Airbnb NYC, мы демонстрируем,
как изменяется целевая переменная в зависимости от числовых признаков, пытаясь обнаружить любые нелинейности или другие характеристики смоделированных данных. Полученные четыре графика, которые нужно проанализировать,
построены с использованием matplotlib.
Листинг 6.5. График частичной зависимости
from XGBoost import XGBClassifier
import matplotlib.pyplot as plt
from sklearn.inspection import PartialDependenceDisplay
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=300,
max_depth=4,
min_child_weight=3)
Создает пайплайн, объединяющий
6.1. Особенности обработки признаков 261
from XGBoost import XGBClassifier
import matplotlib.pyplot as plt
from sklearn.inspection import PartialDependenceDisplay
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=300,
max_depth=4,
min_child_weight=3)
Создает пайплайн, объединяющий
model_pipeline = Pipeline(
[('processing', column_transform),
('XGBoost', xgb)])
обработку данных и классификатор
XGBoost
model_pipeline.fit(X=data, y=target_median)
fig, axes = plt.subplots(
nrows=2,
ncols=2,
figsize=(8, 4)
Создает компоновку графиков 2 × 2
)
fig.subplots_adjust(hspace=0.4, wspace=0.2)
PartialDependenceDisplay.from_estimator(
Создает график частичной зависимости
model_pipeline,
для визуализации усредненного
X=data,
влияния признака
kind='average',
features=[
'minimum_nights',
'number_of_reviews',
'calculated_host_listings_count',
'availability_365'
Добавляет красную
],
пунктирную линию на уровне
Список признаков для графика
ax=axes
y = 0.5 на каждый график
)
в качестве ориентира для
for ax in axes.flatten():
интерпретации
ax.axhline(y=0.5, color='red', linestyle='--')
plt.show()
На рис. 6.2 показаны четыре графика. Пунктирная линия обозначает порог классификации для единицы (выше или равно 0.5) и нуля (ниже 0.5).
Сплошная линия описывает связь между значением признака на оси x и вероятностью целевой переменной на оси y. Метки на оси x указывают на децили распределения признака, показывая диапазоны с большей плотностью
значений (где метки расположены близко друг к другу) и с меньшей плотностью (где метки находятся дальше друг от друга). Оценки в диапазонах
с меньшей плотностью значений менее надежны. Например, minimum_nights
и calculated_host_listings_count отображают нелинейную закономерность,
тогда как number_of_reviews и availability_365 колеблются вокруг постоянного
среднего значения.
Частичная зависимость
Частичная зависимость
262 Глава 6. Продвинутые методы обработки признаков
Рис. 6.2. Графики PDP для числовых характеристик
Учитывая полученные результаты, вы можете поэкспериментировать и попробовать преобразовать признаки minimum_nights и calculated_host_listings_count
с помощью следующих функций:
квадратичное или кубическое преобразование;
квадратный или кубический корень;
логарифмическое или экспоненциальное преобразование;
тангенс, синус и косинус;
обратное преобразование, обратный квадрат, обратный куб, обратный квадратный корень, обратный кубический корень;
обратный логарифм, обратная экспонента, арктангенс, арксинус, арккосинус.
Однако прежде чем спешить с тестированием преобразований, важно убедиться,
что полученная кривая PDP отражает поведение признака при всех условиях. Это
можно проверить с помощью графиков индивидуального условного ожидания (individual conditional expectation, ICE), которые являются отдельными компонентами
кривой PDP. Их можно получить, немного изменив предыдущий код (листинг 6.6).
Листинг 6.6. Графики индивидуального условного ожидания ICE
import matplotlib.pyplot as plt
from sklearn.inspection import PartialDependenceDisplay
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 4))
fig.subplots_adjust(hspace=0.4, wspace=0.2)
Создает график частичной
зависимости, отображающий
PartialDependenceDisplay.from_estimator(model_pipeline,
как индивидуальное, так
X=data,
и усредненное влияние
kind='both',
subsample=30,
features=['minimum_nights',
import matplotlib.pyplot as plt
6.1. Особенности обработки признаков 263
from sklearn.inspection import PartialDependenceDisplay
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 4))
fig.subplots_adjust(hspace=0.4, wspace=0.2)
Создает график частичной
зависимости, отображающий
PartialDependenceDisplay.from_estimator(model_pipeline,
как индивидуальное, так
X=data,
и усредненное влияние
kind='both',
subsample=30,
features=['minimum_nights',
'number_of_reviews',
'calculated_host_listings_
count',
'availability_365'],
ax=axes)
Использует случайную выборку
for ax in axes.flatten():
в 30 % данных для обеспечения
ax.axhline(y=0.5, color='red', linestyle='--')
эффективности построения графика
ax.legend().set_visible(False)
plt.show()
Частичная зависимость
Частичная зависимость
После запуска кода вы можете изучить результаты, подобные показанным на
рис. 6.3. Вы увидите ту же самую усредненную кривую PDP, что и раньше, обозначенную светлой пунктирной линией, а также выборку из 30 случайно взятых
кривых. Если выбранные кривые группируются вместе, приблизительно повторяя форму усредненной кривой, это подтверждает, что усредненная кривая
PDP является репрезентативной для поведения признака относительно целевой
переменной. В противном случае, как в нашем примере, если отдельные кривые
выглядят разнонаправленными, это указывает на опосредованное влияние
других признаков из-за коллинеарности или взаимодействий. В такой ситуации
преобразование исходного признака не принесет значительной пользы.
Рис. 6.3. Графики ICE для числовых признаков
264 Глава 6. Продвинутые методы обработки признаков
До сих пор мы использовали PDP только для числовых признаков. Однако
их также можно применять к бинарным и категориальным после one-hotкодирования. В этом случае нужно сначала вычислить значение кривой с помощью функции partial_dependence и затем представить полученные значения
в виде столбчатой диаграммы (для усредненных кривых PDP) или ящика
с усами (для совместного отображения PDP и ICE). В листинге 6.7 мы извлекаем необходимые значения и создаем ящик с усами для отдельных уровней
neighbor_group.
Листинг 6.7. График частичной зависимости для бинарных признаков
from sklearn.inspection import partial_dependence
import matplotlib.pyplot as plt
pd_ice = partial_dependence(model_pipeline, X=data,
features=['neighbourhood_group'],
kind='both')
fig = plt.figure(figsize=(8, 5))
ax = fig.add_subplot(1, 1, 1)
labels = np.ravel(pd_ice['values'])
plt.boxplot(
pd_ice["individual"].squeeze(),
labels=labels
)
ax.axhline(y=0.5, color='red', linestyle='--')
plt.show()
Импортирует функцию
partial_dependence,
которая вычисляет
значения кривой
Создает ящик с усами для
отдельных кривых ICE
Добавляет красную пунктирную
линию на уровне y = 0.5 на каждый
график в качестве ориентира
для интерпретации
На рис. 6.4 показан результат, подтверждающий мысль о том, что расположение
квартиры на Манхэттене обычно подразумевает более высокие цены. Согласно
модели, в других районах цены ниже. Однако цены в Бруклине демонстрируют
самую большую изменчивость, иногда достигая уровня Манхэттена, что явно
зависит от других факторов, связанных с точным местоположением или характеристиками квартиры.
Как и в случае с числовыми признаками, кривые PDP также дают полезную
информацию о том, как улучшить модель. Например, их можно использовать
для агрегации уровней категориального признака, которые ведут себя одинаково — в нашем примере это Бронкс, Статен-Айленд и, возможно, также Квинс.
PDP показывают, чтˆо мы можем ожидать от целевой переменной на основе
интересующих нас входных признаков. Они также помогают понять характер
взаимосвязи (линейная или нелинейная) между целевой переменной и интересующим нас признаком. Наблюдая за формой кривой, можно определить, какое
преобразование позволит линеаризовать зависимость. Если передать параметру
features функции PartialDependenceDisplay кортежи признаков, функция
6.1. Особенности обработки признаков 265
выведет карту изолиний, показывающих совместное влияние двух конкретных
признаков. Обнаруживать взаимодействия таким способом долго и утомительно,
особенно если признаков много. Решением было бы автоматическое выявление
потенциальных взаимодействий с последующей проверкой с помощью таких
карт. Автоматически обнаружить взаимодействия с помощью XGBoost можно
достаточно легко благодаря проекту XGBoost Feature Interactions Reshaped
(XGBFIR; https://github.com/limexp/xgbfir). В листинге 6.8 показан код, который
можно запустить после установки пакета командой pip install xgbfir в командной строке.
Листинг 6.8. Обнаружение взаимодействий с помощью XGBFIR
import xgbfir
xgbfir.saveXgbFI(
model_pipeline['XGBoost'],
Создает отчет
feature_names=(
с помощью xgbfir
model_pipeline['processing']
и сохраняет его
.get_feature_names_out()
в файл Excel
),
OutputXlsxFile='fir.xlsx')
fir = pd.read_excel('fir.xlsx', sheet_name='Interaction Depth 1')
result = fir[["Interaction", "Gain"]].sort_values(by="Gain",
ascending=False).head(10).round(2)
for index, row in result.iterrows():
print(f"{row['Interaction']}")
PartialDependenceDisplay.from_estimator(
model_pipeline,
X=data,
kind='average',
features=[(
'minimum_nights',
'calculated_host_listings_count')])
Читает файл
Excel, созданный
на предыдущих
этапах
Извлекает и сортирует столбцы
Interaction и Gain из отчета
о взаимодействии признаков
Создает график частичной
зависимости для признаков
minimum_nights и calculated_
host_listings_count
Код выведет на экран ряд взаимодействий. При работе с линейной моделью
каждое из них, обнаруженное XGBFIR, следует протестировать, поскольку оно
может повысить производительность модели. Если вы работаете с деревьями
решений, можно игнорировать взаимодействия, которые включают бинарные
признаки, и сосредоточиться только на числовых. Примером может служить
взаимодействие между minimum_nights и calculated_host_listings_count. На
рис. 6.5 показано, как комбинация их конкретных значений тесно связана с положительным откликом целевой переменной.
В таких случаях объединение числовых признаков путем их умножения позволит оптимизировать вашу модель GDBT гораздо быстрее и эффективнее.
266 Глава 6. Продвинутые методы обработки признаков
Рис. 6.4. Для каждого бинарного признака ящик с усами отображает значения
целевой переменной, полученные с помощью анализа частичных зависимостей (PDP)
Рис. 6.5. Совместный график PDP для двух числовых признаков
6.2. Отбор признаков 267
6.2. Отбор признаков
Отбор признаков не всегда необходим, однако иногда он играет ключевую роль
в определении наиболее ценных признаков для обучения среди существующего
набора, независимо от того, выводятся они непосредственно из извлеченных
данных или являются результатом генерации признаков. Используя эффективные методы отбора, можно точно определить и сохранить наиболее релевантные признаки, которые вносят значительный вклад в процесс машинного
обучения.
В разделе 2.2.3 главы 2 мы обсудили, как избегать сбора нерелевантных и избыточных признаков, основываясь на понимании задачи и разведочном анализе
данных. В последующих главах мы рассматривали алгоритмы машинного обучения, которые способны работать с нерелевантными и избыточными признаками.
В классическом машинном обучении существует большой набор алгоритмов,
включая семейство линейных моделей, которые особенно чувствительны
к нерелевантным и избыточным данным, снижающим их производительность и точность. Неинформативные и зашумленные признаки, не имеющие
значимой связи с целевой переменной, создают значительные проблемы для
линейных алгоритмов. Это связано с возможностью случайного совпадения
между значениями таких признаков и целевой переменной, что может ввести
модель в заблуждение и придать этим признакам необоснованную важность.
Линейные модели используют все предоставленные признаки, что делает модели особенно уязвимыми, поскольку чем больше зашумленных признаков, тем
сильнее ухудшаются результаты. Ансамбли, основанные на деревьях решений,
менее подвержены влиянию нерелевантных и избыточных данных, так как они
автоматически выбирают, какие признаки использовать, а какие игнорировать.
Аналогичная ситуация наблюдается в глубоком обучении. Однако оно может
быть менее устойчивым к зашумленным или нерелевантным признакам в таб
лицах по сравнению с ансамблевыми методами на деревьях. Для достижения
оптимальной производительности в таких условиях требуются большие датасеты, а также тщательный выбор архитектуры (например, использование слоев
dropout, регуляризации или слоев batch normalization) и настройка скорости
обучения.
Отбор признаков повышает эффективность классических алгоритмов машинного обучения, таких как линейные модели. Однако он также ценен в случае
ансамблей на основе деревьев и архитектур глубокого обучения, и не следует
игнорировать тот факт, что он ускоряет процесс машинного обучения из-за
меньшего количества обрабатываемых столбцов. Благодаря отбору признаков
до обучения эти сложные алгоритмы становятся более ясными и понятными,
поскольку в них выделяются наиболее релевантные и информативные признаки. Это позволяет лучше понять базовые закономерности и взаимосвязи,
268 Глава 6. Продвинутые методы обработки признаков
выявленные моделями. Такое упрощение повышает интерпретируемость и облегчает коммуникацию процесса принятия решений алгоритмом.
В следующих разделах мы обсудим и протестируем несколько алгоритмов, которые могут эффективно работать независимо или совместно с другими, чтобы
выбрать только существенные признаки для задачи анализа табличных данных.
Мы рассмотрим алгоритмы, позволяющие выявить как релевантные признаки
(полный релевантный набор), которые могут образовывать избыточные, но
полезные группы, так и алгоритмы для отбора минимальных подмножеств признаков (неизбыточный набор). Такие минимальные подмножества позволяют
строить модели, сравнимые по качеству с моделями на полном релевантном
наборе, но с дополнительным преимуществом — повышенной интерпретируемостью благодаря меньшему количеству признаков.
6.2.1. Стабильный отбор предикторов для линейных моделей
Стабильный отбор (stability selection) основан на идее, что если вы используете процедуру отбора переменных, то не всегда будете получать одинаковые
результаты при сабсемплинге или бутстрэппинге из-за изменчивости самого
процесса. Например, если вы используете регуляризацию L1 для отбора признаков в линейной модели, разные выборки могут возвращать разные ненулевые
коэффициенты, особенно для сильно коррелированных признаков.
Как мы уже обсуждали, регуляризация L1 приводит к разреженности оценок
коэффициентов. Она работает путем добавления штрафного члена к функции
потерь — суммы абсолютных значений коэффициентов. Такой штрафной член
накладывает ограничение на сумму абсолютных величин коэффициентов, что
приводит к обнулению некоторых из них. Следовательно, регуляризация L1
эффективно отбирает признаки, исключая из модели те, чьи коэффициенты
становятся нулевыми. При наличии сильно коррелированных признаков регуляризация L1 может столкнуться с трудностями при выборе уникального набора признаков из-за схожести их вклада в целевую переменную. Здесь играет
роль случайность: конкретные признаки получают ненулевые коэффициенты
в зависимости от состава выборки. Однако эту особенность мы можем использовать себе во благо.
Стабильный отбор вносит случайность через семплирование данных. Тем самым
он направлен на выявление признаков, которые последовательно являются важными в нескольких подмножествах; это указывает на их надежность и снижает
вероятность отбора признаков случайно или из-за шума. Стабильный отбор
предоставляет полезный набор признаков, а не минимальный. Исключая неважные признаки, процесс гарантирует, что все релевантные признаки будут
идентифицированы, и это делает его идеальным алгоритмом для первого шага
в сокращении количества переменных.
6.2. Отбор признаков 269
Стабильный отбор был представлен в статье Майнсхаузена (Meinshausen)
и Бюльмана (Büehlmann) (https://arxiv.org/abs/0809.2932). Некоторое время он
был доступен в scikit-learn, а затем поддерживался в совместимых с этой биб
лиотекой проектах. Мы можем легко воспроизвести его процедуры, используя
BaggingClassifier и LogisticRegression с регуляризацией L1 из scikit-learn
для задачи классификации. Вы также можете использовать этот код для задач
регрессии, используя BaggingRegressor с регрессией L1 (Lasso).
В нашей реализации мы тестируем ряд значений C для логистической регрессии
с L1-регуляризацией на бутстрэп-выборках. Процедура создает серию коэффициентов логистической регрессии, которые мы можем суммировать, усреднять
или подсчитывать, сколько раз они отличаются от нуля. Учитывая, что мы
используем смесь бинарных и непрерывных признаков, более полезным будет
подсчитать, сколько раз коэффициент, связанный с переменной, превышает по
абсолютному значению заданный порог. Таким образом мы можем признать
релевантными все признаки, которые в большинстве случаев имеют значимый
коэффициент, влияющий на итоговый прогноз (листинг 6.9).
Листинг 6.9. Стабильный отбор
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import BaggingClassifier
lambda_grid=np.logspace(-4, -1, 10)
sparse_coef = list()
Генерирует сетку значений
лямбда в логарифмическом
масштабе для использования
в регуляризации L1
for modeling_c in lambda_grid:
estimator = LogisticRegression(
solver='liblinear',
Создает оценщик
penalty='l1',
логистической регрессии
C=modeling_c
со штрафом L1 (Лассо)
)
model = BaggingClassifier(
estimator,
Создает классификатор BaggingClassifier,
n_estimators=100,
использующий оценщик логистической
bootstrap=True
регрессии в качестве базовой модели
)
model_pipeline = Pipeline(
Стандартизация после обработки
[('processing', lm_column_transform),
данных делает все коэффициенты
('standardize', StandardScaler()),
сопоставимыми, независимо от
('modeling', model)])
масштаба
model_pipeline.fit(data, target_median)
sparse_coef += [estimator.coef_.ravel() for estimator in
model_pipeline["modeling"].estimators_]
epsilon = 1e-2
threshold = 0.5
Устанавливает малое значение в качестве
эпсилон для порогового значения
Устанавливает пороговое значение
для отбора значимых коэффициентов
non_zero = (np.abs(sparse_coef) > epsilon).mean(axis=0)
feature_names = model_pipeline["processing"].get_feature_names_out()
print(non_zero)
)
model_pipeline = Pipeline(
Стандартизация после обработки
[('processing', lm_column_transform),
данных делает все коэффициенты
('standardize', StandardScaler()),
сопоставимыми, независимо от
('modeling', model)])
масштаба
model_pipeline.fit(data, target_median)
270sparse_coef
Глава 6.+=
Продвинутые
методы обработки
признаковin
[estimator.coef_.ravel()
for estimator
model_pipeline["modeling"].estimators_]
epsilon = 1e-2
threshold = 0.5
Устанавливает малое значение в качестве
эпсилон для порогового значения
Устанавливает пороговое значение
для отбора значимых коэффициентов
non_zero = (np.abs(sparse_coef) > epsilon).mean(axis=0)
feature_names = model_pipeline["processing"].get_feature_names_out()
print(non_zero)
print(feature_names[non_zero > threshold])
В выходных данных отображаются как распределения соответствующих коэффициентов, так и выбранные признаки:
[0.635 0.
0.9
0.7
0.592 1.
0.
0.6
0.593 0.444 0.6 0.506 0.7 ]
['low_card_categories__neighbourhood_group_Bronx'
'low_card_categories__neighbourhood_group_Manhattan'
'low_card_categories__neighbourhood_group_Queens'
'low_card_categories__neighbourhood_group_Staten Island'
'low_card_categories__room_type_Entire home/apt'
'low_card_categories__room_type_Shared room' 'numeric__minimum_nights'
'numeric__reviews_per_month' 'numeric__calculated_host_listings_count'
'Numeric__availability_365'
Стабильный отбор обладает рядом преимуществ. Он способен обрабатывать
высокоразмерные данные, избегать переобучения путем внесения элемента
случайности и предоставлять меру важности признаков, которая учитывает
стабильность процесса отбора. Этот метод обычно применяется в задачах
с большим количеством признаков, таких как геномика, анализ текстов или
обработка изображений. С другой стороны, этот алгоритм отбора ограничен
классическими моделями машинного обучения, которые используют регуляризацию L1 и возвращают набор коэффициентов. К ним относятся, среди прочих, рассмотренные нами ранее логистическая и лассо-регрессия. Концепцию
стабильного отбора можно расширить, используя важность признаков (многие
ансамблевые модели оценивают важность признаков), например как это сделано
в scikit-learn с помощью команды SelectFromModel (https://mng.bz/oKej). Однако
в этом случае задача усложняется, потому что придется выяснить, что делает
оценку важности значимой и какой порог отбора использовать. В следующем
разделе мы вернемся к тому, как работает важность признаков, и представим
алгоритм Boruta. Используя надежную процедуру автоматического отбора признаков, этот алгоритм способен определять значимые признаки для ансамблей на
основе деревьев решений, таких как случайные леса или градиентный бустинг.
6.2.2. Фоновые признаки и Boruta
Boruta — это интеллектуальный метод определения релевантности признака
в задачах машинного обучения, который опирается на внутренние параметры
модели, такие как коэффициенты в линейных моделях или значения важности,
6.2. Отбор признаков 271
основанные на вкладе (gain) в прогноз, как в деревьях решений и их ансамблях.
Впервые метод был представлен в статье «Feature Selection with the Boruta
Package» Мирона Б. Курсы (Miron B. Kursa) и Витольда Р. Рудницкого (Witold
R. Rudnicki) (Journal of Statistical Software 36 (2010): 1–13); см. https://www.
jstatsoft.org/article/view/v036i11.
Несмотря на свою инновационность, метод Boruta демонстрирует довольно
много аналогий со стабильным отбором. Его можно использовать только с ансамблями на основе деревьев решений. Для оценки значимости признака, как
и в стабильном отборе, мы анализируем ненулевые коэффициенты. В Boruta мы
подсчитываем случаи, когда важность признака превышает наивысшую важность,
полученную теневыми признаками (shadow features). Эти случаи называются попаданиями (hit). Теневые признаки — это рандомизированные версии исходных
(по сути, признаки с перемешанными значениями). Будучи случайными, могут
демонстрировать любую важность лишь по какому-то стечению обстоятельств.
Если какой-либо исходный признак не превосходит по важности теневой, его
нельзя считать более информативным для прогнозирования, чем любая случайная последовательность значений.
В Boruta порог отбора, аналогичный минимальному количеству появлений
ненулевых коэффициентов в стабильном отборе, задается на основе того, как
число попаданий соотносится с биномиальным распределением. Статистически
значимый порог для сохранения или исключения признака устанавливается на
основе проверки гипотезы с использованием этого распределения: если число
попаданий доказывает, что признак стабильно превосходит любой случайный
конструкт (теневые признаки), то он признается релевантным.
В листинге 6.10 показан пример использования Boruta для отбора всех релевантных признаков в задаче классификации XGBoost на датасете Airbnb
NYC. Реализация Boruta в пакете BorutaPy (https://github.com/scikit-learn-contrib/
boruta_py) имеет ряд ограничений. Она может работать только с моделями на
основе деревьев, такими как случайные леса или градиентный бустинг (независимо от реализации), а также не совместима с пайплайнами. Следовательно,
нам сначала пришлось преобразовать данные, а затем запустить Boruta на них,
пока мы обучали финальную модель. Boruta принимает в качестве ключевых
параметров estimator (оценщик, то есть модель, которая будет использоваться),
и гиперпараметр n_estimators (количество деревьев решений в ансамбле), который можно оставить пустым, задать целым числом или установить в значение
«auto», когда количество деревьев определяется автоматически в зависимости от
размера датасета. Другими важными параметрами в Boruta являются max_iter —
количество раундов тестирования (обычно устанавливается в 100) и альфа-порог
для биномиального теста. Его можно увеличить с 0.05, чтобы сохранить больше
признаков, или уменьшить, чтобы исключить больше признаков.
272 Глава 6. Продвинутые методы обработки признаков
Листинг 6.10. Отбор с помощью Boruta
from XGBoost import XGBClassifier
from boruta import BorutaPy
Инициализирует объект BorutaPy
для отбора признаков с помощью
xgb = XGBClassifier(booster='gbtree',
классификатора XGBoost
objective='reg:logistic',
n_estimators=300,
Преобразует входные данные,
max_depth=4,
выполняя все необходимые этапы
min_child_weight=3)
предварительной обработки
X = column_transform.fit_transform(data, target_median)
boruta_selector = BorutaPy(estimator=xgb, n_estimators='auto', verbose=2)
boruta_selector.fit(X, target_median)
selected_features = boruta_selector.support_
selected_data = column_transform.get_feature_names_out()[selected_features]
print(selected_data)
Возвращает булеву маску отобранных признаков,
определенных селектором Boruta
Обучает селектор признаков Boruta
После нескольких итераций вы получите результаты только по одному признаку,
исключенному как нерелевантный:
Iteration:
50 / 100
Confirmed:
13
Tentative:
0
Rejected:
1
['low_card_categories__neighbourhood_group_Bronx'
'low_card_categories__neighbourhood_group_Brooklyn'
'low_card_categories__neighbourhood_group_Manhattan'
'low_card_categories__neighbourhood_group_Queens'
'low_card_categories__room_type_Entire home/apt'
'low_card_categories__room_type_Private room'
'low_card_categories__room_type_Shared room'
'high_card_categories__neighbourhood' 'numeric__minimum_nights'
'numeric__number_of_reviews' 'numeric__reviews_per_month'
'numeric__calculated_host_listings_count' 'numeric__availability_365']
Эту же процедуру можно выполнить, используя в качестве предиктора LightGBM
вместо XGBoost:
from lightgbm import LGBMClassifier
lgbm = LGBMClassifier(boosting_type='gbdt',
n_estimators=300,
max_depth=4,
min_child_samples=3)
Инициализирует объект BorutaPy
для отбора признаков, используя
предоставленный классификатор LightGBM
boruta_selector = BorutaPy(estimator=lgbm, n_estimators='auto', verbose=2)
boruta_selector.fit(X, target_median)
selected_features = boruta_selector.support_
selected_data = column_transform.get_feature_names_out()[selected_features]
print(selected_data)
lgbm = LGBMClassifier(boosting_type='gbdt',
n_estimators=300,
max_depth=4,
min_child_samples=3)
Инициализирует объект BorutaPy
для отбора признаков, используя
предоставленный классификатор LightGBM
6.2. Отбор признаков
273
boruta_selector = BorutaPy(estimator=lgbm, n_estimators='auto',
verbose=2)
boruta_selector.fit(X, target_median)
selected_features = boruta_selector.support_
selected_data = column_transform.get_feature_names_out()[selected_features]
print(selected_data)
Результат достигается всего за 9 итераций, и на этот раз у нас возросло количество исключенных признаков:
Iteration:
9 / 100
Confirmed:
8
Tentative:
0
Rejected:
6
['low_card_categories__neighbourhood_group_Manhattan'
'low_card_categories__room_type_Entire home/apt'
'high_card_categories__neighbourhood' 'numeric__minimum_nights'
'numeric__number_of_reviews' 'numeric__reviews_per_month'
'numeric__calculated_host_listings_count' 'numeric__availability_365']
LightGBM сходится быстрее, а кроме того, способ создания разбиений позволяет
в этой задаче построить эффективную модель с гораздо меньшим количеством
признаков по сравнению с XGBoost.
В нашем примере мы обучали модель на всех доступных данных. Однако можно
использовать Boruta даже в цикле кросс-валидации, где можно консолидировать
результат для датасета, используя все признаки, отобранные на фолдах, или
только те, которые были отобраны минимальное количество раз по всем фолдам.
6.2.3. Прямое включение и обратное исключение
Одним из ограничений Boruta является то, что метод выбирает все релевантные
признаки, но не обязательно только существенные. Это означает, что в итоговом списке могут оказаться избыточные и сильно коррелированные признаки,
и этот список можно сократить до более компактного набора. После применения Boruta мы предлагаем прибегнуть к процедуре последовательного отбора
признаков (sequential feature selection), как это реализовано в функции scikitlearn SequentialFeatureSelector. Эта процедура добавляет (forward selection,
прямое включение) или удаляет (backward elimination, обратное исключение)
признаки из набора на основе их вклада в прогноз по методу жадных алгоритмов — то есть всегда выбирая наиболее эффективный вариант по результатам
кросс-валидации на каждом шаге добавления или удаления. Метод основан на
алгоритме обучения и его целевой функции. Следовательно, отбор всегда будет
одним из лучших возможных. Поскольку это жадная процедура, есть риск выбора локально оптимального набора.
Последовательный отбор — очень эффективная процедура сокращения количества признаков, однако она довольно затратна, поскольку алгоритм должен
оценивать всех кандидатов в каждом раунде. В прямом включении процесс
замедляется по мере добавления признаков: хотя количество кандидатов для
274 Глава 6. Продвинутые методы обработки признаков
оценки уменьшается, растущий размер пространства признаков увеличивает
время обучения модели на каждом шаге. При обратном исключении начальные
этапы работают медленно (так как модель обучается на всех признаках), но с удалением части признаков процесс ускоряется. Однако если исходное количество
признаков очень велико, а обучение модели требует много времени, обратное
исключение может оказаться непрактичным.
В качестве правила остановки для процедуры можно задать определенное количество признаков или оставить алгоритм самостоятельно определить точку,
в которой добавление или удаление признака не будет давать никаких улучшений
в прогнозах. Порог допуска помогает предоставить алгоритму определенную
свободу действий: чем выше значение допуска, тем больше вероятность того,
что алгоритм продолжит работу, даже если добавление или удаление признака
незначительно ухудшает производительность.
В листинге 6.11 мы применяем прямое включение к модели XGBoost, обученной
на датасете Airbnb NYC. Алгоритм отбора свободно определяет оптимальное
количество добавляемых признаков, а низкий допуск (установленный на 0.0001
для доли верных результатов в диапазоне от 0 до 1) останавливает процесс при
первых признаках ухудшения прогнозной производительности.
Листинг 6.11. Прямое включение
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.metrics import accuracy_score, make_scorer
from XGBoost import XGBClassifier
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=300,
max_depth=4,
min_child_weight=3)
Инициализирует объект-разделитель
для пятифолдовой кросс-валидации
Создает функцию оценки для
использования в процессе
отбора признаков
cv = KFold(5, shuffle=True, random_state=0)
accuracy = make_scorer(accuracy_score)
X = column_transform.fit_transform(data, target_median)
selector = SequentialFeatureSelector(
Задает значение допуска, используемое
estimator=xgb,
SequentialFeatureSelector для определения
n_features_to_select="auto",
сходимости во время поиска
tol=0.0001,
direction="forward",
Задает направление отбора признаков
scoring=accuracy,
(в данном случае прямое включение)
cv=cv
)
Извлекает булеву маску
selector.fit(X, target_median)
отобранных признаков
selected_features = selector.support_
selected_data = column_transform.get_feature_names_out()[selected_features]
print(selected_data)
6.2. Отбор признаков 275
Полученные результаты указывают на необходимость использования шести
признаков: трех бинарных, категориального с высокой кардинальностью и двух
числовых:
['low_card_categories__neighbourhood_group_Bronx'
'low_card_categories__room_type_Entire home/apt'
'low_card_categories__room_type_Shared room'
'high_card_categories__neighbourhood' 'numeric__minimum_nights'
'numeric__number_of_reviews' 'numeric__reviews_per_month'
'numeric__calculated_host_listings_count' 'numeric__availability_365']
Мы можем повторить эксперимент в обратном порядке, выполнив следующие
команды:
selector = SequentialFeatureSelector(
estimator=xgb,
n_features_to_select="auto",
tol=0.0001,
direction="backward",
Задает направление отбора признаков
scoring=accuracy,
(в данном случае обратное исключение)
cv=cv
)
selector.fit(X, target_median)
selected_features = selector.support_
selected_data = column_transform.get_feature_names_out()[selected_features]
print(selected_data)
Итоговый набор включает девять признаков, многие из которых уже были представлены в списке, полученном методом прямого включения:
['low_card_categories__neighbourhood_group_Bronx'
'low_card_categories__neighbourhood_group_Manhattan'
'low_card_categories__neighbourhood_group_Queens'
'low_card_categories__neighbourhood_group_Staten Island'
'low_card_categories__room_type_Entire home/apt'
'low_card_categories__room_type_Shared room'
'high_card_categories__neighbourhood' 'numeric__minimum_nights'
'numeric__number_of_reviews' 'numeric__reviews_per_month'
'numeric__calculated_host_listings_count' 'numeric__availability_365']
По нашему опыту, выбор между прямым включением и обратным исключением
зависит от того, готовы ли вы к риску исключить из финального набора некоторые малозначимые признаки. При прямом включении вы гарантированно
сохраните только самые важные, однако рискуете исключить малозначимые,
но все же полезные признаки. Обратное исключение обеспечивает обнаружение
всех ключевых признаков, допуская некоторую избыточность.
Помимо выбора направления последовательный отбор позволяет ускорить обучение и прогнозирование моделей, а также упростить интерпретацию и сопровождение благодаря ограниченному количеству задействованных признаков.
276 Глава 6. Продвинутые методы обработки признаков
6.3. Оптимизация гиперпараметров
Генерация признаков может улучшить результаты, полученные с помощью классических моделей машинного обучения. Создание новых признаков может раскрыть базовые закономерности и взаимосвязи в данных, которые модели не могут
уловить из-за своих ограничений. Отбор признаков может улучшить результаты
работы моделей, удалив бесполезные и избыточные признаки, тем самым уменьшая шум и ложные сигналы в данных. Наконец, оптимизация гиперпараметров
дает дополнительный прирост производительности и позволяет классическим
моделям показывать выдающиеся результаты на табличных данных.
Как обсуждалось в главе 4, гиперпараметры — это те настройки, которые работают под капотом всех алгоритмов машинного обучения и определяют, как именно
они могут действовать. С абстрактной точки зрения каждый из них предлагает
ограниченный, но все же широкий спектр функциональных форм, то есть математических способов связи предикторов с целевой переменной. В стандартной
конфигурации алгоритм может в большей или меньшей степени соответствовать
функциональной форме, требуемой для решения конкретной задачи.
Например, если вы используете алгоритм градиентного бустинга для решения задачи классификации, число итераций или параметры роста деревьев по
умолчанию могут не соответствовать требованиям конкретной задачи. Вам
может потребоваться изменить эти параметры на большие или меньшие. Путем
целенаправленной настройки можно найти наилучшую конфигурацию, которая
оптимально подходит для вашей задачи.
Однако речь идет не просто о хаотичном нажатии на все «кнопки» до получения
желаемых результатов. Иногда «кнопок» слишком много, чтобы их можно было
протестировать в комплексе, и даже если вам удастся проверить достаточное
количество комбинаций, без системного подхода это может привести к пере
обучению модели и, как следствие, ухудшению производительности. Необходим
системный подход после определения одной или нескольких метрик оценки.
Определение пространства поиска, включающего гиперпараметры, влияние
которых вы хотите исследовать, и границы тестируемых значений.
Построение правильной схемы кросс-валидации для обеспечения обобщающей способности найденного решения на новых данных.
Выбор алгоритма поиска, который при правильной стратегии найдет оптимальное решение за меньшее время и с меньшими затратами, например,
вычислительных ресурсов.
В следующих подразделах в контексте различных стратегий поиска мы обсудим,
как можно выполнить настройку гиперпараметров для некоторых классических
алгоритмов машинного обучения, представленных ранее.
6.3. Оптимизация гиперпараметров 277
6.3.1. Системный поиск
Исчерпывающий поиск по сетке (grid search) работает со всеми возможными
комбинациями значений гиперпараметров: для каждого из них вы выбираете
последовательность значений и тщательно перебираете все комбинации. В итоге
вы находите такую, которая возвращает наилучшие результаты.
В листинге 6.12 мы применяем этот поиск к модели логистической регрессии,
чтобы выбрать тип регуляризации и настройки значений L1 и L2. Самая важная
часть кода — сетка поиска, которая представляет собой список, содержащий
один или несколько словарей. Каждый из них — это пространство поиска: последовательность гиперпараметров (ключи словаря), связанных со списком
или с генератором возможных значений, которые нужно проверить (значения
словаря). Структурирование одного или нескольких пространств поиска является обычной практикой во всех методах оптимизации, независимо от того, из
scikit-learn они или нет. Обратите внимание, что имена гиперпараметров формулируются в виде модель__имя_гиперпараметра, потому что мы оптимизируем
пайплайн и обращаемся к параметрам, которые сначала находятся внутри него,
а затем внутри модели. Мы вернемся к этому в следующем подразделе с дополнительными пояснениями.
Листинг 6.12. Поиск по сетке
from
from
from
from
sklearn.linear_model import LogisticRegression
sklearn.metrics import accuracy_score
sklearn.model_selection import KFold, GridSearchCV
sklearn.metrics import make_scorer
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
model = LogisticRegression(solver="saga", max_iter=5_000)
model_pipeline = Pipeline(
[('processing', lm_column_transform),
('model', model)])
search_grid = [
{"model__penalty":
{"model__penalty":
{"model__penalty":
"model__l1_ratio":
]
Список словарей, задающих сетку
поиска гиперпараметров для модели
логистической регрессии
[None]},
["l1", "l2"], "model__C": np.logspace(-4, 4, 10)},
["elasticnet"], "model__C": np.logspace(-4, 4, 10),
[.1, .3, .5, .7, .9, .95, .99]},
search_func = GridSearchCV(estimator=model_pipeline,
param_grid=search_grid,
scoring=accuracy,
n_jobs=-1,
cv=cv)
search_func.fit(X=data, y=target_median)
print (search_func.best_params_)
print (search_func.best_score_)
Инициализирует объект
GridSearchCV, используя
заданную сетку поиска
Выводит на экран лучшие гиперпараметры,
найденные в результате поиска по сетке
Выводит на экран лучший результат,
достигнутый моделью с использованием
лучших гиперпараметров, найденных
в ходе поиска по сетке
]
{"model__penalty": ["elasticnet"], "model__C": np.logspace(-4, 4, 10),
"model__l1_ratio": [.1, .3, .5, .7, .9, .95, .99]},
search_func = GridSearchCV(estimator=model_pipeline,
Инициализирует объект
param_grid=search_grid,
278 Глава 6. Продвинутые
методы обработки признаков
GridSearchCV, используя
scoring=accuracy,
заданную сетку поиска
n_jobs=-1,
cv=cv)
search_func.fit(X=data, y=target_median)
print (search_func.best_params_)
print (search_func.best_score_)
Выводит на экран лучшие гиперпараметры,
найденные в результате поиска по сетке
Выводит на экран лучший результат,
достигнутый моделью с использованием
лучших гиперпараметров, найденных
в ходе поиска по сетке
После проверки всех комбинаций процедура поиска по сетке делает вывод,
что лучший набор гиперпараметров — это полное отсутствие регуляризации.
В подтверждение своего вывода она возвращает лучший результат на кроссвалидации:
{'model__penalty': None}
0.8210860006135597
Поиск по сетке эффективен, когда гиперпараметров немного; они принимают
дискретные значения, и вы можете распараллелить в памяти операции тестирования, поскольку датасет не слишком велик.
Прежде всего чем больше комбинаций, тем больше тестов вам придется сделать и тем больше времени и вычислительных ресурсов предстоит потратить.
Это может стать серьезной проблемой, если нужно протестировать множество
гиперпараметров и есть подозрение, что некоторые из них несущественны для
правильной настройки алгоритма. Когда вы добавляете какой-то гиперпараметр
в поиск по сетке, вы заставляете все гиперпараметры перебирать его значения,
что может превратиться в пустую трату ресурсов, если проверяемый гиперпараметр не является значимым.
Кроме того, если параметр принимает непрерывные значения, вы должны
решить, как превратить его непрерывное пространство поиска в дискретное.
Обычно это делается путем равномерного разделения диапазона непрерывных
значений на дискретные, однако такой подход без понимания того, как алгоритм
ведет себя в зависимости от данного гиперпараметра и его значений, может снова превратиться в трату вычислительных ресурсов на тестирование значений,
которые не способны улучшить производительность алгоритма.
Последний аспект, который следует учитывать, — это использование нескольких ядер и распараллеливание операций. Поиск по сетке не знает о результатах, которые получает каждый тест. Результаты ранжируются только
в конце, и вам предлагается лучший из них. Следовательно, поиск по сетке
подойдет, если алгоритм изначально работает на одном ядре. Однако если
он использует несколько потоков и ядер, как, например, случайный лес или
XGBoost, то придется выбирать между тем, чтобы алгоритм работал на полной скорости или чтобы процедура оптимизации выполнялась параллельно
6.3. Оптимизация гиперпараметров 279
и быстрее. Обычно лучшим решением является ускорение работы алгоритма
за счет параллельного выполнения. Независимо от того, решите вы воспользоваться возможностями распараллеливания самого алгоритма или процедуры
поиска, поиск по сетке — не самый эффективный вариант при работе с многопоточным алгоритмом.
Основываясь на нашем опыте и ограничениях стратегии поиска по сетке, мы
считаем ее наиболее подходящей для тестирования линейных моделей, поскольку
они легко поддаются параллелизации и имеют ограниченное количество параметров, часто принимающих булевы или дискретные значения.
6.3.2. Случайный поиск
Важные ограничения при использовании поиска по сетке заключаются в том, что:
необходимо дискретизировать непрерывные гиперпараметры;
если гиперпараметр не имеет отношения к задаче, множество экспериментов
будет проведено впустую, поскольку они будут проверять пространство нерелевантного признака.
По этим причинам в ML-сообществе укоренилась идея случайного семплирования пространства поиска. Как описано в статье «Random Search for HyperParameter Optimization» Джеймса Бергстры (James Bergstra) и Йошуа Бенджио
(Yoshua Bengio) (Journal of Machine Learning Research; https://mng.bz/nRg8),
оптимизация методом случайного поиска становится стандартом в ситуациях,
когда гиперпараметров много и их влияние на результат, а также взаимодействие
друг с другом точно неизвестны.
В нашем примере мы возвращаемся к задаче классификации, используя классификатор XGBoost. Он, как и другие реализации градиентного бустинга, имеет несколько гиперпараметров, которые можно считать важными, и их стоит
протестировать, чтобы проверить, можно ли улучшить производительность
модели. В этом примере мы также немного усложняем задачу, используя модель
XGBoost в составе пайплайна, что требует особого подхода к указанию гиперпараметров. Поскольку каждый элемент в пайплайне имеет имя, необходимо
обращаться к каждому параметру в составе пайплайна через имя его элемента,
потом ввести два нижних подчеркивания, а затем уже имя гиперпараметра. Так,
в нашем примере XGBoost находится в части пайплайна с именем «xgb». Чтобы
обратиться к гиперпараметру n_estimators для XGBoost, используйте метку
xgb__n_estimators в пространстве поиска. Код ниже показывает, как оптимизировать модель и ее пайплайн без перебора всех возможных вариантов, влияющих
на прогностическую эффективность модели (листинг 6.13).
280 Глава 6. Продвинутые методы обработки признаков
Листинг 6.13. Случайный поиск
from
from
from
from
from
sklearn.utils.fixes import loguniform1
sklearn.model_selection import KFold, RandomizedSearchCV
sklearn.metrics import accuracy_score
sklearn.metrics import make_scorer
XGBoost import XGBClassifier
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
xgb = XGBClassifier(booster='gbtree', objective='reg:logistic')
model_pipeline = Pipeline(
Создает пайплайн,
[('processing', column_transform), ('xgb', xgb)]
объединяющий обработку
)
данных и классификатор
search_dict = {
'xgb__n_estimators': np.arange(100, 2000, 100),
'xgb__learning_rate': loguniform(0.01, 1),
'xgb__max_depth': np.arange(1, 8),
'xgb__subsample': np.arange(0.1, 0.9, 0.05),
'xgb__colsample_bytree': np.arange(0.1, 0.9, 0.05),
'xgb__reg_lambda': loguniform(1e-9, 100),
'xgb__reg_alpha': loguniform(1e-9, 100)
}
XGBoost
Словарь, содержащий
различные гиперпараметры
с соответствующими
им поисковыми
пространствами для
RandomizedSearchCV
Задает количество
итераций для процесса
случайного поиска
search_func = RandomizedSearchCV(estimator=model_pipeline,
param_distributions=search_dict,
n_iter=60,
scoring=accuracy,
n_jobs=1,
Задает количество
cv=cv,
параллельных заданий
random_state=0)
для поиска
search_func.fit(X=data, y=target_median)
print (search_func.best_params_)
print (search_func.best_score_)
Выводит лучшие гиперпараметры,
найденные RandomizedSearchCV
Выводит лучший результат, достигнутый
с использованием лучших найденных
гиперпараметров в ходе случайного поиска
Через некоторое время (код выполняется примерно за час в Google Colab) мы получаем набор лучших параметров и достигнутый результат на кросс-валидации:
{'xgb__colsample_bytree': 0.3500000000000001,
'xgb__learning_rate': 0.020045491299569684,
'xgb__max_depth': 6,
'xgb__n_estimators': 1800,
'xgb__reg_alpha': 3.437821898520205e-08,
'xgb__reg_lambda': 0.021708909914764426,
1
Код устарел и будет удален в новых версиях sklearn. Сейчас рекомендуется использовать
loguniform из scipy напрямую. — Примеч. науч. ред.
6.3. Оптимизация гиперпараметров 281
'xgb__subsample': 0.1}
0.8399836384088353
Оптимизация случайного поиска, несмотря на свою простоту и тот факт, что она
основана на случайности, действительно работает и обеспечивает наилучший
результат во многих ситуациях. Некоторые системы AutoML полагаются на
эту стратегию, когда нужно настроить много гиперпараметров (см., например,
«Google Vizier: A Service for Black-Box Optimization» Д. Головинба (D. Golovinb)
и др., 2017 г., на https://mng.bz/8OrZ). По сравнению с поиском по сетке, который
хорошо работает при ограниченном наборе гиперпараметров, ожидаемо влияющих на результат, и ограниченном наборе значений для тестирования, случайный
поиск оптимален, когда необходимо настроить слишком много значений без
предварительного знания о том, как они работают.
Все, что требуется, — это провести достаточное количество случайных испытаний (по нашему опыту, обычно достаточно 30–60), чтобы найти удачную
комбинацию, и это не займет много времени. Одной из сильных сторон случайного поиска является то, что он хорошо справляется со сложными задачами
и не подвержен влиянию нерелевантных гиперпараметров. Скорость нахождения хорошего решения определяется количеством значимых параметров.
Алгоритм также подходит для параллельного поиска на разных компьютерах
или инстансах (вы просто выбираете лучший результат среди всех). Тем не
менее этот положительный момент имеет ограничение, заключающееся в том,
что, поскольку испытания независимы, они не обмениваются информацией
о своих результатах.
6.3.3. Снижение вычислительной нагрузки
Как поиск по сетке, так и случайный поиск не используют результаты предыдущих экспериментов. Поиск по сетке строго придерживается предопределенной
процедуры, в то время как случайный поиск проводит набор независимых испытаний. В обоих случаях предыдущие результаты не учитываются и не используются в процессе. Однако метод последовательного халвинга (successive halving),
обертка для обеих стратегий, может использовать информацию о предыдущих
результатах. Идея аналогична турниру: сначала проводится множество раундов
с минимальными ресурсами для проверки различных значений гиперпараметров. Затем, по мере продвижения и исключения неэффективных комбинаций,
вкладывается больше ресурсов для тщательной проверки оставшихся значений.
Обычно ресурсом, который изначально распределяется экономно, а затем концентрируется, является количество обучающих примеров. Больше примеров
подразумевают более надежные результаты теста гиперпараметров, но требуют
больших вычислительных затрат.
В листинге 6.14 мы тестируем вариант со случайным поиском (в scikit-learn доступны HalvingGridSearchCV и HalvingRandomSearchCV), чтобы проверить, можем
282 Глава 6. Продвинутые методы обработки признаков
ли получить аналогичные результаты оптимизации. Как уже было сказано, мы
рассматриваем количество примеров как ограниченный ресурс для оптимизации,
используя вначале только 30 % от общего объема доступных данных. Кроме того,
мы указываем алгоритму начинать с 20 первоначальных кандидатов и уменьшать
их количество в три раза на каждом раунде (с 20 до 6, а затем до 2).
Листинг 6.14. Случайный поиск методом последовательного халвинга
from sklearn.experimental import (
Активирует экспериментальный
enable_halving_search_cv
модуль HalvingRandomSearchCV
)
from sklearn.model_selection import HalvingRandomSearchCV
search_func = HalvingRandomSearchCV(
Указывает, что ресурс, используемый
estimator=model_pipeline,
для халвинга, — это количество семплов
param_distributions=search_dict,
Задает количество кандидатов,
resource='n_samples',
которые будут выбраны
n_candidates=20,
и оценены на первой итерации
factor=3,
min_resources=int(len(data) * 0.3),
Определяет коэффициент, на который
max_resources=len(data),
будет уменьшено количество
scoring=accuracy,
кандидатов на каждой итерации
n_jobs=1,
Задает минимальное количество
cv=cv,
ресурсов (семплов), которые будут
random_state=0
использованы в процессе халвинга
)
Задает
максимальное количество
search_func.fit(X=data, y=target_median)
print (search_func.best_params_)
ресурсов (семплов), которые будут
print (search_func.best_score_)
использованы в процессе халвинга
Ниже приведены результаты, которые вы получите, затратив на это в разы
меньше времени, чем требовалось ранее (в Google Colab процедура выполняется
примерно за 10 минут):
{'xgb__colsample_bytree': 0.6500000000000001,
'xgb__learning_rate': 0.02714215181104359,
'xgb__max_depth': 7,
'xgb__n_estimators': 400,
'xgb__reg_alpha': 3.281921389446602,
'xgb__reg_lambda': 0.00039687940902191534,
'xgb__subsample': 0.8000000000000002}
0.8398409090909091
По нашему опыту, при применении этой стратегии оптимизации ключевым
моментом является настройка начального раунда таким образом, чтобы он
мог отобрать несколько хороших гиперпараметров. Поэтому важно запустить
максимально возможное количество кандидатов, работающих с минимальными
ресурсами, но не настолько низкими, чтобы не повлиять на результаты оптимизации. Использование всего лишь 1000 стартовых примеров будет достаточно
6.3. Оптимизация гиперпараметров 283
эффективно, если уменьшить параметр factor. Он определяет долю кандидатов,
выбранных для каждой последующей итерации, равной двум вместо трех, что
приводит к увеличению количества раундов.
6.3.4. Расширение поиска с помощью байесовских методов
Другая стратегия оптимизации, основанная на информированном выборе, —
это байесовская оптимизация. Она была представлена в статье «Practical
Bayesian Optimization of Machine Learning Algorithms» Сноека (Snoek),
Ларошеля (Larochelle) и Адамса (Adams) ( https://arxiv.org/abs/1206.2944 ).
Идея этой стратегии оптимизации заключается в том, чтобы понять влияние гиперпараметров на модель, для которой они предназначены. Для этого
алгоритм оптимизирует прокси-функцию, называемую суррогатной. Она
служит для аппроксимации и повышения эффективности оценки целевой
функции. Суррогатная функция постоянно обновляется на основе откликов
(значений) исходной целевой функции оптимизируемой модели. При этом
решения в байесовской оптимизации принимаются исключительно на основе
предсказаний суррогатной функции.
В частности, существует еще одна функция, которая чередует исследование
(exploration) и использование (exploitation): функция приобретения (acquisition
function). Она оценивает, насколько перспективна та или иная комбинация
параметров и насколько она неопределенна, на основе суррогатной функции.
Исследование подразумевает тестирование новых комбинаций параметров. Это
происходит при высокой неопределенности, а следовательно, и надежде на то, что
некоторые области пространства поиска нуждаются в проверке для улучшения
суррогатной функции. Напротив, использование происходит, когда функция
приобретения гарантирует, что алгоритм может улучшить производительность
при тестировании определенного набора гиперпараметров.
Как следует из названия «байесовский» и нашего краткого описания принципа
работы этого метода, процесс оптимизации основан на априорных ожиданиях,
которые последовательно корректируются апостериорными наблюдениями
в цикле тонкой настройки. Суррогатная функция в этой схеме — не более чем
модель нашей модели. Обычно в качестве модели для суррогатной функции
выбираются гауссовские процессы, но существуют и альтернативы, такие как
алгоритмы на основе деревьев (например, случайные леса или древовидные
парзеновские оценщики (tree-structured Parzen estimator, TPE)), которые представляют собой многомерное распределение, способное описывать поведение
гиперпараметров модели. Такие библиотеки, как scikit-optimize (https://scikitoptimize.github.io/stable/) или KerasTuner (https://keras.io/keras_tuner/), используют
гауссовские процессы, при этом scikit-optimize также может применять ансамбли
деревьев, а KerasTuner — многорукие бандиты. Optuna (фреймворк оптимизации,
284 Глава 6. Продвинутые методы обработки признаков
разработанный Preferred Networks — японской компанией по исследованиям
и разработкам в области искусственного интеллекта) применяет древовидные
парзеновские оценщики. Первоначально выпущенный в мае 2019 года как проект с открытым исходным кодом, Optuna особенно популярен в сообществе
машинного обучения на языке Python благодаря своей простоте, универсальности и интеграции с популярными библиотеками, такими как TensorFlow,
PyTorch и scikit-learn.
В нашем примере мы используем Optuna для улучшения классификатора
XGBoost. При работе с этим фреймворком вы просто создаете объект study
и указываете его рабочие параметры, такие как количество испытаний n_trials
и направление оптимизации direction — минимизация или максимизация целевой функции. За кулисами вся работа выполняется целевой функцией, которую
вы определяете. Она возвращает оценку качества и принимает единственный
входной параметр trial, с помощью которого задаются значения гиперпараметров для тестирования. Затем вы самостоятельно проводите оценку — например,
с помощью кросс-валидации или простого теста на выборке. Эта гибкость позволяет реализовать сложные сценарии оптимизации, где одни гиперпараметры
связаны с другими. Все зависит от того, как вы запрограммируете процедуру
внутри целевой функции (листинг 6.15).
Листинг 6.15. Байесовский поиск с Optuna
import optuna
from XGBoost import XGBClassifier
from sklearn.model_selection import cross_validate
def objective(trial):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 2000),
'learning_rate': trial.suggest_float(
'learning_rate', 0.01, 1.0, log=True
),
'subsample': trial.suggest_float('subsample', 0.1, 1.0),
'colsample_bytree': trial.suggest_float(
'colsample_bytree', 0.1, 1.0
),
'max_depth': trial.suggest_int('max_depth', 1, 7),
'min_child_weight': trial.suggest_int('min_child_weight', 1, 7),
'reg_lambda': trial.suggest_float(
'reg_lambda', 1e-9, 100.0, log=True
),
'reg_alpha': trial.suggest_float(
Словарь, определяющий
'reg_alpha', 1e-9, 100.0, log=True
пространство поиска
),
гиперпараметров для Optuna
}
xgb = XGBClassifier(
booster='gbtree',
Создает классификатор XGBoost
objective='reg:logistic',
с гиперпараметрами,
Выполняет кросс-валидацию
**params
предложенными Optuna
)
для оценки эффективности
model_pipeline = Pipeline(
модели с использованием
[('processing', column_transform), ('xgb', xgb)]
гиперпараметров
}
'min_child_weight': trial.suggest_int('min_child_weight', 1, 7),
'reg_lambda': trial.suggest_float(
'reg_lambda', 1e-9, 100.0, log=True
),
'reg_alpha': trial.suggest_float(
Словарь, определяющий
'reg_alpha', 1e-9, 100.0, log=True
пространство поиска
6.3. Оптимизация
гиперпараметров 285
),
гиперпараметров для Optuna
xgb = XGBClassifier(
booster='gbtree',
Создает классификатор XGBoost
objective='reg:logistic',
с гиперпараметрами,
Выполняет кросс-валидацию
**params
предложенными Optuna
)
для оценки эффективности
model_pipeline = Pipeline(
модели с использованием
[('processing', column_transform), ('xgb', xgb)]
гиперпараметров
)
Функция, выступающая
accuracy = make_scorer(accuracy_score)
в качестве целевого значеcv = KFold(5, shuffle=True, random_state=0)
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv)
cv_accuracy = np.mean(cv_scores['test_score'])
return cv_accuracy
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=60)
print(study.best_value)
print(study.best_params)
Выводит наилучшие
гиперпараметры,
найденные Optuna
ния для оптимизации, возвращает среднее значение
доли верных результатов
(mean accuracy), полученное
на кросс-валидации
Создает объект study
в Optuna с целью
максимизации
целевой функции
Запускает процесс оптимизации,
используя заданную целевую
функцию и не более 60 попыток
Выводит наилучшее достигнутое
значение целевой функции
В Google Colab этот процесс может занять до двух часов, но результаты, безусловно, лучшие, которые только можно получить при оптимизации гипер
параметров:
{'n_estimators': 1434,
'learning_rate': 0.013268588739778429,
'subsample': 0.782534239551612,
'colsample_bytree': 0.9427647573058971,
'max_depth': 7,
'min_child_weight': 2,
'reg_lambda': 2.3123673571345327e-06,
'reg_alpha': 1.8176941971395193e-05}
0.8419879333265161
В качестве дополнительной опции, предлагаемой Optuna, можно сохранить свое
исследование в базе данных и возобновить оптимизацию в любое время, добавив несколько строк к предыдущему коду. Optuna интегрирует свои процедуры
оптимизации с SQLite, если во время создания объекта study вы объявите его
имя и целевую базу данных:
286 Глава 6. Продвинутые методы обработки признаков
Определяет путь к базе данных
SQLite, где Optuna будет хранить
информацию, связанную
с объектом study
sqlite_db = "sqlite:///sqlite.db"
Задает имя для study
study_name = "optimize_XGBoost"
study = optuna.create_study(storage=sqlite_db, study_name=study_name,
direction="maximize", load_if_exists=True)
study.optimize(objective, n_trials=60)
Создает объект study
print(study.best_params)
print(study.best_value)
и подключает его к базе
данных SQLite
Что касается спецификации базы данных SQLite, sqlite:// — это схема унифицированного идентификатора ресурса (Uniform Resource Identifier, URI),
используемая для указания протокола или механизма подключения к базе
данных SQLite. В контексте схемы URI sqlite:// указывает, что подключение
к базе данных будет установлено с использованием движка SQLite. При использовании этой схемы URI за частью sqlite:// следует путь к файлу базы.
В примере sqlite:///sqlite.db указывается, что файл базы данных SQLite
называется sqlite.db и находится в текущем каталоге. Три слеша (///) после
sqlite: являются необязательными и обозначают, что путь указан относительно
текущего каталога.
После завершения процесса вы также можете получить полезную визуализацию
результатов итераций и сведения о последовательных запусках того же поиска.
Например, можно изучить историю оптимизации и проверить, достигли ли вы
плато и стоит ли продолжать выполнение итераций:
fig = optuna.visualization.plot_optimization_history(study)
fig.show()
На рис. 6.6 показан ход нашей оптимизации. После нескольких итераций был
достигнут хороший результат, но затем процесс замедлился, и дальнейшего
значимого улучшения не наблюдалось, несмотря на все остальные итерации.
В таких условиях дальнейший прогресс маловероятен, поскольку любые возможные улучшения на данном этапе будут крайне незначительными.
Другой полезный график показывает, насколько различные гиперпараметры
повлияли на итоговые оптимальные настройки:
fig = optuna.visualization.plot_param_importances(study)
fig.show()
На рис. 6.7 показана оценка важности гиперпараметров в нашей оптимизации
алгоритма XGBoost. Результаты явно определяются гиперпараметром max_depth
и, в некоторой степени, значениями subsample. Это говорит о том, что алгоритм
6.3. Оптимизация гиперпараметров 287
чувствителен к глубине деревьев и что ее увеличение существенно влияет на
результаты оптимизации. Это может означать, что данные содержат сложные
закономерности, для захвата которых требуется бˆольшая глубина, а найденная
оптимизацией «золотая середина» в семь уровней отмечает точку, после которой
алгоритм начинает переобучаться.
Целевой
показатель
Целевой показатель
Лучшее
значение
Номер испытания
Гиперпараметр
Рис. 6.6. История результатов оптимизации в ходе испытаний
Важность для целевого значения
Рис. 6.7. Диаграмма оценки важности гиперпараметров в процессе оптимизации Optuna
288 Глава 6. Продвинутые методы обработки признаков
Понимание того, почему XGBoost (или LightGBM) работает лучше в определенных условиях, зависит от конкретной задачи. Возможность выяснить эти
причины, объяснить их другим людям (например, стейкхолдерам) и предпринять
шаги для корректировки данных или настроек оптимизации, является бесценным
преимуществом Optuna по сравнению с другими методами.
После обзора различных методов оптимизации остается рассмотреть случай,
когда не хочется настраивать сложные системы, но нужно быстро улучшить
работу алгоритмов машинного обучения методом проб и ошибок.
6.3.5. Ручная настройка гиперпараметров
Несмотря на эффективность ранее описанных стратегий оптимизации, многие
специалисты все еще настраивают параметры интуитивно, методом проб и ошибок. Такая процедура кажется особенно оправданной на этапе экспериментов,
когда необходимо найти работоспособное решение путем итеративных улучшений. Поэтому тщательная оптимизация обычно откладывается до завершения
всех исследований.
В приложениях к книге приведено подробное руководство по ключевым
параметрам алгоритмов машинного обучения, рассмотренных ранее. Мы
начнем с линейных моделей, таких как линейная или логистическая регрессии, которые можно эффективно настраивать с помощью поиска по сетке
из-за ограниченного числа настроек и простоты дискретизации. Отдельная
таблица посвящена случайному лесу и особо случайным деревьям, поскольку
они имеют схожие гиперпараметры, основанные на том же подходе бутстрэппированного ансамбля.
Что касается GBDT, наборы гиперпараметров различаются в зависимости от
конкретной реализации. Для удобства мы выбрали самые важные. Не бойтесь
использовать их вместе с предлагаемыми диапазонами для ручной или автоматической оптимизации. Руководство начинается с HistGradientBoosting,
а затем охватывает XGBoost и LightGBM. Важно отметить, что XGBoost
имеет больший набор необходимых гиперпараметров (вы можете найти их
полный список по адресу https://mng.bz/6e7e). Наконец, мы приводим список
гиперпараметров для LightGBM, который немного отличается от XGBoost
(полный перечень см. по адресу https://mng.bz/vK8q ). Это всеобъемлющее
руководство поможет вам эффективно настраивать алгоритмы машинного
обучения и оптимизировать их производительность на основе конкретных
значений гиперпараметров.
GBDT-модели часто показывают неоптимальные результаты с настройками
«из коробки», поэтому стоит учитывать несколько профессиональных рекомендаций. Обратимся к статье 1999 года под названием «Greedy Function
6.3. Оптимизация гиперпараметров 289
Approximation: A Gradient Boosting Machine» Джерома Фридмана (Jerome
Friedman). В ней обсуждается компромисс между количеством деревьев и скоростью обучения. Было замечено, что более низкие скорости обучения обычно
приводят к более высокому оптимальному количеству деревьев. Кроме того,
рекомендуется снижать скорость обучения при увеличении максимальной глубины деревьев в модели. Эта мера предосторожности связана с тем, что более
глубокие деревья повышают сложность модели, что может привести к переобу
чению. Оно возникает, когда модель становится чрезмерно подогнанной под
обучающие данные и плохо работает на новых. Снижение скорости обучения
помогает уменьшить этот риск, так как более низкая скорость означает более
мелкие и осторожные обновления модели. Этот постепенный процесс обучения
позволяет осуществлять более тонкую настройку, помогая модели достичь лучшего баланса между выявлением сложных взаимосвязей и предотвращением
переобучения.
Еще одним замечательным ресурсом по ручной настройке параметров в GBDT
является выступление Оуэна Чжана (Owen Zhang) в Академии науки о данных
в Нью-Йорке в 2015 году под названием «Winning Data Science Competitions».
Оуэн, в прошлом один из лучших участников на Kaggle, дал несколько полезных советов.
Определите количество используемых деревьев на основе размера датасета
(обычно в диапазоне от 100 до 1000) и зафиксируйте его во время оптимизации. Лучше меньше деревьев, чем больше.
Протестируйте скорость обучения в диапазоне от 2 до 10, деленную на количество деревьев. Таким образом, для 1000 деревьев проверяйте значения
в интервале от 0.002 до 0.01.
Протестируйте семплирование строк со значениями 0.5, 0.75, 1.0.
Протестируйте семплирование столбцов со значениями 0.4, 0.6, 0.8, 1.0.
Проверьте максимальную глубину дерева на значениях 4, 6, 8, 10.
Настройте минимальный вес/количество наблюдений в листе как приблизительное отношение 3 к квадратному корню из доли самого редкого класса,
который вы должны предсказать. Таким образом, если целевой класс имеет
10 %-ное покрытие в данных, следует установить минимальный вес/количество наблюдений в листе примерно равным 9. Это значение рассчитывается
путем деления 3 на квадратный корень из 0.1 (поскольку 10 % покрытие
равно 0.1 в десятичной форме).
В заключительном разделе мы продолжим рассматривать некоторые идеи
и приемы для более эффективного использования GBDT при решении задач
анализа табличных данных.
290 Глава 6. Продвинутые методы обработки признаков
6.4. Освоение градиентного бустинга
Обсудив принципы работы градиентного бустинга и его реализации, мы завершаем эту главу рекомендациями, как наиболее эффективно использовать
градиентный бустинг, понять его внутренние механизмы и ускорить процесс
обучения модели и предсказания.
6.4.1. Выбор между XGBoost и LightGBM
XGBoost и LightGBM (наряду с HistGradientBoosting) являются одними из
самых популярных и высокопроизводительных реализаций на основе гистограмм. Несмотря на их мощь, по нашему опыту, априори нельзя утверждать, что
они предпочтительнее других классических или нейросетевых решений из-за
теоремы «не бывает бесплатных обедов» в машинном обучении: не существует
универсального алгоритма, который лучше всего подходит для всех возможных
задач. Следовательно, утверждение, что «XGBoost — это все, что вам нужно»
для анализа табличных данных, безусловно, броское, но не всегда справедливое. GDBT часто превосходят другие решения для табличных данных. Таким
образом, начинать с них, но не ограничиваться ими — это хорошая идея. Возвращаясь к конкретным реализациям: хотя всегда желательно протестировать
любой алгоритм на своих данных и принять собственные решения, существует
также несколько других критериев для выбора. Мы проверили их на собственном
опыте и представили в табл. 6.1.
Таблица 6.1. Критерии, которые следует учитывать, прежде чем выбрать GBDT
для своей задачи
Тип
Описание
Объем данных
XGBoost отлично подходит для всех табличных задач. LightGBM,
из-за своего метода полистового разбиения, позволяющего
создавать более глубокие деревья, имеет склонность к пере
обучению, особенно на небольших датасетах
Масштабируемость
XGBoost более масштабируем и готов к работе с графическими
процессорами. LightGBM испытывает с этим больше трудностей
Скорость экспериментов
На CPU LightGBM, несомненно, быстрее XGBoost
Наличие больших объемов данных — первый критерий, который надо учитывать.
LightGBM использует полистовой рост (вертикальный), что может привести
к переобучению. Эта склонность алгоритма хорошо объясняет его успех в соревнованиях Kaggle. Следовательно, LightGBM работает лучше, когда у вас много
6.4. Освоение градиентного бустинга 291
данных. Напротив, XGBoost строит более надежные модели, чем LightGBM, на
меньших выборках.
Другим критерием является наличие доступа к нескольким GPU и мощным
CPU или ограниченность вычислительных ресурсов. Если у вас достаточно
ресурсов, XGBoost более масштабируем, что делает его лучшим вариантом для
корпоративных решений. Однако, если приоритет за экспериментами и генерацией признаков, а GPU у вас нет, LightGBM предпочтительнее благодаря
более быстрому обучению. Вы можете использовать сэкономленное время для
повышения надежности своей модели. Если ресурсы ограниченны, например
у вас автономный компьютер, то нужно учитывать, что время обучения для
XGBoost увеличивается линейно с размером выборки, а LightGBM обучается
значительно быстрее.
6.4.2. Исследование структуры деревьев
Как обсуждалось ранее, GBDT — это сложные, но объяснимые и воспроизводимые алгоритмы. Вам достаточно воспроизвести составляющие их деревья более
эффективным способом и объединить их для получения быстрых прогнозов.
И XGboost, и LightGBM позволяют исследовать и извлекать структуру модели.
В листинге 6.16 мы демонстрируем это: после сохранения в файл JSON простого
решения XGBoost мы исследуем его структуру как граф, используя стратегию
поиска в глубину. При таком подходе алгоритм исследует каждое ветвление до
его окончания, прежде чем вернуться назад.
При более внимательном рассмотрении кода в листинге 6.16 можно заметить, что
в функции traverse_xgb_tree код рекурсивно обходит дерево, сначала проходя по
левому поддереву (tree['children'][0]), а затем по правому (tree['children']
[1]). Это видно из рекурсивных вызовов traverse_xgb_tree(tree['children']
[0]) и traverse_xgb_tree(tree['children'][1]).
Листинг 6.16. Извлечение древовидной структуры XGBoost
import json
import matplotlib.pyplot as plt
from XGBoost import XGBClassifier, plot_tree
from collections import namedtuple
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=10,
max_depth=3)
model_pipeline = Pipeline(
[('processing', column_transform),
('XGBoost', xgb)])
Создает классификатор XGBoost,
ограниченный 10 оценщиками
и тремя уровнями
Извлекает модель XGBoost
из пайплайна
model_pipeline.fit(X=data, y=target_median)
model = model_pipeline["XGBoost"]
tree_info = model.get_booster().dump_model(
"xgb_model.json",
Загружает информацию
with_stats=True,
о модели XGBoost (бустер)
dump_format="json"
в JSON-файл
)
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=10,
max_depth=3)
Создает классификатор XGBoost,
ограниченный 10 оценщиками
и тремя уровнями
model_pipeline = Pipeline(
292[('processing',
Глава 6. Продвинутые
методы обработки
признаков
column_transform),
Извлекает
модель XGBoost
('XGBoost', xgb)])
из пайплайна
model_pipeline.fit(X=data, y=target_median)
model = model_pipeline["XGBoost"]
tree_info = model.get_booster().dump_model(
"xgb_model.json",
Загружает информацию
with_stats=True,
о модели XGBoost (бустер)
dump_format="json"
в JSON-файл
)
fig, ax = plt.subplots(figsize=(12, 15), dpi=300)
ax = plot_tree(
model, num_trees=0, ax=ax, rankdir='LR'
)
plt.show()
with open("xgb_model.json", "r") as f:
json_model = json.loads(f.read())
print(f"Число деревьев: {len(json_model)}")
tree_structure = json_model[0]
Создает диаграмму первого
дерева в ансамбле
Извлекает JSON-структуру
с информацией о модели с диска
Выводит количество деревьев
в модели и извлекает структуру
первого из них
Split = namedtuple("SplitNode", "feature origin gain count threshold")
Leaf = namedtuple("LeafNode", "index origin count")
def extract_xgb_node_info(tree):
return [tree['split'], tree['origin'], tree['gain'],
tree['cover'], tree['split_condition']]
def extract_xgb_leaf_info(tree):
return (
[tree['nodeid'],
tree['origin'],
tree['cover']
Функция извлечения
]
информации из листового узла
)
в структуре дерева
Функция извлечения различной
информации из разделенного
узла в древовидной структуре
def traverse_xgb_tree(tree):
Функция рекурсивного
if not 'origin' in tree:
обхода структуры дерева
tree['origin'] = "="
для извлечения путей
if not 'children' in tree:
return [[Leaf(*extract_xgb_leaf_info(tree))]]
left_branch = tree['children'][0]
right_branch = tree['children'][1]
left_branch['origin'] = '<'
right_branch['origin'] = '>='
left_paths = traverse_xgb_tree(left_branch)
right_paths = traverse_xgb_tree(right_branch)
node_info = [Split(*extract_xgb_node_info(tree))]
return [node_info + path for path in left_paths + right_paths]
paths = traverse_xgb_tree(tree_structure)
print(f"Число путей на дереве: {len(paths)}")
print("Путь 0:", paths[0])
6.4. Освоение градиентного бустинга 293
Код обучает модель XGBoost, сохраняет ее структуру, преобразует в читаемый
вид и представляет результаты пользователю:
Число деревьев: 10
Число путей на дереве: 8
Путь 0: [SplitNode(
feature='f5',
origin='=',
gain=19998.9316,
count=12223.75,
threshold=0.5),
SplitNode(
feature='f2',
origin='<',
gain=965.524414,
count=5871.5,
threshold=0.5
),
SplitNode(
feature='f13',
origin='<',
gain=66.1962891,
count=3756,
threshold=1.88965869
),
LeafNode(
index=7,
origin='<',
count=3528)
]
На рис. 6.8 полученные результаты сравниваются с графическим представлением
полного дерева, предоставленным plot_tree из самого пакета XGBoost.
Рис. 6.8. Вывод функции plot_tree из пакета XGBoost
294 Глава 6. Продвинутые методы обработки признаков
Из 10 деревьев, построенных моделью, код отображает первое из них и среди
восьми различных путей от всего набора данных до листа прогноза представляет
первый путь. Визуально он является крайним левым и состоит из последовательных узлов. Код выводит имя используемого признака, ветвь разбиения из
предыдущего узла (в XGBoost minor всегда обозначает левую ветвь, а major —
правую), пороговое значение разбиения, прирост целевой функции и результирующее сокращение выборки после разбиения. Вся эта информация позволяет
точно воспроизвести результаты каждого дерева модели XGBoost.
Мы также можем извлечь аналогичную структуру дерева из LightGBM, хотя
подход немного отличается, поскольку пакет LightGBM следует несколько
другим соглашениям. Например, XGBoost всегда разделяет значения, меньшие порога, по левой ветви, тогда как LightGBM для каждого узла определяет
правило с использованием операторов «меньше» либо «больше и равно» и разделяет по левой ветви, если правило выполняется, и по правой — если нет
(листинг 6.17).
Листинг 6.17. Извлечение структуры дерева LightGBM
from lightgbm import LGBMClassifier, plot_tree
lgbm = LGBMClassifier(boosting_type='gbdt',
n_estimators=10,
max_depth=3)
model_pipeline = Pipeline(
[('processing', column_transform),
('lightgbm', lgbm)])
model_pipeline.fit(X=data, y=target_median)
model = model_pipeline["lightgbm"]
tree_info = model._Booster.dump_model()["tree_info"]
tree_structure = tree_info[0]['tree_structure']
plot_tree(
booster=model._Booster,
tree_index=0,
Построение первого дерева в ансамбле
dpi=600
с помощью функции plot_tree
)
Split = namedtuple(
"SplitNode",
"feature origin decision_type threshold gain count"
)
Leaf = namedtuple("LeafNode", "index origin count value")
Извлекает информацию
о дереве из бустера модели
LightGBM
Извлекает структуру
первого дерева из
информации о нем
Функция извлечения
различной информации из
узла разбиения в структуре
дерева LightGBM
def extract_lgbm_node_info(tree):
return [tree['split_feature'], tree['origin'], tree['decision_type'],
tree['threshold'], tree['split_gain'], tree['internal_count']]
def extract_lgbm_leaf_info(tree):
return [
tree['leaf_index'],
tree['origin'],
tree['leaf_count'],
tree['leaf_value']
Функция извлечения информации
из листового узла в структуре
дерева LightGBM
Функция рекурсивного обхода
"SplitNode",
"feature origin decision_type threshold gain count"
)
Leaf = namedtuple("LeafNode", "index origin count value")
Функция извлечения
различной информации из
узла разбиения в структуре
дерева LightGBM
def extract_lgbm_node_info(tree):
6.4. Освоение градиентного бустинга 295
return [tree['split_feature'], tree['origin'], tree['decision_type'],
tree['threshold'], tree['split_gain'], tree['internal_count']]
def extract_lgbm_leaf_info(tree):
return [
tree['leaf_index'],
tree['origin'],
tree['leaf_count'],
tree['leaf_value']
]
Функция извлечения информации
из листового узла в структуре
дерева LightGBM
Функция рекурсивного обхода
структуры дерева LightGBM
для извлечения путей
def traverse_lgbm_tree(tree):
if not 'origin' in tree:
tree['origin'] = ""
if not 'left_child' in tree and not 'right_child' in tree:
return [[Leaf(*extract_lgbm_leaf_info(tree))]]
left_branch = tree['left_child']
right_branch = tree['right_child']
left_branch['origin'] = 'yes'
right_branch['origin'] = 'no'
left_paths = traverse_lgbm_tree(left_branch)
right_paths = traverse_lgbm_tree(right_branch)
node_info = [Split(*extract_lgbm_node_info(tree))]
return [node_info + path for path in left_paths + right_paths]
paths = traverse_lgbm_tree(tree_structure)
print(paths[0])
В результате выводится путь из структуры первого дерева в ансамбле:
[SplitNode(
feature=5,
origin='',
decision_type='<=',
threshold=1.0000000180025095e-35,
gain=20002.19921875,
count=48895),
SplitNode(
feature=2,
origin='yes',
decision_type='<=',
threshold=1.0000000180025095e-35,
gain=967.0560302734375,
count=23486),
SplitNode(
feature=13,
origin='yes',
decision_type='<=',
threshold=1.8896587976897459,
gain=67.53350067138672,
count=15024),
LeafNode(
index=0,
origin='yes',
count=14112,
value=-0.16892421857257725)
]
296 Глава 6. Продвинутые методы обработки признаков
На рис. 6.9 показано все дерево, построенное с помощью функции plot_tree, на
этот раз из пакета LightGBM.
Рис. 6.9. Вывод функции plot_tree из пакета LightGBM
Дерево отображается горизонтально, слева направо. Мы можем убедиться, что
возвращаемый кодом путь является самым верхним и заканчивается листом 0.
6.4.3. Ускорение с помощью GBDT и компиляции
Когда количество наблюдений или признаков велико, даже более быстрому
LightGBM может потребоваться много времени для обучения. Процесс можно
ускорить, уменьшив число наблюдений (параметр subsample) и признаков
(параметр colsample_bytree). Однако сокращение данных может негативно
сказаться на качестве модели. Альтернативой является использование GPU,
которые широко применяются в глубоком обучении. С их помощью обучение
проходит быстрее, особенно моделей XGBoost, а также, хотя и в меньшей степени, LightGBM.
С точки зрения моделирования использовать GPU в XGBoost довольно просто:
нужно просто указать "gpu_hist" в качестве значения параметра tree_method.
Однако в новой версии 2.0.0 этот метод устарел, и пользователи теперь могут
указать нужное устройство через параметр device. Также можно установить
его на "cpu" для выполнения на CPU, а также "cuda" или "gpu" для запуска
на GPU с поддержкой CUDA, что на данный момент является единственным
вариантом. В будущем будет поддерживаться больше типов GPU. Если у вас
несколько графических процессоров, вы можете указать их порядковый номер,
чтобы выбрать конкретное устройство; например, device="cuda:1" будет выполняться на втором GPU.
6.4. Освоение градиентного бустинга 297
Для работы XGBoost требуется как минимум CUDA 11.00 и GPU с вычислительной способностью 5.0. Если доступно несколько GPU, вы можете указать,
какой из них использовать, с помощью параметра gpu_id, который представляет
собой порядковый номер устройства, сообщаемый средой выполнения CUDA
(обычно устанавливается равным нулю, если у вас один графический процессор). Таким образом, XGBoost перемещает рост деревьев решений в память
и процессоры GPU, что значительно ускоряет операции, особенно расчет гистограмм признаков, как описано в статье «Accelerating the XGBoost Algorithm
Using GPU Computing» Митчелла (Mitchell) и Фрэнка (Frank) (https://peerj.
com/articles/cs-127/).
После обучения модели на GPU ее можно использовать для прогнозирования
на машине с GPU. Все, что нужно сделать, — установить параметр predictor на
gpu_predictor (или на cpu_predictor, если вы хотите использовать свой CPU).
Выбор GPU в качестве predictor также может ускорить расчет значений SHAP
и значения взаимодействия SHAP для интерпретации модели:
model.set_param({"predictor": "gpu_predictor"})
shap_values = model.predict(X, pred_contribs=True)
shap_interaction_values = model.predict(X, pred_interactions=True)
Если использование GPU с XGBoost довольно просто, то с LightGBM все обстоит
немного сложнее, поскольку она не имеет опции для работы на GPU, а требует
специальной версии, скомпилированной для этой цели. В зависимости от типа
операционной системы (Windows, Linux/Ubuntu, MacOS) компиляция может
быть более или менее сложной. Инструкции доступны по адресу https://mng.
bz/nRg5 для систем POSIX и по адресу https://mng.bz/vK8p для систем Windows.
Однако, если все необходимые компоненты на вашей системе установлены, как
указано в инструкциях по адресу https://mng.bz/4aJg, вы можете напрямую установить нужную версию с помощью команды pip install в командной строке
или оболочке:
pip install lightgbm --install-option=--gpu
После того как все установлено, нужно задать значение параметра device на
gpu . Однако не ждите поразительных улучшений производительности. Как
утверждают авторы LightGBM (см. https://mng.bz/vK8p), наилучшие результаты
достигаются на больших и плотных датасетах, а на меньших возникают задержки из-за неэффективного обмена данными. Уменьшение количества бинов
в гистограммном алгоритме также повысит эффективность работы GPU. Для
оптимальной производительности рекомендуется установить max_bin=15 и использовать одинарную точность, gpu_use_dp=false.
298 Глава 6. Продвинутые методы обработки признаков
GPU значительно ускоряют обучение, но для этапа предсказаний можно подобрать и другие варианты. Благодаря доступности структур деревьев, как мы
видели в предыдущем разделе, стали возможны проекты, которые используют
эту информацию для пересборки деревьев на более производительных языках
программирования, таких как C, JAVA или LLVM, способных преобразовать
модель в чистый ассемблерный код. Такие проекты компиляции деревьев нацелены на быстрое прогнозирование и более простое развертывание. Примерами
являются Treelite (https://github.com/dmlc/treelite), который может читать модели,
созданные XGBoost, LightGBM и даже scikit-learn, а также lleaves (https://github.
com/siboehm/lleaves), который является проектом только для LightGBM.
Treelite — это универсальный формат для обмена и сериализации лесов деревьев
решений. Он компилирует GBDT в C или Java с минимальным количеством
зависимостей, поэтому можно легко развернуть его в любой системе. Для тестирования необходимо установить несколько пакетов через командную строку:
pip install tl2cgen treelite treelite_runtime (листинг 6.18).
Листинг 6.18. Ускорение прогнозирования XGBoost с помощью Treelite
import treelite
import treelite_runtime
import tl2cgen
xgb = XGBClassifier(booster='gbtree',
objective='reg:logistic',
n_estimators=10,
max_depth=3)
model_pipeline = Pipeline(
[('processing', column_transform),
('XGBoost', xgb)])
model_pipeline.fit(X=data, y=target_median)
model = model_pipeline["XGBoost"]
Сохраняет модель XGBoost
в JSON-файл
model.save_model("./xgb_model.json")
Загружает модель XGBoost
treelite_model = treelite.Model.load("./xgb_model.json",
в формате Treelite из JSON-файла
model_format="XGBoost_json")
tl2cgen.generate_c_code(treelite_model, dirpath="./", Генерирует код C из модели Treelite
params={"parallel_comp": 4})
и экспортирует его как общую
tl2cgen.export_lib(treelite_model, toolchain="gcc",
библиотеку
libpath="./xgb_model.so",
Преобразует входные
params={"parallel_comp": 4})
данные, используя этапы
предварительной обработки,
predictor = tl2cgen.Predictor("./xgb_model.so")
заданные в пайплайне
X = model_pipeline["processing"].transform(data)
dmat = tl2cgen.DMatrix(X)
Создает DMatrix Treelite из преобразованных данных,
predictor.predict(dmat)
совместимый с экспортированной моделью Treelite
Итоги 299
Результатом является скомпилированная модель, которая внутри скрипта Python
может возвращать прогнозы гораздо быстрее. Предикторы всегда должны быть
преобразованы заранее, поскольку пайплайн не является частью компиляции —
компилируется только модель. Кроме того, данные необходимо преобразовать
в формат DMatrix, собственный формат данных XGBoost, перед подачей в скомпилированную модель.
Пакет lleaves был разработан Саймоном Бемом (Simon Boehm). Он обещает
ускорение в 10 раз за счет компиляции LLVM в ассемблерный код на основе текстовой структуры дерева, которую можно экспортировать из модели LightGBM.
После установки пакета с помощью инструкции pip install lleaves в командной
строке можно добиться ускорения, выполнив следующие шаги (листинг 6.19).
Листинг 6.19. Ускорение прогнозирования LightGBM с помощью lleaves
import lleaves
lgbm = LGBMClassifier(boosting_type='gbdt',
n_estimators=10,
max_depth=3)
model_pipeline = Pipeline(
[('processing', column_transform),
('lightgbm', lgbm)])
model_pipeline.fit(X=data, y=target_median)
model = model_pipeline["lightgbm"]
Сохраняет
модель LightGBM
в текстовый файл
model.booster_.save_model('lgb_model.txt')
Загружает модель
LightGBM с помощью
библиотеки lleaves
llvm_model = lleaves.Model(model_file="lgb_model.txt")
llvm_model.compile()
Компилирует загруженную
X = model_pipeline["processing"].transform(data)
модель LightGBM
llvm_model.predict(X)
Преобразует входные данные
с использованием шагов
предобработки, определенных
в пайплайне
в представление LLVM
Кроме того, в этом случае модель компилируется и может предсказывать быстрее
внутри скрипта Python. В целом lleaves, хотя и ограничен только LightGBM, является решением для компиляции, которое требует от пользователя гораздо меньше
настроек и спецификаций. Это делает его использование более простым и удобным.
Итоги
Среди проблем обработки пропущенные данные являются одной из самых
сложных. Если они относятся к типу MCAR или просто MAR, когда пропуски
300 Глава 6. Продвинутые методы обработки признаков
связаны с другими признаками, можно восстановить их с помощью многомерной импутации, которая использует корреляции между предикторами
в наборе данных.
Алгоритмы XGBoost и LightGBM автоматически обрабатывают пропущенные данные, направляя их в ту ветвь разбиения, которая минимизирует
функцию потерь.
Когда категориальный признак имеет высокую кардинальность из-за большого количества меток, можно использовать кодирование по целевой переменной, которое приобрело популярность в соревнованиях Kaggle. Это способ
преобразования значений категориального признака в соответствующие
ожидаемые значения целевой переменной.
PDP — это независимая от модели техника построения диаграмм, которая
объясняет, как связаны признаки и целевая переменная через обученную
модель. Метод помогает лучше понять нелинейные и сложные зависимости
между предикторами и целевой переменной.
XGBoost благодаря таким пакетам, как XGBFIR, позволяет выявлять наиболее важные взаимодействия между признаками.
Используя эффективные методы отбора признаков, можно точно определить
и сохранить наиболее значимые из них, те, что вносят существенный вклад
в процесс машинного обучения. Стандартные методы отбора признаков —
это стабильный отбор на основе регуляризации L1 для линейных моделей,
итеративный отбор и Boruta для ансамблей деревьев.
Стабильный отбор основан на регуляризации L1 и направлен на выявление признаков, которые последовательно обнаруживаются как важные
в нескольких подмножествах, что указывает на их надежность и снижает
вероятность отбора признаков случайно или из-за шума.
Boruta — это процедура определения значимости признака в задаче машинного обучения, основанная на внутренних параметрах модели, таких как
коэффициенты в линейных моделях или значения важности, основанные
на приросте, например в деревьях решений и их ансамблях.
Итеративный отбор добавляет признаки (прямое включение) или удаляет
их (обратное исключение) на основе их влияния на прогнозную производительность, жадным образом оставляя только существенные признаки.
Оптимизация гиперпараметров позволяет дополнительно повысить производительность классической модели машинного обучения. Помимо ручной
настройки популярными методами оптимизации среди специалистов по
данным являются, в зависимости от используемой модели, поиск по сетке,
случайный поиск, последовательный халвинг и байесовская оптимизация.
Итоги 301
Поиск по сетке работает путем перебора всех возможных комбинаций
значений гиперпараметров. Для каждого из них выбирается последовательность значений и тщательно перебираются все их комбинации.
Случайный поиск тестирует значения, случайным образом выбирая их
из заданного пространства. Этот метод особенно эффективен, если вы
мало знаете о своих гиперпараметрах, если их много или некоторые из
них нерелевантны, но вы не знаете, какие именно.
Последовательный халвинг — это обертка ранее обсуждавшихся стратегий. Он работает по принципу соревнований между наборами гиперпараметров, где сначала они тестируются с использованием минимальных
вычислительных ресурсов, а затем только часть лучших проходит дальнейшее тестирование с использованием большего количества ресурсов.
В итоге останется только один-единственный набор гиперпараметров.
Байесовская оптимизация использует информированный поиск для
нахождения наилучшего набора гиперпараметров. Она строит модель
поведения гиперпараметра на основе предшествующих знаний о том,
как он работает с задачей. Затем она проводит ряд экспериментов для
дальнейшего изучения и уточнения внутренней модели, использования
результатов предыдущих испытаний и проверки фактических характеристик решения.
И XGBoost, и LightGBM имеют особые настройки и опции, которые редко
встречаются в других алгоритмах машинного обучения, например возможность извлечения и визуализации внутренней структуры моделей, а также
ускорение их выполнения за счет использования GPU и компиляции.
7
Полный пример
применения XGBoost
В этой главе
3 Сбор и подготовка данных из интернета с использованием
генеративного ИИ
3 Создание базовой модели для последующей оптимизации
3 Принципы работы модели и ее проверка
В этой главе мы завершаем обзор классического машинного обучения для анализа табличных данных. В качестве заключения мы разберем полный пример
из области журналистики данных. По ходу дела обобщим все использованные
до сих пор концепции и методы. Мы также воспользуемся инструментом генеративного ИИ — ChatGPT, — чтобы выполнить задачу и продемонстрировать,
как большая языковая модель (LLM) может облегчить работу с табличными
данными.
Наконец, мы построим модель для прогнозирования цен, на этот раз с использованием подхода на основе регрессии. Это поможет понять, как работает модель и почему она функционирует определенным образом, даст более глубокое
представление о динамике ценообразования для объявлений Airbnb и позволит
проверить нашу первоначальную гипотезу о формировании цены краткосрочной
аренды.
7.1. Подготовка и исследование данных 303
7.1. Подготовка и исследование данных
Для начала мы сосредоточимся на другом датасете, продолжая анализ объявлений о краткосрочной и долгосрочной аренде жилья на Airbnb NYC. Этот набор
данных взят непосредственно из инициативы Inside Airbnb Network (http://
insideairbnb.com/), «проекта, основанного на миссии по предоставлению данных
о влиянии Airbnb на жилые сообщества и защите их интересов». Мы также
будем использовать общедоступные данные из других онлайн-сервисов, таких
как Foursquare (https://foursquare.com) — компании, занимающейся технологиями
геолокации и социальными сетями.
После этапа сбора данных мы организуем и проведем комплексную генерацию
признаков на основе соответствующих бизнес-гипотез, чтобы извлечь ценные
идеи для этапа моделирования. В ходе этого процесса мы также выполним базовый разведочный анализ наших предикторов и целевых переменных, внеся
необходимые корректировки или исключая примеры и признаки, чтобы обес
печить получение оптимальных данных для нашего проекта.
7.1.1. Использование генеративного ИИ
для подготовки данных
ChatGPT — это усовершенствованная языковая модель, разработанная OpenAI.
Для создания и обучения модели генеративного предварительно обученного
трансформера (generative pretrained transformer, GPT), такой как ChatGPT,
OpenAI использовала огромные объемы разнообразных текстов из интернета,
чтобы помочь модели научиться понимать и генерировать текст, похожий на
созданный человеком, предсказывая следующее слово в последовательности
на основе контекстного понимания. Это предварительное обучение позволяет
ChatGPT понимать грамматику, контекст и даже смысловые нюансы, но этого
недостаточно, чтобы сделать его полезным ассистентом в любых обстоятельствах.
Фактически эти модели могут выдавать неточные, вредоносные или токсичные
результаты. Причина в том, что обучающий датасет, то есть интернет, содержит
разнородные тексты с порой недостоверной информацией. Для повышения безопасности, полезности и согласованности моделей ChatGPT используется метод
обучения с подкреплением на основе обратной связи от человека (reinforcement
learning from human feedback). В процессе такого обучения люди-оценщики
предоставляют обратную связь, иллюстрирующую предпочтительное поведение
модели, и оценивают сгенерированные ею результаты путем ранжирования. Эти
данные впоследствии используются для дальнейшей тонкой настройки GPT‑3.5,
улучшая его ответы.
Чтобы использовать бесплатную версию ChatGPT (на данный момент это
версия 3.5, которая обновлена информацией по январь 2022 года), необходимо
304 Глава 7. Полный пример применения XGBoost
сначала создать учетную запись на https://chat.openai.com. После этого можно приступить к использованию ИИ, просто вводя промпт — письменную инструкцию
или ввод, предоставляемый модели для генерации определенного результата.
Промпт служит запросом или указанием, которое подсказывает модели, как
создать релевантный ответ. Промпты могут различаться по сложности: от простых команд до более подробных описаний, и они играют решающую роль в формировании характера выходных данных языковой модели. Качество и ясность
промпта существенно влияют на точность и релевантность сгенерированного
контента, но выбрать подходящую формулировку не всегда просто.
Эффективность различных промптов может варьироваться в зависимости от
конкретной языковой модели, для которой они предназначены. Каждая LLM
имеет свои сильные и слабые стороны, а также особенности, что делает необходимым адаптировать промпты соответствующим образом. Например, при
работе с ChatGPT мы получали наилучшие результаты, начав с простых запросов, оценив их результаты, а затем продолжая добавлять больше спецификаций,
чтобы уточнить результаты в соответствии с нашими ожиданиями. ChatGPT, как
правило, работает лучше, когда вы просите его «написать», «создать», «показать,
как» или «обобщить». Иногда весьма полезно давать примеры и показывать, как
нужно работать с ними. Кроме того, сообщите ИИ свои ожидания относительно ответа: насколько длинным он должен быть, какую информацию включать,
хотите ли вы получить только код или только текст, а также как должен быть
структурирован ответ, например вы можете запросить результат в формате JSON
или списка Python.
LLM, такие как ChatGPT и связанная с ними функция Copilot в GitHub, оказались полезными помощниками для различных задач программирования
и применимы для работы с табличными данными. Вы можете задавать этим
моделям различные вопросы или запрашивать помощь в написании кода, и они
могут предоставить фрагменты кода, объяснения того, как он работает, или
рекомендации по использованию конкретных команд или алгоритмов. LLM,
такие как ChatGPT, помогают пользователям, генерируя фрагменты кода для
задач работы с данными, их очистки и преобразования, а также предоставляя
объяснения и руководство по различным статистическим и машинным методам
обучения, применимым к табличным датасетам. Однако цель этой главы — показать несколько избранных и менее очевидных возможностей ИИ, которые
можно использовать для анализа и моделирования табличных данных.
7.1.2. Получение и подготовка данных
В качестве отправной точки мы перейдем на сайт Inside Airbnb Network (http://
insideairbnb.com/) и найдем необходимые нам данные. Наша цель — изучить рынок недвижимости в другом городе, Токио. Сначала нужно вручную загрузить
данные и сохранить их в рабочем каталоге на вашем компьютере или в облачном
7.1. Подготовка и исследование данных 305
хранилище. Для этого на главной странице инициативы Inside Airbnb Network,
как показано на рис. 7.1, в меню Data выберите Get the Data.
Рис. 7.1. Меню Data
После этого вы перейдете на новую страницу загрузки данных, где представлены различные города и соответствующие файлы для скачивания. Прокрутите
страницу и найдите город Токио.
На рис. 7.2 показана часть страницы, содержащая файлы данных по Токио,
актуальных на момент написания этой книги.
Рис. 7.2. Раздел загрузки данных по Токио
306 Глава 7. Полный пример применения XGBoost
Для анализа нам понадобятся два файла с этой страницы: listings.csv, который
содержит сводные данные и другую информацию о предлагаемом на Airbnb
жилье в Токио, и calendar.csv.gz, архивный файл, содержащий calendar.csv,
датасет с информацией о заполненности предлагаемых в аренду домов и квартир, а также ценах за определенный год для каждого объявления. Наведите
указатель мыши на ссылки, щелкните правой кнопкой и выберите сохранение на
диск в вашем рабочем каталоге. Например, в Google Chrome вам нужно выбрать
«Сохранить ссылку как» (Save link as), а в Mozilla Firefox — «Сохранить объект
как» (Save target as). Затем потребуется извлечь файлы в ваш рабочий каталог.
Как только они будут распакованы в локальном каталоге, можно загрузить их
в pandas DataFrame с помощью команды read_csv:
import pandas as pd
summary_listings = pd.read_csv("listings.csv")
Имея список и тип столбцов, мы можем получить представление о типах данных,
с которыми будем иметь дело:
summary_listings.dtypes
Список состоит из 18 столбцов, в основном таких же, как в датасете Airbnb NYC,
представленном в главе 3. Опишем каждый из них.
id — уникальный идентификатор для каждого объявления на Airbnb. Это
тип данных int64, то есть числовое представление. В других таблицах этот
столбец может называться listing_id.
name — описание объекта на Airbnb. Это тип object, который обычно пред-
ставляет собой строку или текст.
host_id — уникальный идентификатор для каждого арендодателя на Airbnb.
Тип данных int64.
host_name — имя человека, давшего объявление. Тип данных object.
neighborhood_group — представляет более широкую область или регион, в ко-
тором находится район со сдаваемым объектом. Данные хранятся в формате
float64, но важно отметить, что его использование для представления групп
или категорий встречается редко. В нашем случае это указывает на то, что
данные в этом поле полностью состоят из пропущенных значений.
neighborhood — описывает, что находится рядом с объектом, сдаваемым
в аренду. Тип данных object.
latitude — координата широты для дома или квартиры. Тип данных float64.
longitude — координата долготы для дома или квартиры. Тип данных float64.
room_type — тип комнаты или помещения, предлагаемого в объявлении
(например, весь дом/квартира, отдельная комната, комната на несколько
человек). Тип данных object.
7.1. Подготовка и исследование данных 307
price — цена за ночь проживания. Это тип данных int64, представляющий
целочисленное значение цены.
minimum_nights — минимальное количество ночей, доступное для бронирования дома или квартиры. Тип данных int64.
number_of_reviews — общее количество отзывов, полученных по объекту
в объявлении. Тип данных int64.
last_review — дата последнего отзыва. Тип данных object, который может
представлять информацию о дате и времени, но может потребоваться дальнейший анализ для эффективного использования.
reviews_per_month — среднее количество отзывов на объект в месяц. Тип
данных float64.
calculated_host_listings_count — общее количество объявлений, которые
владелец разместил на Airbnb. Тип данных int64.
availability_365 — количество дней, в течение которых объект доступен для
бронирования в году (из 365 дней). Тип данных int64.
number_of_reviews_ltm — количество отзывов, полученных за последние
12 месяцев. Тип данных int64.
license — номер лицензии или информация, связанная с объявлением. Тип
данных object, который обычно представляет собой строку или текст.
Можно спокойно проигнорировать такие признаки, как host_id, host_name,
neighboring_group (потому что данные в них почти полностью пропущены)
или license (который является своего рода идентификатором, составленным
на основе лицензии на право заниматься сдачей в аренду).
Что касается других признаков, то, хотя большинство из них являются числовыми, признак name представляет собой строку, содержащую различную
информацию, которую нам предстоит извлечь. Визуализация одного примера
позволяет получить представление о ее организации:
summary_listings['name'].iloc[0]
Строка состоит из пяти отдельных частей, разделенных условными знаками
и имеющих своего рода частично структурированное и повторяющееся содержание:
'Rental unit in Sumida · «4.78 · 1 bedroom · 2 beds · 1 bath'
Первая часть строки — описание типа и местоположения объекта для сдачи,
вторая — средняя оценка, полученная от гостей, третья — количество спален,
четвертая — количество кроватей, а последняя — количество ванных комнат.
308 Глава 7. Полный пример применения XGBoost
Помимо числовых значений мы также можем извлечь некоторую специфическую
информацию, связанную с типом предлагаемого жилья или услуг: является ли
квартира студией, ванная комната отдельная или общая для нескольких человек,
есть ли в санузле душ или ванна или только туалет и умывальник (half-bath).
Мы можем работать с такой информацией, создавая простые проверки строковых соответствий и получая бинарный признак, указывающий на наличие или
отсутствие характеристики, или используя команды регулярных выражений
regex (сокращение от regular expressions), которые представляют собой последовательность символов, составляющих шаблон поиска. Они применяются для
сопоставления шаблонов в строках. В таблице 7.1 показаны преобразования,
которые мы применяем к полю описания, и выделено, какие строки мы хотим
сопоставить, какую команду регулярных выражений используем и какой результирующий признак получаем.
Таблица 7.1. Команды Regex для проектирования признаков
Совпадающее описание
Регулярное выражение
Результирующий
признак
Текст, начинающийся с «in», затем
пробел, затем захватывает любые
символы до тех пор, пока не встретит еще один пробел, и заканчивается точкой
r'in\s(.*?)\s·'
В объявлении указан
район Токио
Текст, начинающийся с символа
«★» (звезда), за которым следует
одна или несколько цифр, точка
и одна или несколько дополнительных цифр (например, ★ 4.5)
r'★(\d+\.\d+)'
Рейтинг в звездах
Текст, содержащий числовое значение, за которым следует ноль или
более пробелов и слова bedroom
(спальня) или bedrooms (с буквой s
или без нее) на конце
r'(\d+)\s*(?:bedroom
|bedrooms)'
Количество спален
Текст, содержащий числовое значение, за которым следует один или
несколько пробелов и слово bed
(кровать) или beds как целое слово
(с буквой s или без нее на конце)
r'(\d+)\s+(?:beds?\b)'
Количество кроватей
Текст, содержащий числовое значение, представляющее количество
ванных комнат
r'(?P<baths>\d+)\
s*(shared\
s+)?(?:half-)?baths?\b'
Количество ванных
комнат
7.1. Подготовка и исследование данных 309
Работа с регулярными выражениями может оказаться довольно сложной. И это
первый пример, где генеративный ИИ может нам помочь. Большинство LLM,
таких как ChatGPT, хорошо знают различные языки программирования (в частности, большинство из них весьма сильны в Python), поскольку они обучались
на тексте и информации, извлеченной из интернета, где много сведений о том,
как писать код даже для очень специфических задач. В нашем случае достаточно
показать в промпте пример строки и попросить извлечь нужную информацию1:
Имеются строки в таком формате: 'Rental unit in Sumida · «4.78
· 1 bedroom · 2 beds · 1 bath'
С помощью регулярных выражений извлеки из них следующую информацию: 1) район
Токио (area of Tokyo), указанный в объявлении; 2) рейтинг в звездах в виде
числа с плавающей точкой; 3) количество спален; 4) количество спальных мест;
5) количество ванных комнат.
В промпте следует указать, что выходные данные должны быть готовы к использованию и организованы в блоки фрагментов кода с пояснениями о правилах
извлечения, например так, как показано на рис. 7.3.
Рис. 7.3. Результаты запроса на написание регулярных выражений в ChatGPT 3.5
1
В оригинале запрос на английском языке, соответственно скриншот с частью ответа
ChatGPT также на английском. В любом случае (даже если повторить оригинальный
промпт) не следует ожидать, что ответ ChatGPT будет в точности таким же, как у автора. — Примеч. ред.
310 Глава 7. Полный пример применения XGBoost
Без дополнительных запросов языковая модель самостоятельно предложит
решение на Python, а вам просто нужно нажать на значок «Копировать код»
(Copy code) в верхней части листинга, чтобы скопировать фрагмент в буфер
обмена, а затем вставить его в ячейку в Colab или редактор IDE.
Обычно предлагаемые решения могут варьироваться от запроса к запросу
и могут отличаться от решения, приведенного в таблице. Это связано с тем, что
LLM в конечном счете являются вероятностными машинами. Температура —
это параметр, который обычно задается для влияния на случайность выходных
данных модели. Она используется во время генерации текста для управления
креативностью сгенерированного контента. Проще говоря, температура влияет
на вероятность выбора моделью следующего слова в последовательности. Низкие
значения температуры приводят к более детерминированному и ожидаемому
результату. Напротив, высокие значения температуры вносят больше случайности и креативности в сгенерированный вывод, поскольку модель склонна
выбирать менее вероятные слова.
После получения очевидно полезного ответа одним из важных шагов перед
использованием решений, предложенных LLM, является их тестирование на
большем количестве примеров, чем один или два, показанных в промпте. Такой
шаг помогает выявить, что код работает некорректно, и вам также может потребоваться сообщить модели, что ее пример не работает, или указать на проблему
с помощью более детальных инструкций. Например, мы обнаружили, что иногда
команды давали неверные результаты, если в некоторых частях входной строки
были заглавные буквы. Следовательно, нам пришлось найти дополнительное
решение — установить флаг re.IGNORECASE, который делает операцию сопоставления нечувствительной к регистру. В листинге 7.1 мы приступаем к извлечению
информации из текстовых описаний с помощью команд регулярных выражений,
которые мы написали с помощью ChatGPT.
Листинг 7.1. Извлечение информации из текстовых описаний
import pandas as pd
import re
classification_list = [
'aparthotel', 'barn', 'bed and breakfast', 'boutique hotel',
'bungalow', 'cabin', 'camper/rv', 'chalet', 'condo', 'cottage',
'earthen home', 'farm stay', 'guest suite', 'guesthouse', 'home',
'hostel', 'hotel', 'houseboat', 'hut', 'loft', 'place to stay',
'rental unit', 'resort', 'ryokan', 'serviced apartment',
'tiny home', 'townhouse', 'treehouse', 'vacation home', 'villa']
summary_listings = summary_listings.assign(
type_of_accommodation=(
summary_listings['name']
.str.extract(
f"({'|'.join(classification_list)})",
flags=re.IGNORECASE)),
Извлекает тип размещения
area_of_tokyo=(
из списка вариантов
summary_listings['name']
.str.extract(
r'in\s(.*?)\s·',
flags=re.IGNORECASE)),
Извлекает указание на район
score=(
Токио в названии объявления
'earthen home', 'farm stay', 'guest suite', 'guesthouse', 'home',
'hostel', 'hotel', 'houseboat', 'hut', 'loft', 'place to stay',
'rental unit', 'resort', 'ryokan', 'serviced apartment',
'tiny home', 'townhouse', 'treehouse', 'vacation home', 'villa']
summary_listings = summary_listings.assign(
type_of_accommodation=(
summary_listings['name'] 7.1. Подготовка и исследование данных 311
.str.extract(
f"({'|'.join(classification_list)})",
flags=re.IGNORECASE)),
Извлекает тип размещения
area_of_tokyo=(
из списка вариантов
summary_listings['name']
.str.extract(
r'in\s(.*?)\s·',
flags=re.IGNORECASE)),
Извлекает указание на район
score=(
Токио в названии объявления
summary_listings['name']
.str.extract(
r'(\d+\.\d+)',
flags=re.IGNORECASE)
.astype(float)),
Извлекает звездный рейтинг,
number_of_bedrooms=(
за которым следует числовое
summary_listings['name']
значение
.str.extract(
r'(\d+)\s*(?:bedroom|bedrooms)',
flags=re.IGNORECASE)
.fillna(0)
.astype(int)),
Извлекает количество спален из
number_of_beds=(
названия объявления
summary_listings['name']
.str.extract(
r'(\d+)\s+(?:beds?\b)',
flags=re.IGNORECASE)
.fillna(0)
.astype(int)),
Извлекает количество кроватей
number_of_baths=(
из названия объявления
summary_listings['name']
.str.extract(
r'(?P<baths>\d+)\s*(shared\s+)?' +
r'(?:half-)?baths?\b',
flags=re.IGNORECASE)["baths"]
Извлекает количество ванных
.fillna(0)
комнат из названия объявления
.astype(int)),
)
Листинг 7.2 завершает работу по извлечению признаков путем создания дополнительных булевых столбцов на основе определенных ключевых слов в столбце
name. Он вычисляет два новых признака: разницу между сегодняшней датой
и датой последнего отзыва из столбца last_review, а также отношение количества
отзывов за последний год к их общему числу, что может показать, является ли
большая часть отзывов недавними или этот вариант размещения пользовался
успехом в основном в прошлом.
Листинг 7.2. Извлечение бинарных флагов и временнˆых характеристик
import numpy as np
import pandas as pd
from datetime import datetime
Проверяет наличие слова new
summary_listings = summary_listings.assign(
в столбце name (без учета
is_new=(summary_listings['name']
регистра)
.str.contains('new', case=False)
.astype(int)),
Проверяет наличие слова studio
is_studio=(summary_listings['name']
в столбце name (без учета
.str.contains('studio', case=False)
регистра)
.astype(int)),
has_shared_bath=(summary_listings['name']
.str.contains('shared', case=False)
import numpy as np
import pandas as pd
from datetime import datetime
Проверяет наличие слова new
summary_listings = summary_listings.assign(
312is_new=(summary_listings['name']
Глава 7. Полный пример применения XGBoost
в столбце name (без учета
регистра)
.str.contains('new', case=False)
.astype(int)),
Проверяет наличие слова studio
is_studio=(summary_listings['name']
в столбце name (без учета
.str.contains('studio', case=False)
регистра)
.astype(int)),
has_shared_bath=(summary_listings['name']
.str.contains('shared', case=False)
.astype(int)),
Проверяет наличие слова shared
has_half_bath=(summary_listings['name']
в столбце name (без учета
.str.contains('half', case=False)
регистра)
.astype(int)),
)
Проверяет наличие слова half
в столбце name (без учета регистра)
summary_listings['days_since_last_review'] = (
datetime.today() –
pd.to_datetime(
summary_listings['last_review'])
).dt.days
summary_listings['days_since_last_review'] = (
Вычисляет количество дней
summary_listings['days_since_last_review'] summary_listings['days_since_last_review'].min()
между сегодняшней датой
)
и датой последнего отзыва
zero_reviews = summary_listings['number_of_reviews'] == 0
ratio = summary_listings['number_of_reviews_ltm'] /
summary_listings['number_of_reviews']
Вычисляет отношение number_
summary_listings['number_of_reviews_ltm_ratio'] = (
of_reviews_ltm к number_of_
np.where(zero_reviews, 0, ratio)
reviews для каждого объявления
)
В данных summary_listings также присутствует признак price, который мы
могли бы использовать в качестве целевой переменной. Но мы предпочитаем
создать ее путем агрегации данных из calendar.csv, чтобы решить, следует ли
выбрать среднее значение всех цен, минимум или максимум. Файл calendar.
csv содержит информацию на каждый день о доступности размещения, его цене
(с учетом скидок), а также минимальном и максимальном количестве ночей,
доступных для бронирования. Нас интересует обработка скорректированной
цены в качестве целевой переменной, представляющей собой эффективную
рыночную стоимость размещения (листинг 7.3).
Листинг 7.3. Создание целевой переменной из ежедневных данных
объявлений
calendar = pd.read_csv("calendar.csv")
calendar["adjusted_price"] = (
calendar["adjusted_price"]
.apply(lambda x: float(
x.replace('$', '')
.replace(',', ''))
Удаляет знак доллара ($) и запятые (,)
)
из значений, а затем преобразует их
)
в число с плавающей точкой
price_stats = (
calendar.groupby('listing_id')['adjusted_price']
Операция группировки на
.agg(['mean', 'min', 'max'])
основе столбца listing_id
)
Вычисляет три статистических показателя
для столбца adjusted_price: среднее,
минимальное и максимальное значения
calendar["adjusted_price"] = (
calendar["adjusted_price"]
.apply(lambda x: float(
x.replace('$', '')
.replace(',', ''))
Удаляет знак доллара
($) и запятые (,)данных 313
7.1. Подготовка
и исследование
)
из значений, а затем преобразует их
)
в число с плавающей точкой
price_stats = (
calendar.groupby('listing_id')['adjusted_price']
Операция группировки на
.agg(['mean', 'min', 'max'])
основе столбца listing_id
)
Вычисляет три статистических показателя
для столбца adjusted_price: среднее,
минимальное и максимальное значения
После завершения агрегации можно проверить результат, запросив первые пять
строк этого нового датасета:
price_stats.head()
На рис. 7.4 видно, что теперь у нас есть средняя, максимальная и минимальная
цены для каждого объявления.
Рис. 7.4. Статистика цен
в объявлениях Airbnb
Мы сохраним датафрейм price_stats и в следующем разделе сосредоточимся
на оптимизации количества признаков и их улучшении.
7.1.3. Генерация более сложных признаков
Объекты недвижимости обладают весьма специфическими характеристиками,
отличающими их от других товаров или услуг на рынке. В сфере недвижимости
существует поговорка: «Главное — это локация, локация и еще раз локация».
Расположение квартиры в центре города или рядом с трассой может существенно
повлиять на стоимость. Мы возьмем эту поговорку себе на заметку и сгенерируем признаки для объявлений Airbnb на основе местоположения (локации)
представленных в них объектов.
В качестве первого шага повторим пример из предыдущей главы, где мы создали
небольшие географические зоны, которые потом преобразовали с помощью кодирования по целевой переменной. Такой подход позволит уловить специфические
особенности района, хотя объяснить, почему аренда в одном месте стоит дороже,
чем в другом, будет сложно. Мы должны подготовить более конкретные признаки, чтобы обеспечить объяснимость для объявлений в Токио. Вот еще один
пример, когда генеративный ИИ может помочь практикующему специалисту
по табличным данным и предложить разнообразные идеи.
314 Глава 7. Полный пример применения XGBoost
Языковые модели обработали больше данных, чем вы можете себе представить,
и если задать им вопрос с достаточной детализацией (и немного поучаствовать
в ролевой игре, например попросить их изобразить эксперта в определенной
предметной области), они могут предоставить подсказки и идеи, на которые вам
пришлось бы потратить несколько часов исследований и изучения информации
в интернете.
Наш промпт для ChatGPT:
Ты — эксперт в области анализа данных. Ты загрузил датасет, содержащий
агрегированную информацию с Airbnb. Вот структура датасета: id (int64), name
(object), neighborhood (object), latitude (float64), longitude (float64), room_
type (object), price (int64), minimum_nights (int64), number_of_reviews (int64),
last_review (object), reviews_per_month (float64), calculate_host_listings_count
(int64), availability_365 (int64), number_of_reviews_ltm (int64). Ты обучаешь
ML-модель для прогнозирования цен в объявлениях Airbnb. Какие признаки следует
сгенерировать для повышения производительности модели?
Наша стратегия заключается в том, чтобы задать персону («ты эксперт по данным») и предоставить дополнительную информацию о доступных признаках
(удалив те, которые мы уже решили не использовать) и целевой переменной.
Здесь мы использовали тот факт, что ожидаем от ChatGPT некоторого знания о нашем датасете («датасет, содержащий агрегированную информацию
с Airbnb»), но можно предложить LLM более сложные задачи, кратко описав
набор данных, включая типы признаков и их взаимосвязи. Кроме того, если у вас
есть какие-либо знания в области, относящиеся к датасету, например любые гипотезы о том, как признаки могут быть связаны с целевой переменной, поделитесь
ими с LLM. Эта информация поможет ей определить признаки, которые могут
быть важны для задачи. Более того, предоставление только списка имеющихся
в датасете признаков, как это сделали мы, может оказаться недостаточным для
более сложных проблем. Объяснения или описания каждой переменной и четкое
указание цели, например прогнозирование оттока клиентов, оценка стоимости
недвижимости либо прогнозирование продаж, помогут LLM выявить пробелы
или возможности для создания новых, хорошо продуманных признаков.
Ниже приведены признаки или их типы, которые надо сгенерировать для получения наилучших результатов в нашем датасете и решаемой задаче, которые
предложил ChatGPT.
Расстояние до ключевых локаций — вычислить расстояние объекта из каждого
объявления до важных точек города или достопримечательностей. Это можно
сделать, используя информацию о широте и долготе.
Статистика по районам — агрегировать статистику по каждому району,
например средняя, минимальная и максимальная цены, а также количество
объектов в каждом районе.
7.1. Подготовка и исследование данных 315
Кодирование типа помещения — преобразовать категориальную переменную
room_type в числовые значения с помощью one-hot или кодирования меток.
Временнˆые признаки — извлечь признаки из столбца last_review, например
месяц или сезон последнего отзыва или время, прошедшее с момента последнего отзыва.
Опыт арендодателя — объединить информацию из calculate_host_listings_
count и number_of_reviews_ltm для создания признака, отражающего опыт
или популярность арендодателя.
Плотность бронирования — вычислить отношение number_of_reviews
к availability_365 — это отражает частоту бронирования объекта относительно его доступности.
Тональность отзывов — если у вас есть доступ к реальным текстам отзывов,
можно использовать методы обработки естественного языка для анализа
тональности и извлечения соответствующих признаков.
Признаки взаимодействия — создать взаимодействия между соответствующими признаками, например произведение minimum_nights и availability_365,
чтобы учесть общую доступность для длительного проживания.
Биннинг цен — преобразовать непрерывную переменную price в бины или
категории и создать новый категориальный признак, представляющий различные ценовые диапазоны.
Географическая кластеризация — использовать алгоритмы кластеризации
для группировки объектов по географической близости. Это позволит выявить пространственные закономерности в ценообразовании.
Из предложенных идей несколько действительно перекликаются с нашим
опытом, например вычисление расстояния до ключевых локаций и статистики по районам или создание географических кластеров. Кроме того, мы уже
реализовали кодирование типа комнаты в нашей предыдущей работе с текстовыми полями и командами регулярных выражений. Если отставить в сторону
тональность отзывов и ценовой биннинг (цена на самом деле является нашей
целью), то остается набор идей, которые могут направить или вдохновить вас
на улучшение работы. Используйте результаты LLM в своих интересах, но
не забывайте, что ваши собственные знания, опыт, интуиция и критическое
мышление могут сыграть еще более важную роль в задаче анализа табличных
данных. LLM являются всего лишь инструментами, облегчающими ваш труд.
Рассмотрев предложенные ChatGPT признаки, приступим к созданию некоторых из них. Что касается географической кластеризации, в листинге 7.4
приведено все необходимое для создания географического признака с высокой
кардинальностью из координат. Позже, в пайплайне данных, мы преобразуем
его с помощью кодирования по целевой переменной.
316 Глава 7. Полный пример применения XGBoost
Листинг 7.4. Создание географического признака с высокой
кардинальностью
def bin_2_cat(feature, bins=32):
min_value = feature.min()
bin_size = (feature.max() - min_value) / bins
return ((feature - min_value) / bin_size).astype(int)
summary_listings['coordinates'] = (
bin_2_cat(summary_listings['latitude']) *
1000 +
bin_2_cat(summary_listings['longitude'])
)
Дискретизирует
широту и долготу
по размеру бина
Формирует новый признак координат
путем суммирования дискретизированных значений широты и долготы
print(summary_listings['coordinates'].nunique())
Этот код генерирует признак с 317 уникальными значениями. Мы охватили все
муниципалитеты Токио в сетке 32 × 32, что означает потенциально 1024 значения.
Только 317 из них содержатся в объявлениях. Таким образом, в будущем наша
модель сможет делать эффективные предсказания, только если новое объявление
попадет в один из 317 ранее определенных сегментов. В случае появления новой
локации благодаря способности TargetEncoder обрабатывать новые данные (см.
описание параметра handle_unknown на https://mng.bz/oK1p) мы можем просто
заменить неизвестные значения на среднее целевой переменной, используя настройку handle_unknown = "value".
Критически важным аспектом рынка недвижимости в Токио является наличие
культурного и исторического центра — Императорского дворца. Объекты вблизи
этого места имеют более высокую стоимость, и некоторые из самых дорогих
квартир в Японии расположены недалеко от этой достопримечательности. Попытаемся отразить эту реальность, создав признак, сравнивающий местоположение
нашего объекта Airbnb и Императорского дворца (координаты которого можно
взять с таких сайтов, как latlong.net: https://mng.bz/nRW2). Для расстояний мы
преобразуем значение в метры с помощью формулы, которая включает косинус
в радианах, умноженный на коэффициент преобразования, чтобы измерение
было понятным для человека. Мы также используем манхэттенское расстояние
для лучшего представления расстояний в городе. Оно рассчитывается как сумма
абсолютных разностей между широтами и долготами (листинг 7.5).
Листинг 7.5. Вычисление расстояния до центра города
imperial_palace_lat = 35.6841822633
imperial_palace_lon = 139.751471994
Коэффициент пересчета,
представляющий приблизительное
количество метров на градус широты
def degrees_to_meters(distance_degrees, latitude):
conversion_factor = 111000
distance_meters = (distance_degrees * conversion_factor
* np.cos(np.radians(latitude)))
return distance_meters
Рассчитывает расстояние в метрах,
distance_degrees = (
np.abs(
summary_listings['latitude'] imperial_palace_lat) +
умножая расстояние в градусах
на коэффициент пересчета
и корректируя на косинус широты
Вычисляет абсолютное расстояние
imperial_palace_lat = 35.6841822633
imperial_palace_lon = 139.751471994
Коэффициент пересчета,
представляющий приблизительное
7.1. Подготовка
def degrees_to_meters(distance_degrees,
latitude):и исследование данных 317
количество метров на градус широты
conversion_factor = 111000
distance_meters = (distance_degrees * conversion_factor
* np.cos(np.radians(latitude)))
return distance_meters
Рассчитывает расстояние в метрах,
distance_degrees = (
np.abs(
summary_listings['latitude'] imperial_palace_lat) +
np.abs(
summary_listings['longitude'] imperial_palace_lon)
)
умножая расстояние в градусах
на коэффициент пересчета
и корректируя на косинус широты
Вычисляет абсолютное расстояние
в градусах, вычитая координаты широты
и долготы Императорского дворца
из значений в датасете
summary_listings['imperial_palace_distance'] = (
degrees_to_meters(distance_degrees,
summary_listings['latitude'])
)
При работе с координатами внутри города выбор между евклидовым и манхэттенским расстоянием в качестве признака для машинного обучения зависит от
конкретного контекста и задачи, которую вы пытаетесь решить.
Евклидово расстояние основано на прямолинейном расстоянии между двумя
точками в евклидовом пространстве. Оно предполагает прямой путь между
точками и может быть более подходящим при рассмотрении физических
расстояний. Его также называют нормой L2 — это математическое понятие,
обозначающее расстояние между вектором и началом координат векторного
пространства. Поскольку норма L2 основана на формуле евклидова расстоя
ния, эти термины часто используются как взаимозаменяемые.
Манхэттенское расстояние, также известное как расстояние городских
кварталов или метрика такси, измеряет расстояние между двумя точками
путем сложения абсолютных разностей между их координатами. Оно учитывает только горизонтальные и вертикальные перемещения и игнорирует
диагональные пути. Манхэттенское расстояние называют нормой L1.
Манхэттенское расстояние более уместно при рассмотрении фактического перемещения или навигации в городе, где движение обычно происходит по улицам
и дорожным сетям, и лучше подходит для оценки перемещений и доступности
локаций. Оно соответствует концепции следования по дорогам и выполнения
поворотов под прямым углом.
После вычисления признака imperial_palace_distance мы можем проверить его
среднее значение, выраженное в метрах, с помощью следующего кода:
summary_listings.imperial_palace_distance.mean()
Результат показывает, что среднее расстояние до Императорского дворца составляет около 7.9 км.
318 Глава 7. Полный пример применения XGBoost
Далее мы можем определить объект, который находится ближе всего к Императорскому дворцу. Для этого используем функцию idxmin(), чтобы найти индекс
ближайшего объекта, а затем обратимся к соответствующим деталям:
(summary_listings[
['id', 'name', 'neighbourhood', 'imperial_palace_distance']
].iloc[np.argmin(summary_listings['imperial_palace_distance'])])
Результат получился следующим, что немного удивляет:
id
name
neighbourhood
imperial_palace_distance
Name: 10255, dtype: object
874407512426725982
Home in Shibuya City · «New · 3 bedrooms · ...
Chiyoda Ku
137.394271
Действительно, объект расположен не так уж близко к Императорскому дворцу,
что указывает на наличие потенциальных ошибок в геолокации объявлений. Подобные проблемы нередки в датасетах, независимо от того, насколько тщательно
они были подготовлены. Как обсуждалось в главе 2, после некоторых проверок
качества данных, например поиска противоречий между признаками и вероятно
неточных значений, у вас есть несколько вариантов действий, перечисленных
здесь в порядке убывания требуемых усилий.
Геокодирование (преобразование адреса в координаты) и обратное геокодирование (преобразование координат в адрес) — чтобы выяснить, соответствует
ли информация о местоположении предоставленным координатам широты
и долготы, и решить, доверять ли предоставленному адресу или координатам.
Импутация данных — замена сомнительных значений как пропущенных
с использованием координат местоположения по умолчанию.
Удаление записей — удаление всех строк, содержащих сомнительные значения.
Использование XGBoost — методы на основе деревьев, как правило, менее
чувствительны к влиянию ошибочных и сомнительных значений, поскольку
они устойчивы к выбросам и шуму в данных.
В нашем примере мы решили положиться на XGBoost, поскольку наша модель
не настолько критически важна, чтобы требовать тщательной проверки качества
данных. В вашем проекте все может быть иначе, и вы можете выбрать решение,
требующее больше усилий по очистке данных.
Расстояния от достопримечательностей и объектов инфраструктуры хорошо
работают в качестве признаков для моделирования в сфере недвижимости.
Поэтому мы не ограничиваемся вычислением расстояния до Императорского
дворца в центре Токио. В статье «Modeling User Activity Preference by Leveraging
User Spatial Temporal Characteristics in LBSNs» Динци Яна (Dingqi Yang), Дацина
7.1. Подготовка и исследование данных 319
Чжана (Daqing Zhang), Винсента В. Чжэна (Vincent W. Zheng), Чжиюна Юй
(Zhiyong Yu) (IEEE Transactions. on Systems, Man, and Cybernetics: Systems,
45(1), 129-142, 2015) авторы собрали датасеты из чекинов Foursquare NYC и Токио с их географическими координатами и типом местоположения, к которому
они относятся; доступ к данным можно получить через Kaggle Datasets (https://
mng.bz/4aDj).
Foursquare — это социальная сеть, основанная на геопозиционировании. Благодаря мобильному приложению пользователи могут находить близлежащие места
для посещения, такие как рестораны, магазины и транспортные узлы, а также
делиться информацией о посещаемых ими локациях. Еще одной особенностью
приложения являются чекины (check-in), которые фиксируются при использовании платформы в конкретном месте. Чекин отражает присутствие пользователя
в определенной локации. Когда пользователь отмечается в каком-то месте, он
делится своим местоположением с друзьями в Foursquare, а также может пуб
ликовать информацию о своем посещении в других социальных сетях, таких
как Facebook и X. Для оценки удобства расположения объекта размещения мы
можем использовать данные о часто посещаемых местах, включая аэропорты,
автобусные остановки, железнодорожные вокзалы и станции метро. Вместе
с круглосуточными магазинами (тип розничного магазина, который в основном
продает широкий выбор повседневных товаров и продуктов) близость таких
мест может добавить ценность объекту недвижимости.
Таким образом, чтобы обогатить наш датасет, мы сначала извлекли GPSкоординаты этих локаций в Токио напрямую из Kaggle. Код и обработанный
набор данных доступны по адресу https://mng.bz/QDPv, а загрузить готовый файл
в свой рабочий каталог можно со страницы https://mng.bz/XxNa, где находится
файл relevance_spots_Tokyo.csv, который содержит информацию о 3560 круглосуточных магазинах, 1878 автобусных остановках, 439 станциях метро и 264 объектах, связанных с аэропортами. Затем, используя листинг 7.6, можно сравнить
местоположение объектов из Airbnb Токио с каждым из этих мест и определить
ближайшее расстояние до каждого. Идея в том, что чем ближе объект для сдачи
в аренду к магазину и транспортной инфраструктуре, тем выше ожидаемая цена.
В листинге 7.6 мы не сравниваем каждый объект со всеми возможными местами, которые мы собрали, потому что это заняло бы слишком много времени
и вычислений. Вместо этого используем структуру данных k-мерного дерева
(KDTree) из scikit-learn — оптимизированный алгоритм, разработанный для
эффективного поиска ближайшей к заданному местоположению точки среди
многих точек. Scikit-learn применяет подобные алгоритмы (например, метод k
ближайших соседей) в ситуациях, когда нужно найти наиболее похожие примеры
для тестового образца в обучающей выборке. В нашем случае обучающая выборка — это набор мест, и алгоритм обучается находить ближайшие к заданному
местоположению места на основе манхэттенского расстояния.
320 Глава 7. Полный пример применения XGBoost
Листинг 7.6. Поиск ближайших объектов инфраструктуры и транспорта
from sklearn.neighbors import KDTree
relevant_spots = pd.read_csv("relevant_spots_Tokyo.csv")
venue_categories = ['Convenience Store', 'Train Station',
'Airport', 'Bus Station', 'Subway']
min_distances = {'listing_id': summary_listings['id']}
Сохраняет минимальные
расстояния в словаре
for venue in venue_categories:
venue_filter = relevant_spots['venueCategory'] == venue
venues = relevant_spots[
Фильтрует местоположения
['latitude', 'longitude']
релевантных объектов инфраструктуры
][venue_filter]
tree = KDTree(venues, metric='manhattan')
Создает KDTree, используя
distance, index = tree.query(
отфильтрованные местоположения
summary_listings[['latitude', 'longitude']],
объектов и манхэттенское
k=1
расстояние для быстрого поиска
)
ближайших соседей
min_distances[
'nearest_' +
Запрашивает KDTree, чтобы найти ближайшую
venue.lower().replace(" ", "_")
точку и расстояния от нее до каждого объекта
] = degrees_to_meters(
Airbnb (k = 1 возвращает ближайшую точку)
np.ravel(distance),
Словарь минимальных расстояний
summary_listings['latitude']
для каждого типа объекта
)
преобразуется в DataFrame
min_distances = pd.DataFrame(min_distances)
Код выполняется достаточно быстро: для каждого типа объектов он обучается
на местоположениях выбранных точек, находит с помощью KDTree ближайшую
локацию к каждому жилью и автоматически вычисляет расстояние. В итоге
просто нужно сохранить результаты в pandas DataFrame, предварительно преобразовав расстояния (манхэттенское расстояние в градусах) в метры с помощью
ранее рассмотренной функции degrees_to_meters. Мы можем проверить, что
получилось, изучив первые пять строк итогового датасета:
min_distances.head()
На рис. 7.5 показаны результаты.
Рис. 7.5. Первые пять строк датафрейма min_distances
7.1. Подготовка и исследование данных 321
Имея минимальное расстояние до выбранных инфраструктурных объектов для
каждого объявления нашего датасета, мы теперь приступаем к объединению
всех этих новых признаков в окончательный датасет предикторов и извлечению
целевого ряда или вектора для использования в моделировании.
7.1.4. Завершение обработки данных
После создания некоторых дополнительных признаков можем завершить формирование предикторов и целевой переменной. В листинге 7.7 мы объединяем
датасет summary_listing с данными о минимальном расстоянии до выбранных
инфраструктурных объектов (аэропортов, метро, вокзалов, автобусных остановок, круглосуточных магазинов). Затем упорядочиваем объединенные данные
относительно нашей целевой переменной — средней цены, которую мы вычислили в датасете price_stats_ordered.
Листинг 7.7. Сбор данных
summary_listings_features = [
'neighbourhood',
'coordinates',
'room_type',
'minimum_nights', 'number_of_reviews', 'days_since_last_review',
'reviews_per_month', 'calculated_host_listings_count',
'availability_365', 'number_of_reviews_ltm',
'number_of_reviews_ltm_ratio',
'number_of_bedrooms', 'number_of_beds', 'number_of_baths',
'type_of_accommodation', 'score', 'is_new',
'is_studio', 'has_shared_bath', 'has_half_bath',
'imperial_palace_distance'
]
summarized = summary_listings[['id'] +
summary_listings_features].rename({'id': 'listing_id'}, axis=1)
X = summarized.merge(min_distances, on='listing_id').set_index('listing_id')
X = X.reindex(price_stats.index)
price_stats_ordered = price_stats.reindex(X.index)
y = price_stats_ordered['mean'].copy()
Копирует столбец mean со средним значением
цены в качестве целевой переменной
Переиндексирует X для
соответствия индексу
price_stats
Переиндексирует price_stats
для соответствия индексу в X,
обеспечивая соответствие
переиндексированной статистики
цен данным в X
После того как выполнение скрипта завершилось, можем визуализировать наш
датасет:
X.head()
322 Глава 7. Полный пример применения XGBoost
В качестве индекса у нас есть listing_id, а среди столбцов находятся все признаки, которые мы подготовили для задачи (рис. 7.6).
Рис. 7.6. Верхние строки датасета предикторов
Теперь можем приступить к детальному изучению данных и выяснить, есть ли
какие-либо дополнительные проблемы, которые необходимо устранить, или
другие идеи, которые могут сыграть важную роль в разработке нашей модели
XGBoost.
7.1.5. Исследование и исправление данных
Следующим шагом после объединения всех предикторов в единый датасет
является его исследование для обнаружения проблем, таких как пропущенные
данные или экстремальные значения, которые могут повлиять на производительность ML-алгоритма. Поэтому наше первое действие — проверить наличие
пропусков с помощью следующей команды:
X.isna().sum()
Полученный список указывает на то, что есть три признака с пропущенными
данными:
days_since_last_review
reviews_per_month
score
1252
1252
2381
Как мы обсуждали в главах 2 и 6, при наличии пропущенных данных крайне
важно исследовать причину этого и определить, можно ли их считать пропущенными полностью случайно (MCAR), пропущенными случайно (MAR)
или пропущенными неслучайно (MNAR). В этом конкретном случае значения
пропущены совсем не случайно. Причина в том, что для некоторых объектов
7.1. Подготовка и исследование данных 323
отсутствуют отзывы или их недостаточно для вычисления оценки. Действительно, проверив количество объектов для сдачи в аренду без отзывов, мы убедились,
что эта цифра соответствует количеству пропусков в двух признаках:
(X.number_of_reviews==0).sum()
Как и ожидалось, результат равен 1252, что совпадает с количеством пропущенных значений. В этом случае лучше не полагаться на встроенные возможности
XGBoost и других реализаций GBDT, поскольку их поведение будет имитировать усредненную ситуацию. Отсутствие отзывов — это крайний, но возможный
случай, когда объект только появился на рынке или редко сдается в аренду.
Здесь нужно явно задать значение, которое может помочь любому алгоритму
машинного обучения понять, что отзывов нет. Распространенная стратегия —
использовать значение на границах существующего распределения, обычно
отрицательное число, если мы работаем с подсчетами или положительными
величинами. Быстрая проверка может подтвердить, подходят ли условия для
применения такой стратегии обработки пропущенных значений:
X[["days_since_last_review", "reviews_per_month", "score"]].describe()
В результате мы подтвердили, что минимальное значение всегда больше нуля
для всех трех рассматриваемых признаков. Это означает, что можно просто
заменить пропуски значением –1 (мы не можем использовать ноль, поскольку
минимальное значение равно нулю для days_since_last_review). Это решение
будет работать и для линейной модели (значение –1 находится на нижней границе распределения), и для ансамблей на основе деревьев (они просто выделят
его в отдельное правило):
X.fillna(-1, inplace=True)
На следующем шаге проверим наши числовые признаки на наличие экстремальных значений. Как обсуждалось в главе 2, простой подход к поиску выбросов
и экстремальных значений заключается в построении ящика с усами для каждого
числового признака: либо в виде одного ящика (если масштабы сопоставимы)
либо в виде панели из нескольких. В нашем случае в листинге 7.8 мы подготовили
панель из ящиков с усами для проверки на экстремальные значения.
Листинг 7.8. Построение ящиков с усами для числовых признаков
import matplotlib.pyplot as plt
numeric = ['minimum_nights', 'number_of_reviews',
'days_since_last_review', 'reviews_per_month',
'calculated_host_listings_count',
'availability_365', 'score',
'number_of_reviews_ltm',
'number_of_reviews_ltm_ratio',
'number_of_bedrooms', 'number_of_beds',
'number_of_baths', 'imperial_palace_distance',
'nearest_convenience_store',
'nearest_train_station', 'nearest_airport',
'nearest_bus_station', 'nearest_subway']
num_plots = len(numeric)
Оценивает количество строк, необходимое
import matplotlib.pyplot as plt
numeric = ['minimum_nights', 'number_of_reviews',
'days_since_last_review', 'reviews_per_month',
'calculated_host_listings_count',
324 Глава
7. Полный пример
применения XGBoost
'availability_365',
'score',
'number_of_reviews_ltm',
'number_of_reviews_ltm_ratio',
'number_of_bedrooms', 'number_of_beds',
'number_of_baths', 'imperial_palace_distance',
'nearest_convenience_store',
'nearest_train_station', 'nearest_airport',
'nearest_bus_station', 'nearest_subway']
num_plots = len(numeric)
num_rows = (num_plots + 2) // 3
num_cols = min(num_plots, 3)
fig, axes = plt.subplots(
num_rows,
num_cols,
figsize=(8, 12)
)
axes = axes.flatten()
Оценивает количество строк, необходимое
для размещения диаграмм
Рассчитывает количество столбцов,
необходимое для размещения диаграмм
Создает изображение
for i, feat in enumerate(numeric):
X[[feat]].boxplot(ax=axes[i])
Преобразует массив осей в одномерный
массив для итерации
fig.tight_layout()
plt.show()
На рис. 7.7 представлены результаты визуализации. Анализируя значения за
пределами усов диаграмм, представленных в виде пустых точек, мы сразу замечаем, что почти все распределения имеют тяжелые хвосты справа, причем значения намного больше среднего. Если для признаков, основанных на расстоянии,
такие экстремальные значения могут быть объяснимы масштабами токийского
мегаполиса, то для других, например minimum_night и number_of_reviews, они
могут являться выбросами, значительно удаленными от ядра распределения. Для
решения такой проблемы можно использовать винзоризацию (winsorizing) —
метод, который мы предложили в главе 2. Эта техника преобразования данных
заменяет крайние значения менее экстремальными, чтобы уменьшить влияние
выбросов на статистический анализ и моделирование.
В листинге 7.9, используя функцию winsorize из пакета Scipy, мы применяем
винзоризацию 0.1 % верхней части распределения признака minimum_nights.
Листинг 7.9. Винзоризация выбросов
from scipy.stats.mstats import winsorize
lower_cut_percentile = 0.00
upper_cut_percentile = 0.001
Указывает нижний процентиль, ниже
которого значения не будут изменены
X['minimum_nights'] = winsorize(X['minimum_nights'].values,
limits=(lower_cut_percentile,
upper_cut_percentile))
X[['minimum_nights']].boxplot()
Указывает верхний
процентиль, выше
которого значения
не будут измены
7.1. Подготовка и исследование данных 325
Рис. 7.7. Панель ящиков с усами, иллюстрирующих асимметричные распределения
большинства числовых характеристик
326 Глава 7. Полный пример применения XGBoost
Все значения выше 99.9-го процентиля будут заменены на его значение, что
устранит экстремальные выбросы.
На рис. 7.8 показано, что максимальное значение теперь составляет 120, а не
более 1000, как раньше.
Рис. 7.8. Ящик с усами для винзоризованного признака minimum_nights
То же самое мы повторяем и для признака number_of_reviews:
X['number_of_reviews'] = winsorize(X['number_of_reviews'].values,
limits=(lower_cut_percentile,
upper_cut_percentile))
X[['number_of_reviews']].boxplot()
На рис. 7.9 видно, что теперь признак по-прежнему имеет тяжелый правый хвост.
Однако экстремальные значения были сжаты до уровня ниже 500.
Завершив проверку и устранение пропущенных значений и выбросов в нашем
датасете предикторов, можем перейти к следующему подразделу, чтобы рассмотреть целевую переменную.
7.1. Подготовка и исследование данных 327
Рис. 7.9. Ящик с усами для винзоризованного признака number_of_reviews
7.1.6. Изучение целевой переменной
В ходе разведочного анализа данных (EDA) крайне важно изучить предикторы
и целевую переменную, а иногда и их взаимосвязи. В нашем примере, поскольку
это задача регрессии, мы начинаем с определения среднего значения и диапазона
целевой переменной:
print(f"minimum: {y.min()}")
print(f"average: {y.mean().round(2)}")
print(f"maximum: {y.max()}")
Эти команды выведут минимальное, среднее и максимальное значения целевой
переменной y:
minimum: 1450.0
average: 36573.1
maximum: 1306500.0
Мы сразу замечаем, что максимальное значение существенно отличается по
масштабу от среднего и минимального. Оценка процентилей может быть полезна
для лучшего понимания, если есть проблема с экстремальными значениями или
асимметрией распределения в целевой переменной. Наличие экстремальных
значений можно детальнее изучить, запросив диапазон процентилей, фокусирующихся на хвостах распределения:
328 Глава 7. Полный пример применения XGBoost
perc = [1, 5, 10, 25, 50, 75, 90, 95, 99]
for p in perc:
print(f"процентиль {p:2}: {np.percentile(y, p).round(2)}")
Ниже приведены выходные процентили, и у нас есть подтверждение того, что
присутствуют экстремальные значения в правой части распределения, поскольку
даже 99-й процентиль довольно далек от максимума, о котором мы говорили ранее:
процентиль
процентиль
процентиль
процентиль
процентиль
процентиль
процентиль
процентиль
процентиль
1: 3000.0
5: 5198.02
10: 7315.67
25: 11870.07
50: 19830.78
75: 37741.64
90: 83936.03
95: 84857.11
99: 304531.4
Максимальное значение не только довольно далеко от 99-го процентиля, но
также, по-видимому, есть значительный разрыв между 95-м и 99-м процентилями. Наше решение — сосредоточиться на ядре распределения, отбросив от
него 10 %: 5 % в нижней и 5 % в верхней части. Мы реализуем это с помощью
выборки по булевой переменной:
valid_samples = (y >= 5200) & (y <=84857)
Прежде чем отобрать данные, построим график полученного распределения
(листинг 7.10).
Листинг 7.10. Построение графика распределения целевой переменной
import matplotlib.pyplot as plt
import seaborn as sns
valid_y = y[valid_samples]
sns.kdeplot(valid_y, fill=True)
Выбирает только ту часть распределения целевой переменной,
которую мы рассматриваем для моделирования
Представляет медианное значение
распределения
median = np.median(valid_y)
plt.axvline(median, color='r', linestyle='--', linewidth=2, label='Медиана')
plt.xlabel('Значения')
plt.ylabel('Плотность')
plt.title('Кривая плотности вероятности и медиана распределения')
plt.legend()
plt.show()
Предыдущий фрагмент кода выведет график плотности, показывающий распределение и медианное значение для выборки целевой переменной на основе
булевой переменной, которую мы только что определили. На рис. 7.10 показан
полученный график.
7.2. Создание и оптимизация модели 329
Кривая плотности вероятности и медиана распределения
Плотность вероятности
Медиана
Значения
Рис. 7.10. Плотность распределения целевой переменной
Полученное распределение, показанное на рис. 7.10, явно смещено вправо, что
также называется положительным смещением. В частности, можно наблюдать
скопление данных в начале и длинный убывающий хвост справа, хотя в конце
заметна еще одна небольшая группа данных, замыкающая распределение, —
вероятно, отдельный кластер высококлассных вариантов размещения. Однако
диапазон и распределение целевой переменной теперь приемлемы. Поэтому
мы отберем и целевую переменную, и данные, используя ранее определенную
булевую переменную:
X = X[valid_samples]
y = y[valid_samples]
В следующем разделе определим как аспекты процесса валидации, так и пайп
лайн данных, необходимых для моделирования. После этого опробуем базовую
модель с использованием классических линейных моделей машинного обучения,
а также проведем первичную оптимизацию модели XGBoost перед обучением
итоговой модели для задачи анализа на датасете Airbnb Токио.
7.2. Создание и оптимизация модели
В этом разделе мы будем использовать подготовленные данные для построения
модели. Прежде чем получить полную финальную модель, мы решим ряд задач,
330 Глава 7. Полный пример применения XGBoost
связанных с определением стратегии кросс-валидации, подготовкой пайплайна
данных и построением сначала базовой, а затем предварительной первой модели
XGBoost.
7.2.1. Подготовка стратегии кросс-валидации
В целом, стратегия k-блочной кросс-валидации работает довольно хорошо
в большинстве случаев, но в нашей ситуации мы имеем дело с объектами недвижимости, стоимость которых сильно зависит от их местоположения. В нашем
случае более подходящей является стратифицированная K-фолдовая кроссвалидация, которая позволяет учитывать фактор локации. Хотя она похожа на
обычную кросс-валидацию, ключевое различие заключается в том, что в каждом
фолде сохраняется распределение выбранного признака (в нашем случае — территориального деления). Такая стратифицированная выборка позволит фолдам
иметь схожий набор территорий, как и в полном датасете. Однако важно заранее
проверить, нет ли зон, которые сложно разделить по фолдам из-за их малой
представленности. Если мы посчитаем количество уникальных районов, отраженных в данных, то получим длинный список, где некоторые локации имеют
значительное число объявлений, а другие — всего несколько:
X['neighbourhood'].value_counts()
Очевидно, что нельзя учитывать все районы, имеющие меньше определенного
количества примеров, так как при разделении данных на фолды они будут представлены недостаточно. Поскольку районы распределены в пространстве, просто собрать их в дополнительный класс не получится, потому что это смешает
очень разные ситуации из удаленных друг от друга локаций. В листинге 7.11 мы
решаем эту проблему, агрегируя районы с менее чем 30 примерами (что подразумевает около шести примеров на фолд при пятифолдовой кросс-валидации)
с их соседним районом с бˆольшим числом примеров. Чтобы добиться этого,
снова используем структуру данных KDTree. Это позволяет сопоставить каждый
район с малым числом сдаваемых объектов (менее 30) с ближайшим районом,
имеющим более 30 примеров.
Листинг 7.11. Объединение близлежащих районов
neighbourhoods = (
summary_listings[
['neighbourhood', 'latitude', 'longitude']
]
.groupby('neighbourhood')
.agg({'latitude': 'mean',
'longitude': 'mean',
Рассчитывает среднюю широту, среднюю долготу
'neighbourhood': 'count'})
и количество объектов в каждом районе
)
less_than_30 = (
neighbourhoods[neighbourhoods['neighbourhood'] < 30]
)
more_than_30 = (
neighbourhoods[neighbourhoods['neighbourhood'] > 30]
)
Разделяет районы на две
группы по количеству
объектов
)
summary_listings[
['neighbourhood', 'latitude', 'longitude']
]
.groupby('neighbourhood')
.agg({'latitude': 'mean',
'longitude': 'mean',
оптимизация
модели
331
Рассчитываетисреднюю
широту, среднюю
долготу
'neighbourhood': 'count'}) 7.2. Создание
и количество объектов в каждом районе
less_than_30 = (
neighbourhoods[neighbourhoods['neighbourhood'] < 30]
)
more_than_30 = (
neighbourhoods[neighbourhoods['neighbourhood'] > 30]
)
kdtree = KDTree(
more_than_30[['latitude', 'longitude']]
)
change_list = {}
Разделяет районы на две
группы по количеству
объектов
Создает KDTree, используя средние значения
широты и долготы районов с количеством
объектов более 30
Инициализирует пустой
for i in range(len(less_than_30)):
словарь для хранения
row = less_than_30.iloc[[i]]
сопоставлений районов
_, idx = kdtree.query(
row[['latitude', 'longitude']]
)
change_list[row.index[0]] = more_than_30.index[idx[0, 0]]
X["neighbourhood_more_than_30"] = (
X["neighbourhood"].replace(change_list)
)
Заменяет исходные
значения районов на новые
значения в соответствии
с сопоставлением в change_list
Итерируется по каждому району
с количеством примеров менее 30
и запрашивает KDTree для поиска
ближайшего района с количеством
примеров более 30
После запуска кода можно проверить, как было выполнено сопоставление и есть
ли в полученной агрегации соседние области с менее чем 30 списками. Для этого
запустите следующие команды:
print(change_list)
print(X["neighbourhood_more_than_30"].value_counts())
Разработав подходящее разделение на районы, мы теперь можем перейти к определению нашей стратегии стратифицированной кросс-валидации в следующем
листинге.
Листинг 7.12. Определение стратифицированной стратегии
from sklearn.model_selection import StratifiedKFold
cv = StratifiedKFold(5, shuffle=True, random_state=0)
cv_splits = cv.split(
X, y=X["neighbourhood_more_than_30"]
)
Определяет пятифолдовое
стратифицированное
случайное разбиение
Генерирует разбиения для кросс-валидации, сохраняя
при этом одинаковое распределение районов с более чем
30 объявлениями в каждом фолде
332 Глава 7. Полный пример применения XGBoost
Полученный cv_splits является генератором, и можно проверить его с помощью
следующей команды:
print(cv_splits)
Выходными данными является тип объекта:
<generator object _BaseKFold.split at 0x78356223c660>
Поскольку cv_splits — это генератор, его можно использовать один раз, но
можно воссоздать идентичный генератор, повторно выполнив команды в листинге 7.12. В следующем подразделе мы займемся пайплайном данных и определим,
какие преобразования следует применить к нашим данным.
7.2.2. Подготовка пайплайна
Вторым подготовительным шагом является определение пайплайна для преобразования предикторов наиболее подходящим способом, который будет совместим со всеми классическими алгоритмами машинного обучения, а не только
с градиентным бустингом. В идеале было бы лучше иметь несколько пайплайнов
в зависимости от того, как каждая модель обрабатывает различные признаки.
Например, в нашем пайплайне мы будем использовать порядковое кодирование
для нескольких категориальных признаков. Оно подходит для древовидных
моделей, но не всегда корректно работает с линейными. Однако создание уникальных пайплайнов может не только повысить производительность, но и стать
довольно сложным для сопровождения. Трудозатраты на создание и управление
множеством пайплайнов могут перевесить незначительный прирост производительности, достигаемый за счет настройки каждого конвейера под конкретные
модели. Поэтому решение об использовании нескольких пайплайнов следует
принимать только при наличии веских доказательств их необходимости.
Начнем с классификации различных типов признаков, которые мы будем использовать, на категориальные, числовые и бинарные:
categorical = [
'room_type',
'neighbourhood_more_than_30',
'type_of_accommodation',
'coordinates'
]
numeric = [
'minimum_nights',
'number_of_reviews',
'days_since_last_review',
'reviews_per_month',
'calculated_host_listings_count',
'availability_365',
7.2. Создание и оптимизация модели 333
'score',
'number_of_reviews_ltm',
'number_of_reviews_ltm_ratio',
'number_of_bedrooms',
'number_of_beds',
'number_of_baths',
'imperial_palace_distance',
'nearest_convenience_store',
'nearest_train_station',
'nearest_airport',
'nearest_bus_station',
'nearest_subway'
]
binary = [
'is_new',
'is_studio',
'has_shared_bath',
'has_half_bath'
]
Дальнейшая проверка категориальных признаков необходима, поскольку нам
нужно знать, следует ли рассматривать их как признаки высокой кардинальности
или нет. Мы можем лучше понять, что делать, подсчитав, сколько уникальных
значений имеет каждый категориальный признак:
for feat in categorical:
print(f"{feat} имеет {X[feat].nunique()} уникальных значения/й")
Из результатов мы можем определить, что, вероятно, единственный признак,
который можно считать категориальным с высокой кардинальностью, — это признак coordinates, который имеет почти 300 уникальных значений. Что касается
neighborhood_more_than_30 и type_of_accomodation, мы могли бы применить
к ним порядковое кодирование для моделирования на основе дерева, тогда как
для линейных моделей было бы лучше применить к этим признакам one-hot
(таким образом, создавая около 50 новых бинарных признаков) или кодирование
по целевой переменной:
room_type имеет 4 уникальных значения/й
neighbourhood_more_than_30 имеет 24 уникальных значения/й
type_of_accommodation имеет 29 уникальных значения/й
coordinates имеет 296 уникальных значения/й
Поскольку наш пример сосредоточен на XGBoost и демонстрации его работы
с похожими задачами, мы решили применить прямое кодирование только
к room_type, порядковое к neighborhood_more_than_30 и type_of_accomodation,
а также кодирование по целевой переменной к coordinates:
onehot_encoding = ['room_type']
ordinal_encoding = ['neighbourhood_more_than_30', 'type_of_accommodation']
target_encoding = ['coordinates']
334 Глава 7. Полный пример применения XGBoost
Наш выбор XGBoost, модели на основе дерева, также оправдывает решение
оставить все числовые признаки без изменений. При использовании линейных
моделей стандартной практикой является статистическая стандартизация для
лучшей сходимости при использовании регуляризации или обобщенных линейных моделей и преобразование признаков для лучшего учета нелинейностей.
В листинге 7.13 мы определяем все необходимые преобразования признаков
и объединяем их в преобразователь столбцов Column Transformer из scikit-learn,
который станет частью пайплайна, включающего также выбранную модель машинного обучения. Кроме того, важно отметить, что мы предпринимаем шаги
по определению преобразователей столбцов для обработки неизвестных категорий и пропущенных значений, которые могут неожиданно появиться во время
тестирования. Наша стратегия one-hot-кодирования заключается в игнорировании новых неизвестных категорий. Для порядкового кодирования значение,
назначенное параметру unknown_value, которое по умолчанию равно np.nan,
будет использоваться для кодирования неизвестных категорий. Это означает, что
модель XGBoost будет обрабатывать пропущенные случаи, используя наиболее
частое разбиение. Другие алгоритмы машинного обучения могут работать иначе
в такой ситуации, что является преимуществом XGBoost. Для TargetEncoder
среднее значение целевой переменной подставляется для неизвестных категорий.
Не забудьте установить пакет category_encoders. Если он недоступен в вашей
системе, используйте команду pip install category_encoders.
Листинг 7.13. Определение преобразований столбцов
Создает объект OneHotEncoder с обработкой неизвестных
категорий путем их игнорирования во время кодирования
Создает объект OrdinalEncoder с обработкой неизвестных
категорий и заменой неизвестного значения на np.nan
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from category_encoders.target_encoder import TargetEncoder
onehot_encoder = OneHotEncoder(handle_unknown='ignore')
ordinal_enconder = OrdinalEncoder(handle_unknown="use_encoded_value",
unknown_value=np.nan)
target_encoder = TargetEncoder(
cols=target_encoding,
Создает объект TargetEncoder, обрабатывающий неизвестные
handle_unknown="value",
значения путем их кодирования с использованием среднего
smoothing=0.5
значения целевой переменной и применения сглаживания
)
с параметром 0.5
column_transform = ColumnTransformer(
[('onehot_encoding', onehot_encoder, onehot_encoding),
('ordinal_encoding', ordinal_enconder, ordinal_encoding),
('target_encoding', target_encoder, target_encoding),
('numeric', 'passthrough', numeric),
('binary', 'passthrough', binary)],
Объект преобразования столбцов
remainder='drop',
ColumnTransformer, применяющий
verbose_feature_names_out=True,
указанные кодировщики
sparse_threshold=0.0)
Гарантирует, что преобразованные
к соответствующим столбцам
)
cols=target_encoding,
handle_unknown="value",
smoothing=0.5
Создает объект TargetEncoder, обрабатывающий неизвестные
значения путем их кодирования с использованием среднего
значения целевой переменной и применения сглаживания
с параметром 0.5
column_transform = ColumnTransformer(
7.2. Создание
и оптимизация модели 335
[('onehot_encoding', onehot_encoder,
onehot_encoding),
('ordinal_encoding', ordinal_enconder, ordinal_encoding),
('target_encoding', target_encoder, target_encoding),
('numeric', 'passthrough', numeric),
('binary', 'passthrough', binary)],
Объект преобразования столбцов
remainder='drop',
ColumnTransformer, применяющий
verbose_feature_names_out=True,
указанные кодировщики
sparse_threshold=0.0)
Гарантирует, что преобразованные
данные хранятся в виде плотных
массивов
к соответствующим столбцам
Удаляет оставшиеся столбцы,
не указанные в преобразователе
Сохраняет полные названия признаков
для преобразованных столбцов
После запуска кода можем сразу же протестировать преобразование имеющихся
у нас данных и проверить названия преобразованных столбцов:
Xt = column_transform.fit_transform(X, y)
column_transform.get_feature_names_out()
Выходные данные показывают, что теперь перед признаками стоит префикс,
указывающий на преобразование, которое они претерпели. Бинарные признаки,
созданные с помощью прямого кодирования, также сопровождаются категорией,
которую они представляют:
array(['onehot_encoding__room_type_Entire home/apt',
'onehot_encoding__room_type_Hotel room',
'onehot_encoding__room_type_Private room',
'onehot_encoding__room_type_Shared room',
'ordinal_encoding__neighbourhood_more_than_30',
'ordinal_encoding__type_of_accommodation',
'target_encoding__coordinates', 'numeric__minimum_nights',
'numeric__number_of_reviews', 'numeric__days_since_last_review',
'numeric__reviews_per_month',
'numeric__calculated_host_listings_count',
'numeric__availability_365', 'numeric__score',
'numeric__number_of_reviews_ltm',
'numeric__number_of_reviews_ltm_ratio',
'numeric__number_of_bedrooms', 'numeric__number_of_beds',
'numeric__number_of_baths', 'numeric__imperial_palace_distance',
'numeric__nearest_convenience_store',
'numeric__nearest_train_station', 'numeric__nearest_airport',
'numeric__nearest_bus_station', 'numeric__nearest_subway',
'binary__is_new', 'binary__is_studio', 'binary__has_shared_bath',
'binary__has_half_bath'], dtype=object)
В качестве последнего шага просто сохраняем в одном CSV-файле как обработанные признаки, так и целевую переменную. Мы снова воспользуемся этими
данными позже, в главе 12, когда будем тестировать решение для глубокого
обучения и сравнивать его производительность с моделью XGBoost, которую
обучаем в этой главе.
data = pd.DataFrame(
Xt,
columns=column_transform.get_feature_names_out(),
336 Глава 7. Полный пример применения XGBoost
index=y.index
)
data = data.assign(target=y).reset_index()
data.to_csv("airbnb_tokyo.csv", index=False)
Теперь, когда у нас есть часть пайплайна, отвечающая за обработку данных,
можем перейти к определению базовой модели, а затем наконец регрессора
XGBoost.
7.2.3. Построение базовой модели
Наличие базовой модели в машинном обучении важно по нескольким причинам.
Сравнение производительности — базовая модель служит эталоном для
оценки производительности более сложных вариантов, помогая понять,
действительно ли бˆольшая сложность добавляет ценность.
Обнаружение переобучения — сравнивая производительность сложной
модели с базовой на новых данных, вы можете выявить переобучение, если
базовая модель будет работать намного лучше.
Понимание задачи — создание простой базовой модели, особенно если она
линейная, позволяет лучше понять данные и задачу.
Отладка и валидация — базовая модель помогает проверить правильность
пайплайна предварительной обработки данных, поскольку влияние переменных на модель не будет скрыто ее сложностью.
Создание минимально жизнеспособной модели (MVP) — базовая модель представляет минимально жизнеспособное решение рассматриваемой задачи.
По всем этим причинам мы не будем сразу же переходить к обучению модели
с использованием градиентного бустинга, который, как мы ожидаем, покажет
хорошие результаты, а сделаем шаг назад и протестируем простую линейную
модель. Кроме того, попытаемся получить прогнозы от типа модели, который мы
можем легко оценить. Вместо того чтобы просто оценивать метрики с помощью
кросс-валидации, мы применим прогнозирование на основе кросс-валидации.
Этот метод обеспечивает несмещенные прогнозы для всех обучающих случаев
путем предсказаний на валидационных фолдах.
Во время кросс-валидации метрики оценки рассчитываются отдельно для
каждого фолда и отражают производительность модели на каждом из них по
отдельности. Окончательная метрика оценки, полученная на кросс-валидации,
обычно представляет собой среднее значение (среднее арифметическое или
медиану) индивидуальных метрик фолдов. Эта агрегированная метрика дает
оценку обобщающей способности модели на новых данных. Однако, если мы
используем прогнозы кросс-валидации, мы концентрируемся на способности
7.2. Создание и оптимизация модели 337
модели работать на имеющихся данных. Фактически основное назначение прогнозов кросс-валидации заключается в анализе предсказаний, сделанных моделью для различных частей данных, используемых в качестве валидационных.
Использование таких прогнозов помогает понять, насколько хорошо модель
работает на различных подмножествах данных, и определить, не происходит ли
переобучение или недообучение модели, поскольку мы можем сравнить прогнозы
с ожидаемыми значениями целевой переменной (листинг 7.14).
Листинг 7.14. Базовая модель линейной регрессии с диагностическими
диаграммами
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
lm = LinearRegression(fit_intercept=False)
lm.fit(Xt, y)
Обучает модель линейной регрессии на
преобразованных обучающих данных
Инициализирует модель
линейной регрессии без
свободного члена
cv_splits = cv.split(
X,
y = X["neighbourhood_more_than_30"]
)
Создает стратифицированные разбиения для кросс-валидации
y_pred_cv = cross_val_predict(
на основе районов с более чем 30 наблюдениями
lm, Xt, y, cv=cv_splits
)
Выполняет кросс-валидацию
prediction_range = y_pred_cv.min()} - {y_pred_cv.max()
прогнозов
print(f"диапазон прогнозов: {prediction_range}")
r2 = r2_score(y, y_pred_cv)
rmse = np.sqrt(mean_squared_error(y, y_pred_cv))
mae = mean_absolute_error(y, y_pred_cv)
print(f'R-квадрат: {r2:.3f}')
print(f'RMSE: {rmse:.3f}')
print(f'MAE: {mae:.3f}')
Выводит диапазон прогнозов,
полученных в ходе кросс-валидации
Рассчитывает метрики оценки R-квадрат,
среднеквадратичную ошибку и среднюю
абсолютную ошибку для оценки
производительности модели
Строит диаграмму рассеяния фактических
и предсказанных значений
plt.scatter(y, y_pred_cv)
plt.plot([y.min(), y.max()], [y.min(), y.max()], 'r--', label='Идеальные результаты')
plt.axhline(
0, color='orange', linestyle='--', label='Нулевая линия'
)
Проводит на графике пунктирную
plt.xlabel('Реальные данные')
оранжевую нулевую линию
plt.ylabel('Предсказанные данные')
в качестве точки отсчета для
plt.title('Линейная регрессия. Результаты обучения') идеального обучения
plt.legend()
plt.show()
После выполнения кода получаем результаты оценки и сразу можем заметить,
что некоторые прогнозы отрицательные. Поскольку модель линейной регрессии
338 Глава 7. Полный пример применения XGBoost
не ограничена в своих предсказаниях, средняя абсолютная ошибка (MAE) довольно высока (более 12 000 иен), а R-квадрат, типичная мера соответствия,
измеряющая, какая часть дисперсии целевой переменной объясняется моделью,
составляет всего лишь скромные 0.32:
диапазон прогнозов: -34929.50241836217 - 136479.60736257263
R-квадрат: 0.320
RMSE: 17197.323
MAE: 12568.371
Очевидно, что точность модели не особенно впечатляет. Это подтверждает
рис. 7.11, где представлена диаграмма рассеяния предсказаний, полученных
в ходе кросс-валидации, по оси y против ожидаемых значений целевой переменной по оси x. Помимо нескольких отрицательных значений в начале распределения целевой переменной, прогнозы также отклоняются от пунктирной
линии идеального обучения, демонстрируя плоский тренд — явный признак
недообучения, — а также наличие нескольких выбросов.
Линейная регрессия. Результаты обучения
Предсказанные данные
Идеальные
результаты
Нулевой уровень
Реальные данные
Рис. 7.11. График результатов обучения базовой модели линейной регрессии
в сравнении с их идеальными значениями
В качестве первого шага при анализе результатов мы запрашиваем процент прогнозов, которые меньше или равны нулю — недопустимое значение, поскольку
цены должны быть положительным числом:
(y_pred_cv <= 0).sum() / len(y_pred_cv)
7.2. Создание и оптимизация модели 339
Результат — минимальный процент, около 0.5 %:
0.005178767055074196
В идеале наши прогнозы должны быть больше нуля, и в линейной модели этого можно достичь с помощью преобразования целевой переменной, например
логарифмического преобразования. Однако роль базовой модели заключается
не в том, чтобы быть идеальной, а в том, чтобы высвечивать проблемы в данных
и служить полезным ориентиром для сравнения с более сложными алгоритмами.
Теперь приступим к поиску строк, которые являются положительными выбросами:
print(np.where(y_pred_cv > 100_000))
Мы получили два случая, 5509 и 8307:
(array([5509, 8307]),)
Также запрашиваем информацию об отрицательных выбросах:
print(np.where(y_pred_cv < -25_000))
Здесь получаем один случай, 182:
(массив([182]),)
В листинге 7.15 определяем функцию, которая поможет проверить наличие выбросов. Для каждого признака-предиктора эта функция выводит коэффициент
и результат его умножения на значение признака для данного случая, тем самым
явно показывая вклад каждого признака в прогноз.
Листинг 7.15. Проверка коэффициентов
def report_case(model, data, feature_names, case_no):
case_values = data[case_no]
coef_values = case_values * model.coef_
for feature_name, value, coef_value in zip(
feature_names, case_values, coef_values):
print(f"{feature_name:50s}" +
f"({value:10.2f}) : " +
f"{coef_value:+0.2f}")
print("-" * 80)
print(" "*66 + f"{np.sum(coef_values):+0.2f}")
Выводит сумму вычисленных
значений коэффициентов
для наблюдения
Извлекает значения
признаков для указанного
номера наблюдения
из массива данных
Вычисляет значения
коэффициентов для каждого
признака, умножая его
значение на соответствующий
коэффициент из модели
Выполняет цикл
и выводит имена
признаков, их значения
и соответствующие
значения коэффициентов
340 Глава 7. Полный пример применения XGBoost
Подготовив нашу функцию проверки, можем приступить к изучению записи
8307, которая представляет собой пример слишком большого выброса в прогнозах:
report_case(model=lm,
data=Xt,
feature_names=column_transform.get_feature_names_out(),
case_no=8307)
Ниже приведены результаты для записи 8307, где явно видно, что дополнительный вклад, который сделал прогноз выбросом, обусловлен количеством спален
(что намекает на то, что эта недвижимость, вероятно, является хостелом). Это
большое значение подняло вверх итоговый прогноз стоимости:
onehot_encoding__room_type_Entire home/apt
(
1.00) : -8295.89
onehot_encoding__room_type_Hotel room
(
0.00) : -0.00
onehot_encoding__room_type_Private room
(
0.00) : -0.00
onehot_encoding__room_type_Shared room
(
0.00) : -0.00
ordinal_encoding__neighbourhood_more_than_30
(
12.00) : +576.48
ordinal_encoding__type_of_accommodation
(
20.00) : +2377.99
target_encoding__coordinates
( 29649.71) : +26556.25
numeric__minimum_nights
(
1.00) : -268.05
numeric__number_of_reviews
(
0.00) : -0.00
numeric__days_since_last_review
(
-1.00) : -0.66
numeric__reviews_per_month
(
-1.00) : -172.50
numeric__calculated_host_listings_count
(
15.00) : +1470.92
numeric__availability_365
(
354.00) : +16503.11
numeric__score
(
-1.00) : +524.08
numeric__number_of_reviews_ltm
(
0.00) : -0.00
numeric__number_of_reviews_ltm_ratio
(
0.00) : +0.00
numeric__number_of_bedrooms
(
18.00) : +64407.67
numeric__number_of_beds
(
18.00) : +31283.70
numeric__number_of_baths
(
2.00) : -1787.41
numeric__imperial_palace_distance
(
2279.80) : -859.33
numeric__nearest_convenience_store
(
149.84) : +549.07
numeric__nearest_train_station
(
545.08) : -1043.20
numeric__nearest_airport
(
389.85) : -137.44
numeric__nearest_bus_station
(
322.04) : -266.55
numeric__nearest_subway
(
221.93) : -17.29
binary__is_new
(
0.00) : -0.00
binary__is_studio
(
0.00) : +0.00
binary__has_shared_bath
(
0.00) : -0.00
binary__has_half_bath
(
0.00) : -0.00
---------------------------------------------------------------------------+131400.95
Подобные проблемы возникают из-за того, что каждый признак моделируется
линейно. Таким образом, вклад признака в прогноз не ограничен, не имеет
максимума или минимума, а уменьшается и увеличивается в соответствии
со значением признака. Обычно введение нелинейностей и взаимодействий
7.2. Создание и оптимизация модели 341
в модель смягчает такие проблемы. Давайте теперь проверим единственный
отрицательный выброс:
report_case(model=lm,
data=Xt,
feature_names=column_transform.get_feature_names_out(),
case_no=182)
Здесь проблема заключается в значении минимального количества ночей,
которое снова слишком велико и снижает расчетную стоимость. Фактически
некоторые объекты недвижимости функционируют как сезонное жилье, обычно
для рабочих или студентов, а не только для краткосрочного пребывания. Модель
действительно слишком проста, чтобы уловить такие нюансы, и снова — введение
нелинейностей и взаимодействий могло бы помочь:
onehot_encoding__room_type_Entire home/apt
(
0.00) : -0.00
onehot_encoding__room_type_Hotel room
(
0.00) : -0.00
onehot_encoding__room_type_Private room
(
1.00) : -11573.69
onehot_encoding__room_type_Shared room
(
0.00) : -0.00
ordinal_encoding__neighbourhood_more_than_30
(
6.00) : +288.24
ordinal_encoding__type_of_accommodation
(
14.00) : +1664.59
target_encoding__coordinates
( 27178.66) : +24343.02
numeric__minimum_nights
(
120.00) : -32166.38
numeric__number_of_reviews
(
122.00) : -1241.88
numeric__days_since_last_review
(
132.00) : +87.20
numeric__reviews_per_month
(
1.33) : +229.43
numeric__calculated_host_listings_count
(
4.00) : +392.25
numeric__availability_365
(
0.00) : +0.00
numeric__score
(
4.98) : -2609.92
numeric__number_of_reviews_ltm
(
4.00) : -18.33
numeric__number_of_reviews_ltm_ratio
(
0.03) : +54.17
numeric__number_of_bedrooms
(
1.00) : +3578.20
numeric__number_of_beds
(
0.00) : +0.00
numeric__number_of_baths
(
0.00) : -0.00
numeric__imperial_palace_distance
( 32506.70) : -12252.79
numeric__nearest_convenience_store
(
5020.81) : +18397.51
numeric__nearest_train_station
(
5689.32) : -10888.48
numeric__nearest_airport
( 11438.81) : -4032.76
numeric__nearest_bus_station
(
4999.17) : -4137.76
numeric__nearest_subway
( 16976.69) : -1322.52
binary__is_new
(
0.00) : -0.00
binary__is_studio
(
0.00) : +0.00
binary__has_shared_bath
(
0.00) : -0.00
binary__has_half_bath
(
0.00) : -0.00
----------------------------------------------------------------------------31209.90
Итак, наша базовая модель показала, что для успешного решения задачи, представленной датасетом Airbnb Токио, требуется более подходящая модель, которая
может обеспечивать положительные прогнозы (они должны быть обязательно
342 Глава 7. Полный пример применения XGBoost
положительными), а также представлять нелинейные зависимости и взаимодействия между определенными характеристиками жилья (большое количество
спален указывает на хостел, большое минимальное число дней проживания
указывает на размещение для сезонных арендаторов). В следующем подразделе
мы решим все эти проблемы разом с помощью модели XGBoost, которая должна
справиться с нашими данными более сложным и интеллектуальным способом.
7.2.4. Создание первой предварительной модели
Сначала мы выбрали регрессор XGBoost, пытаясь учесть некоторые идеи, возникшие из предыдущих проверок EDA и базовой модели. Мы решили использовать целевую функцию гамма, обычно применяемую в задачах регрессии для
моделирования положительных непрерывных переменных с правосторонним
смещением. Гамма-функция особенно полезна, когда целевая переменная всегда
положительная, содержит много малых значений и несколько больших, поскольку она хорошо обрабатывает такие характеристики распределения.
Кроме того, поскольку наша базовая модель показала признаки недообучения
и некорректной обработки взаимодействий и нелинейностей, мы устанавливаем
максимальную глубину не более шести для деревьев решений, составляющих
ансамбль бустинга. Это позволяет обеспечить достаточное количество разбиений
для обработки наиболее распространенных характеристик подобных данных.
В листинге 7.16, организованном аналогично предыдущему листингу обучения
базовой линейной регрессии, мы обучаем регрессор XGBoost и тестируем его
прогнозы на валидационных фолдах (out-of-fold) кросс-валидации.
Листинг 7.16. Первая модель XGBoost
from sklearn.model_selection import cross_validate1
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
from XGBoost import XGBRegressor
xgb = XGBRegressor(booster='gbtree',
objective='reg:gamma',
n_estimators=300,
max_depth=6)
Настраивает регрессор XGBoost
с заданными гиперпараметрами
Определяет reg:gamma
в качестве целевой функции
Генерирует разбиения для кроссвалидации на основе признака
neighborhood_more_than_30
cv_splits = cv.split(
X, y=X["neighbourhood_more_than_30"]
)
y_pred_cv = cross_val_predict(
xgb, Xt, y, cv=cv_splits
)
prediction_range = y_pred_cv.min()} - {y_pred_cv.max()
print(f"диапазон прогнозов: {prediction_range}")
1
Выполняет прогнозирование
на кросс-валидации
Выводит диапазон
Вероятно, здесь у автора ошибка. Далее используется cross_val_predict.
– Примеч. науч.
r2ред.
= r2_score(y, y_pred_cv)
rmse = np.sqrt(mean_squared_error(y, y_pred_cv))
mae = mean_absolute_error(y, y_pred_cv)
print(f'R-квадрат: {r2:.3f}')
print(f'RMSE: {rmse:.3f}')
print(f'MAE: {mae:.3f}')
предсказанных значений
Рассчитывает метрики оценки R-квадрат,
среднеквадратичную ошибку и MAE
для оценки производительности модели
Создает диаграмму рассеяния
n_estimators=300,
max_depth=6)
Определяет reg:gamma
в качестве целевой функции
Генерирует разбиения для кроссвалидации на основе признака
neighborhood_more_than_30
cv_splits = cv.split(
X, y=X["neighbourhood_more_than_30"]
)
7.2. Создание и оптимизация модели 343
y_pred_cv = cross_val_predict(
Выполняет прогнозирование
xgb, Xt, y, cv=cv_splits
на кросс-валидации
)
prediction_range = y_pred_cv.min()} - {y_pred_cv.max()
print(f"диапазон прогнозов: {prediction_range}")
Выводит диапазон
предсказанных значений
r2 = r2_score(y, y_pred_cv)
rmse = np.sqrt(mean_squared_error(y, y_pred_cv))
mae = mean_absolute_error(y, y_pred_cv)
print(f'R-квадрат: {r2:.3f}')
print(f'RMSE: {rmse:.3f}')
print(f'MAE: {mae:.3f}')
Рассчитывает метрики оценки R-квадрат,
среднеквадратичную ошибку и MAE
для оценки производительности модели
Создает диаграмму рассеяния
фактических и предсказанных значений
plt.scatter(y, y_pred_cv)
plt.plot([y.min(), y.max()], [y.min(), y.max()], 'r--', label='Идеальные
результаты')
plt.axhline(0, color='orange', linestyle='--', label='Нулевой уровень')
plt.xlabel('Реальные данные')
Добавляет на график опорные
plt.ylabel('Предсказанные данные')
линии идеального обучения
plt.title('XGBoost. Результаты обучения')
и нулевую
plt.legend()
plt.show()
На этот раз диапазон прогнозирования строго положительный, как мы и ожидали. MAE почти в два раза меньше, чем у базовой линейной модели, а R-квадрат
почти 0.7, что является достаточно хорошим результатом, показывающим, что
модель теперь способна охватить бˆольшую часть дисперсии целевой переменной:
диапазон прогнозов: 3291.401123046875 - 123069.8828125
R-квадрат: 0.693
RMSE: 11562.836
MAE: 7227.435
Дальнейший анализ результатов, представленных на рис. 7.12 в виде диаграммы
рассеяния между прогнозами кросс-валидации (по оси y) и ожидаемыми целевыми значениями (по оси x), показывает, что точки теперь расположены немного
ближе к линии идеальных результатов. Кроме того, важно отметить, как модель
XGBoost склонна экстраполировать прогнозы: столбец прогнозов в правой части
диаграммы указывает на то, что наша модель иногда предсказывает значения
выше наблюдаемого максимума в целевой переменной, в то время как в самой
левой части диаграммы нет неположительных оценок.
Обычно в проекте по анализу данных не следует довольствоваться одной моделью, как в нашем примере. Из-за ограничений по объему книги мы просто
сосредоточились на модели XGBoost. В рабочем проекте желательно попробовать разные классические алгоритмы машинного обучения, например другие
реализации градиентного бустинга, ансамблей деревьев, обобщенных линейных
моделей и даже более редкие в наше время классические модели машинного
обучения (такие как k ближайших соседей или метод опорных векторов). В машинном обучении нет «бесплатных обедов», и вы можете найти подходящие
344 Глава 7. Полный пример применения XGBoost
решения своей проблемы с помощью других алгоритмов, которые в большей
степени соответствуют вашим потребностям с точки зрения производительности,
скорости вывода, использования памяти и переносимости на другие системы.
XGBoost. Результаты обучения
Предсказанные данные
Идеальные
результаты
Нулевой уровень
Реальные данные
Рис. 7.12. График результатов обучения модели XGBoost в сравнении с их идеальным
значением
В следующем подразделе оптимизируем решение XGBoost с помощью байесовской оптимизации, чтобы добиться наилучшего результата при решении
нашей задачи.
7.2.5. Оптимизация модели
Поскольку XGBoost хорошо подходит для этой задачи, мы потратим некоторое
время на тонкую настройку параметров и тестирование различных подходов
бустинга и целевых функций. Мы будем использовать Optuna — байесовский
оптимизатор, представленный в предыдущей главе, поскольку он может эффективно исследовать пространство поиска гиперпараметров GBDT, адаптивно выбирая следующий их набор за небольшое количество раундов на основе
результатов предыдущих экспериментов.
Если в вашей системе нет Optuna, вы можете установить его, выполнив команду
pip install optuna в командной строке или ячейке Colab.
7.2. Создание и оптимизация модели 345
В листинге 7.17 выполняется оптимизация гиперпараметров с помощью Optuna для
ранее протестированной модели XGBoost Regressor с целью поиска лучших гиперпараметров, которые минимизируют MAE модели на датасете Airbnb Токио. Основой
листинга является целевая функция, которая предлагает Optuna различные значения гиперпараметров с использованием методов trial.suggest_... В частности,
тестируется классический бустер gbtree (градиентный бустинг), а также gblinear,
который использует линейную модель в качестве базового алгоритма, включая как
регуляризацию L1, так и L2 вместо дерева решений. Что касается целевой функции, тестируется классическая квадратичная ошибка, гамма-функция и функция
Твиди, объединяющая преимущества гамма- и пуассоновского распределений.
При выборе бустера gblinear или целевой функции Твиди код переопределяет
выбранные параметры, внося в них изменения и дополнения для соответствия
требованиям бустера gblinear или целевой функции Твиди. Наконец, он создает
XGBoost Regressor с предложенными гиперпараметрами для каждого испытания
и выполняет кросс-валидацию для оценки MAE. Процесс повторяется для указанного количества испытаний (в данном случае 60). После оптимизации выводятся
наилучший достигнутый MAE и соответствующие наилучшие гиперпараметры.
Листинг 7.17. Оптимизация регрессора XGBoost
import optuna
def objective(trial):
Определяет целевую функцию оптимизации
с использованием библиотеки Optuna
params = {
Словарь, содержащий
'booster': trial.suggest_categorical(
гиперпараметры для оптимизации,
'booster',
включая тип бустера, целевой
['gbtree', 'gblinear']
переменной и др.
),
'objective': trial.suggest_categorical(
'objective',
['reg:squarederror', 'reg:gamma', 'reg:tweedie']
),
'n_estimators': trial.suggest_int(
'n_estimators', 100, 1000
),
'learning_rate': trial.suggest_float(
'learning_rate', 0.01, 1.0, log=True
),
'subsample': trial.suggest_float(
'subsample', 0.3, 1.0
),
'colsample_bytree': trial.suggest_float(
'colsample_bytree', 0.3, 1.0
),
'max_depth': trial.suggest_int('max_depth', 1, 7),
'min_child_weight': trial.suggest_int('min_child_weight', 1, 7),
'reg_lambda': trial.suggest_float(
'reg_lambda', 1e-9, 100.0, log=True
),
'reg_alpha': trial.suggest_float(
'reg_alpha', 1e-9, 100.0, log=True
),
}
if params['booster'] == 'gblinear':
Корректирует гиперпараметры на
основе выбранного типа бустера
),
'colsample_bytree': trial.suggest_float(
'colsample_bytree', 0.3, 1.0
),
'max_depth': trial.suggest_int('max_depth', 1, 7),
'min_child_weight': trial.suggest_int('min_child_weight', 1, 7),
346 'reg_lambda':
Глава 7. Полный
пример применения XGBoost
trial.suggest_float(
'reg_lambda', 1e-9, 100.0, log=True
),
'reg_alpha': trial.suggest_float(
'reg_alpha', 1e-9, 100.0, log=True
),
}
Корректирует гиперпараметры на
основе выбранного типа бустера
if params['booster'] == 'gblinear':
keys_to_remove = [
"colsample_bytree", "max_depth",
"min_child_weight", "subsample"
]
params = {
key:value for key, value in params.items()
if key not in keys_to_remove
}
Вводит дополнительный параметр
tweedie_variance_power для
целевой функции Твиди
if params['objective'] == 'reg:tweedie':
# Должен находиться в диапазоне [1, 2) : 1=poisson 2=gamma
params['tweedie_variance_power'] = trial.suggest_float(
'tweedie_variance_power', 1.01, 1.99
)
Инициализирует регрессор
xgb = XGBRegressor(**params)
XGBoost с предложенными
model_pipeline = Pipeline(
[('processing', column_transform),
гиперпараметрами
('xgb', xgb)]
)
cv_splits = cv.split(X, y=X["neighbourhood_more_than_30"])
cv_scores = cross_validate(
estimator=model_pipeline,
X=X,
Выполняет кросс-валидацию,
y=y,
используя заданный пайплайн
scoring='neg_mean_absolute_error',
и оптимизируя по MAE
cv=cv_splits
)
cv_evaluation = np.mean(
Вычисляет MAE на основе
np.abs(cv_scores['test_score'])
отрицательных оценок MAE
)
return cv_evaluation
Возвращает рассчитанное значение метрики
sqlite_db = "sqlite:///sqlite.db"
оценки, которое необходимо минимизировать
Создает объект Optuna study
с сохранением в базе данных
SQLite для оптимизации
Выполняет оптимизацию
для указанного количества
испытаний
Выводит наилучшее значение
метрики оценки, достигнутое
study.optimize(objective, n_trials=60)
в ходе оптимизации
print(study.best_value)
Выводит
наилучшие
гиперпараметры,
print(study.best_params)
найденные в ходе оптимизации
study_name = "optimize_XGBoost_tokyo_airbnb"
study = optuna.create_study(
storage=sqlite_db,
study_name=study_name,
direction="minimize",
load_if_exists=True
)
7.2. Создание и оптимизация модели 347
После выполнения оптимизации мы получаем уменьшенное значение MAE по
сравнению с нашей первой попыткой и набор подходящих гиперпараметров,
показывающий максимальную глубину в семь уровней, около 900 оценщиков
и целевую функцию Твиди со степенью дисперсии 1.5, что указывает на смешанное распределение между пуассоновским и гамма:
6616.859370931483
{'booster': 'gbtree',
'colsample_bytree': 0.946407058507176,
'learning_rate': 0.06867015067874482,
'max_depth': 7,
'min_child_weight': 5,
'n_estimators': 901,
'objective': 'reg:tweedie',
'reg_alpha': 0.0006368936493084075,
'reg_lambda': 3.8302865696045996,
'subsample': 0.8956307610431394,
'tweedie_variance_power': 1.560801988491813
}
На этом этапе мы также можем построить диагностические диаграммы, чтобы
лучше понять ход оптимизации. Например, мы можем сначала визуализировать,
как проходила оптимизация в течение 60 испытаний, которые мы изначально
установили:
fig = optuna.visualization.plot_optimization_history(study)
fig.show()
На рис. 7.13 показано, что лучшее значение было достигнуто довольно быстро,
до 20-го испытания, и не улучшилось после этого. Это важная информация, потому что, если бы наилучшая оптимизация была достигнута позже, можно было
бы предположить возможность дальнейших улучшений после дополнительных
раундов исследования гиперпараметров Optuna. На самом деле это можно сделать в любой момент, повторно выполнив команду study.optimize(objective,
n_trials=60), установив желаемое количество дополнительных испытаний
вместо первоначальных 60. Поскольку мы настроили сохранение испытаний
в базе данных SQLite, можно возобновить оптимизацию в любой момент с точки
ее остановки (что является сильным преимуществом использования Optuna
по сравнению с другими вариантами оптимизации). Еще один важный вывод,
который можно сделать из анализа диаграммы, — существует довольно много
наборов гиперпараметров, которые являются оптимальными или почти оптимальными. Это означает, что для данной задачи не существует единственно
верного оптимизационного решения. Это позволяет исследовать различные
настройки и выбрать вариант, который соответствует вашим потребностям.
Например, вы можете выбрать близкое к оптимальному решение, которое использует больше признаков или требует меньше оценщиков, поскольку так будет
быстрее во время инференса.
348 Глава 7. Полный пример применения XGBoost
Рис. 7.13. Выполнение испытаний Optuna в ходе оптимизации1
Также необходимо построить диаграмму важности гиперпараметров, поскольку
информация в ней может указать на необходимость расширения их пространства
поиска, если они окажутся действительно значимыми для процесса оптимизации:
fig = optuna.visualization.plot_param_importances(study)
fig.show()
В нашем случае наиболее важными факторами оказались colsample_bytree
и min_child_weight — гиперпараметры, которые вызвали наибольшую дисперсию результатов (рис. 7.14).
Теперь у нас есть набор хороших гиперпараметров. В следующем подразделе мы
завершим фазу обучения, протестировав модель с помощью кросс-валидации
для оценки обобщающей способности и обучив финальную модель на всех доступных данных.
7.2.6. Обучение финальной модели
Завершив оптимизацию, мы можем подвести итоги, протестировав результаты
напрямую с помощью кросс-валидации, а затем обучив модель на всех доступных
данных. Код, представленный в листинге 7.18, не сильно отличается от того,
который мы использовали ранее. Обратите внимание, что теперь для оценки
мы используем процедуру кросс-валидации, а не предсказания на ее основе,
поскольку нас больше интересует понимание обобщающей способности модели,
а не то, насколько точно она соответствует имеющимся данным.
1
На графике: Objective value — значение целевой функции, Best value — лучшее значение,
Trial — испытание. — Примеч. ред.
7.2. Создание и оптимизация модели 349
Рис. 7.14. Важность гиперпараметров в процессе настройки1
Листинг 7.18. Обучение модели на полных данных
best_params = study.best_params
print(best_params)
Инициализирует регрессор XGBoost,
используя лучшие гиперпараметры,
полученные в исследовании Optuna
xgb = XGBRegressor(**best_params)
model_pipeline = Pipeline([('processing', column_transform), ('xgb', xgb)])
cv_splits = cv.split(X, y=X["neighbourhood_more_than_30"])
r2_scores = []
rmse_scores = []
mae_scores = []
for train_index, test_index in cv_splits:
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
model_pipeline.fit(X_train, y_train)
y_pred = model_pipeline.predict(X_test)
Разделяет
данные, используя
заданную стратегию
StratifiedKFold
Выполняет итерации
по фолдам кроссвалидации для
проверки модели
r2_scores.append(r2_score(y_test, y_pred))
rmse_scores.append(np.sqrt(mean_squared_error(y_test, y_pred)))
mae_scores.append(mean_absolute_error(y_test, y_pred))
print(f"Среднее и R-квадрат: {np.mean(r2_scores):.3f}")
print(f"Среднее и RMSE: {np.mean(rmse_scores):.3f}")
print(f"Среднее и MAE: {np.mean(mae_scores):.3f}")
1
Обучает финальную
На графике: Importance for Objective Value — важность для значения
целевой функции,
model_pipeline.fit(X,
y)
Hyperparameter — гиперпараметр.
— Примеч. ред.
модель на всем датасете
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
model_pipeline.fit(X_train, y_train)
y_pred = model_pipeline.predict(X_test)
y_pred))
350r2_scores.append(r2_score(y_test,
Глава 7. Полный пример применения XGBoost
rmse_scores.append(np.sqrt(mean_squared_error(y_test,
валидации для
проверки модели
y_pred)))
mae_scores.append(mean_absolute_error(y_test, y_pred))
print(f"Среднее и R-квадрат: {np.mean(r2_scores):.3f}")
print(f"Среднее и RMSE: {np.mean(rmse_scores):.3f}")
print(f"Среднее и MAE: {np.mean(mae_scores):.3f}")
model_pipeline.fit(X, y)
Обучает финальную
модель на всем датасете
Ниже приведен вывод, который вы получите после выполнения кода. Он содержит используемые параметры и метрики оценки, основанные на нашей
стратегии кросс-валидации:
{'booster': 'gbtree',
'colsample_bytree': 0.946407058507176,
'learning_rate': 0.06867015067874482,
'max_depth': 7,
'min_child_weight': 5,
'n_estimators': 901,
'objective': 'reg:tweedie',
'reg_alpha': 0.0006368936493084075,
'reg_lambda': 3.8302865696045996,
'subsample': 0.8956307610431394,
'tweedie_variance_power': 1.560801988491813
}
Среднее и R-квадрат: 0.727
Среднее и RMSE: 10886.568
Среднее и MAE: 6667.187
Мы также можем визуализировать полный пайплайн, который включает в себя
преобразователь столбцов, принимающий различные признаки для своих отдельных операций преобразования, и модель XGBoost, получающую все собранные
от него данные (рис. 7.15).
После тщательного обучения нашей модели можно было бы сказать, что все готово. На самом деле это может быть лишь первый цикл множественных итераций.
Модели часто необходимо повторно обучать, чтобы избежать так называемого
дрейфа концепций, когда взаимосвязи между предикторами и целевой переменной со временем меняются, и это делает более ранние модели неэффективными
с течением времени (см. объяснение в главе 2).
Кроме того, работа ML-инженера и специалиста по данным часто не заканчивается созданием рабочей модели. Крайне важно понимать, как она работает
и как предикторы фактически соотносятся с целевой переменной, что дает
представление о том, как модель формирует свои прогнозы. Объяснение работы
модели помогает укрепить доверие к ее прогнозам, облегчает отладку, способствует соблюдению нормативных требований и позволяет людям понимать,
проверять и улучшать процесс принятия решений системами ИИ. Обсудим это
в заключительном разделе данной главы.
7.3. Объяснение модели с помощью SHAP 351
Рис. 7.15. Пайплайн, включающий преобразователь столбцов и модель XGBoost
7.3. Объяснение модели с помощью SHAP
В заключение мы уделим время разбору принципов работы нашей модели
XGBoost. Если EDA помогает понять, как модель может использовать данные,
то методы объяснимости, такие как SHAP (Shapley Additive exPlanations) или
графики частичной зависимости (описанные в предыдущей главе), дают возможность узнать, как модель использует данные для формирования своих прогнозов.
Объяснимость прогнозов модели может предоставить ценные сведения, которые
помогут лучше подготовить данные, пересмотреть предыдущие предположения
и отбросить бесполезные либо вредные признаки.
Кроме того, объяснимость модели предоставляет сведения о том, как она использует признаки и генерирует прогнозы.
Сотрудничество человека и искусственного интеллекта — при работе с таб
личными данными специалисты сотрудничают с экспертами в предметной
области или бизнес-стейкхолдерами, которые могут не разбираться в сложных моделях. Объяснимость модели позволяет специалистам по данным
эффективно доносить идеи до нетехнической аудитории.
Укрепление доверия — в некоторых областях, таких как здравоохранение или
финансы, объяснимость модели имеет решающее значение для укрепления
доверия со стороны стейкхолдеров и регулирующих органов.
352 Глава 7. Полный пример применения XGBoost
Комплаенс — в некоторых регионах и отраслях существуют нормативные требования к прозрачности и объяснимости моделей, например в Европейском
союзе, где «Общий регламент по защите данных» (General Data Protection
Regulation) подчеркивает «право на объяснение» для автоматизированных
систем принятия решений.
Выявление и смягчение предвзятости — объяснимость может помочь выявить
предвзятость в данных и процессе принятия решений модели, выделяя случаи, когда процесс может ущемлять интересы уязвимых групп.
Учитывая все эти причины, мы решили генерировать значения SHAP с помощью
одноименной библиотеки (https://github.com/shap/shap; установка с помощью
pip install shap) и ее алгоритма TreeSHAP для древовидных моделей, а также
нативно и более эффективно — с помощью XGBoost и LightGBM.
Значения SHAP — это метод, который может объяснить, как формируются
прогнозы моделей машинного обучения. Они основаны на значениях Шепли,
концепции из теории кооперативных игр, которая справедливо распределяет
«вклад» или «важность» каждого признака в прогнозе модели для конкретного
экземпляра данных. Другими словами, значения SHAP распределяют вклад
каждого признака в выходной сигнал модели с использованием простой аддитивной формулы.
Значения Шепли учитывают вклад признака во всех их возможных комбинациях, которые можно рассматривать как «игры» в модели. Они включают ее обучение на различных подмножествах признаков. Значения SHAP аппроксимируют
значения Шепли с помощью стратегии повторной выборки, чтобы избежать
вычисления всех возможных игр для модели и наборов признаков. Используя
значения SHAP, мы получаем представление о том, как каждый признак влияет
на прогнозы модели в конкретных случаях. Эта информация ценна для отладки
модели, генерации признаков и повышения общей интерпретируемости и надежности моделей машинного обучения.
Мы реализовали значения SHAP в листинге 7.19, чтобы получить объяснение
нашей ранее созданной модели XGBoost. В коде сначала извлекаем обученную
модель XGBoost из пайплайна. В частности, получаем ее бустер — основной
компонент модели XGBoost, отвечающий за реализацию алгоритма градиентного
бустинга. Затем преобразовываем обучающие данные дважды: во-первых, потому
что мы не можем использовать пайплайн для прямой подачи данных в бустер.
Поэтому мы предварительно обрабатываем их вручную и извлекаем названия
признаков. Во-вторых, мы преобразуем данные в DMatrix (см. документацию
XGBoost по адресу https://mng.bz/yWQd) — специальную структуру данных
XGBoost для эффективной обработки, которая требуется для прямой подачи
в бустер. На этом этапе мы вычисляем значения SHAP с помощью команды
predict с параметром pred_contribs, установленным в значение Тrue. Другая
7.3. Объяснение модели с помощью SHAP 353
простая команда predict просто предоставляет прогнозы из модели, которые
будут использоваться для сравнения.
Листинг 7.19. Значения SHAP как выходные данные
Извлекает обученный объект
бустера XGBoost из обученной
модели XGBoost пайплайна
from XGBoost import DMatrix
booster = model_pipeline['xgb'].get_booster()
Преобразует входные
данные X с помощью
пайплайна обработки
Xt = model_pipeline['processing'].transform(X)
feature_names = (
Получает имена преобразованных
model_pipeline['processing']
признаков после преобразований
.get_feature_names_out()
в пайплайне обработки
)
Xd = DMatrix(Xt)
Создает DMatrix из
shap_values = booster.predict(Xd, pred_contribs=True)
preds = booster.predict(Xd)
Получает необработанные
предсказанные значения для
входных данных
Вычисляет значения SHAP
с помощью функции predict
бустера с аргументом
pred_contribs=True
преобразованных
входных данных
Для сравнения стоит отметить, что LightGBM способен делать то же самое,
если применить тот же метод прогнозирования с параметром pred_contribs,
установленным на Тrue. Единственное отличие в том, что не нужно извлекать
бустер из обученной модели LightGBM. Следует просто использовать саму
модель напрямую.
Обратите внимание, что независимо от того, выполняете вы классификацию
или регрессию, полученные этим методом значения SHAP являются логарифмическими преобразованиями мультипликативной модели. Это означает, что,
если вы хотите воссоздать исходное предсказание, сначала нужно выполнить
обратное логарифмическое преобразование (возвести число e в степень, равную
SHAP-значению), а затем перемножить значения, как показано в следующем
фрагменте кода. Этот фрагмент восстанавливает для первого примера прогноз
из значений SHAP и сравнивает его с фактическим:
np.prod(np.exp(shap_values[0])), preds[0]
(10627.659, 10627.469)
Как видите, есть небольшие расхождения, которые могут быть связаны с приближениями и небольшими погрешностями. Однако в целом значения SHAP
обеспечивают хорошую аппроксимацию самих предсказаний. При применении того же подхода ко всему обучающему набору и оценке его соответствия
354 Глава 7. Полный пример применения XGBoost
исходным прогнозам с помощью корреляции Пирсона демонстрируется близкое
соответствие значений SHAP предсказаниям:
np.corrcoef(preds, np.prod(np.exp(shap_values), axis=1))
array([[1., 1.],
[1., 1.]])
В качестве альтернативы прямому выводу значений SHAP в качестве прогноза XGBoost можно использовать функцию TreeExplainer из пакета SHAP
(https://mng.bz/pKXR). Хотя она и заявлена как реализованная на быстром C++,
она намного медленнее, чем прямое прогнозирование из XGBoost. Однако при
использовании TreeExplainer можно указать дополнительные параметры вывода, в частности тип вывода и метод расчета. Это позволяет реконструировать
исходный прогноз, как было показано ранее (с помощью параметра feature_
perturbation="tree_path_ dependent"), либо использовать метод интервенции,
который разрывает зависимости между признаками в соответствии с правилами
каузального вывода, тем самым обеспечивая более надежные результаты в условиях сильной коллинеарности признаков (с помощью параметра feature_
perturbation="interventional"). Интервенционные значения SHAP можно
получить, используя следующий фрагмент кода:
from shap import TreeExplainer
explainer = TreeExplainer(model_pipeline['xgb'], data=Xt, model_output='raw',
feature_perturbation='interventional')
interventional_shap_values = explainer.shap_values(Xt)
Полученная матрица значений SHAP менее точно соответствует исходным
данным и не позволяет восстановить прогнозы, как было показано ранее. Тем
не менее такой подход может обеспечить более надежные оценки вклада признаков, которые «верны модели» (true to the model), как объяснено технически
в следующей проблеме на GitHub: https://github.com/shap/shap/issues/1098. Исходя
из нашего опыта, мы рекомендуем использовать TreeExplainer и интервенционный подход, хотя он может потребовать больше времени вычислений при
работе с данными, представляющими высокомультиколлинеарные признаки.
До сих пор мы использовали значения SHAP как метод объяснения отдельных образцов. Мы исследовали путем проверки вклада признаков причины, по которым
делается определенный прогноз. Однако можно рассмотреть все значения SHAP
вместе и проанализировать их, чтобы получить общее объяснение для всей модели. В этом случае, как и для других методов, можно построить сводные и диагностические диаграммы. Первый предлагаемый нами листинг (7.20) количественно
определяет относительную важность признаков по среднему значению SHAP.
Здесь мы используем возможности визуализации из пакета shap. Установите
его, выполнив команду pip install shap в командной строке или ячейке Colab.
7.3. Объяснение модели с помощью SHAP 355
Листинг 7.20. Диаграмма важности SHAP
import shap
shap.summary_plot(
shap_values[:,:-1],
Xt,
plot_type="bar",
Формирует сводную диаграмму
feature_names=feature_names,
max_display=10,
важности признаков SHAP для
show=False
10 наиболее важных признаков
)
plt.xticks(fontsize=8)
plt.yticks(fontsize=8)
plt.xlabel("Важность SHAP", fontsize=10)
plt.show()
На рис. 7.16 показан полученный график, и вы можете сразу определить, что
в прогнозах доминируют четыре признака, а именно: доступность для бронирования, что также является косвенным показателем динамики спроса и предложения на рынке для конкретного жилья (меньшая доступность может подразумевать совместное использование или меньший спрос на это помещение);
координаты, закодированные по целевой переменной (то есть местоположение
объекта в городе); количество спален — косвенный показатель размера помещения; количество кроватей, что вместе с предыдущим показателем помогает
идентифицировать хостелы, которые обычно менее дорогие. Все остальные признаки играют второстепенную роль, это можно увидеть из масштаба графика:
важность десятого по значимости признака составляет примерно пятую часть
самых важных признаков.
Однако важность признаков раскрывает лишь часть картины. Нам также необходимо понимание направленности их влияния. Здесь скрипичная диаграмма
(violin chart) может предоставить еще больше информации о поведении модели.
Эта диаграмма создается с помощью пакета shap. Анализируя ее особенности,
можно получить ряд подсказок.
Важность признака — ширина скрипичной диаграммы указывает на плотность значений SHAP. Более широкие участки представляют большее число
случаев с похожими значениями SHAP для конкретного признака. Таким образом, признаки с более широкими диаграммами, как правило, более важны
для прогнозов модели.
Форма скрипки — показывает распределение значений SHAP для соответствующего признака. Если скрипка симметрична, это говорит о том, что
значения SHAP равномерно распределены вокруг медианы, это указывает на
сбалансированное влияние на прогнозы. Асимметрия указывает на смещение
356 Глава 7. Полный пример применения XGBoost
и предполагает, что определенные значения функции имеют более значимые
эффекты, чем другие.
Положительный и отрицательный вклады — центральная линия скрипичной
диаграммы (медиана) обычно находится на нуле. Левая и правая половины
скрипки представляют соответствующие вклады признаков с положительными и отрицательными значениями SHAP. Положительные значения повышают прогнозы, а отрицательные понижают их.
Связь со значением признака — цвет графика скрипки может помочь связать
синие области (где признак имеет более низкие значения) и красные (с более
высокими значениями) с конкретными вкладами SHAP. Это помогает понять,
как признак в целом связан с результатом.
Выбросы — выбросы, или экстремальные значения SHAP за пределами диапазона скрипичного графика, указывают на случаи, когда соответствующий
признак оказывает необычно сильное влияние на прогноз.
Важность SHAP
Рис. 7.16. Важность SHAP
7.3. Объяснение модели с помощью SHAP 357
В листинге 7.21 строится скрипичная диаграмма для получения полезной информации о распределении и роли каждого признака в прогнозах модели.
Листинг 7.21. Скрипичная диаграмма в SHAP
shap.summary_plot(shap_values[:,:-1], Xt,
plot_type="violin",
feature_names=feature_names,
show=False)
plt.yticks(fontsize=8)
plt.show()
Создает сводную диаграмму SHAP
с использованием скрипичных диаграмм
для визуализации распределения
значений SHAP для каждого объекта
Рис. 7.17 показывает полученные скрипичные диаграммы.
Что касается наиболее важных признаков, то можно выделить следующие:
numeric__availability_365 — более высокая доступность соответствует
положительному влиянию на цену. Объекты с более низкой доступностью
обычно штрафуются;
target_encoding__coordinates — трудно интерпретировать, поскольку его
значения не связаны с определенной направленностью. Можно наблюдать
длинные хвосты с обеих сторон с преобладанием отрицательного вклада
в ценообразование жилья;
numeric_number_of_bedrooms — большее количество спален подразумевает
более высокую цену с длинным, смещенным вправо хвостом;
numeric__number_of_beds — аналогично, большее количество кроватей под-
разумевает более высокую цену с длинным, смещенным вправо хвостом.
Беглый взгляд на другие признаки дает интуитивное представление о том, как
ведет себя модель. Например, чем ближе жилье к Императорскому дворцу или
аэропортам, тем выше цена.
На этом мы завершаем рассмотрение нашего полного примера использования
градиентного бустинга. В следующей главе вернемся к задаче Airbnb NYC. Мы
рассмотрим набор фреймворков глубокого обучения (низкоуровневый фреймворк, высокоуровневый API и библиотеки для работы с табличными данными),
применим три из них (fastai, PyTorch с TabNet и Lightning Flash) для ее решения
и сравним различные подходы.
На этом этапе инструмент генеративного ИИ, такой как ChatGPT, может быть
полезен для объяснения значений SHAP, присвоенных каждому примеру. Это
может оказаться сильным преимуществом при демонстрации возможностей
модели или попытке убедить клиентов и стейкхолдеров.
358 Глава 7. Полный пример применения XGBoost
Рис. 7.17. Скрипичные диаграммы SHAP1
1
На графике: SHAP value (impact on model output) — значение SHAP (влияние на вывод
модели), Feature Value — значение признака, Low — низкое, High — высокое. — Примеч.
ред.
7.3. Объяснение модели с помощью SHAP 359
Кроме того, необходимость в разъяснении прогнозов модели на основе данных
имеет решающее значение в соответствии с такими регуляторными требованиями, как правила Европейского союза. Прозрачность и интерпретируемость
являются важнейшими компонентами нормативных актов, направленных на защиту данных и конфиденциальности, например «Общего регламента по защите
данных в ЕС». Согласно им, граждане имеют право понимать логику автоматизированных процессов принятия решений, которые существенно влияют на их
жизнь. Предоставление четкого и понятного объяснения того, почему был сделан
конкретный прогноз, обеспечивает прозрачность и подотчетность и способствует справедливости: это дает людям возможность запрашивать разъяснения,
оспаривать несправедливые решения и в конечном счете защищать свои права.
Фактически вы можете сгенерировать каждое объяснение с помощью отдельных промптов на лету или с помощью API ChatGPT для пакетной обработки
объяснений, к которым позже можно обратиться, если вас спросят о причине
конкретного прогноза. Однако подход одинаков как при обработке на лету, так
и при пакетной: вы должны попросить LLM дать объяснение, предоставив список признаков (подробный, с их описанием или значением, если необходимо),
исходное значение в датасете и значение SHAP для признака. Конечно, необходимо привести полученный прогноз. Объединение всей этой информации для
обработки LLM с помощью JSON (словаря словарей) может быть идеальным
вариантом. В листинге 7.22 мы предлагаем решение для подготовки структуры
JSON для упрощения запроса к ChatGPT с целью получить объяснение прогноза для конкретного примера в данных, идентифицированного по его индексу
строки. Код генерирует структуру, которая охватывает всю информацию, необходимую для построения связного объяснения.
Листинг 7.22. Создание JSON-файла с объяснениями SHAP как части
промпта
def generate_prediction_explanation(
index,
X,
feature_names,
shapley_values,
predictions
Создает структуру данных JSON
):
как словарь Python
explanation = {}
explanation["prediction"] = predictions[index]
Включает предсказанное
for feature, original_value, shap_value in zip(
значение для дачи
feature_names,
объяснения в JSON
X[index],
shapley_values[index, :]
):
Выполняет итерации по признакам,
explanation[feature] = {
исходному значению и значениям SHAP
"original_value": original_value,
для рассматриваемой строки
"shap_value": shap_value
}
return explanation
Индекс предсказания, которое
нужно объяснить
index_to_explain = 5
explanation_json = generate_prediction_explanation(
index_to_explain,
feature_names,
explanation["prediction"] = predictions[index]
Включает предсказанное
for feature, original_value, shap_value in zip(
значение для дачи
feature_names,
объяснения в JSON
X[index],
shapley_values[index, :]
):
Выполняет итерации по признакам,
360 explanation[feature]
Глава 7. Полный пример
= { применения XGBoost
исходному значению и значениям SHAP
"original_value": original_value,
для рассматриваемой строки
"shap_value": shap_value
}
return explanation
Индекс предсказания, которое
нужно объяснить
index_to_explain = 5
explanation_json = generate_prediction_explanation(
index_to_explain,
feature_names,
Xt,
shap_values,
preds
)
print(explanation_json)
В нашем примере требуется описание того, почему модель предсказала определенное значение для строки 5 датасета. Затем созданный JSON можно вставить
в промпт, например:
Ты эксперт по данным, и тебе нужно интерпретировать прогнозы регрессионной
модели на основе значений, предоставленных в файле JSON. Ты создаешь объяснения
в виде текста о том, как наиболее важные переменные влияют на прогноз. Вот файл
JSON:
{'prediction': 55225.176, 'onehot_encoding__room_type_Entire home/apt':
{'original_value': 1.0, 'shap_value': 0.03404991},
'onehot_encoding__room_type_Hotel room': {'original_value': 0.0,
'shap_value': 0.00020163489}, … }
Когда вы отправите этот промпт в ChatGPT, то получите текст, организованный в виде маркированного списка с категоризацией по типам переменных.
Этот текст описывает влияние каждой отдельной переменной или их группы
на результат. Ниже приведен частичный пример такого вывода, который мы
получили для строки 5.
Тип помещения — значение Entire home/apt (Весь дом/квартира) оказывает
положительное влияние на прогнозируемую цену, значение SHAP 0.034. Это
говорит о том, что объявления с типом помещения «Весь дом/квартира»
обычно содержат более высокие цены.
Другие типы помещений, такие как Hotel room (Гостиничный номер), Private
room (Отдельная комната) и Shared room (Комната на несколько человек)
имеют меньший положительный или незначительный вклад, это указывает
на то, что их влияние на цену не столь существенно.
Район — признак neighborhood_more_than_30 имеет положительное значение
SHAP, равное 0.083, и это говорит о том, что нахождение в районе с более чем
30 объектами положительно влияет на цену.
Тип размещения — признак type_of_accommodation имеет небольшой отрицательный эффект со значением SHAP –0.008. Это означает, что некоторые
типы размещения могут иметь немного более низкую цену.
Итоги 361
Полный текст затрагивает все признаки, и вы можете попросить LLM сократить
результаты только до 5 или 10 наиболее важных, если вам так удобнее. Нет сомнений, что использование языковой модели для этой работы делает сложную
задачу проще и легко автоматизирует ее.
Итоги
Получение и подготовка данных означают их загрузку, реструктуризацию
и объединение. Это часто долгая и трудоемкая часть работы, но она необходима, поскольку закладывает основу успеха всего проекта.
Генерация признаков — не магия или случайное их комбинирование; чаще
всего это внедрение экспертных знаний о задаче и способах ее решения в признаки, которые вы будете использовать для обучения своей модели. Изучение
предметной области, связанной с проблемой, — первый шаг к эффективному
моделированию данных.
Изучение прогнозов и целевой переменной на этапе EDA является неотъемлемой частью работы с табличными данными. Ищите выбросы и экстремальные значения, пропущенные данные и любые другие особенности. Не
стесняйтесь исключать примеры, если не уверены, что они могут принести
реальную пользу вашей модели.
Прежде чем углубляться в моделирование, проверьте свою стратегию валидации, которая может потребовать дополнительной работы: EDA и настройки
пайплайна данных. И то и другое может иметь значение на этапе моделирования. В идеале подготовьте пайплайн для каждого типа модели, который
вы хотите протестировать, поскольку разные модели по-разному работают
с различными типами данных в табличных датасетах. В нашем примере для
упрощения мы попробовали универсальный подход «под одну гребенку».
Помните, что такие примеры хороши для книг, а для реальных проектов
существуют более удачные решения.
Построение базовой модели — часто игнорируемая фаза моделирования таб
личных задач. Тем не менее она может дать ценную информацию, позволяя
проанализировать, как модель недообучается или переобучается, а также
изучить ее внутренние коэффициенты. Базовая модель должна быть простой:
линейная и логистическая регрессия являются лучшими кандидатами для
задач регрессии и классификации соответственно.
После получения инсайтов от своей базовой модели можно перейти к более
сложным алгоритмам, таким как XGBoost. Подсказки о недообучении, нелинейностях, взаимодействиях признаков, целевой переменной и особенностях предикторов следует учитывать при установке начальных значений
ключевых гиперпараметров.
362 Глава 7. Полный пример применения XGBoost
Оптимизация модели с помощью Optuna может сэкономить много времени, если вы настроите поисковое пространство с учетом идей и гипотез,
разработанных на текущий момент касательно того, как модель должна обрабатывать данные и решать задачу. После завершения оптимизации можно
получить новые идеи, анализируя важность гиперпараметров и диаграммы
путей оптимизации.
Объяснение прогнозов обученной модели можно легко получить с помощью
XGBoost и LightGBM, используя метод predict с параметром pred_contribs,
установленным на Тrue. После получения значений SHAP, которые фактически являются множителями относительно прогноза, можно использовать
стандартные диаграммы из пакета shap, такие как диаграмма важности или
скрипичная диаграмма.
Часть 3
Глубокое обучение
для анализа табличных
данных
Часть 3 служит путеводителем по ноу-хау и практическим советам, необходимым
для применения глубокого обучения к задачам табличных данных. В качестве
самостоятельного решения или в сочетании с градиентным бустингом глубокое
обучение может давать хорошие результаты на табличных данных, если вы знае
те, как использовать его уникальный подход к решению предиктивных задач.
В главе 8 рассматриваются различные стеки и фреймворки глубокого обучения
для работы с табличными данными, включая низкоуровневые фреймворки, такие
как TensorFlow и PyTorch, и высокоуровневые API, например fastai и Lightning
Flash. В ней представлено несколько библиотек, специально разработанных для
задач глубокого обучения на табличных данных: TabNet, PyTorch Tabular, SAINT
и DeepTables. Мы сравниваем различные фреймворки и обсуждаем сильные
и слабые стороны каждого из них. Глава 9 расширяет обсуждение до лучших
практик. Для их иллюстрации мы используем датасет о недвижимости КуалаЛумпура, включая подготовку табличных данных, проектирование архитектуры
модели и ее обучение. Пример на основе Keras показывает простые, понятные
и эффективные пайплайны данных, а также модульный подход, который способствует повторному использованию кода.
В главе 10 исследуется, как сделать обученную модель глубокого обучения доступной для использования в реальных условиях с помощью Flask — фреймворка
Python, который отлично подходит для веб-интерфейсов и обслуживания API.
В главе 11 дискуссия расширяется, проводя вас через шаги по определению
пайплайна для обучения и развертывания модели с использованием среды
Vertex AI в Google Cloud, включая создание контейнеров Docker для инкапсуляции кода модели и зависимостей, определение шагов пайплайна и его запуск
364 Часть 3. Глубокое обучение для анализа табличных данных
в Vertex AI. Затем мы переходим к обсуждению возможностей Gemini для Google
Cloud, таких как ответы на вопросы о Google Cloud, генерация кода из текста,
интерпретация кода и обобщение записей логов. Все эти возможности можно
применить в вашем рабочем процессе для создания собственного пайплайна
машинного обучения.
Книга завершается главой 12, из которой вы узнаете, как объединить глубокое
обучение с машинным для достижения самых передовых результатов в предик
тивных задачах. После прочтения книги проектирование, обучение, развертывание и интерпретация моделей больше не будут для вас загадкой!
8
С чего начать?
В этой главе
3 Что такое технологический стек глубокого обучения —
низкоуровневые фреймворки и высокоуровневые API
3 Стек PyTorch и fastai
3 Стек PyTorch и TabNet
3 Стек PyTorch и Lightning Flash
3 Какие стеки мы не использовали и почему
3 Плюсы и минусы использования технологических стеков глубокого
обучения для табличных данных
До этого момента мы фокусировались лишь на классических инструментах
и алгоритмах машинного обучения для анализа табличных данных. Все они — от
традиционных алгоритмов регрессии до более сложных методов градиентного
бустинга — обладают преимуществами простоты, прозрачности и эффективности.
При этом инструменты глубокого обучения стали намного доступнее и проще
в использовании и представляют собой мощную альтернативу для обработки
табличных данных.
366 Глава 8. С чего начать?
В этой главе мы рассмотрим набор стеков глубокого обучения (низкоуровневый фреймворк, высокоуровневый API и библиотеки для табличных данных)
и используем три из них — fastai, PyTorch с TabNet и с Lightning Flash — для
решения задачи на материале датасета Airbnb NYC. Мы решим одну и ту же
задачу трижды, по одному разу с помощью каждого стека. Цель состоит в том,
чтобы не только проиллюстрировать общую форму подхода к глубокому обу
чению, но и подчеркнуть уникальные характеристики трех выбранных нами
инструментов.
8.1. Стек технологий для глубокого обучения
на табличных данных
Прежде чем мы рассмотрим в целом стеки, доступные для глубокого обучения
на табличных данных, давайте вернемся к конкретному примеру из главы 3:
решению для прогнозирования цен на жилье на Airbnb NYC на основе Keras.
Keras или XGBoost
Код, который отличается для решения Keras, содержится в блокноте для обучения
модели. В частности, основные различия между решениями на Keras и на XGBoost,
описанные в главе 3, включают:
• определение модели. Модель Keras имеет объемную функцию определения слоев,
составляющих модель, при этом каждому типу столбцов (с непрерывными, категориальными и текстовыми данными) соответствует свой специфический набор
слоев;
• обучение модели. Модель Keras включает в себя дополнительный код для определения обратных вызовов, необходимых для повышения эффективности обучения,
включая обратный вызов для его ранней остановки, если оно больше не улучшает
модель, и обратный вызов для сохранения оптимальной версии модели среди всех
полученных в этом процессе.
Решение на Keras, приведенное в главе 3, дает конкретную базовую модель для
сравнения с другими стеками, которые мы рассмотрим в этой главе. Так вы сможете
увидеть плюсы и минусы каждого из них.
Мы также обсудим дополнительные стеки, которые не смогли реализовать на
практике, и объясним, какие выводы можно сделать из этого опыта. Важно понимать, какие имеются доступные варианты, их преимущества и недостатки, чтобы
выбрать оптимальный стек, который наилучшим образом соответствует вашим
требованиям.
Давайте коротко рассмотрим решение на Keras из главы 3. На рис. 8.1 показаны
составляющие его файлы, причем блокнот для обучения выделен.
8.1. Стек технологий для глубокого обучения на табличных данных 367
Этот блокнот содержит код, который отличается между решением на Keras
и другими подходами (мы рассмотрим их далее). Остальные файлы остаются
неизменными во всех решениях глубокого обучения, за исключением некоторых
настроек в файле конфигурации обучения.
На рис. 8.2 показаны компоненты, составляющие стек для этого решения. Они
представлены в блокноте для обучения.
В этом стеке базовым низкоуровневым фреймворком является TensorFlow.
Поскольку Keras поставляется как часть дистрибутива TensorFlow и является
рекомендуемым для него высокоуровневым API, может показаться излишним
говорить о них по отдельности. Однако это сделает объяснение более понятным.
CSV-файл
с входными
данными
Блокнот
с кодом для
очистки данных
Файл
конфигурации
очистки
pickle-файл
очищенных
данных
Блокнот с кодом
для обучения
модели на Keras
Файлы
конфигурации
обучения
Рис. 8.1. Файлы,
составляющие решение
на Keras для задачи Airbnb
Пример для датасета
Airbnb NYC из главы 3
Низкоуровневый
фреймворк
TensorFlow
Высокоуровневый
API
Keras
Пользовательский
код
Функция get_model(), которая
определяет слои Keras в модели
для каждого типа столбца
Рис. 8.2. Стек для решения
Airbnb NYC из главы 3
368 Глава 8. С чего начать?
В решении для глубокого обучения из главы 3 мы использовали специально
написанный код для определения самой модели. Например, в листинге 8.1
показан пользовательский код, который определяет слои для категориальных
столбцов. В листинге также показаны инструкции функции get_model(), которые определяют эти слои.
Листинг 8.1. Инструкции в функции get_model() для слоев обработки
категориальных столбцов
Добавляет только что
определенный входной слой
в список входных слоев
Определяет входной слой
модели для текущего столбца
for col in collist:
Определяет слой
catinputs[col] = Input(shape=[1],name=col)
эмбеддинга для
inputlayerlist.append(catinputs[col])
текущего столбца
embeddings[col] = \
(Embedding(max_dict[col],catemb) (catinputs[col]))
embeddings[col]=(BatchNormalization()(embeddings[col]))
collistfix.append(embeddings[col])
Определяет слой batch
Добавляет набор слоев, определенных
для этого столбца, в общий список слоев
normalization для
текущего столбца
Функция get_model() определяет слои Keras в модели для трех типов входных
столбцов: с категориальными, непрерывными и текстовыми данными. Функция
get_model(), показанная в листинге 8.1, содержит инструкции, которые определяют слои для непрерывных и текстовых данных. Обратите внимание, что эта
модель имеет несколько входов (каждый столбец, выбранный для обучения
модели, является входом) и один выход: прогноз того, будет ли цена конкретного
объекта, предлагаемого в аренду на Airbnb, выше или ниже медианы. Подробности того, как определяются слои для каждого из входных столбцов, выходят
за рамки этой главы, поэтому мы не будем разбирать их.
Теперь, когда мы вспомнили, как выглядит стек для решения задачи Airbnb NYC
из главы 3, давайте обобщим эти сведения для других подходов с использованием глубокого обучения для анализа табличных данных. На рис. 8.3 показано
несколько вариантов.
Давайте рассмотрим каждый слой стека более подробно.
Низкоуровневый фреймворк. Существуют два основных низкоуровневых
фреймворка глубокого обучения. TensorFlow чаще всего используется в индустрии. PyTorch — самый популярный выбор среди исследователей.
Высокоуровневый API. Чтобы облегчить новичкам создание приложений
глубокого обучения и абстрагировать часть сложности для опытных раз-
8.1. Стек технологий для глубокого обучения на табличных данных 369
работчиков, в середине 2010-х годов начали создавать высокоуровневые
API для глубокого обучения. Изначально Keras можно было использовать
в качестве фронтенда для нескольких низкоуровневых фреймворков. В 2019
году Keras был интегрирован в экосистему TensorFlow и стал для нее рекомендуемым высокоуровневым фреймворком. Точного аналога Keras в мире
PyTorch нет. Считается, что общий дизайн у PyTorch более доступный,
чем у TensorFlow, и это снижает потребность в высокоуровневых API. Тем
не менее существуют два таких API, абстрагирующих различные аспекты
PyToch. fastai предназначен для специалистов из других областей, которые
хотят использовать глубокое обучение в решении собственных задач, и его
центральная идея заключается в возможности определять, обучать и использовать модель глубокого обучения с помощью всего нескольких строк
кода. Lightning, напротив, абстрагирует только один аспект PyTorch — цикл
обучения. Lightning Flash, который построен на основе Lightning, является,
согласно его документации, «высокоуровневым фреймворком глубокого обу
чения для быстрого прототипирования, создания базовых моделей, тонкой
настройки и решения задач глубокого обучения». Хотя и fastai, и Lightning
имеют своих поклонников, ни один из них не завоевал такой популярности
в мире PyTorch, как Keras в мире TensorFlow.
Библиотека для табличных данных. Низкоуровневый фреймворк и высокоуровневый API предоставляют среду для глубокого обучения в целом.
Специализированные библиотеки глубокого обучения предоставляют возможности для работы с табличными данными. Как мы продемонстрировали
в решении задачи прогнозирования цен Airbnb NYC в главе 3, использовать
специальную библиотеку совсем необязательно.
Два важных замечания о библиотеках для табличных данных.
Они могут поддерживаться как TensorFlow, так и PyTorch. TabNet поддерживается обоими.
fastai — это не только универсальный высокоуровневый API, но и библиотека
табличных данных. fastai абстрагирует часть сложности PyTorch, упрощая
создание и обучение моделей на различных типах данных (включая изображения и текст), а также предоставляет возможности, специально предназначенные для табличных данных (например, автоматически обрабатывает
основные операции, необходимые для категориальных признаков в табличных датасетах).
Теперь, когда мы рассмотрели стеки глубокого обучения для работы с табличными данными, давайте обратимся к инструментам, которые мы применим
в этой главе.
370 Глава 8. С чего начать?
Экосистема TensorFlow
Низкоуровневый
фреймворк
Высокоуровневый
API
Экосистема PyTorch
TensorFlow
PyTorch
Keras
Lightning
fastai
TabNet
SAINT
DeepTables
PyTorch Tabular
Библиотека
для работы
с табличными
данными
TabNet
fastai
Lightning Flash
Рис. 8.3. Стеки глубокого обучения для табличных данных
PyTorch с fastai. Это наиболее традиционный подход, поскольку fastai — это
устоявшийся фреймворк, который используют десятки тысяч разработчиков.
Он очень популярен (у него много звезд на GitHub), явно поддерживает
работу с табличными данными и особенно часто используется новичками
в глубоком обучении и любителями-энтузиастами.
PyTorch с TabNet. TabNet — вторая по популярности после fastai библиотека
для табличных данных (согласно количеству звезд на GitHub), которую
Google выделяет в своей документации (https://mng.bz/av1m). Этот фреймворк
демонстрирует, как можно использовать специализированную библиотеку
для создания модели, обученной на табличных данных.
Lightning Flash. PyTorch Lightning — популярный фреймворк, который абстрагирует часть сложности PyTorch. Lightning Flash построен на основе
PyTorch Lightning и предлагает легкодоступный способ создания приложений глубокого обучения. Он также включает явную поддержку табличных
данных, и, следовательно, его будет интересно сравнить с другими стеками,
которые мы рассматриваем в этой главе.
В следующих трех разделах этой главы описывается решение задачи прогнозирования цен Airbnb NYC с использованием одного из трех упомянутых стеков.
В каждом разделе мы рассмотрим код решения и сравним его с нашим базовым
решением на Keras из главы 3, оценив все плюсы и минусы.
8.2. PyTorch с fastai 371
8.2. PyTorch с fastai
Давайте рассмотрим один из наборов инструментов: PyTorch/fastai. Мы работаем
с теми же датасетом и задачей, которые уже обсуждались выше, поэтому не будем
вновь описывать их здесь. Бˆольшая часть решений довольно схожа, несмотря
на использование разных инструментов. Мы сосредоточимся на отличительных
особенностях кода на PyTorch. Вы найдете полный код в репозитории для книги:
https://mng.bz/gaBv.
8.2.1. Ключевые особенности кода решения на fastai
Итак, погрузимся в решение нашей задачи прогнозирования цен на жилье Airbnb
NYC с использованием fastai. Прежде всего fastai имеет уникальный набор импортов, которые подключают библиотеки, необходимые для работы в блокноте
Jupyter (см. листинг 8.2).
Листинг 8.2. Импортирование fastai
!pip install -Uqq fastbook
import fastbook
from fastbook import *
from fastai.tabular.all import *
Импортирует библиотеки для работы
с табличными датасетами в fastai
Устанавливает библиотеки для
использования fastai в блокноте Jupyter
Импортирует библиотеки для
использования fastai в блокноте Jupyter
После импорта этих библиотек у вас будут все компоненты, необходимые для
запуска приложения fastai для работы с табличными данными в блокноте Jupyter.
Далее следует определить характеристики табличного датасета, включая столбец
с целевой переменной (в fastai она называется зависимой переменной — dependent
variable), и списки категориальных и непрерывных признаков, как показано
в листинге 8.3.
Листинг 8.3. Определение датасета для решения на fastai
Указывает столбец в датасете,
содержащий целевое значение, которое
предсказывает обученная модель
dep_var = 'target'
cat = ['neighbourhood_group','neighbourhood','room_type']
cont = \
['minimum_nights','number_of_reviews',\
'reviews_per_month','calculated_host_listings_count']
Указывает столбцы
с категориальными
признаками в датасете
Указывает столбцы
с непрерывными
признаками в датасете
372 Глава 8. С чего начать?
Мы будем использовать значения, определенные в листинге 8.3, при создании
объекта TabularDataLoaders (https://mng.bz/5gwO) для этой модели. Объект
TabularDataLoaders инкапсулирует выборки из датасета, включая метки, что
упрощает работу с ним.
Далее необходимо убедиться, что целевой столбец содержит строковые значения:
merged_data['target'] =merged_data.target.astype(str)
Если мы этого не сделаем, то столкнемся с небольшой проблемой. Чтобы прочувствовать это самим, закомментируйте приведенную выше инструкцию и запустите блокнот для обучения модели на fastai. Вы увидите, что обучение дает
странные результаты (рис. 8.4).
Рис. 8.4. Результаты обучения на fastai, когда целевой столбец явно не преобразован
в строковые значения
На рис. 8.4 приведены результаты для каждой эпохи процесса обучения, включая
потери на обучении и валидации, а также долю верных результатов. Значения
последней, показанные на рис. 8.4, значительно ниже тех, что мы наблюдали
в главе 3 для решений на XGBoost и глубоком обучении на Keras (от 79 % до
81 %), и не улучшаются от одной эпохи к другой. Обучение в течение большего
количества эпох не помогает: доля верных результатов остается прежней. Почему
fastai дает такие разочаровывающие результаты? В выводе инструкции learn.
loss_func есть подсказка, показанная в листинге 8.4.
Листинг 8.4. Инструкция для отображения функции потерь,
используемой при обучении модели
learn.loss_func
FlattenedLoss of MSELoss()
Инструкция, возвращающая функцию потерь,
использованную при обучении модели fastai
Вывод инструкции, показывающей функцию потерь,
использованную при обучении модели fastai
Вывод, представленный в листинге 8.4, показывает используемую для модели
функцию потерь. Если вы не укажете ее, fastai выберет ее автоматически на
8.2. PyTorch с fastai 373
основе значений в целевом столбце. Мы хотим обучить модель классификации,
поэтому функция потерь должна быть перекрестной энтропией (cross-entropy).
Однако похоже, что fastai выбрал функцию потерь для задачи регрессии. Вот
почему результаты обучения, показанные на рис. 8.4, плохие: fastai пытается
решить задачу регрессии (предсказать непрерывное значение), а не задачу классификации, которую мы предполагали (предсказать 0 или 1 для обозначения,
имеет ли жилье цену выше или ниже медианы).
Вывод dls.valid.show_batch(), показанный на рис. 8.5, дает нам еще одну подсказку, поскольку значения в столбце target являются числами с плавающей
точкой, хотя они должны быть «0» или «1».
Категориальные признаки
Непрерывные признаки
Целевой
столбец
Рис. 8.5. Пример значений батча, когда данные в целевом столбце явно
не преобразованы в строковые
Если вернуться назад и посмотреть на датасет с помощью команды merged_data.
head() (рис. 8.6), все значения в столбце target будут выглядеть как 0 или 1.
Эти значения на самом деле являются числовыми, а это значит, что если мы явно
не преобразуем их в строки, то по умолчанию fastai будет предполагать, что мы
хотим обучить регрессионную модель.
374 Глава 8. С чего начать?
Значения явным образом
преобразованы в строковые
Рис. 8.6. Пример значений батча, когда данные в целевом столбце явно
преобразованы в строковые
Теперь, когда мы разобрались, почему так важно преобразовать значения в столбце target в строковые, давайте рассмотрим остальную часть кода для создания
обученной модели fastai. В листинге 8.5 показан блок кода, определяющий объект
TabularDataLoaders. Этот объект представляет собой обертку, специфичную для
работы с табличными данными, вокруг объекта PyTorch DataLoader (https://mng.
bz/6eDe), являющегося итерируемой инкапсуляцией семплов и меток в датасете.
Листинг 8.5. Определение объекта TabularDataLoaders
Определяет процедуры
преобразований,
применяемые к датасету
в заданном пайплайне
Задает значение-заполнитель
для объекта пути
path = '.'
procs = [FillMissing,Categorify, Normalize]
dls = TabularDataLoaders.from_df(dtrain,
path,
procs= procs,
cat_names= cat,
cont_names = cont,
y_names = dep_var,
valid_idx=\
list(range((merged_data.shape[0]-10000),
merged_data.shape[0])),
bs=32)
Указывает, что объект
TabularDataLoaders основан
на датафрейме merged_data
Указывает список
преобразований,
применяемых к объекту
TabularDataLoaders
Задает категориальные
признаки
Задает непрерывные
признаки
Задает целевой признак
Задает подмножество датасета,
преобразований,
применяемые к датасету
в заданном пайплайне
Задает значение-заполнитель
для объекта пути
path = '.'
procs = [FillMissing,Categorify, Normalize]
dls = TabularDataLoaders.from_df(dtrain,
path,
procs= procs,
cat_names= cat,
cont_names = cont,
y_names = dep_var,
valid_idx=\
list(range((merged_data.shape[0]-10000),
merged_data.shape[0])),
bs=32)
Задает размер батча
Указывает, что объект
TabularDataLoaders основан
на датафрейме merged_data
Указывает список
преобразований,
8.2. PyTorch
с fastai 375
применяемых к объекту
TabularDataLoaders
Задает категориальные
признаки
Задает непрерывные
признаки
Задает целевой признак
Задает подмножество датасета,
используемое для валидации
в процессе обучения
Мы будем использовать объект TabularDataLoaders, определенный в листинге 8.5, для создания модели fastai, показанной в листинге 8.7.
Одной из отличительных особенностей fastai является набор удобных функций,
который позволяет легко исследовать данные на различных этапах обучения.
Одна из них — метод show_batch(), показанный в листинге 8.6.
Листинг 8.6. Отображение батча обучающих данных
dls.valid.show_batch()
Этот метод позволяет легко увидеть, как выглядят данные, используемые для
обучения модели, после преобразований, которые были указаны в параметре
procs определения TabularDataLoaders. На рис. 8.7 показан вывод этого метода.
Рис. 8.7. Вывод метода show_batch()
376 Глава 8. С чего начать?
Теперь, когда мы указали данные, которые будут использоваться для обучения
модели, пришло время определить и обучить модель.
Листинг 8.7. Определение и обучение модели fastai
Определяет модель как объект tabular_learner, используя
объект TabularDataLoaders dls и долю верных результатов
в качестве метрики эффективности обучения
learn = tabular_learner(dls, metrics=accuracy)
learn.fit_one_cycle(3)
Обучает модель на
протяжении трех эпох
Обратите внимание, что методы в листинге 8.7, которые определяют и обучают
модель, намного проще, чем аналогичные для Keras в главе 3. В этом смысле
код решения на fastai похож на код для XGBoost, который мы видели в той
же главе.
На рис. 8.8 показан вывод метода fit. Для каждой эпохи перечислены потери на
обучении и на валидации и доля верных результатов. Если сравнить результаты
обучения, показанные на рис. 8.4 (когда значения целевого столбца не были явно
преобразованы в строковые), с результатами обучения, показанными на рис. 8.8
(значения целевого столбца были преобразованы в строковые), становится очевидно, что мы получаем лучшие результаты, когда fastai рассматривает задачу
как проблему классификации, а не регрессии.
Номер
эпохи
Значение
потерь
на обучении
Значение
Доля
Затраченное
потерь
верных
время
на валидации результатов на эпоху
Рис. 8.8. Вывод оператора fit
Прежде чем продолжить разбор решения на fastai, давайте уделим немного
времени обсуждению взаимосвязи между потерями на обучении, валидации
и тесте. На рис. 8.8 показаны потери на обучении и валидации для каждой
эпохи. Если потери на обучении ниже, чем на валидации, это указывает на
8.2. PyTorch с fastai 377
недообучение1 или на то, что методы регуляризации, которые применяются
только во время обучения (например, dropout), оказывают чрезмерное влияние. На рис. 8.8 видно, что потери на валидации ниже потерь на обучении в первую эпоху. Для последующих эпох потери на обучении снижаются быстрее,
чем на валидации, и к последней эпохе становятся ниже потерь на валидации.
Листинг 8.8 подтверждает, что fastai рассматривает задачу как проблему классификации, поскольку функция потерь — CrossEntropyLoss() — подходит для
этих целей.
Листинг 8.8. Функция потерь, используемая при обучении модели
learn.loss_func
FlattenedLoss of CrossEntropyLoss()
Вывод метода, показывающего функцию потерь,
использованную при обучении модели fastai
Метод, возвращающий
функцию потерь
Вывод, показанный в листинге 8.8, подтверждает, что после преобразования
значений целевого столбца в строковые мы получаем нужную функцию потерь.
Теперь давайте посмотрим, какие слои fastai определяет для модели. Листинг 8.9
показывает метод summary(), который позволяет это сделать.
Листинг 8.9. Метод для получения списка слоев модели fastai
learn.summary()
Результат выполнения листинга 8.9 показан на рис. 8.9, который отображает
вывод инструкции summary(), включая слои, составляющие модель, а также
количество параметров в ней и используемые обратные вызовы.
Теперь, когда мы рассмотрели ключевые фрагменты кода в решении на fastai,
давайте вернемся к диаграмме стека, чтобы разобраться, какое место в ней
занимает fastai. На рис. 8.10 показан стек глубокого обучения для табличных
данных из примера текущего раздела. Обратите внимание, что на рисунке fastai
показан и как высокоуровневый API, и как библиотека для табличных данных,
поскольку выполняет обе эти функции.
Мы рассмотрели код для fastai, и в следующем разделе сравним его с решением
на Keras, которое разобрали ранее в главе 3.
1
Возможно, у автора ошибка и должно быть «указывает на переобучение». Если бы
потери на обучение были выше, чем на валидации, то это бы указывало на недообучение. — Примеч. науч. ред.
378 Глава 8. С чего начать?
Рис. 8.9. Вывод метода summary()
8.2. PyTorch с fastai 379
Экосистема PyTorch
Низкоуровневый
фреймворк
PyTorch
Высокоуровневый
API
fastai
Библиотека
табличных данных
fastai
Рис. 8.10. Стек для PyTorch с fastai
8.2.2. Сравнение решений на fastai и Keras
Мы рассмотрели два решения с использованием глубокого обучения для прогнозирования цен на жилье Airbnb NYC: на Keras и fastai. В этом разделе сравним
их и рассмотрим плюсы и минусы каждого.
Решения на fastai и Keras представляют интерес для сравнения, поскольку они
кардинально различаются. Решение на Keras содержит много пользовательского
кода, и все детали явно прописаны. Фреймворк fastai сам определяет детали
модели на основе данных и делает предположения о значениях по умолчанию,
что позволяет создать рабочую модель практически без явного указания параметров. Преимущество в том, что код fastai намного компактнее кода Keras.
В частности, решение на Keras требует множества строк для описания пайплайна и слоев модели. В fastai мы получаем готовый пайплайн, просто указывая
нужные преобразования входных данных (как показано в листинге 8.6), и не
надо явно определять слои модели. Обратной стороной такой компактности
является то, что могут возникнуть неочевидные на первый взгляд проблемы, поэтому следует быть внимательным. В предыдущем разделе мы видели, что если
явно не преобразовать значения целевого столбца в строковые, то fastai будет
интерпретировать их как непрерывные и предполагать, что мы хотим обучить
регрессор, а не классификатор.
В табл. 8.1 приведено сравнение плюсов и минусов решений на Keras и fastai для
задачи Airbnb NYC. Если сравнить производительность двух моделей, то первая
достигает доли верных результатов от 70 до 74 %, тогда как вторая стабильно
дает около 81 %.
Рассмотрим еще один аспект сравнения решений на Keras и fastai: базовый
низкоуровневый фреймворк глубокого обучения. Для Keras таковым является TensorFlow, тогда как fastai построен на PyTorch. Это означает, что мы
380 Глава 8. С чего начать?
рассмотрели решения глубокого обучения для задач с табличными данными,
использующими оба основных фреймворка.
Таблица 8.1. Сравнение плюсов и минусов решений на Keras и Fastai
Плюсы
Keras
fastai
Детали модели прозрачны.
Фреймворк включает в себя явную поддержку моделей для табличных данных,
что означает бˆольшую компактность кода.
Большое сообщество, использующее фреймворк, а значит, будет
легче найти решения похожих
задач
Минусы
Нет встроенной поддержки
табличного формата данных,
а значит, нужно писать свой код
для определения пайплайна
и слоев модели
Автоматически определяет пайплайн.
Включает в себя удобные функции, которые
позволяют легко изучить датасет
Автоматически сделанные предположения
могут приводить к неожиданным проблемам, которые трудно отлаживать.
Сообщество пользователей невелико
и имеет меньший опыт развертывания
в продакшене
Одно из сходств между Keras и fastai заключается в том, что они оба являются
универсальными высокоуровневыми API. Мы увидели, что оба они могут
использоваться для решения задач анализа табличных данных, равно как и с
другими типами данных, а не только с табличными. В следующем разделе мы
изучим библиотеку глубокого обучения, специально созданную для анализа
табличных данных: TabNet. Мы разберем решение задачи Airbnb NYC с использованием TabNet, а затем сравним его с решением на Keras.
8.3. PyTorch с TabNet
Два рассмотренных нами ранее инструмента разработаны как универсальные
библиотеки глубокого обучения. Теперь мы опробуем специализированную
библиотеку для табличных данных: TabNet. Не будем повторяться и сосредоточимся только на тех частях решения, которые отличаются от предыдущих
примеров. Код можно найти по адресу https://mng.bz/oK1Z.
8.3.1. Ключевые особенности кода решения на TabNet
В этом разделе мы рассмотрим основные части кода, составляющие решение на
TabNet для задачи прогнозирования цен на жилье Airbnb NYC. Нам потребуется
набор импортов, как показано в листинге 8.10.
8.3. PyTorch с TabNet 381
Листинг 8.10. Импорт библиотек для TabNet
Устанавливает реализацию TabNet для PyTorch
! pip install pytorch-tabnet
Импортирует библиотеку torch
import torch
from pytorch_tabnet.tab_model import TabNetClassifier
Импортирует библиотеку TabNetClassifier. Мы будем
использовать ее для определения модели
Обратите внимание, что, в отличие от fastai, инструкции для TabNet в листинге 8.10 включают явное указание на импорт библиотеки PyTorch — torch.
В отличие от решения на fastai, в котором нет явного кода для определения пайп
лайна и есть уникальный код для определения датасета, решение на TabNet использует тот же код, что и Keras и XGBoost, вплоть до той части, где определяются
пайплайны. После этого на TabNet применяется код, аналогичный XGBoost, для
преобразования списка массивов NumPy, полученного из пайплайна, в массив
списков1 NumPy, как показано в листинге 8.11.
Листинг 8.11. Инструкции для генерации массивов NumPy из списков
list_of_lists_train = []
Определяет списки списков для обучающего,
list_of_lists_test = []
валидационного и тестового датасетов
list_of_lists_valid = []
(по одному списку для каждого)
for i in range(0,7):
list_of_lists_train.append(X_train_list[i].tolist())
Преобразует обучающий
list_of_lists_valid.append(X_valid_list[i].tolist())
список списков в массив
list_of_lists_test.append(X_test_list[i].tolist())
NumPy из списков
X_train = np.array(list_of_lists_train).T
X_valid = np.array(list_of_lists_valid).T
Преобразует валидационный
X_test = np.array(list_of_lists_test).T
список списков в массив
y_train = dtrain.target
NumPy из списков
y_valid = dvalid.target
Определяет переменные для
y_test = test.target
Преобразует тестовый список списков
обучающего, валидационного
и тестового наборов
с целевыми значениями
в массив NumPy из списков
Преобразования, показанные в листинге 8.11, необходимы, поскольку TabNet
ожидает, что входные данные для модели будут в форме массива NumPy из
списков. Далее решение TabNet включает код для определения модели.
Определение модели, показанное в листинге 8.12, задает набор гиперпараметров,
включая оптимизатор и скорость обучения. Далее решение на TabNet включает
код для обучения модели.
1
Двумерный массив. — Примеч. науч. ред.
382 Глава 8. С чего начать?
Листинг 8.12. Код для определения модели на TabNet
Определяет объект TabNetClassifier как
модель и задает оптимизатор Adam
Задает скорость
tb_cls = TabNetClassifier(optimizer_fn=torch.optim.Adam,
обучения модели
optimizer_params=dict(lr=1e-3),
scheduler_params={"step_size":10,"gamma":0.9},
scheduler_fn=torch.optim.lr_scheduler.StepLR, Задает параметры
mask_type='entmax' # "sparsemax"
скорости обучения
)
Листинг 8.13. Инструкции для обучения модели TabNet
Задает валидационный датасет
Задает обучающий датасет
tb_cls.fit(X_train, y_train,
eval_set=[(X_train, y_train),(X_valid, y_valid)],
eval_name=['train', 'valid'],
Задает метки для
eval_metric=['accuracy'],
результатов обучения
max_epochs=10, patience=3,
и валидации
batch_size=28, drop_last=False)
Задает размер батча
Задает количество эпох
в тренировочном цикле и количество
эпох, которые нужно выполнить после
того, как модель перестанет улучшаться
Задает метрику
для отслеживания
эффективности
обучения
Инструкция в листинге 8.13, определяющая обучение модели TabNet, включает
настройку ранней остановки, в том числе параметр patience, который указывает,
сколько эпох будет продолжаться процесс, после того как модель перестанет
улучшаться.
Вывод метода обучения показывает результаты каждой эпохи, включая потери
и доли верных результатов на обучении и валидации, а также результат ранней
остановки (рис. 8.11).
Рис. 8.11. Вывод метода fit
8.3. PyTorch с TabNet 383
В выходных данных обучающего прогона, показанных на рис. 8.11, запускается
максимальное количество эпох (10), поскольку точность на валидации не перестает улучшаться дольше чем в течение двух эпох до достижения максимального
их количества. Это означает, что порог patience=3, установленный в методе fit,
никогда не превышается, поэтому обучение продолжается в течение максимального количества эпох max_epochs. На рис. 8.12 показан стек глубокого обучения
для табличных данных из примера в этом разделе.
Экосистема PyTorch
Низкоуровневый
фреймворк
PyTorch
Библиотека для
табличных данных
TabNet
Рис. 8.12. Стек для PyTorch
с TabNet
Теперь, когда мы разобрали код решения на TabNet, в следующем разделе сравним его с решением на Keras, которое было приведено в главе 3.
8.3.2. Сравнение решений на TabNet и Keras
Мы рассмотрели три решения на основе глубокого обучения для задачи прогнозирования цен на жилье Airbnb NYC: на Keras, fastai и TabNet. В этом разделе
мы сравним решения на Keras и TabNet и рассмотрим плюсы и минусы каждого
из них.
Решения на Keras и на TabNet интересно сравнить, поскольку они демонстрируют некоторые сильные и слабые стороны своих базовых фреймворков, TensorFlow и PyTorch. Решение на Keras выигрывает от простого метода
summary(), который предоставляет компактный список слоев модели. В PyTorch
такая элегантная функция отсутствует. С другой стороны, Keras не предоставляет встроенного контроля за процессом обучения, поэтому нужно определить
обратные вызовы, чтобы гарантировать сохранение оптимальной модели по
окончании обучения и избежать бесполезной траты ресурсов на эпохи, когда
модель перестала улучшаться. PyTorch, в свою очередь, по умолчанию включает раннюю остановку и сохранение оптимальной модели, поэтому решение
на TabNet не требует явного определения обратных вызовов для оптимизации
обучения. В табл. 8.2 представлен обзор преимуществ и недостатков решений
на Keras и на TabNet с PyTorch для задачи Airbnb NYC.
В этом разделе мы рассмотрели решение для прогнозирования цен Airbnb NYC
для PyTorch TabNet, а в следующем разберем последний вариант — PyTorch
с Lightning Flash.
384 Глава 8. С чего начать?
Таблица 8.2. Сравнение плюсов и минусов решений на Keras и на TabNet
Keras
TabNet
Плюсы
Большое сообщество, использующее Простой код для определения и обучефреймворк, а значит, легче найти
ния модели.
решения похожих задач.
Не требует явного задания обратного
Простой код для просмотра сведевызова для ранней остановки
ний о слоях модели
Минусы
Нет встроенной поддержки таб
личного формата данных, а значит,
нужно писать свой код для определения модели
Процесс обучения намного медленнее.
Обучение модели Keras занимает примерно 20 сек, тогда как аналогичная
модель на TabNet обучается примерно
4 мин.
Нет удобной возможности увидеть сводный отчет о структуре модели
8.4. PyTorch с Lightning Flash
До сих пор мы рассматривали решения на Keras, fastai и TabNet PyTorch для
задачи прогнозирования цен Airbnb NYC. Теперь давайте перейдем к нашему
последнему стеку: Lightning Flash. Как платформа, разработанная для быстрого
прототипирования, создания базовой модели и тонкой настройки, наряду с понятным API и проработанной документацией, Lightning Flash имеет потенциальные преимущества по сравнению со стеками, которые мы исследовали до сих пор.
Код для этого решения можно найти по адресу https://mng.bz/vKnp. На рис. 8.13
показаны fastai и Tabnet на стеках PyTorch.
Экосистема PyTorch
Низкоуровневый
фреймворк
PyTorch
Высокоуровневый
API
fastai
Библиотека для
табличных данных
fastai
Рис. 8.13. fastai и Tabnet на стеках PyTorch
Экосистема PyTorch
PyTorch
TabNet
8.4. PyTorch с Lightning Flash 385
8.4.1. Ключевые особенности решения на Lightning Flash
Код для решения на Lightning Flash имеет множество отличий от ранее рассмотренных решений. В этом разделе мы пройдемся по блокноту обучения модели
(https://mng.bz/4aDR), чтобы выделить наиболее интересные моменты.
Для работы в Colab Lightning Flash требует набора установок, выполненных
в определенном порядке, как показано в листинге 8.14. Источник этого списка —
https://mng.bz/QDP6.
Листинг 8.14. Необходимые установки для работы Lightning Flash в Colab
!pip install torch==1.8.1+cu102 -f
https://download.pytorch.org/whl/torch_stable.html
Серия команд pip install для
!pip install icevision #==0.9.0a1
получения необходимых
!pip install effdet
уровней PyTorch Lightning
!pip install lightning-flash[image]
!pip install git+https://github.com/PyTorchLightning/lightning-flash.git
!pip install torchtext==0.9.1
!pip uninstall fastai -y
Чтобы исключить потенциаль!curl https://raw.githubusercontent.com/airctic/ \
ные конфликты между fastai
icevision/944b47c5694243ba3f3c8c11a6ef56f05fb111eb/ \
и Lightning Flash, необходимо
icevision/core/record_components.py –output \
удалить fastai
/usr/local/lib/python3.7/dist- \
packages/icevision/core/record_components.py
Вручную применяет
исправление ошибки
в текущей версии icevision
Листинг 8.14 включает длинный список установок (и одно удаление), необходимых для работы Lightning Flash в Colab. Из опыта мы знаем, что выполнить эти
установки необходимо, иначе возникнут конфликты между версиями библиотек,
которые требуются для Lightning Flash, и версиями, установленными в Colab
по умолчанию. Далее импортируем библиотеки, необходимые для Lightning
Flash (листинг 8.15).
Листинг 8.15. Импорт библиотек, необходимых для Lightning Flash
Импортирует библиотеку torch
import torch
Импортирует библиотеку flash
import flash
from flash.tabular import TabularClassificationData
from flash.tabular import TabularClassifier
Импортирует объекты,
необходимые для модели
классификации табличных данных
Обратите внимание, что инструкция импорта torch в листинге 8.15 точно такая
же, как и для TabNet в листинге 8.11.
386 Глава 8. С чего начать?
Далее мы определяем параметры датасета, которые будем использовать для
обучения модели (листинг 8.16).
Листинг 8.16. Настройка параметров датасета
Устанавливает целевое поле
в качестве значения, которое будет
предсказывать обученная модель
dep_var = 'target'
cat=['neighbourhood_group','neighbourhood','room_type']
cont = ['minimum_nights','number_of_reviews',
'reviews_per_month','calculated_host_listings_count']
Определяет список
категориальных
признаков
Определяет список
непрерывных
признаков
Определения в листинге 8.16 похожи на аналогичный блок кода в решении на
fastai (листинг 8.3), где мы задавали целевую переменную вместе со списками
для категориальных и непрерывных признаков.
Далее возьмем только что определенные значения для создания объекта
TabularClassificationData (листинг 8.17). Он задает минимальные характеристики датасета, которые мы будем использовать для обучения.
Листинг 8.17. Определение объекта TabularClassificationData
datamodule = TabularClassificationData.from_csv(
categorical_fields=cat,
numerical_fields=cont,
target_fields="target",
train_file='../data/train.csv',
val_file='../data/valid.csv',
predict_file='../data/test.csv',
batch_size=64
)
Определяет категориальные признаки
Определяет непрерывные признаки
Определяет целевой признак
Определяет обучающий датасет
Определяет валидационный датасет
Определяет тестовый датасет
Следует пояснить, почему определение объекта TabularClassificationData,
показанное в листинге 8.17, использует отдельные файлы CSV для обучающего, валидационного и тестового датасетов. Во всех других решениях набор
данных загружается из файла pickle, который является результатом работы кода
для очистки данных (https://mng.bz/XxN9), а затем разделяется на обучающую,
валидационную и тестовую выборки в коде для обучения модели. Решение на
Lightning Flash отличается тем, что для каждого сегмента набора данных используются отдельные файлы CSV. Причина в том, что очень специфические
требования к установке, показанные в листинге 8.14, оказались несовместимы
с загрузкой файла pickle, который содержит итоговый датафрейм, полученный
после выполнения кода для очистки данных. В качестве обходного пути мы загрузили этот файл pickle в отдельном блокноте в pandas DataFrame и сохранили
отдельные файлы CSV для обучения, валидации и тестирования, что вы и видите
в определении объекта TabularClassificationData в листинге 8.17. В качестве
8.4. PyTorch с Lightning Flash 387
упражнения можете обновить блокнот с кодом для очистки данных решения на
Lightning Flash, чтобы он сохранял очищенный датасет в виде трех отдельных
CSV-файлов, а не в виде одного файла pickle.
Теперь, когда мы указали детали о датасете, мы готовы определить и обучить
модель, как показано в листинге 8.18.
Листинг 8.18. Настройка параметров датасета
model = TabularClassifier.from_data(datamodule,
learning_rate=0.1)
trainer = flash.Trainer(max_epochs=3,
gpus=torch.cuda.device_count())
trainer.fit(model, datamodule=datamodule)
Определяет модель с помощью
объекта TabularClassifierData,
созданного в листинге 8.18
Определяет объект Trainer
Обучает модель
Код для обучения модели из листинга 8.18 генерирует выходные данные, показанные на рис. 8.14.
Рис. 8.14. Выходные данные процесса обучения Lightning Flash
Обратите внимание, что выходные данные включают доли верных результатов
на валидации и обучении. На рис. 8.15 показан стек глубокого обучения на табличных данных из примера в этом разделе.
Код решения на Lightning Flash включает в себя несколько весьма элегантных
идей, таких как возможность указать обучающий, валидационный и тестовый
наборы данных в одном объекте, где определяются общие характеристики датасета. В целом API для Lightning Flash прост для понимания. К сожалению,
его преимущества сводятся на нет из-за необходимости соблюдать строгие
388 Глава 8. С чего начать?
требования для запуска в Colab. В противном случае Lightning Flash мог бы
стать одним из фаворитов у разработчиков, поскольку сочетает простоту fastai
с понятностью Keras.
Экосистема PyTorch
Низкоуровневый
фреймворк
PyTorch
Высокоуровневый
API
Lightning
Библиотека для
табличных данных
Lightning Flash
Рис. 8.15. Стек PyTorch
с Lightning Flash
8.4.2. Сравнение решений на Lightning Flash и Keras
Мы рассмотрели четыре решения на основе глубокого обучения для задачи
прогнозирования цен на жилье Airbnb NYC: на Keras, fastai, TabNet и, наконец,
на Lightning Flash. В этом разделе сравним решения на Keras и Lightning Flash
и оценим плюсы и минусы каждого.
Мы увидели, что Lightning Flash имеет некоторые реальные преимущества для
быстрого и простого развертывания решения. Однако отсутствие проторенного
пути использования этой библиотеки в Colab вызывает неудобства, и стоит
задаться вопросом, как долго сложный набор установок, показанный в листинге 8.15, будет необходимым условием для выполнения экспериментов с ней
в Colab. Таблица 8.3 показывает сильные и слабые стороны решений на Keras
и Lightning Flash для нашей задачи с Airbnb NYC.
Таблица 8.3. Сравнение плюсов и минусов решений на Keras и Lightning Flash
Плюсы
Keras
Lightning Flash
Большое сообщество,
использующее фреймворк,
а значит, легче найти решения похожих задач.
Простой код для определения и обучения модели.
Простой код для просмотра
сведений о слоях модели
Минусы
Нет встроенной поддержки
табличного формата данных,
а значит, нужно писать свой
код для определения модели
Не требует явного задания пайплайна, нужно
только указать столбцы с категориальными и непрерывными данными
Для работы в Colab требуется весьма нетривиальный порядок установки. Видимо, не является
популярным среди пользователей Colab.
Без специальной настройки доля верных результатов на тестовых данных оказалась наихудшей
8.5. Сравнение всех стеков 389
В этой главе мы применили несколько стеков глубокого обучения для анализа
табличных данных на примере решения задачи прогнозирования цен Airbnb NYC
и оценили плюсы и минусы каждого из них. В следующем разделе сравним все
стеки, которые продемонстрировали ранее.
8.5. Сравнение всех стеков
Мы рассмотрели в общей сложности четыре стека глубокого обучения на табличных данных. Таблица 8.4 обобщает сведения об их производительности
в стандартной среде Colab на графическом процессоре, предназначенном для
глубокого обучения.
Таблица 8.4. Сравнение доли верных результатов и времени выполнения глубокого
обучения с табличными стеками (плюс XGBoost) для задачи прогнозирования цен на
жилье Airbnb NYC
Доля верных результатов
на тестовых данных
Время выполнения кода,
в секундах
TensorFlow с Keras
81 %
16
fastai с PyTorch
83 %
69
TabNet с PyTorch
81 %
568
Lightning Flash с PyTorch
Нет данных
14
XGBoost
79 %
14
Здесь важно отметить, что для целей этого сравнения мы не проводили тонкую
настройку ни одного из подходов. Мы хотели получить чистое сравнение того, как
они соотносятся с базовым решением на Keras без дополнительной оптимизации.
В последующих главах мы обсудим некоторые методы, которые можно применить для получения оптимальных результатов на основе глубокого обучения.
По доле верных результатов fastai немного лучше своих конкурентов. По времени выполнения лучший результат у Lightning Flash, а TabNet — самый медленный. Так какой же стек лучше всего использовать для табличных данных?
Прежде всего сравним XGBoost с решениями на основе глубокого обучения:
что лучше? Мы ответим на этот вопрос более подробно в главе 9, но уже сейчас
можем сказать, что если сравнивать глубокое обучение и градиентный бустинг,
то для простоты и общей производительности «из коробки» такие подходы, как
XGBoost, в настоящее время лучше любых решений с глубоким обучением для
большинства датасетов. Однако существуют сценарии использования, когда есть
смысл присмотреться к нейросетям, и мы рассмотрим их в главе 9.
390 Глава 8. С чего начать?
Если вы все же решите использовать глубокое обучение для анализа табличных
данных, какой вариант из четырех, которые мы рассмотрели, следует выбрать?
Вот наши общие рекомендации.
Если вы новичок в глубоком обучении и в первую очередь заинтересованы
в исследовании без необходимости немедленной реализации в продакшене,
fastai — лучший выбор. Это самый простой в использовании стек, и у него достаточно большое сообщество пользователей, так что вы вряд ли столкнетесь
с проблемой, о которой никто раньше не слышал. Fastai включает множество
удобных функций, упрощающих работу с табличными данными, поэтому
вы сможете быстро прототипировать свое решение. Однако если вам нужно
быстро перенести свое решение в продакшен, fastai не лучший выбор; для
этого он обычно не используется.
Если вы уже освоили основные методы глубокого обучения и вам нужно запустить приложение в продакшен, мы рекомендуем стек Keras. Во-первых,
TensorFlow, его низкоуровневый компонент, является наиболее часто используемым фреймворком глубокого обучения в индустрии. Во-вторых,
у Keras огромное сообщество пользователей. Хотя у него пока нет собственной поддержки табличных данных, как у fastai, он обладает достаточным
функционалом, чтобы подходить для задач подобного рода.
Эти рекомендации могут измениться по мере того, как все больше людей будут
использовать глубокое обучение для работы с табличными данными. Какой-либо
другой стек, например TabNet на PyTorch, может быть доработан и может стать
отличным выбором для таких задач. Однако, учитывая текущее состояние дел,
мы рекомендуем fastai для новичков и исследователей, а Keras — для тех, кто
более опытен и кому нужно быстро развернуть модель в продакшене.
В следующем разделе обсудим стеки, которые не рассматривали в этой главе.
8.6. Стеки, которые остались за кадром
Вы можете спросить, почему мы выбрали три конкретных стека (fastai, PyTorch
TabNet и Lightning Flash) для решения задачи Airbnb NYC и не исследовали
другие варианты, такие как TabNet на TensorFlow, SAINT или PyTorch Tabular.
В этом разделе мы ответим на вопрос. На рис. 8.16 показаны не исследованные
нами стеки для глубокого обучения на табличных данных.
У стеков, оставшихся за кадром, есть несколько общих особенностей.
Все они включают в себя специализированные библиотеки табличных данных.
Нам не удалось запустить ни один из них в Colab — среде, которую мы использовали для выполнения примеров кода в этой книге.
8.6. Стеки, которые остались за кадром 391
Экосистема TensorFlow
Низкоуровневый
фреймворк
Высокоуровневый
API
Библиотека для
табличных данных
Экосистема PyTorch
TensorFlow
PyTorch
Keras
Lightning
fastai
TabNet
SAINT
DeepTables
PyTorch Tabular
TabNet
fastai
Lightning Flash
Рис. 8.16. Стеки, которые мы не исследовали
Невозможность запуска этих стеков в Colab может быть вызвана рядом причин.
Теоретически могут быть некоторые ограничения в самой среде Colab. Однако
она очень часто используется для исследований, поэтому то, что не удается применить в ней библиотеку, — очень нехороший знак. Все стеки содержали пример
«hello world» для тестирования, и для всех неисследованных стеков этот пример
выдавал ошибки, в основном связанные с конфликтующими зависимостями
библиотек Python. Возможно, если бы мы проявили больше терпения или изу
чили вопрос глубже, то смогли бы устранить ошибки и запустить базовые примеры в Colab. Стеки, которые мы исследовали (Keras, fastai и PyTorch TabNet),
работали в Colab «из коробки». С другой стороны, Lighting Flash потребовал
некоторой дополнительной настройки перед запуском в Colab.
Вот разница между Lightning Flash и неисследованными стеками: было ясно,
что другие люди уже пытались запустить Lightning Flash в Colab, и мы могли
найти неофициальную документацию, которая показывала, что именно нужно
сделать для его работы в этой среде. Если ваша цель — решить задачу анализа
табличных данных с помощью глубокого обучения, вам нужно сосредоточиться
на проблеме, а не на манипуляциях с conda и pip, чтобы избежать несовместимостей. По этому критерию Keras, fastai, PyTorch TabNet и, в меньшей степени,
Lightning Flash являются приемлемыми вариантами для работы в Colab. SAINT,
DeepTables, PyTorch Tabular и TabNet на TensorFlow таковыми не являются,
потому что они не запустятся сразу, а инструкций по их настройке либо не существует, либо их трудно найти.
392 Глава 8. С чего начать?
Хотя немного разочаровывает, что нам не удалось реализовать задачу прогнозирования цен Airbnb NYC с бˆольшим количеством специализированных
библиотек, мы все же смогли достичь цели этой главы. На рис. 8.17 показаны
все возможности, которые нам удалось изучить.
Экосистема TensorFlow
Низкоуровневый
фреймворк
Высокоуровневый
API
Библиотека для
табличных данных
Экосистема PyTorch
TensorFlow
PyTorch
Keras
Lightning
fastai
TabNet
SAINT
DeepTables
PyTorch Tabular
TabNet
fastai
Lightning Flash
Рис. 8.17. Стеки, которые мы исследовали
Стеки, которые мы рассмотрели в этой главе, а также TensorFlow с Keras, предоставляют хорошо продуманный набор возможностей.
TensorFlow с Keras — это надежный стек с огромным сообществом пользователей. Вы можете быть уверены, что с любой проблемой, с которой вы
столкнулись в процессе использования этого стека, кто-то другой уже имел
дело и опубликовал решение. Стек безупречно работает в Colab, что упрощает
начальные исследования. TensorFlow с Keras часто используется в продакшене для всех видов приложений. С другой стороны, Keras не имеет встроенной
поддержки табличных данных, поэтому нужно быть готовым написать некий
объем кастомного кода для решения своей задачи.
PyTorch с fastai разработан для простоты старта и безупречно работает
в Colab, что позволяет быстро и без лишних сложностей создавать прототипы
решений для задач с табличными данными, на которые ориентирован fastai.
Вы получаете встроенную поддержку для работы с категориальными и непрерывными признаками, и не нужно беспокоиться о ручном создании пайп
лайна: при передаче данных обученной модели для получения прогноза они
Итоги 393
автоматически проходят те же преобразования, что и данные, используемые
для обучения модели. С другой стороны, за простоту кода приходится платить. Автоматизированные шаги, которые выполняет fastai, могут привести
к некоторым трудно отлаживаемым проблемам (например, та, которую мы
описали в этой главе, когда при отсутствии явного преобразования значений
целевого столбца в строковый тип обучается не та модель). И если вы хотите
выйти за рамки стандартных возможностей fastai, потребуется глубокое погружение в его API.
PyTorch с TabNet выделяется среди библиотек, предназначенных для анализа
табличных данных, тем, что беспроблемно работает в Colab. Как и в случае
с fastai, с TabNet вы можете определить и обучить модель глубокого обучения на табличных данных всего несколькими строками кода. В отличие от
fastai, TabNet использует обычные API, которые легко понять любому, кто
использовал scikit-learn. По сравнению с другими стеками, TabNet потребовал
больше времени для обучения модели. Кроме того, TabNet — это библиотека,
специально разработанная для табличных данных, и у нее меньшее сообщество пользователей, чем у fastai и Keras. Если вы столкнетесь с проблемами,
то вероятность того, что кто-то другой уже решил их и задокументировал на
Stack Overflow, будет ниже.
PyTorch с Lightning Flash быстро обучает модель и имеет простой API, если
только вы сможете все это запустить. У Lightning большое сообщество — не
такое большое, как у Keras, но больше, чем у fastai. Однако на Colab ниша
использования Lightning Flash для решения табличных задач не столь велика,
и нам с трудом удалось запустить его в этой среде.
В этой главе мы изучили набор стеков для глубокого обучения на табличных
данных, сравнили плюсы и минусы каждого подхода и обсудили, почему мы
оставили некоторые другие стеки неисследованными. В следующей главе рассмотрим лучшие практики глубокого обучения на табличных данных.
Итоги
Существуют два низкоуровневых фреймворка для глубокого обучения:
TensorFlow и PyTorch.
TensorFlow чаще используется в продакшене.
PyTorch — это преобладающий фреймворк для исследований.
Keras — это высокоуровневый API для TensorFlow.
fastai — это одновременно универсальный высокоуровневый API для PyTorch
и библиотека для табличных данных.
394 Глава 8. С чего начать?
PyTorch Lightning — это высокоуровневый API, который абстрагирует некоторые сложности PyTorch.
Lightning Flash — это библиотека для табличных данных, основанная на
Lightning.
Lightning Flash и fastai предоставляют некоторые из тех же преимуществ для
PyTorch, которые Keras дает для TensorFlow, абстрагируя аспекты базового
фреймворка PyTorch.
TabNet — это библиотека для табличных данных, доступная как для
TensorFlow, так и для PyTorch.
SAINT — это библиотека на TensorFlow для табличных данных.
PyTorch Tabular — это библиотека на PyTorch для табличных данных.
Из всех доступных приемлемыми являются варианты TensorFlow с Keras,
PyTorch с fastai, PyTorch с TabNet и PyTorch с Lightning Flash для глубокого
обучения на табличных данных в Colab.
9
Лучшие практики
глубокого обучения
В этой главе
3 Знакомство с данными о недвижимости Куала-Лумпура
3 Обработка датасета
3 Определение модели глубокого обучения
3 Обучение модели глубокого обучения
3 Применение модели глубокого обучения
В главе 8 мы рассмотрели набор инструментов для глубокого обучения на таб
личных данных. В этой главе мы используем один из этих стеков, Keras, для
изучения лучших практик, включая подготовку данных, разработку и обучение
модели. Мы представляем новую задачу для демонстрации всех этих лучших
техник: прогнозирование того, будет ли цена объекта недвижимости в КуалаЛумпуре выше или ниже медианной на рынке. Мы выбрали этот датасет, потому
что он более сложный и его труднее подготовить, чем Airbnb NYC, который мы
использовали до сих пор. Следовательно, это позволит нам более наглядно продемонстрировать широкий спектр методов глубокого обучения в применении
к табличным данным.
396 Глава 9. Лучшие практики глубокого обучения
Если вы новичок в деле глубокого обучения, примеры в этой главе помогут вам
освоить лучшие практики. Если у вас уже есть большой опыт, эта глава может
быть полезна в качестве повторения основных принципов.
9.1. Знакомство с данными о недвижимости
Куала‑Лумпура
В этой главе мы будем использовать данные о недвижимости Куала-Лумпура,
столицы Малайзии. Датасет состоит из записей, описывающих проданные объекты. На рис. 9.1 представлена небольшая выборка из него, полученная с помощью метода df.head(). Код, приведенный в этой главе, находится по адресу
https://mng.bz/yWQp.
Рис. 9.1. Выборка из датасета о недвижимости Куала-Лумпура
В следующем разделе мы рассмотрим шаги, которые нужно предпринять для
очистки каждого из столбцов. Но сначала давайте разберемся, что находится
в каждом из них.
9.1. Знакомство с данными о недвижимости Куала‑Лумпура 397
Location — район, в котором расположен объект недвижимости.
Price — указанная цена объекта в ринггитах (с обозначением RM, общепри-
нятым символом малайзийской валюты).
Rooms — количество комнат в объекте недвижимости. Значения типа «2 + 1»
в этом столбце означают, что имеются две спальни и одна комната, которая
не может быть классифицирована как спальня.
Bathrooms — количество ванных комнат.
Car Parks — количество парковочных мест на территории.
Property type — категория недвижимости, например «Condominium» (Кон-
доминиум), «Serviced Residence» (Обслуживаемое жилое здание) и т. д.
Size — размеры объекта. Значения в этом столбце могут относиться к различ-
ным аспектам, включая общую площадь земельного участка или застроенную
площадь в пределах собственности.
Furnishing — указание на наличие мебели.
Вот один из основных вопросов, на который нам нужно ответить при работе
с этим датасетом: какие столбцы содержат непрерывные данные и какие — категориальные. Изучив рис. 9.1, можем заметить подмножество столбцов, содержащих
числовые значения. Давайте внимательнее его рассмотрим, чтобы определить,
какие столбцы содержат непрерывные данные. На рис. 9.2 показаны значения
из подмножества, по-видимому, числовых столбцов.
Рис. 9.2. Подмножество столбцов, которые выглядят так, как будто содержат
числовые данные
Мы можем проверить, какие из этих столбцов содержат числовые данные, с помощью следующей команды:
df.describe()
Эта команда возвращает описательную статистику для всех числовых столбцов
в DataFrame, предоставляя информацию о распределении данных, центральной
398 Глава 9. Лучшие практики глубокого обучения
тенденции и разбросе в каждом числовом столбце. Изучив вывод этой команды,
показанный на рис. 9.3, вы можете определить, какие столбцы действительно
содержат числовые значения.
Рис. 9.3. Вывод команды describe()
На рис. 9.3 видно, что только Bathrooms и Car Parks являются числовыми, а Price,
Rooms и Size нет, хотя содержат данные, которые выглядят как числа. В следую
щем разделе в процессе обработки датасета мы опишем шаги по извлечению
числовых данных из признаков Price, Rooms и Size для их последующего использования в обучении модели.
Другой способ определить, какие столбцы являются категориальными или
непрерывными, — это подсчет количества уникальных значений в каждом
столбце. Если их много, это может быть указанием на непрерывные значения,
а если мало, то на категориальные. Фактически признаки с малым числом уникальных значений часто оказываются категориальными, поскольку они обычно
представляют дискретные категории или группы, а не непрерывные числовые
измерения. Это не строгое правило, как мы увидим далее. Выполнение команды
df.nunique() показывает количество уникальных значений в каждом столбце
в датасете (листинг 9.1).
Листинг 9.1 Получение количества уникальных значений в каждом
столбце
counts = df.nunique()
print("количество уникальных значений:\n",counts)
Возвращает количество
уникальных значений в каждом
столбце датафрейма df
9.1. Знакомство с данными о недвижимости Куала‑Лумпура 399
Вывод команды в листинге 9.1 выглядит следующим образом:
количество уникальных значений:
Location
112
Price
4280
Rooms
43
Bathrooms
17
Car Parks
21
Property Type
99
Size
6190
Furnishing
4
dtype: int64
Ограниченное количество значений
в этом столбце подкрепляет наше
интуитивное предположение о том, что
столбец категориальный
Здесь непрерывные данные
Этот столбец требует дальнейшего изучения
Здесь непрерывные данные
Здесь непрерывные данные
Здесь категориальные данные
Здесь непрерывные данные, но они
требуют особого подхода, как мы
увидим далее в этой главе
Здесь категориальные данные
Подведем итог того, что говорит нам о датасете вывод команды в листинге 9.1.
Столбцы, которые, по-видимому, содержат категориальные данные, —
Location, Property Type, Furnishing.
Столбцы, которые, по-видимому, содержат непрерывные данные, — Price,
Bathrooms, Car Parks, Size. Последний нужно дополнительно изучить.
Столбцы, требующие дальнейшего исследования для определения того, следует ли их рассматривать как непрерывные или как категориальные, — Rooms.
Два из этих столбцов требуют дальнейшего анализа: Rooms и Size. В следующем
разделе, посвященном обработке датасета, мы изучим их глубже, чтобы определить, как с ними работать.
Теперь, когда у нас есть представление о столбцах и о том, какую информацию
они отражают, давайте рассмотрим другие аспекты датасета. Сначала проверим
его размеры, как показано в листинге 9.2.
Листинг 9.2. Код для проверки размеров датасета
print("размер ",df.shape)
размер (53883, 8)
Инструкция для получения
размеров входного датафрейма
Вывод инструкции
Листинг 9.2 показывает, что датасет содержит более 53 000 строк и восемь
столбцов. В главе 12 мы рассмотрим взаимосвязь между количеством строк
в таблице, характером столбцов и применимостью модели глубокого обучения
к этим данным. Пока же можно с уверенностью сказать, что, хотя этот датасет
400 Глава 9. Лучшие практики глубокого обучения
и небольшой, он представляет собой неплохой шанс обучить с его помощью
модель глубокого обучения.
Листинг 9.3 содержит методы, позволяющие узнать количество пропущенных
значений в каждом столбце.
Листинг 9.3. Методы для вывода пропущенных значений в каждом столбце
missing_values_count = df.isnull().sum()
print("пропущенные значения перед очисткой:\n",missing_values_count)
Результатом работы этого метода
является подсчет количества
пропущенных значений по столбцам
Ниже приведен вывод команд из листинга 9.3:
пропущенные значения перед очисткой:
Location
0
Location — единственный столбец
Price
248
без пропущенных значений
Rooms
1706
Bathrooms
2013
Car Parks
17567
Столбец с наибольшим количеством
Property Type
25
пропущенных значений — Car Parks
Size
1063
Furnishing
6930
dtype: int64
Из вывода команды в листинге 9.3 мы видим, что все столбцы, кроме одного,
в этом датасете содержат пропущенные значения. Это ранний признак возможных проблем, которые нам нужно будет устранить, чтобы подготовить данные
к обучению. Для сравнения, в датасете Airbnb NYC пропущенные значения были
только в четырех столбцах:
Missing values:
id
name
host_id
host_name
neighbourhood_group
neighbourhood
latitude
longitude
room_type
price
minimum_nights
number_of_reviews
last_review
reviews_per_month
calculated_host_listings_count
availability_365
0
16
0
21
0
0
0
0
0
0
0
0
10052
10052
0
0
Пропущены значения
в столбце name
Пропущены значения
в столбце host_name
Пропущены значения
в столбце last_review
Пропущены значения в столбце
reviews_per_month
9.2. Обработка датасета 401
Как показано здесь, почти в каждом столбце данных о Куала-Лумпуре пропущены какие-то значения. Это предупреждает нас о том, что, как это часто бывает
с реальными данными, предстоит проделать большую работу по их очистке,
прежде чем сможем начать использовать их с моделью.
В этом разделе мы впервые ознакомились с набором данных о недвижимости
Куала-Лумпура. В следующем разделе изучим процесс подготовки этого датасета
для обучения модели глубокого обучения.
9.2. Обработка датасета
Теперь, когда мы рассмотрели набор данных о недвижимости Куала-Лумпура
и увидели, что в нем много пропущенных значений, у нас есть понимание, что
потребуется значительная обработка, прежде чем сможем использовать его
для обучения модели. В этом разделе рассмотрим особенности датасета одну
за другой, чтобы описать необходимые шаги по очистке.
На этом этапе мы еще не решили, будем ли использовать подмножество или полный набор столбцов. Изначально наш подход заключался в использовании всех
доступных признаков. По мере продвижения работы и анализа производительности и поведения модели мы можем решить, что следует исключить какие-то
признаки, потому что они, например, содержат невыявленные недопустимые
значения, которые влияют на производительность. Мы стремимся получить
полный набор очищенных признаков для обучения модели не только потому,
что невозможно заранее предвидеть, какие из них окажутся эффективными, а какие нет. Это также способ извлечь максимальную пользу из данных, поскольку
полная обработка позволяет лучше изучить набор и сделать его пригодным для
повторного использования в других проектах. Этот комплексный подход обес
печивает более высокие результаты, чем те, которые мы получили бы, если бы
очистили только те признаки, которые в итоге используем для обучения модели.
Код, рассмотренный в этом разделе, доступен по адресу https://mng.bz/MDBQ,
а файл конфигурации — по адресу https://mng.bz/av1j.
Начнем с обработки столбцов, в которых требуется только заполнить пропущенные значения: Bathrooms, Car Parks, Furnishing, Property Type, Location.
Далее мы опишем процесс для столбцов, которые нуждаются в более тщательной
очистке, чем просто устранение пропусков: Price, Rooms и Size.
9.2.1. Обработка столбцов Bathrooms, Car Parks, Furnishing,
Property Type и Location
Для подмножества столбцов в датасете (Bathrooms , Car Parks , Furnishing ,
Property Type, Location) можно выполнить очистку, просто обработав пропуски,
402 Глава 9. Лучшие практики глубокого обучения
для замены которых в файле конфигурации содержатся значения по умолчанию.
Мы определим их на основе характеристик столбцов и знаний о предметной
области (листинг 9.4).
Листинг 9.4. Определение значений по умолчанию для замены
пропущенных значений
misc_col_dict: # значения по умолчанию для замены пропусков в некоторых столбцах
Bathrooms: median
Для столбца Bathrooms в качестве
Car Parks: 0
значения по умолчанию устанавливается
Furnishing: unknown_furnishing
медианное значение
Property Type: unknown_property
Location: unknown_location
Для столбца Car Parks устанавливается
Для категориальных столбцов в качестве
значения по умолчанию устанавливается
категория-заполнитель
значение по умолчанию, равное нулю
Листинг 9.4 показывает, что мы установили для пропущенных значений в столбце Bathrooms медианное значение, а для Car Parks установили пропущенные
значения равными нулю. Причина такого различия связана со спецификой
объявлений о недвижимости. В жилых помещениях редко отсутствуют санузлы, поэтому выбор медианного значения для Bathrooms имеет смысл. С другой
стороны, многие объекты недвижимости не имеют парковочных мест. Если же
парковка есть, то в интересах продавца и риелтора указать это в объявлении,
чтобы обеспечить лучшую цену продажи. Таким образом, когда в столбце Car
Parks отсутствует значение, разумно предположить, что у этого объекта нет
парковочных мест, поэтому мы устанавливаем пропущенные значения равными нулю.
В этом словаре у нас также есть уникальные категории-заполнители для категориальных столбцов. Код для получения этой простой очистки размещен
в функции очистки данных clean_up_misc_cols() (листинг 9.5).
Листинг 9.5. Функция для замены пропущенных значений
Выполняет итерацию по столбцам,
для которых выполняется простая
очистка данных
def clean_up_misc_cols(df,misc_col_dict):
for col in misc_col_dict:
if misc_col_dict[col] == 'median':
df[col] = df[col].fillna(df[col].median())
else:
df[col] = df[col].fillna(misc_col_dict[col])
return(df)
Для остальных столбцов заменяет
пропуски значением по умолчанию
для соответствующего столбца
Заменяет пропущенные
значения медианой для
указанного столбца
9.2. Обработка датасета 403
В функции clean_up_misc_cols(), показанной в листинге 9.5, словарь, определенный в файле конфигурации (см. листинг 9.4), используется для замены
пропущенных значений в столбцах, требующих простой очистки.
Следующие подразделы мы посвятим более сложным операциям с данными,
которые требуются для оставшихся трех столбцов: Price, Rooms и Size.
9.2.2. Обработка столбца Price
Прежде чем перейти к тому, что необходимо исправить
в столбце Price, давайте рассмотрим несколько примеров
имеющихся в нем значений (рис. 9.4).
Значения на рис. 9.4 демонстрируют несколько проблем,
которые необходимо устранить в столбце Price:
наличие букв «RM», обозначающих ринггит — малайзийскую валюту;
пропущенные значения;
необходимость преобразования в число с плавающей точкой.
Рис. 9.4.
В листинге 9.6 представлена функция clean_up_price_col(), Примеры
которая содержит код для эффективной очистки столбца значений
Price.
в столбце Price
Листинг 9.6. Функция очистки столбца Price
def clean_up_price_col(df):
df.dropna(subset=['Price'], inplace=True)
Удаляет строки из датафрейма,
df['Price'] = \
в которых пропущено значение
df['Price'].apply(lambda x:\
в столбце Price
remove_currency_symbol("RM ",x))
df['Price'] = \
Удаляет знак валюты
pd.to_numeric(df['Price'].\
str.replace(',',''), errors='coerce')
Удаляет запятые и преобразует значения
return(df)
в число с плавающей точкой
Как показано в листинге 9.6, функция clean_up_price_col() удаляет строки
с пропущенными значениями в столбце Price. Причиной для этого (в отличие
от замены пропусков каким-либо заполнителем) является то, что Price — целевая переменная для нашей модели. Следовательно, не имеет смысла оставлять
строки, где такое значение отсутствует. Результат работы функции clean_up_
price_col() — датафрейм, в котором все значения в столбце Price являются
действительными числами.
404 Глава 9. Лучшие практики глубокого обучения
9.2.3. Обработка столбца Rooms
Прежде чем перейти к очистке столбца Rooms, давайте рассмотрим,
как выглядят значения в нем (рис. 9.5).
Значения на рис. 9.5 также демонстрируют некоторые проблемы:
пропущенные значения;
составные значения, содержащие более одной части. В датасете
о недвижимости Куала-Лумпура некоторые значения включают
строковые выражения, такие как «4 + 1». Они требуют специальной обработки для извлечения полезной информации для
обучения модели.
Рис. 9.5.
Что касается пропущенных значений, мы можем заменить NaN нулями. Примеры
На этом этапе необходимо принять решение по работе со столбцом значений
Rooms: следует ли рассматривать его значения как категориальные в столбце
или как непрерывные. Чтобы сделать обоснованный выбор, давайте Rooms
проанализируем количество уникальных значений в этом столбце:
Rooms
43
Поскольку столбец Rooms содержит около 40 значений, что является относительно небольшим количеством категорий, мы могли бы преобразовать его
в категориальный признак. Однако предположим, что мы решили обрабатывать
его как числовой столбец; давайте рассмотрим необходимые для этого шаги. Для
начала познакомимся с несколькими первыми уникальными значениями и их
количеством (листинг 9.7).
Листинг 9.7. Количество наиболее распространенных значений
в столбце Rooms
3
3+1
2
4
4+1
5+1
1
5
2+1
1+1
6
Studio
6+1
4+2
3+2
5+2
7
7+1
2+2
8
6+
14249
8070
5407
5018
4404
2340
2322
2065
1938
1191
937
874
807
479
477
410
358
237
132
125
86
Пример значения, которое можно сразу
преобразовать в числовое
Пример значения, которое можно преобразовать
в числовое, интерпретировав его как выражение
Пример значения, которое представляет
собой строку и не может быть преобразовано
в числовое значение
Пример значения, которое можно
преобразовать в числовое с помощью
дополнительной интерпретации
1
5
2+1
1+1
6
Studio
6+1
4+2
3+2
5+2
7
7+1
2+2
8
6+
2322
2065
1938
1191
937
874
807
479
477
410
358
237
132
125
86
Пример значения, которое
представляет датасета 405
9.2. Обработка
собой строку и не может быть преобразовано
в числовое значение
Пример значения, которое можно
преобразовать в числовое с помощью
дополнительной интерпретации
Если мы хотим рассматривать Rooms как непрерывный признак, то можем обработать наиболее частые примеры, показанные в листинге 9.7, следующими
способами.
Значения вида 3: преобразуем в числовой формат.
Значения вида 3+1: используем встроенную функцию Python eval() для
вычисления строки как математического выражения.
Значения вида Studio: заменяем на подходящее по смыслу числовое значение, например 1.
Значения вида 6+ трактуем как 6+1. Это не идеальный подход, поскольку
данные не дают информацию, является ли 6+ краткой формой 6+1 или же
означает «6 плюс неопределенное количество дополнительных комнат».
Обратите внимание, что параметр treat_rooms_as_numeric в конфигурационном
файле подготовки данных определяет, будет ли столбец Rooms обрабатываться
как содержащий непрерывные значения или категориальные. Если значение
параметра установлено на True, Rooms будут подготавливаться как непрерывный
признак, а если в False, то как категориальный. Помимо изменения конфигурационного файла подготовки данных также необходимо убедиться, что Rooms
указан в соответствующем списке конфигурационного файла обучения модели,
чтобы процесс обучения корректно интерпретировал тип данных столбца, как
показано ниже:
categorical: # столбцы с категориальными значениями
- 'Location'
#
- 'Rooms'
- 'Property Type'
- 'Furnishing'
- 'Size_type_bin'
continuous: # столбцы с непрерывными значениями
- 'Bathrooms'
- 'Car Parks'
- 'Rooms'
- 'Size'
Теперь, когда мы рассмотрели преобразования для обработки столбца Rooms как
числового, мы можем изучить функцию clean_up_rooms_col() (листинг 9.7).
406 Глава 9. Лучшие практики глубокого обучения
Листинг 9.8. Количество наиболее часто встречающихся значений
в столбце Rooms
Проверяет параметр, чтобы определить,
будет ли столбец Rooms обрабатываться
как непрерывный
Работает со значениями
типа «6+»
Если значение заканчивается на «+», добавляет 1
в конец строки
Если значение — «Studio»,
заменяет его на 1
Пример значения,
которое можно преобразовать в числовое
путем экстраполяции
def clean_up_rooms_col(df,treat_rooms_as_numeric):
if treat_rooms_as_numeric:
print("Rooms как числовой столбец")
df['Rooms'] = df['Rooms'].fillna("0")
df['Rooms'] = \
df['Rooms'].apply(lambda x: x+"1" \
if x.endswith('+') else x)
df['Rooms'] = df['Rooms'].replace("Studio", "1")
df['Rooms']= \
df['Rooms'].replace("20 Above", "21")
df['Rooms']=\
Заменяет строки, являющиеся допуdf['Rooms'].apply(lambda x:eval(str(x)))
стимыми выражениями, числовым
df['Rooms'] = pd.to_numeric(df['Rooms'],
результатом его вычисления
errors='coerce')
Преобразует все значения в числовой формат
# замена пропусков нулем
df['Rooms'] = df['Rooms'].fillna(0)
Если в результате
else:
этих преобразований
print("Rooms как не числовой столбец")
появились значения NaN,
df['Rooms'] = df['Rooms'].fillna("unknown_rooms")
заменяет их на ноль
return(df)
Если столбец обрабатывается
как категориальный, заменяет пропуски
на значение-заполнитель
В листинге 9.8 показано, что для обработки столбца Rooms как непрерывного
нужно выполнить множество преобразований. В частности, требуется заменить уникальные нечисловые значения (Studio, 20 Above) предполагаемыми
числовыми эквивалентами, а также преобразовать значения, которые включают
символ +, результатом вычисления строки как выражения. Для значений, которые заканчиваются на +, мы предполагаем, что строка должна заканчиваться на
+1, чтобы их можно было обработать функцией eval(). Для обработки столбца
нам пришлось сделать ряд предположений о том, что означают такие значения,
как 6+, Studio и 20 Above.
В реальной рабочей ситуации мы можем обратиться за разъяснениями к эксперту в конкретной предметной области. В противном случае придется делать
аналогичные предположения, чтобы проверить, можем ли мы получить полезный
сигнал из этих значений. Учитывая ожидаемую важность столбца Rooms (больше
комнат, как правило, означает бˆольшую площадь, что часто способствует увеличению цены), лакмусовой бумажкой станут обучение модели и сравнение ее
производительности в зависимости от способа обработки этого столбца — как
категориального или непрерывного. Когда позже в этой главе мы перейдем
к обучению модели, то испытаем оба варианта, чтобы определить, какой из них
дает наилучшие результаты.
9.2. Обработка датасета 407
9.2.4. Обработка столбца Size
Среди всех потенциальных признаков для задачи прогнозирования цен на недвижимость в Куала-Лумпуре столбец Size является самым проблемным. Прежде
чем углубляться в детали его очистки, давайте рассмотрим некоторые примеры
значений, содержащихся в нем (рис. 9.6).
Наряду с пропущенными значениями этот столбец содержит строки, включающие классификацию типа участка, застроен он или нет (Built-up или Land area),
а также площадь и единицу измерения (sq. ft — кв. футы). Но это еще не все.
Как видно на рис. 9.7, в столбце Size есть записи, которые выражают площадь
как длину, умноженную на ширину.
В столбце Size необходимо будет сделать много преобразований. На рис. 9.8
показаны примеры значений, когда площадь участка представлена в виде различных выражений.
Рис. 9.6. Примеры
значений в столбце
Size
Рис. 9.7. Примеры
значений в столбце Size,
когда площадь выражена
произведением длины
участка на его ширину
Рис. 9.8. Примеры значений
в Size, когда площадь выражена сложными математическими выражениями
Таким образом, становится ясно, что Size объединяет три или более различных
элемента информации для каждой записи:
тип участка (Land area или Built-up);
площадь, которая может быть представлена в виде числового значения (например, 6900), либо длины, умноженной на ширину (например, 20 × 80), либо
выражения с операциями в одном или обоих измерениях (например, 10 + 24
× 80). Мы предполагаем, что участок имеет прямоугольную форму. Обратите
внимание, что это предположение должно быть подтверждено специалистом
по недвижимости, и это важный момент. Для проведения тщательного анализа датасета крайне необходимо привлечение специалиста в предметной
области, который может подтвердить или опровергнуть наши предположения. Эксперт может предложить другие варианты обработки пропущенных
значений, например замену в столбце Bathrooms на медианное количество
408 Глава 9. Лучшие практики глубокого обучения
санузлов или 0, который хорошо подходит для замены пропусков в столбце
Car Parks. Мы настроили конфигурационные файлы подготовки данных
и блокноты для обучения модели, чтобы упростить внесение изменений,
если наши предположения не совпадут с мнением эксперта. Помещая такие
параметры в файлы конфигурации, мы можем обновлять поведение системы,
не трогая код Python, и методично проводить эксперименты;
единицу измерения площади (например, sq. ft.).
Вы можете спросить, как мы смогли выявить все эти аномалии в столбце Size
и найти решение. Ответ — методом проб и ошибок. Сначала мы отделили тип
участка от его площади и единицы измерения. Затем удалили единицу измерения (так как она всегда одна и та же). Затем последовательно обрабатывали
оставшиеся значения площади, удаляя или заменяя символы до тех пор, пока
каждое значение нельзя было бы обработать функцией eval().
Этот скрупулезный итеративный процесс не является чем-то необычным при работе с реальными данными. Мы выбрали датасет о недвижимости Куала-Лумпура
для этого раздела книги, потому что он иллюстрирует реальные проблемы, и работа с ним показывает подходы — как системные, так и тактические, — которые
необходимы для того, чтобы извлечь из данных максимально полезный сигнал.
Нам нужно разделить три типа значений в столбце Size. Отбросим единицу измерения площади, поскольку она всегда является вариацией «квадратных футов»,
и создадим новый категориальный столбец, который объединит тип участка
с идентификатором интервала, в который он попадает. Для этого нам потребуется:
преобразовать значения площади в числовые, удалив посторонние символы
и используя функцию eval() для вычисления математических выражений
в строковом представлении. Например, значение 20 × 80 заменяется на 160;
разбить полученные числовые значения площади на интервалы (бины) и добавить в набор данных новый столбец с указанием интервала для каждой строки;
создать новый категориальный столбец, объединяющий тип участка и номер
интервала.
В листинге 9.9 показана функция clean_up_size_col(), которая реализует изменения, описанные выше.
Листинг 9.9. Код для очистки столбца Size
Удаляет строки с пропущенными
значениями в столбце Size
def clean_up_size_col(df,clean_up_list,size_bin_count):
df.dropna(subset=['Size'], inplace=True)
df['Size'] = df['Size'].str.lower()
df[['Size_type','Size']] = \
df['Size'].str.split(':',expand=True)
df['Size'] = df['Size'].fillna("0")
df = df[df.Size.str.contains(r'\d')]
for string in clean_up_list:
df = df[~df.Size.str.contains(string,na=False)]
Приводит все значения в столбце Size к нижнему регистру
Разделяет столбец Size,
перемещая значения типа
участка в новый столбец
Заменяет все оставшиеся пропущенные
значения на 0
Удаляет все строки,
в которых столбец Size
не содержит цифр
Удаляет все строки, в кото-
Приводит все значения в столбце Size к нижнему регистру
Разделяет столбец Size,
def clean_up_size_col(df,clean_up_list,size_bin_count):
перемещая значения типа
df.dropna(subset=['Size'], inplace=True)
участка в новый столбец
df['Size'] = df['Size'].str.lower()
9.2.
Обработка
датасета
409
Заменяет
все оставdf[['Size_type','Size']] = \
шиеся пропущенные
df['Size'].str.split(':',expand=True)
значения на 0
df['Size'] = df['Size'].fillna("0")
Удаляет строки с пропущенными
значениями в столбце Size
df = df[df.Size.str.contains(r'\d')]
for string in clean_up_list:
df = df[~df.Size.str.contains(string,na=False)]
Удаляет все строки,
в которых столбец Size
не содержит цифр
Удаляет все строки, в которых значение столбца Size
невозможно преобразовать в числовой формат
df['Size'] = (df['Size'].str.replace(',','')
.str.replace('`','')
.str.replace('@','x')
.str.replace('\+ sq. ft.','')
)
Заменяет символы, котоdf['Size'] = (df['Size'].str.replace(' sq. ft.','')
рые могут вызвать про.str.replace('sf sq.ft.','')
блемы с корректностью
.str.replace('ft','')
умножения
.str.replace('sq','')
.str.replace("xx","*")
.str.replace("x ","*")
Удаляет оставшиеся символы,
.str.replace(" x","*")
которые могут помешать
.str.replace("x","*")
обработке значений столбца
.str.replace("X","*")
Size как числовых
.replace('\'','')
)
Удаляет лишние символы
df['Size'] = \
после пробелов
df['Size'].apply(lambda x: remove_after_space(x))
df['Size'] = \
df['Size'].apply(lambda x: eval(str(x)))
Вычисляет оставшиеся значения столбца
df['Size'] = df['Size'].fillna(0.0)
Size как математические выражения
print("минимум: ",df['Size'].min())
print("максимум: ",df['Size'].max())
Определяет интервалы для значений
bins = np.linspace(df['Size'].min(),
столбца Size
df['Size'].max(), size_bin_count)
print("интервалы: ",bins)
Определяет имена интервалов
bin_labels = range(1,size_bin_count+1)
Создает новый столбец, содержащий
print("bin_labels: ",bin_labels)
значение интервала для значения
df['Size_bin'] = pd.qcut(df['Size'],
площади в этой строке
size_bin_count, labels=bin_labels)
df['Size_type_bin'] = \
df['Size_type']+df['Size_bin'].astype(str)
Создает новый категориальный столбец,
return(df)
объединяющий тип участка Size_type
и интервал размера Size_bin
Функция clean_up_size_col(), показанная в листинге 9.9, включает в себя ряд
преобразований, которые позволяют получить числовое значение площади из
столбца Size. Для этого нужно удалить строки, в которых площадь не может быть
интерпретирована. Площадь объекта недвижимости настолько важна для определения его стоимости, что обучать модель на примерах, из которых невозможно
извлечь информацию о площади, не имеет никакого смысла; поэтому мы просто
их удаляем. Далее необходимо очистить оставшиеся значения площади, чтобы их
можно было преобразовать в числовые значения — либо напрямую, либо путем
интерпретации их как уравнений, пригодных для обработки функцией eval().
Учитывая значительное влияние площади объекта на его цену, стоит приложить достаточно усилий для извлечения этой информации всеми возможными
410 Глава 9. Лучшие практики глубокого обучения
способами. Для этой цели мы создаем новые категориальные столбцы, содержащие номер интервала, соответствующий размеру объекта. Использование
этого интервала позволяет обработать часть данных о площади в столбце Size
как числовое значение. При этом нужно удалить строки, в которых площадь не
может быть интерпретирована. Площадь объекта недвижимости является настолько фундаментальным фактором его стоимости, что использовать данные
из объявлений, из которых невозможно извлечь информацию о площади из-за
ее объединения с типом участка (built-up или land area), для обучения модели
абсолютно нецелесообразно.
На рис. 9.9 показаны примеры значений в измененном столбце Size вместе с новыми столбцами (Size_type, Size_bin и Size_type_bin), созданными функцией
в листинге 9.9.
Рис. 9.9. Примеры
значений в новых столбцах,
сгенерированных из столбца
Size
Давайте рассмотрим каждый столбец из рис. 9.9.
Size — вместо исходных значений столбца Size теперь содержатся единые
непрерывные числовые значения, соответствующие площади из исходного
столбца.
Size_type — этот новый столбец содержит указание на тип участка (land area
или built-in), извлеченный из исходного значения столбца Size.
Size_bin — этот новый столбец содержит номер интервала, к которому относится значение из столбца Size. Обратите внимание, что в примерах на
рис. 9.9 строка с наименьшим значением Size имеет наименьший номер
интервала, а строки с наибольшими значениями Size — наибольший номер .
Size_type_bin — этот новый столбец содержит комбинацию значений двух
предыдущих столбцов.
Рассмотрим конкретное значение из исходного столбца Size и процесс его преобразования для формирования значений в новых столбцах Size, Size_type,
Size_bin и Size_type_bin (рис. 9.10).
9.3. Определение модели глубокого обучения 411
Площадь из исходного столбца Size
становится значением в новом столбце Size
Тип участка из исходного столбца Size
становится значением в новом столбце Size_type
Рис. 9.10. Исходный столбец Size и значения в новых столбцах
Мы начали со столбца Size, который содержал смесь двух видов критически
важной информации, а также числовые данные (площадь объекта), представленные то как число, то в виде выражения. После применения шагов очистки
мы разделили столбец Size на четыре: один с непрерывными данными (Size)
и три с категориальными (Size_type, Size_bin и Size_type_bin), и теперь можем
выбирать какие-то из них для обучения модели.
9.3. Определение модели глубокого обучения
В этом разделе мы рассмотрим код, описывающий модель глубокого обучения
для прогнозирования цен на недвижимость в Куала-Лумпуре. Сначала сравним
подход к определению модели, рассматриваемый в этой главе (с использованием
слоев предварительной обработки Keras), с подходом, описанным в главе 3 (где
применялись пользовательские слои). Затем подробно разберем код определения модели. В заключение обсудим обоснование использования слоев предварительной обработки Keras как лучшей практики для глубокого обучения на
табличных данных.
Код из этого раздела доступен по адресу https://mng.bz/gaBe, а файл конфигурации — по адресу https://mng.bz/ey19.
412 Глава 9. Лучшие практики глубокого обучения
9.3.1. Сравнение подходов с пользовательскими слоями
и слоями предварительной обработки Keras
Начиная с главы 3, мы рассмотрели решение на основе Keras для задачи прогнозирования цен Airbnb NYC. В этом решении мы создали модель глубокого
обучения и связанные с ней пайплайны с нуля, то есть не использовали функции
TensorFlow или Keras, специально разработанные для табличных данных. В предлагаемом нами решении для задачи недвижимости в Куала-Лумпуре изменим
подход и воспользуемся преимуществами слоев предварительной обработки
Keras для упрощения работы с табличным датасетом. Чтобы пояснить различия
между двумя этими подходами, давайте сравним:
использование пользовательских слоев — это подход, который мы применяли
для задачи прогнозирования цен Airbnb NYC в главе 3, а также в качестве базового уровня для сравнения с другими подходами глубокого обучения в главе 8;
использование слоев предварительной обработки Keras — именно такой подход
мы будем использовать в этой и последующих главах нашей книги.
См. табл. 9.1.
Таблица 9.1. Сравнение решений для задачи с недвижимостью Куала-Лумпура:
использование пользовательских классов и слоев предварительной обработки Keras
Использование пользовательских слоев
Использование слоев предварительной обработки Keras
Пайплайн
Пользовательские классы пайплайнов на
основе класса Scikit-learn Pipeline (https://
mng.bz/pKP5); требует определения классов
в отдельном файле, чтобы их можно было
использовать при обучении и выводе
(https://mng.bz/OBxK); сложный код для
определения, обучения, вызова и сохранения пайплайнов
Стандартные готовые слои предварительной обработки Keras
доступны по адресу https://mng.
bz/YD1o. Код для определения,
обучения и вызова пайплайна
стал гораздо проще и надежнее
Описание
модели
Сложный набор слоев, предназначенный
для работы с категориальными, непрерывными и текстовыми входными данными
Простой набор слоев, способный
работать с категориальными и непрерывными входными данными
На рис. 9.11 показаны слои, составляющие модель прогнозирования цен на
недвижимость в Куала-Лумпуре с использованием слоев предварительной обработки Keras.
На рис. 9.12 показаны слои, составляющие модель прогнозирования цен на недвижимость в Куала-Лумпуре с использованием пользовательских слоев; код
доступен по адресу https://mng.bz/KGeP.
9.3. Определение модели глубокого обучения 413
Входные
слои
Внутренние
слои
Рис. 9.11. Модель прогнозирования цен на недвижимость в Куала-Лумпуре
со слоями предварительной обработки Keras
Входные
слои
Внутренние
слои
Выходной
слой
Рис. 9.12. Модель прогнозирования цен на недвижимость в Куала-Лумпуре
с пользовательскими слоями
Выходной
слой
414 Глава 9. Лучшие практики глубокого обучения
Общая структура модели Keras с пользовательскими слоями на рис. 9.12 выглядит сложнее, чем структура на рис. 9.11 для модели со слоями предварительной
обработки Keras, но из-за обилия деталей разобраться непросто. Давайте увеличим масштаб слоев и сосредоточимся только на тех, которые обрабатывают
столбец Size, чтобы получить более конкретное представление о различиях
этих двух архитектур. На рис. 9.13 показаны слои предобработки Keras для
столбца Size.
Рис. 9.13. Слои для столбца Size модели со слоями предварительной обработки
Мы видим, что между входным слоем для признака Size и конечным выходным
слоем расположены еще четыре слоя. Сравним с пользовательскими слоями для
столбца Size в модели Keras, показанными на рис. 9.14.
В модели Keras, определенной с помощью пользовательских слоев, находится
семь слоев между входным слоем для Size и конечным выходным слоем, тогда
как в другой модели промежуточных слоев только четыре. Из-за того, что слои
объединены в цепочку с пользовательскими, происходит серия отдельных
операций конкатенации от слоя к слою. Другая же модель имеет один слой
9.3. Определение модели глубокого обучения 415
конкатенации, который объединяет все входные данные, поступающие с каждого входа. Эта разница в количестве промежуточных слоев отражает общую
дополнительную сложность модели Keras с пользовательскими слоями по
сравнению со второй.
Рис. 9.14. Слои для столбца Size модели с пользовательскими слоями
416 Глава 9. Лучшие практики глубокого обучения
9.3.2. Изучение кода для определения модели с использованием
слоев предварительной обработки Keras
В целом код для определения и обучения модели с использованием слоев предварительной обработки Keras проще и более лаконичен, чем код глубокого обучения
из предыдущих глав, который применял пользовательские слои и пайплайны на
основе scikit-learn. Однако за возросшую простоту модели придется заплатить
небольшую цену. Чтобы избежать ошибок при сохранении модели с помощью
model.save() и в обратном вызове сохранения модели, нужно убедиться, что
все названия столбцов записаны в нижнем регистре и не содержат пробелов
(этого можно достичь с помощью соглашения об именовании snake_case). Код
в листинге 9.10 автоматически делает это.
Листинг 9.10. Приведение названий столбцов к нижнему регистру
и замена пробелов
Для столбцов во входном датафрейме
заменяет пробелы в названиях на
нижние подчеркивания
Приводит названия столбцов
merged_data.columns = \
во входном датафрейме
merged_data.columns.str.replace(' ', '_')
к нижнему регистру
merged_data.columns = merged_data.columns.str.lower()
config['categorical'] = \
Для списка названий категориальных столбцов
[x.replace(" ", "_") for x in \
заменяет пробелы на нижние подчеркивания
config['categorical']]
config['continuous'] = \
Для списка названий непрерывных столбцов
[x.replace(" ", "_") for x in \
заменяет пробелы на нижние подчеркивания
config['continuous']]
config['categorical'] = \
[x.lower() for x in config['categorical']]
Приводит список названий категориальных
config['continuous'] = \
столбцов к нижнему регистру
[x.lower() for x in config['continuous']]
Приводит список названий непрерывных
столбцов к нижнему регистру
Код, представленный в листинге 9.10, заменяет пробелы в названиях столбцов
входного датафрейма и в списках категориальных и непрерывных признаков.
Например, имя переменной Car Parks становится car_parks.
В следующем листинге представлено определение функции df_to_dataset,
которая создает входной пайплайн для модели, использующей слои предварительной обработки Keras.
Функция df_to_dataset, показанная в листинге 9.11 (взята непосредственно
с https://www.tensorflow.org/tutorials/structured_data/preprocessing_layers), будет
применяться к обучающему, валидационному и тестовому датасетам для их
преобразования в объекты tf.data.Dataset с последующим перемешиванием
и разделением на батчи. Обратите внимание, что определение набора данных
ds принимает два аргумента: dict(df) (версия входного датафрейма в виде
9.3. Определение модели глубокого обучения 417
словаря) и labels (целевые значения из входного датафрейма). Также обратите
внимание, что применение prefetch() к набору данных позволяет обрабатывать
его более эффективно за счет совмещения предобработки и выполнения модели
для одного батча с одновременной загрузкой следующего.
Листинг 9.11. Функция создания входного пайплайна
# функция взята по адресу
# https://www.tensorflow.org/tutorials/structured_data/preprocessing_layers
def df_to_dataset(dataframe, shuffle=True, batch_size=32):
df = dataframe.copy()
Извлекает целевой столбец из локальной
labels = df.pop('target')
копии датафрейма
df = {key: value[:,tf.newaxis] for key,
value in dataframe.items()}
Создает новый словарь
ds = tf.data.Dataset.from_tensor_slices((dict(df),
df с теми же ключами
labels))
и значениями, но
if shuffle:
с добавлением новой оси
ds = ds.shuffle(buffer_size=len(dataframe))
ds = ds.batch(batch_size)
Создает датасет TensorFlow
ds = ds.prefetch(batch_size)
ds с помощью метода from_
return ds
Применяет к датасету
tensor_slices
метод prefetch()
Группирует элементы датасета
в батчи размером batch_size
Перемешивает элементы датасета,
чтобы избежать переобучения,
если данные имеют внутреннюю
сортировку
В листинге 9.12 определяется функция get_normalization_layer(), которая
создает слой нормализации для заданного признака.
Листинг 9.12. Создание слоев нормализации для столбцов
с непрерывными значениями
# функция взята по адресу
# https://www.tensorflow.org/tutorials/structured_data/preprocessing_layers
def get_normalization_layer(name, dataset):
normalizer = layers.Normalization(axis=None)
Определяет объект нормализации
feature_ds = dataset.map(lambda x, y: x[name])
normalizer.adapt(feature_ds)
Создает датасет из входного
return normalizer
Обучает нормализатор,
используя указанный
входной признак
набора данных, содержащий
только входной признак
Функция get_normalization_layer(), определенная в листинге 9.12, которая
взята непосредственно из https://www.tensorflow.org/tutorials/structured_data/
preprocessing_layers, будет применена ко всем непрерывным столбцам, которые мы
хотим использовать для обучения модели. Она масштабирует входные значения,
приводя распределение к центрированному вокруг 0 со стандартным отклонением 1. Подробнее об объекте нормализации, определенном в этой функции, см.
418 Глава 9. Лучшие практики глубокого обучения
https://mng.bz/9YDx. Обратите внимание, что для решений на основе градиентного
бустинга нормализация не понадобится.
В листинге 9.13 представлено определение функции get_category_encoding_
layer(), которая задает слой кодирования для указанного категориального
столбца.
Листинг 9.13. Создание слоев кодирования для категориальных столбцов
Создает слои для столбца
в зависимости от того,
является ли он строковым
# функция взята по адресу
# https://www.tensorflow.org/tutorials/structured_data/preprocessing_layers
def get_category_encoding_layer(name, dataset, dtype, max_tokens=None):
if dtype == 'string':
index = layers.StringLookup(max_tokens=max_tokens)
Создает датасет из входного
else:
набора данных, содержащий
index = layers.IntegerLookup(max_tokens=max_tokens)
только входной признак
feature_ds = dataset.map(lambda x, y: x[name])
index.adapt(feature_ds)
Изучает набор возможных значений
encoder = \
для столбца и присваивает им
layers.CategoryEncoding(num_tokens= \
фиксированный числовой индекс
index.vocabulary_size())
Кодирует числовые индексы
return lambda feature: encoder(index(feature))
Применяет multi-hotкодирование к индексам
Функция get_category_encoding_layer(), определенная в листинге 9.13, который взят непосредственно из https://www.tensorflow.org/tutorials/structured_data/
preprocessing_layers, будет применена ко всем категориальным столбцам, которые
мы хотим использовать для обучения модели. В этой функции, если столбец
является строковым, генерируется слой, который преобразует строки в числовые
индексы; в противном случае создается слой, который преобразует целочисленные значения в числовые индексы.
В листинге 9.14 показан код для применения функции df_to_dataset() к обу
чающему, валидационному и тестовому наборам данных.
Листинг 9.14. Применение df_to_dataset() к обучающему,
валидационному и тестовому наборам данных
# функция взята по адресу
# https://www.tensorflow.org/tutorials/structured_data/preprocessing_layers
train_ds = df_to_dataset(train, batch_size=batch_size)
Генерирует обучающий
val_ds = df_to_dataset(val, shuffle=False,
датасет
batch_size=batch_size)
Генерирует валидационный датасет
test_ds = df_to_dataset(test, shuffle=False,
batch_size=batch_size)
Генерирует тестовый датасет
9.3. Определение модели глубокого обучения 419
После выполнения кода листинга 9.14 мы получим необходимые датасеты и будем готовы начать процесс обучения.
В листинге 9.15 показано применение функции df_to_dataset() к обучающему,
валидационному и тестовому наборам данных.
Листинг 9.15. Определение слоев для столбцов с непрерывными
и категориальными значениями
all_inputs = []
encoded_features = []
Список входных признаков
Список закодированных признаков
Создает слой нормализации
для каждого столбца
с непрерывными значениями
for header in config['continuous']:
numeric_col = tf.keras.Input(shape=(1,), name=header)
normalization_layer = get_normalization_layer(header, train_ds)
encoded_numeric_col = normalization_layer(numeric_col)
all_inputs.append(numeric_col)
encoded_features.append(encoded_numeric_col)
Создает слой кодирования для каждого
столбца с категориальными значениями
for header in config['categorical']:
categorical_col = tf.keras.Input(shape=(1,), name=header, dtype='string')
encoding_layer = get_category_encoding_layer(name=header,
dataset=train_ds,
dtype='string',
max_tokens=5)
encoded_categorical_col = encoding_layer(categorical_col)
all_inputs.append(categorical_col)
encoded_features.append(encoded_categorical_col)
В коде в листинге 9.15 для каждого непрерывного столбца функция get_
normalization_layer() определяет для него слой нормализации. Новый слой
добавляется в список encoded_features, а название столбца добавляется в список all_inputs. Для каждого категориального столбца функция get_category_
encoding_layer() определяет слой кодирования. Новый слой добавляется
в список encoded_features, а название столбца включается в список all_inputs.
После выполнения кода из листинга 9.15 список all_features содержит следующие значения:
[<KerasTensor: shape=(None, 1) dtype=float32 (created by layer 'Bathrooms')>,
<KerasTensor: shape=(None, 1) dtype=float32 (created by layer 'Car_Parks')>,
<KerasTensor: shape=(None, 1) dtype=float32 (created by layer 'Rooms')>,
<KerasTensor: shape=(None, 1) dtype=float32 (created by layer 'Size')>,
<KerasTensor: shape=(None, 1) dtype=string (created by layer 'Location')>,
<KerasTensor: shape=\
(None, 1) dtype=string \
(created by layer 'Property_Type')>,
<KerasTensor: shape=(None, 1) dtype=string (created by layer 'Furnishing')>,
<KerasTensor: shape=\
(None, 1) dtype=string \
(created by layer 'Size_type_bin')>]
420 Глава 9. Лучшие практики глубокого обучения
Если мы посмотрим на настройки в файле конфигурации, которые определяют
непрерывные и категориальные столбцы, используемые для обучения модели,
мы увидим, что они соответствуют слоям, указанным в списке all_features:
categorical: # столбцы с категориальными значениями
- 'Location'
- 'Property Type'
- 'Furnishing'
- 'Size_type_bin'
continuous: # столбцы с непрерывными значениями
- 'Bathrooms'
- 'Car Parks'
- 'Rooms'
- 'Size'
Теперь, когда мы задали слои, соответствующие столбцам входного датасета, мы
можем определить модель. Листинг 9.16 показывает код, который определяет
модель.
Листинг 9.16. Код для определения модели
all_features = \
tf.keras.layers.concatenate(encoded_features)
x = \
tf.keras.layers.Dense(32,
activation="relu")(all_features)
x = tf.keras.layers.Dropout(dropout_rate)(x)
output = tf.keras.layers.Dense(1)(x)
model = tf.keras.Model(all_inputs, output)
Добавляет полносвязный слой к модели.
Соответствует слою 4 на рис. 9.15
Объединяет признаки из списка
encoded_features, созданного в коде
из листинга 9.15
Добавляет полносвязный слой
к модели. Соответствует слою 2
на рис. 9.15
Добавляет слой дропаута к модели.
Соответствует слою 3 на рис. 9.15
Листинг 9.16 осуществляет объединение признаков из списка encoded_features
с помощью функции concatenate(). Он соответствует слою 1 на рис. 9.15.
После применения кода из листинга 9.16 набор слоев модели считается определенным. Как показано на рис. 9.15, каждый категориальный столбец имеет
входной слой, слой StringLookup и слой CategoryEncoding, а каждый непрерывный столбец — входной слой и слой нормализации.
На рис. 9.15 приведены слои, отображенные в стандартном вертикальном расположении. Чтобы получить горизонтальное расположение, мы можем использовать параметр rankdir='LR' в функции plot_model:
tf.keras.utils.plot_model(model, show_shapes=True, rankdir="LR")
9.3. Определение модели глубокого обучения 421
Категориальные столбцы
Непрерывные колонны
1
2
3
4
Рис. 9.15. Схема слоев модели
Кроме того, обратите внимание, что архитектура, показанная на рис. 9.15, достаточно проста для умеренного количества столбцов в наборе данных Куала-Лумпура. Архитектура станет сложнее, если вы работаете с датасетом, содержащим
большое количество признаков. Кроме того, подход, описанный в этой главе
для категориальных столбцов, зависит от one-hot-кодирования. Он хорошо работает для данных о недвижимости Куала-Лумпура, поскольку максимальное
количество уникальных значений в любом категориальном столбце составляет
чуть больше 100:
Rooms
Location
Property Type
Furnishing
Size_type_bin
18
108
97
5
20
Однако если бы у нас был датасет с сотнями или тысячами значений в категориальных столбцах, использование one-hot-кодирования могло бы привести
к проблемам с памятью. В этом случае пришлось бы рассмотреть возможность
422 Глава 9. Лучшие практики глубокого обучения
использования эмбеддингов для категориальных столбцов, как мы это сделали
в модели Keras с пользовательскими слоями.
В этом разделе мы рассмотрели код для определения модели, а в следующем
разберем код для ее обучения.
9.4. Обучение модели глубокого обучения
В предыдущем разделе мы рассмотрели код для определения модели и то, как
слои в ней были построены на основе входных столбцов. Здесь мы опишем
процесс обучения. Код из этого раздела доступен по адресу https://mng.bz/gaBe,
а файл конфигурации — по адресу https://mng.bz/ey19. Ниже представлен код,
необходимый для компиляции и обучения модели (листинг 9.17).
Листинг 9.17. Код компиляции и обучения модели
Компилирует модель, определенную
в листинге 9.16, используя гиперпараметры
из файла конфигурации
model.compile(optimizer=config['hyperparameters']['optimizer'],
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=config['metrics'])
if config['general']['early_stop']:
callback_list, save_model_path = set_early_stop(es_monitor, es_mode)
model.fit(train_ds,
epochs=config['hyperparameters']['epochs'],
validation_data=val_ds,
callbacks=callback_list)
Создает список обратных вызовов и использует его
else:
в вызове метода fit для обучения модели
model.fit(train_ds,
epochs=config['hyperparameters']['epochs'],
validation_data=val_ds)
Вызывает fit для обучения модели
без списка обратных вызовов
В листинге 9.17 показано, что значения гиперпараметров взяты из файла конфигурации. Благодаря этому можно настраивать их и перезапускать блокнот для
обучения модели, не трогая код Python. Сохранение значений конфигурации
в файле отдельно от кода Python снижает вероятность регрессий, вызванных
изменением кода, и упрощает отслеживание результатов экспериментов.
Обратите внимание также, что мы можем вызвать метод fit() для обучения
модели с использованием обратных вызовов. Код в листинге 9.17 показывает,
что можно вызвать fit() с набором обратных вызовов, если нужно контролировать процесс обучения, или без них, чтобы пройти все указанные эпохи без
прерывания. Применение обратных вызовов является лучшей практикой для
глубокого обучения с Keras, поскольку позволяет использовать ресурсы более
9.4. Обучение модели глубокого обучения 423
эффективно. Вместо того чтобы проходить все указанные эпохи и получать
производительность модели на основе результата последней из них, можно применять обратные вызовы, чтобы остановить процесс, если производительность
перестает улучшаться в течение заданного количества эпох. Это гарантирует,
что результат обучения будет оптимальным.
Теперь, когда модель обучена, посмотрим в листинге 9.18, как быстро выполнить
ее оценку.
Листинг 9.18. Код для быстрой оценки обученной модели
loss, accuracy = model.evaluate(test_ds)
print("Потери на тесте", loss)
print("Accuracy на тесте", accuracy)
Потери на тесте 0.2754836082458496
Accuracy на тесте 0.8765323758125305
Использует функцию evaluate() для получения потерь и доли верных результатов
обученной модели на тестовых данных
Оценивает потери для данного
цикла обучения
Оценивает долю верных результатов
для данного цикла обучения
Вывод листинга 9.18 демонстрирует достойные результаты на тестовом наборе
с использованием обученной модели.
9.4.1. Кросс-валидация в процессе обучения
В главе 4 мы выделили кросс-валидацию (то есть разделение датасета и многократное обучение модели на разных подмножествах данных с разными контрольными выборками для валидации модели) как лучшую практику для классических
подходов машинного обучения. Нужно ли применять ее для глубокого обучения
на табличных данных? Короткий ответ — «нет». В Keras, когда мы выполняем
повторные обучающие прогоны с указанием пропорций датасета для обучения,
валидации и тестирования, подмножества данных, помещаемые в каждую категорию, рандомизируются по умолчанию, поэтому с Keras мы естественным
образом получаем преимущества кросс-валидации (https://mng.bz/jpPz).
9.4.2. Регуляризация в процессе обучения
Другой метод, который мы выделили в главе 4, — это регуляризация (то есть предотвращение переобучения модели путем снижения ее сложности для улучшения
обобщающей способности). Нужно ли применять регуляризацию при глубоком
обучении, как мы это делали при классическом подходе? Ответ — «безусловно
да». На рис. 9.16 показана часть схемы модели и выделен компонент, специально
предназначенный для предотвращения переобучения, — слой дропаут, который
случайным образом обнуляет входные данные (https://mng.bz/W2z4).
424 Глава 9. Лучшие практики глубокого обучения
Рис. 9.16. Регуляризация в модели глубокого обучения
9.4.3. Нормализация в процессе обучения
Нормализация означает приведение непрерывных значений признаков к единому согласованному диапазону. В главе 4 мы рассмотрели использование этой
техники (также известной как стандартизация) в классическом машинном обу
чении. В листинге 9.12 мы показали код решения для данных о Куала-Лумпуре,
использующий слои нормализации.
В наборе данных о Куала-Лумпуре есть примеры непрерывных столбцов, значения которых имеют существенно отличающиеся диапазоны. Например, признак
Bathrooms имеет диапазон от 0 до 20, а Size — от 0 до 11 миллионов. Если оставить непрерывные столбцы с такими различающимися диапазонами, это может
снизить эффективность обучения. На рис. 9.17 показаны слои нормализации,
которые приводят диапазоны значений для непрерывных признаков к более
согласованным интервалам.
Слои нормализации корректируют значения в каждом из непрерывных столбцов в распределение с центром вокруг 0 и стандартным отклонением 1 (https://
mng.bz/0QKp).
В этом разделе мы рассмотрели код для обучения модели прогнозирования цен
на недвижимость в Куала-Лумпуре, а также набор лучших практик: регуляризацию для предотвращения переобучения и нормализацию для приведения
значений непрерывных столбцов к согласованным диапазонам. В следующем
разделе соберем все наши наработки воедино, применив обученную модель
к совершенно новым данным.
9.5. Применение модели глубокого обучения 425
Рис. 9.17. Нормализация в модели глубокого обучения
9.5. Применение модели глубокого обучения
В этой главе мы уже подготовили датасет по недвижимости Куала-Лумпура,
с использованием слоев предварительной обработки Keras определили модель
глубокого обучения и обучили ее. А теперь попробуем испытать ее в деле.
Код из этого раздела доступен по адресу https://mng.bz/gaBe, а файл конфигурации — по адресу https://mng.bz/ey19.
9.5.1. Тестирование обученной модели на новых данных
Процесс развертывания модели сложен, и мы рассмотрим его более подробно
в главах 10 и 11. Первоначальное развертывание может потребовать значительных затрат, поэтому нужно обеспечить его бесперебойное выполнение
и избежать необходимости возвращаться к предыдущим этапам. Если испытать
обученную модель на нескольких реальных точках данных как можно раньше,
можно предотвратить появление дополнительных проблем на более поздних
этапах процесса. У такого подхода есть два ключевых преимущества.
Это простой способ выявления утечки данных (см. https://mng.bz/zZXw).
Между началом проекта и развертыванием могут пройти месяцы, особенно
если вы имеете дело с глубоким обучением. За это время можно упустить
из виду, какие именно данные будут доступны обученной модели после ее
развертывания. Тестирование на нескольких точках данных — это простой
способ обнаружить потенциальную утечку до того, как проект продвинется
слишком далеко, поскольку оно заставит вас задуматься о том, как выглядят
новые данные.
426 Глава 9. Лучшие практики глубокого обучения
Это способ проверки производительности модели на тестовом датасете. Например, если вы примените обученную модель к совершенно новым данным
о Куала-Лумпуре, то получите представление о том, соответствует ли ее
производительность на тестовом датасете эффективности на новых данных.
Чтобы проиллюстрировать, как тестирование обученной модели на нескольких
новых реальных примерах может помочь предотвратить утечку данных, давайте
рассмотрим, как мы будем использовать обученную модель для прогнозирования
того, будет ли цена продажи нового объекта в Куала-Лумпуре выше или ниже
медианной. Предположим, что в датасете по недвижимости Куала-Лумпура
есть столбец под названием Weeks on the market (Недель на рынке), где указано
количество недель, в течение которых недвижимость находилась на рынке до ее
продажи. Если бы мы включили такой столбец в обучение, что произошло бы при
попытке применить модель к новым данным? На рис. 9.18 проиллюстрирована
проблема, с которой мы столкнулись бы.
Запись об объекте в наборе данных
о Куала-Лумпуре
Признаки, использованные для обучения модели
categorical: # categorical columns
1 - 'Location'
2 - 'Property Type'
3 - 'Furnishing'
4 - 'Size_type_bin'
continuous: # continuous columns
5 - 'Bathrooms'
6 - 'Car Parks'
7 - 'Rooms'
8 - 'Size'
9 - 'Weeks on the market'
7
5
4
8
1
А как быть
с признаками?
6
9
2
3
Рис. 9.18. Проверка признаков, использованных для обучения модели,
на новых данных
На левой стороне рис. 9.18 приведен список признаков, использованных для
обучения модели, как они указаны в файле конфигурации, включая Weeks on
the market. Справа находится отрывок из реального объявления о продаже недвижимости в Куала-Лумпуре. Если мы хотим использовать обученную модель
для получения прогноза цен, то нужно найти значения для всех признаков слева
в объявлении справа.
9.5. Применение модели глубокого обучения 427
Как показано цифрами на рис. 9.18, для большинства признаков, использованных для обучения модели, в реальном объявлении есть соответствующие
значения. Однако для двух признаков получить информацию не представляется
возможным:
Car Parks — в этом конкретном случае упоминается крытая парковка для
здания в целом, но не содержится никакой информации о парковочных
местах для этого конкретного объекта. Для целей быстрого тестирования
мы можем предположить, что значение Car Parks для этого случая должно
быть равно 0;
Weeks on the market — в объявлении не указано количество недель, в течение
которых объект находился на рынке до продажи, поскольку эта информация станет известна только после совершения сделки. Так что мы не можем
включить Weeks on the market в процесс обучения.
Теперь, когда мы объяснили, почему важно тестировать обученную модель на
нескольких реальных точках данных, давайте в следующем подразделе рассмотрим код для реализации этого процесса.
9.5.2. Тестирование обученной модели на новых точках данных
В листинге 9.19 показан код для сохранения модели в файловой системе, определения новой точки данных путем указания значений для всех признаков,
используемых для обучения, и применения модели к этой новой точке данных.
Листинг 9.19. Код для проверки обученной модели на новой
точке данных
Сохраняет обученную модель
в файловой системе
model_file_name = \
os.path.join(get_model_path(),config['file_names']['saved_model'])
model.save(model_file_name)
reloaded_model = \
Загружает сохраненную модель
tf.keras.models.load_model(model_file_name)
sample = {
'location': 'Dutamas, Kuala Lumpur',
'rooms': 7.0,
Определяет новую
'property_type': 'Serviced Residence (Intermediate)',
точку данных
'furnishing': 'Partly Furnished',
'size_type_bin': 'built-up 1',
'bathrooms': 1.0,
'car_parks': 1.0,
'size': 16805.0,
}
input_dict = \
{name: tf.convert_to_tensor([value]) for name,
value in sample.items()}
Переводит новую точку данных
predictions = reloaded_model.predict(input_dict)
в формат, ожидаемый моделью
prob = tf.nn.sigmoid(predictions[0])
print(
"Этот объект с вероятностью %.1f процентов "
"имеет цену выше медианной." % (100 * prob)
)
Получает прогноз для новой
точки данных
'property_type': 'Serviced Residence (Intermediate)',
'furnishing': 'Partly Furnished',
'size_type_bin': 'built-up 1',
'bathrooms': 1.0,
'car_parks': 1.0,
'size': 16805.0,
точку данных
428
Глава 9. Лучшие практики глубокого обучения
}
input_dict = \
{name: tf.convert_to_tensor([value]) for name,
value in sample.items()}
predictions = reloaded_model.predict(input_dict)
prob = tf.nn.sigmoid(predictions[0])
print(
"Этот объект с вероятностью %.1f процентов "
"имеет цену выше медианной." % (100 * prob)
)
Переводит новую точку данных
в формат, ожидаемый моделью
Получает прогноз для новой
точки данных
Если мы запустим код, показанный в листинге 9.19, то получим следующий
вывод:
Этот объект с вероятностью 99.4 процента
имеет цену выше медианной.
Модель, похоже, уверена, что цена этого объекта превысит медианную. Как вы
думаете почему? Это может быть связано с тем, что площадь объекта просто
огромна — более 16 000 квадратных футов. Предположим, мы перезапустим
этот код, указав площадь 1500 и оставив все остальные значения прежними.
Получим следующий вывод:
Этот объект с вероятностью 47.5 процента
имеет цену выше медианной.
Это в целом соответствует нашим ожиданиям. Когда мы уменьшаем площадь до
более разумного значения и оставляем все остальные признаки неизменными,
то получаем прогноз, который примерно соответствует ожидаемому. С помощью
кода из листинга 9.19 можем протестировать разнообразные данные, например
такие, взятые из реального объявления из Куала-Лумпура и показанные на
рис. 9.18:
sample = {
'location': 'Sentul, Kuala Lumpur',
'rooms': 3.0,
'property_type': 'Condominium For Sale',
'furnishing': 'Partly Furnished',
'size_type_bin': 'built-up 1',
'bathrooms': 2.0,
'car_parks': 0.0,
'size': 950.0,
}
И теперь прогноз будет выглядеть следующим образом:
Этот объект с вероятностью 12.6 процента
имеет цену выше медианной.
Этот прогноз, похоже, находится на нижней границе. Мы можем проверить
медианное значение цены из входного датасета, чтобы сравнить его с прогнозом:
Итоги 429
merged_data['price'].median()
980000.0
Итак, медианная цена из входного датасета составляет 980 000 ринггитов, а цена
объекта в объявлении — 450 000. Эта одна точка данных не доказывает, что модель хороша, — датасет был собран за четыре года до появления этого нового
объявления, и цена в нем может оказаться значительно ниже фактической цены
продажи. Однако, опробовав несколько точек данных, включая экстремальные
значения по важным признакам (например, площадь), а также совершенно новые реальные данные, мы можем получить некоторую уверенность в том, что
производительность модели на тестовом наборе не является случайностью. Проверив модель на реальном примере из объявления, мы доказали, что пайплайн
обработки способен работать с реальными данными и что модель не обучалась
на признаках, которые недоступны в момент прогнозирования.
Есть одна сложность при получении прогноза для нового объявления, которая
связана с тем, как мы создавали столбец size_type_bin при подготовке данных.
Напомним, что значения в этом столбце представляют собой комбинацию
значения Size_type (застроен или нет участок земли) и значения интервала
для площади. Мы можем предположить значение Size_type для нового объявления с достаточной уверенностью, а для определения интервала требуется
дополнительная работа. Теперь, когда мы увидели реальное значение, стоит
задуматься: нужен ли нам такой составной признак? Возможно, мы могли бы
переработать его, оставив только значение Size_type без привязки к интервалу
площади. В конце концов, информация о площади объекта уже присутствует
в столбце Size. В следующей главе мы вернемся к этой проблеме, когда будем
рассматривать сквозной процесс разработки решения для глубокого обучения
на табличных данных.
Итоги
Для эффективного обучения модели необходимо провести предварительную
обработку данных. В этой главе мы рассмотрели ряд процессов, используемых для очистки датасета.
Алгоритмы глубокого обучения не могут работать с пропусками, поэтому
пропуски нужно заполнять (например, медианным значением для непрерывного столбца) или удалять записи.
Строки, содержащие математические выражения (например, 24 x 12), можно преобразовать в числа с помощью встроенной функции Python eval().
Преобразуя такие строки в числовые значения, можно извлечь полезную
информацию, которая улучшит производительность модели.
430 Глава 9. Лучшие практики глубокого обучения
Столбцы, содержащие несколько типов данных (например, исходный столбец Size), можно разделить на отдельные столбцы с различными категориями
данных. Таким образом мы сможем использовать каждый из новых столбцов
в качестве отдельного признака для обучения модели.
Используя встроенные в Keras слои предобработки, можем определить модель глубокого обучения, которая будет проще и удобнее в сопровождении,
чем модель Keras, где табличные данные кодируются с нуля.
Обратные вызовы Keras позволяют контролировать процесс обучения и гарантировать, что мы не тратим ресурсы на итерации, когда модель перестает
улучшаться. Обратные вызовы также обеспечивают сохранение модели
с наилучшими показателями производительности, достигнутыми во время
обучения.
Многие лучшие практики классического машинного обучения применимы
и к глубокому, включая регуляризацию для предотвращения переобучения
и нормализацию для приведения непрерывных значений к согласованным
диапазонам.
Тестирование обученной модели на нескольких реальных примерах помогает
избежать утечки данных и проверить эффективность модели на тестовом
датасете.
10
Развертывание модели
В этой главе
3 Развертывание модели глубокого обучения в простом вебприложении на локальной системе
3 Ключевые концепции Google Cloud
3 Знакомство со средой машинного обучения Google Cloud Vertex AI
3 Развертывание модели глубокого обучения с использованием
эндпоинта Vertex AI
3 Адаптация веб-приложения для работы с эндпоинтом Vertex AI
3 Использование генеративного ИИ Gemini в Google Cloud
в качестве помощника
В главе 9 мы рассмотрели лучшие практики для обучения модели глубокого
обучения на табличных данных и представили задачу прогнозирования цен на
недвижимость в Куала-Лумпуре как сложную (из-за смешанного типа признаков). В этой главе возьмем модель, которую обучили в главе 9, и развернем ее
в простом веб-приложении сначала локально, то есть разместим и веб-сервер,
и модель на нашей локальной системе, а потом покажем альтернативный способ
развертывания в Google Cloud. Мы возьмем обученную модель и развернем ее
432 Глава 10. Развертывание модели
на эндпоинте Vertex AI — среде машинного обучения в Google Cloud. Наконец,
рассмотрим, как использовать ИИ-помощника: Gemini в Google Cloud. Код,
приведенный в этой главе, доступен по адресу https://mng.bz/6e1A.
10.1. Простое развертывание в веб-приложении
После того как мы обучили модель глубокого обучения, следуя лучшим практикам из главы 9, мы только поверхностно коснулись процесса извлечения
пользы из этой модели. Возьмем, к примеру, модель прогнозирования цен на
недвижимость в Куала-Лумпуре. Она может быть полезна для риелторов, которые хотят предоставить своим клиентам рекомендации, по какой цене можно
продать объект; для владельцев недвижимости, которые хотят оценить ее потенциальную стоимость, чтобы выставить на продажу; а еще для покупателей,
заинтересованных в приобретении дома в Куала-Лумпуре, чтобы они могли
оценить, на что можно рассчитывать при определенном бюджете.
Очевидно, что модель, которую мы обучили в главе 9, может быть полезна для самых разных аудиторий. Дилемма, с которой мы сталкиваемся, заключается в том,
как сделать модель доступной для всех этих людей с разными бизнес-целями
и потенциально разным уровнем компьютерной грамотности. Позже в этой
главе мы узнаем, как запустить модель в продакшен в общедоступном облачном
окружении. Но что, если мы хотим провести начальный эксперимент, чтобы изу
чить характеристики модели и запустить несколько тестов с бета-клиентами?
Нужно ли нам реализовывать весь процесс развертывания модели в публичном
облаке? Нет, потому что можно воспользоваться Flask — библиотекой для создания веб-приложений на Python — и настроить автономное развертывание
модели. Flask был впервые выпущен в 2010 году Армином Ронахером (Armin
Ronacher) и с тех пор стал одним из самых популярных веб-фреймворков для
Python. Он может показаться немного устаревшим на фоне более новых инструментов, таких как Streamlit (https://streamlit.io/) или Gradio (https://www.gradio.
app/). Однако, несмотря на свой возраст, Flask остается актуальным выбором
для веб-разработки благодаря своей легковесности и гибкости, особенно для
небольших личных проектов. Мы не будем углубляться в детали Flask, а просто
дадим основы для настройки демонстрации модели. Если вы хотите узнать о нем
больше, то вот ссылка на документацию: https://flask.palletsprojects.com/en/2.3.x/.
Теперь, когда мы познакомились с идеей простого веб-развертывания, можем
начинать. В этом разделе мы рассмотрим, как создать простое, но полное развертывание модели в виде пары веб-страниц, обслуживаемых Flask.
10.1.1. Общая схема веб-развертывания
До сих пор в этой книге мы рассматривали библиотеки и фреймворки для
машинного и глубокого обучения. За исключением файлов конфигурации,
10.1. Простое развертывание в веб-приложении 433
определенных в YAML, весь код, который мы видели до сих пор, был написан на
Python. Как и в предыдущих примерах, веб-развертывание моделей машинного
или глубокого обучения включает Python, но также требует работы скриптов
HTML, JavaScript и CSS. На рис. 10.1 показана общая схема развертывания
в вебе.
Обученная
модель
Сервер
Flask
Home.html — веб-страница
для описания характеристик
объекта недвижимости
Модель Keras загружена из локальной файловой системы
Конфигурационные
файлы Flask
Задание параметров сервера Flask
show-prediction.html — веб-страница
для отображения предсказаний модели
CSS-файл
Содержит настройки
отображения веб-страницы
Рис. 10.1. Общая схема веб-развертывания1
Рассмотрим компоненты, из которых состоит веб-приложение, изображенное
на рис. 10.1.
Обученная модель — это модель, которую мы сохранили в блокноте для Keras
со слоями предобработки. Модель фактически сохраняется в виде структуры
каталогов (https://mng.bz/oKJp). В модуле сервера Flask модель загружается
с помощью ссылки на этот каталог.
Модуль сервера Flask — это модуль Python, который загружает обученную
модель и содержит функции просмотра для каждой из HTML-страниц решения. Функции просмотра определяют действия, которые выполняет модуль
сервера Flask при загрузке этих HTML-страниц.
1
Please select the details for the property — «Пожалуйста, введите данные по объекту».
Here is the prediction for this Kuala Lumpur property — «Вот прогноз на данный объект
недвижимости в Куала-Лумпуре». Prediction is: property has a price less than median —
«Прогноз: цена объекта ниже медианной». Текст на кнопках: Get another prediction —
«Получить новый прогноз». Get prediction — «Получить прогноз». — Примеч. ред.
434 Глава 10. Развертывание модели
Файл конфигурации сервера Flask — файл YAML, в котором можно указать параметры для сервера Flask, например каталог, содержащий обученную модель.
home.html — одна из HTML-страниц, обслуживаемых сервером Flask. Она
содержит поля, в которых пользователь может указать характеристики (например, местоположение, количество комнат и парковочных мест) объекта
недвижимости, для которого требуется получить прогноз цены. Эта страница также содержит функции JavaScript, которые загружают значения по
умолчанию в каждое из полей, определяют допустимые значения для ввода
и упаковывают введенные пользователем данные для отправки обратно
в модуль Flask-сервера.
show-prediction.html — другая HTML-страница, обслуживаемая сервером
Flask. Она отображает прогноз, сделанный моделью для объекта с характеристиками, введенными пользователем на странице home.html.
Файл CSS — определяет визуальное оформление HTML-страницы.
В отличие от других примеров в этой книге, веб-развертывание выполняется
на локальной системе по умолчанию. Когда сервер Flask запущен, он обслуживает home.html на localhost:5000, что позволяет тестировать систему прямо
в браузере.
Теперь, когда мы познакомились с концепцией веб-развертывания, мы более
подробно рассмотрим модуль сервера Flask и HTML-страницы.
10.1.2. Модуль сервера Flask
Модуль сервера Flask — это сердце веб-развертывания. В отличие от других
программ на Python, которые мы рассмотрели в этой книге, он представляет
собой автономный файл .py, а не блокнот Jupyter Notebook.
В этом подразделе мы рассмотрим ключевые фрагменты кода в модуле сервера
Flask и объясним, как они управляют веб-развертыванием. Листинг 10.1 показывает первый ключевой раздел модуля сервера: код для загрузки сохраненной
модели Keras из файловой системы в объект в модуле Python.
Листинг 10.1. Загрузка сохраненной модели Keras
rawpath = os.getcwd()
Получает текущую
model_path = os.path.abspath(os.path.join(rawpath, 'models'))
директорию
model_directory_name = \
os.path.join(model_path,config['file_names'] \
['saved_model'])
Получает полный путь
loaded_model = \
к директории модели
tf.keras.models.load_model(model_directory_name)
Загружает модель, используя
полное имя файла модели
10.1. Простое развертывание в веб-приложении 435
В листинге 10.1 полный путь к директории модели формируется с использованием имени файла модели, загруженного из файла конфигурации. Ожидается,
что директория модели будет находиться в папке с именем models, в той же директории, что и серверный модуль Flask. Модель загружается той же функцией
tf.keras.models.load_model, которая в главе 9 использовалась для загрузки
модели в блокноте обучения модели Keras со слоями предварительной обработки.
Наиболее важными частями модуля сервера Flask являются функции представления, которые определяют действия, выполняемые для каждой HTML-страницы
приложения. В листинге 10.2 показана функция представления для home.html —
кода, который вызывается, когда home.html является целевой страницей.
Листинг 10.2. Функция представления home.html
@app.route('/')
def home():
title_text = "web deployment of Keras model"
title = {'titlename':title_text}
return render_template('home.html',title=title)
Декоратор, указывающий, что
эта функция представления
предназначена для home.html
Параметр, передаваемый
в home.html для ее
отображения
Функция представления, показанная в листинге 10.2, просто отправляет заголовок на страницу home.html и отображает страницу.
В листинге 10.3 показана функция представления для страницы show-prediction.
html. Модуль Flask выполняет этот код, когда целью запроса является страница
show_prediction.html. Эта функция обрабатывает значения, введенные пользователем на странице home.html, и вызывает модель для получения прогноза
на основе этих значений.
Листинг 10.3. Функция представления show-prediction.html
Загружает значения категориальных признаков
из строки запроса в словарь scoring_dict
Декоратор, указывающий, что
эта функция предназначена
для show-prediction.html
Загружает
значения
непрерывных
признаков из
строки запроса
в словарь
scoring_dict
@app.route('/show-prediction/')
def show_prediction():
scoring_dict = {}
for col in config['scoring_columns_cat']:
print("value for "+col+" is: "+str(request.args.get(col)))
scoring_dict[col] = str(request.args.get(col))
for col in config['scoring_columns_cont']:
scoring_dict[col] = float(request.args.get(col))
scoring_dict['size_type_bin'] = \
str(request.args.get('size_type'))+' 1'
Устанавливает значение
input_dict = \
параметра size_type_bin
{name: tf.convert_to_tensor([value]) for name, \
value in scoring_dict.items()}
predictions = loaded_model.predict(input_dict)
prob = tf.nn.sigmoid(predictions[0])
if prob < 0.5:
@app.route('/show-prediction/')
строки запроса
def show_prediction():
в словарь
scoring_dict = {}
scoring_dict
for col in config['scoring_columns_cat']:
print("value for "+col+" is: "+str(request.args.get(col)))
scoring_dict[col] = str(request.args.get(col))
436for
col
Глава
Развертывание модели
in 10.
config['scoring_columns_cont']:
scoring_dict[col] = float(request.args.get(col))
scoring_dict['size_type_bin'] = \
str(request.args.get('size_type'))+' 1'
Устанавливает значение
input_dict = \
параметра size_type_bin
{name: tf.convert_to_tensor([value]) for name, \
value in scoring_dict.items()}
predictions = loaded_model.predict(input_dict)
prob = tf.nn.sigmoid(predictions[0])
if prob < 0.5:
predict_string = "Prediction is: property has a price less than
median"
else:
predict_string = "Prediction is: property has a price more than
Устанавливает выходную строку
median"
prediction = {'prediction_key':predict_string}
прогноза на основе его значения
return(render_template('show-prediction.html',\
Получает прогноз из загруженprediction=prediction))
Отображает страницу
show-prediction.html
с использованием строки
прогноза
ной модели для предоставленных значений признаков
Создает входной
словарь из scoring_dict
Функция представления, показанная в листинге 10.3, берет строку запроса,
возвращенную из home.html, загружает все значения из нее в словарь Python
scoring_dict и использует их для вызова модели, загруженной кодом из листинга 10.1, чтобы получить прогноз. Единственное исключение — size_type_bin.
Для этого простого веб-развертывания мы жестко задаем значение бина. В качестве упражнения подумайте, как его можно рассчитать, чтобы избежать жесткого кодирования. Значение прогноза используется для установки строкового
значения, которое отправляется в show-prediction.html для отображения при
рендеринге страницы.
На рис. 10.2 показано, как функции представления из листингов 10.2 и 10.3
взаимодействуют с веб-страницами home.html и show-prediction.html.
На рис. 10.2 поясняется, что функция представления для страницы home.html
в модуле сервера Flask отображает страницу home.html. Функции JavaScript
в home.html создают строку запроса, содержащую значения признаков, введенные пользователем на home.html. Эта строка запроса возвращается в функцию
представления show-prediction в модуле сервера Flask, которая загружает значения признаков из строки запроса и использует их для получения прогноза от
модели. Прогноз, в свою очередь, используется для создания строки прогноза,
передаваемой на страницу show-prediction.html при ее отображении. Строка
прогноза отображается на странице show-prediction.html.
Теперь, когда мы изучили ключевые части кода Python в модуле сервера Flask
(правая часть рис. 10.2), в следующем разделе выйдем за рамки Python и рассмотрим код HTML и JavaScript, который управляет процессами, изображенными
на левой стороне рис. 10.2.
10.1. Простое развертывание в веб-приложении 437
Веб-страницы
Модуль сервера Flask
home.html
home.html функция представления
Строка запроса,
содержащая значения,
введенные на странице
home.html
show-prediction.html
show-prediction.html функция
представления
Строка
прогноза
Рис. 10.2. Взаимодействие между функциями представления
и веб-страницами
10.1.3. Страница home.html
На странице home.html происходит следующее.
Настройка полей, куда пользователь может вводить значения для восьми
характеристик объекта, на которых строится прогноз.
Установка значений по умолчанию и диапазонов для каждого из полей.
Сбор входных данных из полей при нажатии кнопки «Get predicion» («Получить прогноз») и отправка их в функцию представления модуля сервера
Flask.
На рис. 10.3 показано взаимодействие между функциями JavaScript в home.html
и элементами пользовательского интерфейса на этой странице.
Ознакомившись с тем, что происходит в home.html, давайте рассмотрим код этой
веб-страницы. В листинге 10.4 показаны примеры определений полей ввода
в home.html. Это поля HTML, куда пользователь может ввести значения, чтобы
указать сведения об объекте недвижимости, для которого требуется получить
прогноз цены.
438 Глава 10. Развертывание модели
home.html UI
home.html code
load_selections() функция JavaScript
Установить начальные значения
полей и диапазоны
link_with_args() функция JavaScript
Вызвать getOption() и установить
строку запроса в качестве цели страницы
getOption() функция JavaScript
Загрузить значения полей и создать
строку запроса
Рис. 10.3. Взаимодействие между элементами JavaScript и пользовательского
интерфейса в home.html
Листинг 10.4. Примеры HTML-определений полей
<p>
Метка для поля Location
Location:
<select id="location" >
Определяет идентификатор для поля Location
</select>
</p>
<p>
Определяет метку для поля rooms
Определяет
<label for="rooms">
идентификатор
Number of rooms (1 - 20):</label>
для поля rooms
<input type="number" id="rooms" name="rooms"
min="1" max="20">
Определяет максимальное и минимальное
</p>
значения для поля rooms
В листинге 10.4 показаны примеры определений HTML для категориального
поля location и непрерывного поля rooms. Определения HTML для других
полей следуют той же схеме, с заданными минимальными и максимальными
значениями для всех непрерывных полей.
JavaScript-функция load-selections() выполняется при загрузке страницы
с помощью следующего оператора в начале HTML-страницы:
<body onload="load_selections()">
В листинге 10.5 представлены ключевые части функции JavaScript loadselections(), которая настраивает страницу таким образом, чтобы пользователь
мог выбрать характеристики объекта, для которого он хочет получить прогноз.
10.1. Простое развертывание в веб-приложении 439
Листинг 10.5. Функция JavaScript load-selections()
Определяет список со значениями
для поля property type
Определяет список со значениями
для поля location
function load_selections(){
// Инициализация всех настроек
Определяет идентификатор
var select_location =
для поля location
document.getElementById("location");
var select_property_type = document.getElementById("property-type");
var select_furnishing = document.getElementById("furnishing");
var select_size_type = document.getElementById("size-type");
var location_list = ["KLCC, Kuala Lumpur",..
var property_type_list =
["Serviced Residence","Bungalow",...
var furnishing_list = ["Fully Furnished","Partly Furnished",...;
var size_type_list = ["Built-up", "Land area" ];
Определяет список
document.getElementById("rooms").defaultValue
со значениями для
= 1;
поля size type
document.getElementById("bathrooms").defaultValue = 1;
Задает значение
document.getElementById("car-parks").defaultValue = 0;
document.getElementById("size").defaultValue = 500;
по умолчанию
// Заполнение списка location:
для поля rooms
for(var i = 0; i < location_list.length; i++) {
Выполняет цикл заполvar opt = location_list[i];
нения значениями списка
select_location.innerHTML +=
выбора в поле location
"<option value=\"" + opt + "\">" + opt + "</option>";
}
….
};
Из листинга 10.5 мы видим основные действия, выполняемые функцией
JavaScript load_selections().
Для полей, предназначенных для ввода непрерывных значений, устанавливается значение по умолчанию, которое появляется в поле при первоначальной
загрузке страницы и передается в модуль сервера Flask, если пользователь
не меняет его.
Для категориальных полей определяются значения, доступные для выбора
пользователем. В home.html они заданы в виде жестко закодированного
списка. В более надежном веб-приложении эти значения хранились бы
в управляющем файле отдельно от кода, чтобы упростить поддержку значений и снизить риск внесения регрессий в код.
Когда пользователь задает нужные ему значения для своего свойства и нажимает
кнопку «Get prediction», следующий код указывает, что запускается функция
JavaScript link_with_args():
440 Глава 10. Развертывание модели
<button>
<a onclick=
"link_with_args();" style="font-size : 20px; width: 100%;
height: 100px;">Get prediction</a>
</button>
В листинге 10.6 показана функция JavaScript link_with_args(), которая вызывается, когда пользователь нажимает кнопку «Get prediction» в home.html. Она
запускает остальную часть кода в home.html, собирающем введенные пользователем значения и формирующем из них строку запроса, которая передается
обратно в функцию представления show_prediction() в модуле Flask.
Листинг 10.6. Функция JavaScript link_with_args()
function link_with_args(){
getOption();
console.log("in link_with_args");
console.log(window.output);
window.location.href = window.output;
}
Выводит строку
запроса в консоль
Устанавливает целевой URL страницы
Как показано в листинге 10.6, функция JavaScript link_with_args() просто
вызывает функцию getOption() и устанавливает полученную строку запроса
в качестве целевого URL страницы в window.output.
В листинге 10.7 показана функция JavaScript getOption(), которая загружает все значения, введенные пользователем в home.html, и формирует из них
строку запроса. Эта строка передается обратно в функцию представления
show_prediction() в модуле Flask.
Листинг 10.7. Функция JavaScript getOption()
function getOption() {
Получает выбранную запись из списка
selectElementlocation =
выбора для поля location
document.querySelector('#location');
selectElementpropertytype = document.querySelector('#property-type');
selectElementfurnishing = document.querySelector('#furnishing');
selectElementsizetype = document.querySelector('#size-type');
rooms_value = document.getElementById("rooms").value;
Получает
bathrooms_value = document.getElementById("bathrooms").value;
значение,
car_parks_value = document.getElementById("car-parks").value;
введенное
size_value = document.getElementById("size").value;
в поле rooms
location_string =
selectElementlocation.options\
[selectElementlocation.selectedIndex].value
Получает значение выбранной
property_type_string =
записи из списка выбора для
selectElementpropertytype.\
options[selectElementpropertytype.selectedIndex].value поля location
furnishing_string =
selectElementfurnishing.\
options[selectElementfurnishing.selectedIndex].value
size_type_string = selectElementsizetype.\
options[selectElementsizetype.selectedIndex].value
prefix = "/show-prediction/?"
window.output =
prefix.concat("location=",location_string,"&rooms=",\
rooms_value,"&property_type=",property_type_string,\
car_parks_value = document.getElementById("car-parks").value;
введенное
size_value = document.getElementById("size").value;
в поле rooms
location_string =
selectElementlocation.options\
[selectElementlocation.selectedIndex].value
Получает значение выбранной
property_type_string =
записи из списка выбора
для441
10.1.
Простое
развертывание
в
веб-приложении
selectElementpropertytype.\
options[selectElementpropertytype.selectedIndex].value поля location
furnishing_string =
selectElementfurnishing.\
options[selectElementfurnishing.selectedIndex].value
size_type_string = selectElementsizetype.\
options[selectElementsizetype.selectedIndex].value
prefix = "/show-prediction/?"
window.output =
prefix.concat("location=",location_string,"&rooms=",\
rooms_value,"&property_type=",property_type_string,\
"&furnishing=",furnishing_string,"&size_type=
Устанавливает значение
\",size_type_string,"&bathrooms=\
window.output в строке
",bathrooms_value,"&car_parks=",car_parks_value,\
запроса
"&size=",size_value);
document.querySelector('.output').textContent = window.output;
}
Функция JavaScript getOption(), представленная в листинге 10.7, выполняет
следующие действия.
Загружает значения из непрерывных полей: rooms, bathrooms, car-parks
и size.
Загружает выбранные значения из категориальных полей: location, propertytype, furnishing, и size-type.
Формирует строку запроса, которая выглядит следующим образом:
/show-prediction/?location=KLCC, Kuala Lumpur&rooms=1
&property_type=Serviced Residence&furnishing=Fully Furnished
&size_type=Built-up&bathrooms=1&car_parks=0&size=500
Строка запроса состоит из следующих элементов.
URL /show-prediction/. Обратите внимание, что этот URL соответствует декоратору, который предшествует функции представления для showprediction в модуле сервера Flask, показанного в листинге 10.3:
@app.route('/show-prediction/')
Знак ? для обозначения начала строки запроса.
Набор пар параметров и значений, указывающий установленные значения
для каждого поля в home.html, разделенных символом &. Например, пары
параметров и значений могут выглядеть следующим образом:
location=KLCC, Kuala Lumpur
rooms=1
property_type=Serviced Residence
furnishing=Fully Furnished
size_type=Built-up
bathrooms=1
car_parks=0
size=500
442 Глава 10. Развертывание модели
Строка запроса передается в функцию представления show-prediction в модуле
сервера Flask. Как показано в листинге 10.3, в этой функции строка анализируется для получения значений, введенных на странице home.html для каждого
признака и используемых для получения прогноза от модели.
10.1.4. Страница show-prediction.html
Теперь, когда мы рассмотрели модуль сервера Flask и home.html, осталось изу
чить еще один компонент веб-развертывания: show-prediction.html. Эта страница отображает прогноз, который модель делает для объекта недвижимости,
характеристики которого пользователь ввел на странице home.html. Она также
содержит кнопку для возврата на home.html, где можно ввести данные по другому
объекту (листинг 10.8).
Листинг 10.8. Страница show-prediction.html
<!DOCTYPE html>
<head>
Задает текст, отображаемый во вкладке
<title>
браузера для этой страницы
Page for showing prediction
</title>
<link rel="stylesheet" href="{{ url_for('static',
filename='css/main2.css') }}">
Задает таблицу стилей
</head>
<body>
<!-- display the prediction -->
<div class="home">
<h1 style="color: green">
Here is the prediction for \
this Kuala Lumpur property:
</h1>
<h1 style="color: green">
{{ prediction.prediction_key }}
</h1>
для страницы
Вводный текст
<!-- возврат на home.html для ввода
параметров для следующего прогноза -->
<form action="{{ url_for('home') }}">
<input type="submit"
value="Get another prediction"
style="font-size : 20px; width: 40%; height: 30px;"/>
</form>
Кнопка для возвращения на home.html
</div>
</body>
Как показано в листинге 10.8, show-prediction.html не содержит никаких функций JavaScript. HTML для страницы определяет текст, который отображается на
странице, а кнопка «Get Another Prediction» возвращает пользователя на home.
10.1. Простое развертывание в веб-приложении 443
html. На рис. 10.4 показано, как элементы, которые мы обсудили в этом разделе,
отображаются в show-prediction.html.
Текст заголовка
URL, включающий строку запроса
Строка прогноза
Рис. 10.4. Ключевые элементы в show-prediction.html
Мы изучили все компоненты веб-развертывания и в следующем разделе развернем нашу модель в интернете.
10.1.5. Веб-развертывание
Итак, мы ознакомились с компонентами веб-развертывания, и следующим шагом
будет увидеть его в действии. Что необходимо сделать:
1. Создайте новую директорию на вашей локальной системе и клонируйте
репозиторий https://github.com/lmassaron/ml_on_tabular_data.
2. Перейдите в директорию chapter_10 и запустите модуль сервера Flask:
python flask_endpoint_deploy.py
3. После запуска перейдите в браузере по адресу localhost:5000, чтобы выполнить развертывание.
Поздравляем! Вы опробовали модель глубокого обучения на табличных данных
в рамках простого локального веб-приложения. Такой способ развертывания
позволяет эффективно опробовать обученную модель и убедиться, что она ведет
себя ожидаемым образом. Однако в этой базовой реализации отсутствуют многие
характеристики, необходимые для продакшена. Например, мы не хотим, чтобы
ресурсы для обслуживания модели были ограничены возможностями нашей
локальной системы. В оставшейся части этой главы мы рассмотрим, как можно
использовать это же веб-приложение для тестирования модели, развернутой
через эндпоинт в облачной среде.
444 Глава 10. Развертывание модели
10.2. Публичные облачные сервисы и операции
машинного обучения
Простое веб-развертывание, которое мы рассмотрели в предыдущем разделе,
демонстрирует некоторые важные аспекты внедрения обученной модели в продакшен, но имеет серьезные ограничения. Оно работает полностью на локальной системе, поэтому воспользоваться им могут только те, у кого есть доступ
к этой машине. Возможно, это не так уж плохо, поскольку в текущей версии
отсутствуют функции, необходимые для продакшена, в том числе следующие:
Масштабирование мощностей для удовлетворения спроса. Что произойдет,
если процентная ставка упадет и спрос на прогнозы цен удвоится?
Бесперебойное обновление модели. Что делать, если мы повторно обучаем
модель на последних данных и нужно быстро развернуть ее, не прерывая
работу сервиса?
Обслуживание нескольких версий модели одновременно. Что, если мы хотим
поэкспериментировать с версией модели, обученной на другом датасете, предоставив новую модель части клиентов? Как обслуживать несколько версий
модели и контролировать, какая доля пользователей видит каждую версию?
Мониторинг модели. Как отслеживать точность модели по мере развития
рынка недвижимости? Как выявить и устранить проблемы до того, как они
повлияют на пользователей?
Отказоустойчивость и время безотказной работы. Если приложение работает на локальной системе, что происходит, когда система требует обслуживания или возникают незапланированные простои? Как мы можем гарантировать, что наши пользователи сохранят доступ к приложению?
Мы могли бы реализовать собственный код для обработки всех этих сценариев, но есть более простое решение. Общедоступные облака предоставляют
комплексные сквозные среды для машинного обучения и операций машинного
обучения (MLOps), предназначенные для решения всех перечисленных задач.
Вот три наиболее популярных публичных облака в мире:
AWS;
Azur;
Google Cloud.
Многие организации используют один или несколько из этих облачных сервисов
для внедрения своих моделей в продакшен.
Концепции публичного облака, которые мы описываем в этой и следующей главах, справедливы для всех трех облачных платформ, однако в примерах данной
10.3. Начало работы с Google Cloud 445
главы мы будем использовать Google Cloud. Для этого есть несколько причин.
Один из авторов является сотрудником Google, но, что более важно, наше объективное мнение заключается в том, что Google Cloud предоставляет удобную
среду для развертывания моделей, обученных на табличных данных, и изучения
ключевых концепций MLOps. В оставшейся части этой главы мы рассмотрим, как
начать работу с Google Cloud и использовать его для развертывания. В главе 11
мы выйдем за рамки простого развертывания и изучим функции Google Cloud,
которые позволяют с легкостью заново обучить и повторно развернуть модель.
10.3. Начало работы с Google Cloud
В этом разделе мы рассмотрим начальные шаги по работе с Google Cloud, включая первый вход в систему, создание проекта и бакета Google Cloud Storage для
хранения данных. В следующем разделе увидим, как использовать Vertex AI,
платформу машинного обучения в Google Cloud, для развертывания нашей
модели.
10.3.1. Первый вход в Google Cloud
В этом разделе мы рассмотрим, как в первый раз войти в Google Cloud и настроить проект (Google Cloud project). Все действия в этом сервисе выполняются
в рамках проекта. Он позволяет организовать работу и управлять командами
и отдельными пользователями, имеющими доступ к определенным ресурсам
Google Cloud. Чтобы начать, перейдите на https://cloud.google.com и выполните
вход в свой аккаунт Google, нажав Sign In (рис. 10.5).
Рис. 10.5. Вход в Google Cloud
После этого нажмите Console, чтобы открыть консоль Google Cloud (рис. 10.6).
Если вы ранее не использовали Google Cloud, вам нужно будет настроить учетную запись для выставления счетов. Возможно, у вас есть право на бесплатный
кредит, но для использования функций, описанных в этой и следующей главах,
446 Глава 10. Развертывание модели
нужно будет ввести данные кредитной карты. После того как вы войдете в систему и, при необходимости, завершите настройку своей учетной записи с указанием
платежных реквизитов, вы окажетесь в консоли (рис. 10.7).
Рис. 10.6. Вход в консоль Google Cloud
Рис. 10.7. Консоль Google Cloud
Консоль — это пользовательский интерфейс Google Cloud и один из вариантов
взаимодействия с функциями Google Cloud, и именно его мы будем использовать для большинства действий в этой главе. Помимо консоли Cloud, вы можете
взаимодействовать с функциями Google Cloud, используя:
интерфейс командной строки;
клиентские библиотеки.
10.3. Начало работы с Google Cloud 447
ПРИМЕЧАНИЕ Более подробную информацию об интерфейсах Google Cloud см. на
странице https://cloud.google.com/docs/overview.
10.3.2. Создание проекта Google Cloud
После успешного входа в Google Cloud и получения доступа к Google Cloud
Console можно создать проект. В этом разделе мы рассмотрим шаги для этого.
В Google Cloud Console щелкните поле Project Selection (Выбор проекта)
(рис. 10.8). На экране Select a Project (Выберите проект) выберите New Project
(Новый проект) (рис. 10.9). Затем введите first-project-ml-tabular в поле
Project Name (Имя проекта) и щелкните Create (Создать) (рис. 10.10).
Рис. 10.8. Создание проекта
Рис. 10.9. Страница Select a Project
Рис. 10.10. Ввод имени проекта
Поздравляем! Вы создали свой первый проект Google Cloud.
448 Глава 10. Развертывание модели
Обратите внимание, что имя проекта должно быть уникальным. Идентификатор проекта (Project ID), который отображается под полем Project Name, должен
быть глобально уникальным, поэтому, если у вашего проекта название, которое
совпадает с названием любого другого проекта в Google Cloud, идентификатор
будет автоматически изменен для обеспечения уникальности.
10.3.3. Создание бакета Google Cloud Storage
Основной способ хранения данных в Google Cloud — использование бакетов
Cloud Storage. В этом разделе мы рассмотрим только необходимую для развертывания модели информацию о них. Если вас интересуют более подробные
сведения, см. документацию: https://cloud.google.com/storage/docs/buckets. Мы
будем использовать бакет для хранения модели, которую мы обучили в главе 9. Чтобы создать бакет Cloud Storage, введите cloud storage в строке поиска
в Cloud Console и выберите Cloud Storage (Облачное хранилище) (рис. 10.11).
Рис. 10.11. Поиск Cloud Storage
Появится страница Cloud Storage Buckets. Выберите Create, чтобы создать новый
бакет (рис. 10.12).
Рис. 10.12. Страница облачного хранилища Cloud Storage
На странице создания бакета введите его уникальное имя (рис. 10.13), нажмите
Continue (Продолжить) и выберите Region (Регион) в Location type (Местоположение), а затем выберите опцию Region (Регион) (рис. 10.14). Для работы с кодом
10.3. Начало работы с Google Cloud 449
сценария пайплайна машинного обучения, который мы рассмотрим в главе 11,
бакет необходимо создать, включив эту опцию, а местоположение в строке ниже
можно выбрать любое, какое вам нравится. Нажмите Create. Обратите внимание,
что имя бакета должно быть уникальным.
Рис. 10.13. Присвоение
имени бакету
Рис. 10.14. Установка варианта доступа к бакету
Появится страница сведений о бакете Bucket details, на которой будет отображаться новый бакет (рис. 10.15).
Рис. 10.15. Новый бакет на странице Bucket details
Поздравляем! Вы создали свой первый бакет Google Cloud Storage.
450 Глава 10. Развертывание модели
10.4. Развертывание модели в Vertex AI
Ранее в этой главе мы развернули модель прогнозирования цен на недвижимость
в Куала-Лумпуре с помощью простого веб-приложения на базе Flask. В этом
разделе мы собираемся развернуть ту же модель с помощью среды Google Cloud
Vertex AI.
Теперь, когда мы создали проект в Google Cloud, а в нем бакет Cloud Storage,
мы готовы развернуть модель. Ниже приведены шаги, которые мы выполним
для этого развертывания (более подробно они описаны в документации Vertex
AI по адресу https://mng.bz/nRJ2).
1. Загрузить модель, которую мы обучили в главе 9, в хранилище Google Cloud
Storage.
2. Импортировать модель в Vertex AI Model Registry (Реестр моделей Vertex AI).
В следующих разделах описывается каждый из этих шагов.
10.4.1. Загрузка модели в бакет Cloud Storage
Самый простой способ разместить обученную модель в облачном хранилище
Google Cloud Storage — загрузить папку, содержащую обученную модель. Для
этого клонируйте репозиторий по адресу https://github.com/lmassaron/Advanced_
Analytics_for_Business. Папка, которую необходимо загрузить, — chapter_10/
models/kl_real_estate_keras_preprocessing_model.
Чтобы загрузить модель, созданную в главе 9, выберите бакет Cloud Storage,
который вы создали на странице Cloud Storage (рис. 10.16). На странице Bucket
details выберите Upload Folder (Загрузить папку) (рис. 10.17).
Рис. 10.16. Выбор бакета на странице Cloud Storage
Выберите папку в локальной системе, содержащую обученную модель из главы 9. После завершения загрузки папка появится на странице сведений о бакете
Bucket details (рис. 10.18).
Поздравляем! Вы загрузили свою модель в Google Cloud.
10.4. Развертывание модели в Vertex AI 451
Рис. 10.17. Загрузка папки на странице Bucket details
Рис. 10.18. Загруженный бакет на странице Bucket details
452 Глава 10. Развертывание модели
10.4.2. Импорт модели в Vertex AI
Теперь, когда мы загрузили модель в бакет Google Cloud Storage, можем импортировать ее в реестр моделей Vertex AI Model Registry. Введите vertex ai в строке
поиска консоли Google Cloud, чтобы перейти на страницу Vertex AI. Если вы
впервые используете Vertex AI, то увидите кнопку Enable All Recommended API
(Включить все рекомендованные API). В таком случае нажмите на эту кнопку,
чтобы включить API, необходимые для использования Vertex AI (рис. 10.19).
Рис. 10.19. Включение API Vertex AI
Затем выберите Model Registry (Реестр моделей) на левой навигационной панели (рис. 10.20) и на странице Model Registry нажмите Import (Импортировать)
(рис. 10.21).
10.4. Развертывание модели в Vertex AI 453
Рис. 10.20. Панель навигации по страницам
Model Registry в Vertex AI
Рис. 10.21. Импорт реестра моделей Model Registry
454 Глава 10. Развертывание модели
На странице Import model (Импорт модели) выберите Import as New Model (Импортировать как новую модель), введите first-model-ml-tabular в поле Name
и нажмите Continue (рис. 10.22).
Рис. 10.22. Указание имени импортируемой модели в Model Registry
На вкладке Model settings (Параметры модели) страницы Import model выполните
следующие действия.
1. В Model Framework (Фреймворк) выберите TensorFlow. Вспомните главу 8:
TensorFlow — это низкоуровневый фреймворк для Keras, который мы использовали для обучения модели.
2. В Model Framework Version (Версия фреймворка) выберите версию TensorFlow,
использованную для обучения модели. Эти сведения можно получить из
вывода команды tf.__version__ в блокноте, в котором обучалась модель:
tf.__version__
2.9.2
3. В разделе Model Artifact Location (Расположение артефактов модели) нажмите
Browse (Просмотреть) и выберите папку, в которую вы загрузили модель.
4. Нажмите Continue и Import (рис. 10.23).
Процесс импорта модели может занять несколько минут. После завершения процесса вы увидите название новой модели на странице Model Registry (рис. 10.24).
10.4. Развертывание модели в Vertex AI 455
Рис. 10.23. Указание параметров модели
Рис. 10.24. Импортированная модель на странице Model Registry
Поздравляем! Вы импортировали модель в Vertex AI. В следующем разделе
мы рассмотрим шаги, которые позволят сделать эту модель доступной через
эндпоинт.
456 Глава 10. Развертывание модели
10.4.3. Развертывание модели на эндпоинте
Теперь, когда мы импортировали обученную модель в Vertex AI, мы можем развернуть ее на эндпоинте. В результате мы получаем URL-адрес, который можно
использовать для вызова модели. По сути, эндпоинт заменяет файл модели
в локальной файловой системе, который загружал сервер Flask в простом вебразвертывании, выполненном ранее в этой главе.
Чтобы развернуть модель на эндпоинте, на странице Model Registry выберите модель, которую мы создали в предыдущем разделе. На странице сведений о модели
выберите ее версию (по умолчанию 1) (рис. 10.25). На странице версии модели
нажмите Deploy to Endpoint (Развернуть на эндпоинте) (рис. 10.26). На странице Deploy to Endpoint введите название в поле Endpoint name (Имя эндпоинта)
и нажмите Continue (рис. 10.27).
Рис. 10.25. Страница сведений о модели
Рис. 10.26. Развертывание модели
10.4. Развертывание модели в Vertex AI 457
Рис. 10.27. Страница развертывания на эндпоинте
На вкладке Model Settings в разделе Advanced Scaling Options (Расширенные параметры масштабирования) выберите минимальные характеристики машины,
например n1-standard-2 (рис. 10.28), и нажмите Continue.
Рис. 10.28. Установка типа машины для развертывания
Поскольку это тестовое развертывание простой модели, нам достаточно минимальной конфигурации машины. Если бы у нас была более требовательная
модель или мы бы выполняли развертывание в продакшене, мы могли выбрать
тип машины с бˆольшим объемом памяти или вычислительных ресурсов. Минимальных характеристик вполне достаточно для нашего тестового развертывания, и это обойдется дешевле. При использовании облачной среды лучше всего
использовать ресурсы, которых достаточно для работы приложения и не более
того. Это сэкономит вам деньги.
458 Глава 10. Развертывание модели
СОВЕТ Подробную информацию о типах машин для развертываний на эндпоинтах
Vertex AI см. в документации по адресу https://mng.bz/vK14.
Нажмите кнопку Deploy (Развернуть) (рис. 10.29).
Рис. 10.29. Выбор Deploy на странице Deploy to Endpoint
Развертывание может занять несколько минут. После завершения статус развертывания на странице Model version details (Сведения о версии модели) изменится
на Active (Активна), как показано на рис. 10.30.
Рис. 10.30. Подтверждение того, что модель развернута
Поздравляем! Вы завершили развертывание модели в Vertex AI. В следующем
разделе мы рассмотрим шаги для быстрого тестирования этого развертывания.
10.4.4. Начальное тестирование развертывания модели
Теперь, когда мы развернули модель, мы можем провести первоначальное тестирование развертывания модели непосредственно в консоли Google.
Вспомним тестирование обученной модели, которое мы провели в главе 9, чтобы
проверить модель в среде Jupyter Notebook. Мы определили словарь Python, содержащий все признаки, используемые для обучения модели, а также значения
для каждого из признаков:
10.4. Развертывание модели в Vertex AI 459
sample = {
'location': 'Sentul, Kuala Lumpur',
'rooms': 3.0,
'property_type': 'Condominium For Sale',
'furnishing': 'Partly Furnished',
'size_type_bin': 'built-up 1',
'bathrooms': 2.0,
'car_parks': 0.0,
'size': 950.0,
}
Мы можем повторно использовать этот пример для тестирования развертывания
модели в Vertex AI. На странице сведений о версии модели перейдите в раздел
Test Your Model (Тестирование модели) (рис. 10.31).
Рис. 10.31. Раздел Test Your Model на вкладке Deploy and Test
Обновите поле запроса JSON, используя значения из примера из главы 9, где
каждое значение является элементом списка, а все строки заключены в двойные
кавычки. После завершения обновления поле JSON-запроса должно выглядеть
как на рис. 10.32.
Обратите внимание на три различия между форматом образца в запросе JSON
и исходным образцом из главы 9 в Jupyter Notebook.
Значения в парах «ключ — значение» являются массивами, а не отдельными
значениями. Вы получите ошибку при использовании одиночных значений.
Везде используются двойные кавычки (" ") вместо одинарных.
После последней пары «ключ — значение» запятая не ставится.
460 Глава 10. Развертывание модели
Рис. 10.32. Тестовый образец в формате JSON
Как только поле JSON-запроса будет содержать валидный JSON, рамка станет синей. Нажмите Predict (Прогноз), чтобы увидеть вывод эндпоинта в поле
Response (Ответ), как показано на рис. 10.33.
Рис. 10.33. Ответ от эндпоинта для тестового примера
Обратите внимание, что прогнозное значение не является вероятностью. Вспо
мните, что, когда мы получали прогноз от модели в блокноте ее обучения в главе 9
и когда мы получали прогноз от модели в модуле сервера Flask, нам нужно было
10.5. Использование развертывания Vertex AI с Flask 461
применить сигмоидную функцию к выходу модели, чтобы получить вероятность
того, что цена объекта недвижимости выше медианы:
predictions = loaded_model.predict(input_dict)
prob = tf.nn.sigmoid(predictions[0])
Теперь нам снова нужно применить сигмоидную функцию, чтобы получить
вероятность. Если мы обновим выражения, которые использовали в блокноте
для обучения модели, чтобы входные данные для сигмоидной функции были
выходными данными эндпоинта, то получим ту же вероятность для данного
объекта, которую получили при тестировании модели в главе 9:
prob2 = tf.nn.sigmoid(-1.52191401)
print(
"Этот объект с вероятностью %.1f процентов "
"имеет цену выше медианной." % (100 * prob2)
)
Этот объект с вероятностью 17.9 процентов
имеет цену выше медианной.
Теперь мы убедились, что эндпоинт работает как надо и мы получаем те же самые
результаты для одного и того же объекта недвижимости, что и при использовании
модели непосредственно в Jupyter Notebook.
10.5. Использование развертывания Vertex AI с Flask
В предыдущем разделе мы развернули модель на конечной точке в Vertex AI.
В этом разделе мы адаптируем веб-приложение на Flask для использования
этого эндпоинта. После завершения мы должны получить аналогичный опыт
взаимодействия с веб-страницами home.html и show-prediction.html, причем модель будет обслуживаться из эндпоинта Vertex AI, а не из локальной
системы.
Чтобы адаптировать развертывание Flask для работы с развертыванием эндпоинта Vertex AI, необходимо выполнить следующие шаги.
1. Настроить Vertex AI SDK.
2. Обновить модуль сервера Flask для доступа к эндпоинту Vertex AI и получения прогнозов от обслуживаемой там модели.
На рис. 10.34 показаны ключевые компоненты приложения, адаптированного
для развертывания модели на эндпоинте Vertex AI.
В оставшейся части этого раздела мы рассмотрим шаги, необходимые для развертывания модели на эндпоинте Vertex AI с помощью веб-приложения.
462 Глава 10. Развертывание модели
Модель Keras, обслуживаемая из эндпоинта Vertex AI
сервер
Flask
Home.html — веб-страница
для описания характеристик
объекта недвижимости
Конфигурационный
файл сервера Flask
Настройки параметров для
конфигурационного файла сервера Flask
show-prediction.html — веб-страница для
отображения предсказаний модели
CSS-файл
Содержит настройки
отображения веб-страницы
Рис. 10.34. Пример ссылки запроса на вкладке Deploy and Test (Развертывание
и тестирование)
10.5.1. Настройка Vertex AI SDK
Vertex AI предоставляет клиентские библиотеки, которые позволяют получать
доступ к функциям Vertex AI через API в приложениях на Python, Java и node.
js. Python-библиотека входит в SDK Vertex AI, поэтому мы установим SDK,
чтобы получить доступ к API, необходимому для вызова модели через эндпоинт.
ПРИМЕЧАНИЕ Полную информацию о Vertex AI SDK см. в документации по адресу
https://mng.bz/4a1j.
Для установки Vertex AI SDK можно использовать следующую команду:
pip install google-cloud-aiplatform
Теперь мы можем перейти к следующему шагу: обновлению модуля сервера Flask.
10.5.2. Обновление модуля сервера Flask для вызова эндпоинта
Чтобы получить общее представление об использовании API Vertex AI для
доступа к модели через эндпоинт, щелкните ссылку Sample Request (Пример запроса) на странице сведений о версии модели Model version details (рис. 10.35).
10.5. Использование развертывания Vertex AI с Flask 463
Рис. 10.35. Пример ссылки запроса на вкладке Deploy and Test
Первое изменение в модуле сервера Flask — импорт библиотек, необходимых
для Vertex AI:
from
from
from
from
typing import Dict, List, Union
google.cloud import aiplatform
google.protobuf import json_format
google.protobuf.struct_pb2 import Value
Далее мы добавляем функцию predict_custom_trained_model_sample(), определенную в https://mng.bz/QD8v. Нам нужно сделать одно ее обновление, чтобы она
возвращала predictions, содержащие ответ от эндпоинта, обратно в функцию
представления show-prediction.html.
В листинге 10.9 выделены изменения, которые нужно внести в функцию showprediction.html. Они упаковывают входные значения в формате, ожидаемом
эндпоинтом развертывания Vertex AI, и вызывают модель через него с помощью
функции predict_custom_trained_model_sample().
Листинг 10.9. Функция представления show-prediction.html для
эндпоинта
def show_prediction():
Удаляет size_type
scoring_dict = {}
из списка признаков
for col in config['scoring_columns_cat']:
print("value for "+col+" is: "+str(request.args.get(col)))
scoring_dict[col] = str(request.args.get(col))
for col in config['scoring_columns_cont']:
scoring_dict[col] = float(request.args.get(col))
scoring_dict['size_type_bin'] = str(request.args.get('size_type'))+' 1'
scoring_dict.pop('size_type')
input_dict = {name: [value] for name, \
value in scoring_dict.items()}
Преобразует значения
print("input_dict: ",input_dict)
в scoring_dict в списки значений
predictions = predict_custom_trained_model_sample(
project = config['endpoint']['project'],
endpoint_id = config['endpoint']['endpoint_id'],
location = config['endpoint']['location'],
Вызывает predict_custom_
instances = input_dict)
trained_model_sample
prob = tf.nn.sigmoid(predictions[0])
scoring_dict[col] = str(request.args.get(col))
for col in config['scoring_columns_cont']:
scoring_dict[col] = float(request.args.get(col))
scoring_dict['size_type_bin'] = str(request.args.get('size_type'))+' 1'
scoring_dict.pop('size_type')
input_dict = {name: [value] for name, \
464
10. Развертывание модели
value
in Глава
scoring_dict.items()}
Преобразует значения
print("input_dict: ",input_dict)
в scoring_dict в списки значений
predictions = predict_custom_trained_model_sample(
project = config['endpoint']['project'],
endpoint_id = config['endpoint']['endpoint_id'],
location = config['endpoint']['location'],
Вызывает predict_custom_
instances = input_dict)
trained_model_sample
prob = tf.nn.sigmoid(predictions[0])
В листинге 10.9 показаны следующие изменения функции представления showprediction.html.
Вызов эндпоинта должен содержать точный и правильный список признаков
и их значений, без пропущенных или лишних признаков. Поскольку мы не
используем size_type напрямую с моделью, необходимо явно удалить его
из словаря признаков и значений с помощью вызова метода scoring_dict.
pop('size_type').
Эндпоинт ожидает получить признаки и их значения в словаре того же
формата, что и JSON, который мы использовали для прямого тестирования
эндпоинта в консоли в разделе 10.4.4. Это означает, что значения в словаре
должны быть преобразованы в списки значений, каждый из которых содержит ровно одно значение. Это преобразование выполняется следующим
образом:
input_dict = {name: [value] for name, value in scoring_dict.items()}
Необходимо вызвать функцию predict_custom_trained_model_sample()
с ее параметрами, эндпоинт и пары «ключ — значение» для признаков, для
которых мы хотим получить прогноз.
В дополнение к этим изменениям модуля сервера Flask мы также можем удалить инструкции, которые загружали модель из локальной файловой системы,
поскольку в данном решении мы их не используем.
ПРИМЕЧАНИЕ Обновленная версия модуля сервера Flask, использующая эндпоинт,
доступна по адресу https://mng.bz/Xx5a.
Чтобы запустить эту версию модуля сервера Flask, выполните следующие
действия.
1. Авторизуйте учетную запись, которую вы использовали для доступа к Google
Cloud, с помощью этого приложения, выполнив следующую команду в командной строке локальной системы, где вы запускаете модуль сервера Flask:
gcloud auth application-default login
2. Запустите модуль сервера Flask:
python flask_endpoint_deploy.py
10.5. Использование развертывания Vertex AI с Flask 465
Если вы получили ошибку, связанную с уровнем protobuf (буфер протокола),
попробуйте выполнить следующую команду:
pip install protobuf==3.20.*n
Она настраивает уровень protobuf в соответствии с требованиями конечной
точки.
ПРИМЕЧАНИЕ Для работы с данным приложением не требуется углубленных знаний о protobuf, но, если вам интересно, вы можете ознакомиться с документацией:
https://protobuf.dev/.
3. После запуска модуля сервера Flask перейдите в браузере по адресу
localhost:5000, чтобы протестировать развертывание.
Мы рассмотрели два типа развертывания модели: веб-развертывание, полностью
работающее на локальной системе, и веб-развертывание, использующее модель,
обслуживаемую из конечной точки Vertex AI.
10.5.3. Преимущества развертывания модели на эндпоинте
Если мы получаем одинаковые результаты от модели, независимо от того, развернута она в простом веб-решении или на эндпоинте, возникает вопрос: зачем
нам второй вариант? Потому что он имеет ряд преимуществ.
Вы можете масштабировать экземпляр эндпоинта для обработки повышенной нагрузки.
Вы можете развернуть несколько моделей на одном эндпоинте. Представьте себе ситуацию, когда вам нужно заменить одну модель в продакшене на
другую. Если вы развернете обе модели на одном эндпоинте, то сможете
постепенно регулировать объем трафика, направляемого на новую модель,
не внося никаких изменений в приложение и не вызывая резких изменений
для пользователей.
Вы можете развернуть одну и ту же модель на разных эндпоинтах, что позволит оптимизировать машинные ресурсы. Например, если у вас есть среда
продакшена и среда разработки, вы можете развернуть одну и ту же модель на
двух эндпоинтах: с более высокопроизводительными машинными ресурсами
для продакшена и более дешевыми для разработки.
С помощью эндпоинта можно включить мониторинг Vertex AI для обнаружения расхождений (различий в распределении между обучающими данными
и полученными при применении модели в продакшене) и дрейфа концепций
(изменений в распределении данных, обрабатываемых моделью в продакшене). Мониторинг модели помогает обеспечить, что ее производительность
не ухудшается со временем и что изменения в данных, к которым модель
466 Глава 10. Развертывание модели
применяется в продакшене, не возникают неожиданно. Более подробную информацию о мониторинге см. в документации по адресу https://mng.bz/yWdd.
На рис. 10.36 показаны примеры развертывания нескольких моделей на одном
эндпоинте и одной модели на нескольких эндпоинтах.
Несколько моделей развернуты
на одном эндпоинте
Модель А
Модель Б
Конечная
точка 1
Одна модель развернута
на нескольких эндпоинтах
Модель А
Модель А
Конечная
точка 1
Конечная
точка 2
Пользователи
в тестовом
режиме
Пользователи
в продакшене
X % Model A;
Y % Model B
Пользователи
Рис. 10.36. Связь между моделями и эндпоинтами
На рис. 10.36 показаны два сценария.
Несколько моделей развернуты на одном эндпоинте: модель A — это версия,
которая в настоящее время используется в продакшене, а модель Б — это
следующая версия модели, которую мы хотим внедрить в продакшен. Регулируя значения X и Y, мы можем контролировать распределение трафика,
постепенно увеличивая долю для новой модели.
Одна модель развернута на нескольких эндпоинтах — эндпоинт 1 имеет
базовые вычислительные ресурсы, а эндпоинт 2 обладает ресурсами, достаточными для обработки нашей текущей нагрузки в продакшене. Это позволяет оптимизировать стоимость ресурсов системы для удовлетворения
потребностей нескольких групп пользователей.
10.6. Gemini для Google Cloud: помощь генеративного ИИ в Google Cloud 467
ПРИМЕЧАНИЕ Более подробную информацию о развертывании модели в Vertex AI
можно найти в документации по адресу https://mng.bz/MDlB.
10.6. Gemini для Google Cloud: помощь генеративного
ИИ в Google Cloud
Работая по шагам, описанным в этой главе, вы могли столкнуться с трудностями или необходимостью в дополнительных пояснениях. Для решения проблем
можно обратиться к документации Google Cloud, выполнить поиск в Stack
Overflow или попросить коллегу о помощи. В дополнение к этим традиционным
вариантам Google Cloud предлагает интегрированный источник поддержки на
основе генеративного ИИ: Gemini для Google Cloud.
Gemini для Google Cloud — это набор возможностей генеративного ИИ для платформы Google Cloud. Gemini для Google Workspace — это сопутствующий набор
возможностей генеративного ИИ для сервисов Google Workspace (Google Docs,
Sheets, Slides; см. https://workspace.google.com/solutions/ai/). В этой и следующей
главах мы покажем, как можно использовать генеративный ИИ для упрощения
развертывания моделей и автоматического написания скриптов для пайплайнов
машинного обучения. Gemini для Google Cloud предоставляет множество возможностей, включая следующие:
ответы на вопросы о Google Cloud;
генерация кода (включая SQL и различные языки программирования, такие
как Python, Java и JavaScript) из текста;
объяснение кода.
ПРИМЕЧАНИЕ Более подробный обзор возможностей Gemini для Google Cloud см.
в документации: https://cloud.google.com/gemini/docs/overview.
В этом разделе мы рассмотрим настройку Gemini и его использование для
ответов на вопросы о Google Cloud. Мы также обсудим некоторые действия,
которые выполнили в этой главе для развертывания нашей модели на эндпоинте
Vertex AI. В главе 11 мы вернемся к Gemini, чтобы показать, как его можно использовать для генерации и объяснения кода.
Google Cloud — не единственная облачная платформа, которая использует генеративный ИИ для помощи пользователям и автоматизации некоторых этапов
разработки. Copilot в Azure и Code Whisperer в AWS — это функции на основе
генеративного ИИ, каждая из которых обеспечивает некоторые преимущества,
подобные Gemini.
468 Глава 10. Развертывание модели
10.6.1. Настройка Gemini для Google Cloud
Ниже приведены инструкции по настройке.
Настройка Gemini для Google Cloud для проекта: https://cloud.google.com/
gemini/docs/quickstart.
Настройка Gemini Code Assist (компонента Gemini для Google Cloud, предоставляющего помощь ИИ в разработке) для использования в Cloud Shell
Editor в Google Cloud: https://mng.bz/avMm.
После настройки Gemini Google Cloud для проекта вы увидите соответствующий
значок на панели инструментов (рис. 10.37).
Рис. 10.37. Значок Gemini для Google Cloud
Теперь, когда мы выполнили базовую настройку Gemini, в следующем разделе
посмотрим, как можно использовать ИИ для получения ответов на вопросы
о Google Cloud.
10.6.2. Использование Gemini для ответов на вопросы о Google
Cloud
Gemini интегрирован в различные компоненты платформы Google Cloud, в том
числе:
в чат-панель, доступную во всем Google Cloud, — пример показан на рис. 10.38;
в ряде IDE, поддерживаемых Google Cloud, включая VS Code, Cloud Workstations
и Cloud Shell Editor. На рис. 10.39 показано, как Gemini может генерировать
простую функцию Python из комментария в Cloud Shell Editor;
в редакторах запросов для Big Query и Spanner. На рис. 10.40 показано, как
Gemini может генерировать код SQL из комментария в редакторе запросов
Spanner.
А теперь давайте подробнее изучим чат-панель Gemini и посмотрим, как ее
можно использовать для получения ответов на вопросы о Google Cloud. Чтобы
ее открыть, нажмите на значок Gemini (см. рис. 10.37); еще один пример панели
показан на рис. 10.41.
10.6. Gemini для Google Cloud: помощь генеративного ИИ в Google Cloud 469
Рис. 10.38. Чат-панель Gemini для Google Cloud
Рис. 10.39. Gemini для Google Cloud, генерирующий код на Python в Cloud Shell Editor
Рис. 10.40. Gemini для Google Cloud генерирует SQL в редакторе запросов в Spanner
470 Глава 10. Развертывание модели
Рис. 10.41. Чат-панель Gemini для Google Cloud
Опробуйте панель, введя промпт с вопросом или инструкциями в соответствую
щее поле и нажав кнопку для отправки (рис. 10.42).
Рис. 10.42. Кнопка для отправки промпта в Gemini
Чат-панель Gemini доступна в любое время, и вы можете использовать ее, чтобы
задавать всевозможные вопросы о Google Cloud. Например, предположим, что
мы хотим создать бакет в Cloud Storage и забыли шаги, перечисленные ранее
в этой главе. На рис. 10.43 показан ответ Gemini на запрос «Как создать бакет
Google Storage?».
Итоги 471
Рис. 10.43. Ответ Gemini на вопрос «Как создать бакет в Google Cloud?»
Gemini для Google Cloud специально обучен давать полезные ответы на вопросы об использовании платформы. Опробуйте эту опцию, создав запросы,
связанные с некоторыми разделами этой главы, например, «как импортировать
пользовательскую модель в Vertex AI» или «как развернуть модель на эндпоинте
Vertex AI», и сравните полученные ответы с шагами, перечисленными в книге.
Итоги
Развертывание модели в простом веб-приложении позволяет оценить ее
характеристики в среде продакшена.
Мы можем создать простое веб-приложение для развертывания модели,
обученной в главе 9, которое включает в себя модуль сервера Flask и две
простые веб-страницы.
472 Глава 10. Развертывание модели
Публичное облачное окружение предоставляет среду для развертывания
модели, позволяя масштабировать ресурсы и управлять доступностью.
Чтобы подготовить развертывание модели, обученной в главе 9, необходимо
создать проект Google Cloud, потом бакет и загрузить в него модель.
После выполнения всех шагов по подготовке к развертыванию можно разместить обученную модель на эндпоинте Vertex AI.
Мы можем протестировать развертывание модели на эндпоинте Vertex AI,
внеся несколько простых изменений в модуль Flask из веб-приложения,
которое мы создали в начале этой главы.
Использование эндпоинта в Vertex AI делает развертывание более надежным.
В частности, можно указать машинные ресурсы, которые подходят для приложения, обеспечить использование разных версий моделей и отслеживать
производительность модели в продакшене.
Gemini для Google Cloud предоставляет возможности генеративного ИИ,
которые могут быть полезны для задач разработки и развертывания.
11
Пайплайн машинного
обучения
В этой главе
3 Что такое пайплайн машинного обучения
3 Что необходимо для запуска пайплайна машинного обучения
в Vertex AI
3 Обучение и развертывание модели: локальная реализация
и реализация через пайплайн машинного обучения
3 Как задать пайплайн для обучения и развертывания модели
3 Обновление кода обучения модели в пайплайне
3 Использование генеративного ИИ для создания пайплайна
машинного обучения
В главе 10 мы рассмотрели шаги по развертыванию модели глубокого обучения
на табличных данных. Мы разместили ее в веб-приложении: сначала она полностью работала на нашей локальной системе, а затем на эндпоинте Vertex AI.
В этой главе мы изучим дальнейшие шаги по автоматизации процесса обучения
и развертывания с использованием пайплайна машинного обучения в Vertex
AI. Мы начнем с обзора шагов, необходимых для настройки ML-пайплайна,
474 Глава 11. Пайплайн машинного обучения
включая определение датасета Vertex AI. Затем сравним локальное обучение
и развертывание модели из главы 10 с обучением и развертыванием в рамках
ML-пайплайна. Далее рассмотрим специфичный для пайплайна код, а также
изменения существующего кода, необходимые для обучения модели в рамках
пайплайна. Наконец, изучим различные подходы к интеграции генеративного
ИИ в процесс разработки ML-пайплайнов и методы эффективного использования его результатов. Код, приведенный в этой главе, доступен по адресу https://
mng.bz/DM4n.
11.1. Знакомство с пайплайнами
машинного обучения
Вспомним уже рассмотренные в этой книге шаги по подготовке модели глубокого
обучения табличных данных.
Обработка данных для решения таких проблем, как пропущенные значения,
столбцы с двумя различными типами данных и числовые данные, представленные в виде строк.
Обучение модели с использованием обработанных данных.
Развертывание обученной модели для ее использования в приложении.
Предположим, нам нужно регулярно повторять этот процесс для задачи с недвижимостью Куала-Лумпура. Это вполне возможная ситуация на практике,
поскольку рынок недвижимости постоянно меняется: цены колеблются, изменяются процентные ставки, а макроэкономические факторы влияют на спрос.
Вместо того чтобы вручную запускать различные блокноты и этапы развертывания для каждого сквозного цикла, от сырых данных до развернутой модели,
было бы неплохо иметь код решения, которое можно запускать как единое целое
многократно и последовательно. Пайплайн машинного обучения предоставляет
такую возможность, и в этом разделе мы разберем пример настройки простого
сквозного пайплайна для задачи с недвижимостью Куала-Лумпура.
11.1.1. Три вида пайплайнов
Прежде чем углубляться в детали создания ML-пайплайна, стоит отметить, что
сам термин пайплайн с течением времени приобрел множество значений. На
данный момент их как минимум три.
Пайплайн обучения/инференса гарантирует, что преобразования данных
(такие как сопоставление текста с токенами или преобразование категориальных значений в числовые идентификаторы) выполняются согласованно
на этапах обучения и инференса. Слои предварительной обработки Keras
11.1. Знакомство с пайплайнами машинного обучения 475
в модели для Куала-Лумпура представляют собой именно такой пайплайн,
поскольку они гарантируют, например, что преобразования, примененные
к данным до обучения, точно соответствуют преобразованиям, выполняемым
для точек данных, введенных на странице home.html при развертывании.
Пайплайн данных обрабатывает аномалии во входных обучающих данных,
такие как пропущенные значения или проблемы со схемой. Он может частично пересекаться с пайплайном, описанным в предыдущем пункте, но
выполняет отдельную задачу. В контексте Google Cloud примерами продуктов для реализации таких пайплайнов являются Dataflow и Cloud Data
Fusion. Для понимания этой главы не требуется углубленных знаний о них,
но, если интересно, вы можете ознакомиться с документацией: https://cloud.
google.com/dataflow/docs и https://cloud.google.com/data-fusion/docs.
ML-пайплайн автоматизирует различные этапы, такие как обучение, развертывание и мониторинг модели. В Vertex AI для реализации ML-пайплайнов
доступны два подхода: TFX и Kubeflow.
На рис. 11.1 показано, как каждый из этих трех типов пайплайнов вписывается
в сквозной рабочий процесс машинного обучения.
Новые
точки
данных
ML-пайплайн
Сырые
данные
Пайплайн
данных
Подготовленные
данные
Пайплайн обучения/инференса
Обучение
модели
Обученная
модель
Развертывание модели
Развернутая
модель
Мониторинг
модели
Предсказания
Рис. 11.1. Три типа пайплайнов и их взаимосвязь
На рис. 11.1 представлены следующие характеристики пайплайнов.
Пайплайн машинного обучения может охватывать весь рабочий процесс — от
сырых данных до мониторинга развернутой модели, поскольку он предназначен для автоматизации всего процесса при необходимости повторного
обучения или развертывания модели.
476 Глава 11. Пайплайн машинного обучения
Различие между пайплайном данных и пайплайном обучения/инференса
заключается в том, что второй обрабатывает преобразования, которые необходимо применить к новым точкам данных для получения прогнозов,
например замену категориальных значений числовыми идентификаторами.
Эти же преобразования должны применяться к подготовленным данным
перед обучением модели.
Как мы видели в решении с пользовательскими слоями Keras для задачи прогнозирования цен на Airbnb NYC в главе 3, пайплайн обучения/инференса
может быть отделен от процесса обучения модели. В том решении пайплайн
обучения/инференса был реализован с использованием структур пайплайнов
scikit-learn и пользовательских классов, оба из которых необходимо применять к данным перед обучением модели и перед тем, как подать их на вход
обученной модели для получения прогнозов. С другой стороны, в главе 9
мы увидели, как та же обработка может быть встроена непосредственно
в модель Keras.
Пайплайны данных могут существовать вне контекста рабочего процесса
ML. Те же инструменты пайплайнов, такие как Dataflow и Cloud Data Fusion,
которые используются в Google Cloud, могут быть частью приложений, не
связанных с машинным обучением.
Теперь, когда мы описали три различных типа пайплайнов, в следующем разделе
начнем изучать, как создать ML-пайплайн для задачи прогнозирования цен на
недвижимость в Куала-Лумпуре в Google Cloud с использованием Kubeflow.
11.1.2. Обзор ML-пайплайнов Vertex AI
В главе 10 мы рассмотрели процесс развертывания модели прогнозирования
цен на недвижимость в Куала-Лумпуре на эндпоинте Vertex AI.
Чтобы создать ML-пайплайн для такой модели, начнем с шагов, описанных
в документации Vertex AI: https://mng.bz/lYW6.
Ниже приведен порядок действий.
Настройте сервисный аккаунт (service account). Это учетная запись, используемая приложением для выполнения действий в Google Cloud. Когда мы
импортировали модель Keras на платформу и развернули на эндпоинте, то
использовали свои личные учетные данные. Поскольку ML-пайплайн будет
скриптом автоматизации, нам необходим сервисный аккаунт, чтобы скрипт
мог выполнять действия без прямого ручного вмешательства со стороны
пользователя. Более подробную информацию об этом см. в документации
Google Cloud: https://mng.bz/BXA0.
11.2. Этапы подготовки ML-пайплайна 477
Получите ключ сервисного аккаунта и предоставьте ему необходимые права
доступа для выполнения ML-пайплайна.
Создайте скрипт пайплайна для вызова Vertex AI SDK.
Адаптируйте блокнот для обучения модели в автономный Python-скрипт,
который можно запускать в предварительно созданном бакете Vertex AI.
Запустите скрипт пайплайна, чтобы провести обучение внутри бакета и создать обученную модель.
В последующих разделах мы по очереди рассмотрим все эти шаги.
11.2. Этапы подготовки ML-пайплайна
Прежде чем мы сможем запустить ML-пайплайн для обучения и развертывания
модели, нужно настроить необходимые объекты Google Cloud. В этом разделе
мы создадим сервисный аккаунт и познакомимся с Cloud Shell — инстансом,
доступным непосредственно в Google Cloud, который можно использовать для
ввода команд. Мы также загрузим наш набор данных в Google Cloud Storage
и используем его для создания датасета Vertex AI.
11.2.1. Создание сервисного аккаунта для ML-пайплайна
Поскольку мы хотим иметь возможность запускать пайплайн машинного
обучения автоматически без ручного вмешательства, необходимо настроить
сервисный аккаунт.
Для этого выполните следующие действия.
1. Выберите IAM & Admin Service Accounts в общем меню Google Cloud Console
(рис. 11.2).
2. На странице Service accounts выберите CREATE SERVICE ACCOUNT (Создать
сервисный аккаунт) (рис. 11.3).
3. На странице Create service account введите имя сервисного аккаунта и нажмите
Create and Continue (Создать и продолжить) (рис. 11.4). Обратите внимание,
что идентификатор аккаунта заполняется автоматически, а его email-адрес
отображается в формате идентификатор-сервисного-аккаунта@идентификаторпроекта.iam.gserviceaccount.com, в данном случае ml-tabular-pipeline@
first-project-ml-tabular.iam.gserviceaccount.com.
4. Выберите Vertex AI User (Пользователь Vertex AI) в поле Role (Роль) и нажмите Done (Готово) (рис. 11.5).
478 Глава 11. Пайплайн машинного обучения
Рис. 11.2. Выбор Service Accounts в Google Cloud Console
Рис. 11.3. Создание сервисного аккаунта
Теперь, когда мы создали сервисный аккаунт и предоставили ему доступ к Vertex
AI, в следующем разделе можем создать его ключ.
11.2. Этапы подготовки ML-пайплайна 479
Рис. 11.4. Назначение имени для сервисного аккаунта
Рис. 11.5. Назначение пользовательской роли для сервисного аккаунта
480 Глава 11. Пайплайн машинного обучения
11.2.2. Создание ключа сервисного аккаунта
Пайплайн машинного обучения использует ключ сервисного аккаунта для аутен
тификации учетной записи, от имени которой запускается пайплайн.
Чтобы создать такой ключ, выполните следующие действия.
1. На странице Service accounts нажмите на адрес электронной почты только что
созданного сервисного аккаунта (рис. 11.6).
Рис. 11.6. Выбор сервисного аккаунта
2. Выберите вкладку Keys (Ключи) и нажмите Add key (Добавить ключ)Create
new key (Создать новый ключ) (рис. 11.7).
Файл JSON, содержащий ключ сервисного аккаунта, создается и загружается
в локальную систему с именем, которое выглядит следующим образом: firstproject-ml-tabular-039ff1f820a8.json.
11.2. Этапы подготовки ML-пайплайна 481
Рис. 11.7. Создание ключа сервисного аккаунта
Выберите JSON и нажмите CREATE (рис. 11.8).
Рис. 11.8. Загрузка ключа сервисного аккаунта
482 Глава 11. Пайплайн машинного обучения
11.2.3. Предоставление сервисному аккаунту права доступа
к базовому сервисному аккаунту Compute Engine
При настройке проекта в Google Cloud по умолчанию был создан аккаунт сервиса Compute Engine. Эта учетная запись имеет адрес электронной почты типа
НОМЕР_ПРОЕКТА-compute@developer.gserviceaccount.com. Для получения более
подробной информации об этом см. документацию (https://mng.bz/dXdN).
Нам нужно предоставить сервисному аккаунту, который мы настроили в предыдущих разделах, доступ к базовому сервисному аккаунту Compute Engine для
запуска ML-пайплайна. Для этого выполните следующие шаги.
1. На странице Service accounts щелкните значок копирования рядом с адресом
электронной почты только что созданного сервисного аккаунта (понадобится
на следующем шаге), а затем нажмите на адрес электронной почты аккаунта
Compute Engine (рис. 11.9).
Рис. 11.9. Сервисный аккаунт Compute Engine
11.2. Этапы подготовки ML-пайплайна 483
2. Щелкните вкладку Permissions (Разрешения) и потом Grant Access (Предоставить доступ) (рис. 11.10).
Рис. 11.10. Предоставление доступа к сервисному аккаунту Compute Engine
3. На странице Grant Access вставьте адрес электронной почты созданного вами
сервисного аккаунта в поле New Principals (Новые участники), выберите Service
Account User (Пользователь сервисного аккаунта) в поле Role и нажмите Save
(Сохранить) (рис. 11.11).
Теперь, когда мы завершили шаги по настройке сервисного аккаунта для MLпайплайна, можем продолжить настройку пайплайна.
484 Глава 11. Пайплайн машинного обучения
Рис. 11.11. Настройка доступа к сервисному аккаунту Compute Engine
11.2.4. Знакомство с Cloud Shell
До сих пор все действия, которые мы предпринимали в Google Cloud, осуществлялись через интерфейс (UI) в Console. Но также можно воспользоваться
Cloud Shell — автономным инстансом, позволяющим взаимодействовать с платформой через командную строку. Кроме этого, можно использовать Cloud Shell
Editor для редактирования файлов в файловой системе Cloud Shell, которая
сочетает функциональность локального Linux-инстанса с удобством веб-среды,
11.2. Этапы подготовки ML-пайплайна 485
интегрированной с ресурсами Google Cloud, и особенно хорошо подходит для
создания прототипов и работы с туториалами. Мы будем использовать Cloud
Shell на следующих нескольких этапах настройки пайплайна машинного обу
чения. Дополнительные сведения об инструменте см. в документации: https://
cloud.google.com/shell.
Чтобы запустить Cloud Shell, нажмите на значок Activate Cloud Shell (Активировать Cloud Shell) в верхней части консоли Google Cloud Console (рис. 11.12).
Рис. 11.12. Значок Activate Cloud Shell
При нажатии на значок Activate Cloud Shell в нижней части экрана открывается
терминал Cloud Shell с вашим домашним каталогом в качестве текущего, как
показано на рис. 11.13.
Рис. 11.13. Консоль с активированным Cloud Shell
Вы можете выполнять команды непосредственно в терминале Cloud Shell
Terminal, включая стандартные команды Linux и специализированные команды
Google Cloud. Чтобы редактировать файлы в файловой системе Cloud Shell,
нажмите Open Editor (Открыть редактор) (рис. 11.14). Чтобы вернуться в Cloud
Shell Terminal, нажмите Open Terminal (Открыть терминал).
Теперь, когда мы кратко ознакомились с Cloud Shell, можем перейти к следующему этапу настройки пайплайна машинного обучения: сделать ключ сервисного
аккаунта доступным для пайплайна.
486 Глава 11. Пайплайн машинного обучения
Рис. 11.14. Редактор Cloud Shell Editor
11.2.5. Загрузка ключа сервисного аккаунта
В этом разделе мы воспользуемся Cloud Shell для загрузки JSON-файла ключа
сервисного аккаунта, а затем зададим переменную окружения, указывающую
на его расположение.
1. В Cloud Shell установите свой домашний каталог в качестве текущего, создайте новый каталог с именем ml_pipeline и перейдите в него:
cd ~
mkdir ml_pipeline
cd ml_pipeline
2. Чтобы загрузить ключ сервисного аккаунта, нажмите три точки на панели
инструментов Cloud Shell и выберите Upload (Загрузить), как показано на
рис. 11.15.
3. На странице загрузки Upload измените целевую директорию на ml_pipeline
в вашем домашнем каталоге, нажмите Choose Files (Выбрать файлы), выберите
файл JSON ключа сервисного аккаунта, который вы скачали в разделе 11.2.2,
и нажмите Upload (рис. 11.16).
4. Проверьте загрузку, перейдя в каталог ~/ml_pipeline и используя команду
ls, чтобы убедиться, что JSON-ключ сервисного аккаунта теперь находится
в этом каталоге:
cd ~/ml_pipeline
ls
11.2. Этапы подготовки ML-пайплайна 487
Рис. 11.15. Загрузка файла в Cloud
Shell
Рис. 11.16. Настройка параметров
загрузки
5. Установите переменную окружения GOOGLE_APPLICATION_CREDENTIALS на полный путь к файлу ключа сервисного аккаунта в формате JSON. В следующем
примере замените полный путь на актуальный для файла вашего ключа:
export \
GOOGLE_APPLICATION_CREDENTIALS=\
'/home/ryanmark2023/ml_pipeline/\
first-project-ml-tabular-039ff1f820a8.json'
6. Подтвердите значение переменной окружения GOOGLE_APPLICATION_
CREDENTIALS с помощью следующей команды и убедитесь, что оно установлено на полный путь к вашему файлу ключа сервисного аккаунта:
$ echo $GOOGLE_APPLICATION_CREDENTIALS
Теперь, когда мы загрузили ключ сервисного аккаунта и установили переменную
окружения, указывающую на его расположение, мы готовы перейти к главному
этапу — определению ML-пайплайна.
488 Глава 11. Пайплайн машинного обучения
11.2.6. Загрузка очищенного датасета
в бакет Google Cloud Storage
Для упрощения пайплайна мы загрузим в бакет Cloud Storage обработанный,
сгенерированный блокнотом подготовки данных (https://mng.bz/rKjB) датасет,
чтобы он был доступен для остальных этапов ML-пайплайна. В реальном приложении мы бы включили шаги очистки данных непосредственно в пайплайн, но
для простоты запустим его с уже очищенными данными. Выполните следующие
шаги, чтобы загрузить подготовленный датасет в Google Cloud Storage.
1. Загрузите CSV-версию очищенного датасета в тот же бакет, который вы
создали для загрузки модели.
2. В главном меню Google Cloud Console выберите Cloud Storage Buckets
(рис. 11.17).
3. На странице Buckets выберите бакет, созданный в главе 10 для хранения обу
ченной модели. На странице Bucket details выберите Create Folder (Создать
папку) (рис. 11.18).
4. Введите processed_dataset (обработанный датасет) в поле имени и нажмите
Create.
5. Выберите новую папку, которую вы только что создали (рис. 11.19).
6. Нажмите Upload Files и выберите CSV-файл, содержащий обработанную
версию датасета Куала-Лумпура (выходные данные блокнота подготовки
данных).
7. Вы увидите этот файл на странице Bucket details после завершения загрузки.
Нажмите на три точки, затем на Copy gsutil URI (рис. 11.20).
URI-адрес для gsutil будет выглядеть следующим образом: gs://first-projectml-tabular-bucket/processed_dataset/kl_real_estate_output.csv
Теперь, когда мы загрузили очищенный набор данных в бакет Google Cloud,
можем использовать его для создания датасета Vertex AI.
11.2. Этапы подготовки ML-пайплайна 489
Рис. 11.17. Настройка параметров загрузки
Рис. 11.18. Создание папки
Рис. 11.19. Выбор папки
490 Глава 11. Пайплайн машинного обучения
Рис. 11.20. Копирование URI gsutil
11.2.7. Создание управляемого набора данных Vertex AI
ML-пайплайн вызывает Vertex AI SDK для обучения модели; он идентифицирует
датасет, используемый для обучения, как управляемый набор данных Vertex AI.
Подробнее об этом см. в документации: https://mng.bz/VVRP.
Vertex AI SDK автоматически выполняет следующие действия, чтобы сделать
управляемый набор данных доступным для скрипта обучения.
Копирует содержимое датасета в Cloud Storage.
Разделяет датасет на обучающую, валидационную и тестовую части. Доля
данных для каждой из них задается в файле конфигурации пайплайна
pipeline_config.yml (рис. 11.21).
Рис. 11.21. Пропорции обучающей, валидационной и тестовой частей
в конфигурации пайплайна
Разделяет каждую из подвыборок на несколько файлов CSV. На рис. 11.22
показан пример того, как выглядят эти файлы датасета в Cloud Storage.
11.2. Этапы подготовки ML-пайплайна 491
Рис. 11.22. Обработанный датасет в Google Cloud Storage
Теперь, когда мы узнали, как датасет размещается в облачном хранилище, давайте
рассмотрим шаги по созданию набора обучающих данных Vertex AI.
1. В Vertex AI выберите Datasets (Датасеты). На странице Datasets нажмите
Create (рис. 11.23).
Рис. 11.23. Создание датасета
492 Глава 11. Пайплайн машинного обучения
2. На странице Create dataset задайте имя kuala-lumpur-real-estate (недвижимость в Куала-Лумпуре), выберите вкладку Tabular (Табличный), выберите Regression/Classification (Регрессия/Классификация) и нажмите Create
(рис. 11.24).
Рис. 11.24. Указание деталей датасета
3. На вкладке Source выберите Select CSV files from Cloud Storage (Выбрать CSVфайлы из Cloud Storage). В поле Import file path (Импортировать путь к файлу)
нажмите Browse, выберите Cloud Storage bucket location (Расположение бакета
11.2. Этапы подготовки ML-пайплайна 493
в Cloud Storage), куда вы загрузили обработанный файл обучения в предыдущем разделе, и нажмите Continue (рис. 11.25).
Рис. 11.25. Указание источника для датасета
4. Обратите внимание на значение идентификатора датасета, который вы только
что создали (рис. 11.26).
Это значение, которое необходимо задать для dataset_id в файле конфигурации
пайплайна pipeline_config.yml (рис. 11.27).
494 Глава 11. Пайплайн машинного обучения
Рис. 11.26. Идентификатор датасета в Google Cloud Console
Рис. 11.27. dataset_id в файле конфигурации пайплайна
Поздравляем! Вы настроили управляемый набор обучающих данных Vertex AI
пайплайна машинного обучения.
11.3. Определение ML-пайплайна
К настоящему моменту в этой главе мы выполнили следующие подготовительные шаги для ML-пайплайна.
1. Создали сервисный аккаунт и его ключ.
2. Загрузили ключ сервисного аккаунта в каталог, откуда мы будем запускать
скрипт пайплайна.
11.3. Определение ML-пайплайна 495
3. Загрузили очищенный датасет в Cloud Storage.
4. Создали управляемый набор очищенных данных Vertex AI.
В этом разделе мы используем подготовленные элементы для создания пайплайна машинного обучения, который принимает предварительно обработанный
датасет на входе и выдает обученную модель, развернутую на эндпоинте Vertex
AI на выходе.
11.3.1. Что выбрать: локальную реализацию или ML-пайплайн
Прежде чем продолжить определение ML-пайплайна, давайте сравним его с локальной настройкой для обучения модели прогнозирования цен на недвижимость
в Куала-Лумпуре из главы 10. На рис. 11.28 показано это сравнение и выделены
некоторые различия между двумя реализациями.
Обучение модели
в локальной системе
CSV-файл
с входными
данными
Локальная
файловая система
Блокнот
с кодом для
очистки данных
Файл
конфигурации
очистки
Обучение модели в ML-пайплайне в Vertex AI
CSV-файл
с входными
данными
Блокнот
с кодом для
очистки данных
Файл
конфигурации
очистки
CSV-файл
с очищенными
данными
pickle-файл
очищенных
данных
Блокнот
с кодом для
обучения модели
Локальная
файловая система
Файл
конфигурации
обучения
Обученная
модель
Обученная модель
Модель, развернутая
на эндпоинте
CSV-файлы
с обучающей
выборкой
Файл
конфигурации
пайплайна
CSV-файлы
с валидационной
выборкой
Скрипт
пайплайна
Инстанс Cloud Shell
Облачное
хранилище
Реестр
моделей
CSV-файлы
с тестовой
выборкой
Файл
конфигурации
обучения
Скрипт
обучения
модели
Облачное
хранилище
Контейнер
Обученная модель
Реестр
моделей
Модель, развернутая на эндпоинте
Рис. 11.28. Обучение на локальной системе по сравнению с обучением с помощью
пайплайна ML
496 Глава 11. Пайплайн машинного обучения
На рис. 11.28 сравниваются структуры процессов обучения для полностью
локальной реализации и с использованием пайплайна машинного обучения.
Основные отличия заключаются в следующем.
Процесс очистки данных идентичен. В реальном рабочем пайплайне мы бы
переместили этот этап обработки данных в среду Vertex AI и сделали его частью ML-пайплайна, но, чтобы максимально его упростить, мы пропускаем
этот этап для нашей реализации и запускаем пайплайн с уже очищенным
датасетом.
В локальной реализации результатом процесса очистки данных является
файл pickle. Чтобы избежать проблем совместимости, мы перешли на файл
CSV для ML-пайплайна, который принимает содержимое этого CSV-файла
и разбивает его на обучающие, валидационные и тестовые подмножества,
каждое из которых сегментируется на несколько CSV-файлов в Cloud Storage.
Код обучения в реализации ML-пайплайна находится в файле Python .py
(скрипт обучения модели), а не в блокноте. Значимые изменения кода обучения для его работы в контейнерной среде описываются в следующем разделе.
В реализации ML-пайплайна файл конфигурации обучения модели находится в облачном хранилище Cloud Storage, поэтому его местоположение
может использоваться скриптом пайплайна в качестве параметра для скрипта
обучения модели.
Скрипт пайплайна — это новый компонент, который настраивает входные
данные, необходимые для скрипта обучения модели, использует Vertex AI
SDK для создания контейнера для скрипта обучения и запускает его для
выполнения обучения модели.
Файл конфигурации пайплайна — это новый компонент, который содержит
параметры для скрипта пайплайна, включая встроенные контейнеры Vertex
AI для использования в ML-пайплайне; долю очищенного датасета для
каждого из обучающих, валидационного и тестовых подмножеств; идентификатор датасета и местоположение кода для скрипта обучения.
Обученная модель автоматически помещается в реестр моделей в реализации
ML-пайплайна и развертывается на эндпоинте Vertex AI. В локальной реализации мы вручную загружали модель в Cloud Storage, а затем развертывали
ее на эндпоинте.
Эндпоинт, который является результатом как локальной реализации, так и реа
лизации ML-пайплайна, можно подключить к нашему веб-развертыванию,
просто обновив параметр endpoint_id в файле конфигурации сервера Flask, как
показано на рис. 11.29.
11.3. Определение ML-пайплайна 497
Модель, развернутая на эндпоинте
Локальная система
Модель, развернутая на эндпоинте
ML-пайплайн
Файл конфигурации
сервера Flask
сервер Flask
home.html
show-prediction.html
Рис. 11.29. Веб-развертывание на эндпоинте в локальной системе или с помощью
ML-пайплайна
Более подробную информацию о рабочем процессе обучения пользовательской
модели на Vertex AI можно найти в документации: https://mng.bz/xKjW.
11.3.2. Знакомство с контейнерами
Одним из ключевых моментов ML-пайплайна в Vertex AI является использование контейнеров для упрощения автоматизации и повышения гибкости процесса обучения модели. В этом разделе мы кратко рассмотрим контейнеры и их
преимущества. Если вы уже знакомы с этой концепцией и Docker, то можете
пропустить раздел.
Контейнер — это программная конструкция, которая позволяет упаковывать
приложение вместе с его зависимостями, чтобы обеспечить его предсказуемость
и эффективное выполнение в различных средах. Google Cloud использует
контейнеры Docker. Их подробное описание выходит за рамки книги, но нам
498 Глава 11. Пайплайн машинного обучения
необходимо уделить время их изучению, чтобы понять, почему они используются
для пайплайнов и какие ограничения накладывают на код. Более подробную
информацию о контейнерах см. на сайте Docker: https://www.docker.com/resources/
what-container/.
11.3.3. Преимущества контейнеризации в ML-пайплайне
Использование контейнеров для упаковки кода обучения означает, что нам не
нужно беспокоиться о библиотеках Python, поскольку контейнер поставляется
со всеми необходимыми библиотеками, причем предварительно настроенными.
Кроме того, код легко воспроизвести в любой среде. Vertex AI предоставляет ряд
готовых образов контейнеров для самых популярных фреймворков машинного
обучения, включая PyTorch, TensorFlow и XGBoost. Для нашего ML-пайплайна
мы используем предсобранные контейнеры TensorFlow. Подробную информацию
о них см. в документации Vertex AI:
предсобранные контейнеры для обучения пользовательских моделей — https://
mng.bz/AQ8z;
предсобранные контейнеры для прогнозирования — https://mng.bz/ZlRP.
Если наше обучение становится более требовательным (например, к скорости
выполнения цикла обучения или вычислительным ресурсам, необходимым для
завершения цикла обучения заданной продолжительности), можем использовать
контейнерный подход к обучению, чтобы распределить обучение по нескольким
вычислительным системам. Для простой задачи, такой как прогнозирование цен
на недвижимость в Куала-Лумпуре, одного узла более чем достаточно, но более
крупные приложения могут значительно выиграть от распределенного обучения.
Подробное объяснение всех доступных вариантов распределенного обучения
с Vertex AI выходит за рамки этой книги. Ознакомьтесь с документацией, если
вам интересны детали: https://mng.bz/RVmK.
11.3.4. Адаптация кода для запуска в контейнере
Теперь мы можем перейти к изучению изменений, которые требуются для
запуска соответствующего кода в контейнере. Чтобы понять разницу между
запуском кода в невиртуализированной среде и в контейнере, представьте, что
контейнер — это самостоятельная автономная машина, на которой выполняется код. В частности, код, работающий в контейнере, по умолчанию не будет
иметь доступ к файловой системе среды, из которой управляется контейнер.
На рис. 11.30 показано, как код из блокнота обучения модели взаимодействует
с файлами в файловой системе.
11.3. Определение ML-пайплайна 499
pickle-файл
очищенных
данных
Файлы
конфигурации
обучения
Блокнот с кодом
для обучения
модели
Рис. 11.30. Взаимодействие кода
обучения с внешними файлами
Обученная модель
Когда код обучения выполняется в контейнере, он не может получить доступ
к файлам во внешней локальной файловой системе. Артефакты, которые использует скрипт обучения модели, хранятся в Cloud Storage, а их местоположения
передаются скрипту обучения в виде URI. На рис. 11.31 показан пример того,
как интерпретировать URI Google Cloud Storage.
URI для Google
Cloud Storage
Имя
бакета
Имя
файла
gs://mybucket/myfile.csv
Рис. 11.31. Интерпретация
конфигурационного файла URI
Google Cloud Storage
В пайплайне мы используем два метода для передачи URI в скрипт обучения,
работающий в контейнере: через переменные окружения, установленные в контейнере Vertex AI SDK, и через список аргументов вызова job.run в скрипте
пайплайна, как показано на рис. 11.32.
Расположение, заданное
в списке аргументов
файла job.run
Расположение, заданное
переменной окружения AIP
CSV-файлы
обучающей выборки
Файл
конфигурации
обучения
CSV-файлы
валидационной выборки
CSV-файлы
тестовой выборки
Скрипт
обучения
модели
Обученная модель
Рис. 11.32. Взаимодействие кода обучения модели с содержимым в Google Cloud Storage
500 Глава 11. Пайплайн машинного обучения
Расположение данных для обучения модели (разделенных на обучающие, валидационные и тестовые подмножества) автоматически назначается переменными
окружения, которые устанавливаются в контейнере при его настройке скриптом
пайплайна. Это стандарт для всех контейнеров Vertex AI; см. документацию по
адресу https://mng.bz/2y70.
Способ передачи URI для файла конфигурации скрипту обучения модели не
является способом по умолчанию для Vertex AI. Если бы у нас был скрипт обу
чения с небольшим количеством аргументов, мы могли бы создать их список
argparser и передать его скрипту обучения. Однако файл конфигурации для
нашего приложения слишком сложен для такого подхода, поэтому вместо того,
чтобы передавать каждый аргумент по отдельности, мы передаем один-единственный аргумент: URI расположения в Cloud Storage, где сохранена копия
файла конфигурации. Таким образом, скрипту обучения достаточно получить
местоположение в Cloud Storage из списка аргументов и загрузить YAML-файл
оттуда. Как только аргументы загружены в словарь конфигурации в скрипте
обучения, остальная часть кода, который их использует, может работать без
изменений. Это важное преимущество.
11.3.5. Изменение кода обучения для работы в контейнере
В этом разделе мы рассмотрим, как изменить код для обучения (https://mng.
bz/1XJj), который мы запускали в Colab в главе 9, чтобы получить модель для
прогнозирования цен на недвижимость в Куала-Лумпуре. Так мы преобразуем
блокнот в скрипт обучения модели, который может работать во встроенном
контейнере Vertex AI. Ниже приведены основные изменения, которые мы внесли.
Удалены лишние импорты библиотек и связанный с ними код. Например,
нам не нужно строить диаграмму модели при запуске скрипта обучения, поэтому мы удалили код, связанный с plot_model.
Удален код, который разделяет датасет на обучающую, валидационную и тестовую части. В пайплайне машинного обучения Vertex AI SDK берет на себя
разделение датасета до запуска скрипта тестирования.
Добавлен код для интерпретации списка аргументов job.run, как показано
в листинге 11.1.
Обновлен код, который загружает файл конфигурации обучения, чтобы
читать содержимое файла конфигурации из URI Cloud Storage, переданного
скриптом пайплайна (config_bucket из листинга 11.1), а не из локальной файловой системы. Как показано в листинге 11.2, URI для файла конфигурации
в Cloud Storage (config_bucket) используется для копирования файла конфигурации из Cloud Storage в файл внутри контейнера, а затем содержимое
этого файла копируется в словарь config.
11.3. Определение ML-пайплайна 501
Листинг 11.1. Загрузка сохраненной модели Keras
parser = argparse.ArgumentParser()
Определяет объект argparser для
parser.add_argument(
аргументов, переданных Vertex AI SDK
'--config_bucket',
help='Config details',
Добавляет аргумент config_
required=True
bucket к объекту argparser
)
args = parser.parse_args().__dict__
Загружает аргументы, переданные
config_bucket = args['config_bucket']
Vertex AI SDK, в виде словаря
Получает URI файла конфигурации
из словаря аргументов
Листинг 11.2. Загрузка файла конфигурации обучения через аргумент URI
Получает суффикс пути
к файлу config_bucket
Получает префикс бакета config_bucket
bucket_name = config_bucket.split("/")[2]
object_name = "/".join(config_bucket.split("/")[3:])
storage_client2 = storage.Client()
bucket = storage_client2.bucket(bucket_name)
blob_out = bucket.blob(object_name)
destination_file_name = 'config.yml'
blob_out.\
download_to_filename(destination_file_name)
try:
with open (destination_file_name, 'r') as c_file:
config = yaml.safe_load(c_file)
except Exception as e:
print('Ошибка чтения файла конфигурации')
Определяет объект
storage.Client
Создает объект
хранилища для бакета
Создает объект
хранилища для файла
Задает имя копии файла
конфигурации в контейнере
Загружает файл из Cloud Storage
в контейнер
Считывает содержимое контейнерной
версии файла конфигурации в словарь
Скопированы значения переменных окружения AIP, которые Vertex AI
SDK устанавливает в контейнере и которые содержат шаблоны URI для
CSV-файлов, созданные SDK в Google Storage и содержащие обучающее,
валидационное и тестовое подмножества датасета (листинг 11.3).
Листинг 11.3. Копирование значений переменных окружения AIP
def assign_container_env_variables():
OUTPUT_MODEL_DIR = os.getenv("AIP_MODEL_DIR")
TRAIN_DATA_PATTERN = \
os.getenv("AIP_TRAINING_DATA_URI")
EVAL_DATA_PATTERN = \
os.getenv("AIP_VALIDATION_DATA_URI")
TEST_DATA_PATTERN = \
os.getenv("AIP_TEST_DATA_URI")
return OUTPUT_MODEL_DIR, TRAIN_DATA_PATTERN, \
EVAL_DATA_PATTERN, TEST_DATA_PATTERN
Получает URI для местоположения
сохранения обученной модели
Получает URI для CSV-файлов
обучающего датасета
Получает URI для CSV-файлов
валидационного датасета
Получает URI для CSVфайлов тестового датасета
502 Глава 11. Пайплайн машинного обучения
Созданы датафреймы для каждого из шаблонов из переменных окружения
AIP. Для каждой из них мы обработали URI, получили список соответствую
щих файлов CSV (блобов) в Cloud Storage и собрали их в один датафрейм
(листинг 11.4).
Листинг 11.4. Создание датафрейма для подмножеств датасета
Для каждого шаблона файла
получает префикс бакета
Получает шаблон CSV-файла
bucket_pattern = tracer_pattern.split("/")[2]
pattern = "/".join(tracer_pattern.split("/")[3:])
pattern_client = storage.Client()
bucket = pattern_client.get_bucket(bucket_pattern)
blobs = bucket.list_blobs()
matching_files = [f"gs://{bucket_pattern}/{blob.name}" \
for blob in blobs if fnmatch.fnmatch(blob.name, pattern)]
merged_data = \
pd.concat([pd.read_csv(f) for f in matching_files],
ignore_index=True)
Создает датафрейм с содержимым
всех CSV-файлов, соответствующих
шаблону
Определяет объект
storage.Client
Получает список CSVфайлов в бакете, соответствующих шаблону
Получает список полных
URI для CSV-файлов, соответствующих шаблону
Сохранили обученную модель в месте, указанном в OUTPUT_MODEL_DIR — URI,
заданном Vertex AI SDK в качестве места для сохранения модели:
tf.saved_model.save(model, OUTPUT_MODEL_DIR)
С этими изменениями остальная часть кода обучения корректно работает в контейнере. Теперь, когда мы разобрали необходимые изменения для создания
скрипта обучения, в следующем разделе рассмотрим ключевые части скрипта
пайплайна, который настраивает контейнер для выполнения скрипта обучения.
11.3.6. Скрипт пайплайна
Мы уже разобрали скрипт обучения и можем изучить код, составляющий скрипт
пайплайна. Его полная версия доступна по адресу https://mng.bz/PdRn.
Назовем ключевые части скрипта пайплайна.
Загрузка файла конфигурации пайплайна: https://mng.bz/JYdV.
Установка аргументов для скрипта обучения:
model_args =['--config_bucket', config['config_bucket_path']]
Создание объекта CustomTrainingJob, который указывает расположение
скрипта обучения script_path, предсобранный образ контейнера для обу
чения container_uri и дополнительные библиотеки Python, которые необходимо установить в него (requirements), как показано в листинге 11.5.
11.3. Определение ML-пайплайна 503
Листинг 11.5. Создание объекта CustomTrainingJob
def create_job(config):
model_display_name = '{}-{}'.format(config['ENDPOINT_NAME'], TIMESTAMP)
job = aiplatform.CustomTrainingJob(
display_name='train-{}'.format(model_display_name),
script_path = config['script_path'],
container_uri=config['train_image'],
Устанавливает готовый образ
staging_bucket = config['staging_path'],
контейнера Vertex AI для запуска
requirements=['gcsfs'],
скрипта обучения
model_serving_container_image_uri= \
config['deploy_image']
Определяет список дополнительУстанавливает готовый образ
)
ных требований, которые необхоконтейнера Vertex AI для
return job
использования в прогнозировании
димо установить в контейнере
Определение пути для управляемого набора данных, предназначенного для
обучения (с использованием идентификатора датасета, созданного в разделе 11.2.7), и создание объекта TabularDataset с помощью этого пути:
dataset_path = \
'projects/'+config['project_id']+\
'/locations/'+config['region']+\
'/datasets/'+config['dataset_id']
ds = aiplatform.TabularDataset(dataset_path)
Выполнение задания, определенного ранее, с указанием созданного здесь датасета, доли набора данных, используемых для обучения, валидации и тестирования, и типа машины machine_type, используемой для обучения (листинг 11.6).
Листинг 11.6. Запуск задания
def run_job(job, ds, model_args,config):
model_display_name = \
Связывает задание с датасетом
'{}-{}'.format(config['ENDPOINT_NAME'], TIMESTAMP)
model = job.run(
Задает пропорции данных,
dataset=ds,
используемых для обучения,
training_fraction_split = \
валидации и тестирования
config['training_fraction_split'],
validation_fraction_split = config['validation_fraction_split'],
test_fraction_split = config['test_fraction_split'],
model_display_name=model_display_name,
args=model_args,
Задает список аргументов (содержащий
machine_type= config['machine_type']
URI для файла конфигурации скрипта
)
тестирования)
return model
Создание конечной точки и развертывание на ней обученной модели (лис
тинг 11.7).
Листинг 11.7. Развертывание обученной модели на эндпоинте
def deploy_model(model,config):
Задает характеристики эндпоинта
endpoints = aiplatform.Endpoint.list(
filter='display_name="{}"'.format(config['ENDPOINT_NAME']),
order_by='create_time desc',
project=config['project_id'],
location=config['region']
)
Создает эндпоинт
504 Глава 11. Пайплайн машинного обучения
def deploy_model(model,config):
Задает характеристики эндпоинта
endpoints = aiplatform.Endpoint.list(
filter='display_name="{}"'.format(config['ENDPOINT_NAME']),
order_by='create_time desc',
project=config['project_id'],
location=config['region']
)
Создает эндпоинт
endpoint = aiplatform.Endpoint.create(
display_name=config['ENDPOINT_NAME'],
project=config['project_id'],
location=config['region']
Развертывает модель
)
на эндпоинте
model.deploy(
endpoint=endpoint,
traffic_split={"0": 100},
machine_type=config['machine_type_deploy'],
min_replica_count=1,
max_replica_count=1,
)
Листинг 11.8 представляет собой основную функцию скрипта пайплайна,
которая вызывает функции для его запуска.
Листинг 11.8. Основная функция скрипта пайплайна
start_time = time.time()
config = get_pipeline_config('pipeline_config.yml')
model_args = ['--config_bucket', config['config_bucket_path']]
Задает
job = create_job(config)
характеристики
dataset_path = \
эндпоинта
'projects/'+config['project_id']+\
'/locations/'+config['region']+\
'/datasets/'+config['dataset_id']
ds = aiplatform.TabularDataset(dataset_path)
Создает эндпоинт
model = run_job(job, ds, model_args,config)
if config['deploy_model']:
Развертывает модель
deploy_model(model,config)
на эндпоинте
print("пайплайн завершен")
Чтобы запустить скрипт пайплайна, выполните следующие действия.
Клонируйте репозиторий https://github.com/lmassaron/ml_on_tabular_data в новый каталог в Cloud Shell и перейдите в каталог chapter_11.
Обновите файл конфигурации пайплайна, чтобы убедиться, что project_id
и region соответствуют настройкам вашего проекта, в dataset_id указан идентификатор вашего управляемого датасета, в staging_path — путь промежуточного хранения, а в config_bucket_path — местоположение в Cloud Storage,
куда вы скопировали файл конфигурации скрипта обучения (рис. 11.33).
В корневом каталоге, куда вы клонировали репозиторий, введите следующую
команду:
python_pipeline_script.py
11.3. Определение ML-пайплайна 505
Рис. 11.33. Взаимодействие обучающего кода с содержимым в облачном хранилище
Обратите внимание, что выполнение всего скрипта пайплайна может занять
10 минут или более. Если скрипт завершится ошибкой, вы получите сообщение
со ссылкой на файл журнала, содержащий диагностические сведения о запуске
обучения. Если скрипт выполнится успешно, в выводе будет указано, что «пайп
лайн завершен», и время, потребовавшееся на выполнение.
11.3.7. Тестирование модели, обученной в пайплайне
После запуска скрипта пайплайна для обучения и развертывания модели вы
можете использовать полученный эндпоинт Vertex AI для проверки модели
в той же веб-среде, с которой мы работали в главе 10. Обратите внимание, что
тестирование эндпоинта таким способом отличается от того, что вы бы сделали
в среде продакшена. Однако использование того же веб-развертывания, что
и в главе 10, упрощает процесс тестирования для этого упражнения.
Шаги по тестированию модели, обученной в пайплайне, следующие.
1. В Google Cloud Console перейдите на страницу Endpoints в Vertex AI. Скопируйте идентификатор развертывания, созданного пайплайном машинного
обучения, как показано на рис. 11.34.
Рис. 11.34. Идентификатор эндпоинта для модели, созданной пайплайном
506 Глава 11. Пайплайн машинного обучения
2. На той же локальной системе, где вы тестировали первоначальное развертывание эндпоинта с помощью Flask в главе 10, вставьте скопированный
идентификатор эндпоинта в значение параметра endpoint_id в файле конфигурации flask_web_deploy_config.yml и сохраните файл:
endpoint:
project: "1028332300603"
endpoint_id: "1447850105594970112"
location: "us-central1"
3. На локальной системе запустите модуль сервера Flask:
python flask_endpoint_deploy.py
4. После этого перейдите в браузере по адресу localhost:5000. Страница home.
html отобразится, как показано на рис. 11.35. Когда вы нажмете на кнопку
«Get prediction», будет вызвана модель, обученная и развернутая на эндпоин
те Vertex AI с помощью пайплайна машинного обучения (см. рис. 11.35).
Рис. 11.35. Вид страницы home.html
Обратите внимание, что версия TensorFlow, используемая в предустановленном
контейнере для обучения модели, должна соответствовать версии TensorFlow
11.4. Использование генеративного ИИ для создания ML-пайплайна 507
в среде, где запускается веб-приложение для тестирования эндпоинта. Так,
если мы хотим выполнить развертывание эндпоинта в среде с TensorFlow
2.9, то в файле конфигурации пайплайна необходимо указать значение для
train_image (предустановленный контейнер для обучения), совместимое
с этой версией TensorFlow, например us-docker.pkg.dev/vertex-ai/training/
tf-cpu.2-9:latest.
Если при запуске скрипта пайплайна в Cloud Shell возникает ошибка protobuf,
попробуйте выполнить следующую команду для указания версии protobuf:
pip install protobuf==3.20.*
Если вы хотите поэкспериментировать с различными конфигурациями обучения, то можете изменить конфигурационный файл, загрузить его в Cloud Storage
(убедившись, что значение config_bucket_path в файле конфигурации пайплайна
соответствует URI для файла конфигурации обучения) и перезапустить скрипт
пайплайна. Можно использовать веб-приложение для проверки новой модели,
обновив значение endpoint_id в файле конфигурации пайплайна, чтобы оно
соответствовало идентификатору нового эндпоинта, и повторив шаги из этого
раздела. Инкапсулируя несколько шагов в рабочем процессе машинного обучения в ML-пайплайн, мы упрощаем получение воспроизводимых результатов
и эксперименты с новыми настройками.
11.4. Использование генеративного ИИ для создания
ML-пайплайна
До сих пор в этой главе мы видели, как использовать комбинацию действий
в Google Cloud и ручного написания скриптов, чтобы настроить базовый MLпайплайн обучения и развертывания модели для табличных данных. В этом
разделе мы исследуем, как можно использовать возможности генеративного
ИИ — Gemini для Google Cloud, представленные в главе 10, для упрощения или
автоматизации некоторых из этих действий. Как мы видели в главе 10, Gemini
может нам помочь четырьмя способами:
отвечать на вопросы о Google Cloud;
генерировать код на основе текстового описания;
интерпретировать код, то есть, получая фрагмент кода, генерировать текст,
который объясняет, что делает этот код. Эту возможность можно использовать для понимания кода, который мы адаптируем из других источников,
а также для документирования собственного кода;
анализировать записи логов, чтобы помочь в устранении неполадок.
508 Глава 11. Пайплайн машинного обучения
11.4.1. Использование Gemini для ответов на вопросы
об ML‑пайплайне
Как мы видели в главе 10, мы можем использовать возможности Gemini для
Google Cloud, чтобы получать ответы на вопросы об этом сервисе. Ниже приведены некоторые примеры вопросов о создании пайплайна ML, на которые
Gemini может ответить.
Что такое ML-пайплайн? Хотя Gemini для Google Cloud специально обучен
для этого ресурса, он может отвечать и на общие вопросы о технологиях, такие
как этот. Обратите внимание, что ответ, показанный на рис. 11.36, является
общеприменимым и не ограничивается только Google Cloud. Сведения взяты
из различных достоверных источников, включая документацию TensorFlow
и scikit-learn:
что такое пайплайн машинного обучения? (https://mng.bz/wJjP);
создание пайплайна данных (https://cs230.stanford.edu/blog/datapipeline/);
ML-пайплайны с scikit-learn (https://mng.bz/qxjr).
Рис. 11.36. Gemini для Google
Cloud отвечает на вопрос «What
is an ML pipeline?» («Что такое
ML‑пайплайн?»)
Что такое пайплайн Vertex AI? Когда мы берем тот же вопрос и уточняем
его, как показано на рис. 11.37, Gemini для Google Cloud дает ответ, кото-
11.4. Использование генеративного ИИ для создания ML-пайплайна 509
рый характерен для внедрения пайплайна машинного обучения в Google
Cloud.
Рис. 11.37. Gemini для Google Cloud
отвечает на вопрос «What is a Vertex AI
pipeline?» («Что такое пайплайн Vertex AI?»)
Рис. 11.38. Gemini для Google
Cloud отвечает на вопрос «What
are Vertex AI prebuilt containers
for training custom models?»
(«Что такое предустановленные
контейнеры Vertex AI для обучения
пользовательских моделей?»)
Что такое предустановленные контейнеры Vertex AI для обучения пользовательских моделей? Наконец, давайте попробуем задать вопрос, связанный с конкретной задачей, которую мы решали в этой главе. Как видно на
рис. 11.38, ответ, предоставленный Gemini для Google Cloud, описывает как
готовые контейнеры для обучения пользовательских моделей, так и цель их
использования.
510 Глава 11. Пайплайн машинного обучения
В этом разделе мы увидели, как можно использовать Gemini для Google Cloud,
чтобы получать ответы на общие и конкретные вопросы о создании MLпайплайна. В следующем разделе рассмотрим, как можно применять Gemini,
чтобы сгенерировать код, необходимый для пайплайна машинного обучения.
11.4.2. Использование Gemini для генерации кода ML-пайплайна
Теперь, когда мы увидели, как Gemini для Google Cloud отвечает на вопросы по
созданию пайплайна машинного обучения, давайте перейдем непосредственно
к генерации кода для него с помощью этого генеративного ИИ.
Gemini для Google Cloud доступен в нескольких IDE, поддерживаемых платформой, включая VS Code, Cloud Workstations и Cloud Shell Editor. В этом разделе мы будем использовать среду Cloud Shell Editor. Если вам нужно освежить
знания об этом инструменте, см. обзорную документацию: https://mng.bz/7pvv.
Мы увидим, как Gemini для Google Cloud может генерировать код для функций
в скрипте пайплайна https://mng.bz/PdRn, используя сигнатуры функций и ввод
ные комментарии из скрипта.
Для начала, если вы еще этого не сделали, следуйте документации, чтобы включить Gemini Code Assist в Cloud Shell Editor: https://mng.bz/mGja.
После активации Gemini Code Assist в Cloud Shell Editor откройте новый файл
Python в этой IDE и введите сигнатуру и вводный комментарий для функции
get_pipeline_config, как показано в листинге 11.9.
Листинг 11.9. Сигнатура для get_pipeline_config
def get_pipeline_config(path_to_yaml):
'''Загружает конфигурационный yaml-файл
Args:
path_to_yaml: yaml-файл содержит параметры для скрипта пайплайна
Returns:
config: словарь с параметрами, прочитанными из файла конфигурации
'''
Обратите внимание, что этот фрагмент кода не включает логику функции.
Чтобы Gemini для Google Cloud сгенерировал код для этой функции, просто
нажмите Enter. ИИ сгенерирует предварительный код курсивом, как показано
на рис. 11.39.
Нажмите Tab, чтобы принять этот предварительный код, а затем нажмите
Enter еще раз, чтобы сгенерировать следующие строчки кода, как показано на
рис. 11.40.
11.4. Использование генеративного ИИ для создания ML-пайплайна 511
'''Загружает конфигурационный yaml-файл
path_to_yaml: yaml-файл содержит параметры для скрипта пайплайна
config: словарь с параметрами, прочитанными из файла конфигурации
Рис. 11.39. Первый блок предварительного кода, сгенерированный Gemini
для Google Cloud
Рис. 11.40. Второй блок предварительного кода, сгенерированный Gemini
для Google Cloud
Нажмите Tab еще раз, чтобы принять второй блок предварительного кода. Получившаяся функция показана в листинге 11.10.
Листинг 11.10. Функция get_pipeline_config
def get_pipeline_config(path_to_yaml):
'''Загружает конфигурационный yaml-файл
Args:
path_to_yaml: yaml-файл содержит параметры для скрипта пайплайна
Returns:
config: словарь с параметрами, прочитанными из файла конфигурации
'''
with open(path_to_yaml) as file:
Первый блок кода, сгенерированный
config = yaml.safe_load(file)
Gemini для Google Cloud
return config
Второй блок кода, сгенерированный
Gemini для Google Cloud
512 Глава 11. Пайплайн машинного обучения
Код в листинге 11.10 неидентичен написанному вручную для функции get_
pipeline_config, как показано в листинге 11.11.
Листинг 11.11. Функция get_pipeline_config: код, написанный вручную
def get_pipeline_config(path_to_yaml):
'''Загружает конфигурационный yaml-файл
Args:
path_to_yaml: yaml-файл содержит параметры для скрипта пайплайна
Returns:
config: словарь с параметрами, прочитанными из файла конфигурации
'''
print("path_to_yaml "+path_to_yaml)
try:
Написанный вручную код вклюwith open (path_to_yaml, 'r') as c_file:
чает обработку исключений при
config = yaml.safe_load(c_file)
открытии файла
except Exception as e:
Написанный
вручную код включаprint('Ошибка чтения файла конфигурации')
ет параметр ‘r’ при открытии файла
return config
Сравнивая код, сгенерированный Gemini для Google Cloud в листинге 11.10,
с написанным вручную в листинге 11.11, мы видим два отличия:
написанный вручную код включает обработку исключений для решения
проблем с открытием файла конфигурации;
написанный вручную код включает параметр 'r' в операции открытия файла.
Функция get_pipeline_config проста, но тем не менее Gemini для Google Cloud
смог сгенерировать рабочий код для нее.
Вот некоторые дополнительные мысли по поводу генерации кода Gemini для
Google Cloud.
Вам не обязательно принимать предварительно сгенерированный код целиком. Чтобы принимать его пошагово, нажмите CTRL + Стрелка вправо для
принятия одного токена.
Чтобы отменить всю сгенерированную версию кода и начать заново, нажмите
ESC — и весь предварительный код будет стерт.
При многократном запросе генерации кода от Gemini для Google Cloud
с абсолютно одинаковыми входными данными не гарантируется получение
идентичного кода. Так, в примере get_pipeline_config иногда ИИ генерировал функцию в два этапа, как показано в этом разделе, а иногда целиком всю
функцию, включая оператор return, за один шаг.
Теперь, когда мы научились использовать генеративный ИИ для создания кода,
давайте посмотрим, как можно использовать Gemini для его объяснения.
11.4. Использование генеративного ИИ для создания ML-пайплайна 513
11.4.3. Использование Gemini для объяснения кода
ML‑пайплайна
Теперь, когда мы увидели пример того, как Gemini для Google Cloud может
генерировать код, давайте проверим способность ИИ интерпретировать его.
Для этого скопируйте код из листинга 11.12 (функцию main из скрипта пайп
лайна) в новый файл в Cloud Shell Editor.
Листинг 11.12. Функция get_pipeline_config
if __name__ == '__main__':
start_time = time.time()
# загрузка параметров конфигурации
config = get_pipeline_config('pipeline_config.yml')
# все аргументы, отправленные в скрипт обучения, запущенный в контейнере,
# передаются через yaml-файл в Cloud Storage, URI которого –
# единственный аргумент, отправляемый при загрузке
model_args = ['--config_bucket', config['config_bucket_path']]
print("model_args: ",model_args)
# создание объекта CustomTrainingJob
job = create_job(config)
# определение объекта TabularDataset
# для использования при запуске CustomTrainingJob
dataset_path = \
'projects/'+config['project_id']+\
'/locations/'+config['region']+\
'/datasets/'+config['dataset_id']
ds = aiplatform.TabularDataset(dataset_path)
# запуск объекта CustomTrainingJob для получения обученной модели
model = run_job(job, ds, model_args,config)
print("развертывание начинается")
# развертывание модели на эндпоинте Vertex AI
if config['deploy_model']:
deploy_model(model,config)
print("пайплайн завершен")
# выводим время, потребовавшееся на выполнение скрипта
print("--- %s с. ---" % (time.time() - start_time))
После того как вы вставите код из листинга 11.13 в файл, выделите код, а затем
выберите значок Gemini for Google Cloud Smart Actions на панели инструментов
Cloud Shell Editor (рис. 11.41).
Рис. 11.41. Значок Gemini for Google Cloud Smart Actions
В появившемся меню выберите пункт Explain this (Объясни) (рис. 11.42).
514 Глава 11. Пайплайн машинного обучения
Рис. 11.42. Пункт меню Explain this
При выборе этого варианта на левой панели Cloud Shell Editor появится пояснение
к коду (рис. 11.43). Возможности объяснения кода Gemini применимы к широкому спектру языков, включая Python, Java и JavaScript. Вы можете использовать
эту опцию для изучения незнакомого кода, а также для создания документации.
Рис. 11.43. Объяснение кода
До сих пор в этом разделе мы обсуждали, как можно использовать возможности генеративного ИИ для ответов на вопросы, генерации и объяснения кода.
В следующем подразделе рассмотрим, как использовать Gemini для анализа
записей логов.
11.4.4. Использование Gemini для анализа записей логов
В Google Cloud есть лог, который можно использовать для отслеживания поведения среды и отладки проблем. Однако иногда записи логов бывает сложно
11.4. Использование генеративного ИИ для создания ML-пайплайна 515
интерпретировать. Gemini может помочь вам понять суть записи лога, резюмируя
ее содержание. В этом подразделе мы рассмотрим, как использовать Gemini для
извлечения максимума информации из логов Google Cloud.
Чтобы реализовать эту возможность, будем использовать настройку базовой
модели в Vertex AI: возьмем предварительно обученную модель и дообучим ее
с помощью датасета в формате JSONL (JSON Lines: https://jsonlines.org/). Более
подробную информацию о настройке текстовых моделей в Vertex AI см. в документации: https://mng.bz/5goO.
Для подготовки примера создайте новую папку с именем staging в бакете Cloud
Storage, созданном в главе 10.
В Vertex AI в Google Cloud Console выберите Vertex AI StudioLanguage (Язык).
На странице Language выберите Tune and Distill (Дообучение и дистилляция),
а затем Create tuned model (Дообучить модель) (рис. 11.44).
Рис. 11.44. Страница Language в Vertex AI Studio
На панели Tuning method (Метод дообучения) страницы Create tuned model
сделайте следующее (рис. 11.45).
Укажите название модели в поле Tuned model name (Имя дообучаемой модели).
Укажите URI для промежуточной папки, которую вы создали в начале этого
раздела, в поле Output Directory (Выходная директория).
Нажмите Continue.
516 Глава 11. Пайплайн машинного обучения
Рис. 11.45. Панель Tuning method страницы Create tuned model
На панели Tuning dataset (Датасет для дообучения) страницы Create Tuned Model
выполните следующие действия (рис. 11.46).
Выберите Existing File on Cloud Storage (Существующий файл в облачном
хранилище).
Введите URI для этого примера файла JSONL cloud-samples-data/vertexai/model-evaluation/peft_train_sample.jsonl в поле Cloud storage file path
(Путь к файлу в облачном хранилище). Подробную информацию о примерах
JSONL см. в документации: https://mng.bz/6ene.
Нажмите кнопку Start Tuning (Начать дообучение).
После нажатия кнопки Start tuning вы увидите список дообучаемых моделей со
статусом Running (Выполняется), как показано на рис. 11.47.
После завершения настройки статус изменится на Succeeded (Успешно завершено) (рис. 11.48).
11.4. Использование генеративного ИИ для создания ML-пайплайна 517
Рис. 11.46. Панель Tuning dataset на странице Create tuned model
Рис. 11.47. Статус задания по настройке
Рис. 11.48. Задание по дообучению модели успешно завершено
518 Глава 11. Пайплайн машинного обучения
Если дообучение не удалось, не пугайтесь. Целью этого конкретного упражнения
является анализ ошибки, поэтому ничего страшного, если операция по какой-то
причине не увенчалась успехом.
После завершения дообучения введите «logs explorer» в поле поиска в верхней
части консоли, чтобы перейти на страницу Logs Explorer (Просмотр логов). Эта
страница предоставляет множество опций для проверки логов, созданных Google
Cloud. Сейчас мы просто посмотрим на одну из ошибок. Для этого выберите Error
(Ошибка) в левом нижнем углу страницы Logs Explorer (рис. 11.49).
Рис. 11.49. Страница Logs Explorer и вкладка Error
На панели результатов запроса Query Results в нижней части страницы отображаются ошибки (рис. 11.50).
Выберите одну из этих записей об ошибке, чтобы развернуть ее, и нажмите Explain
this log entry (Объяснить эту запись в логе) (рис. 11.51).
Справа Gemini выводит объяснение ошибки (рис. 11.52). В нем ИИ резюмирует
вложенные записи в логе, что облегчает их чтение и интерпретацию. Обратите
внимание, что объяснение, которое вы увидите, будет зависеть от ошибки, которую вы выберете из лога.
11.4. Использование генеративного ИИ для создания ML-пайплайна 519
Рис. 11.50. Панель Query results с отображенными ошибками
Рис. 11.51. Развернутая запись об ошибке
11.4.5. Дообучение базовой модели в Vertex AI
В предыдущем подразделе мы увидели, как использовать возможности Gemini
для Google Cloud для интерпретации логов ошибок. Стоит подробнее рассмотреть действия для генерации логов, которые мы могли бы изучить с помощью
Gemini. Вот краткое изложение того, что мы сделали.
Мы начали с одной из базовых моделей, доступных в Vertex AI, — text-bison.
Она разработана для различных задач обработки естественного языка, таких
как создание контента и классификация.
520 Глава 11. Пайплайн машинного обучения
Рис. 11.52. Объяснение ошибки
Мы применили контролируемое дообучение (supervised tuning), чтобы адаптировать модель text-bison к конкретному сценарию использования — в нашем случае для классификации медицинских транскриптов. Чтобы узнать
больше о контролируемом дообучении базовых моделей в Vertex AI, см.
документацию: https://mng.bz/nR15.
Датасет, который мы использовали для дообучения, содержал транскрипты
медицинских диагнозов в паре с присвоенными им метками классов, как
показано в листинге 11.13.
Листинг 11.13. Пример записи из датасета для дообучения
{
"input_text":"TRANSCRIPT: \nREASON FOR CONSULTATION: ,
Loculated left effusion,
Медицинский транскрипт
multilobar pneumonia.\n\n LABEL:",
"output_text":"Consult - History and Phy."
Метка класса
}
URI для этого датасета — gs://cloud-samples-data/vertex-ai/modelevaluation/peft_train_sample.jsonl.
11.4. Использование генеративного ИИ для создания ML-пайплайна 521
Рис. 11.53. Выбор дообученной модели в Vertex AI Studio
После завершения процесса дообучения
вы можете протестировать получившую
ся модель в Vertex AI Studio, выбрав
LanguageTune and Distill, а затем Test в строке
для модели, дообученной вами в предыдущем подразделе (рис. 11.53).
Редактор промптов открывается с предварительно выбранной дообученной моделью
(рис. 11.54).
Чтобы испытать дообученную модель, введите следующий текст в поле для промпта
и отправьте:
TRANSCRIPT: \nIMPRESSION: ,EEG during
wakefulness,
drowsiness, and sleep with synchronous
video monitoring demonstrated no evidence
of focal or epileptogenic activity.\n\n
LABEL:
Ответ показан на рис. 11.55 (Sleep medicine –
Сомнология).
Теперь измените модель обратно на базовую
text-bison@001 (рис. 11.56) и снова отправьте промпт.
Рис. 11.54. Редактор промптов
с выбранной дообученной моделью
522 Глава 11. Пайплайн машинного обучения
Рис. 11.55. Ответ содержит
рекомендации сомнолога
Рис. 11.56. Изменение модели обратно
на text-bison@001
В чем разница между ответом, полученным на промпт от дообученной модели
и недообученной базовой? В первом случае вы получаете все возможности
базовой модели вместе с релевантными ответами для задачи классификации
медицинских транскриптов. Если вы изучите датасет, который мы использовали для настройки базовой модели (с URI gs://cloud-samples-data/vertex-ai/
model-evaluation/peft_train_sample.jsonl), то заметите, что это, по сути, таб
личный датасет с двумя столбцами: один содержит медицинские транскрипты,
а другой — их категории, например cardiovascular / pulmonary (кардиология /
пульмонология), chiropractic (мануальная терапия) или pain management (нев
рология). Мы рассмотрели применение генеративного ИИ в рабочих процессах
для табличных данных. На примере из упражнения по интерпретации логов мы
видим и обратную связь, когда табличные данные сами становятся частью рабочего процесса генеративного ИИ. Хотя глубокий анализ этой темы не входит
в задачи нашей книги, мы убеждены, что роль табличных данных в рабочих процессах генеративного ИИ недооценена и открывает значительные возможности
для повышения его эффективности.
В этом разделе мы рассмотрели применение Gemini для Google Cloud для решения задач, связанных с ML-пайплайнами: получения ответов на вопросы,
генерации и объяснения фрагментов кода, а также для анализа логов.
Итоги
Перед настройкой ML-пайплайна в Vertex AI следует выполнить несколько
подготовительных шагов.
Необходимо создать сервисный аккаунт и загрузить его в каталог, в котором
вы будете запускать скрипт пайплайна.
Датасет, который будет применяться для обучения модели в пайплайне, надо
загрузить в бакет Google Cloud Storage. Затем этот путь используется для
создания набора данных Vertex AI, который будет передаваться в качестве
аргумента в Vertex AI SDK через скрипт пайплайна.
Итоги 523
Скрипт обучения, запущенный в предустановленном контейнере Vertex AI,
не имеет доступа к внешней файловой системе, поэтому для реализации MLпайплайна обучающий датасет и файл конфигурации обучения размещаются
в Cloud Storage, а затем передаются скрипту обучения в виде URI.
Код из блокнота обучения модели, который мы запускали в Colab в главе 9,
необходимо адаптировать для работы в контейнере. Например, нужно изменить скрипт обучения для использования путей в Cloud Storage, указывающих на файл конфигурации, обучающие данные и директорию для
сохранения обученной модели.
Скрипт пайплайна вызывает ряд функций из Vertex AI SDK: для создания
контейнера, в котором выполняется скрипт обучения, для запуска скрипта
обучения и для развертывания обученной модели на эндпоинте Vertex AI.
Для проверки работы эндпоинта, созданного ML-пайплайном, можно использовать то же веб-приложение, что и для тестирования локального развертывания в главе 10.
Gemini для Google Cloud (инструментарий генеративного ИИ, встроенный
в Google Cloud) можно применять на различных этапах процесса создания
ML-пайплайна: для получения ответов на вопросы, анализа логов, а также
для генерации кода по текстовому описанию и его объяснения.
12
Комбинация градиентного
бустинга и глубокого обучения
В этой главе
3 Обзор примера с градиентным бустингом из главы 7
3 Сравнение решений одной и той же задачи на основе
градиентного бустинга (глава 7) и глубокого обучения
3 Результат ансамблирования моделей градиентного бустинга
и глубокого обучения
В главе 7 мы провели углубленное исследование сквозного примера использования градиентного бустинга. Мы изучили датасет объявлений на Airbnb Токио,
сгенерировали признаки, подходящие для задачи регрессии ценообразования,
а затем создали базовую модель, обученную на этих данных, для прогнозирования
цен. Наконец, применяя рассмотренные к тому моменту методы, мы оптимизировали модель XGBoost, обученную на этом датасете, и разобрали некоторые
подходы для объяснения поведения модели.
В этой главе мы оценим, могло ли применение глубокого обучения привести
к другим результатам и производительности на той же задаче, определим, какой
подход работает лучше, и узнаем, как использовать и интегрировать сильные
и слабые стороны каждого метода. Для этого сначала воспроизведем решение
12.1. Обзор решения с градиентным бустингом из главы 7 525
с градиентным бустингом для задачи Airbnb Токио из главы 7. Затем рассмотрим некоторые подходы, которые можно использовать для применения глубокого обучения к той же задаче, выберем подходящий и подробно поговорим
о нем. В заключение сравним два варианта и определим, в каких случаях для
работы с табличными данными (как для регрессии, так и для классификации)
целесообразнее использовать градиентный бустинг, а в каких — глубокое обу
чение. В дополнение к сравнению по основным показателям производительности (например, насколько точны и быстры прогнозы) мы исследуем, как два
решения соотносятся по более бизнес-ориентированным метрикам, таким как
стоимость сопровождения, понятность для стейкхолдеров и стабильность после
развертывания.
В этой главе объединены темы, которые мы уже обсуждали в книге, включая
сведения о XGBoost из главы 7 и подходы на основе глубокого обучения к таб
личным задачам в главах 1 и 8. В главе 1 мы перечислили несколько статей,
в которых сравниваются применение классического машинного и глубокого
обучения для табличных данных. В этой главе увидим, что результаты нашего
сравнения совпадают с наблюдениями в одной из упоминавшихся статей. В главе 8 мы оценили различные подходы глубокого обучения к работе с табличными
данными. Теперь выберем тот из них, который имеет наибольшие шансы конкурировать с XGBoost.
Объединяя два основных направления, которые мы рассмотрели в этой книге
(классические методы машинного обучения и подходы глубокого обучения
для решения задач с табличными данными), эта глава подводит итог всему,
что мы уже рассмотрели, и дает рекомендации, которые помогут вам добиться успеха в своей работе. Код, представленный в главе, доступен по адресу
https://mng.bz/vKPp.
12.1. Обзор решения с градиентным бустингом
из главы 7
В главе 7 мы собрали датасет из объявлений на Airbnb Токио, проанализировали ключевые характеристики этих данных и создали модель XGBoost для
прогнозирования цены проживания. С веб-сайта Inside Airbnb Network (http://
insideairbnb.com/) мы загрузили следующие файлы, относящиеся к городу Токио:
listings.csv, содержащий краткие описания объектов на Airbnb Токио;
calendar.csv.gz — архивный файл, содержащий calendar.csv — датасет
с информацией о бронировании и ценах за указанный год для каждого объявления.
526 Глава 12. Комбинация градиентного бустинга и глубокого обучения
Напомним, что датасет в listings.csv содержит следующие столбцы:
id — уникальный идентификатор для каждого объявления на Airbnb. Это
тип данных int64, то есть число. В других таблицах он может называться
listing_id;
name — описание объекта на Airbnb. Это тип данных object, который обычно
представляет собой строку или текст;
host_id — уникальный идентификатор для каждого арендодателя на Airbnb.
Тип данных int64;
host_name — имя арендодателя, который дал объявление. Тип данных object;
neighborhood_group — область или регион, к которому относится район. Тип
данных float64, но важно отметить, что использование чисел с плавающей
точкой для представления групп или категорий встречается редко. В этом
случае наличие значений float указывает на то, что они в этом поле полностью пропущены;
neighborhood — особенности района, в котором находится объект. Тип данных object;
latitude — географическая широта местоположения объекта. Тип данных
float64;
longitude — географическая долгота местоположения объекта. Тип данных
float64;
room_type — тип комнаты или помещения, предлагаемого в объявлении, на-
пример entire home / apartment (весь дом / квартира), private room (отдельная
комната), shared room (комната на несколько человек). Тип данных object;
price — цена за ночь аренды объекта. Тип данных int64 (целочисленное
значение);
minimum_nights — минимальное количество ночей для бронирования. Тип
данных int64;
number_of_reviews — общее количество отзывов, полученных объектом недвижимости. Тип данных int64;
last_review — дата последнего отзыва, полученного объектом. Тип данных
object, может представлять информацию о дате и времени, но может потре-
бовать дальнейшего анализа для эффективного использования;
reviews_per_month — среднее количество отзывов в месяц. Тип данных float64;
calculated_host_listings_count — общее количество объявлений конкретного арендодателя на Airbnb. Тип данных int64;
availability_365 — количество дней в году, в течение которых объект доступен для бронирования (из 365 дней). Тип данных int64;
12.1. Обзор решения с градиентным бустингом из главы 7 527
number_of_reviews_ltm — количество отзывов, полученных за последние
12 месяцев. Тип данных int64;
licence — номер лицензии или информация, связанная с объектом. Тип
данных object (обычно строка или текст).
Целью модели, которую мы создали в главе 7, было прогнозирование цены для
нового объекта. Как видно из сравнения в табл. 12.1, эта задача сложнее для
модели глубокого обучения, чем проблема Airbnb NYC, рассмотренная в главе 8.
Таблица 12.1. Сравнение задач Airbnb NYC и Airbnb Токио
Airbnb NYC
Airbnb Токио
Строки в датасете
48 000
10 000
Столбцы в датасете
18
31
Цель
Классификация: предсказать, будет
ли цена выше или ниже медианной
Регрессия: предсказать цену
Действительно, датасет Airbnb Токио содержит менее 25 % строк от набора
данных Airbnb NYC и более чем в два раза больше столбцов. При меньшем
количестве точек данных придется больше полагаться на экспертные знания
в предметной области (отсюда и важность генерации признаков). Наличие
большего количества столбцов подразумевает больший риск переобучения,
и в любом случае придется иметь дело с более сложными связями между признаками и целевой переменной.
В целом классические подходы машинного обучения превосходят нейронные
сети на небольших датасетах. Такие техники, как аугментация данных, могут
частично компенсировать это отставание, однако модели на основе глубокого
обучения по-прежнему будут сталкиваться со сложностями на датасетах объемом
менее десятков тысяч строк. Исследования причин, по которым нейронные сети
требуют больше данных, все еще продолжаются. Однако большое количество
параметров в архитектурах глубокого обучения в сочетании с необходимостью
достаточного объема данных для обобщения рассматриваются как ключевой
фактор, объясняющий их неэффективность на задачах с небольшими наборами
данных (см., например, «The Computational Limits of Deep Learning» Томпсона
(Thompson) и др. (https://arxiv.org/pdf/2007.05558.pdf), где также утверждается, что
вычислительная эффективность необходима для прогресса данного подхода).
Меньшее количество строк в датасете Airbnb Токио, следовательно, представляет сложность для успешной реализации решения на основе глубокого обу
чения. Кроме того, большее количество столбцов требует более тщательной
528 Глава 12. Комбинация градиентного бустинга и глубокого обучения
предварительной обработки и генерации признаков. Поскольку мы повторили
процедуру подготовки данных из главы 7, в рамках данной задачи мы избежали
сложности обработки большого числа столбцов. Однако при работе с другими
датасетами важно помнить, что рост количества признаков требует разработки
эффективных стратегий для обработки пропусков, устранения мультиколлинеарности (как обсуждалось в главе 2) и отбора наиболее информативных признаков. Помимо усложнения подготовки данных, увеличение количества столбцов
повышает риск появления избыточных и зашумленных признаков, что может
снизить эффективность моделей глубокого обучения. Впрочем, стоит отметить,
что эти модели, в силу своей способности выявлять сложные нелинейные зависимости, обычно демонстрируют относительно высокую устойчивость к шуму.
Кроме того, задача, которую мы решаем с датасетом Airbnb Токио, — это регрессия (прогнозирование цены объекта), в отличие от бинарной классификации
(прогнозирование того, превышает ли цена медианную), которую мы решали
для датасета Airbnb NYC. Это важно для бизнеса. Если цель состоит в том, чтобы
предоставить решение, которое дает бизнес-преимущества, наша модель должна
предсказывать правильно как можно чаще. В случае бинарной классификации
решение является дискретным (один класс или другой), и вероятность того, что
прогноз будет правильным, выше, чем в задаче регрессии. В задаче регрессии
бизнес-ожидание заключается в том, что модель предскажет цену, максимально
близкую к реальной, но выходные данные представляют собой непрерывные
значения, которые могут значительно отличаться от ожидаемых. Короче говоря,
задача бинарной классификации проще (с точки зрения удовлетворения бизнеспотребностей), чем регрессия, поскольку имеет четкий порог правильности.
В главе 7, после завершения ряда преобразований в датасете Airbnb Токио, мы
получаем следующий набор признаков:
array(['onehot_encoding__room_type_Entire home/apt',
'onehot_encoding__room_type_Hotel room',
'onehot_encoding__room_type_Private room',
'onehot_encoding__room_type_Shared room',
'ordinal_encoding__neighbourhood_more_than_30',
'ordinal_encoding__type_of_accommodation',
'target_encoding__coordinates', 'numeric__minimum_nights',
'numeric__number_of_reviews', 'numeric__days_since_last_review',
'numeric__reviews_per_month',
'numeric__calculated_host_listings_count',
'numeric__availability_365', 'numeric__score',
'numeric__number_of_reviews_ltm',
'numeric__number_of_reviews_ltm_ratio',
'numeric__number_of_bedrooms', 'numeric__number_of_beds',
'numeric__number_of_baths', 'numeric__imperial_palace_distance',
'numeric__nearest_convenience_store',
'numeric__nearest_train_station', 'numeric__nearest_airport',
'numeric__nearest_bus_station', 'numeric__nearest_subway',
12.1. Обзор решения с градиентным бустингом из главы 7 529
'binary__is_new', 'binary__is_studio', 'binary__has_shared_bath',
'binary__has_half_bath'], dtype=object)
Все эти признаки являются числовыми, и мы также должным образом обработали все пропущенные значения, чтобы иметь возможность работать с линейной
моделью в качестве базовой. Фактически мы начали с создания модели линейной
регрессии в качестве отправной точки, чтобы иметь ориентир для сравнения
дальнейших улучшений модели XGBoost.
Подготовка данных для линейной или логистической регрессии (в зависимости
от того, является задача регрессией или классификацией) автоматически делает
их пригодными и для обработки нейросетью. Однако, хотя это и удобно, такой
подход может упустить некоторые специфические методы подготовки, подходящие исключительно для нейросетей. Например, категориальные признаки
в линейных моделях обычно обрабатываются с помощью one-hot-кодирования,
тогда как в нейронной сети можно использовать слой кодирования для преобразования категориальных значений в числовые непосредственно во время
обучения.
После получения базовых результатов для линейной регрессии мы провели ряд
оптимизаций модели XGBoost. Мы будем использовать этот оптимизированный код XGBoost в качестве основы для решения задачи с данными для Токио
с помощью градиентного бустинга, которое мы будем сравнивать с глубоким
обучением (см. листинг 12.1). Эта версия кода для XGBoost очень близка к окончательной из главы 7, а гиперпараметры соответствуют лучшим из блокнота,
представленного в главе 7 (https://mng.bz/4a6R). В версии кода, показанной в этой
главе, прогнозы сохраняются в массиве xgb_oof_preds, чтобы их можно было
дополнительно обработать или использовать вместе с прогнозами, которые мы
собираемся получить от модели глубокого обучения.
Листинг 12.1. Код для обучения финальной модели XGBoost
xgb_params = {'booster': 'gbtree',
'objective': 'reg:tweedie',
Устанавливает
'n_estimators': 932,
гиперпараметры
'learning_rate': 0.08588055025922144,
'subsample': 0.9566295202123205,
'colsample_bytree': 0.6730567082779646,
'max_depth': 7,
'min_child_weight': 6,
'reg_lambda': 6.643211493348415e-06,
'reg_alpha': 7.024597970671363e-05,
'tweedie_variance_power': 1.6727891016980427}
from sklearn.metrics import r2_score
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error
from XGBoost import XGBRegressor
import numpy as np
xgb = XGBRegressor(**xgb_params)
Импортирует необходимые
библиотеки
Настраивает регрессор XGBoost
с указанными гиперпараметрами
cv_splits = cv.split(X, y=neighbourhood_more_than_30)
r2_scores = []
Определяет разделение для кросс-валидации на
'max_depth': 7,
'min_child_weight': 6,
'reg_lambda': 6.643211493348415e-06,
'reg_alpha': 7.024597970671363e-05,
'tweedie_variance_power': 1.6727891016980427}
530
Глава 12. Комбинация
градиентного бустинга и глубокого обучения
from
sklearn.metrics
import r2_score
Импортирует необходимые
from sklearn.metrics import mean_squared_error
библиотеки
from sklearn.metrics import mean_absolute_error
from XGBoost import XGBRegressor
import numpy as np
Настраивает регрессор XGBoost
с указанными гиперпараметрами
xgb = XGBRegressor(**xgb_params)
cv_splits = cv.split(X, y=neighbourhood_more_than_30)
Определяет разделение для кросс-валидации на
r2_scores = []
основе признака neighborhood_more_than_30
rmse_scores = []
mae_scores = []
Генерирует разделение для кроссxgb_oof_preds = np.zeros(len(X))
валидации на основе признака
neighborhood_more_than_30
for train_index, test_index in cv_splits:
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
xgb.fit(X_train, y_train)
Выполняет прогнозы
y_pred = xgb.predict(X_test)
на кросс-валидации
xgb_oof_preds[test_index] = y_pred
r2_scores.append(r2_score(y_test, y_pred))
rmse_scores.append(np.sqrt(mean_squared_error(y_test, y_pred)))
mae_scores.append(mean_absolute_error(y_test, y_pred))
print(f"Средний R-квадрат на кросс-валидации: {np.mean(r2_scores):.3f}")
print(f"Средняя RMSE на кросс-валидации: {np.mean(rmse_scores):.3f}")
print(f"Средняя MAE на кросс-валидации: {np.mean(mae_scores):.3f}")
Выводит R-квадрат, среднеквадратичную
ошибку и среднюю абсолютную ошибку
Рассчитывает R-квадрат, среднеквадратичную
ошибку и среднюю абсолютную ошибку для оценки
эффективности модели
Оптимизации решения XGBoost дают результаты, которые значительно лучше,
чем у базовой линейной регрессии, как показано в табл. 12.2.
Таблица 12.2. Сводка результатов главы 7
Метрики
Базовая модель линейной
регрессии
Оптимизированный
XGBoost
R-квадрат
0.320
0.729
Среднеквадратичная ошибка (RMSE)
17197.323
10853.661
Средняя абсолютная ошибка (MAE)
12568.371
6611.609
В качестве дальнейшей проверки вы можете убедиться, что для всех трех метрик, которые мы отслеживали в главе 7, то есть R-квадрат, среднеквадратичная
12.2. Выбор решения на основе глубокого обучения 531
(RMSE) и средняя абсолютная ошибка (MAE), оптимизированная модель
XGBoost практически всегда показывает улучшение по сравнению с базовой
линейной регрессией. Теперь, используя модель XGBoost в качестве ориентира,
мы рассмотрим, какие результаты можно получить с тем же датасетом, используя
модель глубокого обучения, в оставшейся части этой главы.
12.2. Выбор решения на основе глубокого обучения
В главе 8 мы рассмотрели набор различных стеков глубокого обучения для
работы с табличными данными, включая Keras, fastai и библиотеки, специально разработанные для таблиц, такие как TabNet. Какой же из них выбрать для
сравнения с решением на XGBoost для задачи с Airbnb Токио из главы 7?
Напомним, что мы провели сопоставление различных подходов глубокого обу
чения на датасете Airbnb NYC, которое снова показано в табл. 12.3.
Таблица 12.3. Сравнение вариантов глубокого обучения
Плюсы
Keras
fastai
Библиотека табличных
данных (например,
TabNet)
Подробности модели
прозрачны.
Фреймворк включает явную поддержку моделей
для табличных данных,
что означает более
компактный код, а также
устанавливает хорошо
подобранные значения
по умолчанию, что позволяет быстро достигать
приемлемых результатов
Специальная библиотека,
созданная для работы
с табличными датасетами
Если мы столкнемся
с проблемой, нам придется искать ее решение
самостоятельно, поскольку сообщество меньше,
чем у Keras
До сих пор ни одна
библиотека не получила
статуса общепризнанного
стандарта; раздробленность сообществ и непоследовательная поддержка некоторых библиотек
затрудняют обеспечение
надежной работы базового кода
Большое сообщество
пользователей фреймворка позволяет легко
находить решения распространенных проблем
Минусы
Нет встроенной поддержки табличных
данных
Различия между датасетом из главы 8 и тем, который мы будем использовать
сейчас, — а именно то, что первый намного больше (в четыре раза больше строк)
532 Глава 12. Комбинация градиентного бустинга и глубокого обучения
и значительно проще (в два раза меньше столбцов), — не оказывают существенного влияния на преимущество какого-либо из предложенных решений.
Фактически Keras и fastai являются универсальными фреймворками глубокого обучения и не предназначены специально для небольших или сложных
датасетов. Дизайн TabNet дает ему преимущество в работе с данными высокой
размерности, но при применении к меньшим таблицам оно оказывается менее
существенным.
На самом деле, большее значение в нашем выборе имеет тот факт, что мы хотим
объективно сравнить XGBoost и подход глубокого обучения. Как вы видели в главе 7, XGBoost отличается простотой использования и дает хорошие результаты
без дополнительной доработки. Если мы выберем сложную модель глубокого
обучения, которая требует много времени на настройку и оптимизацию, то это
будет несправедливо.
Учитывая это, какой фреймворк глубокого обучения следует выбрать? Мы
пропустим TabNet из-за сложностей с обеспечением его надежной работы.
Остаются Keras и fastai. Как упоминалось в табл. 12.3, Keras действительно
популярен в продакшене и у него большое сообщество профессиональных
пользователей. Однако fastai лучше соответствует нашим целям. Вспомните:
в главе 8 мы отметили, что fastai создан для табличных данных, таких как наши,
и он предоставляет хорошо работающие значения параметров по умолчанию.
Это означает, что вы можете быстро получить достойные результаты, не тратя
кучу времени на оптимизацию. Фреймворк fastai обрабатывает множество
рутинных задач и деталей за кулисами. Как вы увидите позже, выбор fastai
для этой задачи вполне себя оправдал. На текущий момент мы считаем, что
это отличный вариант для глубокого обучения на данных Airbnb Токио, который не доставит лишних хлопот и за несколько шагов предоставит модель для
сравнения с XGBoost из главы 7.
12.3. Решение задачи Airbnb Токио с помощью
глубокого обучения
Ранее в этой главе мы рассмотрели решение на XGBoost для Airbnb Токио, изу
чили варианты фреймворков на основе глубокого обучения и выбрали fastai для
сравнения с XGBoost. В этом разделе разберем решение на fastai для выбранного
датасета.
В листинге 12.2 показана основа модели на fastai. Этот код обучает модель регрессии на датасете Airbnb Токио с помощью функции TabularPandas (https://
mng.bz/QDR6) — обертки, выполняющей под капотом все необходимые преобразования.
12.3. Решение задачи Airbnb Токио с помощью глубокого обучения 533
Листинг 12.2. Модель на fastai для Airbnb Токио
from fastai.tabular.all import *
procs = [FillMissing, Normalize, Categorify]
cat_vars = [
col for col in airbnb_tokyo.columns
if "onehot_encoding__" in col
or ordinal_encoding__" in col
or "binary__" in col
]
cont_vars = [
col for col in airbnb_tokyo.columns
if "numeric__" in col
or "target_encoding__" in col
]
dep_var = 'target'
cv_splits = cv.split(X, y=neighbourhood_more_than_30)
r2_scores = []
rmse_scores = []
mae_scores = []
dnn_oof_preds = np.zeros(len(X))
for k, (train_index, test_index) in enumerate(cv_splits):
X_train = airbnb_tokyo.set_index("listing_id").iloc[train_index].copy()
X_test = airbnb_tokyo.set_index("listing_id").iloc[test_index].copy()
y_test = airbnb_tokyo["target"].iloc[test_index].copy()
tab = TabularPandas(
Определяет объект fastai
X_train, procs, cat_vars, cont_vars,
TabularPandas
dep_var, y_block=RegressionBlock(),
splits=RandomSplitter(
valid_pct=0.2, seed=0)(range_of(X_train)),
inplace=True,
reduce_memory=True
Определяет объект
)
dataloaders на основе
TabularPandas
dls = tab.dataloaders(bs=128)
y_range = torch.tensor([0, X_train['target'].max() * 1.2])
tc = tabular_config(ps=[0.001, 0.01], embed_p=0.04, y_range=y_range)
learn = tabular_learner(dls, layers=[1000,500],
Определяет объект tabular_
metrics=mae,
learner на основе dataloaders
config=tc,
loss_func=L1LossFlat())
with learn.no_bar(), learn.no_logging():
lr = learn.lr_find(show_plot=False)
Обучает модель
learn.fit_one_cycle(80, lr.valley)
dl = learn.dls.test_dl(X_test)
y_pred = (
learn.get_preds(dl=dl)[0]
.numpy()
.ravel()
)
dnn_oof_preds[test_index] = y_pred
Получает прогнозы модели
на тестовом наборе
Сохраняет метрики
r2_scores.append(r2_score(y_test, y_pred))
rmse_scores.append(np.sqrt(mean_squared_error(y_test, y_pred)))
metrics=mae,
learner на основе dataloaders
config=tc,
loss_func=L1LossFlat())
with learn.no_bar(), learn.no_logging():
lr = learn.lr_find(show_plot=False)
Обучает модель
learn.fit_one_cycle(80, lr.valley)
534 Глава 12. Комбинация градиентного бустинга и глубокого обучения
dl = learn.dls.test_dl(X_test)
y_pred = (
learn.get_preds(dl=dl)[0]
.numpy()
.ravel()
)
dnn_oof_preds[test_index] = y_pred
Получает прогнозы модели
на тестовом наборе
Сохраняет метрики
r2_scores.append(r2_score(y_test, y_pred))
rmse_scores.append(np.sqrt(mean_squared_error(y_test, y_pred)))
mae_scores.append(mean_absolute_error(y_test, y_pred))
print(f"CV Fold {k} MAE: {mae_scores[-1]:.3f}")
print(f"\nСредний R-квадрат на кросс-валидации: {np.mean(r2_scores):.3f}")
print(f"Средняя RMSE на кросс-валидации: {np.mean(rmse_scores):.3f}")
print(f"Средняя MAE на кросс-валидации: {np.mean(mae_scores):.3f}
Как видно из кода листинга 12.2, решение с использованием fastai довольно простое. Сначала мы определяем шаги предварительной обработки (procs), такие
как заполнение пропущенных значений, нормализация и категоризация. Затем
разделяем категориальные и непрерывные переменные в датасете и выбираем
зависимую переменную (dep_var). После этого выполняем стратифицированную k-фолдовую кросс-валидацию таким же образом, как мы делали это для
решения XGBoost.
На каждой итерации обучающие данные предварительно обрабатываются
с помощью TabularPandas, с указанием переменных (категориальных и непрерывных, целевой) и разделения данных. Объекты DataLoader (dls) создаются
для батчей обучения и валидации. После определения модели нейронной сети
(tabular_learner), состоящей из двух слоев (первый с 1000 нейронов и следующий с 500) и их параметров дропаута (с использованием tabular_config
и установкой более высокого параметра для последнего слоя), мы обучаем модель
с указанием скорости обучения (learning rate), заданной с помощью lr_find,
и с использованием процедуры одного цикла (fit_one_cycle).
Сочетая процедуры lr_find и fit_one_cycle, мы автоматически настраиваем
параметр скорости обучения для достижения наилучших результатов на обрабатываемых данных, что позволяет получить эффективное решение без длительных
экспериментов и ручной корректировки. Процедура lr_find (https://mng.bz/
Xxq9) исследует диапазон скоростей обучения на выборке данных, останавливаясь, когда скорость становится слишком высокой и обучение расходится. Эта
процедура выполняется относительно быстро и возвращает значение скорости,
соответствующее точке на две трети длины участка кривой потерь, где потери
уменьшаются. Это значение используется в качестве верхней границы для другой
процедуры — fit_one_cycle (https://mng.bz/yWZp), метода обучения, при котором
скорость обучения не фиксирована или постоянно уменьшается, а колеблется
между минимальным и максимальным значениями. Колебания позволяют сети
не застревать в локальных минимумах, и в целом такая сеть показывает лучшие
12.4. Сравнение решений на XGBoost и fastai для задачи Airbnb Токио 535
результаты, чем при использовании других подходов, особенно при работе
с табличными данными. Оба метода были разработаны Лесли Смитом (Leslie
Smith) в серии статей:
«Cyclical Learning Rates for Training Neural Networks» (https://arxiv.org/
abs/1506.01186);
«Super-Convergence: Very Fast Training of Neural Networks Using Large Learning Rates» (https://arxiv.org/abs/1708.07120);
«A Disciplined Approach to Neural Network Hyper-Parameters: Part 1 —
Learning Rate, Batch Size, Momentum, and Weight Decay» (https://arxiv.org/
abs/1803.09820).
Насколько нам известно, реализация этих методов с помощью fastai является
наиболее эффективной и высокопроизводительной из доступных в сообществе
разработчиков ПО с открытым исходным кодом.
Вернемся к коду. На каждом проходе цикла for сохраняются прогнозы для
текущего фолда, а также оценки R-квадрат, RMSE и MAE. Средние значения
для всех метрик выводятся на экран после завершения цикла, что позволяет
оценить общую эффективность решения на fastai. Обратите внимание, что мы
пересчитаем эти значения в процессе ансамблирования, когда будем сравнивать
прогнозы и фактические значения y для ансамбля, полностью состоящего из
результатов fastai.
12.4. Сравнение решений на XGBoost и fastai
для задачи Airbnb Токио
Теперь, когда у нас есть решение на основе глубокого обучения для датасета
Airbnb Токио, мы можем сравнить его результаты с решением на XGBoost. Сопоставляя метрики, которые мы собрали для обеих моделей (R-квадрат, RMSE
и MAE), мы можем получить представление о том, насколько эффективно
каждое из них для нашей задачи. В табл. 12.4 представлено краткое сравнение
результатов обоих подходов.
Таблица 12.4. Сравнение результатов моделей XGBoost и fastai
Метрики
XGBoost
fastai
R-квадрат
0.599
0.572
RMSE
10783.027
11719.387
MAE
6531.102
7152.143
536 Глава 12. Комбинация градиентного бустинга и глубокого обучения
Таблица 12.4 демонстрирует, что XGBoost обеспечивает лучшие результаты,
чем fastai, по всем трем метрикам ошибок. В частности, значение R-квадрат для
XGBoost значительно выше, а его значения RMSE и MAE примерно на 8–9 %
ниже.
В дополнение к базовому сравнению метрик ошибок мы можем визуализировать
некоторые различия в том, как два подхода решают задачу. Так, мы можем напрямую сравнить прогнозы XGBoost и fastai, исследуя каждую предсказанную
точку данных в тестовом наборе. На рис. 12.1 ось x представляет прогнозы от
XGBoost, а ось y — от fastai для каждой соответствующей точки данных. Диаграмма демонстрирует связь между прогнозами двух моделей.
Прогнозы fastai
Диаграмма рассеяния прогнозов со сглаженной линией тренда
Прогнозы XGB
Рис. 12.1. Диаграмма рассеяния прогнозов для XGBoost и fastai
На диаграмме сплошная линия тренда довольно точно совпадает с пунктирной
диагональной линией, показывая, что в целом нет значительного расхождения
между прогнозами XGBoost и fastai. Сплошная линия тренда (LOWESS — LOcally
WEighted Scatterplot Smoothing, диаграмма рассеяния локально взвешенного
сглаживания), отражающая взаимосвязь между прогнозами XGBoost и fastai,
незначительно отклоняется от пунктирной. Это подтверждает, что, несмотря
12.4. Сравнение решений на XGBoost и fastai для задачи Airbnb Токио 537
на расхождения в прогнозах, систематического завышения или занижения результатов одним из них относительно другого не наблюдается.
Кроме того, мы также можем изучить взаимосвязь между прогнозами, используя
значение 70 000 на оси x в качестве опорной точки, поскольку можно выделить
два отдельных кластера прогнозов. Обратите внимание, что среднее значение
прогноза fastai составляет около 81 300 против 81 550 для XGBoost при прогнозах XGBoost выше 70 000, и около 23 050 против 22 500 для прогнозов XGBoost
ниже 70 000 (см. листинг 12.3).
Листинг 12.3. Средние прогнозы fastai для прогнозов XGBoost
predictions = pd.DataFrame(
{'xgb': xgb_oof_preds, 'fastai': dnn_oof_preds}
)
Получает датафрейм pandas со столбцом
для прогнозов XGBoost и fastai
avg_fastai_over_70000 = predictions.loc[
predictions['xgb'] > 70000, 'fastai'
].mean()
avg_xgb_over_70000 = predictions.loc[
predictions['xgb'] > 70000, 'xgb'
].mean()
print(f"Средние значения прогнозов при xgb > 70000:",
f"fastai:{avg_fastai_over_70000:0.2f}",
f"xgb:{avg_xgb_over_70000:0.2f}")
Вычисляет среднюю статистику
для прогнозов XGBoost и fastai
в зависимости от прогнозируемого
значения XGBoost
avg_fastai_under_70000 = predictions.loc[predictions['xgb'] <= 70000,
'fastai'].mean()
avg_xgb_under_70000 = predictions.loc[predictions['xgb'] <= 70000, 'xgb'].
mean()
print(f"Средние значения прогнозов при xgb <= 70000: fastai:{avg_fastai_
under_70000:0.2f}
xgb:{avg_xgb_under_70000:0.2f}")
Различия между двумя моделями минимальны; в среднем предсказания fastai
и XGBoost, как правило, совпадают. Однако fastai склонна завышать предсказания для более низких ценовых уровней и слегка занижать для более высоких.
Возвращаясь к рис. 12.1, после анализа диаграммы сравнения прогнозов давайте
посмотрим на диаграмму сравнения ошибок XGBoost и fastai. Для каждой точки данных в тестовом наборе Airbnb Токио на рис. 12.2 отображается ошибка
(абсолютное значение разницы между прогнозом и фактическим значением),
где значение по оси x является ошибкой XGBoost, а по оси y — ошибкой fastai.
На рис. 12.2 видно, что существует большой кластер точек данных, где ошибка
как для XGBoost, так и для fastai ниже 20 000. Общая линия LOWESS показывает, что ошибка XGBoost в целом ниже, чем у fastai.
538 Глава 12. Комбинация градиентного бустинга и глубокого обучения
Ошибки fastai
Диаграмма рассеяния ошибок со сглаженной линией тренда
Ошибки XGB
Рис. 12.2. Диаграмма рассеяния ошибок для XGBoost и fastai
Как демонстрируют агрегированные метрики ошибок и диаграммы прогнозов
и ошибок, модель XGBoost превосходит по производительности fastai. Однако,
учитывая разброс прогнозов между двумя моделями, мы считаем, что есть возможность улучшить результаты путем ансамблирования двух моделей. Есть
серьезные основания полагать, что, помимо различий в производительности,
модели работают по-разному, захватывая различные закономерности и характеристики данных. В следующем разделе мы выясним, улучшает ли ансамблирование результаты.
12.5. Ансамблирование двух решений задачи
Airbnb Токио
Теперь, когда мы оценили производительность решений на XGBoost и fastai
для задачи Airbnb Токио по отдельности, мы рассмотрим их ансамблирование,
чтобы проверить, обеспечивает ли это какие-то улучшения.
В листинге 12.4 показан цикл, в котором мы объединяем результаты обеих
моделей.
12.5. Ансамблирование двух решений задачи Airbnb Токио 539
Листинг 12.4. Код для ансамблирования двух моделей
blend_list = [
[1., 0.], [0., 1.], [0.25,0.75],
Соотношение моделей в ансамбле
[0.75,0.25],[.5, .5]
]
для проверки в цикле
for a, b in blend_list:
print(f"Вес_XGBoost={a}, вес_DNN={b}")
Генерирует прогнозы, смешанные
blended_oof_preds = (
в соответствии с соотношениями
xgb_oof_preds * a + dnn_oof_preds * b
моделей в ансамбле
)
r2 = r2_score(blended_oof_preds, y)
Получает R-квадрат,
rmse = np.sqrt(mean_squared_error(blended_oof_preds, y))
RMSE и MAE для
mae = mean_absolute_error(blended_oof_preds, y)
смешанных прогнозов
print(f"R-квадрат после ансамблирования: {r2:.3f}")
print(f"RMSE после ансамблирования: {rmse:.3f}")
print(f"MAE после ансамблирования: {mae:.3f}\n")
Код в листинге 12.4 объединяет результаты XGBoost и fastai в соответствии
с весовыми коэффициентами в blend_list. Обратите внимание, что эти доли
смешивания не оптимизированы для поиска абсолютного оптимума — мы просто используем набор фиксированных значений, чтобы получить общее представление о влиянии ансамблирования на итоговый результат. Также учтите,
что мы оцениваем результаты с использованием прогнозов на валидационных
фолдах кросс-валидации (out-of-fold prediction). Тем не менее, объединяя
предсказания XGBoost и fastai в соответствии с пропорциями, указанными
в blend_list, мы можем наблюдать эффект ансамблирования двух подходов
в диапазоне значений.
Результат выполнения кода ансамблирования для стандартного запуска:
Вес_XGBoost=1.0, вес_DNN=0.0
R-квадрат после ансамблирования: 0.599
RMSE после ансамблирования: 10783.027
MAE после ансамблирования: 6531.102
Вес_XGBoost=0.75, вес_DNN=0.25
R-квадрат после ансамблирования: 0.619
RMSE после ансамблирования: 10507.904
MAE после ансамблирования: 6366.257
Вес_XGBoost=0.5, вес_DNN=0.5
R-квадрат после ансамблирования: 0.625
RMSE после ансамблирования: 10527.024
MAE после ансамблирования: 6384.576
Вес_XGBoost=0.25, вес_DNN=0.75
R-квадрат после ансамблирования: 0.618
RMSE после ансамблирования: 10838.831
MAE после ансамблирования: 6566.663
540 Глава 12. Комбинация градиентного бустинга и глубокого обучения
Вес_XGBoost=0.0, вес_DNN=1.0
R-квадрат после ансамблирования: 0.599
RMSE после ансамблирования: 11419.374
MAE после ансамблирования: 6959.540
Эти цифры могут оказаться немного сложными для интерпретации, поэтому
давайте посмотрим, как они выглядят на графике. На рис. 12.3 показаны результаты для R-квадрата, RMSE и MAE при различных пропорциях смешивания
моделей XGBoost и fastai.
Как показано на рис. 12.3, мы получаем оптимальные результаты для оценки
R-квадрат, когда используем соотношение прогнозов 50/50. Для метрик ошибок
(RMSE и MAE) мы получаем лучший результат при большем весе XGBoost, чем
fastai. Однако если использовать пропорцию 50/50, то показатели ухудшаются
незначительно. Худшие результаты получаются при использовании 100 % прогноза от fastai, что ожидаемо, исходя из предыдущего анализа каждой модели
в отдельности, но интересно отметить, что использование только XGBoost всегда
хуже, чем ее ансамблирование с другим решением в пропорциях 75/25 или 50/50.
Среднеквадратичная ошибка
(RMSE) для весовых
коэффициентов ансамблирования
R-квадрат для весовых
коэффициентов ансамблирования
0.625
Средняя абсолютная ошибка
(MAE) для весовых
коэффициентов ансамблирования
11400
6900
11200
0.615
6800
RMSE
RMSE
11000
0.610
MAE
MAE
10800
0.605
0%
Fa
st
ai
10
5%
0%
/7
%
25
5%
/5
%
50
00
%
75
XG
Bo
/2
0%
10
5%
Fa
st
ai
/7
0%
%
25
50
%
/5
5%
/2
st
1
75
Bo
o
XG
%
00
%
%
00
i1
5%
/7
Fa
st
a
0%
%
/5
%
50
25
%
5%
00
/2
%
st
1
75
%
6400
0.600
Bo
o
6600
6500
10600
XG
6700
os
t1
R-squared
R-квадрат
0.620
Весовые коэффициенты ансамблирования
Рис. 12.3. Результаты ансамблирования моделей XGBoost и fastai
Ансамблирование моделей XGBoost и fastai дает лучшие результаты, чем использование каждой из них по отдельности. Как мы увидим в следующем разделе, наши наблюдения согласуются с результатами, представленными в важном
исследовании, в котором сравнивались подходы классического машинного
и глубокого обучения на задачах с табличными данными.
12.6. Общее сравнение градиентного бустинга и глубокого обучения 541
12.6. Общее сравнение градиентного бустинга
и глубокого обучения
В главе 1 мы затронули дискуссионный вопрос о целесообразности использования глубокого обучения для решения проблем, связанных с табличными данными. Мы ссылались на научные статьи, поддерживающие обе стороны — тех, кто
отстаивает подходы глубокого обучения, и тех, кто утверждает, что классические
методы, в частности градиентный бустинг, постоянно показывают более высокие результаты. Одна из статей, упомянутых нами в главе 1, стˆоит того, чтобы
вернуться к ней сейчас: «Tabular Data: Deep Learning Is Not All You Need» Равида
Шварца-Зива (Ravid Shwartz-Ziv) и Амитая Армона (Amitai Armon) (https://arxiv.
org/abs/2106.03253). В разделе «Discussion and Conclusions» этой статьи авторы
делают следующее утверждение:
В нашем анализе модели глубокого обучения показали более слабые результаты
на датасетах, которые не фигурировали в их исходных статьях, и уступали
XGBoost, который был базовой моделью. Поэтому мы предложили использовать ансамбль моделей глубокого обучения с XGBoost. Этот ансамбль показал
лучшие результаты на этих датасетах, чем любая модель по отдельности
и классический «неглубокий» ансамбль.
Их наблюдение не является строгим правилом, потому что, по нашему опыту,
бывают ситуации, когда использование каждого из двух подходов по отдельности давало наилучшие результаты. Однако чаще всего простое усреднение
решений приводило к более точным предсказаниям. Мы твердо убеждены, что
это происходило потому, что два алгоритма имеют два разных способа оптимизации прогнозов. Градиентный бустинг основан на деревьях решений, которые
являются формой поиска аналогий: алгоритм разбивает набор данных на части,
где признаки, как правило, схожи по значениям между собой и сопоставляются
с похожими целевыми выходами. Градиентная часть алгоритма разумно объединяет несколько деревьев для улучшения прогноза, хотя и не меняет базовый
принцип работы деревьев. Глубокое обучение, с другой стороны, основано исключительно на принципе дифференцирования и нелинейных преобразований,
где алгоритм ищет оптимальные веса для комбинирования преобразованных
входных данных.
Эти два различных подхода дают разные результаты и их ошибки частично
компенсируют друг друга, поскольку между ними малая корреляция. Похожий
принцип применяется в алгоритме случайного леса при усреднении результатов
множества независимых деревьев. Этот вывод соответствует нашему опыту
сравнения результатов градиентного бустинга и глубокого обучения в задаче
Airbnb Токио. Ансамбль моделей на XGBoost и на fastai показал наилучшие
результаты, как видно на рис. 12.3.
542 Глава 12. Комбинация градиентного бустинга и глубокого обучения
Итоги
Решение на XGBoost для задачи Airbnb Токио, представленное в главе 7, задает точку отсчета, которую можно использовать для оценки эффективности
глубокого обучения для решения той же задачи.
Используя реализацию на XGBoost из главы 7 в качестве отправной точки,
можно создать решение на основе глубокого обучения для датасета Airbnb
Токио. Библиотека fastai предоставляет компактное и относительно эффективное решение.
Мы можем наблюдать эффект ансамблирования, смешивая предсказания
обеих моделей в диапазоне от 100 % XGBoost до 100 % fastai. Наилучшие
результаты достигаются ансамблем с долями 50/50.
Этот результат соответствует рекомендациям исследований, посвященных
анализу эффективности глубокого обучения для табличных данных.
А
Гиперпараметры
для классических моделей
машинного обучения
Таблица A.1. Гиперпараметры для линейных моделей
Гиперпараметр
Описание
С
Обратно пропорционален регуляризации: меньшие значения соответствуют более сильной регуляризации. Диапазон поиска
np.logspace(-4, 4, 10)
alpha
Константа, умножающая член регуляризации: большие значения
соответствуют более сильной регуляризации. Диапазон поиска
np.logspace(-2, 2, 10)
l1_ratio
Смешивание L1 и L2 регуляризации в Elasticnet: выбор из значений [0.1,
0.5, 0.7, 0.9, 0.95, 0.99]
Таблица A.2. Гиперпараметры для случайных лесов и особо случайных деревьев (ERT)
Гиперпараметр
Описание
max_features
Уменьшите этот параметр, чтобы увеличить смещение и уменьшить
дисперсию. Попробуйте значения, такие как sqrt, log2 и целые числа,
представляющие 1/10 и 1/20 от количества признаков
min_samples_
leaf
Способ регуляризации деревьев, обычно устанавливается в 1; попробуйте увеличить его значение до 30
bootstrap
Логический параметр, указывающий, использовать ли бутстрэп для
повторного семплирования. Иногда сабсемплинг может быть эффективнее бутстрэппинга при наличии шума или выбросов
n_estimators
Чем больше деревьев, тем лучше, но после определенного предела вы
тратите вычислительные ресурсы впустую. Начните со 100 и доведите
до 1000. Работает для большинства задач
544 Глава А. Гиперпараметры для классических моделей машинного обучения
Таблица A.3. Гиперпараметры для HistGradientBoosting из библиотеки scikit-learn
Гиперпараметр
Описание
learning_rate
Мультипликативное значение для результатов деревьев решений. Действительное число в диапазоне от 0.001 до 0.1
max_iter
Максимальное количество деревьев, построенных в процессе бустинга.
Целое число от 100 до 1000
max_depth
Максимальная глубина каждого дерева, выступает как регуляризация
роста дерева. Целое число от 1 до 12
max_leaf_nodes
Максимальное количество листьев для каждого дерева. Связано с max_
depth, также контролирует рост дерева. Оптимизируйте либо этот параметр, либо max_depth, но не оба одновременно. Выберите целое число
от 2 до 4096
min_samples_
leaf
Минимальное количество примеров на лист — регуляризация роста
дерева. Целое число от 2 до 300
l2_
regularization
Параметр регуляризации L2 для ансамблирования. Число с плавающей
точкой от 0.0 до 100.0
max_bins
Максимальное количество интервалов (бинов) в гистограммах. Косвенный способ регуляризации деревьев. Выберите целое число от 32 до
512
Таблица A.4. Гиперпараметры для XGBoost
Гиперпараметр
Описание
learning_rate
Мультипликативное значение для уменьшения вклада деревьев решений. Действительное число от 0.001 до 0.1
n_estimators
Количество деревьев в ансамбле бустинга. Целое число от 100 до 1000
max_depth
Максимальная глубина дерева — способ контролировать дисперсию
предсказаний. Целое число от 1 до 12
min_child_
weight
Минимальная сумма веса экземпляра (гессиан) для дочернего узла. По
умолчанию равен единице. Чем выше значение, тем консервативнее
алгоритм. Мы предлагаем целое число от 1 до 10
max_delta_step
Обычно равно нулю, что означает отсутствие ограничений. При положительном значении действует как регуляризатор, ограничивая шаги
обновления. Полезно при дисбалансе классов в классификации. Мы
рекомендуем число с плавающей точкой от 0 до 10
Глава А. Гиперпараметры для классических моделей машинного обучения 545
Гиперпараметр
Описание
max_bin
Максимальное количество бинов в гистограммах. Целое число от 32
до 512
subsample
Доля примеров из обучающих данных, используемых для обучения
дерева. Действительное число от 0.1 до 1.0
colsample_
bytree
Доля признаков, используемых при построении каждого дерева. Действительное число от 0.1 до 1.0
reg_lambda
Параметр L2 регуляризации весов. Действительное число между 1e-9
и 100.0
reg_alpha
Параметр L1 регуляризации весов. Действительное число между 1e-9
и 100,0
gamma
Другой регуляризатор, ограничивающий разбиение дерева путем установки минимального сокращения значения функции потерь. Действительное число между 0 и 0.5
scale_pos_
weight
Значение веса, контролирующее баланс положительных и отрицательных классов, полезно для задач несбалансированной бинарной
классификации. По умолчанию установлено значение 1; типичное
значение: отношение количества отрицательных случаев к количеству
положительных. Мы рекомендуем действительное число в диапазоне
от 1e-6 до 500
Таблица A.5. Гиперпараметры для LightGBM
Гиперпараметр
Описание
learning_rate
Мультипликативное значение для уменьшения вклада деревьев решений. Действительное число от 0.001 до 0.1
n_estimators
Количество деревьев в ансамбле бустинга. Целое число от 100 до 1000
max_depth
Максимальная глубина деревьев решений. Способ контролировать
сложность и переобучение. Целое число от 1 до 12
num_leaves
Целое число от 2 до 2max_depth, представляет собой количество конечных
листьев, которые будет иметь дерево. Низкие значения действуют как
регуляризация сложности дерева
min_data_in_
leaf
Минимальное количество данных в одном листе. Помогает бороться
с переобучением. Ноль означает отсутствие ограничений. Целое число
от 0 до 300
min_gain_to_
split
Минимальный прирост для разделения в деревьях решений. Число
с плавающей точкой от 0 до 15
546 Глава А. Гиперпараметры для классических моделей машинного обучения
Гиперпараметр
Описание
max_bin
Максимальное количество бинов в гистограммах. Целое число от 32
до 512
subsample
Доля данных, выбираемых случайным образом без повторной выборки.
Действительное число от 0.1 до 1.0
subsample_freq
Частота сабсемплинга: целое число от 0 до 10. Если установлено в 0,
алгоритм игнорирует любые параметры, касающиеся сабсемплинга,
и сабсемплинг не происходит
feature_
fraction
Доля признаков, используемых на каждой итерации. Действительное
число от 0.1 до 1.0
reg_lambda
Параметр L2 регуляризации весов. Действительное число от 0.0 до
100.0
reg_alpha
Параметр L1 регуляризации весов. Действительное число от 0.0 до
100.0
scale_pos_
weight
Значение веса, контролирующее баланс положительных и отрицательных классов, полезно для задач несбалансированной бинарной
классификации. По умолчанию установлено значение 1; типичное
значение: отношение количества отрицательных случаев к количеству
положительных. Мы рекомендуем действительное число в диапазоне
от 1e-6 до 500
Б
Методы k ближайших соседей
и опорных векторов
В этом приложении мы рассмотрим классические алгоритмы машинного обу
чения с более вычислительной природой, которые не были затронуты в книге,
поскольку они реже используются в настоящее время и считаются устаревшими
по сравнению с ансамблями деревьев решений. В целом метод опорных векторов
(SVM) по-прежнему остается практичным алгоритмом, хорошо подходящим
для работы с высокоразмерными, зашумленными или небольшими данными.
С другой стороны, k ближайших соседей (k-NN) хорошо подходит для запуска
приложений, где данные имеют мало признаков, могут содержать выбросы
и не требуется высокая точность прогнозов. Например, SVM по-прежнему
можно использовать для классификации медицинских изображений (таких
как маммограммы и рентгеновские снимки), для обнаружения и отслеживания
транспортных средств или для обнаружения спама в электронной почте. В то же
время k-NN в основном применяется в рекомендательных системах, в частности
в подходах совместной (коллаборативной) фильтрации, для рекомендации продуктов или услуг на основе прошлого поведения пользователей.
Они подходят для большинства ситуаций, когда объем табличных данных не
слишком мал и не чрезмерно велик — как правило, когда их менее 10 000 строк.
Мы начнем с k-NN — алгоритма, который специалисты десятилетиями использовали в задачах машинного обучения и который легко понять и реализовать.
Затем мы завершим наш обзор описанием SVM и кратким экскурсом по использованию графических процессоров для выполнения этих алгоритмов на
датасетах среднего размера. Все примеры даны на датасете Airbnb NYC, представленном в главе 4. Вы можете снова поработать с ним, выполнив следующий
фрагмент кода:
548 Глава Б. Методы k ближайших соседей и опорных векторов
import numpy as np
import pandas as pd
excluding_list = [
'price', 'id', 'latitude', 'longitude',
Список названий столбцов, которые
'host_id', 'last_review', 'name', 'host_name'
следует исключить из анализа
]
categorical = [
Список названий столбцов, которые,
'neighbourhood_group', 'neighbourhood', вероятно, содержат категориальные
'room_type'
переменные в датасете
]
continuous = [
'minimum_nights', 'number_of_reviews', 'reviews_per_month',
'Calculated_host_listings_count'
Список названий столбцов, которые
]
data = pd.read_csv("./AB_NYC_2019.csv")
содержат непрерывные числовые
target_median = (
переменные в датасете
data["price"] > data["price"].median()
).astype(int)
Сбалансированная бинарная
целевая переменная
Код загрузит ваш датасет и определит, какие признаки следует исключить из
анализа или считать непрерывными либо категориальными при обработке.
Б.1. Алгоритм k ближайших соседей (k-NN)
Применительно к задачам регрессии и классификации алгоритм k-NN считается
одним из самых простых и интуитивно понятных . Он находит k (где k — целое
число) ближайших примеров из обучающего набора и использует информацию
о них для составления прогноза. Например, если задача — регрессия, он возьмет среднее значение k ближайших примеров. Если задача — классификация,
он выберет наиболее распространенный класс среди k ближайших примеров.
Технически k-NN обычно рассматривается как алгоритм обучения на основе экземпляров (instance-based), поскольку он запоминает обучающие примеры в их
исходном виде. Он также рассматривается как «ленивый» алгоритм, поскольку,
в отличие от большинства других, во время обучения выполняется минимальная
обработка данных — рассчитываются расстояния с помощью оптимизированных
алгоритмов и структур данных, что впоследствии делает поиск точек вблизи
обучающего примера менее затратным в вычислительном плане. Бˆольшая часть
вычислительной работы выполняется во время тестирования (рис. Б.1).
Мы применяем классификатор k-NN к данным Airbnb NYC, как показано
в главе 4, в листинге Б.1. Поскольку k-NN работает на основе расстояний, для
получения работоспособного решения признаки должны быть приведены
к одинаковому масштабу, чтобы обеспечить равный вес для каждого измерения
в процессе вычисления расстояний. Если признак имеет другой или меньший
Б.1. Алгоритм k ближайших соседей (k-NN) 549
масштаб, он будет перевешивать. Обратное произойдет, если признак будет
характеризоваться бˆольшим масштабом. Чтобы проиллюстрировать проблему,
давайте рассмотрим, что происходит, когда мы сравниваем расстояния в километрах, метрах и сантиметрах. Даже если расстояния сопоставимы, метры
и сантиметры будут численно превышать измерения в километрах. Эта проблема
обычно решается путем масштабирования признаков, например путем вычитания их среднего значения и деления на стандартное отклонение (операция,
известная как нормализация z-оценок или стандартизация). Кроме того, такие
методы, как уменьшение размерности или отбор признаков, также полезны для
этого алгоритма, поскольку переупорядочивание предикторов или изменение
их набора может привести к улучшению или ухудшению прогнозной производительности на задаче.
Рис. Б.1. Классификация новых примеров (треугольники) с помощью k-NN, где k = 3
В нашем случае, как это часто бывает с табличными данными, ситуация осложняется наличием категориальных признаков, которые после one-hot-кодирования
превратятся в бинарные переменные (0 или 1), имеющие иной масштаб по сравнению с нормализованными признаками. Предлагаемое решение заключается в том,
чтобы сначала дискретизировать числовые признаки, тем самым фактически
превратив их в бинарные, каждый из которых указывает, попадет ли исходное
числовое значение в определенный диапазон. Бинаризация непрерывных признаков осуществляется с помощью класса KBinsDiscretizer (https://mng.bz/N12N),
встроенного в пайплайн numeric_discretizing, который преобразует каждый
550 Глава Б. Методы k ближайших соседей и опорных векторов
числовой признак в пять бинарных, каждый из которых покрывает определенный
интервал значений. Во время обработки мы также применяем метод главных
компонент (principal component analysis, PCA), чтобы снизить размерность
и сделать все признаки некоррелированными. Однако это может ослабить
нелинейные зависимости в данных, поскольку PCA основан на линейных
комбинациях переменных. Некоррелированность полученных признаков —
характеристика данных, обработанных PCA, что хорошо подходит для k-NN:
алгоритм основан на расстояниях, вычисление которых правильно работает, если
измерения не связаны. Следовательно, любое изменение расстояния происходит
из-за изменений в одном измерении, а не в нескольких. В листинге Б.1 показан
код, реализующий процесс преобразования данных и обучения k-NN.
Листинг Б.1. Классификатор k-NN
from
from
from
from
from
from
sklearn.neighbors import KNeighborsClassifier
sklearn.pipeline import Pipeline
sklearn.compose import ColumnTransformer
sklearn.preprocessing import OneHotEncoder
sklearn.preprocessing import KBinsDiscretizer
sklearn.metrics import accuracy_score
Создает функцию
оценки, используя
метрику accuracy_score
categorical_onehot_encoding = OneHotEncoder(handle_unknown='ignore')
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
model = KNeighborsClassifier(n_neighbors=30,
weights="uniform",
algorithm="auto",
n_jobs=-1)
Создает итератор для пятифолдовой кроссвалидации с перемешиванием данных
и фиксированным случайным начальным
числом random_state
Создает экземпляр KNeighborsClassifier
с заданными гиперпараметрами
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_discretizing, continuous)],
remainder='drop',
verbose_feature_names_out=False,
sparse_threshold=0.0)
model_pipeline = Pipeline(
[('processing', column_transform),
('pca', PCA(n_components="mle")),
('modeling', model)])
Определяет ColumnTransformer для предобработки
признаков, применяя прямое кодирование
к категориальным признакам с низкой кардинальностью
и дискретизацию к числовым признакам
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Создает пайплайн, который
последовательно применяет
преобразование столбцов, снижает
размерность с помощью PCA, а затем
обучает модель k-NN на данных
Выполняет кросс-валидацию
данных, используя заданный
пайплайн, с оценкой доли верных
результатов
Выводит среднее значение
и стандартное отклонение
результатов тестирования
('pca', PCA(n_components="mle")),
('modeling', model)])
и дискретизацию к числовым признакам
Создает пайплайн, который
cv_scores = cross_validate(estimator=model_pipeline, последовательно применяет
преобразование столбцов, снижает
X=data,
размерность с помощью PCA, а затем
y=target_median,
Б.1. Алгоритм k ближайших
соседей
(k-NN)
551
обучает
модель k-NN
на данных
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
Выполняет кросс-валидацию
данных, используя заданный
пайплайн, с оценкой доли верных
результатов
Выводит среднее значение
и стандартное отклонение
результатов тестирования
После запуска скрипта вы получите результат, близкий к производительности
решения с использованием наивного байесовского классификатора:
0.814 (0.005) обучение: 0.13 с, прогнозирование: 8.75 с.
Производительность хорошая, хотя время инференса относительно велико.
Поскольку этот алгоритм работает по аналогии (он ищет похожие случаи в обу
чающих данных, чтобы сделать прогноз), он показывает лучшие результаты на
достаточно больших датасетах, где выше вероятность найти похожие случаи.
Естественно, достаточность размера датасета диктуется количеством используемых признаков, поскольку чем их больше, тем больше примеров понадобится
для эффективного обобщения алгоритмом.
Хотя часто акцент делается на подборе оптимального значения параметра k как
ключевого способа балансировки между недообучением и переобучением, мы
предлагаем обратить внимание на другие аспекты для эффективного использования этой модели. Алгоритм работает на аналогии и опирается на расстояния
в сложных пространствах, поэтому мы выделяем два важных вопроса:
измеряемые размерности и проклятие размерности;
выбор подходящей меры расстояния и предобработка признаков.
В k-NN классификация или оценки регрессии зависят от наиболее похожих
примеров, основанных на метрике расстояния, вычисленной по признакам.
Однако в датасете не все признаки могут считаться одинаково важными для
определения схожести примеров, и не все их можно сравнивать одинаковым
образом. Знание предметной области играет ключевую роль при использовании
k-NN, поскольку нужно отобрать только признаки, релевантные решаемой задаче. Если взять их слишком много, алгоритму придется работать в чрезмерно
сложном пространстве. На рис. Б.1 показано, как алгоритм k-NN работает всего
с двумя признаками (представленными по осям x и y). Интуитивно понятно, что
классификация новых случаев (треугольники на рисунке) может быть затруднена, если в области присутствуют смешанные классы или если рядом с новыми
случаями нет обучающих данных. В таких ситуациях вам придется полагаться
на более удаленные примеры.
552 Глава Б. Методы k ближайших соседей и опорных векторов
Здесь в игру вступает проклятие размерности, которое гласит, что по мере
увеличения числа признаков требуется все больше примеров, чтобы сохранить
осмысленное расстояние между точками данных. Кроме того, это явление подразумевает, что количество необходимых примеров растет экспоненциально
с увеличением числа признаков. Для алгоритма k-NN это означает, что, если
вы используете слишком много признаков и недостаточно примеров, он будет
работать в пустом пространстве. Поиск соседей станет архисложной задачей.
Кроме того, если вы включили как релевантные, так и нерелевантные признаки,
существует риск, что алгоритм отметит в качестве соседей примеры, которые
очень далеки от случая, который вам нужно предсказать, и выбор будет основан
на признаках, которые бесполезны. Следовательно, если вы собираетесь использовать k-NN, нужно тщательно отбирать признаки, которые будут использоваться (если вы не знаете, какие выбрать, положитесь на процедуру отбора
признаков), или хорошо разбираться в предметной области, к которой относится
задача, чтобы определить, что следует подавать на вход алгоритму. Парсимония
(экономия признаков) имеет важное значение для правильной работы k-NN.
Когда вы определились с признаками с учетом метрики расстояния, которую
будете использовать, их нужно стандартизировать, устранить избыточность
и преобразовать. Это связано с тем, что метрики расстояния основаны на абсолютных измерениях, а разные шкалы могут весить по-разному. Представьте, что
вы используете одновременно измерения в километрах, метрах и сантиметрах.
Сантиметры, скорее всего, будут преобладать, поскольку они легко дают самые
большие числовые значения. Кроме того, наличие похожих друг на друга признаков (проблема мультиколлинеарности) может привести к тому, что измерение
расстояния будет переоценивать их наборы по сравнению с отдельными признаками. Наконец, измерение расстояния подразумевает использование одинаковых
мер. Однако в датасете могут встречаться различные типы данных — числовые,
категориальные и временнˆые, — которые часто плохо сочетаются в расчетах
расстояния из-за различных числовых характеристик.
По этой причине, в дополнение к тщательному предварительному отбору признаков, при использовании k-NN мы рекомендуем применять признаки одного типа (только числовые или только категориальные), при необходимости
стандартизировать их, а также уменьшать их информационную избыточность
с помощью таких методов, как PCA (https://mng.bz/8OrZ), который преобразует
датасет в новый, где признаки не коррелируют между собой.
Б.2. Метод опорных векторов (SVM)
До 2010-х годов алгоритмы SVM имели репутацию самых перспективных для
табличных задач. Однако за последние 10 лет древовидные модели затмили славу
SVM (support vector machine), которые тем не менее остаются востребованным
Б.2. Метод опорных векторов (SVM) 553
семейством методов для решения бинарной и многоклассовой классификации,
регрессии, а также распознавания аномалий и новизны. Они основаны на идее, что
если наблюдения можно представить в виде точек в многомерном пространстве,
то существует гиперплоскость (то есть разделяющая плоскость, проходящая через множество измерений), которая может разделить их на классы или значения,
обеспечивая максимально возможное разделение между ними, что гарантирует
наиболее надежные и достоверные прогнозы. На рис. Б.2 показан простой пример SVM, примененного к задаче бинарной классификации с двумя признаками,
представленными на осях x и y, в качестве предикторов. Модель SVM создает
разделяющую линию, максимизирующую зазор между двумя классами, как показано на рисунке, где пунктирные линии показывают границы этого свободного
пространства. При этом модель учитывает только несколько точек вблизи разделителя, называемых опорными векторами, и игнорирует точки, которые расположены рядом, но сбивают алгоритм с толку (например, потому что они находятся
не на той стороне), а также игнорирует точки, которые находятся далеко от линии-разделителя. Выбросы оказывают незначительное влияние на этот алгоритм.
Рис. Б.2. Разделяющая гиперплоскость из SVM
Сильные стороны SVM заключаются в его устойчивости к переобучению, шуму
в данных и выбросам, а также в способности успешно обрабатывать датасеты,
содержащие многочисленные мультиколлинеарные признаки. Применяя различные нелинейные подходы к данным, SVM не требует преобразований (таких как
полиномиальное расширение), которые мы видели для логистической регрессии.
554 Глава Б. Методы k ближайших соседей и опорных векторов
Однако он может использовать генерацию признаков на основе предметной области, к которой относится решаемая задача.
Слабая сторона заключается в сложности оптимизации, которая может быть
осуществима только для ограниченного числа примеров. Более того, SVM лучше
всего подходит для бинарных предсказаний и просто для предсказания классов;
он не является вероятностным алгоритмом, и для извлечения вероятностей
потребуется его обертывание другим алгоритмом для калибровки (например,
логистической регрессией). Это ограничивает применимость SVM узким кругом
задач по оценке рисков.
В нашем примере мы повторно применяем датасет Airbnb NYC, используя SVM
для бинарной классификации с радиально-базисной функцией (radial basis
function, RBF) в качестве ядра — подхода, способного автоматически моделировать сложные нелинейные взаимосвязи между признаками (листинг Б.2).
Листинг Б.2. Классификатор SVM
from
from
from
from
sklearn.svm import SVC
sklearn.metrics import accuracy_score
sklearn.impute import SimpleImputer
sklearn.preprocessing import StandardScaler
numeric_standardization = Pipeline([
("imputation", SimpleImputer(strategy="constant", fill_value=0)),
("standardizing", StandardScaler())
])
Создает функцию оценки, используя
метрику accuracy_score
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
model = SVC(
C=1.0,
kernel='rbf',
Создает экземпляр класса
gamma='scale',
Support Vector Classifier
probability=False
с заданными гиперпараметрами
)
Создает пятиблочный
итератор кросс-валидации
с перемешиванием данных
и фиксированным случайным
начальным числом random_state
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_standardization, continuous)],
remainder='drop',
Определяет ColumnTransformer для
verbose_feature_names_out=False,
предобработки признаков, применяя one-hotsparse_threshold=0.0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
кодирование к категориальным признакам
с низкой кардинальностью и стандартизацию
к числовым признакам
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
Создает пайплайн, который
последовательно применяет
преобразование столбцов
и модель к данным
Выполняет кросс-валидацию на
данных, используя заданный
пайплайн, с оценкой доли
sparse_threshold=0.0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
кодирование к категориальным признакам
с низкой кардинальностью и стандартизацию
к числовым признакам
Создает пайплайн, который
Б.3. Использование
графических процессоров для машинного
обучения
555
последовательно
применяет
cv_scores
= cross_validate(estimator=model_pipeline,
преобразование столбцов
X=data,
и модель к данным
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
Выполняет кросс-валидацию на
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозирование: {score_time:0.2f} с.")
данных, используя заданный
пайплайн, с оценкой доли
верных результатов
Выводит среднее значение
и стандартное отклонение
результатов тестирования
Результаты довольно интересны, и, вероятно, их можно даже улучшить, скорректировав гиперпараметры:
0.821 (0.004) обучение: 102.28 с, прогнозирование: 9.80 с.
Однако времени, необходимого для обучения одного фолда, требуется чрезмерно
много по сравнению со всеми предыдущими алгоритмами машинного обучения.
В следующем разделе этого приложения мы обсудим, как GPU могут ускорить
процесс при работе с API scikit-learn.
Б.3. Использование графических процессоров
для машинного обучения
Благодаря быстрому росту популярности глубокого обучения в области data
science графические процессоры (GPU) получили широкое распространение
как для локальных, так и для облачных вычислений. Раньше мы слышали об
этих устройствах только в контексте 3D-игр, обработке графики и анимации.
Поскольку они дешевы и эффективны для задач быстрого матричного умножения, ученые и практики стали использовать их для вычислений в нейронных
сетях. RAPIDS, разработанный NVIDIA (одним из ведущих производителей
графических процессоров), представляет собой набор пакетов для выполнения
полного спектра задач data science (а не только глубокого обучения) на GPU.
Пакеты RAPIDS обещают помочь на всех этапах пайплайна машинного обучения, от начала до конца. Это меняет правила игры при использовании многих
классических алгоритмов машинного обучения, особенно SVM — наиболее
надежного выбора для более сложных задач, связанных с зашумленными данными, выбросами и большими датасетами (имеющими множество признаков,
особенно если они мультиколлинеарны или разреженны). В пакетах RAPIDS
(табл. Б.1) все команды приспособлены под существующие API. Это способствует быстрому внедрению этих инструментов на рынке, и пользователям не
надо заново изучать, как и что там работает.
556 Глава Б. Методы k ближайших соседей и опорных векторов
Таблица Б.1. Пакеты Rapids
Пакет Rapids
Задача
API
cuPy
Операции с массивами
NumPy
cuDF
Обработка данных
pandas
cuML
Машинное обучение
scikit-learn
В этом разделе мы рассмотрим, как заменить алгоритмы scikit-learn на пакет
RAPIDS cuML. В настоящее время он включает реализации линейных моделей,
k-NN и SVM, а также методов кластеризации и снижения размерности. В лис
тинге Б.3 показан код для тестирования классификатора опорных векторов
с ядром радиально-базисной функции, который мы только что опробовали
в предыдущем разделе, в его реализации RAPIDS (с использованием графического процессора P100).
Листинг Б.3. Классификатор опорных векторов от RAPIDS cuML
from cuml.svm import SVC
from sklearn.metrics import accuracy_score
Создает функцию оценки, используя
метрику accuracy_score
accuracy = make_scorer(accuracy_score)
cv = KFold(5, shuffle=True, random_state=0)
Создает итератор пятифолдовой
model = SVC(
кросс-валидации с перемешиванием
C=1.0,
данных и фиксированным случайным
kernel='rbf',
начальным числом random_state
gamma='scale',
Создает экземпляр класса Support Vector Classifier из
probability=False
библиотеки cuML с ускорением на GPU и заданными
)
гиперпараметрами
column_transform = ColumnTransformer(
[('categories', categorical_onehot_encoding, low_card_categorical),
('numeric', numeric_standardization, continuous)],
remainder='drop',
Определяет ColumnTransformer для предобработки
verbose_feature_names_out=False,
признаков, применяя one-hot-кодирование к категориsparse_threshold=0.0)
model_pipeline = Pipeline(
[('processing', column_transform),
('modeling', model)])
альным признакам с низкой кардинальностью и стандартизацию к числовым признакам
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
Создает пайплайн, который
последовательно применяет
преобразование столбцов
и модель к данным
Выполняет кросс-валидацию
данных, используя заданный
пайплайн, с оценкой доли
верных результатов
Выводит среднее значение
и стандартное отклонение
[('processing', column_transform),
('modeling', model)])
Создает пайплайн, который
последовательно применяет
преобразование столбцов
и модель к данным
cv_scores = cross_validate(estimator=model_pipeline,
X=data,
y=target_median,
Б.3. Использование графических
процессоров для машинного обучения 557
scoring=accuracy,
cv=cv,
return_train_score=True,
return_estimator=True)
mean_cv = np.mean(cv_scores['test_score'])
std_cv = np.std(cv_scores['test_score'])
fit_time = np.mean(cv_scores['fit_time'])
score_time = np.mean(cv_scores['score_time'])
print(f"{mean_cv:0.3f} ({std_cv:0.3f})",
f"обучение: {fit_time:0.2f}",
f"с, прогнозироание: {score_time:0.2f} с.")
Выполняет кросс-валидацию
данных, используя заданный
пайплайн, с оценкой доли
верных результатов
Выводит среднее значение
и стандартное отклонение
результатов тестирования
Результаты, которые мы получили:
0.821 (0.004) обучение: 4.09 с, прогнозирование: 0.11 с.
Как видим, мы получили те же результаты, но с применением cuML. Время
обработки сократилось со 102 секунд на фолд до 4 секунд. Если посчитать
экономию времени, то это 25-кратное увеличение скорости. Конкретный
прирост производительности зависит от модели GPU; чем мощнее графический процессор, тем быстрее вы получите результаты, поскольку это зависит
от того, насколько быстро графическая карта может передавать данные из
памяти центрального процессора и насколько быстро она может выполнять
матричные умножения.
Основываясь на такой производительности стандартных GPU, доступных широкому кругу пользователей, исследователи недавно создали приложения, объединяющие табличные данные с большими эмбеддингами из моделей глубокого
обучения (таких как текст или изображения). SVM хорошо работают с многочисленными признаками (но не превышающими число примеров) и разреженными
значениями (когда много нулей). В таких ситуациях SVM могут легко достичь
результатов, соответствующих самым современным требованиям, превосходя
другие более популярные алгоритмы для табличных данных, а именно XGBoost
и другие реализации градиентного бустинга, а также сквозные решения глубокого обучения, которые показывают себя слабее при недостаточном объеме
обучающих данных.
Наличие графического процессора и адаптация кода для использования алгоритмов RAPIDS делает некоторые классические алгоритмы машинного
обучения для табличных данных снова весьма конкурентоспособными, потому что в машинном обучении «нет бесплатных обедов» (более подробно
о теоремах «no-free-lunch» можно прочесть на сайте http://www.no-free-lunch.
org/). Принимая во внимание ограничения проекта (например, отсутствие
определенных ресурсов в среде проекта), никогда не исключайте априорное
тестирование задачи с помощью всех доступных алгоритмов, если это возможно.
Марк Райан, Лука Массарон
Машинное обучение на табличных данных:
XGBoost, глубокое обучение и ИИ
Перевела с английского Н. Богач
Научный редактор К. Быков
Руководитель дивизиона
Руководитель проекта
Ведущий редактор
Литературный редактор
Художественный редактор
Корректоры
Верстка
Ю. Сергиенко
А. Питиримов
Е. Строганова
Ю. Широнина
В. Мостипан
М. Котова, Т. Никифорова
Е. Неволайнен
Изготовлено в России. Изготовитель: ООО «Прогресс книга».
Место нахождения и фактический адрес: 194044, Россия, г. Санкт-Петербург,
Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373.
Дата изготовления: 05.2026. Наименование: книжная продукция. Срок годности: не ограничен.
Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12 — Книги печатные
профессиональные, технические и научные.
Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01.
Подписано в печать 12.03.26. Формат 70×100/16. Бумага офсетная. Усл. п. л. 45,150. Тираж 700. Заказ 0000.
ВОСПОЛЬЗУЙТЕСЬ
ВОЗ М ОЖ Н ОСТЬЮ
ПРИОБРЕСТИ КНИГИ
НА САЙТЕ ИЗДАТЕЛЬСТВА
piter.com
со скидкой по промокоду
20%
PITER