Пользовательская комбинированная функция потерь на отклонение /kb-расхождение в сиамской сети не позволяет создавать осмысленные встраивания динамиков

В настоящее время я пытаюсь реализовать сиамскую сеть в Керасе, где я должен реализовать следующую функцию потерь:

loss(p ∥ q) = Is · KL(p ∥ q) + Ids · HL(p ∥ q)

подробное описание функции потерь из бумаги

Где KL - дивергенция Кульбака-Лейблера, а HL - потеря шарнира.

Во время обучения я обозначаю пары одинаковых динамиков как 1, а разные динамики - как 0.

Цель состоит в том, чтобы использовать обученную сеть для извлечения вложений из спектрограмм. Спектрограмма представляет собой двумерную матрицу NumPy 40x128 (время х частота)

Проблема в том, что я никогда не получаю точность более 0,5, и при кластеризации вложений динамиков результаты показывают, что между вложениями и динамиками нет никакой корреляции

Я реализовал kb-расхождение как меру расстояния и скорректировал потери шарнира соответственно:

def kullback_leibler_divergence(vects):
    x, y = vects
    x = ks.backend.clip(x, ks.backend.epsilon(), 1)
    y = ks.backend.clip(y, ks.backend.epsilon(), 1)
    return ks.backend.sum(x * ks.backend.log(x / y), axis=-1)


def kullback_leibler_shape(shapes):
    shape1, shape2 = shapes
    return shape1[0], 1


def kb_hinge_loss(y_true, y_pred):
    """
    y_true: binary label, 1 = same speaker
    y_pred: output of siamese net i.e. kullback-leibler distribution
    """
    MARGIN = 1.
    hinge = ks.backend.mean(ks.backend.maximum(MARGIN - y_pred, 0.), axis=-1)
    return y_true * y_pred + (1 - y_true) * hinge

Одна спектрограмма будет подана в ветвь базовой сети, сиамская сеть состоит из двух таких ветвей, поэтому две спектрограммы подаются одновременно и соединяются в дистанционном слое. Выходной сигнал базовой сети равен 1 x 128. Слой расстояний вычисляет расхождение kullback-leibler, а его выходной сигнал подается в kb_hinge_loss. Архитектура базовой сети выглядит следующим образом:

    def create_lstm(units: int, gpu: bool, name: str, is_sequence: bool = True):
        if gpu:
            return ks.layers.CuDNNLSTM(units, return_sequences=is_sequence, input_shape=INPUT_DIMS, name=name)
        else:
            return ks.layers.LSTM(units, return_sequences=is_sequence, input_shape=INPUT_DIMS, name=name)


def build_model(mode: str = 'train') -> ks.Model:
    topology = TRAIN_CONF['topology']

    is_gpu = tf.test.is_gpu_available(cuda_only=True)

    model = ks.Sequential(name='base_network')

    model.add(
        ks.layers.Bidirectional(create_lstm(topology['blstm1_units'], is_gpu, name='blstm_1'), input_shape=INPUT_DIMS))

    model.add(ks.layers.Dropout(topology['dropout1']))

    model.add(ks.layers.Bidirectional(create_lstm(topology['blstm2_units'], is_gpu, is_sequence=False, name='blstm_2')))

    if mode == 'extraction':
        return model

    num_units = topology['dense1_units']
    model.add(ks.layers.Dense(num_units, name='dense_1'))
    model.add(ks.layers.advanced_activations.PReLU(init='zero', weights=None))

    model.add(ks.layers.Dropout(topology['dropout2']))

    num_units = topology['dense2_units']
    model.add(ks.layers.Dense(num_units, name='dense_2'))
    model.add(ks.layers.advanced_activations.PReLU(init='zero', weights=None))

    num_units = topology['dense3_units']
    model.add(ks.layers.Dense(num_units, name='dense_3'))
    model.add(ks.layers.advanced_activations.PReLU(init='zero', weights=None))

    num_units = topology['dense4_units']
    model.add(ks.layers.Dense(num_units, name='dense_4'))
    model.add(ks.layers.advanced_activations.PReLU(init='zero', weights=None))
    return model

