Створюємо нейронну мережу InceptionV3 для розпізнавання зображень



Привіт, Хабр! Під катом піде мова про реалізацію сверточной нейронної мережі архітектури InceptionV3 з використанням фреймворку Keras. Статтю я вирішив написати після ознайомлення з туториалом "Побудова потужних моделей класифікації з використанням невеликої кількості даних". З схвалення автора туториала я трохи змінив зміст своєї статті. На відміну від запропонованої автором нейронної мережі VGG16, ми будемо навчати гугловскую глибоку нейронну мережу Inception V3, яка вже встановлена в Keras.

Ви навчитеся:

  1. Імпортувати нейронну мережу Inception V3 з бібліотеки Keras;
  2. Налаштовувати мережу: завантажувати ваги, змінювати верхню частину моделі (fc-layers), таким чином, пристосовуючи модель під бінарну класифікацію;
  3. Проводити тонке налаштування нижнього згорткового шару нейронної мережі;
  4. Застосовувати аугментацію даних за допомогою ImageDataGenerator;
  5. Навчати мережу по частинах для економії ресурсів і часу;
  6. Оцінювати роботу моделі.
При написанні статті я ставив перед собою завдання представити максимально практичний матеріал, який розкриє деякі цікаві можливості фреймворка Keras.

Останнім часом все частіше з'являються туторіали, присвячені створенню та застосуванню нейронних мереж. З великим задоволенням спостерігаю за цікавою тенденцією: нові пости стають все більш і більш зрозумілими для неспеціалістів в області програмування. Деякі автори і зовсім роблять спроби введення читачів в цю тему, застосовуючи максимально природний мову. Є також відмінні статті (напр. 1, 2, 3), які поєднують розумне кількість теорії і практики, що дозволяє якомога швидше зрозуміти необхідний мінімум і почати створювати щось своє.

Отже, за діло!

Насамперед, трохи про бібліотеки:Рекомендую встановити платформу Anaconda. Я використовував Python 2.7. Для роботи зручно використовувати Jupyter notebook. Він вже встановлений на Anaconda. Нам також знадобиться встановити фреймворк Keras. Як бекенду я використовував Theano. Ви можете використовувати Tensorflow, тому що Keras підтримує їх обидва. Установка CUDA для Theano на Windows описана здесь.
1. Дані:
У нашому прикладі ми будемо використовувати зображення з змагання з машинного навчання під назвою "Dogs vs Cats" kaggle.com. Дані будуть доступними після реєстрації. Набір включає в себе 25000 зображень: 12500 кішок і 12500 собак. Клас 1 відповідає собакам, клас 0 — кішкам. Після завантаження архівів, додайте 1000 зображень кожного класу в директоріях наступним чином:

data/
train/
dogs/
dog001.jpg
dog002.jpg
...
cats/
cat001.jpg
cat002.jpg
...
validation/
dogs/
dog001.jpg
dog002.jpg
...
cats/
cat001.jpg
cat002.jpg
...

Вам нічого не заважає користуватися всім набором даних. Я, також як і автор оригінальної статті, вирішив використовувати обмежену вибірку, щоб перевірити ефективність роботи мережі з малим набором зображень.

