web-dev-qa-db-fra.com

Quelle est la bonne façon de perdre du poids pour Adam Optimizer

Adam Optimizer conservant une paire de moyennes courantes telles que moyenne/variance pour les gradients, je me demande comment il devrait gérer correctement la perte de poids. J'ai vu deux façons de le mettre en œuvre.

  1. Seulement mettre à jour la moyenne/variance à partir des gradients en fonction de la perte objective, du poids de décroissance explicitement à chaque mini-lot. (le code suivant provient de https://github.com/dmlc/mxnet/blob/v0.7.0/python/mxnet/optimizer.py )

    weight[:] -= lr*mean/(sqrt(variance) + self.epsilon)
    
    wd = self._get_wd(index)
    if wd > 0.:
        weight[:] -= (lr * wd) * weight
    
  2. Mettez à jour la moyenne/variance à partir des gradients en fonction de la perte objective + perte de régularisation, et mettez à jour les poids comme d'habitude. (le code suivant provient de https://github.com/dmlc/mxnet/blob/master/src/operator/optimizer_op-inl.h#L210 )

    grad = scalar<DType>(param.rescale_grad) * grad +
    scalar<DType>(param.wd) * weight;
    // stuff
    Assign(out, req[0],
       weight -
       scalar<DType>(param.lr) * mean /
       (F<square_root>(var) + scalar<DType>(param.epsilon)));
    

Ces deux approches montrent parfois une différence significative dans les résultats d’entraînement. Et je pense en fait que le premier a plus de sens (et trouve que cela donne de meilleurs résultats de temps en temps). Caffe et l'ancienne version de mxnet suivent la première approche, tandis que torch, tensorflow et la nouvelle version de mxnet suivent la seconde. 

Apprécions vraiment votre aide

7
Xinyu Zhang

Edit: voir aussi ce PR qui vient d'être fusionné dans TF.

Lorsque vous utilisez un SGD pur (sans élan) comme optimiseur, la décroissance du poids revient à ajouter un terme de régularisation L2 à la perte. Lorsque vous utilisez un autre optimiseur, ce n'est pas vrai.

Poids en déclin (je ne sais pas comment te nuancer ici, alors excuse ma pseudo-notation):

w[t+1] = w[t] - learning_rate * dw - weight_decay * w

L2-régularisation:

loss = actual_loss + lambda * 1/2 sum(||w||_2 for w in network_params)

Le calcul du gradient du terme supplémentaire dans la régularisation de L2 donne lambda * w et l'insère ainsi dans l'équation de mise à jour de SGD

dloss_dw = dactual_loss_dw + lambda * w
w[t+1] = w[t] - learning_rate * dw

donne la même chose que la décroissance du poids, mais mélange lambda avec le learning_rate. Tout autre optimiseur, même SGD avec élan, donne une règle de mise à jour différente pour la perte de poids comme pour la régularisation L2! Voir le document Correction de la décroissance du poids en Adam pour plus de détails. (Edit: autant que je sache, le { ce document de Hinton de 1987 } _ a introduit la "décroissance du poids", littéralement "chaque fois que les poids sont mis à jour, leur ampleur est également diminuée de 0,4%" à la page 10)

Cela étant dit, TensorFlow ne semble pas encore pris en charge. Quelques points en discutent, notamment à cause du document ci-dessus.

Une façon possible de le mettre en œuvre consiste à écrire une opération qui effectue l’étape de désintégration manuellement après chaque étape de l’optimiseur. Une méthode différente, ce que je suis en train de faire, consiste à utiliser un optimiseur SGD supplémentaire uniquement pour réduire le poids et à le "joindre" à votre train_op. Les deux ne sont cependant que des solutions brutes. Mon code actuel:

# In the network definition:
with arg_scope([layers.conv2d, layers.dense],
               weights_regularizer=layers.l2_regularizer(weight_decay)):
    # define the network.

loss = # compute the actual loss of your problem.
train_op = optimizer.minimize(loss, global_step=global_step)
if args.weight_decay not in (None, 0):
    with tf.control_dependencies([train_op]):
        sgd = tf.train.GradientDescentOptimizer(learning_rate=1.0)
        train_op = sgd.minimize(tf.add_n(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)))

Cela utilise un peu la comptabilité fournie par TensorFlow. Notez que le arg_scope s'occupe de l'ajout d'un terme de régularisation L2 pour chaque couche à la REGULARIZATION_LOSSES graph-key, que je résume et optimise ensuite à l'aide de SGD qui, comme indiqué ci-dessus, correspond à la perte de poids réelle.

J'espère que cela aidera, et si quelqu'un obtient un extrait de code plus agréable pour cela, ou que TensorFlow l'implémente mieux (c'est-à-dire dans les optimiseurs), partagez-le.

5
LucasB

Je suis tombé sur la même question. Je pense que ce code que je viens de ici travaillera pour vous. Il implémente l’optimiseur adam optimisation de la perte de poids en héritant du tf.train.Optimizer. C'est la solution la plus propre que j'ai trouvée:

class AdamWeightDecayOptimizer(tf.train.Optimizer):
"""A basic Adam optimizer that includes "correct" L2 weight decay."""

