Нейронные сети довольно популярны. Их главное преимущество в том, что они способны обобщать довольно сложные данные, на которых другие алгоритмы показывают низкое качество. Но что делать, если качество нейронной сети все еще неудовлетворительное? И тут на помощь приходят ансамбли... Ансамбль алгоритмов машинного обучения — это использование нескольких (не обязательно разных) моделей вместо одной. То есть сначала мы обучаем каждую модель, а затем объединяем их предсказания. Получается, что наши модели вместе образуют одну более сложную (в плане обобщающей способности — способности "понимать" данные) модель, которую часто называют метамоделью. Чаще всего метамодель обучается уже не на нашей первоначальной выборке данных, а на предсказаниях других моделей. Она как бы учитывает опыт всех моделей, и это позволяет уменьшить ошибки. Итак, будем работать с датасетом Digits из Sklearn, так как его можно довольно просто получить в две строчки кода: Посмотрим, как выглядят наши данные:  Ага, чиселки принимают значения от 0 до 15. Значит, нужна нормализация: Используем StandardScaler: он переводит все данные в границы от -1 до 1. Обучаем его только на тренировочной выборке, чтобы он не подстраивался под тест — так score модели после теста будет ближе к score'у на реальных данных: И нормализуем наши данные: Простой сверточной сети вполне достаточно для демонстрации: Определим некоторые гиперпараметры : Сперва преобразуем данные в тензоры, с которыми работает PyTorch: В PyTorch есть очень удобные обертки на случай, если ваши данные — это простые тензоры. Здесь датасет — штука, которая может вернуть пару X и y по индексу (прокачанный массив), а DataLoader — итератор, который будет последовательно возвращать наши пары <X, y> батчами по 64 картинки: Штуки, которые нужны для обучения модельки: И само обучение: Из-за некоторых особенностей Torch'а и алгоритмов, результат на Вашей машине может не совпадать с полученным здесь. Это вполне нормально. Рассмотрим только последние 20 эпох. Можно заметить, что моделька переобучилась: качество на тренировочной выборке выше качества на тестовой. Так, и что же имеем: только 74% предсказаний оказываются верными. Not great, not terrible. Будем использовать Bagging. Очень кратко этот подход можно описать так: берутся несколько моделей, обучаются независимо, а затем все модели голосуют за результат. И тут могут быть различные варианты реализации: либо все модели имеют одинаковую "цену" голоса, либо у их голосов есть какие-то веса. Предсказанием метамодели считается усредненный результат. В sklearn есть модуль ensembles, в котором представлено несколько алгоритмов для реализации ансамблей. Наш случай — классификация, используем BaggingClassifier: Ансамбли оперируют базовыми моделями. В случае sklearn, базовая модель — сущность, реализующая методы sklearn.base.BaseEstimator. В зависимости от вида ансамбля, будут нужны различные реализации функций этой сущности. Для нашего случая необходимо описать метод fit (для обучения модели), функцию predict_proba, выдающую вероятность каждого класса (для того, чтобы метамодель смогла по вероятностям оценивать уверенность базовых классификаторов и присваивать их голосам большие или меньшие веса), и функцию predict (которая выдает номер класса), чтобы было удобно оценивать качество модели. Ради понятности буду описывать каждую часть кода отдельно, потом их просто нужно "склеить" в один класс. Конструктор класса базовой модели. Из тех граблей, на которые я успел наткнуться, стоит отметить именование параметров и их типы. Во-первых, если вы не хотите переписывать функции клонирования объекта базового классификатора (а придется переписывать аж 2 функции — get_params и set_params), то нужно давать аргументам в конструкторе те же имена, что и присваиваемым переменным класса. То есть, если вы подаете в конструктор параметр net_type, то в методе __init__ вы должны объявить переменную net_type. Во-вторых, лучше передавать аргументами в конструктор не объекты, а функции для их создания, потому что при копировании базовых моделей (а sklearn именно копирует переданную вами модель несколько раз, пока не получит нужное число базовых моделей) возникают различные сложности с функциями copy, deepcopy и ссылками на объекты. Например, может оказаться, что переданный и скопированный объект оптимизатора ссылается не на новый (скопированный) объект модели, а на самый первый, использованный при инициализации ансамбля. Самое интересное: метод fit. По сути, он работает как и обучение нашей первой нейросети — пропускаем данные, считаем Loss, делаем обратное распространение ошибки. Вот только вопрос: как нам понять, какое количество эпох надо обучать модель? Первым решением может являться задание определенного количество эпох до обучения. Например, 200 (как в первой модели). Но это может привести к переобучению, а может и к недообучению. Второе решение — смотреть, как изменяется accuracy нашей модели. У первой нейросети можно заметить, что в конце accuracy держится примерно на одном уровне несколько эпох подряд. Тогда мы можем остановить обучение, если модель уже на протяжении нескольких (tol_epochs) эпох не улучшает свое качество (то есть accuracy изменяется не более, чем на accuracy_tol). Теперь будем делать функцию для предсказания вероятностей классов. По сути — так же пропускаем данные, только не считаем Loss, а записываем все в какой-нибудь массив: Простая и относительно красивая функция: Итак, протестируем наш новоиспеченный классификатор: Все готово для создания ансамбля. Теперь можно с помощью уже реализованной функции score оценить accuracy нашего ансамбля: Ансамбль действительно лучше справляется с задачей классификации, чем одна модель. Это происходит благодаря большей обобщающей способности. У ансамблей есть ряд преимуществ по сравнению с обычными моделями. Например, товарищи обнаружили, что ансамбли являются более устойчивыми на новых данных (то есть их предсказания не начинают "скакать" из-за неуверенности модели).  Во-первых, обучение. Возможно, нужно смотреть на Loss'ы или как-нибудь подбирать параметры для остановки обучения на ходу. Во-вторых, можно реализовать Boosting. Его отличие от Bagging'а в том, что модели строятся не независимо, а каждая следующая учитывает ошибки предыдущей. То есть при обучении неудачным (неправильно классифицированным) примерам придается больший вес. Тут лучше покопаться в исходном коде sklearn, а потом можно поэлементно умножать Loss нейронки на какие-то веса. В третьих, можно формировать ансамбль из разных видов нейронок. Тогда увеличивается разница в том, как модели "видят" данные. Как будто у метамодели появляется новая точка зрения, и она может глубже понять ситуацию. Спасибо, что дочитали этот пост до конца. Мне было бы очень интересно услышать Ваше мнение по поводу статьи — какие-то замечания, предложения, идеи, в общем, все, что Вас воодушевляет и чем Вы готовы поделиться с миром. Пару слов об мне. Меня зовут Евгений, Data science'ом я занимаюсь уже полтора года. Сейчас в большей степени погружаюсь в Computer vision, но также интересуюсь нейротехнологиями. (Соединить искусственные и натуральные нейронные сети — моя мечта!) Этот пост создан благодаря нашей команде — FARADAY Lab. Мы — начинающие российские стартаперы и готовы делиться с Вами тем, что узнаем сами. Удачи c: ml'щик