Kerasの学習履歴(History)をDataFrameに変換する

Kerasのちょっとした小ネタです。
Kerasで作ったモデルをfitすると、戻り値として損失関数や正解率を格納したHistoryオブジェクトが返されます。
それを使って、学習の進みなどを可視化できます。

例えばこちらの記事を参照: CNNで手書き数字文字の分類
こちらでは可視化だけで10行くらいのコードになっています。

で、改めてHistoryの中身をみてみると、DataFrameに変換できる形式であることに気づきました。
長いので、実データは {数値のリスト} に置換しましたが、次のようなイメージです。


print(history.history)
'''
{'val_loss': {数値のリスト},
 'val_acc': {数値のリスト},
 'loss': {数値のリスト},
 'acc': {数値のリスト}
'''

これは容易にDataFrameに変換できます。


print(pd.DataFrame(history.history))
'''
    val_loss  val_acc      loss       acc
0   0.106729   0.9677  0.590888  0.811850
1   0.072338   0.9764  0.227665  0.931233
2   0.059273   0.9800  0.174741  0.948033
3   0.047335   0.9837  0.149136  0.955500
4   0.042737   0.9859  0.132351  0.960167
5   0.039058   0.9868  0.121810  0.964600
6   0.034511   0.9881  0.110556  0.967050
7   0.032818   0.9882  0.105487  0.967867
8   0.032139   0.9893  0.100333  0.970167
9   0.030482   0.9898  0.095932  0.971383
10  0.027904   0.9900  0.089120  0.973267
11  0.028368   0.9898  0.086760  0.973683
'''

DataFrameになると、これ自体を分析しやすいですし、さらに非常に容易に可視化できます。


history_df = pd.DataFrame(history.history)
history_df.plot()
plt.show()

たったこれだけです。以前の記事の可視化に比べると非常に楽ですね。
出力はこのようになります。

kerasのモデルの中間層の出力を可視化してみる

ディープラーニングのモデルを作成したとき、中間層の出力が気になることがよくあります。
きちんと活性化しているかとか、相関が高すぎて意味がないユニットが多くないかとか、
どんな条件の時に活性するのかなど、確認したい内容は時により様々です。

kerasの場合、学習済みのモデルの層を取り出して新しいモデルを作成することで中間層の出力を確認できます。

中間レイヤーの出力を得るには?

試しに以前下記の記事で作ったモデルでやってみましょう。
CNNで手書き数字文字の分類

公式ドキュメントに紹介されていたのと少し違う方法ですが、普通にSequentialモデルに学習済みの層を一個追加したら動いたので、
その方法で行います。
一層目には16ユニットあるのですが、そのうち2このユニットについて、出力を可視化しました。

# 学習済みモデルの1層目だけ取得してモデルを作成する


model_2 = Sequential()
model_2.add(model.layers[0])

# 元画像と1層目の出力2個を可視化
fig = plt.figure(figsize=(18, 30))
for i in range(5):
    # print(y_test[i].argmax())
    ax = fig.add_subplot(6, 3, 3*i+1)
    ax.imshow(X_test[i][:, :, 0], cmap='gray_r')
    ax = fig.add_subplot(6, 3, 3*i+2)
    ax.imshow(model_2.predict(X_test[i:i+1])[0][:, :, 0], cmap='gray_r')
    ax = fig.add_subplot(6, 3, 3*i+3)
    ax.imshow(model_2.predict(X_test[i:i+1])[0][:, :, 1], cmap='gray_r')
plt.show()

出力がこちらです。

真ん中の列の出力は横線の下辺に反応していることや、右側の列の結果は中抜き文字のような形で反応しているのがわかりますね。

ちなみに、それぞれのユニットのウェイト(バイアスは除く)を可視化すると次のようになります


fig = plt.figure(figsize=(5,10))
for i in range(2):
    w = model_2.get_weights()[0][:, :, 0, i].reshape(3, 3)
    ax = fig.add_subplot(2, 1, i+1)
    ax.imshow(w, cmap='gray_r')
plt.show()

イメージした通りのウェイトでした。

CNNで手書き数字文字の分類

以前の記事で読み込んだ手書き数字文字データ(MINIST)を使って、0~9の数字を判定するモデルを作ってみます。

kerasのサンプルコードもあるのですが、せっかくなので少しだけパラメーターなどを変えてやってみましょう。

最初にライブラリをインポートしてデータを準備します。
前処理として配列の形をConv2Dのinputに合わせるのと、
0〜1への正規化、 ラベルの1hot化を行います。


