Реализация нейронной сети для соревнования Digit Recognizer на Kaggle и её прикладное использование. Часть №1

от автора

В данной статье будет рассмотрено одно из решений обучающей задачи на платформе Kaggle по распознаванию рукописных цифр. Будет продемонстрирован трюк, который может помочь читателю добиться высоких результатов в данном соревновании. После реализации нейронной сети будет реализовано серверное и веб‑приложение, с помощью которых пользователь сможет рисовать цифры и распознавать их с помощью нейронной сети. Статья ориентирована на начинающих специалистов в области машинного обучения и не носит новаторский характер. Списки на используемые источники (в том числе исходный код) будут представлены в конце статьи. Решения не новы, однако с их помощью можно достичь высоких результатов. Например, автору удалось добиться score равному 0.99896.

Введение

В настоящее время существует большое число решений классической задачи Digit Recognizer на платформе Kaggle. Участники представляют свои решения значение score в которых могут быть самые разные и не всегда удаётся повторить успех автора исходного решения, которое довольно часто разбирается новичками решившими взобраться на вершину лидерборда. Чтобы добиться высоких результатов существующего обучающего набора может быть недостаточно и приходится искать обходные пути с помощью применения различных архитектур или наращивания обучающего набора (часто это делают через аугментацию, но в статье будет рассмотрен ещё один приём). Однако из большого числа найденных мной решений лишь не многие позволяли продвигаться по лидерборду и улучшать показатель своего score на более существенные значения. В каждом решении есть определённые недостатки, но один из них самый существенный и распространяется на все решения и это нехватка данных в датасете — проблема, которая будет решена в рамках данной статьи.

Постановка задачи

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

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

Разработка нейронной сети

Подготовка данных

Для начала отметим, что данных для обучения с соревнования Kaggle достаточно мало, чтобы хорошо обучить модель для распознавания цифр — их всего 42 000:

# Тренировочный датасет MNIST с соревнования Kaggle train_dataset = np.loadtxt('train.csv', skiprows=1, delimiter=',') train_dataset.shape # (42000, 785)

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

# Генератор новых данных datagen = ImageDataGenerator(    rotation_range=10,          # Поворот на случайный угол до 10 градусов    zoom_range=0.1,             # Увеличение размера до 10 %    width_shift_range=0.1,      # Сдвих влево / вправо до 10 процентов    height_shift_range=0.1,     # Сдвиг вверх / вниз до 10 процентов )

Помимо этого следует воспользоваться трюком, с помощью которого мы увеличим обучающую выборку до 112 000 записей. Заключается трюк в том, чтобы просто объединить обучающий набор из соревнования в Kaggle и обучающий / тестовый наборы из стандартного датасета MNIST, который также расположен на Kaggle.

Достаточно простой трюк. Следующий код его реализует:

# Тренировочный датасет стандартного MNIST train_mnist = np.loadtxt('mnist_train.csv', skiprows=1, delimiter=',')  # Тестовый датасет стандартного MNIST test_mnist = np.loadtxt('mnist_test.csv', skiprows=1, delimiter=',')  # Тренировочный датасет MNIST с соревнования Kaggle train_dataset = np.loadtxt('train.csv', skiprows=1, delimiter=',')  # Объединяем датасеты dataset = np.concatenate((train_dataset, train_mnist, test_mnist))  print(dataset.shape) # (112000, 785)

Между тренировочным датасетом с соревнования Kaggle и стандартным датасетом MNIST есть небольшая разница: в стандартном датасете записей на 18 000 больше.

train_dataset.shape # (42000, 785)  train_mnist.shape # (60000, 785)

В общем‑то, можно было бы с таким же успехом добавить несколько раз один и тот же датасет предварительно проведя над ним аугментацию, однако на удивление и такой подход неплохо себя показал (генератор работает только при обучении сети). Это будет продемонстрировано позже, после компиляции модели и её обучения.

И да, датасеты абсолютно одинаковые, просто в соревновании не 60 000 записей в обучающей выборке на 10 000 в тестовой, а 42 000 на 28 000. В сумме одно и тоже — 70 000.

С помощью данного трюка также происходит обучение на модифицированной тестовой выборке. Просто она модифицируется во время обучения при работе генератора новых данных (ImageDataGenerator). В этом и кроется суть трюка — наша модель максимально близка к тестовому набору данных соревнования, чтобы можно было их правильнее определить. Ведь модель может найти признаки в модифицированной тестовой выборке, а затем легко их определить и в целевой тестовой выборке. Здесь я ещё раз напомню о задаче — главное реализовать нейронную сеть, которая получит score максимально приближённое единице.

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

