Erweiterter Leitfaden zu Inception v3

In diesem Dokument werden Aspekte des Inception-Modells und deren Zusammenführung erläutert, damit das Modell auf Cloud TPU effizient ausgeführt werden kann. Es ist eine erweiterte Ansicht des Leitfadens zum Ausführen von Inception v3 auf Cloud TPU. Spezifische Änderungen am Modell, die zu erheblichen Verbesserungen geführt haben, werden ausführlicher erörtert. Dieses Dokument ergänzt die Anleitung zu Inception v3.

Inception v3-TPU-Trainingsläufe kommen Genauigkeitskurven gleich, die durch GPU-Jobs mit ähnlicher Konfiguration erzeugt wurden. Das Modell wurde erfolgreich mit v2-8-, v2-128- und v2-512-Konfigurationen trainiert. Das Modell hat in etwa 170 Epochen eine Genauigkeit von mehr als 78,1% erreicht.

Die in diesem Dokument gezeigten Codebeispiele haben illustrativen Charakter und sollen ein allgemeines Bild von den Abläufen bei der konkreten Umsetzung vermitteln. Funktionstüchtigen Code finden Sie auf GitHub.

Einführung

Inception v3 ist ein Bilderkennungsmodell, das nachweislich eine Genauigkeit von mehr als 78,1% im ImageNet-Dataset erreicht. Das Modell ist das Ergebnis zahlreicher Ideen, die im Laufe der Jahre von verschiedenen Wissenschaftlern entwickelt wurden. Es basiert auf dem ursprünglichen Artikel Rethinking the Inception Architecture for Computer Vision von Szegedy et al.

Das Modell selbst besteht aus symmetrischen und asymmetrischen Bausteinen, darunter Faltungen, durchschnittliches Pooling, Max-Pooling, Verkettungen, Dropouts und vollständig verbundene Ebenen. Die Batchnormalisierung wird im gesamten Modell umfassend eingesetzt und auf Aktivierungseingaben angewendet. Der Verlust wird mithilfe von Softmax berechnet.

Der folgende Screenshot zeigt ein allgemeines Diagramm des Modells:

Image

Estimator API

Die TPU-Version von Inception v3 wurde mit TPUEstimator geschrieben. Diese API soll Ihnen die Entwicklung erleichtern, sodass Sie sich auf die Modelle selbst konzentrieren können, anstatt auf die Details der zugrunde liegenden Hardware. Die API erledigt die meisten mühseligen Arbeitsschritte zur Ausführung von Modellen auf TPUs im Hintergrund und automatisiert gleichzeitig gängige Funktionen wie das Speichern und Wiederherstellen von Prüfpunkten.

Die Estimator API erzwingt die Trennung der Modell- und Eingabeabschnitte des Codes. Sie definieren die Funktionen model_fn und input_fn entsprechend der Modelldefinition und der Eingabepipeline. Der folgende Code zeigt die Deklaration dieser Funktionen:

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

Zwei Hauptfunktionen der API sind train() und evaluate() zum Trainieren und Auswerten, wie im folgenden Code dargestellt:

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)

ImageNet-Dataset

Bevor das Modell zum Erkennen von Bildern verwendet werden kann, muss es mit einer großen Anzahl von Bildern mit Labels trainiert werden. ImageNet ist ein häufig zu verwendendes Dataset.

ImageNet enthält über zehn Millionen URLs von beschrifteten Bildern. Eine Million der Bilder haben außerdem Begrenzungsrahmen, um eine genauere Position für die beschrifteten Objekte anzugeben.

Für dieses Modell umfasst das ImageNet-Dataset 1.331.167 Bilder, die in Trainings- und Evaluations-Datasets mit 1.281.167 bzw. 50.000 Bildern unterteilt sind.

Die Trainings- und Evaluations-Datasets werden bewusst getrennt gehalten. Zum Trainieren des Modells werden nur Bilder aus dem Trainings-Dataset verwendet und zum Evaluieren der Modellgenauigkeit nur Bilder aus dem Evaluations-Dataset.

Das Modell erwartet, dass Bilder als TFRecords gespeichert sind. Weitere Informationen zum Konvertieren von Bildern aus RAW-JPEG-Dateien in TFRecords finden Sie unter download_and_preprocess_imagenet.sh.

