MoviNet を使った動画分類の転移学習

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

MoViNet (Mobile Video Networks)は、ストリーミング動画の推論をサポートする効率的な一連の動画分類モデルを提供しています。このチュートリアルでは、事前トレーニング済みの MoviNet モデルを使用して、特に UCF101 データセットからの行動認識タスク用に動画を分類します。事前トレーニング済みのモデルは、過去に大規模なデータセットでトレーニングされたものを保存したネットワークです。MoviNet についての詳細は、2021 年に発表された Kondratyuk, D. et al による論文 MoViNets: Mobile Video Networks for Efficient Video Recognition をご覧ください。このチュートリアルでは、以下の内容を学習します。

  • 事前トレーニング済みの MoviNet モデルをダウンロードする方法を学習します。
  • MoviNet モデルの畳み込みベースを凍結することで、新しい分類器を備えた事前トレーニング済みのモデルを使用して新しいモデルを作成します。
  • 分類器のヘッドを新しいデータセットのラベル数に置き換えます。
  • UCF101 データセットで、移転学習を実行します。

このチュートリアルでは、official/projects/movinet からモデルをダウンロードします。このリポジトリには、TF Hub が TensorFlow 2 SavedModel 形式で使用する一連の MoviNet モデルが含まれます。

この転移学習チュートリアルは、TensorFlow 動画チュートリアルシリーズの第 3 部です。他に、以下の 3 つのチュートリアルがあります。

  • 動画データを読み込む: このチュートリアルでは、このドキュメントで使用されているほとんどのコードが説明されています。特に、FrameGenerator クラスでデータを前処理して読み込む方法について、より詳しく説明されています。
  • 動画分類用の 3D CNN モデルを構築する。このチュートリアルは、3D データの空間と時間の側面を分解する (2+1)D CNN が使用されています。MRI スキャンなどの体積データを使用している場合は、(2+1)D CNN ではなく、3D CNN を使用することを検討してください。
  • MoviNet でストリーミングの行動認識を実行する: TF Hub で提供されている MoviNet モデルについて説明されています。

セットアップ

まず、ZIP ファイルの内容を検査するための remotezip、進捗バーを使用するための tqdm、動画ファイルを処理するための OpenCVopencv-pythonopencv-python-headless のバージョンが同じであることを確認してください)、および事前トレーニング済みの MoviNet モデルをダウンロードするための TensorFlow モデル(tf-models-official)を含む、必要なライブラリのインストールとインポートを行います。TensorFlow モデルパッケージは、TensorFlow の高レベル API を使用するモデルのコレクションです。

pip install remotezip tqdm opencv-python==4.5.2.52 opencv-python-headless==4.5.2.52 tf-models-official
import tqdm
import random
import pathlib
import itertools
import collections

import cv2
import numpy as np
import remotezip as rz
import seaborn as sns
import matplotlib.pyplot as plt

import keras
import tensorflow as tf
import tensorflow_hub as hub
from tensorflow.keras import layers
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.losses import SparseCategoricalCrossentropy

# Import the MoViNet model from TensorFlow Models (tf-models-official) for the MoViNet model
from official.projects.movinet.modeling import movinet
from official.projects.movinet.modeling import movinet_model
2024-01-11 20:18:28.764589: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2024-01-11 20:18:28.764633: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2024-01-11 20:18:28.766277: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered

データを読み込む

以下の非表示セルは、UCF-101 データセットからデータスライスをダウンロードして tf.data.Dataset に読み込むヘルパー関数を定義します。このコードの詳細は、動画データの読み込みチュートリアルをご覧ください。

ここでは、非表示ブロックの最後にある FrameGenerator クラスが最も重要なユーティリティです。TensorFlow データパイプラインにデータをフィードでキルイテレート可能なオブジェクトを作成します。特に、このクラスには、エンコードされたラベルとともに動画フレームを読み込む Python ジェネレータが含まれます。このジェネレータ(__call__)関数は、frames_from_video_file とフレームセットに関連するラベルのワンホットエンコードのベクトルを生成します。

URL = 'https://storage.googleapis.com/thumos14_files/UCF101_videos.zip'
download_dir = pathlib.Path('./UCF101_subset/')
subset_paths = download_ufc_101_subset(URL, 
                        num_classes = 10, 
                        splits = {"train": 30, "test": 20}, 
                        download_dir = download_dir)
train :
100%|██████████| 300/300 [00:21<00:00, 14.23it/s]
test :
100%|██████████| 200/200 [00:12<00:00, 15.54it/s]

トレーニングとテストのデータセットを作成します。

batch_size = 8
num_frames = 8

output_signature = (tf.TensorSpec(shape = (None, None, None, 3), dtype = tf.float32),
                    tf.TensorSpec(shape = (), dtype = tf.int16))

train_ds = tf.data.Dataset.from_generator(FrameGenerator(subset_paths['train'], num_frames, training = True),
                                          output_signature = output_signature)
train_ds = train_ds.batch(batch_size)

test_ds = tf.data.Dataset.from_generator(FrameGenerator(subset_paths['test'], num_frames),
                                         output_signature = output_signature)
test_ds = test_ds.batch(batch_size)

ここで生成されるラベルは、クラスのエンコーディングを表します。たとえば、'ApplyEyeMakeup' は整数にマッピングされます。トレーニングデータのラベルを見て、データセットが十分にシャッフルされていることを確認します。

for frames, labels in train_ds.take(10):
  print(labels)
tf.Tensor([5 9 2 7 4 9 3 8], shape=(8,), dtype=int16)
tf.Tensor([8 3 5 4 8 8 9 5], shape=(8,), dtype=int16)
tf.Tensor([4 4 2 3 8 2 1 1], shape=(8,), dtype=int16)
tf.Tensor([2 4 3 1 3 0 8 9], shape=(8,), dtype=int16)
tf.Tensor([8 2 0 5 6 5 2 9], shape=(8,), dtype=int16)
tf.Tensor([6 6 9 7 3 0 8 3], shape=(8,), dtype=int16)
tf.Tensor([2 4 1 8 0 8 4 5], shape=(8,), dtype=int16)
tf.Tensor([3 6 9 9 0 5 6 1], shape=(8,), dtype=int16)
tf.Tensor([4 1 3 8 7 1 8 0], shape=(8,), dtype=int16)
tf.Tensor([5 7 8 2 4 1 7 8], shape=(8,), dtype=int16)

データの形状を確認します。

print(f"Shape: {frames.shape}")
print(f"Label: {labels.shape}")
Shape: (8, 8, 224, 224, 3)
Label: (8,)

MoViNets とは?

前にも述べたとおり、MoViNets は、アクション認識などのタスクでビデオのストリーミングやオンライン推論に使用されるビデオ分類モデルです。アクション認識向けに動画データを分類するには、MoViNets を使用することを検討してください。

動画全体またはフレームごとのストリーミングに対して実行するには、2D フレームベースの分類器が有効でシンプルです。時間的なコンテキストを考慮できないため、精度が制限され、フレームごとに一貫性のない出力が得られる可能性があります。

シンプル 3D CNN は、双方向の時間コンテキストを使用して、精度と時間の一貫性を高めることができます。 これらのネットワークはより多くのリソースを必要とする可能性があり、将来を見据えているため、データのストリーミングには使用できません。

標準的な畳み込み

MoViNet アーキテクチャは、時間軸に沿って「因果論的」な 3D 畳み込みを使用しています(padding="causal" を使った layers.Conv1D など)。これにより、両方のアプローチのいくつかのメリットが得られますが、主に効率的なストリーミングが可能になります。

カジュアルな畳み込み

コーザル(因果論的)畳み込みでは、時間 t の出力を、確実に時間 t までの入力のみを使って計算できます。このことがストリーミングの効率をどれ程高められるかを示すために、おそらく使い慣れている RNN というより単純な例から始めることにしましょう。RNN は状態を時間の経過とともに渡します。

RNN モデル

gru = layers.GRU(units=4, return_sequences=True, return_state=True)

inputs = tf.random.normal(shape=[1, 10, 8]) # (batch, sequence, channels)

result, state = gru(inputs) # Run it all at once

RNN の return_sequences=True 引数を設定して、計算の最後に状態を戻すように指定します。こうすることで、一時停止して停止した場所から再開しても、まったく同じ結果を得られるようになります。

RNN を通過する状態

first_half, state = gru(inputs[:, :5, :])   # run the first half, and capture the state
second_half, _ = gru(inputs[:,5:, :], initial_state=state)  # Use the state to continue where you left off.

