15

編集: 新しいパッケージ text2vec は優れており、この問題 (および他の多くの問題) を非常にうまく解決します。

CRAN の text2vec github の text2vec 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, ' ')
})

次のように、この文字データを bag-of-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 百万の短い文をバッグオブワード表現にベクトル化できます (悪くない!):

> 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に投入して、テキスト データの非常に優れた定量分析を行うことができます。万歳!

ここで、この分析を、bag-of-words 行列ではなく、bag-of-ngrams 行列に拡張したいと思います。これまでのところ、これを行うために私が見つけた最速の方法は次のとおりです (CRAN で見つけることができるすべての ngram 関数は、このデータセットで詰まっているため、 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 関数としては悪くありません) が、より高速に処理してより大きなデータセットに拡張したいと考えています。

テキストのn-gramベクトル化のためにRに本当に高速な関数はありますか? 理想的には、文字ベクトルを入力として受け取り、ドキュメント x ngrams のスパース行列を出力として返すRcpp関数を探していますが、Rcpp 関数を自分で作成するためのガイダンスもあれば幸いです。

それが主なボトルネックであるため、関数のより高速なバージョンでfind_ngramsも役立ちます。R のトークン化は驚くほど高速です。

編集 1 別のサンプル データセットを次に示します。

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

この場合、bag-of-words 行列を作成する関数には約 30 秒かかり、bag-of-ngrams 行列を作成する関数には約 500 秒かかります。繰り返しになりますが、R の既存の n グラム ベクトライザーは、このデータセットを使用していないようです (ただし、間違っていることが証明されることを望みます!)

2 つのタイミングとタウを編集します。

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
4

2 に答える 2

10

これは非常に興味深い問題であり、quantedaパッケージで多くの時間をかけて取り組んできた問題です。それには、私がコメントする 3 つの側面が含まれますが、実際にあなたの質問に対処するのは 3 番目だけです。しかし、最初の 2 つのポイントは、なぜ私が ngram 作成機能だけに注目したかを説明しています。

  1. トークン化。 ここでstring::str_split_fixed()はスペース文字を使用していますが、これは最速ですが、トークン化の最適な方法ではありません。にあったものとほぼ同じものを実装しましたquanteda::tokenize(x, what = "fastest word")stringiは空白区切り文字のよりスマートな実装を行うことができるため、これは最善ではありません。(文字クラス\\sもよりスマートですが、少し遅くなります - これは として実装されていますwhat = "fasterword")。ただし、あなたの質問はトークン化に関するものではないため、この点は単なるコンテキストです。

  2. ドキュメント機能マトリックスの集計。ここでもMatrixパッケージを使用し、ドキュメントと機能 (用語ではなく機能と呼びます) にインデックスを付け、上記のコードで行ったように疎行列を直接作成します。しかし、data.tablematch()で使用していた match/merge メソッドよりもはるかに高速です。あなたのメソッドはよりエレガントで高速なので、関数を再コーディングします。本当に、本当にこれを見てよかったです!quanteda::dfm()

  3. 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()方法をquantedaに実装して、必要なものを簡単に取得できるようにする必要もあります。

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

(これは既に機能していますが、最終的な疎行列オブジェクトを作成する方法の方が高速であるため、全体的な結果よりも遅くなりますが、これはすぐに変更します。)

于 2015-07-24T02:59:05.353 に答える