Guida avanzata a Inception v3

Questo documento illustra alcuni aspetti del modello Inception e il modo in cui vengono combinati per eseguire in modo efficiente il modello su Cloud TPU. Si tratta di una visualizzazione avanzata della guida all'esecuzione di Inception v3 su Cloud TPU. Vengono discusse nel dettaglio le modifiche specifiche al modello che hanno portato a miglioramenti significativi. Questo documento integra il tutorial di Inception v3.

Le esecuzioni di addestramento TPU di Inception v3 corrispondono alle curve di accuratezza prodotte da job GPU di configurazione simile. Il modello è stato addestrato con le configurazioni v2-8, v2-128 e v2-512. Il modello ha raggiunto un'accuratezza superiore al 78,1% in circa 170 epoche.

Gli esempi di codice mostrati in questo documento hanno uno scopo illustrativo, ovvero un quadro generale di ciò che accade nell'effettiva implementazione. Il codice funzionante è disponibile su GitHub.

Introduzione

Inception v3 è un modello di riconoscimento delle immagini che ha dimostrato di raggiungere un'accuratezza superiore al 78,1% sul set di dati ImageNet. Il modello è il culmine di molte idee sviluppate da diversi ricercatori nel corso degli anni. Si basa sul documento originale: "Rethinking the Inception Architecture for Computer Vision"di Szegedy et. al.

Il modello stesso è composto da componenti di base simmetrici e asimmetrici, tra cui convoluzioni, pooling medio, massimo pooling, concatenazioni, abbandoni e livelli completamente connessi. La normalizzazione batch viene utilizzata ampiamente in tutto il modello e applicata agli input di attivazione. La perdita viene calcolata utilizzando Softmax.

Nello screenshot seguente è riportato un diagramma di alto livello del modello:

immagine

API Estimator

La versione TPU di Inception v3 è scritta utilizzando TPUEstimator, un'API progettata per facilitare lo sviluppo, in modo che tu possa concentrarti sui modelli stessi anziché sui dettagli dell'hardware sottostante. L'API svolge la maggior parte del lavoro grunge di basso livello necessario per l'esecuzione di modelli sulle TPU dietro le quinte, automatizzando al contempo le funzioni comuni, come il salvataggio e il ripristino dei checkpoint.

L'API Estimator applica la separazione delle parti del modello e di input del codice. Devi definire le funzioni model_fn e input_fn, in base alla definizione del modello e alla pipeline di input. Il seguente codice mostra la dichiarazione di queste funzioni:

def model_fn(features, labels, mode, params):
     …
  return tpu_estimator.TPUEstimatorSpec(mode=mode, loss=loss, train_op=train_op)

def input_fn(params):
    def parser(serialized_example):
          …
        return image, label

          …
   images, labels = dataset.make_one_shot_iterator().get_next()
   return images, labels

Due funzioni chiave fornite dall'API sono train() e evaluate() utilizzate per addestrare e valutare, come mostrato nel seguente codice:

def main(unused_argv):
  …
  run_config = tpu_config.RunConfig(
      master=FLAGS.master,
      model_dir=FLAGS.model_dir,
      session_config=tf.ConfigProto(
          allow_soft_placement=True, log_device_placement=True),
      tpu_config=tpu_config.TPUConfig(FLAGS.iterations, FLAGS.num_shards),)

  estimator = tpu_estimator.TPUEstimator(
      model_fn=model_fn,
      use_tpu=FLAGS.use_tpu,
      train_batch_size=FLAGS.batch_size,
      eval_batch_size=FLAGS.batch_size,
      config=run_config)

  estimator.train(input_fn=input_fn, max_steps=FLAGS.train_steps)

  eval_results = inception_classifier.evaluate(
      input_fn=imagenet_eval.input_fn, steps=eval_steps)

set di dati ImageNet

Prima che il modello possa essere utilizzato per riconoscere le immagini, deve essere addestrato utilizzando un ampio set di immagini etichettate. ImageNet è un set di dati comune.

ImageNet ha oltre dieci milioni di URL di immagini etichettate. Un milione di immagini dispone anche di riquadri di delimitazione che specificano una posizione più precisa per gli oggetti etichettati.

Per questo modello, il set di dati ImageNet è composto da 1.331.167 immagini, suddivise in set di dati di addestramento e valutazione contenenti rispettivamente 1.281.167 e 50.000 immagini.

I set di dati di addestramento e valutazione vengono mantenuti separati intenzionalmente. Per l'addestramento del modello vengono utilizzate solo le immagini del set di dati di addestramento e solo le immagini del set di dati di valutazione.

Il modello prevede che le immagini vengano archiviate come TFRecord. Per ulteriori informazioni su come convertire le immagini da file JPEG non elaborati in TFRecord, vedi download_and_preprocess_imagenet.sh.

Pipeline di input

