2018年4月8日日曜日

IFTTT で cron を使って毎日どこかに GET リクエストを送信する方法

概要

例えば毎日 9 時に適当な URL に GET リクエストを送信します

環境

  • IFTTT (2018/03/30 時点)

Applet 作成方法

New Applet
ifttt_timer1.png

「+this」
ifttt_timer2.png

Date&Timer
ifttt_timer3.png

「Every day at」
ifttt_timer4.png

「09 AM」->「Create trigger」
ifttt_timer5.png

「that」
ifttt_timer6.png

Webhooks
ifttt_timer7.png

「Make a web request」
ifttt_timer8.png

「URL」「Method」「Content Type」「Body」を適当に入力 -> Create
ifttt_timer9.png

で終わりです
必要であれば Check now してください

自分は heroku 上にデプロイしたボットにリクエスト送ってます

2018年4月7日土曜日

Ruby で MySQL の基本操作

概要

mysql2 というライブラリを使って基本操作をしてみました
ruby-mysql という Pure Ruby のライブラリもあるようですが mysql2 のほうがメンテもされているようなのでオススメです

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • mysql2 0.5.0

インストール

MySQL 自体は今回 brew を使ってインストールしてしまいました

  • brew install mysql

ライブラリは RubyGems から引っ張ってきます

  • bundle init
  • vim Gemfile
gem "mysql2"
  • bundle install --path vendor

テスト用のテーブル作成

  • mysql -u root
mysql> create database test;
Query OK, 1 row affected (0.01 sec)

mysql> use test
Database changed
mysql> create table user (id int primary key, name varchar(20), age int, height int, weight int);
Query OK, 0 rows affected (0.03 sec)

でテーブルを作成しました
定義の詳細は以下の通りです

mysql> show create table user \G
*************************** 1. row ***************************
       Table: user
Create Table: CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `height` int(11) DEFAULT NULL,
  `weight` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

データ登録

  • vim insert.rb
require 'mysql2'

client = Mysql2::Client.new(:host => "localhost", :username => "root", :database => 'test')
users = [
  ['pika', 17, 80, 50],
  ['kai', 52, 180, 120],
  ['yado', 34, 120, 150],
  ['pijo', 23, 90, 60],
  ['koda', 2, 70, 65],
  ['kora', 10, 35, 30],
]
users.each.with_index(1) { |user, index|
  query = "insert into user values (#{index}, #{user.map { |v| v.instance_of?(String) ? "'#{v}'" : v }.join(',')})"
  p results = client.query(query)
  p client.affected_rows
}
  • bundle exec ruby insert.rb

で結果は以下の通りです

mysql> select * from user;
+----+------+------+--------+--------+
| id | name | age  | height | weight |
+----+------+------+--------+--------+
|  1 | pika |   17 |     80 |     50 |
|  2 | kai  |   52 |    180 |    120 |
|  3 | yado |   34 |    120 |    150 |
|  4 | pijo |   23 |     90 |     60 |
|  5 | koda |    2 |     70 |     65 |
|  6 | kora |   10 |     35 |     30 |
+----+------+------+--------+--------+
6 rows in set (0.00 sec)

insert 時の結果は何も返ってこないようです
もし insert できた行数が必要な場合はクライアントオブジェクトの affected_rows を参照すると追加したレコードの行数が返ってきます

ちなみに今回は id に PRIMARY_KEY を張っているので同じスクリプトを 2 回流すと Duplicate entry '1' for key 'PRIMARY' (Mysql2::Error) のエラーが発生します

データ取得

  • vim select.rb
require 'mysql2'

client = Mysql2::Client.new(:host => "localhost", :username => "root", :database => 'test')

query = "select * from user"
p results = client.query(query)

statement = client.prepare("select * from user where height >= ?")
conditions = [10, 100, 200] 
conditions.each { |height|
  puts height
  results = statement.execute(height)
  results.each { |row|
    puts row.map { |k, v| "#{k} => #{v}" }.join(', ')
  }
}
  • bundle exec ruby select.rb
10
id => 1, name => pika, age => 17, height => 80, weight => 50
id => 2, name => kai, age => 52, height => 180, weight => 120
id => 3, name => yado, age => 34, height => 120, weight => 150
id => 4, name => pijo, age => 23, height => 90, weight => 60
id => 5, name => koda, age => 2, height => 70, weight => 65
id => 6, name => kora, age => 10, height => 35, weight => 30
100
id => 2, name => kai, age => 52, height => 180, weight => 120
id => 3, name => yado, age => 34, height => 120, weight => 150
200

PreparedStatement も使えます

results は Mysql2::Result クラスのオブジェクトです
中身を確認する場合は基本これを each などします
クエリ自体の状態を確認したい場合は query_was_slow などのプロパティを参照することで確認できます

データ更新

  • vim update.rb
require 'mysql2'

client = Mysql2::Client.new(:host => "localhost", :username => "root", :database => 'test')
target_users = [
  [1, 'raichu'],
  [6, 'ratta'],
]
target_users.each { |user|
  results = client.query("update user set name = '#{user[1]}' where id = #{user[0]}")
  p results
  p client.affected_rows
}

insert の時と挙動は同じです

データ削除

  • vim delete.rb
require 'mysql2'

client = Mysql2::Client.new(:host => "localhost", :username => "root", :database => 'test')
ids = [2, 3]
ids.each { |id|
  results = client.query("delete from user where id = #{id}")
  p results
  p client.affected_rows
}

こんな感じです

最後に

mysql2 を使って Ruby から MySQL の簡単な操作をしてみました
基本は文字列でクエリを作って実行するだけなので簡単にできます

トランザクションの機能はないっぽいので自分で実装する必要があります
https://stackoverflow.com/questions/9288593/how-do-i-perform-transactions-with-ruby-mysql2?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

また active_record のようにモデルを自動生成やマイグレート機能があるわけではないので、その辺も必要な場合は自分で実装する必要があります
とりあえずサクッと MySQL に問い合わせたいときに使う感じかなと思います

2018年4月6日金曜日

Docker + Nodejs で JST の時刻を取得する

概要

DockerHub で公開されている nodejs の公式イメージを使うとタイムゾーンは UTC になっています
イメージ自体のタイムゾーンを JST にしても良いですが nodejs 上変換することもできるようなので紹介します

環境

  • macOS 10.13.2
  • nodejs 8.7.0
  • docker 18.03.0-ce, build 0520e24

準備

  • npm init
  • npm install moment-timezone --save

npm test でメインのスクリプトを実行できるようにしておきます

  • vim package.json
{
  "name": "jst",
  "version": "1.0.0",
  "description": "",
  "main": "date.js",
  "scripts": {
    "test": "node date.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "moment-timezone": "^0.5.14"
  }
}

サンプルコード

今回は普通の Date クラスと moment-timezone を使った場合の時刻の違いを見ていきます

  • vim date.js
d = new Date();
console.log(d.getHours());

var moment = require('moment-timezone');
today = moment().tz("Asia/Tokyo");
console.log(today.get('hours'));

こんな感じです
ローカルの Mac はタイムゾーンが JST なので実行すると同じ時刻が表示されると思います

Docker 上で実行してみる

サンプルコードを Docker 上で実行してみましょう

  • vim Dockerfile
FROM node

ADD . /home
WORKDIR /home
RUN npm install

CMD ["npm", "test"]
  • docker build -t date_test .
  • docker run --rm date_test

で実行すると

0
9

という感じで 9 時間差があることがわかると思います

最後に

タイムゾーンを気にして時刻を扱う場合は moment-timezone というライブルを使うのが定石っぽいです

2018年4月5日木曜日

OpenAI Gym で強化学習に入門する

概要

OpenAI Gym は強化学習をゲームなどを使って学ぶことができる学習ツールです
今回は入門ということで「CartPole」問題に挑戦してみました
実際にコーディングしつつ変数などの値をデバッグすることで理解を深めていきたいと思います

環境

  • macOS 10.13.2
  • Python 3.6.4
  • pip 9.0.3
  • OpenAI Gym 0.10.4

