为啥这两种变体之间的速度差异如此之大?

Posted

技术标签:

【中文标题】为啥这两种变体之间的速度差异如此之大?【英文标题】:Why is there so much speed difference between these two variants?为什么这两种变体之间的速度差异如此之大? 【发布时间】:2019-09-13 10:56:44 【问题描述】:

版本 1:

import string, pandas as pd
def correct_contraction1(x, dic):
    for word in dic.keys():
        if word in x:
            x = x.replace(word, " " + dic[word]+ " ")
    return x

版本 2:

import string, pandas as pd
def correct_contraction2(x, dic):
    for word in dic.keys():
        if " " + word + " " in x:
            x = x.replace(" " + word + " ", " " + dic[word]+ " ")
    return x

我如何使用它们:

train['comment_text'] = train['comment_text'].apply(correct_contraction1,args=(contraction_mapping,))
#3 mins 40 sec without that space thing (version1)

train['comment_text'] = train['comment_text'].apply(correct_contraction2,args=(contraction_mapping,))
#5 mins 56 sec with that space thing (version2)

为什么会有如此大的速度差异,这不太可能是这种情况,其次是任何更好/隐藏的 pandas 技巧来进一步优化这一点? (代码已经在 Kaggle Kernels 上测试过多次)

train 是一个数据框,在这两种情况下都有 200 万行,也完全相同 contraction_mapping 是一个字典映射...(两种情况都一样) 希望有最新的熊猫。

编辑

数据来自Kaggle Comp,版本 1 更快!

【问题讨论】:

是否可以创建一些数据样本进行测试? 当然,使用的数据来自这里的kaggle comp kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification/… 在版本 1 中,您为每个“for”创建并添加字符串,但在版本 2 中,您“仅当”时才这样做? 我认为第一个版本是两者中的更快? (1) 您的代码不必要地一遍又一遍地重新计算 " " + word + " "。您不妨将空格放在字典键中。 (2) 测试if word in x: 是多余的。无论如何,对replace() 的调用必须进行存在检查。 【参考方案1】:

很抱歉没有回答差异,但在任何情况下都可以轻松改进当前的方法。它对你来说很慢,因为你必须多次扫描所有句子(每个单词)。您甚至要检查每个单词两次,首先是否存在,然后替换它 - 您可以只替换。

这是进行文本替换时的关键一课,无论是使用正则表达式、简单的字符串替换,还是开发自己的算法:尝试只检查文本一次。无论您要替换多少个单词。正则表达式有很长的路要走,但根据实现需要在找不到命中时返回几个字符。感兴趣的人:查找 trie 数据结构。

尝试一个快速文本搜索的实现(aho-corasick)。我正在为此开发一个库,但在那之前,您可以使用flashtext(它的作用略有不同):

import flashtext
# already considers word boundaries, so no need for " " + word " "
fl = flashtext.KeywordProcessor()
fl.add_keywords_from_dict(dic)

train['comment_text'] = train['comment_text'].apply(fl.replace_keywords)

如果你有很多词要替换,这会快几个数量级。

比较我能找到的第一个数据:

Words to replace: 8520
Sentences to replace in: 11230
Replacements made using flashtext: 1706
Replacements made using correct_contraction1: 25 

flashtext: (considers word boundaries and ignores case)
39 ms ± 355 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

correct_contraction1: (does not consider case nor words at end of line)
11.9 s ± 194 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

<unannounced>
30 ms ± 366 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

所以我们说的是 300 倍的加速。这不会每天都发生;-)

作为参考,Jon Clements 添加了正则表达式:

pandas.str.replace + regex (1733 replacements)
3.02 s ± 82.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

在我测试时,我的新库将再减少 30%。我也看到了 2-3 倍于 flashtext 的改进,但更重要的是,作为用户,您可以拥有更多的控制权。它功能齐全,只需要清理它并添加更多文档即可。

答案到了我会更新的!

【讨论】:

不知道为什么,但稍后会引发错误“IndexError:字符串索引超出范围”;会尝试找出原因;感谢库\ 两个版本都会扫描两次,所以这并不能解释它们之间的速度差异。 @Aditya 确保不要尝试添加我猜的空字符串? 这里是集合,以防它帮助人们将来访问此查询gist.github.com/AdityaSoni19031997/… 真正有趣的是我创建了github.com/kootenpv/contractions 可以帮助你一点哈哈。虽然一旦我的新库出现,我将采用收缩的逻辑并以更快的方式实现它。我也犯了在那里多次扫描文本的错误!请继续关注。【参考方案2】:

您最好在此处使用 Pandas 的 Series.str.replace 并根据查找表的内容为其提供编译的正则表达式。这意味着字符串替换操作可以比应用函数更快地在 Series 上工作,这也意味着您不会以字符串方式扫描,比您需要的时间要多得多......希望它可以将您的时间减少到几秒钟分钟。

import re
import pandas as pd

corrections = 
    "it's": "it is",
    "can't": "can not",
    "won't": "will not",
    "haven't": "have not"


sample = pd.Series([
    "Stays the same",
    "it's horrible!",
    "I hope I haven't got this wrong as that won't do",
    "Cabbage"
])

然后构建您的正则表达式,以便它在字典中查找任何可能的匹配项,不区分大小写并尊重单词边界:

rx = re.compile(r'(?i)\b()\b'.format('|'.join(re.escape(c) for c in corrections)))

然后应用到您的列(例如将sample 更改为training['comment_text']str.replace 传递正则表达式和一个函数,该函数接受匹配并返回找到的键的匹配值:

corrected = sample.str.replace(rx, lambda m: corrections.get(m.group().lower()))

那么您将拥有corrected 作为一个包含:

['Stays the same',
 'it is horrible!',
 'I hope I have not got this wrong as that will not do',
 'Cabbage']

注意It's 的大小写...它已被不区分大小写地提取并改为it is... 有多种方法可以保留大小写,但它可能不是非常重要并且完全是一个不同的问题。

【讨论】:

非常感谢您的回答和提示;(即将将它们转换为 re.compile 然后 re.sub) 请注意,如果关键字可以以特殊的正则表达式元字符开头/结尾,\b 将不起作用。 @WiktorStribiżew 我是re.escape'ing 所有的关键字,但我还有什么遗漏的吗? \b\( 只会匹配a( 而不会匹配a (【参考方案3】:

第二个版本必须在每次循环中执行连接" " + word + " ",当它找到匹配时,它会第二次执行替换。这让它变慢了。

您无法避免第一个串联(除非您修改 dic 以便键周围已经有空格)。但是您可以通过第一次将其保存在变量中来避免第二次串联。它仍然会比第一个版本慢,但不会慢很多。

def correct_contraction2(x, dic):
    for word in dic.keys():
        spaceword = " " + word + " "
        if spaceword in x:
            x = x.replace(spaceword, " " + dic[word]+ " ")
    return x

似乎第二个版本可能无法在所有情况下正常工作。如果单词位于一行的开头或结尾,则不会被空格包围。最好使用带有\b 的正则表达式来匹配单词边界。

【讨论】:

非常感谢您的回答和提示;

以上是关于为啥这两种变体之间的速度差异如此之大?的主要内容,如果未能解决你的问题,请参考以下文章

为啥“快速排序”算法的这两种变体在性能上差别如此之大?

为啥 Google Analytics 和 BigQuery 之间的独特事件差异如此之大?

C ++:这两种将数字写入矩阵的方式之间在速度上有显着差异吗?

自适应和响应式网页设计之间的差异

为啥 C++ 线程/未来的开销如此之大

为啥链式迭代如此复杂?简化此代码