Leaderboard Register and Join the Competition

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

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

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

Contacts

Skype: monashev
Telegramm: monashev
ODS: monashev

FAQ

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