インストール

  • sudo pip3 install gym

とりあえず CartPole を動作させる

CartPole 問題はカート上に置かれた棒を左右に動かしていかに長く棒を倒さないかを求めるゲームです
GUI を使って動作させることができるのでとりあえず動かせる状況を作ります

  • vim test-gym.py
import gym

env = gym.make('CartPole-v0')

for i_episode in range(20):
    observation = env.reset()
    for t in range(100):
        env.render()
        print(observation)
        action = env.action_space.sample()
        print(env.action_space)
        print(env.observation_space)
        print(env.observation_space.high)
        print(env.observation_space.low)
        observation, reward, done, info = env.step(action)
        print(reward)
        if done:
            print("Episode finished after {} timesteps".format(t+1))
            break

これでとりあえず実行してみると CartPole の GUI が表示されてある程度すると終了すると思います
やっていることとしては 20 エピソードテストしてそのテスト内で 100 ステップカートを動かして 100 ステップ完走すれば成功で 100 ステップ完走するまえに if done に行ってしまうと失敗です

実行してみるとわかりますがだいたい 20 - 30 ステップくらいで全エピソードが done してしまっているのがわかると思います

詳細説明

ところどころ print 文をあえて入れています
import 文のところは特に問題ないと思います

env = gym.make('CartPole-v0') ですがこれで CartPole 問題の環境を呼び出しています
OpenAI Gym ではこれ以外にも様々な環境が用意されており、それぞれで強化学習を学ぶためのデータやアクションがすでに実装されています
OpenAI Gym ではそれらのデータやアクションを使って自分なりのロジックを少し付け加えることで強化学習を簡単に学べる仕組みになっています

for i_episode in range(20): エピソードを 20 回実行します
要するにテストする回数です
20 でなくても問題ないですがとりあえず 20 にします

observation = env.reset() これで CartPole 問題をスタートします
返り値として observation が返ってきます
これが所謂「状態」を表す変数となっており CartPole 問題の場合は 4 つの変数が入った配列になっています
状態を表す変数はそれぞれ

  • カート位置 -2.4~2.4
  • カート速度 -3.0~3.0
  • 棒の角度 -41.8~41.8
  • 棒の角速度 -2.0~2.0

となっており reset 直後はスタート直後の棒とカートの状態を取得することができます
print 文をしているので実際に値を確認してみると良いと思います

for t in range(100): はエピソード内で 100 ステップ実行しろという命令になります
冒頭説明しましたがこの 100 ステップの間棒が倒れなければクリアになります

env.render() これは observation に応じて GUI をレンダリングしているだけです

action = env.action_space.sample() で次に取るアクションを決めます
まず env.action_space を見てみると Discrete クラスのオブジェクトだということがわかります
値は 2 となっており要するにカートが取ることができるアクションの種類を管理しています
今回であれば 1 -> 右にカートを動かす, 0 -> 左にカートを動かすの 2 種類を選択することができます
そしてそれに対して .sample() することで 1 or 0 のどちらかをランダムで取得するということをしています
ようするに env.action_space.sample() はランダムでカートを動かすという命令になります

次の行動が決まったらそれを実行するだけです
observation, reward, done, info = env.step(action) で action を引数にすることで実行できます
実行後は 4 つの値が返ってきます
それぞれ

  • observation・・・行動後の状態
  • reward・・・行動によって得られた報酬
  • done・・・エピソードが終了したかどうか (true なら終了、ゲーム終わり)
  • info・・・詳細情報

になります
reward を print していますが中身を見てみると毎回 1.0 が得られていることがわかると思います
action 後もし棒が倒れていないのであれば 1.0 sec の報酬が得られるようです

if done: はエピソードの終了処理になります
内容は何ステップ実行できたかを print しているだけです

説明は以上になります
要するにサンプルはカートを左右ランダムに動かしているだけになります
当たり前ですが何も考えずランダムにカートを動かしているだけではすぐに棒は倒れてしまいます
では何をしなければ行けないかというと observation (状態) に応じて次に取るべき action (行動) を決めなければいけません

