Обучение модели tf.keras с помощью базового низкоуровневого цикла обучения TensorFlow не работает

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

У меня есть экземпляр tf.keras.models.Model и tf.keras.models.Model нужно обучить его с помощью обучающего цикла, написанного на низкоуровневом API TensorFlow.

Проблема: Обучение одной и той же модели tf.keras один раз с помощью стандартного низкоуровневого обучающего цикла model.fit() и один раз с собственным model.fit() дает очень разные результаты. Я хотел бы узнать, что я делаю неправильно в моей низкоуровневой тренировке TF.

Модель представляет собой простую модель классификации изображений, которую я тренирую на Caltech256 (ссылка на tfrecords ниже).

При использовании низкоуровневого тренировочного цикла TensorFlow потери на обучение сначала уменьшаются, как и должно, но затем, после всего лишь 1000 шагов тренировки, плато потери, а затем снова начинает увеличиваться:

enter image description here

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

enter image description here

Чего мне не хватает в моей низкоуровневой тренировочной петле TensorFlow?

Вот код для воспроизведения проблемы (скачайте TFRecords по ссылке внизу):

import tensorflow as tf
from tqdm import trange
import sys
import glob
import os

sess = tf.Session()
tf.keras.backend.set_session(sess)

num_classes = 257
image_size = (224, 224, 3)

# Build a tf.data.Dataset from TFRecords.

tfrecord_directory = 'path/to/tfrecords/directory'

tfrecord_filennames = glob.glob(os.path.join(tfrecord_directory, '*.tfrecord'))

feature_schema = {'image': tf.FixedLenFeature([], tf.string),
                  'filename': tf.FixedLenFeature([], tf.string),
                  'label': tf.FixedLenFeature([], tf.int64)}

dataset = tf.data.Dataset.from_tensor_slices(tfrecord_filennames)
dataset = dataset.shuffle(len(tfrecord_filennames)) # Shuffle the TFRecord file names.
dataset = dataset.flat_map(lambda filename: tf.data.TFRecordDataset(filename))
dataset = dataset.map(lambda single_example_proto: tf.parse_single_example(single_example_proto, feature_schema)) # Deserialize tf.Example objects.
dataset = dataset.map(lambda sample: (sample['image'], sample['label']))
dataset = dataset.map(lambda image, label: (tf.image.decode_jpeg(image, channels=3), label)) # Decode JPEG images.
dataset = dataset.map(lambda image, label: (tf.image.resize_image_with_pad(image, target_height=image_size[0], target_width=image_size[1]), label))
dataset = dataset.map(lambda image, label: (tf.image.per_image_standardization(image), label))
dataset = dataset.map(lambda image, label: (image, tf.one_hot(indices=label, depth=num_classes))) # Convert labels to one-hot format.
dataset = dataset.shuffle(buffer_size=10000)
dataset = dataset.repeat()
dataset = dataset.batch(32)

iterator = dataset.make_one_shot_iterator()
features, labels = iterator.get_next()

# Build a simple model.

input_tensor = tf.keras.layers.Input(shape=image_size)
x = tf.keras.layers.Conv2D(64, (3,3), strides=(2,2), activation='relu', kernel_initializer='he_normal')(input_tensor)
x = tf.keras.layers.Conv2D(64, (3,3), strides=(2,2), activation='relu', kernel_initializer='he_normal')(x)
x = tf.keras.layers.Conv2D(128, (3,3), strides=(2,2), activation='relu', kernel_initializer='he_normal')(x)
x = tf.keras.layers.Conv2D(256, (3,3), strides=(2,2), activation='relu', kernel_initializer='he_normal')(x)
x = tf.keras.layers.GlobalAveragePooling2D()(x)
x = tf.keras.layers.Dense(num_classes, activation=None, kernel_initializer='he_normal')(x)
model = tf.keras.models.Model(input_tensor, x)

Это простой обучающий цикл TensorFlow:

# Build the training-relevant part of the graph.

model_output = model(features)

loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=tf.stop_gradient(labels), logits=model_output))

train_op = tf.train.AdamOptimizer().minimize(loss)

