library(tidyverse)
library(tidymodels)
library(gt)
<- function(x){
p_1 ifelse(x < 0.15, 0.95, 0.95 - 0.7 * (x - 0.15))
}ggplot(tibble(x = seq(0, 1, 0.01)), aes(x = x)) +
stat_function(fun = p_1) +
ylab("Prob al corriente")
8 Clasificación y probabilidad
Una variable \(g\) categórica o cualitativa toma valores que no son numéricos. Por ejemplo, si \(g\) denota el estado del contrato de celular de un cliente dentro de un año, podríamos tener \(g\in \{ activo, cancelado\}\).
En un problema de clasificación buscamos predecir una variable respuesta categórica \(g\) en función de otras variables de entrada \(x=(x_1,x_2,\ldots, x_p)\).
Ejemplos
Predecir si un cliente cae en impago de una tarjeta de crédito, de forma que podemos tener \(g=corriente\) o \(g=impago\). Variables de entrada podrían ser \(x_1=\) porcentaje de saldo usado, \(x_2=\) atrasos en los últimos 3 meses, \(x_3=\) edad, etc
En reconocimiento de imágenes quiza tenemos que \(g\) pertenece a un conjunto que típicamente contiene hasta cientos de valores (manzana, árbol, pluma, perro, coche, persona, cara, etc.). Las \(x_j\) son valores de pixeles de la imagen para tres canales (rojo, verde y azul). Si las imágenes son de 100x100, tendríamos 30,000 variables de entrada.
8.1 ¿Qué estimar en problemas de clasificación?
Cuando clasificamos según entradas \(x\) en una clase, podemos estar más o menos seguros de la clasificación. Por ejemplo: puede ser que la la decisión es clasificar a un cliente dado como “impago en los próximos tres meses”. La incertidumbe está en que quizá la probabilidad de impago es de 95%, pero existe una probabilidad de 5% de que el cliente se mantenga al corriente. Esto es muy diferente a un cliente con probabilidades respectivas de 60% y 40%. En general, cualquier tipo de análisis costo-beneficio que utilice el modelo debe intentar tomar en cuenta que estos dos clientes son muy diferentes.
Consideramos primero un problema de clasificación binaria, y denotamos por \(y\in \{0,1\}\) la indicadora de una de las dos de las categorías. Sea \(p(x)\) una función que mide qué tan seguros estamos que la observación es de clase \(y=1\). Supondremos que \(p(x)\) es una función que toma valores entre \(0\) y \(1\).
Ahora necesitamos una función de pérdida \(L(y, p)\) que evalúa el error cuando nuestra medida de confianza es \(p\) y la clase observada es \(y\). Podemos usar por ejemplo la pérdida cuadrática:
\[L(y, p) = (y - p)^2,\] que también se llama pérdida de Brier en este contexto. Igual que en regresión, dada una población, podemos encontrar la función \(p^*(x)\) que minimiza la pérdida esperada sobre toda la población. En este caso, \[p^*(x) = E(y|x) = P(y=1|x)\] es decir, la verdadera probabilidad de que la clase sea 1 es la función \(p^*(x)\) que minimiza la pérdida cuadrática. Si usamos por ejemplo la pérdida absoluta, entonces la solucion es tomar como \(p^* (x) = 1\) si \(P(y= 1|x) > 0.5\), y \(p^*(x)= 0\) en otro caso (muestra por qué).
Aunque utilizaremos otras pérdidas mejor adaptadas para el problema del clasificación, por el momento notemos que igual que planteamos el problema aprendizaje en regresión como un problema de aproximar una curva \(f^*(x)\) óptima con una función \(\hat{f}(x)\) construida a partir de datos, igualmente podemos plantear el problema de clasificación binaria como sigue:
- Buscamos algoritmos \({\mathcal L} \to \hat{p}(x)\) tal que \(\hat{p}(x)\) está cercana a la probabilidad de clase \(p^* (x) = P(y=1 |x)\).
- Los argumentos de error irreducible, sesgo y variabilidad aplican también en esta situación bajo la pérdida de Brier y puede darse un argumento para cada clase (considerando \(y_k - \hat{p}_k(x)\) como residual). En este caso, el error irreducible proviene del hecho de que \(p^*_k(x)\) no necesariamente toman solo los valores 0 o 1, y dadas las \(x\), existe incertidumbre en la clase que vamos a observar.
Ejemplo
(Impago de tarjetas de crédito) Supongamos que \(x=\) porcentaje del crédito máximo usado, y \(y\in\{0, 1\}\), donde \(1\) corresponde al corriente y \(0\) representa impago. Las probabilidades condicionales de clase para la clase al corriente podrían ser, por ejemplo:
- \(p(x) = P(y=1|x) =0.95\) si \(x < 0.15\)
- \(p(x) = P(y=1|x) = 0.95 - 0.7(x - 0.15)\) si \(x>=0.15\)
Estas son probabilidades y no determinan el resultado, pues hay otras variables que influyen en que un cliente permanezca al corriente o no en sus pagos más allá de información contenida en el porcentaje de crédito usado. Nótese que estas probabilidades son diferentes a las no condicionadas, por ejempo, podríamos tener que a total \(P(y=1)=0.83\)
8.2 Estimación de probabilidades de clase
¿Cómo estimamos ahora las probabilidades de clase a partir de una muestra de entrenamiento? Veremos por ahora dos métodos: k-vecinos más cercanos y regresión logística.
Ejemplo
Vamos a generar unos datos con el modelo simple del ejemplo anterior:
<- function(n = 500){
simular_impago # suponemos que los valores de x están concentrados en valores bajos,
# quizá la manera en que los créditos son otorgados
<- c("al_corriente", "impago")
clases <- pmin(rexp(n, 100 / 40), 1)
x # las probabilidades de estar al corriente:
<- p_1(x)
prob # finalmente, simulamos cuáles clientes siguen al corriente y cuales no:
<- map_chr(1:length(x), ~ sample(clases, size = 1, prob = c(prob[.x], 1- prob[.x])))
g <- factor(g, levels = c("al_corriente", "impago"))
g <- tibble(x = x, p_1 = prob, g = g) |>
datos mutate(y = ifelse(g == "al_corriente", 1, 0))
datos
}set.seed(193)
<- simular_impago() |> select(x, g, y)
dat_ent |> sample_n(20) dat_ent
# A tibble: 20 × 3
x g y
<dbl> <fct> <dbl>
1 0.118 al_corriente 1
2 0.109 al_corriente 1
3 0.444 al_corriente 1
4 0.153 al_corriente 1
5 0.100 al_corriente 1
6 0.0109 al_corriente 1
7 0.216 al_corriente 1
8 1 impago 0
9 0.0846 al_corriente 1
10 0.144 al_corriente 1
11 0.377 impago 0
12 0.0908 al_corriente 1
13 0.128 al_corriente 1
14 0.00262 al_corriente 1
15 0.411 al_corriente 1
16 0.545 al_corriente 1
17 0.402 al_corriente 1
18 0.0544 al_corriente 1
19 0.00265 al_corriente 1
20 0.0927 al_corriente 1
Como este problema es de dos clases, podemos graficar como sigue (agregamos variación artificial en \(y\) para evitar traslape de los puntos):
<- ggplot(dat_ent, aes(x = x)) +
graf_1 geom_jitter(aes(colour = factor(g), y = y),
width=0.02, height=0.1) + ylab("") +
labs(colour = "Clase")
graf_1
8.3 k-vecinos más cercanos
Para usar \(k\)-vecinos más cercanos para estimar la probabilidades de clase para cada \(x\), podemos tomar una vecindad de la \(x\) donde queremos predecir, y tomar el siguiente promedio:
\[\hat{p}(x) = \frac{1}{k}\sum_{x^{(i)} \in N_k(x)} y^{(i)},\]
que es la proporción de unos en una vecindad \(N_k(x)\) de \(x\).
Ejemplo
Vamos a intentar estimar la probabilidad condicional de estar al corriente usando k vecinos más cercanos:
<- nearest_neighbor(neighbors = 100, weight_func = "gaussian") |>
vmc_modelo set_engine("kknn") |>
set_mode("classification")
<- vmc_modelo |> fit(g ~ x, dat_ent)
ajuste_vmc # para graficar:
<- tibble(x = seq(0, 1, 0.01))
graf_kvmc <- predict(ajuste_vmc, graf_kvmc, type = "prob") |>
graf_kvmc bind_cols(graf_kvmc) |>
select(x, .pred_al_corriente)
|> head() graf_kvmc
# A tibble: 6 × 2
x .pred_al_corriente
<dbl> <dbl>
1 0 0.947
2 0.01 0.946
3 0.02 0.948
4 0.03 0.954
5 0.04 0.973
6 0.05 0.985
Y la curva roja da nuestra estimación de probabilidad de impago para cada $x4:
<- tibble(x = seq(0, 1, 0.01), p_1 = p_1(x))
graf_verdadero <- graf_1 +
graf_2 geom_line(data = graf_kvmc, aes(y = .pred_al_corriente), colour = 'red', size=1.2) +
geom_line(data = graf_verdadero, aes(y = p_1)) +
ylab('Prob al corriente') + xlab('% crédito usado')
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.
graf_2
Ahora podríamos usar una muestra de prueba para evaluar nuestra estimación, pues no tenemos la curva negra óptima para comparar.
8.4 Ejemplo: diabetes
Consideremos datos de diabetes en mujeres Pima:
A population of women who were at least 21 years old, of Pima Indian heritage and living near Phoenix, Arizona, was tested for diabetes according to World Health Organization criteria. The data were collected by the US National Institute of Diabetes and Digestive and Kidney Diseases. We used the 532 complete records after dropping the (mainly missing) data on serum insulin.
- npreg number of pregnancies.
- glu plasma glucose concentration in an oral glucose tolerance test.
- bp diastolic blood pressure (mm Hg).
- skin triceps skin fold thickness (mm).
- bmi body mass index (weight in kg/(height in m)^2).
- ped diabetes pedigree function.
- age age in years.
- type Yes or No, for diabetic according to WHO criteria.
<- as_tibble(MASS::Pima.tr)
diabetes_ent <- as_tibble(MASS::Pima.te)
diabetes_pr diabetes_ent
# A tibble: 200 × 8
npreg glu bp skin bmi ped age type
<int> <int> <int> <int> <dbl> <dbl> <int> <fct>
1 5 86 68 28 30.2 0.364 24 No
2 7 195 70 33 25.1 0.163 55 Yes
3 5 77 82 41 35.8 0.156 35 No
4 0 165 76 43 47.9 0.259 26 No
5 0 107 60 25 26.4 0.133 23 No
6 5 97 76 27 35.6 0.378 52 Yes
7 3 83 58 31 34.3 0.336 25 No
8 1 193 50 16 25.9 0.655 24 No
9 3 142 80 15 32.4 0.2 63 No
10 2 128 78 37 43.3 1.22 31 Yes
# ℹ 190 more rows
Intentaremos predecir diabetes dependiendo del una medición de glucosa en la sangre:
ggplot(diabetes_ent, aes(x = glu, y= ifelse(type=='Yes', 1, 0), colour = type)) +
geom_jitter(height = 0.05)
Usamos \(30\) vecinos más cercanos para estimar \(p(x)\):
<- tibble(glu = seq(50, 200, 1))
graf_data # ajustar modelo
<- vmc_modelo |> set_args(neighbors = 50) |>
ajuste_vmc_diabetes fit(type ~ glu, diabetes_ent)
# graficar
<- predict(ajuste_vmc_diabetes, graf_data, type = "prob") |>
graf_data bind_cols(graf_data) |>
select(glu, .pred_Yes)
ggplot(diabetes_ent, aes(x = glu)) +
geom_point(aes(y = as.numeric(type == "Yes"), colour = type)) +
geom_line(data = graf_data, aes(y = .pred_Yes)) +
ylab('Probabilidad diabetes')
Finalmente, evaluamos por ejemplo con la pérdida de Brier:
<- predict(ajuste_vmc_diabetes, diabetes_pr, type = "prob") |>
preds_tbl bind_cols(diabetes_pr |> select(type)) |>
mutate(y = as.numeric(type == "Yes"))
|> head() preds_tbl
# A tibble: 6 × 4
.pred_No .pred_Yes type y
<dbl> <dbl> <fct> <dbl>
1 0.482 0.518 Yes 1
2 0.921 0.0793 No 0
3 0.902 0.0980 No 0
4 0.925 0.0752 Yes 1
5 0.250 0.750 Yes 1
6 0.342 0.658 Yes 1
|>
preds_tbl rmse(truth = y, estimate = .pred_Yes)
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 0.405
Podemos comparar este error con la predicción que haríamos usando la proporción de unos en los datos, por ejemplo:
<- diabetes_ent |> mutate(y = type == "Yes") |>
promedio_ent pull(y) |> mean()
|> mutate(promedio = promedio_ent) |>
preds_tbl rmse(truth = y, estimate = promedio)
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 0.470
Finalmente, probemos usando todas las variables, teniendo cuidado de estandarizar las variables de entranda:
<- recipe(type ~ ., diabetes_ent) |>
receta_diabetes step_normalize(all_predictors())
<- vmc_modelo |>
modelo_vmc set_args(neighbors = 50)
<- workflow() |>
flujo_vmc_diabetes add_recipe(receta_diabetes) |>
add_model(modelo_vmc)
<- fit(flujo_vmc_diabetes, diabetes_ent) |>
preds_todas predict(diabetes_pr, type = "prob") |>
bind_cols(diabetes_pr |> select(type)) |>
mutate(y = as.numeric(type == "Yes"))
|>
preds_todas rmse(truth = y, estimate = .pred_Yes)
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 0.396
8.5 Pérdida logarítmica
La pérdida de Brier es una medida útil para evaluar modelos, pero tiene algunos defectos cuando se usa en el ajuste de probabilidades de clase. Uno importante es que es penaliza relativamente poco a errores donde predecimos por ejemplo \(\hat{p}_1(x) = 0.0001\), y resulta que observamos \(g=1\). La penalización es similar que cuando \(\hat{p}_1(x) = 0.01\), pero se puede argumentar que el primero de los errores es considerablemente más grave que el segundo.
Consideremos entonces que tenemos una estimación \(\hat{p}_g(x)\) de las probabilidad de clase. Supongamos que observamos ahora \((x, g)\) (la clase verdadera es \(g\)).
- Si \(\hat{p}_{g}(x)\) es muy cercana a uno, deberíamos penalizar poco, pues dimos probabilidad alta a la clase \(g\) que ocurrió.
- Si \(\hat{p}_{g}(x)\) es chica, deberíamos penalizar más, pues dimos probabilidad baja a observar la clase \(g\).
- Si \(\hat{p}_{g}(x)\) es muy cercana a cero, y observamos \(g\), deberíamos hacer una penalización muy alta (convergiendo a \(\infty\), pues no es aceptable que sucedan eventos con probabilidad estimada extremadamente baja).
Quisiéramos encontrar una función \(h\) apropiada, de forma que la pérdida al observar \((x, g)\) sea \[s(\hat{p}_{g}(x)),\] y que cumpla con los puntos arriba señalados. Entonces tenemos que
- \(s\) debe ser una función continua y decreciente en \([0,1]\)
- Podemos poner \(s(1)=0\) (no hay pérdida si ocurre algo con que dijimos tiene probabilidad 1)
- \(s(p)\) debe ser muy grande is \(p\) es muy chica.
Una opción analíticamente conveniente es la pérdida logarítmica: \[s(p) = - \log(p)\]
<- tibble(p = seq(0.01, 1, 0.001)) |>
perdidas_tbl mutate(logarítmica = - log(p), brier = 2 * (1 - p)^2) |>
pivot_longer(cols = logarítmica:brier, names_to = "tipo", values_to = "perdida")
ggplot(perdidas_tbl, aes(x = p, y = perdida, colour = tipo)) +
geom_line(size = 1.1)
Así que la pérdida para un caso con entradas \(x\) y clase \(g\) es
\[-\log \hat{p}_g (x)\] Se puede demostrar que, igual que la pérdida de Brier, si queremos minimizar la pérdida logarítmica sobre toda la población, la solución está dada por las verdaderas probabilidades de clase
\[p^*(x) = E(y|x) = P(y = 1|x).\]
De modo que la función que queremos estimar es la misma en ambos casos. La pérdida de Brier sin embargo, es menos sensible a errores en los extremos de la escala de probabilidad, y puede producir estimaciones distintas cuando se usa en entrenamiento.
Observaciones:
La pérdida logarítmica también se llama devianza binomial o devianza multinomial en otros lugares, usualmente multipicada por 2 (lo cual no cambia sus propiedades).
Una razón importante para usar la pérdida logarítmica como el objetivo a minimizar es que equivale a la estimación por máxima verosimilitud (intenta demostrarlo).
No es fácil interpretar la pérdida logarítmica, pero es útil para ajustar y comparar modelos. Veremos otras medidas más fáciles de interpretar más adelante.
Compara la siguiente definición con la que vimos para modelos de regresión:
Sea \[{\mathcal L}=\{ (x^{(1)},g^{(1)}),(x^{(2)},g^{(2)}), \ldots, (x^{(N)}, g^{(N)}) \}\] una muestra de entrenamiento, a partir de las cuales construimos mediante un algoritmo funciones estimadas \(\hat{p}_{g} (x)\) para \(g=1,\ldots, K\). La pérdida logarítmica de entrenamiento está dada por \[\begin{equation} \overline{err} = - \frac{1}{N}\sum_{i=1}^N log(\hat{p}_{g^{(i)}} (x^{(i)})) \end {equation}\] Sea \[{\mathcal T}=\{ (\mathbf{x}^{(1)},\mathbf{g}^{(1)}),(\mathbf{x}^{(2)},\mathbf{g}^{(2)}), \ldots, (\mathbf{x}^{(m)}, \mathbf{g}^{(m)}) \}\] una muestra de prueba. La pérdida logarítmica de prueba es \[\begin{equation} \hat{Err} = - \frac{1}{m}\sum_{i=1}^m log(\hat{p}_{\mathbf{g}^{(i)}} (\mathbf{x}^{(i)})) \end {equation}\] que es una estimación de la devianza de predicción \[-E\left [ \log(\hat{p}_g(x)) \right ],\] donde el promedia se toma sobre todos los valores \((\mathbf{x}, \mathbf{g})\) de la población.
Ejemplo
Regresamos a nuestros ejemplo simulado de impago de tarjetas de crédito. Primero calculamos la pérdida logarítmica de entrenamiento
<- \(x) -log(x)
s <- ajuste_vmc |>
dat_log_loss predict(dat_ent, type = "prob") |>
bind_cols(dat_ent) |>
select(x, g, .pred_impago, .pred_al_corriente)
<- dat_log_loss |>
dat_log_loss mutate(hat_p_g = ifelse(g=="impago", .pred_impago, .pred_al_corriente))
Nótese que dependiendo de qué clase observamos (columna \(g\)), extraemos la probabilidad correspondiente a la columna hat_p_g:
set.seed(125)
|> sample_n(20) |> gt() dat_log_loss
x | g | .pred_impago | .pred_al_corriente | hat_p_g |
---|---|---|---|---|
0.19852287 | al_corriente | 0.14038901 | 0.8596110 | 0.85961099 |
0.67376004 | al_corriente | 0.50190033 | 0.4980997 | 0.49809967 |
0.05182251 | al_corriente | 0.01304586 | 0.9869541 | 0.98695414 |
0.78516363 | al_corriente | 0.53011453 | 0.4698855 | 0.46988547 |
0.51357278 | al_corriente | 0.32252677 | 0.6774732 | 0.67747323 |
0.11636840 | al_corriente | 0.08958375 | 0.9104162 | 0.91041625 |
0.50414410 | impago | 0.30908738 | 0.6909126 | 0.30908738 |
0.68843401 | impago | 0.50778339 | 0.4922166 | 0.50778339 |
0.74915741 | impago | 0.52349703 | 0.4765030 | 0.52349703 |
0.15106073 | al_corriente | 0.06850996 | 0.9314900 | 0.93149004 |
1.00000000 | impago | 0.57659107 | 0.4234089 | 0.57659107 |
0.07148778 | al_corriente | 0.02543890 | 0.9745611 | 0.97456110 |
0.08934583 | al_corriente | 0.06888669 | 0.9311133 | 0.93111331 |
0.32848650 | al_corriente | 0.21323646 | 0.7867635 | 0.78676354 |
0.20386137 | al_corriente | 0.14978783 | 0.8502122 | 0.85021217 |
0.67871350 | impago | 0.50258385 | 0.4974162 | 0.50258385 |
0.19050443 | al_corriente | 0.12406591 | 0.8759341 | 0.87593409 |
0.18182276 | al_corriente | 0.10562898 | 0.8943710 | 0.89437102 |
0.26726265 | al_corriente | 0.16922039 | 0.8307796 | 0.83077961 |
0.02035980 | impago | 0.05230648 | 0.9476935 | 0.05230648 |
Ahora aplicamos la función \(s\) que describimos arriba, y promediamos sobre el conjunto de entrenamiento:
<- dat_log_loss |> mutate(dev = s(hat_p_g))
dat_log_loss |> sample_n(20) |> gt() dat_log_loss
x | g | .pred_impago | .pred_al_corriente | hat_p_g | dev |
---|---|---|---|---|---|
0.32253044 | al_corriente | 0.21043820 | 0.7895618 | 0.7895618 | 0.23627717 |
0.10851963 | al_corriente | 0.09528056 | 0.9047194 | 0.9047194 | 0.10013039 |
0.11801945 | al_corriente | 0.08830654 | 0.9116935 | 0.9116935 | 0.09245146 |
1.00000000 | impago | 0.57659107 | 0.4234089 | 0.5765911 | 0.55062199 |
0.11771241 | al_corriente | 0.08871380 | 0.9112862 | 0.9112862 | 0.09289827 |
0.03576024 | al_corriente | 0.03671995 | 0.9632800 | 0.9632800 | 0.03741110 |
0.02947193 | al_corriente | 0.04638861 | 0.9536114 | 0.9536114 | 0.04749904 |
0.06708467 | al_corriente | 0.01865320 | 0.9813468 | 0.9813468 | 0.01882937 |
0.52670471 | al_corriente | 0.34240787 | 0.6575921 | 0.6575921 | 0.41917040 |
0.48074458 | impago | 0.27713415 | 0.7228659 | 0.2771341 | 1.28325360 |
0.75320749 | impago | 0.52403777 | 0.4759622 | 0.5240378 | 0.64619152 |
0.28789759 | al_corriente | 0.18845940 | 0.8115406 | 0.8115406 | 0.20882086 |
0.50724729 | al_corriente | 0.31267931 | 0.6873207 | 0.6873207 | 0.37495430 |
0.65972691 | impago | 0.49530580 | 0.5046942 | 0.4953058 | 0.70257994 |
1.00000000 | al_corriente | 0.57659107 | 0.4234089 | 0.4234089 | 0.85941682 |
0.03525545 | al_corriente | 0.03771126 | 0.9622887 | 0.9622887 | 0.03844073 |
0.53221265 | impago | 0.35093998 | 0.6490600 | 0.3509400 | 1.04714008 |
0.12557127 | al_corriente | 0.08047375 | 0.9195263 | 0.9195263 | 0.08389669 |
0.12686269 | al_corriente | 0.07830790 | 0.9216921 | 0.9216921 | 0.08154406 |
0.11702054 | al_corriente | 0.08882314 | 0.9111769 | 0.9111769 | 0.09301826 |
|> ungroup() |> summarise(lloss_entrena = mean(dev)) dat_log_loss
# A tibble: 1 × 1
lloss_entrena
<dbl>
1 0.421
Que también podemos calcular como sigue:
|> mn_log_loss(g, .pred_al_corriente) |>
dat_log_loss mutate(.estimate = .estimate )
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 mn_log_loss binary 0.421
Recordemos que la devianza de entrenamiento no es la cantidad que evalúa el desempeño del modelo. Hagamos el cálculo entonces para una muestra de prueba:
set.seed(1213)
<- simular_impago(n = 5000) |> select(x, g)
dat_prueba ## calcular para muestra de prueba
<- ajuste_vmc |>
dat_log_loss_prueba predict(dat_prueba, type = "prob") |>
bind_cols(dat_prueba) |>
select(x, g, .pred_impago, .pred_al_corriente)
<- dat_log_loss_prueba |>
dat_log_loss_prueba mutate(hat_p_g = ifelse(g == "al_corriente", .pred_al_corriente, .pred_impago))
<- dat_log_loss_prueba |> mutate(log_loss = s(hat_p_g))
dat_log_loss_prueba |> ungroup() |> summarise(log_loss_prueba = mean(log_loss)) dat_log_loss_prueba
# A tibble: 1 × 1
log_loss_prueba
<dbl>
1 0.434
8.6 Clasificación multinomial
Cuando tenemos sólo dos clases (clasificación binaria), basta con estimar una probabilidad de clase, pues la segunda es complemento de la primera. Para tres o más clases introducimos notación adicional:
Consideramos un problema donde \(g\) puede ser una de \(K\) clases posibles. Tenemos \(K\) probabilidades de clase \(p(x) = (p_1(x), p_2(x), \ldots p_K(x))\) que deben sumar 1 (de forma que una de ellas es redundante dadas las otras).
Definimos \((y_1,y_2, \ldots, y_k)\) variables indicadoras de las categorías, donde \(y_k = 1\) cuando \(g = k\) y \(y_k = 0\) en otro caso.
La pérdida de Brier en este caso se puede definir como
\[L(g, p(x)) = \sum_{k=1}^K (y_k - p_k(x))^2 = (1 - p_g(x))^2 + \sum_{k\neq g} (p_k(x))^2\] Esto quiere decir que el score de Brier es chico cuando \(p_g(x)\), la probabilidad de la clase \(g\), es cercana a uno y el resto de las probabilidades son cercanas a 0 (verifica que esta definición es equivalente para dos clases según la definición mostrada arriba para clasificación binaria). En este caso, la solución teórica que minimiza esta pérdida promediada sobre toda la población es
\[p_k^*(x) = E(y_k | x) = P(g = k| x)\]
Si usamos la pérdida logarítmica, entonces la pérdida es
\[L(g, p(x)) = -\log p_g (x),\] que en nuestra notación con indicadoras se escribe como
\[L(y, p(x)) = -\sum_{k=1}^K y_k\log p_k (x),\]
Ejercicio: escribe las estimaciones de clase de \(k\)-vecinos más cercanos para clasificación multinomial.
8.7 Regresión logística
En \(k\) vecinos más cercanos, intentamos estimar directamente con promedios las probabilidades de clase, sin considerar ninguna estructura. Ahora consideramos modelos más estructurados, definidos por parámetros, e intentaremos ajustarlos minimizando la pérdida logarítmica.
Igual que en regresión lineal, algunos de los modelos más simples que podemos imaginar son modelos lineales. Solo es necesario hacer una adaptación.
Supongamos que nuestra variable respuesta es \(y\), que toma valores 0 o 1.
Ahora queremos definir \(p(x) = p_1(x)\) (probabilidad de que ocurra la clase 1) en términos de un promedio ponderado de las variables de entrada, como en regresión lineal:
\[\beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_px_p.\]
Sin embargo, observamos que esta expresión puede dar valores negativos o mayores a uno, de forma que no necesariamente puede interpetarse como una probabilidad \(p(x)\). Una de las formas más sencillas de resolver este problema es transformar esta expresión para que necesariamente esté en \([0,1]\) por medio de una función fija \(h\):
\[p_{\beta}(x) = h(\beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_px_p),\] donde \(h\) debe ser una función que mapea valores reales a valores en \([0,1]\).
En este punto hay muchas funciones que podríamos usar. Para simplificar la interpretación y uso de este modelo, podemos escoger entre funciones que satisfagan, por ejemplo:
- \(h\) toma valores en \([0,1]\) es creciente y diferenciable
- \(h(0) = 0.5\) (0 equivale a probabilidad 0.5, negativos dan probabilidades menores a 0.5 y positivos dan probabilidades mayores a 0.5)
- \(h(-x)=1-h(x)\) (simetría). Por ejemplo, si \(h(-2)=0.16\) entonces \(h(2)= 1-0.16=0.84\).
Hay todavía muchas opciones. Una de las más simples es usar la función logística
La función logística está dada por \[h(x)=\frac{e^x}{1+e^x}\]
<- function(x){exp(x)/(1+exp(x)) }
h ggplot(tibble(x = seq(-6, 6, 0.01)), aes(x = x)) + stat_function(fun = h)
Esta función comprime adecuadamente (para nuestros propósitos) el rango de todos los reales dentro del intervalo \([0,1]\). Si aplicamos al predictor lineal que consideramos, obtenemos:
El modelo de regresión logística está dado por \[p_1(x)=p_1(x;\beta)= h(\beta_0+\beta_1x_1 + \beta_2 x_2 + \cdots + \beta_p x_p)\]
y \[p_0(x)=p_0(x;\beta)=1-p_1(x;\beta),\] donde \(\beta=(\beta_0,\beta_1, \beta_2, \cdots, \beta_p)\).
Ejemplo
Consideremos nuestro ejemplo de impago. Podemos examinar qué tipo de probilidades obtendríamos con regresión logística y distintos parametros beta:
<- function(beta_0, beta_1){
crear_p function(x){
h(beta_0 + beta_1 * x)
}
}<- tibble(x = seq(0, 1, 0.01))
df_grid <- tibble(beta_0 = c(-5, -0.5, 2.5),
betas beta_1 = c(10, -2, -4))
<- betas |>
betas mutate(p = map2(beta_0, beta_1, crear_p)) |>
mutate(grid = map(p, ~ df_grid |> mutate(p_1 = .(x)))) |>
select(-p) |>
mutate(fun_nom = paste(beta_0, "+", beta_1, "x")) |>
unnest(cols = c(grid))
+ geom_line(data = betas, aes(x = x, y = p_1)) + facet_wrap(~fun_nom) graf_1
Experimenta con otros valores de \(\beta_0\) y \(\beta_1\).
Encontramos los coeficientes de la regresión logística minimizando la pérdida logarítmica de entrenamiento.
Esto se puede hacer de diversas maneras. Tradicionalmente, se utiliza el método de Newton-Raphson, pero resulta más fácil escalar métodos derivados de descenso máximo. Es decir, calculamos el gradiente de la pérdida y tomamos un paso en la dirección contraria al gradiente, que es la dirección local de descenso máximo.
8.8 Ejercicio: datos de diabetes
Ya están divididos los datos en entrenamiento y prueba
<- as_tibble(MASS::Pima.tr)
diabetes_ent <- as_tibble(MASS::Pima.te)
diabetes_pr |> head() |> gt() diabetes_ent
npreg | glu | bp | skin | bmi | ped | age | type |
---|---|---|---|---|---|---|---|
5 | 86 | 68 | 28 | 30.2 | 0.364 | 24 | No |
7 | 195 | 70 | 33 | 25.1 | 0.163 | 55 | Yes |
5 | 77 | 82 | 41 | 35.8 | 0.156 | 35 | No |
0 | 165 | 76 | 43 | 47.9 | 0.259 | 26 | No |
0 | 107 | 60 | 25 | 26.4 | 0.133 | 23 | No |
5 | 97 | 76 | 27 | 35.6 | 0.378 | 52 | Yes |
$id <- 1:nrow(diabetes_ent)
diabetes_ent$id <- 1:nrow(diabetes_pr) diabetes_pr
Aunque no es necesario, podemos normalizar:
<- recipe(type ~ ., diabetes_ent) |>
receta_diabetes update_role(id, new_role = "id_variable") |>
step_normalize(all_numeric())
<- receta_diabetes |> prep() |> juice()
diabetes_ent_s <- receta_diabetes |> prep() |> bake(diabetes_pr) diabetes_pr_s
<- logistic_reg(mode = "classification") |>
modelo_lineal set_engine("glm")
<- workflow() |>
flujo_diabetes add_model(modelo_lineal) |>
add_recipe(receta_diabetes)
<- fit(flujo_diabetes, diabetes_ent)
flujo_ajustado saveRDS(flujo_ajustado, "cache/flujo_ajustado_diabetes.rds")
flujo_ajustado
══ Workflow [trained] ══════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: logistic_reg()
── Preprocessor ────────────────────────────────────────────────────────────────
1 Recipe Step
• step_normalize()
── Model ───────────────────────────────────────────────────────────────────────
Call: stats::glm(formula = ..y ~ ., family = stats::binomial, data = data)
Coefficients:
(Intercept) npreg glu bp skin bmi
-0.95583 0.34734 1.01705 -0.05473 -0.02247 0.51263
ped age
0.55928 0.45201
Degrees of Freedom: 199 Total (i.e. Null); 192 Residual
Null Deviance: 256.4
Residual Deviance: 178.4 AIC: 194.4
Ahora calculamos devianza de prueba y error de clasificación:
<-
preds_prueba predict(flujo_ajustado, diabetes_pr, type= "prob") |>
bind_cols(predict(flujo_ajustado, diabetes_pr)) |>
bind_cols(diabetes_pr |> select(type))
preds_prueba
# A tibble: 332 × 4
.pred_No .pred_Yes .pred_class type
<dbl> <dbl> <fct> <fct>
1 0.232 0.768 Yes Yes
2 0.960 0.0403 No No
3 0.975 0.0253 No No
4 0.959 0.0413 No Yes
5 0.204 0.796 Yes Yes
6 0.265 0.735 Yes Yes
7 0.590 0.410 No Yes
8 0.780 0.220 No No
9 0.558 0.442 No No
10 0.798 0.202 No Yes
# ℹ 322 more rows
levels(preds_prueba$type)
[1] "No" "Yes"
# ponemos event_level si "positivo" no es el primer factor
<- metric_set(accuracy, mn_log_loss)
metricas metricas(preds_prueba, truth = type, .pred_Yes, estimate = .pred_class,
event_level = "second")
# A tibble: 2 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 accuracy binary 0.801
2 mn_log_loss binary 0.441
Vamos a repetir usando keras.
library(keras)
<- diabetes_ent_s |> select(-type, -id) |> as.matrix()
x_ent <- diabetes_ent_s$type == "Yes"
y_ent <- diabetes_pr_s |> select(-type, -id) |> as.matrix()
x_prueba <- diabetes_pr_s$type == 'Yes'
y_prueba # definición de estructura del modelo (regresión logística)
# es posible hacerlo con workflows como vimos arriba,
# pero aquí usamos directamente la interfaz de keras en R
<- nrow(x_ent)
n_entrena <- keras_model_sequential() |>
modelo_diabetes layer_dense(units = 1, #una sola respuesta,
activation = "sigmoid", # combinar variables linealmente y aplicar función logística
kernel_initializer = initializer_constant(0), #inicializamos coeficientes en 0
bias_initializer = initializer_constant(0)) #inicializamos ordenada en 0
# compilar seleccionando cantidad a minimizar, optimizador y métricas
|> compile(
modelo_diabetes loss = "binary_crossentropy", # devianza es entropía cruzada
optimizer = optimizer_sgd(learning_rate = 0.75), # descenso en gradiente
metrics = list("binary_crossentropy"))
# Ahora iteramos
# Primero probamos con un número bajo de iteraciones
<- modelo_diabetes |> fit(
historia as.matrix(x_ent), # x entradas
# y salida o target
y_ent, batch_size = nrow(x_ent), # para descenso en gradiente
epochs = 10 # número de iteraciones
)plot(historia)
Y ahora podemos correr más iteraciones adicionales:
<- modelo_diabetes |> fit(
historia as.matrix(x_ent), # x entradas
# y salida o target
y_ent, batch_size = nrow(x_ent), # para descenso en gradiente
epochs = 1000, # número de iteraciones
verbose = 0
)
Los errores de entrenamiento y prueba son:
evaluate(modelo_diabetes, x_ent, y_ent)
loss binary_crossentropy
0.4459766 0.4459766
evaluate(modelo_diabetes, x_prueba, y_prueba)
loss binary_crossentropy
0.4406986 0.4406986
Veamos que coeficientes obtuvimos:
get_weights(modelo_diabetes)
[[1]]
[,1]
[1,] 0.34734303
[2,] 1.01705003
[3,] -0.05472931
[4,] -0.02247147
[5,] 0.51263183
[6,] 0.55927497
[7,] 0.45200703
[[2]]
[1] -0.9558302
que coinciden con los valores que obtuvimos usando regresión logística de glm. La única diferencia es que el algoritmo de optimización que se usa en cada caso es diferente: con keras utilizamos descenso en gradiente, mientras que glm usa Newton-Raphson.
|> extract_fit_parsnip() flujo_ajustado
parsnip model object
Call: stats::glm(formula = ..y ~ ., family = stats::binomial, data = data)
Coefficients:
(Intercept) npreg glu bp skin bmi
-0.95583 0.34734 1.01705 -0.05473 -0.02247 0.51263
ped age
0.55928 0.45201
Degrees of Freedom: 199 Total (i.e. Null); 192 Residual
Null Deviance: 256.4
Residual Deviance: 178.4 AIC: 194.4
8.9 Probabilidades y pérdida 0-1
Otra medida común para medir el error de un clasificador es el error de clasificación, que también llamamos probabilidad de clasificación incorrecta, o error bajo pérdida 0-1.
Sean \(\hat{p}_g(x)\) probabilidades de clase estimadas. El clasificador bajo pérdida 0-1 asociado está dado por \[\hat{g} (x) = \arg\max_g \hat{p}_g(x)\] Podemos estimar su error de clasificación \(P(\hat{g}(x) \neq g)\) con una muestra de prueba \[{\mathcal T}=\{ (\mathbf{x}^{(1)},\mathbf{g}^{(1)}),(\mathbf{x}^{(2)},\mathbf{g}^{(2)}), \ldots, (\mathbf{x}^{(m)}, \mathbf{g}^{(m)})\] mediante \[\hat{Err} = \frac{1}{m} \sum_{j=i}^m I(\hat{g}(\mathbf{x}^{(i)}) \neq \mathbf{g}^{(i)}),\] es decir, la proporción de casos de prueba que son clasificados incorrectamente.
Observación: Muy generalmente, esta manera de construir un clasificador es deficiente:
- Rara vez queremos clasificar simplemente a la clase con mayor probabilidad. Por ejemplo, podría ser mucho más razonable que basta investigar una operación si su probabilidad de ser fraude está por arriba de 10%, y no necesariamente por arriba de 50%. Esto se debe a que los costos de los distintos errores no son simétricos.
- La tasa de clasificación incorrecta por lo tanto es un resumen poco relevante para muchos problemas.
- Para la mayoría de problemas de clasificación, es un error ignorar las probabilidades de clase estimadas. No es lo mismo que un cliente tenga 99% de probabilidad de mantenerse al corriente que otro que tiene 55%. Ambos son clasificados de la misma manera por el clasificador de máxima probabilidad.
- Puede haber dos predictores con tasas similares de clasificación incorrecta, pero distintos en cuanto a score de Brier o pérdida logarítmica, y mejor score de Brier y pérdida logarítmica indican muchas veces mejor separación de clases.
Más adelante veremos cómo construir reglas de clasificación a partir de probabilidades, pero por el momento notamos que tomar decisiones de cómo clasificar en el proceso de construcción de predictores es mala idea, pues confundimos el desempeño predictivo con los costos de tomar cada decisión de clasificación. Esos costos muchas veces no están perfectamente planteados, de forma que es mejor usar probabilidades para presentar a tomadores de decisiones o calcular simulaciones de costo-beneficio.
Ejemplo
Veamos cómo se comporta en términos de error de clasificación nuestro último modelo:
|>
dat_log_loss bind_cols(predict(ajuste_vmc, dat_ent)) |>
mutate(correcto = .pred_class == g) |>
ungroup() |> summarise(p_correctos = mean(correcto)) |>
mutate(error_clasif = 1 - p_correctos)
# A tibble: 1 × 2
p_correctos error_clasif
<dbl> <dbl>
1 0.794 0.206
Y calculamos el error de clasificación de prueba:
|>
dat_log_loss_prueba bind_cols(predict(ajuste_vmc, dat_prueba)) |>
mutate(correcto = .pred_class == g) |>
ungroup() |> summarise(p_correctos = mean(correcto)) |>
mutate(error_clasif = 1 - p_correctos)
# A tibble: 1 × 2
p_correctos error_clasif
<dbl> <dbl>
1 0.802 0.198
8.10 Regresión logística multinomial
Para estimar \(K\) probabilidades de clase, consideramos \(k\) predictores lineales individuales
\[f_k(x) = \beta_{0, k} + \beta_{1,k} x_1 + \cdots + \beta_{p,k} x_p\]
y la probabilidad de clase la calculamos haciendo softmax sobre estas:
\[p_k(x) = \frac{e^{f_k(x)}}{\sum_i e^{f_i(x)}}\]
que necesariamente suman uno. Nótese si embargo que este modelo está sobreparametrizado, pues solamente es necesario escribir \(K-1\) de estas probabilidades, y la última tiene que ser el complemento para que sumen uno. Podemos ver esto, por ejemplo, si sumamos \(h(x) = \gamma_0 + \gamma_1x_1 + \cdots \gamma_p x_p\) a todas las funciones:
\[p_k(x) = \frac{e^{f_k(x) + h(x)}}{\sum_i e^{f_i(x) + h(x)}} = \frac{e^{f_k(x)}}{\sum_i e^{f_i(x)}}\]
En regresión logística, por ejemplo, si tenemos \(f_1(x)\) y \(f_0(x)\) podemos tomar \(f_0(x) = 0\), de forma que \[p_1(x) = \frac{e^{f_1(x)}}{1+ e^{f_1(x)}}\] que es justamente el modelo de regresión logística como lo escribimos arriba. Siempre es posible entonces crear una clase de referencia, con coeficientes igual a 0, aunque esto no es necesario si usamos regularización, que generalmente es el caso.
8.11 Ejemplo: clasificación de ropa
Para el siguiente problema tenemos imágenes en blanco y negro de artículos de ropa
library(imager)
<- dataset_fashion_mnist()
ropa_datos <- ropa_datos$train
ropa_entrena <- ropa_datos$test
ropa_prueba # estas son las categorias:
<- c("playera/top", "pantalón", "suéter", "vestido", "abrigo", "sandalia",
articulos "camisa", "tenis", "bolsa", "bota")
<- tibble(
etiquetas_tbl codigo = 0:9,
articulo = c("playera", "pantalón", "suéter", "vestido", "abrigo", "sandalia",
"camisa", "tenis", "bolsa", "bota"))
<- ropa_entrena$x
x <- ropa_entrena$y y
Por ejemplo:
par(mfrow = c(4, 4), mar = c(1,1,1,1))
for(i in 2:17) plot(as.cimg(t(x[2 + i, ,])), axes = FALSE, main = articulos[y[2 + i] + 1])
Utilizaremos regresión logistica multinomial con keras y regularización L2:
<- 10
num_classes <- c(28, 28, 1)
input_shape
# normalizar
<- ropa_entrena$x / 255
x_entrena <- ropa_prueba$x / 255
x_prueba <- to_categorical(ropa_entrena$y, num_classes)
y_entrena <- to_categorical(ropa_prueba$y, num_classes)
y_prueba if(FALSE){
#' Model definition
#' (architecture taken from
#' https://keras.rstudio.com/articles/examples/mnist_cnn.html )
<- keras_model_sequential()
modelo_ropa |>
modelo_ropa layer_flatten() |>
layer_dense(units = 10,
activity_regularizer = regularizer_l2(l = 0.0005),
activation = 'softmax')
# compile model
|> compile(
modelo_ropa loss = "categorical_crossentropy",
optimizer = optimizer_adam(learning_rate = 0.0005),
metrics = c("accuracy")
)
# train and evaluate
|> fit(
modelo_ropa
x_entrena, y_entrena,batch_size = 128,
epochs = 40,
verbose = 1,
validation_data = list(x_prueba, y_prueba)
)save_model_tf(modelo_ropa, "cache/red_ropa_1")
}
<- load_model_tf("cache/red_ropa_1")
modelo_ropa <- modelo_ropa |> evaluate(
scores verbose = 0
x_prueba, y_prueba,
)cat('Test pérdida:', scores[[1]], '\n')
Test pérdida: 0.4358351
cat('Test clasificación correcta:', scores[[2]], '\n')
Test clasificación correcta: 0.8478
Ahora podemos hacer predicciones:
<- predict(modelo_ropa, x_prueba)
preds_mat dim(preds_mat)
[1] 10000 10
<- round(preds_mat[1,], 3)
probs tibble(articulo = articulos, prob = probs) |>
arrange(probs)
# A tibble: 10 × 2
articulo prob
<chr> <dbl>
1 playera/top 0
2 pantalón 0
3 suéter 0
4 vestido 0
5 abrigo 0
6 camisa 0
7 bolsa 0.005
8 tenis 0.051
9 sandalia 0.117
10 bota 0.827
plot(as.cimg(t(ropa_prueba$x[1,,])), axes = FALSE, main = articulos[ropa_prueba$y[1] + 1])