print(np.allclose(result[:, :5,:], first_half))
print(np.allclose(result[:, 5:,:], second_half))
True
True

コーザル畳み込みは、注して処理すれば、同じように使用できます。この手法は、Le Paine et al によって Fast Wavenet Generation Algorithm で使用されています。MoVinet 論文において、state は「ストリームバッファ」と呼ばれています。

コーザル畳み込みを通過する状態

この小さな状態を前方に渡していくことで、上に示した受容野全体を再計算する必要がなくなります。

事前トレーニング済みの MoviNet モデルをダウンロードする

このセクションでは、以下のことを行います。

  1. official/projects/movinet に提供されるオープンソースコードを使用して、TensorFlow モデルから MoviNet モデルを作成します。
  2. 事前トレーニング済みの重みを読み込みます。
  3. 畳み込みベース、または最終的な分類器ヘッドを除くすべてのレイヤーを凍結してファインチューニングを高速化します。

モデルを構築するには、他のモデルに対してベンチマークしたときに最も速い a0 構成から始めます。ユースケースに応じてどれが適しているかを知るには、TensorFlow Model Garden で利用可能な MoviNet モデルをご覧ください。

model_id = 'a0'
resolution = 224

tf.keras.backend.clear_session()

backbone = movinet.Movinet(model_id=model_id)
backbone.trainable = False

# Set num_classes=600 to load the pre-trained weights from the original model
model = movinet_model.MovinetClassifier(backbone=backbone, num_classes=600)
model.build([None, None, None, None, 3])

# Load pre-trained weights
!wget https://storage.googleapis.com/tf_model_garden/vision/movinet/movinet_a0_base.tar.gz -O movinet_a0_base.tar.gz -q
!tar -xvf movinet_a0_base.tar.gz

checkpoint_dir = f'movinet_{model_id}_base'
checkpoint_path = tf.train.latest_checkpoint(checkpoint_dir)
checkpoint = tf.train.Checkpoint(model=model)
status = checkpoint.restore(checkpoint_path)
status.assert_existing_objects_matched()
movinet_a0_base/
movinet_a0_base/checkpoint
movinet_a0_base/ckpt-1.data-00000-of-00001
movinet_a0_base/ckpt-1.index
<tensorflow.python.checkpoint.checkpoint.CheckpointLoadStatus at 0x7f6b0a395fd0>

分類器を構築するために、データセット内のバックボーンとクラス数を受け取る関数を作成します。build_classifier 関数は、データセット内のバックボーンとクラス数を受け取って、分類器を構築します。この場合、新しい分類器は、num_classes 出力(UCF101 のこのサブセットの 10 クラス)を取ります。

def build_classifier(batch_size, num_frames, resolution, backbone, num_classes):
  """Builds a classifier on top of a backbone model."""
  model = movinet_model.MovinetClassifier(
      backbone=backbone,
      num_classes=num_classes)
  model.build([batch_size, num_frames, resolution, resolution, 3])

  return model
model = build_classifier(batch_size, num_frames, resolution, backbone, 10)

このチュートリアルでは、tf.keras.optimizers.Adam オプティマイザと tf.keras.losses.SparseCategoricalCrossentropy 損失関数を選択します。metrics 引数を使用して、各ステップでのモデルパフォーマンスの精度を確認します。

num_epochs = 2

loss_obj = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

optimizer = tf.keras.optimizers.Adam(learning_rate = 0.001)

model.compile(loss=loss_obj, optimizer=optimizer, metrics=['accuracy'])

モデルをトレーニングします。2 エポック後に、トレーニングセットとテストセットの両方で高精度の低損失を確認します。

results = model.fit(train_ds,
                    validation_data=test_ds,
                    epochs=num_epochs,
                    validation_freq=1,
                    verbose=1)
