CNN の作成 – ゼロから作るDeep Learning を参考に

ゼロから作る Deep Learning を参考に CNN を作っているが、難しいのでメモ。

Forward

Convolution Layer の Forward 処理

CNN への入力は 4階テンソル。(Batch Number, Channel, Input Height, Input Width)。Mnist で100枚のバッチ数なら x (100, 1, 28, 28) のテンソルになる。

保持する重みデータはフィルターのこと。重みデータも4階テンソルになっており (Filter Number, Channel, Filter Height, Filter Wedth) になっている。枚数 30枚、チャンネル 1、フィルターサイズ 5×5 で定義されていれば w (30, 1, 5, 5) が Shape の値になる。Filter は 30枚で 5×5 なら、(30, 25) 行列に変換されて転置され (25×30) でレイヤーの変数として保持される。

Affine 変換するために input の値と Weight の値を2次元配列に変換して、np.dot(input, wight) + bias 計算をして、dot( (28800×25), (25×30)) の内積計算に落としこまれる。Bias の個数は、Filter Number と一致する。計算が終わったら reshape と transpose で適切なレイアウトに直している。

出力もまた4階テンソルになる。(Batch Number, Filter Number, Output Height, Output Width)。上の例なら out (100, 30, 24, 24) になる。

Pooling Layer

Convolution から出力された 4階テンソルの値は ReLU を通り Pooling 層に入る。Pooling 層ではサイズを半分に落とした値が保持される。Convolution で行われたように、一旦 matrix に変換した方が計算がしやすい。(100, 30, 24, 24) が入力されたら (100, 30, 12, 12) サイズまで落ちる。Pooling Layer で 4 -> 1 にした箇所の Index を保持しておき、Backward で使用する。

Flatten と Affine 変換

(100, 30, 12, 12) になったフィルターにアフィン変換を行う。その前に Flatten 処理を行い 0 番目の (12, 12) -> 1番目の (12, 12) -> 2番目の (12, 12) というメモリーの連続する並びに展開する。サイズは (100, 4320) の2階テンソルになり、(4320, 100) の重みとの dot を取る。

Backpropagation

Pooling Layer

Pooling Layer では更新が必要なパラメータは無い。入力層に送るだけ。連鎖率の計算として入力 (100, 30, 12, 12 ) が入ってくる。24px -> 12px にしているため、出力は (100, 30, 24, 24) にしなければならない。

4階テンソルの入力から、np.zeros((dout 要素数, 4)) で列方向を4倍した配列を作り、forward の時に最大値となった箇所に、微分の値を代入する。

メモリレイアウトの都合で仕方ないが、4次元配列の入力 -> 2次元配列 -> 5次元配列 -> 2次元配列の reshape を得て col2im を呼び出して (100, 30, 24, 24) に変換して次の層に戻している。複雑だな。。

Convolution

Backward への入力サイズは (100, 30, 24, 24) で出力と一緒。入力の4次元配列から、要素を変換して2次元配列に変換している。

自分の重みとバイアス dw, db を更新する。Convolution の Backward の出力は Stride や Padding なども考慮して col2im を経由して (100, 30, 28, 28) が出力される。

Convolution の場合、複数のチャンネルがある場合は、チャンネル位置をの transpose 処理が複数入るため、分かりづらい。。

im2col と col2im

im2col と col2im は逆写像にはなっていない。つまり im2col で変換した値を、col2im に入れても元には戻らない。理由は im2col で画像から展開した時に、同一ピクセルは複数箇所に展開されるため。連鎖律としては個々の要素として別々に計算されるので、その結果を集約するために加算されている。

こちらの図が非常に分かりやすかったです。素晴らしい記事に感謝です。
https://qiita.com/t-tkd3a/items/6b17f296d61d14e12953

Jupyter Notebook ショートカット

Command Mode (cell が選択されていない状態) で h を押すか、Help -> Keyboard shortcuts を押すとショートカットの一覧が表示される。

Jupyter Notebook は Command Mode (press Esc) と Edit Mode (press Enter) に分かれており、それぞれで使えるショートカットキーが異なる。

# 編集中セルの下に新しいセルを挿入
ESC + B

# 編集中セルの上に新しいセルを挿入
ESC + A

# 編集中セルを削除
ESC + D, D

Convolution の Padding に指定する SAME/VALID

Tensorflow の tf.nn.convolution などは padding という引数を取り、文字列で VALID か SAME のどちらかを指定する。なぜ SAME/VALID という名前なのだろうか。。

Tensorflow の Document に計算式が載っている。
https://www.tensorflow.org/api_docs/python/tf/nn/convolution

SAME を指定すると 0 パディング が行われて、(ストライドの値にもよるが) 入力が小さくならずに、同じサイズで出力される。VALID にすると、足りない分はサイズが小さくなっていく。

英語に疎くてあれなのですが、SAME だと入力サイズと同じになるという意味で使われるのだとしたらまだ分かるのですが、なぜ Padding が Valid (有効) だと、パディングされない挙動になるのでしょうか?

TensorFlow – 畳み込み演算の関数 tf.nn.conv2d
https://blog.logicky.com/2017/01/18/tensorflow-%E7%95%B3%E3%81%BF%E8%BE%BC%E3%81%
ちょっとしたコードを確認するのに凄く参考になります。

Numpy の配列アクセス方法

a = np.arange(18).reshape(2, 3, 3)
という指定を行うと、下記のような 18個の (2, 3, 3) 配列が返る。

a もしくは a[:, :, :] でアクセスする。[:, :, :] って凄い指定の仕方。。

array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8]],

       [[ 9, 10, 11],
        [12, 13, 14],
        [15, 16, 17]]])

この結果に対して a[0] もしくは a[0, :, :] でアクセスすると

array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])

a[1][0] もしくは a[1, :, :] にアクセスすると、

array([ 9, 10, 11])

になる。

配列の範囲指定 [:]

[:, :, :] は範囲指定を行える。[begin:end] 指定。t = np.arange([2, 3, 4, 5, 6, 7]) で t[2:5] とした場合は、index が 2, 3, 4 までの値 array([4, 5, 6]) が返る。

a と a[:, :, :] と a[0:2, 0:3, 0:3] は全て同じ結果になる。a[0, :, :] にアクセスすると [[ 0, 1, 2], [3, 4, 5], [6, 7, 8]] が返る。a[:, 1, :] にアクセスすると [[ 3, 4, 5], [12, 13, 14]] が返る。3次元配列の 2つ目が 1 で固定されるため。a[:, :, 0] にアクセスすると [[ 0, 3, 6], [ 9, 12, 15]] が返る。3要素のベクトルの先頭 [0] のみが返るため。

最後の要素にアクセス

a[-1, -1, -1] という風に -1 を指定すると、値としては 17 が帰ってくる。-1 は配列要素の最後の要素。

転置行列

a.transpose(0, 2, 1) という風に transpose() と入れ替える軸を指定すると、この結果は下記になる。1, 2 -> 2, 1 に順番にしたので転置されてる。

array([[[ 0,  3,  6],
        [ 1,  4,  7],
        [ 2,  5,  8]],

       [[ 9, 12, 15],
        [10, 13, 16],
        [11, 14, 17]]])

一次元化

a.flatten() 命令を使用すると、ndim 1 の 1次元ベクトルになる。

array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17])

よく使う Numpy の関数

np.random.randn()

平均 0, 分散 1 の正規分布 (normal distribution) に従う乱数値を返す。引数には任意の次元を指定できる。randn(d0, d1, …, dn) を指定することができ、仮に randn(2, 2) とすれば、4つの乱数が返る。

np.zeros()

0 で初期化された配列を返す。zeros((2,2)) とすれば、要素4つ、shape が (2,2) ndim 2 の配列が返す。

np.random.choice()

第一引数は乱数の最大値、第二引数は個数。(5, 20) と引数に指定した場合は、5 以下 (0~4) までの乱数が 20 個返る。

np.pad()

指定した変数の前後にパディングを追加出来る。np.pad([5,6], (2, 3), ‘constant’) みたいに指定すれば、array([0, 0, 5, 6, 0, 0, 0]) という要素が返る。5 の前に 2個、6 の後に 3個のパディングが追加される。

Keras のサンプル

Github Keras Examples
https://github.com/keras-team/keras/tree/master/examples

公式のサンプルプログラム

MNIST の CNN

サンプルコードはここにある。
https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py

Summary を出力するとこのようになる。

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 24, 24, 64)        18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 12, 12, 64)        0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 12, 12, 64)        0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 9216)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 128)               1179776   
_________________________________________________________________
dropout_2 (Dropout)          (None, 128)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 10)                1290      
=================================================================
Total params: 1,199,882
Trainable params: 1,199,882
Non-trainable params: 0
_________________________________________________________________

Conv2D() で畳み込みレイヤーの追加。
Conv2D filters 32, kernel size (3, 3) -> ReLU
Conv2D filters 64, kernel size (3, 3) -> ReLU
MaxPooling (2, 2)

(28, 28, 1) の input を、32 filters, size(3, 3) の Conv2D に入力すると、(26, 26, 32) になる。(26, 26, 32) を 64 filters, size(3, 3) に入れれば、(24, 24, 64) になる。

KerasのConv2Dの行列式演算
https://qiita.com/nishiha/items/bfd5dfcd7fffd3c529bc

Keras の Flatten の実装

How flatten layer works in keras?
https://stackoverflow.com/questions/44176982/how-flatten-layer-works-in-keras

どのように展開されるか疑問だったので、凄く分かりやすかった。

Conv2DTranspose

KerasのConv2DTransposeの動作について
https://qiita.com/takurooo/items/9a9f387390f5fcf5a516

An Introduction to different Types of Convolutions in Deep Learning
https://towardsdatascience.com/types-of-convolutions-in-deep-learning-717013397f4d

How to use the UpSampling2D and Conv2DTranspose Layers in Keras
https://machinelearningmastery.com/upsampling-and-transpose-convolution-layers-for-generative-adversarial-networks/

分かりやすい記事に感謝。

Skip Connection U-Net

オートエンコーダーとしてのU-Net
https://qiita.com/koshian2/items/603106c228ac6b7d8356

Affine 変換の Backpropagation メモ

    def backward(self, dout): # dout (3x10)
        dx = np.dot(dout, self.W.T) # W (50x10) W.T (10x50)
        self.dW = np.dot(self.x.T, dout) # x (3x50) x.T (50x3)
        self.db = np.sum(dout, axis=0)
        
        dx = dx.reshape(*self.original_x_shape)
        return dx

self.x は affine 変換する前のレイヤーに入力される値。

全然関係無いですが、ニューラルネットワークをレイヤーの概念図で表すと、入力する値の位置とアフィン変換する位置が、コードと一階層(半階層)ズレてる感覚を受ける。コードで記述する時は、レイヤーの層を表す図から左側の接続の線までを含んでいるイメージを持つようにする。

自身のレイヤーの重みの勾配は np.dot(self.x.T, dout) で求まり、バイアスは np.sum(dout) で総和を取る。

参照コード
ゼロから作る Deep Learningn から

HLSL ByteAddressBuffer の使い方

ByteAddressBuffer は引数にバイト数を指定する。

ByteAddressBuffer byteBuffer : register(t0);
uint value = byteBuffer.Load(0);

4 Byte 単位でメモリーにアクセスしてデータを取得出来るバッファータイプ。戻り値は uint (4 Byte) になる。Load 関数の引数は 4 の倍数でなければいけない。Load2(0) で指定すれば uint2 型で 8 Byte の連続した領域を取得できる。Load3(0) で指定すれば uint3 型で 12 Byte の連続したメモリー。

uint4 value = byteBuffer.Load4(4);
という指定をすれば、アドレスの開始位置が先頭から 4 Byte 目から、16 Byte の連続した領域を取得出来る。