Нейрокурятник: часть 4 — итоговая модель и код на прод +38


image

Типичный день в нейрокурятнике — куры часто еще и крутятся в гнезде

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

  • Точность предсказаний не менее 70-90%;
  • Raspberry pi в самом курятнике в идеале мог бы определять принадлежности фотографий к классам;
  • Нужно как минимум научиться отличать всех кур друг от друга. Программа максимум — также научиться считать яйца;

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

Статьи про нейрокурятник

Заголовок спойлера
  1. Вступление про обучение себя нейросетям
  2. Железо, софт и конфиг для наблюдения за курами
  3. Бот, который постит события из жизни кур — без нейросети
  4. Разметка датасетов
  5. Работающая модель для распознавания кур в курятнике
  6. Итог — работающий бот, распознающий кур в курятнике

0. TL;DR


Для самых нетерпеливых:

  • Точность получилась в итоге порядка 80%;
  • Датасет можно скачать тут;
  • ipynb jupyter notebook'а со всеми выкладками и boiler-plate код с пояснениями можно скачать тут, html версию тут;
  • Скрипты для демонизации и деплоя на прод — тут (ipynb);

1. Прелюдии


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


Из современных инструментов, которые адекватно подходят этой цели в голову приходят только сверточные нейросети. Поковырявшись (по ссылке рекурсивная статья про нейросети, если вы хотите научиться) весьма основательное время в нейросетях, поучаствовав в нескольких соревнованиях (к сожалению уже закрытых) на Kaggle (из открытых заинтересовали морские котики, но там сложновато) и тем самым по большей части пройдя курс от fast.ai, я обрисовал для себя примерно такой план:

  1. Разметить небольшой датасет (а сегодня реально не нужно много данных, чтобы построить классификатор);
  2. Использовать keras для тестирования максимально большого числа архитектур в течение максимум 1 дня;
  3. Если получится использовать неразмеченные фотографии, чтобы увеличить точность (semi-supervised подход);
  4. Попытаться сделать визуализацию активаций внутренних слоев нейросети;

2. Посмотрим, что получилось!


Layer (type)                 Output Shape              Param #   
=================================================================
batch_normalization_1 (Batch (None, 700, 400, 3)       2800      
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 698, 398, 32)      896       
_________________________________________________________________
batch_normalization_2 (Batch (None, 698, 398, 32)      2792      
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 232, 132, 32)      0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 230, 130, 64)      18496     
_________________________________________________________________
batch_normalization_3 (Batch (None, 230, 130, 64)      920       
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 76, 43, 64)        0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 209152)            0         
_________________________________________________________________
dense_1 (Dense)              (None, 200)               41830600  
_________________________________________________________________
batch_normalization_4 (Batch (None, 200)               800       
_________________________________________________________________
dense_2 (Dense)              (None, 8)                 1608      
=================================================================
Total params: 41,858,912
Trainable params: 41,855,256
Non-trainable params: 3,656
_________________________________________________________________

Итоговая архитектура модели in a nutshell, которая дала порядка 80% точности

Для начала давайте разберемся, что же творится в курятнике. Чтобы успешно сделать прикладной проект, всегда нужно пытаться контролировать максимальную часть технологической цепочки. Вряд ли получится сделать проект, если данные поступают в виде «вот вам 10 терабайт видео в ужасном качестве, сделайте нам модель распознавания видео в режиме реального времени за 10,000 рублей » или «вот вам 50 фото этикеток от пива» (юмор, но это примеры, основанные на реальных… проектах, слава богу не моих).

  • Куры постоянно залазят в гнездо, чтобы просто посидеть или из любопытства, особенно молодые;
  • Молодые куры (белые на фото выше) — до определенного момента не откладывали яйца в гнездо, а просто ковыряли чужие яйца;
  • У кур бывают стычки, но в 95% случаев соблюдаются правила: i) в гнезде 1 курица ii) за время пребывания курицы в гнезде делается 10-20 уникальных фоток iii) курица как правило польностью закрывает яйца собой iv) текущий вид на куриц является продуктом нескольких итераций поиска оптимального расположения камеры;
  • В день делается порядка 300-400 фото, откладывается 5-8 яиц (молодые куры по глупости и боязни неслись в прямом смысле слова под пол, потом после того как было найдено скопище из 22 яиц под полом, они стали нестись в гнездо);
  • На ночь курам выключают свет — они ложатся спать;
  • Было размечено порядка 900 фото;

