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

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

pickleを使ってpythonのオブジェクトをファイルに保存する

(注)この記事はscikit-learnのモデルをファイルに保存することを念頭に書いていますが、
pickle自体はscikit-learnのモデル以外のものも直列化してファイルに書き出すことができるモジュールです。

以前の記事で、kerasで作成したmodelを保存したり読み込んだりする方法を書きました。
今回はscikit-learnで作ったモデルを保存してみます。
kerasには専用の関数が用意されていたのですが、scikit-learnにはありません。
そのため、他の方法が必要です。
そこでpython標準ライブラリの pickleが使えます。
ドキュメント

利用方法は、ドキュメントのpickle.dumppickle.loadの説明と、一番下の使用例が参考になります。

clfという変数に、学習済みのモデルが格納されているという想定で、保存と読み込みのコード例を紹介します。
また、保存するファイル名は何でも良いのですが、サンプルコードではclf.pickleとします。

まずは保存。


import pickle
with open("clf.pickle", "wb") as f:
    pickle.dump(clf, f)

次に読み込み。


import pickle
with open("clf.pickle", "rb") as f:
    clf = pickle.load(f)

これで、一度学習したモデルを読み込んで、予測に活用することができます。
scikit-learnで学習したモデルを本番運用するならばほぼ必須の技術です。
(pickle以外の方法を使うという手もありますが、何らかの形での保存と読み込みの手段が必要です)

ブログ記事数が50を超えていたので振り返り

このブログをはじめて1ヶ月半ほどたち、記事数はいつの間にか50記事を超えていました。
想定外だった気づきや想定外だったこともあるので、この辺で一度振り返りをやっておこうと思います。
とりあえずこのブログについて感じていることをそのまま書き出しておきます。

想像以上に勉強になる

元々は、自宅PC、職場PC、個人アカウントのサーバー、職場のサーバーなどに散らばっていたメモ書きを
一箇所に集約して参照できたら便利だということと、
それがネット上にあれば自分より後に勉強を始めた人の役に立つだろうという二つの目的でこのブログを始めました。
要は、最初は自分が知ってることとか調べ終わったことを書き写すだけで新しい学びが増えることはあまり期待していましせんでした。
しかし、改めて記事を書くとなるとドキュメントを読み直したり、最新バージョンの環境でテスト実行したりするので、
新たな気づきが多くあり自分の勉強になりました。
numpyやpandasなんて2年弱使っているのですが、
これまでは、本や誰かが書いたサンプルコードばかり読んで、公式ドキュメントをまともに見ていませんでした。
やはり公式のドキュメントを読むとより効率のいい使い方がわかったり、新しく知る機能があったりと非常に勉強になります。

記事を書くのは思ったより大変

上の内容とかぶりますが元々は既存のメモ書きを元ネタに記事を書こうとしていたので、もっとサクサクと記事を増やせると思っていました。
しかし、記事用に調べ直したり、できるだけ綺麗なスタイルでコードを書いたり、画像を準備したりすると、
既知の内容でも結構時間がかかります。新しく知ったテーマならなおさらです。
今までいろんな人のブログや記事、投稿を読んできましたが、それらを書いてくださっていた
皆さんへの感謝の気持ちと尊敬の念が高まりました。

想定していた内容の記事がまだ書けてない

データサイエンティストになってからユーザーの行動分析や自然言語処理に多く取り組んできたので、
その分野でもっと高度な記事を量産できると思っていました。
実際、書こうと思っているネタは色々ありますので、そのうち書きます。

しかし実際は最初の方の記事はAWSにwordpressを構築するために色々調べた内容ばかりになり、
その後もそれこそデータサイエンティストになったばかりの頃(というよりもpythonを触り始めた頃)のメモ書きを見返して、
非常に基本的な内容の記事ばかり書いています。
ただ、前述の通りそれが勉強になっているので当分はそのまま続けようと思っています。
基本的な内容でもきっと誰かの役に立つこともあるでしょうし。

まだアクセスが全然増えない

最近は、1日2〜3人くらいは自分以外の人が来てくれているようなのですが、
そのレベルで留まって全然アクセスが伸びていません。
昔やっていたyahooやfc2のブログではもっと早い段階で訪問者が増えたのですが、
それぞれのサービスにあった新着記事のフィードに乗る効果やSEOのおかげだったのでしょう。

今の段階で訪問が増えても、基本的な内容しか書いてなくて若干恥ずかしいというのもあるので、
あんまり積極的なアクセスアップ施策は取らず、当分地道に記事を増やしていきたいと思っています。

今後の計画

