Introducción

El paquete caret (classification and regression training, Kuhn (2016)) incluye una serie de funciones que facilitan el uso de decenas de métodos complejos de clasificación y regresión. Utilizar este paquete en lugar de las funciones originales de los métodos presenta dos ventajas:

Mucha información y ayuda sobre uso del paquete se puede encontrar en la página web http://topepo.github.io/caret/index.html, que ha sido la fuente de información básica para elaborar esta introducción. Aquí nos centraremos en el uso de caret en problemas de clasificación. Las aplicaciones en problemas de regresión son similares.

Los datos

Comenzamos cargando los paquetes y datos:

library(dplyr)
library(caret)
load(url('http://verso.mat.uam.es/~joser.berrendero/datos/practica-svm-io.RData'))

Los datos proceden de un estudio sobre diagnóstico del cáncer de mama por imagen. Mediante una punción con aguja fina se extrae una muestra del tejido sospechoso de la paciente. La muestra se tiñe para resaltar los núcleos de las células y se determinan los límites exactos de los núcleos. Las variables consideradas corresponden a distintos aspectos de la forma del núcleo. El fichero contiene un data frame, llamado breast.cancer2, con 2 variables explicativas medidas en pacientes cuyos tumores fueron diagnosticados posteriormente como benignos o malignos y el factor y que toma los valores 0 o 1 en función de si las variables corresponden a un tumor benigno o maligno respectivamente. Más información sobre los datos se puede encontrar en esta dirección. En el fichero original se consideran 30 variables explicativas. Hay 569 observaciones, de las que 357 corresponden a tumores benignos y 212 a tumores malignos.

head(breast.cancer2)
##    x.smoothness x.concavepoints y
## 20      0.09779        0.047810 0
## 21      0.10750        0.031100 0
## 22      0.10240        0.020760 0
## 38      0.08983        0.029230 0
## 47      0.08600        0.005917 0
## 49      0.10310        0.027490 0

A continuación aparece una representación gráfica de los datos:

División de los datos en muestra de entrenamiento y de test

Para dividir los datos en una muestra de entrenamiento y otra de test se usa el comando createDataPartition. En el código siguiente p representa la proporción de datos en la muestra de entrenamiento. La partición se lleva a cabo para cada nivel de la variable y que aparece como primer argumento. El resultado es un vector con los índices de las filas seleccionadas para formar parte de la muestra de entrenamiento. El argumento list=FALSE se usa para evitar que el resultado sea una lista.

datos <- breast.cancer2
set.seed(100)  # Para reproducir los mismos resultados
IndicesEntrenamiento <- createDataPartition(y = datos$y,
                                            p = 0.6,
                                            list = FALSE)
Entrenamiento <- datos[IndicesEntrenamiento,]
Test <- datos[-IndicesEntrenamiento,]

El comando train

El comando más importante de caret es train. Se puede usar este comando único para aplicar un gran número de métodos de clasificación determinando (en caso necesario) los valores óptimos de sus parámetros mediante validación cruzada u otros métodos de remuestreo. Para usar train en general es necesario:

En el siguiente apartado, veremos cómo aplicar la regla lineal de Fisher, que no requiere el ajuste de ningún parámetro. Posteriormente, aplicaremos la regla de \(k\) vecinos más próximos, que requiere determinar apropiadamente el número \(k\) de vecinos.

Análisis discriminante lineal

Los tres primeros argumentos van a ser necesarios siempre que utilicemos el comando train:

En este ejemplo concreto se ha utilizado adicionalmente el argumento prior para fijar las mismas probabilidades a priori para las dos clases (por defecto son las proporciones muestrales de cada clase).

lda.datos <- train(y ~ .,
      data = datos,
      method = "lda",
      prior = c(0.5, 0.5))

Dentro de la lista de resultados generados, el elemento llamado finalModel contiene los resultados básicos finales. Para el caso de la regla de Fisher se obtiene:

lda.datos$finalModel
## Call:
## lda(x, grouping = y, prior = ..1)
## 
## Prior probabilities of groups:
##   0   1 
## 0.5 0.5 
## 
## Group means:
##   x.smoothness x.concavepoints
## 0   0.09247765      0.02571741
## 1   0.10289849      0.08799000
## 
## Coefficients of linear discriminants:
##                       LD1
## x.smoothness    -15.01049
## x.concavepoints  44.01965