observation に応じて action を決定するロジックを追加する

棒がもっと長く立っていられるように変更してみます

  • vim test_gym2.py
import gym
import numpy

env = gym.make('CartPole-v0')
loop = 100

for i_episode in range(20):
    observation = env.reset()
    for t in range(loop):
        env.render()
        sum = numpy.sum(observation)
        action = 0
        if sum > 0:
            action = 1
        observation, reward, done, info = env.step(action)
        if done:
            print("Episode finished after {} timesteps with done".format(t + 1))
            break
        elif t == loop - 1:
            print("Episode finished after {} timesteps with count".format(t + 1))

ポイントは

sum = numpy.sum(observation)
action = 0
if sum > 0:
    action = 1

の部分です
observation の 4 つの値の合計を取りその値が 0 以上であった場合にカートを右に動かします
そうでない場合は左にカードを動かします
要するに棒が左に倒れそうなときはカートを右に動かし、右に倒れそうなときはカートを左に動かすということをしています

また、このロジックを追加することで棒がかなり倒れづらくなるので終了条件にステップが 100 回に達した場合も終了するようにしました
これで実行してみましょう

  • python3 test_gym2.py
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 99 timesteps with done
Episode finished after 100 timesteps with count
Episode finished after 77 timesteps with done
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 78 timesteps with done
Episode finished after 76 timesteps with done
Episode finished after 97 timesteps with done
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count
Episode finished after 100 timesteps with count

するとほぼすべてのエピソードで 100 ステップ達成していることがわかると思います

今回は単純に observation の合計を見ただけです
更に倒れないようなロジックを組むには合計ではなく observation それぞれの値を確認して action を決定する必要があります
所謂 Q-learning を実装する必要があります
Q-Table などを作成し行動に応じて報酬が最大となるような表を作成するなどの実装が必要になります

最後に

OpenAI Gym で手を動かしながら強化学習に入門してみました
強化学習のポイントとなる要素「状態」「行動」「報酬」の仕組みが少しは理解できたと思います

強化学習は教師あり学習や教師なし学習に比べて応用的な分野になるので理解することも多いです
使いこなせるようになればかなり便利だとは思いますが思いついて、それを実装できるレベルになるまでには結構な学習コストが必要だと思います
OpenAI Gym に関してはサンプルコードも豊富なのでとりあえず他に人の学習方法を真似て実装してみるのは良いかもしれません

それぞれの環境における observation や action の種類に関しては Github の Wiki に記載されているのでそちらを確認すると良いと思います

参考サイト

2018年4月4日水曜日

Ruby で SVM を試してみました

概要

前回ナイブベイズをやってみました
今回は SVM を試してみます
SVM も分類器ですがナイブベイズとは異なりマージン最大化を取り入れた境界線を引くことで分類を行います

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • rb-libsvm 1.4.0
  • libsvm

インストール

  • brew install libsvm
  • bundle init
  • vim Gemfile
gem "rb-libsvm"
gem "libsvmloader"
  • bundle install --path vendor

学習データの取得

  • wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/pendigits
  • wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/pendigits.t

今回のデータは分類するラベル数が 0 から 9 までの 10 個、学習させるデータの次元数が 16 になっていました

学習させるサンプルコード

  • vim learn.rb
require 'libsvmloader'
require 'libsvm'

class Model
  def self.gen
    train_data, train_data_labels = LibSVMLoader.load_libsvm_file('./pendigits')
    train_data_ary = train_data.each_row.map { |v| Libsvm::Node.features(v.to_a) }
    train_data_labels_ary = train_data_labels.to_flat_a

    params = Libsvm::SvmParameter.new.tap do |p|
      p.cache_size = 1000
      p.svm_type = Libsvm::SvmType::C_SVC
      p.kernel_type = Libsvm::KernelType::RBF
      p.gamma = 0.0001
      p.c = 1.0
      p.eps = 0.001
    end

    problem = Libsvm::Problem.new
    problem.set_examples(train_data_labels_ary, train_data_ary)
    model = Libsvm::Model.train(problem, params)
  end
