Типичный день в нейрокурятнике — куры часто еще и крутятся в гнезде
Чтобы довести, наконец, проект нейрокурятника до своего логического завершения, нужно произвести на свет работающую модель и задеплоить ее на продакшен, да еще и так, чтобы соблюдался ряд условий:
- Точность предсказаний не менее 70-90%;
- Raspberry pi в самом курятнике в идеале мог бы определять принадлежности фотографий к классам;
- Нужно как минимум научиться отличать всех кур друг от друга. Программа максимум — также научиться считать яйца;
В данной статье мы расскажем что же в итоге у нас получилось, какие модели мы попробовали и какие занятные вещи нам попались на дороге.
Статьи про нейрокурятник
Заголовок спойлера
- Вступление про обучение себя нейросетям
- Железо, софт и конфиг для наблюдения за курами
- Бот, который постит события из жизни кур — без нейросети
- Разметка датасетов
- Работающая модель для распознавания кур в курятнике
- Итог — работающий бот, распознающий кур в курятнике
0. TL;DR
Для самых нетерпеливых:
- Точность получилась в итоге порядка 80%;
- Датасет можно скачать тут;
- ipynb jupyter notebook'а со всеми выкладками и boiler-plate код с пояснениями можно скачать тут, html версию тут;
- Скрипты для демонизации и деплоя на прод — тут (ipynb);
1. Прелюдии
Получилось также весьма кстати, потому что буквально несколько дней назад вышло это видео, под которым есть весьма полезная копипаста.
Из современных инструментов, которые адекватно подходят этой цели в голову приходят только сверточные нейросети. Поковырявшись (по ссылке рекурсивная статья про нейросети, если вы хотите научиться) весьма основательное время в нейросетях, поучаствовав в нескольких соревнованиях (к сожалению уже закрытых) на Kaggle (из открытых заинтересовали морские котики, но там сложновато) и тем самым по большей части пройдя курс от fast.ai, я обрисовал для себя примерно такой план:
- Разметить небольшой датасет (а сегодня реально не нужно много данных, чтобы построить классификатор);
- Использовать keras для тестирования максимально большого числа архитектур в течение максимум 1 дня;
- Если получится использовать неразмеченные фотографии, чтобы увеличить точность (semi-supervised подход);
- Попытаться сделать визуализацию активаций внутренних слоев нейросети;
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 фото;
Давайте посмотрим сколько фото получилось для каждого класса:
Изначально увидев такой расклад, я подумал, что получится даже отличать фотографии с количеством яиц, но нет. Чтобы максимизировать точность на такой небольшой выборке, надо постараться сделать несколько вещей:
- Правильно выбрать алгоритмы предобработки данных (логично, что куры крутятся в гнезде, поэтому можно вращать фотографии хоть на 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
Вот ее графическое представление:
Поясню состав слоев и почему в принципе почти любая современная CNN выглядит очень похоже:
- BatchNormalization — усредняет входящие данные (вычитает среднее и делит на стандартное отклонение), что ускоряет тренировку нейросети в десятки раз. Если интересно почему — то уходите в рекурсию тут или тут. Обязателен к применению;
- Convolution2D — сверточные слои. По сути представляют собой фильтры, чтобы нейросеть смогла научиться распознавать абстрактные образы. По идее визуализация должна была быть включена в эту статью, но у я не осилил в итоге (если вы не знаете как выглядят изнутри нейросети, то вот ссылки для вас 1 2 3;
- MaxPooling2D — усреднение значений фильтров. Обязателен после сверточных слоев;
- Dropout — по сути нужен для регуляризации. В эту спецификацию модели не включил его, потому что брал код из другого своего проекта и просто забыл из-за высокой точности модели;
- 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.
Сам процесс поиска оптимальной модели выглядит так:
- Пробуем разные способы предобработки данных с простейшей моделью (внезапно веса простейшей dense модели из 3 слоев весят несколько гигабайт для больших фото, что не очень хорошо) и выбираем оптимальный;
- Пробуем разные архитектуры сверточных (или каких-либо других) нейросетей, выбираем оптимальную;
- При достижении какого-либо значимого бенчмарка (50%+ точности, 75% точности — выбирайте от задачи, чем больше классов, тем более мягким должен бенчмарк) — нужно проанализовать, с предсказанием каких фото и каких классов у модели возникают проблемы;
- Ансамбли и fine-tuning внешних слоев у больших архитектур нейросетей — опционально — если хотите выиграть конкурс;
- Можно попробовать подмешать в свою выборку 20-30% фото из тестовой, валидационной или просто неразмеченной части датасета (semi-supervised, код по идее работает, но у меня именно в этой задаче постоянно умирали kernel-ы, я забил);
- Повторять до бесконечности;
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, проанализировав итоги модели, я увидел забавную закономерность.
Вот соотношение правильных предсказаний к числу фото
А вот основной источник неверных классификаций
Как ни странно, на такой небольшой выборке оказалось, что нейросеть хорошо отделяет кур от яиц, но яйца считает с трудом. Я смог построить отдельную модель, которая считала яйца с 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()
Модель предсказывает верную курицу ) Ждите финального бота, который постит верных кур.