PyTorch 1.5 : PyTorch の学習 : torch.nn とは実際には何でしょう?

PyTorch 1.5 : PyTorch の学習 : torch.nn とは実際には何でしょう? (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 05/06/2020 (1.5.0)

* 本ページは、PyTorch 1.5 Tutorials の以下のページを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

無料セミナー開催中 クラスキャット主催 人工知能 & ビジネス Web セミナー

人工知能とビジネスをテーマにウェビナー (WEB セミナー) を定期的に開催しています。スケジュールは弊社 公式 Web サイト でご確認頂けます。
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
  • Windows PC のブラウザからご参加が可能です。スマートデバイスもご利用可能です。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
E-Mail:sales-info@classcat.com ; WebSite: https://www.classcat.com/
Facebook: https://www.facebook.com/ClassCatJP/

 

PyTorch の学習 : torch.nn とは実際には何でしょう?

ニューラルネットワークを作成して訓練することを手助けするために PyTorch はエレガントに設計されたモジュールとクラス torch.nn , torch.optim , Dataset と DataLoader を提供します。それらのパワーをフルに活用して貴方の課題にそれらをカスタマイズするためには、実際にはそれらが何をしているか正確に理解する必要があります。この理解を深めるため、最初にこれらのモデルからのどのような機能も利用することなく MNIST データセット上の基本的なニューラルネットを訓練します ; 初期的に最も基本的な PyTorch tensor 機能を利用するだけです。それから、増加的に torch.nn, torch.optim, Dataset, or DataLoader から一つの機能を一度に追加して、各ピースが何をしているか、そしてコードをより簡潔に、あるいは柔軟にするためにそれがどのように動作するかを正確に示します。

 

MNIST データセットアップ

古典的な MNIST データセットを使用します、これは (0 と 9 の間の) 手書き数字の黒と白の画像から成ります。

パスを処理するために pathlib を利用し (Python 3 標準ライブラリの一部)、そして requests を利用してデータセットをダウンロードします。それらを使用するときにモジュールをインポートするだけですので、各ポイントで何が使用されているかを正確に見ることができます。

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")

各画像は 28 x 28 で、長さ 784 (=28×28) の平坦化された行としてストアされています。一つを見ましょう ; 最初にそれを 2d に reshape する必要があります。

from matplotlib import pyplot
import numpy as np

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

(50000, 784)

PyTorch は numpy 配列よりも、torch.tensor を使用しますので、データを変換する必要があります。

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())
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)

 

スクラッチからニューラルネット (no torch.nn)

最初に PyTorch tensor 演算以外の何も使用せずにモデルを作成しましょう。

PyTorch はランダム or ゼロで満たされた tensor を作成するためのメソッドを提供します、単純な線形モデルのための重みとバイアスを作成するためにこれを使用します。これらは一つの非常に特別な追加を持つ単なる通常の tensor です : PyTorch にそれらが勾配を必要とすることを伝えます。これは PyTorch が tensor 上で行なわれる総ての演算を記録することをもたらします、その結果それは逆伝播の間に勾配を自動的に計算することができます。

重みについては、初期化の 後に requires_grad を設定します、何故ならばそのステップを勾配に含めることを望まないためです。(PyTorch で trailling _ は演算が in-place に遂行されることを表すことに注意してください。)

Note: ここでは重みを (1/sqrt(n) で乗算することにより) Xavier 初期化 で初期化しています。

import math

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

勾配を自動的に計算する PyTorch の能力のおかげで、任意の標準 Python 関数 (or callable オブジェクト) をモデルとして利用できます!そこで単純な線形モデルを作成するために平易な行列演算とブロードキャストされた加算を単に書きましょう。活性化関数もまた必要ですので、log_softmax を書いてそれを使用します。覚えておいてください: PyTorch は多くの事前に書かれた損失関数、活性化関数等を提供しますが、貴方は平易な python を使用して貴方自身のものを容易に書くことができます。PyTorch は貴方の関数のために高速な GPU やベクトル化された CPU コードを作成しさえします。

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

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

