pytorchと転移学習でend-to-end多クラス画像分類を実装

2020年07月14日

一部のユーザーが共有している不適切な画像をFacebookがどのようにチェックしているのか疑問に思ったことはありませんか。また、Facebookのタグ付け機能はどのように行われ、Googleレンズはどのように画像から商品を認識しているのでしょうか。上記は全て、画像分類が様々な場面で活用されていることを示す例です。多クラス画像分類は、画像を利用して画像を分類する、コンピュータビジョンの一般的なタスクです。

私は以前、コンピュータビジョンプロジェクトには常にkerasを使用していましたが、最近、多クラス画像分類を行う機会があったので、pytorchを使ってみることにしました。既に全ての自然言語処理タスクでkerasからpytorchに移行していたので、コンピュータビジョンでもpytorchを使ってみようと思ったのです。pytorchは強力で、よりpython的な構造も気に入っています。この記事では、pytorchを利用した多クラス画像分類用のエンドツーエンドのパイプラインを作成します。モデルのトレーニングや、潜在顧客に提示できるような形式でモデルの結果を表示する方法、モデルのデプロイに役立つ機能などが含まれます。

では、画像分類の方法を説明する前に、このような問題を扱うための最も一般的な手法である転移学習について見ていきましょう。

 

転移学習とは

転移学習とは、一つのタスクで得た知識を別のタスクに転用することです。モデリングの観点から言うと、一つのデータセットを用いて学習させたモデルを別のデータセットに利用できるように微調整することです。しかし、これはなぜうまく機能するのでしょうか。

まず少し、背景からご説明しましょう。毎年、視覚認識コミュニティが集まり、ImageNet Challengeと呼ばれる特定のコンペティションが行われます。このチャレンジのタスクは、100万件の画像を1,000個のカテゴリに分類することです。このコンペティションは既に、研究者が大規模な畳み込みニューラルネットワークモデルに学習させる上で役立っています。ResNet50やInceptionなどの優れたモデルの構築に寄与しているのです。

では、ニューラルネットワークモデルに学習させるとはどういうことでしょうか?基本的には、100万件の画像を使用してモデルに学習させて、研究者がニューラルネットワーク用の重みを学ぶことを意味します。私たちは、これらの重みを自分のニューラルネットワークモデルにロードし、テストデータセットに関する予測を取得することができます。実際には、それよりさらに進んで、研究者が準備したニューラルネットワークの上に別の層を追加し、独自のデータセットを分類することもできます。

これら複雑なモデルがどのように動作するのか、その正確なところはまだ謎ですが、下位の畳み込み層はエッジや勾配など、画像の低レベル特徴をキャプチャすることがわかっています。一方、より上位の畳み込み層は身体の部分や顔など、より複雑な構成上の特徴をキャプチャします。

出展: https://arxiv.org/pdf/1311.2901.pdf

 

上記は、Imagenetタスクで成功を収めた最初の畳み込みニューラルネットワークの一つであるZFNet(Alexnetの改良版)の例です。下位層がラインやエッジをキャプチャし、後の層がより複雑な特徴を捉えている様子を確認できます。一般的に、完全に連結された最後の層が、個々のタスクを解決するために必要な情報をキャプチャしているとみなされます。例えば、ZFNetの完全に連結された層は、画像を1,000個の物体カテゴリに分類する際に重要となる特徴を示しています。 

そして、ImageNetで事前に学習させた最新の畳み込みニューラルネットワークを別のコンピュータビジョンタスクのために利用し、これら抽出された特徴を用いて新しいモデルに学習させることが可能です。つまり、直感的に考えて、動物を認識する学習を行ったモデルは、犬と猫を分類するために利用できる可能性があります。同様に、1000個の異なるカテゴリについて学習したモデルは実世界の情報を学んでいるので、この情報を用いて私たち独自の分類器を作ることができるのです。 

