「ゼロから作るDeep Learning」を読みました

諸事情により機械学習エンジニアを志したのでやっていきます

TOC

まえがき

この本の対象者について

自然言語処理をやりたいんだけど、それは2巻の自然言語処理編でやります。

ゼロから作るDeep Learning ? ―自然言語処理編

ゼロから作るDeep Learning ? ―自然言語処理編

第1章 Python入門

このへんはいつの間にかインストールされていたpyenvでAnacondaをインストールしてやっていきます

$ pyenv install anaconda3-5.1.0
$ pyenv global anaconda3-5.1.0
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0, 6, 0.1)
y = np.sin(x)

plt.plot(x, y)
plt.show()

f:id:Cormorant:20190330160959p:plain

第2章 パーセプトロン

  • パーセプトロンとは、複数の信号を入力として受け取り、ひとつの信号を出力するアルゴリズム
  • 信号は0または1を出力し、それぞれに固有の重みが乗算されて入力される
  • 入力の総和が閾値を超えた場合、1を出力する

数式は略。

単純な論理回路

パーセプトロンでAND,OR,NANDで実装すると、構造は同じままパラメータの値の調整だけでそれぞれ実現できる。すごい。(ただし、パラメータの調整は人力)

パーセプトロンの限界

パーセプトロンではXORは実装できない。えー。

ちゃんと書くと、パーセプトロンでは直線による領域(線形な領域)を分けることはできるが、曲線による領域(非線形な領域)を分けることはできない。

多層パーセプトロン

しかしパーセプトロンの層を重ねればXORを実現できる。

def XOR(x1, x2):
    s1 = NAND(x1, x2)
    s2 = OR(x1, x2)
    y = AND(s1, s2)
    return y

ANDやORが単層のパーセプトロンだったことに対し、XORは2層のパーセプトロン(多層パーセプトロン)になっている。 XOR関数内のx1, x2を第0層、s1, s2を第1層、yを第2層とすると、0->1層と1->2層の2層のパーセプトロン

このように層を重ねることでパーセプトロンはより柔軟な表現が可能になり、足し算や2進数を10進数に変換するエンコーダも作ることができる。さらにはNANDだけで理論上コンピューターを表現することも可能であることから、パーセプトロンでコンピュータが行う処理も表現できる。

第3章 ニューラルネットワーク

パーセプトロンは理論上コンピュータが行う処理も表現できるが、そのパラメータの調整は人力だった。それを解決するのがニューラルネットワーク

活性化関数

この活性化関数を別の関数にしたらどうなるか。これをやってるのがニューラルネットワーク

シグモイド関数

ステップ関数はこうだった。

f:id:Cormorant:20190330174738p:plain

シグモイド関数はこう。

f:id:Cormorant:20190330174925p:plain

ReLU関数

最近は ReLU (Rectified Linear Unit) 関数を使う。詳しくは多分後述。

f:id:Cormorant:20190330180604p:plain

3層ニューラルネットワークの実装

import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def identity_function(x):
    return x

def init_network():
    network = {}
    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])
    return network

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 = identity_function(a3)
    return y

network = init_network()
x = np.array([1.0, 5.0])
y = forward(network, x)
print(y) # [0.33098211 0.72778046]

出力層の設計

  • 上記で出てくる identity_function のように、入力をそのまま返す関数を恒等関数
  • 入力信号akの指数関数 / すべての信号の指数関数の和 を返す関数をソフトマックス関数
  • 回帰問題では恒等関数を、分類問題ではソフトマックス関数を使う
  • 回帰問題はあるデータから数値の予測をする問題、分類問題はあるデータのクラス分けをする問題

ソフトマックス関数の特徴

ソフトマックス関数は出力の総和が1になるので確率として扱える。 ソフトマックス関数の出力の大小は入力前と変わらないので、推論フェーズでは省略されることが多い

出力層のニューロンの数

出力層のニューロンの数は、解くべき問題に応じて、適宜決める必要がある。例えば、0から9の数字を画像認識する10クラス分類問題では、出力層のニューロンは10個に設定する。

手書き文字認識

第4章 ニューラルネットワークの学習

ニューラルネットワークの特徴は、データから学習できる→重みのパラメータをデータから自動で決定できること。

データから学習する

機械学習を使った画像認識では、画像から特徴量を抽出してそのパターンを機械学習の技術で学習する。(画像認識の特徴量として、SIFT,SURG,HUGなど)特徴量を使って画像をベクトルに変換し、機械学習で使われる識別器(SVMやKNNなど)で学習させる。機械が規則性を見つけ出すので人がゼロからアルゴリズムを見つけ出すより効率的。

