Очень быстрая векторизация слова ngram в R

edit: новый пакет text2vec отлично подходит и решает эту проблему (и многие другие) очень хорошо.

text2vec на CRAN text2vec на github виньетка, иллюстрирующая токенизацию ngram

у меня есть довольно большой текстовый набор данных в R, который я импортировал как вектор символов:

#Takes about 15 seconds
system.time({
  set.seed(1)
  samplefun <- function(n, x, collapse){
    paste(sample(x, n, replace=TRUE), collapse=collapse)
  }
  words <- sapply(rpois(10000, 3) + 1, samplefun, letters, '')
  sents1 <- sapply(rpois(1000000, 5) + 1, samplefun, words, ' ')
})

Я могу преобразовать эти символьные данные в представление мешка слов как следует:

library(stringi)
library(Matrix)
tokens <- stri_split_fixed(sents1, ' ')
token_vector <- unlist(tokens)
bagofwords <- unique(token_vector)
n.ids <- sapply(tokens, length)
i <- rep(seq_along(n.ids), n.ids)
j <- match(token_vector, bagofwords)
M <- sparseMatrix(i=i, j=j, x=1L)
colnames(M) <- bagofwords

таким образом, R может векторизовать 1,000,000 миллионов коротких предложений в представление мешка слов примерно за 3 секунды (неплохо!):

> M[1:3, 1:7]
10 x 7 sparse Matrix of class "dgCMatrix"
      fqt hqhkl sls lzo xrnh zkuqc mqh
 [1,]   1     1   1   1    .     .   .
 [2,]   .     .   .   .    1     1   1
 [3,]   .     .   .   .    .     .   .

Я могу бросить эту разреженную матрицу в glmnet или irlba и сделать довольно удивительный количественный анализ текстовых данных. Ура!

теперь я хотел бы распространить этот анализ на матрицу мешка-из-ngrams, а не на матрицу мешка-из-слов. До сих пор самый быстрый способ я нашел сделать это можно следующим образом (все функции ngram, которые я мог найти на CRAN, подавились этим набором данных, поэтому я получил небольшую помощь от SO):

find_ngrams <- function(dat, n, verbose=FALSE){
  library(pbapply)
  stopifnot(is.list(dat))
  stopifnot(is.numeric(n))
  stopifnot(n>0)
  if(n == 1) return(dat)
  pblapply(dat, function(y) {
    if(length(y)<=1) return(y)
    c(y, unlist(lapply(2:n, function(n_i) {
      if(n_i > length(y)) return(NULL)
      do.call(paste, unname(as.data.frame(embed(rev(y), n_i), stringsAsFactors=FALSE)), quote=FALSE)
    })))
  })
}

text_to_ngrams <- function(sents, n=2){
  library(stringi)
  library(Matrix)
  tokens <- stri_split_fixed(sents, ' ')
  tokens <- find_ngrams(tokens, n=n, verbose=TRUE)
  token_vector <- unlist(tokens)
  bagofwords <- unique(token_vector)
  n.ids <- sapply(tokens, length)
  i <- rep(seq_along(n.ids), n.ids)
  j <- match(token_vector, bagofwords)
  M <- sparseMatrix(i=i, j=j, x=1L)
  colnames(M) <- bagofwords
  return(M)
}

test1 <- text_to_ngrams(sents1)

это занимает около 150 секунд (неплохо для чистой функции r), но я хотел бы пойти быстрее и расширить на большие наборы данных.

есть очень быстро функции в R для n-граммовой векторизации текста? В идеале я ищу Rcpp функция, которая принимает вектор символов как input, и возвращает разреженную матрицу документов x ngrams в качестве выходных данных,но также был бы рад получить некоторые рекомендации по написанию функции Rcpp.

даже более быстрая версия find_ngrams функция была бы полезна, так как это основное узкое место. R-это на удивление быстро в токенизации.

изменить 1 Вот еще один пример набора данных:

sents2 <- sapply(rpois(100000, 500) + 1, samplefun, words, ' ')

в этом случае мои функции для создания матрицы мешка слов занимают около 30 секунд, а мои функции для создания матрицы bag-of-ngrams занимают около 500 секунд. Опять же, существующие N-граммовые векторизаторы в R, похоже, задыхаются от этого набора данных (хотя я хотел бы быть доказанным неправильно!)

Изменить 2 Timings vs tau:

zach_t1 <- system.time(zach_ng1 <- text_to_ngrams(sents1))
tau_t1 <- system.time(tau_ng1 <- tau::textcnt(as.list(sents1), n = 2L, method = "string", recursive = TRUE))
tau_t1 / zach_t1 #1.598655

