Perceptron Múltiplas Camadas

Author

Jessica Kubrusly

Perceptron Multiplas Camadas

O perceptron multicamadas é a combinação de vários neurônios artificiais organizados em camadas. A ideia principal é, partindo de uma rede perceptron de um único neurônio, e em vez de “imputar” os valores de cováveis neste neurônio vão entrar as saídas de outras rede perceptron de camada única.

Vamos começar com o perceptron de camada única. Supondo 3 variáveis de entrada, são 4 parâmetros desconhecidos para serem estimados.

Figure 1: Perceptron camada única

Suponha agora que em vez desse único neurônio receber os valores das 3 covariáveis da base ele receba as saídas de outros perceptrons de camada única, e estes sim são alimentados pelos valores das covariáveis. A rede, que antes tinha apenas um neurônio na camada de saída, agora possui uma camada oculra com mais 3 neurônios, total de 4 neurônios na rede. Esta rede possui 16 parâmetros desconhecidos.

Figure 2: Perceptron com 1 camada oculta de 3 neurônios

A camada oculta pode ter quantos neurônios a gente quiser. O exemplo da figura abaixo apresenta uma rede com uma camada oculta com 4 neurônios, que resulta em 21 parâmetros desconhecidos.

Figure 3: Perceptron com 1 camada oculta de 4 neurônios

Também podemos incluir quantas novas camadas quisermos. Para o exemplo da figura abaixo, a rede possui 2 camadas ocultas, a primeira com 4 neurônios e a segunda com 3. Para esta rede temos 31 parâmetros desconhecidos.

Figure 4: Perceptron com 2 camadas ocultas

A Arquitetura

Em comparação com o Perceptron camada única, a arquitetura dos perceptrons de múltiplas camadas são bem mais complexas.

Chamamos de camada de entrada a camada da rede representada pelas covariáveis, que é entendida como uma camada visível. Chamamos de camada de saída a última camada, com o(s) neurônio(s) de saída (já explico o caso de mais de um neurônio na saída), que também é visível. Por fim, chamamos de camadas ocultas todas as outras camadas da rede.

Figure 5: Arquitetura Perceptron Múltiplas Camadas

O número de parâmetros desconhecidos da rede depende do tamanho e da arquitetura da rede. Quanto mais camadas ocultas, mais parâmetros teremos e mais complexo o modelo é. É a complexidade das redes que capturam melhor padrões não lineares dos dados.

O Modelo Matemático

O modelo matemático, que define \(\hat{y}\) como função das covariáveis, considerando valores para os parâmetros, também complica muito com a inclusão de camadas ocultas e mais neurônios.

Primeiro, vejamos como é o modelo matemático para a rede neural Perceptron Camada Única.

\[ \hat{y}_i = g(w_1x_{i,1} + w_2x_{i,2} + w_3x_{i,3} + \Theta) \] Se colocarmos uma camada oculta com três neurônios a função que retorna o valor de \(\hat{y}_i\) em termos das covariáveis muda.

\[ \begin{aligned} \hat{y}_i = g\Big( &w_{10}g(w_1x_{i,1} + w_4x_{i,2} + w_7x_{i,3} + \Theta_1) \\ + \ &w_{11}g(w_2x_{i,1} + w_5x_{i,2} + w_8x_{i,3} + \Theta_2) \\ + \ &w_{12}g(w_3x_{i,1} + w_6x_{i,2} + w_9x_{i,3} + \Theta_3) + \Theta_4\Big) \end{aligned} \] Para essa expressão considerei a numeração dos pesos \(w\) de forma sequencial, da esquerda para a direita, de cima para baixo. O mesmo para a numeração dos limiares aditivos \(\Theta\).

Para complicar ainda mais, vejamos o caso de 2 camadas ocultas, sendo a primeira com 4 e a segunda com 3 neurônios.

Perceptron com 2 camadas ocultas