とりあえず今のところは1日1記事のペースを維持できていますが、
より高度な内容を書こうと思うとこのペースを維持するのは厳しいとも感じています。
まだまだネタはあるので、当分1日1記事ペースを頑張りたいですがそれをずっと続けるのはおそらく無理です。
どこかのタイミングで、ちゃんと時間かけて独自に調べた内容を週1回くらいのペースで
上げていくスタイルに移行できた方が読む人のためになるのかなとも思っています。

pandasでgroupbyした時に複数の集計関数を同時に適用する

前の記事の続きです。
pandasでデータフレームをgroupbyした時に使える集計関数
ドキュメントのこの記事で参照した部分のすぐ下に、
Applying multiple functions at once
という段落があります。
実はこれ初めて知りました。
今までグルプごとに個数と、平均と、標準偏差を計算したい、みたいな時は、
groupbyして集計を個別に実施して、その結果をmergeするという非常に面倒なことをずっとやっていました。

それが、aggというのを使うと一発でできるようです。


import pandas as pd
from sklearn.datasets import load_iris

# データフレームの準備
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df["target"] = iris.target
df["target_name"] = df.target.apply(lambda x:iris.target_names[x])
del df["target"]

df.groupby("target_name").agg(["count", "mean", "std"])

出力されるのが次です。(ブログのレイアウトの都合上画像で貼り付けます。)

これは便利です。
また、DataFrameのカラム名が2段になっています。
これをみて、indexだけではなく実はcolumnsでも、MultiIndexが使えることを知りました。

pandasでデータフレームをgroupbyした時に使える集計関数

データの集計や分析をpandasで行う時、平均や合計を求めるために、
groupbyを使って集計することがよくあると思います。

非常に手軽に使え流のでなんとなく .sum()や .mean()と書いていたのですが、
そういえば他にどんな関数が使えるのか調べたことがなかったと思ったのでドキュメントを見てみました。
まずここ。
pandas.DataFrame.groupby
平均をとるサンプルコードがありますが求めていた関数の一覧がないですね。

よく読むと、See the user guide for more.とあります。
そのuser guideがこちらです。

Group By: split-apply-combine

この下の方に一覧がありました。

Function

Description

mean()

Compute mean of groups

sum()

Compute sum of group values

size()

Compute group sizes

count()

Compute count of group

std()

Standard deviation of groups

var()

Compute variance of groups

sem()

Standard error of the mean of groups

describe()

Generates descriptive statistics

first()

Compute first of group values

last()

Compute last of group values

nth()

Take nth value, or a subset if n is a list

min()

Compute min of group values

max()

Compute max of group values

グループ化した後に、describe()なんてできたんですね。
少し試してみたのですがこれ便利そうです。
他にもSeriesをスカラーに変換するlambda式なども使えるようです。

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.]]

Data Pipeline Casual Talk に参加しました

2018年2月13日に、エムスリーさんのオフィスで開催された、Data Pipeline Casual Talkに参加してきました。
実はもともと抽選に漏れていて、補欠だったのですが、開催2時間前くらいに急に補欠から繰り上がり参加になったので慌てて会場に行きました。
一瞬、急すぎるからキャンセルしようかとも思ったのですが、結果的に参加できて非常に幸運でした。

発表一覧

以下感想です。例によって僕の主観が多々入ります。

AI・機械学習チームにおけるデータパイプライン構築

機械学習チームを立ち上げられた時に困ったという各課題については、
自分もまさに経験してきたもので本当に参考になりました。
ログ出力が適切でなかったり、classやtaskの設計が悪かったり、
モデルの再現性の問題やテストの効率の問題など。

Luigi というのを拡張して対応されているということで、
技術力の高さを感じました。
tensorflowやgensimのモデルを同じインタフェースでloadできる仕組みは便利そうですね。
物によって保存や読み込みの方法が違うのでいつも地味に不便な思いをしています。

自分のところではまだそもそも機械学習基盤と呼べるようなものを作れていないので、
luigiも検討対象に加えたいです。

丘サーファーへ「水」を届けるために-これまでとこれから-

発表を聞いていて、金融SEをやっていた時のことを思い出しました。
丘サーファーという表現も面白い。

今の僕の職場では個人情報マスク済みのデータに比較的自由にアクセスでき、
データへのアクセスという面では問題なく業務を進められていますので、やはり恵まれた環境なのでしょう。

ただ、Cloud Composer を活用されているのは参考にしたいです。
生Airflowを使っていて、保守に手が回っていないので。

データ基盤の3分類と進化的データモデリング

論理設計(データモデル)と物理設計(システム構成)を分けて考えられているのが参考になりました。
データパイプラインを設計する時に両端を先に考えて挟み込むように真ん中へ進むのも納得です。
確かに普段の業務でもうまく進んでいるときはこの順番で考えています。

担当者のロカールPCにあるExcelシートが実はデータ基盤の役割を果たしているかもしれない、と聞いて、
即座に具体的なエクセルファイルが思い当たり苦笑いしてしまいました。
各アンチパターンにも思い当たる節が多々あり、今後改善していきたいです。
データ基盤の要素を技術要素と対応させて分けるのもアンチパターンだというのも覚えておこう。

データ分析基盤を「育てる」ための技術

分析作業の主なフローのスライドでまさに自分たち直面している問題が取り上げられていて笑いました。
いろんなところからの依頼が増えてくるとSQLを各作業がどんどん増えて
それで疲弊してしまうのですよね。
良い基盤を作れば解決するというものではなく、
データ基盤を育てていくという考えが大事。

リブセンスのデータ分析基盤とAirflow

Airflowを使ったデータ基盤を構築されています。
僕らの環境とよく似ているので、これも身に覚えがある苦労話に苦笑いする場面が多くありました。
バージョンアップの問題などもまさに。
社員が誰でもSQLをかけるというのは素直にすごいと思います。
特に営業の方たちにまでその文化を広げるのはきっと大変だったのではないかと。
ユーザー数の差があるのはもちろんですが、
それを考慮しても活用具合でずいぶん遅れをとっている気がするので負けないようにしたい。

まとめ

データパイプラインやデータ基盤はその重要性を日々感じているのですが、
専任の担当者もいなくてなかなか手が回らず、いろんな課題意識がある分野でした。
機械学習やデータウェアハウス単体の話に比べて他社の事例もすくなく、
自分たちだけこんなに苦労してるんじゃないかと不安になることもあったので今回のカジュアルトークに参加してよかったです。
だいたいどこも同じような課題に直面されていて、それぞれ工夫して対応されていることがわかりました。
自分が漠然とこんな風にしたいと思っていたことが明文化されていたスライドも多くハッとする場面も多々ありました。
あと、Airflow使ってる会社ってこんなに多かったんですね。
逆に、トレジャーデータは一度も登場しなかった。
今回だけでなく、今後も開催されるそうなので楽しみにしています。

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に比べて読みやすい数字画像データが入ってますね。

パイプラインのグリッドサーチ

scilit-learnでグリッドサーチする方法買いた以前の記事の、
次の記事として、パイプラインを使ったモデルのグリッドサーチ方法を書く予定だったのを失念していたので紹介します。
(パイプラインそのもの紹介もまだなのでそのうち記事に起こします。)

公式のドキュメントはこちらです。
sklearn.pipeline.Pipeline
sklearn.model_selection.GridSearchCV

単一のモデルのグリッドサーチとの違いは、サーチ対象のパラメータを指定する時、
“変数名”: [変数のリスト]
で指定したところを、
“ステップ名”__”変数名”: [変数のリスト]
と指定するようにするだけです。

例を見た方がわかりやすいので、irisのデータと、
モデルは PCA + ロジスティック回帰 でやってみます。


# 必要なライブラリのインポート
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report

# データの準備
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
            iris.data,
            iris.target,
            test_size=0.2,
            stratify=iris.target,
)

# モデル(パイプライン)の作成
clf = Pipeline(
    [
        ("pca", PCA()),
        ("lr", LogisticRegression())
    ]
)

# 探索するパラメータの設定
# ここのkeyの指定方法が重要
params = {
    "pca__n_components": [2, 3, 4],
    "lr__penalty": ["l1", "l2"],
    "lr__C": [0.01, 0.1, 1, 10, 100]
}

# グリッドサーチ
gs_clf = GridSearchCV(
    clf,
    params,
    cv=5
)
gs_clf.fit(X_train, y_train)

# 最適なパラメーター
print(gs_clf.best_params_)

# 最適なモデルの評価
best_clf = gs_clf.best_estimator_
print(classification_report(y_test, best_clf.predict(X_test)))

# 以下出力結果

{'lr__C': 100, 'lr__penalty': 'l2', 'pca__n_components': 3}
             precision    recall  f1-score   support

          0       1.00      1.00      1.00        10
          1       1.00      1.00      1.00        10
          2       1.00      1.00      1.00        10

avg / total       1.00      1.00      1.00        30

最終的にテストデータでの評価が正解率100%なのは運が良かっただけです。
train_test_splitの結果次第で、実行するたびにbest_params_も評価も変わります。

次元削減とその後の分類機は、まとめて最適かしたいので非常に便利です。

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です。