Amazon Web Services ブログ

Apache MXNet で事前にトレーニングを受けたモデルを使用する

このブログ記事では、Apache MXNet で事前トレーニングを受けたモデルの使用方法について解説します。複数モデルを試してみようと思われた理由は? 最高の精度をもったモデルを選ばないのはなぜでしょう? この記事の後半で説明するように、同じデータセット上でこれらのモデルがトレーニングを受け最高の精度を得るために最適化されたとしても、個々のイメージではその動作にわずかながら違いが生じます。また、予測速度も変動する可能性があります。これは多くのアプリケーションにとって重大な要素です。事前にトレーニングされたモデルをいくつか試すことで、自分のビジネス課題を解決するのに最適なモデルを見つけることができます。

まず、Apache の MXNet モデルズーから、3 つのイメージ分類モデルをダウンロードしてみましょう。

各モデルについて次に示す 2 種類のファイルをダウンロードする必要があります。

  • ニューラルネットワークの JSON 定義を含むシンボルファイル: レイヤー、接続、アクティベーション機能など。
  • 全接続加重、バイアスの値を保存する加重ファイルで別名パラメーター。トレーニングフェーズ中にネットワークにより習得。
# MacOS users can easily install 'wget' with Homebrew: 'brew install wget'
!wget http://data.dmlc.ml/models/imagenet/vgg/vgg16-symbol.json -O vgg16-symbol.json
!wget http://data.dmlc.ml/models/imagenet/vgg/vgg16-0000.params -O vgg16-0000.params
!wget http://data.dmlc.ml/models/imagenet/inception-bn/Inception-BN-symbol.json -O Inception-BN-symbol.json
!wget http://data.dmlc.ml/models/imagenet/inception-bn/Inception-BN-0126.params -O Inception-BN-0000.params
!wget http://data.dmlc.ml/models/imagenet/resnet/152-layers/resnet-152-symbol.json -O resnet-152-symbol.json
!wget http://data.dmlc.ml/models/imagenet/resnet/152-layers/resnet-152-0000.params -O resnet-152-0000.params
!wget http://data.dmlc.ml/models/imagenet/synset.txt -O synset.txt

VGG-16 シンボルファイルの 1 行目を見てみましょう。入力レイヤー (「data」) の定義、最初のコンボリューションレイヤーの加重とバイアスが記述されています。コンボリューションオペレーション (「conv1_1」) と、そのほかに、ReLU の活性化関数 (‘relu1_1’) が定義されています。

!head -48 vgg16-symbol.json

3 モデルのいずれも ImageNet データセットで事前トレーニングされており、これには 1,000 種類に分類されるオブジェクトと動物の写真が 120 万種以上含まれています。synset.txt ファイルでこれらのカテゴリを閲覧できます。

!head -10 synset.txt
import mxnet as mx
import numpy as np
import cv2, sys, time   # You can easily install OpenCV with 'pip install cv2'
from collections import namedtuple
from IPython.core.display import Image, display

print("MXNet version: %s"  % mx.__version__)

続いてモデルをロードしてみましょう。

まず、ファイルから加重とモデルについての記述をロードします。MXNet ではこれをチェックポイントと呼びます。各トレーニングのエポック後、加重を保存するのは良い習慣です。トレーニングが完了したら、トレーニングログを見て、最高の検証精度を示した最適なエポックの加重を選ぶことができます。これが最後の最後に現れることはあまりないようです。

ロードが完了したら、Symbol オブジェクトと加重、別名モデルパラメーターを取得できます。その後、新しい Module を作成し、入力 Symbol に割り当てます。 モデルを実行するコンテキストを指定することも可能です。デフォルトの動作は CPU 環境を使用するようになっています。その理由には、次の 2 つがあります。

  • まず、ご使用のコンピューターに GPU が搭載されていなくても、これによりノートブックをテストできます。
  • 次に、1 つのイメージを予測するだけなので、特にパフォーマンスに関する要件はありません。大量のイメージを最高のスループットで予測するような本番環境のアプリケーションでは、GPU が最善の選択肢となります。

