Image classification

TensorFlow.org で表示 Google Colab で実行 GitHub でソースを表示 ノートブックをダウンロード

このチュートリアルでは、花の画像を分類する方法を示します。tf.keras.Sequentialモデルを使用して画像分類器を構築し、tf.keras.utils.image_dataset_from_directory を使用してデータを読み込みます。このチュートリアルでは、次の概念を実際に見ていきます。

  • ディスク上のデータセットを効率的に読み込みます。
  • 過学習を識別し、データ拡張やドロップアウトなどテクニックを使用して過学習を防ぎます。

このチュートリアルは、基本的な機械学習のワークフローに従います。

  1. データの調査及び理解
  2. 入力パイプラインの構築
  3. モデルの構築
  4. モデルの学習
  5. モデルのテスト
  6. モデルの改善とプロセスの繰り返し

パッケージのインポート

import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential
2022-08-09 02:04:08.939257: E tensorflow/stream_executor/cuda/cuda_blas.cc:2981] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2022-08-09 02:04:09.613740: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvrtc.so.11.1: cannot open shared object file: No such file or directory
2022-08-09 02:04:09.614036: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvrtc.so.11.1: cannot open shared object file: No such file or directory
2022-08-09 02:04:09.614050: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.

データの読み込み

このチュートリアルでは、約3,700枚の花の写真のデータセットを使用します。データセットには、クラスごとに1つずつ、5 つのサブディレクトリが含まれています。

flower_photo/
  daisy/
  dandelion/
  roses/
  sunflowers/
  tulips/
import pathlib
dataset_url = "https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz"
data_dir = tf.keras.utils.get_file('flower_photos', origin=dataset_url, untar=True)
data_dir = pathlib.Path(data_dir)
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz
228813984/228813984 [==============================] - 2s 0us/step

ダウンロード後、データセットのコピーが利用できるようになります。合計3,670枚の画像があります。

image_count = len(list(data_dir.glob('*/*.jpg')))
print(image_count)
3670

バラの画像です。

roses = list(data_dir.glob('roses/*'))
PIL.Image.open(str(roses[0]))

png

PIL.Image.open(str(roses[1]))

png

チューリップの画像です。

tulips = list(data_dir.glob('tulips/*'))
PIL.Image.open(str(tulips[0]))

png

PIL.Image.open(str(tulips[1]))

png

Keras ユーティリティを使用してデータを読み込む

便利な image_dataset_from_directory ユーティリティを使用して、これらの画像をディスクから読み込みます。これにより、数行のコードでディスク上の画像のディレクトリからtf.data.Datasetに移動します。また、画像を読み込んで前処理するチュートリアルにアクセスして、独自のデータ読み込みコードを最初から作成することもできます。

データセットを作成する

ローダーのいくつかのパラメーターを定義します。

batch_size = 32
img_height = 180
img_width = 180

モデルを開発するときは、検証分割を使用することをお勧めします。ここでは、画像の80%をトレーニングに使用し、20%を検証に使用します。

train_ds = tf.keras.utils.image_dataset_from_directory(
  data_dir,
  validation_split=0.2,
  subset="training",
  seed=123,
  image_size=(img_height, img_width),
  batch_size=batch_size)
Found 3670 files belonging to 5 classes.
Using 2936 files for training.
val_ds = tf.keras.utils.image_dataset_from_directory(
  data_dir,
  validation_split=0.2,
  subset="validation",
  seed=123,
  image_size=(img_height, img_width),
  batch_size=batch_size)
Found 3670 files belonging to 5 classes.
Using 734 files for validation.

クラス名は、これらのデータセットのclass_names属性にあります。 これらはアルファベット順にディレクトリ名に対応します。

class_names = train_ds.class_names
print(class_names)
['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']

データを視覚化する

以下はトレーニングデータセットの最初の 9 枚の画像です。

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 10))
for images, labels in train_ds.take(1):
  for i in range(9):
    ax = plt.subplot(3, 3, i + 1)
    plt.imshow(images[i].numpy().astype("uint8"))
    plt.title(class_names[labels[i]])
    plt.axis("off")

