Классификация изображений с обучением передаче в Keras. Создаём передовые модели CNN

Вступление

Модели глубокого обучения – универсальные и мощные помощники. Они обычно превосходят людей в решении узких задач, а их способность к обобщению растёт быстрыми темпами. Новые модели часто выпускаются и сравниваются с наборами данных, принятыми сообществом, и идти в ногу со всеми из них становится всё труднее и труднее.

Большинство этих моделей имеют открытый исходный код, поэтому вы можете реализовать их самостоятельно.

Это значит, что среднестатистический энтузиаст может загружать и играть с передовыми моделями у себя дома, на очень средних машинах, не только для того, чтобы глубже понять и оценить ремесло, но и для того, чтобы внести свой вклад в научный дискурс и публиковать свои собственные улучшения всякий раз, как они сделаны.

В этом руководстве вы узнаете, как использовать предварительно обученные, передовые модели глубокого обучения для классификации изображений, и перепрофилировать их для вашего конкретного приложения. Таким образом, вы сможете использовать их высокую производительность, оригинальную архитектуру и чужое время на обучение, применяя эти модели в своей собственной области.

Transfer Learning для Computer Vision и Convolutional Neural Networks (CNNs)

Знания и представления знаний очень универсальны. Модель Computer Vision, обученная на одном наборе данных, учится распознавать шаблоны, которые могут быть очень распространены во многих других наборах данных.

Например, прямые и изогнутые линии, которые обычно изучаются на более низком уровне иерархии CNN, обязательно будут присутствовать практически во всех наборах данных. Некоторые функции высокого уровня, такие как те, которые отличают пчелу от муравья, будут представлены и изучены намного выше в иерархии:

feature hierarchies for convolutional neural networks

Можно использовать «тонкую грань» между ними повторно! В зависимости от уровня сходства между вашим набором данных и тем, на котором модель была предварительно обучена, вы можете повторно использовать небольшую или большую его часть.

Модель, которая классифицирует созданные человеком структуры (обучена набору данных, такому как Places365), и модель, которая классифицирует животных (обучена набору данных, такому как ImageNet), обязательно будут иметь некоторые общие шаблоны, хотя и не так много.

Возможно, вы захотите обучить модель различать, скажем, автобусы и автомобили для системы зрения самоуправляемого автомобиля. Можете выбрать использование производительной архитектуры, которая хорошо зарекомендовала себя в наборах данных, подобных вашему. Затем начинается длительный процесс обучения, и в итоге у вас появляется собственная эффективная модель!

Однако, если другая модель, скорее всего, будет иметь аналогичные представления на более низких и более высоких уровнях абстракции, – нет необходимости переобучать модель с нуля. Вы можете использовать некоторые из уже предварительно подготовленных весов, которые так же применимы к вашей модели, как они были применимы к создателю оригинальной архитектуры. Вы бы переносили часть знаний из уже существующей модели в новую, и это называется обучением передаче.

Чем ближе набор данных предварительно обученной модели к вашему собственному, тем больше вы можете передать. Чем больше вы сможете перевести, тем больше собственного времени вы сможете сэкономить. Стоит помнить, что обучение нейронных сетей действительно имеет углеродный след, так что вы не только экономите время!

Как правило, обучение передаче выполняется путем загрузки предварительно обученной модели и замораживания её слоёв. Во многих случаях вы можете просто отрезать слой классификации (конечные слои или заголовок) и просто повторно обучить слой классификации, сохранив при этом все остальные слои абстракции нетронутыми. В других случаях вы можете вместо этого повторно обучить несколько уровней иерархии. Это обычно делают, когда наборы данных содержат достаточно разные точки данных, чтобы повторное обучение нескольких уровней было оправданным. Вы также можете повторно обучить всю модель, чтобы точно настроить все слои.

Эти два подхода можно резюмировать следующим образом:

  • Использование свёрточной сети в качестве средства извлечения объектов;
  • Точная настройка свёрточной сети.

