クラスの分布の学習

前節のブロードキャストの機能を用いると, for ループを用いなくても,複数の要素に対する演算をまとめて行うことができます. この節では,その方法を,単純ベイズの学習でのクラスの分布の計算の実装を通じて説明します. 例として, 比較演算を利用したクラスごとの事例数の計算 節で紹介した,クラス分布の計算の比較演算を用いた次の実装を,ブロードキャスト機能を用いた実装に書き換えます.

nY = np.zeros(n_classes, dtype=int)
for yi in xrange(n_classes):
    for i in xrange(n_samples):
        if y[i] == yi:
            nY[yi] += 1

書き換えの一般的な手順

for ループによる実装をブロードキャストを用いて書き換える手順について,多くの人が利用している方針は見当たりません. そこで,ここでは著者が採用している手順を紹介します.

  1. 出力配列の次元数を for ループの数とします.
  2. for ループごとに,出力配列の次元を割り当てます.
  3. 計算に必要な配列の生成します.このとき,ループ変数がループに割り当てた次元に対応するようにします.
  4. 冗長な配列を整理統合します.
  5. 要素ごとの演算をユニバーサル関数の機能を用いて実行します.
  6. np.sum() などの集約演算を適用して,最終結果を得ます.

それでは,上記のコードを例として,これらの手順を具体的に説明します.

ループ変数の次元への割り当て

手順の段階1と2により,各ループを次元に割り当てます. 例題のコードでは, for ループは2重なので,出力配列の次元数を 2 とします. ループは外側の yi と内側の i の二つで,これらに次元を一つずつ割り当てます. ここでは,第 0 次元に i のループを,第 1 次元に yi のループを割り当てておきます. 表にまとめると次のようになります.

次元 ループ変数 大きさ 意味
0 i n_samples 事例
1 yi n_classes クラス

計算に必要な配列の生成

段階3では,要素ごとの演算に必要な配列を生成します. for ループ内で行う配列の要素間演算は次の比較演算です.

y[i] == yi

左辺の y[i] と,右辺の yi に対応する配列が必要になります. これらについて,段階2で割り当てた次元にループ変数対応するようにした配列を作成します.

左辺の y[i] では,ループ変数 i で指定した位置の配列 y の値が必要になります. このループ変数 i に関するループを見てみます.

for i in xrange(n_samples):

このループ変数 i0 から n_samples - 1 までの整数をとります. これらの値を含む配列は np.arange(n_samples) により生成できます. 次に,これらの値が,ループ変数 i に段階2で割り当てた次元 0 の要素になり,他の次元の大きさは 1 になるようにします. これは, 配列の次元数や大きさの操作 で紹介した shape の操作技法を用いて次のように実装できます.

ary_i = np.arange(n_samples)[:, np.newaxis]

第0次元の : により, np.arange(n_samples) の内容を第0次元に割り当て,第1次元は np.newaxis により大きさ 1 となるように設定します.

ループ変数 i で指定した位置の配列 y の値 y[i] は次のコードにより得ることができます.

ary_y = y[ary_i]

このコードにより, ary_i と同じ shape で,その要素が y[i] であるような配列を得ることができます.

右辺のループ変数 yi についての次のループも同様に処理します.

for yi in xrange(n_classes):

この変数は 0 から n_classes - 1 までの整数をとり,第1次元に割り当てられているので,この変数に対応する配列は次のようになります.

ary_yi = np.arange(n_classes)[np.newaxis, :]

第0次元には大きさ 1 の次元を設定し,第1次元の要素には np.arange(n_classes) の内容を割り当てています. 以上で,比較演算に必要な配列 ary_yary_yi が得られました.

冗長な配列の整理

段階4では,冗長な配列を整理します. ary_y は, ary_i を展開すると次のようになります.

ary_y = y[np.arange(n_samples)[:, np.newaxis]]

配列の shape を変えてから y 中の値を取り出す代わりに,先に y の値を取り出してから shape を変更するようにすると次のようになります.

ary_y = (y[np.arange(n_samples)])[:, np.newaxis]

ここで y の大きさは n_samples であることから, y[np.arange(n_samples)]y そのものです. このことをふまえると ary_y は,次のように簡潔に生成できます.

ary_y = y[:, np.newaxis]

以上のことから, ary_i を生成することなく目的の ary_y を生成できるようになりました.

この冗長なコードの削除は次のループの書き換えと対応付けて考えると分かりやすいかもしれません. 次のループ変数 i を使って y 中の要素を取り出すコード

for i in xrange(n_samples):
    val_y = y[i]

は, for ループで y の要素を順に参照する次のコードと同じ val_y の値を得ることができます.

for val_y in y:
    pass

これらのコードは,それぞれ,ループ変数配列を用いた y[ary_i]y の値を直接参照する y[:, np.newaxis] とに対応します.

要素ごとの演算と集約演算

段階5では要素ごとの演算を行います. 元の実装では要素ごとの演算は y[i] == yi の比較演算だけでした. この比較演算を,全ての iyi について実行した結果をまとめた配列は次のコードで計算できます.

cmp_y = (ary_y == ary_yi)

ary_yary_yishape はそれぞれ (n_samples, 1)(1, n_classes) で一致していません. しかし,ブロードキャストの機能により, ary_y[:, 0] の内容と, ary_yi[0, :] の内容を,繰り返して比較演算利用するため,明示的に繰り返しを記述しなくても目的の結果を得ることができます.

最後の段階6は集約演算です. 集約 (aggregation) とは,複数の値の代表値,例えば総和,平均,最大などを求めることです. ユニバーサル関数の利用 で述べたように,比較結果が真である組み合わせは np.sum() によって計算できます. ここで問題となるのは,単純に np.sum(cmp_y) とすると配列全体についての総和になってしまいますが,計算したい値は yi がそれぞれの値をとるときの,全ての事例についての和でであることです. そこで, np.sum() 関数の axis 引数を指定します. ここでは,事例に対応するループ変数 i を次元0に割り当てたので, axis=0 と指定します.

nY = np.sum(cmp_y, axis=0)

以上の実装をまとめて書くと次のようになります.

ary_y = y[:, np.newaxis]
ary_yi = np.arange(n_classes)[np.newaxis, :]
cmp_y = (ary_y == ary_yi)
nY = np.sum(cmp_y, axis=0)

途中での変数への代入をしないようにすると,次の1行のコードで同じ結果を得ることができます.

nY = np.sum(y[:, np.newaxis] == np.arange(n_classes)[np.newaxis, :],
            axis=0)

クラスの確率の計算

NaiveBayes1 の実装では,各クラスごとの標本数 nY を,総標本数 n_samples で割って,クラスの確率を計算しました.

self.pY_ = np.empty(n_classes, dtype=float)
for i in xrange(n_classes):
    self.pY_[i] = nY[i] / float(n_samples)

この処理も,ユニバーサル関数の機能を使うと次のように簡潔に実装できます.

self.pY_ = np.true_divide(nY, n_samples)

Python では整数同士の割り算の解は切り捨ての整数になります [1] . しかし,ここでは実数の解を得たいので np.true_divide() 関数を用いて,切り捨てではない実数の解を得ます.

np.true_divide(x1, x2[, out]) = <ufunc 'true_divide'>

Returns a true division of the inputs, element-wise.

この関数はユニバーサル関数なので, nY の各要素は,それぞれ n_samples で割られます.

[1]

逆に浮動小数点に対する場合でも,切り捨てした割り算の結果を得るには np.floor_divide() を用います.

np.floor_divide(x1, x2[, out]) = <ufunc 'floor_divide'>

Return the largest integer smaller or equal to the division of the inputs.