Простой пример различения поездов и автомобилей с ~ 1500 изображениями с использованием экземпляра AWS EC2 p2.xlarge (Ubuntu Deep Learning). Весь код из этого проекта можно найти здесь.
Для запуска этого проекта через Google images было загружено 760 изображений поездов и 760 изображений автомобилей. Используя приведенный ниже Javascript, я удалил URL-адреса самых популярных изображений из соответствующих запросов "поезд" и "автомобиль".
// Adding script to the html page var script = document.createElement('script'); script.src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"; document.getElementsByTagName('head')[0].appendChild(script); // Get all the urls var urls = $('.rg_di .rg_meta').map(function() { return JSON.parse($(this).text()).ou; }); // write url's to a text file and download var textToSave = urls.toArray().join('\n'); var hiddenElement = document.createElement('a'); hiddenElement.href = 'data:attachment/text,'+encodeURI(textToSave); hiddenElement.target = '_blank'; hiddenElement.download = 'urls.txt'; hiddenElement.click();
После того, как текстовый документ со всеми URL-адресами был создан, я использовал скрипт Python, чтобы загрузить их на сервер.
Затем данные были разделены на наборы данных для обучения и тестирования. 660 изображений поездов и вагонов использовались для обучения, а оставшиеся 100 изображений использовались для тестирования / проверки. После разделения нашего набора данных модель была построена:
from keras import layers from keras import models model = models.Sequential() model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(64, 64, 3))) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(64, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Flatten()) model.add(layers.Dense(512, activation='relu')) model.add(layers.Dense(1, activation='sigmoid'))
Эта свёртка построена как стек чередующихся Conv2D
(с активацией relu) и MaxPooling2D
слоев. Поскольку набор данных состоит из больших изображений и довольно сложной задачи (автомобили и поезда могут выглядеть очень похожими!), Сеть содержит больше слоев. Это помогает уменьшить размер карты объектов с 64x64 до 7x7 прямо перед плоским слоем. Поскольку это проблема двоичной классификации, последний плотный слой - это слой размером 1, который будет кодировать вероятность движения поезда по сравнению с автомобилем. Для справки ниже приводится краткое описание модели:
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d_9 (Conv2D) (None, 62, 62, 32) 896 _________________________________________________________________ max_pooling2d_9 (MaxPooling2 (None, 31, 31, 32) 0 _________________________________________________________________ conv2d_10 (Conv2D) (None, 29, 29, 64) 18496 _________________________________________________________________ max_pooling2d_10 (MaxPooling (None, 14, 14, 64) 0 _________________________________________________________________ conv2d_11 (Conv2D) (None, 12, 12, 128) 73856 _________________________________________________________________ max_pooling2d_11 (MaxPooling (None, 6, 6, 128) 0 _________________________________________________________________ conv2d_12 (Conv2D) (None, 4, 4, 128) 147584 _________________________________________________________________ max_pooling2d_12 (MaxPooling (None, 2, 2, 128) 0 _________________________________________________________________ flatten_3 (Flatten) (None, 512) 0 _________________________________________________________________ dense_5 (Dense) (None, 512) 262656 _________________________________________________________________ dense_6 (Dense) (None, 1) 513 ================================================================= Total params: 504,001 Trainable params: 504,001 Non-trainable params: 0 _________________________________________________________________
Далее компилируем модель. Наша сеть заканчивается одним сигмоидальным блоком, поэтому мы будем использовать двоичную кроссентропию для нашей функции потерь и стандартный оптимизатор RMSprop
.
from keras import optimizers model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])
Чтобы использовать наши изображения, нам необходимо:
- Прочтите файлы изображений (.jpg)
- Декодировать изображения в значения RGB пикселей
- Преобразуйте значения RGB в числа с плавающей запятой (от 0 до 255)
- Измените масштаб этих значений на [0,1]
from keras.preprocessing.image import ImageDataGenerator # All images will be rescaled by 1./255 train_datagen = ImageDataGenerator(rescale=1./255) test_datagen = ImageDataGenerator(rescale=1./255) train_generator = train_datagen.flow_from_directory( train_dir, # All images will be resized to 64x64 target_size=(64, 64), batch_size=20, class_mode='binary') #test images are being used to validate validation_generator = test_datagen.flow_from_directory( test_dir, target_size=(64, 64), batch_size=20, class_mode='binary')
Теперь мы можем подогнать нашу модель к нашим данным, используя наш генератор. Метод fit_generator
эквивалентен подходу для генераторов данных. Наш генератор будет генерировать пакеты данных на неопределенный срок, поэтому нам нужно указать steps_per_epoch
, то есть количество выборок, которые нужно извлечь из генератора, прежде чем перейти к следующей эпохе.
history = model.fit_generator( train_generator, steps_per_epoch=100, epochs=20, validation_data=validation_generator, validation_steps=50)
Мы также передаем переменные validation_data
и validation_steps
для оценки нашей модели в каждую эпоху. validation_steps
говорит нам, сколько выборок нужно извлечь из validation_generator
, аналогично steps_per_epoch.
. Построение наших результатов дает нам:
Из приведенных выше графиков мы видим явное переоснащение (как и ожидалось из нашей небольшой обучающей выборки). Наша точность обучения линейно увеличивается почти до 100%, в то время как точность проверки остается на уровне около 80%. Точно так же наши потери в обучении линейно уменьшаются почти до 0, в то время как потери при проверке остаются на уровне около 0,5.
Чтобы противодействовать переобучению, мы можем использовать расширение данных: мощный инструмент, специфичный для компьютерного зрения. По сути, мы генерируем больше обучающих данных на основе наших текущих обучающих выборок, «дополняя» изображения с помощью ряда преобразований. Это создает новые изображения, которые выглядят похожими / правдоподобными на оригинал. Несмотря на то, что исходное изображение и новое увеличенное изображение схожи, они не совпадают, что помогает модели увидеть больше аспектов данных и сделать выводы.
В Keras мы можем использовать ImageDataGenerator
для увеличения:
datagen = ImageDataGenerator( rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest')
Выше приведены лишь несколько примеров преобразований, которые можно использовать. Названия полей объясняются сами по себе, но если вы хотите узнать больше, посетите: https://keras.io/preprocessing/image/#imagedatagenerator-class. Ниже приведен пример дополнения одной из наших фотографий машины:
Теперь мы готовы обучить нашу модель с помощью дополнения данных. В этой модели мы также добавили слой Dropout, чтобы облегчить переоснащение. Техника отсева - это более общий способ борьбы с переобучением путем деактивации частей слоев во время обучения, вынуждая разные нейроны на одном и том же слое изучать одну и ту же концепцию, тем самым улучшая обобщение.
model = models.Sequential() model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(64, 64, 3))) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(64, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Flatten()) model.add(layers.Dropout(0.5)) model.add(layers.Dense(512, activation='relu')) model.add(layers.Dense(1, activation='sigmoid')) model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])
Теперь, когда модель настроена, мы обучаем ее, используя нашу новую технику увеличения данных вместе с выпадением.
train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True,) # Note that the validation data should not be augmented! test_datagen = ImageDataGenerator(rescale=1./255) train_generator = train_datagen.flow_from_directory( train_dir, # All images will be resized to 64x64 target_size=(64, 64), batch_size=20, class_mode='binary') validation_generator = test_datagen.flow_from_directory( test_dir, target_size=(64, 64), batch_size=20, class_mode='binary') history = model.fit_generator( train_generator, steps_per_epoch=100, epochs=60, validation_data=validation_generator, validation_steps=50)
Построение графика результатов дает нам:
Выше мы видим, что мы больше не переоснащаем данные. Теперь мы можем достичь точности, близкой к 90% для нашего набора для валидации, что значительно улучшилось по сравнению с нашей первой моделью. Однако из-за того, что мы начали с такого небольшого количества обучающих данных, мы наблюдаем падение точности. Один из способов обойти это - использовать предварительно обученную модель от Keras.
from keras.applications import VGG19 conv_base = VGG19(weights='imagenet', include_top=False, input_shape=(64, 64, 3)) # The summary of the VGG19 model conv_base.summary() _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_2 (InputLayer) (None, 64, 64, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 64, 64, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 64, 64, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 32, 32, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 32, 32, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 32, 32, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 16, 16, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 16, 16, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 16, 16, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 16, 16, 256) 590080 _________________________________________________________________ block3_conv4 (Conv2D) (None, 16, 16, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 8, 8, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 8, 8, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 8, 8, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 8, 8, 512) 2359808 _________________________________________________________________ block4_conv4 (Conv2D) (None, 8, 8, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 4, 4, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 4, 4, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 4, 4, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 4, 4, 512) 2359808 _________________________________________________________________ block5_conv4 (Conv2D) (None, 4, 4, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 2, 2, 512) 0 ================================================================= Total params: 20,024,384 Trainable params: 20,024,384 Non-trainable params: 0 _________________________________________________________________
Модель VGG19 поставляется с предварительно обученными весами в наборе данных ImageNet. Требуется минимальная input_shape 48x48. Чтобы использовать эту модель, мы начнем с извлечения функций из наших изображений. Извлечение признаков позволяет нам повторно использовать информацию, содержащуюся в слоях модели VGG19. По сути, он берет интересные особенности из образца с использованием представлений, уже изученных VGG19. Мы берем конвенциональную базу модели VGG19, пропускаем через нее набор данных о новых поездах и автомобилях и обучаем наш новый классификатор на ее результате.
import os import numpy as np from keras.preprocessing.image import ImageDataGenerator base_dir = 'trains_cars' train_dir = os.path.join(base_dir, 'train') test_dir = os.path.join(base_dir, 'test') datagen = ImageDataGenerator(rescale=1./255) batch_size = 20 def extract_features(directory, sample_count): features = np.zeros(shape=(sample_count, 2, 2, 512)) labels = np.zeros(shape=(sample_count)) generator = datagen.flow_from_directory( directory, target_size=(64, 64), batch_size=batch_size, class_mode='binary') i = 0 for inputs_batch, labels_batch in generator: features_batch = conv_base.predict(inputs_batch) features[i * batch_size : (i + 1) * batch_size] = features_batch labels[i * batch_size : (i + 1) * batch_size] = labels_batch i += 1 if i * batch_size >= sample_count: break return features, labels train_features, train_labels = extract_features(train_dir, 1322) test_features, test_labels = extract_features(test_dir, 200) train_features = np.reshape(train_features, (1322, 2 * 2 * 512)) test_features = np.reshape(test_features, (200, 2 * 2 * 512))
После извлечения элементов и изменения их формы мы подгоняем их под модель.
model = models.Sequential() model.add(layers.Dense(256, activation='relu', input_dim=2 * 2 * 512)) model.add(layers.Dropout(0.5)) model.add(layers.Dense(1, activation='sigmoid')) model.compile(optimizer=optimizers.RMSprop(lr=2e-5), loss='binary_crossentropy', metrics=['acc']) history = model.fit(train_features, train_labels, epochs=30, batch_size=20, validation_data=(test_features, test_labels))
Ниже приведены результаты этой модели:
Как видите, точность и потеря проверочного набора более точно соответствуют обучающему набору. Мы достигли стабильной точности 88,5% на проверочном наборе, что намного лучше, чем наша модель, созданная с нуля. Однако мы наблюдаем некоторое переоснащение, показанное на приведенных выше графиках, потому что мы не используем увеличение данных. В приведенном ниже коде показано, как мы будем использовать увеличение данных с предварительно обученной моделью:
conv_base.trainable = False train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest') # Note that the validation data should not be augmented! test_datagen = ImageDataGenerator(rescale=1./255) train_generator = train_datagen.flow_from_directory( # This is the target directory train_dir, # All images will be resized to 150x150 target_size=(64, 64), batch_size=20, # Since we use binary_crossentropy loss, we need binary labels class_mode='binary') validation_generator = test_datagen.flow_from_directory( test_dir, target_size=(64, 64), batch_size=20, class_mode='binary') model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=2e-5), metrics=['acc']) history = model.fit_generator( train_generator, steps_per_epoch=100, epochs=20, validation_data=validation_generator, validation_steps=50)
В первой строке приведенного выше фрагмента кода мы фиксируем модель conv. Это важно, поскольку предотвращает обновление весов во время тренировки. Без этого предварительно изученные веса были бы испорчены большими случайно инициализированными весами из двух плотных слоев. С помощью замораживания мы тренируем только плотные слои для наших конкретных потребностей.
Основные выводы:
- ConvNets чрезвычайно полезны для решения проблем с компьютерным зрением; можно обучить одного из ограниченных наборов данных с приличными результатами (›80% точность и всего ~ 1300 обучающих изображений для поездов и автомобилей).
- Самый большой недостаток работы с небольшими наборами данных изображений - это переоснащение. Это делает увеличение данных чрезвычайно мощным инструментом.
- Использование предварительно обученной модели, такой как VGG19 от Keras, с использованием увеличения и исключения данных довольно просто и дает отличные результаты. Все, что нужно сделать, - это извлечь признаки.