MNIST with submit size limitation

Classify photos of handwritten digits.

Description

Задача: распознать цифру на картинке.

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

Соревнование поможет новичкам разобраться в базовых принципах машинного обучения и выполнить задачу, максимально похожую на реальную. Наличие GPU не обязательно. Модель вполне можно обучить за разумное время на обычном компьютере, используя CPU. Или же воспользоваться бесплатным Google Colaboratory, предоставляющим доступ к GPU и TPU для ускорения обучения. Если с какими-то технологиями возникнут сложности или многое из написанного ниже будет совершенно непонятно, не пугайтесь. Почти всё уже реализовано во фреймворках, нужно лишь правильно скомпоновать веб-сервер с ML-кодом. Или же за основу взять готовое base-line решение и менять его на своё усмотрение. Оно также вполне сгодится как шаблон для использования в собственных разработках.

Профессионалам, надеюсь, будет интересно побороться за минимальный размер сабмита и попробовать model compression, model quantization, model pruning, model distillation и много подобного на датасете, который позволяет быстро проводить много экспериментов.

Dataset

Датасет (набор данных) содержит монохромные картинки размера 28 на 28 пикселей с изображениями рукописных арабских цифр и называется MNIST. Всего 70 000 изображений: 60 000 составляет выборка для обучения и 10 000 - тестовая выборка для проверки качества обученной модели. Классы сбалансированы, т.е. каждая цифра встречается примерно одинаковое количество раз. А сами изображения цифр выровнены по центру.

MNIST dataset description on Wikipedia.

Yann LeCun: "It is a good database for people who want to try learning techniques and pattern recognition methods on real-world data while spending minimal efforts on preprocessing and formatting."

Датасет MNIST входит в состав большинства фреймворков для машинного обучения и легко доступен в них. Например, в Keras: keras.datasets.mnist и PyTorch: torchvision.datasets.MNIST. В менее удобной форме его можно скачать здесь: 1.zip.

Metric

Качество модели оценивается метрикой Accuracy. При равных значениях accuracy предпочтение отдаётся сабмиту меньшего размера.

Accuracy расчитывается как отношение числа правильных предсказаний к общему количеству предсказаний. Чем значение Accuracy больше, тем лучше. Пример: из 20 цифр 17 предсказаны правильно, а 3 неправильно. Accuracy = 17 / 20 = 0.85.

Evaluation

Сабмит запускается на двух датасетах: всем доступном оригинальном MNIST-е и приватном датасете, полученном аугментацией MNIST-а. Оценка качества модели ведётся по второму датасету. Значения метрики доступны по обеим датасетам.

Сабмит - это zip-архив. Он состоит из текстовых файлов, описывающих необходимый для установки софт, и делающих предсказания скриптов.

apt.txt описывает пакеты OS Ubuntu, которые будут установлены командой sudo apt install перед запуском кода сабмита. Если в сабмите нет apt.txt, то никакие дополнительные пакеты Ubuntu не устанавливаются. Пример содержимого apt.txt:

ffmpeg graphviz

python.txt описывает модули языка Python 3, которые будут установлены командой pip3 install перед запуском кода сабмита. Если в сабмите нет python.txt, то никакие дополнительные модули Python-а не устанавливаются. Пример содержимого python.txt:

sklearn scipy scikit-image tornado

run.sh - это shell-скрипт. Он автоматически начнёт выполняться после установки софта. В нём описывается, что ещё запустится для корректной работы сабмита. Содержимое сабмита разархивируется в директорию ~/submit/ , поэтому остальные скрипты, например, написанные на Python-е, запускаются из этой директории. Также run.sh получает два параметра запуска: адрес и порт, на котором код сабмита будет принимать запросы. Пример содержимого скрипта run.sh:

#!/bin/sh -x /usr/bin/python3 ~/submit/predict.py --model ~/submit/v1.h5 --host $1 --port $2

Код сабмита должен должен работать в виде микросервиса и принимать http-запросы на том адресе и порту, которые были переданы run.sh. Ему отправляются монохромные изображения размером 28x28 в форматах jpeg или png.

Запросы и ответы делаются по протоколу HTTP 1.1. Правильный ответ на правильный запрос содержит 200-ый код. При ошибке в запросе или в переданных данных правильным будет ответ с 400-ым кодом. В случае любых других ошибок правильным будет ответ с 500-ым кодом.

Все выдаваемые сабмитом ответы имеют JSON-формат. Важно иметь ввиду, что может быть отправлено несколько параллельных запросов. Запросы делаются к трём url-ам: /status , /predict и /exit .

На GET-запросы с url-ом /status правильным будет ответ с содержимым {"status": "Ok"} . Смысл запросов к статусу в том, чтобы быстро убедиться, что сабмит запущен и отвечает на запросы.

В POST-запросах с url-ом /predict будут отправляться одно или несколько изображений с именем параметра x . На них сабмит должен отвечать массивом чисел с предсказанными цифрами в поле y . Пример ответа на запрос с пятью изображениями: {"y":[1,2,3,4,5]} . Пример ответа на запрос с одним изображением: {"y":[8]} .

На GET-запросы с url-ом /exit корректным будет ответ с содержимым {"exit": "Ok"} . После отправки ответа сабмит должен тут же закончить свою работу с нулевым exit code, что скажет о нормальном завершении работы. Это нужно для быстрой и корректной остановки сабмита.

Для проверки работоспособности написанного кода сабмита можно использовать curl. Пример отправки запроса с двумя изображениями, находящихся в файлах 1.jpg и 2.png:

curl -v -F 'x=@1.jpg' -F 'x=@2.png' http://127.0.0.1:12345/predict

Пример predict.py, принимающего запросы и формирующего корректные ответы:

