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 ungefähr 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% mit dem 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, einschließlich Faltungen, durchschnittlicher Pools, Max-Pooling, Verkettungen, Ausstiegen und vollständig verbundenen Ebenen. Die Batchnormalisierung wird umfassend im Modell verwendet und auf Aktivierungseingaben angewendet. Verluste werden mit Softmax berechnet.
Der Screenshot enthält ein allgemeines Diagramm:
Die README-Datei des Inception-Modells enthält weitere Informationen zur Inception-Architektur.
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
, die der Modelldefinition und der Eingabepipeline entsprechen. 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
Die beiden wichtigsten Funktionen der API sind train()
und evaluate()
, die wie im folgenden Code zum Trainieren und Auswerten verwendet werden:
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
Damit das Modell Bilder erkennen kann, muss es mit einer großen Anzahl von Bildern mit Labels trainiert werden.ImageNet ist ein gängiger Datensatz.
ImageNet enthält über zehn Millionen URLs von beschrifteten Bildern. Eine Million der Bilder haben auch Begrenzungsrahmen, die einen genaueren Standort für die mit Labels versehenen Objekte angeben.
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. Verwenden Sie das Open-Source-Batchskript download_and_preprocess_imagenet.sh
, um Bilder aus RAW-JPEG-Dateien in TFRecords umzuwandeln.
Das Skript müsste eine Reihe von Dateien (für Training und Validierung) in folgender Form erzeugen:
${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
Dabei ist DATA_DIR der Speicherort, an dem sich das Dataset befindet, beispielsweise DATA_DIR=$HOME/imagenet-data.
Der Abschnitt "Getting Started" der README-Datei des Inception-Modells enthält eine detaillierte Anleitung zum Erstellen und Ausführen dieses Skripts.
Eingabe-Pipeline
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. Beispielsweise kommuniziert die v2-256-Konfiguration 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 Grafik zeigt ein allgemeines Bild des Diagramms:
Damit das System eine gute Leistung erzielt, sollte es ausgewogen sein. Wenn die Host-CPU länger als die TPU benötigt, um die drei Datenverarbeitungsphasen abzuschließen, ist die Ausführung hostgebunden. Beide Fälle sind im folgenden Diagramm dargestellt:
Die aktuelle Implementierung von Inception v3 befindet sich am Rand der Eingabebindung. Bilder werden aus dem Dateisystem abgerufen, decodiert und dann verarbeitet. Es gibt verschiedene Arten von Vorverarbeitungsphasen, von mäßig bis komplex. Wenn wir die komplexesten Vorverarbeitungsphasen verwenden, ist die Trainingspipeline an die Vorverarbeitung gebunden. Sie können eine Genauigkeit von mehr als 78,1% mit einer mäßig komplexen Vorverarbeitungsphase erreichen, die das Modell TPU-gebunden hält.
Das Modell verwendet tf.data.Dataset, um die Verarbeitung der Eingabepipeline zu verarbeiten. Weitere Informationen zum Optimieren von Eingabepipelines finden Sie im Leitfaden zur Leistung von Datasets.
Obwohl Sie eine Funktion definieren und an die Recommender API übergeben können, enthält die Klasse InputPipeline
alle erforderlichen Funktionen.
Die Recommender API vereinfacht die Verwendung dieser Klasse. Dazu muss sie einfach an den Parameter input_fn
der Funktionen train()
und evaluate()
übergeben werden, wie im folgenden Code-Snippet dargestellt:
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 folgenden Elemente von InputPipeline
sind 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, wobei alle Stände der Einspeisung ignoriert werden, liegt bei etwa 815 ms.
Der Speicher des Hosts wird in den Trace geschrieben und im folgenden Screenshot gezeigt:
Im folgenden Screenshot ist die Vorverarbeitung des Hosts dargestellt, einschließlich Bilddecodierung und einer Reihe von Bildverzerrungsfunktionen:
Die Host-/TPU-Übertragung wird im folgenden Screenshot dargestellt:
Vorverarbeitungsphase
Die Bildvorverarbeitung ist ein wichtiger Bestandteil des Systems und kann die maximale Genauigkeit, die das Modell während des Trainings erzielt, beeinflussen. Bilder müssen zumindest decodiert und skaliert werden, um sie an das Modell anzupassen. Bilder müssen 299 x 299 x 3 Pixel groß sein.
Allerdings lässt sich über Decodierung und Größenanpassung allein noch keine gute Genauigkeit 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 benötigt das Modell mehrere Trainingsläufe, um die Bilderkennungsfunktionen 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 sinnvoll, die Bilder kontinuierlich zu ändern, bevor sie in das Modell eingespeist werden, damit ein bestimmtes Bild in jeder Epoche leicht unterschiedlich ist. Die Vorverarbeitung von Bildern ist genauso viel Kunst wie Wissenschaft. Eine gut durchdachte Vorverarbeitungsphase kann die Erkennungsmöglichkeiten eines Modells erheblich steigern. Eine zu einfache Vorverarbeitungsphase kann die Genauigkeit des Modells, das dasselbe Modell während des Trainings erreichen kann, zu einem ungünstigen Preis bringen.
Inception v3 bietet Optionen für die Vorverarbeitungsphase, die von relativ einfach und rechenintensiv bis zu recht komplex und relativ rechenintensiv ist. Zwei dieser verschiedenen Varianten finden Sie in den Dateien vgg_preprocessing.py und inception_preprocessing.py.
Die Datei vgg_preprocess.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, mit der Inception v3 mit Genauigkeiten zwischen 78,1 und 78,5% beim Ausführen auf TPUs trainiert wird.
Die Vorverarbeitung variiert in Abhängigkeit davon, ob das Modell trainiert oder für die Inferenz/Evaluation verwendet wird.
Bei der Bewertung ist die Vorverarbeitung einfach: Schneiden Sie einen zentralen Bereich des Bilds zu und ändern Sie dann die Größe auf die Standardgröße 299 x 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 Vollbildmodus 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. Im folgenden Code-Snippet wird diese Funktion implementiert:
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.
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 von 1.024 bis 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:
Momentum ist eine beliebte Optimierung, die häufig zu schnellerer 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 vom Impulsoptimierer durchgeführten Aktualisierungen:
Dies lässt sich formulieren als:
Der letzte Term ist die Komponente in Richtung der vorherigen Aktualisierung.
Für den Momentum \({\beta}\) verwenden wir den Wert 0,9.
RMSprop ist eine weitverbreitete Optimierung, die zuerst von Geoff Hinton in einer seiner Vorlesungen vorgestellt wurde. Die folgende Gleichung beschreibt die Funktionsweise des Optimierungstools:
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.
Im folgenden Code-Snippet sehen Sie, wie die 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 Optimierungstool das Inception v3-Modell 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 gemäß der Aktualisierungsregeln des Optimierers während der Rückverteilung aktualisiert. Die Gleichungen, mit denen diese Regeln beschrieben werden, wurden im vorherigen Abschnitt erläutert und der Einfachheit halber wiederholt:
Exponentieller gleitender Durchschnitt (auch als exponentielle Glättung bezeichnet) ist ein optionaler Nachbearbeitungsschritt, der auf die aktualisierten Gewichtungen angewendet wird und manchmal zu signifikanten 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:
Dabei ist \({\alpha}\) ein Abklingfaktor (nahe 1,0). Im Inception v3-Modell ist \({\alpha}\) auf 0,995 festgelegt.
Obwohl diese Berechnung ein Filter des Typs "Infinite Impulse Response" (IIR) ist, bestimmt der Zerfallsfaktor ein effektives Fenster, in dem sich der Großteil der Energie (oder der relevanten Stichproben) befindet, wie im folgenden Diagramm dargestellt:
So können wir die Filtergleichung umschreiben:
Dabei wurde \({\hat\theta_{-1}}=0\) verwendet.
Die Werte von \({\alpha}^k\) nehmen mit steigendem k-Wert ab, sodass nur ein Teil der Beispiele einen beträchtlichen Einfluss auf \(\hat{\theta}_{t+T+1}\) hat. Die Faustregel für den Wert des Zeitverlaufs ist: \(\frac {1} {1-\alpha}\), was \({\alpha}\) = 200 für =0,995 entspricht.
Zuerst rufen wir eine Sammlung trainierbarer Variablen ab und verwenden dann die apply()
-Methode, 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 Checkpoint-Datei anwendet, um sie mit den Namen der Schattenvariablen zu bewerten:
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 dargestellt:
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. Normalisierte Ausgaben \({\hat{x}}\) werden einem nachfolgenden Vorgang \({\gamma\hat{x}}+\beta\) zugewiesen. Dabei sind \({\gamma}\) und \({\beta}\) eine Art Standardabweichung und der vom Modell selbst erschlossene Mittelwert.
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 die Bewertungszeit entspricht dem Verhalten des Modells deterministisch: Das Klassifizierungsergebnis eines Bilds sollte nur von dem Eingabebild abhängen, nicht von der Gruppe von Bildern, die in das Modell eingespeist werden. Darum müssen wir \({\mu}\) und \({\sigma}^2\) korrigieren und Werte verwenden, die die Bildpopulationsstatistiken darstellen.
Das Modell berechnet gleitende Durchschnitte des Mittelwerts und der Abweichung über den Mini-Batches:
\[{\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 konkreten Fall von Inception v3 wurde durch die Hyperparameter-Abstimmung ein vernünftiger Faktor für die Verwendung in GPUs ermittelt. Damit dieser Wert auch auf TPUs verwendet werden kann, müssen jedoch einige Anpassungen vorgenommen werden.
Die Durchschnittsbewegung und die Varianz der Batchnormalisierung werden mit einem Verlustpassfilter berechnet, wie in der folgenden Gleichung gezeigt (hier (\({y_t}\)) steht für den sich bewegenden Mittelwert oder die Varianz):
\[{y_t}={\alpha y_{t-1}}+{(1-\alpha)}{x_t} \]
(1)
In einem synchronen 8-x-1-GPU-Job liest jedes Replikat den aktuellen sich bewegenden Mittelwert und aktualisiert ihn. Das neue Replikat muss die neue bewegende Variable schreiben, bevor sie vom nächsten Replikat gelesen werden 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 der Freisetzung von TPUs auf jedem TPU wird jedes Shard separat berechnet und es gibt keine fragmentierte Kommunikation. Batches werden auf die einzelnen Shards verteilt und jeder Shard verarbeitet ein Achtel aller Batches (wenn acht Shards vorhanden sind).
Obwohl alle Shards die sich bewegenden Momente berechnen (d. h. Mittel und Abweichung), werden nur die Ergebnisse von Shard 0 an die Host-CPU zurückgegeben. Tatsächlich führt also nur ein Replikat die Verschiebung der mittleren/Abweichung durch:
\[{z_t}={\beta {z_{t-1}}}+{(1-\beta)u_t}\]
(3)
und diese Aktualisierung erfolgt mit einem Achtel der Rate der sequenziellen Version. 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 Updates auf der GPU bestehen, mit einer einzelnen Aktualisierung auf der TPU verglichen werden, wie im folgenden Diagramm dargestellt:
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) innerhalb der sequenziellen Aktualisierung mit 8 Mini-Batches ähnliche Werte erzielen, können wir diese Gleichungen so schätzen:
\[{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 der Effekt eines bestimmten Zeitverlaufs auf der GPU berücksichtigt wird, passen wir den Zeitverlauf für die TPU entsprechend an. Insbesondere 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 erhöht die Lernrate allmählich (auch als Anlaufzeit bekannt). Das System wurde so trainiert,dass es eine Genauigkeit von über 78,1% für Batchgrößen von 4.096 bis 16.384 erreicht. Für Inception v3 wird die Lernrate zuerst auf ungefähr 10% der anfänglichen Lernrate festgelegt. Die Lernrate bleibt konstant bei diesem niedrigen Wert für eine bestimmte (kleine) Anzahl von Epochen (&) Am Ende der &Warm-up-Epochen liegt die Lernrate mit dem normalen exponentiellen Verlauf der Falke. Dies wird im folgenden Diagramm veranschaulicht:
Das folgende Code-Snippet zeigt, wie das geht:
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')