Более эффективные средства для создания корпуса и DTM с 4M строк
В моем файле более 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
пакет, но выдает ошибку при попытке загрузить пакет, плюс я даже не знаю, будет ли он работать.
3 ответа
Я думаю, что вы можете рассмотреть более ориентированное на регулярные выражения решение. Вот некоторые из проблем / мышления, с которыми я борюсь как разработчик. Я сейчас смотрю на 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
Какой подход?
data.table
безусловно, правильный путь. Операции с регулярным выражением выполняются медленно, хотя stringi
намного быстрее (в дополнение к тому, чтобы быть намного лучше). Что-нибудь с
Я прошел много итераций решения проблемы в создании quanteda::dfm()
для моего пакета Quanteda (см. репозиторий GitHub здесь). Самое быстрое решение, безусловно, предполагает использование data.table
а также Matrix
пакеты для индексации документов и токенизированных функций, подсчета функций в документах и вставки результатов прямо в разреженную матрицу.
В приведенном ниже коде я взял в качестве примера тексты, найденные в пакете quanteda, который вы можете (и должны!) Установить из CRAN или версию для разработчиков из
devtools::install_github("kbenoit/quanteda")
Мне было бы очень интересно посмотреть, как это работает с вашими документами на 4 млн. Основываясь на моем опыте работы с корпусами такого размера, он будет работать довольно хорошо (если у вас достаточно памяти).
Обратите внимание, что во всем своем профилировании я не мог улучшить скорость операций data.table с помощью какого-либо распараллеливания из-за того, как они написаны на C++.
Ядро квантовой 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
У вас есть несколько вариантов. @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 Task View.
Пример:
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
Это лучше, чем мой предыдущий ответ.
Пакет quanteda значительно эволюционировал, и теперь он стал быстрее и намного проще в использовании благодаря встроенным инструментам для решения подобных задач - именно для этого мы его и разработали. Часть ОП спрашивала, как подготовить тексты для байесовского классификатора. Я добавил пример для этого тоже, так как Quantedatextmodel_nb()
будет обрабатывать 300 тыс. документов, не нарушая пот, плюс он правильно реализует многочленную модель NB (которая является наиболее подходящей для матриц подсчета текста - см. также /questions/31961152/naivnyij-bajes-v-quanteda-vs-caret-sovershenno-raznyie-rezultatyi/31961167#31961167).
Здесь я демонстрирую встроенный вводный объект корпуса, но функции ниже также будут работать с векторным вводом простого символа. Я использовал этот же рабочий процесс для обработки и подгонки моделей к 10 миллионам твитов в считанные минуты на ноутбуке, поэтому это быстро.
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