【问题标题】:Minimum Edit Distance Reconstruction最小编辑距离重建
【发布时间】:2012-05-25 04:47:33
【问题描述】:

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

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

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

这是代码,我的部分解决方案。例如,它适用于我得到的(“lead”->“last”),但不适用于下面的示例(“hint”->“isnt”)。我怀疑这是因为第一个字符是相等的,这会导致我的代码失效。任何正确方向的提示或指示都会很棒!

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')

【问题讨论】:

    标签: python matrix nlp dynamic-programming


    【解决方案1】:

    我建议你看看python-Levenshtein 模块。可能会让你走得很远:

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

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

    【讨论】:

      【解决方案2】:

      我认为在这种情况下更深入地理解算法很重要。我不会给你一些伪代码,而是引导你完成算法的基本步骤,并向你展示你想要的数据是如何在最终的矩阵中“编码”的。当然,如果您不需要推出自己的算法,那么您显然应该使用别人的,如MattH suggests

      大局

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

        _ h
      _ 0 1
      i 1 1
      

      这里,'_' 表示一个空字符串,矩阵中的每个单元格对应一个编辑序列,该序列将输入('''i')接收到输出('''h')。

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

      从那里,您可以通过从上、左和左上值中取最小值并加一来计算任何位置的值,或者,如果字母在字符串中的该点相同,则左上角的值不变。对于上表中(1, 1)处的值,(0, 0)处的最小值为0,所以(1, 1)处的值为1,这就是'i''h'的最小编辑距离(一个替换)。所以一般来说,最小编辑距离总是在矩阵的右下角。

      现在让我们再做一次,比较 ishi。同样,矩阵中的每个单元格对应于一个编辑序列,该序列将输入('''i''is')接收到输出('''h''hi')。

        _ h i
      _ 0 1 2
      i 1 1 #
      s 2 # #
      

      我们首先扩大矩阵,使用# 作为我们还不知道的值的占位符,并通过递增来扩展第一行和第一列。完成后,我们可以开始计算上面标记为# 的位置的结果。让我们从(2, 1)(在(行,列)中,即行主要符号)开始。在upper、upper-left和left值中,最小值为1。表中对应的字母不同——sh——所以我们在最小值上加一得到2,然后继续。

        _ h i
      _ 0 1 2
      i 1 1 #
      s 2 2 #
      

      让我们继续讨论(1, 2) 的值。现在事情有点不同了,因为表中对应的字母是相同的——它们都是i。这意味着我们可以选择取左上角单元格中的值而不加一个。这里的指导直觉是我们不必增加计数,因为在这个位置将相同的字母添加到两个字符串中。由于两个字符串的长度都增加了 1,我们沿对角线移动。

        _ h i
      _ 0 1 2
      i 1 1 1
      s 2 2 #
      

      使用最后一个空单元格后,一切恢复正常。对应的字母是si,所以我们再次取最小值加一,得到2

        _ h i
      _ 0 1 2
      i 1 1 1
      s 2 2 2
      

      如果我们对以ishi 开头的两个较长的单词继续这个过程,我们得到的表格——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) 会将我们从不需要编辑的_ -&gt; _ 带到需要一次编辑、一次插入的_ -&gt; h。同样,从(0, 0)(1, 0) 的向下移动将我们从不需要编辑的_ -&gt; _ 带到需要一次编辑、一次删除的i -&gt; _。最后,从(0, 0)(1, 1) 的对角线移动将我们从不需要编辑的_ -&gt; _ 带到需要一次编辑、一次替换的i -&gt; h

      所以现在我们要做的就是反转我们的步骤,从左上角、左上角和左上角的单元格中追踪局部最小值回到原点(0, 0),请记住,如果当前值相同作为最小值,我们必须转到左上角的单元格,因为这是唯一不会增加编辑距离的移动。

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

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

      把它放在一起

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

      (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 移动到hint。 (一般来说,要插入的字母来自输出字符串,而要删除的字母来自输入字符串。)那就是Delete, s。然后是两个值不变的对角线运动:Equal, n, nEqual, t, t

      结果:

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

      isnt 上执行这些说明:

      isnt   (No change)
      hisnt  (Insertion)
      hisnt  (No change)
      hint   (Deletion)
      hint   (No change)
      hint   (No change)
      

      总编辑距离为 2。

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

      一旦您了解了这一切,编写代码应该就不难了。在这种情况下,关键是首先深入了解算法。完成后,编写代码就轻而易举了。

      积累与重建

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

      【讨论】:

      • 非常感谢您的详细解释。这正是我想要的。
      • @senderle,再次感谢您!这非常有帮助。对于第二条路径,我得到: sub, i, h sub, s, i eq, n, n eq, n, n 因为这也有一个编辑距离 2,我如何决定使用哪一个? (抱歉,我的换行符似乎都不起作用)
      • 在我的原始代码中,我最终得到一个这样的矩阵: [0, 1, 2, 3, 4] [1, 0, 1, 2, 3] [2, 1, 1 , 2, 3] [3, 2, 1, 2, 3] [4, 3, 2, 2, 3] 我注意到我的教授在我原来的评论中提供的例子:Equal, L, L Delete, E Equal, A , A Substitute, D, S Insert, T 但是我不能也只是从中间对角向下走,有: Equal, L, L Sub, E, A Sub, A, S Sub, D, T 要么回答“更多正确”比另一个?
      • 我不这么认为。正如我应该提到的,您的代码将采用的路径取决于您将参数传递给min 的顺序。但是编辑距离是一样的,输入和输出的字符串是一样的,所以我看不出你的教授会因为得到不同的结果而责备你。事实上,这个例子有(据我统计,这可能是错误的)不少于五个不同的正确编辑序列。但是,也许您的教授希望看到您完成所有这些,并弄清楚如何对其进行编码,以使结果与他给您的示例完全相同。
      • @senderle - 我一直在看这个,我认为它可能遗漏了一些东西。在您提出的第二个解决方案中,您将字符串直接沿对角线向上 - (4,4), (3,3), (2,2), (1,1), (0,0)。但是为了从(2,2) -&gt; (1,1) 移动,(2,2) 不是必须等于局部最小值吗?
      【解决方案3】:

      我不知道 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
          }
      

      【讨论】: