LoginSignup
69
50

More than 3 years have passed since last update.

挫折しかけた人のためのPyTorchの初歩の初歩 〜系列モデルを組んでみよう〜

Last updated at Posted at 2019-05-26

趣旨

  • 自然言語処理を独学して9ヶ月になります
  • 深層学習のフレームワークをまさに勉強中で,四苦八苦しながら少しずつ覚えている最中です
  • 自分と似たような境遇の方に少しでも参考になればと思い,ごく基本的な事項をまとめてみました
  • 誤りなどあればぜひ教えてください!

このような方のために

  • RNN, LSTM, GRUについて一通り知っていて,Pythonも一通り書けるが,実装したことはない
  • 『ゼロからつくるDeep Learning 2』を読んでスクラッチ実装した経験はあるが,深層学習のフレームワークを触ったことはない
  • 深層学習のフレームワークを触ってみたいが,苦手意識がある
  • PyTorchのチュートリアルやドキュメントを読んだが,フレームワークの複雑さに諦めかけたことがある

内容

  • 簡単なニューラルネットワークの構造をつくる
  • ネットワークに推論させる
  • PyTorch version 1.1.0 を使用しています

ここで扱わないこと

  • 具体的な自然言語処理のタスク
  • PyTorchのインストールなどの環境構築
  • ニューラルなアプローチを含む自然言語処理の知識
  • データの前処理や入力, 訓練の実装や回し方, 誤差関数の実装など

1. RNNを組む

1-1. ネットワークをつくる

実はRNNのネットワークは比較的短いコードで組むことができます。
まずモジュールをインポートします:

import torch
import torch.nn as nn

「入力が3次元で,隠れ状態の表現が4次元の,1層のRNN」を組みたいときは次のように書きます:

dim_i = 3
dim_h = 4
num_l = 1

# 入力3次元, 隠れ状態4次元, 1層のRNN
model = nn.RNN(input_size=dim_i, hidden_size=dim_h, num_layers=num_l)

なんとたったこれだけでRNNの完成です!

双方向RNN

双方向RNN(BiDirectional RNN)を組むこともできます:

# 入力5次元, 隠れ状態8次元, 1層の双方向RNN
model2 = nn.RNN(input_size=5, hidden_size=8, num_layers=1, bidirectional=True)
多層のRNN

また,多層のRNNを組むこともできます:

# 入力3次元, 隠れ状態6次元, 2層のRNN
model3 = nn.RNN(input_size=3, hidden_size=6, num_layers=2)

1-2. RNNコンストラクタの引数

torch.nn.RNNのコンストラクタに入れることのできる引数は以下のとおりです。
ただ,実際に変更することのあるパラメーターはおそらくbidirectionalくらいでしょう。

model = torch.nn.RNN(input_size, hidden_size, num_layers=1, nonlinearity='tanh', bias=True, batch_first=False, dropout=0, bidirectional=False)

    input_size: int -> 入力ベクトルの次元数
    hidden_size: int -> 隠れ状態の次元数
    *num_layers: int -> RNNの層数。多層にしたいときは2以上に
    *nonlinearity: ('tanh','relu') -> 活性化関数
    *bias: bool -> バイアスを使うかどうか
    *batch_first: bool
    *dropout: float -> 途中の隠れ状態にDropoutを適用する確率
    *bidirectional: bool -> 双方向RNNにするかどうか

1-3. シーケンス長は決まっていなくてよい

ここでのポイントは,隠れ状態をいくつ並べるのかは,モデルの構築時に決める必要がないということです。

このRNNが隠れ状態の時相をいくつ持つのかは,入力に応じて柔軟に決まります。

たとえば,長さ2のシーケンスを入力した時は,自動的にRNNも隠れ状態を(初期のものも含めて)3つ持つものとして扱われます。

入力シーケンス長が5なら,自動的にRNNの隠れ状態も(初期のものも含めて)6つになります。

入力シーケンス長が変わっても,同じネットワークのオブジェクトを使い回すことができ,ネットワークを作り直す必要がないのは便利です。

1-4. RNNのパラメーターの確認

RNNの重み行列やバイアスといったパラメーターは,モデルが作られた段階で適当な値に初期化されています。
実際にパラメーターの値がどうなっているかを見てみましょう。

for param_name, param in model.named_parameters():
    print(param_name, param)

次のような出力が得られるはずです:

weight_ih_l0 Parameter containing:
tensor([[ 0.3952,  0.2076, -0.4457],
        [ 0.4347,  0.4305, -0.4006],
        [-0.2418,  0.2938,  0.3902],
        [ 0.2993, -0.4796,  0.3057]], requires_grad=True)
weight_hh_l0 Parameter containing:
tensor([[ 0.2947,  0.1054, -0.2473,  0.0123],
        [-0.4304, -0.1026, -0.2625,  0.2118],
        [-0.3104, -0.3083, -0.1775, -0.4657],
        [-0.3112,  0.2688, -0.0175,  0.1982]], requires_grad=True)
bias_ih_l0 Parameter containing:
tensor([ 0.2956,  0.0737, -0.3413,  0.2683], requires_grad=True)
bias_hh_l0 Parameter containing:
tensor([-0.4708,  0.1364, -0.0254, -0.3953], requires_grad=True)

これらの出力は次のようなことを意味します。
RNNの隠れ層で行われているのは次のような演算でした:

h_{t}=(W_{ih}x_{t}+b_{ih})+(W_{hh}h_{t-1}+b_{hh})

RNNのパラメーターとして扱われるのは$W_{ih}$,$W_{hh}$,$b_{ih}$,$b_{hh}$の4つの行列もしくはベクトルです。

上の出力例をみると,$W_{ih}$,$W_{hh}$,$b_{ih}$,$b_{hh}$の中身がこの順で出力されていることがわかります。

先ほどmodelを入力3次元,隠れ状態4次元のRNNとしてつくっていたことを思い出せば,$W_{ih}$,$W_{hh}$が4×3行列,$b_{ih}$,$b_{hh}$が4次元ベクトルとなっていることは納得がいきます。

1-5. RNNに推論させる

このRNNに何かを入力して,何らかの出力を得てみましょう。
もちろんこのRNNは初期化された状態のままであり,一切の学習を行なっていないため,でたらめな値を吐き出します。

まず,入力テンソルと初期の隠れ状態を,正規分布に従ってランダムに生成させます。
テンソルの次元を間違えないように気をつけましょう:

dim_i = 3
dim_h = 4
num_l = 1
num_sample = 2
len_seq = 5

# 入力
X = torch.randn(len_seq, num_sample, dim_i)

# 初期の隠れ状態
h0 = torch.randn(num_l, num_sample, dim_h)

あとは構築したネットワークを引数を与えて呼び出すだけです:

# Y:出力, hn:最終の隠れ状態
Y, hn = model(X, h0)

これで推論させることができました!

出力内容の確認

実際の出力内容を見てみましょう:

print(Y)
print(hn)

Yには時系列(ここでは長さ5)に対応するすべての出力がまとめて格納されていることが読み取れます。
hnはここでは隠れ状態の次元と同じ,4次元ベクトルですね。

tensor([[[-0.5985, -0.5996,  0.3071,  0.8372],
         [-0.6360,  0.3991,  0.8016,  0.2106]],

        [[-0.7843,  0.2241,  0.6805,  0.4661],
         [-0.7905, -0.2255, -0.6491,  0.7182]],

        [[-0.8067,  0.1700,  0.4610,  0.5975],
         [-0.4348, -0.5121, -0.6546,  0.0738]],

        [[-0.8926,  0.2469, -0.3201,  0.8214],
         [-0.8078, -0.3981,  0.2478,  0.9111]],

        [[-0.7735,  0.3182,  0.7366,  0.5397],
         [-0.8512,  0.0490,  0.2640,  0.6170]]], grad_fn=<StackBackward>)

tensor([[[-0.7735,  0.3182,  0.7366,  0.5397],
         [-0.8512,  0.0490,  0.2640,  0.6170]]], grad_fn=<StackBackward>)

YにSoftmax関数を通せば,各サンプルの各時系列のベクトルの成分を確率に変換することができます:

import torch.nn.functional as F
Y_prob = F.softmax(Y, dim=2)
print(Y_prob)

たしかに各行の合計が1になり,正規化されていることがわかります:

tensor([[[0.1153, 0.1152, 0.2851, 0.4845],
         [0.0965, 0.2718, 0.4065, 0.2251]],

        [[0.0865, 0.2371, 0.3743, 0.3021],
         [0.1186, 0.2087, 0.1366, 0.5361]],

        [[0.0886, 0.2354, 0.3149, 0.3610],
         [0.2277, 0.2108, 0.1828, 0.3787]],

        [[0.0873, 0.2730, 0.1548, 0.4849],
         [0.0913, 0.1375, 0.2622, 0.5091]],

        [[0.0818, 0.2437, 0.3703, 0.3041],
         [0.0922, 0.2267, 0.2811, 0.4001]]], grad_fn=<SoftmaxBackward>)

2. LSTMを組む

2-1. ネットワークをつくる

LSTMもRNNとほぼ全く同じように組むことができます:

import torch
import torch.nn as nn

# 入力2次元, 隠れ状態3次元, 1層のLSTM
dim_i, dim_h, num_l = 2, 3, 1
model = nn.LSTM(input_size=dim_i, hidden_size=dim_h, num_layers=num_l)

双方向LSTM, 多層LSTMもRNNのときと同様です:

# 入力5次元, 隠れ状態8次元, 1層の双方向LSTM
model2 = nn.LSTM(input_size=5, hidden_size=8, num_layers=1, bidirectional=True)

# 入力3次元, 隠れ状態6次元, 2層のLSTM
model3 = nn.LSTM(3, 6, 2)

2-2. LSTMコンストラクタの引数

torch.nn.LSTMのコンストラクタに入れることのできる引数は以下のとおりです。
RNNのコンストラクタとほぼ変わりありません。
RNNとの違いは活性化関数を指定する項目がない点くらいでしょう。

model = torch.nn.LSTM(input_size, hidden_size, num_layers=1, bias=True, batch_first=False, dropout=0, bidirectional=False)

    input_size: int -> 入力ベクトルの次元数
    hidden_size: int -> 隠れ状態の次元数
    *num_layers: int -> LSTMの層数。多層にしたいときは2以上に
    *bias: bool -> バイアスを使うかどうか
    *batch_first: bool
    *dropout: float -> 途中の隠れ状態にDropoutを適用する確率
    *bidirectional: bool -> 双方向LSTMにするかどうか

2-3. LSTMのパラメーターの確認

LSTMのパラメーターもまた,ネットワークを組んだ時点で適当な値で初期化されています。
現在のパラメーターの様子をみてみましょう:

for param_name, param in model.named_parameters():
    print(param_name, param)

次のような出力が得られるはずです:

weight_ih_l0 Parameter containing:
tensor([[ 0.2848, -0.1366],
        [-0.5757,  0.2086],
        [ 0.4995,  0.4271],
        [-0.1745, -0.3294],
        [ 0.4708, -0.0210],
        [ 0.4829, -0.4076],
        [ 0.4412,  0.3948],
        [ 0.4969, -0.0128],
        [ 0.4600,  0.4799],
        [ 0.3268,  0.2755],
        [ 0.2120,  0.0517],
        [ 0.1208, -0.1436]], requires_grad=True)
weight_hh_l0 Parameter containing:
tensor([[-0.0824,  0.3834, -0.0103],
        [ 0.5396,  0.3769,  0.1899],
        [-0.4365, -0.5241, -0.2395],
        [ 0.4210, -0.5123,  0.1195],
        [-0.3324,  0.2434,  0.3067],
        [-0.2196,  0.3060, -0.3943],
        [ 0.1774, -0.2787,  0.0273],
        [-0.2064, -0.4244, -0.0538],
        [ 0.1785,  0.0495,  0.4612],
        [ 0.1111,  0.4128,  0.5325],
        [ 0.0116, -0.2142,  0.3397],
        [ 0.2183, -0.2899,  0.1467]], requires_grad=True)
bias_ih_l0 Parameter containing:
tensor([ 0.2030, -0.3873,  0.5769, -0.3200,  0.0116, -0.0453, -0.5763, -0.0194,
        -0.1736, -0.0692,  0.2100, -0.0362], requires_grad=True)
bias_hh_l0 Parameter containing:
tensor([ 0.1686, -0.3883, -0.3789, -0.3639,  0.1766,  0.0311, -0.4657,  0.3933,
        -0.0357,  0.2844,  0.3898,  0.3525], requires_grad=True)

これらの出力は次のようなことを意味します。
LSTMの隠れ層で行われているのは次のような演算でした:

i_{t}=\sigma ((W_{ii}x_{t}+b_{ii})+(W_{hi}h_{t-1}+b_{hi}))\\
f_{t}=\sigma ((W_{if}x_{t}+b_{if})+(W_{hf}h_{t-1}+b_{hf}))\\
g_{t}=tanh((W_{ig}x_{t}+b_{ig})+(W_{hg}h_{t-1}+b_{hg}))\\
c_{t}=f_{t}\cdot c_{t-1} + i_{t}\cdot g_{t}\\
o_{t}=\sigma ((W_{io}x_{t}+b_{io})+(W_{ho}h_{t-1}+b_{ho}))\\
h_{t}=o_{t}\cdot tanh(c_{t})

LSTMのパラメーターとして扱われるのは$W_{ii}$,$W_{if}$,$W_{ig}$,$W_{io}$,$W_{hi}$,$W_{hf}$,$W_{hg}$,$W_{ho}$,$b_{ii}$,$b_{if}$,$b_{ig}$,$b_{io}$,$b_{hi}$,$b_{hf}$,$b_{hg}$,$b_{ho}$の16の行列もしくはベクトルです。

上の出力例では,
weight_ih_l0 Parameterに$W_{ii}$,$W_{if}$,$W_{ig}$,$W_{io}$がまとめて格納されて出力され,
weight_hh_l0 Parameterに$W_{hi}$,$W_{hf}$,$W_{hg}$,$W_{ho}$がまとめて格納されて出力され,
bias_ih_l0 Parameterに$b_{ii}$,$b_{if}$,$b_{ig}$,$b_{io}$がまとめて格納されて出力され,
bias_hh_l0 Parameterに$b_{hi}$,$b_{hf}$,$b_{hg}$,$b_{ho}$がまとめて格納されて出力されています。

2-4. LSTMに推論させる

このLSTMに何かを入力して,何らかの出力を得てみましょう。
もちろんこのLSTMは初期化された状態のままであり,一切の学習を行なっていないため,でたらめな値を吐き出します。

n_sample = 2
seq_length = 5

# 入力
X = torch.randn(seq_length, n_sample, dim_input)
# 初期の隠れ状態
h0 = torch.randn(n_layer, n_sample, dim_hidden)
# 初期のメモリセル
c0 = torch.randn(n_layer, n_sample, dim_hidden)

# Y:出力, hn:最終の隠れ状態, cn:最終のメモリセル
# (h0,c0)を省略するとh0,c0には零ベクトルが代入される
Y, (hn,cn) = lstm(X, (h0,c0))
print(Y, hn, cn, sep='\n')

次のような出力が得られるはずです:

tensor([[[-0.0608,  0.0157, -0.3091],
         [-0.1908,  0.1270, -0.0131]],

        [[-0.0604,  0.1197, -0.2682],
         [-0.1019,  0.1923, -0.1177]],

        [[-0.0411, -0.0321, -0.2204],
         [-0.1566,  0.3992,  0.1179]],

        [[-0.0693,  0.0297, -0.1263],
         [-0.0999,  0.4723,  0.2208]],

        [[-0.0499,  0.2873,  0.0223],
         [-0.1095,  0.2102,  0.2421]]], grad_fn=<StackBackward>)
tensor([[[-0.0499,  0.2873,  0.0223],
         [-0.1095,  0.2102,  0.2421]]], grad_fn=<StackBackward>)
tensor([[[-0.0972,  0.4610,  0.0448],
         [-0.2396,  0.3879,  0.4673]]], grad_fn=<StackBackward>)

3. GRUを組む

3-1. ネットワークをつくる

GRUもRNNとほぼ全く同じように組むことができます:

import torch
import torch.nn as nn

# 入力2次元, 隠れ状態3次元, 1層のGRU
dim_input, dim_hidden, n_layer = 2, 3, 1
model = nn.GRU(dim_input, dim_hidden, n_layer)

双方向GRU, 多層GRUもRNNのときと同様です:

# 入力5次元, 隠れ状態8次元, 1層の双方向GRU
model2 = nn.GRU(5, 8, 1, bidirectional=True)

# 入力3次元, 隠れ状態6次元, 2層のGRU
model3 = nn.GRU(3, 6, 2)

3-2. GRUコンストラクタの引数

torch.nn.GRUのコンストラクタに入れることのできる引数は以下のとおりです。
RNNのコンストラクタとほぼ変わりありません。
RNNとの違いは活性化関数を指定する項目がない点くらいでしょう。

