CS231n/Assignment2/PyTorch(パイトーチ)

今回は、Stanford Univ/CS231n/Assignment2/PyTorch(パイトーチ)をやる。いよいよ人気フレームワークを使った機械学習のエクササイズ第二弾だ。現時点では、グーグルのテンソルフローとフェイスブックのパイトーチが人気を二分しているので、両フレームワークだけやっておけばいいという人さえいる。

このエクササイズでは、畳み込みネットアーキテクチャを規定して、CIFAR-10データ・セットでそれを訓練するための強力なPyTorchフレームワークの使い方を学習する。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torch.utils.data import sampler
import torchvision.datasets as dset
import torchvision.transforms as T
import numpy as np
import timeit

Load Datasets

先ずはCIFAR-10 datasetをloadする。初期ロードの場合、数分を要するが、その後はファイルはキャッシュされ続けるはず。

class ChunkSampler(sampler.Sampler):
    """Samples elements sequentially from some offset. 
    Arguments:
        num_samples: # of desired datapoints
        start: offset where we should start selecting from
    """
    def __init__(self, num_samples, start = 0):
        self.num_samples = num_samples
        self.start = start

    def __iter__(self):
        return iter(range(self.start, self.start + self.num_samples))

    def __len__(self):
        return self.num_samples

NUM_TRAIN = 49000
NUM_VAL = 1000

cifar10_train = dset.CIFAR10('./cs231n/datasets', train=True, download=True,
                           transform=T.ToTensor())
loader_train = DataLoader(cifar10_train, batch_size=64, sampler=ChunkSampler(NUM_TRAIN, 0))

cifar10_val = dset.CIFAR10('./cs231n/datasets', train=True, download=True,
                           transform=T.ToTensor())
loader_val = DataLoader(cifar10_val, batch_size=64, sampler=ChunkSampler(NUM_VAL, NUM_TRAIN))

cifar10_test = dset.CIFAR10('./cs231n/datasets', train=False, download=True,
                          transform=T.ToTensor())
loader_test = DataLoader(cifar10_test, batch_size=64)
Files already downloaded and verified
Files already downloaded and verified
Files already downloaded and verified

今のところはCPUフレンドリーのデータ型を使う。後半でGPUフレンドリーのデータ型に移行して全計算をGPUで実行して速度を比較する。

dtype = torch.FloatTensor # the CPU datatype

# Constant to control how frequently we print train loss
print_every = 100

# This is a little utility that we'll use to reset the model
# if we want to re-initialize all our parameters
def reset(m):
    if hasattr(m, 'reset_parameters'):
        m.reset_parameters()

Example Model

Some assorted tidbits(関連情報)

単純なモデルから始める前に、PyTorchがTensorFlowのように、numpy ndarrayに類似したn次元アレイであるテンソルで演算をGPU上で行えることを留意しておく。

ここで、提供されているFlatten function(平坦化関数)の説明をする。画像データ(より適切には、中間特徴マップ)が最初はN x C x H x Wであることを覚えておく。

  • N = データポイント数
  • C = チャネル数(通常はRGBの3)
  • H = 各画像の縦の画素数
  • W = 各画像の横の画素数

これは、画素がどこで互いに関連し合うかの空間把握が必要な2D畳み込みのようなことをやる場合、画像データを構成する正しいやり方だが、全結合アフィン層に画像データを入力する場合、データを別個のチャネルや行、列に分けることに意味がなくなるので、各データ標本は単一ベクトルによって形成する必要がある。なので、各画像のC x H x W値を”Flatten”演算を使って、単一ロングベクトルに一纏めにする。下記のフラット化関数は、最初に渡されたバッチデータからN, C, H, Wを読み込んでからデータのviewを返す。Viewは、numpyのreshapeメソッドに似ていて、xの次元を??が何でもいい(今回はC x H x Wだが、明確に規定する必要はない)N x ??にリシェイプする。

class Flatten(nn.Module):
    def forward(self, x):
        N, C, H, W = x.size() # read in N, C, H, W
        return x.view(N, -1)  # "flatten" the C * H * W values into a single vector per image

The example model itself

