MNIST with submit size limitation

Classify photos of handwritten digits.

Description
Dataset
Metric
Evaluation
Baseline
Functional Check
Limits
Rules
Timeline
Prizes
Links
Discussion
Contacts
FAQ

Description

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

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

Профессионалам, надеюсь, будет интересно побороться за минимальный размер сабмита и попробовать AutoML, 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 responce. 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 responce
            # 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 сабмитов в сутки.

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

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

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;
AutoML system based on Keras: autokeras.com;
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

Contacts

Skype: monashev
Telegramm: monashev
ODS: monashev

FAQ

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