Guia avançado do Inception v3

Neste documento, discutimos aspectos do modelo Inception e como eles são combinados para que o modelo seja executado de maneira eficiente no Cloud TPU. Trata-se de uma visualização avançada do guia para executar o Inception v3 no Cloud TPU. Alterações específicas no modelo que levaram a melhorias significativas são discutidas em mais detalhes. Este documento complementa o tutorial do Inception v3.

O treinamento de TPU do Inception v3 executa curvas de precisão de correspondência produzidas por jobs de GPU com configuração semelhante. O modelo foi treinado com êxito nas configurações v2-8, v2-128 e v2-512. O modelo alcançou uma precisão maior que 78,1% em cerca de 170 períodos.

Os exemplos de código contidos neste documento são ilustrativos, uma imagem de alto nível do que acontece em uma implementação real. O código de trabalho pode ser encontrado no GitHub.

Introdução

O Inception v3 é um modelo de reconhecimento de imagem que foi mostrado para alcançar mais de 78,1% de precisão no conjunto de dados do ImageNet. Esse modelo é o auge de muitas ideias desenvolvidas por vários pesquisadores ao longo dos anos. Ele é baseado no documento original "Rethinking the Inception Architecture for Computer Vision", de Szegedy e outros autores.

O próprio modelo é composto de elementos básicos simétricos e assimétricos, incluindo convoluções, pools médios, pooling máximo, concatenações, dropouts e camadas totalmente conectadas. A normalização em lote é usada extensivamente em todo o modelo e aplicada às entradas de ativação. A perda é calculada usando Softmax.

Um diagrama de alto nível do modelo é mostrado na captura de tela a seguir:

imagem

O arquivo README do modelo Inception fornece mais informações sobre a arquitetura.

API Estimator

Para gravar a versão de TPU do Inception v3 é necessário usar a TPUEstimator, uma API projetada para facilitar o desenvolvimento, para que você possa se concentrar nos modelos em vez de nos detalhes do hardware subjacente. A API faz a maior parte do trabalho sujo de baixo nível necessário para executar os modelos nas TPUs em segundo plano, ao mesmo tempo em que automatiza funções comuns, como salvar e restaurar os pontos de verificação.

A API Estimator impõe a separação entre o modelo e as partes relativas à entrada no código. Defina as funções model_fn e input_fn, que correspondem à definição do modelo e ao pipeline de entrada. O código a seguir mostra a declaração dessas funções:

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

Duas funções principais fornecidas pela API são train() e evaluate() usadas para treinar e avaliar, conforme mostrado no seguinte código:

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)

Conjunto de dados ImageNet

Antes que o modelo possa ser usado para reconhecer imagens, ele precisa ser treinado com um grande conjunto de imagens rotuladas. ImageNet é um conjunto de dados comum a ser usado.

O ImageNet tem mais de dez milhões de URLs de imagens rotuladas. Um milhão de imagens também tem caixas delimitadoras que especificam um local mais preciso para os objetos rotulados.

Para esse modelo, o conjunto de dados ImageNet é composto de 1.331.167 imagens que são divididas em conjuntos de dados de treinamento e avaliação contendo, respectivamente, 1.281.167 e 50.000 imagens.

Os conjuntos de dados de treinamento e avaliação são intencionalmente mantidos separados. Usamos as imagens do conjunto de dados de treinamento para treinar o modelo e as do conjunto de dados de avaliação para avaliar a precisão dele.

Para o modelo, as imagens devem ser armazenadas como TFRecords. Para converter imagens de arquivos JPEG brutos em TFRecord, use o script de lote de código aberto: download_and_preprocess_imagenet.sh. O script produz uma série de arquivos, tanto para treinamento quanto para validação, neste 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

em que DATA_DIR é o local onde o conjunto reside (por exemplo, DATA_DIR=$HOME/imagenet-data).

Consulte as instruções detalhadas sobre como compilar e executar esse script na seção "Getting Started" do arquivo README do modelo Inception.

Canal de entrada

Cada dispositivo do Cloud TPU tem oito núcleos e está conectado a um host (CPU). As partes maiores têm vários hosts. Outras configurações maiores interagem com vários hosts. Por exemplo, v2-256 se comunica com 16 hosts.

