Простой пример различения поездов и автомобилей с ~ 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'])

Чтобы использовать наши изображения, нам необходимо:

  1. Прочтите файлы изображений (.jpg)
  2. Декодировать изображения в значения RGB пикселей
  3. Преобразуйте значения RGB в числа с плавающей запятой (от 0 до 255)
  4. Измените масштаб этих значений на [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. Это важно, поскольку предотвращает обновление весов во время тренировки. Без этого предварительно изученные веса были бы испорчены большими случайно инициализированными весами из двух плотных слоев. С помощью замораживания мы тренируем только плотные слои для наших конкретных потребностей.

Основные выводы:

  1. ConvNets чрезвычайно полезны для решения проблем с компьютерным зрением; можно обучить одного из ограниченных наборов данных с приличными результатами (›80% точность и всего ~ 1300 обучающих изображений для поездов и автомобилей).
  2. Самый большой недостаток работы с небольшими наборами данных изображений - это переоснащение. Это делает увеличение данных чрезвычайно мощным инструментом.
  3. Использование предварительно обученной модели, такой как VGG19 от Keras, с использованием увеличения и исключения данных довольно просто и дает отличные результаты. Все, что нужно сделать, - это извлечь признаки.