上で、@ はドット積演算を表します。私達の関数をデータの一つのバッチ上で呼び出します (この場合、64 画像)。これは一つの forward パスです。予測はこの段階ではランダムよりもどのようにも良くはないことに注意してください、何故ならばランダム重みで始めるからです。

bs = 64  # batch size

xb = x_train[0:bs]  # a mini-batch from x
preds = model(xb)  # predictions
preds[0], preds.shape
print(preds[0], preds.shape)
tensor([-2.1217, -2.3065, -2.7014, -2.3352, -2.2122, -2.4711, -2.0434, -2.1985,
        -2.3491, -2.4453], grad_fn=) torch.Size([64, 10])

見るように、preds tensor は tensor 値だけでなく、勾配関数もまた含みます。これを後で逆伝播を行なうために使用します。

損失関数として使用するために負対数尤度を実装しましょう (再度、標準 Python を使用するだけです) :

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

loss_func = nll

ランダムモデルで損失を確認しましょう、すると後で backprop パスの後で改良するか見ることができます。

yb = y_train[0:bs]
print(loss_func(preds, yb))
tensor(2.3228, grad_fn=<NegBackward>)

モデルの精度を計算するための関数も実装しましょう。各予測に対して、最も大きい値を持つインデックスがターゲット値に一致するのであれば、予測は正しかったです。

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

ランダムモデルの精度を確認すると、損失が改善するにつれて精度が改善するかを見ることができます。

print(accuracy(preds, yb))
tensor(0.0625)

今は訓練ループを実行できます。各反復について、以下を行ないます :

  • データの (サイズ bs の) ミニバッチを選択する
  • 予測するためにモデルを利用する
  • 損失を計算する
  • loss.backward() がモデルの勾配を更新します、この場合、重みとバイアスです。

今は重みとバイアスを更新するためにこれらの勾配を使用します。これを torch.no_grad() コンテキストマネージャ内で行ないます、何故ならばこれらのアクションが勾配の次の計算の間に記録されることを望まないからです。PyTorch の Autograd がどのように演算を記録するかについては ここ で更に読むことができます。

それから次のループのために準備するため、勾配をゼロに設定します、そうでなければ、勾配は発生した総ての演算の実行計算を記録するでしょう (i.e. loss.backward() は既にストアされたものが何でも (それらを置き換えるのでなく) 勾配を追加します)。

TIP: Python コードに踏み入れるために標準 python デバッガーを使用できます、各ステップで様々な変数値を確認することを可能にします。それを試すには下で set_trace() をアンコメントします。

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_()

That’s it: 最小限のニューラルネットワークを完全にスクラッチから作成して訓練しました (この場合、ロジスティック回帰です、何故ならば隠れ層を持たないからです)。

損失と精度を確認してそれらを先に得たものと比較しましょう。私達は損失が減少して精度が増加することを期待します、そしてそれらは以下を持ちます :

print(loss_func(model(xb), yb), accuracy(model(xb), yb))
tensor(0.0820, grad_fn=<NegBackward>) tensor(1.)

 

torch.nn.functional を使用する

今は私達のコードをリファクタリングします、その結果それは前と同じことを行ない、それをより簡潔で柔軟にするために PyTorch の nn クラスを活用し始めるだけです。ここから各ステップで、コードを次の一つかそれ以上にするはずです : より短い、より理解可能、and/or より柔軟である。

最初のそして最も容易なステップは手書きの活性化と損失関数を torch.nn.functional (これは慣習により一般に名前空間 F にインポートされます) からのものと置き換えることによりコードを短くすることです。このモジュールは (ライブラリの他の部分がクラスを含むところ) torch.nn ライブラリ内の総ての関数を含みます。広範囲の損失と活性化関数に加えて、プーリング関数のような、ニューラルネットを作成するための幾つかの便利な関数もここで見つけるでしょう。(畳込み、線形層等を行なうための関数もありますが、見るように、これらは通常はライブラリの他の部分を使用してより良く処理されます。)

負対数尤度損失と log softmax 活性を使用している場合、PyTorch は 2 つを結合する単一関数 F.cross_entropy を提供します。そのためモデルから活性化関数を除去することさえできます。

import torch.nn.functional as F

loss_func = F.cross_entropy

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