У нас є три проблеми:

  1. Обмежений обсяг даних;
  2. Обмежені системні ресурси (у мене, наприклад, Intel Core i5-4440 3.10 GHz, 8 Гб оперативної пам'яті, NVIDIA GeForce GTX 745);
  3. Обмежений час: ми хочемо навчати модель менше доби.

При обмеженому обсязі зображення висока ймовірність переобучения. Для боротьби з цим потрібно:

  1. Встановити великий Dropout. В нашому випадку він буде 0.5;
  2. Будемо використовувати аугментацію даних. Цей прийом дозволить нам збільшити кількість зображень шляхом різних трансформацій (в нашому випадку будуть зміни масштабу, зрушення, горизонтальне відображення).
  3. Для нашого експерименту ми візьмемо глибоку мережу.
Останній пункт повинен нас насторожувати, бо глибока нейронна мережа вимоглива до ресурсів. Я на своїй відеокарті не зміг навчити навіть VGG16, не кажучи вже про такий гигантище, як Inception. Тим не менш, рішення є:

  1. Спочатку ми використовуємо модель, навчену на великій кількості зображень з бази imagenet. Благо, що набір зображень включав кішок і собак;
  2. Будемо навчати модель по частинах:
  3. Спочатку проженемо аугментированные зображення через нижню частину мережі (тільки через Inception) і збережемо їх у вигляді numpy масивів;
  4. Навчимо з допомогою отриманих numpy масивів верхній полносвязный шар;
  5. Потім, ми об'єднаємо верхній і нижній шари моделі і проведемо тонке налаштування нової моделі, але при цьому ми заблокуємо від навчання у Inception всі шари, крім останнього.
Єдиним вирішенням проблеми з часом я поки що бачу застосування паралельних обчислень. Вам для цього потрібна відеокарта з підтримкою CUDA. Я сподіваюся, що установка CUDA для Python не викличе у вас сильних ускладнень.

Імпортуємо бібліотеки:

from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential, Model
from keras.applications.inception_v3 import InceptionV3
from keras.callbacks import ModelCheckpoint
from keras.optimizers import SGD

from keras import backend as K
K. set_image_dim_ordering('th')

import numpy as np
import pandas as pd
import h5py

import matplotlib.pyplot as plt

2. Створимо модель InceptionV3, завантажимо в неї зображення, збережемо їх:
Велика картинка зі схемою наших дій

бібліотеці Keras знаходиться кілька підготовлених нейронних мереж.

Список аргументів моделіinclude_top: включати або не включати верхню частину мережі, яка являє полносвязный шар з 1000 виходів.
Нам він без потреби, тому ставимо: include_top=False;

weights: завантажувати/не завантажувати навчені ваги. Якщо None, ваги ініціалізуються випадково. Якщо «imagenet», будуть завантажені ваги, треновані на даних ImageNet.
В нашій моделі ваги потрібні, тому weights=«imagenet»;

input_tensor: цей аргумент зручний, якщо ми використовуємо шар Input для нашої моделі.
Ми його не будемо чіпати.

input_shape : в цьому аргументі ми задаємо розмір нашого зображення. Його вказують, якщо від'єднується верхній шар (include_top=False). Якщо ми завантажили модель з верхнім шаром, сто розмір зображення повинен бути тільки (3, 299, 299).
Ми прибрали верхній шар і хочемо аналізувати зображення меншого розміру (3, 150, 150). Тому кажем: input_shape=()

Створюємо нашу модель:

inc_model=InceptionV3(include_top=False, 
weights='imagenet', 
input_shape=((3, 150, 150)))

Тепер зробимо аугментацію даних. Для цього в Keras передбачені так звані ImageDataGenerator. Вони будуть брати зображення прямо з папок і проводити над ними всі необхідні трансформації.

Картинки кожного класу повинні бути в окремих папках. Для того, щоб нам не завантажувати оперативну пам'ять зображеннями, ми трансформуємо їх прямо перед завантаженням в мережу. Для цього використовуємо метод .flow_from_directory. Створимо окремі генератори для тренувальних і тестових зображень:

bottleneck_datagen = ImageDataGenerator(rescale=1./255) #власне, генератор

train_generator = bottleneck_datagen.flow_from_directory('data/img_train/',
target_size=(150, 150),
batch_size=32,
class_mode=None,
shuffle=False)

validation_generator = bottleneck_datagen.flow_from_directory('data/img_val/',
target_size=(150, 150),
batch_size=32,
class_mode=None,
shuffle=False

Хочу виділити важливий момент. Ми вказали shuffle=False. Тобто, зображення з різних класів не будуть перемішуватися. Спочатку надходитимуть зображення з першої папки, а коли вони всі закінчаться, з другої. Навіщо це потрібно, побачите пізніше.

Проженемо аугментированные зображення через навчену Inception і збережемо висновок у вигляді numpy масивів:

bottleneck_features_train = inc_model.predict_generator(train_generator, 2000)
np.save(open('bottleneck_features/bn_features_train.npy', 'wb'), bottleneck_features_train)

bottleneck_features_validation = inc_model.predict_generator(validation_generator, 2000)
np.save(open('bottleneck_features/bn_features_validation.npy', 'wb'), bottleneck_features_validation)

Процес займе деякий час.

3. Створимо верхню частину моделі, завантажимо в неї дані, збережемо їх:
Схема

В оригінальному пості автор використав один шар мережі з 256 нейронами, однак я буду використовувати два шари по 64 нейрона в кожному і Dropout шар зі значенням 0.5. Це зміна я був змушений внести з-за того, коли я навчав готову модель (яку ми зробимо в наступному кроці), мій комп'ютер зависав і перезавантажувався.

Завантажимо масиви:

train_data = np.load(open('bottleneck_features_and_weights/bn_features_train.npy', 'rb'))
train_labels = np.array([0] * 1000 + [1] * 1000) 

validation_data = np.load(open('bottleneck_features_and_weights/bn_features_validation.npy', 'rb'))
validation_labels = np.array([0] * 1000 + [1] * 1000)

Зверніть увагу, раніше ми вказували shuffle=False. А тепер ми легко вкажемо labels. Оскільки в кожному класі у нас 2000 зображень і всі зображення надходили по черзі, у нас буде по 1000 нулів і 1000 одиниць для тренувальної і для тестової вибірок.

Створимо модель FFN мережі, скомпилируем її:

fc_model = Sequential()
fc_model.add(Flatten(input_shape=train_data.shape[1:]))
fc_model.add(Dense(64, activation='relu', name='dense_one'))
fc_model.add(Dropout(0.5, name='dropout_one'))
fc_model.add(Dense(64, activation='relu', name='dense_two'))
fc_model.add(Dropout(0.5, name='dropout_two'))
fc_model.add(Dense(1, activation='sigmoid', name='output'))

fc_model.compile(optimizer='rmsprop', 
loss='binary_crossentropy', 
metrics=['accuracy'])

Завантажимо в неї наші масиви:

fc_model.fit(train_data, train_labels,
nb_epoch=50, batch_size=32,
validation_data=(validation_data, validation_labels))

fc_model.save_weights('bottleneck_features_and_weights/fc_inception_cats_dogs_250.hdf5') # зберігаємо ваги

Тепер ми завантажуємо дані не з папок, тому використовуємо звичайний метод fit.

Процес навчання буде непристойно швидким. У мене кожна епоха посіла 1 с:
Train on 2000 samples, validate on 2000 samples
Epoch 1/50
2000/2000 [==============================] - 1s - loss: 2.4588 - acc: 0.8025 - val_loss: 0.7950 - val_acc: 0.9375
Epoch 2/50
2000/2000 [==============================] - 1s - loss: 1.3332 - acc: 0.8870 - val_loss: 0.9330 - val_acc: 0.9160


...
Epoch 48/50
2000/2000 [==============================] - 1s - loss: 0.1096 - acc: 0.9880 - val_loss: 0.5496 - val_acc: 0.9595
Epoch 49/50
2000/2000 [==============================] - 1s - loss: 0.1100 - acc: 0.9875 - val_loss: 0.5600 - val_acc: 0.9560
Epoch 50/50
2000/2000 [==============================] - 1s - loss: 0.0850 - acc: 0.9895 - val_loss: 0.5674 - val_acc: 0.9565


Оцінимо точність моделі:

fc_model.evaluate(validation_data, validation_labels)

[0.56735104312408047, 0.95650000000000002]

Наша модель задовільно справляється зі своїм завданням. Але вона приймає тільки numpy масиви. Це нас не влаштовує. Для того, щоб отримати повноцінну модель, яка приймає на вхід зображення, ми з'єднаємо дві наші моделі і навчимо їх ще раз.

4. Створимо підсумкову модель, завантажимо в неї аугментированные дані, збережемо ваги:
Схема

weights_filename='bottleneck_features_and_weights/fc_inception_cats_dogs_250.hdf5'

x = Flatten()(inc_model.output)
x = Dense(64, activation='relu', name='dense_one')(x)
x = Dropout(0.5, name='dropout_one')(x)
x = Dense(64, activation='relu', name='dense_two')(x)
x = Dropout(0.5, name='dropout_two')(x)
top_model=Dense(1, activation='sigmoid', name='output'(x)

Завантажимо у неї ваги:

weights_filename='bottleneck_features_and_weights/fc_inception_cats_dogs_250.hdf5'
model.load_weights(weights_filename, by_name=True)

Скажу чесно, я не помітив ніякої різниці між ефективністю навчання моделі з завантаженням ваг або без неї. Але залишив цей розділ, тому що він описує, як завантажити ваги в певні шари по імені (by_name=True).

Заблокуємо з 1 по 205 шари Inception:

layer for in inc_model.layers[:205]:
layer.trainable = False

Скомпилируем модель:

model.compile(loss='binary_crossentropy',
optimizer=SGD(lr=1e-4, momentum=0.9),
#optimizer='rmsprop',
metrics=['accuracy'])

Зверніть увагу, коли ми в перший раз навчали полносвязные шари .npy масивів, ми використовували оптимізатор RMSprop. Тепер, для тонкої настройки моделі ми використовуємо Стохастичний градієнтний спуск. Зроблено це для того, щоб запобігти надто виражені оновлення вже навчених ваг.

Зробимо так, щоб в процесі навчання зберігалися тільки ваги з найбільшою точністю на тестовій вибірці:

<source lang="python">filepath="new_model_weights/weights-improvement-{epoch:02d}-{val_acc:.2f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max')
callbacks_list = [checkpoint]

Створимо нові генератори зображень для навчання повної моделі. Ми будемо трансформувати тільки навчальну вибірку. Тестову чіпати не будемо.

train_datagen = ImageDataGenerator(
rescale=1./255,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
'data/img_train/',
target_size=(150, 150),
batch_size=32,
class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
'data/img_val/',
target_size=(150, 150),
batch_size=32,
class_mode='binary')


pred_generator=test_datagen.flow_from_directory('data/img_val/',
target_size=(150,150),
batch_size=100,
class_mode='binary')

pred_generator ми використовуємо пізніше для демонстрації роботи моделі.

Завантажимо зображення в модель:

model.fit_generator(
train_generator,
samples_per_epoch=2000,
nb_epoch=200,
validation_data=validation_generator,
nb_val_samples=2000,
callbacks=callbacks_list)

Чуємо шум кулера і чекаємо...
Epoch 1/200
1984/2000 [============================>.] - ETA: 0s - loss: 1.0814 - acc: 0.5640 Epoch 00000: val_acc improved from -to inf 0.71750, saving model to new_model_weights/weights-improvement-00-0.72.hdf5
2000/2000 [==============================] - 224s - loss: 1.0814 - acc: 0.5640 - val_loss: 0.6016 - val_acc: 0.7175
Epoch 2/200
1984/2000 [============================>.] - ETA: 0s - loss: 0.8523 - acc: 0.6240 Epoch 00001: val_acc improved from 0.71750 to 0.77200, saving model to new_model_weights/weights-improvement-01-0.77.hdf5
2000/2000 [==============================] - 215s - loss: 0.8511 - acc: 0.6240 - val_loss: 0.5403 - val_acc: 0.7720


... 

Epoch 199/200
1968/2000 [============================>.] - ETA: 1s - loss: 0.1439 - acc: 0.9385 Epoch 00008: val_acc improved from 0.90650 to 0.91500, saving model to new_model_weights/weights-improvement-08-0.92.hdf5
2000/2000 [==============================] - 207s - loss: 0.1438 - acc: 0.9385 - val_loss: 0.2786 - val_acc: 0.9150
Epoch 200/200
1968/2000 [============================>.] - ETA: 1s - loss: 0.1444 - acc: 0.9350 Epoch 00009: val_acc did not improve
2000/2000 [==============================] - 206s - loss: 0.1438 - acc: 0.9355 - val_loss: 0.3898 - val_acc: 0.8940


У мене на кожну епоху йшло 210-220 секунд. 200 епох навчання зайняли близько 12 годин.

5. Оцінимо точність моделі
model.evaluate_generator(pred_generator, val_samples=100)

[0.2364250123500824, 0.9100000262260437]

Ось нам і згодився pred_generator. Зверніть увагу, val_samples повинні відповідати величині batch_size в генераторі!

Точність 91,7%. Враховуючи обмеженість вибірки, візьму на себе сміливість сказати, що це непогана точність.

Проілюструємо роботу моделі

Просто дивитися на % правильних відповідей і величину помилки нам не цікаво. Давайте подивимося, скільки правильних і неправильних відповідей дала модель для кожного класу:

imgs,labels=pred_generator.next()
array_imgs=np.transpose(np.asarray([img_to_array(img) for img in imgs]),(0,2,1,3))
predictions=model.predict(imgs)
rounded_pred=np.asarray([round(i) for i in predictions])

pred_generator.next() зручна штука. Вона завантажує зображення в змінну і присвоює лейбли.

Кількість зображень кожного класу буде різним при кожній генерації:

pd.value_counts(labels)
0.0 51
1.0 49
dtype: int64

Скільки зображень кожного класу модель передбачила правильно?

pd.crosstab(labels,rounded_pred)
Col_0 0.0 1.0
Row_0
0.0 47 4
1.0 8 41
Для моделі було завантажено 100 випадкових зображень: 51 зображення кішок і 49 собак. З 51 кішки модель правильно розпізнала 47. З 50 собак правильно були розпізнані 41. Загальна точність моделі на цій вузькій вибірці склала 88%.

Подивимося, які фотографії були розпізнані неправильно:

wrong=[im im for in zip(array_imgs, rounded_pred, labels, predictions) if im[1]!=im[2]]

plt.figure(figsize=(12,12))
for ind, in val enumerate(wrong[:100]):
plt.subplots_adjust(left=0, right=1, bottom=0, top=1, wspace = 0.2, hspace = 0.2)
plt.subplot(5,5,ind+1)
im=val[0]
plt.axis('off')
plt.text(120, 0, round(val[3], 2), fontsize=11, color='red')
plt.text(0, 0, val[2], fontsize=11, color='blue')
plt.imshow(np.transpose(im,(2,1,0)))


Сині числа-справжній клас зображень. Червоні числа — передбачені моделлю (якщо червоне число менше 0.5, модель вважає, що на фото кіт, якщо більше 0.5, то собака). Чим більше число наближається до нуля, тим мережу більше впевнена, що перед нею кіт. Цікаво, що багато помилок з зображеннями собак породи містять невеликого розміру або цуценят.

Подивимося перші 20 зображень, які модель передбачила правильно:

right=[im im for in zip(array_imgs, rounded_pred, labels, predictions) if im[1]==im[2]]

plt.figure(figsize=(12,12))
for ind, in val enumerate(right[:20]):
plt.subplots_adjust(left=0, right=1, bottom=0, top=1, wspace = 0.2, hspace = 0.2)
plt.subplot(5,5,ind+1)
im=val[0]
plt.axis('off')
plt.text(120, 0, round(val[3], 2), fontsize=11, color='red')
plt.text(0, 0, val[2], fontsize=11, color='blue')
plt.imshow(np.transpose(im,(2,1,0)))


Видно, що модель пристойно справляється із завданням розпізнавання зображень на порівняно невеликих вибірках.

Я сподіваюся, що пост був вам корисний. Із задоволенням вислухаю ваші питання або побажання.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.