社畜エンジニア発掘戦線

駆けだしAIエンジニア

③線形分類問題(2クラス分類:ワン・ホット エンコーダ)

第三問

今回はワン・ホット表現を用いた2クラスの分類問題にトライします。前回ちらっと書きましたが、2値(0と1)では2クラス以上の分類ができません(頭がいい人はできると思います)。そこで、3クラス以上の分類にも対応できるようにワンホット表現を導入して問題を解いていきます。

設問1.ガウシアンノイズを付加した入力x1、x2が(x1, x2) = (2.5, 2.5), (7.5, 7.5)となるデータセットをワン・ホット表現を用いて作成せよ

さて、そのワン・ホット表現ですが、もともとデジタル回路とかの分野で使われて表現のようです。

one-hot(ワン・ホット)は1つだけHigh(1)であり、他はLow(0)であるようなビット列のことを指す。
-by Wikipedia

2進数とはまたちょっと違うんですよね。例えば、5クラスをワンホットで表現すると、0=[1, 0, 0, 0, 0]、1=[0, 1, 0, 0, 0]、2=[0, 0, 1, 0, 0]、3=[0, 0, 0, 1, 0]、4=[0, 0, 0, 0, 1]、となります。プログラミングで数字は0から数えるので、5クラスだと最初は0で最後は4です。

今回の問題は2クラスなので入力x1の正解ラベルtは[1, 0]、x2は[0, 1]となります。では、ここまでをコードに落としていきます。

まずはライブラリのインポート、ワン・ホットではクラス別に直接色付けするので、今回カラーマップは必要ありません。

import numpy as np
import matplotlib.pyplot as plt

次にデータセットを作成します。

#dataset
num_of_sam = 40
std_dv = 1.8

group1 = np.array([2.5,2.5])+np.random.randn(num_of_sam, 2)*std_dv
group2 = np.array([7.5,7.5])+np.random.randn(num_of_sam, 2)*std_dv
X = np.vstack((group1, group2))

ここまでは今まで通りです。では正解ラベルを書いていきます。
[note] 上で書いたワン・ホットの順番が逆になっていますが、間違えただけです。順番が違うだけなので問題ないです。

t_group1 = np.tile([0,1],(num_of_sam,1))
t_group2 = np.tile([1,0],(num_of_sam,1))
T = np.vstack((t_group1, t_group2))

イメージとしては[0, 1]をサンプル数だけnp.tileで並べていくって感じですかね。最後はTとして縦方向に結合します。

では、一度グラフ化してみます。2グループを連結したXとTは計算用です、グラフ化には連結前のそれぞれのデータセットを使います。ということは単にデータセットをグラフ化するだけなら正解ラベルTは必要なく、Xのデータセットに直接色付けをして描画します。

plt.scatter(group1[:,0],group1[:,1],marker='o',color='blue')
plt.scatter(group2[:,0],group2[:,1],marker='o',color='red')
plt.show()

f:id:sutokun:20190227104622p:plain
前回と同じようなグラフになりました。これでワン・ホット表現でのデータセットは完成です。

設問2.活性化関数にソフトマックス、損失関数にクロスエントロピーを用いて出力y1、y2と損失量Eを求めよ

データセットができたところで、次はニューラルネットワークの設計です。ワン・ホット表現を用いるとクラスの数だけ正解ラベルと出力が必要になります。今回は2クラスなので出力は2つ、y1とy2を設定します。なので、設計するニューラルネットワークはこんな感じ。
f:id:sutokun:20190224032604j:plain

各パラメータをまとめて大文字の行列で記載しています。出力の数が変わっただけで骨格として大きな違いはありません。

さて新キャラのソフトマックス関数ですが、下記の式で表される関数です。

{\displaystyle
 y_{i} = \frac{e^{a_{i}}}{\sum_{j} e^{a_{j}}}\ 
}
iは計算が行われる出力ニューロンの順番を表しています。このソフトマックス関数は出力の「正解率(確率)」を計算してくれます。expがかかっていますが、分母のシグマは出力層すべてのニューロンの総和です。全体の総和を分母にして、各ニューロンを分子に置くことでそれぞれのニューロンからの出力を確率として表現してくれます。

このソフトマックス関数の特徴として、「出力の総和が1」となる性質があります。つまり、得られた出力がそのまま「正解率」として判断することができます。
f:id:sutokun:20190224035359j:plain

すごい便利ですね。ワン・ホットの正解ラベルは、「正解の順番の要素が1、それ以外は0」という表現でした。これは言い換えると、正解の確率は100%としてニューロンからの出力と比較することができます。こういう背景でワン・ホット表現とソフトマックス関数は相性が良いです。

では、このソフトマックス関数をPythonで関数としてコードに落としておきます。ついでにクロスエントロピーも合わせて書いておきます。

#function
def softmax(x):
    return np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True)

def loss(y, t):
    return -np.sum(np.multiply(t, np.log(y)) + np.multiply((1 - t), np.log(1 - y)))

次に、初期値として重みW(2x2)とバイアスB(1x2)も定義しておきます。前回から出力の数が変わっているので設定する行列の型に注意です。

#initial setting
W = np.random.randn(2,2)
B = np.random.randn(1,2)

では計算してみます。

Y = softmax(np.dot(X, W)+B)
E = loss(Y, T)

関数を使って、そのままの記述ですね。ここまでで設問2は終了です。

設問3.勾配降下法でパラメータを400回更新し、その出力を求めよ

今回もいつ通り損失関数をパラメータで微分し、更新式を求めます。今回、パラメータは行列なので大文字でまとめて表記します。

重み(W)の微分{\displaystyle
\frac{\partial E }{\partial W}\ = \frac{\partial Y }{\partial W}\ \frac{\partial E }{\partial Y}\ 
}

バイアス(B)の微分{\displaystyle
\frac{\partial E }{\partial B}\ = \frac{\partial Y }{\partial B}\ \frac{\partial E }{\partial Y}\ 
}

で、今回は「ソフトマックス関数×クロスエントロピー」の計算なのでそれぞれの式は次のようになります。

重み(W)の微分{\displaystyle
\frac{\partial Y }{\partial W}\ \frac{\partial E }{\partial Y}\ = \frac{\partial }{\partial W}\ (X \cdot W + B) \cdot \Delta = X \cdot (Y-T)
}

バイアス(B)の微分{\displaystyle
\frac{\partial Y }{\partial B}\ \frac{\partial E }{\partial Y}\ = \frac{\partial }{\partial B}\ (X \cdot W + B) \cdot \Delta = 1 \cdot (Y-T)
}

この辺の計算はこちらを参照↓

以上より、勾配降下法を用いたパラメータの更新式は以下のようになります。

重み(W)の更新式:{\displaystyle
W_{new} = W_{old} - \mu \Bigl( X \cdot (Y-T) \Bigr) 
}

バイアス(B)の更新式:{\displaystyle
B_{new} = B_{old} -  \mu \Bigl( 1 \cdot (Y-T) \Bigr) 
}

更新式が求められたので、これをコードに落とします。学習率は0.003とします。

#initial setting
W = np.random.randn(2,2)
B = np.random.randn(1,2)

learning_rate = 0.003
E_save = []

#iteration
num_of_itr = 400
for i in range(num_of_itr):
    #forward propagation
    Y = softmax(np.dot(X, W)+B)
    E = loss(Y, T)
    E_save = np.append(E_save, E)
    #back propagation
    dW = X.T.dot(Y-T)
    dB = np.sum(Y-T, axis=0, keepdims=True)
    #update
    W = W - learning_rate*dW
    B = B - learning_rate*dB

これで設問3は終了です。さすがに計算とそのコード書きはもう慣れてきましたね。

設問4.更新後のパラメータを用いた出力をデータセットと同じグラフ上にプロットせよ

更新式が求まったので、次はグラフへ出力します。グリッドを切ることろまではこれまでと同じです。

#plot_grid
grid_range = 10
resolution = 60
x1_grid = x2_grid = np.linspace(-grid_range, grid_range, resolution)

xx, yy = np.meshgrid(x1_grid, x2_grid)
X_grid = np.c_[xx.ravel(), yy.ravel()]

Y_grid = softmax(np.dot(X_grid, W)+B)
Y_predict = np.around(Y_grid)

ここからはいろいろやり方があると思うので、あくまで一例です(上級Python使いはどんなふうに書くのか気になります)。

まず、各グリッドごとの出力を横方向に結合します。この結合した行列から正解ラベル[1, 0]と[0, 1]に対応する行を抽出してblue_groupとred_groupに分けます、こんなイメージ。
f:id:sutokun:20190224074257j:plain

out_connect = np.hstack((X_grid,Y_predict))
red_group = out_connect[out_connect[:,2]==1]
blue_group = out_connect[out_connect[:,3]==1]

最後に、この分類したグループをグラフ化します。

#plot_dataset
plt.scatter(group1[:,0],group1[:,1],marker='o',color='blue')
plt.scatter(group2[:,0],group2[:,1],marker='o',color='red')

#plot_output
plt.scatter(red_group[:,0],red_group[:,1],marker='o',alpha=0.3,color='red')
plt.scatter(blue_group[:,0],blue_group[:,1],marker='o',alpha=0.3,color='blue')
plt.show()

f:id:sutokun:20190227104654p:plain
しっかり分類できていますね、これで設問4が終了です。

まとめ

今回も学習が進む過程をGIFでアニメ化します。
f:id:sutokun:20190227104713g:plain
ワン・ホットを使っても2値問題と同じように学習が進み、分類できることが確認できます。

今回の問題で2クラスをワン・ホットで分類できるようになりました。ワン・ホットが使えるようになったことで2クラス以上の分類も可能になります。次回の問題では多クラス分類にトライしてみようと思います。

全体コード
github.com

次の問題
④線形分類問題(多クラス分類) - 社畜エンジニア発掘戦線

元の記事
PYTHONISTA3を使って機械学習(ディープラーニング) - 社畜エンジニア発掘戦線

Twitter
世界の社畜 (@sekai_syachiku) | Twitter