Подход к использованию предварительно обученных моделей и переносу обучения

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

Одним из способов решения этих проблем было бы использование уже обученной модели и использование полученных ею знаний для решения вашей проблемы. Что-то вроде того:

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

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

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

  • Извлечение признаков: здесь мы используем значения, полученные сетью, до некоторой точки ее структуры (как правило, полностью связанные слои, в зависимости от вашего набора данных) и используем их в качестве вектора признаков. Это означает, что при добавлении слоя классификации в верхней части сети мы будем обновлять веса только этого последнего слоя, откуда мы позже сможем брать наши прогнозы.
  • Точная настройка. Здесь мы намерены использовать больше слоев предварительно обученной модели, а не просто изменять полностью связанные слои в конце модели. Мы могли бы обновить все параметры модели для новой желаемой классификации, но могли бы также обновить только некоторые из них. Это можно сделать, выбрав слои, которые мы хотим оставить замороженными, и тот, который мы хотим разморозить, а затем снова запустить тренировку. Здесь я буду использовать этот подход, используя два этапа: на первом я добавлю новый полностью связанный слой (более простой, чем тот, что используется в процессе извлечения признаков) и обучу модель, чтобы она могла начать изучать шаблоны из предыдущих слоев и в На втором этапе я размораживаю более глубокие слои в сети и снова обучаю их, позволяя модели фактически изучать функции из моих данных.

Познакомьтесь с моделями

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

ВГГ16

Архитектура:

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

Эта конкретная модель была построена для крупномасштабных задач распознавания изображений и имеет архитектуру, включающую 13 сверточных слоев и 3 полностью связанных слоя, и заканчивается классификатором softmax, что приводит к 16 слоям обучаемых параметров, очень сверточному слою. сфокусированная модель. Входной размер по умолчанию для модели составляет 224x224 изображения и принимает цветные изображения, 3 канала RGB.

MobileNetV1

Архитектура:

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

Согласно оригинальной статье конфигурация тела отверстия его архитектуры организована следующим образом:

НачалоV3

Архитектура:

Это третья версия оригинальной архитектуры Google Inception. Модель была создана с использованием набора данных ImageNet для конкурса Large Visual Recognition Challenge. Архитектура кузова соответствует последовательности модулей Inception, которые были представлены в оригинальной статье.

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

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

РесНет50

Архитектура:

Эта модель была представлена ​​в 2015 году и известна решением проблемы исчезающего градиента. Модель остаточной сети (ResNet) — это длинная модель, в которой несколько раз используются блоки CNN со слоем пакетной нормализации после каждого сверточного слоя, но с некоторыми пропусками соединений по пути. Короче говоря, эти соединения позволяют модели смягчить снижение производительности при использовании множества сверточных слоев. Архитектура сети зависит от количества имеющихся у нее слоев, в ResNet50 архитектура состоит из 50 слоев, которые отвечают почти за 23,5 миллиона параметров. Чтобы показать архитектуру на изображении, я буду использовать исходную 34-уровневую модель конфигурации, представленную в исходной статье.

Руки!

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

Работа с данными:

Чтобы начать проект, нам понадобятся данные, и, как упоминалось ранее, данные, которые мы будем использовать, взяты из набора данных, содержащего изображения с огнем или без него. Название набора данных: Обнаружение пожара с помощью камеры наблюдения на дорогах, опубликованное Роханом Роем на платформе Kaggle, содержит 10 003 изображения (настоящие и синтетические) и разбито на 3 папки: Тест , Поезд и Вали. В каждой из папок есть изображения, разделенные такими ярлыками, как огонь и не огонь. Набор данных был разделен в пропорции 3:1:1, где обучающий набор данных содержит наибольшее количество данных. Тем не менее, взгляните на примеры, обратите внимание, что последнее изображение показывает, как будет выглядеть синтетическое изображение.

Что ж, теперь, когда у нас есть данные, мы можем загрузить их в нашу записную книжку и использовать для обучения наших данных, и, наконец, когда нам нужно будет снова тренироваться, просто снова используйте загрузку в записную книжку, верно? Что ж, это может хорошо работать, если мы тренируем все модели в одном блокноте сразу, но, допустим, вы хотите разделить обучение в разных блокнотах (чтобы все было более организованно) или даже сделать этот же проект с другом, это было бы интереснее, чтобы эти данные можно было хранить и собирать в одном месте. Поэтому, чтобы я беспокоился о сохранении данных только один раз, я использовал отличную платформу MLOps, созданную Weights & Biases, которую я буду ласково называть WandB или W&B. Через платформу я могу сохранять данные в виде артефакта, и всякий раз, когда мне нужно их использовать, я просто получаю к ним доступ через блокнот и быстро загружаю их. Таким образом, я решил смешать все данные, которые изначально были в отдельных файлах, чтобы потом снова разделить их, но более интересным образом.

Загрузить данные и сохранить их

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

args = {
    "train_dataset": "Train",
    "val_dataset": "Vali",
    "test_dataset": "Test",
    "project_name": "fire_detection_classifier",
    "artifact_name": "images_raw_data"
}

