Redes Neurais Convolucionais: Teoria e Implementação com Python

Introdução às Redes Neurais Convolucionais

Redes Neurais Convolucionais (CNNs) são amplamente utilizadas em tarefas como reconhecimento de imagens e processamento de voz. Diferente das redes neurais totalmente conectadas, onde todos os neurônios entre camadas adjacentes estão interligados, as CNNs introduzem camadas convolucionais e de pooling, mantendo a estrutura espacial dos dados de entrada.

Camada Convolucional

Problemas das Camadas Totalmente Conectadas

Em redes totalmente conectadas, a forma dos dados é ignorada. Por exemplo, uma imagem com dimensões (altura, largura, canais) precisa ser achatada em um vetor unidimensional antes de ser processada. Isso pode levar à perda de informações espaciais. As camadas convolucionais, no entanto, preservam a forma tridimensional dos dados, permitindo uma melhor compreensão de padrões visuais.

Dados de entrada e saída em camadas convolucionais são frequentemente chamados de mapas de características. O mapa de entrada é o dado original, e o mapa de saída é o resultado após a aplicação de filtros.

Operação de Convolução

A operação de convolução envolve aplicar um filtro (ou kernel) sobre os dados de entrada para produzir um mapa de características de saída. O filtro desliza sobre a entrada, realizando produtos escalares locais. Um viés (bias) pode ser adicionado ao resultado para ajustar a ativação.

Preenchimento (Padding)

O preenchimento é utilizado para controlar o tamanho da saída. Sem ele, cada operação de convolução reduz a dimensão espacial dos dados. O preenchimento adiciona bordas zeros à entrada, mantendo o tamanho da saída quando necessário.

Passo (Stride)

O passo define o intervalo com que o filtro é aplicado sobre a entrada. Um passo maior reduz o tamanho da saída, enquanto um passo menor preserva mais detalhes.

O tamanho da saída pode ser calculado com a fórmula:

Convolução em Dados Tridimensionais

Para dados com múltiplos canais (como imagens RGB), a convolução é aplicada em cada canal e os resultados são somados. O filtro deve ter a mesma profundidade (número de canais) que a entrada. Para produzir múltiplos mapas de características de saída, utiliza-se um conjunto de filtros, resultando em uma saída com dimensão (número_de_filtros, altura_saida, largura_saida).

Processamento em Lote

Para eficiência, os dados são processados em lotes (batches). As entradas são organizadas em um array de quatro dimensões: (tamanho_do_lote, canais, altura, largura). Isso permite que múltiplas amostras sejam processadas simultaneamente por operações matriciais otimizadas.

Camada de Pooling

A camada de pooling reduz a dimensionalidade espacial dos mapas de características, diminuindo o custo computacional. As operações comuns incluem pooling máximo (max pooling) e pooling médio (average pooling).

Características principais do pooling:

  • Sem parâmetros treináveis: É uma operação fixa que não aprende com os dados.
  • Número de canais inalterado: O pooling é aplicado independentemente a cada canal.
  • Robustez a pequenas variações: Pequenos deslocamentos na entrada não afetam significativamente a saída.

Embora o pooling reduza custos computacionais, modelos modernos com alta capacidade de processamento frequentemente o omittem para melhorar o desempenho em tarefas complexas.

Implementação com Python

Técnica im2col

Para otimizar a convolução, utiliza-se a técnica im2col (imagem para colunas). Ela transforma os patches locais da entrada em colunas de uma matriz, permitindo que a convolução seja realizada como um produto matricial.

Abaixo, uma implementação simplificada da função im2col em Python:

import numpy as np

def imagem_para_colunas(dados, alt_filtro, larg_filtro, passo=1, preenchimento=0):
    """
    Converte dados de entrada em uma matriz colunar para convolução eficiente.
    """
    lotes, canais, alt_entrada, larg_entrada = dados.shape
    alt_saida = (alt_entrada + 2 * preenchimento - alt_filtro) // passo + 1
    larg_saida = (larg_entrada + 2 * preenchimento - larg_filtro) // passo + 1

    dados_preenchidos = np.pad(dados, [(0,0), (0,0), (preenchimento, preenchimento), (preenchimento, preenchimento)], 'constant')
    matriz_colunar = np.zeros((lotes, canais, alt_filtro, larg_filtro, alt_saida, larg_saida))

    for i in range(alt_filtro):
        i_max = i + passo * alt_saida
        for j in range(larg_filtro):
            j_max = j + passo * larg_saida
            matriz_colunar[:, :, i, j, :, :] = dados_preenchidos[:, :, i:i_max:passo, j:j_max:passo]

    matriz_colunar = matriz_colunar.transpose(0, 4, 5, 1, 2, 3).reshape(lotes * alt_saida * larg_saida, -1)
    return matriz_colunar

Implementação da Camada Convolucional

A classe a seguir representa uma camada convolucional, utilizando im2col para eficiência:

class CamadaConvolucional:
    def __init__(self, pesos, viés, passo=1, preenchimento=0):
        self.pesos = pesos  # Forma: (num_filtros, canais, alt_filtro, larg_filtro)
        self.viés = viés
        self.passo = passo
        self.preenchimento = preenchimento

    def forward(self, entrada):
        num_filtros, canais, alt_filtro, larg_filtro = self.pesos.shape
        lotes, _, alt_entrada, larg_entrada = entrada.shape
        alt_saida = (alt_entrada + 2 * self.preenchimento - alt_filtro) // self.passo + 1
        larg_saida = (larg_entrada + 2 * self.preenchimento - larg_filtro) // self.passo + 1

        col_entrada = imagem_para_colunas(entrada, alt_filtro, larg_filtro, self.passo, self.preenchimento)
        col_pesos = self.pesos.reshape(num_filtros, -1).T

        saida = np.dot(col_entrada, col_pesos) + self.viés
        saida = saida.reshape(lotes, alt_saida, larg_saida, -1).transpose(0, 3, 1, 2)
        return saida

Para a retropropagação, utiliza-se a função inversa colunas_para_imagem para reconstruir os gradientes na forma original dos dados.

Implementação da Camada de Pooling

A camada de pooling é implementada similarmente, mas com operações de maximização por região:

class CamadaPooling:
    def __init__(self, alt_pool, larg_pool, passo=1, preenchimento=0):
        self.alt_pool = alt_pool
        self.larg_pool = larg_pool
        self.passo = passo
        self.preenchimento = preenchimento

    def forward(self, entrada):
        lotes, canais, alt_entrada, larg_entrada = entrada.shape
        alt_saida = (alt_entrada - self.alt_pool) // self.passo + 1
        larg_saida = (larg_entrada - self.larg_pool) // self.passo + 1

        col_entrada = imagem_para_colunas(entrada, self.alt_pool, self.larg_pool, self.passo, self.preenchimento)
        col_entrada = col_entrada.reshape(-1, self.alt_pool * self.larg_pool)

        indices_maximo = np.argmax(col_entrada, axis=1)
        saida = np.max(col_entrada, axis=1)
        saida = saida.reshape(lotes, alt_saida, larg_saida, canais).transpose(0, 3, 1, 2)
        return saida

Montando uma Rede Neural Convolucional

Uma CNN completa pode ser construída encadeando camadas convolucionais, de pooling e totalmente conectadas. A inicialização requer parâmetros como dimensões de entrada, configurações dos filtros, tamanho das camadas ocultas e desvio padrão para inicialziação de pesos. Os dados são processados em lotes para eficiência computacional, e a retropropagação é aplicada para treinar os pesos via gradiente descendente.

Tags: CNN Python NumPy Redes Neurais deep learning

Publicado em 6-23 00:07