Amazon Web Services ブログ

複数の GPU に対する深層学習トレーニングをスケーリングするためのハイパーパラメーターの調整の重要性

複数の GPU による並列プロセスは、深層モデルのトレーニングのスケーリングを行う上で重要なステップです。トレーニングを繰り返すたびに、一般的に、ミニバッチと呼ばれるデータセットの小さなサブセットがプロセスされます。単一の GPU が使用可能の場合、それぞれのトレーニングの繰り返しにおけるミニバッチの処理は、この GPU により取り扱われます。複数の GPU でトレーニングするとき、ミニバッチはプロセスの負荷を均等に分散するために使用可能な GPU 全体に分割されます。各 GPU を完全に使用するためには、各追加 GPU でミニバッチのサイズを直線的に増大させる必要があります。ミニバッチのサイズは、トレーニング速度に影響を与えるだけではなく、トレーニングされるモデルの質にも影響を与えます。ミニバッチのサイズが大きくなると、他のハイパーパラメーターを微調整して、類似するモデルの質でより高速なトレーニングができるようにすることが重要です。

Gluon によるマルチ GPU と分散トレーニング

最新の真相学習モデルにより必要な広範なデータの量により、複数の GPU と分散マシンにスケーリングすることで、調査と本番稼働のための大幅な時間節約となる可能性があります。Amazon SageMakerAmazon Elastic Compute Cloud (Amazon EC2) のようなサービスでは、数百 GPU の分散トレーニングをセットアップすることは、痛みのないだけでなく、正確な使用量に対して支払うだけで非常に経済的で、高価で十分に活用されていないハードウェアフリートを維持する必要はありません。

Apache MXNet は、柔軟でより効率的な深層学習プラットフォームです。 これは、複数のホストにわたるマルチ GPU と分散トレーニングに特に適しています。Apache MXNet の Gluon ライブラリは、深層学習のための明確で正確、さらにシンプルな API を提供します。gluon による複数の GPU のトレーニングおよび複数のマシンによる分散トレーニングに関するチュートリアルでは、マルチ GPU と分散トレーニングの容易なセットアップをデモンストレーションします。

トレーニングのハイパーパラメーター

トレーニングのハイパーパラメーターは、勾配降下法によっては学習できないが、最終的なモデル品質に影響を与えるすべてのパラメーターを構成します。これらのパラメーターは、学習速度およびモメンタムなどの最適化パラメーター、ランダムなカラーシフト量などの増強パラメーター、および他の学習以外のパラメーターを含みます。

MXNet Gluon API は、すべての GPU でモデルパラメーターをシームレスに作成し、複数の GPU 間でデータを分割する gluon.utils.split_and_load() 関数を提供することにより、複数の GPU を活用するためのトレーニングコードの変更を容易にします。複数の GPU を使用するためにトレーニングコードをアップグレードするのは簡単ですが、プロセスでは、モデルの質を犠牲にすることなく、より大きなコンピューティングリソースを活用するために、ハイパーパラメータの調整と最適化のヒントを適用する必要があります。

ミニバッチサイズを増大させた結果

1 つの GPU のトレーニングから複数の GPU のトレーニングに移行する場合、GPU の数を乗じて GPU あたりのミニバッチサイズを一定に保つことで、ミニバッチサイズを増やすことが発見を助けます。たとえば、128 のミニバッチサイズで 1 つの GPU を完全に使用する場合、4 つの GPU を使用するとき、ミニバッチサイズ 512 に増やす必要があります。 より大きなミニバッチサイズではデータのスループットが増大しますが、トレーニングはクロック時間でより高速に収束することはありません。より大きなミニバッチサイズでは、バッチ間のノイズの量が減少し、確率的な勾配降下法により最適の方向に近づくことができます。しかし、学習速度を同じに維持することにより、平均のステップサイズは変化がなく、それにより、収束するために必要なステップの数をわずかに節約するにすぎません。

