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.