Welcome to Mashykom WebSite



Pytorch 入門

 PyTorch はディープラーニングを実装する際に用いられるディープラーニング用ライブラリのPython APIの一つです。もともとは、Torch7と呼ばれるLua言語で書かれたライブラリでした。PyTorchは、このTorch7とPreferred Networks社のChainerをベースに2017年2月に作られたPython用ライブラリです。Chainerは日本のPreferred Networks社が開発したライブラリですが、Pytorchに統合されました。Caffe2もPyTorchに併合されました。現在、PyTorch は Team PyTorch によって開発されています。

 PyTorchの利点はDefine by Run(動的計算グラフ)と呼ばれる特徴です。Define by Runは入力データのサイズや次元数に合わせてニューラルネットワークの形や計算方法を変更することができます。一方で、TensorFlowの特徴はDefine and Run(静的計算グラフ)と呼ばれます。Define and Runではニューラルネットワークの計算方法をはじめに決めてしまうため、入力データの次元がデータごとに異なる状況に対応しづらいという特徴があります 。Keras + Tensorflowは見やすく簡易で、非常に簡単にネットワークを作成できるので、人工知能の専門家以外の人たちにとって、使いやすい必須の道具となっています。しかし、アップグレードが後方互換性を持たないという欠点と、動作が遅いという問題点もあります。PyTorchは、Define by Runという特徴ゆえに、Object Detection用のライブラリの中では処理速度が最も最速です。

 PyTorchを利用する上で、tensorという概念の知識は必要不可欠です。tensorは、PyTorchの基本となるデータ構造になります。torch.Tensorと表示されるものです。多次元配列を形成できるデータ構造で、Pythonの拡張モジュールNumPy における ndarray型に似ています。以下で説明するように、Tensor型とndarray型の違いは、GPUでの演算を可能にすることです。

 2016年にFacebookの人工知能研究グループによる開発が公開された以降、徐々に注目を集め、現在ではPythonの機械学習ライブラリとして高い人気を誇ります。PyTorch は、Pythonの機械学習プロセスで頻繁に使われるPythonモジュールのNumPyと操作方法が非常に似ています。そのため、PythonエンジニアならPyTorchに短時間で慣れることができます。現在、AIを開発する専門家に必須のアイテムになりつつあります。

 PyTorchチュートリアル(日本語翻訳版)は小川雄太郎氏によるhttps://yutaroogawa.github.io/pytorch_tutorials_jp/にあります。PyTorch公式チュートリアル全体が詳細な日本語訳になっていて、Google Colab用ノートブックも完備しています。以下のページでの説明と同時に、このチュートリアルも参照しながら読むと理解が進むと思います。

 このページでは、MacOSを使用することを前提に説明します。LinuxへのPyTorchのインストールについては、Ubuntu 20.04LTSでのPython環境構築のページを参照ください。また、GPUを用いたPyTorchの利用はGoogel Colabで行うことにして、ここでは、GPUなしでPyTorchを使用することを前提にします。


Last updated: 2021.5.1(first uploaded 2021.2.20)


PyTorchのインストールとデータの表示


 最初に、anacondaをインストールすることを介して、必要なpython 環境が設定されていることを前提とします。仮想環境ではなく、root環境でインストールします。Python 3.5 以上 が要求されます。まず、PyTorchの公式サイトにアクセスして、「Get Started」のページに行きます。OSの選択をして、condaもしくはpipを選択し、python3を選び、そして、CPUを選択すると、run this commandの欄に以下のコマンドが表示されます。


(base) $ conda install pytorch torchvision torchaudio -c pytorch

このコマンドを入力します。pytorchのインストールが始まります。Anacondaのケースでは、PyTorchは、ディレクトリ anaconda3/lib/python3.8/site-packages/ の下に 'torch'及び'torchvision' という名前でインストールされます。

 Mac OS にPyenv をインストールして、Python環境を構築したのち、PyTorchをインストールすることについて簡単に説明します。


$ brew update
$ brew install pyenv

 これでPyenvがインストールされました。

 次に、bash_profile に環境変数や init コマンドを追加します。


$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

ターミナルを再起動して動作確認します。


$ pyenv --version

pyenv 1.2.27

Python をインストールします。 Python バージョン3.8.8をインストールします。結構時間がかかります。


$ pyenv install 3.8.8