# ライブラリの読み込み
from keras.datasets import mnist
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.callbacks import EarlyStopping
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report

# データの読み込み
(data_train, target_train), (data_test, target_test) = mnist.load_data()

# Conv2D の inputに合わせて変形
X_train = data_train.reshape(-1, 28, 28, 1)
X_test = data_test.reshape(-1, 28, 28, 1)

# 特徴量を0~1に正規化する
X_train = X_train / 255
X_test = X_test / 255

# ラベルを1 hot 表現に変換
y_train = to_categorical(target_train, 10)
y_test = to_categorical(target_test, 10)

続いてモデルの構築です


# モデルの構築
model = Sequential()
model.add(Conv2D(16, kernel_size=(3, 3),
                 activation='relu',
                 input_shape=(28, 28, 1)))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(10, activation='softmax'))
model.compile(
    loss="categorical_crossentropy",
    optimizer="adam",
    metrics=['accuracy']
)
print(model.summary())

# 以下、出力
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 16)        160       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 16)        0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 13, 13, 16)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 32)        4640      
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 32)          0         
_________________________________________________________________
dropout_2 (Dropout)          (None, 5, 5, 32)          0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 800)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 64)                51264     
_________________________________________________________________
dropout_3 (Dropout)          (None, 64)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 10)                650       
=================================================================
Total params: 56,714
Trainable params: 56,714
Non-trainable params: 0
_________________________________________________________________

そして学習です。


# 学習
early_stopping = EarlyStopping(
                        monitor='val_loss',
                        min_delta=0.0,
                        # patience=2,
                )

history = model.fit(X_train, y_train,
                    batch_size=128,
                    epochs=30,
                    verbose=2,
                    validation_data=(X_test, y_test),
                    callbacks=[early_stopping],
                    )

# 以下出力
Train on 60000 samples, validate on 10000 samples
Epoch 1/30
 - 18s - loss: 0.6387 - acc: 0.7918 - val_loss: 0.1158 - val_acc: 0.9651
Epoch 2/30
 - 18s - loss: 0.2342 - acc: 0.9294 - val_loss: 0.0727 - val_acc: 0.9772
Epoch 3/30
 - 17s - loss: 0.1827 - acc: 0.9464 - val_loss: 0.0571 - val_acc: 0.9815
Epoch 4/30
 - 18s - loss: 0.1541 - acc: 0.9552 - val_loss: 0.0519 - val_acc: 0.9826
Epoch 5/30
 - 18s - loss: 0.1359 - acc: 0.9598 - val_loss: 0.0420 - val_acc: 0.9862
Epoch 6/30
 - 17s - loss: 0.1260 - acc: 0.9620 - val_loss: 0.0392 - val_acc: 0.9880
Epoch 7/30
 - 18s - loss: 0.1157 - acc: 0.9657 - val_loss: 0.0381 - val_acc: 0.9885
Epoch 8/30
 - 19s - loss: 0.1106 - acc: 0.9673 - val_loss: 0.0349 - val_acc: 0.9889
Epoch 9/30
 - 17s - loss: 0.1035 - acc: 0.9694 - val_loss: 0.0359 - val_acc: 0.9885

学習の進み方をプロットしておきましょう。


# Epoch ごとの正解率と損失関数のプロット
fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(2, 1, 1, title="loss")
ax.plot(history.epoch, history.history["loss"], label="train_loss")
ax.plot(history.epoch, history.history["val_loss"], linestyle="-.", label="val_loss")
ax.legend()
ax = fig.add_subplot(2, 1, 2, title="acc")
ax.plot(history.epoch, history.history["acc"], label="train_acc")
ax.plot(history.epoch, history.history["val_acc"], linestyle="-.", label="val_acc")
ax.legend()
plt.show()

val_acc は結構高い値を出していますが、一応クラスごとの成績も評価しておきましょう。


# 評価
y_predict = model.predict_classes(X_train)
print(classification_report(target_train, y_predict))

# 以下出力
             precision    recall  f1-score   support

          0       0.99      1.00      0.99      5923
          1       0.99      1.00      0.99      6742
          2       0.99      0.99      0.99      5958
          3       0.99      0.99      0.99      6131
          4       0.99      0.99      0.99      5842
          5       0.99      0.99      0.99      5421
          6       0.99      0.99      0.99      5918
          7       0.98      0.99      0.99      6265
          8       0.99      0.97      0.98      5851
          9       0.98      0.99      0.98      5949

