Introducción al aprendizaje automatizado (machine learning)
Curso 012: Introducción al análisis de datos multivariantes
Introducción al aprendizaje automatizado (machine learning)
Introducción
El aprendizaje automatizado –machine learning (ML)– es una rama de la inteligencia artificial cuyo objetivo es que una máquina aprenda a partir de la experiencia. Básicamente, los algoritmos toman un conjunto de datos, los analizan para buscar en ellos ciertas pautas o patrones y una vez identificadas, las emplean para realizar predicciones. En otras palabras se trata de predecir el comportamiento futuro a partir de comportamientos pasados observados.
Dependiendo de cómo se aborde el problema del ML, los diferentes algoritmos se pueden agrupar en:
Aprendizaje supervisado:
Cuando las entradas al sistema (la base de conocimiento) están formadas por un conjunto de datos etiquetados a priori. Es decir, sabemos la clasificación correcta de un conjunto de datos, y a partir de ellos se va a generar una función predictora.
- Algunos algoritmos supervisados
- KNN, Random Forest, SVM, ANN, Naïve Bayes.
Aprendizaje no supervisado:
Las entradas al sistema están formadas por un conjunto de datos de los que se desconoce su clasificación correcta. En este caso, se espera que el algoritmo sea capaz de reconocer patrones para poder etiquetar y clasificar nuevos datos de entrada.
- Algunos algoritmos no supervisados
- K-means, clustering jerárquico
Pasos para aplicar ML
Recoger y almacenar datos
Explorar y preparar los datos
Entrenar al clasificador con nuestros datos
Evaluar el desempeño del modelo
Mejorarlo si fuese necesario
Una vez completados estos pasos, nuestro modelo puede ser implementado en la clasificación de nuevos datos.
KNN
K-Nearest Neighbours (Los k vecinos más próximos) es un algoritmo de clasificación supervisada, basado en distancias. Generalmente la distancia Euclídea
Siendo \(p\) y \(q\) dos observaciones, y \(p_1, p_2,...,p_n\), y \(q_1, q_2,...,q_n\) los valores que toman esas observaciones en las \(n\) variables, la distancia euclídea entre estas dos observaciones se define como:
\[ dist(p,q) = \sqrt{(p_1-q_1)^2+(p_2-q_2)^2+...+(p_n-q_n)^2}\]
Su funcionamiento es muy simple: dado un conjunto de observaciones etiquetadas —clasificadas—, el algoritmo va a asignar una nueva observación a la clase —al grupo— más cercana.
Esa distancia se puede definir de diferentes maneras y en función de cómo se haga los resultados podrían variar.
En el ejemplo de las siguientes imágenes se trata de clasificar un tomate a alguno de los grupos ya definidos.
El principal problema que puede plantear este método es decidir sobre cuántos vecinos se calcula la distancia
No hay una única regla, una formula general dice que el número de vecinos a tener en cuenta, está en función del tamaño de la muestra \(n\)
\[K = n^{\frac{1}{2}}\]
Otros métodos incluirían ensayos con diferentes números de vecinos y posteriormente evaluar el desempeño de la clasificación —curvas ROC, Cross-Validation, …— para quedarnos con aquel número con el que se obtenga un mejor desempeño.
Implementación de KNN en R
data( iris )
Preprocesar datos
Incluye normalizar si es necesario, reducir el número de variables si fuese necesario….
P.e. \(\frac{x-\overline{x}}{\sigma^2}\), es lo que hace la función scale()
NormIris <- as.data.frame( scale( iris[, 1:4 ], scale = TRUE, center = TRUE ) )
summary( NormIris )
Sepal.Length Sepal.Width Petal.Length
Min. :-1.86378 Min. :-2.4258 Min. :-1.5623
1st Qu.:-0.89767 1st Qu.:-0.5904 1st Qu.:-1.2225
Median :-0.05233 Median :-0.1315 Median : 0.3354
Mean : 0.00000 Mean : 0.0000 Mean : 0.0000
3rd Qu.: 0.67225 3rd Qu.: 0.5567 3rd Qu.: 0.7602
Max. : 2.48370 Max. : 3.0805 Max. : 1.7799
Petal.Width
Min. :-1.4422
1st Qu.:-1.1799
Median : 0.1321
Mean : 0.0000
3rd Qu.: 0.7880
Max. : 1.7064
Previsualizar datos
plot( iris[ ,1:4 ], col = iris$Species )
Generar las muestras
Del conjunto de datos seleccionaremos \(2/3\) de ellos para entrenar la máquina y el resto se reserva para testar los resultados.
#generar muestras
iris.train <- iris[ sample( c( 1:150 ), 100 ), 1:5 ]
iris.test <- iris[ sample( c( 1:150 ), 50 ), 1:5 ]
Entrenar/testar clasificador
La función knn del paquete class entrena y comprueba la clasificación en un único paso. Como parámetros le pasamos en conjunto de datos de entrenamiento, el conjunto de datos para el test, las clases a las que pertenece cada elemento, y el número de vecinos.
# Genera modelo y hace predicción a la vez
iris.pred <- knn( train = iris.train[ , 1:4 ], test = iris.test[ , 1:4 ],
cl = iris.train[ ,5 ], k = 2 )
# predicción
print(iris.pred)
[1] versicolor versicolor virginica setosa virginica
[6] setosa setosa setosa virginica virginica
[11] virginica versicolor versicolor setosa setosa
[16] setosa virginica virginica setosa setosa
[21] virginica setosa setosa setosa setosa
[26] virginica versicolor versicolor virginica setosa
[31] setosa versicolor versicolor virginica versicolor
[36] setosa virginica versicolor setosa setosa
[41] virginica virginica virginica setosa virginica
[46] versicolor versicolor setosa versicolor versicolor
Levels: setosa versicolor virginica
# realidad
print(iris.test[ ,5 ])
[1] versicolor versicolor virginica setosa virginica
[6] setosa setosa setosa virginica virginica
[11] virginica versicolor versicolor setosa setosa
[16] setosa virginica virginica setosa setosa
[21] virginica setosa setosa setosa setosa
[26] virginica versicolor versicolor virginica setosa
[31] setosa versicolor versicolor virginica versicolor
[36] setosa virginica versicolor setosa setosa
[41] virginica virginica virginica setosa virginica
[46] versicolor versicolor setosa versicolor versicolor
Levels: setosa versicolor virginica
Validar resultados
table( Predic = iris.pred, Test = iris.test[,5])
Test
Predic setosa versicolor virginica
setosa 20 0 0
versicolor 0 14 0
virginica 0 0 16
Si encuentro un iris en el campo y mido pétalo y sépalo, puedo utilizar mi clasificador para determinar a que variedad pertenece.
miIris <- c( 5.0, 3.5, 1.3, 0.1 )
mi_predict <- knn( train = iris.train[ ,1:4 ], test = miIris,
cl = iris.train[ ,5 ], k = 2, prob = TRUE )
mi_predict
[1] setosa
attr(,"prob")
[1] 1
Levels: setosa versicolor virginica
Máquina de soporte de vectores (SVM)
Algoritmo de clasificación supervisada, donde dado un conjunto de datos etiquetados, el algoritmo construye un límite o frontera óptimo, llamado hiperplano, que separa los datos según su categoría para posteriormente asignar nuevos datos al grupo con mayor probabilidad de pertenencia.
El fundamento básico es el mismo que en el caso de KNN:
Conjunto de datos etiquetados
Construcción de un modelo
Dado un nuevo dato de etiqueta desconocida
Modelo es capaz de predecir a que clase pertenece
A diferencia de KNN, el algoritmo busca los límites o fronteras entra las clases. Esa frontera en un espacio de dos dimensiones —dos variables— sería una línea, en 3 dimensiones sería un plano, y genéricamente para un espacio multidimensional —multivariante— sería un hiperplano. Este hiperplano separa el hiperespacio en diferentes regiones, donde las observaciones que caigan dentro de la misma región pertenecerán a la misma clase.
La dificultad radica en definir cuál es el hiperplano óptimo. El algoritmo busca una frontra que maximize la distancia entre ella y los elementos marginales de cada clase.
En ocasiones definir esa frontera es intuitivo, pero lo normal cuando se trabaja con más de dos variables es que la definición de ese hiperplano se vuelva muy compleja y computacionalmente muy costosa.
Para simplificar el cálculo del hiperplano se pueden realizar una serie de transformaciones sobre los datos de manera que sea menos compleja la obtención de las diferentes regiones y por tanto hacer menos costoso —computacionalmente hablando— el aprendizaje.
Para poder llevar a cabo esto se utilizan distintas funciones kernel que pueden llevar a cabo diferentes tipos de transformaciones. Estas son algunas de ellas:
Polinomial: \(K(x_i, x_j) = (x_ix_j)^n\)
Perceptrón: \(K(x_i, x_j) = \left \|x_ix_j \right \|\)
Base radial Gaussiana: \(K(xi, xj)=e^{-\frac{(xi-xj)^2}{2\sigma^2}}\)
Sigmoidal: \(K(xi, xj)=tanh(xi\cdot xj-\Theta)\)
Gráficamente la función kernel aplica una transformación sobre los datos, simplificando enormemente la definición de la frontera.
Implementación de SVM en R
data( iris )
Preprocesar datos
Incluye normalizar si es necesario, reducir el número de variables si fuese necesario…
Generar las muestras
#generar muestras
iris.train <- iris[ sample( c( 1:150 ), 100 ), 1:5 ]
iris.test <- iris[ sample( c( 1:150 ), 30 ), 1:5 ]
Aplicar SVM
Con la función svm del paquete e1071 se puede generar generar un modelo mediante algoritmo de soporte de vectores. Como parámetros hay que proporcionar los datos en forma de fórmula de R, donde la variable respuesta son las clases frente al resto de variables predictoras Species ~ .
, hay que definir sobre que datos se está refiriendo la fórmula, si queremos normalizar los datos y el tipo de función kernel que queremos utilizar.
# Esta función sólo genera el modelo
svm_model <- svm( Species ~ . , data = iris.train, scale = TRUE, kernel = "linear", probability = TRUE)
summary( svm_model )
Call:
svm(formula = Species ~ ., data = iris.train, kernel = "linear",
probability = TRUE, scale = TRUE)
Parameters:
SVM-Type: C-classification
SVM-Kernel: linear
cost: 1
gamma: 0.25
Number of Support Vectors: 25
( 10 2 13 )
Number of Classes: 3
Levels:
setosa versicolor virginica
Una vez entrenado el modelo, podemos hacer predicción con el conjunto de datos de iris.test
.
SVM_predict <- predict( svm_model, iris.test[,1:4], probability = TRUE, decision.values = TRUE )
summary(SVM_predict)
setosa versicolor virginica
8 9 13
Contrastar la predicción con la realidad
# Versión simple
table( SVM_predict, linearealidad = as.factor( iris.test[ ,5 ] ) )
linearealidad
SVM_predict setosa versicolor virginica
setosa 8 0 0
versicolor 0 9 0
virginica 0 0 13
# Versión más completa
CrossTable( x = SVM_predict, y = iris.test[ ,5 ], dnn = c("Predicción", "Real" ) )
Cell Contents
|-------------------------|
| N |
| Chi-square contribution |
| N / Row Total |
| N / Col Total |
| N / Table Total |
|-------------------------|
Total Observations in Table: 30
| Real
Predicción | setosa | versicolor | virginica | Row Total |
-------------|------------|------------|------------|------------|
setosa | 8 | 0 | 0 | 8 |
| 16.133 | 2.400 | 3.467 | |
| 1.000 | 0.000 | 0.000 | 0.267 |
| 1.000 | 0.000 | 0.000 | |
| 0.267 | 0.000 | 0.000 | |
-------------|------------|------------|------------|------------|
versicolor | 0 | 9 | 0 | 9 |
| 2.400 | 14.700 | 3.900 | |
| 0.000 | 1.000 | 0.000 | 0.300 |
| 0.000 | 1.000 | 0.000 | |
| 0.000 | 0.300 | 0.000 | |
-------------|------------|------------|------------|------------|
virginica | 0 | 0 | 13 | 13 |
| 3.467 | 3.900 | 9.633 | |
| 0.000 | 0.000 | 1.000 | 0.433 |
| 0.000 | 0.000 | 1.000 | |
| 0.000 | 0.000 | 0.433 | |
-------------|------------|------------|------------|------------|
Column Total | 8 | 9 | 13 | 30 |
| 0.267 | 0.300 | 0.433 | |
-------------|------------|------------|------------|------------|
Igual que en el caso de KNN podemos hacer una predicción sobre nuestro modelo con una muestra recogida en el campo, y obtendremos a qué clase ha sido asignada y con que probabilidad.
miIris <- matrix( c( 5.0, 3.5, 1.3, 0.1 ), nrow = 1, ncol = 4 )
colnames( miIris ) <- colnames( iris[ , 1:4 ] )
# predict requiere un matrix o data.frame no un vector
predict( svm_model, miIris, probability = TRUE )
1
setosa
attr(,"probabilities")
virginica setosa versicolor
1 0.01051367 0.972838 0.01664836
Levels: setosa versicolor virginica
Naïve Bayes
Es un clasificador probabilístico “ingenuo” basado en el teorema de Bayes, que asume que la probabilidad de cada variable es independiente de las demás. Básicamente su lógica se basa en que dado un conjunto de datos de entrenamiento etiquetados, el clasificador calcula la probabilidad observada para cada clase, en función de los valores de sus variables. Cuando es usado posteriormente para predecir datos sin etiquetar, asigna estos datos a la clase con mayor probabilidad de pertenencia.
Refrescando la memoria bayesiana
Probabilidad de un evento
\[ P(X) = \frac{Casos\ \ favorables}{Casos\ \ posibles} \]
Probabilidad de que al tirar un dado salga un 3:
\[ P(X=3) = \frac{1}{6} \]
Probabilidad condicionada
Probabilidad de que suceda un evento cuando ya ha sucedido otro:
\[ P (Y \mid X) \]
Probabilidad de que Y tome un valor determinado cuando X ya ha tomado uno.
Ejemplo: probabilidad de obtener un 10 entre dos dados cuando el primero ha salido 4
\[ P (Y=10 \mid X=4) \]
Teorema de Bayes
Basado en el Teorema de Bayes:
\[ P(Y \mid X) = \frac{P(X \mid Y) \, P(Y)}{P(X)} \]
En castellano
\[ P(posteriori) = \frac{P(probabilidad\ condicional) \, P(a\ priori)}{P(total)} \]
Veamos como funciona Naïve Bayes como predictor con un ejemplo: tenemos datos históricos sobre si se ha jugado o no al tenis con una serie de parámetros meteorológicos muy simples.
En base a esto, y empleando la lógica bayesiana determinaremos qué probabilidad tenemos de que haya partido un día nublado, frío, con alta humedad y sin viento.
Cielo | Temperatura | Humedad | Viento | Jugar |
---|---|---|---|---|
Soleado | Calor | Alta | Si | No |
Soleado | Calor | Alta | No | Si |
Soleado | Templado | Baja | Si | Si |
Nublado | Frio | Baja | No | Si |
Nublado | Calor | Normal | No | Si |
Nublado | Frio | Alta | Si | No |
LLuvia | Templado | Alta | No | No |
Probabilidades
¿Jugaremos al tenis (\(Y\)) un día nublado, frío, con humedad alta y sin viento (\(X\))?
\[ P(Y=Si \mid X_{v1,v2,v3,v4}) = \frac{P(X \mid Y)_{vi} \cdot P(Y)}{P(total)} \]
Probabilidades
\[ P(SI_{jugar}) = 4/7 \ \ P(NO_{jugar}) = 3/7 \] \[ P(nublado \mid SI_{jugar}) = 2/4 \ \ P(nublado \mid NO_{jugar}) = 1/3 \] \[ P(frio \mid SI_{jugar}) = 1/4 \ \ P(frio \mid NO_{jugar}) = 1/3 \] \[ P(Alta_{humedad} \mid SI_{jugar}) = 1/4 \ \ P(Alta_{humedad} \mid NO_{jugar}) = 3/3 \] \[ P(Si_{viento} \mid SI_{jugar}) = 1/4 \ \ P(Si_{Viento} \mid NO_{jugar}) = 2/3 \]
La hora de la verdad, ¿Jugaremos?
\[ P(Y=Si \mid X_{v1,v2,v3,v4}) = \frac{2/4 \cdot 1/4 \cdot 1/4 \cdot 1/4 \cdot 4/7 =0.024}{0.024+0.21}=0.103 \] \[ P(Y=No \mid X_{v1,v2,v3,v4}) = \frac{1/3 \cdot 1/3 \cdot 3/3 \cdot 2/3 \cdot 3/7 = 0.21}{0.024+0.21}=0.897 \]
Implementación de Naïve Bayes en R
iris.NB <- naiveBayes( Species ~ ., data = iris.train )
iris.NB
Naive Bayes Classifier for Discrete Predictors
Call:
naiveBayes.default(x = X, y = Y, laplace = laplace)
A-priori probabilities:
Y
setosa versicolor virginica
0.27 0.36 0.37
Conditional probabilities:
Sepal.Length
Y [,1] [,2]
setosa 5.007407 0.3037280
versicolor 6.008333 0.5044799
virginica 6.540541 0.6555148
Sepal.Width
Y [,1] [,2]
setosa 3.451852 0.3055517
versicolor 2.772222 0.3239145
virginica 2.935135 0.3242223
Petal.Length
Y [,1] [,2]
setosa 1.488889 0.1887883
versicolor 4.313889 0.4636210
virginica 5.527027 0.5378190
Petal.Width
Y [,1] [,2]
setosa 0.2444444 0.1154701
versicolor 1.3388889 0.1989656
virginica 2.0513514 0.2775104
predNB <- predict( iris.NB, iris.test[,1:4] )
predNB
[1] versicolor versicolor setosa setosa virginica
[6] virginica setosa virginica setosa setosa
[11] versicolor setosa virginica virginica versicolor
[16] setosa versicolor virginica versicolor virginica
[21] virginica setosa virginica virginica virginica
[26] virginica versicolor virginica versicolor virginica
Levels: setosa versicolor virginica
Contrastar la predicción
table( Predicción = predNB, Real = iris.test[,5] )
Real
Predicción setosa versicolor virginica
setosa 8 0 0
versicolor 0 8 0
virginica 0 1 13
Y con mi muestra encontrada en el campo
miIris<-matrix(c(5.0, 3.5, 1.3, 0.1), nrow=1, ncol=4)
colnames(miIris)<-colnames(iris[,1:4])
# predict requiere un matrix o data.frame no un vector
pdr <- predict(iris.NB, miIris, probability = TRUE )
summary(pdr)
setosa versicolor virginica
1 0 0
Redes Neuronales Artificiales (ANN)
Paradigma de aprendizaje automatizado inspirado en sistemas nerviosos biológicos, constituidos por una serie de nodos (neuronas) y una serie de conexiones entre los nodos (sinapsis).
La topología básica consta de:
- Unos nodos de entrada al sistema, generalmente tantos nodos como variables.
- Una serie de capas ocultas intermedias con un número variable de nodos.
- Unos nodos de salida, uno por cada respuesta.
Cada nodo de entrada tiene asociado un peso \(W_i\) y en cada nodo se aplica una función de activación (opcional) y una de propagación con la suma de las entradas ponderadas por sus pesos.
A pesar del nombre, su parecido con los sistemas biológicos se queda en la estructura física, es decir una serie de nodos interconectados formando una red con entradas y salidas.
Pero aunque parezca algo complejo, una especie de caja negra en la que le damos información y esperamos a que “eso” obre el milagro y nos de un resultado, el fundamento es bastante simple:
Dados una serie de parámetros de entrada, las redes neuronales buscan la forma de combinarlos para obtener el resultado esperado.
Aunque hay muchas topologías, la más común es la conocida como perceptrón
Componentes
\(N\) entradas, \(x_1,..., x_n\)
Cada entrada con un peso \(x\), \(x_1,...,x_n\)
Un nodo de entrada extra llamada bias (intercepto)
Suma de las entradas ponderada por sus pesos:
\[y = \sum{x_0w_0,...,x_nw_n}\]
- Función de activación p.e.:
\(f_a(x) = 1\) si \(y>0\), \(f_a(x) = -1\) si \(x\leq0\)
Veámoslo con un ejemplo:
Supongamos que tenemos la nota de dos exámenes y la nota final. No sabemos como pondera el profesor las notas de los exámenes para obtenerla. La red neuronal irá probando distintas combinaciones hasta que de con la correcta.
Nuestros parámetros de entrada son las dos notas y la salida será la nota final esperada.
Esto podría complicarse. Imaginemos ahora que tenemos las notas de dos exámenes más la de una práctica. Hemos suspendido un examen y sin embargo hemos aprobado la asignatura. En este caso ya no es tan trivial ponderar el peso de cada examen.
Pero podría ser peor. La retorcida mente de este profesor, podría tener en cuenta, la asistencia, la participación, la actitud con los compañeros además de las notas de teoría y prácticas. Además a cada uno de estos parámetros le podría dar un peso diferente, incluso dar pesos diferentes a combinaciones de parámetros. Es más, podría ser que no todas las combinaciones fuesen lineales, algunas podrían ser cuadráticas, logarítmicas,…
Pero afortunadamente para nosotros, no nos tenemos que preocupar de todo esto, lo único que tendríamos que hacer básicamente, es decir cuáles son nuestras notas de entrada y nuestra nota final. La red se encarga de generar el modelo y de esta forma la próxima vez que sepamos las notas iniciales, no tendremos que esperar a saber la nota final, nuestro modelo nos lo va a predecir.
Implementación de redes neuronales en R
Preprocesar datos
Incluye normalizar si es necesario, reducir el número de variables si fuese necesario….
Generar las muestras
Este paso es igual lo que hemos visto en los ejemplos anteriores
#generar muestras
iris.train <- iris[ sample( c( 1:150 ), 100 ), 1:5 ]
iris.test <- iris[ sample( c( 1:150 ), 50 ), 1:5 ]
Binarizar las categorias
En este caso hay que generar nuevas columnas de VERDADERO/FALSO para pertenencia de cada observación a cada clase —especie—.
iris.train <- cbind( iris.train, iris.train$Species == "setosa" )
iris.train <- cbind( iris.train, iris.train$Species == "versicolor" )
iris.train <- cbind( iris.train, iris.train$Species == "virginica" )
names(iris.train)[6]<-"setosa"
names(iris.train)[7]<-"versicolor"
names(iris.train)[8]<-"virginica"
head(iris.train)
Sepal.Length Sepal.Width Petal.Length Petal.Width
112 6.4 2.7 5.3 1.9
40 5.1 3.4 1.5 0.2
39 4.4 3.0 1.3 0.2
68 5.8 2.7 4.1 1.0
95 5.6 2.7 4.2 1.3
119 7.7 2.6 6.9 2.3
Species setosa versicolor virginica
112 virginica FALSE FALSE TRUE
40 setosa TRUE FALSE FALSE
39 setosa TRUE FALSE FALSE
68 versicolor FALSE TRUE FALSE
95 versicolor FALSE TRUE FALSE
119 virginica FALSE FALSE TRUE
Entrenar la red neuronal
La función neuralnet del paquete del mismo nombre requiere la introducción de parámetros de forma similar a como se hizo en SVM, mediante una foŕmula, donde las variables dependientes —setosa,versicolor,virgínica— lo son en función de las predictoras. Podemos definir también la topología de la red, es decir cuántas capas y cuántos nodos por capa.
Una vez entrenada nuestra red, podemos visualizar su topología, los pesos ponderados asignados a cada nodo. Con la función print podemos obtener bastante información acerca del modelo.
iris.nnt <- neuralnet( setosa + versicolor + virginica ~ Sepal.Length +
Sepal.Width +
Petal.Length +
Petal.Width,
data = iris.train, hidden = c( 3, 2 ) )
# print(iris.nnt) #Ejecutar para ver los resultados
plot( iris.nnt, col.intercept = "blue" , rep="best")
Predicción
Con los datos que nos reservamos para el test, vamos a hacer la predicción, en este caso en lugar de la función predict usada anteriormente, empleamos compute
pred.nn <- compute( iris.nnt, iris.test[ 1:4 ] )
# print(pred.nn) # ejecutar para ver los resultados
El resultado de la predicción no es tan “amigable” como en los otros algoritmos vistos, pero con un poco de código lo podemos extraer y dejar más claro.
resultado <- 0
for ( i in 1:dim( pred.nn$net.result )[1] ){
resultado[i] <- which.max( pred.nn$net.result[ i, ] )
}
resultado[ resultado == 1 ] <- "setosa"
resultado[ resultado == 2 ] <- "versicolor"
resultado[ resultado == 3 ] <- "virginica"
table(Predicción = resultado, Real = iris.test[,5])
Real
Predicción setosa versicolor virginica
setosa 15 0 0
versicolor 0 15 1
virginica 0 2 17
Evaluar la eficacia del modelo
Vamos a estudiar algunos de los mecanismos existentes de validación de la clasificación realizada por nuestro modelo predictivo. para ello generaremos un modelo nuevo con datos de pacientes que tienen un tumor, cuyo diagnóstico está etiquetado como benigno o maligno.
La primera columna contiene un identificador del sujeto, la segunda corresponde al diagnóstico y el resto son variables medidas al tumor.
wdbc <- read.csv( "http://gauss.inf.um.es/datos/wdbc.csv" )
#eliminar columna de ID.
wdbc <- wdbc[ -1 ]
# Normalizar los datos
wdbc_N <- as.data.frame( scale( wdbc[2:31], scale = TRUE, center = TRUE ) )
wdbc_N$diagnosis <- wdbc$diagnosis
Crear datos de entrenamiento y datos de test
# Datos
w_train <- wdbc_N[ sample( c( 1 : nrow( wdbc_N ) ), 425 ), ]
w_test <- wdbc_N[ sample( c( 1 : nrow( wdbc_N ) ), 140 ), ]
# Etiquetas
w_train_label <- w_train$diagnosis
w_test_label <- w_test$diagnosis
Entrenar el modelo
w_predict<- knn( w_train[ , c( 1:( ncol(w_train) -1 ) ) ],
w_test[ , c( 1:( ncol(w_test) -1 ) ) ],
cl = w_train_label, k = 21, prob = TRUE )
w_predict
[1] B B B B B B B M B M B B M B B B B B B B B B M B M M B
[28] M B B B B B B B B B M M B M B B B B M M B M B B M B B
[55] B B B B B M M B B B B B B B B B B B M B M M B M M B B
[82] B B B B M B M B M B M M M B M M B B B M M B M B M M B
[109] M B M B B B M B B M M M B M B B B M M B M B B B B M B
[136] B B B M M
attr(,"prob")
[1] 0.8571428571 0.8571428571 0.7142857143 1.0000000000
[5] 1.0000000000 0.7142857143 0.9523809524 0.6666666667
[9] 0.7142857143 1.0000000000 1.0000000000 0.8095238095
[13] 0.8095238095 0.9047619048 0.8095238095 0.9523809524
[17] 1.0000000000 1.0000000000 1.0000000000 1.0000000000
[21] 1.0000000000 1.0000000000 1.0000000000 1.0000000000
[25] 1.0000000000 1.0000000000 0.8095238095 1.0000000000
[29] 0.5714285714 1.0000000000 0.9523809524 1.0000000000
[33] 1.0000000000 0.9523809524 0.8571428571 1.0000000000
[37] 0.9523809524 1.0000000000 1.0000000000 0.8095238095
[41] 1.0000000000 0.9523809524 0.7142857143 0.5714285714
[45] 1.0000000000 1.0000000000 1.0000000000 1.0000000000
[49] 1.0000000000 0.9523809524 0.6190476190 1.0000000000
[53] 0.9047619048 0.8095238095 0.9523809524 0.7142857143
[57] 0.8571428571 0.7619047619 0.9523809524 1.0000000000
[61] 0.9523809524 0.9047619048 1.0000000000 0.9047619048
[65] 1.0000000000 0.8095238095 0.5238095238 1.0000000000
[69] 1.0000000000 0.8095238095 1.0000000000 0.6190476190
[73] 0.8095238095 1.0000000000 0.9523809524 1.0000000000
[77] 1.0000000000 1.0000000000 1.0000000000 1.0000000000
[81] 1.0000000000 0.7142857143 0.9523809524 0.9523809524
[85] 1.0000000000 0.9047619048 1.0000000000 0.9047619048
[89] 1.0000000000 1.0000000000 0.9523809524 0.6666666667
[93] 1.0000000000 0.8571428571 1.0000000000 0.9047619048
[97] 1.0000000000 0.6666666667 0.8095238095 1.0000000000
[101] 0.9523809524 1.0000000000 1.0000000000 0.9047619048
[105] 1.0000000000 0.8571428571 1.0000000000 1.0000000000
[109] 1.0000000000 1.0000000000 1.0000000000 0.9047619048
[113] 1.0000000000 1.0000000000 1.0000000000 1.0000000000
[117] 0.9047619048 0.5238095238 0.8095238095 0.9047619048
[121] 0.7619047619 1.0000000000 1.0000000000 1.0000000000
[125] 1.0000000000 1.0000000000 0.9523809524 1.0000000000
[129] 0.9047619048 0.9523809524 1.0000000000 1.0000000000
[133] 1.0000000000 1.0000000000 0.9523809524 0.6666666667
[137] 1.0000000000 0.8571428571 0.9523809524 1.0000000000
Levels: B M
Evaluar el modelo con tablas
Lo más sencillo, una tabla de contingencia con table
table( Predicción = w_predict, Observación = w_test_label )
Observación
Predicción B M
B 87 6
M 0 47
Una tabla de contingencia un poco más sofisticada con CrossTable
Calcula una tabla similar a la anterior pero más completa. Aporta proporciones de aciertos por filas, columnas y totales.
CrossTable(x = w_predict, y = w_test_label, dnn = c( "Predicción", "Observación" )
, prop.chisq = FALSE )
Cell Contents
|-------------------------|
| N |
| N / Row Total |
| N / Col Total |
| N / Table Total |
|-------------------------|
Total Observations in Table: 140
| Observación
Predicción | B | M | Row Total |
-------------|-----------|-----------|-----------|
B | 87 | 6 | 93 |
| 0.935 | 0.065 | 0.664 |
| 1.000 | 0.113 | |
| 0.621 | 0.043 | |
-------------|-----------|-----------|-----------|
M | 0 | 47 | 47 |
| 0.000 | 1.000 | 0.336 |
| 0.000 | 0.887 | |
| 0.000 | 0.336 | |
-------------|-----------|-----------|-----------|
Column Total | 87 | 53 | 140 |
| 0.621 | 0.379 | |
-------------|-----------|-----------|-----------|
Curvas ROC (Receiver Operating Characteristic) como evaluadores
Es una representación gráfica de la \(sensibilidad\) frente a \(1 - especificidad\) para un clasificador binario, es decir sólo hay dos respuestas, positivo y negativo.
prob <- attr(w_predict, "prob")
labels <- factor(w_test_label, labels = c(1,0))
rocobj <- pROC::roc( labels, prob, auc = TRUE, ci = TRUE )
print( rocobj )
Call:
roc.default(response = labels, predictor = prob, auc = TRUE, ci = TRUE)
Data: prob in 87 controls (labels 1) < 53 cases (labels 0).
Area under the curve: 0.4902407
95% CI: 0.3968185-0.583663 (DeLong)
pROC::plot.roc( rocobj, legacy.axes = TRUE, print.thres = "best", print.auc = TRUE,
auc.polygon = FALSE, max.auc.polygon = FALSE, auc.polygon.col="gainsboro", col = 2, grid = TRUE )
Repetimos con más vecinos: k=100
w_predict<- knn( w_train[ , c( 1:( ncol(w_train) -1 ) ) ],
w_test[ , c( 1:( ncol(w_test) -1 ) ) ],
cl = w_train_label, k = 100, prob = TRUE )
prob <- attr(w_predict, "prob")
labels <- factor(w_test_label, labels = c(0,1))
rocobj <- pROC::roc( labels, prob, auc = TRUE, ci = TRUE )
print( rocobj )
Call:
roc.default(response = labels, predictor = prob, auc = TRUE, ci = TRUE)
Data: prob in 87 controls (labels 0) > 53 cases (labels 1).
Area under the curve: 0.7217523
95% CI: 0.6336647-0.80984 (DeLong)
pROC::plot.roc( rocobj, legacy.axes = TRUE, print.thres = "best", print.auc = TRUE,
auc.polygon = FALSE, max.auc.polygon = FALSE, auc.polygon.col="gainsboro",
col = 2, grid = TRUE )
Validación cruzada —cross-validation—
Como se ha comentado anteriormente, una vez que hemos generado el modelo, interesa conocer la precisión de este modelo en la predicción de los resultados. Dicho de otra manera, interesa estimar el error cometido en la predicción.
La validación cruzada son una serie de técnicas utilizadas para validar la precisión de un modelo predictivo. Para ello comprueba la bondad predictiva del modelo sobre un conjunto de datos de prueba independientes de los datos empleados para generar el modelo.
Básicamente la secuencia sería:
- Construir el modelo con los datos de entrenamiento
- Aplicar el modelo con los datos de prueba, que serán independientes de los de entrenamiento
- Estimar el error en la predicción.
Hay distintas técnicas para de validación cruzada para la estimación del error, aqui vamos a ver alguna de ellas.
Para todas las técnicas que vamos a ver utilizaremos el paquete caret
, uno de los más completos paquetes de machine learning que hay en R.
En primer lugar cargaremos y prepararemos los datos, esto nos servirá para todos los métodos que vamos a ver. A continuación iremos haciendo las validaciones con las diferentes técnicas
La función CreateDataPartition
nos permite crear un grupo de entrenamiento de forma similar a como lo hacíamos con sample
.
set.seed(111)
train <- createDataPartition( iris$Species, p = 0.7, list = FALSE )
train.iris <- iris[ train, ]
test.iris <- iris[ -train, ]
El método del conjunto de validación
En este caso la cuantificación del error se hace mediante la diferencia cuadrática media entre los valores observados y los predichos.
#Crear el modelo
model <- naiveBayes(Species ~ . , data = train.iris )
predictions <- predict( model, test.iris )
predictions <- as.numeric( predictions )
test <- as.numeric( as.factor( test.iris$Species ) )
data.frame( R2 = R2( predictions, test ),
RMSE = RMSE(predictions, test ),
MAE = MAE(predictions, test ) )
R2 RMSE MAE
1 0.9674079755 0.1490711985 0.02222222222
- \(R^2\)
- Es la correlación al cuadrado entre observaciones y predicciones. Cuanto más alto mejor será el modelo
- RMSE —Root mean squared error—
- Error cuadrático medio, mide el error medio cometido por el modelo al predecir la clasificación de una observación. Cuanto más bajo sea, mejor será el modelo
- MAE —Mean absolute Error—
- Error absoluto medio, es similar al RMSE, pero menos sensible a valores atípicos. Es la diferencia absoluta media entre observaciones y predicciones. Cuanto más bajo, mejor será el modelo
LOOCV —Leave one out cross validation—
La validación cruzada dejando una observación fuera, consiste en dejar fuera del modelo una observación y contruir el modelo con el resto, a continuación comprobar el modelo con esa observación y anotar el error. Este proceso se repetirá con todas las observaciones. Finalmente se obtiene el error total cometido calculado como la media de los errores obtenidos para cada observación que se ha testado.
set.seed(111)
train <- trainControl(method = "LOOCV")
model <- train(Species ~ ., data=iris, method = "naive_bayes", trControl = train )
print(model)
Naive Bayes
150 samples
4 predictor
3 classes: 'setosa', 'versicolor', 'virginica'
No pre-processing
Resampling: Leave-One-Out Cross-Validation
Summary of sample sizes: 149, 149, 149, 149, 149, 149, ...
Resampling results across tuning parameters:
usekernel Accuracy Kappa
FALSE 0.9533333333 0.93
TRUE 0.9600000000 0.94
Tuning parameter 'laplace' was held constant at a value
of 0
Tuning parameter 'adjust' was held constant at
a value of 1
Accuracy was used to select the optimal model using
the largest value.
The final values used for the model were laplace =
0, usekernel = TRUE and adjust = 1.
La ventaja del método es que utiliza todos los datos para entrenar y para testar el modelo.
La desventaja es que precisamente por utilizar todos los datos, tiene un alto coste computacional, especialmente cuando tenemos un conjunto de datos muy grande.
K-fold cross-validation y repeat K-fold cross-validation
Es la validación cruzada de k-iteraciones o de k-iteraciones repetidas.
En el primer caso el conjunto de datos se divide en \(K\) subjuntos, uno de ello se utiliza como datos de prueba y el resto \(K-1\) los datos de entrenamiento. La validación cruzada se realizará durante \(K\) iteraciones. En cada iteración un subconjunto será el conjunto de prueba y el resto de entrenamiento.
La idea es similar al método LOOCV, pero en lugar de hacerlo a nivel de observación se hace a nivel de conjunto de observaciones.
El método de k-fold repetido es idéntico al método k-fold, sólo que en este caso, una vez terminadas las k-iteraciones, se vuelve a particionar en subconjuntos y se repite el proceso anterior. Estos ciclos se repetirán el número de veces que se indique.
K-fold
set.seed(111)
train <- trainControl(method = "cv", number = 10 )
model <- train(Species ~ ., data=iris, method = "naive_bayes", trControl = train )
print(model)
Naive Bayes
150 samples
4 predictor
3 classes: 'setosa', 'versicolor', 'virginica'
No pre-processing
Resampling: Cross-Validated (10 fold)
Summary of sample sizes: 135, 135, 135, 135, 135, 135, ...
Resampling results across tuning parameters:
usekernel Accuracy Kappa
FALSE 0.96 0.94
TRUE 0.96 0.94
Tuning parameter 'laplace' was held constant at a value
of 0
Tuning parameter 'adjust' was held constant at
a value of 1
Accuracy was used to select the optimal model using
the largest value.
The final values used for the model were laplace =
0, usekernel = FALSE and adjust = 1.
repeat K-fold
set.seed(111)
train <- trainControl(method = "repeatedcv", number = 10, repeats = 3 )
model <- train(Species ~ ., data=iris, method = "naive_bayes", trControl = train )
print(model)
Naive Bayes
150 samples
4 predictor
3 classes: 'setosa', 'versicolor', 'virginica'
No pre-processing
Resampling: Cross-Validated (10 fold, repeated 3 times)
Summary of sample sizes: 135, 135, 135, 135, 135, 135, ...
Resampling results across tuning parameters:
usekernel Accuracy Kappa
FALSE 0.9555555556 0.9333333333
TRUE 0.9577777778 0.9366666667
Tuning parameter 'laplace' was held constant at a value
of 0
Tuning parameter 'adjust' was held constant at
a value of 1
Accuracy was used to select the optimal model using
the largest value.
The final values used for the model were laplace =
0, usekernel = TRUE and adjust = 1.