その後、入力 Symbol を入力データに結び付けます。そのネットワークの入力レイヤーの名前から、「data」と呼ぶ必要があります (JSON ファイルの最初の数行を思い出してください)。

最後に、「data」の形式として 1 x 3 x 224 x 224 を定義します。 224 x 224 は画像の解像度で、モデルをトレーニングする方法となります。3 は赤、緑、青 (この順) でチャネルの数です。1 はバッチのサイズです。ここでは一度に 1 つのイメージを予測します。

def loadModel(modelname, gpu=False):
        sym, arg_params, aux_params = mx.model.load_checkpoint(modelname, 0)
        arg_params['prob_label'] = mx.nd.array([0])
        arg_params['softmax_label'] = mx.nd.array([0])
        if gpu:
            mod = mx.mod.Module(symbol=sym, context=mx.gpu(0))
        else:
            mod = mx.mod.Module(symbol=sym)
        mod.bind(for_training=False, data_shapes=[('data', (1,3,224,224))])
        mod.set_params(arg_params, aux_params)
        return mod

また、synset.txt ファイルに分類された 1,000 種のカテゴリもロードする必要があります。予測時には実際の説明が必要です。

def loadCategories():
        synsetfile = open('synset.txt', 'r')
        synsets = []
        for l in synsetfile:
                synsets.append(l.rstrip())
        return synsets
    
synsets = loadCategories()
print(synsets[:10])

それでは、ファイルからイメージをロードする関数を書いていきましょう。このモデルでは 1 つ 224 x 224 の赤、緑、青を持つ、4 次元の NDArray を想定していることを思い出しましょう。入力イメージからこの NDArray を構築するのに、OpenCV ライブラリーを使用します。

ステップは次のとおりです。

  • イメージを読み込みます。これはイメージの高さ、イメージの幅、3 の形状をした、numpy 配列を戻します。これには BGR (青、緑、赤) の順で 3 つのチャネルがあります。
  • イメージを RGB (赤、緑、青) に変換します。
  • イメージのサイズを 224 x 224 に変換します。
  • イメージの高さ、イメージの幅、3 を 3、イメージの高さ、イメージの幅の形状に変換します。
  • 4 番目の次元を追加し、NDArray を構築します。
def prepareNDArray(filename):
        img = cv2.imread(filename)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img = cv2.resize(img, (224, 224,))
        img = np.swapaxes(img, 0, 2)
        img = np.swapaxes(img, 1, 2)
        img = img[np.newaxis, :]
        array = mx.nd.array(img)
        print(array.shape)
        return array

予測を処理しましょう。パラメーターはイメージ、モデル、カテゴリのリスト、そして戻したい上位カテゴリの数です。

Module オブジェクトではバッチに分けてモデルにデータを挿入しなくてはならないことを思い出してください。これにはデータイテレーターを使うのが一般的な方法です。ここで予測するのは 1 つのイメージだけなので、データイテレーターを使用することはできますが、その必要はないでしょう。代わりに、ここでは名前付きのタプルを作りましょう。Batch と名前を付け、「data」属性が参照されたときに入力 NDArray を戻すことで、偽イテレーターとして動作します。

イメージが転送されると、モデルは 1,000 個の確率をもつ NDArray を出力します。この出力された配列はトレーニングを受けた 1,000 個のカテゴリに相当します。バッチサイズが 1 であることから、NDArray には 1 行しかありません。

これを squeeze() を持つ配列に変換しましょう。 続いて、argsort() を使用することで、降順に並べ替えられた確率のインデックスをもつ 2 つの目の配列を作成します。最後に、上から n 位までのカテゴリとその記述を戻します。

