Keras e convnets modernas em TPUs

1. Visão geral

Neste laboratório, você vai aprender a criar, treinar e ajustar suas próprias redes neurais convolucionais do zero com a Keras e o TensorFlow 2. Agora isso pode ser feito em minutos usando o poder das TPUs. Você também vai conhecer várias abordagens, desde o aprendizado por transferência muito simples até arquiteturas convolucionais modernas, como a SqueezeNet. Este laboratório inclui explicações teóricas sobre redes neurais e é um bom ponto de partida para desenvolvedores que estão aprendendo sobre aprendizado profundo.

Ler artigos sobre aprendizado profundo pode ser difícil e confuso. Vamos analisar na prática as arquiteturas modernas de redes neurais convolucionais.

ca8cc21f6838eccc.png

O que você vai aprender

  • Usar o Keras e as unidades de processamento de tensor (TPUs) para criar modelos personalizados mais rápido.
  • Usar a API tf.data.Dataset e o formato TFRecord para carregar dados de treinamento de forma eficiente.
  • Para trapacear 😈, use o aprendizado por transferência em vez de criar seus próprios modelos.
  • Para usar estilos de modelos sequenciais e funcionais do Keras.
  • Para criar seu próprio classificador do Keras com uma camada softmax e perda de entropia cruzada.
  • Para ajustar seu modelo com uma boa escolha de camadas convolucionais.
  • Para explorar ideias modernas de arquitetura de rede neural convolucional, como módulos, agrupamento médio global etc.
  • Para criar uma rede neural convolucional moderna simples usando a arquitetura SqueezeNet.

Feedback

Se você notar algo errado neste codelab, informe-nos. O feedback pode ser enviado usando os problemas do GitHub [ link de feedback].

2. Início rápido do Google Colaboratory

Este laboratório usa o Google Colaboratory e não exige configuração da sua parte. É possível executar em um Chromebook. Abra o arquivo abaixo e execute as células para se familiarizar com os notebooks do Colab.

c3df49e90e5a654f.png Welcome to Colab.ipynb

Selecione um back-end de TPU.

8832c6208c99687d.png

No menu do Colab, selecione Ambiente de execução > Alterar o tipo de ambiente de execução e escolha TPU. Neste codelab, você vai usar uma TPU (Unidade de Processamento de Tensor) avançada com suporte para treinamento acelerado por hardware. A conexão com o ambiente de execução acontece automaticamente na primeira execução, ou você pode usar o botão "Conectar" no canto superior direito.

Execução do notebook

76d05caa8b4db6da.png

Execute as células uma de cada vez clicando em uma delas e usando Shift + ENTER. Você também pode executar o notebook inteiro com Ambiente de execução > Executar tudo.

Índice

429f106990037ec4.png

Todos os notebooks têm um sumário. É possível abrir usando a seta preta à esquerda.

Células ocultas

edc3dba45d26f12a.png

Algumas células vão mostrar apenas o título. Esse é um recurso específico do notebook do Colab. Clique duas vezes neles para ver o código, mas geralmente não é muito interessante. Normalmente, funções de suporte ou visualização. Ainda é necessário executar essas células para que as funções sejam definidas.

Authentication

cdd4b41413100543.png

O Colab pode acessar seus buckets privados do Google Cloud Storage, desde que você faça a autenticação com uma conta autorizada. O snippet de código acima vai acionar um processo de autenticação.

3. [INFO] O que são Unidades de Processamento de Tensor (TPUs)?

Em poucas palavras

f88cf6facfc70166.png

O código para treinar um modelo em TPU no Keras (e usar GPU ou CPU se uma TPU não estiver disponível):

try: # detect TPUs
    tpu = tf.distribute.cluster_resolver.TPUClusterResolver.connect()
    strategy = tf.distribute.TPUStrategy(tpu)
except ValueError: # detect GPUs
    strategy = tf.distribute.MirroredStrategy() # for CPU/GPU or multi-GPU machines

# use TPUStrategy scope to define model
with strategy.scope():
  model = tf.keras.Sequential( ... )
  model.compile( ... )

# train model normally on a tf.data.Dataset
model.fit(training_dataset, epochs=EPOCHS, steps_per_epoch=...)

Hoje, vamos usar TPUs para criar e otimizar um classificador de flores em velocidades interativas (minutos por execução de treinamento).

688858c21e3beff2.png

Por que usar TPUs?

As GPUs modernas são organizadas em torno de "núcleos" programáveis, uma arquitetura muito flexível que permite lidar com várias tarefas, como renderização 3D, aprendizado profundo, simulações físicas etc. As TPUs, por outro lado, combinam um processador vetorial clássico com uma unidade de multiplicação de matrizes dedicada e se destacam em qualquer tarefa em que grandes multiplicações de matrizes predominam, como redes neurais.

8eb3e718b8e2ed08.png

Ilustração: uma camada densa de rede neural como uma multiplicação de matrizes, com um lote de oito imagens processadas pela rede neural de uma só vez. Faça uma multiplicação de linha por coluna para verificar se ela está fazendo uma soma ponderada de todos os valores de pixels de uma imagem. As camadas convolucionais também podem ser representadas como multiplicações de matrizes, embora seja um pouco mais complicado ( explicação aqui, na seção 1).

O hardware

MXU e VPU

Um núcleo de TPU v2 é composto por uma unidade de multiplicação de matrizes (MXU) que executa multiplicações de matrizes e uma unidade de processamento vetorial (VPU) para todas as outras tarefas, como ativações, softmax etc. A VPU processa cálculos float32 e int32. Já a MXU opera em um formato de ponto flutuante de precisão mista de 16 a 32 bits.

7d68944718f76b18.png

Ponto flutuante de precisão mista e bfloat16

A MXU calcula multiplicações de matrizes usando entradas bfloat16 e saídas float32. As acumulações intermediárias são realizadas com precisão float32.

19c5fc432840c714.png

O treinamento de redes neurais geralmente é resistente ao ruído introduzido por uma precisão de ponto flutuante reduzida. Há casos em que o ruído ajuda o otimizador a convergir. A precisão de ponto flutuante de 16 bits é usada tradicionalmente para acelerar cálculos, mas os formatos float16 e float32 têm intervalos muito diferentes. A redução da precisão de float32 para float16 geralmente resulta em estouros e subfluxos. Existem soluções, mas geralmente é necessário trabalho extra para fazer o float16 funcionar.

Por isso, o Google introduziu o formato bfloat16 nas TPUs. O bfloat16 é um float32 truncado com exatamente os mesmos bits de expoente e intervalo do float32. Isso, somado ao fato de que as TPUs calculam multiplicações de matriz em precisão mista com entradas bfloat16, mas saídas float32, significa que, normalmente, não são necessárias mudanças no código para aproveitar os ganhos de desempenho da precisão reduzida.

Matriz sistólica

A MXU implementa multiplicações de matrizes em hardware usando uma arquitetura chamada "matriz sistólica", em que os elementos de dados fluem por uma matriz de unidades de computação de hardware. Na medicina, "sistólica" se refere às contrações cardíacas e ao fluxo sanguíneo, e aqui ao fluxo de dados.

O elemento básico de uma multiplicação de matrizes é um produto escalar entre uma linha de uma matriz e uma coluna da outra (consulte a ilustração na parte de cima desta seção). Para uma multiplicação de matrizes Y=X*W, um elemento do resultado seria:

Y[2,0] = X[2,0]*W[0,0] + X[2,1]*W[1,0] + X[2,2]*W[2,0] + ... + X[2,n]*W[n,0]