# The next block is for the metrics.
with tf.variable_scope('metrics') as scope:
    predictions_argmax = tf.argmax(model_output, axis=-1, output_type=tf.int64)
    labels_argmax = tf.argmax(labels, axis=-1, output_type=tf.int64)
    mean_loss_value, mean_loss_update_op = tf.metrics.mean(loss)
    acc_value, acc_update_op = tf.metrics.accuracy(labels=labels_argmax, predictions=predictions_argmax)
    local_metric_vars = tf.contrib.framework.get_variables(scope=scope, collection=tf.GraphKeys.LOCAL_VARIABLES)
    metrics_reset_op = tf.variables_initializer(var_list=local_metric_vars)

# Run the training

epochs = 3
steps_per_epoch = 1000

fetch_list = [mean_loss_value,
              acc_value,
              train_op,
              mean_loss_update_op,
              acc_update_op]

sess.run(tf.global_variables_initializer())
sess.run(tf.local_variables_initializer())

with sess.as_default():

    for epoch in range(1, epochs+1):

        tr = trange(steps_per_epoch, file=sys.stdout)
        tr.set_description('Epoch {}/{}'.format(epoch, epochs))

        sess.run(metrics_reset_op)

        for train_step in tr:

            ret = sess.run(fetch_list, feed_dict={tf.keras.backend.learning_phase(): 1})

            tr.set_postfix(ordered_dict={'loss': ret[0],
                                         'accuracy': ret[1]})

Ниже приведен стандартный цикл обучения Keras, который работает, как и ожидалось. Обратите внимание, что активация плотного слоя в приведенной выше модели должна быть изменена с None на "softmax", чтобы цикл Keras работал.

epochs = 3
steps_per_epoch = 1000

model.compile(optimizer='adam',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

history = model.fit(dataset,
                    epochs=epochs,
                    steps_per_epoch=steps_per_epoch)

Вы можете скачать TFRecords для набора данных Caltech256 здесь (около 850 МБ).

ОБНОВИТЬ:

Мне удалось решить проблему: замена функции потери TF низкого уровня

loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=tf.stop_gradient(labels), logits=model_output))

его эквивалентом Кераса

loss = tf.reduce_mean(tf.keras.backend.categorical_crossentropy(target=labels, output=model_output, from_logits=True))

делает трюк. Теперь низкоуровневый обучающий цикл model.fit() ведет себя так же, как model.fit().

Это поднимает новый вопрос:

Что делает tf.keras.backend.categorical_crossentropy(), что tf.nn.softmax_cross_entropy_with_logits_v2() не заставляет последнего работать намного хуже? (Я знаю, что последним нужны логиты, а не вывод softmax, так что не проблема)

Ответ 1

Замена функции потери TF низкого уровня

loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=tf.stop_gradient(labels), logits=model_output))

его эквивалентом Кераса

loss = tf.reduce_mean(tf.keras.backend.categorical_crossentropy(target=labels, output=model_output, from_logits=True))

делает трюк. Теперь низкоуровневый обучающий цикл model.fit() ведет себя так же, как model.fit().

Однако я не знаю, почему это так. Если кто-то знает, почему tf.keras.backend.categorical_crossentropy() ведет себя хорошо, а tf.nn.softmax_cross_entropy_with_logits_v2() вообще не работает, пожалуйста, напишите ответ.

Еще одно важное замечание:

Чтобы обучить модель tf.keras с помощью низкоуровневого обучающего цикла TF и объекта tf.data.Dataset, обычно не следует вызывать модель на выходе итератора. То есть не следует этого делать:

model_output = model(features)

Вместо этого следует создать модель, в которой входной слой настроен на построение итератора вместо создания заполнителя, например, так:

input_tensor = tf.keras.layers.Input(tensor=features)

Это не имеет значения в этом примере, но становится уместным, если какие-либо слои в модели имеют внутренние обновления, которые необходимо запустить во время обучения (например, BatchNormalization).

Ответ 2

Вы применяете активацию softmax на свой последний слой

x = tf.keras.layers.Dense(num_classes, activation='softmax', kernel_initializer='he_normal')(x)

и вы снова применяете softmax при использовании tf.nn.softmax_cross_entropy_with_logits_v2 как он ожидает немасштабированных логитов. Из документации:

ВНИМАНИЕ: Эта операция ожидает немасштабированные логиты, так как для эффективности она выполняет softmax внутри логитов. Не вызывайте этот оператор с выводом softmax, так как он даст неверные результаты.

Таким образом, удалите активацию softmax вашего последнего слоя, и она должна работать.

x = tf.keras.layers.Dense(num_classes, activation=None, kernel_initializer='he_normal')(x)
[...]
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=tf.stop_gradient(labels), logits=model_output))