我有〜1,4m个文档,每个文档的平均字符数为(Median:250和Mean: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.7s,将需要0.7 * 1400000/3600/24 = 11.3天来进行计算。
问题:
我如何优化此性能?
最后发言:
目标语言是98%的德语和2%的英语。不确定信息是否重要,只是为了完整性。
hunspell_suggest
只是一项昂贵的操作,因为它会计算字符串与字典中每个单词之间的距离(请参见此处:https://github.com/ropensci/hunspell/issues/7)。当我删除hunspell_suggest
行时,在我的机器上平均只需要25毫秒。因此,如果要加快速度,这是关键部分。请注意,实际文档中有多少个不正确的单词会有所不同。您的示例中大约有50%的拼写错误的单词应该是个例外。您为什么不先对前几份文档尝试算法,以获得更实际的时间估计。我认为语言会很重要(为了您的利益),因为英语中的单词比德语中的单词(想像字典大小)要多。
一个简单而显而易见的事情是使用多个内核。 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
[安德鲁·古斯塔(Andrew Gustar)的建议也可以。即使您仅对一个文档组应用建议功能,也应大大加快计算速度。问题是将文档分开,然后将它们在茎中放在一起-我猜这些文件的“分隔符”将被茎梗,之后便无法识别。从您的问题来看,您已经尝试过此方法或类似方法。
较小的字典也可以提供帮助,但是如果您想要高质量的数据,可能不是一个好主意。
顺便说一句,我不会认为只需要执行一次的计算就需要11天的时间。您可以简单地将脚本上传到已安装R的服务器,然后从外壳通过Rscript
在其中运行该脚本(使用nohup
再次注销而不停止该进程)。如果您可以访问具有许多核心的强大的“工作机器”(例如在大学中),则尤其如此。
您可以通过对词汇表而不是文档中的所有单词执行昂贵的步骤来实质上优化代码。 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
由reprex package(v0.3.0)在2020-02-23创建
从最后一个推算出来,并假设这个较大的样本代表您的100个文档,该函数应该花费不到一个小时的时间
0.17826 * 14000/3600 = 0.69
我也建议将函数的最后一行更改为sapply(as.list(sent_t_new), paste, collapse = " ")
,因为这不会将所有文档折叠成一个长字符串,而是将它们分开。