Em uma GPU, esse produto escalar seria programado em um "núcleo" da GPU e executado em quantos "núcleos" estiverem disponíveis em paralelo para tentar calcular todos os valores da matriz resultante de uma só vez. Se a matriz resultante for grande (128 x 128), serão necessários 128 x 128=16 mil "núcleos", o que geralmente não é possível. As maiores GPUs têm cerca de 4.000 núcleos. Uma TPU, por outro lado, usa o mínimo de hardware para as unidades de computação na MXU: apenas bfloat16 x bfloat16 => float32 acumuladores de multiplicação, nada mais. Elas são tão pequenas que uma TPU pode implementar 16 mil delas em uma MXU de 128x128 e processar essa multiplicação de matrizes de uma só vez.

f1b283fc45966717.gif

Ilustração: a matriz sistólica da MXU. Os elementos de computação são acumuladores de multiplicação. Os valores de uma matriz são carregados na matriz (pontos vermelhos). Os valores da outra matriz fluem pela matriz (pontos cinzas). As linhas verticais propagam os valores para cima. As linhas horizontais propagam somas parciais. O usuário precisa verificar se, à medida que o fluxo de dados passa pela matriz, o resultado da multiplicação de matrizes sai do lado direito.

Além disso, enquanto os produtos escalares são calculados em uma MXU, as somas intermediárias simplesmente fluem entre unidades de computação adjacentes. Não é necessário armazená-los e recuperá-los da memória ou até mesmo de um arquivo de registro. O resultado final é que a arquitetura de matriz sistólica da TPU tem uma vantagem significativa de densidade e energia, além de uma vantagem de velocidade não insignificante em relação a uma GPU, ao calcular multiplicações de matrizes.

Cloud TPU

Ao solicitar um Cloud TPU v2 no Google Cloud Platform, você recebe uma máquina virtual (VM) com uma placa de TPU conectada por PCI. A placa de TPU tem quatro chips de TPU de núcleo duplo. Cada núcleo de TPU tem uma VPU (unidade de processamento vetorial) e uma MXU (unidade de multiplicação de matrizes) de 128x128. Essa "Cloud TPU" geralmente é conectada pela rede à VM que a solicitou. Então, o panorama completo é este:

dfce5522ed644ece.png

Ilustração: sua VM com um acelerador "Cloud TPU" conectado à rede. "O Cloud TPU" em si é feito de uma VM com uma placa de TPU conectada a PCI com quatro chips de TPU de núcleo duplo.

Pods de TPU

Nos data centers do Google, as TPUs são conectadas a uma interconexão de computação de alto desempenho (HPC, na sigla em inglês), o que pode fazê-las parecer um acelerador muito grande. O Google os chama de pods, e eles podem abranger até 512 núcleos de TPU v2 ou 2.048 núcleos de TPU v3.

2ec1e0d341e7fc34.jpeg

Ilustração: um pod de TPU v3. Placas e racks de TPU conectados por interconexão de HPC.

Durante o treinamento, os gradientes são trocados entre os núcleos da TPU usando o algoritmo de redução total ( boa explicação da redução total aqui). O modelo em treinamento pode aproveitar o hardware treinando com tamanhos de lote grandes.

d97b9cc5d40fdb1d.gif

Ilustração: sincronização de gradientes durante o treinamento usando o algoritmo all-reduce na rede HPC de malha toroidal 2D da TPU do Google.

O software

Treinamento com tamanho de lote grande

O tamanho do lote ideal para TPUs é de 128 itens de dados por núcleo de TPU, mas o hardware já pode mostrar boa utilização com 8 itens de dados por núcleo de TPU. Uma Cloud TPU tem oito núcleos.

Neste codelab, vamos usar a API Keras. No Keras, o lote especificado é o tamanho do lote global para toda a TPU. Seus lotes serão divididos automaticamente em 8 e executados nos 8 núcleos da TPU.

da534407825f01e3.png

Para mais dicas de performance, consulte o Guia de performance de TPU. Para tamanhos de lote muito grandes, alguns modelos podem precisar de cuidados especiais. Consulte LARSOptimizer para mais detalhes.

Funcionamento interno: XLA

Os programas do TensorFlow definem gráficos de computação. A TPU não executa o código Python diretamente, mas sim o gráfico de computação definido pelo programa do TensorFlow. Nos bastidores, um compilador chamado XLA (compilador de álgebra linear acelerada) transforma o gráfico do Tensorflow de nós de computação em código de máquina da TPU. Esse compilador também realiza muitas otimizações avançadas no seu código e no layout da memória. A compilação acontece automaticamente à medida que o trabalho é enviado para a TPU. Não é necessário incluir o XLA explicitamente na sua cadeia de build.

edce61112cd57972.png

Ilustração: para ser executado na TPU, o gráfico de computação definido pelo programa do TensorFlow é primeiro traduzido para uma representação do XLA (compilador de álgebra linear acelerada) e depois compilado pelo XLA em código de máquina da TPU.

Como usar TPUs no Keras

As TPUs são compatíveis com a API Keras desde o TensorFlow 2.1. O suporte do Keras funciona em TPUs e pods de TPU. Confira um exemplo que funciona em TPUs, GPUs e CPUs:

try: # detect TPUs
    tpu = tf.distribute.cluster_resolver.TPUClusterResolver.connect()
    strategy = tf.distribute.TPUStrategy(tpu)
except ValueError: # detect GPUs
    strategy = tf.distribute.MirroredStrategy() # for CPU/GPU or multi-GPU machines

# use TPUStrategy scope to define model
with strategy.scope():
  model = tf.keras.Sequential( ... )
  model.compile( ... )

# train model normally on a tf.data.Dataset
model.fit(training_dataset, epochs=EPOCHS, steps_per_epoch=...)

Neste snippet de código:

  • TPUClusterResolver().connect() encontra a TPU na rede. Ele funciona sem parâmetros na maioria dos sistemas do Google Cloud (jobs do AI Platform, Colaboratory, Kubeflow, VMs de aprendizado profundo criadas com o utilitário "ctpu up"). Esses sistemas sabem onde está a TPU graças a uma variável de ambiente TPU_NAME. Se você criar uma TPU manualmente, defina a variável de ambiente TPU_NAME na VM em que ela está sendo usada ou chame TPUClusterResolver com parâmetros explícitos: TPUClusterResolver(tp_uname, zone, project)
  • TPUStrategy é a parte que implementa a distribuição e o algoritmo de sincronização de gradiente "all-reduce".
  • A estratégia é aplicada por um escopo. O modelo precisa ser definido no escopo da estratégia().
  • A função tpu_model.fit espera um objeto tf.data.Dataset como entrada para treinamento de TPU.

Tarefas comuns de portabilidade de TPU

  • Embora haja muitas maneiras de carregar dados em um modelo do TensorFlow, para TPUs, é necessário usar a API tf.data.Dataset.
  • As TPUs são muito rápidas, e a ingestão de dados geralmente se torna o gargalo ao executar nelas. Há ferramentas que podem ser usadas para detectar gargalos de dados e outras dicas de desempenho no Guia de desempenho da TPU.
  • Números int8 ou int16 são tratados como int32. A TPU não tem hardware de números inteiros operando em menos de 32 bits.
  • Algumas operações do TensorFlow não são compatíveis. A lista está aqui. A boa notícia é que essa limitação se aplica apenas ao código de treinamento, ou seja, a passagem direta e indireta pelo modelo. Você ainda pode usar todas as operações do TensorFlow no pipeline de entrada de dados, já que ele será executado na CPU.
  • tf.py_func não é compatível com TPU.

4. Como carregar dados