avg / total       0.99      0.99      0.99     60000

ほとんど適当に作ったモデルでしたが、
ほぼほぼ正解できていますね。

kerasのto_categoricalを使ってみる

機械学習の特徴量や正解ラベルをone-hotベクトルにするとき、
自分で実装するか、sklearnのOneHotEncoderを使うことが多いです。
稀に、pandasのget_dummiesを使うこともあります。

ところが、kerasのサンプルコードを読んでいると、to_categoricalというのもよく使われているので確認してみました。
軽く動かしてみると思った通りの挙動をしたので特に必要というわけでもないのですが、
使うライブラリをkerasに統一したいことがあれば利用するかもしれません。

とりあえず使ってみましょう。


import numpy as np
from keras.utils import to_categorical
# テスト用のデータ生成
data = np.random.randint(low=0, high=5, size=10)
print(data)
# One-Hotベクトルに変換
print(to_categorical(data))

# 以下出力
[3 2 1 4 4 1 0 1 0 2]

[[0. 0. 0. 1. 0.]
 [0. 0. 1. 0. 0.]
 [0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 1.]
 [0. 1. 0. 0. 0.]
 [1. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0.]
 [1. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0.]]

to_categorical の2番目の引数(num_classes)として、数値を渡すと、
データの最大値を指定できます。
ただし、データの最大値+1より小さい値を渡すとエラーです。
極端な不均衡データを扱うときなどは念のため指定しておいたほうが安全かも。

試してみたサンプルです。


print(to_categorical(data, 8))

# 出力
[[0. 0. 0. 1. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 0. 0.]]

kerasのMNISTデータを読み込んでみる

kerasにはscikit-learnと同じように、いつくかのサンプルデータが付属しています。
その中の一つがMNISTという28*28ピクセルの手書き数字文字のデータです。

scikit-learn にも digits という手書き数字のサンプルデータがありますが、こちらは8*8ピクセルの結構データ量の小さいデータです。
ライブラリの使い方を紹介する上ではこれで問題がないのでよく使っていますが、深層学習をつかずとも十分に判別できてしまうのが短所です。
(このブログの過去の記事でも使ってきたのはこちらです。)

せっかくディープラーニングを試すのであればこちらを使った方がいいと思うので、使い方を紹介します。

ドキュメントはここ。
MNIST database of handwritten digits

早速ですが読み込み方法です。


from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

インポートしてloadすれば良いというのは scikit-learnのサンプルデータと同じですが、
load_dataの戻り値が少し特徴的です。
長さ2のタプル2個を値に持つタプルを返してきます。
これを受け取るために、上記のような書き方をします。
変数に格納された配列の次元数を見ると次のようになります。


print(x_train.shape) # (60000, 28, 28)
print(y_train.shape) # (60000,)
print(x_test.shape) # (10000, 28, 28)
print(y_test.shape) # (10000,)

x_train などの中身を見れば、ピクセルごとの濃淡として画像データが入っていることがわかるのですが、
せっかくの画像データなので、画像として可視化してみましょう。
matplotlibのimshow という関数を使うと便利です。

matplotlib.pyplot.imshow

最初の16データを可視化したのが次のコードです。
デフォルトの配色はイマイチだったので、文字らしく白背景に黒文字にしました。


import matplotlib.pyplot as plt
fig = plt.figure(figsize=(12, 12))
for i in range(16):
    ax = fig.add_subplot(4, 4, i+1, title=str(y_train[i]))
    ax.imshow(x_train[i], cmap="gray_r")
plt.show()

結果はこちら。
digitsに比べて読みやすい数字画像データが入ってますね。

kerasで学習途中のモデルを保存する

kerasでモデルを学習している時、学習途中のモデルが欲しいことがよくあります。
デフォルトでepockごとに評価スコア出るので、テストデータに限っては過学習する前のモデルが欲しいですし、
前の記事のEarlyStoppingで、patienceを設定しているのであれば、
Stopした最後のバージョンより、そのいくつか前のものの方が評価が高いからです。
このような時、ModelCheckpointというCallBackを使うことで、Epockごとのモデルを保存しておくことができます。

前回同様データの準備やモデルの構築は省略して、ModelCheckpointに必要な部分のコードだけ紹介します。


from keras.callbacks import ModelCheckpoint

# checkpointの設定
checkpoint = ModelCheckpoint(
                    filepath="model-{epoch:02d}-{val_loss:.2f}.h5",
                    monitor='val_loss',
                    save_best_only=True,
                    period=1,
                )
