【PyTorchチュートリアル⑥】What is torch.nn really?

# はじめに
[前回](https://qiita.com/sudominoru/items/c027f1fe9347f398c858)に引き続き、PyTorch [公式チュートリアル](https://pytorch.org/tutorials/) の第6弾です。
今回は [What is torch.nn really?](https://pytorch.org/tutorials/beginner/nn_tutorial.html) を進めます。

# What is torch.nn really?

このチュートリアルでは、torch.nn、torch.optim、Dataset、DataLoader を説明します。 （torch.nn、torch.optimは前回でも説明しましたが、いろいろな人がチュートリアルを記述しているため、重複する部分もあります。）

使用するデータセットはMNISTデータセットです。
MNISTデータセットは0から9までの手書き数字画像のデータセットです。
理解を深めるため、最初に上記のパッケージを利用せずにモデルを構築します。 
次に、torch.nn、torch.optim、Dataset、DataLoader の順で、一つずつコードを置き換えながら進めていきます。

# 1. MNIST data setup

まずはMNISTデータセット（手書き数字画像データセット）をダウンロードします。

```Python
from pathlib import Path
import requests

DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"

PATH.mkdir(parents=True, exist_ok=True)

URL = "http://deeplearning.net/data/mnist/"
FILENAME = "mnist.pkl.gz"

if not (PATH / FILENAME).exists():
        content = requests.get(URL + FILENAME).content
        (PATH / FILENAME).open("wb").write(content)
```

このデータセットはnumpy配列です。 
pickle 形式で保存されています。


```Python
import pickle
import gzip

with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")
```

一つのデータ(x_train[0])は28×28サイズの画像ですが、784列の1行として保持されています。 
pyplot.imshow で閲覧するには、28×28に変換する必要があります。

```Python
from matplotlib import pyplot
import numpy as np

pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)
```

```Python:out
(50000, 784)
```

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/303fb5da-1339-5c35-e356-764dbd769102.png)

以降では PyTorch の Tensor を利用します。
numpy配列から Tensor に変換します。


```Python
import torch

x_train, y_train, x_valid, y_valid = map(
    torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape
x_train, x_train.shape, y_train.min(), y_train.max()
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())
```


```Python:out
tensor([[0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        ...,
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.]]) tensor([5, 0, 4,  ..., 8, 4, 8])
torch.Size([50000, 784])
tensor(0) tensor(9)
```

学習データの件数は5万件（×784）、教師データは 0 から 9 までの数字であることが分かります。

# 2. Neural net from scratch (no torch.nn)

最初に torch.nn を利用せず Tensor のみでニューラルネットワークを作成します。
作成するモデルは、単純な線形モデル
$ y = w \times x + b$
です。


PyTorch の ランダムメソッド randn で重み $w$ を初期化します。randn は標準化（平均 0、標準偏差 1）されたランダム値です。
初期化するときの勾配を含めたくないため、初期化後に requires_grad_() を行い、requires_grad=True にします。 
重みの初期化は「Xavier initialization」を利用しています。
（とありますが計算式が少し違うような気がします）
bias はゼロで初期化します。

```Python
import math

weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
```

活性化関数も必要ですので、log_softmax 関数を作成します。
PyTorch には損失関数や活性化関数が多数用意されていますが、このように自分で関数を作成することもできます。

```Python
def log_softmax(x):
    return x - x.exp().sum(-1).log().unsqueeze(-1)

def model(xb):
    return log_softmax(xb @ weights + bias)
```

@は内積演算を表します。
バッチサイズ（今回は64個の画像）単位でこの関数が呼び出されます。

```Python
bs = 64  # batch size

xb = x_train[0:bs]  # ミニバッチ
preds = model(xb)  # モデルで予想する
preds[0], preds.shape
print(preds[0], preds.shape)
```

```Python:out
tensor([-2.8486, -2.2823, -2.2740, -2.7800, -2.1906, -1.3280, -2.4680, -2.2958,
        -2.8856, -2.8650], grad_fn=<SelectBackward>) torch.Size([64, 10])
```

予測値 preds を出力すると Tensor に勾配関数（grad_fn）が含まれていることが確認できます。
後でこの勾配関数を利用して逆伝播を計算します。
損失関数として、教師データと予測値の負の対数尤度を実装します。
負の対数尤度は、一般的に交差エントロピー誤差関数と呼ばれます。



```Python
def nll(input, target):
    return -input[range(target.shape[0]), target].mean()

loss_func = nll
```

予測値と教師データで損失を計算し、学習後のパラメータを確認します。

```Python
yb = y_train[0:bs]
print(loss_func(preds, yb))
```

```Python:out
tensor(2.4101, grad_fn=<NegBackward>)
```

モデルの精度を計算する評価関数も実装します。
out には、手書き数字 0 から 9 までの確率が配列で保持されていますので、argmax の最大値の値が一番確率が高い手書き数字ということになります。
その値と教師データの一致した平均をとることで正解率を計算します。


```Python
def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()
```

```Python
print(accuracy(preds, yb))
```

```Python:out
tensor(0.0781)
```

これで学習する準備ができました。
次を繰り返し、学習を行います。

- ミニバッチ単位で学習データを取得します。 
- モデルを使って、学習データから予測します。
- 損失を計算します。
- loss.backward() でモデルの勾配（重みとバイアス）を更新します。

重みとバイアスを更新した後に grad.zero_() で勾配を初期化しています。
これは loss.backward() で勾配を計算するときに、既に保存されているものに追加されるためです。
※set_trace() のコメントを外すことで、各ステップで変数の値を確認できます。

```Python
from IPython.core.debugger import set_trace

lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        #set_trace()
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        with torch.no_grad():
            weights -= weights.grad * lr
            bias -= bias.grad * lr
            weights.grad.zero_()
            bias.grad.zero_()
```

学習後に精度が向上していることが確認できます。

```Python
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
```

```Python:out
tensor(0.0822, grad_fn=<NegBackward>) tensor(1.)
```

学習前は正解率が 7% でしたが、学習後は 100% になっています。
※ 学習データに xb が含まれているため学習後の正解率が高くなっています。 検証データ（x_valid）で試すと正解率は 9% ⇒ 95% くらいでした。

以上で、ゼロから単純なニューラルネットワークが構築できました。
今回の隠れ層がないソフトマックス関数を利用したネットワークはロジスティック回帰と呼ばれます。


# 3. Using torch.nn.functional

ここから PyTorch の nn パッケージを利用してコードをリファクタリングしていきます。
最初のステップでは、活性化関数と損失関数を置き換えていきましょう。
torch.nn.functional には、log_softmax関数と負の対数尤度を組み合わせた F.cross_entropy があります。
損失関数を F.cross_entropy に置き換えます。
F.cross_entropy には log_softmax関数が含まれていますので、活性化関数として定義した def log_softmax(x) も削除できます。

```Python
import torch.nn.functional as F

loss_func = F.cross_entropy

def model(xb):
    return xb @ weights + bias
```

model で呼び出していた log_softmaxも不要になります。(cross_entropyに含まれます）
損失と精度が以前と同じであることを確認しましょう。

```Python
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
```

```Python:out
tensor(0.0822, grad_fn=<NllLossBackward>) tensor(1.)
```

# 4. Refactor using nn.Module

次に、nn.Module と nn.Parameter を利用してリファクタリングしていきます。
nn.Module は Pytorch のニューラルネットワークの基底クラスです。
nn.Module はサブクラスとして実装します。 
作成したサブクラスに重みとバイアスのパラメータを定義します。また、forward メソッドに入力から出力まで順につなげる処理を記述します。
nn.Module には モデルのパラメータを返却する parameters() もあらかじめ用意されています。

```Python
from torch import nn

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784))
        self.bias = nn.Parameter(torch.zeros(10))

    def forward(self, xb):
        return xb @ self.weights + self.bias
```

関数を使用する代わりにオブジェクトを使用しているので、最初にモデルをインスタンス化する必要があります。

```Python
model = Mnist_Logistic()
```

これでリファクタリング前と同じように学習できます。
nn.Module オブジェクトは関数のように呼び出して利用することができます。

```Python
print(loss_func(model(xb), yb))
```

```Python:out
tensor(2.3918, grad_fn=<NllLossBackward>)
```

ここまでの実装では、以下のように重みとバイアスの更新をそれぞれ計算し、勾配を手動でゼロにしていました。

```Python
  with torch.no_grad():
      weights -= weights.grad * lr
      bias -= bias.grad * lr
      weights.grad.zero_()
      bias.grad.zero_()
```

重みとバイアスの更新は、nn.Module で定義された parameters() と zero_grad() で置き換えて簡潔にできます。

```Python
  # 説明用コードのため実行できません（実行時エラーになります）
  with torch.no_grad():
      for p in model.parameters(): p -= p.grad * lr
      model.zero_grad()
```

学習のループを fit 関数として定義し、呼び出しできるようにしておきます。

```Python
def fit():
    for epoch in range(epochs):
        for i in range((n - 1) // bs + 1):
            start_i = i * bs
            end_i = start_i + bs
            xb = x_train[start_i:end_i]
            yb = y_train[start_i:end_i]
            pred = model(xb)
            loss = loss_func(pred, yb)

            loss.backward()
            with torch.no_grad():
                for p in model.parameters():
                    p -= p.grad * lr
                model.zero_grad()

fit()
```

損失が減っていることを再確認しましょう。

```Python
print(loss_func(model(xb), yb))
```

```Python:out
tensor(0.0796, grad_fn=<NllLossBackward>)
```

# 5. Refactor using nn.Linear

最初に weights と bias を自分で定義し、線形関数 $ w \times x + b$ も実装していましたが、nn.Linear （線形レイヤー）に置き換えてみましょう。

```Python
class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)

    def forward(self, xb):

        return self.lin(xb)
```

先ほどと同じようにモデルをインスタンス化し、損失を計算します。

```Python
model = Mnist_Logistic()
print(loss_func(model(xb), yb))
```

```Python:out
tensor(2.3661, grad_fn=<NllLossBackward>)
```

関数化した fit を呼び出して学習します。

```Python
fit()
print(loss_func(model(xb), yb))
```

```Python:out
tensor(0.0813, grad_fn=<NllLossBackward>)
```

loss値が 2.3661 から 0.0813 になり、学習できていることが確認できます。

# 6. Refactor using optim

次に最適化アルゴリズムをリファクタリングします。
Pytorch の torch.optim パッケージにはさまざまな最適化アルゴリズムがあります。
また、torch.optim の各クラスは手動でパラメータを更新する代わりに、 step メソッドを実行することでパラメータを更新してくれます。

```Python
  with torch.no_grad():
      for p in model.parameters(): p -= p.grad * lr
      model.zero_grad()

```

上記のコードを以下に書き換えることができます。
※ opt.zero_grad() で次のミニバッチの計算前に勾配を 0 にする必要があります。

```Python
  # 説明用コードのため実行できません
  opt.step()
  opt.zero_grad()
```

```Python
from torch import optim
```

モデルとオプティマイザの生成を関数化するとコードが簡潔になります。

```Python
def get_model():
    model = Mnist_Logistic()
    return model, optim.SGD(model.parameters(), lr=lr)

model, opt = get_model()
print(loss_func(model(xb), yb))

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))
```

```Python:out
tensor(2.3423, grad_fn=<NllLossBackward>)
tensor(0.0819, grad_fn=<NllLossBackward>)
```

# 7. Refactor using Dataset

PyTorchには抽象 Dataset クラスがあります。 
Dataset を利用することでトレーニング中に訓練用データ(x_train)と教師データ(y_train)が扱いやすくなります。
Dataset は、要素数を返却する ``__len__``関数と、インデックスを指定して要素を返却する ``__getitem__``関数を実装する必要があります。
TensorDatasetは、データセットを Tensor にラップします。

```Python
from torch.utils.data import TensorDataset
```
TensorDataset 作成時に x_train と y_train を指定して作成します。

```Python
train_ds = TensorDataset(x_train, y_train)
```

先ほどまでは、訓練用データ(x_train)と教師データ(y_train)を個別に反復処理していました。

```Python
    xb = x_train[start_i:end_i]
    yb = y_train[start_i:end_i]
```

TensorDataset を利用すると、まとめて処理できます。

```Python
    xb,yb = train_ds[i*bs : i*bs+bs]
```

```Python
model, opt = get_model()

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        xb, yb = train_ds[i * bs: i * bs + bs]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))
```

```Python:out
tensor(0.0803, grad_fn=<NllLossBackward>)
```

# 8. Refactor using DataLoader

DataLoaderを使用すると、Dataset を利用したループ処理を簡潔にできます。
Dataset を元に DataLoader を作成します。

```Python
from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)
```

最初のコードでは、バッチサイズごとにスタート位置を指定し、スライスしていました。

```Python
for i in range((n-1)//bs + 1):
    xb,yb = train_ds[i*bs : i*bs+bs]
    pred = model(xb)
```

DataLoader を利用すると（xb、yb）が DataLoader から自動的に順次読み込まれるため、ループがより簡潔になります。

```Python
for xb,yb in train_dl:
    pred = model(xb)
```

```Python
model, opt = get_model()

for epoch in range(epochs):
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))
```

```Python:out
tensor(0.0802, grad_fn=<NllLossBackward>)
```

ここまでで、nn.Module、nn.Parameter、Dataset、DataLoader を利用しました。
コードを簡潔に理解しやすく記述できました。
次に効果的なモデルを作成するために必要な基本機能を追加してみます。

# 9. Add validation

ここまででは、学習データのみで学習を進めてきましたが、実際の学習では検証データ(validation data)を利用して、過学習していないか、学習が進んでいるかをチェックします。
以下で 検証用のデータセットを設定します。

+ 検証データセットはシャッフルしません。（検証データ全件が検証対象のため、シャッフルする意味はありません。）
+ バッチサイズを2倍にします。検証データでは勾配を計算する必要がないためメモリ消費が少なく、効率化のためバッチサイズを大きくします。

```Python
train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)

valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs * 2)
```

各エポックの終わりに検証データ(validation data)を利用して損失を計算します。
学習の前に model.train() で学習モードにし、検証の前に model.eval() で評価モードにします。これは nn.Dropout などをトレーニング中のみに有効にさせるためです。

```Python
model, opt = get_model()

for epoch in range(epochs):
    model.train()
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

    model.eval()
    with torch.no_grad():
        valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl)

    print(epoch, valid_loss / len(valid_dl))
```

```Python:out
0 tensor(0.3679)
1 tensor(0.2997)
```

# 10. Create fit() and get_data()

次に、学習と検証をどちらも行える関数 loss_batch を作成します。
loss_batch にオプティマイザを渡すと 逆伝播を計算し、パラメータを更新します。
検証時はオプティマイザを渡さないことで逆伝播が計算されません。

```Python
def loss_batch(model, loss_func, xb, yb, opt=None):
    loss = loss_func(model(xb), yb)

    if opt is not None:
        loss.backward()
        opt.step()
        opt.zero_grad()

    return loss.item(), len(xb)
```

fit 関数を定義します。
fit 関数は、各エポックで学習と検証を繰り返し、損失を表示します。

```Python
import numpy as np

def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_dl:
            loss_batch(model, loss_func, xb, yb, opt)

        model.eval()
        with torch.no_grad():
            losses, nums = zip(
                *[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
            )
        val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)

        print(epoch, val_loss)
```

get_data は 学習データと検証データの DataLoader を返却します。

```Python
def get_data(train_ds, valid_ds, bs):
    return (
        DataLoader(train_ds, batch_size=bs, shuffle=True),
        DataLoader(valid_ds, batch_size=bs * 2),
    )
```

これで、DataLoader を取得して学習を実行する処理を3行のコードで記述できるようになりました。

```Python
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
```

```Python:out
0 0.45953697173595426
1 0.3061695278286934
```

3行のコードをリファクタリングすることで、さまざまなモデルを構築することができます。
畳み込みニューラルネットワーク（CNN）を構築できるか試してみましょう！

# 11. Switch to CNN

ここからは、3つの畳み込み層でニューラルネットワークを構築します。ここまでに作成した関数はモデルの制約は特にありませんので、変更を加えることなくCNNをに切り替えることができます。

Pytorch で用意された Conv2d クラスを畳み込み層として使用します。3つの畳み込み層でCNNを定義します。各畳み込み層の活性化関数は ReLU です。
最後に、平均プーリング層を追加します。 （ビューはnumpyの変形のPyTorchバージョンです）

```Python
class Mnist_CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
        self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
        self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)

    def forward(self, xb):
        xb = xb.view(-1, 1, 28, 28)
        xb = F.relu(self.conv1(xb))
        xb = F.relu(self.conv2(xb))
        xb = F.relu(self.conv3(xb))
        xb = F.avg_pool2d(xb, 4)
        return xb.view(-1, xb.size(1))

lr = 0.1
```

Momentum は、確率的勾配降下法のバリエーションで、直前の更新値も考慮され、一般により高速なトレーニングにつながります。

```Python
model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
```

```Python:out
0 0.7808464194297791
1 0.6988550303936004
```

# 12. nn.Sequential

torch.nnには、コードを単純にするために使用できるもう1つの便利なクラス、Sequential があります。 Sequentialオブジェクトは、その中に含まれる各モジュールを順次実行します。ネットワークを簡単に記述できるのが特徴です。

Sequential を利用するためには、カスタムレイヤーが必要な場合があります。PyTorch にはネットワーク（層）の次元を変換するレイヤーがないため、独自でビューレイヤーを作成する必要があります。
以下の Lambda は Sequential で扱う入出力層を定義します。

```Python
class Lambda(nn.Module):
    def __init__(self, func):
        super().__init__()
        self.func = func

    def forward(self, x):
        return self.func(x)


def preprocess(x):
    return x.view(-1, 1, 28, 28)
```

Sequential は、以下のように簡単にネットワークを記述することができます。

```Python
model = nn.Sequential(
    Lambda(preprocess),
    nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.AvgPool2d(4),
    Lambda(lambda x: x.view(x.size(0), -1)),
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
```

```Python:out
0 0.4288556560516357
1 0.2115058801174164
```

# 13. Wrapping DataLoader

作成した CNN はかなり簡潔ですが、以下の制約があるため MNISTデータ（手書き数字画像）でのみ機能します。

 - 入力は28 * 28のデータである必要があります。
 - 最終的なグリッドサイズは4 * 4であると想定しています（カーネルサイズ 4 の2次元平均プーリングを利用しているため）

これらの2つの仮定を取り除いて、モデルが任意の2Dシングルチャネル画像（単色画像）で機能するようにします。
まず、最初のLambdaレイヤーを削除し、データの前処理をDataLoaderに移動します。

```Python
def preprocess(x, y):
    return x.view(-1, 1, 28, 28), y


class WrappedDataLoader:
    def __init__(self, dl, func):
        self.dl = dl
        self.func = func

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

    def __iter__(self):
        batches = iter(self.dl)
        for b in batches:
            yield (self.func(*b))

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
```

次に、nn.AvgPool2dをnn.AdaptiveAvgPool2dに置き換えます。
これにより、入力テンソルではなく、必要な出力テンソルのサイズを定義できます。
その結果、平均プーリング層は任意のサイズの入力で動作します。

```Python
model = nn.Sequential(
    nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.AdaptiveAvgPool2d(1),
    Lambda(lambda x: x.view(x.size(0), -1)),
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
```

試してみましょう。

```Python
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
```

```Python:out
0 0.3351769802570343
1 0.2583931807518005
```

# 14. Using your GPU

CUDA対応のGPUが利用できる場合（ほとんどのクラウドプロバイダーから1時間あたり約0.50ドルでGPUが利用できます）、学習を高速化できます。
まず、GPUがPytorchで動作していることを確認します。
※ Google Colaboratory でGPUを利用するには、「ランタイム」 > 「ランタイムのタイプを変更」から「ハードウェア アクセラレータ」を「GPU」にします。


```Python
print(torch.cuda.is_available())
```

```Python:out
True
```

次に、デバイスオブジェクトを作成します。
デバイスオブジェクトには GPUが利用できる場合は「cuda」、利用できない場合は「cpu」がセットされます。

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

バッチをGPUに移動する前処理を書き加えます。

```Python
def preprocess(x, y):
    return x.view(-1, 1, 28, 28).to(dev), y.to(dev)

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
```

最後に、モデルをGPUに移動します。

```Python
model.to(dev)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
```

処理速度が速くなったことが確認できます。

```Python
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
```

```Python:out
0 0.1938392831325531
1 0.18594802458286286
```

Google Colaboratory で確認すると、CPUだと15秒くらいかかっていた上記の処理が5秒くらいで完了しました。

# 15. Closing thoughts（まとめ）

このチュートリアルで、モデルに依存しないデータ処理と学習処理が作成できました。
データ拡張（Data Augmentation）、ハイパーパラメータ調整、モニタリングト​​レーニング、転移学習など、追加したいことがたくさんあります。
これらの機能はfastaiライブラリで利用できます。fastaiライブラリは、このチュートリアルで示したものと同じ設計アプローチを使用して開発されており、機械学習をさらに習得する人には、よいステップになるでしょう。

このチュートリアルでは、torch.nn、torch.optim、Dataset、DataLoader を利用しました。
これまでに見てきたことをまとめてみましょう。

 - **torch.nn**
   + ``Module``：関数のように呼びだせるオブジェクトで、ニューラルネットワーク層や重みを保持します。パラーメータも保持していて、勾配を計算したり、重みを更新する反復処理を実行します
   + ``Parameter``：勾配の計算中に更新が必要な重みがあることをモジュールに通知する tensor のラッパー。 require_grad 属性が設定されたテンソルのみが更新されます
   + ``functional``：活性化関数、損失関数や畳み込み層や線形層などの層を含むモジュール
 - **torch.optim**：SGD（確率的勾配降下法）などのオプティマイザーが含まれ、勾配計算中にパラメーターの重みを更新します
 - **Dataset**：TensorDataset などの クラスを含む、__len__および__getitem__を持つ抽象インターフェース
 - **DataLoader**：任意の Dataset を受け取り、データのバッチを返すイテレータを作成します

# 16. 最後に

以上が「What is torch.nn really?」です。
前回と似たような内容でしたが、Pytorch とニューラルネットワークの理解を深めることができました。
[次回](https://qiita.com/sudominoru/items/975b9d222e5fcdf7bf4c)は「[Visualizing Models, Data, and Training with TensorBoard](https://qiita.com/sudominoru/items/975b9d222e5fcdf7bf4c)」を進めたいと思います。

#履歴
2020/10/10 初版公開
2020/10/15 次回のリンク追加