添加具有恒定零输出的额外损失会改变模型收敛性

Adding additional loss with constant zero output changes model convergence

我已经为 NMT 设置了一个 Return Transformer 模型,我想在每个解码器层 l 上对每个 encoder/decoder 注意力头 h 进行额外的损失训练(此外香草交叉熵损失),即:

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

对于某些标量 lambda。我将注意力损失本身实现为 eval 层,使用 loss=as_is 选项,returns 每个批次的单个数字(即 lambda * AttentionLoss(l, h) 的值)。

作为测试,我还实现了一个版本,其中每一层都有一个损失 l,相当于 lambda * sum_{Head h=1,...,8} AttentionLoss(l, h) 以减少损失数量,因为我注意到性能下降,并且日志文件变得非常大,因为 Returnn 会打印每批次的所有损失。

但是,我得到了两种实现方式截然不同的结果:每层损失一次且头部训练的模型始终表现更好。我尝试了多次训练 运行s.

为了对此进行调查,我尝试了训练 运行,我在其中设置了参数 lambda=0.0,即有效地阻止了注意力丧失。即使在这里,与没有任何额外损失的基线相比,用这 6 个额外损失训练的模型都输出常量 0 的性能明显更差,请参见 table:

+--------------------------------------------+-------------+-------------+
|                                            |   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 |
+--------------------------------------------+------+------+------+------+

在这里,“简化”版本的实现完全是这样的:

'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}}

虽然我使用的实际损失有点复杂,但我uploaded my full config files here.(这里损失层称为dec_01_att_weight_variance等)

并且上面提到的所有 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),Return 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),每个实验 运行 多次:

据我了解,预训练配置的 TF 图和基线在检查点 n=50 之前应该是相同的。然而,它们的表现却截然不同。怎么回事?

可以找到我用于这种预训练的完整配置 here. The heads of the corresponding log files are found here。 我正在将 NewbobMultiEpoch 与 Adam 一起使用:

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

编辑: 我犯了一个错误,不小心 在我的实验中使用了不同的 Return 版本 。我在带有额外损失的实验中使用的 Returnn 似乎包含了我所做的一些局部更改。当重新运行使用新版本的基线时,它的性能明显更差 - 与此处记录的其他 BLEU 值非常相似。 我的 Returnn 版本中的一个细微错误 - 这就是全部这个问题。

你知道培训是 non-deterministic 无论如何,对吧?您是否尝试过多次重新运行每个案例?也是底线?也许基线本身就是一个异常值。

此外,更改计算图,即使这将是 no-op,也会产生影响。不幸的是它可能很敏感。

您可能想尝试在您的配置中设置 deterministic_train = True。这可能会使它更具确定性。也许您在每种情况下都会得到相同的结果。不过,这可能会使速度变慢。

参数初始化的顺序也可能不同。顺序取决于图层创建的顺序。也许在日志中比较一下。它始终是相同的随机初始化器,但会使用不同的种子偏移量,因此您将获得另一个初始化。 您可以通过在配置中显式设置 random_seed 来尝试一下,看看由此获得的差异有多大。也许所有这些值都在这个范围内。

要进行更多 in-depth 调试,您真的可以直接比较计算图(在 TensorBoard 中)。也许有你没有注意到的差异。此外,对于预训练与基线的情况,可能会在网络构建期间对日志输出进行区分。应该没有差异。

(因为这可能是一个错误,现在仅作为旁注:当然,不同的 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 end 后面的那些)。

请注意,使用 pretrain(默认情况下)将使学习率保持固定。这可能与您的实验有所不同。 (但只需为此检查您的日志/学习率数据文件。) 顺便说一句,如果是这样的话,看起来固定学习率(可能更高的学习率)似乎表现得更好,对吧?所以也许你甚至想默认这样做?

同时检查您的日志以了解“由于网络描述不同而重新启动”。这应该不会有太大影响,但谁知道呢。这也将重置优化器的当前状态(momentum 左右;我猜你使用 Adam?)。但即使使用预训练,我认为你也不会有这个,因为你始终保持网络不变。

其实说到学习率:你是怎么配置学习率调度的?它有一个有点“聪明”的逻辑来确定要查看的分数(用于阈值)。如果它查看您的一些自定义损失,则行为会有所不同。 Esp 如果你不使用 loss_scale 正如我解释的那样,这也会起作用。 您可以通过 learning_rate_control_error_measure.

显式配置它

作为一个小演示,即使对于 0.0 * loss:

,您仍然可以获得一些 non-zero 渐变
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,但我认为您也可以构建获得一些 non-inf/non-nan/non-zero 值的案例。

如果你想在你的 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(":", "_"))

然后你只需要写(在你的评估层的开头): x = debug_grad(source(0, auto_convert=False)) 或者诸如此类。 也许扩展 tf.print(...),例如summarize=-1.