Home About Contact
TensorFlow , Keras , MNIST

TensorFlow で MNIST その1

Keras のこの記事 https://keras.io/examples/vision/mnist_convnet/ を参考にしながら、最終的には これを TensorFlow.js に移植する試み。

今回は Python でモデルを構築して、実際に手描き数字を推測させてみるところまで進めます。

hand written numbers

事前準備

環境は以下の通り。

$ python3 --version
Python 3.8.17

必要なモジュールを入れる。

$ pip install tensorflow==2.13.0 pillow==10.0.0

データを入手して先頭データを画像に変換

まずは、keras.datasets.minst.load_data() を使って MNIST データを入手。

main.py

from tensorflow import keras

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

print( type(x_train) )
print( x_train.shape )

print( x_train.shape[0] )
print( x_train.shape[1] )
print( x_train.shape[2] )

python main.py すると以下のようになります。

$ python main.py
<class 'numpy.ndarray'>
(60000, 28, 28)
60000
28
28

numpy.ndarray のテンソルーデータの形状は (60000, 28, 28) で 60000 件の手描き数字データが 28x28 ピクセルで入っている状態。

この 28x28 は 行(y) x 列(x) の順になっているはず。

では、最初の手描き数字データ x_train[0] を JPEG に描画してみます。

main2.py

from tensorflow import keras
from PIL import Image

(x_train, _), (_, _) = keras.datasets.mnist.load_data()

IMAGE_SIZE_Y = x_train.shape[1]
IMAGE_SIZE_X = x_train.shape[2]
img = Image.new('L', (IMAGE_SIZE_X, IMAGE_SIZE_Y))

firstData = x_train[0]

for yIndex in range(firstData.shape[0]):
    for xIndex in range(firstData.shape[1]):
        value = firstData[yIndex][xIndex]
        img.putpixel((xIndex, yIndex), int(value))

img.save('0.jpg')

では実行します。

$ python main2.py

出来上がったのがこの画像 0.jpg です。

0.jpg

本などで何度か見た画像ですね。

トレーニングデータとテストデータの確認

このページ https://keras.io/examples/vision/mnist_convnet/ の先頭のコードを見ます。

main3.py

# Model / data parameters
num_classes = 10
input_shape = (28, 28, 1)

# Load the data and split it between train and test sets
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()

# Scale images to the [0, 1] range
x_train = x_train.astype("float32") / 255
x_test = x_test.astype("float32") / 255
# Make sure images have shape (28, 28, 1)
x_train = np.expand_dims(x_train, -1)
x_test = np.expand_dims(x_test, -1)
print("x_train shape:", x_train.shape)
print(x_train.shape[0], "train samples")
print(x_test.shape[0], "test samples")

# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

トレーニングデータを x_train, y_train にセット、 テストデータを x_test, y_test にセットしています。

x_hoge が入力用で y_hoge が出力(として期待される)データのようです。

実行してみます。

$ python main3.py
x_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples

トレーニングデータが 60000件、テストデータが 10000件あることがわかります。

入力用の x_hoge データの値を 255 で割って 0.0..1.0 の範囲に変更したり テンソルの形状を expand_dims(x_train, -1)expand_dims(x_test, -1) で変更している。 これは入力用のデータを(これから作成する)モデルが期待する入力形式(形状)に変換するための処理です。

最後の convert class vectors to binary class matrices が何やっているかわからなかったので調べます。

# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
print(y_train.shape)# (60000, 10)

keras.utils.to_categorical(y_train, num_classes) という謎の関数を通した結果 y_train の形状は (60000, 10) になった これは 6万件のデータがあり、それぞれ10個の数値がある。 10個の中身を見てみる。

for index in range(3):
    print( y_train[index] )

結果はこれ:

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

つまり、手描きした数画像が 0..9 のどれなのかを one-hot で表現している。 先頭は (0-index 方式で数えた場合)5番目が 1.0 になっているから 手描きした数画像 は 5 なのであろう。 次のは 0番目が 1.0 になっているから 0 の手描きした数画像 という意味。

モデルの構築とトレーニング

特に不明点はない。モデルを構築してトレーニングするコードです。 参考にしているページ https://keras.io/examples/vision/mnist_convnet/ ほぼそのままです。 ただし、最後にトレーニング結果を model.keras ファイルに保存する処理を追加しました。

main3.py にコードを追記します。

model = keras.Sequential(
    [
        keras.Input(shape=input_shape),
        layers.Conv2D(32, kernel_size=(3, 3), activation="relu"),
        layers.MaxPooling2D(pool_size=(2, 2)),
        layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
        layers.MaxPooling2D(pool_size=(2, 2)),
        layers.Flatten(),
        layers.Dropout(0.5),
        layers.Dense(num_classes, activation="softmax"),
    ]
)
#model.summary()

