Введение

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

В прошедшем семестре в Колледже Статен-Айленда профессор Микаэль Вейдемо-Йоханссон ставил перед нейронной сетью продуманную задачу по классификации с использованием разных моделей и разных оптимизаторов. Это было специально сделано во время конкурса Petals to the Metals на Kaggle. Земля имеет огромное разнообразие в отношении количества живых организмов. Не считая более 5000 видов млекопитающих, 10 000 видов птиц, 30 000 видов рыб и, что удивительно, более 400 000 различных видов цветов, которые являются основным направлением классификации цветов на соревнованиях TPU.

Соревнование

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

Данные

Конкурс предлагает свои файлы в формате TFRecord, формате контейнера, часто используемом в Tensorflow для группировки и разделения файлов данных данных для оптимальной производительности обучения. Каждый файл содержит информацию id, label (класс образца для обучающих данных) и img (фактические пиксели в форме массива) для многих изображений.

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

HEIGHT = 512
WIDTH = 512
model_path = f'model_{HEIGHT}x{WIDTH}.h5'

GCS_PATH = KaggleDatasets().get_gcs_path('tpu-getting-started') + f'/tfrecords-jpeg-{HEIGHT}x{WIDTH}'

TRAINING_FILENAMES = tf.io.gfile.glob(GCS_PATH + '/train/*.tfrec')
VALIDATION_FILENAMES = tf.io.gfile.glob(GCS_PATH + '/val/*.tfrec')
TEST_FILENAMES = tf.io.gfile.glob(GCS_PATH + '/test/*.tfrec')

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

try:
    tpu = tf.distribute.cluster_resolver.TPUClusterResolver()
    print(f"Running on TPU: ${tpu.master()}")
except ValueError:
    tpu = None
    
if tpu:
    tf.config.experimental_connect_to_cluster(tpu)
    tf.tpu.experimental.initialize_tpu_system(tpu)
    strategy = tf.distribute.experimental.TPUStrategy(tpu)
else:
    strategy = tf.distribute.get_strategy()
AUTO = tf.data.experimental.AUTOTUNE
REPLICAS = strategy.num_replicas_in_sync
print(replicas: {strategy.num_replicas_in_sync}")

Увеличение данных

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

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

def data_augment(image, label):
    crop_size = tf.random.uniform([], int(HEIGHT*.7), HEIGHT, dtype=tf.int32)
        
    image = tf.image.random_flip_left_right(image)
    image = tf.image.random_flip_up_down(image)
    image = tf.image.random_saturation(image, lower=0, upper=2)

     image = tf.image.random_contrast(image, lower=.8, upper=2)
     image = tf.image.random_brightness(image, max_delta=.2)

    image = tf.image.random_crop(image, size=[crop size, crop_size, CHANNELS])
    image = tf.image.resize(image, size=[HEIGHT, WIDTH])
    image = random_black_box(image, prob_apply=0.40)

    return image, label

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

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

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

На протяжении всего конкурса было создано несколько моделей, каждая из которых давала разные результаты. Когда моя группа впервые начала участвовать в конкурсе, мы в основном сосредоточились на использовании оптимизатора модели Адама. Сначала мы выбрали оптимизатор Adam случайным образом; однако, углубившись в Адам (оценка адаптивного импульса), мы поняли, что, поскольку Адам отслеживает экспоненциально затухающий средний показатель прошлых градиентов как более адаптивный импульс, он может быть неплохим выбором для данного конкретного случая.

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

model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['sparse_categorical_accuracy']
)
EPOCHS = 16
STEPS_PER_EPOCH = NUM_TRAINING_IMAGES // BATCH_SIZE

history = model.fit(
    ds_train,
    validation_data=ds_val,
    epochs=EPOCHS,
    steps_per_epoch=STEPS_PER_EPOCH,
    callbacks=[]
)

В нашем самом первом представлении с использованием еще меньшего увеличения данных, показанного ранее, 22 эпохи (количество эпох — это гиперпараметр, который определяет, сколько раз алгоритм обучения будет работать через весь набор обучающих данных), и модели, показанной выше, мы получили удивительную оценку 0,16485 за 11 минут и 13 секунд с заметным и смущающим количеством переобучения.

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

Кроме того, реализация переносного обучения, описанная в Руководстве по исследованиям приложений машинного обучения как улучшение обучения новой задаче путем передачи знаний из связанной задачи. что уже выучено. В нашем конкретном случае мы остановились на EfficientNet, поскольку это «архитектура сверточной нейронной сети и метод масштабирования, который равномерно масштабирует все измерения глубины/ширины/разрешения с использованием составного коэффициента», как указано в «EfficientNet: переосмысление масштабирования модели для сверточных нейронных сетей».

В дополнение к EfficientNet, ImageNet представляет собой набор данных из более чем 15 миллионов помеченных изображений с высоким разрешением, принадлежащих примерно к 22 000 категорий. Изображения были собраны из Интернета и помечены людьми.

Изображенная ниже архитектура — VGG16.

