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]\).

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

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, o primeiro contendo 1 neurônio, o segundo contendo 2 neurônios e o terceiro com 3 neurônios, chamados de modelo_1, modelo_2 e modelo_3, respectivamente. Para isso vamos mudar o argumento hidden da função neuralnet.

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")

Criamos também um modelo com 2 camadas ocultas contendo 3 neurônios na primeira e 2 na segunda, que será chamado de modelo_32. Para isso vamos mudar o argumento hidden da função neuralnet.

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.

Vejamos os valores de \(R^2\) na base de treino para cada um dos três modelos. Para isso, primeiro guardamos os valores previstos:

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

Depois fazemos a transformação inversa para voltar a variável resposta á sua unidade valor original. Para isso vamos precisar da base de treino antes da transformação das variáveis.

base_treino= readRDS("base_treino.RDS")
min = min(base_treino$charges)
max = max(base_treino$charges)
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

Em seguida qualculamos os erros na previsão de cada observação.

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((base_treino$charges - mean(base_treino$charges))^2)
(R2_0 = 1 - sse_0/sst)
[1] 0.7529882
(R2_1 = 1 - sse_1/sst)
[1] 0.7746513
(R2_2 = 1 - sse_2/sst)
[1] 0.8529637
(R2_3 = 1 - sse_3/sst)
[1] 0.8595364
(R2_32 = 1 - sse_32/sst)
[1] 0.8479433
(R2_333 = 1 - sse_333/sst)
[1] 0.8541939

Vejamos agora como os modelos se comportam na base de teste.

base_teste= readRDS("base_teste.RDS")

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"

Agora a base de teste está pronta para realizarmos as previsões, que virão na unidade transformada.

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

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
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((base_teste$charges - mean(base_treino$charges))^2)
(R2_teste_0 = 1 - sse_teste_0/sst_teste)
[1] 0.778108
(R2_teste_1 = 1 - sse_teste_1/sst_teste)
[1] 0.7956841
(R2_teste_2 = 1 - sse_teste_2/sst_teste)
[1] 0.8567615
(R2_teste_3 = 1 - sse_teste_3/sst_teste)
[1] 0.8546028
(R2_teste_32 = 1 - sse_teste_32/sst_teste)
[1] 0.8447543
(R2_teste_333 = 1 - sse_teste_333/sst_teste)
[1] 0.8421678
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")