В R расстояние между двумя предложениями: сравнение на уровне слов по минимальному расстоянию редактирования - PullRequest
0 голосов
/ 12 февраля 2019

Пытаясь выучить R, я хочу реализовать алгоритм, приведенный ниже в R. Рассмотрим два списка ниже:

List 1: "crashed", "red", "car"
List 2: "crashed", "blue", "bus"

Я хочу выяснить, сколько действий потребуется для преобразования 'list1'в «list2».Как видите, мне нужно только два действия: 1. Replace "red" with "blue". 2. Replace "car" with "bus".

Но как мы можем автоматически определить количество таких действий?У нас может быть несколько действий для преобразования предложений: ДОБАВИТЬ, УДАЛИТЬ или ЗАМЕНИТЬ слова в списке.Теперь я постараюсь объяснить, как должен работать алгоритм:

На первом шаге: я создам такую ​​таблицу:

строк: i = 0,1,2,3, столбцы: j = 0,1,2,3

(example: value[0,0] = 0 , value[0, 1] = 1 ...)

                 crashed    red     car
         0          1        2       3

crashed  1
blue     2
bus      3

Теперь я попытаюсь заполнить таблицу.Обратите внимание, что в каждой ячейке таблицы указано количество действий, которые необходимо выполнить для переформатирования предложения (ДОБАВИТЬ, удалить или заменить).Рассмотрим взаимодействие между «разбился» и «разбился» (value[1,1]), очевидно, нам не нужно его менять, поэтому значение будет равно 0. Так как ониодни и те же слова.По сути, мы получили диагональное значение = value[0,0]

                 crashed    red     car
         0          1        2       3

crashed  1          0
blue     2
bus      3

Теперь рассмотрим «разбился» и вторую часть предложения, которая «красная».Поскольку это не одно и то же слово, мы можем рассчитать количество изменений следующим образом:

min{value[0,1] , value[0,2] and value[1,1]} + 1 
min{ 1, 2, 0} + 1 = 1 

Поэтому нам нужно просто удалить «красный».Таким образом, таблица будет выглядеть так:

                 crashed    red     car
         0          1        2       3

crashed  1          0        1
blue     2  
bus      3

И мы продолжим так: «разбился» и «машина» будет:

min{value[0,3], value[0,2] and value[1,2]} + 1 
min{3, 2, 1} +1 = 2

и таблица будет:

                 crashed    red     car
         0          1        2       3

crashed  1          0        1       2
blue     2  
bus      3

И мы продолжим это делать.окончательный результат будет:

             crashed    red     car
         0      1        2       3

crashed  1      0        1       2
blue     2      1        1       2
bus      3      2        2       2 

Как видно из последнего числа в таблице, показано расстояние между двумя предложениями: значение [3,3] = 2

По сути, алгоритм должен выглядеть следующим образом:

 if (characters_in_header_of_matrix[i]==characters_in_column_of_matrix [j] & 
                                            value[i,j] == value[i+1][j-1] )

then {get the 'DIAGONAL VALUE' #diagonal value= value[i, j-1]}

else{
value[i,j] = min(value[i-1, j], value[i-1, j-1],  value[i, j-1]) + 1
 }
  endif

для нахождения разницы между элементами двух списков, которые вы видите в заголовке и столбце матрицы, я использовал strcmp() функция, которая даст нам логическое значение (ИСТИНА или ЛОЖЬ) при сравнении слов.Но я не могу реализовать это.Буду признателен за помощь, спасибо.

Ответы [ 2 ]

0 голосов
/ 12 февраля 2019

В R уже есть функция редактирования расстояния, которой мы можем воспользоваться: adist().

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

s1 <- c("crashed", "red", "car")
s2 <- c("crashed", "blue", "bus")

ll <- list(s1, s2)

alnum <- c(letters, LETTERS, 0:9)

ll2 <- relist(alnum[factor(unlist(ll))], ll)

ll2 <- sapply(ll2, paste, collapse="")

adist(ll2)
#      [,1] [,2]
# [1,]    0    2
# [2,]    2    0

Основным ограничением здесь, насколько я могу судить, является количество доступных уникальных символов, которое в данном случае составляет 62, но может быть довольно легко расширено в зависимости от вашей локали.Например: intToUtf8(c(32:126, 161:300), TRUE).

0 голосов
/ 12 февраля 2019

Вопрос

После некоторых разъяснений в предыдущем посте и после обновления поста я понимаю, что Ноль спрашивает: «Как можно итеративно подсчитать количество различий в словах в двух строках»,

Мне неизвестно о какой-либо реализации в R, хотя я был бы удивлен, если бы я еще не существовал.Я потратил немного времени на создание простой реализации, слегка изменив алгоритм для простоты (для всех, кто не заинтересован, прокрутите вниз до 2 реализаций, 1 в чистом R, один из которых использует наименьшее количество Rcpp).Общая идея реализации:

  1. Инициализация с string_1 и string_2 длины n_1 и n_2
  2. Вычисление совокупной разности между первым min(n_1, n_2)elements,
  3. Используйте эту кумулятивную разницу в качестве диагонали в матрице
  4. Установите первый недиагональный элемент на самый первый элемент + 1
  5. Рассчитайте оставшиеся недиагональные элементыas: diag(i) - diag(i-1) + full_matrix(i-1,j)
  6. На предыдущем шаге я перебираю диагонали, j перебирает строки / столбцы (любой из них работает), и мы начинаем с третьей диагонали, поскольку первая матрица 2x2 заполняется на шаге 14
  7. Рассчитайте оставшиеся abs(n_1 - n_2) элементы как full_matrix[,min(n_1 - n_2)] + 1:abs(n_1 - n_2), применяя последние к каждому значению в предыдущем, и связывайте их соответствующим образом с full_matrix.

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

Реализация в R

Dist_between_strings <- function(x, y, 
                                 split = " ", 
                                 split_x = split, split_y = split, 
                                 case_sensitive = TRUE){
  #Safety checks
  if(!is.character(x) || !is.character(y) || 
     nchar(x) == 0 || nchar(y) == 0)
    stop("x, y needs to be none empty character strings.")
  if(length(x) != 1 || length(y) != 1)
    stop("Currency the function is not vectorized, please provide the strings individually or use lapply.")
  if(!is.logical(case_sensitive))
    stop("case_sensitivity needs to be logical")
  #Extract variable names of our variables
  # used for the dimension names later on
  x_name <- deparse(substitute(x))
  y_name <- deparse(substitute(y))
  #Expression which when evaluated will name our output
  dimname_expression <- 
    parse(text = paste0("dimnames(output) <- list(",make.names(x_name, unique = TRUE)," = x_names,",
                        make.names(y_name, unique = TRUE)," = y_names)"))
  #split the strings into words
  x_names <- str_split(x, split_x, simplify = TRUE)
  y_names <- str_split(y, split_y, simplify = TRUE)
  #are we case_sensitive?
  if(isTRUE(case_sensitive)){
    x_split <- str_split(tolower(x), split_x, simplify = TRUE)
    y_split <- str_split(tolower(y), split_y, simplify = TRUE)
  }else{
    x_split <- x_names
    y_split <- y_names
  }
  #Create an index in case the two are of different length
  idx <- seq(1, (n_min <- min((nx <- length(x_split)),
                              (ny <- length(y_split)))))
  n_max <- max(nx, ny)
  #If we have one string that has length 1, the output is simplified
  if(n_min == 1){ 
    distances <- seq(1, n_max) - (x_split[idx] == y_split[idx])
    output <- matrix(distances, nrow = nx)
    eval(dimname_expression)
    return(output)
  }
  #If not we will have to do a bit of work
  output <- diag(cumsum(ifelse(x_split[idx] == y_split[idx], 0, 1)))
  #The loop will fill in the off_diagonal
  output[2, 1] <- output[1, 2] <- output[1, 1] + 1 
  if(n_max > 2)
    for(i in 3:n_min){
      for(j in 1:(i - 1)){
        output[i,j] <- output[j,i] <- output[i,i] - output[i - 1, i - 1] + #are the words different?
          output[i - 1, j] #How many words were different before?
      }
    }
  #comparison if the list is not of the same size
  if(nx != ny){
    #Add the remaining words to the side that does not contain this
    additional_words <- seq(1, n_max - n_min)
    additional_words <- sapply(additional_words, function(x) x + output[,n_min])
    #merge the additional words
    if(nx > ny)
      output <- rbind(output, t(additional_words))
    else
      output <- cbind(output, additional_words)
  }
  #set the dimension names, 
  # I would like the original variable names to be displayed, as such i create an expression and evaluate it
  eval(dimname_expression)
  output
}

Обратите внимание, что реализация не векторизована и как таковая может принимать только одностроковые входы!

Тестированиереализация

Чтобы проверить реализацию, можно использовать данные строки.Поскольку они, как говорили, содержались в списках, мы должны будем преобразовать их в строки.Обратите внимание, что функция позволяет разделить каждую строку по-разному, однако она предполагает разделенные пробелами строки.Итак, сначала я покажу, как можно добиться преобразования в правильный формат:

list_1 <- list("crashed","red","car")
list_2 <- list("crashed","blue","bus")
string_1 <- paste(list_1,collapse = " ")
string_2 <- paste(list_2,collapse = " ")
Dist_between_strings(string_1, string_2)

output

#Strings in the given example
         string_2
string_1  crashed blue bus
  crashed       0    1   2
  red           1    1   2
  car           2    2   2

Это не совсем вывод, ноэто дает ту же информацию, так как слова упорядочены так, как они были указаны в строке. Больше примеров Теперь я заявил, что это работает и для других строк, и это действительно так, поэтому давайте попробуем несколько случайных пользовательских строк:

#More complicated strings
string_3 <- "I am not a blue whale"
string_4 <- "I am a cat"
string_5 <- "I am a beautiful flower power girl with monster wings"
string_6 <- "Hello"
Dist_between_strings(string_3, string_4, case_sensitive = TRUE)
Dist_between_strings(string_3, string_5, case_sensitive = TRUE)
Dist_between_strings(string_4, string_5, case_sensitive = TRUE)
Dist_between_strings(string_6, string_5)

Запуск этих шоу показывает, что они делаютдать правильные ответы.Обратите внимание, что если какая-либо строка имеет размер 1, сравнение выполняется намного быстрее.

Сравнительный анализ реализации

Теперь, когда реализация принята, как правильная, мы хотели бы знать, насколько хорошо онавыполняет (Для незаинтересованного читателя можно прокрутить этот раздел, где дается более быстрая реализация).Для этой цели я буду использовать гораздо большие строки.Для полного теста я должен протестировать различные размеры строк, но для целей я буду использовать только 2 довольно большие строки размером 1000 и 2500. Для этой цели я использую пакет microbenchmark в R, который содержит функцию microbenchmark,который претендует на точность до наносекунд.Сама функция выполняет код 100 (или определенный пользователем) количество раз, возвращая среднее значение и квартили времени выполнения.Из-за других частей R, таких как уборщик мусора, медиана в основном считается хорошей оценкой фактического среднего времени выполнения функции.Выполнение и результаты показаны ниже:

#Benchmarks for larger strings
set.seed(1)
string_7 <- paste(sample(LETTERS,1000,replace = TRUE), collapse = " ")
string_8 <- paste(sample(LETTERS,2500,replace = TRUE), collapse = " ")
microbenchmark::microbenchmark(String_Comparison = Dist_between_strings(string_7, string_8, case_sensitive = FALSE))
# Unit: milliseconds
# expr                   min      lq      mean   median       uq      max neval
# String_Comparison 716.5703 729.4458 816.1161 763.5452 888.1231 1106.959   100

Профилирование

Теперь я считаю, что время выполнения очень медленное.Одним из вариантов использования для реализации может быть начальная проверка сдачи студентами для проверки на плагиат, и в этом случае низкий процент различий, скорее всего, показывает плагиат.Они могут быть очень длинными, и могут быть сотни хендинов, поэтому я хотел бы, чтобы пробег был очень быстрым.Чтобы выяснить, как улучшить мою реализацию, я использовал пакет profvis с соответствующей функцией profvis.Чтобы профилировать функцию, я экспортировал ее в другой R-скрипт, который я поставил, , запустив код 1 один раз перед профилированием, чтобы скомпилировать код и избежать шума профилирования (важно).Код для запуска профилирования можно увидеть ниже, а самая важная часть вывода визуализируется на изображении под ним.

library(profvis)
profvis(Dist_between_strings(string_7, string_8, case_sensitive = FALSE))

Profiling of the string differences

Теперь, несмотря на цвет, здесь я вижу явную проблему.Цикл, заполняющий недиагональ, безусловно, отвечает за большую часть времени выполнения.R (как и Python и другие не скомпилированные языки) петли заведомо медленные.

Использование Rcpp для повышения производительности

Чтобы улучшить реализацию, мы могли бы реализовать цикл в c ++ с использованием пакета Rcpp.Это довольно просто.Код мало чем отличается от того, который мы использовали бы в R, если бы мы избегали итераторов.Сценарий c ++ может быть сделан в файле -> новый файл -> файл c ++.Следующий код C ++ будет вставлен в соответствующий файл и получен с использованием кнопки источника.

//Rcpp Code
#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
NumericMatrix Cpp_String_difference_outer_diag(NumericMatrix output){
  long nrow = output.nrow();
  for(long i = 2; i < nrow; i++){ // note the 
    for(long j = 0; j < i; j++){
      output(i, j) = output(i, i) - output(i - 1, i - 1) + //are the words different?
                                  output(i - 1, j);
      output(j, i) = output(i, j);
    }
  }
  return output;
}

Соответствующую функцию R необходимо изменить, чтобы использовать эту функцию вместо зацикливания.Код похож на первую функцию, только переключая цикл для вызова функции c ++.

Dist_between_strings_cpp <- function(x, y, 
                                 split = " ", 
                                 split_x = split, split_y = split, 
                                 case_sensitive = TRUE){
  #Safety checks
  if(!is.character(x) || !is.character(y) || 
     nchar(x) == 0 || nchar(y) == 0)
    stop("x, y needs to be none empty character strings.")
  if(length(x) != 1 || length(y) != 1)
    stop("Currency the function is not vectorized, please provide the strings individually or use lapply.")
  if(!is.logical(case_sensitive))
    stop("case_sensitivity needs to be logical")
  #Extract variable names of our variables
  # used for the dimension names later on
  x_name <- deparse(substitute(x))
  y_name <- deparse(substitute(y))
  #Expression which when evaluated will name our output
  dimname_expression <- 
    parse(text = paste0("dimnames(output) <- list(", make.names(x_name, unique = TRUE)," = x_names,",
                        make.names(y_name, unique = TRUE)," = y_names)"))
  #split the strings into words
  x_names <- str_split(x, split_x, simplify = TRUE)
  y_names <- str_split(y, split_y, simplify = TRUE)
  #are we case_sensitive?
  if(isTRUE(case_sensitive)){
    x_split <- str_split(tolower(x), split_x, simplify = TRUE)
    y_split <- str_split(tolower(y), split_y, simplify = TRUE)
  }else{
    x_split <- x_names
    y_split <- y_names
  }
  #Create an index in case the two are of different length
  idx <- seq(1, (n_min <- min((nx <- length(x_split)),
                              (ny <- length(y_split)))))
  n_max <- max(nx, ny)
  #If we have one string that has length 1, the output is simplified
  if(n_min == 1){ 
    distances <- seq(1, n_max) - (x_split[idx] == y_split[idx])
    output <- matrix(distances, nrow = nx)
    eval(dimname_expression)
    return(output)
  }
  #If not we will have to do a bit of work
  output <- diag(cumsum(ifelse(x_split[idx] == y_split[idx], 0, 1)))
  #The loop will fill in the off_diagonal
  output[2, 1] <- output[1, 2] <- output[1, 1] + 1 
  if(n_max > 2) 
    output <- Cpp_String_difference_outer_diag(output) #Execute the c++ code
  #comparison if the list is not of the same size
  if(nx != ny){
    #Add the remaining words to the side that does not contain this
    additional_words <- seq(1, n_max - n_min)
    additional_words <- sapply(additional_words, function(x) x + output[,n_min])
    #merge the additional words
    if(nx > ny)
      output <- rbind(output, t(additional_words))
    else
      output <- cbind(output, additional_words)
  }
  #set the dimension names, 
  # I would like the original variable names to be displayed, as such i create an expression and evaluate it
  eval(dimname_expression)
  output
}

Тестирование реализации c ++

Чтобы убедиться, что реализация верна, мы проверяем,тот же результат получается с реализацией c ++.

#Test the cpp implementation
identical(Dist_between_strings(string_3, string_4, case_sensitive = TRUE),
          Dist_between_strings_cpp(string_3, string_4, case_sensitive = TRUE))
#TRUE

Окончательные тесты

Теперь это на самом деле быстрее?Чтобы увидеть это, мы можем запустить другой тест, используя пакет microbenchmark.Код и результаты показаны ниже:

#Final microbenchmarking
microbenchmark::microbenchmark(R = Dist_between_strings(string_7, string_8, case_sensitive = FALSE),
                               Rcpp = Dist_between_strings_cpp(string_7, string_8, case_sensitive = FALSE))
# Unit: milliseconds
# expr       min       lq      mean    median        uq       max neval
# R    721.71899 753.6992 850.21045 787.26555 907.06919 1756.7574   100
# Rcpp  23.90164  32.9145  54.37215  37.28216  47.88256  243.6572   100

Из среднего показателя улучшения микробенчмарка, равного примерно 21 ( = 787 / 37), что является значительным улучшением по сравнению с простой реализацией одного цикла!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...