概要
このチュートリアルでは,kmnistデータに対して,
- データの読み込み
- データをプロットして確認
- 前処理
- kerasを用いてCNNモデルの作成,学習
- 後識別したデータを確認
を行います.
環境
- python 3.6.8
- tensorflow 1.13.1
- numpy 1.16.1
- matplotlib 3.0.3
データのロード
まずはデータの読み込みをしてみましょう.
import numpy as np
import os
class KMNISTDataLoader(object):
"""
Example
-------
>>> kmnist_dl = KMNISTDataLoader()
>>> datapath = "./data"
>>> train_imgs, train_lbls, validation_imgs, validation_lbls = kmnist_dl.load(datapath)
"""
def __init__(self, validation_size: float):
"""
validation_size : float
[0., 1.]
ratio of validation data
"""
self._basename_list = [
'kmnist-train-imgs.npz',\
'kmnist-train-labels.npz'
]
self.validation_size = validation_size
def load(self, datapath: str, random_seed: int=13) -> np.ndarray:
filenames_list = self._make_filenames(datapath)
data_list = [np.load(filename)['arr_0'] for filename in filenames_list]
all_imgs, all_lbls = data_list
np.random.seed(random_seed)
perm_idx = np.random.permutation(len(all_imgs))
all_imgs = all_imgs[perm_idx]
all_lbls = all_lbls[perm_idx]
validation_num = int(len(all_lbls)*self.validation_size)
validation_imgs = all_imgs[:validation_num]
validation_lbls = all_lbls[:validation_num]
train_imgs = all_imgs[validation_num:]
train_lbls = all_lbls[validation_num:]
return train_imgs, train_lbls, validation_imgs, validation_lbls
def _make_filenames(self, datapath: str) -> list:
filenames_list = [os.path.join(datapath, basename) for basename in self._basename_list]
return filenames_list
データのフォーマットが.npz
なので,numpy
のnp.load
関数を使って読み込みます.
それ以外のコードは,データを保存した場所(datapath
)を渡すだけで,そこから読み込んでくれるようにするための処理です.
ここで定義したクラスを使うことで,以下のようにしてデータをロードすることができます.
datapath = "./data"
validation_size = 0.2
train_imgs, train_lbls, validation_imgs, validation_lbls = KMNISTDataLoader(validation_size).load(datapath)
validation_sizeではテストデータの比率を指定しており,ここでは2割のデータをテストデートとして扱っています.
プロットしてみよう
データを各クラスごとに,どんな画像データなのか表示してみます.
ここではプロットにmatplotlib
を用います.
import numpy as np
import matplotlib.pyplot as plt
class RandomPlotter(object):
def __init__(self):
self.label_char = ["お(o)", "き(ki)", "す(su)", "つ(tsu)",\
"な(na)", "は(ha)", "ま(ma)", "や(ya)",\
"れ(re)", "を(wo)"]
plt.rcParams['font.family'] = 'IPAPGothic'
def _get_unique_labels(self, labels: np.ndarray) -> np.ndarray:
label_unique = np.sort(np.unique(labels))
return label_unique
def _get_random_idx_list(self, labels: np.ndarray) -> list:
label_unique = self._get_unique_labels(labels)
random_idx_list = []
for label in label_unique:
label_indices = np.where(labels == label)[0]
random_idx = np.random.choice(label_indices)
random_idx_list.append(random_idx)
return random_idx_list
def plot(self, images: np.ndarray, labels: np.ndarray) -> None:
"""
Parameters
----------
images : np.ndarray
train_imgs or validation_imgs
labels : np.ndarray
train_lbls or validation_lbls
"""
random_idx_list = self._get_random_idx_list(labels)
fig = plt.figure()
for i, idx in enumerate(random_idx_list):
ax = fig.add_subplot(2, 5, i+1)
ax.tick_params(labelbottom=False, bottom=False)
ax.tick_params(labelleft=False, left=False)
img = images[idx]
ax.imshow(img, cmap='gray')
ax.set_title(self.label_char[i])
fig.show()
このコードでは,各クラスについて一つずつランダムにデータを取り出して,それをプロットしています.
__init__()
のplt.rcParams['font.family'] = 'IPAPGothic'
では,matplotlib
のフォントを日本語に対応したものに変更しています.
もしもこのフォントがないとエラーが出る場合は,このフォントを入れるか,すでにある別の日本語対応フォントに変更してください.
_get_random_idx_list()
では,各クラスごとにランダムにデータのインデックスを抜き出しています.
plot()
内が実際に画像をプロットするコードで,matplotlib
のimshow
を用いて表示しています.
ここで定義したクラスを用いると,以下のようにしてデータをプロットすることができます.
RandomPlotter().plot(train_imgs, train_lbls)
RandomPlotter().plot(validation_imgs, validation_lbls)
出力は以下のようになり,どういったくずし字データなのか確認できます.
前処理
データの前処理を行います.
ここでは,画像データに対しては,
- 数値データの型を
float32
へ変更
- 画像の
ndarray
のshapeを(N, 28, 28)
から(N, 28, 28, 1)
に変更(Nは画像の枚数)
- 値を[0, 255]から[0, 1]に標準化
を行います.
kmnistのデータは1枚あたり(28, 28)
のサイズですが,カラーの画像データであればRGBの三色で(28, 28, 3)
の多次元配列で表されます.kmnistのデータが白黒で1次元なので,省略された(28, 28, 1)
を補っています.
ラベルデータに対しては,
- 0から9の
int
で表されたラベルを,one-hot表現に変更
を行います.
import numpy as np
from tensorflow.keras.utils import to_categorical
class Preprocessor(object):
def transform(self, train_imgs, train_lbls, validation_imgs, validation_lbls):
train_imgs, validation_imgs = self._convert_imgs_dtypes(train_imgs, validation_imgs)
train_imgs, validation_imgs = self._convert_imgs_shape(train_imgs, validation_imgs)
train_imgs, validation_imgs = self._normalize(train_imgs, validation_imgs)
train_lbls, validation_lbls = self._to_categorical_labels(train_lbls, validation_lbls)
return train_imgs, train_lbls, validation_imgs, validation_lbls
def _convert_imgs_dtypes(self, train_imgs, validation_imgs):
_train_imgs = train_imgs.astype('float32')
_validation_imgs = validation_imgs.astype('float32')
return _train_imgs, _validation_imgs
def _convert_imgs_shape(self, train_imgs, validation_imgs):
_train_imgs = train_imgs[:,:,:,np.newaxis]
_validation_imgs = validation_imgs[:,:,:,np.newaxis]
return _train_imgs, _validation_imgs
def _normalize(self, train_imgs, validation_imgs):
_train_imgs = train_imgs / 255.0
_validation_imgs = validation_imgs / 255.0
return _train_imgs, _validation_imgs
def _to_categorical_labels(self, train_lbls, validation_lbls):
label_num = len(np.unique(train_lbls))
_train_lbls = to_categorical(train_lbls, label_num)
_validation_lbls = to_categorical(validation_lbls, label_num)
return _train_lbls, _validation_lbls
ここで定義したコードを用いると,以下のように前処理を行うことができます.
train_imgs, train_lbls, validation_imgs, validation_lbls = Preprocessor().transform(train_imgs, train_lbls, validation_imgs, validation_lbls)
識別してみよう
DNNのフレームワークであるkerasを用いて簡易なCNNを作成して識別してみましょう.
kerasはtensorflowに統合され,tensorflowの高レベルAPIとなっているので,tensorflowからインポートします.
(tensorflow.kerasがないというエラーが出た場合は,古いバージョンのtensorflowを使用している可能性があるので,tensorflowをアップデートしてみてください.)
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.models import Sequential
from tensorflow.keras import layers
from tensorflow.keras import backend as K
datapath = "./data"
train_imgs, train_lbls, validation_imgs, validation_lbls = KMNISTDataLoader(validation_size).load(datapath)
train_imgs, train_lbls, validation_imgs, validation_lbls = Preprocessor().transform(train_imgs, train_lbls, validation_imgs, validation_lbls)
batch_size = 128
label_num = 10
epochs = 5
input_shape = (28, 28, 1)
model = Sequential()
model.add(layers.Conv2D(32, kernel_size=(3, 3),\
activation='relu',\
input_shape=input_shape))
model.add(layers.MaxPooling2D(pool_size=(2, 2)))
model.add(layers.Dropout(0.25))
model.add(layers.Conv2D(64, kernel_size=(3, 3),\
activation='relu'))
model.add(layers.MaxPooling2D(pool_size=(2, 2)))
model.add(layers.Dropout(0.4))
model.add(layers.Flatten())
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(label_num, activation='softmax'))
loss = keras.losses.categorical_crossentropy
optimizer = keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999)
model.compile(loss=loss, optimizer=optimizer, metrics=['accuracy'])
model.fit(train_imgs, train_lbls,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(validation_imgs, validation_lbls))
train_score = model.evaluate(train_imgs, train_lbls)
validation_score = model.evaluate(validation_imgs, validation_lbls)
print('Train loss :', train_score[0])
print('Train accuracy :', train_score[1])
print('validation loss :', validation_score[0])
print('validation accuracy :', validation_score[1])
convolution+poolingのレイヤーを二段重ねたモデルです.
CNNを用いた画像の識別モデルの中ではシンプルな部類に入ると思いますが,95から97%ほどのaccになります.
これをベースラインとして改善してみましょう.
誤識別したデータの確認
95から97%の精度と聞くと良さそうに聞こえますが,残りがどんなデータなのか気になってきます.
そこで,誤識別した画像に限ってプロットしてみましょう.
class MisclassifiedDataPlotter(object):
"""
このクラスへの入力はpreprocess処理済みのデータを仮定する.
"""
def __init__(self):
self.label_char = ["お(o)", "き(ki)", "す(su)", "つ(tsu)",\
"な(na)", "は(ha)", "ま(ma)", "や(ya)",\
"れ(re)", "を(wo)"]
plt.rcParams['font.family'] = 'IPAPGothic'
def _convert_onehot2intvec(self, labels):
labels_int_vec = np.argmax(labels, axis=1)
return labels_int_vec
def _get_mixclassified_idx_list(self, labels_intvec, pred_labels_intvec):
misclassified = labels_intvec != pred_labels_intvec
mis_idxs_list = np.where(misclassified == True)[0]
return mis_idxs_list
def plot(self, images, labels, pred_labels, plot_num: int=5):
"""
>>> images.shape
(10000, 28, 28, 1)
>>> labels
array([[0., 0., 1., ..., 0., 0., 0.],
...,
[0., 0., 1., ..., 0., 0., 0.]], dtype=float32)
>>> pred_labels
array([[2.8434190e-06, 4.1375683e-06, 9.9899501e-01, ..., 3.7393205e-05,
2.5519948e-05, 3.0874473e-04],
...,
[4.1747628e-09, 1.8852282e-07, 9.9982470e-01, ..., 1.4897050e-07,
7.9116326e-05, 3.7392081e-05]], dtype=float32)
"""
labels_intvec = self._convert_onehot2intvec(labels)
pred_labels_intvec = self._convert_onehot2intvec(pred_labels)
mis_idxs_list = self._get_mixclassified_idx_list(labels_intvec, pred_labels_intvec)
random_idx_list = list(np.random.choice(mis_idxs_list, size=plot_num, replace=False))
fig = plt.figure()
for i, idx in enumerate(random_idx_list):
ax = fig.add_subplot(1, plot_num, i+1)
ax.tick_params(labelbottom=False, bottom=False)
ax.tick_params(labelleft=False, left=False)
img = images[idx].reshape((28,28))
ax.imshow(img, cmap='gray')
actual_label = self.label_char[labels_intvec[idx]]
pred_label = self.label_char[pred_labels_intvec[idx]]
ax.set_title(f"{pred_label} : actual {actual_label}")
fig.show()
このコードは,前処理をした後のデータを入力すると仮定しています.
つまり,画像データは(28, 28)
から(28, 28, 1)
に変更され,ラベルがone-hot表現になったデータです.
ここで定義したクラスを用いると,以下のようにして誤識別した画像を確認できます.
prediction = model.predict(validation_imgs)
mis_plotter = MisclassifiedDataPlotter()
mis_plotter.plot(validation_imgs, validation_lbls, prediction, plot_num=5)
右から二番目の本来は「な」である画像を「れ」と認識しているケースなどは,人間の目で見ても識別が難しいと思われます.