Более эффективные средства создания корпуса и DTM с 4 млн строк.

В моем файле более 4 миллионов строк, и мне нужен более эффективный способ преобразования моих данных в матрицу терминов корпуса и документа, чтобы я мог передать их байесовскому классификатору.

Рассмотрим следующий код:

library(tm)

GetCorpus <-function(textVector)
{
  doc.corpus <- Corpus(VectorSource(textVector))
  doc.corpus <- tm_map(doc.corpus, tolower)
  doc.corpus <- tm_map(doc.corpus, removeNumbers)
  doc.corpus <- tm_map(doc.corpus, removePunctuation)
  doc.corpus <- tm_map(doc.corpus, removeWords, stopwords("english"))
  doc.corpus <- tm_map(doc.corpus, stemDocument, "english")
  doc.corpus <- tm_map(doc.corpus, stripWhitespace)
  doc.corpus <- tm_map(doc.corpus, PlainTextDocument)
  return(doc.corpus)
}

data <- data.frame(
  c("Let the big dogs hunt","No holds barred","My child is an honor student"), stringsAsFactors = F)

corp <- GetCorpus(data[,1])

inspect(corp)

dtm <- DocumentTermMatrix(corp)

inspect(dtm)

Выход:

> inspect(corp)
<<VCorpus (documents: 3, metadata (corpus/indexed): 0/0)>>

[[1]]
<<PlainTextDocument (metadata: 7)>>
let big dogs hunt

[[2]]
<<PlainTextDocument (metadata: 7)>>
 holds bar

[[3]]
<<PlainTextDocument (metadata: 7)>>
 child honor stud
> inspect(dtm)
<<DocumentTermMatrix (documents: 3, terms: 9)>>
Non-/sparse entries: 9/18
Sparsity           : 67%
Maximal term length: 5
Weighting          : term frequency (tf)

              Terms
Docs           bar big child dogs holds honor hunt let stud
  character(0)   0   1     0    1     0     0    1   1    0
  character(0)   1   0     0    0     1     0    0   0    0
  character(0)   0   0     1    0     0     1    0   0    1

Мой вопрос: что можно использовать для более быстрого создания корпуса и DTM? Кажется, это очень медленно, если я использую более 300 тыс. строк.

Я слышал, что могу использовать data.table, но не знаю, как это сделать.

Я также просмотрел пакет qdap, но он выдает ошибку при попытке загрузить пакет, плюс я даже не знаю, будет ли он работать.

Ссылка http://cran.r-project.org/web/packages/qdap/qdap.pdf


person user1477388    schedule 15.08.2014    source источник
comment
qdap не будет быстрее для этой задачи, поскольку он использует пакет tm в качестве серверной части. Но регулярное выражение с data.table/dplyr или параллельной обработкой может быть.   -  person Tyler Rinker    schedule 15.08.2014
comment
@TylerRinker Большое спасибо за совет. Как вы думаете, вы могли бы указать мне правильное направление или (в идеале) предоставить пример «яблоки для яблок», используя код R, который я предоставил выше?   -  person user1477388    schedule 15.08.2014


Ответы (4)


Я думаю, вы можете рассмотреть решение, более ориентированное на регулярное выражение. Вот некоторые из проблем/мыслей, с которыми я борюсь как разработчик. В настоящее время я внимательно изучаю пакет stringi для разработки, так как в нем есть несколько последовательно названных функций, которые очень быстро подходят для манипуляций со строками.

В этом ответе я пытаюсь использовать любой известный мне инструмент, который быстрее, чем более удобные методы tm, которые могут нам дать (и, конечно, намного быстрее, чем qdap). Здесь я даже не рассматривал параллельную обработку или data.table/dplyr, а вместо этого сосредоточился на манипуляциях со строками с помощью stringi, сохранении данных в матрице и работе с конкретными пакетами, предназначенными для обработки этого формата. Я беру ваш пример и умножаю его в 100000 раз. Даже со стеммингом на моей машине это занимает 17 секунд.

data <- data.frame(
    text=c("Let the big dogs hunt",
        "No holds barred",
        "My child is an honor student"
    ), stringsAsFactors = F)

## eliminate this step to work as a MWE
data <- data[rep(1:nrow(data), 100000), , drop=FALSE]

library(stringi)
library(SnowballC)
out <- stri_extract_all_words(stri_trans_tolower(SnowballC::wordStem(data[[1]], "english"))) #in old package versions it was named 'stri_extract_words'
names(out) <- paste0("doc", 1:length(out))

