python - 基于列表理解的加速功能
问题描述
我正在尝试为每个用户获取 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,值 = 一个数组
解决方案
有几件事可能会导致代码性能不佳。这些中的每一个的影响将取决于您的 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()
axis
np.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 以足够的时间使大多数事情变得更快,但是您已经有了相当快的例程,可以开箱即用地执行您想要的操作。额外的工程努力可能不值得任何潜在收益。
推荐阅读
- php - 如何计算下一个级别的剩余积分?
- ruby - 我怎样才能发现我的 ROR 应用程序正在获取它的 ENV 变量
- json - JSON 仅解析为动态类型,而不是 Map
- wordpress - 在 Wordpress 上制作可点击的图库幻灯片
- swift - 使用 applescript 将我的应用程序添加到登录项,但得到一个奇怪的权利权限问题
- javascript - 使用 Google 的 watchPosition 和 Vue.js 跟踪多个用户的位置
- javascript - Swiper不切换幻灯片
- javascript - 在 import 语句中使用解构
- kubernetes - kubeadm + calico 3.6 单节点永远没有准备好
- java - [JUnit][Mockito] 如何验证方法在调用堆栈的另一层被调用?