----
python-build: use openssl@1.1 from homebrew
python-build: use readline from homebrew
Downloading Python-3.8.8.tar.xz...
-> https://www.python.org/ftp/python/3.8.8/Python-3.8.8.tar.xz
Installing Python-3.8.8...
python-build: use readline from homebrew
python-build: use zlib from xcode sdk
Installed Python-3.8.8 to /Users/koichi/.pyenv/versions/3.8.8

インストールした Python をグローバルで使うように設定して、システムPythonとして使用できます。


$ pyenv global 3.8.8

 Pythonのバージョンを確認します。


$ pyenv versions

----
 system
* 3.8.8 (set by /Users/koichi/.pyenv/version)

システムPythonが変更されたか、バージョンの確認をしましょう。


$ python --version

----
Python 3.8.8

 python環境が3.8.8バージョンに切り替わりました。

 以上で、Python環境の準備が完了したので、PyTorchをインストールしましょう。PyTorchの公式サイトにアクセスして、「Get Started」のページに行きます。OSの選択をして、pipを選択し、python3を選び、そして、CPUを選択すると、run this commandの欄にコマンドが表示されます。以下のコマンド群を入力します。


$ pyenv shell 3.8.8
$ python -m pip install torch torchvision torchaudio
$ python -m pip install numpy scipy pillow pydot matplotlib seaborn scikit-learn scikit-image keras pandas opencv-python

 Python 開発環境で必須のモジュール群(JupyterLab, notebook, spyder)のインストールを行います。


$ python -m pip install -U jupyterlab jupyter jupyter-console  notebook  spyder

 PyTorchを含めて、これらのモジュールは、.pyenv/versions/3.8.8/lib/python3.8/の下にインストールされます。

 以下では、Pytorchの公式サイトのTutorialsに沿って説明します。このTutorials にある例題はすべてGoogle Colab で実行できます。

 PyTorchでは、numpyのndarrayを拡張したTensorsという概念を使います。pythonを起動して、以下のコマンドを入力してください。


from __future__ import print_function
import torch
x=torch.Tensor(5,3)
print(x)

 以下のように表示されます。


tensor([[ 0.0000e+00, -2.5244e-29, -4.7749e+03],
        [-8.5920e+09,  6.2369e-30,  1.4013e-45],
        [ 6.2836e-30,  1.4013e-45,  6.2836e-30],
        [ 1.4013e-45,  6.2778e-30,  1.4013e-45],
        [ 6.2821e-30,  1.4013e-45,  3.8576e-32]])

 5x3行列のインスタンスを作成してますが、要素を指定してませんので内容は無意味です。明らかに、tensorはnumpyのndarrayの拡張です。次に、要素が空である5x3 行列をtorchで作成します。


x = torch.empty(5, 3)
print(x)

 以下のようになります。


