R: найти и подсчитать все различия по положению (одного добавленного, вычтенного или замененного элемента) между векторами символов, вложенными в список.

У меня есть список векторов символов, представляющих слова, разделенные на фонемы:

> head(words)
[[1]]
[1] "UU"

[[2]]
[1] "EY" "Z" 

[[3]]
[1] "T"  "R"  "IH" "P"  "UU" "L"  "EY"

[[4]]
[1] "AA" "B"  "ER" "G" 

[[5]]
[1] "AA" "K"  "UU" "N" 

[[6]]
[1] "AA" "K"  "ER"

Для каждого слова в списке хотелось бы найти количество слов, отличающихся от рассматриваемого слова на одну фонему (одна фонема добавлена, вычтена или заменена) и имеющих одинаковое количество фонем на тех же позициях. В этом смысле для слова "EY" "Z" допустимы следующие случаи:

[1] "M"  "EY" "Z" 

[1] "AY" "Z"

[1] "EY" "D" 

[1] "EY" "Z" "AH"

Но следует отклонить следующие случаи:

[1] "EY" "D"  "Z"

[1] "Z" "EY" "D"

[1] "HH" "EY"

По сути, я хотел бы найти различия одного элемента в отношении положения фонем в векторах. На данный момент лучшее решение, которое я нашел, это:

diffs <- c()
for (i in seq_along(words)) {
  diffs <- c(diffs, sum(sapply(words, function(y) {
    count <- 0
    elements <- list(words[[i]], y)
    len <- c(length(words[[i]]), length(y))
    if (identical(elements[which(len==max(len))][[1]][-1], elements[which(len==min(len))][[1]]) == 1) {
      count + identical(elements[which(len==max(len))][[1]][-1], elements[which(len==min(len))][[1]])
    } else {
      length(elements[which(len==min(len))][[1]]) <- length(elements[which(len==max(len))][[1]])
      elements <- rapply(elements, f=function(x) ifelse(is.na(x),"$$",x), how="replace" )
      count + sum(elements[[1]] != elements[[2]])
    }
  })== 1))
}

Однако это решение требует времени, потому что мой список words содержит 120 000 элементов (слов/векторов), поэтому я хотел бы спросить, знаете ли вы другие решения для ускорения процесса.

Заранее большое спасибо за ваши ответы


person Francesco Cabiddu    schedule 01.09.2017    source источник
comment
Сколько существует фонем?   -  person F. Privé    schedule 01.09.2017
comment
Привет, это все возможные фонемы (сорок): УУ ЭЙ З Т Р И Х П Л А Б Е Р Г К Н С Э Х Т М АО Д В И Й А Е ОУ НГ Ш ХХ АВ УВ А А Ф АЙ ДЖ Х Ы Ч В Ж У Х Д Х ОЙ   -  person Francesco Cabiddu    schedule 02.09.2017
comment
А в случае полного равенства вы считаете это 0 (а не 1)?   -  person F. Privé    schedule 03.09.2017
comment
Почему [1] "EY" "D" "Z" не является приемлемым случаем? Означает ли это, что вы можете добавить только в начале или в конце? Это то же самое для удаления?   -  person F. Privé    schedule 03.09.2017
comment
Привет Ф.! Большое спасибо за Вашу помощь. Равенство будет считаться равным 0. Что касается случая [1] EY D Z, Кристоф заметил, что мой скрипт рассматривает случаи добавления или удаления только в начале и в конце строки. Для работы, которую я делаю, в данный момент я не совсем уверен, какое правило я должен использовать, поэтому я любезно прошу вас, не могли бы вы предложить мне оба решения, одно похожее на мое, а другое рассматривает добавление и удаление в любая возможная позиция строки, как это делает сценарий Кристофа   -  person Francesco Cabiddu    schedule 03.09.2017


Ответы (3)


И другой ответ, использующий обычное расстояние Левенштейна (т.е. разрешающее вставку в любой точке), но на этот раз БЫСТРО - 1000 слов за 15 секунд быстро.

Хитрость заключается в использовании одной из быстрых реализаций Левенштейна, доступных в пакетах R; в этом случае я использую stringdist, но все должно работать. Проблема в том, что они работают со строками и символами, а не с многосимвольными представлениями фонем. Но для этого есть тривиальное решение: поскольку символов больше, чем фонем, мы можем просто перевести фонемы в отдельные символы. Результирующие строки не читаются как фонематическая транскрипция, но прекрасно работают в качестве входных данных для алгоритма плотности соседства.

library(stringdist)

phonemes <- unique(unlist(words))

# add a few buffer characters
targets <- c(letters, LETTERS, 0:9, "!", "§", "%", "&", "/", "=", 
             "#")[1:length(phonemes)]

ptmap <- targets
names(ptmap) <- phonemes

wordsT <- sapply(words, function(i) paste0(ptmap[i], collapse=""))

wordlengths <- nchar(wordsT)

onediffs.M <- function(x) {
  lengthdiff <-  abs(wordlengths - nchar(x))
  sum(stringdist(x, wordsT[lengthdiff == 0], method="hamming") == 1) +
    sum(stringdist(x, wordsT[lengthdiff == 1], method="lv") == 1)
}
person Christoph Wolk    schedule 03.09.2017
comment
Удивительно! Большое спасибо. Прочитав ваш первый ответ, я также подумал преобразовать фонемы в уникальные символы, чтобы затем использовать adist(). В конце я попробовал это решение: neigh_lev <- unlist(lapply(words_converted, function(y) { sum(unlist(lapply(words_converted, function(x) { adist(y, x)[1,1] }) == 1)) })) Но это было не так быстро, потому что я не думал сбрасывать со счетов случаи, когда абсолютная разница в длине между словами больше 1. - person Francesco Cabiddu; 04.09.2017
comment
Хорошая мысль. Но подвыбор по длине — это только половина дела. Другая проблема заключается в том, что вы вызываете adist с помощью одной пары слов; это означает, что R должен постоянно переключаться между относительно медленным кодом R и быстрым собственным кодом. И adist, и stringdist векторизованы, поэтому они могут обрабатывать векторы в качестве входных данных. Это гораздо более простой код и примерно в десять раз быстрее вашей версии: unlist(lapply(wordsT[1:10], function(y) { sum(adist(y, wordsT) == 1)})). Версию в ответе, вероятно, можно было бы сделать примерно в два раза быстрее, если бы не вычислять все дважды. - person Christoph Wolk; 05.09.2017
comment
Это ценные пояснения для меня. Еще раз спасибо Кристоф - person Francesco Cabiddu; 05.09.2017

Таким образом, ключевым здесь является разделение слов по их длине, чтобы мы могли проверить каждое предположение (замену/добавление/удаление) только на интересующем подмножестве.

get_one_diff <- function(words) {

  K <- max(le <- lengths(words))
  i_chr <- as.character(seq_len(K))
  words.spl <- split(words, le)

  test_substitution <- function(i) {
    word1 <- words[[i]]
    do.call(sum, lapply(words.spl[[i_chr[le[i]]]], function(word2) {
      sum(word1 != word2) == 1
    }))
  }

  test_addition <- function(i) {
    if ((le <- le[i]) == K) return(0)
    word1 <- words[[i]]
    do.call(sum, lapply(words.spl[[i_chr[le + 1]]], function(word2) {
      isOneDiff(word1, word2)
    }))
  }

  test_deletion <- function(i) {
    if ((le <- le[i]) == 1) return(0)
    word1 <- words[[i]]
    do.call(sum, lapply(words.spl[[i_chr[le - 1]]], function(word2) {
      isOneDiff(word2, word1)
    }))
  }

  sapply(seq_along(words), function(i) {
    test_substitution(i) + test_addition(i) + test_deletion(i)
  })
}

где isOneDiff — функция Rcpp:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
bool isOneDiff(const StringVector& w1,
               const StringVector& w2) {

  int i, n = w1.size();

  for (i = 0; i < n; i++) if (w1[i] != w2[i]) break;
  for (     ; i < n; i++) if (w1[i] != w2[i+1]) return false;

  return true;
}

Это в 20 раз быстрее, чем ваша версия, и, поскольку это просто sapply, ее можно легко распараллелить.

person F. Privé    schedule 02.09.2017
comment
Привет Ф.! Большое спасибо. Производительность вашего решения мне очень поможет! Однако, когда я пытаюсь запустить его, возникает ошибка: Ошибка в words.spl[[i_chr[le - 1]]]: попытка выбрать менее одного элемента в get1index. Не могли бы вы оказать мне дополнительную помощь, предложив, как решить проблему? - person Francesco Cabiddu; 03.09.2017
comment
Кроме того, как я уже говорил выше, Кристоф заметил, что мой скрипт рассматривает случаи добавления или удаления только в начале и в конце строки. Для работы, которую я делаю, в данный момент я не совсем уверен, какое правило я должен использовать, поэтому я любезно прошу вас, не могли бы вы предложить мне оба решения, одно похожее на мое, а другое рассматривает добавление и удаление в любая возможная позиция строки, как это делает сценарий Кристофа - person Francesco Cabiddu; 03.09.2017