モデル関数で log_softmax をもはや呼び出さないことに注意してください。損失と精度が前と同じであることを確かめましょう :

print(loss_func(model(xb), yb), accuracy(model(xb), yb))
tensor(0.0820, grad_fn=<NllLossBackward>) tensor(1.)

 

nn.Module を使用してリファクタリングする

次です、より明白で簡潔な訓練ループのために、nn.Module と nn.Parameter を利用します。nn.Module (これ自身はクラスで状態を追跡できます) をサブクラス化します。この場合、重み、バイアスと forward ステップのためのメソッドを保持するクラスを作成することを望みます。nn.Module は多くの属性と (.parameters() と .zero_grad() のような) メソッドを持ち、私達はこれらを使用しています。

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

今は関数を単に使用する代わりにオブジェクトを使用していますので、最初にモデルをインスタンス化しなければなりません :

model = Mnist_Logistic()

今では前と同じ方法で損失を計算できます。nn.Module オブジェクトはそれらが関数であるかのように (i.e. それらは callable です) 使用されますが、裏では PyTorch は自動的に forward メソッドを呼び出すことに注意してください。

print(loss_func(model(xb), yb))
tensor(2.3307, grad_fn=<NllLossBackward>)

以前は訓練ループについて各パラメータのために名前で値を更新し、そして各パラメータのために個別に勾配を手動でゼロにしなければなりませんでした、このようにです :

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

今ではそれらのステップをより簡潔で (特により複雑なモデルを持った場合) パラメータの幾つかを忘れる誤りをしがちでなくなるために model.parameters() と model.zero_grad() (これらは両者とも nn.Module のために PyTorch により定義されます) を活用できます :

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

小さい訓練ループを fit 関数にラップしますのでそれを後で再度実行できます。

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()

損失が下がったことを二重にチェックしましょう :

print(loss_func(model(xb), yb))
tensor(0.0813, grad_fn=<NllLossBackward>)

 

nn.Linear を使用してリファクタリングする

私達のコードのリファクタリングを続けます。手動で self.weights と self.bias を定義して初期化し、そして xb @ self.weights + self.bias を計算する代わりに、線形層のために PyTorch クラス nn.Linear を使用します、これは私達のためにその全てを行ないます。PyTorch は多くの型の事前定義された層を持ちます、これはコードを素晴らしく単純化して、しばしばより高速にもできます。

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

    def forward(self, xb):
        return self.lin(xb)

モデルをインスタンス化して前と同じ方法で損失を計算します :

model = Mnist_Logistic()
print(loss_func(model(xb), yb))
tensor(2.4159, grad_fn=<NllLossBackward>)

依然として前のように同じ fit メソッドを使用することができます。

fit()

print(loss_func(model(xb), yb))
tensor(0.0851, grad_fn=<NllLossBackward>)

 

optim を使用してリファクタリングする

PyTorch はまた様々な最適化アルゴリズムを持つパッケージを持ちます、torch.optim です。各パラメータを手動で更新する代わりに、forward ステップを取るために optimizer からの step メソッドを使用できます。

これは先に手動でコーディングした optimization ステップを私達に置き換えさせます :

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

そして代わりに次を単に使用します :

opt.step()
opt.zero_grad()

(optim.zero_grad() は勾配を 0 にリセットします、そして次のミニバッチのための勾配を計算する前にそれを呼び出す必要があります。)

from torch import optim

モデルと optimizer を作成する小さな関数を定義しますので、将来的にそれを再利用できます。

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))
tensor(2.3381, grad_fn=<NllLossBackward>)
tensor(0.0812, grad_fn=<NllLossBackward>)

 

データセットを使用してリファクタリングする

PyTorch は抽象 Dataset クラスを持ちます。Dataset はそれへのインデキシングの方法として (Python の標準 len 関数により呼び出される) __len__ 関数と __getitem__ 関数を持つどのようなものでもあり得ます。このチュートリアル は Dataset のサブクラスとしてカスタム FacialLandmarkDataset クラスを作成する良い例をウォークスルーします。

PyTorch の TensorDataset は tensor をラッピングした Dataset です。length とインデキシングの方法を定義することにより、これはまた tensor の最初の次元に沿って iterate、index と slice の方法を与えます。これは訓練のとき同じ行の独立的なそして依存的な変数の両者へのアクセスを容易にします。