# 学習
history = model.fit(
                    X_train,
                    y_train,
                    epochs=100,
                    batch_size=16,
                    validation_data=[X_test, y_test],
                    callbacks=[early_stopping, checkpoint]
            )

これで、filepathに指定したファイル名でepockごとのモデルが保存できます。
見ての通り、ファイル名にはepock数などを変数で埋め込むことができます。
また、save_best_only=Trueを設定することで、
前のモデルよりも精度が良い場合のみ保存することができます。
ファイル名に変数を含めないように指定しておけば、ベストのモデルだけが保存されている状態になります。

あとは、保存されているモデルをloadして利用すればOKです。

kerasでモデルの学習が進まなくなったら学習を止める方法

機械学習をやっているとき、過学習の抑止や時間の節約のためにモデルの改善が止まった時点で学習を止めたいことがあります。
kerasでは CallBack に EarlyStopping というオブジェクトを設定するおことでそれを実現できます。

モデル本体やデータについてのコードは省略しますので別記事を参照してください、該当部分だけ紹介します。


# インポート
from keras.callbacks import EarlyStopping

# EaelyStoppingの設定
early_stopping =  EarlyStopping(
                            monitor='val_loss',
                            min_delta=0.0,
                            patience=2,
)

# 学習
history = model.fit(
                    X_train,
                    y_train,
                    epochs=100,
                    batch_size=16,
                    validation_data=[X_test, y_test],
                    callbacks=[early_stopping] # CallBacksに設定
            )

これで、 monitor に設定した値が、 patienceの値の回数続けてmin_delta以上改善しないと、
学習がストップします。
monitor には ‘val_loss’ の他、 ‘val_acc’なども設定可能です。

patience の設定が0の時と1の時は挙動が全く同じに見えますね。

特にデメリットも感じられないので、kerasで機械学習を試す時はほぼ確実に使っています。
あまりにも学習が進まないうちに止まってしまう時は、EarlyStopping無しで試したりするのですが、
経験上、EarlyStoppingが悪いことは少なく、モデルの設計が悪かったり、その他どこかにミスがあることが多いです。

kerasで作成したモデルの保存と読み込み

kerasで作成したモデルを保存したり、次回使うときに読み込んだりする方法のメモです。

とりあえず、modelという変数に学習済みのモデルが格納されているとします。
(前回の記事のモデルです。)

 


model.summary()

# 以下出力
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 16)                48        
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 17        
=================================================================
Total params: 65
Trainable params: 65
Non-trainable params: 0
_________________________________________________________________

pythonでオブジェクトを保存するときは、pickleを使うことが多いですが、
kerasではその方法は推奨しないと明記されています。

How can I save a Keras model?

It is not recommended to use pickle or cPickle to save a Keras model.

その代わりに、saveやload_modelというメソッドが用意されていて、
HDF5形式で保存や読み込みができます。


# 保存
model.save("model.h5")

# 読み込み
from keras.models import load_model
model_0 = load_model("model.h5")

kerasで簡単なモデルを作成してみる

前回の記事でkerasを使えるようにしたので、
動作確認を兼ねて非常に単純なモデルを作ってみました。

線形分離可能なサンプルではつまらないので、半径の異なる2円のToyDataでやってみましょう。
scikit-learnにmake_circlesという関数があるので、これを使います。

早速データを生成して訓練データとテストデータに分け、訓練データの方を可視化してみます。


# 必要なモジュールのインポート
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from keras.models import Sequential
from keras.layers.core import Dense

# データの準備
data, target = make_circles(n_samples=2000, noise=0.2, factor=0.3)
X_train, X_test, y_train, y_test = train_test_split(
                                            data,
                                            target,
                                            test_size=0.2,
                                            stratify=target
                                        )
# 訓練データの可視化
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(1, 1, 1, aspect='equal', xlim=(-2, 2), ylim=(-2, 2))
ax.scatter(X_train[y_train == 0, 0], X_train[y_train == 0, 1], marker="o")
ax.scatter(X_train[y_train == 1, 0], X_train[y_train == 1, 1], marker="x")
plt.show()

これが出力された散布図です。
一部ノイズはありますが概ね綺麗に別れていて、動作確認には手頃な問題になっていると思います。

早速ですが、kerasでモデルを作っていきます。
kerasのドキュメントはここです。日本語版があって便利ですね。
特に何も考えず、中間層が1層あるだけのシンプルなニューラルネットワークで作ります。
とりあえず動けば良いので、今回はDropoutもCallbackもなしで。