c0ecb860e4cad0a9.jpeg cc4781a7739c49ae.jpeg 81236b00f8bbf39e.jpeg 961e2228974076bb.jpeg 7517dc163bdffcd5.jpeg 96392df4767f566d.png

Vamos trabalhar com um conjunto de dados de fotos de flores. O objetivo é aprender a categorizá-las em cinco tipos de flores. O carregamento de dados é feito usando a API tf.data.Dataset. Primeiro, vamos conhecer a API.

Prático

Abra o notebook a seguir, execute as células (Shift-ENTER) e siga as instruções sempre que vir o rótulo "TRABALHO NECESSÁRIO".

c3df49e90e5a654f.png Fun with tf.data.Dataset (playground).ipynb

Informações adicionais

Sobre o conjunto de dados "flores"

O conjunto de dados está organizado em cinco pastas. Cada pasta contém flores de um tipo. As pastas são chamadas de girassóis, margaridas, dentes-de-leão, tulipas e rosas. Os dados são hospedados em um bucket público no Google Cloud Storage. Trecho:

gs://flowers-public/sunflowers/5139971615_434ff8ed8b_n.jpg
gs://flowers-public/daisy/8094774544_35465c1c64.jpg
gs://flowers-public/sunflowers/9309473873_9d62b9082e.jpg
gs://flowers-public/dandelion/19551343954_83bb52f310_m.jpg
gs://flowers-public/dandelion/14199664556_188b37e51e.jpg
gs://flowers-public/tulips/4290566894_c7f061583d_m.jpg
gs://flowers-public/roses/3065719996_c16ecd5551.jpg
gs://flowers-public/dandelion/8168031302_6e36f39d87.jpg
gs://flowers-public/sunflowers/9564240106_0577e919da_n.jpg
gs://flowers-public/daisy/14167543177_cd36b54ac6_n.jpg

Por que tf.data.Dataset?

O Keras e o TensorFlow aceitam conjuntos de dados em todas as funções de treinamento e avaliação. Depois de carregar dados em um conjunto de dados, a API oferece todas as funcionalidades comuns úteis para dados de treinamento de rede neural:

dataset = ... # load something (see below)
dataset = dataset.shuffle(1000) # shuffle the dataset with a buffer of 1000
dataset = dataset.cache() # cache the dataset in RAM or on disk
dataset = dataset.repeat() # repeat the dataset indefinitely
dataset = dataset.batch(128) # batch data elements together in batches of 128
AUTOTUNE = tf.data.AUTOTUNE
dataset = dataset.prefetch(AUTOTUNE) # prefetch next batch(es) while training

Confira dicas de performance e práticas recomendadas para conjuntos de dados neste artigo. A documentação de referência está aqui.

Noções básicas do tf.data.Dataset

Os dados geralmente vêm em vários arquivos, aqui imagens. Para criar um conjunto de dados de nomes de arquivos, chame:

filenames_dataset = tf.data.Dataset.list_files('gs://flowers-public/*/*.jpg')
# The parameter is a "glob" pattern that supports the * and ? wildcards.

Em seguida, você "mapeia" uma função para cada nome de arquivo, que normalmente carrega e decodifica o arquivo em dados reais na memória:

def decode_jpeg(filename):
  bits = tf.io.read_file(filename)
  image = tf.io.decode_jpeg(bits)
  return image

image_dataset = filenames_dataset.map(decode_jpeg)
# this is now a dataset of decoded images (uint8 RGB format)

Para iterar em um conjunto de dados:

for data in my_dataset:
  print(data)

Conjuntos de dados de tuplas

No aprendizado supervisionado, um conjunto de dados de treinamento normalmente é composto por pares de dados de treinamento e respostas corretas. Para permitir isso, a função de decodificação pode retornar tuplas. Assim, você terá um conjunto de dados de tuplas, que serão retornadas quando você iterar nele. Os valores retornados são tensores do TensorFlow prontos para serem usados pelo seu modelo. É possível chamar .numpy() neles para conferir os valores brutos:

def decode_jpeg_and_label(filename):
  bits = tf.read_file(filename)
  image = tf.io.decode_jpeg(bits)
  label = ... # extract flower name from folder name
  return image, label

image_dataset = filenames_dataset.map(decode_jpeg_and_label)
# this is now a dataset of (image, label) pairs 

for image, label in dataset:
  print(image.numpy().shape, label.numpy())

Conclusão:carregar imagens uma por uma é lento!

Ao iterar nesse conjunto de dados, você vai perceber que é possível carregar algo como uma ou duas imagens por segundo. Isso é muito lento! Os aceleradores de hardware que vamos usar para treinamento podem manter muitas vezes essa taxa. Consulte a próxima seção para saber como vamos fazer isso.

Solução

Confira o notebook de solução. Use se tiver dificuldades.

c3df49e90e5a654f.png Fun with tf.data.Dataset (solution).ipynb

O que vimos

  • 🤔 tf.data.Dataset.list_files
  • 🤔 tf.data.Dataset.map
  • 🤔 Conjuntos de dados de tuplas
  • 😀 iterando por conjuntos de dados

Confira esta lista de verificação.

5. Carregar dados rapidamente

Os aceleradores de hardware da Unidade de Processamento de Tensor (TPU) que vamos usar neste laboratório são muito rápidos. O desafio é fornecer dados com rapidez suficiente para mantê-los ocupados. O Google Cloud Storage (GCS) é capaz de manter uma alta capacidade de processamento, mas, como em todos os sistemas de armazenamento em nuvem, iniciar uma conexão custa alguma troca de dados de rede. Portanto, não é ideal ter nossos dados armazenados como milhares de arquivos individuais. Vamos agrupar em lotes um número menor de arquivos e usar o poder do tf.data.Dataset para ler de vários arquivos em paralelo.

Read-through

O código que carrega arquivos de imagem, redimensiona para um tamanho comum e os armazena em 16 arquivos TFRecord está no notebook a seguir. Leia rapidamente. Não é necessário executar esse comando, já que dados formatados corretamente em TFRecord serão fornecidos para o restante do codelab.

c3df49e90e5a654f.png Flower pictures to TFRecords.ipynb

Layout de dados ideal para uma capacidade de processamento ideal do GCS

O formato de arquivo TFRecord

O formato de arquivo preferido do TensorFlow para armazenar dados é o TFRecord baseado em protobuf. Outros formatos de serialização também funcionam, mas é possível carregar um conjunto de dados diretamente de arquivos TFRecord escrevendo:

filenames = tf.io.gfile.glob(FILENAME_PATTERN)
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...) # do the TFRecord decoding here - see below

Para um desempenho ideal, recomendamos usar o código mais complexo a seguir para ler vários arquivos TFRecord de uma só vez. Esse código vai ler N arquivos em paralelo e ignorar a ordem dos dados em favor da velocidade de leitura.

AUTOTUNE = tf.data.AUTOTUNE
ignore_order = tf.data.Options()
ignore_order.experimental_deterministic = False

filenames = tf.io.gfile.glob(FILENAME_PATTERN)
dataset = tf.data.TFRecordDataset(filenames, num_parallel_reads=AUTOTUNE)
dataset = dataset.with_options(ignore_order)
dataset = dataset.map(...) # do the TFRecord decoding here - see below

Folha de referência do TFRecord

Três tipos de dados podem ser armazenados em TFRecords: strings de bytes (lista de bytes), inteiros de 64 bits e pontos flutuantes de 32 bits. Eles são sempre armazenados como listas. Um único elemento de dados será uma lista de tamanho 1. Você pode usar as seguintes funções auxiliares para armazenar dados em TFRecords.

gravar strings de bytes