tensor([[ 0.0000e+00, -2.5244e-29,  0.0000e+00],
        [-2.5244e-29,  5.6052e-45,  1.7180e+25],
        [ 5.7738e+16,  4.5912e-41,  0.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  1.0951e+21],
        [-5.7835e+03, -1.5849e+29, -5.785

 ランダムな値が入った行列を作成することもできます。


x = torch.rand(5, 3)
print(x)

 以下のように表示されます。


tensor([[ 0.2561,  0.4928,  0.4191],
        [ 0.0556,  0.4421,  0.0621],
        [ 0.8881,  0.8851,  0.2572],
        [ 0.0567,  0.2491,  0.2788],
        [ 0.3648,  0.2398,  0.8449]])

 加算をして見ましょう。


y=torch.rand(5,3)
print(x+y)

 以下のようになります。


tensor([[ 0.9846,  1.3548,  0.9620],
        [ 0.1496,  1.2262,  0.4178],
        [ 1.2757,  1.3802,  0.8955],
        [ 0.7388,  0.2780,  1.1868],
        [ 0.9870,  0.7609,  1.4777]])

 また、


print(torch.add(x, y))

 と入力しても、同じ結果が得られます。このように、NumPy like な操作ができます。

 テンソルの属性には、shape, datatype, device があります。以下のコマンドを入力します。


x = torch.rand(3,4)

print(f"Shape of tensor: {x.shape}")
print(f"Datatype of tensor: {x.dtype}")
print(f"Device tensor is stored on: {x.device}")

Shape of tensor: torch.Size([3, 4])
Datatype of tensor: torch.float32
Device tensor is stored on: cpu

 と表示されます。shapeはテンソルの次元、dtypeはデータの種類、deviceはCPUかGPUのどちらかです。デフォルトでは、テンソルはCPU形式で作成されますが、GPU形式で作成するためには、属性 .to()を使用します。以下のように打ちます。


# We move our tensor to the GPU if available
if torch.cuda.is_available():
  tensor = tensor.to('cuda')

 テンソルの配列リストに対する処理の方法は、NumPyに似た操作となります。


x = torch.ones(4, 4)
print('First row: ',x[0])
print('First column: ', x[:, 0])
print('Last column:', x[..., -1])
x[:,1] = 0
print(x)


First row:  tensor([1., 1., 1., 1.])
First column:  tensor([1., 1., 1., 1.])
Last column: tensor([1., 1., 1., 1.])
tensor([[1., 0., 1., 1.],
        [1., 0., 1., 1.],
        [1., 0., 1., 1.],
        [1., 0., 1., 1.]])

 x[i]は第(i+1)行目の配列を意味します。x[:, j]は第(j+1)列目の配列になります。特殊な操作は、最後の列をスライスする操作で、x[..., -1]となります。x[:, k]=0は第(k+1)列目の値を0とします。

 torch.cat で、テンソルの配列を接続できます。以下の例を見てください。


t1 = torch.cat([x, x, x], dim=1)
print(t1)


tensor([[1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
        [1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
        [1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.],
        [1., 0., 1., 1., 1., 0., 1., 1., 1., 0., 1., 1.]])


データの読み込みとネットワーク・モデルの定義


 CNN を用いた画像識別や物体検出のために用意されている PyTorch のモジュールは以下の通りです。

初めに、Pytorchにおけるデータの読み込み方法について説明します。公式Tutorialはこちらです。 torch.utils.data.Dataset に幾つかの既存のデータ、MNIST、FashonMNIST、Cifar10などのデータセットが用意されています。FashonMNISTを読み込んでみましょう。以下のように入力します。root="data" はディレクトリdataに読み込んだデータを保存するための設定です。train=True は学習用データの指定、train=False は検証用のデータを指定しています。transform=ToTensor() はデータをテンソル形式で保存することを設定します。

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda, Compose
import matplotlib.pyplot as plt

# Download training data from open datasets.
training_data = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor(),
)

# Download test data from open datasets.
test_data = datasets.FashionMNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor(),
)

 次に、学習を実行するためのデータセットを加工します。データの個数を64個ごとにまとめます。batch_size=64がこれを設定します。



batch_size = 64

# Create data loaders.
train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=batch_size, shuffle=True)

for X, y in test_dataloader:
    print("Shape of X [N, C, H, W]: ", X.shape)
    print("Shape of y: ", y.shape, y.dtype)
    break


Shape of X [N, C, H, W]:  torch.Size([64, 1, 28, 28])
Shape of y:  torch.Size([64]) torch.int64

 画像は28x28のサイズで、白黒なのでチャネル数は1です。ラベルはbatch_sizeの数 64 で、整数値になっています。

 ネットワーク・モデルは nn.Module を用いて定義します。画像サイズが28x28ピクセルなので、nn.flattenを用いて784ピクセルに一次元化します。ミニバッチの0次元目は、サンプル番号を示す次元で、この次元はnn.Flattenを通しても変化しません(1次元目以降がFlattenされます)x.view(-1, 28*28) というコードを使用する方が多いです。


# Get cpu or gpu device for training.
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Using {} device".format(device))

# Define model
class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28*28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
            nn.ReLU()
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

model = NeuralNetwork().to(device)
print(model)

 ネットワーク・モデルは、各ネットワーク層の定義、def __init__(self)、と順伝搬の定義、def forward(self, x)、から構成されます。NeuralNetwork().to(device)で、NeuralNetworkクラスのインスタンスを作成し、変数device上に移動させます。

 以下のようなネットワークの構造が出力されます。

Using cuda device
NeuralNetwork(
  (flatten): Flatten(start_dim=1, end_dim=-1)
  (linear_relu_stack): Sequential(
    (0): Linear(in_features=784, out_features=512, bias=True)
    (1): ReLU()
    (2): Linear(in_features=512, out_features=512, bias=True)
    (3): ReLU()
    (4): Linear(in_features=512, out_features=10, bias=True)
    (5): ReLU()
  )
)


ネットワーク・モデルの学習


 ネットワーク・モデルの重みを学習させます。以下の例はGoogel Colabで実行した結果です。まず、損失関数 CrossEntropyLoss と最適化関数 SGD を利用します。学習用データセットとして、train_dataloader を使用しますが、対応する引数をdataloaderとしておきます。


loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)

def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)

        # Compute prediction error
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")

 検証用のモジュールも定義します。model.eval()とします。