Os hosts recuperam os dados do sistema de arquivos ou da memória local, fazem operações necessárias de pré-processamento de dados e, por fim, transferem os dados pré-processados para os núcleos da TPU. Essas três fases de gerenciamento de dados realizado pelo host são consideradas individualmente e recebem os nomes de: 1) armazenamento, 2) pré-processamento e 3) transferência. Uma imagem de alto nível do diagrama é mostrada na seguinte figura:

imagem

Para que o desempenho seja bom, o sistema deve ser balanceado. Se a CPU host demorar mais do que a TPU para concluir as três fases de processamento de dados, a execução será vinculada ao host. Ambos os casos são mostrados no diagrama a seguir:

imagem

A implementação atual do Inception v3 está na borda do limite de entrada. As imagens são recuperadas do sistema de arquivos, decodificadas e, em seguida, pré-processadas. Diferentes tipos de estágios de pré-processamento estão disponíveis, variando de moderado a complexo. Se usarmos os estágios de pré-processamento mais complexos, o pipeline de treinamento será vinculado ao pré-processamento. A acurácia pode ser maior que 78,1% com um estágio de pré-processamento moderadamente complexo que mantém o modelo vinculado à TPU.

O modelo usa tf.data.Dataset para processar o processamento do pipeline de entrada. Para mais informações sobre como otimizar os pipelines de entrada, consulte o guia de desempenho dos conjuntos de dados.

Embora você possa definir uma função e transmiti-la à API Estimator, a classe InputPipeline encapsula todas as funcionalidades necessárias.

A API Estimator simplifica o uso dessa classe. É necessário simplesmente transmiti-lo para o parâmetro input_fn das funções train() e evaluate(), conforme mostrado no snippet de código a seguir:

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)

Os principais elementos da InputPipeline são mostrados no snippet de código a seguir.

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

A seção storage começa com a criação de um conjunto de dados e inclui a leitura de TFRecords do armazenamento (usando tf.data.TFRecordDataset). As funções de finalidade especial repeat() e shuffle() são usadas conforme necessário. A função tf.contrib.data.parallel_interleave() mapeia a função prefetch_dataset() para a entrada dela a fim de produzir conjuntos de dados aninhados e gera os elementos intercalados. Ela recebe elementos dos conjuntos de dados aninhados cycle_length em paralelo, o que aumenta a capacidade. O argumento sloppy flexibiliza o requisito de que as saídas sejam produzidas em uma ordem determinística e permite que a implementação ignore conjuntos de dados aninhados cujos elementos não estejam disponíveis quando solicitados.

A seção preprocessing chama dataset.map(parser), que por sua vez chama a função do analisador em que as imagens são pré-processadas. Discutiremos os detalhes do estágio de pré-processamento na próxima seção.

A seção transfer (no final da função) inclui a linha return images, labels. A TPUEstimator pega os valores retornados e os transfere automaticamente para o dispositivo.

A figura a seguir mostra um exemplo de trace de desempenho do Cloud TPU do Inception v3. O tempo de computação da TPU, ignorando todas as barracas de alimentação, é de aproximadamente 815 ms.

imagem

O armazenamento do host é gravado no trace e mostrado na seguinte captura de tela:

imagem

O pré-processamento do host, que inclui a decodificação de imagens e uma série de funções de distorção de imagem, é mostrado na captura de tela a seguir:

imagem

A transferência de host/TPU é mostrada na seguinte captura de tela:

imagem

Estágio de pré-processamento

O pré-processamento de imagens é uma parte crucial do sistema e pode influenciar a precisão máxima que o modelo alcança durante o treinamento. No mínimo, as imagens precisam ser decodificadas e redimensionadas para se ajustarem ao modelo. Na Origem, as imagens precisam ter 299 x 299 x 3 pixels.

