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()にしてて無限ループした。

参考

PyCallでMNISTを使った推論

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

RubyでMNISTのデータを読む - blog.tnantoka.com
で読んだデータをそのまま

PycallとNumPyで3層ニューラルネットワーク - blog.tnantoka.com
に突っ込んでみました。

重みやバイアスは完全に適当で何の意味もありません。

network.rb

class Network
  def initialize
    pyimport 'numpy', as: :np

    @weights = [
      np.array.((0...784).map { |i| [0.1, 0.3, 0.5] }),
      np.array.([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]),
      np.array.([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1], [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1]]),
    ]
    @biases = [
      np.array.([0.1, 0.2, 0.3]),
      np.array.([0.1, 0.2]),
      np.array.([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1]),
    ]
  end

  def predict(x)
    w1, w2, w3 = @weights
    b1, b2, b3 = @biases

    a1 = np.dot.(x, w1) + b1
    z1 = Util.sigmoid(a1)
    a2 = np.dot.(z1, w2) + b2
    z2 = Util.sigmoid(a2)
    a3 = np.dot.(z2, w3) + b3

    Util.softmax(a3)
  end
end

動かしてみる。

> y = network.predict(x_train[0]) 
=> array([ 0.03214207,  0.0413279 ,  0.05313893,  0.06832543,  0.08785205,  0.11295915,  0.14524157,  0.18674993,  0.24012091,  0.03214207])

> np.argmax.(y)
=> 8

> t_train[0]
=> 5

見事に外れ。

HerokuでPyCall(NumPy)とSinatraを動かす

heroku-examples/python-miniconda を参考に、PyCallとSinatraをHerokuで動かしてみました。

ソースはこちらです。
https://github.com/tnantoka/pycall-heroku

Docker関連

# 最初とpush前の確認時にやる
$ docker build -t tnantoka/pycall-heroku .

# アプリ開発中
$ docker run -it --rm -v `pwd`/app:/opt/app -p 5000:5000 -e PORT=5000 tnantoka/pycall-heroku

# 確認
$ docker run -it --rm -p 5000:5000 -e PORT=5000 tnantoka/pycall-heroku
  • VOLUMEはサポートされないのでADDで。
  • 最後はbuildしてpushするが、開発中は-vで上書き。
  • Herokuでは$PORTが使われるので-eで渡す。

他のプロジェクトでも使いたかったので、heroku-minicondaにRubyをインストールしたものを公開した。
https://hub.docker.com/r/tnantoka/miniconda-ruby/
DockerHubデビュー!

Herokuにデプロイ

$ heroku plugins:install heroku-container-registry
$ heroku container:login
$ heroku create pycall-heroku
$ heroku container:push web
$ heroku open

この状態だと何故かApplication Errorになってしまったのですが、ブラウザでHerokuにログインしてDynoを手動でONにすればいけました。謎です。
(heroku createの後、pushする前に1度アクセスしておいた方がいい?)

https://pycall-heroku.herokuapp.com/
こちらで無事動いております。

その他

pyimportas指定だとエラーになった。
僕のSinatra力が足りなそう。

参考

RubyでMNISTのデータを読む

PyCallの作者さんのGemがあった。

https://github.com/mrkn/ruby-mnist

images = Mnist.load_images('train-images-idx3-ubyte.gz')[2].size # => 60000
labels = Mnist.load_labels('train-labels-idx1-ubyte.gz').size # => 60000

pixels = images[0].unpack('C*')
pixels.size # => 784
labels[0] # => 5

pixels.map { |p| p > 100 ? '*' : ' ' }.each_slice(28).map { |a| a.join('') }.join("\n")





                *** ****    
           ************     
        **********          
        **********          
         *****   *          
           **               
           ***              
            **              
             ****           
              ****          
               ****         
                 ***        
                 ***        
               *****        
             *******        
           *******          
          ******            
       *******              
     ********               
    *******                 



という感じで使える。

unpack、苦手意識ある。

unpack (String) - Rubyリファレンス

文字列のバイト列を引数formatの書式に従って分解し、配列を返します。文字列をバイナリデータとして扱うときに使います。

わかりやすい。

PycallとNumPyで3層ニューラルネットワーク

O'Reilly Japan - ゼロから作るDeep Learning P.58ページからを参考に、フォワード処理を実装。

utils.rb

require 'bundler'
Bundler.require

require 'pycall/import'
include PyCall::Import
pyimport 'numpy', as: :np

def identity_function(x)
  x 
end

def sigmoid(x)
  1 / (1 + np.exp.(-1 * x))
end

-xが以下のエラーで動かなかったので-1 * xにした。

~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/pycall-0.1.0.alpha.20170317/lib/pycall/pyobject_wrapper.rb:174:in `method_missing': undefined method `-@' for array([ 0.3,  0.7,  1.1]):PyCall::PyObject (NoMethodError)
Did you mean?  -

neuralnet.rb

require 'bundler'
Bundler.require

require 'pycall/import'
include PyCall::Import
pyimport 'numpy', as: :np

require './utils'

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])
  network
end

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)

  y
end

network = init_network
x = np.array.([1.0, 0.5])
y = forward(network, x)
p y # array([ 0.31682708,  0.69627909])

普通に動いて楽しい。

ソース

https://github.com/tnantoka/hello-pycall