Note
Objetivo: Introducir modelos supervisados aplicados a texto, implementar Naive Bayes en R y evaluar su desempeño con métricas clásicas de clasificación.
La clasificación supervisada es una tarea de aprendizaje automático donde el objetivo es predecir una categoría (etiqueta) para un nuevo texto, a partir de ejemplos previamente etiquetados.
Representación de texto
Antes de aplicar un modelo, debemos convertir el texto a una representación numérica:
En este ejemplo vamos a usar TF-IDF con ayuda del paquete textrecipes
.
Usaremos un dataset simulado con textos cortos y etiquetas de sentimiento (pos
o neg
).
# A tibble: 6 × 2
texto clase
<chr> <fct>
1 Muy buena atención, volvería sin dudar ¡Gracias! pos
2 Una pérdida de tiempo total Para nada recomendable. neg
3 Atención al cliente pésima Fue una sorpresa. neg
4 Excelente calidad y servicio pos
5 El producto llegó roto Realmente excelente. neg
6 El producto llegó en perfectas condiciones ¡Gracias! pos
{recipe}
permite definir una secuencia de pasos para procesar los datos
Un workflow en tidymodels es una estructura que combina dos cosas:
Una receta + un modelo: que define qué algoritmo se va a usar
Permite asegurarse de que el preprocesamiento y el modelo se apliquen juntos
modelo_final <- fit(flujo, data = datos_train)
predicciones <- predict(modelo_final, new_data = datos_test, type = "prob") %>%
bind_cols(predict(modelo_final, new_data = datos_test)) %>%
bind_cols(datos_test)
predicciones %>%
conf_mat(truth = clase, estimate = .pred_class)
Truth
Prediction neg pos
neg 25 3
pos 0 22
# A tibble: 2 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 accuracy binary 0.94
2 kap binary 0.88
logistic_reg()
naive_Bayes()
vm_linear()
rand_forest()
boost_tree() con engine = “xgboost”
datos_bigrams <- datos %>%
unnest_tokens(bigram, texto, token = "ngrams", n = 2) %>%
filter(!is.na(bigram))
datos_bigrams
# A tibble: 600 × 2
clase bigram
<fct> <chr>
1 pos muy buena
2 pos buena atención
3 pos atención volvería
4 pos volvería sin
5 pos sin dudar
6 pos dudar gracias
7 neg una pérdida
8 neg pérdida de
9 neg de tiempo
10 neg tiempo total
# ℹ 590 more rows
library(tidyr)
bigrams_separated <- datos_bigrams %>%
separate(bigram, c("word1", "word2"), sep = " ")
bigrams_filtered <- bigrams_separated %>%
filter(!word1 %in% stop_words$word) %>%
filter(!word2 %in% stop_words$word)
bigram_counts <- bigrams_filtered %>%
count(word1, word2, sort = TRUE)
bigram_counts
# A tibble: 132 × 3
word1 word2 n
<chr> <chr> <int>
1 nada recomendable 13
2 para nada 13
3 definitivamente sí 9
4 el producto 9
5 producto llegó 9
6 realmente excelente 9
7 totalmente conforme 9
8 excelente calidad 8
9 fue una 8
10 jamás volveré 8
# ℹ 122 more rows
datos %>%
unnest_tokens(trigram, texto, token = "ngrams", n = 3) %>%
filter(!is.na(trigram)) %>%
separate(trigram, c("word1", "word2", "word3"), sep = " ") %>%
filter(!word1 %in% stop_words$word,
!word2 %in% stop_words$word,
!word3 %in% stop_words$word) %>%
count(word1, word2, word3, sort = TRUE)
# A tibble: 137 × 4
word1 word2 word3 n
<chr> <chr> <chr> <int>
1 para nada recomendable 13
2 el producto llegó 9
3 fue una sorpresa 8
4 nunca llegó lo 8
5 caro para lo 7
6 demasiado caro para 7
7 encantó la película 7
8 joya repetiría sin 7
9 la disfruté mucho 7
10 la película la 7
# ℹ 127 more rows
a <- grid::arrow(type = "closed", length = unit(.15, "inches"))
bigram_counts %>%
filter(n > 5) %>%
graph_from_data_frame() |>
ggraph(layout = "fr") +
geom_edge_link(aes(edge_alpha = n), show.legend = FALSE,
arrow = a, end_cap = circle(.07, 'inches')) +
geom_node_point(color = "lightblue", size = 5) +
geom_node_text(aes(label = name), vjust = 1, hjust = 1) +
theme_void()
library(widyr)
word_pairs <- datos_corr %>%
pairwise_count(word, tipo_comentario, sort = TRUE)
word_pairs %>%
filter(item1 == "producto")
# A tibble: 79 × 3
item1 item2 n
<chr> <chr> <dbl>
1 producto muy 2
2 producto atención 2
3 producto volvería 2
4 producto gracias 2
5 producto una 2
6 producto para 2
7 producto nada 2
8 producto recomendable 2
9 producto fue 2
10 producto sorpresa 2
# ℹ 69 more rows
El coeficiente phi es una medida común de correlación binaria. El coeficiente phi se centra en la probabilidad de que aparezcan las palabras X e Y, o ninguna , que de que una aparezca sin la otra.
word_cors <- datos_corr %>%
group_by(word) %>%
filter(n() >= 5) %>%
pairwise_cor(word, tipo_comentario, sort = TRUE)
word_cors
# A tibble: 3,306 × 3
item1 item2 correlation
<chr> <chr> <dbl>
1 sin buena 1
2 dudar buena 1
3 calidad buena 1
4 servicio buena 1
5 joya buena 1
6 repetiría buena 1
7 pensarlo buena 1
8 encantó buena 1
9 película buena 1
10 disfruté buena 1
# ℹ 3,296 more rows
# A tibble: 57 × 3
item1 item2 correlation
<chr> <chr> <dbl>
1 atención muy NaN
2 atención buena NaN
3 atención volvería NaN
4 atención sin NaN
5 atención dudar NaN
6 atención gracias NaN
7 atención una NaN
8 atención para NaN
9 atención nada NaN
10 atención recomendable NaN
# ℹ 47 more rows
plot <- word_cors %>%
filter(item1 %in% c("comida", "servicio", "cliente")) %>%
group_by(item1) %>%
ungroup() %>%
drop_na(correlation) |>
mutate(item2 = reorder(item2, correlation)) %>%
ggplot(aes(item2, correlation)) +
geom_bar(stat = "identity") +
facet_wrap(~ item1, scales = "free") +
coord_flip()
set.seed(2016)
plot <- word_cors %>%
drop_na(correlation) |>
filter(correlation > .35) %>%
graph_from_data_frame() %>%
ggraph(layout = "fr") +
geom_edge_link(aes(edge_alpha = correlation), show.legend = FALSE) +
geom_node_point(color = "lightblue", size = 5) +
geom_node_text(aes(label = name), repel = TRUE) +
theme_void()