# Выделяем данные для обучения (без первого столбца с ответами) x_train = dataset[:, 1:]  # Изменяем размер данных обучающей выборки (28x28x2) x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)  # Размер входа input_shape = (28, 28, 1)

Как видно из программного кода сначала мы выделяем отдельно обучающую выборку (без ответов, просто данные), а затем изменяем размер обучающей выборке. Изменить размер выборки получилось благодаря удалению столбцов ответов (785 — 1 = 784 = 28×28). Если бы мы его не удаляли, у нас была бы ошибка:

Рисунок 1 - Ошибка если столбец ответов не был удалён

Рисунок 1 — Ошибка если столбец ответов не был удалён

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

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

# Нормализация данных для обучения x_train /= 255.0

Почему 255, а не, скажем, 127? Или может быть 65? Всё достаточно просто.

В многомерном массива x_train содержится обучающая выборка, каждый элемент который представляет собой определённый массив, состоящий из значений от 0 до 255, и этот диапазон выбран не случайно — это интенсивность цветов, берущая своё начало из цветовой кодировки RGB. Полезный материал по данной теме будет представлен в конце статьи. А мы продолжаем.

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

# Выделяем правильные ответы y_train = dataset[:, 0]  # Преобразуем ответы в формат one hot encoding y_train = utils.to_categorical(y_train)
Рисунок 2 - Ответы в формате one hot encoding

Рисунок 2 — Ответы в формате one hot encoding

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

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

# Разделяем данные на два набора - для обучающей выборки и для тестирования X_train, X_val, Y_train, Y_val = train_test_split(x_train, y_train, test_size = 0.1, random_state=random_seed)

Разделение обучающей выборки и ответов на под выборки реализовано с помощью утилиты train_test_split. Ей были переданы такие параметры, как:

  1. Обучающая выборка

  2. Выборка ответов выделенная из обучающей выборки

  3. Размер тестовой выборки (в данном случае — 10% от обучающей)

  4. random_state, который равен инициализатору генератора случайных чисел

Для справки (random seed)

Random seed (рандомное зерно) — это значение, используемое для инициализации генератора случайных чисел. Генератор случайных чисел — это алгоритм, который создает последовательность чисел, которая кажется случайной. Random seed позволяет сделать эту последовательность более детерминированной и повторяемой. То есть, указав определенное random seed мы можем получить одинаковую последовательность чисел при каждом запуске генератора случайных чисел.

Генератор псевдослучайных чисел — это алгоритм, который создаёт последовательность чисел, которая кажется случайной. Рандомное зерно позволяет сделать эту последовательность более детерменированной и повторяемой. Указав определённое рандомное зерно мы можем получить одинаковую последовательность чисел при каждом новом запуске генератора псевдослучайных чисел.

Отличный пример использования рандомного зерна — игра Minecraft. С помощью random seed осуществляется генерация случайного мира. Он определяет уникальный идентификатор для каждого мира, который влияет на генерацию ландшафта, распределение блоков, размещение структур и другое. Это позволяет игрокам иметь более контролируемый и предсказуемый процесс генерации мира, а также возможность делится своими игровыми мирами с другими игроками

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

X_train.shape # (100800, 28, 28, 1)

Всё же больше 100 000 записей лучше, чем меньше 50 000.

Теперь рассмотрим пример того, как работает механизм аугментации. т. е. каким образом данные модифицируются.

Ранее в статье я уже описал модель своего ImageDataGenerator

