Модель Кераса не снижает потери

Я предлагаю пример, в котором tf.keras модель не может учиться на очень простых данных. Я использую tensorflow-gpu==2.0.0, keras==2.3.0 и Python 3.7. В конце поста я привожу код Python, чтобы воспроизвести обнаруженную мной проблему.


  1. Данные

Образцы представляют собой массивы Numpy формы (6, 16, 16, 16, 3). Чтобы упростить задачу, я рассматриваю только массивы, состоящие из единиц и нулей. Массивам с единицами присваивается метка 1, а массивам с нулями - метка 0. Я могу сгенерировать некоторые образцы (далее n_samples = 240) с помощью этого кода:

def generate_fake_data():
    for j in range(1, 240 + 1):
        if j < 120:
            yield np.ones((6, 16, 16, 16, 3)), np.array([0., 1.])
        else:
            yield np.zeros((6, 16, 16, 16, 3)), np.array([1., 0.])

Чтобы ввести эти данные в модель tf.keras, я создаю экземпляр tf.data.Dataset, используя приведенный ниже код. По сути, это создаст перемешанные партии из BATCH_SIZE = 12 семплов.

def make_tfdataset(for_training=True):
    dataset = tf.data.Dataset.from_generator(generator=lambda: generate_fake_data(),
                                             output_types=(tf.float32,
                                                           tf.float32),
                                             output_shapes=(tf.TensorShape([6, 16, 16, 16, 3]),
                                                            tf.TensorShape([2])))
    dataset = dataset.repeat()
    if for_training:
        dataset = dataset.shuffle(buffer_size=1000)
    dataset = dataset.batch(BATCH_SIZE)
    dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)
    return dataset
  1. Модель

Предлагаю следующую модель классификации моих образцов:

def create_model(in_shape=(6, 16, 16, 16, 3)):

    input_layer = Input(shape=in_shape)

    reshaped_input = Lambda(lambda x: K.reshape(x, (-1, *in_shape[1:])))(input_layer)

    conv3d_layer = Conv3D(filters=64, kernel_size=8, strides=(2, 2, 2), padding='same')(reshaped_input)

    relu_layer_1 = ReLU()(conv3d_layer)

    pooling_layer = GlobalAveragePooling3D()(relu_layer_1)

    reshape_layer_1 = Lambda(lambda x: K.reshape(x, (-1, in_shape[0] * 64)))(pooling_layer)

    expand_dims_layer = Lambda(lambda x: K.expand_dims(x, 1))(reshape_layer_1)

    conv1d_layer = Conv1D(filters=1, kernel_size=1)(expand_dims_layer)

    relu_layer_2 = ReLU()(conv1d_layer)

    reshape_layer_2 = Lambda(lambda x: K.squeeze(x, 1))(relu_layer_2)

    out = Dense(units=2, activation='softmax')(reshape_layer_2)

    return Model(inputs=[input_layer], outputs=[out])

Модель оптимизирована с использованием Адама (с параметрами по умолчанию) и с binary_crossentropy потерями:

clf_model = create_model()
clf_model.compile(optimizer=Adam(),
                  loss='categorical_crossentropy',
                  metrics=['accuracy', 'categorical_crossentropy'])

