人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

Kerasによる多クラス分類(Iris)

今回は、機械学習でよく使われるIrisデータセット多層パーセプトロンで分類してみた(ありがち)。Irisデータセットのクラスラベルは3つ(setosa, versicolor, virginica)あるので前回までと違って多クラス分類になる。短いプログラムなので全部載せてポイントだけまとめておこう。

f:id:aidiary:20161108213705j:plain:w400

ソースコード

import numpy as np

from sklearn import datasets
from sklearn.model_selection import train_test_split

from keras.models import Sequential
from keras.layers.core import Dense, Activation
from keras.utils import np_utils
from sklearn import preprocessing

def build_multilayer_perceptron():
    """多層パーセプトロンモデルを構築"""
    model = Sequential()
    model.add(Dense(16, input_shape=(4, )))
    model.add(Activation('relu'))
    model.add(Dense(3))
    model.add(Activation('softmax'))
    return model

if __name__ == "__main__":
    # Irisデータをロード
    iris = datasets.load_iris()
    X = iris.data
    Y = iris.target

    # データの標準化
    X = preprocessing.scale(X)

    # ラベルをone-hot-encoding形式に変換
    # 0 => [1, 0, 0]
    # 1 => [0, 1, 0]
    # 2 => [0, 0, 1]
    Y = np_utils.to_categorical(Y)

    # 訓練データとテストデータに分割
    train_X, test_X, train_Y, test_Y = train_test_split(X, Y, train_size=0.8)
    print(train_X.shape, test_X.shape, train_Y.shape, test_Y.shape)

    # モデル構築
    model = build_multilayer_perceptron()
    model.compile(optimizer='adam',
                  loss='categorical_crossentropy',
                  metrics=['accuracy'])

    # モデル訓練
    model.fit(train_X, train_Y, nb_epoch=50, batch_size=1, verbose=1)

    # モデル評価
    loss, accuracy = model.evaluate(test_X, test_Y, verbose=0)
    print("Accuracy = {:.2f}".format(accuracy))

ポイント

  • irisデータは sklearn.datasets.load_iris() でダウンロードできる。よく使う標準データセットはメソッドが用意されていて便利

  • irisのラベルは文字列だがsklearnのデータセットでは、0, 1, 2のように数値ラベルに変換されている。これをニューラルネットで扱いやすいone-hotエンコーディング型式に変換する。上のコメントにも書いたようにone-hotエンコーディングは、特定のユニットのみ1でそれ以外は0のようなフォーマットのこと。この変換は、keras.utils.np_utilsto_categorical() に実装されている

  • 訓練データとテストデータは、sklearn.model_selectiontrain_test_split() を使う。訓練データが8割、テストデータが2割になるように分割した。少し古いscikit-learnだと sklearn.cross_validation に実装されているけどこのモジュールはすでにdeprecated

  • モデル構築は build_multilayer_perceptron() という独自関数を用意した。モデルが複雑になると関数化したほうがわかりやすそう。入力層が4ユニット、隠れ層が16ユニット、出力層が3ユニットの多層パーセプトロンを構築した

  • 多クラス分類の場合は、損失関数に categorical_crossentropy を指定する

必要な前処理メソッドはたいていscikit-learnに実装されているので自分で実装する前に探してみたほうがよさそう。

5回くらい実行してテストデータの精度を求めると97%、93%、100%、93%、97%となった。どのサンプルがテストデータに選ばれるかによって精度が変わるようだ。分類境界あたりのサンプルがテストデータとして選ばれると予測が難しくなり精度が下がりそうなのは直感的にわかる。

こういう場合は、何度か実行して平均を求めるのがセオリーかな。でも巨大なデータセットの学習だと何日もかかるケースがありそうだし、そんなに何回もデータセットの分割を変えて実行はできないなあ。どうするのが一般的なんだろうか?

参考

Kerasによる2クラス分類(Pima Indians Diabetes)