# warning, the input is a list of byte strings, which are themselves lists of bytes
def _bytestring_feature(list_of_bytestrings):
  return tf.train.Feature(bytes_list=tf.train.BytesList(value=list_of_bytestrings))

escrever números inteiros

def _int_feature(list_of_ints): # int64
  return tf.train.Feature(int64_list=tf.train.Int64List(value=list_of_ints))

gravar números de ponto flutuante

def _float_feature(list_of_floats): # float32
  return tf.train.Feature(float_list=tf.train.FloatList(value=list_of_floats))

gravar um TFRecord usando os helpers acima

# input data in my_img_bytes, my_class, my_height, my_width, my_floats
with tf.python_io.TFRecordWriter(filename) as out_file:
  feature = {
    "image": _bytestring_feature([my_img_bytes]), # one image in the list
    "class": _int_feature([my_class]),            # one class in the list
    "size": _int_feature([my_height, my_width]),  # fixed length (2) list of ints
    "float_data": _float_feature(my_floats)       # variable length  list of floats
  }
  tf_record = tf.train.Example(features=tf.train.Features(feature=feature))
  out_file.write(tf_record.SerializeToString())

Para ler dados de TFRecords, primeiro declare o layout dos registros armazenados. Na declaração, é possível acessar qualquer campo nomeado como uma lista de comprimento fixo ou variável:

leitura de TFRecords

def read_tfrecord(data):
  features = {
    # tf.string = byte string (not text string)
    "image": tf.io.FixedLenFeature([], tf.string), # shape [] means scalar, here, a single byte string
    "class": tf.io.FixedLenFeature([], tf.int64),  # shape [] means scalar, i.e. a single item
    "size": tf.io.FixedLenFeature([2], tf.int64),  # two integers
    "float_data": tf.io.VarLenFeature(tf.float32)  # a variable number of floats
  }

  # decode the TFRecord
  tf_record = tf.io.parse_single_example(data, features)

  # FixedLenFeature fields are now ready to use
  sz = tf_record['size']

  # Typical code for decoding compressed images
  image = tf.io.decode_jpeg(tf_record['image'], channels=3)

  # VarLenFeature fields require additional sparse.to_dense decoding
  float_data = tf.sparse.to_dense(tf_record['float_data'])

  return image, sz, float_data

# decoding a tf.data.TFRecordDataset
dataset = dataset.map(read_tfrecord)
# now a dataset of triplets (image, sz, float_data)

Snippets de código úteis:

leitura de elementos de dados únicos

tf.io.FixedLenFeature([], tf.string)   # for one byte string
tf.io.FixedLenFeature([], tf.int64)    # for one int
tf.io.FixedLenFeature([], tf.float32)  # for one float

ler listas de elementos de tamanho fixo

tf.io.FixedLenFeature([N], tf.string)   # list of N byte strings
tf.io.FixedLenFeature([N], tf.int64)    # list of N ints
tf.io.FixedLenFeature([N], tf.float32)  # list of N floats

ler um número variável de itens de dados

tf.io.VarLenFeature(tf.string)   # list of byte strings
tf.io.VarLenFeature(tf.int64)    # list of ints
tf.io.VarLenFeature(tf.float32)  # list of floats

Um VarLenFeature retorna um vetor esparso, e uma etapa adicional é necessária após a decodificação do TFRecord:

dense_data = tf.sparse.to_dense(tf_record['my_var_len_feature'])

Também é possível ter campos opcionais em TFRecords. Se você especificar um valor padrão ao ler um campo, esse valor será retornado em vez de um erro se o campo estiver faltando.

tf.io.FixedLenFeature([], tf.int64, default_value=0) # this field is optional

O que vimos

  • 🤔 Fragmentar arquivos de dados para acesso rápido do GCS
  • 😓 como escrever TFRecords. (Já se esqueceu da sintaxe? Tudo bem, adicione esta página aos favoritos como uma folha de dicas)
  • 🤔 carregando um conjunto de dados de TFRecords usando TFRecordDataset

Confira esta lista de verificação.

6. [INFO] Classificador de rede neural 101

Em poucas palavras

Se você já conhece todos os termos em negrito no próximo parágrafo, passe para o próximo exercício. Se você está começando no aprendizado profundo, seja bem-vindo e continue lendo.

Para modelos criados como uma sequência de camadas, o Keras oferece a API Sequential. Por exemplo, um classificador de imagens usando três camadas densas pode ser escrito em Keras da seguinte maneira:

model = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=[192, 192, 3]),
    tf.keras.layers.Dense(500, activation="relu"),
    tf.keras.layers.Dense(50, activation="relu"),
    tf.keras.layers.Dense(5, activation='softmax') # classifying into 5 classes
])

# this configures the training of the model. Keras calls it "compiling" the model.
model.compile(
  optimizer='adam',
  loss= 'categorical_crossentropy',
  metrics=['accuracy']) # % of correct answers

# train the model
model.fit(dataset, ... )

688858c21e3beff2.png

Rede neural densa

Essa é a rede neural mais simples para classificar imagens. Ele é feito de "neurônios" organizados em camadas. A primeira camada processa os dados de entrada e alimenta as saídas em outras camadas. Ela é chamada de "densa" porque cada neurônio está conectado a todos os neurônios da camada anterior.

c21bae6dade487bc.png

É possível inserir uma imagem em uma rede desse tipo ao achatar os valores RGB de todos os pixels em um vetor longo e usá-lo como entrada. Não é a melhor técnica para reconhecimento de imagens, mas vamos melhorar isso depois.

Neurônios, ativações, RELU

Um "neurônio" calcula uma soma ponderada de todas as entradas, adiciona um valor chamado "tendência" e transmite o resultado por uma "função de ativação". Os pesos e o viés são desconhecidos no início. Eles são inicializados aleatoriamente e "aprendidos" com o treinamento da rede neural em muitos dados conhecidos.

644f4213a4ee70e5.png

A função de ativação mais popular é chamada de RELU, de Unidade Linear Retificada. É uma função muito simples, como você pode ver no gráfico acima.

Ativação do Softmax

A rede acima termina com uma camada de cinco neurônios porque estamos classificando flores em cinco categorias (rosa, tulipa, dente-de-leão, margarida e girassol). Os neurônios nas camadas intermediárias são ativados usando a função de ativação RELU clássica. Na última camada, queremos calcular números entre 0 e 1 que representam a probabilidade de a flor ser uma rosa, uma tulipa e assim por diante. Para isso, vamos usar uma função de ativação chamada "softmax".

Para aplicar o softmax em um vetor, pegue o exponencial de cada elemento e normalize o vetor, geralmente usando a norma L1 (soma dos valores absolutos) para que os valores somem 1 e possam ser interpretados como probabilidades.

ef0d98c0952c262d.png d51252f75894479e.gif

Perda de entropia cruzada

Agora que nossa rede neural gera previsões com base em imagens de entrada, precisamos medir a qualidade delas, ou seja, a distância entre o que a rede nos diz e as respostas corretas, geralmente chamadas de "rótulos". Lembre-se de que temos rótulos corretos para todas as imagens no conjunto de dados.

Qualquer distância funcionaria, mas para problemas de classificação, a chamada "distância de entropia cruzada" é a mais eficaz. Vamos chamar isso de função de erro ou "perda":

7bdf8753d20617fb.png

Gradiente descendente

"Treinar" a rede neural significa usar imagens e rótulos de treinamento para ajustar pesos e vieses e minimizar a função de perda de entropia cruzada. Confira como funciona.

A entropia cruzada é uma função de pesos, vieses, pixels da imagem de treinamento e da classe conhecida.

