O desenvolvimento de modelos para inferir idade e gênero a partir de imagens faciais é um desafio complexo na visão computacional. Este guia técnico explora o processo de construção de uma solução robusta, desde a preparação dos dados até a avaliação do modelo, utilizando o conjunto de dados UTKFace.
1. Preparação do Ambiante e Constantes
A configuração inicial envolve a importação de bibliotecas essenciais e a definição de constantes cruciais para a reprodutibilidade e o fluxo de trabalho.
# Módulos Gerais e de Dados
import os
import numpy as np
import pandas as pd
import tensorflow as tf
import matplotlib.pyplot as plt
import plotly.express as px
# Pré-processamento e Divisão de Dados
from sklearn.model_selection import train_test_split
# Arquiteturas de Modelos (Backbones)
from keras.applications import VGG16
from keras.applications import ResNet50V2, ResNet152V2
from keras.applications import Xception, InceptionV3
from keras.applications import MobileNetV3Small, MobileNetV3Large
# Construção e Treinamento do Modelo
from keras import Sequential
from keras.layers import (Dense, Dropout, Flatten,
GlobalAveragePooling2D, InputLayer)
from keras.callbacks import EarlyStopping, ModelCheckpoint
# Fixando sementes para reprodutibilidade
SEMENTE = 42
np.random.seed(SEMENTE)
tf.random.set_seed(SEMENTE)
# Parâmetros Globais
TAMANHO_LOTE = 32
DIMENSAO_IMAGEM = 224
2. Carregamento e Exploração do Conjunto UTKFace
O dataset UTKFace contém mais de 20.000 imagens de rostos rotulados com idade, gênero e etnia. O padrão de nomenclatura dos arquivos (idade_genero_etnia_data.jpg) é fundamental para extrair os metadados.
CAMINHO_DADOS = "/kaggle/input/utkface-new/UTKFace/"
lista_arquivos = os.listdir(CAMINHO_DADOS)
mapeamento_genero = ["Masculino", "Feminino"]
# Exemplo de carregamento de uma imagem aleatória
arquivo_aleatorio = np.random.choice(lista_arquivos)
imagem_exemplo = plt.imread(os.path.join(CAMINHO_DADOS, arquivo_aleatorio)) / 255.0
partes_nome = arquivo_aleatorio.split("_")
print(f"Total de imagens: {len(lista_arquivos)}")
print(f"Arquivo: {arquivo_aleatorio}")
print(f"Idade extraída: {partes_nome[0]}")
print(f"Gênero extraído: {mapeamento_genero[int(partes_nome[1])]}\n")
plt.figure(figsize=(5, 5))
plt.imshow(imagem_exemplo)
plt.title("Imagem de Exemplo")
plt.axis('off')
plt.show()
Análise da Distribuição dos Dados
Uma análise exploratória revela vieses inerentes no dataset, como a distribuição desigual de gênero e a concentração em faixas etárias específicas.
# Contagem de gênero
contagem_masc = 0
lista_idades = []
for nome_arq in lista_arquivos:
partes = nome_arq.split("_")
if partes[1] == "0":
contagem_masc += 1
lista_idades.append(int(partes[0]))
contagem_fem = len(lista_arquivos) - contagem_masc
# Visualização com Plotly
fig_pizza = px.pie(names=mapeamento_genero,
values=[contagem_masc, contagem_fem],
hole=0.4, title="Distribuição de Gênero")
fig_pizza.show()
fig_histograma = px.histogram(sorted(lista_idades),
title="Distribuição de Idades",
labels={'value': 'Idade', 'count': 'Contagem'})
fig_histograma.show()
A análise de gênero mostra um leve desbalanceamento (~52% masculino vs. ~48% feminino). A análise etária revela dois picos principais: crianças (0-12 anos) e adultos jovens (16-45 anos), com um pico mais pronunciado em torno dos 26 anos. Esse viés para populações mais jovens deve ser considerado durante o treinamento.
Pré-processamento e Criação dos Datasets
Para gerenciar a memória de forma eficiente com milhares de imagens, utilizamos tf.data.Dataset. As imagens são decodificadas, redimensionadas e normalizadas.
# Divisão inicial dos arquivos
np.random.shuffle(lista_arquivos)
arquivos_treino, arquivos_teste = train_test_split(lista_arquivos, test_size=0.1, random_state=SEMENTE)
arquivos_treino, arquivos_valid = train_test_split(arquivos_treino, test_size=0.1, random_state=SEMENTE)
# Função de pré-processamento genérica
def preprocessar_imagem(caminho_arquivo, rotulo, caminho_base=CAMINHO_DADOS, tamanho=DIMENSAO_IMAGEM):
caminho_completo = tf.strings.join([caminho_base, caminho_arquivo])
img_raw = tf.io.read_file(caminho_completo)
img_decodificada = tf.io.decode_jpeg(img_raw, channels=3)
img_redim = tf.image.resize(img_decodificada, [tamanho, tamanho])
img_normalizada = tf.cast(img_redim, tf.float32) / 255.0
return img_normalizada, rotulo
# Extração de rótulos
def extrair_rotulos(lista):
idades = [int(f.split("_")[0]) for f in lista]
generos = [int(f.split("_")[1]) for f in lista]
return idades, generos
idades_treino, generos_treino = extrair_rotulos(arquivos_treino)
idades_valid, generos_valid = extrair_rotulos(arquivos_valid)
idades_teste, generos_teste = extrair_rotulos(arquivos_teste)
# Criação dos pipelines de dados para Idade
ds_treino_idade = tf.data.Dataset.from_tensor_slices((arquivos_treino, idades_treino))
ds_treino_idade = ds_treino_idade.shuffle(2000).map(preprocessar_imagem).batch(TAMANHO_LOTE).prefetch(tf.data.AUTOTUNE)
# Repetir para validação/teste e para gênero...
3. Comparação de Backbones para Idade e Gênero
Para identificar a melhor arquitetura base, comparamos o desempenho de várias redes pré-treinadas (VGG16, ResNets, Xception, Inception, MobileNets) em uma tarefa simples de regressão (para idade) e classificação (para gênero). A abordagem de usar modelos separados para cada tarefa mostrou-se mais eficaz.
lista_backbones = [
("VGG16", VGG16(input_shape=(DIMENSAO_IMAGEM, DIMENSAO_IMAGEM, 3), include_top=False, weights='imagenet')),
("ResNet50V2", ResNet50V2(input_shape=(DIMENSAO_IMAGEM, DIMENSAO_IMAGEM, 3), include_top=False, weights='imagenet')),
("ResNet152V2", ResNet152V2(input_shape=(DIMENSAO_IMAGEM, DIMENSAO_IMAGEM, 3), include_top=False, weights='imagenet')),
# ... adicionar Xception, InceptionV3, MobileNets ...
]
# Função para criar e treinar um modelo teste rápido
def avaliar_backbone(nome, backbone, ds_treino, ds_valid, eh_regressao=True):
backbone.trainable = False
modelo_temp = Sequential([
InputLayer(input_shape=(DIMENSAO_IMAGEM, DIMENSAO_IMAGEM, 3)),
backbone,
Flatten(),
Dense(1, activation='linear' if eh_regressao else 'sigmoid')
])
perda = 'mae' if eh_regressao else 'binary_crossentropy'
metrica = [] if eh_regressao else ['accuracy']
modelo_temp.compile(optimizer='adam', loss=perda, metrics=metrica)
hist = modelo_temp.fit(ds_treino, validation_data=ds_valid, epochs=5, verbose=0)
return hist.history
historicos_backbones = {}
for nome, backbone in lista_backbones:
historicos_backbones[nome] = avaliar_backbone(nome, backbone, ds_treino_idade, ds_valid_idade, eh_regressao=True)
# Plotar curvas de aprendizado...
Os resultados da comparação indicaram que a VGG16 oferece o melhor equilíbrio entre desempenho e estabilidade para a predição de idade (regressão), enquanto a ResNet152V2 se destacou para a classificação de gênero.
4. Modelo Final para Predição de Idade (VGG16)
Construindo o modelo completo de regressão de idade com a VGG16 como backbone congelado e camadas superiores personalizadas.
# Backbone VGG16
vgg_base = VGG16(input_shape=(DIMENSAO_IMAGEM, DIMENSAO_IMAGEM, 3), include_top=False, weights='imagenet')
vgg_base.trainable = False
# Modelo para Idade
modelo_idade = Sequential([
vgg_base,
Dropout(0.4),
Flatten(),
Dense(256, activation='relu', kernel_initializer='he_normal'),
Dense(1, activation='linear') # Saída única para idade
], name='Modelo_Idade')
modelo_idade.compile(optimizer='adam', loss='mae')
# Treinamento com callbacks
callbacks_idade = [
EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True),
ModelCheckpoint('melhor_modelo_idade.keras', save_best_only=True)
]
historico_idade = modelo_idade.fit(
ds_treino_idade,
validation_data=ds_valid_idade,
epochs=20,
batch_size=TAMANHO_LOTE,
callbacks=callbacks_idade
)
O modelo alcançou um erro absoluto médio (MAE) de ~6.7 anos no conjunto de validação, uma melhoria significativa em relação ao baseline, com early stoping prevenindo overfitting.
5. Modelo Final para Predição de Gênero (ResNet152V2)
O modelo de classificação binária de gênero utiliza a ResNet152V2 e uma camada de pooling global para reduzir parâmetros e overfitting.
# Backbone ResNet152V2
resnet_base = ResNet152V2(input_shape=(DIMENSAO_IMAGEM, DIMENSAO_IMAGEM, 3), include_top=False, weights='imagenet')
resnet_base.trainable = False
# Modelo para Gênero
modelo_genero = Sequential([
resnet_base,
Dropout(0.2),
GlobalAveragePooling2D(),
Dense(1, activation='sigmoid') # Saída única para gênero (probabilidade)
], name='Modelo_Genero')
modelo_genero.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
callbacks_genero = [
EarlyStopping(monitor='val_accuracy', patience=5, restore_best_weights=True),
ModelCheckpoint('melhor_modelo_genero.keras', save_best_only=True)
]
historico_genero = modelo_genero.fit(
ds_treino_genero,
validation_data=ds_valid_genero,
epochs=20,
batch_size=TAMANHO_LOTE,
callbacks=callbacks_genero
)
Com a arquitetura otimizada, o modelo de gênero alcançou ~88% de acurácia no treino e ~87% na validação, demonstrando boa generalização e robustez.
6. Avaliação nos Dados de Teste
A avaliação final nos dados nunca vistos (conjunto de teste) é crucial para estimar o desempenho em produção.
# Carregar os melhores modelos salvos
modelo_idade_final = tf.keras.models.load_model('melhor_modelo_idade.keras')
modelo_genero_final = tf.keras.models.load_model('melhor_modelo_genero.keras')
# Avaliar
perda_idade, mae_teste = modelo_idade_final.evaluate(ds_teste_idade)
perda_genero, acc_genero_teste = modelo_genero_final.evaluate(ds_teste_genero)
print(f"MAE Idade (Teste): {mae_teste:.4f}")
print(f"Acurácia Gênero (Teste): {acc_genero_teste:.4%}")
Os resultados no teste confirmaram a capacidade de generalização dos modelos, com métricas consistentes com as observadas na validação.
7. Inferência e Visualização de Previsões
Para um teste prático, podemos executar ambos os modelos em imagens de teste e visualizar as previsões lado a lado com os rótulos verdadeiros.
# Função de inferência combinada
def prever_idade_genero(imagem_tensor):
# Pré-processar imagem de entrada (se necessário)
img_expandida = tf.expand_dims(imagem_tensor, axis=0)
pred_idade = modelo_idade_final.predict(img_expandida, verbose=0)[0][0]
pred_genero_prob = modelo_genero_final.predict(img_expandida, verbose=0)[0][0]
pred_genero_classe = int(pred_genero_prob > 0.5)
return pred_idade, pred_genero_classe
# Visualização em lote
plt.figure(figsize=(15, 10))
for imagens_batch, _, generos_batch in ds_teste_completo.take(1):
for i in range(min(16, len(imagens_batch))):
img = imagens_batch[i]
idade_real = ... # Extrair do nome do arquivo ou do dataset
genero_real = generos_batch[i]
idade_pred, genero_pred = prever_idade_genero(img)
plt.subplot(4, 4, i+1)
plt.imshow(img.numpy())
plt.title(f"Real: {idade_real}, {mapeamento_genero[genero_real]}\nPred: {int(idade_pred)}, {mapeamento_genero[genero_pred]}")
plt.axis('off')
plt.tight_layout()
plt.show()
A inspeção visual das previsões em amostrsa de teste mostra um desempenho sólido na maioria dos casos, com o modelo capturando tendências de idade e classificando gênero com alta precisão. A análise de erros específicos (como em imagens de bebês) pode fornecer insights para futuras melhorias.