Как я могу адаптировать алгоритм расстояния Левенштейна, чтобы ограничить совпадения одним словом?


Я использую алгоритм расстояния Левенштейна в C++ для сравнения двух строк, чтобы измерить, насколько они близки друг к другу. Однако простой алгоритм расстояния Левенштейна не различает границы слов, разделенные пробелами. Это приводит к вычислению меньшего расстояния, чем я хочу. Я сравниваю названия, чтобы увидеть, насколько они близки друг к другу, и я хочу, чтобы алгоритм не считал символы совпадающими, если они исходят из нескольких слов.

Например, если Я сравниваю эти две строки и получаю следующий результат с + обозначением совпадения и - обозначением несоответствия:

Al Chertoff Et
Al Church Department of finance Et
+++++------+--++-----++-+------+++
Al Ch      e  rt     of f       Et

Я получаю расстояние 20 со словом "Chertoff", совпадающим по четырем словам "Church Department of finance", тогда как я действительно хочу, чтобы они рассматривались дальше друг от друга, не позволяя символам совпадать более чем с одним словом и получая расстояние 25 со словом "Chertoff", наиболее совпадающим с одним словом "Department", с тремя символами соответствие:

Al Chertoff Et
Al Church Department of finance Et
+++--------+--++---------------+++
Al         e  rt                Et
         Ch     off

Как я могу адаптировать расстояние Левенштейна для достижения этой цели или есть другой алгоритм расстояния, который лучше подходит для этого? Может быть, используя расстояние Левенштейна на каждом слове индивидуально слово работать и выбрать слово с наименьшим расстоянием? Однако, что делать, если совпадение одного слова хорошо глубоко в строку приводит к тому, что последующие слова плохо совпадают, потому что их совпадения были лучшими ранее в строке? Можно ли это как-то сделать с Левенштейном расстояние адаптировано, чтобы быть на уровне слов?

Например, кратчайшее расстояние по этой идее для следующего более сложного примера равно 20:

Al Chertoff Deport Et
Al Church Department of finance Et
+++++----++++-++---------------+++
Al Ch     Dep rt                Et
     ertoff  o

Вместо максимизации совпадения "Chertoff" и получения большего расстояния 24:

Al Chertoff Deport Et
Al Church Department of finance Et
+++--------+--++-----+---------+++
Al         e  rt     o          Et
         Ch     off
                  Dep rt

Моя текущая реализация расстояния Левенштейна выглядит следующим образом:

size_t
levenshtein_distance(const std::string& a_compare1,
                     const std::string& a_compare2) {
  const size_t length1 = a_compare1.size();
  const size_t length2 = a_compare2.size();
  std::vector<size_t> curr_col(length2 + 1);
  std::vector<size_t> prev_col(length2 + 1);

  // Prime the previous column for use in the following loop:
  for (size_t idx2 = 0; idx2 < length2 + 1; ++idx2) {
    prev_col[idx2] = idx2;
  }

  for (size_t idx1 = 0; idx1 < length1; ++idx1) {
    curr_col[0] = idx1 + 1;

    for (size_t idx2 = 0; idx2 < length2; ++idx2) {
      const size_t compare = a_compare1[idx1] == a_compare2[idx2] ? 0 : 1;

      curr_col[idx2 + 1] = std::min(std::min(curr_col[idx2] + 1,
                                             prev_col[idx2 + 1] + 1),
                                    prev_col[idx2] + compare);
    }

    curr_col.swap(prev_col);
  }

  return prev_col[length2];
}
2 11

2 ответа:

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