Se calcularmos as derivadas parciais da entropia cruzada em relação a todos os pesos e vieses, vamos obter um "gradiente", calculado para uma determinada imagem, rótulo e valor atual de pesos e vieses. Lembre-se de que podemos ter milhões de pesos e vieses, então calcular o gradiente parece muito trabalhoso. Felizmente, o TensorFlow faz isso por nós. A propriedade matemática de um gradiente é que ele aponta para cima. Como queremos ir para onde a entropia cruzada é baixa, vamos na direção oposta. Atualizamos os pesos e os vieses por uma fração do gradiente. Em seguida, fazemos a mesma coisa repetidamente usando os próximos lotes de imagens e rótulos de treinamento, em um loop de treinamento. Esperamos que isso convirja para um lugar em que a entropia cruzada seja mínima, embora nada garanta que esse mínimo seja único.

gradient descent2.png

Minilote e momentum

Você pode calcular o gradiente em apenas uma imagem de exemplo e atualizar os pesos e os vieses imediatamente, mas fazer isso em um lote de, por exemplo, 128 imagens, gera um gradiente que representa melhor as restrições impostas por diferentes imagens de exemplo e, portanto, provavelmente converge para a solução mais rapidamente. O tamanho do minilote é um parâmetro ajustável.

Essa técnica, às vezes chamada de "gradiente descendente estocástico", tem outro benefício mais pragmático: trabalhar com lotes também significa trabalhar com matrizes maiores, que geralmente são mais fáceis de otimizar em GPUs e TPUs.

No entanto, a convergência ainda pode ser um pouco caótica e até parar se o vetor de gradiente for todo zero. Isso significa que encontramos um mínimo? Nem sempre. Um componente de gradiente pode ser zero em um mínimo ou um máximo. Com um vetor de gradiente com milhões de elementos, se todos forem zeros, a probabilidade de que cada zero corresponda a um mínimo e nenhum deles a um ponto máximo é muito pequena. Em um espaço de muitas dimensões, os pontos de sela são bastante comuns, e não queremos parar neles.

52e824fe4716c4a0.png

Ilustração: um ponto de sela. O gradiente é 0, mas não é um mínimo em todas as direções. (Atribuição da imagem: Wikimedia: Por Nicoguaro - Trabalho próprio, CC BY 3.0)

A solução é adicionar um pouco de impulso ao algoritmo de otimização para que ele possa passar pelos pontos de sela sem parar.

Glossário

Lote ou minilote: o treinamento é sempre realizado em lotes de dados e rótulos de treinamento. Isso ajuda o algoritmo a convergir. A dimensão "lote" geralmente é a primeira dimensão dos tensores de dados. Por exemplo, um tensor de forma [100, 192, 192, 3] contém 100 imagens de 192 x 192 pixels com três valores por pixel (RGB).

Perda de entropia cruzada: uma função de perda especial usada com frequência em classificadores.

Camada densa: uma camada de neurônios em que cada neurônio está conectado a todos os neurônios da camada anterior.

Atributos: as entradas de uma rede neural às vezes são chamadas de "atributos". A arte de descobrir quais partes de um conjunto de dados (ou combinações de partes) alimentar em uma rede neural para receber boas previsões é chamada de "engenharia de atributos".

rótulos: outro nome para "classes" ou respostas corretas em um problema de classificação supervisionada

Taxa de aprendizado: fração do gradiente pela qual os pesos e os vieses são atualizados em cada iteração do loop de treinamento.

Logits: as saídas de uma camada de neurônios antes da aplicação da função de ativação são chamadas de "logits". O termo vem da "função logística", também conhecida como "função sigmoide", que costumava ser a função de ativação mais usada. "Saídas de neurônios antes da função logística" foi abreviado para "logits".

Perda: a função de erro que compara as saídas da rede neural com as respostas corretas.

Neurônio: calcula a soma ponderada das entradas, adiciona um viés e transmite o resultado por uma função de ativação.

Codificação one-hot: a classe 3 de 5 é codificada como um vetor de 5 elementos, todos zeros, exceto o terceiro, que é 1.

relu: unidade linear retificada. Uma função de ativação comum para neurônios.

sigmoid: outra função de ativação que era popular e ainda é útil em casos especiais.

softmax: uma função de ativação especial que atua em um vetor, aumenta a diferença entre o maior componente e todos os outros e também normaliza o vetor para ter uma soma de 1, para que possa ser interpretado como um vetor de probabilidades. Usado como a última etapa nos classificadores.

tensor: um "tensor" é como uma matriz, mas com um número arbitrário de dimensões. Um tensor unidimensional é um vetor. Um tensor bidimensional é uma matriz. E você pode ter tensores com 3, 4, 5 ou mais dimensões.

7. Aprendizado por transferência

Para um problema de classificação de imagens, as camadas densas provavelmente não serão suficientes. Precisamos aprender sobre camadas convolucionais e as várias maneiras de organizá-las.

Mas também podemos pegar um atalho! Há redes neurais convolucionais totalmente treinadas disponíveis para download. É possível remover a última camada, o cabeçalho de classificação softmax, e substituir por uma opção própria. Todos os pesos e vieses treinados permanecem como estão. Você só treina novamente a camada softmax adicionada. Essa técnica é chamada de aprendizado por transferência e funciona desde que o conjunto de dados em que a rede neural é pré-treinada seja "próximo o suficiente" do seu.

Prático

Abra o notebook a seguir, execute as células (Shift-ENTER) e siga as instruções sempre que vir o rótulo "TRABALHO NECESSÁRIO".

c3df49e90e5a654f.png Keras Flowers transfer learning (playground).ipynb

Informações adicionais

Com o aprendizado por transferência, você se beneficia das arquiteturas avançadas de redes neurais convolucionais desenvolvidas pelos melhores pesquisadores e do pré-treinamento em um enorme conjunto de dados de imagens. No nosso caso, vamos usar o aprendizado por transferência de uma rede treinada no ImageNet, um banco de dados de imagens com muitas plantas e cenas externas, que é bem parecido com flores.

b8fc1efd2001f072.png

Ilustração: usando uma rede neural convolucional complexa, já treinada, como uma caixa preta, retreinando apenas o cabeçalho de classificação. Isso é o aprendizado por transferência. Vamos ver como esses arranjos complicados de camadas convolucionais funcionam mais tarde. Por enquanto, é problema de outra pessoa.

Aprendizado por transferência no Keras

No Keras, é possível instanciar um modelo pré-treinado da coleção tf.keras.applications.*. A MobileNet V2, por exemplo, é uma arquitetura convolucional muito boa que permanece razoável em tamanho. Ao selecionar include_top=False, você recebe o modelo pré-treinado sem a camada softmax final para que possa adicionar a sua:

pretrained_model = tf.keras.applications.MobileNetV2(input_shape=[*IMAGE_SIZE, 3], include_top=False)
pretrained_model.trainable = False

model = tf.keras.Sequential([
    pretrained_model,
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(5, activation='softmax')
])

Observe também a configuração pretrained_model.trainable = False. Ele congela os pesos e os vieses do modelo pré-treinado para que você treine apenas a camada softmax. Isso geralmente envolve relativamente poucos pesos e pode ser feito rapidamente sem a necessidade de um conjunto de dados muito grande. No entanto, se você tiver muitos dados, o aprendizado por transferência poderá funcionar ainda melhor com pretrained_model.trainable = True. Os pesos pré-treinados fornecem excelentes valores iniciais e ainda podem ser ajustados pelo treinamento para se adequar melhor ao seu problema.