A continuación llevamos a cabo los mismos cálculos, pero usando únicamente la muestra de test:

lda.entrenamiento <- train(y ~ .,
      data = Entrenamiento,
      method = "lda",
      prior = c(0.5, 0.5))

lda.entrenamiento$finalModel
## Call:
## lda(x, grouping = y, prior = ..1)
## 
## Prior probabilities of groups:
##   0   1 
## 0.5 0.5 
## 
## Group means:
##   x.smoothness x.concavepoints
## 0    0.0928687      0.02584057
## 1    0.1026804      0.08726258
## 
## Coefficients of linear discriminants:
##                       LD1
## x.smoothness    -16.42833
## x.concavepoints  43.29314

Vemos que los resultados no son muy diferentes a los obtenidos cuando usamos la muestra completa.

A continuación clasificamos los datos de test con esta regla de clasificación (mediante el comando predict) y calculamos los porcentajes de errores cometidos. Para este último cálculo se aplica en comando confusionMatrix usando como argumentos las predicciones y los valores verdaderos. Este comando calcula una lista larga de medidas. Únicamente se presenta la tabla de errores y la tasa de error global:

# Tasas de error para datos de test
predicciones <- predict(lda.entrenamiento, Test)
confusionMatrix(predicciones, Test$y)$table
##           Reference
## Prediction   0   1
##          0 136   9
##          1   6  75
confusionMatrix(predicciones, Test$y)$overall[1]
##  Accuracy 
## 0.9336283

Podemos comparar con las tasas de error aparente:

# Tasas de error aparente
predicciones <- predict(lda.datos, datos)
confusionMatrix(predicciones, datos$y)$table
##           Reference
## Prediction   0   1
##          0 340  33
##          1  17 179
confusionMatrix(predicciones, datos$y)$overall[1]
##  Accuracy 
## 0.9121265

Regla de \(k\) vecinos más próximos

Para tener un buen comportamiento en distintas situaciones muchas reglas de clasificación incroporan una serie de parámetros que les confieren una mayor flexibilidad. Normalmente se usan métodos de validación cruzada para determinar estos parámetros. Veamos cómo hacerlo en caret usando un ejemplo sencillo: la regla de \(k\) vecinos más próximos, en la que hay que determinar el número \(k\) de vecinos que intervienen para clasificar cada punto.

Primero hay que determinar el conjunto de valores entre los que vamos a seleccionar \(k\) mediante el comando expand.grid. En el ejemplo se han fijado los valores \(3, 5, 7,\ldots, 15\).

En segundo lugar hay que determinar el método que se va a usar para elegir el valor óptimo de \(k\). Para ello se usa el comando trainControl. En el ejemplo, se ha fijado validación cruzada en 10 partes. Se dividen los datos en 10 submuestras del mismo tamaño. Por turno, una de las submuestras se usa como test y las nueve restantes como entrenamiento. Se promedian los errores de los diez turnos y esto se hace para cada valor de \(k\). Se selecciona el valor que da el mejor resultado.

Posteriormente, se usa el comando train de manera similar a como se hizo para calcular la regla de Fisher, pero añadiendo los ajustes anteriores mediante los nuevos parámetros tuneGrid y trControl respectivamente:

# Define el grid de parámetros a probar
valores <- expand.grid(k = seq(3, 15, 2)) 

# Define los detalles del método de validación cruzada o remuestreo a utilizar
ajustes <- trainControl(method='cv',  # validación cruzada
             number = 10)  # diez submuestras

# Aplica el método seleccionando el valor óptimo de k
knn.datos <- train(y ~ .,
      data = datos,
      method = 'knn',
      tuneGrid = valores,
      trControl = ajustes)
