V. Redes neurais


Última atualização: 01 de junho de 2021.

Descrevemos aqui uma classe de métodos de aprendizagem que foi desenvolvida separadamente em diferentes campos: estatística e inteligência artificial, com base em modelos essencialmente idênticos. A idéia central é extrair combinações lineares das entradas como recursos derivados e modelar o alvo como uma função não linear desses recursos. O resultado é um poderoso método de aprendizado, com aplicações difundidas em muitos campos. Primeiro discutimos o modelo de prospeção de projeção, que evoluiu no domínio de estatísticas semiparamétricas e suavização. Depois nos dedicamos aos modelos de redes neurais.


V.1 Regressão por prospeção de projeção


Como em nosso problema genérico de aprendizado supervisionado, suponha que temos um vetor de entrada \(X\) com \(p\) componentes e um \(Y\) de destino. Sejam \(\omega_m\), \(m = 1, 2,\cdots,M\) vetores de dimensão \(p\) unitários de parâmetros desconhecidos. O modelo de regressão por prospeção de projeção (PPR) tem a forma \begin{equation*} f(X) \, = \, \sum_{m=1}^M g_m(\omega_m^\top X)\cdot \end{equation*}

Este é um modelo aditivo, mas nos recursos derivados \(V_m = \omega_ω^\top X\) em vez das próprias entradas. As funções \(g_m\) não são especificadas e são estimadas junto com as direções \(\omega_m\) usando algum método de suavização flexível, veja abaixo.

Figura V.1. Gráficos em perspectiva de quatro funções de crista.

> library(lattice) > x = expand.grid(x1 = seq(0,2, by = 0.01), x2 = seq(0,0.45, by = 0.01)) > x$V = (x$x1+x$x2)/sqrt(2) > x$f = 1/(1+exp(-5*(x$V-0.5))) > par(mfrow=c(1,1), mar=c(3,2,1,0)+.5, mgp=c(1.6,.6,0), pch=19) > wireframe(f ~ x1*x2, data = x, xlab = expression(X[1]), ylab = expression(X[2]), zlab = "g(V)", shade = TRUE, aspect = c(1, 0.8), light.source = c(5,0,10)) > x = expand.grid(x1 = seq(0,1.3, by = 0.01), x2 = seq(0,0.45, by = 0.01)) > x$V = x$x1 > x$f = (x$V+0.1)*sin(1/(x$V/3+0.1)) > par(mfrow=c(1,1), mar=c(3,2,1,0)+.5, mgp=c(1.6,.6,0), pch=19) > wireframe(f ~ x1*x2, data = x, xlab = expression(X[1]), ylab = expression(X[2]), zlab = "g(V)", shade = TRUE, aspect = c(1, 0.8), light.source = c(5,0,10))

A função \(g_m(\omega_m^\top X)\) é chamada de função de crista em \(\mathbb{R}^p\). Ela varia apenas na direção definida pelo vetor \(\omega_m\). A variável escalar \(V_m = \omega_ω^\top X\) é a projeção de \(X\) no vetor unitário \(\omega_m\), e buscamos \(\omega_m\) para que o modelo se ajuste bem, daí o nome busca de projeção. A Figura V.1 mostra alguns exemplos de funções de crista. No exemplo à esquerda \(\omega = (1/\sqrt{2}) (1,1)^\top\), de forma que a função só varia na direção \(X_1+X_2\). No exemplo à direita, \(\omega = (1,0)\).

O modelo PPR é muito geral, uma vez que a operação de formação de funções não lineares de combinações lineares gera uma classe surpreendentemente grande de modelos. Por exemplo, o produto \(X_1\times X_2\) pode ser escrito como \begin{equation*} \dfrac{1}{4}\big( (X_1+X_2)^2-(X_1-X_2)^2\big) \end{equation*} e produtos de ordem superior podem ser representados de forma semelhante.

Na verdade, se \(M\) for considerado arbitrariamente grande, para a escolha apropriada de \(g_m\), o modelo PPR pode se aproximar arbitrariamente de qualquer função contínua em \(\mathbb{R}^p\). Essa classe de modelos é chamada de aproximador universal. No entanto, essa generalidade tem um preço. A interpretação do modelo ajustado geralmente é difícil, porque cada entrada entra no modelo de uma forma complexa e multifacetada. Como resultado, o modelo PPR &ecute; mais útil para previsão e não muito útil para produzir um modelo compreensível para os dados. O modelo \(M = 1\), conhecido como modelo de índice único em econometria, é uma exceção. É um pouco mais geral do que o modelo de regressão linear e oferece uma interpretação semelhante.

Como ajustamos um modelo PPR, dados os dados de treinamento \((x_i, y_i)\), \(i = 1, 2,\cdots, N\)? Procuramos os minimizadores aproximados da função de erro \begin{equation} \sum_{i=1}^N \left( y_i-\sum_{m=1}^M g_m\big(\omega_m^\top x_i\big)\right)^2 \end{equation} sobre as funções \(g_m\) e vetores de direção \(\omega_m\), \(m = 1,2,\cdots, M\).

Como em outros problemas de suavização, precisamos explicitamente ou implicitamente impor restrições de complexidade ao \(g_m\), para evitar soluções de ajuste excessivo.

Considere apenas um termo, \(M = 1\) e elimine o subscrito. Dado o vetor de direção \(\omega\), formamos as variáveis derivadas \(v_i = \omega^\top x_i\). Então, temos um problema de suavização unidimensional e podemos aplicar qualquer gráfico de dispersão mais suave, como um spline de suavização, para obter uma estimativa de \(g\).

