【发布时间】:2020-03-09 15:07:30
【问题描述】:
我有一个 Pandas 系列,每行包含一个字符串数组:
0 []
1 []
2 []
3 []
4 [0007969760, 0007910220, 0007910309]
...
243223 []
243224 [0009403370]
243225 [0009403370, 0007190939]
243226 []
243227 []
Name: Item History, Length: 243228, dtype: object
我的目标是在这里做一些简单的 Ordinal Encoding,但要尽可能高效(在时间和内存方面),但要注意以下几点:
- 空列表需要插入一个表示“空列表”的整数,该整数也是唯一的。 (例如,如果有 100 个唯一字符串,则空列表可能被编码为
[101])。 - 必须以某种方式保存编码,以便将来我可以对其他列表进行相同的编码
- 如果这些未来列表包含初始输入数据中不存在的字符串,则它必须对其单独的整数进行编码以表示“在 mate 之前从未见过”。
显而易见的问题是“你为什么不只使用 sklearn 中的 OrdinalEncoder”。好吧,除了没有未知的项目处理程序之外,以这种方式逐行应用实际上也非常慢(我们必须将它放在所有不同字符串的组合单个数组中,然后使用 Series.apply(lambda x: oe.transform(x)) 转换每一行),因为它必须做一些 dict-comprehension 来为每一行构建映射表,这需要时间。 每次调用的时间并不多,只有大约 0.01 秒,但这对于我拥有的数据量来说仍然太慢了。
一种解决方案是将字典理解从每一行部分中取出,并在遍历行之前构建一个映射表,如以下函数所示:
def encode_labels(X, table, noHistory, unknownItem):
res = np.empty(len(X), dtype=np.ndarray)
for i in range(len(X)):
if len(X[i]) == 0:
res[i] = np.array([noHistory])
else:
res[i] = np.empty(len(X[i]), dtype=np.ndarray)
for j in range(len(X[i])):
try:
res[i][j] = table[X[i][j]]
except KeyError:
res[i][j] = unknownItem
return res
这比逐行的.apply() 好很多,但仍然不是最快的代码。我可以对它进行 cythonize 并进行一系列其他优化以获得更多的加速,但这并不是更好的数量级:
%%cython
cimport numpy as cnp
import numpy as np
from cpython cimport array
import array
cpdef list encode_labels_cy(cnp.ndarray X, dict table, int noHistory, int unknownItem, array.array rowLengths):
cdef int[:] crc = rowLengths
cdef list flattenedX = []
cdef Py_ssize_t i, j
cdef list row = []
for row in X:
if len(row)==0:
flattenedX.append('ZZ')
else:
flattenedX.extend(row)
cdef Py_ssize_t lenX = len(flattenedX)
cdef array.array res = array.array('i', [0]*lenX)
cdef int[:] cres = res
i=0
while i < lenX:
try:
cres[i] = table[flattenedX[i]]
except KeyError:
cres[i] = unknownItem
i += 1
cdef list pyres = []
cdef Py_ssize_t s = 0
for k in crc:
pyres.append(res[s:s+k])
s+= k
return pyres
# classes is a dict of {string:int} mappings. noHistory and unknownItem are ints encoding those values
%timeit encode_labels(X.values, classes, noHistory, unknownItem)
%timeit encode_labels_cy(X.values, classes, noHistory, unknownItem, array.array('i', [1 if x == 0 else x for x in [len(j) for j in X]]))
50.4 ms ± 2.76 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
11.2 ms ± 1.11 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
(这是针对 5000 行样本,而不是整个数据集)。
更新:我设法得到一个在 ctypes 中工作的实现,并且 比逐行的 .apply() 和我原来的原生 python 都快,但它是仍然比 Cython 慢(在我看来真的不应该这样!)
所以;我怎样才能让它更快?并且理想地同时保持内存使用尽可能低? 这不一定是纯 python。如果您可以在 Cython 或 ctypes 或其他东西中使其变得活泼,那就太好了。这段代码将构成神经网络预处理的一部分,因此此时还有一些 GPU 在等待数据;如果你能利用这些,那就更好了。多处理也可能是我还没有设法探索的一个选项,但问题是它需要每个进程的 string:int 映射表的副本,这 a) 生成速度慢 b) 使用大量内存.
编辑:
忘记提供一些数据。您可以运行以下命令来获取与我的格式相似的输入数据集:
import numpy as np
import pandas as pd
a = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
X = pd.Series([[a[np.random.randint(0, 26)] for i in range(np.random.randint(0, 10))] for j in range(5000)])
classes = dict(zip(a, np.arange(0, 26)))
unknownItem = 26
noHistory = 27
只有 5000 行,但这应该足以准确地确定哪种方法更快。
【问题讨论】:
-
基于 ctypes 的版本效率不高,因为每次迭代都会重新计算参数。您可以将
(ct.c_wchar_p*len(a))(*a)放在循环之前的临时变量中,因为a是常量。同样的事情适用于其他常量参数。此外,您可以使用更快的res.extend(cResult)。您还可以将cResult预先分配给所有行的最大大小以避免ctype 重新分配,然后使用res.extend(cResult[:len(row)])。这在我的机器上快了 4 倍。但是*row仍然是个问题。是否可以更改输入数据结构以提高效率? -
@JérômeRichard 当然,发疯。只要记忆没有膨胀成不合理的东西,我很乐意考虑几乎任何重新调整。 4 倍的速度仍然比纯 Python/Cython 版本慢,对吧?我正在尝试通过链表实现来一次处理整个输入而不是 RBAR。
-
在我的机器上,初始python版本需要27ms,优化python版本20ms,初始ctype版本5.65ms,优化ctype版本1.45。 cython 版本不构建(因为 X 的类型无效)。因此,可能值得在您的机器上进行测试。顺便说一句,C 代码也可以通过对排序键或散列使用二分法来改进,而不是遍历所有键(如果你有很多键,这很有用)。
-
@JérômeRichard 这很奇怪,我最初的 ctype 版本比最初的 Python 版本慢得多。我想知道为什么会有这样的差异
-
@JérômeRichard 哦,对于 Cython 来说,只需传递 X.values
标签: python c numpy optimization encoding