PyTorch 0.4.1 examples (コード解説) : 画像分類 – CIFAR-10 & CIFAR-100 (CNN)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 07/27/2018 (0.4.0 & 0.4.1)
* 本ページは、github 上の以下の pytorch/examples と keras/examples レポジトリのサンプル・コードを参考にしています:
- https://github.com/pytorch/examples/tree/master/mnist
- https://github.com/keras-team/keras/tree/master/examples
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
素朴な CNN モデル for CIFAR-10
PyTorch 0.4.1 の自作のサンプルをコードの簡単な解説とともに提供しています。
初級チュートリアル程度の知識は仮定しています。
先に MNIST 画像分類タスクのための MLP/CNN/Network in Network モデルを実装しましたので、
次に CIFAR-10 の分類のためのモデルを素朴な CNN で実装してみます。
ついでに同じモデルで CIFAR-100 でも試します。
準備
まずは最初に torch モジュールをインポートします :
import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import torch.utils.data as data
0.4.x の作法で device を設定します :
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("device", device)
ついでにシードも設定しておきましょう :
torch.manual_seed(1)
ハイパーパラメータ
そして必要最小限のハイパーパラメータを設定します :
batch_size = 100 num_classes = 10 epochs = 100
CIFAR-10 データセット
最初にデータセットを作成しますが、NumPy 経由でデータを取得します (もちろん TorchVision 経由でも同様です) 。
tf.keras.datasets を利用して NumPy でデータをロードしますが、channels_first にするために np.moveaxis で軸を移動します (np.moveaxis は np.rollaxis の後継です) :
import numpy as np import tensorflow as tf (x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data() x_train = np.moveaxis(x_train, [3, 1, 2], [1, 2, 3]).astype('float32') x_test = np.moveaxis(x_test, [3, 1, 2], [1, 2, 3]).astype('float32') x_train /= 255 x_test /= 255 y_train = y_train.reshape(-1).astype('long') y_test = y_test.reshape(-1).astype('long')
※ tf.keras.datasets.cifar10.load_data() によって得られるラベルの shape は (50000, 1) ですので、便宜上、(50000,) に reshape しておきます。
そして .from_numpy でデータセットを作成します :
ds_train = data.TensorDataset(torch.from_numpy(x_train), torch.from_numpy(y_train)) ds_test = data.TensorDataset(torch.from_numpy(x_test), torch.from_numpy(y_test))
データローダ
データセットから data.DataLoader でデータローダが作成できます :
dataloader_train = data.DataLoader(dataset=ds_train, batch_size=batch_size, shuffle=True) dataloader_test = data.DataLoader(dataset=ds_test, batch_size=batch_size, shuffle=False)
モデル定義
nn.Module を継承して素朴な CNN モデルを定義します。
(畳み込み層 + 畳み込み層 + マックスプーリング層) のブロックを 2 つスタックします :
class Cifar10Model(nn.Module): def __init__(self): super(Cifar10Model, self).__init__() self.conv11 = nn.Conv2d(3, 32, 3, padding=1) self.conv12 = nn.Conv2d(32, 32, 3, padding=1) self.conv21 = nn.Conv2d(32, 64, 3, padding=1) self.conv22 = nn.Conv2d(64, 64, 3, padding=1) self.fc1 = nn.Linear(64 * 8 * 8, 512) self.fc2 = nn.Linear(512, num_classes) self.dropout1 = nn.Dropout2d(0.25) self.dropout2 = nn.Dropout2d(0.25) self.dropout3 = nn.Dropout2d(0.5) def forward(self, x): x = F.relu(self.conv11(x)) x = F.relu(self.conv12(x)) x = F.max_pool2d(x, (2, 2)) x = self.dropout1(x) x = F.relu(self.conv21(x)) x = F.relu(self.conv22(x)) x = F.max_pool2d(x, (2, 2)) x = self.dropout2(x) x = x.view(-1, 64 * 8 * 8) x = F.relu(self.fc1(x)) x = self.dropout3(x) return self.fc2(x)
モデルをインスタンス化して device に転送します :
model = Cifar10Model().to(device)
インスタンスを直接プリントすると、含まれる層の情報が得られます :
print(model)
Out:
Cifar10Model( (conv11): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (conv12): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (conv21): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (conv22): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (fc1): Linear(in_features=4096, out_features=512, bias=True) (fc2): Linear(in_features=512, out_features=10, bias=True) (dropout1): Dropout2d(p=0.25) (dropout2): Dropout2d(p=0.25) (dropout3): Dropout2d(p=0.5) )
損失関数と optimizer
損失関数としては nn.CrossEntropyLoss() を使用します :
criterion = nn.CrossEntropyLoss()
optimizer は先に SGD を使用しましたので、ここでは Adam を使用してみましょう :
optimizer = optim.Adam(model.parameters(), lr=0.001)
ついでに学習率のスケジューラも試してみます (設定数は直感で決めています) :
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.9)
訓練コード (per epoch)
各エポックの訓練コードです。100 ステップ毎に損失値を表示してやります。
学習率スケジューラを使用していますので scheduler.step() を忘れずにエポック毎に呼び出します :
global_step = 0 def train(epoch, writer): model.train() scheduler.step() print("\n--- Epoch : %2d ---" % epoch) print("lr : %f" % optimizer.param_groups[0]['lr']) steps = len(ds_train)//batch_size for step, (images, labels) in enumerate(dataloader_train, 1): global global_step global_step += 1 images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() if step % 100 == 0: print ('Epoch [%d/%d], Step [%d/%d], Loss: %.4f' % (epoch, epochs, step, steps, loss.item())) writer.add_scalar('train/train_loss', loss.item() , global_step)
TensorBoard を利用するためには、グローバルステップをカウントする global_step を追加して、SummaryWriter の .add_scalar() メソッドで損失を書き込みます。
評価コード (per epoch)
各エポックの最後にテスト・データセットで評価します。
単純に正解数をカウントしています :
def eval(epoch, writer): model.eval() correct = 0 total = 0 with torch.no_grad(): for (images, labels) in dataloader_test: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs.data, 1) correct += (predicted == labels).sum().item() total += labels.size(0) print("Val Acc : %.4f" % (correct/total)) writer.add_scalar('eval/val_acc', correct*100/total, epoch)
TensorBoard を利用するために、(評価ルーチンはエポック毎ですので) epoch カウントを利用して検証精度を書き込みます。
訓練の実行
次のループで訓練が実行できます :
from tensorboardX import SummaryWriter writer = SummaryWriter() for epoch in range(1, epochs+1): train(epoch, writer) eval(epoch, writer) writer.close()
tensorboardX モジュールを使用すれば TensorBoard が利用できます。
SummaryWriter をインスタンス化して訓練/評価ルーチンに渡してやります。
モデルは pickle でセーブできます :
torch.save(model.state_dict(), 'model_cifar10.pkl')
実行時出力です :
--- Epoch : 1 --- lr : 0.001000 Epoch [1/100], Step [100/500], Loss: 1.9326 Epoch [1/100], Step [200/500], Loss: 1.7507 Epoch [1/100], Step [300/500], Loss: 1.7669 Epoch [1/100], Step [400/500], Loss: 1.4873 Epoch [1/100], Step [500/500], Loss: 1.3264 Val Acc : 0.4993 ... --- Epoch : 70 --- lr : 0.000590 Epoch [70/100], Step [100/500], Loss: 0.2639 Epoch [70/100], Step [200/500], Loss: 0.1678 Epoch [70/100], Step [300/500], Loss: 0.2088 Epoch [70/100], Step [400/500], Loss: 0.1274 Epoch [70/100], Step [500/500], Loss: 0.2136 Val Acc : 0.7819 ... --- Epoch : 100 --- lr : 0.000063 Epoch [100/100], Step [100/500], Loss: 0.1572 Epoch [100/100], Step [200/500], Loss: 0.3368 Epoch [100/100], Step [300/500], Loss: 0.1692 Epoch [100/100], Step [400/500], Loss: 0.2832 Epoch [100/100], Step [500/500], Loss: 0.2284 Val Acc : 0.7803
検証精度 78.19 % がベストです。平凡な結果で、素朴な CNN で 80 % 前後しか出ません :
明示的な初期化 (CIFAR-10)
次に畳み込み層の重みを明示的に初期化した上で訓練してみます。
モデルのアーキテクチャの変更によるインパクトの方が遥かに大きいですが、感触を掴むために一応試しておきます。
◆ 最初に Xavier 一様分布で明示的に初期化したところ、CIFAR-10 について精度が 78.19 % から 78.37% に改善されました :
◆ 次に訓練の条件を揃えて幾つかの初期化アルゴリズムを比較してみました :
それぞれの到達精度をまとめると以下のようなものです :
- 初期化なし : 77.06 %
- xavier_uniform_ : 77.55 %
- kaiming_uniform_ : 78.00 %
- orthogonal_ : 78.31 %
概ね、次のように推移しています :
orthogonal_ > kaiming_uniform_ > xavier_uniform_ > 初期化なし
CIFAR-100 データセット
ついでに同じモデルで CIFAR-100 でも試しておきます。
上述のコードがほぼそのまま動作しますが、違う箇所だけ指摘しておきます。
まず、クラス数は 100 です。エポック数も試しに多めに取ります :
batch_size = 100 num_classes = 100 epochs = 150
データのロードは tf.keras.datasets.cifar100 モジュールを使用します :
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar100.load_data()
モデルはそのまま流用しても良かったのですが、クラス数が増えたので、分類層の完全結合層を一つ増やしました :
class Cifar100Model(nn.Module): def __init__(self): super(Cifar10Model, self).__init__() self.conv11 = nn.Conv2d(3, 32, 3, padding=1) self.conv12 = nn.Conv2d(32, 32, 3, padding=1) self.conv21 = nn.Conv2d(32, 64, 3, padding=1) self.conv22 = nn.Conv2d(64, 64, 3, padding=1) self.fc1 = nn.Linear(64 * 8 * 8, 1024) self.fc2 = nn.Linear(1024, 512) self.fc3 = nn.Linear(512, num_classes) self.dropout1 = nn.Dropout2d(0.25) self.dropout2 = nn.Dropout2d(0.25) self.dropout3 = nn.Dropout2d(0.5) self.dropout4 = nn.Dropout2d(0.5) def forward(self, x): x = F.relu(self.conv11(x)) x = F.relu(self.conv12(x)) x = F.max_pool2d(x, (2, 2)) x = self.dropout1(x) x = F.relu(self.conv21(x)) x = F.relu(self.conv22(x)) x = F.max_pool2d(x, (2, 2)) x = self.dropout2(x) x = x.view(-1, 64 * 8 * 8) x = F.relu(self.fc1(x)) x = self.dropout3(x) x = self.fc2(x) x = self.dropout4(x) return self.fc3(x)
実行時出力です :
--- Epoch : 1 --- lr : 0.001000 Epoch [1/150], Step [100/500], Loss: 4.4598 Epoch [1/150], Step [200/500], Loss: 4.1714 Epoch [1/150], Step [300/500], Loss: 4.1487 Epoch [1/150], Step [400/500], Loss: 3.8165 Epoch [1/150], Step [500/500], Loss: 3.9835 Val Acc : 0.1219 ... --- Epoch : 33 --- lr : 0.000810 Epoch [33/150], Step [100/500], Loss: 1.5739 Epoch [33/150], Step [200/500], Loss: 2.0363 Epoch [33/150], Step [300/500], Loss: 1.5778 Epoch [33/150], Step [400/500], Loss: 1.7401 Epoch [33/150], Step [500/500], Loss: 2.1234 Val Acc : 0.3785 ... --- Epoch : 150 --- lr : 0.000254 Epoch [150/150], Step [100/500], Loss: 0.5650 Epoch [150/150], Step [200/500], Loss: 0.7036 Epoch [150/150], Step [300/500], Loss: 0.5749 Epoch [150/150], Step [400/500], Loss: 0.5420 Epoch [150/150], Step [500/500], Loss: 0.7754 Val Acc : 0.3666
せいぜい 38% です。CIFAR-100 は必ずしも容易なタスクではありませんが、(少なくとも記録上 75 % は出るはずなので) 50 % 程度の平凡な精度は達成したいところですが、素朴な ConvNet では厳しいようです。
また、TensorBoard のグラフで精度を見ると overfitting の傾向が見られます :
明示的な初期化 (CIFAR-100)
CIFAR-100 の訓練についても明示的な初期化を試しておきます。
◆ Xavier 一様分布でモデルの畳み込み層の重みを明示的に初期化したところ、
CIFAR-100 については 38.23 % が 40.33 % に改善されました :
◆ そして条件を揃えて各種初期化アルゴリズムを比較してみました。
到達精度をまとめると :
- 初期化なし – 43.53 %
- xavier_uniform_ – 44.44 %
- kaiming_uniform_ – 44.80 %
- orthogonal_ – 44.69 %
以上