Недавно я создал свой первый классификатор изображений, чтобы разделить фотографии дочери на две категории: одни, на которых она улыбается, и другие. Это простая проблема бинарной классификации, но, имея очень мало доступных данных и будучи новичком во всем, например, в материнстве и машинном обучении, мне пришлось столкнуться с несколькими проблемами на пути. Мне очень понравилось работать над этим приложением, и хотелось бы думать, что я кое-что узнал в процессе. Было очень удобно использовать ядро Kaggle, чтобы избавить меня от хлопот по настройке собственной локальной среды и получить доступ к графическому процессору, который был полезен при обучении моей модели. В этой статье я подробно расскажу о том, как создать это приложение, о проблемах, с которыми я столкнулся, и о том, как я их решил. Моя работа здесь адаптирована из этого замечательного поста, который демонстрирует, как классифицировать изображения собак и кошек - еще одну проблему бинарной классификации.
Небольшое замечание: Интернет может быть непростым местом, особенно для детей, поэтому, чтобы защитить изображения моей дочери, я преобразовал фотографии в акварельные рисунки (с помощью приложения Waterlogue), прежде чем использовать их в этой статье. Однако я использовал реальные изображения для обучения и тестирования модели.
Зачем использовать трансферное обучение для этой модели? Вместо того, чтобы обучать модель с нуля для случайной инициализации, мы можем добиться гораздо более быстрого прогресса, если загрузим предварительно обученные веса и модели и перенесем их в нашу собственную модель. Один из способов включения этого метода - избавиться от конечного выходного слоя и создать наш собственный модуль, который выводит желаемую классификацию, в моем случае это класс улыбается или not_smiling. Учитываются параметры всех слоев предварительно обученной модели. заморожены, и вам просто нужно обучить параметры, связанные с вашим собственным конечным выходным слоем. Основная причина, по которой я использую трансферное обучение для своего приложения, заключается в том, что, используя предварительно обученные веса, я все еще могу стремиться создать приложение спуска с небольшим помеченным набором данных . В очень полезном курсе по сверточной нейронной сети Эндрю Нг я узнал, что у вас могут быть такие свойства, как «trainable_weights» и «freeze», которые позволяют соответствующим образом изменять ранние слои. Я использовал trainable = 0 для предварительно обученной базовой модели, чтобы не пересчитывать эти активации каждый раз, когда я беру эпоху или прохожу через обучающий набор. Вы также можете заморозить меньшее количество слоев и обучить последующие слои в дополнение к выходному слою. Таким образом, в зависимости от объема данных у вас нет. замороженных слоев могло быть меньше и нет. слоев, обученных поверх базовой модели, может быть больше. Для получения более подробной информации о трансферном обучении я нашел очень полезными мини-курс Kaggle по глубокому обучению Дэна Беккера и курс Эндрю Нг по сверточной нейронной сети на Coursera.
Я начинаю свое приложение с импорта всех пакетов, которые собираюсь использовать.
import cv2 import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline import gc import os import random import seaborn as sns sns.set() from keras import backend as K
С самого начала я решил использовать для этой задачи Keras flow_from_directory (). Я создавал свой собственный набор данных и мог легко организовать свой каталог обучающих данных, чтобы он содержал подкаталоги для каждого класса изображений (необходимое условие для flow_from_directory ()). Однако при попытке создать такие же подкаталоги в папке ввода в Kaggle возникла кратковременная сбой. Я попробовал несколько способов создать структуру папок под входной папкой и попытаться загрузить изображения, но безрезультатно (проблемы для новичков!). Но вы можете сделать это очень легко в Kaggle. Чтобы сохранить структуру каталогов в наборе данных Kaggle, просто заархивируйте файлы и загрузите сжатый файл. Вы можете увидеть мою структуру папок в соседнем подкаталоге image-one для каждого класса. Классы в этом случае улыбаются, а не улыбаются. «Улыбка» содержала все фотографии, на которых моя дочь улыбается, а все остальные изображения были помещены в папку «Не улыбается». Поскольку у меня было очень мало данных и я все время пытался добавлять к ним при одновременном кодировании, я решил не создавать отдельную папку проверки, содержащую два класса. Вместо этого я использовал train_test_split и зарезервировал 20% для проверки. Таким образом, я мог просто добавить свои новые изображения в учебные классы и разделить их позже, не беспокоясь о случайном изменении процента разделения.
train_smiling_dir='../input/smile-detection-dataset/smile-detection/train/smiling/' train_not_smiling_dir='../input/smile-detection-dataset/smile-detection/train/not-smiling/' test_dir='../input/smile-detection-dataset/smile-detection/test/'
У меня было около 400 фотографий моей дочери, которые я мог использовать для обучения модели. Изначально я столкнулся с проблемой классового дисбаланса, но я смог быстро исправить это, сделав больше фотографий улыбающейся дочери - да, я знаю! Я выбрал легкий путь. Одна из моих ближайших целей в ближайшем будущем - поработать над настраиваемой функцией взвешенных потерь для решения этой проблемы.
image_size = 448
Я немного поигрался с image_size, попробовав 150 224 и, наконец, остановился на 448. Это дало мне лучшие результаты.
def create_img_name_list(path): listOfImages=[path+'{}'.format(i) for i in os.listdir(path)] return listOfImages
Я создал функцию create_img_name_list, которая принимает путь и возвращает список, содержащий имена файлов в каталоге.
#getting train and test images train_smiling=create_img_name_list(train_smiling_dir) train_not_smiling=create_img_name_list(train_not_smiling_dir) test_imgs=create_img_name_list(test_dir) train_imgs = train_smiling+train_not_smiling random.shuffle(train_imgs) del train_smiling del train_not_smiling gc.collect()
Используя эту функцию, я создаю два списка для двух классов, с которыми я работаю, улыбаясь и not_smiling. Я также создаю третий список для тестовых изображений. Затем создаются обучающие образы путем добавления списков улыбающихся и not_smiling. Затем я случайным образом перемешал обучающие изображения, что изменило порядок элементов. Без этого была бы куча улыбающихся изображений, за которыми следовала бы куча изображений not_smiling, а это не то, что нам нужно. Следовательно, перетасовка здесь важна.
#Viewing some images in our train images import matplotlib.image as mpimg for key in list(train_imgs)[0:4]: img=mpimg.imread(key) imgplot=plt.imshow(img) plt.show()
Затем я просмотрел некоторые изображения, загруженные в обучающий набор, и заметил, что изображения имеют разные размеры. Это главным образом потому, что в моем стремлении получить больше данных я просмотрел все видео о моей дочери и сделал скриншоты. В некоторых случаях мне даже приходилось вырезать мужа или лицо, чтобы не запутать модель. В результате получались изображения самых разных размеров. Приносим извинения за растачивание акварели! Я не знаю, видно ли это из соседних изображений, но эти 2 изображения (Образцы изображений 1 и 2 из обучающего набора) имеют очень разные размеры - второе больше первого. Не беспокойтесь, здесь в игру вступает моя следующая функция read_and_process_image.
#Resizing images nrows=image_size ncolumns=image_size channels=3
До этого мне нужно было определиться с размером моих изображений после обработки. Поскольку я использую цветные изображения, количество используемых каналов равно 3.
#Reading and processing image to an acceptable format for our model def read_and_process_image(train_images): """"" Returns X:array of resized images y:array of labels """"" X=[] y=[] for img in train_images: try: #for image in list_of_images: X.append(cv2.resize(cv2.imread(img,cv2.IMREAD_COLOR),(nrows,ncolumns),interpolation=cv2.INTER_CUBIC)) #get the labels if 'not-smiling' in img: y.append(0) elif 'smiling' in img: y.append(1) except Exception as e: print(e) return X,y
Команды cv2 используются для изменения размера изображений. Вы можете видеть ниже, что оба изображения теперь имеют одинаковый размер.
Если вы помните, я ранее создавал списки с полным путем к изображениям, с которыми мне нужно было бы работать. Поскольку моя структура каталогов содержала папки «улыбается» и «not_smiling», у меня есть эти тексты в именах изображений, принадлежащих к соответствующие классы. Я использую это, чтобы создать свой список y, присвоив 0 изображениям со словом «не улыбается» в их именах и 1 изображениям со словом «улыбается» в их именах.
X,y=read_and_process_image(train_imgs)
Я вызываю функцию read_and_process_image для создания массивов X и Y. Ниже приведены некоторые изображения после обработки.
#Lets view some of the pics plt.figure(figsize=(20,10)) columns = 5 for i in range(columns): plt.subplot(5 / columns + 1, columns, i + 1) plt.imshow(X[i])
Ниже я нанес на график частоту каждого из классов в моем наборе данных. В идеале мы хотим обучить нашу модель, используя равномерно сбалансированный набор данных, чтобы положительные и отрицательные обучающие примеры вносили равный вклад в потерю. 0 означает отсутствие улыбки, а 1 - класс улыбающихся. Я использовал seaborn для визуализации данных.
import seaborn as sns del train_imgs gc.collect() #Convert list to numpy array X = np.array(X) y = np.array(y) sns.countplot(y) plt.title('Labels for Smiling and Not-Smiling')
Затем я разделил свой набор данных для обучения на подмножества для обучения и проверки. Вы помните, что я не создавал набор данных для проверки до этого момента, потому что я постоянно добавлял изображения в свои папки и не хотел ошибаться относительно того, сколько я добавил для обучения и сколько для проверки. Я использовал train_test_split sklearn, чтобы назначить 20% моего набора данных для проверки. Вот код того же.
#Lets split the data into train and test set from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.20, random_state=2)
После создания приведенных выше списков обучения и проверки X и y больше не требуются. Я также назначил на этом этапе batch_size, который потребуется для нашей модели. Что касается batch_size, я поигрался со значениями 8, 16 и 32 и увидел, что 32 дает наилучшие результаты, поэтому остановился на этом. В какой-то момент, когда я назначил только 10% изображений моему подмножеству проверки, я получал ошибку «журналы локальной переменной», на которые ссылались перед назначением. Это произошло потому, что размер моего набора данных проверки был меньше, чем размер пакета, который я затем быстро исправил, изменив процент изображений, назначенных набору проверки.
if K.image_data_format() == 'channels_first': X_train = X_train.reshape(X_train.shape[0], channels, nrows, ncolumns) #X_test = X_test.reshape(X_test.shape[0], channels, img_rows, img_cols) input_shape = (channels, nrows, ncolumns) else: X_train = X_train.reshape(X_train.shape[0], nrows, ncolumns, channels) #X_test = X_test.reshape(X_test.shape[0], nrows, ncolumns, channels) input_shape = (nrows, ncolumns, channels)
Я напечатал K.image_data_format (), чтобы проверить, какое значение было в моем случае, и он показал каналы_последний. Следовательно, в моем сценарии используется input_shape (nrows, ncolumns, channels). Согласно этой очень полезной ссылке, которая углубляется в предмет - «Каналы последними: данные изображения представлены в трехмерном массиве, где последний канал представляет собой цветовые каналы, например [строки] [столбцы] [каналы]. "
Затем я использовал пакет keras для предварительно обученной модели InceptionResNetV2 с imagenet в качестве весов. Imagenet - это большая база данных изображений, используемая для распознавания изображений. При создании экземпляра модели keras веса загружаются автоматически (ссылка), и модель обучается с использованием набора данных imagenet. Я тоже пробовал модель Resnet50, но получил очень плохие результаты для своего сценария (точность около 40%). Поэтому решил придерживаться модели InceptionResNetV2. Назначенный здесь input_shape должен быть таким же, как input_shape ваших измененных изображений, то есть форма моих изображений после того, как я запустил его через свою функцию read_and_process_image.
from keras.applications import InceptionResNetV2 conv_base = InceptionResNetV2(weights='imagenet', include_top=False, input_shape=input_shape)
Затем я создал последовательную модель, добавил предварительно обученную модель InceptionResNetV2 и настроил верхний уровень.
from keras import layers from keras import models model = models.Sequential() model.add(conv_base) model.add(layers.Flatten()) model.add(layers.Dense(256, activation='relu')) model.add(layers.Dense(1, activation='sigmoid')) model.summary()
Затем я отобразил сводку модели, в которой был указан номер. настроенных параметров.
После этого я напечатал нет. обучаемых весов до и после я присваиваю trainable = False предварительно обученному слою. Поскольку я использую предварительно обученную сеть, веса для базового слоя уже изучены, поэтому я решил не обучать ее снова.
print('Number of trainable weights before freezing the conv base:', len(model.trainable_weights)) conv_base.trainable = False print('Number of trainable weights after freezing the conv base:', len(model.trainable_weights))
Нет. весов до замораживания составляет 492 (в основном веса из conv_base), а после замораживания нет. of trainable_weights равно 4.
Затем я скомпилировал модель и добавил параметры потерь и оптимизатора. Двоичная кросс-энтропия - это функция потерь, используемая для задач двоичной классификации, то есть там, где есть два помеченных класса, в моем случае улыбающийся и не улыбающийся. Для каждого примера должно быть одно значение с плавающей запятой на одно предсказание (ссылка). Подробнее о методике оптимизации адама вы можете прочитать здесь.
model.compile(loss='binary_crossentropy', optimizer='adam',metrics=['acc'])
Поскольку у меня не было достаточно данных, я знал, что мне нужно использовать увеличение изображения. Увеличение данных - один из методов, часто используемых для повышения производительности в системах компьютерного зрения. Я использовал ImageDataGenerator, который предоставляет основные методы увеличения данных, такие как диапазон поворота и горизонтальный переворот. Я особо не играл с гиперпараметрами при увеличении данных - еще один для списка дел.
#Lets create the augmentation configuration #This helps prevent overfitting, since we are using a small dataset from keras.preprocessing.image import ImageDataGenerator from keras.preprocessing.image import img_to_array, load_img train_datagen = ImageDataGenerator(rescale=1./255, #Scale the image between 0 and 1 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') val_datagen = ImageDataGenerator(rescale=1./255) #We do not augment validation data. we only perform rescale #Create the image generators train_generator = train_datagen.flow(X_train, y_train,batch_size=batch_size) val_generator = val_datagen.flow(X_val, y_val, batch_size=batch_size) #The training part #Training for 25 epochs with about 9 steps per epoch history = model.fit_generator(train_generator, steps_per_epoch=ntrain // batch_size, epochs=25, validation_data=val_generator, validation_steps=nval // batch_size, verbose=1)
Я получил точность около 84% за 25 эпох. Учитывая объем предоставленных данных, я очень доволен результатами.
Наконец, я был готов предсказать изображения в моем тестовом наборе данных.
#Now lets predict on the images of the test set X_test, y_test = read_and_process_image(test_imgs) x = np.array(X_test) test_datagen = ImageDataGenerator(rescale=1./255) i = 0 columns = 5 text_labels = [] plt.figure(figsize=(30,20)) for batch in test_datagen.flow(x, batch_size=1): pred = model.predict(batch) if pred > 0.5: text_labels.append('Smile detected!') else: text_labels.append('No smiles here.') plt.subplot(5 / columns + 1, columns, i + 1) plt.title(text_labels[i]) imgplot = plt.imshow(batch[0]) i += 1 if i % 10 == 0: break plt.show()
После запуска модели я получаю точность на 9 из 10 изображений. Вчера вечером я также запустил несколько измененных акварельных изображений и вуаля! это тоже сработало. Также неплохо было бы таким образом увеличить свой набор тренировочных данных. Я видел, что изображения, в которых есть несколько объектов в кадре или где моя дочь находится дальше, обычно оказываются неудачными. Со временем я думаю, что собираюсь добавить больше данных и сделать классы более сбалансированными, добавить настраиваемую функцию потерь и продолжить работу над оптимизацией гиперпараметров для точной настройки моей модели. Я также планирую расширить свое приложение и сделать его мультиклассовым классификатором, обнаруживать больше эмоций - вопли, голод, нюхание носа и т. Д. Если больше ничего не получится, сбор этих данных определенно будет забавным! Я хотел бы услышать ваши предложения, если у вас есть какие-либо способы улучшения этой модели для получения лучших результатов.
Помимо этого, я продолжаю сталкиваться с одной проблемой: мой набор данных Kaggle не может идентифицировать ядро. Из-за растущего характера моих данных я продолжаю удалять, изменять и добавлять в свой набор данных, и часто я сталкиваюсь с ужасной ошибкой FileNotFoundError. Один из способов, которым я справляюсь сейчас, - это перечисление содержимого каталога входного файла.
print(os.listdir("../input"))
В этих сценариях я обнаружил, что содержимое каталога было изменено, и соответственно изменил свой путь, чтобы прочитать файлы.
Спасибо, что остались со мной и прочитали мое путешествие по написанию моей первой забавной модели нейронной сети.