【问题标题】:Python performance improvement request for winklerwinkler的Python性能提升请求
【发布时间】:2011-02-14 01:45:55
【问题描述】:

我是一个 python n00b,我想要一些关于如何改进算法的建议,以提高这种计算两个名字的 Jaro-Winkler 距离的方法的性能。

def winklerCompareP(str1, str2):
"""Return approximate string comparator measure (between 0.0 and 1.0)

USAGE:
  score = winkler(str1, str2)

ARGUMENTS:
  str1  The first string
  str2  The second string

DESCRIPTION:
  As described in 'An Application of the Fellegi-Sunter Model of
  Record Linkage to the 1990 U.S. Decennial Census' by William E. Winkler
  and Yves Thibaudeau.

  Based on the 'jaro' string comparator, but modifies it according to whether
  the first few characters are the same or not.
"""

# Quick check if the strings are the same - - - - - - - - - - - - - - - - - -
#
jaro_winkler_marker_char = chr(1)
if (str1 == str2):
    return 1.0

len1 = len(str1)
len2 = len(str2)
halflen = max(len1,len2) / 2 - 1

ass1  = ''  # Characters assigned in str1
ass2  = '' # Characters assigned in str2
#ass1 = ''
#ass2 = ''
workstr1 = str1
workstr2 = str2

common1 = 0    # Number of common characters
common2 = 0

#print "'len1', str1[i], start, end, index, ass1, workstr2, common1"
# Analyse the first string    - - - - - - - - - - - - - - - - - - - - - - - - -
#
for i in range(len1):
    start = max(0,i-halflen)
    end   = min(i+halflen+1,len2)
    index = workstr2.find(str1[i],start,end)
    #print 'len1', str1[i], start, end, index, ass1, workstr2, common1
    if (index > -1):    # Found common character
        common1 += 1
        #ass1 += str1[i]
        ass1 = ass1 + str1[i]
        workstr2 = workstr2[:index]+jaro_winkler_marker_char+workstr2[index+1:]
#print "str1 analyse result", ass1, common1

#print "str1 analyse result", ass1, common1
# Analyse the second string - - - - - - - - - - - - - - - - - - - - - - - - -
#
for i in range(len2):
    start = max(0,i-halflen)
    end   = min(i+halflen+1,len1)
    index = workstr1.find(str2[i],start,end)
    #print 'len2', str2[i], start, end, index, ass1, workstr1, common2
    if (index > -1):    # Found common character
        common2 += 1
        #ass2 += str2[i]
        ass2 = ass2 + str2[i]
        workstr1 = workstr1[:index]+jaro_winkler_marker_char+workstr1[index+1:]

if (common1 != common2):
    print('Winkler: Wrong common values for strings "%s" and "%s"' % \
                (str1, str2) + ', common1: %i, common2: %i' % (common1, common2) + \
                ', common should be the same.')
    common1 = float(common1+common2) / 2.0    ##### This is just a fix #####

if (common1 == 0):
    return 0.0

# Compute number of transpositions    - - - - - - - - - - - - - - - - - - - - -
#
transposition = 0
for i in range(len(ass1)):
    if (ass1[i] != ass2[i]):
        transposition += 1
transposition = transposition / 2.0

# Now compute how many characters are common at beginning - - - - - - - - - -
#
minlen = min(len1,len2)
for same in range(minlen+1):
    if (str1[:same] != str2[:same]):
        break
same -= 1
if (same > 4):
    same = 4

common1 = float(common1)
w = 1./3.*(common1 / float(len1) + common1 / float(len2) + (common1-transposition) / common1)

wn = w + same*0.1 * (1.0 - w)
return wn

示例输出

ZIMMERMANN  ARMIENTO    0.814583333
ZIMMERMANN  ZIMMERMANN  1
ZIMMERMANN  CANNONS         0.766666667
CANNONS AKKER           0.8
CANNONS ALDERSON    0.845833333
CANNONS ALLANBY         0.833333333

【问题讨论】:

  • 如果您有一些示例可以帮助我们处理...一些名称对和正确的分数,那将会很有帮助。否则很难知道我们的编辑没有破坏功能。
  • 感谢您编辑问题。我会得到这些例子。
  • 我没有得到与您的示例案例相同的输出(除非字符串完全相同),但对于我从 Jaro-Winkler 距离尝试的示例,w 是正确的维基百科页面。
  • 我的输出有误,我会尽快修复。太好了
  • 感谢所有 cmets。最后我用C重写了它;快得多。

标签: python optimization performance jaro-winkler


【解决方案1】:

我更关注优化以从 Python 中获得更多收益,而不是优化算法,因为我认为这里没有太多的算法改进。以下是我提出的一些 Python 优化。

(1)。由于您似乎使用的是 Python 2.x,请将所有 range() 更改为 xrange()。 range() 在迭代它们之前生成完整的数字列表,而 xrange 根据需要生成它们。

(2)。对 max 和 min 进行以下替换:

start = max(0,i-halflen)

start = i - halflen if i > halflen else 0

end = min(i+halflen+1,len2)

end = i+halflen+1 if i+halflen+1 < len2 else len2

在第一个循环和第二个循环类似的循环中。还有一个更远的 min() 和一个靠近函数开头的 max() ,所以对它们做同样的事情。更换 min() 和 max() 确实有助于减少时间。这些是方便的功能,但比我替换它们的方法成本更高。

(3)。使用 common1 代替 len(ass1)。您已经在 common1 中跟踪了 ass1 的长度,所以让我们使用它而不是调用昂贵的函数来再次找到它。

(4)。替换以下代码:

minlen = min(len1,len2)
for same in xrange(minlen+1):
    if (str1[:same] != str2[:same]):
        break
same -= 1

for same in xrange(minlen):
    if str1[same] != str2[same]:
        break

这样做的原因主要是 str1[:same] 每次循环都会创建一个新字符串,您将检查已经检查过的部分。另外,如果不需要,也不需要检查'' != '' 和减少same

(5)。使用psyco,一种即时编译器。下载并安装后,只需添加行

import psyco
psyco.full()

在文件的顶部使用它。除非您进行我提到的其他更改,否则不要使用 psyco。出于某种原因,当我在您的原始代码上运行它时,它实际上减慢了它的速度。

使用 timeit,我发现前 4 次更改的时间减少了大约 20% 左右。但是,当我将 psyco 与这些更改一起添加时,代码比原始代码快了大约 3 到 4 倍。

如果你想要更快的速度

相当多的剩余时间在字符串的 find() 方法中。我决定尝试用我自己的替换它。对于第一个循环,我替换了

index = workstr2.find(str1[i],start,end)

index = -1
for j in xrange(start,end):
    if workstr2[j] == str1[i]:
        index = j
        break

和第二个循环的类似形式。没有 psyco,这会减慢代码的速度,但是使用 psyco,它会加快很多。通过最后的更改,代码比原始代码快了大约 8 到 9 倍。

如果这还不够快

那么你可能应该转向制作一个 C 模块。

祝你好运!

【讨论】:

  • 在没有 pysco 的情况下,这些更改将我的测试脚本的性能提高了 25%。谢谢!
  • xrange v range 使用 python 2.6.5 似乎没有多大作用
  • 是的,也许是这样。我还没有真正看过 2.6.5 中的变化。另外,我们没有使用很大的范围,所以它不应该有很大的影响。使用 xrange 的原因是在迭代它之前不会在内存中形成整个列表。相反,它会根据需要生成每个元素。
【解决方案2】:

除了贾斯汀所说的一切之外,连接字符串的成本很高 - python 必须为新字符串分配内存,然后将两个字符串都复制到其中。

所以这很糟糕:

ass1 = ''
for i in range(len1):
     ...
    if (index > -1):    # Found common character
        ...
        ass1 = ass1 + str1[i]

创建 ass1 和 ass2 字符列表并使用ass1.append(str1[i]) 可能会更快。从我对代码的快速阅读中可以看出,之后您对 ass1 和 ass2 所做的唯一事情就是逐个字符地遍历它们,因此它们不需要是字符串。如果您稍后确实需要将它们用作字符串,则可以使用''.join(ass1) 进行转换。

【讨论】:

  • 我尝试制作 ass1 和 ass2 列表,但它减慢了速度,这让我很吃惊。我还尝试制作 workstr1 和 workstr2 列表,这也减慢了我的速度。我真的认为至少其中一个会有所帮助,但是字符串连接必须非常快或其他什么。
  • 我在执行此操作时也没有注意到任何加速
【解决方案3】:

我想象如果你使用pylevenshtein模块,你可以做得更好。对于大多数用例而言,这是C的。它包含一个 jaro-winkler 函数,它提供相同的输出,但在我的机器上它快 63 倍。

In [1]: import jw

In [2]: jw.winklerCompareP('ZIMMERMANN', 'CANNONS')
Out[2]: 0.41428571428571426

In [3]: timeit jw.winklerCompareP('ZIMMERMANN', 'CANNONS')
10000 loops, best of 3: 28.2 us per loop

In [4]: import Levenshtein

In [5]: Levenshtein.jaro_winkler('ZIMMERMANN', 'CANNONS')
Out[5]: 0.41428571428571431

In [6]: timeit Levenshtein.jaro_winkler('ZIMMERMANN', 'CANNONS')
1000000 loops, best of 3: 442 ns per loop

【讨论】:

  • pypy 1.6 可以使原始代码(实际上取自 Febrl:sf.net/projects/febrl)运行速度提高 10 倍。只是给它一点时间:) span>
  • 我最终使用 C 来制作一个速度快很多倍的 Python 模块。并修复了一个逻辑错误。感谢您的建议。
猜你喜欢
  • 1970-01-01
  • 2014-11-11
  • 2021-07-23
  • 1970-01-01
  • 2010-09-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多