datagen = ImageDataGenerator(    rotation_range=10,          # Поворот на случайный угол до 10 градусов    zoom_range=0.1,            # Увеличение размера до 10 %    width_shift_range=0.1,      # Сдвих влево / вправо до 10 процентов    height_shift_range=0.1,     # Сдвиг вверх / вниз до 10 процентов )

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

Рисунок 3 - Аугментация данных для цифры 6 (к этому числу мы ещё вернёмся ... )

Рисунок 3 — Аугментация данных для цифры 6 (к этому числу мы ещё вернёмся … )

Как мы можем видеть, цифра изменяет своё положение и в целом идут определённые изменения, что можно оценить невооруженным глазом.

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

Разработка архитектуры нейронной сети

Для начала, приведу полный листинг программного кода архитектуры нейронной сети:

# Создание свёрточной нейронной сети  model = Sequential()  # Входной слой model.add(Input(shape=(28, 28, 1))) # Добавление строк и столбцов с нулями сверху, снизу, слева и справа от изображения model.add(ZeroPadding2D(padding=(1, 1), input_shape=(28, 28, 1))) # Слой свёрточной сети с ядром свёртки размера 5x5 и фильтром 32 model.add(Conv2D(filters = 32, kernel_size = (5,5), padding = 'Same',                  activation ='relu', input_shape = (28,28,1))) # Batch-нормализация (ускоряем обучение, стабилизируем нейронную сеть) model.add(BatchNormalization()) # Слой свёртки с ядром свёртки 5x5 и фильтром 32 model.add(Conv2D(filters = 32, kernel_size = (5,5),padding = 'Same',                  activation ='relu')) model.add(BatchNormalization()) # Слой функции активации "relu" model.add(Activation(activations.relu)) # Максимальная операция объединения в пул для 2D-пространственных данных model.add(MaxPooling2D(pool_size=(2,2))) # Добавление нулевых строк и столбцов model.add(ZeroPadding2D(padding=(1, 1))) # Применение метода регуляризации, который случайным образом отключает от изменения 20% нейронов model.add(Dropout(0.2))  # Свёрточный слой с ядром 3x3 и фильтром 64 model.add(Conv2D(filters = 64, kernel_size = (3,3),padding = 'Same',                  activation ='relu')) model.add(BatchNormalization()) model.add(Conv2D(filters = 64, kernel_size = (3,3),padding = 'Same',                  activation ='relu')) model.add(BatchNormalization()) model.add(Activation(activations.relu)) model.add(MaxPooling2D(pool_size=(2,2), strides=(2,2))) model.add(Dropout(0.2))  # Свёрточный слой с ядром 3x3 и фильтром 256 model.add(Conv2D(filters = 256, kernel_size = (3,3),padding = 'Same',                  activation ='relu')) model.add(BatchNormalization()) model.add(Conv2D(filters = 256, kernel_size = (3,3),padding = 'Same',                  activation ='relu')) model.add(BatchNormalization()) model.add(Activation(activations.relu)) model.add(MaxPooling2D(pool_size=(2,2), strides=(2,2))) model.add(Dropout(0.3))  # Сглаживание многомерных тензоров в одно измерение (конвертация в вектор размера 1xN) model.add(Flatten()) # Полносвязный слой с функцией активации "relu" model.add(Dense(128, activation = "relu")) model.add(Dropout(0.4)) # Полносвязный слой с функцией активации "softmax" (категоризация) model.add(Dense(10, activation = "softmax"))

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

Входной слой задаётся явным образом через соответствующий layer и размер данных поступающих на вход (для одного изображения) — 28×28, с одним каналом.

Сразу после входного слоя расположен слой ZeroPadding2D, который добавляет определённое число нулевых строк и столбцов.

После ZeroPadding2D данные переходя в свёрточный слой с ядром свёртки 5×5 и количеством фильтров 32, а затем идёт batch‑нормализация для стабилизации работы нейронной сети и ускорения её обучения.

Затем все данные проходят через функцию активации relu, после которой идёт слой MaxPooling2D и ZeroPadding2D с Dropout’ом, который временно не изменяет какой‑то процент нейроннов.

Далее идут два аналогичных блока, только у каждого из них на свёрточных слоях меняются ядра свёртки и количество фильтров.

В самом конце происходит сглаживание многомерных тензоров в одно измерение (иными словами многомерный массив становится одномерным), после чего данные проходят ещё через один слой с функцией активацией relu, Dropout’ом и на выходе получаем результат работы функции активации softmax, которая определила класс цифры на изображении (от 0 до 9).

В общем‑то, это всё что связано с архитектурой сети.

Рисунок 4 - Результат сборки архитектуры нейронной сети

Рисунок 4 — Результат сборки архитектуры нейронной сети

В результате нейронная сеть в совокупности имеет 1 348 202 параметра. Это не много, но достаточно для достижения хорошего результата.

Перейдём в написанию callbacks для нейронной сети.

Callbacks

Колбэки (или callbacks) — это отличный инструмент, который может быть полезен в течении всего обучения нейронной сети.

Для тех, кто знаком с backend-разработкой

Callbacks можно грубо интерпретировать как middleware в серверном программировании когда перед обработкой нового запроса сначала обрабатываются callbacks, а лишь затем сам запрос (или наоборот — сначала запрос, а затем callbacks). Callbacks является чем‑то вроде middleware, только в машинном обучении.

Для начала, следует определить колбэк, который отработает тогда, когда нейронная сеть зафиксировала свой лучший результат во время обучения. Иными словами — нужно периодически «сохраняться», сохраняя лучший результат. С этим нам поможет колбэк ModelCheckpoint.

# Колбэк для сохранения лучшего варианта работы нейронной сети checkpoint = ModelCheckpoint('mnist-cnn.hd5',                              monitor='val_accuracy',     # Доля правильных ответов на проверочном множестве                              save_best_only=True,        # Сохраняем только лучший результат                              verbose=1)                  # Вывод логов

В качестве критерия мониторинга мы выбрали val_accuracy (множество правильных ответов) и указали, что мы сохраняем только лучший результат в определённую папку.

Следующий колбэк необходим для того, чтобы нейронная сеть двигалась дальше в случае если результат остаётся прежним какое‑то число итераций (т. е. обучение не улучшается). Данный колбэк называется ReduceLROnPlateau и определяется следующим образом:

# Колбэк для изменения скорости обучения learning_rate_reduction = ReduceLROnPlateau(monitor='val_accuracy', # Метрика                                             patience=3,             # Число эпох без улучшения результата                                             verbose=1,              # Вывод логов                                             factor=0.5,             # Коэффициент на который мы будем умножать скорость обучения сети                                             min_lr=0.00001)         # Минимальная скорость обучения сети

Наиболее важными параметрами в этом колбэке выступают patience, factor и min_lr.

Параметр patience отвечает за число эпох (или итераций), преодолев которые нужно умножать скорость обучения на параметр factor, а min_lr определяет насколько минимально мы можем уменьшить скорость обучения.

После всех необходимых процедур определяем размер мини‑выборки для обучения и запускаем сам процесс обучения:

# Размер мини-выборки batch_size = 32  # Сборка модели с сохранением истории изменений history = model.fit(datagen.flow(X_train, Y_train, batch_size=batch_size),                     epochs=35,                     validation_data=(X_val, Y_val),                     steps_per_epoch=X_train.shape[0] // batch_size,                     verbose=1,                     callbacks=[checkpoint, learning_rate_reduction],                     shuffle=True)

Возвращаемся к теме тавтологии обучающих и тестовых выборок. Как видно из кода в datagen.flow передаются данные для обучения X_train (данные) и Y_train (метки). Эти данные являются под выборкой обучающей выборки которую мы разделили с помощью утилиты train_test_split. Но также из обучающей выборки мы взяли данные и для тестирования — X_val (данные) и Y_val (метки). При каждой эпохе обучения у нас генерируются данные с помощью ImageDataGenerator и проверяются с помощью тестирующей выборки. Таким образом нейронная сеть всегда получает почти уникальную обучающую выборку, часть из которой максимальна приближена к тестирующей под выборке и тестирующей выборке (которая не участвовала в объединении выборок).

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

После порядка 30 итераций максимальный результат данной нейронной сети и небольшой хитрости с выборками был получен результат в score = 0.9986, что продемонстрировано на рисунке ниже. Результат локально, а не на Kaggle.

Рисунок 5 - Визуализация результатов обучения нейронной сети

Рисунок 5 — Визуализация результатов обучения нейронной сети

После отправки результатов на Kaggle результат составил 0.99896.

Рисунок 5 — Визуализация результатов обучения нейронной сети

Рисунок 6 — Подтверждение результата

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

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

Выводы

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

В следующей части статьи будет рассмотрена разработка веб‑приложения на React.js, в котором можно будет нарисовать цифру на холсте и отправив на сервер получить определённый ответ (распознанную цифру). Сервер будет написан на Flask и будет разобран алгоритм загрузки весов моделей и работа обученной нейронной сети (и ещё вернёмся к цифре 6…).

Список использованных источников

  1. Исходный код реализованной нейронной сети и обученная модель со score = 0.99896 на Kaggle (public): исходный код

  2. Ссылка на стандартный датасет MNIST на платформе Kaggle: стандартный датасет

  3. Ссылка на соревнование: Digit Recognizer


ссылка на оригинал статьи https://habr.com/ru/articles/778546/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *