Ответ 1
Это действительно интересная проблема, и я потратил много времени на борьбу с пакетом Quanteda. Он включает в себя три аспекта, которые я буду комментировать, хотя это только третий, который действительно затрагивает ваш вопрос. Но первые два момента объясняют, почему я сосредоточен только на функции создания ngram, поскольку, как вы указываете, именно здесь можно улучшить скорость.
- лексемизацию
. Здесь вы используете
string::str_split_fixed()
для символа пробела, который является самым быстрым, но не лучшим методом для токенизации. Мы реализовали это почти точно так же, как и вquanteda::tokenize(x, what = "fastest word")
. Это не самое лучшее, потому что stringi может выполнять гораздо более умные реализации разделителей пробелов. (Даже класс символов\\s
умнее, но немного медленнее - это реализовано какwhat = "fasterword"
). Однако ваш вопрос касался не токенизации, поэтому этот пункт является просто контекстом. -
Табуляция матрицы признаков документа. Здесь мы также используем пакет Матрица и индексируем документы и функции (я называю их функциями, а не терминами) и создаю разреженную матрицу, как и в приведенном выше коде. Но использование
match()
намного быстрее, чем методы совпадения/слияния, которые мы использовали с помощью data.table. Я собираюсь перекодировать функциюquanteda::dfm()
, так как ваш метод более изящный и быстрый. Действительно, очень рад, что увидел это! -
Создание ngram. Здесь я думаю, что могу реально помочь с точки зрения производительности. Мы реализуем это в quanteda с помощью аргумента
quanteda::tokenize()
, называемогоgrams = c(1)
, где это значение может быть любым целым набором. Например, наш матч для униграмм и биграмм был быngrams = 1:2
. Вы можете проверить код на https://github.com/kbenoit/quanteda/blob/master/R/tokenize.R, см. Внутреннюю функциюngram()
. Я воспроизвел это ниже и сделал обертку, чтобы мы могли напрямую сравнить ее с вашей функциейfind_ngrams()
.
код:
# wrapper
find_ngrams2 <- function(x, ngrams = 1, concatenator = " ") {
if (sum(1:length(ngrams)) == sum(ngrams)) {
result <- lapply(x, ngram, n = length(ngrams), concatenator = concatenator, include.all = TRUE)
} else {
result <- lapply(x, function(x) {
xnew <- c()
for (n in ngrams)
xnew <- c(xnew, ngram(x, n, concatenator = concatenator, include.all = FALSE))
xnew
})
}
result
}
# does the work
ngram <- function(tokens, n = 2, concatenator = "_", include.all = FALSE) {
if (length(tokens) < n)
return(NULL)
# start with lower ngrams, or just the specified size if include.all = FALSE
start <- ifelse(include.all,
1,
ifelse(length(tokens) < n, 1, n))
# set max size of ngram at max length of tokens
end <- ifelse(length(tokens) < n, length(tokens), n)
all_ngrams <- c()
# outer loop for all ngrams down to 1
for (width in start:end) {
new_ngrams <- tokens[1:(length(tokens) - width + 1)]
# inner loop for ngrams of width > 1
if (width > 1) {
for (i in 1:(width - 1))
new_ngrams <- paste(new_ngrams,
tokens[(i + 1):(length(tokens) - width + 1 + i)],
sep = concatenator)
}
# paste onto previous results and continue
all_ngrams <- c(all_ngrams, new_ngrams)
}
all_ngrams
}
Вот сравнение простого текста:
txt <- c("The quick brown fox named Seamus jumps over the lazy dog.",
"The dog brings a newspaper from a boy named Seamus.")
tokens <- tokenize(toLower(txt), removePunct = TRUE)
tokens
# [[1]]
# [1] "the" "quick" "brown" "fox" "named" "seamus" "jumps" "over" "the" "lazy" "dog"
#
# [[2]]
# [1] "the" "dog" "brings" "a" "newspaper" "from" "a" "boy" "named" "seamus"
#
# attr(,"class")
# [1] "tokenizedTexts" "list"
microbenchmark::microbenchmark(zach_ng <- find_ngrams(tokens, 2),
ken_ng <- find_ngrams2(tokens, 1:2))
# Unit: microseconds
# expr min lq mean median uq max neval
# zach_ng <- find_ngrams(tokens, 2) 288.823 326.0925 433.5831 360.1815 542.9585 897.469 100
# ken_ng <- find_ngrams2(tokens, 1:2) 74.216 87.5150 130.0471 100.4610 146.3005 464.794 100
str(zach_ng)
# List of 2
# $ : chr [1:21] "the" "quick" "brown" "fox" ...
# $ : chr [1:19] "the" "dog" "brings" "a" ...
str(ken_ng)
# List of 2
# $ : chr [1:21] "the" "quick" "brown" "fox" ...
# $ : chr [1:19] "the" "dog" "brings" "a" ...
Для вашего действительно большого симулированного текста, вот сравнение:
tokens <- stri_split_fixed(sents1, ' ')
zach_ng1_t1 <- system.time(zach_ng1 <- find_ngrams(tokens, 2))
ken_ng1_t1 <- system.time(ken_ng1 <- find_ngrams2(tokens, 1:2))
zach_ng1_t1
# user system elapsed
# 230.176 5.243 246.389
ken_ng1_t1
# user system elapsed
# 58.264 1.405 62.889
Уже улучшение, я был бы рад, если бы это могло быть улучшено дальше. Я также должен был бы реализовать более быстрый метод dfm()
в квантования, чтобы вы могли получить то, что хотите, просто через:
dfm(sents1, ngrams = 1:2, what = "fastestword",
toLower = FALSE, removePunct = FALSE, removeNumbers = FALSE, removeTwitter = TRUE))
(Это уже работает, но медленнее, чем ваш общий результат, потому что способ создания конечного разреженного матричного объекта выполняется быстрее, но я скоро это изменил.)