Как мне оптимизировать производительность стемминга и проверки орфографии в R? - PullRequest
7 голосов
/ 20 февраля 2020

У меня ~ 1,4 млн документов со средним количеством символов на каждый документ (Медиана: 250 и Среднее: 470).

Я хочу выполнить проверку орфографии и основы перед классификацией.

Имитированный документ:

sentence <- "We aree drivng as fast as we drove yestrday or evven fastter zysxzw" %>%
    rep(times = 6) %>%
    paste(collapse = " ")

nchar(sentence)
[1] 407 

функция для выполнения первой проверки орфографии, а затем стебля

library(hunspell)
library(magrittr)

spellAndStem <- function(sent, language = "en_US"){
  words <- sentence %>%
    strsplit(split = " ") %>%
    unlist

  # spelling
  correct <- hunspell_check(
        words = words, 
        dict = dictionary(language)
  )

  words[!correct] %<>%
    hunspell_suggest(dict = language) %>%
    sapply(FUN = "[", 1)

  # stemming
  words %>%
    hunspell_stem(dict = dictionary(language)) %>%
    unlist %>%
    paste(collapse = " ")
}

Я посмотрел на hunspell() функция для выдачи документа в целом для повышения производительности, но я не понимаю, как я мог бы выполнить проверку орфографии и обработку в этой последовательности.

Измерение времени:

> library(microbenchmark)
> microbenchmark(spellAndStem(sentence), times = 100)
Unit: milliseconds
                   expr      min       lq     mean   median       uq      max neval
 spellAndStem(sentence) 680.3601 689.8842 700.7957 694.3781 702.7493 798.9544   100

При 0,7 с на документ для вычисления потребуется 0,7 * 1400000/3600/24 ​​= 11,3 дня.

Вопрос:

Как я могу оптимизировать это выступление?

Заключительное замечание:

Язык перевода 98% немецкого и 2% английского sh. Не уверен, что информация имеет значение, просто для полноты.

Ответы [ 3 ]

8 голосов
/ 23 февраля 2020

Вы можете существенно оптимизировать свой код, выполняя дорогостоящие шаги над словарем вместо всех слов в документе. Пакет 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:

  1. Я бы предложил изменить последнюю строку функции на sapply(as.list(sent_t_new), paste, collapse = " "), так как это не свернет все документы в одну длинную строку, но будет держать их отдельно.
  2. В настоящее время ваша установка удаляет слова, для которых hunspell не может найти никаких предложений. Я скопировал этот подход (см. Команду tokens_remove), но вы можете подумать о том, чтобы хотя бы выводить отброшенные слова, а не удалять их молча.
  3. Если функция выше предназначена для подготовки к другому анализу текста, было бы более разумно преобразовать данные непосредственно в матрицу терминов документа до того, как будут произведены обработка и проверка орфографии.
  4. Основание - это всего лишь приближение к лемматизации, которая представляет собой процесс фактического нахождения базовой формы слово. Кроме того, stemming обычно работает довольно плохо на немецком языке. В зависимости от того, что вы делаете, вы можете вместо этого использовать лемматизацию (например, используя spacyr) или просто отключить ее, так как stemming редко улучшает результаты на немецком языке.
3 голосов
/ 25 февраля 2020

Используется идея сравнения только уникальных слов. Для этого используются факторы, определяющие уникальные уровни.

  words_fct <- sent %>%
    strsplit(split = " ") %>% 
    unlist(use.names = FALSE) %>%
    factor()

  correct_lvl <- words_fct%>%
    levels()%>%
    hunspell_check(dict = language)

  levels(words_fct)[!correct_lvl] %<>% 
    hunspell_suggest(dict = language) %>%
    sapply("[", 1L)

  levels(words_fct)%<>%
    hunspell_stem(dict = language)%>%
    unlist(use.names = FALSE)

  words_fct%>%
    as.character()%>%
    na.omit()%>%
    paste(collapse = " ")
}

Это немного быстрее, чем у @ JBGruber, но во многих отношениях является производным от ответа @ JBGruber.

Я также как идея использования параллельных структур для всех ваших документов. Предполагая, что каждый документ представляет собой одну строку текста, это, вероятно, будет работать:

library(future.apply)
plan(multiprocess)
future_lapply(documents, spellAndStem_fcts, language)
3 голосов
/ 22 февраля 2020

hunspell_suggest - это просто дорогая операция, поскольку она вычисляет расстояние между вашей строкой и каждым словом в словаре (см. Здесь: https://github.com/ropensci/hunspell/issues/7). Когда я удаляю строки hunspell_suggest, это занимает всего 25 мс на моей машине. Так что, если вы хотите ускорить его, это важная часть. Обратите внимание, что имеет значение, сколько неправильных слов в реальных документах. Ваш пример с примерно 50% слов с ошибками должен быть скорее исключением. Почему бы вам сначала не попробовать алгоритм на первой паре документов, чтобы получить более реалистичную c оценку времени. Я предполагаю, что язык будет иметь значение (для вашего удобства), поскольку в английском языке больше слов, чем в немецком (например, размер словаря).

Простая и очевидная вещь, которую нужно сделать, - это использовать несколько ядер. Что-то простое, например, следующее с пакетом parallel уже вдвое сокращает время с моими четырьмя ядрами:

sentences <- rep(sentence, 4)
microbenchmark(lapply = lapply(sentences, spellAndStem),
               mclapply = parallel::mclapply(sentences, spellAndStem),
               times = 10)

Unit: seconds
                                        expr      min       lq     mean   median       uq      max neval cld
             lapply(sentences, spellAndStem) 1.967008 2.023291 2.045705 2.051764 2.077168 2.105420    10   b
 parallel::mclapply(sentences, spellAndStem) 1.011945 1.048055 1.078003 1.081850 1.109274 1.135508    10  a 

Предложение Эндрю Густара также может сработать. Даже если вы просто примените функцию предложения к группе документов, это должно значительно ускорить вычисления. Проблема состоит в том, чтобы отделить документы и соединить их после того, как они будут выделены - я думаю, что «разделитель» для документов будет просто остановлен и впоследствии не будет распознаваем. Судя по вашему вопросу, вы уже пробовали это или что-то подобное.

Меньший словарь также может помочь, но, вероятно, не очень хорошая идея, если вы хотите высококачественные данные.

Кстати Я бы не стал считать 11 дней длинным для вычисления, которое должно быть сделано только один раз. Вы можете просто загрузить скрипт на сервер, на котором установлен R, и запустить его через Rscript из оболочки (используйте nohup, чтобы снова выйти из системы без остановки процесса). Это особенно верно, если у вас есть доступ к сильной «рабочей машине» (например, в университете) со многими ядрами.

...