Определение того, какие ошибки обнаружены алгоритмом расстояния редактирования Дамерау-Левенштейна

Я создаю инструмент для исправления орфографии и хотел реализовать зашумленный канал с помощью теоремы Байеса. Для этого мне нужно рассчитать вероятность P(X|W), где X — заданное (с ошибкой) слово, а W — возможное исправление. Вероятность задается путем получения значения из матрицы путаницы, которая зависит от знания того, какой тип ошибки произошел, а это означает, что если, например, X = egh и W = яйцо, то расстояние редактирования будет равно 1, а ошибка будет подстановкой. ошибка, которая произошла на символе номер 2.

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

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

Вот мой код:

public static int DLD(String s1, String s2) {
    if (s1 == null || s2 == null) {  // Invalid input
        return -1;
    }

    if (s1.equals(s2)) {  // No distance to compute
        return 0;
    }

    // The max possible distance
    int inf = s1.length() + s2.length();

    // Create and initialize the character array indices
    HashMap<Character, Integer> da = new HashMap<>();
    for (int i = 0; i < s1.length(); ++i) {
        da.put(s1.charAt(i), 0);
    }
    for (int j = 0; j < s2.length(); ++j) {
        da.put(s2.charAt(j), 0);
    }

    // Create the distance matrix H[0 .. s1.length+1][0 .. s2.length+1]
    int[][] distances = new int[s1.length() + 2][s2.length() + 2];

    // initialize the left and top edges of H
    for (int i = 0; i <= s1.length(); ++i) {
        distances[i + 1][0] = inf;
        distances[i + 1][1] = i;
    }

    for (int j = 0; j <= s2.length(); ++j) {
        distances[0][j + 1] = inf;
        distances[1][j + 1] = j;

    }

    // fill in the distance matrix H
    // look at each character in s1
    for (int i = 1; i <= s1.length(); ++i) {
        int db = 0;

        // look at each character in s2
        for (int j = 1; j <= s2.length(); ++j) {
            int i1 = da.get(s2.charAt(j - 1));
            int j1 = db;

            int cost = 1;
            if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                cost = 0;
                db = j;
            }

            distances[i + 1][j + 1] = min(
                    distances[i][j] + cost, // substitution
                    distances[i + 1][j] + 1, // insertion
                    distances[i][j + 1] + 1, // deletion
                    distances[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1));

        }

        da.put(s1.charAt(i - 1), i);
    }

    return distances[s1.length() + 1][s2.length() + 1];
}

Любой намек/направление на решение этого будет высоко оценен.

Спасибо!

Редактировать 1: Я кое-что понял, и, похоже, это работает, хотя я не уверен на 100 %. Я заменил сегмент кода, где я использую метод min(), следующим:

int sub = distances[i][j] + cost;
int ins = distances[i + 1][j] + 1;
int del = distances[i][j + 1] + 1;
int trans = distances[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1);

distances[i + 1][j + 1] = min(sub, ins, del, trans);

if ((distances[i][j] == 0 || distances[i - 1][j] == 0 || 
     distances[i][j - 1] == 0 || distances[i + 1][j + 1] == trans) &&
                    distances[i + 1][j + 1] == 1) {
                
    TreeMap<String, Integer> error = mappingTermAndError.getOrDefault(s2, null);
    if (error != null) {
        error.clear();
    } else {
        error = new TreeMap<>();
    }

    if (distances[i + 1][j + 1] == trans) {
        error.put("trans", i - 2);

    } else if (distances[i + 1][j + 1] == del) {
        error.put("del", i - 1);

    } else if (distances[i + 1][j + 1] == ins) {
        error.put("ins", i - 1);

    } else {  // distances[i + 1][j + 1] == sub
        error.put("sub", i - 1);
    }
    mappingTermAndError.put(s2, error);
}

Что он в основном делает, так это получает значение для каждого типа ошибки, а затем вычисляет минимум. если новый минимум равен 1 (так что это первая ошибка), а также одна из предыдущих ячеек в матрице расстояний равна 0 (это означает, что есть путь без ошибок, ведущий к этой точке) или если ошибка транспонирования (что мы можем только знать об этом после того, как у нас уже была ошибка), чем я заменяю ранее зарегистрированную ошибку новой и получаю «i», соответствующий символу, для которого была сделана ошибка.

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


person shahaf hermann    schedule 26.08.2020    source источник


Ответы (1)


Тип ошибки и задействованные символы должны где-то храниться. Вы можете иметь их в отдельных структурах данных или инкапсулировать в объекты.

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

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

class Edit implements Comparable<Edit> {
    int cost;
    Edit parent;
    String type;

    public Edit() {
        // create a "start" node with no parent and zero cost
    }

    public Edit(String type, Edit parent, int cost) {
        this.type = type;
        this.cost = parent.cost + cost;
        this.parent = parent;
    }

    @Override
    public int compareTo(Edit o) {
        return Integer.compare(this.cost, o.cost);
    }

    @Override
    public String toString() {
        return type;
    }
}

Затем вы используете этот класс вместо int для таблицы расстояний. В 0,0 есть специальный начальный узел без родителя. Во всех остальных точках вы выбираете узел с тем или иным родителем в соответствии с минимальной стоимостью, необходимой для достижения этого узла. Для большей гибкости выделим построение матрицы из метода editDistance:

Edit[][] buildMatrix(String s1, String s2) {
    Edit[][] distance = new Edit[s1.length() + 1][s2.length() + 1];

    distance[0][0] = new Edit();
    for (int i = 1; i <= s1.length(); i++) {
        distance[i][0] = new Edit("-" + s1.charAt(i - 1), distance[i - 1][0], 1);
    }
    for (int j = 1; j <= s2.length(); j++) {
        distance[0][j] = new Edit("+" + s2.charAt(j - 1), distance[0][j - 1], 1);
    }

    for (int i = 1; i <= s1.length(); i++) {
        for (int j = 1; j <= s2.length(); j++) {
            int replaceCost = s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1;
            distance[i][j] = Collections.min(List.of(
                // replace or same
                new Edit(s1.charAt(i - 1) + "/" + s2.charAt(j - 1), distance[i - 1][j - 1], replaceCost),
                // delete
                new Edit("-" + s1.charAt(i - 1), distance[i - 1][j], 1),
                // insert
                new Edit("+" + s2.charAt(j - 1), distance[i][j - 1], 1)));
        }
    }

    return distance;
}

Тогда функция редактирования расстояния должна принимать только стоимость последнего узла:

int editDistance(String s1, String s2) {
    Edit[][] distance = buildMatrix(s1, s2);
    return distance[s1.length()][s2.length()].cost;
}

Но благодаря родительским указателям вы также можете легко создать список правок, необходимых для замены одной строки на другую, также известный как diff:

List<Edit> diff(String s1, String s2) {
    Edit[][] distance = buildMatrix(s1, s2);
    List<Edit> diff = new ArrayList<>();
    Edit edit = distance[s1.length()][s2.length()];
    while (edit != distance[0][0]) {
        diff.add(edit);
        edit = edit.parent;
    }
    Collections.reverse(diff);
    return diff;
}
person Joni    schedule 27.08.2020
comment
Спасибо за развернутый ответ! Я мог бы попробовать это. Я также вспомнил сегодня, что на самом деле можно проследить путь в матрице, поскольку это алгоритм динамического программирования, поэтому я тоже мог бы попробовать. - person shahaf hermann; 27.08.2020