png

これらのデータセットをmodel.fitに渡すことで、これらのデータセットを使用してモデルをトレーニングします。 必要に応じて、データセットを手動で繰り返し、画像のバッチを取得することもできます。

for image_batch, labels_batch in train_ds:
  print(image_batch.shape)
  print(labels_batch.shape)
  break
(32, 180, 180, 3)
(32,)

image_batchは、形状(32, 180, 180, 3)のテンソルです。これは、形状180x180x3の 32 枚の画像のバッチです(最後の次元はカラーチャンネル RGB を参照します)。label_batchは、形状(32,)のテンソルであり、これらは 32 枚の画像に対応するラベルです。

image_batchおよびlabels_batchテンソルで.numpy()を呼び出して、それらをnumpy.ndarrayに変換できます。

パフォーマンスのためにデータセットを構成する

I/O がブロックされることなくディスクからデータを取得できるように、必ずバッファ付きプリフェッチを使用します。これらは、データを読み込むときに使用する必要がある 2 つの重要な方法です。

  • Dataset.cache()は、最初のエポック中に画像をディスクから読み込んだ後、メモリに保持します。これにより、モデルのトレーニング中にデータセットがボトルネックになることを回避できます。データセットが大きすぎてメモリに収まらない場合は、この方法を使用して、パフォーマンスの高いオンディスクキャッシュを作成することもできます。
  • Dataset.prefetch はトレーニング中にデータの前処理とモデルの実行をオーバーラップさせます。

以上の 2 つの方法とデータをディスクにキャッシュする方法についての詳細は、データパフォーマンスガイドプリフェッチを参照してください。

AUTOTUNE = tf.data.AUTOTUNE

train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

データを標準化する

RGB チャネル値は [0, 255] の範囲にあり、ニューラルネットワークには理想的ではありません。一般に、入力値は小さくする必要があります。

ここでは、tf.keras.layers.Rescaling を使用して、値を [0, 1] の範囲に標準化します。

normalization_layer = layers.Rescaling(1./255)

このレイヤーを使用するには 2 つの方法があります。Dataset.map を呼び出すことにより、データセットに適用できます。

normalized_ds = train_ds.map(lambda x, y: (normalization_layer(x), y))
image_batch, labels_batch = next(iter(normalized_ds))
first_image = image_batch[0]
# Notice the pixel values are now in `[0,1]`.
print(np.min(first_image), np.max(first_image))
0.0 0.9810147

または、モデル定義内にレイヤーを含めることができます。これにより、デプロイメントを簡素化できます。 ここでは 2 番目のアプローチを使用します。

注意:以前は、tf.keras.utils.image_dataset_from_directoryimage_size 引数を使用して画像のサイズを変更しました。モデルにサイズ変更ロジックも含める場合は、tf.keras.layers.Resizing レイヤーを使用できます。

モデルを作成する

Sequential モデルは、それぞれに最大プールレイヤー(tf.keras.layers.MaxPooling2D)を持つ3つの畳み込みブロック(tf.keras.layers.Conv2D)で構成されます。ReLU 活性化関数('relu')により活性化されたユニットが 128 個ある完全に接続されたレイヤー(tf.keras.layers.Dense)があります。このチュートリアルの目的は、標準的なアプローチを示すことなので、このモデルは高精度に調整されていません。

num_classes = len(class_names)

