Title
Hace algunos años Peter Norvig escribió el artículo
How to Write a Spelling Corrector, creando un corrector ortográfico en 21 líneas de Python. Recientemente, encontré una versión creada por Rasmus Bååth para R,
Peter Norvig's Spell Checker in Two Lines of Base R. En esta entrada me baso en el código de Rasmus Bååth para crear una versión en español. Antes de continuar, recomiendo leer los artículos anteriores.
Código desglosado
# Código modificado
raw_test <- read.csv("https://sites.google.com/site/nubededatosblogspotcom/crea.txt",
header = FALSE) # Importa como data frame
sorted_words <- as.table(as.matrix(raw_test)) # Convierte data frame a table
# Código idéntico al de Rasmus Bååth
correct <- function(word) {
# Calcula la distancia entre la palabra y el resto de palabras ordenadas (sorted words).
edit_dist <- adist(word, sorted_words)
# Calcula la distancia mínima y devuelve una palabra existente en crea.txt
# con un límite de 2 ediciones.
min_edit_dist <- min(edit_dist, 2)
# Genera un vector con todas las palabras con el mínimo de distancia.
# Como sorted_words está ordenada de más a menos común, el vector
# resultante tendrá la primera coincidencia más común/probable.
proposals_by_prob <- c(sorted_words[ edit_dist <= min(edit_dist, 2)])
# En caso de que proposals_by_prob esté vacío asignamos la palabra evaluada
proposals_by_prob <- c(proposals_by_prob, word)
# ... y devuelve la palabra primera/más probable en el vector.
proposals_by_prob[1]
}
Funcionamiento
correct("correrctor")
[1] "corrector"
correct("ortogrfaico")
[1] "ortográfico"
correct("wn")
[1] "en"
correct("foncionamento")
[1] "funcionamiento"
El código define la función correct y como argumento incluimos entre comillas la palabra evaluada. La distancia máxima entre la palabra evaluada y el objetivo (una palabra incluida en el corpus, correcta) es de dos ediciones. Pues, de acuerdo a Norvig, la literatura sobre el tema afirma que la inmensa mayoría de errores ortográficos se encuentran a una distancia de 2 o 1. Si la palabra está incluida en el corpus o si no encuentra una corrección de la misma en dos ediciones, no corregirá dicha palabra.
# Distancia 2 ediciones
correct("edictioness")
[1] "ediciones"
# Distancia 3 ediciones
correct("edictiomess")
[1] "edictiomess"
Notas
Básicamente el código original, prácticamente idéntico al código con nuestro propio corpus, extraía las palabras del fichero big.txt de Norvig y creaba un vector en orden descendente en función de la frecuencia de las palabras. Inicialmente pensé que bastaría con crear fichero equivalente en español, concatenando textos en español del proyecto Gutenberg.
Pero buscando un listado de palabras más frecuentes encontré el Corpus de Referencia del Español Actual (CREA), un listado de frecuencias de palabras.
Las ventajas de utilizar el CREA frente a una recopilación de textos mía son:
- Mayor representatividad de la frecuencia de las palabras por la selección de texto de la RAE.
- Simplificación del código pues ya no hay que extraer o contar la frecuencia de cada palabra.
- Fichero de texto de menor tamaño.
Una
desventaja es que si quisiéramos hacer un corrector más sofisticado, como por ejemplo tener en cuenta el contexto de la palabra dentro del texto, tal y como menciona Norvig, el Corpus de Referencia del Español Actual (CREA) no sería de ayuda.
La lista total de frecuencias comprende 737.799 palabras, pero para ganar en rapidez he seleccionado las primeras 100.000 en el fichero crea.txt. El código anterior (código desglosado) importa este fichero de texto, ya en orden descendente, como data frame y lo transforma en la clase table de R. Después utiliza la función creada por Rasmus Bååth.
Código con nuestro propio corpus
En el caso de que queramos crear nuestro propio corpus agrupando texto en español.
# En dos líneas
sorted_words <- names(sort(table(strsplit(tolower(paste(readLines("corpus.txt"), collapse = " ")), "[^abcdefghijklmnñopqrstuvwxyzáéíóúäëïöüçàèìòùâêîôû']+")), decreasing = TRUE))
correct <- function(word) { c(sorted_words[ adist(word, sorted_words) <= min(adist(word, sorted_words), 2)], word)[1] }
# Desglosado
raw_text <- paste(readLines("corpus.txt"), collapse = " ")
split_text <- strsplit(raw_text, "[^abcdefghijklmnñopqrstuvwxyzáéíóúäëïöüçàèìòùâêîôû']+")
sorted_words <- as.table(as.matrix(data.frame(split_text)))
correct <- function(word) {
edit_dist <- adist(word, sorted_words)
min_edit_dist <- min(edit_dist, 2)
proposals_by_prob <- c(sorted_words[ edit_dist <= min(edit_dist, 2)])
proposals_by_prob <- c(proposals_by_prob, word)
proposals_by_prob[1]
}
He sustituido el rango de caracteres "[^a-z]+" por "[^abcdefghijklmnñopqrstuvwxyzáéíóúäëïöüçàèìòùâêîôû']+" especificando los caracteres para que al crear el vector con las palabras no las divida erróneamente cuando encuentre acentos, apostrofes o letras como la ñ y la ç.
Código en dos líneas
# Código modificado
sorted_words <- as.table(as.matrix(read.csv("https://sites.google.com/site/nubededatosblogspotcom/crea.txt", header = FALSE)))
# Código idéntico al de Rasmus Bååth
correct <- function(word) { c(sorted_words[ adist(word, sorted_words) <= min(adist(word, sorted_words), 2)], word)[1] }
# Código original de Rasmus Bååth
sorted_words <- names(sort(table(strsplit(tolower(paste(readLines("http://www.norvig.com/big.txt"), collapse = " ")), "[^a-z]+")), decreasing = TRUE))
correct <- function(word) { c(sorted_words[ adist(word, sorted_words) <= min(adist(word, sorted_words), 2)], word)[1] }
Referencias