lev <- sort(unique(unlist(out)))
dat <- do.call(cbind, lapply(out, function(x, lev) {
    tabulate(factor(x, levels = lev, ordered = TRUE), nbins = length(lev))
}, lev = lev))
rownames(dat) <- sort(lev)

library(tm)
dat <- dat[!rownames(dat) %in% tm::stopwords("english"), ] 

library(slam)
dat2 <- slam::as.simple_triplet_matrix(dat)

tdm <- tm::as.TermDocumentMatrix(dat2, weighting=weightTf)
tdm

## or...
dtm <- tm::as.DocumentTermMatrix(dat2, weighting=weightTf)
dtm
person Tyler Rinker    schedule 15.08.2014
comment
Это потрясающий ответ. Я использую текст в кодировке UTF-8 (русские символы), и это поддерживает его, тогда как другой ответ, похоже, не работает (на моем компьютере с Windows). Как я могу удалить цифры и знаки препинания с этим? Я просмотрел cran.r-project.org/web/packages/stringi. /stringi.pdf, но я не уверен, как применять эти методы в этом контексте. Кроме того, строка dtm ‹- tm::as.DocumentTermMatrix(dat2, weighting=weightTf), кажется, путает термины и документы, тогда как TermDocumentMatrix правильно различает их. - person user1477388; 16.08.2014
comment
На основе вашего кода я подготовил функцию для вычисления TermDocumentMatrix, избегая создания плотная матрица, которая, как я понимаю, создается do.call(...) в вашем примере. Но работает очень медленно. У вас есть идеи, как ускорить его? - person Krzysztof Jędrzejewski; 06.04.2015

Какой подход?

data.table — это определенно правильный путь. Операции регулярных выражений медленные, хотя операции в stringi намного быстрее (помимо того, что они намного лучше). Что-нибудь с

Я прошел много итераций решения проблемы при создании quanteda::dfm() для моего пакета quanteda (см. репозиторий GitHub здесь ). На сегодняшний день самое быстрое решение включает в себя использование пакетов data.table и Matrix для индексации документов и токенизированных функций, подсчета функций в документах и ​​включения результата прямо в разреженную матрицу.

В приведенном ниже коде я взял за пример тексты, найденные с пакетом quanteda, который вы можете (и должны!) установить из CRAN или версию для разработки из

devtools::install_github("kbenoit/quanteda")

Мне было бы очень интересно посмотреть, как это работает с вашими 4-метровыми документами. Судя по моему опыту работы с корпусами такого размера, это будет работать довольно хорошо (если у вас достаточно памяти).

Обратите внимание, что во всех моих профилированиях я не мог повысить скорость операций data.table с помощью какого-либо распараллеливания из-за того, как они написаны на C++.

Ядро функции Quanteda dfm()

Вот костяк исходного кода, основанного на data.table, на случай, если кто-то захочет попробовать его улучшить. Он принимает на вход список векторов символов, представляющих токенизированные тексты. В пакете Quanteda полнофункциональный dfm() работает непосредственно с символьными векторами документов или объектами корпуса напрямую и реализует строчные буквы, удаление чисел и удаление пробелов по умолчанию (но все это можно изменить при желании).

require(data.table)
require(Matrix)

dfm_quanteda <- function(x) {
    docIndex <- 1:length(x)
    if (is.null(names(x))) 
        names(docIndex) <- factor(paste("text", 1:length(x), sep="")) else
            names(docIndex) <- names(x)

    alltokens <- data.table(docIndex = rep(docIndex, sapply(x, length)),
                            features = unlist(x, use.names = FALSE))
    alltokens <- alltokens[features != ""]  # if there are any "blank" features
    alltokens[, "n":=1L]
    alltokens <- alltokens[, by=list(docIndex,features), sum(n)]

    uniqueFeatures <- unique(alltokens$features)
    uniqueFeatures <- sort(uniqueFeatures)

    featureTable <- data.table(featureIndex = 1:length(uniqueFeatures),
                               features = uniqueFeatures)
    setkey(alltokens, features)
    setkey(featureTable, features)

    alltokens <- alltokens[featureTable, allow.cartesian = TRUE]
    alltokens[is.na(docIndex), c("docIndex", "V1") := list(1, 0)]

    sparseMatrix(i = alltokens$docIndex, 
                 j = alltokens$featureIndex, 
                 x = alltokens$V1, 
                 dimnames=list(docs=names(docIndex), features=uniqueFeatures))
}

require(quanteda)
str(inaugTexts)
## Named chr [1:57] "Fellow-Citizens of the Senate and of the House of Representatives:\n\nAmong the vicissitudes incident to life no event could ha"| __truncated__ ...
## - attr(*, "names")= chr [1:57] "1789-Washington" "1793-Washington" "1797-Adams" "1801-Jefferson" ...
tokenizedTexts <- tokenize(toLower(inaugTexts), removePunct = TRUE, removeNumbers = TRUE)
system.time(dfm_quanteda(tokenizedTexts))
##  user  system elapsed 
## 0.060   0.005   0.064 

Конечно, это всего лишь фрагмент, но полный исходный код легко найти в репозитории GitHub (dfm-main.R).

Quanteda на вашем примере

Как это для простоты?

require(quanteda)
mytext <- c("Let the big dogs hunt",
            "No holds barred",
            "My child is an honor student")
dfm(mytext, ignoredFeatures = stopwords("english"), stem = TRUE)
# Creating a dfm from a character vector ...
# ... lowercasing
# ... tokenizing
# ... indexing 3 documents
# ... shaping tokens into data.table, found 14 total tokens
# ... stemming the tokens (english)
# ... ignoring 174 feature types, discarding 5 total features (35.7%)
# ... summing tokens by document
# ... indexing 9 feature types
# ... building sparse matrix
# ... created a 3 x 9 sparse dfm
# ... complete. Elapsed time: 0.023 seconds.

# Document-feature matrix of: 3 documents, 9 features.
# 3 x 9 sparse Matrix of class "dfmSparse"
# features
# docs    bar big child dog hold honor hunt let student
# text1   0   1     0   1    0     0    1   1       0
# text2   1   0     0   0    1     0    0   0       0
# text3   0   0     1   0    0     1    0   0       1
person Ken Benoit    schedule 09.07.2015
comment
@ user1477388 Спасибо! dfm() отлично работает и с кириллическими символами. Наше решение проблем с TermDocument по сравнению с DocumentTerm было простым: документы всегда и только строки. Это то же самое, что и в любой аналитической структуре данных, где строки индексируют наблюдения или единицы, а столбцы указывают переменные или характеристики единиц. Термины или их варианты — это просто тип характеристики. - person Ken Benoit; 09.07.2015
comment
Это хорошее ускорение. Я бы посоветовал ОП переместить проверку на это решение, если все остальные равны. - person Tyler Rinker; 14.07.2015
comment
Это фантастика! Есть ли способ использовать dfm без модификаций для биграмм (или n-грамм), т. е. не отдельных слов, а комбинаций из двух слов Let the, the big, big dog, псы охотятся в вашем тексте[1]? - person HOSS_JFL; 04.10.2015
comment
Спасибо! Да, dfm() может принимать аргумент ngrams, например. dfm(mytext, ngrams = 2, concatenator = " ") для получения желаемых результатов. - person Ken Benoit; 06.10.2015
comment
Примечание к этой теме: мое исходное решение очень быстрое, но с тех пор я изменил код dfm() на еще более быстрый метод, используя match() и используя метод для построения разреженной матрицы. См. stackoverflow. com/questions/31570437/, где я обнаружил этот подход. - person Ken Benoit; 06.10.2015
comment
Как вы можете выполнять умножение матриц с разреженными матрицами, предоставляемыми пакетом Quanteda? Переместил вопрос в эту тему здесь. - person hhh; 09.01.2017

У вас есть несколько вариантов. @TylerRinker прокомментировал qdap, что, безусловно, является путем. .

В качестве альтернативы (или дополнительно) вы также можете извлечь выгоду из здорового параллелизма. Есть хорошая страница CRAN с подробным описанием ресурсов HPC в R. Однако она немного устарела, и функциональность пакета multicore теперь содержится в пакете parallel.

Вы можете масштабировать интеллектуальный анализ текста, используя многоядерные apply функции пакета parallel или кластерные вычисления (также поддерживаемые этого пакета, а также snowfall и biopara).

Еще один способ — использовать подход MapReduce. Хорошая презентация по объединению tm и MapReduce для больших данных доступна здесь. Хотя этой презентации уже несколько лет, вся информация по-прежнему актуальна, актуальна и актуальна. У тех же авторов есть более новая научная статья по теме, посвященная подключаемому модулю tm.plugin.dc. Чтобы обойти наличие векторного источника вместо DirSource, вы можете использовать принуждение:

data("crude")
as.DistributedCorpus(crude)

