Um tutorial de aprendizado profundo: de perceptrons a redes profundas
Publicados: 2022-03-11Nos últimos anos, houve um ressurgimento no campo da Inteligência Artificial. Ele se espalhou para além do mundo acadêmico com grandes players como Google, Microsoft e Facebook criando suas próprias equipes de pesquisa e fazendo algumas aquisições impressionantes.
Parte disso pode ser atribuído à abundância de dados brutos gerados por usuários de redes sociais, muitos dos quais precisam ser analisados, ao surgimento de soluções avançadas de ciência de dados, bem como ao poder computacional barato disponível via GPGPUs.
Mas além desses fenômenos, esse ressurgimento foi impulsionado em grande parte por uma nova tendência em IA, especificamente em aprendizado de máquina, conhecido como “Deep Learning”. Neste tutorial, apresentarei os principais conceitos e algoritmos por trás do aprendizado profundo, começando com a unidade mais simples de composição e construindo os conceitos de aprendizado de máquina em Java.
(Para divulgação completa: também sou o autor de uma biblioteca de aprendizado profundo Java, disponível aqui, e os exemplos neste artigo são implementados usando a biblioteca acima. Se você gostar, pode apoiá-la dando uma estrela no GitHub , pelo qual eu ficaria grato. As instruções de uso estão disponíveis na página inicial.)
Um tutorial de trinta segundos sobre aprendizado de máquina
Caso você não esteja familiarizado, confira esta introdução ao aprendizado de máquina:
O procedimento geral é o seguinte:
- Temos alguns algoritmos que recebem um punhado de exemplos rotulados, digamos, 10 imagens de cães com o rótulo 1 (“Cachorro”) e 10 imagens de outras coisas com o rótulo 0 (“Não cachorro”) – observe que estamos principalmente aderindo para classificação binária supervisionada para este post.
- O algoritmo “aprende” a identificar imagens de cães e, quando alimentado com uma nova imagem, espera produzir o rótulo correto (1 se for uma imagem de um cachorro e 0 caso contrário).
Essa configuração é incrivelmente geral: seus dados podem ser sintomas e seus rótulos doenças; ou seus dados podem ser imagens de caracteres manuscritos e seus rótulos os caracteres reais que eles representam.
Perceptrons: algoritmos iniciais de aprendizado profundo
Um dos primeiros algoritmos de treinamento supervisionado é o do perceptron, um bloco de construção básico da rede neural.
Digamos que temos n pontos no plano, rotulados como '0' e '1'. Recebemos um novo ponto e queremos adivinhar seu rótulo (isso é semelhante ao cenário “Cachorro” e “Não cachorro” acima). Como fazemos isso?
Uma abordagem pode ser olhar para o vizinho mais próximo e retornar o rótulo desse ponto. Mas uma maneira um pouco mais inteligente de fazer isso seria escolher uma linha que melhor separe os dados rotulados e usá-la como seu classificador.
Nesse caso, cada dado de entrada seria representado como um vetor x = ( x_1, x_2 ) e nossa função seria algo como “'0' se estiver abaixo da linha, '1' se estiver acima”.
Para representar isso matematicamente, deixe nosso separador ser definido por um vetor de pesos w e um deslocamento vertical (ou polarização) b . Então, nossa função combinaria as entradas e os pesos com uma função de transferência de soma ponderada:
O resultado desta função de transferência seria então alimentado em uma função de ativação para produzir uma marcação. No exemplo acima, nossa função de ativação foi um limite de corte (por exemplo, 1 se maior que algum valor):
Treinando o Perceptron
O treinamento do perceptron consiste em alimentá-lo com várias amostras de treinamento e calcular a saída para cada uma delas. Após cada amostra, os pesos w são ajustados de forma a minimizar o erro de saída , definido como a diferença entre as saídas desejadas (alvo) e as reais . Existem outras funções de erro, como o erro quadrático médio, mas o princípio básico do treinamento permanece o mesmo.
Desvantagens do Único Perceptron
A abordagem de um único perceptron para aprendizado profundo tem uma grande desvantagem: ela só pode aprender funções linearmente separáveis. Quão importante é essa desvantagem? Pegue XOR, uma função relativamente simples, e observe que ela não pode ser classificada por um separador linear (observe a tentativa fracassada, abaixo):
Para resolver esse problema, precisaremos usar um perceptron multicamada, também conhecido como rede neural feedforward: na verdade, comporemos vários desses perceptrons juntos para criar um mecanismo mais poderoso de aprendizado.
Redes neurais de feedforward para aprendizado profundo
Uma rede neural é realmente apenas uma composição de perceptrons, conectados de maneiras diferentes e operando em diferentes funções de ativação.
Para começar, veremos a rede neural feedforward, que tem as seguintes propriedades:
- Uma entrada, saída e uma ou mais camadas ocultas . A figura acima mostra uma rede com uma camada de entrada de 3 unidades, camada oculta de 4 unidades e uma camada de saída com 2 unidades (os termos unidades e neurônios são intercambiáveis).
- Cada unidade é um único perceptron como o descrito acima.
- As unidades da camada de entrada servem como entradas para as unidades da camada oculta, enquanto as unidades da camada oculta são entradas para a camada de saída.
- Cada conexão entre dois neurônios tem um peso w (semelhante aos pesos do perceptron).
- Cada unidade da camada t é normalmente conectada a todas as unidades da camada anterior t - 1 (embora você possa desconectá-las configurando seu peso para 0).
- Para processar os dados de entrada, você “grampeia” o vetor de entrada na camada de entrada, definindo os valores do vetor como “saídas” para cada uma das unidades de entrada. Neste caso particular, a rede pode processar um vetor de entrada tridimensional (por causa das 3 unidades de entrada). Por exemplo, se o seu vetor de entrada for [7, 1, 2], você definiria a saída da unidade de entrada superior como 7, a unidade do meio como 1 e assim por diante. Esses valores são então propagados para as unidades ocultas usando a função de transferência de soma ponderada para cada unidade oculta (daí o termo propagação direta), que por sua vez calcula suas saídas (função de ativação).
- A camada de saída calcula suas saídas da mesma forma que a camada oculta. O resultado da camada de saída é a saída da rede.
Além da linearidade
E se cada um de nossos perceptrons só puder usar uma função de ativação linear? Então, a saída final de nossa rede ainda será alguma função linear das entradas, apenas ajustada com uma tonelada de pesos diferentes que são coletados em toda a rede. Em outras palavras, uma composição linear de um monte de funções lineares ainda é apenas uma função linear. Se estivermos restritos a funções de ativação linear, então a rede neural feedforward não é mais poderosa que o perceptron, não importa quantas camadas ele tenha.
Por causa disso, a maioria das redes neurais usa funções de ativação não linear como logística, tanh, binária ou retificadora. Sem eles, a rede só pode aprender funções que são combinações lineares de suas entradas.
Perceptrons de treinamento
O algoritmo de aprendizado profundo mais comum para treinamento supervisionado dos perceptrons multicamadas é conhecido como retropropagação. O procedimento básico:
- Uma amostra de treinamento é apresentada e propagada pela rede.
O erro de saída é calculado, normalmente o erro quadrático médio:
Onde t é o valor alvo e y é a saída real da rede. Outros cálculos de erro também são aceitáveis, mas o MSE é uma boa escolha.
O erro de rede é minimizado usando um método chamado gradiente descendente estocástico.
A descida do gradiente é universal, mas no caso de redes neurais, isso seria um gráfico do erro de treinamento em função dos parâmetros de entrada. O valor ótimo para cada peso é aquele em que o erro atinge um mínimo global . Durante a fase de treinamento, os pesos são atualizados em pequenas etapas (após cada amostra de treinamento ou um mini-lote de várias amostras) de forma que estejam sempre tentando atingir o mínimo global - mas isso não é tarefa fácil, pois você muitas vezes acabam em mínimos locais, como o da direita. Por exemplo, se o peso tiver um valor de 0,6, ele precisa ser alterado para 0,4.
Esta figura representa o caso mais simples, aquele em que o erro depende de um único parâmetro. No entanto, o erro de rede depende de cada peso de rede e a função de erro é muito, muito mais complexa.
Felizmente, a retropropagação fornece um método para atualizar cada peso entre dois neurônios em relação ao erro de saída. A derivação em si é bastante complicada, mas a atualização de peso para um determinado nó tem a seguinte forma (simples):
Onde E é o erro de saída e w_i é o peso da entrada i para o neurônio.
Essencialmente, o objetivo é mover-se na direção do gradiente em relação ao peso i . O termo-chave é, obviamente, a derivada do erro, que nem sempre é fácil de calcular: como você encontraria essa derivada para um peso aleatório de um nó oculto aleatório no meio de uma grande rede?
A resposta: através da retropropagação. Os erros são calculados primeiro nas unidades de saída onde a fórmula é bastante simples (com base na diferença entre os valores alvo e previsto) e, em seguida, propagados de volta pela rede de maneira inteligente, permitindo atualizar nossos pesos com eficiência durante o treinamento e (espero) atingir um mínimo.
Camada oculta
A camada oculta é de particular interesse. Pelo teorema da aproximação universal, uma única rede de camada oculta com um número finito de neurônios pode ser treinada para aproximar uma função aleatória arbitrária. Em outras palavras, uma única camada oculta é poderosa o suficiente para aprender qualquer função. Dito isso, muitas vezes aprendemos melhor na prática com várias camadas ocultas (ou seja, redes mais profundas).
A camada oculta é onde a rede armazena sua representação abstrata interna dos dados de treinamento, semelhante à forma como um cérebro humano (analogia bastante simplificada) tem uma representação interna do mundo real. Avançando no tutorial, veremos diferentes maneiras de brincar com a camada oculta.
Uma rede de exemplo
Você pode ver uma rede neural feedforward simples (4-2-3 camadas) que classifica o conjunto de dados IRIS implementado em Java aqui através do método testMLPSigmoidBP . O conjunto de dados contém três classes de plantas de íris com características como comprimento da sépala, comprimento da pétala, etc. A rede recebe 50 amostras por classe. Os recursos são fixados nas unidades de entrada, enquanto cada unidade de saída corresponde a uma única classe do conjunto de dados: “1/0/0” indica que a planta é da classe Setosa, “0/1/0” indica Versicolor e “ 0/0/1” indica Virgínia). O erro de classificação é de 2/150 (ou seja, classifica incorretamente 2 amostras de 150).
O problema com grandes redes
Uma rede neural pode ter mais de uma camada oculta: nesse caso, as camadas superiores estão “construindo” novas abstrações sobre as camadas anteriores. E, como mencionamos anteriormente, muitas vezes você pode aprender melhor na prática com redes maiores.
No entanto, aumentar o número de camadas ocultas leva a dois problemas conhecidos:
- Desaparecimento de gradientes: à medida que adicionamos mais e mais camadas ocultas, a retropropagação torna-se cada vez menos útil na passagem de informações para as camadas inferiores. Com efeito, à medida que a informação é repassada, os gradientes começam a desaparecer e se tornam pequenos em relação aos pesos das redes.
- Overfitting: talvez o problema central no aprendizado de máquina. Resumidamente, o overfitting descreve o fenômeno de ajustar os dados de treinamento muito próximos, talvez com hipóteses muito complexas. Nesse caso, seu aluno acaba ajustando os dados de treinamento muito bem, mas terá um desempenho muito, muito pior em exemplos reais.
Vejamos alguns algoritmos de aprendizado profundo para resolver esses problemas.
Codificadores automáticos
A maioria das aulas introdutórias de aprendizado de máquina tende a parar com redes neurais de feedforward. Mas o espaço de possíveis redes é muito mais rico, então vamos continuar.
Um autoencoder é tipicamente uma rede neural feedforward que visa aprender uma representação compactada e distribuída (codificação) de um conjunto de dados.
Conceitualmente, a rede é treinada para “recriar” a entrada, ou seja, a entrada e os dados de destino são os mesmos. Em outras palavras: você está tentando produzir a mesma coisa que foi inserida, mas compactada de alguma forma. Esta é uma abordagem confusa, então vamos ver um exemplo.
Comprimindo a entrada: imagens em tons de cinza
Digamos que os dados de treinamento consistem em 28x28 imagens em tons de cinza e o valor de cada pixel é fixado em um neurônio da camada de entrada (ou seja, a camada de entrada terá 784 neurônios). Então, a camada de saída teria o mesmo número de unidades (784) que a camada de entrada e o valor alvo para cada unidade de saída seria o valor em tons de cinza de um pixel da imagem.
A intuição por trás dessa arquitetura é que a rede não aprenderá um “mapeamento” entre os dados de treinamento e seus rótulos, mas aprenderá a estrutura interna e os recursos dos próprios dados. (Por causa disso, a camada oculta também é chamada de detector de recursos .) Normalmente, o número de unidades ocultas é menor que as camadas de entrada/saída, o que força a rede a aprender apenas os recursos mais importantes e obtém uma redução de dimensionalidade.
Na verdade, queremos que alguns pequenos nós no meio realmente aprendam os dados em um nível conceitual, produzindo uma representação compacta que, de alguma forma, capture os principais recursos de nossa entrada.
Doença da gripe
Para demonstrar ainda mais os autoencoders, vejamos mais um aplicativo.
Nesse caso, usaremos um conjunto de dados simples que consiste em sintomas de gripe (crédito a esta postagem do blog pela ideia). Se você estiver interessado, o código para este exemplo pode ser encontrado no método testAEBackpropagation .
Veja como o conjunto de dados é dividido:
- Existem seis recursos de entrada binária.
- Os três primeiros são sintomas da doença. Por exemplo, 1 0 0 0 0 0 indica que este paciente tem temperatura alta, enquanto 0 1 0 0 0 0 indica tosse, 1 1 0 0 0 0 indica tosse e temperatura alta, etc.
- As três características finais são sintomas “contra”; quando um paciente tem um desses, é menos provável que esteja doente. Por exemplo, 0 0 0 1 0 0 indica que este paciente tem uma vacina contra a gripe. É possível ter combinações dos dois conjuntos de características: 0 1 0 1 0 0 indica um paciente vacinado com tosse e assim por diante.
Consideraremos um paciente doente quando tiver pelo menos duas das três primeiras características e saudável se tiver pelo menos duas das três segundas (com desempate em favor dos pacientes saudáveis), por exemplo:
- 111000, 101000, 110000, 011000, 011100 = doente
- 000111, 001110, 000101, 000011, 000110 = saudável
Vamos treinar um autoencoder (usando backpropagation) com seis unidades de entrada e seis unidades de saída, mas apenas duas unidades ocultas .
Após várias centenas de iterações, observamos que quando cada uma das amostras “doentes” é apresentada à rede de aprendizado de máquina, uma das duas unidades ocultas (a mesma unidade para cada amostra “doente”) sempre exibe um valor de ativação mais alto do que o de outros. Ao contrário, quando uma amostra “saudável” é apresentada, a outra unidade oculta tem maior ativação.
Voltando ao aprendizado de máquina
Essencialmente, nossas duas unidades ocultas aprenderam uma representação compacta do conjunto de dados de sintomas da gripe. Para ver como isso se relaciona com a aprendizagem, voltamos ao problema do overfitting. Ao treinar nossa rede para aprender uma representação compacta dos dados, estamos favorecendo uma representação mais simples em vez de uma hipótese altamente complexa que superajuste os dados de treinamento.
De certa forma, ao favorecer essas representações mais simples, estamos tentando aprender os dados em um sentido mais verdadeiro.
Máquinas Boltzmann restritas
O próximo passo lógico é olhar para uma máquina de Boltzmann restrita (RBM), uma rede neural estocástica generativa que pode aprender uma distribuição de probabilidade sobre seu conjunto de entradas .