Kerasのプログラミングは

  1. データのロード
  2. モデルの定義
  3. モデルのコンパイル
  4. モデルの学習
  5. モデルの評価
  6. 新データに対する予測

という手順が一般的。今回はもう少し実践的なデータを使ってこの流れをつかみたい。

ソースコード:pima.py

1. データのロード

参考文献で挙げた記事と同じようにUCI Machine Learning repositoryにあるPima Indians Diabetes Data Setを使おう。 医療系のデータでPimaインディアンが糖尿病にかかったかどうかを表すデータのようだ。

Attribute Information:
1. Number of times pregnant
2. Plasma glucose concentration a 2 hours in an oral glucose tolerance test
3. Diastolic blood pressure (mm Hg)
4. Triceps skin fold thickness (mm)
5. 2-Hour serum insulin (mu U/ml)
6. Body mass index (weight in kg/(height in m)^2)
7. Diabetes pedigree function
8. Age (years)
9. Class variable (0 or 1)

データの説明によると最初の8列が患者の属性情報で9列目が糖尿病を発症しない(0)または糖尿病を発症した(1)というラベルとなっている。つまり、患者の属性情報から糖尿病を発症するかを予測するモデルを学習するのがここでの目的。

このデータはCSV形式でダウンロードできる。データをロードするコードは、

# load pima indians dataset
dataset = np.loadtxt(os.path.join("data", "pima-indians-diabetes.data"), delimiter=',')

# split into input (X) and output (Y) variables
X = dataset[:, 0:8]
Y = dataset[:, 8]

ここで、二次元データならグラフで可視化してみるところだけれど、8次元データはそのままでは可視化できない。分析するなら属性間の相関図や次元圧縮してみるのがセオリーか?今回はKerasの使い方の習得がメインなので飛ばそう。

2. モデルの定義

ここはニューラルネットの構造を組み立てるところ。今回は、隠れ層が2つ、出力層が1つの多層パーセプトロン を構築する。

f:id:aidiary:20161103212842p:plain:w400

# create model
model = Sequential()
model.add(Dense(12, input_dim=8, init='uniform', activation='relu'))
model.add(Dense(8, init='uniform', activation='relu'))
model.add(Dense(1, init='uniform', activation='sigmoid'))
  • initで層の重みの初期化方法を指定できる
  • uniformだと0~0.05の一様乱数。normalだと正規乱数。Deep Learning Tutorialの初期値重みで使われていたglorot_uniformもある
  • 層の活性化関数は、独立した層ではなくDenseactivation引数でも指定できる
  • 隠れ層の活性化関数にはrelu、出力層の活性化関数にはsigmoidを指定
  • 出力層にsigmoidを使うと0.0から1.0の値が出力されるため入力データのクラスが1である確率とみなせる。0.5を閾値として0.5未満ならクラス0、0.5以上ならクラス1として分類する

3. モデルのコンパイル

損失関数、最適化関数、評価指標を指定してモデルをコンパイルする。2クラス分類なのでbinary_crossentropyを使う。ここら辺は前回と同じ。