template<typename T, typename C>
size_t
seq_distance(const T& seq1, const T& seq2, const C& cost,
             const typename T::value_type& empty = typename T::value_type()) {
  const size_t size1 = seq1.size();
  const size_t size2 = seq2.size();

  std::vector<size_t> curr_col(size2 + 1);
  std::vector<size_t> prev_col(size2 + 1);

  // Prime the previous column for use in the following loop:
  prev_col[0] = 0;
  for (size_t idx2 = 0; idx2 < size2; ++idx2) {
    prev_col[idx2 + 1] = prev_col[idx2] + cost(empty, seq2[idx2]);
  }

  for (size_t idx1 = 0; idx1 < size1; ++idx1) {
    curr_col[0] = curr_col[0] + cost(seq1[idx1], empty);

    for (size_t idx2 = 0; idx2 < size2; ++idx2) {
      curr_col[idx2 + 1] = std::min(std::min(
        curr_col[idx2] + cost(empty, seq2[idx2]),
        prev_col[idx2 + 1] + cost(seq1[idx1], empty)),
        prev_col[idx2] + cost(seq1[idx1], seq2[idx2]));
    }

    curr_col.swap(prev_col);
    curr_col[0] = prev_col[0];
  }

  return prev_col[size2];
}

Учитывая вышеизложенное seq_distance, расстояние редактирования между двумя предложениями, такое, что правки не могут быть сделаны между границами слов, может быть определено следующим образом:

size_t
letter_distance(char letter1, char letter2) {
  return letter1 != letter2 ? 1 : 0;
}

size_t
word_distance(const std::string& word1, const std::string& word2) {
  return seq_distance(word1, word2, &letter_distance);
}

size_t
sentence_distance(const std::string& sentence1, const std::string& sentence2) {
  std::vector<std::string> words1;
  std::vector<std::string> words2;
  std::istringstream iss1(sentence1);
  std::istringstream iss2(sentence2);
  std::copy(std::istream_iterator<std::string>(iss1),
            std::istream_iterator<std::string>(),
            std::back_inserter(words1));
  std::copy(std::istream_iterator<std::string>(iss2),
            std::istream_iterator<std::string>(),
            std::back_inserter(words2));
  return seq_distance(words1, words2, &word_distance);
}

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

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

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

curr_col.reserve(length2 + 1);
prev_col.reserve(length2 + 1);

Резервируют емкость в векторах, но фактически не изменяют их размеры, поэтому обращение к массиву после этого было неопределенным поведением. Вы должны на самом деле resize вектор, если вы собираетесь получить доступ к элементам в диапазоне: reserve Обычно для ситуаций, когда вы собираетесь push_back определенное количество элементов один за другим (что увеличивает размер, как вы идете, а не все сразу), и вы хотите избежать затрат на несколько внутренних перераспределений (поскольку внутренняя емкость увеличивается только на определенный коэффициент каждый раз, когда емкость превышена).

Редактировать:

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

Границы слов будут пересекаться, если отдельные слова не имеют одинаковой длины. Если вы хотите, чтобы индексы сравнивались в пределах соответствующих слов, то вам нужно будет сделать слова одинаковой длины. Например, вот Javascript (да, я знаю, что вы спросили или C++, но это для иллюстрации - код взят из Википедии) процедура вычисления расстояния:

var memo = {};

function d(str1, i, len1, str2, j, len2){
    var key = [i,len1,j,len2].join(',');
    if(memo[key] != undefined) return memo[key];

    if(len1 == 0) return len2;
    if(len2 == 0) return len1;
    var cost = 0;
    if(str1[i] != str2[j]) cost = 1;

    var dist = Math.min(
        d(str1, i+1,len1-1, str2,j,len2)+1, 
        d(str1,i,len1,str2,j+1,len2-1)+1,
        d(str1,i+1,len1-1,str2,j+1,len2-1)+cost);
    memo[key] = dist;
    return dist;
}

var str1 = "Al Chertoff Deport$$$$ $$ $$$$$$$ Et";
var str2 = "Al Church$$ Department of finance Et";

console.log(d(str1, 0, str1.length, str2, 0, str2.length));
Обратите внимание, как я изменил две входные строки, чтобы они совпадали на уровне отдельных слов. Работает это у меня расстояние 19. Аналогично, если я изменю строки на:
var str1 = "Al Chertoff $$$$$$$$$$ $$ $$$$$$$ Et";
var str2 = "Al Church$$ Department of finance Et";

Я получаю расстояние 24.