Os RBMs são compostos de uma camada oculta, visível e de polarização. Ao contrário das redes feedforward, as conexões entre as camadas visíveis e ocultas são não direcionadas (os valores podem ser propagados nas direções visível para oculto e oculto para visível) e totalmente conectadas (cada unidade de uma determinada camada é conectada a cada unidade na próxima — se permitíssemos que qualquer unidade em qualquer camada se conectasse a qualquer outra camada, teríamos uma máquina Boltzmann (em vez de uma máquina Boltzmann restrita ).
O RBM padrão possui unidades binárias ocultas e visíveis: ou seja, a ativação da unidade é 0 ou 1 sob uma distribuição de Bernoulli, mas existem variantes com outras não linearidades.
Embora os pesquisadores já conheçam os RBMs há algum tempo, a recente introdução do algoritmo de treinamento não supervisionado de divergência contrastiva renovou o interesse.
Divergência Contrastiva
O algoritmo de divergência contrastiva de etapa única (CD-1) funciona assim:
- Fase positiva :
- Uma amostra de entrada v é presa à camada de entrada.
- v é propagado para a camada oculta de maneira semelhante às redes feedforward. O resultado das ativações da camada oculta é h .
- Fase negativa :
- Propague h de volta à camada visível com resultado v' (as conexões entre as camadas visível e oculta não são direcionadas e, portanto, permitem o movimento em ambas as direções).
- Propagar o novo v' de volta para a camada oculta com o resultado das ativações h' .
Atualização de peso :
Onde a é a taxa de aprendizado e v , v' , h , h' ew são vetores.
A intuição por trás do algoritmo é que a fase positiva ( h dado v ) reflete a representação interna da rede dos dados do mundo real . Enquanto isso, a fase negativa representa uma tentativa de recriar os dados com base nessa representação interna ( v' dado h ). O objetivo principal é que os dados gerados sejam o mais próximo possível do mundo real e isso se reflete na fórmula de atualização de peso.
Em outras palavras, a rede tem alguma percepção de como os dados de entrada podem ser representados, então ela tenta reproduzir os dados com base nessa percepção. Se sua reprodução não estiver próxima o suficiente da realidade, ele faz um ajuste e tenta novamente.
Voltando à gripe
Para demonstrar a divergência contrastiva, usaremos o mesmo conjunto de dados de sintomas de antes. A rede de teste é uma RBM com seis unidades visíveis e duas ocultas. Vamos treinar a rede usando divergência contrastiva com os sintomas v presos à camada visível. Durante o teste, os sintomas são novamente apresentados à camada visível; em seguida, os dados são propagados para a camada oculta. As unidades ocultas representam o estado doente/saudável, uma arquitetura muito semelhante ao autoencoder (propagando dados da camada visível para a camada oculta).
Após várias centenas de iterações, podemos observar o mesmo resultado dos autoencoders: uma das unidades ocultas tem um valor de ativação maior quando qualquer uma das amostras “doentes” é apresentada, enquanto a outra é sempre mais ativa para as amostras “saudáveis”.
Você pode ver este exemplo em ação no método testContrastiveDivergence .
Redes profundas
Agora demonstramos que as camadas ocultas de autoencoders e RBMs atuam como detectores de recursos eficazes; mas é raro que possamos usar esses recursos diretamente. Na verdade, o conjunto de dados acima é mais uma exceção do que uma regra. Em vez disso, precisamos encontrar uma maneira de usar esses recursos detectados indiretamente.
Felizmente, descobriu-se que essas estruturas podem ser empilhadas para formar redes profundas . Essas redes podem ser treinadas vorazmente, uma camada de cada vez, para ajudar a superar o gradiente de fuga e os problemas de sobreajuste associados à retropropagação clássica.
As estruturas resultantes são muitas vezes bastante poderosas, produzindo resultados impressionantes. Veja, por exemplo, o famoso artigo “gato” do Google, no qual eles usam um tipo especial de autoencoders profundos para “aprender” a detecção de rostos humanos e de gatos com base em dados não rotulados .
Vamos olhar mais de perto.
Autoencoders empilhados
Como o nome sugere, essa rede consiste em vários autoencoders empilhados.
A camada oculta do autoencoder t atua como uma camada de entrada para o autoencoder t + 1 . A camada de entrada do primeiro autoencoder é a camada de entrada para toda a rede. O procedimento de treinamento ganancioso em camadas funciona assim:
- Treine o primeiro autoencoder ( t=1 , ou as conexões vermelhas na figura acima, mas com uma camada de saída adicional) individualmente usando o método backpropagation com todos os dados de treinamento disponíveis.
- Treine o segundo autoencoder t=2 (conexões verdes). Como a camada de entrada para t=2 é a camada oculta de t=1 , não estamos mais interessados na camada de saída de t=1 e a removemos da rede. O treinamento começa prendendo uma amostra de entrada na camada de entrada de t=1 , que é propagada para a camada de saída de t=2 . Em seguida, os pesos (input-hidden e hidden-output) de t=2 são atualizados usando retropropagação. t=2 usa todas as amostras de treinamento, semelhante a t=1 .
- Repita o procedimento anterior para todas as camadas (ou seja, remova a camada de saída do autoencoder anterior, substitua-a por outro autoencoder e treine com retropropagação).
- Os passos 1 a 3 são chamados de pré-treino e deixam os pesos devidamente inicializados. No entanto, não há mapeamento entre os dados de entrada e os rótulos de saída. Por exemplo, se a rede for treinada para reconhecer imagens de dígitos manuscritos, ainda não será possível mapear as unidades do último detector de recursos (ou seja, a camada oculta do último autoencoder) para o tipo de dígito da imagem. Nesse caso, a solução mais comum é adicionar uma ou mais camadas totalmente conectadas à última camada (conexões azuis). A rede inteira agora pode ser vista como um perceptron multicamada e é treinada usando retropropagação (essa etapa também é chamada de ajuste fino ).
Codificadores automáticos empilhados, então, têm tudo a ver com fornecer um método de pré-treinamento eficaz para inicializar os pesos de uma rede, deixando você com um perceptron complexo de várias camadas que está pronto para treinar (ou ajustar ).
Redes de crenças profundas
Assim como acontece com os autoencoders, também podemos empilhar máquinas Boltzmann para criar uma classe conhecida como redes de crenças profundas (DBNs) .
Neste caso, a camada oculta de RBM t atua como uma camada visível para RBM t+1 . A camada de entrada do primeiro RBM é a camada de entrada para toda a rede, e o pré-treinamento de camada gananciosa funciona assim:
- Treine o primeiro RBM t=1 usando divergência contrastiva com todas as amostras de treinamento.
- Treine o segundo RBM t=2 . Como a camada visível para t=2 é a camada oculta de t=1 , o treinamento começa prendendo a amostra de entrada à camada visível de t=1 , que é propagada para a camada oculta de t=1 . Esses dados servem então para iniciar o treinamento de divergência contrastiva para t=2 .
- Repita o procedimento anterior para todas as camadas.
- Semelhante aos autoencoders empilhados, após o pré-treinamento, a rede pode ser estendida conectando uma ou mais camadas totalmente conectadas à camada oculta final do RBM. Isso forma um perceptron de várias camadas que pode ser ajustado usando retropropagação.
Este procedimento é semelhante ao dos autoencoders empilhados, mas com os autoencoders substituídos por RBMs e a retropropagação substituída pelo algoritmo de divergência contrastiva.
(Observação: para saber mais sobre como construir e treinar autoencoders empilhados ou redes de crenças profundas, confira o código de exemplo aqui.)
Redes Convolucionais
Como uma arquitetura final de aprendizado profundo, vamos dar uma olhada nas redes convolucionais, uma classe particularmente interessante e especial de redes feedforward que são muito adequadas ao reconhecimento de imagens.
Antes de olharmos para a estrutura real das redes convolucionais, primeiro definimos um filtro de imagem ou uma região quadrada com pesos associados. Um filtro é aplicado em toda a imagem de entrada e, muitas vezes, você aplicará vários filtros. Por exemplo, você pode aplicar quatro filtros 6x6 a uma determinada imagem de entrada. Então, o pixel de saída com coordenadas 1,1 é a soma ponderada de um quadrado 6x6 de pixels de entrada com o canto superior esquerdo 1,1 e os pesos do filtro (que também é quadrado 6x6). O pixel de saída 2,1 é o resultado do quadrado de entrada com o canto superior esquerdo 2,1 e assim por diante.
Com isso coberto, essas redes são definidas pelas seguintes propriedades:
- As camadas convolucionais aplicam vários filtros à entrada. Por exemplo, a primeira camada convolucional da imagem pode ter quatro filtros 6x6. O resultado de um filtro aplicado na imagem é chamado de mapa de características (FM) e o número de mapas de características é igual ao número de filtros. Se a camada anterior também for convolucional, os filtros serão aplicados em todos os FMs com pesos diferentes, de modo que cada FM de entrada seja conectado a cada FM de saída. A intuição por trás dos pesos compartilhados na imagem é que os recursos serão detectados independentemente de sua localização, enquanto a multiplicidade de filtros permite que cada um deles detecte um conjunto diferente de recursos.
- As camadas de subamostragem reduzem o tamanho da entrada. Por exemplo, se a entrada consistir em uma imagem 32x32 e a camada tiver uma região de subamostragem de 2x2, o valor de saída seria uma imagem 16x16, o que significa que 4 pixels (cada quadrado 2x2) da imagem de entrada são combinados em uma única saída pixel. Existem várias maneiras de subamostragem, mas as mais populares são pooling máximo, pooling médio e pooling estocástico.
- A última camada de subamostragem (ou convolucional) geralmente é conectada a uma ou mais camadas totalmente conectadas, a última das quais representa os dados de destino.
- O treinamento é realizado usando retropropagação modificada que leva em consideração as camadas de subamostragem e atualiza os pesos do filtro convolucional com base em todos os valores aos quais esse filtro é aplicado.
Você pode ver vários exemplos de redes convolucionais treinadas (com retropropagação) no conjunto de dados MNIST (imagens em tons de cinza de letras manuscritas) aqui, especificamente nos métodos testLeNet* (eu recomendaria testLeNetTiny2 , pois atinge uma baixa taxa de erro de cerca de 2% em um período de tempo relativamente curto). Há também uma boa visualização JavaScript de uma rede semelhante aqui.
Implementação
Agora que abordamos as variantes de rede neural mais comuns, pensei em escrever um pouco sobre os desafios apresentados durante a implementação dessas estruturas de aprendizado profundo.
De um modo geral, meu objetivo ao criar uma biblioteca de Deep Learning era (e ainda é) construir uma estrutura baseada em rede neural que satisfizesse os seguintes critérios:
- Uma arquitetura comum capaz de representar diversos modelos (todas as variantes de redes neurais que vimos acima, por exemplo).
- A capacidade de usar diversos algoritmos de treinamento (propagação de retorno, divergência contrastiva, etc.).
- Desempenho decente.
Para satisfazer esses requisitos, adotei uma abordagem em camadas (ou modular) para o design do software.
Estrutura
Vamos começar com o básico:
- NeuralNetworkImpl é a classe base para todos os modelos de rede neural.
- Cada rede contém um conjunto de camadas.
- Cada camada possui uma lista de conexões, onde uma conexão é um link entre duas camadas de forma que a rede seja um grafo acíclico direcionado.
Essa estrutura é ágil o suficiente para ser usada para redes feedforward clássicas, bem como para RBMs e arquiteturas mais complexas como ImageNet.
Também permite que uma camada faça parte de mais de uma rede. Por exemplo, as camadas em uma Deep Belief Network também são camadas em seus RBMs correspondentes.
Além disso, essa arquitetura permite que um DBN seja visto como uma lista de RBMs empilhados durante a fase de pré-treinamento e uma rede feedforward durante a fase de ajuste fino, o que é intuitivamente agradável e programaticamente conveniente.
Propagação de Dados
O próximo módulo cuida da propagação de dados pela rede, um processo de duas etapas:
- Determine a ordem das camadas. Por exemplo, para obter os resultados de um perceptron multicamada, os dados são “fixados” na camada de entrada (portanto, esta é a primeira camada a ser calculada) e propagados até a camada de saída. Para atualizar os pesos durante a retropropagação, o erro de saída deve ser propagado por todas as camadas em ordem de largura, começando pela camada de saída. Isso é feito usando várias implementações de LayerOrderStrategy , que tira proveito da estrutura de grafos da rede, empregando diferentes métodos de travessia de grafos. Alguns exemplos incluem a estratégia de amplitude e o direcionamento de uma camada específica. A ordem é realmente determinada pelas conexões entre as camadas, então as estratégias retornam uma lista ordenada de conexões.
- Calcule o valor de ativação. Cada camada tem uma ConnectionCalculator associada que pega sua lista de conexões (da etapa anterior) e valores de entrada (de outras camadas) e calcula a ativação resultante. Por exemplo, em uma rede feedforward sigmoidal simples, a ConnectionCalculator da camada oculta pega os valores das camadas de entrada e de polarização (que são, respectivamente, os dados de entrada e um array de 1s ) e os pesos entre as unidades (no caso de camadas, os pesos são realmente armazenados em uma conexão FullyConnected como uma Matrix ), calcula a soma ponderada e alimenta o resultado na função sigmoid. As calculadoras de conexão implementam uma variedade de funções de transferência (por exemplo, soma ponderada, convolucional) e ativação (por exemplo, logística e tanh para perceptron multicamada, binário para RBM). A maioria deles pode ser executado em uma GPU usando Aparapi e utilizável com treinamento em mini-lote.
Computação GPU com Aparapi
Como mencionei anteriormente, uma das razões pelas quais as redes neurais ressurgiram nos últimos anos é que seus métodos de treinamento são altamente propícios ao paralelismo, permitindo acelerar significativamente o treinamento com o uso de uma GPGPU. Nesse caso, optei por trabalhar com a biblioteca Aparapi para adicionar suporte a GPU.
O Aparapi impõe algumas restrições importantes nas calculadoras de conexão:
- Apenas arrays unidimensionais (e variáveis) de tipos de dados primitivos são permitidos.
- Somente métodos de membro da própria classe Aparapi Kernel podem ser chamados a partir do código executável da GPU.
Como tal, a maioria dos dados (pesos, matrizes de entrada e saída) é armazenada em instâncias Matrix , que usam matrizes flutuantes unidimensionais internamente. Todas as calculadoras de conexão Aparapi usam AparapiWeightedSum (para camadas totalmente conectadas e funções de entrada de soma ponderada), AparapiSubsampling2D (para camadas de subamostragem) ou AparapiConv2D (para camadas convolucionais). Algumas dessas limitações podem ser superadas com a introdução da Arquitetura de Sistemas Heterogêneos. O Aparapi também permite executar o mesmo código tanto na CPU quanto na GPU.
Treinamento
The training module implements various training algorithms. It relies on the previous two modules. For example, BackPropagationTrainer (all the trainers are using the Trainer base class) uses feedforward layer calculator for the feedforward phase and a special breadth-first layer calculator for propagating the error and updating the weights.
My latest work is on Java 8 support and some other improvements, will soon be merged into master.
Conclusão
The aim of this Java deep learning tutorial was to give you a brief introduction to the field of deep learning algorithms, beginning with the most basic unit of composition (the perceptron) and progressing through various effective and popular architectures, like that of the restricted Boltzmann machine.
The ideas behind neural networks have been around for a long time; but today, you can't step foot in the machine learning community without hearing about deep networks or some other take on deep learning. Hype shouldn't be mistaken for justification, but with the advances of GPGPU computing and the impressive progress made by researchers like Geoffrey Hinton, Yoshua Bengio, Yann LeCun and Andrew Ng, the field certainly shows a lot of promise. There's no better time to get familiar and get involved like the present.
Appendix: Resources
If you're interested in learning more, I found the following resources quite helpful during my work:
- DeepLearning.net: a portal for all things deep learning. It has some nice tutorials, software library and a great reading list.
- An active Google+ community.
- Two very good courses: Machine Learning and Neural Networks for Machine Learning, both offered on Coursera.
- The Stanford neural networks tutorial.