Levenshtein距离成本

时间:2011-07-16 16:39:46

标签: haskell haskell-platform

我是haskell的新手,我遇到了一个非常严重的性能问题,它必须是我的代码,而不是haskell平台。

我有一个Levenshtein距离(自己的代码)的python实现,我通过(或试图这样做)这个到haskell。结果如下:

bool2int :: Bool -> Int
bool2int True = 1
bool2int False = 0

levenshtein :: Eq a => [a] -> [a] -> Int -> Int -> Int
levenshtein u v 0 0 = 0
levenshtein u v i 0 = i
levenshtein u v 0 j = j
levenshtein u v i j = minimum [1 + levenshtein u v i (j - 1),
    1 + levenshtein u v (i - 1) j,
    bool2int (u !! (i - 1) /= v !! (j - 1) ) + levenshtein u v (i - 1) (j - 1) ]

distance :: Eq a => [a] -> [a] -> Int
distance u v = levenshtein u v (length u) (length v)

现在,长度为10或更长的字符串的执行时间差异在python和haskell之间具有10的各种幂。还有一些粗略的时间测量(挂钟,因为到目前为止我还没有在haskell中找到clock()命令),似乎我的haskell实现没有花费O(mn),但是其他一些其他成本也在快速增长。

Nota bene:我不希望我的haskell实现与python脚本快速竞争。我只是希望它在一个“明智的”时间内运行,而不是在整个宇宙存在的时间的倍数。

问题:

  • 我做错了什么,我的实施是如此缓慢?
  • 如何解决?
  • 谈论“懒惰的评价”:我认为如果levenshtein "cat" "kit" 2 2被称为三次,则只计算一次。这是对的吗?
  • 我的bool2int必须内置一些东西,对吧?
  • 如果它在掌握哈斯克尔的粗糙路径上推动我前进,那么任何其他意见都会受到高度赞赏。

编辑:这里是用于比较的python代码:

#! /usr/bin/python3.2
# -*- coding, utf-8 -*-

class Levenshtein:
        def __init__ (self, u, v):
                self.__u = ' ' + u
                self.__v = ' ' + v
                self.__D = [ [None for x in self.__u] for x in self.__v]
                for x, _ in enumerate (self.__u): self.__D [0] [x] = x
                for x, _ in enumerate (self.__v): self.__D [x] [0] = x

        @property
        def distance (self):
                return self.__getD (len (self.__v) - 1, len (self.__u) - 1)

        def __getD (self, i, j):
                if self.__D [i] [j] != None: return self.__D [i] [j]
                self.__D [i] [j] = min ( [self.__getD (i - 1, j - 1) + (0 if self.__v [i] == self.__u [j] else 1),
                                  self.__getD (i, j - 1) + 1,
                                  self.__getD (i - 1, j) + 1] )
                return self.__D [i] [j]

print (Levenshtein ('first string', 'second string').distance)

2 个答案:

答案 0 :(得分:5)

  

我做错了什么,我的实施是如此缓慢?

您的算法具有指数复杂性。你好像假设这些电话都是为你记忆的,但事实并非如此。

  

如何解决?

您需要添加显式的memoization,可能使用数组或其他方法。

  

谈论“懒惰的评价”:我认为如果levenshtein "cat" "kit" 2 2被称为三次,则只计算一次。这是对的吗?

不,Haskell不进行自动记忆。懒惰意味着如果你执行let y = f x in y + y,那么只有在需要求和的结果时才会评估f x(一次)。 意味着f x + f x只会在f x的一次调用中评估。当您想要共享子表达式的结果时,您必须明确。

  

我的bool2int必须内置一些内容,对吧?

是的,有一个instance Enum Bool,因此您可以使用fromEnum

*Main> fromEnum True
1
*Main> fromEnum False
0
  

如果它在掌握haskell的粗糙路径上推动我前进,那么任何其他输入都会受到高度赞赏。

虽然从头开始编写内容可能既有趣又有教育意义,但在做这样的常见事情时,学习利用Hackage上的许多优秀库是很重要的。

例如,edit-distance package中有Levenshtein距离的实现。


我将您的Haskell代码翻译回Python进行比较:

def levenshtein(u, v, i, j):
    if i == 0: return j
    if j == 0: return i

    return min(1 + levenshtein(u, v, i, (j-1)),
               1 + levenshtein(u, v, (i-1), j),
               (u[i-1] != v[j-1]) + levenshtein(u, v, (i-1), (j-1)))

def distance(u, v):
    return levenshtein(u, v, len(u), len(v))

if __name__ == "__main__":
    print distance("abbacabaab", "abaddafaca")

即使没有修复chrisdb pointed out in his answer的O(n)索引问题,这在编译时也比Haskell版本慢:

$ time python levenshtein.py
6

real    0m4.793s
user    0m4.690s
sys 0m0.020s

$ time ./Levenshtein 
6

real    0m0.524s
user    0m0.520s
sys 0m0.000s

当然,他们都输入了编辑距离包中正确记忆的版本:

$ time ./LevenshteinEditDistance 
6

real    0m0.015s
user    0m0.010s
sys 0m0.000s

以下是使用Data.Array的简单记忆实现:

import Data.Array

distance u v = table ! (m, n)
   where table = listArray ((0, 0), (m, n)) [levenshtein i j | i <- [0..m], j <- [0..n]]

         levenshtein 0 j = j
         levenshtein i 0 = i
         levenshtein i j = minimum [ 1 + table!(i, j-1)
                                   , 1 + table!(i-1, j)
                                   , fromEnum (u'!(i-1) /= v'!(j-1)) + table!(i-1, j-1) ]

         u' = listArray (0, m-1) u
         v' = listArray (0, n-1) v

         m = length u
         n = length v

main = print $ distance "abbacabaab" "abaddafaca"

它的表现与原始Python代码类似:

$ time python levenshtein-original.py 
6

real    0m0.037s
user    0m0.030s
sys 0m0.000s
$ time ./LevenshteinArray 
6

real    0m0.017s
user    0m0.010s
sys 0m0.000s

答案 1 :(得分:2)

在我看来,可能的原因是使用!!随机访问。 Haskell列表是链表,它们非常适合需要随机而非顺序访问的算法。

您可能希望尝试使用更适合随机访问的内容替换列表。如果您对字符串感兴趣,可以使用Data.Text或ByteStrings,它们是基础数组,应该很快。或者你可以使用像Data.Vector这样的东西。

编辑:实际上,看起来Data.Text会有同样的问题,因为文档说索引是O(n)。可能转换为矢量最好。