PyTorch 2.0 チュートリアル : 学習 : torch.nn とは実際には何でしょう?

PyTorch 2.0 チュートリアル : 学習 : torch.nn とは実際には何でしょう? (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 03/25/2023 (2.0.0)

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

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

 

クラスキャット 人工知能 研究開発支援サービス

クラスキャット は人工知能・テレワークに関する各種サービスを提供しています。お気軽にご相談ください :

◆ 人工知能とビジネスをテーマに WEB セミナーを定期的に開催しています。スケジュール
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。

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

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

 

PyTorch 2.0 チュートリアル : 学習 : torch.nn とは実際には何でしょう?

ニューラルネットワークを作成して訓練することを手助けするために PyTorch はエレガントに設計されたモジュールとクラス torch.nn , torch.optim , Dataset と DataLoader を提供しています。それらのパワーをフルに活用して貴方の問題のためにそれらをカスタマイズするには、実際にはそれらが何をしているか正確に理解する必要があります。この理解を深めるため、最初にこれらのモデルからのどのような機能も利用することなく MNIST データセット上の基本的なニューラルネットを訓練します ; 初期には最も基本的な PyTorch テンソル機能を利用するだけです。それから、順次 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 = "https://github.com/pytorch/tutorials/raw/main/_static/"
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) の平坦化された (flattened) 行としてストアされています。一つを見てみましょう ; 最初にそれを 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
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 テンソル演算だけを使用してモデルを作成しましょう。

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

重みについては、初期化の 後に 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.0526, -2.5643, -2.4484, -2.8947, -2.0901, -2.5724, -2.6086, -1.9757,
        -2.1673, -2.0709], grad_fn=) torch.Size([64, 10])

ご覧のように、preds テンソルはテンソル値だけでなく、勾配関数もまた含みます。これを後で逆伝播を行なうために使用します。

損失関数として使用するために負対数尤度を実装しましょう (再度、標準 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.2954, grad_fn=<NegBackward0>)

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

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

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

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

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

  • データの (サイズ 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.0797, grad_fn=<NegBackward0>) 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.0797, grad_fn=<NllLossBackward0>) 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.2863, grad_fn=<NllLossBackward0>)

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

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.0826, grad_fn=<NllLossBackward0>)

 

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.2692, grad_fn=<NllLossBackward0>)

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

fit()

print(loss_func(model(xb), yb))
tensor(0.0816, grad_fn=<NllLossBackward0>)

 

optim を使用したリファクタリング

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

これにより先に手動でコーディングした最適化ステップを置き換えることができます :

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.3160, grad_fn=<NllLossBackward0>)
tensor(0.0821, grad_fn=<NllLossBackward0>)

 

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

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

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

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.0822, grad_fn=<NllLossBackward0>)

 

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.0808, grad_fn=<NllLossBackward0>)

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

 

検証の追加

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

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

検証セットのために訓練セットのためのものの 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.3156)
1 tensor(0.2896)

 

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.29760321753025054
1 0.2974514301419258

広範囲のモデルを訓練するためにこれらの基本的な 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

モメンタム は前の更新も考慮に入れる、確率的勾配降下のバリエーションで一般により高速な訓練に繋がります。

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.3172487957239151
1 0.2313283410191536

 

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.35441860365867617
1 0.24011159185171127

 

DataLoader のラッピング

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

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

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

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

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.36206412925720216
1 0.28640508375167845

 

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.20506883896589279
1 0.17373736295104028

 

終わりに

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

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

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

  • torch.nn
    • Module: 関数のように動作しながら、(ニューラルネット層の重みのような) 状態も含むことができる callable を作成します。それはそれがどのようなパラメータを含むかを知りそしてそれらの総ての勾配をゼロにして、重み更新のためにそれらを通してループすることができます、等。
    • Parameter: Module にそれが逆伝播の間に更新が必要な重みを持つことを知らせるテンソルのためのラッパーです。requires_grad 属性が設定されたテンソルだけが更新されます。
    • functional: 畳込みと線形層のような non-stateful バージョンの層に加えて、活性化関数、損失関数等を含むモジュールです (通常は慣習により F 名前空間にインポートされます)。

  • torch.optim: SGD のような optimizer を含みます、これは逆伝播ステップの間に Parameter の重みを更新します。

  • Dataset: TensorDataset のような PyTorch で提供されるクラスを含む、__len__ と __getitem__ を持つオブジェクトの抽象インターフェイスです。

  • DataLoader: 任意の Dataset を取りデータのバッチを返す iterator を作成します。

 

以上