model = torch.nn.GRU(input_size, hidden_size, num_layers=1, bias=True, batch_first=False, dropout=0, bidirectional=False)

    input_size: int -> 入力ベクトルの次元数
    hidden_size: int -> 隠れ状態の次元数
    *num_layers: int -> GRUの層数。多層にしたいときは2以上に
    *bias: bool -> バイアスを使うかどうか
    *batch_first: bool
    *dropout: float -> 途中の隠れ状態にDropoutを適用する確率
    *bidirectional: bool -> 双方向GRUにするかどうか

3-3. GRUのパラメーターの確認

GRUのパラメーターもまた,ネットワークを組んだ時点で適当な値で初期化されています。
現在のパラメーターの様子をみてみましょう:

for param_name, param in model.named_parameters():
    print(param_name, param)

次のような出力が得られるはずです:

weight_ih_l0 Parameter containing:
tensor([[ 0.3526,  0.2047],
        [ 0.5587,  0.3424],
        [ 0.3634,  0.0156],
        [-0.0418,  0.1831],
        [-0.4068,  0.4114],
        [-0.5638, -0.2389],
        [-0.1970,  0.0833],
        [-0.2401, -0.2788],
        [ 0.4896, -0.2670]], requires_grad=True)
weight_hh_l0 Parameter containing:
tensor([[ 0.1349, -0.0746, -0.4713],
        [-0.1361, -0.4540, -0.0641],
        [-0.1179, -0.3632, -0.2545],
        [ 0.1209,  0.5216,  0.2496],
        [ 0.2362,  0.1309,  0.1757],
        [ 0.0641, -0.4424,  0.0094],
        [ 0.0433, -0.2761,  0.3010],
        [-0.3071, -0.0923, -0.2459],
        [ 0.2349,  0.3862,  0.5465]], requires_grad=True)
bias_ih_l0 Parameter containing:
tensor([ 0.5506,  0.4258,  0.0540, -0.3532, -0.5515, -0.3412, -0.1674,  0.2784,
        -0.2394], requires_grad=True)
bias_hh_l0 Parameter containing:
tensor([ 0.1500,  0.2692,  0.2734,  0.1079,  0.2887, -0.5322,  0.1495, -0.0939,
         0.2837], requires_grad=True)

これらの出力は次のようなことを意味します。
GRUの隠れ層で行われているのは次のような演算でした:

r_{t}=\sigma ((W_{ir}x_{t}+b_{ir})+(W_{hr}h_{t-1}+b_{hr}))\\
z_{t}=\sigma ((W_{iz}x_{t}+b_{iz})+(W_{hz}h_{t-1}+b_{hz}))\\
n_{t}=tanh(r_{t}\cdot (W_{hn}h_{t-1}+b_{hn})+(W_{in}x_{t}+b_{in}))\\
h_{t}=(1-z_{t})\cdot n_{t} + z_{t}\cdot h_{t-1}

GRUのパラメーターとして扱われるのは$W_{ir}$,$W_{iz}$,$W_{in}$,$W_{hr}$,$W_{hz}$,$W_{hn}$,$b_{ir}$,$b_{iz}$,$b_{in}$,$b_{hr}$,$b_{hz}$,$b_{hn}$の12の行列もしくはベクトルです。

上の出力例では,
weight_ih_l0 Parameterに$W_{ir}$,$W_{iz}$,$W_{in}$がまとめて格納されて出力され,
weight_hh_l0 Parameterに$W_{hr}$,$W_{hz}$,$W_{hn}$がまとめて格納されて出力され,
bias_ih_l0 Parameterに$b_{ir}$,$b_{iz}$,$b_{in}$がまとめて格納されて出力され,
bias_hh_l0 Parameterに$b_{hr}$,$b_{hz}$,$b_{hn}$がまとめて格納されて出力されています。

3-4. GRUに推論させる

このGRUに何かを入力して,何らかの出力を得てみましょう。
もちろんこのGRUは初期化された状態のままであり,一切の学習を行なっていないため,でたらめな値を吐き出します。

n_sample = 2
seq_length = 5

# 入力
X = torch.randn(seq_length, n_sample, dim_input)
# 初期の隠れ状態
h0 = torch.randn(n_layer, n_sample, dim_hidden)

# Y:出力, hn:最終の隠れ状態
# h0を省略するとh0には零ベクトルが代入される
Y, hn = gru(X, h0)
print(Y, hn, sep='\n')