Por outro lado, dado \(g\), queremos minimizar a função de erro em \(\omega\). Uma busca de Gauss-Newton é conveniente para essa tarefa. Este é um método quase Newton, no qual a parte do Hessiano envolvendo a segunda derivada de \(g\) é descartada. Ele pode ser simplesmente derivado da seguinte forma. Seja \(\omega_{old}\) a estimativa atual para \(\omega\). Nós escrevemos \begin{equation*} g(\omega^\top x_i) \approx g(\omega_{old}^\top x_i)+g'(\omega_{old}^\top x_i)(\omega-\omega_{old})^\top x_i \end{equation*} para obtermos \begin{equation*} \sum_{i=1}^N \big(y_i-g(\omega^\top x_i)\big)^2 \approx \sum_{i=1}^N g'(\omega_{old}^\top x_i)^2 \left( \omega_{old}^\top x_i +\dfrac{y_i-g(\omega_{old}^\top x_i)}{g'(\omega_{old}^\top x_i)} - \omega^\top x_i\right)^2\cdot \end{equation*}

Para minimizar o lado direito, realizamos uma regressão de mínimos quadrados com alvo \begin{equation*} \omega_{old}^\top x_i +\big(y_i-g(\omega_{old}^\top x_i)\big)/{g'(\omega_{old}^\top x_i)} \end{equation*} na entrada \(x_i\), com pesos \(g'(\omega_{old}^\top x_i)^2\) e sem termo de intercepto ou viés. Isso produz o vetor de coeficientes atualizado \(\omega_{new}\).

Essas duas etapas, estimativas de \(g\) e \(\omega\), são iteradas até a convergência. Com mais de um termo no modelo PPR, o modelo é construído de uma maneira avançada de estágio, adicionando um par \((\omega_m, g_m)\) em cada estágio.

Existem vários detalhes de implementação.

Existem muitas outras aplicações, como estimação de densidades (Friedman et al., 1984; Friedman, 1987), onde a ideia de busca de projeção pode ser usada. No entanto, o modelo de regressão de busca por projeção não tem sido amplamente utilizado no campo da estatística, talvez porque, na época de sua introdução (1981), suas demandas computacionais excediam as capacidades da maioria dos computadores disponíveis. Mas representa um importante avanço intelectual, que floresceu em sua reencarnação no campo das redes neurais, o tópico do restante deste capítulo.


V.2 Redes neurais


Rede neural ou rede neural artificial tem a capacidade de aprender por meio de exemplos, é um modelo de processamento de informações inspirado no sistema biológico de neurônios. É composto por um grande número de elementos de processamento altamente interconectados, conhecidos como neurônios, para resolver problemas. Ele segue o caminho não linear e processa as informações em paralelo em todos os nós. Uma rede neural é um sistema adaptativo complexo. Adaptável significa que ele tem a capacidade de alterar sua estrutura interna ajustando os pesos das entradas.

O termo rede neural evoluiu para abranger uma grande classe de modelos e métodos de aprendizagem. Aqui descrevemos a rede neural mais usada, às vezes chamada de rede de propagação reversa de camada oculta. Como deixaremos claro são apenas modelos estatísticos não-lineares, muito parecido com o modelo de prospeção de projeção discutido antes.

Uma rede neural é um modelo de regressão ou classificação de dois estágios, tipicamente representado por um diagrama de rede, como na Figura V.2. Essa rede se aplica tanto à regressão quanto à classificação. Para regressão, normalmente \(K = 1\) e há apenas uma unidade de saída \(Y_1\) no topo. No entanto, essas redes podem lidar com várias respostas quantitativas de maneira perfeita, por isso lidaremos com o caso geral.

A rede neural foi projetada para resolver problemas fáceis para os humanos e difíceis para as máquinas, como a identificação de fotos de cães e gatos, a identificação de fotos numeradas. Esses problemas são freqüentemente chamados de reconhecimento de padrões. Sua aplicação abrange desde o reconhecimento óptico de caracteres até a deteção de objetos.

Para a classificação \(K\)-classes, existem \(K\) unidades no topo, com a \(k\)-ésima unidade modelando a probabilidade da classe \(k\). Existem \(K\) medições de alvo \(Y_k\), \(k = 1,\cdots, K\), sendo cada uma codificada como uma variável 0 - 1 para a \(k\)-classe.

Os recursos derivados \(Z_m\) são criados a partir de combinações lineares das entradas e, em seguida, o alvo \(Y_k\) é modelado como uma função das combinações lineares do \(Z_m\), \begin{equation} \begin{array}{rclc} Z_m & = & \sigma(\alpha_{0m}+\alpha^\top_m X), & m=1,\cdots,M, \\ T_k & = & \beta_{0k} \, + \, \beta_k^\top Z, & k=1,\cdots,K, \\ f_k(X) & = & g_k(T), & k=1,\cdots,K, \end{array} \end{equation} onde \(Z=(Z_1,Z_2,\cdots,Z_M)\) e \(T=(T_1,T_2,\cdots,T_K)\).

Figura V.2. Esquema de uma única camada oculta, rede neural feed-forward.

A função de ativação \(\sigma(\nu)\) é geralmente escolhida como sendo o sigmóide \(\sigma(\nu) = 1 / (1 + e^{-\nu})\); veja na Figura V.3 abaixo para uma plotagem desta curva. Às vezes, as funções de base radial gaussiana são usadas para o \(\sigma(\nu)\), produzindo o que é conhecido como uma rede de função de base radial.

Diagramas de rede neural, como a Figura V.2, às vezes são desenhados com uma unidade adicional de polarização alimentando cada unidade nas camadas oculta e de saída. Pensando na constante 1 como um recurso de entrada adicional, esta unidade de polarização captura os interceptos \(\alpha_{0m}\) e \(\beta_{0k}\) no modelo acima.

A função de saída \(g_k(T)\) permite uma transformação final do vetor de saídas \(T\). Para a regressão, normalmente escolhemos a função de identidade \(g_k(T) = T_k\). Os primeiros trabalhos na classificação da classe \(K\) também usaram a função identidade, mas isso foi posteriormente abandonado em favor da função softmax \begin{equation*} g_k(T) \, = \, \dfrac{e^{T_k}}{\sum_{\ell=1}^K e^{T_\ell}}\cdot \end{equation*}

É claro que essa é exatamente a transformação usada no modelo multilogit e produz estimativas positivas que somam um. Outras funções de ativação linear podem ser utilizadas.

As unidades no meio da rede, calculando os recursos derivados \(Z_m\), são chamadas de unidades ocultas porque os valores \(Z_m\) não são observados diretamente. Em geral, pode haver mais de uma camada oculta. Podemos pensar nos \(Z_m\) como uma expansão básica das entradas originais \(X\); a rede neural é então um modelo linear padrão, ou modelo multilogit linear, usando essas transformações como entradas. Há, entretanto, um importante aprimoramento sobre as técnicas de expansão da base, os parâmetros das funções básicas são aprendidos a partir dos dados.

Figura V.3. Gráfico da função sigmóide \(\sigma(\nu) = 1/(1 + \exp(−\nu))\) (curva vermelha), comumente usada na camada oculta de uma rede neural. Estão incluídos \(\sigma(s\nu)\) para \(s = 1/2\) (curva azul) e \(s = 10\) (curva verde). O parâmetro de escala \(s\) controla a taxa de ativação, e podemos ver que \(s\) grande equivale a uma ativação forte em \(\nu = 0\). Observe que \(\sigma \big(s (\nu - \nu_0)\big)\) muda o limite de ativação de 0 para \(\nu_0\).

> x = seq(-10,10,by=0.01) > f1 = 1/(1+exp(-x)) > par(mfrow=c(1,1), mar=c(3,3,1,0)+.5, mgp=c(1.6,.6,0), pch=19) > plot(x, f1, type="l", col = "red", lwd = 2, xlab = expression(nu), ylab = expression(1/(1+e^{-nu}))) > grid() > s = 1/2 > f2 = 1/(1+exp(-s*x)) > lines(x, f2, col = "blue", lwd = 2) > s = 10 > f3 = 1/(1+exp(-s*x)) > lines(x, f3, col = "green", lwd = 2)

Observe que se \(\sigma\) for a fun&cceil;ão identidade, o modelo inteiro se reduz a um modelo linear nas entradas. Portanto, uma rede neural pode ser considerada uma generalização não linear do modelo linear, tanto para regressão quanto para classificação. Ao introduzir a transformação não linear \(\sigma\), ela amplia muito a classe de modelos lineares. Na Figura V.3 vemos que a taxa de ativação do sigmóide depende da norma de \(\alpha_m\), e se \(||\alpha_m||\) for muito pequena, a unidade estará de fato operando na parte linear de sua função de ativação.

Observe também que o modelo de rede neural com uma camada oculta tem exatamente a mesma forma que o modelo de busca de projeção descrito acima. A diferença é que o modelo PPR usa funções não paramétricas \(g_m(\nu)\), enquanto a rede neural usa uma função muito mais simples baseada em \(\sigma(\nu)\), com três parâmetros livres em seu argumento. Em detalhes, vendo o modelo de rede neural como um modelo PPR, identificamos \begin{equation*} \begin{array}{rcl} g_m(\omega_m^\top X) & = & \beta_m \sigma\big( \alpha_{0m}+\alpha_m^\top X\big)\\[0.4em] & = & \beta_m \sigma\big( \alpha_{0m}+||\alpha_m||(\omega_m^\top X)\big), \end{array} \end{equation*} onde \(\omega_m = \alpha_m/||\alpha_m||\) é o \(m\)-ésimo vetor unitário.

Uma vez que \begin{equation*} \sigma_{\beta,\alpha_0,s}(\nu) = \beta \sigma(\alpha_0 + s\nu) \end{equation*} tem menor complexidade do que um \(g\) não paramétrico mais geral \(g(\nu)\), não é surpreendente que uma rede neural possa usar 20 ou 100 dessas funções, enquanto o modelo PPR normalmente usa menos termos, \(M = 5\) ou 10, por exemplo.

Finalmente, notamos que o nome redes neurais deriva do fato de que foram inicialmente desenvolvidas como modelos para o cérebro humano. Cada unidade representa um neurónio e as conexões, links na Figura V.2, representam sinapses. Nos primeiros modelos, os neurónios disparavam quando o sinal total passado para aquela unidade excedia um certo limite. No modelo acima, isso corresponde ao uso de uma função degrau para \(\sigma(Z)\) e \(g_m(T)\). Mais tarde, a rede neural foi reconhecida como uma ferramenta útil para modelagem estatística não linear e, para esse propósito, a função degrau não é suave o suficiente para otimização. Conseqüentemente, a função degrau foi substituída por uma função de limite mais suave, a sigmóide na Figura V.3.


V.3 Ajuste de redes neurais


O modelo de rede neural tem parâmetros desconhecidos, geralmente chamados de pesos e buscamos valores para eles que fazem o modelo se encaixar bem nos dados de treinamento. Denotamos o conjunto completo de pesos por \(\theta\), que consiste em \begin{array}{rcl} \{\alpha_{0m}, \alpha_m \, ; \, m=1,2,\cdots,M\} & M(p+1) & \mbox{pesos} \\ \{\beta_{0k},\beta_k \, : \, k=1,2,\cdots,M\} & K(M+1) & \mbox{pesos} \end{array}

Para a regressão, usamos a soma dos quadrados dos erros como nossa medida de ajuste, considerada a função de erro: \begin{equation} R(\theta) \, = \, \sum_{k=1}^K\sum_{i=1}^N \big(y_{ik}-f_k(x_i) \big)^2\cdot \end{equation}

Para classificação; usamos o erro quadrático, entropia cruzada ou desvio: \begin{equation} R(\theta) \, = \, -\sum_{k=1}^K\sum_{i=1}^N y_{ik}\log\big(f_k(x_i) \big), \end{equation} e o classificador correspondente é \(G(x) = \mbox{argmax}_k f_k(x)\). Com a função de ativação softmax e a função de erro de entropia cruzada, o modelo de rede neural é exatamente um modelo de regressão logística linear nas unidades ocultas e todos os parâmetros são estimados por máxima verossimilhança.

Geralmente, não queremos o minimizador global de \(R(\theta)\), pois é provável que seja uma solução de ajuste excessivo. Em vez disso, é necessária alguma regularização: isso é obtido diretamente por meio de uma pena ou indiretamente por meio de interrupção antecipada. Os detalhes são fornecidos na próxima seção.

A abordagem genérica para minimizar \(R(\theta)\) é por gradiente descendente, chamado de retropropagação neste cenário. Por causa da forma composicional do modelo, o gradiente pode ser facilmente derivado usando a regra da cadeia para diferenciação. Isso pode ser calculado por uma varredura para frente e para trás na rede, mantendo o controle apenas das quantidades locais de cada unidade.

Aqui está a retropropagação em detalhes para perda de erro quadrático. Seja \(z_{mi}=\sigma(\alpha_{0m}+\alpha_m^\top x_i)\), como definido antes e \(z_i=(z_{1i},z_{2i},\cdots,z_{Mi})\). Temos então \begin{equation} R(\theta) \, = \, \sum_{i=1}^N R_i \, = \, \sum_{i=1}^N \sum_{k=1}^K \big(y_{ik}-f_k(x_i) \big)^2, \end{equation} com derivadas \begin{array}{rcl} \dfrac{\partial R_i}{\partial\beta_{km}} & = & -2\big(y_{ik}-f_k(x_i) \big)g'_k\big(\beta_k^\top z_i\big)z_{mi},\\[0.6em] \dfrac{\partial R_i}{\partial\alpha_{m\ell}} & = & \displaystyle -\sum_{k=1}^K 2\big(y_{ik}-f_k(x_i) \big)g'_k\big(\beta_k^\top z_i\big)\beta_{km}\sigma'\big(\alpha_m^\top x_i\big)x_{i\ell}\cdot \end{array}

Dadas essas derivadas, uma atualização de gradiente descendente na iteração \((r+1)\)-ésima tem a forma \begin{array}{rcl} \beta_{km}^{(r+1)} & = & \displaystyle \beta_{km}^{(r)}-\gamma_r\sum_{i=1}^N \dfrac{\partial R_i}{\partial\beta_{km}^{(r)}},\\[0.6em] \alpha_{m\ell}^{(r+1)} & = & \displaystyle \alpha_{m\ell}^{(r)}-\gamma_r\sum_{i=1}^N \dfrac{\partial R_i}{\partial\alpha_{m\ell}^{(r)}}, \end{array} onde \(\gamma_r\) é a taxa de aprendizado, discutida abaixo.

Agora escrevamos as derivadas como \begin{array}{rcl} \dfrac{\partial R_i}{\partial\beta_{km}} & = & \delta_{ki} z_{mi},\\ \dfrac{\partial R_i}{\partial\alpha_{m\ell}} & = & s_{mi}x_{i\ell}\cdot \end{array}

As quantidades \(\delta_{ki}\) e \(s_{mi}\) são erros do modelo atual nas unidades de saída e da camada oculta, respectivamente. A partir de suas definições, esses erros satisfazem \begin{equation} s_{mi} \, = \, \sigma'\big(\alpha_m^\top x_i\big)\sum_{k=1}^K \beta_{km}\delta_{ki}, \end{equation} conhecidas como equações de propagação reversa.

Usando isso, as atualizações podem ser implementadas com um algoritmo de duas passagens. No passo para frente, os pesos atuais são fixos e os valores previstos \(\widehat{f}_k(x_i)\) são calculados a partir das funções das combinações lineares apresentadas no começo da Seção V.2. Na passagem para trás, os erros \(\delta_{ki}\) são calculados e, em seguida, propagados de volta por meio de \(\sigma'\big(\alpha_m^\top x_i\big)\sum_{k=1}^K \beta_{km}\delta_{ki}\) para fornecer os erros \(s_{mi}\). Ambos os conjuntos de erros são então usados para calcular os gradientes para as atualizações de \(\beta_{km}^{(r+1)}\) e \(\alpha_{m\ell}^{(r+1)}\), via \(\partial R_i/\partial\beta_{km}\) e \(\partial R_i/\partial\alpha_{m\ell}\).

Esse procedimento de duas passagens é conhecido como propagação reversa. Também foi chamada de regra delta (Widrow and Hoff, 1960). Os componentes computacionais para entropia cruzada têm a mesma forma que aqueles para a função de erro da soma dos quadrados e são derivados num exercício.

As vantagens da propagação reversa são sua natureza simples e local. No algoritmo de propagação de retorno, cada unidade oculta passa e recebe informações apenas de e para unidades que compartilham uma conexão. Portanto, ele pode ser implementado de forma eficiente em um computador de arquitetura paralela.

As atualizações em \(\beta_{km}^{(r+1)}\) e \(\alpha_{m\ell}^{(r+1)}\) são um tipo de aprendizagem em lote, com as atualizações de parâmetros sendo uma soma de todos os casos de treinamento. O aprendizado também pode ser realizado online - processando cada observação, uma de cada vez, atualizando o gradiente após cada caso de treinamento e percorrendo os casos de treinamento várias vezes. Nesse caso, as somas nas equações que definem as atualizações \(\beta_{km}^{(r+1)}\) e \(\alpha_{m\ell}^{(r+1)}\) são substituídas por uma única soma. Uma época de treinamento se refere a uma varredura em todo o conjunto de treinamento. O treinamento online permite que a rede lide com conjuntos de treinamento muito grandes e também atualize os pesos conforme novas observações surgem.

A taxa de aprendizado \(\gamma_r\) para aprendizado em lote é geralmente considerada uma constante e também pode ser otimizada por uma pesquisa de linha que minimiza a função de erro a cada atualização. Com o aprendizado online, \(\gamma_r\) deve diminuir para zero conforme a iteração \(r \to\infty\). Esse aprendizado é uma forma de aproximação estocástica (Robbins and Munro, 1951); os resultados neste campo garantem a convergência se \(\gamma_r\to 0\), \(\sum_r \gamma_r = \infty\) e \(\sum_r \gamma_r^2 <\infty\), satisfeito, por exemplo, por \(\gamma_r = 1/r\).

A propagação reversa pode ser muito lenta e, por esse motivo, geralmente não é o método escolhido. Técnicas de segunda ordem, como o método de Newton, não são atraentes aqui, porque a matriz derivada de \(R(\theta)\), o Hessiano, pode ser muito grande. Melhores abordagens para o ajuste incluem gradientes conjugados e métodos métricos variáveis. Isso evita o cálculo explícito da segunda matriz de derivadas, embora ainda forneça uma convergência mais rápida.

Ajustamos redes neurais e registramos o erro de teste médio \begin{equation} \mbox{E}_{\mbox{Teste}}\big(Y-\widehat{f}(X)\big)^2\cdot \end{equation} Escolhemos àquela com o menor erro de teste possível.


V.4 Alguns problemas no treinamento de redes neurais


O treinamento de redes neurais é uma arte e tanto. O modelo geralmente é superparametrizado e o problema de otimização não é convexo e instável, a menos que certas diretrizes sejam seguidas. Nesta seção, resumimos algumas das questões importantes.


V.4.1 Valores iniciais


Observe que, se os pesos estão próximos de zero, a parte operativa do sigmóide é aproximadamente linear e, portanto, a rede neural colapsa em um modelo aproximadamente linear. Normalmente, os valores iniciais dos pesos são escolhidos para serem valores aleatórios próximos de zero. Portanto, o modelo começa quase linear e se torna não linear à medida que os pesos aumentam. Unidades individuais localizam em direções e introduzem não linearidades onde necessário. O uso de pesos zero exatos leva a derivadas zero e simetria perfeita e o algoritmo nunca se move. Em vez disso, começar com grandes pesos geralmente leva a soluções ruins


V.4.2 Sobreajuste


Freqüentemente, as redes neurais têm muitos pesos e superdimensionam os dados no mínimo global de \(R(\theta)\). Nos primeiros desenvolvimentos de redes neurais, seja por projeto ou por acidente, uma regra de parada antecipada foi usada para evitar o superajuste. Aqui, treinamos o modelo apenas por um tempo e paramos bem antes de nos aproximarmos do mínimo global. Como os pesos começam em uma soluçõo altamente regularizada linear, isso tem o efeito de reduzir o modelo final em direção a um modelo linear. Um conjunto de dados de validação é útil para determinar quando parar, uma vez que esperamos que o erro de validação comece a aumentar.

Um método mais explícito para regularização é o declínio do peso, que é análogo à regressão de crista usada para modelos lineares. Adicionamos uma penalidade à função de erro \(R(\theta)+\lambda J(\theta)\), onde \begin{equation} J(\theta) \, = \, \sum_{km}\beta^2_{km}+\sum_{m\ell} \alpha^2_{m\ell}, \end{equation} e \(\lambda\geq 0\) é um parâmetro de ajuste.

Valores maiores de \(\lambda\) tenderão a reduzir os pesos até zero: normalmente a validação cruzada é usada para estimar \(\lambda\). O efeito da penalidade é simplesmente adicionar os termos \(2\beta_{km}\) e \(2\alpha_{m\ell}\) às respectivas expressões de gradiente \(\beta_{km}^{(r+1)}\) e \(\alpha_{m\ell}^{(r+1)}\). Outras formas de penalidade foram propostas, por exemplo, \begin{equation} J(\theta) \, = \, \sum_{km} \dfrac{\beta_{km}^2}{1+\beta_{km}^2}+\sum_{m\ell} \dfrac{\alpha_{m\ell}^2}{1+\alpha_{m\ell}^2}, \end{equation} conhecido como penalidade de eliminação de peso. Isso tem o efeito de reduzir pesos menores mais do que \(J(\theta)\) anterior.


V.4.3 Dimensionamento das entradas


Como a escala das entradas determina a escala efetiva dos pesos na camada inferior, ela pode ter um grande efeito na qualidade da solução final. No início, é melhor padronizar todas as entradas para ter média zero e desvio padrão um. Isso garante que todas as entradas sejam tratadas igualmente no processo de regularização e permite escolher um intervalo significativo para os pesos iniciais aleatórios. Com entradas padronizadas, é típico obter pesos uniformes aleatórios no intervalo \((-0.7, +0.7)\).


V.4.4 Número de unidades e camadas ocultas


De modo geral, é melhor ter muitas unidades ocultas do que poucas. Com muito poucas unidades ocultas, o modelo pode não ter flexibilidade suficiente para capturar as não linearidades nos dados; com muitas unidades ocultas, os pesos extras podem ser reduzidos a zero se a regularização apropriada for usada. Normalmente, o número de unidades ocultas está em algum lugar na faixa de 5 a 100, com o número aumentando com o número de entradas e o número de casos de treinamento. É mais comum derrubar um número razoavelmente grande de unidades e treiná-las com regularização.

Alguns pesquisadores usam a validação cruzada para estimar o número ideal, mas isso parece desnecessário se a validação cruzada for usada para estimar o parâmetro de regularização. A escolha do número de camadas ocultas é guiado pelo conhecimento prévio e pela experimentação. Cada camada extrai recursos da entrada para regressão ou classificação. O uso de várias camadas ocultas permite a construção de recursos hierárquicos em diferentes níveis de resolução.


V.4.5 Múltiplos mínimos


A função de erro \(R(\theta)\) é não convexa, possuindo muitos mínimos locais. Como resultado, a solução final obtida é bastante dependente da escolha dos pesos iniciais. Deve-se tentar pelo menos uma série de configurações iniciais aleatórias e escolher a solução que dá o erro mais baixo penalizado. Provavelmente, a melhor abordagem é usar as previsões médias sobre a coleção de redes como a previsão final (Ripley, 1996). Isso é preferível à média dos pesos, uma vez que a não linearidade do modelo implica que essa solução meacute;dia pode ser muito pobre. Outra abordagem é por meio de bagging, que calcula a média das previsões de treinamento de redes a partir de versões perturbadas aleatoriamente dos dados de treinamento.


V.5 Exemplos



Exemplo 1: previsão do valor mediano de casas ocupadas em Boston

Usaremos o conjunto de dados Boston no pacote MASS. Este conjunto de dados é uma coleção de dados sobre valores habitacionais nos subúrbios de Boston. Nosso objetivo é prever o valor mediano das casas ocupadas pelo proprietário (medv) usando todas as outras variáveis contínuas disponíveis.

> set.seed(500) > library(MASS) > dados <- Boston > head(dados) crim zn indus chas nox rm age dis rad tax ptratio black lstat medv 1 0.00632 18 2.31 0 0.538 6.575 65.2 4.0900 1 296 15.3 396.90 4.98 24.0 2 0.02731 0 7.07 0 0.469 6.421 78.9 4.9671 2 242 17.8 396.90 9.14 21.6 3 0.02729 0 7.07 0 0.469 7.185 61.1 4.9671 2 242 17.8 392.83 4.03 34.7 4 0.03237 0 2.18 0 0.458 6.998 45.8 6.0622 3 222 18.7 394.63 2.94 33.4 5 0.06905 0 2.18 0 0.458 7.147 54.2 6.0622 3 222 18.7 396.90 5.33 36.2 6 0.02985 0 2.18 0 0.458 6.430 58.7 6.0622 3 222 18.7 394.12 5.21 28.7

O conjunto de dados de Boston possui 506 linhas e 14 colunas, as colunas contém as seguintes informações:

Primeiro, precisamos verificar se nenhum ponto de dados está faltando, caso contrário, precisamos consertar o conjunto de dados.

> apply(dados,2,function(x) sum(is.na(x))) crim zn indus chas nox rm age dis rad tax ptratio black lstat medv 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Não há dados faltando, ótimo. Prosseguimos dividindo aleatoriamente os dados em um grupo de treinamento e um conjunto de teste, então ajustamos um modelo de regressão linear e o testamos no conjunto de teste. Observe que estou usando a função glm() em vez de lm(). Isso se tornará útil mais tarde, na validação cruzada do modelo linear.

> index <- sample(1:nrow(dados),round(0.75*nrow(dados))) > index [1] 359 411 431 437 498 169 287 236 47 42 54 146 452 19 362 204 38 497 441 224 2 304 406 [24] 496 140 102 457 262 49 358 326 27 293 106 449 334 120 456 349 21 183 217 365 430 284 186 [47] 308 303 434 438 346 158 88 200 190 229 329 39 246 5 167 412 96 232 481 331 178 208 454 [70] 453 55 391 369 132 473 294 394 60 17 380 274 476 316 23 113 53 28 414 446 29 233 305 [93] 94 73 343 122 281 488 283 320 338 263 440 322 10 37 350 166 421 26 261 41 138 264 218 [116] 225 368 201 220 506 383 100 297 141 31 486 398 147 442 216 22 143 161 159 428 323 194 299 [139] 89 407 336 126 226 347 222 84 422 339 172 8 86 342 389 282 242 474 170 98 280 136 209 [162] 15 472 179 300 114 90 231 267 180 30 260 354 213 35 464 462 269 25 214 110 379 390 445 [185] 499 278 386 251 302 129 286 78 332 40 77 483 115 13 377 142 290 148 197 34 237 289 429 [208] 492 433 356 117 184 135 243 123 439 461 410 192 259 335 144 239 67 76 271 273 234 207 455 [231] 130 388 133 409 193 97 443 56 328 206 18 318 81 266 257 381 254 80 105 426 32 71 36 [254] 252 405 425 324 66 413 57 374 333 375 450 315 92 196 65 490 205 52 247 295 370 275 337 [277] 427 344 357 444 69 131 202 212 494 292 156 215 387 345 319 85 348 313 288 191 168 307 203 [300] 327 487 189 276 493 46 163 392 48 58 95 265 485 397 219 50 121 103 75 448 154 108 62 [323] 72 125 250 238 83 291 495 248 187 14 364 272 258 360 469 311 11 489 460 395 44 137 382 [346] 12 408 480 451 505 182 400 404 61 68 376 419 325 385 134 111 321 351 268 477 253 502 198 [369] 484 104 424 174 87 188 70 435 301 151 165 277 > train <- dados[index,] > test <- dados[-index,] > lm.fit <- glm(medv~., data=train) > summary(lm.fit) Call: glm(formula = medv ~ ., data = train) Deviance Residuals: Min 1Q Median 3Q Max -15.2113 -2.5587 -0.6552 1.8275 29.7110 Coefficients: Estimate Std. Error t value Pr(>|t|) (Intercept) 31.111702 5.459811 5.698 2.49e-08 *** crim -0.111372 0.033256 -3.349 0.000895 *** zn 0.042633 0.014307 2.980 0.003077 ** indus 0.001483 0.067455 0.022 0.982473 chas 1.756844 0.981087 1.791 0.074166 . nox -18.184847 4.471572 -4.067 5.84e-05 *** rm 4.760341 0.480472 9.908 < 2e-16 *** age -0.013439 0.014101 -0.953 0.341190 dis -1.553748 0.218929 -7.097 6.65e-12 *** rad 0.288181 0.072017 4.002 7.62e-05 *** tax -0.013739 0.004060 -3.384 0.000791 *** ptratio -0.947549 0.140120 -6.762 5.38e-11 *** black 0.009502 0.002901 3.276 0.001154 ** lstat -0.388902 0.059733 -6.511 2.47e-10 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 (Dispersion parameter for gaussian family taken to be 20.23806) Null deviance: 32463.5 on 379 degrees of freedom Residual deviance: 7407.1 on 366 degrees of freedom AIC: 2237 Number of Fisher Scoring iterations: 2 > pr.lm <- predict(lm.fit,test) > MSE.lm <- sum((pr.lm - test$medv)^2)/nrow(test) > MSE.lm [1] 31.26302

A função sample(x, size) simplesmente produz um vetor do tamanho especificado de amostras selecionadas aleatoriamente do vetor x. Por padrão, a amostragem é sem reposição: o índice é essencialmente um vetor aleatório de indeces. Como estamos lidando com um problema de regressão, vamos usar o erro quadrático médio (MSE) como uma medida de quanto nossas previsões estão longe dos dados reais.

Antes de ajustar uma rede neural, alguns preparativos precisam ser feitos. As redes neurais não são tão fáceis de treinar e ajustar. Como uma primeira etapa, vamos abordar o pré-processamento de dados.

É uma boa prática normalizar seus dados antes de treinar uma rede neural. Não consigo enfatizar o suficiente a importância dessa etapa: dependendo do seu conjunto de dados, evitar a normalização pode levar a resultados inúteis ou a um processo de treinamento muito difícil, na maioria das vezes o algoritmo não convergirá antes do número máximo de iterações permitidas. Você pode escolher diferentes métodos para dimensionar os dados: normalização \(z\), escala mín-máx, etc. . Escolhi usar o método min-max e dimensionar os dados no intervalo \([0,1]\). Geralmente escalar nos intervalos \([0,1]\) ou \([-1,1]\) tende a dar melhores resultados. Portanto, dimensionamos e dividimos os dados antes de prosseguir:

> maxs <- apply(dados, 2, max) > maxs crim zn indus chas nox rm age dis rad tax 88.9762 100.0000 27.7400 1.0000 0.8710 8.7800 100.0000 12.1265 24.0000 711.0000 ptratio black lstat medv 22.0000 396.9000 37.9700 50.0000 > mins <- apply(dados, 2, min) > mins crim zn indus chas nox rm age dis rad 0.00632 0.00000 0.46000 0.00000 0.38500 3.56100 2.90000 1.12960 1.00000 tax ptratio black lstat medv 187.00000 12.60000 0.32000 1.73000 5.00000 > scaled <- as.data.frame(scale(dados, center = mins, scale = maxs - mins)) > head(scaled) crim zn indus chas nox rm age dis rad tax 1 0.0000000000 0.18 0.06781525 0 0.3148148 0.5775053 0.6416066 0.2692031 0.00000000 0.20801527 2 0.0002359225 0.00 0.24230205 0 0.1728395 0.5479977 0.7826982 0.3489620 0.04347826 0.10496183 3 0.0002356977 0.00 0.24230205 0 0.1728395 0.6943859 0.5993821 0.3489620 0.04347826 0.10496183 4 0.0002927957 0.00 0.06304985 0 0.1502058 0.6585553 0.4418126 0.4485446 0.08695652 0.06679389 5 0.0007050701 0.00 0.06304985 0 0.1502058 0.6871048 0.5283213 0.4485446 0.08695652 0.06679389 6 0.0002644715 0.00 0.06304985 0 0.1502058 0.5497222 0.5746653 0.4485446 0.08695652 0.06679389 ptratio black lstat medv 1 0.2872340 1.0000000 0.08967991 0.4222222 2 0.5531915 1.0000000 0.20447020 0.3688889 3 0.5531915 0.9897373 0.06346578 0.6600000 4 0.6489362 0.9942761 0.03338852 0.6311111 5 0.6489362 1.0000000 0.09933775 0.6933333 6 0.6489362 0.9929901 0.09602649 0.5266667 > train_ <- scaled[index,] > test_ <- scaled[-index,]

Observe que scale retorna uma matriz que precisa ser alocada numa data.frame.

Ajuste de redes neurais utilizando a libraria neuralnet

Pelo que sabemos, não existe uma regra fixa sobre quantas camadas e neurônios usar, embora existam várias regras básicas mais ou menos aceitas. Normalmente, se necessário, uma camada oculta é suficiente para um grande número de aplicações. No que diz respeito ao número de neurônios, ele deve estar entre o tamanho da camada de entrada e o tamanho da camada de saída, geralmente 2/3 do tamanho de entrada. Pelo menos em minha breve experiência, testar repetidas vezes é a melhor solução, pois não há garantia de que qualquer uma dessas regras se encaixará melhor em seu modelo.

Como este é um exemplo simples, usaremos 2 camadas ocultas com esta configuração: 13: 5: 3: 1. A camada de entrada tem 13 entradas, as duas camadas ocultas têm 5 e 3 neurônios e a camada de saída tem, é claro, uma única saída, já que estamos fazendo a regressão. Vamos ajustar a rede:

> library(neuralnet) > n <- names(train_) > f <- as.formula(paste("medv ~", paste(n[!n %in% "medv"], collapse = " + "))) > f medv ~ crim + zn + indus + chas + nox + rm + age + dis + rad + tax + ptratio + black + lstat > nn <- neuralnet(f,data=train_,hidden=c(5,3),linear.output=T)

Algumas observações:

O pacote neuralnet fornece uma boa ferramenta para traçar o modelo:

> plot(nn)

Esta é a representação gráfica do modelo com os pesos em cada conexão:

As linhas pretas mostram as conexões entre cada camada e os pesos em cada conexão, enquanto as linhas azuis mostram o termo de polarização adicionado em cada etapa. O viés pode ser pensado como o intercepto de um modelo linear. A rede é essencialmente uma caixa preta, então não podemos dizer muito sobre o encaixe, os pesos e o modelo. Basta dizer que o algoritmo de treinamento convergiu e, portanto, o modelo está pronto para ser usado.

Beck, (2013) descreve o gráfico acima como “deixando muito a desejar”. Essas parcelas são difíceis de trabalhar. Visualmente, eles sofrem de um sério problema de desordem, tornando os pesos de interação praticamente ilegíveis. Além disso, a função plot.nn tem alguns comportamentos indesejáveis. No ambiente RStudio, os gráficos aparecem como pop-outs em vez de na janela do visualizador integrado. A função também não parece ser compatível com o knitr, então as imagens acima foram exportadas manualmente e incluídas como arquivos .png estáticos.

Esta questão foi abordada pelo Diagrama de Interpretação Neural (NID) descrito em Olden & Jackson, (2002) e implementado na biblioteca NeuralNetTools, Beck, (2015) como a função plotnet(). Isso fornece os seguintes gráficos padrão para os mesmos modelos.

> library(NeuralNetTools) > plotnet(nn)

Este gráfico elegante resolve o problema de desordem visual usando a espessura da linha para representar a magnitude do peso e a cor da linha para representar o sinal do peso (preto = positivo, cinza = negativo). Outros recursos úteis incluem uma opção de cor para a camada de entrada para que, por exemplo, a importância variável possa ser representada. Isso será discutido no final.

> pr.nn <- predict(nn,test_[,1:13]) > pr.nn_ <- pr.nn*(max(dados$medv)-min(dados$medv))+min(dados$medv) > test.r <- (test_$medv)*(max(dados$medv)-min(dados$medv))+min(dados$medv) > MSE.nn <- sum((test.r - pr.nn_)^2)/nrow(test_) > MSE.nn [1] 16.45955

A função de ativação padrão é a logística. Utilizamos agora uma outra função de ativação:

> softplus <- function(x) log(1 + exp(x)) > nn1 <- neuralnet(f,data=train_,hidden=c(5,3),linear.output=T,act.fct = softplus) > pr.nn1 <- predict(nn1,test_[,1:13]) > pr.nn1_ <- pr.nn1*(max(dados$medv)-min(dados$medv))+min(dados$medv) > MSE.nn1 <- sum((test.r - pr.nn1_)^2)/nrow(test_) > MSE.nn1 [1] 22.89761
porém o erro de estimação é maior. Descaratamos esta ideia e permanecemos com o modelo com a função de ativação padrão.

Agora podemos tentar prever os valores para o conjunto de teste e calcular o MSE. Lembre-se de que a rede produzirá uma previsão normalizada, portanto, precisamos redimensioná-la para fazer uma comparação significativa ou apenas uma previsão simples.

> print(paste(MSE.lm,MSE.nn)) [1] "31.2630222372615 16.4595537665717"

Aparentemente, a rede está fazendo um trabalho melhor do que o modelo linear na previsão do medv. Mais uma vez, tome cuidado porque esse resultado depende da divisão treino-teste realizada acima. A seguir, após o gráfico visual, faremos uma validação cruzada rápida para termos mais confiança nos resultados.

Uma primeira abordagem visual para o desempenho da rede e do modelo linear no conjunto de teste é apresentada abaixo:

> par(mfrow=c(1,2), mar=c(3,2,1,0)+.5, mgp=c(1.6,.6,0), pch=19) > plot(test$medv,pr.nn_,col='red',main='Dados reais vs preditos NN', xlim=c(0,60), ylim=c(0,60),pch=18,cex=0.7) > abline(0,1,lwd=2) > legend('bottomright',legend='NN',pch=18,col='red', bty='n') > plot(test$medv,pr.lm,col='blue',main='Dados reais vs preditos lm', xlim=c(0,60), ylim=c(0,60),pch=18, cex=0.7) > abline(0,1,lwd=2) > legend('bottomright',legend='LM',pch=18,col='blue', bty='n', cex=.95)

Ao inspecionar visualmente o gráfico, podemos ver que as previsões feitas pela rede neural são, em geral, mais concentradas em torno da linha; um alinhamento perfeito com a linha indicaria um MSE de 0 e, portanto, uma previsão perfeita ideal; do que aquelas feitas por o modelo linear.

Uma comparação visual talvez mais útil é mostrada abaixo:

> par(mfrow=c(1,1), mar=c(3,2,1,0)+.5, mgp=c(1.6,.6,0), pch=19) > plot(test$medv,pr.nn_,col='red',main='Dados reais vs preditos NN', xlim=c(0,60), ylim=c(0,60), pch=18, cex=0.7) > points(test$medv,pr.lm,col='blue',pch=18,cex=0.7) > abline(0,1,lwd=2) > legend('bottomright',legend=c('NN ','LM '),pch=18,col=c('red','blue'))

Validação cruzada

A validação cruzada é outra etapa muito importante na construção de modelos preditivos. Embora existam diferentes tipos de métodos de validação cruzada, a ideia básica é repetir o seguinte processo várias vezes:

> library(boot) > set.seed(200) > lm.fit <- glm(medv~.,data=dados) > cv.glm(data,lm.fit,K=10)$delta[1] [1] 23.17094 > set.seed(450) > cv.error <- NULL > k <- 1000 > library(plyr) > pbar <- create_progress_bar('text') > pbar$init(k) | | 0% > for(i in 1:k){ index <- sample(1:nrow(dados),round(0.9*nrow(dados))) train.cv <- scaled[index,] test.cv <- scaled[-index,] nn <- neuralnet(f,data=train.cv,hidden=c(5,2),linear.output=T) pr.nn <- predict(nn,test.cv[,1:13]) pr.nn <- pr.nn*(max(dados$medv)-min(dados$medv))+min(dados$medv) test.cv.r <- (test.cv$medv)*(max(dados$medv)-min(dados$medv))+min(dados$medv) cv.error[i] <- sum((test.cv.r - pr.nn)^2)/nrow(test.cv) pbar$step() } |========================================================================================| 100% > mean(cv.error) [1] 13.1364 > boxplot(cv.error,xlab='MSE CV',col='cyan', border='blue',names='CV error (MSE)', main='CV error (MSE) para NN',horizontal=TRUE)

Como podemos ver, o MSE médio da rede neural (13.14) é inferior ao do modelo linear embora pareça haver um certo grau de variação nos MSEs da validação cruzada. Isso pode depender da divisão dos dados ou da inicialização aleatória dos pesos na rede. Ao executar a simulação em tempos diferentes com sementes diferentes, podemos obter uma estimativa pontual mais precisa para o MSE médio.

As redes neurais se parecem muito com caixas pretas: explicar seu resultado é muito mais difícil do que explicar o resultado de um modelo mais simples, como um modelo linear. Portanto, dependendo do tipo de aplicação de que se precise, convém levar em consideração esse fator também. Além disso, como vemos acima, é necessário cuidado extra para ajustar uma rede neural e pequenas mudanças podem levar a resultados diferentes.



Exemplo 2: Dados bancários

Os dados referem-se a campanhas de marketing direto (telefonemas) de uma instituição bancária portuguesa. O objetivo da classificação é prever se o cliente fará um depósito a prazo, variável resposta.

Os dados referem-se a campanhas de marketing direto de uma instituição bancária portuguesa. As campanhas de marketing foram baseadas em ligações telefônicas. Muitas vezes, era necessário mais de um contato para o mesmo cliente, para acessar se o produto, depósito bancário a prazo, seria "sim" ou "não".

Existem quatro conjuntos de dados:

Os menores conjuntos de dados são fornecidos para testar algoritmos de aprendizado de máquina mais exigentes computacionalmente (por exemplo, SVM).

O objetivo da classificação é prever se o cliente irá subscrever ou não (sim/não) um depósito a prazo (variável y).

Variáveis ​​de entrada no banco de dados do cliente:

Este conjunto de dados está disponível ao público para pesquisa. Os detalhes estão descritos no artigo
S.Moro, P.Cortez e P. Rita. A Data-Driven Approach to Predict the Success of Bank Telemarketing. Decision Support Systems, Elsevier, 62:22-31, June 2014.

Artigo relevante relacionado:
S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM'2011, pp. 117-121, Guimaraes, Portugal, October, 2011. EUROSIS.

Este e outros bancoa de dados estão disponíveis em
Machine Learning Repository, Center for Machine Learning and Intelligent System.

> banco = read.csv("http://leg.ufpr.br/~lucambio/Nonparam/bank.csv", sep = ";", header = T) > str(banco) 'data.frame': 4521 obs. of 17 variables: $ age : int 30 33 35 30 59 35 36 39 41 43 ... $ job : chr "unemployed" "services" "management" "management" ... $ marital : chr "married" "married" "single" "married" ... $ education: chr "primary" "secondary" "tertiary" "tertiary" ... $ default : chr "no" "no" "no" "no" ... $ balance : int 1787 4789 1350 1476 0 747 307 147 221 -88 ... $ housing : chr "no" "yes" "yes" "yes" ... $ loan : chr "no" "yes" "no" "yes" ... $ contact : chr "cellular" "cellular" "cellular" "unknown" ... $ day : int 19 11 16 3 5 23 14 6 14 17 ... $ month : chr "oct" "may" "apr" "jun" ... $ duration : int 79 220 185 199 226 141 341 151 57 313 ... $ campaign : int 1 1 1 4 1 2 1 2 2 1 ... $ pdays : int -1 339 330 -1 -1 176 330 -1 -1 147 ... $ previous : int 0 4 1 0 0 3 2 0 0 2 ... $ poutcome : chr "unknown" "failure" "failure" "unknown" ... $ y : chr "no" "no" "no" "no" ...

Observemos nossa base de dados. Fizemos um tabulamento da quantidade de observações em cada categoria dos fatores.

> table(banco$job) admin. blue-collar entrepreneur housemaid management retired self-employed 478 946 168 112 969 230 183 services student technician unemployed unknown 417 84 768 128 38 > table(banco$marital) divorced married single 528 2797 1196 > table(banco$education) primary secondary tertiary unknown 678 2306 1350 187 > table(banco$housing) no yes 1962 2559 > table(banco$loan) no yes 3830 691 > table(banco$contact) cellular telephone unknown 2896 301 1324 > table(banco$month) apr aug dec feb jan jul jun mar may nov oct sep 293 633 20 222 148 706 531 49 1398 389 80 52 > table(banco$poutcome) failure other success unknown 490 197 129 3705

Como as redes neurais usam funções de ativação entre -1 e +1 é importante reduzir suas variáveis. Caso contrário, a rede neural terá que gastar iterações de treinamento fazendo esse dimensionamento para você.

> #Min Max Normalização > banco$age <- (banco$age-min(banco$age))/(max(banco$age)-min(banco$age)) > banco$balance <-(banco$balance-min(banco$balance))/(max(banco$balance)-min(banco$balance)) > banco$campaign <- (banco$campaign-min(banco$campaign))/(max(banco$campaign)-min(banco$campaign)) > banco$previous <- (banco$previous-min(banco$previous))/(max(banco$previous)-min(banco$previous))

Para representar variáveis de fatores, precisamos convertê-las em variáveis dummy. Uma variável dummy transforma os N valores distintos e os converte em N-1 variáveis. Usamos N-1 porque o valor final é representado por todos os valores fictícios definidos como zero.

Para qualquer linha, uma ou nenhuma das variáveis dummy estará ativa com um (1) ou inativa com zero (0).

Em nosso conjunto de dados bancários, a variável educação tem quatro valores distintos com "primário" (primary) sendo o caso base, ou seja, o primeiro nível. Para tornar esta variável de fator útil para o pacote neuralnet, precisamos usar a função model.matrix ().

> head(model.matrix(~education, data=banco)) (Intercept) educationsecondary educationtertiary educationunknown 1 1 0 0 0 2 1 1 0 0 3 1 0 1 0 4 1 0 1 0 5 1 1 0 0 6 1 0 1 0

A função model.matrix divide a variável education em todos os valores possíveis, exceto o caso base. Ele adiciona uma variável de Intercept que, eventualmente, abandonaremos. Se quiser incluir esse valor espec7iacute;fico, você precisa relevel() seus dados.

> banco$education <- relevel(banco$education, ref = "secondary") > head(model.matrix(~factor(education), data=banco)) (Intercept) factor(education)primary factor(education)tertiary factor(education)unknown 1 1 1 0 0 2 1 0 0 0 3 1 0 1 0 4 1 0 1 0 5 1 0 0 0 6 1 0 1 0

Depois de decidir sobre todas as nossas variáveis numéricas e de fator, podemos chamar a função model.matrix uma última vez.

> banco_matrix <- model.matrix(~ age + job + marital + education + default + balance + housing + loan + poutcome + campaign + previous + y, data=banco)

Agora temos uma matriz com 28 colunas, 27 excluindo o Intercept. Usando essa nova variável, podemos começar a construir nossa rede neural.

Antes de chegarmos à construção do modelo, precisamos ter certeza de que todos os nomes das colunas são entradas de modelo aceitáveis. Parece que duas colunas têm caracteres especiais e precisamos consertar isso antes de inseri-las em um modelo.

> colnames(banco_matrix) [1] "(Intercept)" "age" "jobblue-collar" "jobentrepreneur" [5] "jobhousemaid" "jobmanagement" "jobretired" "jobself-employed" [9] "jobservices" "jobstudent" "jobtechnician" "jobunemployed" [13] "jobunknown" "maritalmarried" "maritalsingle" "educationprimary" [17] "educationtertiary" "educationunknown" "defaultyes" "balance" [21] "housingyes" "loanyes" "poutcomeother" "poutcomesuccess" [25] "poutcomeunknown" "campaign" "previous" "yyes" > colnames(banco_matrix)[3] <- "jobbluecollar" > colnames(banco_matrix)[8] <- "jobselfemployed"

Agora que todos os nomes das colunas foram limpos, precisamos colocá-los em uma fórmula. Temos que combinar os nomes das colunas (separados por um símbolo de adição) e, em seguida, voltar à variável de resposta.

> col_list <- paste(c(colnames(banco_matrix[,-c(1,28)])),collapse="+") > col_list <- paste(c("yyes~",col_list),collapse="") > f <- formula(col_list) > f yyes ~ age + jobbluecollar + jobentrepreneur + jobhousemaid + jobmanagement + jobretired + jobselfemployed + jobservices + jobstudent + jobtechnician + jobunemployed + jobunknown + maritalmarried + maritalsingle + educationprimary + educationtertiary + educationunknown + defaultyes + balance + housingyes + loanyes + poutcomeother + poutcomesuccess + poutcomeunknown + campaign + previous

Finalmente, estamos prontos para usar esta fórmula em nossos modelos.

Modelo

Com a complexidade das redes neurais, há muitas opções a serem exploradas no pacote neuralnet. Vamos começar com os parâmetros padrão e, em seguida, explorar algumas opções diferentes.

> library(neuralnet) > set.seed(7896129) > nmodel <- neuralnet(f, data=banco_matrix, hidden=1, threshold = 0.01, learningrate.limit = NULL, learningrate.factor = list(minus = 0.5, plus = 1.2), algorithm = "rprop+") > library(NeuralNetTools) > plotnet(nmodel)

> pr.nmodel <- predict(nmodel,banco_matrix[,1:27]) > MSE.nmodel <- sum((banco_matrix[,"yyes"] - pr.nmodel)^2)/nrow(banco_matrix) > MSE.nmodel [1] 0.0900607
erro de estimação.

Uma rede neural com um único nó oculto não é nada melhor do que uma combinação linear, na verdade. Para alterar o número de nós ocultos, simplesmente usamos o parâmetro hidden.

> nmodel1 <- neuralnet(f, data=banco_matrix, hidden=c(1,2), threshold = 0.01, learningrate.limit = NULL, learningrate.factor = list(minus = 0.5, plus = 1.2), algorithm = "rprop+") > pr.nmodel1 <- predict(nmodel1,banco_matrix[,1:27]) > MSE.nmodel1 <- sum((banco_matrix[,"yyes"] - pr.nmodel1)^2)/nrow(banco_matrix) > MSE.nmodel1 [1] 0.08829215
conseguimos assim melhorar um pouco o erro de estimação.

> nmodel2 <- neuralnet(f, data=banco_matrix, hidden=c(4,2), threshold = 0.01, learningrate.limit = NULL, learningrate.factor = list(minus = 0.5, plus = 1.2), algorithm = "rprop+") > pr.nmodel2 <- predict(nmodel2,banco_matrix[,1:27]) > MSE.nmodel2 <- sum((banco_matrix[,"yyes"] - pr.nmodel2)^2)/nrow(banco_matrix) > MSE.nmodel2 [1] 0.07598427
ainda conseguimos diminuir um pouco mais o erro de estimação.

A melhor solução que encontrei considera apenas manter a taxa de aprendizagem relativamente pequena.

> nmodel3 <- neuralnet(f, data=banco_matrix, hidden=c(15,3), threshold = 0.1, learningrate.limit = NULL, learningrate.factor = list(minus = 0.5, plus = 1.2), algorithm = "rprop+") > pr.nmodel3 <- predict(nmodel3,banco_matrix[,1:27]) > MSE.nmodel3 <- sum((banco_matrix[,"yyes"] - pr.nmodel3)^2)/nrow(banco_matrix) > MSE.nmodel3 [1] 0.05154181

Uma outra alternativa é mostrada a seguir; desta maneir conseguimos diminuir em 41% o erro de estimação.

> nmodel4 <- neuralnet(f, data=banco_matrix, hidden=c(15,10), threshold = 0.1, learningrate.limit = NULL, learningrate.factor = list(minus = 0.5, plus = 1.2), algorithm = "rprop+") > pr.nmodel4 <- predict(nmodel4,banco_matrix[,1:27]) > MSE.nmodel4 <- sum((banco_matrix[,"yyes"] - pr.nmodel4)^2)/nrow(banco_matrix) > MSE.nmodel4 [1] 0.05312376 > (MSE.nmodel-MSE.nmodel4)/MSE.nmodel [1] 0.4101339

Por padrão, o algoritmo neuralnet usando o algoritmo Resilient Backpropogation (RPROP+). Isso requer um learningrate.limit, ou seja, um vetor ou uma lista contendo o limite inferior e superior para a taxa de aprendizagem e um learningrate.factor, um vetor ou uma lista contendo os fatores de multiplicação para a taxa de aprendizagem superior e inferior.

Agora você sabe quase tudo sobre como usar o pacote neuralnet em R. Existem muitos parâmetros diferentes para mexer e você pode gerar alguns layouts de rede neural complicados com alguns comandos simples.