def create_model(input_shape, N_CLASSES):
    base_model = EfficientNetB6(weights='/kaggle/input/efficientnet/efficientnet-b6_noisy-student_notop.h5', 
                                    include_top=False,
                                    input_shape=input_shape)

    base_model.trainable = False # Freeze layers
    model = tf.keras.Sequential([
        base_model,
        L.GlobalAveragePooling2D(),
        L.Dense(N_CLASSES, activation='softmax')
    ])
    
    return model
with strategy.scope():
    pretrained_model = tf.keras.applications.VGG16(
        include_top=False,
        weights="imagenet",
        input_tensor=None,
        #input_shape=None,
        pooling=None,
        classes=1000,
        classifier_activation="softmax",
        input_shape=[*IMAGE_SIZE, 3]
    )
    pretrained_model.trainable = False
    
    model = tf.keras.Sequential([
        pretrained_model,
        tf.keras.layers.Conv2D(256,4, activation='relu', input_shape=IMAGE_SIZE),
        #tf.keras.layers.Conv2D(128,2, activation='relu', input_shape=IMAGE_SIZE),
        tf.keras.layers.GlobalAveragePooling2D(),
        tf.keras.layers.Dense(len(CLASSES), activation='softmax')
    ])
    model.compile(
        optimizer='adam',
        loss = 'sparse_categorical_crossentropy',
        metrics=['sparse_categorical_accuracy'],
    )

model.summary()

Как показано в кодовом блоке выше, мы по-прежнему используем оптимизатор adam, а также используем VGG16, ориентированный на шумного ученика, а также с более широким охватом стратегии.

В этом представлении, хотя мы все еще используем 22 эпохи, мы видим значительное увеличение нашей общедоступной оценки примерно вдвое. Наша новая общедоступная оценка была впечатляющей (на наш взгляд) 0,69811 за 23 минуты и 43 секунды с последующей потерей обучения/проверки, а также с низкой категориальной точностью набора обучения и проверки. Как вы можете видеть ниже, обучение и проверка кажутся очень близкими друг к другу, что является хорошим признаком незначительной переобучения.

Окончательные изменения

В этот момент мы с группой были на седьмом небе от счастья, думая про себя, что превзошли своих сверстников и пробились в топ-100 списков лидеров в тот конкретный момент; однако пришлось вмешаться профессору Микаэлю Вейдемо-Йоханссон. На следующей неделе он продемонстрировал свой новый результат примерно 0,88, что привело к еще большему машинному обучению для экспериментов.

В нашей первой итерации правок мы пробовали экспериментировать с различными оптимизаторами, такими как Adadelta и Nadam. В нашем первоначальном моделировании с использованием Adadelta мы получили потрясающий результат 0,00162 за 23 минуты и 48 секунд… Не совсем то, что мы ожидали; однако, как только мы развернули Nadam в первый раз, появилось несколько уведомлений об ошибках. Мы с группой слишком надеялись на такое быстрое решение. К счастью, вскоре после этого мы успешно завершили использование Nadam и снова получили значительное увеличение до 0,74387 со временем выполнения 40 минут и 1 секунды.

model = tf.keras.Sequential([
        
        pretrained_model,
        tf.keras.layers.Conv2D(256,4, activation='relu', input_shape=IMAGE_SIZE),
        tf.keras.layers.Conv2D(128,2, activation='relu', input_shape=IMAGE_SIZE),
        tf.keras.layers.GlobalAveragePooling2D(),
        tf.keras.layers.Dense(len(CLASSES), activation='softmax')
    ])
    model.compile(
        optimizer='Nadam',
        loss = 'sparse_categorical_crossentropy',
        metrics=['sparse_categorical_accuracy'],
    )

model.summary()

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

local_save_options = tf.saved_model.SaveOptions(experimental_io_device='/job:localhost')
lr_callback = keras.callbacks.LearningRateScheduler(lrfn, verbose=True)
cp_callback = keras.callbacks.ModelCheckpoint(
        "best_model.hdf5",
        monitor="val_sparse_categorical_accuracy",
        save_best_only=True,
        verbose=1,
        options=local_save_options
    )

После создания экземпляров параметров сохранения мы затем применили разные модели, добавив и используя обратный вызов lr & cp в течение 64 эпох.

EPOCHS = 64
histories.append(model.fit(
    ds_train,
    validation_data=ds_val,
    epochs=EPOCHS,
    steps_per_epoch=STEPS_PER_EPOCH,
    callbacks=[lr_callback, cp_callback]))
model.load_weights("best_model.hdf5")

Выше и ниже приведены блоки кода для разных моделей, которые объединяются вместе с использованием обратных вызовов. В первой модели мы вызываем lr_callback, а также cp_callback, в то время как в следующей ниже модели единственный обратный вызов, который нам нужен, — это lr_callback.

EPOCHS = 64
enb7.trainable = True
histories.append(model.fit(
    ds_train,
    validation_data=ds_val,
    epochs=EPOCHS,
    steps_per_epoch=STEPS_PER_EPOCH,
    callbacks=[lr_callback]))

С нашей окончательной моделью и отправкой мы получили 0,85647 баллов из 1, что позволило нам занять 54-е место в списках лидеров. Модель пробежала за 1 час 32 минуты 42 секунды.

Окончательный вывод

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

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

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