Indo além do PCA

Tarssio Barreto

21 de abril de 2019

knitr::opts_chunk$set(echo = TRUE)

Utilizando um pouco do PCA da forma mais clássica:

Objetivo

Vamos utilizar o PCA com o intuíto de realizar a redução de dimensionalidade e posteriormente comparar os resultados obtidos para classificação, através do algoritmo de KNN, para os usos de água domésticos.

Realizando o PCA e analisando os resultados

Carregando os dados:

 require(caret)
require(tidyverse)

dados <- load("data_041218_1138.RData")

dados <- features_atual %>% 
  filter(categoria %in% c("Torneira Interna", "Torneira Externa")) %>% 
  filter(casa == "A") %>% 
  sample_n(1000)

dados$categoria <- fct_drop(dados$categoria)

x <- dados[,c(2:9)]

Realizaremos, primeiro, o PCA através do passo-a-passo a seguir:

  1. Escalonando os dados:
x <- scale(x)
  1. Cálculo da covariância:
covariancia <- cov(x)
  1. Autovetores e valores:
auto <- eigen(covariancia)

auto$vectors # Autovetores
##            [,1]       [,2]         [,3]        [,4]        [,5]
## [1,] -0.2874158  0.5177313  0.289541796  0.28822099 -0.11157488
## [2,] -0.1836519  0.6237807  0.160700025 -0.59299595  0.30674104
## [3,] -0.4151302  0.1640234  0.118296188  0.37892024 -0.41937080
## [4,] -0.1819549  0.3046597 -0.907534267  0.15285423  0.02909467
## [5,] -0.3918615 -0.2380082 -0.084915386 -0.55746341 -0.63810544
## [6,] -0.4214431 -0.2646106  0.006626759 -0.07049019  0.35265005
## [7,] -0.4141117 -0.1958094  0.186385659  0.28014688  0.21927746
## [8,] -0.4190052 -0.2410866 -0.103607267 -0.06420994  0.37029637
##             [,6]        [,7]         [,8]
## [1,]  0.31111741  0.60367440 -0.093674419
## [2,] -0.08184488 -0.30666828  0.069762801
## [3,] -0.58710956 -0.35013532  0.004942601
## [4,]  0.14482094 -0.06098780 -0.039407223
## [5,]  0.23701390  0.08834334  0.025727120
## [6,] -0.11435745  0.06005052 -0.778658120
## [7,]  0.58571918 -0.47593444  0.243485516
## [8,] -0.34426220  0.42106214  0.564380499
auto$values # Autovalores
## [1] 4.754869942 1.794947478 0.809040933 0.280322316 0.165782718 0.129812026
## [7] 0.055586277 0.009638309
  1. Predizendo os valores no PCA:
pred <- as.matrix(x) %*% as.matrix(auto$vectors)

head(pred)
##            [,1]        [,2]        [,3]        [,4]        [,5]
## 1442 0.90160802  0.56445830 -1.03244203  0.43822913 -0.44928247
## 2263 2.36393222  0.05878983 -0.24405902  0.08573885 -0.21686342
## 4631 0.28354187 -0.62282949 -0.03892431 -0.15689228 -0.33821503
## 141  0.09813593 -0.24644071  0.18476249  0.45072958  0.02042007
## 3853 1.18560000 -0.56825413 -0.24942334 -0.17194166 -0.01693470
## 1969 0.97643148  0.56956641  1.08122014  0.28491345  0.26805281
##             [,6]        [,7]        [,8]
## 1442  0.39561157  0.24152164 -0.06399957
## 2263 -0.22567180 -0.14991912  0.02318916
## 4631  0.24621661  0.04275761  0.04041907
## 141  -0.02593098  0.26756345 -0.07145361
## 3853 -0.15866571 -0.05155676 -0.01815174
## 1969  0.41234910 -0.55691159 -0.14811048

Exploraremos agora, brevemente, alguns opções de projeção do PCA:

Usaremos para isto o pacote factoextra:

library(factoextra)

Criando nosso objeto PCA:

x_pca <- prcomp(x)

