NNabla モデルのファインチューニングチュートリアル

ここでは、 nnabla の学習済みモデルを使用してファインチューニングを実行する方法を説明します。

モデルを読み込む

モデルの読み込みは非常に簡単です。 必要なのは 2 行だけです。

from nnabla.models.imagenet import ResNet18
model = ResNet18()

引数としてモデルの名前を指定することにより、 ResNet34ResNet50 などの他の ResNet モデルを選択できます。もちろん、他の学習済みモデルを選択することも可能です。 ドキュメント をご覧ください。

: ResNet18 を初めて使用する場合、nnabla は自動的に https://nnabla.org から重みをダウンロードします。これは数分かかる場合があります。

データセット

このチュートリアルでは、ファインチューニング用のデータセットとして Caltech101 を使用します。 Caltech101 は合計 9,000 を超えるオブジェクト画像で構成され、各画像は 101 の異なるカテゴリの 1 つ、または “clutter” カテゴリに属します。単純な分類としては 101 のカテゴリの画像を使用します。

データセットを自動的にダウンロードして nnabla_data に保存できる、 caltech101_data.py という名前のスクリプトを用意しました。

独自のデータセットと、データを読み込むことができる DataIterator がある場合は、代わりにそれを使用できます。

run caltech101_data.py
batch_size = 32  # we set batch_size = 32
all_data = data_iterator_caltech101(batch_size)

caltech101 には学習と検証のための個別のデータがないため、 手動 で分割する必要があります。ここでは、データセットを次のように分割します。 80% を学習、そして 20% を検証。

num_samples = all_data.size
num_train_samples = int(0.8 * num_samples)  # Take 80% for training, and the rest for validation.
num_class = 101
data_iterator_train = all_data.slice(
        rng=None, slice_start=0, slice_end=num_train_samples)
data_iterator_valid = all_data.slice(
        rng=None, slice_start=num_train_samples, slice_end=num_samples)

これでモデルとデータが用意できました !

オプショナル : データセット内の画像の確認

データセットにどのような画像があるか見てみましょう。 に、 DataIterator メソッドで画像を取得します。

import matplotlib.pyplot as plt
%matplotlib inline
images, labels = data_iterator_train.next()
sample_image, sample_label = images[0], labels[0]
plt.imshow(sample_image.transpose(1,2,0))
plt.show()
print("image_shape: {}".format(sample_image.shape))
print("label_id: {}".format(sample_label))
../../_images/model_finetuning_11_0.png
image_shape: (3, 128, 128)
label_id: [94]

グラフ作成の準備

基本的なモジュールのインポートから始めましょう。

import nnabla as nn

# Optional: If you want to use GPU
from nnabla.ext_utils import get_extension_context
ctx = get_extension_context("cudnn")
nn.set_default_context(ctx)
ext = nn.ext_utils.import_extension_module("cudnn")

ネットワークの入力 Variable を作成する

次に、入力 Variable を作成します。

channels, image_height, image_width = sample_image.shape  # use info from the image we got

# input variables for the validation network
image_valid = nn.Variable((batch_size, channels, image_height, image_width))
label_valid = nn.Variable((batch_size, 1))
input_image_valid = {"image": image_valid, "label": label_valid}

# input variables for the training network
image_train = nn.Variable((batch_size, channels, image_height, image_width))
label_train = nn.Variable((batch_size, 1))
input_image_train = {"image": image_train, "label": label_train}

学習済みモデルを使用して学習用グラフを作成する

モデルの API リファレンス を参照すると、 use_up_to オプションがあることがわかります。モデルを呼び出す時に、事前定義された文字列の 1 つを指定すると、指定した層まで計算グラフが作成されます。例えば、 ResNet18 の場合、グラフの最後の層として次のいずれかを選択できます。

  • ‘classifier’ ( デフォルト ) : 分類のための最後の Affine 層の出力。

  • ‘pool’ : 最終的な Global Average Pooling 出力。

  • ‘lastconv’ : ReLU 活性化なしの Global Average Pooling の入力。

  • ‘lastconv+relu’ : ‘lastconv’ ReLU 活性化。

