Introducción

En la sesión práctica pasada de Introducción a los Modelos de Clasificación en R, llevamos a cabo la implementación básica de los siguientes modelos de clasificación: Regresión Logística, k-NN, Kernel-SVM, Naive Bayes, Árboles de Decisión y Bosques Aleatorios y, como se mostró en dicho ejercicio, se aplicaron los clasificadores al conjunto de datos Predicting a Pulsar Star. Sin embargo, en esa oportunidad se probaron los clasificadores dividiendo los datos en un conjunto de entrenamiento y un conjunto de validación y, aunque se evaluó el desempeño de los modelos haciendo uso de la matriz de confusión sobre los datos de validación así como el estudio de la curva ROC, la manera más conveniente de evaluar la calidad de un modelo de Machine Learning sobre un problema dado es haciendo uso de la técnica de la Validación Cruzada o k-fold Cross Validation. En este ejercicio de programación en R realizaremos la implementación de dicha técnica haciendo uso del paquete caret y evaluaremos el resultado de la misma sobre los mismos clasificadores implementados la vez anterior.


Fundamentos del k-fold Cross Validation

Cuando se realizó la implementación anterior de los modelos de clasificación, se partió del conjunto de datos inicial y se crearon al azar dos conjuntos separados, un conjunto de entrenamiento y un conjunto de validación. Una vez realizado el entrenamiento con el primer conjunto, se aplicaron los clasificadores sobre el conjunto de validación a fin de evaluar la precisión en la predicción y su error. Sin embargo, dada la naturaleza estocástica tanto al construir estos conjuntos de datos, como en las variables y parámetros propios de los modelos de clasificación trabajados, no necesariamente se van a obtener siempre los mismos resultados y, en consecuencia, las mismas precisiones y errores en cada corrida de los algoritmos.

Es por ello que, para obtener los mejores resultados en cuanto a la medición de la calidad de la predicción de los modelos, es mejor realizar una Validación Cruzada sobre los datos de entrada.

La Validación Cruzada o k-fold Cross Validation consiste en tomar los datos originales y crear a partir de ellos dos conjuntos separados: un primer conjunto de entrenamiento (y prueba), y un segundo conjunto de validación.

Luego, el conjunto de entrenamiento se va a dividir en k subconjuntos y, al momento de realizar el entrenamiento, se va a tomar cada k subconjunto como conjunto de prueba del modelo, mientras que el resto de los datos se tomará como conjunto de entrenamiento.

Este proceso se repetirá k veces, y en cada iteración se seleccionará un conjunto de prueba diferente, mientras los datos restantes se emplearán, como se mencionó, como conjunto de entrenamiento. Una vez finalizadas las iteraciones, se calcula la precisión y el error para cada uno de los modelos producidos, y para obtener la precisión y el error final se calcula el promedio de los k modelos entrenados.

Una vez se cuenta con esta precisión promedio para un modelo, se puede repetir entonces el procedimiento del Cross Validation para todos los demás modelos de clasificación que se estén evaluando, y se seleccionará al final aquel que produzca el mejor valor de precisión y menor error promedio.

Entonces, puede utilizarse dicho modelo sobre el conjunto de validación generado en la primera parte, ya que, se supone, es este modelo el que mejor resultado en general ofreció durante la fase de entrenamiento.

Veamos entonces cómo se implementa esta técnica en R.


Conjunto de Datos

Como ya sabemos, el conjunto de datos empleados fue el Predicting a Pulsar Star, que cuenta con un total de 17898 observaciones de 9 variables cada una, y que representan características medidas de estrellas Pulsares y No Pulsares.

Como se realizó en el ejercicio práctico de los Modelos de Clasificación, vamos a cargar dicho conjunto de datos, y prepararlos, es decir, vamos a definir la variable dependiente target_class como una variable categórica Pulsar y NoPulsar, vamos a reescalar los datos, y a crear el conjunto de entrenamiento y el de validación:

setwd("D:/ProgramasR/Practicas/CrossValidation")
dataset <- read.csv("pulsar_stars.csv")
colnames(dataset)[9] <- "TipoEstrella"
dataset$TipoEstrella <- factor(dataset$TipoEstrella, levels = c("0", "1"), labels = c("NoPulsar", "Pulsar"))
dataset[, c(1:8)] <- scale(dataset[, c(1:8)])
summary(dataset)
##  Mean.of.the.integrated.profile
##  Min.   :-4.1035               
##  1st Qu.:-0.3957               
##  Median : 0.1559               
##  Mean   : 0.0000               
##  3rd Qu.: 0.6239               
##  Max.   : 3.1785               
##  Standard.deviation.of.the.integrated.profile
##  Min.   :-3.18236                            
##  1st Qu.:-0.60988                            
##  Median : 0.05815                            
##  Mean   : 0.00000                            
##  3rd Qu.: 0.65374                            
##  Max.   : 7.63232                            
##  Excess.kurtosis.of.the.integrated.profile
##  Min.   :-2.212200                        
##  1st Qu.:-0.423630                        
##  Median :-0.239293                        
##  Mean   : 0.000000                        
##  3rd Qu.:-0.004259                        
##  Max.   : 7.134757                        
##  Skewness.of.the.integrated.profile Mean.of.the.DM.SNR.curve
##  Min.   :-0.5775                    Min.   :-0.4208         
##  1st Qu.:-0.3176                    1st Qu.:-0.3628         
##  Median :-0.2548                    Median :-0.3329         
##  Mean   : 0.0000                    Mean   : 0.0000         
##  3rd Qu.:-0.1366                    3rd Qu.:-0.2426         
##  Max.   :10.7543                    Max.   : 7.1516         
##  Standard.deviation.of.the.DM.SNR.curve
##  Min.   :-0.9736                       
##  1st Qu.:-0.6106                       
##  Median :-0.4040                       
##  Mean   : 0.0000                       
##  3rd Qu.: 0.1079                       
##  Max.   : 4.3304                       
##  Excess.kurtosis.of.the.DM.SNR.curve Skewness.of.the.DM.SNR.curve
##  Min.   :-2.53941                    Min.   :-1.0030             
##  1st Qu.:-0.55970                    1st Qu.:-0.6562             
##  Median : 0.02884                    Median :-0.2046             
##  Mean   : 0.00000                    Mean   : 0.0000             
##  3rd Qu.: 0.53248                    3rd Qu.: 0.3234             
##  Max.   : 5.82240                    Max.   :10.1971             
##    TipoEstrella  
##  NoPulsar:16259  
##  Pulsar  : 1639  
##                  
##                  
##                  
## 

Como podemos observar, se tienen 16259 estrellas tipo NoPulsar, y 1639 tipo Pulsar.

Ahora, vamos a tomar un 80% de los datos como conjunto de entrenamiento, y el 20% restante como conjunto de validación:

library(caTools)
set.seed(1234)
split <- sample.split(dataset$TipoEstrella, SplitRatio = 0.80)
training_set <- subset(dataset, split == TRUE)
test_set <- subset(dataset, split == FALSE)

A fin de asegurarnos de que la proporción de estrellas pulsares y no pulsares es aproximadamente la misma en ambos conjuntos de datos, veamos la distribución de los tipos de estrella en cada uno:

table(training_set$TipoEstrella)
## 
## NoPulsar   Pulsar 
##    13007     1311
table(test_set$TipoEstrella)
## 
## NoPulsar   Pulsar 
##     3252      328

En efecto, vemos que para ambos conjuntos, la relación NoPulsar/Pulsar es alrededor de 9.9, de modo que la proporción se mantiene.

Ya que nuestro conjunto de datos está listo y preparado, podemos comenzar a construir nuestros modelos de clasificación usando k-fold Cross Validation.


Implementación del Cross Validation para los Modelos de Clasificación

Para aplicar la Validación Cruzada vamos a hacer uso de la función createFolds del paquete caret, y luego entrenaremos cada modelo sobre k = 10 subconjuntos haciendo uso de la función lapply:

library(caret)
folds <- createFolds(training_set$TipoEstrella, k = 10)
# Regresion Logistica
cvRegresionLogistica <- lapply(folds, function(x){
  training_fold <- training_set[-x, ]
  test_fold <- training_set[x, ]
  clasificador <- glm(TipoEstrella ~ ., family = binomial, data = training_fold)
  y_pred <- predict(clasificador, type = 'response', newdata = test_fold)
  y_pred <- ifelse(y_pred > 0.5, 1, 0)
  y_pred <- factor(y_pred, levels = c("0", "1"), labels = c("NoPulsar", "Pulsar"))
  cm <- table(test_fold$TipoEstrella, y_pred)
  precision <- (cm[1,1] + cm[2,2]) / (cm[1,1] + cm[2,2] +cm[1,2] + cm[2,1])
  return(precision)
})
precisionRegresionLogistica <- mean(as.numeric(cvRegresionLogistica))


# k-NN
library(class)
cvkNN <- lapply(folds, function(x){
  training_fold <- training_set[-x, ]
  test_fold <- training_set[x, ]
  y_pred <- knn(training_fold[, -9], 
                test_fold[, -9], 
                cl = training_fold[, 9], 
                k = 10)
  cm <- table(test_fold$TipoEstrella, y_pred)
  precision <- (cm[1,1] + cm[2,2]) / (cm[1,1] + cm[2,2] +cm[1,2] + cm[2,1])
  return(precision)
})
precisionkNN <- mean(as.numeric(cvkNN))

# Kernel-SVM
library(e1071)
cvKernelSVM <- lapply(folds, function(x){
  training_fold <- training_set[-x, ]
  test_fold <- training_set[x, ]
  clasificador <- svm(TipoEstrella ~ .,
                      data = training_fold, 
                      type = 'C-classification', 
                      kernel = 'radial')
  y_pred <- predict(clasificador, newdata = test_fold)
  cm <- table(test_fold$TipoEstrella, y_pred)
  precision <- (cm[1,1] + cm[2,2]) / (cm[1,1] + cm[2,2] +cm[1,2] + cm[2,1])
  return(precision)
})
precisionKernelSVM <- mean(as.numeric(cvKernelSVM))

# Naive Bayes
cvNaiveBayes <- lapply(folds, function(x){
  training_fold <- training_set[-x, ]
  test_fold <- training_set[x, ]
  clasificador <- naiveBayes(TipoEstrella ~ ., data = training_fold)
  y_pred <- predict(clasificador, newdata = test_fold)
  cm <- table(test_fold$TipoEstrella, y_pred)
  precision <- (cm[1,1] + cm[2,2]) / (cm[1,1] + cm[2,2] +cm[1,2] + cm[2,1])
  return(precision)
})
precisionNaiveBayes <- mean(as.numeric(cvNaiveBayes))

# Decision Tree
library(rpart)
cvDecisionTree <- lapply(folds, function(x){
  training_fold <- training_set[-x, ]
  test_fold <- training_set[x, ]
  clasificador <- rpart(TipoEstrella ~ ., data = training_fold)
  y_pred <- predict(clasificador, newdata = test_fold, type = 'class')
  cm <- table(test_fold$TipoEstrella, y_pred)
  precision <- (cm[1,1] + cm[2,2]) / (cm[1,1] + cm[2,2] +cm[1,2] + cm[2,1])
  return(precision)
})
precisionDecisionTree <- mean(as.numeric(cvDecisionTree))

# Random Forest
library(randomForest)
cvRandomForest <- lapply(folds, function(x){
  training_fold <- training_set[-x, ]
  test_fold <- training_set[x, ]
  clasificador <- randomForest(TipoEstrella ~ ., data = training_fold, ntree = 250)
  y_pred <- predict(clasificador, newdata = test_fold)
  cm <- table(test_fold$TipoEstrella, y_pred)
  precision <- (cm[1,1] + cm[2,2]) / (cm[1,1] + cm[2,2] +cm[1,2] + cm[2,1])
  return(precision)
})
precisionRandomForest <- mean(as.numeric(cvRandomForest))

