- PVSM.RU - https://www.pvsm.ru -
Привет! Под катом пойдёт речь о реализации свёрточной нейронной сети архитектуры InceptionV3 с использованием фреймворка Keras [1]. Статью я решил написать после ознакомления с туториалом "Построение мощных моделей классификации с использованием небольшого количества данных [2]". С одобрения автора туториала я немного изменил содержание своей статьи. В отличие от предложенной автором нейронной сети VGG16 [3], мы будем обучать гугловскую глубокую нейронную сеть Inception V3 [4], которая уже предустановлена в Keras.
Вы научитесь:
При написании статьи я ставил перед собой задачу представить максимально практичный материал, который раскроет некоторые интересные возможности фреймворка Keras.
В последнее время всё чаще появляются туториалы, посвящённые созданию и применению нейронных сетей. С большим удовольствием наблюдаю за интересной тенденцией: новые посты становятся всё более и более понятными для неспециалистов в области программирования. Некоторые авторы [5] и вовсе делают попытки введения читателей в эту тему, применяя максимально естественный язык. Есть также отличные статьи (напр. 1 [6], 2 [7], 3 [8]), которые сочетают разумное количество теории и практики, что позволяет как можно быстрее понять необходимый минимум и начать создавать что-то своё.
Итак, за дело!
В нашем примере мы будем использовать изображения с соревнования по машинному обучению под названием "Dogs vs Cats [15]" на kaggle.com [16]. Данные будут доступны после регистрации. Набор включает в себя 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
...
Вам ничего не мешает пользоваться всем набором данных. Я, также как и автор оригинальной статьи, решил использовать ограниченную выборку, чтобы проверить эффективность работы сети с малым набором изображений.
Последний пункт должен нас настораживать, потому что глубокая нейронная сеть требовательна к ресурсам. Я на своей видеокарте не смог обучить даже VGG16, не говоря уж о таком гигантище, как Inception. Тем не менее, решение есть:
Единственным решением проблемы со временем я пока что вижу применение параллельных вычислений. Вам для этого нужна видеокарта с поддержкой CUDA [19]. Я надеюсь, что установка CUDA для Python не вызовет у вас сильных затруднений [14].
Импортируем библиотеки:
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
В библиотеке Keras [20] находится несколько подготовленных нейронных сетей.
weights: загружать/не загружать обученные веса. Если None, веса инициализируются случайно. Если «imagenet», будут загружены веса, тренированные на данных ImageNet [18].
В нашей модели веса нужны, поэтому 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)
Процесс займёт некоторое время.
В оригинальном посте автор использовал один слой сети с 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 единиц для тренировочной и для тестовой выборок.
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.
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 массивы. Это нас не устраивает. Для того, чтобы получить полноценную модель, которая принимает на вход изображения, мы соединим две наши модели и обучим их ещё раз.
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).
for layer 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.5640Epoch 00000: val_acc improved from -inf to 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.6240Epoch 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.9385Epoch 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.9350Epoch 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 часов.
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 for im in zip(array_imgs, rounded_pred, labels, predictions) if im[1]!=im[2]]
plt.figure(figsize=(12,12))
for ind, val in 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, то собака). Чем больше число приближается к нулю, тем сеть больше уверена, что перед ней кот. Интересно, что много ошибок с изображениями собак содержат породы небольшого размера или щенков.
right=[im for im in zip(array_imgs, rounded_pred, labels, predictions) if im[1]==im[2]]
plt.figure(figsize=(12,12))
for ind, val in 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)))
Видно, что модель прилично справляется с задачей распознавания изображений на сравнительно небольших выборках.
Я надеюсь, что пост был вам полезен. С удовольствием выслушаю ваши вопросы или пожелания.
Автор: bredd_owen
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/243208
Ссылки в тексте:
[1] Keras: https://keras.io/
[2] Построение мощных моделей классификации с использованием небольшого количества данных: https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html
[3] VGG16: http://www.robots.ox.ac.uk/~vgg/practicals/cnn/
[4] Inception V3: https://habrahabr.ru/post/302242/
[5] Некоторые авторы: https://habrahabr.ru/post/317712/
[6] 1: https://habrahabr.ru/company/yandex/blog/307260/
[7] 2: https://habrahabr.ru/post/271563/
[8] 3: https://habrahabr.ru/post/309508/
[9] Anaconda: https://www.continuum.io/downloads
[10] Jupyter notebook: http://jupyter.org/
[11] Keras: https://keras.io/#installation
[12] Theano: http://deeplearning.net/software/theano/
[13] Tensorflow: https://www.tensorflow.org/
[14] здесь: https://lepisma.github.io/articles/2015/07/30/up-with-theano-and-cuda/
[15] Dogs vs Cats: https://www.kaggle.com/c/dogs-vs-cats
[16] kaggle.com: https://www.kaggle.com/
[17] переобучения: https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B5%D0%BE%D0%B1%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5
[18] imagenet: http://www.image-net.org/
[19] CUDA: https://ru.wikipedia.org/wiki/CUDA
[20] библиотеке Keras: https://keras.io/applications/
[21] Источник: https://habrahabr.ru/post/321834/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.