Если ни одно из этих решений не соответствует вашему вкусу или вы просто любите приключения, вы также можете увидеть, насколько хорошо ваш графический процессор может справиться с этой проблемой. Существует много различий в том, насколько хорошо работают графические процессоры по сравнению с центральными процессорами, и это может быть вариантом использования. Если вы хотите попробовать, вы можете использовать gputools или другие пакеты GPU, упомянутые в представлении задач CRAN HPC.

Пример:

library(tm)
install.packages("tm.plugin.dc")
library(tm.plugin.dc)

GetDCorpus <-function(textVector)
{
  doc.corpus <- as.DistributedCorpus(VCorpus(VectorSource(textVector)))
  doc.corpus <- tm_map(doc.corpus, content_transformer(tolower))
  doc.corpus <- tm_map(doc.corpus, content_transformer(removeNumbers))
  doc.corpus <- tm_map(doc.corpus, content_transformer(removePunctuation))
  # <- tm_map(doc.corpus, removeWords, stopwords("english")) # won't accept this for some reason...
  return(doc.corpus)
}

data <- data.frame(
  c("Let the big dogs hunt","No holds barred","My child is an honor student"), stringsAsFactors = F)

dcorp <- GetDCorpus(data[,1])

tdm <- TermDocumentMatrix(dcorp)

inspect(tdm)

Вывод:

> inspect(tdm)
<<TermDocumentMatrix (terms: 10, documents: 3)>>
Non-/sparse entries: 10/20
Sparsity           : 67%
Maximal term length: 7
Weighting          : term frequency (tf)

         Docs
Terms     1 2 3
  barred  0 1 0
  big     1 0 0
  child   0 0 1
  dogs    1 0 0
  holds   0 1 0
  honor   0 0 1
  hunt    1 0 0
  let     1 0 0
  student 0 0 1
  the     1 0 0
person Hack-R    schedule 15.08.2014
comment
Спасибо за ресурсы, но я действительно не могу найти примеры того, как применять пакет hive для Hadoop или пакет tm.plugic.dc для Distributed Corpus. Эти пакеты, кажется, используют DirSource, тогда как у меня есть только векторный источник. Есть ли хорошие образцы кода? - person user1477388; 15.08.2014
comment
Я знаю, что Вы имеете ввиду. Когда я следовал примерам, чтобы сделать это с моей собственной работой, мне также пришлось скорректировать свой код для этой разницы. Хотя это определенно можно сделать. Я посмотрю, смогу ли я найти хороший пример, где это делается таким образом. - person Hack-R; 15.08.2014
comment
@user1477388 user1477388 Это не совсем длинный или красивый пример, но достаточно ли приведения к data("crude"); dcrude <- as.DistributedCorpus(crude), чтобы вы могли затем использовать остальные основные примеры из Vingettes или других ресурсов? - person Hack-R; 15.08.2014
comment
@user1477388 user1477388 у вас опечатка в слове плагин. Если опечатка только в вашем комментарии, а не в коде, который вы запустили, то эта ошибка обычно устраняется для большинства пакетов путем загрузки пакета и его установки из исходного кода. - person Hack-R; 15.08.2014
comment
Спасибо, извините за это. Я получаю сообщение об ошибке > as.DistributedCorpus(data[,1]) Error in UseMethod("as.DCorpus") : no applicable method for 'as.DCorpus' applied to an object of class "character" Должен ли я преобразовать его в другой тип или что-то в этом роде? - person user1477388; 15.08.2014
comment
Попробуйте сначала превратить его в VCorpus, а затем используйте as.DCorpus - person Hack-R; 15.08.2014
comment
Вау, спасибо чувак! Это круто. Я обновил ваш ответ своим кодом. Похоже, мой аргумент стоп-слов не принимается (выдает ошибку), но похоже, что TDM автоматически удаляет стоп-слова... есть идеи? - person user1477388; 15.08.2014
comment
@ user1477388 Добро пожаловать! Рад, что это сработало. Я не уверен навскидку, мне нужно увидеть ошибку/результаты. StackOverflow предупреждает меня о количестве комментариев, поэтому, возможно, вы можете зайти в #R на Freenode или сделать еще один пост StackOverflow, и мы сможем разобраться в этом таким образом. - person Hack-R; 15.08.2014
comment
Они должны превратить комментарии в окно мини-чата и позволить комментариям +1 сохраняться или что-то в этом роде... В любом случае, я обновил ваш ответ кодом воспроизведения и выводом. Я открою новый вопрос, если не смогу понять. Еще раз спасибо, приятель. - person user1477388; 15.08.2014