В первом случае вы используете базовую энтропийную ёмкость модели в качестве экстрактора фиксированных объектов и просто тренируете плотную сеть сверху, чтобы различать эти функции. Во втором случае вы точно настраиваете всю (или часть) сверточную сеть, если в ней еще нет репрезентативных карт объектов для какого-либо другого более конкретного набора данных, а также полагаетесь на уже подготовленные карты объектов.

Вот так работает трансфертное обучение:

how does transfer learning work?

Устоявшиеся и передовые модели классификации изображений

Существует множество моделей, и для хорошо известных наборов данных вы, скорее всего, найдёте сотни хорошо зарекомендовавших себя моделей, опубликованных в онлайн-репозиториях и статьях.

Некоторые из хорошо известных опубликованных архитектур, которые впоследствии были перенесены во многие фреймворки глубокого обучения, включают:

  • EfficientNet
  • SENet
  • Xception
  • ResNet
  • VGGNet
  • AlexNet
  • LeNet-5

Список моделей на PapersWithCode постоянно обновляется, и вы не должны зацикливаться на положении этих моделей там. Многие из них превосходят различные другие модели, и многие из новых моделей фактически основаны на тех, которые описаны в приведенном выше списке. Стоит отметить, что обучение передаче фактически сыграло важную роль в новых моделях с более высокой точностью!

Недостатком заключается в том, что многие новейшие модели не портированы как предварительно обученные модели в рамках таких фреймворков, как Tensorflow и PyTorch. Это не значит, что вы потеряете большую часть производительности, так что использовать любой из хорошо зарекомендовавших себя вариантов совсем не плохо.

Передача обучения с помощью Keras. Адаптация существующих Моделей

В Keras предварительно обученные модели доступны в модуле tensorflow.keras.applications. Каждая модель имеет свой собственный подмодуль и класс. При загрузке модели вы можете задать несколько необязательных аргументов, чтобы управлять тем, как загружаются модели.

Например, аргумент weights, если он присутствует, определяет предварительно подготовленные веса. Если этот параметр опущен, то будет загружена только архитектура (неподготовленная сеть). Если вы укажете имя набора данных – для этого набора данных будет возвращена предварительно обученная сеть.

Кроме того, поскольку вы, скорее всего, будете удалять верхний слой (слои) для обучения передаче, этот аргумент include_top используется для определения того, должен ли присутствовать верхний слой (слои) или нет!

import tensorflow.keras.applications as models

# 98 MB
resnet = models.resnet50.ResNet50(weights='imagenet', include_top=False)
# 528MB
vgg16 = models.vgg16.VGG16(weights='imagenet', include_top=False)
# 23MB
nnm = models.NASNetMobile(weights='imagenet', include_top=False)
# итд...

EfficientNet – это семейство сетей, которые являются достаточно производительными, масштабируемыми и, в общем, эффективными. Они были сделаны с учетом сокращения обучаемых параметров, поэтому у них есть только 4 миллиона параметров для обучения. Хотя 4 миллиона – это всё ещё большое число, учтите, что в VGG16, например, 20 миллионов. В домашних условиях это также значительно сокращает время обучения!

Давайте загрузим одного из членов семейства EfficientNet – EfficientNet-B0:

effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=False)
effnet.summary()

Результат:

Model: "efficientnetb0"
___________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
===========================================================================
input_1 (InputLayer)            [(None, 224, 224, 3) 0                                            
___________________________________________________________________________
rescaling (Rescaling)           (None, 224, 224, 3)  0           input_1[0][0]                    
___________________________________________________________________________
...
...
block7a_project_conv (Conv2D)   (None, 7, 7, 320)    368640      block7a_se_excite[0][0]          
___________________________________________________________________________
block7a_project_bn (BatchNormal (None, 7, 7, 320)    1280        block7a_project_conv[0][0]                    
===========================================================================
Total params: 3,634,851
Trainable params: 3,592,828
Non-trainable params: 42,023
___________________________________________________________________________

С другой стороны, если бы мы загрузили EfficientNet-B0 с включенным верхом, у нас также было бы несколько новых слоёв в конце, которые были обучены классифицировать данные для ImageNet. Это вершина модели, которую мы будем тренировать для нашего собственного приложения:

effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=True)
effnet.summary()

Это будет включать в себя слои Flatten и Dense, которые затем значительно увеличат размер параметра:

Model: "efficientnetb0"
___________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
===========================================================================
input_11 (InputLayer)           [(None, 224, 224, 3) 0                                            
___________________________________________________________________________
...
...
___________________________________________________________________________
top_conv (Conv2D)               (None, 7, 7, 1280)   409600      block7a_project_bn[0][0]         
___________________________________________________________________________
top_bn (BatchNormalization)     (None, 7, 7, 1280)   5120        top_conv[0][0]                   
___________________________________________________________________________
top_activation (Activation)     (None, 7, 7, 1280)   0           top_bn[0][0]                     
___________________________________________________________________________
avg_pool (GlobalAveragePooling2 (None, 1280)         0           top_activation[0][0]             
___________________________________________________________________________
top_dropout (Dropout)           (None, 1280)         0           avg_pool[0][0]                   
___________________________________________________________________________
predictions (Dense)             (None, 1000)         1281000     top_dropout[0][0]                
===========================================================================
Total params: 5,330,571
Trainable params: 5,288,548
Non-trainable params: 42,023
___________________________________________________________________________

Опять же, мы не будем использовать верхние слои, так как мы добавим нашу собственную верхнюю часть к эффективной Новой модели и переподготовим только те, которые мы добавим сверху. Стоит отметить, какую архитектуру уже там построили! Похоже, они используют слой Conv2D, за которым следует BatchNormalization, GlobalAveragePooling2D и отсев перед последним плотным слоем классификации. Хотя нам не обязательно строго следовать этому подходу (и другие подходы могут оказаться лучше для другого набора данных), разумно вспомнить, как выглядел исходный топ.

Функция preprocess_input() применяет к входным данным те же этапы предварительной обработки, что и во время обучения. Вы можете импортировать функцию из соответствующего модуля модели, если модель находится в своем собственном модуле. Например, VGG16 имеет свою собственную функцию preprocess_input:

from keras.applications.vgg16 import preprocess_input

При этом загрузка модели, предварительная обработка входных данных для нее и прогнозирование результата в Keras так же просты:

import tensorflow.keras.applications as models
from keras.applications.vgg16 import preprocess_input

vgg16 = models.vgg16.VGG16(weights='imagenet', include_top=True)

img = # получаем данные
img = preprocess_input(img)
pred = vgg16.predict(img)

Вот и всё! Теперь, поскольку массив pred на самом деле не содержит удобочитаемых данных, вы также можете импортировать функцию decode_predictions() вместе с функцией preprocess_input() из модуля. В качестве альтернативы вы можете импортировать универсальную функцию decode_predictions(), которая также применяется к моделям, у которых нет выделенных модулей:

from keras.applications.model_name import preprocess_input, decode_predictions

from keras.applications.imagenet_utils import decode_predictions

print(decode_predictions(pred))

Связывая всё это, мы получим изображение чёрного медведя с помощью urllib, сохраним этот файл в целевом размере, подходящем для EfficientNet (входной слой ожидает форму (None, 224, 224, 3) и классифицируем его с помощью предварительно обученной модели:

from tensorflow import keras
from keras.applications.vgg16 import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image

import urllib.request
import matplotlib.pyplot as plt
import numpy as np


url = 'https://upload.wikimedia.org/wikipedia/commons/0/02/Black_bear_large.jpg'
urllib.request.urlretrieve(url, 'bear.jpg')


img = image.load_img('bear.jpg', target_size=(224, 224))

img = image.img_to_array(img)

img = np.expand_dims(img, 0)



effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=True)
pred = effnet.predict(img)
print(decode_predictions(pred))

Результат:

[[
('n02133161', 'American_black_bear', 0.6024658),
('n02132136', 'brown_bear', 0.1457715),
('n02134418', 'sloth_bear', 0.09819221),
('n02510455', 'giant_panda', 0.0069221947),
('n02509815', 'lesser_panda', 0.005077324)
]]

Очевидно, что это изображение американского Чёрного медведя, и всё правильно! С помощью функции предварительной обработки изображение может значительно измениться. Например, функция предварительной обработки VGG16 изменит цвет меха медведя:

preprocessing image for VGG16 CNN

Теперь он выглядит коричневым! Если бы мы скинули это изображение в EfficientNet, оно бы подумало, что это бурый медведь:

[[
('n02132136', 'brown_bear', 0.7152758), 
('n02133161', 'American_black_bear', 0.15667434), 
('n02134418', 'sloth_bear', 0.012813852), 
('n02134084', 'ice_bear', 0.0067828503), ('n02117135', 'hyena', 0.0050422684)
]]

Круто! Модель работает. Теперь давайте добавим к нему новую вершину и переучим вершину, чтобы выполнить классификацию для чего-то вне набора ImageNet.

Добавляем верх к предварительно подготовленной модели

Выполняя обучение передаче, вы будете загружать модели без вершин или удалять их вручную:

effnet_base = keras.applications.EfficientNetB0(weights='imagenet', 
                                          include_top=False, 
                                          input_shape=((224, 224, 3)))


full_effnet = keras.applications.EfficientNetB0(weights='imagenet', 
                                            include_top=True, 
                                            input_shape=((224, 224, 3)))
                                            

trimmed_effnet = keras.Model(inputs=full_effnet.input, outputs=full_effnet.layers[-3].output)

Остановимся на первом варианте, так как он более удобен. В зависимости от того, хотите ли вы точно настроить свёрточные блоки или нет – вы либо заморозите, либо не заморозите их. Допустим, мы хотим использовать базовые предварительно подготовленные карты объектов и заморозить слои, чтобы сохранить только новые слои классификации вверху:

effnet_base.trainable = False

Не нужно повторять модель и настраивать каждый слой так, чтобы он был обучаемым, хотя вы можете это сделать. Если вы хотите отключить первые n слоёв и разрешить точную настройку некоторых карт объектов более высокого уровня, но оставить нетронутыми карты объектов более низкого уровня, можете сделать следующее:

for layer in effnet_base.layers[:-2]:
    layer.trainable = False

Здесь мы установили, что все слои в базовой модели не поддаются обучению, за исключением двух последних. Если мы проверим модель, то сейчас существует всего ~ 2,5 Тыс. обучаемых параметров:

effnet_base.summary()
# ...                
===========================================================================
Total params: 4,049,571
Trainable params: 2,560
Non-trainable params: 4,047,011
___________________________________________________________________________

Теперь давайте определим последовательную модель, которая будет помещена поверх этой базы effnet_base. К счастью, связать модели в Keras так же просто, как создать новую модель и положить её поверх другой! Вы можете использовать функциональный API и просто связать несколько новых слоев поверх модели.

Давайте добавим слои Conv2D, BatchNormalization, GlobalAveragePooling2D, некоторый отсев и пару полностью связанных слоёв после выравнивания:

conv2d = keras.layers.Conv2D(7, 7)(effnet_base.output, training=False)
bn = keras.layers.BatchNormalization()(conv2d)
gap = keras.layers.GlobalAveragePooling2D()(bn)
do = keras.layers.Dropout(0.2)(gap)
flatten = keras.layers.Flatten()(gap)
fc1 = keras.layers.Dense(512, activation='relu')(flatten)
output = keras.layers.Dense(100, activation='softmax')(fc1)

new_model = keras.Model(inputs=effnet_base.input, outputs=output)

В качестве альтернативы вы можете использовать последовательный API и вызывать метод add() несколько раз:

new_model = keras.Sequential()
new_model.add(effnet_base)
new_model.add(keras.layers.Conv2D(7,7))
new_model.add(keras.layers.BatchNormalization())
new_model.add(keras.layers.GlobalAveragePooling2D())
new_model.add(keras.layers.Dropout(0.3))
new_model.add(keras.layers.Flatten())
new_model.add(keras.layers.Dense(512, activation='relu'))
new_model.add(keras.layers.Dense(100, activation='softmax'))

Так мы добавили всю модель в виде самого слоя, поэтому она рассматривается как единое целое:

Layer: 0, Trainable: False # Entire EfficientNet model
Layer: 1, Trainable: True
Layer: 2, Trainable: True
...

С другой стороны, вы можете извлечь все слои и добавить их вместо этого как отдельные объекты, добавив вывод effnet_base:

new_model = keras.Sequential()
new_model.add(effnet_base.output)
new_model.add(keras.layers.Conv2D(7,7))
new_model.add(keras.layers.BatchNormalization())
new_model.add(keras.layers.GlobalAveragePooling2D())
new_model.add(keras.layers.Dropout(0.3))
new_model.add(keras.layers.Dense(100, activation='softmax'))

В любом случае мы добавили 10 выходных классов, так как позже мы будем использовать набор данных CIFAR10, в котором 10 классов! Давайте взглянем на обучаемые слои в сети:

for index, layer in enumerate(new_model.layers):
    print("Layer: {}, Trainable: {}".format(index, layer.trainable))

Результат:

Layer: 0, Trainable: False
Layer: 1, Trainable: False
Layer: 2, Trainable: False
...
Layer: 235, Trainable: False
Layer: 236, Trainable: False
Layer: 237, Trainable: True
Layer: 238, Trainable: True
Layer: 239, Trainable: True
Layer: 240, Trainable: True
Layer: 241, Trainable: True

Прекрасно! Давайте загрузим набор данных, предварительно обработаем его и заново обучим слои классификации на нем.

Загрузка и предварительная обработка данных

Мы будем работать с набором данных CIFAR10. Этот набор данных не так сложно классифицировать, поскольку в нём всего 10 классов. Мы будем использовать хорошо принятую архитектуру, которая поможет.

С CIFAR100, его «старшим братом», действительно трудно работать. В нём 50 000 изображений с сотню меток. Это означает, что в каждом классе всего 500 образцов. Очень сложно сделать правильно на таком небольшом количестве меток, и почти все хорошо работающие модели в наборе данных используют интенсивное увеличение данных.

Для краткости мы будем придерживаться CIFAR10, чтобы подражать набору данных, с которым вы будете работать самостоятельно!

Мы будем использовать наборы tensorflow_data для загрузки набора данных CIFAR10, получения меток и количества классов:

import tensorflow_datasets as tfds
import tensorflow as tf

dataset, info = tfds.load("cifar10", as_supervised=True, with_info=True)

class_names = info.features["label"].names
n_classes = info.features["label"].num_classes

print(class_names)
print(n_classes)

Вы можете ознакомиться с набором данных, но мы не будем углубляться в это прямо сейчас. Давайте вместо этого разделим его на train_set, valid_set и test_set:

test_set, valid_set, train_set = tfds.load("cifar10", 
                                           split=["train[:10%]", "train[10%:25%]", "train[25%:]"],
                                           as_supervised=True)

print("Train set size: ", len(train_set))
print("Test set size: ", len(test_set))
print("Valid set size: ", len(valid_set)) # Valid set size:  7500

Теперь изображения CIFAR10 значительно отличаются от изображений ImageNet! А именно, изображения CIFAR10 имеют размер всего 32×32, в то время как наша эффективная сетевая модель ожидает изображений 224×224. В любом случае мы захотим изменить размер изображений. Мы также можем захотеть применить некоторые функции преобразования к дублирующимся изображениям, чтобы искусственно увеличить размер выборки для каждого класса, если в наборе данных их недостаточно. В случае CIFAR10 это не проблема, так как для каждого класса достаточно изображений, но с CIFAR100 – это совсем другая история. Стоит отметить, что при увеличении масштаба изображений такого размера даже людям бывает трудно различить, что изображено на некоторых изображениях.

Например, вот несколько изображений без изменения размера:

cifar100 image examples

Можете ли вы с уверенностью сказать, что на них написано? Подумайте также о том, какой объем контекста у вас есть для этих изображений на протяжении всей жизни, которого нет у модели. Стоит иметь это в виду, когда вы тренируете его и наблюдаете за точностью.

Давайте определим функцию предварительной обработки для каждого изображения и связанной с ним метки:

def preprocess_image(image, label):

    resized_image = tf.image.resize(image, [224, 224])

    img = tf.image.random_flip_left_right(resized_image)
    img = tf.image.random_flip_up_down(img)
    img = tf.image.rot90(img)


return img, label

И, наконец, мы захотим применить эту функцию к каждому изображению в наборах! Мы не выполняли аугментацию, расширяя наборы здесь, хотя вы могли бы. Для краткости мы не будем выполнять увеличение данных.

Это легко сделать с помощью функции map(). Поскольку ввод в сеть также ожидает пакетов ((Нет, 224, 224, 3) вместо (224, 224, 3)) – мы также будем паковать() наборы данных после сопоставления:

train_set = train_set.map(preprocess_image).batch(32).prefetch(1)
test_set = test_set.map(preprocess_image).batch(32).prefetch(1)
valid_set = valid_set.map(preprocess_image).batch(32).prefetch(1)

Наконец-то мы можем обучить модель!

Обучаем модель

Когда данные загружены, предварительно обработаны и разделены на соответствующие наборы, мы можем обучить модель на них. Оптимизатор, а также его гиперпараметры, функция потерь и показатели обычно зависят от вашей конкретной задачи.

Поскольку мы выполняем разреженную классификацию, потеря sparse_categorical_crossentropy должна работать хорошо, а оптимизатор Adam является разумной функцией потерь по умолчанию. Давайте скомпилируем модель и обучим ее нескольким эпохам. Стоит помнить, что большинство слоев в сети заморожены! Мы только обучаем новый классификатор поверх извлеченных карт объектов.

Только после того, как мы обучим верхние слои, мы можем решить разморозить слои извлечения объектов и позволить им немного доработать. Этот шаг необязателен, и во многих случаях вы не будете их размораживать (в основном при работе с действительно большими сетями). Хорошее эмпирическое правило состоит в том, чтобы попытаться сравнить наборы данных и оценить, какие уровни иерархии вы можете повторно использовать без переподготовки.

Если они действительно разные, вы, вероятно, выбрали сеть, предварительно обученную на неправильном наборе данных. Было бы неэффективно использовать извлечение признаков Places365 (искусственных объектов) для классификации животных. Однако имело бы смысл использовать сеть, обученную в ImageNet (в которой есть различные объекты, животные, растения и люди), а затем использовать ее для другого набора данных с относительно похожими категориями, такими как CIFAR10.

Давайте обучим новую сеть (на самом деле, только ее верхнюю часть) в течение 10 эпох:

optimizer = keras.optimizers.Adam(learning_rate=2e-5)

new_model.compile(loss="sparse_categorical_crossentropy", 
                  optimizer=optimizer, 
                  metrics=["accuracy"])

history = new_model.fit(train_set, 
                        epochs=10,
                        validation_data=valid_set)

Это тот момент, когда вы садитесь поудобнее и идете выпить кофе (или чая)! После 10 эпох точность поезда и проверки выглядят хорошо:

Epoch 1/10
1172/1172 [==============================] - 92s 76ms/step - loss: 1.6582 - accuracy: 0.6373 - val_loss: 1.1582 - val_accuracy: 0.7935
...
Epoch 10/10
1172/1172 [==============================] - 89s 76ms/step - loss: 0.0911 - accuracy: 0.9781 - val_loss: 0.3847 - val_accuracy: 0.8792

~98% на тренировочном наборе и ~ 88% на проверочном наборе – это явно переобучение, но не слишком сильно. Давайте проверим это и построим кривые обучения.

Тестируем модель

Давайте сначала протестируем эту модель, прежде чем пытаться разморозить все слои и посмотреть, сможем ли мы ее настроить:

new_model.evaluate(test_set)

88% на тестовом наборе и чрезвычайно близки к точности на тестовом наборе! Похоже, наша модель хорошо обобщается, но все еще есть возможности для улучшения. Давайте взглянем на кривые обучения.

Кривые обучения следует ожидать – они довольно короткие, так как мы тренировались всего 10 эпох, но они быстро вышли на плато, так что мы, вероятно, не добились бы намного лучшей производительности с большим количеством эпох. Хотя колебания действительно происходят, и точность вполне может повыситься в эпоху 11 – это не слишком вероятно, так что мы упустим шанс:

transfer learning training curves

Можем ли мы еще более точно настроить эту сеть? Мы заменили и переподготовили верхние слои, связанные с классификацией карт объектов. Давайте попробуем разморозить сверточные слои и точно настроить их!

Размораживаем слои. Точная настройка сети, обученной с помощью обучения передаче

Как только вы закончите переподготовку верхних слоев, вы сможете заключить сделку и быть довольным своей моделью. Например, предположим, что у вас точность 95% – вам серьезно не нужно идти дальше. Впрочем, почему бы и нет?

Если вы сможете выжать дополнительный 1% точности, это может показаться не очень большим, но подумайте о другом конце сделки. Если ваша модель имеет точность 95% на 100 образцах, она неправильно классифицировала 5 образцов. Если вы увеличите это с точностью до 96%, то неправильно классифицируете 4 образца.

Все, что вы можете дополнительно выжать из своей модели, на самом деле может существенно повлиять на количество неправильных классификаций. У нас довольно удовлетворительная точность 88% с нашей моделью, но мы, скорее всего, сможем выжать из нее больше, если просто немного переобучим экстракторы функций. Опять же, изображения в CIFAR10 намного меньше, чем изображения ImageNet, и это почти так же, как если бы кто-то с отличным зрением внезапно получил огромный рецепт и видел мир только размытыми глазами. Карты объектов должны быть, по крайней мере, несколько другими!

Давайте сохраним модель в файл, чтобы не потерять прогресс, и разморозим / настроим загруженную копию, чтобы случайно не перепутать веса в исходной:

new_model.save('effnet_transfer_learning.h5')
loaded_model = keras.models.load_model('effnet_transfer_learning.h5')

Теперь мы можем повозиться и изменить loaded_model, не влияя на new_model! Для начала мы захотим перевести loaded_model из режима вывода обратно в режим обучения, т.Е. разморозить слои, чтобы они снова могли обучаться.

Давайте отключим слои пакетной нормализации, чтобы наше обучение не пошло насмарку:

for layer in loaded_model.layers:
    if isinstance(layer, keras.layers.BatchNormalization):
        layer.trainable = False
    else:
        layer.trainable = True

for index, layer in enumerate(loaded_model.layers):
    print("Layer: {}, Trainable: {}".format(index, layer.trainable))

Работает? Конечно:

Layer: 0, Trainable: True
Layer: 1, Trainable: True
Layer: 2, Trainable: True
Layer: 3, Trainable: True
Layer: 4, Trainable: True
Layer: 5, Trainable: False
Layer: 6, Trainable: True
Layer: 7, Trainable: True
Layer: 8, Trainable: False
...

Прежде чем мы сможем что-либо сделать с моделью, чтобы “укрепить” обучаемость, мы должны ее перекомпилировать. На этот раз мы будем использовать меньшую скорость обучения, так как мы вообще не хотим сильно изменять сеть и просто хотим точно настроить некоторые возможности извлечения функций и новый слой классификации сверху:

optimizer = keras.optimizers.Adam(learning_rate=1e-6, decay=(1e-6/50))

# Recompile after turning to trainable
loaded_model.compile(loss="sparse_categorical_crossentropy", 
                  optimizer=optimizer, 
                  metrics=["accuracy"])

history = loaded_model.fit(train_set, 
                        epochs=50,
                        validation_data=valid_set)

Опять же, это может занять некоторое время – так что потягивайте другой горячий напиток по вашему выбору, пока это работает в фоновом режиме. Как только он завершится, он должен достичь 92% точности проверки и 92,6% в тестовом наборе:

Epoch 1/10
1172/1172 [==============================] - 389s 328ms/step - loss: 0.0552 - accuracy: 0.9863 - val_loss: 0.3493 - val_accuracy: 0.8941
...
Epoch 50/50
1172/1172 [==============================] - 375s 320ms/step - loss: 0.0046 - accuracy: 0.9989 - val_loss: 0.3454 - val_accuracy: 0.9188

Опять же, это не огромный скачок с точки зрения самой точности, но это гораздо более значительное сокращение доли неправильных классификаций. Давайте оценим это и визуализируем некоторые прогнозы:

loaded_model.evaluate(test_set)



fig = plt.figure(figsize=(15, 10))

i = 1
for entry in test_set.take(25):

    pred = np.argmax(loaded_model.predict(entry[0].numpy()[0].reshape(1, 224, 224, 3)))


    sample_image = entry[0].numpy()[0]

    sample_label = class_names[entry[1].numpy()[0]]

    prediction_label = class_names[pred]
    ax = fig.add_subplot(5, 5, i)
    

    ax.imshow(np.array(sample_image, np.int32))
    ax.set_title("Actual: %s\nPred: %s" % (sample_label, prediction_label))
    i = i+1

plt.tight_layout()
plt.show()
transfer learning efficientnet-b0 model predictions

Здесь единственная неправильная классификация, которую мы видим на первых 25 изображениях, – это грузовик, ошибочно классифицированный как лошадь. Вероятно, это связано с контекстом – он находится в лесу, коричневый и удлиненный. Это также в какой-то степени соответствует описанию лошади, поэтому неудивительно, что на размытом маленьком изображении (224×224) грузовик был неправильно классифицирован.

Еще одна вещь, которая определенно не помогла, – это то, что похоже, что ворота грузовика открыты, что может выглядеть как шея лошади, когда она питается травой.

Заключение

Трансфертное обучение – это процесс передачи уже усвоенных представлений знаний из одной модели в другую, когда это применимо.

На этом заканчивается данное руководство по передаче обучения классификации изображений с помощью Keras и Tensorflow. Мы начали с рассмотрения того, что такое обучение передаче и как представления знаний могут быть разделены между моделями и архитектурами.

Затем мы рассмотрели некоторые из наиболее популярных и передовых моделей классификации изображений, опубликованных публично, и воспользовались одной из них – EfficientNet – чтобы помочь нам классифицировать некоторые из наших собственных данных. Мы рассмотрели, как загружать и изучать предварительно обученные модели, как работать с их слоями, прогнозировать с их помощью и декодировать результаты, а также как определять свои собственные слои и переплетать их с существующей архитектурой.

Наконец, мы загрузили и предварительно обработали набор данных и обучили на нем наши новые верхние слои классификации, прежде чем разморозить слои и доработать их в течение нескольких дополнительных эпох.

Ответить