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

PyCallでMatplotlibを使う

O'Reilly Japan - ゼロから作るDeep LearningのP.17ページにある単純なグラフ。

plot.rb

require 'bundler'
Bundler.require

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

pyimport 'matplotlib.pyplot', as: :plt

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

plt.plot.(x, y)
plt.show.()
$ bundle exec ruby plot.rb 

動いた!

参考

ソース

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