TensorFlow解説、モデルの保存と復元

モデルの保存と復元
https://www.tensorflow.org/tutorials/keras/save_and_load

上記のTensorFlowチュートリアルを読んでいて、つまづいた部分のメモ。

pyyamlとは?

pip install -q pyyaml h5py  # HDF5フォーマットでモデルを保存するために必要

pyyamlは、yamlを扱うためのパッケージ。
yamlはxmlの親戚みたいなもの。
データフォーマットの1つ。

h5pyとは?

htpyは、HDF5を扱うためのパッケージ。
HDFはHierarchical Data Formatの略。
これもデータフォーマットの1つ。
HDF5は、読み書きの速度がCSVの100倍程度早い。
大量のデータを保存するのにおすすめ。

reshape(-1, 28 * 28)って何だ?

(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()

train_labels = train_labels[:1000]
test_labels = test_labels[:1000]

train_images = train_images[:1000].reshape(-1, 28 * 28) / 255.0
test_images = test_images[:1000].reshape(-1, 28 * 28) / 255.0

reshape()が配列の形状を変形するのは知ってる。
reshape(-1、28*28)の-1って何だ?

reshapeの機能をおさらい

import numpy as np

matrix = np.array(range(12))
matrix
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

0~11まで、12個の数字が並んだarrayを作りました。

matrix.reshape(2, 6)
array([[ 0,  1,  2,  3,  4,  5],
      [ 6,  7,  8,  9, 10, 11]])

reshape(2, 6)
を使うと
12個のただの数字だったのが
2行x6列のarrayに変換されました。

matrix.reshape(3, 4)
array([[ 0,  1,  2,  3],
      [ 4,  5,  6,  7],
      [ 8,  9, 10, 11]])

reshape(3, 4)
を使うと
3行x4列のarrayに変換されます。

matrix.reshape(2, 5)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-21-91dfb5063ee8> in <module>()
----> 1 matrix.reshape(2, 5)

ValueError: cannot reshape array of size 12 into shape (2,5)

reshape(2, 5)
にするとエラーになります。
余りが出て12個の数字をキレイに並べ替えられないので。

reshape(-1)って何?

matrix.reshape(3, -1)
array([[ 0,  1,  2,  3],
      [ 4,  5,  6,  7],
      [ 8,  9, 10, 11]])

reshape(3, -1)

reshape(3, 4)
と書いたときのまったく同じになりました。

元のデータ数が12個なんだから、
reshape(3, ○○)
と書くときに、○○の部分に当てはまる数字は「4」以外にありえません。

reshape(3, -1)
みたいな書き方をすれば
-1の部分に本来入るべき数字を自動計算してくれます。

reshape(-1, 4)
にしたら
-1の部分は「3」になるべきってことをpython側が勝手に計算してくれます。

train_images[:1000].reshape(-1, 28 * 28)とは?

train_images[:1000].shape
(1000, 28, 28)

train_images[:1000]の中身は
「縦28ピクセルx横28ピクセルの画像データ」が1000枚入っています。

train_images[:1000].shape

(1000, 28, 28)
なので
1000 x 28 x 28 = 784000
合計784,000個の数字が入った3次元配列です。

reshape(-1, 28 * 28)
を書き直すと
reshape(-1, 784)
ということです。

784,000個の数字をreshapeする場合
reshape(○○, 784)
の○○の部分に入る数字は「1000」以外にありえません。

ということで、
reshape(-1, 784)

reshape(1000, 784)
と同じ意味です。

つまり、
train_images[:1000]

(1000, 28, 28)
という形状の3次元配列でしたが、
train_images[:1000].reshape(-1, 28 * 28)
により、
(1000, 784)
という形状の2次元配列に変換されます。

tf.keras.callbacks.ModelCheckpoint()とは?

# チェックポイントコールバックを作る
cp_callback = tf.keras.callbacks.ModelCheckpoint(checkpoint_path, 
                                                save_weights_only=True,
                                                verbose=1)

model = create_model()

model.fit(train_images,
         train_labels,
         epochs=10, 
         validation_data=(test_images, test_labels),
         callbacks=[cp_callback]  # 訓練にコールバックを渡す
         )

tf.keras.callbacks.ModelCheckpoint()
は、モデルを保存するためのコールバックを生成している。

model.fit()
のオプションとして
callbacks=[cp_callback]
のように
tf.keras.callbacks.ModelCheckpoint()
で作成したコールバックをセットして使う。

save_weights_only=Trueとは?

save_weights_only=True
ならば、重みだけを保存。
save_weights_only=False
ならば、モデルそのものを保存。

save_weights_only=Trueの場合

model = tf.keras.models.Sequential([
   keras.layers.Dense(512, activation='relu', input_shape=(784,)),
   keras.layers.Dropout(0.2),
   keras.layers.Dense(10, activation='softmax')
 ])
 
model.compile(optimizer='adam',
               loss='sparse_categorical_crossentropy',
               metrics=['accuracy'])

model.load_weights(checkpoint_path)

save_weights_only=True
を使ってモデルの重みだけ保存した場合、
保存した重みを再利用するには

ステップ1
model = tf.keras.models.Sequential()
でモデルを作成

ステップ2
model.compile()
でモデルをコンパイル

ステップ3
model.load_weights()
でモデルの重みを読み込み

この3ステップを経て、保存前のモデルと同じ状態が復元できます。

save_weights_only=Falseの場合

new_model = tf.keras.models.load_model('my_model.h5')

save_weights_only=False
の場合、重みだけでなくモデルそのものを保存します。

すると、
tf.keras.models.load_model()
だけで保存前のモデルと同じ状態を復元できます。

「モデルを作成、コンパイル、重みを読み込み」
という3ステップを経ずに
「モデルを読み込む」
という1ステップだけで済みます。

checkpointファイル

# チェックポイントコールバックを作る
cp_callback = tf.keras.callbacks.ModelCheckpoint(checkpoint_path, 
                                                save_weights_only=True,
                                                verbose=1)

model = create_model()

model.fit(train_images,
         train_labels,
         epochs=10, 
         validation_data=(test_images, test_labels),
         callbacks=[cp_callback]  # 訓練にコールバックを渡す
         )

tf.keras.callbacks.ModelCheckpoint()
を使ってモデル保存すると

01_checkpointファイルの構成

こんな感じのファイルが保存されます。

cp.ckpt.data-00000-of-00001
というのが本体で
cp.ckpt.index
というのがインデックスファイル。

複数のサーバーで並列処理をする場合は、以下のように
cp.ckpt.data-00000-of-00002
cp.ckpt.data-00001-of-00002
ナンバリングされた複数のファイルに分割されて保存されるらしいです。
cp.ckpt.index
には、どっちのファイルにどのデータが入っているかを管理しています。

checkpointファイルを別名で保存

# ファイル名に(`str.format`を使って)エポック数を埋め込みます
checkpoint_path = "training_2/cp-{epoch:04d}.ckpt"
checkpoint_dir = os.path.dirname(checkpoint_path)

cp_callback = tf.keras.callbacks.ModelCheckpoint(
   checkpoint_path,
   verbose=1,
   save_weights_only=True,
   period=5  # 重みを5エポックごとに保存します
)

model = create_model()
model.fit(train_images,
         train_labels,
         epochs=50,
         callbacks=[cp_callback],
         validation_data=(test_images, test_labels),
         verbose=0
         )

チェックポイントファイルを保存するパスを
checkpoint_path = "training_2/cp-{epoch:04d}.ckpt"
のように指定すると
model.fit()
の実行中に
{epoch:04d}
の部分が
0000
0001
0002
0003
のように0埋めされた4桁のエポック番号になります。

つまり、
training_2
というディレクトリに

エポック1
cp-0001.ckpt.data-00000-of-00001
cp-0001.ckpt.index
というファイルが保存される。

エポック2
cp-0002.ckpt.data-00000-of-00001
cp-0002.ckpt.index
というファイルが保存される。

エポック3
cp-0003.ckpt.data-00000-of-00001
cp-0003.ckpt.index
というファイルが保存される。

という感じで、エポックごとに別々のファイル名でチェックポイントファイルが保存されます。

checkpoint_path = "training_2/cp-{epoch:04d}.ckpt"
の部分を
checkpoint_path = "training_2/cp.ckpt"
と書いた場合、
({epoch:04d}という記述がない場合)
エポックごとにチェックポイントファイルの保存は実行されているのですが
毎回同じファイル名で保存が実行されます。
つまり、最終エポックで実行された分しか記録が残りません。


model.save_weights

# `checkpoint_path` フォーマットで重みを保存
model.save_weights(checkpoint_path.format(epoch=0))

model.save_weights()
だけでもモデル内の重みを保存できます。

checkpoint_path.format(epoch=0)
の部分は
checkpoint_path = "training_2/cp-{epoch:04d}.ckpt"
なので、

model.save_weights('training_2/cp-0000.ckpt')

と書いたのと同じ意味。

つまり、
training_2
というディレクトリに
cp-0000.ckpt
というファイル名で
現時点でのmodelの重みを保存します。

実際には
cp-0000.ckpt.data-00000-of-00001
cp-0000.ckpt.index
という2つのファイルに分かれて保存されます。
cp-0000.ckpt.data-00000-of-00001がデータの本体。
cp-0000.ckpt.index はデータのインデックス。

tf.train.latest_checkpoint()とは?

latest = tf.train.latest_checkpoint(checkpoint_dir)
latest
training_2/cp-0050.ckpt

tf.train.latest_checkpoint(checkpoint_dir)
を実行すると
training_2/cp-0050.ckpt
という文字列が返ってきました。

checkpoint_dirの中身はtraining_2です。

! ls {checkpoint_dir}
checkpoint			  cp-0030.ckpt.data-00000-of-00001
cp-0005.ckpt.data-00000-of-00001  cp-0030.ckpt.index
cp-0005.ckpt.index		  cp-0035.ckpt.data-00000-of-00001
cp-0010.ckpt.data-00000-of-00001  cp-0035.ckpt.index
cp-0010.ckpt.index		  cp-0040.ckpt.data-00000-of-00001
cp-0015.ckpt.data-00000-of-00001  cp-0040.ckpt.index
cp-0015.ckpt.index		  cp-0045.ckpt.data-00000-of-00001
cp-0020.ckpt.data-00000-of-00001  cp-0045.ckpt.index
cp-0020.ckpt.index		  cp-0050.ckpt.data-00000-of-00001
cp-0025.ckpt.data-00000-of-00001  cp-0050.ckpt.index
cp-0025.ckpt.index

training_2
というディレクトリの下に

checkpoint
cp-0005.ckpt.data-00000-of-00001
cp-0005.ckpt.index
cp-0010.ckpt.data-00000-of-00001
cp-0010.ckpt.index
cp-0015.ckpt.data-00000-of-00001
cp-0015.ckpt.index
cp-0020.ckpt.data-00000-of-00001
cp-0020.ckpt.index
cp-0025.ckpt.data-00000-of-00001
cp-0025.ckpt.index
cp-0030.ckpt.data-00000-of-00001
cp-0030.ckpt.index
cp-0035.ckpt.data-00000-of-00001
cp-0035.ckpt.index
cp-0040.ckpt.data-00000-of-00001
cp-0040.ckpt.index
cp-0045.ckpt.data-00000-of-00001
cp-0045.ckpt.index
cp-0050.ckpt.data-00000-of-00001
cp-0050.ckpt.index

上記のように無数のファイルがあります。

tf.train.latest_checkpointを使うと、
これらのファイルの中で
cp-0050.ckptが一番新しいやつ
というのを自動で認識してくれます。

tf.keras.models.load_model()が機能していない

new_model = tf.keras.models.load_model('saved_model/my_model')

loss, acc = new_model.evaluate(test_images,  test_labels, verbose=2)
print('Restored model, accuracy: {:5.2f}%'.format(100*acc))
32/32 - 0s - loss: 0.4424 - accuracy: 0.0970
Restored model, accuracy:  9.70%

tf.keras.models.load_model()
でモデルをロードして
new_model.evaluate()
でテストデータを評価したところ
accuracy: 0.0970
と表示されました。

保存したモデルは正解率80~90%はあったはず。
それなのに、そのモデルを保存してロードしたら同じ正解率にならないとおかしい。

モデルの保存と復元
このTFチュートリアルの中での実行結果がそもそも
32/32 - 0s - loss: 0.4491 - accuracy: 0.0900
となっています。

チュートリアルの時点で既にコードの書き方が間違っているか、
.save()か.load_model()のモジュール自体がバグを含んでいるんじゃないかと。

model.weights[0]
<tf.Variable 'dense_30/kernel:0' shape=(784, 512) dtype=float32, numpy=
array([[-0.05910126,  0.04120973, -0.02469405, ..., -0.01897333,
        0.00531963,  0.05598266],
      [-0.01659369,  0.02629411,  0.06420413, ...,  0.03532334,
        0.04486157, -0.02987623],
      [ 0.04702625,  0.06235819,  0.03279167, ..., -0.03596481,
       -0.00378699,  0.00882454],
      ...,
      [ 0.0038208 ,  0.02677497, -0.04603399, ...,  0.02877764,
        0.00265282,  0.02529848],
      [ 0.06700519,  0.04117873,  0.01949715, ..., -0.05494581,
       -0.0140672 , -0.02758908],
      [-0.06017714, -0.00531104, -0.03072125, ..., -0.0546917 ,
       -0.01459077, -0.06649452]], dtype=float32)>

元のmodelの重みを確認するとこんな感じ。

new_model.weights[0]
<tf.Variable 'dense_16/kernel:0' shape=(784, 512) dtype=float32, numpy=
array([[ 0.05394331,  0.05020889,  0.01937916, ...,  0.01872102,
        0.02037995,  0.01786885],
      [ 0.04964604,  0.05964319, -0.02135467, ...,  0.02342577,
       -0.06318826, -0.01665034],
      [ 0.028431  , -0.03131487, -0.0382145 , ..., -0.03004158,
       -0.04191445, -0.05662533],
      ...,
      [-0.02580648,  0.05219092, -0.00857667, ..., -0.04783525,
       -0.02737705,  0.06309691],
      [-0.06421142,  0.03545997,  0.02429625, ..., -0.0581848 ,
       -0.01469767, -0.01143724],
      [ 0.05249915,  0.01770395,  0.01906303, ...,  0.04500877,
        0.02094163, -0.03682106]], dtype=float32)>

load_model()で読み込んだnew_modelの重みを確認してみました。
保存元のmodelとまったく同じです。
weightsのsaveはできています。

model.summary()
Model: "sequential_15"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_30 (Dense)             (None, 512)               401920    
_________________________________________________________________
dropout_15 (Dropout)         (None, 512)               0         
_________________________________________________________________
dense_31 (Dense)             (None, 10)                5130      
=================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
_________________________________________________________________

model.summary()

new_model.summary()
でモデルの構造を見比べてみましたが、まったく同じでした。
modelの構造もちゃんと保存されている。

tf.keras.models.load_model?

Google Colaboratoryでは、末尾に?をつけるとヘルプを確認できます。

load_model
のオプションに
compile
というのがありました。
もしかしてこれが原因かな?

new_model = tf.keras.models.load_model('saved_model/my_model',
                                       compile=True)

tf.keras.models.load_model()
のオプションに
compile=True
を追加してみました。
しかし、
new_model.evaluate()
の結果はやはりaccuraryが0.09代(正解率9%代)

compileのデータがロードできないんじゃなくて
そもそもcompileのデータが保存できてない可能性は?

new_model = tf.keras.models.load_model('saved_model/my_model')

new_model.compile(optimizer='adam', 
               loss='sparse_categorical_crossentropy',
               metrics=['accuracy'])

loss, acc = new_model.evaluate(test_images,  test_labels, verbose=2)
print('Restored model, accuracy: {:5.2f}%'.format(100*acc))
32/32 - 0s - loss: 0.4424 - accuracy: 0.8650
Restored model, accuracy: 86.50%

なんと、正解率86.50%になりました。

モデルがコンパイルされていなかったのが原因でした。

tf.keras.models.load_model()
new_model.evaluate()
という手順ではだめだったけど

tf.keras.models.load_model()
new_model.compile()
new_model.evaluate()
という手順にすれば保存前のmodelと同じ状態になった。

ということは、
model.save()
をしたときに
compile
のデータが保存されていなかったんでしょうね。

Tensorflowのマニュアルを読むと
「.save()時にcompileの情報は自動的に保存されます」
みたいなことが書いてあったんだけどな。

なぜcompileの情報が欠落するのか、
compile情報を正しく保存するにはどうすればいいのか
については謎のまま。

とりあえずは、
tf.keras.models.load_model()
new_model.compile()
new_model.evaluate()
のように、load_model()のあとにcompileを再度実行すれば動くというのが確認できたので、今のところはOK。

いいなと思ったら応援しよう!