PyTorchでモデル(ネットワーク)を構築・生成

Posted: | Tags: Python, PyTorch, 機械学習

PyTorchでモデル(ネットワーク)を構築・生成するには、torch.nn.Sequentialを利用したり、torch.nn.Moduleのサブクラスを定義したりする。

ここでは以下の内容について説明する。

  • torch.nn.Sequentialでモデルを構築
    • torch.nn.Sequential()で生成
    • torch.nn.Sequential()OrderedDictを指定
    • add_module()でレイヤーを追加
  • torch.nn.Moduleのサブクラスを定義してモデルを構築
    • 基本的な書き方
    • モデルの形状などを引数で指定
  • torch.nn.functionalの関数の利用
    • torch.nn.functionalの関数の基本的な使い方
    • torch.nn.functionalの関数の注意点
  • モデルを別のモデルに組み込む

本記事におけるPyTorchのバージョンは以下の通り。バージョンが異なると仕様が異なる可能性があるので注意。

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

print(torch.__version__)
# 1.7.1

なお、本記事ではモデルの構築・生成のみにフォーカスし、訓練(学習)のループなどには触れない。

torch.nn.Sequentialでモデルを構築

torch.nn.Sequentialは、Sequentialという名前の通り、一方通行のシンプルなモデル(ネットワーク)のためのクラス。

torch.nn.Sequential()で生成

コンストラクタtorch.nn.Sequential()に、torch.nn.Moduleを継承したクラスのインスタンスを順番に指定する。

基本的なモジュール(レイヤー)のクラスはtorch.nnで提供されている。

以下のようにモデルを生成できる。ここでは全結合層とReLU, Dropoutからなるシンプルなモデルを例とする。あくまでも例であり、特に何らかの意味があるモデルではない。

torch.manual_seed(0)

net_seq = nn.Sequential(
    nn.Linear(1000, 100),
    nn.ReLU(),
    nn.Dropout(0.2),
    nn.Linear(100, 10)
)

結果が一定になるようにtorch.manual_seed()でランダムシードを固定している。以降でも度々呼び出しているが特に気にしなくてもよい。

print()で中身を表示できる。

print(net_seq)
# Sequential(
#   (0): Linear(in_features=1000, out_features=100, bias=True)
#   (1): ReLU()
#   (2): Dropout(p=0.2, inplace=False)
#   (3): Linear(in_features=100, out_features=10, bias=True)
# )

torch.nn.Sequentialtorch.nn.Moduleのサブクラス。

print(type(net_seq))
# <class 'torch.nn.modules.container.Sequential'>

print(issubclass(type(net_seq), nn.Module))
# True

0始まりのインデックスでレイヤーを取得できる。例えばtorch.nn.Linearの場合はweightbias属性で重みやバイアスの値を確認できる。

print(net_seq[0])
# Linear(in_features=1000, out_features=100, bias=True)

print(type(net_seq[0]))
# <class 'torch.nn.modules.linear.Linear'>

print(net_seq[0].weight)
# Parameter containing:
# tensor([[-0.0002,  0.0170, -0.0260,  ..., -0.0102,  0.0001, -0.0061],
#         [-0.0027, -0.0247, -0.0002,  ...,  0.0012, -0.0096,  0.0238],
#         [ 0.0175,  0.0057,  0.0048,  ..., -0.0144, -0.0125, -0.0265],
#         ...,
#         [ 0.0007,  0.0006, -0.0082,  ..., -0.0033, -0.0160, -0.0130],
#         [ 0.0016, -0.0262,  0.0075,  ...,  0.0072,  0.0184,  0.0094],
#         [ 0.0031,  0.0199, -0.0057,  ..., -0.0101, -0.0229, -0.0243]],
#        requires_grad=True)

生成したモデルは呼び出し可能(xxx()で実行可能)で、適切なサイズのtorch.Tensorを引数として与えると結果が得られる。この結果は、例えば10クラス分類の場合の各クラスの確率などに相当する。

torch.manual_seed(0)
t = torch.randn(1, 1000)