\[ \begin{aligned} \hat{y}_i = g\Big( w_{25} g\Big( &w_{13} g(w_1x_{i,1} + w_5x_{i,2} + w_9x_{i,3} + \Theta_1) \\ + \ &w_{16} g(w_2x_{i,1} + w_6x_{i,2} + w_{10}x_{i,3} + \Theta_2) \\ + \ &w_{19} g(w_3x_{i,1} + w_7x_{i,2} + w_{11}x_{i,3} + \Theta_3) \\ + \ &w_{22} g(w_4x_{i,1} + w_8x_{i,2} + w_{12}x_{i,3} + \Theta_4) + \Theta_5\Big) \\ + w_{26}g\Big( &w_{14} g(w_1x_{i,1} + w_5x_{i,2} + w_9x_{i,3} + \Theta_1) \\ + \ &w_{17} g(w_2x_{i,1} + w_6x_{i,2} + w_{10}x_{i,3} + \Theta_2) \\ + \ &w_{20} g(w_3x_{i,1} + w_7x_{i,2} + w_{11}x_{i,3} + \Theta_3) \\ + \ &w_{23} g(w_4x_{i,1} + w_8x_{i,2} + w_{12}x_{i,3} + \Theta_4) + \Theta_6\Big) \\ + w_{27}g\Big( &w_{15} g(w_1x_{i,1} + w_5x_{i,2} + w_9x_{i,3} + \Theta_1) \\ + \ &w_{18} g(w_2x_{i,1} + w_6x_{i,2} + w_{10}x_{i,3} + \Theta_2) \\ + \ &w_{21} g(w_3x_{i,1} + w_7x_{i,2} + w_{11}x_{i,3} + \Theta_3) \\ + \ &w_{24} g(w_4x_{i,1} + w_8x_{i,2} + w_{12}x_{i,3} + \Theta_4) + \Theta_7\Big) + \Theta_8\Big) \end{aligned} \]

Com isso podemos imaginar o quanto complexo seria tentar minimizar a função de custo a partir do gradiente descendente. Precisaríamos, por exemplo, derivar essa função em termos de cada parâmetro desconhecido. Isso não será possível e será necessário outro método iterativo. A forma de estimar os parâmetros será vista na próxima aula. Antes disso, vejamos um exemplo de como treinar no R uma rede Perceptron com camadas ocultas.

Um exemplo de regressão

Vamos continuar o exemplo dos dados de seguro. O objetivo é o mesmo de antes: criar um modelo de redes neurais para prever o gasto de um segurado. Vamos considerar a base de treino já preparada em aulas anteriores (NAs excluídas, variáveis quantitativas padronizados e qualitativas transformadas em binárias/indicadoras).

Vamos aqui comparar diferentes arquiteturas com todas as variáveis menos as de região. Vamos usar também a função de ativação logística, por isso devemos usar a base onde a variável resposta foi transformada para o intervalo \([0,1]\).

Leitura da base de treino

Primeiro vamos carregar os pacotes necessários e a base de treino, que já está pronta, com as variáveis quantitativas padronizadas e as qualitativas transformadas em indicadoras.

library(tidyverse)
library(neuralnet)
base_treino_final= readRDS("base_treino_final_log.RDS")

Treinamento dos modelos

Vamos treinar diferentes modelos, com diferentes arquiteturas. Todos terão as mesmas variáveis independentes: age, sexmale, bmi, children e smokeryes e busca prever o valor de charges.

Vamos chamar de modelo_0 o modelo sem camada oculta.

modelo_0 = neuralnet(
  formula = charges ~ age + sexmale + bmi + children + smokeryes,
  data = base_treino_final,
  hidden = 0,
  linear.output = FALSE)
plot(modelo_0,rep = "best")

Vamos criar também três modelos com 1 camada oculta: com 1 neurônio; com 2 neurônios; e com 3 neurônios. Estes serão chamados, respectivamente, de: modelo_1, modelo_2 e modelo_3.

Os modelos com camadas ocultas serão criados com a mesma função neuralnbet, mas para isso vamos mudar o argumento hidden.

modelo_1 = neuralnet(
  formula = charges ~ age + sexmale + bmi + children + smokeryes,
  data = base_treino_final,
  hidden = 1,
  linear.output = FALSE)
plot(modelo_1,rep = "best")

modelo_2 = neuralnet(
  formula = charges ~ age + sexmale + bmi + children + smokeryes,
  data = base_treino_final,
  hidden = 2,
  linear.output = FALSE)
plot(modelo_2,rep = "best")

modelo_3 = neuralnet(
  formula = charges ~ age + sexmale + bmi + children + smokeryes,
  data = base_treino_final,
  hidden = 3,
  linear.output = FALSE)
plot(modelo_3,rep = "best")

