はじめに
「プログラミング」学習初手『ゼロから作るDeep Learning』民のための実装攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。
この記事は、7.4.3項「Convolutionレイヤの実装」の内容になります。ConvolutionレイヤをPythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
7.4.3 Convolutionレイヤの実装
Convolutionレイヤ(畳み込み演算)の概念的な説明は、7.2節を読んでください。また処理の内容については、「7.4.1-2:im2colの実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」で確認しました。この項では実装します。
・順伝播
まずは処理の効率化のため、4次元の入力データとフィルターの重みを2次元(行列)に展開します。これの処理は7.4.2項で実装したim2col()
を使います。
Convolutionレイヤの順伝播では、2次元に展開した入力データとフィルターの重みの行列の積にバイアスを加えます。つまりim2col()
で展開したデータに対して、Affineレイヤの順伝播と同じ計算をします。この処理は前項で確認しました。展開しない場合は、各要素の積の和をとります(7.2.5項)。
その計算結果を4次元に再変換して出力します。
・逆伝播
逆伝播についても、基本的な処理はAffineレイヤの逆伝播と同様に求められます。詳しくは「5.6.2:Affineレイヤの実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」を確認してください。
2次元に展開した入力データと重み(フィルター)、バイアスに関する勾配を、式(5.13)により求めます。こちらは計算結果をcol2im()
で4次元に変換して出力します。col2im()
についても前項を確認してください。
・実装
前項で確認した処理をまとめて、Convolutionレイヤを実装します。
# Convolutionレイヤの実装 class Convolution: # インスタンス変数の定義 def __init__(self, W, b, stride=1, pad=0): self.W = W # フィルター(重み) self.b = b # バイアス self.stride = stride # ストライド self.pad = pad # パディング # (逆伝播時に使用する)中間データを初期化 self.x = None # 入力データ self.col = None # 2次元配列に展開した入力データ self.col_W = None # 2次元配列に展開したフィルター(重み) # 勾配に関する変数を初期化 self.dW = None # フィルター(重み)に関する勾配 self.db = None # バイアスに関する勾配 # 順伝播メソッドの定義 def forward(self, x): # 各データに関するサイズを取得 FN, C, FH, FW = self.W.shape # フィルター N, C, H, W = x.shape # 入力データ out_h = int(1 + (H + 2 * self.pad - FH) / self.stride) # 出力データ:式(7.1) out_w = int(1 + (W + 2 * self.pad - FW) / self.stride) # 各データを2次元配列に展開 col = im2col(x, FH, FW, self.stride, self.pad) # 入力データ col_W = self.W.reshape(FN, -1).T # フィルター # 出力の計算:(図7-12) out = np.dot(col, col_W) + self.b out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) # (逆伝播時に使用する)中間データを保存 self.x = x self.col = col self.col_W = col_W return out # 逆伝播メソッドの定義 def backward(self, dout): # フィルターに関するサイズを取得 FN, C, FH, FW = self.W.shape # 順伝播の入力を展開 dout = dout.transpose(0,2,3,1).reshape(-1, FN) # 各パラメータの勾配を計算:式(5.13) self.db = np.sum(dout, axis=0) # バイアス self.dW = np.dot(self.col.T, dout) # (展開した)重み self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW) # 本来の形状に変換 dcol = np.dot(dout, self.col_W.T) # (展開した)入力データ dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad) # 本来の形状に変換 return dx
2次元配列に対する.transpose(1, 0)
は、転置.T
と同じことです。ただし変数W
の次元数などを明示できます。
W = np.array([[1, 2, 3], [4, 5, 6]]) print(W) print(W.T) print(W.transpose(1, 0))
[[1 2 3]
[4 5 6]]
[[1 4]
[2 5]
[3 6]]
[[1 4]
[2 5]
[3 6]]
では動作確認をしておきます。フィルターの重みとバイアスを作成します。
# フィルター数を指定 filter_num = 3 # 入力データのチャンネルを指定 C = 3 # フィルターの高さを指定 filter_h = 5 # フィルターの横幅を指定 filter_w = 5 # フィルターの重みをランダムに生成 W = np.random.rand(filter_num, C, filter_h, filter_w) print(W.shape) # バイアスを生成 b = np.zeros(filter_num) print(b.shape)
(3, 3, 5, 5)
(3,)
バイアスの初期値は0です(1回試すだけだと加える意味がないですが、あくまでCNN実装のための確認作業なので)。
Convolutionレイヤのインスタンスを作成します。
# インスタンスを作成 conv = Convolution(W, b, stride=1, pad=0)
入力データを作成します。横幅をW
にすると重みと変数名が被るので、この例では入力サイズに関する変数をinput_
とします。
# 入力データ数を指定 input_num = 10 # 入力データの高さを指定 input_h = 7 # 入力データの横幅を指定 input_w = 7 # 入力データをランダムに生成 X = np.random.rand(input_num, C, input_h, input_w) print(X.shape)
(10, 3, 7, 7)
順伝播メソッドで処理します。
# Convolutionレイヤの順伝播 A = conv.forward(X) print(A.shape)
(10, 3, 3, 3)
これが順伝播の処理になります。この出力は次の活性化レイヤの入力になります。
次は逆伝播の入力データを作成して、逆伝播メソッドで処理ます。
# 逆伝播の入力データを生成 dout = np.random.randn(*A.shape) print(dout.shape) # 逆伝播メソッド dx = conv.backward(dout) print(dx.shape)
(10, 3, 3, 3)
(10, 3, 7, 7)
逆伝播に必要なパラメータ類はインスタンス変数として保存されているので、逆伝播の処理はこれだけです。
以上で畳み込み演算を行うConvolutionレイヤの実装ができました。このレイヤの出力は、活性化レイヤに順伝播します。次項はその活性化レイヤの出力を入力とするPoolingレイヤを実装します。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.
おわりに
構成の都合で何だか短い記事になってしまいました。この項の内容の半分は1つ前の記事に含まれています。
【次節の内容】