# モデルの構築
model = Sequential()
model.add(Dense(16, activation='tanh', input_shape=(2,)))
model.add(Dense(1, activation='sigmoid'))
model.summary()
model.compile(
        optimizer='adam',
        loss='binary_crossentropy',
        metrics=['acc']
)

# 以下、出力
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 16)                48        
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 17        
=================================================================
Total params: 65
Trainable params: 65
Non-trainable params: 0
_________________________________________________________________

モデルができたので、学習させます。
epochsもbatch_sizeも適当です。


# 学習
history = model.fit(
                    X_train,
                    y_train,
                    epochs=100,
                    batch_size=32,
                    validation_data=[X_test, y_test]
            )

正常に学習が進んだことを確認するために、
損失関数と正解率の変動を可視化してみましょう。


# Epoch ごとの正解率と損失関数のプロット
fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(2, 1, 1, title="loss")
ax.plot(history.epoch, history.history["loss"], label="train_loss")
ax.plot(history.epoch, history.history["val_loss"], linestyle="-.", label="val_loss")
ax.legend()
ax = fig.add_subplot(2, 1, 2, title="acc")
ax.plot(history.epoch, history.history["acc"], label="train_acc")
ax.plot(history.epoch, history.history["val_acc"], linestyle="-.", label="val_acc")
ax.legend()
plt.show()

できたグラフがこちら。
順調に学習できていますね。

訓練データで評価してみましょう。
もともとやさしい問題なので、なかなかの正解率です。


print(classification_report(y_train, model.predict_classes(X_train)))

# 出力結果
             precision    recall  f1-score   support

          0       0.95      0.96      0.95       800
          1       0.96      0.95      0.95       800

avg / total       0.95      0.95      0.95      1600

最後に、決定境界を可視化してみます。
以前紹介した、等高線のプロットを使います。


# 決定境界の可視化
X_mesh, Y_mesh = np.meshgrid(np.linspace(-2, 2, 401), np.linspace(-2, 2, 401))
Z_mesh = model.predict_classes(np.array([X_mesh.ravel(), Y_mesh.ravel()]).T)
Z_mesh = Z_mesh.reshape(X_mesh.shape)
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(1, 1, 1, aspect='equal', xlim=(-2, 2), ylim=(-2, 2))
ax.contourf(X_mesh, Y_mesh, Z_mesh, alpha=0.2)
plt.scatter(X_test[y_test == 0, 0], X_test[y_test == 0, 1], marker="o")
plt.scatter(X_test[y_test == 1, 0], X_test[y_test == 1, 1], marker="x")
plt.show()

出力がこちら。
真円にはなっていませんが、きちんと円の内側と外側にデータを分離できました。

Tensorflowをインストールしようとしたら出たエラー

MacのOSをMojaveにあげてから、Homebrewや、pyenvなどを全部入れ直しになってしまったので、
TensorflowやKeras も消えてしまっていましたと。

ということで、サクッと pip インストールを試みたのですが、
下記のエラーが出て失敗しました。

~$ pip install tensorflow
Collecting tensorflow
  Could not find a version that satisfies the requirement tensorflow (from versions: )
No matching distribution found for tensorflow

原因はpythonのバージョンが 3.7.0だったことです。

こちらの記載を見ると、3.4〜3.6じゃないとダメだと書いてあります。
Install TensorFlow with pip
Requires Python 3.4, 3.5, or 3.6

みなさんいろんな方法で回避をここみられていますが、
まぁ、せっかくpyenvで仮想環境として構築しているので、もっと古いバージョンの仮想環境を作りましょう。

ちなみに今入っているバージョンは
anaconda3-5.3.1
です。

anacondaのバージョンとpythonのバージョンの対応表があれば便利なのですが、なかなか見つかりません。
とりあえず、 anaconda3-5.2.0 を試してみましょう。


$ pyenv install anaconda3-5.2.0
$ pyenv global anaconda3-5.2.0
$ python --version
Python 3.6.5 :: Anaconda, Inc.

運良く? 3.6が入りました。

あとは、改めて tensorflowをpipインストールします。
kerasも入れておきましょう。
改めて見るとkerasの方もpythonは3.6までしか対応してないって書いてありますね。
(kerasのドキュメント)


pip install tensorflow
pip install keras

これで再びkerasとtensorflowを使える環境が整いました。