Результат clf_model.summary():

Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 6, 16, 16, 16, 3) 0         
_________________________________________________________________
lambda (Lambda)              (None, 16, 16, 16, 3)     0         
_________________________________________________________________
conv3d (Conv3D)              (None, 8, 8, 8, 64)       98368     
_________________________________________________________________
re_lu (ReLU)                 (None, 8, 8, 8, 64)       0         
_________________________________________________________________
global_average_pooling3d (Gl (None, 64)                0         
_________________________________________________________________
lambda_1 (Lambda)            (None, 384)               0         
_________________________________________________________________
lambda_2 (Lambda)            (None, 1, 384)            0         
_________________________________________________________________
conv1d (Conv1D)              (None, 1, 1)              385       
_________________________________________________________________
re_lu_1 (ReLU)               (None, 1, 1)              0         
_________________________________________________________________
lambda_3 (Lambda)            (None, 1)                 0         
_________________________________________________________________
dense (Dense)                (None, 2)                 4         
=================================================================
Total params: 98,757
Trainable params: 98,757
Non-trainable params: 0
  1. Обучение

Модель обучена на 500 эпох следующим образом:

train_ds = make_tfdataset(for_training=True)

history = clf_model.fit(train_ds,
                        epochs=500,
                        steps_per_epoch=ceil(240 / BATCH_SIZE),
                        verbose=1)
  1. Проблема!

В течение 500 эпох потери модели остаются на уровне 0,69 и никогда не опускаются ниже 0,69. Это также верно, если я установил скорость обучения 1e-2 вместо 1e-3. Данные очень простые (только нули и единицы). Наивно, я ожидал, что модель будет иметь точность лучше, чем 0,6. Фактически, я ожидал, что он быстро достигнет 100% точности. Что я делаю не так?

  1. Полный код ...
import numpy as np
import tensorflow as tf
import tensorflow.keras.backend as K
from math import ceil
from tensorflow.keras.layers import Input, Dense, Lambda, Conv1D, GlobalAveragePooling3D, Conv3D, ReLU
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam

BATCH_SIZE = 12


def generate_fake_data():
    for j in range(1, 240 + 1):
        if j < 120:
            yield np.ones((6, 16, 16, 16, 3)), np.array([0., 1.])
        else:
            yield np.zeros((6, 16, 16, 16, 3)), np.array([1., 0.])


def make_tfdataset(for_training=True):
    dataset = tf.data.Dataset.from_generator(generator=lambda: generate_fake_data(),
                                             output_types=(tf.float32,
                                                           tf.float32),
                                             output_shapes=(tf.TensorShape([6, 16, 16, 16, 3]),
                                                            tf.TensorShape([2])))
    dataset = dataset.repeat()
    if for_training:
        dataset = dataset.shuffle(buffer_size=1000)
    dataset = dataset.batch(BATCH_SIZE)
    dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)
    return dataset


def create_model(in_shape=(6, 16, 16, 16, 3)):

    input_layer = Input(shape=in_shape)

    reshaped_input = Lambda(lambda x: K.reshape(x, (-1, *in_shape[1:])))(input_layer)

    conv3d_layer = Conv3D(filters=64, kernel_size=8, strides=(2, 2, 2), padding='same')(reshaped_input)

    relu_layer_1 = ReLU()(conv3d_layer)

    pooling_layer = GlobalAveragePooling3D()(relu_layer_1)

    reshape_layer_1 = Lambda(lambda x: K.reshape(x, (-1, in_shape[0] * 64)))(pooling_layer)

    expand_dims_layer = Lambda(lambda x: K.expand_dims(x, 1))(reshape_layer_1)

    conv1d_layer = Conv1D(filters=1, kernel_size=1)(expand_dims_layer)

    relu_layer_2 = ReLU()(conv1d_layer)

    reshape_layer_2 = Lambda(lambda x: K.squeeze(x, 1))(relu_layer_2)

    out = Dense(units=2, activation='softmax')(reshape_layer_2)

    return Model(inputs=[input_layer], outputs=[out])


train_ds = make_tfdataset(for_training=True)
clf_model = create_model(in_shape=(6, 16, 16, 16, 3))
clf_model.summary()
clf_model.compile(optimizer=Adam(lr=1e-3),
                  loss='categorical_crossentropy',
                  metrics=['accuracy', 'categorical_crossentropy'])

history = clf_model.fit(train_ds,
                        epochs=500,
                        steps_per_epoch=ceil(240 / BATCH_SIZE),
                        verbose=1)

person Pouteri    schedule 04.10.2019    source источник
comment
Заглянув в ваш код, я вижу несколько сомнительных дизайнерских решений; Во-первых, какова ваша цель с изменениями форм? Вне зависимости от того, намеренно это или нет, вы разрушаете взаимосвязь между элементами и спецификациями формы - этого достаточно, чтобы объяснить, почему ваша модель никуда не денется. Кроме того, вы уверены, что вам нужен ввод 6D - каково предполагаемое приложение?   -  person OverLordGoldDragon    schedule 05.10.2019
comment
TimeDistributed не пойдет, так как это будет означать временные отношения, которых не существует. (могут быть исключения, но это, вероятно, излишне для вашего приложения)   -  person OverLordGoldDragon    schedule 05.10.2019


Ответы (2)


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

Tensor("input_1:0", shape=(12, 6, 16, 16, 16, 3), dtype=float32)
Tensor("lambda/Reshape:0", shape=(72, 16, 16, 16, 3), dtype=float32)

Это похоже на загрузку 72 независимых образцов формы (16,16,16,3). Другие слои страдают аналогичными проблемами.


