PyTorch 0.4.1 examples (コード解説) : 画像分類 – MNIST (CNN)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 07/26/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 までご一報いただけると嬉しいです。
MNIST CNN
PyTorch 0.4.x の自作のサンプルをコードの簡単な解説とともに提供しています。
初級チュートリアル程度の知識は仮定しています。
先に MNIST 画像分類タスクのための MLP モデルを実装しましたので、
同じタスクに対して畳み込みネットワーク (CNN, ConvNet) でモデルを実装してみます。
準備
まずは最初に 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 = 20
MNIST データセット
最初にデータセットを作成しますが、NumPy 経由でデータを取得します (もちろん TorchVision 経由でも同様です) 。
tf.keras.datasets を利用して NumPy でデータをロードしますが、畳み込みネットを使用しますので、shape (1, 28, 28) に reshape している点に注意してください :
import tensorflow as tf # the data, split between train and test sets (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() x_train = x_train.reshape(60000, 1, 28, 28).astype('float32') x_test = x_test.reshape(10000, 1, 28, 28).astype('float32') # 正規化 x_train /= 255 x_test /= 255 y_train = y_train.astype('long') y_test = y_test.astype('long')
そして .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 モデルを定義します。Dropout 層も追加しました :
class CNNModel (nn.Module): def __init__(self): super(CNNModel, self).__init__() self.conv1 = nn.Conv2d(1, 32, 3, padding=1) self.conv2 = nn.Conv2d(32, 64, 3, padding=1) self.dropout1 = nn.Dropout2d(0.25) self.dropout2 = nn.Dropout2d(0.5) self.fc1 = nn.Linear(64*14*14, 128) self.fc2 = nn.Linear(128, num_classes) def forward(self, x): x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = F.max_pool2d(x, (2, 2)) x = self.dropout1(x) x = x.view(-1, 64*14*14) x = F.relu(self.fc1(x)) x = self.dropout2(x) return F.relu(self.fc2(x))
モデルをインスタンス化して device に転送します :
model = CNNModel().to(device)
インスタンスを直接プリントすると、含まれる層の情報が得られます :
print(model)
Out:
CNNModel( (conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (dropout1): Dropout2d(p=0.25) (dropout2): Dropout2d(p=0.5) (fc1): Linear(in_features=12544, out_features=128, bias=True) (fc2): Linear(in_features=128, out_features=10, bias=True) )
損失関数と optimizer
損失関数としては nn.CrossEntropyLoss() を使用します。
nn.NLLLoss() を使用する場合には、モデル定義の forward 関数で F.log_softmax(self.fc3(x)) を返す必要があります :
criterion = nn.CrossEntropyLoss() #criterion = nn.NLLLoss()
optimizer は引続き SGD を使用します :
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
訓練コード (per epoch)
各エポックの訓練コードです。
バッチは最初に device に転送します。100 ステップ毎に損失値を表示してやります :
global_step = 0 def train(epoch, writer): model.train() 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.pkl')
実行時出力です :
Epoch [1/20], Step [100/600], Loss: 1.4448 Epoch [1/20], Step [200/600], Loss: 1.0823 Epoch [1/20], Step [300/600], Loss: 0.6340 Epoch [1/20], Step [400/600], Loss: 0.9224 Epoch [1/20], Step [500/600], Loss: 0.4842 Epoch [1/20], Step [600/600], Loss: 0.3546 Val Acc : 0.9561 Epoch [2/20], Step [100/600], Loss: 0.4396 Epoch [2/20], Step [200/600], Loss: 0.6849 Epoch [2/20], Step [300/600], Loss: 0.3359 Epoch [2/20], Step [400/600], Loss: 0.2871 Epoch [2/20], Step [500/600], Loss: 0.5054 Epoch [2/20], Step [600/600], Loss: 0.4565 Val Acc : 0.9695 ... Epoch [20/20], Step [100/600], Loss: 0.2833 Epoch [20/20], Step [200/600], Loss: 0.1941 Epoch [20/20], Step [300/600], Loss: 0.1546 Epoch [20/20], Step [400/600], Loss: 0.0919 Epoch [20/20], Step [500/600], Loss: 0.2461 Epoch [20/20], Step [600/600], Loss: 0.0397 Val Acc : 0.9813
精度 98.13 % が得られました。先の MLP モデルでは精度 96.99 % でしたので、1 ポイント以上改善しています。
グラフの推移を見ると、もう訓練エポックをもう少し増やせば更に精度が上がるかもしれません。
補記: 明示的な初期化
◆ 補足として、次のようなコードで Xavier (Glorot) の一様分布でモデルの畳み込み層の重みを明示的に初期化してみましたところ :
import torch.nn.init as init def _initialize_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): init.xavier_uniform_(m.weight.data, gain=init.calculate_gain('relu'))
精度 99.24 % まで改善されました :
◆ また、(重みテンソルが直交行列になるように初期化される) init.orthogonal_ では精度 99.27 % が得られました :
◆ 更に、init.kaiming_uniform_ では精度 99.35 % が得られました :
以上