Улучшение результата поиска с помощью расстояния Левенштейна в Java

у меня есть следующий рабочий Java-код для поиска слова против списка слов, и он работает отлично и как ожидалось:

public class Levenshtein {
    private int[][] wordMartix;

    public Set similarExists(String searchWord) {

        int maxDistance = searchWord.length();
        int curDistance;
        int sumCurMax;
        String checkWord;

        // preventing double words on returning list
        Set<String> fuzzyWordList = new HashSet<>();

        for (Object wordList : Searcher.wordList) {
            checkWord = String.valueOf(wordList);
            curDistance = calculateDistance(searchWord, checkWord);
            sumCurMax = maxDistance + curDistance;
            if (sumCurMax == checkWord.length()) {
                fuzzyWordList.add(checkWord);
            }
        }
        return fuzzyWordList;
    }

    public int calculateDistance(String inputWord, String checkWord) {
        wordMartix = new int[inputWord.length() + 1][checkWord.length() + 1];

        for (int i = 0; i <= inputWord.length(); i++) {
            wordMartix[i][0] = i;
        }

        for (int j = 0; j <= checkWord.length(); j++) {
            wordMartix[0][j] = j;
        }

        for (int i = 1; i < wordMartix.length; i++) {
            for (int j = 1; j < wordMartix[i].length; j++) {
                if (inputWord.charAt(i - 1) == checkWord.charAt(j - 1)) {
                    wordMartix[i][j] = wordMartix[i - 1][j - 1];
                } else {
                    int minimum = Integer.MAX_VALUE;
                    if ((wordMartix[i - 1][j]) + 1 < minimum) {
                        minimum = (wordMartix[i - 1][j]) + 1;
                    }

                    if ((wordMartix[i][j - 1]) + 1 < minimum) {
                        minimum = (wordMartix[i][j - 1]) + 1;
                    }

                    if ((wordMartix[i - 1][j - 1]) + 1 < minimum) {
                        minimum = (wordMartix[i - 1][j - 1]) + 1;
                    }

                    wordMartix[i][j] = minimum;
                }
            }
        }

        return wordMartix[inputWord.length()][checkWord.length()];
    }

}

прямо сейчас, когда я ищу слово, как job он возвращает список:

выход

joborienterede
jobannoncer
jobfunktioner
perjacobsen
jakobsen
jobprofiler
jacob
jobtitler
jobbet
jobdatabaserne
jobfunktion
jakob
jobs
studenterjobber
johannesburg
jobmuligheder
jobannoncerne
jobbaser
job
joberfaringer

как вы можете видеть, выход имеет много связанных слов, но также не связанных с ними, таких как jakob, jacob etc., что верно в отношении формулы Левенштейна, но я хотел бы построить дальше и напишите метод, который может настроить мой поиск, чтобы я мог получить более релевантные и связанные слова.

Я работал несколько часов над ним и потерял из виду творчество.

Мой Вопрос: можно ли точно настроить существующий метод для возврата соответствующих / связанных слов или я должен использовать другой подход или??? во всех случаях да или нет, я оценил, если могу получить вклад и вдохновение в отношении улучшения поиска результаты?


обновление

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

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

поэтому я сделал сортировку расстояния, и результаты были такими:

job
jobs
jacob
jakob
jobbet
jakobsen
jobbaser
jobtitler
jobannoncer
jobfunktion
jobprofiler
perjacobsen
johannesburg
jobannoncerne
joberfaringer
jobfunktioner
jobmuligheder
jobdatabaserne
joborienterede
studenterjobber

так слово jobbaser уместно и jacob/jakob не уместно, но расстояние для jobbaser больше, чем jacob/jakob. Так что это не очень помогло.


общие отзывы об ответах

  • @SergioMontoro, это решает почти проблему.
  • @uSeemSurprised, он решает проблему, но нуждается в постоянной манипуляции.
  • @Gene concept отлично, но он ретранслируется по внешнему url.

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

особая благодарность за ответы от @SergioMontoro, @uSeemSurprised и @Gene, это разные, но действительные и полезные ответы.

@D. Kovács указывает на какое-то интересное решение.

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

5 ответов


Не понимая значения таких слов, как @DrYap, следующая логическая единица для сравнения двух слов (если вы не ищете ошибок) - это слоги. Очень легко модифицировать Левенштейна, чтобы сравнивать слоги вместо символов. Самое трудное-разбить слова на слоги. Существует реализация Java TeXHyphenator-J который можно использовать для разделения слов. На основе этой библиотеки переносов, вот модифицированная версия функции Левенштейна написано Michael Gilleland & Chas Emerick. Подробнее о обнаружение слогов здесь и здесь. Конечно, вы захотите избежать слогового сравнения двух односложных слов, вероятно, обрабатывающих этот случай со стандартным Левенштейном.

import net.davidashen.text.Hyphenator;

public class WordDistance {

    public static void main(String args[]) throws Exception {
        Hyphenator h = new Hyphenator();
        h.loadTable(WordDistance.class.getResourceAsStream("hyphen.tex"));
        getSyllableLevenshteinDistance(h, args[0], args[1]);
    }

