Guida avanzata a Inception v3

In questo documento vengono illustrati gli aspetti del modello Inception e come questi elementi vengono uniti per rendere il modello efficiente su Cloud TPU. È una visualizzazione avanzata della guida all'esecuzione di Inception v3 su Cloud TPU. Le modifiche specifiche al modello che hanno portato a miglioramenti significativi sono discusse in modo più dettagliato. Questo documento integra il tutorial di Inception v3.

Le esecuzioni dell'addestramento TPU di Inception v3 corrispondono alle curve di precisione prodotte dai job GPU di configurazione simile. Il modello è stato addestrato correttamente sulle configurazioni v2-8, v2-128 e v2-512. Il modello ha raggiunto una precisione superiore al 78,1% in circa 170 periodi.

Gli esempi di codice mostrati in questo documento hanno lo scopo di illustrare un'immagine generale di ciò che accade nell'implementazione effettiva. Il codice di lavoro è disponibile su GitHub.

Introduzione

Inception v3 è un modello di riconoscimento di immagini che ha dimostrato di avere una precisione superiore al 78,1% nel set di dati ImageNet. Il modello è il culmine di molte idee sviluppate da più ricercatori nel corso degli anni. Si basa sull'articolo originale: "Rethinking the Inception Architecture for Computer Vision" di Szegedy, et al.

Il modello è costituito da componenti di base simmetrici e asimmetrici, tra cui convoluzioni, pool medio, pool massimo, concatenazioni, interruzioni e livelli completamente collegati. La normalizzazione batch è utilizzata ampiamente in tutto il modello e applicata agli input di attivazione. La perdita viene calcolata con Softmax.

Nel seguente screenshot è riportato un diagramma generale del modello:

immagine

Il modello Inception README contiene ulteriori informazioni sull'architettura Inception.

API Estimator

La versione TPU di Inception v3 viene scritta utilizzando TPUEstimator, un'API progettata per facilitare lo sviluppo, in modo da poterti concentrare sui modelli stessi anziché sui dettagli dell'hardware sottostante. L'API esegue la maggior parte del lavoro grunge di basso livello necessario per eseguire i modelli su TPU dietro le quinte, automatizzando le funzioni comuni, come il salvataggio e il ripristino dei punti di controllo.

L'API Estimator applica la separazione delle parti del modello e di input del codice. Devi definire le funzioni model_fn e input_fn, corrispondenti 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

Le due funzioni principali 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 di poter riconoscere le immagini, il modello deve essere addestrato utilizzando un vasto insieme di immagini con etichette.ImageNet è un set di dati comune da utilizzare.

ImageNet ha oltre dieci milioni di URL di immagini con etichette. Un milione di immagini ha anche 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 intenzionalmente separati. Per addestrare il modello vengono utilizzate solo le immagini del set di dati di addestramento e solo per valutare l'accuratezza del modello.

Il modello prevede che le immagini vengano archiviate come TFRecord. Per convertire le immagini da file JPEG non elaborati in record TF, utilizza lo script batch open source: download_and_preprocess_imagenet.sh. Lo script deve generare una serie di file (sia per addestramento sia per convalida) nel formato:

${DATA_DIR}/train-00000-of-01024
${DATA_DIR}/train-00001-of-01024
 ...
${DATA_DIR}/train-01023-of-01024
and
${DATA_DIR}/validation-00000-of-00128
S{DATA_DIR}/validation-00001-of-00128
 ...
${DATA_DIR}/validation-00127-of-00128

dove DATA_DIR è la località in cui si trova l'insieme, ad esempio: DATA_DIR=$HOME/imagenet-data

La sezione Per iniziare nel modello Inception README include istruzioni dettagliate su come creare ed eseguire questo script.

Pipeline di input

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

Gli host recuperano i dati dal file system o dalla memoria locale, eseguono qualsiasi operazione di pre-elaborazione richiesta e quindi trasferiscono i dati pre-elaborati ai core TPU. Considera queste tre fasi di gestione dei dati da parte dell'host singolarmente e facciamo riferimento alle fasi come: 1) Archiviazione, 2) Pre-elaborazione, 3) Trasferimento. Un'immagine di alto livello del diagramma è mostrata nella figura seguente:

immagine

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

immagine

L'attuale implementazione di Inception v3 è all'avanguardia in termini di input. Le immagini vengono recuperate dal file system, decodificate e pre-elaborate. Sono disponibili diversi tipi di fasi di pre-elaborazione, che vanno da moderata a complessa. Se utilizziamo la fase più complessa della pre-elaborazione, la pipeline di addestramento sarà vincolata al pre-elaborazione. Puoi ottenere una precisione superiore al 78,1% utilizzando una fase di pre-elaborazione moderatamente complessa che mantiene il modello TPU associato al modello.

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

Anche se puoi definire una funzione e passarla all'API Estimator, la classe InputPipeline include tutte le funzionalità richieste.

L'API Estimator semplifica l'utilizzo di questo corso. È sufficiente trasmetterlo 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 nel seguente snippet di codice.

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 di TFRecord dallo spazio di archiviazione (utilizzando tf.data.TFRecordDataset). Le funzioni speciali repeat() e shuffle() vengono utilizzate a seconda delle esigenze. La funzione tf.contrib.data.parallel_interleave() mappa la funzione prefetch_dataset() nel suo input per produrre set di dati nidificati e restituisce gli elementi interlacciati. riceve elementi da cycle_length set di dati nidificati in parallelo, aumentando la velocità effettiva. L'argomento sloppy riduce il requisito che richiede la produzione degli output in un ordine deterministico e consente all'implementazione di ignorare i set di dati nidificati i cui elementi non sono immediatamente disponibili quando richiesto.

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

La sezione transfer (alla fine della funzione) include la riga return images, labels. TPUEstimator acquisisce 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 TPU, ignorando eventuali chioschi nel feed, è di circa 815 msec.

immagine

Lo spazio di archiviazione dell'host è scritto nella traccia e viene mostrato nel seguente screenshot:

immagine

Il seguente schermo di hosting, che include la decodifica delle immagini e una serie di funzioni di distorsione delle immagini, è mostrato nel seguente screenshot:

immagine

Il seguente trasferimento host/TPU è riportato 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 l'introduzione, le immagini devono avere una dimensione di 299x299x3 pixel.

Tuttavia, la decodifica e il ridimensionamento non sono sufficienti per ottenere una buona precisione. Il set di dati di addestramento ImageNet contiene 1.281.167 immagini. Un pass per l'insieme di immagini di addestramento è definito periodo. Durante l'addestramento, il modello richiede diversi passaggi attraverso il 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 periodi compreso tra 140 e 200 in base alle dimensioni globali del batch.

È utile modificare continuamente le immagini prima di includerle nel modello, in modo che un'immagine particolare sia leggermente diversa a ogni periodo. Il modo migliore per eseguire questa pre-elaborazione delle immagini è tanto l'arte quanto la scienza. Una fase di pre-elaborazione ben progettata può aumentare in modo significativo le capacità di riconoscimento di un modello. Una fase di pre-elaborazione troppo semplice potrebbe creare un soffitto artificiale nell'accuratezza che lo stesso modello può raggiungere durante l'addestramento.

Inception v3 offre opzioni per la fase di pre-elaborazione, che vanno da relativamente relativo e economici a abbastanza complessi e costosi. È possibile trovare due versioni distinte nei file vgg_preprocessing.py e inception_preprocessing.py.

Il file vgg_preprocessing.py definisce una fase di pre-elaborazione che è stata utilizzata per addestrare resnet con un'accuratezza del 75%, ma genera risultati non ottimali quando applicata in Inception v3.

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

L'elaborazione preliminare varia a seconda che il modello sia in fase di addestramento o di utilizzo per inferenza/valutazione.

Al momento della valutazione, la pre-elaborazione è semplice: ritaglia un'area geografica centrale dell'immagine, quindi ridimensionala secondo le dimensioni predefinite di 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 randomizzato: viene scelto un riquadro di selezione in modo casuale per selezionare un'area dell'immagine che verrà ridimensionata. In seguito l'immagine ridimensionata viene capovolta e i 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 del colore. Offre una modalità veloce in cui vengono modificate solo la luminosità e la saturazione. La modalità completa modifica la luminosità, la saturazione e la 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 è costosamente computazionale, in parte a causa delle conversioni non lineari da RGB a HSV e HSV necessarie per accedere alla tonalità e alla saturazione. Sia la modalità veloce che quella completa richiedono queste conversioni e, anche se la modalità veloce è meno costosa dal punto di vista tecnico, esegue comunque il push del modello all'area geografica associata al calcolo della CPU, se attivata.

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 le cromature di Cr/Cb prima di mappare nuovamente il 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 è riportata un'immagine di esempio che è stata sottoposta a pre-elaborazione. È stata selezionata un'area geografica dell'immagine scelta in modo casuale e i colori sono stati modificati mediante la funzione distort_color_fast.

immagine

La funzione distort_color_fast è efficiente dal punto di vista calcolatovo e consente comunque di addestrare il tempo di esecuzione TPU. Inoltre, è stato utilizzato per addestrare il modello Inception v3 a una precisione maggiore del 78,1% utilizzando dimensioni batch nell'intervallo 1.024-16.384.

Ottimizzatore

Il modello attuale mostra tre gusti di strumenti di ottimizzazione: SGD, momenti e RMSProp.

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

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

Momentum è un ottimizzatore popolare che spesso conduce a una convergenza più rapida rispetto a SGD. Questo strumento di ottimizzazione aggiorna lo stesso peso di SGD, ma aggiunge anche un componente nella direzione dell'aggiornamento precedente. Le seguenti equazioni descrivono gli aggiornamenti eseguiti dallo strumento di ottimizzazione dei momenti:

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

che possono essere scritte come:

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

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

immagine

Per lo slancio \({\beta}\), utilizziamo il valore di 0,9.

RMSprop è un ottimizzatore popolare proposto inizialmente da Geoff Hinton in una delle sue lezioni. Le seguenti equazioni descrivono il funzionamento dello strumento di ottimizzazione:

$$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^{\mathbf}}

Per Inception v3, i test mostrano RMSProp ottenere i migliori risultati in termini di massima accuratezza e tempo per raggiungerlo, con un impulso da poco. Pertanto, RMSprop è impostato come ottimizzatore predefinito. I parametri utilizzati sono: decadimento \({\alpha}\) = 0,9, momento \({\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)

Quando si esegue le TPU e si utilizza l'API Estimator, lo strumento di ottimizzazione deve essere aggregato a una funzione CrossShardOptimizer per garantire la sincronizzazione tra le repliche (insieme a qualsiasi comunicazione incrociata necessaria). Il seguente snippet di codice mostra in che modo il modello Inception v3 aggrega il componente.

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, in base alle regole di aggiornamento dello strumento di ottimizzazione. Per maggiore praticità, le equazioni che descrivono queste regole sono state discusse nella sezione precedente e ripetute qui:

$${\theta_{k+1}} = {\theta_k}-{\alpha ∇f(\theta_k)} \qquad(SGD)$$
$${\theta_{k+1}}={\ttata_k}-{\alpha z_{k+1}} \qquad(momento)$$
$${\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 post-elaborazione facoltativo che viene applicato ai pesi aggiornati e che 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 Infinite Impulse Response (IIR), il fattore di decadimento stabilisce una finestra efficace in cui si trova la maggior parte dell'energia (o 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 utilizzato \({\hat\theta_{-1}}=0\).

I valori \({\alpha}^k\) decadiscono con l'aumento di k, pertanto solo un sottoinsieme di campioni avrà una grande influenza 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.

Innanzitutto, raccogliamo una raccolta di variabili addestrabili e poi utilizziamo il metodo apply() per creare variabili ombra 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)

Ci piacerebbe utilizzare le variabili ema durante la valutazione. Definiamo la classe LoadEMAHook che applica il metodo variables_to_restore() al file di checkpoint per valutare i nomi utilizzando le 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 trasmessa 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 possono portare a una significativa riduzione del tempo di convergenza. Si tratta di uno dei miglioramenti algoritmici più utili e utili nel corso del machine learning degli ultimi anni e utilizzato in una vasta gamma di modelli, tra cui Inception v3.

Gli input di attivazione sono normalizzati sottraendo la media e dividendo per la deviazione standard. Per mantenere un equilibrio tra gli elementi in presenza di propagazione della schiena, in ogni livello vengono introdotti due parametri addestrabili. Gli output normalizzati \({\hat{x}}\) sono 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 nel documento e viene ripetuto qui per praticità:

Ingresso: valori di x superiori a un mini-batch: \(\Phi=\) { \({x_{1..m}usercontent \) } 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\ media)\]

\[{\ssma_\phi}^2 \Freccia sinistra {\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(normalizza)\]

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

La normalizzazione avviene durante l'addestramento, ma arriva al momento della valutazione, come il modello si comporterebbe in modo deterministico: il risultato della classificazione di un'immagine dovrebbe dipendere esclusivamente dall'immagine di input e non dal set di immagini che viene inviato al modello. Di conseguenza, dobbiamo correggere \({\mu}\) e \({\sigma}^2\) e utilizzare i valori che rappresentano le statistiche sulla popolazione delle immagini.

Il modello calcola la media mobile della media e della varianza sui mini batch:

\[{\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 ragionevole (tramite l'ottimizzazione degli iperparametri) per l'utilizzo nelle GPU. Vorremmo utilizzare questo valore anche per le TPU, ma per farlo dobbiamo apportare alcune modifiche.

La media e la varianza di spostamento della normalizzazione batch vengono calcolate utilizzando un filtro di pass per la perdita, come mostrato nella seguente equazione (qui \({y_t}\) rappresenta la media o la varianza in movimento:

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

(1)

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

Quando ci sono 8 repliche, l'insieme di operazioni per un aggiornamento di insieme è 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 8 aggiornamenti sequenziali può essere scritto come segue:

\[{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 movimentato sulle TPU, ogni shard esegue calcoli in modo indipendente e non è necessaria una comunicazione trasversale. I batch vengono distribuiti a ogni shard e ciascuno di essi elabora 1/8 del numero totale di batch (quando gli 8 shard sono presenti).

Ciascun shard calcola i momenti di spostamento (ovvero media e varianza), ma solo i risultati dello shard 0 vengono comunicati alla CPU host. Quindi, in effetti, una sola replica esegue l'aggiornamento media/variazione:

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

(3)

e questo aggiornamento avviene a 1/8 della velocità della sua controparte sequenziale. Per confrontare le equazioni di aggiornamento della GPU e della TPU, dobbiamo allineare le tempistiche. In particolare, l'insieme di operazioni che costituiscono una serie di 8 aggiornamenti sequenziali sulla GPU deve essere confrontato con un singolo aggiornamento sulla TPU, come illustrato nel diagramma seguente:

immagine

Mostra le equazioni con gli indici di tempo 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) \]

Ipotizziamo che 8 mini batch (normalizzati in tutte le dimensioni pertinenti) restituiscano valori simili all'interno dell'aggiornamento sequenziale da 8 batch della GPU, quindi 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 \mathsfG)

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

Per far corrispondere l'effetto di un determinato fattore di decadimento sulla GPU, modifichiamo di conseguenza il fattore di decadimento sulla TPU. In particolare, 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 di \({\beta}\)=0.9976.

Adattamento del tasso di apprendimento

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

Una di queste tecniche è aumentare gradualmente il tasso di apprendimento (chiamato anche potenziamento). È stato utilizzato il ramp-up per addestrare il modello a una precisione superiore al 78,1% per dimensioni batch che vanno da 4096 a 16334. Per Inception v3, il tasso di apprendimento è inizialmente impostato su circa il 10% di quello che normalmente sarebbe il tasso di apprendimento iniziale. La velocità di apprendimento rimane costante a questo basso valore per un numero specificato (piccolo) di 'epoca fredda' e poi inizia un aumento lineare per un numero specificato di 'periodi di riscaldamento'. Alla fine, il periodo di apprendimento può incrociarsi con il normale apprendimento del decadimento esponenziale. Questo è illustrato nel diagramma di seguito.

immagine

Il seguente snippet di codice mostra come fare:

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