Por fim, observe a camada Flatten() inserida antes da camada softmax densa. As camadas densas funcionam em vetores de dados simples, mas não sabemos se é isso que o modelo pré-treinado retorna. Por isso, precisamos achatar. No próximo capítulo, ao analisarmos as arquiteturas convolucionais, vamos explicar o formato de dados retornado pelas camadas convolucionais.

Com essa abordagem, você deve chegar perto de 75% de acurácia.

Solução

Confira o notebook de solução. Use se tiver dificuldades.

c3df49e90e5a654f.png Keras Flowers transfer learning (solution).ipynb

O que vimos

  • 🤔 Como escrever um classificador no Keras
  • 🤓 configurado com uma última camada softmax e perda de entropia cruzada
  • 😈 Aprendizado por transferência
  • 🤔 Treinar seu primeiro modelo
  • 🧐 Acompanhar a perda e a acurácia durante o treinamento

Confira esta lista de verificação.

8. [INFO] Redes neurais convolucionais

Em poucas palavras

Se você já conhece todos os termos em negrito no próximo parágrafo, passe para o próximo exercício. Se você está começando a usar redes neurais convolucionais, continue lendo.

convolutional.gif

Ilustração: filtragem de uma imagem com dois filtros sucessivos feitos de 4x4x3=48 pesos aprendíveis cada.

Esta é a aparência de uma rede neural convolucional simples no Keras:

model = tf.keras.Sequential([
  # input: images of size 192x192x3 pixels (the three stands for RGB channels)
  tf.keras.layers.Conv2D(kernel_size=3, filters=24, padding='same', activation='relu', input_shape=[192, 192, 3]),
  tf.keras.layers.Conv2D(kernel_size=3, filters=24, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=12, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=6, padding='same', activation='relu'),
  tf.keras.layers.Flatten(),
  # classifying into 5 categories
  tf.keras.layers.Dense(5, activation='softmax')
])

model.compile(
  optimizer='adam',
  loss= 'categorical_crossentropy',
  metrics=['accuracy'])

688858c21e3beff2.png

Introdução às redes neurais convolucionais

Em uma camada de uma rede convolucional, um "neurônio" faz uma soma ponderada dos pixels logo acima dele, em uma pequena região da imagem. Ele adiciona um viés e transmite a soma por uma função de ativação, assim como um neurônio em uma camada densa regular. Essa operação é repetida em toda a imagem usando os mesmos pesos. Nas camadas densas, cada neurônio tem os próprios pesos. Aqui, um único "patch" de pesos desliza pela imagem nas duas direções (uma "convolução"). A saída tem tantos valores quanto pixels na imagem (embora seja necessário algum padding nas bordas). É uma operação de filtragem que usa um filtro de 4x4x3=48 pesos.

No entanto, 48 ponderações não serão suficientes. Para adicionar mais graus de liberdade, repetimos a mesma operação com um novo conjunto de pesos. Isso gera um novo conjunto de saídas de filtro. Vamos chamar isso de "canal" de saídas por analogia com os canais R, G e B na imagem de entrada.

Screen Shot 2016-07-29 at 16.02.37.png

Os dois (ou mais) conjuntos de pesos podem ser somados como um tensor adicionando uma nova dimensão. Isso nos dá a forma genérica do tensor de pesos para uma camada convolucional. Como o número de canais de entrada e saída são parâmetros, podemos começar a empilhar e encadear camadas convolucionais.

d1b557707bcd1cb9.png

Ilustração: uma rede neural convolucional transforma "cubos" de dados em outros "cubos" de dados.

Convoluções com stride, max pooling

Ao realizar as convoluções com uma passada de 2 ou 3, também podemos reduzir o cubo de dados resultante nas dimensões horizontais. Há duas maneiras comuns de fazer isso:

  • Convolução com strides: um filtro deslizante como acima, mas com uma stride > 1
  • Agrupamento máximo: uma janela deslizante que aplica a operação MAX (normalmente em patches de 2x2, repetidos a cada 2 pixels)

2b2d4263bb8470b.gif

Ilustração: deslizar a janela de computação em 3 pixels resulta em menos valores de saída. As convoluções com strides ou o max pooling (máximo em uma janela 2x2 com stride de 2) são uma maneira de reduzir o cubo de dados nas dimensões horizontais.

Classificador convolucional

Por fim, anexamos um cabeçalho de classificação ao achatar o último cubo de dados e o transmitir por uma camada densa ativada por softmax. Um classificador convolucional típico pode ter esta aparência:

4a61aaffb6cba3d1.png

Ilustração: um classificador de imagens usando camadas convolucionais e softmax. Ele usa filtros 3x3 e 1x1. As camadas maxpool usam o máximo de grupos de pontos de dados 2x2. O cabeçalho de classificação é implementado com uma camada densa com ativação softmax.

No Keras

A pilha convolucional ilustrada acima pode ser escrita em Keras assim:

model = tf.keras.Sequential([
  # input: images of size 192x192x3 pixels (the three stands for RGB channels)    
  tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu', input_shape=[192, 192, 3]),
  tf.keras.layers.Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu'),
  tf.keras.layers.Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu'),
  tf.keras.layers.Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu'),
  tf.keras.layers.Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
  tf.keras.layers.MaxPooling2D(pool_size=2),
  tf.keras.layers.Conv2D(kernel_size=3, filters=16, padding='same', activation='relu'),
  tf.keras.layers.Conv2D(kernel_size=1, filters=8, padding='same', activation='relu'),
  tf.keras.layers.Flatten(),
  # classifying into 5 categories
  tf.keras.layers.Dense(5, activation='softmax')
])

model.compile(
  optimizer='adam',
  loss= 'categorical_crossentropy',
  metrics=['accuracy'])

9. Sua rede neural convolucional personalizada

Prático

Vamos criar e treinar uma rede neural convolucional do zero. Usar uma TPU permite iterar muito rápido. Abra o notebook a seguir, execute as células (Shift-ENTER) e siga as instruções sempre que vir o rótulo "TRABALHO NECESSÁRIO".

c3df49e90e5a654f.png Keras_Flowers_TPU (playground).ipynb

O objetivo é superar a acurácia de 75% do modelo de aprendizado por transferência. Esse modelo teve uma vantagem, já que foi pré-treinado em um conjunto de dados de milhões de imagens, enquanto temos apenas 3.670 imagens aqui. Você pode pelo menos igualar?

Informações adicionais

Quantas camadas e qual o tamanho?

Selecionar tamanhos de camada é mais uma arte do que uma ciência. É preciso encontrar o equilíbrio certo entre ter poucos e muitos parâmetros (pesos e vieses). Com poucos pesos, a rede neural não consegue representar a complexidade das formas das flores. Com muitos dados, o modelo pode sofrer overfitting, ou seja, se especializar nas imagens de treinamento e não conseguir generalizar. Com muitos parâmetros, o treinamento do modelo também será lento. No Keras, a função model.summary() mostra a estrutura e a contagem de parâmetros do modelo:

Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 192, 192, 16)      448       
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 192, 192, 30)      4350      
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 96, 96, 30)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 96, 96, 60)        16260     
_________________________________________________________________
 ... 
