メモリに乗り切らない大量のデータの平均値と標準偏差をWelfordアルゴリズムで逐次計算する

Kaggleに取り組んでいた時、大量のデータセットが与えられた。

一般的に、ニューラルネットに入力するデータは標準化している方がいいと言われているが、データが大量にあってメモリに乗り切らず、sklearn.preprocessing.StandardScalerが使えない。

StandardScaler
Gallery examples: Release Highlights for scikit-learn 1.5 Release Highlights for scikit-learn 1.4 Release Highlights for...

一度に全ての平均値を計算するのではなく、メモリに乗る量のデータを入力し、逐次的に計算できるような(オンライン処理できるような)アルゴリズムがないか調べた所、Welfordのアルゴリズムというのがあった。

Algorithms for calculating variance - Wikipedia

平均値の計算式を以下のように式変形する。

\begin{eqnarray}
\overline{x}_{n} &=& \frac{1}{n}\sum ^{n}_{i=1}x_{i} \\
\\
&=& \frac{(n-1)\overline{x}_{n-1}+{x}_{n}}{n} \\
\\
&=& \overline{x}_{n-1} + \frac{{x}_{n} – \overline{x}_{n-1}}{n}
\end{eqnarray}

新しいデータを加えて平均値を更新するためには、1つ前の平均値と新しい値さえあれば計算できることがわかる。これはO(1)の計算量で更新できそう。

同様に式変形をすれば、逐次更新に使う新しい値が1つでも複数個でも可能になる。

Pythonで書くと以下の通りシンプルになる。

mean += (x - mean)/k

標準偏差は分散の平方根で求められるので、分散を逐次更新で求める方法を考える。

分散は以下の計算式で求められる。2項目の平均値は上述の式で求められるので、1項目の二乗の平均値がわかればいい。

\begin{eqnarray}
{σ\;}^{ 2} = \overline{x^2} \;\; – \;\;\overline{x}^{\;\;2}
\end{eqnarray}

以上の考察をもとに、バッチ単位でデータを読み込み、逐次更新で平均値と標準偏差を求めていく。

今回は3つの特徴量の平均と分散を求める例を示す。

num_feature = 3

mean = np.zeros(num_feature, dtype=np.float64)
sum_of_square = np.zeros(num_feature, dtype=np.float64)

count = 0

for i in range(num_batch):
    batch = read_batch(...)
    count += len(batch)

    new_val = batch_to_ndarray(...)

    mean += np.sum(new_val - mean, axis=0) / count

    sum_of_square += np.sum(new_val ** 2, axis=0)
    var = (sum_of_square / count) - mean ** 2
    std = np.sqrt(var)

print(mean)
print(std)

fp32で収まらない値になるかもしれないので、fp64で値を保存する配列を初期化。

countでデータの個数を保存しつつ、上述の数式に従って値を更新していく。

read_batch()でメモリに乗る分だけデータを読み込み、batch_to_ndarray()でnumpy形式の配列に変更する。

ここは各自の関数を書く。返り値の次元数は(batch_size, num_feature)になればOK。

numpyで処理すればforループを無くせるので高速。TensorFlowで書き換えればGPUでの処理も可能。

結果はnp.saveなりpd.DataFrameなりにして保存し忘れないように注意。

これで、メモリの乗り切らない大量のデータから平均値と標準偏差を求めることができ、ニューラルネットに入力するデータの前処理を行うことができた。

コメント

タイトルとURLをコピーしました