from torch.utils.data import TensorDataset

x_train と y_train の両者は単一の TensorDataset 内で結合できます、これは反復とスライスが容易です。

train_ds = TensorDataset(x_train, y_train)

以前は、個別に x と y 値のミニバッチを通して反復しなければなりませんでした :

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

今では、これら 2 つのステップを一緒に行なうことができます :

xb,yb = train_ds[i*bs : i*bs+bs]
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))
tensor(0.0811, grad_fn=<NllLossBackward>)

 

DataLoader を使用してリファクタリングする

PyTorch の DataLoader はバッチを管理する責任を負います。任意の Dataset から DataLoader を作成できます。DataLoader はバッチに渡り反復することを容易にします。train_ds[i*bs : i*bs+bs] を使用しなけらばならないのではなく、DataLoader は各ミニバッチを自動的に与えます。

from torch.utils.data import DataLoader

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

以前は、このようにループはバッチ (xb, yb) に渡り反復しました :

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

今は、ループは遥かにきれいです、何故ならば (xb, yb) はデータローダから自動的にロードされるからです。

for xb,yb in train_dl:
    pred = model(xb)
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))
tensor(0.0825, grad_fn=)

Pytorch の nn.Module, nn.Parameter, Dataset と DataLoader のおかげで、訓練ループは今では劇的に小さくそして理解することが容易です。今は実際に効果的なモデルを作成するために必要な基本的な特徴を追加してみましょう。

 

検証を追加する

セクション 1 では、訓練データ上での利用のために合理的な訓練ループを単にセットアップしようとしていました。実際には、overfitting していないかを識別するために、検証セット も常に持つべきです。

バッチと overfitting の間の相関を回避するために訓練データをシャッフルすることは 重要 です。他方、検証損失は検証セットをシャッフルしてもしなくても同一です。シャッフリングは extra 時間を取りますので、検証データをシャッフルすることは意味がありません。

検証セットのためには訓練セットのためのものの 2 倍の大きさのバッチサイズを使用します。これは検証セットは逆伝播を必要とせずそのためメモリもより少なくてすむからです (それは勾配をストアする必要がありません)。より大きいバッチサイズを使用して損失をより迅速に損失を計算するためこれを活用します。

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)

各エポックの最後に検証損失を計算してプリントします。

(訓練前に model.train() を、そして推論前に model.eval() を常に呼び出すことに注意してください、何故ならばこれらの異なるフェーズのための適切な動作を確実にするため nn.BatchNorm2d と nn.Dropout のような層によりこれらは使用されます。)

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))
0 tensor(0.2866)
1 tensor(0.2907)

 

fit() と get_data() を作成する

今は私達自身のものを少しリファクタリングします。訓練セットと検証セットの両者のために損失を計算する類似のプロセスを二度通り抜けていますので、それをそれ自身の関数 loss_batch にします、これっは一つのバッチのための損失を計算します。

訓練セットのために optimizer を渡し、そして逆伝播を遂行するためにそれを使用します。検証セットについては、optimizer を渡しませんので、メソッドは逆伝播を遂行しません。

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 はモデルを訓練して各エポックのために訓練と検証損失を計算するために必要な演算を実行します。

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 は訓練と検証セットのためのデータローダを返します。

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

今では、データローダを得てモデルを fit するプロセス全体は 3 行のコードで実行できます :

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)
0 0.30178326894044877
1 0.31211711330413816

広範囲のモデルを訓練するためにこれらの基本的な 3 行のコードを利用できます。畳込みニューラルネットワーク (CNN) を訓練するためにこれらを利用できるか見ましょう!

 

CNN に切り替える

今は 3 つの畳込み層でニューラルネットワークを構築していきます。前のセクションの関数のどれもモデル形式について何も仮定していませんから、どのような変更もなしに CNN を訓練するためにそれらを利用できます。