ファインチューニングでは、上位層のみを新しい ( 学習されていない ) 層に置き換え、下位層を学習済み重みで再利用するのが一般的です。また、学習済みモデルは、 1000 のカテゴリを持つ ImageNet の分類タスクで学習されているため、 分類 層の出力には、現在のデータセットに適合しない出力形状 ( batch_size、1000 ) があります。このため、ここでは元のグラフにおける Global Average Pooling 層に対応する プール 層までグラフを作成し、 101 カテゴリ分類のために Affine ( 全結合 ) レイヤに繋げます。ファインチューニングでは、新しく追加された層 ( この場合は最後の Affine 層 ) の重みのみを学習するのが一般的ですが、このチュートリアルでは、グラフ内の すべて の層の重みを更新します。また、学習用グラフを作成するときは、 training = True に設定する必要があります。

import nnabla.parametric_functions as PF

y_train = model(image_train, force_global_pooling=True, use_up_to="pool", training=True)
with nn.parameter_scope("finetuning_fc"):
    pred_train = PF.affine(y_train, 101)  # adding the affine layer to the graph.

: モデルに対して想定している入力形状と異なる場合は、 force_global_pooling = True を指定する必要があります。 model.input_shape と入力して、モデルのデフォルトの入力形状を確認できます。

モデルを使用して検証用グラフを作成する

ほぼ同様の方法で検証用グラフを作成できます。 training フラグを False に変更するだけです。

y_valid = model(image_valid,
                force_global_pooling=True, use_up_to="pool", training=False)
with nn.parameter_scope("finetuning_fc"):
    pred_valid = PF.affine(y_valid, 101)
pred_valid.persistent = True  # to keep the value when get `forward(clear_buffer=True)`-ed.

ロスと分類誤差計算のための関数を定義する

import nnabla.functions as F


def loss_function(pred, label):
    """
        Compute loss.
    """
    loss = F.mean(F.softmax_cross_entropy(pred, label))
    return loss

loss_valid = loss_function(pred_valid, label_valid)
top_1_error_valid = F.mean(F.top_n_error(pred_valid, label_valid))
loss_train = loss_function(pred_train, label_train)
top_1_error_train = F.mean(F.top_n_error(pred_train, label_train))

Solver を準備する

import nnabla.solvers as S

solver = S.Momentum(0.01)  # you can choose others as well

solver.set_parameters(nn.get_parameters())

イテレーションのための設定

num_epoch = 10  # arbitrary
one_epoch = data_iterator_train.size // batch_size
max_iter = num_epoch * one_epoch
val_iter = data_iterator_valid.size // batch_size

ファインチューニング前のパフォーマンス

モデルがどれほど 良く 機能するかを見てみましょう。最後の Affine 層を除いて、すべての重みが ImageNet で学習済みであることに注意してください。 まず、モデルのパフォーマンスを示す関数を準備します。

def run_validation(pred_valid, loss_valid, top_1_error_valid,
                   input_image_valid, data_iterator_valid,
                   with_visualized=False, num_visualized=3):
    assert num_visualized < pred_valid.shape[0], "too many images to plot."
    val_iter = data_iterator_valid.size // pred_valid.shape[0]
    ve = 0.
    vloss = 0.
    for j in range(val_iter):
        v_image, v_label = data_iterator_valid.next()
        input_image_valid["image"].d = v_image
        input_image_valid["label"].d = v_label
        nn.forward_all([loss_valid, top_1_error_valid], clear_no_need_grad=True)
        vloss += loss_valid.d.copy()
        ve += top_1_error_valid.d.copy()

    vloss /= val_iter
    ve /= val_iter

    if with_visualized:
        ind = 1
        random_start = np.random.randint(pred_valid.shape[0] - num_visualized)
        fig = plt.figure(figsize=(12., 12.))
        for n in range(random_start, random_start + num_visualized):
            sample_image, sample_label = v_image[n], v_label[n]
            ax = fig.add_subplot(1, num_visualized, ind)
            ax.imshow(sample_image.transpose(1,2,0))
            with nn.auto_forward():
                predicted_id = np.argmax(F.softmax(pred_valid)[n].d)
            result = "true label_id: {} - predicted as {}".format(str(sample_label[0]), str(predicted_id))
            ax.set_title(result)
            ind += 1
        fig.show()

    return ve, vloss