def predict(filename, model, categories, n):
        array = prepareNDArray(filename)
        Batch = namedtuple('Batch', ['data'])
        t1 = time.time()
        model.forward(Batch([array]))
        prob = model.get_outputs()[0].asnumpy()
        t2 = time.time()
        print("Predicted in %.2f microseconds" % (t2-t1))
        prob = np.squeeze(prob)
        sortedprobindex = np.argsort(prob)[::-1]
        
        topn = []
        for i in sortedprobindex[0:n]:
                topn.append((prob[i], categories[i]))
        return topn

これまでの内容をひとまとめにします。3 つのモデルをすべてロードします。

gpu = False
vgg16 = loadModel("vgg16", gpu)
resnet152 = loadModel("resnet-152", gpu)
inceptionv3 = loadModel("Inception-BN", gpu)
categories = loadCategories()

イメージを分類する前に、「.params」ファイルからロードしたばかりの VGG-16 パラメーターをいくつか見てみましょう。まず、全レイヤーの名前を出力します。

params = vgg16.get_params()

layers = []
for layer in params[0].keys():
    layers.append(layer)
    
layers.sort()    
print(layers)

レイヤーごとに、加重とバイアスの 2 つのコンポーネントを見ることができます。加重を数えると 16 レイヤー (13 個のコンボリューションレイヤーと 3 個の完全接続済みレイヤー) あるのがわかります。これでこのモデルが VGG-16 と呼ばれる理由がお分かりいただけたことでしょう。

次に最後の接続済みレイヤー用に加重を出力します。

print(params[0]['fc8_weight'])

このマトリックスの形状に気付きましたか? 1000×4096 です。このレイヤーには 1,000 個のニューロンが含まれます。それぞれのニューロンには、個別のカテゴリに属すイメージの確率が保存されます。また、各ニューロンは前のレイヤー (「fc7」) の全 4,096 個のニューロンに完全に接続されています。

これでおしまいです。これらのモデルをイメージの分類に使用しましょう。

!wget http://jsimon-public.s3.amazonaws.com/violin.jpg -O violin.jpg
image = "violin.jpg"

display(Image(filename=image))

topn = 5
print("*** VGG16")
print(predict(image,vgg16,categories,topn))
print("*** ResNet-152")
print(predict(image,resnet152,categories,topn))
print("*** Inception v3")
print(predict(image,inceptionv3,categories,topn))

今度は GPU を使った環境でもう一度ご覧ください。

gpu = True
vgg16 = loadModel("vgg16", gpu)
resnet152 = loadModel("resnet-152", gpu)
inceptionv3 = loadModel("Inception-BN", gpu)

print("*** VGG16")
print(predict(image,vgg16,categories,topn))
print("*** ResNet-152")
print(predict(image,resnet152,categories,topn))
print("*** Inception v3")
print(predict(image,inceptionv3,categories,topn))

注意 : GPU サポートについてのエラーが表示されたら、ご使用のマシンに GPU が搭載されていないか、GPU サポート (USE_CUDA=1) 付きで構築されなかったバージョンの MXNet を使用していることを意味します。

GPU サポート付きでソースから MXNet を構築する手順についてはこちらをご覧ください。 代わりに、プレビルト版をインストールすることもできます。

パフォーマンスの差は 15 倍から 20 倍と歴然です。 同時に複数のイメージを予測する場合、GPU アーキテクチャの膨大な並列処理が原因で差は広がります。

次は皆さんのイメージで試してみてください。このノートブックと同じフォルダーにそれらをコピーし、上のセルのファイル名を更新して、predict() コールを再実行します。

事前トレーニング型のモデルを使用した処理に挑戦してみましょう!


今回のブログの投稿者について

Julien は EMEA の人工知能およびMachine Learning のエバンジェリストです。彼は、開発者や企業のアイデアを実現させるための支援を中心として活動しています。彼は余暇時間に、JRR Tolkien の作品を何度も読んでいます。