edit: The new package text2vec is excellent, and solves this problem (and many others) really well.
text2vec on CRAN text2vec on github vignette that illustrates ngram tokenization
I have a pretty large text dataset in R, which I've imported as a character vector:
#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, ' ')
})
I can convert this character data to a bag-of-words representation as follows:
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
So R can vectorize 1,000,000 million short sentences into a bag-of-words representation in about 3 seconds (not bad!):
> 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,] . . . . . . .
I can throw this sparse matrix into glmnet or irlba and do some pretty awesome quantitative analysis of textual data. Hooray!
Now I'd like to extend this analysis to a bag-of-ngrams matrix, rather than a bag-of-words matrix. So far, the fastest way I've found to do this is as follows (all of the ngram functions I could find on CRAN choked on this dataset, so 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)
This takes about 150 seconds (not bad for a pure r function), but I'd like to go faster and extend to bigger datasets.
Are there any really fast functions in R for n-gram vectorization of text? Ideally I'm looking for an Rcpp function that takes a character vector as input, and returns a sparse matrix of documents x ngrams as output, but would also be happy to have some guidance writing the Rcpp function myself.
Even a faster version of the find_ngrams
function would be helpful, as that's the main bottleneck. R is surprisingly fast at tokenization.
Edit 1 Here's another example dataset:
sents2 <- sapply(rpois(100000, 500) + 1, samplefun, words, ' ')
In this case, my functions for creating a bag-of-words matrix take about 30 seconds and my functions for creating a bag-of-ngrams matrix take about 500 seconds. Again, existing n-gram vectorizers in R seem to choke on this dataset (though I'd love to be proven wrong!)
Edit 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
tau::textcnt(as.list(sents), n = 2L, method = "string", recursive = TRUE)
instead offind_ngrams
? Takes half the time, but delivers only bigrams (n=2). – Adolescencestringdist::qgrams
does really fast character qgrams. The author is currently working on supporting words (ints). – Yttriatau_t1 / zach_t1
=649.48
/675.82
. Not much of a difference anymore. – Adolescence