PyTorch (6) Convolutional Neural Network
今回は畳み込みニューラルネットワーク。MNISTとCIFAR-10で実験してみた。
MNIST
import numpy as np import torch import torch.nn as nn import torchvision.datasets as dsets import torchvision.transforms as transforms # Hyperparameters num_epochs = 10 batch_size = 100 learning_rate = 0.001 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(device)
今回からGPU対応した。PyTorchはKerasと違ってGPUモードにするために明示的にコーディングが必要。ここがちょっと面倒><
# MNIST Dataset (Images and Labels) train_dataset = dsets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True) test_dataset = dsets.MNIST(root='./data', train=False, transform=transforms.ToTensor()) # Dataset Loader (Input Pipline) train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True) test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)
MNISTを読み込むDataSetとDataLoaderを作成。前回(2018/2/4)と同じ。
class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.layer1 = nn.Sequential( nn.Conv2d(1, 16, kernel_size=5, padding=2), nn.BatchNorm2d(16), nn.ReLU(), nn.MaxPool2d(2)) self.layer2 = nn.Sequential( nn.Conv2d(16, 32, kernel_size=5, padding=2), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2)) self.fc = nn.Linear(7 * 7 * 32, 10) def forward(self, x): out = self.layer1(x) out = self.layer2(out) out = out.view(out.size(0), -1) out = self.fc(out) return out
- CNNはブロック単位で処理した方がよいのでブロック(Conv+BN+ReLU+Pooling)ごとにまとめて
Sequential
を使うとわかりやすくなる。Kerasっぽく書けるのでいい! Conv2d
やBatchNorm2d
はKerasと違って入力と出力のユニットサイズを省略できない。- サイズを自分で計算するのが面倒ならば、モデルの途中結果サイズを
print(out.size())
で出力してみるとよい。
# テスト model = CNN().to(device) images, labels = iter(train_loader).next() print(images.size()) images = images.to(device) outputs = model(images)
torch.Size([100, 1, 28, 28])
モデルオブジェクトの作成。
model = CNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
GPUモードで動かすには
- モデルを
to(device)
でGPUに転送する - テンソルデータも
to(device)
でGPUに転送する
の2つだけ実装すればよい。Kerasより面倒だけど意外に簡単。
print(model)
するといい感じで構造が表示される。
CNN( (layer1): Sequential( (0): Conv2d(1, 16, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2)) (1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (2): ReLU() (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (layer2): Sequential( (0): Conv2d(16, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2)) (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (2): ReLU() (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (fc): Linear(in_features=1568, out_features=10, bias=True) )
次は訓練ループ。これまでとほとんど同じ。
def train(train_loader): model.train() running_loss = 0 for batch_idx, (images, labels) in enumerate(train_loader): images = images.to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) running_loss += loss.item() loss.backward() optimizer.step() train_loss = running_loss / len(train_loader) return train_loss def valid(test_loader): model.eval() running_loss = 0 correct = 0 total = 0 with torch.no_grad(): for batch_idx, (images, labels) in enumerate(test_loader): images = images.to(device) labels = labels.to(device) outputs = model(images) loss = criterion(outputs, labels) running_loss += loss.item() predicted = outputs.max(1, keepdim=True)[1] correct += predicted.eq(labels.view_as(predicted)).sum().item() total += labels.size(0) val_loss = running_loss / len(test_loader) val_acc = correct / total return val_loss, val_acc loss_list = [] val_loss_list = [] val_acc_list = [] for epoch in range(num_epochs): loss = train(train_loader) val_loss, val_acc = valid(test_loader) print('epoch %d, loss: %.4f val_loss: %.4f val_acc: %.4f' % (epoch, loss, val_loss, val_acc)) # logging loss_list.append(loss) val_loss_list.append(val_loss) val_acc_list.append(val_acc) # save the trained model np.save('loss_list.npy', np.array(loss_list)) np.save('val_loss_list.npy', np.array(val_loss_list)) np.save('val_acc_list.npy', np.array(val_acc_list)) torch.save(model.state_dict(), 'cnn.pkl')
Batch Normalizationなど学習時と推論時で挙動が変わるレイヤを使う場合はモデルのモードに要注意!
model.train()
で訓練モードmodel.eval()
で評価モード
に切り替える必要がある。切り替えなくてもエラーにはならないが性能が出なかったりする。Kerasにはなかったので忘れやすい!
また先に書いたようにGPUモードで動かすときはテンソルを下のように to(device)
でGPUに送る必要がある!
images = images.to(device) labels = labels.to(device)
GPUで動かすと下のようになった。
epoch 0, loss: 0.1682 val_loss: 0.0545 val_acc: 0.9819 epoch 1, loss: 0.0498 val_loss: 0.0398 val_acc: 0.9865 epoch 2, loss: 0.0386 val_loss: 0.0330 val_acc: 0.9886 epoch 3, loss: 0.0292 val_loss: 0.0351 val_acc: 0.9887 epoch 4, loss: 0.0248 val_loss: 0.0296 val_acc: 0.9902 epoch 5, loss: 0.0203 val_loss: 0.0320 val_acc: 0.9894 epoch 6, loss: 0.0179 val_loss: 0.0342 val_acc: 0.9886 epoch 7, loss: 0.0144 val_loss: 0.0309 val_acc: 0.9894 epoch 8, loss: 0.0121 val_loss: 0.0285 val_acc: 0.9912 epoch 9, loss: 0.0109 val_loss: 0.0361 val_acc: 0.9898
前回、多層パーセプトロンでの精度が85%くらいだったので99.3%出るCNNはすごく性能がよいことがわかる。
CIFAR-10
次は同じことをCIFAR-10でやってみよう。
import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import torchvision import torchvision.transforms as transforms device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(device)
num_epochs = 30 batch_size = 128 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # [0, 1] => [-1, 1] ]) train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=4) test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=4) classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
- CIFAR-10のデータは画像のピクセルが [0, 1] ではなく、[-1, 1] になるように標準化している。
num_workers
を指定するとファイルの読み込みが並列化される(CPUのコアが複数ある場合はすごく速くなる!)
いくつかサンプルを描画してみよう。
import matplotlib.pyplot as plt import numpy as np %matplotlib inline def imshow(img): # unnormalize [-1, 1] => [0, 1] img = img / 2 + 0.5 npimg = img.numpy() # [c, h, w] => [h, w, c] plt.imshow(np.transpose(npimg, (1, 2, 0))) images, labels = iter(train_loader).next() images, labels = images[:16], labels[:16] imshow(torchvision.utils.make_grid(images, nrow=4, padding=1)) plt.axis('off')
いい感じ。
参考のチュートリアルに従ってCNNの簡単なモデルを定義してみた。上のMNISTよりシンプルという・・・
class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.conv1 = nn.Conv2d(3, 6, kernel_size=5) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 16, kernel_size=5) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(-1, 16 * 5 * 5) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x
ReLUは nn.ReLU()
で層として定義する場合もあるが、パラメータがないので F.relu()
のように関数として使うこともできる。層で書いておくとモデルを print
したときに表示される(さっきの例)。
model = CNN().to(device)
print(model)
CNN( (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1)) (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1)) (fc1): Linear(in_features=400, out_features=120, bias=True) (fc2): Linear(in_features=120, out_features=84, bias=True) (fc3): Linear(in_features=84, out_features=10, bias=True) )
この表示方法はシンプルだがとてもわかりやすい。
あとはこれまでと同じ。
criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) def train(train_loader): model.train() running_loss = 0 for i, (images, labels) in enumerate(train_loader): images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) running_loss += loss.item() loss.backward() optimizer.step() train_loss = running_loss / len(train_loader) return train_loss def valid(test_loader): model.eval() running_loss = 0 correct = 0 total = 0 with torch.no_grad(): for i, (images, labels) in enumerate(test_loader): images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) running_loss += loss.item() predicted = outputs.max(1, keepdim=True)[1] correct += predicted.eq(labels.view_as(predicted)).sum().item() total += labels.size(0) val_loss = running_loss / len(test_loader) val_acc = correct / total return val_loss, val_acc loss_list = [] val_loss_list = [] val_acc_list = [] for epoch in range(num_epochs): loss = train(train_loader) val_loss, val_acc = valid(test_loader) print('epoch %d, loss: %.4f val_loss: %.4f val_acc: %.4f' % (epoch, loss, val_loss, val_acc)) # logging loss_list.append(loss) val_loss_list.append(val_loss) val_acc_list.append(val_acc) print('Finished training') # save the trained model np.save('loss_list.npy', np.array(loss_list)) np.save('val_loss_list.npy', np.array(val_loss_list)) np.save('val_acc_list.npy', np.array(val_acc_list)) torch.save(model.state_dict(), 'cnn.pkl')
結果をプロットしてみよう。
import numpy as np import matplotlib.pyplot as plt %matplotlib inline # plot learning curve plt.figure() plt.plot(range(num_epochs), loss_list, 'r-', label='train_loss') plt.plot(range(num_epochs), val_loss_list, 'b-', label='val_loss') plt.legend() plt.xlabel('epoch') plt.ylabel('loss') plt.grid() plt.figure() plt.plot(range(num_epochs), val_acc_list, 'g-', label='val_acc') plt.legend() plt.xlabel('epoch') plt.ylabel('acc') plt.grid()
今回はチュートリアルのシンプルなCNNなので63%くらい。あとでチューニングしてみよう!