Ogni dispositivo Cloud TPU ha 8 core ed è connesso a un host (CPU). Le sezioni più grandi hanno più host. Altre configurazioni più grandi interagiscono con più host. Ad esempio, una versione v2-256 comunica con 16 host.

Gli host recuperano i dati dal file system o dalla memoria locale, eseguono le operazioni di pre-elaborazione richieste e trasferiscono i dati pre-elaborati ai core TPU. Consideriamo queste tre fasi della gestione dei dati eseguite dall'host individualmente e ci riferiamo a: 1) Archiviazione, 2) Pre-elaborazione, 3) Trasferimento. Nella figura seguente è riportata un'immagine generale del diagramma:

immagine

Per ottenere buone prestazioni, il sistema deve essere bilanciato. Se la CPU host impiega più tempo della TPU per completare le tre fasi di gestione dei dati, l'esecuzione sarà vincolata all'host. Entrambi i casi sono mostrati nel seguente schema:

immagine

L'attuale implementazione di Inception v3 è quasi al limite dell'input. Le immagini vengono recuperate dal file system, decodificate e quindi pre-elaborate. Sono disponibili diversi tipi di fasi di pre-elaborazione, da quelle moderate a quelle complesse. Se utilizziamo le fasi di pre-elaborazione più complesse, la pipeline di addestramento verrà associata alla pre-elaborazione. Puoi ottenere un'accuratezza superiore al 78,1% utilizzando una fase di pre-elaborazione moderatamente complessa che mantiene il vincolo al modello TPU.

Il modello utilizza tf.data.Dataset per gestire l'elaborazione della pipeline di input. Per ulteriori informazioni su come ottimizzare le pipeline di input, consulta la guida alle prestazioni dei set di dati.

Sebbene sia possibile definire una funzione e passarla all'API Estimator, la classe InputPipeline incapsula tutte le funzionalità richieste.

L'API Estimator semplifica l'uso di questa classe. Lo passi al parametro input_fn delle funzioni train() e evaluate(), come mostrato nel seguente snippet di codice:

def main(unused_argv):

          …

  inception_classifier = tpu_estimator.TPUEstimator(
      model_fn=inception_model_fn,
      use_tpu=FLAGS.use_tpu,
      config=run_config,
      params=params,
      train_batch_size=FLAGS.train_batch_size,
      eval_batch_size=eval_batch_size,
      batch_axis=(batch_axis, 0))

          …

  for cycle in range(FLAGS.train_steps // FLAGS.train_steps_per_eval):
    tf.logging.info('Starting training cycle %d.' % cycle)
    inception_classifier.train(
        input_fn=InputPipeline(True), steps=FLAGS.train_steps_per_eval)

    tf.logging.info('Starting evaluation cycle %d .' % cycle)
    eval_results = inception_classifier.evaluate(
        input_fn=InputPipeline(False), steps=eval_steps, hooks=eval_hooks)
    tf.logging.info('Evaluation results: %s' % eval_results)

Gli elementi principali di InputPipeline sono mostrati nello snippet di codice riportato di seguito.

class InputPipeline(object):

  def __init__(self, is_training):
    self.is_training = is_training

  def __call__(self, params):
    # Storage
    file_pattern = os.path.join(
        FLAGS.data_dir, 'train-*' if self.is_training else 'validation-*')
    dataset = tf.data.Dataset.list_files(file_pattern)
    if self.is_training and FLAGS.initial_shuffle_buffer_size > 0:
      dataset = dataset.shuffle(
          buffer_size=FLAGS.initial_shuffle_buffer_size)
    if self.is_training:
      dataset = dataset.repeat()

    def prefetch_dataset(filename):
      dataset = tf.data.TFRecordDataset(
          filename, buffer_size=FLAGS.prefetch_dataset_buffer_size)
      return dataset

    dataset = dataset.apply(
        tf.contrib.data.parallel_interleave(
            prefetch_dataset,
            cycle_length=FLAGS.num_files_infeed,
            sloppy=True))
    if FLAGS.followup_shuffle_buffer_size > 0:
      dataset = dataset.shuffle(
          buffer_size=FLAGS.followup_shuffle_buffer_size)

    # Preprocessing
    dataset = dataset.map(
        self.dataset_parser,
        num_parallel_calls=FLAGS.num_parallel_calls)

    dataset = dataset.prefetch(batch_size)
    dataset = dataset.apply(
        tf.contrib.data.batch_and_drop_remainder(batch_size))
    dataset = dataset.prefetch(2)  # Prefetch overlaps in-feed with training
    images, labels = dataset.make_one_shot_iterator().get_next()

    # Transfer
    return images, labels

La sezione storage inizia con la creazione di un set di dati e include la lettura dei TFRecord dall'archiviazione (utilizzando tf.data.TFRecordDataset). Le funzioni speciali repeat() e shuffle() vengono utilizzate in base alle esigenze. La funzione tf.contrib.data.parallel_interleave() mappa la funzione prefetch_dataset() sul suo input per produrre set di dati nidificati e restituisce i relativi elementi interlealati. Recupera gli elementi da cycle_length set di dati nidificati in parallelo, il che aumenta la velocità effettiva. L'argomento sloppy riduce il requisito secondo cui gli output devono essere prodotti in un ordine deterministico e consente all'implementazione di saltare i set di dati nidificati i cui elementi non sono immediatamente disponibili quando richiesto.

La sezione pre-elaborazione chiama dataset.map(parser), che a sua volta chiama la funzione parser dove le immagini vengono pre-elaborate. I dettagli della fase di pre-elaborazione sono descritti nella sezione successiva.

La sezione transfer (trasferimento) (alla fine della funzione) include la riga return images, labels. TPUEstimator prende i valori restituiti e li trasferisce automaticamente al dispositivo.

La figura seguente mostra un esempio di traccia delle prestazioni di Cloud TPU di Inception v3. Il tempo di calcolo della TPU, ignorando eventuali chioschi durante l'alimentazione, è di circa 815 msec.

immagine

Lo spazio di archiviazione dell'host viene scritto sulla traccia e mostrato nello screenshot seguente:

immagine

Nella seguente screenshot è mostrata la pre-elaborazione dell'host, che include la decodifica delle immagini e una serie di funzioni di distorsione delle immagini:

immagine

Il trasferimento host/TPU è mostrato nel seguente screenshot:

immagine

Fase di pre-elaborazione

La pre-elaborazione delle immagini è una parte fondamentale del sistema e può influenzare la massima precisione che il modello raggiunge durante l'addestramento. Come minimo, le immagini devono essere decodificate e ridimensionate per adattarsi al modello. Per Inception, le immagini devono essere di 299 x 299 x 3 pixel.

Tuttavia, la semplice decodifica e ridimensionamento non sono sufficienti per ottenere una buona precisione. Il set di dati di addestramento ImageNet contiene 1.281.167 immagini. Un passaggio sull'insieme di immagini di addestramento è definito epoca. Durante l'addestramento, il modello richiede diversi passaggi del set di dati di addestramento per migliorare le sue capacità di riconoscimento delle immagini. Per addestrare Inception v3 con una precisione sufficiente, utilizza un numero di epoche compreso tra 140 e 200 a seconda delle dimensioni globali del batch.

È utile modificare continuamente le immagini prima di aggiungerle al modello, in modo che un'immagine particolare sia leggermente diversa a ogni epoca. Il modo migliore per eseguire questa pre-elaborazione delle immagini è tanto arte quanto scienza. Una fase di pre-elaborazione ben progettata può migliorare notevolmente le capacità di riconoscimento di un modello. Una fase di pre-elaborazione troppo semplice può creare un limite artificiale di precisione che lo stesso modello può raggiungere durante l'addestramento.

Inception v3 offre opzioni per la fase di pre-elaborazione, da quelle relativamente semplici e poco costose a livello di calcolo a quelle piuttosto complesse e costose in termini di calcolo. Puoi trovare due versioni diverse nei file vgg_preprocessing.py e inception_preprocessing.py.

Il file vgg_preprocessing.py definisce una fase di pre-elaborazione che è stata utilizzata correttamente per addestrare resnet con un'accuratezza del 75%, ma che restituisce risultati non ottimali se applicato su Inception v3.

Il file inception_preprocessing.py contiene una fase di pre-elaborazione che è stata utilizzata per addestrare Inception v3 con precisione comprese tra il 78,1 e il 78,5% quando viene eseguito su TPU.

La pre-elaborazione varia a seconda che il modello sia in fase di addestramento o di utilizzo per l'inferenza/valutazione.

Al momento della valutazione, la pre-elaborazione è semplice: ritaglia un'area centrale dell'immagine e poi ridimensionala alle dimensioni predefinite (299 x 299). Il seguente snippet di codice mostra un'implementazione di pre-elaborazione:

def preprocess_for_eval(image, height, width, central_fraction=0.875):
  with tf.name_scope(scope, 'eval_image', [image, height, width]):
    if image.dtype != tf.float32:
      image = tf.image.convert_image_dtype(image, dtype=tf.float32)
    image = tf.image.central_crop(image, central_fraction=central_fraction)
    image = tf.expand_dims(image, 0)
    image = tf.image.resize_bilinear(image, [height, width], align_corners=False)
    image = tf.squeeze(image, [0])
    image = tf.subtract(image, 0.5)
    image = tf.multiply(image, 2.0)
    image.set_shape([height, width, 3])
    return image

Durante l'addestramento, il ritaglio viene eseguito in modo casuale: viene scelto in modo casuale un riquadro di delimitazione per selezionare un'area dell'immagine che viene poi ridimensionata. L'immagine ridimensionata viene poi capovolta e i suoi colori vengono distorti. Il seguente snippet di codice mostra un'implementazione di queste operazioni:

def preprocess_for_train(image, height, width, bbox, fast_mode=True, scope=None):
  with tf.name_scope(scope, 'distort_image', [image, height, width, bbox]):
    if bbox is None:
      bbox = tf.constant([0.0, 0.0, 1.0, 1.0], dtype=tf.float32, shape=[1, 1, 4])
    if image.dtype != tf.float32:
      image = tf.image.convert_image_dtype(image, dtype=tf.float32)

    distorted_image, distorted_bbox = distorted_bounding_box_crop(image, bbox)
    distorted_image.set_shape([None, None, 3])

    num_resize_cases = 1 if fast_mode else 4
    distorted_image = apply_with_random_selector(
        distorted_image,
        lambda x, method: tf.image.resize_images(x, [height, width], method),
        num_cases=num_resize_cases)

    distorted_image = tf.image.random_flip_left_right(distorted_image)

    if FLAGS.use_fast_color_distort:
      distorted_image = distort_color_fast(distorted_image)
    else:
      num_distort_cases = 1 if fast_mode else 4
      distorted_image = apply_with_random_selector(
          distorted_image,
          lambda x, ordering: distort_color(x, ordering, fast_mode),
          num_cases=num_distort_cases)

    distorted_image = tf.subtract(distorted_image, 0.5)
    distorted_image = tf.multiply(distorted_image, 2.0)
    return distorted_image

La funzione distort_color è responsabile dell'alterazione dei colori. Offre una modalità veloce in cui vengono modificate solo la luminosità e la saturazione. La modalità Completa modifica luminosità, saturazione e tonalità in ordine casuale.

def distort_color(image, color_ordering=0, fast_mode=True, scope=None):
  with tf.name_scope(scope, 'distort_color', [image]):
    if fast_mode:
      if color_ordering == 0:
        image = tf.image.random_brightness(image, max_delta=32. / 255.)
        image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
      else:
        image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
        image = tf.image.random_brightness(image, max_delta=32. / 255.)
    else:
      if color_ordering == 0:
        image = tf.image.random_brightness(image, max_delta=32. / 255.)
        image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
        image = tf.image.random_hue(image, max_delta=0.2)
        image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
      elif color_ordering == 1:
        image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
        image = tf.image.random_brightness(image, max_delta=32. / 255.)
        image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
        image = tf.image.random_hue(image, max_delta=0.2)
      elif color_ordering == 2:
        image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
        image = tf.image.random_hue(image, max_delta=0.2)
        image = tf.image.random_brightness(image, max_delta=32. / 255.)
        image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
      elif color_ordering == 3:
        image = tf.image.random_hue(image, max_delta=0.2)
        image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
        image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
        image = tf.image.random_brightness(image, max_delta=32. / 255.)

    return tf.clip_by_value(image, 0.0, 1.0)

La funzione distort_color è costosa dal punto di vista del calcolo, in parte a causa delle conversioni da RGB non lineari a HSV e da HSV a RGB necessarie per accedere a tonalità e saturazione. Sia la modalità veloce che quella completa richiedono queste conversioni e, sebbene la modalità veloce sia meno costosa dal punto di vista del calcolo, invia comunque il modello alla regione vincolata al calcolo della CPU, se abilitata.

In alternativa, è stata aggiunta una nuova funzione distort_color_fast all'elenco di opzioni. Questa funzione mappa l'immagine da RGB a YCrCb utilizzando lo schema di conversione JPEG e modifica in modo casuale la luminosità e i cromi Cr/Cb prima di mapparla di nuovo a RGB. Il seguente snippet di codice mostra un'implementazione di questa funzione:

def distort_color_fast(image, scope=None):
  with tf.name_scope(scope, 'distort_color', [image]):
    br_delta = random_ops.random_uniform([], -32./255., 32./255., seed=None)
    cb_factor = random_ops.random_uniform(
        [], -FLAGS.cb_distortion_range, FLAGS.cb_distortion_range, seed=None)
    cr_factor = random_ops.random_uniform(
        [], -FLAGS.cr_distortion_range, FLAGS.cr_distortion_range, seed=None)

    channels = tf.split(axis=2, num_or_size_splits=3, value=image)
    red_offset = 1.402 * cr_factor + br_delta
    green_offset = -0.344136 * cb_factor - 0.714136 * cr_factor + br_delta
    blue_offset = 1.772 * cb_factor + br_delta
    channels[0] += red_offset
    channels[1] += green_offset
    channels[2] += blue_offset
    image = tf.concat(axis=2, values=channels)
    image = tf.clip_by_value(image, 0., 1.)
    return image

Di seguito è riportato un esempio di immagine che è stata sottoposta a pre-elaborazione. È stata selezionata una regione scelta in modo casuale dell'immagine e i colori sono stati modificati utilizzando la funzione distort_color_fast.

immagine

La funzione distort_color_fast è computazionale efficiente e consente comunque all'addestramento di essere vincolato al tempo di esecuzione della TPU. Inoltre, è stato utilizzato per addestrare il modello Inception v3 con una precisione superiore al 78,1% utilizzando dimensioni dei batch nell'intervallo 1024-16.384.

Ottimizzatore

Il modello attuale presenta tre tipi di ottimizzazione: SGD, Momentum e RMSProp.

Stochastic gradient descent (SGD) è l'aggiornamento più semplice: le ponderazioni vengono spostate nella direzione del gradiente negativo. Nonostante la semplicità, alcuni modelli hanno comunque buoni risultati. Le dinamiche degli aggiornamenti possono essere scritte come:

$$w_{k+1}=w_k-\alpha∇f(w_k)$$

Momentum è un noto strumento di ottimizzazione che spesso porta a una convergenza più rapida rispetto a SGD. Questo ottimizzatore aggiorna le ponderazioni in modo molto simile a SGD, ma aggiunge anche un componente nella direzione dell'aggiornamento precedente. Le seguenti equazioni descrivono gli aggiornamenti eseguiti dall'ottimizzatore di moto:

$$z_{k+1}=\beta z_k+∇f(w_k)$$
$$w_{k+1}=w_k-\alpha z_{k+1}$$

che può essere scritta come segue:

$$w_{k+1}=w_k-\alpha ∇f(w_k)+\beta \left(w_k-w_{k-1}\right)$$

L'ultimo termine indica il componente nella direzione dell'aggiornamento precedente.

immagine

Per l'impulso \({\beta}\), usiamo il valore di 0.9.

RMSprop è un popolare ottimizzatore proposto per la prima volta da Geoff Hinton in una delle sue lezioni. Le seguenti equazioni descrivono il funzionamento dell'ottimizzatore:

$$g_{k+1}^{-2} = \alpha g_{k}^{-2} + (1-\alpha) g_{k}^2$$ $$w_{k+1}=\beta w_k + \frac{\eta}{\sqrt {g_{k+1^{\mathf}}$}}

Per Inception v3, i test mostrano che RMSProp offre i risultati migliori in termini di massima precisione e tempo per raggiungerlo, con uno slancio al secondo. Di conseguenza, RMSprop è impostato come ottimizzatore predefinito. I parametri utilizzati sono: decay \({\alpha}\) = 0.9, momentum \({\beta}\) = 0.9 e \({\epsilon}\) = 1.0.

Il seguente snippet di codice mostra come impostare questi parametri:

if FLAGS.optimizer == 'sgd':
  tf.logging.info('Using SGD optimizer')
  optimizer = tf.train.GradientDescentOptimizer(
      learning_rate=learning_rate)
elif FLAGS.optimizer == 'momentum':
  tf.logging.info('Using Momentum optimizer')
  optimizer = tf.train.MomentumOptimizer(
      learning_rate=learning_rate, momentum=0.9)
elif FLAGS.optimizer == 'RMS':
  tf.logging.info('Using RMS optimizer')
  optimizer = tf.train.RMSPropOptimizer(
      learning_rate,
      RMSPROP_DECAY,
      momentum=RMSPROP_MOMENTUM,
      epsilon=RMSPROP_EPSILON)
else:
  tf.logging.fatal('Unknown optimizer:', FLAGS.optimizer)

Durante l'esecuzione su TPU e se utilizzi l'API Estimator, l'ottimizzatore deve essere aggregato in una funzione CrossShardOptimizer per garantire la sincronizzazione tra le repliche (insieme a eventuali comunicazioni cross-device necessarie). Il seguente snippet di codice mostra in che modo il modello Inception v3 esegue il wrapping dell'ottimizzatore:

if FLAGS.use_tpu:
    optimizer = tpu_optimizer.CrossShardOptimizer(optimizer)

update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
with tf.control_dependencies(update_ops):
  train_op = optimizer.minimize(loss, global_step=global_step)

Media mobile esponenziale

Durante l'addestramento, i parametri addestrabili vengono aggiornati durante la retropropagazione dell'ottimizzazione in base alle regole di aggiornamento dell'ottimizzatore. Le equazioni che descrivono queste regole sono state discusse nella sezione precedente e ripetute qui per comodità:

$${\theta_{k+1}} = {\theta_k}-{\alpha ∇f(\theta_k)} \qquad(SGD)$$
$${\theta_{k+1}}={\theta_k}-{\alpha z_{k+1}} \qquad(momentum)$$
$${\theta_{k+1}}= {\beta \theta_k}+\frac{\eta}{\sqrt {g_{k+1^{\mathbf+{\epsilon}}}}^{-2}} ∇f(\theta_k) \qquad(RMSprop)$$

La media mobile esponenziale (nota anche come livellamento esponenziale) è un passaggio facoltativo di post-elaborazione che viene applicato alle ponderazioni aggiornate e a volte può portare a miglioramenti significativi delle prestazioni. TensorFlow fornisce la funzione tf.train.ExponentialMovingAverage che calcola l'ema \({\hat{\theta}}\) del peso \({\theta}\) utilizzando la formula:

$${\hat{\theta_t}}={\alpha {\hat{\theta}{_{t-1}}}}+{(1-\alpha)}{\theta_t}$$

dove \({\alpha}\) è un fattore di decadimento (vicino a 1,0). Nel modello Inception v3, \({\alpha}\) è impostato su 0,995.

Anche se questo calcolo è un filtro IIR (Infinite Impulse Response), il fattore di decadimento stabilisce una finestra effettiva in cui risiede la maggior parte dell'energia (o dei campioni pertinenti), come mostrato nel seguente diagramma:

immagine

Possiamo riscrivere l'equazione del filtro come segue:

$${\hat{\theta}_{t+T+1}}={\alpha(1-\alpha)}({\theta_{t+T}}+{\alpha \theta_{t+T-1}}+...+{\alpha^{t+T}}{\theta_0})$$

dove abbiamo usato \({\hat\theta_{-1}}=0\).

I valori di \({\alpha}^k\) decadono con l'aumento di k, quindi solo un sottoinsieme dei campioni avrà un'influenza considerevole su \(\hat{\theta}_{t+T+1}\). La regola generale per il valore del fattore di decadimento è: \(\frac {1} {1-\alpha}\), che corrisponde a \({\alpha}\) = 200 per =0,995.

Prima otteniamo una raccolta di variabili addestrabili, quindi usiamo il metodo apply() per creare variabili shadow per ogni variabile addestrata. Il seguente snippet di codice mostra l'implementazione del modello Inception v3:

update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
with tf.control_dependencies(update_ops):
  train_op = optimizer.minimize(loss, global_step=global_step)

if FLAGS.moving_average:
  ema = tf.train.ExponentialMovingAverage(
      decay=MOVING_AVERAGE_DECAY, num_updates=global_step)
  variables_to_average = (tf.trainable_variables() +
                          tf.moving_average_variables())
  with tf.control_dependencies([train_op]), tf.name_scope('moving_average'):
    train_op = ema.apply(variables_to_average)

Vorremmo utilizzare le variabili ema durante la valutazione. Definiamo la classe LoadEMAHook che applica il metodo variables_to_restore() al file dei checkpoint per valutare l'utilizzo dei nomi delle variabili shadow:

class LoadEMAHook(tf.train.SessionRunHook):
  def __init__(self, model_dir):
    super(LoadEMAHook, self).__init__()
    self._model_dir = model_dir

  def begin(self):
    ema = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY)
    variables_to_restore = ema.variables_to_restore()
    self._load_ema = tf.contrib.framework.assign_from_checkpoint_fn(
        tf.train.latest_checkpoint(self._model_dir), variables_to_restore)

  def after_create_session(self, sess, coord):
    tf.logging.info('Reloading EMA...')
    self._load_ema(sess)

