Clase 19 Apéndice: Descenso en gradiente para regresión logística
Ahora veremos cómo aprender los coeficientes de regresión logística con una muestra de entrenamiento. La idea general es :
- Usamos la pérdida logarítmica de entrenamiento como medida de ajuste
- Usamos descenso en gradiente para minimizar esta pérdida y aprender los coeficientes.
Sea entonces \({\mathcal L}\) una muestra de entrenamiento:
\[{\mathcal L}=\{ (x^{(1)},y^{(1)}),(x^{(2)},y^{(2)}), \ldots, (x^{(N)}, y^{(N)}) \}\]
Donde \(y=1\) o \(y=0\) son las dos clases. Escribimos también
\[p_1(x)=p_1(x;\beta)= h(\beta_0+\beta_1x_1 + \beta_2x_2 +\cdots + \beta_p x_p),\]
y definimos la pérdida logarítmica sobre el conjunto de entrenamiento
\[D(\beta) = -\frac{1}{N}\sum_{i=1}^N \log(p_{y^{(i)}} (x^{(i)})).\]
Los coeficientes estimados por regresión logística están dados por \[\hat{\beta} = \arg\min_\beta D(\beta)\]
Para minimizar utilizaremos descenso en gradiente (aunque hay más opciones).
La última expresión para \(D(\beta)\) puede ser difícil de operar, pero podemos reescribir como: \[D(\beta) = -\frac{2}{N}\sum_{i=1}^N y^{(i)} \log(p_{1} (x^{(i)})) + (1-y^{(i)}) \log(p_{0} (x^{(i)})).\]
Para hacer descenso en gradiente, necesitamos encontrar \(\frac{\partial D}{\beta_j}\) para \(j=1,2,\ldots,p\).
Comenzamos por calcular la derivada de un término:
\[D^{(i)} (\beta) = y^{(i)} \log(p_{1} (x^{(i)})) + (1-y^{(i)}) \log(1-p_{1} (x^{(i)}))\]
Calculamos primero las derivadas de \(p_1 (x^{(i)};\beta)\) (demostrar la siguiente ecuación): \[\frac{\partial p_1}{\partial \beta_0} = {p_1(x^{(i)})(1-p_1(x^{(i)}))},\] y \[\frac{\partial p_1}{\partial \beta_j} = p_1(x^{(i)})(1-p_1(x^{(i)}))x_j^{(i)},\]
Así que \[\begin{align*} \frac{\partial D^{(i)}}{\partial \beta_j} &= \frac{y^{(i)}}{(p_1(x^{(i)}))}\frac{\partial p_1}{\partial \beta_j} - \frac{1- y^{(i)}}{(1-p_1(x^{(i)}))}\frac{\partial p_1}{\partial \beta_j} \\ &= \left( \frac{y^{(i)} - p_1(x^{(i)})}{(p_1(x^{(i)}))(1-p_1(x^{(i)}))} \right )\frac{\partial p_1}{\partial \beta_j} \\ & = \left ( y^{(i)} - p_1(x^{(i)}) \right ) x_j^{(i)} \\ \end{align*}\]
para \(j=0,1,\ldots,p\), usando la convención de \(x_0^{(i)}=1\). Podemos sumar ahora sobre la muestra de entrenamiento para obtener
\[ \frac{\partial D}{\partial\beta_j} = - \frac{1}{N}\sum_{i=1}^N (y^{(i)}-p(x^{(i)}))x_j^{(i)}\]
De modo que,
Podríamos usar las siguientes implementaciones, que representan cambios menores de lo que hicimos en regresión lineal. En primer lugar, escribimos la función que calcula la log pérdida. Podríamos poner:
<- function(x, y){
log_perdida_calc_simple <- function(beta){
log_perdida_fun <- h(as.matrix(cbind(1, x)) %*% beta)
p_beta - mean(y*log(p_beta) + (1-y)*log(1-p_beta))
}
log_perdida_fun }
*Observación Sin embargo, podemos hacer una simplificación para tener mejor desempeño y estabilidad. Observamos que \[\log (p_1(x;\beta)) = \log\frac{ e^{x^t \beta}}{1+ e^{x^t\beta}} = x^t\beta - \log Z\] donde \(Z = 1+ e^{x^t\beta}\). Por otra parte \[\log(p_0(x;\beta)) = \log\frac{ 1}{1+ e^{x^t\beta}} = - \log Z\] De modo que \[y\log(p_1(x;\beta)) + (1- y)\log(p_0(x;\beta)) = yx^t\beta - \log Z= yx^t\beta - \log (1+e^{x^t\beta})\] Así que podemos escribir:
<- function(x, y){
log_perdida_calc <- function(beta){
dev_fun <- as.matrix(cbind(1, x)) %*% beta
x_beta - mean(y * x_beta - log(1 + exp(x_beta)))
}
dev_fun }
Y para el gradiente
<- function(x_ent, y_ent){
grad_calc <- function(beta){
salida_grad <- nrow(x_ent)
N <- h(as.matrix(cbind(1, x_ent)) %*% beta)
p_beta <- y_ent - p_beta
e <- - (1 / N) * as.numeric(t(cbind(1,x_ent)) %*% e)
grad_out names(grad_out) <- c('Intercept', colnames(x_ent))
grad_out
}
salida_grad
}<- function(n, z_0, eta, h_deriv){
descenso <- matrix(0,n, length(z_0))
z 1, ] <- z_0
z[for(i in 1:(n-1)){
+1, ] <- z[i, ] - eta * h_deriv(z[i, ])
z[i
}
z }
Ejemplo
Probemos nuestros cálculos con el ejemplo de 1 entrada de tarjetas de crédito.
<- function(x){
p_1 ifelse(x < 0.15, 0.95, 0.95 - 0.7 * (x - 0.15))
}<- function(z) { 1 / ( 1 + exp(-z))}
h <- 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
<- dat_ent |> ungroup() |> mutate(x_s = (x - mean(x))/sd(x))
dat_ent <- log_perdida_calc(dat_ent[, 'x_s', drop = FALSE], dat_ent$y)
log_perdida <- grad_calc(dat_ent[, 'x_s', drop = FALSE], dat_ent$y)
grad grad(c(0,1))
## Intercept x_s
## -0.2889566 0.3990662
grad(c(0.5,-0.1))
## Intercept x_s
## -0.1538118 0.1663577
Podemos verificamos el cálculo de gradiente usando una aproximación numérica
log_perdida(c(0.5+0.0001,-0.1)) - log_perdida(c(0.5,-0.1)))/0.0001 (
## [1] -0.1538001
log_perdida(c(0.5,-0.1+0.0001)) - log_perdida(c(0.5,-0.1)))/0.0001 (
## [1] 0.1663696
Y hacemos descenso:
<- descenso(1000, z_0 = c(0,0), eta = 0.1, h_deriv = grad)
iteraciones tail(iteraciones, 20)
## [,1] [,2]
## [981,] 1.505927 -1.056649
## [982,] 1.505928 -1.056650
## [983,] 1.505928 -1.056650
## [984,] 1.505929 -1.056651
## [985,] 1.505930 -1.056651
## [986,] 1.505930 -1.056652
## [987,] 1.505931 -1.056652
## [988,] 1.505932 -1.056653
## [989,] 1.505933 -1.056653
## [990,] 1.505933 -1.056654
## [991,] 1.505934 -1.056654
## [992,] 1.505935 -1.056655
## [993,] 1.505935 -1.056655
## [994,] 1.505936 -1.056656
## [995,] 1.505937 -1.056656
## [996,] 1.505937 -1.056657
## [997,] 1.505938 -1.056657
## [998,] 1.505939 -1.056658
## [999,] 1.505939 -1.056658
## [1000,] 1.505940 -1.056658
#Checamos devianza
qplot(1:nrow(iteraciones), apply(iteraciones, 1, log_perdida)) +
xlab("Iteración") + ylab("Devianza")
# Y gradiente de devianza en la iteración final:
grad(iteraciones[nrow(iteraciones), ])
## Intercept x_s
## -6.253020e-06 4.518278e-06
Comparamos con glm:
<- glm(y ~ x_s, data = dat_ent, family = 'binomial')
mod_1 coef(mod_1)
## (Intercept) x_s
## 1.506007 -1.056707
$deviance mod_1
## [1] 434.0864
log_perdida(iteraciones[200,])
## [1] 0.435333
La devianza que obtenemos con nuestros cálculos es:
$deviance / (2 * nrow(dat_ent)) mod_1
## [1] 0.4340864
log_perdida(iteraciones[200,])
## [1] 0.435333
Máxima verosimilitud
Es fácil ver que este método de estimación de los coeficientes (minimizando la devianza de entrenamiento) es el método de máxima verosimilitud. La verosimilitud de la muestra de entrenamiento está dada por: \[L(\beta) =\prod_{i=1}^N p_{y^{(i)}} (x^{(i)})\] Y la log verosimilitud es \[l(\beta) =\sum_{i=1}^N \log(p_{y^{(i)}} (x^{(i)})).\] Así que ajustar el modelo minimizando la expresión (7.1) es los mismo que hacer máxima verosimilitud (condicional a los valores de \(x\)).
Normalización
Igual que en regresión lineal, en regresión logística conviene normalizar las entradas antes de ajustar el modelo
Desempeño de regresión logística como método de aprendizaje
Igual que en regresión lineal, regresión logística supera a métodos más sofisticados o nuevos en numerosos ejemplos. Las razones son similares: la rigidez de regresión logística es una fortaleza cuando la estructura lineal es una buena aproximación.
Solución analítica
El problema de regresión logística no tiene solución analítica. Paquetes como glm utilizan métodos numéricos (Newton-Raphson para regresión logística, por ejemplo).
Interpretación de modelos logísticos
Todas las precauciones que mencionamos en modelos lineales aplican para los modelos logísticos (aspectos estadísticos del ajuste, relación con fenómeno de interés, argumentos de causalidad). Igual que en regresión lineal, podemos explicar el comportamiento de las probabilidades de clase ajustadas, pero es un poco más difícil por la no linealidad introducida por la función logística.
Ejemplo
Consideremos el modelo ajustado:
head(dat_ent)
## # A tibble: 6 × 4
## x g y x_s
## <dbl> <fct> <dbl> <dbl>
## 1 0.0240 al_corriente 1 -1.05
## 2 0.565 al_corriente 1 0.721
## 3 0.361 impago 0 0.0536
## 4 0.701 impago 0 1.17
## 5 0.128 al_corriente 1 -0.707
## 6 0.207 al_corriente 1 -0.450
<- iteraciones[200,]
coeficientes names(coeficientes) <- c("Intercept", "x_s")
coeficientes
## Intercept x_s
## 1.3766661 -0.9629202
Como centramos todas las entradas, la ordenada al origen (Intercept) se interpreta como la probabilidad de clase cuando todas las variables están en su media:
options(digits = 2)
1] coeficientes[
## Intercept
## 1.4
h(coeficientes[1])
## Intercept
## 0.8
Esto quiere decir que la probabilidad de estar al corriente es de 80% cuando la variable \(x\) está en su media. Si \(x\) se incrementa en una desviación estándar, la cantidad \[z = \beta_0 + \beta_1x\] la probabilidad de estar al corriente cambia a 60%:
h(coeficientes[1]+ coeficientes[2]*1)
## Intercept
## 0.6
Nótese que una desviación estándar de \(x\) equivale a
sd(dat_ent$x)
## [1] 0.31
Así que en las unidades originales, un incremento de 0.31 en la variable \(x\) implica un cambio de
h(coeficientes[1] + coeficientes[2]) - h(coeficientes[1])
## Intercept
## -0.2
es decir, la probabilidad de manenterse al corriente baja 20 puntos porcentuales, de 80% a 60% Ojo: En regresión lineal, las variables contribuyen independientemente de otras al predictor. Eso no pasa en regresión logística debido a la no linealidad introducida por la función logística \(h\). Por ejemplo, imaginemos el modelo: \[p(z) = h(0.5 + 0.2 x_1 -0.5 x_2 + 0.7x_3),\] y suponemos las entradas normalizadas. Si todas las variables están en su media, la probabilidad de clase 1 es
h(0.5)
## [1] 0.62
Si todas las variables están en su media, y cambiamos en 1 desviación estándar la variable \(x_1\), la probabilidad de clase 1 es:
h(0.5 + 0.2)
## [1] 0.67
Y el cambio en puntos de probabilidad es:
h(0.5 + 0.2) - h(0.5)
## [1] 0.046
Pero si la variable \(x_2 = -1\), por ejemplo, el cambio en probabilidad es de
h(0.5 + 0.2 - 0.5 * (-1)) - h(0.5 - 0.5 * (-1))
## [1] 0.037
19.1 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 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
## # … with 190 more rows
$id <- 1:nrow(diabetes_ent)
diabetes_ent$id <- 1:nrow(diabetes_pr) diabetes_pr
Normalizamos
<- recipe(type ~ ., diabetes_ent) |>
receta_diabetes update_role(id, new_role = "id_variable") |>
step_normalize(all_predictors()) |>
prep()
<- receta_diabetes |> juice()
diabetes_ent_s <- receta_diabetes |> bake(diabetes_pr) diabetes_pr_s
<- diabetes_ent_s |> select(-type, -id) |> as.matrix()
x_ent <- ncol(x_ent)
p <- diabetes_ent_s$type == 'Yes'
y_ent <- grad_calc(x_ent, y_ent)
grad <- descenso(1000, rep(0,p+1), 0.1, h_deriv = grad)
iteraciones matplot(iteraciones, type = "l")
<- tibble(variable = c('Intercept',colnames(x_ent)), coef = iteraciones[1000,])
diabetes_coef diabetes_coef
## # A tibble: 8 × 2
## variable coef
## <chr> <dbl>
## 1 Intercept -0.955
## 2 npreg 0.347
## 3 glu 1.02
## 4 bp -0.0547
## 5 skin -0.0187
## 6 bmi 0.509
## 7 ped 0.559
## 8 age 0.451
Ahora calculamos devianza de prueba y error de clasificación:
<- diabetes_pr_s |> select(-type, -id) |> as.matrix()
x_prueba <- diabetes_pr_s$type == 'Yes'
y_prueba <- log_perdida_calc(x_prueba, y_prueba)
log_perdida_prueba log_perdida_prueba(iteraciones[1000,])
## [1] 0.44
Y para el error clasificación de prueba, necesitamos las probabilidades de clase ajustadas:
<- iteraciones[1000, ]
beta <- h(as.matrix(cbind(1, x_prueba)) %*% beta)
p_beta <- as.numeric(p_beta > 0.5)
y_pred mean(y_prueba != y_pred)
## [1] 0.2
Vamos a repetir usando keras.
library(keras)
##
## Attaching package: 'keras'
## The following object is masked from 'package:yardstick':
##
## get_weights
# 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(lr = 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 = 20, # número de iteraciones
verbose = 0
)plot(historia)
## `geom_smooth()` using formula 'y ~ x'
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 = 400, # número de iteraciones
verbose = 0
)
Los errores de entrenamiento y prueba son:
options(scipen = 0, digits = 4)
evaluate(modelo_diabetes, x_ent, y_ent)
## loss binary_crossentropy
## 0.446 0.446
evaluate(modelo_diabetes, x_prueba, y_prueba)
## loss binary_crossentropy
## 0.4407 0.4407
Veamos que coeficientes obtuvimos:
get_weights(modelo_diabetes)
## [[1]]
## [,1]
## [1,] 0.34734
## [2,] 1.01705
## [3,] -0.05473
## [4,] -0.02247
## [5,] 0.51263
## [6,] 0.55927
## [7,] 0.45201
##
## [[2]]
## [1] -0.9558
Y comparamos con lo que obtenemos de glm:
# podemos hacerlo con workflows, como vimos arriba.
# aquí usamos directamente la interfaz de glm en R
<- glm(type ~ ., diabetes_ent_s |> select(-id), family = binomial())
mod_1 |> coef() mod_1
## (Intercept) npreg glu bp skin bmi
## -0.95583 0.34734 1.01705 -0.05473 -0.02247 0.51263
## ped age
## 0.55928 0.45201