knn.datos
## k-Nearest Neighbors 
## 
## 569 samples
##   2 predictor
##   2 classes: '0', '1' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold) 
## Summary of sample sizes: 511, 513, 512, 511, 512, 512, ... 
## Resampling results across tuning parameters:
## 
##   k   Accuracy   Kappa    
##    3  0.9068782  0.8000084
##    5  0.9015837  0.7899831
##    7  0.9120182  0.8116283
##    9  0.9102951  0.8080000
##   11  0.9138342  0.8147353
##   13  0.9103556  0.8064860
##   15  0.9103254  0.8074164
## 
## Accuracy was used to select the optimal model using  the largest value.
## The final value used for the model was k = 11.
plot(knn.datos)

predicciones <- predict(knn.datos, datos)
confusionMatrix(predicciones, datos$y)$table
##           Reference
## Prediction   0   1
##          0 336  25
##          1  21 187
confusionMatrix(predicciones, datos$y)$overall[1]
##  Accuracy 
## 0.9191564

Cuando se desea prefijar el valor de \(k\), una posibilidad es definir un grid de un solo punto. En el siguiente ejemplo, fijamos \(k=3\) y estimamos el error de clasificación mediante el habitual método de validación cruzada dejando uno fuera (leave-one-out CV):

valores <- expand.grid(k = 3) 
ajustes <- trainControl(method='LOOCV')   # leave-one-out CV
knn3.datos <- train(y ~ .,
                   data = datos,
                   method = 'knn',
                   tuneGrid = valores,
                   trControl = ajustes)
knn3.datos
## k-Nearest Neighbors 
## 
## 569 samples
##   2 predictor
##   2 classes: '0', '1' 
## 
## No pre-processing
## Resampling: Leave-One-Out Cross-Validation 
## Summary of sample sizes: 568, 568, 568, 568, 568, 568, ... 
## Resampling results:
## 
##   Accuracy   Kappa    
##   0.9050967  0.7970113
## 
## Tuning parameter 'k' was held constant at a value of 3

Para knn es necesario usar validación cruzada, tal y como hemos hecho en el ejemplo anterior. En otros casos puede ser conveniente fijar method='none' en trainControl.

Observaciones finales

Para otros métodos se recomienda usar el comando modelLookup para saber qué parámetros pueden optimizarse y cómo se llaman. Algunos ejemplos:

modelLookup('lda')
##   model parameter     label forReg forClass probModel
## 1   lda parameter parameter  FALSE     TRUE      TRUE
modelLookup('knn')
##   model parameter      label forReg forClass probModel
## 1   knn         k #Neighbors   TRUE     TRUE      TRUE
modelLookup('qda')
##   model parameter     label forReg forClass probModel
## 1   qda parameter parameter  FALSE     TRUE      TRUE
modelLookup('rpart')
##   model parameter                label forReg forClass probModel
## 1 rpart        cp Complexity Parameter   TRUE     TRUE      TRUE

En algunos métodos la implementación no permite optimizar algunos parámetros mediante validación cruzada. Sus valores se pueden fijar como parámetros adicionales de train.

Ejercicios

  1. Divide la muestra completa en dos submuestras, cada una de ellas con el 50 % de los datos, de forma que una de ellas se use para entrenamiento y otra para test en los ejercicios que siguen.
  2. Aplica la regla lineal de Fisher y la regla cuadrática (regla Bayes bajo normalidad) a la muestra de entrenamiento y clasifica los datos de la muestra de test. ¿Qué porcentaje de aciertos se obtiene?
  3. Construye un árbol de clasificación con la muestra de entrenamiento (método rpart). ¿Hay que determinar algún parámetro usando validacion cruzada? Si la respuesta es afirmativa, fija un conjunto de valores adecuado, define un procedimiento de validación cruzada y determina el valor óptimo del parámetro. Utiliza el árbol construido para clasificar los datos de la muestra de test y determina el porcentaje de ellos que han sido bien clasificados.

Referencias

Max Kuhn. Contributions from Jed Wing, Steve Weston, Andre Williams, Chris Keefer, Allan Engelhardt, Tony Cooper, Zachary Mayer, Brenton Kenkel, the R Core Team, Michael Benesty, Reynald Lescarbeau, Andrew Ziem, Luca Scrucca, Yuan Tang, Can Candan and Tyler Hunt. (2016). caret: Classification and Regression Training. R package version 6.0-72. https://CRAN.R-project.org/package=caret