РЕШЕНИЕ:

  • Вместо того, чтобы изменять форму на каждом этапе (для чего вы должны использовать Reshape), сформируйте существующий Conv и объедините слои, чтобы все работало напрямую.
  • Помимо входных и выходных слоев, лучше назвать каждый слой чем-то коротким и простым - ясность не теряется, так как каждая строка четко определяется именем слоя.
  • GlobalAveragePooling должен быть последним слоем, поскольку он сворачивает размеры элементов - в вашем случае, например: (12,16,16,16,3) --> (12,3); Конвенция после этого бесполезна
  • Как указано выше, я заменил Conv1D на Conv3D
  • Если вы не используете переменные размеры пакетов, всегда выбирайте batch_shape= против shape=, так как вы можете полностью проверить размеры слоя (очень полезно)
  • Ваш истинный batch_size здесь равен 6, исходя из вашего ответа на комментарий.
  • kernel_size=1 и (особенно) filters=1 - очень слабая свертка, я заменил ее соответственно - можете вернуться, если хотите
  • Если у вас есть только 2 класса в предполагаемом приложении, я советую использовать Dense(1, 'sigmoid') с binary_crossentropy потерями.

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

def create_model(batch_size, input_shape):

    ipt = Input(batch_shape=(batch_size, *input_shape))
    x   = Conv3D(filters=64, kernel_size=8, strides=(2, 2, 2),
                             activation='relu', padding='same')(ipt)
    x   = Conv3D(filters=8,  kernel_size=4, strides=(2, 2, 2),
                             activation='relu', padding='same')(x)
    x   = GlobalAveragePooling3D()(x)
    out = Dense(units=2, activation='softmax')(x)

    return Model(inputs=ipt, outputs=out)
BATCH_SIZE = 6
INPUT_SHAPE = (16, 16, 16, 3)
BATCH_SHAPE = (BATCH_SIZE, *INPUT_SHAPE)

def generate_fake_data():
    for j in range(1, 240 + 1):
        if j < 120:
            yield np.ones(INPUT_SHAPE), np.array([0., 1.])
        else:
            yield np.zeros(INPUT_SHAPE), np.array([1., 0.])


def make_tfdataset(for_training=True):
    dataset = tf.data.Dataset.from_generator(generator=lambda: generate_fake_data(),
                                 output_types=(tf.float32,
                                               tf.float32),
                                 output_shapes=(tf.TensorShape(INPUT_SHAPE),
                                                tf.TensorShape([2])))
    dataset = dataset.repeat()
    if for_training:
        dataset = dataset.shuffle(buffer_size=1000)
    dataset = dataset.batch(BATCH_SIZE)
    dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)
    return dataset

РЕЗУЛЬТАТЫ:

Epoch 28/500
40/40 [==============================] - 0s 3ms/step - loss: 0.0808 - acc: 1.0000
person OverLordGoldDragon    schedule 05.10.2019

Поскольку ваши метки могут быть либо 0, либо 1, я бы рекомендовал изменить функцию активации на softmax, а количество выходных нейронов на 2. Теперь последний слой (выход) будет выглядеть следующим образом:

out = Dense(units=2, activation='softmax')(reshaped_conv_features)

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

person Rachayita Giri    schedule 04.10.2019
comment
Я обновил свой пост в соответствии с вашим ответом. Это не помогает ... Настоящая проблема (если таковая имеется) кроется в другом. - person Pouteri; 04.10.2019
comment
Не могли бы вы увеличить размер партии как минимум до 32? Я бы по-прежнему предлагал использовать функцию потерь Softmax. Но вы можете попробовать как Sigmoid, так и Softmax после увеличения размера партии. - person Rachayita Giri; 04.10.2019
comment
Без изменений с BATCH_SIZE = 32. - person Pouteri; 04.10.2019
comment
Не могли бы вы попробовать SGD(lr=0.1) вместо Adam()? Начав с высокой скорости обучения с SGD, вы, по крайней мере, обеспечите значительное обновление ваших весов. А потом, может быть, вы сможете его настроить. - person Rachayita Giri; 04.10.2019
comment
SGD(lr=0.1) все равно не помогает. Я не уверен, что это проблема оптимизатора. - person Pouteri; 04.10.2019
comment
Даже если вы используете сигмоид? - person Rachayita Giri; 04.10.2019
comment
Я добавил полный код. Вы можете попробовать воспроизвести / отладить его. Сигмовидная кишка не помогает. - person Pouteri; 05.10.2019