ミニバッチサイズを増大することによる大きな問題は、モデルの品質への悪影響です。ミニバッチサイズを大きくすると、損失関数の勾配のばらつきが小さくなります。理論的に、凸最適化シナリオでは、より低い勾配分散がより良い最適化をもたらします。しかし、実際には、ミニバッチサイズを大きくすると、一般化が不十分なモデルになります (Keskar らを参照)。このトピックでは、深層学習の分野における研究の活発な分野であり、いくつかの理論が探求されています。研究者はその行動を説明するための単一の理論に落ち着いていませんが、モデルの一般化においてより大きなミニバッチサイズに及ぼす悪影響は、資料で十分にデモンストレーションされています。

主導的な理論の 1 つでは、損失関数の非凸面は、多くの極小点および鞍点を含みます。より小さなミニバッチでは、ミニバッチごとの損失の勾配はノイズが多く、ローカルの最小または鞍点の範囲外の最適化プロセスがもたらされる場合があります。しかし、大きなミニバッチは、確率がより低い勾配をもたらし、最適化は極小または鞍点に詰まる可能性があります。

ハイパーパラメーターの調整

ミニバッチサイズを大きくしてコンバージェンス速度を上げるには、SGD オプティマイザーの学習速度を上げる必要があります。しかし、Keskar らによりデモンストレーションされているように、大きな学習速度によるネットワークの最適化は困難です。最適化のヒントの中には、この困難に対処するのに有効であることが証明されているものもあります (Goyal らを参照)。主にこれらのテクニックには、いくつかのエポックの間、小さな学習速度を使用することにより、ネットワークのプライミング (ウォーミングアップとも呼ばれる) を行います。

興味深いことに、ミニバッチサイズの増加に伴って学習速度を上げる技術は、ミニバッチをモデルの質に与える影響を軽減することが示されています。Goyal らは、ResNet-50 モデルで、学習速度も同じ相対量でスケーリングされる場合、検証エラーを減少させることなく、ミニバッチサイズを 8192 にスケーリングできることを示しています。このミニバッチサイズは、ResNet-50 モデルの ImageNet データセット上でのトレーニングをたった 1 時間で終えるようにします。しかし、ミニバッチのサイズが 8192 を超えると、検証エラーが非常に素早く増えます。

ネットワークアーキテクチャとトレーニングハイパーパラメーターに応じて、ミニバッチサイズと学習速度に加えて他のハイパーパラメーターを調整する必要があり、トレーニングを複数の GPU と分散設定に正常にスケールするために、「プライミング」以外のその他の最適化ヒントが必要になる場合があります。

大きなミニバッチの例

この例では、イメージ分類タスクに対して、ResNet-18 モデルを CIFAR-10 データセットを使用してトレーニングすることによって、ほかのハイパーパラメーターを変更せずに、ミニバッチサイズを増大させるネガティブな影響をデモンストレーションすることができます。16 GPU のマシンを使っている場合でも、この概念はミニバッチサイズが大きな係数で増大した場合でも適用されます。

システム設定

この例で使用されているシステムは、Amazon EC2 p2.16xlarge インスタンスです。私が使用しているのは、MXNet パッケージ mxnet_cu90mkl バージョン 1.1.0 であり、 pip によってインストールしました。この記事を書いていた時点で、MXNet の最新バージョンでした。この例で必要とする MXNet の最低バージョンは 1.0.0 です。 MXNet でのインストール手順については、このページの を参照してください

私は、 深層学習 AMI (Ubuntu) バージョン 7.0 (ami-139a476c) を EC2 インスタンスのための Machine Image として使用しています。この Amazon Machine Image (AMI) は、この記事を書いていた時点で AWS Marketplace で利用可能な最新の 深層学習 AMI です。DLAMI を始めるためのリンクは、こちらです。あるいは、MXNet が事前にインストールされている、Amazon SageMaker サービスを使用することも可能です。Amazon SageMaker ノートブックを使用している場合は、この記事を書いている時点で、ノートブックのバージョン 0.12.1 が開始されているため、MXNet パッケージをアップグレードすることが必要になる可能性があります。

CIFAR-10 データセット

この例で取り上げられている機械学習の問題は、イメージ分類タスクです。CIFAR-10 データセットは、ピクセル解像度 32×32 の 60,000 色のイメージから構成され、50,000 トレーニングセットと 10,000 テストセットに分割されます。CIFAR-10 データセットについての詳細は、データセットとデータを収集するときに従う方法論を説明した 小さなイメージの特徴の複数レイヤーを学習する [3] の第 3 章を参照してください。

ピクセル値を正規化し、トレーニングのためのデータ増強のために多数の変換を行います。最初に値を [0、1] の範囲に正規化し、トレーニングデータセットのグローバル平均を差し引き、データセットの標準偏差でスケーリングすることでピクセル値を正規化します。ランダムなクロップとミラー増強は、トレーニングデータセットにのみ適用されます。

from __future__ import print_function
from time import time
import mxnet as mx
from mxnet import autograd, gluon, nd
import numpy as np

print("MXNet Version:", mx.__version__)

def pad_3d(data, pad_width):
    data = data.reshape((1, 1) + data.shape)
    data = nd.pad(data, pad_width=(0, 0, 0, 0) + pad_width,
                  mode='constant', constant_value=0)
    data = data.reshape(data.shape[2:])
    return data


def transform(data, label, rand_aug):
    data = data.astype(np.float32) / 255
    if rand_aug:
        data = pad_3d(data, (4, 4, 4, 4, 0, 0))
    auglist = mx.image.CreateAugmenter(
        data_shape=(3, 32, 32),
        rand_mirror=rand_aug,
        rand_crop=rand_aug,
        mean=np.array([0.4914, 0.4822, 0.4465]),
        std=np.array([0.2023, 0.1994, 0.2010]))
    for aug in auglist:
        data = aug(data)
    return nd.transpose(data, (2, 0, 1)), nd.array([label]).astype(np.float32)

MXNet Version: 1.1.0

この data_loader() 関数は、 train パラメーターに応じて、トレーニングをロードし、データセットをテストします。ランダムな増強は、テストデータセットに対しては無効に設定されているのでご注意ください。

def data_loader(train, batch_size, num_workers):
    dataset = gluon.data.vision.CIFAR10(
        train=train,
        transform=lambda x, y: transform(x, y, rand_aug=train))
    return gluon.data.DataLoader(
        dataset, batch_size, shuffle=train, num_workers=num_workers)

Gluon モデル zoo を使用して、CIFAR-10 データセットで 10 個のクラスに分類するための ResNet-18 ネットワークを作成します。トレーニングは、softmax 出力でのクロスエントロピー損失を使用して行われます。ネットワークは、計算グラフのパフォーマンスを最適化するためにハイブリッド化されていることにご注意ください。詳細については、gluon ネットワークのハイブリッド化に関するチュートリアルを参照してください。

net = mx.gluon.model_zoo.vision.resnet18_v1(pretrained=False, classes=10)
net.hybridize()
softmax_ce = gluon.loss.SoftmaxCrossEntropyLoss()

精度計算のため、予測されるラベルとターゲットラベル間の精度計算が CPU で実行されるため、ターゲットラベルを GPU にロードする必要はありません。しかし、予測を計算するために、すべての使用可能な GPU を使用します。詳細については、gluon での複数の GPU の使用に関するチュートリアルを参照してください。

def evaluate_accuracy(data_iterator, net):
    acc = mx.metric.Accuracy()
    for data, label in data_iterator:
        data = gluon.utils.split_and_load(data, ctx)
        label = gluon.utils.split_and_load(label, [mx.cpu() for _ in ctx])
        acc.update(
            preds=[nd.argmax(net(d), axis=1, keepdims=True) for d in data],
            labels=label)
    return acc.get()[1]

トレーニングループはトレーニングデータを ctx リスト内の GPU に分割し、ネットワーク上で順方向動作と逆方向オペレーションを実行し、1 エポックあたりの平均損失を計算します。各エポックの先頭で、学習速度は train() 関数に lr_schedとして渡された学習速度スケジュールに基づいて学習速度が調整されます。学習速度のスケジュールは、エポック番号から学習速度までの辞書であり、エポック 0 から始まります。