以上が理論および直感的な説明です。実際にこれを機能させる方法に関しては、いくつかコードを見てみましょう。この記事の完全なコードはGithubをご覧ください。

 

データ探索

多クラス画像分類問題を理解するため、Kaggleの船データセットから始めましょう。このデータセットは、約1,500件の様々な種類の船の画像から構成されています。ブイやクルーズ船、フェリー、貨物船、ゴンドラ、空気注入式ボート、カヤック、紙の船、ヨットなど様々な種類の船の画像が含まれています。私たちの目標は、船の画像を正しいカテゴリに分類するモデルを作ることです。データセットの画像サンプルは次の通りです。

カテゴリは以下となります。

貨物船、空気注入式ボート、ボートのカテゴリにはそれほど多くの画像が含まれていないため、モデルに学習させる際はこれらのカテゴリを削除します。

 

ディレクトリ構造の作成

ディープラーニングモデルに学習させる前に、画像用のディレクトリ構造を作成する必要があります。現在のところ、データディレクトリ構造は次のようになっています。

images
    sailboat
    kayak
    .
    .

画像はtrain, val, testの三つのフォルダに分ける必要があります。trainデータセットの画像でモデルに学習させ、valデータセットで検証し、最後にテスト用データセットでテストを行います。

data
    train
        sailboat
        kayak
        .
        .
    val
        sailboat
        kayak
        .
        .
    test
        sailboat
        kayak
        .
        .

データが別の形式で保存されている場合もあるかもしれませんが、私は、通常のライブラリとは別に、glob.globos.systemの関数を利用すると非常に役立つことを発見しました。データ準備のための完全なコードはこちらです。それでは、データ準備で役立つことが判明した、一般的にはあまり使用されていないライブラリについて簡単に見ていきましょう。

 

glob.globとは

globを使えば、正規表現を使用してディレクトリのファイルやフォルダの名前を取得することができます。例えば、このような感じになります。