# compile model
model.compile(loss='binary_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

4. モデルの学習

# fit the model
model.fit(X, Y, nb_epoch=150, batch_size=10)
  • 訓練データXとラベルYを指定して学習
  • エポックは固定で150回ループを回す
  • 学習はいわゆるミニバッチ学習でバッチサイズは10とした。データを10個ずつ取り出して誤差を蓄積し、その誤差で1回重みを更新する。

5. モデルの評価

# evaluate the model
scores = model.evaluate(X, Y)
print("%s: %.2f%%" % (model.metrics_names[1], scores[1] * 100))
  • モデルの評価にはmodel.evaluate()を使う。
  • 戻り値は評価指標のリスト。デフォルトでは損失(loss)のみ。compile()metricsに評価指標を追加すると、別の評価指標が追加される。今回は、精度(acc)を追加してある。model.metrics_namesで、評価尺度の名前リストが得られる。
> print(model.metrics_names)
['loss', 'acc']
> print(scores)
[0.45379118372996646, 0.7890625]

ここでは、訓練データを使って評価しているが、実際は訓練データとは独立したテストデータを用意するべき。ただ、モデルにバグがないか確認するために最初は訓練データで評価してみるのもありだと思う。訓練データでさえ精度がものすごく低かったらそもそも学習できないかプログラムにバグがある。今回は、訓練データの予測で精度79%なのでそこそこうまくいっている?次回は訓練データとテストデータを分ける例を取り上げたい。

6. 新データに対する予測

predictions = np.round(model.predict(X))
correct = Y[:, np.newaxis]
  • モデルに新しいデータを入力してクラスラベルを予測するにはmodel.predict()
  • 今回は簡単のため訓練データXをそのまま入力
  • 出力層はsigmoidなので下のように0.0から1.0の値が出力される
  • 出力をクラスラベルにするためにround()を使う。0.5以上なら1に切り上げ、未満なら0に切り下げ
> print(predictions)
[[ 0.82117373]
 [ 0.10473501]
 [ 0.90901828]
 [ 0.10512047]
 [ 0.78090101]
 ...
> print(correct)
[[ 1.]
 [ 0.]
 [ 1.]
 [ 0.]
 [ 1.]
 ...

今回は実践的なデータセットを用いてKerasによる実験の流れを一通りまとめた。

参考

Kerasによる2クラスロジスティック回帰

まずはもっとも簡単な2クラスロジスティック回帰モデルをKerasで書いてみる。前にTheanoでやった(2015/5/19)のをKerasで書き換えただけ。ロジスティック回帰は、回帰とつくけど分類のアルゴリズムで、隠れ層がなく、活性化関数にシグモイド関数を使ったニューラルネットとしてモデル化できる。

データは、PRMLの4章のex2data1 を使う。1列目と2列目がデータで3列目がクラスラベル(0または1の2クラス)。

ソースコード:ex2data1.py

データのロードと正規化

データを読み込むライブラリにはpandasなどもあるが、ここではnumpy.genfromtxt()を使う。Xがデータで二次元データのリスト、tがラベルで0または1のリスト。

# load training data
data = np.genfromtxt(os.path.join('data', 'ex2data1.txt'), delimiter=',')
X = data[:, (0, 1)]
t = data[:, 2]

データの各列の平均が0、標準偏差が1になるようにデータを正規化する。この正規化をしないと学習がまったく進まない(=lossが小さくならない)ことが多かったのでやった方がよさそう。sklearn.preprocessingモジュールに正規化のメソッドがあるので使う。

from sklearn import preprocessing
# normalize data
X = preprocessing.scale(X)

以下のコードでデータの各列が平均0、標準偏差1に正規化されたことが確認できる。データのaxis=0(行方向)に平均と標準偏差を求めている。平均は0っぽくないが、e-17なので限りなく0に近い。

print(np.mean(X, axis=0))
print(np.std(X, axis=0))
[ -7.66053887e-17   1.11022302e-15]
[ 1.  1.]

データの可視化

どのようなデータかmatplotlibで可視化する。

import matplotlib.pyplot as plt

def plot_data(X, t):
    positive = [i for i in range(len(t)) if t[i] == 1]
    negative = [i for i in range(len(t)) if t[i] == 0]

    plt.scatter(X[positive, 0], X[positive, 1], c='red', marker='o', label='positive')
    plt.scatter(X[negative, 0], X[negative, 1], c='blue', marker='o', label='negative')

# plot training data
plt.figure(1)
plot_data(X, t)

赤が正例(positive)で青が負例(negative)。この2クラスを直線で分類するのが目標。

f:id:aidiary:20161030205617p:plain

ロジスティック回帰モデルの構築

f:id:aidiary:20161030220153p:plain:w400

ロジスティック回帰モデルを組み立てる。空のSequentialモデルを作成し、そこにレイヤ(Dense)や活性化関数(Activation)を順番に追加し、最後にコンパイルする。Sequentialモデルというのは通常のニューラルネットのように層を積み重ねたモデルを指す。コンパイル時に最適化手法(optimizer)、損失関数(loss)、評価指標(metrics)を指定する。

# create the logistic regression model
model = Sequential()
model.add(Dense(1, input_shape=(2, )))
model.add(Activation('sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
  • 入力データは2次元、出力は1次元
  • このタスクは0または1を予測する二値分類なので損失関数にはbinary_crossentropy
  • あとで実験するがMNISTのような多値分類ではcategorical_crossentropy
  • 最適化アルゴリズムにはAdam、評価指標には精度を用いた。これは訓練データで精度求めているのかな?要検証

Kerasの層(layer)は、一般的な重みがある層だけでなく、活性化関数も層とみなしている。下の実行結果からわかるようにActivationkeras.layersに含まれている。

print(model.layers[0])
print(model.layers[1])
<keras.layers.core.Dense object at 0x7f556811b0b8>
<keras.layers.core.Activation object at 0x7f556816a5c0>

modelの組み立て方は上の方法がスタンダードだけれど、Functional APIを使うともっと柔軟にモデルが作れるようだ。あとでこのAPIを使った書き方もしてみよう。

訓練

モデルの訓練はscikit-learnと同じくfit()という関数にデータとラベルを渡せばよい。

# fit the model
model.fit(X, t, nb_epoch=1000, batch_size=5, verbose=1)
  • fit()には、固定のエポック数(nb_epoch)、バッチサイズ(batch_size)、経過出力方法(verbose)を指定する。
Epoch 1/1000
100/100 [==============================] - 0s - loss: 0.6531 - acc: 0.6600
Epoch 2/1000
100/100 [==============================] - 0s - loss: 0.6455 - acc: 0.6700
Epoch 3/1000
100/100 [==============================] - 0s - loss: 0.6385 - acc: 0.6700
Epoch 4/1000
100/100 [==============================] - 0s - loss: 0.6310 - acc: 0.6800
Epoch 5/1000
100/100 [==============================] - 0s - loss: 0.6243 - acc: 0.6800
  • verbose=1にしておくと学習経過を棒グラフで表示してくれるので非常に便利!
  • あとで紹介するEarly-stoppingを使うと固定エポックだけループを回すのではなく、収束判定して止めてくれるようになる。

学習した重みの取得

model.layersリストに各層の情報が格納されている。好きな層を指定してget_weights()で重みが取得できる。重みが多次元リストなのでややこしいが、最初の次元は、[0]が重み、[1]がバイアス項を表すようだ。モデルからは重みだけでなく、出力も取り出せるのであとでまとめておこう。

# get the learned weight
weights = model.layers[0].get_weights()
w1 = weights[0][0, 0]
w2 = weights[0][1, 0]
b = weights[1][0]

ここでは、決定境界を描画したいので学習した重みw1、w2とバイアスbを取得した。

決定境界の描画

ロジスティック回帰の決定境界の条件は

 w1 * x1 + w2 * x2 + b = 0

なので [x1, x2] 平面上での直線の方程式に直すと

 x2 = - (w1 / w2) * x1 - (b / w2)

となる。この直線をmatplotlibで先ほどのグラフに追記する。

# draw decision boundary
plt.figure(1)
xmin, xmax = min(X[:, 0]), max(X[:, 0])
ymin, ymax = min(X[:, 1]), max(X[:, 1])
xs = np.linspace(xmin, xmax, 100)
ys = [- (w1 / w2) * x - (b / w2) for x in xs]
plt.plot(xs, ys, 'b-', label='decision boundary')
plt.xlabel('x1')
plt.ylabel('x2')
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
plt.legend()
plt.show()

結果は、

f:id:aidiary:20161030205644p:plain

となり、分類する直線が学習できていることがわかる。 今回は、テストデータを使った評価などは行っていない。

参考