def __init__(self,
             learning_rate,
             weight_decay_rate=0.0,
             beta_1=0.9,
             beta_2=0.999,
             epsilon=1e-6,
             exclude_from_weight_decay=None,
             name="AdamWeightDecayOptimizer"):
  """Constructs a AdamWeightDecayOptimizer."""
  super(AdamWeightDecayOptimizer, self).__init__(False, name)

  self.learning_rate = learning_rate
  self.weight_decay_rate = weight_decay_rate
  self.beta_1 = beta_1
  self.beta_2 = beta_2
  self.epsilon = epsilon
  self.exclude_from_weight_decay = exclude_from_weight_decay

def apply_gradients(self, grads_and_vars, global_step=None, name=None):
  """See base class."""
  assignments = []
  for (grad, param) in grads_and_vars:
    if grad is None or param is None:
      continue

    param_name = self._get_variable_name(param.name)

    m = tf.get_variable(
        name=param_name + "/adam_m",
        shape=param.shape.as_list(),
        dtype=tf.float32,
        trainable=False,
        initializer=tf.zeros_initializer())
    v = tf.get_variable(
        name=param_name + "/adam_v",
        shape=param.shape.as_list(),
        dtype=tf.float32,
        trainable=False,
        initializer=tf.zeros_initializer())

    # Standard Adam update.
    next_m = (
        tf.multiply(self.beta_1, m) + tf.multiply(1.0 - self.beta_1, grad))
    next_v = (
        tf.multiply(self.beta_2, v) + tf.multiply(1.0 - self.beta_2,
                                                  tf.square(grad)))

    update = next_m / (tf.sqrt(next_v) + self.epsilon)

    # Just adding the square of the weights to the loss function is *not*
    # the correct way of using L2 regularization/weight decay with Adam,
    # since that will interact with the m and v parameters in strange ways.
    #
    # Instead we want ot decay the weights in a manner that doesn't interact
    # with the m/v parameters. This is equivalent to adding the square
    # of the weights to the loss with plain (non-momentum) SGD.
    if self._do_use_weight_decay(param_name):
      update += self.weight_decay_rate * param

    update_with_lr = self.learning_rate * update

    next_param = param - update_with_lr

    assignments.extend(
        [param.assign(next_param),
         m.assign(next_m),
         v.assign(next_v)])
  return tf.group(*assignments, name=name)

def _do_use_weight_decay(self, param_name):
  """Whether to use L2 weight decay for `param_name`."""
  if not self.weight_decay_rate:
    return False
  if self.exclude_from_weight_decay:
    for r in self.exclude_from_weight_decay:
      if re.search(r, param_name) is not None:
        return False
  return True

def _get_variable_name(self, param_name):
  """Get the variable name from the tensor name."""
  m = re.match("^(.*):\\d+$", param_name)
  if m is not None:
    param_name = m.group(1)
  return param_name

Et vous pouvez l'utiliser de la manière suivante (j'ai apporté quelques modifications pour le rendre utile dans un contexte plus général), cette fonction renverra un train_op pouvant être utilisé dans la session:

def create_optimizer(loss, init_lr, num_train_steps, num_warmup_steps):
  """Creates an optimizer training op."""
  global_step = tf.train.get_or_create_global_step()

  learning_rate = tf.constant(value=init_lr, shape=[], dtype=tf.float32)

  # Implements linear decay of the learning rate.
  learning_rate = tf.train.polynomial_decay(
      learning_rate,
      global_step,
      num_train_steps,
      end_learning_rate=0.0,
      power=1.0,
      cycle=False)

  # Implements linear warmup. I.e., if global_step < num_warmup_steps, the
  # learning rate will be `global_step/num_warmup_steps * init_lr`.
  if num_warmup_steps:
    global_steps_int = tf.cast(global_step, tf.int32)
    warmup_steps_int = tf.constant(num_warmup_steps, dtype=tf.int32)

    global_steps_float = tf.cast(global_steps_int, tf.float32)
    warmup_steps_float = tf.cast(warmup_steps_int, tf.float32)

    warmup_percent_done = global_steps_float / warmup_steps_float
    warmup_learning_rate = init_lr * warmup_percent_done

    is_warmup = tf.cast(global_steps_int < warmup_steps_int, tf.float32)
    learning_rate = (
        (1.0 - is_warmup) * learning_rate + is_warmup * warmup_learning_rate)

  # It is recommended that you use this optimizer for fine tuning, since this
  # is how the model was trained (note that the Adam m/v variables are NOT
  # loaded from init_checkpoint.)
  optimizer = AdamWeightDecayOptimizer(
      learning_rate=learning_rate,
      weight_decay_rate=0.01,
      beta_1=0.9,
      beta_2=0.999,
      epsilon=1e-6)


  tvars = tf.trainable_variables()
  grads = tf.gradients(loss, tvars)

  # You can do clip gradients if you need in this step(in general it is not neccessary)
  # (grads, _) = tf.clip_by_global_norm(grads, clip_norm=1.0)

  train_op = optimizer.apply_gradients(
      Zip(grads, tvars), global_step=global_step)

  # Normally the global step update is done inside of `apply_gradients`.
  # However, `AdamWeightDecayOptimizer` doesn't do this. But if you use
  # a different optimizer, you should probably take this line out.
  new_global_step = global_step + 1
  train_op = tf.group(train_op, [global_step.assign(new_global_step)])
  return train_op
0
Rohola Zandie