No entanto, simplesmente decodificar e redimensionar não são suficientes para obter uma boa precisão. O conjunto de dados de treinamento ImageNet contém 1.281.167 imagens. Uma passagem pelo conjunto de imagens de treinamento é chamada de uma época. Durante o treinamento, o modelo requer várias passagens pelo conjunto de dados de treinamento para melhorar os recursos de reconhecimento de imagem. Para treinar o Inception v3 com precisão suficiente, use entre 140 e 200 períodos, dependendo do tamanho global do lote.

É útil alterar continuamente as imagens antes de alimentá-las ao modelo, de modo que uma imagem específica seja ligeiramente diferente em cada época. A melhor forma de fazer esse pré-processamento de imagens é fazer arte e ciências. Um estágio de pré-processamento bem projetado pode aumentar significativamente os recursos de reconhecimento de um modelo. Um estágio de pré-processamento muito simples pode criar um teto artificial na acurácia do mesmo modelo durante o treinamento.

O Inception v3 oferece opções para o estágio de pré-processamento, que varia entre relativamente simples e computacionalmente barato, de maneira relativamente complexa e cara em termos de computação. Você pode encontrar dois tipos distintos desses estágios nos arquivos vgg_preprocessing.py e inception_preprocessing.py.

O arquivo vgg_preprocessing.py define um estágio de pré-processamento que foi usado para treinar resnet com 75% de acurácia, mas produz resultados abaixo do ideal quando aplicado ao Inception v3.

O arquivo inception_developing.py contém um estágio de pré-processamento que foi usado para treinar o Inception v3 com precisão entre 78,1 e 78,5% quando executado em TPUs.

O pré-processamento varia dependendo se o modelo está em treinamento ou é usado para inferência/avaliação.

No momento da avaliação, o pré-processamento é simples: corte uma região central da imagem e redimensione-a para o tamanho padrão de 299 x 299. O snippet de código a seguir mostra uma implementação de pré-processamento:

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 o treinamento, o corte é aleatório: uma caixa delimitadora é escolhida aleatoriamente para selecionar uma região da imagem que, em seguida, é redimensionada. Depois, a imagem redimensionada pode opcionalmente ser invertida e ter as cores distorcidas. O snippet de código a seguir mostra uma implementação dessas operações:

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

A função distort_color é responsável pela alteração de cor. Ela oferece um modo rápido em que apenas o brilho e a saturação são modificados. O modo completo modifica o brilho, a saturação e a tonalidade, em ordem aleatória.

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)

A função distort_color é cara em termos de computação, em parte devido às conversões não lineares de RGB em HSV e de HSV em RGB, que são necessárias para acessar o matiz e a saturação. Tanto o modo rápido quanto o completo requerem essas conversões. O modo rápido é mais econômico em termos computacionais, mas mesmo assim ele empurra o modelo para a região limitada por computação da CPU quando ativado.

Como alternativa, uma nova função distort_color_fast foi adicionada à lista de opções. Essa função mapeia a imagem de RGB para YCrCb usando o esquema de conversão de JPEG e altera aleatoriamente o brilho e os cromos de Cr/Cb antes de remapear para RGB. O snippet de código a seguir mostra uma implementação dessa função:

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

Abaixo, temos um exemplo de imagem que foi submetida ao pré-processamento. Uma região da imagem escolhida aleatoriamente foi selecionada e as cores alteradas usando a função distort_color_fast.

imagem

A função distort_color_fast é eficiente em termos de computação e ainda permite que o treinamento seja limitado ao tempo de execução da TPU. Além disso, ele foi usado para treinar o modelo Inception v3 com uma precisão maior que 78,1% usando tamanhos de lotes entre 1.024 e 16.384.

Otimizador

O modelo atual apresenta três opções de otimizador: SGD, momentum e RMSProp.

Stochastic gradient descent (SGD) é a atualização mais simples: os pesos são deslocados na direção negativa do gradiente. Ainda que simples, esse tipo consegue bons resultados em alguns modelos. A dinâmica de atualização pode ser escrita como:

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

O Momentum é um otimizador muito usado que geralmente leva à convergência mais rápida do que SGD. Esse otimizador atualiza pesos como o SGD, mas também adiciona um componente na direção da atualização anterior. As equações a seguir descrevem as atualizações realizadas pelo otimizador de momento:

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

que pode ser escrita como:

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

