Projetos de Graduação em Python e Aprendizado Profundo: Do Planejamento à Implementação

Recentemente, ajudei alguns colegas a revisarem seus trabalhos de graduação e percebi um padrão comum: muitos estão entusiasmados com o aprendizado profundo, mas encontram dificuldades ao colocar ideias em prática. Problemas como falta de acesso a GPUs poderosas, dificuldade em encontrar conjuntos de dados adequados e problemas na replicação de modelos são comuns. Este artigo visa fornecer uma abordagem realista para criar um projeto de graduação em Python e aprendizado profundo que seja concluído com sucesso.

1. Três Desafios Comuns em Projetos de Graduação

Antes de escolher um tema, é importante reconhecer algumas barreiras comuns:

  1. Limitações de Recursos Computacionais: Laboratórios geralmente não têm servidores de GPU de alto desempenho. Seu modelo deve rodar em um computador pessoal ou plataformas gratuitas como Google Colab ou Kaggle.
  2. Falta ou Qualidade Inferior de Dados: Conjuntos de dados públicos podem ser muito grandes para download, sujos ou não alinhados com suas necessidades.
  3. Problemas na Reprodução de Modelos: Código clonado do GitHub pode apresentar erros em diferentes ambientes. Mesmo ajustando sementes aleatórias, os resultados podem divergir significativamente dos relatos originais.

Entender essas limitações nos ajuda a focar em projetos que sejam viáveis e tenham pontos de destaque claros.

2. Cinco Dirertizes de Temas Validadas

Com base no princípio de serem "leves", "fáceis de obter" e "profundos", sugiro os seguintes temas:

Diretriz Um: Classificação de Imagens Leves

  • Objetivo: Resolver um problema específico de classificação, sem focar em pontuações altas em conjuntos de dados grandes.
  • Exemplo de Título: "Aplicação de Redes Leves com Mecanismos de Atenção na Classificação de Espécies de Flores".
  • Comparação de Tecnologias:
  • PyTorch: Excelente para pesquisa e desenvolvimento rápido. Oferece modelos pré-treinados e interfaces de dados ricas (torchvision). Recomendado.
  • TensorFlow/Keras: API estável e forte ecossistema de implantação. Ótimo para modelos práticos.
  • Pontos de Destaque: Explorar técnicas de compressão de modelos, estratégias de aumento de dados ou introdução de módulos de atenção simples.

Diretriz Dois: Previsão de Séries Temporais

  • Objetivo: Usar dados históricos para prever tendências futuras.
  • Exemplo de Título: "Modelo de Previsão Curta de Preços de Ações (ou Carga Elétrica) Usando LSTM e Mecanismos de Atenção".
  • Tecnologias: PyTorch ou TensorFlow. PyTorch é mais flexível para estruturas de rede personalizadas.
  • Pontos de Destaque: Foco em pré-processamento de dados, design de modelos e avaliação de resultados.

Diretriz Três: Geração de Texto ou Análise de Sentimentos

  • Objetivo: Fazer com que a máquina entenda ou gere linguagem humana.
  • Exemplo de Título: "Geração de Títulos de Notícias Usando Modelos de Linguagem Pré-Treinados" ou "Análise de Sentimentos em Comentários de E-commerce Usando BERT".
  • Tecnologia: Recomenda-se PyTorch + Biblioteca Transformers da Hugging Face. Facilita a utilização de modelos de ponta.
  • Pontos de Destaque: Microajuste de modelos pré-treinados é uma abordagem comum atualmente. O processo de adaptação para tarefas específicas é um excelente ponto para discussões.

Diretriz Quatro: Geração de Imagens com GANs

  • Objetivo: Criar imagens realistas usando redes adversárias geradoras.
  • Exemplo de Título: "Geração de Pinturas no Estilo Monet Usando CycleGAN" ou "Aumento de Dados Usando Conditional GAN".
  • Tecnologia: PyTorch. Melhor para depuração em cenários complexos.
  • Pontos de Destaque: Resultados visuais impressionantes e fácil inclusão de imagens no trabalho.

Diretriz Cinco: Detecção e Segmentação de Objetos (Avançado)

  • Objetivo: Identificar objetos e localizá-los.
  • Exemplo de Título: "Sistema de Detecção em Tempo Real de Sinalização Viária Usando YOLOv5" ou "Segmentação de Lesões em Imagens Médicas Usando U-Net".
  • Tecnologia: PyTorch. Bibliotecas como YOLOv5/v8 facilitam o início.
  • Observação: Requer mais recursos computacionais e dados anotados.

