最小编辑距离重建

时间:2012-05-17 15:21:22

标签: python matrix nlp dynamic-programming

我知道在堆栈上也有类似的答案,以及在线,但我觉得我错过了一些东西。鉴于下面的代码,我们需要重建导致最小编辑距离的事件序列。对于下面的代码,我们需要编写一个输出函数:

Equal, L, L
Delete, E
Equal, A, A
Substitute, D, S
Insert, T

编辑:我的(部分正确)解决方案更新了代码

这是代码,我的部分解决方案。它的作用例如我被给予(“引导” - >“最后”),但不适用于下面的例子(“提示” - >“不是”)。我怀疑这是因为第一个字符相同,这就是抛弃我的代码。任何正确方向的提示或指示都会很棒!

def printMatrix(M):
        for row in M:
                print row
        print

def med(s, t):  
        k = len(s) + 1
        l = len(t) + 1

        M = [[0 for i in range(k)] for j in range(l)]
        MTrace = [["" for i in range(k)] for j in range(l)]

        M[0][0] = 0


        for i in xrange(0, k):
                M[i][0] = i
                MTrace[i][0] = s[i-1]

        for j in xrange(0, l):
                M[0][j] = j
                MTrace[0][j] = t[j-1]

        MTrace[0][0] = "DONE"

        for i in xrange(1, k):
                for j in xrange(1, l):

                        sub = 1
                        sub_op = "sub"
                        if s[i-1] == t[j-1]:
                                # equality
                                sub = 0
                                sub_op = "eq"


                        # deletion
                        min_value = M[i-1][j] + 1
                        op = "del"
                        if min_value > M[i][j-1] + 1:
                                # insertion
                                min_value = M[i][j-1] + 1
                                op = "ins"
                        if min_value > M[i-1][j-1] + sub:
                                # substitution
                                min_value = M[i-1][j-1] + sub
                                op = sub_op


                        M[i][j] = min_value
                        MTrace[i][j] = op                        

        print "final Matrix"
        printMatrix(M)
        printMatrix(MTrace)

############ MY PARTIAL SOLUTION

        def array_append(array,x,y):
            ops_string = MTrace[x][y]
            if ops_string == 'ins':
                array.append(("Insert",MTrace[0][y]))
            elif ops_string == 'sub':
                array.append(("Substitute",MTrace[x][0],MTrace[0][y]))
            elif ops_string == 'eq':
                array.append(("Equal",MTrace[x][0],MTrace[0][y]))
            elif ops_string == 'del':
                array.append(("Delete",MTrace[x][0]))


        i = len(s)
        j = len(t)

        ops_array = []
        base = M[i][j]
        array_append(ops_array,i,j)


        while MTrace[i][j] != "DONE":
            base = M[i][j]
            local_min = min(M[i][j-1],M[i-1][j],M[i-1][j-1])
            if base == local_min:
                i = i - 1
                j = j - 1
                array_append(ops_array,i,j)
            elif M[i][j-1] < M[i-1][j]:
                j = j -1
                array_append(ops_array,i,j)
            elif M[i-1][j] < M[i][j-1]:
                i = i - 1
                array_append(ops_array,i,j)
            else:
                i = i - 1
                j = j - 1
                array_append(ops_array,i,j)

        print ops_array
#########

        return M[k-1][l-1]      

print med('lead', 'last')

3 个答案:

答案 0 :(得分:32)

我认为在这种情况下更深入地理解算法非常重要。我将向您介绍算法的基本步骤,而不是给您一些伪代码,并向您展示您想要的数据是如何被编码的#34;在最终矩阵中产生。当然,如果您不需要推出自己的算法,那么您显然应该使用其他人作为MattH suggests

大图

这在我看来就像Wagner-Fischer algorithm的实现一样。基本思路是计算&#34;附近和#34;之间的距离。前缀,取最小值,然后计算当前字符串对的距离。例如,假设您有两个字符串'i''h'。让我们沿着矩阵的垂直和水平轴放置它们,如下所示:

  _ h