def test(dataloader, model):
    size = len(dataloader.dataset)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

epochs = 5
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model)
print("Done!")

 Google Colabでこのコードを実行すると、以下のような結果が得られます。


中略

Epoch 5
-------------------------------
loss: 1.479790  [    0/60000]
loss: 1.446915  [ 6400/60000]
loss: 1.287518  [12800/60000]
loss: 1.348354  [19200/60000]
loss: 1.193832  [25600/60000]
loss: 1.252168  [32000/60000]
loss: 1.248695  [38400/60000]
loss: 1.186837  [44800/60000]
loss: 1.219524  [51200/60000]
loss: 1.126422  [57600/60000]
Test Error: 
 Accuracy: 62.7%, Avg loss: 0.018069 

Done!

 ここで学習したモデルの重みを用いた画像分類をしてみましょう。


model = NeuralNetwork()
model.load_state_dict(torch.load("model.pth"))

classes = [
    "T-shirt/top",
    "Trouser",
    "Pullover",
    "Dress",
    "Coat",
    "Sandal",
    "Shirt",
    "Sneaker",
    "Bag",
    "Ankle boot",
]

model.eval()
x, y = test_data[0][0], test_data[0][1]
with torch.no_grad():
    pred = model(x)
    predicted, actual = classes[pred[0].argmax(0)], classes[y]
    print(f'Predicted: "{predicted}", Actual: "{actual}"')

Predicted: "Ankle boot", Actual: "Ankle boot"

 画像分類の結果はこうなります。


Tensor の特性と Autograd の使用法


 以上で、Pytorch を用いた機械学習の大枠は理解できたと思います。ここで、Pytorch の根幹となる tensor という概念についてもう少し詳しく理解しましょう。tensorという概念は、NumPyの配列概念と類似していますが、本質的に異なるのはGPUでの並列計算を実行できるところです。また、tensorとして定義された変数値をprint関数で表示できないところです。print関数で変数の値を表示するためには、NumPy 配列に戻すことが必要です。

Learning PyTorch with Examplesに沿って例を見てみましょう。numpyを用いて、\(\sin\)関数を3次関数 \[ y^e = a + b x + c x^2 + d x^3 \] で近似するプログラムを取り上げます。区間\([-\pi, \pi]\)を2000分割したときの各\(x_i\)の値に対応する\( y_i = \sin(x_i)\)の近似値 を\( y^e_i\)とするとき、損失関数を \[ \phi = \sum _{i=1}^{2000} (y^e_i - y_i)^2 \] とします。勾配降下法で\( a, b, c , d\)の最適な推計値を求めます。勾配降下法については、こちらのページを参照ください。このとき、係数\( a, b, c , d\)に関する偏微分係数は \[ \frac{\partial \phi}{\partial a} = 2 \sum _{i=1}^{2000} (y^e_i - y_i) \\ \frac{\partial \phi}{\partial b} = 2 \sum _{i=1}^{2000} (y^e_i - y_i) x_i\\ \frac{\partial \phi}{\partial c} = 2 \sum _{i=1}^{2000} (y^e_i - y_i) x_i^2\\ \frac{\partial \phi}{\partial d} = 2 \sum _{i=1}^{2000} (y^e_i - y_i) x_i^3\\ \] となります。

 以下のスクリプトを見てください。勾配法による損失最小化問題を解く、通常のpython スクリプトそのものです。


import numpy as np
import math

# Create random input and output data
x = np.linspace(-math.pi, math.pi, 2000)
y = np.sin(x)

# Randomly initialize weights
a = np.random.randn()
b = np.random.randn()
c = np.random.randn()
d = np.random.randn()