Затем я начал создавать артефакт и отправлять данные по тому же пути на платформе:

# create an artifact for all the raw data
run = wandb.init(entity="euripedes",project=args["project_name"], job_type="fetch_data")

# create an artifact for all the raw data
raw_data = wandb.Artifact(args["artifact_name"], type="raw_data")

# grab the list of images that we'll be describing
logger.info("[INFO] loading images...")
train_imagePaths = list(paths.list_images(args["train_dataset"]))
val_imagePaths = list(paths.list_images(args["val_dataset"]))
test_imagePaths = list(paths.list_images(args["test_dataset"]))

# append all images to the artifact
for img in train_imagePaths:
  train_label = img.split(os.path.sep)
  raw_data.add_file(img, name=os.path.join(train_label[-2], train_label[-1]))

for img in val_imagePaths:
  val_label = img.split(os.path.sep)
  raw_data.add_file(img, name=os.path.join(val_label[-2], val_label[-1]))

for img in test_imagePaths:
  test_label = img.split(os.path.sep)
  raw_data.add_file(img, name=os.path.join(test_label[-2], test_label[-1]))

# save artifact to W&B
run.log_artifact(raw_data)
run.finish()

После завершения выполнения вы можете проверить данные на странице вашего проекта на платформе, просто перейдите в раздел Проекты › Имя вашего проекта › Артефакты › Имя вашего артефакта, после чего вы можете выбрать вкладку >Файлы для проверки созданных папок, в моем случае их было две (одна для «пожарного» класса и другая для «непожарного» класса). Таким образом, всякий раз, когда нам нужно использовать данные, сохраненные в этом артефакте, мы можем использовать то, что указывает вкладка Использование того же пути, позже мы увидим, как это работает. Для справки, весь этот шаг был проделан в блокноте отдельно от учебных, и если вам это нужно, вы можете проверить его на github проекта.

Разделение данных:

После загрузки данных в учебный блокнот мы можем создать набор данных с помощью функции image_dataset_from_directory, принадлежащей TensorFlow Keras. Для его запуска достаточно указать каталог скачанного с W&B артефакта, то есть просто переменную, в которой сохранялась загрузка. Затем укажите размер пакета и размер изображения, и все готово. При доработке функция вернула мне, что найдено 10002 файла, принадлежащих 2-м классам.

Теперь, чтобы разделить данные на 80% обучения и 20% тестирования и проверки, нам просто нужно запустить:

all_batches = tf.data.experimental.cardinality(raw_wb)
valtest_dataset = raw_wb.take(all_batches // 5)
train_dataset = raw_wb.skip(all_batches // 5)
 
valtest_batches = tf.data.experimental.cardinality(valtest_dataset)
test_dataset = valtest_dataset.take(valtest_batches // 5)
validation_dataset = valtest_dataset.skip(valtest_batches // 5)

В результате у нас есть данные, разделенные следующим образом:

Использование некоторого расширения данных:

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

data_augmentation = tf.keras.Sequential([
  tf.keras.layers.RandomFlip('horizontal'),
  tf.keras.layers.RandomRotation(0.2),
])

Давайте посмотрим на некоторые результаты:

Создание модели:

Мы создадим базовую модель предварительно обученной модели ResNet50, что очень просто, если мы используем параметры приложений TensorFlow Keras. Для этого вы можете запустить:

IMG_SHAPE = IMG_SIZE + (3,)
model_resNet50 = tf.keras.applications.ResNet50(input_shape=IMG_SHAPE,
                                         include_top=False,
                                         weights='imagenet')

Поступая таким образом, вы создаете экземпляр модели ResNet50 с предварительно загруженными весами, обученными на ImageNet (все обученные модели использовали веса из ImageNet Challenge), и не включаете верхние слои модели, поскольку мы собираемся их обучать. . Как только мы используем include_top=False, нам нужно указать форму ввода, поэтому, следуя рекомендациям для этой модели, мы используем (224,224,3) в качестве формы ввода.

Извлечение функций

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

model_resNet50.trainable = False

При этом почти 23,5 миллиона обучаемых параметров превращаются в необучаемые, вы можете проверить это, запустив model_resNet50.summary() до и после.

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

global_average_layer = tf.keras.layers.GlobalAveragePooling2D()
feature_batch_average = global_average_layer(feature_batch)
print(feature_batch_average.shape)
 
prediction_layer = tf.keras.layers.Dense(2,"softmax")
prediction_batch = prediction_layer(feature_batch_average)
print(prediction_batch.shape)

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

inputs = tf.keras.Input(shape=(224, 224, 3))
x = data_augmentation(inputs)
x = tf.keras.applications.mobilenet.preprocess_input(x)
x = model_resNet50(x, training=False)
x = global_average_layer(x)
x = tf.keras.layers.Dropout(0.5)(x)
outputs = prediction_layer(x)
model_resNet50 = tf.keras.Model(inputs, outputs)

Мы вызываем training=False, чтобы сохранить слои BatchNormalization в режиме вывода, иначе обновления, примененные к необучаемым весам, испортят то, чему научилась модель. С этим мы можем скомпилировать модель и обучить ее.

learning_rate = 0.0001
model_resNet50.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
              loss="categorical_crossentropy",
              metrics=['accuracy'])
 
tracker = EmissionsTracker(log_level="critical")
tracker.start()
 
print("[INFO] training head...")
history = model_resNet50.fit(train_dataset,
                    validation_data=validation_dataset,
                    batch_size=32,
                    epochs=20,
                    verbose=1)

emissions = tracker.stop()

Конфигурация трекера до и после обучения модели — это то, как мы можем использовать Codecarbon для получения потребления энергии и выброса CO2 во время нашего обучения, очень интересная и полезная функция!

Точная настройка

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

IMG_SHAPE = IMG_SIZE + (3,)
base_model = tf.keras.applications.ResNet50(input_shape=IMG_SHAPE,
                                         include_top=False,
                                         weights='imagenet')

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

class FCHeadNet:
  @staticmethod
  def build(baseModel, classes, neurons):
    # initialize the head model that will be placed on top of
    # the base, then add a FC layer
    headModel = baseModel.output
    headModel = Flatten(name="flatten")(headModel)
    headModel = Dense(neurons, activation="relu")(headModel)
    headModel = Dropout(0.5)(headModel)
 
    # add a softmax layer
    headModel = Dense(classes, activation="softmax")(headModel)
 
    # return the model
    return headModel

Затем мы собираем слой, как мы делали это ранее:

inputs = tf.keras.Input(shape=(224, 224, 3))
x = data_augmentation(inputs)
x = tf.keras.applications.resnet50.preprocess_input(x)
outputs = base_model(x, training=False)
base_model = tf.keras.Model(inputs, outputs)

И теперь у нас есть базовая модель ResNet50, готовая к использованию другой головки, поэтому давайте закончим модель головы, чтобы мы могли соединить эти две. Сначала мы используем функцию для создания новой (пока простой) головки, а затем используем ее в качестве вывода для новой модели:

headModel = FCHeadNet.build(base_model, len(class_names), 256)
 
ft_resNet50_model=Model(inputs=base_model.input,
                        outputs=headModel)

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

for layer in ft_resNet50_model.layers[4].layers:
  layer.trainable = False

Теперь мы можем скомпилировать и обучить его:

opt = RMSprop(learning_rate=0.0001)
 
ft_resNet50_model.compile(loss="categorical_crossentropy", 
                       optimizer=opt,
                       metrics=["accuracy"])
 
tracker_1 = EmissionsTracker(log_level="critical")
tracker_1.start()
 
print("[INFO] training head...")
history = ft_resNet50_model.fit(train_dataset,
                    validation_data=validation_dataset,
                    batch_size=32,
                    epochs=3,
                    verbose=1)
 
emissions = tracker_1.stop()

После этого мы можем перейти к следующему шагу разморозки некоторых слоев. Для этой модели я решил разморозить 20 последних слоев, содержащих 5 сверточных слоев. Я сделал это, запустив:

for layer in ft_resNet50_model.layers[4].layers[155:]:
  layer.trainable = True

И теперь мы снова компилируем и обучаем модель, давая ей больше времени для обновления этих последних слоев:

opt = SGD(learning_rate=0.0001)
ft_resNet50_model.compile(loss="categorical_crossentropy", 
              optimizer=opt,
              metrics=["accuracy"])
tracker_2 = EmissionsTracker(log_level="critical")
tracker_2.start()
 
print("[INFO] training head...")
history = ft_resNet50_model.fit(train_dataset,
                    validation_data=validation_dataset,
                    batch_size=32,
                    epochs=10,
                    verbose=1)
 
emissions = tracker_2.stop()

Теперь давайте посмотрим на результаты тестирования обоих методов.

Результаты

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

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

Отлично! Но я уже говорил вам, что мы обучили 4 модели, так что давайте посмотрим, насколько хороша модель ResNet50 по сравнению с другой. Закончив обучение и оценку модели, я загрузил ее в WandB, чтобы сохранить ее и иметь возможность легко сравнивать модели:

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

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

Энергопотребление и выбросы CO2:

Энергопотребление и выбросы CO2:

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

Большой!

Здесь — вторая часть этой статьи, где я использовал платформу Edge Impulse для создания подобного проекта!

На этом статья заканчивается, спасибо, что дочитали до сюда! Не стесняйтесь обращаться ко мне за любыми отзывами, я впервые пишу здесь на Medium, поэтому я надеюсь, что вам понравится это так же, как и мне.

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

Если вам интересно узнать о платформе Weight and Biases, загляните в другой проект в репозитории и узнайте о его интересных функциях!

Рекомендации













https://medium.com/@mygreatlearning/everything-you-need-to-know-about-vgg16-7315defb5918#:~:text=with%20transfer%20learning.-,VGG16%20Architecture,layers%20i.e .%2C%20learnable%20parameters%20layer.



https://serokell.io/blog/guide-to-transfer-learning