前回の記事では、PyTorchでディープラーニングを実装するための基本フローを紹介しました。
今回はMNISTの分類問題をいろんなモデルで実装してみたいと思います。
実装例も合わせて各モデルの基本的な構築方法を紹介していきます。
Contents
[参考]ノートブックのリンク
実装するのは以下の4モデルです。各モデルを実装したノートブックのリンクも貼っておきますので参考にしてください。
- 全結合層 https://github.com/pometa0507/pytorch_mnist/blob/master/mnist_Dence.ipynb
- CNN https://github.com/pometa0507/pytorch_mnist/blob/master/mnist_CNN.ipynb
- RNN https://github.com/pometa0507/pytorch_mnist/blob/master/mnist_RNN.ipynb
- LSTM https://github.com/pometa0507/pytorch_mnist/blob/master/mnist_LSTM.ipynb
実装のフロー
まずは、PyTorchでの実装フローを確認します。(フローの詳細は前回の記事をご参照ください)

今回紹介する4つのモデルはどれも同じフローとなっております。モデルごとで異なるのはネットワークモデルの定義だけです。
そのため、この記事ではネットワークモデルの定義にフォーカスして説明していきます。
とはいえ、今回使用するデータとモデルの入出力に関しては復習も兼ねて記載しておきます。
それでは使用するデータについて。今回もMNISTの手書き数字を扱います。
一枚の画像データの形状は、Height = 28, Width = 28 であり、グレースケールのため Channel = 1となっています。

MNISTのデータセットであれば、PyTorchでデフォルトに用意されている関数を使ってDatasetを簡単に設定することができます。
また、必要な前処理は下記となります。
- ロードしたデータをPyTorchのTensor型に変換
- 標準化
MNISTのDatasetを読み込む方法や前処理の実装については、[参考]ノートブックのリンクで紹介したノートブックをご参照ください。
次にモデルの入出力を確認します。モデルの入出力は今回紹介する4つのモデルで共通となります。
モデルの入力はMNISTの画像です。テンソル形状は(バッチサイズ、チャンネル、高さ、幅)=(N, 1, 28, 28)となります。そして、10クラス分類なので出力の形状は(バッチサイズ、クラス数) = (N, 10)となります。

各ネットワークモデルの定義
ここから本題です。各モデルごとのネットワークモデルの定義をみていきます。
なお、モデルの細かい構造は一例として実装したものです。
全結合層 Fully connectedのモデル
まずは全結合層(fully connected)で構築したモデルの例です。
#全結合層のみのモデル
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(784, 100)
self.fc2 = nn.Linear(100, 100)
self.fc3 = nn.Linear(100, 10)
def forward(self, x):
x = x.view(-1,28*28) # (Batch, 28, 28) -> (Batch, 784)
x = F.relu(self.fc1(x)) # (Batch, 784) -> (Batch, 100)
x = F.relu(self.fc2(x)) # (Batch, 100) -> (Batch, 100)
x = self.fc3(x) # (Batch, 100) -> (Batch, 10)
return x
全結合層はnn.Linear()
で定義します。今回は全結合層が3層の構造となっています。

