PyCallを用いてTensorFlowをRubyで使う
PyCallはmrknさんが開発しているPythonのライブラリをRubyで使うgemである。
バージョンが1.0になって大幅に使いやすくなった。
今回は、このPyCallを用いてPython用のライブラリのTensorFlowをRubyで動かしてみる。
今回の環境はWindows10, Ruby2.4, Python3.6を用いた。
PyCallのインストール
PyCallを使用するにはPythonをインストールする必要がある。(すでにインストール済みの場合は必要ない)
インストール方法はなんでも良いが、公式のWebページのダウンロードリンクからインストーラーをダウンロードしてインストールするのが楽だろう。
パスを通すのとpipもインストールするのを忘れないように。
Rubyは恐らくインストールされていると思うが、ない場合はRubyInstaller2か何かでインストールしておく。
PyCallのインストールはgem install pycall
で可能である。
追加でNumpyを使いたい場合はgem install numpy
でインストールすることも出来る。
今回はPythonのTensorFlowライブラリを用いるので、pip install tensorflow
でインストールしておく。(TensorFlowはGPU版もあるが、PyCallでは対応できないっぽいのでCPU版を用いる)
インストールに失敗する場合はcmdを管理者権限で開いてみたり再起動してみたり…とエラーメッセージを見れば多分解決はわかると思う。
PyCallの基本的な使い方
まず最初にrequire "pycall"
をしておく。(Numpyを使う場合はrequire "numpy"
も)
PythonのライブラリをインポートするにはPyCall::import_module
を用いる。(Numpyの場合は不要)
Pythonのimport tensorflow as tf
はtf=PyCall::import_module("tensorflow")
となる。(Numpy(import numpy as np
)はnp=Numpy
でOK。)
後は、tf.~やnp.~などPythonでやっているように書くだけで動く。
Pythonのメソッドの引数や返り値でのオブジェクトの型変換は可能な場合は自動的に行われる(RubyのArrayとPythonのListなど)。
あくまで、文法や構文はRubyなので、PythonのキーワードなどはRuby用に変える必要がある。(None
をnil
にするなど)
具体的にどんな風に書くのかは実際のコードで説明する方が分かりやすいと思うので、ここでの説明はこの辺にとどめておく。
TensorFlowを用いたクラス分類DNN(ディープニューラルネットワーク)のサンプル
概要
今回のサンプルプログラムの概要は以下のとおりだ。
- NeuralNetworkクラス, Datasetクラスを用意する
- NeuralNwtworkクラスはネットワークを定義し、入力を与えると出力を返せ、入力とラベルを入れると、学習や誤差の計算が可能である
- ファイル名を指定することで学習したモデルの保存・復元が可能
- NeuralNwtworkクラスはコンストラクタにネットワークを構成するノード数とドロップアウト率を渡す
- Datasetクラスはコンストラクタにファイル名を渡すとそれを読み込み、ランダムに入力とラベルを返すことが出来る
- データセットの入力形式はCSVで「属性1,属性2,…,属性N,ラベル」であり, 属性値は実数, ラベルは0から始まる整数とする
- 今回は汎化誤差の計算(ホールドアウト検証やクロスバリデーション)はせず、テスト誤差のみ出力することにする(ただしホールドアウト検証するのはプログラムを2行ほど追加するだけで可能である)
実際にTensorFlowを用いた時のコードを比較しやすいように同じ実装(アルゴリズム)で、PythonとRubyで記述した。
サンプルプログラムでは10001回学習させて、1000回毎に評価している。
データセットは60次元で2クラス分類のソナー問題を読み込むようにしてある。レイヤーの構造は[60,30,15,7,2]である。
なお、今回はPythonでの記述とRubyでの記述の比較が目的のため、TensorFlowの説明はしない。
Pythonのコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# -*- coding: utf-8 -*- # TensorFlowを用いたDNNのサンプル import tensorflow as tf import random import csv import os class NeuralNetwork: # layer: [input, hidden1, ..., output] # 学習時のドロップアウト率(1でドロップアウトなし) def __init__(self,layer,dropout_rate): depth=len(layer) self.dropout_rate=dropout_rate z=[] w=[None] # Noneはダミー b=[None] # Noneはダミー d=[] # ドロップアウト層 self.dropout=tf.placeholder(tf.float32) # 入力層 z.append(tf.placeholder(tf.float32,[None,layer[0]])) d.append(tf.nn.dropout(z[0],1.0)) self.x=z[0] # 入力 # 隠れ層 for i in range(1,depth): w.append(tf.Variable(tf.truncated_normal([layer[i-1],layer[i]]),name="w%d"%i)) b.append(tf.Variable(tf.truncated_normal([layer[i]])*0.1,name="b%d"%i)) z.append(tf.nn.relu(tf.matmul(d[i-1],w[i])+b[i])) d.append(tf.nn.dropout(z[i],self.dropout)) # 出力層 w.append(tf.Variable(tf.truncated_normal([layer[depth-2],layer[-1]]),name="w%d"%depth)) b.append(tf.Variable(tf.truncated_normal([layer[-1]])*0.1,name="b%d"%depth)) self.y=tf.nn.softmax(tf.matmul(z[depth-2],w[-1])+b[-1]) # 学習 self.t=tf.placeholder(tf.float32,[None,layer[-1]]) self.loss=-tf.reduce_sum(self.t*self.y) self.train_step=tf.train.AdamOptimizer().minimize(self.loss) # 評価 correct_prediction=tf.equal(tf.argmax(self.y,1),tf.argmax(self.t,1)) self.accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32)) # 初期化 self.session=tf.InteractiveSession() self.session.run(tf.global_variables_initializer()) tmp=self.flatten([w[1:depth+1],b[1:depth]]) self.saver=tf.train.Saver(tmp) print("--Neural Network--\nlayer: %s\ndropout_rate: %f\ndepth: %d\n"%(layer,self.dropout_rate,depth)) # バッチ学習 def train(self,batch_x,batch_t): self.session.run(self.train_step,feed_dict={self.x:batch_x,self.t:batch_t,self.dropout:self.dropout_rate}) # 評価 def evaluate(self,x,t): tmp=self.session.run([self.loss,self.accuracy],feed_dict={self.x:x,self.t:t,self.dropout:1.0}) return float(tmp[0])/len(x),tmp[1] # 出力層の値 def output(self,x): tmp=self.session.run(self.y,feed_dict={self.x:x,self.dropout:1.0}) return tmp.tolist() # 現在のモデルを保存 def save(self,f): self.saver.save(self.session,f) print("saved model: %s"%f) # 学習済みのモデルを復元 def restore(self,f): self.saver.restore(self.session,f) print("load model: %s"%f) # flattenにする def flatten(self,l): flat=[] fringe=[l] while len(fringe)>0: node=fringe.pop(0) # ノードがリストであれば子要素をフリンジに追加 # リストでなければそのままフラットリストに追加 if isinstance(node,list): fringe=node+fringe else: flat.append(node) return flat # csv形式でラベルは0から整数でナンバリングしたものを与える # ファイルは1行に1サンプルで「属性1,属性2,...属性n,ラベル」のフォーマット class Dataset: # ファイル名とクラス数 def __init__(self,input_file,class_num): print("loading dataset: %s"%input_file) self.class_num=class_num self.data=[] # データ self.label=[] # ラベル with open(input_file,"r") as f: r=csv.reader(f) tmp=[] for row in r: tmp=[float(i) for i in row] d=tmp[0:len(tmp)-1] l=[0.0]*self.class_num l[int(tmp[len(tmp)-1])]=1.0 self.data.append(d) self.label.append(l) self.dim=len(self.data[0]) print("%d data load. (dim: %d, class: %d)"%(len(self.data),self.dim,self.class_num)) # n個のバッチをランダムに獲得 def get_batch(self,n): data=[] label=[] for i in range(0,n): index=random.randint(0,len(self.data)-1) data.append(self.data[index]) label.append(self.label[index]) return data,label # 全データ取得 def get_all_data(self): return self.data,self.label # プログラム開始 PATH=os.path.dirname(os.path.abspath(__file__))+"/" # 現在のパス ds=Dataset("dataset.csv",2) nn=NeuralNetwork([ds.dim,30,15,7,ds.class_num],0.8) nn.restore(PATH+"model") # 学習モデルの復元 # 学習 print("start training!") for i in range(0,10001): x,y=ds.get_batch(100) nn.train(x,y) if i%1000==0: x,y=ds.get_all_data() loss,accuracy=nn.evaluate(x,y) print("%d: loss=%f accuracy=%f"%(i,loss,accuracy)) # 学習モデル保存 nn.save(PATH+"model") print("\nfinished.") |
Pythonには入れ子のリストをFlattenにするメソッドが無いので、84-95行目にRubyで言うflattenメソッドを実装している。
PyCallを用いたRubyのコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# TensorFlowを用いたDNNのサンプル # RubyでPyCallを使用 require "pycall" class NeuralNetwork # layer: [input, hidden1, ..., output] # 学習時のドロップアウト率(1でドロップアウトなし) def initialize(layer,dropout_rate) tf=PyCall::import_module("tensorflow") depth=layer.size # 入出力層を含めた深さ @dropout_rate=dropout_rate # 学習時のドロップアウト率 z=[] w=[nil] # nilはダミー b=[nil] # nilはダミー d=[] # ドロップアウト層 @dropout=tf.placeholder(tf.float32) # 入力層 z.push(tf.placeholder(tf.float32,[nil,layer[0]])) d.push(tf.nn.dropout(z[0],1.0)) @x=z[0] # 入力 # 隠れ層 for i in 1...depth do w.push(tf.Variable.new(tf.truncated_normal([layer[i-1],layer[i]]),name:"w#{i}")) b.push(tf.Variable.new(tf.truncated_normal([layer[i]])*0.1,name:"b#{i}")) z.push(tf.nn.relu(tf.matmul(d[i-1],w[i])+b[i])) d.push(tf.nn.dropout(z[i],@dropout)) end # 出力層 w.push(tf.Variable.new(tf.truncated_normal([layer[depth-2],layer.last]),name:"w#{depth}")) b.push(tf.Variable.new(tf.truncated_normal([layer.last])*0.1,name:"b#{depth}")) @y=tf.nn.softmax(tf.matmul(z[depth-2],w.last)+b.last) # 学習 @t=tf.placeholder(tf.float32,[nil,layer.last]) @loss=-1*tf.reduce_sum(@t*@y) @train=tf.train.AdamOptimizer.new.minimize(@loss) # 評価 correct_prediction=tf.equal(tf.argmax(@y,1),tf.argmax(@t,1)) @accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32)) # 初期化 @session=tf.InteractiveSession.new @session.run(tf.global_variables_initializer) @saver=tf.train.Saver.new([w[1..depth],b[1..depth]].flatten) puts("--Neural Network--\nlayer: #{layer}\ndropout_rate: #{@dropout_rate}\ndepth: #{depth}\n") end # バッチ学習 def train(batch_x,batch_t) @session.run(@train,feed_dict:{@x=>batch_x,@t=>batch_t,@dropout=>@dropout_rate}) end # 評価 def evaluate(x,t) tmp=@session.run([@loss,@accuracy],feed_dict:{@x=>x,@t=>t,@dropout=>1.0}) return tmp[0]/x.size.to_f,tmp[1] end # 出力層の値 def output(x) tmp=@session.run(@y,feed_dict:{@x=>x,@dropout=>1.0}) return tmp.tolist.to_a end # 現在のモデルを保存 def save(file) @saver.save(@session,file) puts("saved model: #{file}") end # 学習済みのモデルを復元 def restore(file) @saver.restore(@session,file) puts("load model: #{file}") end end # csv形式でラベルは0から整数でナンバリングしたものを与える # ファイルは1行に1サンプルで「属性1,属性2,...属性n,ラベル」のフォーマット class Dataset # 入力次元(属性数), クラス数 attr_reader :dim,:class_num # ファイル名とクラス数 def initialize(file,class_num) puts("loading dataset: #{file}") @class_num=class_num @data=[] # データ @label=[] # ラベル(one-hot表現) File::open(file,"r") do |file| file.each_line do |line| tmp=line.chomp!.split(",") @data.push(tmp[0...tmp.size-1].map{|val|val.to_f}) arr=Array.new(class_num,0.0) arr[tmp.last.to_i]=1.0 @label.push(arr) end end @dim=@data[0].size puts("#{@data.size} data load. (dim: #{@dim}, class: #{@class_num})") end # n個のバッチをランダムに獲得 def get_batch(n) data=[] label=[] n.times do index=rand(@data.size) data.push(@data[index]) label.push(@label[index]) end return data,label end # 全データ取得 def get_all_data return @data,@label end end # プログラム開始 PATH=File::expand_path(File::dirname(__FILE__))+"/" # 現在のパス ds=Dataset.new("dataset.csv",2) nn=NeuralNetwork.new([ds.dim,30,15,7,ds.class_num],0.8) #nn.restore(PATH+"model") # 学習モデルの復元 # 学習 puts("\nstart training!") 10001.times do |i| x,y=ds.get_batch(100) nn.train(x,y) if i%1000==0 then x,y=ds.get_all_data loss,accuracy=nn.evaluate(x,y) puts("#{i}: loss=#{loss} accuracy=#{accuracy}") end end # 学習モデル保存 nn.save(PATH+"model") puts("\nfinished.") |
簡単に解説をしよう。
- 4
require "pycall"
: PyCallを読み込む。 - 11
tf=PyCall::import_module("tensorflow")"
:import tensorflow as tf
と同等 - 23
z.push(tf.placeholder(tf.float32,[nil,layer[0]]))
: Pythonと表記はほぼ同等だがNone
をnil
にしている - 29
w.push(tf.Variable.new(tf.truncated_normal([layer[i-1],layer[i]]),name:"w#{i}"))
: tf.Variableはクラスなので、Rubyではnewメソッドでインスタンス化する必要がある。Pythonでのキーワード引数keyword=value
はRubyでの記法keyword:value
に修正している。 - 42
@loss=-1*tf.reduce_sum(@t*@y)
:-tf.rresuce_sum(@t*@y)
ではエラーが出たので-1*
にした。 - 71
return tmp.tolist.to_a
:@y
はnp.ndarray型なのでtolistメソッドでPythonのリスト型に変換し、そのままだとRubyで扱えないのでto_aでArrayに変換している。
これ以外は特にPythonとRubyでの書き方の違いに大きな差はなく、特に解説する必要はないと思う。
動かしてみる
実行の仕方は特に変わらない。
上記のファイルをmain.rbとしたなら、そのディレクトリでruby main.rb
とすれば良い。同じディレクトリにdataset.csvが存在する必要がある。
サンプルで用いたデータセットは以下からダウンロードできる。
実行結果は次のようになった(ランダムシードが実行毎に変わるため、必ずしも同じ結果にならない)。
accuracyが1に近づいてきちんと学習できていることがわかる。
loading dataset: dataset.csv
18008 data load. (dim: 60, class: 2)
–Neural Network–
layer: [60, 30, 15, 7, 2]
dropout_rate: 0.8
depth: 5
start training!
0: loss=-0.463793869391 accuracy=0.463794
1000: loss=-0.463793869391 accuracy=0.463794
2000: loss=-0.79221783591 accuracy=0.791648
3000: loss=-0.85574949545 accuracy=0.85673
4000: loss=-0.888409621765 accuracy=0.888605
5000: loss=-0.918586964821 accuracy=0.91898
6000: loss=-0.933551884926 accuracy=0.934418
7000: loss=-0.946889164712 accuracy=0.94769
8000: loss=-0.958797825966 accuracy=0.95924
9000: loss=-0.963946579298 accuracy=0.964238
10000: loss=-0.966777569344 accuracy=0.967126
saved model: D:/model
finished.
なお、モデルを復元したい場合はコメントアウトされているnn.restore(PATH+"model")
のコメントを外せば良い。
実行した時のメモリの消費量はPyCallを用いたほうが多いが、結果や実行速度に関してはPyCallを用いても変わることはなかった。
PyCallを使えばPythonの様々な便利なライブラリがRubyで使えるので非常に便利で使いやすい。開発者様様である。
同じカテゴリー(技術メモ)の他の記事を表示
全記事を表示
タグ: Python, Ruby, Windows, プログラミング