Это лучше, чем мой предыдущий ответ.

Пакет quanteda претерпел значительные изменения, и теперь он быстрее и проще в использовании благодаря встроенным инструментам для решения такого рода задач — именно для этого мы его и разработали. Часть ОП спрашивала, как подготовить тексты для байесовского классификатора. Я также добавил пример для этого, так как textmodel_nb() от Quanteda справится с 300 000 документов без особых усилий, а также правильно реализует полиномиальную модель NB (которая наиболее подходит для матриц подсчета текста). -- см. также https://stackoverflow.com/a/54431055/4158274).

Здесь я демонстрирую встроенный инаугурационный объект корпуса, но приведенные ниже функции также будут работать с вводом простого вектора символов. Я использовал этот же рабочий процесс для обработки и подбора моделей для десятков миллионов твитов за считанные минуты на ноутбуке, так что это быстро.

library("quanteda", warn.conflicts = FALSE)
## Package version: 1.4.1
## Parallel computing: 2 of 12 threads used.
## See https://quanteda.io for tutorials and examples.

# use a built-in data object
data <- data_corpus_inaugural
data
## Corpus consisting of 58 documents and 3 docvars.

# here we input a corpus, but plain text input works fine too
dtm <- dfm(data, tolower = TRUE, remove_numbers = TRUE, remove_punct = TRUE) %>%
  dfm_wordstem(language = "english") %>%
  dfm_remove(stopwords("english"))

dtm
## Document-feature matrix of: 58 documents, 5,346 features (89.0% sparse).    
tail(dtm, nf = 5)
## Document-feature matrix of: 6 documents, 5 features (83.3% sparse).
## 6 x 5 sparse Matrix of class "dfm"
##               features
## docs           bleed urban sprawl windswept nebraska
##   1997-Clinton     0     0      0         0        0
##   2001-Bush        0     0      0         0        0
##   2005-Bush        0     0      0         0        0
##   2009-Obama       0     0      0         0        0
##   2013-Obama       0     0      0         0        0
##   2017-Trump       1     1      1         1        1

Это довольно тривиальный пример, но для иллюстрации давайте применим модель Наивного Байеса, протягивая документ Трампа. Это была последняя инаугурационная речь на момент публикации ("2017-Трамп"), равная по положению ndoc()-му документу.

# fit a Bayesian classifier
postwar <- ifelse(docvars(data, "Year") > 1945, "post-war", "pre-war")
textmod <- textmodel_nb(dtm[-ndoc(dtm), ], y = postwar[-ndoc(dtm)], prior = "docfreq")

Те же виды команд, которые работают с другими объектами подогнанной модели (например, lm(), glm() и т. д.), будут работать с подогнанным объектом текстовой модели наивного байесовского метода. Так:

summary(textmod)
## 
## Call:
## textmodel_nb.dfm(x = dtm[-ndoc(dtm), ], y = postwar[-ndoc(dtm)], 
##     prior = "docfreq")
## 
## Class Priors:
## (showing first 2 elements)
## post-war  pre-war 
##   0.2982   0.7018 
## 
## Estimated Feature Scores:
##          fellow-citizen  senat   hous  repres among vicissitud   incid
## post-war        0.02495 0.4701 0.2965 0.06968 0.213     0.1276 0.08514
## pre-war         0.97505 0.5299 0.7035 0.93032 0.787     0.8724 0.91486
##            life  event   fill greater anxieti  notif transmit  order
## post-war 0.3941 0.1587 0.3945  0.3625  0.1201 0.3385   0.1021 0.1864
## pre-war  0.6059 0.8413 0.6055  0.6375  0.8799 0.6615   0.8979 0.8136
##          receiv   14th    day present  month    one  hand summon countri
## post-war 0.1317 0.3385 0.5107 0.06946 0.4603 0.3242 0.307 0.6524  0.1891
## pre-war  0.8683 0.6615 0.4893 0.93054 0.5397 0.6758 0.693 0.3476  0.8109
##           whose  voic    can  never   hear  vener
## post-war 0.2097 0.482 0.3464 0.2767 0.6418 0.1021
## pre-war  0.7903 0.518 0.6536 0.7233 0.3582 0.8979

predict(textmod, newdata = dtm[ndoc(dtm), ])
## 2017-Trump 
##   post-war 
## Levels: post-war pre-war

predict(textmod, newdata = dtm[ndoc(dtm), ], type = "probability")
##            post-war       pre-war
## 2017-Trump        1 1.828083e-157
person Ken Benoit    schedule 27.02.2019