batch_size = 128
epochs = 15

model.compile(
    loss="categorical_crossentropy",
    optimizer="adam",
    metrics=["accuracy"])

model.fit(
    x_train,
    y_train,
    batch_size=batch_size,
    epochs=epochs, 
    validation_split=0.1)

score = model.evaluate(x_test, y_test, verbose=0)
print("Test loss:", score[0])
print("Test accuracy:", score[1])

model.save("model.keras")

実行してモデルを生成します。

$ python main3.py

model.keras ファイルがカレントディレクトリに生成されたのを確認しましょう。

このモデルを使って手描き数字を推測する

モデルが意図通り推測できるか確認しましょう。 自分で手描きした番号画像を用意しました。

hand written numbers

この画像を 0.jpg, 1.jpg, ... 9.jpg の10個のファイルに分けて保存します。 画像サイズは 28 x 28 にしておきます。

普通に JPEG をつくると RGB で(28, 28, 3) の shape を持つ画像になります。 そこでこれら画像をグレースケース画像へ変換しておく必要があります。

from PIL import Image, ImageOps

img = Image.open('0.jpg')
gray_img = img.convert('L')# グレースケール
gray_img_inverted = ImageOps.invert(gray_img)# 反転
gray_img_inverted.save('gray_0.jpg')

先に確認した先頭数字画像は背景が黒、数字のピクセルが白でした。 したがって、グレースケールにするだけでなく、反転させる必要もあります。

これで gray_0.jpg は (28, 28, 1) の shape を持つグレースケール画像に変換されました。

この例では、0.jpg を gray_0.jpg に変換しただけです。 以下のコードで gray_0.jpg .. gray_9.jpg まで全部のグレースケール jpg を生成しておきます。

from PIL import Image, ImageOps

def to_gray_inverted_image(image_path):
    img = Image.open(image_path)
    gray_img = img.convert('L')
    return ImageOps.invert(gray_img)

input_image_paths  = "0.jpg 1.jpg 2.jpg 3.jpg 4.jpg 5.jpg 6.jpg 7.jpg 8.jpg 9.jpg".split(" ")
images = list( map(to_gray_inverted_image,  input_image_paths) )
for index in range(len(images)):
    output_file_path = "gray_" + input_image_paths[index]
    images[index].save(output_file_path)

生成された画像は以下の通りです。

画像をテンソルに変換

gray_0.jpg がモデルで推測できるか試します。 推測のためには、gray_0.jpg 画像をこのモデルの入力形状のテンソルに変換する必要があります。

main4.py

import tensorflow as tf

image_path = "gray_0.jpg"
image_tensor0 = tf.io.read_file(image_path)
image_tensor1 = tf.image.decode_jpeg(image_tensor0, channels=1)
print(image_tensor1.shape)
for y in range(28):
    for x in range(28):
        print( image_tensor1[y][x] )

画像を読み込んで tf.image.decode_jpeg 関数でテンソルに変換します。 カレントディレクトリに gray_0.jpg があることを確認した上で、実行します。

$ python main4.py
(28, 28, 1)
tf.Tensor([255], shape=(1,), dtype=uint8)
tf.Tensor([250], shape=(1,), dtype=uint8)
tf.Tensor([253], shape=(1,), dtype=uint8)
tf.Tensor([255], shape=(1,), dtype=uint8)
tf.Tensor([252], shape=(1,), dtype=uint8)
...

テンソルの形状はよいのですが、その値が uint8 で 0..255 の値になっています。

トレーニングに使用した main3.py の先頭を見ると以下のようになっています。

# Scale images to the [0, 1] range
x_train = x_train.astype("float32") / 255

つまり、0..1 の float32 の値にしなければならないということです。 したがって、以下のようにして値を修正(0..1 の float32)します。

(image_tensor.astype("float32") / 255)

ただし、このコードは動きません。type(image_tensor) すればわかることですが、image_tensor の型は <class 'tensorflow.python.framework.ops.EagerTensor'> です。 これは astype できません。 astype できるのは numpy の ndarray にしておく必要がありました。 numpy の ndarray にするには image_tensor.numpy() するだけです。

(image_tensor.numpy().astype("float32") / 255)

これで期待されている値に修正できました。

main4.py

import tensorflow as tf

def toTensor(image_path):
    image_tensor0 = tf.io.read_file(image_path)
    return tf.image.decode_jpeg(image_tensor0, channels=1)

image_path = "gray_0.jpg"
image_tensor = toTensor(image_path)
print(image_tensor.shape)
print(type(image_tensor))

#image_numpy_tensor = (image_tensor.astype("float32") / 255)
image_numpy_tensor = (image_tensor.numpy().astype("float32") / 255)

for y in range(28):
    for x in range(28):
        print( image_numpy_tensor[y][x] )

実行してみると、確かに値が 0..1 の float32 になっています。

(28, 28, 1)
<class 'tensorflow.python.framework.ops.EagerTensor'>
[1.]
[0.98039216]
[0.99215686]
[1.]
[0.9882353]
[0.9843137]
...

モデルをロードして推測

いよいよ model.predict() します。 そのためにモデルをロードします。

import numpy as np
from tensorflow import keras

...

model = keras.models.load_model("model.keras")
predictions = model.predict(np.expand_dims(image_numpy_tensor, axis=0))
print( predictions.shape )

実行すると (1,10) の形状が出力されます。

したがって、以下のようにして、0..9 のどの数字と推測されたか確認できます。

firstPrediction = predictions[0]

for index in range( len(firstPrediction) ):
    v = firstPrediction[index]
    print( "- " + str(index) + " => " + '{:f}'.format(v))

実行します。

$ python main4.py
- 0 => 0.999364
- 1 => 0.000000
- 2 => 0.000007
- 3 => 0.000002
- 4 => 0.000002
- 5 => 0.000000
- 6 => 0.000421
- 7 => 0.000022
- 8 => 0.000181
- 9 => 0.000001

0..9 のそれぞれの確率が出ます。 0 が 0.999364 で最も高い数値が出ているので、0 と推測されたことになります。

それではまとめて gray_0 .. gray_9.jpg まで全部推測させてみます。

main5.py

import numpy as np
from tensorflow import keras
import tensorflow as tf

def toTensor(image_path):
    image_tensor0 = tf.io.read_file(image_path)
    return tf.image.decode_jpeg(image_tensor0, channels=1).numpy().astype("float32") / 255

image_path_names = "gray_0.jpg gray_1.jpg gray_2.jpg gray_3.jpg gray_4.jpg gray_5.jpg gray_6.jpg gray_7.jpg gray_8.jpg gray_9.jpg".split(" ")
image_numpy_tensors = np.array( list( map(toTensor,  image_path_names) ) )
#print( image_numpy_tensors.shape ) # (10, 28, 28, 1)

model = keras.models.load_model("model.keras")
predictions = model.predict(image_numpy_tensors)
#print( predictions.shape ) # (10, 10)
for index in range( len(predictions) ):
    prediction = predictions[index]
    maxValueIndex = np.argmax(prediction)
    maxValue      = np.max(prediction)
    print( "- " + image_path_names[index] + " => " + str(maxValueIndex) + "(" + "{:f}".format(maxValue) + ")")

model.predict するときに引数として与える image_numpy_tensors の形状に注意。 一件ずつ処理していたときは以下のように image_numpy_tensor をそのまま model.predict に渡さないで、 np.expand_dims で加工してから渡していました。

model.predict( np.expand_dims(image_numpy_tensor, axis=0) )

これは model.predict に渡すテンソルの形状を (1, 28, 28, 1) にするための処理です。

今回 10画像まとめてテンソル image_numpy_tensors に変換、その形状は (10, 28, 28, 1) になっているため、 これをそのまま model.predict に渡せます。 しかも、10画像(のテンソルを)まとめて一回で推測処理できる。

結果のテンソルから最大の値を探す(np.max)、それが何番目か(np.argmax)を調べる方法

実行します。

$ python main5.py
- gray_0.jpg => 0(0.999364)
- gray_1.jpg => 1(0.998180)
- gray_2.jpg => 2(0.847639)
- gray_3.jpg => 3(0.998167)
- gray_4.jpg => 4(0.998781)
- gray_5.jpg => 5(0.999974)
- gray_6.jpg => 6(0.967169)
- gray_7.jpg => 7(0.880870)
- gray_8.jpg => 8(0.991546)
- gray_9.jpg => 4(0.919192)

0..8 まではうまく推測できました。 9 は 4 と推測されてしまった。

9 は、かすれ気味の画像なので、線を太くした 9 9 を使って 推測させてみたのですが、結果は同じでした。

畳込み処理などをしているので、 太さの調整くらいでは推測結果は変化しないのかもしれません。

gray_9.jpg に対する推測結果の詳細を確認してみます。

prediction9 = predictions[9]
maxValueIndex = np.argmax(prediction9)
maxValue      = np.max(prediction9)
for i in range(len(prediction9)):
    v = prediction9[i]
    print( "- " + str(i) + " => " + '{:f}'.format(v))

実行すると以下のようになっていました。

- 0 => 0.000000
- 1 => 0.000005
- 2 => 0.000098
- 3 => 0.001375
- 4 => 0.919192
- 5 => 0.000121
- 6 => 0.000004
- 7 => 0.001202
- 8 => 0.000585
- 9 => 0.077418

本来は正しい 9 である確率は 0.077418 でした。

まとめ

モデルをトレーニングして、自分で描いた数字を推測させるところまでできました。次回は TensorFlow.js にこれを移植します。