対してニューラルネットワークでは画像に含まれる特徴量も機械が学習する。

損失関数

基準となる指標をもとに、最適な重みパラメータの探索を行う。この指標は損失関数と呼ばれ、一般には2乗和誤差や交差エントロピー誤差が用いられる。

2乗和誤差

def mean_squared_error(y, t):
    return 0.5 * np.sum((y-t)**2)

# 2 を正解とする one-hot表現
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 例1: 2の確率が最も高い場合(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0 0.1, 0.0, 0.0]
mean_squared_error(np.array(y), np.array(t)) # 0.0975 (教師データとの誤差が小さい)

# 例2: 7の確率が最も高い場合(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0 0.6, 0.0, 0.0]
mean_squared_error(np.array(y), np.array(t)) # 0.5975 (教師データとの誤差が大きい)

上記だと、例1のほうが教師データにより適合していることを示している。

交差エントロピー誤差

def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta)) # 微小数deltaを足すことでlogeが無限になるのを防ぐ

# 2 を正解とする one-hot表現
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 例1: 2の確率が最も高い場合(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0 0.1, 0.0, 0.0]
cross_entropy_error(np.array(y), np.array(t)) # 0.5108 (教師データとの誤差が小さい)

# 例2: 7の確率が最も高い場合(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0 0.6, 0.0, 0.0]
cross_entropy_error(np.array(y), np.array(t)) # 2.3025 (教師データとの誤差が大きい)

tがone-hot表現のとき、正解ラベルが1の出力のみ計算すればいい。(その他の出力は0乗算)

ミニバッチ学習

例えば6000枚の訓練データから、ランダムに100枚選んで、その値で学習を行う学習手法。

なぜ損失関数を設定するのか?

精度を指標にすると、例えば正解率32/100になるようなパラメータのとり方は幅があり、パラメータの変化があっても傾きが生まれないため、そこで学習がストップしてしまう。連続的に変化する損失関数を指標にすれば、少しのパラメータの変化に反応して関数の出力も変化する、つまり殆どの場所で傾きが0にならないため、正しい学習が行われる。

数値微分

数学の話なので略。

勾配

数学の話なので略。

勾配法

勾配を使って損失関数が最も小さくなる場所を探そう、というのが勾配法。ただし、勾配は各地点で値が小さくなる方向でしかないので、そこが関数の最小値である保証はない。勾配の方向にどの程度更新するのか、という学習率の設定が重要になる。一般に、学習率は高くても低くても「良い場所」にはたどり着かない。ニューラルネットワークの学習に置いては、学習率を変更しながら正しく学習できているか確認作業を行うのが一般的。学習率のように自動で獲得されず、人の手によって設定されるパラメータをハイパーパラメータという。

ニューラルネットワークの学習

  • ステップ1: ミニバッチ
  • ステップ2: 勾配の算出
  • ステップ3: パラメータの変更
  • ステップ4: 繰り返す

この4つの手順で行う。これはミニバッチを使用していることから、確率的勾配降下法(stochastic gradient descent)と呼ばれている。ディープラーニングフレームワークではSDGという関数名で実装されていることが多い。

2層ニューラルネットワークのクラス、ミニバッチ学習の実装

テストデータで評価

過学習をしていないか、訓練データ以外のテストデータで確認する。ここでは、1エポック(10000個のデータに対し100個のミニバッチで100回学習した場合、100回で1エポック)ごとにテストする。

テスト自体は略。

まとめ

数値微分による計算には時間がかかるが、その実装は簡単である。一方、次章で実装するやや複雑な誤差逆伝播法は、高速に勾配を求めることができる。

第5章 誤差逆伝播

計算グラフで誤差逆伝搬法を理解する。

計算グラフ

計算グラフ自体については略。

計算グラフを使って問題を解くには、

  1. 計算グラフを構築する
  2. 計算グラフ上で計算を左から右へ進める

という流れで行う。2番目のステップは、順方向の伝播、順伝播(forward propagation)という。逆方向は逆伝播(backward propagation)といい、このさき微分を計算するにあたって重要な働きをする。

局所的な計算

計算グラフでは、各ノードで行うべきことは自分に関係する計算だけであり、全体のことについては何も考えなくてよい。全体がどんなに複雑な計算でも、各ステップでやることは、対象とするノードの局所的な計算になる。局所的な計算は単純だが、その結果を伝搬することで、全体を構成する複雑な計算の結果が得られる。

なぜ計算グラフで解くのか?

  1. 局所的な計算
  2. 途中の計算の結果をすべて保持することができる
  3. 逆方向の伝播によって「微分」を効率良く計算できる(最大の理由)

連鎖律・逆伝播

数学の話なので略。

単純なレイヤの実装

実装を見て計算グラフを感じてほしい。

乗算レイヤ

class MulLayer:
  def __init__(self):
    self.x = None
    self.y = None

  def forward(self, x, y):
    self.x = x
    self.y = y
    out = x * y
    return out
  
  def backward(self, dout):
    dx = dout * self.y # x と y をひっくり返す
    dy = dout * self.x
    return dx, dy

加算レイヤ

class AddLayer:
  def __init__(self):
    pass

  def forward(self, x, y):
    out = x + y
    return out

  def backward(self, dout):
    dx = dout * 1
    dy = dout * 1
    return dx, dy

逆数レイヤ(予想して書いたので間違ってるかも)

class RevLayer:
  def __init__(self):
    self.out = None

  def forward(self, x):
    out = 1 / x
    self.out = out
    return out

  def backward(self, dout):
    dx = dout * (0 - self.out ** 2)
    return dx

活性化関数レイヤの実装

ReLUレイヤ

class Relu:
  def __init__(self):
    self.mask = None
  
  def forward(self, x, y):
    self.mask = (x <= 0)
    out = x.copy()
    out[self.mask] = 0
    return out

  def backward(self, dout):
    dout[self.mask] = 0
    dx = dout
    return dx

Sigmoidレイヤ

class Sigmoid:
  def __init__(self):
    self.out = None
  
  def forward(self, x):
    out = 1 / (1 + np.exp(-x))
    self.out = out
    return out

  def backward(self, dout):
    dx = dout * (1.0 - self.out) * self.out
    return dx

Affineレイヤ

ニューラルネットワークの順伝播で行う行列の積は、幾何学の分野では「アフィン変換」と呼ばれる。そのため、ここでは、アフィン変換を行う処理をAffineレイヤという名前で実装していく。

バッチ版Affineレイヤ

class Affine:
  def __init__(self, W, b):
    self.W = W
    self.b = b
    self.x = None
    self.dW = None
    self.db = None

  def forward(self, x):
    self.x = x
    out = np.dot(x, self.W) + self.b
    return out

  def backward(self, dout):
    dx = np.dot(dout, self.W.T)
    self.dW = np.dot(self.x.T, dout)
    self.db = np.sum(dout, axis=0)
    return dx

Softmax-with-Lossレイヤ

ソフトマックス関数に損失関数である交差エントロピー誤差(cross entropy error)を含めて、Softmax-with-Lossという名前のレイヤを作成する。

Softmax-with-Loss

class SoftmaxWithLoss:
  def __init__(self):
    self.loss = None  # 損失
    self.y = None     # softmaxの出力
    self.t = None     # 教師データ(one-hot vector)

  def forward(self, x, t):
    self.t = t
    self.y = softmax(x)
    self.loss = cross_entropy_error(self.y, self.t)
    return self.loss

  def backward(self, dout=1):
    batch_size = self.t.shape[0]
    dx = (self.y - self.t) / batch_size
    return dx

ソフトマックス関数の損失関数として交差エントロピー誤差を用いると、逆伝播が y - t というきれいな形になる。これはそうなるように交差エントロピー誤差という関数が設計されているから。また、回帰問題では高等関数を用いるが、そちらは2乗和誤差を用いると逆伝播が y - t になる。

誤差逆伝播法の実装

OrderdDict を用いてレイヤの追加順を記録し、追加した順に forward (または逆順に backward)するだけ。

誤差逆伝播法の勾配確認

実装が難しいので、数値微分の結果と比較して勾配確認(gradient check)する。

第6章 学習に関するテクニック

第7章 畳み込みニューラルネットワーク

第8章 ディープラーニング

面白いので買って読んで。

感想

機械学習の本は何冊か読んだんだけど、実際どうやって作っていけばいいのかさっぱりわからない状態だったのでかなり助けになりました。

数学が弱々なので、数式で説明されるよりもコードで実装してくれる方が理解しやすかったようです。

私がやりたいのは8章で紹介されていた画像キャプション生成やDeep Q-Network(強化学習)が近いと思うので、この辺を実際に作ってみたいですね。自然言語処理について取り上げている2巻にも期待。