見出し画像

14章 CNN:2次元の離散畳み込みをスッキリ実行する

はじめに

シリーズ「Python機械学習プログラミング」の紹介

本シリーズは書籍「Python機械学習プログラミング PyTorch & scikit-learn編」(初版第1刷)に関する記事を取り扱います。
この書籍のよいところは、Pythonのコードを動かしたり、アルゴリズムの説明を読み、ときに数式を確認して、包括的に機械学習を学ぶことができることです。
Pythonで機械学習を学びたい方におすすめです!
この記事では、この書籍のことを「テキスト」と呼びます。

記事の内容

この記事は「第14章 画像の分類-ディープ畳み込みニューラルネットワーク」の「14.1.2 離散畳み込みを実行する」の節の図14-7の計算を実現するコードを紹介します。

14章のダイジェスト

14章は、CNN:畳み込みニューラルネットワークで画像を分類するタスクにチャレンジします。
PyTorchで画像データを処理する章です。
まず、特徴マップ、畳み込み層とプーリング層の計算の概念を学び、続いて、PyTorchでCNNを実装します。テーマは2つ。1つ目は手書き文字の分類タスク(おなじみのMNISTデータセット)。2つ目は写真画像から笑顔を分類するタスクです。


離散畳み込み演算をPythonで実行できるようにする

離散畳み込みの演算イメージ

「14.1.2 離散畳み込みを実行する」の節では、離散畳み込みの演算イメージを掴むために、数値例とサンプルコードが用意されています。
テキストの「図14-6:入力行列とカーネル行列の間で2次元の畳み込みを計算」及び「図14-7:要素ごとの積の総和を求める」では、次のような例題を掲載しています。

■入力データ:$${3 \times 3}$$の入力行列$${\boldsymbol{X}_{3 \times 3}}$$

$$
\boldsymbol{X}_{3 \times 3}=
\left[
\begin{matrix}
2&1&2\\
5&0&1\\
1&7&3
\end{matrix}
\right]
$$

■フィルタ:$${3 \times 3}$$のカーネル行列$${\boldsymbol{W}_{3 \times 3}}$$

$$
\boldsymbol{W}_{3 \times 3}=
\left[
\begin{matrix}
0.5&0.7&0.4\\
0.3&0.4&0.1\\
0.5&1&0.5
\end{matrix}
\right]
$$

■パディング:$${p=(1,1)}$$
■ストライド:$${s=(2,2)}$$

カーネル行列$${\boldsymbol{W}_{3 \times 3}}$$を回転させると、次のようになります。

$$
\boldsymbol{W}^r=
\left[
\begin{matrix}
0.5&1&0.5\\
0.1&0.4&0.3\\
0.4&0.7&0.5
\end{matrix}
\right]
$$

このような条件で、$${\boldsymbol{X}_{3 \times 3}}$$の周辺にゼロパディングした$${\boldsymbol{X}^{padded}_{5 \times 5}}$$、ストライド$${s=(2,2)}$$でカーネル行列(回転後)$${\boldsymbol{W}^r}$$を移動させながら、要素ごとの積の総和を計算し、出力行列$${\boldsymbol{Y}}$$を生成するイメージが、「図14-7:要素ごとの積の総和を求める」(次の図)です。

図14-7:要素ごとの積の総和を求める
(テキストより引用)

テキストのコードで畳み込み演算を実行

この図の出力行列$${\boldsymbol{Y}}$$の計算をテキストのコードで実施してみます。
※以下のコードはテキストのコードを若干変更しています。

# 畳み込み演算
import numpy as np

# 畳み込み演算関数の定義
def conv2d(X, W, p=(0, 0), s=(1, 1)):
    W_rot = np.array(W)[::-1,::-1]
    X_orig = np.array(X)
    n1 = X_orig.shape[0] + 2*p[0]
    n2 = X_orig.shape[1] + 2*p[1]
    X_padded = np.zeros(shape=(n1, n2))
    X_padded[p[0]:p[0]+X_orig.shape[0],
    p[1]:p[1]+X_orig.shape[1]] = X_orig

    res = []
    for i in range(0, int((X_padded.shape[0] - 
                           W_rot.shape[0]) / s[0]) + 1, s[0]):
        res.append([])
        for j in range(0, int((X_padded.shape[1] - 
                               W_rot.shape[1]) / s[1]) + 1, s[1]):
            X_sub = X_padded[i:i+W_rot.shape[0],
                             j:j+W_rot.shape[1]]
            res[-1].append(np.sum(X_sub * W_rot))
    return(np.array(res))

# 入力行列X、カーネル行列Wの入力
X = [[2, 1 ,2], [5, 0, 1], [1, 7, 3]]
W = [[0.5, 0.7, 0.4], [0.3, 0.4, 0.1], [0.5, 1  , 0.5]]

# 出力行列Yの出力
print('Y =\n', conv2d(X, W, p=(1, 1), s=(2, 2)))

実行すると、図14-7の行列$${\boldsymbol{Y}}$$の内容とは異なった結果が出力されました。

出力イメージ

Y =
 [[4.6]]

2✕2の行列の出力を期待していましたが、実際に出力されたのは要素が1つだけ。図の左上(1行・1列目)の数値 4.6 のみの出力になっています。
さて、困りました。。。

原因を探る

ひとまず、2つのfor文で i と j を出力するようにしてみました。

出力イメージ

i= 0
j= 0

i も j も1回しかループしていないようです。
ループの条件を確かめてみます。

# 1つ目のfor文
for i in range(0, int((X_padded.shape[0] - W_rot.shape[0])/s[0])+1, s[0]):

rangeのカッコ内の数字を追って見ます。
X_padded.shape[0]は、入力行列$${\boldsymbol{X}}$$の3行にゼロパディングを前後1行ずつ付けているので、値は 5 です。
W_rot.shape[0]は、カーネル行列$${\boldsymbol{W}^r}$$の行数ですので、値は 3 です。
s[0]は、ストライド$${s}$$の行数ですので、値は 2 です。
そうすると、整数( ( X_padded.shape[0] - W_rot.shape[0] ) / s[0] ) + 1 = 整数( (5 - 2 ) / 2 ) + 1 = 2 となります。 
まとめると、range( 0, 2, 2 )は、start: 0 から stop: 2(ただし2は含まないので、実際には1) までの整数の範囲で、2ステップごとに値を返すという動きになりますので、返る値は 0 のみとなります。
期待していた返り値は 0 と 2 の2つでしたので、このrangeの引数によって意図していない動きになっていたことがわかりました。

コードを変更して再度チャレンジする

次のようにコードを変更して、再実行してみます。

# 畳み込み演算【変更】
import numpy as np

# 畳み込み演算関数の定義
def conv2d(X, W, p=(0, 0), s=(1, 1)):
    W_rot = np.array(W)[::-1,::-1]
    X_orig = np.array(X)
    n1 = X_orig.shape[0] + 2*p[0]
    n2 = X_orig.shape[1] + 2*p[1]
    X_padded = np.zeros(shape=(n1, n2))
    X_padded[p[0]:p[0]+X_orig.shape[0],
    p[1]:p[1]+X_orig.shape[1]] = X_orig

    res = []
    for i in range(0, int(X_padded.shape[0] -             # 変更
                          W_rot.shape[0]) + 1, s[0]):     # 変更
        res.append([])
        for j in range(0, int(X_padded.shape[1] -         # 変更
                              W_rot.shape[1]) + 1, s[1]): # 変更
            X_sub = X_padded[i:i+W_rot.shape[0],
                             j:j+W_rot.shape[1]]
            res[-1].append(np.sum(X_sub * W_rot))
    return(np.array(res))

# 入力行列X、カーネル行列Wの入力
X = [[2, 1 ,2], [5, 0, 1], [1, 7, 3]]
W = [[0.5, 0.7, 0.4], [0.3, 0.4, 0.1], [0.5, 1  , 0.5]]

# 出力行列Yの出力
print('Y =\n', conv2d(X, W, p=(1, 1), s=(2, 2)))

for文の変更前後を比較してみます。

# 変更前
for i in range(0, int((X_padded.shape[0] - W_rot.shape[0]) / s[0]) + 1, s[0]):

# 変更後
for i in range(0, int(X_padded.shape[0] - W_rot.shape[0]) + 1, s[0]):
    

s[0]の割り算を削除しました。
rangeの終了位置が変わりました。
ゼロパディングした入力行列$${\boldsymbol{X}^{padded}}$$の 5行 からカーネル行列$${\boldsymbol{W}}$$の 3行を引いてかつ1を足した 3 がstopの指定となります。
よって、0から2までの範囲を2ステップで値を返すようになり、期待していた 0 と 2 の 2つ が返り値になりました。

出力イメージ

Y = 
[[4.6 1.6] 
[7.5 2.9]]

出力行列$${\boldsymbol{Y}}$$の値はテキストの図と同じ結果になりました。
スッキリしました。


まとめ

今回は、2次元の畳み込み演算のコードの改善に取り組みました。
通常のCNNの実装ではPyTorchなどのツールに組み込まれたロジックで畳み込み演算を行うので、今回のような事態は起きないでしょう。
こうやって、コード実行につまずくことで演算の手触りを得られるような気がします。

# 今日の一句
s = 'すものうち'
print(s[0:1]+s[1:2]*8+s[2:])

楽しくPython機械学習プログラミングを学びましょう!

おまけ数式

noteでは数式記法を利用できます。
今回は2次元の離散畳み込み演算の式を紹介します。
2次元の入力行列$${\boldsymbol{X}_{n_1 \times n_2}}$$、カーネル行列(フィルタ行列)$${\boldsymbol{W}_{m_1 \times m_2} (m_1 \leq n_1,\ m_2 \leq n_2)}$$について、畳み込み演算子$${*}$$による畳み込み演算の結果は、行列$${\boldsymbol{Y=X*W}}$$となり、数学的な定義は次のようになります。

$$
\boldsymbol{Y=X*W} \rightarrow Y[i, j] =
\displaystyle \sum^{+\infty}_{k_1=-\infty} \sum^{+\infty}_{k_2=-\infty}
X[i-k_1,\ j-k_2]W[k_1,\ k_2]
$$


おわりに

AI・機械学習の学習でおすすめの書籍を紹介いたします。
「最短コースでわかる ディープラーニングの数学」

機械学習やディープラーニングなどの手法を理解する際に、数学的な知識があると、いっそう深い理解につながると思います。
でも、難しい数式がびっしりと並んでいる書面を想像すると、なんだかゾッとします。
そんな数式にアレルギーのある方にとって、この「ディープラーニングの数学」は優しく寄り添ってくれて、「数学的」な見方を広げてくれるのではないでしょうか。
この書籍は、機械学習/深層学習の基礎的なテーマを、Pythonのコードを動かしながら、そして数学的な見解も実感しながら、楽しく学ぶことができると思います。
機械学習/深層学習の入門者にとって、次のようなトピックの理解を深くするチャンスとなるでしょう。

  • 損失関数とその微分

  • 活性化関数

  • 交差エントロピー関数

  • 誤差逆伝播

  • 勾配降下法(最急降下法)

ちなみに、私が初めて手にした機械学習/深層学習の書籍が、このディープラーニングと数学でした。思い出深い一冊です。
サンプルコードが動かなかった時に、著者の赤石先生とTwitterでやりとりさせていただいたことは、とても嬉しい出来事でした
今もときどき、自分の理解を整理する際に、ページを捲ります。

最後まで読んでくださり、ありがとうございました。

この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?