3. Exercício Prático: Classificação de Flores com Modelo Leve

Vamos seguir o Diretriz Um, usando PyTorch. Suponha que estamos usando o conjunto de dados Oxford 102 Flowers.

3.1 Preparação e Pré-processamento de Dados

Os dados são a base do modelo. Um bom pré-processamento pode melhorar significativamente o desempenho.

import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import os

# 1. Definir transformações (pipeline de pré-processamento)
# Conjunto de treinamento: aumentação + normalização
transformacao_treino = transforms.Compose([
    transforms.RandomResizedCrop(224),        # Recorte e redimensionamento aleatórios
    transforms.RandomHorizontalFlip(),         # Flip horizontal aleatório
    transforms.ToTensor(),                     # Converter para Tensor e normalizar para [0,1]
    transforms.Normalize(mean=[0.485, 0.456, 0.406],  # Médias gerais do ImageNet
                         std=[0.229, 0.224, 0.225])   # Desvios padrão gerais do ImageNet
])

# Conjunto de validação/teste: apenas normalização
transformacao_validacao = transforms.Compose([
    transforms.Resize(256),                    # Redimensionamento
    transforms.CenterCrop(224),                # Recorte central
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

# 2. Carregar conjuntos de dados (assumindo que os dados foram baixados para `./dados/florais`)
diretorio_dados = './dados/florais'
conjunto_treino = datasets.ImageFolder(os.path.join(diretorio_dados, 'treino'), transform=transformacao_treino)
conjunto_validacao = datasets.ImageFolder(os.path.join(diretorio_dados, 'validacao'), transform=transformacao_validacao)

# 3. Criar carregadores de dados
tamanho_lote = 32  # Ajustar conforme a memória da GPU, reduzir se ocorrer OOM (overflow de memória)
carregador_treino = DataLoader(conjunto_treino, batch_size=tamanho_lote, shuffle=True, num_workers=2)
carregador_validacao = DataLoader(conjunto_validacao, batch_size=tamanho_lote, shuffle=False, num_workers=2)

print(f'Samples no conjunto de treino: {len(conjunto_treino)}')
print(f'Número de classes: {len(conjunto_treino.classes)}')

3.2 Definição do Modelo: Usando um Modelo Pré-treinado e Fine-tuning

Não comece do zero! O aprendizado por transferência é uma ferramenta valiosa quando os recursos são limitados.

import torch.nn as nn
import torchvision.models as modelos

class ClassificadorDeFlores(nn.Module):
    """
    Classificador de flores baseado em MobileNetV3 pré-treinado.
    Segue princípios de código limpo: nomes de classes claros, estrutura definida, comentários em etapas-chave.
    """
    def __init__(self, num_classes=102, usar_pre_treinamento=True):
        super(ClassificadorDeFlores, self).__init__()
        # Carregar o modelo骨干 pré-treinado e congelar seus parâmetros inferiores
        self.backbone = modelos.mobilenet_v3_small(pretrained=usar_pre_treinamento)
        
        # Congelar todos os parâmetros do backbone, inicialmente treinando apenas a cabeça de classificação
        for parametro in self.backbone.parameters():
            parametro.requires_grad = False
            
        # Substituir a última camada de classificação para se adequar à nossa tarefa
        entradas_classe = self.backbone.classifier[-1].in_features
        # O classificador do MobileNetV3 é um Sequential, substituímos sua última camada
        self.backbone.classifier[-1] = nn.Linear(entradas_classe, num_classes)

    def forward(self, x):
        """Propagação direta"""
        return self.backbone(x)
    
    def descongelar_backbone(self, etapa=5):
        """
        Descongelar camadas finais do backbone para fine-tuning.
        etapa: número de camadas finais a descongelar (ajustar conforme a arquitetura do modelo)
        """
        # Exemplo simplificado, ajustar conforme os nomes dos módulos do modelo
        camadas_totais = len(list(self.backbone.features.children()))
        camadas_a_descongelar = list(self.backbone.features.children())[-etapa:]
        for camada in camadas_a_descongelar:
            for parametro in camada.parameters():
                parametro.requires_grad = True
        print(f"Camadas finais {etapa} do backbone descongeladas.")


3.3 Estratégia de Treinamento e Loop Principal

O treinamento é um processo iterativo e de ajuste de hiperparâmetros.

import torch.optim as otimizador
from tqdm import tqdm  # Para exibir barra de progresso

def treinar_um_epoca(modelo, carregador, criterio, otimizador, dispositivo):
    """Treina um único épocas"""
    modelo.train()
    perda_acumulada = 0.0
    corretos = 0
    totais = 0
    
    barra_progresso = tqdm(carregador, desc='Treinando')
    for imagens, etiquetas in barra_progresso:
        imagens, etiquetas = imagens.to(dispositivo), etiquetas.to(dispositivo)
        
        # Zerar gradientes
        otimizador.zero_grad()
        
        # Propagação direta
        saidas = modelo(imagens)
        perda = criterio(saidas, etiquetas)
        
        # Retropropagação e otimização
        perda.backward()
        otimizador.step()
        
        # Estatísticas
        perda_acumulada += perda.item() * imagens.size(0)
        _, previstos = torch.max(saidas, 1)
        totais += etiquetas.size(0)
        corretos += (previstos == etiquetas).sum().item()
        
        # Atualizar informações da barra de progresso
        barra_progresso.set_postfix({'Perda': perda.item(), 'Acurácia': corretos/totais})
    
    perda_epoca = perda_acumulada / totais
    acuracia_epoca = corretos / totais
    return perda_epoca, acuracia_epoca

def validar(modelo, carregador, criterio, dispositivo):
    """Valida o modelo"""
    modelo.eval()
    perda_acumulada = 0.0
    corretos = 0
    totais = 0
    
    with torch.no_grad():  # Desativar cálculo de gradientes, economiza memória e computação
        for imagens, etiquetas in tqdm(carregador, desc='Validando'):
            imagens, etiquetas = imagens.to(dispositivo), etiquetas.to(dispositivo)
            saidas = modelo(imagens)
            perda = criterio(saidas, etiquetas)
            
            perda_acumulada += perda.item() * imagens.size(0)
            _, previstos = torch.max(saidas, 1)
            totais += etiquetas.size(0)
            corretos += (previstos == etiquetas).sum().item()
    
    perda_epoca = perda_acumulada / totais
    acuracia_epoca = corretos / totais
    return perda_epoca, acuracia_epoca

# Fluxo principal de treinamento
dispositivo = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
modelo = ClassificadorDeFlores(num_classes=102).to(dispositivo)

# Definir função de perda e otimizador
criterio = nn.CrossEntropyLoss()
# Inicialmente, treinar apenas a nova camada de classificação, usar taxa de aprendizado maior
otimizador = otimizador.Adam(modelo.parameters(), lr=1e-3)

epocas = 10
for epoca in range(epocas):
    print(f'\nÉpoca {epoca+1}/{epocas}')
    perda_treino, acuracia_treino = treinar_um_epoca(modelo, carregador_treino, criterio, otimizador, dispositivo)
    perda_validacao, acuracia_validacao = validar(modelo, carregador_validacao, criterio, dispositivo)
    print(f'Perda de Treino: {perda_treino:.4f}, Acurácia: {acuracia_treino:.4f} | Perda de Validação: {perda_validacao:.4f}, Acurácia: {acuracia_validacao:.4f}')
    
    # Após algumas épocas, descongelar partes do backbone para fine-tuning
    if epoca == 5:
        modelo.descongelar_backbone(etapa=3)
        # Reduzir taxa de aprendizado para fine-tuning
        otimizador = otimizador.Adam(filter(lambda p: p.requires_grad, modelo.parameters()), lr=1e-4)

3.4 Implantação do Modelo: Encapsulando uma API Web Simples

Fazer o modelo "funcionar" é um diferencial no trabalho. Flask ou FastAPI podem ser usados para criar rapidamente.

# app.py (usando Flask)
from flask import Flask, request, jsonify
import torch
from PIL import Image
import io
from seu_arquivo_de_modelo import ClassificadorDeFlores  # Importar a classe do seu modelo treinado
import torchvision.transforms as transformacoes

app = Flask(__name__)
modelo = ClassificadorDeFlores(num_classes=102)
modelo.load_state_dict(torch.load('melhor_modelo.pth', map_location='cpu'))  # Carregar pesos
modelo.eval()  # Mudar para modo de avaliação

# Definir transformações idênticas às usadas no treinamento
transformacao = transformacoes.Compose([
    transformacoes.Resize(256),
    transformacoes.CenterCrop(224),
    transformacoes.ToTensor(),
    transformacoes.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

@app.route('/prever', methods=['POST'])
def prever():
    if 'arquivo' not in request.files:
        return jsonify({'erro': 'Nenhum arquivo enviado'}), 400
    
    arquivo = request.files['arquivo']
    imagem = Image.open(io.BytesIO(arquivo.read())).convert('RGB')
    
    # Pré-processamento
    tensor_imagem = transformacao(imagem).unsqueeze(0)  # Adicionar dimensão de lote
    
    # Previsão
    with torch.no_grad():
        saidas = modelo(tensor_imagem)
        _, previsto = torch.max(saidas, 1)
        id_classe = previsto.item()
        # Supondo que você tem uma lista de nomes de classes class_names
        nome_classe = class_names[id_classe]
    
    return jsonify({'id_classe': id_classe, 'nome_classe': nome_classe})

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

4. Problemas Práticos Que Devemos Enfrentar

  1. Tamanho do Modelo e Latência de Inferência: Modelos leves como MobileNetV3 são projetados para dispositivos móveis ou de borda. Use torch.jit.trace ou torch.jit.script para converter o modelo para TorchScript ou exporte para ONNX. Para APIs web, a latência depende principalmente do desempenho da CPU/GPU do servidor.
  2. Overfitting: Comum entre iniciantes. Indicado por alta precisão no conjunto de treino e baixa precisão no de validação. Soluções:
  • Aumento de Dados: Como recorte aleatório, flip horizontal, variação de cores.
  • Regularização: Adicionar penalidade de peso no otimizador (weight_decay), usar camadas Dropout.
  • Early Stopping: Monitorar perda do conjunto de validação, parar treinamento se não diminuir.
  • Modelos Menores: Use modelos menores se o conjunto de dados for pequeno.
  1. Resultados Não Reprodutíveis: Defina sementes aleatórias!
import random
import numpy as np
torch.manual_seed(42)
np.random.seed(42)
random.seed(42)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(42)

5. Guia de Evitação de Erros: Lições Aprendidas

  1. Conflitos de Configuração de Ambiente: Use conda ou venv para criar ambientes Python independentes. Registre todas as dependências e versões em requirements.txt ou environment.yml.
  2. Overflow de Memória de GPU (CUDA out of memory):
  • Reduza batch_size.
  • Use acumulação de gradiantes: se desejar batch_size=64, mas a memória não suportar, use batch_size=16 e atualize os gradientes a cada 4 lotes.
  • Use treinamento de precisão mista (torch.cuda.amp).
  • Libere variáveis desnecessárias: del variavel; torch.cuda.empty_cache().
  1. Perda de Treinamento NaN: Verifique se a taxa de aprendizado é alta demais, se há valores anômalos nos dados e se a função de perda é adequada.
  2. Modelo Não Convergindo (Perda Parada):
  • Verifique pré-processamento e etiquetas.
  • Verifique a arquitetura do modelo, especialmente dimensões de entrada/saída.
  • Experimente taxas de aprendizado menores ou use aquecimento de taxa de aprendizado (lr_scheduler).
  • Visualize dados de entrada e mapas de características intermediárias.
  1. Escrita de Artigos e Registro de Experimentos: Comece desde o primeiro dia! Use ferramentas de registro de experimentos para acompanhar hiperparâmetros, resultados e observações.

Conclusão: Comece com MVP

Se tudo parecer complicado, minha sugestão é começar com um Produto Mínimo Viável (MVP).

  1. Escolha um tema simples: Por exemplo, "Classificação de Gatos e Cachorros" (classificação binária é mais simples que multiclasse).
  2. Encontre um código pronto: Procure projetos de PyTorch com muitas estrelas no GitHub.
  3. Substitua o conjunto de dados: Substitua as imagens de gatos e cachorros pelo seu próprio conjunto de dados (por exemplo, "Classificação de Maçãs e Bananas").
  4. Execute todo o fluxo de treinamento-validação-teste: Não se preocupe com a precisão inicialmente, apenas garanta que o fluxo funcione.
  5. Itere: Aumento de dados, uso de modelos melhores, ajuste de hiperparâmetros, experimentação com funções de perda diferentes...

O projeto de graduação é um processo de aprendizado, não uma missão de construir foguetes. Com esse MVP, você já dominou o pipeline fundamental de projetos de aprendizado profundo. Cada etapa subsequente de otimização e exploração será mais confiante e resultará em um artigo mais robusto.

Boa sorte com seu projeto de graduação! Não apenas obtenha a nota, mas também adquira experiência prática sólida.

Tags: Pytorch DeepLearning Flask TransferLearning ModelDeployment

Publicado em 8-9 03:03