添加具有恒定零输出的额外损失会改变模型收敛性
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
),每个实验 运行 多次:
- 基线(无额外损失):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
之前应该是相同的。然而,它们的表现却截然不同。怎么回事?
可以找到我用于这种预训练的完整配置 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
.
我已经为 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
),每个实验 运行 多次:
- 基线(无额外损失):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
之前应该是相同的。然而,它们的表现却截然不同。怎么回事?
可以找到我用于这种预训练的完整配置 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
:
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
.