在R

时间:2015-07-22 17:50:11

标签: r vectorization text-mining n-gram text2vec

编辑:新的包text2vec非常好,并且很好地解决了这个问题(以及其他许多问题)。

text2vec on CRAN text2vec on github vignette that illustrates ngram tokenization

我在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可以在大约3秒钟内将1,000,000,000个短句矢量化为一个单词表示形式(不错!):

> 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,]   .     .   .   .    .     .   .

我可以将这个稀疏矩阵抛入glmnetirlba,并对文本数据做一些非常棒的定量分析。万岁!

现在我想将这个分析扩展到一个袋子矩阵,而不是一个词袋矩阵。到目前为止,我发现这样做的最快方法如下(我在CRAN上找到的所有ngram函数都在这个数据集上被阻塞,所以I got a little help from 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-gram矢量化?理想情况下,我正在寻找一个Rcpp函数,它将一个字符向量作为输入,并返回一个文档稀疏矩阵x ngrams作为输出,但也很乐意自己编写Rcpp函数的一些指导。

即使更快版本的find_ngrams功能也会有所帮助,因为这是主要的瓶颈。 R在令牌化方面出人意料地快。

修改1 这是另一个示例数据集:

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

在这种情况下,我创建一个词袋矩阵的函数大约需要30秒,而我创建一个袋子矩阵的函数大约需要500秒。同样,R中现有的n-gram矢量化器似乎扼杀了这个数据集(虽然我很乐意被证明是错误的!)

编辑2 蒂姆斯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 个答案:

答案 0 :(得分:10)

这是一个非常有趣的问题,而且我花了很多时间在 quanteda 包中努力解决这个问题。它涉及三个方面,我将评论,虽然它只是第三个真正解决你的问题。但前两点解释了为什么我只专注于ngram创建功能,因为 - 正如你所指出的那样 - 这就是提高速度的地方。

  1. 标记。这里你在空格字符上使用string::str_split_fixed(),这是最快但不是最好的标记方法。我们在quanteda::tokenize(x, what = "fastest word")中实现的几乎完全相同。它并不是最好的,因为 stringi 可以做更多更明智的空白分隔符实现。 (即使字符类\\s更聪明,但稍慢 - 这实现为what = "fasterword")。你的问题不是关于标记化,所以这一点只是背景。

  2. 制表文档要素矩阵。在这里,我们还使用 Matrix 包,索引文档和功能(我称之为功能,而不是术语),并像上面的代码一样直接创建稀疏矩阵。但是,您使用match()要比我们通过 data.table 使用的匹配/合并方法快得多。我将重新编码quanteda::dfm()函数,因为您的方法更优雅,更快。真的,很高兴我看到了这个!

  3. ngram创建。在这里,我认为我可以在性能方面提供帮助。我们通过quanteda::tokenize()的参数在 quanteda 中实现它,名为grams = c(1),其值可以是任何整数集。例如,我们对unigrams和bigrams的匹配将是ngrams = 1:2。您可以在https://github.com/kbenoit/quanteda/blob/master/R/tokenize.R检查代码,查看内部函数ngram()。我已经在下面复制了这个并制作了一个包装器,以便我们可以直接将它与您的find_ngrams()函数进行比较。

  4. 代码:

    # 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()方法实施到 quanteda 中,以便您可以通过以下方式获得您想要的内容:

    dfm(sents1, ngrams = 1:2, what = "fastestword",
        toLower = FALSE, removePunct = FALSE, removeNumbers = FALSE, removeTwitter = TRUE)) 
    

    (这已经有效,但比你的整体结果慢,因为你创建最终稀疏矩阵对象的方式更快 - 但我会很快改变它。)

答案 1 :(得分:2)

以下是使用tokenizers的开发版本的测试,您可以使用docker run -p 7180:7180 \ --hostname=quickstart.cloudera --privileged=true \ -t -i cloudera/quickstart:latest \ /usr/bin/docker-quickstart 进行测试。

使用上面devtools::install_github("ropensci/tokenizers")sents1sents2的定义:

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

测试sents2

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 

结果:

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 

很多将取决于被标记的文本,但这似乎表明加速2x到20x。