多層パーセプロトロン#
このノートブックでは,線形層と活性化関数を定義して,多層パーセプトロンを構築する.多層パーセプトロン(Multi-Layer Perceptron, MLP) は,線形層による線形変換と活性化関数による非線形変換を組み合わせた層を複数重ねることで多層化し,入力から出力への複雑な非線形マッピングをモデル化する基本的な順伝播型ニューラルネットワークである.
MLPは一般的に,入力データを特定の次元に変換する入力層(input layer),線形・非線形変換を行う隠れ層(hidden layer),最終的な予測結果を出力する出力層(output layer)から構成される.隠れ層は中間層とも呼ばれる.MLPがこれらの層全てで \(L\) 層あるとき,入力データ \(\boldsymbol{x} \in \mathbb{R}^{B \times D_{in}}\) に対する第\(l\)層目(\(l=1,2,...,L\)) の計算を説明する.
まず,\(l=1\) である入力層では,入力データ \(\boldsymbol{h}_{0}=\boldsymbol{x}\) を与えられ,次のように線形層によって線形変換される.
ここで,\(D_{hidden}\) は隠れ層の特徴次元としたとき,重み行列 \(\boldsymbol{W}_1\) の次元は \(D_{hidden} \times D_{in}\) であり,バイアス \(\boldsymbol{b}_1\) は \(D_{hidden}\) 次元の縦ベクトルである.続いて,活性化関数を
と適用した後,計算結果 \(\boldsymbol{a}_1\) は第\(l=2\)層の隠れ層に伝播される.この層では,同様に異なる重みとバイアスを持つ線形層と活性化関数が次のように適用される.
ここでもやはり,重み行列とバイアスの次元に注意されたい.重み行列 \(\boldsymbol{W}_2\) は \(D_{hidden} \times D_{hidden}\) 次元であり,バイアス \(\boldsymbol{b}_2\) は \(D_{hidden}\) 次元である.ニューラルネットワーク実装時のエラーの大半はこの行列計算時の次元の不一致によるものなので注意されたい.
隠れ層ではこの計算を繰り返す.したがって,第 \(l\) 層の計算は,
と書ける.
そして,第\(L\)層目である出力層では,タスクに応じて適切に設定された出力次元 \(D_{out}\) を持つ重みとバイアス,そして活性化関数 \(f\) によって
と計算される.このとき,活性化関数が適用される直前のベクトル \(\boldsymbol{h}_{L}\) をロジットとよび,最終的な結果 \(\boldsymbol{a}_{L}\) を出力値または予測値という.
そして,ここで変換に利用されるすべてのパラメータ \(\boldsymbol{\theta} = \{\boldsymbol{W}_1, \boldsymbol{b}_1, ..., \boldsymbol{W}_L, \boldsymbol{b}_L \}\) は損失関数の勾配に基づいた誤差逆伝播法によって反復的に更新され,損失関数の誤差を最小とするように最適化される.
では,\(L=3\) の層数を持ち,隠れ層の活性化関数がReLU関数で,出力層の活性化関数はSigmoid関数の場合のMLPをPyTorchで実装する.
PyTorchでは torch.nn.Module でニューラルネットワークの構造をクラスとして次のように定義することが一般的である.その他の実装としては,torch.nn.Sequential を使うこともできる.
torch.nn.Module では,基本的に次のように実装する.
class モデル名(nn.Module):
def __init__(self):
super().__init__()
層の定義
def forward(self, x):
順伝播の実装
順伝播 forward を __init__ で定義した層を使って定義する.逆伝播はPyTorchの自動微分機能から実行できる.
以下に,前述したMLPを実装する.
import torch
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super(MLP, self).__init__()
self.layer_in = nn.Linear(input_dim, hidden_dim)
self.layer_hidden = nn.Linear(hidden_dim, hidden_dim)
self.layer_out = nn.Linear(hidden_dim, output_dim)
self.act = nn.ReLU()
self.act_out = nn.Sigmoid()
def forward(self, x):
h = x
h = self.layer_in(h)
h = self.act(h)
h = self.layer_hidden(h)
h = self.act(h)
h = self.layer_out(h)
h = self.act_out(h)
return h
これを input_dim=3,hidden_dim=32,output_dim=1 としてインスタンス化する.
input_dim = 3
hidden_dim = 32
output_dim = 1
model = MLP(input_dim=input_dim, hidden_dim=hidden_dim, output_dim=output_dim)
モデルの構造は print 等で確認できる.
print(model)
MLP(
(layer_in): Linear(in_features=3, out_features=32, bias=True)
(layer_hidden): Linear(in_features=32, out_features=32, bias=True)
(layer_out): Linear(in_features=32, out_features=1, bias=True)
(act): ReLU()
(act_out): Sigmoid()
)
出力結果からもわかるように定義された層と活性化関数は確認できるが,伝播の流れは得られないことに注意されたい.
MLPが正しく構築できているかバグチェックを行うために順伝播をさせる.そのためのランダムの値を持つ入力データを以下で作成する.
batch_size = 3
input_size = (batch_size, input_dim)
dummy_input = torch.randn(*input_size)
print('dummy_input.shape:', dummy_input.shape)
print('dummy_input = ')
print(dummy_input)
dummy_input.shape: torch.Size([3, 3])
dummy_input =
tensor([[-2.8992, 0.4750, -1.8886],
[ 0.0407, 0.6675, 0.7783],
[ 0.9085, 0.5863, -1.2934]])
順伝播は次のように行える.意図した出力が得られているか確認する.
output = model(dummy_input)
print('output.shape: ', output.shape)
print('output = ')
print(output)
output.shape: torch.Size([3, 1])
output =
tensor([[0.4411],
[0.4830],
[0.4701]], grad_fn=<SigmoidBackward0>)
このときエラーが発生する場合は forward 関数内の処理か線形層の入出力の次元を間違えていることが多い.
MLPのパラメータは model.named_parameters() を使って次のように取得できる.
for name, param in model.named_parameters():
print(f'{name} shape: {param.shape}')
layer_in.weight shape: torch.Size([32, 3])
layer_in.bias shape: torch.Size([32])
layer_hidden.weight shape: torch.Size([32, 32])
layer_hidden.bias shape: torch.Size([32])
layer_out.weight shape: torch.Size([1, 32])
layer_out.bias shape: torch.Size([1])
モデルの層数が多いとGPUのメモリ上にモデルのパラメータを格納できないことがある.そのため,正しく実装できたか,そしてそのニューラルネットワークがGPU上のメモリ量を超えていないかをチェックすることも重要である.
そこで,上記のコードをベースにMLPの総パラメータ数を計算する.
total_params = 0
for name, param in model.named_parameters():
print(f'{name} shape: {param.shape}')
total_params += param.numel()
print(f'Total number of trainable parameters: {total_params}')
layer_in.weight shape: torch.Size([32, 3])
layer_in.bias shape: torch.Size([32])
layer_hidden.weight shape: torch.Size([32, 32])
layer_hidden.bias shape: torch.Size([32])
layer_out.weight shape: torch.Size([1, 32])
layer_out.bias shape: torch.Size([1])
Total number of trainable parameters: 1217
メモリに乗り切らない場合は,モデル並列等を使ってモデルを分割して学習させることもできるが,基本的に,後述するバッチサイズなどを調整してメモリ数を減らすことが多い.
しかしながら,特に,線形層は多くのパラメータを必要とするのでこのあたりの次元を見直しても良い.