首页 > 解决方案 > 基于列表理解的加速功能

问题描述

我正在尝试为每个用户获取 15 个最相关的项目,但我尝试的每个功能都需要永恒。(超过 6 小时后我将其关闭......)

我有 418 个唯一用户,3718 个唯一项目。U2tfifd dict 也有 418 个条目,tfidf_feature_names 中有 32645 个单词。我的 interaction_full_df 的形状是 (40733, 3)

我试过 :

  def index_tfidf_users(user_id) : 
    return [users for users in U2tfifd[user_id].flatten().tolist()]

def get_relevant_items(user_id):
    return sorted(zip(tfidf_feature_names, index_tfidf_users(user_id)), key=lambda x: -x[1])[:15]

def get_tfidf_token(user_id) : 
    return [words for words, values in get_relevant_items(user_id)]

然后interactions_full_df["tags"] = interactions_full_df["user_id"].apply(lambda x : get_tfidf_token(x))

或者

def get_tfidf_token(user_id) : 
    tags =  []
    v = sorted(zip(tfidf_feature_names, U2tfifd[user_id].flatten().tolist()), key=lambda x: -x[1])[:15]
    for words, values in v :
        tags.append(words)
    return tags

或者

def get_tfidf_token(user_id) : 
    v = sorted(zip(tfidf_feature_names, U2tfifd[user_id].flatten().tolist()), key=lambda x: -x[1])[:15]
       tags =  [words for words in v]
    return tags

U2tfifd 是一个字典,键 = user_id,值 = 一个数组

标签: pythonpandaslist-comprehension

解决方案


有几件事可能会导致代码性能不佳。这些中的每一个的影响将取决于您的 Python 版本(2.x 或 3.x)、您的 RAM 速度等等。您需要自己对各种潜在的改进进行试验和基准测试。

1. TFIDF 稀疏性(根据稀疏性,加速约 10 倍)

一个明显的潜在问题是 TFIDF 自然地返回稀疏数据(例如,一个段落所使用的唯一单词数量不及整本书的数量),并且当数据可能几乎为零时,使用像 numpy 数组这样的密集结构是一个奇怪的选择到处。

如果您将来要进行相同的分析,则制作/使用具有稀疏数组输出的 TFIDF 版本可能会有所帮助,以便在提取标记时可以跳过零值。对于适合缓存中的每个用户,这可能具有整个稀疏数组的第二个好处,并防止在您的排序和其他操作中进行昂贵的 RAM 访问。

无论如何,稀疏化您的数据可能是值得的。在我的土豆上,一个与你的数据相似的快速基准表明该过程可以在大约 30 秒内完成。这个过程用一个高度优化的 C 编码的例程代替了你正在做的大部分工作,并包装在 Python 中使用。唯一真正的成本是通过非零条目的第二遍,但除非该遍非常有效,否则最好使用稀疏数据。

2. 重复努力和记忆(约 100 倍加速)

如果U2tfifd有 418 个条目并且interactions_full_df有 40733 行,那么您的调用中至少有 40315(或 99.0%)get_tfidf_token()被浪费了,因为您已经计算了答案。那里有大量的记忆装饰器,但你的用例不需要任何非常复杂的东西。

def memoize(f):
    _cache = {}
    def _f(arg):
        if arg not in _cache:
            _cache[arg] = f(arg)
        return _cache[arg]
    return _f

@memoize
def get_tfidf_token(user_id):
    ...

打破这一点,该函数memoize()返回另一个函数。该函数的行为是在计算预期返回值之前检查本地缓存的预期返回值,并在必要时存储它。

语法@memoize...是以下内容的缩写。

def uncached_get_tfidf_token(user_id):
    ...
get_tfidf_token = memoize(uncached_get_tfidf_token)

@符号用于表示我们想要修改或修饰的版本get_tfidf_token()而不是原始版本。根据您的应用程序,将装饰器链接在一起可能会有所帮助。

3. 矢量化操作(不同的加速,必要的基准测试)

Python 并没有像其他语言那样真正有原始类型的概念,甚至整数也会24在我的机器上占用内存中的字节。列表通常不会被打包,因此在浏览它们时可能会导致代价高昂的缓存未命中。无论 CPU 在排序等方面所做的工作有多么少,破坏一个全新的内存块以将您的数组变成一个列表,并且只使用该全新的、昂贵的内存一次会导致性能下降。

您尝试做的许多事情都有快速(SIMD 矢量化、并行化、内存高效、打包内存和其他有趣的优化)numpy 等效项,并避免不必要的数组复制和类型转换。无论如何,您似乎已经在使用 numpy,因此您不会有任何额外的导入或依赖项。

举个例子,zip()在 Python 2.x 的内存中创建另一个列表,当你真的只关心. tfidf_feature_names要计算这些索引,您可以使用类似以下的方法,它可以避免不必要的列表创建,并使用具有稍微更好的渐近复杂度的优化例程作为额外奖励。

def get_tfidf_token(user_id):
    temp = U2tfifd[user_id].flatten()
    ind = np.argpartition(temp, len(temp)-15)[-15:]
    return tfidf_feature_names[ind]  # works if tfidf_feature_names is a numpy array
    return [tfidf_feature_names[i] for i in ind]  # always works

根据 的形状,您可以通过将参数传递给并展平获得的 15 个索引来U2tfifd[user_id]避免昂贵的计算。.flatten()axisnp.argsort()

4.奖金

sorted()函数支持一个reverse参数,这样您就可以避免额外的计算,例如对每个值都抛出一个负数。只需使用

sorted(..., reverse=True)

更好的是,因为您真的不关心排序本身,而只关心您可以逃脱的 15 个最大值

sorted(...)[-15:]

索引最大的 15,而不是颠倒排序并取最小的 15。如果您为应用程序使用更好的功能,这并不重要np.argpartition(),但它在未来可能会有所帮助。

您还可以通过替换为来避免某些函数调用.apply(lambda x : get_tfidf_token(x)).apply(get_tfidf_token)因为get_tfidf_token它已经是具有预期行为的函数。你真的不需要额外的lambda

但据我所知,大多数额外的收益都是相当挑剔和依赖于系统的。例如,您可以使用 Cython 或直接 C 以足够的时间使大多数事情变得更快,但是您已经有了相当快的例程,可以开箱即用地执行您想要的操作。额外的工程努力可能不值得任何潜在收益。


推荐阅读