Давайте посмотрим сколько фото получилось для каждого класса:
image

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

  • Правильно выбрать алгоритмы предобработки данных (логично, что куры крутятся в гнезде, поэтому можно вращать фотографии хоть на 180 градусов!);
  • Выбрать адекватную по размеру и сложности модель;

С предобработкой просто — нужно просто взять простейшую линейную модель и попробовать 5-10 сочетаний параметров — при достаточном количестве прогрммного сахара в Keras это делается подстановкой параметров для такого небольшого датасета в течение часа (тренировка 10 эпох модели занимает по 50-100 секунд на 1 эпоху). Не в последнюю очередь это также делается с целью регуляризации. Вот пример одной из оптимальных методик:

genImage = imageGeneratorSugar(
    featurewise_center = False,
    samplewise_center = False,
    featurewise_std_normalization = False,
    samplewise_std_normalization = False,
    rotation_range = 90,
    width_shift_range = 0.05,
    height_shift_range = 0.05,
    shear_range = 0.2,
    zoom_range = 0.2,
    fill_mode='constant',
    cval=0.,
    horizontal_flip=False,
    vertical_flip=False)

Вы можете посмотреть почти полный лог экспериментов тут:

  • ipynb jupyter notebook'а со всеми выкладками и boiler-plate код с пояснениями можно скачать тут, html версию тут;

Что касается выбора самой модели, то опробовав разные архитектуры на ряде датасетов, остановился примерно на такой модели (забыл Dropout — но тесты с ним не дали прироста точности):

def getTestModelNormalize(inputShapeTuple, classNumber):
    model = Sequential([
            BatchNormalization(axis=1, input_shape = inputShapeTuple),
            Convolution2D(32, (3,3), activation='relu'),
            BatchNormalization(axis=1),
            MaxPooling2D((3,3)),
            Convolution2D(64, (3,3), activation='relu'),
            BatchNormalization(axis=1),
            MaxPooling2D((3,3)),
            Flatten(),
            Dense(200, activation='relu'),
            BatchNormalization(),
            Dense(classNumber, activation='softmax')
        ])
    
    model.compile(Adam(lr=1e-4), loss='categorical_crossentropy', metrics=['accuracy'])
    return model

Вот ее графическое представление:

image

Поясню состав слоев и почему в принципе почти любая современная CNN выглядит очень похоже:

  1. BatchNormalization — усредняет входящие данные (вычитает среднее и делит на стандартное отклонение), что ускоряет тренировку нейросети в десятки раз. Если интересно почему — то уходите в рекурсию тут или тут. Обязателен к применению;
  2. Convolution2D — сверточные слои. По сути представляют собой фильтры, чтобы нейросеть смогла научиться распознавать абстрактные образы. По идее визуализация должна была быть включена в эту статью, но у я не осилил в итоге (если вы не знаете как выглядят изнутри нейросети, то вот ссылки для вас 1 2 3;
  3. MaxPooling2D — усреднение значений фильтров. Обязателен после сверточных слоев;
  4. Dropout — по сути нужен для регуляризации. В эту спецификацию модели не включил его, потому что брал код из другого своего проекта и просто забыл из-за высокой точности модели;
  5. Flatten — чтобы потом вставить dense слой, иначе не получится;

Спецификация модели с dropout выглядит вот так (но почему-то он не дал прироста точности на первый взгляд как я потом не извращался, вероятно регуляризация и так была достигнута предобработкой фото + фотки большие).

# let's try a model w dropout! 
def getTestModelNormalizeDropout(inputShapeTuple, classNumber):
    model = Sequential([
            BatchNormalization(axis=1, input_shape = inputShapeTuple),
            Convolution2D(32, (3,3), activation='relu'),
            BatchNormalization(axis=1),
            Dropout(rate=0.3),
            MaxPooling2D((3,3)),
            Convolution2D(64, (3,3), activation='relu'),
            BatchNormalization(axis=1),
            Dropout(rate=0.1),       
            MaxPooling2D((3,3)),
            Flatten(),
            Dense(200, activation='relu'),
            BatchNormalization(),
            Dense(classNumber, activation='softmax')
        ])
    
    model.compile(Adam(lr=1e-4), loss='categorical_crossentropy', metrics=['accuracy'])
    return model

Обратите внимание, что используется categorical_crossentropy вместе с softmax. В качестве оптимизатора используется adam. Не буду останавливаться почему я выбрал именно их (такое сочетание по сути стандарт), но вы можете почитать тут и посмотреть тут 1, 2 и 3.

Сам процесс поиска оптимальной модели выглядит так:

  1. Пробуем разные способы предобработки данных с простейшей моделью (внезапно веса простейшей dense модели из 3 слоев весят несколько гигабайт для больших фото, что не очень хорошо) и выбираем оптимальный;
  2. Пробуем разные архитектуры сверточных (или каких-либо других) нейросетей, выбираем оптимальную;
  3. При достижении какого-либо значимого бенчмарка (50%+ точности, 75% точности — выбирайте от задачи, чем больше классов, тем более мягким должен бенчмарк) — нужно проанализовать, с предсказанием каких фото и каких классов у модели возникают проблемы;
  4. Ансамбли и fine-tuning внешних слоев у больших архитектур нейросетей — опционально — если хотите выиграть конкурс;
  5. Можно попробовать подмешать в свою выборку 20-30% фото из тестовой, валидационной или просто неразмеченной части датасета (semi-supervised, код по идее работает, но у меня именно в этой задаче постоянно умирали kernel-ы, я забил);
  6. Повторять до бесконечности;

42/42 [==============================] - 87s - loss: 2.4055 - acc: 0.2783 - val_loss: 4.7899 - val_acc: 0.1771
Epoch 2/15
42/42 [==============================] - 90s - loss: 1.7039 - acc: 0.4049 - val_loss: 2.4489 - val_acc: 0.2011
Epoch 3/15
42/42 [==============================] - 90s - loss: 1.4435 - acc: 0.4827 - val_loss: 2.1080 - val_acc: 0.2402
Epoch 4/15
42/42 [==============================] - 90s - loss: 1.2525 - acc: 0.5311 - val_loss: 2.4556 - val_acc: 0.2179
Epoch 5/15
42/42 [==============================] - 85s - loss: 1.2024 - acc: 0.5549 - val_loss: 2.2180 - val_acc: 0.1955
Epoch 6/15
42/42 [==============================] - 84s - loss: 1.0820 - acc: 0.5858 - val_loss: 1.8620 - val_acc: 0.2849
Epoch 7/15
42/42 [==============================] - 84s - loss: 0.9475 - acc: 0.6535 - val_loss: 2.1256 - val_acc: 0.1955
Epoch 8/15
42/42 [==============================] - 84s - loss: 0.9283 - acc: 0.6665 - val_loss: 1.2578 - val_acc: 0.5642
Epoch 9/15
42/42 [==============================] - 84s - loss: 0.9238 - acc: 0.6792 - val_loss: 1.1639 - val_acc: 0.5698
Epoch 10/15
42/42 [==============================] - 84s - loss: 0.8451 - acc: 0.6963 - val_loss: 1.4899 - val_acc: 0.4581
Epoch 11/15
42/42 [==============================] - 84s - loss: 0.8026 - acc: 0.7183 - val_loss: 0.9561 - val_acc: 0.6480
Epoch 12/15
42/42 [==============================] - 84s - loss: 0.8353 - acc: 0.7064 - val_loss: 1.0533 - val_acc: 0.6145
Epoch 13/15
42/42 [==============================] - 84s - loss: 0.7687 - acc: 0.7380 - val_loss: 0.9039 - val_acc: 0.6760
Epoch 14/15
42/42 [==============================] - 84s - loss: 0.7683 - acc: 0.7287 - val_loss: 1.0038 - val_acc: 0.6704
Epoch 15/15
42/42 [==============================] - 84s - loss: 0.7076 - acc: 0.7451 - val_loss: 0.8953 - val_acc: 0.7039

А так выглядит процесс гипнотизации прогресс индикатора...

Кстати этот сниппет кода поможет вам, если вы захотите сделать semi-supervised модель в keras

# Mix iterator class for pseudo-labelling
class MixIterator(object):
    def __init__(self, iters):
        self.iters = iters
        self.multi = type(iters) is list
        if self.multi:
            self.N = sum([it[0].N for it in self.iters])
        else:
            self.N = sum([it.N for it in self.iters])

    def reset(self):
        for it in self.iters: it.reset()

    def __iter__(self):
        return self

    def next(self, *args, **kwargs):
        if self.multi:
            nexts = [[next(it) for it in o] for o in self.iters]
            n0s = np.concatenate([n[0] for n in o])
            n1s = np.concatenate([n[1] for n in o])
            return (n0, n1)
        else:
            nexts = [next(it) for it in self.iters]
            n0 = np.concatenate([n[0] for n in nexts])
            n1 = np.concatenate([n[1] for n in nexts])
            return (n0, n1)

mi = MixIterator([batches, test_batches, val_batches)
bn_model.fit_generator(mi, mi.N, nb_epoch=8, validation_data=(conv_val_feat, val_labels))

Но если вернуться к нашим курам, то после шага 3, проанализировав итоги модели, я увидел забавную закономерность.

Вот соотношение правильных предсказаний к числу фото
image

А вот основной источник неверных классификаций

image

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

Поместив все яйца в один класс в итоге я и получил модель примерно с 80% точности на небольшом датасете.

3. Что пойдет на прод?


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

Весь код такого боевого деплоя выглядит примерно так (тут используется tensor flow в качестве бекенда).

Вот в принципе и все.

# dependencies
import numpy as np
import keras.models
from keras.models import model_from_json
from scipy.misc import imread, imresize,imshow
import tensorflow as tf?
?
In [3]:

def init(model_file,weights_file): 
    json_file = open(model_file,'r')
    loaded_model_json = json_file.read()
    json_file.close()
    loaded_model = model_from_json(loaded_model_json)
    #load woeights into new model
    loaded_model.load_weights(weights_file)
    print("Loaded Model from disk")
    #compile and evaluate loaded model
    loaded_model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
    #loss,accuracy = model.evaluate(X_test,y_test)
    #print('loss:', loss)
    #print('accuracy:', accuracy)
    graph = tf.get_default_graph()
?
    return loaded_model,graph
?
loaded_model,graph = init('model.json','model7_20_epochs.h5')
?
In [4]:

def predict(img_file, model):
    # here you should read the image
    img = imread(img_file,mode='RGB')
    img = imresize(img,(400,800))
    #convert to a 4D tensor to feed into our model
    img = img.reshape(1,400,800,3)
    # print ("debug2")
    #in our computation graph
    with graph.as_default():
        #perform the prediction
        out = model.predict(img)
        print(out)
        print(np.argmax(out,axis=1))
        # print ("debug3")
        #convert the response to a string
        response = (np.argmax(out,axis=1))
        return response
In [6]:

chick_dict = {0: 'back_spot',
 1: 'beauty',
 2: 'dirty',
 3: 'egg',
 4: 'empty',
 5: 'light_back',
 6: 'ordinary',
 7: 'psycho',
 8: 'red_star',
 9: 'red_twin',
 10: 'young_long'}
?
In [7]:

prediction = predict('test.jpg',loaded_model)
print (chick_dict[prediction[0]])
[[  2.34186242e-04   5.02209296e-04   5.61403576e-04   9.51264706e-03
    2.03147720e-04   1.70257801e-04   4.71635815e-03   5.06504579e-03
    1.84403792e-01   7.92831838e-01   1.79908809e-03]]

Out [9]
red_twin
In [11]:

import matplotlib.pyplot as plt
img = imread('test.jpg',mode='RGB')
plt.imshow(img)
plt.show()

Модель предсказывает верную курицу ) Ждите финального бота, который постит верных кур.
-->


К сожалению, не доступен сервер mySQL