learning_rate = 1e-6
for t in range(2000):
    # Forward pass: compute predicted y
    # y = a + b x + c x^2 + d x^3
    y_pred = a + b * x + c * x ** 2 + d * x ** 3

    # Compute and print loss
    loss = np.square(y_pred - y).sum()
    if t % 100 == 99:
        print(t, loss)

    # Backprop to compute gradients of a, b, c, d with respect to loss
    grad_y_pred = 2.0 * (y_pred - y)
    grad_a = grad_y_pred.sum()
    grad_b = (grad_y_pred * x).sum()
    grad_c = (grad_y_pred * x ** 2).sum()
    grad_d = (grad_y_pred * x ** 3).sum()

    # Update weights
    a -= learning_rate * grad_a
    b -= learning_rate * grad_b
    c -= learning_rate * grad_c
    d -= learning_rate * grad_d

print(f'Result: y = {a} + {b} x + {c} x^2 + {d} x^3')

 これを実行すると、近似式として、 \[ y^e = 0.019979441728599796 + 0.8599884284159166 x -0.0034467861575288683 x^2 -0.09379232977529517 x^3 \] が得られます。これと同じことを pytorch を用いて書いたスクリプトは以下の通りです。


import torch
import math


dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0") # Uncomment this to run on GPU

# Create random input and output data
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# Randomly initialize weights
a = torch.randn((), device=device, dtype=dtype)
b = torch.randn((), device=device, dtype=dtype)
c = torch.randn((), device=device, dtype=dtype)
d = torch.randn((), device=device, dtype=dtype)

learning_rate = 1e-6
for t in range(2000):
    # Forward pass: compute predicted y
    y_pred = a + b * x + c * x ** 2 + d * x ** 3

    # Compute and print loss
    loss = (y_pred - y).pow(2).sum().item()
    if t % 100 == 99:
        print(t, loss)

    # Backprop to compute gradients of a, b, c, d with respect to loss
    grad_y_pred = 2.0 * (y_pred - y)
    grad_a = grad_y_pred.sum()
    grad_b = (grad_y_pred * x).sum()
    grad_c = (grad_y_pred * x ** 2).sum()
    grad_d = (grad_y_pred * x ** 3).sum()

    # Update weights using gradient descent
    a -= learning_rate * grad_a
    b -= learning_rate * grad_b
    c -= learning_rate * grad_c
    d -= learning_rate * grad_d


print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')

 numpy配列がtorch.tensorに置き換わっただけです。ただ、

dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0") # Uncomment this to run on GPU

という新たな宣言文が登場します。device = torch.device("cuda:0") はGPUを用いて計算することを宣言します。a.item()やb.item() に見られるように、属性.item()の追加が必要な理由は、torch.tensor形式では、数値を表示できないので、numpyの浮動小数点形式に変換するためです。

 偏微分の計算をautograd関数を用いて書き直すと、こうなります。

import torch
import math

dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0")  # Uncomment this to run on GPU

