Propagação Direta (Forward Propagation)
Uma vez criada uma rede neural artificial, com o número de camadas definidas, e o número de neurônios em cada camada já determinados, devemos treinar de forma supervisionada essa rede com um conjunto de dados, sendo fornecidos os valores de entrada e suas saídas correspondentes. Esse treinamento consiste em calcular o valor dos pesos, de forma que as saídas calculadas pela rede sejam próximas dos valores usados durante o treinamento.
Inicialmente, os pesos de uma RNA são atribuídos aleatoriamente. A partir desses valores, é realizada a propagação direta de um vetor de entrada. Considere, por exemplo, a RNA abaixo:
Esse rede possui 4 camadas, sendo duas intermediárias (escondidas). São usadas 3 variáveis de entrada, e esses dados são classificados em 4 classes distintas (4 neurônios de saída). As unidades de bias, não estão representadas.
Para o cálculo da propagação direta, consideremos um único dado de treinamento, ou seja, um vetor
x3x1 e um vetor y4x1. Assim: a(1)=x logo z(2)=(Θ(1))Ta(1) a(2)=g(z(2)) adicionando a0 (2) =1 logo z(3)=(Θ(2 ))Ta(2) a(3)=g(z(3)) adicionando a0(3) =1 logo z(4 )=( Θ(3))Ta(3 ) hθ(x )=a (4 ) =g(z(4) )
Onde zj(l) é o valor de entrada e aj(l), é o valor de saída do neurônio j de uma camada l.
A partir das equações acima, podemos calcular a saída hΘ(x) para um vetor de entrada x, lembrando
que a função g(z) é a função sigmoid. Como o treinamento é supervisionado, o valor do vetor hΘ(x)
deve ser próximo do valor do vetor y. Comparando os valores desses dois vetores, é possível atualizar os valores das matrizes de pesos Θ para que esses vetores se aproximem.
Propagação Reversa (Backpropagation)
Para fazer a comparação entre a saída calculada pela propagação reversa de um neurônio (aquela a partir dos valores correntes de Θ) com a saída supervisionada (aquela fornecida pelos dados de
treinamento), definimos δj(l) como o “erro” do neurônio j na camada l. Assim, considerando o
exemplo anterior:
δ(4)j =a(4)j −yj
onde o valor aj(4) é o valor da hipótese hΘ(x)j. Esse erro é calculado para todos os neurônios da
camada de saída.
O erro δ, deve então ser calculado para todos os neurônios de todas as camadas anteriores. Usando os conhecimentos de Cálculo Diferencial e Álgebra Linear, sabemos que:
δ(3 )j =( Θ(3))Tδ(4).∗g '( z(3) )
δ(2 )j =(Θ(2))Tδ(3).∗g ' (z(2)
)
onde g'(z) é a derivada da função de ativação e o .* representa a multiplicação termo a termo entre
duas matrizes, ao invés da multiplicação matricial. Repare que não há o cálculo de δ(1), pois a
primeira camada é o vetor de entrada, e não há uma matriz anterior a essa camada. Devemos, portanto, calcular a derivada da função g(z), que é facilmente determinada por:
g '(z(l )
)=a(l ).∗(1−a(l )
)
Assim, uma vez calculada a propagação direta, calcula-se δ da camada de saída. Com esse valor, calcula-se δ da camada anterior, e assim sucessivamente até a segunda camada. Por isso, o algoritmo é chamado de propapagação reversa (backpropagation).
Uma vez calculado o erro δ de uma camada, devemos atualizar a matriz de pesos imediatamente anterior. Esse procedimento é feito usando algoritmos como o gradiente descendente. Por isso, é necessário calcular a função custo e suas derivadas parciais com relação a cada um dos pesos. É possível provar que, considerando λ = 0 (sem regularização):
∂
∂Θij(l)J (Θ)=aj (l)
δ(il+ 1)
De posse dessas derivadas parciais, e da função custo (apresentada na aula anterior), é possível usar algoritmos otimizados para encontrar o valor ótimo das matrizes Θ.
As equações acima foram ilustradas utilizando apenas um dado de treinamento. Em casos práticos, necessitamos de vários (m) dados para achar pesos mais ajustados. Portanto, o vetor δi deve ser
calculado para cada amostra i dos dados. Com isso, formaremos a matriz Δij, formada por vetores
das amostras i dos dados de treinamento. Essa matriz será usada no cálculo das derivadas parciais da equação acima.
Em resumo, o algoritmo backpropagation é implementado na forma: Atribuir valores iniciais às matrizes de pesos Θ(1) a Θ(L-1);
Iniciar a matriz Δij com zeros;
Para i = 1 a m (dados de treinamento)
Utilizando o vetor x(i), calcular a saída aj(l) para l = 2, 3, … , L; Utilizando as saídas de aj(L) e o vetor y(i), calcular δ(L);
Calcular, de forma reversa, δ(L), δ(L-1), …, δ(2);
Preencher a matriz Δij, na forma
Δij = Δij + aj(l) δi(l+1); Calcular as derivadas parciais:
Dij(l) = 1/m Δij(l) + λΘij(1) (para j ≠ 0)
Dij(l) = 1/m Δij(l) (para j = 0)
Após essas iterações, teremos as derivadas parciais calculadas. Usando a equação para o cálculo da função custo, representada abaixo novamente, é possível usar funções e bibliotecas otimizadas para a atualização dos pesos (p.e. a função fminunc( ) do Matlab/Octave), para isso, basta escrever o código da função que fornece os valores da função custo e de suas derivadas parciais, seguindo o descrito acima. J (θ)=−
[
1 m∑
i=1 m∑
k=1 K yk(i) ⋅log(hΘ(x (i ) ))k+(1− y(ki) )⋅log(1−hΘ(x (i) )k)]
+ λ 2 m∑
l=1 L−1∑
i =1 sl∑
j=1 sl+1 (Θ(jil))2Detalhes de Implementação
Considerando que se deseja implementar o algoritmo backpropagation descrito acima, utilizando a função do Matlab/Octave fminunc( ), alguns detalhes devem ser considerados.
Tal função recebe, em geral três parâmetros. O primeiro deles é um ponteiro para a função que calcula a função custo e suas derivadas parciais. O segundo é um vetor contendo os valores iniciais dos parâmetros a serem otimizados, no caso de uma RNA, as matrizes de pesos. O terceiro é uma variável contendo as opções desejadas para a execução do algoritmo, tais como número máximo de iterações, o uso de gradiente negativo, etc.
Sua função custo, a ser usada em funções otimizadas, deve receber como parâmetro a matriz de pesos e retornar o valor da função custo e de suas derivadas parciais:
function [jVal, gradient] = costFunction(theta) …
…
optTheta = fminunc(@costFunction, initialTheta, options)
Para os casos de regressão linear e logística, os parâmetros θ estão agrupados na forma de vetores, e por isso, a função acima pode ser passada diretamente, sem alterações, para funções como a
fminunc( ). Consequentemente, os valores das derivadas parciais, retornadas na variável gradient,
também são agrupadas em um vetor.
No caso de RNAs, os pesos (parâmetros) são um conjunto de matrizes, assim como as derivadas
parciais, agrupadas na matriz Δij. Entretanto, as funções otimizadas do Matlab/Octave, usam os
parâmetros e derivadas na forma de vetores. Assim, é necessário fazer a conversão de matrizes em vetores, e vice-versa.
A forma de transformar uma matriz A em um vetor, em Matlab/Octave, é usando a expressão:
Avec = A(:);
Dessa forma, é possível transformar todas as matrizes Θ(1), … , Θ(L-1) em um único vetor com a
thetaVec = [theta1(:) ; theta2(:) ; … ];
O mesmo pode ser feito com a matriz de derivadas parciais Δij:
Dvec = [D1(:) ; D2(:) ; … ];
O script da sua função custo deve receber, como descrito, um único vetor com todos os pesos da RNA. A algoritmo interno da sua função, entretanto, para o cálculo da propagação direta e as derivadas parciais na propagação reversa, é viável se forem usadas as matrizes em suas formas originais, possibilitando os laços de repetição e a vetorização das operações matriciais.
Por isso, dentro do algoritmo da função, você deve transformar o vetor único de pesos em suas
várias matrizes. Isso pode ser feito com a função reshape( ) (digite help reshape na tela de comando
do Matlab/Octave para mais detalhes).
Considere o exemplo de uma rede neural que classifica de forma binária (camada de saída com 1 neurônio) e 10 variáveis (camada de entrada com 10 neurônios). Considerando duas camadas escondidas de 10 neurônios cada, podemos definir:
L = 4
s1 = 10; s2 = 10; s3 = 10; s4 = 1 (as unidades de bias não são contabilizadas))
Θ(1)(10 x 11); Θ(2)(10 x 11); Θ(3)(1 x 11);
D(1)(10 x 11); D(2)(10 x 11); D(3)(1 x 11); (onde D(l) é a matriz com as derivadas parciais)
Dentro da sua função, que recebe os pesos em forma de vetor, deve-se transformar tal vetor em um conjunto de matrizes. Isso pode ser feito com:
Theta1 = reshape(thetaVec(1:110),10,11); Theta2 = reshape(thetaVec(111:220),10,11);
Theta3 = reshape(thetaVec(221:231),1,11);
De posse dessas variáveis, é mais viável aplicar as propagações direta e inversa. Lembre que é possível generalizar o código acima para aceitar quaisquer número de camadas e número de neurônios, tornando seu código mais portável. Uma vez calculadas as derivadas parciais, a função deve retorná-las na forma de vetor.
Parâmetros (pesos) Iniciais
Durante o processo de treinamento da sua rede neural, serão calculadas sucessivamente a função custo e suas derivadas parciais. Entretanto, o algoritmo de treinamento inicia com o cálculo da propagação direta, que requer valores iniciais dos pesos da rede.
Intuitivamente, é esperado que esses valores sejam iniciados com zeros, que são os valores mais usados para iniciar variáveis cujos valores são indeterminados. No caso de uma RNA, iniciar os pesos com zeros não é uma boa estratégia.
Ao verificar a propagação dos valores ao longo de uma RNA, pode-se mostrar que, se todos os pesos iniciais são iguais a zero, teremos:
a1 (1) =a2 (1) =⋯=aj (1)
Isso significa que a saída de todos os neurônios da segunda camada serão idênticas. Ao aplicar tais valores na propagação direta para a terceira camada, o efeito se manterá. Uma vez propagada a entrada por toda a rede, a propagação reversa atualizará os pesos para valores, possivelmente diferentes de zero, que serão iguais para conexões que se destinam ao mesmo neurônio.
Consequentemente, atualizar os pesos com valores nulos implica que o número de neurônios nas camadas escondidas se tornam irrelevantes, e a RNA terá uma má performance de classificação. Para resolver esse problema, usa-se a atribuição aleatória de valores para todos os pesos de uma RNA. A aleatoriedade desses valores iniciais é, geralmente, associada a alguma distribuição estatística. A função estatística mais utilizada para isso é a função Gaussiana.
Em outras palavras, os valores dos pesos serão valores aleatórios que obedecem uma distribuição normal.
No Matlab/Octave isso pode ser obtido com a função rand( ), que gera um número aleatório entre 0 e 1. Se forem gerados vários números com a mesma função, esses números obedecerão uma distribuição normal. Caso você deseje que esses números tenham uma extensão de valores diferente, basta usar uma expressão na forma:
Theta = rand(m,n) .* (2*epsilon) – epsilon;
Na expressão acima, a função rand(m,n) cria uma matrix m x n de números aleatórios, segundo uma distribuição gaussiana, entre 0 e 1. O restante da expressão faz com esses números sejam escalonados entre -epsilon e +epsilon.
Dessa forma, garantimos que as saídas dos neurônios de cada camada sejam diferentes entre si, e a atualização dos pesos não seja tendenciosa.