POTHIEDU Não Decore a Resposta. Entenda o Caminho

Redes Multiescalares

Redes Multiescalares

Implementando redes multiescalares do zero

A maioria dos tutoriais que você vê na internet começa com uma pirâmide de Gauss ou com a teoria de wavelets e fica no nível conceitual por cinquenta linhas antes de mostrar qualquer código. Isso é inútil na prática. Vou direto ao que funciona. Redes multiescalares, no contexto de visão computacional e processamento de imagens, são arquiteturas que processam informações em diferentes níveis de resolução dentro da mesma rede. O conceito mais conhecido é o da U-Net, mas o termo se aplica também a arquiteturas com múltiplos branches de escala variada, como FPN (Feature Pyramid Networks) e suas variações. A ideia central é simples: características de baixo nível (bordas, texturas) aparecem em resoluções altas, enquanto características semânticas (objetos, estruturas) emergem em resoluções baixas. Capturar ambos os níveis simultaneamente melhora significativamente a precisão, especialmente em tarefas de segmentação semântica e detecção de objetos.

Redes multiescalares: como estruturar na prática

O primeiro erro que eu cometi foi tentar empilhar camadas convolucionais em paralelo em todas as escalas sem conectar nada entre elas. O resultado foi um modelo que simplesmente aprendeu a ignorar as escalas mais grossas porque o gradiente não passava por elas. O problema é que, sem conexões laterais adequadas, cada braço da rede se torna um classificador independente que não colabora com os outros. A solução padrão é usar fusão top-down com conexões laterais. Você começa com um backbone (ResNet, EfficientNet, etc.) que gera features maps em diferentes depths. Depois, percorre esses mapas do mais coarse para o mais fine, fazendo upsampling e somando com as features do caminho lateral correspondente. Em código, usando PyTorch, algo assim: class MultiscaleFusion(nn.Module): def __init__(self, channels_list): super().__init__() self.upsample = nn.Upsample(scale_factor=2, mode='nearest') self.conv = nn.Conv2d(channels_list[-1], channels_list[0], 3, padding=1) def forward(self, features): features é uma lista de tensors em diferentes escalas result = self.upsample(features[-1]) for i in range(len(features)-2, -1, -1): result = result + self.upsample(result) result = self.conv(result) return result

Isto é simplificado, mas captura a estrutura essencial. Na prática, você precisa de camadas convolucionais 1x1 nos ramos laterais para alinhar o número de canais antes de somar. Sem isso, você não consegue fazer o skip connection. Agora, o detalhe que ninguém menciona nos tutorials bons: a escolha do método de upsampling importa mais do que parece. Upsampling por bilinear ou nearest costuma introduzir artefatos de escada que degradam a qualidade final, especialmente quando você está fazendo múltiplos passos de upsampling encadeados. Transposed convolution (convolução transposta) resolve isso parcialmente, mas cria o problema oposto de (checkerboard artifacts) se você não usar kernel size adequado. A saída mais limpa que eu encontrei até hoje foi interpolação bilinear seguida de uma convolução 3x3 com stride 1 — simples, mas elimina a maioria dos artefatos sem aumentar significativamente o custo computacional. Outro ponto: o balanceamento de loss entre as escalas. Se você aplicar supervisionauxiliary em todos os níveis, o gradiente das camadas mais profundas (mais grossas) domina porque a perda absoluta é maior naquele espaço de feature. A prática recomendada é dar pesos decrescentes para as escalas mais grossas, tipicamente usando um fator de 0.4 para a camada mais coarse e 1.0 para a mais fine. Eu já vi gente aplicar pesos iguais em todos os níveis e o modelo convergir muito pior do que deveria.

Redes Escalares - Luis Tica Quispe REDES ESCALARES Capas de acceso ...
Redes Escalares - Luis Tica Quispe REDES ESCALARES Capas de acceso ...

Eu tive um problema específico num projeto de segmentação de lesões dermatológicas. O dataset tinha lesões variando de alguns pixels a várias centenas de pixels na mesma imagem. Uma rede multiescalar padrão com apenas três níveis de pirâmide simplesmente não capturava as lesões menores com precisão suficiente. A workaround que funcionou foi adicionar um braço extra de alta resolução antes da primeira redução de escala — basicamente, processar o input original em full resolution em paralelo com a pirâmide e fazer uma conexão direta do braço mais fino para a última camada de decisão. Isso custou cerca de 15% de FLOPs a mais, mas melhorou o IoU em lesões pequenas de 12 para 19 pontos percentuais. O lado negativo que precisa ser dito: redes multiescalares aumentam a memória VRAM de forma significativa. Um modelo U-Net padrão pode caber num batch size de 32 numa RTX 4090. Uma variação com FPN de 5 níveis reduz o batch size para 8 ou 12 no mesmo hardware. Se você está trabalhando com datasets grandes e tem limitação de GPU, isso é um fator real. A alternativa nesse caso é usar gradient checkpointing ou processar em tiles, mas ambas introduzem complexidade extra que pode não valer a pena dependendo do seu cenário. Se você está começando, recomendo começar com uma implementação existente de FPN ou U-Net do segmentation_models_pytorch ou do timm, e só depois adicionar os braços extras conforme a necessidade do seu dataset. Tentar construir uma arquitetura multiescalar do zero sem entender primeiro como os gradients fluem por ela vai te dar dor de cabeça por semanas antes de funcionar decentemente. E quando funcionar, provavelmente estará muito atrás do que uma versão pronta ofereceria em termos de estabilidade numérica e otimização de memória.