您可以通过对词汇表而不是文档中的所有单词执行昂贵的步骤来大幅优化代码。这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),但计算确实不完美,因为在真实数据上运行它所需的实际时间几乎完全取决于词汇表的大小。
除了编程/性能方面之外,我还有一些可能不适用于您的具体情况的问题:
- 我建议将函数中的最后一行更改为
sapply(as.list(sent_t_new), paste, collapse = " ")
因为这不会将所有文档折叠成一长串,而是将它们分开。
- 目前,您的设置会删除其中的单词
hunspell
找不到任何建议。我复制了这个方法(参见tokens_remove
命令),但您可能需要考虑至少输出丢弃的单词,而不是默默地删除它们。
- 如果上面的函数是为其他一些文本分析做准备,那么在执行词干提取和拼写检查之前将数据直接转换为文档术语矩阵会更有意义。
- 词干提取只是词形还原的一种近似,词形还原是实际找到单词基本形式的过程。此外,词干提取在德语中的效果通常很差。根据您正在做的事情,您可能想要进行词形还原(例如,使用
spacyr
)或者干脆将其关闭,因为词干很少能改善德语的结果。