Em, seguida vamos criar um modelo com 2 camadas ocultas contendo 3 neurônios na primeira camada oculta e 2 neurônios na segunda, que será chamado de modelo_32. Para isso vamos mudar o argumento hidden da função neuralnet novamente.

modelo_32 = neuralnet(
  formula = charges ~ age + sexmale + bmi + children + smokeryes,
  data = base_treino_final,
  hidden = c(3,2),
  linear.output = FALSE)
plot(modelo_32,rep = "best")

Por fim, Criamos um modelo com 3 camadas ocultas contendo 3 neurônios em cada, que será chamado de modelo_33. Para isso vamos mudar o argumento hidden da função neuralnet.

modelo_333 = neuralnet(
  formula = charges ~ age + sexmale + bmi + children + smokeryes,
  data = base_treino_final,
  hidden = c(3,3,3),
  linear.output = FALSE)
plot(modelo_333,rep = "best")

A única diferença entre os três modelos criados é a arquitetura.

Previsão na base de treino

A previsão na base de treino é dada pelo comando $net.result:

y_0_ = modelo_0$net.result[[1]][,1]
y_1_ = modelo_1$net.result[[1]][,1]
y_2_ = modelo_2$net.result[[1]][,1]
y_3_ = modelo_3$net.result[[1]][,1]
y_32_ = modelo_32$net.result[[1]][,1]
y_333_ = modelo_333$net.result[[1]][,1]

Os valores retornados estão transformados, pois são saídas da função de ativação, que nesse caso foi a logística. Precisamos então fazemos a transformação inversa para voltar a variável resposta à sua unidade original.

A transformação inversa precisa dos valores originais da variável alvo na base de treino.

base_treino= readRDS("base_treino_sem_NA.RDS")
min = min(base_treino$charges)
max = max(base_treino$charges)

A transformação inversa é dada por:

y_0 = y_0_ * (max - min) + min
y_1 = y_1_ * (max - min) + min
y_2 = y_2_ * (max - min) + min
y_3 = y_3_ * (max - min) + min
y_32 = y_32_ * (max - min) + min
y_333 = y_333_ * (max - min) + min

Medidas de qualidade na base de treino

Em seguida calculamos os erros na previsão de cada observação da base de treino.

erro_0 = y_0 - base_treino$charges
erro_1 = y_1 - base_treino$charges
erro_2 = y_2 - base_treino$charges
erro_3 = y_3 - base_treino$charges
erro_32 = y_32 - base_treino$charges
erro_333 = y_333 - base_treino$charges

Calculamos então SSE:

sse_0 = sum((erro_0)^2)
sse_1 = sum((erro_1)^2)
sse_2 = sum((erro_2)^2)
sse_3 = sum((erro_3)^2)
sse_32 = sum((erro_32)^2)
sse_333 = sum((erro_333)^2)

E por fim o \(R^2\):

sst = sum(( mean(base_treino$charges) - base_treino$charges )^2)
(R2_0 = 1 - sse_0/sst)
[1] 0.7605819
(R2_1 = 1 - sse_1/sst)
[1] 0.7636
(R2_2 = 1 - sse_2/sst)
[1] 0.8354492
(R2_3 = 1 - sse_3/sst)
[1] 0.8407497
(R2_32 = 1 - sse_32/sst)
[1] 0.8482347
(R2_333 = 1 - sse_333/sst)
[1] 0.8469269

Leitura e preparação da base de teste

Vejamos agora como os modelos se comportam na base de teste. Primeiro a leitura da base e a retirada da variável married e das NAs.

base_teste= readRDS("base_teste_bruta.RDS")
base_teste = base_teste |> select(-married)
base_teste = na.omit(base_teste)

Agora a padronização das variáveis quantitativas independentes, que serão padronizadas considerando os valores da base de treino.

age_teste = (base_teste$age - mean(base_treino$age))/sd(base_treino$age)
bmi_teste = (base_teste$bmi - mean(base_treino$bmi))/sd(base_treino$bmi)
children_teste = (base_teste$children - mean(base_treino$children))/sd(base_treino$children)

Agora vamos criar a base de teste transformada (sem a variável resposta - charge) e realizar o tratamento nas variáveis categóricas.

base_teste_ = tibble(
  age = age_teste,
  bmi = bmi_teste,
  children = children_teste,
  sex = base_teste$sex,
  smoker = base_teste$smoker,
  region = base_teste$region
)
  