モデルを訓練する最初のステップ、アーキテクチャを定義する。下記のパイトーチで定義された畳み込み神経回路網の一例を見て、それぞれの行が何をしているのかを理解し、各層が前の層の上に形成されていることを留意する。モデルの訓練を始める前に、どのようにして全てがセットアップされるのかを理解する。nn.Sequentialは交互に各層に適用されるコンテナ。

今回の例では、2次元畳み込み層 (Conv2d)、ReLU activations (ReLU活性化)、全結合層(線形)が使われている。また、Hinge loss function (ヒンジ損失関数) とAdam optimizer (アダム・オプティマイザ) も使われている。Linear layer (線形層) のパラメーターが5408と10である理由を確実に理解する。

# Here's where we define the architecture of the model... 
simple_model = nn.Sequential(
                nn.Conv2d(3, 32, kernel_size=7, stride=2),
                nn.ReLU(inplace=True),
                Flatten(), # see above for explanation
                nn.Linear(5408, 10), # affine layer
              )

# Set the type of all data in this model to be FloatTensor 
simple_model.type(dtype)

loss_fn = nn.CrossEntropyLoss().type(dtype)
optimizer = optim.Adam(simple_model.parameters(), lr=1e-2) # lr sets the learning rate of the optimizer

PyTorchは、他に多くの層型、損失関数、オプティマイザをサポートする。次にこれらを使用することになるので、下記の資料を参考にする。
※spatial batch normはPyTorchではBatchNorm2Dと呼ばれている。

Training a specific model

ここで構築するモデルを指定する。ここでのゴールは、モデルの性能ではなく(それは次の課題)、TensorFlowの参照資料に慣れ親しむことと、自分のモデルを設定することである。指針として上で提供されたコードを使用し、また、PyTorchの説明書を利用して、下記の仕様でモデルを設定する。

  1. 7×7 Convolutional Layer with 32 filters and stride of 1 ReLU Activation Layer
  2. Spatial Batch Normalization Layer (空間バッチ正規化層)
  3. 2×2 Max Pooling layer with a stride of 2
  4. Affine layer (アフィン層) with 1024 output units
  5. ReLU Activation Layer (ReLU活性化層)
  6. Affine layer from 1024 input units to 10 outputs

最後に交差エントロピー損失関数とRMSprop学習ルールを設定する。

fixed_model_base = nn.Sequential(
                    nn.Conv2d(3, 32, kernel_size=7, stride=1), 
                    nn.ReLU(inplace=True),
                    nn.BatchNorm2d(32),
                    nn.MaxPool2d(2, stride=2),
                    Flatten(),
                    nn.Linear(5408, 1024),
                    nn.ReLU(inplace=True),
                    nn.Linear(1024, 10)
                )

fixed_model = fixed_model_base.type(dtype)

正しいことをしているかを確かめるために、出力の次元(バッチサイズが64で、最後のアフィン層の出力は10クラスに一致して10なので64 x 10になるはず)を下のツールを使ってチェックする。

## Now we're going to feed a random batch into the model you defined and make sure the output is the right size
x = torch.randn(64, 3, 32, 32).type(dtype)
x_var = Variable(x.type(dtype)) # Construct a PyTorch Variable out of your input data
ans = fixed_model(x_var)        # Feed it through the model! 

# Check to make sure what comes out of your model
# is the right dimensionality... this should be True
# if you've done everything correctly
np.array_equal(np.array(ans.size()), np.array([64, 10]))
True

GPU!

次に、モデルとデータのdtypeをGPUフレンドリーテンソルに変えて、何が起こるかを見てみる。実際、モデルと入力テンソルが古い型ではなく新しいdtypeとしてキャストすることを除けば何も変わっていない。

もし、falseを返したり、さもなければ、何かしらのエラーメッセージを吐き出してフェイルした場合、それは、NVIDIA GPUがないことを意味している可能性がある。そういう場合は、Google Colabを使えばいい。

# Verify that CUDA is properly configured and you have a GPU available

torch.cuda.is_available()
True
import copy
gpu_dtype = torch.cuda.FloatTensor

fixed_model_gpu = copy.deepcopy(fixed_model_base).type(gpu_dtype)

