PyTorchでディープラーニング

Jun 8, 2017   #PyTorch  #Python 

PyTorchとは

PyTorchはFacebookの開発するPython上でのテンソル計算・自動微分ライブラリで,特にディープラーニングに必要な機能が充実しています.2017年の初頭に公開され,瞬く間にTensorflow, Keras, Caffeに続くディープラーニングライブラリとして人気を博すこととなりました.

PyTorchはPreferred NetworkのディープラーニングライブラリChainerから影響を受けており,GoogleのTensorFlowやUniversité de MontréalのTheanoとは異なり,実行時に動的にグラフを構築するため,柔軟なコードを書くことができます.

PyTorchは,製品にも用いられているTensorFlowとは異なり,研究向けであることが明言されています.新機能の変更は多いものの,疎テンソルにいち早く対応するなど,最新の研究動向を追うにはよいのではないでしょうか.また,適当なレベルで書くことができて,素のTensorflowのように低レベルでもなく,Kerasの様に高度に抽象化されているわけでもなく,ラッパーによって書き方が多様でサンプルを見てもよく分からない,ということはないので,学びやすいと思います.

チュートリアル

とりあえず動かせるようになるチュートリアルです.

インストール

GPU環境は勿論,CPU環境でも動かすことができます.Linux,macOSの場合は 公式, Windowsの場合は Anaconda Cloudからインストールできます.

GPUを利用する場合,環境の設定が面倒なことが多いですがPyTorchでは特に設定せずにGPU対応版をダウンロードするとGPUが使えるようになるようです

Tensor

PyTorchの基本はテンソルを操作するTensorです.テンソルというと難しく聞こえますが,この場合は多次元配列と同義で,物理学のテンソルのような共変・反変を意識する必要はありません.慣例に従ってテンソル,と言う語を用います.

PyTorchにおけるTensorは端的に言えば「GPU上でも動くnumpy.ndarrayのようなもの」ですが,違いも多いので注意が必要です.例えば

import numpy as np
import torch  # PyTorch
>>> np_tensor = np.zeros([1, 2, 3])
array([[[ 0.,  0.,  0.],
        [ 0.,  0.,  0.]]])

>>> np_tensor.shape
(1, 2, 3)

>>> torch_tensor = torch.zeros(1, 2, 3)
(0 ,.,.) =
  0  0  0
  0  0  0
[torch.FloatTensor of size 1x2x3]

>>> torch_tensor.size()
torch.Size([1, 2, 3])

などです.なお,numpyの配列は

>>> torch.from_numpy(np_tensor)
(0 ,.,.) =
  0  0  0
  0  0  0
[torch.DoubleTensor of size 1x2x3]

によってTensorに変換されます.上記のtorch.**TensorはCPU上で計算を行いますが,GPUを用いる場合にはtorch_tensor.cuda()によってtorch.cuda.**Tensorに変換します.反対に,GPUからCPUに移す場合はtorch_gpu_tensor.cpu()です.

ほかのライブラリとの違いとして,画像のテンソルが$\text{ミニバッチ数}\times\text{チャンネル数}\times\text{高さ}\times\text{幅}$であることが挙げられます.

Variable

VariableTensorと今までの計算の履歴を保持しており,計算グラフの葉(leaf)の勾配を得ることができます.

TensorVariable(torch_tensor)によってVariableとすることができます.逆にVariable内のTensorvar.dataによって取り出すことができます.

from torch.autograd import Variable
>>> a = Variable(torch.Tensor([3, 2]))
>>> a.requires_grad = True
>>> b = a * a * a
>>> c = torch.sum(b)
>>> c.backward()
>>> a.grad
Variable containing:
 27
 12
[torch.FloatTensor of size 2]

つまり$c=\sum_i b_i=\sum_i a_i^3$に対して,$\frac{\partial c}{\partial a_i}=3a_i^2$なので$3\times(a_0^2, a_1^2)=(27, 12)$となります.

実際のニューラルネットワークでは下の図のように,VariableConv2dなどのレイヤーに葉として接続しています.誤差逆伝播を行うことで葉のテンソルを更新していきます.

計算グラフの一部

MNISTの分類

ここまで扱った範囲だけで簡単なネットワークをつくることができます1.手書きの数字認識を行ってみましょう.

手書き数字認識の課題にはMNISTというデータセットがよく用いられます.

weight $W\in\mathcal{R}^{10\times(28\times 28)}$ とbias $b\in\mathcal{R}^{10}$に対してMNISTの画像の入力$x\in\mathcal{R}^{(28\times 28)}$を与え,その$\mathrm{log softmax}$をとった$y\in\mathcal{R}^{10}$を出力とします.ここで$\displaystyle \mathrm{softmax}(z)=\frac{e^{z_i}}{\sum_je^{z_j}}$です.

\[y=\ln\text{softmax}(Wx+b)\]

損失函数lossにはnegative log likelihood(NLL)を用い,これを最小化します.$t$を目標のラベルだとすると,

\[-y_t\]

です.これが最小になるのは$y_t=0$つまり,$\text{softmax}(Wx+b)$の第$t$要素が1,ほかが0となるときです.以上が順伝播のフェーズです.

weight, biasの更新は

\[w \leftarrow w - \mathit{lr}\nabla_w\text{loss}\]

です.誤差を元に重みを更新していく過程が逆伝播です.

# simplified code!
weight = Variable(torch.randn(28*28, 10), requires_grad=True)
bias = Variable(torch.randn(10), requires_grad=True)
lr = 0.001

