Este es un Cuaderno o NoteBook de R que incluye una tutorial práctico sobre “Clasificación No Balanceada” dentro del “Seminario Permanente de Formación en Inteligencia Artificial Aplicada a la Defensa” conjunto entre el Mando de Adiestramiento y Doctrina y la Universidad de Granada.
Existen bloques de texto (como este) y bloques de código (con instrucciones en R). Cuando se ejecuta el código dentro del cuaderno, los resultados aparecen debajo del mismo.
Cualquier conjunto de datos con una distribución de clases desigual está técnicamente desequilibrado. Sin embargo, se dice que un conjunto de datos está desequilibrado cuando hay una desproporción significativa, o en algunos casos extrema, entre el número de ejemplos de cada clase del problema. En otras palabras, el desequilibrio de clases se produce cuando el número de ejemplos que representan una clase es mucho menor que los de las otras clases. Por lo tanto, una o más clases pueden estar subrepresentadas en el conjunto de datos. Una definición tan simple ha atraído mucha atención de los investigadores y profesionales debido al número de aplicaciones en el mundo real en las que los datos en bruto recopilados cumplen esta definición.
En este tutorial paso a paso, se pretende que usted:
Tal como se indicó anteriormente, para facilitar esta sesión “práctica”, el documento contiene trozos de código fuente de R que pueden ser ejecutados directamente. Intente ejecutar este primer trozo haciendo clic en el botón Run dentro del trozo o colocando el cursor dentro de él y pulsando Cmd+Shift+Enter (Ctrl+Shift+Enter en Windows).
print("Welcome to your first R NoteBook")
## [1] "Welcome to your first R NoteBook"
Si quiere añadir un nuevo trozo de código, sólo tiene que hacer clic en el botón Insertar trozo (Insert Chunk) de la barra de herramientas o pulsando Cmd+Opción+I (o Ctrl+Alt+I en Windows). De esta manera, puede añadir su propio código si así lo necesita.
Cuando guarde el cuaderno, se guardará un archivo HTML con el código y la salida junto a él (haz clic en el botón Previsualizar (preview) o pulsando Cmd+Mayús+K para obtener una vista previa del archivo HTML). En realidad, esta es una buena manera de compilar todas las tareas desarrolladas durante el tutorial.
Aquí hay un resumen de lo que se pretende cubrir en este Tutorial sobre Clasificación No Balancedad:
Tómese su tiempo. Trabaje en cada paso.
Instale los paquetes que vamos a usar hoy. Los paquetes son complementos o bibliotecas de terceros que se pueden usar en R.
NOTA: Puede que necesite otros paquetes, pero Caret debería preguntarnos si queremos cargarlos. Si tiene problemas con los paquetes, puede instalar los paquetes de caret y todos los paquetes que pueda necesitar escribiendo lo siguiente (elimine el carácter de comentario #):
#install.packages("caret", dependencies=c("Depends", "Suggests"),repos = "http://cran.r-project.org")
Ahora, cargue los paquetes que va a usar en este tutorial, los paquetes de caret
e imbalance
, entre otros.
library(caret)
## Loading required package: lattice
## Loading required package: ggplot2
library(imbalance)
## Warning: package 'imbalance' was built under R version 3.6.2
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(pROC)
## Type 'citation("pROC")' for a citation.
##
## Attaching package: 'pROC'
## The following objects are masked from 'package:stats':
##
## cov, smooth, var
library(tidyr)
## Warning: package 'tidyr' was built under R version 3.6.2
library(rlist)
library(Rtsne)
Como quizá ya sabe, el paquete caret
proporciona una interfaz consistente en cientos de algoritmos de Machine Learning y ofrece métodos útiles y convenientes para la visualización de datos, el remuestreo de datos, la puesta a punto de modelos y la comparación de modelos, entre otras características. Es una herramienta imprescindible para los proyectos de aprendizaje automático en R.
Una de sus ventajas para el uso del caret
es que integra directamente el uso de técnicas de preprocesamiento al establecer el método de control (se describirámás adelante) para la etapa de entrenamiento.
Para más información sobre el paquete R caret, ver la página web del paquete caret.
Para establecer un escenario controlado, utilizaremos dos datos artificiales diferentes, a saber, los datos de “círculo” y “subclúster”, que pueden encontrarse en diferentes estudios sobre clasificación desequilibrada. Se trata de problemas binarios, tanto en términos de atributos como de clases.
Además, podemos cargar algunos datos del paquete de imbalance
, como el del glass0 (véase Wikipedia). En este caso, podemos observar el comportamiento de los diferentes métodos en un caso de estudio real.
Esto es lo que vamos a hacer en este paso:
imbalance
). Esto es un adelante de la segunda parte del tutorial exclusiva de la citada biblioteca.Para los datos de “subcluster”, siga el mismo procedimiento que en el caso de “círculo”.
Afortunadamente, el paquete de desequilibrio nos proporciona el conjunto de datos de Glass0, entre otros. Cargue el conjunto de datos de la siguiente manera:
# adjunto el dataset glass0 al entorno
data(glass0)
# renombre el dataset por comodidad
dataset <- glass0
Ahora tiene el conjunto de datos glass0 cargado en R y accesible a través de la variable de dataset
.
Una ventaja de utilizar un nombre de variable genérico, como en este caso, es porque resulta útil si se desea copiar y pegar código entre proyectos y el conjunto de datos siempre tiene el mismo nombre.
En caso que haya descargado el conjunto de datos previamente, puede que quieras cargar los datos directamente desde un archivo CSV. También lo podemos hacer desde un enlace web.
circle.csv
(y subclus
) en su directorio de proyecto.Otra alternativa hubiese sido leer el CSV directamente desde la dirección Web (URL) del siguiente modo:
# cargar el fichero CSV desde local o directorio Web
dataset <- read.csv("https://www.dropbox.com/s/06k36xri8c1bepf/circle.csv?raw=1",header = TRUE)
# poner el nomber de los atributos en los datos (deben conocerse a priori o estar en el fichero)
colnames(dataset) <- c("Att1", "Att2", "Class")
#para asegurar que aparece como la primera clase
dataset$Class <- factor(dataset$Class,levels=c("positive","negative"))
Ahora tiene los datos del círculo cargados en R y accesibles a través de la variable dataset
.
Ahora es el momento de echar un vistazo a los datos. Esta etapa se ha realizado parcialmente en otras sesiones prácticas del Curso. En cualquier caso, siempre es positivo dar un repaso a esta tarea tan importante dentro del ciclo de Ciencia de Datos.
En concreto, en este paso vamos a echar un vistazo a los datos de diferentes maneras:
No se preocupe, cada inspección sobre los datos es un breve comando o instrucción. Se mostrarán por tanto comandos útiles que puede usar una y otra vez en futuros proyectos.
Podemos hacernos una idea rápida de cuántas instancias (filas) y cuántos atributos (columnas) contienen los datos con la función dim
.
# dimensions of dataset
dim(dataset)
## [1] 2390 3
Debería ver 2390 instancias y 3 atributos en el caso de los datos del “círculo”.
Es una buena idea tener una idea de los tipos de los atributos. Podrían ser dobles (reales), enteros, cadenas, factores y otros tipos.
Conocer los tipos es importante ya que le dará una idea de cómo resumir mejor los datos que tiene y los tipos de transformaciones que podría necesitar para preparar los datos antes de modelarlos.
En este ejemplo, debería ver que todos los datos de entrada son dobles y que el valor de la clase es un factor.
# Chequear la estructura y tipo de cada atributo
str(dataset)
## 'data.frame': 2390 obs. of 3 variables:
## $ Att1 : num 248 229 229 312 300 ...
## $ Att2 : num 222 219 221 238 254 ...
## $ Class: Factor w/ 2 levels "positive","negative": 1 1 1 1 1 1 1 1 1 1 ...
También es siempre una buena idea observar sus datos. Debería ver las primeras 5 filas de los datos de la siguiente manera:
# Observar las 5 primeras filas de datos
head(dataset)
La variable de clase es un factor. Un factor es una clase que tiene múltiples etiquetas de clase o niveles. Veamos los niveles. Fíjese en cómo podemos referirnos a un atributo por su nombre como una propiedad del conjunto de datos. En los resultados podemos ver que la clase tiene 2 etiquetas diferentes:
# listas los niveles para la clase
levels(dataset$Class)
## [1] "positive" "negative"
Este es un problema de clasificación binaria.
Ahora, finalmente, podemos echar un vistazo a un resumen de cada atributo.
Esto incluye la media, los valores mínimo y máximo, así como algunos percentiles (25, 50 o media y 75, e.g., los valores en estos puntos si ordenamos todos los valores de un atributo). Podemos ver aquí la distribución desigual entre las clases: 2335 vs. 55 (datos del círculo). Esto se confirma calculando el Ratio de Desequilibrio (IR).
# resumir la distribución de los atributos
summary(dataset)
## Att1 Att2 Class
## Min. : 4.442 Min. : 0.5926 positive: 55
## 1st Qu.:118.820 1st Qu.:118.3459 negative:2335
## Median :254.135 Median :249.1955
## Mean :254.997 Mean :253.1674
## 3rd Qu.:389.010 3rd Qu.:389.8631
## Max. :508.239 Max. :505.8055
imbalanceRatio(dataset)
## [1] 0.0235546
Ahora tenemos una idea básica sobre los datos. Necesitamos ampliarla con algunas visualizaciones.
Vamos a ver dos tipos de gráficos:
Empezamos con algunos gráficos univariados, es decir, gráficos de cada variable individual.
Es útil para la visualización tener una forma de referirse sólo a los atributos de entrada y sólo a los atributos de salida. Configurémoslo y llamemos a los atributos de entrada x y al de salida (o clase) y.
# Dividir entre variables de entrada y salida
x <- dataset[,1:2]
y <- dataset[,3]
Dado que las variables de entrada son numéricas, podemos crear gráficos de caja y bigote (boxplots) de cada una. Esto nos da una idea mucho más clara de la distribución de los atributos de entrada:
# boxplot para cada atributo en una imagen
par(mfrow=c(1,2))
for(i in 1:2) {
boxplot(x[,i], main=names(dataset)[i])
}
También podemos crear un barplot diagrama de barras (mejor un gráfico de queso) de la variable de la clase para obtener una representación gráfica de la distribución de la clase. Esto confirma lo que aprendimos en la última sección, que las instancias están distribuidas de manera desigual entre las dos clases.
# simple barplot para ver las clases
# plot(y)
# Un diagrama de queso es más apropiado
n_classes <- c(sum(y=="positive"),sum(y=="negative"))
pct <- round(n_classes/sum(n_classes)*100,digits=2)
lbls <- levels(dataset$Class)
lbls <- paste(lbls, pct) # add percents to labels
lbls <- paste(lbls,"%",sep="") # ad % to labels
pie(n_classes,labels = lbls, main="Class distribution")
Ahora podemos ver las interacciones entre las variables.
Primero veamos los diagramas de dispersión de todos los pares de atributos y coloreemos los puntos por clase. Además, como los gráficos de dispersión muestran que los puntos de cada clase están generalmente separados, podemos dibujar elipses alrededor de ellos. Nuestro objetivo es ver las relaciones entre los atributos de entrada (tendencias) y entre los atributos y los valores de las clases (elipses). En este caso los datos son tan simples que no hay una conclusión clara.
# scatterplot matrix
featurePlot(x=x, y=y, plot="ellipse")
También podemos mirar los gráficos de cajas y bigotes de cada variable de entrada de nuevo, pero esta vez divididos en gráficos separados para cada clase. Esto puede ayudar a determinar las separaciones lineales obvias entre las clases.
# boxplots para cada atributo
featurePlot(x=x, y=y, plot="box")
Las gráficas de dispersión pueden dar una gran idea de lo que se está tratando: puede ser interesante ver cuánto afecta una variable a otra. En otras palabras, se pretende ver si hay alguna correlación entre dos variables. Puede hacer gráficos de dispersión con el paquete ggvis, por ejemplo.
# Load in `ggvis`
library(ggvis)
##
## Attaching package: 'ggvis'
## The following object is masked from 'package:ggplot2':
##
## resolution
# Dataset scatter plot
dataset %>% ggvis(~Att1, ~Att2, fill = ~Class) %>% layer_points()
Ahora es el momento de crear algunos modelos de los datos y estimar su capacidad de predicción sobre datos no vistos.
Esto es lo que vamos a cubrir en este paso:
Como ya se ha comentado, nos centraremos en una partición tipo hold-out. De esta manera, tendremos sólo un entrenamiento y una partición de prueba, lo que puede causar un sesgo en nuestras conclusiones. Sin embargo, se muestra cómo proceder con un procedimiento adecuado de validación cruzada.
Debemos reajustar la semilla de números aleatorios antes de cada ejecución para asegurarnos de que la evaluación de cada algoritmo se realiza utilizando exactamente las mismas divisiones de datos. Esto asegura que los resultados sean directamente comparables.
set.seed(42) #Para asegurar la misma salida (en el mismo equipo)
#Una forma fácil de crear "particiones de datos":
trainIndex <- createDataPartition(dataset$Class, p = .75,list = FALSE, times = 1)
trainData <- dataset[ trainIndex,]
testData <- dataset[-trainIndex,]
#Chequear el IR para asegurar una partición estratificada
imbalanceRatio(trainData)
## [1] 0.0239726
imbalanceRatio(testData)
## [1] 0.02229846
#Ad hoc FCV
#testIndices <- createFolds(dataset$Class, k=5)
#First partition
#dataTrain <- dataset[-testIndices[[1]],]
#dataTest <- dataset[testIndices[[1]],]
No sabemos qué algoritmos serían buenos en este problema o qué configuraciones usar.
Evaluemos 3 metodologías diferentes con kNN:
En primer lugar, necesitamos crear dos funciones auxiliares para las etapas de aprendizaje y predicción. Por favor, tenga en cuenta que estamos optimizando el parámetro k de kNN a través de un procedimiento “grid search”. Si eliminamos esta parte, también podemos construir una función general para cualquier posible algoritmo de clasificación.
# a) Learning function
learn_model <-function(dataset, classifier, hyperp, ctrl, message){
model.fit <- train(Class ~ ., data = dataset, method = classifier, trControl = ctrl, preProcess =
c("center","scale"), metric="ROC",tuneGrid = hyperp)
model.pred <- predict(model.fit,newdata = dataset)
#Get the confusion matrix to see accuracy value and other parameter values
model.cm <- confusionMatrix(model.pred, dataset$Class,positive = "positive")
model.probs <- predict(model.fit,newdata = dataset, type="prob")
model.roc <- roc(dataset$Class,model.probs[,"positive"],color="green")
return(model.fit)
}
# b) Estimation function
test_model <-function(dataset, model.fit,message){
model.pred <- predict(model.fit,newdata = dataset)
#Get the confusion matrix to see accuracy value and other parameter values
model.cm <- confusionMatrix(model.pred, dataset$Class,positive = "positive")
print(model.cm)
model.probs <- predict(model.fit,newdata = dataset, type="prob")
model.roc <- roc(dataset$Class,model.probs[,"positive"])
#print(knn.roc)
plot(model.roc, type="S", print.thres= 0.5,main=c("ROC Test",message),col="blue")
#print(paste0("AUC Test ",message,auc(model.roc)))
return(model.cm)
}
Primero, comprobamos la obtención de los resultados de los datos originales. Por favor, recuerde que una validación interna de CV se utiliza para establecer los mejores parámetros para el modelo de kNN (véase más arriba). Esto se indica en la llamada de trainControl
.
#Execute model ("raw" data)
ctrl <- trainControl(method="repeatedcv",number=5,repeats = 3, classProbs=TRUE,summaryFunction = twoClassSummary)
#ctrl <- trainControl(method="none",classProbs=TRUE,summaryFunction = twoClassSummary)
classifier = "knn"
hyperp = data.frame(k = c(1,3,5,7,9,11))
#hyperp = data.frame(k = 3)
#classifier = "rpart"
#hyperp = data.frame(cp = 0.05)
model.raw <- learn_model(trainData,classifier,hyperp,ctrl,"RAW ")
## Setting levels: control = positive, case = negative
## Setting direction: controls > cases
#Podemos decidir representar los resultados del Grid search de los hiperparámetros del modelo
if (dim(hyperp)[1] > 1){
plot(model.raw,main="Grid Search RAW")
}
print(model.raw)
## k-Nearest Neighbors
##
## 1794 samples
## 2 predictor
## 2 classes: 'positive', 'negative'
##
## Pre-processing: centered (2), scaled (2)
## Resampling: Cross-Validated (5 fold, repeated 3 times)
## Summary of sample sizes: 1436, 1434, 1435, 1436, 1435, 1435, ...
## Resampling results across tuning parameters:
##
## k ROC Sens Spec
## 1 0.9167365 0.8351852 0.9982879
## 3 0.9442402 0.7861111 0.9992386
## 5 0.9755631 0.7240741 1.0000000
## 7 0.9798329 0.6833333 1.0000000
## 9 0.9873061 0.6287037 1.0000000
## 11 0.9993274 0.6212963 1.0000000
##
## ROC was used to select the optimal model using the largest value.
## The final value used for the model was k = 11.
cm.raw <- test_model(testData,model.raw,"RAW ")
## Confusion Matrix and Statistics
##
## Reference
## Prediction positive negative
## positive 10 0
## negative 3 583
##
## Accuracy : 0.995
## 95% CI : (0.9854, 0.999)
## No Information Rate : 0.9782
## P-Value [Acc > NIR] : 0.0009625
##
## Kappa : 0.867
##
## Mcnemar's Test P-Value : 0.2482131
##
## Sensitivity : 0.76923
## Specificity : 1.00000
## Pos Pred Value : 1.00000
## Neg Pred Value : 0.99488
## Prevalence : 0.02181
## Detection Rate : 0.01678
## Detection Prevalence : 0.01678
## Balanced Accuracy : 0.88462
##
## 'Positive' Class : positive
##
## Setting levels: control = positive, case = negative
## Setting direction: controls > cases
Ahora, incluimos la etapa de preprocesamiento en el método de control de caret
y obtenemos nuevos modelos. Primero con una simple técnica de submuestreo. El uso de RUS debe obtener un conjunto de entrenamiento perfectamente equilibrado eliminando instancias de la clase mayoritaria de forma aleatoria.
#Execute model ("preprocessed" data)
#Undersampling
ctrl <- trainControl(method="repeatedcv",number=5,repeats = 3,classProbs=TRUE,summaryFunction = twoClassSummary,sampling = "down")
model.us <- learn_model(trainData,classifier,hyperp,ctrl,"US ")
## Setting levels: control = positive, case = negative
## Setting direction: controls > cases
cm.us <- test_model(testData,model.us,"US ")
## Confusion Matrix and Statistics
##
## Reference
## Prediction positive negative
## positive 13 168
## negative 0 415
##
## Accuracy : 0.7181
## 95% CI : (0.6801, 0.7539)
## No Information Rate : 0.9782
## P-Value [Acc > NIR] : 1
##
## Kappa : 0.0973
##
## Mcnemar's Test P-Value : <2e-16
##
## Sensitivity : 1.00000
## Specificity : 0.71184
## Pos Pred Value : 0.07182
## Neg Pred Value : 1.00000
## Prevalence : 0.02181
## Detection Rate : 0.02181
## Detection Prevalence : 0.30369
## Balanced Accuracy : 0.85592
##
## 'Positive' Class : positive
##
## Setting levels: control = positive, case = negative
## Setting direction: controls > cases
Ahora debemos comprobar el comportamiento del enfoque de sobremuestreo aleatorio. El uso del ROS debe obtener un conjunto de entrenamiento perfectamente equilibrado al replicar instancias de la clase minoritaria de forma aleatoria.
#Oversampling
ctrl <- trainControl(method="repeatedcv",number=5,repeats = 3,
classProbs=TRUE,summaryFunction = twoClassSummary,sampling = "up")
model.os <- learn_model(trainData,classifier,hyperp,ctrl,"OS ")
## Setting levels: control = positive, case = negative
## Setting direction: controls > cases
cm.os <- test_model(testData,model.os,"OS ")
## Confusion Matrix and Statistics
##
## Reference
## Prediction positive negative
## positive 13 20
## negative 0 563
##
## Accuracy : 0.9664
## 95% CI : (0.9486, 0.9794)
## No Information Rate : 0.9782
## P-Value [Acc > NIR] : 0.9763
##
## Kappa : 0.5512
##
## Mcnemar's Test P-Value : 2.152e-05
##
## Sensitivity : 1.00000
## Specificity : 0.96569
## Pos Pred Value : 0.39394
## Neg Pred Value : 1.00000
## Prevalence : 0.02181
## Detection Rate : 0.02181
## Detection Prevalence : 0.05537
## Balanced Accuracy : 0.98285
##
## 'Positive' Class : positive
##
## Setting levels: control = positive, case = negative
## Setting direction: controls > cases
Finalmente, chequeamos la solución del estado del arte conocida como SMOTE. La aplicación de SMOTE debería obtener un conjunto de entrenamiento perfectamente equilibrado, creando nuevas instancias de la clase minoritaria
#SMOTE
ctrl <- trainControl(method="repeatedcv",number=5,repeats = 3, classProbs=TRUE,summaryFunction = twoClassSummary,sampling = "smote")
model.smt <- learn_model(trainData,classifier,hyperp,ctrl,"SMT ")
## Loading required package: grid
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
## Setting levels: control = positive, case = negative
## Setting direction: controls > cases
cm.smt <- test_model(testData,model.smt,"SMT ")
## Confusion Matrix and Statistics
##
## Reference
## Prediction positive negative
## positive 13 41
## negative 0 542
##
## Accuracy : 0.9312
## 95% CI : (0.9078, 0.9502)
## No Information Rate : 0.9782
## P-Value [Acc > NIR] : 1
##
## Kappa : 0.3658
##
## Mcnemar's Test P-Value : 4.185e-10
##
## Sensitivity : 1.00000
## Specificity : 0.92967
## Pos Pred Value : 0.24074
## Neg Pred Value : 1.00000
## Prevalence : 0.02181
## Detection Rate : 0.02181
## Detection Prevalence : 0.09060
## Balanced Accuracy : 0.96484
##
## 'Positive' Class : positive
##
## Setting levels: control = positive, case = negative
## Setting direction: controls > cases
Ahora tenemos 4 modelos aprendidos del mismo conjunto de datos pero con diferentes opciones de preprocesamiento. Cada modelo comprende una estimación de precisión diferente, y por lo tanto necesitamos comparar los modelos entre sí y seleccionar el más “exacto” en términos de métricas de rendimiento desequilibrado, por supuesto.
Podemos informar sobre el rendimiento de cada modelo creando primero una lista de los modelos creados y utilizando la función de resumen:
# resumen del acierto de los modelos
models <- list(raw = model.raw,us = model.us,os = model.os,smt = model.smt)
results <- resamples(models)
summary(results)
##
## Call:
## summary.resamples(object = results)
##
## Models: raw, us, os, smt
## Number of resamples: 15
##
## ROC
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## raw 0.9968254 0.9991097 0.9996429 0.9993274 1.0000000 1.0000000 0
## us 0.9588675 0.9870818 0.9922222 0.9886787 0.9942857 0.9996429 0
## os 0.9939286 0.9975000 0.9979365 0.9982000 0.9998413 1.0000000 0
## smt 0.9864672 0.9918365 0.9942857 0.9942990 0.9970635 1.0000000 0
##
## Sens
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## raw 0.3333333 0.5 0.6666667 0.6212963 0.75 0.875 0
## us 1.0000000 1.0 1.0000000 1.0000000 1.00 1.000 0
## os 1.0000000 1.0 1.0000000 1.0000000 1.00 1.000 0
## smt 1.0000000 1.0 1.0000000 1.0000000 1.00 1.000 0
##
## Spec
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## raw 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 0
## us 0.5641026 0.6890110 0.7200000 0.7079669 0.7400000 0.8085714 0
## os 0.9628571 0.9771429 0.9800000 0.9779297 0.9814571 0.9857143 0
## smt 0.8831909 0.9100000 0.9202279 0.9216182 0.9371429 0.9430199 0
También podemos crear un gráfico de los resultados de la evaluación del modelo y comparar la dispersión y el rendimiento medio de cada modelo. En el caso de los datos de círculos, todos los métodos de preprocesamiento son igualmente buenos en términos de sensibilidad (reconocimiento de clase positiva), pero el sobremuestreo es un poco mejor para la especificidad (reconocimiento de clase negativa).
El procedimiento ideal es obtener una población de medidas de rendimiento para cada algoritmo, porque cada algoritmo se evalúa varias veces (validación cruzada de k folds).
# comparar el acierto de los distintos modelos
bwplot(results)
#dotplot(results)
Finalmente podemos hacer otro gráfico diferente para comparar métricas adicionales para la clasificación no balanceada, como la precisión, el recall y la F1.
#LLevar a cabo una comparación sobre todas las métricas de imbalanced
comparison <- data.frame(model = names(models),
Sensitivity = rep(NA, length(models)),
Specificity = rep(NA, length(models)),
Precision = rep(NA, length(models)),
Recall = rep(NA, length(models)),
F1 = rep(NA, length(models)))
for (name in names(models)) {
cm_model <- get(paste0("cm.", name))
comparison[comparison$model == name, ] <- filter(comparison, model == name) %>%
mutate(Sensitivity = cm_model$byClass["Sensitivity"],
Specificity = cm_model$byClass["Specificity"],
Precision = cm_model$byClass["Precision"],
Recall = cm_model$byClass["Recall"],
F1 = cm_model$byClass["F1"])
}
comparison %>%
gather(x, y, Sensitivity:F1) %>%
ggplot(aes(x = x, y = y, color = model)) +
geom_jitter(width = 0.2, alpha = 0.5, size = 3)
Como comentamos inicialmente, existe un paquete CRAN denominado imbalance
que implementa algunas de las técnicas de preprocesamiento de datos más conocidas para la clasificación con clases no balanceadas. Debemos mirar más de cerca la documentación tanto en la página principal del paquete de desequilibrio, como en la función de ayuda
help("imbalance") #ver la documentación en la esquina inferior derecha
Utilizando la biblioteca imbalance
podemos considerar la aplicación de técnicas avanzadas basadas en SMOTE. Para ello, debemos centrarnos en la función de oversample
(“sobremuestreo”):
help("oversample") #ver la documentación en la esquina inferior derecha
En primer lugar, se actualiza la función de comparación para hacerlo todo más compacto:
#Funcion para comparar la salida de las distintas técnicas de Oversampling
perform.comparison.smote <- function(dataset, imb.ratio, methods){
## Train index generation
trainIndex <- createDataPartition(dataset$Class, p = .75,
list = FALSE, times = 1)
## train-test separation
trainData <- dataset[trainIndex,]
testData <- dataset[-trainIndex,]
## Basic model training (no grid search)
ctrl <- trainControl(method="none",classProbs=TRUE, summaryFunction = twoClassSummary)
## Change for another classifier (or include as parameter)
classifier = "rpart"
hyperp = data.frame(cp = 0.05)
basic.model <- learn_model(trainData, classifier,hyperp,ctrl, "RAW")
basic.model <- test_model(testData, basic.model, "RAW")
## Model training with each data generation policy
cm.models <- sapply(methods, function (x) {
aug.trainData <- oversample(trainData, ratio=imb.ratio, method=x, )
ctrl <- trainControl(method="none", classProbs=TRUE, summaryFunction = twoClassSummary)
model <- learn_model(aug.trainData, classifier,hyperp, ctrl, x)
test_model(testData, model, x)
}, simplify = F)
cm.models <- list.prepend(cm.models, RAW=basic.model)
## Metrics gathering
comparison <- lapply(cm.models, function(x){
x$byClass[c("Balanced Accuracy", "F1", "Precision","Recall", "Specificity")]
})
## Transformation into dataframe
comparison <- as.data.frame(do.call(rbind, comparison))
comparison$model <- rownames(comparison)
comparison
}
A continuación, cargamos alguno de los conjuntos de datos proporcionados por el paquete (banana, o glass0, por ejemplo). Seleccionamos varias técnicas o variantes SMOTE diferentes, para aplicarlas sobre los problemas anteriores.
# Load the data
dataset <- banana
dataset <- unique(dataset)
dataset[,-length(dataset)] <- sapply(dataset[,-length(dataset)], as.numeric)
repr.data <- dataset
colnames(repr.data) <- c("x", "y", "Class")
ggplot(repr.data) + geom_point(aes(x=x, y=y, color=Class))
# Apply preprocessing with oversample function
imb.ratio <- 0.65
methods <- c("SMOTE", "BLSMOTE", "DBSMOTE", "MWMOTE")
comparison <- perform.comparison.smote(dataset, imb.ratio, methods)
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## Confusion Matrix and Statistics
##
## Reference
## Prediction negative positive
## negative 580 32
## positive 12 34
##
## Accuracy : 0.9331
## 95% CI : (0.9113, 0.951)
## No Information Rate : 0.8997
## P-Value [Acc > NIR] : 0.001699
##
## Kappa : 0.5719
##
## Mcnemar's Test P-Value : 0.004179
##
## Sensitivity : 0.51515
## Specificity : 0.97973
## Pos Pred Value : 0.73913
## Neg Pred Value : 0.94771
## Prevalence : 0.10030
## Detection Rate : 0.05167
## Detection Prevalence : 0.06991
## Balanced Accuracy : 0.74744
##
## 'Positive' Class : positive
##
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## Confusion Matrix and Statistics
##
## Reference
## Prediction negative positive
## negative 555 19
## positive 37 47
##
## Accuracy : 0.9149
## 95% CI : (0.8909, 0.9351)
## No Information Rate : 0.8997
## P-Value [Acc > NIR] : 0.1069
##
## Kappa : 0.5794
##
## Mcnemar's Test P-Value : 0.0231
##
## Sensitivity : 0.71212
## Specificity : 0.93750
## Pos Pred Value : 0.55952
## Neg Pred Value : 0.96690
## Prevalence : 0.10030
## Detection Rate : 0.07143
## Detection Prevalence : 0.12766
## Balanced Accuracy : 0.82481
##
## 'Positive' Class : positive
##
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## [1] "Borderline-SMOTE done"
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## Confusion Matrix and Statistics
##
## Reference
## Prediction negative positive
## negative 535 29
## positive 57 37
##
## Accuracy : 0.8693
## 95% CI : (0.8411, 0.8941)
## No Information Rate : 0.8997
## P-Value [Acc > NIR] : 0.994891
##
## Kappa : 0.3907
##
## Mcnemar's Test P-Value : 0.003597
##
## Sensitivity : 0.56061
## Specificity : 0.90372
## Pos Pred Value : 0.39362
## Neg Pred Value : 0.94858
## Prevalence : 0.10030
## Detection Rate : 0.05623
## Detection Prevalence : 0.14286
## Balanced Accuracy : 0.73216
##
## 'Positive' Class : positive
##
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## [1] 8
## [1] 9
## [1] 8
## [1] 13
## [1] 8
## [1] 9
## [1] 5
## [1] 6
## [1] 7
## [1] 2
## [1] 5
## [1] 9
## [1] 9
## [1] 9
## [1] 11
## [1] 5
## [1] 6
## [1] 7
## [1] 8
## [1] 9
## [1] 6
## [1] 7
## [1] 12
## [1] 7
## [1] 3
## [1] 3
## [1] 5
## [1] 9
## [1] 3
## [1] 10
## [1] 7
## [1] 7
## [1] 7
## [1] 7
## [1] 5
## [1] 3
## [1] 5
## [1] 10
## [1] 3
## [1] 2
## [1] 4
## [1] 5
## [1] 9
## [1] 4
## [1] 8
## [1] 3
## [1] 5
## [1] 6
## [1] 6
## [1] 8
## [1] 6
## [1] 6
## [1] 8
## [1] 5
## [1] 9
## [1] 7
## [1] 4
## [1] 5
## [1] 2
## [1] 2
## [1] 4
## [1] 5
## [1] 6
## [1] 3
## [1] 7
## [1] 10
## [1] 4
## [1] 7
## [1] 2
## [1] 4
## [1] 8
## [1] 6
## [1] 6
## [1] 4
## [1] 4
## [1] 6
## [1] 2
## [1] 4
## [1] 8
## [1] 5
## [1] 4
## [1] 2
## [1] 7
## [1] 7
## [1] 6
## [1] 4
## [1] 9
## [1] 4
## [1] 3
## [1] 4
## [1] 13
## [1] 6
## [1] 2
## [1] 5
## [1] 10
## [1] 9
## [1] 9
## [1] 5
## [1] 8
## [1] 7
## [1] 8
## [1] 5
## [1] 9
## [1] 5
## [1] 7
## [1] 6
## [1] 7
## [1] 8
## [1] 7
## [1] 9
## [1] 4
## [1] 5
## [1] 8
## [1] 8
## [1] 13
## [1] 3
## [1] 7
## [1] 2
## [1] 4
## [1] 9
## [1] 7
## [1] 10
## [1] 10
## [1] 12
## [1] 7
## [1] 11
## [1] 8
## [1] 8
## [1] 6
## [1] 7
## [1] 4
## [1] 6
## [1] 8
## [1] 4
## [1] 6
## [1] 5
## [1] 8
## [1] 7
## [1] 8
## [1] 9
## [1] 12
## [1] 6
## [1] 9
## [1] 5
## [1] 9
## [1] 2
## [1] 11
## [1] 4
## [1] 5
## [1] 5
## [1] 3
## [1] 3
## [1] 6
## [1] 5
## [1] 12
## [1] 6
## [1] 8
## [1] 4
## [1] 6
## [1] 7
## [1] 9
## [1] 6
## [1] 3
## [1] 7
## [1] 4
## [1] 7
## [1] 7
## [1] 7
## [1] 5
## [1] 6
## [1] 8
## [1] 2
## [1] 3
## [1] 2
## [1] 3
## [1] 2
## [1] 3
## [1] 2
## [1] 2
## [1] 2
## [1] 2
## [1] 2
## [1] "DBSMOTE is Done"
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## Confusion Matrix and Statistics
##
## Reference
## Prediction negative positive
## negative 570 38
## positive 22 28
##
## Accuracy : 0.9088
## 95% CI : (0.8842, 0.9297)
## No Information Rate : 0.8997
## P-Value [Acc > NIR] : 0.24033
##
## Kappa : 0.4338
##
## Mcnemar's Test P-Value : 0.05281
##
## Sensitivity : 0.42424
## Specificity : 0.96284
## Pos Pred Value : 0.56000
## Neg Pred Value : 0.93750
## Prevalence : 0.10030
## Detection Rate : 0.04255
## Detection Prevalence : 0.07599
## Balanced Accuracy : 0.69354
##
## 'Positive' Class : positive
##
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
## Confusion Matrix and Statistics
##
## Reference
## Prediction negative positive
## negative 500 23
## positive 92 43
##
## Accuracy : 0.8252
## 95% CI : (0.794, 0.8535)
## No Information Rate : 0.8997
## P-Value [Acc > NIR] : 1
##
## Kappa : 0.3388
##
## Mcnemar's Test P-Value : 2.282e-10
##
## Sensitivity : 0.65152
## Specificity : 0.84459
## Pos Pred Value : 0.31852
## Neg Pred Value : 0.95602
## Prevalence : 0.10030
## Detection Rate : 0.06535
## Detection Prevalence : 0.20517
## Balanced Accuracy : 0.74805
##
## 'Positive' Class : positive
##
## Setting levels: control = negative, case = positive
## Setting direction: controls < cases
# Check results with kNN, DT or any other classifier (included in previous chunk)
comparison$model <- factor(comparison$model, levels=comparison$model)
comparison %>%
gather(x, y, "Balanced Accuracy":"Specificity") %>%
ggplot(aes(x = x, y = y, color = model)) +
geom_jitter(width = 0.2, alpha = 0.5, size = 3)
Ahora, haga una comparación gráfica entre el conjunto de datos original y el preprocesado (sólo para SMOTE, por ejemplo). Recordemos que, siendo un gráfico 2D, sólo debe seleccionar dos de las columnas de entrada.
# Visualize the data distribution between original and preprocess data.
representation <- function (ov_technique, data) {
aug.data <- oversample(data, ratio=imb.ratio, method=ov_technique, )
repr.data <- aug.data
colnames(repr.data) <- c("x1", "x2", "Class")
p <- ggplot(repr.data) + geom_point(aes(x=x1, y=x2, color=Class))
p + ggtitle(paste0("2D representation for ",ov_technique)) # for the main title)
}
sapply(methods, representation, data=dataset, simplify = F)
## [1] "Borderline-SMOTE done"
## [1] 6
## [1] 6
## [1] 5
## [1] 5
## [1] 2
## [1] 4
## [1] 8
## [1] 11
## [1] 6
## [1] 6
## [1] 8
## [1] 8
## [1] 3
## [1] 4
## [1] 8
## [1] 5
## [1] 7
## [1] 4
## [1] 5
## [1] 6
## [1] 10
## [1] 10
## [1] 7
## [1] 5
## [1] 9
## [1] 7
## [1] 6
## [1] 8
## [1] 6
## [1] 5
## [1] 7
## [1] 8
## [1] 7
## [1] 6
## [1] 8
## [1] 6
## [1] 10
## [1] 4
## [1] 5
## [1] 3
## [1] 5
## [1] 5
## [1] 10
## [1] 5
## [1] 7
## [1] 3
## [1] 2
## [1] 6
## [1] 2
## [1] 7
## [1] 9
## [1] 8
## [1] 7
## [1] 9
## [1] 8
## [1] 6
## [1] 12
## [1] 7
## [1] 5
## [1] 9
## [1] 8
## [1] 9
## [1] 5
## [1] 7
## [1] 10
## [1] 8
## [1] 6
## [1] 5
## [1] 9
## [1] 4
## [1] 6
## [1] 8
## [1] 4
## [1] 2
## [1] 5
## [1] 4
## [1] 8
## [1] 3
## [1] 10
## [1] 6
## [1] 6
## [1] 4
## [1] 11
## [1] 7
## [1] 9
## [1] 7
## [1] 4
## [1] 4
## [1] 3
## [1] 12
## [1] 6
## [1] 7
## [1] 9
## [1] 6
## [1] 10
## [1] 10
## [1] 8
## [1] 6
## [1] 10
## [1] 11
## [1] 13
## [1] 12
## [1] 3
## [1] 3
## [1] 7
## [1] 6
## [1] 7
## [1] 8
## [1] 6
## [1] 4
## [1] 6
## [1] 7
## [1] 13
## [1] 4
## [1] 8
## [1] 12
## [1] 5
## [1] 9
## [1] 14
## [1] 10
## [1] 10
## [1] 11
## [1] 7
## [1] 7
## [1] 8
## [1] 2
## [1] 2
## [1] 9
## [1] 9
## [1] 9
## [1] 8
## [1] 7
## [1] 6
## [1] 6
## [1] 3
## [1] 2
## [1] 10
## [1] 8
## [1] 10
## [1] 8
## [1] 5
## [1] 9
## [1] 6
## [1] 10
## [1] 8
## [1] 6
## [1] 9
## [1] 8
## [1] 12
## [1] 9
## [1] 2
## [1] 5
## [1] 9
## [1] 3
## [1] 3
## [1] 8
## [1] 5
## [1] 5
## [1] 7
## [1] 13
## [1] 13
## [1] 7
## [1] 2
## [1] 6
## [1] 7
## [1] 8
## [1] 6
## [1] 6
## [1] 4
## [1] 4
## [1] 9
## [1] 6
## [1] 7
## [1] 8
## [1] 6
## [1] 2
## [1] 6
## [1] 9
## [1] 8
## [1] 10
## [1] 8
## [1] 8
## [1] 5
## [1] 6
## [1] 9
## [1] 9
## [1] 10
## [1] 13
## [1] 6
## [1] 7
## [1] 10
## [1] 5
## [1] 8
## [1] 6
## [1] 6
## [1] 6
## [1] 9
## [1] 7
## [1] 8
## [1] 4
## [1] 9
## [1] 10
## [1] 13
## [1] 8
## [1] 8
## [1] 6
## [1] 6
## [1] 6
## [1] 2
## [1] 7
## [1] 8
## [1] 10
## [1] 7
## [1] 7
## [1] 11
## [1] 6
## [1] 2
## [1] 7
## [1] 8
## [1] 6
## [1] 3
## [1] 11
## [1] 4
## [1] 8
## [1] 2
## [1] 2
## [1] 2
## [1] 2
## [1] 4
## [1] 2
## [1] 3
## [1] 2
## [1] 4
## [1] 2
## [1] 2
## [1] 2
## [1] 3
## [1] 2
## [1] 3
## [1] 3
## [1] 2
## [1] "DBSMOTE is Done"
## $SMOTE
##
## $BLSMOTE
##
## $DBSMOTE
##
## $MWMOTE
Alternativamente, puede aplicar el método tsne
antes del gráfico para extraer dos únicas características.
library(Rtsne)
dataset <- ecoli1
#Dos variables que con un valor único son eliminadas
dataset <- subset(dataset, select=-c(Lip, Chg))
#Chequear posibles muestras respetidas
dataset <- unique(dataset)
#Por si hubiese algún valor numérico erróneamente cargado
dataset[,-length(dataset)] <- sapply(dataset[,-length(dataset)], as.numeric)
#No debería haber problemas puesto que hemos utilizado "unique"
tsne.suitable.ind <- !duplicated(dataset[,-length(dataset)])
data.2d <- as.data.frame(Rtsne(dataset[tsne.suitable.ind,-length(dataset)])$Y)
colnames(data.2d) <- c("x1", "x2")
#La clase no estaba inicialmente incluida
data.2d$Class <- dataset$Class[tsne.suitable.ind]
ggplot(data.2d) + geom_point(aes(x=x1, y=x2, color=Class)) + labs(title="Raw data with TSNE")
sapply(methods, representation, data=data.2d, simplify = F)
## [1] "Borderline-SMOTE done"
## [1] 6
## [1] 2
## [1] 4
## [1] 2
## [1] 4
## [1] 4
## [1] 5
## [1] 2
## [1] 6
## [1] 4
## [1] 4
## [1] 3
## [1] 4
## [1] 4
## [1] 4
## [1] 3
## [1] 4
## [1] 2
## [1] 2
## [1] 4
## [1] 3
## [1] 4
## [1] 3
## [1] 2
## [1] 5
## [1] 4
## [1] 6
## [1] 4
## [1] 3
## [1] 6
## [1] 3
## [1] 3
## [1] 7
## [1] 3
## [1] 4
## [1] 3
## [1] 2
## [1] 5
## [1] 3
## [1] 6
## [1] 4
## [1] 4
## [1] 5
## [1] 5
## [1] 4
## [1] 5
## [1] 3
## [1] 4
## [1] 2
## [1] 3
## [1] 4
## [1] 4
## [1] 2
## [1] 4
## [1] 6
## [1] 4
## [1] 5
## [1] 3
## [1] 7
## [1] 3
## [1] 2
## [1] 3
## [1] 5
## [1] 6
## [1] 6
## [1] 2
## [1] 3
## [1] 2
## [1] 2
## [1] "DBSMOTE is Done"
## $SMOTE
##
## $BLSMOTE
##
## $DBSMOTE
##
## $MWMOTE
Por último, es posible utilizar la propia función plotComparison
incluida en el paquete para observar las diferentes entre el conjunto de datos original y el preprocesado, siempre realizando una representación en dos dimensiones realizando todas las combinaciones de atributos del conjunto de datos.
#dataset corresponde con "ecoli1"
aug.data <- oversample(dataset, ratio=0.75, method="SMOTE")
plotComparison(dataset, aug.data, attrs = names(dataset)[1:3], cols = 2, classAttr = "Class")
Espero que hayan disfrutado este tutorial sobre la Clasificación No Balanceada en Aprendizaje Automático. Si necesita más detalles sobre cómo realizar cualquier tipo de tarea, por favor pregúnteme por correo electrónico en la siguiente dirección: alberto@decsai.ugr.es