end

ダウンロードしたデータはすでに SVM で学習できる形式になっています
その場合には LibSVMLoader.load_libsvm_file という便利なメソッドがあります
データ (train_data) とラベル (train_data_labels) は NMatrix クラスのオブジェクトです
データは Libsvm::Node.features クラスのオブジェクトに変換します
ラベルは to_flat_a という関数をコールして NMatrix から Array に変換します

次に学習時のパラメータの設定です
とりあえずこのまま学習させましょう
一応ここがチューニングポイントにはなっているので精度を上げる場合にはこの辺の値を調整します

あとは Libsvm::Problem にテストデータと正解のラベルデータを食わせてパラメータと一緒に学習させればモデルができあがります

テストコード

では先ほど作成したモデルを使ってテストデータを食わせてみましょう

vim test.rb

require './learn.rb'

model = Model.gen

test_data, test_data_labels = LibSVMLoader.load_libsvm_file('./pendigits.t')
# これでテストの実施をしてる
preds = test_data.each_row.map { |v| model.predict(Libsvm::Node.features(v.to_a)) }

ok = 0
preds.each.with_index { |label, index|
  if label == test_data_labels[index]
    ok = ok + 1 
  end
}

# puts preds.count
# puts ok

puts 100.0 * (ok / preds.count.to_f)

テストデータを読み込んだら model.predict でテストさせます
テストデータは学習時同様に Libsvm::Node.features で変換する必要があります
結果は preds という配列のデータで返ってきます
単純にデータに対してモデルが判断したラベルが入っているだけです

なのであとは each で回してそれぞれのラベルが正しかったかどうかを判断します
今回は正しければ ok のカウントを増やしています
あとは全体のデータのうちどれだけ ok があるかを割り算すれば正答率が出せます

今回はサンプルデータを使ったので 98.28473413379074 という値が出ればコードがうまく動作していることになります

最後に

Ruby で SVM を試してみました
SVM はデータさえ揃えばサクっと試せてかつ精度の良い分類器が作れる手法かなと思います
パラメータ部分の説明は今回省略しましたが SVM の肝でもあるのでちゃんと理解しておいたほうが良いかと思います
(RBF カーネル、C、gamma あたりでググるといわいろと出てきます)

また評価の尺度は他にも Precision と Recall, F値 などがあります
SVM には CrossValidation という手法があり全体のデータの一部を学習データ、一部をテストデータとして学習させることができます
これを使えば上記の尺度も出すことができます
CrossValidation も今回のライブラリを使ってできるので興味があれば以下を参考にしてください
https://github.com/febeling/rb-libsvm/blob/master/examples/iris.rb

参考サイト

2018年4月3日火曜日

Ruby で重回帰分析をしてみた

概要

重回帰分析は複数の変数を与えたときにある 1 つの Y が得られるときの関数を求めることができる分析手法です
Ruby で簡単に使えるライブラリがあるので紹介します

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • statsample 2.1.0

ライブラリインストール

  • bundle init
  • vim Gemfile
gem "statsample"
gem "nokogiri"

今回適当な Web 上のデータをスクレーピングするので nokogiri はそのためにインストールしています

学習用サンプルコード

全体としてはこんな感じです

  • learn.rb
# coding: utf-8
require 'statsample'
require 'nokogiri'

charset = 'utf-8'
html = open('http://npb.jp/bis/2017/stats/bat_c.html') do |f|
  f.read
end

doc = Nokogiri::HTML.parse(html, nil, charset)
data = []
doc.xpath('//div[@id="stdivmaintbl"]').each do |node|
  node.css('tr').each { |tr|
    next if tr.css('td').count <= 1
    d = []
    tr.css('td').each { |td|
      d.push(td.text.to_f)
    }
    data.push d
  }
end