x_gpu = torch.randn(64, 3, 32, 32).type(gpu_dtype)
x_var_gpu = Variable(x.type(gpu_dtype)) # Construct a PyTorch Variable out of your input data
ans = fixed_model_gpu(x_var_gpu)        # Feed it through the model! 

# Check to make sure what comes out of your model
# is the right dimensionality... this should be True
# if you've done everything correctly
np.array_equal(np.array(ans.size()), np.array([64, 10]))
True

CPU上で実行するフォワードパスの性能を評価するために下のセルを実行。

%%timeit 
ans = fixed_model(x_var)
16.8 ms ± 365 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

… and now the GPU(今度はGPU)

%%timeit 
torch.cuda.synchronize() # Make sure there are no pending GPU computations
ans = fixed_model_gpu(x_var_gpu)        # Feed it through the model! 
torch.cuda.synchronize() # Make sure there are no pending GPU computations
1.41 ms ± 37.9 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

このように単純なフォワードパスでさえもGPUの方が有意に速いので、今後モデルを訓練する時はGPUを使うようにする。モデルとテンソルにGPUデータ型torch.cuda.FloatTensorを用いる必要があるということを心に留めておく(ここではgpu_dtype)。

Train the model

モデルの定義の仕方と定義したモデルを使って、適当なデータで単一フォワードパスを実践したので、実際に、どのようにして、自分の訓練データで全1エポックを(上で提供したsimple_modelを使用して)訓練するかを一つずつ説明する。

下で使用されている各パイトーチ関数が、自製神経回路網実装で実装したものとどのように対応するのかを理解していることを確認する。

下記のコードでは重みはリセットしていないので、セルを複数回実行すると事実上複数エポック訓練することになる(ので性能は向上するはず)ことに留意する。

まず最初に、RMSpropオプティマイザ(学習率=1e-3)と交差エントロピー損失関数を設定する。

loss_fn = nn.CrossEntropyLoss().type(dtype)
optimizer = optim.RMSprop(fixed_model_gpu.parameters(), lr=1e-3)
# This sets the model in "training" mode. This is relevant for some layers that may have different behavior
# in training mode vs testing mode, such as Dropout and BatchNorm. 
fixed_model_gpu.train()

# Load one batch at a time.
for t, (x, y) in enumerate(loader_train):
    x_var = Variable(x.type(gpu_dtype))
    y_var = Variable(y.type(gpu_dtype).long())

    # This is the forward pass: predict the scores for each class, for each x in the batch.
    scores = fixed_model_gpu(x_var)
    
    # Use the correct y values and the predicted y values to compute the loss.
    loss = loss_fn(scores, y_var)
    
    if (t + 1) % print_every == 0:
        print('t = %d, loss = %.4f' % (t + 1, loss.data[0]))

    # Zero out all of the gradients for the variables which the optimizer will update.
    optimizer.zero_grad()
    
    # This is the backwards pass: compute the gradient of the loss with respect to each 
    # parameter of the model.
    loss.backward()
    
    # Actually update the parameters of the model using the gradients computed by the backwards pass.
    optimizer.step()
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:17: UserWarning: invalid index of a 0-dim tensor. This will be an error in PyTorch 0.5. Use tensor.item() to convert a 0-dim tensor to a Python number
t = 100, loss = 1.4926
t = 200, loss = 1.5233
t = 300, loss = 1.4597
t = 400, loss = 1.2937
t = 500, loss = 1.1986
t = 600, loss = 1.4079
t = 700, loss = 1.2902

PyTorchでの学習工程を理解できたので、下記のヘルパー関数を使って、モデルを複数エポック訓練してモデルの正確度をチェックする。

def train(model, loss_fn, optimizer, num_epochs=1):
    for epoch in range(num_epochs):
        print('Starting epoch %d / %d' % (epoch + 1, num_epochs))
        model.train()
        for t, (x, y) in enumerate(loader_train):
            x_var = Variable(x.type(gpu_dtype))
            y_var = Variable(y.type(gpu_dtype).long())
            scores = model(x_var)            
            loss = loss_fn(scores, y_var)
            if (t + 1) % print_every == 0:
                print('t = %d, loss = %.4f' % (t + 1, loss.data[0]))
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

