はじめに
MLPを使った簡単な画像分類を実装方法を紹介していこうと思います.
理論も大事ですが、実装して動かさないとわからないことも多いので、とりあえず動かしてみるということはとても大事なことだと思います.
この記事ではMLPの画像分類を題材に、tensorflowを使って基本的なニューラルネットワークの定義から学習までを一通り実装していきます.
本記事のソースコードはこちらで実行できるので、是非動かしてみてくだい.
https://colab.research.google.com/drive/10znVBfU2yw2mh2XFHasSmwSR5PuLPxyY?usp=sharing
データセット
データセットにはFashionMnistを用います.
FashionMnistはモノクロの服のイラストとラベルがセットになったデータセットです.
学習用に60000枚,テスト用に10000枚用意されています.
従来の手書き文字のMNISTでは簡単すぎるため、その代替として作られました.
Step1: ネットワークの定義
まずネットワークを定義します.
今回はシンプルにニューロン数128の中間層を2つ挟んでみます.
Tensorrflowでの書き方は大きく
-
keras.Sequentialを用いる ( https://www.tensorflow.org/guide/keras/sequential_model?hl=ja)
-
keras.Modelを継承して定義する(https://www.tensorflow.org/api_docs/python/tf/keras/Model)
の2通りありますが、今回は2の方法で紹介します.
1はかなりサンプルコードが溢れているのと、よりモデルが複雑になった際に、拡張性や可読性の面で、2の方に優位性があるため、今後の発展を考えると2のやり方に慣れておくといいかと思います.
import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras import losses
from tensorflow.keras import Model
class MLPClassifier(Model):
def __init__(self, num_classes: int, optimizer):
super().__init__()
self.flat = layers.Flatten()
self.d1 = layers.Dense(128, activation='relu')
self.d2 = layers.Dense(128, activation='relu')
self.d3 = layers.Dense(num_classes, activation='softmax')
self.optimizer = optimizer
self.loss_function = losses.SparseCategoricalCrossentropy()
def call(self, x):
x = self.flat(x)
x = self.d1(x)
x = self.d2(x)
y = self.d3(x)
return y
@tf.function
def train_step(self, x, y_true):
"""
Args:
x: shape (batch_size, height, width, channels)
labels: shape (batch_size, num_labels, 1)
"""
with tf.GradientTape() as tape:
y = self.call(x)
loss = self.loss_function(y_true, y)
grad = tape.gradient(loss, self.trainable_variables)
self.optimizer.apply_gradients(zip(grad, self.trainable_variables))
return y, loss
def test_step(self, images, y_true):
y = self.call(images)
loss = self.loss_function(y_true, y)
return y, loss
Step2: メトリクスの定義
メトリクスとは評価指標のことです.
機械学習全般で一般的に、損失と精度の二つを記録してプロットすることが多いです.
損失
- Step1のモデル内で計算しているので、平均をとります.
精度
- 単純に正解と予測のラベルが一致している割合を計算します.
- SparseCategoricalAccuracyを使います.
from tensorflow.keras import metrics
class MLPMetrics:
def __init__(self):
self.train_loss = metrics.Mean(name='train_loss')
self.train_accuracy = metrics.SparseCategoricalAccuracy(name='train_accuracy')
self.test_loss = metrics.Mean(name='test_loss')
self.test_accuracy = metrics.SparseCategoricalAccuracy(name='test_accuracy')
def update_on_train(self, y_true, y, loss):
self.train_loss(loss)
self.train_accuracy(y_true, y)
def update_on_test(self, y_true, y, loss):
self.test_loss(loss)
self.test_accuracy(y_true, y)
def reset(self):
self.train_loss.reset_states()
self.train_accuracy.reset_states()
self.test_loss.reset_states()
self.test_accuracy.reset_states()
def summary(self):
return dict(
train_loss=self.train_loss.result(),
train_accuracy=self.train_accuracy.result() * 100,
test_loss=self.test_loss.result(),
test_accuracy=self.test_accuracy.result() * 100
)
Step3: 学習器の定義
学習する際の処理を書いてます.
kerasのfit APIを使えばいいのですが、今後の応用も考えてカスタムでの書き方を紹介します.
基本は
-
バッチ化したデータをループで取得
-
ネットワークに突っ込んで予測と損失を取得
-
メトリクスに記録
-
1~3を繰り返す
の流れです
1~3全体ステップをエポックと言います
class MLPTrainer:
def __init__(self, model, metrics, history):
self.model = model
self.metrics = metrics
self.history = history
def fit(self, train_ds, epochs: int = 3):
""" Train mdoel by epochs
"""
for e in range(1, epochs + 1):
# train model
for images, labels in train_ds.train_loop():
pred, loss = self.model.train_step(images, labels)
self.metrics.update_on_train(labels, pred, loss)
# validate model
for images, labels in train_ds.test_loop():
pred, loss = self.model.test_step(images, labels)
self.metrics.update_on_test(labels, pred, loss)
# get metrics
summary = self.metrics.summary()
self.metrics.reset()
# diisplay learnig state
self.history.update(summary)
self.history.display_current()
# plot learnig process
self.history.plot_learning_process()
class MLPTrainHistory:
def __init__(self):
self.init()
def init(self):
self.train_accuracies = []
self.train_losses = []
self.test_accuracies = []
self.test_losses = []
self.epochs = 0
def update(self, summary: dict):
self.train_losses.append(summary["train_loss"])
self.train_accuracies.append(summary["train_accuracy"])
self.test_losses.append(summary["test_loss"])
self.test_accuracies.append(summary["test_accuracy"])
self.epochs += 1
def display_current(self):
template = 'Epoch {}, Loss: {:.2g}, Accuracy: {:.2g} Test Loss: {:.2g}, Test Accuracy: {:.2g}'
print(
template.format(
self.epochs,
self.train_losses[-1],
self.train_accuracies[-1],
self.test_losses[-1],
self.test_accuracies[-1]
)
)
def plot_learning_process(self):
epochs = range(1, self.epochs+1)
plt.plot(epochs, self.train_accuracies, label='Train Accuracy', ls="-", marker="o")
plt.plot(epochs, self.test_accuracies, label='Validation Accuracy', ls="-", marker="x")
plt.title('Accuracy')
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
plt.plot(epochs, self.train_losses, label='Training Accuracy', ls="-", marker="o")
plt.plot(epochs, self.test_losses, label='Validation Accuracy', ls="-", marker="x")
plt.title('Loss')
plt.ylabel("loss")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
Step4: データセットの定義
FashionMnistをロードして学習で使える形に以下の加工をしています
-
255で割って正規化
-
tf.data.Dataset.from_tensor_slicesでバッチ化
import matplotlib.pyplot as plt
from tensorflow.keras import datasets
def build_image_labels_dataset(images, labels, batch_size=32):
dataset = tf.data.Dataset.from_tensor_slices(
(images, labels)
).batch(batch_size)
return dataset
class FashionMnistDataset:
class_names = (
'T-shirt/top',
'Trouser',
'Pullover',
'Dress',
'Coat',
'Sandal',
'Shirt',
'Sneaker',
'Bag',
'Ankle boot')
def __init__(self):
(train_images, train_labels), (test_images, test_labels) = datasets.fashion_mnist.load_data()
self.train_images = train_images.astype("float32") / 255.0
self.train_labels = train_labels
self.test_images = test_images.astype("float32") / 255.0
self.test_labels = test_labels
def info(self):
return dict(
train=dict(
shape=self.train_images.shape,
size=len(self.train_images)
),
test=dict(
shape=self.test_images.shape,
size=len(self.test_images)
),
)
def get_train_image(self, i: int):
return self.train_images[i]
def get_train_label_name(self, i: int):
return self.class_names[self.train_labels[i]]
def display(self):
plt.figure(figsize=(10,10))
for i in range(25):
plt.subplot(5,5,i+1)
plt.xticks([])
plt.yticks([])
plt.grid(False)
plt.imshow(self.get_train_image(i), cmap=plt.cm.binary)
plt.xlabel(self.get_train_label_name(i))
def train_loop(self, batch_size: int = 28):
ds = build_image_labels_dataset(self.train_images, self.train_labels)
return ds
def test_loop(self):
ds = build_image_labels_dataset(self.test_images, self.test_labels)
return ds
Step5: 各クラスの初期化
定義した各クラスを初期化します
from tensorflow.keras import optimizers
# モデル初期化
mlp_classifier = MLPClassifier(num_classes=10, optimizer=optimizers.Adam())
# メトリクス初期化
mlp_metrics = MLPMetrics()
# 履歴初期化
histiry = MLPTrainHistory()
# 学習用クラス初期化
mlp_trainer = MLPTrainer(mlp_classifier, mlp_metrics, histiry)
# データセットクラス初期化
fashion_mnist_dataset = FashionMnistDataset()
Step6: データの可視化
データの中身を見てみましょう
fashion_mnist_dataset.info()
結果
{'test': {'shape': (10000, 28, 28), 'size': 10000},
'train': {'shape': (60000, 28, 28), 'size': 60000}}
プロットしてみます
fashion_mnist_dataset.display()
Step7: 学習の実行
学習を実行していきます.
適当にエポック数10で回してます.
学習曲線を見ると3エポックくらいから学習データに対して精度が上がってるけど、評価データに対してはあまり上がっていないですね
これは一般的に過学習と言われる現象で、ニューラルネットの学習の課題の1つになります.
ここで詳細に触れませんでしたが、学習における課題についてはまた別の機会に書こうと思います.
学習実行
mlp_trainer.fit(fashion_mnist_dataset, epochs=10)
実行結果
おわりに
最後まで読んでいただきありがとうございました!