順伝播def forward(self, x):
では、まず最初にテンソル形状をx.view(-1,28*28)
で変換しています。全結合層では入出力のテンソル形状を(バッチサイズ、データサイズ)として扱うためです。
x.view(-1,28*28)
の括弧()内の28*28は画像データ(1, 28, 28)を一列の28*28に並べたベクトルとなります。また、-1は残りのデータのことで、ここではバッチサイズを表しています。
その後、x = F.relu(self.fc1(x))
では全結合層とReLUの処理を行っています。(この処理を2層繰り返しています。)
最後の層では、全結合層だけ(ReLUなし)になっています。
10クラス分類なので、最終的に出力されるテンソルの形状は(バッチサイズ, 10)となっています。
畳み込み層CNNのモデル
次は畳み込み層CNN(Convolutional Neural Network)で構築したモデルの例です。
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3, 1)
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.dropout1 = nn.Dropout2d(0.25)
self.dropout2 = nn.Dropout2d(0.5)
self.fc1 = nn.Linear(9216, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.conv1(x) # (Batch, 1, 28, 28) -> (Batch, 32, 26, 26)
x = F.relu(x)
x = self.conv2(x) # (Batch, 32, 26, 26) -> (Batch, 64, 24, 24)
x = F.relu(x)
x = F.max_pool2d(x, 2) # (Batch, 64, 24, 24) -> (Batch, 64, 12, 12)
x = self.dropout1(x)
x = torch.flatten(x, 1) # (Batch, 64, 12, 12) -> (Batch, 9216)
x = self.fc1(x) # (Batch, 9216) -> (Batch, 128)
x = F.relu(x)
x = self.dropout2(x)
x = self.fc2(x) # (Batch, 128) -> (Batch, 10)
return x
今回構築したモデルでは畳み込み層以外にも、ドロップアウト層、プーリング層そして全結合層も使用しています。

畳み込み層はnn.Conv2d()
で定義します。引数は入力チャンネル数、出力チャンネル数、カーネルサイズ、パディングサイズとなっています。
ドロップアウト層nn.Dropout2d()
は、必須の層ではありませんが過学習を防ぐ効果が期待できます。
今回使用しているプーリング層F.max_pool2d(x, 2)
では、マックスプーリングにより画像サイズを高さと幅それぞれ1/2に半減させています。
一連の畳み込み層のあとは、torch.flatten(x, 1)
でテンソル形状を一列のベクトルに変換しています。これはのちの全結合層に入力するためです。ちなみに、torch.flatten(x, 1)
の代わりにx.view(-1, 9216)
を使用しても同様の処理となります。
再帰型ニューラルネットワークRNNのモデル
次はシンプルなRNN(Recurrent Neural Network)モデルです。RNNは日本語で再帰型ニューラルネットワークと言います。
RNNは時系列データに対するモデルですが、今回のような画像データであっても時系列データとして扱うことでモデルを構築することができます。
MNISTの画像データで高さと幅が28×28のサイズとなっていますが、幅Widthを特徴量(feature)とし、高さHeightを時刻(Sequence)として扱います。

class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.seq_len = 28 # 画像の Height を時系列のSequenceとしてRNNに入力する
self.feature_size = 28 # 画像の Width を特徴量の次元としてRNNに入力する
self.hidden_layer_size = 128 # 隠れ層のサイズ
self.rnn_layers = 1 # RNNのレイヤー数 (RNNを何層重ねるか)
self.simple_rnn = nn.RNN(input_size = self.feature_size,
hidden_size = self.hidden_layer_size,
num_layers = self.rnn_layers)
self.fc = nn.Linear(self.hidden_layer_size, 10)
def init_hidden(self, batch_size): # RNNの隠れ層 hidden を初期化
hedden = torch.zeros(self.rnn_layers, batch_size, self.hidden_layer_size)
return hedden
def forward(self, x):
batch_size = x.shape[0]
self.hidden = self.init_hidden(batch_size)
x = x.view(batch_size, self.seq_len, self.feature_size) # (Batch, Cannel, Height, Width) -> (Batch, Height, Width) = (Batch, Seqence, Feature)
# 画像の Height を時系列のSequenceに、Width を特徴量の次元としてRNNに入力する
x = x.permute(1, 0, 2) # (Batch, Seqence, Feature) -> (Seqence , Batch, Feature)
rnn_out, h_n = self.simple_rnn(x, self.hidden) # RNNの入力データのShapeは(Seqence, Batch, Feature)
# (h_n) のShapeは (num_layers, batch, hidden_size)
x = h_n[-1,:,:] # RNN_layersの最後のレイヤーを取り出す (l, B, h)-> (B, h)
x = self.fc(x)
return x
RNNモデルは
で定義されます。引数は、input_sizeが特徴量の次元数、hidden_sizeが隠れ層(隠れ状態)の次元数、num_layersがRNNを何層重ねるかとなっています。nn.RNN()
RNNの入出力は下図のようになります。ポイントは入力が時系列データとして入力される点です。図中のx0~x27が時系列のデータとなります。

順伝播def forward(self, x):
において、RNNモデルnn.RNN()
に入力するテンソル形状は、(Seqence , Batch, Feature)となることに注意してください。テンソル形状を変換するために前段でx.permute(1, 0, 2)
を入れています。
RNNモデルnn.RNN()
の出力である隠れ層rnn_out
のうち、最後の時刻のものh_n
を次の全結合層へ入力させます。これはh_n
に全時刻分の情報が含まれていると考えられるからです。
ちなみにh_n[-1,:,:]
としているのは、RNNを複数重ねた場合に最後のRNNの出力を取り出す操作となっています。
再帰型ニューラルネットワークLSTMのモデル
最後にLSTM(Long short-term memory)のモデルです。LSTMはRNNの一種ですが、シンプルなRNNを改良して長期記憶ができるようになっています。
LSTMモデルもRNN同様に画像データを時系列データとして扱ってモデルを構築しています。
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.seq_len = 28 # 画像の Height を時系列のSequenceとしてLSTMに入力する
self.feature_size = 28 # 画像の Width を特徴量の次元としてLSTMに入力する
self.hidden_layer_size = 128 # 隠れ層のサイズ
self.lstm_layers = 1 # LSTMのレイヤー数 (LSTMを何層重ねるか)
self.lstm = nn.LSTM(self.feature_size,
self.hidden_layer_size,
num_layers = self.lstm_layers)
self.fc = nn.Linear(self.hidden_layer_size, 10)
def init_hidden_cell(self, batch_size): # LSTMの隠れ層 hidden と記憶セル cell を初期化
hedden = torch.zeros(self.lstm_layers, batch_size, self.hidden_layer_size)
cell = torch.zeros(self.lstm_layers, batch_size, self.hidden_layer_size)
return (hedden, cell)
def forward(self, x):
batch_size = x.shape[0]
self.hidden_cell = self.init_hidden_cell(batch_size)
x = x.view(batch_size, self.seq_len, self.feature_size) # (Batch, Cannel, Height, Width) -> (Batch, Height, Width) = (Batch, Seqence, Feature)
# 画像の Height を時系列のSequenceに、Width を特徴量の次元としてLSTMに入力する
x = x.permute(1, 0, 2) # (Batch, Seqence, Feature) -> (Seqence , Batch, Feature)
lstm_out, (h_n, c_n) = self.lstm(x, self.hidden_cell) # LSTMの入力データのShapeは(Seqence, Batch, Feature)
# (h_n) のShapeは (num_layers, batch, hidden_size)
x = h_n[-1,:,:] # lstm_layersの最後のレイヤーを取り出す (B, h)
x = self.fc(x)
return x
LSTMの実装は基本的にRNNと同じです。RNNと異なるところは記憶セルc_n
がある点です。

記憶セルはLSTMの内部で使用するもので、実際に次の全結合層への入力に使うのは、隠れ層の最後の時刻のh_n
となります。
なお、h_n[-1,:,:]
としているのは、RNN同様にLSTMを複数重ねた場合に最後のLSTMの出力を取り出す操作となっています。
おわり
今回は、全結合層のDence、畳み込み層のCNN、再帰型ニューラルネットワークのRNNとLSTMの4モデルを実装例を交えて紹介してみました。
解いているタスクはどれも同じMNISTの分類問題ですが、いろんなモデルでアプローチできるのが面白いですね。
この記事ではネットワークモデルの定義にフォーカスしていますが、[参考]ノートブックのリンクでは一連の動作(データの準備や学習・評価まで)を実装していますので参考にしてください。
なお、ノートブックの実装はこちらの書籍を参考にさせて頂きました。
以上です。最後までお読みいただきありがとうございました。