次のような出力が得られるはずです:

tensor([[[ 0.5822, -0.4229,  0.4796],
         [ 0.0419,  0.0228,  0.4883]],

        [[ 0.0915,  0.1967,  0.6127],
         [ 0.0900, -0.0312, -0.0792]],

        [[ 0.0548, -0.0761,  0.2387],
         [-0.0706, -0.1772,  0.1976]],

        [[ 0.0956,  0.1815, -0.1227],
         [-0.1522,  0.1317,  0.4051]],

        [[-0.0466, -0.0572,  0.0450],
         [-0.3013, -0.1203,  0.7980]]], grad_fn=<StackBackward>)
tensor([[[-0.0466, -0.0572,  0.0450],
         [-0.3013, -0.1203,  0.7980]]], grad_fn=<StackBackward>)

4. より複雑なネットワークを組むには(1)

ここまで,RNN,LSTM,GRUがPyTorchのモジュールを1つ使うだけで簡単に組めることがわかりました。

4-1.はじめに

PyTorchでネットワークを組む方法にはいくつかの方法があります:

  • a. 既存のモジュールを1つ使う(これまでのように)
  • b. 既存のモジュールを複数組み合わせる
  • c. 自分で独自のモジュールを定義する

a.の「既存のモジュールを1つ使う」はこれまでに試してきた通りです。
RNN, LSTM, GRUについては,内部処理を記述することなくモジュール1つで簡単にネットワークを組むことができます。

c.の「自分で独自のモジュールを定義する」はもっとも自由度の高いカスタマイズが可能ですが,PyTorchへの理解を深める必要がありそうです。

ここでは比較的簡単な,b.の「既存のモジュールを複数組み合わせる」方法についてみていきましょう。

4-2.既存のモジュールを複数組み合わせるには

以下,時系列ネットワークからは離れた話になることをご了承ください。
たとえば,次のようなCNN(の一部分)を組みたいとします:

スクリーンショット 2019-05-26 16.35.29.png

Yann LeCun et al. Object Recognition with Gradient-Based Learning

上図は画像認識へのCNNの普及のきっかけとなったLeNetの論文から引用した図です。

このうち,全結合層の手前にあたる部分に似せた,下記を実装してみようと思います:

  • 畳み込み層1
    • 入力チャネル数1, 出力チャネル数6, カーネルサイズ5x5
  • MaxPooling層1
    • カーネルサイズ2x2
  • 活性化(ReLU)1
  • 畳み込み層2
    • 入力チャネル数6, 出力チャネル数16, カーネルサイズ5x5
  • MaxPooling層2
    • カーネルサイズ2x2
  • 活性化(ReLU)2

どのようにすればこれらを1つのネットワークとして表現できるでしょうか?

方法1: 入出力を順次渡していく

畳み込み層,MaxPooling層,活性化はそれぞれが独立したモジュールとして提供されています。
このため,前の層の出力を次の層の入力へと順次渡していくことで一連のネットワークとしての処理が可能です:

import torch
import torch.nn as nn

# 入力
# requires_grad=Trueとしておくとのちほど訓練しやすい
# Xを使って計算されたテンソルをすべて追跡して勾配計算を繋いでくれる
X = torch.randn(10, 1, 32, 32, requires_grad=True)

# モジュールを用意
conv1 = nn.Conv2d(1, 6, 5)
relu1 = nn.ReLU()
maxpool1 = nn.MaxPool2d(2)
conv2 = nn.Conv2d(6, 16, 5)
relu2 = nn.ReLU()
maxpool2 = nn.MaxPool2d(2)

# 推論させたいときは
out = conv1(X)
out = relu1(out)
out = maxpool1(out)
out = conv2(out)
out = relu2(out)
Y = maxpool2(out)

ただし,のちほど学習を進める際,勾配や重みなどのパラメーター全体を統一して扱いにくい印象があります。

方法2: モデルをひとまとめにする

PyTorchには複数のモジュールを1つのモジュールへとまとめるためのtorch.nn.Sequentialが用意されており,まとめた後のモジュールを用いて一括で推論を行うことができます:

# conv1, relu1, maxpool1, conv2, relu2, maxpool2を用意するところまでは方法1と同じ

# 一連の処理を1つのモデルにまとめる
model = nn.Sequential(conv1, relu1, maxpool1, conv2, relu2, maxpool2)

