pythonでディープラーニングの実装第二弾です。前回の記事では画像認識の問題に対して、ディープニューラルネットワークを作りました。前回の記事はこちらからどうぞ。
![](https://masamunetogetoge.com/wp-content/uploads/cocoon-resources/blog-card-cache/298919bf7e28ccfb79807b771ce79821.jpg)
ニューラルネットワークの層をいくら深くしたとしてもあまり上手くいかないのは、画像のデータを1次元のベクトルとして扱っているからです。画像には、上下左右全ての方向に大事な情報があります。例えば人の顔は、大体真ん中に鼻があって、左右に耳、上下に目と口がありますが、画像を輪切りにして1次元にしてしまうとその情報の意味が薄れてしまいます。行列を1次元にして情報を失ってしまうのを防ぐために、畳み込みと呼ばれる手法を考え、それを実装します。畳み込みを使ったニューラルネットワークモデルを畳み込みニューラルネットワーク(CNN)と呼んだりします。
![レナ](https://masamunetogetoge.com/wp-content/uploads/2019/07/CNN.png)
記事で使っているソースコードはgithub に置いてあります。
https://github.com/msamunetogetoge
畳み込みとは
畳み込み(convolution)の説明をします。畳み込みは、行列と行列から行列を作る手法です。行列の掛け算と違う所は、行列同士なら大きさが違っても構わないというところです。畳み込みを\( \odot \)で表します。とりあえず具体例を計算してみます。
$$\begin{eqnarray}
\begin{pmatrix}
1 & 2& 3 \\
4 & 5 & 6 \\
1 & 2 &3
\end{pmatrix} \odot
\begin{pmatrix}
1 & 2 \\
1 & 0
\end{pmatrix} =
\begin{pmatrix}
9 & 13 \\
15 & 19
\end{pmatrix}
\end{eqnarray}$$
左辺の1項目が、入力データを想定しています。左辺の2項目をフィルターと呼びます。
右辺の\( (1,1 ) \)成分は、左辺1項目の 左上の\(2\times 2 \)行列とフィルターの成分同士を掛けて、それぞれを足すと出てきます。 \( (2,1) \) , \( (2,2) \)成分は、左辺1項目の左下、右下の行列を使います。
\( (1,1) \)成分の計算をもう一度書いてみます。
$$\begin{eqnarray}
\left(
\begin{array}{cc|c}
1 & 2& 3 \\
4 & 5 & 6 \\ \hline
1 & 2 &3
\end{array}
\right)
\odot
\begin{pmatrix}
1 & 2 \\
1 & 0
\end{pmatrix} =
1+4 +4+0
\end{eqnarray}$$
入力データの大きさと、フィルターの大きさで、出てくる行列の大きさが変わってきます。入力データを\( ( m,n )\) 行列 とし、フィルターを\( (a,b) \)行列とすると、出力の大きさ \( (h,w ) \) は以下で計算出来ます。
$$\begin{eqnarray}
h &=& m – a + 1 \\
w &=& n – b + 1
\end{eqnarray}$$
入力データが大きいとき、フィルターを掛ける行列の間隔を広げる事が出来ます。その間隔をストライドと言います。上の計算がストライド1です。データの周りに0を並べる事で、入力データを大きくすることもあります。この操作をパディングと言います。上の計算ではパディング0です。入力データはそのままで、フィルターを\( (3 \times 3 )\)行列にして、パディング1, ストライド2で計算してみましょう。1
$$\begin{eqnarray}
\begin{pmatrix}
0 & 0 & 0 & 0 & 0 \\
0 & 1 & 2 & 3 & 0 \\
0 & 4 & 5 & 6 & 0 \\
0 & 1 & 2 & 3 & 0 \\
0 & 0 & 0 & 0 & 0
\end{pmatrix} \odot
\begin{pmatrix}
1 & 2& 3 \\
0& 2 & 1 \\
1 & 2 &0
\end{pmatrix} =
\begin{pmatrix}
12 & 23 \\
27 & 18
\end{pmatrix}
\end{eqnarray}$$
入力データを\( ( m,n )\) 行列 とし、フィルターを\( (a,b) \)行列、 ストライドを\(s \) , パディングを\(p \) とすると、出力の大きさ \( (h,w ) \) は以下のように計算出来ます。
$$ \begin{eqnarray}
h &=& \frac{m – a + 2p}{s} +1 \\
w &=& \frac{n – b + 2p }{s} +1
\end{eqnarray}$$
ストライドの数は、上手く設定しないと計算が上手く出来ませんし、出力の次元が整数では無くなってしまいます。
入力データとなっていた行列が画像1枚のイメージです。画像がカラーの場合は、RGBの3種類のチャンネルを持っているので、畳み込み演算で扱うデータは( データ数 ,チャンネル数, 縦,横) の4次元ベクトルになります 2 。
畳み込みは、入力が行列を要素に持つベクトルでも機能します。ベクトル同士の足し算のようにベクトルの成分である行列毎で畳み込みを計算すれば良いです。これは、フィルターの枚数を増やしても同じです。決まりを作らないと計算で困ってしまうので、次元に関しては以下のように決めておきます。
$$\begin{eqnarray}
(N,C,H,W ) \odot (FN , C , FH , FW) = (N,FN, OH , OW )
\end{eqnarray}$$
ただし、F~はフィルター数、フィルターの大きさを表し、 OH, OWは出力の大きさです。チャンネルに関しては和を取って、出力の次元に出てこないようにします。
行列を掛けて、ベクトルを足すのと同じように、畳み込みを行った後に、ベクトル 3 を足すことが出来ます。畳み込み+ベクトルを足す操作を合わせて畳み込みと呼ぶことが多いです。
畳み込みの実装
畳み込みは、定義を伝えるのは簡単ですが、実際に計算するのは大変です。手で計算するのはしんどいですし、パソコンに計算させようとしてもfor 文の嵐になってしまいます。python でfor 文を大量に使うと計算が非常に重くなることは有名な話なので、何か工夫が必要です。
そこで、昔の偉い人が im2col という技を考えてくれました。4次元のデータを2次元に翻訳する技です。行列のデータが何層にも積み重なっているのが画像なので、頑張れば出来そうです。行列で与えられたデータを1つのデータ毎にベクトルに変換するのと同じことをします。
畳み込みの場合は、フィルターとストライドの大きさによってするべき計算が変わるので、フィルターやストライドありきの仕様にする必要があります。
以下の事をすれば良いです。
- フィルターを1個の成分だと思って、ストライド毎に入力データを分割する。
- 分割をデータ毎に行う。
- 出力が上手く出るようにデータを成形する。
具体的には、(データ数×出力大きさ) ×(フィルターの大きさ)の行列にする。
フィルターの大きさのベクトルは、行列をベクトルに直すのと同じようにする。
コードを書きます。例によってゼロから作るディープラーニングのサンプルコードです。
https://github.com/oreilly-japan/deep-learning-from-scratch
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
"""
Parameters
----------
input_data : (データ数, チャンネル, 高さ, 幅)の4次元配列からなる入力データ
filter_h : フィルターの高さ
filter_w : フィルターの幅
stride : ストライド
pad : パディング
Returns
-------
col : 2次元配列
"""
N, C, H, W = input_data.shape #データの形状
out_h = (H + 2*pad - filter_h)//stride + 1 #出力の縦
out_w = (W + 2*pad - filter_w)//stride + 1 #出力の横
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant') #必要ならパディング
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w)) #答えが入ってくる行列の準備, 出力の大きさも入れておく
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride] #ストライド毎にデータを区切る
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1) #出力の大きさ*(入力データの数)の行列に変換
return col
im2col が実装できたとすると、im2col(データ)×(フィルターを列ベクトルに持つ行列)を計算することで、畳み込みの計算が出来ます。勿論最後には計算結果を4次元の行列に直さなくてはいけません。具体的には、1行に\( OH×OW \)個のデータがあるので、それを\( OH \times OW \)型のデータにします。
im2colを使って、畳み込みを実装します。
class Convolution:
def __init__(self, W, b, stride=1, pad=0):#W:フィルター , b:バイアス
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):#x:入力画像
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = 1 + int((H + 2*self.pad - FH) / self.stride) #出力縦
out_w = 1 + int((W + 2*self.pad - FW) / self.stride) #出力横
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) #(N,FN, OH,OW ) に整形
return out
行列をベクトルに掛けるのと気持ちは同じなので、im2colがあれば簡単に実装出来ます。
画像の畳み込み
実際にどんな感じになるか使ってみましょう。ニューラルネットワークのアファイン層に通した時と、畳み込み層に通した時で比べてみます。コードを書きます。アファイン層については、前の記事を参照してください。
![](https://masamunetogetoge.com/wp-content/uploads/cocoon-resources/blog-card-cache/298919bf7e28ccfb79807b771ce79821.jpg)
コードを書きます。
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
train_images = train_images/255.0
plt.imshow(train_images[0],cmap=plt.cm.binary) #何もしてない画像の描画
plt.title("No Processing")
x=train_images[0].reshape(1,1,28,28)
cov=Convolution(W=W, b=b)
W= np.random.rand(4).reshape(1,1,2,2)
b=0
plt.imshow(np.sum(cov.forward(x)[0], axis=0 ),cmap=plt.cm.binary)#畳み込みした画像の描画
plt.title("Convolution")
x = train_images[0].flatten()
W=np.random.rand(784*784).reshape(784,-1) #28*28=784, 画像の大きさを保つ行列
b=0
af=Affine(W=W,b=b)
plt.imshow(af.forward(x).reshape(28, 28) , cmap=plt.cm.binary)# affine 層に通した画像の描画
plt.title("Affine")
畳み込みしたものは、輪郭がぼやけたかな?くらいの変化で、画像の”形”を保持しているのが分かります。一方で、アファイン層に通したものは、原形が何かは全く分かりません。当たり前ですが、画像の図形としての性質は完全に失われています。
次回は、畳み込みとセットで出てくる操作の解説と実装をしたいと思います。
まとめ
・データを行列として扱う為に、畳み込みを考えた。
・画像を成分に持つ4次元ベクトルを行列に変換するアルゴリズムim2colがある。
・畳み込みを実装した。