def train(epochs, batch_size, lr_sched):
    num_workers = 64
    train_data = data_loader(train=True,
                             batch_size=batch_size,
                             num_workers=num_workers)
    test_data = data_loader(train=False,
                            batch_size=80,
                            num_workers=num_workers)

    # Initialize parameters randomly
    net.collect_params().initialize(mx.init.Xavier(magnitude=2.24),
                                    ctx=ctx,
                                    force_reinit=True)
    trainer = gluon.Trainer(
        net.collect_params(),
        'sgd',
        {'learning_rate': lr_sched[0], 'momentum': 0.9, 'wd': 0.0001})

    train_start = time()
    avg_loss = nd.zeros((1,), ctx=ctx[0])
    for e in range(epochs):
        if e in lr_sched:
            trainer.set_learning_rate(lr_sched[e])
        avg_loss *= 0  # Zero average loss of each epoch
        for i, td in enumerate(train_data):
            if i == 0:
                e_start = time()
            data, label = td
            data = gluon.utils.split_and_load(data, ctx)
            label = gluon.utils.split_and_load(label, ctx)
            # Wait for completion of previous iteration to
            # avoid unnecessary memory allocation
            nd.waitall()
            with autograd.record():
                output = [net(x) for x in data]
                loss = [softmax_ce(o, l) for o, l in zip(output, label)]
            for l in loss:
                l.backward()
            trainer.step(batch_size)
            # Calculate average loss
            for l in loss:
                avg_loss += l.mean().as_in_context(avg_loss.context)

        avg_loss /= (i * len(ctx))
        epoch_time = time() - e_start

        if e < 6 or (e + 1) % 5 == 0 or (e + 1) == epochs:
            print("\tEPOCH {:2}: train loss {:4.2} | batch {:4} | "
                  "lr {:5.3f} | Time per epoch {:5.2f} seconds".format(
                      e, avg_loss.asscalar(), batch_size,
                      trainer.learning_rate, epoch_time))
    train_accuracy = evaluate_accuracy(train_data, net)
    test_accuracy = evaluate_accuracy(test_data, net)
    print("Training time {:6.2f} seconds | train accuracy {:6.4} | "
          "test accuracy {:6.4}".format(
              time() - train_start, train_accuracy, test_accuracy))

単一の GPU (ミニバッチサイズ 128)

最初に、128 のミニバッチサイズ、0.1 の学習、0.9 のモメンタム、および 0.0001 の重み付け減衰を使用して、単一の GPU でネットワークをトレーニングします。

ctx = [mx.gpu(0)]
train(epochs=45, batch_size=128, lr_sched={0: 0.1, 35: 0.05, 40: 0.02, 44: 0.01})

    EPOCH  0: train loss  2.3 | batch  128 | lr 0.100 | Time per epoch 24.11 seconds
    EPOCH  1: train loss  1.6 | batch  128 | lr 0.100 | Time per epoch 20.55 seconds
    EPOCH  2: train loss  1.4 | batch  128 | lr 0.100 | Time per epoch 20.75 seconds
    EPOCH  3: train loss  1.3 | batch  128 | lr 0.100 | Time per epoch 20.74 seconds
    EPOCH  4: train loss  1.2 | batch  128 | lr 0.100 | Time per epoch 20.72 seconds
    EPOCH  5: train loss  1.1 | batch  128 | lr 0.100 | Time per epoch 20.69 seconds
    EPOCH  9: train loss 0.86 | batch  128 | lr 0.100 | Time per epoch 20.60 seconds
    EPOCH 14: train loss 0.72 | batch  128 | lr 0.100 | Time per epoch 20.91 seconds
    EPOCH 19: train loss 0.63 | batch  128 | lr 0.100 | Time per epoch 20.85 seconds
    EPOCH 24: train loss 0.57 | batch  128 | lr 0.100 | Time per epoch 20.83 seconds
    EPOCH 29: train loss 0.54 | batch  128 | lr 0.100 | Time per epoch 20.77 seconds
    EPOCH 34: train loss  0.5 | batch  128 | lr 0.100 | Time per epoch 20.79 seconds
    EPOCH 39: train loss 0.37 | batch  128 | lr 0.050 | Time per epoch 20.73 seconds
    EPOCH 44: train loss 0.25 | batch  128 | lr 0.010 | Time per epoch 20.59 seconds
