Контекст
Я пытался реализовать алгоритм, недавно предложенный в этой статье. Учитывая большой объем текста (корпус), алгоритм должен возвращать характерные n-граммы (т.е. Последовательность из n слов) корпуса. Пользователь может решить соответствующее n, и в данный момент я пытаюсь с n = 2-6, как в оригинальной бумаге. Другими словами, используя алгоритм, я хочу извлечь от 2 до 6 грамм, которые характеризуют корпус.
Я смог реализовать часть, которая вычисляет оценку, на основе которой идентифицируются определенные n-граммы, но пыталась устранить нехарактерные.
Данные
У меня есть список под названием token.df
, который содержит пять фреймов данных, включая все n-граммы, которые появляются в корпусе. Каждый кадр данных соответствует каждому n в n-граммах. Например, token.df[[2]]
включает все биграммы (2 грамма) и их баллы (называемые ми ниже) в алфавитном порядке.
> head(token.df[[2]])
w1 w2 mi
_ eos 17.219346
_ global 7.141789
_ what 8.590394
0 0 2.076421
0 00 5.732846
0 000 3.426785
Здесь bigram 0 0 (хотя они и не совсем такие слова) имеет оценку 2.076421. Поскольку кадры данных включают все n-граммы, которые появляются в корпусе, каждый из них имеет более одного миллиона строк.
> sapply(token.df, nrow)
[[1]]
NULL
[[2]]
[1] 1006059 # number of unique bigrams in the corpus
[[3]]
[1] 2684027 # number of unique trigrams in the corpus
[[4]]
[1] 3635026 # number of unique 4-grams in the corpus
[[5]]
[1] 3965120 # number of unique 5-grams in the corpus
[[6]]
[1] 4055048 # number of unique 6-grams in the corpus
Task
Я хочу определить, какие n-граммы сохранить и какие из них отбрасывать. Для этой цели алгоритм делает следующее.
- биграммы
- В нем сохраняются битрамы, чьи баллы выше, чем у триграмм, чьи первые два слова совпадают с битрамами.
- 3-5 грамм
- Для каждого n-грамма, где n = {3, 4, 5}, он смотрит на
- n-1 грамм, которые соответствуют первым n-1 словам n-грамма и
- n + 1 грамм, чьи первые n слов соответствуют n-грамм.
- Алгоритм сохраняет n-грамм только в том случае, если его оценка выше, чем оценки n-1 грамма и n + 1 грамм, указанных выше.
- Для каждого n-грамма, где n = {3, 4, 5}, он смотрит на
- 6-граммы
- Он сохраняет 6 грамм, чьи баллы выше, чем у 5-граммов, которые соответствуют первым пяти словам из 6 граммов.
Пример
> token.df[[2]][15, ]
w1 w2 mi
0 001 10.56292
> token.df[[3]][33:38, ]
w1 w2 w3 mi
0 001 also 3.223091
0 001 although 5.288097
0 001 and 2.295903
0 001 but 4.331710
0 001 compared 6.270625
0 001 dog 11.002312
> token.df[[4]][46:48, ]
w1 w2 w3 w4 mi
0 001 compared to 5.527626
0 001 dog walkers 10.916028
0 001 environmental concern 10.371769
Здесь bigram 0 001 не сохраняется, потому что одна из триграмм, чьи первые два слова совпадают с bigram (собака 0 001), имеет более высокий балл, чем bigram (11.002312 > 10.56292). Собака триграммы 0 001 сохраняется, потому что ее оценка (11.002312) выше, чем у биграма, которая соответствует первым двум словам триграммы (0 001; score = 10.56292), а ее значение - 4 грамма, первые три слова которого соответствуют триграмма (0 001 собачьего ходока, оценка = 10.916028).
Проблема и неудачные попытки
То, что я хотел бы знать, - это эффективный способ достижения вышеуказанного. Чтобы определить, какие биграммы для сохранения, например, мне нужно выяснить для каждой строки token.df[[2]]
, строки которой в token.df[[3]]
имеют первые два слова, идентичные значению bigram. Однако, так как число строк велико, моя итерация под ним занимает слишком много времени для запуска. Они фокусируются на случае с битрамами, потому что задача выглядела проще, чем в случае с 3-5 граммами.
-
Подход цикла
for
.
Так как приведенный ниже код просматривает все строкиtoken.df[[3]]
на каждой итерации, по оценкам, требуется несколько месяцев. Хотя это немного лучше, аналогично случаюby()
.# for loop retain <- numeric(nrow(token.df[[2]])) for (i in 1:nrow(token.df[[2]])) { mis <- token.df[[3]]$mi[token.df[[2]][i, ]$w1 == token.df[[3]][ , 1] & token.df[[2]][i, ]$w2 == token.df[[3]][ , 2]] retain[i] <- ifelse(token.df[[2]]$mi[i] > max(mis), TRUE, FALSE) } # by mis <- by(token.df[[2]], 1:nrow(token.df[[2]]), function(x) token.df[[3]]$mi[x$w1 == token.df[[3]]$w1 & x$w2 == token.df[[3]]$w2]) retain <- sapply(seq(mis), function(i) token.df[[2]]$mi[i] > max(mis[[i]]))
-
Подход указателя.
Проблема с вышеизложенным заключается в большом количестве итераций над (вертикально) длинным фреймом данных. Чтобы облегчить проблему, я подумал, что могу использовать тот факт, что n-граммы сортируются по алфавиту в каждом кадре данных и используют какой-то указатель, указывающий, в какой строке начать искать. Однако этот подход слишком длительный для запуска (по крайней мере, несколько дней).retain <- numeric(nrow(token.df[[2]])) nrow <- nrow(token.df[[3]]) # number of rows of the trigram data frame pos <- 1 # pointer for (i in seq(nrow(token.df[[2]]))) { j <- 1 target.rows <- numeric(10) while (TRUE) { if (pos == nrow + 1 || !all(token.df[[2]][i, 1:2] == token.df[[3]][pos, 1:2])) break target.rows[j] <- pos pos <- pos + 1 if (j %% 10 == 0) target.rows <- c(target.rows, numeric(10)) j <- j + 1 } target.rows <- target.rows[target.rows != 0] retain[i] <- ifelse(token.df[[2]]$mi[i] > max(token.df[[3]]$mi[target.rows]), TRUE, FALSE) }
Есть ли способ выполнить эту задачу за разумные промежутки времени (например, за одну ночь)? Теперь, когда итерационные подходы были напрасны, мне интересно, возможна ли какая-либо векторизация. Но я открыт для любых способов ускорить процесс.
Данные имеют древовидную структуру в том, что один биграм делится на одну или несколько триграмм, каждая из которых, в свою очередь, делится на один или более 4-граммов и т.д. Я не уверен, как лучше обрабатывать такие данные.
Воспроизводимый пример
Я подумал о том, чтобы раскрыть часть реальных данных, которые я использую, но сокращение данных разрушает всю суть проблемы. Я предполагаю, что люди не хотят загружать весь набор данных 250 МБ только для этого, и я не имею права на его загрузку. Ниже представлен случайный набор данных, который все еще меньше, чем я использую, но помогает справиться с этой проблемой. С помощью приведенного выше кода (подход указателя) он занимает мой компьютер 4-5 секунд для обработки первых 100 строк token.df[[2]]
ниже, и он, по-видимому, занимает 12 часов, чтобы обработать все битрамы.
token.df <- list()
types <- combn(LETTERS, 4, paste, collapse = "")
set.seed(1)
data <- data.frame(matrix(sample(types, 6 * 1E6, replace = TRUE), ncol = 6), stringsAsFactors = FALSE)
colnames(data) <- paste0("w", 1:6)
data <- data[order(data$w1, data$w2, data$w3, data$w4, data$w5, data$w6), ]
set.seed(1)
for (n in 2:6) token.df[[n]] <- cbind(data[ , 1:n], mi = runif(1E6))
Любые идеи по ускорению кода высоко ценятся.