base_teste_final = model.matrix( ~ age + sex + bmi + children + smoker + region,
                                 data = base_teste_)
base_teste_final = base_teste_final[,-1]

É importante que as variáveis categóricas criadas pela função model.matrix sejam as mesmas que foram criadas para a base de treino. Vamos verificar.

colnames(base_treino_final)
[1] "age"             "sexmale"         "bmi"             "children"       
[5] "smokeryes"       "regionnorthwest" "regionsoutheast" "regionsouthwest"
[9] "charges"        
colnames(base_teste_final)
[1] "age"             "sexmale"         "bmi"             "children"       
[5] "smokeryes"       "regionnorthwest" "regionsoutheast" "regionsouthwest"

Vamos salvar a base_teste_final para não precisar realizar essas etapas novamente em análises futuras.

saveRDS(base_teste_final,"base_teste_final.RDS")

Previsão na base de teste

Agora a base de teste está pronta para realizarmos as previsões, que virão na unidade transformada. Para isso será usada a função predict.

pred_teste_0_ = predict(modelo_0,newdata = base_teste_final)[,1]
pred_teste_1_ = predict(modelo_1,newdata = base_teste_final)[,1]
pred_teste_2_ = predict(modelo_2,newdata = base_teste_final)[,1]
pred_teste_3_ = predict(modelo_3,newdata = base_teste_final)[,1]
pred_teste_32_ = predict(modelo_32,newdata = base_teste_final)[,1]
pred_teste_333_ = predict(modelo_333,newdata = base_teste_final)[,1]

E podemos retornar a previsão da variável resposta para a unidade original.

pred_teste_0 = pred_teste_0_ * (max - min) + min
pred_teste_1 = pred_teste_1_ * (max - min) + min
pred_teste_2 = pred_teste_2_ * (max - min) + min
pred_teste_3 = pred_teste_3_ * (max - min) + min
pred_teste_32 = pred_teste_32_ * (max - min) + min
pred_teste_333 = pred_teste_333_ * (max - min) + min

Medidas de qualidade na base de teste

O primeiro passo para se calcular a medida de qualidade R\(^2\) é encontrar os erros na previsão.

erro_teste_0 = pred_teste_0 - base_teste$charges
erro_teste_1 = pred_teste_1 - base_teste$charges
erro_teste_2 = pred_teste_2 - base_teste$charges
erro_teste_3 = pred_teste_3 - base_teste$charges
erro_teste_32 = pred_teste_32 - base_teste$charges
erro_teste_333 = pred_teste_333 - base_teste$charges

Calculamos então SSE:

sse_teste_0 = sum((erro_teste_0)^2)
sse_teste_1 = sum((erro_teste_1)^2)
sse_teste_2 = sum((erro_teste_2)^2)
sse_teste_3 = sum((erro_teste_3)^2)
sse_teste_32 = sum((erro_teste_32)^2)
sse_teste_333 = sum((erro_teste_333)^2)

E por fim o \(R^2\):

sst_teste = sum(( mean(base_treino$charges) - base_teste$charges )^2)
(R2_teste_0 = 1 - sse_teste_0/sst_teste)
[1] 0.7614516
(R2_teste_1 = 1 - sse_teste_1/sst_teste)
[1] 0.7710624
(R2_teste_2 = 1 - sse_teste_2/sst_teste)
[1] 0.8955573
(R2_teste_3 = 1 - sse_teste_3/sst_teste)
[1] 0.8961266
(R2_teste_32 = 1 - sse_teste_32/sst_teste)
[1] 0.9069683
(R2_teste_333 = 1 - sse_teste_333/sst_teste)
[1] 0.9030478

Comparação de resultados

Vamos comparar os resultados de todos os modelos treinados considerando os valores de R\(^2\) tanto na base de treino quanto na base de teste.

barplot(matrix(c(R2_0,R2_1,R2_2,R2_3,R2_32,R2_333,
                 R2_teste_0,R2_teste_1,R2_teste_2,R2_teste_3,R2_teste_32,R2_teste_333),nrow = 2,byrow = T),
        beside = T,
        names.arg = c("0","1","2","3","32","333"),
        ylim = c(0,1),
        col=c("tomato","blue4"),  
        args.legend = list(x = "bottomright"),
        legend.text = c("treino","teste"))
abline(h=R2_0,lty=2,col="lightgray")
abline(h=R2_3,lty=2,col="lightgray")