私達の畳込み層として PyTorch の事前定義された Conv2d クラスを使用します。3 つの畳込み層を持つ CNN を定義します。各畳込み層には ReLU が続きます。最後に、平均プーリングを遂行します (view は numpy の reshape の PyTorch 版です)。

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 は前の更新も考慮に入れる、確率的勾配降下のバリエーションで一般的により高速な訓練に繋がります。

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

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.4125748429298401
1 0.33628556914329527

 

nn.Sequential

torch.nn はコードを単純化するために利用できるもう一つの便利なクラスを持ちます : Sequential です。Sequential オブジェクトはそれの内部に含まれるモジュールの各々を sequential 流儀で実行します。これがニューラルネットワークを書く単純な方法です。

これを活用するためには、与えられた関数から カスタム層 を容易に定義できる必要があります。例えば、PyTorch は view 層を持ちません、そしてネットワークのために一つを作成する必要があります。そして Lambda は Sequential でネットワークを定義するとき利用できる層を作成します。

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 で作成されたモデルは単純に :

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)
0 0.3096521191596985
1 0.2257734361886978

 

DataLoader をラッピングする

私達の CNN は非常に簡潔ですが、それは MNIST で動作するだけです、何故ならば :

  • それは入力が 28*28 long ベクトルであることことを仮定しています。
  • それは最終 CNN グリッドサイズが 4*4 であることを仮定しています (何故ならばそれが使用した平均プーリング・カーネルサイズだからです)

これら 2 つの仮定を取り除きましょう、するとモデルは任意の 2d シングルチャネル画像で動作します。最初に初期 Lambda 層を除去できますが、データ前処理を generator に移動します :

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 で置き換えられます、これは持っている入力 tensor ではなく、望む tensor のサイズを定義することを可能にします。その結果、モデルは任意のサイズ入力で動作します。

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)

それを試してみましょう :

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

0 0.29816289551258085
1 0.2559084745645523

 

GPU を使用する

もし貴方が CUDA-capable GPU へのアクセスを持つほどに十分に幸運であれば (殆どのクラウドプロバイダーから約 $0.50/hour で一つを賃貸できます) コードを高速化するためにそれを利用できます。最初に GPU が PyTorch で動作するか確認します :

print(torch.cuda.is_available())
True

それからそれのために device オブジェクトを作成します :

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

バッチを GPU に移すために preprocess を更新しましょう :

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 に移動できます。

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

今はそれがより高速に動作することを見出すはずです :

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.191569237947464
1 0.24150753207206727

 

終わりに

今では一般的な訓練パイプラインと訓練ループを持ちます、これを PyTorch を使用して多くのタイプのモデルを訓練するために利用できます。今はモデルを訓練することがどれほど単純であり得るかを見るために、mnist_sample サンプルノートブックを見てください。

もちろん、データ増強、ハイパーパラメータ調整、訓練監視、転移学習等のように、貴方が追加することを望む多くのものがあります、これらの特徴は fastai ライブラリで利用可能です、これらはこのチュートリアルで示された同じ設計アプローチを使用して開発され、モデルを更に進めることを求める実践者のために自然な次のステップを提供します。

サンプルを通して torch.nn, torch.optim, Dataset と DataLoader の各々を説明するとこのチュートリアルの最初に約束しましたそこで私達が見たものを要約しましょう :

  • torch.nn
    • Module: 関数のように動作しながら、(ニューラルネット層の重みのような) 状態も含むことができる callable を作成します。それはそれがどのようなパラメータを含むかを知りそしてそれらの総ての勾配をゼロにできて、重み更新のためにそれらを通してループできます、等々。
    • Parameter: Module にそれが逆伝播の間に更新が必要な重みを持つことを知らせる tensor のためのラッパーです。requires_grad 属性が設定された tensor だけが更新されます。
    • functional: 畳込みと線形層のような non-stateful バージョンの層に加えて、活性化関数、損失関数等を含むモジュールです (通常は慣習により F 名前空間にインポートされます)。
  • torch.optim: SGD のような optimizer を含みます、これは逆伝播ステップの間に Prameter の重みを更新します。
  • Dataset: TensorDataset のような PyTorch で提供されるクラスを含む、__len__ と __getitem__ を持つオブジェクトの抽象インターフェイスです。
  • DataLoader: 任意の Dataset を取りデータのバッチを返す iterator を作成します。

 
以上