如何优化 Python 中二维字符串数组的序数编码?

Dan*_*lly 5 c python optimization encoding numpy

我有一个 Pandas 系列,每行包含一个字符串数组:

0                                           []
1                                           []
2                                           []
3                                           []
4         [0007969760, 0007910220, 0007910309]
                          ...                 
243223                                      []
243224                            [0009403370]
243225                [0009403370, 0007190939]
243226                                      []
243227                                      []
Name: Item History, Length: 243228, dtype: object
Run Code Online (Sandbox Code Playgroud)

我的目标是在这里做一些简单的序数编码,但要尽可能高效(在时间和内存方面),但有以下注意事项:

  1. 空列表需要插入一个表示“空列表”的整数,该整数也是唯一的。(例如,如果有 100 个唯一字符串,空列表可能会被编码为[101])。
  2. 必须以某种方式保存编码,以便我将来可以对其他列表进行相同的编码
  3. 如果这些未来列表包含初始输入数据中不存在的字符串,则它必须编码自己的单独整数以表示“在配对之前从未见过”。

显而易见的问题是“你为什么不只使用 sklearn 的 OrdinalEncoder”。好吧,除了没有未知的项目处理程序之外,以这种方式按行应用实际上也非常慢(我们必须将它放在所有不同字符串的组合单个数组上,然后用于Series.apply(lambda x: oe.transform(x))转换每一行),因为它必须做一些字典理解来为每一行构建映射表,这需要时间。每次通话的时间不是很多,只有大约 0.01 秒,但是对于我拥有的数据量来说,这仍然太慢了。

一种解决方案是从每一行部分中取出 dict 理解,并在遍历行之前构建一个映射表,如在此函数中所示:

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
Run Code Online (Sandbox Code Playgroud)

这明显优于 row-wise.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
Run Code Online (Sandbox Code Playgroud)
# 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)
Run Code Online (Sandbox Code Playgroud)

(这是 5000 行样本,而不是整个数据集)。

更新:我设法得到一个实现在ctypes的工作,那不是都在行。适用()和我原来的本机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
Run Code Online (Sandbox Code Playgroud)

只有 5000 行,但这应该足以准确确定哪种方法更快。

Div*_*kar 1

这是一个基于NumPy's searchsorted-

k,v = classes.keys(),classes.values()
k,v = np.array(list(k)),np.array(list(v))

cl = np.concatenate(s)

sidx = k.argsort()
idx = np.searchsorted(k,cl, sorter=sidx)
out_of_bounds_mask = idx==len(k)

idx[out_of_bounds_mask] = 0
ssidx = sidx[idx]
invalidmask = k[ssidx] != cl
out_of_bounds_mask |= invalidmask

vals = v[ssidx]
vals[out_of_bounds_mask] = unknownItem

lens = list(map(len,s))
E = [noHistory] # use np.array() if you need outputs for empty entries as arrays
out = []
start = 0
for l in lens:
    if l==0:
        out.append(E)
    else:
        out.append(vals[start:start+l])
        start += l
Run Code Online (Sandbox Code Playgroud)

另一种方法基本上encode_labels来自发布的问题,但经过优化,减少了对输入的访问并避免了 try-catch -

def encode_labels2(X, table, noHistory, unknownItem):
    L0 = len(X)
    res = [[noHistory]]*L0
    for i in range(L0):
        L = len(X[i])
        if L != 0:
            res_i = [unknownItem]*L
            for j in range(L):
                Xij = X[i][j]
                if Xij in table:
                    res_i[j] = table[Xij]
            res[i] = res_i
    return res
Run Code Online (Sandbox Code Playgroud)

然后,我们就可以介绍numba的jit编译了。所以,变化是——

from numba import jit

@jit
def encode_labels2(X, table, noHistory, unknownItem):
# .. function stays the same
Run Code Online (Sandbox Code Playgroud)

几乎不会有警告,这似乎是因为我们使用的是列表而不是数组。这些可以忽略不计。