Добавление дополнительных потерь при постоянном нулевом выходе меняет сходимость модели - PullRequest
2 голосов
/ 07 августа 2020

Я установил модель Returnn Transformer для NMT, которую я хочу тренировать с дополнительной потерей для каждой точки внимания кодера / декодера h на каждом слое декодера l (в дополнение к ванильной потере перекрестной энтропии), т.е.:

loss = CrossEntropyLoss + sum_{Layer l=1,...,6} sum_{Head h=1,...,8} (lambda * AttentionLoss(l, h))

для некоторого скаляра lambda. Я реализовал саму потерю внимания как eval -уровень, используя опцию loss=as_is, которая возвращает одно число для каждой партии (это значение lambda * AttentionLoss(l, h).

В качестве теста я также реализовал версию, в которой у меня есть одна потеря для каждого слоя l, эквивалентная lambda * sum_{Head h=1,...,8} AttentionLoss(l, h), чтобы уменьшить количество потерь, поскольку я заметил снижение производительности и поскольку файлы журналов становились очень большими, поскольку Returnn печатает каждую потерю для каждой партии .

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

Чтобы исследовать это, Я пробовал тренировочный прогон, в котором я установил параметр lambda=0.0, то есть эффективно отключил потерю внимания. И даже здесь, по сравнению с базовой линией без каких-либо дополнительных потерь, модель, обученная с этими дополнительными 6 потерями, каждая из которых выводит константу 0, работает заметно. хуже, см. эту таблицу:

+--------------------------------------------+-------------+-------------+
|                                            |   Dev Set   |   Test Set  |
+--------------------------------------------+------+------+------+------+
|                                            | BLEU |  TER | BLEU |  TER |
+--------------------------------------------+------+------+------+------+
| Only Cross Entropy Loss                    | 35.7 | 51.4 | 34.2 | 53.5 |
+--------------------------------------------+------+------+------+------+
| + One loss per layer and head (lambda 0)   | 35.5 | 51.5 | 33.9 | 53.7 |
+--------------------------------------------+------+------+------+------+
| + One loss per layer (lambda 0)            | 35.4 | 51.8 | 33.5 | 54.2 |
+--------------------------------------------+------+------+------+------+
| + Simplified One loss per layer (lambda 0) | 35.1 | 52.0 | 33.5 | 54.3 |
+--------------------------------------------+------+------+------+------+

Здесь "упрощенное" ve rsion реализован именно так:

'dec_01_weight_loss': {
   'class': 'eval', 'eval': '0.0 * tf.reduce_sum(source(0, auto_convert=False))',
   'from': ['dec_01_att_weights'], 'loss': 'as_is',
   'out_type': {   'batch_dim_axis': None, 'dim': None, 'dtype': 'float32', 'feature_dim_axis': None,
                   'shape': (), 'time_dim_axis': None}}

, хотя фактические потери, которые я использую, немного сложнее, я загрузил сюда свои полные файлы конфигурации. (Здесь уровень потерь называется dec_01_att_weight_variance et c.)

И все lambda=0.0 упомянутые выше реализации выводят значение 0.0 для всех дополнительных потерь на каждом шаге обучения:

train epoch 1, step 0, cost:output/dec_01_weight_loss 0.0, cost:output/dec_02_weight_loss 0.0, cost:output/dec_03_weight_loss 0.0, [....], cost:output/output_prob 8.541749455164052, error:decision 0.0, error:output/output_prob 0.9999999680730979, loss 8.5417 49, max_mem_usage:GPU:0 1.2GB, mem_usage:GPU:0 1.2GB, 3.999 sec/step, elapsed 0:00:38, exp. remaining 1:30:00, complete 0.71%

Что происходит Вот? Есть ли какое-либо объяснение, почему модели ведут себя по-разному, почему дополнительная потеря с постоянным значением 0.0 меняет поведение модели?

Я использую TF 1.15.0 (v1.15.0-0-g590d6eef7e), Returnn 20200613.152716 - git -23332ca, используя Python 3.8.0 с CUDA 10.1.

Последующее обновление : я протестировал ту же конфигурацию, используя предварительное обучение, где я полностью отключил бы свою потерю для первых n-1 (здесь, например, n=50) контрольных точек, используя следующий код:

def custom_construction_algo(idx, net_dict):
    if idx == 0:
        for lay in range(1, 7):
             del net_dict["output"]["unit"]["dec_%02i_att_loss" % lay]
        return net_dict
    else:
        return None
pretrain = {"repetitions": 49, "construction_algo": custom_construction_algo}

В файле журнала для первых n-1 контрольных точек я (правильно) вижу только отчет о потере CE.

Здесь я показываю свой Dev BLEU на последней контрольной точке, обученной без дополнительная потеря (например, n-1, здесь 49), каждый эксперимент запускается несколько раз:

  • Базовый уровень (без дополнительных потерь): 31,8, 31,7, 31,7 BLEU
  • Один потери на слой, отключенные с предварительным обучением: 29,2, 29,0, 28,5 BLEU
  • Одна потеря на слой с lambda=0.0 (как в исходном вопросе): 28,8, 28,7 BLEU
  • Одна потеря на слой И голова с lambda=0.0 (как в исходном вопросе): 31,8 BLEU

Насколько я понимаю, график TF для конфигурации перед обучением и базовый уровень должны быть идентичны до контрольной точки n=50. Однако они действуют по-разному. Что происходит?

Полную конфигурацию, которую я использовал для такого рода предварительной тренировки, можно найти здесь . Заголовки соответствующих файлов журнала находятся здесь . Я использую NewbobMultiEpoch с Адамом:

learning rate control: NewbobMultiEpoch(num_epochs=9, update_interval=1, relative_error_threshold=0, learning_rate_decay_factor=0.7, learning_rate_growth_factor=1.0), epoch data: , error key: None
Create optimizer <class 'tensorflow.python.training.adam.AdamOptimizer'> with options {'beta1': 0.9, 'beta2': 0.999, 'epsilon': 1e-08, 'learning_rate': <tf.Variable 'learning_rate:0' shape=() dtype=float32_ref>}.

Для всех описанных экспериментов скорость обучения не снижается до тех пор, пока контрольные точки не превышают 100, оставаясь неизменной на начальном уровне 10^-4.

РЕДАКТИРОВАТЬ: Я сделал ошибку и случайно использовал другую версию Returnn в своих экспериментах . Returnn, который я использовал для своих экспериментов с дополнительными потерями, похоже, содержал некоторые локальные изменения, которые я сделал. При повторном запуске базовой линии с новой версией он работал значительно хуже - очень похож на другие значения BLEU, описанные здесь. Небольшая ошибка в одной из моих версий Returnn - вот и все, что касалось этой проблемы.

1 Ответ

1 голос
/ 09 августа 2020

Вы ведь знаете, что обучение не является детерминированным c в любом случае, верно? Вы пытались повторить каждый случай пару раз? Также базовый уровень? Возможно, сама базовая линия является выбросом.

Кроме того, изменение графа вычислений, даже если это не будет работать, также может иметь эффект. К сожалению, он может быть чувствительным.

Вы можете попробовать установить deterministic_train = True в своей конфигурации. Это может сделать его более детерминированным c. Может быть, вы получите тот же результат в каждом из ваших случаев. Однако это могло бы сделать его немного медленнее.

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

Для более глубокой отладки вы действительно можете напрямую сравнить график вычислений (в TensorBoard). Может быть, есть разница, которую вы не заметили. Кроме того, возможно, сделайте различие в выводе журнала во время построения net для случая pretrain vs baseline. Не должно быть различий.

(Возможно, это ошибка, пока только в качестве побочного комментария: конечно, разные версии RETURNN могут вести себя по-разному. Так что это должно быть одинаково.)

Еще одно примечание: вам не нужно это tf.reduce_sum при потере. На самом деле это может быть не такой уж и хорошей идеей. Теперь он забудет о количестве кадров и количестве последовательностей. Если вы просто не используете tf.reduce_sum, он тоже должен работать, но теперь вы получите правильную нормализацию.

Еще одно примечание: вместо вашего lambda вы также можете использовать loss_scale, что проще , и вы получите исходное значение в журнале.

В общем, вы можете записать это так:

'dec_01_weight_loss': {
   'class': 'copy', 'from': 'dec_01_att_weights',
   'loss': 'as_is', 'loss_scale': ...}

Это должно быть (в основном) эквивалентом. На самом деле это должно быть более правильным, так как он не будет учитывать замаскированные кадры (те, что за концом seq).

Обратите внимание, что использование pretrain (по умолчанию) сохранит скорость обучения фиксированной. Это может быть разница в ваших экспериментах. (Но просто проверьте для этого свой файл журнала / данных о скорости обучения.) Между прочим, если это так, похоже, фиксированная скорость обучения (возможно, более высокая скорость обучения) работает лучше, верно? Так что, может быть, вы даже захотите сделать это по умолчанию?

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

Собственно, говоря о скорости обучения: как вы настроили планирование скорости обучения? У него есть несколько "умный" лог c, чтобы определить, на какой счет смотреть (используется для порога). Если он посмотрит на некоторые из ваших индивидуальных потерь, поведение будет другим. Esp, если вы не используете loss_scale, как я объяснил, это также будет иметь значение. Вы можете настроить его явно через learning_rate_control_error_measure.

В качестве небольшой демонстрации, как вы все еще получаете ненулевой градиент, даже для 0.0 * loss:

import tensorflow as tf
import better_exchook


def main():
  max_seq_len = 15
  seq_len = 10

  logits = tf.zeros([max_seq_len])
  mask = tf.less(tf.range(max_seq_len), seq_len)
  logits_masked = tf.where(mask, logits, float("-inf"))
  ce = -tf.reduce_sum(tf.where(mask, tf.nn.softmax(logits_masked) * tf.nn.log_softmax(logits_masked), 0.0))
  loss = 0.0 * ce

  d_logits, = tf.gradients(loss, [logits])

  with tf.compat.v1.Session() as session:
    print(session.run((ce, loss, d_logits)))


if __name__ == "__main__":
  better_exchook.install()
  tf.compat.v1.disable_eager_execution()
  main()

Это будет выводить: (2.3025851, 0.0, array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, 0., 0., 0., 0., 0.], dtype=float32))

Это дает nan, но я думаю, что вы также можете создать случаи, когда вы получаете какое-то значение, отличное от inf / non-nan / ненулевое.

Если вы хотите сбрасывать градиенты на своем уровне eval или в целом в TF-коде очень простым способом вы можете сделать это:

from tensorflow.python.framework import ops


@ops.RegisterGradient("IdentityWithPrint")
def _identity_with_print(op, grad):
  with tf.control_dependencies([tf.print([op.name, "grad:", grad])]):
    return [tf.identity(grad)]


def debug_grad(x):
  """
  :param tf.Tensor x:
  :return: x, but gradient will be printed
  :rtype: tf.Tensor
  """
  g = tf.compat.v1.get_default_graph()
  with g.gradient_override_map({"Identity": "IdentityWithPrint"}):
    return tf.identity(x, name=x.name.split("/")[-1].replace(":", "_"))

И затем вы просто напишите (в начале вашего слоя eval): x = debug_grad(source(0, auto_convert=False)) Или что-то в этом роде. Можно расширить tf.print(...), например, summarize=-1.

...