Training time 999.79 seconds | train accuracy 0.9261 | test accuracy 0.8475

マルチ GPU (ミニバッチサイズ 2048)

次に、トレーニング時間を短縮するために、16 個の GPU にわたり、係数 16 でミニバッチのサイズを増やしましょう。ミニバッチサイズ 2048 を使用し、他のすべてのハイパーパラメーターを同一に維持します。

ctx = [mx.gpu(i) for i in range(16)]
train(epochs=45, batch_size=2048, lr_sched={0: 0.1, 35: 0.05, 40: 0.02, 44: 0.01})
    EPOCH  0: train loss  3.5 | batch 2048 | lr 0.100 | Time per epoch 22.35 seconds
    EPOCH  1: train loss  2.2 | batch 2048 | lr 0.100 | Time per epoch  6.36 seconds
    EPOCH  2: train loss  2.0 | batch 2048 | lr 0.100 | Time per epoch  6.42 seconds
    EPOCH  3: train loss  1.9 | batch 2048 | lr 0.100 | Time per epoch  6.15 seconds
    EPOCH  4: train loss  1.8 | batch 2048 | lr 0.100 | Time per epoch  6.39 seconds
    EPOCH  5: train loss  1.7 | batch 2048 | lr 0.100 | Time per epoch  6.47 seconds
    EPOCH  9: train loss  1.4 | batch 2048 | lr 0.100 | Time per epoch  6.40 seconds
    EPOCH 14: train loss  1.1 | batch 2048 | lr 0.100 | Time per epoch  6.04 seconds
    EPOCH 19: train loss 0.99 | batch 2048 | lr 0.100 | Time per epoch  6.43 seconds
    EPOCH 24: train loss 0.84 | batch 2048 | lr 0.100 | Time per epoch  6.50 seconds
    EPOCH 29: train loss 0.75 | batch 2048 | lr 0.100 | Time per epoch  6.27 seconds
    EPOCH 34: train loss 0.67 | batch 2048 | lr 0.100 | Time per epoch  6.39 seconds
    EPOCH 39: train loss 0.57 | batch 2048 | lr 0.050 | Time per epoch  6.05 seconds
    EPOCH 44: train loss 0.51 | batch 2048 | lr 0.010 | Time per epoch  6.15 seconds
Training time 977.40 seconds | train accuracy 0.8379 | test accuracy 0.7829

エポックごとの時間が大幅に改善されたことがわかりましたが、同じテスト精度を達成することはできませんでした。総トレーニング時間は、1 エポックあたりの時間に比べて改善されていないことに気付くことがあります。これは、各エポックの DataLoader を開始するためのオーバーヘッドのみならず、トレーイングとテスト精度の計算が一定であるためです。一般的に、データセットが当社のデータセット (例、ImageNet データセット) よりもかなり大きい場合、このオーバーヘッドは無視できます。

では、簡単な 5 つのエポックウォームアップステージを紹介し、学習速度の係数を 16 (0.1 から 1.6 まで) に直線的に増加させ、その時点から学習速度のみをスケールして、以前と同様の学習スケジュールに従います。

