web-dev-qa-db-ja.com

Pythonで文字列の2D配列の順序エンコーディングを最適化するにはどうすればよいですか?

行ごとに文字列の配列を保持するPandasシリーズがあります。

_0                                           []
1                                           []
2                                           []
3                                           []
4         [0007969760, 0007910220, 0007910309]
                          ...                 
243223                                      []
243224                            [0009403370]
243225                [0009403370, 0007190939]
243226                                      []
243227                                      []
Name: Item History, Length: 243228, dtype: object
_

私の目標は、ここではいくつかの単純な序数エンコーディングを行うことですが、次の点に注意しながら、可能な限り効率的に(時間とメモリの両方の観点から)行います。

  1. 空のリストには、「空のリスト」を表す整数も挿入する必要があります。これも一意です。 (たとえば、100個の一意の文字列がある場合、空のリストは_[101]_としてエンコードされる可能性があります)。
  2. 将来的には他のリストも同様にエンコードできるように、エンコードを保存する必要があります
  3. これらの将来のリストに、初期入力データには存在しない文字列が含まれている場合、「メイトの前に見たことがない」ことを示す独自の個別の整数をエンコードする必要があります。

明らかな質問は、「なぜsklearnのOrdinalEncoderを使用しないのか」です。まあ、不明なアイテムハンドラーがないことは別にして、この方法で行ごとに適用することも実際には非常に遅くなります(すべての個別の文字列の結合された単一の配列にそれを適合させてから、Series.apply(lambda x: oe.transform(x))を使用する必要があります各行を変換するため)、マッピングテーブル各行ごとにを作成するためにdict-comprehensionを実行する必要があるため、時間がかかります。それほど時間はありませんコールごと、わずか約0.01秒ですが、それでも、私が持っているデータの量に対しては遅すぎます。

1つの解決策は、次の関数のように、各行の部分からその口述の理解を取り、行をループする前にマッピングテーブルを作成することです。

_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行のサンプルです)。

PDATE: ctypesで動作する実装を取得できましたis行単位の.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行ですが、どちらの方法がより高速かを正確に判断するには十分です。

5
Dan Scally

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

警告はほとんどありませんが、配列ではなくリストで作業しているためと思われます。それらは無視できます。

1
Divakar

次の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のリストの成長パターン と同様に割り当てすぎて、tmpリストのサイズを変更することもできます。

1
a_guest