Epoch 1/2
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1705004378.923000  476492 device_compiler.h:186] Compiled cluster using XLA!  This line is logged at most once for the lifetime of the process.
2024-01-11 20:19:49.425506: W external/local_tsl/tsl/framework/bfc_allocator.cc:296] Allocator (GPU_0_bfc) ran out of memory trying to allocate 34.33GiB with freed_by_count=0. The caller indicates that this is not a failure, but this may mean that there could be performance gains if more memory were available.
2024-01-11 20:19:49.445254: W external/local_tsl/tsl/framework/bfc_allocator.cc:296] Allocator (GPU_0_bfc) ran out of memory trying to allocate 34.33GiB with freed_by_count=0. The caller indicates that this is not a failure, but this may mean that there could be performance gains if more memory were available.
38/Unknown - 62s 1s/step - loss: 0.9607 - accuracy: 0.7633
2024-01-11 20:20:28.853326: W external/local_tsl/tsl/framework/bfc_allocator.cc:296] Allocator (GPU_0_bfc) ran out of memory trying to allocate 34.05GiB with freed_by_count=0. The caller indicates that this is not a failure, but this may mean that there could be performance gains if more memory were available.
2024-01-11 20:20:28.872781: W external/local_tsl/tsl/framework/bfc_allocator.cc:296] Allocator (GPU_0_bfc) ran out of memory trying to allocate 34.05GiB with freed_by_count=0. The caller indicates that this is not a failure, but this may mean that there could be performance gains if more memory were available.
38/38 [==============================] - 90s 2s/step - loss: 0.9607 - accuracy: 0.7633 - val_loss: 0.2026 - val_accuracy: 0.9750
Epoch 2/2
38/38 [==============================] - 53s 1s/step - loss: 0.1268 - accuracy: 0.9733 - val_loss: 0.1281 - val_accuracy: 0.9750

モデルを評価する

モデルはトレーニングデータセットで高い精度を達成しました。次に、Keras Model.evaluate を使用して、テストセットで評価します。

model.evaluate(test_ds, return_dict=True)
25/25 [==============================] - 21s 848ms/step - loss: 0.1162 - accuracy: 0.9850
{'loss': 0.11623553931713104, 'accuracy': 0.9850000143051147}

モデルパフォーマンスをさらに可視化するには、混同行列を使用します。混同行列では、精度を超えて分類モデルのパフォーマンスを評価することができます。このマルチクラス分類問題の混同行列を作成するために、テストセットの実際の値と予測される値を取得します。

def get_actual_predicted_labels(dataset):
  """
    Create a list of actual ground truth values and the predictions from the model.

    Args:
      dataset: An iterable data structure, such as a TensorFlow Dataset, with features and labels.

    Return:
      Ground truth and predicted values for a particular dataset.
  """
  actual = [labels for _, labels in dataset.unbatch()]
  predicted = model.predict(dataset)

  actual = tf.stack(actual, axis=0)
  predicted = tf.concat(predicted, axis=0)
  predicted = tf.argmax(predicted, axis=1)

  return actual, predicted
def plot_confusion_matrix(actual, predicted, labels, ds_type):
  cm = tf.math.confusion_matrix(actual, predicted)
  ax = sns.heatmap(cm, annot=True, fmt='g')
  sns.set(rc={'figure.figsize':(12, 12)})
  sns.set(font_scale=1.4)
  ax.set_title('Confusion matrix of action recognition for ' + ds_type)
  ax.set_xlabel('Predicted Action')
  ax.set_ylabel('Actual Action')
  plt.xticks(rotation=90)
  plt.yticks(rotation=0)
  ax.xaxis.set_ticklabels(labels)
  ax.yaxis.set_ticklabels(labels)
fg = FrameGenerator(subset_paths['train'], num_frames, training = True)
label_names = list(fg.class_ids_for_name.keys())
actual, predicted = get_actual_predicted_labels(test_ds)
plot_confusion_matrix(actual, predicted, label_names, 'test')
25/25 [==============================] - 27s 810ms/step

次のステップ

MoviNet モデルと様々な TensorFlow API(移転学習用の API など)の知識をいくらか得たので、このチュートリアルのコードを自分のデータセットで使用してみましょう。データは動画データに限る必要はありません。MRI スキャンなどの体積データを 3D CNN で使用することも可能です。Brain MRI-based 3D Convolutional Neural Networks for Classification of Schizophrenia and Controls で言及されている NUSDAT と IMH データセットを MRI データのソースとして使用することができます。

特に、このチュートリアルや、他の動画データと分類チュートリアルで使用された FrameGenerator クラスを使うと、モデルにデータを読み込みやすくなります。

TensorFlow での動画の操作についての詳細は、以下のチュートリアルをご覧ください。