def check_accuracy(model, loader):
    if loader.dataset.train:
        print('Checking accuracy on validation set')
    else:
        print('Checking accuracy on test set')   
    num_correct = 0
    num_samples = 0
    model.eval() # Put the model in test mode (the opposite of model.train(), essentially)
    for x, y in loader:
        x_var = Variable(x.type(gpu_dtype), volatile=True)
        scores = model(x_var)
        _, preds = scores.data.cpu().max(1)
        num_correct += (preds == y).sum()
        num_samples += preds.size(0)
    acc = float(num_correct) / num_samples
    print('Got %d / %d correct (%.2f)' % (num_correct, num_samples, 100 * acc))

Check the accuracy of the model.

訓練を実行して正確度を確認する(下で作るモデルを検証する際に、これらの手法を遠慮なく利用してよい)。訓練損失は約1.2-1.4、検証精度は大体50-60%に収まるはず。上で述べたように、セルを再実行するとより多くのエポック訓練することになり、モデルの性能は回数に合わせて向上することになる。

これらの数字について心配する必要は全くない。何故なら、これは、自分自身のモデルをデザインする前のただの練習に過ぎないからだ。

torch.cuda.random.manual_seed(12345)
fixed_model_gpu.apply(reset)
train(fixed_model_gpu, loss_fn, optimizer, num_epochs=1)
check_accuracy(fixed_model_gpu, loader_val)
Starting epoch 1 / 1
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:11: UserWarning: invalid index of a 0-dim tensor. This will be an error in PyTorch 0.5. Use tensor.item() to convert a 0-dim tensor to a Python number
  # This is added back by InteractiveShellApp.init_path()
t = 100, loss = 1.3402
t = 200, loss = 1.5124
t = 300, loss = 1.3985
t = 400, loss = 1.2745
t = 500, loss = 1.1373
t = 600, loss = 1.4750
t = 700, loss = 1.2904
Checking accuracy on validation set
Got 112 / 1000 correct (11.20)
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:25: UserWarning: volatile was removed and now has no effect. Use `with torch.no_grad():` instead.
def train(model, loss_fn, optimizer, num_epochs=1):
    for epoch in range(num_epochs):
        print('Starting epoch %d / %d' % (epoch + 1, num_epochs))
        model.train()
        for t, (x, y) in enumerate(loader_train):
            x_var = Variable(x.type(gpu_dtype))
            y_var = Variable(y.type(gpu_dtype).long())
            scores = model(x_var)            
            loss = loss_fn(scores, y_var)
            if (t + 1) % print_every == 0:
                print('t = %d, loss = %.4f' % (t + 1, loss.item()))
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

def check_accuracy(model, loader):
    if loader.dataset.train:
        print('Checking accuracy on validation set')
    else:
        print('Checking accuracy on test set')   
    num_correct = 0
    num_samples = 0
    model.eval() # Put the model in test mode (the opposite of model.train(), essentially)
    with torch.no_grad():
        for x, y in loader:
            x_var = Variable(x.type(gpu_dtype))
            scores = model(x_var)
            _, preds = scores.data.cpu().max(1)
            num_correct += (preds == y).sum()
            num_samples += preds.size(0)
        acc = float(num_correct) / num_samples
        print('Got %d / %d correct (%.2f)' % (num_correct, num_samples, 100 * acc))
torch.cuda.random.manual_seed(12345)
fixed_model_gpu.apply(reset)
train(fixed_model_gpu, loss_fn, optimizer, num_epochs=1)
check_accuracy(fixed_model_gpu, loader_val)
Starting epoch 1 / 1
t = 100, loss = 1.3543
t = 200, loss = 1.5998
t = 300, loss = 1.3523
t = 400, loss = 1.2559
t = 500, loss = 1.1646
t = 600, loss = 1.4642
t = 700, loss = 1.3469
Checking accuracy on validation set
Got 182 / 1000 correct (18.20)

Don’t forget the validation set!

check_accuracyの二番目の引数としてloader_testかloader_valのどちらか一方を渡すことで、テストセットまたは検証セットを用いて評価するのにcheck_accuracy関数を利用することができることに留意する。アーキテクチャとハイパーパラメータの微調整が終わるまでテストセットに手を付けてはならないし、最終的な値を報告するのに最後に一回だけテストセットを実行すること。

参考サイトhttps://github.com/