summary(x_pca)
## Importance of components:
##                           PC1    PC2    PC3     PC4     PC5     PC6
## Standard deviation     2.1806 1.3398 0.8995 0.52945 0.40716 0.36029
## Proportion of Variance 0.5944 0.2244 0.1011 0.03504 0.02072 0.01623
## Cumulative Proportion  0.5944 0.8187 0.9199 0.95490 0.97562 0.99185
##                            PC7     PC8
## Standard deviation     0.23577 0.09817
## Proportion of Variance 0.00695 0.00120
## Cumulative Proportion  0.99880 1.00000
x_pca$rotation
##                PC1        PC2          PC3         PC4         PC5
## duracao -0.2874158 -0.5177313  0.289541796  0.28822099 -0.11157488
## nmoda   -0.1836519 -0.6237807  0.160700025 -0.59299595  0.30674104
## volume  -0.4151302 -0.1640234  0.118296188  0.37892024 -0.41937080
## inercia -0.1819549 -0.3046597 -0.907534267  0.15285423  0.02909467
## moda    -0.3918615  0.2380082 -0.084915386 -0.55746341 -0.63810544
## media   -0.4214431  0.2646106  0.006626759 -0.07049019  0.35265005
## pico    -0.4141117  0.1958094  0.186385659  0.28014688  0.21927746
## mediana -0.4190052  0.2410866 -0.103607267 -0.06420994  0.37029637
##                 PC6         PC7          PC8
## duracao  0.31111741 -0.60367440  0.093674419
## nmoda   -0.08184488  0.30666828 -0.069762801
## volume  -0.58710956  0.35013532 -0.004942601
## inercia  0.14482094  0.06098780  0.039407223
## moda     0.23701390 -0.08834334 -0.025727120
## media   -0.11435745 -0.06005052  0.778658120
## pico     0.58571918  0.47593444 -0.243485516
## mediana -0.34426220 -0.42106214 -0.564380499

É interessante, em momento de exercicio, comparar o obtido através do algoritmo e o resultado da função prcomp. É possível perceber, também, que com as duas componentes principais podemos explicar cerca de 85% da variância dos dados, reduzindo de forma significativa o numéro de dimensões do nosso problema. Vamos, enfim, explorar a projeção das nossas componentes principais.

fviz_pca_biplot(x_pca, repel = FALSE)

Com o gráfico acima verificamos a direção de alguma das variáveis e como os dados se comportam após em função dos dois primeiros componentes gerados pela PCA.

Podemos, ver também como acontece a projeção, tendo em vista as classificações dos usos domésticos de água:

fviz_pca_ind(x_pca,
             label = "none", # hide individual labels
             habillage = dados$categoria, # color by groups
             addEllipses = TRUE # Concentration ellipses
             )

Os resultados não apontam um agrupamento conclusivo, testemos, de forma expedita, o ganho de acurácia utilizando o PCA no processamento dos dados:

Definindo grupo de teste e treino:

# Definindo Split

split=0.80

trainIndex <- createDataPartition(dados$categoria, p=split, list=FALSE)

# Separando 

data_train <- dados[ trainIndex,c(1:8)]

data_test <- dados[-trainIndex,c(1:8)]

Modelo 1 : Sem pca

model <- train(categoria ~ ., data = data_train,
      method = "knn", preProcess = c("center", "scale"), 
      tuneLength = 10, 
      trControl = trainControl(method="cv", number=10))



pred <- predict(model, data_test)

confusionMatrix(pred, data_test$categoria) 
## Confusion Matrix and Statistics
## 
##                   Reference
## Prediction         Torneira Interna Torneira Externa
##   Torneira Interna               98               21
##   Torneira Externa               11               69
##                                           
##                Accuracy : 0.8392          
##                  95% CI : (0.7806, 0.8873)
##     No Information Rate : 0.5477          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.6723          
##                                           
##  Mcnemar's Test P-Value : 0.1116          
##                                           
##             Sensitivity : 0.8991          
##             Specificity : 0.7667          
##          Pos Pred Value : 0.8235          
##          Neg Pred Value : 0.8625          
##              Prevalence : 0.5477          
##          Detection Rate : 0.4925          
##    Detection Prevalence : 0.5980          
##       Balanced Accuracy : 0.8329          
##                                           
##        'Positive' Class : Torneira Interna
## 
acc_sem <- 0.763

Modelo 2 : Com pca

model2 <- train(categoria ~ ., data = data_train,
      method = "knn", preProcess = c("center", "scale", "pca"), 
      tuneLength = 10, 
      trControl = trainControl(method="cv", number=10))



pred <- predict(model2, data_test)

confusionMatrix(pred, data_test$categoria) 
## Confusion Matrix and Statistics
## 
##                   Reference
## Prediction         Torneira Interna Torneira Externa
##   Torneira Interna               87               19
##   Torneira Externa               22               71
##                                           
##                Accuracy : 0.794           
##                  95% CI : (0.7311, 0.8479)
##     No Information Rate : 0.5477          
##     P-Value [Acc > NIR] : 3.381e-13       
##                                           
##                   Kappa : 0.5854          
##                                           
##  Mcnemar's Test P-Value : 0.7548          
##                                           
##             Sensitivity : 0.7982          
##             Specificity : 0.7889          
##          Pos Pred Value : 0.8208          
##          Neg Pred Value : 0.7634          
##              Prevalence : 0.5477          
##          Detection Rate : 0.4372          
##    Detection Prevalence : 0.5327          
##       Balanced Accuracy : 0.7935          
##                                           
##        'Positive' Class : Torneira Interna
## 
acc_sem <- 0.768

