Искусственные нейронные сети (ИНС) — мощный инструмент в области компьютерного зрения, особенно в задачах классификации изображений. Эта область применения была одной из первых, для которой ИНС были разработаны. Например, перцептрон Розенблатта [1], созданный в 1957 году, является одним из самых ранних примеров ИНС, способной классифицировать изображения.
Свёрточные нейронные сети (СНС) [2] стали особенно популярными благодаря их способности эффективно обрабатывать изображения. Они используют механизмы, подобные тем, которые используются человеческим мозгом для обнаружения форм и текстур, что делает их идеальными для задач классификации изображений.
Однако выбор оптимальной архитектуры СНС может быть сложной задачей. Необходимо найти баланс между высокой точностью классификации и эффективным использованием ресурсов. Это включает в себя настройку глубины сети, размера фильтров и других параметров. В 2019 году команда исследователей из Google AI представила решение этой проблемы. Они разработали серию архитектур моделей под названием EfficientNet [3]. Эти модели отличаются высокой степенью эффективности и легко настраиваются. Они позволяют классифицировать изображения с высокой точностью, при этом потребляя минимальное количество ресурсов. EfficientNet стало значительным шагом вперед в развитии ИНС для классификации изображений и продолжает быть актуальным до сих пор.
Особенности EfficientNet
Архитектура EfficientNet представлена набором готовых к использованию моделей. Выбор зависит от требуемой точности, доступных ресурсов для обучения и разрешения входных изображений. Модели маркируются от B0 (самая простая) до B7 (самая «сильная»).
Структурная схема модели EfficientNetB0:
Сводная таблица входных разрешений для моделей EfficientNet:
Имя модели |
Входное разрешение |
EfficientNetB0 |
224х224 |
EfficientNetB1 |
240х240 |
EfficientNetB2 |
260х260 |
EfficientNetB3 |
300х300 |
EfficientNetB4 |
380х380 |
EfficientNetB5 |
456х456 |
EfficientNetB6 |
528х528 |
EfficientNetB7 |
600х600 |
Из таблицы видно, что модели EfficientNet принимают на вход изображения с разрешением, которое кратно 8.
Вот сравнительный график зависимости точности разных моделей от количества параметров [3]:
Кроме этого, представленные модели позволяют использовать готовые веса, которые берутся из предобученных моделей, основанных на базе данных ImageNet [4]. Это позволяет значительно ускорить обучение итоговой модели, так как обучение с нуля требует значительно больше ресурсов.
Рассмотрим на примере EfficientNetB0 основные параметры, которые доступны для настройки [5]:
keras.applications.EfficientNetB0(
include_top=True,
weights="imagenet",
input_tensor=None,
input_shape=None,
pooling=None,
classes=1000,
classifier_activation="softmax",
**kwargs
)
include_top
— указывает, следует ли использовать готовый или пользовательский выходной слой. По умолчанию задано значение True
.
Weights
— может быть одним из следующих вариантов: None
(инициализация весов случайными значениями), "imagenet"
(готовые веса на основе ImageNet), или путь к файлу весов для загрузки. По умолчанию используется "imagenet"
.
input_tensor
— Необязательный параметр, который позволяет использовать тензор Keras (т. е. вывод layers.Input()
) в качестве входных данных для модели.
input_shape
— необязательный параметр, который указывает формат входного изображения. Указывается только в том случае, если include_top
имеет значение False
. Должен представлять собой кортеж из трёх чисел (img_shape = (height, width, channels)
).
pooling
— необязательный параметр, который позволяет настроить Pooling-слои модели. Нужно указывать, когда include_top
имеет значение False
. По умолчанию None
. Есть три доступных значения для аргумента:
None
означает, что выходные данные модели будут выходными данными 4D-тензора последнего свёрточного слоя.avg
означает, что к выходным данным последнего свёрточного слоя будет применён Pooling-слой c функцией усреднения, и, таким образом, выходные данные модели будут представлять собой двумерный тензор.max
означает, что будет применен Pooling-слой с функцией максимума.
classes
— необязательный параметр, который указывает количество классов для классификации изображений. Указывается только в том случае, если include_top
равен True
и аргумент weights
не указан. 1000 — это количество классов ImageNet. По умолчанию 1000.
classifier_activation
— активационная функция выходного слоя. Принимает в качестве аргумента строку или вызываемый объект. Игнорируется, если include_top=True
. По умолчанию выбрана функция "softmax"
. При загрузке предварительно подготовленных весов classifier_activation
может быть только "None"
или "softmax"
.
Вызов функции keras.applications.EfficientNetB0()
возвращает модели объект, который можно использовать для создания итоговой пользовательской модели.
Пример использования EfficientNet
В одной из прошлых статей мы разбирались в вопросе установки и настройки Tensorflow [5]. Если Tensorflow у вас ещё не настроен, то предлагаю сначала этим заняться. А теперь вернёмся к EfficientNet.
В качестве наглядного примера давайте классифицируем изображения кошек и собак. Обучающую выборку можно взять с сайта Microsoft [7].
В общей директории с вашим скриптом создаём папку «PetImages» и выгружаем туда содержимое архива (папки «Cat» и «Dog»). Обязательно удалите посторонние файлы и изображения с нулевым размером. Неправильные файлы могут нарушить обучение. А вот и сам основной скрипт:
Подключаем необходимые библиотеки:
import tensorflow as tf
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.optimizers import Adamax
from tensorflow.keras import regularizers
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.models import Model
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import os
import seaborn as sns
import logging
# В utils мы реализуем дополнительные функции для визуализации процессора обучения
import utils
# Настраиваем логирование
logging.getLogger("tensorflow").setLevel(logging.ERROR)
# Настраиваем стиль графиков
sns.set_style('darkgrid')
# Проверяем подходит ли наша GPU для tensorflow
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
try:
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
logical_gpus = tf.config.list_logical_devices('GPU')
print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
except RuntimeError as e:
print(e)
Функции
# Рисует график обучения
def tr_plot(tr_data, start_epoch):
#Plot the training and validation data
tacc = tr_data.history['accuracy']
tloss = tr_data.history['loss']
vacc = tr_data.history['val_accuracy']
vloss = tr_data.history['val_loss']
Epoch_count = len(tacc) + start_epoch
Epochs = []
for i in range (start_epoch, Epoch_count):
Epochs.append(i + 1)
index_loss = np.argmin(vloss)# this is the epoch with the lowest validation loss
val_lowest = vloss[index_loss]
index_acc = np.argmax(vacc)
acc_highest = vacc[index_acc]
plt.style.use('fivethirtyeight')
sc_label = 'Лучшая эпоха= '+ str(index_loss + 1 + start_epoch)
vc_label = 'Лучшая эпоха= '+ str(index_acc + 1 + start_epoch)
fig,axes=plt.subplots(nrows=1, ncols = 2, figsize = (20,8))
axes[0].plot (Epochs,tloss, 'r', label = 'Потери при обучении')
axes[0].plot (Epochs,vloss,'g',label='Потери при валидации' )
axes[0].scatter (index_loss + 1 + start_epoch,val_lowest, s = 150, c = 'blue', label = sc_label)
axes[0].set_title('Потери при валидации и обучении')
axes[0].set_xlabel('Эпохи')
axes[0].set_ylabel('Потери')
axes[0].legend()
axes[1].plot (Epochs,tacc,'r', label = 'Точность при обучении')
axes[1].plot (Epochs,vacc,'g', label = 'Точность при валидации')
axes[1].scatter(index_acc + 1 + start_epoch, acc_highest, s = 150, c = 'blue', label = vc_label)
axes[1].set_title ('Точность при валидации и обучении')
axes[1].set_xlabel ('Эпохи')
axes[1].set_ylabel ('Точность')
axes[1].legend()
plt.tight_layout
#plt.style.use('fivethirtyeight')
plt.show()
# Создаем data_frame, в котором название папки является меткой класса изображений (коты и собаки в нашем случае)
# Каждому элементу data_frame будет присвоена метка (DX) и название файла (image)
# Функция возвращает объект frame
def load_data_frame(sdir):
classlist = os.listdir(sdir)
filepaths = []
labels = []
for klass in classlist:
classpath=os.path.join(sdir, klass)
flist=os.listdir(classpath)
for f in flist:
fpath = os.path.join(classpath, f)
filepaths.append( fpath.replace('\\', '/') )
labels.append(klass)
Fseries=pd.Series( filepaths, name = 'image' )
Lseries=pd.Series(labels, name = 'dx')
return pd.concat([Fseries, Lseries], axis=1)
Подготавливаем обучающие данные для модели:
# Размер картинки для подачи в модели
height = 224
width = 224
channels = 3
# Размер пачки для обучения
batch_size = 20
# Размер пачки для валидации
test_batch_size = 50
# Инициализаци рандом: None - всегда рандом, Число - повторяемый рандом
my_random = None
# Загружаем сгенерированные картинки (картинка должны лежать по папкам с названием их DX)
df2 = load_data_frame ("./PetImages/")
# Разделяем выборку на обучающую, тестовую и валидационную (случайным образом)
train_df, test_df = train_test_split (df2, train_size= .9, shuffle = True, random_state = my_random)
valid_df, test_df = train_test_split (test_df, train_size= .5, shuffle = True, random_state = my_random)
# Задаем параметры входящей картинки
img_shape = (height, width, channels)
img_size = (height, width)
length = len(test_df)
# выводим найденное число n
test_steps = int(length/test_batch_size)
print ( 'test batch size: ' ,test_batch_size, ' test steps: ', test_steps)
# C помощью ImageDataGenerator () можно аугментировать изображения прямо во время обучения, но аугментация — это отдельная тема
trgen = ImageDataGenerator()
# Генератор для тестовой выборки
tvgen = ImageDataGenerator()
# Выборка для обучения модели
train_gen = trgen.flow_from_dataframe ( train_df, directory = None, x_col = "image", y_col = "dx", target_size = img_size, class_mode = 'categorical',
color_mode='rgb', shuffle=True, batch_size=batch_size)
# Выборка для тестирования сети после обучения
test_gen = tvgen.flow_from_dataframe ( test_df, directory = None, x_col= "image", y_col = "dx", target_size = img_size, class_mode = 'categorical',
color_mode='rgb', shuffle=False, batch_size=test_batch_size)
# Выборка для тестирования сети во время обучения
valid_gen = tvgen.flow_from_dataframe ( valid_df, directory = None, x_col="image", y_col = "dx", target_size = img_size, class_mode = 'categorical',
color_mode='rgb', shuffle = True, batch_size = batch_size)
Создаём архитектуру модели:
# Получаем метки классов
classes = list (train_gen.class_indices.keys())
class_count = len(classes)
train_steps = np.ceil(len(train_gen.labels)/batch_size)
# Задаем имя модели
model_name = 'EfficientNetB0'
# Генерируем экземпляр модели EfficientNetB0
base_model = tf.keras.applications.EfficientNetB0(include_top = False, weights = "imagenet", input_shape = img_shape, pooling = 'max')
# Создаем выходной слой
x = base_model.output
x = tf.keras.layers.BatchNormalization(axis = -1, momentum = 0.99, epsilon = 0.001 )(x)
x = Dense(256, kernel_regularizer = regularizers.l2(l = 0.016), activity_regularizer = regularizers.l1(0.006),
bias_regularizer = regularizers.l1(0.006), activation = 'relu')(x)
x = Dropout(rate = .45, seed = my_random)(x)
#Создаем выходной полносвязный слой и присоединяем его к предыдущим слоям (количество нейронов совпадает с количеством классов
output = Dense(class_count, activation = 'softmax')(x)
# Собираем модель вместе
model = Model(inputs = base_model.input, outputs = output)
# Компилируем модель
model.compile(Adamax(lr = .001), loss = 'categorical_crossentropy', metrics = ['accuracy'])
Обучаем модель:
# Задаем параметры обучения
epochs = 15 # Количество эпох
patience = 1 # количество эпох, в течение которых необходимо отрегулировать lr, если отслеживаемое значение не улучшится
stop_patience = 6 # количество эпох ожидания перед остановкой обучения, если отслеживаемое значение не улучшится
threshold = .9
factor = .5
dwell = True # если True и отслеживаемая метрика не улучшаются по сравнению с текущей эпохой, возвращают веса модели к весам предыдущей эпохи.
freeze = False #
ask_epoch = 10 # количество эпох, которые нужно выполнить, прежде чем спросить, хотите ли вы остановить обучение
batches = train_steps
# utils.LRA реализует вывод информации прямо в процессе обучения
# Об этом стоит рассказать подробнее, но это тема для отдельной статьи
callbacks = [utils.LRA(model = model,
base_model = base_model,
patience=patience,
stop_patience = stop_patience,
threshold = threshold,
factor = factor,
dwell = dwell,
batches = batches,
initial_epoch = 0,
epochs = epochs,
ask_epoch = ask_epoch )]
# Запускаем обучение модели и сохраняем историю обучения
history = model.fit (x = train_gen, epochs = epochs, verbose = 0, callbacks = callbacks, validation_data = valid_gen, validation_steps = None, shuffle = False, initial_epoch = 0)
# Рисуем график обучения и выводим
tr_plot(history,0)
# Проверяем точность модели на тестовой выборке и выводим результат тестирования
save_dir = './'
subject = 'Cat and Dog'
acc = model.evaluate( test_gen, batch_size = test_batch_size, verbose = 1, steps=test_steps, return_dict = False)[1]*100
msg = f'accuracy on the test set is {acc:5.2f} %'
utils.print_in_color(msg, (0,255,0),(55,65,80))
# Сохраняем модель в файл, его потом можно загрузить и использовать без обучения для классификации изображений
save_id = str (model_name + '-' + subject +'-'+ str(acc)[:str(acc).rfind('.')+3] + '.h5')
save_loc = os.path.join(save_dir, save_id)
model.save(save_loc)
generator = train_gen
scale = 1
result = utils.saver(save_dir, model, model_name, subject, acc, img_size, scale, generator)
Содержимое файла Utils.py
# -*- coding: utf-8 -*-
"""
Created on Tue Dec 20 02:45:02 2022
@author: Heigrast
"""
import tensorflow as tf
from tensorflow import keras
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os
import seaborn as sns
from sklearn.metrics import confusion_matrix, classification_report
import time
# Выводит информацию о процессе обучения
def print_info( test_gen, preds, print_code, save_dir, subject ):
class_dict = test_gen.class_indices
labels = test_gen.labels
file_names = test_gen.filenames
error_list = []
true_class = []
pred_class = []
prob_list = []
new_dict = {}
error_indices = []
y_pred = []
for key,value in class_dict.items():
new_dict[value] = key # dictionary {integer of class number: string of class name}
# store new_dict as a text fine in the save_dir
classes = list(new_dict.values()) # list of string of class names
errors = 0
for i, p in enumerate(preds):
pred_index = np.argmax(p)
true_index = labels[i] # labels are integer values
if pred_index != true_index: # a misclassification has occurred
error_list.append(file_names[i])
true_class.append(new_dict[true_index])
pred_class.append(new_dict[pred_index])
prob_list.append(p[pred_index])
error_indices.append(true_index)
errors = errors + 1
y_pred.append(pred_index)
if print_code != 0:
if errors > 0:
if print_code>errors:
r = errors
else:
r = print_code
msg ='{0:^28s}{1:^28s}{2:^28s}{3:^16s}'.format('Filename', 'Predicted Class' , 'True Class', 'Probability')
print_in_color(msg, (0,255,0),(55,65,80))
for i in range(r):
split1 = os.path.split(error_list[i])
split2 = os.path.split(split1[0])
fname = split2[1] + '/' + split1[1]
msg = '{0:^28s}{1:^28s}{2:^28s}{3:4s}{4:^6.4f}'.format(fname, pred_class[i],true_class[i], ' ', prob_list[i])
print_in_color(msg, (255,255,255), (55,65,60))
#print(error_list[i] , pred_class[i], true_class[i], prob_list[i])
else:
msg = 'With accuracy of 100 % there are no errors to print'
print_in_color(msg, (0,255,0),(55,65,80))
if errors > 0:
plot_bar = []
plot_class = []
for key, value in new_dict.items():
count = error_indices.count(key)
if count != 0:
plot_bar.append(count) # list containg how many times a class c had an error
plot_class.append(value) # stores the class
fig=plt.figure()
fig.set_figheight(len(plot_class)/3)
fig.set_figwidth(10)
plt.style.use('fivethirtyeight')
for i in range(0, len(plot_class)):
c = plot_class[i]
x = plot_bar[i]
plt.barh(c, x, )
plt.title( ' Errors by Class on Test Set')
y_true = np.array(labels)
y_pred = np.array(y_pred)
if len(classes)<= 30:
# create a confusion matrix
cm = confusion_matrix(y_true, y_pred )
length = len(classes)
if length < 8:
fig_width = 8
fig_height = 8
else:
fig_width = int(length * .5)
fig_height = int(length * .5)
plt.figure(figsize = (fig_width, fig_height))
sns.heatmap(cm, annot = True, vmin = 0, fmt = 'g', cmap = 'Blues', cbar = False)
plt.xticks(np.arange(length) + .5, classes, rotation = 90)
plt.yticks(np.arange(length) + .5, classes, rotation =0)
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.title("Confusion Matrix")
plt.show()
clr = classification_report(y_true, y_pred, target_names=classes)
print("Classification Report:\n----------------------\n", clr)
# Сохраняет результаты обучения в файл
def saver(save_path, model, model_name, subject, accuracy, img_size, scalar, generator):
# first save the model
save_id = str (model_name + '-' + subject +'-'+ str(accuracy)[:str(accuracy).rfind('.')+3] + '.h5')
model_save_loc = os.path.join(save_path, save_id)
model.save(model_save_loc)
print_in_color ('model was saved as ' + model_save_loc, (0,255,0),(55,65,80))
# now create the class_df and convert to csv file
class_dict = generator.class_indices
height = []
width = []
scale = []
for i in range(len(class_dict)):
height.append(img_size[0])
width.append(img_size[1])
scale.append(scalar)
Index_series = pd.Series(list(class_dict.values()), name='class_index')
Class_series = pd.Series(list(class_dict.keys()), name='class')
Height_series = pd.Series(height, name='height')
Width_series = pd.Series(width, name='width')
Scale_series = pd.Series(scale, name='scale by')
class_df = pd.concat([Index_series, Class_series, Height_series, Width_series, Scale_series], axis=1)
csv_name ='class_dict.csv'
csv_save_loc = os.path.join(save_path, csv_name)
class_df.to_csv(csv_save_loc, index=False)
print_in_color ('class csv file was saved as ' + csv_save_loc, (0,255,0),(55,65,80))
return model_save_loc, csv_save_loc
def print_in_color(txt_msg, fore_tupple, back_tupple,):
#prints the text_msg in the foreground color specified by fore_tupple with the background specified by back_tupple
#text_msg is the text, fore_tupple is foreground color tupple (r,g,b), back_tupple is background tupple (r,g,b)
rf,gf,bf = fore_tupple
rb,gb,bb = back_tupple
msg = '{0}' + txt_msg
mat = '\33[38;2;' + str(rf) +';' + str(gf) + ';' + str(bf) + ';48;2;' + str(rb) + ';' +str(gb) + ';' + str(bb) +'m'
print(msg.format(mat), flush = True)
print('\33[0m', flush = True) # returns default print color to back to black
return
# Класс колбеков, он дает возможность выводить доп информацию во время обучения через переопределение методов родителя
class LRA(keras.callbacks.Callback):
def __init__(self,model, base_model, patience, stop_patience, threshold, factor, dwell, batches, initial_epoch, epochs, ask_epoch):
super(LRA, self).__init__()
self.model=model
self.base_model = base_model
self.patience = patience # specifies how many epochs without improvement before learning rate is adjusted
self.stop_patience = stop_patience # specifies how many times to adjust lr without improvement to stop training
self.threshold = threshold # specifies training accuracy threshold when lr will be adjusted based on validation loss
self.factor = factor # factor by which to reduce the learning rate
self.dwell = dwell
self.batches = batches # number of training batch to run per epoch
self.initial_epoch = initial_epoch
self.epochs = epochs
self.ask_epoch = ask_epoch
self.ask_epoch_initial = ask_epoch # save this value to restore if restarting training
# callback variables
self.count = 0 # how many times lr has been reduced without improvement
self.stop_count = 0
self.best_epoch = 1 # epoch with the lowest loss
self.initial_lr = float(tf.keras.backend.get_value(model.optimizer.lr)) # get the initial learning rate and save it
self.highest_tracc = 0.0 # set highest training accuracy to 0 initially
self.lowest_vloss = np.inf # set lowest validation loss to infinity initially
self.best_weights = self.model.get_weights() # set best weights to model's initial weights
self.initial_weights = self.model.get_weights() # save initial weights if they have to get restored
def on_train_begin(self, logs = None):
if self.base_model != None:
status=self.base_model.trainable
if status:
msg = 'initializing callback starting train with base_model trainable'
else:
msg = 'initializing callback starting training with base_model not trainable'
else:
msg = 'initialing callback and starting training'
print_in_color (msg, (244, 252, 3), (55,65,80))
msg = '{0:^8s}{1:^10s}{2:^9s}{3:^9s}{4:^9s}{5:^9s}{6:^9s}{7:^10s}{8:^8s}'.format('Epoch', 'Loss', 'Accuracy',
'V_loss','V_acc', 'LR', 'Next LR', 'Monitor', 'Duration')
print_in_color(msg, (244,252,3), (55,65,80))
self.start_time = time.time()
def on_train_end(self, logs = None):
stop_time = time.time()
tr_duration = stop_time- self.start_time
hours = tr_duration // 3600
minutes = (tr_duration - (hours * 3600)) // 60
seconds = tr_duration - ((hours * 3600) + (minutes * 60))
self.model.set_weights(self.best_weights) # set the weights of the model to the best weights
msg=f'Training is completed - model is set with weights from epoch {self.best_epoch} '
print_in_color(msg, (0,255,0), (55,65,80))
msg = f'training elapsed time was {str(hours)} hours, {minutes:4.1f} minutes, {seconds:4.2f} seconds)'
print_in_color(msg, (0,255,0), (55,65,80))
def on_train_batch_end(self, batch, logs=None):
acc = logs.get('accuracy')* 100 # get training accuracy
loss = logs.get('loss')
msg = '{0:20s}processing batch {1:4s} of {2:5s} accuracy= {3:8.3f} loss: {4:8.5f}'.format(' ', str(batch), str(self.batches), acc, loss)
print(msg, '\r', end='') # prints over on the same line to show running batch count
def on_epoch_begin(self,epoch, logs = None):
self.now = time.time()
def on_epoch_end(self, epoch, logs = None): # method runs on the end of each epoch
later = time.time()
duration = later-self.now
lr=float(tf.keras.backend.get_value(self.model.optimizer.lr)) # get the current learning rate
current_lr = lr
v_loss = logs.get('val_loss') # get the validation loss for this epoch
acc = logs.get('accuracy') # get training accuracy
v_acc = logs.get('val_accuracy')
loss = logs.get('loss')
# if training accuracy is below threshold adjust lr based on training accuracy
if acc < self.threshold:
monitor = 'accuracy'
if acc > self.highest_tracc: # training accuracy improved in the epoch
self.highest_tracc = acc # set new highest training accuracy
self.best_weights = self.model.get_weights() # traing accuracy improved so save the weights
self.count = 0 # set count to 0 since training accuracy improved
self.stop_count = 0 # set stop counter to 0
if v_loss < self.lowest_vloss:
self.lowest_vloss = v_loss
color = (0,255,0)
self.best_epoch=epoch + 1 # set the value of best epoch for this epoch
else:
# training accuracy did not improve check if this has happened for patience number of epochs
# if so adjust learning rate
if self.count >= self.patience -1: # lr should be adjusted
color = (245, 170, 66)
lr = lr * self.factor # adjust the learning by factor
tf.keras.backend.set_value(self.model.optimizer.lr, lr) # set the learning rate in the optimizer
self.count = 0 # reset the count to 0
self.stop_count = self.stop_count + 1 # count the number of consecutive lr adjustments
self.count = 0 # reset counter
if self.dwell:
self.model.set_weights(self.best_weights) # return to better point in N space
else:
if v_loss < self.lowest_vloss:
self.lowest_vloss = v_loss
else:
self.count=self.count +1 # increment patience counter
else: # training accuracy is above threshold so adjust learning rate based on validation loss
monitor = 'val_loss'
if v_loss < self.lowest_vloss: # check if the validation loss improved
self.lowest_vloss = v_loss # replace lowest validation loss with new validation loss
self.best_weights = self.model.get_weights() # validation loss improved so save the weights
self.count = 0 # reset count since validation loss improved
self.stop_count = 0
color=(0,255,0)
self.best_epoch = epoch + 1 # set the value of the best epoch to this epoch
else: # validation loss did not improve
if self.count >= self.patience-1: # need to adjust lr
color = (245, 170, 66)
lr = lr * self.factor # adjust the learning rate
self.stop_count = self.stop_count + 1 # increment stop counter because lr was adjusted
self.count = 0 # reset counter
tf.keras.backend.set_value(self.model.optimizer.lr, lr) # set the learning rate in the optimizer
if self.dwell:
self.model.set_weights(self.best_weights) # return to better point in N space
else:
self.count = self.count + 1 # increment the patience counter
if acc > self.highest_tracc:
self.highest_tracc = acc
msg=f'{str(epoch+1):^3s}/{str(self.epochs):4s} {loss:^9.3f}{acc*100:^9.3f}{v_loss:^9.5f}{v_acc*100:^9.3f}{current_lr:^9.5f}{lr:^9.5f}{monitor:^11s}{duration:^8.2f}'
print_in_color (msg,color, (55,65,80))
with open('epoch statistics.txt', 'a') as epoch_stats:
epoch_stats.write(msg + '\n')
if self.stop_count > self.stop_patience - 1: # check if learning rate has been adjusted stop_count times with no improvement
msg = f' training has been halted at epoch {epoch + 1} after {self.stop_patience} adjustments of learning rate with no improvement'
print_in_color(msg, (0,255,255), (55,65,80))
self.model.stop_training = True # stop training
else:
if self.ask_epoch != None:
if epoch + 1 >= self.ask_epoch:
msg ='enter H to halt ,F to fine tune model, or an integer for number of epochs to run then ask again'
print_in_color(msg, (0,255,255), (55,65,80))
ans=input('')
if ans =='H' or ans=='h':
msg =f'training has been halted at epoch {epoch + 1} due to user input'
print_in_color(msg, (0,255,255), (55,65,80))
self.model.stop_training = True # stop training
elif ans == 'F' or ans=='f':
msg ='setting base_model as trainable for fine tuning of model'
self.base_model.trainable = True
print_in_color(msg, (0, 255,255), (55,65,80))
msg='{0:^8s}{1:^10s}{2:^9s}{3:^9s}{4:^9s}{5:^9s}{6:^9s}{7:^10s}{8:^8s}'.format('Epoch', 'Loss', 'Accuracy',
'V_loss','V_acc', 'LR', 'Next LR', 'Monitor', 'Duration')
print_in_color(msg, (244,252,3), (55,65,80))
self.count = 0
self.stop_count = 0
self.ask_epoch = epoch + 1 + self.ask_epoch_initial
else:
ans=int(ans)
self.ask_epoch += ans
msg = f' training will continue until epoch ' + str(self.ask_epoch)
print_in_color(msg, (0, 255,255), (55,65,80))
msg = '{0:^8s}{1:^10s}{2:^9s}{3:^9s}{4:^9s}{5:^9s}{6:^9s}{7:^10s}{8:^8s}'.format('Epoch', 'Loss', 'Accuracy',
'V_loss','V_acc', 'LR', 'Next LR', 'Monitor', 'Duration')
print_in_color(msg, (244,252,3), (55,65,80))
Если вы правильно настроили среду, то после запуска скрипта у вас должна появиться в консоли следующая картина:
В консоль выводится информация об обучении каждой текущей пачки изображений. А после завершения всего обучения система строит график:
В представленном примере мы использовали модель EfficientNetB0. Остальные модели EfficientNet применяются аналогично, только нужно изменить параметры входных изображений через следующие константы:
# Размер картинки для подачи в модели
height = 224
width = 224
channels = 3
Другие примеры использования EfficientNet
Кошечки и собачки — это, конечно, хорошо, но для демонстрации реальных преимуществ архитектуры EfficientNet давайте рассмотрим другой пример использования архитектуры:
Model_name = 'EfficientNetB3'
base_model = tf.keras.applications.EfficientNetB3(include_top = False, weights = "imagenet", input_shape = img_shape, pooling = 'max')
x = base_model.output
x = tf.keras.layers.BatchNormalization(axis = -1, momentum = 0.99, epsilon = 0.001 )(x)
x = Dense(256, kernel_regularizer = regularizers.l2(l = 0.016), activity_regularizer = regularizers.l1(0.006), bias_regularizer = regularizers.l1(0.006), activation = 'relu')(x)
x = Dropout(rate = .45, seed = my_random)(x)
output = Dense(class_count, activation = 'softmax')(x)
model = Model(inputs = base_model.input, outputs = output)
model.compile(Adamax(lr = .001), loss = 'categorical_crossentropy', metrics = ['accuracy'])
Эта модель отличается только применением EfficientNetB3, в остальном всё то же самое.
А вот пример архитектуры свёрточной сети без EfficientNet. Эта модель показала точность всего 77 % при решении задачи классификации кожных образований [8]:
# Set the CNN model
# my CNN architecture is In -> [[Conv2D->relu]*2 -> MaxPool2D -> Dropout]*2 -> Flatten -> Dense -> Dropout -> Out
input_shape = (75, 100, 3)
num_classes = 7
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),activation='relu',padding = 'Same',input_shape=input_shape))
model.add(Conv2D(32,kernel_size=(3, 3), activation='relu',padding = 'Same',))
model.add(MaxPool2D(pool_size = (2, 2)))
model.add(Dropout(0.25))
model.add(Conv2D(64, (3, 3), activation='relu',padding = 'Same'))
model.add(Conv2D(64, (3, 3), activation='relu',padding = 'Same'))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.40))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))
Приведенный выше скрипт генерирует следующую модель:
Вот её график обучения:
А вот график обучения нашей модели, основанной на EfficientNetB3:
Результаты применения EfficientNet налицо. Мы получили прирост точности классификации примерно на 10 %. А если провести больше экспериментов с настройками, то можно получить ещё большую точность.
И вот ещё один пример эффективности использования моделей EfficientNet [9]. Он тоже наглядно показывает преимущества архитектур.
Советы по использованию EfficientNet
Можно выделить несколько советов по использованию моделей:
Слои BatchNormalization нужно держать замороженными (не обучать). Если их ещё и сделать обучаемыми, то первая эпоха после разморозки значительно снизит точность [10].
В некоторых случаях может оказаться полезным разморозить (разрешить обучать) только часть слоёв вместо того, чтобы размораживать все. Это значительно ускорит точную настройку при переходе на более крупные модели, такие как B7.
Более крупные варианты EfficientNet (B3+) не гарантируют повышения производительности, особенно в задачах с меньшим количеством данных или классов. В таком случае, чем больший вариант EfficientNet выбран, тем сложнее настроить гиперпараметры.
Меньший размер пачки может положительно сказаться на точности проверки, возможно, из-за эффективной регуляризации.
Полезные ссылки
https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D1%86%D0%B5%D0%BF%D1%82%D1%80%D0%BE%D0%BD
https://keras.io/api/applications/efficientnet/#efficientnetb0-function
https://www.microsoft.com/en-us/download/details.aspx?id=54765
https://www.kaggle.com/code/sid321axn/step-wise-approach-cnn-model-77-0344-accuracy