    /**
     * <p>
     * Calculate Syllable Levenshtein distance between two words </p>
     * The Syllable Levenshtein distance is defined as the minimal number of
     * case-insensitive syllables you have to replace, insert or delete to transform word1 into word2.
     * @return int
     * @throws IllegalArgumentException if either str1 or str2 is <b>null</b>
     */
    public static int getSyllableLevenshteinDistance(Hyphenator h, String s, String t) {
        if (s == null || t == null)
            throw new NullPointerException("Strings must not be null");

        final String hyphen = Character.toString((char) 173);
        final String[] ss = h.hyphenate(s).split(hyphen);
        final String[] st = h.hyphenate(t).split(hyphen);

        final int n = ss.length;
        final int m = st.length;

        if (n == 0)
            return m;
        else if (m == 0)
            return n;

        int p[] = new int[n + 1]; // 'previous' cost array, horizontally
        int d[] = new int[n + 1]; // cost array, horizontally

        for (int i = 0; i <= n; i++)
            p[i] = i;

        for (int j = 1; j <= m; j++) {
            d[0] = j;
            for (int i = 1; i <= n; i++) {
                int cost = ss[i - 1].equalsIgnoreCase(st[j - 1]) ? 0 : 1;
                // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
                d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
            }
            // copy current distance counts to 'previous row' distance counts
            int[] _d = p;
            p = d;
            d = _d;
        }

        // our last action in the above loop was to switch d and p, so p now actually has the most recent cost counts
        return p[n];
    }

}

вы можете изменить расстояние Левенштейна, регулируя счет, когда последовательные символы совпадают.

всякий раз, когда есть последовательные символы, которые соответствуют, оценка может быть уменьшена, таким образом, делая поиск более релевантным.

например: скажем, коэффициент, на который мы хотим уменьшить счет на 10, тогда, если одним словом мы найдем подстроку "работа", мы можем уменьшить счет на 10, когда мы столкнемся с" j", далее уменьшим его на (10 + 20) , когда мы найдем строку " jo " и, наконец, уменьшить счет на (10 + 20 + 30 когда найдем "работу".

Я написал код C++ ниже:

#include <bits/stdc++.h>

#define INF -10000000
#define FACTOR 10

using namespace std;

double memo[100][100][100];

double Levenshtein(string inputWord, string checkWord, int i, int j, int count){
    if(i == inputWord.length() && j == checkWord.length()) return 0;    
    if(i == inputWord.length()) return checkWord.length() - j;
    if(j == checkWord.length()) return inputWord.length() - i;
    if(memo[i][j][count] != INF) return memo[i][j][count];

    double ans1 = 0, ans2 = 0, ans3 = 0, ans = 0;
    if(inputWord[i] == checkWord[j]){
        ans1 = Levenshtein(inputWord, checkWord, i+1, j+1, count+1) - (FACTOR*(count+1));
        ans2 = Levenshtein(inputWord, checkWord, i+1, j, 0) + 1;
        ans3 = Levenshtein(inputWord, checkWord, i, j+1, 0) + 1;
        ans = min(ans1, min(ans2, ans3));
    }else{
        ans1 = Levenshtein(inputWord, checkWord, i+1, j, 0) + 1;
        ans2 = Levenshtein(inputWord, checkWord, i, j+1, 0) + 1;
        ans = min(ans1, ans2);
    }
    return memo[i][j][count] = ans;
}

int main(void) {
    // your code goes here
    string word = "job";
    string wordList[40];
    vector< pair <double, string> > ans;
    for(int i = 0;i < 40;i++){
        cin >> wordList[i];
        for(int j = 0;j < 100;j++) for(int k = 0;k < 100;k++){
            for(int m = 0;m < 100;m++) memo[j][k][m] = INF;
        }
        ans.push_back( make_pair(Levenshtein(word, wordList[i], 
            0, 0, 0), wordList[i]) );
    }
    sort(ans.begin(), ans.end());
    for(int i = 0;i < ans.size();i++){
        cout << ans[i].second << " " << ans[i].first << endl;
    }
    return 0;
}

ссылка на демо:http://ideone.com/4UtCX3

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

также обратите внимание, что сложность вышеуказанного расстояния Левенштейна также увеличилась, теперь это O(n^3) вместо O(n^2) как теперь мы также отслеживаем счетчика это подсчитывает, сколько последовательных символов мы встретили.

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

также В приведенном выше решении вы можете удалить строки, которые имеют score >=0, поскольку они вообще не releavent вы также можете выбрать другой порог для того, чтобы иметь больше точный поиск.


Так как вы спросили, я покажу, как семантическая сеть UMBC может делать такие вещи. Не уверен, что это то, что вы действительно хотите:

import static java.lang.String.format;
import static java.util.Comparator.comparingDouble;
import static java.util.stream.Collectors.toMap;
import static java.util.function.Function.identity;

import java.util.Map.Entry;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.regex.Pattern;

public class SemanticSimilarity {
  private static final String GET_URL_FORMAT
      = "http://swoogle.umbc.edu/SimService/GetSimilarity?"
          + "operation=api&phrase1=%s&phrase2=%s";
  private static final Pattern VALID_WORD_PATTERN = Pattern.compile("\w+");
  private static final String[] DICT = {
    "cat",
    "building",
    "girl",
    "ranch",
    "drawing",
    "wool",
    "gear",
    "question",
    "information",
    "tank" 
  };

  public static String httpGetLine(String urlToRead) throws IOException {
    URL url = new URL(urlToRead);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setRequestMethod("GET");
    try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(conn.getInputStream()))) {
      return reader.readLine();
    }
  }

  public static double getSimilarity(String a, String b) {
    if (!VALID_WORD_PATTERN.matcher(a).matches()
        || !VALID_WORD_PATTERN.matcher(b).matches()) {
      throw new RuntimeException("Bad word");
    }
    try {
      return Double.parseDouble(httpGetLine(format(GET_URL_FORMAT, a, b)));
    } catch (IOException | NumberFormatException ex) {
      return -1.0;
    }
  }

  public static void test(String target) throws IOException {
    System.out.println("Target: " + target);
    Arrays.stream(DICT)
        .collect(toMap(identity(), word -> getSimilarity(target, word)))
        .entrySet().stream()
        .sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
        .forEach(System.out::println);
    System.out.println();
  }

  public static void main(String[] args) throws Exception {
    test("sheep");
    test("vehicle");
    test("house");
    test("data");
    test("girlfriend");
  }
}