La funzione hooks viene passata a evaluate() come mostrato nel seguente snippet di codice:

if FLAGS.moving_average:
    eval_hooks = [LoadEMAHook(FLAGS.model_dir)]
else:
    eval_hooks = []

    …

eval_results = inception_classifier.evaluate(
    input_fn=InputPipeline(False), steps=eval_steps, hooks=eval_hooks)

Normalizzazione batch

La normalizzazione batch è una tecnica ampiamente utilizzata per normalizzare le caratteristiche di input sui modelli che può portare a una riduzione sostanziale dei tempi di convergenza. È uno dei miglioramenti algoritmici più utili e popolari nel machine learning degli ultimi anni e viene utilizzato in un'ampia gamma di modelli, tra cui Inception v3.

Gli input di attivazione vengono normalizzati sottraendo la media e dividendo per la deviazione standard. Per mantenere il bilanciamento in presenza di una propagazione inversa, vengono introdotti due parametri addestrabili in ogni livello. Gli output normalizzati \({\hat{x}}\) vengono sottoposti a un'operazione successiva \({\gamma\hat{x}}+\beta\), dove \({\gamma}\) e \({\beta}\) sono una sorta di deviazione standard e significano apprese dal modello stesso.

Il set completo di equazioni è riportato nell'articolo e viene ripetuto qui per praticità:

Input: valori di x in un mini-batch: \(\Phi=\) { \({x_{1..m}ytc \) } Parametri da apprendere: \({\gamma}\),\({\beta}\)

Output: { \({y_i}=BN_{\gamma,\beta}{(x_i)}\) }

\[{\mu_\phi} \leftarrow {\frac{1}{m}}{\sum_{i=1}^m}x_i \qquad \mathsf(mini-batch\ mean)\]

\[{\sigma_\phi}^2 \leftarrow {\frac{1}{m}}{\sum_{i=1}^m} {(x_i - {\mu_\phi})^2} \qquad \mathbf(mini-batch\ varianza)\]

\[{\hat{x_i}} \leftarrow {\frac{x_i-{\mu_\phi}}{\sqrt {\sigma^2_\phi}+{\epsilon}}}\qquad \mathbf(normalize)\]

\[{y_i}\leftarrow {\gamma \hat{x_i}} + \beta \equiv BN_{\gamma,\beta}{(x_i)}\qquad \mathbf(scale \ e \shift)\]

La normalizzazione avviene durante l'addestramento, ma una volta terminata la valutazione, vorremmo che il modello si comportasse in modo deterministico: il risultato della classificazione di un'immagine dovrebbe dipendere esclusivamente dall'immagine di input e non dall'insieme di immagini che viene inviato al modello. Pertanto, dobbiamo correggere \({\mu}\) e \({\sigma}^2\) e utilizzare i valori che rappresentano le statistiche di compilazione delle immagini.

Il modello calcola le medie mobili della media e della varianza nei minibatch:

\[{\hat\mu_i} = {\alpha \hat\mu_{t-1}}+{(1-\alpha)\mu_t}\]

\[{\hat\sigma_t}^2 = {\alpha{\hat\sigma^2_{t-1}}} + {(1-\alpha) {\sigma_t}^2}\]

Nel caso specifico di Inception v3, è stato ottenuto un fattore di decadimento sensibile (utilizzando l'ottimizzazione degli iperparametri) da utilizzare nelle GPU. Vorremmo utilizzare questo valore anche sulle TPU, ma per farlo dobbiamo apportare delle modifiche.

La media mobile e la varianza della normalizzazione del batch vengono calcolate utilizzando un filtro per il passaggio delle perdite, come mostrato nella seguente equazione (qui, \({y_t}\) rappresenta la media mobile o la varianza):

\[{y_t}={\alpha y_{t-1}}+{(1-\alpha)}{x_t} \]

(1)

In un job GPU 8x1 (sincrono), ogni replica legge la media mobile attuale e la aggiorna. La replica attuale deve scrivere la nuova variabile in movimento prima che la replica successiva possa leggerla.

Se sono presenti 8 repliche, il set di operazioni per un aggiornamento di ensemble è il seguente:

\[{y_t}={\alpha y_{t-1}}+{(1-\alpha)}{x_t} \]

\[{y_{t+1}}={\alpha y_{t}}+{(1-\alpha)}{x_{t+1}} \]

\[{y_{t+2}}={\alpha y_{t+1}}+{(1-\alpha)}{x_{t+2}} \]

\[{y_{t+3}}={\alpha y_{t+2}}+{(1-\alpha)}{x_{t+3}} \]

\[{y_{t+4}}={\alpha y_{t+3}}+{(1-\alpha)}{x_{t+4}} \]

\[{y_{t+5}}={\alpha y_{t+4}}+{(1-\alpha)}{x_{t+5}} \]

\[{y_{t+6}}={\alpha y_{t+5}}+{(1-\alpha)}{x_{t+6}} \]

\[{y_{t+7}}={\alpha y_{t+6}}+{(1-\alpha)}{x_{t+7}} \]

Questo insieme di otto aggiornamenti sequenziali può essere scritto come:

\[{y_{t+7}}={\alpha^8y_{t-1}}+(1-\alpha){\sum_{k=0}^7} {\alpha^{7-k}}{x_{t+k}}\]

(2)

Nell'attuale implementazione del calcolo del momento in movimento sulle TPU, ogni shard esegue i calcoli in modo indipendente e non esiste una comunicazione cross-shard. I batch vengono distribuiti a ogni shard e ognuno di essi elabora 1/8 del numero totale di batch (con 8 shard).

Sebbene ogni shard calcoli i momenti in movimento (media e varianza), solo i risultati dello shard 0 vengono comunicati alla CPU host. Quindi, in pratica, solo una replica sta eseguendo l'aggiornamento della media mobile/varianza:

\[{z_t}={\beta {z_{t-1}}}+{(1-\beta)u_t}\]

(3)

e questo aggiornamento avviene a 1/8 della sua controparte sequenziale. Per confrontare le equazioni di aggiornamento di GPU e TPU, dobbiamo allineare le rispettive scale di tempo. In particolare, il set di operazioni che compongono un insieme di 8 aggiornamenti sequenziali sulla GPU deve essere confrontato con un singolo aggiornamento sulla TPU, come illustrato nel seguente diagramma:

immagine

Mostriamo le equazioni con gli indici temporali modificati:

\[{y_t}={\alpha^8y_{t-1}}+(1-\alpha){\sum_{k=0}^7} {\alpha^{7-k}}{x_{t-k/8}} \qquad \mathsf(GPU)\]

\[{z_t}={\beta {z_{t-1}}}+{(1-\beta)u_t}\qquad \mathsf(TPU) \]

Se supponiamo che 8 mini batch (normalizzati in tutte le dimensioni pertinenti) restituiscano valori simili nell'aggiornamento sequenziale di GPU 8-minibatch, possiamo approssimare queste equazioni come segue:

\[{y_t}={\alpha^8y_{t-1}}+(1-\alpha){\sum_{k=0}^7} {\alpha^{7-k}}{\hat{x_t}}={\alpha^8y_{t-1}+(1-\alpha^8){\hat{x_t}}} \qquad \]sf(GPU

\[{z_t}={\beta {z_{t-1}}}+{(1-\beta)u_t}\qquad \mathsf(TPU) \]

Per adeguarci all'effetto di un determinato fattore di decadimento sulla GPU, modifichiamo di conseguenza il fattore di decadimento sulla TPU. Nello specifico, impostiamo \({\beta}\)=\({\alpha}^8\).

Per Inception v3, il valore di decadimento utilizzato nella GPU è \({\alpha}\)=0,9997, che si traduce in un valore di decadimento TPU pari a \({\beta}\)=0,9976.

Adattamento del tasso di apprendimento

Man mano che le dimensioni dei batch diventano più grandi, l'addestramento diventa più difficile. Continuano a essere proposte tecniche diverse per consentire un addestramento efficiente per batch di grandi dimensioni (vedi qui, qui e qui, ad esempio).

Una di queste tecniche è l'aumento graduale del tasso di apprendimento (detto anche applicazione graduale). È stato utilizzato il periodo di applicazione graduale per addestrare il modello a un'accuratezza superiore al 78,1% per dimensioni batch da 4096 a 16.384. Per Inception v3, il tasso di apprendimento viene inizialmente impostato a circa il 10% di quello che sarebbe normalmente il tasso di apprendimento iniziale. Il tasso di apprendimento rimane costante a questo valore basso per un numero specificato (piccolo) di "epoche fredde", per poi iniziare un aumento lineare per un numero specificato di "epoche di preparazione". Alla fine delle "epoche di riscaldamento", il tasso di apprendimento si interseca con il normale apprendimento di decadimento esponenziale. come illustrato nel seguente schema.

immagine

Lo snippet di codice riportato di seguito mostra come eseguire questa operazione:

initial_learning_rate = FLAGS.learning_rate * FLAGS.train_batch_size / 256
if FLAGS.use_learning_rate_warmup:
  warmup_decay = FLAGS.learning_rate_decay**(
    (FLAGS.warmup_epochs + FLAGS.cold_epochs) /
    FLAGS.learning_rate_decay_epochs)
  adj_initial_learning_rate = initial_learning_rate * warmup_decay

final_learning_rate = 0.0001 * initial_learning_rate

train_op = None
if training_active:
  batches_per_epoch = _NUM_TRAIN_IMAGES / FLAGS.train_batch_size
  global_step = tf.train.get_or_create_global_step()
  current_epoch = tf.cast(
    (tf.cast(global_step, tf.float32) / batches_per_epoch), tf.int32)

  learning_rate = tf.train.exponential_decay(
    learning_rate=initial_learning_rate,
    global_step=global_step,
    decay_steps=int(FLAGS.learning_rate_decay_epochs * batches_per_epoch),
    decay_rate=FLAGS.learning_rate_decay,
    staircase=True)

  if FLAGS.use_learning_rate_warmup:
    wlr = 0.1 * adj_initial_learning_rate
    wlr_height = tf.cast(
      0.9 * adj_initial_learning_rate /
      (FLAGS.warmup_epochs + FLAGS.learning_rate_decay_epochs - 1),
      tf.float32)
    epoch_offset = tf.cast(FLAGS.cold_epochs - 1, tf.int32)
    exp_decay_start = (FLAGS.warmup_epochs + FLAGS.cold_epochs +
                   FLAGS.learning_rate_decay_epochs)
    lin_inc_lr = tf.add(
      wlr, tf.multiply(
        tf.cast(tf.subtract(current_epoch, epoch_offset), tf.float32),
        wlr_height))
    learning_rate = tf.where(
      tf.greater_equal(current_epoch, FLAGS.cold_epochs),
      (tf.where(tf.greater_equal(current_epoch, exp_decay_start),
              learning_rate, lin_inc_lr)),
       wlr)

  # Set a minimum boundary for the learning rate.
  learning_rate = tf.maximum(
      learning_rate, final_learning_rate, name='learning_rate')