からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

7.4.3:Convolutionレイヤの実装【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」学習初手『ゼロから作るDeep Learning』民のための実装攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。

 この記事は、7.4.3項「Convolutionレイヤの実装」の内容になります。ConvolutionレイヤをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

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つ前の記事に含まれています。

【次節の内容】

www.anarchive-beta.com