_, _ = run_validation(pred_valid, loss_valid, top_1_error_valid, input_image_valid, data_iterator_valid, with_visualized=True)
../../_images/model_finetuning_29_1.png

ご覧いただける様に、モデルは画像を適切に分類できません。 それでは、ファインチューニングを始めて、パフォーマンスがどのように向上するか見てみましょう。

ファインチューニングを開始する

学習用の monitor を準備しましょう。

from nnabla.monitor import Monitor, MonitorSeries, MonitorTimeElapsed
monitor = Monitor("tmp.monitor")
monitor_loss = MonitorSeries("Training loss", monitor, interval=200)
monitor_err = MonitorSeries("Training error", monitor, interval=200)
monitor_vloss = MonitorSeries("Test loss", monitor, interval=200)
monitor_verr = MonitorSeries("Test error", monitor, interval=200)
# Training-loop
for i in range(max_iter):
    image, label = data_iterator_train.next()
    input_image_train["image"].d = image
    input_image_train["label"].d = label
    nn.forward_all([loss_train, top_1_error_train], clear_no_need_grad=True)

    monitor_loss.add(i, loss_train.d.copy())
    monitor_err.add(i, top_1_error_train.d.copy())

    solver.zero_grad()
    loss_train.backward(clear_buffer=True)

    # update parameters
    solver.weight_decay(3e-4)
    solver.update()

    if i % 200 == 0:
        ve, vloss = run_validation(pred_valid, loss_valid, top_1_error_valid,
                                   input_image_valid, data_iterator_valid,
                                   with_visualized=False, num_visualized=3)

        monitor_vloss.add(i, vloss)
        monitor_verr.add(i, ve)
