【问题标题】:How can I optimise the ordinal encoding of a 2D array of strings in Python?如何在 Python 中优化二维字符串数组的序数编码?
【发布时间】: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,但要尽可能高效(在时间和内存方面),但要注意以下几点:

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


【解决方案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

另一种方法基本上是来自已发布问题的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

那么,我们就可以介绍numba的jit编译了。因此,更改将是 -

from numba import jit

@jit
def encode_labels2(X, table, noHistory, unknownItem):
# .. function stays the same

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

【讨论】:

  • 谢谢,运行起来了,但它的启动时间大约为 100 毫秒,而当前最佳努力的 6-7 毫秒。
  • 约11ms;明显好于现有的 Cython 版本,但仍略慢。不过对于纯 python 来说相当快!
  • @DanScally 然后 numba 的添加(编辑)应该会有所帮助。
  • 抱歉,我错过了编辑。 numba 版本 明显更快......这让我很恼火,因为我发誓我之前尝试过它并得出结论,在这种特定情况下它比 Cython 更糟糕!但是谢谢;这是迄今为止最快的解决方案。
  • 再次感谢;最快的方式和赏金期已经到了,所以你得到了勾号:)
【解决方案2】:

使用以下 Cython 函数,我得到了大约 5 倍的加速因子。它使用一个临时列表来存储相关数据的逐行副本,这些数据应该被初始化得足够大,以便它可以容纳每一行的数据(即,如果每行最大元素数的上限是已知的,请使用该上限,否则请使用启发式值,以将必要的调整大小数量保持在最低限度。

cpdef list encode_labels_cy_2(cnp.ndarray X, dict table, int noHistory, int unknownItem):

    cdef Py_ssize_t i, n
    cdef list result = []
    cdef list tmp = [noHistory] * 10  # initialize big enough so that it's likely to fit all elements of a row

    for row in X:
        n = len(row)
        while len(tmp) < n:  # if too small, resize
            tmp.append(noHistory)
        if n > 0:
            i = 0
            while i < n:
                tmp[i] = table.get(row[i], unknownItem)
                i += 1
        else:
            tmp[0] = noHistory
            i = 1
        result.append(tmp[:i])

    return result

如果每行的元素数量变化很大,并且无法在启动时做出好的估计,您还可以通过类似于CPython's list growth pattern 的过度分配来调整tmp 列表的大小。

【讨论】:

  • 我得到了与现有 cython 相同的 5 倍加速;谢谢你!令人遗憾的是,元素的数量确实变化很大,我将测试计算最大值(已知但本身可能是一个稍微昂贵的操作)和过度分配,看看是什么样子。
  • @DanScally 首先计算整个数据的最大值会非常昂贵,应该避免这种情况。相反,只需使用任何大数字。我期待最大值。行的长度要明显小于数据帧的长度,因此即使您选择比训练数据最大值大 10 或 100 的因子。它不会以明显的方式影响内存。这是很重要的一点,因此它可能有助于分析训练数据的长度分布。你有任何数字吗(例如平均值、标准差、最大值、直方图等)?
猜你喜欢
  • 1970-01-01
  • 2018-03-02
  • 1970-01-01
  • 2016-11-12
  • 2015-08-27
  • 2020-06-09
  • 2011-06-04
  • 2021-01-28
  • 2015-10-10
相关资源
最近更新 更多