train(epochs=45,
      batch_size=2048,
      lr_sched={
          0: 0.1,
          1: 0.1 + 0.3,
          2: 0.1 + 0.6,
          3: 0.1 + 0.9,
          4: 0.1 + 1.2,
          5: 0.1 + 1.5,
          35: 0.05 * 16,
          40: 0.02 * 16,
          44: 0.01 * 16})

    EPOCH  0: train loss  4.1 | batch 2048 | lr 0.100 | Time per epoch  7.11 seconds
    EPOCH  1: train loss  2.8 | batch 2048 | lr 0.400 | Time per epoch  6.48 seconds
    EPOCH  2: train loss  2.2 | batch 2048 | lr 0.700 | Time per epoch  6.41 seconds
    EPOCH  3: train loss  1.8 | batch 2048 | lr 1.000 | Time per epoch  6.69 seconds
    EPOCH  4: train loss  1.6 | batch 2048 | lr 1.300 | Time per epoch  6.42 seconds
    EPOCH  5: train loss  1.6 | batch 2048 | lr 1.600 | Time per epoch  6.37 seconds
    EPOCH  9: train loss  1.2 | batch 2048 | lr 1.600 | Time per epoch  6.50 seconds
    EPOCH 14: train loss 0.89 | batch 2048 | lr 1.600 | Time per epoch  6.44 seconds
    EPOCH 19: train loss 0.73 | batch 2048 | lr 1.600 | Time per epoch  6.46 seconds
    EPOCH 24: train loss 0.65 | batch 2048 | lr 1.600 | Time per epoch  6.41 seconds
    EPOCH 29: train loss 0.58 | batch 2048 | lr 1.600 | Time per epoch  6.53 seconds
    EPOCH 34: train loss 0.56 | batch 2048 | lr 1.600 | Time per epoch  6.46 seconds
    EPOCH 39: train loss  0.4 | batch 2048 | lr 0.800 | Time per epoch  6.51 seconds
    EPOCH 44: train loss 0.29 | batch 2048 | lr 0.160 | Time per epoch  6.48 seconds
Training time 916.39 seconds | train accuracy 0.9115 | test accuracy 0.8416

ミニバッチサイズと同じ係数で学習速度をスケールすることで、テスト精度は大幅に向上し、128 のミニバッチサイズとほぼ同じ精度を達成することがわかります。さらなるハイパーパラメーターの最適化は、テスト精度をさらに近づけることができます。ハイパーパラメーターの最適化は、達成可能なテスト精度を最大化するために、128 のミニバッチサイズを持つ事例にも適用できます。

Amazon SageMaker

前述のとおり、上記のコードは、深層学習 AMI を使用して Amazon EC2 ノードから、または Amazon SageMaker ノートブックインスタンスから実行できました。  ハイパーパラメーターの組み合わせの候補に大きなスペースが指定されると、ハイパーパラメーターの手動調整に非常に時間がかかる場合があります。Amazon SageMaker は、モデルをさらに改善するために、複数のトレーニングクラスターを並行して使用する大きなハイパーパラメーター領域の効率的な自動探査のためのインテリジェントアルゴリズムを使用するハイパーパラメーター最適化 (HPO) ツール (この記事を書いている時点では、プレビュー段階) を提供しています。

まとめ

モデルのトレーニングを複数の GPU にスケールすることは、研究を加速し、生産モデルを再トレーニングする際の遅延を減らすための論理的で、経済的なステップです。しかし、追加されたコンピューティング能力でミニバッチサイズを単純にスケールするだけでは、トレーニングが速くならず、トレーニングされたモデルの質が低下する可能性があることに注意する必要があります。ハイパーパラメーターの調整は、ミニバッチサイズを増大させモデルの質を維持する上で重要なステップです。ネットワークをプライミングするなどの特定の最適化のヒントが、適切なトレーニングに必要な場合があります。ハイパーパラメータースペースを効率的に探索するために、Amazon SageMaker HPO ツールを使用することができます。

リファレンス

[1] On Large-Batch Training for Deep Learning: Generalization Gap and Sharp Minima, Keskar et al., 2016
[2] Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour, Goyal et al, 2017
[3] Learning Multiple Layers of Features from Tiny Images, Alex Krizhevsky, 2009.


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

Sina Afrooze は AWS ソフトウェアエンジニアであり、人工知能の深層学習における Apache MXNet の応用に焦点を当てています。彼の専門分野は、デジタルイメージングとコンピュータビジョンです。  彼は、AWS 顧客が Apache MXNet を使用して深層学習ソリューションの規模を拡大することを支援しています。