Eingabepipeline

Jedes Cloud TPU-Gerät hat acht Kerne und ist mit einem Host (CPU) verbunden. Größere Segmente haben mehrere Hosts. Andere größere Konfigurationen interagieren mit mehreren Hosts. Beispiel: Ein v2-256 kommuniziert mit 16 Hosts.

Hosts rufen Daten aus dem Dateisystem oder lokalen Speicher ab, erledigen die erforderliche Datenvorverarbeitung und übertragen dann vorverarbeitete Daten an die TPU-Kerne. Diese drei Phasen der Datenverarbeitung durch den Host werden getrennt betrachtet und als Speicher, Vorverarbeitung und Übertragung bezeichnet. Die folgende Abbildung zeigt eine allgemeine Übersicht des Diagramms:

Image

Damit das System eine gute Leistung erzielt, sollte es ausgewogen sein. Wenn die Host-CPU für die drei Datenverarbeitungsphasen länger als die TPU benötigt, ist die Ausführung hostgebunden. Beide Fälle sind im folgenden Diagramm dargestellt:

Image

Die aktuelle Implementierung von Inception v3 steht kurz vor der Eingabe. Die Bilder werden aus dem Dateisystem abgerufen, decodiert und dann vorverarbeitet. Es stehen verschiedene Arten von Vorverarbeitungsphasen zur Verfügung, von moderat bis komplex. Wenn wir die komplexesten Vorverarbeitungsphasen verwenden, ist die Trainingspipeline vorverarbeitungsgebunden. Mit einer mäßig komplexen Vorverarbeitungsphase, die das Modell TPU-gebunden hält, können Sie eine Genauigkeit von über 78,1% erreichen.

Das Modell verwendet tf.data.Dataset für die Verarbeitung der Eingabepipeline. Weitere Informationen zum Optimieren von Eingabepipelines finden Sie im Leitfaden zur Leistung von Datasets.

Sie können zwar eine Funktion definieren und an die Schätzer API übergeben, aber die Klasse InputPipeline kapselt alle erforderlichen Features.

Die Schätzer-API vereinfacht die Verwendung dieser Klasse. Sie übergeben ihn an den Parameter input_fn der Funktionen train() und evaluate(), wie im folgenden Code-Snippet gezeigt:

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)

Die Hauptelemente von InputPipeline werden im folgenden Code-Snippet dargestellt.

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

Der Abschnitt Storage (Speicher) beginnt mit der Erstellung eines Datasets und enthält das Lesen von TFRecords aus dem Speicher (mit tf.data.TFRecordDataset). Die Sonderfunktionen repeat() und shuffle() werden nach Bedarf verwendet. Die Funktion tf.contrib.data.parallel_interleave() ordnet ihrer Eingabe eine Funktion prefetch_dataset() zu, um verschachtelte Datasets zu erzeugen, und gibt ihre Elemente verschränkt aus. Sie ruft Elemente aus cycle_length verschachtelten Datasets parallel ab, wodurch der Durchsatz erhöht wird. Das Argument sloppy lockert die Vorgabe, dass die Ausgaben in einer deterministischen Reihenfolge zu erzeugen sind. Damit kann die Implementierung verschachtelte Datasets überspringen, deren Elemente beim Abruf nicht verfügbar sind.

Im Abschnitt Preprocessing (Vorverarbeitung) wird dataset.map(parser) aufgerufen, wodurch wiederum die Parser-Funktion aufgerufen wird, in der Bilder vorverarbeitet werden. Die Vorverarbeitungsphase wird im nächsten Abschnitt ausführlich erläutert.

Der Abschnitt Transfer (Übertragung) am Ende der Funktion enthält die Zeile return images, labels. TPUEstimator greift die zurückgegebenen Werte auf und überträgt sie automatisch an das Gerät.

Die folgende Abbildung zeigt ein Beispiel für einen Cloud TPU-Leistungs-Trace von Inception v3. Die TPU-Rechenzeit beträgt ohne Berücksichtigung von Einspeisungsständen ca. 815 Millisekunden.

Image

Der Host Speicher wird in den Trace geschrieben und im folgenden Screenshot dargestellt:

Image

Der folgende Screenshot zeigt die Host-Vorverarbeitung, die die Bilddecodierung und eine Reihe von Bildverzerrungsfunktionen einschließt:

Image

Im folgenden Screenshot ist die Übertragung von Host und TPU zu sehen:

Image

Vorverarbeitungsphase

Die Bildvorverarbeitung ist ein entscheidender Teil des Systems und kann die maximale Genauigkeit beeinflussen, die das Modell während des Trainings erreicht. Bilder müssen zumindest decodiert und skaliert werden, um sie an das Modell anzupassen. Für Inception müssen Bilder 299 x 299 x 3 Pixel groß sein.

Allerdings reichen Decodierung und Größenanpassung nicht aus, um eine gute Genauigkeit zu erzielen. Das ImageNet-Trainings-Dataset enthält 1.281.167 Bilder. Wenn dieser Satz von Trainingsbildern einmal durchlaufen wird, spricht man von einer Epoche. Während des Trainings muss das Modell das Trainings-Dataset mehrmals durchlaufen, um die Bilderkennung zu verbessern. Verwenden Sie je nach globaler Batchgröße zwischen 140 und 200 Epochen, um Inception v3 mit einer ausreichenden Genauigkeit zu trainieren.

Es ist nützlich, die Bilder kontinuierlich zu ändern, bevor sie in das Modell eingespeist werden, sodass sich ein bestimmtes Bild in jeder Epoche leicht unterscheidet. Die beste Vorgehensweise für diese Vorverarbeitung von Bildern ist sowohl Kunst als auch Wissenschaft. Eine gut konzipierte Vorverarbeitungsphase kann die Erkennungsfunktionen eines Modells erheblich verbessern. Eine zu einfache Vorverarbeitungsphase kann die Genauigkeit beeinträchtigen, die dasselbe Modell während des Trainings erreichen kann.

Inception v3 bietet Optionen für die Vorverarbeitung, die von relativ einfach und rechenintensiv bis hin zu komplex und rechenintensiv sind. Zwei dieser verschiedenen Varianten finden Sie in den Dateien vgg_preprocessing.py und inception_preprocessing.py.

Die Datei vgg_preprocessing.py definiert eine Vorverarbeitungsphase, die erfolgreich zum Trainieren von resnet mit einer Genauigkeit von 75 % verwendet wurde, aber bei Anwendung auf Inception v3 unzulängliche Ergebnisse liefert.

Die Datei inception_preprocessing.py enthält eine Vorverarbeitungsphase, die zum Trainieren von Inception v3 mit einer Genauigkeit zwischen 78,1 und 78,5% bei Ausführung auf TPUs verwendet wurde.

Die Vorverarbeitung variiert in Abhängigkeit davon, ob das Modell trainiert oder für die Inferenz/Evaluation verwendet wird.

Zum Zeitpunkt der Bewertung ist die Vorverarbeitung unkompliziert: Sie schneiden einen zentralen Bereich des Bildes zu und ändern die Größe dann auf die Standardgröße von 299 × 299. Das folgende Code-Snippet zeigt eine Vorverarbeitungsimplementierung:

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

In der Trainingsphase ist das Zuschneiden randomisiert: Ein Bereich des Bildes wird mit einem Markierungsrahmen zufällig ausgewählt und dann in der Größe geändert. Es folgt eine optionale Spiegelung des skalierten Bildes und eine Verzerrung seiner Farben. Das folgende Code-Snippet zeigt eine Implementierung dieser Vorgänge:

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

Die Funktion distort_color ist für die Farbänderung zuständig. Sie umfasst einen Schnellmodus, in dem nur Helligkeit und Sättigung geändert werden. Im Vollmodus werden Helligkeit, Sättigung und Farbton in einer zufälligen Reihenfolge geändert.

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)

Die Funktion distort_color ist rechenintensiv, was teilweise auf die nichtlinearen Konvertierungen von RGB zu HSV und HSV zu RGB zurückzuführen ist, die für den Zugriff auf Farbton und Sättigung erforderlich sind. Diese Konvertierungen werden sowohl für den Schnell- als auch den Vollmodus benötigt. Obwohl der Schnellmodus weniger rechenintensiv ist, verschiebt er das Modell dennoch in den CPU-rechengebundenen Bereich, wenn er aktiviert wird.

Als Alternative wurde eine neue Funktion distort_color_fast zur Liste der Optionen hinzugefügt. Diese Funktion ordnet das Bild mithilfe des JPEG-Konvertierungsschemas von RGB zu YCrCb zu und ändert nach dem Zufallsprinzip die Helligkeit und die Cr/Cb-Farbtöne, bevor es wieder RGB zugeordnet wird. Das folgende Code-Snippet zeigt eine Implementierung dieser Funktion:

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

Hier sehen Sie ein Beispielbild, das einer Vorverarbeitung unterzogen wurde. Mit der Funktion distort_color_fast wurden ein zufällig ausgewählter Bildbereich ausgewählt und die Farben geändert.

Image

Die Funktion distort_color_fast ist recheneffizient und ermöglicht gleichzeitig, dass das Training an die TPU-Ausführungszeit gebunden ist. Darüber hinaus wurde es verwendet, um das Inception v3-Modell mit einer Genauigkeit von mehr als 78,1% mit Batchgrößen im Bereich 1.024–16.384 zu trainieren.

Optimierung

Im aktuellen Modell kommen drei verschiedene Optimierungen zum Einsatz: SGD, Momentum und RMSProp.

Stochastic gradient descent (SGD) ist die einfachste Aktualisierung: Die Gewichtungen werden in die Richtung des negativen Gradienten verschoben. Trotz der Einfachheit dieser Optimierung können bei einigen Modellen noch gute Ergebnisse erzielt werden. Die Aktualisierungsdynamik lässt sich formulieren als:

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

Momentum ist eine beliebte Optimierung, die häufig zu einer schnelleren Konvergenz als SGD führt. Dieses Optimierungsprogramm aktualisiert die Gewichtung ähnlich wie SGD, fügt aber auch eine Komponente in Richtung der vorherigen Aktualisierung hinzu. Die folgenden Gleichungen beschreiben die Aktualisierungen, die von der Momentumoptimierung durchgeführt werden:

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

Dies lässt sich formulieren als:

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

Der letzte Term ist die Komponente in Richtung der vorherigen Aktualisierung.

Image

Für den Impuls \({\beta}\) verwenden wir den Wert 0,9.

RMSprop ist eine weitverbreitete Optimierung, die zuerst von Geoff Hinton in einer seiner Vorlesungen vorgestellt wurde. Die folgenden Gleichungen beschreiben die Funktionsweise der Optimierung:

$$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{+{\epsilon}}}}^{-2}}} ∇f(w_k)$$

Für Inception v3 zeigen Tests, dass RMSProp die besten Ergebnisse in Bezug auf die maximale Genauigkeit und die Zeit bis zum Erreichen dieser Genauigkeit liefert, dicht gefolgt von Momentum. Aus diesem Grund wird RMSprop als Standardoptimierung festgelegt. Die verwendeten Parameter sind: decay \({\alpha}\) = 0,9, momentum \({\beta}\) = 0,9 und \({\epsilon}\) = 1,0.

Das folgende Code-Snippet zeigt, wie diese Parameter festgelegt werden:

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)

Beim Ausführen auf TPUs und mithilfe der Estimator API muss das Optimierungstool in eine CrossShardOptimizer-Funktion eingebunden werden, um für die Synchronisierung zwischen den Replikaten zu sorgen (zusammen mit der erforderlichen Kommunikation). Das folgende Code-Snippet zeigt, wie das Inception v3-Modell das Optimierungstool umschließt:

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)

Exponentieller gleitender Durchschnitt

Während des Trainings werden die trainierbaren Parameter während der Rückpropagierung gemäß den Aktualisierungsregeln des Optimierungstools aktualisiert. Die Gleichungen zur Beschreibung dieser Regeln wurden im vorherigen Abschnitt besprochen und der Einfachheit halber hier wiederholt:

$${\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)$$

Der exponentielle gleitende Durchschnitt (auch exponentielle Glättung) ist ein optionaler Nachbearbeitungsschritt, der auf die aktualisierten Gewichtungen angewendet wird und manchmal zu deutlichen Leistungsverbesserungen führen kann. TensorFlow stellt die Funktion tf.train.ExponentialMovingAverage bereit, die den exponentiellen gleitenden Durchschnitt \({\hat{\theta}}\) der Gewichtung \({\theta}\) mithilfe der folgenden Formel berechnet:

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

Dabei ist \({\alpha}\) ein Abklingfaktor (nahe 1,0). Im Inception v3-Modell ist \({\alpha}\) auf 0,995 festgelegt.

Obwohl es sich bei dieser Berechnung um einen IIR-Filter (Infinite Impulse Response) handelt, legt der Abklingfaktor ein effektives Fenster fest, in dem sich die meiste Energie (oder relevante Proben) befindet, wie im folgenden Diagramm dargestellt:

Image

Wir können die Filtergleichung wie folgt umschreiben:

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

Dabei wurde \({\hat\theta_{-1}}=0\) verwendet.

Die \({\alpha}^k\)-Werte nehmen mit zunehmendem k ab, sodass nur eine Teilmenge der Stichproben einen großen Einfluss auf \(\hat{\theta}_{t+T+1}\) hat. Die Faustregel für den Wert des Abklingfaktors lautet: \(\frac {1} {1-\alpha}\), was \({\alpha}\) = 200 für =0,995 entspricht.

Wir erhalten zuerst eine Sammlung trainierbarer Variablen und verwenden dann die Methode apply(), um Schattenvariablen für jede trainierte Variable zu erstellen. Das folgende Code-Snippet zeigt die Implementierung des Inception v3-Modells:

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)

Jetzt sollen die Variablen des exponentiellen gleitenden Durchschnitts bei der Evaluation verwendet werden. Wir definieren die Klasse LoadEMAHook, die die Methode variables_to_restore() auf die Checkpointdatei anwendet, die mithilfe der Namen von Schattenvariablen ausgewertet werden soll:

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)

Die Funktion hooks wird an evaluate() übergeben, wie im folgenden Code-Snippet gezeigt:

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)

Batchnormalisierung

Die Batchnormalisierung ist eine weitverbreitete Technik zur Normalisierung von Eingabemerkmalen in Modellen, die die Konvergenzzeit erheblich verringern kann. Sie zählt zu den gängigsten und wertvollsten algorithmischen Verbesserungen des maschinellen Lernens der letzten Jahre und wird in einer Vielzahl von Modellen, einschließlich Inception v3, verwendet.

Aktivierungseingaben werden normalisiert, indem der Mittelwert subtrahiert und durch die Standardabweichung geteilt wird. Damit die Balance bei der Rückpropagierung gewahrt bleibt, werden zwei trainierbare Parameter in jeder Schicht hinzugefügt. Die normalisierten Ausgaben \({\hat{x}}\) werden anschließend einem Vorgang \({\gamma\hat{x}}+\beta\) ausgeführt, wobei \({\gamma}\) und \({\beta}\) eine Art Standardabweichung sind und den vom Modell selbst erlernten Mittelwert sind.

Der vollständige Satz von Gleichungen ist in diesem Artikel zu finden und wird hier noch einmal aufgeführt:

Eingabe: Werte von x über einem Minibatch: \(\Phi=\) { \({x_{1..m}\\} \) } Zu lernende Parameter: \({\gamma}\),\({\beta}\)

Ausgabe: { \({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\ variance)\]

\[{\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 \ and \ shift)\]

Die Normalisierung erfolgt während des Trainings, aber zur Bewertungszeit soll sich das Modell deterministisch verhalten: Das Klassifizierungsergebnis eines Bildes sollte ausschließlich vom Eingabebild und nicht vom Satz von Bildern abhängen, die in das Modell eingespeist werden. Daher müssen wir \({\mu}\) und \({\sigma}^2\) korrigieren und Werte verwenden, die die Bevölkerungsstatistik des Bildes darstellen.

Das Modell berechnet den gleitenden Durchschnitt des Mittelwerts und der Varianz über die Minibatches:

\[{\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}\]

Im speziellen Fall von Inception v3 wurde (mithilfe der Hyperparameter-Abstimmung) ein vernünftiger Abklingfaktor für die Verwendung in GPUs ermittelt. Damit dieser Wert auch auf TPUs verwendet werden kann, müssen jedoch einige Anpassungen vorgenommen werden.

Sowohl der gleitende Mittelwert als auch die Varianz der Batchnormalisierung werden mit einem Verlustpassfilter berechnet, wie in der folgenden Gleichung dargestellt (hier steht \({y_t}\) für den gleitenden Mittelwert oder die gleitende Varianz):

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

(1)

In einem (synchronen) 8x1-GPU-Job liest jedes Replikat den aktuellen gleitenden Mittelwert und aktualisiert ihn. Das aktuelle Replikat muss die neue gleitende Variable schreiben, bevor das nächste Replikat sie lesen kann.

Bei acht Replikaten sieht der Satz von Vorgängen für eine komplette Aktualisierung so aus:

\[{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}} \]

Dieser Satz von acht sequenziellen Aktualisierungen lässt sich formulieren als:

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

(2)

In der aktuellen Implementierung zur Berechnung des Bewegungsmoments auf TPUs führt jeder Shard Berechnungen unabhängig durch und es gibt keine fragmentübergreifende Kommunikation. Batches werden auf die einzelnen Shards verteilt und jeder Shard verarbeitet ein Achtel aller Batches (wenn acht Shards vorhanden sind).

Obwohl jeder Shard die gleitenden Momente (d. h. Mittelwert und Varianz) berechnet, werden nur die Ergebnisse von Shard 0 an die Host-CPU zurückgegeben. So führt nur ein Replikat die Aktualisierung des gleitenden Mittelwerts/der gleitenden Varianz durch:

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

(3)

Diese Aktualisierung erfolgt mit einem Achtel der Rate des sequenziellen Gegenstücks. Damit GPU- und TPU-Aktualisierungsgleichungen verglichen werden können, müssen die entsprechenden Zeitskalen ausgerichtet werden. Insbesondere sollten die Vorgänge, aus denen acht sequenzielle Aktualisierungen auf der GPU bestehen, mit einer einzelnen Aktualisierung auf der TPU verglichen werden, wie im folgenden Diagramm dargestellt:

Image

Im Folgenden sind die Gleichungen mit den geänderten Zeitindexen aufgeführt:

\[{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) \]

Wenn wir davon ausgehen, dass 8 Mini-Batches (über alle relevanten Dimensionen normalisiert) ähnliche Werte innerhalb der sequenziellen Aktualisierung mit 8 Minibatches auf der GPU liefern, können wir diese Gleichungen so annähern:

\[{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 \mathsf(GPU)\]

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

Damit sich ein bestimmter Abklingfaktor auf die GPU auswirkt, ändern wir den Abklingfaktor auf der TPU entsprechend. Konkret haben wir \({\beta}\)=\({\alpha}^8\) festgelegt.

Bei Inception v3 ist der auf der GPU verwendete Abklingwert \({\alpha}\)=0,9997, was einem TPU-Abklingwert von \({\beta}\)=0,9976 entspricht.

Anpassung der Lernrate

Je umfangreicher die Batchgrößen, desto schwieriger gestaltet sich das Training. Damit auch bei umfangreichen Batchgrößen ein effizientes Training möglich ist, werden fortlaufend neue Techniken vorgeschlagen (Beispiele finden Sie hier, hier und hier).

Eine dieser Techniken ist die schrittweise Erhöhung der Lernrate (auch Anlaufzeit genannt). Die Erhöhung wurde verwendet,um das Modell mit einer Genauigkeit von mehr als 78,1% für Batchgrößen zwischen 4.096 und 16.384 zu trainieren. Für Inception v3 wird die Lernrate zuerst auf etwa 10% der normalerweise verwendeten Ausgangslernrate festgelegt. Die Lernrate bleibt für eine angegebene (kleine) Anzahl von „kalten Epochen“ bei diesem niedrigen Wert konstant und beginnt dann für eine bestimmte Anzahl von „Aufwärmphasen“ mit einem linearen Anstieg. Am Ende der „Aufwärmphasen“ schneidet sich die Lernrate mit dem normalen exponentiellen Abkling-Lernen. Dies wird im folgenden Diagramm veranschaulicht.

Image

Das folgende Code-Snippet zeigt, wie das funktioniert:

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