最小二乗法で解を求めるコード

 今回は前回に引き続き書籍のサンプルコードの下記部分を ruby で実装します。最小二乗法の公式を用いて係数を計算するメソッドです。 メソッドの戻り値として、決定された多項式と係数を返しています。

# 最小二乗法で解を求める
def resolve(dataset, m):
    t = dataset.y
    phi = DataFrame()
    for i in range(0,m+1):
        p = dataset.x**i
        p.name="x**%d" % i
        phi = pd.concat([phi,p], axis=1)
    tmp = np.linalg.inv(np.dot(phi.T, phi))
    ws = np.dot(np.dot(tmp, phi.T), t)

    def f(x):
        y = 0
        for i, w in enumerate(ws):
            y += w * (x ** i)
        return y

    return (f, ws)

 引用元のサンプルスクリプトの全体は下記で公開されています。

github.com

行列の結合

 python では pandas の concat メソッドで複数の pandas オブジェクトを結合します。

http://pandas.pydata.org/pandas-docs/stable/generated/pandas.concat.html

 オプションとして axis に 0 を指定すると縦方向の結合、1 を指定すると横方向の結合になります。 ここでは 1 を指定していますので、まず空の DataFrame を用意して、そこに dataset の x 列を i 乗した Series を順次横方向に追加してく形になります。

 ruby では空の Daru::DataFrame インスタンスは作成できないようでしたので、まず Hash に行列の内容を構成して、それを元に Daru::DataFrame のインスンタンスを作成しました。

  columns = {}
  (m+1).times do |i|
    columns["x**#{i}"] = dataset.x ** i
  end
  phi = Daru::DataFrame.new(columns)

転置行列

python では転置行列は pandas.DataFrame.T メソッドで取得することができます。

http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.T.html

ruby では Daru::DataFrame#transpose メソッドで取得することができます。

http://www.rubydoc.info/gems/daru/0.1.0/Daru%2FDataFrame%3Atranspose

irb(main):016:0* dataset = create_dataset(10)
=> #<Daru::DataFrame(10x2)>
                     x          y
          0        0.0 0.35893030
          1 0.11111111 0.75466334
          2 0.22222222 1.16130076
          3 0.33333333 0.76565346
          4 0.44444444 0.56397365
          5 0.55555555 -0.5168586
          6 0.66666666 -1.2497477
          7 0.77777777 -0.7234279
          8 0.88888888 -0.4937113
          9        1.0 -0.4791075
irb(main):017:0> 
irb(main):018:0* dataset.transpose
=> #<Daru::DataFrame(2x10)>
                     0          1          2          3          4          5          6          7          8          9
          x        0.0 0.11111111 0.22222222 0.33333333 0.44444444 0.55555555 0.66666666 0.77777777 0.88888888 1.0
          y 0.35893030 0.75466334 1.16130076 0.76565346 0.56397365 -0.5168586 -1.2497477 -0.7234279 -0.4937113 -0.479 075
irb(main):019:0> 

行列の積

 python では pandas.DataFrame.dot メソッドで行列の積を計算できます。

http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.dot.html

 ruby では Daru::DataFrame で * メソッドが定義されているので、* で積を計算することができるのですが、今回は元の行列と転置行列をかけているため、indexに数値と文字列が混ざることになり、下記のようなエラーになってしまいます。

irb(main):024:0* dataset
=> #<Daru::DataFrame(10x2)>
                     x          y
          0        0.0 -0.0742627
          1 0.11111111 0.64832694
          2 0.22222222 1.62979372
          3 0.33333333 1.16074147
          4 0.44444444 0.19131551
          5 0.55555555 0.09922296
          6 0.66666666 -0.6080503
          7 0.77777777 -0.9894763
          8 0.88888888 -0.4535080
          9        1.0 0.03518189
irb(main):025:0> 
irb(main):026:0* dataset * dataset
=> #<Daru::DataFrame(10x2)>
                     x          y
          0        0.0 0.00551495
          1 0.01234567 0.42032782
          2 0.04938271 2.65622758
          3 0.11111111 1.34732077
          4 0.19753086 0.03660162
          5 0.30864197 0.00984519
          6 0.44444444 0.36972517
          7 0.60493827 0.97906348
          8 0.79012345 0.20566954
          9        1.0 0.00123776
