「ゼロから作る Deep Learning」の読書まとめ。後で思い出すためのメモです。わかっていたつもりでしたがコードを見ると納得感ありました。
1章 Python入門
Python入れましょう。numpyとmatplotlibは必須で。
2章 パーセプトロン
複数の入力から1つの出力を得るアルゴリズムで、「重み」と「バイアス」によってその出力が変化します。単層では線形な表現しかできませんが、多層にすることで非線形な表現も可能になります。ニューラルネットワークの基本要素になります。
基本的には積和するだけ。本章ではAND、OR、NAND、XORについて解説されています。単層でAND、OR、NANDは表現できますが、XORは多層でないと作れないよ。という解説です。
実装もシンプルで、重みwとバイアスb、を使って入力の積和をして閾値と比較して1,0を出すだけです。回路によって係数が違うだけ。
def AND(x1, x2): x = np.array([x1, x2]) w = np.array([0.5, 0.5]) b = -0.7 tmp = np.sum(w*x) + b if tmp <= 0: return 0 else: return 1 def OR(x1, x2): x = np.array([x1, x2]) w = np.array([0.5, 0.5]) b = -0.2 : def NAND(x1, x2): x = np.array([x1, x2]) w = np.array([-0.5, -0.5]) b = 0.7 :
NANDはANDを否定すればよいので、w,bの符号をひっくり返してるのだと思います。XORは多層になっていて、
def XOR(x1, x2): s1 = NAND(x1, x2) s2 = OR(x1, x2) y = AND(s1, s2) return y
こんな感じ
3章 ニューラルネットワーク
積和結果にシグモイド関数やReLU関数のような活性化関数を付け、それを多数、幾層にも組み合わせたネットワーク。出力最終層では、ソフトマックス関数や、恒等関数を用い、分類問題や回帰問題を解くことができるようになります。
構造として、入力層、中間層(隠れ層)、出力層と呼ばれる層構図になっています。中間層にあるのは前章のパーセプトロンが複数並んでいるもので、係数は2つの入力に対する重みWと、加算のバイアスの3つになります。
活性化関数
ニューロンの出力に対してどのように発火(活性化)するかを決定する役割で、ここに非線形性があることで複雑な問題を解くことができるようになります。
ステップ関数
前述のパーセプトロンは閾値を境に0 or 1なので活性化関数にステップ関数を使っていたことと等価です。
シグモイド関数
$$ h(x)=\frac{1}{1+e^{-x}} $$
def sigmoid(x): return 1 / (1 + np.exp(-x))
こんなややこしい式がよく使われるのには訳があって、後述の微分が簡単になるという特徴があるからです。
ReLU関数
$$ h(x)=\begin{cases}x & x > 0\\0 & x \leq 0\end{cases} $$
def relu(x): return np.maximum(0, x)
多次元配列の計算
行列演算、特に行列の掛け算で複数の積和が一気にできてしまうから便利。ニューラルネットワークの重み付き積和が一発でできてしまうというお話です。展開するとよくわかります。後述の画像の畳み込み(CNN)でも使えちゃうのですごい。numpy万歳。
3層ニューラルネットワークの実装
基本的に行列の掛け算で各層の計算ができてしまうので記述はかなりシンプルになっています。基本的に順伝播(forward)は
行列の掛け算→活性化関数→行列の掛け算→活性化関数…
と続くだけです。コードもその通りになっています。重みWが大文字でそれ以外小文字というのが慣例のようです。
def forward(network, x): W1, W2, W3 = network['W1'], network['W2'], network['W3'] b1, b2, b3 = network['b1'], network['b2'], network['b3'] a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) a2 = np.dot(z1, W2) + b2 z2 = sigmoid(a2) a3 = np.dot(z2, W3) + b3 y = softmax(a3)
出力層の設計
分類問題なのか回帰問題なのかによって用いる出力層の活性化関数を決める必要があります。
分類の例は写真から男or女を分類する問題。ソフトマックスを使う。
回帰の例は写真からその人の体重を推定する問題(連続値の推定)。恒等関数を使う。
恒等関数とソフトマックス関数
恒等関数
入力をそのまま
ソフトマックス関数
$$ y_{k}=\frac{e^{a_{k}}}{\sum_{i=1}^ne^{a_{k}}} $$
def softmax(x): x = x - np.max(x, axis=-1, keepdims=True) # オーバーフロー対策 return np.exp(x) / np.sum(np.exp(x), axis=-1, keepdims=True)
式にない最大値取得の部分がありますが、コメント通りオーバーフロー対策で、累乗が無限大に行ってしまうのを回避しています。すべての要素から減算しているので数式上等価です。累乗の肩に乗った指数なので加算減算でOK。どちらかというとややこしいのは合計や最大値をとる次元が-1になっている点。-1は最終次元を意味し、例えば10クラス分類ならこの10個の要素が入っていて、その方向での合計をとっています。
なんでこんなややこしい計算にしているかというと、これもシグモイド関数同様に後述の学習時の微分、クロスエントロピーロスとのセットで考えたときに都合がよいためです。詳細は後述しますが、Wikiに丁寧に説明があります。
ただこれは合計1の確率として正規化しているだけで、大小関係は変わらないため通常の推論過程では省略されることが通例のようです。学習時には必要になります。
手書き数字認識
MNISTデータセットを利用した手書き文字認識
いわずと知れたMNISTのデータセットを使ってこの3層のモデルの推論をします。
バッチ処理
複数の入力を一気に処理するのがバッチ処理です。これも行列の掛け算でしかも記述を変えずにできてしまいます。
行列演算を行うので、掛け算する行列の左側の値の列数と右側の値の行数が一致している必要があります。この様子が図で示されていますがイマイチ効果が伝わりにくいので、以下のコードで確認してみました。
W1 = np.ones((784,50)) W2 = np.ones((50,100)) W3 = np.ones((100,10)) print(np.shape(W1)) # (784, 50) print(np.shape(W2)) # (50, 100) print(np.shape(W3)) # (100, 10) # 1つずつforward x = np.ones(784) a1 = np.dot(x,W1) a2 = np.dot(a1,W2) y = np.dot(a2,W3) print(np.shape(x)) # (784,) print(np.shape(a1)) # (50,) print(np.shape(a2)) # (100,) print(np.shape(y)) # (10,) # batchsize 100 でforward x = np.ones((100,784)) a1 = np.dot(x,W1) a2 = np.dot(a1,W2) y = np.dot(a2,W3) print(np.shape(x)) # (100, 784) print(np.shape(a1)) # (100, 50) print(np.shape(a2)) # (100, 100) print(np.shape(y)) # (100, 10)
ポイントはバッチ単位にしようがしまいが
a1 = np.dot(x,W1) a2 = np.dot(a1,W2) y = np.dot(a2,W3)
のW1,W2,W3の形を変えず、また行列の掛け算の記述も変えず、いっぺんにバッチ単位(今回は100個の入力に対して)で計算できています。束で処理することで高速化が期待できます。もちろん活性化関数もこのように次元追加されていることを想定して正しく動くように記述しておく必要があります。バッチといってもfor文をバッチ数回すわけじゃないです。
実装も
batch_size = 100 # バッチの数 accuracy_cnt = 0 for i in range(0, len(x), batch_size): x_batch = x[i:i+batch_size] y_batch = predict(network, x_batch) p = np.argmax(y_batch, axis=1) accuracy_cnt += np.sum(p == t[i:i+batch_size])
1次元目にバッチ数を追加したnparrayにしたうえでpredictしてます。
コメント