# 推論させたいときは
Y = model(X)

もしくは,順序つき辞書OrderedDictを作成して代入しても構いません:

# 個々のモジュールを用意
from collections import OrderedDict
layers = OrderedDict([('conv1', nn.Conv2d(1,6,5)),('relu1',nn.ReLU()),('conv2',nn.Conv2d(6,16,5)),('relu2',nn.ReLU())])

# 1つのモデルにまとめる
model = nn.Sequential(layers)

# 推論させたいときは
Y = model(X)

この方法では,モデル全体のパラメーターがmodel.parameters()model.named_parameters()に一括で格納されるため,統一して扱いやすくなります。

しかし,細やかな処理が得意ではないのが最大の欠点です。

たとえば,CNNの全結合層を定義するにはテンソルをreshapeしてベクトルへとほどいてやる必要がありますが,reshapeの操作を行ってくれるレイヤーは存在しません。

このため,レイヤーの積み重ねであるnn.Sequentialでは,画像認識システムは完成させられないことになってしまいます
(参考:What is the reshape layer in pytorch?)。

4-3.torch.nnのモジュール

PyTorchのtorch.nnの内部に用意されているモジュールの例を以下に挙げます。
眺めていると,ネットワークを組むのに必要な道具がだいたい揃っているように思えるのではないでしょうか。
ポイントは,「単体でネットワークとして使えるもの」と,通常は単体では使用されることはなく「ネットワークの処理の一部を担うもの」の2種類に分けられる点です:

  • 単体でもネットワークとして使えるもの
    • 時系列ネットワーク
      • torch.nn.RNN
      • torch.nn.LSTM
      • torch.nn.GRU
  • 通常単体では使用されないもの(ネットワークの処理の一部を担う)
    • 行列計算を行うもの
      • torch.nn.Identity
      • torch.nn.Linear など
    • 活性化関数の適用を行うもの
      • torch.nn.ReLU
      • torch.nn.LeakyReLU
      • torch.nn.PReLU
      • torch.nn.LogSigmoid
      • torch.nn.Tanh
      • torch.nn.Hardtanh など
    • パディング(padding)を行うもの
      • torch.nn.ZeroPad2d
      • torch.nn.ConstantPad2d
      • torch.nn.ReflectionPad2d
      • torch.nn.ReplicationPad2d など
    • 畳み込み(convolution)を行うもの
      • torch.nn.Conv2d など
    • プーリング(pooling)を行うもの
      • torch.nn.MaxPool2d
      • torch.nn.AvgPool2d など
    • 正規化(normalization)を行うもの
      • torch.nn.BatchNorm2d
      • torch.nn.LocalResponseNorm など
    • ドロップアウト(dropout)を行うもの
      • torch.nn.Dropout2d など
    • Softmax関数などの適用を行うもの
      • torch.nn.Softmax など
    • 画像処理を行うもの
      • torch.nn.PixelShuffle
      • torch.nn.Upsample など
    • 時系列ネットワークの部品を提供するもの
      • torch.nn.RNNCell
      • torch.nn.LSTMCell
      • torch.nn.GRUCell

5. より複雑なネットワークを組むには(2)

先ほど,複数のモジュールをつなぎ合わせる方法は,やはり柔軟性に欠けており,限界があることを確認しました。
次に,「自分で独自のモジュールを定義する」方法で,より自由なモデルを作成してみます。

独自のモジュールは次のように抽象クラスtorch.nn.Moduleを継承することで定義します:

import torch
import torch.nn as nn

class MyModule(nn.Module):
    def __init__(self):
        super().__init__() # 親クラス torch.nn.Module のコンストラクタを呼ぶ
        hogehoge  # モジュールの内容やパラメーターを定義
    def forward(self):
        fugafuga  # 推論するときの挙動を定義

このとき,forward()メソッドは必ずoverrideしないと例外NotImplementedErrorが返ることに注意してください。

6. まとめ

PyTorchのモジュールを用いて,ネットワークのごく大雑把な組み方を把握してみました。
実際のネットワークの学習に必要な,損失関数の計算,誤差逆伝播,パラメーター更新などはまだ扱っていません。
のちほど追って他の記事に載せようと思っています。

参考文献

PyTorch.org Package reference
#1 Neural Networks : PyTorchチュートリアルをやってみた

69
50
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
50