概要
Rubyistでも機械学習がしたい!
ということで、タイトルの通りRubyで機械学習のためのgemをつくってみました。機械学習のための、と書きましたが、試作品ということもあり2016年4月現在実装されているのは多層パーセプトロンと自己符号化器のみです。今後、時間が空いたときに少しずつ更新して、中身を増やせていければよいなと思っています。
Rubyにも機械学習のためのライブラリがいくつかあるのは知っていますが、選択肢を増やすためにも稚拙ながら実装してみました。
インストール
$ gem install sabina
ソースコード https://github.com/seinosuke/sabina
デモ
examples/ 以下にあるサンプルの実行結果をデモとして示します。これらのデモの実行には本gemに加えてv5.0以上のgnuplotが必要です。
多層パーセプトロン その1
以下の図はタグ付けされた訓練データの画像と、学習中のGIFです。データ各点のタグによって色分けされており、この例は2クラス分類です。GIFの左図は多層パーセプトロンから見てどのあたりがどのクラスに分類できるかという範囲を色分けした図で、右図は教師信号との誤差です。
examples/example_mp_01/main.rb
多層パーセプトロン その2
ついでにもうひとつ。こちらはクラス数が3つの場合です。
examples/example_mp_02/main.rb
自己符号化器
左図が元のデータで、右図がエンコード後デコードされたデータで、学習が進むにつれ元のデータがより復元できるようになっていく様子がわかります。ただ、これに関しては少し微妙な話があるので、詳しくは使い方の自己符号化器の項目をご覧ください。
examples/example_ae_01/main.rb
基本的な使い方
サンプルコードがいくつか出てきますが、そこで使われている訓練データファイルは(後述するフォーマットであれば何でもかまいませんが)一応 examples/example_mp_02/training_data.csv を想定しています。
訓練データのCSVファイルの形式
以下に示すようなCSVファイルを用意すれば Sabina::MultilayerPerceptron.load_csv("file_name")
で多層パーセプトロン用の訓練データを読み込むことができます。
x0,x1,label
0.8616722150185228,0.7958526101017311,0
0.548524744634457,0.8355704092991548,1
0.2430915120750876,0.6252296416575435,1
0.8616722150185228,0.7958526101017311,0
0.968877668321639,0.7502385938940324,2
...
これはクラス数が3つの2次元データの例です。一行目は必須であり、訓練データがD次元ベクトルであれば
x0,x1,...,x(D-1),label
となります。
label
の行にはクラスのタグとなる数字をつけます。例えば3つのクラスがあるのであればそのベクトルデータの行の末尾に0か1か2をつけることになります。
自己符号化器用のCSVファイルには label
の行があってもなくてもどとらでもいいですが、読み込みは Sabina::AutoEncoder.load_csv("file_name")
を使います。
多層パーセプトロン
以下の図に示すような多層パーセプトロンを構成する使用例を示します。図では省略されていますが、各層の計算ではバイアス値を加えており、この値も重みとして学習時に更新されます。
多層パーセプトロンを構成するには、 Sabina::MultilayerPerceptron.new
を使います。 MultilayerPerceptron#learn
メソッドで1ステップ学習が進むので、使用例を実行すれば1ステップごとに教師信号との誤差が減少していくのがわかると思います。ちなみに1ステップは指定したミニバッチ数に分けた全てのデータに対して
- 順伝播計算
- 逆伝播計算
- 重みの更新
をこの順で行うように構成されています。(誤差逆伝播法)
require 'sabina'
DIM = 2
K = 3
LOOP_NUM = 10
training_data = Sabina::MultilayerPerceptron.load_csv('training_data.csv')
options = {
:layers => [
Sabina::Layer::MPInputLayer.new(DIM),
Sabina::Layer::MPHiddenLayer.new(4),
Sabina::Layer::MPOutputLayer.new(K)
],
:mini_batch_size => 10,
:learning_rate => 0.01,
:training_data => training_data,
}
mp = Sabina::MultilayerPerceptron.new(options)
LOOP_NUM.times do |t|
mp.learn
error = mp.error(training_data)
puts " error : #{error}"
end
誤差関数 $E(\boldsymbol{w})$ には以下に示す交差エントロピーを用いています。
- $N$ データ数
- $K$ クラス数
- $d_{nk}$ n番目のデータの出力層k番目のユニットに対する教師信号
- $y_{k}$ 出力層k番目のユニットの出力値
E(\boldsymbol{w}) = -\sum_{n=1}^{N} \sum_{k=1}^{K} d_{nk} \log y_{k}
自己符号化器
自己符号化器は
- (入力層のユニット数) = (出力層のユニット数)
- (入力層のユニット数) > (隠れ層のユニット数)
として主成分分析と同様に次元圧縮に用いられますが、デモで示したサンプルで入力したデータは2次元であり、隠れ層のユニット数は20なので
- (入力層のユニット数) = (出力層のユニット数)
- (入力層のユニット数) < (隠れ層のユニット数)
となっています。こういう場合、意味のある学習をするためにはスパース自己符号化器というものを使うのですが、それでももっと高次元の入力に使うものなので、果たしてデモで示したサンプルが意味のある学習ができているかはわかりません。とりあえず、ここでは元のデータが復元できているので実装はできているのだなということが伝わればと思います。いずれ、手書き文字などのデータに対しての実行結果を示すことができれば、と考えています。
ちなみに先述の自己符号化器とスパース自己符号化器についてはふたつとも実装されているので、入力層と隠れ層のユニット数の関係に応じて使い分けてください。
AutoEncoderクラスの誤差関数は二乗誤差を用いています。
require 'sabina'
DIM = 2
LOOP_NUM = 10
original_data = Sabina::AutoEncoder.load_csv('training_data.csv')
options = {
:layers => [
Sabina::Layer::AEInputLayer.new(DIM),
Sabina::Layer::AEHiddenLayer.new(4),
Sabina::Layer::AEOutputLayer.new(DIM)
],
:mini_batch_size => 10,
:learning_rate => 0.001,
:training_data => original_data,
}
sae = Sabina::SparseAutoEncoder.new(options)
LOOP_NUM.times do |t|
sae.learn
error = sae.error(original_data)
puts " error : #{error}"
end
カスタマイズ要素
デフォルト値の設定
以下のように Sabina.configure
メソッドでデフォルト値を設定しておけば、インスタンス生成時に引数を省略することができます。また、デフォルト値が設定されている状態で引数を与えると、その時のみ引数の値で上書きされます。
require 'sabina'
training_data = Sabina::MultilayerPerceptron.load_csv('training_data.csv')
Sabina.configure do |config|
config.layers = [
Sabina::Layer::MPInputLayer.new(2),
Sabina::Layer::MPHiddenLayer.new(8),
Sabina::Layer::MPOutputLayer.new(3)
]
config.mini_batch_size = 10
config.learning_rate = 0.01
config.training_data = training_data
end
options = {
:mini_batch_size => 20
}
mp_01 = Sabina::MultilayerPerceptron.new
mp_02 = Sabina::MultilayerPerceptron.new(options)
mp_01.mini_batch_size # => 10
mp_02.mini_batch_size # => 20
自作Layer
MP~Layer
や AE~Layer
はそれぞれ多層パーセプトロン用、自己符号化器用にこちらで用意した層クラスで、どちらも Sabina::Layer::BaseLayer
を継承しています。つまり、 Sabina::Layer::BaseLayer
クラスを継承したクラスを自作Layerとして扱うことができます。
デフォルトでは活性化関数には以下に示すシグモイド関数が使われていますが、
f(u) = \frac{1}{1+\exp^{-u}}
ここでは正規化線形関数 $f(u)=\max(u, 0)$ を隠れ層の各ユニットの活性化関数とする例を示します。
@f
は活性化関数、@f_
はその微分です。ちなみにこの例のように :layers
に与える層の配列の大きさ(層の数)は3つ以上ならどれだけ大きくても大丈夫ですが、層が深いとそれだけ時間がかかるし学習も大変になります。
class MyHiddenLayer < Sabina::Layer::BaseLayer
def initialize(size)
super
# f(x) = max(0, x)
@f = ->(x){ x > 0.0 ? x : 0.0 }
@f_ = ->(x){ x > 0.0 ? 1.0 : 0.0 }
end
end
options = {
:layers => [
Sabina::Layer::MPInputLayer.new(2),
MyHiddenLayer.new(16),
MyHiddenLayer.new(8),
Sabina::Layer::MPOutputLayer.new(3)
],
}
@f
、@f_
はその層の全てのユニットの入力を格納した配列 u_ary
に対して
def activate(u_ary)
u_ary.map { |u| @f[u] }
end
のように使用されます。なので、その層の全ての入力が関係してくるソフトマックス関数のような活性化関数を使いたい場合は BaseLayer#activate
メソッドを上書きします。
以下にソフトマックス関数とソフトマックス関数を使用している mp_hidden_layer.rb の実装を示します。
- $y_{k}$ 出力層k番目のユニットの出力値
- $u_{k}^{(L)}$ 出力層k番目のユニットへの入力値
y_{k} = \frac{\exp(u_{k}^{(L)})}{\sum_{j=1}^{K}\exp(u_{j}^{(L)})}
module Sabina::Layer
class OutputLayer < BaseLayer
# softmax function
def activate(u_ary)
sum = u_ary.inject(0.0) { |s, u| s + Math.exp(u) }
u_ary.map do |u|
Math.exp(u) / sum
end
end
end
end
おわりに
とりあえず、誤差を減らす方向に学習が進んだので実装は間違ってはないと思いますが、小さなミス等多々あると思いますので、お気づきの点がありましたら意見していただけると幸いです。
また、本gemを実装する際は参考文献の「深層学習」本に大変お世話になったので、この本を読みながらだといくらかソースコードが追いやすくなると思います。
RubyでもPythonのように機械学習が普及してほしいと願っています。
参考文献
岡谷 貴之(2015)「深層学習」講談社.