Los resultados obtenidos para cada modelo son:

Medida Regresión Logística k-NN Kernel-SVM Naive Bayes Decision Tree Random Forests
Precisión 97.95 97.92 97.90 94.58 97.76 98.03
Error 2.05 2.08 2.10 5.02 2.24 1.97

Mientras que, en la primera práctica de modelos de clasificación, se obtuvo:

Medida Regresión Logística k-NN Kernel-SVM Naive Bayes Decision Tree Random Forests
Precisión 97.80 97.61 97.72 94.25 97.49 97.94
Error 2.20 2.39 2.27 5.74 2.50 2.05

Al comparar las tablas y observar los valores de precisión para la primera práctica, se tiene que:

Random Forest > Regresión Logística > Kernel-SVM > k-NN > Decision Tree > Naive Bayes

Mientras que cuando se implementa el k-fold Cross Validation se obtiene:

Random Forest > Regresión Logística > k-NN > Kernel-SVM > Decision Tree > Naive Bayes

De manera que tanto el modelo Random Forest como la Regresión Logística siguen siendo los mejores modelos, aunque para ambos se observa que al aplicar la Validación Cruzada se presenta un incremento de la precisión, pues el Random Forest pasó de 97.94 a 98.03, y la Regresión Logística de 97.80 a 97.95.

Sin embargo, en el caso con Validación Cruzada, se produce una mejora del modelo k-NN sobre el caso anterior, superando al modelo Kernel-SVM.

A partir de estos resultados podemos afirmar entonces que el modelo de Random Forest es el que mejor precisión en la predicción ofrece, y por lo tanto ya podemos usar ese modelo sobre el conjunto de validación incial creado:

library(randomForest)
clasificadorRF <- randomForest(TipoEstrella ~ ., data = training_set, ntree = 250)
y_pred <- predict(clasificadorRF, newdata = test_set)
cm <- table(test_set$TipoEstrella, y_pred)
cm
##           y_pred
##            NoPulsar Pulsar
##   NoPulsar     3233     19
##   Pulsar         60    268

Con una precisión de:

precisionRF <- (cm[1,1] + cm[2,2]) / (cm[1,1] + cm[2,2] +cm[1,2] + cm[2,1])
precisionRF
## [1] 0.977933

Es decir, tenemos un 97.73% de precisión en la predicción, y un error de 2.27% para el conjunto de validación final.


Conclusiones

La técnica del k-fold Cross Validation es una de las maneras más convenientes de seleccionar un modelo de aprendizaje automático frente a un problema o un conjunto de datos en particular. Ya que la Validación Cruzada aplica de manera iterativa cada modelo sobre todo el conjunto de entrenamiento tomando diversas (k) particiones, podemos afirmar que el resultado en promedio nos da una idea precisa de cuál es el desempeño del modelo de clasificación sobre el conjunto de datos entero, de modo que podemos caracterizar de manera adecuada cada algoritmo.

Sin embargo, aunado a la Validación Cruzada, a fin de mejorar todavía más el desempeño de los modelos sería necesario como siguiente paso hacer una afinación de los hiper parámetros de los modelos, es decir, todos esos parámetros que no se modifican durante la fase de entrenamiento, sino que son variables ajustables de cada modelo (como el valor de k en k-NN, el valor de C en las SVM, o la cantidad de árboles en el Random Forest, por mencionar algunos) y que, sin duda, van a afectar la calidad de cada uno de ellos.

Al final, la selección del mejor modelo para un problema dado dependerá no solo del resultado de la Validación Cruzada, sino de cuánto más se pueden afinar los parámetros de los modelos a fin de obtener resultados con alta precisión y poco error, además de baja variancia y bajo bias, es decir, modelos adecuados que no estén ni sub-entrenados ni sobre-entrenados.

Así, terminamos esta sesión práctica.