h = {}
h['rate'] = data.map {|i| i[3]}
h['game'] = data.map {|i| i[4]}
h['plate'] = data.map {|i| i[5]}
h['bat'] = data.map {|i| i[6]}
h['run'] = data.map {|i| i[7]}
h['hit'] = data.map {|i| i[8]}
h['double'] = data.map {|i| i[9]}
h['triple'] = data.map {|i| i[10]}
h['shot'] = data.map {|i| i[11]}
h['total'] = data.map {|i| i[12]}
h['rbi'] = data.map {|i| i[13]}
h['steal'] = data.map {|i| i[14]}
h['steal_out'] = data.map {|i| i[15]}
h['bunt'] = data.map {|i| i[16]}
h['fly'] = data.map {|i| i[17]} 
h['walk'] = data.map {|i| i[18]}
h['intentional_walk'] = data.map {|i| i[19]}
# h['hit_by_pitch'] = data.map {|i| i[20]}
h['k'] = data.map {|i| i[21]}
h['double_play'] = data.map {|i| i[22]}
h['extra_hit_rate'] = data.map {|i| i[23]}
h['on_base_rate'] = data.map {|i| i[24]}

lr = Statsample::Regression.multiple(h.to_dataset, 'rate')
puts lr.summary

今回はプロ野球セリーグの打者データ上位 28 名のデータを学習データに使っています (前半部分)
出力したい情報を「打率 (rate)」とし、それ以外のデータを入力変数として使っています
打率なので単純な計算でも出せますがあえて学習させています

data に選手ごとの各打者成績が入っているので、それを項目ごとに変数に格納し直しています

h が最終的な学習させるデータでハッシュ形式でデータの部分を配列として定義します
そして Statsample::Regression.multiple に食わせる際に to_dataset をコールすることで学習データに変形します
第二引数の rate は算出したい値の項目になるので今回は打率を指定します

これで実行すると以下のような出力が得られます

  • bundle exec ruby learn
= Multiple reggresion of bat,bunt,double,double_play,extra_hit_rate,fly,game,hit,intentional_walk,k,on_base_rate,plate,rbi,run,shot,steal,steal_out,triple,walk on rate
  Engine: Statsample::Regression::Multiple::RubyEngine
  Cases(listwise)=28(28)
  R=1.000
  R^2=0.999
  R^2 Adj=0.998
  Std.Error R=0.001
  Equation=-0.039 + 0.001bat + 0.001bunt + -0.000double + -0.000double_play + 0.025extra_hit_rate + 0.001fly + 0.000game + 0.001hit + 0.000intentional_walk + 0.000k + 0.833on_base_rate + -0.001plate + 0.000rbi + 0.000run + -0.000shot + 0.000steal + -0.000steal_out + -0.001triple + -0.000walk
  == ANOVA
    ANOVA Table
+------------+-------+----+-------+---------+-------+
|   source   |  ss   | df |  ms   |    f    |   p   |
+------------+-------+----+-------+---------+-------+
| Regression | 0.020 | 19 | 0.001 | 606.688 | 0.000 |
| Error      | 0.000 | 8  | 0.000 |         |       |
| Total      | 0.020 | 27 | 0.001 |         |       |
+------------+-------+----+-------+---------+-------+

  Beta coefficients
+------------------+--------+--------+-------+--------+
|      coeff       |   b    |  beta  |  se   |   t    |
+------------------+--------+--------+-------+--------+
| Constant         | -0.039 | -      | 0.043 | -0.912 |
| bat              | 0.001  | 1.292  | 0.000 | 3.279  |
| bunt             | 0.001  | 0.177  | 0.000 | 4.850  |
| double           | -0.000 | -0.074 | 0.000 | -2.086 |
| double_play      | -0.000 | -0.009 | 0.000 | -0.551 |
| extra_hit_rate   | 0.025  | 0.067  | 0.057 | 0.438  |
| fly              | 0.001  | 0.064  | 0.000 | 2.394  |
| game             | 0.000  | 0.052  | 0.000 | 1.599  |
| hit              | 0.001  | 0.534  | 0.000 | 3.037  |
| intentional_walk | 0.000  | 0.004  | 0.000 | 0.192  |
| k                | 0.000  | 0.058  | 0.000 | 1.508  |
| on_base_rate     | 0.833  | 1.031  | 0.134 | 6.241  |
| plate            | -0.001 | -1.630 | 0.000 | -5.231 |
| rbi              | 0.000  | 0.067  | 0.000 | 1.577  |
| run              | 0.000  | 0.042  | 0.000 | 1.101  |
| shot             | -0.000 | -0.151 | 0.000 | -1.202 |
| steal            | 0.000  | 0.017  | 0.000 | 0.561  |
| steal_out        | -0.000 | -0.016 | 0.000 | -0.445 |
| triple           | -0.001 | -0.039 | 0.000 | -1.814 |
| walk             | -0.000 | -0.256 | 0.000 | -2.547 |
+------------------+--------+--------+-------+--------+

