Perceptron Múltiplas Camadas
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.
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.
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.
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.
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.
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.
\[ \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) + minMedidas 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$chargesCalculamos 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) + minMedidas 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$chargesCalculamos 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")