результаты являются своего рода увлекательными:

Target: sheep
ranch=0.38563728
cat=0.37816614
wool=0.36558008
question=0.047607
girl=0.0388761
information=0.027191084
drawing=0.0039623436
tank=0.0
building=0.0
gear=0.0

Target: vehicle
tank=0.65860236
gear=0.2673374
building=0.20197356
cat=0.06057514
information=0.041832563
ranch=0.017701812
question=0.017145569
girl=0.010708235
wool=0.0
drawing=0.0

Target: house
building=1.0
ranch=0.104496084
tank=0.103863
wool=0.059761923
girl=0.056549154
drawing=0.04310725
cat=0.0418914
gear=0.026439993
information=0.020329408
question=0.0012588014

Target: data
information=0.9924584
question=0.03476312
gear=0.029112043
wool=0.019744944
tank=0.014537057
drawing=0.013742204
ranch=0.0
cat=0.0
girl=0.0
building=0.0

Target: girlfriend
girl=0.70060706
ranch=0.11062875
cat=0.09766617
gear=0.04835723
information=0.02449007
wool=0.0
question=0.0
drawing=0.0
tank=0.0
building=0.0

Это действительно открытый вопрос, но я бы предложил альтернативный подход, который использует, например, алгоритм Смита-Уотермана как описано в это так.

другим (более легким) решением было бы использовать другие метрики расстояния / сходства из НЛП (например, Косинус сходство или Дамерау–Левенштейна расстояние).


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

(поскольку я не мог найти, как я не мог найти класс Searcher из вашего кода, я взял на себя смелость использовать другой источник wordlist, реализацию Levenshtein и язык.)

используя список слов, предоставленный в Ubuntu, и реализацию Levenshtein algo из - https://github.com/ztane/python-Levenshtein, Я создал небольшой скрипт, который запрашивает Слово и печатает все ближайшие слова и расстояние как кортеж.

код https://gist.github.com/atdaemon/9f59ad886c35024bdd28

from Levenshtein import distance
import os

def read_dict() :
    with open('/usr/share/dict/words','r') as f : 
        for line in f :
            yield str(line).strip()

inp = str(raw_input('Enter a word : '))

wordlist = read_dict()
matches = []
for word in wordlist :
    dist = distance(inp,word)
    if dist < 3 :
        matches.append((dist,word))
print os.linesep.join(map(str,sorted(matches)))

пример вывода -

Enter a word : job
(0, 'job')
(1, 'Bob')
(1, 'Job')
(1, 'Rob')
(1, 'bob')
(1, 'cob')
(1, 'fob')
(1, 'gob')
(1, 'hob')
(1, 'jab')
(1, 'jib')
(1, 'jobs')
(1, 'jog')
(1, 'jot')
(1, 'joy')
(1, 'lob')
(1, 'mob')
(1, 'rob')
(1, 'sob')
...

Enter a word : checker
(0, 'checker')
(1, 'checked')
(1, 'checkers')
(2, 'Becker')
(2, 'Decker')
(2, 'cheaper')
(2, 'cheater')
(2, 'check')
(2, "check's")
(2, "checker's")
(2, 'checkered')
(2, 'checks')
(2, 'checkup')
(2, 'cheeked')
(2, 'cheekier')
(2, 'cheer')
(2, 'chewer')
(2, 'chewier')
(2, 'chicer')
(2, 'chicken')
(2, 'chocked')
(2, 'choker')
(2, 'chucked')
(2, 'cracker')
(2, 'hacker')
(2, 'heckler')
(2, 'shocker')
(2, 'thicker')
(2, 'wrecker')