# Create Tensors to hold input and outputs.
# By default, requires_grad=False, which indicates that we do not need to
# compute gradients with respect to these Tensors during the backward pass.
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# Create random Tensors for weights. For a third order polynomial, we need
# 4 weights: y = a + b x + c x^2 + d x^3
# Setting requires_grad=True indicates that we want to compute gradients with
# respect to these Tensors during the backward pass.
a = torch.randn((), device=device, dtype=dtype, requires_grad=True)
b = torch.randn((), device=device, dtype=dtype, requires_grad=True)
c = torch.randn((), device=device, dtype=dtype, requires_grad=True)
d = torch.randn((), device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(2000):
    # Forward pass: compute predicted y using operations on Tensors.
    y_pred = a + b * x + c * x ** 2 + d * x ** 3

    # Compute and print loss using operations on Tensors.
    # Now loss is a Tensor of shape (1,)
    # loss.item() gets the scalar value held in the loss.
    loss = (y_pred - y).pow(2).sum()
    if t % 100 == 99:
        print(t, loss.item())

    # Use autograd to compute the backward pass. This call will compute the
    # gradient of loss with respect to all Tensors with requires_grad=True.
    # After this call a.grad, b.grad. c.grad and d.grad will be Tensors holding
    # the gradient of the loss with respect to a, b, c, d respectively.
    loss.backward()

    # Manually update weights using gradient descent. Wrap in torch.no_grad()
    # because weights have requires_grad=True, but we don't need to track this
    # in autograd.
    with torch.no_grad():
        a -= learning_rate * a.grad
        b -= learning_rate * b.grad
        c -= learning_rate * c.grad
        d -= learning_rate * d.grad

        # Manually zero the gradients after updating weights
        a.grad = None
        b.grad = None
        c.grad = None
        d.grad = None

print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')

 \( a, b , c, d \)の宣言文にrequires_grad=True を追加することにより、微分をする変数であることを宣言します。loss.backward() を宣言することにより、autogradを用いて損失関数の偏微分計算を行うことを指示します。.backward と表記されているのは、逆伝播法を用いて偏微分計算を行うからです。この結果、偏微分 a.grad, b.grad, c.grad, d.grad を読み込むことができます。係数に関する微分の計算式を明示的に表現する必要がなくなりました。

 このコードを実行すると、 \[ y = 0.0218451377004385 + 0.886846125125885 x - 0.0037686508148908615 x^2 - 0.09761260449886322 x^3 \] という結果となります。numpy配列を用いた計算結果とは大差ありません。


torch.nn モジュール の使用法


 torch.nn モジュールを用いて、上記の関数近似のスクリプトを改変する事例を取り上げます。torch.nn モジュールはニューラル・ネットワーク層を定義する上で便利です。以下のコードは、torch.nn.sequential を活用して、ニューラル・ネットワークを定義して、autograd関数を利用します。p = torch.tensor([1, 2, 3])は3次関数の各次数に対応したテンソルを作成します。xはshape(2000,)なので、x.unsqueeze(-1) は−1軸に次元数を一つだけ加えて、shape (2000, 1)を作成します。xxはxの各次数に対応したテンソル、shape (2000, 3)です。これを線形ニューラル・ネットワークの入力信号とします。

 torch.nn.Linear(3, 1)は入力信号が3次元で、出力が1次元となる線形結合層を作成します。バイアス信号を加えて、4次元の入力信号となります。ネットワークの重みパラメータ数は入力信号に対応した4種類(a, b, c, d)ということです。model.parameters()でアクセスできます。Flatten layer は線形層の出力を一次元テンソルに変換します。損失関数としてMean Squared Error (MSE)を採用します。


import torch
import math


# Create Tensors to hold input and outputs.
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

p = torch.tensor([1, 2, 3])
xx = x.unsqueeze(-1).pow(p)

model = torch.nn.Sequential(
    torch.nn.Linear(3, 1),
    torch.nn.Flatten(0, 1)
)

learning_rate = 1e-6
for t in range(2000):

    y_pred = model(xx)

    # Compute and print loss. We pass Tensors containing the predicted and true
    # values of y, and the loss function returns a Tensor containing the
    # loss.
    loss = loss_fn(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())

    # Zero the gradients before running the backward pass.
    model.zero_grad()

    loss.backward()

    # Update the weights using gradient descent. Each parameter is a Tensor, so
    # we can access its gradients like we did before.
    with torch.no_grad():
        for param in model.parameters():
            param -= learning_rate * param.grad

# You can access the first layer of `model` like accessing the first item of a list
linear_layer = model[0]

# For linear layer, its parameters are stored as `weight` and `bias`.
print(f'Result: y = {linear_layer.bias.item()} + {linear_layer.weight[:, 0].item()} x + {linear_layer.weight[:, 1].item()} x^2 + {linear_layer.weight[:, 2].item()} x^3')

 このコードを実行すると、 \[ y = 0.014212202280759811 + 0.8439575433731079 x - 0.00245184195227921 x^2 - 0.09151206910610199 x^3 \] が得られます。結果は少しづつ異なります。


Datasets と DataLoadersの使用


 PyTorchは、ライブラリ内に所蔵するデータセットだけではなく、ユーザーの独自のデータセットを容易に使用できるためのモジュールとして、torch.utils.data.DataLoader と torch.utils.data.Dataset を利用できます。「データの読み込みとネットワーク・モデルの定義」での説明と重複しますが、繰り返します。

 torchvision.datasetsでダウンロードすることができるデータセットの種類などについては、この公式ページを参照ください。使用例は

imagenet_data = torchvision.datasets.ImageNet('path/to/imagenet_root/')
data_loader = torch.utils.data.DataLoader(imagenet_data,
                                          batch_size=4,
                                          shuffle=True,
                                          num_workers=args.nThreads)

となっています。少なくとも27種類のデータセットが用意されています。

ここでは、torchvision.datasetsにあるFashion-MNIST datasetを読み込む例を取り上げます。パラメータの指定は以下のとおり。

import torch
from torch.utils.data import Dataset
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda
import matplotlib.pyplot as plt


training_data = datasets.FashionMNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()
)