O último termo é o componente na direção da atualização anterior.

imagem

Para o momento \({\beta}\), usamos o valor de 0,9.

RMSprop é um otimizador muito usado proposto pela primeira vez por Geoff Hinton em uma de suas palestras. As seguintes equações descrevem como o otimizador funciona:

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

No caso do Inception v3, os testes mostram que o RMSProp gera os melhores resultados em termos de precisão máxima e tempo para atingi-la, com momentum em segundo lugar. Portanto, o RMSprop é configurado como o otimizador padrão. Os parâmetros utilizados são: decay \({\alpha}\) = 0.9, momentum \({\beta}\) = 0.9 e \({\epsilon}\) = 1.0.

O snippet de código a seguir mostra como definir esses parâmetros:

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)

Ao executar em TPUs e usar a API Estimator, o otimizador precisa ser agrupado em uma função CrossShardOptimizer para assegurar a sincronização entre as réplicas (com qualquer comunicação cruzada necessária). O snippet de código a seguir mostra como o modelo Inception v3 envolve o otimizador:

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)

Média móvel exponencial

Durante o treinamento, os parâmetros treináveis são atualizados durante a retropropagação de acordo com as regras de atualização do otimizador. As equações que descrevem essas regras foram discutidas na seção anterior e repetidas aqui por conveniência:

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

A média móvel exponencial (também conhecida como suavização exponencial) é uma etapa opcional de pós-processamento, aplicada às ponderações atualizadas. s vezes, ela pode melhorar o desempenho de maneira perceptível. O TensorFlow fornece a função tf.train.ExponentialMovingAverage que calcula a média móvel exponencial \({\ hat {\ theta}} \) de peso \({\ theta} \) usando a fórmula:

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

em que \({\alpha}\) é um fator de decaimento (próximo a 1,0). No modelo Inception v3, \({\alpha}\) está definido como 0,995.

Mesmo que esse cálculo seja um filtro de resposta ao impulso infinito (IIR, na sigla em inglês), o fator de decaimento estabelece uma janela efetiva em que reside a maior parte da energia (ou amostras relevantes), conforme mostrado no diagrama a seguir:

imagem

Podemos reescrever a equação do filtro da seguinte maneira:

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

em que usamos\({\hat\theta_{-1}}=0\).

Os valores de \({\alpha}k\) reduzem com o aumento de k, então apenas um subconjunto de amostras terá uma influência considerável sobre \(\hat{\theta}_{t+T+1}\). A regra geral para o valor do fator de redução é: \(\frac {1} {1-\alpha}\), que corresponde a \({\alpha}\) = 200 para =0,995.

Primeiro, recebemos uma coleção de variáveis treináveis e, em seguida, usamos o método apply() para criar variáveis de sombra para cada variável treinada. O snippet de código a seguir mostra a implementação do modelo 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)

Queremos usar as variáveis de média móvel exponencial durante a avaliação. Definimos a classe LoadEMAHook que aplica o método variables_to_restore() ao arquivo de ponto de verificação para avaliação usando os nomes das variáveis de sombra:

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)

A função hooks é transmitida para evaluate(), conforme mostrado no snippet de código a seguir:

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)

Normalização em lote

A normalização em lote é uma técnica amplamente usada para normalizar as características de entrada em modelos, podendo resultar na redução substancial do tempo de convergência. Essa é uma das melhorias algorítmicas mais úteis e utilizadas no aprendizado de máquina nos últimos anos. Ela é aplicada a uma ampla gama de modelos, incluindo o Inception v3.

As entradas de ativação são normalizadas subtraindo a média e dividindo pelo desvio padrão. Para manter tudo equilibrado em vista da retropropagação, dois parâmetros treináveis são introduzidos em cada camada. As saídas normalizadas \({\hat{x}}\) passam por uma operação subsequente \({\gamma\hat{x}}+\beta\), em que \({\gamma}\) e \({\beta}\) são um tipo de desvio padrão e significam do próprio aprendizado.

Você pode ver o conjunto completo de equações neste artigo. Para sua comodidade, também as repetimos aqui:

Entrada: valores de x em um minilote: \(\Phi=\{ {x_{1..m}\} }\) Parâmetros a serem aprendidos: \({\gamma}\),\({\beta}\)

Saída: { \({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)\]

A normalização acontece durante o treinamento, mas há um tempo de avaliação. O modelo precisa se comportar de maneira determinista: o resultado da classificação de uma imagem precisa depender apenas da imagem de entrada, e não do conjunto de imagens que estão sendo alimentadas no modelo. Assim, precisamos corrigir \({\mu}\) e \({\sigma}2\) e usar valores que representem as estatísticas de população da imagem.

O modelo calcula médias móveis das médias e variâncias sobre os minilotes:

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

No caso específico do Inception v3, um fator de declínio sensato foi recebido (usando ajuste de hiperparâmetro) para uso em GPUs. Queremos usar esse valor nas TPUs também. Mas para tanto, precisamos fazer alguns ajustes.

A média de movimentação e a variância de normalização em lote são calculadas usando um filtro de passagem de perda, conforme mostrado na seguinte equação (aqui, \({y_t}\) representa média de variação ou variação):

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

(1)

Em um job de GPU (síncrono) de 8 x 1, cada réplica lê a média móvel atual e a atualiza. A réplica atual precisa gravar a nova variável em movimento antes que a próxima réplica possa lê-la.

Quando há oito réplicas, o conjunto de operações para uma atualização combinada é:

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

Esse conjunto de oito atualizações sequenciais pode ser escrito como:

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

(2)

Na implementação atual do cálculo de momento em movimento nas TPUs, cada fragmento executa cálculos de maneira independente, e não há comunicação entre fragmentos. Os lotes são distribuídos para cada fragmento. Cada um deles processa 1/8 do número total de lotes (quando há oito fragmentos).

Embora cada fragmento calcule os momentos em movimento (ou seja, média e variação), apenas os resultados do fragmento 0 são comunicados de volta para a CPU do host. Assim, efetivamente, apenas uma réplica faz a atualização da média/variação:

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

(3)

e essa atualização acontece em 1/8 da taxa do equivalente na sequência. Para comparar as equações de atualização da GPU e da TPU, precisamos alinhar as respectivas escalas de tempo. Especificamente, o conjunto de operações que compõem um conjunto de oito atualizações sequenciais na GPU deve ser comparado com uma única atualização na TPU, conforme ilustrado no diagrama a seguir:

imagem

Estas são as equações com os índices de tempo modificados:

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

Supondo que 8 minilotes (normalizados em todas as dimensões relevantes) produzem valores semelhantes na atualização sequencial de 8 minilotes da GPU, podemos aproximar essas equações da seguinte maneira:

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

Para corresponder o efeito de um determinado fator de redução na GPU, modificamos o fator de redução na TPU de acordo com isso. Especificamente, definimos \({\beta}\)=\({\alpha}8\).

No caso do modelo Inception v3, o valor de decaimento usado na GPU é \({\alpha}\)=0.9997, que se traduz em um valor de decaimento na TPU de \({\beta}\)=0.9976.

Adaptação da taxa de aprendizado

À medida que os tamanhos dos lotes aumentam, o treinamento fica mais difícil. Diferentes técnicas continuam a ser propostas para proporcionar um treinamento eficiente de lotes grandes (consulte aqui, aqui e aqui, por exemplo).

Uma dessas técnicas é aumentar a taxa de aprendizado gradualmente. A ampliação foi usada para treinar o modelo com mais de 78,1% de precisão para tamanhos de lotes que variam de 4.096 a 16.384. No Inception v3, a taxa de aprendizado é definida primeiro em cerca de 10% do que normalmente seria a taxa de aprendizado inicial. A taxa de aprendizado permanece constante com esse valor baixo para um número especificado (pequeno) de 'tempos frios' e, em seguida, inicia um aumento linear para um número especificado de períodos de aquecimento'. No final dos períodos de aquecimento, a taxa de aprendizado cruza-se com o aprendizado de declínio exponencial normal. Isso é ilustrado no diagrama a seguir.

imagem

O snippet de código a seguir mostra como fazer isso.

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