如何在Python中优化2D字符串数组的顺序编码?

如何在Python中优化2D字符串数组的顺序编码?,python,c,numpy,optimization,encoding,Python,C,Numpy,Optimization,Encoding,我有一个熊猫系列,每行包含一个字符串数组: 0 [] 1 [] 2 [] 3 [] 4 [0007969760, 0007910220, 00079

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

0                                           []
1                                           []
2                                           []
3                                           []
4         [0007969760, 0007910220, 0007910309]
                          ...                 
243223                                      []
243224                            [0009403370]
243225                [0009403370, 0007190939]
243226                                      []
243227                                      []
Name: Item History, Length: 243228, dtype: object
我的目标是在这里进行一些简单的顺序编码,但要尽可能高效(在时间和内存方面),并注意以下几点:

  • 空列表需要插入一个表示“空列表”的整数,该整数也是唯一的。(例如,如果有100个唯一字符串,空列表可能编码为
    [101]
  • 编码必须以某种方式保存,以便将来我可以对其他列表进行相同的编码
  • 如果这些未来的列表包含初始输入数据中不存在的字符串,那么它必须对自己的单独整数进行编码,以表示“在mate之前从未见过”
  • 一个显而易见的问题是“为什么你不只是使用sklearn的OrdinalCoder?”。好的,除了没有未知的项目处理程序外,以这种方式应用行方式实际上也非常慢(我们必须将其装配在所有不同字符串的组合单个数组上,然后使用
    Series.apply(lambda x:oe.transform(x))
    来转换每一行),因为它必须做一些dict理解来为每一行构建映射表,这需要时间。每次通话的时间不多,只有大约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
    
    这明显优于行方式
    .apply()
    ,但仍然不是最快的代码。我可以对其进行Cythonization并进行一系列其他优化,以获得更多的加速,但这并不是更好的数量级:

    %%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
    
    (这是针对5000行样本,而不是整个数据集)

    更新:我设法让一个实现在ctypes中工作,它比row-wise.apply()和我的原始python都快,但它仍然比Cython慢(在我看来这真的不应该是这样!)

    所以,;我怎样才能使它更快?理想情况下,同时尽可能降低内存使用率?这不一定是纯python。如果你能用Cython或ctypes之类的东西让它变得活泼,那就太好了。这段代码将构成神经网络预处理的一部分,所以此时还有一些GPU坐在那里等待数据;如果你能做到这一点,充分利用这些,那就更好了。多处理可能也是一个我还没有设法探索的选项,但问题是它需要每个进程一个字符串: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行,但这足以准确确定哪种方法更快。

    这里有一个基于-

    另一种方法基本上是从发布的问题中
    encode_标签
    ,但经过优化,对输入的访问更少,并且避免了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
    

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

    通过下面的Cython函数,我得到了大约5的加速系数。它为相关数据的行拷贝使用一个临时列表,该列表应初始化得足够大,以便能够保存每行的数据(即,如果已知每行最大元素数的上限,则使用该上限,否则使用一个启发式值,将必要的重定大小数量保持在最小)

    cpdef列表编码标签(cnp.ndarray X、dict表格、int noHistory、int unknownItem):
    cdef Py_ssize_t i,n
    cdef列表结果=[]
    cdef list tmp=[noHistory]*10#初始化足够大,以便它可能适合一行的所有元素
    对于X中的行:
    n=长(行)
    而len(tmp)0:
    i=0
    而i

    如果每行元素的数量变化很大,并且无法做出良好的估计,那么您也可以通过过度分配来调整
    tmp
    列表的大小,方法类似于。

    基于ctypes的版本效率不高,因为每次迭代都会重新计算参数。您可以在循环之前将
    (ct.c\u wchar\u p*len(a))(*a)
    放入临时变量,因为
    a
    是常量。这同样适用于其他常量参数。此外,您可以使用更快的
    res.extend(cResult)
    。您还可以将
    cResult
    预分配到所有行的最大大小,以避免ctype重新分配,然后使用
    res.extend(cResult[:len(row)])
    。这在我的机器上快了4倍。但是,
    *行
    仍然是一个问题。有可能改变输入数据结构以提高效率?@JérômeRichard当然,发疯吧。只要记忆没有膨胀成不合理的东西,我很乐意考虑几乎所有的重新调整。比纯Python/Cython版本快4倍仍然使其速度变慢,对吗?我正在尝试通过链表实现一次处理整个输入,而不是RBAR。在我的机器上,初始python版本需要27ms,优化python版本20ms,初始ctype版本5.65ms,优化ctype版本1.45。cython版本未生成(因为X的类型无效)。所以在你的机器上测试这个可能是值得的。顺便说一下,C代码也可以通过对排序键使用二分法或散列而不是对所有键进行迭代来改进(如果
    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
    
    from numba import jit
    
    @jit
    def encode_labels2(X, table, noHistory, unknownItem):
    # .. function stays the same
    
    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