クイックPyTorch入門


いままでKerasしか使ったことなかったが、他の人のコードを読んだり、修正したりするのにPyTorch需要が出てきたので、いまさらPyTorchを学びました。
細かいことは後から知っていけばよいので、とにかく超簡単な例で一連の流れを学びます。

今回使うnotebookはこちら
notebookだけ見てもわかるようにコメント書いてあります。

データ準備

手書き数字データのmnistを使っていきます。

import torch
import torchvision
import torchvision.transforms as transforms
# データの変換用
transform = transforms.Compose(
    [
        # numpy.ndarrayをテンソルに変換
        transforms.ToTensor(),
        # 平均0.5、標準偏差0.5へ。チャンネル数のタプルで渡す。
        transforms.Normalize(mean=(0.5, ), std=(0.5, ))
    ]
)
# 学習データ用意
trainset = torchvision.datasets.MNIST(
    root='./dataset', # 保存先
    train=True,
    download=True,
    transform=transform
)
# 学習用ローダー
trainloader = torch.utils.data.DataLoader(
    trainset,
    batch_size=128,
    shuffle=True,
    num_workers=2
)
  • transforms: データセットを前処理するためのツールです
  • torchvision.datasets.MNIST: pytorchがMNISTデータセットを用意してくれているのでこれを使います
  • DataLoader: MNISTに限らず、自分で用意したデータでもよく使うもの。指定したデータセットをバッチサイズごと取り出してくれる。

データの確認

dataiter = iter(trainloader)
images, labels = dataiter.next()


バッチサイズの分数字ラベルと、画像データが取り出されていることが確認できます。

モデル

あえて単純なモデルにしてます。

import torch.nn as nn
import torch.nn.functional as F

# ネットワークの定義
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 28x28次元 -> 256次元
        self.fc1 = nn.Linear(1*28*28, 256)
        # 256次元 -> 10次元(10クラスあるため)
        self.fc2 = nn.Linear(256, 10)

    def forward(self, x):
        # shapeを変形する(バッチサイズ x 次元数)
        x = x.view(-1, 1*28*28)
        # 線形変換後ReLuをかます
        x = F.relu(self.fc1(x))
        # 【注意】Kerasだと最後にsoftmaxをかますが、
        # pytorchだと損失関数を計算する nn.CrossEntropyLoss の中にsoftmaxの計算が含まれている
        # ただし、予測時に確率にしたい場合はアウトプットを nn.functional.softmax() で変換する必要がある
        x = self.fc2(x)
        return x
  • nn.Moduleを継承してモデルを定義します
  • __init__内では、モデルで使う層を定義しておきます
    • KerasだとSequentialにaddしていくだけだったが、PyTorchでは事前に層を定義してからforwardで使う
  • forwardでは、データ入力されたときのモデルの出力を書きます
    • この例では、1次元ベクトルにしたあとに、線形変換に活性化関数をかまして、最後に線形変換して返す
    • 【注意】のコメントはぜひ見ておいてください
net = Net()

import torch.optim as optim
# 目的関数
criterion = nn.CrossEntropyLoss()
# 最適化手法
optimizer = optim.Adam(net.parameters(), lr=0.001)
  • モデルのインスタンス作成
  • 目的関数の設定(今回はCrossEntropyLoss
  • 最適化手法を設定します(今回はAdam

学習

Kerasと違って自分で学習のループを書くことになります。

# Kerasやsklearnでいうfit
epochs=10
varbose=100
for epoch in tqdm(range(epochs)):
    # 損失(表示用)
    running_loss = 0.0
    for i, (inputs, labels) in enumerate(trainloader):
        # 勾配の初期化
        optimizer.zero_grad()
        # Netのforwardに基づいて出力が計算される
        outputs = net(inputs)
        # 損失の計算
        loss = criterion(outputs, labels)
        # ↑ここまでで計算グラフの構築が行われ、勾配に使う情報が設定される
        # 勾配を計算
        loss.backward()
        # 最適化法に基づいてパラメータ更新
        optimizer.step()
        # varbose回分の損失の和。item() で値を取り出している
        running_loss += loss.item()
        if i % varbose == varbose-1:
            print(f"epoch:{epoch + 1}, batch:{i + 1}, loss: {running_loss / varbose}")
            running_loss = 0.0
  • optimizer.zero_grad(): 勾配情報をクリアします
    • 前の勾配情報が残っているため、ゼロクリアする必要がある
  • outputs = net(inputs): モデルのforwardに基づいて出力が計算されます
  • loss = criterion(outputs, labels): 先程指定したCrossEntropyLossに基づいて損失計算されます
  • loss.backward(): 逆伝播で勾配を計算します
    • この段階ではまだパラメータは更新されてません
  • optimizer.step(): パラメータ更新されます

その他の記述は学習の進捗が見えるようにするもので、特に意味はありません。

epoch:1, batch:100, loss: 0.6820216017961502
epoch:1, batch:200, loss: 0.3693145060539246
epoch:1, batch:300, loss: 0.332705043554306
epoch:1, batch:400, loss: 0.29559941463172434
epoch:2, batch:100, loss: 0.23795114882290364
epoch:2, batch:200, loss: 0.20222241077572106
epoch:2, batch:300, loss: 0.1960952030867338
epoch:2, batch:400, loss: 0.1974168461561203
epoch:3, batch:100, loss: 0.15257078558206558
epoch:3, batch:200, loss: 0.1440433470159769
epoch:3, batch:300, loss: 0.142299246750772
epoch:3, batch:400, loss: 0.13810560397803784
epoch:4, batch:100, loss: 0.11472118373960256
epoch:4, batch:200, loss: 0.10906709022819996
epoch:4, batch:300, loss: 0.11881133031100034
epoch:4, batch:400, loss: 0.10873796993866564
epoch:5, batch:100, loss: 0.09303514676168562
epoch:5, batch:200, loss: 0.09735027667135
epoch:5, batch:300, loss: 0.09419337672181427
epoch:5, batch:400, loss: 0.08241723172366619

実行するとこのように表示されます。

予測

correct = 0
total = 0
# 予測時に勾配更新しないため、no_gradを指定
with torch.no_grad():
    for (images, labels) in testloader:
        # forwardした結果
        # これを nn.functional.softmax() すると確率が得られるが、ここでは使わない
        outputs = net(images)
        # torch.max() は (最大値, そのindex)を返す関数。dimは最大値を見る軸
        _, predicted = torch.max(outputs.data, dim=1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
print(f"Accuracy: {100 * float(correct/total)}")

Accuracy: 97.26

  • outputs = net(images): モデルのforwardに基づいて予測されます
    • 【注意】でも書いたとおり、nn.functional.softmax()を取らないと確率になってません
  • _, predicted = torch.max(outputs.data, dim=1): 予測値が最大となるindexを取ってきます(これが予測した数字)
  • total += labels.size(0): 予測したサンプル数の合計を計算してます
  • correct += (predicted == labels).sum().item(): 正解した個数を記録してます
    • item()はTensorから値を取ってくる関数です

GPUを使うには

コードの書き方だけのメモで、環境構築は別のサイトを参照してください。

# cudaが使えるか確認
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device

device(type='cpu')

  • torch.cuda.is_available(): GCPを使えるかどうか判定します
# .toメソッドでデバイスに移すようにする
# ネットワークに関して
net = Net().to(device)
# データに関して
images, labels = dataiter.next()
images, labels = images.to(device), labels.to(device)
  • .to(device)でネットワークとデータをデバイスに移すようにします