大事なのは Equation で得られた式になります
この式に当てはめると打率を機械学習的に求めることができます

また表を見ると b の欄で各変数の係数がわかります
ここで 0.000 になっている項目は計算に不必要な項目になるので例えば rbi (打点) は機械学習的には打率を算出するのには不必要な項目だと判断されています

Tips

今回「塁打 (total)」と「死球 (hit_by_pitch)」は学習に含められませんでした
含めようとすると以下のエラーとなり学習できません

Regressors are linearly dependent (Statsample::Regression::LinearDependency)

どうやら重回帰分析には学習できるデータに条件があるようです (詳細は不明)
Github に Issue があがっており「R でも同じデータでエラーになるよ、たぶんデータが悪い」と指摘がされていました
https://github.com/clbustos/statsample/issues/2

テスト用サンプルコード

では先ほどの式を使ってテストしてみましょう
テストデータはパリーグの選手の打席データを使ってみたいと思います

  • vim test.rb
# coding: utf-8
# 秋山翔吾 -> 0.322
bat = 575
bunt = 0
extra_hit_rate = 0.536
fly = 7
hit = 185
on_base_rate = 0.398
plate = 659
triple = 5
rate =- 0.039 + (0.001 * bat) + (0.001 * bunt) + (0.025 * extra_hit_rate) + (0.001 * fly) + (0.001 * hit) + (0.833 * on_base_rate) + (-0.001 * plate) + (-0.001 * triple)
puts rate

# 柳田悠岐 -> 0.310
bat = 448
bunt = 0
extra_hit_rate = 0.589
fly = 7
hit = 139
on_base_rate = 0.426
plate = 551
triple = 1
rate =- 0.039 + (0.001 * bat) + (0.001 * bunt) + (0.025 * extra_hit_rate) + (0.001 * fly) + (0.001 * hit) + (0.833 * on_base_rate) + (-0.001 * plate) + (-0.001 * triple)
puts rate

# 茂木栄五郎 -> 0.296
bat = 398
bunt = 4
extra_hit_rate = 0.497
fly = 1
hit = 118
on_base_rate = 0.370
plate = 450
triple = 2
rate =- 0.039 + (0.001 * bat) + (0.001 * bunt) + (0.025 * extra_hit_rate) + (0.001 * fly) + (0.001 * hit) + (0.833 * on_base_rate) + (-0.001 * plate) + (-0.001 * triple)
puts rate

# 西川遥輝 -> 0.296
bat = 541
bunt = 6
extra_hit_rate = 0.416
fly = 3
hit = 160
on_base_rate = 0.378
plate = 623
triple = 6
rate =- 0.039 + (0.001 * bat) + (0.001 * bunt) + (0.025 * extra_hit_rate) + (0.001 * fly) + (0.001 * hit) + (0.833 * on_base_rate) + (-0.001 * plate) + (-0.001 * triple)
puts rate

不要と判断させた項目は削除しています
これで算出すると上から

  • 秋山翔吾 -> 0.408934
  • 柳田悠岐 -> 0.3725829999999999
  • 茂木栄五郎 -> 0.350635
  • 西川遥輝 -> 0.367274

という感じになりました
今回算出できた式は成績が良い方向に計算されるようになったようです
もしくはパリーグの選手がもしセリーグで出場したら上記のような成績が残せたのかもしれません (たぶんそれはないが)

