diff --git a/05-regularizacion-1_files/figure-html/unnamed-chunk-34-1.png b/05-regularizacion-1_files/figure-html/unnamed-chunk-34-1.png index df7dc44a..8c3cc6ec 100644 Binary files a/05-regularizacion-1_files/figure-html/unnamed-chunk-34-1.png and b/05-regularizacion-1_files/figure-html/unnamed-chunk-34-1.png differ diff --git a/08-clasificacion-1.html b/08-clasificacion-1.html index 7f98076a..9f563ce8 100644 --- a/08-clasificacion-1.html +++ b/08-clasificacion-1.html @@ -2625,11 +2625,11 @@

Ejemplo

.threshold specificity sensitivity <dbl> <dbl> <dbl> 1 -Inf 0 1 - 2 0.00305 0 1 - 3 0.00345 0.00448 1 - 4 0.00406 0.00897 1 - 5 0.00437 0.0135 1 - 6 0.00476 0.0224 1 - 7 0.00486 0.0269 1 - 8 0.00504 0.0314 1 - 9 0.00512 0.0359 1 -10 0.00562 0.0404 1 + 2 0.00306 0 1 + 3 0.00347 0.00448 1 + 4 0.00408 0.00897 1 + 5 0.00439 0.0135 1 + 6 0.00478 0.0224 1 + 7 0.00488 0.0269 1 + 8 0.00506 0.0314 1 + 9 0.00514 0.0359 1 +10 0.00564 0.0404 1 # ℹ 311 more rows
ggplot(roc_tbl, 
diff --git a/09-clasificacion-2_files/figure-html/unnamed-chunk-17-1.png b/09-clasificacion-2_files/figure-html/unnamed-chunk-17-1.png
index 50c15b03..738c7e07 100644
Binary files a/09-clasificacion-2_files/figure-html/unnamed-chunk-17-1.png and b/09-clasificacion-2_files/figure-html/unnamed-chunk-17-1.png differ
diff --git a/09-clasificacion-2_files/figure-html/unnamed-chunk-20-1.png b/09-clasificacion-2_files/figure-html/unnamed-chunk-20-1.png
index 93cff6da..940cc6bf 100644
Binary files a/09-clasificacion-2_files/figure-html/unnamed-chunk-20-1.png and b/09-clasificacion-2_files/figure-html/unnamed-chunk-20-1.png differ
diff --git a/09-clasificacion-2_files/figure-html/unnamed-chunk-22-1.png b/09-clasificacion-2_files/figure-html/unnamed-chunk-22-1.png
index ec25ab47..d671f967 100644
Binary files a/09-clasificacion-2_files/figure-html/unnamed-chunk-22-1.png and b/09-clasificacion-2_files/figure-html/unnamed-chunk-22-1.png differ
diff --git a/09-clasificacion-2_files/figure-html/unnamed-chunk-23-1.png b/09-clasificacion-2_files/figure-html/unnamed-chunk-23-1.png
index 1670ad12..50ffaf03 100644
Binary files a/09-clasificacion-2_files/figure-html/unnamed-chunk-23-1.png and b/09-clasificacion-2_files/figure-html/unnamed-chunk-23-1.png differ
diff --git a/09-clasificacion-2_files/figure-html/unnamed-chunk-24-1.png b/09-clasificacion-2_files/figure-html/unnamed-chunk-24-1.png
index d54799ec..cd24d457 100644
Binary files a/09-clasificacion-2_files/figure-html/unnamed-chunk-24-1.png and b/09-clasificacion-2_files/figure-html/unnamed-chunk-24-1.png differ
diff --git a/09-clasificacion-2_files/figure-html/unnamed-chunk-25-1.png b/09-clasificacion-2_files/figure-html/unnamed-chunk-25-1.png
index cccea466..e7c4bafe 100644
Binary files a/09-clasificacion-2_files/figure-html/unnamed-chunk-25-1.png and b/09-clasificacion-2_files/figure-html/unnamed-chunk-25-1.png differ
diff --git a/09-clasificacion-2_files/figure-html/unnamed-chunk-3-1.png b/09-clasificacion-2_files/figure-html/unnamed-chunk-3-1.png
index 4ec707c0..5fb36c73 100644
Binary files a/09-clasificacion-2_files/figure-html/unnamed-chunk-3-1.png and b/09-clasificacion-2_files/figure-html/unnamed-chunk-3-1.png differ
diff --git a/10-clasificacion-calibracion.html b/10-clasificacion-calibracion.html
index afdc9663..c598516d 100644
--- a/10-clasificacion-calibracion.html
+++ b/10-clasificacion-calibracion.html
@@ -353,23 +353,23 @@ 

Ejemplo: diabe dat_calibracion |> head() |> gt()

-
- @@ -885,23 +885,23 @@

Ejemplo: dia calibracion_gpos |> gt() |> fmt_number(where(is_double), decimals = 3)

-
- @@ -1458,23 +1458,23 @@

preds_tbl |> head() |> gt() |> fmt_number(where(is_double), decimals = 3)

-
- diff --git a/10-clasificacion-calibracion_files/figure-html/unnamed-chunk-4-1.png b/10-clasificacion-calibracion_files/figure-html/unnamed-chunk-4-1.png index 7ca1fc85..7fa623d4 100644 Binary files a/10-clasificacion-calibracion_files/figure-html/unnamed-chunk-4-1.png and b/10-clasificacion-calibracion_files/figure-html/unnamed-chunk-4-1.png differ diff --git a/12-arboles_files/figure-html/unnamed-chunk-17-1.png b/12-arboles_files/figure-html/unnamed-chunk-17-1.png index 7604b978..e657af74 100644 Binary files a/12-arboles_files/figure-html/unnamed-chunk-17-1.png and b/12-arboles_files/figure-html/unnamed-chunk-17-1.png differ diff --git a/13-arboles-boosting.html b/13-arboles-boosting.html index 9d5328d0..c49105a5 100644 --- a/13-arboles-boosting.html +++ b/13-arboles-boosting.html @@ -947,7 +947,7 @@

system.time(modelo <- xgb.train(d_entrena, verbose = 1, nrounds = 10000, params = params))

   user  system elapsed 
- 15.276   0.799   4.087 
+ 15.522 0.782 4.154
xgb.save(model = modelo, fname = "./cache/casas_boost.xgb")
diff --git a/14-interpretacion.html b/14-interpretacion.html index fb2ee018..e8c6a2a0 100644 --- a/14-interpretacion.html +++ b/14-interpretacion.html @@ -984,7 +984,7 @@

Ejemplo

} graf_caso(data, contrib, 10, pred_base)
-
[1] "Predicción base: 0.18, Predicción: 0.88"
+
[1] "Predicción base: 0.17, Predicción: 0.88"

@@ -993,7 +993,7 @@

Ejemplo

graf_caso(data, contrib, 102, pred_base)
-
[1] "Predicción base: 0.18, Predicción: 0.48"
+
[1] "Predicción base: 0.17, Predicción: 0.77"

diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-10-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-10-1.png index d738f211..30ef3534 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-10-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-10-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-27-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-27-1.png index 05954396..09343bf1 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-27-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-27-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-28-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-28-1.png index f91c0cf5..0bfe723a 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-28-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-28-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-29-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-29-1.png index c98838a9..d248e9d8 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-29-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-29-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-30-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-30-1.png index d0649ec1..40a22f70 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-30-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-30-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-4-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-4-1.png index b7080364..945d5c51 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-4-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-4-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-5-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-5-1.png index a9dbf888..8331d880 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-5-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-5-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-5-2.png b/14-interpretacion_files/figure-html/unnamed-chunk-5-2.png index 03632774..68d5de69 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-5-2.png and b/14-interpretacion_files/figure-html/unnamed-chunk-5-2.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-6-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-6-1.png index 2495d9f8..8410dbf4 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-6-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-6-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-7-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-7-1.png index 781240d4..22392bb6 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-7-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-7-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-8-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-8-1.png index d4225c3f..a5337526 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-8-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-8-1.png differ diff --git a/14-interpretacion_files/figure-html/unnamed-chunk-9-1.png b/14-interpretacion_files/figure-html/unnamed-chunk-9-1.png index d133917b..a0799a30 100644 Binary files a/14-interpretacion_files/figure-html/unnamed-chunk-9-1.png and b/14-interpretacion_files/figure-html/unnamed-chunk-9-1.png differ diff --git a/15-reduccion-dim_files/figure-html/unnamed-chunk-70-1.png b/15-reduccion-dim_files/figure-html/unnamed-chunk-70-1.png index 53b8ce41..71f82e67 100644 Binary files a/15-reduccion-dim_files/figure-html/unnamed-chunk-70-1.png and b/15-reduccion-dim_files/figure-html/unnamed-chunk-70-1.png differ diff --git a/16-recom-factorizacion.html b/16-recom-factorizacion.html index 84eeebdc..03c03759 100644 --- a/16-recom-factorizacion.html +++ b/16-recom-factorizacion.html @@ -305,27 +305,29 @@

Tabla de contenidos

-
  • 16.4 Retroalimentación implícita +
  • 16.4 Búsqueda por similitud y Hashing
  • +
  • 16.5 Retroalimentación implícita
  • -
  • 17 Representación de palabras y word2vec +
  • 17 Embeddings de imágenes
  • +
  • 18 Representación de palabras y word2vec
      -
    • 17.1 Modelo de red neuronal +
    • 18.1 Modelo de red neuronal
    • -
    • 17.2 Representación de palabras
    • -
    • 17.3 Modelos de word2vec +
    • 18.2 Representación de palabras y similitud
    • +
    • 18.3 Modelos de word2vec
    • -
    • 17.4 Espacio de representación de palabras +
    • 18.4 Espacio de representación de palabras
      • Geometría en el espacio de representaciones
      • Geometría en el espacio de representaciones
      • @@ -501,7 +503,7 @@

        16.1.3 Combinación con modelo base

        Podemos usar también ideas de nuestro modelo base y modelar desviaciones en lugar de calificaciones directamente:

        -

        Si \(X^0\) son las predicciones del modelo de referencia, y \[R = X-X^0\] son los residuales del modelo base, buscamos mejor \[R\approx \widetilde{X} = UV^t\] de manera que las predicciones finales son \[X^0 + \widetilde{X}\]

        +

        Si \(X^0\) son las predicciones del modelo de referencia (la media de la película más un ajuste que es la diferencia de la media de cada individuo menos la media global), y \[R = X-X^0\] son los residuales del modelo base, buscamos mejor \[R\approx \widetilde{X} = UV^t\] de manera que las predicciones finales son \[X^0 + \widetilde{X}\]

        Veremos también más adelante cómo regularizar estos sesgos como parte de la construcción del modelo.

        @@ -538,8 +540,343 @@

        spark. La implementación está basada parcialmente en (Zhou et al. 2008). La inicialización es diferente en spark, ver el código, donde cada renglón se inicializa con un vector de \(N(0,1)\) normalizado.

        -
        -

        16.4 Retroalimentación implícita

        +
        +

        16.4 Búsqueda por similitud y Hashing

        +

        Cuando tenemos las representaciones de personas/artículos, muchas veces los sistemas de recomendación pueden requerir buscar personas o artículos similares a uno dado (por ejemplo, para hacer una playlist basada en una canción, o mostrar artículos relacionados). Con este fin, podemos definir una medida de similitud entre vectores, por ejemplo, distancia euclideana o distancia coseno (ángulo entre los vectores).

        +

        El objetivo es entonces, dado el embedding (vector) de un artículo, por ejemplo, encontrar los 10 vectores más cercanos según nuestra medida de similitud. Este problema no es trivial si el espacio de artículos es muy grande, pues requiere recorrer todos los vectores y calcular la similitud. Igualmente, precalcular todas las similitudes es costoso en tiempo y espacio, pues requiere \(O(n^2)\) operaciones (considerar todos los pares posibles).

        +

        Una solución aproximada que se utiliza con frecuencia (ver por ejemplo esta librería de Spotify) cuando buscamos encontrar elementos muy similares comienza utilizando reducción de dimensionalidad utilizando proyecciones aleatorias, y formando cubetas de candidatos de similitud.

        +

        La idea general es como sigue: supongamos que utilizamos la similitud coseno. Escogemos al azar \(k\) vectores \(w_1,\ldots, w_k\) en el espacio de embeddings que nos interesa. Si tomamos un artículo dado representado por el vector \(v\), entonces para cada \(j\) consideramos:

        +
          +
        • Si el producto punto \(w_j^t v\) es positivo, ponemos \(f_j(v) = 1\)
        • +
        • En otro caso ponemos \(f_j(v) = -1\).
        • +
        +

        Con esto obtenemos otra representación discreta de \(v\) como vector \((f_1(v),f_2(v),\dots, f_k(v) )\), que es una representación más pequeña (podemos codificar como entero por ejemplo). Agrupamos todos los artículos que tienen la misma representación en una misma cubeta. Así, cuando queremos buscar los artículos muy similares, sólo es necesario buscar en la cubeta correspondiente.

        +

        Estas funciones que transforman vectores en valores 1 o -1 se llaman, por su naturaleza aleatoria funciones hash. Construimos ahora nuestra función generadora de hashes:

        +
        +
        gen_hash <- function(p){
        +  # p es la dimensión del espacio
        +  v <- rnorm(p)
        +  # devolvemos una función que calcula la cubeta:
        +  function(x){
        +    sum(x * v) |> sign()
        +  }
        +}
        +set.seed(823)
        +hash_1 <- gen_hash(2)
        +# los hashes de dos puntos:
        +hash_1(c(4, 7))
        +
        +
        [1] -1
        +
        +
        hash_1(c(-4, 7))
        +
        +
        [1] 1
        +
        +
        # el vector que escogimos es
        +environment(hash_1)$v
        +
        +
        [1] -0.9929091 -0.4400476
        +
        +
        +
        +

        Ejemplo

        +

        La siguiente función genera dos clusters de puntos mezclados con puntos distribuidos normales con desviación estándar relativamente grande

        +
        +
        set.seed(1021)
        +simular_puntos <- function(d = 2, n = 200){
        +  #puntos muy cercanos a (3,3,..., 3):
        +  mat_1 <- matrix(rnorm(10 * d, sd = 0.01) + 3, ncol = d)
        +  #puntos muy cercanos a (-3,-3,..., -3):
        +  mat_2 <- matrix(rnorm(10 * d, sd = 0.01) - 3, ncol = d)
        +  # puntos distribuidos alrededor del origen:
        +  mat_3 <- matrix(rnorm(n * d, sd = 10), ncol = d)
        +  datos_tbl_vars <- rbind(mat_3, mat_1, mat_2)  |> 
        +    as_tibble() |> 
        +    mutate(id_1 = row_number())
        +  datos_tbl_vars
        +}
        +# diez puntos en cluster 1, diez en cluster 2, y 100 sin cluster:
        +datos_tbl_vars <- simular_puntos(d = 2, n = 100)
        +
        +
        Warning: The `x` argument of `as_tibble.matrix()` must have unique column names if
        +`.name_repair` is omitted as of tibble 2.0.0.
        +ℹ Using compatibility `.name_repair`.
        +
        +
        ggplot(datos_tbl_vars, aes(x = V1, y= V2)) + 
        +  geom_jitter(width = 0.5, height = 0.5, alpha = 0.3)
        +
        +

        +
        +
        +

        Para este ejemplo calculamos las similitudes reales entre todos los pares de puntos (esto generalmente es muy lento para datos grandes):

        +
        +
        sim_e <- function(x, y){
        +  sum(x * y) / sqrt(sum(x^2) * sum(y^2))
        +}
        +datos_tbl <- datos_tbl_vars |>
        +  pivot_longer(-id_1, names_to = "variable", values_to = "valor") |> 
        +  group_by(id_1) |>
        +  arrange(variable) |>
        +  summarise(vec_1 = list(valor))
        +system.time(
        +pares_tbl <- datos_tbl |> 
        +    crossing(datos_tbl |> 
        +        rename(id_2 = id_1, vec_2 = vec_1)) |>
        +    filter(id_1 < id_2) |>
        +    mutate(sim = map2_dbl(vec_1, vec_2, sim_e))
        +)
        +
        +
           user  system elapsed 
        +  0.028   0.000   0.028 
        +
        +
        pares_tbl |> head()
        +
        +
        # A tibble: 6 × 5
        +   id_1 vec_1      id_2 vec_2         sim
        +  <int> <list>    <int> <list>      <dbl>
        +1     1 <dbl [2]>     2 <dbl [2]>  0.925 
        +2     1 <dbl [2]>     3 <dbl [2]>  0.0689
        +3     1 <dbl [2]>     4 <dbl [2]> -0.127 
        +4     1 <dbl [2]>     5 <dbl [2]> -1.00  
        +5     1 <dbl [2]>     6 <dbl [2]>  0.994 
        +6     1 <dbl [2]>     7 <dbl [2]>  0.486 
        +
        +
        +
        +
        nrow(pares_tbl)
        +
        +
        [1] 7140
        +
        +
        +

        Supongamos que queremos encontrar los pares con similitud mayor a 0.999:

        +
        +
        pares_sim <- pares_tbl |> filter(sim > 0.999)
        +nrow(pares_sim)
        +
        +
        [1] 228
        +
        +
        +

        Ahora veremos cómo encontrar estos pares de puntos cercanos.

        +
        +
        +

        Cálculo de firmas

        +

        Usaremos 4 hashes:

        +
        +
        #generar hashes
        +set.seed(88)
        +hash_f <- map(1:4, ~ gen_hash(p = 2))
        +# esta es una función de conveniencia:
        +calculador_hashes <- function(hash_f){
        +  function(z) {
        +    map_int(hash_f, ~ .x(z))
        +  }
        +}
        +calc_hashes <- calculador_hashes(hash_f)
        +
        +

        Calculamos las firmas:

        +
        +
        firmas_tbl <- datos_tbl_vars |> 
        +  pivot_longer(cols = -id_1, names_to = "variable", values_to = "valor") |> 
        +  group_by(id_1) |> 
        +  summarise(vec_1 = list(valor)) |> 
        +  mutate(firma = map(vec_1, ~ calc_hashes(.x))) |> 
        +  select(id_1, firma)
        +firmas_tbl
        +
        +
        # A tibble: 120 × 2
        +    id_1 firma    
        +   <int> <list>   
        + 1     1 <int [4]>
        + 2     2 <int [4]>
        + 3     3 <int [4]>
        + 4     4 <int [4]>
        + 5     5 <int [4]>
        + 6     6 <int [4]>
        + 7     7 <int [4]>
        + 8     8 <int [4]>
        + 9     9 <int [4]>
        +10    10 <int [4]>
        +# ℹ 110 more rows
        +
        +
        firmas_tbl$firma[[1]]
        +
        +
        [1] -1  1 -1  1
        +
        +
        firmas_tbl$firma[[2]]
        +
        +
        [1] -1  1 -1  1
        +
        +
        +

        Para este ejemplo, consideraremos todos los pares que coinciden en todas las cubetas

        +
        +
        cubetas_tbl  <- firmas_tbl |> 
        +  mutate(cubeta = map_chr(firma, ~ paste(.x, collapse = "/"))) 
        +cubetas_tbl
        +
        +
        # A tibble: 120 × 3
        +    id_1 firma     cubeta    
        +   <int> <list>    <chr>     
        + 1     1 <int [4]> -1/1/-1/1 
        + 2     2 <int [4]> -1/1/-1/1 
        + 3     3 <int [4]> 1/-1/-1/-1
        + 4     4 <int [4]> -1/1/1/1  
        + 5     5 <int [4]> 1/-1/1/-1 
        + 6     6 <int [4]> -1/1/-1/1 
        + 7     7 <int [4]> 1/-1/-1/1 
        + 8     8 <int [4]> 1/-1/-1/1 
        + 9     9 <int [4]> 1/-1/-1/1 
        +10    10 <int [4]> -1/1/1/1  
        +# ℹ 110 more rows
        +
        +
        +

        Ahora agrupamos cubetas y filtramos las que tienen más de un elemento

        +
        +
        cubetas_tbl <- cubetas_tbl |> group_by(cubeta) |> 
        +  summarise(ids = list(id_1), n = length(id_1)) |> 
        +  filter(n > 1)
        +cubetas_tbl
        +
        +
        # A tibble: 8 × 3
        +  cubeta     ids            n
        +  <chr>      <list>     <int>
        +1 -1/-1/-1/1 <int [20]>    20
        +2 -1/1/-1/1  <int [16]>    16
        +3 -1/1/1/-1  <int [5]>      5
        +4 -1/1/1/1   <int [20]>    20
        +5 1/-1/-1/-1 <int [16]>    16
        +6 1/-1/-1/1  <int [8]>      8
        +7 1/-1/1/-1  <int [16]>    16
        +8 1/1/1/-1   <int [19]>    19
        +
        +
        +

        Y finalmente, extraemos los pares candidatos:

        +
        +
        candidatos_tbl <- 
        +  cubetas_tbl |> 
        +  mutate(pares_cand = map(ids, ~ combn(.x, 2, simplify = FALSE))) |> 
        +  select(cubeta, pares_cand) |> 
        +  unnest(pares_cand) |> 
        +  unnest_wider(pares_cand, names_sep = "_") |> 
        +  select(-cubeta) |> 
        +  unique()
        +candidatos_tbl
        +
        +
        # A tibble: 949 × 2
        +   pares_cand_1 pares_cand_2
        +          <int>        <int>
        + 1           15           18
        + 2           15           22
        + 3           15           35
        + 4           15           37
        + 5           15           38
        + 6           15           60
        + 7           15           63
        + 8           15           66
        + 9           15           82
        +10           15          111
        +# ℹ 939 more rows
        +
        +
        +
        +
        nrow(candidatos_tbl)
        +
        +
        [1] 949
        +
        +
        +

        En este caso, seguramente tenemos algunos falsos positivos que tenemos que filtrar, y quizá algunos falsos negativos.

        +

        Calculamos distancias para todos los pares candidatos (es una lista mucho más corta generalmente):

        +
        +
        puntos_tbl <- datos_tbl_vars |> 
        +  pivot_longer(V1:V2) |>
        +  group_by(id_1) |> 
        +  select(-name) |> 
        +  summarise(punto = list(value))
        +candidatos_tbl <- 
        +  candidatos_tbl |> 
        +  left_join(puntos_tbl |> rename(pares_cand_1 = id_1, punto_1 = punto)) |> 
        +  left_join(puntos_tbl |> rename(pares_cand_2 = id_1, punto_2 = punto))
        +
        +
        Joining with `by = join_by(pares_cand_1)`
        +Joining with `by = join_by(pares_cand_2)`
        +
        +
        candidatos_tbl
        +
        +
        # A tibble: 949 × 4
        +   pares_cand_1 pares_cand_2 punto_1   punto_2  
        +          <int>        <int> <list>    <list>   
        + 1           15           18 <dbl [2]> <dbl [2]>
        + 2           15           22 <dbl [2]> <dbl [2]>
        + 3           15           35 <dbl [2]> <dbl [2]>
        + 4           15           37 <dbl [2]> <dbl [2]>
        + 5           15           38 <dbl [2]> <dbl [2]>
        + 6           15           60 <dbl [2]> <dbl [2]>
        + 7           15           63 <dbl [2]> <dbl [2]>
        + 8           15           66 <dbl [2]> <dbl [2]>
        + 9           15           82 <dbl [2]> <dbl [2]>
        +10           15          111 <dbl [2]> <dbl [2]>
        +# ℹ 939 more rows
        +
        +
        +
        +
        pares_similares_tbl <- 
        +  candidatos_tbl |> 
        +  mutate(sim = map2_dbl(punto_1, punto_2, sim_e)) |> 
        +  filter(sim > 0.999)
        +
        +
        +
        nrow(pares_similares_tbl)
        +
        +
        [1] 224
        +
        +
        +
        +
        +

        Probando con datos de “gold standard”

        +

        En este caso, sabemos cuáles son los pares que buscamos, así que podemos evaluar nuestro método:

        +
        +
        verdadero_pos <- nrow(inner_join(pares_similares_tbl, pares_sim))
        +
        +
        Joining with `by = join_by(sim)`
        +
        +
        verdadero_pos
        +
        +
        [1] 224
        +
        +
        +
        +
        sensibilidad <- verdadero_pos / nrow(pares_sim)
        +sensibilidad
        +
        +
        [1] 0.9824561
        +
        +
        precision <- verdadero_pos / nrow(pares_similares_tbl)
        +precision
        +
        +
        [1] 1
        +
        +
        +

        Como vemos, la precisión es 1 y la sensibilidad es alta. Nos faltó encontrar una pequeña parte de los pares similares.

        +
          +
        • Si queremos ser más exhaustivos (con el mayor cómputo que implica), podemos utilizar menos hashes, pero el tiempo de cómputo aumenta.
        • +
        +
        +
        +
        + +
        +
        +Hashing y similitud +
        +
        +
        +

        Este método de hashing se llama más generalmente Locality Sensitive Hashing, y se utiliza para encontrar de manera eficiente pares de puntos similares en embeddings.

        +

        En lugar de sólo construir cubetas, también es posible almacenar la salida en estructura de árbol para hacer más eficientes las búsquedas.

        +
        +
        +
        +
        +
        +

        16.5 Retroalimentación implícita

        Esta sección está basada en (Hu, Koren, y Volinsky 2008).

        En el ejemplo que vimos arriba, la retroalimentación es expícita en el sentido de que los usuarios califican los artículos (\(1-\) no me gustó nada, hasta \(5-\) me gustó mucho). Sin embargo, es común encontrar casos donde no existe esta retroalimentación explícita, y solo tenemos medidas del gusto implícito, por ejemplo:

          @@ -557,16 +894,16 @@

          \(r_{ij}\) no tiene faltantes, o tiene un valor implícito para faltantes. Por ejemplo, si la medida es % de la película que vi, todas las películas con las que no he interactuado tienen \(r_{ij}=0\).

        Así que en nuestro modelo no necesariamente queremos predecir directamente la variable \(r_{ij}\): puede haber artículos con predicción baja de \(r_{ij}\) que descubramos de todas formas van a ser altamente preferidos. Un modelo que haga una predicción de \(r_{îj}\) reflejaría más los patrones de consumo actual en lugar de permitirnos descubrir artículos preferidos con los que no necesariamente existe interacción.

        -
        -

        16.4.1 Ejemplo

        +
        +

        16.5.1 Ejemplo

        Consideremos los siguientes usuarios, donde medimos por ejemplo el número de minutos que pasó cada usuario viendo cada película:

        -
        imp <- tibble(usuario = 1:6,
        -              StarWars1 = c(0, 0, 0, 150, 300, 250),
        -              StarWars2 = c(250,  200, 0, 200, 220,180), 
        -              StarWars3 = c(0, 250, 300, 0, 0, 0),
        -              Psycho = c(5, 1, 0, 0, 0, 2)) 
        -imp
        +
        imp <- tibble(usuario = 1:6,
        +              StarWars1 = c(0, 0, 0, 150, 300, 250),
        +              StarWars2 = c(250,  200, 0, 200, 220,180), 
        +              StarWars3 = c(0, 250, 300, 0, 0, 0),
        +              Psycho = c(5, 1, 0, 0, 0, 2)) 
        +imp
        # A tibble: 6 × 5
           usuario StarWars1 StarWars2 StarWars3 Psycho
        @@ -600,33 +937,33 @@ 

        Esto permite que en el ajuste podamos descubrir artículos con \(p_{ij}\) alta para algún usuario, aún cuando \(r_{ij}\) es cero o muy chico.

      -
      -

      16.4.2 Ejemplo

      +
      +

      16.5.2 Ejemplo

      Veamos cómo se ven soluciones de un factor

      -
      imp_mat <- imp |> select(-usuario) |> as.matrix()
      -error_explicito <- function(uv){
      -  u <- matrix(uv[1:6], ncol = 1)
      -  v <- matrix(uv[7:10], ncol = 1)
      -  sum((imp_mat - u %*% t(v))^2)
      -}
      -error_implicito <- function(uv){
      -  u <- matrix(uv[1:6], ncol = 1)
      -  v <- matrix(uv[7:10], ncol = 1)
      -  pref_mat <- as.numeric(imp_mat > 0) - u %*% t(v)
      -  confianza <- 1 + 0.1 * imp_mat
      -  sum((confianza * pref_mat)^2 )
      -}
      +
      imp_mat <- imp |> select(-usuario) |> as.matrix()
      +error_explicito <- function(uv){
      +  u <- matrix(uv[1:6], ncol = 1)
      +  v <- matrix(uv[7:10], ncol = 1)
      +  sum((imp_mat - u %*% t(v))^2)
      +}
      +error_implicito <- function(uv){
      +  u <- matrix(uv[1:6], ncol = 1)
      +  v <- matrix(uv[7:10], ncol = 1)
      +  pref_mat <- as.numeric(imp_mat > 0) - u %*% t(v)
      +  confianza <- 1 + 0.1 * imp_mat
      +  sum((confianza * pref_mat)^2 )
      +}

      Si intentamos ajustar los ratings implícitos como si fueran explícitos, obtenemos los siguientes ajustados con un solo factor latente:

      -
      uv_inicial <- runif(10)
      -opt_exp <- optim(par = uv_inicial, error_explicito, method = "BFGS")
      -opt_exp$par[7:10]
      +
      uv_inicial <- runif(10)
      +opt_exp <- optim(par = uv_inicial, error_explicito, method = "BFGS")
      +opt_exp$par[7:10]
      -
      [1] 10.95575245 13.57814959  3.44180907  0.08839186
      +
      [1] 28.6668987 35.5285942  9.0062834  0.2312475
      -
      t(t(opt_exp$par[1:6])) %*% t(opt_exp$par[7:10]) |> round()
      +
      t(t(opt_exp$par[1:6])) %*% t(opt_exp$par[7:10]) |> round()
           [,1] [,2] [,3] [,4]
       [1,]  118  146   37    1
      @@ -640,12 +977,12 @@ 

      Nótese que esta solución no es muy buena: una componente intenta capturar los patrones de consumo de estas cuatro películas.

      Si usamos preferencias y confianza, obtenemos:

      -
      opt_imp <- optim(par = uv_inicial, error_implicito, method = "BFGS")
      -opt_imp$par[7:10]
      +
      opt_imp <- optim(par = uv_inicial, error_implicito, method = "BFGS")
      +opt_imp$par[7:10]
      -
      [1] 0.8208828 0.8221935 0.8186859 0.5101005
      +
      [1] 1.1950285 1.1969381 1.1918477 0.7423818
      -
      t(t(opt_imp$par[1:6])) %*% t(opt_imp$par[7:10]) |> round(2)
      +
      t(t(opt_imp$par[1:6])) %*% t(opt_imp$par[7:10]) |> round(2)
           [,1] [,2] [,3] [,4]
       [1,]    1    1 0.99 0.62
      @@ -660,8 +997,8 @@ 

      Igual que en los ejemplos anteriores, usualmente se agregan términos de regularización para los vectores renglón \(u_i\) y \(v_j\).

      -
      -

      16.4.3 Evaluación para modelos implícitos

      +
      +

      16.5.3 Evaluación para modelos implícitos

      La evaluación para modelos implícitos no es tan simple como en el caso explícito, pues no estamos modelando directamente los valores observados \(r_{ij}\). Medidas como RECM o MAD que usamos en el caso explícito no son tan apropiadas para este problema.

      Una alternativa es, para cada usuario \(i\), ordenar los artículos de mayor a menor valor de \(\hat{p}_{ij} = u_iv_j^t\) (canciones, pellículas), y calculamos:

      \[ @@ -684,19 +1021,145 @@

      -

      17 Representación de palabras y word2vec

      +
      +

      17 Embeddings de imágenes

      +

      Otro tipo de embeddings similar al de los sistemas de recomendación es el de imágenes. En este caso, comenzamos con un clasificador de imágenes construido a partir de una red convolucional profunda, y consideramos las últimas capas que se utilizan para la clasificación. Estas últimas capas contienen información más relacionada con el contenido de la imagen que con patrones de pixeles generales, y puede ser utilizado para encontrar imágenes similares.

      +

      En esta aplicación, resolveremos el problema de encontrar imágenes duplicadas, por ejemplo en pinterest. Existen muchas imágenes con variaciones mínimas (crop, rotaciones, algunos cambios de colores, etc), y en general no queremos repetir muchas veces una imagen en el feed de un usuario.

      +

      Es claro que hacer comparaciones de pixeles no es una estrategia muy buena, porque las transformaciones de arriba puede producir diferencias grandes en los valores de los pixeles, aún cuando el contenido de la imagen es el mismo. Consideramos tres imágenes para probar:

      +
      +
      +

      +
      +
      +

      +
      +
      +

      +
      +
      +

      Como veremos, estas tres imagenes son similares en distancia unas de otras en términos de pixeles (muy diferentes), aún cuando el contenido de las primeras dos es altamente similar.

      +

      En espacios de dimensión muy alta, como en imágenes, conviene hacer reducción de dimensionalidad para definir la métrica de distancia y utilizar estos métodos para encontrar vecinos cercanos.

      +

      Utilizaremos una red convolucional pre-entrenada para extraer características de las imágenes. En este caso, tomaremos la última penúltima capa (antes de la capa de softmax), que nos dará embeddings de tamaño 4096:

      +
      +
      library(keras)
      +
      +
      
      +Attaching package: 'keras'
      +
      +
      +
      The following object is masked from 'package:yardstick':
      +
      +    get_weights
      +
      +
      modelo <- application_vgg16(weights = 'imagenet')
      +# obtener la penúltima
      +embed_modelo <-  keras_model(inputs = modelo$input, 
      +                     outputs = get_layer(modelo, "fc2")$output)
      +embed_modelo
      +
      +
      Model: "model"
      +________________________________________________________________________________
      + Layer (type)                       Output Shape                    Param #     
      +================================================================================
      + input_1 (InputLayer)               [(None, 224, 224, 3)]           0           
      + block1_conv1 (Conv2D)              (None, 224, 224, 64)            1792        
      + block1_conv2 (Conv2D)              (None, 224, 224, 64)            36928       
      + block1_pool (MaxPooling2D)         (None, 112, 112, 64)            0           
      + block2_conv1 (Conv2D)              (None, 112, 112, 128)           73856       
      + block2_conv2 (Conv2D)              (None, 112, 112, 128)           147584      
      + block2_pool (MaxPooling2D)         (None, 56, 56, 128)             0           
      + block3_conv1 (Conv2D)              (None, 56, 56, 256)             295168      
      + block3_conv2 (Conv2D)              (None, 56, 56, 256)             590080      
      + block3_conv3 (Conv2D)              (None, 56, 56, 256)             590080      
      + block3_pool (MaxPooling2D)         (None, 28, 28, 256)             0           
      + block4_conv1 (Conv2D)              (None, 28, 28, 512)             1180160     
      + block4_conv2 (Conv2D)              (None, 28, 28, 512)             2359808     
      + block4_conv3 (Conv2D)              (None, 28, 28, 512)             2359808     
      + block4_pool (MaxPooling2D)         (None, 14, 14, 512)             0           
      + block5_conv1 (Conv2D)              (None, 14, 14, 512)             2359808     
      + block5_conv2 (Conv2D)              (None, 14, 14, 512)             2359808     
      + block5_conv3 (Conv2D)              (None, 14, 14, 512)             2359808     
      + block5_pool (MaxPooling2D)         (None, 7, 7, 512)               0           
      + flatten (Flatten)                  (None, 25088)                   0           
      + fc1 (Dense)                        (None, 4096)                    102764544   
      + fc2 (Dense)                        (None, 4096)                    16781312    
      +================================================================================
      +Total params: 134,260,544
      +Trainable params: 134,260,544
      +Non-trainable params: 0
      +________________________________________________________________________________
      +
      +
      +
      +
      obtener_pixeles <- function(imagen_ruta){
      +  img <- image_load(imagen_ruta, target_size = c(224,224))
      +  x <- image_to_array(img)
      +  array_reshape(x, c(1, dim(x))) 
      +}
      +calcular_capa <- function(imagen_ruta){
      +  x <- obtener_pixeles(imagen_ruta) |> imagenet_preprocess_input()
      +  embed_modelo |>  predict(x) |> as.numeric()
      +}
      +pixeles_1 <- obtener_pixeles("./figuras/elefante_1.jpg") |> 
      +  as.numeric()
      +pixeles_2 <- obtener_pixeles("./figuras/elefante_3.jpg") |> 
      +  as.numeric()
      +pixeles_3 <- obtener_pixeles("./figuras/leon_1.jpg") |> 
      +  as.numeric()
      +
      +

      Calculamos la distancia pixel a pixel:

      +
      +
      mean((pixeles_2 - pixeles_1)^2)
      +
      +
      [1] 7040.36
      +
      +
      mean((pixeles_1 - pixeles_3)^2)
      +
      +
      [1] 7060.643
      +
      +
      +

      Calculamos la penúltima capa de nuestro modelo para las imágenes de prueba:

      +
      +
      features_1 <- calcular_capa("./figuras/elefante_1.jpg")
      +features_2 <- calcular_capa("./figuras/elefante_3.jpg")
      +features_3 <- calcular_capa("./figuras/leon_1.jpg")
      +length(features_1)
      +
      +
      [1] 4096
      +
      +
      +

      Nótese ahora que la distancia en nuestro nuevo espacio de imágenes es mucho más chica para los elefantes que entre el león y los elefantes:

      +
      +
      mean((features_2 - features_1)^2)
      +
      +
      [1] 0.8760022
      +
      +
      mean((features_1 - features_3)^2)
      +
      +
      [1] 3.17355
      +
      +
      +

      Podemos usar entonces el siguiente proceso:

      +
        +
      1. Calculamos para cada imagen la representación dada por la última capa de una red nueronal de clasificación para imagen (Embedding de la imagen)
      2. +
      3. Definimos como nuestra medida de distancia entre imagenes (por ejemplo similitud coseno)
      4. +
      5. Definimos funciones hash con proyecciones en cubetas como vimos arriba.
      6. +
      7. Con estos hashes, podemos encontrar imagenes duplicadas o muy similares.
      8. +
      +
      +
      +

      18 Representación de palabras y word2vec

      En esta parte empezamos a ver los enfoques más modernos (redes neuronales) para construir modelos de lenguajes y resolver tareas de NLP. Se trata de modelos de lenguaje que incluyen más estructura, son más fáciles de regularizar y de ampliar si es necesario para incluir dependencias de mayor distancia. El método de conteo/suavizamiento de ngramas es simple y funciona bien para algunas tareas, pero podemos construir mejores modelos con enfoques más estructurados, y con más capacidad para aprender aspectos más complejos del lenguaje natural.

      Si \(w=w_1w_2\cdots w_N\) es una frase, y las \(w\) representan palabras, recordemos que un modelo de lenguaje con dependencia de \(n\)-gramas consiste de las probabilidades

      \[P(w_t | w_{t-1} w_{t-2} \cdots w_{t-n+1}),\]

      (n=2, bigramas, n=3 trigramas, etc.)

      -

      Un enfoque donde intentamos estimar directamente estas probabilidades de los datos observados (modelos de n-gramas) puede ser útil en algunos casos (por ejemplo autocorrección simple), pero en general la mayoría de las sucesiones del lenguaje no son observadas en ningún corpues, y es necesario considerar un un enfoque más estructurado pensando en representaciones “distribucionales” de palabras:

      +

      Un enfoque donde intentamos estimar directamente estas probabilidades de los datos observados (modelos de n-gramas) puede ser útil en algunos casos (por ejemplo autocorrección simple), pero en general la mayoría de las sucesiones del lenguaje no son observadas en ningún corpus, y es necesario considerar un un enfoque más estructurado pensando en representaciones “distribucionales” de palabras:

      1. Asociamos a cada palabra en el vocabulario un vector numérico con \(d\) dimensiones, que es su representación distribuida.
      2. Expresamos la función de probabilidad como combinaciones de las representaciones vectoriales del primer paso.
      3. Aprendemos (máxima verosimiltud posiblemente regularización) simultáneamente los vectores y la manera de combinar estos vectores para producir probabilidades.
      -

      La idea de este modelo es entonces subsanar la relativa escasez de datos (comparado con todos los trigramas que pueden existir) con estructura. Sabemos que esta es una buena estrategia si la estrucutura impuesta es apropiada.

      +

      La idea de este modelo es entonces subsanar la relativa escasez de datos (comparado con todos los trigramas que pueden existir) con estructura. Sabemos que esta es una buena estrategia si la estructura impuesta es apropiada.

      @@ -711,12 +1174,12 @@

      17 Representaci

      El objeto es entonces abstraer características de palabras (mediante estas representaciones) intentando no perder mucho de su sentido original, lo que nos permite conocer palabras por su contexto, aún cuando no las hayamos observado antes.

      -
      -

      Ejemplo

      +
      +

      Ejemplo

      ¿Cómo puede funcionar este enfoque? Por ejemplo, si vemos la frase “El gato corre en el jardín”, sabemos que una frase probable debe ser también “El perro corre en el jardín”, pero quizá nunca vimos en el corpus la sucesión “El perro corre”. La idea es que como “perro” y “gato” son funcionalmente similares (aparecen en contextos similares en otros tipos de oraciones como el perro come, el gato come, el perro duerme, este es mi gato, etc.), un modelo como el de arriba daría vectores similares a “perro” y “gato”, pues aparecen en contextos similares. Entonces el modelo daría una probabilidad alta a “El perro corre en el jardín”.

      -
      -

      17.1 Modelo de red neuronal

      +
      +

      18.1 Modelo de red neuronal

      Podemos entonces construir una red neuronal con 2 capas ocultas como sigue (segimos (Bengio et al. 2003), una de las primeras referencias en usar este enfoque). Notamos que los enfoques más efectivos actualmente, con conjuntos de datos más grandes, se utilizan arquitecturas más refinadas que permiten modelación de dependencias de mayor distancia, comenzando con la idea de atención.

      En este ejemplo usaremos el ejemplo de trigramas:

        @@ -740,19 +1203,19 @@

      Esta idea original ha sido explotada con éxito, pero en arquitecturas más modernas (mediante el mecanismo de atención). Nótese que el número de parámetros es del orden de \(|V|(nm+h)\), donde \(|V|\) es el tamaño del vocabulario (decenas o cientos de miles), \(n\) es 3 o 4 (trigramas, 4-gramas), \(m\) es el tamaño de la representacion (cientos) y \(h\) es el número de nodos en la segunda capa (también cientos o miles). Esto resulta en el mejor de los casos en modelos con miles de millones de parámetros. Adicionalmente, hay algunos cálculos costosos, como el softmax (donde hay que hacer una suma sobre el vocabulario completo). En el paper original se propone descenso estocástico.

      -
      -

      Ejemplo

      +
      +

      Ejemplo

      Veamos un ejemplo chico de cómo se vería el paso feed-forward de esta red. Supondremos en este ejemplo que los sesgos \(a,b\) son iguales a cero para simplificar los cálculos.

      Consideremos que el texto de entrenamiento es “El perro corre. El gato corre. El león corre. El león ruge.”

      En este caso, nuestro vocabulario consiste de los 8 tokens \(<s>\), el, perro, gato, león, corre, caza \(</s>\). Consideremos un modelo con \(d=2\) (representaciones de palabras en 2 dimensiones), y consideramos un modelo de trigramas.

      Nuestra primera capa es una matriz \(C\) de tamaño \(2\times 8\), es decir, un vector de tamaño 2 para cada palabra. Por ejemplo, podríamos tener

      -
      library(tidyverse)
      -set.seed(63)
      -C <- round(matrix(rnorm(16, 0, 0.1), 2, 8), 2)
      -colnames(C) <- c("_s_", "el", "perro", "gato", "león", "corre", "caza", "_ss_")
      -rownames(C) <- c("d_1", "d_2")
      -C
      +
      library(tidyverse)
      +set.seed(63)
      +C <- round(matrix(rnorm(16, 0, 0.1), 2, 8), 2)
      +colnames(C) <- c("_s_", "el", "perro", "gato", "león", "corre", "caza", "_ss_")
      +rownames(C) <- c("d_1", "d_2")
      +C
            _s_    el perro gato  león corre caza  _ss_
       d_1  0.13  0.05  0.05 0.04 -0.17  0.04 0.03 -0.02
      @@ -761,8 +1224,8 @@ 

      Ejemplo

      En la siguiente capa consideremos que usaremos, arbitrariamente, \(h=3\) unidades. Como estamos considerando bigramas, necesitamos una entrada de tamaño 4 (representación de un bigrama, que son dos vectores de la matriz \(C\), para predecir la siguiente palabra).

      -
      H <- round(matrix(rnorm(12, 0, 0.1), 3, 4), 2)
      -H
      +
      H <- round(matrix(rnorm(12, 0, 0.1), 3, 4), 2)
      +H
            [,1]  [,2]  [,3]  [,4]
       [1,] -0.04  0.12 -0.09  0.18
      @@ -772,9 +1235,9 @@ 

      Ejemplo

      Y la última capa es la del vocabulario. Son entonces 8 unidades, con 3 entradas cada una. La matriz de pesos es:

      -
      U <- round(matrix(rnorm(24, 0, 0.1), 8, 3), 2)
      -rownames(U) <- c("_s_", "el", "perro", "gato", "león", "corre", "caza", "_ss_")
      -U
      +
      U <- round(matrix(rnorm(24, 0, 0.1), 8, 3), 2)
      +rownames(U) <- c("_s_", "el", "perro", "gato", "león", "corre", "caza", "_ss_")
      +U
             [,1]  [,2]  [,3]
       _s_    0.05 -0.15 -0.30
      @@ -789,8 +1252,8 @@ 

      Ejemplo

      Ahora consideremos cómo se calcula el objetivo con los datos de entrenamiento. El primer trigrama es (_s_, el). La primera capa entonces devuelve los dos vectores correspondientes a cada palabra (concatenado):

      -
      capa_1 <- c(C[, "_s_"], C[, "el"])
      -capa_1
      +
      capa_1 <- c(C[, "_s_"], C[, "el"])
      +capa_1
        d_1   d_2   d_1   d_2 
        0.13 -0.19  0.05 -0.19 
      @@ -798,9 +1261,9 @@

      Ejemplo

      La siguiente capa es:

      -
      sigma <- function(z){ 1 / (1 + exp(-z))}
      -capa_2 <- sigma(H %*% capa_1)
      -capa_2
      +
      sigma <- function(z){ 1 / (1 + exp(-z))}
      +capa_2 <- sigma(H %*% capa_1)
      +capa_2
                [,1]
       [1,] 0.4833312
      @@ -810,8 +1273,8 @@ 

      Ejemplo

      Y la capa final da

      -
      y <- U %*% capa_2
      -y
      +
      y <- U %*% capa_2
      +y
                    [,1]
       _s_   -0.203806461
      @@ -826,8 +1289,8 @@ 

      Ejemplo

      Y aplicamos softmax para encontrar las probabilidades

      -
      p <- exp(y)/sum(exp(y)) |> as.numeric()
      -p
      +
      p <- exp(y)/sum(exp(y)) |> as.numeric()
      +p
                  [,1]
       _s_   0.09931122
      @@ -842,8 +1305,8 @@ 

      Ejemplo

      Y la probabilidad es entonces

      -
      p_1 <- p["perro", 1]
      -p_1
      +
      p_1 <- p["perro", 1]
      +p_1
          perro 
       0.1226738 
      @@ -851,7 +1314,7 @@

      Ejemplo

      Cuya log probabilidad es

      -
      log(p_1)
      +
      log(p_1)
          perro 
       -2.098227 
      @@ -859,22 +1322,22 @@

      Ejemplo

      Ahora seguimos con el siguiente trigrama, que es “(perro, corre)”. Necesitamos calcular la probabilidad de corre dado el contexto “el perro”. Repetimos nuestro cálculo:

      -
      capa_1 <- c(C[, "el"], C[, "perro"])
      -capa_1
      +
      capa_1 <- c(C[, "el"], C[, "perro"])
      +capa_1
        d_1   d_2   d_1   d_2 
        0.05 -0.19  0.05 -0.11 
      -
      capa_2 <- sigma(H %*% capa_1)
      -capa_2
      +
      capa_2 <- sigma(H %*% capa_1)
      +capa_2
                [,1]
       [1,] 0.4877275
       [2,] 0.4949252
       [3,] 0.5077494
      -
      y <- U %*% capa_2
      -y
      +
      y <- U %*% capa_2
      +y
                    [,1]
       _s_   -0.202177217
      @@ -886,8 +1349,8 @@ 

      Ejemplo

      caza 0.079237709 _ss_ -0.010274101
      -
      p <- exp(y)/sum(exp(y)) |> as.numeric()
      -p
      +
      p <- exp(y)/sum(exp(y)) |> as.numeric()
      +p
                  [,1]
       _s_   0.09947434
      @@ -902,8 +1365,8 @@ 

      Ejemplo

      Y la probabilidad es entonces

      -
      p_2 <- p["corre", 1]
      -log(p_2)
      +
      p_2 <- p["corre", 1]
      +log(p_2)
          corre 
       -2.120711 
      @@ -911,7 +1374,7 @@

      Ejemplo

      Sumando, la log probabilidad es:

      -
      log(p_1) + log(p_2)
      +
      log(p_1) + log(p_2)
          perro 
       -4.218937 
      @@ -919,29 +1382,29 @@

      Ejemplo

      y continuamos con los siguientes trigramas del texto de entrenamiento. Creamos una función

      -
      feed_fow_p <- function(trigrama, C, H, U){
      -  trigrama <- strsplit(trigrama, " ", fixed = TRUE)[[1]]
      -  capa_1 <- c(C[, trigrama[1]], C[, trigrama[2]])
      -  capa_2 <- sigma(H %*% capa_1)
      -  y <- U %*% capa_2
      -  p <- exp(y)/sum(exp(y)) |> as.numeric()
      -  p
      -}
      -
      -feed_fow_dev <- function(trigrama, C, H, U) {
      -  p <- feed_fow_p(trigrama, C, H, U)
      -  trigrama_s <- strsplit(trigrama, " ", fixed = TRUE)[[1]]
      -  log(p)[trigrama_s[3], 1]
      -}
      +
      feed_fow_p <- function(trigrama, C, H, U){
      +  trigrama <- strsplit(trigrama, " ", fixed = TRUE)[[1]]
      +  capa_1 <- c(C[, trigrama[1]], C[, trigrama[2]])
      +  capa_2 <- sigma(H %*% capa_1)
      +  y <- U %*% capa_2
      +  p <- exp(y)/sum(exp(y)) |> as.numeric()
      +  p
      +}
      +
      +feed_fow_dev <- function(trigrama, C, H, U) {
      +  p <- feed_fow_p(trigrama, C, H, U)
      +  trigrama_s <- strsplit(trigrama, " ", fixed = TRUE)[[1]]
      +  log(p)[trigrama_s[3], 1]
      +}

      Y ahora aplicamos a todos los textos:

      -
      texto_entrena <- c("_s_ el perro corre _ss_", " _s_ el gato corre _ss_", " _s_ el león corre _ss_",
      -  "_s_ el león caza _ss_",  "_s_ el gato caza _ss_")
      -entrena_trigramas <- map(texto_entrena, 
      -  ~tokenizers::tokenize_ngrams(.x, n = 3)[[1]]) |> 
      -  flatten() |> unlist()
      -entrena_trigramas
      +
      texto_entrena <- c("_s_ el perro corre _ss_", " _s_ el gato corre _ss_", " _s_ el león corre _ss_",
      +  "_s_ el león caza _ss_",  "_s_ el gato caza _ss_")
      +entrena_trigramas <- map(texto_entrena, 
      +  ~tokenizers::tokenize_ngrams(.x, n = 3)[[1]]) |> 
      +  flatten() |> unlist()
      +entrena_trigramas
       [1] "_s_ el perro"     "el perro corre"   "perro corre _ss_" "_s_ el gato"     
        [5] "el gato corre"    "gato corre _ss_"  "_s_ el león"      "el león corre"   
      @@ -950,19 +1413,19 @@ 

      Ejemplo

      -
      log_p <- sapply(entrena_trigramas, function(x) feed_fow_dev(x, C, H, U))
      -sum(log_p)
      +
      log_p <- sapply(entrena_trigramas, function(x) feed_fow_dev(x, C, H, U))
      +sum(log_p)
      [1] -31.21475

      Ahora piensa como harías más grande esta verosimilitud. Observa que “perro”, “gato” y “león”” están comunmente seguidos de “corre”. Esto implica que nos convendría que hubiera cierta similitud entre los vectores de estas tres palabras, por ejemplo:

      -
      C_1 <- C
      -indices <- colnames(C) %in%  c("perro", "gato", "león")
      -C_1[1, indices] <- 3.0
      -C_1[1, !indices] <- -1.0
      -C_1
      +
      C_1 <- C
      +indices <- colnames(C) %in%  c("perro", "gato", "león")
      +C_1[1, indices] <- 3.0
      +C_1[1, !indices] <- -1.0
      +C_1
            _s_    el perro gato león corre  caza  _ss_
       d_1 -1.00 -1.00  3.00 3.00 3.00 -1.00 -1.00 -1.00
      @@ -971,9 +1434,9 @@ 

      Ejemplo

      La siguiente capa queremos que extraiga el concepto “animal” en la palabra anterior, o algo similar, así que podríamos poner en la unidad 1:

      -
      H_1 <- H
      -H_1[1, ] <- c(0, 0, 5, 0)
      -H_1
      +
      H_1 <- H
      +H_1[1, ] <- c(0, 0, 5, 0)
      +H_1
           [,1]  [,2]  [,3]  [,4]
       [1,] 0.00  0.00  5.00  0.00
      @@ -983,10 +1446,10 @@ 

      Ejemplo

      Nótese que la unidad 1 de la segunda capa se activa cuando la primera componente de la palabra anterior es alta. En la última capa, podríamos entonces poner

      -
      U_1 <- U
      -U_1["corre", ] <- c(4.0, -2, -2)
      -U_1["caza", ] <- c(4.2, -2, -2)
      -U_1
      +
      U_1 <- U
      +U_1["corre", ] <- c(4.0, -2, -2)
      +U_1["caza", ] <- c(4.2, -2, -2)
      +U_1
             [,1]  [,2]  [,3]
       _s_    0.05 -0.15 -0.30
      @@ -1001,15 +1464,15 @@ 

      Ejemplo

      que captura cuando la primera unidad se activa. Ahora el cálculo completo es:

      -
      log_p <- sapply(entrena_trigramas, function(x) feed_fow_dev(x, C_1, H_1, U_1))
      -sum(log_p)
      +
      log_p <- sapply(entrena_trigramas, function(x) feed_fow_dev(x, C_1, H_1, U_1))
      +sum(log_p)
      [1] -23.53883

      Y logramos aumentar la verosimilitud considerablemente. Compara las probabilidades:

      -
      feed_fow_p("el perro", C, H, U)
      +
      feed_fow_p("el perro", C, H, U)
                  [,1]
       _s_   0.09947434
      @@ -1021,7 +1484,7 @@ 

      Ejemplo

      caza 0.13180383 _ss_ 0.12051845
      -
      feed_fow_p("el perro", C_1, H_1, U_1)
      +
      feed_fow_p("el perro", C_1, H_1, U_1)
                  [,1]
       _s_   0.03493901
      @@ -1033,7 +1496,7 @@ 

      Ejemplo

      caza 0.41087194 _ss_ 0.04179531
      -
      feed_fow_p("el gato", C, H, U)
      +
      feed_fow_p("el gato", C, H, U)
                  [,1]
       _s_   0.09957218
      @@ -1045,7 +1508,7 @@ 

      Ejemplo

      caza 0.13183822 _ss_ 0.12052489
      -
      feed_fow_p("el gato", C_1, H_1, U_1)
      +
      feed_fow_p("el gato", C_1, H_1, U_1)
                  [,1]
       _s_   0.03489252
      @@ -1062,15 +1525,15 @@ 

      Ejemplo

      Abajo exploramos una parte fundamental de estos modelos: representaciones de palabras, y modelos relativamente simples para obtener estas representaciones.

      -
      -

      17.2 Representación de palabras

      +
      +

      18.2 Representación de palabras y similitud

      Un aspecto interesante de el modelo de arriba es que nos da una representación vectorial de las palabras, en la forma de los parámetros ajustados de la matriz \(C\). Esta se puede entender como una descripción numérica de cómo funciona una palabra en el contexto de su n-grama.

      Por ejemplo, deberíamos encontrar que palabras como “perro” y “gato” tienen representaciones similares. La razón es que cuando aparecen, las probabilidades sobre las palabras siguientes deberían ser similares, pues estas son dos palabras que se pueden usar en muchos contextos compartidos.

      También podríamos encontrar que palabras como perro, gato, águila, león, etc. tienen partes o entradas similares en sus vectores de representación, que es la parte que hace que funcionen como “animal mamífero” dentro de frases.

      Veremos que hay más razones por las que es interesante esta representación.

      -
      -

      17.3 Modelos de word2vec

      +
      +

      18.3 Modelos de word2vec

      En estos ejemplos veremos cómo producir embeddings de palabras que son precursores de embeddings más refinados como los producidos por Modelos grandes de lenguajes (LLMs). Ver por ejemplo aquí.

      Si lo que principalmente nos interesa es obtener una representación vectorial de palabras, es posible simplificar considerablemente el modelo de arriba o LLMs para poder entrenarlos mucho más rápido, y obtener una representación que en muchas tareas se desempeña bien ((Mikolov et al. 2013)).

      Hay dos ideas básicas que se pueden usar para reducir la complejidad del entrenamiento (ver más en (Mikolov et al. 2013)):

      @@ -1124,17 +1587,17 @@

      Muestreo nega

      \[E = -\log\sigma(y_{w_a}) + \sum_{j=1}^k \log\sigma(y_j),\] en donde queremos maximizar la probabilidad de que ocurra \(w_a\) vs. la probabilidad de que ocurra alguna de las \(v_j\). Es decir, solo buscamos optimizar parámetros para separar lo mejor que podamos la observación de \(k\) observaciones falsas, lo cual implica que tenemos que mover un número relativamente chico de parámetros (en lugar de todos los parámetros de todas las palabras del vocabulario).

      Las palabras “falsas” se escogen según una probabilidad ajustada de unigramas (se observó empíricamente mejor desempeño cuando escogemos cada palabra con probabilidad proporcional a \(P(w)^{3/4}\), en lugar de \(P(w)\), ver (Mikolov et al. 2013)).

      -
      -

      Ejemplo

      +
      +

      Ejemplo

      -
      if(!require(wordVectors)){
      -  devtools::install_github("bmschmidt/wordVectors", 
      -                           dependencies = TRUE)
      -}
      +
      if(!require(wordVectors)){
      +  devtools::install_github("bmschmidt/wordVectors", 
      +                           dependencies = TRUE)
      +}
      
       ── R CMD build ─────────────────────────────────────────────────────────────────
      -* checking for file ‘/tmp/RtmpxKVLkt/remotes8fbc2192c597/bmschmidt-wordVectors-ad127c1/DESCRIPTION’ ... OK
      +* checking for file ‘/tmp/RtmpPIgqH8/remotes8fbba8c9463/bmschmidt-wordVectors-ad127c1/DESCRIPTION’ ... OK
       * preparing ‘wordVectors’:
       * checking DESCRIPTION meta-information ... OK
       * cleaning src
      @@ -1142,53 +1605,53 @@ 

      Ejemplo

      * checking for empty or unneeded directories * building ‘wordVectors_2.0.tar.gz’
      -
      library(wordVectors)
      -
      -
      -
      library(tidyverse)
      -ruta <- "../datos/noticias/ES_Newspapers.txt"
      -if(!file.exists(ruta)){
      -    periodico <- 
      -      read_lines(file= "https://es-noticias.s3.amazonaws.com/Es_Newspapers.txt",
      -                        progress = FALSE)
      -    write_lines(periodico, ruta)
      -} else {
      -    periodico <- read_lines(file= ruta,
      -                        progress = FALSE)
      -}
      -normalizar <- function(texto, vocab = NULL){
      -  # minúsculas
      -  texto <- tolower(texto)
      -  # varios ajustes
      -  texto <- gsub("\\s+", " ", texto)
      -  texto <- gsub("\\.[^0-9]", " _punto_ ", texto)
      -  texto <- gsub(" _s_ $", "", texto)
      -  texto <- gsub("\\.", " _punto_ ", texto)
      -  texto <- gsub("[«»¡!¿?-]", "", texto) 
      -  texto <- gsub(";", " _punto_coma_ ", texto) 
      -  texto <- gsub("\\:", " _dos_puntos_ ", texto) 
      -  texto <- gsub("\\,[^0-9]", " _coma_ ", texto)
      -  texto <- gsub("\\s+", " ", texto)
      -  texto
      -}
      -periodico_df <- tibble(txt = periodico) |>
      -                mutate(id = row_number()) |>
      -                mutate(txt = normalizar(txt))
      +
      library(wordVectors)
      +
      +
      +
      library(tidyverse)
      +ruta <- "../datos/noticias/ES_Newspapers.txt"
      +if(!file.exists(ruta)){
      +    periodico <- 
      +      read_lines(file= "https://es-noticias.s3.amazonaws.com/Es_Newspapers.txt",
      +                        progress = FALSE)
      +    write_lines(periodico, ruta)
      +} else {
      +    periodico <- read_lines(file= ruta,
      +                        progress = FALSE)
      +}
      +normalizar <- function(texto, vocab = NULL){
      +  # minúsculas
      +  texto <- tolower(texto)
      +  # varios ajustes
      +  texto <- gsub("\\s+", " ", texto)
      +  texto <- gsub("\\.[^0-9]", " _punto_ ", texto)
      +  texto <- gsub(" _s_ $", "", texto)
      +  texto <- gsub("\\.", " _punto_ ", texto)
      +  texto <- gsub("[«»¡!¿?-]", "", texto) 
      +  texto <- gsub(";", " _punto_coma_ ", texto) 
      +  texto <- gsub("\\:", " _dos_puntos_ ", texto) 
      +  texto <- gsub("\\,[^0-9]", " _coma_ ", texto)
      +  texto <- gsub("\\s+", " ", texto)
      +  texto
      +}
      +periodico_df <- tibble(txt = periodico) |>
      +                mutate(id = row_number()) |>
      +                mutate(txt = normalizar(txt))

      Construimos un modelo con vectores de palabras de tamaño 100, skip-grams de tamaño 4, y ajustamos con muestreo negativo de tamaño 20:

      -
      if(!file.exists('./cache/noticias_w2v.txt')){
      -  tmp <- tempfile()
      -  # tokenización
      -  write_lines(periodico_df$txt,  tmp)
      -  prep <- prep_word2vec(tmp, 
      -          destination = './cache/noticias_w2v.txt', bundle_ngrams = 2)
      -  } 
      +
      if(!file.exists('./cache/noticias_w2v.txt')){
      +  tmp <- tempfile()
      +  # tokenización
      +  write_lines(periodico_df$txt,  tmp)
      +  prep <- prep_word2vec(tmp, 
      +          destination = './cache/noticias_w2v.txt', bundle_ngrams = 2)
      +  } 
      Beginning tokenization to text file at ./cache/noticias_w2v.txt
      -
      Prepping /tmp/RtmpxKVLkt/file8fbc9992291
      +
      Prepping /tmp/RtmpPIgqH8/file8fbb3f22963d
      Starting training using file ./cache/noticias_w2v.txt
      @@ -1617,19 +2080,19 @@ 

      Ejemplo

      -
      if (!file.exists("./cache/noticias_vectors.bin")) {
      -  modelo <- train_word2vec("./cache/noticias_w2v.txt", 
      -          "./cache/noticias_vectors.bin",
      -          vectors = 100, threads = 8, window = 4, cbow = 0,  
      -          iter = 20, negative_samples = 20, min_count = 10) 
      -} else {
      -  modelo <- read.vectors("./cache/noticias_vectors.bin")
      -}
      +
      if (!file.exists("./cache/noticias_vectors.bin")) {
      +  modelo <- train_word2vec("./cache/noticias_w2v.txt", 
      +          "./cache/noticias_vectors.bin",
      +          vectors = 100, threads = 8, window = 4, cbow = 0,  
      +          iter = 20, negative_samples = 20, min_count = 10) 
      +} else {
      +  modelo <- read.vectors("./cache/noticias_vectors.bin")
      +}

      El resultado son los vectores aprendidos de las palabras, por ejemplo

      -
      vector_gol <- modelo[["gol"]] |> as.numeric()
      -vector_gol
      +
      vector_gol <- modelo[["gol"]] |> as.numeric()
      +vector_gol
        [1] -0.389627248  0.048135430  0.501164794 -0.035961468  0.291238993
         [6]  0.642733335 -0.386596769  0.281559557 -0.199183896 -0.554564893
      @@ -1655,13 +2118,13 @@ 

      Ejemplo

      -
      -

      17.4 Espacio de representación de palabras

      +
      +

      18.4 Espacio de representación de palabras

      Como discutimos arriba, palabras que se usan en contextos similares por su significado o por su función (por ejemplo, “perro” y “gato”“) deben tener representaciones similares, pues su contexto tiende a ser similar. La similitud que usamos el similitud coseno.

      Podemos verificar con nuestro ejemplo:

      -
      ejemplos <- modelo |>  closest_to("gol", n = 5)
      -ejemplos
      +
      ejemplos <- modelo |>  closest_to("gol", n = 5)
      +ejemplos
                   word similarity to "gol"
       1             gol           1.0000000
      @@ -1674,8 +2137,8 @@ 

      -
      vector_penalti <- modelo[["penalti"]] |> as.numeric()
      -cosineSimilarity(modelo[["gol"]], modelo[["penalti"]])
      +
      vector_penalti <- modelo[["penalti"]] |> as.numeric()
      +cosineSimilarity(modelo[["gol"]], modelo[["penalti"]])
                [,1]
       [1,] 0.7764458
      @@ -1683,8 +2146,8 @@

      -
      norma <- function(x) sqrt(sum(x^2))
      -sum(vector_gol * vector_penalti) / (norma(vector_gol) * norma(vector_penalti))
      +
      norma <- function(x) sqrt(sum(x^2))
      +sum(vector_gol * vector_penalti) / (norma(vector_gol) * norma(vector_penalti))
      [1] 0.7764458
      @@ -1700,8 +2163,8 @@

      -
      ejemplos <- modelo |>  closest_to("dos", n = 15)
      -ejemplos
      +
      ejemplos <- modelo |>  closest_to("dos", n = 15)
      +ejemplos
            word similarity to "dos"
       1      dos           1.0000000
      @@ -1722,8 +2185,8 @@ 

      -
      ejemplos <- modelo |>  closest_to(c("lluvioso"), n = 5)
      -ejemplos
      +
      ejemplos <- modelo |>  closest_to(c("lluvioso"), n = 5)
      +ejemplos
            word similarity to c("lluvioso")
       1 lluvioso                   1.0000000
      @@ -1734,8 +2197,8 @@ 

      -
      ejemplos <- modelo |>  closest_to("presidente", n = 5)
      -ejemplos
      +
      ejemplos <- modelo |>  closest_to("presidente", n = 5)
      +ejemplos
                        word similarity to "presidente"
       1           presidente                  1.0000000
      @@ -1746,8 +2209,8 @@ 

      -
      ejemplos <- modelo |>  closest_to("parís", n = 5)
      -ejemplos
      +
      ejemplos <- modelo |>  closest_to("parís", n = 5)
      +ejemplos
              word similarity to "parís"
       1      parís             1.0000000
      @@ -1770,19 +2233,19 @@ 

      -
      plural_1 <- modelo[["goles"]] - modelo[["gol"]]
      -plural_1
      +
      plural_1 <- modelo[["goles"]] - modelo[["gol"]]
      +plural_1
      A VectorSpaceModel object of  1  words and  100  vectors
                  [,1]       [,2]        [,3]       [,4]        [,5]       [,6]
       [1,] -0.2301596 -0.2543171 -0.04071745 -0.2292878 0.004059255 -0.2283908
       attr(,".cache")
      -<environment: 0x55c12c6f72e8>
      +<environment: 0x559f14cdb450>

      que es un vector en el espacio de representación de palabras. Ahora sumamos este vector a un sustantivo en singular, y vemos qué palabras están cercas de esta “palabra sintética”:

      -
      closest_to(modelo, ~ "partido" + "goles" - "gol", n = 5)
      +
      closest_to(modelo, ~ "partido" + "goles" - "gol", n = 5)
                       word similarity to "partido" + "goles" - "gol"
       1            partidos                                 0.7961097
      @@ -1795,7 +2258,7 @@ 

      -
      closest_to(modelo, ~ "mes" + "días" - "día", n = 20) 
      +
      closest_to(modelo, ~ "mes" + "días" - "día", n = 20) 
                     word similarity to "mes" + "días" - "día"
       1        tres_meses                            0.7858109
      @@ -1822,8 +2285,8 @@ 

      -
      fem_1 <- modelo[["presidenta"]] - modelo[["presidente"]]
      -closest_to(modelo, ~ "rey" + "presidenta" - "presidente", n = 5) |> filter(word != "rey")
      +
      fem_1 <- modelo[["presidenta"]] - modelo[["presidente"]]
      +closest_to(modelo, ~ "rey" + "presidenta" - "presidente", n = 5) |> filter(word != "rey")
            word similarity to "rey" + "presidenta" - "presidente"
       1    reina                                         0.7402226
      @@ -1833,7 +2296,7 @@ 

      -
      closest_to(modelo, ~ "tío" + "presidenta" - "presidente", n = 5) |> filter(word != "tío")
      +
      closest_to(modelo, ~ "tío" + "presidenta" - "presidente", n = 5) |> filter(word != "tío")
           word similarity to "tío" + "presidenta" - "presidente"
       1   dueña                                         0.7036596
      diff --git a/16-recom-factorizacion_files/figure-html/unnamed-chunk-9-1.png b/16-recom-factorizacion_files/figure-html/unnamed-chunk-9-1.png
      new file mode 100644
      index 00000000..efe87bd4
      Binary files /dev/null and b/16-recom-factorizacion_files/figure-html/unnamed-chunk-9-1.png differ
      diff --git a/81-apendice-descenso.html b/81-apendice-descenso.html
      index 6ce1b09b..c8cc40ca 100644
      --- a/81-apendice-descenso.html
      +++ b/81-apendice-descenso.html
      @@ -624,9 +624,9 @@ 

      [[1]]
                   [,1]
      -[1,] 0.007331367
      +[1,] 0.007331368
       [2,] 0.016437240
      -[3,] 0.004633876
      +[3,] 0.004633875
       
       [[2]]
       [1] 0.003028649
      @@ -829,11 +829,11 @@

      Ejemplo

      [[1]]
                    [,1]
       [1,] 0.0079817399
      -[2,] 0.0019486141
      +[2,] 0.0019486138
       [3,] 0.0005532746
       
       [[2]]
      -[1] 0.0003491881
      +[1] 0.0003491883

      coef(lm.fit(cbind(1, x_ent), y_ent))
      diff --git a/81-apendice-descenso_files/figure-html/unnamed-chunk-19-1.png b/81-apendice-descenso_files/figure-html/unnamed-chunk-19-1.png index 91f35b24..7e5886e0 100644 Binary files a/81-apendice-descenso_files/figure-html/unnamed-chunk-19-1.png and b/81-apendice-descenso_files/figure-html/unnamed-chunk-19-1.png differ diff --git a/82-apendice-descenso-estocastico.html b/82-apendice-descenso-estocastico.html index 14b5d1e9..47b3cae7 100644 --- a/82-apendice-descenso-estocastico.html +++ b/82-apendice-descenso-estocastico.html @@ -811,12 +811,12 @@

      [[1]]
                   [,1]
      -[1,] -2.66908264
      -[2,] -0.17915317
      -[3,]  0.03563109
      +[1,] -2.57393885
      +[2,] -0.08030029
      +[3,]  0.13267761
       
       [[2]]
      -[1] -0.3472215
      +[1] -0.3571931

      Y verificamos que concuerda con la salida de lm:

      diff --git a/82-apendice-descenso-estocastico_files/figure-html/unnamed-chunk-24-1.png b/82-apendice-descenso-estocastico_files/figure-html/unnamed-chunk-24-1.png index db8eb9e5..34e516d5 100644 Binary files a/82-apendice-descenso-estocastico_files/figure-html/unnamed-chunk-24-1.png and b/82-apendice-descenso-estocastico_files/figure-html/unnamed-chunk-24-1.png differ diff --git a/figuras/elefante_1.jpg b/figuras/elefante_1.jpg new file mode 100644 index 00000000..9ce38961 Binary files /dev/null and b/figuras/elefante_1.jpg differ diff --git a/figuras/elefante_3.jpg b/figuras/elefante_3.jpg new file mode 100644 index 00000000..bdd4eb50 Binary files /dev/null and b/figuras/elefante_3.jpg differ diff --git a/figuras/leon_1.jpg b/figuras/leon_1.jpg new file mode 100644 index 00000000..b205bf4e Binary files /dev/null and b/figuras/leon_1.jpg differ diff --git a/search.json b/search.json index 3778faf8..47014d2a 100644 --- a/search.json +++ b/search.json @@ -361,7 +361,7 @@ "href": "08-clasificacion-1.html#ejercicio-datos-de-diabetes", "title": "8  Clasificación y probabilidad", "section": "8.8 Ejercicio: datos de diabetes", - "text": "8.8 Ejercicio: datos de diabetes\nYa están divididos los datos en entrenamiento y prueba\n\ndiabetes_ent <- as_tibble(MASS::Pima.tr)\ndiabetes_pr <- as_tibble(MASS::Pima.te)\ndiabetes_ent |> head() |> gt()\n\n\n\n\n\n \n \n \n npreg\n glu\n bp\n skin\n bmi\n ped\n age\n type\n \n \n \n 5\n86\n68\n28\n30.2\n0.364\n24\nNo\n 7\n195\n70\n33\n25.1\n0.163\n55\nYes\n 5\n77\n82\n41\n35.8\n0.156\n35\nNo\n 0\n165\n76\n43\n47.9\n0.259\n26\nNo\n 0\n107\n60\n25\n26.4\n0.133\n23\nNo\n 5\n97\n76\n27\n35.6\n0.378\n52\nYes\n \n \n \n\n\n\ndiabetes_ent$id <- 1:nrow(diabetes_ent)\ndiabetes_pr$id <- 1:nrow(diabetes_pr)\n\nAunque no es necesario, podemos normalizar:\n\nreceta_diabetes <- recipe(type ~ ., diabetes_ent) |>\n update_role(id, new_role = \"id_variable\") |> \n step_normalize(all_numeric()) \ndiabetes_ent_s <- receta_diabetes |> prep() |> juice() \ndiabetes_pr_s <- receta_diabetes |> prep() |> bake(diabetes_pr)\n\n\nmodelo_lineal <- logistic_reg(mode = \"classification\") |> \n set_engine(\"glm\")\nflujo_diabetes <- workflow() |> \n add_model(modelo_lineal) |> \n add_recipe(receta_diabetes)\nflujo_ajustado <- fit(flujo_diabetes, diabetes_ent)\nsaveRDS(flujo_ajustado, \"cache/flujo_ajustado_diabetes.rds\")\nflujo_ajustado\n\n══ Workflow [trained] ══════════════════════════════════════════════════════════\nPreprocessor: Recipe\nModel: logistic_reg()\n\n── Preprocessor ────────────────────────────────────────────────────────────────\n1 Recipe Step\n\n• step_normalize()\n\n── Model ───────────────────────────────────────────────────────────────────────\n\nCall: stats::glm(formula = ..y ~ ., family = stats::binomial, data = data)\n\nCoefficients:\n(Intercept) npreg glu bp skin bmi \n -0.95583 0.34734 1.01705 -0.05473 -0.02247 0.51263 \n ped age \n 0.55928 0.45201 \n\nDegrees of Freedom: 199 Total (i.e. Null); 192 Residual\nNull Deviance: 256.4 \nResidual Deviance: 178.4 AIC: 194.4\n\n\nAhora calculamos devianza de prueba y error de clasificación:\n\npreds_prueba <- \n predict(flujo_ajustado, diabetes_pr, type= \"prob\") |> \n bind_cols(predict(flujo_ajustado, diabetes_pr)) |> \n bind_cols(diabetes_pr |> select(type))\npreds_prueba\n\n# A tibble: 332 × 4\n .pred_No .pred_Yes .pred_class type \n <dbl> <dbl> <fct> <fct>\n 1 0.232 0.768 Yes Yes \n 2 0.960 0.0403 No No \n 3 0.975 0.0253 No No \n 4 0.959 0.0413 No Yes \n 5 0.204 0.796 Yes Yes \n 6 0.265 0.735 Yes Yes \n 7 0.590 0.410 No Yes \n 8 0.780 0.220 No No \n 9 0.558 0.442 No No \n10 0.798 0.202 No Yes \n# ℹ 322 more rows\n\n\n\nlevels(preds_prueba$type)\n\n[1] \"No\" \"Yes\"\n\n# ponemos event_level si \"positivo\" no es el primer factor\nmetricas <- metric_set(accuracy, mn_log_loss)\nmetricas(preds_prueba, truth = type, .pred_Yes, estimate = .pred_class, \n event_level = \"second\")\n\n# A tibble: 2 × 3\n .metric .estimator .estimate\n <chr> <chr> <dbl>\n1 accuracy binary 0.801\n2 mn_log_loss binary 0.441\n\n\nVamos a repetir usando keras.\n\nlibrary(keras)\nx_ent <- diabetes_ent_s |> select(-type, -id) |> as.matrix()\ny_ent <- diabetes_ent_s$type == \"Yes\"\nx_prueba <- diabetes_pr_s |> select(-type, -id) |> as.matrix()\ny_prueba <- diabetes_pr_s$type == 'Yes'\n# definición de estructura del modelo (regresión logística)\n# es posible hacerlo con workflows como vimos arriba, \n# pero aquí usamos directamente la interfaz de keras en R\nn_entrena <- nrow(x_ent)\nmodelo_diabetes <- keras_model_sequential() |>\n layer_dense(units = 1, #una sola respuesta,\n activation = \"sigmoid\", # combinar variables linealmente y aplicar función logística\n kernel_initializer = initializer_constant(0), #inicializamos coeficientes en 0\n bias_initializer = initializer_constant(0)) #inicializamos ordenada en 0\n# compilar seleccionando cantidad a minimizar, optimizador y métricas\nmodelo_diabetes |> compile(\n loss = \"binary_crossentropy\", # devianza es entropía cruzada\n optimizer = optimizer_sgd(learning_rate = 0.75), # descenso en gradiente\n metrics = list(\"binary_crossentropy\"))\n# Ahora iteramos\n# Primero probamos con un número bajo de iteraciones\nhistoria <- modelo_diabetes |> fit(\n as.matrix(x_ent), # x entradas\n y_ent, # y salida o target\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = 10 # número de iteraciones\n)\nplot(historia)\n\n\n\n\nY ahora podemos correr más iteraciones adicionales:\n\nhistoria <- modelo_diabetes |> fit(\n as.matrix(x_ent), # x entradas\n y_ent, # y salida o target\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = 1000, # número de iteraciones\n verbose = 0\n)\n\nLos errores de entrenamiento y prueba son:\n\nevaluate(modelo_diabetes, x_ent, y_ent)\n\n loss binary_crossentropy \n 0.4459766 0.4459766 \n\n\n\nevaluate(modelo_diabetes, x_prueba, y_prueba)\n\n loss binary_crossentropy \n 0.4406986 0.4406986 \n\n\nVeamos que coeficientes obtuvimos:\n\nget_weights(modelo_diabetes)\n\n[[1]]\n [,1]\n[1,] 0.34734306\n[2,] 1.01705003\n[3,] -0.05472930\n[4,] -0.02247146\n[5,] 0.51263183\n[6,] 0.55927491\n[7,] 0.45200700\n\n[[2]]\n[1] -0.9558302\n\n\nque 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.\n\nflujo_ajustado |> extract_fit_parsnip()\n\nparsnip model object\n\n\nCall: stats::glm(formula = ..y ~ ., family = stats::binomial, data = data)\n\nCoefficients:\n(Intercept) npreg glu bp skin bmi \n -0.95583 0.34734 1.01705 -0.05473 -0.02247 0.51263 \n ped age \n 0.55928 0.45201 \n\nDegrees of Freedom: 199 Total (i.e. Null); 192 Residual\nNull Deviance: 256.4 \nResidual Deviance: 178.4 AIC: 194.4" + "text": "8.8 Ejercicio: datos de diabetes\nYa están divididos los datos en entrenamiento y prueba\n\ndiabetes_ent <- as_tibble(MASS::Pima.tr)\ndiabetes_pr <- as_tibble(MASS::Pima.te)\ndiabetes_ent |> head() |> gt()\n\n\n\n\n\n \n \n \n npreg\n glu\n bp\n skin\n bmi\n ped\n age\n type\n \n \n \n 5\n86\n68\n28\n30.2\n0.364\n24\nNo\n 7\n195\n70\n33\n25.1\n0.163\n55\nYes\n 5\n77\n82\n41\n35.8\n0.156\n35\nNo\n 0\n165\n76\n43\n47.9\n0.259\n26\nNo\n 0\n107\n60\n25\n26.4\n0.133\n23\nNo\n 5\n97\n76\n27\n35.6\n0.378\n52\nYes\n \n \n \n\n\n\ndiabetes_ent$id <- 1:nrow(diabetes_ent)\ndiabetes_pr$id <- 1:nrow(diabetes_pr)\n\nAunque no es necesario, podemos normalizar:\n\nreceta_diabetes <- recipe(type ~ ., diabetes_ent) |>\n update_role(id, new_role = \"id_variable\") |> \n step_normalize(all_numeric()) \ndiabetes_ent_s <- receta_diabetes |> prep() |> juice() \ndiabetes_pr_s <- receta_diabetes |> prep() |> bake(diabetes_pr)\n\n\nmodelo_lineal <- logistic_reg(mode = \"classification\") |> \n set_engine(\"glm\")\nflujo_diabetes <- workflow() |> \n add_model(modelo_lineal) |> \n add_recipe(receta_diabetes)\nflujo_ajustado <- fit(flujo_diabetes, diabetes_ent)\nsaveRDS(flujo_ajustado, \"cache/flujo_ajustado_diabetes.rds\")\nflujo_ajustado\n\n══ Workflow [trained] ══════════════════════════════════════════════════════════\nPreprocessor: Recipe\nModel: logistic_reg()\n\n── Preprocessor ────────────────────────────────────────────────────────────────\n1 Recipe Step\n\n• step_normalize()\n\n── Model ───────────────────────────────────────────────────────────────────────\n\nCall: stats::glm(formula = ..y ~ ., family = stats::binomial, data = data)\n\nCoefficients:\n(Intercept) npreg glu bp skin bmi \n -0.95583 0.34734 1.01705 -0.05473 -0.02247 0.51263 \n ped age \n 0.55928 0.45201 \n\nDegrees of Freedom: 199 Total (i.e. Null); 192 Residual\nNull Deviance: 256.4 \nResidual Deviance: 178.4 AIC: 194.4\n\n\nAhora calculamos devianza de prueba y error de clasificación:\n\npreds_prueba <- \n predict(flujo_ajustado, diabetes_pr, type= \"prob\") |> \n bind_cols(predict(flujo_ajustado, diabetes_pr)) |> \n bind_cols(diabetes_pr |> select(type))\npreds_prueba\n\n# A tibble: 332 × 4\n .pred_No .pred_Yes .pred_class type \n <dbl> <dbl> <fct> <fct>\n 1 0.232 0.768 Yes Yes \n 2 0.960 0.0403 No No \n 3 0.975 0.0253 No No \n 4 0.959 0.0413 No Yes \n 5 0.204 0.796 Yes Yes \n 6 0.265 0.735 Yes Yes \n 7 0.590 0.410 No Yes \n 8 0.780 0.220 No No \n 9 0.558 0.442 No No \n10 0.798 0.202 No Yes \n# ℹ 322 more rows\n\n\n\nlevels(preds_prueba$type)\n\n[1] \"No\" \"Yes\"\n\n# ponemos event_level si \"positivo\" no es el primer factor\nmetricas <- metric_set(accuracy, mn_log_loss)\nmetricas(preds_prueba, truth = type, .pred_Yes, estimate = .pred_class, \n event_level = \"second\")\n\n# A tibble: 2 × 3\n .metric .estimator .estimate\n <chr> <chr> <dbl>\n1 accuracy binary 0.801\n2 mn_log_loss binary 0.441\n\n\nVamos a repetir usando keras.\n\nlibrary(keras)\nx_ent <- diabetes_ent_s |> select(-type, -id) |> as.matrix()\ny_ent <- diabetes_ent_s$type == \"Yes\"\nx_prueba <- diabetes_pr_s |> select(-type, -id) |> as.matrix()\ny_prueba <- diabetes_pr_s$type == 'Yes'\n# definición de estructura del modelo (regresión logística)\n# es posible hacerlo con workflows como vimos arriba, \n# pero aquí usamos directamente la interfaz de keras en R\nn_entrena <- nrow(x_ent)\nmodelo_diabetes <- keras_model_sequential() |>\n layer_dense(units = 1, #una sola respuesta,\n activation = \"sigmoid\", # combinar variables linealmente y aplicar función logística\n kernel_initializer = initializer_constant(0), #inicializamos coeficientes en 0\n bias_initializer = initializer_constant(0)) #inicializamos ordenada en 0\n# compilar seleccionando cantidad a minimizar, optimizador y métricas\nmodelo_diabetes |> compile(\n loss = \"binary_crossentropy\", # devianza es entropía cruzada\n optimizer = optimizer_sgd(learning_rate = 0.75), # descenso en gradiente\n metrics = list(\"binary_crossentropy\"))\n# Ahora iteramos\n# Primero probamos con un número bajo de iteraciones\nhistoria <- modelo_diabetes |> fit(\n as.matrix(x_ent), # x entradas\n y_ent, # y salida o target\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = 10 # número de iteraciones\n)\nplot(historia)\n\n\n\n\nY ahora podemos correr más iteraciones adicionales:\n\nhistoria <- modelo_diabetes |> fit(\n as.matrix(x_ent), # x entradas\n y_ent, # y salida o target\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = 1000, # número de iteraciones\n verbose = 0\n)\n\nLos errores de entrenamiento y prueba son:\n\nevaluate(modelo_diabetes, x_ent, y_ent)\n\n loss binary_crossentropy \n 0.4459766 0.4459766 \n\n\n\nevaluate(modelo_diabetes, x_prueba, y_prueba)\n\n loss binary_crossentropy \n 0.4406986 0.4406986 \n\n\nVeamos que coeficientes obtuvimos:\n\nget_weights(modelo_diabetes)\n\n[[1]]\n [,1]\n[1,] 0.34734306\n[2,] 1.01705003\n[3,] -0.05472932\n[4,] -0.02247145\n[5,] 0.51263183\n[6,] 0.55927497\n[7,] 0.45200703\n\n[[2]]\n[1] -0.9558302\n\n\nque 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.\n\nflujo_ajustado |> extract_fit_parsnip()\n\nparsnip model object\n\n\nCall: stats::glm(formula = ..y ~ ., family = stats::binomial, data = data)\n\nCoefficients:\n(Intercept) npreg glu bp skin bmi \n -0.95583 0.34734 1.01705 -0.05473 -0.02247 0.51263 \n ped age \n 0.55928 0.45201 \n\nDegrees of Freedom: 199 Total (i.e. Null); 192 Residual\nNull Deviance: 256.4 \nResidual Deviance: 178.4 AIC: 194.4" }, { "objectID": "08-clasificacion-1.html#probabilidades-y-pérdida-0-1", @@ -417,7 +417,7 @@ "href": "09-clasificacion-2.html#perfil-de-un-clasificador-binario-y-curvas-roc", "title": "9  Decisiones de clasificación", "section": "9.5 Perfil de un clasificador binario y curvas ROC", - "text": "9.5 Perfil de un clasificador binario y curvas ROC\nEn lugar de examinar cada punto de corte por separado, podemos hacer el análisis de todos los posibles puntos de corte mediante la curva ROC (receiver operating characteristic, de ingeniería).\n\n\n\n\n\n\nTip\n\n\n\nPara un problema de clasificación binaria, dadas estimaciones \\(\\hat{p}_1(x)\\), la curva ROC grafica todos los pares de (1-especificidad, sensibilidad) para cada posible punto de corte \\(\\hat{p}_1(x) > \\alpha\\).\n\n\n\nEjemplo\n\nroc_tbl <- roc_curve(preds_prueba, \n truth = type, .pred_Yes)\nroc_tbl\n\n# A tibble: 321 × 3\n .threshold specificity sensitivity\n <dbl> <dbl> <dbl>\n 1 -Inf 0 1\n 2 0.00305 0 1\n 3 0.00345 0.00448 1\n 4 0.00406 0.00897 1\n 5 0.00437 0.0135 1\n 6 0.00476 0.0224 1\n 7 0.00486 0.0269 1\n 8 0.00504 0.0314 1\n 9 0.00512 0.0359 1\n10 0.00562 0.0404 1\n# ℹ 311 more rows\n\nggplot(roc_tbl, \n aes(x = 1 - specificity, y = sensitivity)) +\n geom_path(aes(colour = .threshold), size = 1.2) +\n geom_abline(colour = \"gray\") + \n coord_equal() +\n xlab(\"Tasa de falsos positivos\") + \n ylab(\"Sensibilidad\")\n\nWarning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.\nℹ Please use `linewidth` instead.\n\n\n\n\n\nEn esta gráfica podemos ver todos los clasificadores posibles basados en las probabilidades de clase. Podemos usar estas curvas como evaluación de nuestros clasificadores, dejando para más tarde la selección del punto de corte, si esto es necesario (por ejemplo, dependiendo de los costos de cada tipo de error).\nTambién podemos definir una medida resumen del desempeño de un clasificador según esta curva:\n\n\n\n\n\n\nTip\n\n\n\nLa medida AUC (area under the curve) para un clasificador es el área bajo la curva generada por los pares sensibilidad-especificidad de la curva ROC.\n\n\n\nroc_auc(preds_prueba, type, .pred_Yes)\n\n# A tibble: 1 × 3\n .metric .estimator .estimate\n <chr> <chr> <dbl>\n1 roc_auc binary 0.761\n\n\nCuanto más cerca de uno es este valor, mejor discriminan las probabilidades.\nTambién es útil para comparar modelos. Consideremos el modelo de los datos de diabetes que incluyen todas las variables:\n\nmod_2 <- logistic_reg() |> \n set_engine(\"keras\") |> \n set_mode(\"classification\") |>\n set_args(epochs = 250, \n optimizer = optimizer_sgd(lr = 0.3),\n batch_size = nrow(diabetes_ent), \n verbose = FALSE) |> \n fit(type ~ ., juice(receta_diabetes))\npreds_prueba_completo <- predict(\n mod_2, prueba_baked, type ='prob') |> \n bind_cols(prueba_baked)\npreds_prueba_2 <- bind_rows(\n preds_prueba |> mutate(modelo = \"IMC y edad\"),\n preds_prueba_completo |> mutate(modelo = \"Todas\")) |> \n group_by(modelo)\n\nY graficamos juntas:\n\nroc_2_tbl <- roc_curve(preds_prueba_2, type, .pred_Yes)\nggplot(roc_2_tbl, aes(x = 1 - specificity, y = sensitivity)) +\n geom_path(aes(colour = modelo), size = 1.2) +\n geom_abline(colour = \"gray\") + \n coord_equal() +\n xlab(\"Tasa de falsos positivos\") + \n ylab(\"Sensibilidad\")\n\n\n\n\nComparación auc:\n\nroc_auc(preds_prueba_2, type, .pred_Yes) \n\n# A tibble: 2 × 4\n modelo .metric .estimator .estimate\n <chr> <chr> <chr> <dbl>\n1 IMC y edad roc_auc binary 0.761\n2 Todas roc_auc binary 0.866\n\n\nEn este ejemplo, vemos que casi no importa qué perfil de especificidad y sensibilidad busquemos: el clasificador que usa todas las variables domina casi siempre al clasificador que sólo utiliza IMC y edad." + "text": "9.5 Perfil de un clasificador binario y curvas ROC\nEn lugar de examinar cada punto de corte por separado, podemos hacer el análisis de todos los posibles puntos de corte mediante la curva ROC (receiver operating characteristic, de ingeniería).\n\n\n\n\n\n\nTip\n\n\n\nPara un problema de clasificación binaria, dadas estimaciones \\(\\hat{p}_1(x)\\), la curva ROC grafica todos los pares de (1-especificidad, sensibilidad) para cada posible punto de corte \\(\\hat{p}_1(x) > \\alpha\\).\n\n\n\nEjemplo\n\nroc_tbl <- roc_curve(preds_prueba, \n truth = type, .pred_Yes)\nroc_tbl\n\n# A tibble: 321 × 3\n .threshold specificity sensitivity\n <dbl> <dbl> <dbl>\n 1 -Inf 0 1\n 2 0.00306 0 1\n 3 0.00347 0.00448 1\n 4 0.00408 0.00897 1\n 5 0.00439 0.0135 1\n 6 0.00478 0.0224 1\n 7 0.00488 0.0269 1\n 8 0.00506 0.0314 1\n 9 0.00514 0.0359 1\n10 0.00564 0.0404 1\n# ℹ 311 more rows\n\nggplot(roc_tbl, \n aes(x = 1 - specificity, y = sensitivity)) +\n geom_path(aes(colour = .threshold), size = 1.2) +\n geom_abline(colour = \"gray\") + \n coord_equal() +\n xlab(\"Tasa de falsos positivos\") + \n ylab(\"Sensibilidad\")\n\nWarning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.\nℹ Please use `linewidth` instead.\n\n\n\n\n\nEn esta gráfica podemos ver todos los clasificadores posibles basados en las probabilidades de clase. Podemos usar estas curvas como evaluación de nuestros clasificadores, dejando para más tarde la selección del punto de corte, si esto es necesario (por ejemplo, dependiendo de los costos de cada tipo de error).\nTambién podemos definir una medida resumen del desempeño de un clasificador según esta curva:\n\n\n\n\n\n\nTip\n\n\n\nLa medida AUC (area under the curve) para un clasificador es el área bajo la curva generada por los pares sensibilidad-especificidad de la curva ROC.\n\n\n\nroc_auc(preds_prueba, type, .pred_Yes)\n\n# A tibble: 1 × 3\n .metric .estimator .estimate\n <chr> <chr> <dbl>\n1 roc_auc binary 0.761\n\n\nCuanto más cerca de uno es este valor, mejor discriminan las probabilidades.\nTambién es útil para comparar modelos. Consideremos el modelo de los datos de diabetes que incluyen todas las variables:\n\nmod_2 <- logistic_reg() |> \n set_engine(\"keras\") |> \n set_mode(\"classification\") |>\n set_args(epochs = 250, \n optimizer = optimizer_sgd(lr = 0.3),\n batch_size = nrow(diabetes_ent), \n verbose = FALSE) |> \n fit(type ~ ., juice(receta_diabetes))\npreds_prueba_completo <- predict(\n mod_2, prueba_baked, type ='prob') |> \n bind_cols(prueba_baked)\npreds_prueba_2 <- bind_rows(\n preds_prueba |> mutate(modelo = \"IMC y edad\"),\n preds_prueba_completo |> mutate(modelo = \"Todas\")) |> \n group_by(modelo)\n\nY graficamos juntas:\n\nroc_2_tbl <- roc_curve(preds_prueba_2, type, .pred_Yes)\nggplot(roc_2_tbl, aes(x = 1 - specificity, y = sensitivity)) +\n geom_path(aes(colour = modelo), size = 1.2) +\n geom_abline(colour = \"gray\") + \n coord_equal() +\n xlab(\"Tasa de falsos positivos\") + \n ylab(\"Sensibilidad\")\n\n\n\n\nComparación auc:\n\nroc_auc(preds_prueba_2, type, .pred_Yes) \n\n# A tibble: 2 × 4\n modelo .metric .estimator .estimate\n <chr> <chr> <chr> <dbl>\n1 IMC y edad roc_auc binary 0.761\n2 Todas roc_auc binary 0.866\n\n\nEn este ejemplo, vemos que casi no importa qué perfil de especificidad y sensibilidad busquemos: el clasificador que usa todas las variables domina casi siempre al clasificador que sólo utiliza IMC y edad." }, { "objectID": "09-clasificacion-2.html#curvas-de-precisión-sensibilidad", @@ -613,7 +613,7 @@ "href": "13-arboles-boosting.html#submuestreo", "title": "13  Métodos basados en árboles: boosting", "section": "13.11 Submuestreo", - "text": "13.11 Submuestreo\nPuede funcionar bien construir cada uno de los árboles con submuestras de la muestra de entrenamiento, como una manera adicional de reducir varianza al construir nuestro predictor (esta idea es parecida a la de los bosques aleatorios, aquí igualmente perturbamos la muestra de entrenamiento en cada paso para evitar sobreajuste). Adicionalmente, este proceso acelera considerablemente las iteraciones de boosting, y en algunos casos sin penalización en desempeño.\nEn boosting se pueden tomar submuestras (una fracción mayor a 0.5 de la muestra de entrenamiento, pero puede ser más chica para conjuntos grandes de entrenamiento) sin reemplazo.\nEste parámetro también puede ser afinado con muestra de validación o validación cruzada.\nFinalmente, hacemos una evaluación correcta de validación cruzada:\n\n#xgboost es el default\nmodelo_boosting <- boost_tree(learn_rate = 0.003, trees = tune(), \n mtry = 5, tree_depth = 4) |> \n set_mode(\"regression\") |> \n set_args(objective = \"reg:squarederror\")\nflujo_casas <- workflow() |> add_recipe(receta_casas) |> add_model(modelo_boosting)\n\n\nnum_arboles_tbl <- tibble(trees = seq(1, 10000, 100))\nset.seed(81)\nparticion_vc <- vfold_cv(casas_entrena, v = 10)\nmis_metricas <- metric_set(mape, rsq)\nresultados <- tune_grid(flujo_casas, particion_vc, grid = num_arboles_tbl, metrics = mis_metricas)\n\n\ncollect_metrics(resultados) |> \n filter(.metric == \"mape\", trees > 10) |> \n ggplot(aes(x = trees, y = mean, ymin = mean - std_err, ymax = mean + std_err)) + \n geom_point() +\n geom_line() + geom_ribbon(alpha = 0.2) + ylab(\"mape val_cruzada\")\n\n\n\n\n\ncollect_metrics(resultados) |> filter(.metric == \"mape\") |> arrange(mean) |> head()\n\n# A tibble: 6 × 7\n trees .metric .estimator mean n std_err .config \n <dbl> <chr> <chr> <dbl> <int> <dbl> <chr> \n1 9901 mape standard 9.62 10 0.516 Preprocessor1_Model100\n2 9801 mape standard 9.62 10 0.516 Preprocessor1_Model099\n3 9701 mape standard 9.62 10 0.516 Preprocessor1_Model098\n4 9301 mape standard 9.62 10 0.515 Preprocessor1_Model094\n5 9401 mape standard 9.62 10 0.515 Preprocessor1_Model095\n6 9201 mape standard 9.62 10 0.515 Preprocessor1_Model093\n\n\nFinalmente podemos guardar el modelo en un formato estándar (R, Python, GCP y otros):\n\nsystem.time(modelo <- xgb.train(d_entrena, verbose = 1, nrounds = 10000, params = params))\n\n user system elapsed \n 15.276 0.799 4.087 \n\nxgb.save(model = modelo, fname = \"./cache/casas_boost.xgb\")\n\n[1] TRUE" + "text": "13.11 Submuestreo\nPuede funcionar bien construir cada uno de los árboles con submuestras de la muestra de entrenamiento, como una manera adicional de reducir varianza al construir nuestro predictor (esta idea es parecida a la de los bosques aleatorios, aquí igualmente perturbamos la muestra de entrenamiento en cada paso para evitar sobreajuste). Adicionalmente, este proceso acelera considerablemente las iteraciones de boosting, y en algunos casos sin penalización en desempeño.\nEn boosting se pueden tomar submuestras (una fracción mayor a 0.5 de la muestra de entrenamiento, pero puede ser más chica para conjuntos grandes de entrenamiento) sin reemplazo.\nEste parámetro también puede ser afinado con muestra de validación o validación cruzada.\nFinalmente, hacemos una evaluación correcta de validación cruzada:\n\n#xgboost es el default\nmodelo_boosting <- boost_tree(learn_rate = 0.003, trees = tune(), \n mtry = 5, tree_depth = 4) |> \n set_mode(\"regression\") |> \n set_args(objective = \"reg:squarederror\")\nflujo_casas <- workflow() |> add_recipe(receta_casas) |> add_model(modelo_boosting)\n\n\nnum_arboles_tbl <- tibble(trees = seq(1, 10000, 100))\nset.seed(81)\nparticion_vc <- vfold_cv(casas_entrena, v = 10)\nmis_metricas <- metric_set(mape, rsq)\nresultados <- tune_grid(flujo_casas, particion_vc, grid = num_arboles_tbl, metrics = mis_metricas)\n\n\ncollect_metrics(resultados) |> \n filter(.metric == \"mape\", trees > 10) |> \n ggplot(aes(x = trees, y = mean, ymin = mean - std_err, ymax = mean + std_err)) + \n geom_point() +\n geom_line() + geom_ribbon(alpha = 0.2) + ylab(\"mape val_cruzada\")\n\n\n\n\n\ncollect_metrics(resultados) |> filter(.metric == \"mape\") |> arrange(mean) |> head()\n\n# A tibble: 6 × 7\n trees .metric .estimator mean n std_err .config \n <dbl> <chr> <chr> <dbl> <int> <dbl> <chr> \n1 9901 mape standard 9.62 10 0.516 Preprocessor1_Model100\n2 9801 mape standard 9.62 10 0.516 Preprocessor1_Model099\n3 9701 mape standard 9.62 10 0.516 Preprocessor1_Model098\n4 9301 mape standard 9.62 10 0.515 Preprocessor1_Model094\n5 9401 mape standard 9.62 10 0.515 Preprocessor1_Model095\n6 9201 mape standard 9.62 10 0.515 Preprocessor1_Model093\n\n\nFinalmente podemos guardar el modelo en un formato estándar (R, Python, GCP y otros):\n\nsystem.time(modelo <- xgb.train(d_entrena, verbose = 1, nrounds = 10000, params = params))\n\n user system elapsed \n 15.522 0.782 4.154 \n\nxgb.save(model = modelo, fname = \"./cache/casas_boost.xgb\")\n\n[1] TRUE" }, { "objectID": "13-arboles-boosting.html#otros-parámetros", @@ -669,7 +669,7 @@ "href": "14-interpretacion.html#explicación-de-predicciones", "title": "14  Interpretación de modelos", "section": "14.5 Explicación de predicciones", - "text": "14.5 Explicación de predicciones\nUna tarea común que se puede requerir para transparentar las predicciones es distribuir contribuciones a la predicción de cada variable.\nConsideremos una predictor \\(f(X)\\), y hacemos una predicción \\(f(x)\\) para un caso \\(x = (x_1,x_2,\\ldots, x_p)\\) particular. ¿Cómo contribuye cada variable a la predicción f(x)?\nUna manera común (ojo: no es la única) de asignar contribuciones si el modelo es lineal y no tiene interacciones es la siguiente:\nSi \\(f(X_1,\\ldots, X_p) = \\beta_0 + \\beta_1 X_1 + \\cdots + \\beta_p X_p\\) , y \\(x^* = (x_1^*,x_2^*,\\ldots, x_p^*)\\) es un caso o instancia particular, podemos definir la contribución \\(\\phi_j = \\phi_j (x^*)\\) de la variable \\(j\\) en la predicción como\n\\[\\phi_j = \\beta_j(x_j^* - \\mu_j),\\] donde \\(\\mu_j\\) es la media de la variable \\(x_j\\) (por ejemplo en el conjunto de entrenamiento). Podemos también definir fácilmente la contribución de un subconjunto \\(W\\) de variables dada, como \\[\\phi_W (x) = \\sum_{j\\in W} \\beta_j(x_j^* - \\mu_j)\\].\nContribución es un buen nombre para estas cantidades, pues satisfacen (usando la linealidad y las definiciones de arriba):\n\nLas contribuciones suman la diferencia de la predicción con la predicción media: \\[\\sum_j \\phi_j(x) = f(x) - E(f(x))\\]\nSi una variable satisface \\(\\phi_W (x) = \\phi_{W\\cup j} (x)\\) para cualquier subconjunto de variables \\(W\\), entonces \\(\\phi_j\\) es cero.\nSi nuestro predictor se puede escribir como \\(f(x) = f_1(x) + f_2(x)\\) entonces la contribución de cada variable en la predicción \\(f(x)\\) es la suma de las contribución en \\(f_1\\) y en \\(f_2\\)\nPara cualquier subconjunto de variables, podemos considerar la contribución de una variable \\(j\\) cuando agregamos al subcojunto la variable \\(j\\) como \\(\\phi_{W \\cup j} (x) - \\phi_W (x)\\). Si para cualquier subconjunto \\(W\\) tenemos que las variables \\(j\\) y \\(k\\) tienen la misma contribución al agregarlos a \\(W\\) (para toda \\(W\\)), entonces \\(\\phi_j =\\phi_k\\).\n\nAhora la pregunta es: ¿cómo definimos las contribuciones para un predictor \\(f\\) más complejo? (por ejemplo, un bosque aleatorio). Resulta que el concepto de contribución o atribución es uno que se estudia en teoría de juegos, y se puede demostrar que hay una sola forma de hacerlo cumpliendo las propiedades señaladas arriba: mediante los valores de Shapley.\nAntes de seguir, veremos las dificultades que podemos encontrar para definir la atribución o contribuciones:\n\nEjemplo\nConsideramos un modelo lineal para empezar. Por ejemplo \\[f(x_1, x_2) = \\beta_0 + \\beta_1 x_1 + \\beta_2 x_2,\\] ¿Cómo podemos atribuir contribuciones de \\(x_1\\) y \\(x_2\\) para una entrada particular \\(x^* = (x_1^*, x_2^*).\\)\nEn primer lugar, podríamos ver qué pasa cuando pensamos que no tenemos ninguna variable disponible. Esta cantidad la podríamos definir como (promediando sobre la muestra de entrenamiento): \\[v(0) = \\frac{1}{N}\\sum_i f(x_1^{(i)},x_2^{(i)}) = \\beta_0 + \\beta_1 \\bar{x_1} + \\beta_2\\bar{x_2}\\]\nque es la predicción media (usamos linealidad de \\(f\\)).\nAhora supongamos que usamos la variable \\(x_1\\). En este caso, podríamos calcular una predicción promediando sobre la variable \\(x_2\\), que no tenemos disponible:\n\\[v(1) = \\frac{1}{N}\\sum_i f(x_1^{*},x_2^{(i)}) = \\beta_0 + \\beta_1 x_1^* + \\beta_2\\bar{x_2}\\] Entonces la contribución cuando agregamos la variable 1 es: \\[v(1) -v(0) = \\beta_1(x_1^* - \\bar{x_1})\\] Ahora supongamos que ya tenemos la variable 2, y agregamos la variable 1. La predicción con todas las variables es \\[v(1,2) = \\beta_0 + \\beta_1 x_1^* + \\beta_2 x_2^*\\] y cuando solo tenemos la variable \\(2\\) es \\[v(2) = \\frac{1}{N}\\sum_i f(x_1^{(i)},x_2^{*}) = \\beta_0 + \\beta_1\\bar{x_1} + \\beta_2 x_2^*, \\] y la contribución en este caso es: \\[v(1,2) - v(2) = \\beta_1 (x_1^* - \\bar{x_1}),\\] que es la misma cantidad que \\(v(1)-v(0)\\). Así que no importa como “incluyamos” la variable 1 bajo este criterio, el resultado es el mismo, y podemos definir la contribución de \\(x_1\\) como definimos arriba para un predictor lineal.\nAhora consideremos el caso más complicado donde tenemos una interacción multiplicativa:\n\\[f(x_1, x_2) = \\beta_0 + \\beta_1 x_1 + \\beta_2 x_2 + \\beta_3x_1x_2\\]\nVeremos qué pasa cuando seguimos la misma idea de arriba: en primer lugar, tenemos\n\\[v(0) = \\frac{1}{N}\\sum_i f(x_1^{(i)},x_2^{(i)}) =\n\\beta_0 + \\beta_1 \\overline{x_1} + \\beta_2\\overline{x_2} + \\beta_3\\overline{x_1x_2}\\]\ny \\[v(1) = \\frac{1}{N}\\sum_i f(x_1^{*},x_2^{(i)}) = \\beta_0 + \\beta_1 x_1^* + \\beta_2\\bar{x_2} + (\\beta_3 \\bar{x_2})x_1^*\\] La contribución sería \\[v(1)-v(0) = \\beta_1(x_1^* - \\bar{x_1}) +\\beta_3\\bar{x_2}\\left(x_1^*-\\frac{\\overline{x_1x_2}}{\\bar{x_2}}\\right)\\] Por otro lado, igual que arriba\n\\[v(2) = \\frac{1}{N}\\sum_i f(x_1^{(i)},x_2^{*}) = \\beta_0 + \\beta_1\\bar{x_1} + \\beta_2 x_2^* + \\beta_3x_2^*\\bar{x_1}\\]\ny entonces: \\[v(1,2) - v(2) = \\beta_1(x_1^*-\\bar{x_1})+ \\beta_3x_2^*(x_1^* - \\bar{x_1})\\] Y las cantidades \\(v(1) - v(0)\\) y \\(v(1,2)- v(2)\\) no son iguales en este caso. Tenemos dos órdenes distintos para poner las variables en el modelo, así que ponemos:\n\\[\\phi_1 = \\frac{1}{2}(v(1,2)-v(2)) + \\frac{1}{2}(v(1)-v(0)).\\] y análogamente para \\[\\phi_2 = \\frac{1}{2}(v(1,2)-v(1)) + \\frac{1}{2}(v(2)-v(0)).\\]\nNótese que estas cantidades satisfacen: \\(\\phi_1 + \\phi_2 = v(1,2) -v(0) = f(x_1^*, x_2^*) - \\frac{1}{N}\\sum_i f(x_1^{(i)}, x_2^{(i)}),\\)\nes decir, la diferencia entre la predicción que nos interesa y la predicción media, que es la primera propiedad que buscábamos.\nConsideremos ahora el caso de 3 variables, y nos interesa encontrar la contribución de la variable 1. Ahora tenemos 6 órdenes distintos para poner las variables en el predictor. Dos de ellos terminan con \\(v(1,2,3) - v(2,3)\\), uno de ellos contiene la comparación \\(v(1,2)-v(2)\\), uno de ellos contiene la comparación \\(v(1,3)-v(3)\\) y dos de ellos contiene la comparación \\(v(1)-v(0)\\). Así que ponderaríamos:\n\\[\\phi_1 = \\frac{2}{6}(v(1,2,3) - v(2,3)) + \\frac{1}{6}(v(1,2)-v(2)) + \\frac{1}{6}(v(1,3)-v(3)) + \\frac{2}{6}(v(1)-v(0))\\] Puedes checar que \\(\\phi_1+ \\phi_2 +\\phi_3 = f(x_1^*, x_2^*, x_3^*) - \\frac{1}{N}\\sum_i f(x_1^{(i)}, x_2^{(i)}, x_3^{(i)})\\). Los valores así definidos satisfacen todas las propiedades que discutimos arriba.\n\n\n14.5.1 Valores de Shapley\nLa idea principal en los valores de Shapley es que para entender la contribución de una variable, es necesario considerar su contribución según distintos órdenes en el que consideramos las variables.\nDefinimos primero la contribución de un conjunto de variables \\(S\\subset M,\\) donde \\(M =\\{x_1, x_2\\ldots, x_p\\}\\), marginalizando como sigue con la muestra de entrenamiento:\nSuponiendo que \\(S = \\{1,2,3,\\ldots ,k\\}\\) entonces \\[v(S) = \\frac{1}{N}\\sum_i f(x_1^*,x_2^*,\\ldots, x_k^*, x_k^{(i)}, x_{k+1}^{(i)}, \\ldots, x_p^{(i)})\\] Donde se mantienen fijos los elementos de \\(S\\), y promediamos sobre los elementos que no están en \\(S\\). Esto podemos hacerlo para cualquier subconjunto \\(S\\) de variables. Ahora consideramos una variable dada \\(j\\), y consideramos las diferencias: \\[v(S\\cup {j}) - v(S)\\] Es decir, cuánto contribuye la variable \\(j\\) cuando la agregamos a las variables que ya teníamos en \\(S\\).\nEl valor de Shapley de la variable \\(j\\) es\n\\[\\phi_j = \\sum_{S\\subset M, j\\notin S} \\frac{|S|!(M - |S| -1)!}{M!} (v(S\\cup j) -v(S))\\]\nLos ponderadores están dados como discutimos arriba: hay \\(M!\\) posibles ordenamientos de las variables. Los ordenamientos que incluyen la comparación \\(v(S\\cup j) -v(S)\\) satisfacen: las primeras \\(|S|\\) variables pueden haber entrado de \\(|S|!\\) maneras, luego sigue la \\(j\\), y el restante de las variables pueden entrar de \\((M-|S|-1)!\\) maneras.\n\n\nEjemplo: precios de casas\nConsideremos entonces el bosque que construimos arriba para los precios de casas, y consideramos una casa particular:\n\nset.seed(232)\nshapley_val <- Shapley$new(predictor, x.interest = casas_entrena[21,])\nshapley_val$results <- filter(shapley_val$results, feature %in% vars_usadas)\nshapley_val$plot()\n\n\n\n\nLa suma de los valores phi da (aproximadamente, por el algoritmo usado) la diferencia entre nuestra predicción para esta instancia y la predicción promedio:\n\nsum(shapley_val$results$phi)\n\n[1] -10.40783\n\n\nAhora hacemos otro ejemplo:\n\nshapley_val <- Shapley$new(predictor, x.interest = casas_entrena[52,])\nshapley_val$results <- filter(shapley_val$results, feature %in% vars_usadas)\nshapley_val$plot()\n\n\n\nsum(shapley_val$results$phi)\n\n[1] -47.69727\n\n\n\nshapley_val <- Shapley$new(predictor, x.interest = casas_entrena[121,])\nshapley_val$results <- filter(shapley_val$results, feature %in% vars_usadas)\nshapley_val$plot()\n\n\n\n\nObservaciones:\n\nNótese que las contribuciones de cada variable en un mismo nivel puede ser diferente en diferentes instancias, pues los modelos que usamos típicamente incluyen interacciones.\nCalcular sobre todas las posibles permutaciones de las variables es demasiado costoso. Para estimar el valor de Shapley, en iml se toma una muestra de permutaciones, y para cada una se calcula el valor correspondiente \\(v(S\\cup j) - v(S),\\) dependiendo dónde aparece \\(j\\), y promediando sobre las variables que no aparecen en \\(S\\) como mostramos arriba. No es necesario ponderar la muestra de permutaciones.\nExisten también versiones adaptadas a los árboles que son más rápìdas.\n\n\n\nEjemplo\nPara un modelo de clasificación, xgboost calcula los valores de shapley en la escala logit (del predictor lineal)\n\nlibrary(xgboost)\nnse_tbl <- nse_entrena %>% ungroup %>% \n dplyr::select(ab, conex_inte, ocupados, \n num_tvd, educa_jefe, tam_loc, combustible, num_auto)\nmat_x <- model.matrix(ab ~ -1 + ., nse_tbl)\nd_entrena <- xgb.DMatrix(data = mat_x, label = nse_tbl$ab)\nboost_nse <- xgboost(mat_x, label = as.numeric(nse_tbl$ab == \"1\"), \n nrounds = 500, eta = 0.1, \n max_depth = 3, subsample = .5,\n objective = \"binary:logistic\", nthread = 2, verbose = 0)\nshap_nse <- xgb.plot.shap(mat_x, model = boost_nse, top_n = 8, n_col = 2)\n\n\n\npred_base <- mean(nse_tbl$ab==1)\n\n\ndata <- shap_nse$data\ncontrib <- shap_nse$shap_contrib\ndat_g <- tibble(educa_jefe = data[, \"educa_jefe\"], shap_educa = contrib[, \"educa_jefe\"],\n shap_ocupados = contrib[, \"ocupados\"], ) %>% \n sample_n(2000)\nggplot(dat_g, aes(x = educa_jefe, y = shap_educa, colour = shap_ocupados)) + \n geom_jitter(alpha = 0.9, width = 0.5, height = 0) +\n scale_color_gradient(low=\"orange\", high=\"black\")\n\n\n\n\nY podemos ver predicciones individuales:\n\ngraf_caso <- function(data, contrib, ind_caso, pred_base){\n dat_tbl <- tibble(variable = colnames(data), valor = data[ind_caso, ], \n shap = contrib[ind_caso,]) %>% \n unite(\"variable_valor\", variable, valor) %>% \n mutate(variable_valor = fct_reorder(variable_valor, shap))\n pred_logit <- log(pred_base / (1-pred_base)) + sum(dat_tbl$shap)\n pred <- 1 / (1 + exp(-pred_logit))\n sprintf(\"Predicción base: %0.2f, Predicción: %0.2f\", pred_base, pred) %>% print\n ggplot(dat_tbl, aes(x= variable_valor, y = shap, ymax = shap, ymin = 0)) + \n coord_flip() +geom_point() + geom_hline(yintercept = 0, colour = \"gray\") +\n geom_linerange() \n}\ngraf_caso(data, contrib, 10, pred_base)\n\n[1] \"Predicción base: 0.18, Predicción: 0.88\"\n\n\n\n\n\n\ngraf_caso(data, contrib, 102, pred_base)\n\n[1] \"Predicción base: 0.18, Predicción: 0.48\"\n\n\n\n\n\nDiscusión:\n\nUn valor de contribución que puede ser más apropiado para los valores Shapley es condicionando en cada caso a la información que se tiene durante el proceso de adición de variables. Es decir, usamos \\[v(S) = E_{X_C|X_S}\\left [ f(X_S,X_C) \\, | \\, X_S = x_s^* \\right].\\] Esta cantidad es teóricamente más apropiada para hacer predicciones cuando “no tenemos” las variables de \\(X_C\\). Sin embargo, calcular esta cantidad es considerablemente difícil, pues requiere modelar también la conjunta de \\((X_1,\\ldots, X_p)\\), lo cuál en general es difícil. Hasta en regresión lineal, incluso sin interacciones, no es trivial hacer estos cálculos. Generalmente se utilizan como mostramos arriba. Una desventaja clara del proceso como lo mostramos arriba es que puede ser que al hacer estos promedios, usemos partes del modelo con poca información y malas predicciones. Los valores de Shapley pueden ser ruidosos en este caso.\nLos que mostramos arriba son llamados comunmente valores SHAP o explicación aditiva de Shapley. Existen muchas variaciones de la aplicación de valores de Shapley para entender predictores y es un tema de investigación activo.\n\n\n\n\n\nHastie, Trevor, Robert Tibshirani, y Jerome Friedman. 2017. The Elements of Statistical Learning. Springer Series en Statistics. Springer New York Inc. http://web.stanford.edu/~hastie/ElemStatLearn/." + "text": "14.5 Explicación de predicciones\nUna tarea común que se puede requerir para transparentar las predicciones es distribuir contribuciones a la predicción de cada variable.\nConsideremos una predictor \\(f(X)\\), y hacemos una predicción \\(f(x)\\) para un caso \\(x = (x_1,x_2,\\ldots, x_p)\\) particular. ¿Cómo contribuye cada variable a la predicción f(x)?\nUna manera común (ojo: no es la única) de asignar contribuciones si el modelo es lineal y no tiene interacciones es la siguiente:\nSi \\(f(X_1,\\ldots, X_p) = \\beta_0 + \\beta_1 X_1 + \\cdots + \\beta_p X_p\\) , y \\(x^* = (x_1^*,x_2^*,\\ldots, x_p^*)\\) es un caso o instancia particular, podemos definir la contribución \\(\\phi_j = \\phi_j (x^*)\\) de la variable \\(j\\) en la predicción como\n\\[\\phi_j = \\beta_j(x_j^* - \\mu_j),\\] donde \\(\\mu_j\\) es la media de la variable \\(x_j\\) (por ejemplo en el conjunto de entrenamiento). Podemos también definir fácilmente la contribución de un subconjunto \\(W\\) de variables dada, como \\[\\phi_W (x) = \\sum_{j\\in W} \\beta_j(x_j^* - \\mu_j)\\].\nContribución es un buen nombre para estas cantidades, pues satisfacen (usando la linealidad y las definiciones de arriba):\n\nLas contribuciones suman la diferencia de la predicción con la predicción media: \\[\\sum_j \\phi_j(x) = f(x) - E(f(x))\\]\nSi una variable satisface \\(\\phi_W (x) = \\phi_{W\\cup j} (x)\\) para cualquier subconjunto de variables \\(W\\), entonces \\(\\phi_j\\) es cero.\nSi nuestro predictor se puede escribir como \\(f(x) = f_1(x) + f_2(x)\\) entonces la contribución de cada variable en la predicción \\(f(x)\\) es la suma de las contribución en \\(f_1\\) y en \\(f_2\\)\nPara cualquier subconjunto de variables, podemos considerar la contribución de una variable \\(j\\) cuando agregamos al subcojunto la variable \\(j\\) como \\(\\phi_{W \\cup j} (x) - \\phi_W (x)\\). Si para cualquier subconjunto \\(W\\) tenemos que las variables \\(j\\) y \\(k\\) tienen la misma contribución al agregarlos a \\(W\\) (para toda \\(W\\)), entonces \\(\\phi_j =\\phi_k\\).\n\nAhora la pregunta es: ¿cómo definimos las contribuciones para un predictor \\(f\\) más complejo? (por ejemplo, un bosque aleatorio). Resulta que el concepto de contribución o atribución es uno que se estudia en teoría de juegos, y se puede demostrar que hay una sola forma de hacerlo cumpliendo las propiedades señaladas arriba: mediante los valores de Shapley.\nAntes de seguir, veremos las dificultades que podemos encontrar para definir la atribución o contribuciones:\n\nEjemplo\nConsideramos un modelo lineal para empezar. Por ejemplo \\[f(x_1, x_2) = \\beta_0 + \\beta_1 x_1 + \\beta_2 x_2,\\] ¿Cómo podemos atribuir contribuciones de \\(x_1\\) y \\(x_2\\) para una entrada particular \\(x^* = (x_1^*, x_2^*).\\)\nEn primer lugar, podríamos ver qué pasa cuando pensamos que no tenemos ninguna variable disponible. Esta cantidad la podríamos definir como (promediando sobre la muestra de entrenamiento): \\[v(0) = \\frac{1}{N}\\sum_i f(x_1^{(i)},x_2^{(i)}) = \\beta_0 + \\beta_1 \\bar{x_1} + \\beta_2\\bar{x_2}\\]\nque es la predicción media (usamos linealidad de \\(f\\)).\nAhora supongamos que usamos la variable \\(x_1\\). En este caso, podríamos calcular una predicción promediando sobre la variable \\(x_2\\), que no tenemos disponible:\n\\[v(1) = \\frac{1}{N}\\sum_i f(x_1^{*},x_2^{(i)}) = \\beta_0 + \\beta_1 x_1^* + \\beta_2\\bar{x_2}\\] Entonces la contribución cuando agregamos la variable 1 es: \\[v(1) -v(0) = \\beta_1(x_1^* - \\bar{x_1})\\] Ahora supongamos que ya tenemos la variable 2, y agregamos la variable 1. La predicción con todas las variables es \\[v(1,2) = \\beta_0 + \\beta_1 x_1^* + \\beta_2 x_2^*\\] y cuando solo tenemos la variable \\(2\\) es \\[v(2) = \\frac{1}{N}\\sum_i f(x_1^{(i)},x_2^{*}) = \\beta_0 + \\beta_1\\bar{x_1} + \\beta_2 x_2^*, \\] y la contribución en este caso es: \\[v(1,2) - v(2) = \\beta_1 (x_1^* - \\bar{x_1}),\\] que es la misma cantidad que \\(v(1)-v(0)\\). Así que no importa como “incluyamos” la variable 1 bajo este criterio, el resultado es el mismo, y podemos definir la contribución de \\(x_1\\) como definimos arriba para un predictor lineal.\nAhora consideremos el caso más complicado donde tenemos una interacción multiplicativa:\n\\[f(x_1, x_2) = \\beta_0 + \\beta_1 x_1 + \\beta_2 x_2 + \\beta_3x_1x_2\\]\nVeremos qué pasa cuando seguimos la misma idea de arriba: en primer lugar, tenemos\n\\[v(0) = \\frac{1}{N}\\sum_i f(x_1^{(i)},x_2^{(i)}) =\n\\beta_0 + \\beta_1 \\overline{x_1} + \\beta_2\\overline{x_2} + \\beta_3\\overline{x_1x_2}\\]\ny \\[v(1) = \\frac{1}{N}\\sum_i f(x_1^{*},x_2^{(i)}) = \\beta_0 + \\beta_1 x_1^* + \\beta_2\\bar{x_2} + (\\beta_3 \\bar{x_2})x_1^*\\] La contribución sería \\[v(1)-v(0) = \\beta_1(x_1^* - \\bar{x_1}) +\\beta_3\\bar{x_2}\\left(x_1^*-\\frac{\\overline{x_1x_2}}{\\bar{x_2}}\\right)\\] Por otro lado, igual que arriba\n\\[v(2) = \\frac{1}{N}\\sum_i f(x_1^{(i)},x_2^{*}) = \\beta_0 + \\beta_1\\bar{x_1} + \\beta_2 x_2^* + \\beta_3x_2^*\\bar{x_1}\\]\ny entonces: \\[v(1,2) - v(2) = \\beta_1(x_1^*-\\bar{x_1})+ \\beta_3x_2^*(x_1^* - \\bar{x_1})\\] Y las cantidades \\(v(1) - v(0)\\) y \\(v(1,2)- v(2)\\) no son iguales en este caso. Tenemos dos órdenes distintos para poner las variables en el modelo, así que ponemos:\n\\[\\phi_1 = \\frac{1}{2}(v(1,2)-v(2)) + \\frac{1}{2}(v(1)-v(0)).\\] y análogamente para \\[\\phi_2 = \\frac{1}{2}(v(1,2)-v(1)) + \\frac{1}{2}(v(2)-v(0)).\\]\nNótese que estas cantidades satisfacen: \\(\\phi_1 + \\phi_2 = v(1,2) -v(0) = f(x_1^*, x_2^*) - \\frac{1}{N}\\sum_i f(x_1^{(i)}, x_2^{(i)}),\\)\nes decir, la diferencia entre la predicción que nos interesa y la predicción media, que es la primera propiedad que buscábamos.\nConsideremos ahora el caso de 3 variables, y nos interesa encontrar la contribución de la variable 1. Ahora tenemos 6 órdenes distintos para poner las variables en el predictor. Dos de ellos terminan con \\(v(1,2,3) - v(2,3)\\), uno de ellos contiene la comparación \\(v(1,2)-v(2)\\), uno de ellos contiene la comparación \\(v(1,3)-v(3)\\) y dos de ellos contiene la comparación \\(v(1)-v(0)\\). Así que ponderaríamos:\n\\[\\phi_1 = \\frac{2}{6}(v(1,2,3) - v(2,3)) + \\frac{1}{6}(v(1,2)-v(2)) + \\frac{1}{6}(v(1,3)-v(3)) + \\frac{2}{6}(v(1)-v(0))\\] Puedes checar que \\(\\phi_1+ \\phi_2 +\\phi_3 = f(x_1^*, x_2^*, x_3^*) - \\frac{1}{N}\\sum_i f(x_1^{(i)}, x_2^{(i)}, x_3^{(i)})\\). Los valores así definidos satisfacen todas las propiedades que discutimos arriba.\n\n\n14.5.1 Valores de Shapley\nLa idea principal en los valores de Shapley es que para entender la contribución de una variable, es necesario considerar su contribución según distintos órdenes en el que consideramos las variables.\nDefinimos primero la contribución de un conjunto de variables \\(S\\subset M,\\) donde \\(M =\\{x_1, x_2\\ldots, x_p\\}\\), marginalizando como sigue con la muestra de entrenamiento:\nSuponiendo que \\(S = \\{1,2,3,\\ldots ,k\\}\\) entonces \\[v(S) = \\frac{1}{N}\\sum_i f(x_1^*,x_2^*,\\ldots, x_k^*, x_k^{(i)}, x_{k+1}^{(i)}, \\ldots, x_p^{(i)})\\] Donde se mantienen fijos los elementos de \\(S\\), y promediamos sobre los elementos que no están en \\(S\\). Esto podemos hacerlo para cualquier subconjunto \\(S\\) de variables. Ahora consideramos una variable dada \\(j\\), y consideramos las diferencias: \\[v(S\\cup {j}) - v(S)\\] Es decir, cuánto contribuye la variable \\(j\\) cuando la agregamos a las variables que ya teníamos en \\(S\\).\nEl valor de Shapley de la variable \\(j\\) es\n\\[\\phi_j = \\sum_{S\\subset M, j\\notin S} \\frac{|S|!(M - |S| -1)!}{M!} (v(S\\cup j) -v(S))\\]\nLos ponderadores están dados como discutimos arriba: hay \\(M!\\) posibles ordenamientos de las variables. Los ordenamientos que incluyen la comparación \\(v(S\\cup j) -v(S)\\) satisfacen: las primeras \\(|S|\\) variables pueden haber entrado de \\(|S|!\\) maneras, luego sigue la \\(j\\), y el restante de las variables pueden entrar de \\((M-|S|-1)!\\) maneras.\n\n\nEjemplo: precios de casas\nConsideremos entonces el bosque que construimos arriba para los precios de casas, y consideramos una casa particular:\n\nset.seed(232)\nshapley_val <- Shapley$new(predictor, x.interest = casas_entrena[21,])\nshapley_val$results <- filter(shapley_val$results, feature %in% vars_usadas)\nshapley_val$plot()\n\n\n\n\nLa suma de los valores phi da (aproximadamente, por el algoritmo usado) la diferencia entre nuestra predicción para esta instancia y la predicción promedio:\n\nsum(shapley_val$results$phi)\n\n[1] -10.40783\n\n\nAhora hacemos otro ejemplo:\n\nshapley_val <- Shapley$new(predictor, x.interest = casas_entrena[52,])\nshapley_val$results <- filter(shapley_val$results, feature %in% vars_usadas)\nshapley_val$plot()\n\n\n\nsum(shapley_val$results$phi)\n\n[1] -47.69727\n\n\n\nshapley_val <- Shapley$new(predictor, x.interest = casas_entrena[121,])\nshapley_val$results <- filter(shapley_val$results, feature %in% vars_usadas)\nshapley_val$plot()\n\n\n\n\nObservaciones:\n\nNótese que las contribuciones de cada variable en un mismo nivel puede ser diferente en diferentes instancias, pues los modelos que usamos típicamente incluyen interacciones.\nCalcular sobre todas las posibles permutaciones de las variables es demasiado costoso. Para estimar el valor de Shapley, en iml se toma una muestra de permutaciones, y para cada una se calcula el valor correspondiente \\(v(S\\cup j) - v(S),\\) dependiendo dónde aparece \\(j\\), y promediando sobre las variables que no aparecen en \\(S\\) como mostramos arriba. No es necesario ponderar la muestra de permutaciones.\nExisten también versiones adaptadas a los árboles que son más rápìdas.\n\n\n\nEjemplo\nPara un modelo de clasificación, xgboost calcula los valores de shapley en la escala logit (del predictor lineal)\n\nlibrary(xgboost)\nnse_tbl <- nse_entrena %>% ungroup %>% \n dplyr::select(ab, conex_inte, ocupados, \n num_tvd, educa_jefe, tam_loc, combustible, num_auto)\nmat_x <- model.matrix(ab ~ -1 + ., nse_tbl)\nd_entrena <- xgb.DMatrix(data = mat_x, label = nse_tbl$ab)\nboost_nse <- xgboost(mat_x, label = as.numeric(nse_tbl$ab == \"1\"), \n nrounds = 500, eta = 0.1, \n max_depth = 3, subsample = .5,\n objective = \"binary:logistic\", nthread = 2, verbose = 0)\nshap_nse <- xgb.plot.shap(mat_x, model = boost_nse, top_n = 8, n_col = 2)\n\n\n\npred_base <- mean(nse_tbl$ab==1)\n\n\ndata <- shap_nse$data\ncontrib <- shap_nse$shap_contrib\ndat_g <- tibble(educa_jefe = data[, \"educa_jefe\"], shap_educa = contrib[, \"educa_jefe\"],\n shap_ocupados = contrib[, \"ocupados\"], ) %>% \n sample_n(2000)\nggplot(dat_g, aes(x = educa_jefe, y = shap_educa, colour = shap_ocupados)) + \n geom_jitter(alpha = 0.9, width = 0.5, height = 0) +\n scale_color_gradient(low=\"orange\", high=\"black\")\n\n\n\n\nY podemos ver predicciones individuales:\n\ngraf_caso <- function(data, contrib, ind_caso, pred_base){\n dat_tbl <- tibble(variable = colnames(data), valor = data[ind_caso, ], \n shap = contrib[ind_caso,]) %>% \n unite(\"variable_valor\", variable, valor) %>% \n mutate(variable_valor = fct_reorder(variable_valor, shap))\n pred_logit <- log(pred_base / (1-pred_base)) + sum(dat_tbl$shap)\n pred <- 1 / (1 + exp(-pred_logit))\n sprintf(\"Predicción base: %0.2f, Predicción: %0.2f\", pred_base, pred) %>% print\n ggplot(dat_tbl, aes(x= variable_valor, y = shap, ymax = shap, ymin = 0)) + \n coord_flip() +geom_point() + geom_hline(yintercept = 0, colour = \"gray\") +\n geom_linerange() \n}\ngraf_caso(data, contrib, 10, pred_base)\n\n[1] \"Predicción base: 0.17, Predicción: 0.88\"\n\n\n\n\n\n\ngraf_caso(data, contrib, 102, pred_base)\n\n[1] \"Predicción base: 0.17, Predicción: 0.77\"\n\n\n\n\n\nDiscusión:\n\nUn valor de contribución que puede ser más apropiado para los valores Shapley es condicionando en cada caso a la información que se tiene durante el proceso de adición de variables. Es decir, usamos \\[v(S) = E_{X_C|X_S}\\left [ f(X_S,X_C) \\, | \\, X_S = x_s^* \\right].\\] Esta cantidad es teóricamente más apropiada para hacer predicciones cuando “no tenemos” las variables de \\(X_C\\). Sin embargo, calcular esta cantidad es considerablemente difícil, pues requiere modelar también la conjunta de \\((X_1,\\ldots, X_p)\\), lo cuál en general es difícil. Hasta en regresión lineal, incluso sin interacciones, no es trivial hacer estos cálculos. Generalmente se utilizan como mostramos arriba. Una desventaja clara del proceso como lo mostramos arriba es que puede ser que al hacer estos promedios, usemos partes del modelo con poca información y malas predicciones. Los valores de Shapley pueden ser ruidosos en este caso.\nLos que mostramos arriba son llamados comunmente valores SHAP o explicación aditiva de Shapley. Existen muchas variaciones de la aplicación de valores de Shapley para entender predictores y es un tema de investigación activo.\n\n\n\n\n\nHastie, Trevor, Robert Tibshirani, y Jerome Friedman. 2017. The Elements of Statistical Learning. Springer Series en Statistics. Springer New York Inc. http://web.stanford.edu/~hastie/ElemStatLearn/." }, { "objectID": "15-reduccion-dim.html#descomposición-aditiva-en-matrices-de-rango-1", @@ -739,14 +739,14 @@ "href": "16-recom-factorizacion.html", "title": "16  Dimensiones latentes: embeddings", "section": "", - "text": "17 Representación de palabras y word2vec\nEn esta parte empezamos a ver los enfoques más modernos (redes neuronales) para construir modelos de lenguajes y resolver tareas de NLP. Se trata de modelos de lenguaje que incluyen más estructura, son más fáciles de regularizar y de ampliar si es necesario para incluir dependencias de mayor distancia. El método de conteo/suavizamiento de ngramas es simple y funciona bien para algunas tareas, pero podemos construir mejores modelos con enfoques más estructurados, y con más capacidad para aprender aspectos más complejos del lenguaje natural.\nSi \\(w=w_1w_2\\cdots w_N\\) es una frase, y las \\(w\\) representan palabras, recordemos que un modelo de lenguaje con dependencia de \\(n\\)-gramas consiste de las probabilidades\n\\[P(w_t | w_{t-1} w_{t-2} \\cdots w_{t-n+1}),\\]\n(n=2, bigramas, n=3 trigramas, etc.)\nUn enfoque donde intentamos estimar directamente estas probabilidades de los datos observados (modelos de n-gramas) puede ser útil en algunos casos (por ejemplo autocorrección simple), pero en general la mayoría de las sucesiones del lenguaje no son observadas en ningún corpues, y es necesario considerar un un enfoque más estructurado pensando en representaciones “distribucionales” de palabras:\nLa idea de este modelo es entonces subsanar la relativa escasez de datos (comparado con todos los trigramas que pueden existir) con estructura. Sabemos que esta es una buena estrategia si la estrucutura impuesta es apropiada.\nEl objeto es entonces abstraer características de palabras (mediante estas representaciones) intentando no perder mucho de su sentido original, lo que nos permite conocer palabras por su contexto, aún cuando no las hayamos observado antes." + "text": "17 Embeddings de imágenes\nOtro tipo de embeddings similar al de los sistemas de recomendación es el de imágenes. En este caso, comenzamos con un clasificador de imágenes construido a partir de una red convolucional profunda, y consideramos las últimas capas que se utilizan para la clasificación. Estas últimas capas contienen información más relacionada con el contenido de la imagen que con patrones de pixeles generales, y puede ser utilizado para encontrar imágenes similares.\nEn esta aplicación, resolveremos el problema de encontrar imágenes duplicadas, por ejemplo en pinterest. Existen muchas imágenes con variaciones mínimas (crop, rotaciones, algunos cambios de colores, etc), y en general no queremos repetir muchas veces una imagen en el feed de un usuario.\nEs claro que hacer comparaciones de pixeles no es una estrategia muy buena, porque las transformaciones de arriba puede producir diferencias grandes en los valores de los pixeles, aún cuando el contenido de la imagen es el mismo. Consideramos tres imágenes para probar:\nComo veremos, estas tres imagenes son similares en distancia unas de otras en términos de pixeles (muy diferentes), aún cuando el contenido de las primeras dos es altamente similar.\nEn espacios de dimensión muy alta, como en imágenes, conviene hacer reducción de dimensionalidad para definir la métrica de distancia y utilizar estos métodos para encontrar vecinos cercanos.\nUtilizaremos una red convolucional pre-entrenada para extraer características de las imágenes. En este caso, tomaremos la última penúltima capa (antes de la capa de softmax), que nos dará embeddings de tamaño 4096:\nlibrary(keras)\n\n\nAttaching package: 'keras'\n\n\nThe following object is masked from 'package:yardstick':\n\n get_weights\n\nmodelo <- application_vgg16(weights = 'imagenet')\n# obtener la penúltima\nembed_modelo <- keras_model(inputs = modelo$input, \n outputs = get_layer(modelo, \"fc2\")$output)\nembed_modelo\n\nModel: \"model\"\n________________________________________________________________________________\n Layer (type) Output Shape Param # \n================================================================================\n input_1 (InputLayer) [(None, 224, 224, 3)] 0 \n block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 \n block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 \n block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 \n block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 \n block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 \n block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 \n block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 \n block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 \n block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 \n block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 \n block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 \n block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 \n block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 \n block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 \n block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 \n block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 \n block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 \n block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 \n flatten (Flatten) (None, 25088) 0 \n fc1 (Dense) (None, 4096) 102764544 \n fc2 (Dense) (None, 4096) 16781312 \n================================================================================\nTotal params: 134,260,544\nTrainable params: 134,260,544\nNon-trainable params: 0\n________________________________________________________________________________\nobtener_pixeles <- function(imagen_ruta){\n img <- image_load(imagen_ruta, target_size = c(224,224))\n x <- image_to_array(img)\n array_reshape(x, c(1, dim(x))) \n}\ncalcular_capa <- function(imagen_ruta){\n x <- obtener_pixeles(imagen_ruta) |> imagenet_preprocess_input()\n embed_modelo |> predict(x) |> as.numeric()\n}\npixeles_1 <- obtener_pixeles(\"./figuras/elefante_1.jpg\") |> \n as.numeric()\npixeles_2 <- obtener_pixeles(\"./figuras/elefante_3.jpg\") |> \n as.numeric()\npixeles_3 <- obtener_pixeles(\"./figuras/leon_1.jpg\") |> \n as.numeric()\nCalculamos la distancia pixel a pixel:\nmean((pixeles_2 - pixeles_1)^2)\n\n[1] 7040.36\n\nmean((pixeles_1 - pixeles_3)^2)\n\n[1] 7060.643\nCalculamos la penúltima capa de nuestro modelo para las imágenes de prueba:\nfeatures_1 <- calcular_capa(\"./figuras/elefante_1.jpg\")\nfeatures_2 <- calcular_capa(\"./figuras/elefante_3.jpg\")\nfeatures_3 <- calcular_capa(\"./figuras/leon_1.jpg\")\nlength(features_1)\n\n[1] 4096\nNótese ahora que la distancia en nuestro nuevo espacio de imágenes es mucho más chica para los elefantes que entre el león y los elefantes:\nmean((features_2 - features_1)^2)\n\n[1] 0.8760022\n\nmean((features_1 - features_3)^2)\n\n[1] 3.17355\nPodemos usar entonces el siguiente proceso:\nEn esta parte empezamos a ver los enfoques más modernos (redes neuronales) para construir modelos de lenguajes y resolver tareas de NLP. Se trata de modelos de lenguaje que incluyen más estructura, son más fáciles de regularizar y de ampliar si es necesario para incluir dependencias de mayor distancia. El método de conteo/suavizamiento de ngramas es simple y funciona bien para algunas tareas, pero podemos construir mejores modelos con enfoques más estructurados, y con más capacidad para aprender aspectos más complejos del lenguaje natural.\nSi \\(w=w_1w_2\\cdots w_N\\) es una frase, y las \\(w\\) representan palabras, recordemos que un modelo de lenguaje con dependencia de \\(n\\)-gramas consiste de las probabilidades\n\\[P(w_t | w_{t-1} w_{t-2} \\cdots w_{t-n+1}),\\]\n(n=2, bigramas, n=3 trigramas, etc.)\nUn enfoque donde intentamos estimar directamente estas probabilidades de los datos observados (modelos de n-gramas) puede ser útil en algunos casos (por ejemplo autocorrección simple), pero en general la mayoría de las sucesiones del lenguaje no son observadas en ningún corpus, y es necesario considerar un un enfoque más estructurado pensando en representaciones “distribucionales” de palabras:\nLa idea de este modelo es entonces subsanar la relativa escasez de datos (comparado con todos los trigramas que pueden existir) con estructura. Sabemos que esta es una buena estrategia si la estructura impuesta es apropiada.\nEl objeto es entonces abstraer características de palabras (mediante estas representaciones) intentando no perder mucho de su sentido original, lo que nos permite conocer palabras por su contexto, aún cuando no las hayamos observado antes." }, { "objectID": "16-recom-factorizacion.html#sistemas-de-recomendación", "href": "16-recom-factorizacion.html#sistemas-de-recomendación", "title": "16  Dimensiones latentes: embeddings", "section": "16.1 Sistemas de recomendación", - "text": "16.1 Sistemas de recomendación\nEn esta parte, consideramos la idea de utilizar reducción de dimensionalidad para hacer recomendaciones. Esta idea propone que hay ciertos factores latentes (no observados) que describen películas con “contenido implícito similar”, y usuarios según su interés en esa dimensión.\nOtra manera de llamar estos factores latentes es embedding: buscamos un embedding (una representación numérica en cierta dimensión no muy alta) que nos permita predecir el gusto de un usuario por una película.\nEste método nos permitirá también controlar mejor los resultados ruidosos que obtuvimos en los ejemplos anteriores (usando regularización y reducción de dimensión).\n\n16.1.1 Ejemplo: una dimensión latente\nPor ejemplo: consideramos una dimensión de películas serias contra películas divertidas. \\(3\\) películas podrían describirse con\n\\[v=(-2,0,1)\\],\nlo que interpretamos como la película \\(1\\) es divertida (negativa en seriedad-diversión), la película \\(2\\) está en promedio, y la película \\(3\\) es más seria que las dos anteriores.\nPor otro lado, tenemos descriptores de 5 usuarios:\n\\[u=(2,3,-3,0,1)\\] que dice que a los primeros dos usuarios les gustan las películas serias, al tercero le gustan las divertidas, y los dos últimos no tienen preferencia clara a lo largo de esta dimensión.\nQusiéramos predecir el gusto usando estos dos vectores. Nuestras predicciones (considerando que \\(u\\) y \\(v\\) son matrices de una columna) serían simplemente\n\\[\\widetilde{X} = u v^t\\]\n\nu <- c(2,3,-3,0,1)\nv <- c(-2,0,1)\ngustos <- u %*% t(v)\ngustos\n\n [,1] [,2] [,3]\n[1,] -4 0 2\n[2,] -6 0 3\n[3,] 6 0 -3\n[4,] 0 0 0\n[5,] -2 0 1\n\n\nAsí que al usuario \\(1\\) le recomndamos la película \\(3\\), pero al usuario \\(3\\) le recomendamos la película \\(1\\).\n\nLa idea es entonces encontrar pesos para películas \\(u\\) y para usuarios \\(v\\) de forma que \\(X\\approx \\widetilde{X} = uv^t\\): podemos reproducir las calificaciones observadas a partir de nuestro modelo de factores latentes.\nNótese sin embargo que hay varias dimensiones que pueden describir a películas y usuarios: por ejemplo, seria-divertida, artística-hollywood, ciencia ficción, con/sin violencia, etc. Podemos proponer más dimensiones latentes de la siguiente forma:\n\n\n16.1.2 Ejemplo: dos dimensiones latentes\nTenemos la dimensión anterior de seria-divertida\n\nv_1 <- c(-2,0,1)\nu_1 <- c(2,3,-3,0,1)\n\nY supongamos que tenemos otra dimensión con violencia - sin violencia\n\nv_2 <- c(-3,2,2)\nu_2 <- c(-3,-3,0,-2,4)\n\nQue quiere decir que las películas \\(2, 3\\) tienen volencia, pero la película \\(1\\) no. Por otra parte, a los usuarios \\(1,2\\) y \\(5\\) no les gustan las películas con violencia, mientras que al usuario \\(5\\) si les gustan.\nLa idea ahora es que el gusto de una persona por una película se escribe como combinación de las dos dimensiones. Por ejemplo, para la persona \\(1\\) tenemos, y la película \\(1\\), empezamos haciendo\n\nu_1[1]*v_1[1]\n\n[1] -4\n\nu_2[1]*v_2[1]\n\n[1] 9\n\n\nlo que quiere decir que el hecho de que la película \\(1\\) no sea seria le resta \\(4\\) en gusto (pues la película \\(1\\) está del lado “divertido”), pero le suma \\(9\\) en gusto, pues es una película sin violencia y esta persona está del lado “sin violencia”.\nSumamos para encontrar el gusto total\n\nu_1[1]*v_1[1] + u_2[1]*v_2[1]\n\n[1] 5\n\n\nPara calcular los gustos sobre todas las personas y películas, haríamos\n\nU <- cbind(u_1, u_2)\nV <- cbind(v_1, v_2)\nU\n\n u_1 u_2\n[1,] 2 -3\n[2,] 3 -3\n[3,] -3 0\n[4,] 0 -2\n[5,] 1 4\n\nV\n\n v_1 v_2\n[1,] -2 -3\n[2,] 0 2\n[3,] 1 2\n\nU %*% t(V)\n\n [,1] [,2] [,3]\n[1,] 5 -6 -4\n[2,] 3 -6 -3\n[3,] 6 0 -3\n[4,] 6 -4 -4\n[5,] -14 8 9\n\n\n\nEl renglón \\(j\\) de \\(U\\) son los valores en las dimensiones latentes para la película \\(i\\) (descriptores de usuarios).\nEl renglón \\(j\\) de \\(V\\) son los valores en las dimensiones latentes para el usuario \\(j\\) (descriptores de películas)\n\n\n\n\n\n\n\nFactorización de matrices para recomendación\n\n\n\nCon \\(k\\) dimensiones latentes, el modelo que proponemos es:\n\\[\\widetilde{X} = UV^t\\]\ndonde \\(U\\) es una matrix de \\(n\\times k\\) (\\(n=\\) número de usuarios), y \\(V\\) es una matriz de \\(p \\times k\\), donde \\(p\\) es el número de películas.\nBuscamos que, si \\(X\\) son las verdaderas calificaciones, entonces \\[X\\approx \\widetilde{X}.\\]\ny nótese que esta aproximación es en el sentido de las entradas de \\(X\\) que son observadas. Sin embargo, \\(\\widetilde{X}\\) nos da predicciones para todos los pares película-persona.\n\n\nBajo este modelo, la predicción para el usuario \\(i\\) y la película \\(j\\) es la siguiente suma sobre las dimensiones latentes:\n\\[\\widetilde{x}_{ij} =\\sum_k u_{ik} v_{jk}\\]\nque expresa el hecho de que el gusto de \\(i\\) por \\(j\\) depende de una combinación (suma) de factores latentes de películas ponderados por gusto por esos factores del usuario.\nEl número de factores latentes \\(k\\) debe ser seleccionado (por ejemplo, según el error de validación). Dado \\(k\\), para encontrar \\(U\\) y \\(V\\) (un total de \\(k(n+p)\\) parámetros) buscamos minimizar\n\\[\\sum_{(i,j)\\, obs} (x_{ij}-\\widetilde{x}_{ij})^2,\\]\nque también podemos escribir este problema (recuérdese que \\(u_i\\) y \\(v_j\\) aquí son vectores renglón) como\n\\[\\min_{U,V}\\sum_{(i,j)\\, obs} (x_{ij}-u_iv_j^t)^2\\] donde \\(u_i\\) es el renglón \\(i\\)-esimo de \\(U\\) (gustos latentes del usuario \\(i\\) en cada dimensión), y \\(v_j\\) es el renglón \\(j\\)-ésimo de la matriz \\(V\\) (calificación latente de la película en cada dimensión)\n\n\n\n\n\n\n¿Por qué funciona la idea de factores latentes?\n\n\n\n\nEl método de factorización de matrices de grado bajo (\\(k\\)) funciona compartiendo información a lo largo de películas y usuarios. Como tenemos que ajustar los datos observados, y solo tenemos a nuestra disposición \\(k\\) descriptores para cada película y usuario, una minimización exitosa captura regularidades en los datos.\nEs importante que la representación sea de grado relativamente bajo, pues esta “compresión” es la que permite que las dimensiones latentes capturen regularidades que están en los datos observados (que esperamos encontrar en el proceso de ajuste).\nAl reducir la dimensión, también funcionan mejor métricas relativamente simples para calcular similitud entre usuarios o películas.\n\n\n\nPor ejemplo, supongamos que el gusto por las películas sólo depende de una dimensión sería - divertida. Si ajustamos un modelo de un solo factor latente, un mínimo se alcanzaría separando con la dimensión latente las películas serias de las divertidas, y los usuarios que prefieren películas serias o divertidas. Esta sería una buena explicación de los datos observados, y las predicciones para películas no vistas sería buena usando simplemente el valor en seriedad de la película (extraída de otras personas con gustos divertido o serio) y el gusto por seriedad de esa persona (extraida de la observación de que le gustan otras películas serias u otras divertidas).\n\n\n16.1.3 Combinación con modelo base\nPodemos usar también ideas de nuestro modelo base y modelar desviaciones en lugar de calificaciones directamente:\nSi \\(X^0\\) son las predicciones del modelo de referencia, y \\[R = X-X^0\\] son los residuales del modelo base, buscamos mejor \\[R\\approx \\widetilde{X} = UV^t\\] de manera que las predicciones finales son \\[X^0 + \\widetilde{X}\\]\nVeremos también más adelante cómo regularizar estos sesgos como parte de la construcción del modelo." + "text": "16.1 Sistemas de recomendación\nEn esta parte, consideramos la idea de utilizar reducción de dimensionalidad para hacer recomendaciones. Esta idea propone que hay ciertos factores latentes (no observados) que describen películas con “contenido implícito similar”, y usuarios según su interés en esa dimensión.\nOtra manera de llamar estos factores latentes es embedding: buscamos un embedding (una representación numérica en cierta dimensión no muy alta) que nos permita predecir el gusto de un usuario por una película.\nEste método nos permitirá también controlar mejor los resultados ruidosos que obtuvimos en los ejemplos anteriores (usando regularización y reducción de dimensión).\n\n16.1.1 Ejemplo: una dimensión latente\nPor ejemplo: consideramos una dimensión de películas serias contra películas divertidas. \\(3\\) películas podrían describirse con\n\\[v=(-2,0,1)\\],\nlo que interpretamos como la película \\(1\\) es divertida (negativa en seriedad-diversión), la película \\(2\\) está en promedio, y la película \\(3\\) es más seria que las dos anteriores.\nPor otro lado, tenemos descriptores de 5 usuarios:\n\\[u=(2,3,-3,0,1)\\] que dice que a los primeros dos usuarios les gustan las películas serias, al tercero le gustan las divertidas, y los dos últimos no tienen preferencia clara a lo largo de esta dimensión.\nQusiéramos predecir el gusto usando estos dos vectores. Nuestras predicciones (considerando que \\(u\\) y \\(v\\) son matrices de una columna) serían simplemente\n\\[\\widetilde{X} = u v^t\\]\n\nu <- c(2,3,-3,0,1)\nv <- c(-2,0,1)\ngustos <- u %*% t(v)\ngustos\n\n [,1] [,2] [,3]\n[1,] -4 0 2\n[2,] -6 0 3\n[3,] 6 0 -3\n[4,] 0 0 0\n[5,] -2 0 1\n\n\nAsí que al usuario \\(1\\) le recomndamos la película \\(3\\), pero al usuario \\(3\\) le recomendamos la película \\(1\\).\n\nLa idea es entonces encontrar pesos para películas \\(u\\) y para usuarios \\(v\\) de forma que \\(X\\approx \\widetilde{X} = uv^t\\): podemos reproducir las calificaciones observadas a partir de nuestro modelo de factores latentes.\nNótese sin embargo que hay varias dimensiones que pueden describir a películas y usuarios: por ejemplo, seria-divertida, artística-hollywood, ciencia ficción, con/sin violencia, etc. Podemos proponer más dimensiones latentes de la siguiente forma:\n\n\n16.1.2 Ejemplo: dos dimensiones latentes\nTenemos la dimensión anterior de seria-divertida\n\nv_1 <- c(-2,0,1)\nu_1 <- c(2,3,-3,0,1)\n\nY supongamos que tenemos otra dimensión con violencia - sin violencia\n\nv_2 <- c(-3,2,2)\nu_2 <- c(-3,-3,0,-2,4)\n\nQue quiere decir que las películas \\(2, 3\\) tienen volencia, pero la película \\(1\\) no. Por otra parte, a los usuarios \\(1,2\\) y \\(5\\) no les gustan las películas con violencia, mientras que al usuario \\(5\\) si les gustan.\nLa idea ahora es que el gusto de una persona por una película se escribe como combinación de las dos dimensiones. Por ejemplo, para la persona \\(1\\) tenemos, y la película \\(1\\), empezamos haciendo\n\nu_1[1]*v_1[1]\n\n[1] -4\n\nu_2[1]*v_2[1]\n\n[1] 9\n\n\nlo que quiere decir que el hecho de que la película \\(1\\) no sea seria le resta \\(4\\) en gusto (pues la película \\(1\\) está del lado “divertido”), pero le suma \\(9\\) en gusto, pues es una película sin violencia y esta persona está del lado “sin violencia”.\nSumamos para encontrar el gusto total\n\nu_1[1]*v_1[1] + u_2[1]*v_2[1]\n\n[1] 5\n\n\nPara calcular los gustos sobre todas las personas y películas, haríamos\n\nU <- cbind(u_1, u_2)\nV <- cbind(v_1, v_2)\nU\n\n u_1 u_2\n[1,] 2 -3\n[2,] 3 -3\n[3,] -3 0\n[4,] 0 -2\n[5,] 1 4\n\nV\n\n v_1 v_2\n[1,] -2 -3\n[2,] 0 2\n[3,] 1 2\n\nU %*% t(V)\n\n [,1] [,2] [,3]\n[1,] 5 -6 -4\n[2,] 3 -6 -3\n[3,] 6 0 -3\n[4,] 6 -4 -4\n[5,] -14 8 9\n\n\n\nEl renglón \\(j\\) de \\(U\\) son los valores en las dimensiones latentes para la película \\(i\\) (descriptores de usuarios).\nEl renglón \\(j\\) de \\(V\\) son los valores en las dimensiones latentes para el usuario \\(j\\) (descriptores de películas)\n\n\n\n\n\n\n\nFactorización de matrices para recomendación\n\n\n\nCon \\(k\\) dimensiones latentes, el modelo que proponemos es:\n\\[\\widetilde{X} = UV^t\\]\ndonde \\(U\\) es una matrix de \\(n\\times k\\) (\\(n=\\) número de usuarios), y \\(V\\) es una matriz de \\(p \\times k\\), donde \\(p\\) es el número de películas.\nBuscamos que, si \\(X\\) son las verdaderas calificaciones, entonces \\[X\\approx \\widetilde{X}.\\]\ny nótese que esta aproximación es en el sentido de las entradas de \\(X\\) que son observadas. Sin embargo, \\(\\widetilde{X}\\) nos da predicciones para todos los pares película-persona.\n\n\nBajo este modelo, la predicción para el usuario \\(i\\) y la película \\(j\\) es la siguiente suma sobre las dimensiones latentes:\n\\[\\widetilde{x}_{ij} =\\sum_k u_{ik} v_{jk}\\]\nque expresa el hecho de que el gusto de \\(i\\) por \\(j\\) depende de una combinación (suma) de factores latentes de películas ponderados por gusto por esos factores del usuario.\nEl número de factores latentes \\(k\\) debe ser seleccionado (por ejemplo, según el error de validación). Dado \\(k\\), para encontrar \\(U\\) y \\(V\\) (un total de \\(k(n+p)\\) parámetros) buscamos minimizar\n\\[\\sum_{(i,j)\\, obs} (x_{ij}-\\widetilde{x}_{ij})^2,\\]\nque también podemos escribir este problema (recuérdese que \\(u_i\\) y \\(v_j\\) aquí son vectores renglón) como\n\\[\\min_{U,V}\\sum_{(i,j)\\, obs} (x_{ij}-u_iv_j^t)^2\\] donde \\(u_i\\) es el renglón \\(i\\)-esimo de \\(U\\) (gustos latentes del usuario \\(i\\) en cada dimensión), y \\(v_j\\) es el renglón \\(j\\)-ésimo de la matriz \\(V\\) (calificación latente de la película en cada dimensión)\n\n\n\n\n\n\n¿Por qué funciona la idea de factores latentes?\n\n\n\n\nEl método de factorización de matrices de grado bajo (\\(k\\)) funciona compartiendo información a lo largo de películas y usuarios. Como tenemos que ajustar los datos observados, y solo tenemos a nuestra disposición \\(k\\) descriptores para cada película y usuario, una minimización exitosa captura regularidades en los datos.\nEs importante que la representación sea de grado relativamente bajo, pues esta “compresión” es la que permite que las dimensiones latentes capturen regularidades que están en los datos observados (que esperamos encontrar en el proceso de ajuste).\nAl reducir la dimensión, también funcionan mejor métricas relativamente simples para calcular similitud entre usuarios o películas.\n\n\n\nPor ejemplo, supongamos que el gusto por las películas sólo depende de una dimensión sería - divertida. Si ajustamos un modelo de un solo factor latente, un mínimo se alcanzaría separando con la dimensión latente las películas serias de las divertidas, y los usuarios que prefieren películas serias o divertidas. Esta sería una buena explicación de los datos observados, y las predicciones para películas no vistas sería buena usando simplemente el valor en seriedad de la película (extraída de otras personas con gustos divertido o serio) y el gusto por seriedad de esa persona (extraida de la observación de que le gustan otras películas serias u otras divertidas).\n\n\n16.1.3 Combinación con modelo base\nPodemos usar también ideas de nuestro modelo base y modelar desviaciones en lugar de calificaciones directamente:\nSi \\(X^0\\) son las predicciones del modelo de referencia (la media de la película más un ajuste que es la diferencia de la media de cada individuo menos la media global), y \\[R = X-X^0\\] son los residuales del modelo base, buscamos mejor \\[R\\approx \\widetilde{X} = UV^t\\] de manera que las predicciones finales son \\[X^0 + \\widetilde{X}\\]\nVeremos también más adelante cómo regularizar estos sesgos como parte de la construcción del modelo." }, { "objectID": "16-recom-factorizacion.html#factorización-de-matrices", @@ -762,40 +762,47 @@ "section": "16.3 Mínimos cuadrados alternados", "text": "16.3 Mínimos cuadrados alternados\nSupongamos entonces que queremos encontrar matrices \\(U\\) y \\(V\\), donde \\(U\\) es una matrix de \\(n \\times k\\) (\\(n=\\) número de usuarios), y \\(V\\) es una matriz de \\(p \\times k\\), donde \\(p\\) es el número de películas que nos de una aproximación de la matrix \\(X\\) de calificaciones \\[\nX \\approx UV^t\n\\] Ahora supongamos que conocemos \\(V_1\\). Si este es el caso, entonces queremos resolver para \\(U_1\\): \\[ \\min_{U_1}|| X - U_1V_1^t||_{obs}^2\\] Como \\(V_1^t\\) están fijas, este es un problema de mínimos cuadrados usual, y puede resolverse analíticamente (o usar descenso en gradiente, que es simple de calcular de forma analítica) para encontrar \\(U_1\\). Una vez que encontramos \\(U_1\\), la fijamos, e intentamos ahora resolver para \\(V\\):\n\\[ \\min_{V_2}|| X - U_1V_2^t||_{obs}^2\\] Y una vez que encontramos \\(V_2\\) resolvemos\n\\[ \\min_{U_2}|| X - U_2V_2^t||_{obs}^2\\]\nContinuamos este proceso hasta encontrar un mínimo local o hasta cierto número de iteraciones. Para inicializar \\(V_1\\), en (Zhou et al. 2008) se recomienda tomar como primer renglón el promedio de las calificaciones de las películas, y el resto números aleatorios chicos (por ejemplo \\(U(0,1)\\)). También pueden inicializarse con números aleatorios chicos las dos matrices.\n\n16.3.1 Mínimos cuadrados alternados con regularización\nPara agregar regularización y lidiar con los datos ralos, podemos incluir un coeficiente adicional. Minimizamos entonces (como en (Zhou et al. 2008)):\n\\[\\min_{U,V}\\sum_{(i,j)\\, obs} (x_{ij}-u_i^tv_j)^2 +\n\\lambda \\left ( \\sum_i n_{i}||u_i||^2 + \\sum_j m_{j} ||v_j||^2 \\right)\\]\ny modificamos de manera correspondiente cada paso de mínimos cuadrados mostrado arriba. \\(n_{i}\\) es el número de evaluaciones del usuario \\(i\\), y \\(m_j\\) es el número de evaluaciones de la película \\(j\\).\nObservaciones:\n\nNótese que penalizamos el tamaños de los vectores \\(u_i\\) y \\(v_j\\) para evitar sobreajuste (como en regresión ridge).\nNótese también que los pesos \\(n_i\\) y \\(m_j\\) en esta regularización hace comparables el término que aparece en la suma de los residuales al cuadrado (la primera suma), y el término de regularización: por ejemplo, si el usuario \\(i\\) hizo \\(n_i\\) evaluaciones, entonces habrá \\(n_i\\) términos en la suma de la izquierda. Lo mismo podemos decir acerca de las películas.\nEste no es el único término de regularización posible. Por ejemplo, podríamos no usar los pesos \\(n_i\\) y \\(m_j\\), y obtendríamos un esquema razonable también, donde hay más regularización relativa para usuarios/películas que tengan pocas evaluaciones.\n\nEste método está implementado en spark. La implementación está basada parcialmente en (Zhou et al. 2008). La inicialización es diferente en spark, ver el código, donde cada renglón se inicializa con un vector de \\(N(0,1)\\) normalizado." }, + { + "objectID": "16-recom-factorizacion.html#búsqueda-por-similitud-y-hashing", + "href": "16-recom-factorizacion.html#búsqueda-por-similitud-y-hashing", + "title": "16  Dimensiones latentes: embeddings", + "section": "16.4 Búsqueda por similitud y Hashing", + "text": "16.4 Búsqueda por similitud y Hashing\nCuando tenemos las representaciones de personas/artículos, muchas veces los sistemas de recomendación pueden requerir buscar personas o artículos similares a uno dado (por ejemplo, para hacer una playlist basada en una canción, o mostrar artículos relacionados). Con este fin, podemos definir una medida de similitud entre vectores, por ejemplo, distancia euclideana o distancia coseno (ángulo entre los vectores).\nEl objetivo es entonces, dado el embedding (vector) de un artículo, por ejemplo, encontrar los 10 vectores más cercanos según nuestra medida de similitud. Este problema no es trivial si el espacio de artículos es muy grande, pues requiere recorrer todos los vectores y calcular la similitud. Igualmente, precalcular todas las similitudes es costoso en tiempo y espacio, pues requiere \\(O(n^2)\\) operaciones (considerar todos los pares posibles).\nUna solución aproximada que se utiliza con frecuencia (ver por ejemplo esta librería de Spotify) cuando buscamos encontrar elementos muy similares comienza utilizando reducción de dimensionalidad utilizando proyecciones aleatorias, y formando cubetas de candidatos de similitud.\nLa idea general es como sigue: supongamos que utilizamos la similitud coseno. Escogemos al azar \\(k\\) vectores \\(w_1,\\ldots, w_k\\) en el espacio de embeddings que nos interesa. Si tomamos un artículo dado representado por el vector \\(v\\), entonces para cada \\(j\\) consideramos:\n\nSi el producto punto \\(w_j^t v\\) es positivo, ponemos \\(f_j(v) = 1\\)\nEn otro caso ponemos \\(f_j(v) = -1\\).\n\nCon esto obtenemos otra representación discreta de \\(v\\) como vector \\((f_1(v),f_2(v),\\dots, f_k(v) )\\), que es una representación más pequeña (podemos codificar como entero por ejemplo). Agrupamos todos los artículos que tienen la misma representación en una misma cubeta. Así, cuando queremos buscar los artículos muy similares, sólo es necesario buscar en la cubeta correspondiente.\nEstas funciones que transforman vectores en valores 1 o -1 se llaman, por su naturaleza aleatoria funciones hash. Construimos ahora nuestra función generadora de hashes:\n\ngen_hash <- function(p){\n # p es la dimensión del espacio\n v <- rnorm(p)\n # devolvemos una función que calcula la cubeta:\n function(x){\n sum(x * v) |> sign()\n }\n}\nset.seed(823)\nhash_1 <- gen_hash(2)\n# los hashes de dos puntos:\nhash_1(c(4, 7))\n\n[1] -1\n\nhash_1(c(-4, 7))\n\n[1] 1\n\n# el vector que escogimos es\nenvironment(hash_1)$v\n\n[1] -0.9929091 -0.4400476\n\n\n\nEjemplo\nLa siguiente función genera dos clusters de puntos mezclados con puntos distribuidos normales con desviación estándar relativamente grande\n\nset.seed(1021)\nsimular_puntos <- function(d = 2, n = 200){\n #puntos muy cercanos a (3,3,..., 3):\n mat_1 <- matrix(rnorm(10 * d, sd = 0.01) + 3, ncol = d)\n #puntos muy cercanos a (-3,-3,..., -3):\n mat_2 <- matrix(rnorm(10 * d, sd = 0.01) - 3, ncol = d)\n # puntos distribuidos alrededor del origen:\n mat_3 <- matrix(rnorm(n * d, sd = 10), ncol = d)\n datos_tbl_vars <- rbind(mat_3, mat_1, mat_2) |> \n as_tibble() |> \n mutate(id_1 = row_number())\n datos_tbl_vars\n}\n# diez puntos en cluster 1, diez en cluster 2, y 100 sin cluster:\ndatos_tbl_vars <- simular_puntos(d = 2, n = 100)\n\nWarning: The `x` argument of `as_tibble.matrix()` must have unique column names if\n`.name_repair` is omitted as of tibble 2.0.0.\nℹ Using compatibility `.name_repair`.\n\nggplot(datos_tbl_vars, aes(x = V1, y= V2)) + \n geom_jitter(width = 0.5, height = 0.5, alpha = 0.3)\n\n\n\n\nPara este ejemplo calculamos las similitudes reales entre todos los pares de puntos (esto generalmente es muy lento para datos grandes):\n\nsim_e <- function(x, y){\n sum(x * y) / sqrt(sum(x^2) * sum(y^2))\n}\ndatos_tbl <- datos_tbl_vars |>\n pivot_longer(-id_1, names_to = \"variable\", values_to = \"valor\") |> \n group_by(id_1) |>\n arrange(variable) |>\n summarise(vec_1 = list(valor))\nsystem.time(\npares_tbl <- datos_tbl |> \n crossing(datos_tbl |> \n rename(id_2 = id_1, vec_2 = vec_1)) |>\n filter(id_1 < id_2) |>\n mutate(sim = map2_dbl(vec_1, vec_2, sim_e))\n)\n\n user system elapsed \n 0.028 0.000 0.028 \n\npares_tbl |> head()\n\n# A tibble: 6 × 5\n id_1 vec_1 id_2 vec_2 sim\n <int> <list> <int> <list> <dbl>\n1 1 <dbl [2]> 2 <dbl [2]> 0.925 \n2 1 <dbl [2]> 3 <dbl [2]> 0.0689\n3 1 <dbl [2]> 4 <dbl [2]> -0.127 \n4 1 <dbl [2]> 5 <dbl [2]> -1.00 \n5 1 <dbl [2]> 6 <dbl [2]> 0.994 \n6 1 <dbl [2]> 7 <dbl [2]> 0.486 \n\n\n\nnrow(pares_tbl)\n\n[1] 7140\n\n\nSupongamos que queremos encontrar los pares con similitud mayor a 0.999:\n\npares_sim <- pares_tbl |> filter(sim > 0.999)\nnrow(pares_sim)\n\n[1] 228\n\n\nAhora veremos cómo encontrar estos pares de puntos cercanos.\n\n\nCálculo de firmas\nUsaremos 4 hashes:\n\n#generar hashes\nset.seed(88)\nhash_f <- map(1:4, ~ gen_hash(p = 2))\n# esta es una función de conveniencia:\ncalculador_hashes <- function(hash_f){\n function(z) {\n map_int(hash_f, ~ .x(z))\n }\n}\ncalc_hashes <- calculador_hashes(hash_f)\n\nCalculamos las firmas:\n\nfirmas_tbl <- datos_tbl_vars |> \n pivot_longer(cols = -id_1, names_to = \"variable\", values_to = \"valor\") |> \n group_by(id_1) |> \n summarise(vec_1 = list(valor)) |> \n mutate(firma = map(vec_1, ~ calc_hashes(.x))) |> \n select(id_1, firma)\nfirmas_tbl\n\n# A tibble: 120 × 2\n id_1 firma \n <int> <list> \n 1 1 <int [4]>\n 2 2 <int [4]>\n 3 3 <int [4]>\n 4 4 <int [4]>\n 5 5 <int [4]>\n 6 6 <int [4]>\n 7 7 <int [4]>\n 8 8 <int [4]>\n 9 9 <int [4]>\n10 10 <int [4]>\n# ℹ 110 more rows\n\nfirmas_tbl$firma[[1]]\n\n[1] -1 1 -1 1\n\nfirmas_tbl$firma[[2]]\n\n[1] -1 1 -1 1\n\n\nPara este ejemplo, consideraremos todos los pares que coinciden en todas las cubetas\n\ncubetas_tbl <- firmas_tbl |> \n mutate(cubeta = map_chr(firma, ~ paste(.x, collapse = \"/\"))) \ncubetas_tbl\n\n# A tibble: 120 × 3\n id_1 firma cubeta \n <int> <list> <chr> \n 1 1 <int [4]> -1/1/-1/1 \n 2 2 <int [4]> -1/1/-1/1 \n 3 3 <int [4]> 1/-1/-1/-1\n 4 4 <int [4]> -1/1/1/1 \n 5 5 <int [4]> 1/-1/1/-1 \n 6 6 <int [4]> -1/1/-1/1 \n 7 7 <int [4]> 1/-1/-1/1 \n 8 8 <int [4]> 1/-1/-1/1 \n 9 9 <int [4]> 1/-1/-1/1 \n10 10 <int [4]> -1/1/1/1 \n# ℹ 110 more rows\n\n\nAhora agrupamos cubetas y filtramos las que tienen más de un elemento\n\ncubetas_tbl <- cubetas_tbl |> group_by(cubeta) |> \n summarise(ids = list(id_1), n = length(id_1)) |> \n filter(n > 1)\ncubetas_tbl\n\n# A tibble: 8 × 3\n cubeta ids n\n <chr> <list> <int>\n1 -1/-1/-1/1 <int [20]> 20\n2 -1/1/-1/1 <int [16]> 16\n3 -1/1/1/-1 <int [5]> 5\n4 -1/1/1/1 <int [20]> 20\n5 1/-1/-1/-1 <int [16]> 16\n6 1/-1/-1/1 <int [8]> 8\n7 1/-1/1/-1 <int [16]> 16\n8 1/1/1/-1 <int [19]> 19\n\n\nY finalmente, extraemos los pares candidatos:\n\ncandidatos_tbl <- \n cubetas_tbl |> \n mutate(pares_cand = map(ids, ~ combn(.x, 2, simplify = FALSE))) |> \n select(cubeta, pares_cand) |> \n unnest(pares_cand) |> \n unnest_wider(pares_cand, names_sep = \"_\") |> \n select(-cubeta) |> \n unique()\ncandidatos_tbl\n\n# A tibble: 949 × 2\n pares_cand_1 pares_cand_2\n <int> <int>\n 1 15 18\n 2 15 22\n 3 15 35\n 4 15 37\n 5 15 38\n 6 15 60\n 7 15 63\n 8 15 66\n 9 15 82\n10 15 111\n# ℹ 939 more rows\n\n\n\nnrow(candidatos_tbl)\n\n[1] 949\n\n\nEn este caso, seguramente tenemos algunos falsos positivos que tenemos que filtrar, y quizá algunos falsos negativos.\nCalculamos distancias para todos los pares candidatos (es una lista mucho más corta generalmente):\n\npuntos_tbl <- datos_tbl_vars |> \n pivot_longer(V1:V2) |>\n group_by(id_1) |> \n select(-name) |> \n summarise(punto = list(value))\ncandidatos_tbl <- \n candidatos_tbl |> \n left_join(puntos_tbl |> rename(pares_cand_1 = id_1, punto_1 = punto)) |> \n left_join(puntos_tbl |> rename(pares_cand_2 = id_1, punto_2 = punto))\n\nJoining with `by = join_by(pares_cand_1)`\nJoining with `by = join_by(pares_cand_2)`\n\ncandidatos_tbl\n\n# A tibble: 949 × 4\n pares_cand_1 pares_cand_2 punto_1 punto_2 \n <int> <int> <list> <list> \n 1 15 18 <dbl [2]> <dbl [2]>\n 2 15 22 <dbl [2]> <dbl [2]>\n 3 15 35 <dbl [2]> <dbl [2]>\n 4 15 37 <dbl [2]> <dbl [2]>\n 5 15 38 <dbl [2]> <dbl [2]>\n 6 15 60 <dbl [2]> <dbl [2]>\n 7 15 63 <dbl [2]> <dbl [2]>\n 8 15 66 <dbl [2]> <dbl [2]>\n 9 15 82 <dbl [2]> <dbl [2]>\n10 15 111 <dbl [2]> <dbl [2]>\n# ℹ 939 more rows\n\n\n\npares_similares_tbl <- \n candidatos_tbl |> \n mutate(sim = map2_dbl(punto_1, punto_2, sim_e)) |> \n filter(sim > 0.999)\n\n\nnrow(pares_similares_tbl)\n\n[1] 224\n\n\n\n\nProbando con datos de “gold standard”\nEn este caso, sabemos cuáles son los pares que buscamos, así que podemos evaluar nuestro método:\n\nverdadero_pos <- nrow(inner_join(pares_similares_tbl, pares_sim))\n\nJoining with `by = join_by(sim)`\n\nverdadero_pos\n\n[1] 224\n\n\n\nsensibilidad <- verdadero_pos / nrow(pares_sim)\nsensibilidad\n\n[1] 0.9824561\n\nprecision <- verdadero_pos / nrow(pares_similares_tbl)\nprecision\n\n[1] 1\n\n\nComo vemos, la precisión es 1 y la sensibilidad es alta. Nos faltó encontrar una pequeña parte de los pares similares.\n\nSi queremos ser más exhaustivos (con el mayor cómputo que implica), podemos utilizar menos hashes, pero el tiempo de cómputo aumenta.\n\n\n\n\n\n\n\nHashing y similitud\n\n\n\nEste método de hashing se llama más generalmente Locality Sensitive Hashing, y se utiliza para encontrar de manera eficiente pares de puntos similares en embeddings.\nEn lugar de sólo construir cubetas, también es posible almacenar la salida en estructura de árbol para hacer más eficientes las búsquedas." + }, { "objectID": "16-recom-factorizacion.html#retroalimentación-implícita", "href": "16-recom-factorizacion.html#retroalimentación-implícita", "title": "16  Dimensiones latentes: embeddings", - "section": "16.4 Retroalimentación implícita", - "text": "16.4 Retroalimentación implícita\nEsta sección está basada en (Hu, Koren, y Volinsky 2008).\nEn el ejemplo que vimos arriba, la retroalimentación es expícita en el sentido de que los usuarios califican los artículos (\\(1-\\) no me gustó nada, hasta \\(5-\\) me gustó mucho). Sin embargo, es común encontrar casos donde no existe esta retroalimentación explícita, y solo tenemos medidas del gusto implícito, por ejemplo:\n\nCuántas veces un usuario ha pedido un cierto artículo.\nQué porcentaje del programa fue visto.\nCuánto tiempo pasó en la página web.\nCuántas veces oyó una canción.\n\nEstos datos tienen la ventaja de que describen acciones del usuario, en lugar de un rating que puede estar influido por sesgos de imagen o de la calificación que “debería” tener un artículo además de la preferencia: quizá disfruto muchísimo Buffy the Vampire Slayer, pero lo califico con un \\(3\\), aunque un documental de ballenas que simplemente me gustó le pongo un \\(5\\). En los datos implícitos se vería de todas formas mi consumo frecuente de Buffy the Vampire Slayer, y quizá unos cuantos de documentales famosos.\nSea \\(r_{ij}\\) una medida implícita como las mencionadas arriba, para el usuario \\(i\\) y el artículo \\(j\\). Ponemos \\(r_{i,j}=0\\) cuando no se ha observado interacción entre este usuario y el artículo.\nUna diferencia importante con los ratings explícitos es que los datos implícitos son en un sentido menos informativos que los explícitos:\n\nPuede ser que el valor de \\(r_{ij}\\) sea relativamente bajo (pocas interacciones), pero de todas formas se trate de un artículo que es muy preferido (por ejemplo, solo vi Star Wars I una vez, pero me gusta mucho, o nunca he encontrado Star Wars I en el catálogo). Esto no pasa con los ratings, pues ratings bajos indican baja preferencia.\nSin embargo, estamos seguros de que niveles altos de interacción (oyó muchas veces una canción, etc.), es indicación de preferencia alta.\nUsualmente la medida \\(r_{ij}\\) no tiene faltantes, o tiene un valor implícito para faltantes. Por ejemplo, si la medida es % de la película que vi, todas las películas con las que no he interactuado tienen \\(r_{ij}=0\\).\n\nAsí que en nuestro modelo no necesariamente queremos predecir directamente la variable \\(r_{ij}\\): puede haber artículos con predicción baja de \\(r_{ij}\\) que descubramos de todas formas van a ser altamente preferidos. Un modelo que haga una predicción de \\(r_{îj}\\) reflejaría más los patrones de consumo actual en lugar de permitirnos descubrir artículos preferidos con los que no necesariamente existe interacción.\n\n16.4.1 Ejemplo\nConsideremos los siguientes usuarios, donde medimos por ejemplo el número de minutos que pasó cada usuario viendo cada película:\n\nimp <- tibble(usuario = 1:6,\n StarWars1 = c(0, 0, 0, 150, 300, 250),\n StarWars2 = c(250, 200, 0, 200, 220,180), \n StarWars3 = c(0, 250, 300, 0, 0, 0),\n Psycho = c(5, 1, 0, 0, 0, 2)) \nimp\n\n# A tibble: 6 × 5\n usuario StarWars1 StarWars2 StarWars3 Psycho\n <int> <dbl> <dbl> <dbl> <dbl>\n1 1 0 250 0 5\n2 2 0 200 250 1\n3 3 0 0 300 0\n4 4 150 200 0 0\n5 5 300 220 0 0\n6 6 250 180 0 2\n\n\nQuiséramos encontrar una manera de considerar los 0’s como información más suave (es decir, alguien puede tener valores bajos de interacción con una película, pero esto no implica necesariamente que no sea preferida). Esto implica que es más importante ajustar los valores altos del indicador implícito de preferencia.\n\nUna solución propuesta en (Hu, Koren, y Volinsky 2008) (e implementada en spark) es darle menos importancia al valor \\(r_{ij}\\) en la construcción de los factores latentes, especialmente si tiene valores bajos.\nPara hacer esto, primero definimos la variable de preferencia\n\\[p_{ij} =\n\\begin{cases}\n1, &\\mbox{si } r_{ij}>0,\\\\\n0, &\\mbox{si } r_{ij}=0.\\\\\n\\end{cases}\\]\nEsta variable \\(p_{ij}\\), cuando vale uno, indica algún nivel de confianza en la preferencia. ¿Pero qué tanto valor debemos darle a esta preferencia? Definimos la confianza como \\[c_{ij} = 1+ \\alpha r_{ui},\\] donde \\(\\alpha\\) es un parámetro que hay que afinar (por ejemplo \\(\\alpha\\) entre \\(1\\) y \\(50\\)). Para predicciones de vistas de TV, en (Hu, Koren, y Volinsky 2008) utilizan \\(\\alpha = 40\\), donde \\(r_{ij}\\) es el número de veces que el usuario ha visto un programa (contando vistas parciales, así que es un número real).\nLa función objetivo (sin regularización) se define como\n\\[\nL = \\sum_{(i,j)} c_{ij}(p_{ij} - \\sum_{l=1}^k u_{i,l}v_{j,l})^2\n\\tag{16.1}\\]\nNótese que :\n\nCuando \\(c_ij\\) es alta (porque \\(r_{i,j}\\) es alta), para minimizar esta cantidad tenemos que hacer la predicción de \\(p_{ij}\\) cercana a 1, pues el error se multiplica por \\(c_{ij}\\). Sin embargo,\nCuando \\(r_{i,j}\\) es bajo, no es tan importante ajustar esta información con precisión: si \\(p_{ij} = 1\\), puede ser que \\(\\sum_{l=1}^k u_{i,l}v_{j,l}\\) sea muy bajo, y si \\(p_{ij}=0\\), puede ser que \\(\\sum_{l=1}^k u_{i,l}v_{j,l}\\) sea cercano a 1 sin afectar tanto el error.\nEsto permite que en el ajuste podamos descubrir artículos con \\(p_{ij}\\) alta para algún usuario, aún cuando \\(r_{ij}\\) es cero o muy chico.\n\n\n\n16.4.2 Ejemplo\nVeamos cómo se ven soluciones de un factor\n\nimp_mat <- imp |> select(-usuario) |> as.matrix()\nerror_explicito <- function(uv){\n u <- matrix(uv[1:6], ncol = 1)\n v <- matrix(uv[7:10], ncol = 1)\n sum((imp_mat - u %*% t(v))^2)\n}\nerror_implicito <- function(uv){\n u <- matrix(uv[1:6], ncol = 1)\n v <- matrix(uv[7:10], ncol = 1)\n pref_mat <- as.numeric(imp_mat > 0) - u %*% t(v)\n confianza <- 1 + 0.1 * imp_mat\n sum((confianza * pref_mat)^2 )\n}\n\nSi intentamos ajustar los ratings implícitos como si fueran explícitos, obtenemos los siguientes ajustados con un solo factor latente:\n\nuv_inicial <- runif(10)\nopt_exp <- optim(par = uv_inicial, error_explicito, method = \"BFGS\")\nopt_exp$par[7:10]\n\n[1] 10.95575245 13.57814959 3.44180907 0.08839186\n\nt(t(opt_exp$par[1:6])) %*% t(opt_exp$par[7:10]) |> round()\n\n [,1] [,2] [,3] [,4]\n[1,] 118 146 37 1\n[2,] 124 154 39 1\n[3,] 36 44 11 0\n[4,] 151 187 47 1\n[5,] 217 269 68 2\n[6,] 180 223 56 1\n\n\nNótese que esta solución no es muy buena: una componente intenta capturar los patrones de consumo de estas cuatro películas.\nSi usamos preferencias y confianza, obtenemos:\n\nopt_imp <- optim(par = uv_inicial, error_implicito, method = \"BFGS\")\nopt_imp$par[7:10]\n\n[1] 0.8208828 0.8221935 0.8186859 0.5101005\n\nt(t(opt_imp$par[1:6])) %*% t(opt_imp$par[7:10]) |> round(2)\n\n [,1] [,2] [,3] [,4]\n[1,] 1 1 0.99 0.62\n[2,] 1 1 1.00 0.62\n[3,] 1 1 1.00 0.62\n[4,] 1 1 0.99 0.62\n[5,] 1 1 1.00 0.62\n[6,] 1 1 1.00 0.62\n\n\nque indica que la información en esta matriz es consistente con que todos los usuarios tienen preferencia alta por las tres películas de Star Wars, y menos por la cuarta.\n\nIgual que en los ejemplos anteriores, usualmente se agregan términos de regularización para los vectores renglón \\(u_i\\) y \\(v_j\\).\n\n\n16.4.3 Evaluación para modelos implícitos\nLa evaluación para modelos implícitos no es tan simple como en el caso explícito, pues no estamos modelando directamente los valores observados \\(r_{ij}\\). Medidas como RECM o MAD que usamos en el caso explícito no son tan apropiadas para este problema.\nUna alternativa es, para cada usuario \\(i\\), ordenar los artículos de mayor a menor valor de \\(\\hat{p}_{ij} = u_iv_j^t\\) (canciones, pellículas), y calculamos:\n\\[\nrank = \\frac{\\sum_{j} p_{ij}rank_{i,j}}{\\sum_j p_{ij}}\n\\]\ndonde \\(rank_{ij}\\) es el percentil del artículo \\(j\\) en la lista ordenada de artículos. \\(rank_{ij}=0\\) para el mejor artículo, y \\(rank_{ij}=1\\) para el peor. Es decir, obtenemos valores más bajos si observamos que los usuarios interactúan con artículos que están más arriba en el ranking.\nEsta suma es un promedio sobre los rankings del usuario con \\(p_{ij}=1\\), y menores valores son mejores (quiere decir que hubo alguna preferencia por los items con \\(rank_{ij}\\) bajo, es decir, los mejores de nuestra lista predicha. Es posible también hacer un promedio ponderado por \\(r_{ij}\\): \\[\nrank = \\frac{\\sum_{j} r_{ij}rank_{i,j}}{\\sum_j r_{ij}}\n\\]\nque es lo mismo que la ecuación anterior pero ponderando por el interés mostrado en cada artículo con \\(p_{ij}=1\\).\n\nMenores valores de \\(rank\\) son mejores.\nSi escogemos al azar el ordenamiento de los artículos, el valor esperado de \\(rank_{ij}\\) es \\(0.5\\) (en medio de la lista), lo que implica que el valor esperado de \\(rank\\) es \\(0.50\\). Cualquier modelo con \\(rank\\geq 0.5\\) es peor que dar recomendaciones al azar.\n\nEsta cantidad la podemos evaluar en entrenamiento y en validación. Para construir el conjunto de validación podemos hacer:\n\nEscogemos un número de usuarios para validación (por ejemplo \\(20\\%\\))\nPonemos \\(50\\%\\) de los artículos evaluados por estas personas en validación, por ejemplo.\n\nEstas cantidades dependen de cuántos datos tengamos, como siempre, para tener un tamaño razonable de datos de validación." + "section": "16.5 Retroalimentación implícita", + "text": "16.5 Retroalimentación implícita\nEsta sección está basada en (Hu, Koren, y Volinsky 2008).\nEn el ejemplo que vimos arriba, la retroalimentación es expícita en el sentido de que los usuarios califican los artículos (\\(1-\\) no me gustó nada, hasta \\(5-\\) me gustó mucho). Sin embargo, es común encontrar casos donde no existe esta retroalimentación explícita, y solo tenemos medidas del gusto implícito, por ejemplo:\n\nCuántas veces un usuario ha pedido un cierto artículo.\nQué porcentaje del programa fue visto.\nCuánto tiempo pasó en la página web.\nCuántas veces oyó una canción.\n\nEstos datos tienen la ventaja de que describen acciones del usuario, en lugar de un rating que puede estar influido por sesgos de imagen o de la calificación que “debería” tener un artículo además de la preferencia: quizá disfruto muchísimo Buffy the Vampire Slayer, pero lo califico con un \\(3\\), aunque un documental de ballenas que simplemente me gustó le pongo un \\(5\\). En los datos implícitos se vería de todas formas mi consumo frecuente de Buffy the Vampire Slayer, y quizá unos cuantos de documentales famosos.\nSea \\(r_{ij}\\) una medida implícita como las mencionadas arriba, para el usuario \\(i\\) y el artículo \\(j\\). Ponemos \\(r_{i,j}=0\\) cuando no se ha observado interacción entre este usuario y el artículo.\nUna diferencia importante con los ratings explícitos es que los datos implícitos son en un sentido menos informativos que los explícitos:\n\nPuede ser que el valor de \\(r_{ij}\\) sea relativamente bajo (pocas interacciones), pero de todas formas se trate de un artículo que es muy preferido (por ejemplo, solo vi Star Wars I una vez, pero me gusta mucho, o nunca he encontrado Star Wars I en el catálogo). Esto no pasa con los ratings, pues ratings bajos indican baja preferencia.\nSin embargo, estamos seguros de que niveles altos de interacción (oyó muchas veces una canción, etc.), es indicación de preferencia alta.\nUsualmente la medida \\(r_{ij}\\) no tiene faltantes, o tiene un valor implícito para faltantes. Por ejemplo, si la medida es % de la película que vi, todas las películas con las que no he interactuado tienen \\(r_{ij}=0\\).\n\nAsí que en nuestro modelo no necesariamente queremos predecir directamente la variable \\(r_{ij}\\): puede haber artículos con predicción baja de \\(r_{ij}\\) que descubramos de todas formas van a ser altamente preferidos. Un modelo que haga una predicción de \\(r_{îj}\\) reflejaría más los patrones de consumo actual en lugar de permitirnos descubrir artículos preferidos con los que no necesariamente existe interacción.\n\n16.5.1 Ejemplo\nConsideremos los siguientes usuarios, donde medimos por ejemplo el número de minutos que pasó cada usuario viendo cada película:\n\nimp <- tibble(usuario = 1:6,\n StarWars1 = c(0, 0, 0, 150, 300, 250),\n StarWars2 = c(250, 200, 0, 200, 220,180), \n StarWars3 = c(0, 250, 300, 0, 0, 0),\n Psycho = c(5, 1, 0, 0, 0, 2)) \nimp\n\n# A tibble: 6 × 5\n usuario StarWars1 StarWars2 StarWars3 Psycho\n <int> <dbl> <dbl> <dbl> <dbl>\n1 1 0 250 0 5\n2 2 0 200 250 1\n3 3 0 0 300 0\n4 4 150 200 0 0\n5 5 300 220 0 0\n6 6 250 180 0 2\n\n\nQuiséramos encontrar una manera de considerar los 0’s como información más suave (es decir, alguien puede tener valores bajos de interacción con una película, pero esto no implica necesariamente que no sea preferida). Esto implica que es más importante ajustar los valores altos del indicador implícito de preferencia.\n\nUna solución propuesta en (Hu, Koren, y Volinsky 2008) (e implementada en spark) es darle menos importancia al valor \\(r_{ij}\\) en la construcción de los factores latentes, especialmente si tiene valores bajos.\nPara hacer esto, primero definimos la variable de preferencia\n\\[p_{ij} =\n\\begin{cases}\n1, &\\mbox{si } r_{ij}>0,\\\\\n0, &\\mbox{si } r_{ij}=0.\\\\\n\\end{cases}\\]\nEsta variable \\(p_{ij}\\), cuando vale uno, indica algún nivel de confianza en la preferencia. ¿Pero qué tanto valor debemos darle a esta preferencia? Definimos la confianza como \\[c_{ij} = 1+ \\alpha r_{ui},\\] donde \\(\\alpha\\) es un parámetro que hay que afinar (por ejemplo \\(\\alpha\\) entre \\(1\\) y \\(50\\)). Para predicciones de vistas de TV, en (Hu, Koren, y Volinsky 2008) utilizan \\(\\alpha = 40\\), donde \\(r_{ij}\\) es el número de veces que el usuario ha visto un programa (contando vistas parciales, así que es un número real).\nLa función objetivo (sin regularización) se define como\n\\[\nL = \\sum_{(i,j)} c_{ij}(p_{ij} - \\sum_{l=1}^k u_{i,l}v_{j,l})^2\n\\tag{16.1}\\]\nNótese que :\n\nCuando \\(c_ij\\) es alta (porque \\(r_{i,j}\\) es alta), para minimizar esta cantidad tenemos que hacer la predicción de \\(p_{ij}\\) cercana a 1, pues el error se multiplica por \\(c_{ij}\\). Sin embargo,\nCuando \\(r_{i,j}\\) es bajo, no es tan importante ajustar esta información con precisión: si \\(p_{ij} = 1\\), puede ser que \\(\\sum_{l=1}^k u_{i,l}v_{j,l}\\) sea muy bajo, y si \\(p_{ij}=0\\), puede ser que \\(\\sum_{l=1}^k u_{i,l}v_{j,l}\\) sea cercano a 1 sin afectar tanto el error.\nEsto permite que en el ajuste podamos descubrir artículos con \\(p_{ij}\\) alta para algún usuario, aún cuando \\(r_{ij}\\) es cero o muy chico.\n\n\n\n16.5.2 Ejemplo\nVeamos cómo se ven soluciones de un factor\n\nimp_mat <- imp |> select(-usuario) |> as.matrix()\nerror_explicito <- function(uv){\n u <- matrix(uv[1:6], ncol = 1)\n v <- matrix(uv[7:10], ncol = 1)\n sum((imp_mat - u %*% t(v))^2)\n}\nerror_implicito <- function(uv){\n u <- matrix(uv[1:6], ncol = 1)\n v <- matrix(uv[7:10], ncol = 1)\n pref_mat <- as.numeric(imp_mat > 0) - u %*% t(v)\n confianza <- 1 + 0.1 * imp_mat\n sum((confianza * pref_mat)^2 )\n}\n\nSi intentamos ajustar los ratings implícitos como si fueran explícitos, obtenemos los siguientes ajustados con un solo factor latente:\n\nuv_inicial <- runif(10)\nopt_exp <- optim(par = uv_inicial, error_explicito, method = \"BFGS\")\nopt_exp$par[7:10]\n\n[1] 28.6668987 35.5285942 9.0062834 0.2312475\n\nt(t(opt_exp$par[1:6])) %*% t(opt_exp$par[7:10]) |> round()\n\n [,1] [,2] [,3] [,4]\n[1,] 118 146 37 1\n[2,] 124 154 39 1\n[3,] 36 44 11 0\n[4,] 151 187 47 1\n[5,] 217 269 68 2\n[6,] 180 223 56 1\n\n\nNótese que esta solución no es muy buena: una componente intenta capturar los patrones de consumo de estas cuatro películas.\nSi usamos preferencias y confianza, obtenemos:\n\nopt_imp <- optim(par = uv_inicial, error_implicito, method = \"BFGS\")\nopt_imp$par[7:10]\n\n[1] 1.1950285 1.1969381 1.1918477 0.7423818\n\nt(t(opt_imp$par[1:6])) %*% t(opt_imp$par[7:10]) |> round(2)\n\n [,1] [,2] [,3] [,4]\n[1,] 1 1 0.99 0.62\n[2,] 1 1 1.00 0.62\n[3,] 1 1 1.00 0.62\n[4,] 1 1 0.99 0.62\n[5,] 1 1 1.00 0.62\n[6,] 1 1 1.00 0.62\n\n\nque indica que la información en esta matriz es consistente con que todos los usuarios tienen preferencia alta por las tres películas de Star Wars, y menos por la cuarta.\n\nIgual que en los ejemplos anteriores, usualmente se agregan términos de regularización para los vectores renglón \\(u_i\\) y \\(v_j\\).\n\n\n16.5.3 Evaluación para modelos implícitos\nLa evaluación para modelos implícitos no es tan simple como en el caso explícito, pues no estamos modelando directamente los valores observados \\(r_{ij}\\). Medidas como RECM o MAD que usamos en el caso explícito no son tan apropiadas para este problema.\nUna alternativa es, para cada usuario \\(i\\), ordenar los artículos de mayor a menor valor de \\(\\hat{p}_{ij} = u_iv_j^t\\) (canciones, pellículas), y calculamos:\n\\[\nrank = \\frac{\\sum_{j} p_{ij}rank_{i,j}}{\\sum_j p_{ij}}\n\\]\ndonde \\(rank_{ij}\\) es el percentil del artículo \\(j\\) en la lista ordenada de artículos. \\(rank_{ij}=0\\) para el mejor artículo, y \\(rank_{ij}=1\\) para el peor. Es decir, obtenemos valores más bajos si observamos que los usuarios interactúan con artículos que están más arriba en el ranking.\nEsta suma es un promedio sobre los rankings del usuario con \\(p_{ij}=1\\), y menores valores son mejores (quiere decir que hubo alguna preferencia por los items con \\(rank_{ij}\\) bajo, es decir, los mejores de nuestra lista predicha. Es posible también hacer un promedio ponderado por \\(r_{ij}\\): \\[\nrank = \\frac{\\sum_{j} r_{ij}rank_{i,j}}{\\sum_j r_{ij}}\n\\]\nque es lo mismo que la ecuación anterior pero ponderando por el interés mostrado en cada artículo con \\(p_{ij}=1\\).\n\nMenores valores de \\(rank\\) son mejores.\nSi escogemos al azar el ordenamiento de los artículos, el valor esperado de \\(rank_{ij}\\) es \\(0.5\\) (en medio de la lista), lo que implica que el valor esperado de \\(rank\\) es \\(0.50\\). Cualquier modelo con \\(rank\\geq 0.5\\) es peor que dar recomendaciones al azar.\n\nEsta cantidad la podemos evaluar en entrenamiento y en validación. Para construir el conjunto de validación podemos hacer:\n\nEscogemos un número de usuarios para validación (por ejemplo \\(20\\%\\))\nPonemos \\(50\\%\\) de los artículos evaluados por estas personas en validación, por ejemplo.\n\nEstas cantidades dependen de cuántos datos tengamos, como siempre, para tener un tamaño razonable de datos de validación." }, { "objectID": "16-recom-factorizacion.html#modelo-de-red-neuronal", "href": "16-recom-factorizacion.html#modelo-de-red-neuronal", "title": "16  Dimensiones latentes: embeddings", - "section": "17.1 Modelo de red neuronal", - "text": "17.1 Modelo de red neuronal\nPodemos entonces construir una red neuronal con 2 capas ocultas como sigue (segimos (Bengio et al. 2003), una de las primeras referencias en usar este enfoque). Notamos que los enfoques más efectivos actualmente, con conjuntos de datos más grandes, se utilizan arquitecturas más refinadas que permiten modelación de dependencias de mayor distancia, comenzando con la idea de atención.\nEn este ejemplo usaremos el ejemplo de trigramas:\n\nCapa de incrustación o embedding. En la primera capa oculta, tenemos un mapeo de las entradas \\(w_1,\\ldots, w_{n-1}\\) a \\(x=C(w_1),\\ldots, C(w_{n-1})\\), donde \\(C\\) es una función que mapea palabras a vectores de dimensión \\(d\\). \\(C\\) también se puede pensar como una matriz de dimensión \\(|V|\\) por \\(d\\). En la capa de entrada,\n\n\\[w_{n-2},w_{n-1} \\to x = (C(w_{n-2}), C(w_{n-1})).\\]\n\nCapa totalmente conexa. En la siguiente capa oculta tenemos una matriz de pesos \\(H\\) y la función logística (o tangente hiperbólica) \\(\\sigma (z) = \\frac{e^z}{1+e^z}\\), como en una red neuronal usual.\n\nEn esta capa calculamos \\[z = \\sigma (a + Hx),\\] que resulta en un vector de tamaño \\(h\\).\n\nLa capa de salida debe ser un vector de probabilidades sobre todo el vocabulario \\(|V|\\). En esta capa tenemos pesos \\(U\\) y hacemos \\[y = b + U\\sigma (z),\\] y finalmente usamos softmax para tener probabilidades que suman uno: \\[p_i = \\frac{\\exp (y_i) }{\\sum_j exp(y_j)}.\\]\n\nEn el ajuste maximizamos la verosimilitud:\n\\[\\sum_t \\log \\hat{P}(w_{t,n}|w_{t,n-2}w_{t-n-1}) \\]\nLa representación en la referencia (Bengio et al. 2003) es:\n\n\n\nImagen\n\n\nEsta idea original ha sido explotada con éxito, pero en arquitecturas más modernas (mediante el mecanismo de atención). Nótese que el número de parámetros es del orden de \\(|V|(nm+h)\\), donde \\(|V|\\) es el tamaño del vocabulario (decenas o cientos de miles), \\(n\\) es 3 o 4 (trigramas, 4-gramas), \\(m\\) es el tamaño de la representacion (cientos) y \\(h\\) es el número de nodos en la segunda capa (también cientos o miles). Esto resulta en el mejor de los casos en modelos con miles de millones de parámetros. Adicionalmente, hay algunos cálculos costosos, como el softmax (donde hay que hacer una suma sobre el vocabulario completo). En el paper original se propone descenso estocástico.\n\nEjemplo\nVeamos un ejemplo chico de cómo se vería el paso feed-forward de esta red. Supondremos en este ejemplo que los sesgos \\(a,b\\) son iguales a cero para simplificar los cálculos.\nConsideremos que el texto de entrenamiento es “El perro corre. El gato corre. El león corre. El león ruge.”\nEn este caso, nuestro vocabulario consiste de los 8 tokens \\(<s>\\), el, perro, gato, león, corre, caza \\(</s>\\). Consideremos un modelo con \\(d=2\\) (representaciones de palabras en 2 dimensiones), y consideramos un modelo de trigramas.\nNuestra primera capa es una matriz \\(C\\) de tamaño \\(2\\times 8\\), es decir, un vector de tamaño 2 para cada palabra. Por ejemplo, podríamos tener\n\nlibrary(tidyverse)\nset.seed(63)\nC <- round(matrix(rnorm(16, 0, 0.1), 2, 8), 2)\ncolnames(C) <- c(\"_s_\", \"el\", \"perro\", \"gato\", \"león\", \"corre\", \"caza\", \"_ss_\")\nrownames(C) <- c(\"d_1\", \"d_2\")\nC\n\n _s_ el perro gato león corre caza _ss_\nd_1 0.13 0.05 0.05 0.04 -0.17 0.04 0.03 -0.02\nd_2 -0.19 -0.19 -0.11 0.01 0.04 -0.01 0.02 0.02\n\n\nEn la siguiente capa consideremos que usaremos, arbitrariamente, \\(h=3\\) unidades. Como estamos considerando bigramas, necesitamos una entrada de tamaño 4 (representación de un bigrama, que son dos vectores de la matriz \\(C\\), para predecir la siguiente palabra).\n\nH <- round(matrix(rnorm(12, 0, 0.1), 3, 4), 2)\nH\n\n [,1] [,2] [,3] [,4]\n[1,] -0.04 0.12 -0.09 0.18\n[2,] 0.09 0.10 0.06 0.08\n[3,] 0.10 -0.08 -0.07 -0.13\n\n\nY la última capa es la del vocabulario. Son entonces 8 unidades, con 3 entradas cada una. La matriz de pesos es:\n\nU <- round(matrix(rnorm(24, 0, 0.1), 8, 3), 2)\nrownames(U) <- c(\"_s_\", \"el\", \"perro\", \"gato\", \"león\", \"corre\", \"caza\", \"_ss_\")\nU\n\n [,1] [,2] [,3]\n_s_ 0.05 -0.15 -0.30\nel 0.01 0.16 0.15\nperro -0.14 0.10 0.05\ngato 0.04 0.09 0.12\nleón 0.06 -0.03 0.02\ncorre -0.01 0.00 -0.02\ncaza 0.10 0.00 0.06\n_ss_ 0.07 -0.10 0.01\n\n\nAhora consideremos cómo se calcula el objetivo con los datos de entrenamiento. El primer trigrama es (_s_, el). La primera capa entonces devuelve los dos vectores correspondientes a cada palabra (concatenado):\n\ncapa_1 <- c(C[, \"_s_\"], C[, \"el\"])\ncapa_1\n\n d_1 d_2 d_1 d_2 \n 0.13 -0.19 0.05 -0.19 \n\n\nLa siguiente capa es:\n\nsigma <- function(z){ 1 / (1 + exp(-z))}\ncapa_2 <- sigma(H %*% capa_1)\ncapa_2\n\n [,1]\n[1,] 0.4833312\n[2,] 0.4951252\n[3,] 0.5123475\n\n\nY la capa final da\n\ny <- U %*% capa_2\ny\n\n [,1]\n_s_ -0.203806461\nel 0.160905460\nperro 0.007463525\ngato 0.125376210\nleón 0.024393066\ncorre -0.015080262\ncaza 0.079073967\n_ss_ -0.010555858\n\n\nY aplicamos softmax para encontrar las probabilidades\n\np <- exp(y)/sum(exp(y)) |> as.numeric()\np\n\n [,1]\n_s_ 0.09931122\nel 0.14301799\nperro 0.12267376\ngato 0.13802588\nleón 0.12476825\ncorre 0.11993917\ncaza 0.13178067\n_ss_ 0.12048306\n\n\nY la probabilidad es entonces\n\np_1 <- p[\"perro\", 1]\np_1\n\n perro \n0.1226738 \n\n\nCuya log probabilidad es\n\nlog(p_1)\n\n perro \n-2.098227 \n\n\nAhora seguimos con el siguiente trigrama, que es “(perro, corre)”. Necesitamos calcular la probabilidad de corre dado el contexto “el perro”. Repetimos nuestro cálculo:\n\ncapa_1 <- c(C[, \"el\"], C[, \"perro\"])\ncapa_1\n\n d_1 d_2 d_1 d_2 \n 0.05 -0.19 0.05 -0.11 \n\ncapa_2 <- sigma(H %*% capa_1)\ncapa_2\n\n [,1]\n[1,] 0.4877275\n[2,] 0.4949252\n[3,] 0.5077494\n\ny <- U %*% capa_2\ny\n\n [,1]\n_s_ -0.202177217\nel 0.160227709\nperro 0.006598141\ngato 0.124982290\nleón 0.024570880\ncorre -0.015032262\ncaza 0.079237709\n_ss_ -0.010274101\n\np <- exp(y)/sum(exp(y)) |> as.numeric()\np\n\n [,1]\n_s_ 0.09947434\nel 0.14292280\nperro 0.12256912\ngato 0.13797317\nleón 0.12479193\ncorre 0.11994636\ncaza 0.13180383\n_ss_ 0.12051845\n\n\nY la probabilidad es entonces\n\np_2 <- p[\"corre\", 1]\nlog(p_2)\n\n corre \n-2.120711 \n\n\nSumando, la log probabilidad es:\n\nlog(p_1) + log(p_2)\n\n perro \n-4.218937 \n\n\ny continuamos con los siguientes trigramas del texto de entrenamiento. Creamos una función\n\nfeed_fow_p <- function(trigrama, C, H, U){\n trigrama <- strsplit(trigrama, \" \", fixed = TRUE)[[1]]\n capa_1 <- c(C[, trigrama[1]], C[, trigrama[2]])\n capa_2 <- sigma(H %*% capa_1)\n y <- U %*% capa_2\n p <- exp(y)/sum(exp(y)) |> as.numeric()\n p\n}\n\nfeed_fow_dev <- function(trigrama, C, H, U) {\n p <- feed_fow_p(trigrama, C, H, U)\n trigrama_s <- strsplit(trigrama, \" \", fixed = TRUE)[[1]]\n log(p)[trigrama_s[3], 1]\n}\n\nY ahora aplicamos a todos los textos:\n\ntexto_entrena <- c(\"_s_ el perro corre _ss_\", \" _s_ el gato corre _ss_\", \" _s_ el león corre _ss_\",\n \"_s_ el león caza _ss_\", \"_s_ el gato caza _ss_\")\nentrena_trigramas <- map(texto_entrena, \n ~tokenizers::tokenize_ngrams(.x, n = 3)[[1]]) |> \n flatten() |> unlist()\nentrena_trigramas\n\n [1] \"_s_ el perro\" \"el perro corre\" \"perro corre _ss_\" \"_s_ el gato\" \n [5] \"el gato corre\" \"gato corre _ss_\" \"_s_ el león\" \"el león corre\" \n [9] \"león corre _ss_\" \"_s_ el león\" \"el león caza\" \"león caza _ss_\" \n[13] \"_s_ el gato\" \"el gato caza\" \"gato caza _ss_\" \n\n\n\nlog_p <- sapply(entrena_trigramas, function(x) feed_fow_dev(x, C, H, U))\nsum(log_p)\n\n[1] -31.21475\n\n\nAhora piensa como harías más grande esta verosimilitud. Observa que “perro”, “gato” y “león”” están comunmente seguidos de “corre”. Esto implica que nos convendría que hubiera cierta similitud entre los vectores de estas tres palabras, por ejemplo:\n\nC_1 <- C\nindices <- colnames(C) %in% c(\"perro\", \"gato\", \"león\")\nC_1[1, indices] <- 3.0\nC_1[1, !indices] <- -1.0\nC_1\n\n _s_ el perro gato león corre caza _ss_\nd_1 -1.00 -1.00 3.00 3.00 3.00 -1.00 -1.00 -1.00\nd_2 -0.19 -0.19 -0.11 0.01 0.04 -0.01 0.02 0.02\n\n\nLa siguiente capa queremos que extraiga el concepto “animal” en la palabra anterior, o algo similar, así que podríamos poner en la unidad 1:\n\nH_1 <- H\nH_1[1, ] <- c(0, 0, 5, 0)\nH_1\n\n [,1] [,2] [,3] [,4]\n[1,] 0.00 0.00 5.00 0.00\n[2,] 0.09 0.10 0.06 0.08\n[3,] 0.10 -0.08 -0.07 -0.13\n\n\nNótese que la unidad 1 de la segunda capa se activa cuando la primera componente de la palabra anterior es alta. En la última capa, podríamos entonces poner\n\nU_1 <- U\nU_1[\"corre\", ] <- c(4.0, -2, -2)\nU_1[\"caza\", ] <- c(4.2, -2, -2)\nU_1\n\n [,1] [,2] [,3]\n_s_ 0.05 -0.15 -0.30\nel 0.01 0.16 0.15\nperro -0.14 0.10 0.05\ngato 0.04 0.09 0.12\nleón 0.06 -0.03 0.02\ncorre 4.00 -2.00 -2.00\ncaza 4.20 -2.00 -2.00\n_ss_ 0.07 -0.10 0.01\n\n\nque captura cuando la primera unidad se activa. Ahora el cálculo completo es:\n\nlog_p <- sapply(entrena_trigramas, function(x) feed_fow_dev(x, C_1, H_1, U_1))\nsum(log_p)\n\n[1] -23.53883\n\n\nY logramos aumentar la verosimilitud considerablemente. Compara las probabilidades:\n\nfeed_fow_p(\"el perro\", C, H, U)\n\n [,1]\n_s_ 0.09947434\nel 0.14292280\nperro 0.12256912\ngato 0.13797317\nleón 0.12479193\ncorre 0.11994636\ncaza 0.13180383\n_ss_ 0.12051845\n\nfeed_fow_p(\"el perro\", C_1, H_1, U_1)\n\n [,1]\n_s_ 0.03493901\nel 0.04780222\nperro 0.03821035\ngato 0.04690264\nleón 0.04308502\ncorre 0.33639351\ncaza 0.41087194\n_ss_ 0.04179531\n\nfeed_fow_p(\"el gato\", C, H, U)\n\n [,1]\n_s_ 0.09957218\nel 0.14289131\nperro 0.12246787\ngato 0.13795972\nleón 0.12480659\ncorre 0.11993921\ncaza 0.13183822\n_ss_ 0.12052489\n\nfeed_fow_p(\"el gato\", C_1, H_1, U_1)\n\n [,1]\n_s_ 0.03489252\nel 0.04769205\nperro 0.03813136\ngato 0.04679205\nleón 0.04298749\ncorre 0.33663831\ncaza 0.41117094\n_ss_ 0.04169529\n\n\nObservación: a partir de este principio, es posible construir arquitecturas más refinadas que tomen en cuenta, por ejemplo, relaciones más lejanas entre partes de oraciones (no solo el contexto del n-grama), ver por ejemplo el capítulo 10 del libro de Deep Learning de Goodfellow, Bengio y Courville.\nAbajo exploramos una parte fundamental de estos modelos: representaciones de palabras, y modelos relativamente simples para obtener estas representaciones." + "section": "18.1 Modelo de red neuronal", + "text": "18.1 Modelo de red neuronal\nPodemos entonces construir una red neuronal con 2 capas ocultas como sigue (segimos (Bengio et al. 2003), una de las primeras referencias en usar este enfoque). Notamos que los enfoques más efectivos actualmente, con conjuntos de datos más grandes, se utilizan arquitecturas más refinadas que permiten modelación de dependencias de mayor distancia, comenzando con la idea de atención.\nEn este ejemplo usaremos el ejemplo de trigramas:\n\nCapa de incrustación o embedding. En la primera capa oculta, tenemos un mapeo de las entradas \\(w_1,\\ldots, w_{n-1}\\) a \\(x=C(w_1),\\ldots, C(w_{n-1})\\), donde \\(C\\) es una función que mapea palabras a vectores de dimensión \\(d\\). \\(C\\) también se puede pensar como una matriz de dimensión \\(|V|\\) por \\(d\\). En la capa de entrada,\n\n\\[w_{n-2},w_{n-1} \\to x = (C(w_{n-2}), C(w_{n-1})).\\]\n\nCapa totalmente conexa. En la siguiente capa oculta tenemos una matriz de pesos \\(H\\) y la función logística (o tangente hiperbólica) \\(\\sigma (z) = \\frac{e^z}{1+e^z}\\), como en una red neuronal usual.\n\nEn esta capa calculamos \\[z = \\sigma (a + Hx),\\] que resulta en un vector de tamaño \\(h\\).\n\nLa capa de salida debe ser un vector de probabilidades sobre todo el vocabulario \\(|V|\\). En esta capa tenemos pesos \\(U\\) y hacemos \\[y = b + U\\sigma (z),\\] y finalmente usamos softmax para tener probabilidades que suman uno: \\[p_i = \\frac{\\exp (y_i) }{\\sum_j exp(y_j)}.\\]\n\nEn el ajuste maximizamos la verosimilitud:\n\\[\\sum_t \\log \\hat{P}(w_{t,n}|w_{t,n-2}w_{t-n-1}) \\]\nLa representación en la referencia (Bengio et al. 2003) es:\n\n\n\nImagen\n\n\nEsta idea original ha sido explotada con éxito, pero en arquitecturas más modernas (mediante el mecanismo de atención). Nótese que el número de parámetros es del orden de \\(|V|(nm+h)\\), donde \\(|V|\\) es el tamaño del vocabulario (decenas o cientos de miles), \\(n\\) es 3 o 4 (trigramas, 4-gramas), \\(m\\) es el tamaño de la representacion (cientos) y \\(h\\) es el número de nodos en la segunda capa (también cientos o miles). Esto resulta en el mejor de los casos en modelos con miles de millones de parámetros. Adicionalmente, hay algunos cálculos costosos, como el softmax (donde hay que hacer una suma sobre el vocabulario completo). En el paper original se propone descenso estocástico.\n\nEjemplo\nVeamos un ejemplo chico de cómo se vería el paso feed-forward de esta red. Supondremos en este ejemplo que los sesgos \\(a,b\\) son iguales a cero para simplificar los cálculos.\nConsideremos que el texto de entrenamiento es “El perro corre. El gato corre. El león corre. El león ruge.”\nEn este caso, nuestro vocabulario consiste de los 8 tokens \\(<s>\\), el, perro, gato, león, corre, caza \\(</s>\\). Consideremos un modelo con \\(d=2\\) (representaciones de palabras en 2 dimensiones), y consideramos un modelo de trigramas.\nNuestra primera capa es una matriz \\(C\\) de tamaño \\(2\\times 8\\), es decir, un vector de tamaño 2 para cada palabra. Por ejemplo, podríamos tener\n\nlibrary(tidyverse)\nset.seed(63)\nC <- round(matrix(rnorm(16, 0, 0.1), 2, 8), 2)\ncolnames(C) <- c(\"_s_\", \"el\", \"perro\", \"gato\", \"león\", \"corre\", \"caza\", \"_ss_\")\nrownames(C) <- c(\"d_1\", \"d_2\")\nC\n\n _s_ el perro gato león corre caza _ss_\nd_1 0.13 0.05 0.05 0.04 -0.17 0.04 0.03 -0.02\nd_2 -0.19 -0.19 -0.11 0.01 0.04 -0.01 0.02 0.02\n\n\nEn la siguiente capa consideremos que usaremos, arbitrariamente, \\(h=3\\) unidades. Como estamos considerando bigramas, necesitamos una entrada de tamaño 4 (representación de un bigrama, que son dos vectores de la matriz \\(C\\), para predecir la siguiente palabra).\n\nH <- round(matrix(rnorm(12, 0, 0.1), 3, 4), 2)\nH\n\n [,1] [,2] [,3] [,4]\n[1,] -0.04 0.12 -0.09 0.18\n[2,] 0.09 0.10 0.06 0.08\n[3,] 0.10 -0.08 -0.07 -0.13\n\n\nY la última capa es la del vocabulario. Son entonces 8 unidades, con 3 entradas cada una. La matriz de pesos es:\n\nU <- round(matrix(rnorm(24, 0, 0.1), 8, 3), 2)\nrownames(U) <- c(\"_s_\", \"el\", \"perro\", \"gato\", \"león\", \"corre\", \"caza\", \"_ss_\")\nU\n\n [,1] [,2] [,3]\n_s_ 0.05 -0.15 -0.30\nel 0.01 0.16 0.15\nperro -0.14 0.10 0.05\ngato 0.04 0.09 0.12\nleón 0.06 -0.03 0.02\ncorre -0.01 0.00 -0.02\ncaza 0.10 0.00 0.06\n_ss_ 0.07 -0.10 0.01\n\n\nAhora consideremos cómo se calcula el objetivo con los datos de entrenamiento. El primer trigrama es (_s_, el). La primera capa entonces devuelve los dos vectores correspondientes a cada palabra (concatenado):\n\ncapa_1 <- c(C[, \"_s_\"], C[, \"el\"])\ncapa_1\n\n d_1 d_2 d_1 d_2 \n 0.13 -0.19 0.05 -0.19 \n\n\nLa siguiente capa es:\n\nsigma <- function(z){ 1 / (1 + exp(-z))}\ncapa_2 <- sigma(H %*% capa_1)\ncapa_2\n\n [,1]\n[1,] 0.4833312\n[2,] 0.4951252\n[3,] 0.5123475\n\n\nY la capa final da\n\ny <- U %*% capa_2\ny\n\n [,1]\n_s_ -0.203806461\nel 0.160905460\nperro 0.007463525\ngato 0.125376210\nleón 0.024393066\ncorre -0.015080262\ncaza 0.079073967\n_ss_ -0.010555858\n\n\nY aplicamos softmax para encontrar las probabilidades\n\np <- exp(y)/sum(exp(y)) |> as.numeric()\np\n\n [,1]\n_s_ 0.09931122\nel 0.14301799\nperro 0.12267376\ngato 0.13802588\nleón 0.12476825\ncorre 0.11993917\ncaza 0.13178067\n_ss_ 0.12048306\n\n\nY la probabilidad es entonces\n\np_1 <- p[\"perro\", 1]\np_1\n\n perro \n0.1226738 \n\n\nCuya log probabilidad es\n\nlog(p_1)\n\n perro \n-2.098227 \n\n\nAhora seguimos con el siguiente trigrama, que es “(perro, corre)”. Necesitamos calcular la probabilidad de corre dado el contexto “el perro”. Repetimos nuestro cálculo:\n\ncapa_1 <- c(C[, \"el\"], C[, \"perro\"])\ncapa_1\n\n d_1 d_2 d_1 d_2 \n 0.05 -0.19 0.05 -0.11 \n\ncapa_2 <- sigma(H %*% capa_1)\ncapa_2\n\n [,1]\n[1,] 0.4877275\n[2,] 0.4949252\n[3,] 0.5077494\n\ny <- U %*% capa_2\ny\n\n [,1]\n_s_ -0.202177217\nel 0.160227709\nperro 0.006598141\ngato 0.124982290\nleón 0.024570880\ncorre -0.015032262\ncaza 0.079237709\n_ss_ -0.010274101\n\np <- exp(y)/sum(exp(y)) |> as.numeric()\np\n\n [,1]\n_s_ 0.09947434\nel 0.14292280\nperro 0.12256912\ngato 0.13797317\nleón 0.12479193\ncorre 0.11994636\ncaza 0.13180383\n_ss_ 0.12051845\n\n\nY la probabilidad es entonces\n\np_2 <- p[\"corre\", 1]\nlog(p_2)\n\n corre \n-2.120711 \n\n\nSumando, la log probabilidad es:\n\nlog(p_1) + log(p_2)\n\n perro \n-4.218937 \n\n\ny continuamos con los siguientes trigramas del texto de entrenamiento. Creamos una función\n\nfeed_fow_p <- function(trigrama, C, H, U){\n trigrama <- strsplit(trigrama, \" \", fixed = TRUE)[[1]]\n capa_1 <- c(C[, trigrama[1]], C[, trigrama[2]])\n capa_2 <- sigma(H %*% capa_1)\n y <- U %*% capa_2\n p <- exp(y)/sum(exp(y)) |> as.numeric()\n p\n}\n\nfeed_fow_dev <- function(trigrama, C, H, U) {\n p <- feed_fow_p(trigrama, C, H, U)\n trigrama_s <- strsplit(trigrama, \" \", fixed = TRUE)[[1]]\n log(p)[trigrama_s[3], 1]\n}\n\nY ahora aplicamos a todos los textos:\n\ntexto_entrena <- c(\"_s_ el perro corre _ss_\", \" _s_ el gato corre _ss_\", \" _s_ el león corre _ss_\",\n \"_s_ el león caza _ss_\", \"_s_ el gato caza _ss_\")\nentrena_trigramas <- map(texto_entrena, \n ~tokenizers::tokenize_ngrams(.x, n = 3)[[1]]) |> \n flatten() |> unlist()\nentrena_trigramas\n\n [1] \"_s_ el perro\" \"el perro corre\" \"perro corre _ss_\" \"_s_ el gato\" \n [5] \"el gato corre\" \"gato corre _ss_\" \"_s_ el león\" \"el león corre\" \n [9] \"león corre _ss_\" \"_s_ el león\" \"el león caza\" \"león caza _ss_\" \n[13] \"_s_ el gato\" \"el gato caza\" \"gato caza _ss_\" \n\n\n\nlog_p <- sapply(entrena_trigramas, function(x) feed_fow_dev(x, C, H, U))\nsum(log_p)\n\n[1] -31.21475\n\n\nAhora piensa como harías más grande esta verosimilitud. Observa que “perro”, “gato” y “león”” están comunmente seguidos de “corre”. Esto implica que nos convendría que hubiera cierta similitud entre los vectores de estas tres palabras, por ejemplo:\n\nC_1 <- C\nindices <- colnames(C) %in% c(\"perro\", \"gato\", \"león\")\nC_1[1, indices] <- 3.0\nC_1[1, !indices] <- -1.0\nC_1\n\n _s_ el perro gato león corre caza _ss_\nd_1 -1.00 -1.00 3.00 3.00 3.00 -1.00 -1.00 -1.00\nd_2 -0.19 -0.19 -0.11 0.01 0.04 -0.01 0.02 0.02\n\n\nLa siguiente capa queremos que extraiga el concepto “animal” en la palabra anterior, o algo similar, así que podríamos poner en la unidad 1:\n\nH_1 <- H\nH_1[1, ] <- c(0, 0, 5, 0)\nH_1\n\n [,1] [,2] [,3] [,4]\n[1,] 0.00 0.00 5.00 0.00\n[2,] 0.09 0.10 0.06 0.08\n[3,] 0.10 -0.08 -0.07 -0.13\n\n\nNótese que la unidad 1 de la segunda capa se activa cuando la primera componente de la palabra anterior es alta. En la última capa, podríamos entonces poner\n\nU_1 <- U\nU_1[\"corre\", ] <- c(4.0, -2, -2)\nU_1[\"caza\", ] <- c(4.2, -2, -2)\nU_1\n\n [,1] [,2] [,3]\n_s_ 0.05 -0.15 -0.30\nel 0.01 0.16 0.15\nperro -0.14 0.10 0.05\ngato 0.04 0.09 0.12\nleón 0.06 -0.03 0.02\ncorre 4.00 -2.00 -2.00\ncaza 4.20 -2.00 -2.00\n_ss_ 0.07 -0.10 0.01\n\n\nque captura cuando la primera unidad se activa. Ahora el cálculo completo es:\n\nlog_p <- sapply(entrena_trigramas, function(x) feed_fow_dev(x, C_1, H_1, U_1))\nsum(log_p)\n\n[1] -23.53883\n\n\nY logramos aumentar la verosimilitud considerablemente. Compara las probabilidades:\n\nfeed_fow_p(\"el perro\", C, H, U)\n\n [,1]\n_s_ 0.09947434\nel 0.14292280\nperro 0.12256912\ngato 0.13797317\nleón 0.12479193\ncorre 0.11994636\ncaza 0.13180383\n_ss_ 0.12051845\n\nfeed_fow_p(\"el perro\", C_1, H_1, U_1)\n\n [,1]\n_s_ 0.03493901\nel 0.04780222\nperro 0.03821035\ngato 0.04690264\nleón 0.04308502\ncorre 0.33639351\ncaza 0.41087194\n_ss_ 0.04179531\n\nfeed_fow_p(\"el gato\", C, H, U)\n\n [,1]\n_s_ 0.09957218\nel 0.14289131\nperro 0.12246787\ngato 0.13795972\nleón 0.12480659\ncorre 0.11993921\ncaza 0.13183822\n_ss_ 0.12052489\n\nfeed_fow_p(\"el gato\", C_1, H_1, U_1)\n\n [,1]\n_s_ 0.03489252\nel 0.04769205\nperro 0.03813136\ngato 0.04679205\nleón 0.04298749\ncorre 0.33663831\ncaza 0.41117094\n_ss_ 0.04169529\n\n\nObservación: a partir de este principio, es posible construir arquitecturas más refinadas que tomen en cuenta, por ejemplo, relaciones más lejanas entre partes de oraciones (no solo el contexto del n-grama), ver por ejemplo el capítulo 10 del libro de Deep Learning de Goodfellow, Bengio y Courville.\nAbajo exploramos una parte fundamental de estos modelos: representaciones de palabras, y modelos relativamente simples para obtener estas representaciones." }, { - "objectID": "16-recom-factorizacion.html#representación-de-palabras", - "href": "16-recom-factorizacion.html#representación-de-palabras", + "objectID": "16-recom-factorizacion.html#representación-de-palabras-y-similitud", + "href": "16-recom-factorizacion.html#representación-de-palabras-y-similitud", "title": "16  Dimensiones latentes: embeddings", - "section": "17.2 Representación de palabras", - "text": "17.2 Representación de palabras\nUn aspecto interesante de el modelo de arriba es que nos da una representación vectorial de las palabras, en la forma de los parámetros ajustados de la matriz \\(C\\). Esta se puede entender como una descripción numérica de cómo funciona una palabra en el contexto de su n-grama.\nPor ejemplo, deberíamos encontrar que palabras como “perro” y “gato” tienen representaciones similares. La razón es que cuando aparecen, las probabilidades sobre las palabras siguientes deberían ser similares, pues estas son dos palabras que se pueden usar en muchos contextos compartidos.\nTambién podríamos encontrar que palabras como perro, gato, águila, león, etc. tienen partes o entradas similares en sus vectores de representación, que es la parte que hace que funcionen como “animal mamífero” dentro de frases.\nVeremos que hay más razones por las que es interesante esta representación." + "section": "18.2 Representación de palabras y similitud", + "text": "18.2 Representación de palabras y similitud\nUn aspecto interesante de el modelo de arriba es que nos da una representación vectorial de las palabras, en la forma de los parámetros ajustados de la matriz \\(C\\). Esta se puede entender como una descripción numérica de cómo funciona una palabra en el contexto de su n-grama.\nPor ejemplo, deberíamos encontrar que palabras como “perro” y “gato” tienen representaciones similares. La razón es que cuando aparecen, las probabilidades sobre las palabras siguientes deberían ser similares, pues estas son dos palabras que se pueden usar en muchos contextos compartidos.\nTambién podríamos encontrar que palabras como perro, gato, águila, león, etc. tienen partes o entradas similares en sus vectores de representación, que es la parte que hace que funcionen como “animal mamífero” dentro de frases.\nVeremos que hay más razones por las que es interesante esta representación." }, { "objectID": "16-recom-factorizacion.html#modelos-de-word2vec", "href": "16-recom-factorizacion.html#modelos-de-word2vec", "title": "16  Dimensiones latentes: embeddings", - "section": "17.3 Modelos de word2vec", - "text": "17.3 Modelos de word2vec\nEn estos ejemplos veremos cómo producir embeddings de palabras que son precursores de embeddings más refinados como los producidos por Modelos grandes de lenguajes (LLMs). Ver por ejemplo aquí.\nSi lo que principalmente nos interesa es obtener una representación vectorial de palabras, es posible simplificar considerablemente el modelo de arriba o LLMs para poder entrenarlos mucho más rápido, y obtener una representación que en muchas tareas se desempeña bien ((Mikolov et al. 2013)).\nHay dos ideas básicas que se pueden usar para reducir la complejidad del entrenamiento (ver más en (Mikolov et al. 2013)):\n\nEliminar la segunda capa oculta: modelo de bag-of-words continuo y modelo de skip-gram.\nCambiar la función objetivo (minimizar devianza/maximizar verosimilitud) por una más simple, mediante un truco que se llama negative sampling.\n\nComo ya no es de interés central predecir la siguiente palabra a partir de las anteriores, en estos modelos intentamos predecir la palabra central a partir de las que están alrededor.\n\nArquitectura continuous bag-of-words\nLa entrada es igual que en el modelo completo. En primer lugar, simplificamos la segunda capa oculta pondiendo en \\(z\\) el promedio de los vectores \\(C(w_{n-2}), C(w_{n-1})\\). La última capa la dejamos igual por el momento:\n\n\n\nImagen\n\n\nEl modelo se llama bag-of-words porque todas las entradas de la primera capa oculta contribuyen de la misma manera en la salida, independientemente del orden. Aunque esto no suena como buena idea para construir un modelo de lenguaje, veremos que resulta en una representación adecuada para algunos problemas.\n\nEn la primera capa oculta, tenemos un mapeo de las entradas \\(w_1,\\ldots, w_{n-1}\\) a \\(x=C(w_1),\\ldots, C(w_{n-1})\\), donde \\(C\\) es una función que mapea palabras a vectores de dimensión \\(d\\). \\(C\\) también se puede pensar como una matriz de dimensión \\(|V|\\) por \\(d\\). En la capa de entrada,\n\n\\[w_{n-2},w_{n-1} \\to x = (C(w_{n-2}), C(w_{n-1})).\\]\n\nEn la siguiente “capa” oculta simplemente sumamos las entradas de \\(x\\). Aquí nótese que realmente no hay parámetros.\nFinalmente, la capa de salida debe ser un vector de probabilidades sobre todo el vocabulario \\(|V|\\). En esta capa tenemos pesos \\(U\\) y hacemos \\[y = b + U\\sigma (z),\\] y finalmente usamos softmax para tener probabilidades que suman uno: \\[p_i = \\frac{\\exp (y_i) }{\\sum_j exp(y_j)}.\\]\n\nEn el ajuste maximizamos la verosimilitud sobre el corpus. Por ejemplo, para una frase, su log verosimilitud es:\n\\[\\sum_t \\log \\hat{P}(w_{t,n}|w_{t,n+1} \\cdots w_{t-n-1}) \\]\n\n\nArquitectura skip-grams\nOtro modelo simplificado, con más complejidad computacional pero mejores resultados (ver (Mikolov et al. 2013)) que el bag-of-words, es el modelo de skip-grams. En este caso, dada cada palabra que encontramos, intentamos predecir un número fijo de las palabras anteriores y palabras posteriores (el contexto es una vecindad de la palabra).\n\n\n\nImagen\n\n\nLa función objetivo se defina ahora (simplificando) como suma sobre \\(t\\):\n\\[-\\sum_t \\sum_{ -2\\leq j \\leq 2, j\\neq 0} \\log P(w_{t-j} | w_t)\\] (no tomamos en cuenta dónde aparece exactamente \\(w_{t-j}\\) en relación a \\(w_t\\), simplemente consideramos que está en su contexto), donde\n\\[\\log P(w_{t-j}|w_t) = u_{t-j}^tC(w_t) - \\log\\sum_k \\exp{u_{k}^tC(w_t)}\\]\nTodavía se propone una simplificación adicional que resulta ser efectiva:\n\n\nMuestreo negativo\nLa siguiente simplificación consiste en cambiar la función objetivo. En word2vec puede usarse “muestreo negativo”.\nPara empezar, la función objetivo original (para contexto de una sola palabra) es\n\\[E = -\\log \\hat{P}(w_{a}|w_{n}) = -y_{w_a} + \\log\\sum_j \\exp(y_j),\\]\ndonde las \\(y_i\\) son las salidas de la penúltima capa. La dificultad está en el segundo término, que es sobre todo el vocabulario en incluye todos los parámetros del modelo (hay que calcular las parciales de \\(y_j\\)’s sobre cada una de las palabras del vocabulario).\nLa idea del muestreo negativo es que si \\(w_a\\) está en el contexto de \\(w_{n}\\), tomamos una muestra de \\(k\\) palabras \\(v_1,\\ldots v_k\\) al azar (2-50, dependiendo del tamaño de la colección), y creamos \\(k\\) “contextos falsos” \\(v_j w_{n}\\), \\(j=1\\ldots,k\\). Minimizamos en lugar de la observación de arriba\n\\[E = -\\log\\sigma(y_{w_a}) + \\sum_{j=1}^k \\log\\sigma(y_j),\\] en donde queremos maximizar la probabilidad de que ocurra \\(w_a\\) vs. la probabilidad de que ocurra alguna de las \\(v_j\\). Es decir, solo buscamos optimizar parámetros para separar lo mejor que podamos la observación de \\(k\\) observaciones falsas, lo cual implica que tenemos que mover un número relativamente chico de parámetros (en lugar de todos los parámetros de todas las palabras del vocabulario).\nLas palabras “falsas” se escogen según una probabilidad ajustada de unigramas (se observó empíricamente mejor desempeño cuando escogemos cada palabra con probabilidad proporcional a \\(P(w)^{3/4}\\), en lugar de \\(P(w)\\), ver (Mikolov et al. 2013)).\n\n\nEjemplo\n\nif(!require(wordVectors)){\n devtools::install_github(\"bmschmidt/wordVectors\", \n dependencies = TRUE)\n}\n\n\n── R CMD build ─────────────────────────────────────────────────────────────────\n* checking for file ‘/tmp/RtmpxKVLkt/remotes8fbc2192c597/bmschmidt-wordVectors-ad127c1/DESCRIPTION’ ... OK\n* preparing ‘wordVectors’:\n* checking DESCRIPTION meta-information ... OK\n* cleaning src\n* checking for LF line-endings in source and make files and shell scripts\n* checking for empty or unneeded directories\n* building ‘wordVectors_2.0.tar.gz’\n\nlibrary(wordVectors)\n\n\nlibrary(tidyverse)\nruta <- \"../datos/noticias/ES_Newspapers.txt\"\nif(!file.exists(ruta)){\n periodico <- \n read_lines(file= \"https://es-noticias.s3.amazonaws.com/Es_Newspapers.txt\",\n progress = FALSE)\n write_lines(periodico, ruta)\n} else {\n periodico <- read_lines(file= ruta,\n progress = FALSE)\n}\nnormalizar <- function(texto, vocab = NULL){\n # minúsculas\n texto <- tolower(texto)\n # varios ajustes\n texto <- gsub(\"\\\\s+\", \" \", texto)\n texto <- gsub(\"\\\\.[^0-9]\", \" _punto_ \", texto)\n texto <- gsub(\" _s_ $\", \"\", texto)\n texto <- gsub(\"\\\\.\", \" _punto_ \", texto)\n texto <- gsub(\"[«»¡!¿?-]\", \"\", texto) \n texto <- gsub(\";\", \" _punto_coma_ \", texto) \n texto <- gsub(\"\\\\:\", \" _dos_puntos_ \", texto) \n texto <- gsub(\"\\\\,[^0-9]\", \" _coma_ \", texto)\n texto <- gsub(\"\\\\s+\", \" \", texto)\n texto\n}\nperiodico_df <- tibble(txt = periodico) |>\n mutate(id = row_number()) |>\n mutate(txt = normalizar(txt))\n\nConstruimos un modelo con vectores de palabras de tamaño 100, skip-grams de tamaño 4, y ajustamos con muestreo negativo de tamaño 20:\n\nif(!file.exists('./cache/noticias_w2v.txt')){\n tmp <- tempfile()\n # tokenización\n write_lines(periodico_df$txt, tmp)\n prep <- prep_word2vec(tmp, \n destination = './cache/noticias_w2v.txt', bundle_ngrams = 2)\n } \n\nBeginning tokenization to text file at ./cache/noticias_w2v.txt\n\n\nPrepping /tmp/RtmpxKVLkt/file8fbc9992291\n\n\nStarting training using file ./cache/noticias_w2v.txt\nWords processed: 100K Vocab size: 73K \nWords processed: 200K Vocab size: 124K \nWords processed: 300K Vocab size: 168K \nWords processed: 400K Vocab size: 209K \nWords processed: 500K Vocab size: 247K \nWords processed: 600K Vocab size: 281K \nWords processed: 700K Vocab size: 314K \nWords processed: 800K Vocab size: 346K \nWords processed: 900K Vocab size: 376K \nWords processed: 1000K Vocab size: 406K \nWords processed: 1100K Vocab size: 434K \nWords processed: 1200K Vocab size: 462K \nWords processed: 1300K Vocab size: 489K \nWords processed: 1400K Vocab size: 515K \nWords processed: 1500K Vocab size: 540K \nWords processed: 1600K Vocab size: 565K \nWords processed: 1700K Vocab size: 590K \nWords processed: 1800K Vocab size: 613K \nWords processed: 1900K Vocab size: 637K \nWords processed: 2000K Vocab size: 661K \nWords processed: 2100K Vocab size: 684K \nWords processed: 2200K Vocab size: 706K \nWords processed: 2300K Vocab size: 729K \nWords processed: 2400K Vocab size: 750K \nWords processed: 2500K Vocab size: 771K \nWords processed: 2600K Vocab size: 792K \nWords processed: 2700K Vocab size: 813K \nWords processed: 2800K Vocab size: 834K \nWords processed: 2900K Vocab size: 854K \nWords processed: 3000K Vocab size: 873K \nWords processed: 3100K Vocab size: 893K \nWords processed: 3200K Vocab size: 913K \nWords processed: 3300K Vocab size: 932K \nWords processed: 3400K Vocab size: 951K \nWords processed: 3500K Vocab size: 970K \nWords processed: 3600K Vocab size: 989K \nWords processed: 3700K Vocab size: 1007K \nWords processed: 3800K Vocab size: 1026K \nWords processed: 3900K Vocab size: 1044K \nWords processed: 4000K Vocab size: 1062K \nWords processed: 4100K Vocab size: 1080K \nWords processed: 4200K Vocab size: 1098K \nWords processed: 4300K Vocab size: 1115K \nWords processed: 4400K Vocab size: 1132K \nWords processed: 4500K Vocab size: 1150K \nWords processed: 4600K Vocab size: 1167K \nWords processed: 4700K Vocab size: 1184K \nWords processed: 4800K Vocab size: 1201K \nWords processed: 4900K Vocab size: 1218K \nWords processed: 5000K Vocab size: 1235K \nWords processed: 5100K Vocab size: 1252K \nWords processed: 5200K Vocab size: 1268K \nWords processed: 5300K Vocab size: 1285K \nWords processed: 5400K Vocab size: 1301K \nWords processed: 5500K Vocab size: 1317K \nWords processed: 5600K Vocab size: 1333K \nWords processed: 5700K Vocab size: 1349K \nWords processed: 5800K Vocab size: 1364K \nWords processed: 5900K Vocab size: 1380K \nWords processed: 6000K Vocab size: 1395K \nWords processed: 6100K Vocab size: 1411K \nWords processed: 6200K Vocab size: 1426K \nWords processed: 6300K Vocab size: 1441K \nWords processed: 6400K Vocab size: 1456K \nWords processed: 6500K Vocab size: 1471K \nWords processed: 6600K Vocab size: 1486K \nWords processed: 6700K Vocab size: 1501K \nWords processed: 6800K Vocab size: 1516K \nWords processed: 6900K Vocab size: 1530K \nWords processed: 7000K Vocab size: 1545K \nWords processed: 7100K Vocab size: 1560K \nWords processed: 7200K Vocab size: 1575K \nWords processed: 7300K Vocab size: 1589K \nWords processed: 7400K Vocab size: 1604K \nWords processed: 7500K Vocab size: 1618K \nWords processed: 7600K Vocab size: 1632K \nWords processed: 7700K Vocab size: 1646K \nWords processed: 7800K Vocab size: 1661K \nWords processed: 7900K Vocab size: 1675K \nWords processed: 8000K Vocab size: 1689K \nWords processed: 8100K Vocab size: 1703K \nWords processed: 8200K Vocab size: 1717K \nWords processed: 8300K Vocab size: 1731K \nWords processed: 8400K Vocab size: 1744K \nWords processed: 8500K Vocab size: 1758K \nWords processed: 8600K Vocab size: 1771K \nWords processed: 8700K Vocab size: 1785K \nWords processed: 8800K Vocab size: 1798K \nWords processed: 8900K Vocab size: 1812K \nWords processed: 9000K Vocab size: 1825K \nWords processed: 9100K Vocab size: 1839K \nWords processed: 9200K Vocab size: 1852K \nWords processed: 9300K Vocab size: 1865K \nWords processed: 9400K Vocab size: 1878K \nWords processed: 9500K Vocab size: 1892K \nWords processed: 9600K Vocab size: 1905K \nWords processed: 9700K Vocab size: 1918K \nWords processed: 9800K Vocab size: 1931K \nWords processed: 9900K Vocab size: 1943K \nWords processed: 10000K Vocab size: 1956K \nWords processed: 10100K Vocab size: 1969K \nWords processed: 10200K Vocab size: 1982K \nWords processed: 10300K Vocab size: 1995K \nWords processed: 10400K Vocab size: 2008K \nWords processed: 10500K Vocab size: 2020K \nWords processed: 10600K Vocab size: 2033K \nWords processed: 10700K Vocab size: 2045K \nWords processed: 10800K Vocab size: 2057K \nWords processed: 10900K Vocab size: 2070K \nWords processed: 11000K Vocab size: 2082K \nWords processed: 11100K Vocab size: 2094K \nWords processed: 11200K Vocab size: 2107K \nWords processed: 11300K Vocab size: 2119K \nWords processed: 11400K Vocab size: 2131K \nWords processed: 11500K Vocab size: 2143K \nWords processed: 11600K Vocab size: 2156K \nWords processed: 11700K Vocab size: 2168K \nWords processed: 11800K Vocab size: 2180K \nWords processed: 11900K Vocab size: 2192K \nWords processed: 12000K Vocab size: 2204K \nWords processed: 12100K Vocab size: 2216K \nWords processed: 12200K Vocab size: 2227K \nWords processed: 12300K Vocab size: 2239K \nWords processed: 12400K Vocab size: 2251K \nWords processed: 12500K Vocab size: 2263K \nWords processed: 12600K Vocab size: 2274K \nWords processed: 12700K Vocab size: 2286K \nWords processed: 12800K Vocab size: 2298K \nWords processed: 12900K Vocab size: 2310K \nWords processed: 13000K Vocab size: 2321K \nWords processed: 13100K Vocab size: 2333K \nWords processed: 13200K Vocab size: 2344K \nWords processed: 13300K Vocab size: 2355K \nWords processed: 13400K Vocab size: 2367K \nWords processed: 13500K Vocab size: 2378K \nWords processed: 13600K Vocab size: 2390K \nWords processed: 13700K Vocab size: 2401K \nWords processed: 13800K Vocab size: 2412K \nWords processed: 13900K Vocab size: 2424K \nWords processed: 14000K Vocab size: 2435K \nWords processed: 14100K Vocab size: 2446K \nWords processed: 14200K Vocab size: 2457K \nWords processed: 14300K Vocab size: 2469K \nWords processed: 14400K Vocab size: 2479K \nWords processed: 14500K Vocab size: 2490K \nWords processed: 14600K Vocab size: 2502K \nWords processed: 14700K Vocab size: 2513K \nWords processed: 14800K Vocab size: 2524K \nWords processed: 14900K Vocab size: 2535K \nWords processed: 15000K Vocab size: 2546K \nWords processed: 15100K Vocab size: 2557K \nWords processed: 15200K Vocab size: 2568K \nWords processed: 15300K Vocab size: 2579K \nWords processed: 15400K Vocab size: 2590K \nWords processed: 15500K Vocab size: 2600K \nWords processed: 15600K Vocab size: 2611K \nWords processed: 15700K Vocab size: 2622K \nWords processed: 15800K Vocab size: 2633K \nWords processed: 15900K Vocab size: 2644K \nWords processed: 16000K Vocab size: 2654K \nWords processed: 16100K Vocab size: 2665K \nWords processed: 16200K Vocab size: 2676K \nWords processed: 16300K Vocab size: 2687K \nWords processed: 16400K Vocab size: 2697K \nWords processed: 16500K Vocab size: 2708K \nWords processed: 16600K Vocab size: 2719K \nWords processed: 16700K Vocab size: 2729K \nWords processed: 16800K Vocab size: 2739K \nWords processed: 16900K Vocab size: 2750K \nWords processed: 17000K Vocab size: 2760K \nWords processed: 17100K Vocab size: 2771K \nWords processed: 17200K Vocab size: 2781K \nWords processed: 17300K Vocab size: 2792K \nWords processed: 17400K Vocab size: 2802K \nWords processed: 17500K Vocab size: 2813K \nWords processed: 17600K Vocab size: 2823K \nWords processed: 17700K Vocab size: 2833K \nWords processed: 17800K Vocab size: 2844K \nWords processed: 17900K Vocab size: 2854K \nWords processed: 18000K Vocab size: 2864K \nWords processed: 18100K Vocab size: 2875K \nWords processed: 18200K Vocab size: 2885K \nWords processed: 18300K Vocab size: 2896K \nWords processed: 18400K Vocab size: 2906K \nWords processed: 18500K Vocab size: 2916K \nWords processed: 18600K Vocab size: 2926K \nWords processed: 18700K Vocab size: 2936K \nWords processed: 18800K Vocab size: 2946K \nWords processed: 18900K Vocab size: 2956K \nWords processed: 19000K Vocab size: 2966K \nWords processed: 19100K Vocab size: 2975K \nWords processed: 19200K Vocab size: 2986K \nWords processed: 19300K Vocab size: 2995K \nWords processed: 19400K Vocab size: 3005K \nWords processed: 19500K Vocab size: 3015K \nWords processed: 19600K Vocab size: 3025K \nWords processed: 19700K Vocab size: 3035K \nWords processed: 19800K Vocab size: 3045K \nWords processed: 19900K Vocab size: 3055K \nWords processed: 20000K Vocab size: 3065K \nWords processed: 20100K Vocab size: 3075K \nWords processed: 20200K Vocab size: 3085K \nWords processed: 20300K Vocab size: 3095K \nWords processed: 20400K Vocab size: 3104K \nWords processed: 20500K Vocab size: 3114K \nWords processed: 20600K Vocab size: 3124K \nWords processed: 20700K Vocab size: 3133K \nWords processed: 20800K Vocab size: 3143K \nWords processed: 20900K Vocab size: 3153K \nWords processed: 21000K Vocab size: 3162K \nVocab size (unigrams + bigrams): 1814048\nWords in train file: 21011278\nWords written: 100K\nWords written: 200K\nWords written: 300K\nWords written: 400K\nWords written: 500K\nWords written: 600K\nWords written: 700K\nWords written: 800K\nWords written: 900K\nWords written: 1000K\nWords written: 1100K\nWords written: 1200K\nWords written: 1300K\nWords written: 1400K\nWords written: 1500K\nWords written: 1600K\nWords written: 1700K\nWords written: 1800K\nWords written: 1900K\nWords written: 2000K\nWords written: 2100K\nWords written: 2200K\nWords written: 2300K\nWords written: 2400K\nWords written: 2500K\nWords written: 2600K\nWords written: 2700K\nWords written: 2800K\nWords written: 2900K\nWords written: 3000K\nWords written: 3100K\nWords written: 3200K\nWords written: 3300K\nWords written: 3400K\nWords written: 3500K\nWords written: 3600K\nWords written: 3700K\nWords written: 3800K\nWords written: 3900K\nWords written: 4000K\nWords written: 4100K\nWords written: 4200K\nWords written: 4300K\nWords written: 4400K\nWords written: 4500K\nWords written: 4600K\nWords written: 4700K\nWords written: 4800K\nWords written: 4900K\nWords written: 5000K\nWords written: 5100K\nWords written: 5200K\nWords written: 5300K\nWords written: 5400K\nWords written: 5500K\nWords written: 5600K\nWords written: 5700K\nWords written: 5800K\nWords written: 5900K\nWords written: 6000K\nWords written: 6100K\nWords written: 6200K\nWords written: 6300K\nWords written: 6400K\nWords written: 6500K\nWords written: 6600K\nWords written: 6700K\nWords written: 6800K\nWords written: 6900K\nWords written: 7000K\nWords written: 7100K\nWords written: 7200K\nWords written: 7300K\nWords written: 7400K\nWords written: 7500K\nWords written: 7600K\nWords written: 7700K\nWords written: 7800K\nWords written: 7900K\nWords written: 8000K\nWords written: 8100K\nWords written: 8200K\nWords written: 8300K\nWords written: 8400K\nWords written: 8500K\nWords written: 8600K\nWords written: 8700K\nWords written: 8800K\nWords written: 8900K\nWords written: 9000K\nWords written: 9100K\nWords written: 9200K\nWords written: 9300K\nWords written: 9400K\nWords written: 9500K\nWords written: 9600K\nWords written: 9700K\nWords written: 9800K\nWords written: 9900K\nWords written: 10000K\nWords written: 10100K\nWords written: 10200K\nWords written: 10300K\nWords written: 10400K\nWords written: 10500K\nWords written: 10600K\nWords written: 10700K\nWords written: 10800K\nWords written: 10900K\nWords written: 11000K\nWords written: 11100K\nWords written: 11200K\nWords written: 11300K\nWords written: 11400K\nWords written: 11500K\nWords written: 11600K\nWords written: 11700K\nWords written: 11800K\nWords written: 11900K\nWords written: 12000K\nWords written: 12100K\nWords written: 12200K\nWords written: 12300K\nWords written: 12400K\nWords written: 12500K\nWords written: 12600K\nWords written: 12700K\nWords written: 12800K\nWords written: 12900K\nWords written: 13000K\nWords written: 13100K\nWords written: 13200K\nWords written: 13300K\nWords written: 13400K\nWords written: 13500K\nWords written: 13600K\nWords written: 13700K\nWords written: 13800K\nWords written: 13900K\nWords written: 14000K\nWords written: 14100K\nWords written: 14200K\nWords written: 14300K\nWords written: 14400K\nWords written: 14500K\nWords written: 14600K\nWords written: 14700K\nWords written: 14800K\nWords written: 14900K\nWords written: 15000K\nWords written: 15100K\nWords written: 15200K\nWords written: 15300K\nWords written: 15400K\nWords written: 15500K\nWords written: 15600K\nWords written: 15700K\nWords written: 15800K\nWords written: 15900K\nWords written: 16000K\nWords written: 16100K\nWords written: 16200K\nWords written: 16300K\nWords written: 16400K\nWords written: 16500K\nWords written: 16600K\nWords written: 16700K\nWords written: 16800K\nWords written: 16900K\nWords written: 17000K\nWords written: 17100K\nWords written: 17200K\nWords written: 17300K\nWords written: 17400K\nWords written: 17500K\nWords written: 17600K\nWords written: 17700K\nWords written: 17800K\nWords written: 17900K\nWords written: 18000K\nWords written: 18100K\nWords written: 18200K\nWords written: 18300K\nWords written: 18400K\nWords written: 18500K\nWords written: 18600K\nWords written: 18700K\nWords written: 18800K\nWords written: 18900K\nWords written: 19000K\nWords written: 19100K\nWords written: 19200K\nWords written: 19300K\nWords written: 19400K\nWords written: 19500K\nWords written: 19600K\nWords written: 19700K\nWords written: 19800K\nWords written: 19900K\nWords written: 20000K\nWords written: 20100K\nWords written: 20200K\nWords written: 20300K\nWords written: 20400K\nWords written: 20500K\nWords written: 20600K\nWords written: 20700K\nWords written: 20800K\nWords written: 20900K\nWords written: 21000K\n\n\n\nif (!file.exists(\"./cache/noticias_vectors.bin\")) {\n modelo <- train_word2vec(\"./cache/noticias_w2v.txt\", \n \"./cache/noticias_vectors.bin\",\n vectors = 100, threads = 8, window = 4, cbow = 0, \n iter = 20, negative_samples = 20, min_count = 10) \n} else {\n modelo <- read.vectors(\"./cache/noticias_vectors.bin\")\n}\n\nEl resultado son los vectores aprendidos de las palabras, por ejemplo\n\nvector_gol <- modelo[[\"gol\"]] |> as.numeric()\nvector_gol\n\n [1] -0.389627248 0.048135430 0.501164794 -0.035961468 0.291238993\n [6] 0.642733335 -0.386596769 0.281559557 -0.199183896 -0.554564893\n [11] 0.451201737 0.495587140 -0.525584400 0.166191801 -0.180947676\n [16] 0.034590811 0.731496751 0.259901792 -0.201457486 -0.308042079\n [21] -0.177875623 -0.220428273 0.408699900 0.001920983 0.011449666\n [26] -0.718980432 0.153631359 -0.049470965 0.981541216 0.082757361\n [31] -0.331263602 0.458369821 -0.429754555 0.128275126 -0.421742797\n [36] 0.596242130 -0.093633644 0.066455603 -0.016802812 -0.301688135\n [41] 0.079358041 0.446704596 -0.244078919 -0.137954682 0.695054173\n [46] 0.335903019 0.216709450 0.604890466 -0.538004100 -0.291783333\n [51] -0.579949379 -0.048889056 0.324184030 -0.055591993 -0.012452535\n [56] -0.200338170 0.254620761 0.082836255 0.389545202 -0.185363784\n [61] -0.021011911 0.307440221 0.415608138 0.248776823 -0.139897019\n [66] 0.008641024 0.235776618 0.324411124 -0.171800703 0.131596789\n [71] -0.163520932 0.370538741 -0.134094939 -0.193797469 -0.543500543\n [76] 0.312639445 -0.172534481 -0.115350038 -0.293528855 -0.534602344\n [81] 0.515545666 0.708557248 0.444676250 -0.054800753 0.388787180\n [86] 0.483029991 0.281573176 0.434132993 0.441057146 -0.347387016\n [91] -0.174339339 0.060069371 -0.034651209 0.407196820 0.661161661\n [96] 0.261399239 -0.089392163 -0.043052837 -0.539683878 0.105241157" + "section": "18.3 Modelos de word2vec", + "text": "18.3 Modelos de word2vec\nEn estos ejemplos veremos cómo producir embeddings de palabras que son precursores de embeddings más refinados como los producidos por Modelos grandes de lenguajes (LLMs). Ver por ejemplo aquí.\nSi lo que principalmente nos interesa es obtener una representación vectorial de palabras, es posible simplificar considerablemente el modelo de arriba o LLMs para poder entrenarlos mucho más rápido, y obtener una representación que en muchas tareas se desempeña bien ((Mikolov et al. 2013)).\nHay dos ideas básicas que se pueden usar para reducir la complejidad del entrenamiento (ver más en (Mikolov et al. 2013)):\n\nEliminar la segunda capa oculta: modelo de bag-of-words continuo y modelo de skip-gram.\nCambiar la función objetivo (minimizar devianza/maximizar verosimilitud) por una más simple, mediante un truco que se llama negative sampling.\n\nComo ya no es de interés central predecir la siguiente palabra a partir de las anteriores, en estos modelos intentamos predecir la palabra central a partir de las que están alrededor.\n\nArquitectura continuous bag-of-words\nLa entrada es igual que en el modelo completo. En primer lugar, simplificamos la segunda capa oculta pondiendo en \\(z\\) el promedio de los vectores \\(C(w_{n-2}), C(w_{n-1})\\). La última capa la dejamos igual por el momento:\n\n\n\nImagen\n\n\nEl modelo se llama bag-of-words porque todas las entradas de la primera capa oculta contribuyen de la misma manera en la salida, independientemente del orden. Aunque esto no suena como buena idea para construir un modelo de lenguaje, veremos que resulta en una representación adecuada para algunos problemas.\n\nEn la primera capa oculta, tenemos un mapeo de las entradas \\(w_1,\\ldots, w_{n-1}\\) a \\(x=C(w_1),\\ldots, C(w_{n-1})\\), donde \\(C\\) es una función que mapea palabras a vectores de dimensión \\(d\\). \\(C\\) también se puede pensar como una matriz de dimensión \\(|V|\\) por \\(d\\). En la capa de entrada,\n\n\\[w_{n-2},w_{n-1} \\to x = (C(w_{n-2}), C(w_{n-1})).\\]\n\nEn la siguiente “capa” oculta simplemente sumamos las entradas de \\(x\\). Aquí nótese que realmente no hay parámetros.\nFinalmente, la capa de salida debe ser un vector de probabilidades sobre todo el vocabulario \\(|V|\\). En esta capa tenemos pesos \\(U\\) y hacemos \\[y = b + U\\sigma (z),\\] y finalmente usamos softmax para tener probabilidades que suman uno: \\[p_i = \\frac{\\exp (y_i) }{\\sum_j exp(y_j)}.\\]\n\nEn el ajuste maximizamos la verosimilitud sobre el corpus. Por ejemplo, para una frase, su log verosimilitud es:\n\\[\\sum_t \\log \\hat{P}(w_{t,n}|w_{t,n+1} \\cdots w_{t-n-1}) \\]\n\n\nArquitectura skip-grams\nOtro modelo simplificado, con más complejidad computacional pero mejores resultados (ver (Mikolov et al. 2013)) que el bag-of-words, es el modelo de skip-grams. En este caso, dada cada palabra que encontramos, intentamos predecir un número fijo de las palabras anteriores y palabras posteriores (el contexto es una vecindad de la palabra).\n\n\n\nImagen\n\n\nLa función objetivo se defina ahora (simplificando) como suma sobre \\(t\\):\n\\[-\\sum_t \\sum_{ -2\\leq j \\leq 2, j\\neq 0} \\log P(w_{t-j} | w_t)\\] (no tomamos en cuenta dónde aparece exactamente \\(w_{t-j}\\) en relación a \\(w_t\\), simplemente consideramos que está en su contexto), donde\n\\[\\log P(w_{t-j}|w_t) = u_{t-j}^tC(w_t) - \\log\\sum_k \\exp{u_{k}^tC(w_t)}\\]\nTodavía se propone una simplificación adicional que resulta ser efectiva:\n\n\nMuestreo negativo\nLa siguiente simplificación consiste en cambiar la función objetivo. En word2vec puede usarse “muestreo negativo”.\nPara empezar, la función objetivo original (para contexto de una sola palabra) es\n\\[E = -\\log \\hat{P}(w_{a}|w_{n}) = -y_{w_a} + \\log\\sum_j \\exp(y_j),\\]\ndonde las \\(y_i\\) son las salidas de la penúltima capa. La dificultad está en el segundo término, que es sobre todo el vocabulario en incluye todos los parámetros del modelo (hay que calcular las parciales de \\(y_j\\)’s sobre cada una de las palabras del vocabulario).\nLa idea del muestreo negativo es que si \\(w_a\\) está en el contexto de \\(w_{n}\\), tomamos una muestra de \\(k\\) palabras \\(v_1,\\ldots v_k\\) al azar (2-50, dependiendo del tamaño de la colección), y creamos \\(k\\) “contextos falsos” \\(v_j w_{n}\\), \\(j=1\\ldots,k\\). Minimizamos en lugar de la observación de arriba\n\\[E = -\\log\\sigma(y_{w_a}) + \\sum_{j=1}^k \\log\\sigma(y_j),\\] en donde queremos maximizar la probabilidad de que ocurra \\(w_a\\) vs. la probabilidad de que ocurra alguna de las \\(v_j\\). Es decir, solo buscamos optimizar parámetros para separar lo mejor que podamos la observación de \\(k\\) observaciones falsas, lo cual implica que tenemos que mover un número relativamente chico de parámetros (en lugar de todos los parámetros de todas las palabras del vocabulario).\nLas palabras “falsas” se escogen según una probabilidad ajustada de unigramas (se observó empíricamente mejor desempeño cuando escogemos cada palabra con probabilidad proporcional a \\(P(w)^{3/4}\\), en lugar de \\(P(w)\\), ver (Mikolov et al. 2013)).\n\n\nEjemplo\n\nif(!require(wordVectors)){\n devtools::install_github(\"bmschmidt/wordVectors\", \n dependencies = TRUE)\n}\n\n\n── R CMD build ─────────────────────────────────────────────────────────────────\n* checking for file ‘/tmp/RtmpPIgqH8/remotes8fbba8c9463/bmschmidt-wordVectors-ad127c1/DESCRIPTION’ ... OK\n* preparing ‘wordVectors’:\n* checking DESCRIPTION meta-information ... OK\n* cleaning src\n* checking for LF line-endings in source and make files and shell scripts\n* checking for empty or unneeded directories\n* building ‘wordVectors_2.0.tar.gz’\n\nlibrary(wordVectors)\n\n\nlibrary(tidyverse)\nruta <- \"../datos/noticias/ES_Newspapers.txt\"\nif(!file.exists(ruta)){\n periodico <- \n read_lines(file= \"https://es-noticias.s3.amazonaws.com/Es_Newspapers.txt\",\n progress = FALSE)\n write_lines(periodico, ruta)\n} else {\n periodico <- read_lines(file= ruta,\n progress = FALSE)\n}\nnormalizar <- function(texto, vocab = NULL){\n # minúsculas\n texto <- tolower(texto)\n # varios ajustes\n texto <- gsub(\"\\\\s+\", \" \", texto)\n texto <- gsub(\"\\\\.[^0-9]\", \" _punto_ \", texto)\n texto <- gsub(\" _s_ $\", \"\", texto)\n texto <- gsub(\"\\\\.\", \" _punto_ \", texto)\n texto <- gsub(\"[«»¡!¿?-]\", \"\", texto) \n texto <- gsub(\";\", \" _punto_coma_ \", texto) \n texto <- gsub(\"\\\\:\", \" _dos_puntos_ \", texto) \n texto <- gsub(\"\\\\,[^0-9]\", \" _coma_ \", texto)\n texto <- gsub(\"\\\\s+\", \" \", texto)\n texto\n}\nperiodico_df <- tibble(txt = periodico) |>\n mutate(id = row_number()) |>\n mutate(txt = normalizar(txt))\n\nConstruimos un modelo con vectores de palabras de tamaño 100, skip-grams de tamaño 4, y ajustamos con muestreo negativo de tamaño 20:\n\nif(!file.exists('./cache/noticias_w2v.txt')){\n tmp <- tempfile()\n # tokenización\n write_lines(periodico_df$txt, tmp)\n prep <- prep_word2vec(tmp, \n destination = './cache/noticias_w2v.txt', bundle_ngrams = 2)\n } \n\nBeginning tokenization to text file at ./cache/noticias_w2v.txt\n\n\nPrepping /tmp/RtmpPIgqH8/file8fbb3f22963d\n\n\nStarting training using file ./cache/noticias_w2v.txt\nWords processed: 100K Vocab size: 73K \nWords processed: 200K Vocab size: 124K \nWords processed: 300K Vocab size: 168K \nWords processed: 400K Vocab size: 209K \nWords processed: 500K Vocab size: 247K \nWords processed: 600K Vocab size: 281K \nWords processed: 700K Vocab size: 314K \nWords processed: 800K Vocab size: 346K \nWords processed: 900K Vocab size: 376K \nWords processed: 1000K Vocab size: 406K \nWords processed: 1100K Vocab size: 434K \nWords processed: 1200K Vocab size: 462K \nWords processed: 1300K Vocab size: 489K \nWords processed: 1400K Vocab size: 515K \nWords processed: 1500K Vocab size: 540K \nWords processed: 1600K Vocab size: 565K \nWords processed: 1700K Vocab size: 590K \nWords processed: 1800K Vocab size: 613K \nWords processed: 1900K Vocab size: 637K \nWords processed: 2000K Vocab size: 661K \nWords processed: 2100K Vocab size: 684K \nWords processed: 2200K Vocab size: 706K \nWords processed: 2300K Vocab size: 729K \nWords processed: 2400K Vocab size: 750K \nWords processed: 2500K Vocab size: 771K \nWords processed: 2600K Vocab size: 792K \nWords processed: 2700K Vocab size: 813K \nWords processed: 2800K Vocab size: 834K \nWords processed: 2900K Vocab size: 854K \nWords processed: 3000K Vocab size: 873K \nWords processed: 3100K Vocab size: 893K \nWords processed: 3200K Vocab size: 913K \nWords processed: 3300K Vocab size: 932K \nWords processed: 3400K Vocab size: 951K \nWords processed: 3500K Vocab size: 970K \nWords processed: 3600K Vocab size: 989K \nWords processed: 3700K Vocab size: 1007K \nWords processed: 3800K Vocab size: 1026K \nWords processed: 3900K Vocab size: 1044K \nWords processed: 4000K Vocab size: 1062K \nWords processed: 4100K Vocab size: 1080K \nWords processed: 4200K Vocab size: 1098K \nWords processed: 4300K Vocab size: 1115K \nWords processed: 4400K Vocab size: 1132K \nWords processed: 4500K Vocab size: 1150K \nWords processed: 4600K Vocab size: 1167K \nWords processed: 4700K Vocab size: 1184K \nWords processed: 4800K Vocab size: 1201K \nWords processed: 4900K Vocab size: 1218K \nWords processed: 5000K Vocab size: 1235K \nWords processed: 5100K Vocab size: 1252K \nWords processed: 5200K Vocab size: 1268K \nWords processed: 5300K Vocab size: 1285K \nWords processed: 5400K Vocab size: 1301K \nWords processed: 5500K Vocab size: 1317K \nWords processed: 5600K Vocab size: 1333K \nWords processed: 5700K Vocab size: 1349K \nWords processed: 5800K Vocab size: 1364K \nWords processed: 5900K Vocab size: 1380K \nWords processed: 6000K Vocab size: 1395K \nWords processed: 6100K Vocab size: 1411K \nWords processed: 6200K Vocab size: 1426K \nWords processed: 6300K Vocab size: 1441K \nWords processed: 6400K Vocab size: 1456K \nWords processed: 6500K Vocab size: 1471K \nWords processed: 6600K Vocab size: 1486K \nWords processed: 6700K Vocab size: 1501K \nWords processed: 6800K Vocab size: 1516K \nWords processed: 6900K Vocab size: 1530K \nWords processed: 7000K Vocab size: 1545K \nWords processed: 7100K Vocab size: 1560K \nWords processed: 7200K Vocab size: 1575K \nWords processed: 7300K Vocab size: 1589K \nWords processed: 7400K Vocab size: 1604K \nWords processed: 7500K Vocab size: 1618K \nWords processed: 7600K Vocab size: 1632K \nWords processed: 7700K Vocab size: 1646K \nWords processed: 7800K Vocab size: 1661K \nWords processed: 7900K Vocab size: 1675K \nWords processed: 8000K Vocab size: 1689K \nWords processed: 8100K Vocab size: 1703K \nWords processed: 8200K Vocab size: 1717K \nWords processed: 8300K Vocab size: 1731K \nWords processed: 8400K Vocab size: 1744K \nWords processed: 8500K Vocab size: 1758K \nWords processed: 8600K Vocab size: 1771K \nWords processed: 8700K Vocab size: 1785K \nWords processed: 8800K Vocab size: 1798K \nWords processed: 8900K Vocab size: 1812K \nWords processed: 9000K Vocab size: 1825K \nWords processed: 9100K Vocab size: 1839K \nWords processed: 9200K Vocab size: 1852K \nWords processed: 9300K Vocab size: 1865K \nWords processed: 9400K Vocab size: 1878K \nWords processed: 9500K Vocab size: 1892K \nWords processed: 9600K Vocab size: 1905K \nWords processed: 9700K Vocab size: 1918K \nWords processed: 9800K Vocab size: 1931K \nWords processed: 9900K Vocab size: 1943K \nWords processed: 10000K Vocab size: 1956K \nWords processed: 10100K Vocab size: 1969K \nWords processed: 10200K Vocab size: 1982K \nWords processed: 10300K Vocab size: 1995K \nWords processed: 10400K Vocab size: 2008K \nWords processed: 10500K Vocab size: 2020K \nWords processed: 10600K Vocab size: 2033K \nWords processed: 10700K Vocab size: 2045K \nWords processed: 10800K Vocab size: 2057K \nWords processed: 10900K Vocab size: 2070K \nWords processed: 11000K Vocab size: 2082K \nWords processed: 11100K Vocab size: 2094K \nWords processed: 11200K Vocab size: 2107K \nWords processed: 11300K Vocab size: 2119K \nWords processed: 11400K Vocab size: 2131K \nWords processed: 11500K Vocab size: 2143K \nWords processed: 11600K Vocab size: 2156K \nWords processed: 11700K Vocab size: 2168K \nWords processed: 11800K Vocab size: 2180K \nWords processed: 11900K Vocab size: 2192K \nWords processed: 12000K Vocab size: 2204K \nWords processed: 12100K Vocab size: 2216K \nWords processed: 12200K Vocab size: 2227K \nWords processed: 12300K Vocab size: 2239K \nWords processed: 12400K Vocab size: 2251K \nWords processed: 12500K Vocab size: 2263K \nWords processed: 12600K Vocab size: 2274K \nWords processed: 12700K Vocab size: 2286K \nWords processed: 12800K Vocab size: 2298K \nWords processed: 12900K Vocab size: 2310K \nWords processed: 13000K Vocab size: 2321K \nWords processed: 13100K Vocab size: 2333K \nWords processed: 13200K Vocab size: 2344K \nWords processed: 13300K Vocab size: 2355K \nWords processed: 13400K Vocab size: 2367K \nWords processed: 13500K Vocab size: 2378K \nWords processed: 13600K Vocab size: 2390K \nWords processed: 13700K Vocab size: 2401K \nWords processed: 13800K Vocab size: 2412K \nWords processed: 13900K Vocab size: 2424K \nWords processed: 14000K Vocab size: 2435K \nWords processed: 14100K Vocab size: 2446K \nWords processed: 14200K Vocab size: 2457K \nWords processed: 14300K Vocab size: 2469K \nWords processed: 14400K Vocab size: 2479K \nWords processed: 14500K Vocab size: 2490K \nWords processed: 14600K Vocab size: 2502K \nWords processed: 14700K Vocab size: 2513K \nWords processed: 14800K Vocab size: 2524K \nWords processed: 14900K Vocab size: 2535K \nWords processed: 15000K Vocab size: 2546K \nWords processed: 15100K Vocab size: 2557K \nWords processed: 15200K Vocab size: 2568K \nWords processed: 15300K Vocab size: 2579K \nWords processed: 15400K Vocab size: 2590K \nWords processed: 15500K Vocab size: 2600K \nWords processed: 15600K Vocab size: 2611K \nWords processed: 15700K Vocab size: 2622K \nWords processed: 15800K Vocab size: 2633K \nWords processed: 15900K Vocab size: 2644K \nWords processed: 16000K Vocab size: 2654K \nWords processed: 16100K Vocab size: 2665K \nWords processed: 16200K Vocab size: 2676K \nWords processed: 16300K Vocab size: 2687K \nWords processed: 16400K Vocab size: 2697K \nWords processed: 16500K Vocab size: 2708K \nWords processed: 16600K Vocab size: 2719K \nWords processed: 16700K Vocab size: 2729K \nWords processed: 16800K Vocab size: 2739K \nWords processed: 16900K Vocab size: 2750K \nWords processed: 17000K Vocab size: 2760K \nWords processed: 17100K Vocab size: 2771K \nWords processed: 17200K Vocab size: 2781K \nWords processed: 17300K Vocab size: 2792K \nWords processed: 17400K Vocab size: 2802K \nWords processed: 17500K Vocab size: 2813K \nWords processed: 17600K Vocab size: 2823K \nWords processed: 17700K Vocab size: 2833K \nWords processed: 17800K Vocab size: 2844K \nWords processed: 17900K Vocab size: 2854K \nWords processed: 18000K Vocab size: 2864K \nWords processed: 18100K Vocab size: 2875K \nWords processed: 18200K Vocab size: 2885K \nWords processed: 18300K Vocab size: 2896K \nWords processed: 18400K Vocab size: 2906K \nWords processed: 18500K Vocab size: 2916K \nWords processed: 18600K Vocab size: 2926K \nWords processed: 18700K Vocab size: 2936K \nWords processed: 18800K Vocab size: 2946K \nWords processed: 18900K Vocab size: 2956K \nWords processed: 19000K Vocab size: 2966K \nWords processed: 19100K Vocab size: 2975K \nWords processed: 19200K Vocab size: 2986K \nWords processed: 19300K Vocab size: 2995K \nWords processed: 19400K Vocab size: 3005K \nWords processed: 19500K Vocab size: 3015K \nWords processed: 19600K Vocab size: 3025K \nWords processed: 19700K Vocab size: 3035K \nWords processed: 19800K Vocab size: 3045K \nWords processed: 19900K Vocab size: 3055K \nWords processed: 20000K Vocab size: 3065K \nWords processed: 20100K Vocab size: 3075K \nWords processed: 20200K Vocab size: 3085K \nWords processed: 20300K Vocab size: 3095K \nWords processed: 20400K Vocab size: 3104K \nWords processed: 20500K Vocab size: 3114K \nWords processed: 20600K Vocab size: 3124K \nWords processed: 20700K Vocab size: 3133K \nWords processed: 20800K Vocab size: 3143K \nWords processed: 20900K Vocab size: 3153K \nWords processed: 21000K Vocab size: 3162K \nVocab size (unigrams + bigrams): 1814048\nWords in train file: 21011278\nWords written: 100K\nWords written: 200K\nWords written: 300K\nWords written: 400K\nWords written: 500K\nWords written: 600K\nWords written: 700K\nWords written: 800K\nWords written: 900K\nWords written: 1000K\nWords written: 1100K\nWords written: 1200K\nWords written: 1300K\nWords written: 1400K\nWords written: 1500K\nWords written: 1600K\nWords written: 1700K\nWords written: 1800K\nWords written: 1900K\nWords written: 2000K\nWords written: 2100K\nWords written: 2200K\nWords written: 2300K\nWords written: 2400K\nWords written: 2500K\nWords written: 2600K\nWords written: 2700K\nWords written: 2800K\nWords written: 2900K\nWords written: 3000K\nWords written: 3100K\nWords written: 3200K\nWords written: 3300K\nWords written: 3400K\nWords written: 3500K\nWords written: 3600K\nWords written: 3700K\nWords written: 3800K\nWords written: 3900K\nWords written: 4000K\nWords written: 4100K\nWords written: 4200K\nWords written: 4300K\nWords written: 4400K\nWords written: 4500K\nWords written: 4600K\nWords written: 4700K\nWords written: 4800K\nWords written: 4900K\nWords written: 5000K\nWords written: 5100K\nWords written: 5200K\nWords written: 5300K\nWords written: 5400K\nWords written: 5500K\nWords written: 5600K\nWords written: 5700K\nWords written: 5800K\nWords written: 5900K\nWords written: 6000K\nWords written: 6100K\nWords written: 6200K\nWords written: 6300K\nWords written: 6400K\nWords written: 6500K\nWords written: 6600K\nWords written: 6700K\nWords written: 6800K\nWords written: 6900K\nWords written: 7000K\nWords written: 7100K\nWords written: 7200K\nWords written: 7300K\nWords written: 7400K\nWords written: 7500K\nWords written: 7600K\nWords written: 7700K\nWords written: 7800K\nWords written: 7900K\nWords written: 8000K\nWords written: 8100K\nWords written: 8200K\nWords written: 8300K\nWords written: 8400K\nWords written: 8500K\nWords written: 8600K\nWords written: 8700K\nWords written: 8800K\nWords written: 8900K\nWords written: 9000K\nWords written: 9100K\nWords written: 9200K\nWords written: 9300K\nWords written: 9400K\nWords written: 9500K\nWords written: 9600K\nWords written: 9700K\nWords written: 9800K\nWords written: 9900K\nWords written: 10000K\nWords written: 10100K\nWords written: 10200K\nWords written: 10300K\nWords written: 10400K\nWords written: 10500K\nWords written: 10600K\nWords written: 10700K\nWords written: 10800K\nWords written: 10900K\nWords written: 11000K\nWords written: 11100K\nWords written: 11200K\nWords written: 11300K\nWords written: 11400K\nWords written: 11500K\nWords written: 11600K\nWords written: 11700K\nWords written: 11800K\nWords written: 11900K\nWords written: 12000K\nWords written: 12100K\nWords written: 12200K\nWords written: 12300K\nWords written: 12400K\nWords written: 12500K\nWords written: 12600K\nWords written: 12700K\nWords written: 12800K\nWords written: 12900K\nWords written: 13000K\nWords written: 13100K\nWords written: 13200K\nWords written: 13300K\nWords written: 13400K\nWords written: 13500K\nWords written: 13600K\nWords written: 13700K\nWords written: 13800K\nWords written: 13900K\nWords written: 14000K\nWords written: 14100K\nWords written: 14200K\nWords written: 14300K\nWords written: 14400K\nWords written: 14500K\nWords written: 14600K\nWords written: 14700K\nWords written: 14800K\nWords written: 14900K\nWords written: 15000K\nWords written: 15100K\nWords written: 15200K\nWords written: 15300K\nWords written: 15400K\nWords written: 15500K\nWords written: 15600K\nWords written: 15700K\nWords written: 15800K\nWords written: 15900K\nWords written: 16000K\nWords written: 16100K\nWords written: 16200K\nWords written: 16300K\nWords written: 16400K\nWords written: 16500K\nWords written: 16600K\nWords written: 16700K\nWords written: 16800K\nWords written: 16900K\nWords written: 17000K\nWords written: 17100K\nWords written: 17200K\nWords written: 17300K\nWords written: 17400K\nWords written: 17500K\nWords written: 17600K\nWords written: 17700K\nWords written: 17800K\nWords written: 17900K\nWords written: 18000K\nWords written: 18100K\nWords written: 18200K\nWords written: 18300K\nWords written: 18400K\nWords written: 18500K\nWords written: 18600K\nWords written: 18700K\nWords written: 18800K\nWords written: 18900K\nWords written: 19000K\nWords written: 19100K\nWords written: 19200K\nWords written: 19300K\nWords written: 19400K\nWords written: 19500K\nWords written: 19600K\nWords written: 19700K\nWords written: 19800K\nWords written: 19900K\nWords written: 20000K\nWords written: 20100K\nWords written: 20200K\nWords written: 20300K\nWords written: 20400K\nWords written: 20500K\nWords written: 20600K\nWords written: 20700K\nWords written: 20800K\nWords written: 20900K\nWords written: 21000K\n\n\n\nif (!file.exists(\"./cache/noticias_vectors.bin\")) {\n modelo <- train_word2vec(\"./cache/noticias_w2v.txt\", \n \"./cache/noticias_vectors.bin\",\n vectors = 100, threads = 8, window = 4, cbow = 0, \n iter = 20, negative_samples = 20, min_count = 10) \n} else {\n modelo <- read.vectors(\"./cache/noticias_vectors.bin\")\n}\n\nEl resultado son los vectores aprendidos de las palabras, por ejemplo\n\nvector_gol <- modelo[[\"gol\"]] |> as.numeric()\nvector_gol\n\n [1] -0.389627248 0.048135430 0.501164794 -0.035961468 0.291238993\n [6] 0.642733335 -0.386596769 0.281559557 -0.199183896 -0.554564893\n [11] 0.451201737 0.495587140 -0.525584400 0.166191801 -0.180947676\n [16] 0.034590811 0.731496751 0.259901792 -0.201457486 -0.308042079\n [21] -0.177875623 -0.220428273 0.408699900 0.001920983 0.011449666\n [26] -0.718980432 0.153631359 -0.049470965 0.981541216 0.082757361\n [31] -0.331263602 0.458369821 -0.429754555 0.128275126 -0.421742797\n [36] 0.596242130 -0.093633644 0.066455603 -0.016802812 -0.301688135\n [41] 0.079358041 0.446704596 -0.244078919 -0.137954682 0.695054173\n [46] 0.335903019 0.216709450 0.604890466 -0.538004100 -0.291783333\n [51] -0.579949379 -0.048889056 0.324184030 -0.055591993 -0.012452535\n [56] -0.200338170 0.254620761 0.082836255 0.389545202 -0.185363784\n [61] -0.021011911 0.307440221 0.415608138 0.248776823 -0.139897019\n [66] 0.008641024 0.235776618 0.324411124 -0.171800703 0.131596789\n [71] -0.163520932 0.370538741 -0.134094939 -0.193797469 -0.543500543\n [76] 0.312639445 -0.172534481 -0.115350038 -0.293528855 -0.534602344\n [81] 0.515545666 0.708557248 0.444676250 -0.054800753 0.388787180\n [86] 0.483029991 0.281573176 0.434132993 0.441057146 -0.347387016\n [91] -0.174339339 0.060069371 -0.034651209 0.407196820 0.661161661\n [96] 0.261399239 -0.089392163 -0.043052837 -0.539683878 0.105241157" }, { "objectID": "16-recom-factorizacion.html#esprep", "href": "16-recom-factorizacion.html#esprep", "title": "16  Dimensiones latentes: embeddings", - "section": "17.4 Espacio de representación de palabras", - "text": "17.4 Espacio de representación de palabras\nComo discutimos arriba, palabras que se usan en contextos similares por su significado o por su función (por ejemplo, “perro” y “gato”“) deben tener representaciones similares, pues su contexto tiende a ser similar. La similitud que usamos el similitud coseno.\nPodemos verificar con nuestro ejemplo:\n\nejemplos <- modelo |> closest_to(\"gol\", n = 5)\nejemplos\n\n word similarity to \"gol\"\n1 gol 1.0000000\n2 golazo 0.8252912\n3 segundo_gol 0.7978025\n4 penalti 0.7764458\n5 potente_disparo 0.7755037\n\n\nTambién podríamos calcular manualmente:\nQue también podemos calcular como:\n\nvector_penalti <- modelo[[\"penalti\"]] |> as.numeric()\ncosineSimilarity(modelo[[\"gol\"]], modelo[[\"penalti\"]])\n\n [,1]\n[1,] 0.7764458\n\n\nO directamente:\n\nnorma <- function(x) sqrt(sum(x^2))\nsum(vector_gol * vector_penalti) / (norma(vector_gol) * norma(vector_penalti))\n\n[1] 0.7764458\n\n\n\nGeometría en el espacio de representaciones\nAhora consideremos cómo se distribuyen las palabras en este espacio, y si existe estructura geométrica en este espacio que tenga información acerca del lenguaje.\nConsideremos primero el caso de plurales de sustantivos.\n\nComo el contexto de los plurales es distinto de los singulares, nuestro modelo debería poder capturar en los vectores su diferencia.\nExaminamos entonces cómo son geométricamente diferentes las representaciones de plurales vs singulares\nSi encontramos un patrón reconocible, podemos utilizar este patrón, por ejemplo, para encontrar la versión plural de una palabra singular, sin usar ninguna regla del lenguaje.\n\nUna de las relaciones geométricas más simples es la adición de vectores. Por ejemplo, extraemos la diferencia entre gol y goles:\n\nejemplos <- modelo |> closest_to(\"dos\", n = 15)\nejemplos\n\n word similarity to \"dos\"\n1 dos 1.0000000\n2 tres 0.9666800\n3 cuatro 0.9527403\n4 cinco 0.9205234\n5 siete 0.9024807\n6 seis 0.8977667\n7 ocho 0.8879153\n8 nueve 0.8550580\n9 trece 0.8514542\n10 catorce 0.8321762\n11 diez 0.8133345\n12 quince 0.8102052\n13 doce 0.8085939\n14 once 0.8033385\n15 veinte 0.7814970\n\n\n\nejemplos <- modelo |> closest_to(c(\"lluvioso\"), n = 5)\nejemplos\n\n word similarity to c(\"lluvioso\")\n1 lluvioso 1.0000000\n2 caluroso 0.8041209\n3 cálido 0.6896448\n4 húmedo 0.6866749\n5 gélido 0.6660152\n\n\n\nejemplos <- modelo |> closest_to(\"presidente\", n = 5)\nejemplos\n\n word similarity to \"presidente\"\n1 presidente 1.0000000\n2 vicepresidente 0.8412900\n3 ex_presidente 0.8321029\n4 máximo_representante 0.7781001\n5 máximo_dirigente 0.7629962\n\n\n\nejemplos <- modelo |> closest_to(\"parís\", n = 5)\nejemplos\n\n word similarity to \"parís\"\n1 parís 1.0000000\n2 londres 0.9232452\n3 nueva_york 0.8464673\n4 roma 0.8443222\n5 berlín 0.8081766\n\n\nY vemos, por ejemplo, que el modelo puede capturar conceptos relacionados con el estado del clima, capitales de países y números - aún cuando no hemos anotado estas funciones en el corpus original. Estos vectores son similares porque tienden a ocurrir en contextos similares.\n\n\nGeometría en el espacio de representaciones\nAhora consideremos cómo se distribuyen las palabras en este espacio, y si existe estructura geométrica en este espacio que tenga información acerca del lenguaje.\nConsideremos primero el caso de plurales de sustantivos.\n\nComo el contexto de los plurales es distinto de los singulares, nuestro modelo debería poder capturar en los vectores su diferencia.\nExaminamos entonces cómo son geométricamente diferentes las representaciones de plurales vs singulares\nSi encontramos un patrón reconocible, podemos utilizar este patrón, por ejemplo, para encontrar la versión plural de una palabra singular, sin usar ninguna regla del lenguaje.\n\nUna de las relaciones geométricas más simples es la adición de vectores. Por ejemplo, extraemos la diferencia entre gol y goles:\n\nplural_1 <- modelo[[\"goles\"]] - modelo[[\"gol\"]]\nplural_1\n\nA VectorSpaceModel object of 1 words and 100 vectors\n [,1] [,2] [,3] [,4] [,5] [,6]\n[1,] -0.2301596 -0.2543171 -0.04071745 -0.2292878 0.004059255 -0.2283908\nattr(,\".cache\")\n<environment: 0x55c12c6f72e8>\n\n\nque es un vector en el espacio de representación de palabras. Ahora sumamos este vector a un sustantivo en singular, y vemos qué palabras están cercas de esta “palabra sintética”:\n\nclosest_to(modelo, ~ \"partido\" + \"goles\" - \"gol\", n = 5)\n\n word similarity to \"partido\" + \"goles\" - \"gol\"\n1 partidos 0.7961097\n2 goles 0.7589920\n3 partidos_disputados 0.6937101\n4 últimos_encuentros 0.6788752\n5 encuentros 0.6697611\n\n\nNótese que la más cercana es justamente el plural correcto, o otros plurales con relación al que buscábamos (como encuentros)\nOtro ejemplo:\n\nclosest_to(modelo, ~ \"mes\" + \"días\" - \"día\", n = 20) \n\n word similarity to \"mes\" + \"días\" - \"día\"\n1 tres_meses 0.7858109\n2 días 0.7708776\n3 meses 0.7655628\n4 diez_días 0.7199002\n5 seis_meses 0.7110105\n6 quince_días 0.7092209\n7 nueve_meses 0.6903626\n8 doce_meses 0.6887811\n9 mes 0.6785786\n10 18_meses 0.6483637\n11 48_horas 0.6392776\n12 diez_años 0.6365554\n13 años 0.6339559\n14 semanas 0.6284049\n15 quince_años 0.6281021\n16 dos_semanas 0.6147185\n17 trimestres 0.6012591\n18 días_hábiles 0.5972889\n19 veinticinco_años 0.5955164\n20 nueves_meses 0.5947687\n\n\nVeremos ahora cómo funciona para el género de sustantivos:\n\nfem_1 <- modelo[[\"presidenta\"]] - modelo[[\"presidente\"]]\nclosest_to(modelo, ~ \"rey\" + \"presidenta\" - \"presidente\", n = 5) |> filter(word != \"rey\")\n\n word similarity to \"rey\" + \"presidenta\" - \"presidente\"\n1 reina 0.7402226\n2 princesa 0.6662326\n3 pía 0.6249812\n4 perla 0.6189366\n\n\n\nclosest_to(modelo, ~ \"tío\" + \"presidenta\" - \"presidente\", n = 5) |> filter(word != \"tío\")\n\n word similarity to \"tío\" + \"presidenta\" - \"presidente\"\n1 dueña 0.7036596\n2 hermana 0.6947787\n3 abuela 0.6871846\n4 tía 0.6850960\n\n\n\n\nEvaluación de calidad de modelos\nLa evaluación de estas aplicaciones puede hacerse por ejemplo, con tareas de analogía, con listas de singular/plurales, de adjetivos/adverbios, masculino/femenino, etc (ver (Mikolov et al. 2013)), ver por ejemplo aquí. Adicionalmente, si se utilizan en alguna tarea downstream, pueden evaluarse en el desempeño de esa tarea particular.\nEjercicio: ¿cómo usarías esta geometría para encontrar el país en el que está una capital dada?\nObservación: falta afinar los parámetros en este modelo. Puedes probar cambiando negative sampling (por ejemplo, incrementa a 40), el número de vectores (50-200, por ejemplo), e incrementando window y el número de iteraciones.\nConsidera también un modelo preentrenado mucho más grande como este. Puedes bajar los vectores de palabras y repetir las tareas mostradas (el formato bin es estándar para la implementación que usamos de word2vec).\n\n\n\n\nBengio, Yoshua, Réjean Ducharme, Pascal Vincent, y Christian Janvin. 2003. «A Neural Probabilistic Language Model». J. Mach. Learn. Res. 3 (marzo): 1137-55. http://dl.acm.org/citation.cfm?id=944919.944966.\n\n\nHu, Y., Y. Koren, y C. Volinsky. 2008. «Collaborative Filtering for Implicit Feedback Datasets». En 2008 Eighth IEEE International Conference on Data Mining, 263-72. https://doi.org/10.1109/ICDM.2008.22.\n\n\nMikolov, Tomas, Kai Chen, Greg Corrado, y Jeffrey Dean. 2013. «Efficient Estimation of Word Representations in Vector Space». CoRR abs/1301.3781. http://arxiv.org/abs/1301.3781.\n\n\nZhou, Yunhong, Dennis Wilkinson, Robert Schreiber, y Rong Pan. 2008. «Large-Scale Parallel Collaborative Filtering for the Netflix Prize». En Algorithmic Aspects in Information and Management, editado por Rudolf Fleischer y Jinhui Xu, 337-48. Berlin, Heidelberg: Springer Berlin Heidelberg." + "section": "18.4 Espacio de representación de palabras", + "text": "18.4 Espacio de representación de palabras\nComo discutimos arriba, palabras que se usan en contextos similares por su significado o por su función (por ejemplo, “perro” y “gato”“) deben tener representaciones similares, pues su contexto tiende a ser similar. La similitud que usamos el similitud coseno.\nPodemos verificar con nuestro ejemplo:\n\nejemplos <- modelo |> closest_to(\"gol\", n = 5)\nejemplos\n\n word similarity to \"gol\"\n1 gol 1.0000000\n2 golazo 0.8252912\n3 segundo_gol 0.7978025\n4 penalti 0.7764458\n5 potente_disparo 0.7755037\n\n\nTambién podríamos calcular manualmente:\nQue también podemos calcular como:\n\nvector_penalti <- modelo[[\"penalti\"]] |> as.numeric()\ncosineSimilarity(modelo[[\"gol\"]], modelo[[\"penalti\"]])\n\n [,1]\n[1,] 0.7764458\n\n\nO directamente:\n\nnorma <- function(x) sqrt(sum(x^2))\nsum(vector_gol * vector_penalti) / (norma(vector_gol) * norma(vector_penalti))\n\n[1] 0.7764458\n\n\n\nGeometría en el espacio de representaciones\nAhora consideremos cómo se distribuyen las palabras en este espacio, y si existe estructura geométrica en este espacio que tenga información acerca del lenguaje.\nConsideremos primero el caso de plurales de sustantivos.\n\nComo el contexto de los plurales es distinto de los singulares, nuestro modelo debería poder capturar en los vectores su diferencia.\nExaminamos entonces cómo son geométricamente diferentes las representaciones de plurales vs singulares\nSi encontramos un patrón reconocible, podemos utilizar este patrón, por ejemplo, para encontrar la versión plural de una palabra singular, sin usar ninguna regla del lenguaje.\n\nUna de las relaciones geométricas más simples es la adición de vectores. Por ejemplo, extraemos la diferencia entre gol y goles:\n\nejemplos <- modelo |> closest_to(\"dos\", n = 15)\nejemplos\n\n word similarity to \"dos\"\n1 dos 1.0000000\n2 tres 0.9666800\n3 cuatro 0.9527403\n4 cinco 0.9205234\n5 siete 0.9024807\n6 seis 0.8977667\n7 ocho 0.8879153\n8 nueve 0.8550580\n9 trece 0.8514542\n10 catorce 0.8321762\n11 diez 0.8133345\n12 quince 0.8102052\n13 doce 0.8085939\n14 once 0.8033385\n15 veinte 0.7814970\n\n\n\nejemplos <- modelo |> closest_to(c(\"lluvioso\"), n = 5)\nejemplos\n\n word similarity to c(\"lluvioso\")\n1 lluvioso 1.0000000\n2 caluroso 0.8041209\n3 cálido 0.6896448\n4 húmedo 0.6866749\n5 gélido 0.6660152\n\n\n\nejemplos <- modelo |> closest_to(\"presidente\", n = 5)\nejemplos\n\n word similarity to \"presidente\"\n1 presidente 1.0000000\n2 vicepresidente 0.8412900\n3 ex_presidente 0.8321029\n4 máximo_representante 0.7781001\n5 máximo_dirigente 0.7629962\n\n\n\nejemplos <- modelo |> closest_to(\"parís\", n = 5)\nejemplos\n\n word similarity to \"parís\"\n1 parís 1.0000000\n2 londres 0.9232452\n3 nueva_york 0.8464673\n4 roma 0.8443222\n5 berlín 0.8081766\n\n\nY vemos, por ejemplo, que el modelo puede capturar conceptos relacionados con el estado del clima, capitales de países y números - aún cuando no hemos anotado estas funciones en el corpus original. Estos vectores son similares porque tienden a ocurrir en contextos similares.\n\n\nGeometría en el espacio de representaciones\nAhora consideremos cómo se distribuyen las palabras en este espacio, y si existe estructura geométrica en este espacio que tenga información acerca del lenguaje.\nConsideremos primero el caso de plurales de sustantivos.\n\nComo el contexto de los plurales es distinto de los singulares, nuestro modelo debería poder capturar en los vectores su diferencia.\nExaminamos entonces cómo son geométricamente diferentes las representaciones de plurales vs singulares\nSi encontramos un patrón reconocible, podemos utilizar este patrón, por ejemplo, para encontrar la versión plural de una palabra singular, sin usar ninguna regla del lenguaje.\n\nUna de las relaciones geométricas más simples es la adición de vectores. Por ejemplo, extraemos la diferencia entre gol y goles:\n\nplural_1 <- modelo[[\"goles\"]] - modelo[[\"gol\"]]\nplural_1\n\nA VectorSpaceModel object of 1 words and 100 vectors\n [,1] [,2] [,3] [,4] [,5] [,6]\n[1,] -0.2301596 -0.2543171 -0.04071745 -0.2292878 0.004059255 -0.2283908\nattr(,\".cache\")\n<environment: 0x559f14cdb450>\n\n\nque es un vector en el espacio de representación de palabras. Ahora sumamos este vector a un sustantivo en singular, y vemos qué palabras están cercas de esta “palabra sintética”:\n\nclosest_to(modelo, ~ \"partido\" + \"goles\" - \"gol\", n = 5)\n\n word similarity to \"partido\" + \"goles\" - \"gol\"\n1 partidos 0.7961097\n2 goles 0.7589920\n3 partidos_disputados 0.6937101\n4 últimos_encuentros 0.6788752\n5 encuentros 0.6697611\n\n\nNótese que la más cercana es justamente el plural correcto, o otros plurales con relación al que buscábamos (como encuentros)\nOtro ejemplo:\n\nclosest_to(modelo, ~ \"mes\" + \"días\" - \"día\", n = 20) \n\n word similarity to \"mes\" + \"días\" - \"día\"\n1 tres_meses 0.7858109\n2 días 0.7708776\n3 meses 0.7655628\n4 diez_días 0.7199002\n5 seis_meses 0.7110105\n6 quince_días 0.7092209\n7 nueve_meses 0.6903626\n8 doce_meses 0.6887811\n9 mes 0.6785786\n10 18_meses 0.6483637\n11 48_horas 0.6392776\n12 diez_años 0.6365554\n13 años 0.6339559\n14 semanas 0.6284049\n15 quince_años 0.6281021\n16 dos_semanas 0.6147185\n17 trimestres 0.6012591\n18 días_hábiles 0.5972889\n19 veinticinco_años 0.5955164\n20 nueves_meses 0.5947687\n\n\nVeremos ahora cómo funciona para el género de sustantivos:\n\nfem_1 <- modelo[[\"presidenta\"]] - modelo[[\"presidente\"]]\nclosest_to(modelo, ~ \"rey\" + \"presidenta\" - \"presidente\", n = 5) |> filter(word != \"rey\")\n\n word similarity to \"rey\" + \"presidenta\" - \"presidente\"\n1 reina 0.7402226\n2 princesa 0.6662326\n3 pía 0.6249812\n4 perla 0.6189366\n\n\n\nclosest_to(modelo, ~ \"tío\" + \"presidenta\" - \"presidente\", n = 5) |> filter(word != \"tío\")\n\n word similarity to \"tío\" + \"presidenta\" - \"presidente\"\n1 dueña 0.7036596\n2 hermana 0.6947787\n3 abuela 0.6871846\n4 tía 0.6850960\n\n\n\n\nEvaluación de calidad de modelos\nLa evaluación de estas aplicaciones puede hacerse por ejemplo, con tareas de analogía, con listas de singular/plurales, de adjetivos/adverbios, masculino/femenino, etc (ver (Mikolov et al. 2013)), ver por ejemplo aquí. Adicionalmente, si se utilizan en alguna tarea downstream, pueden evaluarse en el desempeño de esa tarea particular.\nEjercicio: ¿cómo usarías esta geometría para encontrar el país en el que está una capital dada?\nObservación: falta afinar los parámetros en este modelo. Puedes probar cambiando negative sampling (por ejemplo, incrementa a 40), el número de vectores (50-200, por ejemplo), e incrementando window y el número de iteraciones.\nConsidera también un modelo preentrenado mucho más grande como este. Puedes bajar los vectores de palabras y repetir las tareas mostradas (el formato bin es estándar para la implementación que usamos de word2vec).\n\n\n\n\nBengio, Yoshua, Réjean Ducharme, Pascal Vincent, y Christian Janvin. 2003. «A Neural Probabilistic Language Model». J. Mach. Learn. Res. 3 (marzo): 1137-55. http://dl.acm.org/citation.cfm?id=944919.944966.\n\n\nHu, Y., Y. Koren, y C. Volinsky. 2008. «Collaborative Filtering for Implicit Feedback Datasets». En 2008 Eighth IEEE International Conference on Data Mining, 263-72. https://doi.org/10.1109/ICDM.2008.22.\n\n\nMikolov, Tomas, Kai Chen, Greg Corrado, y Jeffrey Dean. 2013. «Efficient Estimation of Word Representations in Vector Space». CoRR abs/1301.3781. http://arxiv.org/abs/1301.3781.\n\n\nZhou, Yunhong, Dennis Wilkinson, Robert Schreiber, y Rong Pan. 2008. «Large-Scale Parallel Collaborative Filtering for the Netflix Prize». En Algorithmic Aspects in Information and Management, editado por Rudolf Fleischer y Jinhui Xu, 337-48. Berlin, Heidelberg: Springer Berlin Heidelberg." }, { "objectID": "81-apendice-descenso.html#cálculo-del-gradiente", @@ -809,14 +816,14 @@ "href": "81-apendice-descenso.html#implementación", "title": "Apéndice A — Apéndice 1: descenso en gradiente", "section": "A.2 Implementación", - "text": "A.2 Implementación\nEn este punto, podemos intentar una implementación simple basada en el código anterior para hacer descenso en gradiente para nuestro problema de regresión (es un buen ejercicio). En lugar de eso, mostraremos cómo usar librerías ahora estándar para hacer esto. En particular usamos keras (con tensorflow), que tienen la ventaja:\n\nEn tensorflow y keras no es necesario calcular las derivadas a mano. Utiliza diferenciación automática, que no es diferenciación numérica ni simbólica: se basa en la regla de la cadena y la codificación explícita de las derivadas de funciones elementales.\n\n\nlibrary(tidymodels)\nlibrary(keras)\nsource(\"../R/casas_traducir_geo.R\")\nset.seed(68821)\n# dividir muestra\ncasas_split <- initial_split(casas |>\n select(precio_m2_miles, area_hab_m2, calidad_gral, num_coches), \n prop = 0.75)\n# obtener muestra de entrenamiento\ncasas_entrena <- training(casas_split)\ncasas_receta <- recipe(precio_m2_miles ~ ., casas_entrena) \n\n\n# definición de estructura del modelo (regresión lineal)\nx_ent <- casas_receta |> prep() |> juice() |> select(-precio_m2_miles) |> as.matrix()\ny_ent <- casas_receta |> prep() |> juice() |> pull(precio_m2_miles)\nn_entrena <- nrow(x_ent)\ncrear_modelo <- function(lr = 0.01){\n modelo_casas <- \n keras_model_sequential() |>\n layer_dense(units = 1, #una sola respuesta,\n activation = \"linear\", # combinar variables linealmente\n kernel_initializer = initializer_constant(0), #inicializamos coeficientes en 0\n bias_initializer = initializer_constant(0)) #inicializamos ordenada en 0\n # compilar seleccionando cantidad a minimizar, optimizador y métricas\n modelo_casas |> compile(\n loss = \"mean_squared_error\", # pérdida cuadrática\n optimizer = optimizer_sgd(learning_rate = lr), # descenso en gradiente\n metrics = list(\"mean_squared_error\"))\n modelo_casas\n}\n# tasa de aprendizaje es lr, tenemos que poner una tasa chica (prueba)\nmodelo_casas <- crear_modelo(lr = 0.00001)\n# Ahora iteramos\n# Primero probamos con un número bajo de iteraciones\nhistoria <- modelo_casas |> fit(\n x_ent, # x entradas\n y_ent, # y salida o target\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = 20, # número de iteraciones\n verbose = 0\n)\n\n\nplot(historia, metrics = \"mean_squared_error\", smooth = FALSE) +\n geom_line()\n\n\n\nhistoria$metrics$mean_squared_error |> round(4)\n\n [1] 1.7903 0.7636 0.4549 0.3621 0.3342 0.3258 0.3232 0.3224 0.3222 0.3221\n[11] 0.3220 0.3220 0.3220 0.3219 0.3219 0.3219 0.3218 0.3218 0.3218 0.3217\n\n\nProbamos con más corridas para checar convergencia:\n\n# Agregamos iteraciones: esta historia comienza en los últimos valores de\n# la corrida anterior\nhistoria <- modelo_casas |> fit(\n as.matrix(x_ent), # x entradas\n y_ent, # y salida o target\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = 1000, # número de iteraciones\n verbose = 0\n)\n\n\nplot(historia, metrics = \"mean_squared_error\", smooth = FALSE) \n\n\n\n\nEl modelo parece todavía ir mejorando. Veamos de todas formas los coeficientes estimados hasta ahora:\n\nkeras::get_weights(modelo_casas)\n\n[[1]]\n [,1]\n[1,] 0.007331367\n[2,] 0.016437240\n[3,] 0.004633876\n\n[[2]]\n[1] 0.003028649\n\n\nLa implementación oficial de R es lm, que en general tiene buen desempeño para datos que caben en memoria:\n\nlm(precio_m2_miles ~ area_hab_m2 + calidad_gral + num_coches, \n data = casas_entrena) |> \n coef()\n\n (Intercept) area_hab_m2 calidad_gral num_coches \n 0.66869194 -0.00449751 0.16807663 0.13115749 \n\n\nDe modo que todavía requerimos más iteraciones para alcanzar convergencia. ¿Por qué la convergencia es tan lenta? En parte, la razón es que las escalas de las variables de entrada son muy diferentes, de modo que es difícil ajustar una tasa de aprendizaje constante que funcione bien. Podemos remediar esto poniendo todas las entradas en la misma escala (normalizando)" + "text": "A.2 Implementación\nEn este punto, podemos intentar una implementación simple basada en el código anterior para hacer descenso en gradiente para nuestro problema de regresión (es un buen ejercicio). En lugar de eso, mostraremos cómo usar librerías ahora estándar para hacer esto. En particular usamos keras (con tensorflow), que tienen la ventaja:\n\nEn tensorflow y keras no es necesario calcular las derivadas a mano. Utiliza diferenciación automática, que no es diferenciación numérica ni simbólica: se basa en la regla de la cadena y la codificación explícita de las derivadas de funciones elementales.\n\n\nlibrary(tidymodels)\nlibrary(keras)\nsource(\"../R/casas_traducir_geo.R\")\nset.seed(68821)\n# dividir muestra\ncasas_split <- initial_split(casas |>\n select(precio_m2_miles, area_hab_m2, calidad_gral, num_coches), \n prop = 0.75)\n# obtener muestra de entrenamiento\ncasas_entrena <- training(casas_split)\ncasas_receta <- recipe(precio_m2_miles ~ ., casas_entrena) \n\n\n# definición de estructura del modelo (regresión lineal)\nx_ent <- casas_receta |> prep() |> juice() |> select(-precio_m2_miles) |> as.matrix()\ny_ent <- casas_receta |> prep() |> juice() |> pull(precio_m2_miles)\nn_entrena <- nrow(x_ent)\ncrear_modelo <- function(lr = 0.01){\n modelo_casas <- \n keras_model_sequential() |>\n layer_dense(units = 1, #una sola respuesta,\n activation = \"linear\", # combinar variables linealmente\n kernel_initializer = initializer_constant(0), #inicializamos coeficientes en 0\n bias_initializer = initializer_constant(0)) #inicializamos ordenada en 0\n # compilar seleccionando cantidad a minimizar, optimizador y métricas\n modelo_casas |> compile(\n loss = \"mean_squared_error\", # pérdida cuadrática\n optimizer = optimizer_sgd(learning_rate = lr), # descenso en gradiente\n metrics = list(\"mean_squared_error\"))\n modelo_casas\n}\n# tasa de aprendizaje es lr, tenemos que poner una tasa chica (prueba)\nmodelo_casas <- crear_modelo(lr = 0.00001)\n# Ahora iteramos\n# Primero probamos con un número bajo de iteraciones\nhistoria <- modelo_casas |> fit(\n x_ent, # x entradas\n y_ent, # y salida o target\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = 20, # número de iteraciones\n verbose = 0\n)\n\n\nplot(historia, metrics = \"mean_squared_error\", smooth = FALSE) +\n geom_line()\n\n\n\nhistoria$metrics$mean_squared_error |> round(4)\n\n [1] 1.7903 0.7636 0.4549 0.3621 0.3342 0.3258 0.3232 0.3224 0.3222 0.3221\n[11] 0.3220 0.3220 0.3220 0.3219 0.3219 0.3219 0.3218 0.3218 0.3218 0.3217\n\n\nProbamos con más corridas para checar convergencia:\n\n# Agregamos iteraciones: esta historia comienza en los últimos valores de\n# la corrida anterior\nhistoria <- modelo_casas |> fit(\n as.matrix(x_ent), # x entradas\n y_ent, # y salida o target\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = 1000, # número de iteraciones\n verbose = 0\n)\n\n\nplot(historia, metrics = \"mean_squared_error\", smooth = FALSE) \n\n\n\n\nEl modelo parece todavía ir mejorando. Veamos de todas formas los coeficientes estimados hasta ahora:\n\nkeras::get_weights(modelo_casas)\n\n[[1]]\n [,1]\n[1,] 0.007331368\n[2,] 0.016437240\n[3,] 0.004633875\n\n[[2]]\n[1] 0.003028649\n\n\nLa implementación oficial de R es lm, que en general tiene buen desempeño para datos que caben en memoria:\n\nlm(precio_m2_miles ~ area_hab_m2 + calidad_gral + num_coches, \n data = casas_entrena) |> \n coef()\n\n (Intercept) area_hab_m2 calidad_gral num_coches \n 0.66869194 -0.00449751 0.16807663 0.13115749 \n\n\nDe modo que todavía requerimos más iteraciones para alcanzar convergencia. ¿Por qué la convergencia es tan lenta? En parte, la razón es que las escalas de las variables de entrada son muy diferentes, de modo que es difícil ajustar una tasa de aprendizaje constante que funcione bien. Podemos remediar esto poniendo todas las entradas en la misma escala (normalizando)" }, { "objectID": "81-apendice-descenso.html#normalización-de-entradas", "href": "81-apendice-descenso.html#normalización-de-entradas", "title": "Apéndice A — Apéndice 1: descenso en gradiente", "section": "A.3 Normalización de entradas", - "text": "A.3 Normalización de entradas\nLa convergencia de descenso en gradiente (y también el desempeño numérico para otros algoritmos) puede dificultarse cuando las variables tienen escalas muy diferentes. Esto produce curvaturas altas en la función que queremos minimizar.\nEn este ejemplo simple, una variable tiene desviación estándar 10 y otra 1:\n\nx1 <- rnorm(100, 0, 5) \nx2 <- rnorm(100, 0, 1) + 0.1*x1\ny <- 0*x1 + 0*x2 + rnorm(100, 0, 0.1) \ndat <- tibble(x1, x2, y)\nrss <- function(beta) mean((as.matrix(dat[, 1:2]) %*% beta - y)^2) \ngrid_beta <- expand.grid(beta1 = seq(-1, 1, length.out = 50), \n beta2 = seq(-1, 1, length.out = 50))\nrss_1 <- apply(grid_beta, 1, rss) \ndat_x <- data.frame(grid_beta, rss_1)\nggplot(dat_x, aes(x = beta1, y = beta2, z = rss_1)) + \n geom_contour(binwidth = 0.5) +\n coord_equal() \n\n\n\n\nEn algunas direcciones el gradiente es muy grande, y en otras chico. Esto implica que la convergencia puede ser muy lenta en algunas direcciones, puede diverger en otras, y que hay que ajustar el paso \\(\\eta > 0\\) con cuidado, dependiendo de dónde comiencen las iteraciones.\nPor ejemplo, con un tamaño de paso relativamente chico, damos unos saltos grandes al principio y luego avanzamos muy lentamente:\n\ngrad_calc <- function(x_ent, y_ent){\n # calculamos directamente el gradiente\n salida_grad <- function(beta){\n n <- length(y_ent)\n f_beta <- as.matrix(cbind(1, x_ent)) %*% beta\n e <- y_ent - f_beta\n grad_out <- - as.numeric(t(cbind(1, x_ent)) %*% e) / n\n names(grad_out) <- c('Intercept', colnames(x_ent))\n grad_out\n }\n salida_grad\n}\ngrad_sin_norm <- grad_calc(dat[, 1:2, drop = FALSE], dat$y)\niteraciones <- descenso(10, c(0, -0.25, -0.75), 0.02, grad_sin_norm)\nggplot(dat_x) + \n geom_contour(aes(x = beta1, y = beta2, z = rss_1), binwidth = 0.5) +\n coord_equal() +\n geom_path(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red') +\n geom_point(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red')\n\n\n\n\nSi incrementamos el tamaño de paso observamos también convergencia lenta. En este caso particular, subir más el tamaño de paso puede producir divergencia:\n\niteraciones <- descenso(10, c(0, -0.25, -0.75), 0.07, grad_sin_norm)\nggplot(dat_x) + \n geom_contour(aes(x = beta1, y = beta2, z = rss_1), binwidth = 0.5) +\n coord_equal() +\n geom_path(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red') +\n geom_point(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red')\n\n\n\n\nUna normalización usual es con la media y desviación estándar, donde hacemos, para cada variable de entrada \\(j=1,2,\\ldots, p\\) \\[ x_j^{(i)} = \\frac{ x_j^{(i)} - \\bar{x}_j}{s_j}\\] donde \\[\\bar{x}_j = \\frac{1}{N} \\sum_{i=1}^N x_j^{(i)}\\] \\[s_j = \\sqrt{\\frac{1}{N-1}\\sum_{i=1}^N (x_j^{(i)}- \\bar{x}_j )^2}\\] es decir, centramos y normalizamos por columna. Otra opción común es restar el mínimo y dividir entre la diferencia del máximo y el mínimo, de modo que las variables resultantes toman valores en \\([0,1]\\).\nEntonces escalamos antes de ajustar:\n\nx1_s = (x1 - mean(x1))/sd(x1)\nx2_s = (x2 - mean(x2))/sd(x2)\ndat <- tibble(x1_s = x1_s, x2_s = x2_s, y = y)\nrss <- function(beta) mean((as.matrix(dat[, 1:2]) %*% beta - y)^2) \ngrid_beta <- expand.grid(beta1 = seq(-1, 1, length.out = 50), \n beta2 = seq(-1, 1, length.out = 50))\nrss_1 <- apply(grid_beta, 1, rss) \ndat_x <- data.frame(grid_beta, rss_1)\nggplot(dat_x, aes(x = beta1, y = beta2, z = rss_1)) + \n geom_contour(binwidth = 0.5) +\n coord_equal() \n\n\n\n\nNótese que los coeficientes ajustados serán diferentes a los del caso no normalizado.\nSi normalizamos, obtenemos convergencia más rápida\n\ngrad_sin_norm <- grad_calc(dat[, 1:2, drop = FALSE], dat$y)\niteraciones <- descenso(10, c(0, -0.25, -0.75), 0.5, grad_sin_norm)\nggplot(dat_x) + \n geom_contour(aes(x = beta1, y = beta2, z = rss_1), binwidth = 0.5) +\n coord_equal() +\n geom_path(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red') +\n geom_point(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red')\n\n\n\n\n\n\n\n\n\n\nTip\n\n\n\nCuando normalizamos antes de ajustar el modelo, las predicciones deben hacerse con entradas normalizadas. La normalización se hace con los mismos valores que se usaron en el entrenamiento (y no recalculando medias y desviaciones estándar con el conjunto de prueba). En cuanto a la forma funcional del predictor \\(f\\), el problema con entradas normalizadas es equivalente al de las entradas no normalizadas. Asegúrate de esto escribiendo cómo correponden los coeficientes de cada modelo normalizado con los coeficientes del modelo no normalizado.\n\n\nSupongamos que el modelo en las variables originales es \\[{f}_\\beta (X) = \\beta_0 + \\beta_1 X_1 + \\beta_2 X_2 + \\cdots + \\beta_p X_p,\\] Consideramos el modelo con variables estandarizadas \\[{g}_{\\beta^s} (X) = \\beta_0^s + \\beta_1^s Z_1 + \\beta_2^s Z_2 + \\cdots + \\beta_p^s Z_p,\\]\nSustituyendo \\(Z_j = (X_j - \\mu_j)/s_j,\\)\n\\[{g}_{\\beta^s} (X) = (\\beta_0^s - \\sum_{j=1}^p \\beta_j^s \\mu_j/s_j) + \\frac{\\beta_1^s}{s_j} X_1 + \\frac{\\beta_2^s}{s_2} X_2 + \\cdots + \\frac{\\beta_p^s}{s_p} X_p,\\] Y vemos que tiene la misma forma funcional de \\(f_\\beta(X)\\). Si la solución de mínimos cuadrados es única, entonces una vez que ajustemos tenemos que tener \\(\\hat{f}_\\beta(X) = \\hat{g}_{\\beta^s} (X)\\), lo que implica que \\[\\hat{\\beta}_0 = \\hat{\\beta}_0^s - \\sum_{j=1}^p \\hat{\\beta}_j^s\\mu_j/s_j\\] y \\[\\hat{\\beta}_j = \\hat{\\beta}_j^s/s_j.\\]\nNótese que para pasar del problema estandarizado al no estandarizado simplemente se requiere escalar los coeficientes por la \\(s_j\\) correspondiente.\n\nEjemplo\nRepetimos nuestro modelo, pero normalizando las entradas:\n\n# usamos recipes para este ejemplo, no necesitas usarlo\ncasas_receta <- recipe(precio_m2_miles ~ ., casas_entrena) |>\n step_normalize(all_predictors()) \ncasas_receta |> summary()\n\n# A tibble: 4 × 4\n variable type role source \n <chr> <list> <chr> <chr> \n1 area_hab_m2 <chr [2]> predictor original\n2 calidad_gral <chr [2]> predictor original\n3 num_coches <chr [2]> predictor original\n4 precio_m2_miles <chr [2]> outcome original\n\n\n\nmodelo_lineal <- linear_reg() |>\n set_engine(\"lm\")\ncasas_flujo <- workflow() |>\n add_recipe(casas_receta) |> \n add_model(modelo_lineal)\n\n\nlibrary(keras)\n# definición de estructura del modelo (regresión lineal)\nx_ent_s <- prep(casas_receta) |> juice() |> select(-precio_m2_miles) |> \n as.matrix()\najustar_casas <- function(modelo, x, y, n_epochs = 100){\n ajuste <- modelo |> fit(\n as.matrix(x), y,\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = n_epochs, # número de iteraciones\n verbose = 0) |> as_tibble()\n ajuste\n}\nmodelo_casas_ns <- crear_modelo(0.00001)\nmodelo_casas_s <- crear_modelo(0.2)\nhistoria_s <- ajustar_casas(modelo_casas_s, x_ent_s, y_ent) |>\n mutate(tipo = \"Estandarizar\")\nhistoria_ns <- ajustar_casas(modelo_casas_ns, x_ent, y_ent) |> \n mutate(tipo = \"Sin estandarizar\")\nhistoria <- bind_rows(historia_ns, historia_s) |> filter(metric == \"mean_squared_error\")\nggplot(historia, aes(x = epoch, y = value, colour = tipo)) +\n geom_line() + geom_point() +scale_x_log10() + scale_y_log10()\n\n\n\n\nObservamos que el modelo con datos estandarizados convergió:\n\nkeras::get_weights(modelo_casas_s)\n\n[[1]]\n [,1]\n[1,] -0.22045916\n[2,] 0.23149671\n[3,] 0.09673478\n\n[[2]]\n[1] 1.295027\n\ncoef(lm.fit(cbind(1,x_ent_s), y_ent))\n\n area_hab_m2 calidad_gral num_coches \n 1.29502679 -0.22045919 0.23149675 0.09673476 \n\n\nMientras que el modelo no estandarizado todavía requiere iteraciones:\n\nkeras::get_weights(modelo_casas_ns)\n\n[[1]]\n [,1]\n[1,] 0.0079817399\n[2,] 0.0019486141\n[3,] 0.0005532746\n\n[[2]]\n[1] 0.0003491881\n\ncoef(lm.fit(cbind(1, x_ent), y_ent))\n\n area_hab_m2 calidad_gral num_coches \n 0.66869194 -0.00449751 0.16807663 0.13115749" + "text": "A.3 Normalización de entradas\nLa convergencia de descenso en gradiente (y también el desempeño numérico para otros algoritmos) puede dificultarse cuando las variables tienen escalas muy diferentes. Esto produce curvaturas altas en la función que queremos minimizar.\nEn este ejemplo simple, una variable tiene desviación estándar 10 y otra 1:\n\nx1 <- rnorm(100, 0, 5) \nx2 <- rnorm(100, 0, 1) + 0.1*x1\ny <- 0*x1 + 0*x2 + rnorm(100, 0, 0.1) \ndat <- tibble(x1, x2, y)\nrss <- function(beta) mean((as.matrix(dat[, 1:2]) %*% beta - y)^2) \ngrid_beta <- expand.grid(beta1 = seq(-1, 1, length.out = 50), \n beta2 = seq(-1, 1, length.out = 50))\nrss_1 <- apply(grid_beta, 1, rss) \ndat_x <- data.frame(grid_beta, rss_1)\nggplot(dat_x, aes(x = beta1, y = beta2, z = rss_1)) + \n geom_contour(binwidth = 0.5) +\n coord_equal() \n\n\n\n\nEn algunas direcciones el gradiente es muy grande, y en otras chico. Esto implica que la convergencia puede ser muy lenta en algunas direcciones, puede diverger en otras, y que hay que ajustar el paso \\(\\eta > 0\\) con cuidado, dependiendo de dónde comiencen las iteraciones.\nPor ejemplo, con un tamaño de paso relativamente chico, damos unos saltos grandes al principio y luego avanzamos muy lentamente:\n\ngrad_calc <- function(x_ent, y_ent){\n # calculamos directamente el gradiente\n salida_grad <- function(beta){\n n <- length(y_ent)\n f_beta <- as.matrix(cbind(1, x_ent)) %*% beta\n e <- y_ent - f_beta\n grad_out <- - as.numeric(t(cbind(1, x_ent)) %*% e) / n\n names(grad_out) <- c('Intercept', colnames(x_ent))\n grad_out\n }\n salida_grad\n}\ngrad_sin_norm <- grad_calc(dat[, 1:2, drop = FALSE], dat$y)\niteraciones <- descenso(10, c(0, -0.25, -0.75), 0.02, grad_sin_norm)\nggplot(dat_x) + \n geom_contour(aes(x = beta1, y = beta2, z = rss_1), binwidth = 0.5) +\n coord_equal() +\n geom_path(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red') +\n geom_point(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red')\n\n\n\n\nSi incrementamos el tamaño de paso observamos también convergencia lenta. En este caso particular, subir más el tamaño de paso puede producir divergencia:\n\niteraciones <- descenso(10, c(0, -0.25, -0.75), 0.07, grad_sin_norm)\nggplot(dat_x) + \n geom_contour(aes(x = beta1, y = beta2, z = rss_1), binwidth = 0.5) +\n coord_equal() +\n geom_path(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red') +\n geom_point(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red')\n\n\n\n\nUna normalización usual es con la media y desviación estándar, donde hacemos, para cada variable de entrada \\(j=1,2,\\ldots, p\\) \\[ x_j^{(i)} = \\frac{ x_j^{(i)} - \\bar{x}_j}{s_j}\\] donde \\[\\bar{x}_j = \\frac{1}{N} \\sum_{i=1}^N x_j^{(i)}\\] \\[s_j = \\sqrt{\\frac{1}{N-1}\\sum_{i=1}^N (x_j^{(i)}- \\bar{x}_j )^2}\\] es decir, centramos y normalizamos por columna. Otra opción común es restar el mínimo y dividir entre la diferencia del máximo y el mínimo, de modo que las variables resultantes toman valores en \\([0,1]\\).\nEntonces escalamos antes de ajustar:\n\nx1_s = (x1 - mean(x1))/sd(x1)\nx2_s = (x2 - mean(x2))/sd(x2)\ndat <- tibble(x1_s = x1_s, x2_s = x2_s, y = y)\nrss <- function(beta) mean((as.matrix(dat[, 1:2]) %*% beta - y)^2) \ngrid_beta <- expand.grid(beta1 = seq(-1, 1, length.out = 50), \n beta2 = seq(-1, 1, length.out = 50))\nrss_1 <- apply(grid_beta, 1, rss) \ndat_x <- data.frame(grid_beta, rss_1)\nggplot(dat_x, aes(x = beta1, y = beta2, z = rss_1)) + \n geom_contour(binwidth = 0.5) +\n coord_equal() \n\n\n\n\nNótese que los coeficientes ajustados serán diferentes a los del caso no normalizado.\nSi normalizamos, obtenemos convergencia más rápida\n\ngrad_sin_norm <- grad_calc(dat[, 1:2, drop = FALSE], dat$y)\niteraciones <- descenso(10, c(0, -0.25, -0.75), 0.5, grad_sin_norm)\nggplot(dat_x) + \n geom_contour(aes(x = beta1, y = beta2, z = rss_1), binwidth = 0.5) +\n coord_equal() +\n geom_path(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red') +\n geom_point(data = data.frame(iteraciones[, 2:3]), aes(x=X1, y=X2), colour = 'red')\n\n\n\n\n\n\n\n\n\n\nTip\n\n\n\nCuando normalizamos antes de ajustar el modelo, las predicciones deben hacerse con entradas normalizadas. La normalización se hace con los mismos valores que se usaron en el entrenamiento (y no recalculando medias y desviaciones estándar con el conjunto de prueba). En cuanto a la forma funcional del predictor \\(f\\), el problema con entradas normalizadas es equivalente al de las entradas no normalizadas. Asegúrate de esto escribiendo cómo correponden los coeficientes de cada modelo normalizado con los coeficientes del modelo no normalizado.\n\n\nSupongamos que el modelo en las variables originales es \\[{f}_\\beta (X) = \\beta_0 + \\beta_1 X_1 + \\beta_2 X_2 + \\cdots + \\beta_p X_p,\\] Consideramos el modelo con variables estandarizadas \\[{g}_{\\beta^s} (X) = \\beta_0^s + \\beta_1^s Z_1 + \\beta_2^s Z_2 + \\cdots + \\beta_p^s Z_p,\\]\nSustituyendo \\(Z_j = (X_j - \\mu_j)/s_j,\\)\n\\[{g}_{\\beta^s} (X) = (\\beta_0^s - \\sum_{j=1}^p \\beta_j^s \\mu_j/s_j) + \\frac{\\beta_1^s}{s_j} X_1 + \\frac{\\beta_2^s}{s_2} X_2 + \\cdots + \\frac{\\beta_p^s}{s_p} X_p,\\] Y vemos que tiene la misma forma funcional de \\(f_\\beta(X)\\). Si la solución de mínimos cuadrados es única, entonces una vez que ajustemos tenemos que tener \\(\\hat{f}_\\beta(X) = \\hat{g}_{\\beta^s} (X)\\), lo que implica que \\[\\hat{\\beta}_0 = \\hat{\\beta}_0^s - \\sum_{j=1}^p \\hat{\\beta}_j^s\\mu_j/s_j\\] y \\[\\hat{\\beta}_j = \\hat{\\beta}_j^s/s_j.\\]\nNótese que para pasar del problema estandarizado al no estandarizado simplemente se requiere escalar los coeficientes por la \\(s_j\\) correspondiente.\n\nEjemplo\nRepetimos nuestro modelo, pero normalizando las entradas:\n\n# usamos recipes para este ejemplo, no necesitas usarlo\ncasas_receta <- recipe(precio_m2_miles ~ ., casas_entrena) |>\n step_normalize(all_predictors()) \ncasas_receta |> summary()\n\n# A tibble: 4 × 4\n variable type role source \n <chr> <list> <chr> <chr> \n1 area_hab_m2 <chr [2]> predictor original\n2 calidad_gral <chr [2]> predictor original\n3 num_coches <chr [2]> predictor original\n4 precio_m2_miles <chr [2]> outcome original\n\n\n\nmodelo_lineal <- linear_reg() |>\n set_engine(\"lm\")\ncasas_flujo <- workflow() |>\n add_recipe(casas_receta) |> \n add_model(modelo_lineal)\n\n\nlibrary(keras)\n# definición de estructura del modelo (regresión lineal)\nx_ent_s <- prep(casas_receta) |> juice() |> select(-precio_m2_miles) |> \n as.matrix()\najustar_casas <- function(modelo, x, y, n_epochs = 100){\n ajuste <- modelo |> fit(\n as.matrix(x), y,\n batch_size = nrow(x_ent), # para descenso en gradiente\n epochs = n_epochs, # número de iteraciones\n verbose = 0) |> as_tibble()\n ajuste\n}\nmodelo_casas_ns <- crear_modelo(0.00001)\nmodelo_casas_s <- crear_modelo(0.2)\nhistoria_s <- ajustar_casas(modelo_casas_s, x_ent_s, y_ent) |>\n mutate(tipo = \"Estandarizar\")\nhistoria_ns <- ajustar_casas(modelo_casas_ns, x_ent, y_ent) |> \n mutate(tipo = \"Sin estandarizar\")\nhistoria <- bind_rows(historia_ns, historia_s) |> filter(metric == \"mean_squared_error\")\nggplot(historia, aes(x = epoch, y = value, colour = tipo)) +\n geom_line() + geom_point() +scale_x_log10() + scale_y_log10()\n\n\n\n\nObservamos que el modelo con datos estandarizados convergió:\n\nkeras::get_weights(modelo_casas_s)\n\n[[1]]\n [,1]\n[1,] -0.22045916\n[2,] 0.23149671\n[3,] 0.09673478\n\n[[2]]\n[1] 1.295027\n\ncoef(lm.fit(cbind(1,x_ent_s), y_ent))\n\n area_hab_m2 calidad_gral num_coches \n 1.29502679 -0.22045919 0.23149675 0.09673476 \n\n\nMientras que el modelo no estandarizado todavía requiere iteraciones:\n\nkeras::get_weights(modelo_casas_ns)\n\n[[1]]\n [,1]\n[1,] 0.0079817399\n[2,] 0.0019486138\n[3,] 0.0005532746\n\n[[2]]\n[1] 0.0003491883\n\ncoef(lm.fit(cbind(1, x_ent), y_ent))\n\n area_hab_m2 calidad_gral num_coches \n 0.66869194 -0.00449751 0.16807663 0.13115749" }, { "objectID": "82-apendice-descenso-estocastico.html#algoritmo-de-descenso-estocástico", @@ -851,7 +858,7 @@ "href": "82-apendice-descenso-estocastico.html#ajuste-de-redes-con-descenso-estocástico", "title": "Apéndice B — Apéndice 2: Descenso estocástico", "section": "B.5 Ajuste de redes con descenso estocástico", - "text": "B.5 Ajuste de redes con descenso estocástico\n\nlibrary(keras)\n\n\nset.seed(21321)\nx_ent <- as.matrix(dat_ent[,c('x_1','x_2','x_3')])\nx_valid <- as.matrix(dat_valid[,c('x_1','x_2','x_3')])\ny_ent <- dat_ent$y\ny_valid <- dat_valid$y\n\nEmpezamos con regresión (sin capas ocultas), que se escribe y ajusta como sigue:\n\nmodelo <- keras_model_sequential() \nmodelo |>\n layer_dense(units = 1, \n activation = \"linear\",\n input_shape = c(3))\n\nmodelo |> compile(loss = 'mse',\n optimizer = optimizer_sgd(learning_rate = 0.1, momentum = 0,\n decay = 0))\n\nhistory <- modelo |> \n fit(x_ent, y_ent, \n epochs = 50, batch_size = 10, \n verbose = 0,\n validation_data = list(x_valid, y_valid))\n\nPodemos ver el progreso del algoritmo por época\n\naprendizaje <- as_tibble(history)\nggplot(aprendizaje, \n aes(x=epoch, y=value, colour=data, group=data)) +\n facet_wrap(~metric, ncol = 1) + geom_line() + geom_point(size = 0.5)\n\n\n\n\nVer los pesos:\n\nget_weights(modelo)\n\n[[1]]\n [,1]\n[1,] -2.66908264\n[2,] -0.17915317\n[3,] 0.03563109\n\n[[2]]\n[1] -0.3472215\n\n\nY verificamos que concuerda con la salida de lm:\n\nmod_lineal <- lm(y ~ x_1 + x_2+ x_3, data = dat_ent) \ncoef(mod_lineal)\n\n(Intercept) x_1 x_2 x_3 \n-0.36904266 -2.46877687 -0.07368414 0.06632769 \n\n\n\n\n\n\nGoodfellow, Ian, Yoshua Bengio, y Aaron Courville. 2016. Deep Learning. MIT Press." + "text": "B.5 Ajuste de redes con descenso estocástico\n\nlibrary(keras)\n\n\nset.seed(21321)\nx_ent <- as.matrix(dat_ent[,c('x_1','x_2','x_3')])\nx_valid <- as.matrix(dat_valid[,c('x_1','x_2','x_3')])\ny_ent <- dat_ent$y\ny_valid <- dat_valid$y\n\nEmpezamos con regresión (sin capas ocultas), que se escribe y ajusta como sigue:\n\nmodelo <- keras_model_sequential() \nmodelo |>\n layer_dense(units = 1, \n activation = \"linear\",\n input_shape = c(3))\n\nmodelo |> compile(loss = 'mse',\n optimizer = optimizer_sgd(learning_rate = 0.1, momentum = 0,\n decay = 0))\n\nhistory <- modelo |> \n fit(x_ent, y_ent, \n epochs = 50, batch_size = 10, \n verbose = 0,\n validation_data = list(x_valid, y_valid))\n\nPodemos ver el progreso del algoritmo por época\n\naprendizaje <- as_tibble(history)\nggplot(aprendizaje, \n aes(x=epoch, y=value, colour=data, group=data)) +\n facet_wrap(~metric, ncol = 1) + geom_line() + geom_point(size = 0.5)\n\n\n\n\nVer los pesos:\n\nget_weights(modelo)\n\n[[1]]\n [,1]\n[1,] -2.57393885\n[2,] -0.08030029\n[3,] 0.13267761\n\n[[2]]\n[1] -0.3571931\n\n\nY verificamos que concuerda con la salida de lm:\n\nmod_lineal <- lm(y ~ x_1 + x_2+ x_3, data = dat_ent) \ncoef(mod_lineal)\n\n(Intercept) x_1 x_2 x_3 \n-0.36904266 -2.46877687 -0.07368414 0.06632769 \n\n\n\n\n\n\nGoodfellow, Ian, Yoshua Bengio, y Aaron Courville. 2016. Deep Learning. MIT Press." }, { "objectID": "99-referencias.html",