2019-07-05 14:26:26,885 [nnabla][INFO]: iter=199 {Training loss}=1.5021580457687378
2019-07-05 14:26:26,887 [nnabla][INFO]: iter=199 {Training error}=0.3345312476158142
2019-07-05 14:26:28,756 [nnabla][INFO]: iter=200 {Test loss}=2.975713219355654
2019-07-05 14:26:28,756 [nnabla][INFO]: iter=200 {Test error}=0.5384837962962963
2019-07-05 14:26:50,249 [nnabla][INFO]: iter=399 {Training loss}=0.22022955119609833
2019-07-05 14:26:50,250 [nnabla][INFO]: iter=399 {Training error}=0.053437501192092896
2019-07-05 14:26:52,256 [nnabla][INFO]: iter=400 {Test loss}=0.12045302835327608
2019-07-05 14:26:52,257 [nnabla][INFO]: iter=400 {Test error}=0.029513888888888888
2019-07-05 14:27:14,151 [nnabla][INFO]: iter=599 {Training loss}=0.0659928247332573
2019-07-05 14:27:14,152 [nnabla][INFO]: iter=599 {Training error}=0.012500000186264515
2019-07-05 14:27:16,175 [nnabla][INFO]: iter=600 {Test loss}=0.08744175952893717
2019-07-05 14:27:16,175 [nnabla][INFO]: iter=600 {Test error}=0.02199074074074074
2019-07-05 14:27:38,097 [nnabla][INFO]: iter=799 {Training loss}=0.03324155509471893
2019-07-05 14:27:38,098 [nnabla][INFO]: iter=799 {Training error}=0.0054687499068677425
2019-07-05 14:27:40,120 [nnabla][INFO]: iter=800 {Test loss}=0.07678695395588875
2019-07-05 14:27:40,121 [nnabla][INFO]: iter=800 {Test error}=0.02025462962962963
2019-07-05 14:28:02,041 [nnabla][INFO]: iter=999 {Training loss}=0.019672293215990067
2019-07-05 14:28:02,042 [nnabla][INFO]: iter=999 {Training error}=0.0017187499906867743
2019-07-05 14:28:04,064 [nnabla][INFO]: iter=1000 {Test loss}=0.06333287184437116
2019-07-05 14:28:04,065 [nnabla][INFO]: iter=1000 {Test error}=0.017361111111111112
2019-07-05 14:28:25,984 [nnabla][INFO]: iter=1199 {Training loss}=0.009992362931370735
2019-07-05 14:28:25,985 [nnabla][INFO]: iter=1199 {Training error}=0.0003124999930150807
2019-07-05 14:28:28,008 [nnabla][INFO]: iter=1200 {Test loss}=0.06950318495984431
2019-07-05 14:28:28,008 [nnabla][INFO]: iter=1200 {Test error}=0.015625
2019-07-05 14:28:49,954 [nnabla][INFO]: iter=1399 {Training loss}=0.007941835559904575
2019-07-05 14:28:49,955 [nnabla][INFO]: iter=1399 {Training error}=0.0003124999930150807
2019-07-05 14:28:51,978 [nnabla][INFO]: iter=1400 {Test loss}=0.06711215277512868
2019-07-05 14:28:51,979 [nnabla][INFO]: iter=1400 {Test error}=0.016203703703703703
2019-07-05 14:29:13,898 [nnabla][INFO]: iter=1599 {Training loss}=0.008225565776228905
2019-07-05 14:29:13,899 [nnabla][INFO]: iter=1599 {Training error}=0.0007812500116415322
2019-07-05 14:29:15,923 [nnabla][INFO]: iter=1600 {Test loss}=0.06447940292181792
2019-07-05 14:29:15,923 [nnabla][INFO]: iter=1600 {Test error}=0.016203703703703703
2019-07-05 14:29:37,850 [nnabla][INFO]: iter=1799 {Training loss}=0.005678100511431694
2019-07-05 14:29:37,850 [nnabla][INFO]: iter=1799 {Training error}=0.0
2019-07-05 14:29:39,873 [nnabla][INFO]: iter=1800 {Test loss}=0.06282947226255028
2019-07-05 14:29:39,873 [nnabla][INFO]: iter=1800 {Test error}=0.01678240740740741
2019-07-05 14:30:01,795 [nnabla][INFO]: iter=1999 {Training loss}=0.006834140978753567
2019-07-05 14:30:01,796 [nnabla][INFO]: iter=1999 {Training error}=0.00046874998952262104
2019-07-05 14:30:03,818 [nnabla][INFO]: iter=2000 {Test loss}=0.05948294078310331
2019-07-05 14:30:03,818 [nnabla][INFO]: iter=2000 {Test error}=0.014467592592592593

ご覧いただける様に、ファインチューニングが進むにつれて、 loss とエラー率は減少しています。ファインチューニング後の分類結果を見てみましょう。

_, _ = run_validation(pred_valid, loss_valid, top_1_error_valid, input_image_valid, data_iterator_valid, with_visualized=True)
../../_images/model_finetuning_36_0.png

これで、モデルが画像を適切に分類できるようになりました。

更にファインチューニング

finetuning.py という名前の便利なスクリプトがあります。これを使用することで、 独自のオリジナルのデータセットでも 、様々なモデルでファインチューニングを試すことができます。

これを行うには、独自のデータセットを準備し、いくつかの前処理を行う必要があります。 以下にその方法を説明します。

独自のデータセットを準備する

画像の分類に使用できる画像がたくさんあるとします。データを特定の方法で整理する必要があります。ここでは、他のデータセットである Stanford Dogs Dataset について説明します。 まず、公式ページにアクセスして、 images.tar をダウンロードします ( リンクは こちら です ) 。次に、アーカイブを展開すると、 Images という名前のディレクトリが表示されます。そのディレクトリ内には多くのサブディレクトリがあり、各サブディレクトリには 1 つのカテゴリに属する画像が格納されています。例えば、ディレクトリ n02099712-Labrador_retriever には、ラブラドールレトリバーの画像のみが含まれています。従って、独自のデータセットを使用する場合は、次のように画像とディレクトリを同じ様に整理する必要があります。

parent_directory
├── subdirectory_for_category_A
│   ├── image_0.jpg
│   ├── image_1.jpg
│   ├── image_2.jpg
│   ├── ...
│
├── subdirectory_for_category_B
│   ├── image_0.jpg
│   ├── ...
│
├── subdirectory_for_category_C
│   ├── image_0.jpg
│   ├── ...
│
├── subdirectory_for_category_D
│   ├── image_0.jpg
│   ├── ...
│
 ...

各カテゴリの画像の数は異なる場合があり、完全に同じである必要はありません。 独自データセットを配置したら、準備完了です!

NNabla CLI を使用して画像分類データセットを作成する

データセットを準備して整理したら、次に行う必要があるのは、 finetuning.py で使用される .csv ファイルを作成するだけです。そのために、 NNabla の Python コマンドラインインターフェース を使用できます。 次のように入力してください。

nnabla_cli create_image_classification_dataset -i <path to parent directory> -o <output directory which contains "preprocessed" images> -c <number of channels> -w <width> -g <height> -m <padding or trimming> -s <whether apply shuffle or not> -f1 <name of the output csv file for training data> -f2 <name of the output csv file for test data> -r2 <ratio(%) of test data to training data>

Stanford Dogs Dataset でこれを行うと、以下のようになります。

nnabla_cli create_image_classification_dataset -i Images -o arranged_images -c 3 -w 128 -g 128 -m padding -s true -f1 stanford_dog_train.csv -f2 stanford_dog_test.csv -r2 20

出力 .csv ファイルは、 -o オプションで指定したものと同じディレクトリに保存されることに注意してください。 詳細については、 ドキュメント を参照してください。

上記のコマンドを実行した後、データセットのファインチューニングを開始できます。

ファインチューニングを実行

必要なのは、 1 行を入力することだけです。

python finetuning.py --model <model name> --train-csv <.csv file containing training data>  --test-csv <.csv file containing test data>

データセットのファインチューニングを実行します!

run finetuning.py --model ResNet34 --epoch 10 --train-csv ~/nnabla_data/stanford_dog_arranged/stanford_dog_train.csv --test-csv ~/nnabla_data/stanford_dog_arranged/stanford_dog_test.csv --shuffle True

推論のためにファインチューニングの結果を使用する方法の例

ファインチューニングが終了したら、推論に使用しましょう! 上記のスクリプトは、指定した特定のイテレーターごとにパラメータを保存しました。そこで、学習したものと同じモデルを呼び出して、今度はファインチューニングされたパラメータを次のように使用します。

from nnabla.models.imagenet import ResNet34
import nnabla as nn

param_path = "params_XXX.h5"  # specify the path to the saved parameter (.h5)

model = ResNet34()
batch_size = 1  # just for inference
input_shape = (batch_size, ) + model.input_shape

次に、入力 Variable と推論用のネットワークを定義します。スクリプトのファインチューニングと全く同じ方法で、ネットワークを構築する必要があることに注意してください ( レイヤ構成、パラメータ名など... ) 。

x = nn.Variable(input_shape)  # input Variable
pooled = model(x, use_up_to="pool", training=False)
with nn.parameter_scope("finetuning"):
    with nn.parameter_scope("last_fc"):
        pred = PF.affine(pooled, 120)

上記でファインチューニングしたパラメータを読み込みます。 nn.load_parameters() を使用してパラメータを読み込むことができます。これを呼び出すと、 params.h5 に格納されているパラメータがグローバルスコープに格納されます。 nn.get_parameters() を使用して、 nn.load_parameters() の前後でパラメータが異なることを確認できます。

nn.load_parameters(param_path)  # load the finetuned parameters.
pred.forward()