model = Sequential([
  layers.Rescaling(1./255, input_shape=(img_height, img_width, 3)),
  layers.Conv2D(16, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(32, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(64, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Flatten(),
  layers.Dense(128, activation='relu'),
  layers.Dense(num_classes)
])

モデルをコンパイルする

このチュートリアルでは、tf.keras.optimizers.Adam オプティマイザとtf.keras.losses.SparseCategoricalCrossentropy 損失関数を選択します。各トレーニングエポックのトレーニングと検証の精度を表示するには、Model.compilemetrics 引数を渡します。

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

モデルの概要

モデルの Model.summary メソッドを使用して、ネットワークのすべてのレイヤーを表示します。

model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 rescaling_1 (Rescaling)     (None, 180, 180, 3)       0         
                                                                 
 conv2d (Conv2D)             (None, 180, 180, 16)      448       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 90, 90, 16)       0         
 )                                                               
                                                                 
 conv2d_1 (Conv2D)           (None, 90, 90, 32)        4640      
                                                                 
 max_pooling2d_1 (MaxPooling  (None, 45, 45, 32)       0         
 2D)                                                             
                                                                 
 conv2d_2 (Conv2D)           (None, 45, 45, 64)        18496     
                                                                 
 max_pooling2d_2 (MaxPooling  (None, 22, 22, 64)       0         
 2D)                                                             
                                                                 
 flatten (Flatten)           (None, 30976)             0         
                                                                 
 dense (Dense)               (None, 128)               3965056   
                                                                 
 dense_1 (Dense)             (None, 5)                 645       
                                                                 
=================================================================
Total params: 3,989,285
Trainable params: 3,989,285
Non-trainable params: 0
_________________________________________________________________

モデルをトレーニングする

epochs=10
history = model.fit(
  train_ds,
  validation_data=val_ds,
  epochs=epochs
)
Epoch 1/10
92/92 [==============================] - 3s 19ms/step - loss: 1.2247 - accuracy: 0.4813 - val_loss: 1.1346 - val_accuracy: 0.5354
Epoch 2/10
92/92 [==============================] - 1s 15ms/step - loss: 0.9529 - accuracy: 0.6267 - val_loss: 0.9023 - val_accuracy: 0.6485
Epoch 3/10
92/92 [==============================] - 1s 15ms/step - loss: 0.7535 - accuracy: 0.7149 - val_loss: 0.8664 - val_accuracy: 0.6649
Epoch 4/10
92/92 [==============================] - 1s 15ms/step - loss: 0.5361 - accuracy: 0.8106 - val_loss: 0.9106 - val_accuracy: 0.6744
Epoch 5/10
92/92 [==============================] - 1s 16ms/step - loss: 0.3431 - accuracy: 0.8764 - val_loss: 0.9713 - val_accuracy: 0.6621
Epoch 6/10
92/92 [==============================] - 1s 15ms/step - loss: 0.2081 - accuracy: 0.9329 - val_loss: 1.1649 - val_accuracy: 0.6785
Epoch 7/10
92/92 [==============================] - 1s 15ms/step - loss: 0.1044 - accuracy: 0.9663 - val_loss: 1.3237 - val_accuracy: 0.6594
Epoch 8/10
92/92 [==============================] - 1s 15ms/step - loss: 0.0479 - accuracy: 0.9884 - val_loss: 1.7254 - val_accuracy: 0.6294
Epoch 9/10
92/92 [==============================] - 1s 15ms/step - loss: 0.0433 - accuracy: 0.9881 - val_loss: 1.6022 - val_accuracy: 0.6553
Epoch 10/10
92/92 [==============================] - 1s 15ms/step - loss: 0.0288 - accuracy: 0.9928 - val_loss: 1.9491 - val_accuracy: 0.6512

トレーニングの結果を視覚化する

トレーニングセットと検証セットで損失と精度のプロットを作成します。

acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs_range = range(epochs)

plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')

plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

png

プロットには、トレーニングの精度と検証の精度は大幅にずれており、モデルは検証セットで約 60% の精度しか達成していないことが示されています。

原因を調べ、モデルの全体的なパフォーマンスを向上させます。

過剰適合

上記のプロットでは、トレーニングの精度は時間の経過とともに直線的に増加していますが、検証の精度はトレーニングプロセスで約60%のままです。また、トレーニングと検証の精度に大きな違いがあり、これは過剰適合の兆候を示しています。

トレーニングサンプルの数が少ない場合、モデルは、トレーニングサンプルのノイズや不要な詳細から学習し、新しいサンプルでのモデルのパフォーマンスに悪影響を及ぼすことがあります。 この現象は過剰適合として知られています。 これは、モデルが新しいデータセットで一般化する上で問題があることを意味します。

トレーニングプロセスで過剰適合を回避する方法は複数あります。このチュートリアルでは、データ拡張を使用して、モデルにドロップアウトを追加します。

データ拡張

過剰適合は、一般に、トレーニングサンプルの数が少ない場合に発生します。データ拡張は、既存のサンプルに対してランダムな変換を使用してサンプルを拡張することにより、追加のトレーニングデータを生成します。これにより、モデルをデータのより多くの側面でトレーニングし、より一般化することができます。

tf.keras.layers.RandomFliptf.keras.layers.RandomRotation、および tf.keras.layers.RandomZoom の前処理レイヤーを使用して、データ拡張を実装します。これらは、他のレイヤーと同様にモデル内に含めて、GPU で実行できます。

data_augmentation = keras.Sequential(
  [
    layers.RandomFlip("horizontal",
                      input_shape=(img_height,
                                  img_width,
                                  3)),
    layers.RandomRotation(0.1),
    layers.RandomZoom(0.1),
  ]
)

同じ画像にデータ拡張を数回適用して、いくつかの拡張されたデータがどのようになるかを視覚化してみましょう。

plt.figure(figsize=(10, 10))
for images, _ in train_ds.take(1):
  for i in range(9):
    augmented_images = data_augmentation(images)
    ax = plt.subplot(3, 3, i + 1)
    plt.imshow(augmented_images[0].numpy().astype("uint8"))
    plt.axis("off")

png

データ拡張を使用して、モデルをトレーニングします。

ドロップアウト

過剰適合を回避するもう 1 つの方法は、ドロップアウト正則化をネットワークに導入することです。

ドロップアウトをレイヤーに適用すると、トレーニングプロセス中にレイヤーからいくつかの出力ユニットがランダムにドロップアウトされます(アクティベーションをゼロに設定することにより)。ドロップアウトは、0.1、0.2、0.4 などの形式で、入力値として小数を取ります。これは、適用されたレイヤーから出力ユニットの 10%、20%、または 40% をランダムにドロップアウトすることを意味します。

拡張された画像を使用してトレーニングする前に、tf.keras.layers.Dropout を使用して新しいニューラルネットワークを作成しましょう。

model = Sequential([
  data_augmentation,
  layers.Rescaling(1./255),
  layers.Conv2D(16, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(32, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(64, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Dropout(0.2),
  layers.Flatten(),
  layers.Dense(128, activation='relu'),
  layers.Dense(num_classes)
])

モデルをコンパイルしてトレーニングする

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])
model.summary()
Model: "sequential_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 sequential_1 (Sequential)   (None, 180, 180, 3)       0         
                                                                 
 rescaling_2 (Rescaling)     (None, 180, 180, 3)       0         
                                                                 
 conv2d_3 (Conv2D)           (None, 180, 180, 16)      448       
                                                                 
 max_pooling2d_3 (MaxPooling  (None, 90, 90, 16)       0         
 2D)                                                             
                                                                 
 conv2d_4 (Conv2D)           (None, 90, 90, 32)        4640      
                                                                 
 max_pooling2d_4 (MaxPooling  (None, 45, 45, 32)       0         
 2D)                                                             
                                                                 
 conv2d_5 (Conv2D)           (None, 45, 45, 64)        18496     
                                                                 
 max_pooling2d_5 (MaxPooling  (None, 22, 22, 64)       0         
 2D)                                                             
                                                                 
 dropout (Dropout)           (None, 22, 22, 64)        0         
                                                                 
 flatten_1 (Flatten)         (None, 30976)             0         
                                                                 
 dense_2 (Dense)             (None, 128)               3965056   
                                                                 
 dense_3 (Dense)             (None, 5)                 645       
                                                                 
=================================================================
Total params: 3,989,285
Trainable params: 3,989,285
Non-trainable params: 0
_________________________________________________________________
epochs = 15
history = model.fit(
  train_ds,
  validation_data=val_ds,
  epochs=epochs
)
Epoch 1/15
92/92 [==============================] - 3s 27ms/step - loss: 1.3658 - accuracy: 0.3913 - val_loss: 1.1980 - val_accuracy: 0.5218
Epoch 2/15
92/92 [==============================] - 2s 26ms/step - loss: 1.0472 - accuracy: 0.5838 - val_loss: 0.9624 - val_accuracy: 0.6294
Epoch 3/15
92/92 [==============================] - 2s 25ms/step - loss: 0.9316 - accuracy: 0.6339 - val_loss: 0.9279 - val_accuracy: 0.6417
Epoch 4/15
92/92 [==============================] - 2s 26ms/step - loss: 0.8792 - accuracy: 0.6557 - val_loss: 0.9192 - val_accuracy: 0.6512
Epoch 5/15
92/92 [==============================] - 2s 26ms/step - loss: 0.8396 - accuracy: 0.6713 - val_loss: 0.8316 - val_accuracy: 0.6785
Epoch 6/15
92/92 [==============================] - 2s 25ms/step - loss: 0.7726 - accuracy: 0.7010 - val_loss: 0.8146 - val_accuracy: 0.6771
Epoch 7/15
92/92 [==============================] - 2s 24ms/step - loss: 0.7517 - accuracy: 0.7119 - val_loss: 0.7949 - val_accuracy: 0.7044
Epoch 8/15
92/92 [==============================] - 2s 25ms/step - loss: 0.7127 - accuracy: 0.7228 - val_loss: 0.7524 - val_accuracy: 0.7193
Epoch 9/15
92/92 [==============================] - 2s 25ms/step - loss: 0.6895 - accuracy: 0.7333 - val_loss: 0.7363 - val_accuracy: 0.7098
Epoch 10/15
92/92 [==============================] - 2s 25ms/step - loss: 0.6604 - accuracy: 0.7449 - val_loss: 0.7850 - val_accuracy: 0.7016
Epoch 11/15
92/92 [==============================] - 2s 24ms/step - loss: 0.6307 - accuracy: 0.7568 - val_loss: 0.7790 - val_accuracy: 0.7112
Epoch 12/15
92/92 [==============================] - 2s 24ms/step - loss: 0.5990 - accuracy: 0.7711 - val_loss: 0.7173 - val_accuracy: 0.7302
Epoch 13/15
92/92 [==============================] - 2s 26ms/step - loss: 0.5846 - accuracy: 0.7841 - val_loss: 0.6794 - val_accuracy: 0.7425
Epoch 14/15
92/92 [==============================] - 2s 25ms/step - loss: 0.5482 - accuracy: 0.7851 - val_loss: 0.7331 - val_accuracy: 0.7180
Epoch 15/15
92/92 [==============================] - 2s 25ms/step - loss: 0.5343 - accuracy: 0.7916 - val_loss: 0.6951 - val_accuracy: 0.7411

トレーニングの結果を視覚化する

データ拡張と tf.keras.layers.Dropout を適用した後は、以前よりも過剰適合が少なくなり、トレーニングと検証がより高精度に調整されます。

acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs_range = range(epochs)

plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')

plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

png

新しいデータを予測する

最後に、モデルを使用して、トレーニングセットまたは検証セットに含まれていなかった画像を分類します。

注意:データ拡張レイヤーとドロップアウトレイヤーは、推論時に非アクティブになります。

sunflower_url = "https://storage.googleapis.com/download.tensorflow.org/example_images/592px-Red_sunflower.jpg"
sunflower_path = tf.keras.utils.get_file('Red_sunflower', origin=sunflower_url)

img = tf.keras.utils.load_img(
    sunflower_path, target_size=(img_height, img_width)
)
img_array = tf.keras.utils.img_to_array(img)
img_array = tf.expand_dims(img_array, 0) # Create a batch

predictions = model.predict(img_array)
score = tf.nn.softmax(predictions[0])

print(
    "This image most likely belongs to {} with a {:.2f} percent confidence."
    .format(class_names[np.argmax(score)], 100 * np.max(score))
)
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/592px-Red_sunflower.jpg
117948/117948 [==============================] - 0s 0us/step
1/1 [==============================] - 0s 116ms/step
This image most likely belongs to sunflowers with a 71.80 percent confidence.