Existe um pequeno ganho no PCA, o que pode ser visto é que é preciso uma técnica mais robusta de redução de dimensionalidade para que encontremos uma melhor separação dos dados e consequentemente um resultado melhor. Mais a frente será debatido o t-SNE e seus resultados para este banco de dados.

PCA para textos

Objetivo:

Um dos claros objetivos desta apresentação é ir além do PCA, porém, pede-se um pouco de paciência para que vejamos de forma breve (sem entrar nas questões de NLP) como esta técnica pode ser utilizada em análise textual.

Utilizaremos os seguintes pacotes para determinar a similaridade entre as palavras utilizados por Albert Camus no seu clássico: “O Estrangeiro”.

knitr::include_graphics("7248407GG.jpg")

“Hoje, a mãe morreu. Ou talvez ontem, não sei bem. Recebi um telegrama do asilo: “Sua mãe falecida: Enterro amanhã. Sentidos pêsames”. Isto não quer dizer nada. Talvez tenha sido ontem.”

Carregando o livro

library(epubr)
library(tm)
library(lexiconPT)
library(broom)
library(tidytext)


x <- epub("O Estrangeiro - Albert Camus.epub")

estrangeiro <- x$data[[1]]

Alguns ajustes

Neste momento, vamos separar as palavras utilizadas e remover as “stopwords” em português:

Analisando os “unigrams”:

stop_words <- stopwords(kind = "pt") %>% 
  as.tibble()

unigram_probs <- estrangeiro %>%
  unnest_tokens(word, text) %>%
  count(word, sort = TRUE) %>%
  mutate(p = n / sum(n)) 

head(unigram_probs)
## # A tibble: 6 x 3
##   word      n      p
##   <chr> <int>  <dbl>
## 1 que    1227 0.0408
## 2 a      1112 0.0370
## 3 o      1053 0.0350
## 4 e       990 0.0329
## 5 de      905 0.0301
## 6 me      664 0.0221

Vamos escolher uma janela móvel de 15 palavras, pode-se aplicar maiores ou menores, é interessante testar algumas afim de entender qual é o valor que propociona os melhores resultados.

library(widyr)

colnames(stop_words) <- c("word")

tidy_skipgrams <- estrangeiro %>%
  unnest_tokens(ngram, text, token = "ngrams", n = 15) %>%
  mutate(ngramID = row_number()) %>% 
  unite(skipgramID, ngramID) %>%
  unnest_tokens(word, ngram) %>% 
  anti_join(stop_words, by = "word")

head(tidy_skipgrams)
## # A tibble: 6 x 5
##   section           nword nchar skipgramID word    
##   <chr>             <int> <int> <chr>      <chr>   
## 1 content0002.xhtml     2    14 1          <NA>    
## 2 content0003.xhtml  4167 24617 2          capítulo
## 3 content0003.xhtml  4167 24617 2          i       
## 4 content0003.xhtml  4167 24617 2          hoje    
## 5 content0003.xhtml  4167 24617 2          mãe     
## 6 content0003.xhtml  4167 24617 2          morreu

Calculando as probabilidades:

skipgram_probs <- tidy_skipgrams %>%
  pairwise_count(word, skipgramID, diag = TRUE, sort = TRUE) %>%
  mutate(p = n / sum(n))

Normalizando a probabilidade

Vamos utilizar um indicador para visualizar quais palavras ocorreram juntas com mais frequência do que o esperado, tendo em base a frequência com que elas ocorreram sozinhas.

O quanto maior o resultado, mais estas palavras estão associadas e possuem boa probabilidade de ocorrem juntas, em relação a probabilidade de serem encontradas individualmente.

normalized_prob <- skipgram_probs %>%
  filter(n > 20) %>%
  rename(word1 = item1, word2 = item2) %>%
  left_join(unigram_probs %>%
              select(word1 = word, p1 = p),
            by = "word1") %>%
  left_join(unigram_probs %>%
              select(word2 = word, p2 = p),
            by = "word2") %>%
  mutate(p_together = p / p1 / p2)


