Вы можете существенно оптимизировать свой код, выполняя дорогостоящие шаги над словарем вместо всех слов в документе. Пакет quanteda
предлагает действительно полезный объектный класс или его название tokens
:
toks <- quanteda::tokens(sentence)
unclass(toks)
#> $text1
#> [1] 1 2 3 4 5 4 6 7 8 9 10 11 12 1 2 3 4 5 4 6 7 8 9 10 11
#> [26] 12 1 2 3 4 5 4 6 7 8 9 10 11 12 1 2 3 4 5 4 6 7 8 9 10
#> [51] 11 12 1 2 3 4 5 4 6 7 8 9 10 11 12 1 2 3 4 5 4 6 7 8 9
#> [76] 10 11 12
#>
#> attr(,"types")
#> [1] "We" "aree" "drivng" "as" "fast" "we"
#> [7] "drove" "yestrday" "or" "evven" "fastter" "zysxzw"
#> attr(,"padding")
#> [1] FALSE
#> attr(,"what")
#> [1] "word"
#> attr(,"ngrams")
#> [1] 1
#> attr(,"skip")
#> [1] 0
#> attr(,"concatenator")
#> [1] "_"
#> attr(,"docvars")
#> data frame with 0 columns and 1 row
Как видите, текст разбивается на словарь (types
) и положение слов. Мы можем использовать это для оптимизации вашего кода, выполнив все шаги над types
вместо всего текста:
spellAndStem_tokens <- function(sent, language = "en_US") {
sent_t <- quanteda::tokens(sent)
# extract types to only work on them
types <- quanteda::types(sent_t)
# spelling
correct <- hunspell_check(
words = as.character(types),
dict = hunspell::dictionary(language)
)
pattern <- types[!correct]
replacement <- sapply(hunspell_suggest(pattern, dict = language), FUN = "[", 1)
types <- stringi::stri_replace_all_fixed(
types,
pattern,
replacement,
vectorize_all = FALSE
)
# stemming
types <- hunspell_stem(types, dict = dictionary(language))
# replace original tokens
sent_t_new <- quanteda::tokens_replace(sent_t, quanteda::types(sent_t), as.character(types))
sent_t_new <- quanteda::tokens_remove(sent_t_new, pattern = "NULL", valuetype = "fixed")
paste(as.character(sent_t_new), collapse = " ")
}
Я использую пакет bench
для проведения сравнительного анализа, так как он также проверяет, результаты двух функций идентичны, и, как мне кажется, в целом удобнее:
res <- bench::mark(
spellAndStem(sentence),
spellAndStem_tokens(sentence)
)
res
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 spellAndStem(sentence) 807ms 807ms 1.24 259KB 0
#> 2 spellAndStem_tokens(sentence) 148ms 150ms 6.61 289KB 0
summary(res, relative = TRUE)
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 spellAndStem(sentence) 5.44 5.37 1 1 NaN
#> 2 spellAndStem_tokens(sentence) 1 1 5.33 1.11 NaN
Новая функция в 5,44 раза быстрее оригинальной. Обратите внимание, что разница становится еще более заметной, чем больше вводимый текст:
sentence <- "We aree drivng as fast as we drove yestrday or evven fastter zysxzw" %>%
rep(times = 600) %>%
paste(collapse = " ")
res_big <- bench::mark(
spellAndStem(sentence),
spellAndStem_tokens(sentence)
)
res_big
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 spellAndStem(sentence) 1.27m 1.27m 0.0131 749.81KB 0
#> 2 spellAndStem_tokens(sentence) 178.26ms 182.12ms 5.51 1.94MB 0
summary(res_big, relative = TRUE)
#> # A tibble: 2 x 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 spellAndStem(sentence) 428. 419. 1 1 NaN
#> 2 spellAndStem_tokens(sentence) 1 1 420. 2.65 NaN
Как видите, время, необходимое для обработки семикратного сэмпла в 100 раз, почти такое же, как для меньшего один. Это потому, что словарный запас между ними точно такой же. Мы можем экстраполировать этот результат на весь ваш набор данных, предполагая, что этот большой образец представляет 100 ваших документов. Функция должна занимать менее часа (0,17826 * 14000/3600 = 0,69), но вычисление действительно несовершенно, так как фактическое время, необходимое для его выполнения на ваших реальных данных, будет зависеть почти исключительно от размера словаря.
Помимо аспекта программирования / производительности, у меня есть еще несколько проблем, которые могут быть неприменимы в вашем конкретном случае c:
- Я бы предложил изменить последнюю строку функции на
sapply(as.list(sent_t_new), paste, collapse = " ")
, так как это не свернет все документы в одну длинную строку, но будет держать их отдельно. - В настоящее время ваша установка удаляет слова, для которых
hunspell
не может найти никаких предложений. Я скопировал этот подход (см. Команду tokens_remove
), но вы можете подумать о том, чтобы хотя бы выводить отброшенные слова, а не удалять их молча. - Если функция выше предназначена для подготовки к другому анализу текста, было бы более разумно преобразовать данные непосредственно в матрицу терминов документа до того, как будут произведены обработка и проверка орфографии.
- Основание - это всего лишь приближение к лемматизации, которая представляет собой процесс фактического нахождения базовой формы слово. Кроме того, stemming обычно работает довольно плохо на немецком языке. В зависимости от того, что вы делаете, вы можете вместо этого использовать лемматизацию (например, используя
spacyr
) или просто отключить ее, так как stemming редко улучшает результаты на немецком языке.