Вот версия, использующая расстояние Левенштейна с алгоритмом Вагнера-Фишера.

vecLeven <- function(s, t) {
  d <- matrix(0, nrow = length(s) + 1, ncol=length(t) + 1)
  d[, 1] <- (1:nrow(d)) - 1
  d[1,] <- (1:ncol(d))-1
  for (i in 1:length(s))  {
    for (j in 1:length(t)) {
      d[i+1, j+1] <- min(
        d[i, j+1] + 1, # deletion
        d[i+1, j] + 1, # insertion
        d[i, j] + if (s[i] == t[j]) 0 else 1 # substitution
      )
    }
  }

  d[nrow(d), ncol(d)]
}


onediff <- sapply(words[1:10], function(x) {
  lengthdiff <- sapply(words, function(word) abs(length(word) - length(x)))
  sum(sapply(words[lengthdiff == 0], function(word) sum(word != x) == 1)) +
        sum(mapply(vecLeven, list(x), words[lengthdiff == 1]) == 1)
})

Я проверил обе версии на словаре CMU, который имеет аналогичный размер. Это немного быстрее вашей версии (около 30 секунд вместо 50 для 10 слов) и должно хорошо распараллеливаться. Тем не менее, запуск его на полном наборе данных занял бы несколько дней.

Одним из важных факторов производительности является то, что все пары вычисляются дважды, один раз для первого слова и один раз для второго; выполнение поиска вместо этого уменьшит это вдвое. Однако существует более 7 миллиардов пар, поэтому вам потребуется база данных для их хранения.

person Christoph Wolk    schedule 01.09.2017
comment
Привет Кристоф! Большое спасибо за ваш ответ. Этот алгоритм очень интересен. На самом деле я делаю этот расчет, используя словарь CMU. Я хотел бы иметь значение плотности окрестности (расчет, который я объяснил выше) для каждого слова в словаре. К сожалению, я заметил, что ваш скрипт дает мне разные значения. Как вы думаете, есть ли способ узнать, какие слова ваш сценарий оценивает как соседние, например, для первого слова? Просто чтобы иметь возможность сравнить, что делают две функции. - person Francesco Cabiddu; 02.09.2017
comment
Привет, Франческо, да, просто получите оба алгоритма для вывода всех соседей для одного слова и сравните их. Конечно, ваш алгоритм, например. судов не находит. Я думаю, что обнаружение падежей с неравной длиной неправильно, и вы рассматриваете только удаление первой фонемы самого длинного слова вместо всех и взятие более короткого. Но я могу неправильно понять алгоритм. - person Christoph Wolk; 03.09.2017
comment
Ах, вы исключаете случаи, когда вставки в середине в вашем примере. Это значительно упрощает задачу. Однако это не стандартное определение плотности соседства, не так ли? - person Christoph Wolk; 03.09.2017
comment
Привет Кристоф! благодарю вас. Я только начал работать с фонологическим соседством, поэтому, вероятно, я не очень хорошо понял определение, как вы сказали. Я знаю, что это место предназначено только для тем программирования, но не могли бы вы предоставить мне ссылку, где я могу прочитать четкое определение плотности окрестности, чтобы я мог быть уверен, что необходимо также включать вставки в середине слова и отредактировать мой исходный вопрос здесь? А также, знаете ли вы, существует ли где-то в Интернете плотность соседства каждого слова CMU? В любом случае большое спасибо за помощь - person Francesco Cabiddu; 03.09.2017
comment
Извините, я не знаю, существует ли общедоступный список расстояний CMUdict. У меня также нет под рукой канонической ссылки, Баайен иногда использует ее в своем учебнике (2009, Анализ лингвистических данных, CUP), так что вы, вероятно, можете просмотреть оригинальные статьи и увидеть его определение. См. также здесь: corpustools.readthedocs.io/en/latest/neighborhood_density.html Но то, что я помню, читал, по сути, просто слова, которые отличаются одной фонемой/графемой и т. д., поэтому я не вижу ясной причины их опускать. - person Christoph Wolk; 03.09.2017
comment
Большое спасибо за вашу помощь, Кристоф, я изучил это и определенно собираюсь использовать расстояние редактирования Левенштейна, которое кажется лучшим распространенным способом расчета плотности соседства. И глядя на разные документы и читая определение, нет никаких оснований исключать те случаи, которые вы отметили. - person Francesco Cabiddu; 04.09.2017