import argparse import traceback import sys import tornado.web import tornado.httpserver import tornado.ioloop import tornado.log import numpy as np import PIL.Image import io import keras.models import keras.backend def preprocess(image): image = image.astype(keras.backend.floatx()) image /= 255 image = np.expand_dims(image, axis=-1) return image class StatusHandler(tornado.web.RequestHandler): def get(self): self.write({"status": "Ok"}) # JSON class ExitHandler(tornado.web.RequestHandler): def get(self): self.write({"exit": "Ok"}) # JSON def on_finish(self): sys.exit(0) class PredictHandler(tornado.web.RequestHandler): def initialize(self, model): self.model = model def post(self): ### process request. Incorrect request produces a response with status 400. try: # prevent decompression bomb PIL.Image.MAX_IMAGE_PIXELS = 28 * 28 # get images from HTTP-request images = [] # https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPServerRequest.files for field_name, files in self.request.files.items(): #print('Argument:', field_name, len(files)) if field_name == 'x': for info in files: # https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPFile filename = info["filename"] #content_type = info["content_type"] image = info["body"] #print("Image:", len(image), filename, content_type) # decode image image = PIL.Image.open(io.BytesIO(image)) if image.size != (28, 28): raise ValueError("Incorrect image shape:", filename, image.size) if image.mode != 'L': raise ValueError("Image is not grayscale:", filename, image.mode) image = np.asarray(image, dtype=np.uint8) images.append(image) except: e = traceback.format_exc() self.set_status(400) self.write({"error": e}) # JSON return ### generate response. Any error produces a response with status 500. try: # predict digits digits = [] for image in images: image = preprocess(image) image = np.expand_dims(image, axis=0) # add batch dimension predict = self.model.predict(image) digit = np.argmax(predict[0]) # convert from numpy to int to prevent error: "TypeError: 8 is not JSON serializable" digit = int(digit) digits.append(digit) # send JSON response # https://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.write self.write({'y': digits}) # JSON except: e = traceback.format_exc() self.set_status(500) self.write({"error": e}) # JSON def _main(args): modelPath = args.model host = args.host port = args.port # based on https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py model = keras.models.load_model(modelPath) # for debug only tornado.log.enable_pretty_logging() app = tornado.web.Application([ (r"/predict", PredictHandler, dict(model=model)), (r"/status", StatusHandler), (r"/exit", ExitHandler), ]) server = tornado.httpserver.HTTPServer( app, decompress_request=True, max_body_size=1 * 1024 * 1024 * 1024, # 1Gb ) server.listen(port=port, address=host) tornado.ioloop.IOLoop.current().start() if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument( '--model', help='Path to the trained model. Example: --model 123.h5', required=True) parser.add_argument( '--host', help='Host to listen. Example: --host 127.0.0.1', required=True) parser.add_argument( '--port', help='Port to listen. Example: --port 12345', required=True) _main(parser.parse_args())

Baseline

Пример train.py из base-line решения, использованного для получения модели mnist_v1.h5 в Google Colaboratory:

# Based on https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py import keras from keras.datasets import mnist from keras.models import Sequential from keras.layers import Dense, Dropout, Flatten from keras.layers import Conv2D, MaxPooling2D from keras import backend as K batch_size = 128 num_classes = 10 epochs = 12 # input image dimensions img_rows, img_cols = 28, 28 # the data, split between train and test sets (x_train, y_train), (x_test, y_test) = mnist.load_data() x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1) x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1) input_shape = (img_rows, img_cols, 1) x_train = x_train.astype(K.floatx()) x_test = x_test.astype(K.floatx()) x_train /= 255 x_test /= 255 print('x_train shape:', x_train.shape) print(x_train.shape[0], 'train samples') print(x_test.shape[0], 'test samples') # convert class vectors to binary class matrices y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) model = Sequential() model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape)) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(32, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(num_classes, activation='softmax')) model.compile( loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adadelta(), metrics=['accuracy']) model.fit( x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_data=(x_test, y_test)) score = model.evaluate(x_test, y_test, verbose=0) print('Test loss:', score[0]) print('Test accuracy:', score[1]) # print model layers model.summary() # save model model.save('mnist_v1.h5')

Functional Check

Все сабмиты перед запуском проходят функциональную проверку. Во время неё сабмит запускается и получает корректные и некорректные запросы. А ответы на них проверяются. Содержимое STDOUT и STDERR доступно Вам для отладки.

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

Limits

Architecture: x86_64.

OS: Linux.

Python version: 3.6 or higher.

Максимальный размер сабмита: 4Mb.

GPU может отсуствовать.

Время запуска (от старта run.sh до начала приёма запросов) не более 60 секунд.

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

OS memory: 2Gb.

CPU: 1 core of modern CPU.

GPU max memory: 2Gb.

Disk space: 1Gb.

Rules

Один участник соревнования - один аккаунт.

Не более 5 сабмитов за 24 часа. Учитываются только сабмиты, отработавшие до конца без ошибок и получившие score.

При запуске сабмита интернет не должен использоваться.

Не использовать лики, баги, эксплойты. Уведомлять обо всём подобном организаторов соревнования.

Timeline

Соревнование проводится постоянно и не имеет сроков окончания.

Prizes

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

Обратите внимание на лучшие решениям прошлых годов.

Convolutional neural network on Keras: keras.io/examples/mnist_cnn/;

Convolutional neural network on Pytorch: github.com/pytorch/examples/blob/master/mnist/main.py;

Pytorch knowledge distillation: github.com/karanchahal/distiller.

If you find a useful link, contact us and we will add it.

Discussion

Telegramm: Ryxi Group

ODS каналы #theory_and_practice, #cv, #deep_learning.

Skype: monashev

Telegramm: monashev

ODS: monashev

FAQ

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