("Комментарии")

На прошлой неделе мы исследовали использование предварительно обученной модели VGG-Face2 для идентификации человека по лицу. Поскольку мы знаем, что модель обучена идентифицировать 8631 знаменитость и спортсмена.

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

Есть два способа использовать предварительно обученную сеть: извлечение признаков и тонкая настройка.

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

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

{0:'angry',1:'disgust',2:'fear',3:'happy', 4:'sad',5:'surprise',6:'neutral'}

Вы можете скачать набор данных fer2013 с Kaggle. Каждое изображение представляет собой изображения лиц в градациях серого размером 48x48 пикселей.

Первая попытка со слоем извлечения объектов

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

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

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

conv_base = VGGFace(model='resnet50', include_top=False, input_shape=(224, 224, 3),
                                pooling='avg')  # pooling: None, avg or max
model = models.Sequential()
model.add(conv_base)
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(7, activation='softmax'))
conv_base.trainable = False

Как оказалось, модель не может хорошо обобщать и всегда предсказывает «счастливое» лицо. Почему «счастливое» лицо, модель любит счастливые лица больше, чем другие лица, как мы? Нет, правда.

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

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

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

Ответ: ДА, мы должны «тонко настроить» модель.

Точная настройка предварительно обученной модели

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

Большая сверточная сеть, такая как предварительно обученная модель лица resnet50, имеет множество стеков «Conv-блоков», расположенных друг над другом.

Пример остаточного блока показан на рисунке ниже.

Кредит изображения: http://torch.ch/blog/2016/02/04/resnets.html

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

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

Мы разморозим веса модели, найдя слой для начала. Слой с именем «activation_46» в нашем случае показан ниже.

# set 'activation_46' and following layers trainable
conv_base.trainable = True

set_trainable = False
for layer in conv_base.layers:
    if layer.name == 'activation_46':
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False

Подготовка и обучение модели

Нам дано 35887 изображений лиц в градациях серого размером 48x48 пикселей. И наша предварительно обученная модель ожидает входное цветное изображение 224x224.

Преобразование всех изображений 35887 в размер 224x224 и сохранение в ОЗУ займет значительное количество места. Мое решение состоит в том, чтобы преобразовывать и сохранять по одному изображению в файл TFRecord, который мы можем загрузить позже с помощью TensorFlow с небольшой головной болью.

С TFRecord в качестве формата набора обучающих данных он также обучается быстрее. Вы можете ознакомиться с моим предыдущим экспериментом.

И вот код, чтобы разговор состоялся.

# Helper-function for wrapping an integer so it can be saved to the TFRecord file.
def wrap_int64(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

# Helper-function for wrapping a list of integer so it can be saved to the TFRecord file.
def wrap_int64_list(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=value))

# Helper-function for wrapping raw bytes so they can be saved to the TFRecord file.
def wrap_bytes(value):
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
# Function for reading images from disk and writing them along with the class-labels to a TFRecord file.
def convert(image_arrays, labels, out_path, size=(224,224)):
    # Args:
    # image_paths   List of numpy image arrays.
    # labels        Class-labels for the images.
    # out_path      File-path for the TFRecords output file.    
    print("Converting: " + out_path)
    # Number of images. Used when printing the progress.
    num_images = len(image_arrays)    
    # Open a TFRecordWriter for the output-file.
    with tf.python_io.TFRecordWriter(out_path) as writer:
        # Iterate over all the image-paths and class-labels.
        for i, (img, label) in enumerate(zip(image_arrays, labels)):
            # Print the percentage-progress.
            print_progress(count=i, total=num_images-1)
            # resize the image array to desired size
            img = cv2.resize(img.astype('uint8'), size)    
            # Turn gray to color.
            img = cv2.cvtColor(img,cv2.COLOR_GRAY2RGB)
            # Convert the image to raw bytes.
            img_bytes = img.tostring()
            # Create a dict with the data we want to save in the
            # TFRecords file. You can add more relevant data here.
            data = \
                {
                    'image': wrap_bytes(img_bytes),
                    'label': wrap_int64_list(label)
                }
            # Wrap the data as TensorFlow Features.
            feature = tf.train.Features(feature=data)
            # Wrap again as a TensorFlow Example.
            example = tf.train.Example(features=feature)
            # Serialize the data.
            serialized = example.SerializeToString()        
            # Write the serialized data to the TFRecords file.
            writer.write(serialized)

convert(image_arrays=train_data[0],
        labels=train_data[1],
        out_path=path_tfrecords_train)
convert(image_arrays=val_data[0],
        labels=val_data[1],
        out_path=path_tfrecords_test)

В приведенном выше коде train_data[0] содержит список массивов изображений лиц, каждое из которых имеет форму (48, 48), а train_data[1] — это список фактических меток эмоций в формате one-hot.

Например, одна эмоция кодируется как

[0, 0, 1, 0, 0, 0, 0]

С 1 по индексу 2, а индекс 2 в нашем отображении — это эмоция «страх».

Чтобы обучить нашу модель Keras с помощью набора данных TFRecord, нам сначала нужно превратить ее в оценщик TF с помощью метода tf.keras.estimator.model_to_estimator.

est_emotion = tf.keras.estimator.model_to_estimator(keras_model=model,
                                                    model_dir=model_dir)

Мы представили, как написать функцию ввода изображения для TF Estimator в моем предыдущем посте для задачи бинарной классификации. Здесь у нас есть 7 категорий эмоций для классификации. Таким образом, функция ввода выглядит немного иначе.

Следующий фрагмент показал основную разницу.

features = \
        {
            'image': tf.FixedLenFeature([], tf.string),
            'label': tf.FixedLenFeature([7], tf.int64)
        }
        # Parse the serialized data so we get a dict with our data.
        parsed_example = tf.parse_single_example(serialized=serialized,
                                                 features=features)
        # Get the image as raw bytes.
        image_shape = tf.stack([224, 224, 3])

Теперь мы готовы полностью обучить и оценить нашу модель, вызвав train_and_evaluate .

train_spec = tf.estimator.TrainSpec(input_fn=lambda: imgs_input_fn(path_tfrecords_train,
                                                                   perform_shuffle=True,
                                                                   repeat_count=5,
                                                                   batch_size=20), 
                                    max_steps=500)
eval_spec = tf.estimator.EvalSpec(input_fn=lambda: imgs_input_fn(path_tfrecords_test,
                                                                 perform_shuffle=False,
                                                                 batch_size=1))

import time
start_time = time.time()
tf.estimator.train_and_evaluate(est_emotion, train_spec, eval_spec)
print("--- %s seconds ---" % (time.time() - start_time))

Всего потребовалось 2 минуты, а точность проверки составила 0,55. Что неплохо, учитывая, что одно лицо может на самом деле выражать разные эмоции одновременно. Быть одновременно удивленным и счастливым, например.

Резюме и дальнейшие размышления

В этом посте мы попробовали два разных подхода к предварительно обученной модели VGG-Face2 для задачи классификации эмоций. Извлечение признаков и тонкая настройка.

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

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

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

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

Исходный код доступен в моем репозитории GitHub.

Я также рассматриваю возможность создания живого демо. Так что следите за обновлениями.

Поделиться в Twitter Поделиться в Facebook

Первоначально опубликовано на www.dlology.com.

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

Удачного кодирования!