for epoch in range(2):
    for (input, target) in train_loader:
       input, target = Variable(input), Variable(target)
       input = input.view(-1, 28*28)
       # weight, biasの勾配を0にする
       weight.grad.data.zero_()
       bias.grad.data.zero_()

       # 順伝播
       output = F.log_softmax(input.mm(weight).add(bias))
       loss = F.nll_loss(output, target)

       # 逆伝播
       loss.backward()

       # weight, biasの更新
       weight.data -= lr * weight.grad.data
       bias.data -=  lr * bias.grad.data

correct =  0
for (input, target) in test_loader:
    input, target = Variable(input), Variable(target)
    input = input.view(-1, 28*28)
    output = F.log_softmax(input.mm(weight).add(bias))
    pred = output.data.max(1)[1]
    correct += pred.eq(target.data).sum()

2エポックでも87%ほどの精度が出ました.

コードを見れば大体分かるかと思いますが,以下の点に注意してください.

  • input = input.view(-1, 28*28)では $28\times 28$ のMNISTの画像を1次元にして,ベクトルとして扱っています.viewを行うためにはメモリ上において連続である必要があるので,torch.contiguous()によって連続にすることが必要な場合があります.

  • Ftorch.nn.functionalのことで,ニューラルネットワークに必要な函数類があります.

  • targetはonehotではなくてラベルで与えます.

詳細はこちらをご覧下さい.

nn

上述のMNISTの分類では重みを自分で定義し,手動で更新する必要がありましたが,複雑なモデルをつくっていくのは大変です.

PyTorchには上記が抽象化されたnnモジュールが用意されています.nnを使うと上記のコードは

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

class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.dense = nn.Linear(28*28, 10)

    def forward(self, x):
        x = self.dense(x)
        return F.log_softmax(x)

model = SimpleNet()
optimizer = torch.optim.SGD(model.parameters(), lr=lr)

for (input, target) in train_loader:
    data, target = Variable(data), Variable(target)
    # weight, biasの勾配を0にする
    optimizer.zero_grad()

    # 順伝播
    output = model(data)
    loss = F.nll_loss(output, target)

    # 逆伝播
    loss.backward()

    # weight, biasの更新
    optimizer.step()
...

のようになります.ここでは先ほどと違ってnn.Moduletorch.optim.***を使っています.

SimpleNet__init__メソッドではモデルのレイヤーやブロックを定義します.forwardメソッドに順伝播時のデータの流れを記述していき,model(input)によって出力を得ます.nnには各種レイヤーが用意されていて,今回は全結合層,つまり$Wx+b$のnn.Linearを用いています.

torch.optimにはSGDを初めとするoptimizerが用意されています.optimizerにモデルのパラメータを渡して,効率的に重みを更新していきます.今回は最も単純なoptim.SGDを用いています.

さらに複雑なモデルを書くには

上に示した例は1層でしたが,もちろん更に複雑なネットワークを構築することができます.ここでは例として畳み込み層2,全結合層2の畳み込みニューラルネットワーク(CNN)を考えます.

Conv2dmax_pool2dの挙動については こちらが分かりやすいです.

class Net1(nn.Module):
    def __init__(self):
        super(Net1, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1,
                               out_channels=10,
                               kernel_size=5,
                               stride=1)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.dense1 = nn.Linear(in_features=320,
                                out_features=50)
        self.dense2 = nn.Linear(50, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = F.max_pool2d(x, kernel_size=2)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.max_pool2d(x, 2)
        x = F.relu(x)
        x = x.view(-1, 320)
        x = self.dense1(x)
        x = F.relu(x)
        x = self.dense2(x)
        return F.log_softmax(x)

以下のように書くこともできます.

class Net2(nn.Module):
    def __init__(self):
        super(Net2, self).__init__()
        self.head = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=10,
                      kernel_size=5, stride=1),
            nn.MaxPool2d(kernel_size=2),
            nn.ReLU(),
            nn.Conv2d(10, 20, kernel_size=5),
            nn.MaxPool2d(kernel_size=2),
            nn.ReLU())
        self.tail = nn.Sequential(
            nn.Linear(320, 50),
            nn.ReLU(),
            nn.Linear(50, 10))

    def forward(self, x):
        x = self.head(x)
        x = x.view(-1, 320)
        x = self.tail(x)
        return F.log_softmax(x)

両者は同一のネットワークを表していますが,

>>> Net1()
Net1 (
  (conv1): Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))
  (dense1): Linear (320 -> 50)
  (dense2): Linear (50 -> 10)
)

>>> Net2()
Net2 (
  (head): Sequential (
    (0): Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))
    (1): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
    (2): ReLU ()
    (3): Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))
    (4): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
    (5): ReLU ()
  )
  (tail): Sequential (
    (0): Linear (320 -> 50)
    (1): ReLU ()
    (2): Linear (50 -> 10)
  )
)

>>> Net2().head
Sequential (
  (0): Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))
  (1): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (2): ReLU ()
  (3): Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))
  (4): MaxPool2d (size=(2, 2), stride=(2, 2), dilation=(1, 1))
  (5): ReLU ()
)

と違いがあります.前者はforwardメソッドの自由度が高く,後者はブロックの一部を再利用するのに向いています.層を積んでいき,重みを再利用することの多いCNNでは後者を用いた方が便利かもしれません

リンク集

PyTorchの基本は以上で説明できたと思います.更に知りたい方は以下をご覧下さい.

サポートが充実しているのも特徴です.


  1. Raschka氏のdiscussionを参考にしています. [return]