_________________________________________________________________
global_average_pooling2d (Gl (None, 130)               0         
_________________________________________________________________
dense (Dense)                (None, 90)                11790     
_________________________________________________________________
dense_1 (Dense)              (None, 5)                 455       
=================================================================
Total params: 300,033
Trainable params: 300,033
Non-trainable params: 0
_________________________________________________________________

Algumas dicas:

  • Ter várias camadas é o que torna as redes neurais "profundas" eficazes. Para esse problema simples de reconhecimento de flores, de 5 a 10 camadas fazem sentido.
  • Use filtros pequenos. Normalmente, os filtros 3x3 são bons em todos os lugares.
  • Filtros 1x1 também podem ser usados e são baratos. Eles não "filtram" nada, mas calculam combinações lineares de canais. Alterne com filtros reais. Saiba mais sobre "convoluções 1x1" na próxima seção.
  • Para um problema de classificação como esse, faça o downsampling com frequência usando camadas de max-pooling (ou convoluções com stride >1). Você não se importa com a localização da flor, apenas se ela é uma rosa ou um dente-de-leão. Portanto, perder informações x e y não é importante, e filtrar áreas menores é mais barato.
  • O número de filtros geralmente se torna semelhante ao número de classes no final da rede. Por quê? Consulte o truque de "agrupamento médio global" abaixo. Se você classificar em centenas de classes, aumente a contagem de filtros progressivamente em camadas consecutivas. Para o conjunto de dados de flores com cinco classes, filtrar com apenas cinco filtros não seria suficiente. É possível usar a mesma contagem de filtros na maioria das camadas, por exemplo, 32, e diminuir no final.
  • As camadas densas finais são caras. Ele/ela/eles pode ter mais ponderações do que todas as camadas convolucionais combinadas. Por exemplo, mesmo com uma saída muito razoável do último cubo de dados de 24x24x10 pontos de dados, uma camada densa de 100 neurônios custaria 24x24x10x100=576.000 pesos. Tente ser ponderado ou use o agrupamento médio global (veja abaixo).

Agrupamento médio global

Em vez de usar uma camada densa cara no final de uma rede neural convolucional, você pode dividir o "cubo" de dados recebidos em quantas partes forem necessárias, calcular a média dos valores e transmitir esses dados por uma função de ativação softmax. Essa forma de criar o cabeçalho de classificação não tem custo de ponderação. No Keras, a sintaxe é tf.keras.layers.GlobalAveragePooling2D().

93240029f59df7c2.png

Solução

Confira o notebook de solução. Use se tiver dificuldades.

c3df49e90e5a654f.png Keras_Flowers_TPU (solution).ipynb

O que vimos

  • 🤔 Brincou com camadas convolucionais
  • 🤓 Testei o max pooling, strides, global average pooling etc.
  • 😀 iterou um modelo do mundo real rapidamente em uma TPU

Confira esta lista de verificação.

10. [INFO] Arquiteturas convolucionais modernas

Em poucas palavras

7968830b57b708c0.png

Ilustração: um "módulo" convolucional. O que é melhor neste momento? Uma camada de max-pooling seguida por uma camada convolucional 1x1 ou uma combinação diferente de camadas? Teste todas, concatene os resultados e deixe a rede decidir. À direita: a arquitetura convolucional " inception" usando esses módulos.

No Keras, para criar modelos em que o fluxo de dados pode ramificar para dentro e para fora, é necessário usar o estilo de modelo "funcional". Confira um exemplo:

l = tf.keras.layers # syntax shortcut

y = l.Conv2D(filters=32, kernel_size=3, padding='same',
             activation='relu', input_shape=[192, 192, 3])(x) # x=input image

# module start: branch out
y1 = l.Conv2D(filters=32, kernel_size=1, padding='same', activation='relu')(y)
y3 = l.Conv2D(filters=32, kernel_size=3, padding='same', activation='relu')(y)
y = l.concatenate([y1, y3]) # output now has 64 channels
# module end: concatenation

# many more layers ...

# Create the model by specifying the input and output tensors.
# Keras layers track their connections automatically so that's all that's needed.
z = l.Dense(5, activation='softmax')(y)
model = tf.keras.Model(x, z)

688858c21e3beff2.png

Outros truques baratos

Filtros pequenos de 3x3

40a7b15fb7dbe75c.png

Nesta ilustração, você vê o resultado de dois filtros 3x3 consecutivos. Tente rastrear quais pontos de dados contribuíram para o resultado: esses dois filtros 3x3 consecutivos calculam alguma combinação de uma região 5x5. Não é exatamente a mesma combinação que um filtro 5x5 calcularia, mas vale a pena tentar porque dois filtros 3x3 consecutivos são mais baratos do que um único filtro 5x5.

Convoluções 1x1?

fd7cac16f8ecb423.png

Em termos matemáticos, uma convolução "1x1" é uma multiplicação por uma constante, um conceito não muito útil. No entanto, em redes neurais convolucionais, lembre-se de que o filtro é aplicado a um cubo de dados, não apenas a uma imagem 2D. Portanto, um filtro "1x1" calcula uma soma ponderada de uma coluna de dados 1x1 (veja a ilustração). Ao deslizar o filtro pelos dados, você vai obter uma combinação linear dos canais da entrada. Isso é útil. Se você pensar nos canais como os resultados de operações de filtragem individuais, por exemplo, um filtro para "orelhas pontudas", outro para "bigodes" e um terceiro para "olhos amendoados", uma camada convolucional "1x1" vai calcular várias combinações lineares possíveis desses recursos, o que pode ser útil ao procurar um "gato". Além disso, as camadas 1x1 usam menos ponderações.

11. Squeezenet

Uma maneira simples de juntar essas ideias foi mostrada no artigo"Squeezenet". Os autores sugerem um design de módulo convolucional muito simples, usando apenas camadas convolucionais 1x1 e 3x3.

1730ac375379269b.png

Ilustração: arquitetura SqueezeNet baseada em "módulos de incêndio". Elas alternam uma camada 1x1 que "comprime" os dados recebidos na dimensão vertical, seguida por duas camadas convolucionais paralelas 1x1 e 3x3 que "expandem" novamente a profundidade dos dados.

Prático

Continue no notebook anterior e crie uma rede neural convolucional inspirada na SqueezeNet. Você terá que mudar o código do modelo para o "estilo funcional" do Keras.

c3df49e90e5a654f.png Keras_Flowers_TPU (playground).ipynb

Mais informações

É útil para este exercício definir uma função auxiliar para um módulo squeezenet:

def fire(x, squeeze, expand):
  y = l.Conv2D(filters=squeeze, kernel_size=1, padding='same', activation='relu')(x)
  y1 = l.Conv2D(filters=expand//2, kernel_size=1, padding='same', activation='relu')(y)
  y3 = l.Conv2D(filters=expand//2, kernel_size=3, padding='same', activation='relu')(y)
  return tf.keras.layers.concatenate([y1, y3])

# this is to make it behave similarly to other Keras layers
def fire_module(squeeze, expand):
  return lambda x: fire(x, squeeze, expand)

# usage:
x = l.Input(shape=[192, 192, 3])
y = fire_module(squeeze=24, expand=48)(x) # typically, squeeze is less than expand
y = fire_module(squeeze=32, expand=64)(y)
...
model = tf.keras.Model(x, y)

O objetivo agora é atingir 80% de acurácia.

O que tentar

Comece com uma única camada convolucional e siga com "fire_modules", alternando com camadas MaxPooling2D(pool_size=2). Você pode testar de 2 a 4 camadas de max pooling na rede e também 1, 2 ou 3 módulos de ativação consecutivos entre as camadas de max pooling.

Em módulos de incêndio, o parâmetro "squeeze" geralmente é menor que o parâmetro "expand". Esses parâmetros são números de filtros. Normalmente, eles variam de 8 a 196. Você pode testar arquiteturas em que o número de filtros aumenta gradualmente na rede ou arquiteturas simples em que todos os módulos de disparo têm o mesmo número de filtros.

Confira um exemplo:

x = tf.keras.layers.Input(shape=[*IMAGE_SIZE, 3]) # input is 192x192 pixels RGB

y = tf.keras.layers.Conv2D(kernel_size=3, filters=32, padding='same', activation='relu')(x)
y = fire_module(24, 48)(y)
y = tf.keras.layers.MaxPooling2D(pool_size=2)(y)
y = fire_module(24, 48)(y)
y = tf.keras.layers.MaxPooling2D(pool_size=2)(y)
y = fire_module(24, 48)(y)
y = tf.keras.layers.GlobalAveragePooling2D()(y)
y = tf.keras.layers.Dense(5, activation='softmax')(y)

model = tf.keras.Model(x, y)

Nesse momento, você pode notar que seus experimentos não estão indo tão bem e que o objetivo de 80% de acurácia parece distante. Hora de mais alguns truques baratos.

Normalização em lote

A normalização em lote vai ajudar com os problemas de convergência que você está enfrentando. No próximo workshop, vamos explicar essa técnica em detalhes. Por enquanto, use-a como uma caixa preta "mágica" adicionando esta linha após cada camada convolucional na sua rede, incluindo as camadas dentro da função fire_module:

y = tf.keras.layers.BatchNormalization(momentum=0.9)(y)
# please adapt the input and output "y"s to whatever is appropriate in your context

O parâmetro de momentum precisa ser reduzido do valor padrão de 0,99 para 0,9 porque nosso conjunto de dados é pequeno. Não se preocupe com esse detalhe por enquanto.

Ampliação de dados

Você vai ganhar mais alguns pontos percentuais ao aumentar os dados com transformações fáceis, como inversões da esquerda para a direita e mudanças de saturação:

4ed2958e09b487ca.png

ad795b70334e0d6b.png

Isso é muito fácil de fazer no TensorFlow com a API tf.data.Dataset. Defina uma nova função de transformação para seus dados:

def data_augment(image, label):
    image = tf.image.random_flip_left_right(image)
    image = tf.image.random_saturation(image, lower=0, upper=2)
    return image, label

Em seguida, use-o na transformação final de dados (célula "Conjuntos de dados de treinamento e validação", função "get_batched_dataset"):

dataset = dataset.repeat() # existing line
# insert this
if augment_data:
  dataset = dataset.map(data_augment, num_parallel_calls=AUTO)
dataset = dataset.shuffle(2048) # existing line

Não se esqueça de tornar o aumento de dados opcional e adicionar o código necessário para garantir que apenas o conjunto de dados de treinamento seja aumentado. Não faz sentido aumentar o conjunto de dados de validação.

Agora, uma acurácia de 80% em 35 épocas está ao seu alcance.

Solução

Confira o notebook de solução. Use se tiver dificuldades.

c3df49e90e5a654f.png Keras_Flowers_TPU_squeezenet.ipynb

O que vimos

  • 🤔 Modelos de "estilo funcional" do Keras
  • 🤓 Arquitetura SqueezeNet
  • 🤓 Aumento de dados com tf.data.datset

Confira esta lista de verificação.

12. Xception ajustado

Convoluções separáveis

Uma maneira diferente de implementar camadas convolucionais tem ganhado popularidade recentemente: convoluções separáveis por profundidade. Sei que é um nome complicado, mas o conceito é bem simples. Eles são implementados no TensorFlow e no Keras como tf.keras.layers.SeparableConv2D.

Uma convolução separável também executa um filtro na imagem, mas usa um conjunto distinto de pesos para cada canal da imagem de entrada. Em seguida, há uma "convolução 1x1", uma série de produtos escalares que resultam em uma soma ponderada dos canais filtrados. Com novos pesos a cada vez, são calculadas quantas recombinações ponderadas dos canais forem necessárias.

615720b803bf8dda.gif

Ilustração: convoluções separáveis. Fase 1: convoluções com um filtro separado para cada canal. Fase 2: recombinações lineares de canais. Repetido com um novo conjunto de ponderações até que o número desejado de canais de saída seja alcançado. A Fase 1 também pode ser repetida, com novos pesos a cada vez, mas na prática isso raramente acontece.

As convoluções separáveis são usadas nas arquiteturas de redes convolucionais mais recentes: MobileNetV2, Xception e EfficientNet. Aliás, o MobileNetV2 é o que você usou para o aprendizado por transferência antes.

Elas são mais baratas do que as convoluções regulares e se mostraram tão eficazes na prática. Confira a contagem de peso do exemplo ilustrado acima:

Camada convolucional: 4 x 4 x 3 x 5 = 240

Camada convolucional separável: 4 x 4 x 3 + 3 x 5 = 48 + 15 = 63

Fica como exercício para o leitor calcular o número de multiplicações necessárias para aplicar cada estilo de camadas convolucionais de maneira semelhante. As convoluções separáveis são menores e muito mais eficazes em termos computacionais.

Prático

Reinicie o notebook do playground "aprendizado por transferência", mas desta vez selecione Xception como o modelo pré-treinado. A Xception usa apenas convoluções separáveis. Deixe todas as ponderações treináveis. Vamos ajustar os pesos pré-treinados nos nossos dados em vez de usar as camadas pré-treinadas como tal.

c3df49e90e5a654f.png Keras Flowers transfer learning (playground).ipynb

Meta: acurácia > 95% (sim, é possível!)

Como este é o exercício final, ele exige um pouco mais de código e trabalho de ciência de dados.

Mais informações sobre o ajuste

O Xception está disponível nos modelos pré-treinados padrão em tf.keras.application.* Não se esqueça de deixar todos os pesos treináveis desta vez.

pretrained_model = tf.keras.applications.Xception(input_shape=[*IMAGE_SIZE, 3],
                                                  include_top=False)
pretrained_model.trainable = True

Para conseguir bons resultados ao ajustar um modelo, é preciso prestar atenção à taxa de aprendizado e usar uma programação com um período de otimização. Assim:

9b1af213b2b36d47.png

Começar com uma taxa de aprendizado padrão interromperia os pesos pré-treinados do modelo. O início progressivo preserva esses valores até que o modelo se ajuste aos seus dados e possa modificá-los de maneira adequada. Depois da aceleração, você pode continuar com uma taxa de aprendizado constante ou exponencialmente decrescente.

No Keras, a taxa de aprendizado é especificada por um callback em que você pode calcular a taxa de aprendizado adequada para cada época. O Keras vai transmitir a taxa de aprendizado correta ao otimizador em cada época.

def lr_fn(epoch):
  lr = ...
  return lr

lr_callback = tf.keras.callbacks.LearningRateScheduler(lr_fn, verbose=True)

model.fit(..., callbacks=[lr_callback])

Solução

Confira o notebook de solução. Use se tiver dificuldades.

c3df49e90e5a654f.png 07_Keras_Flowers_TPU_xception_fine_tuned_best.ipynb

O que vimos

  • 🤔 Convolução separável por profundidade
  • 🤓 Programações de taxa de aprendizado
  • 😈 Ajustar um modelo pré-treinado.

Confira esta lista de verificação.

13. Parabéns!

Você criou sua primeira rede neural convolucional moderna e a treinou para ter mais de 90% de acurácia, repetindo execuções de treinamento sucessivas em apenas alguns minutos graças às TPUs.

TPUs na prática

As TPUs e GPUs estão disponíveis na Vertex AI do Google Cloud:

Por fim, adoramos receber feedback. Informe se você notar algo errado neste laboratório ou se achar que ele pode ser melhorado. O feedback pode ser enviado usando os problemas do GitHub [ link de feedback].

HR.png

Martin Görner ID small.jpg
O autor: Martin Görner
Twitter: @martin_gorner