编辑距离成本
我是 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)
现在,Python 和 Haskell 之间长度为 10 或以上的字符串的执行时间差异为 10 的不同次方。另外,通过一些粗略的时间测量(挂钟,因为到目前为止我还没有在 haskell 中找到 Clock() 命令),看来我的 haskell 实现成本不是 O(mn),而是其他一些快速增长的成本。
注意:我不希望我的 haskell 实现与 python 脚本竞争速度。我只是希望它在“合理”的时间内运行,而不是整个宇宙存在的时间的倍数。
问题:
- 我做错了什么,我的实施速度这么慢?
- 如何修复它?
- 谈论“惰性求值”:我认为如果
levenshtein "cat" "kit" 2 2 被调用三次,则只计算一次。这是对的吗?
- 我的 bool2int 一定有内置的东西,对吗?
- 任何其他输入如果能推动我在掌握 Haskell 的艰难道路上前进,我都会非常感激。
编辑:这里是用于比较的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)
I am new to haskell and I encountered a performance issue that is so grave that it must be my code and not the haskell platform.
I have a python implementation of the Levenshtein distance (own code) and I passed (or tried to do so) this to haskell. The result is the following:
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)
Now, the difference in execution time for strings of length 10 or more is of various powers of 10 between python and haskell. Also with some rough time measuring (wall clock, as I haven't found a clock() command in haskell so far) it seems that my haskell implementation has not cost O(mn), but some other exorbitantly fast growing cost.
Nota bene: I do not want my haskell implementation to compete speed wise with the python script. I just want it to run in a "sensible" time and not in multiples of the time the whole universe exists.
Questions:
- What am I doing wrong, that my implementation is so darn slow?
- How to fix it?
- Talking about "lazy evaluation": I gather that if
levenshtein "cat" "kit" 2 2
is called thrice, it is only calculated once. Is this right? - There must be something built-in for my bool2int, right?
- Any other input is highly appreciated if it shoves me ahead on the rough path to mastering haskell.
EDIT: Here goes the python code for comparison:
#! /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)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
您的算法具有指数复杂度。您似乎假设正在为您记住这些电话,但事实并非如此。
您需要添加显式记忆,可能使用数组或其他方法。
不,Haskell 不进行自动记忆。惰性意味着,如果您执行
let y = fx in y + y
,则仅在需要求和结果时才对f x
求值(一次)。它不意味着fx + f x
将仅在对f x
的一次调用中求值。当您想要共享子表达式的结果时,您必须明确。是的,有一个
instance Enum Bool
,因此您可以使用fromEnum
。虽然从头开始编写东西可能很有趣且具有教育意义,但学习利用 Hackage< 上的许多优秀库非常重要/a> 当做这样的常见事情时。
例如,edit-distance 包中有 Levenshtein 距离的实现。
我将你的 Haskell 代码翻译回 Python 进行比较:
即使没有修复 的 O(n) 索引问题chrisdb 在他的回答中指出,这在编译时比 Haskell 版本慢:
当然,它们都输给了正确记忆的版本edit-distance 包:
这是一个使用
Data.Array
的简单记忆实现:它的执行方式与原始 Python 代码类似:
Your algorithm has exponential complexity. You seem to be assuming that the calls are being memoized for you, but that's not the case.
You'll need to add explicit memoization, possibly using an array or some other method.
No, Haskell does not do automatic memoization. Laziness means that if you do
let y = f x in y + y
, then thef x
will only be evaluated (once) if the result of the sum is demanded. It does not mean thatf x + f x
will evaluate in only one call tof x
. You have to be explicit when you want to share results from subexpressions.Yes, there is an
instance Enum Bool
, so you can usefromEnum
.While writing stuff from scratch may be fun and educational, it is important to learn to take advantage of the many great libraries on Hackage when doing common things like this.
For example there is an implementation of the Levenshtein distance in the edit-distance package.
I translated your Haskell code back to Python for comparison:
Even without fixing the O(n) indexing issue that chrisdb pointed out in his answer, this performs slower than the Haskell version when compiled:
Of course, they both lose to the properly memoized version in the edit-distance package:
Here's a simple memoized implementation using
Data.Array
:It performs similarly to your original Python code:
在我看来,可能的原因是使用 !!用于随机访问。 Haskell 列表是链表,不太适合需要随机访问而不是顺序访问的算法。
您可能想尝试用更适合随机访问的内容替换列表。如果您对字符串感兴趣,您可以使用 Data.Text 或 ByteStrings,它们本质上是数组,并且应该很快。或者你可以使用类似 Data.Vector 的东西。
编辑:实际上,看起来 Data.Text 也会有同样的问题,因为文档说索引是 O(n) 。可能转换为向量是最好的。
It looks to me like the likely cause is the use of !! for random access. Haskell lists are linked lists, which are poorly suited to algorithms that require random rather than sequential access.
You might want to try replacing the lists with something better suited to random access. If you are interested in strings you could use Data.Text or ByteStrings, which are underlyingly arrays and should be fast. Or you could use something like Data.Vector perhaps.
EDIT: Actually, it looks like Data.Text will have the same issue, since the documentation says indexing is O(n). Probably converting to a vector would be best.