簡體   English   中英

用常數零添加額外損失 output 改變 model 收斂

[英]Adding additional loss with constant zero output changes model convergence

我已經為 NMT 設置了一個 Returnn Transformer Model,我想對每個解碼器層l上的每個編碼器/解碼器注意力頭h進行額外損失訓練(除了香草交叉熵損失),即:

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

對於一些標量lambda 我使用loss=as_is選項將注意力損失本身實現為eval -Layer,它為每個批次返回一個數字(即lambda * AttentionLoss(l, h)的值。

作為測試,我還實現了一個版本,其中每一層l都有一個損失,相當於lambda * sum_{Head h=1,...,8} AttentionLoss(l, h)以減少損失的數量,因為我注意到性能下降,並且日志文件變得非常大,因為 Returnn 打印了每批的每個損失。

然而,我對這兩種實現都得到了非常不同的結果:一個 model 訓練有一個損失,每層和頭部始終表現更好。 我嘗試了多次訓練。

為了調查這一點,我嘗試了一個訓練運行,我設置了參數lambda=0.0 ,即有效地禁用了注意力損失。 即使在這里,與沒有任何額外損失的基線相比,使用這 6 個額外損失訓練的 model 都輸出常數 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 |
+--------------------------------------------+------+------+------+------+

在這里,“簡化”版本的實現方式完全一樣:

'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等)

上面提到的所有lambda=0.0實現 output 每個訓練步驟中所有額外損失的值為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的額外損失會改變 model 行為?

我正在使用 TF 1.15.0 (v1.15.0-0-g590d6eef7e),返回 20200613.152716--git-23332ca,使用 Python 3.8.0 和 ZA33B7755E5F9B504DZCA103。


后續更新:我使用預訓練測試了相同的配置,我將使用以下代碼完全禁用第一個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之前應該是相同的。 然而,它們的表現卻截然不同。 到底是怎么回事?

我用於這種預訓練的完整配置可以在這里找到。 可在此處找到相應日志文件的頭。 我正在與 Adam 一起使用 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 版本中的一個微妙錯誤 - 這就是這個問題的全部內容。

你知道訓練無論如何都是不確定的,對吧? 您是否嘗試過多次重新運行每個案例? 也是底線? 也許基線本身就是一個異常值。

此外,更改計算圖,即使這將是空操作,也會產生影響。 不幸的是,它可能很敏感。

您可能想嘗試在您的配置中設置deterministic_train = True 這可能使它更具確定性。 也許您在每種情況下都會得到相同的結果。 不過,這可能會使它變慢一些。

參數初始化的順序也可能不同。 順序取決於創建圖層的順序。 也許在日志中進行比較。 它始終是相同的隨機初始化器,但會使用不同的種子偏移量,因此您將獲得另一個初始化。 您可以通過在配置中顯式設置random_seed來玩轉,看看您會得到多少差異。 也許所有這些值都在這個范圍內。

對於更深入的調試,您可以直接比較計算圖(在 TensorBoard 中)。 也許有一個你沒有注意到的差異。 此外,對於預訓練與基線的情況,可能在網絡構建期間對日志 output 進行比較。 應該沒有差異。

(因為這可能是一個錯誤,現在僅作為旁注:當然,不同的 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 (默認情況下)將保持學習率固定。 這可能是您的實驗中的差異。 (但只需檢查您的日志/學習率數據文件。)順便說一句,如果是這種情況,看起來固定學習率(可能更高的學習率)似乎表現更好,對吧? 所以也許你甚至想默認這樣做?

還要檢查您的日志中的“重新初始化,因為網絡描述不同”。 這應該沒有什么大的影響,但誰知道呢。 這也將重置優化器的當前 state (動量左右;我猜你使用亞當?)。 但即使有預訓練,我認為你不會有這個,因為你總是保持網絡不變。

實際上,說到學習率:你是如何配置學習率調度的? 它有一個有點“聰明”的邏輯來確定要查看的分數(用於閾值)。 如果它查看您的一些自定義損失,則行為會有所不同。 特別是如果你不按照我解釋的那樣使用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()

這將 output: (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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM