PyCallでMNISTの実データを使った学習・推論

今までは100個のサンプルデータとかでやってたんだけど、概ね動いてきたので実データで。

学習

https://github.com/oreilly-japan/deep-learning-from-scratch/blob/master/ch04/train_neuralnet.py とほぼ同じ処理をしているはず。

$ ruby train.rb 0 10000 100 data/params.json
10000 / 10000
      user     system      total        real
351.320000  38.590000 389.910000 (183.712062)

3分ぐらいかかった。

結果はいい感じ。

推論

$ ruby predict.rb 
7 == 7: o
2 == 2: o
1 == 1: o
0 == 0: o
4 == 4: o
1 == 1: o
4 == 4: o
9 == 9: o
6 == 5: x
9 == 9: o
      user     system      total        real
  0.040000   0.000000   0.040000 (  0.020253)

こちらは全然時間かからず。精度も大丈夫そう。

ゼロから作るDeep Learning: 第4章のgradientをRuby化

O'Reilly Japan - ゼロから作るDeep Learning

下のような感じで書き方が変わる。
第4章のバージョンについては説明がないので、誤差逆伝播法の理解を深めるためにもRuby化してみる。

ch04/two_layer_net.py

https://github.com/oreilly-japan/deep-learning-from-scratch/blob/master/ch04/two_layer_net.py

    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}

        batch_num = x.shape[0]

        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)

        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)

        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

ch05/two_layer_net.py

https://github.com/oreilly-japan/deep-learning-from-scratch/blob/master/ch05/two_layer_net.py

    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

dはデルタ(δ)のd。

f(x, y) = x + yという関数があったら、
xの偏微分はδf/δx
yの偏微分はδf/δy
と書く。

Ruby化

Ruby版はこんな感じになった。
dがdeltaの頭文字ということがわかっていれば、やっていることはレイヤー版と何ら変わりないことがわかるので、あとは本を読めば良い。
sigmoid_gradはSigmoidレイヤーのbackwardメソッドと同じ意味。(レイヤー版ではself.outに保持していたものを引数で渡すようにした)

  def gradient(x, t)
    grads = {}

    batch_num = x.shape[0]

    forward = predict(x)
    w2, a1, z1, a2, y = forward[:w2], forward[:a1], forward[:z1], forward[:a2], forward[:y]

    dy = (y - t) / batch_num
    grads[:W2] = np.dot.(z1.T, dy)
    grads[:b2] = np.sum.(dy, 0)

    da1 = np.dot.(dy, w2.T)
    dz1 = Util.sigmoid_grad(z1) * da1
    grads[:W1] = np.dot.(x.T, dz1)
    grads[:b1] = np.sum.(dz1, 0)

    return grads
  end

参考

[読書メモ] ゼロから作るDeep Learning: 第5章

O'Reilly Japan - ゼロから作るDeep Learning

誤差逆伝播法

  • 数値微分は実装簡単だが遅い
  • 本書では数式ではなく計算グラフで説明

計算グラフ

  • メリット
    • 局所的な計算に集中できる
    • 途中の計算結果を保持できる
    • 逆方向の伝播によって微分を効率的に計算できる
  • リンゴの値段があがったら支払い金額はどうなる?
    • 100円のリンゴ2個、消費税10%
    • りんごの値段に対する支払い金額の微分
    • 逆方向の矢印をたどればすぐわかる
    • あがった値段の2.2倍増える
    • 途中までの計算を再利用して、消費税に対する微分もすぐわかる

連鎖率の原理

  • 合成関数の微分はそれぞれの関数の微分の積となる
  • 合成関数
    • 複数の関数で構成される関数
  • 計算グラフの逆伝播はこれによって成り立つ

ノード

  • 加算ノード
    • 値をそのまま流す
  • 乗算ノード
    • xとyをひっくり返して掛けて流す

単純なレイヤの実装

  • forward, backwordを持つ
  • forwardはx, yを受け取る
  • backwardはdout(微分)を受け取る

ニューラルネットのレイヤ実装

  • ReLU
  • Sigmoid
  • 中身はややこしいけど計算グラフ上は1つのノードで表せる
  • Affine
    • 行列の内積(アフィン変換)+バイアス
  • Softmax with Loss
    • Softmaxと損失関数(交差エントロピー誤差)を合わせたレイヤ
    • Softmaxは推論では省略される。省略されたAffineの生データのことをスコアと呼ぶ
    • 逆電波はSoftmaxの出力(y1, y2, y3)と教師データ(t1, t2, t3)の差分という綺麗な結果になる
    • 交差エントロピー誤差がそのために設計されたものだから
    • 恒等関数は2乗和誤差を使うと綺麗になる
    • 認識率が低いと大きな誤差が逆伝播していく、認識率が高いと小さな誤差が逆伝播

数値微分の必要性

  • 勾配確認
    • 誤差逆伝播法は実装複雑なのでバグの可能性
    • 数値微分と勾配を比べる

学習の全体図

  1. ミニバッチの抽出
  2. 勾配の算出
  3. パラメータの更新
  4. 繰り返す

誤差逆伝播法は2を効率化するもの。

そういえば積分は?

全然本書とは関係ない話。

数値積分(長方形近似)

  • 数式解く→ 解析的
  • 長方形を細かく分けて面積求める → 数値積分

積分も本書で扱ってる微分と同じ。
数値積分はわかりやすい。

誤差逆伝播法みたいに積分を効率的にやる手法もあるのかな?

session_store :redis_store, expires_in:の罠

https://github.com/redis-store/redis-rails#usage
には、

MyApplication::Application.config.session_store :redis_store, {
  servers: [
    {
      host: "localhost",
      port: 6379,
      db: 0,
      password: "mysecret",
      namespace: "session"
    },
  ],
  expires_in: 90.minutes
}

という例があり、ほぼこのまま書いていたアプリがあった。

しかし、iPhoneのSafariで頻繁にログアウトされるという報告があり、改めて確認するとPCでもブラウザを再起動するとCookieが消えていた。

これはCookieにExpiresが付いていない時の挙動である。

そこで、真っ先に疑われたのがこの設定。Rails標準のCookieStoreでは、expire_afterを設定するのでそちらに変更してみたら解消した。
http://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html

とはいえREADMEに書いてあるので、中身を一応追ってみる。

1

redis-railsには実コードはないけどテストによると、session_storeの実体にはActionDispatch::Session::RedisStoreが使われる。
https://github.com/redis-store/redis-rails/blob/master/test/redis_rails_test.rb

2

ActionDispatch::Session::RedisStoreredis-actionpackで定義され、Rack::Session::Redisを継承している。
https://github.com/redis-store/redis-actionpack/blob/master/lib/action_dispatch/middleware/session/redis_store.rb

3

Rack::Session::Redisredis-rackで定義され、Rack::Session::Abstract::IDを継承している。
https://github.com/redis-store/redis-rack/blob/master/lib/rack/session/redis.rb

4

Rack::Session::Abstract::IDrackで定義され、さらにRack::Session::Abstract::Persistedを継承している。

https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb
(Persistedも同一ファイルに定義)

ここにexpire_afterはあるが、expires_inは見当たらない。

CookieStoreは?

ActionDispatch::Session::AbstractStoreを継承して、それがさらにRack::Session::Abstract::Persistedを継承している。

https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb

つまり結局Rack::Session::Abstract::Persistedが使われる。クラス階層の途中でexpires_inを見てそれらしき処理をしてるクラスがいない場合、READMEが間違っている、でよさそう。

ぷるり出してみようかな。
→ 出してみた https://github.com/redis-store/redis-rails/pull/64
→ 無事マージされた

PyCallでMNISTの学習

O'Reilly Japan - ゼロから作るDeep Learning P.112〜を参考に。

交差エントロピー誤差

P.94-95のバッチ対応版の理解に少し時間がかかった。

  • tが0の場合はlogは0になるので無視できる
  • 正解ラベルのインデックスに対してどういう出力をしたかだけが問題
  • それのlogを取りたい
  • 正解ラベルは数字なのでそのままインデックスとして使える
  • y[0, 正解ラベル], y[1, 正解ラベル]とすることで各バッチの正解ラベルに対応した出力が取れる

バッチ非対応版

    def cross_entropy_error(y, t)
      delta = 1e-7
      -np.sum.(t * np.log.(y + delta))
    end 
> t = [0,0,1,0,0,0,0,0,0,0] 
> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
> Util.cross_entropy_error(np.array.(y), np.array.(t))
=> 0.510825457099338

バッチ対応版

    def cross_entropy_error(y, t)
      not_batch = y.ndim == 1 
      if not_batch
        t = t.reshape.(1, t.size)
        y = y.reshape.(1, y.size)
      end 

      one_hot_vector = t.size == y.size
      t = t.argmax.(1) if one_hot_vector

      batch_size = y.shape[0]
      output_for_answer = y[np.arange.(batch_size), t]
      sum = -np.sum.(np.log.(output_for_answer))
      return sum / batch_size
    end
> Util.cross_entropy_error(np.array.(y), np.array.(t))
=> 0.5108256237659907

結果が変わったので気になったけど、0がないyを使って、deltaをなくしたら同じ値になったので、deltaのせい。問題なさそう。

loss_Wの罠

two_layer_net.pyloss_WW使ってない
全部一緒の結果返ってくるんじゃ?

他にも同じところで詰まってる人がいた。
ゼロから学ぶDeep Learningの4章で詰まった – bitter chains

今の版だと説明が変わって少しわかりやすくなってるっぽい。
https://github.com/oreilly-japan/deep-learning-from-scratch/wiki/errata#%E7%AC%AC4%E5%88%B7%E3%81%BE%E3%81%A7

two_layer_net.py

    # x:入力データ, t:教師データ
    def numerical_gradient(self, x, t): 
        loss_W = lambda W: self.loss(x, t)

        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])

        return grads

functions.py

def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)

        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)

        x[idx] = tmp_val # 値を元に戻す
        it.iternext()   

    return grad

ここでxに渡されたものを、直接いじってるからもとのnetも変わってる、ということ。
Wfに渡したx)はやはり使ってない。

Ruby化する時に直接書き換えないコードにした。(Util側はxをnp.copyして使っている)

  def numerical_gradient(x, t)
    {
      W1: Util.numerical_gradient(loss_w(:W1, x, t), params[:W1]),
      b1: Util.numerical_gradient(loss_w(:b1, x, t), params[:b1]),
      W2: Util.numerical_gradient(loss_w(:W2, x, t), params[:W2]),
      b2: Util.numerical_gradient(loss_w(:b2, x, t), params[:b2]),
    }
  end  

  def loss_w(key, x, t)
    -> w {
      tmp_w = params[key]
      params[key] = w
      l = loss(x, t)
      params[key] = tmp_w
      l
    }
  end

結局numerical_gradientだと速度的に実用できなそうだったので、gradientメソッドの方を使ったけど…。

その他

  • .call()[].()でもいいけど結局callが冗長ながらもわかりやすい。numerical_gradientに渡したfの話。
  • it.iternext.()it.iternext()にしてて無限ループした。

参考