単純ベイズの実装 (2)

単純ベイズ:入門編学習メソッドの実装(1) 節では,NumPy 配列を単なる多次元配列として利用し, for ループで,訓練データを数え挙げて,単純ベイズの学習を実装しました. ここでは, np.sum() 関数などを用いて,NumPy の利点を生かして実装をします.

予測メソッドの実装の準備

それでは, NaiveBayes1 クラスとは,学習メソッドの実装だけが異なる NaiveBayes2 クラスの作成を始めます. コンストラクタや予測メソッドは NaiveBayes1 クラスと共通なので,この NaiveBayes2 クラスも,抽象クラス BaseBinaryNaiveBayes の下位クラスとして作成します. クラスの定義と,コンストラクタの定義は,クラス名を除いて NaiveBayes1 クラスと同じです.

class NaiveBayes2(BaseBinaryNaiveBayes):
    """
    Naive Bayes class (2)
    """

    def __init__(self):
        super(NaiveBayes2, self).__init__()

学習を行う fit() メソッドも,引数などの定義は NaiveBayes1 クラスのそれと全く同じです. さらに,サンプル数 n_samples などのメソッド内の定数の定義も, 定数の設定 節で述べたものと共通です.

比較演算を利用したクラスごとの事例数の計算

単純ベイズ:カテゴリ特徴の場合 の式(4)のクラスの分布のパラメータを求めるために,各クラスごとの事例数を NaiveBayes1 クラスでは,次のように求めていました.

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

この実装では,クラスの対応する添え字の要素のカウンタを一つずつ増やしていました. これを,各クラスごとに,現在の対象クラスの事例であったらなら対応する要素のカウンタを一つずつ増やす実装にします.

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

外側のループの添え字 yi は処理対象のクラスを指定し,その次のループの添え字 i は処理対象の事例を指定しています. ループの内部では,対象事例のクラスが,現在の処理対象クラスであるかどうかを,等号演算によって判定し,もし結果が真であれば,対応するカウンタの値を一つずつ増やしています.

ユニバーサル関数の利用

このコードの中で,内側のループでは全ての事例について等号演算を適用していますが,これを,ユニバーサル関数の機能を利用してまとめて処理します. 等号演算 == を適用すると,次の関数が実際には呼び出されます.

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

Return (x1 == x2) element-wise.

この関数は x1x2 を比較し,その真偽値を論理型で返します. out が指定されていれば,結果をその配列に格納し,指定されていなければ結果を格納する配列を新たに作成します.

この関数はユニバーサル関数であるため,y == yi を実行すると,配列 y 各要素と,添え字 yi とを比較した結果をまとめた配列を返します. すなわち, y の要素が yi と等しいときには True ,それ以外は False を要素とする配列を返します.

この比較結果を格納した配列があれば,このうち True の要素の数を数え挙げれば,クラスが yi に等しい事例の数が計算できます. この数え挙げには,合計を計算する np.sum() を用います. 論理型の定数 True は,整数型に変換すると 1 に,もう一方の False は変換すると 0 になります. このことを利用すると, np.sum()y == yi に適用することで,配列 y のうち,その値が yi に等しい要素の数が計算できます.

以上のことを利用して,各クラスごとの事例数を数え挙げるコードは次のようになります.

nY = np.empty(n_classes, dtype=int)
for yi in xrange(n_classes):
    nY[yi] = np.sum(y ==yi)

なお,配列 nY0 で初期化しておく必要がなくなったので, np.zeros() ではなく, np.empty() で作成しています.

配列要素の一括処理の試み

コードは簡潔になりましたが,まだクラスについてのループが残っていますので,さらにこれを簡潔に記述できるか検討します. ここで, 対数同時確率の計算 節の 方針(2) で紹介した,配列の要素をまとめて処理する技法を利用します. これは,ループの添え字がとりうる値をまとめた配列を np.arange() 関数によって作成し,対応する添え字がある部分と置き換えるというものでした.

では,添え字 yi について検討します. この変数は,ループ内で 0 から n_classes - 1 まで変化するので, np.arange(n_classes) により,それらの値をまとめた配列を作成できます. この配列を導入した,クラスごとの事例数の数え挙げのコードは次のようになります.

nY = np.sum(y == np.arange(n_classes))

しかし,このコードは期待した動作をしません. ここでは, y 内の要素それぞれが, yi 内の要素それぞれと比較され,それらの和が計算されることを期待していました. しかし, yyi も共に1次元の配列であるため,単純に配列の最初から要素同士を比較することになってしまいます. この問題を避けて, y の各要素と yi 内の各要素をそれぞれ比較するには,それぞれの配列を2次元にして,ブロードキャスト (broadcasting) という機能を利用する必要があります. 次の節では,このブロードキャストについて説明します.