from glob import glob
categories = glob(“images/*”)
print(categories)
------------------------------------------------------------------
['images/kayak', 'images/boats', 'images/gondola', 'images/sailboat', 'images/inflatable boat', 'images/paper boat', 'images/buoy', 'images/cruise ship', 'images/freight boat', 'images/ferry boat']

 

os.systemとは

os.systemとは、osライブラリの関数であり、python自体のどんなコマンドライン関数を実行することも可能です。私は一般的にLinuxの関数を実行するために使用しますが、以下に示すようにpythonでRスクリプトを実行するためにも利用できます。例えば、pandasデータフレームから情報を取得後、一つのディレクトリから別のディレクトリにファイルをコピーするためにデータ準備で使用します。また、私はf文字列フォーマットも利用します。

import os
for i,row in fulldf.iterrows():
    # Boat category
    cat = row['category']
    # section is train,val or test
    section = row['type']
    # input filepath to copy
    ipath = row['filepath']
    # output filepath to paste
    opath = ipath.replace(f"images/",f"data/{section}/")
    # running the cp command
    os.system(f"cp '{ipath}' '{opath}'")

所定のフォルダ構造にデータがそろったところで、より面白いパートに移りましょう。

 

データ前処理

① ImageNetの前処理

ImageNetデータセットで学習させたネットワークを用いて画像を処理するためには、ImageNetネットワークと同様の方法で画像を前処理する必要があります。そのためには、画像を224×224ピクセルにリサイズし、ImageNet基準に基づいて標準化しなければなりません。これは、torchvisiontransformsライブラリを用いて行えます。ここでは、224×224ピクセルのCenterCrop画像をImageNet基準に基づいて標準化しています。以下に記載されている操作が順次実行されます。pytorchが提供している全てのtransformsのリストは、こちらのページをご覧ください。

transforms.Compose([
        transforms.CenterCrop(size=224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225])
    ])

 

② データ拡張

データ拡張を行うため、さらに前処理を追加することができます。ニューラルネットワークは、データが多いほどうまく機能します。データ拡張は、トレーニング時のデータ量を増やすための戦略です。

例えば、船の画像を水平方向に反転させても、船の画像に変わりありません。また、ランダムに画像をトリミングしたり、色ジッターを加えたりすることもできます。ImageNetの前処理と拡張の両方に適用できる、私が使用した画像変換ディクショナリを以下にご紹介します。こちらディクショナリには、こちら記事のトレーニング用、検証用、テスト用データを作成するために使用した様々な変換が含まれています。お察しの通り、テストデータと検証データには水平方向の反転などのデータ拡張変換を施していません。拡張画像に対する予測を取得したくないからです。

# Image transformations
image_transforms = {
    # Train uses data augmentation
    'train':
    transforms.Compose([
        transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)),
        transforms.RandomRotation(degrees=15),
        transforms.ColorJitter(),
        transforms.RandomHorizontalFlip(),
        transforms.CenterCrop(size=224), # Image net standards
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225]) # Imagenet standards
    ]),
    # Validation does not use augmentation
    'valid':
    transforms.Compose([
        transforms.Resize(size=256),
        transforms.CenterCrop(size=224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),

        # Test does not use augmentation
    'test':
    transforms.Compose([
        transforms.Resize(size=256),
        transforms.CenterCrop(size=224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

以下は、トレーニング用データセットの画像に適用された変換の一例です。一つの画像から多種多様な画像を取得できるだけでなく、ネットワークが物体の向きに左右されないようにするためにも役立ちます。

ex_img = Image.open('/home/rahul/projects/compvisblog/data/train/cruise ship/cruise-ship-oasis-of-the-seas-boat-water-482183.jpg')
t = image_transforms['train']
plt.figure(figsize=(24, 24))
for i in range(16):
    ax = plt.subplot(4, 4, i + 1)
    _ = imshow_tensor(t(ex_img), ax=ax)
plt.tight_layout()

 

データローダ

次のステップは、トレーニング用、検証用、テスト用データセットの場所をpytorchに提供することです。このためには、pytorchデータセットとDataLoaderクラスを利用します。所定のディレクトリ構造にデータが保存されていれば、コードのこの部分はほとんど変わりません。

# Datasets from folders
traindir = "data/train"
validdir = "data/val"
testdir = "data/test"
data = {
    'train':
    datasets.ImageFolder(root=traindir, transform=image_transforms['train']),
    'valid':
    datasets.ImageFolder(root=validdir, transform=image_transforms['valid']),
    'test':
    datasets.ImageFolder(root=testdir, transform=image_transforms['test'])
}
# Dataloader iterators, make sure to shuffle
dataloaders = {
    'train': DataLoader(data['train'], batch_size=batch_size, shuffle=True,num_workers=10),
    'val': DataLoader(data['valid'], batch_size=batch_size, shuffle=True,num_workers=10),
    'test': DataLoader(data['test'], batch_size=batch_size, shuffle=True,num_workers=10)
}

これらのデータローダは、データセットの反復処理に役立ちます。ここでは、モデルのトレーニングに以下のデータローダを使用します。データ変数は(batch_size, color_channels, height, width)の形状でデータを記憶する一方、ターゲットは(batch_size)の形状を取り、ラベル情報を含みます。

train_loader = dataloaders['train']
for ii, (data, target) in enumerate(train_loader):

 

モデリング

① 学習済みモデルを利用してモデルを作成する

現在のところtorchvisionライブラリでは、次のような学習済みモデルが利用できます。AlexNet、VGG、ResNet、SqueezeNet、DenseNet、Inception v3、GoogLeNet、ShuffleNet、MobileNet、ResNeXt、Wide ResNet、MNASNet。

ここではResNet50を使用しますが、お好みによって他のどのモデルを利用しても構いません。

from torchvision import models
model = models.resnet50(pretrained=True)

ResNet50モデルの重みを変更したくないので、モデルの重みをフリーズすることから始めます。

# Freeze model weights
for param in model.parameters():
    param.requires_grad = False

次に行うのは、モデルの線形分類層を私たち独自の分類器に置き換えることです。このためには、最初にモデル構造を確認してから最後の線形層を決定する方がよいことに気づきました。モデルオブジェクトをプリントするだけでこれを行うことができます。

print(model)
------------------------------------------------------------------
ResNet(
  (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
  .
  .
  .
  .
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
    )
  )
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Linear(in_features=2048, out_features=1000, bias=True)
)

ここで、畳み込み層から入力を受け取る最後の線形層はfc層であることがわかります。

次は、fc層を私たち独自のニューラルネットワークに置き換えるだけです。このニューラルネットワークが、前の層からfc層への入力を受け取り、出力にLogSoftmaxを使用し、(batch_size x n_classe)となります。

n_inputs = model.fc.in_features
model.fc = nn.Sequential(
                      nn.Linear(n_inputs, 256),
                      nn.ReLU(),
                      nn.Dropout(0.4),
                      nn.Linear(256, n_classes),
                      nn.LogSoftmax(dim=1))

さらに、現在追加された新しい層は、デフォルトで完全にトレーニング可能です。

 

② モデルをGPUにロードする

pytorchのDataParallelを利用して、単一のGPUまたは複数のGPU(複数保有している場合)にモデルをロードすることができます。以下のコードは、モデルをGPUにロードする際、GPUやGPUの数の識別に利用できます。ここでは、Titan RTX GPUを二つ用いてモデルのトレーニングを行っています。

# Whether to train on a gpu
train_on_gpu = cuda.is_available()
print(f'Train on gpu: {train_on_gpu}')
# Number of gpus
if train_on_gpu:
    gpu_count = cuda.device_count()
    print(f'{gpu_count} gpus detected.')
    if gpu_count > 1:
        multi_gpu = True
    else:
        multi_gpu = False
if train_on_gpu:
    model = model.to('cuda')
if multi_gpu:
    model = nn.DataParallel(model)

 

③ オプティマイザを決定する

モデルに学習させる際、注意すべき最も重要なポイントは、使用する損失関数とオプティマイザの選択です。ここでは、多クラス画像分類問題を扱っているので、多クラス交差エントロピーを使いたいと思います。また、最も一般的に使用されているオプティマイザであるAdamオプティマイザを使用します。さらに、モデルの出力にLogSoftmaxを使っているので、NLL(負の対数尤度)を利用します。

from torch import optim
criteration = nn.NLLLoss()
optimizer = optim.Adam(model.parameters())

 

④ モデルを学習させる

以下に、モデルのトレーニングに使用する完全なコードを記載します。一見するとかなり大変に見えるかもしれませんが、基本的に私たちが行っているのは次のようなことです。

  • エポックの実行を開始します。各エポックごとに…。
  • model.train()を使用して、モデルをトレーニングモードに設定します。
  • トレーニングデータローダを用いてデータを処理します。
  • data, target = data.cuda(), target.cuda()コマンドを使用してGPUにデータをロードします。
  • optimizer.zero_grad()を使用してオプティマイザの既存の勾配を0に設定します。
  • output = model(data)を使用してバッチに順方向パスを実行します。
  • loss = criterion(output, target)を使用して損失を計算します。
  • loss.backward()を使用してネットワークで誤差逆伝播を行います。
  • optimizer.step()を使用してネットワーク全体の重みを変更します。
  • トレーニングループの他の全てのステップは、単に履歴を維持し、精度を計算するためのものです。
  • model.eval()を使用してモデルを評価モードにします。
  • valid_loaderを使用して検証データに対する予測を取得しvalid_lossvalid_accを計算します。
  • print_everyを使用して全てのエポックの検証損失と検証精度をプリントします。
  • 検証損失に基づいて最も優れたモデルを保存します。
  • 早期停止: 交差検証損失がmax_epochs_stopで改善されない場合、学習を停止して、最小の検証損失を持つ最善のモデルをロードします。 

上記のコードを実行した場合の出力は以下のとおりです。最後のいくつかのエポックだけを表示しています。検証精度に関しては、最初のエポックは55%程度からスタートし、最終的には90%ほどに改善しています。

損失と精度をプロットした学習曲線は次のようになります。

モデル評価

モデルを使用する際、様々な方法で結果を評価する必要があります。テストの精度や混同行列もその指標の例です。これらの指標を作成するための全てのコードはコードノートブックに記載されています。

 

1. テスト結果

テストモデルの全般的な精度は次のようになります。

Overall Accuracy: 88.65 %

テストデータセットに対する結果の混同行列は次のようになります。

カテゴリごとの精度も確認できます。新たな観点から結果を検証するため、学習回数も追加しました。

 

2. 単一の画像に対する予測結果を視覚化

モデルをデプロイするためには、単一の画像に対する予測結果を取得できると役立ちます。コードはノートブックをご覧ください。

 

3. カテゴリごとの予測結果を視覚化

デバッグやプレゼンテーションのためにカテゴリごとの予測結果も視覚化できます。

4. テスト時間を拡張した場合のテスト結果

テスト時間を拡張して精度を高めることもできます。ここでは、新しいテストデータローダと画像変換を利用しています。

# Image transformations
tta_random_image_transforms = transforms.Compose([
        transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)),
        transforms.RandomRotation(degrees=15),
        transforms.ColorJitter(),
        transforms.RandomHorizontalFlip(),
        transforms.CenterCrop(size=224), # Image net standards
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225]) # Imagenet standards
    ])
# Datasets from folders
ttadata = {
    'test':
    datasets.ImageFolder(root=testdir, transform=tta_random_image_transforms)
}
# Dataloader iterators
ttadataloader = {
    'test': DataLoader(ttadata['test'], batch_size=512, shuffle=False,num_workers=10)
}

上記の関数では、各画像にtta_random_image_transformsを5回適用してから予測を取得しています。最終的な予測結果は、五つの予測全ての平均です。全てのテストデータセットでテスト時間を拡張すると、精度が1%ほど上昇したのがわかりました。

TTA Accuracy: 89.71%

テスト時間を拡張した場合のカテゴリごとの結果を通常の結果と比較すると次のようになります。

このような小規模なデータセットではテスト時間を拡張してもさほど変化はありませんが、大規模なデータセットでこれを実行すると、非常に役立ちます。

この記事では、pytorchを利用して多クラス画像分類プロジェクトを行うためのエンドツーエンドパイプラインについてご説明しました。まず、転移学習を利用してモデルに学習させるためのコードを作成し、結果を視覚化しました。そして、テスト時間を拡張し、必要な時にStreamlitなどのツールを使用してモデルをデプロイするため、単一画像に対する予測結果の取得を行いました。この記事の完全なコードはGithubをご覧ください。

 

LionbridgeのAI学習データサービスについて

当社はAI向け教師データの作成やアノテーションのサービスを提供し、研究開発を支援しています。世界の各タイムゾーンを渡る、100万人のコントリビューターが登録されており、大規模なAIプロジェクトも素早く仕上げることができます。無料トライアルやご相談は、こちらからお気軽にお問い合わせください。

※ 本記事は2020年6月1日、弊社英語ブログに掲載された寄稿記事に基づいたものです。

AI向け教師データの作成やアノテーションサービスを提供しております。

メディア掲載結果

    AI・機械学習の最新情報をお届けします!

    Lionbridge AIのブログで紹介している事例記事やトレンドニュースといったビジネスに役立つ情報はもちろん、オープンデータセット集なども合わせてメール配信しております。