Затем я строю сиамскую сеть следующим образом:

    base_network = build_model()

    input_a = ks.Input(shape=INPUT_DIMS, name='input_a')
    input_b = ks.Input(shape=INPUT_DIMS, name='input_b')

    processed_a = base_network(input_a)
    processed_b = base_network(input_b)

    distance = ks.layers.Lambda(kullback_leibler_divergence,
                                output_shape=kullback_leibler_shape,
                                name='distance')([processed_a, processed_b])

    model = ks.Model(inputs=[input_a, input_b], outputs=distance)
    adam = build_optimizer()
    model.compile(loss=kb_hinge_loss, optimizer=adam, metrics=['accuracy'])

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

utterance_embedding = np.mean(embedding_extractor.predict_on_batch(spectrogram), axis=0)

Тренируем сеть на наборе динамиков voxceleb.

Полный код можно посмотреть здесь: GitHub repo

Я пытаюсь выяснить, сделал ли я какие-то неправильные предположения и как улучшить свою точность.

Ответ 1

Проблема с точностью

Обратите внимание, что в вашей модели:

  • y_true= ярлыки
  • y_pred= kullback-leibler расхождение

Эти два нельзя сравнить, см. Этот пример:

Для получения правильных результатов, когда y_true == 1 (тот же спикер), Kullback-Leibler равен y_pred == 0 (без расхождения).

Поэтому вполне ожидаемо, что показатели не будут работать должным образом.

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

Возможные проблемы с потерей

вырезка

Это может быть проблемой

Во-первых, обратите внимание, что вы используете clip в значениях для Kullback-Leibler. Это может быть плохо, потому что клипы теряют градиенты в обрезанных областях. А так как ваша активация - PRelu, у вас есть значения ниже нуля и больше 1. Тогда, безусловно, есть случаи нулевого градиента здесь и там, с риском иметь замороженную модель.

Таким образом, вы можете не захотеть обрезать эти значения. И чтобы избежать использования отрицательных значений в PRelu, вы можете попытаться использовать активацию 'softplus', которая является своего рода мягким относительным значением без отрицательных значений. Вы также можете "суммировать" эпсилон, чтобы избежать проблем, но нет проблем с тем, чтобы оставить значения больше единицы:

#considering you used 'softplus' instead of 'PRelu' in speakers
def kullback_leibler_divergence(speakers):
    x, y = speakers
    x = x + ks.backend.epsilon()
    y = y + ks.backend.epsilon()
    return ks.backend.sum(x * ks.backend.log(x / y), axis=-1)

Ассиметрия в Куллбек-Лейблере

Это проблема

Также обратите внимание, что Kullback-Leibler не является симметричной функцией, а также не имеет своего минимума в нуле! Идеальное совпадение - ноль, но плохие совпадения могут иметь более низкие значения, и это плохо для функции потерь, потому что это приведет вас к дивергенции.

Посмотрите на эту картинку, показывающую график КБ

Ваша статья утверждает, что вы должны сложить две потери: (p || q) и (q || p).
Это исключает ассиметрию, а также отрицательные значения.

Так:

distance1 = ks.layers.Lambda(kullback_leibler_divergence,
                            name='distance1')([processed_a, processed_b])
distance2 = ks.layers.Lambda(kullback_leibler_divergence,
                            name='distance2')([processed_b, processed_a])
distance = ks.layers.Add(name='dist_add')([distance1,distance2])

Очень низкий край и шарнир

Это может быть проблемой

Наконец, обратите внимание, что потеря шарнира также обрезает значения ниже нуля!
Поскольку Kullback-Leibler не ограничен 1, выборки с высокой дивергенцией могут не контролироваться этой потерей. Не уверен, что это действительно проблема, но вы можете:

  • увеличить маржу
  • внутри Кульбака-Лейблера используйте mean вместо sum
  • используйте softplus в шарнире вместо max, чтобы избежать потери градиентов.

Увидеть:

MARGIN = someValue
hinge = ks.backend.mean(ks.backend.softplus(MARGIN - y_pred), axis=-1)

Теперь мы можем думать о пользовательской точности

Это не очень просто, так как у нас нет четких ограничений на КБ, которые говорят нам "правильно/не правильно"

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

def customMetric(y_true_targets, y_pred_KBL):
    isMatch = ks.backend.less(y_pred_KBL, threshold)
    isMatch = ks.backend.cast(isMatch, ks.backend.floatx())

    isMatch = ks.backend.equal(y_true_targets, isMatch)
    isMatch = ks.backend.cast(isMatch, ks.backend.floatx())

    return ks.backend.mean(isMatch)