最後に

Ruby で重回帰分析を行ってみました
学習データが 28 件しかないのでかなり結果にズレが出ましたが、それっぽいモデルは作れました
使い方はこれでマスタできたかなと思います

あとはデータを増やして再度学習させればかなり近い値が出せるようになるかなと思います
今回は「打率」を算出するモデルを作りましたが打率は「打数」と「安打数」の計算で求められてしまうので本来であれば入力と出力は少し工夫したほうが良かったかもしれません

重回帰分析は「予測」や「推測」をするための手法だと思います
既存のデータからある規則 (式) を導き出し、今後起こり得る状況 (変数) に当てはめることで正解を探す方法かなと思います

何か予測したいデータがある場合に使ってみてはいかがでしょうか

2018年4月2日月曜日

Ruby でサクっとナイブベイズ

概要

ナイブベイズはベイズ定理が基になっている単純な分類器です
確率分布を出した上でどれになるか確率が一番高いところを正として判断する感じです
もう既にライブラリになっているのでそれを使います
https://github.com/oasic/nbayes

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • nbayes 1.15.4

準備

  • bundle init
  • vim Gemfile
gem "nbayes"

サンプルコード

例えば 2 教科の点数がそれぞれ 70 点と 80 点以上なら合格とデータを学習させます

  • vim test1.rb
require 'nbayes'

nbayes = NBayes::Base.new

# >= 70 and >= 80 -> OK
nbayes.train([70, 80], 'OK')
nbayes.train([75, 90], 'OK')
nbayes.train([80, 80], 'OK')
nbayes.train([90, 100], 'OK')
nbayes.train([100, 80], 'OK')
nbayes.train([72, 82], 'OK')
nbayes.train([71, 93], 'OK')
nbayes.train([89, 80], 'OK')
nbayes.train([70, 99], 'OK')
nbayes.train([80, 95], 'OK')
nbayes.train([30, 50], 'NG')
nbayes.train([0, 80], 'NG')
nbayes.train([100, 10], 'NG')
nbayes.train([20, 60], 'NG')
nbayes.train([10, 80], 'NG')
nbayes.train([50, 70], 'NG')
nbayes.train([13, 21], 'NG')
nbayes.train([69, 55], 'NG')
nbayes.train([100, 79], 'NG')
nbayes.train([69, 98], 'NG')

test1 = [0, 100] # NG
test2 = [90, 98] # OK
test3 = [69, 79] # NG
test4 = [71, 81] # OK
test5 = [99, 2]  # NG
puts nbayes.classify(test1).max_class
puts nbayes.classify(test2).max_class
puts nbayes.classify(test3).max_class
puts nbayes.classify(test4).max_class
puts nbayes.classify(test5).max_class
  • bundle exec ruby test1.rb

結果は以下の通り

NG
OK
NG
OK
OK

なんと 2 教科目が 2 点の子が OK になってしまっています
これは良くないということで再度学習させます
以下の行を追加しましょう

nbayes.train([99, 2], 'NG')

これで再度実行するとちゃんと最後も NG になると思います

最後に

Ruby で内部ベイズをサクッと試してみました
アプリケーションエンジニアとしてはこれだけ簡単に使えるようになっているとかなり嬉しいと思います
今回のデータは if 文でも簡単に書けるので機械学習させる必要は全く無いと思います
例えば今回のように「2」教科しか次元がないデータであれば if 文で問題ないですが 1000 やら 10000 次元やらデータがある場合には有効です

またナイブベイズは分類器になり代表的な使われ方にはスパム判定があります
今回は OK or NG の 2 値で行いましたが、複数の多値判定もできます

これだけサクッと使えるのであれば何かデータができたらとりあえず投げてみるのはありかなーと思うので使い方を覚えておいて損はないかと思います
ただ、中身のアルゴリズムはさっぱりなので勉強する必要はあるかと思いますが、まずは試せる環境があるっていうのは勉強するきっかけにもなり良いことかなと思います
とりあえず試して精度が良い学習方法を深く学習していくという方法もありだと思います

参考サイト