Modelos pré-treinados e transferência de aprendizagem
- 10 minutos
Treinar CNNs pode demorar uma quantidade considerável de tempo e é necessária uma grande quantidade de dados para essa tarefa. Grande parte do tempo é gasto a experimentar para encontrar os melhores filtros de baixo nível que uma rede precisa para extrair padrões das imagens. Surge uma questão natural – podemos usar uma rede neural treinada com um único conjunto de dados e adaptá-la para classificar diferentes imagens sem um processo de treino completo?
Esta abordagem chama-se aprendizagem por transferência, porque transferimos algum conhecimento de um modelo de rede neural para outro. Na aprendizagem por transferência, normalmente começamos com um modelo pré-treinado, que foi treinado num grande conjunto de dados de imagens, como o ImageNet. Esses modelos já fazem um bom trabalho a extrair diferentes características de imagens genéricas e, em muitos casos, simplesmente construir um classificador por cima dessas características extraídas pode dar um bom resultado.
import tensorflow as tf
import keras
import matplotlib.pyplot as plt
import numpy as np
import os
import glob
from PIL import Image
Conjunto de dados gatos vs. cães
Nesta unidade, vamos resolver um problema real de classificar imagens de gatos e cães. Por esta razão, vamos usar o Kaggle Cats vs. Dogs Dataset, que também pode ser descarregado da Microsoft.
Vamos descarregar este conjunto de dados e extraí-lo para o data diretório:
import urllib.request
import zipfile
dataset_url = 'https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_5340.zip'
data_dir = 'data'
os.makedirs(data_dir, exist_ok=True)
zip_path = os.path.join(data_dir, 'kagglecatsanddogs_5340.zip')
if not os.path.exists(zip_path):
urllib.request.urlretrieve(dataset_url, zip_path)
if not os.path.exists(os.path.join(data_dir, 'PetImages')):
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(data_dir)
O conjunto de dados pode conter alguns ficheiros de imagem corrompidos. Vamos definir uma função auxiliar para verificar e remover antes de carregar:
def check_image(fn):
try:
im = Image.open(fn)
im.verify()
return True
except (IOError, SyntaxError):
return False
def check_image_dir(dir_path):
for fn in glob.glob(dir_path):
if not check_image(fn):
print(f"Corrupt image: {fn}")
os.remove(fn)
# Remove any corrupt images from the dataset
check_image_dir('data/PetImages/Cat/*.jpg')
check_image_dir('data/PetImages/Dog/*.jpg')
Carregamento do conjunto de dados
Nos exemplos anteriores, estávamos a carregar conjuntos de dados incorporados no Keras. Agora vamos usar o nosso próprio conjunto de dados, que precisamos de carregar a partir de um diretório de imagens. O Keras inclui uma função image_dataset_from_directory auxiliar que pode criar a tf.data.Dataset partir de um diretório de imagens organizadas em subdiretórios por classe.
data_dir = 'data/PetImages'
batch_size = 32
ds_train = keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset='training',
seed=13,
image_size=(224, 224),
batch_size=batch_size
)
ds_test = keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset='validation',
seed=13,
image_size=(224, 224),
batch_size=batch_size
)
Observação
Usamos o mesmo seed valor ao criar as divisões de treino e validação para garantir que não há sobreposição entre os dois subconjuntos.
Podemos verificar os nomes das classes que foram automaticamente inferidos a partir da estrutura de diretórios:
# Expected output: ['Cat', 'Dog']
ds_train.class_names
Vamos definir um ajudante para visualizar amostras do nosso conjunto de dados (esta é uma nova versão do display_dataset adaptada para dados em lote):
def display_dataset(images, labels, classes=None, cols=8):
n = len(images)
rows = (n + cols - 1) // cols
fig, axes = plt.subplots(rows, cols, figsize=(cols * 1.5, rows * 1.5))
axes = axes.flatten() if n > 1 else [axes]
for i, ax in enumerate(axes):
if i < n:
ax.imshow(images[i])
label = int(labels[i][0]) if labels[i].ndim > 0 else int(labels[i])
title = classes[label] if classes else str(label)
ax.set_title(title, fontsize=8)
ax.axis('off')
plt.tight_layout()
plt.show()
O conjunto de dados produz lotes de imagens e etiquetas. Cada lote contém 32 imagens de tamanho 224×224 com 3 canais de cor e etiquetas correspondentes:
for x, y in ds_train:
print(f"Training batch shape: features={x.shape}, labels={y.shape}")
x_sample, y_sample = x, y
break
# Expected output: Training batch shape: features=(32, 224, 224, 3), labels=(32,)
display_dataset(x_sample.numpy().astype(np.uint8), np.expand_dims(y_sample, 1), classes=ds_train.class_names)
Observação
Os valores dos píxeis da imagem situam-se entre 0 e 255. Alguns modelos requerem que a entrada seja escalada para 0-1 ou pré-processada usando uma função específica do modelo. O VGG-16 tem a sua própria preprocess_input função que usaremos mais tarde.
Modelos pré-treinados
Existem muitas redes neurais pré-treinadas para classificação de imagens que foram treinadas no conjunto de dados ImageNet, que contém mais de 14 milhões de imagens em 1.000 categorias. Uma das arquiteturas mais conhecidas é a VGG-16, que alcança boa precisão e é simples de compreender. Vamos carregar um modelo VGG-16 com pesos previamente treinados.
vgg = keras.applications.VGG16()
Vamos experimentar utilizar esta rede pré-treinada para classificar uma das nossas fotos. A rede VGG-16 foi treinada no ImageNet, que inclui categorias para várias raças de cães e gatos:
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])
res = vgg(inp)
# tf.argmax returns the index of the highest-probability class
print(f"Most probable class = {tf.argmax(res, 1)}")
# decode_predictions maps class indices to human-readable labels
keras.applications.vgg16.decode_predictions(res.numpy())
A preprocess_input função escala os valores dos píxeis adequadamente para o modelo VGG-16. A decode_predictions função devolve as 5 classes mais prováveis do ImageNet juntamente com as suas pontuações de confiança.
Vamos ver a arquitetura do VGG-16:
# Shows all layers including convolutional blocks and final Dense classifier
vgg.summary()
Cálculos de GPU
Redes neuronais profundas requerem uma potência computacional bastante substancial para treino. Usar uma GPU pode acelerar significativamente o processo de treino. Vamos verificar se existe uma GPU disponível:
# Lists available GPU devices; an empty list means CPU-only
tf.config.list_physical_devices('GPU')
Extração de características VGG
Se quisermos usar o VGG-16 para extrair características das nossas imagens, precisamos do modelo sem as camadas finais de classificação. Podemos fazer isto especificando include_top=False:
vgg = keras.applications.VGG16(include_top=False)
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])
res = vgg(inp)
# The output is a 7x7 grid of 512 feature maps
print(f"Shape after applying VGG-16: {res[0].shape}")
plt.figure(figsize=(15, 3))
plt.imshow(res[0].numpy().reshape(-1, 512))
O vetor de características resultante tem a forma 7×7×512 = 25088 valores. Isto representa as características de alto nível que o VGG-16 aprendeu a extrair da imagem. Podemos pré-calcular manualmente estas funcionalidades para todo o nosso conjunto de dados e depois treinar um classificador por cima:
Advertência
Usamos .take(25) e .take(10) abaixo para limitar o tamanho do conjunto de dados para um treino mais rápido neste exemplo. Cada lote contém 32 imagens, por isso estamos a usar apenas 800 imagens de treino e 320 imagens de teste. A precisão de ~90% aqui reportada reflete este pequeno subconjunto e pode não generalizar para o conjunto de dados completo. Para uso em produção, treine com base no conjunto de dados completo.
def preprocess(x, y):
return keras.applications.vgg16.preprocess_input(x), y
ds_features_train = ds_train.take(25).map(preprocess).map(lambda x, y: (vgg(x), y)).cache()
ds_features_test = ds_test.take(10).map(preprocess).map(lambda x, y: (vgg(x), y)).cache()
for x, y in ds_features_train:
# Expected output: (32, 7, 7, 512) (32,)
print(x.shape, y.shape)
break
Observação
Depois de extrair as características, chamamos .cache() para que o modelo VGG-16 só funcione uma vez por lote em vez de em cada época.
Agora podemos construir um classificador simples sobre as características extraídas. Como as funcionalidades VGG já são altamente informativas, mesmo uma única camada densa pode alcançar bons resultados:
model = keras.Sequential([
keras.layers.Input(shape=(7, 7, 512)),
keras.layers.Flatten(),
keras.layers.Dense(1, activation='sigmoid')
])
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
hist = model.fit(ds_features_train, validation_data=ds_features_test)
# Expected: validation accuracy around 90%
Com cerca de 90% de precisão, isto demonstra o poder das funcionalidades pré-treinadas! No entanto, pré-calcular manualmente funcionalidades é complicado.
Aprendizagem por transferência usando uma única rede VGG
Podemos evitar pré-computar manualmente as funcionalidades combinando o extrator de características VGG-16 e o nosso classificador numa única rede. A chave é congelar as camadas pré-treinadas, de modo que os seus pesos não sejam atualizados durante o treino.
Movemos o preprocess_input passo para o pipeline de dados em vez de o incorporar no modelo como uma Lambda camada. Isto mantém o modelo serializável para que possamos guardá-lo e carregá-lo mais tarde:
def preprocess(x, y):
return keras.applications.vgg16.preprocess_input(x), y
ds_train_preprocessed = ds_train.map(preprocess)
ds_test_preprocessed = ds_test.map(preprocess)
Observação
Como o pré-processamento agora faz parte do pipeline de dados e não do modelo, também deve aplicar preprocess_input aos dados de entrada no momento da inferência.
Agora construímos o modelo com a base congelada do VGG-16:
vgg_base = keras.applications.VGG16(include_top=False, input_shape=(224, 224, 3))
vgg_base.trainable = False
model = keras.Sequential([
keras.layers.Input(shape=(224, 224, 3)),
vgg_base,
keras.layers.Flatten(),
keras.layers.Dense(1, activation='sigmoid')
])
# Notice: ~15 million params are non-trainable (VGG-16), only ~25k are trainable
model.summary()
Ao congelar as camadas VGG-16, só precisamos treinar a camada final densa, que tem cerca de 25.000 parâmetros em vez dos 15 milhões completos. Isto torna o treino mais rápido:
Advertência
Tal como na secção anterior, usamos .take(50) e .take(10) limitamos o conjunto de dados para um treino mais rápido. Isto significa que estamos a treinar com aproximadamente 1.600 imagens e a validar em 320. Os resultados de precisão podem diferir ao treinar com o conjunto de dados completo.
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
hist = model.fit(ds_train_preprocessed.take(50), validation_data=ds_test_preprocessed.take(10))
# Expected: validation accuracy around 90% or higher
Guardar e carregar o modelo
Uma vez que temos um modelo treinado, podemos guardá-lo no disco e recarregá-lo mais tarde sem necessidade de retreinar:
model.save('data/cats_dogs.keras')
Observação
A .keras extensão utiliza o formato nativo Keras 3. Se estiveres a usar uma versão mais antiga do TensorFlow/Keras, usa .h5 (formato HDF5) ou o formato de diretório SavedModel em vez disso.
Para carregar o modelo guardado:
model = keras.models.load_model('data/cats_dogs.keras')
Outros modelos de visão computacional
O VGG-16 é uma das arquiteturas CNN profundas mais simples de compreender, devido à sua estrutura uniforme de convoluções empilhadas 3×3. O Keras oferece muitas mais redes pré-treinadas. As mais usadas entre elas são as arquiteturas ResNet , desenvolvidas pela Microsoft, e a Inception pela Google.
Melhorar os resultados com aumento de dados
Quando se trabalha com dados de treino limitados, a ampliação de dados pode melhorar significativamente a generalização. Ao aplicar transformações aleatórias (como flips horizontais, rotações e zooms) às imagens de treino, aumentamos artificialmente a diversidade do conjunto de dados. O Keras fornece camadas de aumento como keras.layers.RandomFlip, keras.layers.RandomRotation, e keras.layers.RandomZoom que podem ser adicionadas diretamente ao seu modelo ou pipeline de dados.
Conclusão
Com a aprendizagem por transferência, conseguimos rapidamente montar um classificador para a nossa tarefa personalizada de classificação de objetos e alcançar grande precisão. Este exemplo não era totalmente justo porque a rede original VGG-16 estava pré-treinada no ImageNet, que já inclui categorias para várias raças de gatos e cães, e por isso estávamos apenas a reutilizar a maioria dos padrões que já estavam presentes na rede. Pode esperar menor precisão para outros objetos específicos do domínio, como detalhes numa linha de produção numa fábrica ou diferentes folhas de árvores. Pode ver que tarefas mais complexas exigem maior poder computacional e muitas vezes beneficiam da aceleração da GPU para treino.