torch.manual_seed(0)
print(net_seq(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

print(net_seq(t).shape)
# torch.Size([1, 10])

バッチ処理に対応しており、最初の次元(バッチ用の次元)の大きさが変わっても問題なく処理される。

t_ = torch.randn(3, 1000)
print(net_seq(t_))
# tensor([[-0.4004, -0.1475,  0.0014, -0.0756,  0.2095, -0.3645,  0.7861, -0.0645,
#           0.1356, -0.0600],
#         [-0.2170, -0.0610,  0.0520, -0.0137,  0.1295,  0.0086,  0.0625, -0.6118,
#           0.1942, -0.5471],
#         [-0.2405, -0.0499, -0.1613,  0.4955,  0.1280, -0.3260, -0.1218, -0.1814,
#           0.1854,  0.0027]], grad_fn=<AddmmBackward>)

print(net_seq(t_).shape)
# torch.Size([3, 10])

例えば、高さhwチャンネル数c(RGB画像の場合はc = 3)の画像を入力とする場合、形状(c, h, w)のテンソルを入力とするモデルを構築すると、n枚の画像をまとめたテンソル、すなわち形状(n, c, h, w)のテンソルを処理できる。

torch.nn.Sequential()にOrderedDictを指定

コンストラクタtorch.nn.Sequential()にはOrderedDictも指定できる。キーとしてレイヤーの名前を指定する。

from collections import OrderedDict

torch.manual_seed(0)

net_seq_od = nn.Sequential(OrderedDict([
    ('fc1', nn.Linear(1000, 100)),
    ('relu', nn.ReLU()),
    ('dropout', nn.Dropout(0.2)),
    ('fc2', nn.Linear(100, 10))
]))

print(net_seq_od)
# Sequential(
#   (fc1): Linear(in_features=1000, out_features=100, bias=True)
#   (relu): ReLU()
#   (dropout): Dropout(p=0.2, inplace=False)
#   (fc2): Linear(in_features=100, out_features=10, bias=True)
# )

torch.manual_seed(0)
print(net_seq_od(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

0始まりのインデックスだけでなく、.<名前>でも各レイヤーを取得できる。辞書のように['名前']だとエラーになるので注意。

print(net_seq_od[0])
# Linear(in_features=1000, out_features=100, bias=True)

print(net_seq_od.fc1)
# Linear(in_features=1000, out_features=100, bias=True)

# print(net_seq_od['fc1'])
# TypeError: 'str' object cannot be interpreted as an integer

add_module()でレイヤーを追加

空のtorch.nn.Sequentialを生成してからadd_module()メソッドでレイヤーを追加することもできる。

add_module()には、第一引数に名前、第二引数にtorch.nn.Moduleを継承したクラスのインスタンスを指定する。

torch.manual_seed(0)

net_seq_add = nn.Sequential()
net_seq_add.add_module('fc1', nn.Linear(1000, 100))
net_seq_add.add_module('relu', nn.ReLU())
net_seq_add.add_module('dropout', nn.Dropout(0.2))
net_seq_add.add_module('fc2', nn.Linear(100, 10))

print(net_seq_add)
# Sequential(
#   (fc1): Linear(in_features=1000, out_features=100, bias=True)
#   (relu): ReLU()
#   (dropout): Dropout(p=0.2, inplace=False)
#   (fc2): Linear(in_features=100, out_features=10, bias=True)
# )

torch.manual_seed(0)
print(net_seq_add(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

OrderedDictの場合と同様、0始まりのインデックス、.<名前>で各レイヤーを取得可能。

print(net_seq_add[0])
# Linear(in_features=1000, out_features=100, bias=True)

print(net_seq_add.fc1)
# Linear(in_features=1000, out_features=100, bias=True)

torch.nn.Moduleのサブクラスを定義してモデルを構築

一方通行ではない複雑なモデル(ネットワーク)を構築するには、torch.nn.Moduleを継承したサブクラスを定義する。

torch.nn.ModuleはPyTorchにおけるニューラルネットワークのモジュール(レイヤー)すべてのベースとなるクラス。

ここでは、上でtorch.nn.Sequentialを使って生成したモデルと同じ構成のシンプルなモデルを構築する。

基本的な書き方

torch.nn.Moduleのサブクラスを定義する基本的な書き方は以下の通り。

__init__()で使用するモジュール(レイヤー)のインスタンスを生成し、forward()で所望の順番で適用していく。super().__init__()を忘れないように注意。

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(1000, 100)
        self.fc2 = nn.Linear(100, 10)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(0.2)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.dropout(x)
        x = self.fc2(x)
        return x

forward()において、特に処理が分岐したりせずに連続して適用される場合はまとめて書いてもよい。

#     def forward(self, x):
#         x = self.relu(self.fc1(x))
#         x = self.dropout(x)
#         x = self.fc2(x)
#         return x

#     def forward(self, x):
#         return self.fc2(self.dropout(self.relu(self.fc1(x))))

インスタンスを生成するとprint()でモデルの中身を表示できる。print()での出力順は__init__()で書いた順番で、forward()において適用される順番ではないので注意。

torch.manual_seed(0)
net = Net()
print(net)
# Net(
#   (fc1): Linear(in_features=1000, out_features=100, bias=True)
#   (fc2): Linear(in_features=100, out_features=10, bias=True)
#   (relu): ReLU()
#   (dropout): Dropout(p=0.2, inplace=False)
# )

torch.nn.Sequentialを使って生成したモデルと同じ結果が得られる。

torch.manual_seed(0)
t = torch.randn(1, 1000)

torch.manual_seed(0)
print(net(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

torch.nn.Sequentialのように一方通行のモデルとは限らないので、[インデックス]でレイヤーを取得することはできない。レイヤーは.<名前>で取得できる。['名前']だとエラー。

# print(net[0])
# TypeError: 'Net' object is not subscriptable

print(net.fc1)
# Linear(in_features=1000, out_features=100, bias=True)

# print(net['fc1'])
# TypeError: 'Net' object is not subscriptable

モデルの形状などを引数で指定

モデルの形状などを決め打ちではなく、インスタンス生成時に引数で指定したい場合は、以下のように__init__()を定義すればよい。

class NetParam(nn.Module):
    def __init__(self, n_input, n_hidden, n_output, p_dropout):
        super().__init__()
        self.fc1 = nn.Linear(n_input, n_hidden)
        self.fc2 = nn.Linear(n_hidden, n_output)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(p_dropout)

    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        return x

torch.manual_seed(0)
net_param = NetParam(1000, 100, 10, 0.2)
print(net_param)
# NetParam(
#   (fc1): Linear(in_features=1000, out_features=100, bias=True)
#   (fc2): Linear(in_features=100, out_features=10, bias=True)
#   (relu): ReLU()
#   (dropout): Dropout(p=0.2, inplace=False)
# )

torch.manual_seed(0)
print(net_param(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

torch.nn.functionalの関数の利用

torch.nn.functionalにはtorch.nnのクラスの関数版が提供されている。

torch.nn.functionalは慣例的にFという名前でインポートされることが多い。

import torch.nn.functional as F

torch.nn.functionalの関数を利用するとtorch.nn.Moduleのサブクラスの定義がスッキリ書ける。

なお、torch.nn.Flattenに対するtorch.flatten()torch.nn.Tanhに対するtorch.tanh()のように、torch直下で関数が提供されているものもある。考え方はtorch.nn.functionalと同じ。

torch.nn.Flattentorch.flatten()のようにデフォルトの設定が異なる場合もあるので注意。

torch.nn.functionalの関数の基本的な使い方

ReLUのような活性化関数やプーリング関数などは、重みやバイアスといったパラメータを持たず、入力に対して出力を一意に決定する。

このような関数は、torch.nn.Moduleを継承したクラスのインスタンスを生成して使用するのではなく、torch.nn.functional以下の関数をそのまま使用できる。

例えば、torch.nn.ReLUに対してはtorch.nn.functional.relu()が提供されている。これを使うとこれまでと同じモデルは以下のように書ける。

class NetFunctional(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(1000, 100)
        self.fc2 = nn.Linear(100, 10)
        self.dropout = nn.Dropout(0.2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        return x

torch.manual_seed(0)
net_f = NetFunctional()
print(net_f)
# NetFunctional(
#   (fc1): Linear(in_features=1000, out_features=100, bias=True)
#   (fc2): Linear(in_features=100, out_features=10, bias=True)
#   (dropout): Dropout(p=0.2, inplace=False)
# )

torch.manual_seed(0)
print(net_f(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

__init__()でインスタンスを生成していない場合、print()では表示されないので注意。

torch.nn.functionalの関数の注意点

torch.nn.functionalには様々な関数があるが、何も考えずに使っていいということではない。

パラメータを保持しているレイヤーの場合

例えばtorch.nn.Linearの関数版であるtorch.nn.functional.linear()もある。

torch.nn.Lineartorch.nn.Moduleを継承したクラスであり、そのインスタンスはパラメータとして重みやバイアスを保持している。torch.nn.Linearのインスタンスを生成して実行すると、そのとき保持されている重みとバイアスで結果が出力される。最適化アルゴリズムでパラメータを更新することでモデルの訓練が行われる。

一方、torch.nn.functional.linear()は、引数として入力と重み・バイアスを指定して出力を算出する関数である。

訓練対象のパラメータを保持するレイヤーに対してはtorch.nn.Moduleを継承したクラスのインスタンスを生成し用いる。

訓練時と推論時で振る舞いが異なるレイヤーの場合

DropoutやBatch Normalizationといったレイヤーは訓練時と推論時で振る舞いが異なる。例えばDropoutは基本的には訓練時のみに適用し、何らかの意図がない限り推論時にはスキップする。

torch.nn.Dropoutクラスのインスタンスを用いる場合は、訓練時と推論時で自動的に処理が切り替えられる。

上でtorch.nn.Dropoutクラスのインスタンスを用いて定義したモデルの場合、訓練時と推論時で結果が異なっていることが分かる。なお、PyTorchにおいてモデルの訓練・推論の切り替えはtrain(), eval()を用いる。

torch.manual_seed(0)
net = Net()

net.train()

torch.manual_seed(0)
print(net(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

net.eval()

torch.manual_seed(0)
print(net(t))
# tensor([[-0.2834,  0.0206, -0.0293,  0.3862,  0.3254, -0.5541, -0.1213,  0.1510,
#          -0.0269, -0.0560]], grad_fn=<AddmmBackward>)

一方、torch.nn.functional.dropout()を用いると、訓練時も推論時も処理が切り替わらず、同じ結果となる。

class NetFunctionalDropoutError(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(1000, 100)
        self.fc2 = nn.Linear(100, 10)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.dropout(x, 0.2)
        x = self.fc2(x)
        return x

torch.manual_seed(0)
net_f_dropout_error = NetFunctionalDropoutError()

net_f_dropout_error.train()

torch.manual_seed(0)
print(net_f_dropout_error(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

net_f_dropout_error.eval()

torch.manual_seed(0)
print(net_f_dropout_error(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

モデルが訓練中か推論中かはtraining属性で取得できる。これをtorch.nn.functional.dropout()の第二引数trainingに指定すると、訓練時と推論時で処理を切り替えることも可能。

class NetFunctionalDropout(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(1000, 100)
        self.fc2 = nn.Linear(100, 10)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.dropout(x, 0.2, self.training)
        x = self.fc2(x)
        return x

torch.manual_seed(0)
net_f_dropout = NetFunctionalDropout()

net_f_dropout.train()

torch.manual_seed(0)
print(net_f_dropout(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

net_f_dropout.eval()

torch.manual_seed(0)
print(net_f_dropout(t))
# tensor([[-0.2834,  0.0206, -0.0293,  0.3862,  0.3254, -0.5541, -0.1213,  0.1510,
#          -0.0269, -0.0560]], grad_fn=<AddmmBackward>)

モデルを別のモデルに組み込む

上述の通り、PyTorchにおけるニューラルネットワークのモジュール(レイヤー)はすべてtorch.nn.Moduleのサブクラスである。

torch.nn.Linearなどのtorch.nnで提供されているクラスと同様に、独自に定義したtorch.nn.Moduleのサブクラスを別のモデルに組み込むこともできる。同じ構造を繰り返すモデルなどはブロックを最初に定義しておくと楽。また、torch.nnで提供されていないレイヤーを新たに定義して使うことも可能。

これまでの例と同じモデルの前半部分をクラスとして定義する。

class NetInner(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(1000, 100)
        self.dropout = nn.Dropout(0.2)

    def forward(self, x):
        x = F.relu(self.fc(x))
        x = self.dropout(x)
        return x

torch.nn.Sequential()の引数にそのインスタンスを指定できる。

torch.manual_seed(0)

net_nested_seq = nn.Sequential(
    NetInner(),
    nn.Linear(100, 10)
)

print()での出力は以下の通り。入れ子になって表示される。

print(net_nested_seq)
# Sequential(
#   (0): NetInner(
#     (fc): Linear(in_features=1000, out_features=100, bias=True)
#     (dropout): Dropout(p=0.2, inplace=False)
#   )
#   (1): Linear(in_features=100, out_features=10, bias=True)
# )

それぞれのレイヤーもこれまでの例と同様に取得できる。

print(net_nested_seq[0])
# NetInner(
#   (fc): Linear(in_features=1000, out_features=100, bias=True)
#   (dropout): Dropout(p=0.2, inplace=False)
# )

print(net_nested_seq[0].fc)
# Linear(in_features=1000, out_features=100, bias=True)

結果も同じ。

torch.manual_seed(0)
t = torch.randn(1, 1000)

torch.manual_seed(0)
print(net_nested_seq(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

torch.nn.Moduleのサブクラスを定義する場合も同様。

class NetNested(nn.Module):
    def __init__(self):
        super().__init__()
        self.my_net = NetInner()
        self.fc = nn.Linear(100, 10)

    def forward(self, x):
        x = self.my_net(x)
        x = self.fc(x)
        return x

torch.manual_seed(0)
net_nested = NetNested()
print(net_nested)
# NetNested(
#   (my_net): NetInner(
#     (fc): Linear(in_features=1000, out_features=100, bias=True)
#     (dropout): Dropout(p=0.2, inplace=False)
#   )
#   (fc): Linear(in_features=100, out_features=10, bias=True)
# )

print(net_nested.my_net)
# NetInner(
#   (fc): Linear(in_features=1000, out_features=100, bias=True)
#   (dropout): Dropout(p=0.2, inplace=False)
# )

print(net_nested.my_net.fc)
# Linear(in_features=1000, out_features=100, bias=True)

torch.manual_seed(0)
print(net_nested(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

torch.nn.Moduleのサブクラスを定義する際にtorch.nn.Sequential()を用いることもできる。

class NetNestedSeq(nn.Module):
    def __init__(self):
        super().__init__()
        self.my_net = nn.Sequential(
            nn.Linear(1000, 100),
            nn.ReLU(),
            nn.Dropout(0.2),
        )
        self.fc = nn.Linear(100, 10)

    def forward(self, x):
        x = self.my_net(x)
        x = self.fc(x)
        return x

torch.manual_seed(0)
net_nested_seq = NetNestedSeq()
print(net_nested_seq)
# NetNestedSeq(
#   (my_net): Sequential(
#     (0): Linear(in_features=1000, out_features=100, bias=True)
#     (1): ReLU()
#     (2): Dropout(p=0.2, inplace=False)
#   )
#   (fc): Linear(in_features=100, out_features=10, bias=True)
# )

print(net_nested_seq.my_net)
# Sequential(
#   (0): Linear(in_features=1000, out_features=100, bias=True)
#   (1): ReLU()
#   (2): Dropout(p=0.2, inplace=False)
# )

print(net_nested_seq.my_net[0])
# Linear(in_features=1000, out_features=100, bias=True)

torch.manual_seed(0)
print(net_nested_seq(t))
# tensor([[-0.3884,  0.0370,  0.0175,  0.3579,  0.1390, -0.4750, -0.3484,  0.2648,
#           0.1452,  0.1219]], grad_fn=<AddmmBackward>)

全体をtorch.nn.Sequential()で書けない複雑なモデルの場合も、シーケンシャルな部分はtorch.nn.Sequential()を用いるとforward()をスッキリ書けることがある。

関連カテゴリー

関連記事