test_data = datasets.FashionMNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)

 このコードを実行すると、Fashon-MNISTデータがディレクトリdata に読み込まれます。Fashion-MNIST は 60,000 個の訓練サンプルと 10,000 個のテストサンプルから成る画像のデータセットです。各サンプルは 28×28 グレイスケール画像で、ラベルは 10 クラスラベルから成ります。


labels_map = {
    0: "T-Shirt",
    1: "Trouser",
    2: "Pullover",
    3: "Dress",
    4: "Coat",
    5: "Sandal",
    6: "Shirt",
    7: "Sneaker",
    8: "Bag",
    9: "Ankle Boot",
}
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 3
for i in range(1, cols * rows + 1):
    sample_idx = torch.randint(len(training_data), size=(1,)).item()
    img, label = training_data[sample_idx]
    figure.add_subplot(rows, cols, i)
    plt.title(labels_map[label])
    plt.axis("off")
    plt.imshow(img.squeeze(), cmap="gray")
plt.show()

 画像のサンプルが表示されます

 DataLoaders を活用して学習用データセットを準備します。指定されたバッチ数でデータセットを作成します。過学習を避けるために、shuffle=True で、各エポックごとにデータを組み替えます。torch.multiprocessingのnum_workers オプションで、並列計算 Python’s multiprocessing を指定して、処理をスピードアップさせることもできます。


from torch.utils.data import DataLoader

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

 データセットをデータローダにロードしましたので必要に応じてデータセットを通して iterate できます。下の各 iter は train_features と train_labels のバッチを返します。 (batch_size=64 の特徴とラベルをそれぞれ含みます)。


# Display image and label.
train_features, train_labels = next(iter(train_dataloader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")
img = train_features[0].squeeze()
label = train_labels[0]
plt.imshow(img, cmap="gray")
plt.show()
print(f"Label: {label}")

Feature batch shape: torch.Size([64, 1, 28, 28])
Labels batch shape: torch.Size([64])

 Pytorch用の画像データセットを作成する方法として、torch.utils.data.Datasetを継承して、カスタムデータセットと呼ばれるクラスを作成する方法があります。カスタムデータセットクラスは 3 つの関数を実装する必要があります。 __init__, __len__ と __getitem__ です。__init__ で読み込むファイルパスの一覧を準備します。__getitem__ でパスからデータを取り出して前処理して、画像とラベルのペアを返します。__len__ には総データ数を入れます。torch.utils.data.Dataset には、 __getitem__ 及び __len__ メソッドはすでに実装されています。この関数の実装を見ましょう。 FashionMNIST 画像はディレクトリ img_dir に保存され、それらのラベルは CSV ファイル annotations_file に保存されているとします。この場合、以下のようなコードとなります。


import os
import torch
from torch.utils.data import Dataset

import pandas as pd
from torchvision.io import read_image

class CustomDataset(Dataset):
    def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
        self.img_labels = pd.read_csv(annotations_file)
        self.img_dir = img_dir
        self.transform = transform
        self.target_transform = target_transform

    def __len__(self):
        return len(self.img_labels)

    def __getitem__(self, idx):
        img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
        image = read_image(img_path)
        label = self.img_labels.iloc[idx, 1]
        if self.transform:
            image = self.transform(image)
        if self.target_transform:
            label = self.target_transform(label)
        sample = {"image": image, "label": label}
        return sample

 作成されたカスタムデータセットはDataLoaderで読み込みます。以下のように読み込みます。


from torch.utils.data import DataLoader
 
custom_dataset = CustomDataset(annotations_file = 'data/csv_file', img_dir = 'data')

train_dataloader = DataLoader(custom_dataset, batch_size=64, shuffle=True)

 カスタムデータセットを用いたコードの例はこのTutorialsにあります。



ニューラル・ネットワーク層の定義


上の項でもネットワークの定義について簡単に触れましたが、ここでも詳しく説明します。PyTorch では、torch.nn内のモジュールやクラスを使用してネットワークの構造を設定します。nn.Module はネットワークの各層を定義するとき使用されます。メソッド forward(input)はネットワークの出力を返すために利用されます。

 ここでは、torch.nn を用いて、MNISTデータセット(画像サイズが28x28、グレイスケール)を読み込む想定で、ニューラル・ネットワークを作成します。以下の順序でコードを組みます。

 以下にコードを説明します。最初に、必要なすべてのライブラリをインポートします


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

 次に、ニューラル・ネットのクラス Net を作成します。__init__ function で nn.Module を継承します。

 最初の 2D convolutional layerは 1 input channel (グレイスケールなので)を入力して、カーネルサイズ 3 で畳み込みを行い、32 convolutional featuresを出力します。第2の2D convolutional layerは、カーネルサイズ 3 で、32 input layersから、64 convolutional featuresを出力します。その後、dropout層を通します。最初の全結合層は 128 種類の出力をして、第2の全結合層はその出力を 10 種類の出力信号に変換します。MNISTデータのクラス数は10(0から9)なので。


class Net(nn.Module):
    def __init__(self):
      super(Net, self).__init__()

      self.conv1 = nn.Conv2d(1, 32, 3, 1)
      self.conv2 = nn.Conv2d(32, 64, 3, 1)

      self.dropout1 = nn.Dropout2d(0.25)
      self.dropout2 = nn.Dropout2d(0.5)

      self.fc1 = nn.Linear(9216, 128)
      self.fc2 = nn.Linear(128, 10)

my_nn = Net()
print(my_nn)

Net(
  (conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1))
  (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1))
  (dropout1): Dropout2d(p=0.25, inplace=False)
  (dropout2): Dropout2d(p=0.5, inplace=False)
  (fc1): Linear(in_features=9216, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=10, bias=True)
)

 上記で定義されたネットワーク・モデルにデータをどのように通すかを設定する必要があります。これは、forward function で定義されます。入力データを 引数 x としています。