zach_t2 <- system.time(zach_ng2 <- text_to_ngrams(sents2))
tau_t2 <- system.time(tau_ng2 <- tau::textcnt(as.list(sents2), n = 2L, method = "string", recursive = TRUE))
tau_t2 / zach_t2 #1.9295619

2 ответов


это действительно интересная проблема, и я потратил много времени, борясь с ней в quanteda пакета. Он включает в себя три аспекта, которые я прокомментирую, хотя это только третий, который действительно касается вашего вопроса. Но первые два момента объясняют, почему я сосредоточился только на функции создания ngram, поскольку-как вы указываете-именно там можно улучшить скорость.

  1. токенизации. здесь вы используете string::str_split_fixed() на пробел, который является самым быстрым, но не лучшим способом для разбора. Мы реализовали это почти точно так же, как было в quanteda::tokenize(x, what = "fastest word"). Это не самое лучшее, потому что stringi может делать гораздо более умные реализации разделителей пробелов. (Даже класс символов \s умнее, но немного медленнее - это реализовано как what = "fasterword"). Ваш вопрос был не о однако токенизация, поэтому этот момент - просто контекст.

  2. табуляция матрицы функций документа. Здесь мы также используем Матрица пакет, и индексировать документы и функции (я называю их функциями, а не терминами), и создать разреженную матрицу непосредственно, как вы делаете в коде выше. Но ваше использование match() намного быстрее, чем методы сопоставления / слияния, которые мы использовали через данные.таблица. Я собираюсь перекодировать quanteda::dfm() функция, так как ваш метод более элегантный и быстрый. Очень, очень рад, что увидел это!

  3. создание ngram. Здесь я думаю, что действительно могу помочь с точки зрения производительности. Мы реализуем это в quanteda через аргумент quanteda::tokenize(), под названием grams = c(1) где значение может быть любым целым набором. Наш матч для unigrams и bigrams будет ngrams = 1:2, например. Вы можете проверить код в https://github.com/kbenoit/quanteda/blob/master/R/tokenize.R, см. внутреннюю функцию ngram(). Я воспроизвел это ниже и сделал обертку, чтобы мы могли напрямую сравнить ее с вашим


вот тест с использованием версии dev tokenizers, который вы можете получить с помощью devtools::install_github("ropensci/tokenizers").

используя определения sents1, sents2 и find_ngrams() выше:

library(stringi)
library(magrittr)
library(tokenizers)
library(microbenchmark)
library(pbapply)


set.seed(198)
sents1_sample <- sample(sents1, 1000)
sents2_sample <- sample(sents2, 1000)

test_sents1 <- microbenchmark(
  find_ngrams(stri_split_fixed(sents1_sample, ' '), n = 2), 
  tokenize_ngrams(sents1_sample, n = 2),
  times = 25)
test_sents1

результаты:

Unit: milliseconds
                                                     expr       min        lq       mean
 find_ngrams(stri_split_fixed(sents1_sample, " "), n = 2) 79.855282 83.292816 102.564965
                    tokenize_ngrams(sents1_sample, n = 2)  4.048635  5.147252   5.472604
    median         uq        max neval cld
 93.622532 109.398341 226.568870    25   b
  5.479414   5.805586   6.595556    25  a 

тестирование на sents2

test_sents2 <- microbenchmark(
  find_ngrams(stri_split_fixed(sents2_sample, ' '), n = 2), 
  tokenize_ngrams(sents2_sample, n = 2),
  times = 25)
test_sents2

результаты:

Unit: milliseconds
                                                     expr      min       lq     mean
 find_ngrams(stri_split_fixed(sents2_sample, " "), n = 2) 509.4257 521.7575 562.9227
                    tokenize_ngrams(sents2_sample, n = 2) 288.6050 295.3262 306.6635
   median       uq      max neval cld
 529.4479 554.6749 844.6353    25   b
 306.4858 310.6952 332.5479    25  a 

проверять просто времени

timing <- system.time({find_ngrams(stri_split_fixed(sents1, ' '), n = 2)})
timing

   user  system elapsed 
 90.499   0.506  91.309 

timing_tokenizers <- system.time({tokenize_ngrams(sents1, n = 2)})
timing_tokenizers

   user  system elapsed 
  6.940   0.022   6.964 

timing <- system.time({find_ngrams(stri_split_fixed(sents2, ' '), n = 2)})
timing

   user  system elapsed 
138.957   3.131 142.581 

timing_tokenizers <- system.time({tokenize_ngrams(sents2, n = 2)})
timing_tokenizers

   user  system elapsed 
  65.22    1.57   66.91

многое будет зависеть от токенизации текстов, но это, похоже, указывает на ускорение 2x в 20 раз.