head(normalized_prob)
## # A tibble: 6 x 7
##   word1    word2        n        p      p1      p2 p_together
##   <chr>    <chr>    <dbl>    <dbl>   <dbl>   <dbl>      <dbl>
## 1 disse    disse     2844 0.00157  0.00642 0.00642       38.2
## 2 mim      mim       1514 0.000838 0.00343 0.00343       71.4
## 3 é        é         1448 0.000801 0.00349 0.00349       65.7
## 4 pouco    pouco     1307 0.000723 0.00323 0.00323       69.5
## 5 raimundo raimundo  1303 0.000721 0.00306 0.00306       77.0
## 6 então    então     1227 0.000679 0.00279 0.00279       87.0

Vamos observar quais palavras estão associadas a “praia”:

normalized_prob %>% 
  filter(word1 == "praia") %>%
  arrange(-p_together)
## # A tibble: 23 x 7
##    word1 word2         n         p      p1        p2 p_together
##    <chr> <chr>     <dbl>     <dbl>   <dbl>     <dbl>      <dbl>
##  1 praia praia       443 0.000245  0.00103 0.00103        231. 
##  2 praia saco         39 0.0000216 0.00103 0.0000998      210. 
##  3 praia brilho       22 0.0000122 0.00103 0.000233        50.7
##  4 praia autocarro    33 0.0000183 0.00103 0.000366        48.4
##  5 praia banho        22 0.0000122 0.00103 0.000266        44.4
##  6 praia longe        44 0.0000243 0.00103 0.000565        41.8
##  7 praia voltei       57 0.0000315 0.00103 0.000765        40.0
##  8 praia longo        24 0.0000133 0.00103 0.000333        38.7
##  9 praia mar          57 0.0000315 0.00103 0.000831        36.8
## 10 praia pequena      24 0.0000133 0.00103 0.000466        27.7
## # ... with 13 more rows

Vamos observar quais palavras estão associadas a “raimundo”:

normalized_prob %>% 
  filter(word1 == "raimundo") %>%
  arrange(-p_together)
## # A tibble: 108 x 7
##    word1    word2        n         p      p1        p2 p_together
##    <chr>    <chr>    <dbl>     <dbl>   <dbl>     <dbl>      <dbl>
##  1 raimundo raimundo  1303 0.000721  0.00306 0.00306         77.0
##  2 raimundo copo        23 0.0000127 0.00306 0.0000665       62.5
##  3 raimundo bêbedo      22 0.0000122 0.00306 0.0000665       59.8
##  4 raimundo vítima      21 0.0000116 0.00306 0.0000665       57.1
##  5 raimundo ouviu       26 0.0000144 0.00306 0.0000998       47.1
##  6 raimundo ouvimos     22 0.0000122 0.00306 0.0000998       39.9
##  7 raimundo revólver    36 0.0000199 0.00306 0.000200        32.6
##  8 raimundo insultá     24 0.0000133 0.00306 0.000133        32.6
##  9 raimundo navalha     35 0.0000194 0.00306 0.000200        31.7
## 10 raimundo bateu       23 0.0000127 0.00306 0.000133        31.3
## # ... with 98 more rows

Vamos transformar estes dados em uma matriz esparsa:

pmi_matrix <- normalized_prob %>%
    mutate(pmi = log10(p_together)) %>%
    cast_sparse(word1, word2, pmi)

head(pmi_matrix@x)
## [1] 1.5821239 0.6537674 0.7127086 0.4269799 0.8530843 0.8116916

A transformação de dados textuais em matriz, possuem muitos zeros, esta estrutura utilizada economiza tempo e memória.

PCA para textos:

Neste caso, usaremos a abordagem do PCA afim de reduzir a dimensionalidade dos dados e buscar uma forma mais interessante de representar a similaridade entre as palavras :

library(irlba)

pmi_pca <- irlba::prcomp_irlba(pmi_matrix, n = 256)

word_vectors <- pmi_pca$x

rownames(word_vectors) <- rownames(pmi_matrix)

dim(word_vectors)
## [1] 1884  256

Encontrando similaridades:

Vamos utilizar uma função publicada pela Julia Silge para para varrer o vetor de word_vectors atrás das palavras de maiores similaridades:

search_synonyms <- function(word_vectors, selected_vector) {
  
  similarities <- word_vectors %*% selected_vector %>%
    tidy() %>%
    as_tibble() %>%
    rename(token = .rownames,
           similarity = unrowname.x.)
  
  similarities %>%
    arrange(-similarity)    
}

Testando a função:

search_synonyms(word_vectors, word_vectors["praia",])
## # A tibble: 1,884 x 2
##    token    similarity
##    <chr>         <dbl>
##  1 praia          36.9
##  2 mar            21.1
##  3 longe          16.3
##  4 raimundo       14.7
##  5 voltei         14.2
##  6 maria          14.1
##  7 banho          13.6
##  8 alguns         13.2
##  9 brilho         13.1
## 10 pequena        12.7
## # ... with 1,874 more rows