class Net(nn.Module):
    def __init__(self):
      super(Net, self).__init__()
      self.conv1 = nn.Conv2d(1, 32, 3, 1)
      self.conv2 = nn.Conv2d(32, 64, 3, 1)
      self.dropout1 = nn.Dropout2d(0.25)
      self.dropout2 = nn.Dropout2d(0.5)
      self.fc1 = nn.Linear(9216, 128)
      self.fc2 = nn.Linear(128, 10)

    # x represents our data
    def forward(self, x):
      x = self.conv1(x)
      x = F.relu(x)
      x = self.conv2(x)
      x = F.relu(x)

      x = F.max_pool2d(x, 2)
      x = self.dropout1(x)
      x = torch.flatten(x, 1)
      x = self.fc1(x)
      x = F.relu(x)
      x = self.dropout2(x)
      x = self.fc2(x)

      # Apply softmax to x
      output = F.log_softmax(x, dim=1)
      return output

 forward 関数によって、データを各ネットワーク層をどの順番で流すのかが定義されています。F.relu は rectified-linear activation 関数です。

 この後、得られた出力を受け取って、どのように処理するかを決めます。ランダムなデータを流すことをコードで書くと以下のようになります。


# Equates to one random 28x28 image
random_data = torch.rand((1, 1, 28, 28))

my_nn = Net()
result = my_nn(random_data)
print (result)

 forward関数の引数 x にrandom_dataを入力して、ネットワーク・モデルを稼働させます。出力が output に渡されます。resultの中身は、10次元となっています。

tensor([[-2.4070, -2.2313, -2.2228, -2.3443, -2.3335, -2.3090, -2.2862, -2.3289,
         -2.2819, -2.2942]], grad_fn=)

 torch.nn.Module で定義されたモデルのパラメータ(重みとバイアス)はmodel’s parametersとして保存されます。model.parameters() を用いてアクセスできます。各層に対応したパラメータのマップは state_dict というPython 辞書オブジェクトになっています。


# Print model's state_dict
print("Model's state_dict:")
for param_tensor in my_nn.state_dict():
    print(param_tensor, "\t", my_nn.state_dict()[param_tensor].size())

print()
Model's state_dict:
conv1.weight 	 torch.Size([32, 1, 3, 3])
conv1.bias 	 torch.Size([32])
conv2.weight 	 torch.Size([64, 32, 3, 3])
conv2.bias 	 torch.Size([64])
fc1.weight 	 torch.Size([128, 9216])
fc1.bias 	 torch.Size([128])
fc2.weight 	 torch.Size([10, 128])
fc2.bias 	 torch.Size([10])

 畳み込みのconv1層とconv2層の重みとバイアス、全結合層fc1とfc2の重みとバイアスのサイズが表示されています。当然ながら、プーリング層、dropout層と活性化層にはパラメータはありません。


ページの先頭に戻る

Pytorch で画像分類と転移学習のページに行く

トップページに戻る