irb(main):027:0> 
irb(main):028:0* dataset * dataset.transpose
ArgumentError: comparison of Symbol with 0 failed
        from /usr/local/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/daru-0.1.4.1/lib/daru/maths/arithmetic/dataframe.rb:62:in `sort'
        from /usr/local/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/daru-0.1.4.1/lib/daru/maths/arithmetic/dataframe.rb:62:in `dataframe_binary_operation'
        from /usr/local/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/daru-0.1.4.1/lib/daru/maths/arithmetic/dataframe.rb:55:in `binary_operation'
        from /usr/local/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/daru-0.1.4.1/lib/daru/maths/arithmetic/dataframe.rb:18:in `*'
        from (irb):28
        from /usr/local/rbenv/versions/2.3.1/bin/irb:11:in `<main>'

 なので今回は Daru::DataFrame#to_matrix メソッドで標準の Matrix クラスに変換した上で * メソッドで計算します。

http://www.rubydoc.info/gems/daru/0.1.0/Daru%2FDataFrame%3Ato_matrix

irb(main):034:0* dataset.to_matrix * dataset.transpose.to_matrix
=> Matrix[[0.00551495178032287, -0.04814652292445256, -0.12103291720768458, -0.08619982081517798, -0.014207610754013434, -0.007368567440448755, 0.045155470293611104, 0.07348120775362572, 0.033678741797590464, -0.002612703024438115], [-0.04814652292445256, 0.4326735009138319, 1.0813305373090474, 0.7895770073355075, 0.1734177183698852, 0.12605731675017345, -0.3201413193247968, -0.555084433087857, -0.19525605324040052, 0.13392047936519433], [-0.12103291720768458, 1.0813305373090474, 2.705610300861487, 1.9658432484239334, 0.4105702573458222, 0.28516975670661376, -0.8428484217621567, -1.4398028683439108, -0.5415937066084376, 0.2795614486454444], [-0.08619982081517798, 0.7895770073355075, 1.9658432484239334, 1.4584318864404977, 0.3702160011396125, 0.3003573967170241, -0.4835669855859539, -0.8892670006157779, -0.23010930503481497, 0.37417041433074033], [-0.014207610754013434, 0.1734177183698852, 0.4105702573458222, 0.3702160011396125, 0.23413249035458505, 0.2658964729681377, 0.17996683941236247, 0.1563768318219559, 0.30829860276768317, 0.45117528617537195], [-0.007368567440448755, 0.12605731675017345, 0.28516975670661376, 0.3003573967170241, 0.2658964729681377, 0.31848717220014827, 0.3100378159950401, 0.33391998590584915, 0.4488287470660896, 0.5590464071902203], [0.045155470293611104, -0.3201413193247968, -0.8428484217621567, -0.4835669855859539, 0.17996683941236247, 0.3100378159950401, 0.8141696167863541, 1.120169924736844, 0.8683482991417188, 0.6452743066777754], [0.07348120775362572, -0.555084433087857, -1.4398028683439108, -0.8892670006157779, 0.1563768318219559, 0.33391998590584915, 1.120169924736844, 1.5840017535734776, 1.1400935207548575, 0.7429661273074588], [0.033678741797590464, -0.19525605324040052, -0.5415937066084376, -0.23010930503481497, 0.30829860276768317, 0.4488287470660896, 0.8683482991417188, 1.1400935207548575, 0.9957930064525073, 0.872933617825996], [-0.002612703024438115, 0.13392047936519433, 0.2795614486454444, 0.37417041433074033, 0.45117528617537195, 0.5590464071902203, 0.6452743066777754, 0.7429661273074588, 0.872933617825996, 1.001237765508352]]

逆行列

 python では pandas.linalg.inv メソッドで逆行列が取得できます。

https://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.linalg.inv.html

 ruby では Matrix#inv メソッドで取得できます。

irb(main):050:0* (dataset.to_matrix * dataset.transpose.to_matrix).inv                                                
=> Matrix[[6.438056238931283e+17, -3.5959222998233816e+16, 4.88081712482937e+16, -4.18039023242074e+16, -4.90936881765618e+16, 1.9473195412556384e+16, 3.021796172248485e+16, -1.1756838226941134e+17, 1.104440295083465e+17, -8.790885266451936e+15], [7.894377025631253e+15, 4.130512217329567e+15, 1.226948454741468e+15, -1.080925575443484e+15, 9.51855025409944e+15, 1576790734528046.5, -1355771888949234.5, 1.191314652268012e+16, -1.3688955666893698e+16, -1.671726231048433e+15], [-4.530896280313147e+15, 6.832627236988067e+15, -256467997105680.2, -2.854089097619272e+15, -361754002852689.25, 1442321524185798.5, -293843073250848.9, 2.291785009164425e+15, -2164437097239707.2, -53995045129483.5], [6.56400708662395e+15, -8.278847572597992e+15, 2945428805276741.5, 611874690847290.5, -8.040224399925562e+15, 4.146425733346407e+15, 1.0063070988405478e+16, -1.099361383748988e+16, 7.012887589668771e+15, -3.060566106986782e+15], [2.0276047297689546e+17, -2.150234284194681e+16, 2.325837285911574e+15, 1.3855500557957406e+16, -9.66616800705062e+15, -1.4971038030737476e+16, -6.859023781680976e+15, -2.61127868104491e+16, 3.872186122638793e+16, 330309925585268.0], [1.1925716211143941e+17, -1.6248336393828732e+16, 6.71882570034233e+15, 7.539028212975366e+15, -2.6810813298632516e+16, 1.540682966685851e+16, 8.331481526823681e+15, -2.4692863560056996e+16, 3.42380291696946e+16, -1.5626708042627156e+16], [-6.604745687626929e+15, 727565952289667.5, 1.286427881109916e+15, 1508601318395916.8, -1.6562628335161202e+16, -4.61937542936672e+15, 2.284703494536934e+15, -3.360855015175375e+15, 7.170630317185643e+15, 3774876268811336.5], [6.0918212393723944e+16, -3.266090027295123e+15, 1.229518586531585e+15, 4197189849440108.5, -2.435086220896028e+15, 4.660830138590578e+15, 5.09467236899953e+15, -3.794140272523739e+15, 1.59348998039823e+15, -4.678366072552667e+15], [-1.1382587909489373e+17, 9.677645630477756e+15, -2.030349802911037e+15, -7.640675796532893e+15, 1.8680150397993788e+16, 7.114574957016326e+15, 1.658128693269082e+15, 1.4272195974858228e+16, -1.875905159959477e+16, -5.863347355441972e+15], [-1.00227143117868e+17, 1.2818655421880554e+16, -5.836686165645619e+15, -7.27446600919549e+15, 1.7224948871781866e+16, -1.0652985027149534e+16, -1.1678113837058996e+16, 1.966068157926607e+16, -2.591158029231125e+16, 1.6086644321288758e+16]]

resolveメソッドをrubyで

ここまでの内容を踏まえてサンプルコードの resolve メソッドを ruby で実装します。

def resolve(dataset, m)
  t = dataset.y

  columns = {}
  (m+1).times do |i|
    columns["x**#{i}"] = dataset.x ** i
  end
  phi = Daru::DataFrame.new(columns)

  tmp = (phi.transpose.to_matrix * phi.to_matrix).inv
  ws = (tmp * phi.transpose.to_matrix) * Vector.elements(t.to_a)

  f = lambda {|x|
    y = 0
    ws.each_with_index do |w, i|
      y = y + w * (x ** i)
    end

    y
  }

  return f, ws
end

 下記については、 (tmp * phi.transpose.to_matrix) の結果は標準の Matrix クラスになり、t は Daru::Vector になるのですが、 標準の Matrix と Daru::Vector はそのまま内積が計算できないので、 Daru::Vector を標準の Vector クラスに変換した上で内積を計算しています。

  tmp = (phi.transpose.to_matrix * phi.to_matrix).inv
  ws = (tmp * phi.transpose.to_matrix) * Vector.elements(t.to_a)

 また、python版ではメソッド内でさらに def でメソッドを定義して return でメソッドを返している部分を、 ruby では lambda を返すように実装しています。

 python版とruby版それぞれの実行結果は下記のようになりました。

>>> train_set                                                  
          x         y                                          
0  0.000000 -0.333941                                          
1  0.111111  1.425262                                          
2  0.222222  0.853840                                          
3  0.333333  0.982145                                          
4  0.444444  0.244465                                          
5  0.555556 -0.136371                                          
6  0.666667 -0.961705                                          
7  0.777778 -1.462275                                          
8  0.888889 -0.668861                                          
9  1.000000 -0.216649                                          
>>>                                                            
>>> f, ws = resolve(train_set, 3)                               
>>>                                                            
>>> type(f)                                                    
<type 'function'>                                              
>>>                                                                                                        
>>> ws                                                         
array([ -0.12912272,  13.31977786, -38.77768783,  25.50897351])
irb(main):072:0* train_set
=> #<Daru::DataFrame(10x2)>
                     x          y
          0        0.0 -0.3822801
          1 0.11111111 0.74486105
          2 0.22222222 0.83353384
          3 0.33333333 0.53626611
          4 0.44444444 0.31220508
          5 0.55555555 -0.6527569
          6 0.66666666 -1.2765675
          7 0.77777777 -0.9600806
          8 0.88888888 -0.7323687
          9        1.0 -0.5115570
irb(main):073:0> 
irb(main):074:0* f, ws = resolve(train_set, 3)
=> [#<Proc:0x007f496bd59c90@/vagrant/02-square_error.rb:42 (lambda)>, Vector[-0.2636530068380138, 10.784779039058368, 
-31.470536541107172, 20.64727993759569]]
irb(main):075:0> 
irb(main):076:0* f.class
=> Proc
irb(main):077:0> 
irb(main):078:0* ws
=> Vector[-0.2636530068380138, 10.784779039058368, -31.470536541107172, 20.64727993759569]

誤差関数(最小二乗法)による回帰分析サンプルのデータセット作成コード

 とりあえず前回で ruby と python のコードを動かす環境を作ったので、サンプルコードを ruby に書き換えていきます。まずは誤差関数(最小二乗法)による回帰分析のサンプルコード。書籍のコードは下記に公開されています。

github.com

 この中で、まず今回はデータセットを作成するためのコードをrubyで書いてみます。上記サンプルコードの中の下記の部分です。

import numpy as np
from pandas import Series, DataFrame

from numpy.random import normal

# データセット {x_n,y_n} (n=1...N) を用意
def create_dataset(num):
    dataset = DataFrame(columns=['x','y'])
    for i in range(num):
        x = float(i)/float(num-1)
        y = np.sin(2*np.pi*x) + normal(scale=0.3)
        dataset = dataset.append(Series([x,y], index=['x','y']),
                                 ignore_index=True)
    return dataset

 ここでは下記のライブラリが使われています。

  • NumPy:ベクトルや行列を扱う数値計算ライブラリ
  • pandas:Rに類似のデータフレームを提供するライブラリ

 こういったライブラリが充実しているのが、pythonが機械学習系に強い理由の一つですね。 これらのライブラリの中で、サンプルコードの中で使われている内容を、rubyで書くにはどうするのが良いか調べてみました。

pandas.DataFrame

http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html#pandas.DataFrame

 pythonのDataFrameクラスはスプレッドシートのようなデータ構造を持っており、サンプルの中では下記のように使われています。

>>> from pandas import DataFrame
>>> 
>>> df = DataFrame(columns = ['x', 'y'])
>>> df
Empty DataFrame
Columns: [x, y]
Index: []
>>> 

 DataFrameのコンストラクタに渡している columns パラメータの意味は下記のように説明されています。データカラムのラベルとして使われます。

columns : Index or array-like Column labels to use for resulting frame. Will default to np.arange(n) if no column labels are provided

 rubyで pandas にあたるライブラリとして、daruというgemがあるようなのでこれを利用し、Daru::DataFrameクラスを使用します。

Daru::DataFrame
http://www.rubydoc.info/gems/daru/0.1.4.1/Daru/DataFrame

irb(main):001:0> require 'daru'

Install the reportbuilder gem version ~>1.4 for using reportbuilder functions.

Install the spreadsheet gem version ~>1.1.1 for using spreadsheet functions.
=> true
irb(main):002:0> df = Daru::DataFrame.new({'x': [], 'y': []})
=> #<Daru::DataFrame(0x2)>
   x   y
irb(main):003:0> 

pandas.Series

http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.html#pandas.Series

 Seriesは一次元配列に似た構造を持っており、サンプルコードでは下記のような使われ方をしています。

>>> from pandas import Series
>>>                                       
>>> x = 0.0                               
>>> y = 1.0                               
>>> s = Series([x, y], index = ['x', 'y'])
>>> s                                     
x    0.0                                  
y    1.0                                  
dtype: float64                            
>>>                                       

 rubyではDataFrameと同じくdaruのVectorクラスを使用します。

Daru::Vector http://www.rubydoc.info/gems/daru/0.1.4.1/Daru/Vector

irb(main):005:0* x = 0.0
=> 0.0
irb(main):006:0> y = 1.0
=> 1.0
irb(main):007:0> v = Daru::Vector.new([x, y], index: [:x, :y])
=> #<Daru::Vector(2)>
   x 0.0
   y 1.0
irb(main):008:0> 

pandas.DataFrame.append

http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.append.html#pandas.DataFrame.append

 サンプルコードでは空のDataFrameに下記のようにappendメソッドでSeriesのインスタンスを追加しています。

>>> dataset = DataFrame(columns = ['x', 'y'])
>>> s = Series([x, y], index = ['x', 'y'])   
>>> dataset.append(s, ignore_index = True) 
     x    y                                
0  0.0  1.0                                
>>>                                        

 rubyではDataFrameクラスのadd_rowメソッドでVectorインスタンスを追加します。

Daru::DataFrame.add_row
http://www.rubydoc.info/gems/daru/0.1.4.1/Daru%2FDataFrame%3Aadd_row

irb(main):026:0* dataset = Daru::DataFrame.new({x: [], y: []})
=> #<Daru::DataFrame(0x2)>
   x   y
irb(main):027:0> v = Daru::Vector.new([x, y], index: [:x, :y])
=> #<Daru::Vector(2)>
   x 0.0
   y 1.0
irb(main):028:0> dataset.add_row(v)
=> #<Daru::Vector(2)>
   x 0.0
   y 1.0
irb(main):029:0> dataset
=> #<Daru::DataFrame(1x2)>
       x   y
   0 0.0 1.0
irb(main):030:0> 

numpy.pi

 pythonではpi(円周率)はnumpyに定数として用意されています。

nullege.com

>>> np.pi        
3.141592653589793
>>>              

 rubyではMathモジュールの定数として用意されています。

Math::PI
https://docs.ruby-lang.org/ja/latest/class/Math.html#C_-P-I

irb(main):002:0* Math::PI
=> 3.141592653589793
irb(main):003:0> 

numpy.sin

 pythonでは三角関数のsinを求めるメソッドはnumpyに用意されています。

numpy.sin
https://docs.scipy.org/doc/numpy-1.9.1/reference/generated/numpy.sin.html

>>> np.sin(2 * np.pi)  
-2.4492935982947064e-16
>>>                    

 rubyではMathモジュールに用意されています。

Math.sin
https://docs.ruby-lang.org/ja/latest/class/Math.html#M_SIN

irb(main):004:0* Math.sin(2 * Math::PI)
=> -2.4492935982947064e-16
irb(main):005:0> 

正規分布

 pythonでは正規分布を求めるメソッドがnumpyに用意されています。

numpy.random.normal
https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.normal.html

>>> np.random.normal(scale = 0.3)
0.0948107519352898               
>>>                              

 rubyでは調べてみた限りでは正規分布を求めるメソッドは用意されていないようなので、下記サイトを参考にボックス=ミュラー法で正規分布を求めるメソッドを定義します。

乱数発生の手法

irb(main):363:0* def normal_rand(mu = 0, sigma = 1.0)
irb(main):364:1>   random = Random.new
irb(main):365:1>   (Math.sqrt(-2 * Math.log(random.rand)) * Math.sin(2 * Math::PI * random.rand) * sigma) + mu
irb(main):366:1> end
=> :normal_rand
irb(main):367:0> 
irb(main):368:0* normal_rand(0, 0.3)
=> -0.5960439895226316
irb(main):369:0> 

 下記のようなgemもあるようですが、あまり使われている感じではなさそうかなと。

bitbucket.org

create_datasetメソッドをrubyで

 ここまでの内容を使ってサンプルコードのcreate_datasetメソッドをrubyで実装します。

require 'daru'

def normal_rand(mu = 0, sigma = 1.0)
  random = Random.new
  (Math.sqrt(-2 * Math.log(random.rand)) * Math.sin(2 * Math::PI * random.rand) * sigma) + mu
end


def create_dataset(num)
  dataset = Daru::DataFrame.new({'x': [], 'y': []})  

  num.times do |i|
    x = i.to_f / (num - 1).to_f
    y = Math.sin(2 * Math::PI * x) + normal_rand(0, 0.3)
    dataset.add_row(Daru::Vector.new([x, y], index: [:x, :y]))
  end
  
  return dataset
end

 元のサンプルコードとrubyで実装したメソッドのそれぞれの実行結果は下記のようになりました。

>>> create_dataset(10) 
          x         y  
0  0.000000 -0.470924  
1  0.111111  0.417868  
2  0.222222  0.420972  
3  0.333333  0.494881  
4  0.444444  0.654892  
5  0.555556 -0.737098  
6  0.666667 -0.605475  
7  0.777778 -1.809355  
8  0.888889 -1.211044  
9  1.000000 -0.114083  
>>>                    
irb(main):382:0* create_dataset(10)
=> #<Daru::DataFrame(10x2)>
                     x          y
          0        0.0 -0.3909174
          1 0.11111111 0.78616375
          2 0.22222222 1.11320947
          3 0.33333333 0.97217041
          4 0.44444444 0.21530738
          5 0.55555555 -0.8419577
          6 0.66666666 -0.8907612
          7 0.77777777 -1.2237687
          8 0.88888888 -1.0662619
          9        1.0 0.26103827
irb(main):383:0> 

 とりあえずちゃんと動いているようですが、他の部分の実装を進める上で不都合があった場合は都度修正していきたいと思います。

2016/11/15 追記: rubyのnormal_randへの引数の渡し方が間違っていたので修正しました。

Vagrant + rbenv + pyenv で機械学習の勉強用環境構築

 機械学習の勉強をしようと下記書籍を読み始めました。機械学習といえばやはり言語はPythonなのですが、普段Rubyをメインで使っている自分としては、Rubyで同様のことができないかなと思い、書籍のサンプルコード実行の為の環境に加えてRubyの実行環境も用意し、Pythonのサンプルコードの内容をRubyに置き換えていくことに挑戦してみたいと思います。結果として機械学習はやっぱりPythonだね、ということになる可能性は大いにありますが。。

ITエンジニアのための機械学習理論入門
https://www.amazon.co.jp/IT-ebook/dp/B016Q22IX2/

 ということでまず今回は環境作成です。環境はVagrantで作っておいた方があとで何かと便利なので、VagrantでCentOSのVMを用意し、そこにrbenvとpyenvをインストールしてRubyとPythonの実行環境を用意します。CentOSを使うのは、上記書籍での例でCentOSが使われているためです。

Vagrantfile

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  # Every Vagrant virtual environment requires a box to build off of.
  config.vm.box = "esss/centos-7.1-desktop"

  # Create a private network, which allows host-only access to the machine
  # using a specific IP.
  config.vm.network :private_network, ip: "192.168.59.104"

  config.vm.provider "virtualbox" do |vb|
    vb.customize ["modifyvm", :id, "--memory", "2048"]
  end

  config.vm.provision :shell, path: "provisionings/libs.sh"
  config.vm.provision :shell, path: "provisionings/rbenv.sh"
  config.vm.provision :shell, path: "provisionings/pyenv.sh"
  config.vm.provision :shell, path: "provisionings/ruby2.3.1.sh"
  config.vm.provision :shell, path: "provisionings/anaconda2-4.1.1.sh"
  config.vm.provision :shell, path: "provisionings/env.sh"
  config.vm.provision :shell, path: "provisionings/os_env.sh"
end

 サンプルコードの実行結果としてグラフを表示するためにGUI環境が必要なので、Vagrantのboxには esss/centos-7.1-desktop を使用します。

Vagrant box esss/centos-7.1-desktop | Atlas by HashiCorp

 必要なライブラリやrbenv, pyenv等のインストール用の設定は provisionings ディレクトリを作成してその下にまとめ、 プロビジョニングで実行されるようにします。ファイルを細分化するのは、あとで一部だけ変更して再実行したいときに、そのファイルだけ run オプションで "always" を指定すれば、 vagrant reload 時に再実行できるようにするためです。例えば provisionings/env.sh の内容を変更して再実行したいときは、下記のような記述に変更して vagrant reload します。

config.vm.provision :shell, path: "provisionings/env.sh", run: "always"

 Rubyの違うバージョンを追加でインストールしたいときは、provisionings/ruby2.0.0.sh のようにファイルを用意して、下記のような記述を追加して vagrant reload すればインストールされます。

config.vm.provision :shell, path: "provisionings/ruby2.0.0.sh", run: "always"

 一度実行した後は run: "always" を削除しておけば、次回以降は実行されません。また、各プロビジョニングファイルの内容はデフォルトで root アカウントとして実行されるので、 sudo は不要です。(参考:下記ドキュメントページの privileged についての説明を参照)

www.vagrantup.com

各プロビジョニングファイル

provisionings/libs.sh

#!/bin/bash

yum -y update
yum install -y git
yum install -y openssl-devel readline-devel zlib-devel

 rbenvとpyenvのインストールに必要なgit等をインストールしておきます。

provisionings/rbenv.sh

#!/bin/bash

RBENV_ROOT=/usr/local/rbenv

git clone https://github.com/sstephenson/rbenv.git ${RBENV_ROOT}
git clone https://github.com/sstephenson/ruby-build.git ${RBENV_ROOT}/plugins/ruby-build

echo "export RBENV_ROOT=${RBENV_ROOT}" >> /etc/profile.d/rbenv.sh
echo 'export PATH="${RBENV_ROOT}/bin:$PATH"' >> /etc/profile.d/rbenv.sh
echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh
source /etc/profile.d/rbenv.sh
rbenv --version

${RBENV_ROOT}/plugins/ruby-build/install.sh

 システムワイドで rbenv が使えるように /usr/local/rbenv にインストールします。また、ログイン時にパスが通るように /etc/profile.d/rbenv.sh として設定ファイルを追加します。ちなみに rbenv の環境構築についてググっていくつか記事を見てみましたが、 ruby-build の install.sh の実行について書かれていない記事が多かった気がしました。私の環境ではこれを実行しないと ruby-build が使えるようにならなかったのですが、環境によるものなんですかね?

provisionings/pyenv.sh

#!/bin/bash

PYENV_ROOT=/usr/local/pyenv

git clone https://github.com/yyuu/pyenv.git ${PYENV_ROOT}

echo "export PYENV_ROOT=${PYENV_ROOT}" >> /etc/profile.d/pyenv.sh
echo 'export PATH="${PYENV_ROOT}/bin:$PATH"' >> /etc/profile.d/pyenv.sh
echo 'eval "$(pyenv init -)"' >> /etc/profile.d/pyenv.sh
source /etc/profile.d/pyenv.sh

 pyenv も rbenv と同様にインストールします。pyenv では rbenv の ruby-build にあたるものは不要のようです。

provisionings/ruby2.3.1.sh

#!/bin/bash

rbenv install 2.3.1

 ruby2.3.1をインストールします。古い rbenv だと ruby をインストールした後に rehash が必要でしたが、最近のバージョンでは rehash は不要になっています。

github.com

provisionings/anaconda2-4.1.1.sh

#!/bin/bash

pyenv install anaconda2-4.1.1
pyenv rehash

 Pythonの実行環境を anaconda を使って用意します。書籍の実行環境のセットアップの説明では、必要なツールやライブラリが一括でセットアップされるように Enthought Canopy を使用する方法が紹介されていますが、 anaconda によるインストールでも機械学習関連のライブラリが一通り一括でインストールされるため、こちらを使用しています。また、書籍のサンプルではpython2.7系を使っているので、anacondaの2系を使用します。書籍のサンプル実行において必要なライブラリは下記の通りです。

  • NumPy
  • SciPy
  • matplotlib
  • pandas
  • PIL
  • scikit-learn
  • IPython

provisionings/env.sh

#!/bin/bash

rbenv global 2.3.1
pyenv global anaconda2-4.1.1

 rbenvとpyenvで使用するrubyとpythonのバージョンを指定します。特にディレクトリでの切り分けも現状は必要ないので、globalで同じバージョンを使用します。

provisionings/os_env.sh

#!/bin/bash

echo 'vagrant' | passwd --stdin vagrant

 Vagrantで作成したVMのユーザとパスワードはいずれもvagrantだと思っていたのですが、GUIでそのパスワードだとログインできなかったため、ここで vagrant ユーザのパスワードを設定し直しています。

起動後の確認

 vagrant up 後の ruby と python のバージョンを確認してみます。

$ vagrant ssh
Last login: Sun Nov  6 06:25:32 2016
[vagrant@localhost ~]$ 
[vagrant@localhost ~]$ ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]
[vagrant@localhost ~]$ 
[vagrant@localhost ~]$ python -V
Python 2.7.12 :: Anaconda 4.1.1 (64-bit)

 想定通りのバージョンが使われるようになっています。また、GUI環境でログインし、IPythonからサンプルスクリプトを実行したところ、正しく実行されグラフが表示されました。

f:id:akanuma-hiroaki:20161106153533p:plain

vagrantディレクトリのマウントエラーの対処

 ここまでの手順を終えた後に vagrant reload もしくは、 vagrant halt / vagrant up した場合に、下記のようなエラーが出て vagrant ディレクトリのマウントに失敗することがあります。

Failed to mount folders in Linux guest. This is usually because
the "vboxsf" file system is not available. Please verify that
the guest additions are properly installed in the guest and
can work properly. The command attempted was:

mount -t vboxsf -o uid=`id -u vagrant`,gid=`getent group vagrant | cut -d: -f3` vagrant /vagrant
mount -t vboxsf -o uid=`id -u vagrant`,gid=`id -g vagrant` vagrant /vagrant

The error output from the last command was:

/sbin/mount.vboxsf: mounting failed with the error: No such device

 ホストOSとゲストOS間のディレクトリ共有機能は Vagrant の Guest Additions によって提供されていますが、 yum -y update することで kernel が更新された場合に、古いバージョンの Kernel でビルドされた Guest Additions が動作しなくなるためにエラーになるようです。VMは起動していますので、 vagrant ssh でログインした後に下記コマンドを実行して Guest Additions を再インストールしてから vagrant reload することで解消されます。

$ sudo /etc/rc.d/init.d/vboxadd setup
Removing existing VirtualBox non-DKMS kernel modules       [  OK  ]
Building the VirtualBox Guest Additions kernel modules
Building the main Guest Additions module                   [  OK  ]
Building the shared folder support module                  [  OK  ]
Building the OpenGL support module                         [  OK  ]
Doing non-kernel setup of the Guest Additions              [  OK  ]
Starting the VirtualBox Guest Additions                    [  OK  ]

 Guest Additions の再インストールを自動化してくれる、 vagrant-vbguest というプラグインがあるようなのですが、私の環境ではこのプラグインをインストールしようとするとエラーになってしまいました。原因を追求して解決したいところではありますが、本来の目的とは違うので、ひとまず手動での対応ができれば良いということにしておきます。

 ちなみに書き終わってから知ったのですが、 anyenv という、rbenv や pyenv など複数の **env をまとめて管理できるものがあるようなので、いずれはそちらに乗り換えてみたいと思います。

からあげ Beer Bash を開催(社内イベント)

 先日社内イベントとして、開発部で「からあげ Beer Bash」を開催しました。イベント名は、唐揚げとビール片手に交流しましょう、ということでつけました。今まであまり開発部内でこういったイベントはできていなかったのですが、下記のような意図から今回やってみることにしました。

意図・目的

  • リアルなコミュニケーション機会の増加
  • ディスカッション機会の増加
  • プレゼンの練習の場

 弊社では開発部は自宅勤務を取り入れていることもあり、全員が一堂に会することがあまり多くありません。普段のコミュニケーションはSlackを中心に行っていて、業務に支障はないのですが、やはりリアルなコミュニケーションに勝るものではありません。また、何かディスカッションするにしても、やはり直接話す方がやりやすいものです。なので、気軽に対面でのコミュニケーションをする機会を定期的に持つことで、部内での風通しをさらに良くしていきたいと思っています。

 それともう一つの側面として、各メンバーにプレゼンの練習の場を提供したいということもありました。開発部メンバーといえども技術的なスキルだけではなく、アウトプットのスキルも磨いて、外部の勉強会等でも発表の機会があれば積極的に発表して欲しいと思っています。アウトプットすることによって技術的なスキルも確かなものになっていきますし、個人のキャリア形成、ブランディングにもつながります。また、それが結果的に会社にとってもプラスになります。

事前準備

 今回は業務外の時間ということで、任意で参加者を募りました。発表者もやりたい人がやるという感じで、事前にConfluenceにページを作って、やりたい人は名前と内容を書いておくという形にしました。

 飲み物・食べ物は、からあげ Beer Bash ということで、基本的に唐揚げとビールだけです。色々手配しようと思うと準備が大変になるので。その分、量は多くということで、10人の参加者で唐揚げ100個を手配しました。ビールは以前オフィスで懇親会をやったときの余りがあったのでそれを消費+チューハイを少々買い足した感じです。

f:id:akanuma-hiroaki:20161025084202j:plain:w300,left 唐揚げ 10個 x 10パック。オフィス近くの なか卯 に事前に予約しておきました。

からあげ Beer Bash 当日

 当日は業務終了後に執務スペース内のミーティングスペースのテーブルを移動して、プロジェクタを設置して会場を準備しました。そしてさらっと乾杯した後にとりあえず私から今回のイベントのイントロダクションを兼ねてLTして、その後は発表予定のメンバーで、やりたい人から発表していくという形。特に制限時間等も設けずにゆるい感じでやってみました。

 こういう形でそれぞれのメンバーに発表してもらうというのは初めてだったのでどうなるかなーと思っていたのですが、皆熱心に発表してくれて、聞いているメンバーからの質問もあり、思ったより良い感じのイベントになったのではないかと思っています。

 発表内容についても特にテーマを限定していなかったので、業務に直結するものではなくても、お互いにどんなことに興味を持っていて、どんなことを考えているのかが見えたのが良かったと思います。普段は基本的には業務に関係ない話をする時間はあまり取れないのと、飲み会やランチのときに話すにしても技術的な内容などしっかり話すのは難しいので、良い機会になったのではないかと思います。

f:id:akanuma-hiroaki:20161026082301j:plain

f:id:akanuma-hiroaki:20161025091646j:plain

 一通り発表が終わったところで一旦締めて、後は各自時間の許す限り懇親会という形で、みんな終電近くまで話していました。

良かったこと

 重複するところもありますが、改めて今回開催して良かったことをまとめてみます。

  • 業務と関係ないところでそれぞれどんなことに興味があるかとか、どんなことをやっているかがわかった
  • プレゼンで自分の考えを発表する練習の場になった
  • それぞれの発表に対して質問も複数あり、一方的に話すだけという感じにならなかった
  • 飲み物や食べ物にあまり力を入れ過ぎなかったことで、準備の手間をあまりかけない形で実施できた
  • 唐揚げ100個というインパクト
  • 残さず食べ切れた

改善したいこと

 逆に今後改善した方が良さそうなところもあるのでまとめてみます。

  • 一人あたりの発表の一応の制限時間を設けた方が良さそう。完全自由だと、毎回うまく収まるとは限らないので。
  • 懇親会の方で各発表に対してのフィードバックがもっとあっても良い。発表者のスキル向上のために。
  • リモートメンバーの参加のハードルを下げたい。
  • プロジェクタの性能があまり良くなく、部屋を暗くしないと良く見えないので、明るい中でも良く見える環境を用意したい。(部屋が暗いと雰囲気も暗くなりがちな気がする)

 また、今回は業務とは直結しない内容についての発表&ディスカッションで交流することを意図していたのですが、業務上解決すべき課題についてのディスカッションの時間も十分に確保できてはいないので、そちらもどのようにディスカッションしていくのがいいか、考えていきたいと思っています。

 ともあれ今回はそれなりにうまくいったと思うので、ひとまず月一回ペースで開催してきたいと思っています。唐揚げは増量かな?

システム思考セミナー

 先日、システム思考のセミナーに参加してきました。

learningvesper.doorkeeper.jp

 イベントやセミナー情報のメルマガでたまたま今回のセミナーを見かけて、課題の深掘りをできるようにしていくというところに興味を持ちました。

システム思考とは

 システム思考がどんなものかは、今回のセミナーの告知ページに下記のように説明されています。

ビジネス課題など対象の物事に影響を与える構造を見極め、
その要素間の因果関係をグラフとして表し、
その構造を利用して振舞の特徴把握や定性的な分析を行う考え方。

 つまりは対象の領域の構造を図解して、現状の課題を視覚的に捉えることによって、そこから特徴や関連を見つけやすくするということかと思います。

 また、根本原因を分析するためのツールではなく、現状を正しく把握するためのツールということで、使いどころを間違えないように注意が必要のことでした。

ワークショップ

 セミナーでは最初にシステム思考についての説明が少しあった後、実際にシステム思考で課題を表してみるというワークショップが実施されました。入門編ということもあり、下記参考書籍の中から2問ほど、状況の説明が与えられてそれを元に課題を図式するという問題に取り組み、図式したものを近くの人と見せ合うということをやりました。

www.amazon.co.jp

やってみた感想

  • 与えられた状況についてどこから図(グラフ)にしていくのかのとっかかりを考えるのに少し苦労しました。一度どこから取り掛かるかを決めてしまえれば、それなりに芋づる式でグラフを広げていくことができます。

  • グラフの各要素を事実(ファクト)にするのか、取り得るアクションにするのかで悩みました。また、それぞれの要素の粒度についても、どれぐらいが最適なのかというのは判断が難しいところでした。

  • ひとまず最初は細かいところは気にせず、また、全体の構成のようなところも一度置いておいて、思いつくままにグラフを書いていき、書いたもの全体を眺めた上でどことどこが関連しているかを考えるのが良さそうに思えました。

  • 原因や対策の検討につなげていくためには事実だけでなく、関連や原因を推測してそれもグラフに落としていくことが必要なので、推測の部分についても様々な角度から考える必要があります。

 図式するというのは現状を正しく把握するということに有効な手段だと改めて感じました。現状の課題を正しく把握することが、有効な解決策につながりますので、最初に見えていることだけでなく、色々な角度から物事を捉えていくのが大切ですね。

 セミナーで紹介されたシステム思考のツールについても、下記で詳しく説明されているので、あとで改めて読んでみようと思います。

www.change-agent.jp

 また、同僚から下記の書籍も紹介してもらいましたので、読んでみたいと思います。

www.amazon.co.jp

SlackでRSSフィードを通知する

 個人的にはFeedlyというRSS Readerを使って技術系のニュース等をチェックしているのですが、弊社の主なサービス対象である業界に関するニュースは、個人任せではなくチームとして最新の動向をキャッチアップできる仕組みを作っておいた方が何かと便利です。そこで、社内で使用しているSlackのチャンネルに業界関連のニュースのRSSフィードを通知するようにしてみました。最初はBOTを用意してRSSフィードを定期的に読むように実装が必要かと思っていましたが、SlackにはRSS Integrationが用意されていたので、非常に簡単に実現することができました。

RSS Integration を追加

 SlackにRSS Integrationを追加します。「RSS」で検索すると RSS Fox という Integration もヒットしますが、今回は RSS Integration を使用します。

f:id:akanuma-hiroaki:20161017082433p:plain

RSSフィードの登録

 続いて読み込むRSSフィードのURLと、通知するチャンネルを選択します。今回はあらかじめ業界関連ニュースのキーワード検索結果を通知するように設定しておいた Google Alert のRSSフィードを読み込みます。

f:id:akanuma-hiroaki:20161017082810p:plain

 RSSフィードが追加されました。

f:id:akanuma-hiroaki:20161017083342p:plain

 また、チャットウィンドウ上で下記コマンドを使うことでフィードの追加、削除、確認が行えます。

チャンネルにフィードを追加する: /feed subscribe http://kotaku.com/vip.xml
チャンネルに登録されているフィードのリストを表示する: /feed list
チャンネルからフィードを削除する: /feed remove ID

フィードの表示

 フィードの登録時点以降の通知が対象になるようで、登録直後には何も通知されませんが、登録後は定期的にRSSフィードがチェックされ、新しい記事があると通知されるようになります。

f:id:akanuma-hiroaki:20161017084450p:plain

 読み込んでいるフィードがキーワード検索の結果なので、全く関係ないニュースがヒットすることもありますが、チームとしての最新ニュースのキャッチアップは非常にやりやすくなると思います。

ユーザーローカルの人工知能ボットAPIを試す

 以前申し込んであったユーザーローカル人工知能ボットAPIのAPIキーが届いたので試してみました。

 下記サイトの開発者向けAPI申込フォームから利用登録をしておくと、後日APIキーがメールで送られてきます。

ai.userlocal.jp

 APIの機能はこちら

  1. 自動会話API
    ユーザー入力メッセージに対し、自然な受け答えを返す

  2. キャラクター会話変換API
    ネコやイヌなどの語尾に自動変換する 例)愉快ですね→愉快だニャ

  3. 氏名自動識別API
    会話中の相手の名前をもとに性別判定したり、姓・名を切り分ける

  4. 形態素解析API
    文章を単語に分割・活用形抽出するAPI

それぞれブラウザから直接URLを叩く感じでどんなレスポンスが返ってくるか試してみました。

自動会話API

APIキーのお知らせメールにも記載されていた下記リクエストで試してみました。

https://chatbot-api.userlocal.jp/api/chat?message=野球したい&key=xxxxxxxxxxxxxxxxxxxx

keyの部分は実際はメールで届いたAPIキーを入れます。
また、messageの内容は実際はURIエンコードしたものを指定します。

レスポンスはこんな感じでした。

{
  status: "success",
  result: "イチローすごいよね"
}

数回試しても同じレスポンスだったのですが、さらに同じリクエストを繰り返すと下記のようなレスポンスに変わりました。

{
  status: "success",
  result: "人って何度も同じこと言われたら怒るんだよ(# ゚Д゚)"
}

完全にランダムもしくは同一の返答かと思っていたのですが、何回続けて同じリクエストが来たかという状態を保持してそれを元にレスポンスを変えているということのようですね。

キャラクター会話変換API

自動会話APIのレスポンスの語尾を、いろいろなタイプのキャラクターに合ったものに変えてくれるというものです。
こちらは下記のような感じでリクエストを投げてみました。

https://chatbot-api.userlocal.jp/api/character?message=野球したい&key=xxxxxxxxxxxxxxxxxxxx&character_type=dog

こちらもmessageとkeyについては自動会話APIの時と同様です。
今回は追加でcharacter_typeというパラメータを指定して、どんなキャラクターのレスポンスにするかを指定しています。
この例では character_type=dog なので、犬のキャラクターを指定しています。
レスポンスは下記のようになりました。

{
  status: "success",
  result: "野球したいワン"
}

語尾が犬のキャラクターっぽくなってますね。
今度は character_type=roujin にしてみます。

https://chatbot-api.userlocal.jp/api/character?message=野球したい&key=xxxxxxxxxxxxxxxxxxxx&character_type=roujin

レスポンスは下記のように変わりました。

{
  status: "success",
  result: "野球したいのじゃ"
}

他にも character_type=cat を指定することができます。

氏名自動識別API

指名と渡すと姓・名と性別を推測してくれて、ニックネームの候補も提示してくれます。
リクエストは下記のようにしてみました。

https://chatbot-api.userlocal.jp/api/name?name=赤沼寛明&key=xxxxxxxxxxxxxxxxxxxx

結果は下記の通りです。

{
  status: "success",
  result: {
    last_name: "赤沼",
    last_name_yomi: "akanuma",
    first_name: "寛明",
    first_name_yomi: "ひろあき",
    gender: 1,
    gender_accuracy: 5,
    nickname: [
      "ひろぴー",
      "ひろぴょん",
      "akきち",
      "ひろたろー",
      "ひろタソ",
      "ひろすけ",
      "ひろべえ",
      "ひーちゃ",
      "寛太郎",
      "寛どん",
      "akひろ"
    ]
  }
}

姓・名と性別が正しく判定されています。

他にもいくつかの氏名で試してみたのですが、性別の判断がつかない場合は gender と gender_accuracy が 0 で返ってくるようです。

形態素解析API

日本語の文章を単語に分解してくれるAPIです。
リクエストは下記のようにしてみました。

https://chatbot-api.userlocal.jp/api/decompose?message=進捗どうですか?&key=xxxxxxxxxxxxxxxxxxxx

結果は下記のようになりました。

{
  status: "success",
  result: [
    {
      surface: "進捗どうですか",
      pos: "名詞",
      origin: "進捗どうですか",
      yomi: "シンチョクドウデスカ"
    },
    {
      surface: "?",
      pos: "記号",
      origin: "?",
      yomi: "?"
    }
  ]
}

クエスチョンマーク以外は正しく解析されませんでした。そこでmessageを「進捗はどうですか?」に変更してみたところ、下記のようになりました。

{
  status: "success",
  result: [
    {
      surface: "進捗",
      pos: "名詞",
      origin: "進捗",
      yomi: "シンチョク"
    },
    {
      surface: "は",
      pos: "助詞",
      origin: "は",
      yomi: "ハ"
    },
    {
      surface: "どう",
      pos: "副詞",
      origin: "どう",
      yomi: "ドウ"
    },
    {
      surface: "です",
      pos: "助動詞",
      origin: "です",
      yomi: "デス"
    },
    {
      surface: "か",
      pos: "助詞",
      origin: "か",
      yomi: "カ"
    },
    {
      surface: "?",
      pos: "記号",
      origin: "?",
      yomi: "?"
    }
  ]
}

今度は正しく解析してくれたようです。やはり文章が文法的に完全ではない場合の解析は十分ではないケースがありそうです。

パラメータに detail=true を追加すると、より詳細な情報を出してくれるようになります。

{
  status: "success",
  result: [
    {
      surface: "進捗",
      pos: "名詞",
      pos1: "サ変接続",
      pos2: "",
      pos3: "",
      form_type: "",
      form: "",
      origin: "進捗",
      yomi: "シンチョク",
      pronounce: "シンチョク"
    },
    {
      surface: "は",
      pos: "助詞",
      pos1: "係助詞",
      pos2: "",
      pos3: "",
      form_type: "",
      form: "",
      origin: "は",
      yomi: "ハ",
      pronounce: "ワ"
    },
    {
      surface: "どう",
      pos: "副詞",
      pos1: "助詞類接続",
      pos2: "",
      pos3: "",
      form_type: "",
      form: "",
      origin: "どう",
      yomi: "ドウ",
      pronounce: "ドー"
    },
    {
      surface: "です",
      pos: "助動詞",
      pos1: "",
      pos2: "",
      pos3: "",
      form_type: "特殊・デス",
      form: "基本形",
      origin: "です",
      yomi: "デス",
      pronounce: "デス"
    },
    {
      surface: "か",
      pos: "助詞",
      pos1: "副助詞/並立助詞/終助詞",
      pos2: "",
      pos3: "",
      form_type: "",
      form: "",
      origin: "か",
      yomi: "カ",
      pronounce: "カ"
    },
    {
      surface: "?",
      pos: "記号",
      pos1: "一般",
      pos2: "",
      pos3: "",
      form_type: "",
      form: "",
      origin: "?",
      yomi: "?",
      pronounce: "?"
    }
  ]
}

チャットプラットフォームとの連携もできる

 ユーザーローカルの人工知能ボットAPIは管理画面から設定することで、LINE、Facebook, Twitterと連携することができるようになっています。
 ただ試してみたところ、LINEについては先日 Line Messaging API が発表になり、これまでの BOT API が Deprecated になって仕様が変わったせいか、うまく連携できませんでした。Facebookの方もまだうまくいっていないので、もう少し試してみて連携できたらまた書きたいと思います。