LoginSignup
1
2

More than 3 years have passed since last update.

Keras好きがPyTorchに挑戦してみた

Last updated at Posted at 2020-09-11

はじめに

ずっとKerasでばかりDeep Learningのプログラムを書いてきたが、いよいよPyTorchで書かなくてはいけない状況になってきたので、お勉強がてら、MNISTを例にとりあえず動くものを作ってみました。

【修正履歴】
・モデル内でFlatten()とSoftmax()を使うようにしました
・対応して、ロス関数をCrossEntropyLoss()にしました

まずはチュートリアル

とりあえず最初はチュートリアルだろうということで、PyTorchのチュートリアルを最初から始めてみました。

しかし、これが実に分かりづらい。
「Autograd」が感覚的につかめず、モチベーションが10%以下まで落ち込みました。

今では、最初からではなく、「What is torch.nn really?」から初めて、わからないところを調べたほうがよかったと思っています。

バックアップ

そんな予感がしていたので、チュートリアルを始めるとともに、書籍の購入を行いました。
色々調べて、以下の2冊を選びました。

こちらは、写経しながら、これからじっくりと読んでいこうと思います。

そしてMNIST

ということで、やっぱり最初はMNISTだろ?ということで、PyTorch公式のコードを参考に、必要な部分だけ抜き出して、自分がわかりやすいように改造してみました。

環境

とりあえずWindows 10+Anaconda+CUDAでやってます。

import

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR
from torchvision import datasets, transforms

from torchsummary import summary

基本的にはサンプルのままですが、確認用に「torchsummary」を増やしました。
残念ながらcondaではインストールできないので、pipでインストールしました。
あと、torchvisionもバージョンが合わず、GUIからインストールできませんでした。(condaでインストールしました)

パラメータ

seed = 1

epochs = 14
batch_size = 64
log_interval = 100

lr = 1.0
gamma = 0.7

サンプルでは引数になってましたが、必要な変数だけ抜き出して固定値にしました。
値はデフォルトになっていた数値です。(log_intervalだけ変えました)

モデル

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=2)
        self.dropout1 = nn.Dropout2d(0.25)
        self.dropout2 = nn.Dropout2d(0.5)
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)
        self.softmax = nn.Softmax(dim=1)

    def forward(self, x):
        x = self.conv1(x)
        x = self.relu(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.maxpool(x)
        x = self.flatten(x)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.dropout1(x)
        x = self.fc2(x)
        x = self.relu(x)
        x = self.dropout2(x)
        output = self.softmax(x)
        return output

なるべく__init__()で内部変数にしました。
最後はSoftmax()にしました。

なお、モデルをSequential APIで書くことも考えたのですが、今後これをベースにいろいろいじりそうな気がするので、Functional APIのままにしました。

学習

def train(model, loss_fn, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = loss_fn(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()/len(data)))

基本的にはサンプルのままです。
ロス関数は呼び出し側から指定するようにしました。

なお、呼び出し側でepochを回し、ここでバッチを回すという仕組みは、わかりやすくて気に入っています。

評価

def test(model, loss_fn, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += loss_fn(output, target)  # sum up batch loss
            pred = output.argmax(dim=1, keepdim=True)  # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)

    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

こちらもロス関数は呼び出し側で指定することにしました。

PyTorchには「predict」という概念がないようなので、あとで学習済みモデルを使って推論するときは、このあたりのコードが参考になると思っています。
(まだ推論のコードは書いていません)

view_as()とか、便利すぎてビビッています。

メイン処理

torch.manual_seed(seed)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

まず、ランダムの種を埋め込み、実行環境(CUDA)を指定します。

transform=transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.0,), (1.0,))
    ])
kwargs = {'batch_size': batch_size,
          'num_workers': 1,
          'pin_memory': True,
          'shuffle': True}

dataset1 = datasets.MNIST('../data', train=True, download=True, transform=transform)
dataset2 = datasets.MNIST('../data', train=False, transform=transform)
train_loader = torch.utils.data.DataLoader(dataset1,**kwargs)
test_loader = torch.utils.data.DataLoader(dataset2, **kwargs)

次に、MNISTデータセットを読み込みます。
読み込んだ時に、平均0、標準偏差1にしてます。(ここはサンプルから変えました)

で、データローダーを用意します。こいつはなかなか便利です。
こいつでデータ拡張してくれないかなと期待しています。(まだ調べていない)

なお、目的変数をone-hot形式にしないでいいのもうれしいです。

model = Net().to(device)
summary(model, (1,28,28))

loss_fn = nn.CrossEntropyLoss()
optimizer = optim.Adadelta(model.parameters(), lr=lr)
scheduler = StepLR(optimizer, step_size=1, gamma=gamma)

モデルを作成し、その内容を表示しています。
torchsummary、優れものです。keras好きの人は必須です。これ見ないと安心できないです。
ちなみに、今回はこんな感じに表示されます。

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 32, 26, 26]             320
              ReLU-2           [-1, 32, 26, 26]               0
            Conv2d-3           [-1, 64, 24, 24]          18,496
              ReLU-4           [-1, 64, 24, 24]               0
         MaxPool2d-5           [-1, 64, 12, 12]               0
           Flatten-6                 [-1, 9216]               0
            Linear-7                  [-1, 128]       1,179,776
              ReLU-8                  [-1, 128]               0
         Dropout2d-9                  [-1, 128]               0
           Linear-10                   [-1, 10]           1,290
             ReLU-11                   [-1, 10]               0
        Dropout2d-12                   [-1, 10]               0
          Softmax-13                   [-1, 10]               0
================================================================
Total params: 1,199,882
Trainable params: 1,199,882
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 1.04
Params size (MB): 4.58
Estimated Total Size (MB): 5.62
----------------------------------------------------------------

見慣れた表示でうれしいです。
そういえば、配列の順番が違っているのは注意が必要です。ずっと(バッチ, 高さ, 幅, チャンネル)だったので(バッチ, チャンネル, 高さ, 幅)に違和感を感じます。

忘れずにロス関数も用意しています。モデルのSoftmax()に対応してCrossEntropyLoss()を使用しています。

あと、最適化アルゴリズムと学習率の調整方法を決めます。簡単に学習率を変える仕組みを組み込めるのはいいですね。

for epoch in range(1, epochs + 1):
    train(model, loss_fn, device, train_loader, optimizer, epoch)
    test(model, loss_fn, device, test_loader)
    scheduler.step()

epochで学習を回していきます。
 学習→評価→学習率調整
と、非常にわかりやすいです。

学習済みモデルの保存/読み込み

torch.save(model.state_dict(), "mnist_cnn.pt")
model.load_state_dict(torch.load("mnist_cnn.pt"))

おまけで、学習済みモデルの保存方法と読み込み方法を書いておきました。
バイナリエディタで中身を見てみたのですが、さっぱりでした。

まとめ

まだなじめないですが、以下の特徴があると感じました。

  • 流れがつかみやすい
  • 細かく記述ができる
  • コード量が増える

もっと勉強します!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2