_ 0 1
i 1 1

这里,'_'表示一个空字符串,矩阵中的每个单元格对应一个输入('''i')到输出的编辑序列({{1 }或'')。

从空字符串到任何长度为L的字符串的距离为L,(需要L插入)。从任何长度为L的字符串到空字符串的距离也是L(需要L个删除)。这涵盖了第一行和第一列中的值,它们只是递增。

从那里,您可以通过从上左,右和左上角值中取最小值并添加一个来计算任何位置的值,或者,如果字符串中该点的字母相同,取左上角值不变。对于上表中'h'处的值,(1, 1)处的最小值为0,因此(0, 0)处的值为(1, 1),且该值为1。从'i''h'的最小编辑距离(一次替换)。因此,通常,最小编辑距离始终位于矩阵的右下角。

现在让我们做另一个,将ishi进行比较。同样,矩阵中的每个单元格对应一个编辑序列,该输入将输入('''i''is')输出到输出('',{{1 },或'h')。

'hi'

我们首先扩展矩阵,使用 _ h i _ 0 1 2 i 1 1 # s 2 # # 作为我们尚不了解的值的占位符,并通过递增来扩展第一行和第一列。完成后,我们可以开始计算上面标有#的位置的结果。让我们从#开始(在(行,列)中,即行主要表示法)。在上,左上和左值中,最小值为(2, 1)。表格中的相应字母不同 - 1s - 因此我们将一个最小值添加到h,然后继续。

2

让我们继续 _ h i _ 0 1 2 i 1 1 # s 2 2 # 的值。现在情况有所不同,因为表中相应的字母是相同的 - 它们都是(1, 2)。这意味着我们可以选择在左上角的单元格中取值而不添加一个。这里的指导性直觉是我们不必增加计数,因为在这个位置上两个字符串都添加了相同的字母。而且由于两根琴弦的长度都增加了一倍,我们会对角移动。

i

对于最后一个空单元格,事情会恢复正常。相应的字母为 _ h i _ 0 1 2 i 1 1 1 s 2 2 # s,因此我们再次使用最小值并添加一个,以获得i

2

如果我们继续这个过程,我会得到两个较长的单词,这些单词以 _ h i _ 0 1 2 i 1 1 1 s 2 2 2 is - hi开头(忽略标点符号)和{{1 }}:

isnt

这个矩阵稍微复杂一点,但这里的最终最小编辑距离仍然只是hint,因为这两个字符串的最后两个字母是相同的。方便!

重新创建编辑序列

那么我们如何从这个表中提取编辑类型呢?关键是要认识到桌子上的移动对应于特定类型的编辑。例如,从 _ h i n t _ 0 1 2 3 4 i 1 1 1 2 3 s 2 2 2 2 3 n 3 3 3 2 3 t 4 4 4 3 2 2的向右移动将我们从(0, 0)转移到(0, 1),不需要编辑,需要一次编辑,插入。同样,从_ -> __ -> h的向下移动将我们从(0, 0)转移到(1, 0),不需要编辑,需要一次编辑,删除。最后,从_ -> _i -> _的对角线移动将我们从(0, 0)转移到(1, 1),不需要编辑,需要一次编辑,替换。

所以现在我们所要做的就是颠倒我们的步骤,从上,左和左上角的单元格中追溯局部最小值,回到原点_ -> _,记住如果当前值是与最小值相同,然后我们必须转到左上角的单元格,因为这是唯一一种不会增加编辑距离的运动。

以下是您可以采取的步骤的详细说明。从完成矩阵的右下角开始,重复以下操作,直至到达左上角:

  1. 查看左上方的相邻单元格。如果它不存在,请转到步骤3.如果单元格存在,请记下存储在那里的值。
  2. 左上角单元格中的值是否等于当前单元格中的值?如果是,请执行以下操作:
    • 记录空操作(即i -> h)。在这种情况下不需要编辑,因为此位置的字符是相同的。
    • 更新当前单元格,向上和向左移动。
    • 返回第1步。
  3. 这里有很多分店:
    • 如果左侧没有单元格且上方没有单元格,则表示您位于左上角,算法已完成。
    • 如果左侧没有单元格,请转到步骤4.(这将继续循环,直到您到达左上角。)
    • 如果上面没有单元格,请转到步骤5.(这将继续循环,直到您到达左上角。)
    • 否则,在左边的单元格,左上角的单元格和上面的单元格之间进行三向比较。选择价值最小的那个。如果有多个候选人,您可以随机选择一个;在这个阶段,他们所有都有效。 (它们对应于具有相同总编辑距离的不同编辑路径。)
      • 如果您选择了上面的单元格,请转到步骤4.
      • 如果您选择了左侧的单元格,请转到步骤5.
      • 如果您选择了左上方的单元格,请转到步骤6.
  4. 你正在向上移动。请执行下列操作:
    • 在当前单元格中记录输入字符的删除。
    • 更新当前单元格,向上移动。
    • 返回第1步。
  5. 你向左移动。请执行下列操作:
    • 在当前单元格中记录输出字符的插入。
    • 更新当前单元格,向左移动。
    • 返回第1步。
  6. 你正在对角线移动。请执行下列操作:
    • 在当前单元格中记录输入字符的替换,以代替当前单元格的输出字符。
    • 更新当前单元格,向上和向左移动。
    • 返回第1步。
  7. 将它放在一起

    在上面的示例中,有两种可能的路径:

    (0, 0)

    Equal

    扭转它们,我们得到

    (4, 4) -> (3, 3) -> (2, 2) -> (1, 2) -> (0, 1) -> (0, 0)
    

    (4, 4) -> (3, 3) -> (2, 2) -> (1, 1) -> (0, 0)
    

    因此对于第一个版本,我们的第一个操作是向右移动,即插入。插入的字母为(0, 0) -> (0, 1) -> (1, 2) -> (2, 2) -> (3, 3) -> (4, 4) ,因为我们已从(0, 0) -> (1, 1) -> (2, 2) -> (3, 3) -> (4, 4) 移至h。 (这对应于详细输出中的isnt。)我们的下一个操作是对角线移动,即替换或无操作。在这种情况下,它是无操作,因为两个位置的编辑距离相同(即字母相同)。所以hint。然后向下移动,对应于删除。删除的字母为Insert, h,因为我们再次从Equal, i, i移至s。 (通常,要插入的字母来自输出字符串,而要删除的字母来自输入字符串。)因此isnt。然后两个对角线移动,值没有变化:hintDelete, s

    结果:

    Equal, n, n

    Equal, t, t上执行这些说明:

    Insert, h
    Equal, i, i
    Delete, s
    Equal, n, n
    Equal, t, t
    

    总编辑距离为2.

    我将第二条最小路径作为练习。请记住,两条路径完全相同;它们可能不同,但它们会产生相同的最小编辑距离2,因此完全可以互换。当您在矩阵中向后工作时,如果您看到两个不同的可能的局部最小值,您可以选择其中一个,并确保最终结果是正确的

    一旦你理解了这一切,根本不应该编码。在这种情况下,关键是首先深入理解算法。一旦你完成了这项工作,编码就很容易了。

    累积与重建

    作为最后一点,您可以在填充矩阵时选择累积编辑。在这种情况下,矩阵中的每个单元格都可以是元组:isnt。您将增加长度,附加与最小先前状态的移动相对应的操作。这消除了回溯,因此降低了代码的复杂性;但它占用了额外的内存。如果这样做,最终的编辑序列将与矩阵右下角的最终编辑距离一起出现。

答案 1 :(得分:9)

我建议您查看python-Levenshtein模块。可能会在那里找到很长的路要走:

>>> import Levenshtein
>>> Levenshtein.editops('LEAD','LAST')
[('replace', 1, 1), ('replace', 2, 2), ('replace', 3, 3)]

您可以处理编辑操作的输出以创建详细说明。

答案 2 :(得分:2)

我不知道python,但如果有任何帮助,以下C#代码可以正常工作。

public class EditDistanceCalculator
{
    public double SubstitutionCost { get; private set; }
    public double DeletionCost { get; private set; }
    public double InsertionCost { get; private set; }

    public EditDistanceCalculator() : this(1,1, 1)
    {
    }

    public EditDistanceCalculator(double substitutionCost, double insertionCost, double deletionCost)
    {
        InsertionCost = insertionCost;
        DeletionCost = deletionCost;
        SubstitutionCost = substitutionCost;
    }

    public Move[] CalcEditDistance(string s, string t)
    {
        if (s == null) throw new ArgumentNullException("s");
        if (t == null) throw new ArgumentNullException("t");

        var distances = new Cell[s.Length + 1, t.Length + 1];
        for (int i = 0; i <= s.Length; i++)
            distances[i, 0] = new Cell(i, Move.Delete);
        for (int j = 0; j <= t.Length; j++)
            distances[0, j] = new Cell(j, Move.Insert);

        for (int i = 1; i <= s.Length; i++)
            for (int j = 1; j <= t.Length; j++)
                distances[i, j] = CalcEditDistance(distances, s, t, i, j);

        return GetEdit(distances, s.Length, t.Length);
    }

    private Cell CalcEditDistance(Cell[,] distances, string s, string t, int i, int j)
    {
        var cell = s[i - 1] == t[j - 1]
                            ? new Cell(distances[i - 1, j - 1].Cost, Move.Match)
                            : new Cell(SubstitutionCost + distances[i - 1, j - 1].Cost, Move.Substitute);
        double deletionCost = DeletionCost + distances[i - 1, j].Cost;
        if (deletionCost < cell.Cost)
            cell = new Cell(deletionCost, Move.Delete);

        double insertionCost = InsertionCost + distances[i, j - 1].Cost;
        if (insertionCost < cell.Cost)
            cell = new Cell(insertionCost, Move.Insert);

        return cell;
    }

    private static Move[] GetEdit(Cell[,] distances, int i, int j)
    {
        var moves = new Stack<Move>();
        while (i > 0 && j > 0)
        {
            var move = distances[i, j].Move;
            moves.Push(move);
            switch (move)
            {
                case Move.Match:
                case Move.Substitute:
                    i--;
                    j--;
                    break;
                case Move.Insert:
                    j--;
                    break;
                case Move.Delete:
                    i--;
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
        for (int k = 0; k < i; k++)
            moves.Push(Move.Delete);
        for (int k = 0; k < j; k++)
            moves.Push(Move.Insert);

        return moves.ToArray();
    }

    class Cell
    {
        public double Cost { get; private set; }
        public Move Move { get; private set; }

        public Cell(double cost, Move move)
        {
            Cost = cost;
            Move = move;
        }
    }
}

public enum Move
{
    Match,
    Substitute,
    Insert,
    Delete
}

一些测试:

    [TestMethod]
    public void TestEditDistance()
    {
        var expected = new[]
            {
                Move.Delete, 
                Move.Substitute, 
                Move.Match, 
                Move.Match, 
                Move.Match, 
                Move.Match, 
                Move.Match, 
                Move.Insert,
                Move.Substitute, 
                Move.Match, 
                Move.Substitute, 
                Move.Match, 
                Move.Match, 
                Move.Match, 
                Move.Match
            };
        Assert.IsTrue(expected.SequenceEqual(new EditDistanceCalculator().CalcEditDistance("thou-shalt-not", "you-should-not")));

        var calc = new EditDistanceCalculator(3, 1, 1);
        var edit = calc.CalcEditDistance("democrat", "republican");
        Console.WriteLine(string.Join(",", edit));
        Assert.AreEqual(3, edit.Count(m => m == Move.Match)); //eca
    }